Fix to support use of the Social Screening Tool encounter form in the patient portal
[openemr.git] / apis / dispatch.php
blobbb34928caf6c883b8af15b34070e813eadd7add6
1 <?php
3 /**
4 * Rest Dispatch
6 * @package OpenEMR
7 * @link http://www.open-emr.org
8 * @author Matthew Vita <matthewvita48@gmail.com>
9 * @author Jerry Padgett <sjpadgett@gmail.com>
10 * @author Brady Miller <brady.g.miller@gmail.com>
11 * @copyright Copyright (c) 2018 Matthew Vita <matthewvita48@gmail.com>
12 * @copyright Copyright (c) 2020 Jerry Padgett <sjpadgett@gmail.com>
13 * @copyright Copyright (c) 2019-2020 Brady Miller <brady.g.miller@gmail.com>
14 * @license https://github.com/openemr/openemr/blob/master/LICENSE GNU General Public License 3
17 // below brings in autoloader
18 require_once("./../_rest_config.php");
20 use OpenEMR\Common\Auth\UuidUserAccount;
21 use OpenEMR\Common\Csrf\CsrfUtils;
22 use OpenEMR\Common\Http\HttpRestRouteHandler;
23 use OpenEMR\Common\Http\HttpRestRequest;
24 use OpenEMR\Common\Logging\SystemLogger;
25 use OpenEMR\Common\Session\SessionUtil;
26 use OpenEMR\Common\Uuid\UuidRegistry;
27 use OpenEMR\FHIR\SMART\SmartLaunchController;
28 use OpenEMR\Events\RestApiExtend\RestApiCreateEvent;
29 use Psr\Http\Message\ResponseInterface;
31 $gbl = RestConfig::GetInstance();
32 $restRequest = new HttpRestRequest($gbl, $_SERVER);
33 $routes = array();
35 // Parse needed information from Redirect or REQUEST_URI
36 $resource = $gbl::getRequestEndPoint();
37 $logger = new SystemLogger();
38 $logger->debug("dispatch.php requested", ["resource" => $resource, "method" => $_SERVER['REQUEST_METHOD']]);
40 $skipApiAuth = false;
41 if (!empty($_SERVER['HTTP_APICSRFTOKEN'])) {
42 // Calling api from within the same session (ie. isLocalApi) since a apicsrftoken header was passed
43 $isLocalApi = true;
44 $gbl::setLocalCall();
45 $skipApiAuth = false;
46 $ignoreAuth = false;
47 } elseif ($gbl::skipApiAuth($resource)) {
48 // For rest api endpoints that do not require auth, such as the capability statement
49 // note that the site is validated in the skipApiAuth() function
50 // refactor resource
51 $resource = str_replace('/' . $gbl::$SITE, '', $resource);
52 // set site
53 $_GET['site'] = $gbl::$SITE;
54 $isLocalApi = false;
55 $skipApiAuth = true;
56 $ignoreAuth = true;
57 } else {
58 // Calling api via rest
59 // ensure token is valid
60 $tokenRaw = $gbl::verifyAccessToken();
61 if ($tokenRaw instanceof ResponseInterface) {
62 $logger->error("dispatch.php failed token verify for resource", ["resource" => $resource]);
63 // failed token verify
64 // not a request object so send the error as response obj
65 $gbl::emitResponse($tokenRaw);
66 exit;
69 // collect token attributes
70 $attributes = $tokenRaw->getAttributes();
72 // collect site
73 $site = '';
74 $scopes = $attributes['oauth_scopes'];
75 $logger->debug("Parsed oauth_scopes in AccessToken", ["scopes" => $scopes]);
76 foreach ($scopes as $attr) {
77 if (stripos($attr, 'site:') !== false) {
78 $site = str_replace('site:', '', $attr);
79 // while here parse site from endpoint
80 $resource = str_replace('/' . $site, '', $resource);
83 // set our scopes and updated resources as needed
84 $restRequest->setAccessTokenScopes($scopes);
86 // ensure 1) sane site 2) site from gbl and access token are the same and 3) ensure the site exists on filesystem
87 if (empty($site) || empty($gbl::$SITE) || preg_match('/[^A-Za-z0-9\\-.]/', $gbl::$SITE) || ($site !== $gbl::$SITE) || !file_exists(__DIR__ . '/../sites/' . $gbl::$SITE)) {
88 $logger->error("OpenEMR Error - api site error, so forced exit");
89 http_response_code(400);
90 exit();
92 // set the site
93 $_GET['site'] = $site;
95 // set the scopes globals for endpoint permission checking
96 $GLOBALS['oauth_scopes'] = $scopes;
98 // collect openemr user uuid
99 $userId = $attributes['oauth_user_id'];
100 // collect client id (will be empty for PKCE)
101 $clientId = $attributes['oauth_client_id'] ?? null;
102 // collect token id
103 $tokenId = $attributes['oauth_access_token_id'];
104 // ensure user uuid and token id are populated
105 if (empty($userId) || empty($tokenId)) {
106 $logger->error("OpenEMR Error - userid or tokenid not available, so forced exit", ['attributes' => $attributes]);
107 http_response_code(400);
108 exit();
110 $restRequest->setClientId($clientId);
111 $restRequest->setAccessTokenId($tokenId);
113 // Get a site id from initial login authentication.
114 $isLocalApi = false;
115 $skipApiAuth = false;
116 $ignoreAuth = true;
119 // set the route as well as the resource information. Note $resource is actually the route and not the resource name.
120 $restRequest->setRequestPath($resource);
122 if (!$isLocalApi) {
123 // Will start the api OpenEMR session/cookie.
124 SessionUtil::apiSessionStart($gbl::$web_root);
127 $GLOBALS['is_local_api'] = $isLocalApi;
128 $restRequest->setIsLocalApi($isLocalApi);
130 // Set $sessionAllowWrite to true here for following reasons:
131 // 1. !$isLocalApi - not applicable since use the SessionUtil::apiSessionStart session, which was set above
132 // 2. $isLocalApi - in this case, basically setting this to true downstream after some session sets via session_write_close() call
133 $sessionAllowWrite = true;
134 require_once("./../interface/globals.php");
136 // we now can check the database to see if the token is revoked
137 // Note despite League\OAuth2\Server\AuthorizationValidators\BearerTokenValidator.php:L117 already checking for revoked
138 // access token we have to do this logic here as we use the access token SCOPE parameter to determine our multi-site setting
139 // and load up the correct database, our earlier access token logic returns false for revoked as we don't have db access
140 // for that reason we have this double check on validating the access token.
141 if (!empty($tokenId)) {
142 $result = $gbl::validateAccessTokenRevoked($tokenId);
143 if ($result instanceof ResponseInterface) {
144 $logger->error("dispatch.php access token was revoked", ["resource" => $resource]);
145 // failed token verify
146 // not a request object so send the error as response obj
147 $gbl::emitResponse($result);
148 exit;
153 // recollect this so the DEBUG global can be used if set
154 $logger = new SystemLogger();
156 $gbl::$apisBaseFullUrl = $GLOBALS['site_addr_oath'] . $GLOBALS['webroot'] . "/apis/" . $gbl::$SITE;
157 $restRequest->setApiBaseFullUrl($gbl::$apisBaseFullUrl);
159 if ($isLocalApi) {
160 // need to check for csrf match when using api locally
161 $csrfFail = false;
163 if (empty($_SERVER['HTTP_APICSRFTOKEN'])) {
164 $logger->error("OpenEMR Error: internal api failed because csrf token not received");
165 $csrfFail = true;
168 if ((!$csrfFail) && (!CsrfUtils::verifyCsrfToken($_SERVER['HTTP_APICSRFTOKEN'], 'api'))) {
169 $logger->error("OpenEMR Error: internal api failed because csrf token did not match");
170 $csrfFail = true;
173 if ($csrfFail) {
174 $logger->error("dispatch.php CSRF failed", ["resource" => $resource]);
175 http_response_code(401);
176 exit();
178 } elseif ($skipApiAuth) {
179 $logger->debug("dispatch.php skipping api auth");
180 // For endpoints that do not require auth, such as the capability statement
181 } else {
182 $logger->debug("dispatch.php authenticating user");
183 // verify that user tokens haven't been revoked.
184 // this is done by verifying the user is trusted with active auth session.
185 $isTrusted = $gbl::isTrustedUser($attributes["oauth_client_id"], $attributes["oauth_user_id"]);
186 if ($isTrusted instanceof ResponseInterface) {
187 $logger->debug("dispatch.php oauth2 inactive user api attempt");
188 // user is not logged on to server with an active session.
189 // too me this is easier than revoking tokens or using phantom tokens.
190 // give a 400(unsure here, could be a 401) so client can redirect to server.
191 $gbl::destroySession();
192 $gbl::emitResponse($isTrusted);
193 exit;
195 // $isTrusted can be used for further validations using session_cache
196 // which is a json. json_decode($isTrusted['session_cache'])
198 // authenticate the token
199 if (!$gbl->authenticateUserToken($tokenId, $clientId, $userId)) {
200 $logger->error("dispatch.php api call with invalid token");
201 $gbl::destroySession();
202 http_response_code(401);
203 exit();
206 // collect user information and user role
207 $uuidToUser = new UuidUserAccount($userId);
208 $user = $uuidToUser->getUserAccount();
209 $userRole = $uuidToUser->getUserRole();
210 if (empty($user)) {
211 // unable to identify the users user role
212 $logger->error("OpenEMR Error - api user account could not be identified, so forced exit", [
213 'userId' => $userId,
214 'userRole' => $uuidToUser->getUserRole()]);
215 $gbl::destroySession();
216 http_response_code(400);
217 exit();
219 if (empty($userRole)) {
220 // unable to identify the users user role
221 $logger->error("OpenEMR Error - api user role for user could not be identified, so forced exit");
222 $gbl::destroySession();
223 http_response_code(400);
224 exit();
227 $restRequest->setAccessTokenId($tokenId);
228 $restRequest->setRequestUserRole($userRole);
229 $restRequest->setRequestUser($userId, $user);
231 // verify that the scope covers the route
232 if (
233 // fhir routes are the default and can send openid/fhirUser w/ authorization_code, or no scopes at all
234 // with Client Credentials, so we only reject requests for standard or portal if the correct scope is not
235 // sent.
236 ($gbl::is_api_request($resource) && !in_array('api:oemr', $GLOBALS['oauth_scopes'])) ||
237 ($gbl::is_portal_request($resource) && !in_array('api:port', $GLOBALS['oauth_scopes']))
239 $logger->error("dispatch.php api call with token that does not cover the requested route");
240 $gbl::destroySession();
241 http_response_code(401);
242 exit();
244 // ensure user role has access to the resource
245 // for now assuming:
246 // users has access to oemr and fhir
247 // patient has access to port and fhir
248 if ($userRole == 'users' && ($gbl::is_api_request($resource) || $gbl::is_fhir_request($resource))) {
249 $logger->debug("dispatch.php valid role and user has access to api/fhir resource", ['resource' => $resource]);
250 // good to go
251 } elseif ($userRole == 'patient' && ($gbl::is_portal_request($resource) || $gbl::is_fhir_request($resource))) {
252 $logger->debug("dispatch.php valid role and patient has access portal resource", ['resource' => $resource]);
253 // good to go
254 } elseif ($userRole === 'system' && ($gbl::is_fhir_request($resource))) {
255 $logger->debug("dispatch.php valid role and system has access to api/fhir resource", ['resource' => $resource]);
256 } else {
257 $logger->error("OpenEMR Error: api failed because user role does not have access to the resource", ['resource' => $resource, 'userRole' => $userRole]);
258 $gbl::destroySession();
259 http_response_code(401);
260 exit();
262 // set pertinent session variables
263 if ($userRole == 'users') {
264 $_SESSION['authUser'] = $user["username"] ?? null;
265 $_SESSION['authUserID'] = $user["id"] ?? null;
266 $_SESSION['authProvider'] = sqlQueryNoLog("SELECT `name` FROM `groups` WHERE `user` = ?", [$_SESSION['authUser']])['name'] ?? null;
267 if (empty($_SESSION['authUser']) || empty($_SESSION['authUserID']) || empty($_SESSION['authProvider'])) {
268 // this should never happen
269 $logger->error("OpenEMR Error: api failed because unable to set critical users session variables");
270 $gbl::destroySession();
271 http_response_code(401);
272 exit();
274 if ($restRequest->requestHasScope(SmartLaunchController::CLIENT_APP_STANDALONE_LAUNCH_SCOPE)) {
275 $restRequest = $gbl->populateTokenContextForRequest($restRequest);
277 } elseif ($userRole == 'patient') {
278 $_SESSION['pid'] = $user['pid'] ?? null;
279 $puuidCheck = $user['uuid'] ?? null;
280 $puuidStringCheck = UuidRegistry::uuidToString($puuidCheck) ?? null;
281 if (empty($_SESSION['pid']) || empty($puuidCheck) || empty($puuidStringCheck)) {
282 // this should never happen
283 $logger->error("OpenEMR Error: api failed because unable to set critical patient session variables");
284 $gbl::destroySession();
285 http_response_code(401);
286 exit();
288 $restRequest->setPatientRequest(true);
289 $restRequest->setPatientUuidString($puuidStringCheck);
290 } else if ($userRole === 'system') {
291 $_SESSION['authUser'] = $user["username"] ?? null;
292 $_SESSION['authUserID'] = $user["id"] ?? null;
293 if (
294 empty($_SESSION['authUser'])
295 // this should never happen as the system role depends on the system username... but we safety check it anyways
296 || $_SESSION['authUser'] != \OpenEMR\Services\UserService::SYSTEM_USER_USERNAME
297 || empty($_SESSION['authUserID'])
299 $logger->error("OpenEMR Error: api failed because unable to set critical users session variables");
300 $gbl::destroySession();
301 http_response_code(401);
302 exit();
304 } else {
305 // this user role is not supported
306 $logger->error("OpenEMR Error - api user role that was provided is not supported, so forced exit");
307 $gbl::destroySession();
308 http_response_code(400);
309 exit();
313 //Extend API using RestApiCreateEvent
314 $restApiCreateEvent = new RestApiCreateEvent($gbl::$ROUTE_MAP, $gbl::$FHIR_ROUTE_MAP, $gbl::$PORTAL_ROUTE_MAP, $restRequest);
315 $restApiCreateEvent = $GLOBALS["kernel"]->getEventDispatcher()->dispatch(RestApiCreateEvent::EVENT_HANDLE, $restApiCreateEvent, 10);
316 $gbl::$ROUTE_MAP = $restApiCreateEvent->getRouteMap();
317 $gbl::$FHIR_ROUTE_MAP = $restApiCreateEvent->getFHIRRouteMap();
318 $gbl::$PORTAL_ROUTE_MAP = $restApiCreateEvent->getPortalRouteMap();
319 $restRequest = $restApiCreateEvent->getRestRequest();
321 // api flag must be four chars
322 // Pass only routes for current api.
323 // Also check to ensure route is turned on in globals
324 if ($gbl::is_fhir_request($resource)) {
325 if (!$GLOBALS['rest_fhir_api'] && !$isLocalApi) {
326 // if the external fhir api is turned off and this is not a local api call, then exit
327 $logger->error("dispatch.php attempted to access resource with FHIR api turned off ", ['resource' => $resource]);
328 $gbl::destroySession();
329 http_response_code(501);
330 exit();
332 $_SESSION['api'] = 'fhir';
333 $routes = $gbl::$FHIR_ROUTE_MAP;
334 } elseif ($gbl::is_portal_request($resource)) {
335 if (!$GLOBALS['rest_portal_api'] && !$isLocalApi) {
336 $logger->error("dispatch.php attempted to access resource with portal api turned off ", ['resource' => $resource]);
337 // if the external portal api is turned off and this is not a local api call, then exit
338 $gbl::destroySession();
339 http_response_code(501);
340 exit();
342 $_SESSION['api'] = 'port';
343 $routes = $gbl::$PORTAL_ROUTE_MAP;
344 } elseif ($gbl::is_api_request($resource)) {
345 if (!$GLOBALS['rest_api'] && !$isLocalApi) {
346 $logger->error(
347 "dispatch.php attempted to access resource with REST api turned off ",
348 ['resource' => $resource]
350 // if the external api is turned off and this is not a local api call, then exit
351 $gbl::destroySession();
352 http_response_code(501);
353 exit();
355 $_SESSION['api'] = 'oemr';
356 $routes = $gbl::$ROUTE_MAP;
357 } else {
358 $logger->error("dispatch.php invalid access to resource", ['resource' => $resource]);
360 // somebody is up to no good
361 if (!$isLocalApi) {
362 $gbl::destroySession();
364 http_response_code(501);
365 exit();
368 $restRequest->setApiType($_SESSION['api']);
370 if ($isLocalApi) {
371 // Ensure that a local process does not hold up other processes
372 // Note can not do this for !$isLocalApi since need to be able to set
373 // session variables and it won't help performance anyways.
374 session_write_close();
377 // dispatch $routes called by ref (note storing the output in a variable to allow option
378 // to destroy the session/cookie before sending the output back)
379 ob_start();
380 $dispatchResult = HttpRestRouteHandler::dispatch($routes, $restRequest);
381 $apiCallOutput = ob_get_clean();
382 // Tear down session for security.
383 if (!$isLocalApi) {
384 $gbl::destroySession();
386 // Send the output if not empty
387 if (!empty($apiCallOutput)) {
388 echo $apiCallOutput;
389 } else if ($dispatchResult instanceof ResponseInterface) {
390 RestConfig::emitResponse($dispatchResult);
393 // prevent 200 if route doesn't exist
394 if ($dispatchResult === false) {
395 $logger->debug("dispatch.php no route found for resource", ['resource' => $resource]);
396 http_response_code(404);