Skip to content

Commit 6cbbf60

Browse files
authored
fix: handling binary data for prepared statement (#9337)
* fix prepare statement sqlite * fix prepare statement postgre * fix prepare statement sqlsrv * fix prepare statement oci8 * tests * abstract isBinary() method * fix prepare statement mysqli * fix prepare statement oci8 * sqlsrv blob support * fix tests * add changelog * fix rector * make sqlsrv happy * make oci8 happy - hopefully * add a note about options for prepared statement * ignore PreparedQueryTest.php file * apply code suggestion for oci8
1 parent cc1b8f2 commit 6cbbf60

File tree

17 files changed

+124
-17
lines changed

17 files changed

+124
-17
lines changed

.php-cs-fixer.tests.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
'_support/View/Cells/multiplier.php',
2727
'_support/View/Cells/colors.php',
2828
'_support/View/Cells/addition.php',
29+
'system/Database/Live/PreparedQueryTest.php',
2930
])
3031
->notName('#Foobar.php$#');
3132

system/Database/BasePreparedQuery.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,4 +259,12 @@ public function getErrorMessage(): string
259259
{
260260
return $this->errorString;
261261
}
262+
263+
/**
264+
* Whether the input contain binary data.
265+
*/
266+
protected function isBinary(string $input): bool
267+
{
268+
return mb_detect_encoding($input, 'UTF-8', true) === false;
269+
}
262270
}

system/Database/MySQLi/PreparedQuery.php

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,15 +66,19 @@ public function _execute(array $data): bool
6666
throw new BadMethodCallException('You must call prepare before trying to execute a prepared statement.');
6767
}
6868

69-
// First off -bind the parameters
70-
$bindTypes = '';
69+
// First off - bind the parameters
70+
$bindTypes = '';
71+
$binaryData = [];
7172

7273
// Determine the type string
73-
foreach ($data as $item) {
74+
foreach ($data as $key => $item) {
7475
if (is_int($item)) {
7576
$bindTypes .= 'i';
7677
} elseif (is_numeric($item)) {
7778
$bindTypes .= 'd';
79+
} elseif (is_string($item) && $this->isBinary($item)) {
80+
$bindTypes .= 'b';
81+
$binaryData[$key] = $item;
7882
} else {
7983
$bindTypes .= 's';
8084
}
@@ -83,6 +87,11 @@ public function _execute(array $data): bool
8387
// Bind it
8488
$this->statement->bind_param($bindTypes, ...$data);
8589

90+
// Stream binary data
91+
foreach ($binaryData as $key => $value) {
92+
$this->statement->send_long_data($key, $value);
93+
}
94+
8695
try {
8796
return $this->statement->execute();
8897
} catch (mysqli_sql_exception $e) {

system/Database/OCI8/PreparedQuery.php

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
use BadMethodCallException;
1717
use CodeIgniter\Database\BasePreparedQuery;
1818
use CodeIgniter\Database\Exceptions\DatabaseException;
19+
use OCILob;
1920

2021
/**
2122
* Prepared query for OCI8
@@ -73,12 +74,24 @@ public function _execute(array $data): bool
7374
throw new BadMethodCallException('You must call prepare before trying to execute a prepared statement.');
7475
}
7576

77+
$binaryData = null;
78+
7679
foreach (array_keys($data) as $key) {
77-
oci_bind_by_name($this->statement, ':' . $key, $data[$key]);
80+
if (is_string($data[$key]) && $this->isBinary($data[$key])) {
81+
$binaryData = oci_new_descriptor($this->db->connID, OCI_D_LOB);
82+
$binaryData->writeTemporary($data[$key], OCI_TEMP_BLOB);
83+
oci_bind_by_name($this->statement, ':' . $key, $binaryData, -1, OCI_B_BLOB);
84+
} else {
85+
oci_bind_by_name($this->statement, ':' . $key, $data[$key]);
86+
}
7887
}
7988

8089
$result = oci_execute($this->statement, $this->db->commitMode);
8190

91+
if ($binaryData instanceof OCILob) {
92+
$binaryData->free();
93+
}
94+
8295
if ($result && $this->lastInsertTableName !== '') {
8396
$this->db->lastInsertedTableName = $this->lastInsertTableName;
8497
}

system/Database/Postgre/Forge.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,10 @@ protected function _attributeType(array &$attributes)
173173
$attributes['TYPE'] = 'TIMESTAMP';
174174
break;
175175

176+
case 'BLOB':
177+
$attributes['TYPE'] = 'BYTEA';
178+
break;
179+
176180
default:
177181
break;
178182
}

system/Database/Postgre/PreparedQuery.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,12 @@ public function _execute(array $data): bool
8787
throw new BadMethodCallException('You must call prepare before trying to execute a prepared statement.');
8888
}
8989

90+
foreach ($data as &$item) {
91+
if (is_string($item) && $this->isBinary($item)) {
92+
$item = pg_escape_bytea($this->db->connID, $item);
93+
}
94+
}
95+
9096
$this->result = pg_execute($this->db->connID, $this->name, $data);
9197

9298
return (bool) $this->result;

system/Database/SQLSRV/Connection.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -368,7 +368,11 @@ protected function _fieldData(string $table): array
368368

369369
$retVal[$i]->max_length = $query[$i]->CHARACTER_MAXIMUM_LENGTH > 0
370370
? $query[$i]->CHARACTER_MAXIMUM_LENGTH
371-
: $query[$i]->NUMERIC_PRECISION;
371+
: (
372+
$query[$i]->CHARACTER_MAXIMUM_LENGTH === -1
373+
? 'max'
374+
: $query[$i]->NUMERIC_PRECISION
375+
);
372376

373377
$retVal[$i]->nullable = $query[$i]->IS_NULLABLE !== 'NO';
374378
$retVal[$i]->default = $query[$i]->COLUMN_DEFAULT;

system/Database/SQLSRV/Forge.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -397,6 +397,11 @@ protected function _attributeType(array &$attributes)
397397
$attributes['TYPE'] = 'BIT';
398398
break;
399399

400+
case 'BLOB':
401+
$attributes['TYPE'] = 'VARBINARY';
402+
$attributes['CONSTRAINT'] ??= 'MAX';
403+
break;
404+
400405
default:
401406
break;
402407
}

system/Database/SQLSRV/PreparedQuery.php

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ public function _prepare(string $sql, array $options = []): PreparedQuery
5959
// Prepare parameters for the query
6060
$queryString = $this->getQueryString();
6161

62-
$parameters = $this->parameterize($queryString);
62+
$parameters = $this->parameterize($queryString, $options);
6363

6464
// Prepare the query
6565
$this->statement = sqlsrv_prepare($this->db->connID, $sql, $parameters);
@@ -120,16 +120,22 @@ protected function _close(): bool
120120

121121
/**
122122
* Handle parameters.
123+
*
124+
* @param array<int, mixed> $options
123125
*/
124-
protected function parameterize(string $queryString): array
126+
protected function parameterize(string $queryString, array $options): array
125127
{
126128
$numberOfVariables = substr_count($queryString, '?');
127129

128130
$params = [];
129131

130132
for ($c = 0; $c < $numberOfVariables; $c++) {
131133
$this->parameters[$c] = null;
132-
$params[] = &$this->parameters[$c];
134+
if (isset($options[$c])) {
135+
$params[] = [&$this->parameters[$c], SQLSRV_PARAM_IN, $options[$c]];
136+
} else {
137+
$params[] = &$this->parameters[$c];
138+
}
133139
}
134140

135141
return $params;

system/Database/SQLite3/PreparedQuery.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,8 @@ public function _execute(array $data): bool
7575
$bindType = SQLITE3_INTEGER;
7676
} elseif (is_float($item)) {
7777
$bindType = SQLITE3_FLOAT;
78+
} elseif (is_string($item) && $this->isBinary($item)) {
79+
$bindType = SQLITE3_BLOB;
7880
} else {
7981
$bindType = SQLITE3_TEXT;
8082
}

0 commit comments

Comments
 (0)