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

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

http://github.com/facebook/phabricator
PHP | 5406 lines | 3683 code | 971 blank | 752 comment | 427 complexity | 420a5bc5efdd5e47135045a109caa51f MD5 | raw file
Possible License(s): JSON, MPL-2.0-no-copyleft-exception, Apache-2.0, BSD-3-Clause, LGPL-2.0, MIT, LGPL-2.1, LGPL-3.0

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

  1. <?php
  2. /**
  3. *
  4. * Publishing and Managing State
  5. * ======
  6. *
  7. * After applying changes, the Editor queues a worker to publish mail, feed,
  8. * and notifications, and to perform other background work like updating search
  9. * indexes. This allows it to do this work without impacting performance for
  10. * users.
  11. *
  12. * When work is moved to the daemons, the Editor state is serialized by
  13. * @{method:getWorkerState}, then reloaded in a daemon process by
  14. * @{method:loadWorkerState}. **This is fragile.**
  15. *
  16. * State is not persisted into the daemons by default, because we can not send
  17. * arbitrary objects into the queue. This means the default behavior of any
  18. * state properties is to reset to their defaults without warning prior to
  19. * publishing.
  20. *
  21. * The easiest way to avoid this is to keep Editors stateless: the overwhelming
  22. * majority of Editors can be written statelessly. If you need to maintain
  23. * state, you can either:
  24. *
  25. * - not require state to exist during publishing; or
  26. * - pass state to the daemons by implementing @{method:getCustomWorkerState}
  27. * and @{method:loadCustomWorkerState}.
  28. *
  29. * This architecture isn't ideal, and we may eventually split this class into
  30. * "Editor" and "Publisher" parts to make it more robust. See T6367 for some
  31. * discussion and context.
  32. *
  33. * @task mail Sending Mail
  34. * @task feed Publishing Feed Stories
  35. * @task search Search Index
  36. * @task files Integration with Files
  37. * @task workers Managing Workers
  38. */
  39. abstract class PhabricatorApplicationTransactionEditor
  40. extends PhabricatorEditor {
  41. private $contentSource;
  42. private $object;
  43. private $xactions;
  44. private $isNewObject;
  45. private $mentionedPHIDs;
  46. private $continueOnNoEffect;
  47. private $continueOnMissingFields;
  48. private $raiseWarnings;
  49. private $parentMessageID;
  50. private $heraldAdapter;
  51. private $heraldTranscript;
  52. private $subscribers;
  53. private $unmentionablePHIDMap = array();
  54. private $transactionGroupID;
  55. private $applicationEmail;
  56. private $isPreview;
  57. private $isHeraldEditor;
  58. private $isInverseEdgeEditor;
  59. private $actingAsPHID;
  60. private $heraldEmailPHIDs = array();
  61. private $heraldForcedEmailPHIDs = array();
  62. private $heraldHeader;
  63. private $mailToPHIDs = array();
  64. private $mailCCPHIDs = array();
  65. private $feedNotifyPHIDs = array();
  66. private $feedRelatedPHIDs = array();
  67. private $feedShouldPublish = false;
  68. private $mailShouldSend = false;
  69. private $modularTypes;
  70. private $silent;
  71. private $mustEncrypt = array();
  72. private $stampTemplates = array();
  73. private $mailStamps = array();
  74. private $oldTo = array();
  75. private $oldCC = array();
  76. private $mailRemovedPHIDs = array();
  77. private $mailUnexpandablePHIDs = array();
  78. private $mailMutedPHIDs = array();
  79. private $webhookMap = array();
  80. private $transactionQueue = array();
  81. private $sendHistory = false;
  82. private $shouldRequireMFA = false;
  83. private $hasRequiredMFA = false;
  84. private $request;
  85. private $cancelURI;
  86. private $extensions;
  87. private $parentEditor;
  88. private $subEditors = array();
  89. private $publishableObject;
  90. private $publishableTransactions;
  91. const STORAGE_ENCODING_BINARY = 'binary';
  92. /**
  93. * Get the class name for the application this editor is a part of.
  94. *
  95. * Uninstalling the application will disable the editor.
  96. *
  97. * @return string Editor's application class name.
  98. */
  99. abstract public function getEditorApplicationClass();
  100. /**
  101. * Get a description of the objects this editor edits, like "Differential
  102. * Revisions".
  103. *
  104. * @return string Human readable description of edited objects.
  105. */
  106. abstract public function getEditorObjectsDescription();
  107. public function setActingAsPHID($acting_as_phid) {
  108. $this->actingAsPHID = $acting_as_phid;
  109. return $this;
  110. }
  111. public function getActingAsPHID() {
  112. if ($this->actingAsPHID) {
  113. return $this->actingAsPHID;
  114. }
  115. return $this->getActor()->getPHID();
  116. }
  117. /**
  118. * When the editor tries to apply transactions that have no effect, should
  119. * it raise an exception (default) or drop them and continue?
  120. *
  121. * Generally, you will set this flag for edits coming from "Edit" interfaces,
  122. * and leave it cleared for edits coming from "Comment" interfaces, so the
  123. * user will get a useful error if they try to submit a comment that does
  124. * nothing (e.g., empty comment with a status change that has already been
  125. * performed by another user).
  126. *
  127. * @param bool True to drop transactions without effect and continue.
  128. * @return this
  129. */
  130. public function setContinueOnNoEffect($continue) {
  131. $this->continueOnNoEffect = $continue;
  132. return $this;
  133. }
  134. public function getContinueOnNoEffect() {
  135. return $this->continueOnNoEffect;
  136. }
  137. /**
  138. * When the editor tries to apply transactions which don't populate all of
  139. * an object's required fields, should it raise an exception (default) or
  140. * drop them and continue?
  141. *
  142. * For example, if a user adds a new required custom field (like "Severity")
  143. * to a task, all existing tasks won't have it populated. When users
  144. * manually edit existing tasks, it's usually desirable to have them provide
  145. * a severity. However, other operations (like batch editing just the
  146. * owner of a task) will fail by default.
  147. *
  148. * By setting this flag for edit operations which apply to specific fields
  149. * (like the priority, batch, and merge editors in Maniphest), these
  150. * operations can continue to function even if an object is outdated.
  151. *
  152. * @param bool True to continue when transactions don't completely satisfy
  153. * all required fields.
  154. * @return this
  155. */
  156. public function setContinueOnMissingFields($continue_on_missing_fields) {
  157. $this->continueOnMissingFields = $continue_on_missing_fields;
  158. return $this;
  159. }
  160. public function getContinueOnMissingFields() {
  161. return $this->continueOnMissingFields;
  162. }
  163. /**
  164. * Not strictly necessary, but reply handlers ideally set this value to
  165. * make email threading work better.
  166. */
  167. public function setParentMessageID($parent_message_id) {
  168. $this->parentMessageID = $parent_message_id;
  169. return $this;
  170. }
  171. public function getParentMessageID() {
  172. return $this->parentMessageID;
  173. }
  174. public function getIsNewObject() {
  175. return $this->isNewObject;
  176. }
  177. public function getMentionedPHIDs() {
  178. return $this->mentionedPHIDs;
  179. }
  180. public function setIsPreview($is_preview) {
  181. $this->isPreview = $is_preview;
  182. return $this;
  183. }
  184. public function getIsPreview() {
  185. return $this->isPreview;
  186. }
  187. public function setIsSilent($silent) {
  188. $this->silent = $silent;
  189. return $this;
  190. }
  191. public function getIsSilent() {
  192. return $this->silent;
  193. }
  194. public function getMustEncrypt() {
  195. return $this->mustEncrypt;
  196. }
  197. public function getHeraldRuleMonograms() {
  198. // Convert the stored "<123>, <456>" string into a list: "H123", "H456".
  199. $list = $this->heraldHeader;
  200. $list = preg_split('/[, ]+/', $list);
  201. foreach ($list as $key => $item) {
  202. $item = trim($item, '<>');
  203. if (!is_numeric($item)) {
  204. unset($list[$key]);
  205. continue;
  206. }
  207. $list[$key] = 'H'.$item;
  208. }
  209. return $list;
  210. }
  211. public function setIsInverseEdgeEditor($is_inverse_edge_editor) {
  212. $this->isInverseEdgeEditor = $is_inverse_edge_editor;
  213. return $this;
  214. }
  215. public function getIsInverseEdgeEditor() {
  216. return $this->isInverseEdgeEditor;
  217. }
  218. public function setIsHeraldEditor($is_herald_editor) {
  219. $this->isHeraldEditor = $is_herald_editor;
  220. return $this;
  221. }
  222. public function getIsHeraldEditor() {
  223. return $this->isHeraldEditor;
  224. }
  225. public function addUnmentionablePHIDs(array $phids) {
  226. foreach ($phids as $phid) {
  227. $this->unmentionablePHIDMap[$phid] = true;
  228. }
  229. return $this;
  230. }
  231. private function getUnmentionablePHIDMap() {
  232. return $this->unmentionablePHIDMap;
  233. }
  234. protected function shouldEnableMentions(
  235. PhabricatorLiskDAO $object,
  236. array $xactions) {
  237. return true;
  238. }
  239. public function setApplicationEmail(
  240. PhabricatorMetaMTAApplicationEmail $email) {
  241. $this->applicationEmail = $email;
  242. return $this;
  243. }
  244. public function getApplicationEmail() {
  245. return $this->applicationEmail;
  246. }
  247. public function setRaiseWarnings($raise_warnings) {
  248. $this->raiseWarnings = $raise_warnings;
  249. return $this;
  250. }
  251. public function getRaiseWarnings() {
  252. return $this->raiseWarnings;
  253. }
  254. public function setShouldRequireMFA($should_require_mfa) {
  255. if ($this->hasRequiredMFA) {
  256. throw new Exception(
  257. pht(
  258. 'Call to setShouldRequireMFA() is too late: this Editor has already '.
  259. 'checked for MFA requirements.'));
  260. }
  261. $this->shouldRequireMFA = $should_require_mfa;
  262. return $this;
  263. }
  264. public function getShouldRequireMFA() {
  265. return $this->shouldRequireMFA;
  266. }
  267. public function getTransactionTypesForObject($object) {
  268. $old = $this->object;
  269. try {
  270. $this->object = $object;
  271. $result = $this->getTransactionTypes();
  272. $this->object = $old;
  273. } catch (Exception $ex) {
  274. $this->object = $old;
  275. throw $ex;
  276. }
  277. return $result;
  278. }
  279. public function getTransactionTypes() {
  280. $types = array();
  281. $types[] = PhabricatorTransactions::TYPE_CREATE;
  282. $types[] = PhabricatorTransactions::TYPE_HISTORY;
  283. if ($this->object instanceof PhabricatorEditEngineSubtypeInterface) {
  284. $types[] = PhabricatorTransactions::TYPE_SUBTYPE;
  285. }
  286. if ($this->object instanceof PhabricatorSubscribableInterface) {
  287. $types[] = PhabricatorTransactions::TYPE_SUBSCRIBERS;
  288. }
  289. if ($this->object instanceof PhabricatorCustomFieldInterface) {
  290. $types[] = PhabricatorTransactions::TYPE_CUSTOMFIELD;
  291. }
  292. if ($this->object instanceof PhabricatorTokenReceiverInterface) {
  293. $types[] = PhabricatorTransactions::TYPE_TOKEN;
  294. }
  295. if ($this->object instanceof PhabricatorProjectInterface ||
  296. $this->object instanceof PhabricatorMentionableInterface) {
  297. $types[] = PhabricatorTransactions::TYPE_EDGE;
  298. }
  299. if ($this->object instanceof PhabricatorSpacesInterface) {
  300. $types[] = PhabricatorTransactions::TYPE_SPACE;
  301. }
  302. $types[] = PhabricatorTransactions::TYPE_MFA;
  303. $template = $this->object->getApplicationTransactionTemplate();
  304. if ($template instanceof PhabricatorModularTransaction) {
  305. $xtypes = $template->newModularTransactionTypes();
  306. foreach ($xtypes as $xtype) {
  307. $types[] = $xtype->getTransactionTypeConstant();
  308. }
  309. }
  310. if ($template) {
  311. $comment = $template->getApplicationTransactionCommentObject();
  312. if ($comment) {
  313. $types[] = PhabricatorTransactions::TYPE_COMMENT;
  314. }
  315. }
  316. return $types;
  317. }
  318. private function adjustTransactionValues(
  319. PhabricatorLiskDAO $object,
  320. PhabricatorApplicationTransaction $xaction) {
  321. if ($xaction->shouldGenerateOldValue()) {
  322. $old = $this->getTransactionOldValue($object, $xaction);
  323. $xaction->setOldValue($old);
  324. }
  325. $new = $this->getTransactionNewValue($object, $xaction);
  326. $xaction->setNewValue($new);
  327. }
  328. private function getTransactionOldValue(
  329. PhabricatorLiskDAO $object,
  330. PhabricatorApplicationTransaction $xaction) {
  331. $type = $xaction->getTransactionType();
  332. $xtype = $this->getModularTransactionType($type);
  333. if ($xtype) {
  334. $xtype = clone $xtype;
  335. $xtype->setStorage($xaction);
  336. return $xtype->generateOldValue($object);
  337. }
  338. switch ($type) {
  339. case PhabricatorTransactions::TYPE_CREATE:
  340. case PhabricatorTransactions::TYPE_HISTORY:
  341. return null;
  342. case PhabricatorTransactions::TYPE_SUBTYPE:
  343. return $object->getEditEngineSubtype();
  344. case PhabricatorTransactions::TYPE_MFA:
  345. return null;
  346. case PhabricatorTransactions::TYPE_SUBSCRIBERS:
  347. return array_values($this->subscribers);
  348. case PhabricatorTransactions::TYPE_VIEW_POLICY:
  349. if ($this->getIsNewObject()) {
  350. return null;
  351. }
  352. return $object->getViewPolicy();
  353. case PhabricatorTransactions::TYPE_EDIT_POLICY:
  354. if ($this->getIsNewObject()) {
  355. return null;
  356. }
  357. return $object->getEditPolicy();
  358. case PhabricatorTransactions::TYPE_JOIN_POLICY:
  359. if ($this->getIsNewObject()) {
  360. return null;
  361. }
  362. return $object->getJoinPolicy();
  363. case PhabricatorTransactions::TYPE_SPACE:
  364. if ($this->getIsNewObject()) {
  365. return null;
  366. }
  367. $space_phid = $object->getSpacePHID();
  368. if ($space_phid === null) {
  369. $default_space = PhabricatorSpacesNamespaceQuery::getDefaultSpace();
  370. if ($default_space) {
  371. $space_phid = $default_space->getPHID();
  372. }
  373. }
  374. return $space_phid;
  375. case PhabricatorTransactions::TYPE_EDGE:
  376. $edge_type = $xaction->getMetadataValue('edge:type');
  377. if (!$edge_type) {
  378. throw new Exception(
  379. pht(
  380. "Edge transaction has no '%s'!",
  381. 'edge:type'));
  382. }
  383. // See T13082. If this is an inverse edit, the parent editor has
  384. // already populated the transaction values correctly.
  385. if ($this->getIsInverseEdgeEditor()) {
  386. return $xaction->getOldValue();
  387. }
  388. $old_edges = array();
  389. if ($object->getPHID()) {
  390. $edge_src = $object->getPHID();
  391. $old_edges = id(new PhabricatorEdgeQuery())
  392. ->withSourcePHIDs(array($edge_src))
  393. ->withEdgeTypes(array($edge_type))
  394. ->needEdgeData(true)
  395. ->execute();
  396. $old_edges = $old_edges[$edge_src][$edge_type];
  397. }
  398. return $old_edges;
  399. case PhabricatorTransactions::TYPE_CUSTOMFIELD:
  400. // NOTE: Custom fields have their old value pre-populated when they are
  401. // built by PhabricatorCustomFieldList.
  402. return $xaction->getOldValue();
  403. case PhabricatorTransactions::TYPE_COMMENT:
  404. return null;
  405. default:
  406. return $this->getCustomTransactionOldValue($object, $xaction);
  407. }
  408. }
  409. private function getTransactionNewValue(
  410. PhabricatorLiskDAO $object,
  411. PhabricatorApplicationTransaction $xaction) {
  412. $type = $xaction->getTransactionType();
  413. $xtype = $this->getModularTransactionType($type);
  414. if ($xtype) {
  415. $xtype = clone $xtype;
  416. $xtype->setStorage($xaction);
  417. return $xtype->generateNewValue($object, $xaction->getNewValue());
  418. }
  419. switch ($type) {
  420. case PhabricatorTransactions::TYPE_CREATE:
  421. return null;
  422. case PhabricatorTransactions::TYPE_SUBSCRIBERS:
  423. return $this->getPHIDTransactionNewValue($xaction);
  424. case PhabricatorTransactions::TYPE_VIEW_POLICY:
  425. case PhabricatorTransactions::TYPE_EDIT_POLICY:
  426. case PhabricatorTransactions::TYPE_JOIN_POLICY:
  427. case PhabricatorTransactions::TYPE_TOKEN:
  428. case PhabricatorTransactions::TYPE_INLINESTATE:
  429. case PhabricatorTransactions::TYPE_SUBTYPE:
  430. case PhabricatorTransactions::TYPE_HISTORY:
  431. return $xaction->getNewValue();
  432. case PhabricatorTransactions::TYPE_MFA:
  433. return true;
  434. case PhabricatorTransactions::TYPE_SPACE:
  435. $space_phid = $xaction->getNewValue();
  436. if (!strlen($space_phid)) {
  437. // If an install has no Spaces or the Spaces controls are not visible
  438. // to the viewer, we might end up with the empty string here instead
  439. // of a strict `null`, because some controller just used `getStr()`
  440. // to read the space PHID from the request.
  441. // Just make this work like callers might reasonably expect so we
  442. // don't need to handle this specially in every EditController.
  443. return $this->getActor()->getDefaultSpacePHID();
  444. } else {
  445. return $space_phid;
  446. }
  447. case PhabricatorTransactions::TYPE_EDGE:
  448. // See T13082. If this is an inverse edit, the parent editor has
  449. // already populated appropriate transaction values.
  450. if ($this->getIsInverseEdgeEditor()) {
  451. return $xaction->getNewValue();
  452. }
  453. $new_value = $this->getEdgeTransactionNewValue($xaction);
  454. $edge_type = $xaction->getMetadataValue('edge:type');
  455. $type_project = PhabricatorProjectObjectHasProjectEdgeType::EDGECONST;
  456. if ($edge_type == $type_project) {
  457. $new_value = $this->applyProjectConflictRules($new_value);
  458. }
  459. return $new_value;
  460. case PhabricatorTransactions::TYPE_CUSTOMFIELD:
  461. $field = $this->getCustomFieldForTransaction($object, $xaction);
  462. return $field->getNewValueFromApplicationTransactions($xaction);
  463. case PhabricatorTransactions::TYPE_COMMENT:
  464. return null;
  465. default:
  466. return $this->getCustomTransactionNewValue($object, $xaction);
  467. }
  468. }
  469. protected function getCustomTransactionOldValue(
  470. PhabricatorLiskDAO $object,
  471. PhabricatorApplicationTransaction $xaction) {
  472. throw new Exception(pht('Capability not supported!'));
  473. }
  474. protected function getCustomTransactionNewValue(
  475. PhabricatorLiskDAO $object,
  476. PhabricatorApplicationTransaction $xaction) {
  477. throw new Exception(pht('Capability not supported!'));
  478. }
  479. protected function transactionHasEffect(
  480. PhabricatorLiskDAO $object,
  481. PhabricatorApplicationTransaction $xaction) {
  482. switch ($xaction->getTransactionType()) {
  483. case PhabricatorTransactions::TYPE_CREATE:
  484. case PhabricatorTransactions::TYPE_HISTORY:
  485. return true;
  486. case PhabricatorTransactions::TYPE_CUSTOMFIELD:
  487. $field = $this->getCustomFieldForTransaction($object, $xaction);
  488. return $field->getApplicationTransactionHasEffect($xaction);
  489. case PhabricatorTransactions::TYPE_EDGE:
  490. // A straight value comparison here doesn't always get the right
  491. // result, because newly added edges aren't fully populated. Instead,
  492. // compare the changes in a more granular way.
  493. $old = $xaction->getOldValue();
  494. $new = $xaction->getNewValue();
  495. $old_dst = array_keys($old);
  496. $new_dst = array_keys($new);
  497. // NOTE: For now, we don't consider edge reordering to be a change.
  498. // We have very few order-dependent edges and effectively no order
  499. // oriented UI. This might change in the future.
  500. sort($old_dst);
  501. sort($new_dst);
  502. if ($old_dst !== $new_dst) {
  503. // We've added or removed edges, so this transaction definitely
  504. // has an effect.
  505. return true;
  506. }
  507. // We haven't added or removed edges, but we might have changed
  508. // edge data.
  509. foreach ($old as $key => $old_value) {
  510. $new_value = $new[$key];
  511. if ($old_value['data'] !== $new_value['data']) {
  512. return true;
  513. }
  514. }
  515. return false;
  516. }
  517. $type = $xaction->getTransactionType();
  518. $xtype = $this->getModularTransactionType($type);
  519. if ($xtype) {
  520. return $xtype->getTransactionHasEffect(
  521. $object,
  522. $xaction->getOldValue(),
  523. $xaction->getNewValue());
  524. }
  525. if ($xaction->hasComment()) {
  526. return true;
  527. }
  528. return ($xaction->getOldValue() !== $xaction->getNewValue());
  529. }
  530. protected function shouldApplyInitialEffects(
  531. PhabricatorLiskDAO $object,
  532. array $xactions) {
  533. return false;
  534. }
  535. protected function applyInitialEffects(
  536. PhabricatorLiskDAO $object,
  537. array $xactions) {
  538. throw new PhutilMethodNotImplementedException();
  539. }
  540. private function applyInternalEffects(
  541. PhabricatorLiskDAO $object,
  542. PhabricatorApplicationTransaction $xaction) {
  543. $type = $xaction->getTransactionType();
  544. $xtype = $this->getModularTransactionType($type);
  545. if ($xtype) {
  546. $xtype = clone $xtype;
  547. $xtype->setStorage($xaction);
  548. return $xtype->applyInternalEffects($object, $xaction->getNewValue());
  549. }
  550. switch ($type) {
  551. case PhabricatorTransactions::TYPE_CUSTOMFIELD:
  552. $field = $this->getCustomFieldForTransaction($object, $xaction);
  553. return $field->applyApplicationTransactionInternalEffects($xaction);
  554. case PhabricatorTransactions::TYPE_CREATE:
  555. case PhabricatorTransactions::TYPE_HISTORY:
  556. case PhabricatorTransactions::TYPE_SUBTYPE:
  557. case PhabricatorTransactions::TYPE_MFA:
  558. case PhabricatorTransactions::TYPE_TOKEN:
  559. case PhabricatorTransactions::TYPE_VIEW_POLICY:
  560. case PhabricatorTransactions::TYPE_EDIT_POLICY:
  561. case PhabricatorTransactions::TYPE_JOIN_POLICY:
  562. case PhabricatorTransactions::TYPE_SUBSCRIBERS:
  563. case PhabricatorTransactions::TYPE_INLINESTATE:
  564. case PhabricatorTransactions::TYPE_EDGE:
  565. case PhabricatorTransactions::TYPE_SPACE:
  566. case PhabricatorTransactions::TYPE_COMMENT:
  567. return $this->applyBuiltinInternalTransaction($object, $xaction);
  568. }
  569. return $this->applyCustomInternalTransaction($object, $xaction);
  570. }
  571. private function applyExternalEffects(
  572. PhabricatorLiskDAO $object,
  573. PhabricatorApplicationTransaction $xaction) {
  574. $type = $xaction->getTransactionType();
  575. $xtype = $this->getModularTransactionType($type);
  576. if ($xtype) {
  577. $xtype = clone $xtype;
  578. $xtype->setStorage($xaction);
  579. return $xtype->applyExternalEffects($object, $xaction->getNewValue());
  580. }
  581. switch ($type) {
  582. case PhabricatorTransactions::TYPE_SUBSCRIBERS:
  583. $subeditor = id(new PhabricatorSubscriptionsEditor())
  584. ->setObject($object)
  585. ->setActor($this->requireActor());
  586. $old_map = array_fuse($xaction->getOldValue());
  587. $new_map = array_fuse($xaction->getNewValue());
  588. $subeditor->unsubscribe(
  589. array_keys(
  590. array_diff_key($old_map, $new_map)));
  591. $subeditor->subscribeExplicit(
  592. array_keys(
  593. array_diff_key($new_map, $old_map)));
  594. $subeditor->save();
  595. // for the rest of these edits, subscribers should include those just
  596. // added as well as those just removed.
  597. $subscribers = array_unique(array_merge(
  598. $this->subscribers,
  599. $xaction->getOldValue(),
  600. $xaction->getNewValue()));
  601. $this->subscribers = $subscribers;
  602. return $this->applyBuiltinExternalTransaction($object, $xaction);
  603. case PhabricatorTransactions::TYPE_CUSTOMFIELD:
  604. $field = $this->getCustomFieldForTransaction($object, $xaction);
  605. return $field->applyApplicationTransactionExternalEffects($xaction);
  606. case PhabricatorTransactions::TYPE_CREATE:
  607. case PhabricatorTransactions::TYPE_HISTORY:
  608. case PhabricatorTransactions::TYPE_SUBTYPE:
  609. case PhabricatorTransactions::TYPE_MFA:
  610. case PhabricatorTransactions::TYPE_EDGE:
  611. case PhabricatorTransactions::TYPE_TOKEN:
  612. case PhabricatorTransactions::TYPE_VIEW_POLICY:
  613. case PhabricatorTransactions::TYPE_EDIT_POLICY:
  614. case PhabricatorTransactions::TYPE_JOIN_POLICY:
  615. case PhabricatorTransactions::TYPE_INLINESTATE:
  616. case PhabricatorTransactions::TYPE_SPACE:
  617. case PhabricatorTransactions::TYPE_COMMENT:
  618. return $this->applyBuiltinExternalTransaction($object, $xaction);
  619. }
  620. return $this->applyCustomExternalTransaction($object, $xaction);
  621. }
  622. protected function applyCustomInternalTransaction(
  623. PhabricatorLiskDAO $object,
  624. PhabricatorApplicationTransaction $xaction) {
  625. $type = $xaction->getTransactionType();
  626. throw new Exception(
  627. pht(
  628. "Transaction type '%s' is missing an internal apply implementation!",
  629. $type));
  630. }
  631. protected function applyCustomExternalTransaction(
  632. PhabricatorLiskDAO $object,
  633. PhabricatorApplicationTransaction $xaction) {
  634. $type = $xaction->getTransactionType();
  635. throw new Exception(
  636. pht(
  637. "Transaction type '%s' is missing an external apply implementation!",
  638. $type));
  639. }
  640. /**
  641. * @{class:PhabricatorTransactions} provides many built-in transactions
  642. * which should not require much - if any - code in specific applications.
  643. *
  644. * This method is a hook for the exceedingly-rare cases where you may need
  645. * to do **additional** work for built-in transactions. Developers should
  646. * extend this method, making sure to return the parent implementation
  647. * regardless of handling any transactions.
  648. *
  649. * See also @{method:applyBuiltinExternalTransaction}.
  650. */
  651. protected function applyBuiltinInternalTransaction(
  652. PhabricatorLiskDAO $object,
  653. PhabricatorApplicationTransaction $xaction) {
  654. switch ($xaction->getTransactionType()) {
  655. case PhabricatorTransactions::TYPE_VIEW_POLICY:
  656. $object->setViewPolicy($xaction->getNewValue());
  657. break;
  658. case PhabricatorTransactions::TYPE_EDIT_POLICY:
  659. $object->setEditPolicy($xaction->getNewValue());
  660. break;
  661. case PhabricatorTransactions::TYPE_JOIN_POLICY:
  662. $object->setJoinPolicy($xaction->getNewValue());
  663. break;
  664. case PhabricatorTransactions::TYPE_SPACE:
  665. $object->setSpacePHID($xaction->getNewValue());
  666. break;
  667. case PhabricatorTransactions::TYPE_SUBTYPE:
  668. $object->setEditEngineSubtype($xaction->getNewValue());
  669. break;
  670. }
  671. }
  672. /**
  673. * See @{method::applyBuiltinInternalTransaction}.
  674. */
  675. protected function applyBuiltinExternalTransaction(
  676. PhabricatorLiskDAO $object,
  677. PhabricatorApplicationTransaction $xaction) {
  678. switch ($xaction->getTransactionType()) {
  679. case PhabricatorTransactions::TYPE_EDGE:
  680. if ($this->getIsInverseEdgeEditor()) {
  681. // If we're writing an inverse edge transaction, don't actually
  682. // do anything. The initiating editor on the other side of the
  683. // transaction will take care of the edge writes.
  684. break;
  685. }
  686. $old = $xaction->getOldValue();
  687. $new = $xaction->getNewValue();
  688. $src = $object->getPHID();
  689. $const = $xaction->getMetadataValue('edge:type');
  690. foreach ($new as $dst_phid => $edge) {
  691. $new[$dst_phid]['src'] = $src;
  692. }
  693. $editor = new PhabricatorEdgeEditor();
  694. foreach ($old as $dst_phid => $edge) {
  695. if (!empty($new[$dst_phid])) {
  696. if ($old[$dst_phid]['data'] === $new[$dst_phid]['data']) {
  697. continue;
  698. }
  699. }
  700. $editor->removeEdge($src, $const, $dst_phid);
  701. }
  702. foreach ($new as $dst_phid => $edge) {
  703. if (!empty($old[$dst_phid])) {
  704. if ($old[$dst_phid]['data'] === $new[$dst_phid]['data']) {
  705. continue;
  706. }
  707. }
  708. $data = array(
  709. 'data' => $edge['data'],
  710. );
  711. $editor->addEdge($src, $const, $dst_phid, $data);
  712. }
  713. $editor->save();
  714. $this->updateWorkboardColumns($object, $const, $old, $new);
  715. break;
  716. case PhabricatorTransactions::TYPE_VIEW_POLICY:
  717. case PhabricatorTransactions::TYPE_SPACE:
  718. $this->scrambleFileSecrets($object);
  719. break;
  720. case PhabricatorTransactions::TYPE_HISTORY:
  721. $this->sendHistory = true;
  722. break;
  723. }
  724. }
  725. /**
  726. * Fill in a transaction's common values, like author and content source.
  727. */
  728. protected function populateTransaction(
  729. PhabricatorLiskDAO $object,
  730. PhabricatorApplicationTransaction $xaction) {
  731. $actor = $this->getActor();
  732. // TODO: This needs to be more sophisticated once we have meta-policies.
  733. $xaction->setViewPolicy(PhabricatorPolicies::POLICY_PUBLIC);
  734. if ($actor->isOmnipotent()) {
  735. $xaction->setEditPolicy(PhabricatorPolicies::POLICY_NOONE);
  736. } else {
  737. $xaction->setEditPolicy($this->getActingAsPHID());
  738. }
  739. // If the transaction already has an explicit author PHID, allow it to
  740. // stand. This is used by applications like Owners that hook into the
  741. // post-apply change pipeline.
  742. if (!$xaction->getAuthorPHID()) {
  743. $xaction->setAuthorPHID($this->getActingAsPHID());
  744. }
  745. $xaction->setContentSource($this->getContentSource());
  746. $xaction->attachViewer($actor);
  747. $xaction->attachObject($object);
  748. if ($object->getPHID()) {
  749. $xaction->setObjectPHID($object->getPHID());
  750. }
  751. if ($this->getIsSilent()) {
  752. $xaction->setIsSilentTransaction(true);
  753. }
  754. return $xaction;
  755. }
  756. protected function didApplyInternalEffects(
  757. PhabricatorLiskDAO $object,
  758. array $xactions) {
  759. return $xactions;
  760. }
  761. protected function applyFinalEffects(
  762. PhabricatorLiskDAO $object,
  763. array $xactions) {
  764. return $xactions;
  765. }
  766. final protected function didCommitTransactions(
  767. PhabricatorLiskDAO $object,
  768. array $xactions) {
  769. foreach ($xactions as $xaction) {
  770. $type = $xaction->getTransactionType();
  771. // See T13082. When we're writing edges that imply corresponding inverse
  772. // transactions, apply those inverse transactions now. We have to wait
  773. // until the object we're editing (with this editor) has committed its
  774. // transactions to do this. If we don't, the inverse editor may race,
  775. // build a mail before we actually commit this object, and render "alice
  776. // added an edge: Unknown Object".
  777. if ($type === PhabricatorTransactions::TYPE_EDGE) {
  778. // Don't do anything if we're already an inverse edge editor.
  779. if ($this->getIsInverseEdgeEditor()) {
  780. continue;
  781. }
  782. $edge_const = $xaction->getMetadataValue('edge:type');
  783. $edge_type = PhabricatorEdgeType::getByConstant($edge_const);
  784. if ($edge_type->shouldWriteInverseTransactions()) {
  785. $this->applyInverseEdgeTransactions(
  786. $object,
  787. $xaction,
  788. $edge_type->getInverseEdgeConstant());
  789. }
  790. continue;
  791. }
  792. $xtype = $this->getModularTransactionType($type);
  793. if (!$xtype) {
  794. continue;
  795. }
  796. $xtype = clone $xtype;
  797. $xtype->setStorage($xaction);
  798. $xtype->didCommitTransaction($object, $xaction->getNewValue());
  799. }
  800. }
  801. public function setContentSource(PhabricatorContentSource $content_source) {
  802. $this->contentSource = $content_source;
  803. return $this;
  804. }
  805. public function setContentSourceFromRequest(AphrontRequest $request) {
  806. $this->setRequest($request);
  807. return $this->setContentSource(
  808. PhabricatorContentSource::newFromRequest($request));
  809. }
  810. public function getContentSource() {
  811. return $this->contentSource;
  812. }
  813. public function setRequest(AphrontRequest $request) {
  814. $this->request = $request;
  815. return $this;
  816. }
  817. public function getRequest() {
  818. return $this->request;
  819. }
  820. public function setCancelURI($cancel_uri) {
  821. $this->cancelURI = $cancel_uri;
  822. return $this;
  823. }
  824. public function getCancelURI() {
  825. return $this->cancelURI;
  826. }
  827. protected function getTransactionGroupID() {
  828. if ($this->transactionGroupID === null) {
  829. $this->transactionGroupID = Filesystem::readRandomCharacters(32);
  830. }
  831. return $this->transactionGroupID;
  832. }
  833. final public function applyTransactions(
  834. PhabricatorLiskDAO $object,
  835. array $xactions) {
  836. $is_new = ($object->getID() === null);
  837. $this->isNewObject = $is_new;
  838. $is_preview = $this->getIsPreview();
  839. $read_locking = false;
  840. $transaction_open = false;
  841. // If we're attempting to apply transactions, lock and reload the object
  842. // before we go anywhere. If we don't do this at the very beginning, we
  843. // may be looking at an older version of the object when we populate and
  844. // filter the transactions. See PHI1165 for an example.
  845. if (!$is_preview) {
  846. if (!$is_new) {
  847. $this->buildOldRecipientLists($object, $xactions);
  848. $object->openTransaction();
  849. $transaction_open = true;
  850. $object->beginReadLocking();
  851. $read_locking = true;
  852. $object->reload();
  853. }
  854. }
  855. try {
  856. $this->object = $object;
  857. $this->xactions = $xactions;
  858. $this->validateEditParameters($object, $xactions);
  859. $xactions = $this->newMFATransactions($object, $xactions);
  860. $actor = $this->requireActor();
  861. // NOTE: Some transaction expansion requires that the edited object be
  862. // attached.
  863. foreach ($xactions as $xaction) {
  864. $xaction->attachObject($object);
  865. $xaction->attachViewer($actor);
  866. }
  867. $xactions = $this->expandTransactions($object, $xactions);
  868. $xactions = $this->expandSupportTransactions($object, $xactions);
  869. $xactions = $this->combineTransactions($xactions);
  870. foreach ($xactions as $xaction) {
  871. $xaction = $this->populateTransaction($object, $xaction);
  872. }
  873. if (!$is_preview) {
  874. $errors = array();
  875. $type_map = mgroup($xactions, 'getTransactionType');
  876. foreach ($this->getTransactionTypes() as $type) {
  877. $type_xactions = idx($type_map, $type, array());
  878. $errors[] = $this->validateTransaction(
  879. $object,
  880. $type,
  881. $type_xactions);
  882. }
  883. $errors[] = $this->validateAllTransactions($object, $xactions);
  884. $errors[] = $this->validateTransactionsWithExtensions(
  885. $object,
  886. $xactions);
  887. $errors = array_mergev($errors);
  888. $continue_on_missing = $this->getContinueOnMissingFields();
  889. foreach ($errors as $key => $error) {
  890. if ($continue_on_missing && $error->getIsMissingFieldError()) {
  891. unset($errors[$key]);
  892. }
  893. }
  894. if ($errors) {
  895. throw new PhabricatorApplicationTransactionValidationException(
  896. $errors);
  897. }
  898. if ($this->raiseWarnings) {
  899. $warnings = array();
  900. foreach ($xactions as $xaction) {
  901. if ($this->hasWarnings($object, $xaction)) {
  902. $warnings[] = $xaction;
  903. }
  904. }
  905. if ($warnings) {
  906. throw new PhabricatorApplicationTransactionWarningException(
  907. $warnings);
  908. }
  909. }
  910. }
  911. foreach ($xactions as $xaction) {
  912. $this->adjustTransactionValues($object, $xaction);
  913. }
  914. // Now that we've merged and combined transactions, check for required
  915. // capabilities. Note that we're doing this before filtering
  916. // transactions: if you try to apply an edit which you do not have
  917. // permission to apply, we want to give you a permissions error even
  918. // if the edit would have no effect.
  919. $this->applyCapabilityChecks($object, $xactions);
  920. $xactions = $this->filterTransactions($object, $xactions);
  921. if (!$is_preview) {
  922. $this->hasRequiredMFA = true;
  923. if ($this->getShouldRequireMFA()) {
  924. $this->requireMFA($object, $xactions);
  925. }
  926. if ($this->shouldApplyInitialEffects($object, $xactions)) {
  927. if (!$transaction_open) {
  928. $object->openTransaction();
  929. $transaction_open = true;
  930. }
  931. }
  932. }
  933. if ($this->shouldApplyInitialEffects($object, $xactions)) {
  934. $this->applyInitialEffects($object, $xactions);
  935. }
  936. // TODO: Once everything is on EditEngine, just use getIsNewObject() to
  937. // figure this out instead.
  938. $mark_as_create = false;
  939. $create_type = PhabricatorTransactions::TYPE_CREATE;
  940. foreach ($xactions as $xaction) {
  941. if ($xaction->getTransactionType() == $create_type) {
  942. $mark_as_create = true;
  943. }
  944. }
  945. if ($mark_as_create) {
  946. foreach ($xactions as $xaction) {
  947. $xaction->setIsCreateTransaction(true);
  948. }
  949. }
  950. $xactions = $this->sortTransactions($xactions);
  951. $file_phids = $this->extractFilePHIDs($object, $xactions);
  952. if ($is_preview) {
  953. $this->loadHandles($xactions);
  954. return $xactions;
  955. }
  956. $comment_editor = id(new PhabricatorApplicationTransactionCommentEditor())
  957. ->setActor($actor)
  958. ->setActingAsPHID($this->getActingAsPHID())
  959. ->setContentSource($this->getContentSource())
  960. ->setIsNewComment(true);
  961. if (!$transaction_open) {
  962. $object->openTransaction();
  963. $transaction_open = true;
  964. }
  965. // We can technically test any object for CAN_INTERACT, but we can
  966. // run into some issues in doing so (for example, in project unit tests).
  967. // For now, only test for CAN_INTERACT if the object is explicitly a
  968. // lockable object.
  969. $was_locked = false;
  970. if ($object instanceof PhabricatorEditEngineLockableInterface) {
  971. $was_locked = !PhabricatorPolicyFilter::canInteract($actor, $object);
  972. }
  973. foreach ($xactions as $xaction) {
  974. $this->applyInternalEffects($object, $xaction);
  975. }
  976. $xactions = $this->didApplyInternalEffects($object, $xactions);
  977. try {
  978. $object->save();
  979. } catch (AphrontDuplicateKeyQueryException $ex) {
  980. // This callback has an opportunity to throw a better exception,
  981. // so execution may end here.
  982. $this->didCatchDuplicateKeyException($object, $xactions, $ex);
  983. throw $ex;
  984. }
  985. $group_id = $this->getTransactionGroupID();
  986. foreach ($xactions as $xaction) {
  987. if ($was_locked) {
  988. $is_override = $this->isLockOverrideTransaction($xaction);
  989. if ($is_override) {
  990. $xaction->setIsLockOverrideTransaction(true);
  991. }
  992. }
  993. $xaction->setObjectPHID($object->getPHID());
  994. $xaction->setTransactionGroupID($group_id);
  995. if ($xaction->getComment()) {
  996. $xaction->setPHID($xaction->generatePHID());
  997. $comment_editor->applyEdit($xaction, $xaction->getComment());
  998. } else {
  999. // TODO: This is a transitional hack to let us migrate edge
  1000. // transactions to a more efficient storage format. For now, we're
  1001. // going to write a new slim format to the database but keep the old
  1002. // bulky format on the objects so we don't have to upgrade all the
  1003. // edit logic to the new format yet. See T13051.
  1004. $edge_type = PhabricatorTransactions::TYPE_EDGE;
  1005. if ($xaction->getTransactionType() == $edge_type) {
  1006. $bulky_old = $xaction->getOldValue();
  1007. $bulky_new = $xaction->getNewValue();
  1008. $record = PhabricatorEdgeChangeRecord::newFromTransaction($xaction);
  1009. $slim_old = $record->getModernOldEdgeTransactionData();
  1010. $slim_new = $record->getModernNewEdgeTransactionData();
  1011. $xaction->setOldValue($slim_old);
  1012. $xaction->setNewValue($slim_new);
  1013. $xaction->save();
  1014. $xaction->setOldValue($bulky_old);
  1015. $xaction->setNewValue($bulky_new);
  1016. } else {
  1017. $xaction->save();
  1018. }
  1019. }
  1020. }
  1021. if ($file_phids) {
  1022. $this->attachFiles($object, $file_phids);
  1023. }
  1024. foreach ($xactions as $xaction) {
  1025. $this->applyExternalEffects($object, $xaction);
  1026. }
  1027. $xactions = $this->applyFinalEffects($object, $xactions);
  1028. if ($read_locking) {
  1029. $object->endReadLocking();
  1030. $read_locking = false;
  1031. }
  1032. if ($transaction_open) {
  1033. $object->saveTransaction();
  1034. $transaction_open = false;
  1035. }
  1036. $this->didCommitTransactions($object, $xactions);
  1037. } catch (Exception $ex) {
  1038. if ($read_locking) {
  1039. $object->endReadLocking();
  1040. $read_locking = false;
  1041. }
  1042. if ($transaction_open) {
  1043. $object->killTransaction();
  1044. $transaction_open = false;
  1045. }
  1046. throw $ex;
  1047. }
  1048. // If we need to perform cache engine updates, execute them now.
  1049. id(new PhabricatorCacheEngine())
  1050. ->updateObject($object);
  1051. // Now that we've completely applied the core transaction set, try to apply
  1052. // Herald rules. Herald rules are allowed to either take direct actions on
  1053. // the database (like writing flags), or take indirect actions (like saving
  1054. // some targets for CC when we generate mail a little later), or return
  1055. // transactions which we'll apply normally using another Editor.
  1056. // First, check if *this* is a sub-editor which is itself applying Herald
  1057. // rules: if it is, stop working and return so we don't descend into
  1058. // madness.
  1059. // Otherwise, we're not a Herald editor, so process Herald rules (possibly
  1060. // using a Herald editor to apply resulting transactions) and then send out
  1061. // mail, notifications, and feed updates about everything.
  1062. if ($this->getIsHeraldEditor()) {
  1063. // We are the Herald editor, so stop work here and return the updated
  1064. // transactions.
  1065. return $xactions;
  1066. } else if ($this->getIsInverseEdgeEditor()) {
  1067. // Do not run Herald if we're just recording that this object was
  1068. // mentioned elsewhere. This tends to create Herald side effects which
  1069. // feel arbitrary, and can really slow down edits which mention a large
  1070. // number of other objects. See T13114.
  1071. } else if ($this->shouldApplyHeraldRules($object, $xactions)) {
  1072. // We are not the Herald editor, so try to apply Herald rules.
  1073. $herald_xactions = $this->applyHeraldRules($object, $xactions);
  1074. if ($herald_xactions) {
  1075. $xscript_id = $this->getHeraldTranscript()->getID();
  1076. foreach ($herald_xactions as $herald_xaction) {
  1077. // Don't set a transcript ID if this is a transaction from another
  1078. // application or source, like Owners.
  1079. if ($herald_xaction->getAuthorPHID()) {
  1080. continue;
  1081. }
  1082. $herald_xaction->setMetadataValue('herald:transcriptID', $xscript_id);
  1083. }
  1084. // NOTE: We're acting as the omnipotent user because rules deal with
  1085. // their own policy issues. We use a synthetic author PHID (the
  1086. // Herald application) as the author of record, so that transactions
  1087. // will render in a reasonable way ("Herald assigned this task ...").
  1088. $herald_actor = PhabricatorUser::getOmnipotentUser();
  1089. $herald_phid = id(new PhabricatorHeraldApplication())->getPHID();
  1090. // TODO: It would be nice to give transactions a more specific source
  1091. // which points at the rule which generated them. You can figure this
  1092. // out from transcripts, but it would be cleaner if you didn't have to.
  1093. $herald_source = PhabricatorContentSource::newForSource(
  1094. PhabricatorHeraldContentSource::SOURCECONST);
  1095. $herald_editor = $this->newEditorCopy()
  1096. ->setContinueOnNoEffect(true)
  1097. ->setContinueOnMissingFields(true)
  1098. ->setIsHeraldEditor(true)
  1099. ->setActor($herald_actor)
  1100. ->setActingAsPHID($herald_phid)
  1101. ->setContentSource($herald_source);
  1102. $herald_xactions = $herald_editor->applyTransactions(
  1103. $object,
  1104. $herald_xactions);
  1105. // Merge the new transactions into the transaction list: we want to
  1106. // send email and publish feed stories about them, too.
  1107. $xactions = array_merge($xactions, $herald_xactions);
  1108. }
  1109. // If Herald did not generate transactions, we may still need to handle
  1110. // "Send an Email" rules.
  1111. $adapter = $this->getHeraldAdapter();
  1112. $this->heraldEmailPHIDs = $adapter->getEmailPHIDs();
  1113. $this->heraldForcedEmailPHIDs = $adapter->getForcedEmailPHIDs();
  1114. $this->webhookMap = $adapter->getWebhookMap();
  1115. }
  1116. $xactions = $this->didApplyTransactions($object, $xactions);
  1117. if ($object instanceof PhabricatorCustomFieldInterface) {
  1118. // Maybe this makes more sense to move into the search index itself? For
  1119. // now I'm putting it here since I think we might end up with things that
  1120. // need it to be up to date once the next page loads, but if we don't go
  1121. // there we could move it into search once search moves to the daemons.
  1122. // It now happens in the search indexer as well, but the search indexer is
  1123. // always daemonized, so the logic above still potentially holds. We could
  1124. // possibly get rid of this. The major motivation for putting it in the
  1125. // indexer was to enable reindexing to work.
  1126. $fields = PhabricatorCustomField::getObjectFields(
  1127. $object,
  1128. PhabricatorCustomField::ROLE_APPLICATIONSEARCH);
  1129. $fields->readFieldsFromStorage($object);
  1130. $fields->rebuildIndexes($object);
  1131. }
  1132. $herald_xscript = $this->getHeraldTranscript();
  1133. if ($herald_xscript) {
  1134. $herald_header = $herald_xscript->getXHeraldRulesHeader();
  1135. $herald_header = HeraldTranscript::saveXHeraldRulesHeader(
  1136. $object->getPHID(),
  1137. $herald_header);
  1138. } else {
  1139. $herald_header = HeraldTranscript::loadXHeraldRulesHeader(
  1140. $object->getPHID());
  1141. }
  1142. $this->heraldHeader = $herald_header;
  1143. // See PHI1134. If we're a subeditor, we don't publish information about
  1144. // the edit yet. Our parent editor still needs to finish applying
  1145. // transactions and execute Herald, which may change the information we
  1146. // publish.
  1147. // For example, Herald actions may change the parent object's title or
  1148. // visibility, or Herald may apply rules like "Must Encrypt" that affect
  1149. // email.
  1150. // Once the parent finishes work, it will queue its own publish step and
  1151. // then queue publish steps for its children.
  1152. $this->publishableObject = $object;
  1153. $this->publishableTransactions = $xactions;
  1154. if (!$this->parentEditor) {
  1155. $this->queuePublishing();
  1156. }
  1157. return $xactions;
  1158. }
  1159. final private function queuePublishing() {
  1160. $object = $this->publishableObject;
  1161. $xactions = $this->publishableTransactions;
  1162. if (!$object) {
  1163. throw new Exception(
  1164. pht(
  1165. 'Editor method "queuePublishing()" was called, but no publishable '.
  1166. 'object is present. This Editor is not ready to publish.'));
  1167. }
  1168. // We're going to compute some of the data we'll use to publish these
  1169. // transactions here, before queueing a worker.
  1170. //
  1171. // Primarily, this is more correct: we want to publish the object as it
  1172. // exists right now. The worker may not execute for some time, and we want
  1173. // to use the current To/CC list, not respect any changes which may occur
  1174. // between now and when the worker executes.
  1175. //
  1176. // As a secondary benefit, this tends to reduce the amount of state that
  1177. // Editors need to pass into workers.
  1178. $object = $this->willPublish($object, $xactions);
  1179. if (!$this->getIsSilent()) {
  1180. if ($this->shouldSendMail($object, $xactions)) {
  1181. $this->mailShouldSend = true;
  1182. $this->mailToPHIDs = $this->getMailTo($object);
  1183. $this->mailCCPHIDs = $this->getMailCC($object);
  1184. $this->mailUnexpandablePHIDs = $this->newMailUnexpandablePHIDs($object);
  1185. // Add any recipients who were previously on the notification list
  1186. // but were removed by this change.
  1187. $this->applyOldRecipientLists();
  1188. if ($object instanceof PhabricatorSubscribableInterface) {
  1189. $this->mailMutedPHIDs = PhabricatorEdgeQuery::loadDestinationPHIDs(
  1190. $object->getPHID(),
  1191. PhabricatorMutedByEdgeType::EDGECONST);
  1192. } else {
  1193. $this->mailMutedPHIDs = array();
  1194. }
  1195. $mail_xactions = $this->getTransactionsForMail($object, $xactions);
  1196. $stamps = $this->newMailStamps($object, $xactions);
  1197. foreach ($stamps as $stamp) {
  1198. $this->mailStamps[] = $stamp->toDictionary();
  1199. }
  1200. }
  1201. if ($this->shouldPublishFeedStory($object, $xactions)) {
  1202. $this->feedShouldPublish = true;
  1203. $this->feedRelatedPHIDs = $this->getFeedRelatedPHIDs(
  1204. $object,
  1205. $xactions);
  1206. $this->feedNotifyPHIDs = $this->getFeedNotifyPHIDs(
  1207. $object,
  1208. $xactions);
  1209. }
  1210. }
  1211. PhabricatorWorker::scheduleTask(
  1212. 'PhabricatorApplicationTransactionPublishWorker',
  1213. array(
  1214. 'objectPHID' => $object->getPHID(),
  1215. 'actorPHID' => $this->getActingAsPHID(),
  1216. 'xactionPHIDs' => mpull($xactions, 'getPHID'),
  1217. 'state' => $this->getWorkerState(),
  1218. ),
  1219. array(
  1220. 'objectPHID' => $object->getPHID(),
  1221. 'priority' => PhabricatorWorker::PRIORITY_ALERTS,
  1222. ));
  1223. foreach ($this->subEditors as $sub_editor) {
  1224. $sub_editor->queuePublishing();
  1225. }
  1226. $this->flushTransactionQueue($object);
  1227. }
  1228. protected function didCatchDuplicateKeyException(
  1229. PhabricatorLiskDAO $object,
  1230. array $xactions,
  1231. Exception $ex) {
  1232. return;
  1233. }
  1234. public function publishTransactions(
  1235. PhabricatorLiskDAO $object,
  1236. array $xactions) {
  1237. $this->object = $object;
  1238. $this->xactions = $xactions;
  1239. // Hook for edges or other properties that may need (re-)loading
  1240. $object = $this->willPublish($object, $xactions);
  1241. // The object might have changed, so reassign it.
  1242. $this->object = $object;
  1243. $messages = array();
  1244. if ($this->mailShouldSend) {
  1245. $messages = $this->buildMail($object, $xactions);
  1246. }
  1247. if ($this->supportsSearch()) {
  1248. PhabricatorSearchWorker::queueDocumentForIndexing(
  1249. $object->getPHID(),
  1250. array(
  1251. 'transactionPHIDs' => mpull($xactions, 'getPHID'),
  1252. ));
  1253. }
  1254. if ($this->feedShouldPublish) {
  1255. $mailed = array();
  1256. foreach ($messages as $mail) {
  1257. foreach ($mail->buildRecipientList() as $phid) {
  1258. $mailed[$phid] = $phid;
  1259. }
  1260. }
  1261. $this->publishFeedStory($object, $xactions, $mailed);
  1262. }
  1263. if ($this->sendHistory) {
  1264. $history_mail = $this->buildHistoryMail($object);
  1265. if ($history_mail) {
  1266. $messages[] = $history_mail;
  1267. }
  1268. }
  1269. foreach ($this->newAuxiliaryMail($object, $xactions) as $message) {
  1270. $messages[] = $message;
  1271. }
  1272. // NOTE: This actually sends the mail. We do this last to reduce the chance
  1273. // that we send some mail, hit an exception, then send the mail again when
  1274. // retrying.
  1275. foreach ($mes

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