Page MenuHomePhorge

No OneTemporary

diff --git a/src/applications/herald/storage/transcript/HeraldTranscript.php b/src/applications/herald/storage/transcript/HeraldTranscript.php
index 9539d4653d..f977c44485 100644
--- a/src/applications/herald/storage/transcript/HeraldTranscript.php
+++ b/src/applications/herald/storage/transcript/HeraldTranscript.php
@@ -1,246 +1,250 @@
<?php
final class HeraldTranscript extends HeraldDAO
implements
PhabricatorPolicyInterface,
PhabricatorDestructibleInterface {
protected $objectTranscript;
protected $ruleTranscripts = array();
protected $conditionTranscripts = array();
protected $applyTranscripts = array();
protected $time;
protected $host;
protected $duration;
protected $objectPHID;
protected $dryRun;
protected $garbageCollected = 0;
private $object = self::ATTACHABLE;
const TABLE_SAVED_HEADER = 'herald_savedheader';
public function getXHeraldRulesHeader() {
$ids = array();
foreach ($this->applyTranscripts as $xscript) {
if ($xscript->getApplied()) {
if ($xscript->getRuleID()) {
$ids[] = $xscript->getRuleID();
}
}
}
if (!$ids) {
return 'none';
}
// A rule may have multiple effects, which will cause it to be listed
// multiple times.
$ids = array_unique($ids);
foreach ($ids as $k => $id) {
$ids[$k] = '<'.$id.'>';
}
return implode(', ', $ids);
}
public static function saveXHeraldRulesHeader($phid, $header) {
// Combine any existing header with the new header, listing all rules
// which have ever triggered for this object.
$header = self::combineXHeraldRulesHeaders(
self::loadXHeraldRulesHeader($phid),
$header);
queryfx(
id(new HeraldTranscript())->establishConnection('w'),
'INSERT INTO %T (phid, header) VALUES (%s, %s)
ON DUPLICATE KEY UPDATE header = VALUES(header)',
self::TABLE_SAVED_HEADER,
$phid,
$header);
return $header;
}
private static function combineXHeraldRulesHeaders($u, $v) {
+ if ($u === null) {
+ return $v;
+ }
+
$u = preg_split('/[, ]+/', $u);
$v = preg_split('/[, ]+/', $v);
$combined = array_unique(array_filter(array_merge($u, $v)));
return implode(', ', $combined);
}
public static function loadXHeraldRulesHeader($phid) {
$header = queryfx_one(
id(new HeraldTranscript())->establishConnection('r'),
'SELECT * FROM %T WHERE phid = %s',
self::TABLE_SAVED_HEADER,
$phid);
if ($header) {
return idx($header, 'header');
}
return null;
}
protected function getConfiguration() {
// Ugh. Too much of a mess to deal with.
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_TIMESTAMPS => false,
self::CONFIG_SERIALIZATION => array(
'objectTranscript' => self::SERIALIZATION_PHP,
'ruleTranscripts' => self::SERIALIZATION_PHP,
'conditionTranscripts' => self::SERIALIZATION_PHP,
'applyTranscripts' => self::SERIALIZATION_PHP,
),
self::CONFIG_BINARY => array(
'objectTranscript' => true,
'ruleTranscripts' => true,
'conditionTranscripts' => true,
'applyTranscripts' => true,
),
self::CONFIG_COLUMN_SCHEMA => array(
'time' => 'epoch',
'host' => 'text255',
'duration' => 'double',
'dryRun' => 'bool',
'garbageCollected' => 'bool',
),
self::CONFIG_KEY_SCHEMA => array(
'key_phid' => null,
'phid' => array(
'columns' => array('phid'),
'unique' => true,
),
'objectPHID' => array(
'columns' => array('objectPHID'),
),
'garbageCollected' => array(
'columns' => array('garbageCollected', 'time'),
),
),
) + parent::getConfiguration();
}
public function __construct() {
$this->time = time();
$this->host = php_uname('n');
}
public function addApplyTranscript(HeraldApplyTranscript $transcript) {
$this->applyTranscripts[] = $transcript;
return $this;
}
public function getApplyTranscripts() {
return nonempty($this->applyTranscripts, array());
}
public function setDuration($duration) {
$this->duration = $duration;
return $this;
}
public function setObjectTranscript(HeraldObjectTranscript $transcript) {
$this->objectTranscript = $transcript;
return $this;
}
public function getObjectTranscript() {
return $this->objectTranscript;
}
public function addRuleTranscript(HeraldRuleTranscript $transcript) {
$this->ruleTranscripts[$transcript->getRuleID()] = $transcript;
return $this;
}
public function discardDetails() {
$this->applyTranscripts = null;
$this->ruleTranscripts = null;
$this->objectTranscript = null;
$this->conditionTranscripts = null;
}
public function getRuleTranscripts() {
return nonempty($this->ruleTranscripts, array());
}
public function addConditionTranscript(
HeraldConditionTranscript $transcript) {
$rule_id = $transcript->getRuleID();
$cond_id = $transcript->getConditionID();
$this->conditionTranscripts[$rule_id][$cond_id] = $transcript;
return $this;
}
public function getConditionTranscriptsForRule($rule_id) {
return idx($this->conditionTranscripts, $rule_id, array());
}
public function getMetadataMap() {
return array(
pht('Run At Epoch') => date('F jS, g:i:s A', $this->time),
pht('Run On Host') => $this->host,
pht('Run Duration') => (int)(1000 * $this->duration).' ms',
);
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
HeraldTranscriptPHIDType::TYPECONST);
}
public function attachObject($object = null) {
$this->object = $object;
return $this;
}
public function getObject() {
return $this->assertAttached($this->object);
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
);
}
public function getPolicy($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
return PhabricatorPolicies::POLICY_USER;
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return false;
}
public function describeAutomaticCapability($capability) {
return pht(
'To view a transcript, you must be able to view the object the '.
'transcript is about.');
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$this->openTransaction();
$this->delete();
$this->saveTransaction();
}
}
diff --git a/src/applications/project/__tests__/PhabricatorProjectCoreTestCase.php b/src/applications/project/__tests__/PhabricatorProjectCoreTestCase.php
index a31bf8853c..e3a138c702 100644
--- a/src/applications/project/__tests__/PhabricatorProjectCoreTestCase.php
+++ b/src/applications/project/__tests__/PhabricatorProjectCoreTestCase.php
@@ -1,1694 +1,1694 @@
<?php
final class PhabricatorProjectCoreTestCase extends PhabricatorTestCase {
protected function getPhabricatorTestCaseConfiguration() {
return array(
self::PHABRICATOR_TESTCONFIG_BUILD_STORAGE_FIXTURES => true,
);
}
public function testViewProject() {
$user = $this->createUser();
$user->save();
$user2 = $this->createUser();
$user2->save();
$proj = $this->createProject($user);
$proj = $this->refreshProject($proj, $user, true);
$this->joinProject($proj, $user);
$proj->setViewPolicy(PhabricatorPolicies::POLICY_USER);
$proj->save();
$can_view = PhabricatorPolicyCapability::CAN_VIEW;
// When the view policy is set to "users", any user can see the project.
$this->assertTrue((bool)$this->refreshProject($proj, $user));
$this->assertTrue((bool)$this->refreshProject($proj, $user2));
// When the view policy is set to "no one", members can still see the
// project.
$proj->setViewPolicy(PhabricatorPolicies::POLICY_NOONE);
$proj->save();
$this->assertTrue((bool)$this->refreshProject($proj, $user));
$this->assertFalse((bool)$this->refreshProject($proj, $user2));
}
public function testApplicationPolicy() {
$user = $this->createUser()
->save();
$proj = $this->createProject($user);
$this->assertTrue(
PhabricatorPolicyFilter::hasCapability(
$user,
$proj,
PhabricatorPolicyCapability::CAN_VIEW));
// This object is visible so its handle should load normally.
$handle = id(new PhabricatorHandleQuery())
->setViewer($user)
->withPHIDs(array($proj->getPHID()))
->executeOne();
$this->assertEqual($proj->getPHID(), $handle->getPHID());
// Change the "Can Use Application" policy for Projecs to "No One". This
// should cause filtering checks to fail even when they are executed
// directly rather than via a Query.
$env = PhabricatorEnv::beginScopedEnv();
$env->overrideEnvConfig(
'phabricator.application-settings',
array(
'PHID-APPS-PhabricatorProjectApplication' => array(
'policy' => array(
'view' => PhabricatorPolicies::POLICY_NOONE,
),
),
));
// Application visibility is cached because it does not normally change
// over the course of a single request. Drop the cache so the next filter
// test uses the new visibility.
PhabricatorCaches::destroyRequestCache();
$this->assertFalse(
PhabricatorPolicyFilter::hasCapability(
$user,
$proj,
PhabricatorPolicyCapability::CAN_VIEW));
// We should still be able to load a handle for the project, even if we
// can not see the application.
$handle = id(new PhabricatorHandleQuery())
->setViewer($user)
->withPHIDs(array($proj->getPHID()))
->executeOne();
// The handle should load...
$this->assertEqual($proj->getPHID(), $handle->getPHID());
// ...but be policy filtered.
$this->assertTrue($handle->getPolicyFiltered());
unset($env);
}
public function testIsViewerMemberOrWatcher() {
$user1 = $this->createUser()
->save();
$user2 = $this->createUser()
->save();
$user3 = $this->createUser()
->save();
$proj1 = $this->createProject($user1);
$proj1 = $this->refreshProject($proj1, $user1);
$this->joinProject($proj1, $user1);
$this->joinProject($proj1, $user3);
$this->watchProject($proj1, $user3);
$proj1 = $this->refreshProject($proj1, $user1);
$this->assertTrue($proj1->isUserMember($user1->getPHID()));
$proj1 = $this->refreshProject($proj1, $user1, false, true);
$this->assertTrue($proj1->isUserMember($user1->getPHID()));
$this->assertFalse($proj1->isUserWatcher($user1->getPHID()));
$proj1 = $this->refreshProject($proj1, $user1, true, false);
$this->assertTrue($proj1->isUserMember($user1->getPHID()));
$this->assertFalse($proj1->isUserMember($user2->getPHID()));
$this->assertTrue($proj1->isUserMember($user3->getPHID()));
$proj1 = $this->refreshProject($proj1, $user1, true, true);
$this->assertTrue($proj1->isUserMember($user1->getPHID()));
$this->assertFalse($proj1->isUserMember($user2->getPHID()));
$this->assertTrue($proj1->isUserMember($user3->getPHID()));
$this->assertFalse($proj1->isUserWatcher($user1->getPHID()));
$this->assertFalse($proj1->isUserWatcher($user2->getPHID()));
$this->assertTrue($proj1->isUserWatcher($user3->getPHID()));
}
public function testEditProject() {
$user = $this->createUser();
$user->save();
$user->setAllowInlineCacheGeneration(true);
$proj = $this->createProject($user);
// When edit and view policies are set to "user", anyone can edit.
$proj->setViewPolicy(PhabricatorPolicies::POLICY_USER);
$proj->setEditPolicy(PhabricatorPolicies::POLICY_USER);
$proj->save();
$this->assertTrue($this->attemptProjectEdit($proj, $user));
// When edit policy is set to "no one", no one can edit.
$proj->setEditPolicy(PhabricatorPolicies::POLICY_NOONE);
$proj->save();
$caught = null;
try {
$this->attemptProjectEdit($proj, $user);
} catch (Exception $ex) {
$caught = $ex;
}
$this->assertTrue($caught instanceof Exception);
}
public function testAncestorMembers() {
$user1 = $this->createUser();
$user1->save();
$user2 = $this->createUser();
$user2->save();
$parent = $this->createProject($user1);
$child = $this->createProject($user1, $parent);
$this->joinProject($child, $user1);
$this->joinProject($child, $user2);
$project = id(new PhabricatorProjectQuery())
->setViewer($user1)
->withPHIDs(array($child->getPHID()))
->needAncestorMembers(true)
->executeOne();
$members = array_fuse($project->getParentProject()->getMemberPHIDs());
ksort($members);
$expect = array_fuse(
array(
$user1->getPHID(),
$user2->getPHID(),
));
ksort($expect);
$this->assertEqual($expect, $members);
}
public function testAncestryQueries() {
$user = $this->createUser();
$user->save();
$ancestor = $this->createProject($user);
$parent = $this->createProject($user, $ancestor);
$child = $this->createProject($user, $parent);
$projects = id(new PhabricatorProjectQuery())
->setViewer($user)
->withAncestorProjectPHIDs(array($ancestor->getPHID()))
->execute();
$this->assertEqual(2, count($projects));
$projects = id(new PhabricatorProjectQuery())
->setViewer($user)
->withParentProjectPHIDs(array($ancestor->getPHID()))
->execute();
$this->assertEqual(1, count($projects));
$this->assertEqual(
$parent->getPHID(),
head($projects)->getPHID());
$projects = id(new PhabricatorProjectQuery())
->setViewer($user)
->withAncestorProjectPHIDs(array($ancestor->getPHID()))
->withDepthBetween(2, null)
->execute();
$this->assertEqual(1, count($projects));
$this->assertEqual(
$child->getPHID(),
head($projects)->getPHID());
$parent2 = $this->createProject($user, $ancestor);
$child2 = $this->createProject($user, $parent2);
$grandchild2 = $this->createProject($user, $child2);
$projects = id(new PhabricatorProjectQuery())
->setViewer($user)
->withAncestorProjectPHIDs(array($ancestor->getPHID()))
->execute();
$this->assertEqual(5, count($projects));
$projects = id(new PhabricatorProjectQuery())
->setViewer($user)
->withParentProjectPHIDs(array($ancestor->getPHID()))
->execute();
$this->assertEqual(2, count($projects));
$projects = id(new PhabricatorProjectQuery())
->setViewer($user)
->withAncestorProjectPHIDs(array($ancestor->getPHID()))
->withDepthBetween(2, null)
->execute();
$this->assertEqual(3, count($projects));
$projects = id(new PhabricatorProjectQuery())
->setViewer($user)
->withAncestorProjectPHIDs(array($ancestor->getPHID()))
->withDepthBetween(3, null)
->execute();
$this->assertEqual(1, count($projects));
$projects = id(new PhabricatorProjectQuery())
->setViewer($user)
->withPHIDs(
array(
$child->getPHID(),
$grandchild2->getPHID(),
))
->execute();
$this->assertEqual(2, count($projects));
}
public function testMemberMaterialization() {
$material_type = PhabricatorProjectMaterializedMemberEdgeType::EDGECONST;
$user = $this->createUser();
$user->save();
$parent = $this->createProject($user);
$child = $this->createProject($user, $parent);
$this->joinProject($child, $user);
$parent_material = PhabricatorEdgeQuery::loadDestinationPHIDs(
$parent->getPHID(),
$material_type);
$this->assertEqual(
array($user->getPHID()),
$parent_material);
}
public function testMilestones() {
$user = $this->createUser();
$user->save();
$parent = $this->createProject($user);
$m1 = $this->createProject($user, $parent, true);
$m2 = $this->createProject($user, $parent, true);
$m3 = $this->createProject($user, $parent, true);
$this->assertEqual(1, $m1->getMilestoneNumber());
$this->assertEqual(2, $m2->getMilestoneNumber());
$this->assertEqual(3, $m3->getMilestoneNumber());
}
public function testMilestoneMembership() {
$user = $this->createUser();
$user->save();
$parent = $this->createProject($user);
$milestone = $this->createProject($user, $parent, true);
$this->joinProject($parent, $user);
$milestone = id(new PhabricatorProjectQuery())
->setViewer($user)
->withPHIDs(array($milestone->getPHID()))
->executeOne();
$this->assertTrue($milestone->isUserMember($user->getPHID()));
$milestone = id(new PhabricatorProjectQuery())
->setViewer($user)
->withPHIDs(array($milestone->getPHID()))
->needMembers(true)
->executeOne();
$this->assertEqual(
array($user->getPHID()),
$milestone->getMemberPHIDs());
}
public function testSameSlugAsName() {
// It should be OK to type the primary hashtag into "additional hashtags",
// even if the primary hashtag doesn't exist yet because you're creating
// or renaming the project.
$user = $this->createUser();
$user->save();
$project = $this->createProject($user);
// In this first case, set the name and slugs at the same time.
$name = 'slugproject';
$xactions = array();
$xactions[] = id(new PhabricatorProjectTransaction())
->setTransactionType(PhabricatorProjectNameTransaction::TRANSACTIONTYPE)
->setNewValue($name);
$this->applyTransactions($project, $user, $xactions);
$xactions = array();
$xactions[] = id(new PhabricatorProjectTransaction())
->setTransactionType(PhabricatorProjectSlugsTransaction::TRANSACTIONTYPE)
->setNewValue(array($name));
$this->applyTransactions($project, $user, $xactions);
$project = id(new PhabricatorProjectQuery())
->setViewer($user)
->withPHIDs(array($project->getPHID()))
->needSlugs(true)
->executeOne();
$slugs = $project->getSlugs();
$slugs = mpull($slugs, 'getSlug');
$this->assertTrue(in_array($name, $slugs));
// In this second case, set the name first and then the slugs separately.
$name2 = 'slugproject2';
$xactions = array();
$xactions[] = id(new PhabricatorProjectTransaction())
->setTransactionType(PhabricatorProjectNameTransaction::TRANSACTIONTYPE)
->setNewValue($name2);
$xactions[] = id(new PhabricatorProjectTransaction())
->setTransactionType(PhabricatorProjectSlugsTransaction::TRANSACTIONTYPE)
->setNewValue(array($name2));
$this->applyTransactions($project, $user, $xactions);
$project = id(new PhabricatorProjectQuery())
->setViewer($user)
->withPHIDs(array($project->getPHID()))
->needSlugs(true)
->executeOne();
$slugs = $project->getSlugs();
$slugs = mpull($slugs, 'getSlug');
$this->assertTrue(in_array($name2, $slugs));
}
public function testDuplicateSlugs() {
// Creating a project with multiple duplicate slugs should succeed.
$user = $this->createUser();
$user->save();
$project = $this->createProject($user);
$input = 'duplicate';
$xactions = array();
$xactions[] = id(new PhabricatorProjectTransaction())
->setTransactionType(PhabricatorProjectSlugsTransaction::TRANSACTIONTYPE)
->setNewValue(array($input, $input));
$this->applyTransactions($project, $user, $xactions);
$project = id(new PhabricatorProjectQuery())
->setViewer($user)
->withPHIDs(array($project->getPHID()))
->needSlugs(true)
->executeOne();
$slugs = $project->getSlugs();
$slugs = mpull($slugs, 'getSlug');
$this->assertTrue(in_array($input, $slugs));
}
public function testNormalizeSlugs() {
// When a user creates a project with slug "XxX360n0sc0perXxX", normalize
// it before writing it.
$user = $this->createUser();
$user->save();
$project = $this->createProject($user);
$input = 'NoRmAlIzE';
$expect = 'normalize';
$xactions = array();
$xactions[] = id(new PhabricatorProjectTransaction())
->setTransactionType(PhabricatorProjectSlugsTransaction::TRANSACTIONTYPE)
->setNewValue(array($input));
$this->applyTransactions($project, $user, $xactions);
$project = id(new PhabricatorProjectQuery())
->setViewer($user)
->withPHIDs(array($project->getPHID()))
->needSlugs(true)
->executeOne();
$slugs = $project->getSlugs();
$slugs = mpull($slugs, 'getSlug');
$this->assertTrue(in_array($expect, $slugs));
// If another user tries to add the same slug in denormalized form, it
// should be caught and fail, even though the database version of the slug
// is normalized.
$project2 = $this->createProject($user);
$xactions = array();
$xactions[] = id(new PhabricatorProjectTransaction())
->setTransactionType(PhabricatorProjectSlugsTransaction::TRANSACTIONTYPE)
->setNewValue(array($input));
$caught = null;
try {
$this->applyTransactions($project2, $user, $xactions);
} catch (PhabricatorApplicationTransactionValidationException $ex) {
$caught = $ex;
}
$this->assertTrue((bool)$caught);
}
public function testProjectMembersVisibility() {
// This is primarily testing that you can create a project and set the
// visibility or edit policy to "Project Members" immediately.
$user1 = $this->createUser();
$user1->save();
$user2 = $this->createUser();
$user2->save();
$project = PhabricatorProject::initializeNewProject($user1);
$name = pht('Test Project %d', mt_rand());
$xactions = array();
$xactions[] = id(new PhabricatorProjectTransaction())
->setTransactionType(PhabricatorProjectNameTransaction::TRANSACTIONTYPE)
->setNewValue($name);
$xactions[] = id(new PhabricatorProjectTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_VIEW_POLICY)
->setNewValue(
id(new PhabricatorProjectMembersPolicyRule())
->getObjectPolicyFullKey());
$edge_type = PhabricatorProjectProjectHasMemberEdgeType::EDGECONST;
$xactions[] = id(new PhabricatorProjectTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
->setMetadataValue('edge:type', $edge_type)
->setNewValue(
array(
'=' => array($user1->getPHID() => $user1->getPHID()),
));
$this->applyTransactions($project, $user1, $xactions);
$this->assertTrue((bool)$this->refreshProject($project, $user1));
$this->assertFalse((bool)$this->refreshProject($project, $user2));
$this->leaveProject($project, $user1);
$this->assertFalse((bool)$this->refreshProject($project, $user1));
}
public function testParentProject() {
$user = $this->createUser();
$user->save();
$parent = $this->createProject($user);
$child = $this->createProject($user, $parent);
$this->assertTrue(true);
$child = $this->refreshProject($child, $user);
$this->assertEqual(
$parent->getPHID(),
$child->getParentProject()->getPHID());
$this->assertEqual(1, (int)$child->getProjectDepth());
$this->assertFalse(
$child->isUserMember($user->getPHID()));
$this->assertFalse(
$child->getParentProject()->isUserMember($user->getPHID()));
$this->joinProject($child, $user);
$child = $this->refreshProject($child, $user);
$this->assertTrue(
$child->isUserMember($user->getPHID()));
$this->assertTrue(
$child->getParentProject()->isUserMember($user->getPHID()));
// Test that hiding a parent hides the child.
$user2 = $this->createUser();
$user2->save();
// Second user can see the project for now.
$this->assertTrue((bool)$this->refreshProject($child, $user2));
// Hide the parent.
$this->setViewPolicy($parent, $user, $user->getPHID());
// First user (who can see the parent because they are a member of
// the child) can see the project.
$this->assertTrue((bool)$this->refreshProject($child, $user));
// Second user can not, because they can't see the parent.
$this->assertFalse((bool)$this->refreshProject($child, $user2));
}
public function testSlugMaps() {
// When querying by slugs, slugs should be normalized and the mapping
// should be reported correctly.
$user = $this->createUser();
$user->save();
$name = 'queryslugproject';
$name2 = 'QUERYslugPROJECT';
$slug = 'queryslugextra';
$slug2 = 'QuErYSlUgExTrA';
$project = PhabricatorProject::initializeNewProject($user);
$xactions = array();
$xactions[] = id(new PhabricatorProjectTransaction())
->setTransactionType(PhabricatorProjectNameTransaction::TRANSACTIONTYPE)
->setNewValue($name);
$xactions[] = id(new PhabricatorProjectTransaction())
->setTransactionType(PhabricatorProjectSlugsTransaction::TRANSACTIONTYPE)
->setNewValue(array($slug));
$this->applyTransactions($project, $user, $xactions);
$project_query = id(new PhabricatorProjectQuery())
->setViewer($user)
->withSlugs(array($name));
$project_query->execute();
$map = $project_query->getSlugMap();
$this->assertEqual(
array(
$name => $project->getPHID(),
),
ipull($map, 'projectPHID'));
$project_query = id(new PhabricatorProjectQuery())
->setViewer($user)
->withSlugs(array($slug));
$project_query->execute();
$map = $project_query->getSlugMap();
$this->assertEqual(
array(
$slug => $project->getPHID(),
),
ipull($map, 'projectPHID'));
$project_query = id(new PhabricatorProjectQuery())
->setViewer($user)
->withSlugs(array($name, $slug, $name2, $slug2));
$project_query->execute();
$map = $project_query->getSlugMap();
$expect = array(
$name => $project->getPHID(),
$slug => $project->getPHID(),
$name2 => $project->getPHID(),
$slug2 => $project->getPHID(),
);
$actual = ipull($map, 'projectPHID');
ksort($expect);
ksort($actual);
$this->assertEqual($expect, $actual);
$expect = array(
$name => $name,
$slug => $slug,
$name2 => $name,
$slug2 => $slug,
);
$actual = ipull($map, 'slug');
ksort($expect);
ksort($actual);
$this->assertEqual($expect, $actual);
}
public function testJoinLeaveProject() {
$user = $this->createUser();
$user->save();
$proj = $this->createProjectWithNewAuthor();
$proj = $this->refreshProject($proj, $user, true);
$this->assertTrue(
(bool)$proj,
pht(
'Assumption that projects are default visible '.
'to any user when created.'));
$this->assertFalse(
$proj->isUserMember($user->getPHID()),
pht('Arbitrary user not member of project.'));
// Join the project.
$this->joinProject($proj, $user);
$proj = $this->refreshProject($proj, $user, true);
$this->assertTrue((bool)$proj);
$this->assertTrue(
$proj->isUserMember($user->getPHID()),
pht('Join works.'));
// Join the project again.
$this->joinProject($proj, $user);
$proj = $this->refreshProject($proj, $user, true);
$this->assertTrue((bool)$proj);
$this->assertTrue(
$proj->isUserMember($user->getPHID()),
pht('Joining an already-joined project is a no-op.'));
// Leave the project.
$this->leaveProject($proj, $user);
$proj = $this->refreshProject($proj, $user, true);
$this->assertTrue((bool)$proj);
$this->assertFalse(
$proj->isUserMember($user->getPHID()),
pht('Leave works.'));
// Leave the project again.
$this->leaveProject($proj, $user);
$proj = $this->refreshProject($proj, $user, true);
$this->assertTrue((bool)$proj);
$this->assertFalse(
$proj->isUserMember($user->getPHID()),
pht('Leaving an already-left project is a no-op.'));
// If a user can't edit or join a project, joining fails.
$proj->setEditPolicy(PhabricatorPolicies::POLICY_NOONE);
$proj->setJoinPolicy(PhabricatorPolicies::POLICY_NOONE);
$proj->save();
$proj = $this->refreshProject($proj, $user, true);
$caught = null;
try {
$this->joinProject($proj, $user);
} catch (Exception $ex) {
$caught = $ex;
}
$this->assertTrue($ex instanceof Exception);
// If a user can edit a project, they can join.
$proj->setEditPolicy(PhabricatorPolicies::POLICY_USER);
$proj->setJoinPolicy(PhabricatorPolicies::POLICY_NOONE);
$proj->save();
$proj = $this->refreshProject($proj, $user, true);
$this->joinProject($proj, $user);
$proj = $this->refreshProject($proj, $user, true);
$this->assertTrue(
$proj->isUserMember($user->getPHID()),
pht('Join allowed with edit permission.'));
$this->leaveProject($proj, $user);
// If a user can join a project, they can join, even if they can't edit.
$proj->setEditPolicy(PhabricatorPolicies::POLICY_NOONE);
$proj->setJoinPolicy(PhabricatorPolicies::POLICY_USER);
$proj->save();
$proj = $this->refreshProject($proj, $user, true);
$this->joinProject($proj, $user);
$proj = $this->refreshProject($proj, $user, true);
$this->assertTrue(
$proj->isUserMember($user->getPHID()),
pht('Join allowed with join permission.'));
// A user can leave a project even if they can't edit it or join.
$proj->setEditPolicy(PhabricatorPolicies::POLICY_NOONE);
$proj->setJoinPolicy(PhabricatorPolicies::POLICY_NOONE);
$proj->save();
$proj = $this->refreshProject($proj, $user, true);
$this->leaveProject($proj, $user);
$proj = $this->refreshProject($proj, $user, true);
$this->assertFalse(
$proj->isUserMember($user->getPHID()),
pht('Leave allowed without any permission.'));
}
public function testComplexConstraints() {
$user = $this->createUser();
$user->save();
$engineering = $this->createProject($user);
$engineering_scan = $this->createProject($user, $engineering);
$engineering_warp = $this->createProject($user, $engineering);
$exploration = $this->createProject($user);
$exploration_diplomacy = $this->createProject($user, $exploration);
$task_engineering = $this->newTask(
$user,
array($engineering),
pht('Engineering Only'));
$task_exploration = $this->newTask(
$user,
array($exploration),
pht('Exploration Only'));
$task_warp_explore = $this->newTask(
$user,
array($engineering_warp, $exploration),
pht('Warp to New Planet'));
$task_diplomacy_scan = $this->newTask(
$user,
array($engineering_scan, $exploration_diplomacy),
pht('Scan Diplomat'));
$task_diplomacy = $this->newTask(
$user,
array($exploration_diplomacy),
pht('Diplomatic Meeting'));
$task_warp_scan = $this->newTask(
$user,
array($engineering_scan, $engineering_warp),
pht('Scan Warp Drives'));
$this->assertQueryByProjects(
$user,
array(
$task_engineering,
$task_warp_explore,
$task_diplomacy_scan,
$task_warp_scan,
),
array($engineering),
pht('All Engineering'));
$this->assertQueryByProjects(
$user,
array(
$task_diplomacy_scan,
$task_warp_scan,
),
array($engineering_scan),
pht('All Scan'));
$this->assertQueryByProjects(
$user,
array(
$task_warp_explore,
$task_diplomacy_scan,
),
array($engineering, $exploration),
pht('Engineering + Exploration'));
// This is testing that a query for "Parent" and "Parent > Child" works
// properly.
$this->assertQueryByProjects(
$user,
array(
$task_diplomacy_scan,
$task_warp_scan,
),
array($engineering, $engineering_scan),
pht('Engineering + Scan'));
}
public function testTagAncestryConflicts() {
$user = $this->createUser();
$user->save();
$stonework = $this->createProject($user);
$stonework_masonry = $this->createProject($user, $stonework);
$stonework_sculpting = $this->createProject($user, $stonework);
$task = $this->newTask($user, array());
$this->assertEqual(array(), $this->getTaskProjects($task));
$this->addProjectTags($user, $task, array($stonework->getPHID()));
$this->assertEqual(
array(
$stonework->getPHID(),
),
$this->getTaskProjects($task));
// Adding a descendant should remove the parent.
$this->addProjectTags($user, $task, array($stonework_masonry->getPHID()));
$this->assertEqual(
array(
$stonework_masonry->getPHID(),
),
$this->getTaskProjects($task));
// Adding an ancestor should remove the descendant.
$this->addProjectTags($user, $task, array($stonework->getPHID()));
$this->assertEqual(
array(
$stonework->getPHID(),
),
$this->getTaskProjects($task));
// Adding two tags in the same hierarchy which are not mutual ancestors
// should remove the ancestor but otherwise work fine.
$this->addProjectTags(
$user,
$task,
array(
$stonework_masonry->getPHID(),
$stonework_sculpting->getPHID(),
));
$expect = array(
$stonework_masonry->getPHID(),
$stonework_sculpting->getPHID(),
);
sort($expect);
$this->assertEqual($expect, $this->getTaskProjects($task));
}
public function testTagMilestoneConflicts() {
$user = $this->createUser();
$user->save();
$stonework = $this->createProject($user);
$stonework_1 = $this->createProject($user, $stonework, true);
$stonework_2 = $this->createProject($user, $stonework, true);
$task = $this->newTask($user, array());
$this->assertEqual(array(), $this->getTaskProjects($task));
$this->addProjectTags($user, $task, array($stonework->getPHID()));
$this->assertEqual(
array(
$stonework->getPHID(),
),
$this->getTaskProjects($task));
// Adding a milesone should remove the parent.
$this->addProjectTags($user, $task, array($stonework_1->getPHID()));
$this->assertEqual(
array(
$stonework_1->getPHID(),
),
$this->getTaskProjects($task));
// Adding the parent should remove the milestone.
$this->addProjectTags($user, $task, array($stonework->getPHID()));
$this->assertEqual(
array(
$stonework->getPHID(),
),
$this->getTaskProjects($task));
// First, add one milestone.
$this->addProjectTags($user, $task, array($stonework_1->getPHID()));
// Now, adding a second milestone should remove the first milestone.
$this->addProjectTags($user, $task, array($stonework_2->getPHID()));
$this->assertEqual(
array(
$stonework_2->getPHID(),
),
$this->getTaskProjects($task));
}
public function testBoardMoves() {
$user = $this->createUser();
$user->save();
$board = $this->createProject($user);
$backlog = $this->addColumn($user, $board, 0);
$column = $this->addColumn($user, $board, 1);
// New tasks should appear in the backlog.
$task1 = $this->newTask($user, array($board));
$expect = array(
$backlog->getPHID(),
);
$this->assertColumns($expect, $user, $board, $task1);
// Moving a task should move it to the destination column.
$this->moveToColumn($user, $board, $task1, $backlog, $column);
$expect = array(
$column->getPHID(),
);
$this->assertColumns($expect, $user, $board, $task1);
// Same thing again, with a new task.
$task2 = $this->newTask($user, array($board));
$expect = array(
$backlog->getPHID(),
);
$this->assertColumns($expect, $user, $board, $task2);
// Move it, too.
$this->moveToColumn($user, $board, $task2, $backlog, $column);
$expect = array(
$column->getPHID(),
);
$this->assertColumns($expect, $user, $board, $task2);
// Now the stuff should be in the column, in order, with the more recently
// moved task on top.
$expect = array(
$task2->getPHID(),
$task1->getPHID(),
);
$label = pht('Simple move');
$this->assertTasksInColumn($expect, $user, $board, $column, $label);
// Move the second task after the first task.
$options = array(
'afterPHIDs' => array($task1->getPHID()),
);
$this->moveToColumn($user, $board, $task2, $column, $column, $options);
$expect = array(
$task1->getPHID(),
$task2->getPHID(),
);
$label = pht('With afterPHIDs');
$this->assertTasksInColumn($expect, $user, $board, $column, $label);
// Move the second task before the first task.
$options = array(
'beforePHIDs' => array($task1->getPHID()),
);
$this->moveToColumn($user, $board, $task2, $column, $column, $options);
$expect = array(
$task2->getPHID(),
$task1->getPHID(),
);
$label = pht('With beforePHIDs');
$this->assertTasksInColumn($expect, $user, $board, $column, $label);
}
public function testMilestoneMoves() {
$user = $this->createUser();
$user->save();
$board = $this->createProject($user);
$backlog = $this->addColumn($user, $board, 0);
// Create a task into the backlog.
$task = $this->newTask($user, array($board));
$expect = array(
$backlog->getPHID(),
);
$this->assertColumns($expect, $user, $board, $task);
$milestone = $this->createProject($user, $board, true);
$this->addProjectTags($user, $task, array($milestone->getPHID()));
// We just want the side effect of looking at the board: creation of the
// milestone column.
$this->loadColumns($user, $board, $task);
$column = id(new PhabricatorProjectColumnQuery())
->setViewer($user)
->withProjectPHIDs(array($board->getPHID()))
->withProxyPHIDs(array($milestone->getPHID()))
->executeOne();
$this->assertTrue((bool)$column);
// Moving the task to the milestone should have moved it to the milestone
// column.
$expect = array(
$column->getPHID(),
);
$this->assertColumns($expect, $user, $board, $task);
// Move the task within the "Milestone" column. This should not affect
// the projects the task is tagged with. See T10912.
$task_a = $task;
$task_b = $this->newTask($user, array($backlog));
$this->moveToColumn($user, $board, $task_b, $backlog, $column);
$a_options = array(
'beforePHID' => $task_b->getPHID(),
);
$b_options = array(
'beforePHID' => $task_a->getPHID(),
);
$old_projects = $this->getTaskProjects($task);
// Move the target task to the top.
$this->moveToColumn($user, $board, $task_a, $column, $column, $a_options);
$new_projects = $this->getTaskProjects($task_a);
$this->assertEqual($old_projects, $new_projects);
// Move the other task.
$this->moveToColumn($user, $board, $task_b, $column, $column, $b_options);
$new_projects = $this->getTaskProjects($task_a);
$this->assertEqual($old_projects, $new_projects);
// Move the target task again.
$this->moveToColumn($user, $board, $task_a, $column, $column, $a_options);
$new_projects = $this->getTaskProjects($task_a);
$this->assertEqual($old_projects, $new_projects);
// Add the parent project to the task. This should move it out of the
// milestone column and into the parent's backlog.
$this->addProjectTags($user, $task, array($board->getPHID()));
$expect_columns = array(
$backlog->getPHID(),
);
$this->assertColumns($expect_columns, $user, $board, $task);
$new_projects = $this->getTaskProjects($task);
$expect_projects = array(
$board->getPHID(),
);
$this->assertEqual($expect_projects, $new_projects);
}
public function testColumnExtendedPolicies() {
$user = $this->createUser();
$user->save();
$board = $this->createProject($user);
$column = $this->addColumn($user, $board, 0);
// At first, the user should be able to view and edit the column.
$column = $this->refreshColumn($user, $column);
$this->assertTrue((bool)$column);
$can_edit = PhabricatorPolicyFilter::hasCapability(
$user,
$column,
PhabricatorPolicyCapability::CAN_EDIT);
$this->assertTrue($can_edit);
// Now, set the project edit policy to "Members of Project". This should
// disable editing.
$members_policy = id(new PhabricatorProjectMembersPolicyRule())
->getObjectPolicyFullKey();
$board->setEditPolicy($members_policy)->save();
$column = $this->refreshColumn($user, $column);
$this->assertTrue((bool)$column);
$can_edit = PhabricatorPolicyFilter::hasCapability(
$user,
$column,
PhabricatorPolicyCapability::CAN_EDIT);
$this->assertFalse($can_edit);
// Now, join the project. This should make the column editable again.
$this->joinProject($board, $user);
$column = $this->refreshColumn($user, $column);
$this->assertTrue((bool)$column);
// This test has been failing randomly in a way that doesn't reproduce
// on any host, so add some extra assertions to try to nail it down.
$board = $this->refreshProject($board, $user, true);
$this->assertTrue((bool)$board);
$this->assertTrue($board->isUserMember($user->getPHID()));
$can_view = PhabricatorPolicyFilter::hasCapability(
$user,
$column,
PhabricatorPolicyCapability::CAN_VIEW);
$this->assertTrue($can_view);
$can_edit = PhabricatorPolicyFilter::hasCapability(
$user,
$column,
PhabricatorPolicyCapability::CAN_EDIT);
$this->assertTrue($can_edit);
}
public function testProjectPolicyRules() {
$author = $this->generateNewTestUser();
$proj_a = PhabricatorProject::initializeNewProject($author)
->setName('Policy A')
->save();
$proj_b = PhabricatorProject::initializeNewProject($author)
->setName('Policy B')
->save();
$user_none = $this->generateNewTestUser();
$user_any = $this->generateNewTestUser();
$user_all = $this->generateNewTestUser();
$this->joinProject($proj_a, $user_any);
$this->joinProject($proj_a, $user_all);
$this->joinProject($proj_b, $user_all);
$any_policy = id(new PhabricatorPolicy())
->setRules(
array(
array(
'action' => PhabricatorPolicy::ACTION_ALLOW,
'rule' => 'PhabricatorProjectsPolicyRule',
'value' => array(
$proj_a->getPHID(),
$proj_b->getPHID(),
),
),
))
->save();
$all_policy = id(new PhabricatorPolicy())
->setRules(
array(
array(
'action' => PhabricatorPolicy::ACTION_ALLOW,
'rule' => 'PhabricatorProjectsAllPolicyRule',
'value' => array(
$proj_a->getPHID(),
$proj_b->getPHID(),
),
),
))
->save();
$any_task = ManiphestTask::initializeNewTask($author)
->setViewPolicy($any_policy->getPHID())
->save();
$all_task = ManiphestTask::initializeNewTask($author)
->setViewPolicy($all_policy->getPHID())
->save();
$map = array(
array(
pht('Project policy rule; user in no projects'),
$user_none,
false,
false,
),
array(
pht('Project policy rule; user in some projects'),
$user_any,
true,
false,
),
array(
pht('Project policy rule; user in all projects'),
$user_all,
true,
true,
),
);
foreach ($map as $test_case) {
list($label, $user, $expect_any, $expect_all) = $test_case;
$can_any = PhabricatorPolicyFilter::hasCapability(
$user,
$any_task,
PhabricatorPolicyCapability::CAN_VIEW);
$can_all = PhabricatorPolicyFilter::hasCapability(
$user,
$all_task,
PhabricatorPolicyCapability::CAN_VIEW);
$this->assertEqual($expect_any, $can_any, pht('%s / Any', $label));
$this->assertEqual($expect_all, $can_all, pht('%s / All', $label));
}
}
private function moveToColumn(
PhabricatorUser $viewer,
PhabricatorProject $board,
ManiphestTask $task,
PhabricatorProjectColumn $src,
PhabricatorProjectColumn $dst,
$options = null) {
$xactions = array();
if (!$options) {
$options = array();
}
$value = array(
'columnPHID' => $dst->getPHID(),
) + $options;
$xactions[] = id(new ManiphestTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_COLUMNS)
->setNewValue(array($value));
$editor = id(new ManiphestTransactionEditor())
->setActor($viewer)
->setContentSource($this->newContentSource())
->setContinueOnNoEffect(true)
->applyTransactions($task, $xactions);
}
private function assertColumns(
array $expect,
PhabricatorUser $viewer,
PhabricatorProject $board,
ManiphestTask $task) {
$column_phids = $this->loadColumns($viewer, $board, $task);
$this->assertEqual($expect, $column_phids);
}
private function loadColumns(
PhabricatorUser $viewer,
PhabricatorProject $board,
ManiphestTask $task) {
$engine = id(new PhabricatorBoardLayoutEngine())
->setViewer($viewer)
->setBoardPHIDs(array($board->getPHID()))
->setObjectPHIDs(
array(
$task->getPHID(),
))
->executeLayout();
$columns = $engine->getObjectColumns($board->getPHID(), $task->getPHID());
$column_phids = mpull($columns, 'getPHID');
$column_phids = array_values($column_phids);
return $column_phids;
}
private function assertTasksInColumn(
array $expect,
PhabricatorUser $viewer,
PhabricatorProject $board,
PhabricatorProjectColumn $column,
$label = null) {
$engine = id(new PhabricatorBoardLayoutEngine())
->setViewer($viewer)
->setBoardPHIDs(array($board->getPHID()))
->setObjectPHIDs($expect)
->executeLayout();
$object_phids = $engine->getColumnObjectPHIDs(
$board->getPHID(),
$column->getPHID());
$object_phids = array_values($object_phids);
$this->assertEqual($expect, $object_phids, $label);
}
private function addColumn(
PhabricatorUser $viewer,
PhabricatorProject $project,
$sequence) {
$project->setHasWorkboard(1)->save();
return PhabricatorProjectColumn::initializeNewColumn($viewer)
->setSequence(0)
->setProperty('isDefault', ($sequence == 0))
->setProjectPHID($project->getPHID())
->save();
}
private function getTaskProjects(ManiphestTask $task) {
$project_phids = PhabricatorEdgeQuery::loadDestinationPHIDs(
$task->getPHID(),
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST);
sort($project_phids);
return $project_phids;
}
private function attemptProjectEdit(
PhabricatorProject $proj,
PhabricatorUser $user,
$skip_refresh = false) {
$proj = $this->refreshProject($proj, $user, true);
$new_name = $proj->getName().' '.mt_rand();
$params = array(
'objectIdentifier' => $proj->getID(),
'transactions' => array(
array(
'type' => 'name',
'value' => $new_name,
),
),
);
id(new ConduitCall('project.edit', $params))
->setUser($user)
->execute();
return true;
}
private function addProjectTags(
PhabricatorUser $viewer,
ManiphestTask $task,
array $phids) {
$xactions = array();
$xactions[] = id(new ManiphestTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
->setMetadataValue(
'edge:type',
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST)
->setNewValue(
array(
'+' => array_fuse($phids),
));
$editor = id(new ManiphestTransactionEditor())
->setActor($viewer)
->setContentSource($this->newContentSource())
->setContinueOnNoEffect(true)
->applyTransactions($task, $xactions);
}
private function newTask(
PhabricatorUser $viewer,
array $projects,
$name = null) {
$task = ManiphestTask::initializeNewTask($viewer);
- if (!strlen($name)) {
+ if ($name === null || $name === '') {
$name = pht('Test Task');
}
$xactions = array();
$xactions[] = id(new ManiphestTransaction())
->setTransactionType(ManiphestTaskTitleTransaction::TRANSACTIONTYPE)
->setNewValue($name);
if ($projects) {
$xactions[] = id(new ManiphestTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
->setMetadataValue(
'edge:type',
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST)
->setNewValue(
array(
'=' => array_fuse(mpull($projects, 'getPHID')),
));
}
$editor = id(new ManiphestTransactionEditor())
->setActor($viewer)
->setContentSource($this->newContentSource())
->setContinueOnNoEffect(true)
->applyTransactions($task, $xactions);
return $task;
}
private function assertQueryByProjects(
PhabricatorUser $viewer,
array $expect,
array $projects,
$label = null) {
$datasource = id(new PhabricatorProjectLogicalDatasource())
->setViewer($viewer);
$project_phids = mpull($projects, 'getPHID');
$constraints = $datasource->evaluateTokens($project_phids);
$query = id(new ManiphestTaskQuery())
->setViewer($viewer);
$query->withEdgeLogicConstraints(
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST,
$constraints);
$tasks = $query->execute();
$expect_phids = mpull($expect, 'getTitle', 'getPHID');
ksort($expect_phids);
$actual_phids = mpull($tasks, 'getTitle', 'getPHID');
ksort($actual_phids);
$this->assertEqual($expect_phids, $actual_phids, $label);
}
private function refreshProject(
PhabricatorProject $project,
PhabricatorUser $viewer,
$need_members = false,
$need_watchers = false) {
$results = id(new PhabricatorProjectQuery())
->setViewer($viewer)
->needMembers($need_members)
->needWatchers($need_watchers)
->withIDs(array($project->getID()))
->execute();
if ($results) {
return head($results);
} else {
return null;
}
}
private function refreshColumn(
PhabricatorUser $viewer,
PhabricatorProjectColumn $column) {
$results = id(new PhabricatorProjectColumnQuery())
->setViewer($viewer)
->withIDs(array($column->getID()))
->execute();
if ($results) {
return head($results);
} else {
return null;
}
}
private function createProject(
PhabricatorUser $user,
PhabricatorProject $parent = null,
$is_milestone = false) {
$project = PhabricatorProject::initializeNewProject($user, $parent);
$name = pht('Test Project %d', mt_rand());
$xactions = array();
$xactions[] = id(new PhabricatorProjectTransaction())
->setTransactionType(PhabricatorProjectNameTransaction::TRANSACTIONTYPE)
->setNewValue($name);
if ($parent) {
if ($is_milestone) {
$xactions[] = id(new PhabricatorProjectTransaction())
->setTransactionType(
PhabricatorProjectMilestoneTransaction::TRANSACTIONTYPE)
->setNewValue($parent->getPHID());
} else {
$xactions[] = id(new PhabricatorProjectTransaction())
->setTransactionType(
PhabricatorProjectParentTransaction::TRANSACTIONTYPE)
->setNewValue($parent->getPHID());
}
}
$this->applyTransactions($project, $user, $xactions);
// Force these values immediately; they are normally updated by the
// index engine.
if ($parent) {
if ($is_milestone) {
$parent->setHasMilestones(1)->save();
} else {
$parent->setHasSubprojects(1)->save();
}
}
return $project;
}
private function setViewPolicy(
PhabricatorProject $project,
PhabricatorUser $user,
$policy) {
$xactions = array();
$xactions[] = id(new PhabricatorProjectTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_VIEW_POLICY)
->setNewValue($policy);
$this->applyTransactions($project, $user, $xactions);
return $project;
}
private function createProjectWithNewAuthor() {
$author = $this->createUser();
$author->save();
$project = $this->createProject($author);
return $project;
}
private function createUser() {
$rand = mt_rand();
$user = new PhabricatorUser();
$user->setUsername('unittestuser'.$rand);
$user->setRealName(pht('Unit Test User %d', $rand));
return $user;
}
private function joinProject(
PhabricatorProject $project,
PhabricatorUser $user) {
return $this->joinOrLeaveProject($project, $user, '+');
}
private function leaveProject(
PhabricatorProject $project,
PhabricatorUser $user) {
return $this->joinOrLeaveProject($project, $user, '-');
}
private function watchProject(
PhabricatorProject $project,
PhabricatorUser $user) {
return $this->watchOrUnwatchProject($project, $user, '+');
}
private function unwatchProject(
PhabricatorProject $project,
PhabricatorUser $user) {
return $this->watchOrUnwatchProject($project, $user, '-');
}
private function joinOrLeaveProject(
PhabricatorProject $project,
PhabricatorUser $user,
$operation) {
return $this->applyProjectEdgeTransaction(
$project,
$user,
$operation,
PhabricatorProjectProjectHasMemberEdgeType::EDGECONST);
}
private function watchOrUnwatchProject(
PhabricatorProject $project,
PhabricatorUser $user,
$operation) {
return $this->applyProjectEdgeTransaction(
$project,
$user,
$operation,
PhabricatorObjectHasWatcherEdgeType::EDGECONST);
}
private function applyProjectEdgeTransaction(
PhabricatorProject $project,
PhabricatorUser $user,
$operation,
$edge_type) {
$spec = array(
$operation => array($user->getPHID() => $user->getPHID()),
);
$xactions = array();
$xactions[] = id(new PhabricatorProjectTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
->setMetadataValue('edge:type', $edge_type)
->setNewValue($spec);
$this->applyTransactions($project, $user, $xactions);
return $project;
}
private function applyTransactions(
PhabricatorProject $project,
PhabricatorUser $user,
array $xactions) {
$editor = id(new PhabricatorProjectTransactionEditor())
->setActor($user)
->setContentSource($this->newContentSource())
->setContinueOnNoEffect(true)
->applyTransactions($project, $xactions);
}
}
diff --git a/src/applications/project/storage/PhabricatorProject.php b/src/applications/project/storage/PhabricatorProject.php
index 860d6e1749..6aebc954df 100644
--- a/src/applications/project/storage/PhabricatorProject.php
+++ b/src/applications/project/storage/PhabricatorProject.php
@@ -1,918 +1,920 @@
<?php
final class PhabricatorProject extends PhabricatorProjectDAO
implements
PhabricatorApplicationTransactionInterface,
PhabricatorFlaggableInterface,
PhabricatorPolicyInterface,
PhabricatorExtendedPolicyInterface,
PhabricatorCustomFieldInterface,
PhabricatorDestructibleInterface,
PhabricatorFulltextInterface,
PhabricatorFerretInterface,
PhabricatorConduitResultInterface,
PhabricatorColumnProxyInterface,
PhabricatorSpacesInterface,
PhabricatorEditEngineSubtypeInterface,
PhabricatorWorkboardInterface {
protected $name;
protected $status = PhabricatorProjectStatus::STATUS_ACTIVE;
protected $authorPHID;
protected $primarySlug;
protected $profileImagePHID;
protected $icon;
protected $color;
protected $mailKey;
protected $viewPolicy;
protected $editPolicy;
protected $joinPolicy;
protected $isMembershipLocked;
protected $parentProjectPHID;
protected $hasWorkboard;
protected $hasMilestones;
protected $hasSubprojects;
protected $milestoneNumber;
protected $projectPath;
protected $projectDepth;
protected $projectPathKey;
protected $properties = array();
protected $spacePHID;
protected $subtype;
private $memberPHIDs = self::ATTACHABLE;
private $watcherPHIDs = self::ATTACHABLE;
private $sparseWatchers = self::ATTACHABLE;
private $sparseMembers = self::ATTACHABLE;
private $customFields = self::ATTACHABLE;
private $profileImageFile = self::ATTACHABLE;
private $slugs = self::ATTACHABLE;
private $parentProject = self::ATTACHABLE;
const TABLE_DATASOURCE_TOKEN = 'project_datasourcetoken';
const ITEM_PICTURE = 'project.picture';
const ITEM_PROFILE = 'project.profile';
const ITEM_POINTS = 'project.points';
const ITEM_WORKBOARD = 'project.workboard';
const ITEM_REPORTS = 'project.reports';
const ITEM_MEMBERS = 'project.members';
const ITEM_MANAGE = 'project.manage';
const ITEM_MILESTONES = 'project.milestones';
const ITEM_SUBPROJECTS = 'project.subprojects';
public static function initializeNewProject(
PhabricatorUser $actor,
PhabricatorProject $parent = null) {
$app = id(new PhabricatorApplicationQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withClasses(array('PhabricatorProjectApplication'))
->executeOne();
$view_policy = $app->getPolicy(
ProjectDefaultViewCapability::CAPABILITY);
$edit_policy = $app->getPolicy(
ProjectDefaultEditCapability::CAPABILITY);
$join_policy = $app->getPolicy(
ProjectDefaultJoinCapability::CAPABILITY);
// If this is the child of some other project, default the Space to the
// Space of the parent.
if ($parent) {
$space_phid = $parent->getSpacePHID();
} else {
$space_phid = $actor->getDefaultSpacePHID();
}
$default_icon = PhabricatorProjectIconSet::getDefaultIconKey();
$default_color = PhabricatorProjectIconSet::getDefaultColorKey();
return id(new PhabricatorProject())
->setAuthorPHID($actor->getPHID())
->setIcon($default_icon)
->setColor($default_color)
->setViewPolicy($view_policy)
->setEditPolicy($edit_policy)
->setJoinPolicy($join_policy)
->setSpacePHID($space_phid)
->setIsMembershipLocked(0)
->attachMemberPHIDs(array())
->attachSlugs(array())
->setHasWorkboard(0)
->setHasMilestones(0)
->setHasSubprojects(0)
->setSubtype(PhabricatorEditEngineSubtype::SUBTYPE_DEFAULT)
->attachParentProject($parent);
}
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
PhabricatorPolicyCapability::CAN_JOIN,
);
}
public function getPolicy($capability) {
if ($this->isMilestone()) {
return $this->getParentProject()->getPolicy($capability);
}
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
return $this->getViewPolicy();
case PhabricatorPolicyCapability::CAN_EDIT:
return $this->getEditPolicy();
case PhabricatorPolicyCapability::CAN_JOIN:
return $this->getJoinPolicy();
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
if ($this->isMilestone()) {
return $this->getParentProject()->hasAutomaticCapability(
$capability,
$viewer);
}
$can_edit = PhabricatorPolicyCapability::CAN_EDIT;
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
if ($this->isUserMember($viewer->getPHID())) {
// Project members can always view a project.
return true;
}
break;
case PhabricatorPolicyCapability::CAN_EDIT:
$parent = $this->getParentProject();
if ($parent) {
$can_edit_parent = PhabricatorPolicyFilter::hasCapability(
$viewer,
$parent,
$can_edit);
if ($can_edit_parent) {
return true;
}
}
break;
case PhabricatorPolicyCapability::CAN_JOIN:
if (PhabricatorPolicyFilter::hasCapability($viewer, $this, $can_edit)) {
// Project editors can always join a project.
return true;
}
break;
}
return false;
}
public function describeAutomaticCapability($capability) {
// TODO: Clarify the additional rules that parent and subprojects imply.
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
return pht('Members of a project can always view it.');
case PhabricatorPolicyCapability::CAN_JOIN:
return pht('Users who can edit a project can always join it.');
}
return null;
}
public function getExtendedPolicy($capability, PhabricatorUser $viewer) {
$extended = array();
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
$parent = $this->getParentProject();
if ($parent) {
$extended[] = array(
$parent,
PhabricatorPolicyCapability::CAN_VIEW,
);
}
break;
}
return $extended;
}
public function isUserMember($user_phid) {
if ($this->memberPHIDs !== self::ATTACHABLE) {
return in_array($user_phid, $this->memberPHIDs);
}
return $this->assertAttachedKey($this->sparseMembers, $user_phid);
}
public function setIsUserMember($user_phid, $is_member) {
if ($this->sparseMembers === self::ATTACHABLE) {
$this->sparseMembers = array();
}
$this->sparseMembers[$user_phid] = $is_member;
return $this;
}
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'properties' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'name' => 'sort128',
'status' => 'text32',
'primarySlug' => 'text128?',
'isMembershipLocked' => 'bool',
'profileImagePHID' => 'phid?',
'icon' => 'text32',
'color' => 'text32',
'mailKey' => 'bytes20',
'joinPolicy' => 'policy',
'parentProjectPHID' => 'phid?',
'hasWorkboard' => 'bool',
'hasMilestones' => 'bool',
'hasSubprojects' => 'bool',
'milestoneNumber' => 'uint32?',
'projectPath' => 'hashpath64',
'projectDepth' => 'uint32',
'projectPathKey' => 'bytes4',
'subtype' => 'text64',
),
self::CONFIG_KEY_SCHEMA => array(
'key_icon' => array(
'columns' => array('icon'),
),
'key_color' => array(
'columns' => array('color'),
),
'key_milestone' => array(
'columns' => array('parentProjectPHID', 'milestoneNumber'),
'unique' => true,
),
'key_primaryslug' => array(
'columns' => array('primarySlug'),
'unique' => true,
),
'key_path' => array(
'columns' => array('projectPath', 'projectDepth'),
),
'key_pathkey' => array(
'columns' => array('projectPathKey'),
'unique' => true,
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhabricatorProjectProjectPHIDType::TYPECONST);
}
public function attachMemberPHIDs(array $phids) {
$this->memberPHIDs = $phids;
return $this;
}
public function getMemberPHIDs() {
return $this->assertAttached($this->memberPHIDs);
}
public function isArchived() {
return ($this->getStatus() == PhabricatorProjectStatus::STATUS_ARCHIVED);
}
public function getProfileImageURI() {
return $this->getProfileImageFile()->getBestURI();
}
public function attachProfileImageFile(PhabricatorFile $file) {
$this->profileImageFile = $file;
return $this;
}
public function getProfileImageFile() {
return $this->assertAttached($this->profileImageFile);
}
public function isUserWatcher($user_phid) {
if ($this->watcherPHIDs !== self::ATTACHABLE) {
return in_array($user_phid, $this->watcherPHIDs);
}
return $this->assertAttachedKey($this->sparseWatchers, $user_phid);
}
public function isUserAncestorWatcher($user_phid) {
$is_watcher = $this->isUserWatcher($user_phid);
if (!$is_watcher) {
$parent = $this->getParentProject();
if ($parent) {
return $parent->isUserWatcher($user_phid);
}
}
return $is_watcher;
}
public function getWatchedAncestorPHID($user_phid) {
if ($this->isUserWatcher($user_phid)) {
return $this->getPHID();
}
$parent = $this->getParentProject();
if ($parent) {
return $parent->getWatchedAncestorPHID($user_phid);
}
return null;
}
public function setIsUserWatcher($user_phid, $is_watcher) {
if ($this->sparseWatchers === self::ATTACHABLE) {
$this->sparseWatchers = array();
}
$this->sparseWatchers[$user_phid] = $is_watcher;
return $this;
}
public function attachWatcherPHIDs(array $phids) {
$this->watcherPHIDs = $phids;
return $this;
}
public function getWatcherPHIDs() {
return $this->assertAttached($this->watcherPHIDs);
}
public function getAllAncestorWatcherPHIDs() {
$parent = $this->getParentProject();
if ($parent) {
$watchers = $parent->getAllAncestorWatcherPHIDs();
} else {
$watchers = array();
}
foreach ($this->getWatcherPHIDs() as $phid) {
$watchers[$phid] = $phid;
}
return $watchers;
}
public function attachSlugs(array $slugs) {
$this->slugs = $slugs;
return $this;
}
public function getSlugs() {
return $this->assertAttached($this->slugs);
}
public function getColor() {
if ($this->isArchived()) {
return PHUITagView::COLOR_DISABLED;
}
return $this->color;
}
public function getURI() {
$id = $this->getID();
return "/project/view/{$id}/";
}
public function getProfileURI() {
$id = $this->getID();
return "/project/profile/{$id}/";
}
public function getWorkboardURI() {
return urisprintf('/project/board/%d/', $this->getID());
}
public function getReportsURI() {
return urisprintf('/project/reports/%d/', $this->getID());
}
public function save() {
if (!$this->getMailKey()) {
$this->setMailKey(Filesystem::readRandomCharacters(20));
}
- if (!strlen($this->getPHID())) {
+ $phid = $this->getPHID();
+ if ($phid === null || $phid === '') {
$this->setPHID($this->generatePHID());
}
- if (!strlen($this->getProjectPathKey())) {
+ $path_key = $this->getProjectPathKey();
+ if ($path_key === null || $path_key === '') {
$hash = PhabricatorHash::digestForIndex($this->getPHID());
$hash = substr($hash, 0, 4);
$this->setProjectPathKey($hash);
}
$path = array();
$depth = 0;
if ($this->parentProjectPHID) {
$parent = $this->getParentProject();
$path[] = $parent->getProjectPath();
$depth = $parent->getProjectDepth() + 1;
}
$path[] = $this->getProjectPathKey();
$path = implode('', $path);
$limit = self::getProjectDepthLimit();
if ($depth >= $limit) {
throw new Exception(pht('Project depth is too great.'));
}
$this->setProjectPath($path);
$this->setProjectDepth($depth);
$this->openTransaction();
$result = parent::save();
$this->updateDatasourceTokens();
$this->saveTransaction();
return $result;
}
public static function getProjectDepthLimit() {
// This is limited by how many path hashes we can fit in the path
// column.
return 16;
}
public function updateDatasourceTokens() {
$table = self::TABLE_DATASOURCE_TOKEN;
$conn_w = $this->establishConnection('w');
$id = $this->getID();
$slugs = queryfx_all(
$conn_w,
'SELECT * FROM %T WHERE projectPHID = %s',
id(new PhabricatorProjectSlug())->getTableName(),
$this->getPHID());
$all_strings = ipull($slugs, 'slug');
$all_strings[] = $this->getDisplayName();
$all_strings = implode(' ', $all_strings);
$tokens = PhabricatorTypeaheadDatasource::tokenizeString($all_strings);
$sql = array();
foreach ($tokens as $token) {
$sql[] = qsprintf($conn_w, '(%d, %s)', $id, $token);
}
$this->openTransaction();
queryfx(
$conn_w,
'DELETE FROM %T WHERE projectID = %d',
$table,
$id);
foreach (PhabricatorLiskDAO::chunkSQL($sql) as $chunk) {
queryfx(
$conn_w,
'INSERT INTO %T (projectID, token) VALUES %LQ',
$table,
$chunk);
}
$this->saveTransaction();
}
public function isMilestone() {
return ($this->getMilestoneNumber() !== null);
}
public function getParentProject() {
return $this->assertAttached($this->parentProject);
}
public function attachParentProject(PhabricatorProject $project = null) {
$this->parentProject = $project;
return $this;
}
public function getAncestorProjectPaths() {
$parts = array();
$path = $this->getProjectPath();
$parent_length = (strlen($path) - 4);
for ($ii = $parent_length; $ii > 0; $ii -= 4) {
$parts[] = substr($path, 0, $ii);
}
return $parts;
}
public function getAncestorProjects() {
$ancestors = array();
$cursor = $this->getParentProject();
while ($cursor) {
$ancestors[] = $cursor;
$cursor = $cursor->getParentProject();
}
return $ancestors;
}
public function supportsEditMembers() {
if ($this->isMilestone()) {
return false;
}
if ($this->getHasSubprojects()) {
return false;
}
return true;
}
public function supportsMilestones() {
if ($this->isMilestone()) {
return false;
}
return true;
}
public function supportsSubprojects() {
if ($this->isMilestone()) {
return false;
}
return true;
}
public function loadNextMilestoneNumber() {
$current = queryfx_one(
$this->establishConnection('w'),
'SELECT MAX(milestoneNumber) n
FROM %T
WHERE parentProjectPHID = %s',
$this->getTableName(),
$this->getPHID());
if (!$current) {
$number = 1;
} else {
$number = (int)$current['n'] + 1;
}
return $number;
}
public function getDisplayName() {
$name = $this->getName();
// If this is a milestone, show it as "Parent > Sprint 99".
if ($this->isMilestone()) {
$name = pht(
'%s (%s)',
$this->getParentProject()->getName(),
$name);
}
return $name;
}
public function getDisplayIconKey() {
if ($this->isMilestone()) {
$key = PhabricatorProjectIconSet::getMilestoneIconKey();
} else {
$key = $this->getIcon();
}
return $key;
}
public function getDisplayIconIcon() {
$key = $this->getDisplayIconKey();
return PhabricatorProjectIconSet::getIconIcon($key);
}
public function getDisplayIconName() {
$key = $this->getDisplayIconKey();
return PhabricatorProjectIconSet::getIconName($key);
}
public function getDisplayColor() {
if ($this->isMilestone()) {
return $this->getParentProject()->getColor();
}
return $this->getColor();
}
public function getDisplayIconComposeIcon() {
$icon = $this->getDisplayIconIcon();
return $icon;
}
public function getDisplayIconComposeColor() {
$color = $this->getDisplayColor();
$map = array(
'grey' => 'charcoal',
'checkered' => 'backdrop',
);
return idx($map, $color, $color);
}
public function getProperty($key, $default = null) {
return idx($this->properties, $key, $default);
}
public function setProperty($key, $value) {
$this->properties[$key] = $value;
return $this;
}
public function getDefaultWorkboardSort() {
return $this->getProperty('workboard.sort.default');
}
public function setDefaultWorkboardSort($sort) {
return $this->setProperty('workboard.sort.default', $sort);
}
public function getDefaultWorkboardFilter() {
return $this->getProperty('workboard.filter.default');
}
public function setDefaultWorkboardFilter($filter) {
return $this->setProperty('workboard.filter.default', $filter);
}
public function getWorkboardBackgroundColor() {
return $this->getProperty('workboard.background');
}
public function setWorkboardBackgroundColor($color) {
return $this->setProperty('workboard.background', $color);
}
public function getDisplayWorkboardBackgroundColor() {
$color = $this->getWorkboardBackgroundColor();
if ($color === null) {
$parent = $this->getParentProject();
if ($parent) {
return $parent->getDisplayWorkboardBackgroundColor();
}
}
if ($color === 'none') {
$color = null;
}
return $color;
}
/* -( PhabricatorCustomFieldInterface )------------------------------------ */
public function getCustomFieldSpecificationForRole($role) {
return PhabricatorEnv::getEnvConfig('projects.fields');
}
public function getCustomFieldBaseClass() {
return 'PhabricatorProjectCustomField';
}
public function getCustomFields() {
return $this->assertAttached($this->customFields);
}
public function attachCustomFields(PhabricatorCustomFieldAttachment $fields) {
$this->customFields = $fields;
return $this;
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new PhabricatorProjectTransactionEditor();
}
public function getApplicationTransactionTemplate() {
return new PhabricatorProjectTransaction();
}
/* -( PhabricatorSpacesInterface )----------------------------------------- */
public function getSpacePHID() {
if ($this->isMilestone()) {
return $this->getParentProject()->getSpacePHID();
}
return $this->spacePHID;
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$this->openTransaction();
$this->delete();
$columns = id(new PhabricatorProjectColumn())
->loadAllWhere('projectPHID = %s', $this->getPHID());
foreach ($columns as $column) {
$engine->destroyObject($column);
}
$slugs = id(new PhabricatorProjectSlug())
->loadAllWhere('projectPHID = %s', $this->getPHID());
foreach ($slugs as $slug) {
$slug->delete();
}
$this->saveTransaction();
}
/* -( PhabricatorFulltextInterface )--------------------------------------- */
public function newFulltextEngine() {
return new PhabricatorProjectFulltextEngine();
}
/* -( PhabricatorFerretInterface )--------------------------------------- */
public function newFerretEngine() {
return new PhabricatorProjectFerretEngine();
}
/* -( PhabricatorConduitResultInterface )---------------------------------- */
public function getFieldSpecificationsForConduit() {
return array(
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('name')
->setType('string')
->setDescription(pht('The name of the project.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('slug')
->setType('string')
->setDescription(pht('Primary slug/hashtag.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('subtype')
->setType('string')
->setDescription(pht('Subtype of the project.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('milestone')
->setType('int?')
->setDescription(pht('For milestones, milestone sequence number.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('parent')
->setType('map<string, wild>?')
->setDescription(
pht(
'For subprojects and milestones, a brief description of the '.
'parent project.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('depth')
->setType('int')
->setDescription(
pht(
'For subprojects and milestones, depth of this project in the '.
'tree. Root projects have depth 0.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('icon')
->setType('map<string, wild>')
->setDescription(pht('Information about the project icon.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('color')
->setType('map<string, wild>')
->setDescription(pht('Information about the project color.')),
);
}
public function getFieldValuesForConduit() {
$color_key = $this->getColor();
$color_name = PhabricatorProjectIconSet::getColorName($color_key);
if ($this->isMilestone()) {
$milestone = (int)$this->getMilestoneNumber();
} else {
$milestone = null;
}
$parent = $this->getParentProject();
if ($parent) {
$parent_ref = $parent->getRefForConduit();
} else {
$parent_ref = null;
}
return array(
'name' => $this->getName(),
'slug' => $this->getPrimarySlug(),
'subtype' => $this->getSubtype(),
'milestone' => $milestone,
'depth' => (int)$this->getProjectDepth(),
'parent' => $parent_ref,
'icon' => array(
'key' => $this->getDisplayIconKey(),
'name' => $this->getDisplayIconName(),
'icon' => $this->getDisplayIconIcon(),
),
'color' => array(
'key' => $color_key,
'name' => $color_name,
),
);
}
public function getConduitSearchAttachments() {
return array(
id(new PhabricatorProjectsMembersSearchEngineAttachment())
->setAttachmentKey('members'),
id(new PhabricatorProjectsWatchersSearchEngineAttachment())
->setAttachmentKey('watchers'),
id(new PhabricatorProjectsAncestorsSearchEngineAttachment())
->setAttachmentKey('ancestors'),
);
}
/**
* Get an abbreviated representation of this project for use in providing
* "parent" and "ancestor" information.
*/
public function getRefForConduit() {
return array(
'id' => (int)$this->getID(),
'phid' => $this->getPHID(),
'name' => $this->getName(),
);
}
/* -( PhabricatorColumnProxyInterface )------------------------------------ */
public function getProxyColumnName() {
return $this->getName();
}
public function getProxyColumnIcon() {
return $this->getDisplayIconIcon();
}
public function getProxyColumnClass() {
if ($this->isMilestone()) {
return 'phui-workboard-column-milestone';
}
return null;
}
/* -( PhabricatorEditEngineSubtypeInterface )------------------------------ */
public function getEditEngineSubtype() {
return $this->getSubtype();
}
public function setEditEngineSubtype($value) {
return $this->setSubtype($value);
}
public function newEditEngineSubtypeMap() {
$config = PhabricatorEnv::getEnvConfig('projects.subtypes');
return PhabricatorEditEngineSubtype::newSubtypeMap($config)
->setDatasource(new PhabricatorProjectSubtypeDatasource());
}
public function newSubtypeObject() {
$subtype_key = $this->getEditEngineSubtype();
$subtype_map = $this->newEditEngineSubtypeMap();
return $subtype_map->getSubtype($subtype_key);
}
}
diff --git a/src/applications/transactions/editengine/PhabricatorEditEngine.php b/src/applications/transactions/editengine/PhabricatorEditEngine.php
index ea1d7ed773..d0bb1972b6 100644
--- a/src/applications/transactions/editengine/PhabricatorEditEngine.php
+++ b/src/applications/transactions/editengine/PhabricatorEditEngine.php
@@ -1,2746 +1,2748 @@
<?php
/**
* @task fields Managing Fields
* @task text Display Text
* @task config Edit Engine Configuration
* @task uri Managing URIs
* @task load Creating and Loading Objects
* @task web Responding to Web Requests
* @task edit Responding to Edit Requests
* @task http Responding to HTTP Parameter Requests
* @task conduit Responding to Conduit Requests
*/
abstract class PhabricatorEditEngine
extends Phobject
implements PhabricatorPolicyInterface {
const EDITENGINECONFIG_DEFAULT = 'default';
const SUBTYPE_DEFAULT = 'default';
private $viewer;
private $controller;
private $isCreate;
private $editEngineConfiguration;
private $contextParameters = array();
private $targetObject;
private $page;
private $pages;
private $navigation;
final public function setViewer(PhabricatorUser $viewer) {
$this->viewer = $viewer;
return $this;
}
final public function getViewer() {
return $this->viewer;
}
final public function setController(PhabricatorController $controller) {
$this->controller = $controller;
$this->setViewer($controller->getViewer());
return $this;
}
final public function getController() {
return $this->controller;
}
final public function getEngineKey() {
$key = $this->getPhobjectClassConstant('ENGINECONST', 64);
if (strpos($key, '/') !== false) {
throw new Exception(
pht(
'EditEngine ("%s") contains an invalid key character "/".',
get_class($this)));
}
return $key;
}
final public function getApplication() {
$app_class = $this->getEngineApplicationClass();
return PhabricatorApplication::getByClass($app_class);
}
final public function addContextParameter($key) {
$this->contextParameters[] = $key;
return $this;
}
public function isEngineConfigurable() {
return true;
}
public function isEngineExtensible() {
return true;
}
public function isDefaultQuickCreateEngine() {
return false;
}
public function getDefaultQuickCreateFormKeys() {
$keys = array();
if ($this->isDefaultQuickCreateEngine()) {
$keys[] = self::EDITENGINECONFIG_DEFAULT;
}
foreach ($keys as $idx => $key) {
$keys[$idx] = $this->getEngineKey().'/'.$key;
}
return $keys;
}
public static function splitFullKey($full_key) {
return explode('/', $full_key, 2);
}
public function getQuickCreateOrderVector() {
return id(new PhutilSortVector())
->addString($this->getObjectCreateShortText());
}
/**
* Force the engine to edit a particular object.
*/
public function setTargetObject($target_object) {
$this->targetObject = $target_object;
return $this;
}
public function getTargetObject() {
return $this->targetObject;
}
public function setNavigation(AphrontSideNavFilterView $navigation) {
$this->navigation = $navigation;
return $this;
}
public function getNavigation() {
return $this->navigation;
}
/* -( Managing Fields )---------------------------------------------------- */
abstract public function getEngineApplicationClass();
abstract protected function buildCustomEditFields($object);
public function getFieldsForConfig(
PhabricatorEditEngineConfiguration $config) {
$object = $this->newEditableObject();
$this->editEngineConfiguration = $config;
// This is mostly making sure that we fill in default values.
$this->setIsCreate(true);
return $this->buildEditFields($object);
}
final protected function buildEditFields($object) {
$viewer = $this->getViewer();
$fields = $this->buildCustomEditFields($object);
foreach ($fields as $field) {
$field
->setViewer($viewer)
->setObject($object);
}
$fields = mpull($fields, null, 'getKey');
if ($this->isEngineExtensible()) {
$extensions = PhabricatorEditEngineExtension::getAllEnabledExtensions();
} else {
$extensions = array();
}
// See T13248. Create a template object to provide to extensions. We
// adjust the template to have the intended subtype, so that extensions
// may change behavior based on the form subtype.
$template_object = clone $object;
if ($this->getIsCreate()) {
if ($this->supportsSubtypes()) {
$config = $this->getEditEngineConfiguration();
$subtype = $config->getSubtype();
$template_object->setSubtype($subtype);
}
}
foreach ($extensions as $extension) {
$extension->setViewer($viewer);
if (!$extension->supportsObject($this, $template_object)) {
continue;
}
$extension_fields = $extension->buildCustomEditFields(
$this,
$template_object);
// TODO: Validate this in more detail with a more tailored error.
assert_instances_of($extension_fields, 'PhabricatorEditField');
foreach ($extension_fields as $field) {
$field
->setViewer($viewer)
->setObject($object);
$group_key = $field->getBulkEditGroupKey();
if ($group_key === null) {
$field->setBulkEditGroupKey('extension');
}
}
$extension_fields = mpull($extension_fields, null, 'getKey');
foreach ($extension_fields as $key => $field) {
$fields[$key] = $field;
}
}
$config = $this->getEditEngineConfiguration();
$fields = $this->willConfigureFields($object, $fields);
$fields = $config->applyConfigurationToFields($this, $object, $fields);
$fields = $this->applyPageToFields($object, $fields);
return $fields;
}
protected function willConfigureFields($object, array $fields) {
return $fields;
}
final public function supportsSubtypes() {
try {
$object = $this->newEditableObject();
} catch (Exception $ex) {
return false;
}
return ($object instanceof PhabricatorEditEngineSubtypeInterface);
}
final public function newSubtypeMap() {
return $this->newEditableObject()->newEditEngineSubtypeMap();
}
/* -( Display Text )------------------------------------------------------- */
/**
* @task text
*/
abstract public function getEngineName();
/**
* @task text
*/
abstract protected function getObjectCreateTitleText($object);
/**
* @task text
*/
protected function getFormHeaderText($object) {
$config = $this->getEditEngineConfiguration();
return $config->getName();
}
/**
* @task text
*/
abstract protected function getObjectEditTitleText($object);
/**
* @task text
*/
abstract protected function getObjectCreateShortText();
/**
* @task text
*/
abstract protected function getObjectName();
/**
* @task text
*/
abstract protected function getObjectEditShortText($object);
/**
* @task text
*/
protected function getObjectCreateButtonText($object) {
return $this->getObjectCreateTitleText($object);
}
/**
* @task text
*/
protected function getObjectEditButtonText($object) {
return pht('Save Changes');
}
/**
* @task text
*/
protected function getCommentViewSeriousHeaderText($object) {
return pht('Take Action');
}
/**
* @task text
*/
protected function getCommentViewSeriousButtonText($object) {
return pht('Submit');
}
/**
* @task text
*/
protected function getCommentViewHeaderText($object) {
return $this->getCommentViewSeriousHeaderText($object);
}
/**
* @task text
*/
protected function getCommentViewButtonText($object) {
return $this->getCommentViewSeriousButtonText($object);
}
/**
* @task text
*/
protected function getPageHeader($object) {
return null;
}
/**
* Return a human-readable header describing what this engine is used to do,
* like "Configure Maniphest Task Forms".
*
* @return string Human-readable description of the engine.
* @task text
*/
abstract public function getSummaryHeader();
/**
* Return a human-readable summary of what this engine is used to do.
*
* @return string Human-readable description of the engine.
* @task text
*/
abstract public function getSummaryText();
/* -( Edit Engine Configuration )------------------------------------------ */
protected function supportsEditEngineConfiguration() {
return true;
}
final protected function getEditEngineConfiguration() {
return $this->editEngineConfiguration;
}
public function newConfigurationQuery() {
return id(new PhabricatorEditEngineConfigurationQuery())
->setViewer($this->getViewer())
->withEngineKeys(array($this->getEngineKey()));
}
private function loadEditEngineConfigurationWithQuery(
PhabricatorEditEngineConfigurationQuery $query,
$sort_method) {
if ($sort_method) {
$results = $query->execute();
$results = msort($results, $sort_method);
$result = head($results);
} else {
$result = $query->executeOne();
}
if (!$result) {
return null;
}
$this->editEngineConfiguration = $result;
return $result;
}
private function loadEditEngineConfigurationWithIdentifier($identifier) {
$query = $this->newConfigurationQuery()
->withIdentifiers(array($identifier));
return $this->loadEditEngineConfigurationWithQuery($query, null);
}
private function loadDefaultConfiguration() {
$query = $this->newConfigurationQuery()
->withIdentifiers(
array(
self::EDITENGINECONFIG_DEFAULT,
))
->withIgnoreDatabaseConfigurations(true);
return $this->loadEditEngineConfigurationWithQuery($query, null);
}
private function loadDefaultCreateConfiguration() {
$query = $this->newConfigurationQuery()
->withIsDefault(true)
->withIsDisabled(false);
return $this->loadEditEngineConfigurationWithQuery(
$query,
'getCreateSortKey');
}
public function loadDefaultEditConfiguration($object) {
$query = $this->newConfigurationQuery()
->withIsEdit(true)
->withIsDisabled(false);
// If this object supports subtyping, we edit it with a form of the same
// subtype: so "bug" tasks get edited with "bug" forms.
if ($object instanceof PhabricatorEditEngineSubtypeInterface) {
$query->withSubtypes(
array(
$object->getEditEngineSubtype(),
));
}
return $this->loadEditEngineConfigurationWithQuery(
$query,
'getEditSortKey');
}
final public function getBuiltinEngineConfigurations() {
$configurations = $this->newBuiltinEngineConfigurations();
if (!$configurations) {
throw new Exception(
pht(
'EditEngine ("%s") returned no builtin engine configurations, but '.
'an edit engine must have at least one configuration.',
get_class($this)));
}
assert_instances_of($configurations, 'PhabricatorEditEngineConfiguration');
$has_default = false;
foreach ($configurations as $config) {
if ($config->getBuiltinKey() == self::EDITENGINECONFIG_DEFAULT) {
$has_default = true;
}
}
if (!$has_default) {
$first = head($configurations);
if (!$first->getBuiltinKey()) {
$first
->setBuiltinKey(self::EDITENGINECONFIG_DEFAULT)
->setIsDefault(true)
->setIsEdit(true);
- if (!strlen($first->getName())) {
+ $first_name = $first->getName();
+
+ if ($first_name === null || $first_name === '') {
$first->setName($this->getObjectCreateShortText());
}
} else {
throw new Exception(
pht(
'EditEngine ("%s") returned builtin engine configurations, '.
'but none are marked as default and the first configuration has '.
'a different builtin key already. Mark a builtin as default or '.
'omit the key from the first configuration',
get_class($this)));
}
}
$builtins = array();
foreach ($configurations as $key => $config) {
$builtin_key = $config->getBuiltinKey();
if ($builtin_key === null) {
throw new Exception(
pht(
'EditEngine ("%s") returned builtin engine configurations, '.
'but one (with key "%s") is missing a builtin key. Provide a '.
'builtin key for each configuration (you can omit it from the '.
'first configuration in the list to automatically assign the '.
'default key).',
get_class($this),
$key));
}
if (isset($builtins[$builtin_key])) {
throw new Exception(
pht(
'EditEngine ("%s") returned builtin engine configurations, '.
'but at least two specify the same builtin key ("%s"). Engines '.
'must have unique builtin keys.',
get_class($this),
$builtin_key));
}
$builtins[$builtin_key] = $config;
}
return $builtins;
}
protected function newBuiltinEngineConfigurations() {
return array(
$this->newConfiguration(),
);
}
final protected function newConfiguration() {
return PhabricatorEditEngineConfiguration::initializeNewConfiguration(
$this->getViewer(),
$this);
}
/* -( Managing URIs )------------------------------------------------------ */
/**
* @task uri
*/
abstract protected function getObjectViewURI($object);
/**
* @task uri
*/
protected function getObjectCreateCancelURI($object) {
return $this->getApplication()->getApplicationURI();
}
/**
* @task uri
*/
protected function getEditorURI() {
return $this->getApplication()->getApplicationURI('edit/');
}
/**
* @task uri
*/
protected function getObjectEditCancelURI($object) {
return $this->getObjectViewURI($object);
}
/**
* @task uri
*/
public function getCreateURI($form_key) {
try {
$create_uri = $this->getEditURI(null, "form/{$form_key}/");
} catch (Exception $ex) {
$create_uri = null;
}
return $create_uri;
}
/**
* @task uri
*/
public function getEditURI($object = null, $path = null) {
$parts = array();
$parts[] = $this->getEditorURI();
if ($object && $object->getID()) {
$parts[] = $object->getID().'/';
}
if ($path !== null) {
$parts[] = $path;
}
return implode('', $parts);
}
public function getEffectiveObjectViewURI($object) {
if ($this->getIsCreate()) {
return $this->getObjectViewURI($object);
}
$page = $this->getSelectedPage();
if ($page) {
$view_uri = $page->getViewURI();
if ($view_uri !== null) {
return $view_uri;
}
}
return $this->getObjectViewURI($object);
}
public function getEffectiveObjectEditDoneURI($object) {
return $this->getEffectiveObjectViewURI($object);
}
public function getEffectiveObjectEditCancelURI($object) {
$page = $this->getSelectedPage();
if ($page) {
$view_uri = $page->getViewURI();
if ($view_uri !== null) {
return $view_uri;
}
}
return $this->getObjectEditCancelURI($object);
}
/* -( Creating and Loading Objects )--------------------------------------- */
/**
* Initialize a new object for creation.
*
* @return object Newly initialized object.
* @task load
*/
abstract protected function newEditableObject();
/**
* Build an empty query for objects.
*
* @return PhabricatorPolicyAwareQuery Query.
* @task load
*/
abstract protected function newObjectQuery();
/**
* Test if this workflow is creating a new object or editing an existing one.
*
* @return bool True if a new object is being created.
* @task load
*/
final public function getIsCreate() {
return $this->isCreate;
}
/**
* Initialize a new object for object creation via Conduit.
*
* @return object Newly initialized object.
* @param list<wild> Raw transactions.
* @task load
*/
protected function newEditableObjectFromConduit(array $raw_xactions) {
return $this->newEditableObject();
}
/**
* Initialize a new object for documentation creation.
*
* @return object Newly initialized object.
* @task load
*/
protected function newEditableObjectForDocumentation() {
return $this->newEditableObject();
}
/**
* Flag this workflow as a create or edit.
*
* @param bool True if this is a create workflow.
* @return this
* @task load
*/
private function setIsCreate($is_create) {
$this->isCreate = $is_create;
return $this;
}
/**
* Try to load an object by ID, PHID, or monogram. This is done primarily
* to make Conduit a little easier to use.
*
* @param wild ID, PHID, or monogram.
* @param list<const> List of required capability constants, or omit for
* defaults.
* @return object Corresponding editable object.
* @task load
*/
private function newObjectFromIdentifier(
$identifier,
array $capabilities = array()) {
if (is_int($identifier) || ctype_digit($identifier)) {
$object = $this->newObjectFromID($identifier, $capabilities);
if (!$object) {
throw new Exception(
pht(
'No object exists with ID "%s".',
$identifier));
}
return $object;
}
$type_unknown = PhabricatorPHIDConstants::PHID_TYPE_UNKNOWN;
if (phid_get_type($identifier) != $type_unknown) {
$object = $this->newObjectFromPHID($identifier, $capabilities);
if (!$object) {
throw new Exception(
pht(
'No object exists with PHID "%s".',
$identifier));
}
return $object;
}
$target = id(new PhabricatorObjectQuery())
->setViewer($this->getViewer())
->withNames(array($identifier))
->executeOne();
if (!$target) {
throw new Exception(
pht(
'Monogram "%s" does not identify a valid object.',
$identifier));
}
$expect = $this->newEditableObject();
$expect_class = get_class($expect);
$target_class = get_class($target);
if ($expect_class !== $target_class) {
throw new Exception(
pht(
'Monogram "%s" identifies an object of the wrong type. Loaded '.
'object has class "%s", but this editor operates on objects of '.
'type "%s".',
$identifier,
$target_class,
$expect_class));
}
// Load the object by PHID using this engine's standard query. This makes
// sure it's really valid, goes through standard policy check logic, and
// picks up any `need...()` clauses we want it to load with.
$object = $this->newObjectFromPHID($target->getPHID(), $capabilities);
if (!$object) {
throw new Exception(
pht(
'Failed to reload object identified by monogram "%s" when '.
'querying by PHID.',
$identifier));
}
return $object;
}
/**
* Load an object by ID.
*
* @param int Object ID.
* @param list<const> List of required capability constants, or omit for
* defaults.
* @return object|null Object, or null if no such object exists.
* @task load
*/
private function newObjectFromID($id, array $capabilities = array()) {
$query = $this->newObjectQuery()
->withIDs(array($id));
return $this->newObjectFromQuery($query, $capabilities);
}
/**
* Load an object by PHID.
*
* @param phid Object PHID.
* @param list<const> List of required capability constants, or omit for
* defaults.
* @return object|null Object, or null if no such object exists.
* @task load
*/
private function newObjectFromPHID($phid, array $capabilities = array()) {
$query = $this->newObjectQuery()
->withPHIDs(array($phid));
return $this->newObjectFromQuery($query, $capabilities);
}
/**
* Load an object given a configured query.
*
* @param PhabricatorPolicyAwareQuery Configured query.
* @param list<const> List of required capability constants, or omit for
* defaults.
* @return object|null Object, or null if no such object exists.
* @task load
*/
private function newObjectFromQuery(
PhabricatorPolicyAwareQuery $query,
array $capabilities = array()) {
$viewer = $this->getViewer();
if (!$capabilities) {
$capabilities = array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
$object = $query
->setViewer($viewer)
->requireCapabilities($capabilities)
->executeOne();
if (!$object) {
return null;
}
return $object;
}
/**
* Verify that an object is appropriate for editing.
*
* @param wild Loaded value.
* @return void
* @task load
*/
private function validateObject($object) {
if (!$object || !is_object($object)) {
throw new Exception(
pht(
'EditEngine "%s" created or loaded an invalid object: object must '.
'actually be an object, but is of some other type ("%s").',
get_class($this),
gettype($object)));
}
if (!($object instanceof PhabricatorApplicationTransactionInterface)) {
throw new Exception(
pht(
'EditEngine "%s" created or loaded an invalid object: object (of '.
'class "%s") must implement "%s", but does not.',
get_class($this),
get_class($object),
'PhabricatorApplicationTransactionInterface'));
}
}
/* -( Responding to Web Requests )----------------------------------------- */
final public function buildResponse() {
$viewer = $this->getViewer();
$controller = $this->getController();
$request = $controller->getRequest();
$action = $this->getEditAction();
$capabilities = array();
$use_default = false;
$require_create = true;
switch ($action) {
case 'comment':
$capabilities = array(
PhabricatorPolicyCapability::CAN_VIEW,
);
$use_default = true;
break;
case 'parameters':
$use_default = true;
break;
case 'nodefault':
case 'nocreate':
case 'nomanage':
$require_create = false;
break;
default:
break;
}
$object = $this->getTargetObject();
if (!$object) {
$id = $request->getURIData('id');
if ($id) {
$this->setIsCreate(false);
$object = $this->newObjectFromID($id, $capabilities);
if (!$object) {
return new Aphront404Response();
}
} else {
// Make sure the viewer has permission to create new objects of
// this type if we're going to create a new object.
if ($require_create) {
$this->requireCreateCapability();
}
$this->setIsCreate(true);
$object = $this->newEditableObject();
}
} else {
$id = $object->getID();
}
$this->validateObject($object);
if ($use_default) {
$config = $this->loadDefaultConfiguration();
if (!$config) {
return new Aphront404Response();
}
} else {
$form_key = $request->getURIData('formKey');
if (strlen($form_key)) {
$config = $this->loadEditEngineConfigurationWithIdentifier($form_key);
if (!$config) {
return new Aphront404Response();
}
if ($id && !$config->getIsEdit()) {
return $this->buildNotEditFormRespose($object, $config);
}
} else {
if ($id) {
$config = $this->loadDefaultEditConfiguration($object);
if (!$config) {
return $this->buildNoEditResponse($object);
}
} else {
$config = $this->loadDefaultCreateConfiguration();
if (!$config) {
return $this->buildNoCreateResponse($object);
}
}
}
}
if ($config->getIsDisabled()) {
return $this->buildDisabledFormResponse($object, $config);
}
$page_key = $request->getURIData('pageKey');
if (!strlen($page_key)) {
$pages = $this->getPages($object);
if ($pages) {
$page_key = head_key($pages);
}
}
if (strlen($page_key)) {
$page = $this->selectPage($object, $page_key);
if (!$page) {
return new Aphront404Response();
}
}
switch ($action) {
case 'parameters':
return $this->buildParametersResponse($object);
case 'nodefault':
return $this->buildNoDefaultResponse($object);
case 'nocreate':
return $this->buildNoCreateResponse($object);
case 'nomanage':
return $this->buildNoManageResponse($object);
case 'comment':
return $this->buildCommentResponse($object);
default:
return $this->buildEditResponse($object);
}
}
private function buildCrumbs($object, $final = false) {
$controller = $this->getController();
$crumbs = $controller->buildApplicationCrumbsForEditEngine();
if ($this->getIsCreate()) {
$create_text = $this->getObjectCreateShortText();
if ($final) {
$crumbs->addTextCrumb($create_text);
} else {
$edit_uri = $this->getEditURI($object);
$crumbs->addTextCrumb($create_text, $edit_uri);
}
} else {
$crumbs->addTextCrumb(
$this->getObjectEditShortText($object),
$this->getEffectiveObjectViewURI($object));
$edit_text = pht('Edit');
if ($final) {
$crumbs->addTextCrumb($edit_text);
} else {
$edit_uri = $this->getEditURI($object);
$crumbs->addTextCrumb($edit_text, $edit_uri);
}
}
return $crumbs;
}
private function buildEditResponse($object) {
$viewer = $this->getViewer();
$controller = $this->getController();
$request = $controller->getRequest();
$fields = $this->buildEditFields($object);
$template = $object->getApplicationTransactionTemplate();
$page_state = new PhabricatorEditEnginePageState();
if ($this->getIsCreate()) {
$cancel_uri = $this->getObjectCreateCancelURI($object);
$submit_button = $this->getObjectCreateButtonText($object);
$page_state->setIsCreate(true);
} else {
$cancel_uri = $this->getEffectiveObjectEditCancelURI($object);
$submit_button = $this->getObjectEditButtonText($object);
}
$config = $this->getEditEngineConfiguration()
->attachEngine($this);
// NOTE: Don't prompt users to override locks when creating objects,
// even if the default settings would create a locked object.
$can_interact = PhabricatorPolicyFilter::canInteract($viewer, $object);
if (!$can_interact &&
!$this->getIsCreate() &&
!$request->getBool('editEngine') &&
!$request->getBool('overrideLock')) {
$lock = PhabricatorEditEngineLock::newForObject($viewer, $object);
$dialog = $this->getController()
->newDialog()
->addHiddenInput('overrideLock', true)
->setDisableWorkflowOnSubmit(true)
->addCancelButton($cancel_uri);
return $lock->willPromptUserForLockOverrideWithDialog($dialog);
}
$validation_exception = null;
if ($request->isFormOrHisecPost() && $request->getBool('editEngine')) {
$page_state->setIsSubmit(true);
$submit_fields = $fields;
foreach ($submit_fields as $key => $field) {
if (!$field->shouldGenerateTransactionsFromSubmit()) {
unset($submit_fields[$key]);
continue;
}
}
// Before we read the submitted values, store a copy of what we would
// use if the form was empty so we can figure out which transactions are
// just setting things to their default values for the current form.
$defaults = array();
foreach ($submit_fields as $key => $field) {
$defaults[$key] = $field->getValueForTransaction();
}
foreach ($submit_fields as $key => $field) {
$field->setIsSubmittedForm(true);
if (!$field->shouldReadValueFromSubmit()) {
continue;
}
$field->readValueFromSubmit($request);
}
$xactions = array();
if ($this->getIsCreate()) {
$xactions[] = id(clone $template)
->setTransactionType(PhabricatorTransactions::TYPE_CREATE);
if ($this->supportsSubtypes()) {
$xactions[] = id(clone $template)
->setTransactionType(PhabricatorTransactions::TYPE_SUBTYPE)
->setNewValue($config->getSubtype());
}
}
foreach ($submit_fields as $key => $field) {
$field_value = $field->getValueForTransaction();
$type_xactions = $field->generateTransactions(
clone $template,
array(
'value' => $field_value,
));
foreach ($type_xactions as $type_xaction) {
$default = $defaults[$key];
if ($default === $field->getValueForTransaction()) {
$type_xaction->setIsDefaultTransaction(true);
}
$xactions[] = $type_xaction;
}
}
$editor = $object->getApplicationTransactionEditor()
->setActor($viewer)
->setContentSourceFromRequest($request)
->setCancelURI($cancel_uri)
->setContinueOnNoEffect(true);
try {
$xactions = $this->willApplyTransactions($object, $xactions);
$editor->applyTransactions($object, $xactions);
$this->didApplyTransactions($object, $xactions);
return $this->newEditResponse($request, $object, $xactions);
} catch (PhabricatorApplicationTransactionValidationException $ex) {
$validation_exception = $ex;
foreach ($fields as $field) {
$message = $this->getValidationExceptionShortMessage($ex, $field);
if ($message === null) {
continue;
}
$field->setControlError($message);
}
$page_state->setIsError(true);
}
} else {
if ($this->getIsCreate()) {
$template = $request->getStr('template');
if (strlen($template)) {
$template_object = $this->newObjectFromIdentifier(
$template,
array(
PhabricatorPolicyCapability::CAN_VIEW,
));
if (!$template_object) {
return new Aphront404Response();
}
} else {
$template_object = null;
}
if ($template_object) {
$copy_fields = $this->buildEditFields($template_object);
$copy_fields = mpull($copy_fields, null, 'getKey');
foreach ($copy_fields as $copy_key => $copy_field) {
if (!$copy_field->getIsCopyable()) {
unset($copy_fields[$copy_key]);
}
}
} else {
$copy_fields = array();
}
foreach ($fields as $field) {
if (!$field->shouldReadValueFromRequest()) {
continue;
}
$field_key = $field->getKey();
if (isset($copy_fields[$field_key])) {
$field->readValueFromField($copy_fields[$field_key]);
}
$field->readValueFromRequest($request);
}
}
}
$action_button = $this->buildEditFormActionButton($object);
if ($this->getIsCreate()) {
$header_text = $this->getFormHeaderText($object);
} else {
$header_text = $this->getObjectEditTitleText($object);
}
$show_preview = !$request->isAjax();
if ($show_preview) {
$previews = array();
foreach ($fields as $field) {
$preview = $field->getPreviewPanel();
if (!$preview) {
continue;
}
$control_id = $field->getControlID();
$preview
->setControlID($control_id)
->setPreviewURI('/transactions/remarkuppreview/');
$previews[] = $preview;
}
} else {
$previews = array();
}
$form = $this->buildEditForm($object, $fields);
$crumbs = $this->buildCrumbs($object, $final = true);
$crumbs->setBorder(true);
if ($request->isAjax()) {
return $this->getController()
->newDialog()
->setWidth(AphrontDialogView::WIDTH_FULL)
->setTitle($header_text)
->setValidationException($validation_exception)
->appendForm($form)
->addCancelButton($cancel_uri)
->addSubmitButton($submit_button);
}
$box_header = id(new PHUIHeaderView())
->setHeader($header_text);
if ($action_button) {
$box_header->addActionLink($action_button);
}
$request_submit_key = $request->getSubmitKey();
$engine_submit_key = $this->getEditEngineSubmitKey();
if ($request_submit_key === $engine_submit_key) {
$page_state->setIsSubmit(true);
$page_state->setIsSave(true);
}
$head = $this->newEditFormHeadContent($page_state);
$tail = $this->newEditFormTailContent($page_state);
$box = id(new PHUIObjectBoxView())
->setUser($viewer)
->setHeader($box_header)
->setValidationException($validation_exception)
->setBackground(PHUIObjectBoxView::WHITE_CONFIG)
->appendChild($form);
$content = array(
$head,
$box,
$previews,
$tail,
);
$view = new PHUITwoColumnView();
$page_header = $this->getPageHeader($object);
if ($page_header) {
$view->setHeader($page_header);
}
$view->setFooter($content);
$page = $controller->newPage()
->setTitle($header_text)
->setCrumbs($crumbs)
->appendChild($view);
$navigation = $this->getNavigation();
if ($navigation) {
$page->setNavigation($navigation);
}
return $page;
}
protected function newEditFormHeadContent(
PhabricatorEditEnginePageState $state) {
return null;
}
protected function newEditFormTailContent(
PhabricatorEditEnginePageState $state) {
return null;
}
protected function newEditResponse(
AphrontRequest $request,
$object,
array $xactions) {
$submit_cookie = PhabricatorCookies::COOKIE_SUBMIT;
$submit_key = $this->getEditEngineSubmitKey();
$request->setTemporaryCookie($submit_cookie, $submit_key);
return id(new AphrontRedirectResponse())
->setURI($this->getEffectiveObjectEditDoneURI($object));
}
private function getEditEngineSubmitKey() {
return 'edit-engine/'.$this->getEngineKey();
}
private function buildEditForm($object, array $fields) {
$viewer = $this->getViewer();
$controller = $this->getController();
$request = $controller->getRequest();
$fields = $this->willBuildEditForm($object, $fields);
$request_path = $request->getPath();
$form = id(new AphrontFormView())
->setUser($viewer)
->setAction($request_path)
->addHiddenInput('editEngine', 'true');
foreach ($this->contextParameters as $param) {
$form->addHiddenInput($param, $request->getStr($param));
}
$requires_mfa = false;
if ($object instanceof PhabricatorEditEngineMFAInterface) {
$mfa_engine = PhabricatorEditEngineMFAEngine::newEngineForObject($object)
->setViewer($viewer);
$requires_mfa = $mfa_engine->shouldRequireMFA();
}
if ($requires_mfa) {
$message = pht(
'You will be required to provide multi-factor credentials to make '.
'changes.');
$form->appendChild(
id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_MFA)
->setErrors(array($message)));
// TODO: This should also set workflow on the form, so the user doesn't
// lose any form data if they "Cancel". However, Maniphest currently
// overrides "newEditResponse()" if the request is Ajax and returns a
// bag of view data. This can reasonably be cleaned up when workboards
// get their next iteration.
}
foreach ($fields as $field) {
if (!$field->getIsFormField()) {
continue;
}
$field->appendToForm($form);
}
if ($this->getIsCreate()) {
$cancel_uri = $this->getObjectCreateCancelURI($object);
$submit_button = $this->getObjectCreateButtonText($object);
} else {
$cancel_uri = $this->getEffectiveObjectEditCancelURI($object);
$submit_button = $this->getObjectEditButtonText($object);
}
if (!$request->isAjax()) {
$buttons = id(new AphrontFormSubmitControl())
->setValue($submit_button);
if ($cancel_uri) {
$buttons->addCancelButton($cancel_uri);
}
$form->appendControl($buttons);
}
return $form;
}
protected function willBuildEditForm($object, array $fields) {
return $fields;
}
private function buildEditFormActionButton($object) {
if (!$this->isEngineConfigurable()) {
return null;
}
$viewer = $this->getViewer();
$action_view = id(new PhabricatorActionListView())
->setUser($viewer);
foreach ($this->buildEditFormActions($object) as $action) {
$action_view->addAction($action);
}
$action_button = id(new PHUIButtonView())
->setTag('a')
->setText(pht('Configure Form'))
->setHref('#')
->setIcon('fa-gear')
->setDropdownMenu($action_view);
return $action_button;
}
private function buildEditFormActions($object) {
$actions = array();
if ($this->supportsEditEngineConfiguration()) {
$engine_key = $this->getEngineKey();
$config = $this->getEditEngineConfiguration();
$can_manage = PhabricatorPolicyFilter::hasCapability(
$this->getViewer(),
$config,
PhabricatorPolicyCapability::CAN_EDIT);
if ($can_manage) {
$manage_uri = $config->getURI();
} else {
$manage_uri = $this->getEditURI(null, 'nomanage/');
}
$view_uri = "/transactions/editengine/{$engine_key}/";
$actions[] = id(new PhabricatorActionView())
->setLabel(true)
->setName(pht('Configuration'));
$actions[] = id(new PhabricatorActionView())
->setName(pht('View Form Configurations'))
->setIcon('fa-list-ul')
->setHref($view_uri);
$actions[] = id(new PhabricatorActionView())
->setName(pht('Edit Form Configuration'))
->setIcon('fa-pencil')
->setHref($manage_uri)
->setDisabled(!$can_manage)
->setWorkflow(!$can_manage);
}
$actions[] = id(new PhabricatorActionView())
->setLabel(true)
->setName(pht('Documentation'));
$actions[] = id(new PhabricatorActionView())
->setName(pht('Using HTTP Parameters'))
->setIcon('fa-book')
->setHref($this->getEditURI($object, 'parameters/'));
$doc_href = PhabricatorEnv::getDoclink('User Guide: Customizing Forms');
$actions[] = id(new PhabricatorActionView())
->setName(pht('User Guide: Customizing Forms'))
->setIcon('fa-book')
->setHref($doc_href);
return $actions;
}
public function newNUXButton($text) {
$specs = $this->newCreateActionSpecifications(array());
$head = head($specs);
return id(new PHUIButtonView())
->setTag('a')
->setText($text)
->setHref($head['uri'])
->setDisabled($head['disabled'])
->setWorkflow($head['workflow'])
->setColor(PHUIButtonView::GREEN);
}
final public function addActionToCrumbs(
PHUICrumbsView $crumbs,
array $parameters = array()) {
$viewer = $this->getViewer();
$specs = $this->newCreateActionSpecifications($parameters);
$head = head($specs);
$menu_uri = $head['uri'];
$dropdown = null;
if (count($specs) > 1) {
$menu_icon = 'fa-caret-square-o-down';
$menu_name = $this->getObjectCreateShortText();
$workflow = false;
$disabled = false;
$dropdown = id(new PhabricatorActionListView())
->setUser($viewer);
foreach ($specs as $spec) {
$dropdown->addAction(
id(new PhabricatorActionView())
->setName($spec['name'])
->setIcon($spec['icon'])
->setHref($spec['uri'])
->setDisabled($head['disabled'])
->setWorkflow($head['workflow']));
}
} else {
$menu_icon = $head['icon'];
$menu_name = $head['name'];
$workflow = $head['workflow'];
$disabled = $head['disabled'];
}
$action = id(new PHUIListItemView())
->setName($menu_name)
->setHref($menu_uri)
->setIcon($menu_icon)
->setWorkflow($workflow)
->setDisabled($disabled);
if ($dropdown) {
$action->setDropdownMenu($dropdown);
}
$crumbs->addAction($action);
}
/**
* Build a raw description of available "Create New Object" UI options so
* other methods can build menus or buttons.
*/
public function newCreateActionSpecifications(array $parameters) {
$viewer = $this->getViewer();
$can_create = $this->hasCreateCapability();
if ($can_create) {
$configs = $this->loadUsableConfigurationsForCreate();
} else {
$configs = array();
}
$disabled = false;
$workflow = false;
$menu_icon = 'fa-plus-square';
$specs = array();
if (!$configs) {
if ($viewer->isLoggedIn()) {
$disabled = true;
} else {
// If the viewer isn't logged in, assume they'll get hit with a login
// dialog and are likely able to create objects after they log in.
$disabled = false;
}
$workflow = true;
if ($can_create) {
$create_uri = $this->getEditURI(null, 'nodefault/');
} else {
$create_uri = $this->getEditURI(null, 'nocreate/');
}
$specs[] = array(
'name' => $this->getObjectCreateShortText(),
'uri' => $create_uri,
'icon' => $menu_icon,
'disabled' => $disabled,
'workflow' => $workflow,
);
} else {
foreach ($configs as $config) {
$config_uri = $config->getCreateURI();
if ($parameters) {
$config_uri = (string)new PhutilURI($config_uri, $parameters);
}
$specs[] = array(
'name' => $config->getDisplayName(),
'uri' => $config_uri,
'icon' => 'fa-plus',
'disabled' => false,
'workflow' => false,
);
}
}
return $specs;
}
final public function buildEditEngineCommentView($object) {
$config = $this->loadDefaultEditConfiguration($object);
if (!$config) {
// TODO: This just nukes the entire comment form if you don't have access
// to any edit forms. We might want to tailor this UX a bit.
return id(new PhabricatorApplicationTransactionCommentView())
->setNoPermission(true);
}
$viewer = $this->getViewer();
$can_interact = PhabricatorPolicyFilter::canInteract($viewer, $object);
if (!$can_interact) {
$lock = PhabricatorEditEngineLock::newForObject($viewer, $object);
return id(new PhabricatorApplicationTransactionCommentView())
->setEditEngineLock($lock);
}
$object_phid = $object->getPHID();
$is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business');
if ($is_serious) {
$header_text = $this->getCommentViewSeriousHeaderText($object);
$button_text = $this->getCommentViewSeriousButtonText($object);
} else {
$header_text = $this->getCommentViewHeaderText($object);
$button_text = $this->getCommentViewButtonText($object);
}
$comment_uri = $this->getEditURI($object, 'comment/');
$requires_mfa = false;
if ($object instanceof PhabricatorEditEngineMFAInterface) {
$mfa_engine = PhabricatorEditEngineMFAEngine::newEngineForObject($object)
->setViewer($viewer);
$requires_mfa = $mfa_engine->shouldRequireMFA();
}
$view = id(new PhabricatorApplicationTransactionCommentView())
->setUser($viewer)
->setObjectPHID($object_phid)
->setHeaderText($header_text)
->setAction($comment_uri)
->setRequiresMFA($requires_mfa)
->setSubmitButtonName($button_text);
$draft = PhabricatorVersionedDraft::loadDraft(
$object_phid,
$viewer->getPHID());
if ($draft) {
$view->setVersionedDraft($draft);
}
$view->setCurrentVersion($this->loadDraftVersion($object));
$fields = $this->buildEditFields($object);
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$object,
PhabricatorPolicyCapability::CAN_EDIT);
$comment_actions = array();
foreach ($fields as $field) {
if (!$field->shouldGenerateTransactionsFromComment()) {
continue;
}
if (!$can_edit) {
if (!$field->getCanApplyWithoutEditCapability()) {
continue;
}
}
$comment_action = $field->getCommentAction();
if (!$comment_action) {
continue;
}
$key = $comment_action->getKey();
// TODO: Validate these better.
$comment_actions[$key] = $comment_action;
}
$comment_actions = msortv($comment_actions, 'getSortVector');
$view->setCommentActions($comment_actions);
$comment_groups = $this->newCommentActionGroups();
$view->setCommentActionGroups($comment_groups);
return $view;
}
protected function loadDraftVersion($object) {
$viewer = $this->getViewer();
if (!$viewer->isLoggedIn()) {
return null;
}
$template = $object->getApplicationTransactionTemplate();
$conn_r = $template->establishConnection('r');
// Find the most recent transaction the user has written. We'll use this
// as a version number to make sure that out-of-date drafts get discarded.
$result = queryfx_one(
$conn_r,
'SELECT id AS version FROM %T
WHERE objectPHID = %s AND authorPHID = %s
ORDER BY id DESC LIMIT 1',
$template->getTableName(),
$object->getPHID(),
$viewer->getPHID());
if ($result) {
return (int)$result['version'];
} else {
return null;
}
}
/* -( Responding to HTTP Parameter Requests )------------------------------ */
/**
* Respond to a request for documentation on HTTP parameters.
*
* @param object Editable object.
* @return AphrontResponse Response object.
* @task http
*/
private function buildParametersResponse($object) {
$controller = $this->getController();
$viewer = $this->getViewer();
$request = $controller->getRequest();
$fields = $this->buildEditFields($object);
$crumbs = $this->buildCrumbs($object);
$crumbs->addTextCrumb(pht('HTTP Parameters'));
$crumbs->setBorder(true);
$header_text = pht(
'HTTP Parameters: %s',
$this->getObjectCreateShortText());
$header = id(new PHUIHeaderView())
->setHeader($header_text);
$help_view = id(new PhabricatorApplicationEditHTTPParameterHelpView())
->setUser($viewer)
->setFields($fields);
$document = id(new PHUIDocumentView())
->setUser($viewer)
->setHeader($header)
->appendChild($help_view);
return $controller->newPage()
->setTitle(pht('HTTP Parameters'))
->setCrumbs($crumbs)
->appendChild($document);
}
private function buildError($object, $title, $body) {
$cancel_uri = $this->getObjectCreateCancelURI($object);
$dialog = $this->getController()
->newDialog()
->addCancelButton($cancel_uri);
if ($title !== null) {
$dialog->setTitle($title);
}
if ($body !== null) {
$dialog->appendParagraph($body);
}
return $dialog;
}
private function buildNoDefaultResponse($object) {
return $this->buildError(
$object,
pht('No Default Create Forms'),
pht(
'This application is not configured with any forms for creating '.
'objects that are visible to you and enabled.'));
}
private function buildNoCreateResponse($object) {
return $this->buildError(
$object,
pht('No Create Permission'),
pht('You do not have permission to create these objects.'));
}
private function buildNoManageResponse($object) {
return $this->buildError(
$object,
pht('No Manage Permission'),
pht(
'You do not have permission to configure forms for this '.
'application.'));
}
private function buildNoEditResponse($object) {
return $this->buildError(
$object,
pht('No Edit Forms'),
pht(
'You do not have access to any forms which are enabled and marked '.
'as edit forms.'));
}
private function buildNotEditFormRespose($object, $config) {
return $this->buildError(
$object,
pht('Not an Edit Form'),
pht(
'This form ("%s") is not marked as an edit form, so '.
'it can not be used to edit objects.',
$config->getName()));
}
private function buildDisabledFormResponse($object, $config) {
return $this->buildError(
$object,
pht('Form Disabled'),
pht(
'This form ("%s") has been disabled, so it can not be used.',
$config->getName()));
}
private function buildLockedObjectResponse($object) {
$dialog = $this->buildError($object, null, null);
$viewer = $this->getViewer();
$lock = PhabricatorEditEngineLock::newForObject($viewer, $object);
return $lock->willBlockUserInteractionWithDialog($dialog);
}
private function buildCommentResponse($object) {
$viewer = $this->getViewer();
if ($this->getIsCreate()) {
return new Aphront404Response();
}
$controller = $this->getController();
$request = $controller->getRequest();
// NOTE: We handle hisec inside the transaction editor with "Sign With MFA"
// comment actions.
if (!$request->isFormOrHisecPost()) {
return new Aphront400Response();
}
$can_interact = PhabricatorPolicyFilter::canInteract($viewer, $object);
if (!$can_interact) {
return $this->buildLockedObjectResponse($object);
}
$config = $this->loadDefaultEditConfiguration($object);
if (!$config) {
return new Aphront404Response();
}
$fields = $this->buildEditFields($object);
$is_preview = $request->isPreviewRequest();
$view_uri = $this->getEffectiveObjectViewURI($object);
$template = $object->getApplicationTransactionTemplate();
$comment_template = $template->getApplicationTransactionCommentObject();
$comment_text = $request->getStr('comment');
$actions = $request->getStr('editengine.actions');
if ($actions) {
$actions = phutil_json_decode($actions);
}
if ($is_preview) {
$version_key = PhabricatorVersionedDraft::KEY_VERSION;
$request_version = $request->getInt($version_key);
$current_version = $this->loadDraftVersion($object);
if ($request_version >= $current_version) {
$draft = PhabricatorVersionedDraft::loadOrCreateDraft(
$object->getPHID(),
$viewer->getPHID(),
$current_version);
$is_empty = (!strlen($comment_text) && !$actions);
$draft
->setProperty('comment', $comment_text)
->setProperty('actions', $actions)
->save();
$draft_engine = $this->newDraftEngine($object);
if ($draft_engine) {
$draft_engine
->setVersionedDraft($draft)
->synchronize();
}
}
}
$xactions = array();
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$object,
PhabricatorPolicyCapability::CAN_EDIT);
if ($actions) {
$action_map = array();
foreach ($actions as $action) {
$type = idx($action, 'type');
if (!$type) {
continue;
}
if (empty($fields[$type])) {
continue;
}
$action_map[$type] = $action;
}
foreach ($action_map as $type => $action) {
$field = $fields[$type];
if (!$field->shouldGenerateTransactionsFromComment()) {
continue;
}
// If you don't have edit permission on the object, you're limited in
// which actions you can take via the comment form. Most actions
// need edit permission, but some actions (like "Accept Revision")
// can be applied by anyone with view permission.
if (!$can_edit) {
if (!$field->getCanApplyWithoutEditCapability()) {
// We know the user doesn't have the capability, so this will
// raise a policy exception.
PhabricatorPolicyFilter::requireCapability(
$viewer,
$object,
PhabricatorPolicyCapability::CAN_EDIT);
}
}
if (array_key_exists('initialValue', $action)) {
$field->setInitialValue($action['initialValue']);
}
$field->readValueFromComment(idx($action, 'value'));
$type_xactions = $field->generateTransactions(
clone $template,
array(
'value' => $field->getValueForTransaction(),
));
foreach ($type_xactions as $type_xaction) {
$xactions[] = $type_xaction;
}
}
}
$auto_xactions = $this->newAutomaticCommentTransactions($object);
foreach ($auto_xactions as $xaction) {
$xactions[] = $xaction;
}
if (strlen($comment_text) || !$xactions) {
$xactions[] = id(clone $template)
->setTransactionType(PhabricatorTransactions::TYPE_COMMENT)
->attachComment(
id(clone $comment_template)
->setContent($comment_text));
}
$editor = $object->getApplicationTransactionEditor()
->setActor($viewer)
->setContinueOnNoEffect($request->isContinueRequest())
->setContinueOnMissingFields(true)
->setContentSourceFromRequest($request)
->setCancelURI($view_uri)
->setRaiseWarnings(!$request->getBool('editEngine.warnings'))
->setIsPreview($is_preview);
try {
$xactions = $editor->applyTransactions($object, $xactions);
} catch (PhabricatorApplicationTransactionValidationException $ex) {
return id(new PhabricatorApplicationTransactionValidationResponse())
->setCancelURI($view_uri)
->setException($ex);
} catch (PhabricatorApplicationTransactionNoEffectException $ex) {
return id(new PhabricatorApplicationTransactionNoEffectResponse())
->setCancelURI($view_uri)
->setException($ex);
} catch (PhabricatorApplicationTransactionWarningException $ex) {
return id(new PhabricatorApplicationTransactionWarningResponse())
->setObject($object)
->setCancelURI($view_uri)
->setException($ex);
}
if (!$is_preview) {
PhabricatorVersionedDraft::purgeDrafts(
$object->getPHID(),
$viewer->getPHID());
$draft_engine = $this->newDraftEngine($object);
if ($draft_engine) {
$draft_engine
->setVersionedDraft(null)
->synchronize();
}
}
if ($request->isAjax() && $is_preview) {
$preview_content = $this->newCommentPreviewContent($object, $xactions);
$raw_view_data = $request->getStr('viewData');
try {
$view_data = phutil_json_decode($raw_view_data);
} catch (Exception $ex) {
$view_data = array();
}
return id(new PhabricatorApplicationTransactionResponse())
->setObject($object)
->setViewer($viewer)
->setTransactions($xactions)
->setIsPreview($is_preview)
->setViewData($view_data)
->setPreviewContent($preview_content);
} else {
return id(new AphrontRedirectResponse())
->setURI($view_uri);
}
}
protected function newDraftEngine($object) {
$viewer = $this->getViewer();
if ($object instanceof PhabricatorDraftInterface) {
$engine = $object->newDraftEngine();
} else {
$engine = new PhabricatorBuiltinDraftEngine();
}
return $engine
->setObject($object)
->setViewer($viewer);
}
/* -( Conduit )------------------------------------------------------------ */
/**
* Respond to a Conduit edit request.
*
* This method accepts a list of transactions to apply to an object, and
* either edits an existing object or creates a new one.
*
* @task conduit
*/
final public function buildConduitResponse(ConduitAPIRequest $request) {
$viewer = $this->getViewer();
$config = $this->loadDefaultConfiguration();
if (!$config) {
throw new Exception(
pht(
'Unable to load configuration for this EditEngine ("%s").',
get_class($this)));
}
$raw_xactions = $this->getRawConduitTransactions($request);
$identifier = $request->getValue('objectIdentifier');
if ($identifier) {
$this->setIsCreate(false);
// After T13186, each transaction can individually weaken or replace the
// capabilities required to apply it, so we no longer need CAN_EDIT to
// attempt to apply transactions to objects. In practice, almost all
// transactions require CAN_EDIT so we won't get very far if we don't
// have it.
$capabilities = array(
PhabricatorPolicyCapability::CAN_VIEW,
);
$object = $this->newObjectFromIdentifier(
$identifier,
$capabilities);
} else {
$this->requireCreateCapability();
$this->setIsCreate(true);
$object = $this->newEditableObjectFromConduit($raw_xactions);
}
$this->validateObject($object);
$fields = $this->buildEditFields($object);
$types = $this->getConduitEditTypesFromFields($fields);
$template = $object->getApplicationTransactionTemplate();
$xactions = $this->getConduitTransactions(
$request,
$raw_xactions,
$types,
$template);
$editor = $object->getApplicationTransactionEditor()
->setActor($viewer)
->setContentSource($request->newContentSource())
->setContinueOnNoEffect(true);
if (!$this->getIsCreate()) {
$editor->setContinueOnMissingFields(true);
}
$xactions = $editor->applyTransactions($object, $xactions);
$xactions_struct = array();
foreach ($xactions as $xaction) {
$xactions_struct[] = array(
'phid' => $xaction->getPHID(),
);
}
return array(
'object' => array(
'id' => (int)$object->getID(),
'phid' => $object->getPHID(),
),
'transactions' => $xactions_struct,
);
}
private function getRawConduitTransactions(ConduitAPIRequest $request) {
$transactions_key = 'transactions';
$xactions = $request->getValue($transactions_key);
if (!is_array($xactions)) {
throw new Exception(
pht(
'Parameter "%s" is not a list of transactions.',
$transactions_key));
}
foreach ($xactions as $key => $xaction) {
if (!is_array($xaction)) {
throw new Exception(
pht(
'Parameter "%s" must contain a list of transaction descriptions, '.
'but item with key "%s" is not a dictionary.',
$transactions_key,
$key));
}
if (!array_key_exists('type', $xaction)) {
throw new Exception(
pht(
'Parameter "%s" must contain a list of transaction descriptions, '.
'but item with key "%s" is missing a "type" field. Each '.
'transaction must have a type field.',
$transactions_key,
$key));
}
if (!array_key_exists('value', $xaction)) {
throw new Exception(
pht(
'Parameter "%s" must contain a list of transaction descriptions, '.
'but item with key "%s" is missing a "value" field. Each '.
'transaction must have a value field.',
$transactions_key,
$key));
}
}
return $xactions;
}
/**
* Generate transactions which can be applied from edit actions in a Conduit
* request.
*
* @param ConduitAPIRequest The request.
* @param list<wild> Raw conduit transactions.
* @param list<PhabricatorEditType> Supported edit types.
* @param PhabricatorApplicationTransaction Template transaction.
* @return list<PhabricatorApplicationTransaction> Generated transactions.
* @task conduit
*/
private function getConduitTransactions(
ConduitAPIRequest $request,
array $xactions,
array $types,
PhabricatorApplicationTransaction $template) {
$viewer = $request->getUser();
$results = array();
foreach ($xactions as $key => $xaction) {
$type = $xaction['type'];
if (empty($types[$type])) {
throw new Exception(
pht(
'Transaction with key "%s" has invalid type "%s". This type is '.
'not recognized. Valid types are: %s.',
$key,
$type,
implode(', ', array_keys($types))));
}
}
if ($this->getIsCreate()) {
$results[] = id(clone $template)
->setTransactionType(PhabricatorTransactions::TYPE_CREATE);
}
$is_strict = $request->getIsStrictlyTyped();
foreach ($xactions as $xaction) {
$type = $types[$xaction['type']];
// Let the parameter type interpret the value. This allows you to
// use usernames in list<user> fields, for example.
$parameter_type = $type->getConduitParameterType();
$parameter_type->setViewer($viewer);
try {
$value = $xaction['value'];
$value = $parameter_type->getValue($xaction, 'value', $is_strict);
$value = $type->getTransactionValueFromConduit($value);
$xaction['value'] = $value;
} catch (Exception $ex) {
throw new PhutilProxyException(
pht(
'Exception when processing transaction of type "%s": %s',
$xaction['type'],
$ex->getMessage()),
$ex);
}
$type_xactions = $type->generateTransactions(
clone $template,
$xaction);
foreach ($type_xactions as $type_xaction) {
$results[] = $type_xaction;
}
}
return $results;
}
/**
* @return map<string, PhabricatorEditType>
* @task conduit
*/
private function getConduitEditTypesFromFields(array $fields) {
$types = array();
foreach ($fields as $field) {
$field_types = $field->getConduitEditTypes();
if ($field_types === null) {
continue;
}
foreach ($field_types as $field_type) {
$types[$field_type->getEditType()] = $field_type;
}
}
return $types;
}
public function getConduitEditTypes() {
$config = $this->loadDefaultConfiguration();
if (!$config) {
return array();
}
$object = $this->newEditableObjectForDocumentation();
$fields = $this->buildEditFields($object);
return $this->getConduitEditTypesFromFields($fields);
}
final public static function getAllEditEngines() {
return id(new PhutilClassMapQuery())
->setAncestorClass(__CLASS__)
->setUniqueMethod('getEngineKey')
->execute();
}
final public static function getByKey(PhabricatorUser $viewer, $key) {
return id(new PhabricatorEditEngineQuery())
->setViewer($viewer)
->withEngineKeys(array($key))
->executeOne();
}
public function getIcon() {
$application = $this->getApplication();
return $application->getIcon();
}
private function loadUsableConfigurationsForCreate() {
$viewer = $this->getViewer();
$configs = id(new PhabricatorEditEngineConfigurationQuery())
->setViewer($viewer)
->withEngineKeys(array($this->getEngineKey()))
->withIsDefault(true)
->withIsDisabled(false)
->execute();
$configs = msort($configs, 'getCreateSortKey');
// Attach this specific engine to configurations we load so they can access
// any runtime configuration. For example, this allows us to generate the
// correct "Create Form" buttons when editing forms, see T12301.
foreach ($configs as $config) {
$config->attachEngine($this);
}
return $configs;
}
protected function getValidationExceptionShortMessage(
PhabricatorApplicationTransactionValidationException $ex,
PhabricatorEditField $field) {
$xaction_type = $field->getTransactionType();
if ($xaction_type === null) {
return null;
}
return $ex->getShortMessage($xaction_type);
}
protected function getCreateNewObjectPolicy() {
return PhabricatorPolicies::POLICY_USER;
}
private function requireCreateCapability() {
PhabricatorPolicyFilter::requireCapability(
$this->getViewer(),
$this,
PhabricatorPolicyCapability::CAN_EDIT);
}
private function hasCreateCapability() {
return PhabricatorPolicyFilter::hasCapability(
$this->getViewer(),
$this,
PhabricatorPolicyCapability::CAN_EDIT);
}
public function isCommentAction() {
return ($this->getEditAction() == 'comment');
}
public function getEditAction() {
$controller = $this->getController();
$request = $controller->getRequest();
return $request->getURIData('editAction');
}
protected function newCommentActionGroups() {
return array();
}
protected function newAutomaticCommentTransactions($object) {
return array();
}
protected function newCommentPreviewContent($object, array $xactions) {
return null;
}
/* -( Form Pages )--------------------------------------------------------- */
public function getSelectedPage() {
return $this->page;
}
private function selectPage($object, $page_key) {
$pages = $this->getPages($object);
if (empty($pages[$page_key])) {
return null;
}
$this->page = $pages[$page_key];
return $this->page;
}
protected function newPages($object) {
return array();
}
protected function getPages($object) {
if ($this->pages === null) {
$pages = $this->newPages($object);
assert_instances_of($pages, 'PhabricatorEditPage');
$pages = mpull($pages, null, 'getKey');
$this->pages = $pages;
}
return $this->pages;
}
private function applyPageToFields($object, array $fields) {
$pages = $this->getPages($object);
if (!$pages) {
return $fields;
}
if (!$this->getSelectedPage()) {
return $fields;
}
$page_picks = array();
$default_key = head($pages)->getKey();
foreach ($pages as $page_key => $page) {
foreach ($page->getFieldKeys() as $field_key) {
$page_picks[$field_key] = $page_key;
}
if ($page->getIsDefault()) {
$default_key = $page_key;
}
}
$page_map = array_fill_keys(array_keys($pages), array());
foreach ($fields as $field_key => $field) {
if (isset($page_picks[$field_key])) {
$page_map[$page_picks[$field_key]][$field_key] = $field;
continue;
}
// TODO: Maybe let the field pick a page to associate itself with so
// extensions can force themselves onto a particular page?
$page_map[$default_key][$field_key] = $field;
}
$page = $this->getSelectedPage();
if (!$page) {
$page = head($pages);
}
$selected_key = $page->getKey();
return $page_map[$selected_key];
}
protected function willApplyTransactions($object, array $xactions) {
return $xactions;
}
protected function didApplyTransactions($object, array $xactions) {
return;
}
/* -( Bulk Edits )--------------------------------------------------------- */
final public function newBulkEditGroupMap() {
$groups = $this->newBulkEditGroups();
$map = array();
foreach ($groups as $group) {
$key = $group->getKey();
if (isset($map[$key])) {
throw new Exception(
pht(
'Two bulk edit groups have the same key ("%s"). Each bulk edit '.
'group must have a unique key.',
$key));
}
$map[$key] = $group;
}
if ($this->isEngineExtensible()) {
$extensions = PhabricatorEditEngineExtension::getAllEnabledExtensions();
} else {
$extensions = array();
}
foreach ($extensions as $extension) {
$extension_groups = $extension->newBulkEditGroups($this);
foreach ($extension_groups as $group) {
$key = $group->getKey();
if (isset($map[$key])) {
throw new Exception(
pht(
'Extension "%s" defines a bulk edit group with the same key '.
'("%s") as the main editor or another extension. Each bulk '.
'edit group must have a unique key.',
get_class($extension),
$key));
}
$map[$key] = $group;
}
}
return $map;
}
protected function newBulkEditGroups() {
return array(
id(new PhabricatorBulkEditGroup())
->setKey('default')
->setLabel(pht('Primary Fields')),
id(new PhabricatorBulkEditGroup())
->setKey('extension')
->setLabel(pht('Support Applications')),
);
}
final public function newBulkEditMap() {
$viewer = $this->getViewer();
$config = $this->loadDefaultConfiguration();
if (!$config) {
throw new Exception(
pht('No default edit engine configuration for bulk edit.'));
}
$object = $this->newEditableObject();
$fields = $this->buildEditFields($object);
$groups = $this->newBulkEditGroupMap();
$edit_types = $this->getBulkEditTypesFromFields($fields);
$map = array();
foreach ($edit_types as $key => $type) {
$bulk_type = $type->getBulkParameterType();
if ($bulk_type === null) {
continue;
}
$bulk_type->setViewer($viewer);
$bulk_label = $type->getBulkEditLabel();
if ($bulk_label === null) {
continue;
}
$group_key = $type->getBulkEditGroupKey();
if (!$group_key) {
$group_key = 'default';
}
if (!isset($groups[$group_key])) {
throw new Exception(
pht(
'Field "%s" has a bulk edit group key ("%s") with no '.
'corresponding bulk edit group.',
$key,
$group_key));
}
$map[] = array(
'label' => $bulk_label,
'xaction' => $key,
'group' => $group_key,
'control' => array(
'type' => $bulk_type->getPHUIXControlType(),
'spec' => (object)$bulk_type->getPHUIXControlSpecification(),
),
);
}
return $map;
}
final public function newRawBulkTransactions(array $xactions) {
$config = $this->loadDefaultConfiguration();
if (!$config) {
throw new Exception(
pht('No default edit engine configuration for bulk edit.'));
}
$object = $this->newEditableObject();
$fields = $this->buildEditFields($object);
$edit_types = $this->getBulkEditTypesFromFields($fields);
$template = $object->getApplicationTransactionTemplate();
$raw_xactions = array();
foreach ($xactions as $key => $xaction) {
PhutilTypeSpec::checkMap(
$xaction,
array(
'type' => 'string',
'value' => 'optional wild',
));
$type = $xaction['type'];
if (!isset($edit_types[$type])) {
throw new Exception(
pht(
'Unsupported bulk edit type "%s".',
$type));
}
$edit_type = $edit_types[$type];
// Replace the edit type with the underlying transaction type. Usually
// these are 1:1 and the transaction type just has more internal noise,
// but it's possible that this isn't the case.
$xaction['type'] = $edit_type->getTransactionType();
$value = $xaction['value'];
$value = $edit_type->getTransactionValueFromBulkEdit($value);
$xaction['value'] = $value;
$xaction_objects = $edit_type->generateTransactions(
clone $template,
$xaction);
foreach ($xaction_objects as $xaction_object) {
$raw_xaction = array(
'type' => $xaction_object->getTransactionType(),
'metadata' => $xaction_object->getMetadata(),
'new' => $xaction_object->getNewValue(),
);
if ($xaction_object->hasOldValue()) {
$raw_xaction['old'] = $xaction_object->getOldValue();
}
if ($xaction_object->hasComment()) {
$comment = $xaction_object->getComment();
$raw_xaction['comment'] = $comment->getContent();
}
$raw_xactions[] = $raw_xaction;
}
}
return $raw_xactions;
}
private function getBulkEditTypesFromFields(array $fields) {
$types = array();
foreach ($fields as $field) {
$field_types = $field->getBulkEditTypes();
if ($field_types === null) {
continue;
}
foreach ($field_types as $field_type) {
$types[$field_type->getEditType()] = $field_type;
}
}
return $types;
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getPHID() {
return get_class($this);
}
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
return PhabricatorPolicies::getMostOpenPolicy();
case PhabricatorPolicyCapability::CAN_EDIT:
return $this->getCreateNewObjectPolicy();
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return false;
}
}
diff --git a/src/applications/transactions/storage/PhabricatorEditEngineConfiguration.php b/src/applications/transactions/storage/PhabricatorEditEngineConfiguration.php
index b1919a0ee0..ae55b9a4bf 100644
--- a/src/applications/transactions/storage/PhabricatorEditEngineConfiguration.php
+++ b/src/applications/transactions/storage/PhabricatorEditEngineConfiguration.php
@@ -1,339 +1,339 @@
<?php
final class PhabricatorEditEngineConfiguration
extends PhabricatorSearchDAO
implements
PhabricatorApplicationTransactionInterface,
PhabricatorPolicyInterface {
protected $engineKey;
protected $builtinKey;
protected $name;
protected $viewPolicy;
protected $properties = array();
protected $isDisabled = 0;
protected $isDefault = 0;
protected $isEdit = 0;
protected $createOrder = 0;
protected $editOrder = 0;
protected $subtype;
private $engine = self::ATTACHABLE;
const LOCK_VISIBLE = 'visible';
const LOCK_LOCKED = 'locked';
const LOCK_HIDDEN = 'hidden';
public function getTableName() {
return 'search_editengineconfiguration';
}
public static function initializeNewConfiguration(
PhabricatorUser $actor,
PhabricatorEditEngine $engine) {
return id(new PhabricatorEditEngineConfiguration())
->setSubtype(PhabricatorEditEngine::SUBTYPE_DEFAULT)
->setEngineKey($engine->getEngineKey())
->attachEngine($engine)
->setViewPolicy(PhabricatorPolicies::getMostOpenPolicy());
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhabricatorEditEngineConfigurationPHIDType::TYPECONST);
}
public function getCreateSortKey() {
return $this->getSortKey($this->createOrder);
}
public function getEditSortKey() {
return $this->getSortKey($this->editOrder);
}
private function getSortKey($order) {
// Put objects at the bottom by default if they haven't previously been
// reordered. When they're explicitly reordered, the smallest sort key we
// assign is 1, so if the object has a value of 0 it means it hasn't been
// ordered yet.
if ($order != 0) {
$group = 'A';
} else {
$group = 'B';
}
return sprintf(
"%s%012d%s\0%012d",
$group,
$order,
$this->getName(),
$this->getID());
}
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'properties' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'engineKey' => 'text64',
'builtinKey' => 'text64?',
'name' => 'text255',
'isDisabled' => 'bool',
'isDefault' => 'bool',
'isEdit' => 'bool',
'createOrder' => 'uint32',
'editOrder' => 'uint32',
'subtype' => 'text64',
),
self::CONFIG_KEY_SCHEMA => array(
'key_engine' => array(
'columns' => array('engineKey', 'builtinKey'),
'unique' => true,
),
'key_default' => array(
'columns' => array('engineKey', 'isDefault', 'isDisabled'),
),
'key_edit' => array(
'columns' => array('engineKey', 'isEdit', 'isDisabled'),
),
),
) + parent::getConfiguration();
}
public function getProperty($key, $default = null) {
return idx($this->properties, $key, $default);
}
public function setProperty($key, $value) {
$this->properties[$key] = $value;
return $this;
}
public function setBuiltinKey($key) {
if (strpos($key, '/') !== false) {
throw new Exception(
pht('EditEngine BuiltinKey contains an invalid key character "/".'));
}
return parent::setBuiltinKey($key);
}
public function attachEngine(PhabricatorEditEngine $engine) {
$this->engine = $engine;
return $this;
}
public function getEngine() {
return $this->assertAttached($this->engine);
}
public function applyConfigurationToFields(
PhabricatorEditEngine $engine,
$object,
array $fields) {
$fields = mpull($fields, null, 'getKey');
$is_new = !$object->getID();
$values = $this->getProperty('defaults', array());
foreach ($fields as $key => $field) {
if (!$field->getIsFormField()) {
continue;
}
if (!$field->getIsDefaultable()) {
continue;
}
if ($is_new) {
if (array_key_exists($key, $values)) {
$field->readDefaultValueFromConfiguration($values[$key]);
}
}
}
$locks = $this->getFieldLocks();
foreach ($fields as $field) {
$key = $field->getKey();
switch (idx($locks, $key)) {
case self::LOCK_LOCKED:
$field->setIsHidden(false);
if ($field->getIsLockable()) {
$field->setIsLocked(true);
}
break;
case self::LOCK_HIDDEN:
$field->setIsHidden(true);
if ($field->getIsLockable()) {
$field->setIsLocked(false);
}
break;
case self::LOCK_VISIBLE:
$field->setIsHidden(false);
if ($field->getIsLockable()) {
$field->setIsLocked(false);
}
break;
default:
// If we don't have an explicit value, don't make any adjustments.
break;
}
}
$fields = $this->reorderFields($fields);
$preamble = $this->getPreamble();
- if (strlen($preamble)) {
+ if ($preamble !== null && strlen($preamble)) {
$fields = array(
'config.preamble' => id(new PhabricatorInstructionsEditField())
->setKey('config.preamble')
->setIsReorderable(false)
->setIsDefaultable(false)
->setIsLockable(false)
->setValue($preamble),
) + $fields;
}
return $fields;
}
private function reorderFields(array $fields) {
// Fields which can not be reordered are fixed in order at the top of the
// form. These are used to show instructions or contextual information.
$fixed = array();
foreach ($fields as $key => $field) {
if (!$field->getIsReorderable()) {
$fixed[$key] = $field;
}
}
$keys = $this->getFieldOrder();
$fields = $fixed + array_select_keys($fields, $keys) + $fields;
return $fields;
}
public function getURI() {
$engine_key = $this->getEngineKey();
$key = $this->getIdentifier();
return "/transactions/editengine/{$engine_key}/view/{$key}/";
}
public function getCreateURI() {
$form_key = $this->getIdentifier();
$engine = $this->getEngine();
return $engine->getCreateURI($form_key);
}
public function getIdentifier() {
$key = $this->getID();
if (!$key) {
$key = $this->getBuiltinKey();
}
return $key;
}
public function getDisplayName() {
$name = $this->getName();
if (strlen($name)) {
return $name;
}
$builtin = $this->getBuiltinKey();
if ($builtin !== null) {
return pht('Builtin Form "%s"', $builtin);
}
return pht('Untitled Form');
}
public function getPreamble() {
return $this->getProperty('preamble');
}
public function setPreamble($preamble) {
return $this->setProperty('preamble', $preamble);
}
public function setFieldOrder(array $field_order) {
return $this->setProperty('order', $field_order);
}
public function getFieldOrder() {
return $this->getProperty('order', array());
}
public function setFieldLocks(array $field_locks) {
return $this->setProperty('locks', $field_locks);
}
public function getFieldLocks() {
return $this->getProperty('locks', array());
}
public function getFieldDefault($key) {
$defaults = $this->getProperty('defaults', array());
return idx($defaults, $key);
}
public function setFieldDefault($key, $value) {
$defaults = $this->getProperty('defaults', array());
$defaults[$key] = $value;
return $this->setProperty('defaults', $defaults);
}
public function getIcon() {
return $this->getEngine()->getIcon();
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
return $this->getViewPolicy();
case PhabricatorPolicyCapability::CAN_EDIT:
return $this->getEngine()
->getApplication()
->getPolicy($capability);
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
return PhabricatorPolicyFilter::hasCapability(
$viewer,
$this->getEngine()->getApplication(),
PhabricatorPolicyCapability::CAN_EDIT);
}
return false;
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new PhabricatorEditEngineConfigurationEditor();
}
public function getApplicationTransactionTemplate() {
return new PhabricatorEditEngineConfigurationTransaction();
}
}

File Metadata

Mime Type
text/x-diff
Expires
Sun, Jan 19, 15:08 (3 w, 1 d ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1125818
Default Alt Text
(165 KB)

Event Timeline