Skip to content

Commit 434cc56

Browse files
author
Wout Gevaert
committed
Be less strict about escaping.
1 parent abdf69c commit 434cc56

File tree

6 files changed

+32
-55
lines changed

6 files changed

+32
-55
lines changed

src/Traits/ErrorTrait.php

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -110,11 +110,8 @@ private static function assertValidName(string $name): void
110110
throw new InvalidArgumentException("A name cannot be an empty string");
111111
}
112112

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-
117113
if (strlen($name) >= 65535) {
114+
// Remark: Actual limit depends on Neo4j version, but we just take the lower bound.
118115
throw new InvalidArgumentException('A name cannot be longer than 65534 characters');
119116
}
120117
}

src/Traits/EscapeTrait.php

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -30,25 +30,33 @@
3030
trait EscapeTrait
3131
{
3232
/**
33-
* Escapes the given 'name'. A name is an unquoted literal in a Cypher query, such as variables,
34-
* types or property names.
33+
* Escapes a 'name' if it needs to be escaped.
34+
* A 'name' in cypher is any string that should be included directly in a cypher query,
35+
* such as variable names, labels, property names and relation types
36+
*
37+
* Note that empty strings are usually not allowed as names, so these should not be passed to this function.
38+
* However, some neo4j versions do not crash on empty string as variable name, so let's just escape them anyways.
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 ($name !== '' && 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: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,13 +49,12 @@ public function testEmptyNode(): void
4949
$this->assertSame($name, $node->getVariable());
5050
}
5151

52-
public function testBacktickThrowsException(): void
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: 8 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -66,11 +66,9 @@ public function testUnsafeValueIsEscaped(string $value)
6666
$this->assertSame($expected, $actual);
6767
}
6868

69-
public function testValueWithBacktickThrowsException()
69+
public function testValueWithBacktickIsProperlyEscaped()
7070
{
71-
$this->expectException(InvalidArgumentException::class);
72-
73-
$this->trait->escape("foo`bar");
71+
$this->assertSame('`foo``bar`', $this->trait->escape("foo`bar"));
7472
}
7573

7674
public function provideSafeValueIsNotEscapedData(): array
@@ -79,45 +77,23 @@ public function provideSafeValueIsNotEscapedData(): array
7977
['foobar'],
8078
['fooBar'],
8179
['FOOBAR'],
80+
['foo_bar'],
81+
['FOO_BAR'],
8282
['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-
[''],
10983
['aaa100'],
11084
['a0'],
11185
['z10'],
11286
['z99'],
87+
['ça'],
88+
[''],
11389
];
11490
}
11591

11692
public function provideUnsafeValueIsEscapedData(): array
11793
{
11894
return [
119-
['foo_bar'],
120-
['FOO_BAR'],
95+
[''],
96+
['__FooBar__'],
12197
['_'],
12298
['__'],
12399
['\''],

0 commit comments

Comments
 (0)