@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
at recaptime-dev/main 517 lines 18 kB view raw
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}