diff --git a/modules/acknowledgements/php/acknowledgementrow.class.inc b/modules/acknowledgements/php/acknowledgementrow.class.inc index ad51c9039e3..218a6aa2e00 100644 --- a/modules/acknowledgements/php/acknowledgementrow.class.inc +++ b/modules/acknowledgements/php/acknowledgementrow.class.inc @@ -49,4 +49,16 @@ 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 28997721e13..2fca86fe195 100644 --- a/modules/api/php/models/candidatesrow.class.inc +++ b/modules/api/php/models/candidatesrow.class.inc @@ -101,4 +101,19 @@ 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( + $user, + $this + ); + } } diff --git a/modules/api/php/models/projectimagesrow.class.inc b/modules/api/php/models/projectimagesrow.class.inc index c052e6f15b1..9617ee2baeb 100644 --- a/modules/api/php/models/projectimagesrow.class.inc +++ b/modules/api/php/models/projectimagesrow.class.inc @@ -106,4 +106,19 @@ 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( + $user, + $this + ); + } } diff --git a/modules/api/php/models/projectinstrumentsrow.class.inc b/modules/api/php/models/projectinstrumentsrow.class.inc index 9bbd429e22a..1dcd4e14aa4 100644 --- a/modules/api/php/models/projectinstrumentsrow.class.inc +++ b/modules/api/php/models/projectinstrumentsrow.class.inc @@ -119,4 +119,16 @@ 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 667eb0984bb..28e3c5d7216 100644 --- a/modules/api/php/models/projectrecordingsrow.class.inc +++ b/modules/api/php/models/projectrecordingsrow.class.inc @@ -91,4 +91,19 @@ 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( + $user, + $this + ); + } } diff --git a/modules/api/php/models/sitesrow.class.inc b/modules/api/php/models/sitesrow.class.inc index f83c78131d0..760daea6181 100644 --- a/modules/api/php/models/sitesrow.class.inc +++ b/modules/api/php/models/sitesrow.class.inc @@ -56,4 +56,19 @@ 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( + $user, + $this + ); + } } diff --git a/modules/battery_manager/php/test.class.inc b/modules/battery_manager/php/test.class.inc index 047351f02e8..4bc6c8a13c6 100644 --- a/modules/battery_manager/php/test.class.inc +++ b/modules/battery_manager/php/test.class.inc @@ -105,4 +105,19 @@ class Test implements 'Active' => $this->row['active'] ?? null, ]; } + + /** + * 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( + $user, + $this + ); + } } diff --git a/modules/behavioural_qc/php/models/behaviouraldto.class.inc b/modules/behavioural_qc/php/models/behaviouraldto.class.inc index 4f0314c5d6d..2b8f5ad6d59 100644 --- a/modules/behavioural_qc/php/models/behaviouraldto.class.inc +++ b/modules/behavioural_qc/php/models/behaviouraldto.class.inc @@ -164,4 +164,19 @@ class BehaviouralDTO implements \LORIS\Data\DataInstance, 'feedback_status' => $this->_feedback_status, ]; } + + /** + * 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( + $user, + $this + ); + } } diff --git a/modules/behavioural_qc/php/models/conflictsdto.class.inc b/modules/behavioural_qc/php/models/conflictsdto.class.inc index fa5a28c7593..d7b2a4a2cd0 100644 --- a/modules/behavioural_qc/php/models/conflictsdto.class.inc +++ b/modules/behavioural_qc/php/models/conflictsdto.class.inc @@ -148,4 +148,19 @@ class ConflictsDTO implements \LORIS\Data\DataInstance, 'commentID' => $this->_commentID, ]; } + + /** + * 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( + $user, + $this + ); + } } diff --git a/modules/behavioural_qc/php/models/incompletedto.class.inc b/modules/behavioural_qc/php/models/incompletedto.class.inc index 81153d9a414..817ef5aa20f 100644 --- a/modules/behavioural_qc/php/models/incompletedto.class.inc +++ b/modules/behavioural_qc/php/models/incompletedto.class.inc @@ -156,4 +156,19 @@ class IncompleteDTO implements \LORIS\Data\DataInstance, 'commentID' => $this->_commentID, ]; } + + /** + * 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( + $user, + $this + ); + } } diff --git a/modules/biobank/php/container.class.inc b/modules/biobank/php/container.class.inc index f1004114267..7d2848a2f01 100644 --- a/modules/biobank/php/container.class.inc +++ b/modules/biobank/php/container.class.inc @@ -558,4 +558,19 @@ 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( + $user, + $this + ); + } } diff --git a/modules/biobank/php/log.class.inc b/modules/biobank/php/log.class.inc index a848cd61502..4a49a5eec4e 100644 --- a/modules/biobank/php/log.class.inc +++ b/modules/biobank/php/log.class.inc @@ -191,5 +191,19 @@ 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( + $user, + $this + ); + } +} diff --git a/modules/biobank/php/pool.class.inc b/modules/biobank/php/pool.class.inc index aa45ba6947d..f146603722f 100644 --- a/modules/biobank/php/pool.class.inc +++ b/modules/biobank/php/pool.class.inc @@ -497,5 +497,19 @@ 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( + $user, + $this + ); + } +} diff --git a/modules/biobank/php/shipment.class.inc b/modules/biobank/php/shipment.class.inc index a0f2c5d8e4f..63e52f9ca2b 100644 --- a/modules/biobank/php/shipment.class.inc +++ b/modules/biobank/php/shipment.class.inc @@ -267,5 +267,19 @@ 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( + $user, + $this + ); + } +} diff --git a/modules/biobank/php/specimen.class.inc b/modules/biobank/php/specimen.class.inc index aae398e9ce8..8d5723d4d03 100644 --- a/modules/biobank/php/specimen.class.inc +++ b/modules/biobank/php/specimen.class.inc @@ -713,4 +713,19 @@ 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( + $user, + $this + ); + } } diff --git a/modules/candidate_list/php/candidatelistrow.class.inc b/modules/candidate_list/php/candidatelistrow.class.inc index a180670061d..630f65accd0 100644 --- a/modules/candidate_list/php/candidatelistrow.class.inc +++ b/modules/candidate_list/php/candidatelistrow.class.inc @@ -80,4 +80,19 @@ 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( + $user, + $this + ); + } } diff --git a/modules/conflict_resolver/php/models/resolveddto.class.inc b/modules/conflict_resolver/php/models/resolveddto.class.inc index dbe52d151e3..c8c60b45936 100644 --- a/modules/conflict_resolver/php/models/resolveddto.class.inc +++ b/modules/conflict_resolver/php/models/resolveddto.class.inc @@ -97,4 +97,19 @@ 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( + $user, + $this + ); + } } diff --git a/modules/conflict_resolver/php/models/unresolveddto.class.inc b/modules/conflict_resolver/php/models/unresolveddto.class.inc index 25a500ac67f..a34c8426c85 100644 --- a/modules/conflict_resolver/php/models/unresolveddto.class.inc +++ b/modules/conflict_resolver/php/models/unresolveddto.class.inc @@ -93,4 +93,19 @@ 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( + $user, + $this + ); + } } diff --git a/modules/data_release/php/datareleaserow.class.inc b/modules/data_release/php/datareleaserow.class.inc index f475e9cd065..957358d25c2 100644 --- a/modules/data_release/php/datareleaserow.class.inc +++ b/modules/data_release/php/datareleaserow.class.inc @@ -49,4 +49,16 @@ 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 374a6ef77cc..f1e36cfb8cc 100644 --- a/modules/datadict/php/datadictrow.class.inc +++ b/modules/datadict/php/datadictrow.class.inc @@ -50,4 +50,17 @@ 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') + || $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..f0fa9ed1c21 100644 --- a/modules/dicom_archive/php/dicomarchiverowwithoutsession.class.inc +++ b/modules/dicom_archive/php/dicomarchiverowwithoutsession.class.inc @@ -76,4 +76,21 @@ 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()) { + 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..8c8a63b0f63 100644 --- a/modules/dicom_archive/php/dicomarchiverowwithsession.class.inc +++ b/modules/dicom_archive/php/dicomarchiverowwithsession.class.inc @@ -113,4 +113,19 @@ 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( + $user, + $this + ); + } } diff --git a/modules/document_repository/php/docreporow.class.inc b/modules/document_repository/php/docreporow.class.inc index 9a9827fff04..234cc683f06 100644 --- a/modules/document_repository/php/docreporow.class.inc +++ b/modules/document_repository/php/docreporow.class.inc @@ -70,4 +70,19 @@ 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( + $user, + $this + ); + } } diff --git a/modules/electrophysiology_browser/php/electrophysiologybrowserrow.class.inc b/modules/electrophysiology_browser/php/electrophysiologybrowserrow.class.inc index f9c84977f4c..7c0e56e966d 100644 --- a/modules/electrophysiology_browser/php/electrophysiologybrowserrow.class.inc +++ b/modules/electrophysiology_browser/php/electrophysiologybrowserrow.class.inc @@ -79,4 +79,19 @@ 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( + $user, + $this + ); + } } diff --git a/modules/electrophysiology_browser/php/models/electrophysiofile.class.inc b/modules/electrophysiology_browser/php/models/electrophysiofile.class.inc index 327cc7014af..bc078579279 100644 --- a/modules/electrophysiology_browser/php/models/electrophysiofile.class.inc +++ b/modules/electrophysiology_browser/php/models/electrophysiofile.class.inc @@ -192,4 +192,35 @@ 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'); + 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..908b595c411 100644 --- a/modules/electrophysiology_uploader/php/electrophysiologyuploaderrow.class.inc +++ b/modules/electrophysiology_uploader/php/electrophysiologyuploaderrow.class.inc @@ -67,4 +67,19 @@ 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( + $user, + $this + ); + } } diff --git a/modules/examiner/php/examinerrow.class.inc b/modules/examiner/php/examinerrow.class.inc index 50c49a8640e..159c402b741 100644 --- a/modules/examiner/php/examinerrow.class.inc +++ b/modules/examiner/php/examinerrow.class.inc @@ -81,4 +81,19 @@ 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( + $user, + $this + ); + } } diff --git a/modules/genomic_browser/php/models/cnvdto.class.inc b/modules/genomic_browser/php/models/cnvdto.class.inc index af20931d6be..429cec2d6c0 100644 --- a/modules/genomic_browser/php/models/cnvdto.class.inc +++ b/modules/genomic_browser/php/models/cnvdto.class.inc @@ -258,4 +258,19 @@ 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( + $user, + $this + ); + } } diff --git a/modules/genomic_browser/php/models/filesdto.class.inc b/modules/genomic_browser/php/models/filesdto.class.inc index 206d8044ead..b8451c2c87e 100644 --- a/modules/genomic_browser/php/models/filesdto.class.inc +++ b/modules/genomic_browser/php/models/filesdto.class.inc @@ -95,4 +95,21 @@ class FilesDTO implements DataInstance 'Notes' => $this->_Notes, ]; } + + /** + * 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( + [ + '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..047c98a24ce 100644 --- a/modules/genomic_browser/php/models/gwasdto.class.inc +++ b/modules/genomic_browser/php/models/gwasdto.class.inc @@ -114,4 +114,21 @@ class GwasDTO implements DataInstance 'Pvalue' => $this->_Pvalue, ]; } + + /** + * 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( + [ + '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..70ebf865bfa 100644 --- a/modules/genomic_browser/php/models/methylationdto.class.inc +++ b/modules/genomic_browser/php/models/methylationdto.class.inc @@ -322,4 +322,19 @@ 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( + $user, + $this + ); + } } diff --git a/modules/genomic_browser/php/models/profiledto.class.inc b/modules/genomic_browser/php/models/profiledto.class.inc index b36c99ae24a..7974c7b2367 100644 --- a/modules/genomic_browser/php/models/profiledto.class.inc +++ b/modules/genomic_browser/php/models/profiledto.class.inc @@ -158,4 +158,19 @@ 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( + $user, + $this + ); + } } diff --git a/modules/genomic_browser/php/models/snpdto.class.inc b/modules/genomic_browser/php/models/snpdto.class.inc index 0bf8791fa00..e236a959a2f 100644 --- a/modules/genomic_browser/php/models/snpdto.class.inc +++ b/modules/genomic_browser/php/models/snpdto.class.inc @@ -309,4 +309,19 @@ 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( + $user, + $this + ); + } } diff --git a/modules/help_editor/php/helprow.class.inc b/modules/help_editor/php/helprow.class.inc index 4f1032ba7a0..b48f3476003 100644 --- a/modules/help_editor/php/helprow.class.inc +++ b/modules/help_editor/php/helprow.class.inc @@ -50,4 +50,16 @@ 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 7399c79577d..633559e1280 100644 --- a/modules/imaging_browser/php/imagingbrowserrow.class.inc +++ b/modules/imaging_browser/php/imagingbrowserrow.class.inc @@ -88,4 +88,19 @@ 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( + $user, + $this + ); + } } diff --git a/modules/instrument_manager/php/instrumentrow.class.inc b/modules/instrument_manager/php/instrumentrow.class.inc index c893bc1fa53..584bc042b2b 100644 --- a/modules/instrument_manager/php/instrumentrow.class.inc +++ b/modules/instrument_manager/php/instrumentrow.class.inc @@ -339,4 +339,16 @@ 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 ee15d780a45..b91edb662ab 100644 --- a/modules/issue_tracker/php/issuerow.class.inc +++ b/modules/issue_tracker/php/issuerow.class.inc @@ -67,4 +67,19 @@ 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( + $user, + $this + ); + } } diff --git a/modules/issue_tracker/php/models/attachmentdto.class.inc b/modules/issue_tracker/php/models/attachmentdto.class.inc index 6a6ac97d539..294e2b1a363 100644 --- a/modules/issue_tracker/php/models/attachmentdto.class.inc +++ b/modules/issue_tracker/php/models/attachmentdto.class.inc @@ -59,4 +59,22 @@ class AttachmentDTO implements \LORIS\Data\DataInstance 'mime_type' => $this->mime_type, ]; } + + /** + * 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( + [ + '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..cb0c6d43ba0 100644 --- a/modules/media/php/mediafile.class.inc +++ b/modules/media/php/mediafile.class.inc @@ -88,4 +88,19 @@ 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( + $user, + $this + ); + } } diff --git a/modules/module_manager/php/modulerow.class.inc b/modules/module_manager/php/modulerow.class.inc index 437035d9055..523f7517c81 100644 --- a/modules/module_manager/php/modulerow.class.inc +++ b/modules/module_manager/php/modulerow.class.inc @@ -62,4 +62,16 @@ class ModuleRow implements \LORIS\Data\DataInstance 'Active' => $this->Active, ]; } + + /** + * 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 7519702774e..2618f1f8be3 100644 --- a/modules/mri_violations/php/mriviolation.class.inc +++ b/modules/mri_violations/php/mriviolation.class.inc @@ -59,4 +59,30 @@ 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')) { + 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..4420cee2fb0 100644 --- a/modules/mri_violations/php/protocolcheckviolation.class.inc +++ b/modules/mri_violations/php/protocolcheckviolation.class.inc @@ -44,4 +44,21 @@ 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')) { + 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..fd73f5cbf6a 100644 --- a/modules/mri_violations/php/protocolviolation.class.inc +++ b/modules/mri_violations/php/protocolviolation.class.inc @@ -31,4 +31,21 @@ 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( + [ + '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..a4a671d7fd6 100644 --- a/modules/schedule_module/php/schedulerow.class.inc +++ b/modules/schedule_module/php/schedulerow.class.inc @@ -126,4 +126,19 @@ 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( + $user, + $this + ); + } } diff --git a/modules/survey_accounts/php/surveyaccountsrow.class.inc b/modules/survey_accounts/php/surveyaccountsrow.class.inc index ea1f96c44e8..2bfc6dd8a5a 100644 --- a/modules/survey_accounts/php/surveyaccountsrow.class.inc +++ b/modules/survey_accounts/php/surveyaccountsrow.class.inc @@ -79,4 +79,19 @@ 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( + $user, + $this + ); + } } diff --git a/modules/user_accounts/php/useraccountrow.class.inc b/modules/user_accounts/php/useraccountrow.class.inc index 9efa3edecc8..2148813ba8f 100644 --- a/modules/user_accounts/php/useraccountrow.class.inc +++ b/modules/user_accounts/php/useraccountrow.class.inc @@ -62,4 +62,19 @@ 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( + $user, + $this + ); + } } diff --git a/php/libraries/CohortData.class.inc b/php/libraries/CohortData.class.inc index a6fb5c6d166..1dc3d577d48 100644 --- a/php/libraries/CohortData.class.inc +++ b/php/libraries/CohortData.class.inc @@ -72,4 +72,16 @@ class CohortData implements DataInstance "RecruitmentTarget" => $this->recruitmentTarget ]; } + + /** + * 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/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..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 * @@ -119,4 +120,16 @@ function ($item) { 'series' => $series, ]; } + + /** + * 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 d0171601448..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, @@ -173,4 +173,19 @@ 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( + $user, + $this + ); + } } diff --git a/src/Data/Models/MRIUploadDTO.php b/src/Data/Models/MRIUploadDTO.php index d61c18c1113..fea2f04517f 100644 --- a/src/Data/Models/MRIUploadDTO.php +++ b/src/Data/Models/MRIUploadDTO.php @@ -167,4 +167,16 @@ 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 0bbe47f4609..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, @@ -163,4 +163,19 @@ 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( + $user, + $this + ); + } } diff --git a/src/StudyEntities/DataInstanceAccess.php b/src/StudyEntities/DataInstanceAccess.php new file mode 100644 index 00000000000..28a7b0a3707 --- /dev/null +++ b/src/StudyEntities/DataInstanceAccess.php @@ -0,0 +1,250 @@ +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; + } +} diff --git a/test/unittests/DataInstanceAccessTest.php b/test/unittests/DataInstanceAccessTest.php new file mode 100644 index 00000000000..35863361ab1 --- /dev/null +++ b/test/unittests/DataInstanceAccessTest.php @@ -0,0 +1,267 @@ +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)); + } + + /** + * 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 + { + 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)); + } + + /** + * 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; + } + }; + + $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)); + } + + /** + * 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; + } + }; + + $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)); + } + + /** + * 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); + } + }; + + $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) + ); + } + + /** + * Verify missing center/project getters fail closed. + * + * @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)); + } + + /** + * 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'; + } + }; + + $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) + ); + } + + /** + * 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'); + } + }; + + $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..8543bc81c0f --- /dev/null +++ b/test/unittests/DataInstanceBehaviorTest.php @@ -0,0 +1,170 @@ + 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)); + } + + /** + * Verify MRI violations deny center mismatch without all-sites permission. + * + * @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)); + } + + /** + * Verify protocol check violations allow rows with null center. + * + * @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)); + } + + /** + * Verify DICOM no-session rows are accessible by their creator. + * + * @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)); + } + + /** + * Verify DICOM no-session rows are accessible via explicit permissions. + * + * @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)); + } + + /** + * Verify DICOM no-session rows are denied without creator/permission. + * + * @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..f11a32d8561 --- /dev/null +++ b/test/unittests/DataInstanceContractTest.php @@ -0,0 +1,92 @@ +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; + } +}