From fef7d8d08b333b7ec9b5091c3c3d510ea1656ec1 Mon Sep 17 00:00:00 2001 From: arnav-makkar Date: Fri, 13 Feb 2026 15:46:43 +0530 Subject: [PATCH 1/4] modules updated --- .../php/acknowledgementrow.class.inc | 5 ++++ .../api/php/models/candidatesrow.class.inc | 8 +++++++ .../api/php/models/projectimagesrow.class.inc | 8 +++++++ .../models/projectinstrumentsrow.class.inc | 5 ++++ .../php/models/projectrecordingsrow.class.inc | 8 +++++++ modules/api/php/models/sitesrow.class.inc | 8 +++++++ modules/battery_manager/php/test.class.inc | 8 +++++++ .../php/models/behaviouraldto.class.inc | 8 +++++++ .../php/models/conflictsdto.class.inc | 8 +++++++ .../php/models/incompletedto.class.inc | 8 +++++++ modules/biobank/php/container.class.inc | 8 +++++++ modules/biobank/php/log.class.inc | 9 ++++++- modules/biobank/php/pool.class.inc | 9 ++++++- modules/biobank/php/shipment.class.inc | 9 ++++++- modules/biobank/php/specimen.class.inc | 8 +++++++ .../php/candidatelistrow.class.inc | 8 +++++++ .../php/models/resolveddto.class.inc | 8 +++++++ .../php/models/unresolveddto.class.inc | 8 +++++++ .../data_release/php/datareleaserow.class.inc | 5 ++++ modules/datadict/php/datadictrow.class.inc | 6 +++++ .../dicomarchiverowwithoutsession.class.inc | 10 ++++++++ .../php/dicomarchiverowwithsession.class.inc | 8 +++++++ .../php/docreporow.class.inc | 8 +++++++ .../php/electrophysiologybrowserrow.class.inc | 8 +++++++ .../php/models/electrophysiofile.class.inc | 24 +++++++++++++++++++ .../electrophysiologyuploaderrow.class.inc | 8 +++++++ modules/examiner/php/examinerrow.class.inc | 8 +++++++ .../php/models/cnvdto.class.inc | 8 +++++++ .../php/models/filesdto.class.inc | 10 ++++++++ .../php/models/gwasdto.class.inc | 10 ++++++++ .../php/models/methylationdto.class.inc | 8 +++++++ .../php/models/profiledto.class.inc | 8 +++++++ .../php/models/snpdto.class.inc | 8 +++++++ modules/help_editor/php/helprow.class.inc | 5 ++++ .../php/imagingbrowserrow.class.inc | 8 +++++++ .../php/instrumentrow.class.inc | 5 ++++ modules/issue_tracker/php/issuerow.class.inc | 8 +++++++ .../php/models/attachmentdto.class.inc | 11 +++++++++ modules/media/php/mediafile.class.inc | 8 +++++++ .../module_manager/php/modulerow.class.inc | 5 ++++ .../mri_violations/php/mriviolation.class.inc | 19 +++++++++++++++ .../php/protocolcheckviolation.class.inc | 10 ++++++++ .../php/protocolviolation.class.inc | 10 ++++++++ .../schedule_module/php/schedulerow.class.inc | 8 +++++++ .../php/surveyaccountsrow.class.inc | 8 +++++++ .../php/useraccountrow.class.inc | 8 +++++++ 46 files changed, 388 insertions(+), 3 deletions(-) diff --git a/modules/acknowledgements/php/acknowledgementrow.class.inc b/modules/acknowledgements/php/acknowledgementrow.class.inc index ad51c9039e3..52e9cf5f55b 100644 --- a/modules/acknowledgements/php/acknowledgementrow.class.inc +++ b/modules/acknowledgements/php/acknowledgementrow.class.inc @@ -49,4 +49,9 @@ class AcknowledgementRow implements \LORIS\Data\DataInstance { return $this->DBRow; } + + public function isAccessibleBy(\User $user): bool + { + return true; + } } diff --git a/modules/api/php/models/candidatesrow.class.inc b/modules/api/php/models/candidatesrow.class.inc index 28997721e13..cd94ec708c9 100644 --- a/modules/api/php/models/candidatesrow.class.inc +++ b/modules/api/php/models/candidatesrow.class.inc @@ -101,4 +101,12 @@ class CandidatesRow implements \LORIS\Data\DataInstance, } return $this->_projectid; } + + public function isAccessibleBy(\User $user): bool + { + return \LORIS\StudyEntities\DataInstanceAccess::centerAndProjectMatch( + $user, + $this + ); + } } diff --git a/modules/api/php/models/projectimagesrow.class.inc b/modules/api/php/models/projectimagesrow.class.inc index c052e6f15b1..87578b673fd 100644 --- a/modules/api/php/models/projectimagesrow.class.inc +++ b/modules/api/php/models/projectimagesrow.class.inc @@ -106,4 +106,12 @@ class ProjectImagesRow implements \LORIS\Data\DataInstance, { return $this->_entitytype === 'Scanner'; } + + public function isAccessibleBy(\User $user): bool + { + return \LORIS\StudyEntities\DataInstanceAccess::centerMatch( + $user, + $this + ); + } } diff --git a/modules/api/php/models/projectinstrumentsrow.class.inc b/modules/api/php/models/projectinstrumentsrow.class.inc index 9bbd429e22a..bf407fa66ec 100644 --- a/modules/api/php/models/projectinstrumentsrow.class.inc +++ b/modules/api/php/models/projectinstrumentsrow.class.inc @@ -119,4 +119,9 @@ class ProjectInstrumentsRow implements \LORIS\Data\DataInstance } return $obj; } + + public function isAccessibleBy(\User $user): bool + { + return true; + } } diff --git a/modules/api/php/models/projectrecordingsrow.class.inc b/modules/api/php/models/projectrecordingsrow.class.inc index 667eb0984bb..468dd95977b 100644 --- a/modules/api/php/models/projectrecordingsrow.class.inc +++ b/modules/api/php/models/projectrecordingsrow.class.inc @@ -91,4 +91,12 @@ class ProjectRecordingsRow implements \LORIS\Data\DataInstance { return intval($this->_centerid); } + + public function isAccessibleBy(\User $user): bool + { + return \LORIS\StudyEntities\DataInstanceAccess::centerMatch( + $user, + $this + ); + } } diff --git a/modules/api/php/models/sitesrow.class.inc b/modules/api/php/models/sitesrow.class.inc index f83c78131d0..5b8dc928ce4 100644 --- a/modules/api/php/models/sitesrow.class.inc +++ b/modules/api/php/models/sitesrow.class.inc @@ -56,4 +56,12 @@ class SitesRow implements DataInstance, SiteHaver { return \CenterID::singleton($this->_id); } + + public function isAccessibleBy(\User $user): bool + { + return \LORIS\StudyEntities\DataInstanceAccess::centerMatch( + $user, + $this + ); + } } diff --git a/modules/battery_manager/php/test.class.inc b/modules/battery_manager/php/test.class.inc index 047351f02e8..4bb936205b4 100644 --- a/modules/battery_manager/php/test.class.inc +++ b/modules/battery_manager/php/test.class.inc @@ -105,4 +105,12 @@ class Test implements 'Active' => $this->row['active'] ?? null, ]; } + + public function isAccessibleBy(\User $user): bool + { + return \LORIS\StudyEntities\DataInstanceAccess::centerMatch( + $user, + $this + ); + } } diff --git a/modules/behavioural_qc/php/models/behaviouraldto.class.inc b/modules/behavioural_qc/php/models/behaviouraldto.class.inc index 4f0314c5d6d..a2e3f8e0f90 100644 --- a/modules/behavioural_qc/php/models/behaviouraldto.class.inc +++ b/modules/behavioural_qc/php/models/behaviouraldto.class.inc @@ -164,4 +164,12 @@ class BehaviouralDTO implements \LORIS\Data\DataInstance, 'feedback_status' => $this->_feedback_status, ]; } + + public function isAccessibleBy(\User $user): bool + { + return \LORIS\StudyEntities\DataInstanceAccess::centerAndProjectMatch( + $user, + $this + ); + } } diff --git a/modules/behavioural_qc/php/models/conflictsdto.class.inc b/modules/behavioural_qc/php/models/conflictsdto.class.inc index fa5a28c7593..8301ce1d99d 100644 --- a/modules/behavioural_qc/php/models/conflictsdto.class.inc +++ b/modules/behavioural_qc/php/models/conflictsdto.class.inc @@ -148,4 +148,12 @@ class ConflictsDTO implements \LORIS\Data\DataInstance, 'commentID' => $this->_commentID, ]; } + + public function isAccessibleBy(\User $user): bool + { + return \LORIS\StudyEntities\DataInstanceAccess::centerAndProjectMatch( + $user, + $this + ); + } } diff --git a/modules/behavioural_qc/php/models/incompletedto.class.inc b/modules/behavioural_qc/php/models/incompletedto.class.inc index 81153d9a414..d23161c170a 100644 --- a/modules/behavioural_qc/php/models/incompletedto.class.inc +++ b/modules/behavioural_qc/php/models/incompletedto.class.inc @@ -156,4 +156,12 @@ class IncompleteDTO implements \LORIS\Data\DataInstance, 'commentID' => $this->_commentID, ]; } + + public function isAccessibleBy(\User $user): bool + { + return \LORIS\StudyEntities\DataInstanceAccess::centerAndProjectMatch( + $user, + $this + ); + } } diff --git a/modules/biobank/php/container.class.inc b/modules/biobank/php/container.class.inc index f1004114267..13ab6891071 100644 --- a/modules/biobank/php/container.class.inc +++ b/modules/biobank/php/container.class.inc @@ -558,4 +558,12 @@ class Container implements { return json_encode($this); } + + public function isAccessibleBy(\User $user): bool + { + return \LORIS\StudyEntities\DataInstanceAccess::centerMatch( + $user, + $this + ); + } } diff --git a/modules/biobank/php/log.class.inc b/modules/biobank/php/log.class.inc index a848cd61502..ff689bcb0b7 100644 --- a/modules/biobank/php/log.class.inc +++ b/modules/biobank/php/log.class.inc @@ -191,5 +191,12 @@ class Log implements \JsonSerializable, \LORIS\Data\DataInstance { return json_encode($this); } -} + public function isAccessibleBy(\User $user): bool + { + return \LORIS\StudyEntities\DataInstanceAccess::centerMatch( + $user, + $this + ); + } +} diff --git a/modules/biobank/php/pool.class.inc b/modules/biobank/php/pool.class.inc index aa45ba6947d..24bb38afd84 100644 --- a/modules/biobank/php/pool.class.inc +++ b/modules/biobank/php/pool.class.inc @@ -497,5 +497,12 @@ class Pool implements { return json_encode($this); } -} + public function isAccessibleBy(\User $user): bool + { + return \LORIS\StudyEntities\DataInstanceAccess::centerAndProjectMatch( + $user, + $this + ); + } +} diff --git a/modules/biobank/php/shipment.class.inc b/modules/biobank/php/shipment.class.inc index a0f2c5d8e4f..ce279309369 100644 --- a/modules/biobank/php/shipment.class.inc +++ b/modules/biobank/php/shipment.class.inc @@ -267,5 +267,12 @@ class Shipment implements { return json_encode($this); } -} + public function isAccessibleBy(\User $user): bool + { + return \LORIS\StudyEntities\DataInstanceAccess::centerMatch( + $user, + $this + ); + } +} diff --git a/modules/biobank/php/specimen.class.inc b/modules/biobank/php/specimen.class.inc index aae398e9ce8..ab4d45b7222 100644 --- a/modules/biobank/php/specimen.class.inc +++ b/modules/biobank/php/specimen.class.inc @@ -713,4 +713,12 @@ class Specimen implements { return json_encode($this); } + + public function isAccessibleBy(\User $user): bool + { + return \LORIS\StudyEntities\DataInstanceAccess::centerAndProjectMatch( + $user, + $this + ); + } } diff --git a/modules/candidate_list/php/candidatelistrow.class.inc b/modules/candidate_list/php/candidatelistrow.class.inc index a180670061d..551579f1219 100644 --- a/modules/candidate_list/php/candidatelistrow.class.inc +++ b/modules/candidate_list/php/candidatelistrow.class.inc @@ -80,4 +80,12 @@ class CandidateListRow implements \LORIS\Data\DataInstance, { return $this->ProjectID; } + + public function isAccessibleBy(\User $user): bool + { + return \LORIS\StudyEntities\DataInstanceAccess::centerAndProjectMatch( + $user, + $this + ); + } } diff --git a/modules/conflict_resolver/php/models/resolveddto.class.inc b/modules/conflict_resolver/php/models/resolveddto.class.inc index dbe52d151e3..49f09e4fa25 100644 --- a/modules/conflict_resolver/php/models/resolveddto.class.inc +++ b/modules/conflict_resolver/php/models/resolveddto.class.inc @@ -97,4 +97,12 @@ class ResolvedDTO implements DataInstance, SiteHaver { return \ProjectID::singleton($this->projectid); } + + public function isAccessibleBy(\User $user): bool + { + return \LORIS\StudyEntities\DataInstanceAccess::centerAndProjectMatch( + $user, + $this + ); + } } diff --git a/modules/conflict_resolver/php/models/unresolveddto.class.inc b/modules/conflict_resolver/php/models/unresolveddto.class.inc index 25a500ac67f..92ce6b7a780 100644 --- a/modules/conflict_resolver/php/models/unresolveddto.class.inc +++ b/modules/conflict_resolver/php/models/unresolveddto.class.inc @@ -93,4 +93,12 @@ class UnresolvedDTO implements DataInstance, SiteHaver { return \ProjectID::singleton($this->projectid); } + + public function isAccessibleBy(\User $user): bool + { + return \LORIS\StudyEntities\DataInstanceAccess::centerAndProjectMatch( + $user, + $this + ); + } } diff --git a/modules/data_release/php/datareleaserow.class.inc b/modules/data_release/php/datareleaserow.class.inc index f475e9cd065..89b830c7c45 100644 --- a/modules/data_release/php/datareleaserow.class.inc +++ b/modules/data_release/php/datareleaserow.class.inc @@ -49,4 +49,9 @@ class DataReleaseRow implements \LORIS\Data\DataInstance { return $this->DBRow; } + + public function isAccessibleBy(\User $user): bool + { + return true; + } } diff --git a/modules/datadict/php/datadictrow.class.inc b/modules/datadict/php/datadictrow.class.inc index 374a6ef77cc..8f66a4bef53 100644 --- a/modules/datadict/php/datadictrow.class.inc +++ b/modules/datadict/php/datadictrow.class.inc @@ -50,4 +50,10 @@ class DataDictRow implements \LORIS\Data\DataInstance { return $this->DBRow; } + + public function isAccessibleBy(\User $user): bool + { + return $user->hasPermission('data_dict_view') + || $user->hasPermission('data_dict_edit'); + } } diff --git a/modules/dicom_archive/php/dicomarchiverowwithoutsession.class.inc b/modules/dicom_archive/php/dicomarchiverowwithoutsession.class.inc index c8f7dcaa5b5..529f8add630 100644 --- a/modules/dicom_archive/php/dicomarchiverowwithoutsession.class.inc +++ b/modules/dicom_archive/php/dicomarchiverowwithoutsession.class.inc @@ -76,4 +76,14 @@ class DICOMArchiveRowWithoutSession implements \LORIS\Data\DataInstance { return $this->DBRow; } + + public function isAccessibleBy(\User $user): bool + { + if ($this->createdBy()->getId() === $user->getId()) { + return true; + } + + return $user->hasPermission('dicom_archive_view_allsites') + || $user->hasPermission('dicom_archive_nosessionid'); + } } diff --git a/modules/dicom_archive/php/dicomarchiverowwithsession.class.inc b/modules/dicom_archive/php/dicomarchiverowwithsession.class.inc index 09c35087ede..78f9c2ef23f 100644 --- a/modules/dicom_archive/php/dicomarchiverowwithsession.class.inc +++ b/modules/dicom_archive/php/dicomarchiverowwithsession.class.inc @@ -113,4 +113,12 @@ class DICOMArchiveRowWithSession implements \LORIS\Data\DataInstance, { return $this->CreatedBy; } + + public function isAccessibleBy(\User $user): bool + { + return \LORIS\StudyEntities\DataInstanceAccess::centerAndProjectMatch( + $user, + $this + ); + } } diff --git a/modules/document_repository/php/docreporow.class.inc b/modules/document_repository/php/docreporow.class.inc index 9a9827fff04..b5da3e03f66 100644 --- a/modules/document_repository/php/docreporow.class.inc +++ b/modules/document_repository/php/docreporow.class.inc @@ -70,4 +70,12 @@ class DocRepoRow implements { return $this->DBRow; } + + public function isAccessibleBy(\User $user): bool + { + return \LORIS\StudyEntities\DataInstanceAccess::centerMatch( + $user, + $this + ); + } } diff --git a/modules/electrophysiology_browser/php/electrophysiologybrowserrow.class.inc b/modules/electrophysiology_browser/php/electrophysiologybrowserrow.class.inc index f9c84977f4c..d54609cdc8a 100644 --- a/modules/electrophysiology_browser/php/electrophysiologybrowserrow.class.inc +++ b/modules/electrophysiology_browser/php/electrophysiologybrowserrow.class.inc @@ -79,4 +79,12 @@ class ElectrophysiologyBrowserRow implements \LORIS\Data\DataInstance, { return $this->ProjectID; } + + public function isAccessibleBy(\User $user): bool + { + return \LORIS\StudyEntities\DataInstanceAccess::centerAndProjectMatch( + $user, + $this + ); + } } diff --git a/modules/electrophysiology_browser/php/models/electrophysiofile.class.inc b/modules/electrophysiology_browser/php/models/electrophysiofile.class.inc index 327cc7014af..4d4e08de10d 100644 --- a/modules/electrophysiology_browser/php/models/electrophysiofile.class.inc +++ b/modules/electrophysiology_browser/php/models/electrophysiofile.class.inc @@ -192,4 +192,28 @@ class ElectrophysioFile implements \LORIS\Data\DataInstance { return ''; } + + public function isAccessibleBy(\User $user): bool + { + $sessionID = $this->getParameter('SessionID'); + if ($sessionID === '') { + return false; + } + + try { + $timepoint = \NDB_Factory::singleton()->timepoint( + new \SessionID($sessionID) + ); + } catch (\Throwable) { + return false; + } + + return ( + $user->hasPermission('electrophysiology_browser_view_allsites') + || ( + $user->hasCenter($timepoint->getCenterID()) + && $user->hasPermission('electrophysiology_browser_view_site') + ) + ) && $user->hasProject($timepoint->getProject()->getId()); + } } diff --git a/modules/electrophysiology_uploader/php/electrophysiologyuploaderrow.class.inc b/modules/electrophysiology_uploader/php/electrophysiologyuploaderrow.class.inc index b0ef23a43da..bde842429de 100644 --- a/modules/electrophysiology_uploader/php/electrophysiologyuploaderrow.class.inc +++ b/modules/electrophysiology_uploader/php/electrophysiologyuploaderrow.class.inc @@ -67,4 +67,12 @@ class ElectrophysiologyUploaderRow { return $this->ProjectID; } + + public function isAccessibleBy(\User $user): bool + { + return \LORIS\StudyEntities\DataInstanceAccess::centerAndProjectMatch( + $user, + $this + ); + } } diff --git a/modules/examiner/php/examinerrow.class.inc b/modules/examiner/php/examinerrow.class.inc index 50c49a8640e..0834e7c4cbe 100644 --- a/modules/examiner/php/examinerrow.class.inc +++ b/modules/examiner/php/examinerrow.class.inc @@ -81,4 +81,12 @@ class ExaminerRow implements } return $row; } + + public function isAccessibleBy(\User $user): bool + { + return \LORIS\StudyEntities\DataInstanceAccess::centerMatch( + $user, + $this + ); + } } diff --git a/modules/genomic_browser/php/models/cnvdto.class.inc b/modules/genomic_browser/php/models/cnvdto.class.inc index af20931d6be..bf6f4560fc9 100644 --- a/modules/genomic_browser/php/models/cnvdto.class.inc +++ b/modules/genomic_browser/php/models/cnvdto.class.inc @@ -258,4 +258,12 @@ class CnvDTO implements DataInstance, SiteHaver { return \ProjectID::singleton($this->_projectID); } + + public function isAccessibleBy(\User $user): bool + { + return \LORIS\StudyEntities\DataInstanceAccess::centerAndProjectMatch( + $user, + $this + ); + } } diff --git a/modules/genomic_browser/php/models/filesdto.class.inc b/modules/genomic_browser/php/models/filesdto.class.inc index 206d8044ead..b39ce3c7d29 100644 --- a/modules/genomic_browser/php/models/filesdto.class.inc +++ b/modules/genomic_browser/php/models/filesdto.class.inc @@ -95,4 +95,14 @@ class FilesDTO implements DataInstance 'Notes' => $this->_Notes, ]; } + + public function isAccessibleBy(\User $user): bool + { + return $user->hasAnyPermission( + [ + 'genomic_browser_view_allsites', + 'genomic_browser_view_site', + ] + ); + } } diff --git a/modules/genomic_browser/php/models/gwasdto.class.inc b/modules/genomic_browser/php/models/gwasdto.class.inc index 9c89e66b254..3f6f3f46821 100644 --- a/modules/genomic_browser/php/models/gwasdto.class.inc +++ b/modules/genomic_browser/php/models/gwasdto.class.inc @@ -114,4 +114,14 @@ class GwasDTO implements DataInstance 'Pvalue' => $this->_Pvalue, ]; } + + public function isAccessibleBy(\User $user): bool + { + return $user->hasAnyPermission( + [ + 'genomic_browser_view_allsites', + 'genomic_browser_view_site', + ] + ); + } } diff --git a/modules/genomic_browser/php/models/methylationdto.class.inc b/modules/genomic_browser/php/models/methylationdto.class.inc index 1b5d329084b..7bd68fc33a3 100644 --- a/modules/genomic_browser/php/models/methylationdto.class.inc +++ b/modules/genomic_browser/php/models/methylationdto.class.inc @@ -322,4 +322,12 @@ class MethylationDTO implements DataInstance, SiteHaver { return \ProjectID::singleton($this->_projectID); } + + public function isAccessibleBy(\User $user): bool + { + return \LORIS\StudyEntities\DataInstanceAccess::centerAndProjectMatch( + $user, + $this + ); + } } diff --git a/modules/genomic_browser/php/models/profiledto.class.inc b/modules/genomic_browser/php/models/profiledto.class.inc index b36c99ae24a..dcd7491d149 100644 --- a/modules/genomic_browser/php/models/profiledto.class.inc +++ b/modules/genomic_browser/php/models/profiledto.class.inc @@ -158,4 +158,12 @@ class ProfileDTO implements DataInstance, SiteHaver { return \ProjectID::singleton($this->_projectID); } + + public function isAccessibleBy(\User $user): bool + { + return \LORIS\StudyEntities\DataInstanceAccess::centerAndProjectMatch( + $user, + $this + ); + } } diff --git a/modules/genomic_browser/php/models/snpdto.class.inc b/modules/genomic_browser/php/models/snpdto.class.inc index 0bf8791fa00..221554b0860 100644 --- a/modules/genomic_browser/php/models/snpdto.class.inc +++ b/modules/genomic_browser/php/models/snpdto.class.inc @@ -309,4 +309,12 @@ class SnpDTO implements DataInstance, SiteHaver { return \ProjectID::singleton($this->_projectID); } + + public function isAccessibleBy(\User $user): bool + { + return \LORIS\StudyEntities\DataInstanceAccess::centerAndProjectMatch( + $user, + $this + ); + } } diff --git a/modules/help_editor/php/helprow.class.inc b/modules/help_editor/php/helprow.class.inc index 4f1032ba7a0..81ba0d8d07e 100644 --- a/modules/help_editor/php/helprow.class.inc +++ b/modules/help_editor/php/helprow.class.inc @@ -50,4 +50,9 @@ class HelpRow implements \LORIS\Data\DataInstance { return $this->DBRow; } + + public function isAccessibleBy(\User $user): bool + { + return $user->hasPermission('context_help'); + } } diff --git a/modules/imaging_browser/php/imagingbrowserrow.class.inc b/modules/imaging_browser/php/imagingbrowserrow.class.inc index 7399c79577d..fdc718a662b 100644 --- a/modules/imaging_browser/php/imagingbrowserrow.class.inc +++ b/modules/imaging_browser/php/imagingbrowserrow.class.inc @@ -88,4 +88,12 @@ class ImagingBrowserRow implements \LORIS\Data\DataInstance, { return $this->DBRow['entityType'] === 'Scanner'; } + + public function isAccessibleBy(\User $user): bool + { + return \LORIS\StudyEntities\DataInstanceAccess::centerAndProjectMatch( + $user, + $this + ); + } } diff --git a/modules/instrument_manager/php/instrumentrow.class.inc b/modules/instrument_manager/php/instrumentrow.class.inc index c893bc1fa53..018aeb08202 100644 --- a/modules/instrument_manager/php/instrumentrow.class.inc +++ b/modules/instrument_manager/php/instrumentrow.class.inc @@ -339,4 +339,9 @@ class InstrumentRow implements \LORIS\Data\DataInstance { return $this->DBRow; } + + public function isAccessibleBy(\User $user): bool + { + return $user->hasAnyPermission(Instrument_Manager::PERMISSIONS); + } } diff --git a/modules/issue_tracker/php/issuerow.class.inc b/modules/issue_tracker/php/issuerow.class.inc index ee15d780a45..37e1004ecbb 100644 --- a/modules/issue_tracker/php/issuerow.class.inc +++ b/modules/issue_tracker/php/issuerow.class.inc @@ -67,4 +67,12 @@ class IssueRow implements { return $this->Module; } + + public function isAccessibleBy(\User $user): bool + { + return \LORIS\StudyEntities\DataInstanceAccess::centerMatch( + $user, + $this + ); + } } diff --git a/modules/issue_tracker/php/models/attachmentdto.class.inc b/modules/issue_tracker/php/models/attachmentdto.class.inc index 6a6ac97d539..44b728aa154 100644 --- a/modules/issue_tracker/php/models/attachmentdto.class.inc +++ b/modules/issue_tracker/php/models/attachmentdto.class.inc @@ -59,4 +59,15 @@ class AttachmentDTO implements \LORIS\Data\DataInstance 'mime_type' => $this->mime_type, ]; } + + public function isAccessibleBy(\User $user): bool + { + return $user->hasAnyPermission( + [ + 'issue_tracker_all_issue', + 'issue_tracker_own_issue', + 'issue_tracker_site_issue', + ] + ); + } } diff --git a/modules/media/php/mediafile.class.inc b/modules/media/php/mediafile.class.inc index 8980b7260f3..2c4868159bb 100644 --- a/modules/media/php/mediafile.class.inc +++ b/modules/media/php/mediafile.class.inc @@ -88,4 +88,12 @@ class MediaFile implements \LORIS\Data\DataInstance, } return 0; } + + public function isAccessibleBy(\User $user): bool + { + return \LORIS\StudyEntities\DataInstanceAccess::centerAndProjectMatch( + $user, + $this + ); + } } diff --git a/modules/module_manager/php/modulerow.class.inc b/modules/module_manager/php/modulerow.class.inc index 437035d9055..6026e00e98b 100644 --- a/modules/module_manager/php/modulerow.class.inc +++ b/modules/module_manager/php/modulerow.class.inc @@ -62,4 +62,9 @@ class ModuleRow implements \LORIS\Data\DataInstance 'Active' => $this->Active, ]; } + + public function isAccessibleBy(\User $user): bool + { + return $user->hasPermission('module_manager_edit'); + } } diff --git a/modules/mri_violations/php/mriviolation.class.inc b/modules/mri_violations/php/mriviolation.class.inc index 7519702774e..981c829f4e4 100644 --- a/modules/mri_violations/php/mriviolation.class.inc +++ b/modules/mri_violations/php/mriviolation.class.inc @@ -59,4 +59,23 @@ class MRIViolation implements \LORIS\Data\DataInstance } return \ProjectID::singleton(intval($this->DBRow['Project'])); } + + public function isAccessibleBy(\User $user): bool + { + if ($user->hasPermission('violated_scans_view_allsites')) { + return true; + } + + $center = $this->getCenterID(); + if ($center !== null && !$user->hasCenter($center)) { + return false; + } + + $project = $this->getProjectID(); + if ($project !== null && !$user->hasProject($project)) { + return false; + } + + return true; + } } diff --git a/modules/mri_violations/php/protocolcheckviolation.class.inc b/modules/mri_violations/php/protocolcheckviolation.class.inc index 97a25bdd624..7dc9b924767 100644 --- a/modules/mri_violations/php/protocolcheckviolation.class.inc +++ b/modules/mri_violations/php/protocolcheckviolation.class.inc @@ -44,4 +44,14 @@ class ProtocolCheckViolation implements \LORIS\Data\DataInstance } return \CenterID::singleton($this->DBRow['CenterID']); } + + public function isAccessibleBy(\User $user): bool + { + if ($user->hasPermission('violated_scans_view_allsites')) { + return true; + } + + $center = $this->getCenterID(); + return $center === null || $user->hasCenter($center); + } } diff --git a/modules/mri_violations/php/protocolviolation.class.inc b/modules/mri_violations/php/protocolviolation.class.inc index 8474e80ed0d..a81326e8f88 100644 --- a/modules/mri_violations/php/protocolviolation.class.inc +++ b/modules/mri_violations/php/protocolviolation.class.inc @@ -31,4 +31,14 @@ class ProtocolViolation implements \LORIS\Data\DataInstance { return $this->DBRow; } + + public function isAccessibleBy(\User $user): bool + { + return $user->hasAnyPermission( + [ + 'violated_scans_view_allsites', + 'violated_scans_view_ownsite', + ] + ); + } } diff --git a/modules/schedule_module/php/schedulerow.class.inc b/modules/schedule_module/php/schedulerow.class.inc index 193b157a132..81f069a273c 100644 --- a/modules/schedule_module/php/schedulerow.class.inc +++ b/modules/schedule_module/php/schedulerow.class.inc @@ -126,4 +126,12 @@ class ScheduleRow implements \LORIS\Data\DataInstance, { return $this->ProjectID; } + + public function isAccessibleBy(\User $user): bool + { + return \LORIS\StudyEntities\DataInstanceAccess::centerAndProjectMatch( + $user, + $this + ); + } } diff --git a/modules/survey_accounts/php/surveyaccountsrow.class.inc b/modules/survey_accounts/php/surveyaccountsrow.class.inc index ea1f96c44e8..05cd64f57a5 100644 --- a/modules/survey_accounts/php/surveyaccountsrow.class.inc +++ b/modules/survey_accounts/php/surveyaccountsrow.class.inc @@ -79,4 +79,12 @@ class SurveyAccountsRow implements \LORIS\Data\DataInstance, { return $this->ProjectID; } + + public function isAccessibleBy(\User $user): bool + { + return \LORIS\StudyEntities\DataInstanceAccess::centerAndProjectMatch( + $user, + $this + ); + } } diff --git a/modules/user_accounts/php/useraccountrow.class.inc b/modules/user_accounts/php/useraccountrow.class.inc index 9efa3edecc8..6d80c24ed26 100644 --- a/modules/user_accounts/php/useraccountrow.class.inc +++ b/modules/user_accounts/php/useraccountrow.class.inc @@ -62,4 +62,12 @@ class UserAccountRow implements { return $this->DBRow; } + + public function isAccessibleBy(\User $user): bool + { + return \LORIS\StudyEntities\DataInstanceAccess::centerAndProjectMatch( + $user, + $this + ); + } } From 00a002855f1c5753a34849325f48eb33acfc15d8 Mon Sep 17 00:00:00 2001 From: arnav-makkar Date: Fri, 13 Feb 2026 15:47:28 +0530 Subject: [PATCH 2/4] core contract update --- php/libraries/CohortData.class.inc | 5 + src/Data/DataInstance.php | 4 +- src/Data/Filters/AccessibleResourceFilter.php | 8 - src/Data/Models/DicomTarDTO.php | 5 + src/Data/Models/ImageDTO.php | 8 + src/Data/Models/MRIUploadDTO.php | 5 + src/Data/Models/RecordingDTO.php | 8 + src/StudyEntities/DataInstanceAccess.php | 231 ++++++++++++++++++ 8 files changed, 265 insertions(+), 9 deletions(-) create mode 100644 src/StudyEntities/DataInstanceAccess.php diff --git a/php/libraries/CohortData.class.inc b/php/libraries/CohortData.class.inc index a6fb5c6d166..2b1c656e697 100644 --- a/php/libraries/CohortData.class.inc +++ b/php/libraries/CohortData.class.inc @@ -72,4 +72,9 @@ class CohortData implements DataInstance "RecruitmentTarget" => $this->recruitmentTarget ]; } + + public function isAccessibleBy(\User $user): bool + { + return $user->hasPermission('config'); + } } diff --git a/src/Data/DataInstance.php b/src/Data/DataInstance.php index d837633cde0..e634250581e 100644 --- a/src/Data/DataInstance.php +++ b/src/Data/DataInstance.php @@ -27,6 +27,8 @@ * @license http://www.gnu.org/licenses/gpl-3.0.txt GPLv3 * @link https://www.github.com/aces/Loris/ */ -interface DataInstance extends \JsonSerializable +interface DataInstance extends + \JsonSerializable, + \LORIS\StudyEntities\AccessibleResource { } diff --git a/src/Data/Filters/AccessibleResourceFilter.php b/src/Data/Filters/AccessibleResourceFilter.php index 3e7c36570ed..a810b3e9797 100644 --- a/src/Data/Filters/AccessibleResourceFilter.php +++ b/src/Data/Filters/AccessibleResourceFilter.php @@ -36,14 +36,6 @@ public function __construct(protected ?bool $defaultReturn = null) */ public function filter(\User $user, \Loris\Data\DataInstance $resource) : bool { - if (!($resource instanceof \LORIS\StudyEntities\AccessibleResource)) { - if ($this->defaultReturn === null) { - throw new \LorisException( - "Resource is not an AccessibleResource instance" - ); - } - return $this->defaultReturn; - } return $resource->isAccessibleBy($user); } } diff --git a/src/Data/Models/DicomTarDTO.php b/src/Data/Models/DicomTarDTO.php index a65182619d0..f4a9c881e53 100644 --- a/src/Data/Models/DicomTarDTO.php +++ b/src/Data/Models/DicomTarDTO.php @@ -119,4 +119,9 @@ function ($item) { 'series' => $series, ]; } + + public function isAccessibleBy(\User $user): bool + { + return true; + } } diff --git a/src/Data/Models/ImageDTO.php b/src/Data/Models/ImageDTO.php index d0171601448..604562c4c55 100644 --- a/src/Data/Models/ImageDTO.php +++ b/src/Data/Models/ImageDTO.php @@ -173,4 +173,12 @@ public function isPhantom(): bool { return $this->entitytype === 'Scanner'; } + + public function isAccessibleBy(\User $user): bool + { + return \LORIS\StudyEntities\DataInstanceAccess::centerMatch( + $user, + $this + ); + } } diff --git a/src/Data/Models/MRIUploadDTO.php b/src/Data/Models/MRIUploadDTO.php index d61c18c1113..aa3cfce1f22 100644 --- a/src/Data/Models/MRIUploadDTO.php +++ b/src/Data/Models/MRIUploadDTO.php @@ -167,4 +167,9 @@ public function jsonSerialize() : string { return $this->toJSON(); } + + public function isAccessibleBy(\User $user): bool + { + return true; + } } diff --git a/src/Data/Models/RecordingDTO.php b/src/Data/Models/RecordingDTO.php index 0bbe47f4609..298156c1237 100644 --- a/src/Data/Models/RecordingDTO.php +++ b/src/Data/Models/RecordingDTO.php @@ -163,4 +163,12 @@ public function getCenterID(): \CenterID { return $this->centerid; } + + public function isAccessibleBy(\User $user): bool + { + return \LORIS\StudyEntities\DataInstanceAccess::centerMatch( + $user, + $this + ); + } } diff --git a/src/StudyEntities/DataInstanceAccess.php b/src/StudyEntities/DataInstanceAccess.php new file mode 100644 index 00000000000..d04d194bace --- /dev/null +++ b/src/StudyEntities/DataInstanceAccess.php @@ -0,0 +1,231 @@ +hasCenter($center)) { + return true; + } + } + return false; + } + + /** + * Return true if the user has at least one matching project on the resource. + * + * @param \User $user User whose access is being checked. + * @param object $resource Data resource with project getters. + * @param bool $allowNullProject Whether null project means accessible. + * + * @return bool + */ + public static function projectMatch( + \User $user, + object $resource, + bool $allowNullProject = false + ): bool { + $projectData = self::getMethodValue( + $resource, + ['getProjectIDs', 'getProjectIds', 'getProjectID', 'getProjectId'] + ); + if (!$projectData['found']) { + return false; + } + + if ($projectData['value'] === null) { + return $allowNullProject; + } + + $value = $projectData['value']; + if (!is_iterable($value)) { + $project = self::normalizeProject($value); + return $project !== null && $user->hasProject($project); + } + + $projects = []; + foreach ($value as $project) { + if ($project === null && $allowNullProject) { + return true; + } + $normalized = self::normalizeProject($project); + if ($normalized !== null) { + $projects[] = $normalized; + } + } + if (count($projects) === 0) { + return false; + } + + foreach ($projects as $project) { + if ($user->hasProject($project)) { + return true; + } + } + return false; + } + + /** + * Return true if center and project access checks both pass. + * + * @param \User $user User whose access is being checked. + * @param object $resource Data resource. + * @param bool $allowNullProject Whether null project means accessible. + * + * @return bool + */ + public static function centerAndProjectMatch( + \User $user, + object $resource, + bool $allowNullProject = false + ): bool { + return self::centerMatch($user, $resource) + && self::projectMatch($user, $resource, $allowNullProject); + } + + /** + * Try calling the first existing method from a list of method names. + * + * @param object $resource Object to read from. + * @param string[] $methods Candidate method names. + * + * @return array{found: bool, value: mixed} + */ + private static function getMethodValue(object $resource, array $methods): array + { + foreach ($methods as $method) { + if (method_exists($resource, $method)) { + try { + return ['found' => true, 'value' => $resource->$method()]; + } catch (\Throwable) { + return ['found' => false, 'value' => null]; + } + } + } + return ['found' => false, 'value' => null]; + } + + /** + * Normalize a center payload to a list of CenterID objects. + * + * @param mixed $value Center payload. + * + * @return \CenterID[] + */ + private static function normalizeCenters(mixed $value): array + { + if ($value === null) { + return []; + } + + if ($value instanceof \CenterID) { + return [$value]; + } + + if (!is_iterable($value)) { + $center = self::normalizeCenter($value); + return $center === null ? [] : [$center]; + } + + $centers = []; + foreach ($value as $center) { + $normalized = self::normalizeCenter($center); + if ($normalized !== null) { + $centers[] = $normalized; + } + } + return $centers; + } + + /** + * Normalize one center value. + * + * @param mixed $center Center value. + * + * @return ?\CenterID + */ + private static function normalizeCenter(mixed $center): ?\CenterID + { + if ($center instanceof \CenterID) { + return $center; + } + if (is_int($center)) { + try { + return \CenterID::singleton($center); + } catch (\Throwable) { + return null; + } + } + if (is_string($center) && ctype_digit($center)) { + try { + return \CenterID::singleton((int) $center); + } catch (\Throwable) { + return null; + } + } + return null; + } + + /** + * Normalize one project value. + * + * @param mixed $project Project value. + * + * @return ?\ProjectID + */ + private static function normalizeProject(mixed $project): ?\ProjectID + { + if ($project instanceof \ProjectID) { + return $project; + } + if (is_int($project)) { + try { + return \ProjectID::singleton($project); + } catch (\Throwable) { + return null; + } + } + if (is_string($project) && ctype_digit($project)) { + try { + return \ProjectID::singleton((int) $project); + } catch (\Throwable) { + return null; + } + } + return null; + } +} From 60ea6809123eec92108b28019678151803f45c7d Mon Sep 17 00:00:00 2001 From: arnav-makkar Date: Fri, 13 Feb 2026 15:48:04 +0530 Subject: [PATCH 3/4] tests added --- test/unittests/DataInstanceAccessTest.php | 203 ++++++++++++++++++++ test/unittests/DataInstanceBehaviorTest.php | 153 +++++++++++++++ test/unittests/DataInstanceContractTest.php | 89 +++++++++ 3 files changed, 445 insertions(+) create mode 100644 test/unittests/DataInstanceAccessTest.php create mode 100644 test/unittests/DataInstanceBehaviorTest.php create mode 100644 test/unittests/DataInstanceContractTest.php diff --git a/test/unittests/DataInstanceAccessTest.php b/test/unittests/DataInstanceAccessTest.php new file mode 100644 index 00000000000..b599238d026 --- /dev/null +++ b/test/unittests/DataInstanceAccessTest.php @@ -0,0 +1,203 @@ +getMockBuilder(\User::class) + ->disableOriginalConstructor() + ->onlyMethods(['hasCenter']) + ->getMock(); + $user->expects($this->once()) + ->method('hasCenter') + ->with(\CenterID::singleton(1)) + ->willReturn(true); + + $this->assertTrue(DataInstanceAccess::centerMatch($user, $resource)); + } + + /** + * @return void + */ + public function testCenterMatchWithMultipleCenters(): void + { + $resource = new class () { + /** + * @return int[] + */ + public function getCenterIDs(): array + { + return [5, 7]; + } + }; + + $user = $this->getMockBuilder(\User::class) + ->disableOriginalConstructor() + ->onlyMethods(['hasCenter']) + ->getMock(); + $user->expects($this->exactly(2)) + ->method('hasCenter') + ->willReturnOnConsecutiveCalls(false, true); + + $this->assertTrue(DataInstanceAccess::centerMatch($user, $resource)); + } + + /** + * @return void + */ + public function testProjectMatchWithScalarProject(): void + { + $resource = new class () { + public function getProjectID(): int + { + return 3; + } + }; + + $user = $this->getMockBuilder(\User::class) + ->disableOriginalConstructor() + ->onlyMethods(['hasProject']) + ->getMock(); + $user->expects($this->once()) + ->method('hasProject') + ->with(\ProjectID::singleton(3)) + ->willReturn(true); + + $this->assertTrue(DataInstanceAccess::projectMatch($user, $resource)); + } + + /** + * @return void + */ + public function testProjectMatchHandlesNullProjectByFlag(): void + { + $resource = new class () { + public function getProjectID(): ?\ProjectID + { + return null; + } + }; + + $user = $this->getMockBuilder(\User::class) + ->disableOriginalConstructor() + ->onlyMethods(['hasProject']) + ->getMock(); + $user->expects($this->never())->method('hasProject'); + + $this->assertFalse(DataInstanceAccess::projectMatch($user, $resource)); + $this->assertTrue(DataInstanceAccess::projectMatch($user, $resource, true)); + } + + /** + * @return void + */ + public function testCenterAndProjectMatchRequiresBoth(): void + { + $resource = new class () { + public function getCenterID(): \CenterID + { + return \CenterID::singleton(1); + } + + public function getProjectID(): \ProjectID + { + return \ProjectID::singleton(2); + } + }; + + $user = $this->getMockBuilder(\User::class) + ->disableOriginalConstructor() + ->onlyMethods(['hasCenter', 'hasProject']) + ->getMock(); + $user->method('hasCenter')->willReturn(true); + $user->method('hasProject')->willReturn(false); + + $this->assertFalse(DataInstanceAccess::centerAndProjectMatch($user, $resource)); + } + + /** + * @return void + */ + public function testMissingGettersReturnFalse(): void + { + $resource = new class () { + }; + + $user = $this->getMockBuilder(\User::class) + ->disableOriginalConstructor() + ->onlyMethods(['hasCenter', 'hasProject']) + ->getMock(); + $user->expects($this->never())->method('hasCenter'); + $user->expects($this->never())->method('hasProject'); + + $this->assertFalse(DataInstanceAccess::centerMatch($user, $resource)); + $this->assertFalse(DataInstanceAccess::projectMatch($user, $resource)); + } + + /** + * @return void + */ + public function testInvalidScalarIdentifiersReturnFalse(): void + { + $resource = new class () { + public function getCenterID(): string + { + return 'invalid-center'; + } + + public function getProjectID(): string + { + return 'invalid-project'; + } + }; + + $user = $this->getMockBuilder(\User::class) + ->disableOriginalConstructor() + ->onlyMethods(['hasCenter', 'hasProject']) + ->getMock(); + $user->expects($this->never())->method('hasCenter'); + $user->expects($this->never())->method('hasProject'); + + $this->assertFalse(DataInstanceAccess::centerMatch($user, $resource)); + $this->assertFalse(DataInstanceAccess::projectMatch($user, $resource)); + $this->assertFalse(DataInstanceAccess::centerAndProjectMatch($user, $resource)); + } + + /** + * @return void + */ + public function testThrowingGetterReturnsFalse(): void + { + $resource = new class () { + public function getProjectID(): ?\ProjectID + { + throw new \RuntimeException('broken getter'); + } + }; + + $user = $this->getMockBuilder(\User::class) + ->disableOriginalConstructor() + ->onlyMethods(['hasProject']) + ->getMock(); + $user->expects($this->never())->method('hasProject'); + + $this->assertFalse(DataInstanceAccess::projectMatch($user, $resource)); + $this->assertFalse(DataInstanceAccess::projectMatch($user, $resource, true)); + } +} diff --git a/test/unittests/DataInstanceBehaviorTest.php b/test/unittests/DataInstanceBehaviorTest.php new file mode 100644 index 00000000000..23035c8bc1d --- /dev/null +++ b/test/unittests/DataInstanceBehaviorTest.php @@ -0,0 +1,153 @@ + 1, + 'Project' => 2, + ] + ); + + $user = $this->getMockBuilder(\User::class) + ->disableOriginalConstructor() + ->onlyMethods(['hasPermission', 'hasCenter', 'hasProject']) + ->getMock(); + $user->method('hasPermission')->willReturn(false); + $user->method('hasCenter')->willReturn(true); + $user->method('hasProject')->willReturn(true); + + $this->assertTrue($resource->isAccessibleBy($user)); + } + + /** + * @return void + */ + public function testMRIViolationDeniedOnCenterMismatchWithoutAllSites(): void + { + $resource = new MRIViolation( + [ + 'Site' => 1, + 'Project' => 2, + ] + ); + + $user = $this->getMockBuilder(\User::class) + ->disableOriginalConstructor() + ->onlyMethods(['hasPermission', 'hasCenter', 'hasProject']) + ->getMock(); + $user->method('hasPermission')->willReturn(false); + $user->method('hasCenter')->willReturn(false); + $user->expects($this->never())->method('hasProject'); + + $this->assertFalse($resource->isAccessibleBy($user)); + } + + /** + * @return void + */ + public function testProtocolCheckViolationAllowsNullCenter(): void + { + $resource = new ProtocolCheckViolation(['CenterID' => null]); + + $user = $this->getMockBuilder(\User::class) + ->disableOriginalConstructor() + ->onlyMethods(['hasPermission', 'hasCenter']) + ->getMock(); + $user->method('hasPermission')->willReturn(false); + $user->expects($this->never())->method('hasCenter'); + + $this->assertTrue($resource->isAccessibleBy($user)); + } + + /** + * @return void + */ + public function testDicomWithoutSessionAccessibleByCreator(): void + { + $creator = $this->getMockBuilder(\User::class) + ->disableOriginalConstructor() + ->onlyMethods(['getId']) + ->getMock(); + $creator->method('getId')->willReturn(9); + + $resource = new DICOMArchiveRowWithoutSession([], $creator); + + $user = $this->getMockBuilder(\User::class) + ->disableOriginalConstructor() + ->onlyMethods(['getId', 'hasPermission']) + ->getMock(); + $user->method('getId')->willReturn(9); + $user->expects($this->never())->method('hasPermission'); + + $this->assertTrue($resource->isAccessibleBy($user)); + } + + /** + * @return void + */ + public function testDicomWithoutSessionAccessibleByPermission(): void + { + $creator = $this->getMockBuilder(\User::class) + ->disableOriginalConstructor() + ->onlyMethods(['getId']) + ->getMock(); + $creator->method('getId')->willReturn(3); + + $resource = new DICOMArchiveRowWithoutSession([], $creator); + + $user = $this->getMockBuilder(\User::class) + ->disableOriginalConstructor() + ->onlyMethods(['getId', 'hasPermission']) + ->getMock(); + $user->method('getId')->willReturn(11); + $user->method('hasPermission')->willReturnMap( + [ + ['dicom_archive_view_allsites', true], + ['dicom_archive_nosessionid', false], + ] + ); + + $this->assertTrue($resource->isAccessibleBy($user)); + } + + /** + * @return void + */ + public function testDicomWithoutSessionDeniedWithoutCreatorOrPermissions(): void + { + $creator = $this->getMockBuilder(\User::class) + ->disableOriginalConstructor() + ->onlyMethods(['getId']) + ->getMock(); + $creator->method('getId')->willReturn(3); + + $resource = new DICOMArchiveRowWithoutSession([], $creator); + + $user = $this->getMockBuilder(\User::class) + ->disableOriginalConstructor() + ->onlyMethods(['getId', 'hasPermission']) + ->getMock(); + $user->method('getId')->willReturn(11); + $user->method('hasPermission')->willReturn(false); + + $this->assertFalse($resource->isAccessibleBy($user)); + } +} diff --git a/test/unittests/DataInstanceContractTest.php b/test/unittests/DataInstanceContractTest.php new file mode 100644 index 00000000000..89322f0cc9e --- /dev/null +++ b/test/unittests/DataInstanceContractTest.php @@ -0,0 +1,89 @@ +assertContains( + \LORIS\StudyEntities\AccessibleResource::class, + $interfaces + ); + } + + /** + * Ensure each DataInstance class declares isAccessibleBy with a bool return. + * + * @return void + */ + public function testAllDataInstancesDeclareIsAccessibleBy(): void + { + $files = $this->getDataInstanceFiles(); + $this->assertNotEmpty($files, 'No DataInstance classes found'); + + $pattern = '/public\\s+function\\s+isAccessibleBy\\s*\\(\\s*\\\\User\\s+\\$user\\s*\\)\\s*:\\s*bool/s'; + foreach ($files as $file) { + $contents = file_get_contents($file); + $this->assertNotFalse($contents, "Unable to read file: $file"); + $this->assertMatchesRegularExpression( + $pattern, + $contents, + "Missing required isAccessibleBy signature in $file" + ); + } + } + + /** + * Discover PHP files containing DataInstance implementations. + * + * @return string[] + */ + private function getDataInstanceFiles(): array + { + $roots = [ + __DIR__ . '/../../src', + __DIR__ . '/../../php', + __DIR__ . '/../../modules', + ]; + + $matches = []; + $classPattern = '/class\\s+\\w+(?:\\s+extends\\s+[\\w\\\\]+)?\\s+implements[\\s\\S]{0,250}?DataInstance[\\s\\S]{0,120}?\\{/s'; + + foreach ($roots as $root) { + $iterator = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator($root) + ); + foreach ($iterator as $file) { + $extension = strtolower($file->getExtension()); + if ( + !$file->isFile() + || !in_array($extension, ['php', 'inc'], true) + ) { + continue; + } + + $path = $file->getPathname(); + $contents = file_get_contents($path); + if ($contents === false) { + continue; + } + if (preg_match($classPattern, $contents) === 1) { + $matches[] = $path; + } + } + } + + sort($matches); + return $matches; + } +} From 40b8cb6eccd1c212ece4750ff559a1b5a7a804d7 Mon Sep 17 00:00:00 2001 From: arnav-makkar Date: Fri, 13 Feb 2026 17:06:26 +0530 Subject: [PATCH 4/4] lint --- .../php/acknowledgementrow.class.inc | 7 ++ .../api/php/models/candidatesrow.class.inc | 7 ++ .../api/php/models/projectimagesrow.class.inc | 7 ++ .../models/projectinstrumentsrow.class.inc | 7 ++ .../php/models/projectrecordingsrow.class.inc | 7 ++ modules/api/php/models/sitesrow.class.inc | 7 ++ modules/battery_manager/php/test.class.inc | 7 ++ .../php/models/behaviouraldto.class.inc | 7 ++ .../php/models/conflictsdto.class.inc | 7 ++ .../php/models/incompletedto.class.inc | 7 ++ modules/biobank/php/container.class.inc | 7 ++ modules/biobank/php/log.class.inc | 7 ++ modules/biobank/php/pool.class.inc | 7 ++ modules/biobank/php/shipment.class.inc | 7 ++ modules/biobank/php/specimen.class.inc | 7 ++ .../php/candidatelistrow.class.inc | 7 ++ .../php/models/resolveddto.class.inc | 7 ++ .../php/models/unresolveddto.class.inc | 7 ++ .../data_release/php/datareleaserow.class.inc | 7 ++ modules/datadict/php/datadictrow.class.inc | 7 ++ .../dicomarchiverowwithoutsession.class.inc | 7 ++ .../php/dicomarchiverowwithsession.class.inc | 7 ++ .../php/docreporow.class.inc | 7 ++ .../php/electrophysiologybrowserrow.class.inc | 7 ++ .../php/models/electrophysiofile.class.inc | 7 ++ .../electrophysiologyuploaderrow.class.inc | 7 ++ modules/examiner/php/examinerrow.class.inc | 7 ++ .../php/models/cnvdto.class.inc | 7 ++ .../php/models/filesdto.class.inc | 7 ++ .../php/models/gwasdto.class.inc | 7 ++ .../php/models/methylationdto.class.inc | 7 ++ .../php/models/profiledto.class.inc | 7 ++ .../php/models/snpdto.class.inc | 7 ++ modules/help_editor/php/helprow.class.inc | 7 ++ .../php/imagingbrowserrow.class.inc | 7 ++ .../php/instrumentrow.class.inc | 7 ++ modules/issue_tracker/php/issuerow.class.inc | 7 ++ .../php/models/attachmentdto.class.inc | 7 ++ modules/media/php/mediafile.class.inc | 7 ++ .../module_manager/php/modulerow.class.inc | 7 ++ .../mri_violations/php/mriviolation.class.inc | 7 ++ .../php/protocolcheckviolation.class.inc | 7 ++ .../php/protocolviolation.class.inc | 7 ++ .../schedule_module/php/schedulerow.class.inc | 7 ++ .../php/surveyaccountsrow.class.inc | 7 ++ .../php/useraccountrow.class.inc | 7 ++ php/libraries/CohortData.class.inc | 7 ++ src/Data/Models/DicomTarDTO.php | 8 +++ src/Data/Models/ImageDTO.php | 21 ++++-- src/Data/Models/MRIUploadDTO.php | 7 ++ src/Data/Models/RecordingDTO.php | 21 ++++-- src/StudyEntities/DataInstanceAccess.php | 29 ++++++-- test/unittests/DataInstanceAccessTest.php | 68 ++++++++++++++++++- test/unittests/DataInstanceBehaviorTest.php | 23 ++++++- test/unittests/DataInstanceContractTest.php | 19 +++--- 55 files changed, 493 insertions(+), 32 deletions(-) diff --git a/modules/acknowledgements/php/acknowledgementrow.class.inc b/modules/acknowledgements/php/acknowledgementrow.class.inc index 52e9cf5f55b..218a6aa2e00 100644 --- a/modules/acknowledgements/php/acknowledgementrow.class.inc +++ b/modules/acknowledgements/php/acknowledgementrow.class.inc @@ -50,6 +50,13 @@ class AcknowledgementRow implements \LORIS\Data\DataInstance return $this->DBRow; } + /** + * Check whether a user can access this data instance. + * + * @param \User $user The user whose access is being checked + * + * @return bool + */ public function isAccessibleBy(\User $user): bool { return true; diff --git a/modules/api/php/models/candidatesrow.class.inc b/modules/api/php/models/candidatesrow.class.inc index cd94ec708c9..2fca86fe195 100644 --- a/modules/api/php/models/candidatesrow.class.inc +++ b/modules/api/php/models/candidatesrow.class.inc @@ -102,6 +102,13 @@ class CandidatesRow implements \LORIS\Data\DataInstance, return $this->_projectid; } + /** + * Check whether a user can access this data instance. + * + * @param \User $user The user whose access is being checked + * + * @return bool + */ public function isAccessibleBy(\User $user): bool { return \LORIS\StudyEntities\DataInstanceAccess::centerAndProjectMatch( diff --git a/modules/api/php/models/projectimagesrow.class.inc b/modules/api/php/models/projectimagesrow.class.inc index 87578b673fd..9617ee2baeb 100644 --- a/modules/api/php/models/projectimagesrow.class.inc +++ b/modules/api/php/models/projectimagesrow.class.inc @@ -107,6 +107,13 @@ class ProjectImagesRow implements \LORIS\Data\DataInstance, return $this->_entitytype === 'Scanner'; } + /** + * Check whether a user can access this data instance. + * + * @param \User $user The user whose access is being checked + * + * @return bool + */ public function isAccessibleBy(\User $user): bool { return \LORIS\StudyEntities\DataInstanceAccess::centerMatch( diff --git a/modules/api/php/models/projectinstrumentsrow.class.inc b/modules/api/php/models/projectinstrumentsrow.class.inc index bf407fa66ec..1dcd4e14aa4 100644 --- a/modules/api/php/models/projectinstrumentsrow.class.inc +++ b/modules/api/php/models/projectinstrumentsrow.class.inc @@ -120,6 +120,13 @@ class ProjectInstrumentsRow implements \LORIS\Data\DataInstance return $obj; } + /** + * Check whether a user can access this data instance. + * + * @param \User $user The user whose access is being checked + * + * @return bool + */ public function isAccessibleBy(\User $user): bool { return true; diff --git a/modules/api/php/models/projectrecordingsrow.class.inc b/modules/api/php/models/projectrecordingsrow.class.inc index 468dd95977b..28e3c5d7216 100644 --- a/modules/api/php/models/projectrecordingsrow.class.inc +++ b/modules/api/php/models/projectrecordingsrow.class.inc @@ -92,6 +92,13 @@ class ProjectRecordingsRow implements \LORIS\Data\DataInstance return intval($this->_centerid); } + /** + * Check whether a user can access this data instance. + * + * @param \User $user The user whose access is being checked + * + * @return bool + */ public function isAccessibleBy(\User $user): bool { return \LORIS\StudyEntities\DataInstanceAccess::centerMatch( diff --git a/modules/api/php/models/sitesrow.class.inc b/modules/api/php/models/sitesrow.class.inc index 5b8dc928ce4..760daea6181 100644 --- a/modules/api/php/models/sitesrow.class.inc +++ b/modules/api/php/models/sitesrow.class.inc @@ -57,6 +57,13 @@ class SitesRow implements DataInstance, SiteHaver return \CenterID::singleton($this->_id); } + /** + * Check whether a user can access this data instance. + * + * @param \User $user The user whose access is being checked + * + * @return bool + */ public function isAccessibleBy(\User $user): bool { return \LORIS\StudyEntities\DataInstanceAccess::centerMatch( diff --git a/modules/battery_manager/php/test.class.inc b/modules/battery_manager/php/test.class.inc index 4bb936205b4..4bc6c8a13c6 100644 --- a/modules/battery_manager/php/test.class.inc +++ b/modules/battery_manager/php/test.class.inc @@ -106,6 +106,13 @@ class Test implements ]; } + /** + * Check whether a user can access this data instance. + * + * @param \User $user The user whose access is being checked + * + * @return bool + */ public function isAccessibleBy(\User $user): bool { return \LORIS\StudyEntities\DataInstanceAccess::centerMatch( diff --git a/modules/behavioural_qc/php/models/behaviouraldto.class.inc b/modules/behavioural_qc/php/models/behaviouraldto.class.inc index a2e3f8e0f90..2b8f5ad6d59 100644 --- a/modules/behavioural_qc/php/models/behaviouraldto.class.inc +++ b/modules/behavioural_qc/php/models/behaviouraldto.class.inc @@ -165,6 +165,13 @@ class BehaviouralDTO implements \LORIS\Data\DataInstance, ]; } + /** + * Check whether a user can access this data instance. + * + * @param \User $user The user whose access is being checked + * + * @return bool + */ public function isAccessibleBy(\User $user): bool { return \LORIS\StudyEntities\DataInstanceAccess::centerAndProjectMatch( diff --git a/modules/behavioural_qc/php/models/conflictsdto.class.inc b/modules/behavioural_qc/php/models/conflictsdto.class.inc index 8301ce1d99d..d7b2a4a2cd0 100644 --- a/modules/behavioural_qc/php/models/conflictsdto.class.inc +++ b/modules/behavioural_qc/php/models/conflictsdto.class.inc @@ -149,6 +149,13 @@ class ConflictsDTO implements \LORIS\Data\DataInstance, ]; } + /** + * Check whether a user can access this data instance. + * + * @param \User $user The user whose access is being checked + * + * @return bool + */ public function isAccessibleBy(\User $user): bool { return \LORIS\StudyEntities\DataInstanceAccess::centerAndProjectMatch( diff --git a/modules/behavioural_qc/php/models/incompletedto.class.inc b/modules/behavioural_qc/php/models/incompletedto.class.inc index d23161c170a..817ef5aa20f 100644 --- a/modules/behavioural_qc/php/models/incompletedto.class.inc +++ b/modules/behavioural_qc/php/models/incompletedto.class.inc @@ -157,6 +157,13 @@ class IncompleteDTO implements \LORIS\Data\DataInstance, ]; } + /** + * Check whether a user can access this data instance. + * + * @param \User $user The user whose access is being checked + * + * @return bool + */ public function isAccessibleBy(\User $user): bool { return \LORIS\StudyEntities\DataInstanceAccess::centerAndProjectMatch( diff --git a/modules/biobank/php/container.class.inc b/modules/biobank/php/container.class.inc index 13ab6891071..7d2848a2f01 100644 --- a/modules/biobank/php/container.class.inc +++ b/modules/biobank/php/container.class.inc @@ -559,6 +559,13 @@ class Container implements return json_encode($this); } + /** + * Check whether a user can access this data instance. + * + * @param \User $user The user whose access is being checked + * + * @return bool + */ public function isAccessibleBy(\User $user): bool { return \LORIS\StudyEntities\DataInstanceAccess::centerMatch( diff --git a/modules/biobank/php/log.class.inc b/modules/biobank/php/log.class.inc index ff689bcb0b7..4a49a5eec4e 100644 --- a/modules/biobank/php/log.class.inc +++ b/modules/biobank/php/log.class.inc @@ -192,6 +192,13 @@ class Log implements \JsonSerializable, \LORIS\Data\DataInstance return json_encode($this); } + /** + * Check whether a user can access this data instance. + * + * @param \User $user The user whose access is being checked + * + * @return bool + */ public function isAccessibleBy(\User $user): bool { return \LORIS\StudyEntities\DataInstanceAccess::centerMatch( diff --git a/modules/biobank/php/pool.class.inc b/modules/biobank/php/pool.class.inc index 24bb38afd84..f146603722f 100644 --- a/modules/biobank/php/pool.class.inc +++ b/modules/biobank/php/pool.class.inc @@ -498,6 +498,13 @@ class Pool implements return json_encode($this); } + /** + * Check whether a user can access this data instance. + * + * @param \User $user The user whose access is being checked + * + * @return bool + */ public function isAccessibleBy(\User $user): bool { return \LORIS\StudyEntities\DataInstanceAccess::centerAndProjectMatch( diff --git a/modules/biobank/php/shipment.class.inc b/modules/biobank/php/shipment.class.inc index ce279309369..63e52f9ca2b 100644 --- a/modules/biobank/php/shipment.class.inc +++ b/modules/biobank/php/shipment.class.inc @@ -268,6 +268,13 @@ class Shipment implements return json_encode($this); } + /** + * Check whether a user can access this data instance. + * + * @param \User $user The user whose access is being checked + * + * @return bool + */ public function isAccessibleBy(\User $user): bool { return \LORIS\StudyEntities\DataInstanceAccess::centerMatch( diff --git a/modules/biobank/php/specimen.class.inc b/modules/biobank/php/specimen.class.inc index ab4d45b7222..8d5723d4d03 100644 --- a/modules/biobank/php/specimen.class.inc +++ b/modules/biobank/php/specimen.class.inc @@ -714,6 +714,13 @@ class Specimen implements return json_encode($this); } + /** + * Check whether a user can access this data instance. + * + * @param \User $user The user whose access is being checked + * + * @return bool + */ public function isAccessibleBy(\User $user): bool { return \LORIS\StudyEntities\DataInstanceAccess::centerAndProjectMatch( diff --git a/modules/candidate_list/php/candidatelistrow.class.inc b/modules/candidate_list/php/candidatelistrow.class.inc index 551579f1219..630f65accd0 100644 --- a/modules/candidate_list/php/candidatelistrow.class.inc +++ b/modules/candidate_list/php/candidatelistrow.class.inc @@ -81,6 +81,13 @@ class CandidateListRow implements \LORIS\Data\DataInstance, return $this->ProjectID; } + /** + * Check whether a user can access this data instance. + * + * @param \User $user The user whose access is being checked + * + * @return bool + */ public function isAccessibleBy(\User $user): bool { return \LORIS\StudyEntities\DataInstanceAccess::centerAndProjectMatch( diff --git a/modules/conflict_resolver/php/models/resolveddto.class.inc b/modules/conflict_resolver/php/models/resolveddto.class.inc index 49f09e4fa25..c8c60b45936 100644 --- a/modules/conflict_resolver/php/models/resolveddto.class.inc +++ b/modules/conflict_resolver/php/models/resolveddto.class.inc @@ -98,6 +98,13 @@ class ResolvedDTO implements DataInstance, SiteHaver return \ProjectID::singleton($this->projectid); } + /** + * Check whether a user can access this data instance. + * + * @param \User $user The user whose access is being checked + * + * @return bool + */ public function isAccessibleBy(\User $user): bool { return \LORIS\StudyEntities\DataInstanceAccess::centerAndProjectMatch( diff --git a/modules/conflict_resolver/php/models/unresolveddto.class.inc b/modules/conflict_resolver/php/models/unresolveddto.class.inc index 92ce6b7a780..a34c8426c85 100644 --- a/modules/conflict_resolver/php/models/unresolveddto.class.inc +++ b/modules/conflict_resolver/php/models/unresolveddto.class.inc @@ -94,6 +94,13 @@ class UnresolvedDTO implements DataInstance, SiteHaver return \ProjectID::singleton($this->projectid); } + /** + * Check whether a user can access this data instance. + * + * @param \User $user The user whose access is being checked + * + * @return bool + */ public function isAccessibleBy(\User $user): bool { return \LORIS\StudyEntities\DataInstanceAccess::centerAndProjectMatch( diff --git a/modules/data_release/php/datareleaserow.class.inc b/modules/data_release/php/datareleaserow.class.inc index 89b830c7c45..957358d25c2 100644 --- a/modules/data_release/php/datareleaserow.class.inc +++ b/modules/data_release/php/datareleaserow.class.inc @@ -50,6 +50,13 @@ class DataReleaseRow implements \LORIS\Data\DataInstance return $this->DBRow; } + /** + * Check whether a user can access this data instance. + * + * @param \User $user The user whose access is being checked + * + * @return bool + */ public function isAccessibleBy(\User $user): bool { return true; diff --git a/modules/datadict/php/datadictrow.class.inc b/modules/datadict/php/datadictrow.class.inc index 8f66a4bef53..f1e36cfb8cc 100644 --- a/modules/datadict/php/datadictrow.class.inc +++ b/modules/datadict/php/datadictrow.class.inc @@ -51,6 +51,13 @@ class DataDictRow implements \LORIS\Data\DataInstance return $this->DBRow; } + /** + * Check whether a user can access this data instance. + * + * @param \User $user The user whose access is being checked + * + * @return bool + */ public function isAccessibleBy(\User $user): bool { return $user->hasPermission('data_dict_view') diff --git a/modules/dicom_archive/php/dicomarchiverowwithoutsession.class.inc b/modules/dicom_archive/php/dicomarchiverowwithoutsession.class.inc index 529f8add630..f0fa9ed1c21 100644 --- a/modules/dicom_archive/php/dicomarchiverowwithoutsession.class.inc +++ b/modules/dicom_archive/php/dicomarchiverowwithoutsession.class.inc @@ -77,6 +77,13 @@ class DICOMArchiveRowWithoutSession implements \LORIS\Data\DataInstance return $this->DBRow; } + /** + * Check whether a user can access this data instance. + * + * @param \User $user The user whose access is being checked + * + * @return bool + */ public function isAccessibleBy(\User $user): bool { if ($this->createdBy()->getId() === $user->getId()) { diff --git a/modules/dicom_archive/php/dicomarchiverowwithsession.class.inc b/modules/dicom_archive/php/dicomarchiverowwithsession.class.inc index 78f9c2ef23f..8c8a63b0f63 100644 --- a/modules/dicom_archive/php/dicomarchiverowwithsession.class.inc +++ b/modules/dicom_archive/php/dicomarchiverowwithsession.class.inc @@ -114,6 +114,13 @@ class DICOMArchiveRowWithSession implements \LORIS\Data\DataInstance, return $this->CreatedBy; } + /** + * Check whether a user can access this data instance. + * + * @param \User $user The user whose access is being checked + * + * @return bool + */ public function isAccessibleBy(\User $user): bool { return \LORIS\StudyEntities\DataInstanceAccess::centerAndProjectMatch( diff --git a/modules/document_repository/php/docreporow.class.inc b/modules/document_repository/php/docreporow.class.inc index b5da3e03f66..234cc683f06 100644 --- a/modules/document_repository/php/docreporow.class.inc +++ b/modules/document_repository/php/docreporow.class.inc @@ -71,6 +71,13 @@ class DocRepoRow implements return $this->DBRow; } + /** + * Check whether a user can access this data instance. + * + * @param \User $user The user whose access is being checked + * + * @return bool + */ public function isAccessibleBy(\User $user): bool { return \LORIS\StudyEntities\DataInstanceAccess::centerMatch( diff --git a/modules/electrophysiology_browser/php/electrophysiologybrowserrow.class.inc b/modules/electrophysiology_browser/php/electrophysiologybrowserrow.class.inc index d54609cdc8a..7c0e56e966d 100644 --- a/modules/electrophysiology_browser/php/electrophysiologybrowserrow.class.inc +++ b/modules/electrophysiology_browser/php/electrophysiologybrowserrow.class.inc @@ -80,6 +80,13 @@ class ElectrophysiologyBrowserRow implements \LORIS\Data\DataInstance, return $this->ProjectID; } + /** + * Check whether a user can access this data instance. + * + * @param \User $user The user whose access is being checked + * + * @return bool + */ public function isAccessibleBy(\User $user): bool { return \LORIS\StudyEntities\DataInstanceAccess::centerAndProjectMatch( diff --git a/modules/electrophysiology_browser/php/models/electrophysiofile.class.inc b/modules/electrophysiology_browser/php/models/electrophysiofile.class.inc index 4d4e08de10d..bc078579279 100644 --- a/modules/electrophysiology_browser/php/models/electrophysiofile.class.inc +++ b/modules/electrophysiology_browser/php/models/electrophysiofile.class.inc @@ -193,6 +193,13 @@ class ElectrophysioFile implements \LORIS\Data\DataInstance return ''; } + /** + * Check whether a user can access this data instance. + * + * @param \User $user The user whose access is being checked + * + * @return bool + */ public function isAccessibleBy(\User $user): bool { $sessionID = $this->getParameter('SessionID'); diff --git a/modules/electrophysiology_uploader/php/electrophysiologyuploaderrow.class.inc b/modules/electrophysiology_uploader/php/electrophysiologyuploaderrow.class.inc index bde842429de..908b595c411 100644 --- a/modules/electrophysiology_uploader/php/electrophysiologyuploaderrow.class.inc +++ b/modules/electrophysiology_uploader/php/electrophysiologyuploaderrow.class.inc @@ -68,6 +68,13 @@ class ElectrophysiologyUploaderRow return $this->ProjectID; } + /** + * Check whether a user can access this data instance. + * + * @param \User $user The user whose access is being checked + * + * @return bool + */ public function isAccessibleBy(\User $user): bool { return \LORIS\StudyEntities\DataInstanceAccess::centerAndProjectMatch( diff --git a/modules/examiner/php/examinerrow.class.inc b/modules/examiner/php/examinerrow.class.inc index 0834e7c4cbe..159c402b741 100644 --- a/modules/examiner/php/examinerrow.class.inc +++ b/modules/examiner/php/examinerrow.class.inc @@ -82,6 +82,13 @@ class ExaminerRow implements return $row; } + /** + * Check whether a user can access this data instance. + * + * @param \User $user The user whose access is being checked + * + * @return bool + */ public function isAccessibleBy(\User $user): bool { return \LORIS\StudyEntities\DataInstanceAccess::centerMatch( diff --git a/modules/genomic_browser/php/models/cnvdto.class.inc b/modules/genomic_browser/php/models/cnvdto.class.inc index bf6f4560fc9..429cec2d6c0 100644 --- a/modules/genomic_browser/php/models/cnvdto.class.inc +++ b/modules/genomic_browser/php/models/cnvdto.class.inc @@ -259,6 +259,13 @@ class CnvDTO implements DataInstance, SiteHaver return \ProjectID::singleton($this->_projectID); } + /** + * Check whether a user can access this data instance. + * + * @param \User $user The user whose access is being checked + * + * @return bool + */ public function isAccessibleBy(\User $user): bool { return \LORIS\StudyEntities\DataInstanceAccess::centerAndProjectMatch( diff --git a/modules/genomic_browser/php/models/filesdto.class.inc b/modules/genomic_browser/php/models/filesdto.class.inc index b39ce3c7d29..b8451c2c87e 100644 --- a/modules/genomic_browser/php/models/filesdto.class.inc +++ b/modules/genomic_browser/php/models/filesdto.class.inc @@ -96,6 +96,13 @@ class FilesDTO implements DataInstance ]; } + /** + * Check whether a user can access this data instance. + * + * @param \User $user The user whose access is being checked + * + * @return bool + */ public function isAccessibleBy(\User $user): bool { return $user->hasAnyPermission( diff --git a/modules/genomic_browser/php/models/gwasdto.class.inc b/modules/genomic_browser/php/models/gwasdto.class.inc index 3f6f3f46821..047c98a24ce 100644 --- a/modules/genomic_browser/php/models/gwasdto.class.inc +++ b/modules/genomic_browser/php/models/gwasdto.class.inc @@ -115,6 +115,13 @@ class GwasDTO implements DataInstance ]; } + /** + * Check whether a user can access this data instance. + * + * @param \User $user The user whose access is being checked + * + * @return bool + */ public function isAccessibleBy(\User $user): bool { return $user->hasAnyPermission( diff --git a/modules/genomic_browser/php/models/methylationdto.class.inc b/modules/genomic_browser/php/models/methylationdto.class.inc index 7bd68fc33a3..70ebf865bfa 100644 --- a/modules/genomic_browser/php/models/methylationdto.class.inc +++ b/modules/genomic_browser/php/models/methylationdto.class.inc @@ -323,6 +323,13 @@ class MethylationDTO implements DataInstance, SiteHaver return \ProjectID::singleton($this->_projectID); } + /** + * Check whether a user can access this data instance. + * + * @param \User $user The user whose access is being checked + * + * @return bool + */ public function isAccessibleBy(\User $user): bool { return \LORIS\StudyEntities\DataInstanceAccess::centerAndProjectMatch( diff --git a/modules/genomic_browser/php/models/profiledto.class.inc b/modules/genomic_browser/php/models/profiledto.class.inc index dcd7491d149..7974c7b2367 100644 --- a/modules/genomic_browser/php/models/profiledto.class.inc +++ b/modules/genomic_browser/php/models/profiledto.class.inc @@ -159,6 +159,13 @@ class ProfileDTO implements DataInstance, SiteHaver return \ProjectID::singleton($this->_projectID); } + /** + * Check whether a user can access this data instance. + * + * @param \User $user The user whose access is being checked + * + * @return bool + */ public function isAccessibleBy(\User $user): bool { return \LORIS\StudyEntities\DataInstanceAccess::centerAndProjectMatch( diff --git a/modules/genomic_browser/php/models/snpdto.class.inc b/modules/genomic_browser/php/models/snpdto.class.inc index 221554b0860..e236a959a2f 100644 --- a/modules/genomic_browser/php/models/snpdto.class.inc +++ b/modules/genomic_browser/php/models/snpdto.class.inc @@ -310,6 +310,13 @@ class SnpDTO implements DataInstance, SiteHaver return \ProjectID::singleton($this->_projectID); } + /** + * Check whether a user can access this data instance. + * + * @param \User $user The user whose access is being checked + * + * @return bool + */ public function isAccessibleBy(\User $user): bool { return \LORIS\StudyEntities\DataInstanceAccess::centerAndProjectMatch( diff --git a/modules/help_editor/php/helprow.class.inc b/modules/help_editor/php/helprow.class.inc index 81ba0d8d07e..b48f3476003 100644 --- a/modules/help_editor/php/helprow.class.inc +++ b/modules/help_editor/php/helprow.class.inc @@ -51,6 +51,13 @@ class HelpRow implements \LORIS\Data\DataInstance return $this->DBRow; } + /** + * Check whether a user can access this data instance. + * + * @param \User $user The user whose access is being checked + * + * @return bool + */ public function isAccessibleBy(\User $user): bool { return $user->hasPermission('context_help'); diff --git a/modules/imaging_browser/php/imagingbrowserrow.class.inc b/modules/imaging_browser/php/imagingbrowserrow.class.inc index fdc718a662b..633559e1280 100644 --- a/modules/imaging_browser/php/imagingbrowserrow.class.inc +++ b/modules/imaging_browser/php/imagingbrowserrow.class.inc @@ -89,6 +89,13 @@ class ImagingBrowserRow implements \LORIS\Data\DataInstance, return $this->DBRow['entityType'] === 'Scanner'; } + /** + * Check whether a user can access this data instance. + * + * @param \User $user The user whose access is being checked + * + * @return bool + */ public function isAccessibleBy(\User $user): bool { return \LORIS\StudyEntities\DataInstanceAccess::centerAndProjectMatch( diff --git a/modules/instrument_manager/php/instrumentrow.class.inc b/modules/instrument_manager/php/instrumentrow.class.inc index 018aeb08202..584bc042b2b 100644 --- a/modules/instrument_manager/php/instrumentrow.class.inc +++ b/modules/instrument_manager/php/instrumentrow.class.inc @@ -340,6 +340,13 @@ class InstrumentRow implements \LORIS\Data\DataInstance return $this->DBRow; } + /** + * Check whether a user can access this data instance. + * + * @param \User $user The user whose access is being checked + * + * @return bool + */ public function isAccessibleBy(\User $user): bool { return $user->hasAnyPermission(Instrument_Manager::PERMISSIONS); diff --git a/modules/issue_tracker/php/issuerow.class.inc b/modules/issue_tracker/php/issuerow.class.inc index 37e1004ecbb..b91edb662ab 100644 --- a/modules/issue_tracker/php/issuerow.class.inc +++ b/modules/issue_tracker/php/issuerow.class.inc @@ -68,6 +68,13 @@ class IssueRow implements return $this->Module; } + /** + * Check whether a user can access this data instance. + * + * @param \User $user The user whose access is being checked + * + * @return bool + */ public function isAccessibleBy(\User $user): bool { return \LORIS\StudyEntities\DataInstanceAccess::centerMatch( diff --git a/modules/issue_tracker/php/models/attachmentdto.class.inc b/modules/issue_tracker/php/models/attachmentdto.class.inc index 44b728aa154..294e2b1a363 100644 --- a/modules/issue_tracker/php/models/attachmentdto.class.inc +++ b/modules/issue_tracker/php/models/attachmentdto.class.inc @@ -60,6 +60,13 @@ class AttachmentDTO implements \LORIS\Data\DataInstance ]; } + /** + * Check whether a user can access this data instance. + * + * @param \User $user The user whose access is being checked + * + * @return bool + */ public function isAccessibleBy(\User $user): bool { return $user->hasAnyPermission( diff --git a/modules/media/php/mediafile.class.inc b/modules/media/php/mediafile.class.inc index 2c4868159bb..cb0c6d43ba0 100644 --- a/modules/media/php/mediafile.class.inc +++ b/modules/media/php/mediafile.class.inc @@ -89,6 +89,13 @@ class MediaFile implements \LORIS\Data\DataInstance, return 0; } + /** + * Check whether a user can access this data instance. + * + * @param \User $user The user whose access is being checked + * + * @return bool + */ public function isAccessibleBy(\User $user): bool { return \LORIS\StudyEntities\DataInstanceAccess::centerAndProjectMatch( diff --git a/modules/module_manager/php/modulerow.class.inc b/modules/module_manager/php/modulerow.class.inc index 6026e00e98b..523f7517c81 100644 --- a/modules/module_manager/php/modulerow.class.inc +++ b/modules/module_manager/php/modulerow.class.inc @@ -63,6 +63,13 @@ class ModuleRow implements \LORIS\Data\DataInstance ]; } + /** + * Check whether a user can access this data instance. + * + * @param \User $user The user whose access is being checked + * + * @return bool + */ public function isAccessibleBy(\User $user): bool { return $user->hasPermission('module_manager_edit'); diff --git a/modules/mri_violations/php/mriviolation.class.inc b/modules/mri_violations/php/mriviolation.class.inc index 981c829f4e4..2618f1f8be3 100644 --- a/modules/mri_violations/php/mriviolation.class.inc +++ b/modules/mri_violations/php/mriviolation.class.inc @@ -60,6 +60,13 @@ class MRIViolation implements \LORIS\Data\DataInstance return \ProjectID::singleton(intval($this->DBRow['Project'])); } + /** + * Check whether a user can access this data instance. + * + * @param \User $user The user whose access is being checked + * + * @return bool + */ public function isAccessibleBy(\User $user): bool { if ($user->hasPermission('violated_scans_view_allsites')) { diff --git a/modules/mri_violations/php/protocolcheckviolation.class.inc b/modules/mri_violations/php/protocolcheckviolation.class.inc index 7dc9b924767..4420cee2fb0 100644 --- a/modules/mri_violations/php/protocolcheckviolation.class.inc +++ b/modules/mri_violations/php/protocolcheckviolation.class.inc @@ -45,6 +45,13 @@ class ProtocolCheckViolation implements \LORIS\Data\DataInstance return \CenterID::singleton($this->DBRow['CenterID']); } + /** + * Check whether a user can access this data instance. + * + * @param \User $user The user whose access is being checked + * + * @return bool + */ public function isAccessibleBy(\User $user): bool { if ($user->hasPermission('violated_scans_view_allsites')) { diff --git a/modules/mri_violations/php/protocolviolation.class.inc b/modules/mri_violations/php/protocolviolation.class.inc index a81326e8f88..fd73f5cbf6a 100644 --- a/modules/mri_violations/php/protocolviolation.class.inc +++ b/modules/mri_violations/php/protocolviolation.class.inc @@ -32,6 +32,13 @@ class ProtocolViolation implements \LORIS\Data\DataInstance return $this->DBRow; } + /** + * Check whether a user can access this data instance. + * + * @param \User $user The user whose access is being checked + * + * @return bool + */ public function isAccessibleBy(\User $user): bool { return $user->hasAnyPermission( diff --git a/modules/schedule_module/php/schedulerow.class.inc b/modules/schedule_module/php/schedulerow.class.inc index 81f069a273c..a4a671d7fd6 100644 --- a/modules/schedule_module/php/schedulerow.class.inc +++ b/modules/schedule_module/php/schedulerow.class.inc @@ -127,6 +127,13 @@ class ScheduleRow implements \LORIS\Data\DataInstance, return $this->ProjectID; } + /** + * Check whether a user can access this data instance. + * + * @param \User $user The user whose access is being checked + * + * @return bool + */ public function isAccessibleBy(\User $user): bool { return \LORIS\StudyEntities\DataInstanceAccess::centerAndProjectMatch( diff --git a/modules/survey_accounts/php/surveyaccountsrow.class.inc b/modules/survey_accounts/php/surveyaccountsrow.class.inc index 05cd64f57a5..2bfc6dd8a5a 100644 --- a/modules/survey_accounts/php/surveyaccountsrow.class.inc +++ b/modules/survey_accounts/php/surveyaccountsrow.class.inc @@ -80,6 +80,13 @@ class SurveyAccountsRow implements \LORIS\Data\DataInstance, return $this->ProjectID; } + /** + * Check whether a user can access this data instance. + * + * @param \User $user The user whose access is being checked + * + * @return bool + */ public function isAccessibleBy(\User $user): bool { return \LORIS\StudyEntities\DataInstanceAccess::centerAndProjectMatch( diff --git a/modules/user_accounts/php/useraccountrow.class.inc b/modules/user_accounts/php/useraccountrow.class.inc index 6d80c24ed26..2148813ba8f 100644 --- a/modules/user_accounts/php/useraccountrow.class.inc +++ b/modules/user_accounts/php/useraccountrow.class.inc @@ -63,6 +63,13 @@ class UserAccountRow implements return $this->DBRow; } + /** + * Check whether a user can access this data instance. + * + * @param \User $user The user whose access is being checked + * + * @return bool + */ public function isAccessibleBy(\User $user): bool { return \LORIS\StudyEntities\DataInstanceAccess::centerAndProjectMatch( diff --git a/php/libraries/CohortData.class.inc b/php/libraries/CohortData.class.inc index 2b1c656e697..1dc3d577d48 100644 --- a/php/libraries/CohortData.class.inc +++ b/php/libraries/CohortData.class.inc @@ -73,6 +73,13 @@ class CohortData implements DataInstance ]; } + /** + * Check whether a user can access this data instance. + * + * @param \User $user The user whose access is being checked + * + * @return bool + */ public function isAccessibleBy(\User $user): bool { return $user->hasPermission('config'); diff --git a/src/Data/Models/DicomTarDTO.php b/src/Data/Models/DicomTarDTO.php index f4a9c881e53..1cee4c2f042 100644 --- a/src/Data/Models/DicomTarDTO.php +++ b/src/Data/Models/DicomTarDTO.php @@ -68,6 +68,7 @@ public function getTarname(): ?string { return $this->tarname; } + /** * Accessor for ArchiveLocation * @@ -120,6 +121,13 @@ function ($item) { ]; } + /** + * Check whether a user can access this data instance. + * + * @param \User $user The user whose access is being checked + * + * @return bool + */ public function isAccessibleBy(\User $user): bool { return true; diff --git a/src/Data/Models/ImageDTO.php b/src/Data/Models/ImageDTO.php index 604562c4c55..4bab8d98aaa 100644 --- a/src/Data/Models/ImageDTO.php +++ b/src/Data/Models/ImageDTO.php @@ -47,14 +47,14 @@ class ImageDTO implements /** * Constructor * - * @param ?int $fileid The FileID - * @param ?string $filename The image filename - * @param ?string $filelocation The image location - * @param ?string $outputtype The output type - * @param ?string $acquisitionprotocol The aquisition protocol - * @param ?string $filetype The file type + * @param ?int $fileid The FileID + * @param ?string $filename The image filename + * @param ?string $filelocation The image location + * @param ?string $outputtype The output type + * @param ?string $acquisitionprotocol The aquisition protocol + * @param ?string $filetype The file type * @param \CenterID $centerid The image session's centerid - * @param ?string $entitytype The image candidate's entity_type + * @param ?string $entitytype The image candidate's entity_type */ public function __construct( ?int $fileid, @@ -174,6 +174,13 @@ public function isPhantom(): bool return $this->entitytype === 'Scanner'; } + /** + * Check whether a user can access this data instance. + * + * @param \User $user The user whose access is being checked + * + * @return bool + */ public function isAccessibleBy(\User $user): bool { return \LORIS\StudyEntities\DataInstanceAccess::centerMatch( diff --git a/src/Data/Models/MRIUploadDTO.php b/src/Data/Models/MRIUploadDTO.php index aa3cfce1f22..fea2f04517f 100644 --- a/src/Data/Models/MRIUploadDTO.php +++ b/src/Data/Models/MRIUploadDTO.php @@ -168,6 +168,13 @@ public function jsonSerialize() : string return $this->toJSON(); } + /** + * Check whether a user can access this data instance. + * + * @param \User $user The user whose access is being checked + * + * @return bool + */ public function isAccessibleBy(\User $user): bool { return true; diff --git a/src/Data/Models/RecordingDTO.php b/src/Data/Models/RecordingDTO.php index 298156c1237..2167243b96b 100644 --- a/src/Data/Models/RecordingDTO.php +++ b/src/Data/Models/RecordingDTO.php @@ -47,14 +47,14 @@ class RecordingDTO implements /** * Constructor * - * @param ?int $fileid The FileID - * @param ?string $filename The image filename - * @param ?string $filelocation The image location - * @param ?string $outputtype The output type - * @param ?string $acquisitionmodality The aquisition modality - * @param ?string $filetype The file type + * @param ?int $fileid The FileID + * @param ?string $filename The image filename + * @param ?string $filelocation The image location + * @param ?string $outputtype The output type + * @param ?string $acquisitionmodality The aquisition modality + * @param ?string $filetype The file type * @param \CenterID $centerid The image session's centerid - * @param ?string $entitytype The image candidate's entity_type + * @param ?string $entitytype The image candidate's entity_type */ public function __construct( ?int $fileid, @@ -164,6 +164,13 @@ public function getCenterID(): \CenterID return $this->centerid; } + /** + * Check whether a user can access this data instance. + * + * @param \User $user The user whose access is being checked + * + * @return bool + */ public function isAccessibleBy(\User $user): bool { return \LORIS\StudyEntities\DataInstanceAccess::centerMatch( diff --git a/src/StudyEntities/DataInstanceAccess.php b/src/StudyEntities/DataInstanceAccess.php index d04d194bace..28a7b0a3707 100644 --- a/src/StudyEntities/DataInstanceAccess.php +++ b/src/StudyEntities/DataInstanceAccess.php @@ -26,7 +26,12 @@ public static function centerMatch(\User $user, object $resource): bool { $centerData = self::getMethodValue( $resource, - ['getCenterIDs', 'getCenterIds', 'getCenterID', 'getCenterId'] + [ + 'getCenterIDs', + 'getCenterIds', + 'getCenterID', + 'getCenterId', + ] ); if (!$centerData['found']) { return false; @@ -61,7 +66,12 @@ public static function projectMatch( ): bool { $projectData = self::getMethodValue( $resource, - ['getProjectIDs', 'getProjectIds', 'getProjectID', 'getProjectId'] + [ + 'getProjectIDs', + 'getProjectIds', + 'getProjectID', + 'getProjectId', + ] ); if (!$projectData['found']) { return false; @@ -130,13 +140,22 @@ private static function getMethodValue(object $resource, array $methods): array foreach ($methods as $method) { if (method_exists($resource, $method)) { try { - return ['found' => true, 'value' => $resource->$method()]; + return [ + 'found' => true, + 'value' => $resource->$method(), + ]; } catch (\Throwable) { - return ['found' => false, 'value' => null]; + return [ + 'found' => false, + 'value' => null, + ]; } } } - return ['found' => false, 'value' => null]; + return [ + 'found' => false, + 'value' => null, + ]; } /** diff --git a/test/unittests/DataInstanceAccessTest.php b/test/unittests/DataInstanceAccessTest.php index b599238d026..35863361ab1 100644 --- a/test/unittests/DataInstanceAccessTest.php +++ b/test/unittests/DataInstanceAccessTest.php @@ -5,15 +5,24 @@ /** * Unit tests for DataInstanceAccess helper methods. + * + * @license http://www.gnu.org/licenses/gpl-3.0.txt GPLv3 */ class DataInstanceAccessTest extends TestCase { /** + * Verify center matching works with a single center getter. + * * @return void */ public function testCenterMatchWithSingleCenter(): void { $resource = new class () { + /** + * Return center for this fake resource. + * + * @return \CenterID + */ public function getCenterID(): \CenterID { return \CenterID::singleton(1); @@ -33,12 +42,16 @@ public function getCenterID(): \CenterID } /** + * Verify center matching succeeds when one center matches in a list. + * * @return void */ public function testCenterMatchWithMultipleCenters(): void { $resource = new class () { /** + * Return center IDs for the anonymous test resource. + * * @return int[] */ public function getCenterIDs(): array @@ -59,11 +72,18 @@ public function getCenterIDs(): array } /** + * Verify project matching accepts scalar project IDs. + * * @return void */ public function testProjectMatchWithScalarProject(): void { $resource = new class () { + /** + * Return project for this fake resource. + * + * @return int + */ public function getProjectID(): int { return 3; @@ -83,11 +103,18 @@ public function getProjectID(): int } /** + * Verify allowNullProject behavior for null project IDs. + * * @return void */ public function testProjectMatchHandlesNullProjectByFlag(): void { $resource = new class () { + /** + * Return null project for this fake resource. + * + * @return ?\ProjectID + */ public function getProjectID(): ?\ProjectID { return null; @@ -105,16 +132,28 @@ public function getProjectID(): ?\ProjectID } /** + * Verify combined center/project matching requires both checks. + * * @return void */ public function testCenterAndProjectMatchRequiresBoth(): void { $resource = new class () { + /** + * Return center for this fake resource. + * + * @return \CenterID + */ public function getCenterID(): \CenterID { return \CenterID::singleton(1); } + /** + * Return project for this fake resource. + * + * @return \ProjectID + */ public function getProjectID(): \ProjectID { return \ProjectID::singleton(2); @@ -128,10 +167,14 @@ public function getProjectID(): \ProjectID $user->method('hasCenter')->willReturn(true); $user->method('hasProject')->willReturn(false); - $this->assertFalse(DataInstanceAccess::centerAndProjectMatch($user, $resource)); + $this->assertFalse( + DataInstanceAccess::centerAndProjectMatch($user, $resource) + ); } /** + * Verify missing center/project getters fail closed. + * * @return void */ public function testMissingGettersReturnFalse(): void @@ -151,16 +194,28 @@ public function testMissingGettersReturnFalse(): void } /** + * Verify invalid scalar IDs fail closed. + * * @return void */ public function testInvalidScalarIdentifiersReturnFalse(): void { $resource = new class () { + /** + * Return invalid center value for this fake resource. + * + * @return string + */ public function getCenterID(): string { return 'invalid-center'; } + /** + * Return invalid project value for this fake resource. + * + * @return string + */ public function getProjectID(): string { return 'invalid-project'; @@ -176,15 +231,24 @@ public function getProjectID(): string $this->assertFalse(DataInstanceAccess::centerMatch($user, $resource)); $this->assertFalse(DataInstanceAccess::projectMatch($user, $resource)); - $this->assertFalse(DataInstanceAccess::centerAndProjectMatch($user, $resource)); + $this->assertFalse( + DataInstanceAccess::centerAndProjectMatch($user, $resource) + ); } /** + * Verify thrown getter exceptions fail closed. + * * @return void */ public function testThrowingGetterReturnsFalse(): void { $resource = new class () { + /** + * Throw to simulate broken accessor implementation. + * + * @return ?\ProjectID + */ public function getProjectID(): ?\ProjectID { throw new \RuntimeException('broken getter'); diff --git a/test/unittests/DataInstanceBehaviorTest.php b/test/unittests/DataInstanceBehaviorTest.php index 23035c8bc1d..8543bc81c0f 100644 --- a/test/unittests/DataInstanceBehaviorTest.php +++ b/test/unittests/DataInstanceBehaviorTest.php @@ -1,8 +1,11 @@ getDataInstanceFiles(); + $files = $this->_getDataInstanceFiles(); $this->assertNotEmpty($files, 'No DataInstance classes found'); - $pattern = '/public\\s+function\\s+isAccessibleBy\\s*\\(\\s*\\\\User\\s+\\$user\\s*\\)\\s*:\\s*bool/s'; + $pattern = '/public\\s+function\\s+isAccessibleBy\\s*\\(' + . '\\s*\\\\User\\s+\\$user\\s*\\)\\s*:\\s*bool/s'; foreach ($files as $file) { $contents = file_get_contents($file); $this->assertNotFalse($contents, "Unable to read file: $file"); @@ -48,7 +51,7 @@ public function testAllDataInstancesDeclareIsAccessibleBy(): void * * @return string[] */ - private function getDataInstanceFiles(): array + private function _getDataInstanceFiles(): array { $roots = [ __DIR__ . '/../../src', @@ -56,8 +59,9 @@ private function getDataInstanceFiles(): array __DIR__ . '/../../modules', ]; - $matches = []; - $classPattern = '/class\\s+\\w+(?:\\s+extends\\s+[\\w\\\\]+)?\\s+implements[\\s\\S]{0,250}?DataInstance[\\s\\S]{0,120}?\\{/s'; + $matches = []; + $classPattern = '/class\\s+\\w+(?:\\s+extends\\s+[\\w\\\\]+)?\\s+' + . 'implements[\\s\\S]{0,250}?DataInstance[\\s\\S]{0,120}?\\{/s'; foreach ($roots as $root) { $iterator = new RecursiveIteratorIterator( @@ -65,14 +69,13 @@ private function getDataInstanceFiles(): array ); foreach ($iterator as $file) { $extension = strtolower($file->getExtension()); - if ( - !$file->isFile() + if (!$file->isFile() || !in_array($extension, ['php', 'inc'], true) ) { continue; } - $path = $file->getPathname(); + $path = $file->getPathname(); $contents = file_get_contents($path); if ($contents === false) { continue;