Correct a parameter order swap in "diffusion.historyquery" for Mercurial
[phabricator.git] / src / applications / diffusion / editor / DiffusionURIEditor.php
blob90ced865f0b9db80da89e0c056bac59d1b6b9abc
1 <?php
3 final class DiffusionURIEditor
4 extends PhabricatorApplicationTransactionEditor {
6 private $repository;
7 private $repositoryPHID;
9 public function getEditorApplicationClass() {
10 return 'PhabricatorDiffusionApplication';
13 public function getEditorObjectsDescription() {
14 return pht('Diffusion URIs');
17 public function getTransactionTypes() {
18 $types = parent::getTransactionTypes();
20 $types[] = PhabricatorRepositoryURITransaction::TYPE_REPOSITORY;
21 $types[] = PhabricatorRepositoryURITransaction::TYPE_URI;
22 $types[] = PhabricatorRepositoryURITransaction::TYPE_IO;
23 $types[] = PhabricatorRepositoryURITransaction::TYPE_DISPLAY;
24 $types[] = PhabricatorRepositoryURITransaction::TYPE_CREDENTIAL;
25 $types[] = PhabricatorRepositoryURITransaction::TYPE_DISABLE;
27 return $types;
30 protected function getCustomTransactionOldValue(
31 PhabricatorLiskDAO $object,
32 PhabricatorApplicationTransaction $xaction) {
34 switch ($xaction->getTransactionType()) {
35 case PhabricatorRepositoryURITransaction::TYPE_URI:
36 return $object->getURI();
37 case PhabricatorRepositoryURITransaction::TYPE_IO:
38 return $object->getIOType();
39 case PhabricatorRepositoryURITransaction::TYPE_DISPLAY:
40 return $object->getDisplayType();
41 case PhabricatorRepositoryURITransaction::TYPE_REPOSITORY:
42 return $object->getRepositoryPHID();
43 case PhabricatorRepositoryURITransaction::TYPE_CREDENTIAL:
44 return $object->getCredentialPHID();
45 case PhabricatorRepositoryURITransaction::TYPE_DISABLE:
46 return (int)$object->getIsDisabled();
49 return parent::getCustomTransactionOldValue($object, $xaction);
52 protected function getCustomTransactionNewValue(
53 PhabricatorLiskDAO $object,
54 PhabricatorApplicationTransaction $xaction) {
56 switch ($xaction->getTransactionType()) {
57 case PhabricatorRepositoryURITransaction::TYPE_URI:
58 case PhabricatorRepositoryURITransaction::TYPE_IO:
59 case PhabricatorRepositoryURITransaction::TYPE_DISPLAY:
60 case PhabricatorRepositoryURITransaction::TYPE_REPOSITORY:
61 case PhabricatorRepositoryURITransaction::TYPE_CREDENTIAL:
62 return $xaction->getNewValue();
63 case PhabricatorRepositoryURITransaction::TYPE_DISABLE:
64 return (int)$xaction->getNewValue();
67 return parent::getCustomTransactionNewValue($object, $xaction);
70 protected function applyCustomInternalTransaction(
71 PhabricatorLiskDAO $object,
72 PhabricatorApplicationTransaction $xaction) {
74 switch ($xaction->getTransactionType()) {
75 case PhabricatorRepositoryURITransaction::TYPE_URI:
76 if (!$this->getIsNewObject()) {
77 $old_uri = $object->getEffectiveURI();
78 } else {
79 $old_uri = null;
81 // When creating a URI via the API, we may not have processed the
82 // repository transaction yet. Attach the repository here to make
83 // sure we have it for the calls below.
84 if ($this->repository) {
85 $object->attachRepository($this->repository);
89 $object->setURI($xaction->getNewValue());
91 // If we've changed the domain or protocol of the URI, remove the
92 // current credential. This improves behavior in several cases:
94 // If a user switches between protocols with different credential
95 // types, like HTTP and SSH, the old credential won't be valid anyway.
96 // It's cleaner to remove it than leave a bad credential in place.
98 // If a user switches hosts, the old credential is probably not
99 // correct (and potentially confusing/misleading). Removing it forces
100 // users to double check that they have the correct credentials.
102 // If an attacker can't see a symmetric credential like a username and
103 // password, they could still potentially capture it by changing the
104 // host for a URI that uses it to `evil.com`, a server they control,
105 // then observing the requests. Removing the credential prevents this
106 // kind of escalation.
108 // Since port and path changes are less likely to fall among these
109 // cases, they don't trigger a credential wipe.
111 $new_uri = $object->getEffectiveURI();
112 if ($old_uri) {
113 $new_proto = ($old_uri->getProtocol() != $new_uri->getProtocol());
114 $new_domain = ($old_uri->getDomain() != $new_uri->getDomain());
115 if ($new_proto || $new_domain) {
116 $object->setCredentialPHID(null);
119 break;
120 case PhabricatorRepositoryURITransaction::TYPE_IO:
121 $object->setIOType($xaction->getNewValue());
122 break;
123 case PhabricatorRepositoryURITransaction::TYPE_DISPLAY:
124 $object->setDisplayType($xaction->getNewValue());
125 break;
126 case PhabricatorRepositoryURITransaction::TYPE_REPOSITORY:
127 $object->setRepositoryPHID($xaction->getNewValue());
128 $object->attachRepository($this->repository);
129 break;
130 case PhabricatorRepositoryURITransaction::TYPE_CREDENTIAL:
131 $object->setCredentialPHID($xaction->getNewValue());
132 break;
133 case PhabricatorRepositoryURITransaction::TYPE_DISABLE:
134 $object->setIsDisabled($xaction->getNewValue());
135 break;
139 protected function applyCustomExternalTransaction(
140 PhabricatorLiskDAO $object,
141 PhabricatorApplicationTransaction $xaction) {
143 switch ($xaction->getTransactionType()) {
144 case PhabricatorRepositoryURITransaction::TYPE_URI:
145 case PhabricatorRepositoryURITransaction::TYPE_IO:
146 case PhabricatorRepositoryURITransaction::TYPE_DISPLAY:
147 case PhabricatorRepositoryURITransaction::TYPE_REPOSITORY:
148 case PhabricatorRepositoryURITransaction::TYPE_CREDENTIAL:
149 case PhabricatorRepositoryURITransaction::TYPE_DISABLE:
150 return;
153 return parent::applyCustomExternalTransaction($object, $xaction);
156 protected function validateTransaction(
157 PhabricatorLiskDAO $object,
158 $type,
159 array $xactions) {
161 $errors = parent::validateTransaction($object, $type, $xactions);
163 switch ($type) {
164 case PhabricatorRepositoryURITransaction::TYPE_REPOSITORY:
165 // Save this, since we need it to validate TYPE_IO transactions.
166 $this->repositoryPHID = $object->getRepositoryPHID();
168 $missing = $this->validateIsEmptyTextField(
169 $object->getRepositoryPHID(),
170 $xactions);
171 if ($missing) {
172 // NOTE: This isn't being marked as a missing field error because
173 // it's a fundamental, required property of the URI.
174 $errors[] = new PhabricatorApplicationTransactionValidationError(
175 $type,
176 pht('Required'),
177 pht(
178 'When creating a repository URI, you must specify which '.
179 'repository the URI will belong to.'),
180 nonempty(last($xactions), null));
181 break;
184 $viewer = $this->getActor();
186 foreach ($xactions as $xaction) {
187 $repository_phid = $xaction->getNewValue();
189 // If this isn't changing anything, let it through as-is.
190 if ($repository_phid == $object->getRepositoryPHID()) {
191 continue;
194 if (!$this->getIsNewObject()) {
195 $errors[] = new PhabricatorApplicationTransactionValidationError(
196 $type,
197 pht('Invalid'),
198 pht(
199 'The repository a URI is associated with is immutable, and '.
200 'can not be changed after the URI is created.'),
201 $xaction);
202 continue;
205 $repository = id(new PhabricatorRepositoryQuery())
206 ->setViewer($viewer)
207 ->withPHIDs(array($repository_phid))
208 ->requireCapabilities(
209 array(
210 PhabricatorPolicyCapability::CAN_VIEW,
211 PhabricatorPolicyCapability::CAN_EDIT,
213 ->executeOne();
214 if (!$repository) {
215 $errors[] = new PhabricatorApplicationTransactionValidationError(
216 $type,
217 pht('Invalid'),
218 pht(
219 'To create a URI for a repository ("%s"), it must exist and '.
220 'you must have permission to edit it.',
221 $repository_phid),
222 $xaction);
223 continue;
226 $this->repository = $repository;
227 $this->repositoryPHID = $repository_phid;
229 break;
230 case PhabricatorRepositoryURITransaction::TYPE_CREDENTIAL:
231 $viewer = $this->getActor();
232 foreach ($xactions as $xaction) {
233 $credential_phid = $xaction->getNewValue();
235 if ($credential_phid == $object->getCredentialPHID()) {
236 continue;
239 // Anyone who can edit a URI can remove the credential.
240 if ($credential_phid === null) {
241 continue;
244 $credential = id(new PassphraseCredentialQuery())
245 ->setViewer($viewer)
246 ->withPHIDs(array($credential_phid))
247 ->executeOne();
248 if (!$credential) {
249 $errors[] = new PhabricatorApplicationTransactionValidationError(
250 $type,
251 pht('Invalid'),
252 pht(
253 'You can only associate a credential ("%s") with a repository '.
254 'URI if it exists and you have permission to see it.',
255 $credential_phid),
256 $xaction);
257 continue;
260 break;
261 case PhabricatorRepositoryURITransaction::TYPE_URI:
262 $missing = $this->validateIsEmptyTextField(
263 $object->getURI(),
264 $xactions);
266 if ($missing) {
267 $error = new PhabricatorApplicationTransactionValidationError(
268 $type,
269 pht('Required'),
270 pht('A repository URI must have a nonempty URI.'),
271 nonempty(last($xactions), null));
273 $error->setIsMissingFieldError(true);
274 $errors[] = $error;
275 break;
278 foreach ($xactions as $xaction) {
279 $new_uri = $xaction->getNewValue();
280 if ($new_uri == $object->getURI()) {
281 continue;
284 try {
285 PhabricatorRepository::assertValidRemoteURI($new_uri);
286 } catch (Exception $ex) {
287 $errors[] = new PhabricatorApplicationTransactionValidationError(
288 $type,
289 pht('Invalid'),
290 $ex->getMessage(),
291 $xaction);
292 continue;
296 break;
297 case PhabricatorRepositoryURITransaction::TYPE_IO:
298 $available = $object->getAvailableIOTypeOptions();
299 foreach ($xactions as $xaction) {
300 $new = $xaction->getNewValue();
302 if (empty($available[$new])) {
303 $errors[] = new PhabricatorApplicationTransactionValidationError(
304 $type,
305 pht('Invalid'),
306 pht(
307 'Value "%s" is not a valid IO setting for this URI. '.
308 'Available types for this URI are: %s.',
309 $new,
310 implode(', ', array_keys($available))),
311 $xaction);
312 continue;
315 // If we are setting this URI to use "Observe", we must have no
316 // other "Observe" URIs and must also have no "Read/Write" URIs.
318 // If we are setting this URI to "Read/Write", we must have no
319 // other "Observe" URIs. It's OK to have other "Read/Write" URIs.
321 $no_observers = false;
322 $no_readwrite = false;
323 switch ($new) {
324 case PhabricatorRepositoryURI::IO_OBSERVE:
325 $no_readwrite = true;
326 $no_observers = true;
327 break;
328 case PhabricatorRepositoryURI::IO_READWRITE:
329 $no_observers = true;
330 break;
333 if ($no_observers || $no_readwrite) {
334 $repository = id(new PhabricatorRepositoryQuery())
335 ->setViewer(PhabricatorUser::getOmnipotentUser())
336 ->withPHIDs(array($this->repositoryPHID))
337 ->needURIs(true)
338 ->executeOne();
339 $uris = $repository->getURIs();
341 $observe_conflict = null;
342 $readwrite_conflict = null;
343 foreach ($uris as $uri) {
344 // If this is the URI being edited, it can not conflict with
345 // itself.
346 if ($uri->getID() == $object->getID()) {
347 continue;
350 $io_type = $uri->getEffectiveIOType();
352 if ($io_type == PhabricatorRepositoryURI::IO_READWRITE) {
353 if ($no_readwrite) {
354 $readwrite_conflict = $uri;
355 break;
359 if ($io_type == PhabricatorRepositoryURI::IO_OBSERVE) {
360 if ($no_observers) {
361 $observe_conflict = $uri;
362 break;
367 if ($observe_conflict) {
368 if ($new == PhabricatorRepositoryURI::IO_OBSERVE) {
369 $message = pht(
370 'You can not set this URI to use Observe IO because '.
371 'another URI for this repository is already configured '.
372 'in Observe IO mode. A repository can not observe two '.
373 'different remotes simultaneously. Turn off IO for the '.
374 'other URI first.');
375 } else {
376 $message = pht(
377 'You can not set this URI to use Read/Write IO because '.
378 'another URI for this repository is already configured '.
379 'in Observe IO mode. An observed repository can not be '.
380 'made writable. Turn off IO for the other URI first.');
383 $errors[] = new PhabricatorApplicationTransactionValidationError(
384 $type,
385 pht('Invalid'),
386 $message,
387 $xaction);
388 continue;
391 if ($readwrite_conflict) {
392 $message = pht(
393 'You can not set this URI to use Observe IO because '.
394 'another URI for this repository is already configured '.
395 'in Read/Write IO mode. A repository can not simultaneously '.
396 'be writable and observe a remote. Turn off IO for the '.
397 'other URI first.');
399 $errors[] = new PhabricatorApplicationTransactionValidationError(
400 $type,
401 pht('Invalid'),
402 $message,
403 $xaction);
404 continue;
409 break;
410 case PhabricatorRepositoryURITransaction::TYPE_DISPLAY:
411 $available = $object->getAvailableDisplayTypeOptions();
412 foreach ($xactions as $xaction) {
413 $new = $xaction->getNewValue();
415 if (empty($available[$new])) {
416 $errors[] = new PhabricatorApplicationTransactionValidationError(
417 $type,
418 pht('Invalid'),
419 pht(
420 'Value "%s" is not a valid display setting for this URI. '.
421 'Available types for this URI are: %s.',
422 $new,
423 implode(', ', array_keys($available))));
426 break;
428 case PhabricatorRepositoryURITransaction::TYPE_DISABLE:
429 $old = $object->getIsDisabled();
430 foreach ($xactions as $xaction) {
431 $new = $xaction->getNewValue();
433 if ($old == $new) {
434 continue;
437 if (!$object->isBuiltin()) {
438 continue;
441 $errors[] = new PhabricatorApplicationTransactionValidationError(
442 $type,
443 pht('Invalid'),
444 pht('You can not manually disable builtin URIs.'));
446 break;
449 return $errors;
452 protected function applyFinalEffects(
453 PhabricatorLiskDAO $object,
454 array $xactions) {
456 // Synchronize the repository state based on the presence of an "Observe"
457 // URI.
458 $repository = $object->getRepository();
460 $uris = id(new PhabricatorRepositoryURIQuery())
461 ->setViewer(PhabricatorUser::getOmnipotentUser())
462 ->withRepositories(array($repository))
463 ->execute();
465 // Reattach the current URIs to the repository: we're going to rebuild
466 // the index explicitly below, and want to include any changes made to
467 // this URI in the index update.
468 $repository->attachURIs($uris);
470 $observe_uri = null;
471 foreach ($uris as $uri) {
472 if ($uri->getIoType() != PhabricatorRepositoryURI::IO_OBSERVE) {
473 continue;
476 $observe_uri = $uri;
477 break;
480 $was_hosted = $repository->isHosted();
482 if ($observe_uri) {
483 $repository
484 ->setHosted(false)
485 ->setDetail('remote-uri', (string)$observe_uri->getEffectiveURI())
486 ->setCredentialPHID($observe_uri->getCredentialPHID());
487 } else {
488 $repository
489 ->setHosted(true)
490 ->setDetail('remote-uri', null)
491 ->setCredentialPHID(null);
494 $repository->save();
496 // Explicitly update the URI index.
497 $repository->updateURIIndex();
499 $is_hosted = $repository->isHosted();
501 // If we've swapped the repository from hosted to observed or vice versa,
502 // reset all the cluster version clocks.
503 if ($was_hosted != $is_hosted) {
504 $cluster_engine = id(new DiffusionRepositoryClusterEngine())
505 ->setViewer($this->getActor())
506 ->setRepository($repository)
507 ->synchronizeWorkingCopyAfterHostingChange();
510 $repository->writeStatusMessage(
511 PhabricatorRepositoryStatusMessage::TYPE_NEEDS_UPDATE,
512 null);
514 return $xactions;