Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F2889235
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Advanced/Developer...
View Handle
View Hovercard
Size
94 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/src/applications/maniphest/editor/ManiphestTransactionEditor.php b/src/applications/maniphest/editor/ManiphestTransactionEditor.php
index 2c9463f928..2ef22808e7 100644
--- a/src/applications/maniphest/editor/ManiphestTransactionEditor.php
+++ b/src/applications/maniphest/editor/ManiphestTransactionEditor.php
@@ -1,425 +1,435 @@
<?php
final class ManiphestTransactionEditor
extends PhabricatorApplicationTransactionEditor {
public function getTransactionTypes() {
$types = parent::getTransactionTypes();
$types[] = PhabricatorTransactions::TYPE_COMMENT;
$types[] = ManiphestTransaction::TYPE_PRIORITY;
$types[] = ManiphestTransaction::TYPE_STATUS;
$types[] = ManiphestTransaction::TYPE_TITLE;
$types[] = ManiphestTransaction::TYPE_DESCRIPTION;
$types[] = ManiphestTransaction::TYPE_OWNER;
$types[] = ManiphestTransaction::TYPE_CCS;
$types[] = ManiphestTransaction::TYPE_PROJECTS;
$types[] = ManiphestTransaction::TYPE_ATTACH;
$types[] = ManiphestTransaction::TYPE_EDGE;
$types[] = ManiphestTransaction::TYPE_SUBPRIORITY;
$types[] = ManiphestTransaction::TYPE_PROJECT_COLUMN;
$types[] = PhabricatorTransactions::TYPE_VIEW_POLICY;
$types[] = PhabricatorTransactions::TYPE_EDIT_POLICY;
return $types;
}
protected function getCustomTransactionOldValue(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case ManiphestTransaction::TYPE_PRIORITY:
if ($this->getIsNewObject()) {
return null;
}
return (int)$object->getPriority();
case ManiphestTransaction::TYPE_STATUS:
if ($this->getIsNewObject()) {
return null;
}
return (int)$object->getStatus();
case ManiphestTransaction::TYPE_TITLE:
if ($this->getIsNewObject()) {
return null;
}
return $object->getTitle();
case ManiphestTransaction::TYPE_DESCRIPTION:
if ($this->getIsNewObject()) {
return null;
}
return $object->getDescription();
case ManiphestTransaction::TYPE_OWNER:
return nonempty($object->getOwnerPHID(), null);
case ManiphestTransaction::TYPE_CCS:
return array_values(array_unique($object->getCCPHIDs()));
case ManiphestTransaction::TYPE_PROJECTS:
return array_values(array_unique($object->getProjectPHIDs()));
case ManiphestTransaction::TYPE_ATTACH:
return $object->getAttached();
case ManiphestTransaction::TYPE_EDGE:
case ManiphestTransaction::TYPE_PROJECT_COLUMN:
// These are pre-populated.
return $xaction->getOldValue();
case ManiphestTransaction::TYPE_SUBPRIORITY:
return $object->getSubpriority();
}
}
protected function getCustomTransactionNewValue(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case ManiphestTransaction::TYPE_PRIORITY:
case ManiphestTransaction::TYPE_STATUS:
return (int)$xaction->getNewValue();
case ManiphestTransaction::TYPE_CCS:
case ManiphestTransaction::TYPE_PROJECTS:
return array_values(array_unique($xaction->getNewValue()));
case ManiphestTransaction::TYPE_OWNER:
return nonempty($xaction->getNewValue(), null);
case ManiphestTransaction::TYPE_TITLE:
case ManiphestTransaction::TYPE_DESCRIPTION:
case ManiphestTransaction::TYPE_ATTACH:
case ManiphestTransaction::TYPE_EDGE:
case ManiphestTransaction::TYPE_SUBPRIORITY:
case ManiphestTransaction::TYPE_PROJECT_COLUMN:
return $xaction->getNewValue();
}
}
protected function transactionHasEffect(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
$old = $xaction->getOldValue();
$new = $xaction->getNewValue();
switch ($xaction->getTransactionType()) {
case ManiphestTransaction::TYPE_PROJECTS:
case ManiphestTransaction::TYPE_CCS:
sort($old);
sort($new);
return ($old !== $new);
case ManiphestTransaction::TYPE_PROJECT_COLUMN:
$new_column_phids = $new['columnPHIDs'];
$old_column_phids = $old['columnPHIDs'];
sort($new_column_phids);
sort($old_column_phids);
return ($old !== $new);
}
return parent::transactionHasEffect($object, $xaction);
}
protected function applyCustomInternalTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case ManiphestTransaction::TYPE_PRIORITY:
return $object->setPriority($xaction->getNewValue());
case ManiphestTransaction::TYPE_STATUS:
return $object->setStatus($xaction->getNewValue());
case ManiphestTransaction::TYPE_TITLE:
return $object->setTitle($xaction->getNewValue());
case ManiphestTransaction::TYPE_DESCRIPTION:
return $object->setDescription($xaction->getNewValue());
case ManiphestTransaction::TYPE_OWNER:
$phid = $xaction->getNewValue();
// Update the "ownerOrdering" column to contain the full name of the
// owner, if the task is assigned.
$handle = null;
if ($phid) {
$handle = id(new PhabricatorHandleQuery())
->setViewer($this->getActor())
->withPHIDs(array($phid))
->executeOne();
}
if ($handle) {
$object->setOwnerOrdering($handle->getName());
} else {
$object->setOwnerOrdering(null);
}
return $object->setOwnerPHID($phid);
case ManiphestTransaction::TYPE_CCS:
return $object->setCCPHIDs($xaction->getNewValue());
case ManiphestTransaction::TYPE_PROJECTS:
return $object->setProjectPHIDs($xaction->getNewValue());
case ManiphestTransaction::TYPE_ATTACH:
return $object->setAttached($xaction->getNewValue());
case ManiphestTransaction::TYPE_EDGE:
// These are a weird, funky mess and are already being applied by the
// time we reach this.
return;
case ManiphestTransaction::TYPE_SUBPRIORITY:
$data = $xaction->getNewValue();
$new_sub = $this->getNextSubpriority(
$data['newPriority'],
$data['newSubpriorityBase']);
$object->setSubpriority($new_sub);
return;
case ManiphestTransaction::TYPE_PROJECT_COLUMN:
// these do external (edge) updates
return;
}
}
protected function expandTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
$xactions = parent::expandTransaction($object, $xaction);
switch ($xaction->getTransactionType()) {
case ManiphestTransaction::TYPE_SUBPRIORITY:
$data = $xaction->getNewValue();
$new_pri = $data['newPriority'];
if ($new_pri != $object->getPriority()) {
$xactions[] = id(new ManiphestTransaction())
->setTransactionType(ManiphestTransaction::TYPE_PRIORITY)
->setNewValue($new_pri);
}
break;
default:
break;
}
return $xactions;
}
protected function applyCustomExternalTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case ManiphestTransaction::TYPE_PROJECT_COLUMN:
$new = $xaction->getNewValue();
$old = $xaction->getOldValue();
$src = $object->getPHID();
$dst = head($new['columnPHIDs']);
$edges = $old['columnPHIDs'];
$edge_type = PhabricatorEdgeConfig::TYPE_OBJECT_HAS_COLUMN;
// NOTE: Normally, we expect only one edge to exist, but this works in
// a general way so it will repair any stray edges.
$remove = array();
$edge_missing = true;
foreach ($edges as $phid) {
if ($phid == $dst) {
$edge_missing = false;
} else {
$remove[] = $phid;
}
}
$add = array();
if ($edge_missing) {
$add[] = $dst;
}
// This should never happen because of the code in
// transactionHasEffect, but keep it for maximum conservativeness
if (!$add && !$remove) {
return;
}
$editor = id(new PhabricatorEdgeEditor())
->setActor($this->getActor())
->setSuppressEvents(true);
foreach ($add as $phid) {
$editor->addEdge($src, $edge_type, $phid);
}
foreach ($remove as $phid) {
$editor->removeEdge($src, $edge_type, $phid);
}
$editor->save();
break;
default:
break;
}
}
protected function shouldSendMail(
PhabricatorLiskDAO $object,
array $xactions) {
$xactions = mfilter($xactions, 'shouldHide', true);
return $xactions;
}
protected function getMailSubjectPrefix() {
return PhabricatorEnv::getEnvConfig('metamta.maniphest.subject-prefix');
}
protected function getMailThreadID(PhabricatorLiskDAO $object) {
return 'maniphest-task-'.$object->getPHID();
}
protected function getMailTo(PhabricatorLiskDAO $object) {
return array(
$object->getOwnerPHID(),
$this->requireActor()->getPHID(),
);
}
protected function getMailCC(PhabricatorLiskDAO $object) {
return $object->getCCPHIDs();
}
protected function buildReplyHandler(PhabricatorLiskDAO $object) {
return id(new ManiphestReplyHandler())
->setMailReceiver($object);
}
protected function buildMailTemplate(PhabricatorLiskDAO $object) {
$id = $object->getID();
$title = $object->getTitle();
return id(new PhabricatorMetaMTAMail())
->setSubject("T{$id}: {$title}")
->addHeader('Thread-Topic', "T{$id}: ".$object->getOriginalTitle());
}
protected function buildMailBody(
PhabricatorLiskDAO $object,
array $xactions) {
$body = parent::buildMailBody($object, $xactions);
if ($this->getIsNewObject()) {
$body->addTextSection(
pht('TASK DESCRIPTION'),
$object->getDescription());
}
$body->addTextSection(
pht('TASK DETAIL'),
PhabricatorEnv::getProductionURI('/T'.$object->getID()));
return $body;
}
protected function shouldPublishFeedStory(
PhabricatorLiskDAO $object,
array $xactions) {
return $this->shouldSendMail($object, $xactions);
}
protected function supportsSearch() {
return true;
}
- protected function supportsHerald() {
+ protected function shouldApplyHeraldRules(
+ PhabricatorLiskDAO $object,
+ array $xactions) {
return true;
}
protected function buildHeraldAdapter(
PhabricatorLiskDAO $object,
array $xactions) {
return id(new HeraldManiphestTaskAdapter())
->setTask($object);
}
protected function didApplyHeraldRules(
PhabricatorLiskDAO $object,
HeraldAdapter $adapter,
HeraldTranscript $transcript) {
+ // TODO: Convert these to transactions. The way Maniphest deals with these
+ // transactions is currently unconventional and messy.
+
$save_again = false;
$cc_phids = $adapter->getCcPHIDs();
if ($cc_phids) {
$existing_cc = $object->getCCPHIDs();
$new_cc = array_unique(array_merge($cc_phids, $existing_cc));
$object->setCCPHIDs($new_cc);
$save_again = true;
}
- $assign_phid = $adapter->getAssignPHID();
- if ($assign_phid) {
- $object->setOwnerPHID($assign_phid);
- $save_again = true;
- }
-
$project_phids = $adapter->getProjectPHIDs();
if ($project_phids) {
$existing_projects = $object->getProjectPHIDs();
$new_projects = array_unique(
array_merge($project_phids, $existing_projects));
$object->setProjectPHIDs($new_projects);
$save_again = true;
}
if ($save_again) {
$object->save();
}
+
+ $xactions = array();
+
+ $assign_phid = $adapter->getAssignPHID();
+ if ($assign_phid) {
+ $xactions[] = id(new ManiphestTransaction())
+ ->setTransactionType(ManiphestTransaction::TYPE_OWNER)
+ ->setNewValue($assign_phid);
+ }
+
+ return $xactions;
}
protected function requireCapabilities(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
parent::requireCapabilities($object, $xaction);
$app_capability_map = array(
ManiphestTransaction::TYPE_PRIORITY =>
ManiphestCapabilityEditPriority::CAPABILITY,
ManiphestTransaction::TYPE_STATUS =>
ManiphestCapabilityEditStatus::CAPABILITY,
ManiphestTransaction::TYPE_PROJECTS =>
ManiphestCapabilityEditProjects::CAPABILITY,
ManiphestTransaction::TYPE_OWNER =>
ManiphestCapabilityEditAssign::CAPABILITY,
PhabricatorTransactions::TYPE_EDIT_POLICY =>
ManiphestCapabilityEditPolicies::CAPABILITY,
PhabricatorTransactions::TYPE_VIEW_POLICY =>
ManiphestCapabilityEditPolicies::CAPABILITY,
);
$transaction_type = $xaction->getTransactionType();
$app_capability = idx($app_capability_map, $transaction_type);
if ($app_capability) {
$app = id(new PhabricatorApplicationQuery())
->setViewer($this->getActor())
->withClasses(array('PhabricatorApplicationManiphest'))
->executeOne();
PhabricatorPolicyFilter::requireCapability(
$this->getActor(),
$app,
$app_capability);
}
}
private function getNextSubpriority($pri, $sub) {
if ($sub === null) {
$next = id(new ManiphestTask())->loadOneWhere(
'priority = %d ORDER BY subpriority ASC LIMIT 1',
$pri);
if ($next) {
return $next->getSubpriority() - ((double)(2 << 16));
}
} else {
$next = id(new ManiphestTask())->loadOneWhere(
'priority = %d AND subpriority > %s ORDER BY subpriority ASC LIMIT 1',
$pri,
$sub);
if ($next) {
return ($sub + $next->getSubpriority()) / 2;
}
}
return (double)(2 << 32);
}
}
diff --git a/src/applications/metamta/contentsource/PhabricatorContentSource.php b/src/applications/metamta/contentsource/PhabricatorContentSource.php
index 8c062ba880..57ebb1b75d 100644
--- a/src/applications/metamta/contentsource/PhabricatorContentSource.php
+++ b/src/applications/metamta/contentsource/PhabricatorContentSource.php
@@ -1,96 +1,98 @@
<?php
final class PhabricatorContentSource {
const SOURCE_UNKNOWN = 'unknown';
const SOURCE_WEB = 'web';
const SOURCE_EMAIL = 'email';
const SOURCE_CONDUIT = 'conduit';
const SOURCE_MOBILE = 'mobile';
const SOURCE_TABLET = 'tablet';
const SOURCE_FAX = 'fax';
const SOURCE_CONSOLE = 'console';
+ const SOURCE_HERALD = 'herald';
const SOURCE_LEGACY = 'legacy';
private $source;
private $params = array();
private function __construct() {
// <empty>
}
public static function newForSource($source, array $params) {
$obj = new PhabricatorContentSource();
$obj->source = $source;
$obj->params = $params;
return $obj;
}
public static function newFromSerialized($serialized) {
$dict = json_decode($serialized, true);
if (!is_array($dict)) {
$dict = array();
}
$obj = new PhabricatorContentSource();
$obj->source = idx($dict, 'source', self::SOURCE_UNKNOWN);
$obj->params = idx($dict, 'params', array());
return $obj;
}
public static function newConsoleSource() {
return self::newForSource(
PhabricatorContentSource::SOURCE_CONSOLE,
array());
}
public static function newFromRequest(AphrontRequest $request) {
return self::newForSource(
PhabricatorContentSource::SOURCE_WEB,
array(
'ip' => $request->getRemoteAddr(),
));
}
public static function newFromConduitRequest(ConduitAPIRequest $request) {
return self::newForSource(
PhabricatorContentSource::SOURCE_CONDUIT,
array());
}
public static function getSourceNameMap() {
return array(
self::SOURCE_WEB => pht('Web'),
self::SOURCE_EMAIL => pht('Email'),
self::SOURCE_CONDUIT => pht('Conduit'),
self::SOURCE_MOBILE => pht('Mobile'),
self::SOURCE_TABLET => pht('Tablet'),
self::SOURCE_FAX => pht('Fax'),
self::SOURCE_CONSOLE => pht('Console'),
self::SOURCE_LEGACY => pht('Legacy'),
- self::SOURCE_UNKNOWN => pht('Other'),
+ self::SOURCE_HERALD => pht('Herald'),
+ self::SOURCE_UNKNOWN => pht('Old World'),
);
}
public function serialize() {
return json_encode(array(
'source' => $this->getSource(),
'params' => $this->getParams(),
));
}
public function getSource() {
return $this->source;
}
public function getParams() {
return $this->params;
}
public function getParam($key, $default = null) {
return idx($this->params, $key, $default);
}
}
diff --git a/src/applications/metamta/contentsource/PhabricatorContentSourceView.php b/src/applications/metamta/contentsource/PhabricatorContentSourceView.php
index f519cf98f2..6a37e3c824 100644
--- a/src/applications/metamta/contentsource/PhabricatorContentSourceView.php
+++ b/src/applications/metamta/contentsource/PhabricatorContentSourceView.php
@@ -1,40 +1,32 @@
<?php
final class PhabricatorContentSourceView extends AphrontView {
private $contentSource;
public function setContentSource(PhabricatorContentSource $content_source) {
$this->contentSource = $content_source;
return $this;
}
public function render() {
require_celerity_resource('phabricator-content-source-view-css');
- $map = array(
- PhabricatorContentSource::SOURCE_WEB => pht('Web'),
- PhabricatorContentSource::SOURCE_CONDUIT => pht('Conduit'),
- PhabricatorContentSource::SOURCE_EMAIL => pht('Email'),
- PhabricatorContentSource::SOURCE_MOBILE => pht('Mobile'),
- PhabricatorContentSource::SOURCE_TABLET => pht('Tablet'),
- PhabricatorContentSource::SOURCE_FAX => pht('Fax'),
- PhabricatorContentSource::SOURCE_LEGACY => pht('Old World'),
- );
+ $map = PhabricatorContentSource::getSourceNameMap();
$source = $this->contentSource->getSource();
$type = idx($map, $source, null);
if (!$type) {
return null;
}
return phutil_tag(
'span',
array(
'class' => "phabricator-content-source-view",
),
"Via {$type}");
}
}
diff --git a/src/applications/pholio/editor/PholioMockEditor.php b/src/applications/pholio/editor/PholioMockEditor.php
index 25a1369bec..6b6b9c8425 100644
--- a/src/applications/pholio/editor/PholioMockEditor.php
+++ b/src/applications/pholio/editor/PholioMockEditor.php
@@ -1,471 +1,477 @@
<?php
/**
* @group pholio
*/
final class PholioMockEditor extends PhabricatorApplicationTransactionEditor {
private $newImages = array();
private function setNewImages(array $new_images) {
assert_instances_of($new_images, 'PholioImage');
$this->newImages = $new_images;
return $this;
}
private function getNewImages() {
return $this->newImages;
}
public function getTransactionTypes() {
$types = parent::getTransactionTypes();
$types[] = PhabricatorTransactions::TYPE_EDGE;
$types[] = PhabricatorTransactions::TYPE_COMMENT;
$types[] = PhabricatorTransactions::TYPE_VIEW_POLICY;
$types[] = PhabricatorTransactions::TYPE_EDIT_POLICY;
$types[] = PholioTransactionType::TYPE_NAME;
$types[] = PholioTransactionType::TYPE_DESCRIPTION;
$types[] = PholioTransactionType::TYPE_INLINE;
$types[] = PholioTransactionType::TYPE_IMAGE_FILE;
$types[] = PholioTransactionType::TYPE_IMAGE_NAME;
$types[] = PholioTransactionType::TYPE_IMAGE_DESCRIPTION;
$types[] = PholioTransactionType::TYPE_IMAGE_REPLACE;
$types[] = PholioTransactionType::TYPE_IMAGE_SEQUENCE;
return $types;
}
protected function getCustomTransactionOldValue(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PholioTransactionType::TYPE_NAME:
return $object->getName();
case PholioTransactionType::TYPE_DESCRIPTION:
return $object->getDescription();
case PholioTransactionType::TYPE_IMAGE_FILE:
$images = $object->getImages();
return mpull($images, 'getPHID');
case PholioTransactionType::TYPE_IMAGE_NAME:
$name = null;
$phid = null;
$image = $this->getImageForXaction($object, $xaction);
if ($image) {
$name = $image->getName();
$phid = $image->getPHID();
}
return array($phid => $name);
case PholioTransactionType::TYPE_IMAGE_DESCRIPTION:
$description = null;
$phid = null;
$image = $this->getImageForXaction($object, $xaction);
if ($image) {
$description = $image->getDescription();
$phid = $image->getPHID();
}
return array($phid => $description);
case PholioTransactionType::TYPE_IMAGE_REPLACE:
$raw = $xaction->getNewValue();
return $raw->getReplacesImagePHID();
case PholioTransactionType::TYPE_IMAGE_SEQUENCE:
$sequence = null;
$phid = null;
$image = $this->getImageForXaction($object, $xaction);
if ($image) {
$sequence = $image->getSequence();
$phid = $image->getPHID();
}
return array($phid => $sequence);
}
}
protected function getCustomTransactionNewValue(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PholioTransactionType::TYPE_NAME:
case PholioTransactionType::TYPE_DESCRIPTION:
case PholioTransactionType::TYPE_IMAGE_NAME:
case PholioTransactionType::TYPE_IMAGE_DESCRIPTION:
case PholioTransactionType::TYPE_IMAGE_SEQUENCE:
return $xaction->getNewValue();
case PholioTransactionType::TYPE_IMAGE_REPLACE:
$raw = $xaction->getNewValue();
return $raw->getPHID();
case PholioTransactionType::TYPE_IMAGE_FILE:
$raw_new_value = $xaction->getNewValue();
$new_value = array();
foreach ($raw_new_value as $key => $images) {
$new_value[$key] = mpull($images, 'getPHID');
}
$xaction->setNewValue($new_value);
return $this->getPHIDTransactionNewValue($xaction);
}
}
protected function extractFilePHIDsFromCustomTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PholioTransactionType::TYPE_IMAGE_FILE:
$new = $xaction->getNewValue();
$phids = array();
foreach ($new as $key => $images) {
$phids[] = mpull($images, 'getFilePHID');
}
return array_mergev($phids);
case PholioTransactionType::TYPE_IMAGE_REPLACE:
return array($xaction->getNewValue()->getFilePHID());
}
return array();
}
protected function transactionHasEffect(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PholioTransactionType::TYPE_INLINE:
return true;
}
return parent::transactionHasEffect($object, $xaction);
}
protected function shouldApplyInitialEffects(
PhabricatorLiskDAO $object,
array $xactions) {
foreach ($xactions as $xaction) {
switch ($xaction->getTransactionType()) {
case PholioTransactionType::TYPE_IMAGE_FILE:
case PholioTransactionType::TYPE_IMAGE_REPLACE:
return true;
break;
}
}
return false;
}
protected function applyInitialEffects(
PhabricatorLiskDAO $object,
array $xactions) {
$new_images = array();
foreach ($xactions as $xaction) {
switch ($xaction->getTransactionType()) {
case PholioTransactionType::TYPE_IMAGE_FILE:
$new_value = $xaction->getNewValue();
foreach ($new_value as $key => $txn_images) {
if ($key != '+') {
continue;
}
foreach ($txn_images as $image) {
$image->save();
$new_images[] = $image;
}
}
break;
case PholioTransactionType::TYPE_IMAGE_REPLACE:
$image = $xaction->getNewValue();
$image->save();
$new_images[] = $image;
break;
}
}
$this->setNewImages($new_images);
}
protected function applyCustomInternalTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PholioTransactionType::TYPE_NAME:
$object->setName($xaction->getNewValue());
if ($object->getOriginalName() === null) {
$object->setOriginalName($xaction->getNewValue());
}
break;
case PholioTransactionType::TYPE_DESCRIPTION:
$object->setDescription($xaction->getNewValue());
break;
}
}
private function getImageForXaction(
PholioMock $mock,
PhabricatorApplicationTransaction $xaction) {
$raw_new_value = $xaction->getNewValue();
$image_phid = key($raw_new_value);
$images = $mock->getImages();
foreach ($images as $image) {
if ($image->getPHID() == $image_phid) {
return $image;
}
}
return null;
}
protected function applyCustomExternalTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PholioTransactionType::TYPE_IMAGE_FILE:
$old_map = array_fuse($xaction->getOldValue());
$new_map = array_fuse($xaction->getNewValue());
$obsolete_map = array_diff_key($old_map, $new_map);
$images = $object->getImages();
foreach ($images as $seq => $image) {
if (isset($obsolete_map[$image->getPHID()])) {
$image->setIsObsolete(1);
$image->save();
unset($images[$seq]);
}
}
$object->attachImages($images);
break;
case PholioTransactionType::TYPE_IMAGE_REPLACE:
$old = $xaction->getOldValue();
$images = $object->getImages();
foreach ($images as $seq => $image) {
if ($image->getPHID() == $old) {
$image->setIsObsolete(1);
$image->save();
unset($images[$seq]);
}
}
$object->attachImages($images);
break;
case PholioTransactionType::TYPE_IMAGE_NAME:
$image = $this->getImageForXaction($object, $xaction);
$value = (string) head($xaction->getNewValue());
$image->setName($value);
$image->save();
break;
case PholioTransactionType::TYPE_IMAGE_DESCRIPTION:
$image = $this->getImageForXaction($object, $xaction);
$value = (string) head($xaction->getNewValue());
$image->setDescription($value);
$image->save();
break;
case PholioTransactionType::TYPE_IMAGE_SEQUENCE:
$image = $this->getImageForXaction($object, $xaction);
$value = (int) head($xaction->getNewValue());
$image->setSequence($value);
$image->save();
break;
}
}
protected function applyFinalEffects(
PhabricatorLiskDAO $object,
array $xactions) {
$images = $this->getNewImages();
foreach ($images as $image) {
$image->setMockID($object->getID());
$image->save();
}
return $xactions;
}
protected function mergeTransactions(
PhabricatorApplicationTransaction $u,
PhabricatorApplicationTransaction $v) {
$type = $u->getTransactionType();
switch ($type) {
case PholioTransactionType::TYPE_NAME:
case PholioTransactionType::TYPE_DESCRIPTION:
return $v;
case PholioTransactionType::TYPE_IMAGE_REPLACE:
$u_img = $u->getNewValue();
$v_img = $v->getNewValue();
if ($u_img->getReplacesImagePHID() == $v_img->getReplacesImagePHID()) {
return $v;
}
break;
case PholioTransactionType::TYPE_IMAGE_FILE:
return $this->mergePHIDOrEdgeTransactions($u, $v);
case PholioTransactionType::TYPE_IMAGE_NAME:
case PholioTransactionType::TYPE_IMAGE_DESCRIPTION:
case PholioTransactionType::TYPE_IMAGE_SEQUENCE:
$raw_new_value_u = $u->getNewValue();
$raw_new_value_v = $v->getNewValue();
$phid_u = key($raw_new_value_u);
$phid_v = key($raw_new_value_v);
if ($phid_u == $phid_v) {
return $v;
}
break;
}
return parent::mergeTransactions($u, $v);
}
protected function shouldSendMail(
PhabricatorLiskDAO $object,
array $xactions) {
return true;
}
protected function buildReplyHandler(PhabricatorLiskDAO $object) {
return id(new PholioReplyHandler())
->setMailReceiver($object);
}
protected function buildMailTemplate(PhabricatorLiskDAO $object) {
$id = $object->getID();
$name = $object->getName();
$original_name = $object->getOriginalName();
return id(new PhabricatorMetaMTAMail())
->setSubject("M{$id}: {$name}")
->addHeader('Thread-Topic', "M{$id}: {$original_name}");
}
protected function getMailTo(PhabricatorLiskDAO $object) {
return array(
$object->getAuthorPHID(),
$this->requireActor()->getPHID(),
);
}
protected function buildMailBody(
PhabricatorLiskDAO $object,
array $xactions) {
$body = new PhabricatorMetaMTAMailBody();
$headers = array();
$comments = array();
$inline_comments = array();
foreach ($xactions as $xaction) {
if ($xaction->shouldHide()) {
continue;
}
$comment = $xaction->getComment();
switch ($xaction->getTransactionType()) {
case PholioTransactionType::TYPE_INLINE:
if ($comment && strlen($comment->getContent())) {
$inline_comments[] = $comment;
}
break;
case PhabricatorTransactions::TYPE_COMMENT:
if ($comment && strlen($comment->getContent())) {
$comments[] = $comment->getContent();
}
// fallthrough
default:
$headers[] = id(clone $xaction)
->setRenderingTarget('text')
->getTitle();
break;
}
}
$body->addRawSection(implode("\n", $headers));
foreach ($comments as $comment) {
$body->addRawSection($comment);
}
if ($inline_comments) {
$body->addRawSection(pht('INLINE COMMENTS'));
foreach ($inline_comments as $comment) {
$text = pht(
'Image %d: %s',
$comment->getImageID(),
$comment->getContent());
$body->addRawSection($text);
}
}
$body->addTextSection(
pht('MOCK DETAIL'),
PhabricatorEnv::getProductionURI('/M'.$object->getID()));
return $body;
}
protected function getMailSubjectPrefix() {
return PhabricatorEnv::getEnvConfig('metamta.pholio.subject-prefix');
}
protected function shouldPublishFeedStory(
PhabricatorLiskDAO $object,
array $xactions) {
return true;
}
protected function supportsSearch() {
return true;
}
- protected function supportsHerald() {
+ protected function shouldApplyHeraldRules(
+ PhabricatorLiskDAO $object,
+ array $xactions) {
return true;
}
protected function buildHeraldAdapter(
PhabricatorLiskDAO $object,
array $xactions) {
return id(new HeraldPholioMockAdapter())
->setMock($object);
}
protected function didApplyHeraldRules(
PhabricatorLiskDAO $object,
HeraldAdapter $adapter,
HeraldTranscript $transcript) {
+ // TODO: Convert this to be transaction-based.
+
$cc_phids = $adapter->getCcPHIDs();
if ($cc_phids) {
id(new PhabricatorSubscriptionsEditor())
->setObject($object)
->setActor($this->requireActor())
->subscribeImplicit($cc_phids)
->save();
}
+
+ return array();
}
protected function sortTransactions(array $xactions) {
$head = array();
$tail = array();
// Move inline comments to the end, so the comments precede them.
foreach ($xactions as $xaction) {
$type = $xaction->getTransactionType();
if ($type == PholioTransactionType::TYPE_INLINE) {
$tail[] = $xaction;
} else {
$head[] = $xaction;
}
}
return array_values(array_merge($head, $tail));
}
protected function shouldImplyCC(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PholioTransactionType::TYPE_INLINE:
return true;
}
return parent::shouldImplyCC($object, $xaction);
}
}
diff --git a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php
index 36d1e35009..12da96f964 100644
--- a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php
+++ b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php
@@ -1,1970 +1,2053 @@
<?php
/**
* @task mail Sending Mail
* @task feed Publishing Feed Stories
* @task search Search Index
* @task files Integration with Files
*/
abstract class PhabricatorApplicationTransactionEditor
extends PhabricatorEditor {
private $contentSource;
private $object;
private $xactions;
private $isNewObject;
private $mentionedPHIDs;
private $continueOnNoEffect;
private $continueOnMissingFields;
private $parentMessageID;
private $heraldAdapter;
private $heraldTranscript;
private $subscribers;
private $isPreview;
+ private $isHeraldEditor;
+ private $actingAsPHID;
+
+ 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;
}
protected function getIsNewObject() {
return $this->isNewObject;
}
protected function getMentionedPHIDs() {
return $this->mentionedPHIDs;
}
public function setIsPreview($is_preview) {
$this->isPreview = $is_preview;
return $this;
}
public function getIsPreview() {
return $this->isPreview;
}
+ public function setIsHeraldEditor($is_herald_editor) {
+ $this->isHeraldEditor = $is_herald_editor;
+ return $this;
+ }
+
+ public function getIsHeraldEditor() {
+ return $this->isHeraldEditor;
+ }
+
public function getTransactionTypes() {
$types = array();
if ($this->object instanceof PhabricatorSubscribableInterface) {
$types[] = PhabricatorTransactions::TYPE_SUBSCRIBERS;
}
if ($this->object instanceof PhabricatorCustomFieldInterface) {
$types[] = PhabricatorTransactions::TYPE_CUSTOMFIELD;
}
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) {
switch ($xaction->getTransactionType()) {
case PhabricatorTransactions::TYPE_SUBSCRIBERS:
return array_values($this->subscribers);
case PhabricatorTransactions::TYPE_VIEW_POLICY:
return $object->getViewPolicy();
case PhabricatorTransactions::TYPE_EDIT_POLICY:
return $object->getEditPolicy();
case PhabricatorTransactions::TYPE_JOIN_POLICY:
return $object->getJoinPolicy();
case PhabricatorTransactions::TYPE_EDGE:
$edge_type = $xaction->getMetadataValue('edge:type');
if (!$edge_type) {
throw new Exception("Edge transaction has no 'edge:type'!");
}
$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) {
switch ($xaction->getTransactionType()) {
case PhabricatorTransactions::TYPE_SUBSCRIBERS:
return $this->getPHIDTransactionNewValue($xaction);
case PhabricatorTransactions::TYPE_VIEW_POLICY:
case PhabricatorTransactions::TYPE_EDIT_POLICY:
case PhabricatorTransactions::TYPE_JOIN_POLICY:
return $xaction->getNewValue();
case PhabricatorTransactions::TYPE_EDGE:
return $this->getEdgeTransactionNewValue($xaction);
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("Capability not supported!");
}
protected function getCustomTransactionNewValue(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
throw new Exception("Capability not supported!");
}
protected function transactionHasEffect(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorTransactions::TYPE_COMMENT:
return $xaction->hasComment();
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;
}
return ($xaction->getOldValue() !== $xaction->getNewValue());
}
protected function shouldApplyInitialEffects(
PhabricatorLiskDAO $object,
array $xactions) {
return false;
}
protected function applyInitialEffects(
PhabricatorLiskDAO $object,
array $xactions) {
throw new Exception('Not implemented.');
}
private function applyInternalEffects(
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_CUSTOMFIELD:
$field = $this->getCustomFieldForTransaction($object, $xaction);
return $field->applyApplicationTransactionInternalEffects($xaction);
}
return $this->applyCustomInternalTransaction($object, $xaction);
}
private function applyExternalEffects(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
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;
break;
case PhabricatorTransactions::TYPE_EDGE:
$old = $xaction->getOldValue();
$new = $xaction->getNewValue();
$src = $object->getPHID();
$type = $xaction->getMetadataValue('edge:type');
foreach ($new as $dst_phid => $edge) {
$new[$dst_phid]['src'] = $src;
}
$editor = id(new PhabricatorEdgeEditor())
->setActor($this->getActor());
foreach ($old as $dst_phid => $edge) {
if (!empty($new[$dst_phid])) {
if ($old[$dst_phid]['data'] === $new[$dst_phid]['data']) {
continue;
}
}
$editor->removeEdge($src, $type, $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, $type, $dst_phid, $data);
}
$editor->save();
break;
case PhabricatorTransactions::TYPE_CUSTOMFIELD:
$field = $this->getCustomFieldForTransaction($object, $xaction);
return $field->applyApplicationTransactionExternalEffects($xaction);
}
return $this->applyCustomExternalTransaction($object, $xaction);
}
protected function applyCustomInternalTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
$type = $xaction->getTransactionType();
throw new Exception(
"Transaction type '{$type}' is missing an internal apply ".
"implementation!");
}
protected function applyCustomExternalTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
$type = $xaction->getTransactionType();
throw new Exception(
"Transaction type '{$type}' is missing an external apply ".
"implementation!");
}
/**
* 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);
- $xaction->setEditPolicy($actor->getPHID());
- $xaction->setAuthorPHID($actor->getPHID());
+ if ($actor->isOmnipotent()) {
+ $xaction->setEditPolicy(PhabricatorPolicies::POLICY_NOONE);
+ } else {
+ $xaction->setEditPolicy($actor->getPHID());
+ }
+
+ $xaction->setAuthorPHID($this->getActingAsPHID());
$xaction->setContentSource($this->getContentSource());
$xaction->attachViewer($actor);
$xaction->attachObject($object);
if ($object->getPHID()) {
$xaction->setObjectPHID($object->getPHID());
}
return $xaction;
}
protected function applyFinalEffects(
PhabricatorLiskDAO $object,
array $xactions) {
return $xactions;
}
public function setContentSource(PhabricatorContentSource $content_source) {
$this->contentSource = $content_source;
return $this;
}
public function setContentSourceFromRequest(AphrontRequest $request) {
return $this->setContentSource(
PhabricatorContentSource::newFromRequest($request));
}
public function getContentSource() {
return $this->contentSource;
}
final public function applyTransactions(
PhabricatorLiskDAO $object,
array $xactions) {
$this->object = $object;
$this->xactions = $xactions;
$this->isNewObject = ($object->getPHID() === null);
$this->validateEditParameters($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);
}
$is_preview = $this->getIsPreview();
$read_locking = false;
$transaction_open = false;
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 = 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);
}
$file_phids = $this->extractFilePHIDs($object, $xactions);
if ($object->getID()) {
foreach ($xactions as $xaction) {
// If any of the transactions require a read lock, hold one and
// reload the object. We need to do this fairly early so that the
// call to `adjustTransactionValues()` (which populates old values)
// is based on the synchronized state of the object, which may differ
// from the state when it was originally loaded.
if ($this->shouldReadLock($object, $xaction)) {
$object->openTransaction();
$object->beginReadLocking();
$transaction_open = true;
$read_locking = true;
$object->reload();
break;
}
}
}
if ($this->shouldApplyInitialEffects($object, $xactions)) {
if (!$transaction_open) {
$object->openTransaction();
$transaction_open = true;
}
}
}
if ($this->shouldApplyInitialEffects($object, $xactions)) {
$this->applyInitialEffects($object, $xactions);
}
foreach ($xactions as $xaction) {
$this->adjustTransactionValues($object, $xaction);
}
$xactions = $this->filterTransactions($object, $xactions);
if (!$xactions) {
if ($read_locking) {
$object->endReadLocking();
$read_locking = false;
}
if ($transaction_open) {
$object->killTransaction();
$transaction_open = false;
}
return array();
}
// Now that we've merged, filtered, and combined transactions, check for
// required capabilities.
foreach ($xactions as $xaction) {
$this->requireCapabilities($object, $xaction);
}
$xactions = $this->sortTransactions($xactions);
if ($is_preview) {
$this->loadHandles($xactions);
return $xactions;
}
$comment_editor = id(new PhabricatorApplicationTransactionCommentEditor())
->setActor($actor)
->setContentSource($this->getContentSource());
if (!$transaction_open) {
$object->openTransaction();
}
foreach ($xactions as $xaction) {
$this->applyInternalEffects($object, $xaction);
}
$object->save();
foreach ($xactions as $xaction) {
$xaction->setObjectPHID($object->getPHID());
if ($xaction->getComment()) {
$xaction->setPHID($xaction->generatePHID());
$comment_editor->applyEdit($xaction, $xaction->getComment());
} else {
$xaction->save();
}
}
if ($file_phids) {
$this->attachFiles($object, $file_phids);
}
foreach ($xactions as $xaction) {
$this->applyExternalEffects($object, $xaction);
}
- if ($this->supportsHerald()) {
- $this->applyHeraldRules($object, $xactions);
- }
-
$xactions = $this->applyFinalEffects($object, $xactions);
if ($read_locking) {
$object->endReadLocking();
$read_locking = false;
}
$object->saveTransaction();
+ // 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->shouldApplyHeraldRules($object, $xactions)) {
+ // We are not the Herald editor, so try to apply Herald rules.
+ $herald_xactions = $this->applyHeraldRules($object, $xactions);
+
+ if ($herald_xactions) {
+ // 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 PhabricatorApplicationHerald())->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(
+ PhabricatorContentSource::SOURCE_HERALD,
+ array());
+
+ $herald_editor = newv(get_class($this), array())
+ ->setContinueOnNoEffect(true)
+ ->setContinueOnMissingFields(true)
+ ->setParentMessageID($this->getParentMessageID())
+ ->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);
+ }
+ }
+
$this->loadHandles($xactions);
$mail = null;
if ($this->shouldSendMail($object, $xactions)) {
$mail = $this->sendMail($object, $xactions);
}
if ($this->supportsSearch()) {
id(new PhabricatorSearchIndexer())
->queueDocumentForIndexing($object->getPHID());
}
if ($this->shouldPublishFeedStory($object, $xactions)) {
$mailed = array();
if ($mail) {
$mailed = $mail->buildRecipientList();
}
$this->publishFeedStory(
$object,
$xactions,
$mailed);
}
$this->didApplyTransactions($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 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);
}
return $xactions;
}
protected function didApplyTransactions(array $xactions) {
// Hook for subclasses.
return;
}
/**
* Determine if the editor should hold a read lock on the object while
* applying a transaction.
*
* If the editor does not hold a lock, two editors may read an object at the
* same time, then apply their changes without any synchronization. For most
* transactions, this does not matter much. However, it is important for some
* transactions. For example, if an object has a transaction count on it, both
* editors may read the object with `count = 23`, then independently update it
* and save the object with `count = 24` twice. This will produce the wrong
* state: the object really has 25 transactions, but the count is only 24.
*
* Generally, transactions fall into one of four buckets:
*
* - Append operations: Actions like adding a comment to an object purely
* add information to its state, and do not depend on the current object
* state in any way. These transactions never need to hold locks.
* - Overwrite operations: Actions like changing the title or description
* of an object replace the current value with a new value, so the end
* state is consistent without a lock. We currently do not lock these
* transactions, although we may in the future.
* - Edge operations: Edge and subscription operations have internal
* synchronization which limits the damage race conditions can cause.
* We do not currently lock these transactions, although we may in the
* future.
* - Update operations: Actions like incrementing a count on an object.
* These operations generally should use locks, unless it is not
* important that the state remain consistent in the presence of races.
*
* @param PhabricatorLiskDAO Object being updated.
* @param PhabricatorApplicationTransaction Transaction being applied.
* @return bool True to synchronize the edit with a lock.
*/
protected function shouldReadLock(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
return false;
}
private function loadHandles(array $xactions) {
$phids = array();
foreach ($xactions as $key => $xaction) {
$phids[$key] = $xaction->getRequiredHandlePHIDs();
}
$handles = array();
$merged = array_mergev($phids);
if ($merged) {
$handles = id(new PhabricatorHandleQuery())
->setViewer($this->requireActor())
->withPHIDs($merged)
->execute();
}
foreach ($xactions as $key => $xaction) {
$xaction->setHandles(array_select_keys($handles, $phids[$key]));
}
}
private function loadSubscribers(PhabricatorLiskDAO $object) {
if ($object->getPHID() &&
($object instanceof PhabricatorSubscribableInterface)) {
$subs = PhabricatorSubscribersQuery::loadSubscribersForPHID(
$object->getPHID());
$this->subscribers = array_fuse($subs);
} else {
$this->subscribers = array();
}
}
private function validateEditParameters(
PhabricatorLiskDAO $object,
array $xactions) {
if (!$this->getContentSource()) {
throw new Exception(
"Call setContentSource() before applyTransactions()!");
}
// Do a bunch of sanity checks that the incoming transactions are fresh.
// They should be unsaved and have only "transactionType" and "newValue"
// set.
$types = array_fill_keys($this->getTransactionTypes(), true);
assert_instances_of($xactions, 'PhabricatorApplicationTransaction');
foreach ($xactions as $xaction) {
if ($xaction->getPHID() || $xaction->getID()) {
throw new PhabricatorApplicationTransactionStructureException(
$xaction,
pht(
"You can not apply transactions which already have IDs/PHIDs!"));
}
if ($xaction->getObjectPHID()) {
throw new PhabricatorApplicationTransactionStructureException(
$xaction,
pht(
"You can not apply transactions which already have objectPHIDs!"));
}
if ($xaction->getAuthorPHID()) {
throw new PhabricatorApplicationTransactionStructureException(
$xaction,
pht(
'You can not apply transactions which already have authorPHIDs!'));
}
if ($xaction->getCommentPHID()) {
throw new PhabricatorApplicationTransactionStructureException(
$xaction,
pht(
'You can not apply transactions which already have '.
'commentPHIDs!'));
}
if ($xaction->getCommentVersion() !== 0) {
throw new PhabricatorApplicationTransactionStructureException(
$xaction,
pht(
'You can not apply transactions which already have '.
'commentVersions!'));
}
$expect_value = !$xaction->shouldGenerateOldValue();
$has_value = $xaction->hasOldValue();
if ($expect_value && !$has_value) {
throw new PhabricatorApplicationTransactionStructureException(
$xaction,
pht(
'This transaction is supposed to have an oldValue set, but '.
'it does not!'));
}
if ($has_value && !$expect_value) {
throw new PhabricatorApplicationTransactionStructureException(
$xaction,
pht(
'This transaction should generate its oldValue automatically, '.
'but has already had one set!'));
}
$type = $xaction->getTransactionType();
if (empty($types[$type])) {
throw new PhabricatorApplicationTransactionStructureException(
$xaction,
pht(
'Transaction has type "%s", but that transaction type is not '.
'supported by this editor (%s).',
$type,
get_class($this)));
}
}
// The actor must have permission to view and edit the object.
$actor = $this->requireActor();
PhabricatorPolicyFilter::requireCapability(
$actor,
$object,
PhabricatorPolicyCapability::CAN_VIEW);
}
protected function requireCapabilities(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorTransactions::TYPE_EDIT_POLICY:
// You must have the edit capability to alter the edit policy of an
// object. For other default transaction types, we don't enforce
// anything for the moment.
PhabricatorPolicyFilter::requireCapability(
$this->requireActor(),
$object,
PhabricatorPolicyCapability::CAN_EDIT);
break;
}
}
private function buildMentionTransaction(
PhabricatorLiskDAO $object,
array $xactions,
array $blocks) {
if (!($object instanceof PhabricatorSubscribableInterface)) {
return null;
}
$texts = array_mergev($blocks);
$phids = PhabricatorMarkupEngine::extractPHIDsFromMentions($texts);
$this->mentionedPHIDs = $phids;
if ($object->getPHID()) {
// Don't try to subscribe already-subscribed mentions: we want to generate
// a dialog about an action having no effect if the user explicitly adds
// existing CCs, but not if they merely mention existing subscribers.
$phids = array_diff($phids, $this->subscribers);
}
foreach ($phids as $key => $phid) {
if ($object->isAutomaticallySubscribed($phid)) {
unset($phids[$key]);
}
}
$phids = array_values($phids);
if (!$phids) {
return null;
}
$xaction = newv(get_class(head($xactions)), array());
$xaction->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS);
$xaction->setNewValue(array('+' => $phids));
return $xaction;
}
protected function getRemarkupBlocksFromTransaction(
PhabricatorApplicationTransaction $transaction) {
return $transaction->getRemarkupBlocks();
}
protected function mergeTransactions(
PhabricatorApplicationTransaction $u,
PhabricatorApplicationTransaction $v) {
$type = $u->getTransactionType();
switch ($type) {
case PhabricatorTransactions::TYPE_SUBSCRIBERS:
return $this->mergePHIDOrEdgeTransactions($u, $v);
case PhabricatorTransactions::TYPE_EDGE:
$u_type = $u->getMetadataValue('edge:type');
$v_type = $v->getMetadataValue('edge:type');
if ($u_type == $v_type) {
return $this->mergePHIDOrEdgeTransactions($u, $v);
}
return null;
}
// By default, do not merge the transactions.
return null;
}
/**
* Optionally expand transactions which imply other effects. For example,
* resigning from a revision in Differential implies removing yourself as
* a reviewer.
*/
private function expandTransactions(
PhabricatorLiskDAO $object,
array $xactions) {
$results = array();
foreach ($xactions as $xaction) {
foreach ($this->expandTransaction($object, $xaction) as $expanded) {
$results[] = $expanded;
}
}
return $results;
}
protected function expandTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
return array($xaction);
}
private function expandSupportTransactions(
PhabricatorLiskDAO $object,
array $xactions) {
$this->loadSubscribers($object);
$xactions = $this->applyImplicitCC($object, $xactions);
$blocks = array();
foreach ($xactions as $key => $xaction) {
$blocks[$key] = $this->getRemarkupBlocksFromTransaction($xaction);
}
$mention_xaction = $this->buildMentionTransaction(
$object,
$xactions,
$blocks);
if ($mention_xaction) {
$xactions[] = $mention_xaction;
}
// TODO: For now, this is just a placeholder.
$engine = PhabricatorMarkupEngine::getEngine('extract');
$block_xactions = $this->expandRemarkupBlockTransactions(
$object,
$xactions,
$blocks,
$engine);
foreach ($block_xactions as $xaction) {
$xactions[] = $xaction;
}
return $xactions;
}
private function expandRemarkupBlockTransactions(
PhabricatorLiskDAO $object,
array $xactions,
$blocks,
PhutilMarkupEngine $engine) {
return $this->expandCustomRemarkupBlockTransactions(
$object,
$xactions,
$blocks,
$engine);
}
protected function expandCustomRemarkupBlockTransactions(
PhabricatorLiskDAO $object,
array $xactions,
$blocks,
PhutilMarkupEngine $engine) {
return array();
}
/**
* Attempt to combine similar transactions into a smaller number of total
* transactions. For example, two transactions which edit the title of an
* object can be merged into a single edit.
*/
private function combineTransactions(array $xactions) {
$stray_comments = array();
$result = array();
$types = array();
foreach ($xactions as $key => $xaction) {
$type = $xaction->getTransactionType();
if (isset($types[$type])) {
foreach ($types[$type] as $other_key) {
$merged = $this->mergeTransactions($result[$other_key], $xaction);
if ($merged) {
$result[$other_key] = $merged;
if ($xaction->getComment() &&
($xaction->getComment() !== $merged->getComment())) {
$stray_comments[] = $xaction->getComment();
}
if ($result[$other_key]->getComment() &&
($result[$other_key]->getComment() !== $merged->getComment())) {
$stray_comments[] = $result[$other_key]->getComment();
}
// Move on to the next transaction.
continue 2;
}
}
}
$result[$key] = $xaction;
$types[$type][] = $key;
}
// If we merged any comments away, restore them.
foreach ($stray_comments as $comment) {
$xaction = newv(get_class(head($result)), array());
$xaction->setTransactionType(PhabricatorTransactions::TYPE_COMMENT);
$xaction->setComment($comment);
$result[] = $xaction;
}
return array_values($result);
}
protected function mergePHIDOrEdgeTransactions(
PhabricatorApplicationTransaction $u,
PhabricatorApplicationTransaction $v) {
$result = $u->getNewValue();
foreach ($v->getNewValue() as $key => $value) {
if ($u->getTransactionType() == PhabricatorTransactions::TYPE_EDGE) {
if (empty($result[$key])) {
$result[$key] = $value;
} else {
// We're merging two lists of edge adds, sets, or removes. Merge
// them by merging individual PHIDs within them.
$merged = $result[$key];
foreach ($value as $dst => $v_spec) {
if (empty($merged[$dst])) {
$merged[$dst] = $v_spec;
} else {
// Two transactions are trying to perform the same operation on
// the same edge. Normalize the edge data and then merge it. This
// allows transactions to specify how data merges execute in a
// precise way.
$u_spec = $merged[$dst];
if (!is_array($u_spec)) {
$u_spec = array('dst' => $u_spec);
}
if (!is_array($v_spec)) {
$v_spec = array('dst' => $v_spec);
}
$ux_data = idx($u_spec, 'data', array());
$vx_data = idx($v_spec, 'data', array());
$merged_data = $this->mergeEdgeData(
$u->getMetadataValue('edge:type'),
$ux_data,
$vx_data);
$u_spec['data'] = $merged_data;
$merged[$dst] = $u_spec;
}
}
$result[$key] = $merged;
}
} else {
$result[$key] = array_merge($value, idx($result, $key, array()));
}
}
$u->setNewValue($result);
// When combining an "ignore" transaction with a normal transaction, make
// sure we don't propagate the "ignore" flag.
if (!$v->getIgnoreOnNoEffect()) {
$u->setIgnoreOnNoEffect(false);
}
return $u;
}
protected function mergeEdgeData($type, array $u, array $v) {
return $v + $u;
}
protected function getPHIDTransactionNewValue(
PhabricatorApplicationTransaction $xaction) {
$old = array_fuse($xaction->getOldValue());
$new = $xaction->getNewValue();
$new_add = idx($new, '+', array());
unset($new['+']);
$new_rem = idx($new, '-', array());
unset($new['-']);
$new_set = idx($new, '=', null);
if ($new_set !== null) {
$new_set = array_fuse($new_set);
}
unset($new['=']);
if ($new) {
throw new Exception(
"Invalid 'new' value for PHID transaction. Value should contain only ".
"keys '+' (add PHIDs), '-' (remove PHIDs) and '=' (set PHIDS).");
}
$result = array();
foreach ($old as $phid) {
if ($new_set !== null && empty($new_set[$phid])) {
continue;
}
$result[$phid] = $phid;
}
if ($new_set !== null) {
foreach ($new_set as $phid) {
$result[$phid] = $phid;
}
}
foreach ($new_add as $phid) {
$result[$phid] = $phid;
}
foreach ($new_rem as $phid) {
unset($result[$phid]);
}
return array_values($result);
}
protected function getEdgeTransactionNewValue(
PhabricatorApplicationTransaction $xaction) {
$new = $xaction->getNewValue();
$new_add = idx($new, '+', array());
unset($new['+']);
$new_rem = idx($new, '-', array());
unset($new['-']);
$new_set = idx($new, '=', null);
unset($new['=']);
if ($new) {
throw new Exception(
"Invalid 'new' value for Edge transaction. Value should contain only ".
"keys '+' (add edges), '-' (remove edges) and '=' (set edges).");
}
$old = $xaction->getOldValue();
$lists = array($new_set, $new_add, $new_rem);
foreach ($lists as $list) {
$this->checkEdgeList($list);
}
$result = array();
foreach ($old as $dst_phid => $edge) {
if ($new_set !== null && empty($new_set[$dst_phid])) {
continue;
}
$result[$dst_phid] = $this->normalizeEdgeTransactionValue(
$xaction,
$edge,
$dst_phid);
}
if ($new_set !== null) {
foreach ($new_set as $dst_phid => $edge) {
$result[$dst_phid] = $this->normalizeEdgeTransactionValue(
$xaction,
$edge,
$dst_phid);
}
}
foreach ($new_add as $dst_phid => $edge) {
$result[$dst_phid] = $this->normalizeEdgeTransactionValue(
$xaction,
$edge,
$dst_phid);
}
foreach ($new_rem as $dst_phid => $edge) {
unset($result[$dst_phid]);
}
return $result;
}
private function checkEdgeList($list) {
if (!$list) {
return;
}
foreach ($list as $key => $item) {
if (phid_get_type($key) === PhabricatorPHIDConstants::PHID_TYPE_UNKNOWN) {
throw new Exception(
"Edge transactions must have destination PHIDs as in edge ".
"lists (found key '{$key}').");
}
if (!is_array($item) && $item !== $key) {
throw new Exception(
"Edge transactions must have PHIDs or edge specs as values ".
"(found value '{$item}').");
}
}
}
private function normalizeEdgeTransactionValue(
PhabricatorApplicationTransaction $xaction,
$edge,
$dst_phid) {
if (!is_array($edge)) {
if ($edge != $dst_phid) {
throw new Exception(
pht(
'Transaction edge data must either be the edge PHID or an edge '.
'specification dictionary.'));
}
$edge = array();
} else {
foreach ($edge as $key => $value) {
switch ($key) {
case 'src':
case 'dst':
case 'type':
case 'data':
case 'dateCreated':
case 'dateModified':
case 'seq':
case 'dataID':
break;
default:
throw new Exception(
pht(
'Transaction edge specification contains unexpected key '.
'"%s".',
$key));
}
}
}
$edge['dst'] = $dst_phid;
$edge_type = $xaction->getMetadataValue('edge:type');
if (empty($edge['type'])) {
$edge['type'] = $edge_type;
} else {
if ($edge['type'] != $edge_type) {
$this_type = $edge['type'];
throw new Exception(
"Edge transaction includes edge of type '{$this_type}', but ".
"transaction is of type '{$edge_type}'. Each edge transaction must ".
"alter edges of only one type.");
}
}
if (!isset($edge['data'])) {
$edge['data'] = array();
}
return $edge;
}
protected function sortTransactions(array $xactions) {
$head = array();
$tail = array();
// Move bare comments to the end, so the actions precede them.
foreach ($xactions as $xaction) {
$type = $xaction->getTransactionType();
if ($type == PhabricatorTransactions::TYPE_COMMENT) {
$tail[] = $xaction;
} else {
$head[] = $xaction;
}
}
return array_values(array_merge($head, $tail));
}
protected function filterTransactions(
PhabricatorLiskDAO $object,
array $xactions) {
$type_comment = PhabricatorTransactions::TYPE_COMMENT;
$no_effect = array();
$has_comment = false;
$any_effect = false;
foreach ($xactions as $key => $xaction) {
if ($this->transactionHasEffect($object, $xaction)) {
if ($xaction->getTransactionType() != $type_comment) {
$any_effect = true;
}
} else if ($xaction->getIgnoreOnNoEffect()) {
unset($xactions[$key]);
} else {
$no_effect[$key] = $xaction;
}
if ($xaction->hasComment()) {
$has_comment = true;
}
}
if (!$no_effect) {
return $xactions;
}
if (!$this->getContinueOnNoEffect() && !$this->getIsPreview()) {
throw new PhabricatorApplicationTransactionNoEffectException(
$no_effect,
$any_effect,
$has_comment);
}
if (!$any_effect && !$has_comment) {
// If we only have empty comment transactions, just drop them all.
return array();
}
foreach ($no_effect as $key => $xaction) {
if ($xaction->getComment()) {
$xaction->setTransactionType($type_comment);
$xaction->setOldValue(null);
$xaction->setNewValue(null);
} else {
unset($xactions[$key]);
}
}
return $xactions;
}
/**
* Hook for validating transactions. This callback will be invoked for each
* available transaction type, even if an edit does not apply any transactions
* of that type. This allows you to raise exceptions when required fields are
* missing, by detecting that the object has no field value and there is no
* transaction which sets one.
*
* @param PhabricatorLiskDAO Object being edited.
* @param string Transaction type to validate.
* @param list<PhabricatorApplicationTransaction> Transactions of given type,
* which may be empty if the edit does not apply any transactions of the
* given type.
* @return list<PhabricatorApplicationTransactionValidationError> List of
* validation errors.
*/
protected function validateTransaction(
PhabricatorLiskDAO $object,
$type,
array $xactions) {
$errors = array();
switch ($type) {
case PhabricatorTransactions::TYPE_EDIT_POLICY:
// Make sure the user isn't editing away their ability to edit this
// object.
foreach ($xactions as $xaction) {
try {
PhabricatorPolicyFilter::requireCapabilityWithForcedPolicy(
$this->requireActor(),
$object,
PhabricatorPolicyCapability::CAN_EDIT,
$xaction->getNewValue());
} catch (PhabricatorPolicyException $ex) {
$errors[] = array(
new PhabricatorApplicationTransactionValidationError(
$type,
pht('Invalid'),
pht(
'You can not select this edit policy, because you would '.
'no longer be able to edit the object.'),
$xaction),
);
}
}
break;
case PhabricatorTransactions::TYPE_CUSTOMFIELD:
$groups = array();
foreach ($xactions as $xaction) {
$groups[$xaction->getMetadataValue('customfield:key')][] = $xaction;
}
$field_list = PhabricatorCustomField::getObjectFields(
$object,
PhabricatorCustomField::ROLE_EDIT);
$role_xactions = PhabricatorCustomField::ROLE_APPLICATIONTRANSACTIONS;
foreach ($field_list->getFields() as $field) {
if (!$field->shouldEnableForRole($role_xactions)) {
continue;
}
$errors[] = $field->validateApplicationTransactions(
$this,
$type,
idx($groups, $field->getFieldKey(), array()));
}
break;
}
return array_mergev($errors);
}
/**
* Check for a missing text field.
*
* A text field is missing if the object has no value and there are no
* transactions which set a value, or if the transactions remove the value.
* This method is intended to make implementing @{method:validateTransaction}
* more convenient:
*
* $missing = $this->validateIsEmptyTextField(
* $object->getName(),
* $xactions);
*
* This will return `true` if the net effect of the object and transactions
* is an empty field.
*
* @param wild Current field value.
* @param list<PhabricatorApplicationTransaction> Transactions editing the
* field.
* @return bool True if the field will be an empty text field after edits.
*/
protected function validateIsEmptyTextField($field_value, array $xactions) {
if (strlen($field_value) && empty($xactions)) {
return false;
}
if ($xactions && strlen(last($xactions)->getNewValue())) {
return false;
}
return true;
}
/* -( Implicit CCs )------------------------------------------------------- */
/**
* When a user interacts with an object, we might want to add them to CC.
*/
final public function applyImplicitCC(
PhabricatorLiskDAO $object,
array $xactions) {
if (!($object instanceof PhabricatorSubscribableInterface)) {
// If the object isn't subscribable, we can't CC them.
return $xactions;
}
$actor_phid = $this->requireActor()->getPHID();
if ($object->isAutomaticallySubscribed($actor_phid)) {
// If they're auto-subscribed, don't CC them.
return $xactions;
}
$should_cc = false;
foreach ($xactions as $xaction) {
if ($this->shouldImplyCC($object, $xaction)) {
$should_cc = true;
break;
}
}
if (!$should_cc) {
// Only some types of actions imply a CC (like adding a comment).
return $xactions;
}
if ($object->getPHID()) {
if (isset($this->subscribers[$actor_phid])) {
// If the user is already subscribed, don't implicitly CC them.
return $xactions;
}
$unsub = PhabricatorEdgeQuery::loadDestinationPHIDs(
$object->getPHID(),
PhabricatorEdgeConfig::TYPE_OBJECT_HAS_UNSUBSCRIBER);
$unsub = array_fuse($unsub);
if (isset($unsub[$actor_phid])) {
// If the user has previously unsubscribed from this object explicitly,
// don't implicitly CC them.
return $xactions;
}
}
$xaction = newv(get_class(head($xactions)), array());
$xaction->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS);
$xaction->setNewValue(array('+' => array($actor_phid)));
array_unshift($xactions, $xaction);
return $xactions;
}
protected function shouldImplyCC(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
return $xaction->isCommentTransaction();
}
/* -( Sending Mail )------------------------------------------------------- */
/**
* @task mail
*/
protected function shouldSendMail(
PhabricatorLiskDAO $object,
array $xactions) {
return false;
}
/**
* @task mail
*/
protected function sendMail(
PhabricatorLiskDAO $object,
array $xactions) {
$email_to = array_filter(array_unique($this->getMailTo($object)));
$email_cc = array_filter(array_unique($this->getMailCC($object)));
$phids = array_merge($email_to, $email_cc);
$handles = id(new PhabricatorHandleQuery())
->setViewer($this->requireActor())
->withPHIDs($phids)
->execute();
$template = $this->buildMailTemplate($object);
$body = $this->buildMailBody($object, $xactions);
$mail_tags = $this->getMailTags($object, $xactions);
$action = $this->getStrongestAction($object, $xactions)->getActionName();
$template
->setFrom($this->requireActor()->getPHID())
->setSubjectPrefix($this->getMailSubjectPrefix())
->setVarySubjectPrefix('['.$action.']')
->setThreadID($this->getMailThreadID($object), $this->getIsNewObject())
->setRelatedPHID($object->getPHID())
->setExcludeMailRecipientPHIDs($this->getExcludeMailRecipientPHIDs())
->setMailTags($mail_tags)
->setIsBulk(true)
->setBody($body->render());
if ($this->getParentMessageID()) {
$template->setParentMessageID($this->getParentMessageID());
}
$mails = $this
->buildReplyHandler($object)
->multiplexMail(
$template,
array_select_keys($handles, $email_to),
array_select_keys($handles, $email_cc));
foreach ($mails as $mail) {
$mail->saveAndSend();
}
$template->addTos($email_to);
$template->addCCs($email_cc);
return $template;
}
protected function getMailThreadID(PhabricatorLiskDAO $object) {
return $object->getPHID();
}
/**
* @task mail
*/
protected function getStrongestAction(
PhabricatorLiskDAO $object,
array $xactions) {
return last(msort($xactions, 'getActionStrength'));
}
/**
* @task mail
*/
protected function buildReplyHandler(PhabricatorLiskDAO $object) {
throw new Exception("Capability not supported.");
}
/**
* @task mail
*/
protected function getMailSubjectPrefix() {
throw new Exception("Capability not supported.");
}
/**
* @task mail
*/
protected function getMailTags(
PhabricatorLiskDAO $object,
array $xactions) {
$tags = array();
foreach ($xactions as $xaction) {
$tags[] = $xaction->getMailTags();
}
return array_mergev($tags);
}
/**
* @task mail
*/
protected function buildMailTemplate(PhabricatorLiskDAO $object) {
throw new Exception("Capability not supported.");
}
/**
* @task mail
*/
protected function getMailTo(PhabricatorLiskDAO $object) {
throw new Exception("Capability not supported.");
}
/**
* @task mail
*/
protected function getMailCC(PhabricatorLiskDAO $object) {
if ($object instanceof PhabricatorSubscribableInterface) {
return $this->subscribers;
}
throw new Exception("Capability not supported.");
}
/**
* @task mail
*/
protected function buildMailBody(
PhabricatorLiskDAO $object,
array $xactions) {
$headers = array();
$comments = array();
foreach ($xactions as $xaction) {
if ($xaction->shouldHideForMail($xactions)) {
continue;
}
$header = $xaction->getTitleForMail();
if ($header !== null) {
$headers[] = $header;
}
$comment = $xaction->getBodyForMail();
if ($comment !== null) {
$comments[] = $comment;
}
}
$body = new PhabricatorMetaMTAMailBody();
$body->addRawSection(implode("\n", $headers));
foreach ($comments as $comment) {
$body->addRawSection($comment);
}
return $body;
}
/* -( Publishing Feed Stories )-------------------------------------------- */
/**
* @task feed
*/
protected function shouldPublishFeedStory(
PhabricatorLiskDAO $object,
array $xactions) {
return false;
}
/**
* @task feed
*/
protected function getFeedStoryType() {
return 'PhabricatorApplicationTransactionFeedStory';
}
/**
* @task feed
*/
protected function getFeedRelatedPHIDs(
PhabricatorLiskDAO $object,
array $xactions) {
return array(
$object->getPHID(),
$this->requireActor()->getPHID(),
);
}
/**
* @task feed
*/
protected function getFeedNotifyPHIDs(
PhabricatorLiskDAO $object,
array $xactions) {
return array_unique(array_merge(
$this->getMailTo($object),
$this->getMailCC($object)));
}
/**
* @task feed
*/
protected function getFeedStoryData(
PhabricatorLiskDAO $object,
array $xactions) {
$xactions = msort($xactions, 'getActionStrength');
$xactions = array_reverse($xactions);
return array(
'objectPHID' => $object->getPHID(),
'transactionPHIDs' => mpull($xactions, 'getPHID'),
);
}
/**
* @task feed
*/
protected function publishFeedStory(
PhabricatorLiskDAO $object,
array $xactions,
array $mailed_phids) {
$xactions = mfilter($xactions, 'shouldHideForFeed', true);
$related_phids = $this->getFeedRelatedPHIDs($object, $xactions);
$subscribed_phids = $this->getFeedNotifyPHIDs($object, $xactions);
$story_type = $this->getFeedStoryType();
$story_data = $this->getFeedStoryData($object, $xactions);
id(new PhabricatorFeedStoryPublisher())
->setStoryType($story_type)
->setStoryData($story_data)
->setStoryTime(time())
->setStoryAuthorPHID($this->requireActor()->getPHID())
->setRelatedPHIDs($related_phids)
->setPrimaryObjectPHID($object->getPHID())
->setSubscribedPHIDs($subscribed_phids)
->setMailRecipientPHIDs($mailed_phids)
->publish();
}
/* -( Search Index )------------------------------------------------------- */
/**
* @task search
*/
protected function supportsSearch() {
return false;
}
/* -( Herald Integration )-------------------------------------------------- */
- protected function supportsHerald() {
+ protected function shouldApplyHeraldRules(
+ PhabricatorLiskDAO $object,
+ array $xactions) {
return false;
}
protected function buildHeraldAdapter(
PhabricatorLiskDAO $object,
array $xactions) {
throw new Exception('No herald adapter specified.');
}
private function setHeraldAdapter(HeraldAdapter $adapter) {
$this->heraldAdapter = $adapter;
return $this;
}
protected function getHeraldAdapter() {
return $this->heraldAdapter;
}
private function setHeraldTranscript(HeraldTranscript $transcript) {
$this->heraldTranscript = $transcript;
return $this;
}
protected function getHeraldTranscript() {
return $this->heraldTranscript;
}
private function applyHeraldRules(
PhabricatorLiskDAO $object,
array $xactions) {
$adapter = $this->buildHeraldAdapter($object, $xactions);
$adapter->setContentSource($this->getContentSource());
$adapter->setIsNewObject($this->getIsNewObject());
$xscript = HeraldEngine::loadAndApplyRules($adapter);
$this->setHeraldAdapter($adapter);
$this->setHeraldTranscript($xscript);
- $this->didApplyHeraldRules($object, $adapter, $xscript);
+ return $this->didApplyHeraldRules($object, $adapter, $xscript);
}
protected function didApplyHeraldRules(
PhabricatorLiskDAO $object,
HeraldAdapter $adapter,
HeraldTranscript $transcript) {
-
+ return array();
}
/* -( Custom Fields )------------------------------------------------------ */
/**
* @task customfield
*/
private function getCustomFieldForTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
$field_key = $xaction->getMetadataValue('customfield:key');
if (!$field_key) {
throw new Exception(
"Custom field transaction has no 'customfield:key'!");
}
$field = PhabricatorCustomField::getObjectField(
$object,
PhabricatorCustomField::ROLE_APPLICATIONTRANSACTIONS,
$field_key);
if (!$field) {
throw new Exception(
"Custom field transaction has invalid 'customfield:key'; field ".
"'{$field_key}' is disabled or does not exist.");
}
if (!$field->shouldAppearInApplicationTransactions()) {
throw new Exception(
"Custom field transaction '{$field_key}' does not implement ".
"integration for ApplicationTransactions.");
}
$field->setViewer($this->getActor());
return $field;
}
/* -( Files )-------------------------------------------------------------- */
/**
* Extract the PHIDs of any files which these transactions attach.
*
* @task files
*/
private function extractFilePHIDs(
PhabricatorLiskDAO $object,
array $xactions) {
$blocks = array();
foreach ($xactions as $xaction) {
$blocks[] = $this->getRemarkupBlocksFromTransaction($xaction);
}
$blocks = array_mergev($blocks);
$phids = array();
if ($blocks) {
$phids[] = PhabricatorMarkupEngine::extractFilePHIDsFromEmbeddedFiles(
$blocks);
}
foreach ($xactions as $xaction) {
$phids[] = $this->extractFilePHIDsFromCustomTransaction(
$object,
$xaction);
}
$phids = array_unique(array_filter(array_mergev($phids)));
if (!$phids) {
return array();
}
// Only let a user attach files they can actually see, since this would
// otherwise let you access any file by attaching it to an object you have
// view permission on.
$files = id(new PhabricatorFileQuery())
->setViewer($this->getActor())
->withPHIDs($phids)
->execute();
return mpull($files, 'getPHID');
}
/**
* @task files
*/
protected function extractFilePHIDsFromCustomTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
return array();
}
/**
* @task files
*/
private function attachFiles(
PhabricatorLiskDAO $object,
array $file_phids) {
if (!$file_phids) {
return;
}
$editor = id(new PhabricatorEdgeEditor())
->setActor($this->getActor());
// TODO: Edge-based events were almost certainly a terrible idea. If we
// don't suppress this event, the Maniphest listener reenters and adds
// more transactions. Just suppress it until that can get cleaned up.
$editor->setSuppressEvents(true);
$src = $object->getPHID();
$type = PhabricatorEdgeConfig::TYPE_OBJECT_HAS_FILE;
foreach ($file_phids as $dst) {
$editor->addEdge($src, $type, $dst);
}
$editor->save();
}
}
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Jan 19 2025, 11:51 (4 w, 5 d ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1124267
Default Alt Text
(94 KB)
Attached To
Mode
rP Phorge
Attached
Detach File
Event Timeline
Log In to Comment