Browse Source
The query part is not parsed for now because PSR-7 request objects/PHP take care of that parsing for us.microsub
J. King
6 years ago
3 changed files with 207 additions and 9 deletions
@ -0,0 +1,131 @@ |
|||||
|
<?php |
||||
|
/** @license MIT |
||||
|
* Copyright 2017 J. King, Dustin Wilson et al. |
||||
|
* See LICENSE and AUTHORS files for details */ |
||||
|
|
||||
|
declare(strict_types=1); |
||||
|
namespace JKingWeb\Arsse\REST; |
||||
|
|
||||
|
use JKingWeb\Arsse\Misc\ValueInfo; |
||||
|
|
||||
|
class Target { |
||||
|
public $relative = false; |
||||
|
public $index = false; |
||||
|
public $path = []; |
||||
|
public $query = ""; |
||||
|
public $fragment = ""; |
||||
|
|
||||
|
public function __construct(string $target) { |
||||
|
$target = $this->parseFragment($target); |
||||
|
$target = $this->parseQuery($target); |
||||
|
$this->path = $this->parsePath($target); |
||||
|
} |
||||
|
|
||||
|
public function __toString(): string { |
||||
|
$out = ""; |
||||
|
$path = []; |
||||
|
foreach ($this->path as $segment) { |
||||
|
if (is_null($segment)) { |
||||
|
if (!$path) { |
||||
|
$path[] = ".."; |
||||
|
} else { |
||||
|
continue; |
||||
|
} |
||||
|
} elseif ($segment==".") { |
||||
|
$path[] = "%2E"; |
||||
|
} elseif ($segment=="..") { |
||||
|
$path[] = "%2E%2E"; |
||||
|
} else { |
||||
|
$path[] = rawurlencode(ValueInfo::normalize($segment, ValueInfo::T_STRING)); |
||||
|
} |
||||
|
} |
||||
|
$path = implode("/", $path); |
||||
|
if (!$this->relative) { |
||||
|
$out .= "/"; |
||||
|
} |
||||
|
$out .= $path; |
||||
|
if ($this->index && strlen($path)) { |
||||
|
$out .= "/"; |
||||
|
} |
||||
|
if (strlen($this->query)) { |
||||
|
$out .= "?".$this->query; |
||||
|
} |
||||
|
if (strlen($this->fragment)) { |
||||
|
$out .= "#".rawurlencode($this->fragment); |
||||
|
} |
||||
|
return $out; |
||||
|
} |
||||
|
|
||||
|
public static function normalize(string $target): string { |
||||
|
return (string) new self($target); |
||||
|
} |
||||
|
|
||||
|
protected function parseFragment(string $target): string { |
||||
|
// store and strip off any fragment identifier and return the target without a fragment |
||||
|
$pos = strpos($target,"#"); |
||||
|
if ($pos !== false) { |
||||
|
$this->fragment = rawurldecode(substr($target, $pos + 1)); |
||||
|
$target = substr($target, 0, $pos); |
||||
|
} |
||||
|
return $target; |
||||
|
} |
||||
|
|
||||
|
protected function parseQuery(string $target): string { |
||||
|
// store and strip off any query string and return the target without a query |
||||
|
// note that the function assumes any fragment identifier has already been stripped off |
||||
|
// unlike the other parts the query string is currently neither parsed nor normalized |
||||
|
$pos = strpos($target,"?"); |
||||
|
if ($pos !== false) { |
||||
|
$this->query = substr($target, $pos + 1); |
||||
|
$target = substr($target, 0, $pos); |
||||
|
} |
||||
|
return $target; |
||||
|
} |
||||
|
|
||||
|
protected function parsePath(string $target): array { |
||||
|
// note that the function assumes any fragment identifier or query has already been stripped off |
||||
|
// syntax-based normalization is applied to the path segments (see RFC 3986 sec. 6.2.2) |
||||
|
// duplicate slashes are NOT collapsed |
||||
|
if (substr($target, 0, 1)=="/") { |
||||
|
// if the path starts with a slash, strip it off |
||||
|
$target = substr($target, 1); |
||||
|
} else { |
||||
|
// otherwise this is a relative target |
||||
|
$this->relative = true; |
||||
|
} |
||||
|
if (!strlen($target)) { |
||||
|
// if the target is an empty string, this is an index target |
||||
|
$this->index = true; |
||||
|
} elseif (substr($target, -1, 1)=="/") { |
||||
|
// if the path ends in a slash, this is an index target and the slash should be stripped off |
||||
|
$this->index = true; |
||||
|
$target = substr($target, 0, strlen($target) -1); |
||||
|
} |
||||
|
// after stripping, explode the path parts |
||||
|
if (strlen($target)) { |
||||
|
$target = explode("/", $target); |
||||
|
$out = []; |
||||
|
// resolve relative path segments and decode each retained segment |
||||
|
foreach($target as $index => $segment) { |
||||
|
if ($segment==".") { |
||||
|
// self-referential segments can be ignored |
||||
|
continue; |
||||
|
} elseif ($segment=="..") { |
||||
|
if ($index==0) { |
||||
|
// if the first path segment refers to its parent (which we don't know about) we cannot output a correct path, so we do the best we can |
||||
|
$out[] = null; |
||||
|
} else { |
||||
|
// for any other segments after the first we pop off the last stored segment |
||||
|
array_pop($out); |
||||
|
} |
||||
|
} else { |
||||
|
// any other segment is decoded and retained |
||||
|
$out[] = rawurldecode($segment); |
||||
|
} |
||||
|
} |
||||
|
return $out; |
||||
|
} else { |
||||
|
return []; |
||||
|
} |
||||
|
} |
||||
|
} |
@ -0,0 +1,66 @@ |
|||||
|
<?php |
||||
|
/** @license MIT |
||||
|
* Copyright 2017 J. King, Dustin Wilson et al. |
||||
|
* See LICENSE and AUTHORS files for details */ |
||||
|
|
||||
|
declare(strict_types=1); |
||||
|
namespace JKingWeb\Arsse\TestCase\REST; |
||||
|
|
||||
|
use JKingWeb\Arsse\REST\Target; |
||||
|
|
||||
|
/** @covers \JKingWeb\Arsse\REST\Target<extended> */ |
||||
|
class TestTarget extends \JKingWeb\Arsse\Test\AbstractTest { |
||||
|
|
||||
|
/** @dataProvider provideTargetUrls */ |
||||
|
public function testParseTargetUrl(string $target, array $path, bool $relative, bool $index, string $query, string $fragment, string $normalized) { |
||||
|
$test = new Target($target); |
||||
|
$this->assertEquals($path, $test->path, "Path does not match"); |
||||
|
$this->assertSame($path, $test->path, "Path does not match exactly"); |
||||
|
$this->assertSame($relative, $test->relative, "Relative flag does not match"); |
||||
|
$this->assertSame($index, $test->index, "Index flag does not match"); |
||||
|
$this->assertSame($query, $test->query, "Query does not match"); |
||||
|
$this->assertSame($fragment, $test->fragment, "Fragment does not match"); |
||||
|
} |
||||
|
|
||||
|
/** @dataProvider provideTargetUrls */ |
||||
|
public function testNormalizeTargetUrl(string $target, array $path, bool $relative, bool $index, string $query, string $fragment, string $normalized) { |
||||
|
$test = new Target(""); |
||||
|
$test->path = $path; |
||||
|
$test->relative = $relative; |
||||
|
$test->index = $index; |
||||
|
$test->query = $query; |
||||
|
$test->fragment = $fragment; |
||||
|
$this->assertSame($normalized, (string) $test); |
||||
|
$this->assertSame($normalized, Target::normalize($target)); |
||||
|
} |
||||
|
|
||||
|
public function provideTargetUrls() { |
||||
|
return [ |
||||
|
["/", [], false, true, "", "", "/"], |
||||
|
["", [], true, true, "", "", ""], |
||||
|
["/index.php", ["index.php"], false, false, "", "", "/index.php"], |
||||
|
["index.php", ["index.php"], true, false, "", "", "index.php"], |
||||
|
["/ook/", ["ook"], false, true, "", "", "/ook/"], |
||||
|
["ook/", ["ook"], true, true, "", "", "ook/"], |
||||
|
["/eek/../ook/", ["ook"], false, true, "", "", "/ook/"], |
||||
|
["eek/../ook/", ["ook"], true, true, "", "", "ook/"], |
||||
|
["/./ook/", ["ook"], false, true, "", "", "/ook/"], |
||||
|
["./ook/", ["ook"], true, true, "", "", "ook/"], |
||||
|
["/../ook/", [null,"ook"], false, true, "", "", "/../ook/"], |
||||
|
["../ook/", [null,"ook"], true, true, "", "", "../ook/"], |
||||
|
["0", ["0"], true, false, "", "", "0"], |
||||
|
["%6f%6F%6b", ["ook"], true, false, "", "", "ook"], |
||||
|
["%2e%2E%2f%2E%2Fook%2f", [".././ook/"], true, false, "", "", "..%2F.%2Fook%2F"], |
||||
|
["%2e%2E/%2E/ook%2f", ["..",".","ook/"], true, false, "", "", "%2E%2E/%2E/ook%2F"], |
||||
|
["...", ["..."], true, false, "", "", "..."], |
||||
|
["%2e%2e%2e", ["..."], true, false, "", "", "..."], |
||||
|
["/?", [], false, true, "", "", "/"], |
||||
|
["/#", [], false, true, "", "", "/"], |
||||
|
["/?#", [], false, true, "", "", "/"], |
||||
|
["#%2e", [], true, true, "", ".", "#."], |
||||
|
["?%2e", [], true, true, "%2e", "", "?%2e"], |
||||
|
["?%2e#%2f", [], true, true, "%2e", "/", "?%2e#%2F"], |
||||
|
["#%2e?%2f", [], true, true, "", ".?/", "#.%3F%2F"], |
||||
|
]; |
||||
|
} |
||||
|
} |
Loading…
Reference in new issue