Skip to content

Commit d3850f8

Browse files
committed
Fix XPath escaping in WebDriverSelect
1 parent a5ba38a commit d3850f8

File tree

4 files changed

+105
-38
lines changed

4 files changed

+105
-38
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ This project versioning adheres to [Semantic Versioning](http://semver.org/).
1515
- `elementTextMatches` - text in element matches regular expression
1616
- `numberOfWindowsToBe` - number of opened windows equals given number
1717
- Possibility to select option of `<select>` by its partial text (using `selectByVisiblePartialText()`)
18+
- `XPathEscaper` helper class to quote XPaths containing both single and double quotes.
1819

1920
### Changed
2021
- `Symfony\Process` is used to start local WebDriver processes (when browsers are run directly, without Selenium server) to workaround some PHP bugs and improve portability.
@@ -23,6 +24,9 @@ This project versioning adheres to [Semantic Versioning](http://semver.org/).
2324
- Deprecated `WebDriverExpectedCondition::textToBePresentInElement()` in favor of `elementTextContains()`
2425
- Throw an exception when attempting to deselect options of non-multiselect (it already didn't have any effect, but was silently ignored).
2526

27+
### Fixed
28+
- XPath escaping in `select*()` and `deselect*()` methods of `WebDriverSelect`.
29+
2630
## 1.2.0 - 2016-10-14
2731
- Added initial support of remote Microsoft Edge browser (but starting local EdgeDriver is still not supported).
2832
- Utilize late static binding to make eg. `WebDriverBy` and `DesiredCapabilities` classes easily extensible.

lib/Support/XPathEscaper.php

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php
2+
// Copyright 2004-present Facebook. All Rights Reserved.
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
16+
namespace Facebook\WebDriver\Support;
17+
18+
class XPathEscaper
19+
{
20+
/**
21+
* Converts xpath strings with both quotes and ticks into:
22+
* foo'"bar -> concat('foo', "'" ,'"bar')
23+
*
24+
* @param string $xpathToEscape The xpath to be converted.
25+
* @return string The escaped string.
26+
*/
27+
public static function escapeQuotes($xpathToEscape)
28+
{
29+
// Single quotes not present => we can quote in them
30+
if (strpos($xpathToEscape, "'") === false) {
31+
return sprintf("'%s'", $xpathToEscape);
32+
}
33+
34+
// Double quotes not present => we can quote in them
35+
if (strpos($xpathToEscape, '"') === false) {
36+
return sprintf('"%s"', $xpathToEscape);
37+
}
38+
39+
// Both single and double quotes are present
40+
return sprintf(
41+
"concat('%s')",
42+
str_replace("'", "', \"'\" ,'", $xpathToEscape)
43+
);
44+
}
45+
}

lib/WebDriverSelect.php

Lines changed: 7 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
use Facebook\WebDriver\Exception\NoSuchElementException;
1919
use Facebook\WebDriver\Exception\UnexpectedTagNameException;
2020
use Facebook\WebDriver\Exception\UnsupportedOperationException;
21+
use Facebook\WebDriver\Support\XPathEscaper;
2122

2223
/**
2324
* Models a SELECT tag, providing helper methods to select and deselect options.
@@ -128,7 +129,7 @@ public function selectByIndex($index)
128129
public function selectByValue($value)
129130
{
130131
$matched = false;
131-
$xpath = './/option[@value = ' . $this->escapeQuotes($value) . ']';
132+
$xpath = './/option[@value = ' . XPathEscaper::escapeQuotes($value) . ']';
132133
$options = $this->element->findElements(WebDriverBy::xpath($xpath));
133134

134135
foreach ($options as $option) {
@@ -161,7 +162,7 @@ public function selectByValue($value)
161162
public function selectByVisibleText($text)
162163
{
163164
$matched = false;
164-
$xpath = './/option[normalize-space(.) = ' . $this->escapeQuotes($text) . ']';
165+
$xpath = './/option[normalize-space(.) = ' . XPathEscaper::escapeQuotes($text) . ']';
165166
$options = $this->element->findElements(WebDriverBy::xpath($xpath));
166167

167168
foreach ($options as $option) {
@@ -210,7 +211,7 @@ public function selectByVisibleText($text)
210211
public function selectByVisiblePartialText($text)
211212
{
212213
$matched = false;
213-
$xpath = './/option[contains(normalize-space(.), ' . $this->escapeQuotes($text) . ')]';
214+
$xpath = './/option[contains(normalize-space(.), ' . XPathEscaper::escapeQuotes($text) . ')]';
214215
$options = $this->element->findElements(WebDriverBy::xpath($xpath));
215216

216217
foreach ($options as $option) {
@@ -282,7 +283,7 @@ public function deselectByValue($value)
282283
throw new UnsupportedOperationException('You may only deselect options of a multi-select');
283284
}
284285

285-
$xpath = './/option[@value = ' . $this->escapeQuotes($value) . ']';
286+
$xpath = './/option[@value = ' . XPathEscaper::escapeQuotes($value) . ']';
286287
$options = $this->element->findElements(WebDriverBy::xpath($xpath));
287288
foreach ($options as $option) {
288289
if ($option->isSelected()) {
@@ -306,7 +307,7 @@ public function deselectByVisibleText($text)
306307
throw new UnsupportedOperationException('You may only deselect options of a multi-select');
307308
}
308309

309-
$xpath = './/option[normalize-space(.) = ' . $this->escapeQuotes($text) . ']';
310+
$xpath = './/option[normalize-space(.) = ' . XPathEscaper::escapeQuotes($text) . ']';
310311
$options = $this->element->findElements(WebDriverBy::xpath($xpath));
311312
foreach ($options as $option) {
312313
if ($option->isSelected()) {
@@ -330,44 +331,12 @@ public function deselectByVisiblePartialText($text)
330331
throw new UnsupportedOperationException('You may only deselect options of a multi-select');
331332
}
332333

333-
$xpath = './/option[contains(normalize-space(.), ' . $this->escapeQuotes($text) . ')]';
334+
$xpath = './/option[contains(normalize-space(.), ' . XPathEscaper::escapeQuotes($text) . ')]';
334335
$options = $this->element->findElements(WebDriverBy::xpath($xpath));
335336
foreach ($options as $option) {
336337
if ($option->isSelected()) {
337338
$option->click();
338339
}
339340
}
340341
}
341-
342-
/**
343-
* Convert strings with both quotes and ticks into:
344-
* foo'"bar -> concat("foo'", '"', "bar")
345-
*
346-
* @param string $to_escape The string to be converted.
347-
* @return string The escaped string.
348-
*/
349-
protected function escapeQuotes($to_escape)
350-
{
351-
if (strpos($to_escape, '"') !== false && strpos($to_escape, "'") !== false) {
352-
$substrings = explode('"', $to_escape);
353-
354-
$escaped = 'concat(';
355-
$first = true;
356-
foreach ($substrings as $string) {
357-
if (!$first) {
358-
$escaped .= ", '\"',";
359-
$first = false;
360-
}
361-
$escaped .= '"' . $string . '"';
362-
}
363-
364-
return $escaped;
365-
}
366-
367-
if (strpos($to_escape, '"') !== false) {
368-
return sprintf("'%s'", $to_escape);
369-
}
370-
371-
return sprintf('"%s"', $to_escape);
372-
}
373342
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<?php
2+
// Copyright 2004-present Facebook. All Rights Reserved.
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
16+
namespace Facebook\WebDriver\Support;
17+
18+
class XPathEscaperTest extends \PHPUnit_Framework_TestCase
19+
{
20+
/**
21+
* @dataProvider xpathProvider
22+
* @param string $input
23+
* @param string $expectedOutput
24+
*/
25+
public function testShouldInstantiateWithCapabilitiesGivenInConstructor($input, $expectedOutput)
26+
{
27+
$output = XPathEscaper::escapeQuotes($input);
28+
29+
$this->assertSame($expectedOutput, $output);
30+
}
31+
32+
/**
33+
* @return array[]
34+
*/
35+
public function xpathProvider()
36+
{
37+
return [
38+
'empty string encapsulate in single quotes' => ['', "''"],
39+
'string without quotes encapsulate in single quotes' => ['foo bar', "'foo bar'"],
40+
'string with single quotes encapsulate in double quotes' => ['foo\'bar\'', '"foo\'bar\'"'],
41+
'string with double quotes encapsulate in single quotes' => ['foo"bar"', '\'foo"bar"\''],
42+
'string with both types of quotes concatenate' => ['\'"', "concat('', \"'\" ,'\"')"],
43+
'string with multiple both types of quotes concatenate' => [
44+
'a \'b\'"c"',
45+
"concat('a ', \"'\" ,'b', \"'\" ,'\"c\"')",
46+
],
47+
];
48+
}
49+
}

0 commit comments

Comments
 (0)