diff --git a/library/globals.inc.php b/library/globals.inc.php index 25c9baf31b6..02fdbbdb963 100644 --- a/library/globals.inc.php +++ b/library/globals.inc.php @@ -2888,6 +2888,20 @@ function gblTimeZones() xl('Patient is required to enter their contact e-mail if present in Demographics Contact.') ), + 'google_recaptcha_site_key' => array( + xl('Google reCAPTCHA V2 site key'), + 'text', + '', + xl('Google reCAPTCHA V2 site key') + ), + + 'google_recaptcha_secret_key' => array( + xl('Google reCAPTCHA V2 secret key'), + 'encrypted', + '', + xl('Google reCAPTCHA V2 secret key') + ), + 'portal_onsite_two_register' => array( xl('Allow New Patient Registration Widget'), 'bool', // data type diff --git a/library/js/xl/jquery-datetimepicker-2-5-4-alternate.js.php b/library/js/xl/jquery-datetimepicker-2-5-4-alternate.js.php index 08ab4979013..f6375b92687 100644 --- a/library/js/xl/jquery-datetimepicker-2-5-4-alternate.js.php +++ b/library/js/xl/jquery-datetimepicker-2-5-4-alternate.js.php @@ -55,7 +55,7 @@ var datepicker_xlMonths = [, , , , , , , , , , , ]; var datepicker_xlDayofwkshort= [, , , , , , ]; var datepicker_xlDayofwk= [, , , , , , ]; -var datepicker_rtl = ; +var datepicker_rtl = ; var datepicker_yearStart = '1900'; var datepicker_format = 'Y-m-d'; var datepicker_scrollInput = false; @@ -65,7 +65,7 @@ var datetimepicker_xlMonths = [, , , , , , , , , , , ]; var datetimepicker_xlDayofwkshort= [, , , , , , ]; var datetimepicker_xlDayofwk= [, , , , , , ]; -var datetimepicker_rtl = ; +var datetimepicker_rtl = ; var datetimepicker_yearStart = '1900'; var datetimepicker_format = 'Y-m-d H:i:s'; var datetimepicker_step = '30'; diff --git a/library/js/xl/jquery-datetimepicker-2-5-4.js.php b/library/js/xl/jquery-datetimepicker-2-5-4.js.php index 680d970e57d..713be9306f2 100644 --- a/library/js/xl/jquery-datetimepicker-2-5-4.js.php +++ b/library/js/xl/jquery-datetimepicker-2-5-4.js.php @@ -54,7 +54,7 @@ ] }, }, - + /** * In RTL languages a datepicker popup is opened in left and it's cutted by the edge of the window * This patch resolves that and moves a datepicker popup to right side. @@ -76,7 +76,7 @@ yearStart: '1900', scrollInput: false, scrollMonth: false, - rtl: , + rtl: , minDate: '', diff --git a/portal/account/account.lib.php b/portal/account/account.lib.php index 063627ca167..40cc68bdc31 100644 --- a/portal/account/account.lib.php +++ b/portal/account/account.lib.php @@ -14,11 +14,14 @@ /* Library functions for register*/ +use GuzzleHttp\Client; use OpenEMR\Common\Auth\AuthHash; use OpenEMR\Common\Crypto\CryptoGen; +use OpenEMR\Common\Logging\EventAuditLogger; +use OpenEMR\Common\Logging\SystemLogger; use OpenEMR\Common\Utils\RandomGenUtils; -function notifyAdmin($pid, $provider) +function notifyAdmin($pid, $provider): void { $note = xlt("New patient registration received from patient portal. Reminder to check for possible new appointment"); @@ -29,60 +32,273 @@ function notifyAdmin($pid, $provider) $user['username'] = "portal-user"; } - $rtn = addPnote($pid, $note, 1, 1, $title, $user['username'], '', 'New'); + addPnote($pid, $note, 1, 1, $title, $user['username'], '', 'New'); +} + +function processRecaptcha($gRecaptchaResponse): bool +{ + if (empty($gRecaptchaResponse)) { + (new SystemLogger())->error("processRecaptcha function: gRecaptchaResponse is empty, so unable to verify recaptcha"); + return false; + } + if (empty($GLOBALS['google_recaptcha_site_key'])) { + (new SystemLogger())->error("processRecaptcha function: google_recaptcha_site_key is empty, so unable to verify recaptcha"); + return false; + } + if (empty($GLOBALS['google_recaptcha_secret_key'])) { + (new SystemLogger())->error("processRecaptcha function: google_recaptcha_secret_key is empty, so unable to verify recaptcha"); + return false; + } + $googleRecaptchaSecretKey = (new CryptoGen())->decryptStandard($GLOBALS['google_recaptcha_secret_key']); + if (empty($googleRecaptchaSecretKey)) { + (new SystemLogger())->error("processRecaptcha function: decrypted google_recaptcha_secret_key global is empty, so unable to verify recaptcha"); + return false; + } - return $rtn; + $client = new Client([ + 'base_uri' => 'https://www.google.com/recaptcha/api/', + 'timeout' => 2.0 + ]); + $response = $client->request('POST', 'siteverify', [ + 'query' => [ + 'secret' => $googleRecaptchaSecretKey, + 'response' => $gRecaptchaResponse + ] + ]); + $responseArray = json_decode($response->getBody(), true); + (new SystemLogger())->debug("processRecaptcha function: recaptcha verification returned following", ['returnJson' => $responseArray]); + if (empty($responseArray)) { + (new SystemLogger())->debug("processRecaptcha function: recaptcha verification was unsuccessful since empty response from google"); + return false; + } + if (empty($responseArray['success'])) { + (new SystemLogger())->debug("processRecaptcha function: recaptcha verification was unsuccessful since empty success status from google"); + return false; + } + if ($responseArray['success'] === true) { + (new SystemLogger())->debug("processRecaptcha function: recaptcha verification was successful from host " . ($responseArray['hostname'] ?? '')); + return true; + } else { + (new SystemLogger())->debug("processRecaptcha function: recaptcha verification was not successful from host " . ($responseArray['hostname'] ?? ''), ['errorCodes' => ($responseArray['error-codes'] ?? '')]); + return false; + } } -function isNew($dob = '', $lname = '', $fname = '', $email = '') + +// note function only returns false when there is an error in something and does not flag if a email exists or not +// (this is done so a bad actor can not see if certain patients exist in the instance) +function verifyEmail(string $languageChoice, string $fname, string $mname, string $lname, string $dob, string $email): bool { - // no sense doing a weighted search because we want specific criteria - // for new patients. Mainly catch those trying to just get a new password. - // or change email. - $last = trim(urldecode($lname)); - $first = trim(urldecode($fname)); - $dob = trim(urldecode($dob)); - $semail = trim(urldecode($email)); - // first check email both contact and secure - if ($email) { - $sql = "select pid from patient_data " . - "Where (patient_data.email LIKE ? OR patient_data.email_direct LIKE ?) " . - "And patient_data.DOB LIKE ? order by date limit 0,1"; - $data = array( - $semail, - $semail, - $dob - ); - $tier1 = sqlQuery($sql, $data); - if (!empty($tier1['pid'])) { - // email with this dob already on file so, skedaddle ... - return (int)$tier1['pid']; - } + if (empty($languageChoice) || empty($fname) || empty($lname) || empty($dob) || empty($email)) { + // only optional setting is the mname + (new SystemLogger())->error("a required verifyEmail function parameter is empty"); + return false; + } + + if (!validEmail($email)) { + (new SystemLogger())->debug("verifyEmail function is using a email that failed validEmail test, so can not use"); + return true; } - // fully matched for our purposes - $sql = "select pid from patient_data Where patient_data.lname LIKE ? And patient_data.fname LIKE ? And patient_data.DOB LIKE ? And (patient_data.email LIKE ? OR patient_data.email_direct LIKE ?) order by date limit 0,1"; - $data = array( - $last, - $first, - $dob, - $semail, - $semail + + $emailPrepSend = false; + + // check to ensure email not used + $sql = sqlQuery( + "SELECT `pid` FROM `patient_data` WHERE `email` = ? OR `email_direct` = ?", + [ + $email, + $email + ] ); - $tier2 = sqlQuery($sql, $data); - if (!empty($tier2['pid'])) { - return (int)$tier2['pid']; - } - // name and dob match. Most likely trying to change email! - // too much of a coincidence... - $sql = "select pid from patient_data Where patient_data.lname LIKE ? And patient_data.fname LIKE ? And patient_data.DOB LIKE ? order by date limit 0,1"; - $data = array( - $last, - $first, - $dob + + if (!empty($sql['pid'])) { + (new SystemLogger())->debug("verifyEmail function: the email is already in use, so can not use"); + $message = '

' . xlt("We received a patient registration email verification request from this email address at") . ' ' . text($email) . '

'; + $message .= '

' . xlt("However, you can not use this email for patient registration since it is already being used.") . '

'; + $message .= '

' . xlt("Recommend contacting the clinic if need guidance.") . "

"; + $message .= '

' . xlt("Please ignore this email if you did not make this request.") . '

'; + $message .= "

" . xlt("Thank You.") . "

"; + $emailPrepSend = true; + } else { + (new SystemLogger())->debug("verifyEmail function: the email will be used to register the patient"); + + // create token (1 hour expiry) and ensure the token is unique + $unique = false; + for ($i = 1; $i <= 10; $i++) { + $expiry = new DateTime('NOW'); + $expiry->add(new DateInterval('PT01H')); + $token_raw = RandomGenUtils::createUniqueToken(32); + $token_encrypt = (new CryptoGen())->encryptStandard($token_raw); + if (empty($token_encrypt)) { + // Serious issue if this is case, so return that something bad happened. + (new SystemLogger())->error("OpenEMR Error : Portal email verification token encryption broken - exiting"); + return false; + } + $token_database = $token_raw . bin2hex($expiry->format('U')); + + $sqlVerify = sqlQueryNoLog("SELECT `id` FROM `verify_email` WHERE `token_onetime` LIKE BINARY ?", [$token_raw . '%']); + if (empty($sqlVerify['id'])) { + $unique = true; + break; + } else { + (new SystemLogger())->error("was unable to create a unique token in verifyEmail function, which is very odd, so will try again (will try up to 10 times)"); + } + } + if (!$unique) { + (new SystemLogger())->error("was unable to create a unique token in verifyEmail function, so failed"); + return false; + } + + // place/replace database entry + $sql = sqlQuery("SELECT `id` FROM `verify_email` WHERE `email` = ?", [$email]); + if (empty($sql['id'])) { + sqlStatementNoLog( + "INSERT INTO `verify_email` (`email`, `language`, `fname`, `mname`, `lname`, `dob`, `token_onetime`, `active`, `pid_holder`) VALUES (?, ?, ?, ?, ?, ?, ?, 1, null)", + [ + $email, + $languageChoice, + $fname, + ($mname ?? ''), + $lname, + $dob, + $token_database + ] + ); + } else { + sqlStatementNoLog( + "UPDATE `verify_email` SET `language` = ?, `fname` = ?, `mname` = ?, `lname` = ?, `dob` = ?, `token_onetime` = ?, `active` = 1, `pid_holder` = null WHERE `email` = ?", + [ + $languageChoice, + $fname, + ($mname ?? ''), + $lname, + $dob, + $token_database, + $email + ] + ); + } + + // create $encoded_link + $site_addr = $GLOBALS['portal_onsite_two_address']; + $site_id = $_SESSION['site_id']; + if (stripos($site_addr, $site_id) === false) { + $encoded_link = sprintf("%s?%s", attr($site_addr), http_build_query([ + 'forward_email_verify' => $token_encrypt, + 'site' => $_SESSION['site_id'] + ])); + } else { + $encoded_link = sprintf("%s&%s", attr($site_addr), http_build_query([ + 'forward_email_verify' => $token_encrypt + ])); + } + + // create message with messageCreateVerifyEmail (no pin code) + $message = messageCreateVerifyEmail($encoded_link); + $emailPrepSend = true; + } + + if ($emailPrepSend) { + // send email + $mail = new MyMailer(); + $email_sender = $GLOBALS['patient_reminder_sender_email']; + $mail->AddReplyTo($email_sender, $email_sender); + $mail->SetFrom($email_sender, $email_sender); + $mail->AddAddress($email, ($fname . ' ' . $lname)); + $mail->Subject = xlt('Verify your email for patient portal registration'); + $mail->MsgHTML("
" . $message . "
"); + $mail->IsHTML(true); + $mail->AltBody = $message; + + if ($mail->Send()) { + EventAuditLogger::instance()->newEvent('patient-reg-email-verify', '', '', 1, "The patient registration verification email was successfully sent to " . $email); + (new SystemLogger())->debug("The patient registration verification email was successfully sent to " . $email); + return true; + } else { + $email_status = $mail->ErrorInfo; + EventAuditLogger::instance()->newEvent('patient-reg-email-verify', '', '', 0, "The patient registration verification email was not successfully sent to " . $email . " because of following issue: " . $email_status); + (new SystemLogger())->error("The patient registration verification email was not successfully sent to " . $email . " because of following issue: " . $email_status); + return false; + } + } + + // should never get to below + return true; +} + +function messageCreateVerifyEmail($encoded_link = '') +{ + $message = '

' . xlt("We received a patient registration email verification request. The link to verify your email is below.") . '

'; + $message .= '

' . xlt("Please ignore this email if you did not make this request") . '

'; + $message .= '

' . xlt("Below link is only valid for one hour.") . ":

"; + $message .= sprintf('%s', attr($encoded_link), text($encoded_link)); + $message .= "

" . xlt("Thank You.") . "

"; + + return $message; +} + +// note function only returns 0 when there is an error in something and does not flag if a patient exists or not +// (this is done so a bad actor can not see if certain patients exist in the instance) +function resetPassword(string $dob, string $lname, string $fname, string $email): int +{ + if (empty($dob) || empty($lname) || empty($fname) || empty($email)) { + (new SystemLogger())->error("a resetPassword function parameter is empty"); + return 0; + } + + $sql = sqlStatement( + "SELECT `pid` FROM `patient_data` WHERE `dob` = ? AND `lname` = ? AND `fname` = ? AND (`email` = ? OR `email_direct` = ?)", + [ + $dob, + $lname, + $fname, + $email, + $email + ] ); - $tier3 = sqlQuery($sql, $data); - return (int)$tier3['pid'] ?: 0; + if (sqlNumRows($sql) > 1) { + EventAuditLogger::instance()->newEvent('patient-password-reset', '', '', 0, "Patient password reset failure: Multiple patients were found in patient_data for search of: " . $fname . " " . $lname . " " . $dob . " " . $email); + (new SystemLogger())->error("resetPassword function selected more than 1 patient from patient_data, so was unable to reset the password"); + return 1; + } + if (!sqlNumRows($sql)) { + EventAuditLogger::instance()->newEvent('patient-password-reset', '', '', 0, "Patient password reset failure: No patient was found in patient_data for search of: " . $fname . " " . $lname . " " . $dob . " " . $email); + (new SystemLogger())->debug("resetPassword function found no patient in patient_data, so was unable to reset the password"); + return 1; + } + $row = sqlFetchArray($sql); + if (empty($row['pid'])) { + EventAuditLogger::instance()->newEvent('patient-password-reset', '', '', 0, "Patient password reset failure: No patient was found in patient_data for search of: " . $fname . " " . $lname . " " . $dob . " " . $email); + (new SystemLogger())->debug("resetPassword function found no patient in patient_data, so was unable to reset the password"); + return 1; + } + $tempPid = $row['pid']; + + $sql = sqlStatement("SELECT `pid` FROM `patient_access_onsite` WHERE `pid`=?", [$tempPid]); + if (sqlNumRows($sql) > 1) { + EventAuditLogger::instance()->newEvent('patient-password-reset', '', '', 0, "Patient password reset failure: Multiple patients were found in patient_access_onsite for search of pid " . $tempPid); + (new SystemLogger())->error("resetPassword function selected more than 1 patient from patient_access_onsite, so was unable to reset the password"); + return 1; + } + if (!sqlNumRows($sql)) { + EventAuditLogger::instance()->newEvent('patient-password-reset', '', '', 0, "Patient password reset failure: No patient was found in patient_access_onsite for search of pid " . $tempPid); + (new SystemLogger())->debug("resetPassword function found no patient in patient_access_onsite, so was unable to reset the password"); + return 1; + } + $row = sqlFetchArray($sql); + if (empty($row['pid'])) { + EventAuditLogger::instance()->newEvent('patient-password-reset', '', '', 0, "Patient password reset failure: No patient was found in patient_access_onsite for search of pid " . $tempPid); + (new SystemLogger())->debug("resetPassword function found no patient in patient_access_onsite, so was unable to reset the password"); + return 1; + } + + $rtn = doCredentials($row['pid'], true, $email); + if ($rtn) { + return 1; + } else { + return 0; + } } function saveInsurance($pid) @@ -122,19 +338,6 @@ function saveInsurance($pid) newInsuranceData($pid, "tertiary"); } -function getNewPid() -{ - $result = sqlQuery("select max(pid)+1 as pid from patient_data"); - $newpid = 1; - if ($result['pid'] > 1) { - $newpid = $result['pid']; - } - if ($newpid == null) { - $newpid = 0; - } - return (int)$newpid; -} - function validEmail($email) { if (preg_match("/^[_a-z0-9-]+(\.[_a-z0-9-]+)*@[a-z0-9-]+(\.[a-z0-9-]+)*(\.[a-z]{2,3})$/i", $email)) { @@ -144,7 +347,7 @@ function validEmail($email) return false; } -function messageCreate($uname, $pass, $encoded_link = '') +function messageCreate($pass, $encoded_link = '') { $message = '

' . xlt("We received a credentials reset request. The link to reset your credentials is below.") . '

'; $message .= '

' . xlt("Please ignore this email if you did not make this request") . '

'; @@ -158,30 +361,69 @@ function messageCreate($uname, $pass, $encoded_link = '') return $message; } -function doCredentials($pid) +// $resetPass mode return false when something breaks (although returns true if related to a patient existing or not to prevent fishing for patients) +// !$resetPass mode return false when something breaks (no need to protect against from fishing since can't do from registration workflow) +function doCredentials($pid, $resetPass = false, $resetPassEmail = ''): bool { $newpd = sqlQuery("SELECT id,fname,mname,lname,email,email_direct, providerID FROM `patient_data` WHERE `pid` = ?", array($pid)); $user = sqlQueryNoLog("SELECT users.username FROM users WHERE authorized = 1 And id = ?", array($newpd['providerID'])); + + // ensure pid exists if (empty($newpd)) { - error_log('OpenEMR Error : Portal credential retrieve error. Record not found.'); - return xl("ERROR Portal credentials retrieve error. Record not found."); + if ($resetPass) { + (new SystemLogger())->error("doCredentials function did not find a patient from patient_data for " . $pid . " (this should never happen since checked in resetPassword function), so was unable to reset the password"); + return true; + } else { // !$resetPass + EventAuditLogger::instance()->newEvent('patient-registration', '', '', 0, "Patient credential creation failure: Following pid did not exist: " . $pid); + (new SystemLogger())->error("doCredentials function did not find a patient from patient_data for " . $pid . " , so was unable to create credentials"); + return false; + } + } + + // ensure email is valid + if ($resetPass) { + if ((empty($resetPassEmail)) || ((($newpd['email'] ?? '') != $resetPassEmail) && (($newpd['email_direct'] ?? '') != $resetPassEmail))) { + (new SystemLogger())->error("doCredentials function with empty email or unable to find correct email " . $resetPassEmail . " in patient from patient_data for pid " . $pid . " (this should never happen since checked in resetPassword function), so was unable to reset the password"); + return true; + } + if (!validEmail($resetPassEmail)) { + EventAuditLogger::instance()->newEvent('patient-password-reset', '', '', 0, "Patient password reset failure: Email " . $resetPassEmail . " was not considered valid for pid: " . $pid); + (new SystemLogger())->error("doCredentials function with email " . $resetPassEmail . " for pid " . $pid . " that was not valid per validEmail function, so was unable to reset the password"); + return false; + } + $newpd['email'] = $resetPassEmail; + } else { // !$resetPass + if (!validEmail($newpd['email'])) { + EventAuditLogger::instance()->newEvent('patient-registration', '', '', 0, "Patient password reset failure: Email " . $newpd['email'] . " was not considered valid for pid: " . $pid); + (new SystemLogger())->error("doCredentials function with email " . $newpd['email'] . " for pid " . $pid . " was not valid per validEmail function, so was unable to complete the registration"); + return false; + } } - $crypto = new CryptoGen(); - $uname = $newpd['fname'] . $newpd['id']; + // Token expiry 1 hour $expiry = new DateTime('NOW'); $expiry->add(new DateInterval('PT01H')); - $clear_pass = RandomGenUtils::generatePortalPassword(); $token_new = RandomGenUtils::createUniqueToken(32); $pin = RandomGenUtils::createUniqueToken(6); + if (!$resetPass) { + $clear_pass = RandomGenUtils::generatePortalPassword(); + $uname = $newpd['fname'] . $newpd['id']; + } // Will send a link to user with encrypted token - $token = $crypto->encryptStandard($token_new); + $token = (new CryptoGen())->encryptStandard($token_new); if (empty($token)) { - // Serious issue if this is case, so die. - error_log('OpenEMR Error : Portal token encryption broken - exiting'); - die('Error : Token encryption failed - exiting'); + // Serious issue if this is case, so exit. + if ($resetPass) { + EventAuditLogger::instance()->newEvent('patient-password-reset', '', '', 0, "Patient password reset failure secondary critical encryption error for email " . $newpd['email'] . " and pid: " . $pid); + (new SystemLogger())->error("Error : Token encryption failed during patient password reset - exiting"); + return false; + } else { // !$resetPass + EventAuditLogger::instance()->newEvent('patient-registration', '', '', 0, "Patient credential creation registration failure secondary critical encryption error for email " . $newpd['email'] . " and pid: " . $pid); + (new SystemLogger())->error("Error : Token encryption failed during patient registration - exiting"); + return false; + } } $site_addr = $GLOBALS['portal_onsite_two_address']; $site_id = $_SESSION['site_id']; @@ -196,35 +438,42 @@ function doCredentials($pid) ])); } - // Will store unencrypted token in database with the pin and expiration date - $one_time = $token_new . $pin . bin2hex($expiry->format('U')); - $res = sqlStatement("SELECT * FROM patient_access_onsite WHERE pid=?", array($pid)); - $query_parameters = array($uname, $one_time); - $newHash = (new AuthHash('auth'))->passwordHash($clear_pass); - if (empty($newHash)) { - // Something is seriously wrong - error_log('OpenEMR Error : OpenEMR is not working because unable to create a hash.'); - die("OpenEMR Error : OpenEMR is not working because unable to create a hash."); - } - $query_parameters[] = $newHash; - $query_parameters[] = $pid; - if (sqlNumRows($res)) { - sqlStatementNoLog("UPDATE patient_access_onsite SET portal_username=?,portal_onetime=?,portal_pwd=?,portal_pwd_status=0 WHERE pid=?", $query_parameters); - } else { - sqlStatementNoLog("INSERT INTO patient_access_onsite SET portal_username=?,portal_onetime=?,portal_pwd=?,portal_pwd_status=0,pid=?", $query_parameters); + if (!$resetPass) { + $newHash = (new AuthHash('auth'))->passwordHash($clear_pass); + if (empty($newHash)) { + // Serious issue if this is case, so exit. + EventAuditLogger::instance()->newEvent('patient-registration', '', '', 0, "Patient credential creation registration failure secondary critical hashing error for email " . $newpd['email'] . " and pid: " . $pid); + (new SystemLogger())->error("Error : Hashing failed during patient registration - exiting"); + return false; + } } - if (!validEmail($newpd['email_direct'])) { - if (validEmail($newpd['email'])) { - $newpd['email_direct'] = $newpd['email']; + // Will store unencrypted token in database with the pin and expiration date + $one_time = $token_new . $pin . bin2hex($expiry->format('U')); + if ($resetPass) { + // already confirmed there is an entry in patient_access_onsite in previously called resetPassword function + // (note that portal_username, portal_pwd_status and portal_pwd are not touched here since password needs to remain valid until patient + // actually changes the password) + $query_parameters = [$one_time, $pid]; + sqlStatementNoLog("UPDATE `patient_access_onsite` SET `portal_onetime` = ? WHERE `pid` = ?", $query_parameters); + } else { // !$resetPass + $query_parameters = [$uname, $one_time, $newHash, $pid]; + $res = sqlStatement("SELECT `id` FROM `patient_access_onsite` WHERE `pid` = ?", [$pid]); + if (sqlNumRows($res)) { + // this should never happen in current use case where these credentials are created after a new patient registers, so will return error + EventAuditLogger::instance()->newEvent('patient-registration', '', '', 0, "Patient credential creation registration failure secondary to credentials already existing for email " . $newpd['email'] . " and pid: " . $pid); + (new SystemLogger())->error("OpenEMR Error : doCredentials for registration - already credentials exists, so unable to create new credentials."); + return false; + } else { + sqlStatementNoLog("INSERT INTO patient_access_onsite SET portal_username=?,portal_onetime=?,portal_pwd=?,portal_pwd_status=0,pid=?", $query_parameters); } } - $message = messageCreate($uname, $pin, $encoded_link); + $message = messageCreate($pin, $encoded_link); $mail = new MyMailer(); $pt_name = text($newpd['fname'] . ' ' . $newpd['lname']); - $pt_email = text($newpd['email_direct']); + $pt_email = text($newpd['email']); $email_subject = xlt('Access Your Patient Portal'); $email_sender = $GLOBALS['patient_reminder_sender_email']; $mail->AddReplyTo($email_sender, $email_sender); @@ -236,31 +485,69 @@ function doCredentials($pid) $mail->AltBody = $message; if ($mail->Send()) { - $sent = 1; + if ($resetPass) { + EventAuditLogger::instance()->newEvent('patient-password-reset', '', '', 1, "The patient reset email was successfully sent to " . $newpd['email'] . " for pid " . $pid . "."); + (new SystemLogger())->debug("The patient reset email was successfully sent to " . $newpd['email'] . " for pid " . $pid . "."); + } else { // !$resetPass + EventAuditLogger::instance()->newEvent('patient-registration', '', '', 1, "The patient registration credentials email was successfully sent to " . $newpd['email'] . " for pid " . $pid . "."); + (new SystemLogger())->debug("The patient registration credentials email was successfully sent to " . $newpd['email'] . " for pid " . $pid . "."); + } + return true; } else { $email_status = $mail->ErrorInfo; - $errorMsg = "EMAIL ERROR: " . errorLogEscape($email_status) . '
'; - if ($newpd['id']) { - $errorMsg .= xlt("Your account has been successfully created, however; we were unable to send your new account information."); - $errorMsg .= "
" . xlt("Please contact your providers office with the following account information") . ":
"; - $errorMsg1 = xlt("Account Id") . ": " . $uname . " " . xlt("MRN Reference") . ": " . $pid; - $errorMsg .= $errorMsg1; - $errorMsg .= "

" . xlt("The providers office has been notified. Thank you.") . "
"; + if ($resetPass) { + EventAuditLogger::instance()->newEvent('patient-password-reset', '', '', 0, "Patient password reset failure: The reset email to " . $newpd['email'] . " for pid " . $pid . " was not successful because of following issue: " . $email_status); + (new SystemLogger())->error("Patient password reset failure: The reset email to " . $newpd['email'] . " for pid " . $pid . " was not successful because of following issue: " . $email_status); + } else { // !$resetPass + EventAuditLogger::instance()->newEvent('patient-registration', '', '', 0, "The patient registration credentials email was not successfully sent to " . $newpd['email'] . " for pid " . $pid . " because of following issue: " . $email_status); + (new SystemLogger())->error("The patient registration credentials email was not successfully sent to " . $newpd['email'] . " for pid " . $pid . " because of following issue: " . $email_status); // notify admin of failure. $title = xlt("Failed Registration"); $admin_msg = "\n" . xlt("A new patients credentials could not be sent after portal registration."); - $admin_msg .= "\n" . $errorMsg1; + $admin_msg .= "\n" . "EMAIL ERROR: " . $email_status; $admin_msg .= "\n" . xlt("Please follow up."); // send note addPnote($pid, $admin_msg, 1, 1, $title, $user['username'], '', 'New'); - } else { - $errorMsg .= "
" . xlt("We were unable to create an account.") . "
"; - $errorMsg .= xlt("Please try again or contact the providers office for further assistance."); } - error_log("Portal Registration error: " . errorLogEscape($errorMsg), 0); + return false; + } +} - return $errorMsg; +// the race condition can happen in registration since basically submitting patient info and insurance info at same time +// where the pid is created and stored by the patient info. so, will sleep 1 seconds prior first attempt and then 5 seconds +// prior second attempt to allow things to work out. Can make this mechanism more sophisticated in future if needed. In the +// case of insurance, if it does fail getting the pid for some reason then the registration will still happen (and will +// just not store the insurance info in worst case scenario). +function getPidHolder($preventRaceCondition = false): int +{ + if (empty($_SESSION['token_id_holder'])) { + (new SystemLogger())->debug("getPidHolder function failed because token_id_holder session variable was not set"); + return 0; } + if ($preventRaceCondition) { + sleep(1); + } + $sql = sqlQueryNoLog("SELECT `pid_holder` FROM `verify_email` WHERE `id` = ?", [$_SESSION['token_id_holder']]); + if (!empty($sql['pid_holder'])) { + return $sql['pid_holder']; + } else { + if (!$preventRaceCondition) { + return 0; + } else { // $preventRaceCondition + (new SystemLogger())->debug("getPidHolder function sleeping fo 5 seconds to deal with race condition"); + sleep(5); + return getPidHolder(); + } + } +} - return $sent; +function cleanupRegistrationSession() +{ + unset($_SESSION['patient_portal_onsite_two']); + unset($_SESSION['authUser']); + unset($_SESSION['pid']); + unset($_SESSION['site_id']); + unset($_SESSION['register']); + unset($_SESSION['register_silo_ajax']); + OpenEMR\Common\Session\SessionUtil::portalSessionCookieDestroy(); } diff --git a/portal/account/account.php b/portal/account/account.php index 9263dd16a90..af98cb01b60 100644 --- a/portal/account/account.php +++ b/portal/account/account.php @@ -17,6 +17,7 @@ OpenEMR\Common\Session\SessionUtil::portalSessionStart(); if ( + (!empty($_SESSION['verifyPortalEmail']) && ($_SESSION['verifyPortalEmail'] === true)) || (($_SESSION['register'] ?? null) === true && isset($_SESSION['pid'])) || (($_SESSION['credentials_update'] ?? null) === 1 && isset($_SESSION['pid'])) || (($_SESSION['itsme'] ?? null) === 1 && isset($_SESSION['password_update'])) @@ -29,11 +30,41 @@ require_once(__DIR__ . "/../lib/portal_mail.inc"); require_once("$srcdir/pnotes.inc"); require_once("./account.lib.php"); + +use OpenEMR\Common\Csrf\CsrfUtils; +use OpenEMR\Common\Logging\SystemLogger; +use OpenEMR\Core\Header; + $action = $_REQUEST['action'] ?? ''; -if ($action == 'set_lang') { - $_SESSION['language_choice'] = (int)$_REQUEST['value']; - echo 'okay'; - exit(); + +if ($action == 'verify_email') { + if (!empty($_SESSION['verifyPortalEmail']) && ($_SESSION['verifyPortalEmail'] === true)) { + if (!empty($GLOBALS['portal_onsite_two_register']) && !empty($GLOBALS['google_recaptcha_site_key']) && !empty($GLOBALS['google_recaptcha_secret_key'])) { + // check csrf + if (!CsrfUtils::verifyCsrfToken($_POST["csrf_token_form"], 'verifyEmailCsrf')) { + CsrfUtils::csrfNotVerified(true, true, false); + cleanupRegistrationSession(); + exit; + } + // check recaptcha + $recaptcha = processRecaptcha($_POST['g-recaptcha-response'] ?? ''); + if (!$recaptcha) { + echo xlt("Something went wrong. Recommend contacting the clinic."); + cleanupRegistrationSession(); + exit; + } + // process + $rtn = verifyEmail($_POST['languageChoice'] ?? '', $_POST['fname'] ?? '', $_POST['mname'] ?? '', $_POST['lname'] ?? '', $_POST['dob'] ?? '', $_POST['email'] ?? ''); + if ($rtn) { + Header::setupHeader(); + echo ''; + } else { + echo xlt("Something went wrong. Recommend contacting the clinic."); + } + } + } + cleanupRegistrationSession(); + exit; } if ($action == 'userIsUnique') { @@ -66,58 +97,85 @@ exit; } -if ($action == 'get_newpid') { - $email = $_REQUEST['email'] ?? ''; - $rtn = isNew($_REQUEST['dob'], $_REQUEST['last'], $_REQUEST['first'], $email); - if ((int)$rtn != 0) { - echo xlt("This account already exists.") . "\r\n\r\n" . - xlt("We are sorry you are having troubles with your account.") . "\r\n" . - xlt("Please contact your provider.") . "\r\n" . - xlt("Reference this Account Number") . " " . $rtn; +if ($action == 'reset_password') { + if (($_SESSION['register'] ?? null) === true && isset($_SESSION['pid'])) { + $rtn = 0; + if (!empty($GLOBALS['portal_two_pass_reset']) && !empty($GLOBALS['google_recaptcha_site_key']) && !empty($GLOBALS['google_recaptcha_secret_key'])) { + // check csrf + if (!CsrfUtils::verifyCsrfToken($_GET["csrf_token_form"], 'passwordResetCsrf')) { + CsrfUtils::csrfNotVerified(true, true, false); + cleanupRegistrationSession(); + exit; + } + // check recaptcha + $recaptcha = processRecaptcha($_GET['g-recaptcha-response'] ?? ''); + if ($recaptcha) { + // Allow Patients to Reset Credentials setting is turned on + $rtn = resetPassword($_GET['dob'] ?? '', $_GET['last'] ?? '', $_GET['first'] ?? '', $_GET['email'] ?? ''); + } + } + echo js_escape($rtn); + exit(); + } else { + cleanupRegistrationSession(); exit(); } - $rtn = getNewPid(); - echo js_escape($rtn); - exit(); -} - -if ($action == 'is_new') { - $email = isset($_REQUEST['email']) ? $_REQUEST['email'] : ''; - $rtn = isNew($_REQUEST['dob'], $_REQUEST['last'], $_REQUEST['first'], $email); - echo js_escape($rtn); - exit(); } if ($action == 'do_signup') { - $rtn = doCredentials($_REQUEST['pid']); - echo js_escape($rtn); + if (($_SESSION['register_silo_ajax'] ?? null) === true && ($_SESSION['register'] ?? null) === true && isset($_SESSION['pid'])) { + if (!empty($GLOBALS['portal_onsite_two_register']) && !empty($GLOBALS['google_recaptcha_site_key']) && !empty($GLOBALS['google_recaptcha_secret_key'])) { + $pidHolder = getPidHolder(); + if ($pidHolder == 0) { + (new SystemLogger())->error("account.php action do_signup failed because unable to collect pid from pid_holder"); + cleanupRegistrationSession(); + exit(); + } + $rtn = doCredentials($pidHolder); + if ($rtn) { + (new SystemLogger())->debug("account.php action do_signup apparently successful"); + if (!empty($_GET['provider'])) { + notifyAdmin($pidHolder, $_GET['provider']); + (new SystemLogger())->debug("account.php action do_signup apparently successful, so sent a pnote to the provider"); + } + Header::setupHeader(); + echo ''; + } else { + (new SystemLogger())->debug("account.php action do_signup apparently not successful"); + Header::setupHeader(); + echo ''; + } + } else { + (new SystemLogger())->error("account.php action do_signup attempted without registration module on, so failed"); + } + } + cleanupRegistrationSession(); exit(); } if ($action == 'new_insurance') { - $pid = $_REQUEST['pid']; - saveInsurance($pid); - exit(); -} - -if ($action == 'notify_admin') { - $pid = $_REQUEST['pid']; - $provider = $_REQUEST['provider']; - $rtn = notifyAdmin($pid, $provider); - echo js_escape($rtn); - exit(); + if (($_SESSION['register_silo_ajax'] ?? null) === true && ($_SESSION['register'] ?? null) === true && isset($_SESSION['pid'])) { + if (!empty($GLOBALS['portal_onsite_two_register']) && !empty($GLOBALS['google_recaptcha_site_key']) && !empty($GLOBALS['google_recaptcha_secret_key'])) { + $pidHolder = getPidHolder(true); + if ($pidHolder == 0) { + (new SystemLogger())->error("account.php action new_insurance was not successful because unable to collect pid from pid_holder. will still complete registration process, which will not include insurance."); + exit(); + } + saveInsurance($pidHolder); + (new SystemLogger())->debug("account.php action new_insurance was apparently successful"); + exit(); + } else { + (new SystemLogger())->error("account.php action new_insurance attempted without registration module on, so failed"); + cleanupRegistrationSession(); + exit(); + } + } else { + cleanupRegistrationSession(); + exit(); + } } if ($action == 'cleanup') { - unset($_SESSION['patient_portal_onsite_two']); - unset($_SESSION['authUser']); - unset($_SESSION['pid']); - unset($_SESSION['site_id']); - unset($_SESSION['register']); - echo 'gone'; - OpenEMR\Common\Session\SessionUtil::portalSessionCookieDestroy(); -// I know, makes little sense. -} else { + cleanupRegistrationSession(); exit(); } -die(); //too be sure diff --git a/portal/account/register.php b/portal/account/register.php index 8014cb6e6bb..ae102456e8c 100644 --- a/portal/account/register.php +++ b/portal/account/register.php @@ -12,73 +12,33 @@ * @license https://github.com/openemr/openemr/blob/master/LICENSE GNU General Public License 3 */ -use OpenEMR\Core\Header; - -// Will start the (patient) portal OpenEMR session/cookie. -require_once(dirname(__FILE__) . "/../../src/Common/Session/SessionUtil.php"); -OpenEMR\Common\Session\SessionUtil::portalSessionStart(); -session_regenerate_id(true); - -unset($_SESSION['itsme']); -$_SESSION['authUser'] = 'portal-user'; -$_SESSION['pid'] = true; -$_SESSION['register'] = true; +// script is brought in as require_once in index.php when applicable -$_SESSION['site_id'] = $_SESSION['site_id'] ?? 'default'; -$landingpage = "index.php?site=" . urlencode($_SESSION['site_id']); - -$ignoreAuth_onsite_portal = true; +use OpenEMR\Core\Header; -require_once("../../interface/globals.php"); -if (!$GLOBALS['portal_onsite_two_register']) { +if ($portalRegistrationAuthorization !== true) { + (new SystemLogger())->debug("attempted to use register.php directly, so failed"); OpenEMR\Common\Session\SessionUtil::portalSessionCookieDestroy(); echo xlt("Not Authorized"); - @header('HTTP/1.1 401 Unauthorized'); + header('HTTP/1.1 401 Unauthorized'); die(); } -$res2 = sqlStatement("select * from lang_languages where lang_description = ?", array( - $GLOBALS['language_default'] -)); -for ($iter = 0; $row = sqlFetchArray($res2); $iter++) { - $result2[$iter] = $row; -} -if (count($result2) == 1) { - $defaultLangID = $result2[0]["lang_id"]; - $defaultLangName = $result2[0]["lang_description"]; -} else { - // default to english if any problems - $defaultLangID = 1; - $defaultLangName = "English"; +if (empty($GLOBALS['portal_onsite_two_register']) || empty($GLOBALS['google_recaptcha_site_key']) || empty($GLOBALS['google_recaptcha_secret_key'])) { + (new SystemLogger())->debug("attempted to use register.php despite register feature being turned off, so failed"); + OpenEMR\Common\Session\SessionUtil::portalSessionCookieDestroy(); + echo xlt("Not Authorized"); + header('HTTP/1.1 401 Unauthorized'); + die(); } -if (!isset($_SESSION['language_choice'])) { - $_SESSION['language_choice'] = $defaultLangID; -} -// collect languages if showing language menu -if ($GLOBALS['language_menu_login']) { - // sorting order of language titles depends on language translation options. - $mainLangID = empty($_SESSION['language_choice']) ? '1' : $_SESSION['language_choice']; - // Use and sort by the translated language name. - $sql = "SELECT ll.lang_id, " . "IF(LENGTH(ld.definition),ld.definition,ll.lang_description) AS trans_lang_description, " . "ll.lang_description " . - "FROM lang_languages AS ll " . "LEFT JOIN lang_constants AS lc ON lc.constant_name = ll.lang_description " . - "LEFT JOIN lang_definitions AS ld ON ld.cons_id = lc.cons_id AND " . "ld.lang_id = ? " . - "ORDER BY IF(LENGTH(ld.definition),ld.definition,ll.lang_description), ll.lang_id"; - $res3 = SqlStatement($sql, array( - $mainLangID - )); - - for ($iter = 0; $row = sqlFetchArray($res3); $iter++) { - $result3[$iter] = $row; - } - - if (count($result3) == 1) { - // default to english if only return one language - $hiddenLanguageField = "\n"; - } -} else { - $hiddenLanguageField = "\n"; -} +unset($_SESSION['itsme']); +$_SESSION['authUser'] = 'portal-user'; +$_SESSION['pid'] = true; +$_SESSION['register'] = true; +$_SESSION['register_silo_ajax'] = true; + +$landingpage = "index.php?site=" . urlencode($_SESSION['site_id']); ?> @@ -92,8 +52,6 @@ @@ -405,7 +265,7 @@ function callServerAction(data) {
1 -

+

2 @@ -422,74 +282,27 @@ function callServerAction(data) {
-
+
-
- - -
- - -
- -
-
- - -
-
- - -
-
- - -
-
- - -
-
-
- - -
+ + + + + + +
-
+
@@ -550,7 +363,7 @@ function callServerAction(data) {


- +


diff --git a/portal/account/verify.php b/portal/account/verify.php new file mode 100644 index 00000000000..2b4e5c39480 --- /dev/null +++ b/portal/account/verify.php @@ -0,0 +1,195 @@ + + * @author Brady Miller + * @copyright Copyright (c) 2017-2019 Jerry Padgett + * @copyright Copyright (c) 2019-2022 Brady Miller + * @license https://github.com/openemr/openemr/blob/master/LICENSE GNU General Public License 3 + */ + +use OpenEMR\Common\Csrf\CsrfUtils; +use OpenEMR\Core\Header; +use OpenEMR\Common\Logging\SystemLogger; + +// Will start the (patient) portal OpenEMR session/cookie. +require_once(dirname(__FILE__) . "/../../src/Common/Session/SessionUtil.php"); +OpenEMR\Common\Session\SessionUtil::portalSessionStart(); +session_regenerate_id(true); + +unset($_SESSION['itsme']); +$_SESSION['verifyPortalEmail'] = true; + +$ignoreAuth_onsite_portal = true; +require_once("../../interface/globals.php"); + +$landingpage = "../index.php?site=" . urlencode($_SESSION['site_id']); + +if (empty($GLOBALS['portal_onsite_two_register']) || empty($GLOBALS['google_recaptcha_site_key']) || empty($GLOBALS['google_recaptcha_secret_key'])) { + OpenEMR\Common\Session\SessionUtil::portalSessionCookieDestroy(); + echo xlt("Not Authorized"); + header('HTTP/1.1 401 Unauthorized'); + die(); +} + +// set up csrf +CsrfUtils::setupCsrfKey(); + +$res2 = sqlStatement("select * from lang_languages where lang_description = ?", array( + $GLOBALS['language_default'] +)); +for ($iter = 0; $row = sqlFetchArray($res2); $iter++) { + $result2[$iter] = $row; +} +if (count($result2) == 1) { + $defaultLangID = $result2[0]["lang_id"]; + $defaultLangName = $result2[0]["lang_description"]; +} else { + // default to english if any problems + $defaultLangID = 1; + $defaultLangName = "English"; +} + +if (!isset($_SESSION['language_choice'])) { + $_SESSION['language_choice'] = $defaultLangID; +} +// collect languages if showing language menu +if ($GLOBALS['language_menu_login']) { + // sorting order of language titles depends on language translation options. + $mainLangID = empty($_SESSION['language_choice']) ? '1' : $_SESSION['language_choice']; + // Use and sort by the translated language name. + $sql = "SELECT ll.lang_id, " . "IF(LENGTH(ld.definition),ld.definition,ll.lang_description) AS trans_lang_description, " . "ll.lang_description " . + "FROM lang_languages AS ll " . "LEFT JOIN lang_constants AS lc ON lc.constant_name = ll.lang_description " . + "LEFT JOIN lang_definitions AS ld ON ld.cons_id = lc.cons_id AND " . "ld.lang_id = ? " . + "ORDER BY IF(LENGTH(ld.definition),ld.definition,ll.lang_description), ll.lang_id"; + $res3 = SqlStatement($sql, array( + $mainLangID + )); + + for ($iter = 0; $row = sqlFetchArray($res3); $iter++) { + $result3[$iter] = $row; + } + + if (count($result3) == 1) { + // default to english if only return one language + $hiddenLanguageField = "\n"; + } +} else { + $hiddenLanguageField = "\n"; +} + +?> + + + + <?php echo xlt('New Patient'); ?> | <?php echo xlt('Register'); ?> + + + + + + + + + + +
+

+
+
+
+ 1 +

+
+
+ 2 +

+
+
+ 3 +

+
+
+ 4 +

+
+
+
+ +
+ ' /> +
+ +
+ + +
+ + +
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
+
+
+
+
+
+ +
+
+
+ + diff --git a/portal/index.php b/portal/index.php index 9cdf065f032..c99099c6c8d 100644 --- a/portal/index.php +++ b/portal/index.php @@ -31,6 +31,9 @@ $logit = new ApplicationTable(); use OpenEMR\Common\Crypto\CryptoGen; +use OpenEMR\Common\Csrf\CsrfUtils; +use OpenEMR\Common\Logging\EventAuditLogger; +use OpenEMR\Common\Logging\SystemLogger; use OpenEMR\Core\Header; //For redirect if the site on session does not match @@ -46,7 +49,99 @@ unset($_GET['woops']); unset($_SESSION['password_update']); } -if (isset($_GET['forward'])) { + +if (!empty($_GET['forward_email_verify'])) { + if (empty($GLOBALS['portal_onsite_two_register']) || empty($GLOBALS['google_recaptcha_site_key']) || empty($GLOBALS['google_recaptcha_secret_key'])) { + (new SystemLogger())->debug("registration not supported, so stopped attempt to use forward_email_verify token"); + OpenEMR\Common\Session\SessionUtil::portalSessionCookieDestroy(); + header('Location: ' . $landingpage . '&w&u'); + exit(); + } + + $crypto = new CryptoGen(); + if (!$crypto->cryptCheckStandard($_GET['forward_email_verify'])) { + (new SystemLogger())->debug("illegal token, so stopped attempt to use forward_email_verify token"); + OpenEMR\Common\Session\SessionUtil::portalSessionCookieDestroy(); + header('Location: ' . $landingpage . '&w&u'); + exit(); + } + + $token_one_time = $crypto->decryptStandard($_GET['forward_email_verify'], null, 'drive', 6); + if (empty($token_one_time)) { + (new SystemLogger())->debug("unable to decrypt token, so stopped attempt to use forward_email_verify token"); + OpenEMR\Common\Session\SessionUtil::portalSessionCookieDestroy(); + header('Location: ' . $landingpage . '&w&u'); + exit(); + } + + $sqlResource = sqlStatementNoLog("SELECT `id`, `token_onetime`, `fname`, `mname`, `lname`, `dob`, `email`, `language` FROM `verify_email` WHERE `active` = 1 AND `token_onetime` LIKE BINARY ?", [$token_one_time . '%']); + if (sqlNumRows($sqlResource) > 1) { + (new SystemLogger())->debug("active token (" . $token_one_time . ") found more than once, so stopped attempt to use forward_email_verify token"); + EventAuditLogger::instance()->newEvent('patient-reg-email-verify', '', '', 0, "active token (" . $token_one_time . ") found more than once, so stopped attempt to use forward_email_verify token"); + OpenEMR\Common\Session\SessionUtil::portalSessionCookieDestroy(); + header('Location: ' . $landingpage . '&w&u'); + exit(); + } + if (!sqlNumRows($sqlResource)) { + (new SystemLogger())->debug("active token (" . $token_one_time . ") not found, so stopped attempt to use forward_email_verify token"); + EventAuditLogger::instance()->newEvent('patient-reg-email-verify', '', '', 0, "active token (" . $token_one_time . ") not found, so stopped attempt to use forward_email_verify token"); + OpenEMR\Common\Session\SessionUtil::portalSessionCookieDestroy(); + header('Location: ' . $landingpage . '&w&u'); + exit(); + } + $sqlVerify = sqlFetchArray($sqlResource); + if (empty($sqlVerify['id']) || empty($sqlVerify['token_onetime'])) { + (new SystemLogger())->debug("active token (" . $token_one_time . ") not properly set up, so stopped attempt to use forward_email_verify token"); + EventAuditLogger::instance()->newEvent('patient-reg-email-verify', '', '', 0, "active token (" . $token_one_time . ") not properly set up, so stopped attempt to use forward_email_verify token"); + OpenEMR\Common\Session\SessionUtil::portalSessionCookieDestroy(); + header('Location: ' . $landingpage . '&w&u'); + exit(); + } + // have "used" token, so now make it inactive + sqlStatementNoLog("UPDATE `verify_email` SET `active` = 0 WHERE `id` = ?", [$sqlVerify['id']]); + + $validateTime = hex2bin(str_replace($token_one_time, '', $sqlVerify['token_onetime'])); + if ($validateTime <= time()) { + (new SystemLogger())->debug("active token (" . $token_one_time . ") has expired, so stopped attempt to use forward_email_verify token"); + EventAuditLogger::instance()->newEvent('patient-reg-email-verify', '', '', 0, "active token (" . $token_one_time . ") has expired, so stopped attempt to use forward_email_verify token"); + OpenEMR\Common\Session\SessionUtil::portalSessionCookieDestroy(); + die(xlt("Your email verification link has expired. Reset and try again.")); + } + + if (!empty($sqlVerify['fname']) && !empty($sqlVerify['lname']) && !empty($sqlVerify['dob']) && !empty($sqlVerify['email']) && !empty($sqlVerify['language'])) { + // token has passed and have all needed data + $fnameRegistration = $sqlVerify['fname']; + $_SESSION['fnameRegistration'] = $fnameRegistration; + $mnameRegistration = $sqlVerify['mname'] ?? ''; + $_SESSION['mnameRegistration'] = $mnameRegistration; + $lnameRegistration = $sqlVerify['lname']; + $_SESSION['lnameRegistration'] = $lnameRegistration; + $dobRegistration = $sqlVerify['dob']; + $_SESSION['dobRegistration'] = $dobRegistration; + $emailRegistration = $sqlVerify['email']; + $_SESSION['emailRegistration'] = $emailRegistration; + $languageRegistration = $sqlVerify['language']; + $_SESSION['language_choice'] = (int)($languageRegistration ?? 1); + $portalRegistrationAuthorization = true; + $_SESSION['token_id_holder'] = $sqlVerify['id']; + (new SystemLogger())->debug("token worked for forward_email_verify token, now on to registration"); + EventAuditLogger::instance()->newEvent('patient-reg-email-verify', '', '', 1, "token (" . $token_one_time . ") was successful for forward_email_verify token"); + require_once(__DIR__ . "/account/register.php"); + exit(); + } else { + (new SystemLogger())->debug("active token (" . $token_one_time . ") did not have all required data, so stopped attempt to use forward_email_verify token"); + EventAuditLogger::instance()->newEvent('patient-reg-email-verify', '', '', 0, "active token (" . $token_one_time . ") did not have all required data, so stopped attempt to use forward_email_verify token"); + OpenEMR\Common\Session\SessionUtil::portalSessionCookieDestroy(); + header('Location: ' . $landingpage . '&w&u'); + exit(); + } +} else if (isset($_GET['forward'])) { + if ((empty($GLOBALS['portal_two_pass_reset']) && empty($GLOBALS['portal_onsite_two_register'])) || empty($GLOBALS['google_recaptcha_site_key']) || empty($GLOBALS['google_recaptcha_secret_key'])) { + (new SystemLogger())->debug("reset password and registration not supported, so stopped attempt to use forward token"); + OpenEMR\Common\Session\SessionUtil::portalSessionCookieDestroy(); + header('Location: ' . $landingpage . '&w&u'); + exit(); + } $auth = false; if (strlen($_GET['forward']) >= 64) { $crypto = new CryptoGen(); @@ -87,8 +182,8 @@ // // Deal with language selection // -// collect default language id (skip this if this is a password update) -if (!(isset($_SESSION['password_update']) || isset($_GET['requestNew']))) { +// collect default language id (skip this if this is a password update or reset) +if (!(isset($_SESSION['password_update']) || (!empty($GLOBALS['portal_two_pass_reset']) && !empty($GLOBALS['google_recaptcha_site_key']) && !empty($GLOBALS['google_recaptcha_secret_key']) && isset($_GET['requestNew'])))) { $res2 = sqlStatement("select * from lang_languages where lang_description = ?", array($GLOBALS['language_default'])); for ($iter = 0; $row = sqlFetchArray($res2); $iter++) { $result2[$iter] = $row; @@ -226,6 +321,19 @@ function validate_new_pass() { return pass; } + + + + + + +
@@ -287,8 +395,9 @@ function validate_new_pass() { - +
+ ' />

@@ -318,8 +427,13 @@ function validate_new_pass() {
+
+
+
+
+
- + @@ -383,10 +497,10 @@ function validate_new_pass() {
- - + + - + @@ -421,7 +535,7 @@ function restoreSession() { $(function () { @@ -453,27 +567,20 @@ function restoreSession() { return false; }); - function callServer(action, value = '', value2 = '') { - var data = { - 'action': action, - 'value': value, - 'dob': $("#dob").val(), - 'last': $("#lname").val(), - 'first': $("#fname").val(), - 'email': $("#emailInput").val() - }; - if (action === 'do_signup') { + function callServer(action) { + var data = {}; + if (action === 'reset_password') { data = { 'action': action, - 'pid': value - }; - } else if (action === 'notify_admin') { - data = { - 'action': action, - 'pid': value, - 'provider': value2 - }; - } else if (action === 'cleanup') { + 'dob': $("#dob").val(), + 'last': $("#lname").val(), + 'first': $("#fname").val(), + 'email': $("#emailInput").val(), + 'g-recaptcha-response': grecaptcha.getResponse(), + 'csrf_token_form': $("#csrf_token_form").val() + } + } + if (action === 'cleanup') { data = { 'action': action } @@ -485,36 +592,14 @@ function callServer(action, value = '', value2 = '') { }).done(function (rtn) { if (action === "cleanup") { window.location.href = "./index.php?site=" + ; // Goto landing page. - } else if (action === "userIsUnique") { - return rtn === '1'; - } else if (action === "is_new") { - if (parseInt(rtn) !== 0) { - let yes = confirm(); - if (!yes) { - callServer('cleanup'); - } else { - callServer('do_signup', parseInt(rtn)); - } - } else { - // After error alert app exit to landing page. - var message = ; - message += "
" + ; - dialog.alert(message, ).then(function (result) { - console.error('Reset failed to vaidate'); - }); + } else if (action === "reset_password") { + if (JSON.parse(rtn) === 1) { + dialog.alert(); return false; - } - } else if (action === 'do_signup') { - if (rtn.indexOf('ERROR') !== -1) { - let message = ; - message += "

" + +": " + rtn + "
"; - dialog.alert(message); + } else { + dialog.alert(); return false; } - //alert(rtn); // sync alert.. rtn holds username and password for testing. - let message = ; - dialog.alert(message); // This is an async call. The modal close event exits us to portal landing page after cleanup. - return false; } }).fail(function (err) { var message = ; diff --git a/portal/patient/_app_config.php b/portal/patient/_app_config.php index 1886b316b36..79776ff7c7e 100644 --- a/portal/patient/_app_config.php +++ b/portal/patient/_app_config.php @@ -107,7 +107,7 @@ 'POST:api/patient' => array( 'route' => 'Patient.Create', 'p_acl' => 'p_none', - 'p_reg' => true + 'p_reg' => true // Secured this at downstream function level ), 'GET:api/patient/(:num)' => array( 'route' => 'Patient.Read', diff --git a/portal/patient/fwk/libs/verysimple/Phreeze/GenericRouter.php b/portal/patient/fwk/libs/verysimple/Phreeze/GenericRouter.php index 072fa981922..0ebd72d3b1a 100644 --- a/portal/patient/fwk/libs/verysimple/Phreeze/GenericRouter.php +++ b/portal/patient/fwk/libs/verysimple/Phreeze/GenericRouter.php @@ -146,6 +146,15 @@ public function GetRoute($uri = "") } } + if (!empty($GLOBALS['bootstrap_register'])) { + // p_reg check + if ($this->routeMap[$unalteredKey]["p_reg"] !== true) { + // failed p_reg check + $error = 'Unauthorized'; + throw new Exception($error); + } + } + $this->matchedRoute = array ( "key" => $unalteredKey, "route" => $value ["route"], diff --git a/portal/patient/index.php b/portal/patient/index.php index 31e640078e8..d5f0b0c9cdb 100644 --- a/portal/patient/index.php +++ b/portal/patient/index.php @@ -9,9 +9,9 @@ //require_once ("./../verify_session.php"); /* GlobalConfig object contains all configuration information for the app */ -include_once("_global_config.php"); -include_once("_app_config.php"); -@include_once("_machine_config.php"); // This include auth any framework calls +require_once("_global_config.php"); +require_once("_app_config.php"); +require_once("_machine_config.php"); // This include auth any framework calls if (!GlobalConfig::$CONNECTION_SETTING) { throw new Exception('GlobalConfig::$CONNECTION_SETTING is not configured. Are you missing _machine_config.php?'); diff --git a/portal/patient/libs/Controller/PatientController.php b/portal/patient/libs/Controller/PatientController.php index c0f7d689579..0de28ffe970 100644 --- a/portal/patient/libs/Controller/PatientController.php +++ b/portal/patient/libs/Controller/PatientController.php @@ -202,14 +202,33 @@ public function Create() if ($_SESSION['pid'] !== true && $_SESSION['register'] !== true) { throw new Exception('Unauthorized'); } + + if (empty($_SESSION['fnameRegistration']) || empty($_SESSION['lnameRegistration']) || empty($_SESSION['dobRegistration']) || empty($_SESSION['emailRegistration']) || empty($_SESSION['token_id_holder'])) { + throw new Exception('Something went wrong'); + } + + // get new pid + $result = sqlQueryNoLog("select max(`pid`)+1 as `pid` from `patient_data`"); + if (empty($result['pid'])) { + $pidRegistration = 1; + } else { + $pidRegistration = $result['pid']; + } + // store the pid so can use for other registration elements inserted later (such as insurance) + sqlStatementNoLog("UPDATE `verify_email` SET `pid_holder` = ? WHERE `id` = ?", [$pidRegistration , $_SESSION['token_id_holder']]); + $patient = new Patient($this->Phreezer); $patient->Title = $this->SafeGetVal($json, 'title', $patient->Title); $patient->Language = $this->SafeGetVal($json, 'language', $patient->Language); $patient->Financial = $this->SafeGetVal($json, 'financial', $patient->Financial); - $patient->Fname = $this->SafeGetVal($json, 'fname', $patient->Fname); - $patient->Lname = $this->SafeGetVal($json, 'lname', $patient->Lname); - $patient->Mname = $this->SafeGetVal($json, 'mname', $patient->Mname); - $patient->Dob = date('Y-m-d', strtotime($this->SafeGetVal($json, 'dob', $patient->Dob))); + //$patient->Fname = $this->SafeGetVal($json, 'fname', $patient->Fname); + $patient->Fname = $_SESSION['fnameRegistration']; + //$patient->Lname = $this->SafeGetVal($json, 'lname', $patient->Lname); + $patient->Lname = $_SESSION['lnameRegistration']; + //$patient->Mname = $this->SafeGetVal($json, 'mname', $patient->Mname); + $patient->Mname = $_SESSION['mnameRegistration']; + //$patient->Dob = date('Y-m-d', strtotime($this->SafeGetVal($json, 'dob', $patient->Dob))); + $patient->Dob = $_SESSION['dobRegistration']; $patient->Street = $this->SafeGetVal($json, 'street', $patient->Street); $patient->PostalCode = $this->SafeGetVal($json, 'postalCode', $patient->PostalCode); $patient->City = $this->SafeGetVal($json, 'city', $patient->City); @@ -231,8 +250,9 @@ public function Create() $patient->Referrerid = $this->SafeGetVal($json, 'referrerid', $patient->Referrerid); $patient->Providerid = $this->SafeGetVal($json, 'providerid', $patient->Providerid); $patient->RefProviderid = $this->SafeGetVal($json, 'refProviderid', $patient->RefProviderid); - $patient->Email = $this->SafeGetVal($json, 'email', $patient->Email); - $patient->EmailDirect = $this->SafeGetVal($json, 'emailDirect', $patient->EmailDirect); + //$patient->Email = $this->SafeGetVal($json, 'email', $patient->Email); + $patient->Email = $_SESSION['emailRegistration']; + //$patient->EmailDirect = $this->SafeGetVal($json, 'emailDirect', $patient->EmailDirect); $patient->Ethnoracial = $this->SafeGetVal($json, 'ethnoracial', $patient->Ethnoracial); $patient->Race = $this->SafeGetVal($json, 'race', $patient->Race); $patient->Ethnicity = $this->SafeGetVal($json, 'ethnicity', $patient->Ethnicity); @@ -244,8 +264,10 @@ public function Create() //$patient->BillingNote = $this->SafeGetVal($json, 'billingNote', $patient->BillingNote); //$patient->Homeless = $this->SafeGetVal($json, 'homeless', $patient->Homeless); //$patient->FinancialReview = date('Y-m-d H:i:s', strtotime($this->SafeGetVal($json, 'financialReview', $patient->FinancialReview))); - $patient->Pubpid = $this->SafeGetVal($json, 'pubpid', $patient->Pubpid); - $patient->Pid = $this->SafeGetVal($json, 'pid', $patient->Pid); + //$patient->Pubpid = $this->SafeGetVal($json, 'pubpid', $patient->Pubpid); + $patient->Pubpid = $pidRegistration; + //$patient->Pid = $this->SafeGetVal($json, 'pid', $patient->Pid); + $patient->Pid = $pidRegistration; //$patient->Genericname1 = $this->SafeGetVal($json, 'genericname1', $patient->Genericname1); //$patient->Genericval1 = $this->SafeGetVal($json, 'genericval1', $patient->Genericval1); //$patient->Genericname2 = $this->SafeGetVal($json, 'genericname2', $patient->Genericname2); diff --git a/sql/database.sql b/sql/database.sql index 190d1268b92..00a473a30b4 100644 --- a/sql/database.sql +++ b/sql/database.sql @@ -8488,6 +8488,28 @@ CREATE TABLE `uuid_registry` ( -- -------------------------------------------------------- +-- +-- Table structure for table `validate_email` +-- + +DROP TABLE IF EXISTS `verify_email`; +CREATE TABLE `verify_email` ( + `id` bigint NOT NULL auto_increment, + `pid_holder` bigint DEFAULT NULL, + `email` varchar(255) DEFAULT NULL, + `language` varchar(100) DEFAULT NULL, + `fname` varchar(255) DEFAULT NULL, + `mname` varchar(255) DEFAULT NULL, + `lname` varchar(255) DEFAULT NULL, + `dob` date DEFAULT NULL, + `token_onetime` VARCHAR(255) DEFAULT NULL, + `active` tinyint NOT NULL default 1, + PRIMARY KEY (`id`), + UNIQUE KEY (`email`) +) ENGINE=InnoDB; + +-- -------------------------------------------------------- + -- -- Table structure for table `voids` -- diff --git a/sql/patch.sql b/sql/patch.sql index ca9e021792a..60608376e42 100644 --- a/sql/patch.sql +++ b/sql/patch.sql @@ -250,3 +250,21 @@ DROP TABLE IF EXISTS `onsite_activity_view`; UPDATE `layout_options` SET `datacols` = '3' WHERE `form_id` = 'HIS' AND `field_id` = 'usertext11'; UPDATE `layout_options` SET `datacols` = '3' WHERE `form_id` = 'HIS' AND `field_id` = 'exams'; #EndIf + +#IfNotTable verify_email +CREATE TABLE `verify_email` ( +`id` bigint NOT NULL auto_increment, +`pid_holder` bigint DEFAULT NULL, +`email` varchar(255) DEFAULT NULL, +`language` varchar(100) DEFAULT NULL, +`fname` varchar(255) DEFAULT NULL, +`mname` varchar(255) DEFAULT NULL, +`lname` varchar(255) DEFAULT NULL, +`dob` date DEFAULT NULL, +`token_onetime` VARCHAR(255) DEFAULT NULL, +`active` tinyint NOT NULL default 1, +PRIMARY KEY (`id`), +UNIQUE KEY (`email`) +) ENGINE=InnoDB; +#EndIf +