diff --git a/src/Content/Text/HTML.php b/src/Content/Text/HTML.php index fa5a0a5905..00d609cb60 100644 --- a/src/Content/Text/HTML.php +++ b/src/Content/Text/HTML.php @@ -33,6 +33,7 @@ use Friendica\Util\Network; use Friendica\Util\Strings; use Friendica\Util\XML; use League\HTMLToMarkdown\HtmlConverter; +use Psr\Http\Message\UriInterface; class HTML { @@ -1007,4 +1008,51 @@ class HTML return $text; } + + /** + * XPath arbitrary string quoting + * + * @see https://stackoverflow.com/a/45228168 + * @param string $value + * @return string + */ + public static function xpathQuote(string $value): string + { + if (false === strpos($value, '"')) { + return '"' . $value . '"'; + } + + if (false === strpos($value, "'")) { + return "'" . $value . "'"; + } + + // if the value contains both single and double quotes, construct an + // expression that concatenates all non-double-quote substrings with + // the quotes, e.g.: + // + // concat("'foo'", '"', "bar") + return 'concat(' . implode(', \'"\', ', array_map(['self', 'xpathQuote'], explode('"', $value))) . ')'; + } + + /** + * Checks if the provided URL is present in the DOM document in an element with the rel="me" attribute + * + * XHTML Friends Network http://gmpg.org/xfn/ + * + * @param DOMDocument $doc + * @param UriInterface $meUrl + * @return bool + */ + public static function checkRelMeLink(DOMDocument $doc, UriInterface $meUrl): bool + { + $xpath = new \DOMXpath($doc); + + // This expression checks that "me" is among the space-delimited values of the "rel" attribute. + // And that the href attribute contains exactly the provided URL + $expression = "//*[contains(concat(' ', normalize-space(@rel), ' '), ' me ')][@href = " . self::xpathQuote($meUrl) . "]"; + + $result = $xpath->query($expression); + + return $result !== false && $result->length > 0; + } } diff --git a/tests/datasets/dom/relme/a-multiple-rel-value-end.html b/tests/datasets/dom/relme/a-multiple-rel-value-end.html new file mode 100644 index 0000000000..5fa4cbdf4a --- /dev/null +++ b/tests/datasets/dom/relme/a-multiple-rel-value-end.html @@ -0,0 +1,10 @@ + + + + + Remote page + + + My Profile + + diff --git a/tests/datasets/dom/relme/a-multiple-rel-value-middle.html b/tests/datasets/dom/relme/a-multiple-rel-value-middle.html new file mode 100644 index 0000000000..08d33f051b --- /dev/null +++ b/tests/datasets/dom/relme/a-multiple-rel-value-middle.html @@ -0,0 +1,10 @@ + + + + + Remote page + + + My Profile + + diff --git a/tests/datasets/dom/relme/a-multiple-rel-value-start.html b/tests/datasets/dom/relme/a-multiple-rel-value-start.html new file mode 100644 index 0000000000..c71d8288ca --- /dev/null +++ b/tests/datasets/dom/relme/a-multiple-rel-value-start.html @@ -0,0 +1,10 @@ + + + + + Remote page + + + My Profile + + diff --git a/tests/datasets/dom/relme/a-single-rel-value-fail.html b/tests/datasets/dom/relme/a-single-rel-value-fail.html new file mode 100644 index 0000000000..2735aa4aa7 --- /dev/null +++ b/tests/datasets/dom/relme/a-single-rel-value-fail.html @@ -0,0 +1,10 @@ + + + + + Remote page + + + My Profile + + diff --git a/tests/datasets/dom/relme/a-single-rel-value.html b/tests/datasets/dom/relme/a-single-rel-value.html new file mode 100644 index 0000000000..26d61204e7 --- /dev/null +++ b/tests/datasets/dom/relme/a-single-rel-value.html @@ -0,0 +1,10 @@ + + + + + Remote page + + + My Profile + + diff --git a/tests/datasets/dom/relme/link-single-rel-value-fail.html b/tests/datasets/dom/relme/link-single-rel-value-fail.html new file mode 100644 index 0000000000..2b7df5cb22 --- /dev/null +++ b/tests/datasets/dom/relme/link-single-rel-value-fail.html @@ -0,0 +1,11 @@ + + + + + Remote page + + + + + + diff --git a/tests/datasets/dom/relme/link-single-rel-value.html b/tests/datasets/dom/relme/link-single-rel-value.html new file mode 100644 index 0000000000..f18d000f3d --- /dev/null +++ b/tests/datasets/dom/relme/link-single-rel-value.html @@ -0,0 +1,11 @@ + + + + + Remote page + + + + + + diff --git a/tests/src/Content/Text/HTMLTest.php b/tests/src/Content/Text/HTMLTest.php index bc352e5427..e4a8603595 100644 --- a/tests/src/Content/Text/HTMLTest.php +++ b/tests/src/Content/Text/HTMLTest.php @@ -25,6 +25,8 @@ use Exception; use Friendica\Content\Text\HTML; use Friendica\Network\HTTPException\InternalServerErrorException; use Friendica\Test\FixtureTest; +use GuzzleHttp\Psr7\Uri; +use Psr\Http\Message\UriInterface; class HTMLTest extends FixtureTest { @@ -105,4 +107,152 @@ its surprisingly good", self::assertEquals($expectedBBCode, $actual); } + + public function dataXpathQuote(): array + { + return [ + 'no quotes' => [ + 'value' => "foo", + ], + 'double quotes only' => [ + 'value' => "\"foo", + ], + 'single quotes only' => [ + 'value' => "'foo", + ], + 'both; double quotes in mid-string' => [ + 'value' => "'foo\"bar", + ], + 'multiple double quotes in mid-string' => [ + 'value' => "'foo\"bar\"baz", + ], + 'string ends with double quotes' => [ + 'value' => "'foo\"", + ], + 'string ends with run of double quotes' => [ + 'value' => "'foo\"\"", + ], + 'string begins with double quotes' => [ + 'value' => "\"'foo", + ], + 'string begins with run of double quotes' => [ + 'value' => "\"\"'foo", + ], + 'run of double quotes in mid-string' => [ + 'value' => "'foo\"\"bar", + ], + ]; + } + + /** + * @dataProvider dataXpathQuote + * @param string $value + * @return void + * @throws \DOMException + */ + public function testXpathQuote(string $value) + { + $dom = new \DOMDocument(); + $element = $dom->createElement('test'); + $attribute = $dom->createAttribute('value'); + $attribute->value = $value; + $element->appendChild($attribute); + $dom->appendChild($element); + + $xpath = new \DOMXPath($dom); + + $result = $xpath->query('//test[@value = ' . HTML::xpathQuote($value) . ']'); + + $this->assertInstanceOf(\DOMNodeList::class, $result); + $this->assertEquals(1, $result->length); + } + + public function dataCheckRelMeLink(): array + { + $aSingleRelValue = new \DOMDocument(); + $aSingleRelValue->load(__DIR__ . '/../../../datasets/dom/relme/a-single-rel-value.html'); + + $aMultipleRelValueStart = new \DOMDocument(); + $aMultipleRelValueStart->load(__DIR__ . '/../../../datasets/dom/relme/a-multiple-rel-value-start.html'); + + $aMultipleRelValueMiddle = new \DOMDocument(); + $aMultipleRelValueMiddle->load(__DIR__ . '/../../../datasets/dom/relme/a-multiple-rel-value-middle.html'); + + $aMultipleRelValueEnd = new \DOMDocument(); + $aMultipleRelValueEnd->load(__DIR__ . '/../../../datasets/dom/relme/a-multiple-rel-value-end.html'); + + $linkSingleRelValue = new \DOMDocument(); + $linkSingleRelValue->load(__DIR__ . '/../../../datasets/dom/relme/link-single-rel-value.html'); + + $meUrl = new Uri('https://example.com/profile/me'); + + return [ + 'a-single-rel-value' => [ + 'doc' => $aSingleRelValue, + 'meUrl' => $meUrl + ], + 'a-multiple-rel-value-start' => [ + 'doc' => $aMultipleRelValueStart, + 'meUrl' => $meUrl + ], + 'a-multiple-rel-value-middle' => [ + 'doc' => $aMultipleRelValueMiddle, + 'meUrl' => $meUrl + ], + 'a-multiple-rel-value-end' => [ + 'doc' => $aMultipleRelValueEnd, + 'meUrl' => $meUrl + ], + 'link-single-rel-value' => [ + 'doc' => $linkSingleRelValue, + 'meUrl' => $meUrl + ], + ]; + } + + + /** + * @dataProvider dataCheckRelMeLink + * @param \DOMDocument $doc + * @param UriInterface $meUrl + * @return void + */ + public function testCheckRelMeLink(\DOMDocument $doc, UriInterface $meUrl) + { + $this->assertTrue(HTML::checkRelMeLink($doc, $meUrl)); + } + + public function dataCheckRelMeLinkFail(): array + { + $aSingleRelValueFail = new \DOMDocument(); + $aSingleRelValueFail->load(__DIR__ . '/../../../datasets/dom/relme/a-single-rel-value-fail.html'); + + $linkSingleRelValueFail = new \DOMDocument(); + $linkSingleRelValueFail->load(__DIR__ . '/../../../datasets/dom/relme/link-single-rel-value-fail.html'); + + $meUrl = new Uri('https://example.com/profile/me'); + + return [ + 'a-single-rel-value-fail' => [ + 'doc' => $aSingleRelValueFail, + 'meUrl' => $meUrl + ], + 'link-single-rel-value-fail' => [ + 'doc' => $linkSingleRelValueFail, + 'meUrl' => $meUrl + ], + ]; + } + + + /** + * @dataProvider dataCheckRelMeLinkFail + * @param \DOMDocument $doc + * @param UriInterface $meUrl + * @return void + */ + public function testCheckRelMeLinkFail(\DOMDocument $doc, UriInterface $meUrl) + { + $this->assertFalse(HTML::checkRelMeLink($doc, $meUrl)); + } }