Skip to content

Commit 2fd1194

Browse files
authored
Merge pull request #55 from wgevaert/improve-name-escape
Be less strict about escaping.
2 parents 90abe78 + 04bb158 commit 2fd1194

File tree

6 files changed

+51
-62
lines changed

6 files changed

+51
-62
lines changed

src/Traits/ErrorTrait.php

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@
3535
use function is_resource;
3636
use function is_string;
3737
use function key;
38-
use function preg_match;
3938
use function strpos;
4039
use TypeError;
4140

@@ -110,11 +109,8 @@ private static function assertValidName(string $name): void
110109
throw new InvalidArgumentException("A name cannot be an empty string");
111110
}
112111

113-
if (!preg_match('/^\p{L}[\p{L}\d_]*$/u', $name)) {
114-
throw new InvalidArgumentException('A name can only contain alphanumeric characters and underscores and must begin with an alphabetic character');
115-
}
116-
117112
if (strlen($name) >= 65535) {
113+
// Remark: Actual limit depends on Neo4j version, but we just take the lower bound.
118114
throw new InvalidArgumentException('A name cannot be longer than 65534 characters');
119115
}
120116
}

src/Traits/EscapeTrait.php

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@
2121

2222
namespace WikibaseSolutions\CypherDSL\Traits;
2323

24-
use InvalidArgumentException;
24+
use function preg_match;
25+
use function sprintf;
26+
use function str_replace;
2527

2628
/**
2729
* Trait for encoding certain structures that are used in multiple clauses in a
@@ -30,25 +32,31 @@
3032
trait EscapeTrait
3133
{
3234
/**
33-
* Escapes the given 'name'. A name is an unquoted literal in a Cypher query, such as variables,
34-
* types or property names.
35+
* Escapes a 'name' if it needs to be escaped.
36+
* @see https://neo4j.com/docs/cypher-manual/4.4/syntax/naming
37+
* A 'name' in cypher is any string that should be included directly in a cypher query,
38+
* such as variable names, labels, property names and relation types
3539
*
3640
* @param string $name
3741
* @return string
3842
*/
3943
public static function escape(string $name): string
4044
{
41-
if ($name === "") {
42-
return "";
43-
}
44-
45-
if (ctype_alnum($name) && !ctype_digit($name)) {
45+
if (preg_match('/^\p{L}[\p{L}\d_]*$/u', $name)) {
4646
return $name;
4747
}
4848

49-
if (strpos($name, '`') !== false) {
50-
throw new InvalidArgumentException("A name must not contain a backtick (`)");
51-
}
49+
return self::escapeRaw($name);
50+
}
51+
52+
/**
53+
* Escapes the given $name to be used directly in a CYPHER query.
54+
* Note: according to https://github.com/neo4j/neo4j/issues/12901 backslashes might give problems in some Neo4j versions.
55+
*/
56+
public static function escapeRaw($name)
57+
{
58+
// Escape backticks that are included in $name by doubling them.
59+
$name = str_replace('`', '``', $name);
5260

5361
return sprintf("`%s`", $name);
5462
}

tests/Unit/ParameterTest.php

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -68,10 +68,7 @@ public function provideThrowsExceptionOnInvalidData(): array
6868
{
6969
return [
7070
[""],
71-
["@"],
72-
["!"],
73-
["-"],
74-
[''],
71+
[str_repeat('a', 65535)],
7572
];
7673
}
7774
}

tests/Unit/Patterns/NodeTest.php

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121

2222
namespace WikibaseSolutions\CypherDSL\Tests\Unit\Patterns;
2323

24-
use InvalidArgumentException;
2524
use PHPUnit\Framework\TestCase;
2625
use WikibaseSolutions\CypherDSL\ExpressionList;
2726
use WikibaseSolutions\CypherDSL\Literals\Decimal;
@@ -49,13 +48,13 @@ public function testEmptyNode(): void
4948
$this->assertSame($name, $node->getVariable());
5049
}
5150

52-
public function testBacktickThrowsException(): void
51+
// Further tests can be found in Traits/EscapeTraitTest
52+
public function testBacktickIsEscaped(): void
5353
{
5454
$node = new Node();
5555

56-
$this->expectException(InvalidArgumentException::class);
57-
$this->expectDeprecationMessage('A name can only contain alphanumeric characters and underscores');
5856
$node->named('abcdr`eer');
57+
$this->assertEquals('(`abcdr``eer`)', $node->toQuery());
5958
}
6059

6160
/**

tests/Unit/QueryTest.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -762,7 +762,7 @@ public function testWikiExamples(): void
762762
->returning([$tom, $tomHanksMovies])
763763
->build();
764764

765-
$this->assertSame("MATCH (tom:Person {name: 'Tom Hanks'})-[:`ACTED_IN`]->(tomHanksMovies) RETURN tom, tomHanksMovies", $statement);
765+
$this->assertSame("MATCH (tom:Person {name: 'Tom Hanks'})-[:ACTED_IN]->(tomHanksMovies) RETURN tom, tomHanksMovies", $statement);
766766

767767
$cloudAtlas = Query::variable("cloudAtlas");
768768
$cloudAtlasNode = Query::node()
@@ -795,7 +795,7 @@ public function testWikiExamples(): void
795795
->returning($coActors->property("name"))
796796
->build();
797797

798-
$this->assertSame("MATCH (tom:Person {name: 'Tom Hanks'})-[:`ACTED_IN`]->(m)<-[:`ACTED_IN`]-(coActors) RETURN coActors.name", $statement);
798+
$this->assertSame("MATCH (tom:Person {name: 'Tom Hanks'})-[:ACTED_IN]->(m)<-[:ACTED_IN]-(coActors) RETURN coActors.name", $statement);
799799

800800
/*
801801
* @see https://gitlab.wikibase.nl/community/libraries/php-cypher-dsl/-/wikis/Usage/Clauses/CALL-procedure-clause

tests/Unit/Traits/EscapeTraitTest.php

Lines changed: 25 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121

2222
namespace WikibaseSolutions\CypherDSL\Tests\Unit\Traits;
2323

24-
use InvalidArgumentException;
2524
use PHPUnit\Framework\MockObject\MockObject;
2625
use PHPUnit\Framework\TestCase;
2726
use WikibaseSolutions\CypherDSL\Traits\EscapeTrait;
@@ -66,58 +65,29 @@ public function testUnsafeValueIsEscaped(string $value)
6665
$this->assertSame($expected, $actual);
6766
}
6867

69-
public function testValueWithBacktickThrowsException()
70-
{
71-
$this->expectException(InvalidArgumentException::class);
72-
73-
$this->trait->escape("foo`bar");
74-
}
75-
7668
public function provideSafeValueIsNotEscapedData(): array
7769
{
7870
return [
7971
['foobar'],
8072
['fooBar'],
8173
['FOOBAR'],
74+
['foo_bar'],
75+
['FOO_BAR'],
8276
['aaa'],
83-
['bbb'],
84-
['ccc'],
85-
['ddd'],
86-
['eee'],
87-
['fff'],
88-
['ggg'],
89-
['hhh'],
90-
['iii'],
91-
['jjj'],
92-
['kkk'],
93-
['lll'],
94-
['mmm'],
95-
['nnn'],
96-
['ooo'],
97-
['ppp'],
98-
['qqq'],
99-
['rrr'],
100-
['sss'],
101-
['ttt'],
102-
['uuu'],
103-
['vvv'],
104-
['www'],
105-
['xxx'],
106-
['yyy'],
107-
['zzz'],
108-
[''],
10977
['aaa100'],
11078
['a0'],
11179
['z10'],
11280
['z99'],
81+
['ça'],
82+
[''],
11383
];
11484
}
11585

11686
public function provideUnsafeValueIsEscapedData(): array
11787
{
11888
return [
119-
['foo_bar'],
120-
['FOO_BAR'],
89+
[''],
90+
['__FooBar__'],
12191
['_'],
12292
['__'],
12393
['\''],
@@ -129,4 +99,23 @@ public function provideUnsafeValueIsEscapedData(): array
12999
['2'],
130100
];
131101
}
102+
103+
/**
104+
* @dataProvider provideValueWithBacktickIsProperlyEscapedData
105+
*/
106+
public function testValueWithBacktickIsProperlyEscaped($input, $expected)
107+
{
108+
$this->assertSame('`foo``bar`', $this->trait->escape("foo`bar"));
109+
}
110+
111+
public function provideValueWithBacktickIsProperlyEscapedData(): array
112+
{
113+
return [
114+
['foo`bar','`foo``bar`'],
115+
['`foo','```foo`'],
116+
['foo`','`foo```'],
117+
['foo``bar','`foo````bar`'],
118+
['`foo`','```foo```'],
119+
];
120+
}
132121
}

0 commit comments

Comments
 (0)