Skip to content

Commit 77cfd47

Browse files
committed
Security Fixes
1 parent 6a00c68 commit 77cfd47

3 files changed

Lines changed: 83 additions & 41 deletions

File tree

app/classes/Services/CommonService.php

Lines changed: 65 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,17 @@ public static function isSessionActive()
4646
* Syncs sub-table data by deleting existing records and inserting new ones.
4747
* Uses dynamic field detection from DDL for future-proofing across different schema versions.
4848
*
49+
* Flow:
50+
* 1. Bail out early if there's nothing to sync.
51+
* 2. DELETE all existing rows for this foreign key (full replace strategy).
52+
* 3. Detect the table's columns from its DDL (excluding auto-increment/internal fields).
53+
* 4. Loop through $remoteData and build an insert row for each entry:
54+
* - If $keyValueMapping is provided, the data is a simple key => value map
55+
* (e.g., risk_factor_id => detected_value), handled by buildKeyValueInsertData().
56+
* - Otherwise, each entry is an associative array of column values,
57+
* handled by buildStandardInsertData().
58+
* 5. Insert each built row into the table.
59+
*
4960
* @param string $tableName Target table name
5061
* @param string $foreignKey Foreign key column name
5162
* @param mixed $foreignKeyValue Foreign key value
@@ -67,40 +78,73 @@ public function syncSubTable(
6778
return;
6879
}
6980

81+
// Step 1: Wipe existing child rows for this foreign key
7082
$this->db->where($foreignKey, $foreignKeyValue);
7183
$this->db->delete($tableName);
7284

85+
// Step 2: Get the table's writable columns (auto-increment/internal fields excluded)
7386
$fields = $this->getTableFieldsAsArray($tableName, $excludeFields);
7487

88+
// Step 3: Re-insert each remote row
7589
foreach ($remoteData as $key => $row) {
90+
// Every row must reference the parent record
7691
$insertData = [$foreignKey => $foreignKeyValue];
7792

78-
if (!empty($keyValueMapping)) {
79-
// Key-value pair mapping (e.g., hepatitis risk factors: [id => detected_value])
80-
if (isset($keyValueMapping['keyField'])) {
81-
$insertData[$keyValueMapping['keyField']] = $key;
82-
}
83-
if (isset($keyValueMapping['valueField'])) {
84-
$insertData[$keyValueMapping['valueField']] = $row;
85-
}
86-
} else {
87-
// Standard array-of-objects mapping
88-
foreach ($fields as $field => $default) {
89-
if ($field === $foreignKey) {
90-
continue;
91-
}
92-
if ($setUpdatedDatetime && $field === 'updated_datetime') {
93-
$insertData[$field] = DateUtility::getCurrentDateTime();
94-
} elseif (isset($row[$field])) {
95-
$insertData[$field] = $row[$field];
96-
}
97-
}
98-
}
93+
// Two data shapes are supported:
94+
// - Key-value map: $remoteData = [risk_factor_id => 'yes', ...]
95+
// - Standard rows: $remoteData = [['col1' => val, 'col2' => val], ...]
96+
$insertData = !empty($keyValueMapping)
97+
? $this->buildKeyValueInsertData($insertData, $keyValueMapping, $key, $row)
98+
: $this->buildStandardInsertData($insertData, $fields, $foreignKey, $row, $setUpdatedDatetime);
9999

100100
$this->db->insert($tableName, $insertData);
101101
}
102102
}
103103

104+
/**
105+
* Builds an insert row for key-value pair data.
106+
*
107+
* Used when $remoteData is a flat map like [risk_factor_id => detected_value].
108+
* $keyValueMapping tells us which columns to put the key and value into:
109+
* ['keyField' => 'risk_factor', 'valueField' => 'detected_value']
110+
*
111+
* Result example: ['sample_id' => 123, 'risk_factor' => 'hep_b', 'detected_value' => 'yes']
112+
*/
113+
private function buildKeyValueInsertData(array $insertData, array $keyValueMapping, mixed $key, mixed $row): array
114+
{
115+
if (isset($keyValueMapping['keyField'])) {
116+
$insertData[$keyValueMapping['keyField']] = $key;
117+
}
118+
if (isset($keyValueMapping['valueField'])) {
119+
$insertData[$keyValueMapping['valueField']] = $row;
120+
}
121+
return $insertData;
122+
}
123+
124+
/**
125+
* Builds an insert row by matching remote data fields to the table's DDL columns.
126+
*
127+
* Iterates over every column the table has (from DDL), and if the remote $row
128+
* contains a value for that column, includes it. Skips the foreign key column
129+
* (already set by the caller) and optionally stamps updated_datetime.
130+
*/
131+
private function buildStandardInsertData(array $insertData, array $fields, string $foreignKey, mixed $row, bool $setUpdatedDatetime): array
132+
{
133+
foreach ($fields as $field => $default) {
134+
if ($field === $foreignKey) {
135+
continue; // Already set by the caller
136+
}
137+
if ($setUpdatedDatetime && $field === 'updated_datetime') {
138+
$insertData[$field] = DateUtility::getCurrentDateTime();
139+
} elseif (isset($row[$field])) {
140+
$insertData[$field] = $row[$field];
141+
}
142+
// Fields not present in $row are intentionally skipped —
143+
// the DB will use its column default.
144+
}
145+
return $insertData;
146+
}
147+
104148

105149
public function getAppVersion($composerFilePath = ROOT_PATH . '/composer.json')
106150
{

app/classes/Services/SystemService.php

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -92,23 +92,21 @@ public function getDateFormat($category = null, $inputFormat = null)
9292
}
9393

9494
if (empty($category)) {
95-
// Return all date formats
9695
return $dateFormatArray;
97-
} elseif ($category == 'php') {
98-
return $dateFormatArray['phpDateFormat'] ?? 'd-m-Y';
99-
} elseif ($category == 'js') {
100-
return $dateFormatArray['jsDateFieldFormat'] ?? 'dd-mm-yy';
101-
} elseif ($category == 'dayjs') {
102-
return $dateFormatArray['dayjsDateFieldFormat'] ?? 'DD-MM-YYYY';
103-
} elseif ($category == 'jsDateRange') {
104-
return $dateFormatArray['jsDateRangeFormat'] ?? 'DD-MM-YYYY';
105-
} elseif ($category == 'jsMask') {
106-
return $dateFormatArray['jsDateFormatMask'] ?? '99-99-9999';
107-
} elseif ($category == 'mysql') {
108-
return $dateFormatArray['mysqlDateFormat'] ?? '%d-%b-%Y';
109-
} else {
110-
return null;
11196
}
97+
98+
$categoryKeyMap = [
99+
'php' => 'phpDateFormat',
100+
'js' => 'jsDateFieldFormat',
101+
'dayjs' => 'dayjsDateFieldFormat',
102+
'jsDateRange' => 'jsDateRangeFormat',
103+
'jsMask' => 'jsDateFormatMask',
104+
'mysql' => 'mysqlDateFormat',
105+
];
106+
107+
return isset($categoryKeyMap[$category])
108+
? $dateFormatArray[$categoryKeyMap[$category]]
109+
: null;
112110
}
113111

114112

public/assets/js/utils.js

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -266,7 +266,7 @@ class Utilities {
266266

267267
// email validation
268268
static validateEmail(email) {
269-
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
269+
const emailRegex = /^[^\s@]+@([^\s@.]+\.)+[^\s@.]+$/;
270270
return emailRegex.test(email);
271271
}
272272

@@ -364,15 +364,15 @@ class Utilities {
364364
if (!str) return '';
365365
return str
366366
// Handle sequences of uppercase letters as single words
367-
.replace(/([A-Z]+)([A-Z][a-z])/g, '$1_$2')
367+
.replace(/([A-Z])([A-Z][a-z])/g, '$1_$2')
368368
// Add an underscore before any uppercase letter followed by lowercase letters
369369
.replace(/([a-z\d])([A-Z])/g, '$1_$2')
370370
// Lowercase the whole string
371371
.toLowerCase()
372372
// Replace spaces and any non-alphanumeric characters (excluding underscores) with underscores
373373
.replace(/[\s\W]+/g, '_')
374374
// Remove leading/trailing underscores
375-
.replace(/^_+|_+$/g, '');
375+
.replace(/^_+/, '').replace(/_+$/, '');
376376
}
377377

378378
static toCamelCase(str) {
@@ -395,7 +395,7 @@ class Utilities {
395395
.replace(/([a-z])([A-Z])/g, '$1-$2')
396396
.replace(/[\s_]+/g, '-')
397397
.toLowerCase()
398-
.replace(/^-+|-+$/g, '');
398+
.replace(/^-+/, '').replace(/-+$/, '');
399399
}
400400

401401
// Capitalize first letter of each word
@@ -414,7 +414,7 @@ class Utilities {
414414
.trim()
415415
.replace(/[^\w\s-]/g, '')
416416
.replace(/[\s_-]+/g, '-')
417-
.replace(/^-+|-+$/g, '');
417+
.replace(/^-+/, '').replace(/-+$/, '');
418418
}
419419

420420
// Truncate string with ellipsis

0 commit comments

Comments
 (0)