Page MenuHomestyx hydra

No OneTemporary

diff --git a/resources/sql/autopatches/20150602.mlist.2.php b/resources/sql/autopatches/20150602.mlist.2.php
index a8f2a090ba..26d08e6f89 100644
--- a/resources/sql/autopatches/20150602.mlist.2.php
+++ b/resources/sql/autopatches/20150602.mlist.2.php
@@ -1,145 +1,146 @@
<?php
$conn_w = id(new PhabricatorMetaMTAMail())->establishConnection('w');
$lists = new LiskRawMigrationIterator($conn_w, 'metamta_mailinglist');
echo pht('Migrating mailing lists...')."\n";
foreach ($lists as $list) {
$name = $list['name'];
$email = $list['email'];
$uri = $list['uri'];
$old_phid = $list['phid'];
$username = preg_replace('/[^a-zA-Z0-9_-]+/', '-', $name);
$username = preg_replace('/-{2,}/', '-', $username);
$username = trim($username, '-');
if (!strlen($username)) {
$username = 'mailinglist';
}
$username .= '-list';
$username_okay = false;
for ($suffix = 1; $suffix <= 9; $suffix++) {
if ($suffix == 1) {
$effective_username = $username;
} else {
$effective_username = $username.$suffix;
}
$collision = id(new PhabricatorPeopleQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withUsernames(array($effective_username))
->executeOne();
if (!$collision) {
$username_okay = true;
break;
}
}
if (!$username_okay) {
echo pht(
'Failed to migrate mailing list "%s": unable to generate a unique '.
- 'username for it.')."\n";
+ 'username for it.',
+ $name)."\n";
continue;
}
$username = $effective_username;
if (!PhabricatorUser::validateUsername($username)) {
echo pht(
'Failed to migrate mailing list "%s": unable to generate a valid '.
'username for it.',
$name)."\n";
continue;
}
$address = id(new PhabricatorUserEmail())->loadOneWhere(
'address = %s',
$email);
if ($address) {
echo pht(
'Failed to migrate mailing list "%s": an existing user already '.
'has the email address "%s".',
$name,
$email)."\n";
continue;
}
$user = id(new PhabricatorUser())
->setUsername($username)
->setRealName(pht('Mailing List "%s"', $name))
->setIsApproved(1)
->setIsMailingList(1);
$email_object = id(new PhabricatorUserEmail())
->setAddress($email)
->setIsVerified(1);
try {
id(new PhabricatorUserEditor())
->setActor($user)
->createNewUser($user, $email_object);
} catch (Exception $ex) {
echo pht(
'Failed to migrate mailing list "%s": %s.',
$name,
$ex->getMessage())."\n";
continue;
}
$new_phid = $user->getPHID();
// NOTE: After the PHID type is removed we can't use any Edge code to
// modify edges.
$edge_type = PhabricatorSubscribedToObjectEdgeType::EDGECONST;
$edge_inverse = PhabricatorObjectHasSubscriberEdgeType::EDGECONST;
$map = PhabricatorPHIDType::getAllTypes();
foreach ($map as $type => $spec) {
try {
$object = $spec->newObject();
if (!$object) {
continue;
}
$object_conn_w = $object->establishConnection('w');
queryfx(
$object_conn_w,
'UPDATE %T SET dst = %s WHERE dst = %s AND type = %s',
PhabricatorEdgeConfig::TABLE_NAME_EDGE,
$new_phid,
$old_phid,
$edge_inverse);
} catch (Exception $ex) {
// Just ignore these; they're mostly tables not existing.
continue;
}
}
try {
$dst_phids = queryfx_all(
$conn_w,
'SELECT dst FROM %T WHERE src = %s AND type = %s',
PhabricatorEdgeConfig::TABLE_NAME_EDGE,
$old_phid,
$edge_type);
if ($dst_phids) {
$editor = new PhabricatorEdgeEditor();
foreach ($dst_phids as $dst_phid) {
$editor->addEdge($new_phid, $edge_type, $dst_phid['dst']);
}
$editor->save();
}
} catch (Exception $ex) {
echo pht(
'Unable to migrate some inverse edges for mailing list "%s": %s.',
$name,
$ex->getMessage())."\n";
continue;
}
echo pht(
'Migrated mailing list "%s" to mailing list user "%s".',
$name,
$user->getUsername())."\n";
}
diff --git a/src/applications/differential/management/PhabricatorDifferentialRebuildChangesetsWorkflow.php b/src/applications/differential/management/PhabricatorDifferentialRebuildChangesetsWorkflow.php
index 80a8ac29a8..44e5be5d4a 100644
--- a/src/applications/differential/management/PhabricatorDifferentialRebuildChangesetsWorkflow.php
+++ b/src/applications/differential/management/PhabricatorDifferentialRebuildChangesetsWorkflow.php
@@ -1,97 +1,98 @@
<?php
final class PhabricatorDifferentialRebuildChangesetsWorkflow
extends PhabricatorDifferentialManagementWorkflow {
protected function didConstruct() {
$this
->setName('rebuild-changesets')
->setExamples('**rebuild-changesets** --revision __revision__')
->setSynopsis(pht('Rebuild changesets for a revision.'))
->setArguments(
array(
array(
'name' => 'revision',
'param' => 'revision',
'help' => pht('Revision to rebuild changesets for.'),
),
));
}
public function execute(PhutilArgumentParser $args) {
$viewer = $this->getViewer();
$revision_identifier = $args->getArg('revision');
if (!$revision_identifier) {
throw new PhutilArgumentUsageException(
pht('Specify a revision to rebuild changesets for with "--revision".'));
}
$revision = id(new PhabricatorObjectQuery())
->setViewer($viewer)
->withNames(array($revision_identifier))
->executeOne();
if ($revision) {
if (!($revision instanceof DifferentialRevision)) {
throw new PhutilArgumentUsageException(
pht(
'Object "%s" specified by "--revision" must be a Differential '.
- 'revision.'));
+ 'revision.',
+ $revision_identifier));
}
} else {
$revision = id(new DifferentialRevisionQuery())
->setViewer($viewer)
->withIDs(array($revision_identifier))
->executeOne();
}
if (!$revision) {
throw new PhutilArgumentUsageException(
pht(
'No revision "%s" exists.',
$revision_identifier));
}
$diffs = id(new DifferentialDiffQuery())
->setViewer($viewer)
->withRevisionIDs(array($revision->getID()))
->execute();
$changesets = id(new DifferentialChangesetQuery())
->setViewer($viewer)
->withDiffs($diffs)
->needHunks(true)
->execute();
$changeset_groups = mgroup($changesets, 'getDiffID');
foreach ($changeset_groups as $diff_id => $changesets) {
echo tsprintf(
"%s\n",
pht(
'Rebuilding %s changeset(s) for diff ID %d.',
phutil_count($changesets),
$diff_id));
foreach ($changesets as $changeset) {
echo tsprintf(
" %s\n",
$changeset->getFilename());
}
id(new DifferentialChangesetEngine())
->setViewer($viewer)
->rebuildChangesets($changesets);
foreach ($changesets as $changeset) {
$changeset->save();
}
echo tsprintf(
"%s\n",
pht('Done.'));
}
}
}
diff --git a/src/applications/differential/xaction/DifferentialRevisionRepositoryTransaction.php b/src/applications/differential/xaction/DifferentialRevisionRepositoryTransaction.php
index 63c7f2c32d..f92871ca16 100644
--- a/src/applications/differential/xaction/DifferentialRevisionRepositoryTransaction.php
+++ b/src/applications/differential/xaction/DifferentialRevisionRepositoryTransaction.php
@@ -1,95 +1,96 @@
<?php
final class DifferentialRevisionRepositoryTransaction
extends DifferentialRevisionTransactionType {
const TRANSACTIONTYPE = 'differential.revision.repository';
public function generateOldValue($object) {
return $object->getRepositoryPHID();
}
public function applyInternalEffects($object, $value) {
$object->setRepositoryPHID($value);
}
public function getTitle() {
$old = $this->getOldValue();
$new = $this->getNewValue();
if ($old && $new) {
return pht(
'%s changed the repository for this revision from %s to %s.',
$this->renderAuthor(),
$this->renderHandle($old),
$this->renderHandle($new));
} else if ($new) {
return pht(
'%s set the repository for this revision to %s.',
$this->renderAuthor(),
$this->renderHandle($new));
} else {
return pht(
'%s removed %s as the repository for this revision.',
$this->renderAuthor(),
$this->renderHandle($old));
}
}
public function getTitleForFeed() {
$old = $this->getOldValue();
$new = $this->getNewValue();
if ($old && $new) {
return pht(
'%s changed the repository for %s from %s to %s.',
$this->renderAuthor(),
$this->renderObject(),
$this->renderHandle($old),
$this->renderHandle($new));
} else if ($new) {
return pht(
'%s set the repository for %s to %s.',
$this->renderAuthor(),
$this->renderObject(),
$this->renderHandle($new));
} else {
return pht(
'%s removed %s as the repository for %s.',
$this->renderAuthor(),
$this->renderHandle($old),
$this->renderObject());
}
}
public function validateTransactions($object, array $xactions) {
$actor = $this->getActor();
$errors = array();
$old_value = $object->getRepositoryPHID();
foreach ($xactions as $xaction) {
$new_value = $xaction->getNewValue();
if (!$new_value) {
continue;
}
if ($new_value == $old_value) {
continue;
}
$repository = id(new PhabricatorRepositoryQuery())
->setViewer($actor)
->withPHIDs(array($new_value))
->executeOne();
if (!$repository) {
$errors[] = $this->newInvalidError(
pht(
'Repository "%s" is not a valid repository, or you do not have '.
- 'permission to view it.'),
+ 'permission to view it.',
+ $new_value),
$xaction);
}
}
return $errors;
}
}
diff --git a/src/applications/fact/chart/PhabricatorChartFunctionArgumentParser.php b/src/applications/fact/chart/PhabricatorChartFunctionArgumentParser.php
index 01fec105a3..a3b8554c5e 100644
--- a/src/applications/fact/chart/PhabricatorChartFunctionArgumentParser.php
+++ b/src/applications/fact/chart/PhabricatorChartFunctionArgumentParser.php
@@ -1,214 +1,215 @@
<?php
final class PhabricatorChartFunctionArgumentParser
extends Phobject {
private $function;
private $rawArguments;
private $unconsumedArguments;
private $haveAllArguments = false;
private $unparsedArguments;
private $argumentMap = array();
private $argumentPosition = 0;
private $argumentValues = array();
private $repeatableArgument = null;
public function setFunction(PhabricatorChartFunction $function) {
$this->function = $function;
return $this;
}
public function getFunction() {
return $this->function;
}
public function setRawArguments(array $arguments) {
$this->rawArguments = $arguments;
$this->unconsumedArguments = $arguments;
}
public function addArgument(PhabricatorChartFunctionArgument $spec) {
$name = $spec->getName();
if (!strlen($name)) {
throw new Exception(
pht(
'Chart function "%s" emitted an argument specification with no '.
'argument name. Argument specifications must have unique names.',
$this->getFunctionArgumentSignature()));
}
$type = $spec->getType();
if (!strlen($type)) {
throw new Exception(
pht(
'Chart function "%s" emitted an argument specification ("%s") with '.
'no type. Each argument specification must have a valid type.',
+ $this->getFunctionArgumentSignature(),
$name));
}
if (isset($this->argumentMap[$name])) {
throw new Exception(
pht(
'Chart function "%s" emitted multiple argument specifications '.
'with the same name ("%s"). Each argument specification must have '.
'a unique name.',
$this->getFunctionArgumentSignature(),
$name));
}
if ($this->repeatableArgument) {
if ($spec->getRepeatable()) {
throw new Exception(
pht(
'Chart function "%s" emitted multiple repeatable argument '.
'specifications ("%s" and "%s"). Only one argument may be '.
'repeatable and it must be the last argument.',
$this->getFunctionArgumentSignature(),
$name,
$this->repeatableArgument->getName()));
} else {
throw new Exception(
pht(
'Chart function "%s" emitted a repeatable argument ("%s"), then '.
'another argument ("%s"). No arguments are permitted after a '.
'repeatable argument.',
$this->getFunctionArgumentSignature(),
$this->repeatableArgument->getName(),
$name));
}
}
if ($spec->getRepeatable()) {
$this->repeatableArgument = $spec;
}
$this->argumentMap[$name] = $spec;
$this->unparsedArguments[] = $spec;
return $this;
}
public function parseArgument(
PhabricatorChartFunctionArgument $spec) {
$this->addArgument($spec);
return $this->parseArguments();
}
public function setHaveAllArguments($have_all) {
$this->haveAllArguments = $have_all;
return $this;
}
public function getAllArguments() {
return array_values($this->argumentMap);
}
public function getRawArguments() {
return $this->rawArguments;
}
public function parseArguments() {
$have_count = count($this->rawArguments);
$want_count = count($this->argumentMap);
if ($this->haveAllArguments) {
if ($this->repeatableArgument) {
if ($want_count > $have_count) {
throw new Exception(
pht(
'Function "%s" expects %s or more argument(s), but only %s '.
'argument(s) were provided.',
$this->getFunctionArgumentSignature(),
$want_count,
$have_count));
}
} else if ($want_count !== $have_count) {
throw new Exception(
pht(
'Function "%s" expects %s argument(s), but %s argument(s) were '.
'provided.',
$this->getFunctionArgumentSignature(),
$want_count,
$have_count));
}
}
while ($this->unparsedArguments) {
$argument = array_shift($this->unparsedArguments);
$name = $argument->getName();
if (!$this->unconsumedArguments) {
throw new Exception(
pht(
'Function "%s" expects at least %s argument(s), but only %s '.
'argument(s) were provided.',
$this->getFunctionArgumentSignature(),
$want_count,
$have_count));
}
$raw_argument = array_shift($this->unconsumedArguments);
$this->argumentPosition++;
$is_repeatable = $argument->getRepeatable();
// If this argument is repeatable and we have more arguments, add it
// back to the end of the list so we can continue parsing.
if ($is_repeatable && $this->unconsumedArguments) {
$this->unparsedArguments[] = $argument;
}
try {
$value = $argument->newValue($raw_argument);
} catch (Exception $ex) {
throw new Exception(
pht(
'Argument "%s" (in position "%s") to function "%s" is '.
'invalid: %s',
$name,
$this->argumentPosition,
$this->getFunctionArgumentSignature(),
$ex->getMessage()));
}
if ($is_repeatable) {
if (!isset($this->argumentValues[$name])) {
$this->argumentValues[$name] = array();
}
$this->argumentValues[$name][] = $value;
} else {
$this->argumentValues[$name] = $value;
}
}
}
public function getArgumentValue($key) {
if (!array_key_exists($key, $this->argumentValues)) {
throw new Exception(
pht(
'Function "%s" is requesting an argument ("%s") that it did '.
'not define.',
$this->getFunctionArgumentSignature(),
$key));
}
return $this->argumentValues[$key];
}
private function getFunctionArgumentSignature() {
$argument_list = array();
foreach ($this->argumentMap as $key => $spec) {
$argument_list[] = $key;
}
if (!$this->haveAllArguments || $this->repeatableArgument) {
$argument_list[] = '...';
}
return sprintf(
'%s(%s)',
$this->getFunction()->getFunctionKey(),
implode(', ', $argument_list));
}
}
diff --git a/src/applications/harbormaster/management/HarbormasterManagementPublishWorkflow.php b/src/applications/harbormaster/management/HarbormasterManagementPublishWorkflow.php
index df42d8632f..fecce6136e 100644
--- a/src/applications/harbormaster/management/HarbormasterManagementPublishWorkflow.php
+++ b/src/applications/harbormaster/management/HarbormasterManagementPublishWorkflow.php
@@ -1,87 +1,88 @@
<?php
final class HarbormasterManagementPublishWorkflow
extends HarbormasterManagementWorkflow {
protected function didConstruct() {
$this
->setName('publish')
->setExamples(pht('**publish** __buildable__ ...'))
->setSynopsis(
pht(
'Publish a buildable. This is primarily useful for developing '.
'and debugging applications which have buildable objects.'))
->setArguments(
array(
array(
'name' => 'buildable',
'wildcard' => true,
),
));
}
public function execute(PhutilArgumentParser $args) {
$viewer = $this->getViewer();
$buildable_names = $args->getArg('buildable');
if (!$buildable_names) {
throw new PhutilArgumentUsageException(
pht(
'Name one or more buildables to publish, like "B123".'));
}
$query = id(new PhabricatorObjectQuery())
->setViewer($viewer)
->withNames($buildable_names);
$query->execute();
$result_map = $query->getNamedResults();
foreach ($buildable_names as $name) {
if (!isset($result_map[$name])) {
throw new PhutilArgumentUsageException(
pht(
'Argument "%s" does not name a buildable. Provide one or more '.
'valid buildable monograms or PHIDs.',
$name));
}
}
foreach ($result_map as $name => $result) {
if (!($result instanceof HarbormasterBuildable)) {
throw new PhutilArgumentUsageException(
pht(
'Object "%s" is not a HarbormasterBuildable (it is a "%s"). '.
'Name one or more buildables to publish, like "B123".',
+ $name,
get_class($result)));
}
}
foreach ($result_map as $buildable) {
echo tsprintf(
"%s\n",
pht(
'Publishing "%s"...',
$buildable->getMonogram()));
// Reload the buildable to pick up builds.
$buildable = id(new HarbormasterBuildableQuery())
->setViewer($viewer)
->withIDs(array($buildable->getID()))
->needBuilds(true)
->executeOne();
$engine = id(new HarbormasterBuildEngine())
->setViewer($viewer)
->publishBuildable($buildable, $buildable);
}
echo tsprintf(
"%s\n",
pht('Done.'));
return 0;
}
}
diff --git a/src/applications/harbormaster/storage/build/HarbormasterBuildLog.php b/src/applications/harbormaster/storage/build/HarbormasterBuildLog.php
index 2b160445d8..5df8b30bff 100644
--- a/src/applications/harbormaster/storage/build/HarbormasterBuildLog.php
+++ b/src/applications/harbormaster/storage/build/HarbormasterBuildLog.php
@@ -1,786 +1,788 @@
<?php
final class HarbormasterBuildLog
extends HarbormasterDAO
implements
PhabricatorPolicyInterface,
PhabricatorDestructibleInterface,
PhabricatorConduitResultInterface {
protected $buildTargetPHID;
protected $logSource;
protected $logType;
protected $duration;
protected $live;
protected $filePHID;
protected $byteLength;
protected $chunkFormat;
protected $lineMap = array();
private $buildTarget = self::ATTACHABLE;
private $rope;
private $isOpen;
private $lock;
const CHUNK_BYTE_LIMIT = 1048576;
public function __construct() {
$this->rope = new PhutilRope();
}
public function __destruct() {
if ($this->isOpen) {
$this->closeBuildLog();
}
if ($this->lock) {
if ($this->lock->isLocked()) {
$this->lock->unlock();
}
}
}
public static function initializeNewBuildLog(
HarbormasterBuildTarget $build_target) {
return id(new HarbormasterBuildLog())
->setBuildTargetPHID($build_target->getPHID())
->setDuration(null)
->setLive(1)
->setByteLength(0)
->setChunkFormat(HarbormasterBuildLogChunk::CHUNK_ENCODING_TEXT);
}
public function scheduleRebuild($force) {
PhabricatorWorker::scheduleTask(
'HarbormasterLogWorker',
array(
'logPHID' => $this->getPHID(),
'force' => $force,
),
array(
'objectPHID' => $this->getPHID(),
));
}
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'lineMap' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
// T6203/NULLABILITY
// It seems like these should be non-nullable? All logs should have a
// source, etc.
'logSource' => 'text255?',
'logType' => 'text255?',
'duration' => 'uint32?',
'live' => 'bool',
'filePHID' => 'phid?',
'byteLength' => 'uint64',
'chunkFormat' => 'text32',
),
self::CONFIG_KEY_SCHEMA => array(
'key_buildtarget' => array(
'columns' => array('buildTargetPHID'),
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
HarbormasterBuildLogPHIDType::TYPECONST);
}
public function attachBuildTarget(HarbormasterBuildTarget $build_target) {
$this->buildTarget = $build_target;
return $this;
}
public function getBuildTarget() {
return $this->assertAttached($this->buildTarget);
}
public function getName() {
return pht('Build Log');
}
public function newChunkIterator() {
return id(new HarbormasterBuildLogChunkIterator($this))
->setPageSize(8);
}
public function newDataIterator() {
return $this->newChunkIterator()
->setAsString(true);
}
private function loadLastChunkInfo() {
$chunk_table = new HarbormasterBuildLogChunk();
$conn_w = $chunk_table->establishConnection('w');
return queryfx_one(
$conn_w,
'SELECT id, size, encoding FROM %T WHERE logID = %d
ORDER BY id DESC LIMIT 1',
$chunk_table->getTableName(),
$this->getID());
}
public function loadData($offset, $length) {
$end = ($offset + $length);
$chunks = id(new HarbormasterBuildLogChunk())->loadAllWhere(
'logID = %d AND headOffset < %d AND tailOffset >= %d
ORDER BY headOffset ASC',
$this->getID(),
$end,
$offset);
// Make sure that whatever we read out of the database is a single
// contiguous range which contains all of the requested bytes.
$ranges = array();
foreach ($chunks as $chunk) {
$ranges[] = array(
'head' => $chunk->getHeadOffset(),
'tail' => $chunk->getTailOffset(),
);
}
$ranges = isort($ranges, 'head');
$ranges = array_values($ranges);
$count = count($ranges);
for ($ii = 0; $ii < ($count - 1); $ii++) {
if ($ranges[$ii + 1]['head'] === $ranges[$ii]['tail']) {
$ranges[$ii + 1]['head'] = $ranges[$ii]['head'];
unset($ranges[$ii]);
}
}
if (count($ranges) !== 1) {
$display_ranges = array();
foreach ($ranges as $range) {
$display_ranges[] = pht(
'(%d - %d)',
$range['head'],
$range['tail']);
}
if (!$display_ranges) {
$display_ranges[] = pht('<null>');
}
throw new Exception(
pht(
'Attempt to load log bytes (%d - %d) failed: failed to '.
'load a single contiguous range. Actual ranges: %s.',
+ $offset,
+ $end,
implode('; ', $display_ranges)));
}
$range = head($ranges);
if ($range['head'] > $offset || $range['tail'] < $end) {
throw new Exception(
pht(
'Attempt to load log bytes (%d - %d) failed: the loaded range '.
'(%d - %d) does not span the requested range.',
$offset,
$end,
$range['head'],
$range['tail']));
}
$parts = array();
foreach ($chunks as $chunk) {
$parts[] = $chunk->getChunkDisplayText();
}
$parts = implode('', $parts);
$chop_head = ($offset - $range['head']);
$chop_tail = ($range['tail'] - $end);
if ($chop_head) {
$parts = substr($parts, $chop_head);
}
if ($chop_tail) {
$parts = substr($parts, 0, -$chop_tail);
}
return $parts;
}
public function getLineSpanningRange($min_line, $max_line) {
$map = $this->getLineMap();
if (!$map) {
throw new Exception(pht('No line map.'));
}
$min_pos = 0;
$min_line = 0;
$max_pos = $this->getByteLength();
list($map) = $map;
foreach ($map as $marker) {
list($offset, $count) = $marker;
if ($count < $min_line) {
if ($offset > $min_pos) {
$min_pos = $offset;
$min_line = $count;
}
}
if ($count > $max_line) {
$max_pos = min($max_pos, $offset);
break;
}
}
return array($min_pos, $max_pos, $min_line);
}
public function getReadPosition($read_offset) {
$position = array(0, 0);
$map = $this->getLineMap();
if (!$map) {
throw new Exception(pht('No line map.'));
}
list($map) = $map;
foreach ($map as $marker) {
list($offset, $count) = $marker;
if ($offset > $read_offset) {
break;
}
$position = $marker;
}
return $position;
}
public function getLogText() {
// TODO: Remove this method since it won't scale for big logs.
$all_chunks = $this->newChunkIterator();
$full_text = array();
foreach ($all_chunks as $chunk) {
$full_text[] = $chunk->getChunkDisplayText();
}
return implode('', $full_text);
}
public function getURI() {
$id = $this->getID();
return "/harbormaster/log/view/{$id}/";
}
public function getRenderURI($lines) {
if (strlen($lines)) {
$lines = '$'.$lines;
}
$id = $this->getID();
return "/harbormaster/log/render/{$id}/{$lines}";
}
/* -( Chunks )------------------------------------------------------------- */
public function canCompressLog() {
return function_exists('gzdeflate');
}
public function compressLog() {
$this->processLog(HarbormasterBuildLogChunk::CHUNK_ENCODING_GZIP);
}
public function decompressLog() {
$this->processLog(HarbormasterBuildLogChunk::CHUNK_ENCODING_TEXT);
}
private function processLog($mode) {
if (!$this->getLock()->isLocked()) {
throw new Exception(
pht(
'You can not process build log chunks unless the log lock is '.
'held.'));
}
$chunks = $this->newChunkIterator();
// NOTE: Because we're going to insert new chunks, we need to stop the
// iterator once it hits the final chunk which currently exists. Otherwise,
// it may start consuming chunks we just wrote and run forever.
$last = $this->loadLastChunkInfo();
if ($last) {
$chunks->setRange(null, $last['id']);
}
$byte_limit = self::CHUNK_BYTE_LIMIT;
$rope = new PhutilRope();
$this->openTransaction();
$offset = 0;
foreach ($chunks as $chunk) {
$rope->append($chunk->getChunkDisplayText());
$chunk->delete();
while ($rope->getByteLength() > $byte_limit) {
$offset += $this->writeEncodedChunk($rope, $offset, $byte_limit, $mode);
}
}
while ($rope->getByteLength()) {
$offset += $this->writeEncodedChunk($rope, $offset, $byte_limit, $mode);
}
$this
->setChunkFormat($mode)
->save();
$this->saveTransaction();
}
private function writeEncodedChunk(
PhutilRope $rope,
$offset,
$length,
$mode) {
$data = $rope->getPrefixBytes($length);
$size = strlen($data);
switch ($mode) {
case HarbormasterBuildLogChunk::CHUNK_ENCODING_TEXT:
// Do nothing.
break;
case HarbormasterBuildLogChunk::CHUNK_ENCODING_GZIP:
$data = gzdeflate($data);
if ($data === false) {
throw new Exception(pht('Failed to gzdeflate() log data!'));
}
break;
default:
throw new Exception(pht('Unknown chunk encoding "%s"!', $mode));
}
$this->writeChunk($mode, $offset, $size, $data);
$rope->removeBytesFromHead($size);
return $size;
}
private function writeChunk($encoding, $offset, $raw_size, $data) {
$head_offset = $offset;
$tail_offset = $offset + $raw_size;
return id(new HarbormasterBuildLogChunk())
->setLogID($this->getID())
->setEncoding($encoding)
->setHeadOffset($head_offset)
->setTailOffset($tail_offset)
->setSize($raw_size)
->setChunk($data)
->save();
}
/* -( Writing )------------------------------------------------------------ */
public function getLock() {
if (!$this->lock) {
$phid = $this->getPHID();
$phid_key = PhabricatorHash::digestToLength($phid, 14);
$lock_key = "build.log({$phid_key})";
$lock = PhabricatorGlobalLock::newLock($lock_key);
$this->lock = $lock;
}
return $this->lock;
}
public function openBuildLog() {
if ($this->isOpen) {
throw new Exception(pht('This build log is already open!'));
}
$is_new = !$this->getID();
if ($is_new) {
$this->save();
}
$this->getLock()->lock();
$this->isOpen = true;
$this->reload();
if (!$this->getLive()) {
$this->setLive(1)->save();
}
return $this;
}
public function closeBuildLog($forever = true) {
if (!$this->isOpen) {
throw new Exception(
pht(
'You must openBuildLog() before you can closeBuildLog().'));
}
$this->flush();
if ($forever) {
$start = $this->getDateCreated();
$now = PhabricatorTime::getNow();
$this
->setDuration($now - $start)
->setLive(0)
->save();
}
$this->getLock()->unlock();
$this->isOpen = false;
if ($forever) {
$this->scheduleRebuild(false);
}
return $this;
}
public function append($content) {
if (!$this->isOpen) {
throw new Exception(
pht(
'You must openBuildLog() before you can append() content to '.
'the log.'));
}
$content = (string)$content;
$this->rope->append($content);
$this->flush();
return $this;
}
private function flush() {
// TODO: Maybe don't flush more than a couple of times per second. If a
// caller writes a single character over and over again, we'll currently
// spend a lot of time flushing that.
$chunk_table = id(new HarbormasterBuildLogChunk())->getTableName();
$chunk_limit = self::CHUNK_BYTE_LIMIT;
$encoding_text = HarbormasterBuildLogChunk::CHUNK_ENCODING_TEXT;
$rope = $this->rope;
while (true) {
$length = $rope->getByteLength();
if (!$length) {
break;
}
$conn_w = $this->establishConnection('w');
$last = $this->loadLastChunkInfo();
$can_append =
($last) &&
($last['encoding'] == $encoding_text) &&
($last['size'] < $chunk_limit);
if ($can_append) {
$append_id = $last['id'];
$prefix_size = $last['size'];
} else {
$append_id = null;
$prefix_size = 0;
}
$data_limit = ($chunk_limit - $prefix_size);
$append_data = $rope->getPrefixBytes($data_limit);
$data_size = strlen($append_data);
$this->openTransaction();
if ($append_id) {
queryfx(
$conn_w,
'UPDATE %T SET
chunk = CONCAT(chunk, %B),
size = %d,
tailOffset = headOffset + %d
WHERE
id = %d',
$chunk_table,
$append_data,
$prefix_size + $data_size,
$prefix_size + $data_size,
$append_id);
} else {
$this->writeChunk(
$encoding_text,
$this->getByteLength(),
$data_size,
$append_data);
}
$this->updateLineMap($append_data);
$this->save();
$this->saveTransaction();
$rope->removeBytesFromHead($data_size);
}
}
public function updateLineMap($append_data, $marker_distance = null) {
$this->byteLength += strlen($append_data);
if (!$marker_distance) {
$marker_distance = (self::CHUNK_BYTE_LIMIT / 2);
}
if (!$this->lineMap) {
$this->lineMap = array(
array(),
0,
0,
null,
);
}
list($map, $map_bytes, $line_count, $prefix) = $this->lineMap;
$buffer = $append_data;
if ($prefix) {
$prefix = base64_decode($prefix);
$buffer = $prefix.$buffer;
}
if ($map) {
list($last_marker, $last_count) = last($map);
} else {
$last_marker = 0;
$last_count = 0;
}
$max_utf8_width = 8;
$next_marker = $last_marker + $marker_distance;
$pos = 0;
$len = strlen($buffer);
while (true) {
// If we only have a few bytes left in the buffer, leave it as a prefix
// for next time.
if (($len - $pos) <= ($max_utf8_width * 2)) {
$prefix = substr($buffer, $pos);
break;
}
// The next slice we're going to look at is the smaller of:
//
// - the number of bytes we need to make it to the next marker; or
// - all the bytes we have left, minus one.
$slice_length = min(
($marker_distance - $map_bytes),
($len - $pos) - 1);
// We don't slice all the way to the end for two reasons.
// First, we want to avoid slicing immediately after a "\r" if we don't
// know what the next character is, because we want to make sure to
// count "\r\n" as a single newline, rather than counting the "\r" as
// a newline and then later counting the "\n" as another newline.
// Second, we don't want to slice in the middle of a UTF8 character if
// we can help it. We may not be able to avoid this, since the whole
// buffer may just be binary data, but in most cases we can backtrack
// a little bit and try to make it out of emoji or other legitimate
// multibyte UTF8 characters which appear in the log.
$min_width = max(1, $slice_length - $max_utf8_width);
while ($slice_length >= $min_width) {
$here = $buffer[$pos + ($slice_length - 1)];
$next = $buffer[$pos + ($slice_length - 1) + 1];
// If this is "\r" and the next character is "\n", extend the slice
// to include the "\n". Otherwise, we're fine to slice here since we
// know we're not in the middle of a UTF8 character.
if ($here === "\r") {
if ($next === "\n") {
$slice_length++;
}
break;
}
// If the next character is 0x7F or lower, or between 0xC2 and 0xF4,
// we're not slicing in the middle of a UTF8 character.
$ord = ord($next);
if ($ord <= 0x7F || ($ord >= 0xC2 && $ord <= 0xF4)) {
break;
}
$slice_length--;
}
$slice = substr($buffer, $pos, $slice_length);
$pos += $slice_length;
$map_bytes += $slice_length;
// Count newlines in the slice. This goofy approach is meaningfully
// faster than "preg_match_all()" or "preg_split()". See PHI766.
$n_rn = substr_count($slice, "\r\n");
$n_r = substr_count($slice, "\r");
$n_n = substr_count($slice, "\n");
$line_count += ($n_rn) + ($n_r - $n_rn) + ($n_n - $n_rn);
if ($map_bytes >= ($marker_distance - $max_utf8_width)) {
$map[] = array(
$last_marker + $map_bytes,
$last_count + $line_count,
);
$last_count = $last_count + $line_count;
$line_count = 0;
$last_marker = $last_marker + $map_bytes;
$map_bytes = 0;
$next_marker = $last_marker + $marker_distance;
}
}
$this->lineMap = array(
$map,
$map_bytes,
$line_count,
base64_encode($prefix),
);
return $this;
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
);
}
public function getPolicy($capability) {
return $this->getBuildTarget()->getPolicy($capability);
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return $this->getBuildTarget()->hasAutomaticCapability(
$capability,
$viewer);
}
public function describeAutomaticCapability($capability) {
return pht(
'Users must be able to see a build target to view its build log.');
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$this->destroyFile($engine);
$this->destroyChunks();
$this->delete();
}
public function destroyFile(PhabricatorDestructionEngine $engine = null) {
if (!$engine) {
$engine = new PhabricatorDestructionEngine();
}
$file_phid = $this->getFilePHID();
if ($file_phid) {
$viewer = $engine->getViewer();
$file = id(new PhabricatorFileQuery())
->setViewer($viewer)
->withPHIDs(array($file_phid))
->executeOne();
if ($file) {
$engine->destroyObject($file);
}
}
$this->setFilePHID(null);
return $this;
}
public function destroyChunks() {
$chunk = new HarbormasterBuildLogChunk();
$conn = $chunk->establishConnection('w');
// Just delete the chunks directly so we don't have to pull the data over
// the wire for large logs.
queryfx(
$conn,
'DELETE FROM %T WHERE logID = %d',
$chunk->getTableName(),
$this->getID());
return $this;
}
/* -( PhabricatorConduitResultInterface )---------------------------------- */
public function getFieldSpecificationsForConduit() {
return array(
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('buildTargetPHID')
->setType('phid')
->setDescription(pht('Build target this log is attached to.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('byteLength')
->setType('int')
->setDescription(pht('Length of the log in bytes.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('filePHID')
->setType('phid?')
->setDescription(pht('A file containing the log data.')),
);
}
public function getFieldValuesForConduit() {
return array(
'buildTargetPHID' => $this->getBuildTargetPHID(),
'byteLength' => (int)$this->getByteLength(),
'filePHID' => $this->getFilePHID(),
);
}
public function getConduitSearchAttachments() {
return array();
}
}
diff --git a/src/applications/maniphest/constants/ManiphestTaskPriority.php b/src/applications/maniphest/constants/ManiphestTaskPriority.php
index d559299af9..8b43da132b 100644
--- a/src/applications/maniphest/constants/ManiphestTaskPriority.php
+++ b/src/applications/maniphest/constants/ManiphestTaskPriority.php
@@ -1,262 +1,261 @@
<?php
final class ManiphestTaskPriority extends ManiphestConstants {
const UNKNOWN_PRIORITY_KEYWORD = '!!unknown!!';
/**
* Get the priorities and their full descriptions.
*
* @return map Priorities to descriptions.
*/
public static function getTaskPriorityMap() {
$map = self::getConfig();
foreach ($map as $key => $spec) {
$map[$key] = idx($spec, 'name', $key);
}
return $map;
}
/**
* Get the priorities and their command keywords.
*
* @return map Priorities to lists of command keywords.
*/
public static function getTaskPriorityKeywordsMap() {
$map = self::getConfig();
foreach ($map as $key => $spec) {
$words = idx($spec, 'keywords', array());
if (!is_array($words)) {
$words = array($words);
}
foreach ($words as $word_key => $word) {
$words[$word_key] = phutil_utf8_strtolower($word);
}
$words = array_unique($words);
$map[$key] = $words;
}
return $map;
}
/**
* Get the canonical keyword for a given priority constant.
*
* @return string|null Keyword, or `null` if no keyword is configured.
*/
public static function getKeywordForTaskPriority($priority) {
$map = self::getConfig();
$spec = idx($map, $priority);
if (!$spec) {
return null;
}
$keywords = idx($spec, 'keywords');
if (!$keywords) {
return null;
}
return head($keywords);
}
/**
* Get a map of supported alternate names for each priority.
*
* Keys are aliases, like "wish" and "wishlist". Values are canonical
* priority keywords, like "wishlist".
*
* @return map<string, string> Map of aliases to canonical priority keywords.
*/
public static function getTaskPriorityAliasMap() {
$keyword_map = self::getTaskPriorityKeywordsMap();
$result = array();
foreach ($keyword_map as $key => $keywords) {
$target = self::getKeywordForTaskPriority($key);
if ($target === null) {
continue;
}
// NOTE: Include the raw priority value, like "25", in the list of
// aliases. This supports legacy sources like saved EditEngine forms.
$result[$key] = $target;
foreach ($keywords as $keyword) {
$result[$keyword] = $target;
}
}
return $result;
}
/**
* Get the priorities and their related short (one-word) descriptions.
*
* @return map Priorities to short descriptions.
*/
public static function getShortNameMap() {
$map = self::getConfig();
foreach ($map as $key => $spec) {
$map[$key] = idx($spec, 'short', idx($spec, 'name', $key));
}
return $map;
}
/**
* Get a map from priority constants to their colors.
*
* @return map<int, string> Priorities to colors.
*/
public static function getColorMap() {
$map = self::getConfig();
foreach ($map as $key => $spec) {
$map[$key] = idx($spec, 'color', 'grey');
}
return $map;
}
/**
* Return the default priority for this instance of Phabricator.
*
* @return int The value of the default priority constant.
*/
public static function getDefaultPriority() {
return PhabricatorEnv::getEnvConfig('maniphest.default-priority');
}
/**
* Retrieve the full name of the priority level provided.
*
* @param int A priority level.
* @return string The priority name if the level is a valid one.
*/
public static function getTaskPriorityName($priority) {
return idx(self::getTaskPriorityMap(), $priority, $priority);
}
/**
* Retrieve the color of the priority level given
*
* @param int A priority level.
* @return string The color of the priority if the level is valid,
* or black if it is not.
*/
public static function getTaskPriorityColor($priority) {
return idx(self::getColorMap(), $priority, 'black');
}
public static function getTaskPriorityIcon($priority) {
return 'fa-arrow-right';
}
public static function getTaskPriorityFromKeyword($keyword) {
$map = self::getTaskPriorityKeywordsMap();
foreach ($map as $priority => $keywords) {
if (in_array($keyword, $keywords)) {
return $priority;
}
}
return null;
}
public static function isDisabledPriority($priority) {
$config = idx(self::getConfig(), $priority, array());
return idx($config, 'disabled', false);
}
public static function getConfig() {
$config = PhabricatorEnv::getEnvConfig('maniphest.priorities');
krsort($config);
return $config;
}
private static function isValidPriorityKeyword($keyword) {
if (!strlen($keyword) || strlen($keyword) > 64) {
return false;
}
// Alphanumeric, but not exclusively numeric
if (!preg_match('/^(?![0-9]*$)[a-zA-Z0-9]+$/', $keyword)) {
return false;
}
return true;
}
public static function validateConfiguration($config) {
if (!is_array($config)) {
throw new Exception(
pht(
'Configuration is not valid. Maniphest priority configurations '.
- 'must be dictionaries.',
- $config));
+ 'must be dictionaries.'));
}
$all_keywords = array();
foreach ($config as $key => $value) {
if (!ctype_digit((string)$key)) {
throw new Exception(
pht(
'Key "%s" is not a valid priority constant. Priority constants '.
'must be nonnegative integers.',
$key));
}
if (!is_array($value)) {
throw new Exception(
pht(
'Value for key "%s" should be a dictionary.',
$key));
}
PhutilTypeSpec::checkMap(
$value,
array(
'name' => 'string',
'keywords' => 'list<string>',
'short' => 'optional string',
'color' => 'optional string',
'disabled' => 'optional bool',
));
$keywords = $value['keywords'];
foreach ($keywords as $keyword) {
if (!self::isValidPriorityKeyword($keyword)) {
throw new Exception(
pht(
'Key "%s" is not a valid priority keyword. Priority keywords '.
'must be 1-64 alphanumeric characters and cannot be '.
'exclusively digits. For example, "%s" or "%s" are '.
'reasonable choices.',
$keyword,
'low',
'critical'));
}
if (isset($all_keywords[$keyword])) {
throw new Exception(
pht(
'Two different task priorities ("%s" and "%s") have the same '.
'keyword ("%s"). Keywords must uniquely identify priorities.',
$value['name'],
$all_keywords[$keyword],
$keyword));
}
$all_keywords[$keyword] = $value['name'];
}
}
}
}
diff --git a/src/applications/metamta/adapter/PhabricatorMailAdapter.php b/src/applications/metamta/adapter/PhabricatorMailAdapter.php
index 8c1a6c0ba7..7a0f1bae31 100644
--- a/src/applications/metamta/adapter/PhabricatorMailAdapter.php
+++ b/src/applications/metamta/adapter/PhabricatorMailAdapter.php
@@ -1,173 +1,174 @@
<?php
abstract class PhabricatorMailAdapter
extends Phobject {
private $key;
private $priority;
private $media;
private $options = array();
private $supportsInbound = true;
private $supportsOutbound = true;
private $mediaMap;
final public function getAdapterType() {
return $this->getPhobjectClassConstant('ADAPTERTYPE');
}
final public static function getAllAdapters() {
return id(new PhutilClassMapQuery())
->setAncestorClass(__CLASS__)
->setUniqueMethod('getAdapterType')
->execute();
}
abstract public function getSupportedMessageTypes();
abstract public function sendMessage(PhabricatorMailExternalMessage $message);
/**
* Return true if this adapter supports setting a "Message-ID" when sending
* email.
*
* This is an ugly implementation detail because mail threading is a horrible
* mess, implemented differently by every client in existence.
*/
public function supportsMessageIDHeader() {
return false;
}
final public function supportsMessageType($message_type) {
if ($this->mediaMap === null) {
$media_map = $this->getSupportedMessageTypes();
$media_map = array_fuse($media_map);
if ($this->media) {
$config_map = $this->media;
$config_map = array_fuse($config_map);
$media_map = array_intersect_key($media_map, $config_map);
}
$this->mediaMap = $media_map;
}
return isset($this->mediaMap[$message_type]);
}
final public function setMedia(array $media) {
$native_map = $this->getSupportedMessageTypes();
$native_map = array_fuse($native_map);
foreach ($media as $medium) {
if (!isset($native_map[$medium])) {
throw new Exception(
pht(
'Adapter ("%s") is configured for medium "%s", but this is not '.
'a supported delivery medium. Supported media are: %s.',
+ get_class($this),
$medium,
implode(', ', $native_map)));
}
}
$this->media = $media;
$this->mediaMap = null;
return $this;
}
final public function getMedia() {
return $this->media;
}
final public function setKey($key) {
$this->key = $key;
return $this;
}
final public function getKey() {
return $this->key;
}
final public function setPriority($priority) {
$this->priority = $priority;
return $this;
}
final public function getPriority() {
return $this->priority;
}
final public function setSupportsInbound($supports_inbound) {
$this->supportsInbound = $supports_inbound;
return $this;
}
final public function getSupportsInbound() {
return $this->supportsInbound;
}
final public function setSupportsOutbound($supports_outbound) {
$this->supportsOutbound = $supports_outbound;
return $this;
}
final public function getSupportsOutbound() {
return $this->supportsOutbound;
}
final public function getOption($key) {
if (!array_key_exists($key, $this->options)) {
throw new Exception(
pht(
'Mailer ("%s") is attempting to access unknown option ("%s").',
get_class($this),
$key));
}
return $this->options[$key];
}
final public function setOptions(array $options) {
$this->validateOptions($options);
$this->options = $options;
return $this;
}
abstract protected function validateOptions(array $options);
abstract public function newDefaultOptions();
final protected function guessIfHostSupportsMessageID($config, $host) {
// See T13265. Mailers like "SMTP" and "sendmail" usually allow us to
// set the "Message-ID" header to a value we choose, but we may not be
// able to if the mailer is being used as API glue and the outbound
// pathway ends up routing to a service with an SMTP API that selects
// its own "Message-ID" header, like Amazon SES.
// If users configured a behavior explicitly, use that behavior.
if ($config !== null) {
return $config;
}
// If the server we're connecting to is part of a service that we know
// does not support "Message-ID", guess that we don't support "Message-ID".
if ($host !== null) {
$host_blocklist = array(
'/\.amazonaws\.com\z/',
'/\.postmarkapp\.com\z/',
'/\.sendgrid\.net\z/',
);
$host = phutil_utf8_strtolower($host);
foreach ($host_blocklist as $regexp) {
if (preg_match($regexp, $host)) {
return false;
}
}
}
return true;
}
}
diff --git a/src/applications/notification/config/PhabricatorNotificationServersConfigType.php b/src/applications/notification/config/PhabricatorNotificationServersConfigType.php
index 5f0c1f7e2f..f13105a249 100644
--- a/src/applications/notification/config/PhabricatorNotificationServersConfigType.php
+++ b/src/applications/notification/config/PhabricatorNotificationServersConfigType.php
@@ -1,138 +1,140 @@
<?php
final class PhabricatorNotificationServersConfigType
extends PhabricatorJSONConfigType {
const TYPEKEY = 'cluster.notifications';
public function validateStoredValue(
PhabricatorConfigOption $option,
$value) {
foreach ($value as $index => $spec) {
if (!is_array($spec)) {
throw $this->newException(
pht(
'Notification server configuration is not valid: each entry in '.
'the list must be a dictionary describing a service, but '.
'the value with index "%s" is not a dictionary.',
$index));
}
}
$has_admin = false;
$has_client = false;
$map = array();
foreach ($value as $index => $spec) {
try {
PhutilTypeSpec::checkMap(
$spec,
array(
'type' => 'string',
'host' => 'string',
'port' => 'int',
'protocol' => 'string',
'path' => 'optional string',
'disabled' => 'optional bool',
));
} catch (Exception $ex) {
throw $this->newException(
pht(
'Notification server configuration has an invalid service '.
'specification (at index "%s"): %s.',
$index,
$ex->getMessage()));
}
$type = $spec['type'];
$host = $spec['host'];
$port = $spec['port'];
$protocol = $spec['protocol'];
$disabled = idx($spec, 'disabled');
switch ($type) {
case 'admin':
if (!$disabled) {
$has_admin = true;
}
break;
case 'client':
if (!$disabled) {
$has_client = true;
}
break;
default:
throw $this->newException(
pht(
'Notification server configuration describes an invalid '.
'host ("%s", at index "%s") with an unrecognized type ("%s"). '.
'Valid types are "%s" or "%s".',
$host,
$index,
$type,
'admin',
'client'));
}
switch ($protocol) {
case 'http':
case 'https':
break;
default:
throw $this->newException(
pht(
'Notification server configuration describes an invalid '.
'host ("%s", at index "%s") with an invalid protocol ("%s"). '.
'Valid protocols are "%s" or "%s".',
$host,
$index,
$protocol,
'http',
'https'));
}
$path = idx($spec, 'path');
if ($type == 'admin' && strlen($path)) {
throw $this->newException(
pht(
'Notification server configuration describes an invalid host '.
'("%s", at index "%s"). This is an "admin" service but it has a '.
'"path" property. This property is only valid for "client" '.
- 'services.'));
+ 'services.',
+ $host,
+ $index));
}
// We can't guarantee that you didn't just give the same host two
// different names in DNS, but this check can catch silly copy/paste
// mistakes.
$key = "{$host}:{$port}";
if (isset($map[$key])) {
throw $this->newException(
pht(
'Notification server configuration is invalid: it describes the '.
'same host and port ("%s") multiple times. Each host and port '.
'combination should appear only once in the list.',
$key));
}
$map[$key] = true;
}
if ($value) {
if (!$has_admin) {
throw $this->newException(
pht(
'Notification server configuration is invalid: it does not '.
'specify any enabled servers with type "admin". Notifications '.
'require at least one active "admin" server.'));
}
if (!$has_client) {
throw $this->newException(
pht(
'Notification server configuration is invalid: it does not '.
'specify any enabled servers with type "client". Notifications '.
'require at least one active "client" server.'));
}
}
}
}
diff --git a/src/applications/nuance/github/__tests__/NuanceGitHubRawEventTestCase.php b/src/applications/nuance/github/__tests__/NuanceGitHubRawEventTestCase.php
index 29b9e3c0a9..4af9a440e2 100644
--- a/src/applications/nuance/github/__tests__/NuanceGitHubRawEventTestCase.php
+++ b/src/applications/nuance/github/__tests__/NuanceGitHubRawEventTestCase.php
@@ -1,113 +1,114 @@
<?php
final class NuanceGitHubRawEventTestCase
extends PhabricatorTestCase {
public function testIssueEvents() {
$path = dirname(__FILE__).'/issueevents/';
$cases = $this->readTestCases($path);
foreach ($cases as $name => $info) {
$input = $info['input'];
$expect = $info['expect'];
$event = NuanceGitHubRawEvent::newEvent(
NuanceGitHubRawEvent::TYPE_ISSUE,
$input);
$this->assertGitHubRawEventParse($expect, $event, $name);
}
}
public function testRepositoryEvents() {
$path = dirname(__FILE__).'/repositoryevents/';
$cases = $this->readTestCases($path);
foreach ($cases as $name => $info) {
$input = $info['input'];
$expect = $info['expect'];
$event = NuanceGitHubRawEvent::newEvent(
NuanceGitHubRawEvent::TYPE_REPOSITORY,
$input);
$this->assertGitHubRawEventParse($expect, $event, $name);
}
}
private function assertGitHubRawEventParse(
array $expect,
NuanceGitHubRawEvent $event,
$name) {
$actual = array(
'repository.name.full' => $event->getRepositoryFullName(),
'is.issue' => $event->isIssueEvent(),
'is.pull' => $event->isPullRequestEvent(),
'issue.number' => $event->getIssueNumber(),
'pull.number' => $event->getPullRequestNumber(),
'id' => $event->getID(),
'uri' => $event->getURI(),
'title.full' => $event->getEventFullTitle(),
'comment' => $event->getComment(),
'actor.id' => $event->getActorGitHubUserID(),
);
// Only verify the keys which are actually present in the test. This
// allows tests to specify only relevant keys.
$actual = array_select_keys($actual, array_keys($expect));
ksort($expect);
ksort($actual);
$this->assertEqual($expect, $actual, $name);
}
private function readTestCases($path) {
$files = Filesystem::listDirectory($path, $include_hidden = false);
$tests = array();
foreach ($files as $file) {
$data = Filesystem::readFile($path.$file);
$parts = preg_split('/^~{5,}$/m', $data);
if (count($parts) < 2) {
throw new Exception(
pht(
'Expected test file "%s" to contain an input section in JSON, '.
'then an expected result section in JSON, with the two sections '.
'separated by a line of "~~~~~", but the divider is not present '.
'in the file.',
$file));
} else if (count($parts) > 2) {
throw new Exception(
pht(
'Expected test file "%s" to contain exactly two sections, '.
- 'but it has more than two sections.'));
+ 'but it has more than two sections.',
+ $file));
}
list($input, $expect) = $parts;
try {
$input = phutil_json_decode($input);
$expect = phutil_json_decode($expect);
} catch (Exception $ex) {
throw new PhutilProxyException(
pht(
'Exception while decoding test data for test "%s".',
$file),
$ex);
}
$tests[$file] = array(
'input' => $input,
'expect' => $expect,
);
}
return $tests;
}
}
diff --git a/src/applications/phragment/controller/PhragmentRevertController.php b/src/applications/phragment/controller/PhragmentRevertController.php
index e9d56eb112..b9aa050327 100644
--- a/src/applications/phragment/controller/PhragmentRevertController.php
+++ b/src/applications/phragment/controller/PhragmentRevertController.php
@@ -1,79 +1,78 @@
<?php
final class PhragmentRevertController extends PhragmentController {
public function handleRequest(AphrontRequest $request) {
$viewer = $request->getViewer();
$id = $request->getURIData('id');
$dblob = $request->getURIData('dblob');
$fragment = id(new PhragmentFragmentQuery())
->setViewer($viewer)
->withPaths(array($dblob))
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
if ($fragment === null) {
return new Aphront404Response();
}
$version = id(new PhragmentFragmentVersionQuery())
->setViewer($viewer)
->withFragmentPHIDs(array($fragment->getPHID()))
->withIDs(array($id))
->executeOne();
if ($version === null) {
return new Aphront404Response();
}
if ($request->isDialogFormPost()) {
$file_phid = $version->getFilePHID();
$file = null;
if ($file_phid !== null) {
$file = id(new PhabricatorFileQuery())
->setViewer($viewer)
->withPHIDs(array($file_phid))
->executeOne();
if ($file === null) {
throw new Exception(
pht('The file associated with this version was not found.'));
}
}
if ($file === null) {
$fragment->deleteFile($viewer);
} else {
$fragment->updateFromFile($viewer, $file);
}
return id(new AphrontRedirectResponse())
->setURI($this->getApplicationURI('/history/'.$dblob));
}
return $this->createDialog($fragment, $version);
}
public function createDialog(
PhragmentFragment $fragment,
PhragmentFragmentVersion $version) {
$viewer = $this->getViewer();
$dialog = id(new AphrontDialogView())
->setTitle(pht('Really revert this fragment?'))
->setUser($this->getViewer())
->addSubmitButton(pht('Revert'))
->addCancelButton(pht('Cancel'))
->appendParagraph(pht(
'Reverting this fragment to version %d will create a new version of '.
'the fragment. It will not delete any version history.',
- $version->getSequence(),
$version->getSequence()));
return id(new AphrontDialogResponse())->setDialog($dialog);
}
}
diff --git a/src/applications/policy/storage/PhabricatorPolicy.php b/src/applications/policy/storage/PhabricatorPolicy.php
index 66a7d9e3be..7904f17927 100644
--- a/src/applications/policy/storage/PhabricatorPolicy.php
+++ b/src/applications/policy/storage/PhabricatorPolicy.php
@@ -1,515 +1,514 @@
<?php
final class PhabricatorPolicy
extends PhabricatorPolicyDAO
implements
PhabricatorPolicyInterface,
PhabricatorDestructibleInterface {
const ACTION_ALLOW = 'allow';
const ACTION_DENY = 'deny';
private $name;
private $shortName;
private $type;
private $href;
private $workflow;
private $icon;
protected $rules = array();
protected $defaultAction = self::ACTION_DENY;
private $ruleObjects = self::ATTACHABLE;
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'rules' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'defaultAction' => 'text32',
),
self::CONFIG_KEY_SCHEMA => array(
'key_phid' => null,
'phid' => array(
'columns' => array('phid'),
'unique' => true,
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhabricatorPolicyPHIDTypePolicy::TYPECONST);
}
public static function newFromPolicyAndHandle(
$policy_identifier,
PhabricatorObjectHandle $handle = null) {
$is_global = PhabricatorPolicyQuery::isGlobalPolicy($policy_identifier);
if ($is_global) {
return PhabricatorPolicyQuery::getGlobalPolicy($policy_identifier);
}
$policy = PhabricatorPolicyQuery::getObjectPolicy($policy_identifier);
if ($policy) {
return $policy;
}
if (!$handle) {
throw new Exception(
pht(
"Policy identifier is an object PHID ('%s'), but no object handle ".
"was provided. A handle must be provided for object policies.",
$policy_identifier));
}
$handle_phid = $handle->getPHID();
if ($policy_identifier != $handle_phid) {
throw new Exception(
pht(
"Policy identifier is an object PHID ('%s'), but the provided ".
"handle has a different PHID ('%s'). The handle must correspond ".
"to the policy identifier.",
$policy_identifier,
$handle_phid));
}
$policy = id(new PhabricatorPolicy())
->setPHID($policy_identifier)
->setHref($handle->getURI());
$phid_type = phid_get_type($policy_identifier);
switch ($phid_type) {
case PhabricatorProjectProjectPHIDType::TYPECONST:
$policy
->setType(PhabricatorPolicyType::TYPE_PROJECT)
->setName($handle->getName())
->setIcon($handle->getIcon());
break;
case PhabricatorPeopleUserPHIDType::TYPECONST:
$policy->setType(PhabricatorPolicyType::TYPE_USER);
$policy->setName($handle->getFullName());
break;
case PhabricatorPolicyPHIDTypePolicy::TYPECONST:
// TODO: This creates a weird handle-based version of a rule policy.
// It behaves correctly, but can't be applied since it doesn't have
// any rules. It is used to render transactions, and might need some
// cleanup.
break;
default:
$policy->setType(PhabricatorPolicyType::TYPE_MASKED);
$policy->setName($handle->getFullName());
break;
}
$policy->makeEphemeral();
return $policy;
}
public function setType($type) {
$this->type = $type;
return $this;
}
public function getType() {
if (!$this->type) {
return PhabricatorPolicyType::TYPE_CUSTOM;
}
return $this->type;
}
public function setName($name) {
$this->name = $name;
return $this;
}
public function getName() {
if (!$this->name) {
return pht('Custom Policy');
}
return $this->name;
}
public function setShortName($short_name) {
$this->shortName = $short_name;
return $this;
}
public function getShortName() {
if ($this->shortName) {
return $this->shortName;
}
return $this->getName();
}
public function setHref($href) {
$this->href = $href;
return $this;
}
public function getHref() {
return $this->href;
}
public function setWorkflow($workflow) {
$this->workflow = $workflow;
return $this;
}
public function getWorkflow() {
return $this->workflow;
}
public function setIcon($icon) {
$this->icon = $icon;
return $this;
}
public function getIcon() {
if ($this->icon) {
return $this->icon;
}
switch ($this->getType()) {
case PhabricatorPolicyType::TYPE_GLOBAL:
static $map = array(
PhabricatorPolicies::POLICY_PUBLIC => 'fa-globe',
PhabricatorPolicies::POLICY_USER => 'fa-users',
PhabricatorPolicies::POLICY_ADMIN => 'fa-eye',
PhabricatorPolicies::POLICY_NOONE => 'fa-ban',
);
return idx($map, $this->getPHID(), 'fa-question-circle');
case PhabricatorPolicyType::TYPE_USER:
return 'fa-user';
case PhabricatorPolicyType::TYPE_PROJECT:
return 'fa-briefcase';
case PhabricatorPolicyType::TYPE_CUSTOM:
case PhabricatorPolicyType::TYPE_MASKED:
return 'fa-certificate';
default:
return 'fa-question-circle';
}
}
public function getSortKey() {
return sprintf(
'%02d%s',
PhabricatorPolicyType::getPolicyTypeOrder($this->getType()),
$this->getSortName());
}
private function getSortName() {
if ($this->getType() == PhabricatorPolicyType::TYPE_GLOBAL) {
static $map = array(
PhabricatorPolicies::POLICY_PUBLIC => 0,
PhabricatorPolicies::POLICY_USER => 1,
PhabricatorPolicies::POLICY_ADMIN => 2,
PhabricatorPolicies::POLICY_NOONE => 3,
);
return idx($map, $this->getPHID());
}
return $this->getName();
}
public static function getPolicyExplanation(
PhabricatorUser $viewer,
$policy) {
$type = phid_get_type($policy);
if ($type === PhabricatorProjectProjectPHIDType::TYPECONST) {
$handle = id(new PhabricatorHandleQuery())
->setViewer($viewer)
->withPHIDs(array($policy))
->executeOne();
return pht(
'Members of the project "%s" can take this action.',
$handle->getFullName());
}
return self::getOpaquePolicyExplanation($viewer, $policy);
}
public static function getOpaquePolicyExplanation(
PhabricatorUser $viewer,
$policy) {
$rule = PhabricatorPolicyQuery::getObjectPolicyRule($policy);
if ($rule) {
return $rule->getPolicyExplanation();
}
switch ($policy) {
case PhabricatorPolicies::POLICY_PUBLIC:
return pht(
'This object is public and can be viewed by anyone, even if they '.
'do not have a Phabricator account.');
case PhabricatorPolicies::POLICY_USER:
return pht('Logged in users can take this action.');
case PhabricatorPolicies::POLICY_ADMIN:
return pht('Administrators can take this action.');
case PhabricatorPolicies::POLICY_NOONE:
return pht('By default, no one can take this action.');
default:
$handle = id(new PhabricatorHandleQuery())
->setViewer($viewer)
->withPHIDs(array($policy))
->executeOne();
$type = phid_get_type($policy);
if ($type == PhabricatorProjectProjectPHIDType::TYPECONST) {
return pht(
'Members of a particular project can take this action. (You '.
'can not see this object, so the name of this project is '.
- 'restricted.)',
- $handle->getFullName());
+ 'restricted.)');
} else if ($type == PhabricatorPeopleUserPHIDType::TYPECONST) {
return pht(
'%s can take this action.',
$handle->getFullName());
} else if ($type == PhabricatorPolicyPHIDTypePolicy::TYPECONST) {
return pht(
'This object has a custom policy controlling who can take this '.
'action.');
} else {
return pht(
'This object has an unknown or invalid policy setting ("%s").',
$policy);
}
}
}
public function getFullName() {
switch ($this->getType()) {
case PhabricatorPolicyType::TYPE_PROJECT:
return pht('Members of Project: %s', $this->getName());
case PhabricatorPolicyType::TYPE_MASKED:
return pht('Other: %s', $this->getName());
case PhabricatorPolicyType::TYPE_USER:
return pht('Only User: %s', $this->getName());
default:
return $this->getName();
}
}
public function newRef(PhabricatorUser $viewer) {
return id(new PhabricatorPolicyRef())
->setViewer($viewer)
->setPolicy($this);
}
public function isProjectPolicy() {
return ($this->getType() === PhabricatorPolicyType::TYPE_PROJECT);
}
public function isCustomPolicy() {
return ($this->getType() === PhabricatorPolicyType::TYPE_CUSTOM);
}
public function isMaskedPolicy() {
return ($this->getType() === PhabricatorPolicyType::TYPE_MASKED);
}
/**
* Return a list of custom rule classes (concrete subclasses of
* @{class:PhabricatorPolicyRule}) this policy uses.
*
* @return list<string> List of class names.
*/
public function getCustomRuleClasses() {
$classes = array();
foreach ($this->getRules() as $rule) {
if (!is_array($rule)) {
// This rule is invalid. We'll reject it later, but don't need to
// extract anything from it for now.
continue;
}
$class = idx($rule, 'rule');
try {
if (class_exists($class)) {
$classes[$class] = $class;
}
} catch (Exception $ex) {
continue;
}
}
return array_keys($classes);
}
/**
* Return a list of all values used by a given rule class to implement this
* policy. This is used to bulk load data (like project memberships) in order
* to apply policy filters efficiently.
*
* @param string Policy rule classname.
* @return list<wild> List of values used in this policy.
*/
public function getCustomRuleValues($rule_class) {
$values = array();
foreach ($this->getRules() as $rule) {
if ($rule['rule'] == $rule_class) {
$values[] = $rule['value'];
}
}
return $values;
}
public function attachRuleObjects(array $objects) {
$this->ruleObjects = $objects;
return $this;
}
public function getRuleObjects() {
return $this->assertAttached($this->ruleObjects);
}
/**
* Return `true` if this policy is stronger (more restrictive) than some
* other policy.
*
* Because policies are complicated, determining which policies are
* "stronger" is not trivial. This method uses a very coarse working
* definition of policy strength which is cheap to compute, unambiguous,
* and intuitive in the common cases.
*
* This method returns `true` if the //class// of this policy is stronger
* than the other policy, even if the policies are (or might be) the same in
* practice. For example, "Members of Project X" is considered a stronger
* policy than "All Users", even though "Project X" might (in some rare
* cases) contain every user.
*
* Generally, the ordering here is:
*
* - Public
* - All Users
* - (Everything Else)
* - No One
*
* In the "everything else" bucket, we can't make any broad claims about
* which policy is stronger (and we especially can't make those claims
* cheaply).
*
* Even if we fully evaluated each policy, the two policies might be
* "Members of X" and "Members of Y", each of which permits access to some
* set of unique users. In this case, neither is strictly stronger than
* the other.
*
* @param PhabricatorPolicy Other policy.
* @return bool `true` if this policy is more restrictive than the other
* policy.
*/
public function isStrongerThan(PhabricatorPolicy $other) {
$this_policy = $this->getPHID();
$other_policy = $other->getPHID();
$strengths = array(
PhabricatorPolicies::POLICY_PUBLIC => -2,
PhabricatorPolicies::POLICY_USER => -1,
// (Default policies have strength 0.)
PhabricatorPolicies::POLICY_NOONE => 1,
);
$this_strength = idx($strengths, $this->getPHID(), 0);
$other_strength = idx($strengths, $other->getPHID(), 0);
return ($this_strength > $other_strength);
}
public function isValidPolicyForEdit() {
return $this->getType() !== PhabricatorPolicyType::TYPE_MASKED;
}
public static function getSpecialRules(
PhabricatorPolicyInterface $object,
PhabricatorUser $viewer,
$capability,
$active_only) {
$exceptions = array();
if ($object instanceof PhabricatorPolicyCodexInterface) {
$codex = id(PhabricatorPolicyCodex::newFromObject($object, $viewer))
->setCapability($capability);
$rules = $codex->getPolicySpecialRuleDescriptions();
foreach ($rules as $rule) {
$is_active = $rule->getIsActive();
if ($is_active) {
$rule_capabilities = $rule->getCapabilities();
if ($rule_capabilities) {
if (!in_array($capability, $rule_capabilities)) {
$is_active = false;
}
}
}
if (!$is_active && $active_only) {
continue;
}
$description = $rule->getDescription();
if (!$is_active) {
$description = phutil_tag(
'span',
array(
'class' => 'phui-policy-section-view-inactive-rule',
),
$description);
}
$exceptions[] = $description;
}
}
if (!$exceptions) {
if (method_exists($object, 'describeAutomaticCapability')) {
$exceptions = (array)$object->describeAutomaticCapability($capability);
$exceptions = array_filter($exceptions);
}
}
return $exceptions;
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
);
}
public function getPolicy($capability) {
// NOTE: We implement policies only so we can comply with the interface.
// The actual query skips them, as enforcing policies on policies seems
// perilous and isn't currently required by the application.
return PhabricatorPolicies::POLICY_PUBLIC;
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return false;
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$this->delete();
}
}
diff --git a/src/applications/project/icon/PhabricatorProjectIconSet.php b/src/applications/project/icon/PhabricatorProjectIconSet.php
index b128a35cad..5462da78d6 100644
--- a/src/applications/project/icon/PhabricatorProjectIconSet.php
+++ b/src/applications/project/icon/PhabricatorProjectIconSet.php
@@ -1,508 +1,509 @@
<?php
final class PhabricatorProjectIconSet
extends PhabricatorIconSet {
const ICONSETKEY = 'projects';
const SPECIAL_MILESTONE = 'milestone';
public function getSelectIconTitleText() {
return pht('Choose Project Icon');
}
public static function getDefaultConfiguration() {
return array(
array(
'key' => 'project',
'icon' => 'fa-briefcase',
'name' => pht('Project'),
'default' => true,
'image' => 'v3/briefcase.png',
),
array(
'key' => 'tag',
'icon' => 'fa-tags',
'name' => pht('Tag'),
'image' => 'v3/tag.png',
),
array(
'key' => 'policy',
'icon' => 'fa-lock',
'name' => pht('Policy'),
'image' => 'v3/lock.png',
),
array(
'key' => 'group',
'icon' => 'fa-users',
'name' => pht('Group'),
'image' => 'v3/people.png',
),
array(
'key' => 'folder',
'icon' => 'fa-folder',
'name' => pht('Folder'),
'image' => 'v3/folder.png',
),
array(
'key' => 'timeline',
'icon' => 'fa-calendar',
'name' => pht('Timeline'),
'image' => 'v3/calendar.png',
),
array(
'key' => 'goal',
'icon' => 'fa-flag-checkered',
'name' => pht('Goal'),
'image' => 'v3/flag.png',
),
array(
'key' => 'release',
'icon' => 'fa-truck',
'name' => pht('Release'),
'image' => 'v3/truck.png',
),
array(
'key' => 'bugs',
'icon' => 'fa-bug',
'name' => pht('Bugs'),
'image' => 'v3/bug.png',
),
array(
'key' => 'cleanup',
'icon' => 'fa-trash-o',
'name' => pht('Cleanup'),
'image' => 'v3/trash.png',
),
array(
'key' => 'umbrella',
'icon' => 'fa-umbrella',
'name' => pht('Umbrella'),
'image' => 'v3/umbrella.png',
),
array(
'key' => 'communication',
'icon' => 'fa-envelope',
'name' => pht('Communication'),
'image' => 'v3/mail.png',
),
array(
'key' => 'organization',
'icon' => 'fa-building',
'name' => pht('Organization'),
'image' => 'v3/organization.png',
),
array(
'key' => 'infrastructure',
'icon' => 'fa-cloud',
'name' => pht('Infrastructure'),
'image' => 'v3/cloud.png',
),
array(
'key' => 'account',
'icon' => 'fa-credit-card',
'name' => pht('Account'),
'image' => 'v3/creditcard.png',
),
array(
'key' => 'experimental',
'icon' => 'fa-flask',
'name' => pht('Experimental'),
'image' => 'v3/experimental.png',
),
array(
'key' => 'milestone',
'icon' => 'fa-map-marker',
'name' => pht('Milestone'),
'special' => self::SPECIAL_MILESTONE,
'image' => 'v3/marker.png',
),
);
}
protected function newIcons() {
$map = self::getIconSpecifications();
$icons = array();
foreach ($map as $spec) {
$special = idx($spec, 'special');
if ($special === self::SPECIAL_MILESTONE) {
continue;
}
$icons[] = id(new PhabricatorIconSetIcon())
->setKey($spec['key'])
->setIsDisabled(idx($spec, 'disabled'))
->setIcon($spec['icon'])
->setLabel($spec['name']);
}
return $icons;
}
private static function getIconSpecifications() {
return PhabricatorEnv::getEnvConfig('projects.icons');
}
public static function getDefaultIconKey() {
$icons = self::getIconSpecifications();
foreach ($icons as $icon) {
if (idx($icon, 'default')) {
return $icon['key'];
}
}
return null;
}
public static function getIconIcon($key) {
$spec = self::getIconSpec($key);
return idx($spec, 'icon', null);
}
public static function getIconName($key) {
$spec = self::getIconSpec($key);
return idx($spec, 'name', null);
}
public static function getIconImage($key) {
$spec = self::getIconSpec($key);
return idx($spec, 'image', 'v3/briefcase.png');
}
private static function getIconSpec($key) {
$icons = self::getIconSpecifications();
foreach ($icons as $icon) {
if (idx($icon, 'key') === $key) {
return $icon;
}
}
return array();
}
public static function getMilestoneIconKey() {
$icons = self::getIconSpecifications();
foreach ($icons as $icon) {
if (idx($icon, 'special') === self::SPECIAL_MILESTONE) {
return idx($icon, 'key');
}
}
return null;
}
public static function validateConfiguration($config) {
if (!is_array($config)) {
throw new Exception(
pht('Configuration must be a list of project icon specifications.'));
}
foreach ($config as $idx => $value) {
if (!is_array($value)) {
throw new Exception(
pht(
'Value for index "%s" should be a dictionary.',
$idx));
}
PhutilTypeSpec::checkMap(
$value,
array(
'key' => 'string',
'name' => 'string',
'icon' => 'string',
'image' => 'optional string',
'special' => 'optional string',
'disabled' => 'optional bool',
'default' => 'optional bool',
));
if (!preg_match('/^[a-z]{1,32}\z/', $value['key'])) {
throw new Exception(
pht(
'Icon key "%s" is not a valid icon key. Icon keys must be 1-32 '.
'characters long and contain only lowercase letters. For example, '.
'"%s" and "%s" are reasonable keys.',
+ $value['key'],
'tag',
'group'));
}
$special = idx($value, 'special');
$valid = array(
self::SPECIAL_MILESTONE => true,
);
if ($special !== null) {
if (empty($valid[$special])) {
throw new Exception(
pht(
'Icon special attribute "%s" is not valid. Recognized special '.
'attributes are: %s.',
$special,
implode(', ', array_keys($valid))));
}
}
}
$default = null;
$milestone = null;
$keys = array();
foreach ($config as $idx => $value) {
$key = $value['key'];
if (isset($keys[$key])) {
throw new Exception(
pht(
'Project icons must have unique keys, but two icons share the '.
'same key ("%s").',
$key));
} else {
$keys[$key] = true;
}
$is_disabled = idx($value, 'disabled');
$image = idx($value, 'image');
if ($image !== null) {
$builtin = idx($value, 'image');
$builtin_map = id(new PhabricatorFilesOnDiskBuiltinFile())
->getProjectBuiltinFiles();
$builtin_map = array_flip($builtin_map);
$root = dirname(phutil_get_library_root('phabricator'));
$image = $root.'/resources/builtin/projects/'.$builtin;
if (!array_key_exists($image, $builtin_map)) {
throw new Exception(
pht(
'The project image ("%s") specified for ("%s") '.
'was not found in the folder "resources/builtin/projects/".',
$builtin,
$key));
}
}
if (idx($value, 'default')) {
if ($default === null) {
if ($is_disabled) {
throw new Exception(
pht(
'The project icon marked as the default icon ("%s") must not '.
'be disabled.',
$key));
}
$default = $value;
} else {
$original_key = $default['key'];
throw new Exception(
pht(
'Two different icons ("%s", "%s") are marked as the default '.
'icon. Only one icon may be marked as the default.',
$key,
$original_key));
}
}
$special = idx($value, 'special');
if ($special === self::SPECIAL_MILESTONE) {
if ($milestone === null) {
if ($is_disabled) {
throw new Exception(
pht(
'The project icon ("%s") with special attribute "%s" must '.
'not be disabled',
$key,
self::SPECIAL_MILESTONE));
}
$milestone = $value;
} else {
$original_key = $milestone['key'];
throw new Exception(
pht(
'Two different icons ("%s", "%s") are marked with special '.
'attribute "%s". Only one icon may be marked with this '.
'attribute.',
$key,
$original_key,
self::SPECIAL_MILESTONE));
}
}
}
if ($default === null) {
throw new Exception(
pht(
'Project icons must include one icon marked as the "%s" icon, '.
'but no such icon exists.',
'default'));
}
if ($milestone === null) {
throw new Exception(
pht(
'Project icons must include one icon marked with special attribute '.
'"%s", but no such icon exists.',
self::SPECIAL_MILESTONE));
}
}
private static function getColorSpecifications() {
return PhabricatorEnv::getEnvConfig('projects.colors');
}
public static function getColorMap() {
$specifications = self::getColorSpecifications();
return ipull($specifications, 'name', 'key');
}
public static function getDefaultColorKey() {
$specifications = self::getColorSpecifications();
foreach ($specifications as $specification) {
if (idx($specification, 'default')) {
return $specification['key'];
}
}
return null;
}
private static function getAvailableColorKeys() {
$list = array();
$specifications = self::getDefaultColorMap();
foreach ($specifications as $specification) {
$list[] = $specification['key'];
}
return $list;
}
public static function getColorName($color_key) {
$map = self::getColorMap();
return idx($map, $color_key);
}
public static function getDefaultColorMap() {
return array(
array(
'key' => PHUITagView::COLOR_RED,
'name' => pht('Red'),
),
array(
'key' => PHUITagView::COLOR_ORANGE,
'name' => pht('Orange'),
),
array(
'key' => PHUITagView::COLOR_YELLOW,
'name' => pht('Yellow'),
),
array(
'key' => PHUITagView::COLOR_GREEN,
'name' => pht('Green'),
),
array(
'key' => PHUITagView::COLOR_BLUE,
'name' => pht('Blue'),
'default' => true,
),
array(
'key' => PHUITagView::COLOR_INDIGO,
'name' => pht('Indigo'),
),
array(
'key' => PHUITagView::COLOR_VIOLET,
'name' => pht('Violet'),
),
array(
'key' => PHUITagView::COLOR_PINK,
'name' => pht('Pink'),
),
array(
'key' => PHUITagView::COLOR_GREY,
'name' => pht('Grey'),
),
array(
'key' => PHUITagView::COLOR_CHECKERED,
'name' => pht('Checkered'),
),
);
}
public static function validateColorConfiguration($config) {
if (!is_array($config)) {
throw new Exception(
pht('Configuration must be a list of project color specifications.'));
}
$available_keys = self::getAvailableColorKeys();
$available_keys = array_fuse($available_keys);
foreach ($config as $idx => $value) {
if (!is_array($value)) {
throw new Exception(
pht(
'Value for index "%s" should be a dictionary.',
$idx));
}
PhutilTypeSpec::checkMap(
$value,
array(
'key' => 'string',
'name' => 'string',
'default' => 'optional bool',
));
$key = $value['key'];
if (!isset($available_keys[$key])) {
throw new Exception(
pht(
'Color key "%s" is not a valid color key. The supported color '.
'keys are: %s.',
$key,
implode(', ', $available_keys)));
}
}
$default = null;
$keys = array();
foreach ($config as $idx => $value) {
$key = $value['key'];
if (isset($keys[$key])) {
throw new Exception(
pht(
'Project colors must have unique keys, but two icons share the '.
'same key ("%s").',
$key));
} else {
$keys[$key] = true;
}
if (idx($value, 'default')) {
if ($default === null) {
$default = $value;
} else {
$original_key = $default['key'];
throw new Exception(
pht(
'Two different colors ("%s", "%s") are marked as the default '.
'color. Only one color may be marked as the default.',
$key,
$original_key));
}
}
}
if ($default === null) {
throw new Exception(
pht(
'Project colors must include one color marked as the "%s" color, '.
'but no such color exists.',
'default'));
}
}
}
diff --git a/src/applications/search/ferret/function/FerretSearchFunction.php b/src/applications/search/ferret/function/FerretSearchFunction.php
index 60886c2fb6..8019a741ca 100644
--- a/src/applications/search/ferret/function/FerretSearchFunction.php
+++ b/src/applications/search/ferret/function/FerretSearchFunction.php
@@ -1,122 +1,124 @@
<?php
abstract class FerretSearchFunction
extends Phobject {
abstract public function getFerretFunctionName();
abstract public function getFerretFieldKey();
abstract public function supportsObject(PhabricatorFerretInterface $object);
final public static function getNormalizedFunctionName($name) {
return phutil_utf8_strtolower($name);
}
final public static function validateFerretFunctionName($function_name) {
if (!preg_match('/^[a-zA-Z-]+\z/', $function_name)) {
throw new Exception(
pht(
'Ferret search engine function name ("%s") is invalid. Function '.
'names must be nonempty and may only contain latin letters and '.
- 'hyphens.'));
+ 'hyphens.',
+ $function_name));
}
}
final public static function validateFerretFunctionFieldKey($field_key) {
if (!preg_match('/^[a-z]{4}\z/', $field_key)) {
throw new Exception(
pht(
'Ferret search engine field key ("%s") is invalid. Field keys '.
'must be exactly four characters long and contain only '.
'lowercase latin letters.',
$field_key));
}
}
final public static function newFerretSearchFunctions() {
$extensions = PhabricatorFulltextEngineExtension::getAllExtensions();
$function_map = array();
$field_map = array();
$results = array();
foreach ($extensions as $extension) {
$functions = $extension->newFerretSearchFunctions();
if (!is_array($functions)) {
throw new Exception(
pht(
'Expected fulltext engine extension ("%s") to return a '.
'list of functions from "newFerretSearchFunctions()", '.
'got "%s".',
get_class($extension),
phutil_describe_type($functions)));
}
foreach ($functions as $idx => $function) {
if (!($function instanceof FerretSearchFunction)) {
throw new Exception(
pht(
'Expected fulltext engine extension ("%s") to return a list '.
'of "FerretSearchFunction" objects from '.
'"newFerretSearchFunctions()", but found something else '.
'("%s") at index "%s".',
get_class($extension),
phutil_describe_type($function),
$idx));
}
$function_name = $function->getFerretFunctionName();
self::validateFerretFunctionName($function_name);
$normal_name = self::getNormalizedFunctionName(
$function_name);
if ($normal_name !== $function_name) {
throw new Exception(
pht(
'Ferret function "%s" is specified with a denormalized name. '.
'Instead, specify the function using the normalized '.
'function name ("%s").',
+ $function_name,
$normal_name));
}
if (isset($function_map[$function_name])) {
$other_extension = $function_map[$function_name];
throw new Exception(
pht(
'Two different fulltext engine extensions ("%s" and "%s") '.
'both define a search function with the same name ("%s"). '.
'Each function must have a unique name.',
get_class($extension),
get_class($other_extension),
$function_name));
}
$function_map[$function_name] = $extension;
$field_key = $function->getFerretFieldKey();
self::validateFerretFunctionFieldKey($field_key);
if (isset($field_map[$field_key])) {
$other_extension = $field_map[$field_key];
throw new Exception(
pht(
'Two different fulltext engine extensions ("%s" and "%s") '.
'both define a search function with the same key ("%s"). '.
'Each function must have a unique key.',
get_class($extension),
get_class($other_extension),
$field_key));
}
$field_map[$field_key] = $extension;
$results[$function_name] = $function;
}
}
ksort($results);
return $results;
}
}
diff --git a/src/applications/transactions/editengine/PhabricatorEditEngine.php b/src/applications/transactions/editengine/PhabricatorEditEngine.php
index 1c142847a3..ea1d7ed773 100644
--- a/src/applications/transactions/editengine/PhabricatorEditEngine.php
+++ b/src/applications/transactions/editengine/PhabricatorEditEngine.php
@@ -1,2744 +1,2746 @@
<?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->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.'));
+ '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/editengine/PhabricatorEditEngineSubtype.php b/src/applications/transactions/editengine/PhabricatorEditEngineSubtype.php
index f471fcd92f..d177595a2b 100644
--- a/src/applications/transactions/editengine/PhabricatorEditEngineSubtype.php
+++ b/src/applications/transactions/editengine/PhabricatorEditEngineSubtype.php
@@ -1,316 +1,317 @@
<?php
final class PhabricatorEditEngineSubtype
extends Phobject {
const SUBTYPE_DEFAULT = 'default';
private $key;
private $name;
private $icon;
private $tagText;
private $color;
private $childSubtypes = array();
private $childIdentifiers = array();
private $fieldConfiguration = array();
private $mutations;
public function setKey($key) {
$this->key = $key;
return $this;
}
public function getKey() {
return $this->key;
}
public function setName($name) {
$this->name = $name;
return $this;
}
public function getName() {
return $this->name;
}
public function setIcon($icon) {
$this->icon = $icon;
return $this;
}
public function getIcon() {
return $this->icon;
}
public function setTagText($text) {
$this->tagText = $text;
return $this;
}
public function getTagText() {
return $this->tagText;
}
public function setColor($color) {
$this->color = $color;
return $this;
}
public function getColor() {
return $this->color;
}
public function setChildSubtypes(array $child_subtypes) {
$this->childSubtypes = $child_subtypes;
return $this;
}
public function getChildSubtypes() {
return $this->childSubtypes;
}
public function setChildFormIdentifiers(array $child_identifiers) {
$this->childIdentifiers = $child_identifiers;
return $this;
}
public function getChildFormIdentifiers() {
return $this->childIdentifiers;
}
public function setMutations($mutations) {
$this->mutations = $mutations;
return $this;
}
public function getMutations() {
return $this->mutations;
}
public function hasTagView() {
return (bool)strlen($this->getTagText());
}
public function newTagView() {
$view = id(new PHUITagView())
->setType(PHUITagView::TYPE_OUTLINE)
->setName($this->getTagText());
$color = $this->getColor();
if ($color) {
$view->setColor($color);
}
return $view;
}
public function setSubtypeFieldConfiguration(
$subtype_key,
array $configuration) {
$this->fieldConfiguration[$subtype_key] = $configuration;
return $this;
}
public function getSubtypeFieldConfiguration($subtype_key) {
return idx($this->fieldConfiguration, $subtype_key);
}
public static function validateSubtypeKey($subtype) {
if (strlen($subtype) > 64) {
throw new Exception(
pht(
'Subtype "%s" is not valid: subtype keys must be no longer than '.
'64 bytes.',
$subtype));
}
if (strlen($subtype) < 3) {
throw new Exception(
pht(
'Subtype "%s" is not valid: subtype keys must have a minimum '.
'length of 3 bytes.',
$subtype));
}
if (!preg_match('/^[a-z]+\z/', $subtype)) {
throw new Exception(
pht(
'Subtype "%s" is not valid: subtype keys may only contain '.
'lowercase latin letters ("a" through "z").',
$subtype));
}
}
public static function validateConfiguration($config) {
if (!is_array($config)) {
throw new Exception(
pht(
'Subtype configuration is invalid: it must be a list of subtype '.
'specifications.'));
}
$map = array();
foreach ($config as $value) {
PhutilTypeSpec::checkMap(
$value,
array(
'key' => 'string',
'name' => 'string',
'tag' => 'optional string',
'color' => 'optional string',
'icon' => 'optional string',
'children' => 'optional map<string, wild>',
'fields' => 'optional map<string, wild>',
'mutations' => 'optional list<string>',
));
$key = $value['key'];
self::validateSubtypeKey($key);
if (isset($map[$key])) {
throw new Exception(
pht(
'Subtype configuration is invalid: two subtypes use the same '.
'key ("%s"). Each subtype must have a unique key.',
$key));
}
$map[$key] = true;
$name = $value['name'];
if (!strlen($name)) {
throw new Exception(
pht(
'Subtype configuration is invalid: subtype with key "%s" has '.
'no name. Subtypes must have a name.',
$key));
}
$children = idx($value, 'children');
if ($children) {
PhutilTypeSpec::checkMap(
$children,
array(
'subtypes' => 'optional list<string>',
'forms' => 'optional list<string|int>',
));
$child_subtypes = idx($children, 'subtypes');
$child_forms = idx($children, 'forms');
if ($child_subtypes && $child_forms) {
throw new Exception(
pht(
'Subtype configuration is invalid: subtype with key "%s" '.
'specifies both child subtypes and child forms. Specify one '.
- 'or the other, but not both.'));
+ 'or the other, but not both.',
+ $key));
}
}
$fields = idx($value, 'fields');
if ($fields) {
foreach ($fields as $field_key => $configuration) {
PhutilTypeSpec::checkMap(
$configuration,
array(
'disabled' => 'optional bool',
'name' => 'optional string',
));
}
}
}
if (!isset($map[self::SUBTYPE_DEFAULT])) {
throw new Exception(
pht(
'Subtype configuration is invalid: there is no subtype defined '.
'with key "%s". This subtype is required and must be defined.',
self::SUBTYPE_DEFAULT));
}
foreach ($config as $value) {
$key = idx($value, 'key');
$mutations = idx($value, 'mutations');
if (!$mutations) {
continue;
}
foreach ($mutations as $mutation) {
if (!isset($map[$mutation])) {
throw new Exception(
pht(
'Subtype configuration is invalid: subtype with key "%s" '.
'specifies that it can mutate into subtype "%s", but that is '.
'not a valid subtype.',
$key,
$mutation));
}
}
}
}
public static function newSubtypeMap(array $config) {
$map = array();
foreach ($config as $entry) {
$key = $entry['key'];
$name = $entry['name'];
$tag_text = idx($entry, 'tag');
if ($tag_text === null) {
if ($key != self::SUBTYPE_DEFAULT) {
$tag_text = phutil_utf8_strtoupper($name);
}
}
$color = idx($entry, 'color', 'blue');
$icon = idx($entry, 'icon', 'fa-drivers-license-o');
$subtype = id(new self())
->setKey($key)
->setName($name)
->setTagText($tag_text)
->setIcon($icon);
if ($color) {
$subtype->setColor($color);
}
$children = idx($entry, 'children', array());
$child_subtypes = idx($children, 'subtypes');
$child_forms = idx($children, 'forms');
if ($child_subtypes) {
$subtype->setChildSubtypes($child_subtypes);
}
if ($child_forms) {
$subtype->setChildFormIdentifiers($child_forms);
}
$field_configurations = idx($entry, 'fields');
if ($field_configurations) {
foreach ($field_configurations as $field_key => $field_configuration) {
$subtype->setSubtypeFieldConfiguration(
$field_key,
$field_configuration);
}
}
$subtype->setMutations(idx($entry, 'mutations'));
$map[$key] = $subtype;
}
return new PhabricatorEditEngineSubtypeMap($map);
}
public function newIconView() {
return id(new PHUIIconView())
->setIcon($this->getIcon(), $this->getColor());
}
}
diff --git a/src/infrastructure/cluster/PhabricatorDatabaseRefParser.php b/src/infrastructure/cluster/PhabricatorDatabaseRefParser.php
index 989095c52c..db498e8a82 100644
--- a/src/infrastructure/cluster/PhabricatorDatabaseRefParser.php
+++ b/src/infrastructure/cluster/PhabricatorDatabaseRefParser.php
@@ -1,219 +1,221 @@
<?php
final class PhabricatorDatabaseRefParser
extends Phobject {
private $defaultPort = 3306;
private $defaultUser;
private $defaultPass;
public function setDefaultPort($default_port) {
$this->defaultPort = $default_port;
return $this;
}
public function getDefaultPort() {
return $this->defaultPort;
}
public function setDefaultUser($default_user) {
$this->defaultUser = $default_user;
return $this;
}
public function getDefaultUser() {
return $this->defaultUser;
}
public function setDefaultPass($default_pass) {
$this->defaultPass = $default_pass;
return $this;
}
public function getDefaultPass() {
return $this->defaultPass;
}
public function newRefs(array $config) {
$default_port = $this->getDefaultPort();
$default_user = $this->getDefaultUser();
$default_pass = $this->getDefaultPass();
$refs = array();
$master_count = 0;
foreach ($config as $key => $server) {
$host = $server['host'];
$port = idx($server, 'port', $default_port);
$user = idx($server, 'user', $default_user);
$disabled = idx($server, 'disabled', false);
$pass = idx($server, 'pass');
if ($pass) {
$pass = new PhutilOpaqueEnvelope($pass);
} else {
$pass = clone $default_pass;
}
$role = $server['role'];
$is_master = ($role == 'master');
$use_persistent = (bool)idx($server, 'persistent', false);
$ref = id(new PhabricatorDatabaseRef())
->setHost($host)
->setPort($port)
->setUser($user)
->setPass($pass)
->setDisabled($disabled)
->setIsMaster($is_master)
->setUsePersistentConnections($use_persistent);
if ($is_master) {
$master_count++;
}
$refs[$key] = $ref;
}
$is_partitioned = ($master_count > 1);
if ($is_partitioned) {
$default_ref = null;
$partition_map = array();
foreach ($refs as $key => $ref) {
if (!$ref->getIsMaster()) {
continue;
}
$server = $config[$key];
$partition = idx($server, 'partition');
if (!is_array($partition)) {
throw new Exception(
pht(
'Phabricator is configured with multiple master databases, '.
'but master "%s" is missing a "partition" configuration key to '.
'define application partitioning.',
$ref->getRefKey()));
}
$application_map = array();
foreach ($partition as $application) {
if ($application === 'default') {
if ($default_ref) {
throw new Exception(
pht(
'Multiple masters (databases "%s" and "%s") specify that '.
'they are the "default" partition. Only one master may be '.
'the default.',
$ref->getRefKey(),
$default_ref->getRefKey()));
} else {
$default_ref = $ref;
$ref->setIsDefaultPartition(true);
}
} else if (isset($partition_map[$application])) {
throw new Exception(
pht(
'Multiple masters (databases "%s" and "%s") specify that '.
'they are the partition for application "%s". Each '.
'application may be allocated to only one partition.',
$partition_map[$application]->getRefKey(),
$ref->getRefKey(),
$application));
} else {
// TODO: We should check that the application is valid, to
// prevent typos in application names. However, we do not
// currently have an efficient way to enumerate all of the valid
// application database names.
$partition_map[$application] = $ref;
$application_map[$application] = $application;
}
}
$ref->setApplicationMap($application_map);
}
} else {
// If we only have one master, make it the default.
foreach ($refs as $ref) {
if ($ref->getIsMaster()) {
$ref->setIsDefaultPartition(true);
}
}
}
$ref_map = array();
$master_keys = array();
foreach ($refs as $ref) {
$ref_key = $ref->getRefKey();
if (isset($ref_map[$ref_key])) {
throw new Exception(
pht(
'Multiple configured databases have the same internal '.
'key, "%s". You may have listed a database multiple times.',
$ref_key));
} else {
$ref_map[$ref_key] = $ref;
if ($ref->getIsMaster()) {
$master_keys[] = $ref_key;
}
}
}
foreach ($refs as $key => $ref) {
if ($ref->getIsMaster()) {
continue;
}
$server = $config[$key];
$partition = idx($server, 'partition');
if ($partition !== null) {
throw new Exception(
pht(
'Database "%s" is configured as a replica, but specifies a '.
'"partition". Only master databases may have a partition '.
'configuration. Replicas use the same configuration as the '.
'master they follow.',
$ref->getRefKey()));
}
$master_key = idx($server, 'master');
if ($master_key === null) {
if ($is_partitioned) {
throw new Exception(
pht(
'Database "%s" is configured as a replica, but does not '.
'specify which "master" it follows in configuration. Valid '.
'masters are: %s.',
$ref->getRefKey(),
implode(', ', $master_keys)));
} else if ($master_keys) {
$master_key = head($master_keys);
} else {
throw new Exception(
pht(
'Database "%s" is configured as a replica, but there is no '.
'master configured.',
$ref->getRefKey()));
}
}
if (!isset($ref_map[$master_key])) {
throw new Exception(
pht(
'Database "%s" is configured as a replica and specifies a '.
'master ("%s"), but that master is not a valid master. Valid '.
'masters are: %s.',
+ $ref->getRefKey(),
+ $master_key,
implode(', ', $master_keys)));
}
$master_ref = $ref_map[$master_key];
$ref->setMasterRef($ref_map[$master_key]);
$master_ref->addReplicaRef($ref);
}
return array_values($refs);
}
}
diff --git a/src/infrastructure/customfield/datasource/PhabricatorCustomFieldApplicationSearchNoneFunctionDatasource.php b/src/infrastructure/customfield/datasource/PhabricatorCustomFieldApplicationSearchNoneFunctionDatasource.php
index 591e4fda01..7302da3181 100644
--- a/src/infrastructure/customfield/datasource/PhabricatorCustomFieldApplicationSearchNoneFunctionDatasource.php
+++ b/src/infrastructure/customfield/datasource/PhabricatorCustomFieldApplicationSearchNoneFunctionDatasource.php
@@ -1,72 +1,72 @@
<?php
final class PhabricatorCustomFieldApplicationSearchNoneFunctionDatasource
extends PhabricatorTypeaheadDatasource {
public function getBrowseTitle() {
return pht('Browse No Value');
}
public function getPlaceholderText() {
return pht('Type "none()"...');
}
public function getDatasourceApplicationClass() {
return null;
}
public function getDatasourceFunctions() {
return array(
'none' => array(
'name' => pht('No Value'),
'summary' => pht('Find results with no value.'),
'description' => pht(
"This function includes results which have no value. Use a query ".
- "like this to find results with no value:\n\n%s\n\n",
+ "like this to find results with no value:\n\n%s\n\n".
'If you combine this function with other constraints, results '.
'which have no value or the specified values will be returned.',
'> any()'),
),
);
}
public function loadResults() {
$results = array(
$this->newNoneFunction(),
);
return $this->filterResultsAgainstTokens($results);
}
protected function evaluateFunction($function, array $argv_list) {
$results = array();
foreach ($argv_list as $argv) {
$results[] = new PhabricatorQueryConstraint(
PhabricatorQueryConstraint::OPERATOR_NULL,
null);
}
return $results;
}
public function renderFunctionTokens($function, array $argv_list) {
$results = array();
foreach ($argv_list as $argv) {
$results[] = PhabricatorTypeaheadTokenView::newFromTypeaheadResult(
$this->newNoneFunction());
}
return $results;
}
private function newNoneFunction() {
$name = pht('No Value');
return $this->newFunctionResult()
->setName($name.' none')
->setDisplayName($name)
->setIcon('fa-ban')
->setPHID('none()')
->setUnique(true)
->addAttribute(pht('Select results with no value.'));
}
}

File Metadata

Mime Type
text/x-diff
Expires
Mon, Jun 9, 5:00 PM (10 m, 35 s)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
140093
Default Alt Text
(190 KB)

Event Timeline