Move ins type codes to database (#4488)
[openemr.git] / _rest_config.php
blob5f48f3be547f98ad43ab06e1e2c4de1cc730bbd4
1 <?php
3 /**
4 * Useful globals class for Rest
6 * @package OpenEMR
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\Logging\EventAuditLogger;
25 use OpenEMR\Common\Logging\SystemLogger;
26 use OpenEMR\Common\Session\SessionUtil;
27 use OpenEMR\Services\TrustedUserService;
28 use Psr\Http\Message\ResponseInterface;
29 use Psr\Http\Message\ServerRequestInterface;
32 // also a handy place to add utility methods
33 // TODO before v6 release: refactor http_response_code(); for psr responses.
35 class RestConfig
37 /** @var routemap is an array of patterns and routes */
38 public static $ROUTE_MAP;
40 /** @var fhir routemap is an of patterns and routes */
41 public static $FHIR_ROUTE_MAP;
43 /** @var portal routemap is an of patterns and routes */
44 public static $PORTAL_ROUTE_MAP;
46 /** @var app root is the root directory of the application */
47 public static $APP_ROOT;
49 /** @var root url of the application */
50 public static $ROOT_URL;
51 // you can guess what the rest are!
52 public static $VENDOR_DIR;
53 public static $SITE;
54 public static $apisBaseFullUrl;
55 public static $webserver_root;
56 public static $web_root;
57 public static $server_document_root;
58 public static $publicKey;
59 private static $INSTANCE;
60 private static $IS_INITIALIZED = false;
61 /** @var set to true if local api call */
62 private static $localCall = false;
63 /** @var set to true if not rest call */
64 private static $notRestCall = false;
66 /** prevents external construction */
67 private function __construct()
71 /**
72 * Returns an instance of the RestConfig singleton
74 * @return RestConfig
76 public static function GetInstance(): \RestConfig
78 if (!self::$IS_INITIALIZED) {
79 self::Init();
82 if (!self::$INSTANCE instanceof self) {
83 self::$INSTANCE = new self();
86 return self::$INSTANCE;
89 /**
90 * Initialize the RestConfig object
92 public static function Init(): void
94 if (self::$IS_INITIALIZED) {
95 return;
97 // The busy stuff.
98 self::setPaths();
99 self::setSiteFromEndpoint();
100 self::$ROOT_URL = self::$web_root . "/apis";
101 self::$VENDOR_DIR = self::$webserver_root . "/vendor";
102 self::$publicKey = self::$webserver_root . "/sites/" . self::$SITE . "/documents/certificates/oapublic.key";
103 self::$IS_INITIALIZED = true;
107 * Basic paths when GLOBALS are not yet available.
109 * @return void
111 private static function SetPaths(): void
113 $isWindows = (stripos(PHP_OS_FAMILY, 'WIN') === 0);
114 // careful if moving this class to modify where's root.
115 self::$webserver_root = __DIR__;
116 if ($isWindows) {
117 //convert windows path separators
118 self::$webserver_root = str_replace("\\", "/", self::$webserver_root);
120 // Collect the apache server document root (and convert to windows slashes, if needed)
121 self::$server_document_root = realpath($_SERVER['DOCUMENT_ROOT']);
122 if ($isWindows) {
123 //convert windows path separators
124 self::$server_document_root = str_replace("\\", "/", self::$server_document_root);
126 self::$web_root = substr(self::$webserver_root, strspn(self::$webserver_root ^ self::$server_document_root, "\0"));
127 // Ensure web_root starts with a path separator
128 if (preg_match("/^[^\/]/", self::$web_root)) {
129 self::$web_root = "/" . self::$web_root;
131 // Will need these occasionally. sql init comes to mind!
132 $GLOBALS['rootdir'] = self::$web_root . "/interface";
133 // Absolute path to the source code include and headers file directory (Full path):
134 $GLOBALS['srcdir'] = self::$webserver_root . "/library";
135 // Absolute path to the location of documentroot directory for use with include statements:
136 $GLOBALS['fileroot'] = self::$webserver_root;
137 // Absolute path to the location of interface directory for use with include statements:
138 $GLOBALS['incdir'] = self::$webserver_root . "/interface";
139 // Absolute path to the location of documentroot directory for use with include statements:
140 $GLOBALS['webroot'] = self::$web_root;
141 // Static assets directory, relative to the webserver root.
142 $GLOBALS['assets_static_relative'] = self::$web_root . "/public/assets";
143 // Relative themes directory, relative to the webserver root.
144 $GLOBALS['themes_static_relative'] = self::$web_root . "/public/themes";
145 // Relative images directory, relative to the webserver root.
146 $GLOBALS['images_static_relative'] = self::$web_root . "/public/images";
147 // Static images directory, absolute to the webserver root.
148 $GLOBALS['images_static_absolute'] = self::$webserver_root . "/public/images";
149 //Composer vendor directory, absolute to the webserver root.
150 $GLOBALS['vendor_dir'] = self::$webserver_root . "/vendor";
153 private static function setSiteFromEndpoint(): void
155 // Get site from endpoint if available. Unsure about this though!
156 // Will fail during sql init otherwise.
157 $endPointParts = self::parseEndPoint(self::getRequestEndPoint());
158 if (count($endPointParts) > 1) {
159 $site_id = $endPointParts[0] ?? '';
160 if ($site_id) {
161 self::$SITE = $site_id;
166 public static function parseEndPoint($resource): array
168 if ($resource[0] === '/') {
169 $resource = substr($resource, 1);
171 return explode('/', $resource);
174 public static function getRequestEndPoint(): string
176 $resource = null;
177 if (!empty($_REQUEST['_REWRITE_COMMAND'])) {
178 $resource = "/" . $_REQUEST['_REWRITE_COMMAND'];
179 } elseif (!empty($_SERVER['REDIRECT_QUERY_STRING'])) {
180 $resource = str_replace('_REWRITE_COMMAND=', '/', $_SERVER['REDIRECT_QUERY_STRING']);
181 } else {
182 if (!empty($_SERVER['REQUEST_URI'])) {
183 if (strpos($_SERVER['REQUEST_URI'], '?') > 0) {
184 $resource = strstr($_SERVER['REQUEST_URI'], '?', true);
185 } else {
186 $resource = str_replace(self::$ROOT_URL, '', $_SERVER['REQUEST_URI']);
191 return $resource;
194 public static function verifyAccessToken()
196 $logger = new SystemLogger();
197 $response = self::createServerResponse();
198 $request = self::createServerRequest();
199 $server = new ResourceServer(
200 new AccessTokenRepository(),
201 self::$publicKey
203 try {
204 $raw = $server->validateAuthenticatedRequest($request);
205 } catch (OAuthServerException $exception) {
206 $logger->error("RestConfig->verifyAccessToken() OAuthServerException", ["message" => $exception->getMessage()]);
207 return $exception->generateHttpResponse($response);
208 } catch (\Exception $exception) {
209 $logger->error("RestConfig->verifyAccessToken() Exception", ["message" => $exception->getMessage()]);
210 return (new OAuthServerException($exception->getMessage(), 0, 'unknown_error', 500))
211 ->generateHttpResponse($response);
214 return $raw;
217 public static function isTrustedUser($clientId, $userId)
219 $trustedUserService = new TrustedUserService();
220 $response = self::createServerResponse();
221 try {
222 if (!$trustedUserService->isTrustedUser($clientId, $userId)) {
223 (new SystemLogger())->debug(
224 "invalid Trusted User. Refresh Token revoked or logged out",
225 ['clientId' => $clientId, 'userId' => $userId]
227 throw new OAuthServerException('Refresh Token revoked or logged out', 0, 'invalid _request', 400);
229 return $trustedUserService->getTrustedUser($clientId, $userId);
230 } catch (OAuthServerException $exception) {
231 return $exception->generateHttpResponse($response);
232 } catch (\Exception $exception) {
233 return (new OAuthServerException($exception->getMessage(), 0, 'unknown_error', 500))
234 ->generateHttpResponse($response);
238 public static function createServerResponse(): ResponseInterface
240 $psr17Factory = new Psr17Factory();
242 return $psr17Factory->createResponse();
245 public static function createServerRequest(): ServerRequestInterface
247 $psr17Factory = new Psr17Factory();
248 $creator = new ServerRequestCreator(
249 $psr17Factory, // ServerRequestFactory
250 $psr17Factory, // UriFactory
251 $psr17Factory, // UploadedFileFactory
252 $psr17Factory // StreamFactory
255 return $creator->fromGlobals();
258 public static function destroySession(): void
260 SessionUtil::apiSessionCookieDestroy();
263 public static function getPostData($data)
265 if (count($_POST)) {
266 return $_POST;
269 if ($post_data = file_get_contents('php://input')) {
270 if ($post_json = json_decode($post_data, true)) {
271 return $post_json;
273 parse_str($post_data, $post_variables);
274 if (count($post_variables)) {
275 return $post_variables;
279 return null;
282 public static function authorization_check($section, $value, $user = ''): void
284 $result = AclMain::aclCheckCore($section, $value, $user);
285 if (!$result) {
286 if (!self::$notRestCall) {
287 http_response_code(401);
289 exit();
293 // Main function to check scope
294 // Use cases:
295 // Only sending $scopeType would be for something like 'openid'
296 // For using all 3 parameters would be for something like 'user/Organization.write'
297 // $scopeType = 'user', $resource = 'Organization', $permission = 'write'
298 public static function scope_check($scopeType, $resource = null, $permission = null): void
300 if (!empty($GLOBALS['oauth_scopes'])) {
301 // Need to ensure has scope
302 if (empty($resource)) {
303 // Simply check to see if $scopeType is an allowed scope
304 $scope = $scopeType;
305 } else {
306 // Resource scope check
307 $scope = $scopeType . '/' . $resource . '.' . $permission;
309 if (!in_array($scope, $GLOBALS['oauth_scopes'])) {
310 (new SystemLogger())->debug("RestConfig::scope_check scope not in access token", ['scope' => $scope]);
311 http_response_code(401);
312 exit;
314 } else {
315 (new SystemLogger())->error("RestConfig::scope_check global scope array is empty");
316 http_response_code(401);
317 exit;
321 public static function setLocalCall(): void
323 self::$localCall = true;
326 public static function setNotRestCall(): void
328 self::$notRestCall = true;
331 public static function is_fhir_request($resource): bool
333 return stripos(strtolower($resource), "/fhir/") !== false;
336 public static function is_portal_request($resource): bool
338 return stripos(strtolower($resource), "/portal/") !== false;
341 public static function is_api_request($resource): bool
343 return stripos(strtolower($resource), "/api/") !== false;
346 public static function skipApiAuth($resource): bool
348 if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
349 // we don't authenticate OPTIONS requests
350 return true;
353 // ensure 1) sane site and 2) ensure the site exists on filesystem before even considering for skip api auth
354 if (empty(self::$SITE) || preg_match('/[^A-Za-z0-9\\-.]/', self::$SITE) || !file_exists(__DIR__ . '/sites/' . self::$SITE)) {
355 error_log("OpenEMR Error - api site error, so forced exit");
356 http_response_code(400);
357 exit();
359 // let the capability statement for FHIR or the SMART-on-FHIR through
360 if (
361 $resource === ("/" . self::$SITE . "/fhir/metadata") ||
362 $resource === ("/" . self::$SITE . "/fhir/.well-known/smart-configuration")
364 return true;
365 } else {
366 return false;
370 public static function apiLog($response = '', $requestBody = ''): void
372 $logResponse = $response;
374 // only log when using standard api calls (skip when using local api calls from within OpenEMR)
375 // and when api log option is set
376 if (!$GLOBALS['is_local_api'] && !self::$notRestCall && $GLOBALS['api_log_option']) {
377 if ($GLOBALS['api_log_option'] == 1) {
378 // Do not log the response and requestBody
379 $logResponse = '';
380 $requestBody = '';
382 if ($response instanceof ResponseInterface) {
383 if (self::shouldLogResponse($response)) {
384 $body = $response->getBody();
385 $logResponse = $body->getContents();
386 $body->rewind();
387 } else {
388 $logResponse = 'Content not application/json - Skip binary data';
390 } else {
391 $logResponse = (!empty($logResponse)) ? json_encode($response) : '';
394 // convert pertinent elements to json
395 $requestBody = (!empty($requestBody)) ? json_encode($requestBody) : '';
397 // prepare values and call the log function
398 $event = 'api';
399 $category = 'api';
400 $method = $_SERVER['REQUEST_METHOD'];
401 $url = $_SERVER['REQUEST_URI'];
402 $patientId = (int)($_SESSION['pid'] ?? 0);
403 $userId = (int)($_SESSION['authUserID'] ?? 0);
404 $api = [
405 'user_id' => $userId,
406 'patient_id' => $patientId,
407 'method' => $method,
408 'request' => $GLOBALS['resource'],
409 'request_url' => $url,
410 'request_body' => $requestBody,
411 'response' => $logResponse
413 if ($patientId === 0) {
414 $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
416 EventAuditLogger::instance()->recordLogItem(1, $event, ($_SESSION['authUser'] ?? ''), ($_SESSION['authProvider'] ?? ''), 'api log', $patientId, $category, 'open-emr', null, null, '', $api);
420 public static function emitResponse($response, $build = false): void
422 if (headers_sent()) {
423 throw new RuntimeException('Headers already sent.');
425 $statusLine = sprintf(
426 'HTTP/%s %s %s',
427 $response->getProtocolVersion(),
428 $response->getStatusCode(),
429 $response->getReasonPhrase()
431 header($statusLine, true);
432 foreach ($response->getHeaders() as $name => $values) {
433 $responseHeader = sprintf('%s: %s', $name, $response->getHeaderLine($name));
434 header($responseHeader, false);
436 echo $response->getBody();
440 * If the FHIR System scopes enabled or not. True if its turned on, false otherwise.
441 * @return bool
443 public static function areSystemScopesEnabled()
445 return $GLOBALS['rest_system_scopes_api'] === '1';
448 public function authenticateUserToken($tokenId, $clientId, $userId): bool
450 $ip = collectIpAddresses();
452 // check for token
453 $accessTokenRepo = new AccessTokenRepository();
454 $authTokenExpiration = $accessTokenRepo->getTokenExpiration($tokenId, $clientId, $userId);
456 if (empty($authTokenExpiration)) {
457 EventAuditLogger::instance()->newEvent('api', '', '', 0, "API failure: " . $ip['ip_string'] . ". Token not found for client[" . $clientId . "] and user " . $userId . ".");
458 return false;
461 // Ensure token not expired (note an expired token should have already been caught by oauth2, however will also check here)
462 $currentDateTime = date("Y-m-d H:i:s");
463 $expiryDateTime = date("Y-m-d H:i:s", strtotime($authTokenExpiration));
464 if ($expiryDateTime <= $currentDateTime) {
465 EventAuditLogger::instance()->newEvent('api', '', '', 0, "API failure: " . $ip['ip_string'] . ". Token expired for client[" . $clientId . "] and user " . $userId . ".");
466 return false;
469 // Token authentication passed
470 EventAuditLogger::instance()->newEvent('api', '', '', 1, "API success: " . $ip['ip_string'] . ". Token successfully used for client[" . $clientId . "] and user " . $userId . ".");
471 return true;
475 * Checks if we should log the response interface (we don't want to log binary documents or anything like that)
476 * We only log requests with a content-type of any form of json fhir+application/json or application/json
477 * @param ResponseInterface $response
478 * @return bool If the request should be logged, false otherwise
480 private static function shouldLogResponse(ResponseInterface $response)
482 if ($response->hasHeader("Content-Type")) {
483 $contentType = $response->getHeaderLine("Content-Type");
484 if ($contentType === 'application/json') {
485 return true;
489 return false;
493 /** prevents external cloning */
494 private function __clone()
499 // Include our routes and init routes global
501 require_once(__DIR__ . "/_rest_routes.inc.php");