4 * Useful globals class for Rest
7 * @link http://www.open-emr.org
8 * @author Jerry Padgett <sjpadgett@gmail.com>
9 * @author Brady Miller <brady.g.miller@gmail.com>
10 * @copyright Copyright (c) 2018-2020 Jerry Padgett <sjpadgett@gmail.com>
11 * @copyright Copyright (c) 2019 Brady Miller <brady.g.miller@gmail.com>
12 * @license https://github.com/openemr/openemr/blob/master/LICENSE GNU General Public License 3
15 require_once __DIR__
. '/vendor/autoload.php';
17 use Laminas\HttpHandlerRunner\Emitter\SapiEmitter
;
18 use League\OAuth2\Server\Exception\OAuthServerException
;
19 use League\OAuth2\Server\ResourceServer
;
20 use Nyholm\Psr7\Factory\Psr17Factory
;
21 use Nyholm\Psr7Server\ServerRequestCreator
;
22 use OpenEMR\Common\Acl\AclMain
;
23 use OpenEMR\Common\Auth\OpenIDConnect\Repositories\AccessTokenRepository
;
24 use OpenEMR\Common\Http\HttpRestRequest
;
25 use OpenEMR\Common\Logging\EventAuditLogger
;
26 use OpenEMR\Common\Logging\SystemLogger
;
27 use OpenEMR\Common\Session\SessionUtil
;
28 use OpenEMR\FHIR\Config\ServerConfig
;
29 use OpenEMR\Services\TrustedUserService
;
30 use Psr\Http\Message\ResponseInterface
;
31 use Psr\Http\Message\ServerRequestInterface
;
34 // also a handy place to add utility methods
35 // TODO before v6 release: refactor http_response_code(); for psr responses.
39 /** @var routemap is an array of patterns and routes */
40 public static $ROUTE_MAP;
42 /** @var fhir routemap is an of patterns and routes */
43 public static $FHIR_ROUTE_MAP;
45 /** @var portal routemap is an of patterns and routes */
46 public static $PORTAL_ROUTE_MAP;
48 /** @var app root is the root directory of the application */
49 public static $APP_ROOT;
51 /** @var root url of the application */
52 public static $ROOT_URL;
53 // you can guess what the rest are!
54 public static $VENDOR_DIR;
56 public static $apisBaseFullUrl;
57 public static $webserver_root;
58 public static $web_root;
59 public static $server_document_root;
60 public static $publicKey;
61 private static $INSTANCE;
62 private static $IS_INITIALIZED = false;
63 /** @var set to true if local api call */
64 private static $localCall = false;
65 /** @var set to true if not rest call */
66 private static $notRestCall = false;
68 /** prevents external construction */
69 private function __construct()
74 * Returns an instance of the RestConfig singleton
78 public static function GetInstance(): \RestConfig
80 if (!self
::$IS_INITIALIZED) {
84 if (!self
::$INSTANCE instanceof self
) {
85 self
::$INSTANCE = new self();
88 return self
::$INSTANCE;
92 * Initialize the RestConfig object
94 public static function Init(): void
96 if (self
::$IS_INITIALIZED) {
101 self
::setSiteFromEndpoint();
102 $serverConfig = new ServerConfig();
103 $serverConfig->setWebServerRoot(self
::$webserver_root);
104 $serverConfig->setSiteId(self
::$SITE);
105 self
::$ROOT_URL = self
::$web_root . "/apis";
106 self
::$VENDOR_DIR = self
::$webserver_root . "/vendor";
107 self
::$publicKey = $serverConfig->getPublicRestKey();
108 self
::$IS_INITIALIZED = true;
112 * Basic paths when GLOBALS are not yet available.
116 private static function SetPaths(): void
118 $isWindows = (stripos(PHP_OS_FAMILY
, 'WIN') === 0);
119 // careful if moving this class to modify where's root.
120 self
::$webserver_root = __DIR__
;
122 //convert windows path separators
123 self
::$webserver_root = str_replace("\\", "/", self
::$webserver_root);
125 // Collect the apache server document root (and convert to windows slashes, if needed)
126 self
::$server_document_root = realpath($_SERVER['DOCUMENT_ROOT']);
128 //convert windows path separators
129 self
::$server_document_root = str_replace("\\", "/", self
::$server_document_root);
131 self
::$web_root = substr(self
::$webserver_root, strspn(self
::$webserver_root ^ self
::$server_document_root, "\0"));
132 // Ensure web_root starts with a path separator
133 if (preg_match("/^[^\/]/", self
::$web_root)) {
134 self
::$web_root = "/" . self
::$web_root;
136 // Will need these occasionally. sql init comes to mind!
137 $GLOBALS['rootdir'] = self
::$web_root . "/interface";
138 // Absolute path to the source code include and headers file directory (Full path):
139 $GLOBALS['srcdir'] = self
::$webserver_root . "/library";
140 // Absolute path to the location of documentroot directory for use with include statements:
141 $GLOBALS['fileroot'] = self
::$webserver_root;
142 // Absolute path to the location of interface directory for use with include statements:
143 $GLOBALS['incdir'] = self
::$webserver_root . "/interface";
144 // Absolute path to the location of documentroot directory for use with include statements:
145 $GLOBALS['webroot'] = self
::$web_root;
146 // Static assets directory, relative to the webserver root.
147 $GLOBALS['assets_static_relative'] = self
::$web_root . "/public/assets";
148 // Relative themes directory, relative to the webserver root.
149 $GLOBALS['themes_static_relative'] = self
::$web_root . "/public/themes";
150 // Relative images directory, relative to the webserver root.
151 $GLOBALS['images_static_relative'] = self
::$web_root . "/public/images";
152 // Static images directory, absolute to the webserver root.
153 $GLOBALS['images_static_absolute'] = self
::$webserver_root . "/public/images";
154 //Composer vendor directory, absolute to the webserver root.
155 $GLOBALS['vendor_dir'] = self
::$webserver_root . "/vendor";
158 private static function setSiteFromEndpoint(): void
160 // Get site from endpoint if available. Unsure about this though!
161 // Will fail during sql init otherwise.
162 $endPointParts = self
::parseEndPoint(self
::getRequestEndPoint());
163 if (count($endPointParts) > 1) {
164 $site_id = $endPointParts[0] ??
'';
166 self
::$SITE = $site_id;
171 public static function parseEndPoint($resource): array
173 if ($resource[0] === '/') {
174 $resource = substr($resource, 1);
176 return explode('/', $resource);
179 public static function getRequestEndPoint(): string
182 if (!empty($_REQUEST['_REWRITE_COMMAND'])) {
183 $resource = "/" . $_REQUEST['_REWRITE_COMMAND'];
184 } elseif (!empty($_SERVER['REDIRECT_QUERY_STRING'])) {
185 $resource = str_replace('_REWRITE_COMMAND=', '/', $_SERVER['REDIRECT_QUERY_STRING']);
187 if (!empty($_SERVER['REQUEST_URI'])) {
188 if (strpos($_SERVER['REQUEST_URI'], '?') > 0) {
189 $resource = strstr($_SERVER['REQUEST_URI'], '?', true);
191 $resource = str_replace(self
::$ROOT_URL ??
'', '', $_SERVER['REQUEST_URI']);
199 public static function verifyAccessToken()
201 $logger = new SystemLogger();
202 $response = self
::createServerResponse();
203 $request = self
::createServerRequest();
204 $server = new ResourceServer(
205 new AccessTokenRepository(),
209 $raw = $server->validateAuthenticatedRequest($request);
210 } catch (OAuthServerException
$exception) {
211 $logger->error("RestConfig->verifyAccessToken() OAuthServerException", ["message" => $exception->getMessage()]);
212 return $exception->generateHttpResponse($response);
213 } catch (\Exception
$exception) {
214 $logger->error("RestConfig->verifyAccessToken() Exception", ["message" => $exception->getMessage()]);
215 return (new OAuthServerException($exception->getMessage(), 0, 'unknown_error', 500))
216 ->generateHttpResponse($response);
223 * Returns true if the access token for the given token id is valid. Otherwise returns the access denied response.
225 * @return bool|ResponseInterface
227 public static function validateAccessTokenRevoked($tokenId)
229 $repository = new AccessTokenRepository();
230 if ($repository->isAccessTokenRevokedInDatabase($tokenId)) {
231 $response = self
::createServerResponse();
232 return OAuthServerException
::accessDenied('Access token has been revoked')->generateHttpResponse($response);
237 public static function isTrustedUser($clientId, $userId)
239 $trustedUserService = new TrustedUserService();
240 $response = self
::createServerResponse();
242 if (!$trustedUserService->isTrustedUser($clientId, $userId)) {
243 (new SystemLogger())->debug(
244 "invalid Trusted User. Refresh Token revoked or logged out",
245 ['clientId' => $clientId, 'userId' => $userId]
247 throw new OAuthServerException('Refresh Token revoked or logged out', 0, 'invalid _request', 400);
249 return $trustedUserService->getTrustedUser($clientId, $userId);
250 } catch (OAuthServerException
$exception) {
251 return $exception->generateHttpResponse($response);
252 } catch (\Exception
$exception) {
253 return (new OAuthServerException($exception->getMessage(), 0, 'unknown_error', 500))
254 ->generateHttpResponse($response);
258 public static function createServerResponse(): ResponseInterface
260 $psr17Factory = new Psr17Factory();
262 return $psr17Factory->createResponse();
265 public static function createServerRequest(): ServerRequestInterface
267 $psr17Factory = new Psr17Factory();
268 $creator = new ServerRequestCreator(
269 $psr17Factory, // ServerRequestFactory
270 $psr17Factory, // UriFactory
271 $psr17Factory, // UploadedFileFactory
272 $psr17Factory // StreamFactory
275 return $creator->fromGlobals();
278 public static function destroySession(): void
280 SessionUtil
::apiSessionCookieDestroy();
283 public static function getPostData($data)
289 if ($post_data = file_get_contents('php://input')) {
290 if ($post_json = json_decode($post_data, true)) {
293 parse_str($post_data, $post_variables);
294 if (count($post_variables)) {
295 return $post_variables;
302 public static function authorization_check($section, $value, $user = ''): void
304 $result = AclMain
::aclCheckCore($section, $value, $user);
306 if (!self
::$notRestCall) {
307 http_response_code(401);
313 // Main function to check scope
315 // Only sending $scopeType would be for something like 'openid'
316 // For using all 3 parameters would be for something like 'user/Organization.write'
317 // $scopeType = 'user', $resource = 'Organization', $permission = 'write'
318 public static function scope_check($scopeType, $resource = null, $permission = null): void
320 if (!empty($GLOBALS['oauth_scopes'])) {
321 // Need to ensure has scope
322 if (empty($resource)) {
323 // Simply check to see if $scopeType is an allowed scope
326 // Resource scope check
327 $scope = $scopeType . '/' . $resource . '.' . $permission;
329 if (!in_array($scope, $GLOBALS['oauth_scopes'])) {
330 (new SystemLogger())->debug("RestConfig::scope_check scope not in access token", ['scope' => $scope, 'scopes_granted' => $GLOBALS['oauth_scopes']]);
331 http_response_code(401);
335 (new SystemLogger())->error("RestConfig::scope_check global scope array is empty");
336 http_response_code(401);
341 public static function setLocalCall(): void
343 self
::$localCall = true;
346 public static function setNotRestCall(): void
348 self
::$notRestCall = true;
351 public static function is_fhir_request($resource): bool
353 return stripos(strtolower($resource), "/fhir/") !== false;
356 public static function is_portal_request($resource): bool
358 return stripos(strtolower($resource), "/portal/") !== false;
361 public static function is_api_request($resource): bool
363 return stripos(strtolower($resource), "/api/") !== false;
366 public static function skipApiAuth($resource): bool
368 if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
369 // we don't authenticate OPTIONS requests
373 // ensure 1) sane site and 2) ensure the site exists on filesystem before even considering for skip api auth
374 if (empty(self
::$SITE) ||
preg_match('/[^A-Za-z0-9\\-.]/', self
::$SITE) ||
!file_exists(__DIR__
. '/sites/' . self
::$SITE)) {
375 error_log("OpenEMR Error - api site error, so forced exit");
376 http_response_code(400);
379 // let the capability statement for FHIR or the SMART-on-FHIR through
380 $resource = str_replace('/' . self
::$SITE, '', $resource);
382 // TODO: @adunsulag we need to centralize our auth skipping logic... as we have this duplicated in HttpRestRouteHandler
383 // however, at the point of this method we don't have the resource identified and haven't gone through our parsing
384 // routine to handle that logic...
385 $resource === ("/fhir/metadata") ||
386 $resource === ("/fhir/.well-known/smart-configuration") ||
387 // skip list and single instance routes
388 0 === strpos("/fhir/OperationDefinition", $resource)
396 public static function apiLog($response = '', $requestBody = ''): void
398 $logResponse = $response;
400 // only log when using standard api calls (skip when using local api calls from within OpenEMR)
401 // and when api log option is set
402 if (!$GLOBALS['is_local_api'] && !self
::$notRestCall && $GLOBALS['api_log_option']) {
403 if ($GLOBALS['api_log_option'] == 1) {
404 // Do not log the response and requestBody
408 if ($response instanceof ResponseInterface
) {
409 if (self
::shouldLogResponse($response)) {
410 $body = $response->getBody();
411 $logResponse = $body->getContents();
414 $logResponse = 'Content not application/json - Skip binary data';
417 $logResponse = (!empty($logResponse)) ?
json_encode($response) : '';
420 // convert pertinent elements to json
421 $requestBody = (!empty($requestBody)) ?
json_encode($requestBody) : '';
423 // prepare values and call the log function
426 $method = $_SERVER['REQUEST_METHOD'];
427 $url = $_SERVER['REQUEST_URI'];
428 $patientId = (int)($_SESSION['pid'] ??
0);
429 $userId = (int)($_SESSION['authUserID'] ??
0);
431 'user_id' => $userId,
432 'patient_id' => $patientId,
434 'request' => $GLOBALS['resource'],
435 'request_url' => $url,
436 'request_body' => $requestBody,
437 'response' => $logResponse
439 if ($patientId === 0) {
440 $patientId = null; //entries in log table are blank for no patient_id, whereas in api_log are 0, which is why above $api value uses 0 when empty
442 EventAuditLogger
::instance()->recordLogItem(1, $event, ($_SESSION['authUser'] ??
''), ($_SESSION['authProvider'] ??
''), 'api log', $patientId, $category, 'open-emr', null, null, '', $api);
446 public static function emitResponse($response, $build = false): void
448 if (headers_sent()) {
449 throw new RuntimeException('Headers already sent.');
451 $statusLine = sprintf(
453 $response->getProtocolVersion(),
454 $response->getStatusCode(),
455 $response->getReasonPhrase()
457 header($statusLine, true);
458 foreach ($response->getHeaders() as $name => $values) {
459 $responseHeader = sprintf('%s: %s', $name, $response->getHeaderLine($name));
460 header($responseHeader, false);
462 echo $response->getBody();
466 * If the FHIR System scopes enabled or not. True if its turned on, false otherwise.
469 public static function areSystemScopesEnabled()
471 return $GLOBALS['rest_system_scopes_api'] === '1';
474 public function authenticateUserToken($tokenId, $clientId, $userId): bool
476 $ip = collectIpAddresses();
479 $accessTokenRepo = new AccessTokenRepository();
480 $authTokenExpiration = $accessTokenRepo->getTokenExpiration($tokenId, $clientId, $userId);
482 if (empty($authTokenExpiration)) {
483 EventAuditLogger
::instance()->newEvent('api', '', '', 0, "API failure: " . $ip['ip_string'] . ". Token not found for client[" . $clientId . "] and user " . $userId . ".");
487 // Ensure token not expired (note an expired token should have already been caught by oauth2, however will also check here)
488 $currentDateTime = date("Y-m-d H:i:s");
489 $expiryDateTime = date("Y-m-d H:i:s", strtotime($authTokenExpiration));
490 if ($expiryDateTime <= $currentDateTime) {
491 EventAuditLogger
::instance()->newEvent('api', '', '', 0, "API failure: " . $ip['ip_string'] . ". Token expired for client[" . $clientId . "] and user " . $userId . ".");
495 // Token authentication passed
496 EventAuditLogger
::instance()->newEvent('api', '', '', 1, "API success: " . $ip['ip_string'] . ". Token successfully used for client[" . $clientId . "] and user " . $userId . ".");
501 * Checks if we should log the response interface (we don't want to log binary documents or anything like that)
502 * We only log requests with a content-type of any form of json fhir+application/json or application/json
503 * @param ResponseInterface $response
504 * @return bool If the request should be logged, false otherwise
506 private static function shouldLogResponse(ResponseInterface
$response)
508 if ($response->hasHeader("Content-Type")) {
509 $contentType = $response->getHeaderLine("Content-Type");
510 if ($contentType === 'application/json') {
519 * Grabs all of the context information for the request's access token and populates any context variables the
520 * request needs (such as patient binding information). Returns the populated request
521 * @param HttpRestRequest $restRequest
522 * @return HttpRestRequest
524 public function populateTokenContextForRequest(HttpRestRequest
$restRequest)
527 $context = $this->getTokenContextForRequest($restRequest);
528 // note that the context here is the SMART value that is returned in the response for an AccessToken in this
529 // case it is the patient value which is the logical id (ie uuid) of the patient.
530 $patientUuid = $context['patient'] ??
null;
531 if (!empty($patientUuid)) {
532 // we only set the bound patient access if the underlying user can still access the patient
533 if ($this->checkUserHasAccessToPatient($restRequest->getRequestUserId(), $patientUuid)) {
534 $restRequest->setPatientUuidString($patientUuid);
536 (new SystemLogger())->error("OpenEMR Error: api had patient launch scope but user did not have access to patient uuid."
537 . " Resources restricted with patient scopes will not return results");
540 (new SystemLogger())->error("OpenEMR Error: api had patient launch scope but no patient was set in the "
541 . " session cache. Resources restricted with patient scopes will not return results");
546 public function getTokenContextForRequest(HttpRestRequest
$restRequest)
548 $accessTokenRepo = new AccessTokenRepository();
549 // note this is pretty confusing as getAccessTokenId comes from the oauth_access_id which is the token NOT
550 // the database id even though this is called accessTokenId....
551 $token = $accessTokenRepo->getTokenByToken($restRequest->getAccessTokenId());
552 $context = $token['context'] ??
"{}"; // if there is no populated context we just return an empty return
554 return json_decode($context, true);
555 } catch (\Exception
$exception) {
556 (new SystemLogger())->error("OpenEMR Error: failed to decode token context json", ['exception' => $exception->getMessage()
557 , 'tokenId' => $restRequest->getAccessTokenId()]);
564 * Checks whether a user has access to the patient. Returns true if the user can access the given patient, false otherwise
565 * @param $userId The id from the users table that represents the user
566 * @param $patientUuid The uuid from the patient_data table that represents the patient
567 * @return bool True if has access, false otherwise
569 private function checkUserHasAccessToPatient($userId, $patientUuid)
571 // TODO: the session should never be populated with the pid from the access token unless the user had access to
572 // it. However, if we wanted an additional check or if we wanted to fire off any kind of event that does
573 // patient filtering by provider / clinic we would handle that here.
578 /** prevents external cloning */
579 private function __clone()
584 // Include our routes and init routes global
586 require_once(__DIR__
. "/_rest_routes.inc.php");