@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.)
hq.recaptime.dev/wiki/Phorge
phorge
phabricator
1<?php
2
3final class DiffusionURIEditor
4 extends PhabricatorApplicationTransactionEditor {
5
6 private $repository;
7 private $repositoryPHID;
8
9 public function getEditorApplicationClass() {
10 return PhabricatorDiffusionApplication::class;
11 }
12
13 public function getEditorObjectsDescription() {
14 return pht('Diffusion URIs');
15 }
16
17 public function getTransactionTypes() {
18 $types = parent::getTransactionTypes();
19
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;
26
27 return $types;
28 }
29
30 protected function getCustomTransactionOldValue(
31 PhabricatorLiskDAO $object,
32 PhabricatorApplicationTransaction $xaction) {
33
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();
47 }
48
49 return parent::getCustomTransactionOldValue($object, $xaction);
50 }
51
52 protected function getCustomTransactionNewValue(
53 PhabricatorLiskDAO $object,
54 PhabricatorApplicationTransaction $xaction) {
55
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();
65 }
66
67 return parent::getCustomTransactionNewValue($object, $xaction);
68 }
69
70 protected function applyCustomInternalTransaction(
71 PhabricatorLiskDAO $object,
72 PhabricatorApplicationTransaction $xaction) {
73
74 switch ($xaction->getTransactionType()) {
75 case PhabricatorRepositoryURITransaction::TYPE_URI:
76 if (!$this->getIsNewObject()) {
77 $old_uri = $object->getEffectiveURI();
78 } else {
79 $old_uri = null;
80
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);
86 }
87 }
88
89 $object->setURI($xaction->getNewValue());
90
91 // If we've changed the domain or protocol of the URI, remove the
92 // current credential. This improves behavior in several cases:
93
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.
97
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.
101
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.
107
108 // Since port and path changes are less likely to fall among these
109 // cases, they don't trigger a credential wipe.
110
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);
117 }
118 }
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;
136 }
137 }
138
139 protected function applyCustomExternalTransaction(
140 PhabricatorLiskDAO $object,
141 PhabricatorApplicationTransaction $xaction) {
142
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;
151 }
152
153 return parent::applyCustomExternalTransaction($object, $xaction);
154 }
155
156 protected function validateTransaction(
157 PhabricatorLiskDAO $object,
158 $type,
159 array $xactions) {
160
161 $errors = parent::validateTransaction($object, $type, $xactions);
162
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();
167
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;
182 }
183
184 $viewer = $this->getActor();
185
186 foreach ($xactions as $xaction) {
187 $repository_phid = $xaction->getNewValue();
188
189 // If this isn't changing anything, let it through as-is.
190 if ($repository_phid == $object->getRepositoryPHID()) {
191 continue;
192 }
193
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;
203 }
204
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,
212 ))
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;
224 }
225
226 $this->repository = $repository;
227 $this->repositoryPHID = $repository_phid;
228 }
229 break;
230 case PhabricatorRepositoryURITransaction::TYPE_CREDENTIAL:
231 $viewer = $this->getActor();
232 foreach ($xactions as $xaction) {
233 $credential_phid = $xaction->getNewValue();
234
235 if ($credential_phid == $object->getCredentialPHID()) {
236 continue;
237 }
238
239 // Anyone who can edit a URI can remove the credential.
240 if ($credential_phid === null) {
241 continue;
242 }
243
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;
258 }
259 }
260 break;
261 case PhabricatorRepositoryURITransaction::TYPE_URI:
262 $missing = $this->validateIsEmptyTextField(
263 $object->getURI(),
264 $xactions);
265
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));
272
273 $error->setIsMissingFieldError(true);
274 $errors[] = $error;
275 break;
276 }
277
278 foreach ($xactions as $xaction) {
279 $new_uri = $xaction->getNewValue();
280 if ($new_uri == $object->getURI()) {
281 continue;
282 }
283
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;
293 }
294 }
295
296 break;
297 case PhabricatorRepositoryURITransaction::TYPE_IO:
298 $available = $object->getAvailableIOTypeOptions();
299 foreach ($xactions as $xaction) {
300 $new = $xaction->getNewValue();
301
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;
313 }
314
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.
317
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.
320
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;
331 }
332
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();
340
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;
348 }
349
350 $io_type = $uri->getEffectiveIOType();
351
352 if ($io_type == PhabricatorRepositoryURI::IO_READWRITE) {
353 if ($no_readwrite) {
354 $readwrite_conflict = $uri;
355 break;
356 }
357 }
358
359 if ($io_type == PhabricatorRepositoryURI::IO_OBSERVE) {
360 if ($no_observers) {
361 $observe_conflict = $uri;
362 break;
363 }
364 }
365 }
366
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.');
381 }
382
383 $errors[] = new PhabricatorApplicationTransactionValidationError(
384 $type,
385 pht('Invalid'),
386 $message,
387 $xaction);
388 continue;
389 }
390
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.');
398
399 $errors[] = new PhabricatorApplicationTransactionValidationError(
400 $type,
401 pht('Invalid'),
402 $message,
403 $xaction);
404 continue;
405 }
406 }
407 }
408
409 break;
410 case PhabricatorRepositoryURITransaction::TYPE_DISPLAY:
411 $available = $object->getAvailableDisplayTypeOptions();
412 foreach ($xactions as $xaction) {
413 $new = $xaction->getNewValue();
414
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))));
424 }
425 }
426 break;
427
428 case PhabricatorRepositoryURITransaction::TYPE_DISABLE:
429 $old = $object->getIsDisabled();
430 foreach ($xactions as $xaction) {
431 $new = $xaction->getNewValue();
432
433 if ($old == $new) {
434 continue;
435 }
436
437 if (!$object->isBuiltin()) {
438 continue;
439 }
440
441 $errors[] = new PhabricatorApplicationTransactionValidationError(
442 $type,
443 pht('Invalid'),
444 pht('You can not manually disable builtin URIs.'));
445 }
446 break;
447 }
448
449 return $errors;
450 }
451
452 protected function applyFinalEffects(
453 PhabricatorLiskDAO $object,
454 array $xactions) {
455
456 // Synchronize the repository state based on the presence of an "Observe"
457 // URI.
458 $repository = $object->getRepository();
459
460 $uris = id(new PhabricatorRepositoryURIQuery())
461 ->setViewer(PhabricatorUser::getOmnipotentUser())
462 ->withRepositories(array($repository))
463 ->execute();
464
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);
469
470 $observe_uri = null;
471 foreach ($uris as $uri) {
472 if ($uri->getIoType() != PhabricatorRepositoryURI::IO_OBSERVE) {
473 continue;
474 }
475
476 $observe_uri = $uri;
477 break;
478 }
479
480 $was_hosted = $repository->isHosted();
481
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);
492 }
493
494 $repository->save();
495
496 // Explicitly update the URI index.
497 $repository->updateURIIndex();
498
499 $is_hosted = $repository->isHosted();
500
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();
508 }
509
510 $repository->writeStatusMessage(
511 PhabricatorRepositoryStatusMessage::TYPE_NEEDS_UPDATE,
512 null);
513
514 return $xactions;
515 }
516
517}