Fix ABDM HIP link-token failure: strictly sanitize abha_number, decouple abha_address from 14-digit abha_id in profile & modal

This commit is contained in:
DevSoftTech Server 2026-09-24 10:03:19 +05:30
parent 24089f36a6
commit 3656bf9b94
6 changed files with 87 additions and 16 deletions

View File

@ -10385,7 +10385,20 @@ class AbdmGateway extends BaseController
->get(1)
->getRowArray();
if (! empty($pat['abha_id'])) {
$body['abha_number'] = trim((string) $pat['abha_id']);
$candidateNum = trim((string) $pat['abha_id']);
if (strpos($candidateNum, '@') === false && strlen(preg_replace('/\D/', '', $candidateNum)) === 14) {
$body['abha_number'] = preg_replace('/\D/', '', $candidateNum);
}
}
}
// Sanitize abha_number: only keep if valid 14-digit numeric ABHA number. Never pass email/abha_address!
if (! empty($body['abha_number'])) {
$cleanedAbhaNum = preg_replace('/\D/', '', (string) $body['abha_number']);
if (strlen($cleanedAbhaNum) === 14 && strpos((string) $body['abha_number'], '@') === false) {
$body['abha_number'] = $cleanedAbhaNum;
} else {
unset($body['abha_number']);
}
}
@ -10434,7 +10447,20 @@ class AbdmGateway extends BaseController
->get(1)
->getRowArray();
if (! empty($pat['abha_id'])) {
$body['abha_number'] = trim((string) $pat['abha_id']);
$candidateNum = trim((string) $pat['abha_id']);
if (strpos($candidateNum, '@') === false && strlen(preg_replace('/\D/', '', $candidateNum)) === 14) {
$body['abha_number'] = preg_replace('/\D/', '', $candidateNum);
}
}
}
// Sanitize abha_number: only keep if valid 14-digit numeric ABHA number. Never pass email/abha_address!
if (! empty($body['abha_number'])) {
$cleanedAbhaNum = preg_replace('/\D/', '', (string) $body['abha_number']);
if (strlen($cleanedAbhaNum) === 14 && strpos((string) $body['abha_number'], '@') === false) {
$body['abha_number'] = $cleanedAbhaNum;
} else {
unset($body['abha_number']);
}
}
@ -10656,7 +10682,7 @@ class AbdmGateway extends BaseController
'year_of_birth' => $yob,
'dob' => $dob,
'abha_address' => $effAbhaAddress,
'abha_number' => trim((string) ($patient['abha_id'] ?? '')),
'abha_number' => (strpos((string) ($patient['abha_id'] ?? ''), '@') === false && strlen(preg_replace('/\D/', '', (string) ($patient['abha_id'] ?? ''))) === 14) ? preg_replace('/\D/', '', (string) $patient['abha_id']) : '',
'phone' => $cleanPhone,
],
'care_contexts' => $contextsList,

View File

@ -1728,6 +1728,15 @@ class EAtriaBridgeConnector implements AbdmConnectorInterface
}
}
if (! empty($payload['abha_number'])) {
$cleaned = preg_replace('/\D/', '', (string) $payload['abha_number']);
if (strlen($cleaned) === 14 && strpos((string) $payload['abha_number'], '@') === false) {
$payload['abha_number'] = $cleaned;
} else {
unset($payload['abha_number']);
}
}
if ($this->hfrId !== '' && empty($payload['hfr_id'])) {
$payload['hfr_id'] = $this->hfrId;
}
@ -1736,6 +1745,15 @@ class EAtriaBridgeConnector implements AbdmConnectorInterface
public function hipLinkCareContext(array $payload): array
{
if (! empty($payload['abha_number'])) {
$cleaned = preg_replace('/\D/', '', (string) $payload['abha_number']);
if (strlen($cleaned) === 14 && strpos((string) $payload['abha_number'], '@') === false) {
$payload['abha_number'] = $cleaned;
} else {
unset($payload['abha_number']);
}
}
if ($this->hfrId !== '' && empty($payload['hfr_id'])) {
$payload['hfr_id'] = $this->hfrId;
}

View File

@ -69,8 +69,11 @@ if ($patientPhotoPath === '') {
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center flex-wrap gap-2">
<h3 class="card-title mb-0">OPD History</h3>
<div class="d-flex align-items-center gap-2">
<button type="button" class="btn btn-outline-primary btn-sm fw-semibold" onclick="openAbdmHipLinkModal(<?= (int)$patient->id ?>, '<?= esc($patientAbhaAddress ?? '') ?>', { name: '<?= esc($patient->p_fname ?? '') ?>', gender: '<?= $patient->gender == 1 ? 'M' : ($patient->gender == 2 ? 'F' : 'O') ?>', yob: '<?= !empty($patient->dob) ? date('Y', strtotime($patient->dob)) : '' ?>', phone: '<?= esc($patient->mphone1 ?? '') ?>', abha_number: '<?= esc($patient->abha_id ?? '') ?>' })">
<?php
$rawAbhaId = trim((string)($patient->abha_id ?? ''));
$validModalAbhaNum = (strpos($rawAbhaId, '@') === false && strlen(preg_replace('/\D/', '', $rawAbhaId)) === 14) ? preg_replace('/\D/', '', $rawAbhaId) : '';
?>
<button type="button" class="btn btn-outline-primary btn-sm fw-semibold" onclick="openAbdmHipLinkModal(<?= (int)$patient->id ?>, '<?= esc($patientAbhaAddress ?? '') ?>', { name: '<?= esc($patient->p_fname ?? '') ?>', gender: '<?= $patient->gender == 1 ? 'M' : ($patient->gender == 2 ? 'F' : 'O') ?>', yob: '<?= !empty($patient->dob) ? date('Y', strtotime($patient->dob)) : '' ?>', phone: '<?= esc($patient->mphone1 ?? '') ?>', abha_number: '<?= esc($validModalAbhaNum) ?>' })">
<i class="bi bi-link-45deg me-1"></i>Link to ABHA (HIP)
</button>
<button type="button" class="btn btn-outline-secondary btn-sm"

View File

@ -15,9 +15,9 @@
<?= csrf_field() ?>
<?php
$patientAbhaId = '';
foreach (['abha_id', 'abha_no', 'abha', 'abha_address'] as $abhaField) {
foreach (['abha_id', 'abha_no', 'abha'] as $abhaField) {
$candidateAbha = trim((string) ($data[0]->{$abhaField} ?? ''));
if ($candidateAbha !== '') {
if ($candidateAbha !== '' && strpos($candidateAbha, '@') === false) {
$patientAbhaId = $candidateAbha;
break;
}
@ -51,9 +51,6 @@
}
}
}
if ($patientAbhaId === '' && $abhaAddress !== '') {
$patientAbhaId = $abhaAddress;
}
$abhaVerifiedStatus = trim((string) ($data[0]->abha_verified_status ?? ''));
$abhaVerificationType = trim((string) ($data[0]->abha_verification_type ?? ''));
$abhaKycVerified = (int) ($data[0]->abha_kyc_verified ?? 0) === 1;
@ -103,7 +100,8 @@
<i class="bi bi-upload me-1"></i>Upload
</button>
</div>
<button type="button" class="btn btn-outline-primary btn-sm w-100 mt-2 fw-semibold" onclick="openAbdmHipLinkModal(<?= (int)$data[0]->id ?>, '<?= esc($abhaAddress) ?>', { name: '<?= esc($data[0]->p_fname ?? '') ?>', gender: '<?= $data[0]->gender == 1 ? 'M' : ($data[0]->gender == 2 ? 'F' : 'O') ?>', yob: '<?= !empty($data[0]->dob) ? date('Y', strtotime($data[0]->dob)) : '' ?>', phone: '<?= esc($data[0]->mphone1 ?? '') ?>', abha_number: '<?= esc($patientAbhaId) ?>' })">
<?php $validAbhaNumForModal = (strpos($patientAbhaId, '@') === false && strlen(preg_replace('/\D/', '', $patientAbhaId)) === 14) ? preg_replace('/\D/', '', $patientAbhaId) : ''; ?>
<button type="button" class="btn btn-outline-primary btn-sm w-100 mt-2 fw-semibold" onclick="openAbdmHipLinkModal(<?= (int)$data[0]->id ?>, '<?= esc($abhaAddress) ?>', { name: '<?= esc($data[0]->p_fname ?? '') ?>', gender: '<?= $data[0]->gender == 1 ? 'M' : ($data[0]->gender == 2 ? 'F' : 'O') ?>', yob: '<?= !empty($data[0]->dob) ? date('Y', strtotime($data[0]->dob)) : '' ?>', phone: '<?= esc($data[0]->mphone1 ?? '') ?>', abha_number: '<?= esc($validAbhaNumForModal) ?>' })">
<i class="bi bi-link-45deg me-1"></i>Link Records to ABHA (HIP)
</button>
<?php if (empty($abhaAddress) && !empty($data[0]->mphone1)) : ?>

View File

@ -573,6 +573,8 @@
}
}
}
$rawPmAbhaId = trim((string) ($patient_master[0]->abha_id ?? ''));
$validPmAbhaNum = (strpos($rawPmAbhaId, '@') === false && strlen(preg_replace('/\D/', '', $rawPmAbhaId)) === 14) ? preg_replace('/\D/', '', $rawPmAbhaId) : '';
?>
<a href="javascript:load_form('<?= esc($historyUrl, 'js') ?>','Consult History');"
class="btn btn-info btn-sm"
@ -596,7 +598,7 @@
<button type="button" class="btn btn-outline-success btn-sm text-start" id="btn_local_clinical_assist" title="Local rule-based support using complaints + vitals">Clinical Assist (Local)</button>
<button type="button" class="btn btn-outline-primary btn-sm text-start" id="btn_ai_full_draft" title="Use complete OPD data to generate draft notes">AI Draft (Full Form)</button>
<button type="button" class="btn btn-outline-primary btn-sm text-start" id="btn_quick_preview_fhir">Preview FHIR JSON</button>
<button type="button" class="btn btn-outline-success btn-sm text-start fw-semibold" onclick="openAbdmHipLinkModal(<?= (int)($patient_master[0]->id ?? 0) ?>, $('#abha_address').val(), { name: '<?= esc($patientName) ?>', gender: '<?= esc($patient_master[0]->gender == 1 ? 'M' : ($patient_master[0]->gender == 2 ? 'F' : 'O')) ?>', yob: '<?= !empty($patient_master[0]->dob) ? date('Y', strtotime($patient_master[0]->dob)) : '' ?>', phone: '<?= esc($patient_master[0]->mphone1 ?? '') ?>', abha_number: '<?= esc($patient_master[0]->abha_id ?? '') ?>' })">
<button type="button" class="btn btn-outline-success btn-sm text-start fw-semibold" onclick="openAbdmHipLinkModal(<?= (int)($patient_master[0]->id ?? 0) ?>, $('#abha_address').val(), { name: '<?= esc($patientName) ?>', gender: '<?= esc($patient_master[0]->gender == 1 ? 'M' : ($patient_master[0]->gender == 2 ? 'F' : 'O')) ?>', yob: '<?= !empty($patient_master[0]->dob) ? date('Y', strtotime($patient_master[0]->dob)) : '' ?>', phone: '<?= esc($patient_master[0]->mphone1 ?? '') ?>', abha_number: '<?= esc($validPmAbhaNum) ?>' })">
<i class="bi bi-link-45deg me-1"></i>Link Records to ABHA (HIP)
</button>
</div>
@ -659,7 +661,7 @@
<?php if ($patientAbhaAddress !== '') : ?>
<div class="rx-meta-line d-flex align-items-center justify-content-between">
<span><strong>ABHA:</strong> <?= esc($patientAbhaAddress) ?></span>
<button type="button" class="btn btn-outline-success btn-xs py-0 px-2" style="font-size:0.75rem;" title="Link Records to ABHA (Method 4)" onclick="openAbdmHipLinkModal(<?= (int)($patient_master[0]->id ?? 0) ?>, $('#abha_address').val(), { name: '<?= esc($patientName) ?>', gender: '<?= esc($patient_master[0]->gender == 1 ? 'M' : ($patient_master[0]->gender == 2 ? 'F' : 'O')) ?>', yob: '<?= !empty($patient_master[0]->dob) ? date('Y', strtotime($patient_master[0]->dob)) : '' ?>', phone: '<?= esc($patient_master[0]->mphone1 ?? '') ?>', abha_number: '<?= esc($patient_master[0]->abha_id ?? '') ?>' })">
<button type="button" class="btn btn-outline-success btn-xs py-0 px-2" style="font-size:0.75rem;" title="Link Records to ABHA (Method 4)" onclick="openAbdmHipLinkModal(<?= (int)($patient_master[0]->id ?? 0) ?>, $('#abha_address').val(), { name: '<?= esc($patientName) ?>', gender: '<?= esc($patient_master[0]->gender == 1 ? 'M' : ($patient_master[0]->gender == 2 ? 'F' : 'O')) ?>', yob: '<?= !empty($patient_master[0]->dob) ? date('Y', strtotime($patient_master[0]->dob)) : '' ?>', phone: '<?= esc($patient_master[0]->mphone1 ?? '') ?>', abha_number: '<?= esc($validPmAbhaNum) ?>' })">
<i class="bi bi-link-45deg"></i> Link
</button>
</div>

View File

@ -388,7 +388,11 @@
if (data.ok && data.patient) {
var p = data.patient;
if (p.abha_address) document.getElementById('hipAbhaAddress').value = p.abha_address;
if (p.abha_number) document.getElementById('hipAbhaNumber').value = p.abha_number;
var pAbhaNum = (p.abha_number || '').trim();
if (pAbhaNum.indexOf('@') !== -1 || pAbhaNum.replace(/\D/g, '').length !== 14) {
pAbhaNum = '';
}
document.getElementById('hipAbhaNumber').value = pAbhaNum;
if (p.name) document.getElementById('hipPatientName').value = p.name;
if (p.gender) document.getElementById('hipGender').value = p.gender;
if (p.year_of_birth) document.getElementById('hipYob').value = p.year_of_birth;
@ -428,7 +432,11 @@
// Reset fields
document.getElementById('hipAbhaAddress').value = abhaAddress || prefill.abha_address || '';
document.getElementById('hipAbhaNumber').value = prefill.abha_number || prefill.abha_id || '';
var rawPrefillNum = (prefill.abha_number || prefill.abha_id || '').trim();
if (rawPrefillNum.indexOf('@') !== -1 || rawPrefillNum.replace(/\D/g, '').length !== 14) {
rawPrefillNum = '';
}
document.getElementById('hipAbhaNumber').value = rawPrefillNum;
document.getElementById('hipPatientName').value = prefill.name || prefill.patient_name || '';
document.getElementById('hipGender').value = prefill.gender || 'M';
document.getElementById('hipYob').value = prefill.year_of_birth || prefill.yob || '';
@ -484,7 +492,11 @@
*/
window.executeHipInitiatedLinking = async function() {
var abhaAddress = document.getElementById('hipAbhaAddress').value.trim();
var abhaNumber = document.getElementById('hipAbhaNumber').value.trim();
var rawAbhaNum = (document.getElementById('hipAbhaNumber').value || '').trim();
var abhaNumber = '';
if (rawAbhaNum.indexOf('@') === -1 && rawAbhaNum.replace(/\D/g, '').length === 14) {
abhaNumber = rawAbhaNum.replace(/\D/g, '');
}
var name = document.getElementById('hipPatientName').value.trim();
var gender = document.getElementById('hipGender').value;
var yobVal = document.getElementById('hipYob').value.trim();
@ -519,6 +531,18 @@
return;
}
var allAlreadyLinked = selectedContexts.length > 0 && selectedContexts.every(function(c) {
var cb = document.querySelector('.care-context-cb[value="' + c.ref.replace(/"/g, '\\"') + '"]');
return cb && cb.closest('.border') && cb.closest('.border').querySelector('.badge.bg-success-subtle');
});
if (allAlreadyLinked) {
alertBox.className = 'alert alert-success py-2 px-3 small';
alertBox.innerHTML = '<strong><i class="bi bi-check-circle-fill me-1"></i>Already Linked!</strong> '
+ 'The selected care context(s) are already linked with this patient\'s ABHA. Patient can view and fetch the records directly in their PHR app.';
alertBox.classList.remove('d-none');
return;
}
var origBtnHtml = submitBtn.innerHTML;
submitBtn.disabled = true;
submitBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Verifying...';