|
7 | 7 | use Exception; |
8 | 8 | use flight\database\DatabaseInterface; |
9 | 9 | use flight\database\DatabaseStatementInterface; |
| 10 | +use flight\database\mysqli\MysqliAdapter; |
| 11 | +use flight\database\pdo\PdoAdapter; |
10 | 12 | use JsonSerializable; |
11 | 13 | use mysqli; |
12 | 14 | use PDO; |
@@ -93,15 +95,20 @@ abstract class ActiveRecord extends Base implements JsonSerializable |
93 | 95 | /** |
94 | 96 | * Database connection |
95 | 97 | * |
96 | | - * @var DatabaseInterface |
| 98 | + * @var DatabaseInterface|null |
97 | 99 | */ |
98 | | - protected DatabaseInterface $databaseConnection; |
| 100 | + protected ?DatabaseInterface $databaseConnection; |
99 | 101 |
|
100 | 102 | /** |
101 | 103 | * @var string The table name in database. |
102 | 104 | */ |
103 | 105 | protected string $table; |
104 | 106 |
|
| 107 | + /** |
| 108 | + * @var string The type of database engine |
| 109 | + */ |
| 110 | + protected string $databaseEngineType; |
| 111 | + |
105 | 112 | /** |
106 | 113 | * @var string The primary key of this ActiveRecord, only supports single primary key. |
107 | 114 | */ |
@@ -162,8 +169,12 @@ public function __construct($databaseConnection = null, ?string $table = '', arr |
162 | 169 | $this->transformAndPersistConnection($rawConnection); |
163 | 170 | } elseif ($databaseConnection instanceof DatabaseInterface) { |
164 | 171 | $this->databaseConnection = $databaseConnection; |
| 172 | + } else { |
| 173 | + $this->databaseConnection = null; |
165 | 174 | } |
166 | 175 |
|
| 176 | + $this->databaseEngineType = $this->getDatabaseEngine(); |
| 177 | + |
167 | 178 | if ($table) { |
168 | 179 | $this->table = $table; |
169 | 180 | } |
@@ -193,9 +204,9 @@ public function __call($name, $args) |
193 | 204 | 'operator' => ActiveRecordData::SQL_PARTS[$name], |
194 | 205 | 'target' => implode(', ', $args) |
195 | 206 | ]); |
196 | | - } else if(method_exists($this->databaseConnection, $name) === true) { |
197 | | - return call_user_func_array([ $this->databaseConnection, $name ], $args); |
198 | | - } |
| 207 | + } elseif (method_exists($this->databaseConnection, $name) === true) { |
| 208 | + return call_user_func_array([ $this->databaseConnection, $name ], $args); |
| 209 | + } |
199 | 210 | return $this; |
200 | 211 | } |
201 | 212 |
|
@@ -317,7 +328,7 @@ protected function resetQueryData(): self |
317 | 328 | { |
318 | 329 | $this->params = []; |
319 | 330 | $this->sqlExpressions = []; |
320 | | - $this->join = null; |
| 331 | + $this->join = null; |
321 | 332 | return $this; |
322 | 333 | } |
323 | 334 | /** |
@@ -391,6 +402,23 @@ public function setDatabaseConnection($databaseConnection): void |
391 | 402 | } |
392 | 403 | } |
393 | 404 |
|
| 405 | + /** |
| 406 | + * Returns the type of database engine. Can be one of: mysql, pgsql, sqlite, oci, sqlsrv, odbc, ibm, informix, firebird, 4D, generic. |
| 407 | + * |
| 408 | + * @return string |
| 409 | + */ |
| 410 | + public function getDatabaseEngine(): string |
| 411 | + { |
| 412 | + if ($this->databaseConnection instanceof PdoAdapter || is_subclass_of($this->databaseConnection, PDO::class) === true) { |
| 413 | + // returns value of mysql, pgsql, sqlite, oci, sqlsrv, odbc, ibm, informix, firebird, 4D, generic. |
| 414 | + return $this->databaseConnection->getConnection()->getAttribute(PDO::ATTR_DRIVER_NAME); |
| 415 | + } elseif ($this->databaseConnection instanceof MysqliAdapter) { |
| 416 | + return 'mysql'; |
| 417 | + } else { |
| 418 | + return 'generic'; |
| 419 | + } |
| 420 | + } |
| 421 | + |
394 | 422 | /** |
395 | 423 | * function to find one record and assign in to current object. |
396 | 424 | * @param int|string $id If call this function using this param, will find record by using this id. If not set, just find the first record in database. |
@@ -449,9 +477,14 @@ public function insert(): ActiveRecord |
449 | 477 | } |
450 | 478 |
|
451 | 479 | $value = $this->filterParam($this->dirty); |
| 480 | + |
| 481 | + // escape column names from dirty data |
| 482 | + $columnNames = array_keys($this->dirty); |
| 483 | + $escapedColumnNames = array_map([$this, 'escapeIdentifier'], $columnNames); |
| 484 | + |
452 | 485 | $this->insert = new Expressions([ |
453 | | - 'operator' => 'INSERT INTO ' . $this->table, |
454 | | - 'target' => new WrapExpressions(['target' => array_keys($this->dirty)]) |
| 486 | + 'operator' => 'INSERT INTO ' . $this->escapeIdentifier($this->table), |
| 487 | + 'target' => new WrapExpressions(['target' => $escapedColumnNames]) |
455 | 488 | ]); |
456 | 489 | $this->values = new Expressions(['operator' => 'VALUES', 'target' => new WrapExpressions(['target' => $value])]); |
457 | 490 |
|
@@ -622,9 +655,9 @@ protected function &getRelation(string $name) |
622 | 655 | protected function buildSqlCallback(string $sql_statement, ActiveRecord $object): string |
623 | 656 | { |
624 | 657 | if ('select' === $sql_statement && null == $object->$sql_statement) { |
625 | | - $sql_statement = strtoupper($sql_statement) . ' ' . $object->table . '.*'; |
| 658 | + $sql_statement = strtoupper($sql_statement) . ' ' . $this->escapeIdentifier($object->table) . '.*'; |
626 | 659 | } elseif (('update' === $sql_statement || 'from' === $sql_statement) && null == $object->$sql_statement) { |
627 | | - $sql_statement = strtoupper($sql_statement) . ' ' . $object->table; |
| 660 | + $sql_statement = strtoupper($sql_statement) . ' ' . $this->escapeIdentifier($object->table); |
628 | 661 | } elseif ('delete' === $sql_statement) { |
629 | 662 | $sql_statement = strtoupper($sql_statement) . ' '; |
630 | 663 | } else { |
@@ -723,7 +756,7 @@ public function addCondition(string $field, string $operator, $value, string $de |
723 | 756 | // skip adding the `table.` prefix if it's already there or a function is being supplied. |
724 | 757 | $skip_table_prefix = (strpos($field, '.') !== false || strpos($field, '(') !== false); |
725 | 758 | $expressions = new Expressions([ |
726 | | - 'source' => ('where' === $name && $skip_table_prefix === false ? $this->table . '.' : '') . $field, |
| 759 | + 'source' => ('where' === $name && $skip_table_prefix === false ? $this->escapeIdentifier($this->table) . '.' : '') . $this->escapeIdentifier($field), |
727 | 760 | 'operator' => $operator, |
728 | 761 | 'target' => ( |
729 | 762 | is_array($value) |
@@ -816,6 +849,28 @@ protected function processEvent($event, array $data_to_pass = []) |
816 | 849 | } |
817 | 850 | } |
818 | 851 |
|
| 852 | + |
| 853 | + /** |
| 854 | + * Escapes a database identifier (e.g., table or column name) to prevent SQL injection. |
| 855 | + * |
| 856 | + * @param string $name The database identifier to be escaped. |
| 857 | + * @return string The escaped database identifier. |
| 858 | + */ |
| 859 | + public function escapeIdentifier(string $name) |
| 860 | + { |
| 861 | + switch ($this->databaseEngineType) { |
| 862 | + case 'sqlite': |
| 863 | + case 'pgsql': |
| 864 | + return '"' . $name . '"'; |
| 865 | + case 'mysql': |
| 866 | + return '`' . $name . '`'; |
| 867 | + case 'sqlsrv': |
| 868 | + return '[' . $name . ']'; |
| 869 | + default: |
| 870 | + return $name; |
| 871 | + } |
| 872 | + } |
| 873 | + |
819 | 874 | /** |
820 | 875 | * @inheritDoc |
821 | 876 | */ |
|
0 commit comments