Correct Aphlict websocket URI construction after PHP8 compatibility changes
[phabricator.git] / src / applications / repository / storage / PhabricatorRepositoryURI.php
blob011d46482fe0192fb4ba520428a6ff49bd4ca3ca
1 <?php
3 final class PhabricatorRepositoryURI
4 extends PhabricatorRepositoryDAO
5 implements
6 PhabricatorApplicationTransactionInterface,
7 PhabricatorPolicyInterface,
8 PhabricatorExtendedPolicyInterface,
9 PhabricatorConduitResultInterface {
11 protected $repositoryPHID;
12 protected $uri;
13 protected $builtinProtocol;
14 protected $builtinIdentifier;
15 protected $credentialPHID;
16 protected $ioType;
17 protected $displayType;
18 protected $isDisabled;
20 private $repository = self::ATTACHABLE;
22 const BUILTIN_PROTOCOL_SSH = 'ssh';
23 const BUILTIN_PROTOCOL_HTTP = 'http';
24 const BUILTIN_PROTOCOL_HTTPS = 'https';
26 const BUILTIN_IDENTIFIER_ID = 'id';
27 const BUILTIN_IDENTIFIER_SHORTNAME = 'shortname';
28 const BUILTIN_IDENTIFIER_CALLSIGN = 'callsign';
30 const DISPLAY_DEFAULT = 'default';
31 const DISPLAY_NEVER = 'never';
32 const DISPLAY_ALWAYS = 'always';
34 const IO_DEFAULT = 'default';
35 const IO_OBSERVE = 'observe';
36 const IO_MIRROR = 'mirror';
37 const IO_NONE = 'none';
38 const IO_READ = 'read';
39 const IO_READWRITE = 'readwrite';
41 protected function getConfiguration() {
42 return array(
43 self::CONFIG_AUX_PHID => true,
44 self::CONFIG_COLUMN_SCHEMA => array(
45 'uri' => 'text255',
46 'builtinProtocol' => 'text32?',
47 'builtinIdentifier' => 'text32?',
48 'credentialPHID' => 'phid?',
49 'ioType' => 'text32',
50 'displayType' => 'text32',
51 'isDisabled' => 'bool',
53 self::CONFIG_KEY_SCHEMA => array(
54 'key_builtin' => array(
55 'columns' => array(
56 'repositoryPHID',
57 'builtinProtocol',
58 'builtinIdentifier',
60 'unique' => true,
63 ) + parent::getConfiguration();
66 public static function initializeNewURI() {
67 return id(new self())
68 ->setIoType(self::IO_DEFAULT)
69 ->setDisplayType(self::DISPLAY_DEFAULT)
70 ->setIsDisabled(0);
73 public function generatePHID() {
74 return PhabricatorPHID::generateNewPHID(
75 PhabricatorRepositoryURIPHIDType::TYPECONST);
78 public function attachRepository(PhabricatorRepository $repository) {
79 $this->repository = $repository;
80 return $this;
83 public function getRepository() {
84 return $this->assertAttached($this->repository);
87 public function getRepositoryURIBuiltinKey() {
88 if (!$this->getBuiltinProtocol()) {
89 return null;
92 $parts = array(
93 $this->getBuiltinProtocol(),
94 $this->getBuiltinIdentifier(),
97 return implode('.', $parts);
100 public function isBuiltin() {
101 return (bool)$this->getBuiltinProtocol();
104 public function getEffectiveDisplayType() {
105 $display = $this->getDisplayType();
107 if ($display != self::DISPLAY_DEFAULT) {
108 return $display;
111 return $this->getDefaultDisplayType();
114 public function getDefaultDisplayType() {
115 switch ($this->getEffectiveIOType()) {
116 case self::IO_MIRROR:
117 case self::IO_OBSERVE:
118 case self::IO_NONE:
119 return self::DISPLAY_NEVER;
120 case self::IO_READ:
121 case self::IO_READWRITE:
122 // By default, only show the "best" version of the builtin URI, not the
123 // other redundant versions.
124 $repository = $this->getRepository();
125 $other_uris = $repository->getURIs();
127 $identifier_value = array(
128 self::BUILTIN_IDENTIFIER_SHORTNAME => 3,
129 self::BUILTIN_IDENTIFIER_CALLSIGN => 2,
130 self::BUILTIN_IDENTIFIER_ID => 1,
133 $have_identifiers = array();
134 foreach ($other_uris as $other_uri) {
135 if ($other_uri->getIsDisabled()) {
136 continue;
139 $identifier = $other_uri->getBuiltinIdentifier();
140 if (!$identifier) {
141 continue;
144 $have_identifiers[$identifier] = $identifier_value[$identifier];
147 $best_identifier = max($have_identifiers);
148 $this_identifier = $identifier_value[$this->getBuiltinIdentifier()];
150 if ($this_identifier < $best_identifier) {
151 return self::DISPLAY_NEVER;
154 return self::DISPLAY_ALWAYS;
157 return self::DISPLAY_NEVER;
161 public function getEffectiveIOType() {
162 $io = $this->getIoType();
164 if ($io != self::IO_DEFAULT) {
165 return $io;
168 return $this->getDefaultIOType();
171 public function getDefaultIOType() {
172 if ($this->isBuiltin()) {
173 $repository = $this->getRepository();
174 $other_uris = $repository->getURIs();
176 $any_observe = false;
177 foreach ($other_uris as $other_uri) {
178 if ($other_uri->getIoType() == self::IO_OBSERVE) {
179 $any_observe = true;
180 break;
184 if ($any_observe) {
185 return self::IO_READ;
186 } else {
187 return self::IO_READWRITE;
191 return self::IO_NONE;
194 public function getNormalizedURI() {
195 $vcs = $this->getRepository()->getVersionControlSystem();
197 $map = array(
198 PhabricatorRepositoryType::REPOSITORY_TYPE_GIT =>
199 ArcanistRepositoryURINormalizer::TYPE_GIT,
200 PhabricatorRepositoryType::REPOSITORY_TYPE_SVN =>
201 ArcanistRepositoryURINormalizer::TYPE_SVN,
202 PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL =>
203 ArcanistRepositoryURINormalizer::TYPE_MERCURIAL,
206 $type = $map[$vcs];
207 $display = (string)$this->getDisplayURI();
209 $normalizer = new ArcanistRepositoryURINormalizer($type, $display);
211 $domain_map = self::getURINormalizerDomainMap();
212 $normalizer->setDomainMap($domain_map);
214 return $normalizer->getNormalizedURI();
217 public function getDisplayURI() {
218 return $this->getURIObject();
221 public function getEffectiveURI() {
222 return $this->getURIObject();
225 public function getURIEnvelope() {
226 $uri = $this->getEffectiveURI();
228 $command_engine = $this->newCommandEngine();
230 $is_http = $command_engine->isAnyHTTPProtocol();
231 // For SVN, we use `--username` and `--password` flags separately in the
232 // CommandEngine, so we don't need to add any credentials here.
233 $is_svn = $this->getRepository()->isSVN();
234 $credential_phid = $this->getCredentialPHID();
236 if ($is_http && !$is_svn && $credential_phid) {
237 $key = PassphrasePasswordKey::loadFromPHID(
238 $credential_phid,
239 PhabricatorUser::getOmnipotentUser());
241 $uri->setUser($key->getUsernameEnvelope()->openEnvelope());
242 $uri->setPass($key->getPasswordEnvelope()->openEnvelope());
245 return new PhutilOpaqueEnvelope((string)$uri);
248 private function getURIObject() {
249 // Users can provide Git/SCP-style URIs in the form "user@host:path".
250 // In the general case, these are not equivalent to any "ssh://..." form
251 // because the path is relative.
253 if ($this->isBuiltin()) {
254 $builtin_protocol = $this->getForcedProtocol();
255 $builtin_domain = $this->getForcedHost();
256 $raw_uri = "{$builtin_protocol}://{$builtin_domain}";
257 } else {
258 $raw_uri = $this->getURI();
261 $port = $this->getForcedPort();
263 $default_ports = array(
264 'ssh' => 22,
265 'http' => 80,
266 'https' => 443,
269 $uri = new PhutilURI($raw_uri);
271 // Make sure to remove any password from the URI before we do anything
272 // with it; this should always be provided by the associated credential.
273 $uri->setPass(null);
275 $protocol = $this->getForcedProtocol();
276 if ($protocol) {
277 $uri->setProtocol($protocol);
280 if ($port) {
281 $uri->setPort($port);
284 // Remove any explicitly set default ports.
285 $uri_port = $uri->getPort();
286 $uri_protocol = $uri->getProtocol();
288 $uri_default = idx($default_ports, $uri_protocol);
289 if ($uri_default && ($uri_default == $uri_port)) {
290 $uri->setPort(null);
293 $user = $this->getForcedUser();
294 if ($user) {
295 $uri->setUser($user);
298 $host = $this->getForcedHost();
299 if ($host) {
300 $uri->setDomain($host);
303 $path = $this->getForcedPath();
304 if ($path) {
305 $uri->setPath($path);
308 return $uri;
312 private function getForcedProtocol() {
313 $repository = $this->getRepository();
315 switch ($this->getBuiltinProtocol()) {
316 case self::BUILTIN_PROTOCOL_SSH:
317 if ($repository->isSVN()) {
318 return 'svn+ssh';
319 } else {
320 return 'ssh';
322 case self::BUILTIN_PROTOCOL_HTTP:
323 return 'http';
324 case self::BUILTIN_PROTOCOL_HTTPS:
325 return 'https';
326 default:
327 return null;
331 private function getForcedUser() {
332 switch ($this->getBuiltinProtocol()) {
333 case self::BUILTIN_PROTOCOL_SSH:
334 return AlmanacKeys::getClusterSSHUser();
335 default:
336 return null;
340 private function getForcedHost() {
341 $phabricator_uri = PhabricatorEnv::getURI('/');
342 $phabricator_uri = new PhutilURI($phabricator_uri);
344 $phabricator_host = $phabricator_uri->getDomain();
346 switch ($this->getBuiltinProtocol()) {
347 case self::BUILTIN_PROTOCOL_SSH:
348 $ssh_host = PhabricatorEnv::getEnvConfig('diffusion.ssh-host');
349 if ($ssh_host !== null) {
350 return $ssh_host;
352 return $phabricator_host;
353 case self::BUILTIN_PROTOCOL_HTTP:
354 case self::BUILTIN_PROTOCOL_HTTPS:
355 return $phabricator_host;
356 default:
357 return null;
361 private function getForcedPort() {
362 $protocol = $this->getBuiltinProtocol();
364 if ($protocol == self::BUILTIN_PROTOCOL_SSH) {
365 return PhabricatorEnv::getEnvConfig('diffusion.ssh-port');
368 // If Phabricator is running on a nonstandard port, use that as the default
369 // port for URIs with the same protocol.
371 $is_http = ($protocol == self::BUILTIN_PROTOCOL_HTTP);
372 $is_https = ($protocol == self::BUILTIN_PROTOCOL_HTTPS);
374 if ($is_http || $is_https) {
375 $uri = PhabricatorEnv::getURI('/');
376 $uri = new PhutilURI($uri);
378 $port = $uri->getPort();
379 if (!$port) {
380 return null;
383 $uri_protocol = $uri->getProtocol();
384 $use_port =
385 ($is_http && ($uri_protocol == 'http')) ||
386 ($is_https && ($uri_protocol == 'https'));
388 if (!$use_port) {
389 return null;
392 return $port;
395 return null;
398 private function getForcedPath() {
399 if (!$this->isBuiltin()) {
400 return null;
403 $repository = $this->getRepository();
405 $id = $repository->getID();
406 $callsign = $repository->getCallsign();
407 $short_name = $repository->getRepositorySlug();
409 $clone_name = $repository->getCloneName();
411 if ($repository->isGit()) {
412 $suffix = '.git';
413 } else if ($repository->isHg()) {
414 $suffix = '/';
415 } else {
416 $suffix = '';
417 $clone_name = '';
420 switch ($this->getBuiltinIdentifier()) {
421 case self::BUILTIN_IDENTIFIER_ID:
422 return "/diffusion/{$id}/{$clone_name}{$suffix}";
423 case self::BUILTIN_IDENTIFIER_SHORTNAME:
424 return "/source/{$short_name}{$suffix}";
425 case self::BUILTIN_IDENTIFIER_CALLSIGN:
426 return "/diffusion/{$callsign}/{$clone_name}{$suffix}";
427 default:
428 return null;
432 public function getViewURI() {
433 $id = $this->getID();
434 return $this->getRepository()->getPathURI("uri/view/{$id}/");
437 public function getEditURI() {
438 $id = $this->getID();
439 return $this->getRepository()->getPathURI("uri/edit/{$id}/");
442 public function getAvailableIOTypeOptions() {
443 $options = array(
444 self::IO_DEFAULT,
445 self::IO_NONE,
448 if ($this->isBuiltin()) {
449 $options[] = self::IO_READ;
450 $options[] = self::IO_READWRITE;
451 } else {
452 $options[] = self::IO_OBSERVE;
453 $options[] = self::IO_MIRROR;
456 $map = array();
457 $io_map = self::getIOTypeMap();
458 foreach ($options as $option) {
459 $spec = idx($io_map, $option, array());
461 $label = idx($spec, 'label', $option);
462 $short = idx($spec, 'short');
464 $name = pht('%s: %s', $label, $short);
465 $map[$option] = $name;
468 return $map;
471 public function getAvailableDisplayTypeOptions() {
472 $options = array(
473 self::DISPLAY_DEFAULT,
474 self::DISPLAY_ALWAYS,
475 self::DISPLAY_NEVER,
478 $map = array();
479 $display_map = self::getDisplayTypeMap();
480 foreach ($options as $option) {
481 $spec = idx($display_map, $option, array());
483 $label = idx($spec, 'label', $option);
484 $short = idx($spec, 'short');
486 $name = pht('%s: %s', $label, $short);
487 $map[$option] = $name;
490 return $map;
493 public static function getIOTypeMap() {
494 return array(
495 self::IO_DEFAULT => array(
496 'label' => pht('Default'),
497 'short' => pht('Use default behavior.'),
499 self::IO_OBSERVE => array(
500 'icon' => 'fa-download',
501 'color' => 'green',
502 'label' => pht('Observe'),
503 'note' => pht(
504 'Changes to this URI will be observed and pulled.'),
505 'short' => pht('Copy from a remote.'),
507 self::IO_MIRROR => array(
508 'icon' => 'fa-upload',
509 'color' => 'green',
510 'label' => pht('Mirror'),
511 'note' => pht(
512 'A copy of any changes will be pushed to this URI.'),
513 'short' => pht('Push a copy to a remote.'),
515 self::IO_NONE => array(
516 'icon' => 'fa-times',
517 'color' => 'grey',
518 'label' => pht('No I/O'),
519 'note' => pht(
520 'No changes will be pushed or pulled from this URI.'),
521 'short' => pht('Do not perform any I/O.'),
523 self::IO_READ => array(
524 'icon' => 'fa-folder',
525 'color' => 'blue',
526 'label' => pht('Read Only'),
527 'note' => pht(
528 'A read-only copy of the repository will be served from this URI.'),
529 'short' => pht('Serve repository in read-only mode.'),
531 self::IO_READWRITE => array(
532 'icon' => 'fa-folder-open',
533 'color' => 'blue',
534 'label' => pht('Read/Write'),
535 'note' => pht(
536 'A read/write copy of the repository will be served from this URI.'),
537 'short' => pht('Serve repository in read/write mode.'),
542 public static function getDisplayTypeMap() {
543 return array(
544 self::DISPLAY_DEFAULT => array(
545 'label' => pht('Default'),
546 'short' => pht('Use default behavior.'),
548 self::DISPLAY_ALWAYS => array(
549 'icon' => 'fa-eye',
550 'color' => 'green',
551 'label' => pht('Visible'),
552 'note' => pht('This URI will be shown to users as a clone URI.'),
553 'short' => pht('Show as a clone URI.'),
555 self::DISPLAY_NEVER => array(
556 'icon' => 'fa-eye-slash',
557 'color' => 'grey',
558 'label' => pht('Hidden'),
559 'note' => pht(
560 'This URI will be hidden from users.'),
561 'short' => pht('Do not show as a clone URI.'),
566 public function newCommandEngine() {
567 $repository = $this->getRepository();
569 return DiffusionCommandEngine::newCommandEngine($repository)
570 ->setCredentialPHID($this->getCredentialPHID())
571 ->setURI($this->getEffectiveURI());
574 public function getURIScore() {
575 $score = 0;
577 $io_points = array(
578 self::IO_READWRITE => 200,
579 self::IO_READ => 100,
581 $score += idx($io_points, $this->getEffectiveIOType(), 0);
583 $protocol_points = array(
584 self::BUILTIN_PROTOCOL_SSH => 30,
585 self::BUILTIN_PROTOCOL_HTTPS => 20,
586 self::BUILTIN_PROTOCOL_HTTP => 10,
588 $score += idx($protocol_points, $this->getBuiltinProtocol(), 0);
590 $identifier_points = array(
591 self::BUILTIN_IDENTIFIER_SHORTNAME => 3,
592 self::BUILTIN_IDENTIFIER_CALLSIGN => 2,
593 self::BUILTIN_IDENTIFIER_ID => 1,
595 $score += idx($identifier_points, $this->getBuiltinIdentifier(), 0);
597 return $score;
602 /* -( PhabricatorApplicationTransactionInterface )------------------------- */
605 public function getApplicationTransactionEditor() {
606 return new DiffusionURIEditor();
609 public function getApplicationTransactionTemplate() {
610 return new PhabricatorRepositoryURITransaction();
614 /* -( PhabricatorPolicyInterface )----------------------------------------- */
617 public function getCapabilities() {
618 return array(
619 PhabricatorPolicyCapability::CAN_VIEW,
620 PhabricatorPolicyCapability::CAN_EDIT,
624 public function getPolicy($capability) {
625 switch ($capability) {
626 case PhabricatorPolicyCapability::CAN_VIEW:
627 case PhabricatorPolicyCapability::CAN_EDIT:
628 return PhabricatorPolicies::getMostOpenPolicy();
632 public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
633 return false;
637 /* -( PhabricatorExtendedPolicyInterface )--------------------------------- */
640 public function getExtendedPolicy($capability, PhabricatorUser $viewer) {
641 $extended = array();
643 switch ($capability) {
644 case PhabricatorPolicyCapability::CAN_EDIT:
645 // To edit a repository URI, you must be able to edit the
646 // corresponding repository.
647 $extended[] = array($this->getRepository(), $capability);
648 break;
651 return $extended;
655 /* -( PhabricatorConduitResultInterface )---------------------------------- */
658 public function getFieldSpecificationsForConduit() {
659 return array(
660 id(new PhabricatorConduitSearchFieldSpecification())
661 ->setKey('repositoryPHID')
662 ->setType('phid')
663 ->setDescription(pht('The associated repository PHID.')),
664 id(new PhabricatorConduitSearchFieldSpecification())
665 ->setKey('uri')
666 ->setType('map<string, string>')
667 ->setDescription(pht('The raw and effective URI.')),
668 id(new PhabricatorConduitSearchFieldSpecification())
669 ->setKey('io')
670 ->setType('map<string, const>')
671 ->setDescription(
672 pht('The raw, default, and effective I/O Type settings.')),
673 id(new PhabricatorConduitSearchFieldSpecification())
674 ->setKey('display')
675 ->setType('map<string, const>')
676 ->setDescription(
677 pht('The raw, default, and effective Display Type settings.')),
678 id(new PhabricatorConduitSearchFieldSpecification())
679 ->setKey('credentialPHID')
680 ->setType('phid?')
681 ->setDescription(
682 pht('The associated credential PHID, if one exists.')),
683 id(new PhabricatorConduitSearchFieldSpecification())
684 ->setKey('disabled')
685 ->setType('bool')
686 ->setDescription(pht('True if the URI is disabled.')),
687 id(new PhabricatorConduitSearchFieldSpecification())
688 ->setKey('builtin')
689 ->setType('map<string, string>')
690 ->setDescription(
691 pht('Information about builtin URIs.')),
692 id(new PhabricatorConduitSearchFieldSpecification())
693 ->setKey('dateCreated')
694 ->setType('int')
695 ->setDescription(
696 pht('Epoch timestamp when the object was created.')),
697 id(new PhabricatorConduitSearchFieldSpecification())
698 ->setKey('dateModified')
699 ->setType('int')
700 ->setDescription(
701 pht('Epoch timestamp when the object was last updated.')),
705 public function getFieldValuesForConduit() {
706 return array(
707 'repositoryPHID' => $this->getRepositoryPHID(),
708 'uri' => array(
709 'raw' => $this->getURI(),
710 'display' => (string)$this->getDisplayURI(),
711 'effective' => (string)$this->getEffectiveURI(),
712 'normalized' => (string)$this->getNormalizedURI(),
714 'io' => array(
715 'raw' => $this->getIOType(),
716 'default' => $this->getDefaultIOType(),
717 'effective' => $this->getEffectiveIOType(),
719 'display' => array(
720 'raw' => $this->getDisplayType(),
721 'default' => $this->getDefaultDisplayType(),
722 'effective' => $this->getEffectiveDisplayType(),
724 'credentialPHID' => $this->getCredentialPHID(),
725 'disabled' => (bool)$this->getIsDisabled(),
726 'builtin' => array(
727 'protocol' => $this->getBuiltinProtocol(),
728 'identifier' => $this->getBuiltinIdentifier(),
730 'dateCreated' => $this->getDateCreated(),
731 'dateModified' => $this->getDateModified(),
735 public function getConduitSearchAttachments() {
736 return array();
739 public static function getURINormalizerDomainMap() {
740 $domain_map = array();
742 // See T13435. If the domain for a repository URI is same as the install
743 // base URI, store it as a "<base-uri>" token instead of the actual domain
744 // so that the index does not fall out of date if the install moves.
746 $base_uri = PhabricatorEnv::getURI('/');
747 $base_uri = new PhutilURI($base_uri);
748 $base_domain = $base_uri->getDomain();
749 $domain_map['<base-uri>'] = $base_domain;
751 // Likewise, store a token for the "SSH Host" domain so it can be changed
752 // without requiring an index rebuild.
754 $ssh_host = PhabricatorEnv::getEnvConfig('diffusion.ssh-host');
755 if ($ssh_host !== null && strlen($ssh_host)) {
756 $domain_map['<ssh-host>'] = $ssh_host;
759 return $domain_map;