/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php
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
- <?php
- /**
- *
- * Publishing and Managing State
- * ======
- *
- * After applying changes, the Editor queues a worker to publish mail, feed,
- * and notifications, and to perform other background work like updating search
- * indexes. This allows it to do this work without impacting performance for
- * users.
- *
- * When work is moved to the daemons, the Editor state is serialized by
- * @{method:getWorkerState}, then reloaded in a daemon process by
- * @{method:loadWorkerState}. **This is fragile.**
- *
- * State is not persisted into the daemons by default, because we can not send
- * arbitrary objects into the queue. This means the default behavior of any
- * state properties is to reset to their defaults without warning prior to
- * publishing.
- *
- * The easiest way to avoid this is to keep Editors stateless: the overwhelming
- * majority of Editors can be written statelessly. If you need to maintain
- * state, you can either:
- *
- * - not require state to exist during publishing; or
- * - pass state to the daemons by implementing @{method:getCustomWorkerState}
- * and @{method:loadCustomWorkerState}.
- *
- * This architecture isn't ideal, and we may eventually split this class into
- * "Editor" and "Publisher" parts to make it more robust. See T6367 for some
- * discussion and context.
- *
- * @task mail Sending Mail
- * @task feed Publishing Feed Stories
- * @task search Search Index
- * @task files Integration with Files
- * @task workers Managing Workers
- */
- abstract class PhabricatorApplicationTransactionEditor
- extends PhabricatorEditor {
- private $contentSource;
- private $object;
- private $xactions;
- private $isNewObject;
- private $mentionedPHIDs;
- private $continueOnNoEffect;
- private $continueOnMissingFields;
- private $raiseWarnings;
- private $parentMessageID;
- private $heraldAdapter;
- private $heraldTranscript;
- private $subscribers;
- private $unmentionablePHIDMap = array();
- private $transactionGroupID;
- private $applicationEmail;
- private $isPreview;
- private $isHeraldEditor;
- private $isInverseEdgeEditor;
- private $actingAsPHID;
- private $heraldEmailPHIDs = array();
- private $heraldForcedEmailPHIDs = array();
- private $heraldHeader;
- private $mailToPHIDs = array();
- private $mailCCPHIDs = array();
- private $feedNotifyPHIDs = array();
- private $feedRelatedPHIDs = array();
- private $feedShouldPublish = false;
- private $mailShouldSend = false;
- private $modularTypes;
- private $silent;
- private $mustEncrypt = array();
- private $stampTemplates = array();
- private $mailStamps = array();
- private $oldTo = array();
- private $oldCC = array();
- private $mailRemovedPHIDs = array();
- private $mailUnexpandablePHIDs = array();
- private $mailMutedPHIDs = array();
- private $webhookMap = array();
- private $transactionQueue = array();
- private $sendHistory = false;
- private $shouldRequireMFA = false;
- private $hasRequiredMFA = false;
- private $request;
- private $cancelURI;
- private $extensions;
- private $parentEditor;
- private $subEditors = array();
- private $publishableObject;
- private $publishableTransactions;
- const STORAGE_ENCODING_BINARY = 'binary';
- /**
- * Get the class name for the application this editor is a part of.
- *
- * Uninstalling the application will disable the editor.
- *
- * @return string Editor's application class name.
- */
- abstract public function getEditorApplicationClass();
- /**
- * Get a description of the objects this editor edits, like "Differential
- * Revisions".
- *
- * @return string Human readable description of edited objects.
- */
- abstract public function getEditorObjectsDescription();
- public function setActingAsPHID($acting_as_phid) {
- $this->actingAsPHID = $acting_as_phid;
- return $this;
- }
- public function getActingAsPHID() {
- if ($this->actingAsPHID) {
- return $this->actingAsPHID;
- }
- return $this->getActor()->getPHID();
- }
- /**
- * When the editor tries to apply transactions that have no effect, should
- * it raise an exception (default) or drop them and continue?
- *
- * Generally, you will set this flag for edits coming from "Edit" interfaces,
- * and leave it cleared for edits coming from "Comment" interfaces, so the
- * user will get a useful error if they try to submit a comment that does
- * nothing (e.g., empty comment with a status change that has already been
- * performed by another user).
- *
- * @param bool True to drop transactions without effect and continue.
- * @return this
- */
- public function setContinueOnNoEffect($continue) {
- $this->continueOnNoEffect = $continue;
- return $this;
- }
- public function getContinueOnNoEffect() {
- return $this->continueOnNoEffect;
- }
- /**
- * When the editor tries to apply transactions which don't populate all of
- * an object's required fields, should it raise an exception (default) or
- * drop them and continue?
- *
- * For example, if a user adds a new required custom field (like "Severity")
- * to a task, all existing tasks won't have it populated. When users
- * manually edit existing tasks, it's usually desirable to have them provide
- * a severity. However, other operations (like batch editing just the
- * owner of a task) will fail by default.
- *
- * By setting this flag for edit operations which apply to specific fields
- * (like the priority, batch, and merge editors in Maniphest), these
- * operations can continue to function even if an object is outdated.
- *
- * @param bool True to continue when transactions don't completely satisfy
- * all required fields.
- * @return this
- */
- public function setContinueOnMissingFields($continue_on_missing_fields) {
- $this->continueOnMissingFields = $continue_on_missing_fields;
- return $this;
- }
- public function getContinueOnMissingFields() {
- return $this->continueOnMissingFields;
- }
- /**
- * Not strictly necessary, but reply handlers ideally set this value to
- * make email threading work better.
- */
- public function setParentMessageID($parent_message_id) {
- $this->parentMessageID = $parent_message_id;
- return $this;
- }
- public function getParentMessageID() {
- return $this->parentMessageID;
- }
- public function getIsNewObject() {
- return $this->isNewObject;
- }
- public function getMentionedPHIDs() {
- return $this->mentionedPHIDs;
- }
- public function setIsPreview($is_preview) {
- $this->isPreview = $is_preview;
- return $this;
- }
- public function getIsPreview() {
- return $this->isPreview;
- }
- public function setIsSilent($silent) {
- $this->silent = $silent;
- return $this;
- }
- public function getIsSilent() {
- return $this->silent;
- }
- public function getMustEncrypt() {
- return $this->mustEncrypt;
- }
- public function getHeraldRuleMonograms() {
- // Convert the stored "<123>, <456>" string into a list: "H123", "H456".
- $list = $this->heraldHeader;
- $list = preg_split('/[, ]+/', $list);
- foreach ($list as $key => $item) {
- $item = trim($item, '<>');
- if (!is_numeric($item)) {
- unset($list[$key]);
- continue;
- }
- $list[$key] = 'H'.$item;
- }
- return $list;
- }
- public function setIsInverseEdgeEditor($is_inverse_edge_editor) {
- $this->isInverseEdgeEditor = $is_inverse_edge_editor;
- return $this;
- }
- public function getIsInverseEdgeEditor() {
- return $this->isInverseEdgeEditor;
- }
- public function setIsHeraldEditor($is_herald_editor) {
- $this->isHeraldEditor = $is_herald_editor;
- return $this;
- }
- public function getIsHeraldEditor() {
- return $this->isHeraldEditor;
- }
- public function addUnmentionablePHIDs(array $phids) {
- foreach ($phids as $phid) {
- $this->unmentionablePHIDMap[$phid] = true;
- }
- return $this;
- }
- private function getUnmentionablePHIDMap() {
- return $this->unmentionablePHIDMap;
- }
- protected function shouldEnableMentions(
- PhabricatorLiskDAO $object,
- array $xactions) {
- return true;
- }
- public function setApplicationEmail(
- PhabricatorMetaMTAApplicationEmail $email) {
- $this->applicationEmail = $email;
- return $this;
- }
- public function getApplicationEmail() {
- return $this->applicationEmail;
- }
- public function setRaiseWarnings($raise_warnings) {
- $this->raiseWarnings = $raise_warnings;
- return $this;
- }
- public function getRaiseWarnings() {
- return $this->raiseWarnings;
- }
- public function setShouldRequireMFA($should_require_mfa) {
- if ($this->hasRequiredMFA) {
- throw new Exception(
- pht(
- 'Call to setShouldRequireMFA() is too late: this Editor has already '.
- 'checked for MFA requirements.'));
- }
- $this->shouldRequireMFA = $should_require_mfa;
- return $this;
- }
- public function getShouldRequireMFA() {
- return $this->shouldRequireMFA;
- }
- public function getTransactionTypesForObject($object) {
- $old = $this->object;
- try {
- $this->object = $object;
- $result = $this->getTransactionTypes();
- $this->object = $old;
- } catch (Exception $ex) {
- $this->object = $old;
- throw $ex;
- }
- return $result;
- }
- public function getTransactionTypes() {
- $types = array();
- $types[] = PhabricatorTransactions::TYPE_CREATE;
- $types[] = PhabricatorTransactions::TYPE_HISTORY;
- if ($this->object instanceof PhabricatorEditEngineSubtypeInterface) {
- $types[] = PhabricatorTransactions::TYPE_SUBTYPE;
- }
- if ($this->object instanceof PhabricatorSubscribableInterface) {
- $types[] = PhabricatorTransactions::TYPE_SUBSCRIBERS;
- }
- if ($this->object instanceof PhabricatorCustomFieldInterface) {
- $types[] = PhabricatorTransactions::TYPE_CUSTOMFIELD;
- }
- if ($this->object instanceof PhabricatorTokenReceiverInterface) {
- $types[] = PhabricatorTransactions::TYPE_TOKEN;
- }
- if ($this->object instanceof PhabricatorProjectInterface ||
- $this->object instanceof PhabricatorMentionableInterface) {
- $types[] = PhabricatorTransactions::TYPE_EDGE;
- }
- if ($this->object instanceof PhabricatorSpacesInterface) {
- $types[] = PhabricatorTransactions::TYPE_SPACE;
- }
- $types[] = PhabricatorTransactions::TYPE_MFA;
- $template = $this->object->getApplicationTransactionTemplate();
- if ($template instanceof PhabricatorModularTransaction) {
- $xtypes = $template->newModularTransactionTypes();
- foreach ($xtypes as $xtype) {
- $types[] = $xtype->getTransactionTypeConstant();
- }
- }
- if ($template) {
- $comment = $template->getApplicationTransactionCommentObject();
- if ($comment) {
- $types[] = PhabricatorTransactions::TYPE_COMMENT;
- }
- }
- return $types;
- }
- private function adjustTransactionValues(
- PhabricatorLiskDAO $object,
- PhabricatorApplicationTransaction $xaction) {
- if ($xaction->shouldGenerateOldValue()) {
- $old = $this->getTransactionOldValue($object, $xaction);
- $xaction->setOldValue($old);
- }
- $new = $this->getTransactionNewValue($object, $xaction);
- $xaction->setNewValue($new);
- }
- private function getTransactionOldValue(
- PhabricatorLiskDAO $object,
- PhabricatorApplicationTransaction $xaction) {
- $type = $xaction->getTransactionType();
- $xtype = $this->getModularTransactionType($type);
- if ($xtype) {
- $xtype = clone $xtype;
- $xtype->setStorage($xaction);
- return $xtype->generateOldValue($object);
- }
- switch ($type) {
- case PhabricatorTransactions::TYPE_CREATE:
- case PhabricatorTransactions::TYPE_HISTORY:
- return null;
- case PhabricatorTransactions::TYPE_SUBTYPE:
- return $object->getEditEngineSubtype();
- case PhabricatorTransactions::TYPE_MFA:
- return null;
- case PhabricatorTransactions::TYPE_SUBSCRIBERS:
- return array_values($this->subscribers);
- case PhabricatorTransactions::TYPE_VIEW_POLICY:
- if ($this->getIsNewObject()) {
- return null;
- }
- return $object->getViewPolicy();
- case PhabricatorTransactions::TYPE_EDIT_POLICY:
- if ($this->getIsNewObject()) {
- return null;
- }
- return $object->getEditPolicy();
- case PhabricatorTransactions::TYPE_JOIN_POLICY:
- if ($this->getIsNewObject()) {
- return null;
- }
- return $object->getJoinPolicy();
- case PhabricatorTransactions::TYPE_SPACE:
- if ($this->getIsNewObject()) {
- return null;
- }
- $space_phid = $object->getSpacePHID();
- if ($space_phid === null) {
- $default_space = PhabricatorSpacesNamespaceQuery::getDefaultSpace();
- if ($default_space) {
- $space_phid = $default_space->getPHID();
- }
- }
- return $space_phid;
- case PhabricatorTransactions::TYPE_EDGE:
- $edge_type = $xaction->getMetadataValue('edge:type');
- if (!$edge_type) {
- throw new Exception(
- pht(
- "Edge transaction has no '%s'!",
- 'edge:type'));
- }
- // See T13082. If this is an inverse edit, the parent editor has
- // already populated the transaction values correctly.
- if ($this->getIsInverseEdgeEditor()) {
- return $xaction->getOldValue();
- }
- $old_edges = array();
- if ($object->getPHID()) {
- $edge_src = $object->getPHID();
- $old_edges = id(new PhabricatorEdgeQuery())
- ->withSourcePHIDs(array($edge_src))
- ->withEdgeTypes(array($edge_type))
- ->needEdgeData(true)
- ->execute();
- $old_edges = $old_edges[$edge_src][$edge_type];
- }
- return $old_edges;
- case PhabricatorTransactions::TYPE_CUSTOMFIELD:
- // NOTE: Custom fields have their old value pre-populated when they are
- // built by PhabricatorCustomFieldList.
- return $xaction->getOldValue();
- case PhabricatorTransactions::TYPE_COMMENT:
- return null;
- default:
- return $this->getCustomTransactionOldValue($object, $xaction);
- }
- }
- private function getTransactionNewValue(
- PhabricatorLiskDAO $object,
- PhabricatorApplicationTransaction $xaction) {
- $type = $xaction->getTransactionType();
- $xtype = $this->getModularTransactionType($type);
- if ($xtype) {
- $xtype = clone $xtype;
- $xtype->setStorage($xaction);
- return $xtype->generateNewValue($object, $xaction->getNewValue());
- }
- switch ($type) {
- case PhabricatorTransactions::TYPE_CREATE:
- return null;
- case PhabricatorTransactions::TYPE_SUBSCRIBERS:
- return $this->getPHIDTransactionNewValue($xaction);
- case PhabricatorTransactions::TYPE_VIEW_POLICY:
- case PhabricatorTransactions::TYPE_EDIT_POLICY:
- case PhabricatorTransactions::TYPE_JOIN_POLICY:
- case PhabricatorTransactions::TYPE_TOKEN:
- case PhabricatorTransactions::TYPE_INLINESTATE:
- case PhabricatorTransactions::TYPE_SUBTYPE:
- case PhabricatorTransactions::TYPE_HISTORY:
- return $xaction->getNewValue();
- case PhabricatorTransactions::TYPE_MFA:
- return true;
- case PhabricatorTransactions::TYPE_SPACE:
- $space_phid = $xaction->getNewValue();
- if (!strlen($space_phid)) {
- // If an install has no Spaces or the Spaces controls are not visible
- // to the viewer, we might end up with the empty string here instead
- // of a strict `null`, because some controller just used `getStr()`
- // to read the space PHID from the request.
- // Just make this work like callers might reasonably expect so we
- // don't need to handle this specially in every EditController.
- return $this->getActor()->getDefaultSpacePHID();
- } else {
- return $space_phid;
- }
- case PhabricatorTransactions::TYPE_EDGE:
- // See T13082. If this is an inverse edit, the parent editor has
- // already populated appropriate transaction values.
- if ($this->getIsInverseEdgeEditor()) {
- return $xaction->getNewValue();
- }
- $new_value = $this->getEdgeTransactionNewValue($xaction);
- $edge_type = $xaction->getMetadataValue('edge:type');
- $type_project = PhabricatorProjectObjectHasProjectEdgeType::EDGECONST;
- if ($edge_type == $type_project) {
- $new_value = $this->applyProjectConflictRules($new_value);
- }
- return $new_value;
- case PhabricatorTransactions::TYPE_CUSTOMFIELD:
- $field = $this->getCustomFieldForTransaction($object, $xaction);
- return $field->getNewValueFromApplicationTransactions($xaction);
- case PhabricatorTransactions::TYPE_COMMENT:
- return null;
- default:
- return $this->getCustomTransactionNewValue($object, $xaction);
- }
- }
- protected function getCustomTransactionOldValue(
- PhabricatorLiskDAO $object,
- PhabricatorApplicationTransaction $xaction) {
- throw new Exception(pht('Capability not supported!'));
- }
- protected function getCustomTransactionNewValue(
- PhabricatorLiskDAO $object,
- PhabricatorApplicationTransaction $xaction) {
- throw new Exception(pht('Capability not supported!'));
- }
- protected function transactionHasEffect(
- PhabricatorLiskDAO $object,
- PhabricatorApplicationTransaction $xaction) {
- switch ($xaction->getTransactionType()) {
- case PhabricatorTransactions::TYPE_CREATE:
- case PhabricatorTransactions::TYPE_HISTORY:
- return true;
- case PhabricatorTransactions::TYPE_CUSTOMFIELD:
- $field = $this->getCustomFieldForTransaction($object, $xaction);
- return $field->getApplicationTransactionHasEffect($xaction);
- case PhabricatorTransactions::TYPE_EDGE:
- // A straight value comparison here doesn't always get the right
- // result, because newly added edges aren't fully populated. Instead,
- // compare the changes in a more granular way.
- $old = $xaction->getOldValue();
- $new = $xaction->getNewValue();
- $old_dst = array_keys($old);
- $new_dst = array_keys($new);
- // NOTE: For now, we don't consider edge reordering to be a change.
- // We have very few order-dependent edges and effectively no order
- // oriented UI. This might change in the future.
- sort($old_dst);
- sort($new_dst);
- if ($old_dst !== $new_dst) {
- // We've added or removed edges, so this transaction definitely
- // has an effect.
- return true;
- }
- // We haven't added or removed edges, but we might have changed
- // edge data.
- foreach ($old as $key => $old_value) {
- $new_value = $new[$key];
- if ($old_value['data'] !== $new_value['data']) {
- return true;
- }
- }
- return false;
- }
- $type = $xaction->getTransactionType();
- $xtype = $this->getModularTransactionType($type);
- if ($xtype) {
- return $xtype->getTransactionHasEffect(
- $object,
- $xaction->getOldValue(),
- $xaction->getNewValue());
- }
- if ($xaction->hasComment()) {
- return true;
- }
- return ($xaction->getOldValue() !== $xaction->getNewValue());
- }
- protected function shouldApplyInitialEffects(
- PhabricatorLiskDAO $object,
- array $xactions) {
- return false;
- }
- protected function applyInitialEffects(
- PhabricatorLiskDAO $object,
- array $xactions) {
- throw new PhutilMethodNotImplementedException();
- }
- private function applyInternalEffects(
- PhabricatorLiskDAO $object,
- PhabricatorApplicationTransaction $xaction) {
- $type = $xaction->getTransactionType();
- $xtype = $this->getModularTransactionType($type);
- if ($xtype) {
- $xtype = clone $xtype;
- $xtype->setStorage($xaction);
- return $xtype->applyInternalEffects($object, $xaction->getNewValue());
- }
- switch ($type) {
- case PhabricatorTransactions::TYPE_CUSTOMFIELD:
- $field = $this->getCustomFieldForTransaction($object, $xaction);
- return $field->applyApplicationTransactionInternalEffects($xaction);
- case PhabricatorTransactions::TYPE_CREATE:
- case PhabricatorTransactions::TYPE_HISTORY:
- case PhabricatorTransactions::TYPE_SUBTYPE:
- case PhabricatorTransactions::TYPE_MFA:
- case PhabricatorTransactions::TYPE_TOKEN:
- case PhabricatorTransactions::TYPE_VIEW_POLICY:
- case PhabricatorTransactions::TYPE_EDIT_POLICY:
- case PhabricatorTransactions::TYPE_JOIN_POLICY:
- case PhabricatorTransactions::TYPE_SUBSCRIBERS:
- case PhabricatorTransactions::TYPE_INLINESTATE:
- case PhabricatorTransactions::TYPE_EDGE:
- case PhabricatorTransactions::TYPE_SPACE:
- case PhabricatorTransactions::TYPE_COMMENT:
- return $this->applyBuiltinInternalTransaction($object, $xaction);
- }
- return $this->applyCustomInternalTransaction($object, $xaction);
- }
- private function applyExternalEffects(
- PhabricatorLiskDAO $object,
- PhabricatorApplicationTransaction $xaction) {
- $type = $xaction->getTransactionType();
- $xtype = $this->getModularTransactionType($type);
- if ($xtype) {
- $xtype = clone $xtype;
- $xtype->setStorage($xaction);
- return $xtype->applyExternalEffects($object, $xaction->getNewValue());
- }
- switch ($type) {
- case PhabricatorTransactions::TYPE_SUBSCRIBERS:
- $subeditor = id(new PhabricatorSubscriptionsEditor())
- ->setObject($object)
- ->setActor($this->requireActor());
- $old_map = array_fuse($xaction->getOldValue());
- $new_map = array_fuse($xaction->getNewValue());
- $subeditor->unsubscribe(
- array_keys(
- array_diff_key($old_map, $new_map)));
- $subeditor->subscribeExplicit(
- array_keys(
- array_diff_key($new_map, $old_map)));
- $subeditor->save();
- // for the rest of these edits, subscribers should include those just
- // added as well as those just removed.
- $subscribers = array_unique(array_merge(
- $this->subscribers,
- $xaction->getOldValue(),
- $xaction->getNewValue()));
- $this->subscribers = $subscribers;
- return $this->applyBuiltinExternalTransaction($object, $xaction);
- case PhabricatorTransactions::TYPE_CUSTOMFIELD:
- $field = $this->getCustomFieldForTransaction($object, $xaction);
- return $field->applyApplicationTransactionExternalEffects($xaction);
- case PhabricatorTransactions::TYPE_CREATE:
- case PhabricatorTransactions::TYPE_HISTORY:
- case PhabricatorTransactions::TYPE_SUBTYPE:
- case PhabricatorTransactions::TYPE_MFA:
- case PhabricatorTransactions::TYPE_EDGE:
- case PhabricatorTransactions::TYPE_TOKEN:
- case PhabricatorTransactions::TYPE_VIEW_POLICY:
- case PhabricatorTransactions::TYPE_EDIT_POLICY:
- case PhabricatorTransactions::TYPE_JOIN_POLICY:
- case PhabricatorTransactions::TYPE_INLINESTATE:
- case PhabricatorTransactions::TYPE_SPACE:
- case PhabricatorTransactions::TYPE_COMMENT:
- return $this->applyBuiltinExternalTransaction($object, $xaction);
- }
- return $this->applyCustomExternalTransaction($object, $xaction);
- }
- protected function applyCustomInternalTransaction(
- PhabricatorLiskDAO $object,
- PhabricatorApplicationTransaction $xaction) {
- $type = $xaction->getTransactionType();
- throw new Exception(
- pht(
- "Transaction type '%s' is missing an internal apply implementation!",
- $type));
- }
- protected function applyCustomExternalTransaction(
- PhabricatorLiskDAO $object,
- PhabricatorApplicationTransaction $xaction) {
- $type = $xaction->getTransactionType();
- throw new Exception(
- pht(
- "Transaction type '%s' is missing an external apply implementation!",
- $type));
- }
- /**
- * @{class:PhabricatorTransactions} provides many built-in transactions
- * which should not require much - if any - code in specific applications.
- *
- * This method is a hook for the exceedingly-rare cases where you may need
- * to do **additional** work for built-in transactions. Developers should
- * extend this method, making sure to return the parent implementation
- * regardless of handling any transactions.
- *
- * See also @{method:applyBuiltinExternalTransaction}.
- */
- protected function applyBuiltinInternalTransaction(
- PhabricatorLiskDAO $object,
- PhabricatorApplicationTransaction $xaction) {
- switch ($xaction->getTransactionType()) {
- case PhabricatorTransactions::TYPE_VIEW_POLICY:
- $object->setViewPolicy($xaction->getNewValue());
- break;
- case PhabricatorTransactions::TYPE_EDIT_POLICY:
- $object->setEditPolicy($xaction->getNewValue());
- break;
- case PhabricatorTransactions::TYPE_JOIN_POLICY:
- $object->setJoinPolicy($xaction->getNewValue());
- break;
- case PhabricatorTransactions::TYPE_SPACE:
- $object->setSpacePHID($xaction->getNewValue());
- break;
- case PhabricatorTransactions::TYPE_SUBTYPE:
- $object->setEditEngineSubtype($xaction->getNewValue());
- break;
- }
- }
- /**
- * See @{method::applyBuiltinInternalTransaction}.
- */
- protected function applyBuiltinExternalTransaction(
- PhabricatorLiskDAO $object,
- PhabricatorApplicationTransaction $xaction) {
- switch ($xaction->getTransactionType()) {
- case PhabricatorTransactions::TYPE_EDGE:
- if ($this->getIsInverseEdgeEditor()) {
- // If we're writing an inverse edge transaction, don't actually
- // do anything. The initiating editor on the other side of the
- // transaction will take care of the edge writes.
- break;
- }
- $old = $xaction->getOldValue();
- $new = $xaction->getNewValue();
- $src = $object->getPHID();
- $const = $xaction->getMetadataValue('edge:type');
- foreach ($new as $dst_phid => $edge) {
- $new[$dst_phid]['src'] = $src;
- }
- $editor = new PhabricatorEdgeEditor();
- foreach ($old as $dst_phid => $edge) {
- if (!empty($new[$dst_phid])) {
- if ($old[$dst_phid]['data'] === $new[$dst_phid]['data']) {
- continue;
- }
- }
- $editor->removeEdge($src, $const, $dst_phid);
- }
- foreach ($new as $dst_phid => $edge) {
- if (!empty($old[$dst_phid])) {
- if ($old[$dst_phid]['data'] === $new[$dst_phid]['data']) {
- continue;
- }
- }
- $data = array(
- 'data' => $edge['data'],
- );
- $editor->addEdge($src, $const, $dst_phid, $data);
- }
- $editor->save();
- $this->updateWorkboardColumns($object, $const, $old, $new);
- break;
- case PhabricatorTransactions::TYPE_VIEW_POLICY:
- case PhabricatorTransactions::TYPE_SPACE:
- $this->scrambleFileSecrets($object);
- break;
- case PhabricatorTransactions::TYPE_HISTORY:
- $this->sendHistory = true;
- break;
- }
- }
- /**
- * Fill in a transaction's common values, like author and content source.
- */
- protected function populateTransaction(
- PhabricatorLiskDAO $object,
- PhabricatorApplicationTransaction $xaction) {
- $actor = $this->getActor();
- // TODO: This needs to be more sophisticated once we have meta-policies.
- $xaction->setViewPolicy(PhabricatorPolicies::POLICY_PUBLIC);
- if ($actor->isOmnipotent()) {
- $xaction->setEditPolicy(PhabricatorPolicies::POLICY_NOONE);
- } else {
- $xaction->setEditPolicy($this->getActingAsPHID());
- }
- // If the transaction already has an explicit author PHID, allow it to
- // stand. This is used by applications like Owners that hook into the
- // post-apply change pipeline.
- if (!$xaction->getAuthorPHID()) {
- $xaction->setAuthorPHID($this->getActingAsPHID());
- }
- $xaction->setContentSource($this->getContentSource());
- $xaction->attachViewer($actor);
- $xaction->attachObject($object);
- if ($object->getPHID()) {
- $xaction->setObjectPHID($object->getPHID());
- }
- if ($this->getIsSilent()) {
- $xaction->setIsSilentTransaction(true);
- }
- return $xaction;
- }
- protected function didApplyInternalEffects(
- PhabricatorLiskDAO $object,
- array $xactions) {
- return $xactions;
- }
- protected function applyFinalEffects(
- PhabricatorLiskDAO $object,
- array $xactions) {
- return $xactions;
- }
- final protected function didCommitTransactions(
- PhabricatorLiskDAO $object,
- array $xactions) {
- foreach ($xactions as $xaction) {
- $type = $xaction->getTransactionType();
- // See T13082. When we're writing edges that imply corresponding inverse
- // transactions, apply those inverse transactions now. We have to wait
- // until the object we're editing (with this editor) has committed its
- // transactions to do this. If we don't, the inverse editor may race,
- // build a mail before we actually commit this object, and render "alice
- // added an edge: Unknown Object".
- if ($type === PhabricatorTransactions::TYPE_EDGE) {
- // Don't do anything if we're already an inverse edge editor.
- if ($this->getIsInverseEdgeEditor()) {
- continue;
- }
- $edge_const = $xaction->getMetadataValue('edge:type');
- $edge_type = PhabricatorEdgeType::getByConstant($edge_const);
- if ($edge_type->shouldWriteInverseTransactions()) {
- $this->applyInverseEdgeTransactions(
- $object,
- $xaction,
- $edge_type->getInverseEdgeConstant());
- }
- continue;
- }
- $xtype = $this->getModularTransactionType($type);
- if (!$xtype) {
- continue;
- }
- $xtype = clone $xtype;
- $xtype->setStorage($xaction);
- $xtype->didCommitTransaction($object, $xaction->getNewValue());
- }
- }
- public function setContentSource(PhabricatorContentSource $content_source) {
- $this->contentSource = $content_source;
- return $this;
- }
- public function setContentSourceFromRequest(AphrontRequest $request) {
- $this->setRequest($request);
- return $this->setContentSource(
- PhabricatorContentSource::newFromRequest($request));
- }
- public function getContentSource() {
- return $this->contentSource;
- }
- public function setRequest(AphrontRequest $request) {
- $this->request = $request;
- return $this;
- }
- public function getRequest() {
- return $this->request;
- }
- public function setCancelURI($cancel_uri) {
- $this->cancelURI = $cancel_uri;
- return $this;
- }
- public function getCancelURI() {
- return $this->cancelURI;
- }
- protected function getTransactionGroupID() {
- if ($this->transactionGroupID === null) {
- $this->transactionGroupID = Filesystem::readRandomCharacters(32);
- }
- return $this->transactionGroupID;
- }
- final public function applyTransactions(
- PhabricatorLiskDAO $object,
- array $xactions) {
- $is_new = ($object->getID() === null);
- $this->isNewObject = $is_new;
- $is_preview = $this->getIsPreview();
- $read_locking = false;
- $transaction_open = false;
- // If we're attempting to apply transactions, lock and reload the object
- // before we go anywhere. If we don't do this at the very beginning, we
- // may be looking at an older version of the object when we populate and
- // filter the transactions. See PHI1165 for an example.
- if (!$is_preview) {
- if (!$is_new) {
- $this->buildOldRecipientLists($object, $xactions);
- $object->openTransaction();
- $transaction_open = true;
- $object->beginReadLocking();
- $read_locking = true;
- $object->reload();
- }
- }
- try {
- $this->object = $object;
- $this->xactions = $xactions;
- $this->validateEditParameters($object, $xactions);
- $xactions = $this->newMFATransactions($object, $xactions);
- $actor = $this->requireActor();
- // NOTE: Some transaction expansion requires that the edited object be
- // attached.
- foreach ($xactions as $xaction) {
- $xaction->attachObject($object);
- $xaction->attachViewer($actor);
- }
- $xactions = $this->expandTransactions($object, $xactions);
- $xactions = $this->expandSupportTransactions($object, $xactions);
- $xactions = $this->combineTransactions($xactions);
- foreach ($xactions as $xaction) {
- $xaction = $this->populateTransaction($object, $xaction);
- }
- if (!$is_preview) {
- $errors = array();
- $type_map = mgroup($xactions, 'getTransactionType');
- foreach ($this->getTransactionTypes() as $type) {
- $type_xactions = idx($type_map, $type, array());
- $errors[] = $this->validateTransaction(
- $object,
- $type,
- $type_xactions);
- }
- $errors[] = $this->validateAllTransactions($object, $xactions);
- $errors[] = $this->validateTransactionsWithExtensions(
- $object,
- $xactions);
- $errors = array_mergev($errors);
- $continue_on_missing = $this->getContinueOnMissingFields();
- foreach ($errors as $key => $error) {
- if ($continue_on_missing && $error->getIsMissingFieldError()) {
- unset($errors[$key]);
- }
- }
- if ($errors) {
- throw new PhabricatorApplicationTransactionValidationException(
- $errors);
- }
- if ($this->raiseWarnings) {
- $warnings = array();
- foreach ($xactions as $xaction) {
- if ($this->hasWarnings($object, $xaction)) {
- $warnings[] = $xaction;
- }
- }
- if ($warnings) {
- throw new PhabricatorApplicationTransactionWarningException(
- $warnings);
- }
- }
- }
- foreach ($xactions as $xaction) {
- $this->adjustTransactionValues($object, $xaction);
- }
- // Now that we've merged and combined transactions, check for required
- // capabilities. Note that we're doing this before filtering
- // transactions: if you try to apply an edit which you do not have
- // permission to apply, we want to give you a permissions error even
- // if the edit would have no effect.
- $this->applyCapabilityChecks($object, $xactions);
- $xactions = $this->filterTransactions($object, $xactions);
- if (!$is_preview) {
- $this->hasRequiredMFA = true;
- if ($this->getShouldRequireMFA()) {
- $this->requireMFA($object, $xactions);
- }
- if ($this->shouldApplyInitialEffects($object, $xactions)) {
- if (!$transaction_open) {
- $object->openTransaction();
- $transaction_open = true;
- }
- }
- }
- if ($this->shouldApplyInitialEffects($object, $xactions)) {
- $this->applyInitialEffects($object, $xactions);
- }
- // TODO: Once everything is on EditEngine, just use getIsNewObject() to
- // figure this out instead.
- $mark_as_create = false;
- $create_type = PhabricatorTransactions::TYPE_CREATE;
- foreach ($xactions as $xaction) {
- if ($xaction->getTransactionType() == $create_type) {
- $mark_as_create = true;
- }
- }
- if ($mark_as_create) {
- foreach ($xactions as $xaction) {
- $xaction->setIsCreateTransaction(true);
- }
- }
- $xactions = $this->sortTransactions($xactions);
- $file_phids = $this->extractFilePHIDs($object, $xactions);
- if ($is_preview) {
- $this->loadHandles($xactions);
- return $xactions;
- }
- $comment_editor = id(new PhabricatorApplicationTransactionCommentEditor())
- ->setActor($actor)
- ->setActingAsPHID($this->getActingAsPHID())
- ->setContentSource($this->getContentSource())
- ->setIsNewComment(true);
- if (!$transaction_open) {
- $object->openTransaction();
- $transaction_open = true;
- }
- // We can technically test any object for CAN_INTERACT, but we can
- // run into some issues in doing so (for example, in project unit tests).
- // For now, only test for CAN_INTERACT if the object is explicitly a
- // lockable object.
- $was_locked = false;
- if ($object instanceof PhabricatorEditEngineLockableInterface) {
- $was_locked = !PhabricatorPolicyFilter::canInteract($actor, $object);
- }
- foreach ($xactions as $xaction) {
- $this->applyInternalEffects($object, $xaction);
- }
- $xactions = $this->didApplyInternalEffects($object, $xactions);
- try {
- $object->save();
- } catch (AphrontDuplicateKeyQueryException $ex) {
- // This callback has an opportunity to throw a better exception,
- // so execution may end here.
- $this->didCatchDuplicateKeyException($object, $xactions, $ex);
- throw $ex;
- }
- $group_id = $this->getTransactionGroupID();
- foreach ($xactions as $xaction) {
- if ($was_locked) {
- $is_override = $this->isLockOverrideTransaction($xaction);
- if ($is_override) {
- $xaction->setIsLockOverrideTransaction(true);
- }
- }
- $xaction->setObjectPHID($object->getPHID());
- $xaction->setTransactionGroupID($group_id);
- if ($xaction->getComment()) {
- $xaction->setPHID($xaction->generatePHID());
- $comment_editor->applyEdit($xaction, $xaction->getComment());
- } else {
- // TODO: This is a transitional hack to let us migrate edge
- // transactions to a more efficient storage format. For now, we're
- // going to write a new slim format to the database but keep the old
- // bulky format on the objects so we don't have to upgrade all the
- // edit logic to the new format yet. See T13051.
- $edge_type = PhabricatorTransactions::TYPE_EDGE;
- if ($xaction->getTransactionType() == $edge_type) {
- $bulky_old = $xaction->getOldValue();
- $bulky_new = $xaction->getNewValue();
- $record = PhabricatorEdgeChangeRecord::newFromTransaction($xaction);
- $slim_old = $record->getModernOldEdgeTransactionData();
- $slim_new = $record->getModernNewEdgeTransactionData();
- $xaction->setOldValue($slim_old);
- $xaction->setNewValue($slim_new);
- $xaction->save();
- $xaction->setOldValue($bulky_old);
- $xaction->setNewValue($bulky_new);
- } else {
- $xaction->save();
- }
- }
- }
- if ($file_phids) {
- $this->attachFiles($object, $file_phids);
- }
- foreach ($xactions as $xaction) {
- $this->applyExternalEffects($object, $xaction);
- }
- $xactions = $this->applyFinalEffects($object, $xactions);
- if ($read_locking) {
- $object->endReadLocking();
- $read_locking = false;
- }
- if ($transaction_open) {
- $object->saveTransaction();
- $transaction_open = false;
- }
- $this->didCommitTransactions($object, $xactions);
- } catch (Exception $ex) {
- if ($read_locking) {
- $object->endReadLocking();
- $read_locking = false;
- }
- if ($transaction_open) {
- $object->killTransaction();
- $transaction_open = false;
- }
- throw $ex;
- }
- // If we need to perform cache engine updates, execute them now.
- id(new PhabricatorCacheEngine())
- ->updateObject($object);
- // Now that we've completely applied the core transaction set, try to apply
- // Herald rules. Herald rules are allowed to either take direct actions on
- // the database (like writing flags), or take indirect actions (like saving
- // some targets for CC when we generate mail a little later), or return
- // transactions which we'll apply normally using another Editor.
- // First, check if *this* is a sub-editor which is itself applying Herald
- // rules: if it is, stop working and return so we don't descend into
- // madness.
- // Otherwise, we're not a Herald editor, so process Herald rules (possibly
- // using a Herald editor to apply resulting transactions) and then send out
- // mail, notifications, and feed updates about everything.
- if ($this->getIsHeraldEditor()) {
- // We are the Herald editor, so stop work here and return the updated
- // transactions.
- return $xactions;
- } else if ($this->getIsInverseEdgeEditor()) {
- // Do not run Herald if we're just recording that this object was
- // mentioned elsewhere. This tends to create Herald side effects which
- // feel arbitrary, and can really slow down edits which mention a large
- // number of other objects. See T13114.
- } else if ($this->shouldApplyHeraldRules($object, $xactions)) {
- // We are not the Herald editor, so try to apply Herald rules.
- $herald_xactions = $this->applyHeraldRules($object, $xactions);
- if ($herald_xactions) {
- $xscript_id = $this->getHeraldTranscript()->getID();
- foreach ($herald_xactions as $herald_xaction) {
- // Don't set a transcript ID if this is a transaction from another
- // application or source, like Owners.
- if ($herald_xaction->getAuthorPHID()) {
- continue;
- }
- $herald_xaction->setMetadataValue('herald:transcriptID', $xscript_id);
- }
- // NOTE: We're acting as the omnipotent user because rules deal with
- // their own policy issues. We use a synthetic author PHID (the
- // Herald application) as the author of record, so that transactions
- // will render in a reasonable way ("Herald assigned this task ...").
- $herald_actor = PhabricatorUser::getOmnipotentUser();
- $herald_phid = id(new PhabricatorHeraldApplication())->getPHID();
- // TODO: It would be nice to give transactions a more specific source
- // which points at the rule which generated them. You can figure this
- // out from transcripts, but it would be cleaner if you didn't have to.
- $herald_source = PhabricatorContentSource::newForSource(
- PhabricatorHeraldContentSource::SOURCECONST);
- $herald_editor = $this->newEditorCopy()
- ->setContinueOnNoEffect(true)
- ->setContinueOnMissingFields(true)
- ->setIsHeraldEditor(true)
- ->setActor($herald_actor)
- ->setActingAsPHID($herald_phid)
- ->setContentSource($herald_source);
- $herald_xactions = $herald_editor->applyTransactions(
- $object,
- $herald_xactions);
- // Merge the new transactions into the transaction list: we want to
- // send email and publish feed stories about them, too.
- $xactions = array_merge($xactions, $herald_xactions);
- }
- // If Herald did not generate transactions, we may still need to handle
- // "Send an Email" rules.
- $adapter = $this->getHeraldAdapter();
- $this->heraldEmailPHIDs = $adapter->getEmailPHIDs();
- $this->heraldForcedEmailPHIDs = $adapter->getForcedEmailPHIDs();
- $this->webhookMap = $adapter->getWebhookMap();
- }
- $xactions = $this->didApplyTransactions($object, $xactions);
- if ($object instanceof PhabricatorCustomFieldInterface) {
- // Maybe this makes more sense to move into the search index itself? For
- // now I'm putting it here since I think we might end up with things that
- // need it to be up to date once the next page loads, but if we don't go
- // there we could move it into search once search moves to the daemons.
- // It now happens in the search indexer as well, but the search indexer is
- // always daemonized, so the logic above still potentially holds. We could
- // possibly get rid of this. The major motivation for putting it in the
- // indexer was to enable reindexing to work.
- $fields = PhabricatorCustomField::getObjectFields(
- $object,
- PhabricatorCustomField::ROLE_APPLICATIONSEARCH);
- $fields->readFieldsFromStorage($object);
- $fields->rebuildIndexes($object);
- }
- $herald_xscript = $this->getHeraldTranscript();
- if ($herald_xscript) {
- $herald_header = $herald_xscript->getXHeraldRulesHeader();
- $herald_header = HeraldTranscript::saveXHeraldRulesHeader(
- $object->getPHID(),
- $herald_header);
- } else {
- $herald_header = HeraldTranscript::loadXHeraldRulesHeader(
- $object->getPHID());
- }
- $this->heraldHeader = $herald_header;
- // See PHI1134. If we're a subeditor, we don't publish information about
- // the edit yet. Our parent editor still needs to finish applying
- // transactions and execute Herald, which may change the information we
- // publish.
- // For example, Herald actions may change the parent object's title or
- // visibility, or Herald may apply rules like "Must Encrypt" that affect
- // email.
- // Once the parent finishes work, it will queue its own publish step and
- // then queue publish steps for its children.
- $this->publishableObject = $object;
- $this->publishableTransactions = $xactions;
- if (!$this->parentEditor) {
- $this->queuePublishing();
- }
- return $xactions;
- }
- final private function queuePublishing() {
- $object = $this->publishableObject;
- $xactions = $this->publishableTransactions;
- if (!$object) {
- throw new Exception(
- pht(
- 'Editor method "queuePublishing()" was called, but no publishable '.
- 'object is present. This Editor is not ready to publish.'));
- }
- // We're going to compute some of the data we'll use to publish these
- // transactions here, before queueing a worker.
- //
- // Primarily, this is more correct: we want to publish the object as it
- // exists right now. The worker may not execute for some time, and we want
- // to use the current To/CC list, not respect any changes which may occur
- // between now and when the worker executes.
- //
- // As a secondary benefit, this tends to reduce the amount of state that
- // Editors need to pass into workers.
- $object = $this->willPublish($object, $xactions);
- if (!$this->getIsSilent()) {
- if ($this->shouldSendMail($object, $xactions)) {
- $this->mailShouldSend = true;
- $this->mailToPHIDs = $this->getMailTo($object);
- $this->mailCCPHIDs = $this->getMailCC($object);
- $this->mailUnexpandablePHIDs = $this->newMailUnexpandablePHIDs($object);
- // Add any recipients who were previously on the notification list
- // but were removed by this change.
- $this->applyOldRecipientLists();
- if ($object instanceof PhabricatorSubscribableInterface) {
- $this->mailMutedPHIDs = PhabricatorEdgeQuery::loadDestinationPHIDs(
- $object->getPHID(),
- PhabricatorMutedByEdgeType::EDGECONST);
- } else {
- $this->mailMutedPHIDs = array();
- }
- $mail_xactions = $this->getTransactionsForMail($object, $xactions);
- $stamps = $this->newMailStamps($object, $xactions);
- foreach ($stamps as $stamp) {
- $this->mailStamps[] = $stamp->toDictionary();
- }
- }
- if ($this->shouldPublishFeedStory($object, $xactions)) {
- $this->feedShouldPublish = true;
- $this->feedRelatedPHIDs = $this->getFeedRelatedPHIDs(
- $object,
- $xactions);
- $this->feedNotifyPHIDs = $this->getFeedNotifyPHIDs(
- $object,
- $xactions);
- }
- }
- PhabricatorWorker::scheduleTask(
- 'PhabricatorApplicationTransactionPublishWorker',
- array(
- 'objectPHID' => $object->getPHID(),
- 'actorPHID' => $this->getActingAsPHID(),
- 'xactionPHIDs' => mpull($xactions, 'getPHID'),
- 'state' => $this->getWorkerState(),
- ),
- array(
- 'objectPHID' => $object->getPHID(),
- 'priority' => PhabricatorWorker::PRIORITY_ALERTS,
- ));
- foreach ($this->subEditors as $sub_editor) {
- $sub_editor->queuePublishing();
- }
- $this->flushTransactionQueue($object);
- }
- protected function didCatchDuplicateKeyException(
- PhabricatorLiskDAO $object,
- array $xactions,
- Exception $ex) {
- return;
- }
- public function publishTransactions(
- PhabricatorLiskDAO $object,
- array $xactions) {
- $this->object = $object;
- $this->xactions = $xactions;
- // Hook for edges or other properties that may need (re-)loading
- $object = $this->willPublish($object, $xactions);
- // The object might have changed, so reassign it.
- $this->object = $object;
- $messages = array();
- if ($this->mailShouldSend) {
- $messages = $this->buildMail($object, $xactions);
- }
- if ($this->supportsSearch()) {
- PhabricatorSearchWorker::queueDocumentForIndexing(
- $object->getPHID(),
- array(
- 'transactionPHIDs' => mpull($xactions, 'getPHID'),
- ));
- }
- if ($this->feedShouldPublish) {
- $mailed = array();
- foreach ($messages as $mail) {
- foreach ($mail->buildRecipientList() as $phid) {
- $mailed[$phid] = $phid;
- }
- }
- $this->publishFeedStory($object, $xactions, $mailed);
- }
- if ($this->sendHistory) {
- $history_mail = $this->buildHistoryMail($object);
- if ($history_mail) {
- $messages[] = $history_mail;
- }
- }
- foreach ($this->newAuxiliaryMail($object, $xactions) as $message) {
- $messages[] = $message;
- }
- // NOTE: This actually sends the mail. We do this last to reduce the chance
- // that we send some mail, hit an exception, then send the mail again when
- // retrying.
- foreach ($mes…
Large files files are truncated, but you can click here to view the full file