e2e panther update that should now work with php8 (#4134)
[openemr.git] / _rest_config.php
blobb5f9365cc78d630ba96ed58ad8ee0383f2917c11
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';
16 require_once(__DIR__ . "/src/Common/Session/SessionUtil.php");
18 use Laminas\HttpHandlerRunner\Emitter\SapiEmitter;
19 use League\OAuth2\Server\Exception\OAuthServerException;
20 use League\OAuth2\Server\ResourceServer;
21 use Nyholm\Psr7\Factory\Psr17Factory;
22 use Nyholm\Psr7Server\ServerRequestCreator;
23 use OpenEMR\Common\Acl\AclMain;
24 use OpenEMR\Common\Auth\OpenIDConnect\Repositories\AccessTokenRepository;
25 use OpenEMR\Common\Logging\EventAuditLogger;
26 use OpenEMR\Common\Logging\SystemLogger;
27 use OpenEMR\Common\Uuid\UuidRegistry;
28 use Psr\Http\Message\ResponseInterface;
29 use Psr\Http\Message\ServerRequestInterface;
31 // also a handy place to add utility methods
32 // TODO before v6 release: refactor http_response_code(); for psr responses.
34 class RestConfig
36 /** @var routemap is an array of patterns and routes */
37 public static $ROUTE_MAP;
39 /** @var fhir routemap is an of patterns and routes */
40 public static $FHIR_ROUTE_MAP;
42 /** @var portal routemap is an of patterns and routes */
43 public static $PORTAL_ROUTE_MAP;
45 /** @var portal fhir routemap is an of patterns and routes */
46 public static $PORTAL_FHIR_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;
55 public static $SITE;
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()
73 /**
74 * Returns an instance of the RestConfig singleton
76 * @return RestConfig
78 public static function GetInstance(): \RestConfig
80 if (!self::$IS_INITIALIZED) {
81 self::Init();
84 if (!self::$INSTANCE instanceof self) {
85 self::$INSTANCE = new self();
88 return self::$INSTANCE;
91 /**
92 * Initialize the RestConfig object
94 public static function Init(): void
96 if (self::$IS_INITIALIZED) {
97 return;
99 // The busy stuff.
100 self::setPaths();
101 self::setSiteFromEndpoint();
102 self::$ROOT_URL = self::$web_root . "/apis";
103 self::$VENDOR_DIR = self::$webserver_root . "/vendor";
104 self::$publicKey = self::$webserver_root . "/sites/" . self::$SITE . "/documents/certificates/oapublic.key";
105 self::$IS_INITIALIZED = true;
109 * Basic paths when GLOBALS are not yet available.
111 * @return void
113 private static function SetPaths(): void
115 $isWindows = (stripos(PHP_OS_FAMILY, 'WIN') === 0);
116 // careful if moving this class to modify where's root.
117 self::$webserver_root = __DIR__;
118 if ($isWindows) {
119 //convert windows path separators
120 self::$webserver_root = str_replace("\\", "/", self::$webserver_root);
122 // Collect the apache server document root (and convert to windows slashes, if needed)
123 self::$server_document_root = realpath($_SERVER['DOCUMENT_ROOT']);
124 if ($isWindows) {
125 //convert windows path separators
126 self::$server_document_root = str_replace("\\", "/", self::$server_document_root);
128 self::$web_root = substr(self::$webserver_root, strspn(self::$webserver_root ^ self::$server_document_root, "\0"));
129 // Ensure web_root starts with a path separator
130 if (preg_match("/^[^\/]/", self::$web_root)) {
131 self::$web_root = "/" . self::$web_root;
133 // Will need these occasionally. sql init comes to mind!
134 $GLOBALS['rootdir'] = self::$web_root . "/interface";
135 // Absolute path to the source code include and headers file directory (Full path):
136 $GLOBALS['srcdir'] = self::$webserver_root . "/library";
137 // Absolute path to the location of documentroot directory for use with include statements:
138 $GLOBALS['fileroot'] = self::$webserver_root;
139 // Absolute path to the location of interface directory for use with include statements:
140 $GLOBALS['incdir'] = self::$webserver_root . "/interface";
141 // Absolute path to the location of documentroot directory for use with include statements:
142 $GLOBALS['webroot'] = self::$web_root;
143 // Static assets directory, relative to the webserver root.
144 $GLOBALS['assets_static_relative'] = self::$web_root . "/public/assets";
145 // Relative themes directory, relative to the webserver root.
146 $GLOBALS['themes_static_relative'] = self::$web_root . "/public/themes";
147 // Relative images directory, relative to the webserver root.
148 $GLOBALS['images_static_relative'] = self::$web_root . "/public/images";
149 // Static images directory, absolute to the webserver root.
150 $GLOBALS['images_static_absolute'] = self::$webserver_root . "/public/images";
151 //Composer vendor directory, absolute to the webserver root.
152 $GLOBALS['vendor_dir'] = self::$webserver_root . "/vendor";
155 private static function setSiteFromEndpoint(): void
157 // Get site from endpoint if available. Unsure about this though!
158 // Will fail during sql init otherwise.
159 $endPointParts = self::parseEndPoint(self::getRequestEndPoint());
160 if (count($endPointParts) > 1) {
161 $site_id = $endPointParts[0] ?? '';
162 if ($site_id) {
163 self::$SITE = $site_id;
168 public static function parseEndPoint($resource): array
170 if ($resource[0] === '/') {
171 $resource = substr($resource, 1);
173 return explode('/', $resource);
176 public static function getRequestEndPoint(): string
178 $resource = null;
179 if (!empty($_REQUEST['_REWRITE_COMMAND'])) {
180 $resource = "/" . $_REQUEST['_REWRITE_COMMAND'];
181 } elseif (!empty($_SERVER['REDIRECT_QUERY_STRING'])) {
182 $resource = str_replace('_REWRITE_COMMAND=', '/', $_SERVER['REDIRECT_QUERY_STRING']);
183 } else {
184 if (!empty($_SERVER['REQUEST_URI'])) {
185 if (strpos($_SERVER['REQUEST_URI'], '?') > 0) {
186 $resource = strstr($_SERVER['REQUEST_URI'], '?', true);
187 } else {
188 $resource = str_replace(self::$ROOT_URL, '', $_SERVER['REQUEST_URI']);
193 return $resource;
196 public static function verifyAccessToken()
198 $logger = SystemLogger::instance();
199 $response = self::createServerResponse();
200 $request = self::createServerRequest();
201 $server = new ResourceServer(
202 new AccessTokenRepository(),
203 self::$publicKey
205 try {
206 $raw = $server->validateAuthenticatedRequest($request);
207 } catch (OAuthServerException $exception) {
208 $logger->error("RestConfig->verifyAccessToken() OAuthServerException", ["message" => $exception->getMessage()]);
209 return $exception->generateHttpResponse($response);
210 } catch (\Exception $exception) {
211 $logger->error("RestConfig->verifyAccessToken() Exception", ["message" => $exception->getMessage()]);
212 return (new OAuthServerException($exception->getMessage(), 0, 'unknown_error', 500))
213 ->generateHttpResponse($response);
216 return $raw;
219 public static function isTrustedUser($clientId, $userId)
221 $response = self::createServerResponse();
222 try {
223 $trusted = sqlQueryNoLog("SELECT * FROM `oauth_trusted_user` WHERE `client_id`= ? AND `user_id`= ?", array($clientId, $userId));
224 if (empty($trusted['session_cache'])) {
225 throw new OAuthServerException('Refresh Token revoked or logged out', 0, 'invalid _request', 400);
227 } catch (OAuthServerException $exception) {
228 return $exception->generateHttpResponse($response);
229 } catch (\Exception $exception) {
230 return (new OAuthServerException($exception->getMessage(), 0, 'unknown_error', 500))
231 ->generateHttpResponse($response);
234 return $trusted;
237 public static function createServerResponse(): ResponseInterface
239 $psr17Factory = new Psr17Factory();
241 return $psr17Factory->createResponse();
244 public static function createServerRequest(): ServerRequestInterface
246 $psr17Factory = new Psr17Factory();
247 $creator = new ServerRequestCreator(
248 $psr17Factory, // ServerRequestFactory
249 $psr17Factory, // UriFactory
250 $psr17Factory, // UploadedFileFactory
251 $psr17Factory // StreamFactory
254 return $creator->fromGlobals();
257 public static function destroySession(): void
259 OpenEMR\Common\Session\SessionUtil::apiSessionCookieDestroy();
262 public static function getPostData($data)
264 if (count($_POST)) {
265 return $_POST;
268 if ($post_data = file_get_contents('php://input')) {
269 if ($post_json = json_decode($post_data, true)) {
270 return $post_json;
272 parse_str($post_data, $post_variables);
273 if (count($post_variables)) {
274 return $post_variables;
278 return null;
281 public static function authorization_check($section, $value): void
283 $result = AclMain::aclCheckCore($section, $value);
284 if (!$result) {
285 if (!self::$notRestCall) {
286 http_response_code(401);
288 exit();
292 // Main function to check scope
293 // Use cases:
294 // Only sending $scopeType would be for something like 'openid'
295 // For using all 3 parameters would be for something like 'user/Organization.write'
296 // $scopeType = 'user', $resource = 'Organization', $permission = 'write'
297 public static function scope_check($scopeType, $resource = null, $permission = null): void
299 if (!empty($GLOBALS['oauth_scopes'])) {
300 // Need to ensure has scope
301 if (empty($resource)) {
302 // Simply check to see if $scopeType is an allowed scope
303 $scope = $scopeType;
304 } else {
305 // Resource scope check
306 $scope = $scopeType . '/' . $resource . '.' . $permission;
308 if (!in_array($scope, $GLOBALS['oauth_scopes'])) {
309 http_response_code(401);
310 exit;
315 public static function setLocalCall(): void
317 self::$localCall = true;
320 public static function setNotRestCall(): void
322 self::$notRestCall = true;
325 public static function is_fhir_request($resource): bool
327 return stripos(strtolower($resource), "/fhir/") !== false;
330 public static function is_portal_request($resource): bool
332 return stripos(strtolower($resource), "/portal/") !== false;
335 public static function is_portal_fhir_request($resource): bool
337 return stripos(strtolower($resource), "/portalfhir/") !== false;
340 public static function is_api_request($resource): bool
342 return stripos(strtolower($resource), "/api/") !== false;
345 public static function skipApiAuth($resource): bool
347 if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
348 // we don't authenticate OPTIONS requests
349 return true;
352 // ensure 1) sane site and 2) ensure the site exists on filesystem before even considering for skip api auth
353 if (empty(self::$SITE) || preg_match('/[^A-Za-z0-9\\-.]/', self::$SITE) || !file_exists(__DIR__ . '/sites/' . self::$SITE)) {
354 error_log("OpenEMR Error - api site error, so forced exit");
355 http_response_code(400);
356 exit();
358 // let the capability statement for FHIR or the SMART-on-FHIR through
359 if (
360 $resource === ("/" . self::$SITE . "/fhir/metadata") ||
361 $resource === ("/" . self::$SITE . "/fhir/.well-known/smart-configuration")
363 return true;
364 } else {
365 return false;
369 public static function apiLog($response = '', $requestBody = ''): void
371 // only log when using standard api calls (skip when using local api calls from within OpenEMR)
372 // and when api log option is set
373 if (!$GLOBALS['is_local_api'] && !self::$notRestCall && $GLOBALS['api_log_option']) {
374 if ($GLOBALS['api_log_option'] == 1) {
375 // Do not log the response and requestBody
376 $response = '';
377 $requestBody = '';
380 // convert pertinent elements to json
381 $requestBody = (!empty($requestBody)) ? json_encode($requestBody) : '';
382 $response = (!empty($response)) ? json_encode($response) : '';
384 // prepare values and call the log function
385 $event = 'api';
386 $category = 'api';
387 $method = $_SERVER['REQUEST_METHOD'];
388 $url = $_SERVER['REQUEST_URI'];
389 $patientId = (int)($_SESSION['pid'] ?? 0);
390 $userId = (int)($_SESSION['authUserID'] ?? 0);
391 $api = [
392 'user_id' => $userId,
393 'patient_id' => $patientId,
394 'method' => $method,
395 'request' => $GLOBALS['resource'],
396 'request_url' => $url,
397 'request_body' => $requestBody,
398 'response' => $response
400 if ($patientId === 0) {
401 $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
403 EventAuditLogger::instance()->recordLogItem(1, $event, ($_SESSION['authUser'] ?? ''), ($_SESSION['authProvider'] ?? ''), 'api log', $patientId, $category, 'open-emr', null, null, '', $api);
407 public static function emitResponse($response, $build = false): void
409 if (headers_sent()) {
410 throw new RuntimeException('Headers already sent.');
412 $statusLine = sprintf(
413 'HTTP/%s %s %s',
414 $response->getProtocolVersion(),
415 $response->getStatusCode(),
416 $response->getReasonPhrase()
418 header($statusLine, true);
419 foreach ($response->getHeaders() as $name => $values) {
420 $responseHeader = sprintf('%s: %s', $name, $response->getHeaderLine($name));
421 header($responseHeader, false);
423 echo $response->getBody();
426 public function authenticateUserToken($tokenId, $userId): bool
428 $ip = collectIpAddresses();
430 // check for token
431 $authToken = sqlQueryNoLog("SELECT `expiry` FROM `api_token` WHERE `token` = ? AND `user_id` = ?", [$tokenId, $userId]);
432 if (empty($authToken) || empty($authToken['expiry'])) {
433 EventAuditLogger::instance()->newEvent('api', '', '', 0, "API failure: " . $ip['ip_string'] . ". Token not found for " . $userId . ".");
434 return false;
437 // Ensure token not expired (note an expired token should have already been caught by oauth2, however will also check here)
438 $currentDateTime = date("Y-m-d H:i:s");
439 $expiryDateTime = date("Y-m-d H:i:s", strtotime($authToken['expiry']));
440 if ($expiryDateTime <= $currentDateTime) {
441 EventAuditLogger::instance()->newEvent('api', '', '', 0, "API failure: " . $ip['ip_string'] . ". Token expired for " . $userId . ".");
442 return false;
445 // Token authentication passed
446 EventAuditLogger::instance()->newEvent('api', '', '', 1, "API success: " . $ip['ip_string'] . ". Token successfully used for " . $userId . ".");
447 return true;
450 /** prevents external cloning */
451 private function __clone()
456 // Include our routes and init routes global
458 require_once(__DIR__ . "/_rest_routes.inc.php");