Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F15398927
D16099.id38733.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
7 KB
Referenced Files
None
Subscribers
None
D16099.id38733.diff
View Options
diff --git a/src/parser/PhutilURI.php b/src/parser/PhutilURI.php
--- a/src/parser/PhutilURI.php
+++ b/src/parser/PhutilURI.php
@@ -1,7 +1,16 @@
<?php
/**
- * Basic URI parser object.
+ * Structural representation of a URI.
+ *
+ * This class handles URIs of two types: standard URIs and Git URIs.
+ *
+ * Standard URIs look like `proto://user:pass@domain:port/path?query#fragment`.
+ * Almost all URIs are in this form.
+ *
+ * Git URIs look like `user@host:path`. These URIs are used by Git and SCP
+ * and have an implicit "ssh" protocol, no port, and interpret paths as
+ * relative instead of absolute.
*/
final class PhutilURI extends Phobject {
@@ -13,10 +22,18 @@
private $path;
private $query = array();
private $fragment;
+ private $type;
+
+ const TYPE_URI = 'uri';
+ const TYPE_GIT = 'git';
+
+ const INVALID_PATH = '<invalid-path>';
public function __construct($uri) {
$uri = (string)$uri;
+ $type = self::TYPE_URI;
+
$matches = null;
if (preg_match('(^([^/:]*://[^/]*)(\\?.*)\z)', $uri, $matches)) {
// If the URI is something like `idea://open?file=/path/to/file`, the
@@ -26,6 +43,25 @@
$parts = parse_url($matches[1].'/'.$matches[2]);
unset($parts['path']);
+ } else if (preg_match('(^[^/]+:(?!//))', $uri)) {
+ // Handle Git/SCP URIs in the form "user@domain:relative/path".
+
+ $user = '(?:(?P<user>[^/@]+)@)?';
+ $host = '(?P<host>[^/:]+)';
+ $path = ':(?P<path>.*)';
+
+ $ok = preg_match('(^\s*'.$user.$host.$path.'\z)', $uri, $matches);
+ if (!$ok) {
+ throw new Exception(
+ pht(
+ 'Failed to parse URI "%s" as a Git URI.',
+ $uri));
+ }
+
+ $parts = $matches;
+ $parts['protocol'] = 'ssh';
+
+ $type = self::TYPE_GIT;
} else {
$parts = parse_url($uri);
}
@@ -39,7 +75,6 @@
}
}
-
// NOTE: `parse_url()` is very liberal about host names; fail the parse if
// the host looks like garbage.
if ($parts) {
@@ -56,35 +91,58 @@
// stringyness is to preserve API compatibility and
// allow the tests to continue passing
$this->protocol = idx($parts, 'scheme', '');
- $this->user = rawurldecode(idx($parts, 'user', ''));
- $this->pass = rawurldecode(idx($parts, 'pass', ''));
- $this->domain = idx($parts, 'host', '');
- $this->port = (string)idx($parts, 'port', '');
- $this->path = idx($parts, 'path', '');
+ $this->user = rawurldecode(idx($parts, 'user', ''));
+ $this->pass = rawurldecode(idx($parts, 'pass', ''));
+ $this->domain = idx($parts, 'host', '');
+ $this->port = (string)idx($parts, 'port', '');
+ $this->path = idx($parts, 'path', '');
$query = idx($parts, 'query');
if ($query) {
$this->query = id(new PhutilQueryStringParser())->parseQueryString(
$query);
}
$this->fragment = idx($parts, 'fragment', '');
+
+ $this->type = $type;
}
public function __toString() {
$prefix = null;
- if ($this->protocol || $this->domain || $this->port) {
+
+ $protocol = $this->protocol;
+ if ($this->isGitURI()) {
+ $protocol = null;
+ } else {
$protocol = nonempty($this->protocol, 'http');
+ }
+
+ if ($this->isGitURI()) {
+ $port = null;
+ } else {
+ $port = $this->port;
+ }
+
+ $domain = $this->domain;
+
+ $user = $this->user;
+ $pass = $this->pass;
+ if (strlen($user) && strlen($pass)) {
+ $auth = rawurlencode($user).':'.rawurlencode($pass).'@';
+ } else if (strlen($user)) {
+ $auth = rawurlencode($user).'@';
+ } else {
+ $auth = null;
+ }
- $auth = '';
- if (strlen($this->user) && strlen($this->pass)) {
- $auth = rawurlencode($this->user).':'.
- rawurlencode($this->pass).'@';
- } else if (strlen($this->user)) {
- $auth = rawurlencode($this->user).'@';
+ if (strlen($protocol) || strlen($auth) || strlen($domain)) {
+ if ($this->isGitURI()) {
+ $prefix = "{$auth}{$domain}";
+ } else {
+ $prefix = "{$protocol}://{$auth}{$domain}";
}
- $prefix = $protocol.'://'.$auth.$this->domain;
- if ($this->port) {
- $prefix .= ':'.$this->port;
+ if (strlen($port)) {
+ $prefix .= ':'.$port;
}
}
@@ -100,8 +158,14 @@
$fragment = null;
}
+ $path = $this->getPath();
+ if ($this->isGitURI()) {
+ if (strlen($path)) {
+ $path = ':'.$path;
+ }
+ }
- return $prefix.$this->getPath().$query.$fragment;
+ return $prefix.$path.$query.$fragment;
}
public function setQueryParam($key, $value) {
@@ -161,9 +225,14 @@
}
public function setPath($path) {
- if ($this->domain && strlen($path) && $path[0] !== '/') {
- $path = '/'.$path;
+ if ($this->isGitURI()) {
+ // Git URIs use relative paths which do not need to begin with "/".
+ } else {
+ if ($this->domain && strlen($path) && $path[0] !== '/') {
+ $path = '/'.$path;
+ }
}
+
$this->path = $path;
return $this;
}
@@ -221,4 +290,33 @@
return $altered;
}
+ public function isGitURI() {
+ return ($this->type == self::TYPE_GIT);
+ }
+
+ public function setType($type) {
+
+ if ($type == self::TYPE_URI) {
+ $path = $this->getPath();
+ if (strlen($path) && ($path[0] !== '/')) {
+ // Try to catch this here because we are not allowed to throw from
+ // inside __toString() so we don't have a reasonable opportunity to
+ // react properly if we catch it later.
+ throw new Exception(
+ pht(
+ 'Unable to convert URI "%s" into a standard URI because the '.
+ 'path is relative. Standard URIs can not represent relative '.
+ 'paths.',
+ $this));
+ }
+ }
+
+ $this->type = $type;
+ return $this;
+ }
+
+ public function getType() {
+ return $this->type;
+ }
+
}
diff --git a/src/parser/__tests__/PhutilURITestCase.php b/src/parser/__tests__/PhutilURITestCase.php
--- a/src/parser/__tests__/PhutilURITestCase.php
+++ b/src/parser/__tests__/PhutilURITestCase.php
@@ -164,4 +164,36 @@
$this->assertEqual('', $uri->getPortWithProtocolDefault());
}
+ public function testGitURIParsing() {
+ $uri = new PhutilURI('git@host.com:path/to/something');
+ $this->assertEqual('git', $uri->getUser());
+ $this->assertEqual('host.com', $uri->getDomain());
+ $this->assertEqual('path/to/something', $uri->getPath());
+ $this->assertEqual('git@host.com:path/to/something', (string)$uri);
+
+ $uri = new PhutilURI('host.com:path/to/something');
+ $this->assertEqual('', $uri->getUser());
+ $this->assertEqual('host.com', $uri->getDomain());
+ $this->assertEqual('path/to/something', $uri->getPath());
+ $this->assertEqual('host.com:path/to/something', (string)$uri);
+ }
+
+ public function testStrictGitURIParsingOfLeadingWhitespace() {
+ $uri = new PhutilURI(' user@example.com:path');
+ $this->assertEqual('', $uri->getDomain());
+ }
+
+ public function testNoRelativeURIPaths() {
+ $uri = new PhutilURI('user@example.com:relative_path');
+
+ $caught = null;
+ try {
+ $uri->setType(PhutilURI::TYPE_URI);
+ } catch (Exception $ex) {
+ $caught = $ex;
+ }
+
+ $this->assertTrue($caught instanceof Exception);
+ }
+
}
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Tue, Mar 18, 1:55 AM (6 d, 4 h ago)
Storage Engine
blob
Storage Format
Encrypted (AES-256-CBC)
Storage Handle
7705271
Default Alt Text
D16099.id38733.diff (7 KB)
Attached To
Mode
D16099: Merge PhutilGitURI into PhutilURI
Attached
Detach File
Event Timeline
Log In to Comment