Page MenuHomePhabricator

D16099.id38733.diff
No OneTemporary

D16099.id38733.diff

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

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)

Event Timeline