@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 upstream/main 582 lines 18 kB view raw
1<?php 2 3final class PhrictionTransactionEditor 4 extends PhabricatorApplicationTransactionEditor { 5 6 const VALIDATE_CREATE_ANCESTRY = 'create'; 7 const VALIDATE_MOVE_ANCESTRY = 'move'; 8 9 private $description; 10 private $oldContent; 11 private $newContent; 12 private $moveAwayDocument; 13 private $skipAncestorCheck; 14 private $contentVersion; 15 private $processContentVersionError = true; 16 private $contentDiffURI; 17 18 public function setDescription($description) { 19 $this->description = $description; 20 return $this; 21 } 22 23 private function getDescription() { 24 return $this->description; 25 } 26 27 private function setOldContent(PhrictionContent $content) { 28 $this->oldContent = $content; 29 return $this; 30 } 31 32 public function getOldContent() { 33 return $this->oldContent; 34 } 35 36 public function getNewContent() { 37 return $this->newContent; 38 } 39 40 public function setSkipAncestorCheck($bool) { 41 $this->skipAncestorCheck = $bool; 42 return $this; 43 } 44 45 public function getSkipAncestorCheck() { 46 return $this->skipAncestorCheck; 47 } 48 49 public function setContentVersion($version) { 50 $this->contentVersion = $version; 51 return $this; 52 } 53 54 public function getContentVersion() { 55 return $this->contentVersion; 56 } 57 58 public function setProcessContentVersionError($process) { 59 $this->processContentVersionError = $process; 60 return $this; 61 } 62 63 public function getProcessContentVersionError() { 64 return $this->processContentVersionError; 65 } 66 67 public function setMoveAwayDocument(PhrictionDocument $document) { 68 $this->moveAwayDocument = $document; 69 return $this; 70 } 71 72 public function setShouldPublishContent( 73 PhrictionDocument $object, 74 $publish) { 75 76 if ($publish) { 77 $content_phid = $this->getNewContent()->getPHID(); 78 } else { 79 $content_phid = $this->getOldContent()->getPHID(); 80 } 81 82 $object->setContentPHID($content_phid); 83 84 return $this; 85 } 86 87 public function getEditorApplicationClass() { 88 return PhabricatorPhrictionApplication::class; 89 } 90 91 public function getEditorObjectsDescription() { 92 return pht('Phriction Documents'); 93 } 94 95 public function getTransactionTypes() { 96 $types = parent::getTransactionTypes(); 97 98 $types[] = PhabricatorTransactions::TYPE_EDGE; 99 $types[] = PhabricatorTransactions::TYPE_COMMENT; 100 $types[] = PhabricatorTransactions::TYPE_VIEW_POLICY; 101 $types[] = PhabricatorTransactions::TYPE_EDIT_POLICY; 102 103 return $types; 104 } 105 106 public function getCreateObjectTitle($author, $object) { 107 return pht('%s created this document.', $author); 108 } 109 110 public function getCreateObjectTitleForFeed($author, $object) { 111 return pht('%s created %s.', $author, $object); 112 } 113 114 protected function expandTransactions( 115 PhabricatorLiskDAO $object, 116 array $xactions) { 117 118 $this->setOldContent($object->getContent()); 119 120 return parent::expandTransactions($object, $xactions); 121 } 122 123 protected function expandTransaction( 124 PhabricatorLiskDAO $object, 125 PhabricatorApplicationTransaction $xaction) { 126 127 $xactions = parent::expandTransaction($object, $xaction); 128 switch ($xaction->getTransactionType()) { 129 case PhrictionDocumentContentTransaction::TRANSACTIONTYPE: 130 if ($this->getIsNewObject()) { 131 break; 132 } 133 $content = $xaction->getNewValue(); 134 if ($content === '') { 135 $xactions[] = id(new PhrictionTransaction()) 136 ->setTransactionType( 137 PhrictionDocumentDeleteTransaction::TRANSACTIONTYPE) 138 ->setNewValue(true) 139 ->setMetadataValue('contentDelete', true); 140 } 141 break; 142 case PhrictionDocumentMoveToTransaction::TRANSACTIONTYPE: 143 $document = $xaction->getNewValue(); 144 $xactions[] = id(new PhrictionTransaction()) 145 ->setTransactionType(PhabricatorTransactions::TYPE_VIEW_POLICY) 146 ->setNewValue($document->getViewPolicy()); 147 $xactions[] = id(new PhrictionTransaction()) 148 ->setTransactionType(PhabricatorTransactions::TYPE_EDIT_POLICY) 149 ->setNewValue($document->getEditPolicy()); 150 break; 151 default: 152 break; 153 } 154 155 return $xactions; 156 } 157 158 protected function applyFinalEffects( 159 PhabricatorLiskDAO $object, 160 array $xactions) { 161 162 if ($this->hasNewDocumentContent()) { 163 $content = $this->getNewDocumentContent($object); 164 165 $content 166 ->setDocumentPHID($object->getPHID()) 167 ->save(); 168 } 169 170 if ($this->getIsNewObject() && !$this->getSkipAncestorCheck()) { 171 // Stub out empty parent documents if they don't exist 172 $ancestral_slugs = PhabricatorSlug::getAncestry($object->getSlug()); 173 if ($ancestral_slugs) { 174 $ancestors = id(new PhrictionDocumentQuery()) 175 ->setViewer(PhabricatorUser::getOmnipotentUser()) 176 ->withSlugs($ancestral_slugs) 177 ->needContent(true) 178 ->execute(); 179 $ancestors = mpull($ancestors, null, 'getSlug'); 180 $stub_type = PhrictionChangeType::CHANGE_STUB; 181 foreach ($ancestral_slugs as $slug) { 182 $ancestor_doc = idx($ancestors, $slug); 183 // We check for change type to prevent near-infinite recursion 184 if (!$ancestor_doc && $content->getChangeType() != $stub_type) { 185 $ancestor_doc = PhrictionDocument::initializeNewDocument( 186 $this->getActor(), 187 $slug); 188 $stub_xactions = array(); 189 $stub_xactions[] = id(new PhrictionTransaction()) 190 ->setTransactionType( 191 PhrictionDocumentTitleTransaction::TRANSACTIONTYPE) 192 ->setNewValue(PhabricatorSlug::getDefaultTitle($slug)) 193 ->setMetadataValue('stub:create:phid', $object->getPHID()); 194 $stub_xactions[] = id(new PhrictionTransaction()) 195 ->setTransactionType( 196 PhrictionDocumentContentTransaction::TRANSACTIONTYPE) 197 ->setNewValue('') 198 ->setMetadataValue('stub:create:phid', $object->getPHID()); 199 $stub_xactions[] = id(new PhrictionTransaction()) 200 ->setTransactionType(PhabricatorTransactions::TYPE_VIEW_POLICY) 201 ->setNewValue($object->getViewPolicy()); 202 $stub_xactions[] = id(new PhrictionTransaction()) 203 ->setTransactionType(PhabricatorTransactions::TYPE_EDIT_POLICY) 204 ->setNewValue($object->getEditPolicy()); 205 $sub_editor = id(new PhrictionTransactionEditor()) 206 ->setActor($this->getActor()) 207 ->setContentSource($this->getContentSource()) 208 ->setContinueOnNoEffect($this->getContinueOnNoEffect()) 209 ->setSkipAncestorCheck(true) 210 ->setDescription(pht('Empty Parent Document')) 211 ->applyTransactions($ancestor_doc, $stub_xactions); 212 } 213 } 214 } 215 } 216 217 if ($this->moveAwayDocument !== null) { 218 $move_away_xactions = array(); 219 $move_away_xactions[] = id(new PhrictionTransaction()) 220 ->setTransactionType( 221 PhrictionDocumentMoveAwayTransaction::TRANSACTIONTYPE) 222 ->setNewValue($object); 223 $sub_editor = id(new PhrictionTransactionEditor()) 224 ->setActor($this->getActor()) 225 ->setContentSource($this->getContentSource()) 226 ->setContinueOnNoEffect($this->getContinueOnNoEffect()) 227 ->setDescription($this->getDescription()) 228 ->applyTransactions($this->moveAwayDocument, $move_away_xactions); 229 } 230 231 // Compute the content diff URI for the publishing phase. 232 foreach ($xactions as $xaction) { 233 switch ($xaction->getTransactionType()) { 234 case PhrictionDocumentContentTransaction::TRANSACTIONTYPE: 235 $params = array( 236 'l' => $this->getOldContent()->getVersion(), 237 'r' => $this->getNewContent()->getVersion(), 238 ); 239 240 $path = '/phriction/diff/'.$object->getID().'/'; 241 $uri = new PhutilURI($path, $params); 242 243 $this->contentDiffURI = (string)$uri; 244 break 2; 245 default: 246 break; 247 } 248 } 249 250 return $xactions; 251 } 252 253 protected function shouldSendMail( 254 PhabricatorLiskDAO $object, 255 array $xactions) { 256 return true; 257 } 258 259 protected function getMailSubjectPrefix() { 260 return '[Phriction]'; 261 } 262 263 protected function getMailTo(PhabricatorLiskDAO $object) { 264 return array( 265 $this->getActingAsPHID(), 266 ); 267 } 268 269 public function getMailTagsMap() { 270 return array( 271 PhrictionTransaction::MAILTAG_TITLE => 272 pht("A document's title changes."), 273 PhrictionTransaction::MAILTAG_CONTENT => 274 pht("A document's content changes."), 275 PhrictionTransaction::MAILTAG_DELETE => 276 pht('A document is deleted.'), 277 PhrictionTransaction::MAILTAG_SUBSCRIBERS => 278 pht('A document\'s subscribers change.'), 279 PhrictionTransaction::MAILTAG_OTHER => 280 pht('Other document activity not listed above occurs.'), 281 ); 282 } 283 284 protected function buildReplyHandler(PhabricatorLiskDAO $object) { 285 return id(new PhrictionReplyHandler()) 286 ->setMailReceiver($object); 287 } 288 289 protected function buildMailTemplate(PhabricatorLiskDAO $object) { 290 $title = $object->getContent()->getTitle(); 291 292 return id(new PhabricatorMetaMTAMail()) 293 ->setSubject($title); 294 } 295 296 protected function buildMailBody( 297 PhabricatorLiskDAO $object, 298 array $xactions) { 299 300 $body = parent::buildMailBody($object, $xactions); 301 302 if ($this->getIsNewObject()) { 303 $body->addRemarkupSection( 304 pht('DOCUMENT CONTENT'), 305 $object->getContent()->getContent()); 306 } else if ($this->contentDiffURI) { 307 $body->addLinkSection( 308 pht('DOCUMENT DIFF'), 309 PhabricatorEnv::getProductionURI($this->contentDiffURI)); 310 } 311 312 $description = $object->getContent()->getDescription(); 313 if (strlen($description)) { 314 $body->addTextSection( 315 pht('EDIT NOTES'), 316 $description); 317 } 318 319 $body->addLinkSection( 320 pht('DOCUMENT DETAIL'), 321 PhabricatorEnv::getProductionURI( 322 PhrictionDocument::getSlugURI($object->getSlug()))); 323 324 return $body; 325 } 326 327 protected function shouldPublishFeedStory( 328 PhabricatorLiskDAO $object, 329 array $xactions) { 330 return $this->shouldSendMail($object, $xactions); 331 } 332 333 protected function getFeedRelatedPHIDs( 334 PhabricatorLiskDAO $object, 335 array $xactions) { 336 337 $phids = parent::getFeedRelatedPHIDs($object, $xactions); 338 339 foreach ($xactions as $xaction) { 340 switch ($xaction->getTransactionType()) { 341 case PhrictionDocumentMoveToTransaction::TRANSACTIONTYPE: 342 $dict = $xaction->getNewValue(); 343 $phids[] = $dict['phid']; 344 break; 345 } 346 } 347 348 return $phids; 349 } 350 351 protected function validateTransaction( 352 PhabricatorLiskDAO $object, 353 $type, 354 array $xactions) { 355 356 $errors = parent::validateTransaction($object, $type, $xactions); 357 358 foreach ($xactions as $xaction) { 359 switch ($type) { 360 case PhrictionDocumentContentTransaction::TRANSACTIONTYPE: 361 if ($xaction->getMetadataValue('stub:create:phid')) { 362 break; 363 } 364 365 if ($this->getProcessContentVersionError()) { 366 $error = $this->validateContentVersion($object, $type, $xaction); 367 if ($error) { 368 $this->setProcessContentVersionError(false); 369 $errors[] = $error; 370 } 371 } 372 373 if ($this->getIsNewObject()) { 374 $ancestry_errors = $this->validateAncestry( 375 $object, 376 $type, 377 $xaction, 378 self::VALIDATE_CREATE_ANCESTRY); 379 if ($ancestry_errors) { 380 $errors = array_merge($errors, $ancestry_errors); 381 } 382 } 383 break; 384 385 case PhrictionDocumentMoveToTransaction::TRANSACTIONTYPE: 386 $source_document = $xaction->getNewValue(); 387 388 $ancestry_errors = $this->validateAncestry( 389 $object, 390 $type, 391 $xaction, 392 self::VALIDATE_MOVE_ANCESTRY); 393 if ($ancestry_errors) { 394 $errors = array_merge($errors, $ancestry_errors); 395 } 396 397 $target_document = id(new PhrictionDocumentQuery()) 398 ->setViewer(PhabricatorUser::getOmnipotentUser()) 399 ->withSlugs(array($object->getSlug())) 400 ->needContent(true) 401 ->executeOne(); 402 403 // Prevent overwrites and no-op moves. 404 $exists = PhrictionDocumentStatus::STATUS_EXISTS; 405 if ($target_document) { 406 $message = null; 407 if ($target_document->getSlug() == $source_document->getSlug()) { 408 $message = pht( 409 'You can not move a document to its existing location. '. 410 'Choose a different location to move the document to.'); 411 } else if ($target_document->getStatus() == $exists) { 412 $message = pht( 413 'You can not move this document there, because it would '. 414 'overwrite an existing document which is already at that '. 415 'location. Move or delete the existing document first.'); 416 } 417 if ($message !== null) { 418 $error = new PhabricatorApplicationTransactionValidationError( 419 $type, 420 pht('Invalid'), 421 $message, 422 $xaction); 423 $errors[] = $error; 424 } 425 } 426 break; 427 428 } 429 } 430 431 return $errors; 432 } 433 434 public function validateAncestry( 435 PhabricatorLiskDAO $object, 436 $type, 437 PhabricatorApplicationTransaction $xaction, 438 $verb) { 439 440 $errors = array(); 441 // NOTE: We use the omnipotent user for these checks because policy 442 // doesn't matter; existence does. 443 $other_doc_viewer = PhabricatorUser::getOmnipotentUser(); 444 $ancestral_slugs = PhabricatorSlug::getAncestry($object->getSlug()); 445 if ($ancestral_slugs) { 446 $ancestors = id(new PhrictionDocumentQuery()) 447 ->setViewer($other_doc_viewer) 448 ->withSlugs($ancestral_slugs) 449 ->execute(); 450 $ancestors = mpull($ancestors, null, 'getSlug'); 451 foreach ($ancestral_slugs as $slug) { 452 $ancestor_doc = idx($ancestors, $slug); 453 if (!$ancestor_doc) { 454 $create_uri = '/phriction/edit/?slug='.$slug; 455 $create_link = phutil_tag( 456 'a', 457 array( 458 'href' => $create_uri, 459 ), 460 $slug); 461 switch ($verb) { 462 case self::VALIDATE_MOVE_ANCESTRY: 463 $message = pht( 464 'Can not move document because the parent document with '. 465 'slug %s does not exist!', 466 $create_link); 467 break; 468 case self::VALIDATE_CREATE_ANCESTRY: 469 $message = pht( 470 'Can not create document because the parent document with '. 471 'slug %s does not exist!', 472 $create_link); 473 break; 474 } 475 $error = new PhabricatorApplicationTransactionValidationError( 476 $type, 477 pht('Missing Ancestor'), 478 $message, 479 $xaction); 480 $errors[] = $error; 481 } 482 } 483 } 484 return $errors; 485 } 486 487 private function validateContentVersion( 488 PhabricatorLiskDAO $object, 489 $type, 490 PhabricatorApplicationTransaction $xaction) { 491 492 $error = null; 493 if ($this->getContentVersion() && 494 ($object->getMaxVersion() != $this->getContentVersion())) { 495 $error = new PhabricatorApplicationTransactionValidationError( 496 $type, 497 pht('Edit Conflict'), 498 pht( 499 'Another user made changes to this document after you began '. 500 'editing it. Do you want to overwrite their changes? '. 501 '(If you choose to overwrite their changes, you should review '. 502 'the document edit history to see what you overwrote, and '. 503 'then make another edit to merge the changes if necessary.)'), 504 $xaction); 505 } 506 return $error; 507 } 508 509 protected function supportsSearch() { 510 return true; 511 } 512 513 protected function shouldApplyHeraldRules( 514 PhabricatorLiskDAO $object, 515 array $xactions) { 516 return true; 517 } 518 519 protected function buildHeraldAdapter( 520 PhabricatorLiskDAO $object, 521 array $xactions) { 522 523 return id(new PhrictionDocumentHeraldAdapter()) 524 ->setDocument($object); 525 } 526 527 private function hasNewDocumentContent() { 528 return (bool)$this->newContent; 529 } 530 531 public function getNewDocumentContent(PhrictionDocument $document) { 532 if (!$this->hasNewDocumentContent()) { 533 $content = $this->newDocumentContent($document); 534 535 // Generate a PHID now so we can populate "contentPHID" before saving 536 // the document to the database: the column is not nullable so we need 537 // a value. 538 $content_phid = $content->generatePHID(); 539 540 $content->setPHID($content_phid); 541 542 $document->setContentPHID($content_phid); 543 $document->attachContent($content); 544 $document->setEditedEpoch(PhabricatorTime::getNow()); 545 $document->setMaxVersion($content->getVersion()); 546 547 $this->newContent = $content; 548 } 549 550 return $this->newContent; 551 } 552 553 private function newDocumentContent(PhrictionDocument $document) { 554 $content = id(new PhrictionContent()) 555 ->setSlug($document->getSlug()) 556 ->setAuthorPHID($this->getActingAsPHID()) 557 ->setChangeType(PhrictionChangeType::CHANGE_EDIT) 558 ->setTitle($this->getOldContent()->getTitle()) 559 ->setContent($this->getOldContent()->getContent()) 560 ->setDescription(''); 561 562 if (phutil_nonempty_string($this->getDescription())) { 563 $content->setDescription($this->getDescription()); 564 } 565 566 $content->setVersion($document->getMaxVersion() + 1); 567 568 return $content; 569 } 570 571 protected function getCustomWorkerState() { 572 return array( 573 'contentDiffURI' => $this->contentDiffURI, 574 ); 575 } 576 577 protected function loadCustomWorkerState(array $state) { 578 $this->contentDiffURI = idx($state, 'contentDiffURI'); 579 return $this; 580 } 581 582}