PageRenderTime 62ms CodeModel.GetById 29ms RepoModel.GetById 0ms app.codeStats 1ms

/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php

https://gitlab.com/jforge/phabricator
PHP | 2543 lines | 1761 code | 452 blank | 330 comment | 196 complexity | 3288ef90cc0ab60c79ce68e001bad454 MD5 | raw file
Possible License(s): LGPL-3.0, MIT, MPL-2.0-no-copyleft-exception, BSD-3-Clause, Apache-2.0, LGPL-2.0, LGPL-2.1

Large files files are truncated, but you can click here to view the full file

  1. <?php
  2. /**
  3. * @task mail Sending Mail
  4. * @task feed Publishing Feed Stories
  5. * @task search Search Index
  6. * @task files Integration with Files
  7. */
  8. abstract class PhabricatorApplicationTransactionEditor
  9. extends PhabricatorEditor {
  10. private $contentSource;
  11. private $object;
  12. private $xactions;
  13. private $isNewObject;
  14. private $mentionedPHIDs;
  15. private $continueOnNoEffect;
  16. private $continueOnMissingFields;
  17. private $parentMessageID;
  18. private $heraldAdapter;
  19. private $heraldTranscript;
  20. private $subscribers;
  21. private $unmentionablePHIDMap = array();
  22. private $isPreview;
  23. private $isHeraldEditor;
  24. private $isInverseEdgeEditor;
  25. private $actingAsPHID;
  26. private $disableEmail;
  27. /**
  28. * Get the class name for the application this editor is a part of.
  29. *
  30. * Uninstalling the application will disable the editor.
  31. *
  32. * @return string Editor's application class name.
  33. */
  34. abstract public function getEditorApplicationClass();
  35. /**
  36. * Get a description of the objects this editor edits, like "Differential
  37. * Revisions".
  38. *
  39. * @return string Human readable description of edited objects.
  40. */
  41. abstract public function getEditorObjectsDescription();
  42. public function setActingAsPHID($acting_as_phid) {
  43. $this->actingAsPHID = $acting_as_phid;
  44. return $this;
  45. }
  46. public function getActingAsPHID() {
  47. if ($this->actingAsPHID) {
  48. return $this->actingAsPHID;
  49. }
  50. return $this->getActor()->getPHID();
  51. }
  52. /**
  53. * When the editor tries to apply transactions that have no effect, should
  54. * it raise an exception (default) or drop them and continue?
  55. *
  56. * Generally, you will set this flag for edits coming from "Edit" interfaces,
  57. * and leave it cleared for edits coming from "Comment" interfaces, so the
  58. * user will get a useful error if they try to submit a comment that does
  59. * nothing (e.g., empty comment with a status change that has already been
  60. * performed by another user).
  61. *
  62. * @param bool True to drop transactions without effect and continue.
  63. * @return this
  64. */
  65. public function setContinueOnNoEffect($continue) {
  66. $this->continueOnNoEffect = $continue;
  67. return $this;
  68. }
  69. public function getContinueOnNoEffect() {
  70. return $this->continueOnNoEffect;
  71. }
  72. /**
  73. * When the editor tries to apply transactions which don't populate all of
  74. * an object's required fields, should it raise an exception (default) or
  75. * drop them and continue?
  76. *
  77. * For example, if a user adds a new required custom field (like "Severity")
  78. * to a task, all existing tasks won't have it populated. When users
  79. * manually edit existing tasks, it's usually desirable to have them provide
  80. * a severity. However, other operations (like batch editing just the
  81. * owner of a task) will fail by default.
  82. *
  83. * By setting this flag for edit operations which apply to specific fields
  84. * (like the priority, batch, and merge editors in Maniphest), these
  85. * operations can continue to function even if an object is outdated.
  86. *
  87. * @param bool True to continue when transactions don't completely satisfy
  88. * all required fields.
  89. * @return this
  90. */
  91. public function setContinueOnMissingFields($continue_on_missing_fields) {
  92. $this->continueOnMissingFields = $continue_on_missing_fields;
  93. return $this;
  94. }
  95. public function getContinueOnMissingFields() {
  96. return $this->continueOnMissingFields;
  97. }
  98. /**
  99. * Not strictly necessary, but reply handlers ideally set this value to
  100. * make email threading work better.
  101. */
  102. public function setParentMessageID($parent_message_id) {
  103. $this->parentMessageID = $parent_message_id;
  104. return $this;
  105. }
  106. public function getParentMessageID() {
  107. return $this->parentMessageID;
  108. }
  109. public function getIsNewObject() {
  110. return $this->isNewObject;
  111. }
  112. protected function getMentionedPHIDs() {
  113. return $this->mentionedPHIDs;
  114. }
  115. public function setIsPreview($is_preview) {
  116. $this->isPreview = $is_preview;
  117. return $this;
  118. }
  119. public function getIsPreview() {
  120. return $this->isPreview;
  121. }
  122. public function setIsInverseEdgeEditor($is_inverse_edge_editor) {
  123. $this->isInverseEdgeEditor = $is_inverse_edge_editor;
  124. return $this;
  125. }
  126. public function getIsInverseEdgeEditor() {
  127. return $this->isInverseEdgeEditor;
  128. }
  129. public function setIsHeraldEditor($is_herald_editor) {
  130. $this->isHeraldEditor = $is_herald_editor;
  131. return $this;
  132. }
  133. public function getIsHeraldEditor() {
  134. return $this->isHeraldEditor;
  135. }
  136. /**
  137. * Prevent this editor from generating email when applying transactions.
  138. *
  139. * @param bool True to disable email.
  140. * @return this
  141. */
  142. public function setDisableEmail($disable_email) {
  143. $this->disableEmail = $disable_email;
  144. return $this;
  145. }
  146. public function getDisableEmail() {
  147. return $this->disableEmail;
  148. }
  149. public function setUnmentionablePHIDMap(array $map) {
  150. $this->unmentionablePHIDMap = $map;
  151. return $this;
  152. }
  153. public function getUnmentionablePHIDMap() {
  154. return $this->unmentionablePHIDMap;
  155. }
  156. public function getTransactionTypes() {
  157. $types = array();
  158. if ($this->object instanceof PhabricatorSubscribableInterface) {
  159. $types[] = PhabricatorTransactions::TYPE_SUBSCRIBERS;
  160. }
  161. if ($this->object instanceof PhabricatorCustomFieldInterface) {
  162. $types[] = PhabricatorTransactions::TYPE_CUSTOMFIELD;
  163. }
  164. if ($this->object instanceof HarbormasterBuildableInterface) {
  165. $types[] = PhabricatorTransactions::TYPE_BUILDABLE;
  166. }
  167. if ($this->object instanceof PhabricatorTokenReceiverInterface) {
  168. $types[] = PhabricatorTransactions::TYPE_TOKEN;
  169. }
  170. if ($this->object instanceof PhabricatorProjectInterface) {
  171. $types[] = PhabricatorTransactions::TYPE_EDGE;
  172. }
  173. return $types;
  174. }
  175. private function adjustTransactionValues(
  176. PhabricatorLiskDAO $object,
  177. PhabricatorApplicationTransaction $xaction) {
  178. if ($xaction->shouldGenerateOldValue()) {
  179. $old = $this->getTransactionOldValue($object, $xaction);
  180. $xaction->setOldValue($old);
  181. }
  182. $new = $this->getTransactionNewValue($object, $xaction);
  183. $xaction->setNewValue($new);
  184. }
  185. private function getTransactionOldValue(
  186. PhabricatorLiskDAO $object,
  187. PhabricatorApplicationTransaction $xaction) {
  188. switch ($xaction->getTransactionType()) {
  189. case PhabricatorTransactions::TYPE_SUBSCRIBERS:
  190. return array_values($this->subscribers);
  191. case PhabricatorTransactions::TYPE_VIEW_POLICY:
  192. return $object->getViewPolicy();
  193. case PhabricatorTransactions::TYPE_EDIT_POLICY:
  194. return $object->getEditPolicy();
  195. case PhabricatorTransactions::TYPE_JOIN_POLICY:
  196. return $object->getJoinPolicy();
  197. case PhabricatorTransactions::TYPE_EDGE:
  198. $edge_type = $xaction->getMetadataValue('edge:type');
  199. if (!$edge_type) {
  200. throw new Exception("Edge transaction has no 'edge:type'!");
  201. }
  202. $old_edges = array();
  203. if ($object->getPHID()) {
  204. $edge_src = $object->getPHID();
  205. $old_edges = id(new PhabricatorEdgeQuery())
  206. ->withSourcePHIDs(array($edge_src))
  207. ->withEdgeTypes(array($edge_type))
  208. ->needEdgeData(true)
  209. ->execute();
  210. $old_edges = $old_edges[$edge_src][$edge_type];
  211. }
  212. return $old_edges;
  213. case PhabricatorTransactions::TYPE_CUSTOMFIELD:
  214. // NOTE: Custom fields have their old value pre-populated when they are
  215. // built by PhabricatorCustomFieldList.
  216. return $xaction->getOldValue();
  217. case PhabricatorTransactions::TYPE_COMMENT:
  218. return null;
  219. default:
  220. return $this->getCustomTransactionOldValue($object, $xaction);
  221. }
  222. }
  223. private function getTransactionNewValue(
  224. PhabricatorLiskDAO $object,
  225. PhabricatorApplicationTransaction $xaction) {
  226. switch ($xaction->getTransactionType()) {
  227. case PhabricatorTransactions::TYPE_SUBSCRIBERS:
  228. return $this->getPHIDTransactionNewValue($xaction);
  229. case PhabricatorTransactions::TYPE_VIEW_POLICY:
  230. case PhabricatorTransactions::TYPE_EDIT_POLICY:
  231. case PhabricatorTransactions::TYPE_JOIN_POLICY:
  232. case PhabricatorTransactions::TYPE_BUILDABLE:
  233. case PhabricatorTransactions::TYPE_TOKEN:
  234. return $xaction->getNewValue();
  235. case PhabricatorTransactions::TYPE_EDGE:
  236. return $this->getEdgeTransactionNewValue($xaction);
  237. case PhabricatorTransactions::TYPE_CUSTOMFIELD:
  238. $field = $this->getCustomFieldForTransaction($object, $xaction);
  239. return $field->getNewValueFromApplicationTransactions($xaction);
  240. case PhabricatorTransactions::TYPE_COMMENT:
  241. return null;
  242. default:
  243. return $this->getCustomTransactionNewValue($object, $xaction);
  244. }
  245. }
  246. protected function getCustomTransactionOldValue(
  247. PhabricatorLiskDAO $object,
  248. PhabricatorApplicationTransaction $xaction) {
  249. throw new Exception('Capability not supported!');
  250. }
  251. protected function getCustomTransactionNewValue(
  252. PhabricatorLiskDAO $object,
  253. PhabricatorApplicationTransaction $xaction) {
  254. throw new Exception('Capability not supported!');
  255. }
  256. protected function transactionHasEffect(
  257. PhabricatorLiskDAO $object,
  258. PhabricatorApplicationTransaction $xaction) {
  259. switch ($xaction->getTransactionType()) {
  260. case PhabricatorTransactions::TYPE_COMMENT:
  261. return $xaction->hasComment();
  262. case PhabricatorTransactions::TYPE_CUSTOMFIELD:
  263. $field = $this->getCustomFieldForTransaction($object, $xaction);
  264. return $field->getApplicationTransactionHasEffect($xaction);
  265. case PhabricatorTransactions::TYPE_EDGE:
  266. // A straight value comparison here doesn't always get the right
  267. // result, because newly added edges aren't fully populated. Instead,
  268. // compare the changes in a more granular way.
  269. $old = $xaction->getOldValue();
  270. $new = $xaction->getNewValue();
  271. $old_dst = array_keys($old);
  272. $new_dst = array_keys($new);
  273. // NOTE: For now, we don't consider edge reordering to be a change.
  274. // We have very few order-dependent edges and effectively no order
  275. // oriented UI. This might change in the future.
  276. sort($old_dst);
  277. sort($new_dst);
  278. if ($old_dst !== $new_dst) {
  279. // We've added or removed edges, so this transaction definitely
  280. // has an effect.
  281. return true;
  282. }
  283. // We haven't added or removed edges, but we might have changed
  284. // edge data.
  285. foreach ($old as $key => $old_value) {
  286. $new_value = $new[$key];
  287. if ($old_value['data'] !== $new_value['data']) {
  288. return true;
  289. }
  290. }
  291. return false;
  292. }
  293. return ($xaction->getOldValue() !== $xaction->getNewValue());
  294. }
  295. protected function shouldApplyInitialEffects(
  296. PhabricatorLiskDAO $object,
  297. array $xactions) {
  298. return false;
  299. }
  300. protected function applyInitialEffects(
  301. PhabricatorLiskDAO $object,
  302. array $xactions) {
  303. throw new PhutilMethodNotImplementedException();
  304. }
  305. private function applyInternalEffects(
  306. PhabricatorLiskDAO $object,
  307. PhabricatorApplicationTransaction $xaction) {
  308. switch ($xaction->getTransactionType()) {
  309. case PhabricatorTransactions::TYPE_BUILDABLE:
  310. case PhabricatorTransactions::TYPE_TOKEN:
  311. return;
  312. case PhabricatorTransactions::TYPE_VIEW_POLICY:
  313. $object->setViewPolicy($xaction->getNewValue());
  314. break;
  315. case PhabricatorTransactions::TYPE_EDIT_POLICY:
  316. $object->setEditPolicy($xaction->getNewValue());
  317. break;
  318. case PhabricatorTransactions::TYPE_CUSTOMFIELD:
  319. $field = $this->getCustomFieldForTransaction($object, $xaction);
  320. return $field->applyApplicationTransactionInternalEffects($xaction);
  321. }
  322. return $this->applyCustomInternalTransaction($object, $xaction);
  323. }
  324. private function applyExternalEffects(
  325. PhabricatorLiskDAO $object,
  326. PhabricatorApplicationTransaction $xaction) {
  327. switch ($xaction->getTransactionType()) {
  328. case PhabricatorTransactions::TYPE_BUILDABLE:
  329. case PhabricatorTransactions::TYPE_TOKEN:
  330. return;
  331. case PhabricatorTransactions::TYPE_SUBSCRIBERS:
  332. $subeditor = id(new PhabricatorSubscriptionsEditor())
  333. ->setObject($object)
  334. ->setActor($this->requireActor());
  335. $old_map = array_fuse($xaction->getOldValue());
  336. $new_map = array_fuse($xaction->getNewValue());
  337. $subeditor->unsubscribe(
  338. array_keys(
  339. array_diff_key($old_map, $new_map)));
  340. $subeditor->subscribeExplicit(
  341. array_keys(
  342. array_diff_key($new_map, $old_map)));
  343. $subeditor->save();
  344. // for the rest of these edits, subscribers should include those just
  345. // added as well as those just removed.
  346. $subscribers = array_unique(array_merge(
  347. $this->subscribers,
  348. $xaction->getOldValue(),
  349. $xaction->getNewValue()));
  350. $this->subscribers = $subscribers;
  351. break;
  352. case PhabricatorTransactions::TYPE_EDGE:
  353. if ($this->getIsInverseEdgeEditor()) {
  354. // If we're writing an inverse edge transaction, don't actually
  355. // do anything. The initiating editor on the other side of the
  356. // transaction will take care of the edge writes.
  357. break;
  358. }
  359. $old = $xaction->getOldValue();
  360. $new = $xaction->getNewValue();
  361. $src = $object->getPHID();
  362. $const = $xaction->getMetadataValue('edge:type');
  363. $type = PhabricatorEdgeType::getByConstant($const);
  364. if ($type->shouldWriteInverseTransactions()) {
  365. $this->applyInverseEdgeTransactions(
  366. $object,
  367. $xaction,
  368. $type->getInverseEdgeConstant());
  369. }
  370. foreach ($new as $dst_phid => $edge) {
  371. $new[$dst_phid]['src'] = $src;
  372. }
  373. $editor = new PhabricatorEdgeEditor();
  374. foreach ($old as $dst_phid => $edge) {
  375. if (!empty($new[$dst_phid])) {
  376. if ($old[$dst_phid]['data'] === $new[$dst_phid]['data']) {
  377. continue;
  378. }
  379. }
  380. $editor->removeEdge($src, $const, $dst_phid);
  381. }
  382. foreach ($new as $dst_phid => $edge) {
  383. if (!empty($old[$dst_phid])) {
  384. if ($old[$dst_phid]['data'] === $new[$dst_phid]['data']) {
  385. continue;
  386. }
  387. }
  388. $data = array(
  389. 'data' => $edge['data'],
  390. );
  391. $editor->addEdge($src, $const, $dst_phid, $data);
  392. }
  393. $editor->save();
  394. break;
  395. case PhabricatorTransactions::TYPE_CUSTOMFIELD:
  396. $field = $this->getCustomFieldForTransaction($object, $xaction);
  397. return $field->applyApplicationTransactionExternalEffects($xaction);
  398. }
  399. return $this->applyCustomExternalTransaction($object, $xaction);
  400. }
  401. protected function applyCustomInternalTransaction(
  402. PhabricatorLiskDAO $object,
  403. PhabricatorApplicationTransaction $xaction) {
  404. $type = $xaction->getTransactionType();
  405. throw new Exception(
  406. "Transaction type '{$type}' is missing an internal apply ".
  407. "implementation!");
  408. }
  409. protected function applyCustomExternalTransaction(
  410. PhabricatorLiskDAO $object,
  411. PhabricatorApplicationTransaction $xaction) {
  412. $type = $xaction->getTransactionType();
  413. throw new Exception(
  414. "Transaction type '{$type}' is missing an external apply ".
  415. "implementation!");
  416. }
  417. /**
  418. * Fill in a transaction's common values, like author and content source.
  419. */
  420. protected function populateTransaction(
  421. PhabricatorLiskDAO $object,
  422. PhabricatorApplicationTransaction $xaction) {
  423. $actor = $this->getActor();
  424. // TODO: This needs to be more sophisticated once we have meta-policies.
  425. $xaction->setViewPolicy(PhabricatorPolicies::POLICY_PUBLIC);
  426. if ($actor->isOmnipotent()) {
  427. $xaction->setEditPolicy(PhabricatorPolicies::POLICY_NOONE);
  428. } else {
  429. $xaction->setEditPolicy($this->getActingAsPHID());
  430. }
  431. $xaction->setAuthorPHID($this->getActingAsPHID());
  432. $xaction->setContentSource($this->getContentSource());
  433. $xaction->attachViewer($actor);
  434. $xaction->attachObject($object);
  435. if ($object->getPHID()) {
  436. $xaction->setObjectPHID($object->getPHID());
  437. }
  438. return $xaction;
  439. }
  440. protected function applyFinalEffects(
  441. PhabricatorLiskDAO $object,
  442. array $xactions) {
  443. return $xactions;
  444. }
  445. public function setContentSource(PhabricatorContentSource $content_source) {
  446. $this->contentSource = $content_source;
  447. return $this;
  448. }
  449. public function setContentSourceFromRequest(AphrontRequest $request) {
  450. return $this->setContentSource(
  451. PhabricatorContentSource::newFromRequest($request));
  452. }
  453. public function setContentSourceFromConduitRequest(
  454. ConduitAPIRequest $request) {
  455. $content_source = PhabricatorContentSource::newForSource(
  456. PhabricatorContentSource::SOURCE_CONDUIT,
  457. array());
  458. return $this->setContentSource($content_source);
  459. }
  460. public function getContentSource() {
  461. return $this->contentSource;
  462. }
  463. final public function applyTransactions(
  464. PhabricatorLiskDAO $object,
  465. array $xactions) {
  466. $this->object = $object;
  467. $this->xactions = $xactions;
  468. $this->isNewObject = ($object->getPHID() === null);
  469. $this->validateEditParameters($object, $xactions);
  470. $actor = $this->requireActor();
  471. // NOTE: Some transaction expansion requires that the edited object be
  472. // attached.
  473. foreach ($xactions as $xaction) {
  474. $xaction->attachObject($object);
  475. $xaction->attachViewer($actor);
  476. }
  477. $xactions = $this->expandTransactions($object, $xactions);
  478. $xactions = $this->expandSupportTransactions($object, $xactions);
  479. $xactions = $this->combineTransactions($xactions);
  480. foreach ($xactions as $xaction) {
  481. $xaction = $this->populateTransaction($object, $xaction);
  482. }
  483. $is_preview = $this->getIsPreview();
  484. $read_locking = false;
  485. $transaction_open = false;
  486. if (!$is_preview) {
  487. $errors = array();
  488. $type_map = mgroup($xactions, 'getTransactionType');
  489. foreach ($this->getTransactionTypes() as $type) {
  490. $type_xactions = idx($type_map, $type, array());
  491. $errors[] = $this->validateTransaction($object, $type, $type_xactions);
  492. }
  493. $errors = array_mergev($errors);
  494. $continue_on_missing = $this->getContinueOnMissingFields();
  495. foreach ($errors as $key => $error) {
  496. if ($continue_on_missing && $error->getIsMissingFieldError()) {
  497. unset($errors[$key]);
  498. }
  499. }
  500. if ($errors) {
  501. throw new PhabricatorApplicationTransactionValidationException($errors);
  502. }
  503. $file_phids = $this->extractFilePHIDs($object, $xactions);
  504. if ($object->getID()) {
  505. foreach ($xactions as $xaction) {
  506. // If any of the transactions require a read lock, hold one and
  507. // reload the object. We need to do this fairly early so that the
  508. // call to `adjustTransactionValues()` (which populates old values)
  509. // is based on the synchronized state of the object, which may differ
  510. // from the state when it was originally loaded.
  511. if ($this->shouldReadLock($object, $xaction)) {
  512. $object->openTransaction();
  513. $object->beginReadLocking();
  514. $transaction_open = true;
  515. $read_locking = true;
  516. $object->reload();
  517. break;
  518. }
  519. }
  520. }
  521. if ($this->shouldApplyInitialEffects($object, $xactions)) {
  522. if (!$transaction_open) {
  523. $object->openTransaction();
  524. $transaction_open = true;
  525. }
  526. }
  527. }
  528. if ($this->shouldApplyInitialEffects($object, $xactions)) {
  529. $this->applyInitialEffects($object, $xactions);
  530. }
  531. foreach ($xactions as $xaction) {
  532. $this->adjustTransactionValues($object, $xaction);
  533. }
  534. $xactions = $this->filterTransactions($object, $xactions);
  535. if (!$xactions) {
  536. if ($read_locking) {
  537. $object->endReadLocking();
  538. $read_locking = false;
  539. }
  540. if ($transaction_open) {
  541. $object->killTransaction();
  542. $transaction_open = false;
  543. }
  544. return array();
  545. }
  546. // Now that we've merged, filtered, and combined transactions, check for
  547. // required capabilities.
  548. foreach ($xactions as $xaction) {
  549. $this->requireCapabilities($object, $xaction);
  550. }
  551. $xactions = $this->sortTransactions($xactions);
  552. if ($is_preview) {
  553. $this->loadHandles($xactions);
  554. return $xactions;
  555. }
  556. $comment_editor = id(new PhabricatorApplicationTransactionCommentEditor())
  557. ->setActor($actor)
  558. ->setActingAsPHID($this->getActingAsPHID())
  559. ->setContentSource($this->getContentSource());
  560. if (!$transaction_open) {
  561. $object->openTransaction();
  562. }
  563. foreach ($xactions as $xaction) {
  564. $this->applyInternalEffects($object, $xaction);
  565. }
  566. $object->save();
  567. foreach ($xactions as $xaction) {
  568. $xaction->setObjectPHID($object->getPHID());
  569. if ($xaction->getComment()) {
  570. $xaction->setPHID($xaction->generatePHID());
  571. $comment_editor->applyEdit($xaction, $xaction->getComment());
  572. } else {
  573. $xaction->save();
  574. }
  575. }
  576. if ($file_phids) {
  577. $this->attachFiles($object, $file_phids);
  578. }
  579. foreach ($xactions as $xaction) {
  580. $this->applyExternalEffects($object, $xaction);
  581. }
  582. $xactions = $this->applyFinalEffects($object, $xactions);
  583. if ($read_locking) {
  584. $object->endReadLocking();
  585. $read_locking = false;
  586. }
  587. $object->saveTransaction();
  588. // Now that we've completely applied the core transaction set, try to apply
  589. // Herald rules. Herald rules are allowed to either take direct actions on
  590. // the database (like writing flags), or take indirect actions (like saving
  591. // some targets for CC when we generate mail a little later), or return
  592. // transactions which we'll apply normally using another Editor.
  593. // First, check if *this* is a sub-editor which is itself applying Herald
  594. // rules: if it is, stop working and return so we don't descend into
  595. // madness.
  596. // Otherwise, we're not a Herald editor, so process Herald rules (possibly
  597. // using a Herald editor to apply resulting transactions) and then send out
  598. // mail, notifications, and feed updates about everything.
  599. if ($this->getIsHeraldEditor()) {
  600. // We are the Herald editor, so stop work here and return the updated
  601. // transactions.
  602. return $xactions;
  603. } else if ($this->shouldApplyHeraldRules($object, $xactions)) {
  604. // We are not the Herald editor, so try to apply Herald rules.
  605. $herald_xactions = $this->applyHeraldRules($object, $xactions);
  606. if ($herald_xactions) {
  607. $xscript_id = $this->getHeraldTranscript()->getID();
  608. foreach ($herald_xactions as $herald_xaction) {
  609. $herald_xaction->setMetadataValue('herald:transcriptID', $xscript_id);
  610. }
  611. // NOTE: We're acting as the omnipotent user because rules deal with
  612. // their own policy issues. We use a synthetic author PHID (the
  613. // Herald application) as the author of record, so that transactions
  614. // will render in a reasonable way ("Herald assigned this task ...").
  615. $herald_actor = PhabricatorUser::getOmnipotentUser();
  616. $herald_phid = id(new PhabricatorHeraldApplication())->getPHID();
  617. // TODO: It would be nice to give transactions a more specific source
  618. // which points at the rule which generated them. You can figure this
  619. // out from transcripts, but it would be cleaner if you didn't have to.
  620. $herald_source = PhabricatorContentSource::newForSource(
  621. PhabricatorContentSource::SOURCE_HERALD,
  622. array());
  623. $herald_editor = newv(get_class($this), array())
  624. ->setContinueOnNoEffect(true)
  625. ->setContinueOnMissingFields(true)
  626. ->setParentMessageID($this->getParentMessageID())
  627. ->setIsHeraldEditor(true)
  628. ->setActor($herald_actor)
  629. ->setActingAsPHID($herald_phid)
  630. ->setContentSource($herald_source);
  631. $herald_xactions = $herald_editor->applyTransactions(
  632. $object,
  633. $herald_xactions);
  634. // Merge the new transactions into the transaction list: we want to
  635. // send email and publish feed stories about them, too.
  636. $xactions = array_merge($xactions, $herald_xactions);
  637. }
  638. }
  639. // Before sending mail or publishing feed stories, reload the object
  640. // subscribers to pick up changes caused by Herald (or by other side effects
  641. // in various transaction phases).
  642. $this->loadSubscribers($object);
  643. $this->loadHandles($xactions);
  644. $mail = null;
  645. if (!$this->getDisableEmail()) {
  646. if ($this->shouldSendMail($object, $xactions)) {
  647. $mail = $this->sendMail($object, $xactions);
  648. }
  649. }
  650. if ($this->supportsSearch()) {
  651. id(new PhabricatorSearchIndexer())
  652. ->queueDocumentForIndexing($object->getPHID());
  653. }
  654. if ($this->shouldPublishFeedStory($object, $xactions)) {
  655. $mailed = array();
  656. if ($mail) {
  657. $mailed = $mail->buildRecipientList();
  658. }
  659. $this->publishFeedStory(
  660. $object,
  661. $xactions,
  662. $mailed);
  663. }
  664. $this->didApplyTransactions($xactions);
  665. if ($object instanceof PhabricatorCustomFieldInterface) {
  666. // Maybe this makes more sense to move into the search index itself? For
  667. // now I'm putting it here since I think we might end up with things that
  668. // need it to be up to date once the next page loads, but if we don't go
  669. // there we we could move it into search once search moves to the daemons.
  670. // It now happens in the search indexer as well, but the search indexer is
  671. // always daemonized, so the logic above still potentially holds. We could
  672. // possibly get rid of this. The major motivation for putting it in the
  673. // indexer was to enable reindexing to work.
  674. $fields = PhabricatorCustomField::getObjectFields(
  675. $object,
  676. PhabricatorCustomField::ROLE_APPLICATIONSEARCH);
  677. $fields->readFieldsFromStorage($object);
  678. $fields->rebuildIndexes($object);
  679. }
  680. return $xactions;
  681. }
  682. protected function didApplyTransactions(array $xactions) {
  683. // Hook for subclasses.
  684. return;
  685. }
  686. /**
  687. * Determine if the editor should hold a read lock on the object while
  688. * applying a transaction.
  689. *
  690. * If the editor does not hold a lock, two editors may read an object at the
  691. * same time, then apply their changes without any synchronization. For most
  692. * transactions, this does not matter much. However, it is important for some
  693. * transactions. For example, if an object has a transaction count on it, both
  694. * editors may read the object with `count = 23`, then independently update it
  695. * and save the object with `count = 24` twice. This will produce the wrong
  696. * state: the object really has 25 transactions, but the count is only 24.
  697. *
  698. * Generally, transactions fall into one of four buckets:
  699. *
  700. * - Append operations: Actions like adding a comment to an object purely
  701. * add information to its state, and do not depend on the current object
  702. * state in any way. These transactions never need to hold locks.
  703. * - Overwrite operations: Actions like changing the title or description
  704. * of an object replace the current value with a new value, so the end
  705. * state is consistent without a lock. We currently do not lock these
  706. * transactions, although we may in the future.
  707. * - Edge operations: Edge and subscription operations have internal
  708. * synchronization which limits the damage race conditions can cause.
  709. * We do not currently lock these transactions, although we may in the
  710. * future.
  711. * - Update operations: Actions like incrementing a count on an object.
  712. * These operations generally should use locks, unless it is not
  713. * important that the state remain consistent in the presence of races.
  714. *
  715. * @param PhabricatorLiskDAO Object being updated.
  716. * @param PhabricatorApplicationTransaction Transaction being applied.
  717. * @return bool True to synchronize the edit with a lock.
  718. */
  719. protected function shouldReadLock(
  720. PhabricatorLiskDAO $object,
  721. PhabricatorApplicationTransaction $xaction) {
  722. return false;
  723. }
  724. private function loadHandles(array $xactions) {
  725. $phids = array();
  726. foreach ($xactions as $key => $xaction) {
  727. $phids[$key] = $xaction->getRequiredHandlePHIDs();
  728. }
  729. $handles = array();
  730. $merged = array_mergev($phids);
  731. if ($merged) {
  732. $handles = id(new PhabricatorHandleQuery())
  733. ->setViewer($this->requireActor())
  734. ->withPHIDs($merged)
  735. ->execute();
  736. }
  737. foreach ($xactions as $key => $xaction) {
  738. $xaction->setHandles(array_select_keys($handles, $phids[$key]));
  739. }
  740. }
  741. private function loadSubscribers(PhabricatorLiskDAO $object) {
  742. if ($object->getPHID() &&
  743. ($object instanceof PhabricatorSubscribableInterface)) {
  744. $subs = PhabricatorSubscribersQuery::loadSubscribersForPHID(
  745. $object->getPHID());
  746. $this->subscribers = array_fuse($subs);
  747. } else {
  748. $this->subscribers = array();
  749. }
  750. }
  751. private function validateEditParameters(
  752. PhabricatorLiskDAO $object,
  753. array $xactions) {
  754. if (!$this->getContentSource()) {
  755. throw new Exception(
  756. 'Call setContentSource() before applyTransactions()!');
  757. }
  758. // Do a bunch of sanity checks that the incoming transactions are fresh.
  759. // They should be unsaved and have only "transactionType" and "newValue"
  760. // set.
  761. $types = array_fill_keys($this->getTransactionTypes(), true);
  762. assert_instances_of($xactions, 'PhabricatorApplicationTransaction');
  763. foreach ($xactions as $xaction) {
  764. if ($xaction->getPHID() || $xaction->getID()) {
  765. throw new PhabricatorApplicationTransactionStructureException(
  766. $xaction,
  767. pht(
  768. 'You can not apply transactions which already have IDs/PHIDs!'));
  769. }
  770. if ($xaction->getObjectPHID()) {
  771. throw new PhabricatorApplicationTransactionStructureException(
  772. $xaction,
  773. pht(
  774. 'You can not apply transactions which already have objectPHIDs!'));
  775. }
  776. if ($xaction->getAuthorPHID()) {
  777. throw new PhabricatorApplicationTransactionStructureException(
  778. $xaction,
  779. pht(
  780. 'You can not apply transactions which already have authorPHIDs!'));
  781. }
  782. if ($xaction->getCommentPHID()) {
  783. throw new PhabricatorApplicationTransactionStructureException(
  784. $xaction,
  785. pht(
  786. 'You can not apply transactions which already have '.
  787. 'commentPHIDs!'));
  788. }
  789. if ($xaction->getCommentVersion() !== 0) {
  790. throw new PhabricatorApplicationTransactionStructureException(
  791. $xaction,
  792. pht(
  793. 'You can not apply transactions which already have '.
  794. 'commentVersions!'));
  795. }
  796. $expect_value = !$xaction->shouldGenerateOldValue();
  797. $has_value = $xaction->hasOldValue();
  798. if ($expect_value && !$has_value) {
  799. throw new PhabricatorApplicationTransactionStructureException(
  800. $xaction,
  801. pht(
  802. 'This transaction is supposed to have an oldValue set, but '.
  803. 'it does not!'));
  804. }
  805. if ($has_value && !$expect_value) {
  806. throw new PhabricatorApplicationTransactionStructureException(
  807. $xaction,
  808. pht(
  809. 'This transaction should generate its oldValue automatically, '.
  810. 'but has already had one set!'));
  811. }
  812. $type = $xaction->getTransactionType();
  813. if (empty($types[$type])) {
  814. throw new PhabricatorApplicationTransactionStructureException(
  815. $xaction,
  816. pht(
  817. 'Transaction has type "%s", but that transaction type is not '.
  818. 'supported by this editor (%s).',
  819. $type,
  820. get_class($this)));
  821. }
  822. }
  823. }
  824. protected function requireCapabilities(
  825. PhabricatorLiskDAO $object,
  826. PhabricatorApplicationTransaction $xaction) {
  827. if ($this->getIsNewObject()) {
  828. return;
  829. }
  830. $actor = $this->requireActor();
  831. switch ($xaction->getTransactionType()) {
  832. case PhabricatorTransactions::TYPE_COMMENT:
  833. PhabricatorPolicyFilter::requireCapability(
  834. $actor,
  835. $object,
  836. PhabricatorPolicyCapability::CAN_VIEW);
  837. break;
  838. case PhabricatorTransactions::TYPE_VIEW_POLICY:
  839. PhabricatorPolicyFilter::requireCapability(
  840. $actor,
  841. $object,
  842. PhabricatorPolicyCapability::CAN_EDIT);
  843. break;
  844. case PhabricatorTransactions::TYPE_EDIT_POLICY:
  845. PhabricatorPolicyFilter::requireCapability(
  846. $actor,
  847. $object,
  848. PhabricatorPolicyCapability::CAN_EDIT);
  849. break;
  850. case PhabricatorTransactions::TYPE_JOIN_POLICY:
  851. PhabricatorPolicyFilter::requireCapability(
  852. $actor,
  853. $object,
  854. PhabricatorPolicyCapability::CAN_EDIT);
  855. break;
  856. }
  857. }
  858. private function buildSubscribeTransaction(
  859. PhabricatorLiskDAO $object,
  860. array $xactions,
  861. array $blocks) {
  862. if (!($object instanceof PhabricatorSubscribableInterface)) {
  863. return null;
  864. }
  865. $texts = array_mergev($blocks);
  866. $phids = PhabricatorMarkupEngine::extractPHIDsFromMentions(
  867. $this->getActor(),
  868. $texts);
  869. $this->mentionedPHIDs = $phids;
  870. if ($object->getPHID()) {
  871. // Don't try to subscribe already-subscribed mentions: we want to generate
  872. // a dialog about an action having no effect if the user explicitly adds
  873. // existing CCs, but not if they merely mention existing subscribers.
  874. $phids = array_diff($phids, $this->subscribers);
  875. }
  876. foreach ($phids as $key => $phid) {
  877. if ($object->isAutomaticallySubscribed($phid)) {
  878. unset($phids[$key]);
  879. }
  880. }
  881. $phids = array_values($phids);
  882. if (!$phids) {
  883. return null;
  884. }
  885. $xaction = newv(get_class(head($xactions)), array());
  886. $xaction->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS);
  887. $xaction->setNewValue(array('+' => $phids));
  888. return $xaction;
  889. }
  890. protected function getRemarkupBlocksFromTransaction(
  891. PhabricatorApplicationTransaction $transaction) {
  892. return $transaction->getRemarkupBlocks();
  893. }
  894. protected function mergeTransactions(
  895. PhabricatorApplicationTransaction $u,
  896. PhabricatorApplicationTransaction $v) {
  897. $type = $u->getTransactionType();
  898. switch ($type) {
  899. case PhabricatorTransactions::TYPE_SUBSCRIBERS:
  900. return $this->mergePHIDOrEdgeTransactions($u, $v);
  901. case PhabricatorTransactions::TYPE_EDGE:
  902. $u_type = $u->getMetadataValue('edge:type');
  903. $v_type = $v->getMetadataValue('edge:type');
  904. if ($u_type == $v_type) {
  905. return $this->mergePHIDOrEdgeTransactions($u, $v);
  906. }
  907. return null;
  908. }
  909. // By default, do not merge the transactions.
  910. return null;
  911. }
  912. /**
  913. * Optionally expand transactions which imply other effects. For example,
  914. * resigning from a revision in Differential implies removing yourself as
  915. * a reviewer.
  916. */
  917. private function expandTransactions(
  918. PhabricatorLiskDAO $object,
  919. array $xactions) {
  920. $results = array();
  921. foreach ($xactions as $xaction) {
  922. foreach ($this->expandTransaction($object, $xaction) as $expanded) {
  923. $results[] = $expanded;
  924. }
  925. }
  926. return $results;
  927. }
  928. protected function expandTransaction(
  929. PhabricatorLiskDAO $object,
  930. PhabricatorApplicationTransaction $xaction) {
  931. return array($xaction);
  932. }
  933. private function expandSupportTransactions(
  934. PhabricatorLiskDAO $object,
  935. array $xactions) {
  936. $this->loadSubscribers($object);
  937. $xactions = $this->applyImplicitCC($object, $xactions);
  938. $blocks = array();
  939. foreach ($xactions as $key => $xaction) {
  940. $blocks[$key] = $this->getRemarkupBlocksFromTransaction($xaction);
  941. }
  942. $subscribe_xaction = $this->buildSubscribeTransaction(
  943. $object,
  944. $xactions,
  945. $blocks);
  946. if ($subscribe_xaction) {
  947. $xactions[] = $subscribe_xaction;
  948. }
  949. // TODO: For now, this is just a placeholder.
  950. $engine = PhabricatorMarkupEngine::getEngine('extract');
  951. $engine->setConfig('viewer', $this->requireActor());
  952. $block_xactions = $this->expandRemarkupBlockTransactions(
  953. $object,
  954. $xactions,
  955. $blocks,
  956. $engine);
  957. foreach ($block_xactions as $xaction) {
  958. $xactions[] = $xaction;
  959. }
  960. return $xactions;
  961. }
  962. private function expandRemarkupBlockTransactions(
  963. PhabricatorLiskDAO $object,
  964. array $xactions,
  965. $blocks,
  966. PhutilMarkupEngine $engine) {
  967. $block_xactions = $this->expandCustomRemarkupBlockTransactions(
  968. $object,
  969. $xactions,
  970. $blocks,
  971. $engine);
  972. $mentioned_phids = array();
  973. foreach ($blocks as $key => $xaction_blocks) {
  974. foreach ($xaction_blocks as $block) {
  975. $engine->markupText($block);
  976. $mentioned_phids += $engine->getTextMetadata(
  977. PhabricatorObjectRemarkupRule::KEY_MENTIONED_OBJECTS,
  978. array());
  979. }
  980. }
  981. if (!$mentioned_phids) {
  982. return $block_xactions;
  983. }
  984. if ($object instanceof PhabricatorProjectInterface) {
  985. $phids = $mentioned_phids;
  986. $project_type = PhabricatorProjectProjectPHIDType::TYPECONST;
  987. foreach ($phids as $key => $phid) {
  988. if (phid_get_type($phid) != $project_type) {
  989. unset($phids[$key]);
  990. }
  991. }
  992. if ($phids) {
  993. $edge_type = PhabricatorProjectObjectHasProjectEdgeType::EDGECONST;
  994. $block_xactions[] = newv(get_class(head($xactions)), array())
  995. ->setIgnoreOnNoEffect(true)
  996. ->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
  997. ->setMetadataValue('edge:type', $edge_type)
  998. ->setNewValue(array('+' => $phids));
  999. }
  1000. }
  1001. $mentioned_objects = id(new PhabricatorObjectQuery())
  1002. ->setViewer($this->getActor())
  1003. ->withPHIDs($mentioned_phids)
  1004. ->execute();
  1005. $mentionable_phids = array();
  1006. foreach ($mentioned_objects as $mentioned_object) {
  1007. if ($mentioned_object instanceof PhabricatorMentionableInterface) {
  1008. $mentioned_phid = $mentioned_object->getPHID();
  1009. if (idx($this->getUnmentionablePHIDMap(), $mentioned_phid)) {
  1010. continue;
  1011. }
  1012. // don't let objects mention themselves
  1013. if ($object->getPHID() && $mentioned_phid == $object->getPHID()) {
  1014. continue;
  1015. }
  1016. $mentionable_phids[$mentioned_phid] = $mentioned_phid;
  1017. }
  1018. }
  1019. if ($mentionable_phids) {
  1020. $edge_type = PhabricatorObjectMentionsObject::EDGECONST;
  1021. $block_xactions[] = newv(get_class(head($xactions)), array())
  1022. ->setIgnoreOnNoEffect(true)
  1023. ->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
  1024. ->setMetadataValue('edge:type', $edge_type)
  1025. ->setNewValue(array('+' => $mentionable_phids));
  1026. }
  1027. return $block_xactions;
  1028. }
  1029. protected function expandCustomRemarkupBlockTransactions(
  1030. PhabricatorLiskDAO $object,
  1031. array $xactions,
  1032. $blocks,
  1033. PhutilMarkupEngine $engine) {
  1034. return array();
  1035. }
  1036. /**
  1037. * Attempt to combine similar transactions into a smaller number of total
  1038. * transactions. For example, two transactions which edit the title of an
  1039. * object can be merged into a single edit.
  1040. */
  1041. private function combineTransactions(array $xactions) {
  1042. $stray_comments = array();
  1043. $result = array();
  1044. $types = array();
  1045. foreach ($xactions as $key => $xaction) {
  1046. $type = $xaction->getTransactionType();
  1047. if (isset($types[$type])) {
  1048. foreach ($types[$type] as $other_key) {
  1049. $merged = $this->mergeTransactions($result[$other_key], $xaction);
  1050. if ($merged) {
  1051. $result[$other_key] = $merged;
  1052. if ($xaction->getComment() &&
  1053. ($xaction->getComment() !== $merged->getComment())) {
  1054. $stray_comments[] = $xaction->getComment();
  1055. }
  1056. if ($result[$other_key]->getComment() &&
  1057. ($result[$other_key]->getComment() !== $merged->getComment())) {
  1058. $stray_comments[] = $result[$other_key]->getComment();
  1059. }
  1060. // Move on to the next transaction.
  1061. continue 2;
  1062. }
  1063. }
  1064. }
  1065. $result[$key] = $xaction;
  1066. $types[$type][] = $key;
  1067. }
  1068. // If we merged any comments away, restore them.
  1069. foreach ($stray_comments as $comment) {
  1070. $xaction = newv(get_class(head($result)), array());
  1071. $xaction->setTransactionType(PhabricatorTransactions::TYPE_COMMENT);
  1072. $xaction->setComment($comment);
  1073. $result[] = $xaction;
  1074. }
  1075. return array_values($result);
  1076. }
  1077. protected function mergePHIDOrEdgeTransactions(
  1078. PhabricatorApplicationTransaction $u,
  1079. PhabricatorApplicationTransaction $v) {
  1080. $result = $u->getNewValue();
  1081. foreach ($v->getNewValue() as $key => $value) {
  1082. if ($u->getTransactionType() == PhabricatorTransactions::TYPE_EDGE) {
  1083. if (empty($result[$key])) {
  1084. $result[$key] = $value;
  1085. } else {
  1086. // We're merging two lists of edge adds, sets, or removes. Merge
  1087. // them by merging individual PHIDs within them.
  1088. $merged = $result[$key];
  1089. foreach ($value as $dst => $v_spec) {
  1090. if (empty($merged[$dst])) {
  1091. $merged[$dst] = $v_spec;
  1092. } else {
  1093. // Two transactions are trying to perform the same operation on
  1094. // the same edge. Normalize the edge data and then merge it. This
  1095. // allows transactions to specify how data merges execute in a
  1096. // precise way.
  1097. $u_spec = $merged[$dst];
  1098. if (!is_array($u_spec)) {
  1099. $u_spec = array('dst' => $u_spec);
  1100. }
  1101. if (!is_array($v_spec)) {
  1102. $v_spec = array('dst' => $v_spec);
  1103. }
  1104. $ux_data = idx($u_spec, 'data', array());
  1105. $vx_data = idx($v_spec, 'data', array());
  1106. $merged_data = $this->mergeEdgeData(
  1107. $u->getMetadataValue('edge:type'),
  1108. $ux_data,
  1109. $vx_data);
  1110. $u_spec['data'] = $merged_data;
  1111. $merged[$dst] = $u_spec;
  1112. }
  1113. }
  1114. $result[$key] = $merged;
  1115. }
  1116. } else {
  1117. $result[$key] = array_merge($value, idx($result, $key, array()));
  1118. }
  1119. }
  1120. $u->setNewValue($result);
  1121. // When combining an "ignore" transaction with a normal transaction, make
  1122. // sure we don't propagate the "ignore" flag.
  1123. if (!$v->getIgnoreOnNoEffect()) {
  1124. $u->setIgnoreOnNoEffect(false);
  1125. }
  1126. return $u;
  1127. }
  1128. protected function mergeEdgeData($type, array $u, array $v) {
  1129. return $v + $u;
  1130. }
  1131. protected function getPHIDTransactionNewValue(
  1132. PhabricatorApplicationTransaction $xaction) {
  1133. $old = array_fuse($xaction->getOldValue());
  1134. $new = $xaction->getNewValue();
  1135. $new_add = idx($new, '+', array());
  1136. unset($new['+']);
  1137. $new_rem = idx($new, '-', array());
  1138. unset($new['-']);
  1139. $new_set = idx($new, '=', null);
  1140. if ($new_set !== null) {
  1141. $new_set = array_fuse($new_set);
  1142. }
  1143. unset($new['=']);
  1144. if ($new) {
  1145. throw new Exception(
  1146. "Invalid 'new' value for PHID transaction. Value should contain only ".
  1147. "keys '+' (add PHIDs), '-' (remove PHIDs) and '=' (set PHIDS).");
  1148. }
  1149. $result = array();
  1150. foreach ($old as $phid) {
  1151. if ($new_set !== null && empty($new_set[$phid])) {
  1152. continue;
  1153. }
  1154. $result[$phid] = $phid;
  1155. }
  1156. if ($new_set !== null) {
  1157. foreach ($new_set as $phid) {
  1158. $result[$phid] = $phid;
  1159. }
  1160. }
  1161. foreach ($new_add as $phid) {
  1162. $result[$phid] = $phid;
  1163. }
  1164. foreach ($new_rem as $phid) {
  1165. unset($result[$phid]);
  1166. }
  1167. return array_values($result);
  1168. }
  1169. protected function getEdgeTransactionNewValue(
  1170. PhabricatorApplicationTransaction $xaction) {
  1171. $new = $xaction->getNewValue();
  1172. $new_add = idx($new, '+', array());
  1173. unset($new['+']);
  1174. $new_rem = idx($new, '-', array());
  1175. unset($new['-']);
  1176. $new_set = idx($new, '=', null);
  1177. unset($new['=']);
  1178. if ($new) {
  1179. throw new Exception(
  1180. "Invalid 'new' value for Edge transaction. Value should contain only ".
  1181. "keys '+' (add edges), '-' (remove edges) and '=' (set edges).");
  1182. }
  1183. $old = $xaction->getOldValue();
  1184. $lists = array($new_set, $new_add, $new_rem);
  1185. foreach ($lists as $list) {
  1186. $this->checkEdgeList($list);
  1187. }
  1188. $result = array();
  1189. foreach ($old as $dst_phid => $edge) {
  1190. if ($new_set !== null && empty($new_set[$dst_phid])) {
  1191. continue;
  1192. }
  1193. $result[$dst_phid] = $this->normalizeEdgeTransactionValue(
  1194. $xaction,
  1195. $edge,
  1196. $dst_phid);
  1197. }
  1198. if ($new_set !== null) {
  1199. foreach ($new_set as $dst_phid => $edge) {
  1200. $result[$dst_phid] = $this->normalizeEdgeTransactionValue(
  1201. $xaction,
  1202. $edge,
  1203. $dst_phid);
  1204. }
  1205. }
  1206. foreach ($new_add as $dst_phid => $edge) {
  1207. $result[$dst_phid] = $this->normalizeEdgeTransactionValue(
  1208. $xaction,
  1209. $edge,
  1210. $dst_phid);
  1211. }
  1212. foreach ($new_rem as $dst_phid => $edge) {
  1213. unset($result[$dst_phid]);
  1214. }
  1215. return $result;
  1216. }
  1217. private function checkEdgeList($list) {
  1218. if (!$list) {
  1219. return;
  1220. }
  1221. foreach ($list as $key => $item) {
  1222. if (phid_get_type($key) === PhabricatorPHIDConstants::PHID_TYPE_UNKNOWN) {
  1223. throw new Exception(
  1224. "Edge transactions must have destination PHIDs as in edge ".
  1225. "lists (found key '{$key}').");
  1226. }
  1227. if (!is_array($item) && $item !== $key) {
  1228. throw new Exception(
  1229. "Edge transactions must have PHIDs or edge specs as values ".
  1230. "(found value '{$item}').");
  1231. }
  1232. }
  1233. }
  1234. private function normalizeEdgeTransactionValue(
  1235. PhabricatorApplicationTransaction $xaction,
  1236. $edge,
  1237. $dst_phid) {
  1238. if (!is_array($edge)) {
  1239. if ($edge != $dst_phid) {
  1240. throw new Exception(
  1241. pht(
  1242. 'Transaction edge data must either be the edge PHID or an edge '.
  1243. 'specification dictionary.'));
  1244. }
  1245. $edge = array();
  1246. } else {
  1247. foreach ($edge as $key => $value) {
  1248. switch ($key) {
  1249. case 'src':
  1250. case 'dst':
  1251. case 'type':
  1252. case 'data':
  1253. case 'dateCreated':
  1254. case 'dateModified':
  1255. case 'seq':
  1256. case 'dataID':
  1257. break;
  1258. default:
  1259. throw new Exception(
  1260. pht(
  1261. 'Transaction edge specification contains unexpected key '.
  1262. '"%s".',
  1263. $key));
  1264. }
  1265. }
  1266. }
  1267. $edge['dst'] = $dst_phid;
  1268. $edge_type = $xaction->getMetadataValue('edge:type');
  1269. if (empty($edge['type'])) {
  1270. $edge['type'] = $edge_type;
  1271. } else {
  1272. if ($edge['type'] != $edge_type) {
  1273. $this_type = $edge['type'];
  1274. throw new Exception(
  1275. "Edge transaction includes edge of type '{$this_type}', but ".
  1276. "transaction is of type '{$edge_type}'. Each edge transaction must ".
  1277. "alter edges of only one type.");
  1278. }
  1279. }
  1280. if (!isset($edge['data'])) {
  1281. $edge['data'] = array();
  1282. }
  1283. return $edge;
  1284. }
  1285. protected function sortTransactions(array $xactions) {
  1286. $head = array();
  1287. $tail = array();
  1288. // Move bare comments to the end, so the actions precede them.
  1289. foreach ($xactions as $xaction) {
  1290. $type = $xaction->getTransactionType();
  1291. if ($type == PhabricatorTransactions::TYPE_COMMENT) {
  1292. $tail[] = $xaction;
  1293. } else {
  1294. $head[] = $xaction;
  1295. }
  1296. }
  1297. return array_values(array_merge($head, $tail));
  1298. }
  1299. protected function filterTransactions(
  1300. PhabricatorLiskDAO $object,
  1301. array $xactions) {
  1302. $type_comment = PhabricatorTransactions::TYPE_COMMENT;
  1303. $no_effect = array();
  1304. $has_comment = false;
  1305. $any_effect = false;
  1306. foreach ($xactions as $key => $xaction) {
  1307. if ($this->transactionHasEffect($object, $xaction)) {
  1308. if ($xaction->getTransactionType() != $type_comment) {
  1309. $any_effect = true;
  1310. }
  1311. } else if ($xaction->getIgnoreOnNoEffect()) {
  1312. unset($xactions[$key]);
  1313. } else {
  1314. $no_effect[$key] = $xaction;
  1315. }
  1316. if ($xaction->hasComment()) {
  1317. $has_comment = true;
  1318. }
  1319. }
  1320. if (!$no_effect) {
  1321. return $xactions;
  1322. }
  1323. if (!$this->getContinueOnNoEffect() && !$this->getIsPreview()) {
  1324. throw new PhabricatorApplicationTransactionNoEffectException(
  1325. $no_effect,
  1326. $any_effect,
  1327. $has_comment);
  1328. }
  1329. if (!$any_effect && !$has_comment) {
  1330. // If we only have empty comment transactions, just drop them all.
  1331. return array();
  1332. }
  1333. foreach ($no_effect as $key => $xaction) {
  1334. if ($xaction->getComment()) {
  1335. $xaction->setTransactionType($type_comment);
  1336. $xaction->setOldValue(null);
  1337. $xaction->setNewValue(null);
  1338. } else {
  1339. unset($xactions[$key]);
  1340. }

Large files files are truncated, but you can click here to view the full file