From c2bea1b0e79e06d03cf812e883ddbd7228fbf449 Mon Sep 17 00:00:00 2001 From: Xavier Lecours Date: Wed, 20 Dec 2017 15:50:52 -0500 Subject: [PATCH 001/108] [RestAPI] - Initial copy of v0.0.2 (#3296) This makes a copy of htdocs/api/v0.0.2 as a starting point for further additions in the next iteration. It will make reviewing easier by allowing for diffs of changes to v0.0.3-dev --- docs/API/LorisRESTAPI_v0.0.3.md | 698 ++++++++++++++++++ htdocs/api/v0.0.3-dev/.htaccess | 41 + htdocs/api/v0.0.3-dev/APIBase.php | 266 +++++++ htdocs/api/v0.0.3-dev/Candidates.php | 247 +++++++ htdocs/api/v0.0.3-dev/Login.php | 205 +++++ htdocs/api/v0.0.3-dev/Projects.php | 102 +++ htdocs/api/v0.0.3-dev/SafeExitException.php | 42 ++ .../api/v0.0.3-dev/candidates/Candidate.php | 130 ++++ .../v0.0.3-dev/candidates/InstrumentData.php | 243 ++++++ .../api/v0.0.3-dev/candidates/Instruments.php | 101 +++ htdocs/api/v0.0.3-dev/candidates/Visit.php | 250 +++++++ .../v0.0.3-dev/candidates/visits/Images.php | 112 +++ .../candidates/visits/images/Image.php | 209 ++++++ .../visits/images/format/BrainBrowser.php | 147 ++++ .../candidates/visits/images/format/Raw.php | 95 +++ .../visits/images/format/Thumbnail.php | 118 +++ .../candidates/visits/images/headers/Full.php | 146 ++++ .../visits/images/headers/Headers.php | 135 ++++ .../visits/images/headers/Specific.php | 100 +++ .../candidates/visits/images/qc/QC.php | 237 ++++++ .../candidates/visits/qc/Imaging.php | 219 ++++++ .../v0.0.3-dev/projects/InstrumentForm.php | 92 +++ htdocs/api/v0.0.3-dev/projects/Project.php | 173 +++++ 23 files changed, 4108 insertions(+) create mode 100644 docs/API/LorisRESTAPI_v0.0.3.md create mode 100644 htdocs/api/v0.0.3-dev/.htaccess create mode 100644 htdocs/api/v0.0.3-dev/APIBase.php create mode 100644 htdocs/api/v0.0.3-dev/Candidates.php create mode 100644 htdocs/api/v0.0.3-dev/Login.php create mode 100644 htdocs/api/v0.0.3-dev/Projects.php create mode 100644 htdocs/api/v0.0.3-dev/SafeExitException.php create mode 100644 htdocs/api/v0.0.3-dev/candidates/Candidate.php create mode 100644 htdocs/api/v0.0.3-dev/candidates/InstrumentData.php create mode 100644 htdocs/api/v0.0.3-dev/candidates/Instruments.php create mode 100644 htdocs/api/v0.0.3-dev/candidates/Visit.php create mode 100644 htdocs/api/v0.0.3-dev/candidates/visits/Images.php create mode 100644 htdocs/api/v0.0.3-dev/candidates/visits/images/Image.php create mode 100644 htdocs/api/v0.0.3-dev/candidates/visits/images/format/BrainBrowser.php create mode 100644 htdocs/api/v0.0.3-dev/candidates/visits/images/format/Raw.php create mode 100644 htdocs/api/v0.0.3-dev/candidates/visits/images/format/Thumbnail.php create mode 100644 htdocs/api/v0.0.3-dev/candidates/visits/images/headers/Full.php create mode 100644 htdocs/api/v0.0.3-dev/candidates/visits/images/headers/Headers.php create mode 100644 htdocs/api/v0.0.3-dev/candidates/visits/images/headers/Specific.php create mode 100644 htdocs/api/v0.0.3-dev/candidates/visits/images/qc/QC.php create mode 100644 htdocs/api/v0.0.3-dev/candidates/visits/qc/Imaging.php create mode 100644 htdocs/api/v0.0.3-dev/projects/InstrumentForm.php create mode 100644 htdocs/api/v0.0.3-dev/projects/Project.php diff --git a/docs/API/LorisRESTAPI_v0.0.3.md b/docs/API/LorisRESTAPI_v0.0.3.md new file mode 100644 index 00000000000..688ceb7de6a --- /dev/null +++ b/docs/API/LorisRESTAPI_v0.0.3.md @@ -0,0 +1,698 @@ +# Loris API - v0.0.3 + +## 1.0 Overview + +This document specifies the Loris REST API. + +Any request sent to `$LorisRoot/api/$APIVERSION/$API_CALL` will return either a JSON object +or no data. The Loris API uses standard HTTP error codes and the body of any response will +either be empty or contain only a JSON object for any request. + +For brevity, the `$LorisRoot/api/$APIVERSION` is omitted from the definitions in this +document. This document specifies $APIVERSION v0.0.3 and it +MUST be included before the request in all requests. + +HTTP GET requests NEVER modify data. PUT, POST or PATCH requests MUST be used to modify +data as per their definitions in the HTTP/1.1 specification. Any methods not supported +will respond with a 405 Method Not Allowed response and an appropriate Allow header set (as +per HTTP documentation.) + +PUT requests either create or overwrite all data for a given resource (instrument/ +candidate/visit/etc.) Any fields not explicitly specified in the PUT request are nulled. + +PATCH requests are identical to PUT requests, but any fields not explicitly mentioned are +unmodified from their current value. + +All GET requests include an ETag header. If a PUT or PATCH request is sent and it does +not include an ETag, or the ETag does not match the currently existing ETag for that resource, +it will result in a 403 Forbidden response. PUT or POST requests used for the creation of resources +do not require ETags. + +DELETE is not supported on any resource defined in this API. + +# 1.1 Authentication + +If a user is logged in to Loris and can be authenticated using the standard session mechanism, +no further authentication is required. Requests will be evaluated as requests from that user, +so that standard Loris modules can simply use the API. + +If a user is not logged in to Loris (for instance, in a third party app or a CORS application), +they can be be authenticated using [JSON Web Tokens](https://jwt.io). + +The client should POST a request to /login with a payload of the form + +```js +{ + "username" : username, + "password" : password +} +``` + +If the username and password are valid, the API will respond with a 200 OK and payload +of the form + +```js +{ + "token" : /* JWT token */ +} +``` + +Otherwise, it will return a 401 Unauthorized response. + +If the token is returned, it should be included in an "Authorization: Bearer token" header +for any future requests to authenciate the request. + +# 2.0 Project API + +The Project API lives under the /projects portion of the API URL hierarchy. It is used to get +project specific settings or data. PUT and PATCH are not currently supported for the part of +the API living under /projects. + +``` +GET /projects +``` + +Will return a list of projects in this Loris instance. There is no corresponding PUT or PATCH +request. The JSON returned is of the form: + +```js +{ + "Projects" : { + "ProjectName1" : { + "useEDC" : boolean, + "PSCID" : PSCIDSettings + }, + "ProjectName2" : { + "useEDC" : boolean, + "PSCID" : PSCIDSettings + }, + ... +} +``` + +If the Loris instance does not use projects, the API will return a single project called "loris" +with the appropriate settings for the Loris instance. + +useEDC represents a boolean determining whether the EDC date should be included +in candidates returned by the API. + +PSCID represents a JSON object with the configuration settings for PSCIDs in this +project. + +It has the form: + +```js +{ + "Type" : "prompt|auto", + "Regex" : "/regex/" +} +``` + +Where regex is a regular expression that can be used to validate a PSCID for this project. + +If the type is "prompt", the user should be prompted to enter the PSCID for new candidates. +If the type is "auto", the server will automatically generate the PSCID. + +Note that sometimes in Loris configurations "Site" is a part of the PSCID. This will be +denoted by the string "SITE{1,1}" inside of the regex returned. This string should be replaced +by the 3 letter site alias before attempting to pass this regex to a regular expression parser +or it will result in false negatives. + + +``` +GET /projects/$ProjectName +``` + +Returns a 200 OK response if the project exists, and 404 Not Found if it does not (the same is +true of any portion of the API under /projects/$ProjectName.) + +The body of the request to /projects/$ProjectName will be an entity of the form: + +```js +{ + "Meta" : { + "Project" : "ProjectName" + }, + "Visits" : ["V1", "V2", ... ], + "Instruments" : ["InstrumentName", "InstrumentName2", ...], + "Candidates" : ["123543", "523234", ...] +} +``` + +``` +GET /projects/$ProjectName/instruments/ +``` + +Will return a JSON object of the form + +```js +{ + "Meta" : { + "Project" : "ProjectName" + }, + "Instruments": { + "InstrumentName" : { + "FullName" : "Long Name", + "Subgroup" : "Subgroup Name", + "DoubleDataEntryEnabled" : boolean + }, + "Instrument2" : { + "FullName" : "Long Name", + "Subgroup" : "Subgroup Name", + "DoubleDataEntryEnabled" : boolean + }, + ... + } +} +``` + +Where the InstrumentNames are the "Short Name" of all the instruments used/installed in this project. + +``` +GET /projects/$ProjectName/visits/ +``` + +Will return a JSON object of the form + +```js +{ + "Meta" : { + "Project" : "ProjectName" + }, + "Visits" : ["V1", "V2", ... ], +} +``` + +Where V1, V2, ... are the visits that may exist for this project + +``` +GET /projects/$ProjectName/candidates/ +``` + +will return a JSON object of the form + +```js +{ + "Meta" : { + "Project" : "ProjectName" + }, + "Candidates" : ["123456", "342332", ... ], +} +``` + +where 123456, 342332, etc are the candidates that exist for this project. + +## 2.2 Instrument Forms + +``` +GET /projects/$ProjectName/instruments/$InstrumentName +``` + +Will return a 200 response on success and 404 Not Found if $InstrumentName is not a +valid instrument for this instance of Loris. + +This will return a JSON representation of the instrument form. If available, rules and form will +be combined into a single JSON object. The format for the JSON returned is specified in the +accompanying InstrumentFormat.md and RulesFormat.md documents. The JSON document can be used +to render the form by a client. + +PUT and PATCH are not supported for instrument forms. + +Methods for getting/putting data into specific candidates are specified in section 3. + +# 3.0 Candidate API + +The /candidate portion of the API is used for retrieving and modifying candidate data and +data attached to a specific candidate or visit such as visits or instrument data. Portions +of this reference a CandidateObject. A CandidateObject is a JSON object of the form + +```js +{ + "CandID" : CandID, + "Project" : ProjectName, + "PSCID" : PSCID, + "Site" : Site, + "EDC" : "YYYY-MM-DD", + "DoB" : "YYYY-MM-DD", + "Gender" : "Male|Female" +} +``` + +representing a candidate in Loris. + +``` +GET /candidates/ +``` + +will return a JSON object of the form + +```js +{ + "Candidates" : [CandidateObject1, CandidateObject2, CandidateObject3, ...] +} +``` + +containing ALL candidates present in this Loris instance. + +A new candidate can be created by sending a POST request to /candidates. + +The body of the POST request should be a candidate key with a JSON object of the form: + +```js +{ + "Candidate" : { + "Project" : ProjectName, + "PSCID" : PSCID, + "EDC" : "YYYY-MM-DD", + "DoB" : "YYYY-MM-DD", + "Gender" : "Male|Female" + } +} +``` + +EDC is only required if useEDC is enabled for the project according to the +project settings. + +PSCID is only required if the generation type in the Loris config is set to +"prompt". + +The candidate will be created at the site of the user using the API's site. +A response code of 201 Created will be returned on success, 409 Conflict if +the PSCID already exists, and a 400 Bad Request if any data provided is invalid +(PSCID format, date format, gender something other than Male|Female, invalid project +name, etc). A successful POST request will return the CandID for the newly +created candidate. + +PUT / PATCH methods are not supported on /candidate in this +version of the Loris API. + +# 3.1 Specific Candidate + +If a GET request for a candidate is issued such as + +``` +GET /candidates/$CandID +``` + +A JSON object representing that candidate will be returned. + +The JSON object is of the form + +```js +{ + "Meta" : CandidateObject, + "Visits" : ["V1", "V2", ...] +} +``` + +where V1, V2, etc are the visit labels that are registered for this +candidate. + +PUT / PATCH are not supported for candidates in this version of the +API. + +It will return a 200 OK on success, a 404 if the candidate does not exist, and +a 400 Bad Request if the CandID is invalid (not a 6 digit integer). The same is +true of all of the API hierarchy under /candidates/$CandID. + +### 3.2 Getting Candidate visit data + +A GET request of the form + +``` +GET /candidates/$CandID/$VisitLabel +``` + +Will return a JSON object of the metadata for that candidate's visit. + +The JSON object is of the form: + +```js +{ + "Meta" : { + "CandID" : CandID, + "Visit" : VisitLabel, + "Battery" : "NameOfSubproject" + }, + "Stages" : { + "Screening" : { + "Date" : "YYYY-MM-DD", + "Status" : "Pass|Failure|Withdrawal|In Progress" + }, + "Visit" : { + "Date" : "YYYY-MM-DD", + "Status" : "Pass|Failure|Withdrawal|In Progress" + }, + "Approval" : { + "Date" : "YYYY-MM-DD", + "Status" : "Pass|Failure|Withdrawal|In Progress" + } + } +} +``` + +A PUT of the same format but with only the Meta fields will create the VisitLabel +for this candidate, in an unstarted stage if the Visit label provided is valid. + +PATCH is not supported for Visit Labels. + +It will return a 404 Not Found if the visit label does not exist for this candidate +(as well as anything under the /candidates/$CandID/$VisitLabel hierarchy) + +Any of the Stages may not be present in the returned result if the stage has not +started yet or is not enabled for this project (ie. if useScreening is false in +Loris, or Approval has not occured) + +### 3.3 Candidate Instruments +``` +GET /candidates/$CandID/$VisitLabel/instruments +``` + +Will return a JSON object of the form. + +```js +{ + "Meta" : { + "CandID" : CandID, + "Visit" : VisitLabel + }, + "Instruments" : [ "InstrumentName", "AnotherInstrument", ...] +} +``` + +Where the instruments array represents the instruments that were administered for that +candidate at that visit. InstrumentNames are the short names and the forms for them +SHOULD all be retrievable through the `project` portion of the API. + +PUT / PATCH / POST are not currently supported for candidate instruments. + +### 3.3 The Candidate Instrument Data + +``` +GET /candidates/$CandID/$VisitLabel/instruments/$InstrumentName[/dde] +PUT /candidates/$CandID/$VisitLabel/instruments/$InstrumentName[/dde] +PATCH /candidates/$CandID/$VisitLabel/instruments/$InstrumentName[/dde] +``` + +These will retrieve or modifiy the data for $InstrumentName. If /dde is present, the double data +entry form of the data will be retrieved/modified. If absent, the "single data entry" version +of the form is used instead. + +The format returned by a GET request is a JSON document of the form: + +```js +{ + "Meta" : { + "Instrument" : $InstrumentName, + "Visit" : $VisitLabel, + "Candidate" : $CandID, + "DDE" : boolean + }, + "$InstrumentName" : { + "FieldName1" : "Value1", + "FieldName2" : "Value2", + ... + } +} +``` + +Including the values of ALL fields for this instrument, including score field values. + +The body of a PUT request to the same URL MUST contain a JSON object of the same format. Data PUT +to the URL SHOULD contain all fields with data entry. The server will null the data for keys not +specified. A PUT request MAY not specify score columns that will be calculated/overwriten by +server-side scoring. If the client attempted to score fields client-side and the value passed by the PUT +request conflict with the server-side calculation of the score, the server-side calculation will win. +Any keys specified in the document PUT that do not match a corresponding field in the form MAY be ignored. + +The specification for PATCH request is similar to a PUT request, with the exception that any +fields not specified MUST be unmodified by the server rather than nulled. In most cases a series +of PATCH requests SHOULD be used rather than a single PUT request for a client with pagination. + +A 200 OK will be returned on success, and a 404 Not Found if $InstrumentName is not a valid instrument installed in this Loris instance. + +### 3.3.1 Instrument flags +``` +GET /candidates/$CandID/$VisitLabel/instruments/$InstrumentName[/dde]/flags +PUT /candidates/$CandID/$VisitLabel/instruments/$InstrumentName[/dde]/flags +PATCH /candidates/$CandID/$VisitLabel/instruments/$InstrumentName[/dde]/flags +``` + +This URL is used to GET and modify flags for an instrument. The standard GET/PUT/PATCH +rules apply. However, PATCH and PUT requests MUST include the "Meta" attribute and all +fields in it MUST be specified and match the values in the URL, otherwise a "400 Bad request" +error is returned and no data is modified. Like instruments, the [/dde] component is optional +and used to differentiate single data entry and double data entry flags. + +The "Validity" flag may be missing, if the ValidityEnabled flag is not true for this instrument. + +The format of the JSON object for these URLS is: + +```js +{ + "Meta" : { + "Candidate" : CandID, + "Visit" : VisitLabel, + "Instrument" : InstrumentName, + "DDE" : boolean + }, + "Flags" : { + "Data_entry" : "In Progress|Complete", + "Administration" : "None|Partial|All", + "Validity" : "Questionable|Invalid|Valid" + } +} +``` + +# 4.0 Imaging Data + +The imaging data mostly lives in the `/candidates/$CandID/$Visit` portion of the REST API +namespaces, but is defined in a separate section of this document for clarity purposes. + +## 4.1 Candidate Images +``` +GET /candidates/$CandID/$Visit/images +``` + +A GET request to `/candidates/$CandID/$Visit/images` will return a JSON object of +all the images which have been acquired for that visit. It will return an object of +the form: + +```js +{ + "Meta" : { + "CandID" : $CandID, + "Visit" : $VisitLabel, + }, + "Files" : [{ + "OutputType" : "native", + "Filename" : "abc.mnc", + "AcquisitionType" : "t1w/t2w/etc", + }, /* More files */] +} +``` + +## 4.2 Session Imaging QC +``` +GET /candidates/$CandID/$Visit/qc/imaging +PUT /candidates/$CandID/$Visit/qc/imaging +``` + +To retrieve the session level imaging QC data for a visit, a request can +be made `/candidates/$CandID/$Visit/qc/imaging`. It will return a JSON object +of the form + +```js +{ + "Meta" : { + "CandID" : $CandID, + "Visit" : $VisitLabel + }, + "SessionQC" : "Pass|Fail" + "Pending" : boolean +} +``` + +A PUT to the same location will update the QC information. + +## 4.3 Image Level Data +``` +GET /candidates/$CandID/$VisitLabel/images/$Filename +``` + +Returns raw file with the appropriate MimeType headers for each Filename retrieved from +`/candidates/$CandID/$Visit/images`. + +Only `GET` is currently supported, but future versions of this API may include `PUT` +support to insert new (or processed) data into LORIS. + +## 4.3.1 Image Level QC Data +``` +GET /candidates/$CandID/$VisitLabel/images/$Filename/qc +PUT /candidates/$CandID/$VisitLabel/images/$Filename/qc +``` + +Returns file level QC information. It will return a JSON object of the form + +```js +{ + "Meta" : { + "CandID" : $CandID, + "Visit" : $VisitLabel, + "File" : $Filename + }, + "QC" : "Pass|Fail", + "Selected" : boolean +} +``` + +`PUT` requests to the same URL will update the QC information. + +## 4.4 Alternate formats + +There are occasions where you may want to retrieve a file in a different format +than it is stored in LORIS. This can be achieved by adding `/format/$FormatType` +to the URL in the API. Currently supported other formats are below. Other formats +may be added in a future version of this API. + +An attempt to convert an image to an unsupported format may result in a + `415 Unsupported Media Type` HTTP error. + +### 4.4.1 Raw Format +``` +GET /candidates/$CandID/$VisitLabel/images/$Filename/format/raw +``` + +This will return the data in raw format (ie. the output of mnc2raw) + +### 4.4.2 BrainBrowser Format +``` +GET /candidates/$CandID/$VisitLabel/images/$Filename/format/brainbrowser +``` + +This (in combination with raw) will let you extract the headers in a JSON +format that BrainBrowser can load. It will return a JSON object of the format + +```js +{ + "xspace": { + "start":"", + "space_length":"", + "step":""}, + "yspace": { + "start":"", + "space_length":"", + "step":"" + }, + "zspace": { + "start":"", + "space_length":"", + "step":"" + }, + "order":["xspace","zspace","yspace"] +} +``` + +### 4.4.3 Thumbnail Format +``` +GET /candidates/$CandID/$VisitLabel/images/$Filename/format/thumbnail +``` + +This will return a JPEG image that can be used as a thumbnail to represent this +imaging acquisition statically (such as in the LORIS imaging browser.) + +### 4.5 Image Headers +The LORIS API allows you to extract headers from the images in a RESTful manner. +The following methods are defined: + +### 4.5.1 Header Summary +``` +GET /candidates/$CandID/$VisitLabel/images/$Filename/headers +``` + +This will return a JSON summary of the important headers for this filename. It +will return a JSON object of the form: + +```js +{ + "Meta" : { + "CandID" : $CandID, + "Visit" : $VisitLabel, + "File" : $Filename + }, + "Physical" : { + "TE" : "", + "TR" : "", + "TI" : "", + "SliceThickness" : "", + }, + "Description" : { + "SeriesName" : "", + "SeriesDescription" : "" + } + "Dimensions" : { + "XSpace" : { + "Length" : "", + "StepSize" : "" + }, + "YSpace" : { + "Length" : "", + "StepSize" : "" + }, + "ZSpace" : { + "Length" : "", + "StepSize" : "" + }, + "TimeDimension" : { + "Length" : "", + "StepSize" : "" + } + } +} +``` + +All of the dimensions are optional and may not exist for any given +file (for instance, a 3D image will not have a time dimension.) + +### 4.5.2 Complete Headers +``` +GET /candidates/$CandID/$VisitLabel/images/$Filename/headers/full +``` + +This will return a JSON object with ALL headers for this acquisition. + +The JSON will be of the form: + +```js +{ + "Meta" : { + "CandID" : $CandID, + "Visit" : $VisitLabel, + "File" : $Filename + }, + "Headers" : { + "dicomheader" : "value", + /* more headers ... */ + } +} +``` + +### 4.5.3 Specific Header +``` +GET /candidates/$CandID/$VisitLabel/images/$Filename/headers/$HeaderName +``` + +This will return a JSON object that extracts one specific header from $Filename. + +The JSON object is of the form: +```js +{ + "Meta" : { + "CandID" : $CandID, + "Visit" : $VisitLabel, + "File" : $Filename, + "Header" : $HeaderName + }, + "Value" : string +} +``` diff --git a/htdocs/api/v0.0.3-dev/.htaccess b/htdocs/api/v0.0.3-dev/.htaccess new file mode 100644 index 00000000000..cb63bd75aab --- /dev/null +++ b/htdocs/api/v0.0.3-dev/.htaccess @@ -0,0 +1,41 @@ +Header always set Access-Control-Allow-Origin "*" +Header always set Access-Control-Allow-Headers "*" +RewriteEngine on +Options -Indexes + +# pass-through if another rewrite rule has been applied already +RewriteCond %{ENV:REDIRECT_STATUS} 200 +RewriteRule ^ - [L] + +RewriteRule ^login Login.php?PrintLogin=true [L] +# Projects API rewrite rules +RewriteRule ^projects/([a-zA-Z0-9_\w\s.]+)/instruments/([a-zA-Z0-9_.]+)$ projects/InstrumentForm.php?Instrument=$2&PrintInstrumentForm=true [L] +RewriteRule ^projects/([a-zA-Z0-9_\w\s.]+)/visits(/*)$ projects/Project.php?Project=$1&Visits=true&PrintProjectJSON=true [L] +RewriteRule ^projects/([a-zA-Z0-9_\w\s.]+)/candidates(/*)$ projects/Project.php?Project=$1&Candidates=true&PrintProjectJSON=true [L] +RewriteRule ^projects/([a-zA-Z0-9_\w\s.]+)/instruments(/*)$ projects/Project.php?Project=$1&Instruments=true&PrintProjectJSON=true&InstrumentDetails=true [L] +RewriteRule ^projects/([a-zA-Z0-9_\w\s.]+)(/*)$ projects/Project.php?Project=$1&Instruments=true&Visits=true&Candidates=true&PrintProjectJSON=true [L] + +RewriteRule ^projects(/*)$ Projects.php?PrintProjects=true [L] + +# Candidates API rewrite rules + +RewriteRule ^candidates(/*)$ Candidates.php?PrintCandidates=true [L] +RewriteRule ^candidates/([0-9]+)(/*)$ candidates/Candidate.php?CandID=$1&PrintCandidate=true [L] +RewriteRule ^candidates/([0-9]+)/([a-zA-Z0-9_.]+)(/*)$ candidates/Visit.php?CandID=$1&VisitLabel=$2&PrintVisit=true [L] +RewriteRule ^candidates/([0-9]+)/([a-zA-Z0-9_.]+)/instruments$ candidates/Instruments.php?CandID=$1&VisitLabel=$2&NoCandidate=true&PrintInstruments=true [L] +RewriteRule ^candidates/([0-9]+)/([a-zA-Z0-9_.]+)/instruments/([a-zA-Z0-9_.]+)$ candidates/InstrumentData.php?Instrument=$3&Visit=$2&CandID=$1&PrintInstrumentData=true [L] +RewriteRule ^candidates/([0-9]+)/([a-zA-Z0-9_.]+)/instruments/([a-zA-Z0-9_.]+)/dde$ candidates/InstrumentData.php?Instrument=$3&Visit=$2&CandID=$1&DDE=true&PrintInstrumentData=true [L] +RewriteRule ^candidates/([0-9]+)/([a-zA-Z0-9_.]+)/instruments/([a-zA-Z0-9_.]+)/flags$ candidates/InstrumentData.php?Instrument=$3&Visit=$2&CandID=$1&flags=true&PrintInstrumentData=true [L] +RewriteRule ^candidates/([0-9]+)/([a-zA-Z0-9_.]+)/instruments/([a-zA-Z0-9_.]+)/dde/flags$ candidates/InstrumentData.php?Instrument=$3&Visit=$2&CandID=$1&DDE=true&flags=true&PrintInstrumentData=true [L] + +## Imaging API related URLs +RewriteRule ^candidates/([0-9]+)/([a-zA-Z0-9_.]+)/images$ candidates/visits/Images.php?CandID=$1&VisitLabel=$2&PrintImages=true [L] +RewriteRule ^candidates/([0-9]+)/([a-zA-Z0-9_.]+)/qc/imaging$ candidates/visits/qc/Imaging.php?CandID=$1&VisitLabel=$2&PrintQC=true [L] +RewriteRule ^candidates/([0-9]+)/([a-zA-Z0-9_.]+)/images/([a-zA-Z0-9_.]+)/qc$ candidates/visits/images/qc/Qc.php?CandID=$1&VisitLabel=$2&Filename=$3&PrintImageQC=true [L] +RewriteRule ^candidates/([0-9]+)/([a-zA-Z0-9_.]+)/images/([a-zA-Z0-9_.]+)/format/raw$ candidates/visits/images/format/Raw.php?CandID=$1&VisitLabel=$2&Filename=$3&PrintRawFormat=true [L] +RewriteRule ^candidates/([0-9]+)/([a-zA-Z0-9_.]+)/images/([a-zA-Z0-9_.]+)/format/thumbnail$ candidates/visits/images/format/Thumbnail.php?CandID=$1&VisitLabel=$2&Filename=$3&PrintThumbnailFormat=true [L] +RewriteRule ^candidates/([0-9]+)/([a-zA-Z0-9_.]+)/images/([a-zA-Z0-9_.]+)$ candidates/visits/images/Image.php?CandID=$1&VisitLabel=$2&Filename=$3&PrintImageData=true [L] +RewriteRule ^candidates/([0-9]+)/([a-zA-Z0-9_.]+)/images/([a-zA-Z0-9_.]+)/headers$ candidates/visits/images/headers/Headers.php?CandID=$1&VisitLabel=$2&Filename=$3&PrintHeadersSummary=true [L] +RewriteRule ^candidates/([0-9]+)/([a-zA-Z0-9_.]+)/images/([a-zA-Z0-9_.]+)/headers/full$ candidates/visits/images/headers/Full.php?CandID=$1&VisitLabel=$2&Filename=$3&PrintHeadersFull=true [L] +RewriteRule ^candidates/([0-9]+)/([a-zA-Z0-9_.]+)/images/([a-zA-Z0-9_.]+)/headers/(.*)$ candidates/visits/images/headers/Specific.php?CandID=$1&VisitLabel=$2&Filename=$3&Header=$4&PrintSpecificHeader=true [L] +RewriteRule ^candidates/([0-9]+)/([a-zA-Z0-9_.]+)/images/([a-zA-Z0-9_.]+)$ candidates/visits/images/Image.php?CandID=$1&VisitLabel=$2&Filename=$3&PrintImageData=true [L] diff --git a/htdocs/api/v0.0.3-dev/APIBase.php b/htdocs/api/v0.0.3-dev/APIBase.php new file mode 100644 index 00000000000..dde294e8059 --- /dev/null +++ b/htdocs/api/v0.0.3-dev/APIBase.php @@ -0,0 +1,266 @@ + + * @license http://www.gnu.org/licenses/gpl-3.0.txt GPLv3 + * @link https://www.github.com/aces/Loris/ + */ +namespace Loris\API; +require_once __DIR__ . '/SafeExitException.php'; +require_once __DIR__ . '/../../../vendor/autoload.php'; + +/** + * Base class to handle requests to the Loris API and perform + * validation common to all API requests. + * + * @category Main + * @package API + * @author Dave MacFarlane + * @license http://www.gnu.org/licenses/gpl-3.0.txt GPLv3 + * @link https://www.github.com/aces/Loris/ + */ +abstract class APIBase +{ + var $DB; + var $client; + var $JSON; + var $AllowedMethods = []; + var $AutoHandleRequestDelegation = true; + var $HTTPMethod; + + var $Factory; + var $Headers; + + /** + * Constructor to handle basic validation + * + * @param string $method The HTTP request method + */ + function __construct($method) + { + if (empty($this->AllowedMethods)) { + $this->AllowedMethods = ['GET']; + } + // Verify that method is allowed for this type of request. + if (!in_array($method, $this->AllowedMethods)) { + $this->header("HTTP/1.1 405 Method Not Allowed"); + $this->header("Allow: " . join(", ", $this->AllowedMethods)); + $this->safeExit(0); + } + + $this->HTTPMethod = $method; + + //Load config file and ensure paths are correct + set_include_path( + get_include_path() . ":" . + __DIR__ . "/../../../php/libraries" + ); + include_once 'NDB_Client.class.inc'; + + $this->Factory = \NDB_Factory::singleton(); + $this->client = new \NDB_Client(); + if (defined("UNIT_TESTING")) { + // Unit tests are run from the command line, so avoid all + // the session stuff if we're in a unit test + $this->client->makeCommandLine(); + } + $this->client->initialize(__DIR__ . "/../../../project/config.xml"); + + if (!defined("UNIT_TESTING")) { + + if (!$this->client->isLoggedIn()) { + $this->header("HTTP/1.1 401 Unauthorized"); + $this->error("User not authenticated"); + $this->safeExit(0); + } + } + + $this->DB = $this->Factory->database(); + + if ($this->AutoHandleRequestDelegation) { + $this->handleRequest(); + } + } + + /** + * Handles a request by delegating to the appropriate + * handle method + * + * @return none + */ + function handleRequest() + { + $method = $this->HTTPMethod; + + switch($this->HTTPMethod) { + case 'GET': + $this->handleETag(); + $this->handleGET(); + break; + case 'PUT': + $this->handlePUT(); + break; + case 'POST': + $this->handlePOST(); + break; + case 'OPTIONS': + $this->handleOPTIONS(); + break; + case 'PATCH': + $this->handlePATCH(); + break; + + } + } + + /** + * Determine calculate the ETag for this resource and abort + * early if the client already has it. + * + * @return None + */ + function handleETag() + { + session_cache_limiter('private'); + $ETag = $this->calculateETag(); + + $this->header("ETag: $ETag"); + if (isset($_SERVER['HTTP_IF_NONE_MATCH']) + && $_SERVER['HTTP_IF_NONE_MATCH'] === $ETag + ) { + $this->header("HTTP/1.1 304 Not Modified"); + $this->safeExit(0); + } + + } + + /** + * Calculate the ETag for this resource + * + * @return string an ETag for this resource + */ + abstract function calculateETag(); + + /** + * Handle a GET request + * + * @return none + */ + function handleGET() + { + + } + + /** + * Handle a PUT request + * + * @return none + */ + function handlePUT() + { + $this->header("HTTP/1.1 501 Not Implemented"); + $this->safeExit(0); + } + + /** + * Handle a POST request + * + * @return none + */ + function handlePOST() + { + $this->header("HTTP/1.1 501 Not Implemented"); + $this->safeExit(0); + } + + /** + * Handle a PATCH request + * + * @return none + */ + function handlePATCH() + { + $this->header("HTTP/1.1 501 Not Implemented"); + $this->safeExit(0); + } + + /** + * Handle a OPTIONS request + * + * @return none + */ + function handleOPTIONS() + { + $this->Header( + "Access-Control-Allow-Methods: ". + join($this->AllowedMethods, ",") + ); + $this->safeExit(0); + } + + /** + * Encodes this object as a string of valid JSON + * + * @return string encoding of JSON + */ + function toJSONString() + { + return json_encode($this->JSON); + } + + /** + * Print an error message to the client + * + * @param string $msg The error message to display + * + * @return none + */ + function error($msg) + { + print json_encode(["error" => $msg]); + } + + /** + * Send a header to the client, or put it in an array + * to evaluate in unit testing if UNIT_TESTING is defined + * + * @param string $head The header to send to the client + * + * @return none + */ + function header($head) + { + if (defined("UNIT_TESTING")) { + $this->Headers[] = $head; + } else { + header($head); + } + } + + /** + * Exits the program in a way that is safe for unit testing + * + * @param integer $code The program exit code + * + * @return none, but exits the running program + */ + function safeExit($code) + { + if (defined("UNIT_TESTING")) { + throw new SafeExitException( + "Aborting test with code $code", + $code, + $this + ); + } else { + exit($code); + } + } +} +?> diff --git a/htdocs/api/v0.0.3-dev/Candidates.php b/htdocs/api/v0.0.3-dev/Candidates.php new file mode 100644 index 00000000000..d2e0a91be2d --- /dev/null +++ b/htdocs/api/v0.0.3-dev/Candidates.php @@ -0,0 +1,247 @@ + + * @license http://www.gnu.org/licenses/gpl-3.0.txt GPLv3 + * @link https://www.github.com/aces/Loris/ + */ +namespace Loris\API; +set_include_path(get_include_path() . ":" . __DIR__); +require_once 'APIBase.php'; + +/** + * Class to handle a request to the candidates portion of the Loris API + * + * @category Main + * @package API + * @author Dave MacFarlane + * @license http://www.gnu.org/licenses/gpl-3.0.txt GPLv3 + * @link https://www.github.com/aces/Loris/ + */ +class Candidates extends APIBase +{ + var $RequestData; + + /** + * Create a Candidates request handler + * + * @param string $method The HTTP request method of the request + * @param array $data The data that was POSTed to the request + */ + public function __construct($method, $data=null) + { + $this->AllowedMethods = [ + 'GET', + 'POST', + ]; + $this->RequestData = $data; + + parent::__construct($method); + } + + /** + * Calculate an ETag by taking a hash of the number of candidates in the + * database and the time of the most recently changed one. + * + * @return string An ETag for ths candidates object + */ + function calculateETag() + { + $ETagCriteria = $this->DB->pselectRow( + "SELECT MAX(TestDate) as Time, + COUNT(DISTINCT CandID) as NumCandidates + FROM candidate WHERE Active='Y'", + array() + ); + return md5( + 'Candidates:' + . $ETagCriteria['Time'] + . ':' . $ETagCriteria['NumCandidates'] + ); + } + /** + * Handles a candidates GET request + * + * @return none, but populates $this->JSON + */ + public function handleGET() + { + $candidates = $this->DB->pselect( + "SELECT CandID, ProjectID, PSCID, s.Alias as Site, + EDC, DoB, Gender + FROM candidate c JOIN psc s on (s.CenterID=c.CenterID) + WHERE Active='Y' + ", + [] + ); + + $projects = \Utility::getProjectList(); + $candValues = array_map( + function ($row) use ($projects) { + $row['Project'] = isset($projects[$row['ProjectID']]) + ? $projects[$row['ProjectID']] + : "loris"; + unset($row['ProjectID']); + return $row; + }, + $candidates + ); + + $this->JSON = ["Candidates" => $candValues]; + } + + /** + * Handles a candidates POST request to validate data and if everything + * is valid, create the candidate + * + * @return none, but populates $this->JSON and writes to DB + */ + public function handlePOST() + { + $data = $this->RequestData; + if ($data === null) { + $this->header("HTTP/1.1 400 Bad Request"); + $this->error("Can't parse data"); + $this->safeExit(0); + } + + if (!isset($data['Candidate'])) { + $this->header("HTTP/1.1 400 Bad Request"); + $this->error("There is no Candidate object in the POST data"); + $this->safeExit(0); + } + + // This version of the API does not handle candidate creation + // when users are at multiple sites + $user = \User::singleton(); + $centerIDs = $user->getCenterIDs(); + $num_sites = count($centerIDs); + + if ($num_sites == 0) { + $this->header("HTTP/1.1 401 Unauthorized"); + $this->error("You are not affiliated with any site"); + $this->safeExit(0); + } + + if ($num_sites > 1) { + $this->header("HTTP/1.1 501 Not Implemented"); + $this->error( + "This API version does not support candidate creation " . + "by users with multiple site affiliations. This will be ". + "implemented in a future release." + ); + $this->safeExit(0); + } + + $centerID = $centerIDs[0]; + $this->verifyField($data, 'Gender', ['Male', 'Female']); + $this->verifyField($data, 'DoB', 'YYYY-MM-DD'); + + //Candidate::createNew + try { + $candid = $this->createNew( + $centerID, + $data['Candidate']['DoB'], + $data['Candidate']['EDC'], + $data['Candidate']['Gender'], + $data['Candidate']['PSCID'] + ); + + } catch(\LorisException $e) { + $this->header("HTTP/1.1 500 Internal Server Error"); + $this->error("Candidate can't be created"); + $this->safeExit(0); + } + + if (isset($data['Candidate']['Project'])) { + $projectName = $data['Candidate']['Project']; + $project = \Project::singleton($projectName); + if (!empty($project)) { + \Candidate::singleton($candid)->setData( + array('ProjectID' => $project->getId()) + ); + } + } + + $this->header("HTTP/1.1 201 Created"); + $this->JSON = [ + 'Meta' => ["CandID" => $candid], + ]; + } + + /** + * Verifies that the field POSTed to the URL is valid. + * + * @param array $data The data that was posted + * @param string $field The field to be validated in $data + * @param mixed $values Can either be an array of valid values for + * the field, or a string representing the format + * expected of the data. + * + * @return none, but will generate an error and exit if the value is invalid. + */ + protected function verifyField($data, $field, $values) + { + if (!isset($data['Candidate'][$field])) { + $this->header("HTTP/1.1 400 Bad Request"); + $this->error("Candidate's field missing"); + $this->safeExit(0); + } + if (is_array($values) && !in_array($data['Candidate'][$field], $values)) { + $this->header("HTTP/1.1 400 Bad Request"); + $this->error("Value not permitted"); + $this->safeExit(0); + } + if ($values === 'YYYY-MM-DD' + && !preg_match("/\d\d\d\d\-\d\d\-\d\d/", $data['Candidate'][$field]) + ) { + $this->header("HTTP/1.1 400 Bad Request"); + $this->error("Invalid date format"); + $this->safeExit(0); + } + } + + /** + * Testable wrapper for Candidate::createNew + * + * @param string $centerID The center id of the candidate + * @param string $DoB Date of birth of the candidate + * @param string $edc EDC of the candidate + * @param string $gender Gender of the candidate to be created + * @param string $PSCID PSCID of the candidate to be created + * + * @return none + */ + public function createNew($centerID, $DoB, $edc, $gender, $PSCID) + { + return \Candidate::createNew( + $centerID, + $DoB, + $edc, + $gender, + $PSCID + ); + } +} + +if (isset($_REQUEST['PrintCandidates'])) { + if ($_SERVER['REQUEST_METHOD'] === 'POST') { + $fp = fopen("php://input", "r"); + $data = ''; + while (!feof($fp)) { + $data .= fread($fp, 1024); + } + fclose($fp); + + $obj = new Candidates($_SERVER['REQUEST_METHOD'], json_decode($data, true)); + } else { + $obj = new Candidates($_SERVER['REQUEST_METHOD']); + } + print $obj->toJSONString(); +} +?> diff --git a/htdocs/api/v0.0.3-dev/Login.php b/htdocs/api/v0.0.3-dev/Login.php new file mode 100644 index 00000000000..4af8a28253e --- /dev/null +++ b/htdocs/api/v0.0.3-dev/Login.php @@ -0,0 +1,205 @@ + + * @license http://www.gnu.org/licenses/gpl-3.0.txt GPLv3 + * @link https://www.github.com/aces/Loris/ + */ + +namespace Loris\API; +set_include_path(get_include_path() . ":" . __DIR__); +require_once __DIR__ . '/APIBase.php'; +require_once __DIR__ . '/../../../vendor/autoload.php'; + +/** + * Implementation of Login endpoint + * + * @category Main + * @package API + * @author Dave MacFarlane + * @license http://www.gnu.org/licenses/gpl-3.0.txt GPLv3 + * @link https://www.github.com/aces/Loris/ + */ +class Login extends APIBase +{ + /** + * Validates the data which was passed in the request. + * + * @param string $method The HTTP method of the request + * @param array $data The array-encode JSON posted to + * the URL. + */ + function __construct($method, $data = array()) + { + $this->AllowedMethods = array('POST'); + $this->RequestData = $data; + + if (!in_array($method, $this->AllowedMethods)) { + $this->header("HTTP/1.1 405 Method Not Allowed"); + $this->header("Allow: " . join(", ", $this->AllowedMethods)); + $this->safeExit(0); + } + + $client = new \NDB_Client(); + // Bypass session handling because we're trying to authenticate a session + $client->makeCommandLine(); + $client->initialize(__DIR__ . "/../../../project/config.xml"); + + $this->HTTPMethod = $method; + $this->handleRequest(); + } + + /** + * Handle a POST request + * + * @return none + */ + function handlePOST() + { + if (empty($this->RequestData['username']) + || empty($this->RequestData['password']) + ) { + $this->header("HTTP/1.1 400 Bad Request"); + $this->JSON = array("error" => "Missing username or password"); + return; + + } + $user = $this->RequestData['username']; + $password = $this->RequestData['password']; + + $login = $this->getLoginAuthenticator(); + + if ($login->passwordAuthenticate($user, $password, false)) { + $token = $this->getEncodedToken($user); + if (!empty($token)) { + $this->JSON = array("token" => $token); + } else { + $this->header("HTTP/1.1 500 Internal Server Error"); + $this->JSON = array("error" => "Unacceptable JWT key."); + } + } else { + $this->header("HTTP/1.1 401 Unauthorized"); + if (!empty($login->_lastError)) { + $this->JSON = array( + "error" => $login->_lastError, + ); + } + + } + } + + /** + * Get the SinglePointLogin class used to authenticate this request + * in a separate function so that it can be mocked out for testing. + * + * @return SinglePointLogin Loris Authenticator + */ + function getLoginAuthenticator() + { + return new \SinglePointLogin(); + } + + /** + * Return a valid JWT encoded identification token for the user + * + * @param string $user The user to return an identification token for + * + * @return string JWT encoded token + */ + function getEncodedToken($user) + { + $factory = \NDB_Factory::singleton(); + $config = $factory->config(); + + $www = $config->getSetting("www"); + $baseURL = $www['url']; + + $token = array( + // JWT related tokens to for the JWT library to validate + "iss" => $baseURL, + "aud" => $baseURL, + // Issued at + "iat" => time(), + "nbf" => time(), + // Expire in 1 day + "exp" => time() + 86400, + // Additional payload data + "user" => $user, + ); + + $key = $config->getSetting("JWTKey"); + if (!self::isKeyStrong($key)) { + error_log( + 'ERROR: JWTKey config variable is weak. ' + .'Please change the key to a more cryptographically-secure value.' + ); + return ""; + } + return \Firebase\JWT\JWT::encode($token, $key, "HS256"); + } + + /** + * Calculate the ETag for this resource. + * + * Since this isn't a real resource, it just returns the + * empty string. + * + * @return empty string + */ + function calculateETag() + { + return; + } + + /** + * Verify key meets cryptographic strength requirements + * + * @param string $key The JWT key to verify + * + * @return boolean Key passes strength test + */ + static function isKeyStrong($key) + { + // Note: this code adapted from User::isPasswordStrong + $CharTypes = 0; + // less than 20 characters + if (strlen($key) < 20) { + return false; + } + // nothing but letters + if (!preg_match('/[^A-Za-z]/', $key)) { + return false; + } + // nothing but numbers + if (!preg_match('/[^0-9]/', $key)) { + return false; + } + // preg_match returns 1 on match, 0 on non-match + $CharTypes += preg_match('/[0-9]+/', $key); + $CharTypes += preg_match('/[A-Za-z]+/', $key); + $CharTypes += preg_match('/[!\\\$\^@#%&\*\(\)]+/', $key); + if ($CharTypes < 3) { + return false; + } + + return true; + } +} + +if (isset($_REQUEST['PrintLogin'])) { + // Without this line, mod_rewrite eats the $_POST variable. + $body = file_get_contents("php://input"); + if ($_SERVER['REQUEST_METHOD'] === 'POST') { + $obj = new Login($_SERVER['REQUEST_METHOD'], json_decode($body, true)); + } else { + $obj = new Login($_SERVER['REQUEST_METHOD']); + } + header('Content-type: application/json'); + print $obj->toJSONString(); +} +?> diff --git a/htdocs/api/v0.0.3-dev/Projects.php b/htdocs/api/v0.0.3-dev/Projects.php new file mode 100644 index 00000000000..614e10cbf4c --- /dev/null +++ b/htdocs/api/v0.0.3-dev/Projects.php @@ -0,0 +1,102 @@ + + * @license http://www.gnu.org/licenses/gpl-3.0.txt GPLv3 + * @link https://www.github.com/aces/Loris/ + */ +namespace Loris\API; +set_include_path(get_include_path() . ":" . __DIR__); +require_once 'APIBase.php'; + +/** + * Handles requests to the projects portion of the API + * + * @category Main + * @package API + * @author Dave MacFarlane + * @license http://www.gnu.org/licenses/gpl-3.0.txt GPLv3 + * @link https://www.github.com/aces/Loris/ + */ +class Projects extends APIBase +{ + /** + * Constructs a Projects handler + * + * @param string $method The HTTP request method + */ + public function __construct($method) + { + parent::__construct($method); + } + + /** + * Handles a GET request + * + * @return none, but populates $this->JSON + */ + public function handleGET() + { + if (!empty($this->JSON)) { + return; + } + $config = $this->Factory->config(); + + $useProjects = $config->getSetting("useProjects"); + $useEDC = $config->getSetting("useEDC"); + + if ($useEDC === '1' || $useEDC === 'true') { + $useEDC = true; + } else { + $useEDC = false; + } + $PSCID = $config->getSetting("PSCID"); + $PSCIDFormat = \Utility::structureToPCRE($PSCID['structure'], "SITE"); + + $type = $PSCID['generation'] == 'sequential' ? 'auto' : 'prompt'; + + $settings = [ + "useEDC" => $useEDC, + "PSCID" => [ + "Type" => $type, + "Regex" => $PSCIDFormat, + ], + ]; + + if ($useProjects && $useProjects !== "false" && $useProjects !== "0") { + $projects = \Utility::getProjectList(); + $projArray = []; + foreach ($projects as $project) { + $projArray[$project] = $settings; + } + $this->JSON = ["Projects" => $projArray]; + } else { + $this->JSON = [ + "Projects" => array("loris" => $settings), + ]; + } + } + + /** + * Calculates ETag for projects based on the JSON encoding + * + * @return string ETag for projects + */ + function calculateETag() + { + $this->handleGET(); + $etag = md5(json_encode($this->JSON, true)); + return $etag; + } +} + +if (isset($_REQUEST['PrintProjects'])) { + $obj = new Projects($_SERVER['REQUEST_METHOD']); + print $obj->toJSONString(); +} +?> diff --git a/htdocs/api/v0.0.3-dev/SafeExitException.php b/htdocs/api/v0.0.3-dev/SafeExitException.php new file mode 100644 index 00000000000..e6387914253 --- /dev/null +++ b/htdocs/api/v0.0.3-dev/SafeExitException.php @@ -0,0 +1,42 @@ + + * @license http://www.gnu.org/licenses/gpl-3.0.txt GPLv3 + * @link https://www.github.com/aces/Loris/ + */ +namespace Loris\API; +/** + * SafeExitException for exiting API in a way that is testable + * + * @category Main + * @package API + * @author Dave MacFarlane + * @license http://www.gnu.org/licenses/gpl-3.0.txt GPLv3 + * @link https://www.github.com/aces/Loris/ + */ +class SafeExitException extends \Exception +{ + var $Object; + + /** + * Exception to throw to exit program + * + * @param string $message Message to display + * @param integer $exitStatus The exit code to exit the program with + * @param object $obj The object throwing the exception. + */ + function __construct($message, $exitStatus, $obj) + { + parent::__construct($message, $exitStatus); + $this->Object = $obj; + } +} +?> diff --git a/htdocs/api/v0.0.3-dev/candidates/Candidate.php b/htdocs/api/v0.0.3-dev/candidates/Candidate.php new file mode 100644 index 00000000000..9ffc1a1355d --- /dev/null +++ b/htdocs/api/v0.0.3-dev/candidates/Candidate.php @@ -0,0 +1,130 @@ + + * @license http://www.gnu.org/licenses/gpl-3.0.txt GPLv3 + * @link https://www.github.com/aces/Loris/ + */ +namespace Loris\API\Candidates; +set_include_path(get_include_path() . ":" . __DIR__ . "/../"); +require_once 'APIBase.php'; + +/** + * Class to handle HTTP requests to the Candidate portion of the + * Loris REST API. + * + * @category Main + * @package API + * @author Dave MacFarlane + * @license http://www.gnu.org/licenses/gpl-3.0.txt GPLv3 + * @link https://www.github.com/aces/Loris/ + */ +class Candidate extends \Loris\API\APIBase +{ + var $Candidate; + + /** + * Construct class to handle request + * + * @param string $method HTTP method of request + * @param string $CandID CandID of candidate to be serialized + */ + public function __construct($method, $CandID) + { + $requestDelegationCascade = $this->AutoHandleRequestDelegation; + + $this->AutoHandleRequestDelegation = false; + $this->CandID = $CandID; + + parent::__construct($method); + + if (!is_numeric($CandID) + || $CandID < 100000 + || $CandID > 999999 + ) { + $this->header("HTTP/1.1 400 Bad Request"); + $this->error("Invalid CandID format"); + $this->safeExit(0); + + } + + try { + $this->Candidate = $this->Factory->Candidate($CandID); + } catch(\Exception $e) { + $this->header("HTTP/1.1 404 Not Found"); + $this->error("Unknown CandID"); + $this->safeExit(0); + } + + if ($requestDelegationCascade) { + $this->handleRequest(); + } + } + + /** + * Handle a GET request + * + * @return none, but populates $this->JSON + */ + public function handleGET() + { + $Site = $this->Candidate->getCandidateSite(); + $Gender = $this->Candidate->getCandidateGender(); + + $this->JSON = [ + "Meta" => [ + "CandID" => $this->CandID, + 'Project' => $this->Candidate->getProjectTitle(), + 'PSCID' => $this->Candidate->getPSCID(), + 'Site' => $Site, + 'EDC' => $this->Candidate->getCandidateEDC(), + 'DoB' => $this->Candidate->getCandidateDoB(), + 'Gender' => $Gender, + ], + "Visits" => array_values( + $this->Candidate->getListOfVisitLabels() + ), + ]; + } + + /** + * Calculate the ETag for this Candidate by taking a hash of the + * most recent change to the candidate, visit tables or number of + * visits + * + * @return string An ETag for this object + */ + function calculateETag() + { + $row = $this->DB->pselectRow( + "SELECT MAX(c.Testdate) as CandChange, + MAX(s.Testdate) as VisitChange, + COUNT(s.Visit_label) as VisitCount + FROM candidate c JOIN session s ON (c.CandID=s.CandID) + WHERE c.CandID=:candidate", + array("candidate" => $this->CandID) + ); + return md5( + 'Candidate:' . $this->CandID . ':' + . $row['CandChange'] . ':' + . $row['VisitChange'] . ':' + . $row['VisitCount'] + ); + } + +} + +if (isset($_REQUEST['PrintCandidate'])) { + $obj = new \Loris\API\Candidates\Candidate( + $_SERVER['REQUEST_METHOD'], + $_REQUEST['CandID'] + ); + print $obj->toJSONString(); +} +?> diff --git a/htdocs/api/v0.0.3-dev/candidates/InstrumentData.php b/htdocs/api/v0.0.3-dev/candidates/InstrumentData.php new file mode 100644 index 00000000000..2b5a789b610 --- /dev/null +++ b/htdocs/api/v0.0.3-dev/candidates/InstrumentData.php @@ -0,0 +1,243 @@ + + * @license http://www.gnu.org/licenses/gpl-3.0.txt GPLv3 + * @link https://www.github.com/aces/Loris/ + */ + +namespace Loris\API\Candidates\Candidate; +set_include_path(get_include_path() . ":" . __DIR__); +require_once 'Instruments.php'; + +/** + * Class to handle Instrument Data HTTP requests + * + * @category Main + * @package API + * @author Dave MacFarlane + * @license http://www.gnu.org/licenses/gpl-3.0.txt GPLv3 + * @link https://www.github.com/aces/Loris/ + */ +class InstrumentData extends \Loris\API\Candidates\Candidate\Instruments +{ + var $Instrument; + + /** + * Construct a request handler for candidate instrument data + * + * @param string $method The HTTP method to be handled + * @param string $CandID The CandID this API call is for + * @param string $Visit The Visit this API call is for + * @param string $Instrument The instrument this API call is for + * @param boolean $bDDE If true, handle DDE instrument instead of + * normal instrument data + * @param boolean $bFlags If true, include instrument flag data in + * serialization + */ + public function __construct( + $method, + $CandID, + $Visit, + $Instrument, + $bDDE, + $bFlags + ) { + if (empty($this->AllowedMethods)) { + $this->AllowedMethods = [ + 'GET', + 'PUT', + 'PATCH', + 'OPTIONS', + ]; + } + $this->AutoHandleRequestDelegation = false; + $this->bDDE = $bDDE; + $this->bFlags = $bFlags; + + parent::__construct($method, $CandID, $Visit); + + // instruments may need access to project libraries + set_include_path( + get_include_path() + . ":" . __DIR__ . "/../../../../project/libraries" + ); + include_once "NDB_BVL_Instrument.class.inc"; + + $CommentID = $this->DB->pselectOne( + "SELECT CommentID FROM flag f + LEFT JOIN session s ON (s.ID=f.SessionID AND s.Visit_label=:VL) + LEFT JOIN candidate c USING (CandID) + WHERE Test_name=:TN AND s.CandID=:CID AND + s.Active='Y' AND c.Active='Y' AND f.CommentID NOT LIKE 'DDE%'", + array( + 'VL' => $this->VisitLabel, + 'TN' => $Instrument, + 'CID' => $this->CandID, + ) + ); + + if (empty($CommentID)) { + $this->header("HTTP/1.1 404 Not Found"); + $this->error("Invalid instrument for candidate"); + $this->safeExit(0); + } + if ($this->bDDE) { + $CommentID = 'DDE_' . $CommentID; + } + + try { + $this->Instrument = \NDB_BVL_Instrument::factory( + $Instrument, + $CommentID, + null, + true + ); + } catch(\Exception $e) { + $this->header("HTTP/1.1 404 Not Found"); + $this->error("Invalid instrument"); + $this->safeExit(0); + } + + $this->handleRequest(); + + } + + /** + * Handle a GET request + * + * @return none, but populate $this->JSON + */ + function handleGET() + { + $this->JSON = [ + "Meta" => [ + "Instrument" => $this->Instrument->testName, + "Visit" => $this->VisitLabel, + "Candidate" => $this->CandID, + "DDE" => $this->bDDE, + ], + ]; + + if (!$this->bFlags) { + $Values = \NDB_BVL_Instrument::loadInstanceData($this->Instrument); + + unset($Values['CommentID']); + unset($Values['UserID']); + unset($Values['Testdate']); + unset($Values['Data_entry_completion_status']); + + $this->JSON[$this->Instrument->testName] = $Values; + } else { + $flags = $this->DB->pselectRow( + "SELECT Data_entry, Administration, Validity + FROM flag WHERE CommentID=:CID", + ['CID' => $this->Instrument->getCommentID()] + ); + + if (!$this->Instrument->ValidityEnabled) { + unset($flags['Validity']); + } + $this->JSON['Flags'] = $flags; + } + } + + /** + * Handle an OPTIONS request + * + * @return none, but modifies HTTP headers sent + */ + function handleOPTIONS() + { + $this->Header( + "Access-Control-Allow-Methods: ". + join($this->AllowedMethods, ",") + ); + } + + /** + * Handle a PUT request + * + * @return none, but populates $this->JSON and writes to database + */ + function handlePUT() + { + $fp = fopen("php://input", "r"); + $data = ''; + while (!feof($fp)) { + $data .= fread($fp, 1024); + } + fclose($fp); + + $data = json_decode($data); + $instrument_name = $this->Instrument->testName; + + if ($this->Instrument->validate($data)) { + $this->Instrument->clearInstrument(); + $this->Instrument->_save($data->${instrument_name}); + $this->JSON = array("success" => "Updated"); + } else { + $this->Header("HTTP/1.1 403 Forbidden"); + if (!$this->Instrument->determineDataEntryAllowed()) { + $msg = "Can not update instruments that" + . " are flagged as complete"; + + $this->JSON = array('error' => $msg); + } else { + $this->JSON = array("error" => "Could not update."); + } + } + } + + /** + * Handle a PUT request + * + * @return none, but populates $this->JSON and writes to database + */ + function handlePATCH() + { + $fp = fopen("php://input", "r"); + $data = ''; + while (!feof($fp)) { + $data .= fread($fp, 1024); + } + fclose($fp); + + $data = json_decode($data); + $instrument_name = $this->Instrument->testName; + + if ($this->Instrument->validate($data)) { + $this->Instrument->_save($data->${instrument_name}); + $this->JSON = array("success" => "Updated"); + } else { + $this->Header("HTTP/1.1 403 Forbidden"); + if (!$this->Instrument->determineDataEntryAllowed()) { + $msg = "Can not update instruments that" + . " are flagged as complete"; + + $this->JSON = array('error' => $msg); + } else { + $this->JSON = array("error" => "Could not update."); + } + } + } + +} + +if (isset($_REQUEST['PrintInstrumentData'])) { + $obj = new InstrumentData( + $_SERVER['REQUEST_METHOD'], + $_REQUEST['CandID'], + $_REQUEST['Visit'], + $_REQUEST['Instrument'], + isset($_REQUEST['DDE']) ? true : false, + isset($_REQUEST['flags']) ? true : false + ); + print $obj->toJSONString(); +} +?> diff --git a/htdocs/api/v0.0.3-dev/candidates/Instruments.php b/htdocs/api/v0.0.3-dev/candidates/Instruments.php new file mode 100644 index 00000000000..aee446588dd --- /dev/null +++ b/htdocs/api/v0.0.3-dev/candidates/Instruments.php @@ -0,0 +1,101 @@ + + * @license http://www.gnu.org/licenses/gpl-3.0.txt GPLv3 + * @link https://www.github.com/aces/Loris/ + */ +namespace Loris\API\Candidates\Candidate; +set_include_path(get_include_path() . ":" . __DIR__ . "/../"); + +require_once 'Visit.php'; + +/** + * Class to handle HTTP requests for a candidate's instruments. + * Extends Visit because the visit for the candidate also needs + * to be validated before the instruments can be serialized. + * + * @category Main + * @package API + * @author Dave MacFarlane + * @license http://www.gnu.org/licenses/gpl-3.0.txt GPLv3 + * @link https://www.github.com/aces/Loris/ + */ +class Instruments extends Visit +{ + /** + * Construct an Instruments API handler + * + * @param string $method The HTTP request method + * @param string $CandID The CandID to get the instruments for + * @param string $Visit The visit label for $CandID to retrieve + * instruments for + */ + public function __construct($method, $CandID, $Visit) + { + if (empty($this->AllowedMethods)) { + $this->AllowedMethods = ['GET']; + } + $requestDelegationCascade = $this->AutoHandleRequestDelegation; + // Parent will validate CandID and Visit Label and abort if necessary + parent::__construct($method, $CandID, $Visit); + + if ($requestDelegationCascade) { + $this->handleRequest(); + } + } + + /** + * Handles a GET request for this API call. + * + * @return none, but populates $this->JSON + */ + public function handleGET() + { + $Insts = $this->DB->pselect( + "SELECT DISTINCT Test_name" + . " FROM flag f" + . " JOIN session s ON (s.ID=f.SessionID)" + . " WHERE s.CandID=:CID AND s.Active='Y' AND s.Visit_label=:VL", + array( + 'CID' => $this->CandID, + 'VL' => $this->VisitLabel, + ) + ); + + //array_column only exists in PHP 5.5+, need to use array_map + //until we no longer support 5.4.. + //$Instruments = array_column($Insts, 'Test_name'); + $Instruments = array_map( + function ($element) { + return $element['Test_name']; + }, + $Insts + ); + + $this->JSON = [ + "Meta" => [ + "CandID" => $this->CandID, + 'Visit' => $this->VisitLabel, + ], + 'Instruments' => $Instruments, + ]; + } +} + +if (isset($_REQUEST['PrintInstruments'])) { + $obj = new Instruments( + $_SERVER['REQUEST_METHOD'], + $_REQUEST['CandID'], + $_REQUEST['VisitLabel'] + ); + print $obj->toJSONString(); +} + +?> diff --git a/htdocs/api/v0.0.3-dev/candidates/Visit.php b/htdocs/api/v0.0.3-dev/candidates/Visit.php new file mode 100644 index 00000000000..bc203ff357f --- /dev/null +++ b/htdocs/api/v0.0.3-dev/candidates/Visit.php @@ -0,0 +1,250 @@ + + * @license http://www.gnu.org/licenses/gpl-3.0.txt GPLv3 + * @link https://www.github.com/aces/Loris/ + */ +namespace Loris\API\Candidates\Candidate; +set_include_path( + get_include_path() + . ":" . __DIR__ . "/../" + . ':' . __DIR__ . "/../../../../php/libraries/" +); + +require_once 'Candidate.php'; +require_once 'TimePoint.class.inc'; + +/** + * Handles API requests for the candidate's visit. Extends + * Candidate so that the constructor will validate the candidate + * portion of URL automatically. + * + * @category Main + * @package API + * @author Dave MacFarlane + * @license http://www.gnu.org/licenses/gpl-3.0.txt GPLv3 + * @link https://www.github.com/aces/Loris/ + */ +class Visit extends \Loris\API\Candidates\Candidate +{ + /** + * Construct a visit class object to serialize candidate visits + * + * @param string $method The method of the HTTP request + * @param string $CandID The CandID to be serialized + * @param string $VisitLabel The visit label to be serialized + * @param string $InputData The data posted to this URL + */ + public function __construct($method, $CandID, $VisitLabel, $InputData=null) + { + $requestDelegationCascade = $this->AutoHandleRequestDelegation; + + $this->AutoHandleRequestDelegation = false; + + if (empty($this->AllowedMethods)) { + $this->AllowedMethods = [ + 'GET', + 'PUT', + ]; + } + $this->CandID = $CandID; + $this->VisitLabel = $VisitLabel; + + // $this->Timepoint = \Timepoint::singleton($timepointID); + // Parent constructor will handle validation of + // CandID + parent::__construct($method, $CandID); + if ($method === 'PUT') { + $this->ReceivedJSON = $InputData; + } else { + $timepoints = $this->Candidate->getListOfVisitLabels(); + $Visits = array_values($timepoints); + + $session = array_keys($timepoints, $VisitLabel); + if (isset($session[0])) { + $this->Timepoint = $this->Factory->TimePoint($session[0]); + } + + if (!in_array($VisitLabel, $Visits)) { + $this->header("HTTP/1.1 404 Not Found"); + $this->error("Invalid visit $VisitLabel"); + $this->safeExit(0); + } + } + + if ($requestDelegationCascade) { + $this->handleRequest(); + } + + } + + /** + * Handles a GET request + * + * @return none, but populates $this->JSON + */ + public function handleGET() + { + $SubProjTitle = $this->Timepoint->getData("SubprojectTitle"); + + $this->JSON = [ + "Meta" => [ + "CandID" => $this->CandID, + 'Visit' => $this->VisitLabel, + 'Battery' => $SubProjTitle, + ], + ]; + if ($this->Timepoint) { + $stages = []; + if ($this->Timepoint->getDateOfScreening() !== null) { + $Date = $this->Timepoint->getDateOfScreening(); + $Status = $this->Timepoint->getScreeningStatus(); + + $stages['Screening'] = [ + 'Date' => $Date, + 'Status' => $Status, + ]; + } + if ($this->Timepoint->getDateOfVisit() !== null) { + $Date = $this->Timepoint->getDateOfVisit(); + $Status = $this->Timepoint->getVisitStatus(); + + $stages['Visit'] = [ + 'Date' => $Date, + 'Status' => $Status, + ]; + } + if ($this->Timepoint->getDateOfApproval() !== null) { + $Date = $this->Timepoint->getDateOfApproval(); + $Status = $this->Timepoint->getApprovalStatus(); + + $stages['Approval'] = [ + 'Date' => $Date, + 'Status' => $Status, + ]; + } + $this->JSON['Stages'] = $stages; + } + } + + /** + * Handles a PUT request for a visit + * + * @return none + */ + public function handlePUT() + { + if (!isset($this->ReceivedJSON['Meta']['CandID']) + || $this->ReceivedJSON['Meta']['CandID'] != $this->CandID + ) { + $this->header("HTTP/1.1 400 Bad Request"); + $this->error("Candidate from URL does not match metadata"); + $this->safeExit(0); + } + if (!isset($this->ReceivedJSON['Meta']['Visit']) + || $this->ReceivedJSON['Meta']['Visit'] != $this->VisitLabel + ) { + $this->header("HTTP/1.1 400 Bad Request"); + $this->error("Visit from URL does not match metadata"); + $this->safeExit(0); + } + + $subprojects = \Utility::getSubprojectList(); + $subprojectID = null; + foreach ($subprojects as $subproject => $title) { + if ($title === $this->ReceivedJSON['Meta']['Battery']) { + $subprojectID = $subproject; + break; + } + } + if ($subprojectID === null) { + $this->header("HTTP/1.1 400 Bad Request"); + $this->error("Test battery specified does not exist"); + $this->safeExit(0); + + } + // This version of the API does not handle timepoint creation + // when users are at multiple sites + $user = \User::singleton(); + $centerIDs = $user->getCenterIDs(); + $num_sites = count($centerIDs); + if ($num_sites == 0) { + $this->header("HTTP/1.1 401 Unauthorized"); + $this->error("You are not affiliated with any site"); + $this->safeExit(0); + } else if ($num_sites > 1) { + $this->header("HTTP/1.1 501 Not Implemented"); + $this->error( + "This API version does not support timepoint creation " . + "by users with multiple site affiliations. This will be ". + "implemented in a future release." + ); + $this->safeExit(0); + } else { + $centerID = $centerIDs[0]; + $candidateCenterID = \Candidate::singleton($this->CandID) + ->getCenterID(); + if ($centerID != $candidateCenterID) { + $this->header("HTTP/1.1 401 Unauthorized"); + $this->error("You are not affiliated with the candidate's site"); + $this->safeExit(0); + } + // need to extract subprojectID + $this->createNew($this->CandID, $subprojectID, $this->VisitLabel); + $this->header("HTTP/1.1 201 Created"); + } + } + + /** + * Create a new timepoint + * + * This is a wrapper around the Timepoint::createNew function + * that can be stubbed out for testing. + * + * @param integer $CandID The candidate with the visit + * @param integer $subprojectID The subproject for the new visit + * @param string $VL The visit label of the visit to + * be created + * + * @return none + */ + function createNew($CandID, $subprojectID, $VL) + { + try { + \TimePoint::isValidVisitLabel($CandID, $subprojectID, $VL); + } catch (\LorisException $e) { + $this->header("HTTP/1.1 400 Bad Request"); + $this->error($e->getMessage()); + $this->safeExit(0); + } + + \TimePoint::createNew($CandID, $subprojectID, $VL); + } +} + +if (isset($_REQUEST['PrintVisit'])) { + $InputDataArray = file_get_contents("php://input"); + $InputData = json_decode($InputDataArray, true); + if ($_SERVER['REQUEST_METHOD'] === 'PUT') { + $obj = new Visit( + $_SERVER['REQUEST_METHOD'], + $_REQUEST['CandID'], + $_REQUEST['VisitLabel'], + $InputData + ); + } else { + $obj = new Visit( + $_SERVER['REQUEST_METHOD'], + $_REQUEST['CandID'], + $_REQUEST['VisitLabel'] + ); + } + print $obj->toJSONString(); +} +?> diff --git a/htdocs/api/v0.0.3-dev/candidates/visits/Images.php b/htdocs/api/v0.0.3-dev/candidates/visits/Images.php new file mode 100644 index 00000000000..e67cf56e94d --- /dev/null +++ b/htdocs/api/v0.0.3-dev/candidates/visits/Images.php @@ -0,0 +1,112 @@ + + * @license http://www.gnu.org/licenses/gpl-3.0.txt GPLv3 + * @link https://www.github.com/aces/Loris/ + */ +namespace Loris\API\Candidates\Candidate\Visit; +require_once '../Visit.php'; +/** + * Handles API requests for the candidate's visit. Extends + * Candidate so that the constructor will validate the candidate + * portion of URL automatically. + * + * @category Main + * @package API + * @author Dave MacFarlane + * @license http://www.gnu.org/licenses/gpl-3.0.txt GPLv3 + * @link https://www.github.com/aces/Loris/ + */ +class Images extends \Loris\API\Candidates\Candidate\Visit +{ + /** + * Construct a visit class object to serialize candidate visits + * + * @param string $method The method of the HTTP request + * @param string $CandID The CandID to be serialized + * @param string $VisitLabel The visit label to be serialized + */ + public function __construct($method, $CandID, $VisitLabel) + { + $requestDelegationCascade = $this->AutoHandleRequestDelegation; + + $this->AutoHandleRequestDelegation = false; + + if (empty($this->AllowedMethods)) { + $this->AllowedMethods = ['GET']; + } + $this->CandID = $CandID; + $this->VisitLabel = $VisitLabel; + + // $this->Timepoint = \Timepoint::singleton($timepointID); + // Parent constructor will handle validation of + // CandID + parent::__construct($method, $CandID, $VisitLabel); + + if ($requestDelegationCascade) { + $this->handleRequest(); + } + + } + + /** + * Handles a GET request + * + * @return none, but populates $this->JSON + */ + public function handleGET() + { + $this->JSON = [ + 'Meta' => [ + 'CandID' => $this->CandID, + 'Visit' => $this->VisitLabel, + ], + ]; + $this->JSON['Files'] = $this->GetVisitImages(); + + } + + /** + * Gets a list of images for this visit. Filename only. + * + * @return an array of strings of filenames + */ + function getVisitImages() + { + $factory = \NDB_Factory::singleton(); + $DB = $factory->database(); + $rows = $DB->pselect( + "SELECT OutputType, + SUBSTRING_INDEX(File, '/', -1) as Filename, + mst.Scan_type as AcquisitionType + FROM files f + JOIN mri_scan_type mst ON (mst.ID=f.AcquisitionProtocolID) + JOIN session s ON (s.ID=f.SessionID) + JOIN candidate c ON (s.CandID=c.CandID) + WHERE s.Visit_label=:VL AND c.CandID=:CID + AND c.Active='Y' AND s.Active='Y'", + [ + 'VL' => $this->VisitLabel, + 'CID' => $this->CandID, + ] + ); + return $rows; + } + +} + +if (isset($_REQUEST['PrintImages'])) { + $obj = new Images( + $_SERVER['REQUEST_METHOD'], + $_REQUEST['CandID'], + $_REQUEST['VisitLabel'] + ); + print $obj->toJSONString(); +} +?> diff --git a/htdocs/api/v0.0.3-dev/candidates/visits/images/Image.php b/htdocs/api/v0.0.3-dev/candidates/visits/images/Image.php new file mode 100644 index 00000000000..3f1857ba00f --- /dev/null +++ b/htdocs/api/v0.0.3-dev/candidates/visits/images/Image.php @@ -0,0 +1,209 @@ + + * @license http://www.gnu.org/licenses/gpl-3.0.txt GPLv3 + * @link https://www.github.com/aces/Loris/ + */ +namespace Loris\API\Candidates\Candidate\Visit\Imaging; +require_once __DIR__ . '/../../Visit.php'; +/** + * Handles API requests for the candidate's visit. Extends + * Candidate so that the constructor will validate the candidate + * portion of URL automatically. + * + * @category Main + * @package API + * @author Dave MacFarlane + * @license http://www.gnu.org/licenses/gpl-3.0.txt GPLv3 + * @link https://www.github.com/aces/Loris/ + */ +class Image extends \Loris\API\Candidates\Candidate\Visit +{ + /** + * Construct a visit class object to serialize candidate visits + * + * @param string $method The method of the HTTP request + * @param string $CandID The CandID to be serialized + * @param string $VisitLabel The visit label to be serialized + * @param string $Filename The file name to be retrieved + */ + public function __construct($method, $CandID, $VisitLabel, $Filename) + { + ob_start(); + $requestDelegationCascade = $this->AutoHandleRequestDelegation; + + $this->AutoHandleRequestDelegation = false; + + if (empty($this->AllowedMethods)) { + $this->AllowedMethods = [ + 'GET', + 'PUT', + ]; + } + $this->CandID = $CandID; + $this->VisitLabel = $VisitLabel; + $this->Filename = $Filename; + + // $this->Timepoint = \Timepoint::singleton($timepointID); + // Parent constructor will handle validation of + // CandID + parent::__construct($method, $CandID, $VisitLabel); + + $results = $this->getDatabaseDir(); + if (empty($results)) { + $this->header("HTTP/1.1 404 Not Found"); + $this->error("File not found"); + $this->safeExit(0); + } + + if ($requestDelegationCascade) { + $this->handleRequest(); + } + + } + + /** + * Handles a GET request + * + * @return none, but populates $this->JSON + */ + public function handleGET() + { + $fullDir = $this->getFullPath(); + ob_end_clean(); + + $fp = fopen($fullDir, "r"); + if ($fp === false) { + $this->header("HTTP/1.1 500 Internal Server Error", true, 500); + error_log("Could not open $fullDir to send to client"); + $this->safeExit(1); + } + + $this->Header('Cache-control: private'); + + $mincHeader = $this->getHeader("header"); + + if (substr($mincHeader, 0, 4) === 'hdf5') { + $contentType = 'application/x.minc2'; + } else { + $contentType = 'application/octet-stream'; + } + $this->Header("Content-Type: $contentType"); + $this->Header('Content-Length: '.filesize($fullDir)); + $this->Header('Content-Disposition: filename='.$this->Filename); + + while (!feof($fp)) { + print fread($fp, 1024); + } + $this->safeExit(); + } + + /** + * Gets an image's header from the database. + * + * @param string $headerName The header to retrieve for the current + * file + * + * @return string of the header value + */ + protected function getHeader($headerName) + { + $factory = \NDB_Factory::singleton(); + $db = $factory->Database(); + + return $db->pselectOne( + "SELECT Value + FROM parameter_file pf + JOIN parameter_type pt USING (ParameterTypeID) + JOIN files f USING (FileID) + JOIN session s ON (f.SessionID=s.ID) + JOIN candidate c ON (s.CandID=c.CandID) + WHERE c.Active='Y' AND s.Active='Y' AND c.CandID=:CID + AND s.Visit_label=:VL + AND f.File LIKE CONCAT('%', :Fname) AND pt.Name = :Header", + array( + 'CID' => $this->CandID, + 'VL' => $this->VisitLabel, + 'Fname' => $this->Filename, + 'Header' => $headerName, + ) + ); + + } + + /** + * Calculate the entity tag for this image + * + * @return string + */ + public function calculateETag() + { + return null; + } + + /** + * Gets the full path of this image on the filesystem, in order + * to be able to pass it to an fopen command (or similar) + * + * @return string + */ + protected function getFullPath() + { + return $this->getAssemblyRoot() . "/" . $this->getDatabaseDir(); + } + + /** + * Gets the root of the assembly directory, so that we know where + * to retrieve images relative to. + * + * @return string + */ + protected function getAssemblyRoot() + { + $factory = \NDB_Factory::singleton(); + $config = $factory->Config(); + return $config->getSetting("mincPath"); + } + + /** + * Gets the filename saved in the database for this file + * + * @return string + */ + protected function getDatabaseDir() + { + $factory = \NDB_Factory::singleton(); + $db = $factory->Database(); + return $db->pselectOne( + "SELECT File + FROM files f + JOIN session s ON (f.SessionID=s.ID) + JOIN candidate c ON (s.CandID=c.CandID) + WHERE c.Active='Y' AND s.Active='Y' + AND c.CandID=:CID AND s.Visit_label=:VL + AND f.File LIKE CONCAT('%', :Fname)", + array( + 'CID' => $this->CandID, + 'VL' => $this->VisitLabel, + 'Fname' => $this->Filename, + ) + ); + } +} + +if (isset($_REQUEST['PrintImageData'])) { + $obj = new Image( + $_SERVER['REQUEST_METHOD'], + $_REQUEST['CandID'], + $_REQUEST['VisitLabel'], + $_REQUEST['Filename'] + ); + print $obj->toJSONString(); +} +?> diff --git a/htdocs/api/v0.0.3-dev/candidates/visits/images/format/BrainBrowser.php b/htdocs/api/v0.0.3-dev/candidates/visits/images/format/BrainBrowser.php new file mode 100644 index 00000000000..d640a187a39 --- /dev/null +++ b/htdocs/api/v0.0.3-dev/candidates/visits/images/format/BrainBrowser.php @@ -0,0 +1,147 @@ + + * @license http://www.gnu.org/licenses/gpl-3.0.txt GPLv3 + * @link https://www.github.com/aces/Loris/ + */ +namespace Loris\API\Candidates\Candidate\Visit\Imaging\Format; +require_once __DIR__ . '/../Image.php'; +/** + * Handles API requests for the candidate's visit. Extends + * Candidate so that the constructor will validate the candidate + * portion of URL automatically. + * + * @category Main + * @package API + * @author Dave MacFarlane + * @license http://www.gnu.org/licenses/gpl-3.0.txt GPLv3 + * @link https://www.github.com/aces/Loris/ + */ +class BrainBrowser extends \Loris\API\Candidates\Candidate\Visit\Imaging\Image +{ + /** + * Construct a visit class object to serialize candidate visits + * + * @param string $method The method of the HTTP request + * @param string $CandID The CandID to be serialized + * @param string $VisitLabel The visit label to be serialized + * @param string $Filename The filename whose brainbrowser data we want + */ + public function __construct($method, $CandID, $VisitLabel, $Filename) + { + $requestDelegationCascade = $this->AutoHandleRequestDelegation; + + $this->AutoHandleRequestDelegation = false; + + if (empty($this->AllowedMethods)) { + $this->AllowedMethods = [ + 'GET', + 'PUT', + 'PATCH', + ]; + } + + parent::__construct($method, $CandID, $VisitLabel, $Filename); + + if ($requestDelegationCascade) { + $this->handleRequest(); + } + + } + + /** + * Helper function for handleGET to retrieve a dimension. + * + * @param string $dim The dimension to extract the headers for + * + * @return array + */ + private function _getDimension($dim) + { + return [ + 'start' => $this->getHeader("$dim:start"), + 'space_length' => $this->getHeader("$dim:length"), + 'step' => $this->getHeader("$dim:step"), + ]; + } + + /** + * Handles a GET request + * + * @return none, but populates $this->JSON + */ + public function handleGET() + { + $order = $this->getHeader("image:dimorder"); + $orderArray = explode(",", $order); + $this->JSON = [ + 'xspace' => $this->_getDimension("xspace"), + 'yspace' => $this->_getDimension("yspace"), + "zspace" => $this->_getDimension("zspace"), + ]; + if (count($orderArray) === 4) { + $this->JSON['time'] = $this->_getDimension("time"); + } + $this->JSON['order'] = $orderArray; + } + + /** + * Extracts a header for this image from the database + * + * @param string $headerName The name of the header to extract + * + * @return string The Header value + */ + protected function getHeader($headerName) + { + $factory = \NDB_Factory::singleton(); + $db = $factory->Database(); + + return $db->pselectOne( + "SELECT Value + FROM parameter_file pf + JOIN parameter_type pt USING (ParameterTypeID) + JOIN files f USING (FileID) + JOIN session s ON (f.SessionID=s.ID) + JOIN candidate c ON (s.CandID=c.CandID) + WHERE c.Active='Y' AND s.Active='Y' + AND c.CandID=:CID and s.Visit_label=:VL + AND f.File LIKE CONCAT('%', :Fname) AND pt.Name = :Header", + array( + 'CID' => $this->CandID, + 'VL' => $this->VisitLabel, + 'Fname' => $this->Filename, + 'Header' => $headerName, + ) + ); + + } + + /** + * Calculate the entity tag for this URL. + * + * @return string + */ + public function calculateETag() + { + return null; + } + +} + +if (isset($_REQUEST['PrintBBFormat'])) { + $obj = new BrainBrowser( + $_SERVER['REQUEST_METHOD'], + $_REQUEST['CandID'], + $_REQUEST['VisitLabel'], + $_REQUEST['Filename'] + ); + print $obj->toJSONString(); +} +?> diff --git a/htdocs/api/v0.0.3-dev/candidates/visits/images/format/Raw.php b/htdocs/api/v0.0.3-dev/candidates/visits/images/format/Raw.php new file mode 100644 index 00000000000..978144496fb --- /dev/null +++ b/htdocs/api/v0.0.3-dev/candidates/visits/images/format/Raw.php @@ -0,0 +1,95 @@ + + * @license http://www.gnu.org/licenses/gpl-3.0.txt GPLv3 + * @link https://www.github.com/aces/Loris/ + */ +namespace Loris\API\Candidates\Candidate\Visit\Imaging\Format; +require_once __DIR__ . '/../Image.php'; +/** + * Handles API requests for the candidate's visit. Extends + * Candidate so that the constructor will validate the candidate + * portion of URL automatically. + * + * @category Main + * @package API + * @author Dave MacFarlane + * @license http://www.gnu.org/licenses/gpl-3.0.txt GPLv3 + * @link https://www.github.com/aces/Loris/ + */ +class Raw extends \Loris\API\Candidates\Candidate\Visit\Imaging\Image +{ + /** + * Construct a visit class object to serialize candidate visits + * + * @param string $method The method of the HTTP request + * @param string $CandID The CandID to be serialized + * @param string $VisitLabel The visit label to be serialized + * @param string $Filename The file name whose raw data we want + */ + public function __construct($method, $CandID, $VisitLabel, $Filename) + { + $requestDelegationCascade = $this->AutoHandleRequestDelegation; + + $this->AutoHandleRequestDelegation = false; + + if (empty($this->AllowedMethods)) { + $this->AllowedMethods = ['GET']; + } + + parent::__construct($method, $CandID, $VisitLabel, $Filename); + + if ($requestDelegationCascade) { + $this->handleRequest(); + } + + } + + /** + * Handles a GET request + * + * @return none, but populates $this->JSON + */ + public function handleGET() + { + $mincfile = $this->getFullPath(); + if (`which minctoraw`) { + $this->Header("Content-Type: application/x.raw"); + passthru( + "minctoraw -byte -unsigned -normalize " + . escapeshellarg($mincfile) + ); + $this->safeExit(0); + } + + $this->header("HTTP/1.1 500 Internal Server Error", true, 500); + $this->error("Minc Tools not installed on server"); + } + + + /** + * Calculate the entity tag for this URL + * + * @return string + */ + public function calculateETag() + { + return null; + } +} + +if (isset($_REQUEST['PrintRawFormat'])) { + $obj = new Raw( + $_SERVER['REQUEST_METHOD'], + $_REQUEST['CandID'], + $_REQUEST['VisitLabel'], + $_REQUEST['Filename'] + ); +} +?> diff --git a/htdocs/api/v0.0.3-dev/candidates/visits/images/format/Thumbnail.php b/htdocs/api/v0.0.3-dev/candidates/visits/images/format/Thumbnail.php new file mode 100644 index 00000000000..e4d5164dfa9 --- /dev/null +++ b/htdocs/api/v0.0.3-dev/candidates/visits/images/format/Thumbnail.php @@ -0,0 +1,118 @@ + + * @license http://www.gnu.org/licenses/gpl-3.0.txt GPLv3 + * @link https://www.github.com/aces/Loris/ + */ +namespace Loris\API\Candidates\Candidate\Visit\Imaging\Format; +require_once __DIR__ . '/../Image.php'; +/** + * Handles API requests for the candidate's visit. Extends + * Candidate so that the constructor will validate the candidate + * portion of URL automatically. + * + * @category Main + * @package API + * @author Dave MacFarlane + * @license http://www.gnu.org/licenses/gpl-3.0.txt GPLv3 + * @link https://www.github.com/aces/Loris/ + */ +class Thumbnail extends \Loris\API\Candidates\Candidate\Visit\Imaging\Image +{ + /** + * Construct a visit class object to serialize candidate visits + * + * @param string $method The method of the HTTP request + * @param string $CandID The CandID to be serialized + * @param string $VisitLabel The visit label to be serialized + * @param string $Filename The file whose thumbnail we want + */ + public function __construct($method, $CandID, $VisitLabel, $Filename) + { + $requestDelegationCascade = $this->AutoHandleRequestDelegation; + + $this->AutoHandleRequestDelegation = false; + + if (empty($this->AllowedMethods)) { + $this->AllowedMethods = ['GET']; + } + + parent::__construct($method, $CandID, $VisitLabel, $Filename); + + if ($requestDelegationCascade) { + $this->handleRequest(); + } + + } + + /** + * Handles a GET request + * + * @return none, but populates $this->JSON + */ + public function handleGET() + { + $FullPath = $this->getFullPath(); + $fp = fopen($FullPath, 'r'); + if ($fp !== false) { + $this->Header("Content-Type: image/jpeg"); + fpassthru($fp); + fclose($fp); + $this->safeExit(0); + } else { + $this->header("HTTP/1.1 500 Internal Server Error", true, 500); + $this->error("Could not load thumbnail file"); + $this->safeExit(1); + } + } + + /** + * Calculate the entity tag for this URL + * + * @return string + */ + public function calculateETag() + { + return null; + } + + /** + * Get the root directory that images are stored under. + * + * @return string a directory on the server + */ + protected function getAssemblyRoot() + { + $factory = \NDB_Factory::singleton(); + $config = $factory->Config(); + return $config->getSetting("imagePath") . "/pic/"; + } + + /** + * Get the filename under the assemblyRoot that the image + * is stored at. + * + * @return string + */ + protected function getDatabaseDir() + { + return $this->getHeader("check_pic_filename"); + } + +} + +if (isset($_REQUEST['PrintThumbnailFormat'])) { + $obj = new Thumbnail( + $_SERVER['REQUEST_METHOD'], + $_REQUEST['CandID'], + $_REQUEST['VisitLabel'], + $_REQUEST['Filename'] + ); +} +?> diff --git a/htdocs/api/v0.0.3-dev/candidates/visits/images/headers/Full.php b/htdocs/api/v0.0.3-dev/candidates/visits/images/headers/Full.php new file mode 100644 index 00000000000..4b2e2003413 --- /dev/null +++ b/htdocs/api/v0.0.3-dev/candidates/visits/images/headers/Full.php @@ -0,0 +1,146 @@ + + * @license http://www.gnu.org/licenses/gpl-3.0.txt GPLv3 + * @link https://www.github.com/aces/Loris/ + */ +namespace Loris\API\Candidates\Candidate\Visit\Imaging\Headers; +require_once __DIR__ . '/../Image.php'; +/** + * Handles API requests for the candidate's visit. Extends + * Candidate so that the constructor will validate the candidate + * portion of URL automatically. + * + * @category Main + * @package API + * @author Dave MacFarlane + * @license http://www.gnu.org/licenses/gpl-3.0.txt GPLv3 + * @link https://www.github.com/aces/Loris/ + */ +class Full extends \Loris\API\Candidates\Candidate\Visit\Imaging\Image +{ + /** + * Construct a visit class object to serialize candidate visits + * + * @param string $method The method of the HTTP request + * @param string $CandID The CandID to be serialized + * @param string $VisitLabel The visit label to be serialized + * @param string $Filename The filename being retrieved + */ + public function __construct($method, $CandID, $VisitLabel, $Filename) + { + $requestDelegationCascade = $this->AutoHandleRequestDelegation; + + $this->AutoHandleRequestDelegation = false; + + if (empty($this->AllowedMethods)) { + $this->AllowedMethods = [ + 'GET', + 'PUT', + 'PATCH', + ]; + } + + parent::__construct($method, $CandID, $VisitLabel, $Filename); + + if ($requestDelegationCascade) { + $this->handleRequest(); + } + + } + + /** + * Handles a GET request + * + * @return none, but populates $this->JSON + */ + public function handleGET() + { + $headersDB = $this->getHeaders(); + + $headers = []; + foreach ($headersDB as $row) { + $headers[$row['Header']] = $row['Value']; + } + $this->JSON = [ + 'Meta' => [ + 'CandID' => $this->CandID, + 'Visit' => $this->VisitLabel, + 'Filename' => $this->Filename, + ], + "Headers" => $headers, + ]; + } + + /** + * Retrieves all headers for this file from the database. + * + * @return array + */ + protected function getHeaders() + { + $factory = \NDB_Factory::singleton(); + $db = $factory->Database(); + + // Get all fields from parameter_type "magically created by + // neurodb", since those are the dicom headers. + // There's a few headers that get magically created which + // aren't header fields, so we manually exclude them. + // Namely: + // + // md5hash, tarchiveMD5, image_comments, check_pic_filename, + // jiv_path + return $db->pselect( + "SELECT pt.Name as Header, Value + FROM parameter_file pf + JOIN parameter_type pt USING (ParameterTypeID) + JOIN files f USING (FileID) + JOIN session s ON (f.SessionID=s.ID) + JOIN candidate c ON (s.CandID=c.CandID) + WHERE c.Active='Y' AND s.Active='Y' AND c.CandID=:CID AND + s.Visit_label=:VL AND f.File LIKE CONCAT('%', :Fname) + AND pt.Description LIKE '%magically%' + AND pt.Name NOT IN ( + 'md5hash', + 'tarchiveMD5', + 'image_comments', + 'check_pic_filename', + 'jiv_path' + ) + ", + array( + 'CID' => $this->CandID, + 'VL' => $this->VisitLabel, + 'Fname' => $this->Filename, + ) + ); + + } + + /** + * Calculate the entity tag for this URL + * + * @return string + */ + public function calculateETag() + { + return null; + } +} + +if (isset($_REQUEST['PrintHeadersFull'])) { + $obj = new Full( + $_SERVER['REQUEST_METHOD'], + $_REQUEST['CandID'], + $_REQUEST['VisitLabel'], + $_REQUEST['Filename'] + ); + print $obj->toJSONString(); +} +?> diff --git a/htdocs/api/v0.0.3-dev/candidates/visits/images/headers/Headers.php b/htdocs/api/v0.0.3-dev/candidates/visits/images/headers/Headers.php new file mode 100644 index 00000000000..15ee8a34ca2 --- /dev/null +++ b/htdocs/api/v0.0.3-dev/candidates/visits/images/headers/Headers.php @@ -0,0 +1,135 @@ + + * @license http://www.gnu.org/licenses/gpl-3.0.txt GPLv3 + * @link https://www.github.com/aces/Loris/ + */ +namespace Loris\API\Candidates\Candidate\Visit\Imaging\Headers; +require_once __DIR__ . '/../Image.php'; +/** + * Handles API requests for the candidate's visit. Extends + * Candidate so that the constructor will validate the candidate + * portion of URL automatically. + * + * @category Main + * @package API + * @author Dave MacFarlane + * @license http://www.gnu.org/licenses/gpl-3.0.txt GPLv3 + * @link https://www.github.com/aces/Loris/ + */ +class Headers extends \Loris\API\Candidates\Candidate\Visit\Imaging\Image +{ + /** + * Construct a visit class object to serialize candidate visits + * + * @param string $method The method of the HTTP request + * @param string $CandID The CandID to be serialized + * @param string $VisitLabel The visit label to be serialized + * @param string $Filename The filename whose headers we want + */ + public function __construct($method, $CandID, $VisitLabel, $Filename) + { + $requestDelegationCascade = $this->AutoHandleRequestDelegation; + + $this->AutoHandleRequestDelegation = false; + + if (empty($this->AllowedMethods)) { + $this->AllowedMethods = [ + 'GET', + 'PUT', + 'PATCH', + ]; + } + + parent::__construct($method, $CandID, $VisitLabel, $Filename); + + if ($requestDelegationCascade) { + $this->handleRequest(); + } + + } + + /** + * Handles a GET request + * + * @return none, but populates $this->JSON + */ + public function handleGET() + { + $TE = $this->getHeader('acquisition:echo_time'); + $TR = $this->getHeader('acquisition:repetition_time'); + $TI = $this->getHeader('acquisition:inversion_time'); + $ST = $this->getHeader('acquisition:slice_thickness'); + + $SeriesName = $this->getHeader("acquisition:protocol"); + $SeriesDescription = $this->getHeader("acquisition:series_description"); + + $XSpace = [ + "Length" => $this->getHeader("xspace:length"), + "StepSize" => $this->getHeader("xspace:step"), + ]; + $YSpace = [ + "Length" => $this->getHeader("yspace:length"), + "StepSize" => $this->getHeader("yspace:step"), + ]; + $ZSpace = [ + "Length" => $this->getHeader("zspace:length"), + "StepSize" => $this->getHeader("zspace:step"), + ]; + $TimeD = [ + "Length" => $this->getHeader("time:length"), + "StepSize" => $this->getHeader("time:step"), + ]; + $this->JSON = [ + 'Meta' => [ + 'CandID' => $this->CandID, + 'Visit' => $this->VisitLabel, + 'Filename' => $this->Filename, + ], + 'Physical' => [ + "TE" => $TE, + "TR" => $TR, + "TI" => $TI, + "SliceThickness" => $ST, + ], + 'Description' => [ + "SeriesName" => $SeriesName, + "SeriesDescription" => $SeriesDescription, + ], + 'Dimensions' => [ + "XSpace" => $XSpace, + "YSpace" => $YSpace, + "ZSpace" => $ZSpace, + "TimeDimension" => $TimeD, + ], + + ]; + } + + /** + * Calculate the entity tag for this URL + * + * @return string + */ + public function calculateETag() + { + return null; + } +} + +if (isset($_REQUEST['PrintHeadersSummary'])) { + $obj = new Headers( + $_SERVER['REQUEST_METHOD'], + $_REQUEST['CandID'], + $_REQUEST['VisitLabel'], + $_REQUEST['Filename'] + ); + print $obj->toJSONString(); +} +?> diff --git a/htdocs/api/v0.0.3-dev/candidates/visits/images/headers/Specific.php b/htdocs/api/v0.0.3-dev/candidates/visits/images/headers/Specific.php new file mode 100644 index 00000000000..295cf3e7ada --- /dev/null +++ b/htdocs/api/v0.0.3-dev/candidates/visits/images/headers/Specific.php @@ -0,0 +1,100 @@ + + * @license http://www.gnu.org/licenses/gpl-3.0.txt GPLv3 + * @link https://www.github.com/aces/Loris/ + */ +namespace Loris\API\Candidates\Candidate\Visit\Imaging\Headers; +require_once __DIR__ . '/../Image.php'; +/** + * Handles API requests for the candidate's visit. Extends + * Candidate so that the constructor will validate the candidate + * portion of URL automatically. + * + * @category Main + * @package API + * @author Dave MacFarlane + * @license http://www.gnu.org/licenses/gpl-3.0.txt GPLv3 + * @link https://www.github.com/aces/Loris/ + */ +class SpecificHeader extends \Loris\API\Candidates\Candidate\Visit\Imaging\Image +{ + /** + * Construct a visit class object to serialize candidate visits + * + * @param string $method The method of the HTTP request + * @param string $CandID The CandID to be serialized + * @param string $VisitLabel The visit label to be serialized + * @param string $Filename The file to retrieve the header for + * @param string $Header The header field to extract + */ + public function __construct($method, $CandID, $VisitLabel, $Filename, $Header) + { + $requestDelegationCascade = $this->AutoHandleRequestDelegation; + + $this->AutoHandleRequestDelegation = false; + + if (empty($this->AllowedMethods)) { + $this->AllowedMethods = ['GET']; + } + + parent::__construct($method, $CandID, $VisitLabel, $Filename); + + $this->Header = $Header; + + if ($requestDelegationCascade) { + $this->handleRequest(); + } + + } + + /** + * Handles a GET request + * + * @return none, but populates $this->JSON + */ + public function handleGET() + { + + foreach ($headersDB as $row) { + $headers[$row['Header']] = $row['Value']; + } + $this->JSON = [ + 'Meta' => [ + 'CandID' => $this->CandID, + 'Visit' => $this->VisitLabel, + 'Filename' => $this->Filename, + "Header" => $this->Header, + ], + "Value" => $this->getHeader($this->Header), + ]; + } + + /** + * Calculate the ETag for this header + * + * @return string + */ + public function calculateETag() + { + return null; + } +} + +if (isset($_REQUEST['PrintSpecificHeader'])) { + $obj = new SpecificHeader( + $_SERVER['REQUEST_METHOD'], + $_REQUEST['CandID'], + $_REQUEST['VisitLabel'], + $_REQUEST['Filename'], + $_REQUEST['Header'] + ); + print $obj->toJSONString(); +} +?> diff --git a/htdocs/api/v0.0.3-dev/candidates/visits/images/qc/QC.php b/htdocs/api/v0.0.3-dev/candidates/visits/images/qc/QC.php new file mode 100644 index 00000000000..03c7a59fc48 --- /dev/null +++ b/htdocs/api/v0.0.3-dev/candidates/visits/images/qc/QC.php @@ -0,0 +1,237 @@ + + * @license http://www.gnu.org/licenses/gpl-3.0.txt GPLv3 + * @link https://www.github.com/aces/Loris/ + */ +namespace Loris\API\Candidates\Candidate\Visit\Imaging\Qc; +require_once __DIR__ . '/../Image.php'; +/** + * Handles API requests for the candidate's visit. Extends + * Candidate so that the constructor will validate the candidate + * portion of URL automatically. + * + * @category Main + * @package API + * @author Dave MacFarlane + * @license http://www.gnu.org/licenses/gpl-3.0.txt GPLv3 + * @link https://www.github.com/aces/Loris/ + */ +class QC extends \Loris\API\Candidates\Candidate\Visit\Imaging\Image +{ + /** + * Construct an Image class object to serialize candidate images + * + * @param string $method The method of the HTTP request + * @param string $CandID The CandID to be serialized + * @param string $VisitLabel The visit label to be serialized + * @param string $Filename The file name to be serialized + */ + public function __construct($method, $CandID, $VisitLabel, $Filename) + { + $requestDelegationCascade = $this->AutoHandleRequestDelegation; + + $this->AutoHandleRequestDelegation = false; + + if (empty($this->AllowedMethods)) { + $this->AllowedMethods = [ + 'GET', + 'PUT', + ]; + } + + parent::__construct($method, $CandID, $VisitLabel, $Filename); + + if ($requestDelegationCascade) { + $this->handleRequest(); + } + + } + + /** + * Handles a GET request + * + * @return none, but populates $this->JSON + */ + public function handleGET() + { + $factory = \NDB_Factory::singleton(); + $DB = $factory->Database(); + $QCStatus = $DB->pselectRow( + "SELECT QCStatus, + pf.Value as Selected FROM files f + LEFT JOIN files_qcstatus fqc ON (f.FileID=fqc.FileID) + LEFT JOIN parameter_file pf ON (f.FileID=pf.FileID) + LEFT JOIN parameter_type pt + ON (pf.ParameterTypeID=pt.ParameterTypeID AND pt.Name='Selected') + WHERE f.File LIKE CONCAT('%', :FName)", + array('FName' => $this->Filename) + ); + $this->JSON = [ + 'Meta' => [ + 'CandID' => $this->CandID, + 'Visit' => $this->VisitLabel, + 'File' => $this->Filename, + ], + 'QC' => $QCStatus['QCStatus'], + 'Selected' => $QCStatus['Selected'], + ]; + } + + /** + * Calculates the ETag for the current QC status + * + * @return string + */ + public function calculateETag() + { + return null; + } + + /** + * Handles a PUT request for QC data + * + * @return none + */ + public function handlePUT() + { + $fp = fopen("php://input", "r"); + $data = ''; + while (!feof($fp)) { + $data .= fread($fp, 1024); + } + fclose($fp); + + //parse_str(urldecode($data), $data); + $data = json_decode($data, true); + if (!isset($data['Meta']['CandID']) + || $data['Meta']['CandID'] != $this->CandID + ) { + $this->header("HTTP/1.1 400 Bad Request"); + $this->error("Candidate from URL does not match JSON metadata."); + $this->safeExit(0); + } + if (!isset($data['Meta']['Visit']) + || $data['Meta']['Visit'] != $this->VisitLabel + ) { + $this->header("HTTP/1.1 400 Bad Request"); + $this->error("Visit from URL does not match JSON metadata"); + $this->safeExit(0); + } + if (!isset($data['Meta']['File']) + || $data['Meta']['File'] != $this->Filename + ) { + $this->header("HTTP/1.1 400 Bad Request"); + $this->error("File name from URL does not match JSON metadata"); + $this->safeExit(0); + } + + if (!isset($data['QCStatus'])) { + $this->header("HTTP/1.1 400 Bad Request"); + $this->error("Missing QCStatus to save."); + $this->safeExit(0); + } + if ($data['QCStatus'] != "Pass" && $data['QCStatus'] != "Fail") { + $this->header("HTTP/1.1 400 Bad Request"); + $this->error("Invalid value for QCStatus . Must be Pass or Fail."); + $this->safeExit(0); + } + if (!isset($data['Selected'])) { + $this->header("HTTP/1.1 400 Bad Request"); + $this->error("Missing Selected flag."); + $this->safeExit(0); + } + + // We know that it's set to something, because we checked above, so verify + // that Pending is a valid value. + // true is equal to "true", but false is not equal to "false". + if ($data['Selected'] != "true" + && $data['Selected'] != "false" + && $data['Selected'] !== false + ) { + $this->header("HTTP/1.1 400 Bad Request"); + $this->error("Invalid value for Selected. Must be true or false."); + $this->safeExit(0); + } + + $selval = ""; + // don't need to handle false, because $selval was initialized to the empty + // string (which is what // false would save.. + if ($data['Selected'] == "true") { + $factory = \NDB_Factory::singleton(); + $DB = $factory->Database(); + $selval = $DB->pselectOne( + "SELECT mst.Scan_type + FROM files f + LEFT JOIN mri_scan_type mst ON (f.AcquisitionProtocolID=mst.ID) + WHERE f.File LIKE CONCAT('%', :FName)", + array('FName' => $this->Filename) + ); + + } + $this->_saveFileQC($data['QCStatus'], $selval); + + $this->JSON = ['success' => 'Updated file QC information']; + + } + + /** + * Save the QC value to the database. Only call this after everything + * has been validated + * + * @param string $qcval The Pass/Fail status + * @param string $selval The value to set the selected field to. + * + * @return none + */ + private function _saveFileQC($qcval, $selval) + { + $factory = \NDB_Factory::singleton(); + $DB = $factory->Database(); + $FileID = $DB->pselectOne( + "SELECT f.FileID FROM files f + WHERE f.File LIKE CONCAT('%', :FName)", + array('FName' => $this->Filename) + ); + $AlreadySavedQC = $DB->pselectOne( + "SELECT COUNT(*) FROM files_qcstatus WHERE FileID=:FID", + array('FID' => $FileID) + ); + if ($AlreadySavedQC > 0) { + $DB->update( + "files_qcstatus", + [ + 'QCStatus' => $qcval, + 'Selected' => $selval, + ], + ['FileID' => $FileID] + ); + } else { + $DB->insert( + "files_qcstatus", + [ + 'QCStatus' => $qcval, + 'Selected' => $selval, + 'FileID' => $FileID, + ] + ); + } + } +} + +if (isset($_REQUEST['PrintImageQC'])) { + $obj = new QC( + $_SERVER['REQUEST_METHOD'], + $_REQUEST['CandID'], + $_REQUEST['VisitLabel'], + $_REQUEST['Filename'] + ); + print $obj->toJSONString(); +} +?> diff --git a/htdocs/api/v0.0.3-dev/candidates/visits/qc/Imaging.php b/htdocs/api/v0.0.3-dev/candidates/visits/qc/Imaging.php new file mode 100644 index 00000000000..138bb48c687 --- /dev/null +++ b/htdocs/api/v0.0.3-dev/candidates/visits/qc/Imaging.php @@ -0,0 +1,219 @@ + + * @license http://www.gnu.org/licenses/gpl-3.0.txt GPLv3 + * @link https://www.github.com/aces/Loris/ + */ +namespace Loris\API\Candidates\Candidate\Visit\QC; +require_once '../../Visit.php'; +/** + * Handles API requests for the candidate's visit. Extends + * Candidate so that the constructor will validate the candidate + * portion of URL automatically. + * + * @category Main + * @package API + * @author Dave MacFarlane + * @license http://www.gnu.org/licenses/gpl-3.0.txt GPLv3 + * @link https://www.github.com/aces/Loris/ + */ +class Imaging extends \Loris\API\Candidates\Candidate\Visit +{ + /** + * Construct a visit class object to serialize candidate visits + * + * @param string $method The method of the HTTP request + * @param string $CandID The CandID to be serialized + * @param string $VisitLabel The visit label to be serialized + */ + public function __construct($method, $CandID, $VisitLabel) + { + $requestDelegationCascade = $this->AutoHandleRequestDelegation; + + $this->AutoHandleRequestDelegation = false; + + if (empty($this->AllowedMethods)) { + $this->AllowedMethods = [ + 'GET', + 'PUT', + ]; + } + $this->CandID = $CandID; + $this->VisitLabel = $VisitLabel; + + // Parent constructor will handle validation of + // CandID and VisitLabel + parent::__construct($method, $CandID, $VisitLabel); + + if ($requestDelegationCascade) { + $this->handleRequest(); + } + + } + + /** + * Handles a GET request + * + * @return none, but populates $this->JSON + */ + public function handleGET() + { + $factory = \NDB_Factory::singleton(); + $DB = $factory->database(); + $qcstatus = $DB->pselectRow( + "SELECT MRIQCStatus, MRIQCPending + FROM session s JOIN candidate c ON (c.CandID=s.CandID) + WHERE c.Active='Y' AND s.Active='Y' + AND s.Visit_label=:VL AND c.CandID=:CID", + array( + 'VL' => $this->VisitLabel, + 'CID' => $this->CandID, + ) + ); + + $this->JSON = [ + 'Meta' => [ + 'CandID' => $this->CandID, + 'Visit' => $this->VisitLabel, + ], + ]; + + $this->JSON['SessionQC'] = $qcstatus['MRIQCStatus']; + $this->JSON['Pending'] = $qcstatus['MRIQCPending'] === 'N' ? false : true; + + } + + /** + * Calculate the ETag for the current QC status + * + * @return string The JSON's entity tag + */ + public function calculateETag() + { + // mod_rewrite seems to be eatting the ETag for some reason that I won't + // be able to figure out in time for the release. + // Return a null ETag so that PUT requests can be processed. + return null; + } + + /** + * Handle a PUT request by validating the metadata matches the URL + * and updating the database + * + * @return none + */ + public function handlePUT() + { + $fp = fopen("php://input", "r"); + $data = ''; + while (!feof($fp)) { + $data .= fread($fp, 1024); + } + fclose($fp); + + //parse_str(urldecode($data), $data); + $data = json_decode($data, true); + if (!isset($data['Meta']['CandID']) + || $data['Meta']['CandID'] != $this->CandID + ) { + $this->header("HTTP/1.1 400 Bad Request"); + $this->error("Candidate from URL does not match metadata."); + $this->safeExit(0); + } + if (!isset($data['Meta']['Visit']) + || $data['Meta']['Visit'] != $this->VisitLabel + ) { + $this->header("HTTP/1.1 400 Bad Request"); + $this->error("Visit from URL does not match metadata"); + $this->safeExit(0); + } + + if (!isset($data['SessionQC'])) { + $this->header("HTTP/1.1 400 Bad Request"); + $this->error("Missing SessionQC to save."); + $this->safeExit(0); + } + if ($data['SessionQC'] != "Pass" + && $data['SessionQC'] != "Fail" + && !empty($data['SessionQC']) + ) { + $this->header("HTTP/1.1 400 Bad Request"); + $this->error( + "Invalid value for SessionQC." + . " Must be Pass, Fail, or the empty string." + ); + $this->safeExit(0); + } + if (!isset($data['Pending'])) { + $this->header("HTTP/1.1 400 Bad Request"); + $this->error("Missing Pending flag."); + $this->safeExit(0); + } + + // We know that it's set to something, because we checked above, + // so verify that Pending is a valid value. + // true is equal to "true", but false is not equal to "false". + if ($data['Pending'] != "true" + && $data['Pending'] != "false" + && $data['Pending'] !== false + ) { + $this->header("HTTP/1.1 400 Bad Request"); + $this->error("Invalid value for Pending. Must be true or false."); + $this->safeExit(0); + } + + switch( $data['Pending'] ){ + case 'true': + $savePending = 'Y'; + break; + case 'false': + case false: + $savePending = 'N'; + break; + default: + $savePending = null; + } + + // Manually extract the sessionID with a select statement, + // since the keys used to look it up are in different tables + // and we can't join in the update wrapper. + $factory = \NDB_Factory::singleton(); + $DB = $factory->database(); + $sessionID = $DB->pselectOne( + "SELECT s.ID + FROM session s + JOIN candidate c ON (c.CandID=s.CandID) + WHERE c.Active='Y' AND s.Active='Y' + AND s.Visit_label=:VL AND c.CandID=:CID", + array( + 'VL' => $this->VisitLabel, + 'CID' => $this->CandID, + ) + ); + $qcstatus = $DB->update( + 'session', + [ + 'MRIQCStatus' => $data['SessionQC'], + 'MRIQCPending' => $savePending, + ], + ['ID' => $sessionID] + ); + $this->JSON = ["success" => "Updated QC"]; + } +} + +if (isset($_REQUEST['PrintQC'])) { + $obj = new Imaging( + $_SERVER['REQUEST_METHOD'], + $_REQUEST['CandID'], + $_REQUEST['VisitLabel'] + ); + print $obj->toJSONString(); +} +?> diff --git a/htdocs/api/v0.0.3-dev/projects/InstrumentForm.php b/htdocs/api/v0.0.3-dev/projects/InstrumentForm.php new file mode 100644 index 00000000000..74c77854071 --- /dev/null +++ b/htdocs/api/v0.0.3-dev/projects/InstrumentForm.php @@ -0,0 +1,92 @@ + + * @license Loris license + * @link https://github.com/aces/Loris + */ +namespace Loris\API\Projects; +//Load config file and ensure paths are correct +set_include_path(get_include_path() . ":" . __DIR__ . "/../"); +require_once 'APIBase.php'; + +/** + * Class which handles serialization of instrument forms in Loris API + * + * @category Loris + * @package API + * @author Dave MacFarlane + * @license Loris license + * @link https://github.com/aces/Loris + */ +class InstrumentForm extends \Loris\API\APIBase +{ + var $Instrument; + + /** + * Construct the object to handle requests. This will instantiate + * the NDB_BVL_Instrument object and call toJSON to return the + * JSON to the client. + * + * @param string $method The HTTP method used for the request + * @param string $Instrument The instrument to be serialized + */ + function __construct($method, $Instrument) + { + $this->AutoHandleRequestDelegation = false; + parent::__construct($method); + + try { + $this->Instrument = \NDB_BVL_Instrument::factory( + $Instrument, + null, + null, + true + ); + } catch(\Exception $e) { + $this->header("HTTP/1.1 404 Not Found"); + $this->error("Invalid Instrument"); + $this->safeExit(0); + } + + // JSON is used by both calculateETag and handleGET, so do it + // before either is called. + $this->JSONString = $this->Instrument->toJSON(); + $this->handleRequest(); + } + + + /** + * Handles a GET request to this URL + * + * @return none, but populates JSON class variable + */ + function handleGET() + { + $this->JSON = json_decode($this->JSONString, true); + } + + /** + * Calculates the ETag for this instrument by just taking + * an MD5 of the JSON + * + * @return string ETag for this instrument + */ + function calculateETag() + { + return md5('Instrument:' . $this->JSONString); + } + +} + +if (isset($_REQUEST['PrintInstrumentForm'])) { + $obj = new InstrumentForm($_SERVER['REQUEST_METHOD'], $_REQUEST['Instrument']); + print $obj->toJSONString(); +} +?> diff --git a/htdocs/api/v0.0.3-dev/projects/Project.php b/htdocs/api/v0.0.3-dev/projects/Project.php new file mode 100644 index 00000000000..7c3b752c4c2 --- /dev/null +++ b/htdocs/api/v0.0.3-dev/projects/Project.php @@ -0,0 +1,173 @@ + + * @license http://www.gnu.org/licenses/gpl-3.0.txt GPLv3 + * @link https://www.github.com/aces/Loris/ + */ +namespace Loris\API\Projects; + +set_include_path(get_include_path() . ":" . __DIR__ . "/.."); +require_once 'APIBase.php'; + +/** + * This class handles project related API requests. Depending on how it + * is called it can include either the candidates, visits, instruments, + * or all of the above that should be serialized. + * + * PHP Version 5 + * + * @category Main + * @package API + * @author Dave MacFarlane + * @license http://www.gnu.org/licenses/gpl-3.0.txt GPLv3 + * @link https://www.github.com/aces/Loris/ + */ +class Project extends \Loris\API\APIBase +{ + private $_project; + + /** + * Constructs an object to handle JSON serialization + * + * @param string $method The HTTP method of the request + * @param string $projectName The project to be serialized + * @param boolean $bCandidates If true, candidates for project + * should be included in serialization + * be included in serialization be + * included in serialization + * @param boolean $bInstruments If true, list of instruments for + * project should be included in + * serialization should be included + * in serialization should be included + * in serialization + * @param boolean $bVisits If true, visits for project should be + * included in serialization included in + * serialization included in serialization + * @param boolean $bInstrumentDetails If true, InstrumentDetails are populated + * instead of instrument names + */ + public function __construct( + $method, + $projectName, + $bCandidates, + $bInstruments, + $bVisits, + $bInstrumentDetails = false + ) { + $this->AutoHandleRequestDelegation = false; + parent::__construct($method); + + $this->bCandidates = $bCandidates; + $this->bInstruments = $bInstruments; + $this->bInstrumentDetails = $bInstrumentDetails; + $this->bVisits = $bVisits; + + try { + $this->project = $this->Factory->project($projectName); + } catch (\LorisException $e) { + // This projectName does not exists + $this->header("HTTP/1.1 404 Not Found"); + $this->error(['error' => 'Invalid project']); + $this->safeExit(0); + } + + $this->handleRequest(); + } + + /** + * Handles a GET request for a project data + * + * @return none, but populates $this->JSON + */ + function handleGET() + { + if (!empty($this->JSON)) { + return $this->JSON; + } + $JSONArray = [ + "Meta" => [ + "Project" => $this->project->getName(), + ], + ]; + + if ($this->bCandidates) { + $JSONArray['Candidates'] = $this->project->getCandidateIds(); + } + + if ($this->bInstruments) { + $Instruments = \Utility::getAllInstruments(); + + if ($this->bInstrumentDetails) { + $dets = []; + $config = $this->Factory->config(); + $DB = $this->Factory->database(); + + $DDE = $config->getSetting("DoubleDataEntryInstruments"); + + foreach ($Instruments as $instrument=> $FullName ) { + $subgroup = $DB->pselectOne( + "SELECT sg.Subgroup_name + FROM test_names tn + LEFT JOIN test_subgroups sg ON (tn.Sub_group=sg.ID) + WHERE tn.Test_name=:inst", + array('inst' => $instrument) + ); + + $DDEEn = in_array($instrument, $DDE); + + $dets[$instrument] = [ + 'FullName' => $FullName, + 'Subgroup' => $subgroup, + 'DoubleDataEntryEnabled' => $DDEEn, + ]; + } + $JSONArray['Instruments'] = $dets; + } else { + $JSONArray['Instruments'] = array_keys($Instruments); + } + } + + if ($this->bVisits) { + $Visits = \Utility::getExistingVisitLabels($this->project->getId()); + $VisitNames = array_keys($Visits); + + $JSONArray['Visits'] = $VisitNames; + } + + $this->JSON = $JSONArray; + } + + /** + * Calculates the ETag for this project by taking an MD5 of the + * JSON + * + * @return string ETag for project + */ + function calculateETag() + { + return md5('Project:' . json_encode($this->JSON, true)); + } + +} + +if (isset($_REQUEST['PrintProjectJSON'])) { + $Proj = new Project( + $_SERVER['REQUEST_METHOD'], + $_REQUEST['Project'], + isset($_REQUEST['Candidates']) ? true : false, + isset($_REQUEST['Instruments']) ? true : false, + isset($_REQUEST['Visits']) ? true : false, + isset($_REQUEST['InstrumentDetails']) ? true : false + ); + + print $Proj->toJSONString(); +} +?> From 2340c8b376c621f12722edb9977be3b22002eded Mon Sep 17 00:00:00 2001 From: Xavier Lecours Date: Thu, 21 Dec 2017 09:57:47 -0500 Subject: [PATCH 002/108] [.htaccess] removing rewrite rule (#3288) This removes the rewrite rule used for my_preferences. The Loris menu now use the /test_name/subtest_name/ url since the my_preference page has been extracted from edit_user. see: #3282 --- htdocs/.htaccess | 2 -- htdocs/router.php | 13 ------------- smarty/templates/email/notifier_custom_html.tpl | 2 +- 3 files changed, 1 insertion(+), 16 deletions(-) diff --git a/htdocs/.htaccess b/htdocs/.htaccess index 677f983efac..e4eb28b11e8 100644 --- a/htdocs/.htaccess +++ b/htdocs/.htaccess @@ -22,8 +22,6 @@ RewriteRule ^([0-9]{6,6})/([0-9]+)/([a-zA-Z0-9_]+)/$ main.php?test_name=$3&candID=$1&sessionID=$2 [QSA] RewriteRule ^([0-9]{6,6})/([0-9]+)/([a-zA-Z0-9_]+)/([a-zA-Z0-9_]+)/$ main.php?test_name=$3&candID=$1&sessionID=$2&subtest=$4 [QSA] - # Preferences is a special case for url rewriting - RewriteRule ^preferences/$ main.php?test_name=user_accounts&subtest=my_preferences # Rewrite /foo/ to appropriate module # Includes /foo/css/cssfile.css # /foo/js/javascriptfile.js diff --git a/htdocs/router.php b/htdocs/router.php index c2a699b54a8..1d2c58b374d 100644 --- a/htdocs/router.php +++ b/htdocs/router.php @@ -176,19 +176,6 @@ $_REQUEST["test_name"] = $getParams[0]; $_REQUEST['subtest'] = $getParams[1]; - include_once __DIR__ . "/main.php"; -} else if (preg_match( - '#^preferences/$#', - $url -)) { - // Preferences is a special case for url rewriting - // RewriteRule - // ^preferences/$ - // /main.php?test_name=user_accounts&subtest=my_preferences - - $_REQUEST["test_name"] = "user_accounts"; - $_REQUEST['subtest'] = "my_preferences"; - include_once __DIR__ . "/main.php"; } else { return false; diff --git a/smarty/templates/email/notifier_custom_html.tpl b/smarty/templates/email/notifier_custom_html.tpl index 1d00be49f49..18289490724 100644 --- a/smarty/templates/email/notifier_custom_html.tpl +++ b/smarty/templates/email/notifier_custom_html.tpl @@ -43,7 +43,7 @@

This is an automated message sent by the Loris system. To configure your notification settings, - follow this link to your preference page.

+ follow this link to your preference page.

From c0d60808508bc59f74714d4922dc51f78c8f21bd Mon Sep 17 00:00:00 2001 From: Xavier Lecours Date: Thu, 21 Dec 2017 11:51:56 -0500 Subject: [PATCH 003/108] [Issue Tracker] History actions grouped by time (#3194) This groups the changes in the history. It create a div for each chunk and adds some basic styling in the css file. This is probably doing less than #2730 but should satisfy what ever motivated it at first. --- modules/issue_tracker/ajax/EditIssue.php | 3 +- modules/issue_tracker/css/issue_tracker.css | 8 ++++ modules/issue_tracker/js/index.js | 2 +- modules/issue_tracker/jsx/CommentList.js | 50 ++++++++++++++------- 4 files changed, 43 insertions(+), 20 deletions(-) diff --git a/modules/issue_tracker/ajax/EditIssue.php b/modules/issue_tracker/ajax/EditIssue.php index c63b0220a91..098a9eb0bf1 100644 --- a/modules/issue_tracker/ajax/EditIssue.php +++ b/modules/issue_tracker/ajax/EditIssue.php @@ -429,8 +429,7 @@ function getComments($issueID) "FROM issues_history where issueID=:issueID " . "UNION " . "SELECT issueComment, 'comment', dateAdded, addedBy " . - "FROM issues_comments where issueID=:issueID " . - "ORDER BY dateAdded DESC", + "FROM issues_comments where issueID=:issueID ", array('issueID' => $issueID) ); diff --git a/modules/issue_tracker/css/issue_tracker.css b/modules/issue_tracker/css/issue_tracker.css index a74dd977623..e1bf02d1c30 100644 --- a/modules/issue_tracker/css/issue_tracker.css +++ b/modules/issue_tracker/css/issue_tracker.css @@ -72,3 +72,11 @@ h3 { .page-issue-tracker h3 { margin: 25px 0; } + +.history-item-label > span { + font-weight: bold; +} + +.history-item-changes { + margin-left: 2em; +} diff --git a/modules/issue_tracker/js/index.js b/modules/issue_tracker/js/index.js index d5e8d4083a5..e1ee90eadc7 100644 --- a/modules/issue_tracker/js/index.js +++ b/modules/issue_tracker/js/index.js @@ -1,2 +1,2 @@ -!function(modules){function __webpack_require__(moduleId){if(installedModules[moduleId])return installedModules[moduleId].exports;var module=installedModules[moduleId]={exports:{},id:moduleId,loaded:!1};return modules[moduleId].call(module.exports,module,module.exports,__webpack_require__),module.loaded=!0,module.exports}var installedModules={};return __webpack_require__.m=modules,__webpack_require__.c=installedModules,__webpack_require__.p="",__webpack_require__(0)}({0:function(module,exports,__webpack_require__){"use strict";function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}var _IssueForm=__webpack_require__(20),_IssueForm2=_interopRequireDefault(_IssueForm);$(function(){var args=QueryString.get(),issueTracker=React.createElement("div",{className:"page-issue-tracker"},React.createElement(_IssueForm2.default,{Module:"issue_tracker",DataURL:loris.BaseURL+"/issue_tracker/ajax/EditIssue.php?action=getData&issueID="+args.issueID,action:loris.BaseURL+"/issue_tracker/ajax/EditIssue.php?action=edit"}));ReactDOM.render(issueTracker,document.getElementById("lorisworkspace"))})},20:function(module,exports,__webpack_require__){"use strict";function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _classCallCheck(instance,Constructor){if(!(instance instanceof Constructor))throw new TypeError("Cannot call a class as a function")}function _possibleConstructorReturn(self,call){if(!self)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!call||"object"!=typeof call&&"function"!=typeof call?self:call}function _inherits(subClass,superClass){if("function"!=typeof superClass&&null!==superClass)throw new TypeError("Super expression must either be null or a function, not "+typeof superClass);subClass.prototype=Object.create(superClass&&superClass.prototype,{constructor:{value:subClass,enumerable:!1,writable:!0,configurable:!0}}),superClass&&(Object.setPrototypeOf?Object.setPrototypeOf(subClass,superClass):subClass.__proto__=superClass)}Object.defineProperty(exports,"__esModule",{value:!0});var _createClass=function(){function defineProperties(target,props){for(var i=0;i - [{commentHistory[commentID].dateAdded}] - {commentHistory[commentID].addedBy} - {action} - {commentHistory[commentID].newValue} + const changes = this.props.commentHistory.reduce(function(carry, item) { + let label = item.dateAdded.concat(" - ", item.addedBy); + if (!carry[label]) { + carry[label] = {}; + } + carry[label][item.fieldChanged] = item.newValue; + return carry; + }, {}); + + const history = Object.keys(changes).sort().reverse().map(function(key, i) { + const textItems = Object.keys(changes[key]).map(function(index, j) { + return ( +
+
+
{index}
+
to
+
+
{changes[key][index]}
); - } - } + }, this); + + return ( +
+
+
+ {key} updated : +
+
+ {textItems} +
+
+ ); + }, this); return (
@@ -52,7 +68,7 @@ class CommentList extends React.Component { {btnCommentsLabel}
- {historyText} + {history}
); From f13d4bc3e8d7d22aef881f23627afb097c5fd326 Mon Sep 17 00:00:00 2001 From: Justin Kat Date: Thu, 21 Dec 2017 15:52:37 -0500 Subject: [PATCH 004/108] [Core] Smarty cleanup (#3172) Remove unused smarty config files. --- smarty/cache/empty | 0 smarty/configs/empty | 0 2 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 smarty/cache/empty delete mode 100644 smarty/configs/empty diff --git a/smarty/cache/empty b/smarty/cache/empty deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/smarty/configs/empty b/smarty/configs/empty deleted file mode 100644 index e69de29bb2d..00000000000 From 9e5ff0e7d4874430ca48b19f7a8f0eaf5ec54d79 Mon Sep 17 00:00:00 2001 From: Shen Date: Thu, 21 Dec 2017 16:08:36 -0500 Subject: [PATCH 005/108] [Acknowledgements] update UI to react table (#3322) This adds React table for the Acknowledgements menu, and also fixes the bug where it can't show more than 25 pieces of data. --- .../acknowledgements/js/columnFormatter.js | 2 + .../acknowledgements/jsx/columnFormatter.js | 16 ++++++++ .../php/acknowledgements.class.inc | 1 + .../templates/menu_acknowledgements.tpl | 39 +++++-------------- .../test/candidate_listTest.php | 1 + webpack.config.js | 3 +- 6 files changed, 32 insertions(+), 30 deletions(-) create mode 100644 modules/acknowledgements/js/columnFormatter.js create mode 100644 modules/acknowledgements/jsx/columnFormatter.js diff --git a/modules/acknowledgements/js/columnFormatter.js b/modules/acknowledgements/js/columnFormatter.js new file mode 100644 index 00000000000..48f9c0d7244 --- /dev/null +++ b/modules/acknowledgements/js/columnFormatter.js @@ -0,0 +1,2 @@ +!function(modules){function __webpack_require__(moduleId){if(installedModules[moduleId])return installedModules[moduleId].exports;var module=installedModules[moduleId]={exports:{},id:moduleId,loaded:!1};return modules[moduleId].call(module.exports,module,module.exports,__webpack_require__),module.loaded=!0,module.exports}var installedModules={};return __webpack_require__.m=modules,__webpack_require__.c=installedModules,__webpack_require__.p="",__webpack_require__(0)}([function(module,exports){"use strict";function formatAcknowledgementsColumn(column,cell,rowData,rowHeaders){return React.createElement("td",null,cell)}window.formatAcknowledgementsColumn=formatAcknowledgementsColumn}]); +//# sourceMappingURL=columnFormatter.js.map \ No newline at end of file diff --git a/modules/acknowledgements/jsx/columnFormatter.js b/modules/acknowledgements/jsx/columnFormatter.js new file mode 100644 index 00000000000..c7779cec5c5 --- /dev/null +++ b/modules/acknowledgements/jsx/columnFormatter.js @@ -0,0 +1,16 @@ +/* exported formatAcknowledgementsColumn */ + +/** + * Modify behaviour of specified column cells in the Data Table component + * @param {string} column - column name + * @param {string} cell - cell content + * @param {arrray} rowData - array of cell contents for a specific row + * @param {arrray} rowHeaders - array of headers for the table + * @return {*} a formated table cell for a given column + */ +function formatAcknowledgementsColumn(column, cell, rowData, rowHeaders) { + return {cell}; +} + +window.formatAcknowledgementsColumn = formatAcknowledgementsColumn; + diff --git a/modules/acknowledgements/php/acknowledgements.class.inc b/modules/acknowledgements/php/acknowledgements.class.inc index fd7fae7e93c..e56d3cf8951 100644 --- a/modules/acknowledgements/php/acknowledgements.class.inc +++ b/modules/acknowledgements/php/acknowledgements.class.inc @@ -231,6 +231,7 @@ class Acknowledgements extends \NDB_Menu_Filter_Form $baseDeps, array( $baseurl . '/acknowledgements/js/acknowledgements_helper.js', + $baseurl . '/acknowledgements/js/columnFormatter.js', ) ); diff --git a/modules/acknowledgements/templates/menu_acknowledgements.tpl b/modules/acknowledgements/templates/menu_acknowledgements.tpl index 7af64edb67a..aac79c57f05 100644 --- a/modules/acknowledgements/templates/menu_acknowledgements.tpl +++ b/modules/acknowledgements/templates/menu_acknowledgements.tpl @@ -187,39 +187,20 @@ - {section name=item loop=$items} - - {section name=piece loop=$items[item]} - {if $items[item][piece].name != ""} - - {if $items[item][piece].value == "bachelors"} - Bachelors - {elseif $items[item][piece].value == "masters"} - Masters - {elseif $items[item][piece].value == "phd"} - PhD - {elseif $items[item][piece].value == "postdoc"} - Postdoctoral - {elseif $items[item][piece].value == "md"} - MD - {elseif $items[item][piece].value == "registered_nurse"} - Registered Nurse - {else} - {$items[item][piece].value} - {/if} - - {/if} - {/section} - - {sectionelse} - - You're not alone. - - {/section} +
+
+ diff --git a/modules/candidate_list/test/candidate_listTest.php b/modules/candidate_list/test/candidate_listTest.php index 5f25a3e60d2..343cced3c9f 100644 --- a/modules/candidate_list/test/candidate_listTest.php +++ b/modules/candidate_list/test/candidate_listTest.php @@ -350,6 +350,7 @@ function testPSCIDLink() "document.querySelectorAll('#dynamictable > tbody > tr >". " td.dynamictableFrozenColumn > a')[0].click()" ); + sleep(1); //make sure that breadcrumb contains DCCID $text = $this->webDriver->executescript( "return document.querySelector('#bc2 > a:nth-child(3)>div').textContent" diff --git a/webpack.config.js b/webpack.config.js index 1909a4e47b7..55f0143fc60 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -49,7 +49,8 @@ var config = [{ './modules/help_editor/js/columnFormatter.js': './modules/help_editor/jsx/columnFormatter.js', './modules/brainbrowser/js/Brainbrowser.js': './modules/brainbrowser/jsx/Brainbrowser.js', './modules/data_integrity_flag/js/index.js': './modules/data_integrity_flag/jsx/index.js', - './modules/imaging_uploader/js/index.js': './modules/imaging_uploader/jsx/index.js' + './modules/imaging_uploader/js/index.js': './modules/imaging_uploader/jsx/index.js', + './modules/acknowledgements/js/columnFormatter.js': './modules/acknowledgements/jsx/columnFormatter.js' }, output: { path: './', From f8caf2b1483b4baa92f37520411ad24e00dc1fdb Mon Sep 17 00:00:00 2001 From: Dave MacFarlane Date: Thu, 21 Dec 2017 16:09:04 -0500 Subject: [PATCH 006/108] [survey_accounts] Add design specification for survey_accounts (#3309) This adds a document describing the design of survey_accounts following the format in PR#3286 --- modules/survey_accounts/README.md | 44 +++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 modules/survey_accounts/README.md diff --git a/modules/survey_accounts/README.md b/modules/survey_accounts/README.md new file mode 100644 index 00000000000..7f3a08d5eb6 --- /dev/null +++ b/modules/survey_accounts/README.md @@ -0,0 +1,44 @@ +# Survey Accounts + +## Purpose + +The survey accounts module is intended to manage the surveys which +are sent to participants in a study. It is used to generate a survey +key, and (optionally) automatically send it to the participant, if +their email address is provided. + +## Intended Users + +The survey accounts module is primarily used by study coordinators +in order to generate a survey key to send to participants. + +## Scope + +The survey accounts module only generates new survey keys. It does +*not* collect the data for the surveys, which are written as LORIS +instruments and collected via a different survey.php script in the +LORIS `htdocs` directory. + +## Permissions + +Accessing the survey accounts module requires the `user_accounts` +LORIS permission. + +## Configurations + +Each survey must be created as a LORIS instrument, and the +"IsDirectEntry" column in the `test_names` table in MySQL must be +set to "true" in order for the survey to show the instrument in the +dropdown for the list of available surveys in the `add_survey` page. + + +## Interactions with LORIS + +The module validates that surveys have not already been sent to a +participant by comparing the test_name against the LORIS `flag` +table. As a result, surveys should not be inserted into the LORIS +`test_battery` table or attempting to send a survey will result in +an error. + +The `add_survey` page generates a link to `$LORIS/survey.php?key=....` +in the email that it sends after generating a new survey key. From a92e2ca498c75e8edeb34c688c95192817659710 Mon Sep 17 00:00:00 2001 From: Dave MacFarlane Date: Thu, 21 Dec 2017 16:12:11 -0500 Subject: [PATCH 007/108] [Imaging Browser] Add specification (#3286) This adds a brief specification for the imaging browser as discussed in the LORIS meeting. It serves as a template for other modules. --- modules/imaging_browser/README.md | 82 +++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 modules/imaging_browser/README.md diff --git a/modules/imaging_browser/README.md b/modules/imaging_browser/README.md new file mode 100644 index 00000000000..71db858c0c6 --- /dev/null +++ b/modules/imaging_browser/README.md @@ -0,0 +1,82 @@ +# Imaging Browser + +## Purpose + +The imaging browser is intended to allow users to view candidate +scan sessions collected for a study. + +## Intended Users + +The three primary types of users are: +1. Imaging specialists using the modules to do online QC +2. Project radiologists viewing images to report incidental findings +3. Site coordinators or researchers ensuring their uploaded scans have + been processed and inserted into LORIS. + +## Scope + +The imaging browser displays processed images which meet the study's +inclusion criteria. The inclusion criteria (for most images) is defined +and enforced in the LORIS imaging pipeline scripts. Derived or +processed scan types are also included and have their own insertion +criteria. + +NOT in scope: + +The imaging browser module does not display raw DICOMs or perform automated +quality control on images. It only displays images that have already been +inserted into LORIS. + +## Permissions + +The imaging browser module uses the following permissions. Any one of them +(except imaging_browser_qc) is sufficient to have access to the module. + +imaging_browser_view_allsites + - This permission gives the user access to all scans in the database + +imaging_browser_view_site + - This permission gives the user access to scans from their own site(s) only + +imaging_browser_phantom_allsites + - This permission gives the user access to phantom, but not candidate, scans + across the database + +imaging_browser_phantom_ownsite + - This permission gives the user access to phantom, but not candidate, data + at the user's site(s). + +imaging_browser_qc + - This permission gives the user access to modify the quality control data + for the associated scans and timepoints. + +## Configurations + +The imaging browser has the following configurations that affect its usage + +tblScanTypes - This setting determines which scan types are considered "NEW" for + QC purposes. It also determines which modalities are displayed on the + main imaging browser menu page. + +useProjects - This setting determines whether "project" filtering dropdowns exist + on the menu page. + +useEDC - This setting determines whether "EDC" filtering dropdowns exist + on the menu page. + +mantis_url - This setting defines a URL for LORIS to include a link to for bug reporting + on the viewsession page. + +## Interactions with LORIS + +- The "Selected" set by the imaging QC specialist is used by the dataquery + module in order to determine which scan to insert when multiple scans of + a modality type exist for a given session. (The importer exists in + `$LORIS/tools/CouchDB_Import_MRI.php` alongside all other CouchDB + import scripts, but should logically be considered part of this module.) +- The imaging browser module includes links to BrainBrowser to visualize data. +- The control panel on the viewsession page includes links to instruments + named "mri_parameter_form" and "radiologyreview" if they exist for the + currently viewed session. +- The control panel on the viewsession page includes links to the DICOM Archive + for any DICOM tars associated with the given session. From 105024454252e4dcb28c3e6bcf2b0b23fb2719ac Mon Sep 17 00:00:00 2001 From: Xavier Lecours Date: Fri, 22 Dec 2017 14:07:35 -0500 Subject: [PATCH 008/108] [LorisForm] Fix PHP warning (#3326) Fix the notice "PHP Notice: Undefined index: ... in php/libraries/LorisForm.class.inc on line 1296" --- php/libraries/LorisForm.class.inc | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/php/libraries/LorisForm.class.inc b/php/libraries/LorisForm.class.inc index 27f173a457b..d5924b34c7d 100644 --- a/php/libraries/LorisForm.class.inc +++ b/php/libraries/LorisForm.class.inc @@ -1292,11 +1292,7 @@ class LorisForm // Check if date fields have errors and add them to error list if ($el['type'] === 'date') { - $date = ( - $values[$elName] ? - $values[$elName] : - $this->getValue($el['name']) - ); + $date = $values[$elName] ?? $this->getValue($el['name']); if (empty($date)) { continue; } From 6da917a359ff346bc3bafbf3d9f93e70e91b45cf Mon Sep 17 00:00:00 2001 From: HenriRabalais Date: Wed, 3 Jan 2018 15:42:46 +0100 Subject: [PATCH 009/108] [JSX] Added a LinkElement to jsx library (#3347) This creates a react form element that has an href hyperlink destination which fits takes up a row in the LORIS bootstrap theme. This is accomplished by creating a new form element in the loris/jsx library that is modelled after the "Static Element". However, the "Link Element" also includes an 'href' prop type which allows the developer to set a hyperlink destination for the given element. --- htdocs/js/components/Form.js | 2 +- jsx/Form.js | 45 +++++++++++++++++++++++++++++++++++- 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/htdocs/js/components/Form.js b/htdocs/js/components/Form.js index 70d44cec088..dddcc5b590e 100644 --- a/htdocs/js/components/Form.js +++ b/htdocs/js/components/Form.js @@ -1,2 +1,2 @@ -!function(modules){function __webpack_require__(moduleId){if(installedModules[moduleId])return installedModules[moduleId].exports;var module=installedModules[moduleId]={exports:{},id:moduleId,loaded:!1};return modules[moduleId].call(module.exports,module,module.exports,__webpack_require__),module.loaded=!0,module.exports}var installedModules={};return __webpack_require__.m=modules,__webpack_require__.c=installedModules,__webpack_require__.p="",__webpack_require__(0)}([function(module,exports){"use strict";Object.defineProperty(exports,"__esModule",{value:!0});var FormElement=React.createClass({displayName:"FormElement",propTypes:{name:React.PropTypes.string.isRequired,id:React.PropTypes.string,method:React.PropTypes.oneOf(["POST","GET"]),action:React.PropTypes.string,class:React.PropTypes.string,columns:React.PropTypes.number,formElements:React.PropTypes.shape({elementName:React.PropTypes.shape({name:React.PropTypes.string,type:React.PropTypes.string})}),onSubmit:React.PropTypes.func,onUserInput:React.PropTypes.func},getDefaultProps:function(){return{name:null,id:null,method:"POST",action:void 0,class:"form-horizontal",columns:1,fileUpload:!1,formElements:{},onSubmit:function(){console.warn("onSubmit() callback is not set!")}}},getFormElements:function(){var formElementsHTML=[],columns=this.props.columns,maxColumnSize=12,colSize=Math.floor(maxColumnSize/columns),colClass="col-xs-12 col-sm-"+colSize+" col-md-"+colSize,filter=this.props.formElements;return Object.keys(filter).forEach(function(objKey,index){var userInput=this.props.onUserInput?this.props.onUserInput:filter[objKey].onUserInput,value=filter[objKey].value?filter[objKey].value:"";formElementsHTML.push(React.createElement("div",{key:"el_"+index,className:colClass},React.createElement(LorisElement,{element:filter[objKey],onUserInput:userInput,value:value})))}.bind(this)),React.Children.forEach(this.props.children,function(child,key){var elementClass="col-xs-12 col-sm-12 col-md-12";React.isValidElement(child)&&"function"==typeof child.type&&(elementClass=colClass),formElementsHTML.push(React.createElement("div",{key:"el_child_"+key,className:elementClass},child))}),formElementsHTML},handleSubmit:function(e){this.props.onSubmit&&(e.preventDefault(),this.props.onSubmit(e))},render:function(){var encType=this.props.fileUpload?"multipart/form-data":null,formElements=this.getFormElements(),rowStyles={display:"flex",flexWrap:"wrap"};return React.createElement("form",{name:this.props.name,id:this.props.id,className:this.props.class,method:this.props.method,action:this.props.action,encType:encType,onSubmit:this.handleSubmit},React.createElement("div",{className:"row",style:rowStyles},formElements))}}),SelectElement=React.createClass({displayName:"SelectElement",propTypes:{name:React.PropTypes.string.isRequired,options:React.PropTypes.object.isRequired,label:React.PropTypes.string,value:React.PropTypes.oneOfType([React.PropTypes.string,React.PropTypes.array]),id:React.PropTypes.string,class:React.PropTypes.string,multiple:React.PropTypes.bool,disabled:React.PropTypes.bool,required:React.PropTypes.bool,emptyOption:React.PropTypes.bool,hasError:React.PropTypes.bool,errorMessage:React.PropTypes.string,onUserInput:React.PropTypes.func},getDefaultProps:function(){return{name:"",options:{},label:"",value:void 0,id:"",class:"",multiple:!1,disabled:!1,required:!1,emptyOption:!0,hasError:!1,errorMessage:"The field is required!",onUserInput:function(){console.warn("onUserInput() callback is not set")}}},handleChange:function(e){var value=e.target.value,options=e.target.options;if(this.props.multiple&&options.length>1){value=[];for(var i=0,l=options.length;i1){value=[];for(var i=0,l=options.length;i + + + + ); + } +}); + /** * Button component * React wrapper for