diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -789,6 +789,7 @@ 'DiffusionRepositorySymbolsManagementPanel' => 'applications/diffusion/management/DiffusionRepositorySymbolsManagementPanel.php', 'DiffusionRepositoryTag' => 'applications/diffusion/data/DiffusionRepositoryTag.php', 'DiffusionRepositoryTestAutomationController' => 'applications/diffusion/controller/DiffusionRepositoryTestAutomationController.php', + 'DiffusionRepositoryURICredentialController' => 'applications/diffusion/controller/DiffusionRepositoryURICredentialController.php', 'DiffusionRepositoryURIDisableController' => 'applications/diffusion/controller/DiffusionRepositoryURIDisableController.php', 'DiffusionRepositoryURIEditController' => 'applications/diffusion/controller/DiffusionRepositoryURIEditController.php', 'DiffusionRepositoryURIViewController' => 'applications/diffusion/controller/DiffusionRepositoryURIViewController.php', @@ -5020,6 +5021,7 @@ 'DiffusionRepositorySymbolsManagementPanel' => 'DiffusionRepositoryManagementPanel', 'DiffusionRepositoryTag' => 'Phobject', 'DiffusionRepositoryTestAutomationController' => 'DiffusionRepositoryEditController', + 'DiffusionRepositoryURICredentialController' => 'DiffusionController', 'DiffusionRepositoryURIDisableController' => 'DiffusionController', 'DiffusionRepositoryURIEditController' => 'DiffusionController', 'DiffusionRepositoryURIViewController' => 'DiffusionController', diff --git a/src/applications/diffusion/application/PhabricatorDiffusionApplication.php b/src/applications/diffusion/application/PhabricatorDiffusionApplication.php --- a/src/applications/diffusion/application/PhabricatorDiffusionApplication.php +++ b/src/applications/diffusion/application/PhabricatorDiffusionApplication.php @@ -97,6 +97,8 @@ => 'DiffusionRepositoryURIDisableController', $this->getEditRoutePattern('edit/') => 'DiffusionRepositoryURIEditController', + 'credential/(?P[0-9]\d*)/(?Pedit|remove)/' + => 'DiffusionRepositoryURICredentialController', ), 'edit/' => array( '' => 'DiffusionRepositoryEditMainController', diff --git a/src/applications/diffusion/controller/DiffusionMirrorEditController.php b/src/applications/diffusion/controller/DiffusionMirrorEditController.php --- a/src/applications/diffusion/controller/DiffusionMirrorEditController.php +++ b/src/applications/diffusion/controller/DiffusionMirrorEditController.php @@ -108,7 +108,7 @@ ->setName('remoteURI') ->setValue($v_remote) ->setError($e_remote)) - ->appendChild( + ->appendControl( id(new PassphraseCredentialControl()) ->setLabel(pht('Credentials')) ->setName('credential') diff --git a/src/applications/diffusion/controller/DiffusionRepositoryCreateController.php b/src/applications/diffusion/controller/DiffusionRepositoryCreateController.php --- a/src/applications/diffusion/controller/DiffusionRepositoryCreateController.php +++ b/src/applications/diffusion/controller/DiffusionRepositoryCreateController.php @@ -510,6 +510,7 @@ ->setAdjustFormPageCallback(array($this, 'adjustAuthPage')) ->addControl( id(new PassphraseCredentialControl()) + ->setViewer($this->getViewer()) ->setName('credential')); } diff --git a/src/applications/diffusion/controller/DiffusionRepositoryURICredentialController.php b/src/applications/diffusion/controller/DiffusionRepositoryURICredentialController.php new file mode 100644 --- /dev/null +++ b/src/applications/diffusion/controller/DiffusionRepositoryURICredentialController.php @@ -0,0 +1,159 @@ +loadDiffusionContextForEdit(); + if ($response) { + return $response; + } + + $viewer = $this->getViewer(); + $drequest = $this->getDiffusionRequest(); + $repository = $drequest->getRepository(); + + $id = $request->getURIData('id'); + $uri = id(new PhabricatorRepositoryURIQuery()) + ->setViewer($viewer) + ->withIDs(array($id)) + ->withRepositories(array($repository)) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->executeOne(); + if (!$uri) { + return new Aphront404Response(); + } + + $is_builtin = $uri->isBuiltin(); + $has_credential = (bool)$uri->getCredentialPHID(); + $view_uri = $uri->getViewURI(); + $is_remove = ($request->getURIData('action') == 'remove'); + + if ($is_builtin) { + return $this->newDialog() + ->setTitle(pht('Builtin URIs Do Not Use Credentials')) + ->appendParagraph( + pht( + 'You can not set a credential for builtin URIs which Phabricator '. + 'hosts and serves. Phabricator does not fetch from these URIs or '. + 'push to these URIs, and does not need credentials to '. + 'authenticate any activity against them.')) + ->addCancelButton($view_uri); + } + + if ($request->isFormPost()) { + $xactions = array(); + + if ($is_remove) { + $new_phid = null; + } else { + $new_phid = $request->getStr('credentialPHID'); + } + + $type_credential = PhabricatorRepositoryURITransaction::TYPE_CREDENTIAL; + + $xactions[] = id(new PhabricatorRepositoryURITransaction()) + ->setTransactionType($type_credential) + ->setNewValue($new_phid); + + $editor = id(new DiffusionURIEditor()) + ->setActor($viewer) + ->setContinueOnNoEffect(true) + ->setContinueOnMissingFields(true) + ->setContentSourceFromRequest($request) + ->applyTransactions($uri, $xactions); + + return id(new AphrontRedirectResponse())->setURI($view_uri); + } + + $command_engine = $uri->newCommandEngine(); + $is_supported = $command_engine->isCredentialSupported(); + + $body = null; + $form = null; + $width = AphrontDialogView::WIDTH_DEFAULT; + if ($is_remove) { + if ($has_credential) { + $title = pht('Remove Credential'); + $body = pht( + 'This credential will no longer be used to authenticate activity '. + 'against this URI.'); + $button = pht('Remove Credential'); + } else { + $title = pht('No Credential'); + $body = pht( + 'This URI does not have an associated credential.'); + $button = null; + } + } else if (!$is_supported) { + $title = pht('Unauthenticated Protocol'); + $body = pht( + 'The protocol for this URI ("%s") does not use authentication, so '. + 'you can not provide a credential.', + $command_engine->getDisplayProtocol()); + $button = null; + } else { + $effective_uri = $uri->getEffectiveURI(); + + $label = $command_engine->getPassphraseCredentialLabel(); + $credential_type = $command_engine->getPassphraseDefaultCredentialType(); + + $provides_type = $command_engine->getPassphraseProvidesCredentialType(); + $options = id(new PassphraseCredentialQuery()) + ->setViewer($viewer) + ->withIsDestroyed(false) + ->withProvidesTypes(array($provides_type)) + ->execute(); + + $control = id(new PassphraseCredentialControl()) + ->setName('credentialPHID') + ->setLabel($label) + ->setValue($uri->getCredentialPHID()) + ->setCredentialType($credential_type) + ->setOptions($options); + + $default_user = $effective_uri->getUser(); + if (strlen($default_user)) { + $control->setDefaultUsername($default_user); + } + + $form = id(new AphrontFormView()) + ->setViewer($viewer) + ->appendControl($control); + + if ($has_credential) { + $title = pht('Update Credential'); + $button = pht('Update Credential'); + } else { + $title = pht('Set Credential'); + $button = pht('Set Credential'); + } + + $width = AphrontDialogView::WIDTH_FORM; + } + + $dialog = $this->newDialog() + ->setWidth($width) + ->setTitle($title) + ->addCancelButton($view_uri); + + if ($body) { + $dialog->appendParagraph($body); + } + + if ($form) { + $dialog->appendForm($form); + } + + if ($button) { + $dialog->addSubmitButton($button); + } + + return $dialog; + } + +} diff --git a/src/applications/diffusion/controller/DiffusionRepositoryURIViewController.php b/src/applications/diffusion/controller/DiffusionRepositoryURIViewController.php --- a/src/applications/diffusion/controller/DiffusionRepositoryURIViewController.php +++ b/src/applications/diffusion/controller/DiffusionRepositoryURIViewController.php @@ -82,6 +82,7 @@ private function buildCurtain(PhabricatorRepositoryURI $uri) { $viewer = $this->getViewer(); + $repository = $uri->getRepository(); $id = $uri->getID(); $can_edit = PhabricatorPolicyFilter::hasCapability( @@ -89,7 +90,6 @@ $uri, PhabricatorPolicyCapability::CAN_EDIT); - $curtain = $this->newCurtainView($uri); $edit_uri = $uri->getEditURI(); @@ -102,6 +102,43 @@ ->setWorkflow(!$can_edit) ->setDisabled(!$can_edit)); + $credential_uri = $repository->getPathURI("uri/credential/{$id}/edit/"); + $remove_uri = $repository->getPathURI("uri/credential/{$id}/remove/"); + $has_credential = (bool)$uri->getCredentialPHID(); + + if ($uri->isBuiltin()) { + $can_credential = false; + } else if (!$uri->newCommandEngine()->isCredentialSupported()) { + $can_credential = false; + } else { + $can_credential = true; + } + + $can_update = ($can_edit && $can_credential); + $can_remove = ($can_edit && $has_credential); + + if ($has_credential) { + $credential_name = pht('Update Credential'); + } else { + $credential_name = pht('Set Credential'); + } + + $curtain->addAction( + id(new PhabricatorActionView()) + ->setIcon('fa-key') + ->setName($credential_name) + ->setHref($credential_uri) + ->setWorkflow(true) + ->setDisabled(!$can_edit)); + + $curtain->addAction( + id(new PhabricatorActionView()) + ->setIcon('fa-times') + ->setName(pht('Remove Credential')) + ->setHref($remove_uri) + ->setWorkflow(true) + ->setDisabled(!$can_remove)); + if ($uri->getIsDisabled()) { $disable_name = pht('Enable URI'); $disable_icon = 'fa-check'; @@ -110,7 +147,7 @@ $disable_icon = 'fa-ban'; } - $disable_uri = $uri->getRepository()->getPathURI("uri/disable/{$id}/"); + $disable_uri = $repository->getPathURI("uri/disable/{$id}/"); $curtain->addAction( id(new PhabricatorActionView()) @@ -130,7 +167,84 @@ ->setUser($viewer); $properties->addProperty(pht('URI'), $uri->getDisplayURI()); - $properties->addProperty(pht('Credential'), 'TODO'); + + $credential_phid = $uri->getCredentialPHID(); + $command_engine = $uri->newCommandEngine(); + $is_optional = $command_engine->isCredentialOptional(); + $is_supported = $command_engine->isCredentialSupported(); + $is_builtin = $uri->isBuiltin(); + + if ($is_builtin) { + $credential_icon = 'fa-circle-o'; + $credential_color = 'grey'; + $credential_label = pht('Builtin'); + $credential_note = pht('Builtin URIs do not use credentials.'); + } else if (!$is_supported) { + $credential_icon = 'fa-circle-o'; + $credential_color = 'grey'; + $credential_label = pht('Not Supported'); + $credential_note = pht('This protocol does not support authentication.'); + } else if (!$credential_phid) { + if ($is_optional) { + $credential_icon = 'fa-circle-o'; + $credential_color = 'green'; + $credential_label = pht('No Credential'); + $credential_note = pht('Configured for anonymous access.'); + } else { + $credential_icon = 'fa-times'; + $credential_color = 'red'; + $credential_label = pht('Required'); + $credential_note = pht('Credential required but not configured.'); + } + } else { + // Don't raise a policy exception if we can't see the credential. + $credentials = id(new PassphraseCredentialQuery()) + ->setViewer($viewer) + ->withPHIDs(array($credential_phid)) + ->execute(); + $credential = head($credentials); + + if (!$credential) { + $handles = $viewer->loadHandles(array($credential_phid)); + $handle = $handles[$credential_phid]; + if ($handle->getPolicyFiltered()) { + $credential_icon = 'fa-lock'; + $credential_color = 'grey'; + $credential_label = pht('Restricted'); + $credential_note = pht( + 'You do not have permission to view the configured '. + 'credential.'); + } else { + $credential_icon = 'fa-times'; + $credential_color = 'red'; + $credential_label = pht('Invalid'); + $credential_note = pht('Configured credential is invalid.'); + } + } else { + $provides = $credential->getProvidesType(); + $needs = $command_engine->getPassphraseProvidesCredentialType(); + if ($provides != $needs) { + $credential_icon = 'fa-times'; + $credential_color = 'red'; + $credential_label = pht('Wrong Type'); + } else { + $credential_icon = 'fa-check'; + $credential_color = 'green'; + $credential_label = $command_engine->getPassphraseCredentialLabel(); + } + $credential_note = $viewer->renderHandle($credential_phid); + } + } + + $credential_item = id(new PHUIStatusItemView()) + ->setIcon($credential_icon, $credential_color) + ->setTarget(phutil_tag('strong', array(), $credential_label)) + ->setNote($credential_note); + + $credential_view = id(new PHUIStatusListView()) + ->addItem($credential_item); + + $properties->addProperty(pht('Credential'), $credential_view); $io_type = $uri->getEffectiveIOType(); diff --git a/src/applications/diffusion/editor/DiffusionURIEditEngine.php b/src/applications/diffusion/editor/DiffusionURIEditEngine.php --- a/src/applications/diffusion/editor/DiffusionURIEditEngine.php +++ b/src/applications/diffusion/editor/DiffusionURIEditEngine.php @@ -83,6 +83,14 @@ protected function buildCustomEditFields($object) { $viewer = $this->getViewer(); + if ($object->isBuiltin()) { + $is_builtin = true; + $uri_value = (string)$object->getDisplayURI(); + } else { + $is_builtin = false; + $uri_value = $object->getURI(); + } + return array( id(new PhabricatorHandlesEditField()) ->setKey('repository') @@ -104,12 +112,13 @@ id(new PhabricatorTextEditField()) ->setKey('uri') ->setLabel(pht('URI')) - ->setIsRequired(true) ->setTransactionType(PhabricatorRepositoryURITransaction::TYPE_URI) ->setDescription(pht('The repository URI.')) ->setConduitDescription(pht('Change the repository URI.')) ->setConduitTypeDescription(pht('New repository URI.')) - ->setValue($object->getURI()), + ->setIsRequired(!$is_builtin) + ->setIsLocked($is_builtin) + ->setValue($uri_value), id(new PhabricatorSelectEditField()) ->setKey('io') ->setLabel(pht('I/O Type')) diff --git a/src/applications/diffusion/editor/DiffusionURIEditor.php b/src/applications/diffusion/editor/DiffusionURIEditor.php --- a/src/applications/diffusion/editor/DiffusionURIEditor.php +++ b/src/applications/diffusion/editor/DiffusionURIEditor.php @@ -70,7 +70,42 @@ switch ($xaction->getTransactionType()) { case PhabricatorRepositoryURITransaction::TYPE_URI: + if (!$this->getIsNewObject()) { + $old_uri = $object->getEffectiveURI(); + } else { + $old_uri = null; + } + $object->setURI($xaction->getNewValue()); + + // If we've changed the domain or protocol of the URI, remove the + // current credential. This improves behavior in several cases: + + // If a user switches between protocols with different credential + // types, like HTTP and SSH, the old credential won't be valid anyway. + // It's cleaner to remove it than leave a bad credential in place. + + // If a user switches hosts, the old credential is probably not + // correct (and potentially confusing/misleading). Removing it forces + // users to double check that they have the correct credentials. + + // If an attacker can't see a symmetric credential like a username and + // password, they could still potentially capture it by changing the + // host for a URI that uses it to `evil.com`, a server they control, + // then observing the requests. Removing the credential prevents this + // kind of escalation. + + // Since port and path changes are less likely to fall among these + // cases, they don't trigger a credential wipe. + + $new_uri = $object->getEffectiveURI(); + if ($old_uri) { + $new_proto = ($old_uri->getProtocol() != $new_uri->getProtocol()); + $new_domain = ($old_uri->getDomain() != $new_uri->getDomain()); + if ($new_proto || $new_domain) { + $object->setCredentialPHID(null); + } + } break; case PhabricatorRepositoryURITransaction::TYPE_IO: $object->setIOType($xaction->getNewValue()); @@ -184,6 +219,11 @@ continue; } + // Anyone who can edit a URI can remove the credential. + if ($credential_phid === null) { + continue; + } + $credential = id(new PassphraseCredentialQuery()) ->setViewer($viewer) ->withPHIDs(array($credential_phid)) diff --git a/src/applications/diffusion/protocol/DiffusionCommandEngine.php b/src/applications/diffusion/protocol/DiffusionCommandEngine.php --- a/src/applications/diffusion/protocol/DiffusionCommandEngine.php +++ b/src/applications/diffusion/protocol/DiffusionCommandEngine.php @@ -57,6 +57,10 @@ return $this->protocol; } + public function getDisplayProtocol() { + return $this->getProtocol().'://'; + } + public function setCredentialPHID($credential_phid) { $this->credentialPHID = $credential_phid; return $this; @@ -197,34 +201,82 @@ return $env; } - protected function isSSHProtocol() { + public function isSSHProtocol() { return ($this->getProtocol() == 'ssh'); } - protected function isSVNProtocol() { + public function isSVNProtocol() { return ($this->getProtocol() == 'svn'); } - protected function isSVNSSHProtocol() { + public function isSVNSSHProtocol() { return ($this->getProtocol() == 'svn+ssh'); } - protected function isHTTPProtocol() { + public function isHTTPProtocol() { return ($this->getProtocol() == 'http'); } - protected function isHTTPSProtocol() { + public function isHTTPSProtocol() { return ($this->getProtocol() == 'https'); } - protected function isAnyHTTPProtocol() { + public function isAnyHTTPProtocol() { return ($this->isHTTPProtocol() || $this->isHTTPSProtocol()); } - protected function isAnySSHProtocol() { + public function isAnySSHProtocol() { return ($this->isSSHProtocol() || $this->isSVNSSHProtocol()); } + public function isCredentialSupported() { + return ($this->getPassphraseProvidesCredentialType() !== null); + } + + public function isCredentialOptional() { + if ($this->isAnySSHProtocol()) { + return false; + } + + return true; + } + + public function getPassphraseCredentialLabel() { + if ($this->isAnySSHProtocol()) { + return pht('SSH Key'); + } + + if ($this->isAnyHTTPProtocol() || $this->isSVNProtocol()) { + return pht('Password'); + } + + return null; + } + + public function getPassphraseDefaultCredentialType() { + if ($this->isAnySSHProtocol()) { + return PassphraseSSHPrivateKeyTextCredentialType::CREDENTIAL_TYPE; + } + + if ($this->isAnyHTTPProtocol() || $this->isSVNProtocol()) { + return PassphrasePasswordCredentialType::CREDENTIAL_TYPE; + } + + return null; + } + + public function getPassphraseProvidesCredentialType() { + if ($this->isAnySSHProtocol()) { + return PassphraseSSHPrivateKeyCredentialType::PROVIDES_TYPE; + } + + if ($this->isAnyHTTPProtocol() || $this->isSVNProtocol()) { + return PassphrasePasswordCredentialType::PROVIDES_TYPE; + } + + return null; + } + protected function getSSHWrapper() { $root = dirname(phutil_get_library_root('phabricator')); return $root.'/bin/ssh-connect'; diff --git a/src/applications/passphrase/view/PassphraseCredentialControl.php b/src/applications/passphrase/view/PassphraseCredentialControl.php --- a/src/applications/passphrase/view/PassphraseCredentialControl.php +++ b/src/applications/passphrase/view/PassphraseCredentialControl.php @@ -42,10 +42,50 @@ foreach ($this->options as $option) { $options_map[$option->getPHID()] = pht( '%s %s', - 'K'.$option->getID(), + $option->getMonogram(), $option->getName()); } + // The user editing the form may not have permission to see the current + // credential. Populate it into the menu to allow them to save the form + // without making any changes. + $current_phid = $this->getValue(); + if (strlen($current_phid) && empty($options_map[$current_phid])) { + $viewer = $this->getViewer(); + + $user_credential = id(new PassphraseCredentialQuery()) + ->setViewer($viewer) + ->withPHIDs(array($current_phid)) + ->executeOne(); + if (!$user_credential) { + // Pull the credential with the ominipotent viewer so we can look up + // the ID and tell if it's restricted or invalid. + $omnipotent_credential = id(new PassphraseCredentialQuery()) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->withPHIDs(array($current_phid)) + ->executeOne(); + if ($omnipotent_credential) { + $current_name = pht( + '%s (Restricted Credential)', + $omnipotent_credential->getMonogram()); + } else { + $current_name = pht( + 'Invalid Credential ("%s")', + $current_phid); + } + } else { + $current_name = pht( + '%s %s', + $user_credential->getMonogram(), + $user_credential->getName()); + } + + $options_map = array( + $current_phid => $current_name, + ) + $options_map; + } + + $disabled = $this->getDisabled(); if ($this->allowNull) { $options_map = array('' => pht('(No Credentials)')) + $options_map; diff --git a/src/applications/repository/storage/PhabricatorRepositoryURI.php b/src/applications/repository/storage/PhabricatorRepositoryURI.php --- a/src/applications/repository/storage/PhabricatorRepositoryURI.php +++ b/src/applications/repository/storage/PhabricatorRepositoryURI.php @@ -193,11 +193,73 @@ public function getDisplayURI() { - $uri = new PhutilURI($this->getURI()); + return $this->getURIObject(false); + } + + public function getEffectiveURI() { + return $this->getURIObject(true); + } + + private function getURIObject($normalize) { + // Users can provide Git/SCP-style URIs in the form "user@host:path". + // These are equivalent to "ssh://user@host/path". We use the more standard + // form internally, and convert to it if we need to specify a port number, + // but try to preserve what the user typed when displaying the URI. + + if ($this->isBuiltin()) { + $builtin_protocol = $this->getForcedProtocol(); + $builtin_domain = $this->getForcedHost(); + $raw_uri = "{$builtin_protocol}://{$builtin_domain}"; + } else { + $raw_uri = $this->getURI(); + } + + $port = $this->getForcedPort(); + + $default_ports = array( + 'ssh' => 22, + 'http' => 80, + 'https' => 443, + ); + + $uri = new PhutilURI($raw_uri); + if (!$uri->getProtocol()) { + $git_uri = new PhutilGitURI($raw_uri); + + // We must normalize this Git-style URI into a normal URI + $must_normalize = ($port && ($port != $default_ports['ssh'])); + if ($must_normalize || $normalize) { + $domain = $git_uri->getDomain(); - $protocol = $this->getForcedProtocol(); - if ($protocol) { - $uri->setProtocol($protocol); + + $uri = id(new PhutilURI("ssh://{$domain}")) + ->setUser($git_uri->getUser()) + ->setPath($git_uri->getPath()); + } else { + $uri = $git_uri; + } + } + + $is_normal = ($uri instanceof PhutilURI); + + if ($is_normal) { + $protocol = $this->getForcedProtocol(); + if ($protocol) { + $uri->setProtocol($protocol); + } + + if ($port) { + $uri->setPort($port); + } + + // Remove any explicitly set default ports. + $uri_port = $uri->getPort(); + $uri_protocol = $uri->getProtocol(); + + $uri_default = idx($default_ports, $uri_protocol); + if ($uri_default && ($uri_default == $uri_port)) { + $uri->setPort(null); + } } $user = $this->getForcedUser(); @@ -210,11 +272,6 @@ $uri->setDomain($host); } - $port = $this->getForcedPort(); - if ($port) { - $uri->setPort($port); - } - $path = $this->getForcedPath(); if ($path) { $uri->setPath($path); @@ -223,6 +280,7 @@ return $uri; } + private function getForcedProtocol() { switch ($this->getBuiltinProtocol()) { case self::BUILTIN_PROTOCOL_SSH: @@ -446,6 +504,16 @@ ); } + public function newCommandEngine() { + $repository = $this->getRepository(); + $protocol = $this->getEffectiveURI()->getProtocol(); + + return DiffusionCommandEngine::newCommandEngine($repository) + ->setCredentialPHID($this->getCredentialPHID()) + ->setProtocol($protocol); + } + + /* -( PhabricatorApplicationTransactionInterface )------------------------- */ @@ -569,7 +637,8 @@ 'repositoryPHID' => $this->getRepositoryPHID(), 'uri' => array( 'raw' => $this->getURI(), - 'effective' => (string)$this->getDisplayURI(), + 'display' => (string)$this->getDisplayURI(), + 'effective' => (string)$this->getEffectiveURI(), ), 'io' => array( 'raw' => $this->getIOType(), diff --git a/src/applications/repository/storage/PhabricatorRepositoryURITransaction.php b/src/applications/repository/storage/PhabricatorRepositoryURITransaction.php --- a/src/applications/repository/storage/PhabricatorRepositoryURITransaction.php +++ b/src/applications/repository/storage/PhabricatorRepositoryURITransaction.php @@ -18,6 +18,26 @@ return PhabricatorRepositoryURIPHIDType::TYPECONST; } + public function getRequiredHandlePHIDs() { + $phids = parent::getRequiredHandlePHIDs(); + + $old = $this->getOldValue(); + $new = $this->getNewValue(); + + switch ($this->getTransactionType()) { + case self::TYPE_CREDENTIAL: + if ($old) { + $phids[] = $old; + } + if ($new) { + $phids[] = $new; + } + break; + } + + return $phids; + } + public function getTitle() { $author_phid = $this->getAuthorPHID(); @@ -61,6 +81,24 @@ '%s enabled this URI.', $this->renderHandleLink($author_phid)); } + case self::TYPE_CREDENTIAL: + if ($old && $new) { + return pht( + '%s changed the credential for this URI from %s to %s.', + $this->renderHandleLink($author_phid), + $this->renderHandleLink($old), + $this->renderHandleLink($new)); + } else if ($old) { + return pht( + '%s removed %s as the credential for this URI.', + $this->renderHandleLink($author_phid), + $this->renderHandleLink($old)); + } else if ($new) { + return pht( + '%s set the credential for this URI to %s.', + $this->renderHandleLink($author_phid), + $this->renderHandleLink($new)); + } } diff --git a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldCredential.php b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldCredential.php --- a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldCredential.php +++ b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldCredential.php @@ -34,6 +34,7 @@ ->execute(); return id(new PassphraseCredentialControl()) + ->setViewer($this->getViewer()) ->setLabel($this->getFieldName()) ->setName($this->getFieldKey()) ->setCaption($this->getCaption())