Skip to content

Commit 14399d8

Browse files
committed
backport: [10.x] Escaping functionality within the Grammar
1 parent 387779b commit 14399d8

File tree

9 files changed

+374
-47
lines changed

9 files changed

+374
-47
lines changed

src/Backports/ConnectionBackport.php

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tpetry\PostgresqlEnhanced\Backports;
6+
7+
use Closure;
8+
use RuntimeException;
9+
use Tpetry\PostgresqlEnhanced\Query\Grammar as QueryGrammar;
10+
use Tpetry\PostgresqlEnhanced\Schema\Grammars\Grammar as SchemaGrammar;
11+
12+
/**
13+
* To support some features these commits from laravel needed to be backported for older versions:
14+
* - [8.x] Adds new RefreshDatabaseLazily testing trait (https://github.com/laravel/framework/commit/3d1ead403e05ee0b9aa93a6ff720704970aec9c8).
15+
* - [10.x] Escaping functionality within the Grammar (https://github.com/laravel/framework/commit/e953137280cdf6e0fe3c3e4c49d7209ad86c92c0).
16+
*/
17+
trait ConnectionBackport
18+
{
19+
/**
20+
* All of the callbacks that should be invoked before a query is executed.
21+
*
22+
* @var Closure[]
23+
*/
24+
protected $beforeExecutingCallbacks = [];
25+
26+
/**
27+
* Register a hook to be run just before a database query is executed.
28+
*/
29+
public function beforeExecuting(Closure $callback): static
30+
{
31+
$this->beforeExecutingCallbacks[] = $callback;
32+
33+
return $this;
34+
}
35+
36+
/**
37+
* Escape a value for safe SQL embedding.
38+
*
39+
* @param string|float|int|bool|null $value
40+
* @param bool $binary
41+
*/
42+
public function escape($value, $binary = false): string
43+
{
44+
if (null === $value) {
45+
return 'null';
46+
} elseif ($binary) {
47+
return $this->escapeBinary($value);
48+
} elseif (\is_int($value) || \is_float($value)) {
49+
return (string) $value;
50+
} elseif (\is_bool($value)) {
51+
return $this->escapeBool($value);
52+
} else {
53+
if (str_contains($value, "\00")) {
54+
throw new RuntimeException('Strings with null bytes cannot be escaped. Use the binary escape option.');
55+
}
56+
if (false === preg_match('//u', $value)) {
57+
throw new RuntimeException('Strings with invalid UTF-8 byte sequences cannot be escaped.');
58+
}
59+
60+
return $this->escapeString($value);
61+
}
62+
}
63+
64+
/**
65+
* Escape a binary value for safe SQL embedding.
66+
*
67+
* @param string $value
68+
*/
69+
protected function escapeBinary($value): string
70+
{
71+
$hex = bin2hex($value);
72+
73+
return "'\x{$hex}'::bytea";
74+
}
75+
76+
/**
77+
* Escape a boolean value for safe SQL embedding.
78+
*
79+
* @param bool $value
80+
*/
81+
protected function escapeBool($value): string
82+
{
83+
return $value ? 'true' : 'false';
84+
}
85+
86+
/**
87+
* Escape a string value for safe SQL embedding.
88+
*
89+
* @param string $value
90+
*/
91+
protected function escapeString($value): string
92+
{
93+
return $this->getPdo()->quote($value);
94+
}
95+
96+
/**
97+
* Get the default query grammar instance.
98+
*/
99+
protected function getDefaultQueryGrammar(): QueryGrammar
100+
{
101+
($grammar = new QueryGrammar())->setConnection($this);
102+
103+
return $this->withTablePrefix($grammar);
104+
}
105+
106+
/**
107+
* Get the default schema grammar instance.
108+
*/
109+
protected function getDefaultSchemaGrammar(): SchemaGrammar
110+
{
111+
($grammar = new SchemaGrammar())->setConnection($this);
112+
113+
return $this->withTablePrefix($grammar);
114+
}
115+
116+
/**
117+
* Run a SQL statement and log its execution context.
118+
*
119+
* @param string $query
120+
* @param array $bindings
121+
*/
122+
protected function run($query, $bindings, Closure $callback): mixed
123+
{
124+
foreach ($this->beforeExecutingCallbacks as $beforeExecutingCallback) {
125+
$beforeExecutingCallback($query, $bindings, $this);
126+
}
127+
128+
return parent::run($query, $bindings, $callback);
129+
}
130+
}

src/Backports/GrammarBackport.php

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tpetry\PostgresqlEnhanced\Backports;
6+
7+
use RuntimeException;
8+
9+
/**
10+
* To support some features these commits from laravel needed to be backported for older versions:
11+
* - [10.x] Escaping functionality within the Grammar (https://github.com/laravel/framework/commit/e953137280cdf6e0fe3c3e4c49d7209ad86c92c0).
12+
*/
13+
trait GrammarBackport
14+
{
15+
/**
16+
* The connection used for escaping values.
17+
*
18+
* @var \Illuminate\Database\Connection
19+
*/
20+
protected $connection;
21+
22+
/**
23+
* Escapes a value for safe SQL embedding.
24+
*
25+
* @param string|float|int|bool|null $value
26+
* @param bool $binary
27+
*/
28+
public function escape($value, $binary = false): string
29+
{
30+
if (null === $this->connection) {
31+
throw new RuntimeException("The database driver's grammar implementation does not support escaping values.");
32+
}
33+
34+
return $this->connection->escape($value, $binary);
35+
}
36+
37+
/**
38+
* Set the grammar's database connection.
39+
*
40+
* @param \Illuminate\Database\Connection $connection
41+
*/
42+
public function setConnection($connection): static
43+
{
44+
$this->connection = $connection;
45+
46+
return $this;
47+
}
48+
}

src/PostgresConnectionBackport.php

Lines changed: 0 additions & 46 deletions
This file was deleted.

src/PostgresEnhancedConnection.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
use Illuminate\Database\QueryException;
1111
use Illuminate\Support\Str;
1212
use Throwable;
13+
use Tpetry\PostgresqlEnhanced\Backports\ConnectionBackport;
1314
use Tpetry\PostgresqlEnhanced\Query\Builder as QueryBuilder;
1415
use Tpetry\PostgresqlEnhanced\Query\Grammar as QueryGrammar;
1516
use Tpetry\PostgresqlEnhanced\Schema\Builder as SchemaBuilder;
@@ -18,7 +19,7 @@
1819

1920
class PostgresEnhancedConnection extends PostgresConnection
2021
{
21-
use PostgresConnectionBackport;
22+
use ConnectionBackport;
2223

2324
/**
2425
* Additional bindings which will be used in run().

src/Query/Grammar.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@
66

77
use Illuminate\Database\Query\Builder as BaseBuilder;
88
use Illuminate\Database\Query\Grammars\PostgresGrammar;
9+
use Tpetry\PostgresqlEnhanced\Backports\GrammarBackport;
910

1011
class Grammar extends PostgresGrammar
1112
{
13+
use GrammarBackport;
1214
use GrammarCte;
1315
use GrammarFullText;
1416
use GrammarOrder;

src/Schema/Grammars/Grammar.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@
55
namespace Tpetry\PostgresqlEnhanced\Schema\Grammars;
66

77
use Illuminate\Database\Schema\Grammars\PostgresGrammar;
8+
use Tpetry\PostgresqlEnhanced\Backports\GrammarBackport;
89

910
class Grammar extends PostgresGrammar
1011
{
12+
use GrammarBackport;
1113
use GrammarIndex;
1214
use GrammarTable;
1315
use GrammarTrigger;

tests/Connection/EscapeTest.php

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tpetry\PostgresqlEnhanced\Tests\Connection;
6+
7+
use RuntimeException;
8+
use Tpetry\PostgresqlEnhanced\Tests\TestCase;
9+
10+
class EscapeTest extends TestCase
11+
{
12+
public function testEscapeBinary(): void
13+
{
14+
$this->assertSame("'\\xdead00beef'::bytea", $this->getConnection()->escape(hex2bin('dead00beef'), true));
15+
}
16+
17+
public function testEscapeBool(): void
18+
{
19+
$this->assertSame('true', $this->getConnection()->escape(true));
20+
$this->assertSame('false', $this->getConnection()->escape(false));
21+
}
22+
23+
public function testEscapeFloat(): void
24+
{
25+
$this->assertSame('3.14159', $this->getConnection()->escape(3.14159));
26+
$this->assertSame('-3.14159', $this->getConnection()->escape(-3.14159));
27+
}
28+
29+
public function testEscapeInt(): void
30+
{
31+
$this->assertSame('42', $this->getConnection()->escape(42));
32+
$this->assertSame('-6', $this->getConnection()->escape(-6));
33+
}
34+
35+
public function testEscapeNull(): void
36+
{
37+
$this->assertSame('null', $this->getConnection()->escape(null));
38+
$this->assertSame('null', $this->getConnection()->escape(null, true));
39+
}
40+
41+
public function testEscapeString(): void
42+
{
43+
$this->assertSame("'2147483647'", $this->getConnection()->escape('2147483647'));
44+
$this->assertSame("'true'", $this->getConnection()->escape('true'));
45+
$this->assertSame("'false'", $this->getConnection()->escape('false'));
46+
$this->assertSame("'null'", $this->getConnection()->escape('null'));
47+
$this->assertSame("'Hello''World'", $this->getConnection()->escape("Hello'World"));
48+
}
49+
50+
public function testEscapeStringInvalidUtf8(): void
51+
{
52+
$this->expectException(RuntimeException::class);
53+
$this->getConnection()->escape("I am hiding an invalid \x80 utf-8 continuation byte");
54+
}
55+
56+
public function testEscapeStringNullByte(): void
57+
{
58+
$this->expectException(RuntimeException::class);
59+
$this->getConnection()->escape("I am hiding a \00 byte");
60+
}
61+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tpetry\PostgresqlEnhanced\Tests\Connection;
6+
7+
use RuntimeException;
8+
use Tpetry\PostgresqlEnhanced\Tests\TestCase;
9+
10+
class QueryGrammarEscapeTest extends TestCase
11+
{
12+
public function testEscapeBinary(): void
13+
{
14+
$this->assertSame("'\\xdead00beef'::bytea", $this->getConnection()->getQueryGrammar()->escape(hex2bin('dead00beef'), true));
15+
}
16+
17+
public function testEscapeBool(): void
18+
{
19+
$this->assertSame('true', $this->getConnection()->getQueryGrammar()->escape(true));
20+
$this->assertSame('false', $this->getConnection()->getQueryGrammar()->escape(false));
21+
}
22+
23+
public function testEscapeFloat(): void
24+
{
25+
$this->assertSame('3.14159', $this->getConnection()->getQueryGrammar()->escape(3.14159));
26+
$this->assertSame('-3.14159', $this->getConnection()->getQueryGrammar()->escape(-3.14159));
27+
}
28+
29+
public function testEscapeInt(): void
30+
{
31+
$this->assertSame('42', $this->getConnection()->getQueryGrammar()->escape(42));
32+
$this->assertSame('-6', $this->getConnection()->getQueryGrammar()->escape(-6));
33+
}
34+
35+
public function testEscapeNull(): void
36+
{
37+
$this->assertSame('null', $this->getConnection()->getQueryGrammar()->escape(null));
38+
$this->assertSame('null', $this->getConnection()->getQueryGrammar()->escape(null, true));
39+
}
40+
41+
public function testEscapeString(): void
42+
{
43+
$this->assertSame("'2147483647'", $this->getConnection()->getQueryGrammar()->escape('2147483647'));
44+
$this->assertSame("'true'", $this->getConnection()->getQueryGrammar()->escape('true'));
45+
$this->assertSame("'false'", $this->getConnection()->getQueryGrammar()->escape('false'));
46+
$this->assertSame("'null'", $this->getConnection()->getQueryGrammar()->escape('null'));
47+
$this->assertSame("'Hello''World'", $this->getConnection()->getQueryGrammar()->escape("Hello'World"));
48+
}
49+
50+
public function testEscapeStringInvalidUtf8(): void
51+
{
52+
$this->expectException(RuntimeException::class);
53+
$this->getConnection()->getQueryGrammar()->escape("I am hiding an invalid \x80 utf-8 continuation byte");
54+
}
55+
56+
public function testEscapeStringNullByte(): void
57+
{
58+
$this->expectException(RuntimeException::class);
59+
$this->getConnection()->getQueryGrammar()->escape("I am hiding a \00 byte");
60+
}
61+
}

0 commit comments

Comments
 (0)