diff --git a/src/applications/diffusion/controller/DiffusionRepositoryEditUpdateController.php b/src/applications/diffusion/controller/DiffusionRepositoryEditUpdateController.php index dd95654302..88ce864f9a 100644 --- a/src/applications/diffusion/controller/DiffusionRepositoryEditUpdateController.php +++ b/src/applications/diffusion/controller/DiffusionRepositoryEditUpdateController.php @@ -1,68 +1,68 @@ loadDiffusionContextForEdit(); if ($response) { return $response; } $viewer = $this->getViewer(); $drequest = $this->getDiffusionRequest(); $repository = $drequest->getRepository(); $panel_uri = id(new DiffusionRepositoryBasicsManagementPanel()) ->setRepository($repository) ->getPanelURI(); if ($request->isFormPost()) { $params = array( 'repositories' => array( $repository->getPHID(), ), ); id(new ConduitCall('diffusion.looksoon', $params)) ->setUser($viewer) ->execute(); return id(new AphrontRedirectResponse())->setURI($panel_uri); } $doc_name = 'Diffusion User Guide: Repository Updates'; $doc_href = PhabricatorEnv::getDoclink($doc_name); $doc_link = phutil_tag( 'a', array( 'href' => $doc_href, 'target' => '_blank', ), $doc_name); return $this->newDialog() ->setTitle(pht('Update Repository Now')) ->appendParagraph( pht( - 'Normally, Phabricator automatically updates repositories '. + 'Normally, repositories are automatically updated '. 'based on how much time has elapsed since the last commit. '. 'This helps reduce load if you have a large number of mostly '. 'inactive repositories, which is common.')) ->appendParagraph( pht( 'You can manually schedule an update for this repository. The '. 'daemons will perform the update as soon as possible. This may '. 'be helpful if you have just made a commit to a rarely used '. 'repository.')) ->appendParagraph( pht( - 'To learn more about how Phabricator updates repositories, '. + 'To learn more about how repositories are updated, '. 'read %s in the documentation.', $doc_link)) ->addCancelButton($panel_uri) ->addSubmitButton(pht('Schedule Update')); } } diff --git a/src/applications/diffusion/controller/DiffusionRepositoryURICredentialController.php b/src/applications/diffusion/controller/DiffusionRepositoryURICredentialController.php index 96b97673ce..63763a6c04 100644 --- a/src/applications/diffusion/controller/DiffusionRepositoryURICredentialController.php +++ b/src/applications/diffusion/controller/DiffusionRepositoryURICredentialController.php @@ -1,159 +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.')) + 'You can not set a credential for builtin URIs which this '. + 'server hosts. These URIs are not fetched from or pushed to, '. + 'and credentials are not required 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/DiffusionServeController.php b/src/applications/diffusion/controller/DiffusionServeController.php index d0d897e1f6..3040328fbf 100644 --- a/src/applications/diffusion/controller/DiffusionServeController.php +++ b/src/applications/diffusion/controller/DiffusionServeController.php @@ -1,1303 +1,1303 @@ getRequest()->setUser($viewer); $this->serviceViewer = $viewer; return $this; } public function getServiceViewer() { return $this->serviceViewer; } public function setServiceRepository(PhabricatorRepository $repository) { $this->serviceRepository = $repository; return $this; } public function getServiceRepository() { return $this->serviceRepository; } public function getIsGitLFSRequest() { return $this->isGitLFSRequest; } public function getGitLFSToken() { return $this->gitLFSToken; } public function isVCSRequest(AphrontRequest $request) { $identifier = $this->getRepositoryIdentifierFromRequest($request); if ($identifier === null) { return null; } $content_type = $request->getHTTPHeader('Content-Type'); $user_agent = idx($_SERVER, 'HTTP_USER_AGENT'); $request_type = $request->getHTTPHeader('X-Phabricator-Request-Type'); // This may have a "charset" suffix, so only match the prefix. $lfs_pattern = '(^application/vnd\\.git-lfs\\+json(;|\z))'; $vcs = null; if ($request->getExists('service')) { $service = $request->getStr('service'); // We get this initially for `info/refs`. // Git also gives us a User-Agent like "git/1.8.2.3". $vcs = PhabricatorRepositoryType::REPOSITORY_TYPE_GIT; } else if (strncmp($user_agent, 'git/', 4) === 0) { $vcs = PhabricatorRepositoryType::REPOSITORY_TYPE_GIT; } else if ($content_type == 'application/x-git-upload-pack-request') { // We get this for `git-upload-pack`. $vcs = PhabricatorRepositoryType::REPOSITORY_TYPE_GIT; } else if ($content_type == 'application/x-git-receive-pack-request') { // We get this for `git-receive-pack`. $vcs = PhabricatorRepositoryType::REPOSITORY_TYPE_GIT; } else if (preg_match($lfs_pattern, $content_type)) { // This is a Git LFS HTTP API request. $vcs = PhabricatorRepositoryType::REPOSITORY_TYPE_GIT; $this->isGitLFSRequest = true; } else if ($request_type == 'git-lfs') { // This is a Git LFS object content request. $vcs = PhabricatorRepositoryType::REPOSITORY_TYPE_GIT; $this->isGitLFSRequest = true; } else if ($request->getExists('cmd')) { // Mercurial also sends an Accept header like // "application/mercurial-0.1", and a User-Agent like // "mercurial/proto-1.0". $vcs = PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL; } else { // Subversion also sends an initial OPTIONS request (vs GET/POST), and // has a User-Agent like "SVN/1.8.3 (x86_64-apple-darwin11.4.2) // serf/1.3.2". $dav = $request->getHTTPHeader('DAV'); $dav = new PhutilURI($dav); if ($dav->getDomain() === 'subversion.tigris.org') { $vcs = PhabricatorRepositoryType::REPOSITORY_TYPE_SVN; } } return $vcs; } public function handleRequest(AphrontRequest $request) { $service_exception = null; $response = null; try { $response = $this->serveRequest($request); } catch (Exception $ex) { $service_exception = $ex; } try { $remote_addr = $request->getRemoteAddress(); if ($request->isHTTPS()) { $remote_protocol = PhabricatorRepositoryPullEvent::PROTOCOL_HTTPS; } else { $remote_protocol = PhabricatorRepositoryPullEvent::PROTOCOL_HTTP; } $pull_event = id(new PhabricatorRepositoryPullEvent()) ->setEpoch(PhabricatorTime::getNow()) ->setRemoteAddress($remote_addr) ->setRemoteProtocol($remote_protocol); if ($response) { $response_code = $response->getHTTPResponseCode(); if ($response_code == 200) { $pull_event ->setResultType(PhabricatorRepositoryPullEvent::RESULT_PULL) ->setResultCode($response_code); } else { $pull_event ->setResultType(PhabricatorRepositoryPullEvent::RESULT_ERROR) ->setResultCode($response_code); } if ($response instanceof PhabricatorVCSResponse) { $pull_event->setProperties( array( 'response.message' => $response->getMessage(), )); } } else { $pull_event ->setResultType(PhabricatorRepositoryPullEvent::RESULT_EXCEPTION) ->setResultCode(500) ->setProperties( array( 'exception.class' => get_class($ex), 'exception.message' => $ex->getMessage(), )); } $viewer = $this->getServiceViewer(); if ($viewer) { $pull_event->setPullerPHID($viewer->getPHID()); } $repository = $this->getServiceRepository(); if ($repository) { $pull_event->setRepositoryPHID($repository->getPHID()); } $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); $pull_event->save(); unset($unguarded); } catch (Exception $ex) { if ($service_exception) { throw $service_exception; } throw $ex; } if ($service_exception) { throw $service_exception; } return $response; } private function serveRequest(AphrontRequest $request) { $identifier = $this->getRepositoryIdentifierFromRequest($request); // If authentication credentials have been provided, try to find a user // that actually matches those credentials. // We require both the username and password to be nonempty, because Git // won't prompt users who provide a username but no password otherwise. // See T10797 for discussion. $have_user = strlen(idx($_SERVER, 'PHP_AUTH_USER')); $have_pass = strlen(idx($_SERVER, 'PHP_AUTH_PW')); if ($have_user && $have_pass) { $username = $_SERVER['PHP_AUTH_USER']; $password = new PhutilOpaqueEnvelope($_SERVER['PHP_AUTH_PW']); // Try Git LFS auth first since we can usually reject it without doing // any queries, since the username won't match the one we expect or the // request won't be LFS. $viewer = $this->authenticateGitLFSUser( $username, $password, $identifier); // If that failed, try normal auth. Note that we can use normal auth on // LFS requests, so this isn't strictly an alternative to LFS auth. if (!$viewer) { $viewer = $this->authenticateHTTPRepositoryUser($username, $password); } if (!$viewer) { return new PhabricatorVCSResponse( 403, pht('Invalid credentials.')); } } else { // User hasn't provided credentials, which means we count them as // being "not logged in". $viewer = new PhabricatorUser(); } // See T13590. Some pathways, like error handling, may require unusual // access to things like timezone information. These are fine to build // inline; this pathway is not lightweight anyway. $viewer->setAllowInlineCacheGeneration(true); $this->setServiceViewer($viewer); $allow_public = PhabricatorEnv::getEnvConfig('policy.allow-public'); $allow_auth = PhabricatorEnv::getEnvConfig('diffusion.allow-http-auth'); if (!$allow_public) { if (!$viewer->isLoggedIn()) { if ($allow_auth) { return new PhabricatorVCSResponse( 401, pht('You must log in to access repositories.')); } else { return new PhabricatorVCSResponse( 403, pht('Public and authenticated HTTP access are both forbidden.')); } } } try { $repository = id(new PhabricatorRepositoryQuery()) ->setViewer($viewer) ->withIdentifiers(array($identifier)) ->needURIs(true) ->executeOne(); if (!$repository) { return new PhabricatorVCSResponse( 404, pht('No such repository exists.')); } } catch (PhabricatorPolicyException $ex) { if ($viewer->isLoggedIn()) { return new PhabricatorVCSResponse( 403, pht('You do not have permission to access this repository.')); } else { if ($allow_auth) { return new PhabricatorVCSResponse( 401, pht('You must log in to access this repository.')); } else { return new PhabricatorVCSResponse( 403, pht( 'This repository requires authentication, which is forbidden '. 'over HTTP.')); } } } $response = $this->validateGitLFSRequest($repository, $viewer); if ($response) { return $response; } $this->setServiceRepository($repository); if (!$repository->isTracked()) { return new PhabricatorVCSResponse( 403, pht('This repository is inactive.')); } $is_push = !$this->isReadOnlyRequest($repository); if ($this->getIsGitLFSRequest() && $this->getGitLFSToken()) { // We allow git LFS requests over HTTP even if the repository does not // otherwise support HTTP reads or writes, as long as the user is using a // token from SSH. If they're using HTTP username + password auth, they // have to obey the normal HTTP rules. } else { // For now, we don't distinguish between HTTP and HTTPS-originated // requests that are proxied within the cluster, so the user can connect // with HTTPS but we may be on HTTP by the time we reach this part of // the code. Allow things to move forward as long as either protocol // can be served. $proto_https = PhabricatorRepositoryURI::BUILTIN_PROTOCOL_HTTPS; $proto_http = PhabricatorRepositoryURI::BUILTIN_PROTOCOL_HTTP; $can_read = $repository->canServeProtocol($proto_https, false) || $repository->canServeProtocol($proto_http, false); if (!$can_read) { return new PhabricatorVCSResponse( 403, pht('This repository is not available over HTTP.')); } if ($is_push) { if ($repository->isReadOnly()) { return new PhabricatorVCSResponse( 503, $repository->getReadOnlyMessageForDisplay()); } $can_write = $repository->canServeProtocol($proto_https, true) || $repository->canServeProtocol($proto_http, true); if (!$can_write) { return new PhabricatorVCSResponse( 403, pht('This repository is read-only over HTTP.')); } } } if ($is_push) { $can_push = PhabricatorPolicyFilter::hasCapability( $viewer, $repository, DiffusionPushCapability::CAPABILITY); if (!$can_push) { if ($viewer->isLoggedIn()) { $error_code = 403; $error_message = pht( 'You do not have permission to push to this repository ("%s").', $repository->getDisplayName()); if ($this->getIsGitLFSRequest()) { return DiffusionGitLFSResponse::newErrorResponse( $error_code, $error_message); } else { return new PhabricatorVCSResponse( $error_code, $error_message); } } else { if ($allow_auth) { return new PhabricatorVCSResponse( 401, pht('You must log in to push to this repository.')); } else { return new PhabricatorVCSResponse( 403, pht( 'Pushing to this repository requires authentication, '. 'which is forbidden over HTTP.')); } } } } $vcs_type = $repository->getVersionControlSystem(); $req_type = $this->isVCSRequest($request); if ($vcs_type != $req_type) { switch ($req_type) { case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: $result = new PhabricatorVCSResponse( 500, pht( 'This repository ("%s") is not a Git repository.', $repository->getDisplayName())); break; case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: $result = new PhabricatorVCSResponse( 500, pht( 'This repository ("%s") is not a Mercurial repository.', $repository->getDisplayName())); break; case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: $result = new PhabricatorVCSResponse( 500, pht( 'This repository ("%s") is not a Subversion repository.', $repository->getDisplayName())); break; default: $result = new PhabricatorVCSResponse( 500, pht('Unknown request type.')); break; } } else { switch ($vcs_type) { case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: $caught = null; try { $result = $this->serveVCSRequest($repository, $viewer); } catch (Exception $ex) { $caught = $ex; } catch (Throwable $ex) { $caught = $ex; } if ($caught) { // We never expect an uncaught exception here, so dump it to the // log. All routine errors should have been converted into Response // objects by a lower layer. phlog($caught); $result = new PhabricatorVCSResponse( 500, phutil_string_cast($caught->getMessage())); } break; case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: $result = new PhabricatorVCSResponse( 500, pht( - 'Phabricator does not support HTTP access to Subversion '. + 'This server does not support HTTP access to Subversion '. 'repositories.')); break; default: $result = new PhabricatorVCSResponse( 500, pht('Unknown version control system.')); break; } } $code = $result->getHTTPResponseCode(); if ($is_push && ($code == 200)) { $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); $repository->writeStatusMessage( PhabricatorRepositoryStatusMessage::TYPE_NEEDS_UPDATE, PhabricatorRepositoryStatusMessage::CODE_OKAY); unset($unguarded); } return $result; } private function serveVCSRequest( PhabricatorRepository $repository, PhabricatorUser $viewer) { // We can serve Git LFS requests first, since we don't need to proxy them. // It's also important that LFS requests never fall through to standard // service pathways, because that would let you use LFS tokens to read // normal repository data. if ($this->getIsGitLFSRequest()) { return $this->serveGitLFSRequest($repository, $viewer); } // If this repository is hosted on a service, we need to proxy the request // to a host which can serve it. $is_cluster_request = $this->getRequest()->isProxiedClusterRequest(); $uri = $repository->getAlmanacServiceURI( $viewer, array( 'neverProxy' => $is_cluster_request, 'protocols' => array( 'http', 'https', ), 'writable' => !$this->isReadOnlyRequest($repository), )); if ($uri) { $future = $this->getRequest()->newClusterProxyFuture($uri); return id(new AphrontHTTPProxyResponse()) ->setHTTPFuture($future); } // Otherwise, we're going to handle the request locally. $vcs_type = $repository->getVersionControlSystem(); switch ($vcs_type) { case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: $result = $this->serveGitRequest($repository, $viewer); break; case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: $result = $this->serveMercurialRequest($repository, $viewer); break; } return $result; } private function isReadOnlyRequest( PhabricatorRepository $repository) { $request = $this->getRequest(); $method = $_SERVER['REQUEST_METHOD']; // TODO: This implementation is safe by default, but very incomplete. if ($this->getIsGitLFSRequest()) { return $this->isGitLFSReadOnlyRequest($repository); } switch ($repository->getVersionControlSystem()) { case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: $service = $request->getStr('service'); $path = $this->getRequestDirectoryPath($repository); // NOTE: Service names are the reverse of what you might expect, as they // are from the point of view of the server. The main read service is // "git-upload-pack", and the main write service is "git-receive-pack". if ($method == 'GET' && $path == '/info/refs' && $service == 'git-upload-pack') { return true; } if ($path == '/git-upload-pack') { return true; } break; case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: $cmd = $request->getStr('cmd'); if ($cmd == 'batch') { $cmds = idx($this->getMercurialArguments(), 'cmds'); return DiffusionMercurialWireProtocol::isReadOnlyBatchCommand($cmds); } return DiffusionMercurialWireProtocol::isReadOnlyCommand($cmd); case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: break; } return false; } /** * @phutil-external-symbol class PhabricatorStartup */ private function serveGitRequest( PhabricatorRepository $repository, PhabricatorUser $viewer) { $request = $this->getRequest(); $request_path = $this->getRequestDirectoryPath($repository); $repository_root = $repository->getLocalPath(); // Rebuild the query string to strip `__magic__` parameters and prevent // issues where we might interpret inputs like "service=read&service=write" // differently than the server does and pass it an unsafe command. // NOTE: This does not use getPassthroughRequestParameters() because // that code is HTTP-method agnostic and will encode POST data. $query_data = $_GET; foreach ($query_data as $key => $value) { if (!strncmp($key, '__', 2)) { unset($query_data[$key]); } } $query_string = phutil_build_http_querystring($query_data); // We're about to wipe out PATH with the rest of the environment, so // resolve the binary first. $bin = Filesystem::resolveBinary('git-http-backend'); if (!$bin) { throw new Exception( pht( 'Unable to find `%s` in %s!', 'git-http-backend', '$PATH')); } // NOTE: We do not set HTTP_CONTENT_ENCODING here, because we already // decompressed the request when we read the request body, so the body is // just plain data with no encoding. $env = array( 'REQUEST_METHOD' => $_SERVER['REQUEST_METHOD'], 'QUERY_STRING' => $query_string, 'CONTENT_TYPE' => $request->getHTTPHeader('Content-Type'), 'REMOTE_ADDR' => $_SERVER['REMOTE_ADDR'], 'GIT_PROJECT_ROOT' => $repository_root, 'GIT_HTTP_EXPORT_ALL' => '1', 'PATH_INFO' => $request_path, 'REMOTE_USER' => $viewer->getUsername(), // TODO: Set these correctly. // GIT_COMMITTER_NAME // GIT_COMMITTER_EMAIL ) + $this->getCommonEnvironment($viewer); $input = PhabricatorStartup::getRawInput(); $command = csprintf('%s', $bin); $command = PhabricatorDaemon::sudoCommandAsDaemonUser($command); $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); $cluster_engine = id(new DiffusionRepositoryClusterEngine()) ->setViewer($viewer) ->setRepository($repository); $did_write_lock = false; if ($this->isReadOnlyRequest($repository)) { $cluster_engine->synchronizeWorkingCopyBeforeRead(); } else { $did_write_lock = true; $cluster_engine->synchronizeWorkingCopyBeforeWrite(); } $caught = null; try { list($err, $stdout, $stderr) = id(new ExecFuture('%C', $command)) ->setEnv($env, true) ->write($input) ->resolve(); } catch (Exception $ex) { $caught = $ex; } if ($did_write_lock) { $cluster_engine->synchronizeWorkingCopyAfterWrite(); } unset($unguarded); if ($caught) { throw $caught; } if ($err) { if ($this->isValidGitShallowCloneResponse($stdout, $stderr)) { // Ignore the error if the response passes this special check for // validity. $err = 0; } } if ($err) { return new PhabricatorVCSResponse( 500, pht( 'Error %d: %s', $err, phutil_utf8ize($stderr))); } return id(new DiffusionGitResponse())->setGitData($stdout); } private function getRequestDirectoryPath(PhabricatorRepository $repository) { $request = $this->getRequest(); $request_path = $request->getRequestURI()->getPath(); $info = PhabricatorRepository::parseRepositoryServicePath( $request_path, $repository->getVersionControlSystem()); $base_path = $info['path']; // For Git repositories, strip an optional directory component if it // isn't the name of a known Git resource. This allows users to clone // repositories as "/diffusion/X/anything.git", for example. if ($repository->isGit()) { $known = array( 'info', 'git-upload-pack', 'git-receive-pack', ); foreach ($known as $key => $path) { $known[$key] = preg_quote($path, '@'); } $known = implode('|', $known); if (preg_match('@^/([^/]+)/('.$known.')(/|$)@', $base_path)) { $base_path = preg_replace('@^/([^/]+)@', '', $base_path); } } return $base_path; } private function authenticateGitLFSUser( $username, PhutilOpaqueEnvelope $password, $identifier) { // Never accept these credentials for requests which aren't LFS requests. if (!$this->getIsGitLFSRequest()) { return null; } // If we have the wrong username, don't bother checking if the token // is right. if ($username !== DiffusionGitLFSTemporaryTokenType::HTTP_USERNAME) { return null; } // See PHI1123. We need to be able to constrain the token query with // "withTokenResources(...)" to take advantage of the key on the table. // In this case, the repository PHID is the "resource" we're after. // In normal workflows, we figure out the viewer first, then use the // viewer to load the repository, but that won't work here. Load the // repository as the omnipotent viewer, then use the repository PHID to // look for a token. $omnipotent_viewer = PhabricatorUser::getOmnipotentUser(); $repository = id(new PhabricatorRepositoryQuery()) ->setViewer($omnipotent_viewer) ->withIdentifiers(array($identifier)) ->executeOne(); if (!$repository) { return null; } $lfs_pass = $password->openEnvelope(); $lfs_hash = PhabricatorHash::weakDigest($lfs_pass); $token = id(new PhabricatorAuthTemporaryTokenQuery()) ->setViewer($omnipotent_viewer) ->withTokenResources(array($repository->getPHID())) ->withTokenTypes(array(DiffusionGitLFSTemporaryTokenType::TOKENTYPE)) ->withTokenCodes(array($lfs_hash)) ->withExpired(false) ->executeOne(); if (!$token) { return null; } $user = id(new PhabricatorPeopleQuery()) ->setViewer($omnipotent_viewer) ->withPHIDs(array($token->getUserPHID())) ->executeOne(); if (!$user) { return null; } if (!$user->isUserActivated()) { return null; } $this->gitLFSToken = $token; return $user; } private function authenticateHTTPRepositoryUser( $username, PhutilOpaqueEnvelope $password) { if (!PhabricatorEnv::getEnvConfig('diffusion.allow-http-auth')) { // No HTTP auth permitted. return null; } if (!strlen($username)) { // No username. return null; } if (!strlen($password->openEnvelope())) { // No password. return null; } $user = id(new PhabricatorPeopleQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withUsernames(array($username)) ->executeOne(); if (!$user) { // Username doesn't match anything. return null; } if (!$user->isUserActivated()) { // User is not activated. return null; } $request = $this->getRequest(); $content_source = PhabricatorContentSource::newFromRequest($request); $engine = id(new PhabricatorAuthPasswordEngine()) ->setViewer($user) ->setContentSource($content_source) ->setPasswordType(PhabricatorAuthPassword::PASSWORD_TYPE_VCS) ->setObject($user); if (!$engine->isValidPassword($password)) { return null; } return $user; } private function serveMercurialRequest( PhabricatorRepository $repository, PhabricatorUser $viewer) { $request = $this->getRequest(); $bin = Filesystem::resolveBinary('hg'); if (!$bin) { throw new Exception( pht( 'Unable to find `%s` in %s!', 'hg', '$PATH')); } $env = $this->getCommonEnvironment($viewer); $input = PhabricatorStartup::getRawInput(); $cmd = $request->getStr('cmd'); $args = $this->getMercurialArguments(); $args = $this->formatMercurialArguments($cmd, $args); if (strlen($input)) { $input = strlen($input)."\n".$input."0\n"; } $command = csprintf( '%s -R %s serve --stdio', $bin, $repository->getLocalPath()); $command = PhabricatorDaemon::sudoCommandAsDaemonUser($command); list($err, $stdout, $stderr) = id(new ExecFuture('%C', $command)) ->setEnv($env, true) ->setCWD($repository->getLocalPath()) ->write("{$cmd}\n{$args}{$input}") ->resolve(); if ($err) { return new PhabricatorVCSResponse( 500, pht('Error %d: %s', $err, $stderr)); } if ($cmd == 'getbundle' || $cmd == 'changegroup' || $cmd == 'changegroupsubset') { // We're not completely sure that "changegroup" and "changegroupsubset" // actually work, they're for very old Mercurial. $body = gzcompress($stdout); } else if ($cmd == 'unbundle') { // This includes diagnostic information and anything echoed by commit // hooks. We ignore `stdout` since it just has protocol garbage, and // substitute `stderr`. $body = strlen($stderr)."\n".$stderr; } else { list($length, $body) = explode("\n", $stdout, 2); if ($cmd == 'capabilities') { $body = DiffusionMercurialWireProtocol::filterBundle2Capability($body); } } return id(new DiffusionMercurialResponse())->setContent($body); } private function getMercurialArguments() { // Mercurial sends arguments in HTTP headers. "Why?", you might wonder, // "Why would you do this?". $args_raw = array(); for ($ii = 1;; $ii++) { $header = 'HTTP_X_HGARG_'.$ii; if (!array_key_exists($header, $_SERVER)) { break; } $args_raw[] = $_SERVER[$header]; } $args_raw = implode('', $args_raw); return id(new PhutilQueryStringParser()) ->parseQueryString($args_raw); } private function formatMercurialArguments($command, array $arguments) { $spec = DiffusionMercurialWireProtocol::getCommandArgs($command); $out = array(); // Mercurial takes normal arguments like this: // // name // value $has_star = false; foreach ($spec as $arg_key) { if ($arg_key == '*') { $has_star = true; continue; } if (isset($arguments[$arg_key])) { $value = $arguments[$arg_key]; $size = strlen($value); $out[] = "{$arg_key} {$size}\n{$value}"; unset($arguments[$arg_key]); } } if ($has_star) { // Mercurial takes arguments for variable argument lists roughly like // this: // // * // argname1 // argvalue1 // argname2 // argvalue2 $count = count($arguments); $out[] = "* {$count}\n"; foreach ($arguments as $key => $value) { if (in_array($key, $spec)) { // We already added this argument above, so skip it. continue; } $size = strlen($value); $out[] = "{$key} {$size}\n{$value}"; } } return implode('', $out); } private function isValidGitShallowCloneResponse($stdout, $stderr) { // If you execute `git clone --depth N ...`, git sends a request which // `git-http-backend` responds to by emitting valid output and then exiting // with a failure code and an error message. If we ignore this error, // everything works. // This is a pretty funky fix: it would be nice to more precisely detect // that a request is a `--depth N` clone request, but we don't have any code // to decode protocol frames yet. Instead, look for reasonable evidence // in the output that we're looking at a `--depth` clone. // A valid x-git-upload-pack-result response during packfile negotiation // should end with a flush packet ("0000"). As long as that packet // terminates the response body in the response, we'll assume the response // is correct and complete. // See https://git-scm.com/docs/pack-protocol#_packfile_negotiation $stdout_regexp = '(^Content-Type: application/x-git-upload-pack-result)m'; $has_pack = preg_match($stdout_regexp, $stdout); if (strlen($stdout) >= 4) { $has_flush_packet = (substr($stdout, -4) === "0000"); } else { $has_flush_packet = false; } return ($has_pack && $has_flush_packet); } private function getCommonEnvironment(PhabricatorUser $viewer) { $remote_address = $this->getRequest()->getRemoteAddress(); return array( DiffusionCommitHookEngine::ENV_USER => $viewer->getUsername(), DiffusionCommitHookEngine::ENV_REMOTE_ADDRESS => $remote_address, DiffusionCommitHookEngine::ENV_REMOTE_PROTOCOL => 'http', ); } private function validateGitLFSRequest( PhabricatorRepository $repository, PhabricatorUser $viewer) { if (!$this->getIsGitLFSRequest()) { return null; } if (!$repository->canUseGitLFS()) { return new PhabricatorVCSResponse( 403, pht( 'The requested repository ("%s") does not support Git LFS.', $repository->getDisplayName())); } // If this is using an LFS token, sanity check that we're using it on the // correct repository. This shouldn't really matter since the user could // just request a proper token anyway, but it suspicious and should not // be permitted. $token = $this->getGitLFSToken(); if ($token) { $resource = $token->getTokenResource(); if ($resource !== $repository->getPHID()) { return new PhabricatorVCSResponse( 403, pht( 'The authentication token provided in the request is bound to '. 'a different repository than the requested repository ("%s").', $repository->getDisplayName())); } } return null; } private function serveGitLFSRequest( PhabricatorRepository $repository, PhabricatorUser $viewer) { if (!$this->getIsGitLFSRequest()) { throw new Exception(pht('This is not a Git LFS request!')); } $path = $this->getGitLFSRequestPath($repository); $matches = null; if (preg_match('(^upload/(.*)\z)', $path, $matches)) { $oid = $matches[1]; return $this->serveGitLFSUploadRequest($repository, $viewer, $oid); } else if ($path == 'objects/batch') { return $this->serveGitLFSBatchRequest($repository, $viewer); } else { return DiffusionGitLFSResponse::newErrorResponse( 404, pht( 'Git LFS operation "%s" is not supported by this server.', $path)); } } private function serveGitLFSBatchRequest( PhabricatorRepository $repository, PhabricatorUser $viewer) { $input = $this->getGitLFSInput(); $operation = idx($input, 'operation'); switch ($operation) { case 'upload': $want_upload = true; break; case 'download': $want_upload = false; break; default: return DiffusionGitLFSResponse::newErrorResponse( 404, pht( 'Git LFS batch operation "%s" is not supported by this server.', $operation)); } $objects = idx($input, 'objects', array()); $hashes = array(); foreach ($objects as $object) { $hashes[] = idx($object, 'oid'); } if ($hashes) { $refs = id(new PhabricatorRepositoryGitLFSRefQuery()) ->setViewer($viewer) ->withRepositoryPHIDs(array($repository->getPHID())) ->withObjectHashes($hashes) ->execute(); $refs = mpull($refs, null, 'getObjectHash'); } else { $refs = array(); } $file_phids = mpull($refs, 'getFilePHID'); if ($file_phids) { $files = id(new PhabricatorFileQuery()) ->setViewer($viewer) ->withPHIDs($file_phids) ->execute(); $files = mpull($files, null, 'getPHID'); } else { $files = array(); } $authorization = null; $output = array(); foreach ($objects as $object) { $oid = idx($object, 'oid'); $size = idx($object, 'size'); $ref = idx($refs, $oid); $error = null; // NOTE: If we already have a ref for this object, we only emit a // "download" action. The client should not upload the file again. $actions = array(); if ($ref) { $file = idx($files, $ref->getFilePHID()); if ($file) { // Git LFS may prompt users for authentication if the action does // not provide an "Authorization" header and does not have a query // parameter named "token". See here for discussion: // $no_authorization = 'Basic '.base64_encode('none'); $get_uri = $file->getCDNURI('data'); $actions['download'] = array( 'href' => $get_uri, 'header' => array( 'Authorization' => $no_authorization, 'X-Phabricator-Request-Type' => 'git-lfs', ), ); } else { $error = array( 'code' => 404, 'message' => pht( 'Object "%s" was previously uploaded, but no longer exists '. 'on this server.', $oid), ); } } else if ($want_upload) { if (!$authorization) { // Here, we could reuse the existing authorization if we have one, // but it's a little simpler to just generate a new one // unconditionally. $authorization = $this->newGitLFSHTTPAuthorization( $repository, $viewer, $operation); } $put_uri = $repository->getGitLFSURI("info/lfs/upload/{$oid}"); $actions['upload'] = array( 'href' => $put_uri, 'header' => array( 'Authorization' => $authorization, 'X-Phabricator-Request-Type' => 'git-lfs', ), ); } $object = array( 'oid' => $oid, 'size' => $size, ); if ($actions) { $object['actions'] = $actions; } if ($error) { $object['error'] = $error; } $output[] = $object; } $output = array( 'objects' => $output, ); return id(new DiffusionGitLFSResponse()) ->setContent($output); } private function serveGitLFSUploadRequest( PhabricatorRepository $repository, PhabricatorUser $viewer, $oid) { $ref = id(new PhabricatorRepositoryGitLFSRefQuery()) ->setViewer($viewer) ->withRepositoryPHIDs(array($repository->getPHID())) ->withObjectHashes(array($oid)) ->executeOne(); if ($ref) { return DiffusionGitLFSResponse::newErrorResponse( 405, pht( 'Content for object "%s" is already known to this server. It can '. 'not be uploaded again.', $oid)); } // Remove the execution time limit because uploading large files may take // a while. set_time_limit(0); $request_stream = new AphrontRequestStream(); $request_iterator = $request_stream->getIterator(); $hashing_iterator = id(new PhutilHashingIterator($request_iterator)) ->setAlgorithm('sha256'); $source = id(new PhabricatorIteratorFileUploadSource()) ->setName('lfs-'.$oid) ->setViewPolicy(PhabricatorPolicies::POLICY_NOONE) ->setIterator($hashing_iterator); $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); $file = $source->uploadFile(); unset($unguarded); $hash = $hashing_iterator->getHash(); if ($hash !== $oid) { return DiffusionGitLFSResponse::newErrorResponse( 400, pht( 'Uploaded data is corrupt or invalid. Expected hash "%s", actual '. 'hash "%s".', $oid, $hash)); } $ref = id(new PhabricatorRepositoryGitLFSRef()) ->setRepositoryPHID($repository->getPHID()) ->setObjectHash($hash) ->setByteSize($file->getByteSize()) ->setAuthorPHID($viewer->getPHID()) ->setFilePHID($file->getPHID()); $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); // Attach the file to the repository to give users permission // to access it. $file->attachToObject($repository->getPHID()); $ref->save(); unset($unguarded); // This is just a plain HTTP 200 with no content, which is what `git lfs` // expects. return new DiffusionGitLFSResponse(); } private function newGitLFSHTTPAuthorization( PhabricatorRepository $repository, PhabricatorUser $viewer, $operation) { $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); $authorization = DiffusionGitLFSTemporaryTokenType::newHTTPAuthorization( $repository, $viewer, $operation); unset($unguarded); return $authorization; } private function getGitLFSRequestPath(PhabricatorRepository $repository) { $request_path = $this->getRequestDirectoryPath($repository); $matches = null; if (preg_match('(^/info/lfs(?:\z|/)(.*))', $request_path, $matches)) { return $matches[1]; } return null; } private function getGitLFSInput() { if (!$this->gitLFSInput) { $input = PhabricatorStartup::getRawInput(); $input = phutil_json_decode($input); $this->gitLFSInput = $input; } return $this->gitLFSInput; } private function isGitLFSReadOnlyRequest(PhabricatorRepository $repository) { if (!$this->getIsGitLFSRequest()) { return false; } $path = $this->getGitLFSRequestPath($repository); if ($path === 'objects/batch') { $input = $this->getGitLFSInput(); $operation = idx($input, 'operation'); switch ($operation) { case 'download': return true; default: return false; } } return false; } } diff --git a/src/applications/diffusion/editor/DiffusionRepositoryEditEngine.php b/src/applications/diffusion/editor/DiffusionRepositoryEditEngine.php index a026e176fc..07213afd2b 100644 --- a/src/applications/diffusion/editor/DiffusionRepositoryEditEngine.php +++ b/src/applications/diffusion/editor/DiffusionRepositoryEditEngine.php @@ -1,525 +1,525 @@ versionControlSystem = $version_control_system; return $this; } public function getVersionControlSystem() { return $this->versionControlSystem; } public function isEngineConfigurable() { return false; } public function isDefaultQuickCreateEngine() { return true; } public function getQuickCreateOrderVector() { return id(new PhutilSortVector())->addInt(300); } public function getEngineName() { return pht('Repositories'); } public function getSummaryHeader() { return pht('Edit Repositories'); } public function getSummaryText() { return pht('Creates and edits repositories.'); } public function getEngineApplicationClass() { return 'PhabricatorDiffusionApplication'; } protected function newEditableObject() { $viewer = $this->getViewer(); $repository = PhabricatorRepository::initializeNewRepository($viewer); $repository->setDetail('newly-initialized', true); $vcs = $this->getVersionControlSystem(); if ($vcs) { $repository->setVersionControlSystem($vcs); } // Pick a random open service to allocate this repository on, if any exist. // If there are no services, we aren't in cluster mode and will allocate // locally. If there are services but none permit allocations, we fail. // Eventually we can make this more flexible, but this rule is a reasonable // starting point as we begin to deploy cluster services. $services = id(new AlmanacServiceQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withServiceTypes( array( AlmanacClusterRepositoryServiceType::SERVICETYPE, )) ->needProperties(true) ->execute(); if ($services) { // Filter out services which do not permit new allocations. foreach ($services as $key => $possible_service) { if ($possible_service->getAlmanacPropertyValue('closed')) { unset($services[$key]); } } if (!$services) { throw new Exception( pht( 'This install is configured in cluster mode, but all available '. 'repository cluster services are closed to new allocations. '. 'At least one service must be open to allow new allocations to '. 'take place.')); } shuffle($services); $service = head($services); $repository->setAlmanacServicePHID($service->getPHID()); } return $repository; } protected function newObjectQuery() { return new PhabricatorRepositoryQuery(); } protected function getObjectCreateTitleText($object) { return pht('Create Repository'); } protected function getObjectCreateButtonText($object) { return pht('Create Repository'); } protected function getObjectEditTitleText($object) { return pht('Edit Repository: %s', $object->getName()); } protected function getObjectEditShortText($object) { return $object->getDisplayName(); } protected function getObjectCreateShortText() { return pht('Create Repository'); } protected function getObjectName() { return pht('Repository'); } protected function getObjectViewURI($object) { return $object->getPathURI('manage/'); } protected function getCreateNewObjectPolicy() { return $this->getApplication()->getPolicy( DiffusionCreateRepositoriesCapability::CAPABILITY); } protected function newPages($object) { $panels = DiffusionRepositoryManagementPanel::getAllPanels(); $pages = array(); $uris = array(); foreach ($panels as $panel_key => $panel) { $panel->setRepository($object); $uris[$panel_key] = $panel->getPanelURI(); $page = $panel->newEditEnginePage(); if (!$page) { continue; } $pages[] = $page; } $basics_key = DiffusionRepositoryBasicsManagementPanel::PANELKEY; $basics_uri = $uris[$basics_key]; $more_pages = array( id(new PhabricatorEditPage()) ->setKey('encoding') ->setLabel(pht('Text Encoding')) ->setViewURI($basics_uri) ->setFieldKeys( array( 'encoding', )), id(new PhabricatorEditPage()) ->setKey('extensions') ->setLabel(pht('Extensions')) ->setIsDefault(true), ); foreach ($more_pages as $page) { $pages[] = $page; } return $pages; } protected function willConfigureFields($object, array $fields) { // Change the default field order so related fields are adjacent. $after = array( 'policy.edit' => array('policy.push'), ); $result = array(); foreach ($fields as $key => $value) { $result[$key] = $value; if (!isset($after[$key])) { continue; } foreach ($after[$key] as $next_key) { if (!isset($fields[$next_key])) { continue; } unset($result[$next_key]); $result[$next_key] = $fields[$next_key]; unset($fields[$next_key]); } } return $result; } protected function buildCustomEditFields($object) { $viewer = $this->getViewer(); $policies = id(new PhabricatorPolicyQuery()) ->setViewer($viewer) ->setObject($object) ->execute(); $fetch_value = $object->getFetchRules(); $track_value = $object->getTrackOnlyRules(); $permanent_value = $object->getPermanentRefRules(); $automation_instructions = pht( - "Configure **Repository Automation** to allow Phabricator to ". + "Configure **Repository Automation** to allow this server to ". "write to this repository.". "\n\n". "IMPORTANT: This feature is new, experimental, and not supported. ". "Use it at your own risk."); $staging_instructions = pht( "To make it easier to run integration tests and builds on code ". "under review, you can configure a **Staging Area**. When `arc` ". "creates a diff, it will push a copy of the changes to the ". "configured staging area with a corresponding tag.". "\n\n". "IMPORTANT: This feature is new, experimental, and not supported. ". "Use it at your own risk."); $subpath_instructions = pht( 'If you want to import only part of a repository, like `trunk/`, '. - 'you can set a path in **Import Only**. Phabricator will ignore '. + 'you can set a path in **Import Only**. The import process will ignore '. 'commits which do not affect this path.'); $filesize_warning = null; if ($object->isGit()) { $git_binary = PhutilBinaryAnalyzer::getForBinary('git'); $git_version = $git_binary->getBinaryVersion(); $filesize_version = '1.8.4'; if (version_compare($git_version, $filesize_version, '<')) { $filesize_warning = pht( '(WARNING) {icon exclamation-triangle} The version of "git" ("%s") '. 'installed on this server does not support '. '"--batch-check=", a feature required to enforce filesize '. 'limits. Upgrade to "git" %s or newer to use this feature.', $git_version, $filesize_version); } } $track_instructions = pht( 'WARNING: The "Track Only" feature is deprecated. Use "Fetch Refs" '. 'and "Permanent Refs" instead. This feature will be removed in a '. - 'future version of Phabricator.'); + 'future version of this software.'); return array( id(new PhabricatorSelectEditField()) ->setKey('vcs') ->setLabel(pht('Version Control System')) ->setTransactionType( PhabricatorRepositoryVCSTransaction::TRANSACTIONTYPE) ->setIsFormField(false) ->setIsCopyable(true) ->setOptions(PhabricatorRepositoryType::getAllRepositoryTypes()) ->setDescription(pht('Underlying repository version control system.')) ->setConduitDescription( pht( 'Choose which version control system to use when creating a '. 'repository.')) ->setConduitTypeDescription(pht('Version control system selection.')) ->setValue($object->getVersionControlSystem()), id(new PhabricatorTextEditField()) ->setKey('name') ->setLabel(pht('Name')) ->setIsRequired(true) ->setTransactionType( PhabricatorRepositoryNameTransaction::TRANSACTIONTYPE) ->setDescription(pht('The repository name.')) ->setConduitDescription(pht('Rename the repository.')) ->setConduitTypeDescription(pht('New repository name.')) ->setValue($object->getName()), id(new PhabricatorTextEditField()) ->setKey('callsign') ->setLabel(pht('Callsign')) ->setTransactionType( PhabricatorRepositoryCallsignTransaction::TRANSACTIONTYPE) ->setDescription(pht('The repository callsign.')) ->setConduitDescription(pht('Change the repository callsign.')) ->setConduitTypeDescription(pht('New repository callsign.')) ->setValue($object->getCallsign()), id(new PhabricatorTextEditField()) ->setKey('shortName') ->setLabel(pht('Short Name')) ->setTransactionType( PhabricatorRepositorySlugTransaction::TRANSACTIONTYPE) ->setDescription(pht('Short, unique repository name.')) ->setConduitDescription(pht('Change the repository short name.')) ->setConduitTypeDescription(pht('New short name for the repository.')) ->setValue($object->getRepositorySlug()), id(new PhabricatorRemarkupEditField()) ->setKey('description') ->setLabel(pht('Description')) ->setTransactionType( PhabricatorRepositoryDescriptionTransaction::TRANSACTIONTYPE) ->setDescription(pht('Repository description.')) ->setConduitDescription(pht('Change the repository description.')) ->setConduitTypeDescription(pht('New repository description.')) ->setValue($object->getDetail('description')), id(new PhabricatorTextEditField()) ->setKey('encoding') ->setLabel(pht('Text Encoding')) ->setIsCopyable(true) ->setTransactionType( PhabricatorRepositoryEncodingTransaction::TRANSACTIONTYPE) ->setDescription(pht('Default text encoding.')) ->setConduitDescription(pht('Change the default text encoding.')) ->setConduitTypeDescription(pht('New text encoding.')) ->setValue($object->getDetail('encoding')), id(new PhabricatorBoolEditField()) ->setKey('allowDangerousChanges') ->setLabel(pht('Allow Dangerous Changes')) ->setIsCopyable(true) ->setIsFormField(false) ->setOptions( pht('Prevent Dangerous Changes'), pht('Allow Dangerous Changes')) ->setTransactionType( PhabricatorRepositoryDangerousTransaction::TRANSACTIONTYPE) ->setDescription(pht('Permit dangerous changes to be made.')) ->setConduitDescription(pht('Allow or prevent dangerous changes.')) ->setConduitTypeDescription(pht('New protection setting.')) ->setValue($object->shouldAllowDangerousChanges()), id(new PhabricatorBoolEditField()) ->setKey('allowEnormousChanges') ->setLabel(pht('Allow Enormous Changes')) ->setIsCopyable(true) ->setIsFormField(false) ->setOptions( pht('Prevent Enormous Changes'), pht('Allow Enormous Changes')) ->setTransactionType( PhabricatorRepositoryEnormousTransaction::TRANSACTIONTYPE) ->setDescription(pht('Permit enormous changes to be made.')) ->setConduitDescription(pht('Allow or prevent enormous changes.')) ->setConduitTypeDescription(pht('New protection setting.')) ->setValue($object->shouldAllowEnormousChanges()), id(new PhabricatorSelectEditField()) ->setKey('status') ->setLabel(pht('Status')) ->setTransactionType( PhabricatorRepositoryActivateTransaction::TRANSACTIONTYPE) ->setIsFormField(false) ->setOptions(PhabricatorRepository::getStatusNameMap()) ->setDescription(pht('Active or inactive status.')) ->setConduitDescription(pht('Active or deactivate the repository.')) ->setConduitTypeDescription(pht('New repository status.')) ->setValue($object->getStatus()), id(new PhabricatorTextEditField()) ->setKey('defaultBranch') ->setLabel(pht('Default Branch')) ->setTransactionType( PhabricatorRepositoryDefaultBranchTransaction::TRANSACTIONTYPE) ->setIsCopyable(true) ->setDescription(pht('Default branch name.')) ->setConduitDescription(pht('Set the default branch name.')) ->setConduitTypeDescription(pht('New default branch name.')) ->setValue($object->getDetail('default-branch')), id(new PhabricatorTextAreaEditField()) ->setIsStringList(true) ->setKey('fetchRefs') ->setLabel(pht('Fetch Refs')) ->setTransactionType( PhabricatorRepositoryFetchRefsTransaction::TRANSACTIONTYPE) ->setIsCopyable(true) ->setDescription(pht('Fetch only these refs.')) ->setConduitDescription(pht('Set the fetched refs.')) ->setConduitTypeDescription(pht('New fetched refs.')) ->setValue($fetch_value), id(new PhabricatorTextAreaEditField()) ->setIsStringList(true) ->setKey('permanentRefs') ->setLabel(pht('Permanent Refs')) ->setTransactionType( PhabricatorRepositoryPermanentRefsTransaction::TRANSACTIONTYPE) ->setIsCopyable(true) ->setDescription(pht('Only these refs are considered permanent.')) ->setConduitDescription(pht('Set the permanent refs.')) ->setConduitTypeDescription(pht('New permanent ref rules.')) ->setValue($permanent_value), id(new PhabricatorTextAreaEditField()) ->setIsStringList(true) ->setKey('trackOnly') ->setLabel(pht('Track Only')) ->setTransactionType( PhabricatorRepositoryTrackOnlyTransaction::TRANSACTIONTYPE) ->setIsCopyable(true) ->setControlInstructions($track_instructions) ->setDescription(pht('Track only these branches.')) ->setConduitDescription(pht('Set the tracked branches.')) ->setConduitTypeDescription(pht('New tracked branches.')) ->setValue($track_value), id(new PhabricatorTextEditField()) ->setKey('importOnly') ->setLabel(pht('Import Only')) ->setTransactionType( PhabricatorRepositorySVNSubpathTransaction::TRANSACTIONTYPE) ->setIsCopyable(true) ->setDescription(pht('Subpath to selectively import.')) ->setConduitDescription(pht('Set the subpath to import.')) ->setConduitTypeDescription(pht('New subpath to import.')) ->setValue($object->getDetail('svn-subpath')) ->setControlInstructions($subpath_instructions), id(new PhabricatorTextEditField()) ->setKey('stagingAreaURI') ->setLabel(pht('Staging Area URI')) ->setTransactionType( PhabricatorRepositoryStagingURITransaction::TRANSACTIONTYPE) ->setIsCopyable(true) ->setDescription(pht('Staging area URI.')) ->setConduitDescription(pht('Set the staging area URI.')) ->setConduitTypeDescription(pht('New staging area URI.')) ->setValue($object->getStagingURI()) ->setControlInstructions($staging_instructions), id(new PhabricatorDatasourceEditField()) ->setKey('automationBlueprintPHIDs') ->setLabel(pht('Use Blueprints')) ->setTransactionType( PhabricatorRepositoryBlueprintsTransaction::TRANSACTIONTYPE) ->setIsCopyable(true) ->setDatasource(new DrydockBlueprintDatasource()) ->setDescription(pht('Automation blueprints.')) ->setConduitDescription(pht('Change automation blueprints.')) ->setConduitTypeDescription(pht('New blueprint PHIDs.')) ->setValue($object->getAutomationBlueprintPHIDs()) ->setControlInstructions($automation_instructions), id(new PhabricatorStringListEditField()) ->setKey('symbolLanguages') ->setLabel(pht('Languages')) ->setTransactionType( PhabricatorRepositorySymbolLanguagesTransaction::TRANSACTIONTYPE) ->setIsCopyable(true) ->setDescription( pht('Languages which define symbols in this repository.')) ->setConduitDescription( pht('Change symbol languages for this repository.')) ->setConduitTypeDescription( pht('New symbol languages.')) ->setValue($object->getSymbolLanguages()), id(new PhabricatorDatasourceEditField()) ->setKey('symbolRepositoryPHIDs') ->setLabel(pht('Uses Symbols From')) ->setTransactionType( PhabricatorRepositorySymbolSourcesTransaction::TRANSACTIONTYPE) ->setIsCopyable(true) ->setDatasource(new DiffusionRepositoryDatasource()) ->setDescription(pht('Repositories to link symbols from.')) ->setConduitDescription(pht('Change symbol source repositories.')) ->setConduitTypeDescription(pht('New symbol repositories.')) ->setValue($object->getSymbolSources()), id(new PhabricatorBoolEditField()) ->setKey('publish') ->setLabel(pht('Publish/Notify')) ->setTransactionType( PhabricatorRepositoryNotifyTransaction::TRANSACTIONTYPE) ->setIsCopyable(true) ->setOptions( pht('Disable Notifications, Feed, and Herald'), pht('Enable Notifications, Feed, and Herald')) ->setDescription(pht('Configure how changes are published.')) ->setConduitDescription(pht('Change publishing options.')) ->setConduitTypeDescription(pht('New notification setting.')) ->setValue(!$object->isPublishingDisabled()), id(new PhabricatorPolicyEditField()) ->setKey('policy.push') ->setLabel(pht('Push Policy')) ->setAliases(array('push')) ->setIsCopyable(true) ->setCapability(DiffusionPushCapability::CAPABILITY) ->setPolicies($policies) ->setTransactionType( PhabricatorRepositoryPushPolicyTransaction::TRANSACTIONTYPE) ->setDescription( pht('Controls who can push changes to the repository.')) ->setConduitDescription( pht('Change the push policy of the repository.')) ->setConduitTypeDescription(pht('New policy PHID or constant.')) ->setValue($object->getPolicy(DiffusionPushCapability::CAPABILITY)), id(new PhabricatorTextEditField()) ->setKey('filesizeLimit') ->setLabel(pht('Filesize Limit')) ->setTransactionType( PhabricatorRepositoryFilesizeLimitTransaction::TRANSACTIONTYPE) ->setDescription(pht('Maximum permitted file size.')) ->setConduitDescription(pht('Change the filesize limit.')) ->setConduitTypeDescription(pht('New repository filesize limit.')) ->setControlInstructions($filesize_warning) ->setValue($object->getFilesizeLimit()), id(new PhabricatorTextEditField()) ->setKey('copyTimeLimit') ->setLabel(pht('Clone/Fetch Timeout')) ->setTransactionType( PhabricatorRepositoryCopyTimeLimitTransaction::TRANSACTIONTYPE) ->setDescription( pht('Maximum permitted duration of internal clone/fetch.')) ->setConduitDescription(pht('Change the copy time limit.')) ->setConduitTypeDescription(pht('New repository copy time limit.')) ->setValue($object->getCopyTimeLimit()), id(new PhabricatorTextEditField()) ->setKey('touchLimit') ->setLabel(pht('Touched Paths Limit')) ->setTransactionType( PhabricatorRepositoryTouchLimitTransaction::TRANSACTIONTYPE) ->setDescription(pht('Maximum permitted paths touched per commit.')) ->setConduitDescription(pht('Change the touch limit.')) ->setConduitTypeDescription(pht('New repository touch limit.')) ->setValue($object->getTouchLimit()), ); } } diff --git a/src/applications/diffusion/panel/DiffusionSetPasswordSettingsPanel.php b/src/applications/diffusion/panel/DiffusionSetPasswordSettingsPanel.php index 789adfbf57..3b6d0c0f6b 100644 --- a/src/applications/diffusion/panel/DiffusionSetPasswordSettingsPanel.php +++ b/src/applications/diffusion/panel/DiffusionSetPasswordSettingsPanel.php @@ -1,268 +1,268 @@ getUser()->getIsMailingList()) { return false; } return true; } public function getPanelKey() { return 'vcspassword'; } public function getPanelName() { return pht('VCS Password'); } public function getPanelMenuIcon() { return 'fa-code'; } public function getPanelGroupKey() { return PhabricatorSettingsAuthenticationPanelGroup::PANELGROUPKEY; } public function isEnabled() { return PhabricatorEnv::getEnvConfig('diffusion.allow-http-auth'); } public function processRequest(AphrontRequest $request) { $viewer = $request->getUser(); $user = $this->getUser(); $token = id(new PhabricatorAuthSessionEngine())->requireHighSecuritySession( $viewer, $request, '/settings/'); $vcs_type = PhabricatorAuthPassword::PASSWORD_TYPE_VCS; $vcspasswords = id(new PhabricatorAuthPasswordQuery()) ->setViewer($viewer) ->withObjectPHIDs(array($user->getPHID())) ->withPasswordTypes(array($vcs_type)) ->withIsRevoked(false) ->execute(); if ($vcspasswords) { $vcspassword = head($vcspasswords); } else { $vcspassword = PhabricatorAuthPassword::initializeNewPassword( $user, $vcs_type); } $panel_uri = $this->getPanelURI('?saved=true'); $errors = array(); $e_password = true; $e_confirm = true; $content_source = PhabricatorContentSource::newFromRequest($request); // NOTE: This test is against $viewer (not $user), so that the error // message below makes sense in the case that the two are different, // and because an admin reusing their own password is bad, while // system agents generally do not have passwords anyway. $engine = id(new PhabricatorAuthPasswordEngine()) ->setViewer($viewer) ->setContentSource($content_source) ->setObject($viewer) ->setPasswordType($vcs_type); if ($request->isFormPost()) { if ($request->getBool('remove')) { if ($vcspassword->getID()) { $vcspassword->delete(); return id(new AphrontRedirectResponse())->setURI($panel_uri); } } $new_password = $request->getStr('password'); $confirm = $request->getStr('confirm'); $envelope = new PhutilOpaqueEnvelope($new_password); $confirm_envelope = new PhutilOpaqueEnvelope($confirm); try { $engine->checkNewPassword($envelope, $confirm_envelope); $e_password = null; $e_confirm = null; } catch (PhabricatorAuthPasswordException $ex) { $errors[] = $ex->getMessage(); $e_password = $ex->getPasswordError(); $e_confirm = $ex->getConfirmError(); } if (!$errors) { $vcspassword ->setPassword($envelope, $user) ->save(); return id(new AphrontRedirectResponse())->setURI($panel_uri); } } $title = pht('Set VCS Password'); $form = id(new AphrontFormView()) ->setUser($viewer) ->appendRemarkupInstructions( pht( - 'To access repositories hosted by Phabricator over HTTP, you must '. + 'To access repositories hosted on this server over HTTP, you must '. 'set a version control password. This password should be unique.'. "\n\n". "This password applies to all repositories available over ". "HTTP.")); if ($vcspassword->getID()) { $form ->appendChild( id(new AphrontFormPasswordControl()) ->setDisableAutocomplete(true) ->setLabel(pht('Current Password')) ->setDisabled(true) ->setValue('********************')); } else { $form ->appendChild( id(new AphrontFormMarkupControl()) ->setLabel(pht('Current Password')) ->setValue(phutil_tag('em', array(), pht('No Password Set')))); } $form ->appendChild( id(new AphrontFormPasswordControl()) ->setDisableAutocomplete(true) ->setName('password') ->setLabel(pht('New VCS Password')) ->setError($e_password)) ->appendChild( id(new AphrontFormPasswordControl()) ->setDisableAutocomplete(true) ->setName('confirm') ->setLabel(pht('Confirm VCS Password')) ->setError($e_confirm)) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Change Password'))); if (!$vcspassword->getID()) { $is_serious = PhabricatorEnv::getEnvConfig( 'phabricator.serious-business'); $suggest = Filesystem::readRandomBytes(128); $suggest = preg_replace('([^A-Za-z0-9/!().,;{}^&*%~])', '', $suggest); $suggest = substr($suggest, 0, 20); if ($is_serious) { $form->appendRemarkupInstructions( pht( 'Having trouble coming up with a good password? Try this randomly '. 'generated one, made by a computer:'. "\n\n". "`%s`", $suggest)); } else { $form->appendRemarkupInstructions( pht( 'Having trouble coming up with a good password? Try this '. 'artisanal password, hand made in small batches by our expert '. 'craftspeople: '. "\n\n". "`%s`", $suggest)); } } $hash_envelope = new PhutilOpaqueEnvelope($vcspassword->getPasswordHash()); $form->appendChild( id(new AphrontFormStaticControl()) ->setLabel(pht('Current Algorithm')) ->setValue( PhabricatorPasswordHasher::getCurrentAlgorithmName($hash_envelope))); $form->appendChild( id(new AphrontFormStaticControl()) ->setLabel(pht('Best Available Algorithm')) ->setValue(PhabricatorPasswordHasher::getBestAlgorithmName())); if (strlen($hash_envelope->openEnvelope())) { try { $can_upgrade = PhabricatorPasswordHasher::canUpgradeHash( $hash_envelope); } catch (PhabricatorPasswordHasherUnavailableException $ex) { $can_upgrade = false; $errors[] = pht( 'Your VCS password is currently hashed using an algorithm which is '. 'no longer available on this install.'); $errors[] = pht( 'Because the algorithm implementation is missing, your password '. 'can not be used.'); $errors[] = pht( 'You can set a new password to replace the old password.'); } if ($can_upgrade) { $errors[] = pht( 'The strength of your stored VCS password hash can be upgraded. '. 'To upgrade, either: use the password to authenticate with a '. 'repository; or change your password.'); } } $object_box = id(new PHUIObjectBoxView()) ->setHeaderText($title) ->setBackground(PHUIObjectBoxView::WHITE_CONFIG) ->setForm($form) ->setFormErrors($errors); $remove_form = id(new AphrontFormView()) ->setUser($viewer); if ($vcspassword->getID()) { $remove_form ->addHiddenInput('remove', true) ->appendRemarkupInstructions( pht( 'You can remove your VCS password, which will prevent your '. 'account from accessing repositories.')) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Remove Password'))); } else { $remove_form->appendRemarkupInstructions( pht( 'You do not currently have a VCS password set. If you set one, you '. 'can remove it here later.')); } $remove_box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Remove VCS Password')) ->setBackground(PHUIObjectBoxView::WHITE_CONFIG) ->setForm($remove_form); $saved = null; if ($request->getBool('saved')) { $saved = id(new PHUIInfoView()) ->setSeverity(PHUIInfoView::SEVERITY_NOTICE) ->setTitle(pht('Password Updated')) ->appendChild(pht('Your VCS password has been updated.')); } return array( $saved, $object_box, $remove_box, ); } } diff --git a/src/applications/diffusion/protocol/DiffusionRepositoryClusterEngine.php b/src/applications/diffusion/protocol/DiffusionRepositoryClusterEngine.php index 0372e44f4b..dfc86e5f56 100644 --- a/src/applications/diffusion/protocol/DiffusionRepositoryClusterEngine.php +++ b/src/applications/diffusion/protocol/DiffusionRepositoryClusterEngine.php @@ -1,941 +1,942 @@ repository = $repository; return $this; } public function getRepository() { return $this->repository; } public function setViewer(PhabricatorUser $viewer) { $this->viewer = $viewer; return $this; } public function getViewer() { return $this->viewer; } public function setLog(DiffusionRepositoryClusterEngineLogInterface $log) { $this->logger = $log; return $this; } public function setActingAsPHID($acting_as_phid) { $this->actingAsPHID = $acting_as_phid; return $this; } public function getActingAsPHID() { return $this->actingAsPHID; } private function getEffectiveActingAsPHID() { if ($this->actingAsPHID) { return $this->actingAsPHID; } return $this->getViewer()->getPHID(); } /* -( Cluster Synchronization )-------------------------------------------- */ /** * Synchronize repository version information after creating a repository. * * This initializes working copy versions for all currently bound devices to * 0, so that we don't get stuck making an ambiguous choice about which * devices are leaders when we later synchronize before a read. * * @task sync */ public function synchronizeWorkingCopyAfterCreation() { if (!$this->shouldEnableSynchronization(false)) { return; } $repository = $this->getRepository(); $repository_phid = $repository->getPHID(); $service = $repository->loadAlmanacService(); if (!$service) { throw new Exception(pht('Failed to load repository cluster service.')); } $bindings = $service->getActiveBindings(); foreach ($bindings as $binding) { PhabricatorRepositoryWorkingCopyVersion::updateVersion( $repository_phid, $binding->getDevicePHID(), 0); } return $this; } /** * @task sync */ public function synchronizeWorkingCopyAfterHostingChange() { if (!$this->shouldEnableSynchronization(false)) { return; } $repository = $this->getRepository(); $repository_phid = $repository->getPHID(); $versions = PhabricatorRepositoryWorkingCopyVersion::loadVersions( $repository_phid); $versions = mpull($versions, null, 'getDevicePHID'); // After converting a hosted repository to observed, or vice versa, we // need to reset version numbers because the clocks for observed and hosted // repositories run on different units. // We identify all the cluster leaders and reset their version to 0. // We identify all the cluster followers and demote them. // This allows the cluster to start over again at version 0 but keep the // same leaders. if ($versions) { $max_version = (int)max(mpull($versions, 'getRepositoryVersion')); foreach ($versions as $version) { $device_phid = $version->getDevicePHID(); if ($version->getRepositoryVersion() == $max_version) { PhabricatorRepositoryWorkingCopyVersion::updateVersion( $repository_phid, $device_phid, 0); } else { PhabricatorRepositoryWorkingCopyVersion::demoteDevice( $repository_phid, $device_phid); } } } return $this; } /** * @task sync */ public function synchronizeWorkingCopyBeforeRead() { if (!$this->shouldEnableSynchronization(true)) { return; } $repository = $this->getRepository(); $repository_phid = $repository->getPHID(); $device = AlmanacKeys::getLiveDevice(); $device_phid = $device->getPHID(); $read_lock = PhabricatorRepositoryWorkingCopyVersion::getReadLock( $repository_phid, $device_phid); $lock_wait = phutil_units('2 minutes in seconds'); $this->logLine( pht( 'Acquiring read lock for repository "%s" on device "%s"...', $repository->getDisplayName(), $device->getName())); try { $start = PhabricatorTime::getNow(); $read_lock->lock($lock_wait); $waited = (PhabricatorTime::getNow() - $start); if ($waited) { $this->logLine( pht( 'Acquired read lock after %s second(s).', new PhutilNumber($waited))); } else { $this->logLine( pht( 'Acquired read lock immediately.')); } } catch (PhutilLockException $ex) { throw new PhutilProxyException( pht( 'Failed to acquire read lock after waiting %s second(s). You '. 'may be able to retry later. (%s)', new PhutilNumber($lock_wait), $ex->getHint()), $ex); } $versions = PhabricatorRepositoryWorkingCopyVersion::loadVersions( $repository_phid); $versions = mpull($versions, null, 'getDevicePHID'); $this_version = idx($versions, $device_phid); if ($this_version) { $this_version = (int)$this_version->getRepositoryVersion(); } else { $this_version = null; } if ($versions) { // This is the normal case, where we have some version information and // can identify which nodes are leaders. If the current node is not a // leader, we want to fetch from a leader and then update our version. $max_version = (int)max(mpull($versions, 'getRepositoryVersion')); if (($this_version === null) || ($max_version > $this_version)) { if ($repository->isHosted()) { $fetchable = array(); foreach ($versions as $version) { if ($version->getRepositoryVersion() == $max_version) { $fetchable[] = $version->getDevicePHID(); } } $this->synchronizeWorkingCopyFromDevices( $fetchable, $this_version, $max_version); } else { $this->synchronizeWorkingCopyFromRemote(); } PhabricatorRepositoryWorkingCopyVersion::updateVersion( $repository_phid, $device_phid, $max_version); } else { $this->logLine( pht( 'Device "%s" is already a cluster leader and does not need '. 'to be synchronized.', $device->getName())); } $result_version = $max_version; } else { // If no version records exist yet, we need to be careful, because we // can not tell which nodes are leaders. // There might be several nodes with arbitrary existing data, and we have // no way to tell which one has the "right" data. If we pick wrong, we // might erase some or all of the data in the repository. // Since this is dangerous, we refuse to guess unless there is only one // device. If we're the only device in the group, we obviously must be // a leader. $service = $repository->loadAlmanacService(); if (!$service) { throw new Exception(pht('Failed to load repository cluster service.')); } $bindings = $service->getActiveBindings(); $device_map = array(); foreach ($bindings as $binding) { $device_map[$binding->getDevicePHID()] = true; } if (count($device_map) > 1) { throw new Exception( pht( 'Repository "%s" exists on more than one device, but no device '. - 'has any repository version information. Phabricator can not '. - 'guess which copy of the existing data is authoritative. Promote '. - 'a device or see "Ambiguous Leaders" in the documentation.', + 'has any repository version information. There is no way for the '. + 'software to determine which copy of the existing data is '. + 'authoritative. Promote a device or see "Ambiguous Leaders" in '. + 'the documentation.', $repository->getDisplayName())); } if (empty($device_map[$device->getPHID()])) { throw new Exception( pht( 'Repository "%s" is being synchronized on device "%s", but '. 'this device is not bound to the corresponding cluster '. 'service ("%s").', $repository->getDisplayName(), $device->getName(), $service->getName())); } // The current device is the only device in service, so it must be a // leader. We can safely have any future nodes which come online read // from it. PhabricatorRepositoryWorkingCopyVersion::updateVersion( $repository_phid, $device_phid, 0); $result_version = 0; } $read_lock->unlock(); return $result_version; } /** * @task sync */ public function synchronizeWorkingCopyBeforeWrite() { if (!$this->shouldEnableSynchronization(true)) { return; } $repository = $this->getRepository(); $viewer = $this->getViewer(); $repository_phid = $repository->getPHID(); $device = AlmanacKeys::getLiveDevice(); $device_phid = $device->getPHID(); $table = new PhabricatorRepositoryWorkingCopyVersion(); $locked_connection = $table->establishConnection('w'); $write_lock = PhabricatorRepositoryWorkingCopyVersion::getWriteLock( $repository_phid); $write_lock->setExternalConnection($locked_connection); $this->logLine( pht( 'Acquiring write lock for repository "%s"...', $repository->getDisplayName())); // See T13590. On the HTTP pathway, it's possible for us to hit the script // time limit while holding the durable write lock if a user makes a big // push. Remove the time limit before we acquire the durable lock. set_time_limit(0); $lock_wait = phutil_units('2 minutes in seconds'); try { $write_wait_start = microtime(true); $start = PhabricatorTime::getNow(); $step_wait = 1; while (true) { try { $write_lock->lock((int)floor($step_wait)); $write_wait_end = microtime(true); break; } catch (PhutilLockException $ex) { $waited = (PhabricatorTime::getNow() - $start); if ($waited > $lock_wait) { throw $ex; } $this->logActiveWriter($viewer, $repository); } // Wait a little longer before the next message we print. $step_wait = $step_wait + 0.5; $step_wait = min($step_wait, 3); } $waited = (PhabricatorTime::getNow() - $start); if ($waited) { $this->logLine( pht( 'Acquired write lock after %s second(s).', new PhutilNumber($waited))); } else { $this->logLine( pht( 'Acquired write lock immediately.')); } } catch (PhutilLockException $ex) { throw new PhutilProxyException( pht( 'Failed to acquire write lock after waiting %s second(s). You '. 'may be able to retry later. (%s)', new PhutilNumber($lock_wait), $ex->getHint()), $ex); } $versions = PhabricatorRepositoryWorkingCopyVersion::loadVersions( $repository_phid); foreach ($versions as $version) { if (!$version->getIsWriting()) { continue; } throw new Exception( pht( 'An previous write to this repository was interrupted; refusing '. 'new writes. This issue requires operator intervention to resolve, '. 'see "Write Interruptions" in the "Cluster: Repositories" in the '. 'documentation for instructions.')); } $read_wait_start = microtime(true); try { $max_version = $this->synchronizeWorkingCopyBeforeRead(); } catch (Exception $ex) { $write_lock->unlock(); throw $ex; } $read_wait_end = microtime(true); $pid = getmypid(); $hash = Filesystem::readRandomCharacters(12); $this->clusterWriteOwner = "{$pid}.{$hash}"; PhabricatorRepositoryWorkingCopyVersion::willWrite( $locked_connection, $repository_phid, $device_phid, array( 'userPHID' => $this->getEffectiveActingAsPHID(), 'epoch' => PhabricatorTime::getNow(), 'devicePHID' => $device_phid, ), $this->clusterWriteOwner); $this->clusterWriteVersion = $max_version; $this->clusterWriteLock = $write_lock; $write_wait = ($write_wait_end - $write_wait_start); $read_wait = ($read_wait_end - $read_wait_start); $log = $this->logger; if ($log) { $log->writeClusterEngineLogProperty('writeWait', $write_wait); $log->writeClusterEngineLogProperty('readWait', $read_wait); } } public function synchronizeWorkingCopyAfterDiscovery($new_version) { if (!$this->shouldEnableSynchronization(true)) { return; } $repository = $this->getRepository(); $repository_phid = $repository->getPHID(); if ($repository->isHosted()) { return; } $device = AlmanacKeys::getLiveDevice(); $device_phid = $device->getPHID(); // NOTE: We are not holding a lock here because this method is only called // from PhabricatorRepositoryDiscoveryEngine, which already holds a device // lock. Even if we do race here and record an older version, the // consequences are mild: we only do extra work to correct it later. $versions = PhabricatorRepositoryWorkingCopyVersion::loadVersions( $repository_phid); $versions = mpull($versions, null, 'getDevicePHID'); $this_version = idx($versions, $device_phid); if ($this_version) { $this_version = (int)$this_version->getRepositoryVersion(); } else { $this_version = null; } if (($this_version === null) || ($new_version > $this_version)) { PhabricatorRepositoryWorkingCopyVersion::updateVersion( $repository_phid, $device_phid, $new_version); } } /** * @task sync */ public function synchronizeWorkingCopyAfterWrite() { if (!$this->shouldEnableSynchronization(true)) { return; } if (!$this->clusterWriteLock) { throw new Exception( pht( 'Trying to synchronize after write, but not holding a write '. 'lock!')); } $repository = $this->getRepository(); $repository_phid = $repository->getPHID(); $device = AlmanacKeys::getLiveDevice(); $device_phid = $device->getPHID(); // It is possible that we've lost the global lock while receiving the push. // For example, the master database may have been restarted between the // time we acquired the global lock and now, when the push has finished. // We wrote a durable lock while we were holding the the global lock, // essentially upgrading our lock. We can still safely release this upgraded // lock even if we're no longer holding the global lock. // If we fail to release the lock, the repository will be frozen until // an operator can figure out what happened, so we try pretty hard to // reconnect to the database and release the lock. $now = PhabricatorTime::getNow(); $duration = phutil_units('5 minutes in seconds'); $try_until = $now + $duration; $did_release = false; $already_failed = false; while (PhabricatorTime::getNow() <= $try_until) { try { // NOTE: This means we're still bumping the version when pushes fail. We // could select only un-rejected events instead to bump a little less // often. $new_log = id(new PhabricatorRepositoryPushEventQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withRepositoryPHIDs(array($repository_phid)) ->setLimit(1) ->executeOne(); $old_version = $this->clusterWriteVersion; if ($new_log) { $new_version = $new_log->getID(); } else { $new_version = $old_version; } PhabricatorRepositoryWorkingCopyVersion::didWrite( $repository_phid, $device_phid, $this->clusterWriteVersion, $new_version, $this->clusterWriteOwner); $did_release = true; break; } catch (AphrontConnectionQueryException $ex) { $connection_exception = $ex; } catch (AphrontConnectionLostQueryException $ex) { $connection_exception = $ex; } if (!$already_failed) { $already_failed = true; $this->logLine( pht('CRITICAL. Failed to release cluster write lock!')); $this->logLine( pht( 'The connection to the master database was lost while receiving '. 'the write.')); $this->logLine( pht( 'This process will spend %s more second(s) attempting to '. 'recover, then give up.', new PhutilNumber($duration))); } sleep(1); } if ($did_release) { if ($already_failed) { $this->logLine( pht('RECOVERED. Link to master database was restored.')); } $this->logLine(pht('Released cluster write lock.')); } else { throw new Exception( pht( 'Failed to reconnect to master database and release held write '. 'lock ("%s") on device "%s" for repository "%s" after trying '. 'for %s seconds(s). This repository will be frozen.', $this->clusterWriteOwner, $device->getName(), $this->getDisplayName(), new PhutilNumber($duration))); } // We can continue even if we've lost this lock, everything is still // consistent. try { $this->clusterWriteLock->unlock(); } catch (Exception $ex) { // Ignore. } $this->clusterWriteLock = null; $this->clusterWriteOwner = null; } /* -( Internals )---------------------------------------------------------- */ /** * @task internal */ private function shouldEnableSynchronization($require_device) { $repository = $this->getRepository(); $service_phid = $repository->getAlmanacServicePHID(); if (!$service_phid) { return false; } if (!$repository->supportsSynchronization()) { return false; } if ($require_device) { $device = AlmanacKeys::getLiveDevice(); if (!$device) { return false; } } return true; } /** * @task internal */ private function synchronizeWorkingCopyFromRemote() { $repository = $this->getRepository(); $device = AlmanacKeys::getLiveDevice(); $local_path = $repository->getLocalPath(); $fetch_uri = $repository->getRemoteURIEnvelope(); if ($repository->isGit()) { $this->requireWorkingCopy(); $argv = array( 'fetch --prune -- %P %s', $fetch_uri, '+refs/*:refs/*', ); } else { throw new Exception(pht('Remote sync only supported for git!')); } $future = DiffusionCommandEngine::newCommandEngine($repository) ->setArgv($argv) ->setSudoAsDaemon(true) ->setCredentialPHID($repository->getCredentialPHID()) ->setURI($repository->getRemoteURIObject()) ->newFuture(); $future->setCWD($local_path); try { $future->resolvex(); } catch (Exception $ex) { $this->logLine( pht( 'Synchronization of "%s" from remote failed: %s', $device->getName(), $ex->getMessage())); throw $ex; } } /** * @task internal */ private function synchronizeWorkingCopyFromDevices( array $device_phids, $local_version, $remote_version) { $repository = $this->getRepository(); $service = $repository->loadAlmanacService(); if (!$service) { throw new Exception(pht('Failed to load repository cluster service.')); } $device_map = array_fuse($device_phids); $bindings = $service->getActiveBindings(); $fetchable = array(); foreach ($bindings as $binding) { // We can't fetch from nodes which don't have the newest version. $device_phid = $binding->getDevicePHID(); if (empty($device_map[$device_phid])) { continue; } // TODO: For now, only fetch over SSH. We could support fetching over // HTTP eventually. if ($binding->getAlmanacPropertyValue('protocol') != 'ssh') { continue; } $fetchable[] = $binding; } if (!$fetchable) { throw new Exception( pht( 'Leader lost: no up-to-date nodes in repository cluster are '. 'fetchable.')); } // If we can synchronize from multiple sources, choose one at random. shuffle($fetchable); $caught = null; foreach ($fetchable as $binding) { try { $this->synchronizeWorkingCopyFromBinding( $binding, $local_version, $remote_version); $caught = null; break; } catch (Exception $ex) { $caught = $ex; } } if ($caught) { throw $caught; } } /** * @task internal */ private function synchronizeWorkingCopyFromBinding( AlmanacBinding $binding, $local_version, $remote_version) { $repository = $this->getRepository(); $device = AlmanacKeys::getLiveDevice(); $this->logLine( pht( 'Synchronizing this device ("%s") from cluster leader ("%s").', $device->getName(), $binding->getDevice()->getName())); $fetch_uri = $repository->getClusterRepositoryURIFromBinding($binding); $local_path = $repository->getLocalPath(); if ($repository->isGit()) { $this->requireWorkingCopy(); $argv = array( 'fetch --prune -- %s %s', $fetch_uri, '+refs/*:refs/*', ); } else { throw new Exception(pht('Binding sync only supported for git!')); } $future = DiffusionCommandEngine::newCommandEngine($repository) ->setArgv($argv) ->setConnectAsDevice(true) ->setSudoAsDaemon(true) ->setURI($fetch_uri) ->newFuture(); $future->setCWD($local_path); $log = PhabricatorRepositorySyncEvent::initializeNewEvent() ->setRepositoryPHID($repository->getPHID()) ->setEpoch(PhabricatorTime::getNow()) ->setDevicePHID($device->getPHID()) ->setFromDevicePHID($binding->getDevice()->getPHID()) ->setDeviceVersion($local_version) ->setFromDeviceVersion($remote_version); $sync_start = microtime(true); try { $future->resolvex(); } catch (Exception $ex) { $log->setSyncWait(phutil_microseconds_since($sync_start)); if ($ex instanceof CommandException) { if ($future->getWasKilledByTimeout()) { $result_type = PhabricatorRepositorySyncEvent::RESULT_TIMEOUT; } else { $result_type = PhabricatorRepositorySyncEvent::RESULT_ERROR; } $log ->setResultCode($ex->getError()) ->setResultType($result_type) ->setProperty('stdout', $ex->getStdout()) ->setProperty('stderr', $ex->getStderr()); } else { $log ->setResultCode(1) ->setResultType(PhabricatorRepositorySyncEvent::RESULT_EXCEPTION) ->setProperty('message', $ex->getMessage()); } $log->save(); $this->logLine( pht( 'Synchronization of "%s" from leader "%s" failed: %s', $device->getName(), $binding->getDevice()->getName(), $ex->getMessage())); throw $ex; } $log ->setSyncWait(phutil_microseconds_since($sync_start)) ->setResultCode(0) ->setResultType(PhabricatorRepositorySyncEvent::RESULT_SYNC) ->save(); } /** * @task internal */ private function logLine($message) { return $this->logText("# {$message}\n"); } /** * @task internal */ private function logText($message) { $log = $this->logger; if ($log) { $log->writeClusterEngineLogMessage($message); } return $this; } private function requireWorkingCopy() { $repository = $this->getRepository(); $local_path = $repository->getLocalPath(); if (!Filesystem::pathExists($local_path)) { $device = AlmanacKeys::getLiveDevice(); throw new Exception( pht( 'Repository "%s" does not have a working copy on this device '. 'yet, so it can not be synchronized. Wait for the daemons to '. 'construct one or run `bin/repository update %s` on this host '. '("%s") to build it explicitly.', $repository->getDisplayName(), $repository->getMonogram(), $device->getName())); } } private function logActiveWriter( PhabricatorUser $viewer, PhabricatorRepository $repository) { $writer = PhabricatorRepositoryWorkingCopyVersion::loadWriter( $repository->getPHID()); if (!$writer) { $this->logLine(pht('Waiting on another user to finish writing...')); return; } $user_phid = $writer->getWriteProperty('userPHID'); $device_phid = $writer->getWriteProperty('devicePHID'); $epoch = $writer->getWriteProperty('epoch'); $phids = array($user_phid, $device_phid); $handles = $viewer->loadHandles($phids); $duration = (PhabricatorTime::getNow() - $epoch) + 1; $this->logLine( pht( 'Waiting for %s to finish writing (on device "%s" for %ss)...', $handles[$user_phid]->getName(), $handles[$device_phid]->getName(), new PhutilNumber($duration))); } public function newMaintenanceEvent() { $viewer = $this->getViewer(); $repository = $this->getRepository(); $now = PhabricatorTime::getNow(); $event = PhabricatorRepositoryPushEvent::initializeNewEvent($viewer) ->setRepositoryPHID($repository->getPHID()) ->setEpoch($now) ->setPusherPHID($this->getEffectiveActingAsPHID()) ->setRejectCode(PhabricatorRepositoryPushLog::REJECT_ACCEPT); return $event; } public function newMaintenanceLog() { $viewer = $this->getViewer(); $repository = $this->getRepository(); $now = PhabricatorTime::getNow(); $device = AlmanacKeys::getLiveDevice(); if ($device) { $device_phid = $device->getPHID(); } else { $device_phid = null; } return PhabricatorRepositoryPushLog::initializeNewLog($viewer) ->setDevicePHID($device_phid) ->setRepositoryPHID($repository->getPHID()) ->attachRepository($repository) ->setEpoch($now) ->setPusherPHID($this->getEffectiveActingAsPHID()) ->setChangeFlags(PhabricatorRepositoryPushLog::CHANGEFLAG_MAINTENANCE) ->setRefType(PhabricatorRepositoryPushLog::REFTYPE_MAINTENANCE) ->setRefNew(''); } } diff --git a/src/applications/diffusion/request/DiffusionRequest.php b/src/applications/diffusion/request/DiffusionRequest.php index e6e4c5b16d..4bcb4d3bc5 100644 --- a/src/applications/diffusion/request/DiffusionRequest.php +++ b/src/applications/diffusion/request/DiffusionRequest.php @@ -1,700 +1,700 @@ getRepository()->supportsRefs(); } abstract protected function isStableCommit($symbol); protected function didInitialize() { return null; } /* -( Creating Requests )-------------------------------------------------- */ /** * Create a new synthetic request from a parameter dictionary. If you need * a @{class:DiffusionRequest} object in order to issue a DiffusionQuery, you * can use this method to build one. * * Parameters are: * * - `repository` Repository object or identifier. * - `user` Viewing user. Required if `repository` is an identifier. * - `branch` Optional, branch name. * - `path` Optional, file path. * - `commit` Optional, commit identifier. * - `line` Optional, line range. * * @param map See documentation. * @return DiffusionRequest New request object. * @task new */ final public static function newFromDictionary(array $data) { $repository_key = 'repository'; $identifier_key = 'callsign'; $viewer_key = 'user'; $repository = idx($data, $repository_key); $identifier = idx($data, $identifier_key); $have_repository = ($repository !== null); $have_identifier = ($identifier !== null); if ($have_repository && $have_identifier) { throw new Exception( pht( 'Specify "%s" or "%s", but not both.', $repository_key, $identifier_key)); } if (!$have_repository && !$have_identifier) { throw new Exception( pht( 'One of "%s" and "%s" is required.', $repository_key, $identifier_key)); } if ($have_repository) { if (!($repository instanceof PhabricatorRepository)) { if (empty($data[$viewer_key])) { throw new Exception( pht( 'Parameter "%s" is required if "%s" is provided.', $viewer_key, $identifier_key)); } $identifier = $repository; $repository = null; } } if ($identifier !== null) { $object = self::newFromIdentifier( $identifier, $data[$viewer_key], idx($data, 'edit')); } else { $object = self::newFromRepository($repository); } if (!$object) { return null; } $object->initializeFromDictionary($data); return $object; } /** * Internal. * * @task new */ private function __construct() { // } /** * Internal. Use @{method:newFromDictionary}, not this method. * * @param string Repository identifier. * @param PhabricatorUser Viewing user. * @return DiffusionRequest New request object. * @task new */ private static function newFromIdentifier( $identifier, PhabricatorUser $viewer, $need_edit = false) { $query = id(new PhabricatorRepositoryQuery()) ->setViewer($viewer) ->withIdentifiers(array($identifier)) ->needProfileImage(true) ->needURIs(true); if ($need_edit) { $query->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )); } $repository = $query->executeOne(); if (!$repository) { return null; } return self::newFromRepository($repository); } /** * Internal. Use @{method:newFromDictionary}, not this method. * * @param PhabricatorRepository Repository object. * @return DiffusionRequest New request object. * @task new */ private static function newFromRepository( PhabricatorRepository $repository) { $map = array( PhabricatorRepositoryType::REPOSITORY_TYPE_GIT => 'DiffusionGitRequest', PhabricatorRepositoryType::REPOSITORY_TYPE_SVN => 'DiffusionSvnRequest', PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL => 'DiffusionMercurialRequest', ); $class = idx($map, $repository->getVersionControlSystem()); if (!$class) { throw new Exception(pht('Unknown version control system!')); } $object = new $class(); $object->repository = $repository; return $object; } /** * Internal. Use @{method:newFromDictionary}, not this method. * * @param map Map of parsed data. * @return void * @task new */ private function initializeFromDictionary(array $data) { $blob = idx($data, 'blob'); if (phutil_nonempty_string($blob)) { $blob = self::parseRequestBlob($blob, $this->supportsBranches()); $data = $blob + $data; } $this->path = idx($data, 'path'); $this->line = idx($data, 'line'); $this->initFromConduit = idx($data, 'initFromConduit', true); $this->lint = idx($data, 'lint'); $this->symbolicCommit = idx($data, 'commit'); if ($this->supportsBranches()) { $this->branch = idx($data, 'branch'); } if (!$this->getUser()) { $user = idx($data, 'user'); if (!$user) { throw new Exception( pht( 'You must provide a %s in the dictionary!', 'PhabricatorUser')); } $this->setUser($user); } $this->didInitialize(); } final public function setUser(PhabricatorUser $user) { $this->user = $user; return $this; } final public function getUser() { return $this->user; } public function getRepository() { return $this->repository; } public function setPath($path) { $this->path = $path; return $this; } public function getPath() { return $this->path; } public function getLine() { return $this->line; } public function getCommit() { // TODO: Probably remove all of this. if ($this->getSymbolicCommit() !== null) { return $this->getSymbolicCommit(); } return $this->getStableCommit(); } /** * Get the symbolic commit associated with this request. * * A symbolic commit may be a commit hash, an abbreviated commit hash, a * branch name, a tag name, or an expression like "HEAD^^^". The symbolic * commit may also be absent. * * This method always returns the symbol present in the original request, * in unmodified form. * * See also @{method:getStableCommit}. * * @return string|null Symbolic commit, if one was present in the request. */ public function getSymbolicCommit() { return $this->symbolicCommit; } /** * Modify the request to move the symbolic commit elsewhere. * * @param string New symbolic commit. * @return this */ public function updateSymbolicCommit($symbol) { $this->symbolicCommit = $symbol; $this->symbolicType = null; $this->stableCommit = null; return $this; } /** * Get the ref type (`commit` or `tag`) of the location associated with this * request. * * If a symbolic commit is present in the request, this method identifies * the type of the symbol. Otherwise, it identifies the type of symbol of * the location the request is implicitly associated with. This will probably * always be `commit`. * * @return string Symbolic commit type (`commit` or `tag`). */ public function getSymbolicType() { if ($this->symbolicType === null) { // As a side effect, this resolves the symbolic type. $this->getStableCommit(); } return $this->symbolicType; } /** * Retrieve the stable, permanent commit name identifying the repository * location associated with this request. * * This returns a non-symbolic identifier for the current commit: in Git and * Mercurial, a 40-character SHA1; in SVN, a revision number. * * See also @{method:getSymbolicCommit}. * * @return string Stable commit name, like a git hash or SVN revision. Not * a symbolic commit reference. */ public function getStableCommit() { if (!$this->stableCommit) { if ($this->isStableCommit($this->symbolicCommit)) { $this->stableCommit = $this->symbolicCommit; $this->symbolicType = 'commit'; } else { $this->queryStableCommit(); } } return $this->stableCommit; } public function getBranch() { return $this->branch; } public function getLint() { return $this->lint; } protected function getArcanistBranch() { return $this->getBranch(); } public function loadBranch() { // TODO: Get rid of this and do real Queries on real objects. if ($this->branchObject === false) { $this->branchObject = PhabricatorRepositoryBranch::loadBranch( $this->getRepository()->getID(), $this->getArcanistBranch()); } return $this->branchObject; } public function loadCoverage() { // TODO: This should also die. $branch = $this->loadBranch(); if (!$branch) { return; } $path = $this->getPath(); $path_map = id(new DiffusionPathIDQuery(array($path)))->loadPathIDs(); $coverage_row = queryfx_one( id(new PhabricatorRepository())->establishConnection('r'), 'SELECT * FROM %T WHERE branchID = %d AND pathID = %d ORDER BY commitID DESC LIMIT 1', 'repository_coverage', $branch->getID(), $path_map[$path]); if (!$coverage_row) { return null; } return idx($coverage_row, 'coverage'); } public function loadCommit() { if (empty($this->repositoryCommit)) { $repository = $this->getRepository(); $commit = id(new DiffusionCommitQuery()) ->setViewer($this->getUser()) ->withRepository($repository) ->withIdentifiers(array($this->getStableCommit())) ->executeOne(); if ($commit) { $commit->attachRepository($repository); } $this->repositoryCommit = $commit; } return $this->repositoryCommit; } public function loadCommitData() { if (empty($this->repositoryCommitData)) { $commit = $this->loadCommit(); $data = id(new PhabricatorRepositoryCommitData())->loadOneWhere( 'commitID = %d', $commit->getID()); if (!$data) { $data = new PhabricatorRepositoryCommitData(); $data->setCommitMessage( pht('(This commit has not been fully parsed yet.)')); } $this->repositoryCommitData = $data; } return $this->repositoryCommitData; } /* -( Managing Diffusion URIs )-------------------------------------------- */ public function generateURI(array $params) { if (empty($params['stable'])) { $default_commit = $this->getSymbolicCommit(); } else { $default_commit = $this->getStableCommit(); } $defaults = array( 'path' => $this->getPath(), 'branch' => $this->getBranch(), 'commit' => $default_commit, 'lint' => idx($params, 'lint', $this->getLint()), ); foreach ($defaults as $key => $val) { if (!isset($params[$key])) { // Overwrite NULL. $params[$key] = $val; } } return $this->getRepository()->generateURI($params); } /** * Internal. Public only for unit tests. * * Parse the request URI into components. * * @param string URI blob. * @param bool True if this VCS supports branches. * @return map Parsed URI. * * @task uri */ public static function parseRequestBlob($blob, $supports_branches) { $result = array( 'branch' => null, 'path' => null, 'commit' => null, 'line' => null, ); $matches = null; if ($supports_branches) { // Consume the front part of the URI, up to the first "/". This is the // path-component encoded branch name. if (preg_match('@^([^/]+)/@', $blob, $matches)) { $result['branch'] = phutil_unescape_uri_path_component($matches[1]); $blob = substr($blob, strlen($matches[1]) + 1); } } // Consume the back part of the URI, up to the first "$". Use a negative // lookbehind to prevent matching '$$'. We double the '$' symbol when // encoding so that files with names like "money/$100" will survive. $pattern = '@(?:(?:^|[^$])(?:[$][$])*)[$]([\d,-]+)$@'; if (preg_match($pattern, $blob, $matches)) { $result['line'] = $matches[1]; $blob = substr($blob, 0, -(strlen($matches[1]) + 1)); } // We've consumed the line number if it exists, so unescape "$" in the // rest of the string. $blob = str_replace('$$', '$', $blob); // Consume the commit name, stopping on ';;'. We allow any character to // appear in commits names, as they can sometimes be symbolic names (like // tag names or refs). if (preg_match('@(?:(?:^|[^;])(?:;;)*);([^;].*)$@', $blob, $matches)) { $result['commit'] = $matches[1]; $blob = substr($blob, 0, -(strlen($matches[1]) + 1)); } // We've consumed the commit if it exists, so unescape ";" in the rest // of the string. $blob = str_replace(';;', ';', $blob); if (strlen($blob)) { $result['path'] = $blob; } if ($result['path'] !== null) { $parts = explode('/', $result['path']); foreach ($parts as $part) { // Prevent any hyjinx since we're ultimately shipping this to the // filesystem under a lot of workflows. if ($part == '..') { throw new Exception(pht('Invalid path URI.')); } } } return $result; } /** * Check that the working copy of the repository is present and readable. * * @param string Path to the working copy. */ protected function validateWorkingCopy($path) { if (!is_readable(dirname($path))) { $this->raisePermissionException(); } if (!Filesystem::pathExists($path)) { $this->raiseCloneException(); } } protected function raisePermissionException() { $host = php_uname('n'); throw new DiffusionSetupException( pht( 'The clone of this repository ("%s") on the local machine ("%s") '. 'could not be read. Ensure that the repository is in a '. 'location where the web server has read permissions.', $this->getRepository()->getDisplayName(), $host)); } protected function raiseCloneException() { $host = php_uname('n'); throw new DiffusionSetupException( pht( 'The working copy for this repository ("%s") has not been cloned yet '. - 'on this machine ("%s"). Make sure you havestarted the Phabricator '. + 'on this machine ("%s"). Make sure you have started the '. 'daemons. If this problem persists for longer than a clone should '. 'take, check the daemon logs (in the Daemon Console) to see if there '. 'were errors cloning the repository. Consult the "Diffusion User '. 'Guide" in the documentation for help setting up repositories.', $this->getRepository()->getDisplayName(), $host)); } private function queryStableCommit() { $types = array(); if ($this->symbolicCommit) { $ref = $this->symbolicCommit; } else { if ($this->supportsBranches()) { $ref = $this->getBranch(); $types = array( PhabricatorRepositoryRefCursor::TYPE_BRANCH, ); } else { $ref = 'HEAD'; } } $results = $this->resolveRefs(array($ref), $types); $matches = idx($results, $ref, array()); if (!$matches) { $message = pht( 'Ref "%s" does not exist in this repository.', $ref); throw id(new DiffusionRefNotFoundException($message)) ->setRef($ref); } if (count($matches) > 1) { $match = $this->chooseBestRefMatch($ref, $matches); } else { $match = head($matches); } $this->stableCommit = $match['identifier']; $this->symbolicType = $match['type']; } public function getRefAlternatives() { // Make sure we've resolved the reference into a stable commit first. try { $this->getStableCommit(); } catch (DiffusionRefNotFoundException $ex) { // If we have a bad reference, just return the empty set of // alternatives. } return $this->refAlternatives; } private function chooseBestRefMatch($ref, array $results) { // First, filter out less-desirable matches. $candidates = array(); foreach ($results as $result) { // Exclude closed heads. if ($result['type'] == 'branch') { if (idx($result, 'closed')) { continue; } } $candidates[] = $result; } // If we filtered everything, undo the filtering. if (!$candidates) { $candidates = $results; } // TODO: Do a better job of selecting the best match? $match = head($candidates); // After choosing the best alternative, save all the alternatives so the // UI can show them to the user. if (count($candidates) > 1) { $this->refAlternatives = $candidates; } return $match; } public function resolveRefs(array $refs, array $types = array()) { // First, try to resolve refs from fast cache sources. $cached_query = id(new DiffusionCachedResolveRefsQuery()) ->setRepository($this->getRepository()) ->withRefs($refs); if ($types) { $cached_query->withTypes($types); } $cached_results = $cached_query->execute(); // Throw away all the refs we resolved. Hopefully, we'll throw away // everything here. foreach ($refs as $key => $ref) { if (isset($cached_results[$ref])) { unset($refs[$key]); } } // If we couldn't pull everything out of the cache, execute the underlying // VCS operation. if ($refs) { $vcs_results = DiffusionQuery::callConduitWithDiffusionRequest( $this->getUser(), $this, 'diffusion.resolverefs', array( 'types' => $types, 'refs' => $refs, )); } else { $vcs_results = array(); } return $vcs_results + $cached_results; } public function setIsClusterRequest($is_cluster_request) { $this->isClusterRequest = $is_cluster_request; return $this; } public function getIsClusterRequest() { return $this->isClusterRequest; } } diff --git a/src/applications/diffusion/ssh/DiffusionSSHWorkflow.php b/src/applications/diffusion/ssh/DiffusionSSHWorkflow.php index 358418f44c..0645e76356 100644 --- a/src/applications/diffusion/ssh/DiffusionSSHWorkflow.php +++ b/src/applications/diffusion/ssh/DiffusionSSHWorkflow.php @@ -1,329 +1,329 @@ repository) { throw new Exception(pht('Repository is not available yet!')); } return $this->repository; } private function setRepository(PhabricatorRepository $repository) { $this->repository = $repository; return $this; } public function getArgs() { return $this->args; } public function getEnvironment() { $env = array( DiffusionCommitHookEngine::ENV_USER => $this->getSSHUser()->getUsername(), DiffusionCommitHookEngine::ENV_REMOTE_PROTOCOL => 'ssh', ); $identifier = $this->getRequestIdentifier(); if ($identifier !== null) { $env[DiffusionCommitHookEngine::ENV_REQUEST] = $identifier; } $remote_address = $this->getSSHRemoteAddress(); if ($remote_address !== null) { $env[DiffusionCommitHookEngine::ENV_REMOTE_ADDRESS] = $remote_address; } return $env; } /** * Identify and load the affected repository. */ abstract protected function identifyRepository(); abstract protected function executeRepositoryOperations(); abstract protected function raiseWrongVCSException( PhabricatorRepository $repository); protected function getBaseRequestPath() { return $this->baseRequestPath; } protected function writeError($message) { $this->getErrorChannel()->write($message); return $this; } protected function getCurrentDeviceName() { $device = AlmanacKeys::getLiveDevice(); if ($device) { return $device->getName(); } return php_uname('n'); } protected function shouldProxy() { return $this->shouldProxy; } final protected function getAlmanacServiceRefs($for_write) { $viewer = $this->getSSHUser(); $repository = $this->getRepository(); $is_cluster_request = $this->getIsClusterRequest(); $refs = $repository->getAlmanacServiceRefs( $viewer, array( 'neverProxy' => $is_cluster_request, 'protocols' => array( 'ssh', ), 'writable' => $for_write, )); if (!$refs) { throw new Exception( pht( 'Failed to generate an intracluster proxy URI even though this '. 'request was routed as a proxy request.')); } return $refs; } final protected function getProxyCommand($for_write) { $refs = $this->getAlmanacServiceRefs($for_write); $ref = head($refs); return $this->getProxyCommandForServiceRef($ref); } final protected function getProxyCommandForServiceRef( DiffusionServiceRef $ref) { $uri = new PhutilURI($ref->getURI()); $username = AlmanacKeys::getClusterSSHUser(); if ($username === null) { throw new Exception( pht( 'Unable to determine the username to connect with when trying '. - 'to proxy an SSH request within the Phabricator cluster.')); + 'to proxy an SSH request within the cluster.')); } $port = $uri->getPort(); $host = $uri->getDomain(); $key_path = AlmanacKeys::getKeyPath('device.key'); if (!Filesystem::pathExists($key_path)) { throw new Exception( pht( 'Unable to proxy this SSH request within the cluster: this device '. 'is not registered and has a missing device key (expected to '. 'find key at "%s").', $key_path)); } $options = array(); $options[] = '-o'; $options[] = 'StrictHostKeyChecking=no'; $options[] = '-o'; $options[] = 'UserKnownHostsFile=/dev/null'; // This is suppressing "added
to the list of known hosts" // messages, which are confusing and irrelevant when they arise from // proxied requests. It might also be suppressing lots of useful errors, // of course. Ideally, we would enforce host keys eventually. See T13121. $options[] = '-o'; $options[] = 'LogLevel=ERROR'; // NOTE: We prefix the command with "@username", which the far end of the // connection will parse in order to act as the specified user. This // behavior is only available to cluster requests signed by a trusted // device key. return csprintf( 'ssh %Ls -l %s -i %s -p %s %s -- %s %Ls', $options, $username, $key_path, $port, $host, '@'.$this->getSSHUser()->getUsername(), $this->getOriginalArguments()); } final public function execute(PhutilArgumentParser $args) { $this->args = $args; $viewer = $this->getSSHUser(); $have_diffusion = PhabricatorApplication::isClassInstalledForViewer( 'PhabricatorDiffusionApplication', $viewer); if (!$have_diffusion) { throw new Exception( pht( 'You do not have permission to access the Diffusion application, '. 'so you can not interact with repositories over SSH.')); } $repository = $this->identifyRepository(); $this->setRepository($repository); // NOTE: Here, we're just figuring out if this is a proxyable request to // a clusterized repository or not. We don't (and can't) use the URI we get // back directly. // For example, we may get a read-only URI here but be handling a write // request. We only care if we get back `null` (which means we should // handle the request locally) or anything else (which means we should // proxy it to an appropriate device). $is_cluster_request = $this->getIsClusterRequest(); $uri = $repository->getAlmanacServiceURI( $viewer, array( 'neverProxy' => $is_cluster_request, 'protocols' => array( 'ssh', ), )); $this->shouldProxy = (bool)$uri; try { return $this->executeRepositoryOperations(); } catch (Exception $ex) { $this->writeError(get_class($ex).': '.$ex->getMessage()); return 1; } } protected function loadRepositoryWithPath($path, $vcs) { $viewer = $this->getSSHUser(); $info = PhabricatorRepository::parseRepositoryServicePath($path, $vcs); if ($info === null) { throw new Exception( pht( 'Unrecognized repository path "%s". Expected a path like "%s", '. '"%s", or "%s".', $path, '/diffusion/X/', '/diffusion/123/', '/source/thaumaturgy.git')); } $identifier = $info['identifier']; $base = $info['base']; $this->baseRequestPath = $base; $repository = id(new PhabricatorRepositoryQuery()) ->setViewer($viewer) ->withIdentifiers(array($identifier)) ->needURIs(true) ->executeOne(); if (!$repository) { throw new Exception( pht('No repository "%s" exists!', $identifier)); } $is_cluster = $this->getIsClusterRequest(); $protocol = PhabricatorRepositoryURI::BUILTIN_PROTOCOL_SSH; if (!$repository->canServeProtocol($protocol, false, $is_cluster)) { throw new Exception( pht( 'This repository ("%s") is not available over SSH.', $repository->getDisplayName())); } if ($repository->getVersionControlSystem() != $vcs) { $this->raiseWrongVCSException($repository); } return $repository; } protected function requireWriteAccess($protocol_command = null) { if ($this->hasWriteAccess === true) { return; } $repository = $this->getRepository(); $viewer = $this->getSSHUser(); if ($viewer->isOmnipotent()) { throw new Exception( pht( 'This request is authenticated as a cluster device, but is '. 'performing a write. Writes must be performed with a real '. 'user account.')); } if ($repository->isReadOnly()) { throw new Exception($repository->getReadOnlyMessageForDisplay()); } $protocol = PhabricatorRepositoryURI::BUILTIN_PROTOCOL_SSH; if ($repository->canServeProtocol($protocol, true)) { $can_push = PhabricatorPolicyFilter::hasCapability( $viewer, $repository, DiffusionPushCapability::CAPABILITY); if (!$can_push) { throw new Exception( pht('You do not have permission to push to this repository.')); } } else { if ($protocol_command !== null) { throw new Exception( pht( 'This repository is read-only over SSH (tried to execute '. 'protocol command "%s").', $protocol_command)); } else { throw new Exception( pht('This repository is read-only over SSH.')); } } $this->hasWriteAccess = true; return $this->hasWriteAccess; } protected function shouldSkipReadSynchronization() { $viewer = $this->getSSHUser(); // Currently, the only case where devices interact over SSH without // assuming user credentials is when synchronizing before a read. These // synchronizing reads do not themselves need to be synchronized. if ($viewer->isOmnipotent()) { return true; } return false; } protected function newPullEvent() { $viewer = $this->getSSHUser(); $repository = $this->getRepository(); $remote_address = $this->getSSHRemoteAddress(); return id(new PhabricatorRepositoryPullEvent()) ->setEpoch(PhabricatorTime::getNow()) ->setRemoteAddress($remote_address) ->setRemoteProtocol(PhabricatorRepositoryPullEvent::PROTOCOL_SSH) ->setPullerPHID($viewer->getPHID()) ->setRepositoryPHID($repository->getPHID()); } } diff --git a/src/applications/diviner/controller/DivinerMainController.php b/src/applications/diviner/controller/DivinerMainController.php index 400fb48d0f..21450698e6 100644 --- a/src/applications/diviner/controller/DivinerMainController.php +++ b/src/applications/diviner/controller/DivinerMainController.php @@ -1,75 +1,77 @@ getViewer(); $books = id(new DivinerBookQuery()) ->setViewer($viewer) ->execute(); $crumbs = $this->buildApplicationCrumbs(); $crumbs->setBorder(true); $crumbs->addTextCrumb(pht('Books')); $query_button = id(new PHUIButtonView()) ->setTag('a') ->setHref($this->getApplicationURI('query/')) ->setText(pht('Advanced Search')) ->setIcon('fa-search'); $header = id(new PHUIHeaderView()) ->setHeader(pht('Documentation Books')) ->addActionLink($query_button); $document = new PHUIDocumentView(); $document->setHeader($header); $document->addClass('diviner-view'); if ($books) { $books = msort($books, 'getTitle'); $list = array(); foreach ($books as $book) { $item = id(new DivinerBookItemView()) ->setTitle($book->getTitle()) ->setHref('/book/'.$book->getName().'/') ->setSubtitle($book->getPreface()); $list[] = $item; } $list = id(new PHUIBoxView()) ->addPadding(PHUI::PADDING_MEDIUM_TOP) ->appendChild($list); $document->appendChild($list); } else { $text = pht( - "(NOTE) **Looking for Phabricator documentation?** ". - "If you're looking for help and information about Phabricator, ". + "(NOTE) **Looking for documentation?** ". + "If you're looking for help and information about %s, ". "you can [[https://secure.phabricator.com/diviner/ | ". - "browse the public Phabricator documentation]] on the live site.\n\n". - "Diviner is the documentation generator used to build the ". - "Phabricator documentation.\n\n". + "browse the public %s documentation]] on the live site.\n\n". + "Diviner is the documentation generator used to build this ". + "documentation.\n\n". "You haven't generated any Diviner documentation books yet, so ". "there's nothing to show here. If you'd like to generate your own ". - "local copy of the Phabricator documentation and have it appear ". + "local copy of the documentation and have it appear ". "here, run this command:\n\n". " %s\n\n", - 'phabricator/ $ ./bin/diviner generate'); + PlatformSymbols::getPlatformServerName(), + PlatformSymbols::getPlatformServerName(), + '$ ./bin/diviner generate'); $text = new PHUIRemarkupView($viewer, $text); $document->appendChild($text); } return $this->newPage() ->setTitle(pht('Documentation Books')) ->setCrumbs($crumbs) ->appendChild(array( $document, )); } } diff --git a/src/applications/doorkeeper/option/PhabricatorAsanaConfigOptions.php b/src/applications/doorkeeper/option/PhabricatorAsanaConfigOptions.php index e93c818378..64f2f17dd3 100644 --- a/src/applications/doorkeeper/option/PhabricatorAsanaConfigOptions.php +++ b/src/applications/doorkeeper/option/PhabricatorAsanaConfigOptions.php @@ -1,163 +1,165 @@ newOption('asana.workspace-id', 'string', null) ->setSummary(pht('Asana Workspace ID to publish into.')) ->setDescription( pht( 'To enable synchronization into Asana, enter an Asana Workspace '. 'ID here.'. "\n\n". "NOTE: This feature is new and experimental.")), $this->newOption('asana.project-ids', 'wild', null) ->setSummary(pht('Optional Asana projects to use as application tags.')) ->setDescription( pht( - 'When Phabricator creates tasks in Asana, it can add the tasks '. + 'When %s creates tasks in Asana, it can add the tasks '. 'to Asana projects based on which application the corresponding '. - 'object in Phabricator comes from. For example, you can add code '. + 'object in %s comes from. For example, you can add code '. 'reviews in Asana to a "Differential" project.'. "\n\n". - 'NOTE: This feature is new and experimental.')), + 'NOTE: This feature is new and experimental.', + PlatformSymbols::getPlatformServerName(), + PlatformSymbols::getPlatformServerName())), ); } public function renderContextualDescription( PhabricatorConfigOption $option, AphrontRequest $request) { switch ($option->getKey()) { case 'asana.workspace-id': break; case 'asana.project-ids': return $this->renderContextualProjectDescription($option, $request); default: return parent::renderContextualDescription($option, $request); } $viewer = $request->getUser(); $provider = PhabricatorAsanaAuthProvider::getAsanaProvider(); if (!$provider) { return null; } $account = id(new PhabricatorExternalAccountQuery()) ->setViewer($viewer) ->withUserPHIDs(array($viewer->getPHID())) ->withProviderConfigPHIDs( array( $provider->getProviderConfigPHID(), )) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); if (!$account) { return null; } $token = $provider->getOAuthAccessToken($account); if (!$token) { return null; } try { $workspaces = id(new PhutilAsanaFuture()) ->setAccessToken($token) ->setRawAsanaQuery('workspaces') ->resolve(); } catch (Exception $ex) { return null; } if (!$workspaces) { return null; } $out = array(); $out[] = sprintf( '| %s | %s |', pht('Workspace ID'), pht('Workspace Name')); $out[] = '| ------------ | -------------- |'; foreach ($workspaces as $workspace) { $out[] = sprintf( '| `%s` | `%s` |', $workspace['gid'], $workspace['name']); } $out = implode("\n", $out); $out = pht( "The Asana Workspaces your linked account has access to are:\n\n%s", $out); return new PHUIRemarkupView($viewer, $out); } private function renderContextualProjectDescription( PhabricatorConfigOption $option, AphrontRequest $request) { $viewer = $request->getUser(); $publishers = id(new PhutilClassMapQuery()) ->setAncestorClass('DoorkeeperFeedStoryPublisher') ->execute(); $out = array(); $out[] = pht( 'To specify projects to add tasks to, enter a JSON map with publisher '. 'class names as keys and a list of project IDs as values. For example, '. 'to put Differential tasks into Asana projects with IDs `123` and '. '`456`, enter:'. "\n\n". " lang=txt\n". " {\n". " \"DifferentialDoorkeeperRevisionFeedStoryPublisher\" : [123, 456]\n". " }\n"); $out[] = pht('Available publishers class names are:'); foreach ($publishers as $publisher) { $out[] = ' - `'.get_class($publisher).'`'; } $out[] = pht( 'You can find an Asana project ID by clicking the project in Asana and '. 'then examining the URL:'. "\n\n". " lang=txt\n". " https://app.asana.com/0/12345678901234567890/111111111111111111\n". " ^^^^^^^^^^^^^^^^^^^^\n". " This is the ID to use.\n"); $out = implode("\n", $out); return new PHUIRemarkupView($viewer, $out); } }