Page MenuHomec4science

No OneTemporary

File Metadata

Created
Thu, May 2, 10:55
This file is larger than 256 KB, so syntax highlighting was skipped.
diff --git a/.arclint b/.arclint
index 0077bdf51..e8b804fed 100644
--- a/.arclint
+++ b/.arclint
@@ -1,87 +1,86 @@
{
"exclude": [
"(^externals/)",
"(^webroot/rsrc/externals/(?!javelin/))"
],
"linters": {
"chmod": {
"type": "chmod"
},
"filename": {
"type": "filename"
},
"generated": {
"type": "generated"
},
"javelin": {
"type": "javelin",
"include": "(\\.js$)",
"exclude": [
"(^support/aphlict/)"
]
},
"jshint-browser": {
"type": "jshint",
"include": "(\\.js$)",
"exclude": [
"(^support/aphlict/server/.*\\.js$)",
"(^webroot/rsrc/externals/javelin/core/init_node\\.js$)"
],
"jshint.jshintrc": "support/lint/browser.jshintrc"
},
"jshint-node": {
"type": "jshint",
"include": [
"(^support/aphlict/server/.*\\.js$)",
"(^webroot/rsrc/externals/javelin/core/init_node\\.js$)"
],
"jshint.jshintrc": "support/lint/node.jshintrc"
},
"json": {
"type": "json",
"include": [
"(^src/docs/book/.*\\.book$)",
"(^support/lint/jshintrc$)",
"(^\\.arcconfig$)",
"(^\\.arclint$)",
"(\\.json$)"
]
},
"merge-conflict": {
"type": "merge-conflict"
},
"nolint": {
"type": "nolint"
},
"phutil-library": {
"type": "phutil-library",
"include": "(\\.php$)"
},
"phutil-xhpast": {
"type": "phutil-xhpast",
"include": "(\\.php$)",
"phutil-xhpast.deprecated.functions": {
"phutil_escape_html": "The phutil_escape_html() function is deprecated. Raw strings passed to phutil_tag() or hsprintf() are escaped automatically."
}
},
"spelling": {
"type": "spelling"
},
"text": {
"type": "text"
},
"xhpast": {
"type": "xhpast",
"include": "(\\.php$)",
"severity": {
- "16": "advice",
"34": "error"
},
"xhpast.blacklisted.function": {
"eval": "The eval() function should be avoided. It is potentially unsafe and makes debugging more difficult."
},
"xhpast.php-version": "5.2.3",
"xhpast.php-version.windows": "5.3.0"
}
}
}
diff --git a/resources/sql/autopatches/20140106.macromailkey.2.php b/resources/sql/autopatches/20140106.macromailkey.2.php
index fdca4e4d5..a91e5a2c2 100644
--- a/resources/sql/autopatches/20140106.macromailkey.2.php
+++ b/resources/sql/autopatches/20140106.macromailkey.2.php
@@ -1,23 +1,23 @@
<?php
-echo "Adding mailkeys to macros.\n";
+echo pht('Adding mailkeys to macros.')."\n";
$table = new PhabricatorFileImageMacro();
$conn_w = $table->establishConnection('w');
$iterator = new LiskMigrationIterator($table);
foreach ($iterator as $macro) {
$id = $macro->getID();
- echo "Populating macro {$id}...\n";
+ echo pht('Populating macro %d...', $id)."\n";
if (!$macro->getMailKey()) {
queryfx(
$conn_w,
'UPDATE %T SET mailKey = %s WHERE id = %d',
$table->getTableName(),
Filesystem::readRandomCharacters(20),
$id);
}
}
-echo "Done.\n";
+echo pht('Done.')."\n";
diff --git a/resources/sql/autopatches/20140108.ddbpname.2.php b/resources/sql/autopatches/20140108.ddbpname.2.php
index ca7e3ef8b..3316f3df1 100644
--- a/resources/sql/autopatches/20140108.ddbpname.2.php
+++ b/resources/sql/autopatches/20140108.ddbpname.2.php
@@ -1,23 +1,23 @@
<?php
-echo "Adding names to Drydock blueprints.\n";
+echo pht('Adding names to Drydock blueprints.')."\n";
$table = new DrydockBlueprint();
$conn_w = $table->establishConnection('w');
$iterator = new LiskMigrationIterator($table);
foreach ($iterator as $blueprint) {
$id = $blueprint->getID();
- echo "Populating blueprint {$id}...\n";
+ echo pht('Populating blueprint %d...', $id)."\n";
if (!strlen($blueprint->getBlueprintName())) {
queryfx(
$conn_w,
'UPDATE %T SET blueprintName = %s WHERE id = %d',
$table->getTableName(),
pht('Blueprint %s', $id),
$id);
}
}
-echo "Done.\n";
+echo pht('Done.')."\n";
diff --git a/resources/sql/autopatches/20140113.legalpadsig.2.php b/resources/sql/autopatches/20140113.legalpadsig.2.php
index 8b244e248..6c7b0131b 100644
--- a/resources/sql/autopatches/20140113.legalpadsig.2.php
+++ b/resources/sql/autopatches/20140113.legalpadsig.2.php
@@ -1,23 +1,23 @@
<?php
-echo "Adding secretkeys to legalpad document signatures.\n";
+echo pht('Adding secretkeys to legalpad document signatures.')."\n";
$table = new LegalpadDocumentSignature();
$conn_w = $table->establishConnection('w');
$iterator = new LiskMigrationIterator($table);
foreach ($iterator as $sig) {
$id = $sig->getID();
- echo "Populating signature {$id}...\n";
+ echo pht('Populating signature %d...', $id)."\n";
if (!$sig->getSecretKey()) {
queryfx(
$conn_w,
'UPDATE %T SET secretKey = %s WHERE id = %d',
$table->getTableName(),
Filesystem::readRandomCharacters(20),
$id);
}
}
-echo "Done.\n";
+echo pht('Done.')."\n";
diff --git a/resources/sql/autopatches/20140115.auth.3.unlimit.php b/resources/sql/autopatches/20140115.auth.3.unlimit.php
index 80c316f64..9e5bc7fed 100644
--- a/resources/sql/autopatches/20140115.auth.3.unlimit.php
+++ b/resources/sql/autopatches/20140115.auth.3.unlimit.php
@@ -1,26 +1,26 @@
<?php
// Prior to this patch, we issued sessions "web-1", "web-2", etc., up to some
// limit. This collapses all the "web-X" sessions into "web" sessions.
$session_table = new PhabricatorAuthSession();
$conn_w = $session_table->establishConnection('w');
foreach (new LiskMigrationIterator($session_table) as $session) {
$id = $session->getID();
- echo "Migrating session {$id}...\n";
+ echo pht('Migrating session %d...', $id)."\n";
$old_type = $session->getType();
$new_type = preg_replace('/-.*$/', '', $old_type);
if ($old_type !== $new_type) {
queryfx(
$conn_w,
'UPDATE %T SET type = %s WHERE id = %d',
$session_table->getTableName(),
$new_type,
$id);
}
}
-echo "Done.\n";
+echo pht('Done.')."\n";
diff --git a/resources/sql/autopatches/20140205.cal.3.phid-mig.php b/resources/sql/autopatches/20140205.cal.3.phid-mig.php
index 6ba4ff7c5..7aff389d1 100644
--- a/resources/sql/autopatches/20140205.cal.3.phid-mig.php
+++ b/resources/sql/autopatches/20140205.cal.3.phid-mig.php
@@ -1,22 +1,22 @@
<?php
$table = new PhabricatorCalendarEvent();
$conn_w = $table->establishConnection('w');
-echo "Assigning PHIDs to events...\n";
+echo pht('Assigning PHIDs to events...')."\n";
foreach (new LiskMigrationIterator($table) as $event) {
$id = $event->getID();
- echo "Updating event {$id}...\n";
+ echo pht('Updating event %d...', $id)."\n";
if ($event->getPHID()) {
continue;
}
queryfx(
$conn_w,
'UPDATE %T SET phid = %s WHERE id = %d',
$table->getTableName(),
$table->generatePHID(),
$id);
}
-echo "Done.\n";
+echo pht('Done.')."\n";
diff --git a/resources/sql/autopatches/20140210.herald.rule-condition-mig.php b/resources/sql/autopatches/20140210.herald.rule-condition-mig.php
index c653fa9d5..90f8aa0b3 100644
--- a/resources/sql/autopatches/20140210.herald.rule-condition-mig.php
+++ b/resources/sql/autopatches/20140210.herald.rule-condition-mig.php
@@ -1,31 +1,32 @@
<?php
$table = new HeraldCondition();
$conn_w = $table->establishConnection('w');
-echo "Migrating Herald conditions of type Herald rule from IDs to PHIDs...\n";
+echo pht(
+ "Migrating Herald conditions of type Herald rule from IDs to PHIDs...\n");
foreach (new LiskMigrationIterator($table) as $condition) {
if ($condition->getFieldName() != HeraldAdapter::FIELD_RULE) {
continue;
}
$value = $condition->getValue();
if (!is_numeric($value)) {
continue;
}
$id = $condition->getID();
- echo "Updating condition {$id}...\n";
+ echo pht('Updating condition %s...', $id)."\n";
$rule = id(new HeraldRuleQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withIDs(array($value))
->executeOne();
queryfx(
$conn_w,
'UPDATE %T SET value = %s WHERE id = %d',
$table->getTableName(),
json_encode($rule->getPHID()),
$id);
}
-echo "Done.\n";
+echo pht('Done.')."\n";
diff --git a/resources/sql/autopatches/20140210.projcfield.1.blurb.php b/resources/sql/autopatches/20140210.projcfield.1.blurb.php
index 3cb39c5dc..90c4242e8 100644
--- a/resources/sql/autopatches/20140210.projcfield.1.blurb.php
+++ b/resources/sql/autopatches/20140210.projcfield.1.blurb.php
@@ -1,26 +1,25 @@
<?php
$conn_w = id(new PhabricatorProject())->establishConnection('w');
$table_name = id(new PhabricatorProjectCustomFieldStorage())->getTableName();
$rows = new LiskRawMigrationIterator($conn_w, 'project_profile');
-echo "Migrating project descriptions to custom storage...\n";
+echo pht('Migrating project descriptions to custom storage...')."\n";
foreach ($rows as $row) {
$phid = $row['projectPHID'];
- echo "Migrating {$phid}...\n";
$desc = $row['blurb'];
if (strlen($desc)) {
queryfx(
$conn_w,
'INSERT IGNORE INTO %T (objectPHID, fieldIndex, fieldValue)
VALUES (%s, %s, %s)',
$table_name,
$phid,
PhabricatorHash::digestForIndex('std:project:internal:description'),
$desc);
}
}
-echo "Done.\n";
+echo pht('Done.')."\n";
diff --git a/resources/sql/autopatches/20140211.dx.2.migcommenttext.php b/resources/sql/autopatches/20140211.dx.2.migcommenttext.php
index 8a76690ec..705e85806 100644
--- a/resources/sql/autopatches/20140211.dx.2.migcommenttext.php
+++ b/resources/sql/autopatches/20140211.dx.2.migcommenttext.php
@@ -1,71 +1,71 @@
<?php
$conn_w = id(new DifferentialRevision())->establishConnection('w');
$rows = new LiskRawMigrationIterator($conn_w, 'differential_comment');
$content_source = PhabricatorContentSource::newForSource(
PhabricatorContentSource::SOURCE_LEGACY,
array())->serialize();
-echo "Migrating Differential comment text to modern storage...\n";
+echo pht('Migrating Differential comment text to modern storage...')."\n";
foreach ($rows as $row) {
$id = $row['id'];
- echo "Migrating Differential comment {$id}...\n";
+ echo pht('Migrating Differential comment %d...', $id)."\n";
if (!strlen($row['content'])) {
- echo "Comment has no text, continuing.\n";
+ echo pht('Comment has no text, continuing.')."\n";
continue;
}
$revision = id(new DifferentialRevision())->load($row['revisionID']);
if (!$revision) {
- echo "Comment has no valid revision, continuing.\n";
+ echo pht('Comment has no valid revision, continuing.')."\n";
continue;
}
$revision_phid = $revision->getPHID();
$dst_table = 'differential_inline_comment';
$xaction_phid = PhabricatorPHID::generateNewPHID(
PhabricatorApplicationTransactionTransactionPHIDType::TYPECONST,
DifferentialRevisionPHIDType::TYPECONST);
$comment_phid = PhabricatorPHID::generateNewPHID(
PhabricatorPHIDConstants::PHID_TYPE_XCMT,
DifferentialRevisionPHIDType::TYPECONST);
queryfx(
$conn_w,
'INSERT IGNORE INTO %T
(phid, transactionPHID, authorPHID, viewPolicy, editPolicy,
commentVersion, content, contentSource, isDeleted,
dateCreated, dateModified, revisionPHID, changesetID,
legacyCommentID)
VALUES (%s, %s, %s, %s, %s,
%d, %s, %s, %d,
%d, %d, %s, %nd,
%d)',
'differential_transaction_comment',
// phid, transactionPHID, authorPHID, viewPolicy, editPolicy
$comment_phid,
$xaction_phid,
$row['authorPHID'],
'public',
$row['authorPHID'],
// commentVersion, content, contentSource, isDeleted
1,
$row['content'],
$content_source,
0,
// dateCreated, dateModified, revisionPHID, changesetID, legacyCommentID
$row['dateCreated'],
$row['dateModified'],
$revision_phid,
null,
$row['id']);
}
-echo "Done.\n";
+echo pht('Done.')."\n";
diff --git a/resources/sql/autopatches/20140212.dx.1.armageddon.php b/resources/sql/autopatches/20140212.dx.1.armageddon.php
index 525bf7831..d2749bd5c 100644
--- a/resources/sql/autopatches/20140212.dx.1.armageddon.php
+++ b/resources/sql/autopatches/20140212.dx.1.armageddon.php
@@ -1,222 +1,222 @@
<?php
$conn_w = id(new DifferentialRevision())->establishConnection('w');
$rows = new LiskRawMigrationIterator($conn_w, 'differential_comment');
$content_source = PhabricatorContentSource::newForSource(
PhabricatorContentSource::SOURCE_LEGACY,
array())->serialize();
-echo "Migrating Differential comments to modern storage...\n";
+echo pht('Migrating Differential comments to modern storage...')."\n";
foreach ($rows as $row) {
$id = $row['id'];
- echo "Migrating comment {$id}...\n";
+ echo pht('Migrating comment %d...', $id)."\n";
$revision = id(new DifferentialRevision())->load($row['revisionID']);
if (!$revision) {
- echo "No revision, continuing.\n";
+ echo pht('No revision, continuing.')."\n";
continue;
}
$revision_phid = $revision->getPHID();
$comments = queryfx_all(
$conn_w,
'SELECT * FROM %T WHERE legacyCommentID = %d',
'differential_transaction_comment',
$id);
$main_comments = array();
$inline_comments = array();
foreach ($comments as $comment) {
if ($comment['changesetID']) {
$inline_comments[] = $comment;
} else {
$main_comments[] = $comment;
}
}
$metadata = json_decode($row['metadata'], true);
if (!is_array($metadata)) {
$metadata = array();
}
$key_cc = 'added-ccs';
$key_add_rev = 'added-reviewers';
$key_rem_rev = 'removed-reviewers';
$key_diff_id = 'diff-id';
$xactions = array();
// Build the main action transaction.
switch ($row['action']) {
case DifferentialAction::ACTION_COMMENT:
case DifferentialAction::ACTION_ADDREVIEWERS:
case DifferentialAction::ACTION_ADDCCS:
case DifferentialAction::ACTION_UPDATE:
case DifferentialTransaction::TYPE_INLINE:
// These actions will have their transactions created by other rules.
break;
default:
// Otherwise, this is a normal action (like an accept or reject).
$xactions[] = array(
'type' => DifferentialTransaction::TYPE_ACTION,
'old' => null,
'new' => $row['action'],
);
break;
}
// Build the diff update transaction, if one exists.
$diff_id = idx($metadata, $key_diff_id);
if (!is_scalar($diff_id)) {
$diff_id = null;
}
if ($diff_id || $row['action'] == DifferentialAction::ACTION_UPDATE) {
$xactions[] = array(
'type' => DifferentialTransaction::TYPE_UPDATE,
'old' => null,
'new' => $diff_id,
);
}
// Build the add/remove reviewers transaction, if one exists.
$add_rev = idx($metadata, $key_add_rev, array());
if (!is_array($add_rev)) {
$add_rev = array();
}
$rem_rev = idx($metadata, $key_rem_rev, array());
if (!is_array($rem_rev)) {
$rem_rev = array();
}
if ($add_rev || $rem_rev) {
$old = array();
foreach ($rem_rev as $phid) {
if (!is_scalar($phid)) {
continue;
}
$old[$phid] = array(
'src' => $revision_phid,
'type' => DifferentialRevisionHasReviewerEdgeType::EDGECONST,
'dst' => $phid,
);
}
$new = array();
foreach ($add_rev as $phid) {
if (!is_scalar($phid)) {
continue;
}
$new[$phid] = array(
'src' => $revision_phid,
'type' => DifferentialRevisionHasReviewerEdgeType::EDGECONST,
'dst' => $phid,
);
}
$xactions[] = array(
'type' => PhabricatorTransactions::TYPE_EDGE,
'old' => $old,
'new' => $new,
'meta' => array(
'edge:type' => DifferentialRevisionHasReviewerEdgeType::EDGECONST,
),
);
}
// Build the CC transaction, if one exists.
$add_cc = idx($metadata, $key_cc, array());
if (!is_array($add_cc)) {
$add_cc = array();
}
if ($add_cc) {
$xactions[] = array(
'type' => PhabricatorTransactions::TYPE_SUBSCRIBERS,
'old' => array(),
'new' => array_fuse($add_cc),
);
}
// Build the main comment transaction.
foreach ($main_comments as $main) {
$xactions[] = array(
'type' => PhabricatorTransactions::TYPE_COMMENT,
'old' => null,
'new' => null,
'phid' => $main['transactionPHID'],
'comment' => $main,
);
}
// Build inline comment transactions.
foreach ($inline_comments as $inline) {
$xactions[] = array(
'type' => DifferentialTransaction::TYPE_INLINE,
'old' => null,
'new' => null,
'phid' => $inline['transactionPHID'],
'comment' => $inline,
);
}
foreach ($xactions as $xaction) {
// Generate a new PHID, if we don't already have one from the comment
// table. We pregenerated into the comment table to make this a little
// easier, so we only need to write to one table.
$xaction_phid = idx($xaction, 'phid');
if (!$xaction_phid) {
$xaction_phid = PhabricatorPHID::generateNewPHID(
PhabricatorApplicationTransactionTransactionPHIDType::TYPECONST,
DifferentialRevisionPHIDType::TYPECONST);
}
unset($xaction['phid']);
$comment_phid = null;
$comment_version = 0;
if (idx($xaction, 'comment')) {
$comment_phid = $xaction['comment']['phid'];
$comment_version = 1;
}
$old = idx($xaction, 'old');
$new = idx($xaction, 'new');
$meta = idx($xaction, 'meta', array());
queryfx(
$conn_w,
'INSERT INTO %T (phid, authorPHID, objectPHID, viewPolicy, editPolicy,
commentPHID, commentVersion, transactionType, oldValue, newValue,
contentSource, metadata, dateCreated, dateModified)
VALUES (%s, %s, %s, %s, %s, %ns, %d, %s, %ns, %ns, %s, %s, %d, %d)',
'differential_transaction',
// PHID, authorPHID, objectPHID
$xaction_phid,
(string)$row['authorPHID'],
$revision_phid,
// viewPolicy, editPolicy, commentPHID, commentVersion
'public',
(string)$row['authorPHID'],
$comment_phid,
$comment_version,
// transactionType, oldValue, newValue, contentSource, metadata
$xaction['type'],
json_encode($old),
json_encode($new),
$content_source,
json_encode($meta),
// dates
$row['dateCreated'],
$row['dateModified']);
}
}
-echo "Done.\n";
+echo pht('Done.')."\n";
diff --git a/resources/sql/autopatches/20140218.passwords.4.vcs.php b/resources/sql/autopatches/20140218.passwords.4.vcs.php
index 5e58c63e8..103077532 100644
--- a/resources/sql/autopatches/20140218.passwords.4.vcs.php
+++ b/resources/sql/autopatches/20140218.passwords.4.vcs.php
@@ -1,27 +1,27 @@
<?php
$table = new PhabricatorRepositoryVCSPassword();
$conn_w = $table->establishConnection('w');
-echo "Upgrading password hashing for VCS passwords.\n";
+echo pht('Upgrading password hashing for VCS passwords.')."\n";
$best_hasher = PhabricatorPasswordHasher::getBestHasher();
foreach (new LiskMigrationIterator($table) as $password) {
$id = $password->getID();
- echo "Migrating VCS password {$id}...\n";
+ echo pht('Migrating VCS password %d...', $id)."\n";
$input_hash = $password->getPasswordHash();
$input_envelope = new PhutilOpaqueEnvelope($input_hash);
$storage_hash = $best_hasher->getPasswordHashForStorage($input_envelope);
queryfx(
$conn_w,
'UPDATE %T SET passwordHash = %s WHERE id = %d',
$table->getTableName(),
$storage_hash->openEnvelope(),
$id);
}
-echo "Done.\n";
+echo pht('Done.')."\n";
diff --git a/resources/sql/autopatches/20140226.dxcustom.1.fielddata.php b/resources/sql/autopatches/20140226.dxcustom.1.fielddata.php
index aa1adf41b..9b1382f5b 100644
--- a/resources/sql/autopatches/20140226.dxcustom.1.fielddata.php
+++ b/resources/sql/autopatches/20140226.dxcustom.1.fielddata.php
@@ -1,22 +1,22 @@
<?php
$conn_w = id(new DifferentialRevision())->establishConnection('w');
$rows = new LiskRawMigrationIterator($conn_w, 'differential_auxiliaryfield');
-echo "Modernizing Differential auxiliary field storage...\n";
+echo pht('Modernizing Differential auxiliary field storage...')."\n";
$table_name = id(new DifferentialCustomFieldStorage())->getTableName();
foreach ($rows as $row) {
$id = $row['id'];
- echo "Migrating row {$id}...\n";
+ echo pht('Migrating row %d...', $id)."\n";
queryfx(
$conn_w,
'INSERT IGNORE INTO %T (objectPHID, fieldIndex, fieldValue)
VALUES (%s, %s, %s)',
$table_name,
$row['revisionPHID'],
PhabricatorHash::digestForIndex($row['name']),
$row['value']);
}
-echo "Done.\n";
+echo pht('Done.')."\n";
diff --git a/resources/sql/autopatches/20140321.mstatus.2.mig.php b/resources/sql/autopatches/20140321.mstatus.2.mig.php
index 66c13844c..7f91e00e1 100644
--- a/resources/sql/autopatches/20140321.mstatus.2.mig.php
+++ b/resources/sql/autopatches/20140321.mstatus.2.mig.php
@@ -1,94 +1,94 @@
<?php
$status_map = array(
0 => 'open',
1 => 'resolved',
2 => 'wontfix',
3 => 'invalid',
4 => 'duplicate',
5 => 'spite',
);
$conn_w = id(new ManiphestTask())->establishConnection('w');
-echo "Migrating tasks to new status constants...\n";
+echo pht('Migrating tasks to new status constants...')."\n";
foreach (new LiskMigrationIterator(new ManiphestTask()) as $task) {
$id = $task->getID();
- echo "Migrating T{$id}...\n";
+ echo pht('Migrating %s...', "T{$id}")."\n";
$status = $task->getStatus();
if (isset($status_map[$status])) {
queryfx(
$conn_w,
'UPDATE %T SET status = %s WHERE id = %d',
$task->getTableName(),
$status_map[$status],
$id);
}
}
-echo "Done.\n";
+echo pht('Done.')."\n";
-echo "Migrating task transactions to new status constants...\n";
+echo pht('Migrating task transactions to new status constants...')."\n";
foreach (new LiskMigrationIterator(new ManiphestTransaction()) as $xaction) {
$id = $xaction->getID();
- echo "Migrating {$id}...\n";
+ echo pht('Migrating %d...', $id)."\n";
if ($xaction->getTransactionType() == ManiphestTransaction::TYPE_STATUS) {
$old = $xaction->getOldValue();
if ($old !== null && isset($status_map[$old])) {
$old = $status_map[$old];
}
$new = $xaction->getNewValue();
if (isset($status_map[$new])) {
$new = $status_map[$new];
}
queryfx(
$conn_w,
'UPDATE %T SET oldValue = %s, newValue = %s WHERE id = %d',
$xaction->getTableName(),
json_encode($old),
json_encode($new),
$id);
}
}
-echo "Done.\n";
+echo pht('Done.')."\n";
$conn_w = id(new PhabricatorSavedQuery())->establishConnection('w');
-echo "Migrating searches to new status constants...\n";
+echo pht('Migrating searches to new status constants...')."\n";
foreach (new LiskMigrationIterator(new PhabricatorSavedQuery()) as $query) {
$id = $query->getID();
- echo "Migrating {$id}...\n";
+ echo pht('Migrating %d...', $id)."\n";
if ($query->getEngineClassName() !== 'ManiphestTaskSearchEngine') {
continue;
}
$params = $query->getParameters();
$statuses = idx($params, 'statuses', array());
if ($statuses) {
$changed = false;
foreach ($statuses as $key => $status) {
if (isset($status_map[$status])) {
$statuses[$key] = $status_map[$status];
$changed = true;
}
}
if ($changed) {
$params['statuses'] = $statuses;
queryfx(
$conn_w,
'UPDATE %T SET parameters = %s WHERE id = %d',
$query->getTableName(),
json_encode($params),
$id);
}
}
}
-echo "Done.\n";
+echo pht('Done.')."\n";
diff --git a/resources/sql/autopatches/20140323.harbor.1.renames.php b/resources/sql/autopatches/20140323.harbor.1.renames.php
index 892402bf8..8aab25d87 100644
--- a/resources/sql/autopatches/20140323.harbor.1.renames.php
+++ b/resources/sql/autopatches/20140323.harbor.1.renames.php
@@ -1,35 +1,35 @@
<?php
$names = array(
'CommandBuildStepImplementation',
'LeaseHostBuildStepImplementation',
'PublishFragmentBuildStepImplementation',
'SleepBuildStepImplementation',
'UploadArtifactBuildStepImplementation',
'WaitForPreviousBuildStepImplementation',
);
$tables = array(
id(new HarbormasterBuildStep())->getTableName(),
id(new HarbormasterBuildTarget())->getTableName(),
);
-echo "Renaming Harbormaster classes...\n";
+echo pht('Renaming Harbormaster classes...')."\n";
$conn_w = id(new HarbormasterBuildStep())->establishConnection('w');
foreach ($names as $name) {
$old = $name;
$new = 'Harbormaster'.$name;
- echo "Renaming {$old} -> {$new}...\n";
+ echo pht('Renaming %s -> %s...', $old, $new)."\n";
foreach ($tables as $table) {
queryfx(
$conn_w,
'UPDATE %T SET className = %s WHERE className = %s',
$table,
$new,
$old);
}
}
-echo "Done.\n";
+echo pht('Done.')."\n";
diff --git a/resources/sql/autopatches/20140325.push.3.groups.php b/resources/sql/autopatches/20140325.push.3.groups.php
index b6a33ad4d..8706da306 100644
--- a/resources/sql/autopatches/20140325.push.3.groups.php
+++ b/resources/sql/autopatches/20140325.push.3.groups.php
@@ -1,43 +1,43 @@
<?php
$conn_w = id(new PhabricatorRepository())->establishConnection('w');
-echo "Adding transaction log event groups...\n";
+echo pht('Adding transaction log event groups...')."\n";
$logs = queryfx_all(
$conn_w,
'SELECT * FROM %T GROUP BY transactionKey ORDER BY id ASC',
'repository_pushlog');
foreach ($logs as $log) {
$id = $log['id'];
- echo "Migrating log {$id}...\n";
+ echo pht('Migrating log %d...', $id)."\n";
if ($log['pushEventPHID']) {
continue;
}
$event_phid = id(new PhabricatorRepositoryPushEvent())->generatePHID();
queryfx(
$conn_w,
'INSERT INTO %T (phid, repositoryPHID, epoch, pusherPHID, remoteAddress,
remoteProtocol, rejectCode, rejectDetails)
VALUES (%s, %s, %d, %s, %d, %s, %d, %s)',
'repository_pushevent',
$event_phid,
$log['repositoryPHID'],
$log['epoch'],
$log['pusherPHID'],
$log['remoteAddress'],
$log['remoteProtocol'],
$log['rejectCode'],
$log['rejectDetails']);
queryfx(
$conn_w,
'UPDATE %T SET pushEventPHID = %s WHERE transactionKey = %s',
'repository_pushlog',
$event_phid,
$log['transactionKey']);
}
-echo "Done.\n";
+echo pht('Done.')."\n";
diff --git a/resources/sql/autopatches/20140410.accountsecret.2.php b/resources/sql/autopatches/20140410.accountsecret.2.php
index bd8fd3968..7d0cf7ad8 100644
--- a/resources/sql/autopatches/20140410.accountsecret.2.php
+++ b/resources/sql/autopatches/20140410.accountsecret.2.php
@@ -1,23 +1,21 @@
<?php
-echo "Updating users...\n";
-
+echo pht('Updating users...')."\n";
foreach (new LiskMigrationIterator(new PhabricatorUser()) as $user) {
-
$id = $user->getID();
- echo "Updating {$id}...\n";
+ echo pht('Updating %d...', $id)."\n";
if (strlen($user->getAccountSecret())) {
continue;
}
queryfx(
$user->establishConnection('w'),
'UPDATE %T SET accountSecret = %s WHERE id = %d',
$user->getTableName(),
Filesystem::readRandomCharacters(64),
$id);
}
-echo "Done.\n";
+echo pht('Done.')."\n";
diff --git a/resources/sql/autopatches/20140420.rel.2.objectmig.php b/resources/sql/autopatches/20140420.rel.2.objectmig.php
index 6441f7c00..f54416a5b 100644
--- a/resources/sql/autopatches/20140420.rel.2.objectmig.php
+++ b/resources/sql/autopatches/20140420.rel.2.objectmig.php
@@ -1,45 +1,45 @@
<?php
$pull_table = new ReleephRequest();
$table_name = $pull_table->getTableName();
$conn_w = $pull_table->establishConnection('w');
-echo "Setting object PHIDs for requests...\n";
+echo pht('Setting object PHIDs for requests...')."\n";
foreach (new LiskMigrationIterator($pull_table) as $pull) {
$id = $pull->getID();
- echo "Migrating pull request {$id}...\n";
+ echo pht('Migrating pull request %d...', $id)."\n";
if ($pull->getRequestedObjectPHID()) {
// We already have a valid PHID, so skip this request.
continue;
}
$commit_phids = $pull->getCommitPHIDs();
if (count($commit_phids) != 1) {
// At the time this migration was written, all requests had exactly one
// commit. If a request has more than one, we don't have the information
// we need to process it.
continue;
}
$commit_phid = head($commit_phids);
$revision_phids = PhabricatorEdgeQuery::loadDestinationPHIDs(
$commit_phid,
DiffusionCommitHasRevisionEdgeType::EDGECONST);
if ($revision_phids) {
$object_phid = head($revision_phids);
} else {
$object_phid = $commit_phid;
}
queryfx(
$conn_w,
'UPDATE %T SET requestedObjectPHID = %s WHERE id = %d',
$table_name,
$object_phid,
$id);
}
-echo "Done.\n";
+echo pht('Done.')."\n";
diff --git a/resources/sql/autopatches/20140521.projectslug.2.mig.php b/resources/sql/autopatches/20140521.projectslug.2.mig.php
index ad086e10b..ca6ccf886 100644
--- a/resources/sql/autopatches/20140521.projectslug.2.mig.php
+++ b/resources/sql/autopatches/20140521.projectslug.2.mig.php
@@ -1,33 +1,33 @@
<?php
$project_table = new PhabricatorProject();
$table_name = $project_table->getTableName();
$conn_w = $project_table->establishConnection('w');
$slug_table_name = id(new PhabricatorProjectSlug())->getTableName();
$time = time();
-echo "Migrating project phriction slugs...\n";
+echo pht('Migrating project phriction slugs...')."\n";
foreach (new LiskMigrationIterator($project_table) as $project) {
$id = $project->getID();
- echo "Migrating project {$id}...\n";
+ echo pht('Migrating project %d...', $id)."\n";
$phriction_slug = rtrim($project->getPhrictionSlug(), '/');
$slug = id(new PhabricatorProjectSlug())
->loadOneWhere('slug = %s', $phriction_slug);
if ($slug) {
- echo "Already migrated {$id}... Continuing.\n";
+ echo pht('Already migrated %d... Continuing.', $id)."\n";
continue;
}
queryfx(
$conn_w,
'INSERT INTO %T (projectPHID, slug, dateCreated, dateModified) '.
'VALUES (%s, %s, %d, %d)',
$slug_table_name,
$project->getPHID(),
$phriction_slug,
$time,
$time);
- echo "Migrated {$id}.\n";
+ echo pht('Migrated %d.', $id)."\n";
}
-echo "Done.\n";
+echo pht('Done.')."\n";
diff --git a/resources/sql/autopatches/20140711.pnames.2.php b/resources/sql/autopatches/20140711.pnames.2.php
index ee3658384..16030b585 100644
--- a/resources/sql/autopatches/20140711.pnames.2.php
+++ b/resources/sql/autopatches/20140711.pnames.2.php
@@ -1,11 +1,11 @@
<?php
-echo "Updating project datasource tokens...\n";
+echo pht('Updating project datasource tokens...')."\n";
foreach (new LiskMigrationIterator(new PhabricatorProject()) as $project) {
$name = $project->getName();
- echo "Updating project '{$name}'...\n";
+ echo pht("Updating project '%d'...", $name)."\n";
$project->updateDatasourceTokens();
}
-echo "Done.\n";
+echo pht('Done.')."\n";
diff --git a/resources/sql/autopatches/20140722.appname.php b/resources/sql/autopatches/20140722.appname.php
index fca11bb4b..8c3e5918b 100644
--- a/resources/sql/autopatches/20140722.appname.php
+++ b/resources/sql/autopatches/20140722.appname.php
@@ -1,166 +1,166 @@
<?php
$applications = array(
'Audit',
'Auth',
'Calendar',
'ChatLog',
'Conduit',
'Config',
'Conpherence',
'Countdown',
'Daemons',
'Dashboard',
'Differential',
'Diffusion',
'Diviner',
'Doorkeeper',
'Drydock',
'Fact',
'Feed',
'Files',
'Flags',
'Harbormaster',
'Help',
'Herald',
'Home',
'Legalpad',
'Macro',
'MailingLists',
'Maniphest',
'Applications',
'MetaMTA',
'Notifications',
'Nuance',
'OAuthServer',
'Owners',
'Passphrase',
'Paste',
'People',
'Phame',
'Phlux',
'Pholio',
'Phortune',
'PHPAST',
'Phragment',
'Phrequent',
'Phriction',
'Policy',
'Ponder',
'Project',
'Releeph',
'Repositories',
'Search',
'Settings',
'Slowvote',
'Subscriptions',
'Support',
'System',
'Test',
'Tokens',
'Transactions',
'Typeahead',
'UIExamples',
'XHProf',
);
$map = array();
foreach ($applications as $application) {
$old_name = 'PhabricatorApplication'.$application;
$new_name = 'Phabricator'.$application.'Application';
$map[$old_name] = $new_name;
}
/* -( User preferences )--------------------------------------------------- */
-echo "Migrating user preferences...\n";
+echo pht('Migrating user preferences...')."\n";
$table = new PhabricatorUserPreferences();
$conn_w = $table->establishConnection('w');
$pref_pinned = PhabricatorUserPreferences::PREFERENCE_APP_PINNED;
foreach (new LiskMigrationIterator(new PhabricatorUser()) as $user) {
$user_preferences = $user->loadPreferences();
$old_pinned_apps = $user_preferences->getPreference($pref_pinned);
$new_pinned_apps = array();
if (!$old_pinned_apps) {
continue;
}
foreach ($old_pinned_apps as $pinned_app) {
$new_pinned_apps[] = idx($map, $pinned_app, $pinned_app);
}
$user_preferences
->setPreference($pref_pinned, $new_pinned_apps);
queryfx(
$conn_w,
'UPDATE %T SET preferences = %s WHERE id = %d',
$user_preferences->getTableName(),
json_encode($user_preferences->getPreferences()),
$user_preferences->getID());
}
/* -( Dashboard installs )------------------------------------------------- */
-echo "Migrating dashboard installs...\n";
+echo pht('Migrating dashboard installs...')."\n";
$table = new PhabricatorDashboardInstall();
$conn_w = $table->establishConnection('w');
foreach (new LiskMigrationIterator($table) as $dashboard_install) {
$application = $dashboard_install->getApplicationClass();
queryfx(
$conn_w,
'UPDATE %T SET applicationClass = %s WHERE id = %d',
$table->getTableName(),
idx($map, $application, $application),
$dashboard_install->getID());
}
/* -( Phabricator configuration )------------------------------------------ */
$config_key = 'phabricator.uninstalled-applications';
-echo "Migrating `{$config_key}` config...\n";
+echo pht('Migrating `%s` config...', $config_key)."\n";
$config = PhabricatorConfigEntry::loadConfigEntry($config_key);
$old_config = $config->getValue();
$new_config = array();
if ($old_config) {
foreach ($old_config as $application => $uninstalled) {
$new_config[idx($map, $application, $application)] = $uninstalled;
}
$config
->setIsDeleted(0)
->setValue($new_config)
->save();
}
/* -( phabricator.application-settings )----------------------------------- */
$config_key = 'phabricator.application-settings';
-echo "Migrating `{$config_key}` config...\n";
+echo pht('Migrating `%s` config...', $config_key)."\n";
$config = PhabricatorConfigEntry::loadConfigEntry($config_key);
$old_config = $config->getValue();
$new_config = array();
if ($old_config) {
foreach ($old_config as $application => $settings) {
$application = preg_replace('/^PHID-APPS-/', '', $application);
$new_config['PHID-APPS-'.idx($map, $application, $application)] = $settings;
}
$config
->setIsDeleted(0)
->setValue($new_config)
->save();
}
diff --git a/resources/sql/autopatches/20140722.audit.3.miginlines.php b/resources/sql/autopatches/20140722.audit.3.miginlines.php
index 30d9b1c79..c6778fa65 100644
--- a/resources/sql/autopatches/20140722.audit.3.miginlines.php
+++ b/resources/sql/autopatches/20140722.audit.3.miginlines.php
@@ -1,77 +1,77 @@
<?php
$audit_table = new PhabricatorAuditTransaction();
$conn_w = $audit_table->establishConnection('w');
$conn_w->openTransaction();
$src_table = 'audit_inlinecomment';
$dst_table = 'audit_transaction_comment';
-echo "Migrating Audit inline comments to new format...\n";
+echo pht('Migrating Audit inline comments to new format...')."\n";
$content_source = PhabricatorContentSource::newForSource(
PhabricatorContentSource::SOURCE_LEGACY,
array())->serialize();
$rows = new LiskRawMigrationIterator($conn_w, $src_table);
foreach ($rows as $row) {
$id = $row['id'];
- echo "Migrating inline #{$id}...\n";
+ echo pht('Migrating inline #%d...', $id);
if ($row['auditCommentID']) {
$xaction_phid = PhabricatorPHID::generateNewPHID(
PhabricatorApplicationTransactionTransactionPHIDType::TYPECONST,
PhabricatorRepositoryCommitPHIDType::TYPECONST);
} else {
$xaction_phid = null;
}
$comment_phid = PhabricatorPHID::generateNewPHID(
PhabricatorPHIDConstants::PHID_TYPE_XCMT,
PhabricatorRepositoryCommitPHIDType::TYPECONST);
queryfx(
$conn_w,
'INSERT IGNORE INTO %T
(id, phid, transactionPHID, authorPHID, viewPolicy, editPolicy,
commentVersion, content, contentSource, isDeleted,
dateCreated, dateModified, commitPHID, pathID,
isNewFile, lineNumber, lineLength, hasReplies, legacyCommentID)
VALUES (%d, %s, %ns, %s, %s, %s,
%d, %s, %s, %d,
%d, %d, %s, %nd,
%d, %d, %d, %d, %nd)',
$dst_table,
// id, phid, transactionPHID, authorPHID, viewPolicy, editPolicy
$row['id'],
$comment_phid,
$xaction_phid,
$row['authorPHID'],
'public',
$row['authorPHID'],
// commentVersion, content, contentSource, isDeleted
1,
$row['content'],
$content_source,
0,
// dateCreated, dateModified, commitPHID, pathID
$row['dateCreated'],
$row['dateModified'],
$row['commitPHID'],
$row['pathID'],
// isNewFile, lineNumber, lineLength, hasReplies, legacyCommentID
$row['isNewFile'],
$row['lineNumber'],
$row['lineLength'],
0,
$row['auditCommentID']);
}
$conn_w->saveTransaction();
-echo "Done.\n";
+echo pht('Done.')."\n";
diff --git a/resources/sql/autopatches/20140722.audit.4.migtext.php b/resources/sql/autopatches/20140722.audit.4.migtext.php
index 1bcd3d36b..2da4e2683 100644
--- a/resources/sql/autopatches/20140722.audit.4.migtext.php
+++ b/resources/sql/autopatches/20140722.audit.4.migtext.php
@@ -1,61 +1,61 @@
<?php
$conn_w = id(new PhabricatorAuditTransaction())->establishConnection('w');
$rows = new LiskRawMigrationIterator($conn_w, 'audit_comment');
$content_source = PhabricatorContentSource::newForSource(
PhabricatorContentSource::SOURCE_LEGACY,
array())->serialize();
-echo "Migrating Audit comment text to modern storage...\n";
+echo pht('Migrating Audit comment text to modern storage...')."\n";
foreach ($rows as $row) {
$id = $row['id'];
- echo "Migrating Audit comment {$id}...\n";
+ echo pht('Migrating Audit comment %d...', $id)."\n";
if (!strlen($row['content'])) {
- echo "Comment has no text, continuing.\n";
+ echo pht('Comment has no text, continuing.')."\n";
continue;
}
$xaction_phid = PhabricatorPHID::generateNewPHID(
PhabricatorApplicationTransactionTransactionPHIDType::TYPECONST,
PhabricatorRepositoryCommitPHIDType::TYPECONST);
$comment_phid = PhabricatorPHID::generateNewPHID(
PhabricatorPHIDConstants::PHID_TYPE_XCMT,
PhabricatorRepositoryCommitPHIDType::TYPECONST);
queryfx(
$conn_w,
'INSERT IGNORE INTO %T
(phid, transactionPHID, authorPHID, viewPolicy, editPolicy,
commentVersion, content, contentSource, isDeleted,
dateCreated, dateModified, commitPHID, pathID,
legacyCommentID)
VALUES (%s, %s, %s, %s, %s,
%d, %s, %s, %d,
%d, %d, %s, %nd,
%d)',
'audit_transaction_comment',
// phid, transactionPHID, authorPHID, viewPolicy, editPolicy
$comment_phid,
$xaction_phid,
$row['actorPHID'],
'public',
$row['actorPHID'],
// commentVersion, content, contentSource, isDeleted
1,
$row['content'],
$content_source,
0,
// dateCreated, dateModified, commitPHID, pathID, legacyCommentID
$row['dateCreated'],
$row['dateModified'],
$row['targetPHID'],
null,
$row['id']);
}
-echo "Done.\n";
+echo pht('Done.')."\n";
diff --git a/resources/sql/autopatches/20140722.renameauth.php b/resources/sql/autopatches/20140722.renameauth.php
index 8a021c834..225031d2f 100644
--- a/resources/sql/autopatches/20140722.renameauth.php
+++ b/resources/sql/autopatches/20140722.renameauth.php
@@ -1,34 +1,34 @@
<?php
$map = array(
'PhabricatorAuthProviderOAuthAmazon' => 'PhabricatorAmazonAuthProvider',
'PhabricatorAuthProviderOAuthAsana' => 'PhabricatorAsanaAuthProvider',
'PhabricatorAuthProviderOAuth1Bitbucket'
=> 'PhabricatorBitbucketAuthProvider',
'PhabricatorAuthProviderOAuthDisqus' => 'PhabricatorDisqusAuthProvider',
'PhabricatorAuthProviderOAuthFacebook' => 'PhabricatorFacebookAuthProvider',
'PhabricatorAuthProviderOAuthGitHub' => 'PhabricatorGitHubAuthProvider',
'PhabricatorAuthProviderOAuthGoogle' => 'PhabricatorGoogleAuthProvider',
'PhabricatorAuthProviderOAuth1JIRA' => 'PhabricatorJIRAAuthProvider',
'PhabricatorAuthProviderLDAP' => 'PhabricatorLDAPAuthProvider',
'PhabricatorAuthProviderPassword' => 'PhabricatorPasswordAuthProvider',
'PhabricatorAuthProviderPersona' => 'PhabricatorPersonaAuthProvider',
'PhabricatorAuthProviderOAuthTwitch' => 'PhabricatorTwitchAuthProvider',
'PhabricatorAuthProviderOAuth1Twitter' => 'PhabricatorTwitterAuthProvider',
'PhabricatorAuthProviderOAuthWordPress' => 'PhabricatorWordPressAuthProvider',
);
-echo "Migrating auth providers...\n";
+echo pht('Migrating auth providers...')."\n";
$table = new PhabricatorAuthProviderConfig();
$conn_w = $table->establishConnection('w');
foreach (new LiskMigrationIterator($table) as $provider) {
$provider_class = $provider->getProviderClass();
queryfx(
$conn_w,
'UPDATE %T SET providerClass = %s WHERE id = %d',
$provider->getTableName(),
idx($map, $provider_class, $provider_class),
$provider->getID());
}
diff --git a/resources/sql/autopatches/20140725.audit.1.migxactions.php b/resources/sql/autopatches/20140725.audit.1.migxactions.php
index 064ac9220..e0e14e439 100644
--- a/resources/sql/autopatches/20140725.audit.1.migxactions.php
+++ b/resources/sql/autopatches/20140725.audit.1.migxactions.php
@@ -1,150 +1,150 @@
<?php
$conn_w = id(new PhabricatorAuditTransaction())->establishConnection('w');
$rows = new LiskRawMigrationIterator($conn_w, 'audit_comment');
$content_source = PhabricatorContentSource::newForSource(
PhabricatorContentSource::SOURCE_LEGACY,
array())->serialize();
-echo "Migrating Audit comments to modern storage...\n";
+echo pht('Migrating Audit comments to modern storage...')."\n";
foreach ($rows as $row) {
$id = $row['id'];
- echo "Migrating comment {$id}...\n";
+ echo pht('Migrating comment %d...', $id)."\n";
$comments = queryfx_all(
$conn_w,
'SELECT * FROM %T WHERE legacyCommentID = %d',
'audit_transaction_comment',
$id);
$main_comments = array();
$inline_comments = array();
foreach ($comments as $comment) {
if ($comment['pathID']) {
$inline_comments[] = $comment;
} else {
$main_comments[] = $comment;
}
}
$metadata = json_decode($row['metadata'], true);
if (!is_array($metadata)) {
$metadata = array();
}
$xactions = array();
// Build the main action transaction.
switch ($row['action']) {
case PhabricatorAuditActionConstants::ADD_AUDITORS:
$phids = idx($metadata, 'added-auditors', array());
$xactions[] = array(
'type' => $row['action'],
'old' => null,
'new' => array_fuse($phids),
);
break;
case PhabricatorAuditActionConstants::ADD_CCS:
$phids = idx($metadata, 'added-ccs', array());
$xactions[] = array(
'type' => $row['action'],
'old' => null,
'new' => array_fuse($phids),
);
break;
case PhabricatorAuditActionConstants::COMMENT:
case PhabricatorAuditActionConstants::INLINE:
// These actions will have their transactions created by other rules.
break;
default:
// Otherwise, this is an accept/concern/etc action.
$xactions[] = array(
'type' => PhabricatorAuditActionConstants::ACTION,
'old' => null,
'new' => $row['action'],
);
break;
}
// Build the main comment transaction.
foreach ($main_comments as $main) {
$xactions[] = array(
'type' => PhabricatorTransactions::TYPE_COMMENT,
'old' => null,
'new' => null,
'phid' => $main['transactionPHID'],
'comment' => $main,
);
}
// Build inline comment transactions.
foreach ($inline_comments as $inline) {
$xactions[] = array(
'type' => PhabricatorAuditActionConstants::INLINE,
'old' => null,
'new' => null,
'phid' => $inline['transactionPHID'],
'comment' => $inline,
);
}
foreach ($xactions as $xaction) {
// Generate a new PHID, if we don't already have one from the comment
// table. We pregenerated into the comment table to make this a little
// easier, so we only need to write to one table.
$xaction_phid = idx($xaction, 'phid');
if (!$xaction_phid) {
$xaction_phid = PhabricatorPHID::generateNewPHID(
PhabricatorApplicationTransactionTransactionPHIDType::TYPECONST,
PhabricatorRepositoryCommitPHIDType::TYPECONST);
}
unset($xaction['phid']);
$comment_phid = null;
$comment_version = 0;
if (idx($xaction, 'comment')) {
$comment_phid = $xaction['comment']['phid'];
$comment_version = 1;
}
$old = idx($xaction, 'old');
$new = idx($xaction, 'new');
$meta = idx($xaction, 'meta', array());
queryfx(
$conn_w,
'INSERT INTO %T (phid, authorPHID, objectPHID, viewPolicy, editPolicy,
commentPHID, commentVersion, transactionType, oldValue, newValue,
contentSource, metadata, dateCreated, dateModified)
VALUES (%s, %s, %s, %s, %s, %ns, %d, %s, %ns, %ns, %s, %s, %d, %d)',
'audit_transaction',
// PHID, authorPHID, objectPHID
$xaction_phid,
$row['actorPHID'],
$row['targetPHID'],
// viewPolicy, editPolicy, commentPHID, commentVersion
'public',
$row['actorPHID'],
$comment_phid,
$comment_version,
// transactionType, oldValue, newValue, contentSource, metadata
$xaction['type'],
json_encode($old),
json_encode($new),
$content_source,
json_encode($meta),
// dates
$row['dateCreated'],
$row['dateModified']);
}
}
-echo "Done.\n";
+echo pht('Done.')."\n";
diff --git a/resources/sql/autopatches/20140731.audit.1.subscribers.php b/resources/sql/autopatches/20140731.audit.1.subscribers.php
index c648ce3c0..b45227529 100644
--- a/resources/sql/autopatches/20140731.audit.1.subscribers.php
+++ b/resources/sql/autopatches/20140731.audit.1.subscribers.php
@@ -1,30 +1,30 @@
<?php
$table = new PhabricatorRepositoryAuditRequest();
$conn_w = $table->establishConnection('w');
-echo "Migrating Audit subscribers to subscriptions...\n";
+echo pht('Migrating Audit subscribers to subscriptions...')."\n";
foreach (new LiskMigrationIterator($table) as $request) {
$id = $request->getID();
- echo "Migrating auditor {$id}...\n";
+ echo pht("Migrating audit %d...\n", $id);
if ($request->getAuditStatus() != 'cc') {
// This isn't a "subscriber", so skip it.
continue;
}
queryfx(
$conn_w,
'INSERT IGNORE INTO %T (src, type, dst) VALUES (%s, %d, %s)',
PhabricatorEdgeConfig::TABLE_NAME_EDGE,
$request->getCommitPHID(),
PhabricatorObjectHasSubscriberEdgeType::EDGECONST,
$request->getAuditorPHID());
// Wipe the row.
$request->delete();
}
-echo "Done.\n";
+echo pht('Done.')."\n";
diff --git a/resources/sql/autopatches/20140731.cancdn.php b/resources/sql/autopatches/20140731.cancdn.php
index 7994d88ab..f4090b092 100644
--- a/resources/sql/autopatches/20140731.cancdn.php
+++ b/resources/sql/autopatches/20140731.cancdn.php
@@ -1,20 +1,20 @@
<?php
$table = new PhabricatorFile();
$conn_w = $table->establishConnection('w');
foreach (new LiskMigrationIterator($table) as $file) {
$id = $file->getID();
- echo "Updating flags for file {$id}...\n";
+ echo pht('Updating flags for file %d...', $id)."\n";
$meta = $file->getMetadata();
if (!idx($meta, 'canCDN')) {
$meta['canCDN'] = true;
queryfx(
$conn_w,
'UPDATE %T SET metadata = %s WHERE id = %d',
$table->getTableName(),
json_encode($meta),
$id);
}
}
diff --git a/resources/sql/autopatches/20140805.boardcol.2.php b/resources/sql/autopatches/20140805.boardcol.2.php
index b8a78278d..317de4e37 100644
--- a/resources/sql/autopatches/20140805.boardcol.2.php
+++ b/resources/sql/autopatches/20140805.boardcol.2.php
@@ -1,53 +1,53 @@
<?php
// Was PhabricatorEdgeConfig::TYPE_COLUMN_HAS_OBJECT
$type_has_object = 44;
$column = new PhabricatorProjectColumn();
$conn_w = $column->establishConnection('w');
$rows = queryfx_all(
$conn_w,
'SELECT src, dst FROM %T WHERE type = %d',
PhabricatorEdgeConfig::TABLE_NAME_EDGE,
$type_has_object);
$cols = array();
foreach ($rows as $row) {
$cols[$row['src']][] = $row['dst'];
}
$sql = array();
foreach ($cols as $col_phid => $obj_phids) {
- echo "Migrating column '{$col_phid}'...\n";
+ echo pht("Migrating column '%s'...", $col_phid)."\n";
$column = id(new PhabricatorProjectColumn())->loadOneWhere(
'phid = %s',
$col_phid);
if (!$column) {
- echo "Column '{$col_phid}' does not exist.\n";
+ echo pht("Column '%s' does not exist.", $col_phid)."\n";
continue;
}
$sequence = 0;
foreach ($obj_phids as $obj_phid) {
$sql[] = qsprintf(
$conn_w,
'(%s, %s, %s, %d)',
$column->getProjectPHID(),
$column->getPHID(),
$obj_phid,
$sequence++);
}
}
-echo "Inserting rows...\n";
+echo pht('Inserting rows...')."\n";
foreach (PhabricatorLiskDAO::chunkSQL($sql) as $chunk) {
queryfx(
$conn_w,
'INSERT INTO %T (boardPHID, columnPHID, objectPHID, sequence)
VALUES %Q',
id(new PhabricatorProjectColumnPosition())->getTableName(),
$chunk);
}
-echo "Done.\n";
+echo pht('Done.')."\n";
diff --git a/resources/sql/autopatches/20140808.boardprop.3.php b/resources/sql/autopatches/20140808.boardprop.3.php
index 116cc773a..947ce85f6 100644
--- a/resources/sql/autopatches/20140808.boardprop.3.php
+++ b/resources/sql/autopatches/20140808.boardprop.3.php
@@ -1,24 +1,24 @@
<?php
$table = new PhabricatorProjectColumn();
$conn_w = $table->establishConnection('w');
foreach (new LiskMigrationIterator($table) as $column) {
$id = $column->getID();
- echo "Adjusting column {$id}...\n";
+ echo pht('Adjusting column %d...', $id)."\n";
if ($column->getSequence() == 0) {
$properties = $column->getProperties();
$properties['isDefault'] = true;
queryfx(
$conn_w,
'UPDATE %T SET properties = %s WHERE id = %d',
$table->getTableName(),
json_encode($properties),
$id);
}
}
-echo "Done.\n";
+echo pht('Done.')."\n";
diff --git a/resources/sql/autopatches/20140815.cancdncase.php b/resources/sql/autopatches/20140815.cancdncase.php
index 400fdbb01..0e9cafaab 100644
--- a/resources/sql/autopatches/20140815.cancdncase.php
+++ b/resources/sql/autopatches/20140815.cancdncase.php
@@ -1,24 +1,27 @@
<?php
// This corrects files which incorrectly had a 'cancdn' property written;
// the property should be 'canCDN'.
$table = new PhabricatorFile();
$conn_w = $table->establishConnection('w');
foreach (new LiskMigrationIterator($table) as $file) {
$id = $file->getID();
- echo "Updating capitalization of canCDN property for file {$id}...\n";
+ echo pht(
+ "Updating capitalization of %s property for file %d...\n",
+ 'canCDN',
+ $id);
$meta = $file->getMetadata();
if (isset($meta['cancdn'])) {
$meta['canCDN'] = $meta['cancdn'];
unset($meta['cancdn']);
queryfx(
$conn_w,
'UPDATE %T SET metadata = %s WHERE id = %d',
$table->getTableName(),
json_encode($meta),
$id);
}
}
diff --git a/resources/sql/autopatches/20140904.macroattach.php b/resources/sql/autopatches/20140904.macroattach.php
index 5e82f3aa5..476196475 100644
--- a/resources/sql/autopatches/20140904.macroattach.php
+++ b/resources/sql/autopatches/20140904.macroattach.php
@@ -1,26 +1,26 @@
<?php
$table = new PhabricatorFileImageMacro();
foreach (new LiskMigrationIterator($table) as $macro) {
$name = $macro->getName();
- echo "Linking macro '{$name}'...\n";
+ echo pht("Linking macro '%s'...", $name)."\n";
$editor = new PhabricatorEdgeEditor();
$phids[] = $macro->getFilePHID();
$phids[] = $macro->getAudioPHID();
$phids = array_filter($phids);
if ($phids) {
foreach ($phids as $phid) {
$editor->addEdge(
$macro->getPHID(),
- PhabricatorObjectHasFileEdgeType::EDGECONST ,
+ PhabricatorObjectHasFileEdgeType::EDGECONST,
$phid);
}
$editor->save();
}
}
-echo "Done.\n";
+echo pht('Done.')."\n";
diff --git a/resources/sql/autopatches/20140914.betaproto.php b/resources/sql/autopatches/20140914.betaproto.php
index 4471d47fe..849c5acb3 100644
--- a/resources/sql/autopatches/20140914.betaproto.php
+++ b/resources/sql/autopatches/20140914.betaproto.php
@@ -1,24 +1,24 @@
<?php
$old_key = 'phabricator.show-beta-applications';
$new_key = 'phabricator.show-prototypes';
-echo "Migrating '{$old_key}' to '{$new_key}'...\n";
+echo pht("Migrating '%s' to '%s'...", $old_key, $new_key)."\n";
if (PhabricatorEnv::getEnvConfig($new_key)) {
- echo "Skipping migration, new data is already set.\n";
+ echo pht('Skipping migration, new data is already set.')."\n";
return;
}
$old = PhabricatorEnv::getEnvConfigIfExists($old_key);
if (!$old) {
- echo "Skipping migration, old data does not exist.\n";
+ echo pht('Skipping migration, old data does not exist.')."\n";
return;
}
PhabricatorConfigEntry::loadConfigEntry($new_key)
->setIsDeleted(0)
->setValue($old)
->save();
-echo "Done.\n";
+echo pht('Done.')."\n";
diff --git a/resources/sql/autopatches/20141107.phriction.policy.2.php b/resources/sql/autopatches/20141107.phriction.policy.2.php
index 9391173c6..a7cc6ca3e 100644
--- a/resources/sql/autopatches/20141107.phriction.policy.2.php
+++ b/resources/sql/autopatches/20141107.phriction.policy.2.php
@@ -1,55 +1,58 @@
<?php
$table = new PhrictionDocument();
$conn_w = $table->establishConnection('w');
-echo "Populating Phriction policies.\n";
+echo pht('Populating Phriction policies.')."\n";
$default_view_policy = PhabricatorPolicies::POLICY_USER;
$default_edit_policy = PhabricatorPolicies::POLICY_USER;
foreach (new LiskMigrationIterator($table) as $doc) {
$id = $doc->getID();
if ($doc->getViewPolicy() && $doc->getEditPolicy()) {
- echo "Skipping doc $id; already has policy set.\n";
+ echo pht('Skipping document %d; already has policy set.', $id)."\n";
continue;
}
// If this was previously a magical project wiki page (under projects/, but
// not projects/ itself) we need to apply the project policies. Otherwise,
// apply the default policies.
$slug = $doc->getSlug();
$slug = PhabricatorSlug::normalize($slug);
$prefix = 'projects/';
if (($slug != $prefix) && (strncmp($slug, $prefix, strlen($prefix)) === 0)) {
$parts = explode('/', $slug);
$project_slug = $parts[1].'/';
$project_slugs = array($project_slug);
$project = id(new PhabricatorProjectQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withPhrictionSlugs($project_slugs)
->executeOne();
if ($project) {
$view_policy = nonempty($project->getViewPolicy(), $default_view_policy);
$edit_policy = nonempty($project->getEditPolicy(), $default_edit_policy);
$project_name = $project->getName();
- echo "Migrating doc $id to project policy $project_name...\n";
+ echo pht(
+ "Migrating document %d to project policy %s...\n",
+ $id,
+ $project_name);
$doc->setViewPolicy($view_policy);
$doc->setEditPolicy($edit_policy);
$doc->save();
continue;
}
}
- echo "Migrating doc $id to default install policy...\n";
+ echo pht('Migrating document %d to default install policy...', $id)."\n";
$doc->setViewPolicy($default_view_policy);
$doc->setEditPolicy($default_edit_policy);
$doc->save();
}
-echo "Done.\n";
+echo pht('Done.')."\n";
diff --git a/resources/sql/autopatches/20141107.phriction.popkeys.php b/resources/sql/autopatches/20141107.phriction.popkeys.php
index 9af7e3a95..31629c7fe 100644
--- a/resources/sql/autopatches/20141107.phriction.popkeys.php
+++ b/resources/sql/autopatches/20141107.phriction.popkeys.php
@@ -1,29 +1,29 @@
<?php
$table = new PhrictionDocument();
$conn_w = $table->establishConnection('w');
-echo "Populating Phriction mailkeys.\n";
+echo pht('Populating Phriction mailkeys.')."\n";
foreach (new LiskMigrationIterator($table) as $doc) {
$id = $doc->getID();
$key = $doc->getMailKey();
if ((strlen($key) == 20) && (strpos($key, "\0") === false)) {
// To be valid, keys must have length 20 and not contain any null bytes.
// See T6487.
- echo "Document has valid mailkey.\n";
+ echo pht('Document has valid mailkey.')."\n";
continue;
} else {
- echo "Populating mailkey for document {$id}...\n";
+ echo pht('Populating mailkey for document %d...', $id)."\n";
$mail_key = Filesystem::readRandomCharacters(20);
queryfx(
$conn_w,
'UPDATE %T SET mailKey = %s WHERE id = %d',
$table->getTableName(),
$mail_key,
$id);
}
}
-echo "Done.\n";
+echo pht('Done.')."\n";
diff --git a/resources/sql/autopatches/20141107.ssh.4.keymig.php b/resources/sql/autopatches/20141107.ssh.4.keymig.php
index 2388a282c..f3e2d3f09 100644
--- a/resources/sql/autopatches/20141107.ssh.4.keymig.php
+++ b/resources/sql/autopatches/20141107.ssh.4.keymig.php
@@ -1,50 +1,50 @@
<?php
$table = new PhabricatorAuthSSHKey();
$conn_w = $table->establishConnection('w');
-echo "Updating SSH public key indexes...\n";
+echo pht('Updating SSH public key indexes...')."\n";
$keys = new LiskMigrationIterator($table);
foreach ($keys as $key) {
$id = $key->getID();
- echo "Updating key {$id}...\n";
+ echo pht('Updating key %d...', $id)."\n";
try {
$hash = $key->toPublicKey()->getHash();
} catch (Exception $ex) {
- echo "Key has bad format! Removing key.\n";
+ echo pht('Key has bad format! Removing key.')."\n";
queryfx(
$conn_w,
'DELETE FROM %T WHERE id = %d',
$table->getTableName(),
$id);
continue;
}
$collision = queryfx_all(
$conn_w,
'SELECT * FROM %T WHERE keyIndex = %s AND id < %d',
$table->getTableName(),
$hash,
$key->getID());
if ($collision) {
- echo "Key is a duplicate! Removing key.\n";
+ echo pht('Key is a duplicate! Removing key.')."\n";
queryfx(
$conn_w,
'DELETE FROM %T WHERE id = %d',
$table->getTableName(),
$id);
continue;
}
queryfx(
$conn_w,
'UPDATE %T SET keyIndex = %s WHERE id = %d',
$table->getTableName(),
$hash,
$key->getID());
}
-echo "Done.\n";
+echo pht('Done.')."\n";
diff --git a/resources/sql/autopatches/20141113.auditdupes.php b/resources/sql/autopatches/20141113.auditdupes.php
index 2cc7b9bab..32a6586ca 100644
--- a/resources/sql/autopatches/20141113.auditdupes.php
+++ b/resources/sql/autopatches/20141113.auditdupes.php
@@ -1,22 +1,22 @@
<?php
$table = new PhabricatorRepositoryAuditRequest();
$conn_w = $table->establishConnection('w');
-echo "Removing duplicate Audit requests...\n";
+echo pht('Removing duplicate Audit requests...')."\n";
$seen_audit_map = array();
foreach (new LiskMigrationIterator($table) as $request) {
$commit_phid = $request->getCommitPHID();
$auditor_phid = $request->getAuditorPHID();
if (isset($seen_audit_map[$commit_phid][$auditor_phid])) {
$request->delete();
}
if (!isset($seen_audit_map[$commit_phid])) {
$seen_audit_map[$commit_phid] = array();
}
$seen_audit_map[$commit_phid][$auditor_phid] = 1;
}
-echo "Done.\n";
+echo pht('Done.')."\n";
diff --git a/resources/sql/autopatches/20141218.maniphestcctxn.php b/resources/sql/autopatches/20141218.maniphestcctxn.php
index fb1a67a41..4ac3c6210 100644
--- a/resources/sql/autopatches/20141218.maniphestcctxn.php
+++ b/resources/sql/autopatches/20141218.maniphestcctxn.php
@@ -1,20 +1,21 @@
<?php
$table = new ManiphestTransaction();
$conn_w = $table->establishConnection('w');
-echo "Converting Maniphest CC transactions to modern SUBSCRIBER ".
- "transactions...\n";
+echo pht(
+ "Converting Maniphest CC transactions to modern ".
+ "subscriber transactions...\n");
foreach (new LiskMigrationIterator($table) as $txn) {
// ManiphestTransaction::TYPE_CCS
if ($txn->getTransactionType() == 'ccs') {
queryfx(
$conn_w,
'UPDATE %T SET transactionType = %s WHERE id = %d',
$table->getTableName(),
PhabricatorTransactions::TYPE_SUBSCRIBERS,
$txn->getID());
}
}
-echo "Done.\n";
+echo pht('Done.')."\n";
diff --git a/resources/sql/autopatches/20141222.maniphestprojtxn.php b/resources/sql/autopatches/20141222.maniphestprojtxn.php
index 1fa5726fa..ba7a8f2f1 100644
--- a/resources/sql/autopatches/20141222.maniphestprojtxn.php
+++ b/resources/sql/autopatches/20141222.maniphestprojtxn.php
@@ -1,49 +1,49 @@
<?php
$table = new ManiphestTransaction();
$conn_w = $table->establishConnection('w');
-echo "Converting Maniphest project transactions to modern EDGE ".
- "transactions...\n";
+echo pht(
+ "Converting Maniphest project transactions to modern edge transactions...\n");
$metadata = array(
'edge:type' => PhabricatorProjectObjectHasProjectEdgeType::EDGECONST,
);
foreach (new LiskMigrationIterator($table) as $txn) {
// ManiphestTransaction::TYPE_PROJECTS
if ($txn->getTransactionType() == 'projects') {
$old_value = mig20141222_build_edge_data(
$txn->getOldValue(),
$txn->getObjectPHID());
$new_value = mig20141222_build_edge_data(
$txn->getNewvalue(),
$txn->getObjectPHID());
queryfx(
$conn_w,
'UPDATE %T SET '.
'transactionType = %s, oldValue = %s, newValue = %s, metaData = %s '.
'WHERE id = %d',
$table->getTableName(),
PhabricatorTransactions::TYPE_EDGE,
json_encode($old_value),
json_encode($new_value),
json_encode($metadata),
$txn->getID());
}
}
-echo "Done.\n";
+echo pht('Done.')."\n";
function mig20141222_build_edge_data(array $project_phids, $task_phid) {
$edge_data = array();
foreach ($project_phids as $project_phid) {
if (!is_scalar($project_phid)) {
continue;
}
$edge_data[$project_phid] = array(
'src' => $task_phid,
'type' => PhabricatorProjectObjectHasProjectEdgeType::EDGECONST,
'dst' => $project_phid,
);
}
return $edge_data;
}
diff --git a/resources/sql/autopatches/20150102.policyname.php b/resources/sql/autopatches/20150102.policyname.php
index a76143527..cf94b918e 100644
--- a/resources/sql/autopatches/20150102.policyname.php
+++ b/resources/sql/autopatches/20150102.policyname.php
@@ -1,38 +1,38 @@
<?php
$policies = array(
'Administrators',
'LegalpadSignature',
'LunarPhase',
'Projects',
'Users',
);
$map = array();
foreach ($policies as $policy) {
$old_name = "PhabricatorPolicyRule{$policy}";
$new_name = "Phabricator{$policy}PolicyRule";
$map[$old_name] = $new_name;
}
-echo "Migrating policies...\n";
+echo pht('Migrating policies...')."\n";
$table = new PhabricatorPolicy();
$conn_w = $table->establishConnection('w');
foreach (new LiskMigrationIterator($table) as $policy) {
$old_rules = $policy->getRules();
$new_rules = array();
foreach ($old_rules as $rule) {
$existing_rule = $rule['rule'];
$rule['rule'] = idx($map, $existing_rule, $existing_rule);
$new_rules[] = $rule;
}
queryfx(
$conn_w,
'UPDATE %T SET rules = %s WHERE id = %d',
$table->getTableName(),
json_encode($new_rules),
$policy->getID());
}
diff --git a/resources/sql/autopatches/20150116.maniphestapplicationemails.php b/resources/sql/autopatches/20150116.maniphestapplicationemails.php
index c3e1f53ee..91430568c 100644
--- a/resources/sql/autopatches/20150116.maniphestapplicationemails.php
+++ b/resources/sql/autopatches/20150116.maniphestapplicationemails.php
@@ -1,20 +1,20 @@
<?php
$key = 'metamta.maniphest.public-create-email';
-echo "Migrating `$key` to new application email infrastructure...\n";
+echo pht("Migrating `%s` to new application email infrastructure...\n", $key);
$value = PhabricatorEnv::getEnvConfigIfExists($key);
$maniphest = new PhabricatorManiphestApplication();
if ($value) {
try {
PhabricatorMetaMTAApplicationEmail::initializeNewAppEmail(
PhabricatorUser::getOmnipotentUser())
->setAddress($value)
->setApplicationPHID($maniphest->getPHID())
->save();
} catch (AphrontDuplicateKeyQueryException $ex) {
- // already migrated?
+ // Already migrated?
}
}
-echo "Done.\n";
+echo pht('Done.')."\n";
diff --git a/resources/sql/autopatches/20150120.maniphestdefaultauthor.php b/resources/sql/autopatches/20150120.maniphestdefaultauthor.php
index ae064544f..352dd4cbb 100644
--- a/resources/sql/autopatches/20150120.maniphestdefaultauthor.php
+++ b/resources/sql/autopatches/20150120.maniphestdefaultauthor.php
@@ -1,22 +1,22 @@
<?php
$key = 'metamta.maniphest.default-public-author';
-echo "Migrating `$key` to new application email infrastructure...\n";
+echo pht("Migrating `%s` to new application email infrastructure...\n", $key);
$value = PhabricatorEnv::getEnvConfigIfExists($key);
$maniphest = new PhabricatorManiphestApplication();
$config_key =
PhabricatorMetaMTAApplicationEmail::CONFIG_DEFAULT_AUTHOR;
if ($value) {
$app_emails = id(new PhabricatorMetaMTAApplicationEmailQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withApplicationPHIDs(array($maniphest->getPHID()))
->execute();
foreach ($app_emails as $app_email) {
$app_email->setConfigValue($config_key, $value);
$app_email->save();
}
}
-echo "Done.\n";
+echo pht('Done.')."\n";
diff --git a/resources/sql/autopatches/20150129.pastefileapplicationemails.php b/resources/sql/autopatches/20150129.pastefileapplicationemails.php
index 68c3dd238..80e92fb30 100644
--- a/resources/sql/autopatches/20150129.pastefileapplicationemails.php
+++ b/resources/sql/autopatches/20150129.pastefileapplicationemails.php
@@ -1,38 +1,40 @@
<?php
$key_files = 'metamta.files.public-create-email';
$key_paste = 'metamta.paste.public-create-email';
-echo "Migrating `$key_files` and `$key_paste` to new application email ".
- "infrastructure...\n";
+echo pht(
+ "Migrating `%s` and `%s` to new application email infrastructure...\n",
+ $key_files,
+ $key_paste);
$value_files = PhabricatorEnv::getEnvConfigIfExists($key_files);
$files_app = new PhabricatorFilesApplication();
if ($value_files) {
try {
PhabricatorMetaMTAApplicationEmail::initializeNewAppEmail(
PhabricatorUser::getOmnipotentUser())
->setAddress($value_files)
->setApplicationPHID($files_app->getPHID())
->save();
} catch (AphrontDuplicateKeyQueryException $ex) {
- // already migrated?
+ // Already migrated?
}
}
$value_paste = PhabricatorEnv::getEnvConfigIfExists($key_paste);
$paste_app = new PhabricatorPasteApplication();
if ($value_paste) {
try {
PhabricatorMetaMTAApplicationEmail::initializeNewAppEmail(
PhabricatorUser::getOmnipotentUser())
->setAddress($value_paste)
->setApplicationPHID($paste_app->getPHID())
->save();
} catch (AphrontDuplicateKeyQueryException $ex) {
- // already migrated?
+ // Already migrated?
}
}
-echo "Done.\n";
+echo pht('Done.')."\n";
diff --git a/resources/sql/autopatches/20150501.calendar.2.reply.php b/resources/sql/autopatches/20150501.calendar.2.reply.php
index 0b448e79c..2dab8268a 100644
--- a/resources/sql/autopatches/20150501.calendar.2.reply.php
+++ b/resources/sql/autopatches/20150501.calendar.2.reply.php
@@ -1,21 +1,21 @@
<?php
-echo "Adding mailkeys to events.\n";
+echo pht('Adding %s to events.', 'mailkeys')."\n";
$table = new PhabricatorCalendarEvent();
$conn_w = $table->establishConnection('w');
$iterator = new LiskMigrationIterator($table);
foreach ($iterator as $event) {
$id = $event->getID();
- echo "Populating event {$id}...\n";
+ echo pht('Populating event %d...', $id)."\n";
queryfx(
$conn_w,
'UPDATE %T SET mailKey = %s WHERE id = %d',
$table->getTableName(),
Filesystem::readRandomCharacters(20),
$id);
}
-echo "Done.\n";
+echo pht('Done.')."\n";
diff --git a/resources/sql/autopatches/20150506.calendarunnamedevents.1.php b/resources/sql/autopatches/20150506.calendarunnamedevents.1.php
index 0b8c02958..00512de9e 100644
--- a/resources/sql/autopatches/20150506.calendarunnamedevents.1.php
+++ b/resources/sql/autopatches/20150506.calendarunnamedevents.1.php
@@ -1,38 +1,38 @@
<?php
-echo "Retro-naming unnamed events.\n";
+echo pht('Retro-naming unnamed events.')."\n";
$table = new PhabricatorCalendarEvent();
$conn_w = $table->establishConnection('w');
$iterator = new LiskMigrationIterator($table);
foreach ($iterator as $event) {
$id = $event->getID();
if (strlen($event->getName()) == 0) {
- echo "Renaming event {$id}...\n";
+ echo pht('Renaming event %d...', $id)."\n";
$viewer = PhabricatorUser::getOmnipotentUser();
// NOTE: This uses PeopleQuery directly, instead of HandleQuery, to avoid
// performing cache fills as a side effect; the caches were added by a
// later patch. See T8209.
$user = id(new PhabricatorPeopleQuery())
->setViewer($viewer)
->withPHIDs(array($event->getUserPHID()))
->executeOne();
if ($user) {
$new_name = $user->getUsername();
} else {
$new_name = pht('Unnamed Event');
}
queryfx(
$conn_w,
'UPDATE %T SET name = %s WHERE id = %d',
$table->getTableName(),
$new_name,
$id);
}
}
-echo "Done.\n";
+echo pht('Done.')."\n";
diff --git a/resources/sql/patches/059.engines.php b/resources/sql/patches/059.engines.php
index 8c1c7bc8e..96e28b7a3 100644
--- a/resources/sql/patches/059.engines.php
+++ b/resources/sql/patches/059.engines.php
@@ -1,31 +1,32 @@
<?php
$conn = $schema_conn;
$tables = queryfx_all(
$conn,
"SELECT TABLE_SCHEMA db, TABLE_NAME tbl
FROM information_schema.TABLES s
WHERE s.TABLE_SCHEMA LIKE %>
AND s.TABLE_NAME != 'search_documentfield'
AND s.ENGINE != 'InnoDB'",
'{$NAMESPACE}_');
if (!$tables) {
return;
}
-echo "There are ".count($tables)." tables using the MyISAM engine. These will ".
- "now be converted to InnoDB. This process may take a few minutes, please ".
- "be patient.\n";
+echo pht(
+ "There are %d tables using the MyISAM engine. These will now be converted ".
+ "to InnoDB. This process may take a few minutes, please be patient.\n",
+ count($tables));
foreach ($tables as $table) {
$name = $table['db'].'.'.$table['tbl'];
- echo "Converting {$name}...\n";
+ echo pht('Converting %s...', $name)."\n";
queryfx(
$conn,
'ALTER TABLE %T.%T ENGINE=InnoDB',
$table['db'],
$table['tbl']);
}
-echo "Done!\n";
+echo pht('Done!')."\n";
diff --git a/resources/sql/patches/079.nametokenindex.php b/resources/sql/patches/079.nametokenindex.php
index 931a850bf..e0f607e0b 100644
--- a/resources/sql/patches/079.nametokenindex.php
+++ b/resources/sql/patches/079.nametokenindex.php
@@ -1,18 +1,18 @@
<?php
-echo "Indexing username tokens for typeaheads...\n";
+echo pht('Indexing username tokens for typeaheads...')."\n";
$table = new PhabricatorUser();
$table->openTransaction();
$table->beginReadLocking();
$users = $table->loadAll();
-echo count($users).' users to index';
+echo pht('%d users to index', count($users));
foreach ($users as $user) {
$user->updateNameTokens();
echo '.';
}
$table->endReadLocking();
$table->saveTransaction();
-echo "\nDone.\n";
+echo "\n".pht('Done.')."\n";
diff --git a/resources/sql/patches/081.filekeys.php b/resources/sql/patches/081.filekeys.php
index 1a4a3851f..c129e8091 100644
--- a/resources/sql/patches/081.filekeys.php
+++ b/resources/sql/patches/081.filekeys.php
@@ -1,22 +1,22 @@
<?php
-echo "Generating file keys...\n";
+echo pht('Generating file keys...')."\n";
$table = new PhabricatorFile();
$table->openTransaction();
$table->beginReadLocking();
$files = $table->loadAllWhere('secretKey IS NULL');
-echo count($files).' files to generate keys for';
+echo pht('%d files to generate keys for', count($files));
foreach ($files as $file) {
queryfx(
$file->establishConnection('w'),
'UPDATE %T SET secretKey = %s WHERE id = %d',
$file->getTableName(),
$file->generateSecretKey(),
$file->getID());
echo '.';
}
$table->endReadLocking();
$table->saveTransaction();
-echo "\nDone.\n";
+echo "\n".pht('Done.')."\n";
diff --git a/resources/sql/patches/090.forceuniqueprojectnames.php b/resources/sql/patches/090.forceuniqueprojectnames.php
index c3881ad01..a12de8ecf 100644
--- a/resources/sql/patches/090.forceuniqueprojectnames.php
+++ b/resources/sql/patches/090.forceuniqueprojectnames.php
@@ -1,107 +1,111 @@
<?php
-echo "Ensuring project names are unique enough...\n";
+echo pht('Ensuring project names are unique enough...')."\n";
$table = new PhabricatorProject();
$table->openTransaction();
$table->beginReadLocking();
$projects = $table->loadAll();
$slug_map = array();
foreach ($projects as $project) {
$project->setPhrictionSlug($project->getName());
$slug = $project->getPhrictionSlug();
if ($slug == '/') {
$project_id = $project->getID();
- echo "Project #{$project_id} doesn't have a meaningful name...\n";
- $project->setName(trim('Unnamed Project '.$project->getName()));
+ echo pht("Project #%d doesn't have a meaningful name...", $project_id)."\n";
+ $project->setName(trim(pht('Unnamed Project %s', $project->getName())));
}
$slug_map[$slug][] = $project->getID();
}
foreach ($slug_map as $slug => $similar) {
if (count($similar) <= 1) {
continue;
}
- echo "Too many projects are similar to '{$slug}'...\n";
+ echo pht("Too many projects are similar to '%s'...", $slug)."\n";
foreach (array_slice($similar, 1, null, true) as $key => $project_id) {
$project = $projects[$project_id];
$old_name = $project->getName();
$new_name = rename_project($project, $projects);
- echo "Renaming project #{$project_id} ".
- "from '{$old_name}' to '{$new_name}'.\n";
+ echo pht(
+ "Renaming project #%d from '%s' to '%s'.\n",
+ $project_id,
+ $old_name,
+ $new_name);
$project->setName($new_name);
}
}
$update = $projects;
while ($update) {
$size = count($update);
foreach ($update as $key => $project) {
$id = $project->getID();
$name = $project->getName();
$project->setPhrictionSlug($name);
$slug = $project->getPhrictionSlug();
- echo "Updating project #{$id} '{$name}' ({$slug})...";
+ echo pht("Updating project #%d '%s' (%s)... ", $id, $name, $slug);
try {
queryfx(
$project->establishConnection('w'),
'UPDATE %T SET name = %s, phrictionSlug = %s WHERE id = %d',
$project->getTableName(),
$name,
$slug,
$project->getID());
unset($update[$key]);
- echo "okay.\n";
+ echo pht('OKAY')."\n";
} catch (AphrontDuplicateKeyQueryException $ex) {
- echo "failed, will retry.\n";
+ echo pht('Failed, will retry.')."\n";
}
}
if (count($update) == $size) {
throw new Exception(
- 'Failed to make any progress while updating projects. Schema upgrade '.
- 'has failed. Go manually fix your project names to be unique (they are '.
- 'probably ridiculous?) and then try again.');
+ pht(
+ 'Failed to make any progress while updating projects. Schema upgrade '.
+ 'has failed. Go manually fix your project names to be unique '.
+ '(they are probably ridiculous?) and then try again.'));
}
}
$table->endReadLocking();
$table->saveTransaction();
-echo "Done.\n";
+echo pht('Done.')."\n";
/**
* Rename the project so that it has a unique slug, by appending (2), (3), etc.
* to its name.
*/
function rename_project($project, $projects) {
$suffix = 2;
while (true) {
$new_name = $project->getName().' ('.$suffix.')';
$project->setPhrictionSlug($new_name);
$new_slug = $project->getPhrictionSlug();
$okay = true;
foreach ($projects as $other) {
if ($other->getID() == $project->getID()) {
continue;
}
if ($other->getPhrictionSlug() == $new_slug) {
$okay = false;
break;
}
}
if ($okay) {
break;
} else {
$suffix++;
}
}
return $new_name;
}
diff --git a/resources/sql/patches/093.gitremotes.php b/resources/sql/patches/093.gitremotes.php
index 7af320bf8..5817919e2 100644
--- a/resources/sql/patches/093.gitremotes.php
+++ b/resources/sql/patches/093.gitremotes.php
@@ -1,44 +1,49 @@
<?php
-echo "Stripping remotes from repository default branches...\n";
+echo pht('Stripping remotes from repository default branches...')."\n";
$table = new PhabricatorRepository();
$table->openTransaction();
$conn_w = $table->establishConnection('w');
$repos = queryfx_all(
$conn_w,
'SELECT id, name, details FROM %T WHERE versionControlSystem = %s FOR UPDATE',
$table->getTableName(),
'git');
foreach ($repos as $repo) {
$details = json_decode($repo['details'], true);
$old = idx($details, 'default-branch', '');
if (strpos($old, '/') === false) {
continue;
}
$parts = explode('/', $old);
$parts = array_filter($parts);
$new = end($parts);
$details['default-branch'] = $new;
$new_details = json_encode($details);
$id = $repo['id'];
$name = $repo['name'];
- echo "Updating default branch for repository #{$id} '{$name}' from ".
- "'{$old}' to '{$new}' to remove the explicit remote.\n";
+ echo pht(
+ "Updating default branch for repository #%d '%s' from ".
+ "'%s' to '%s' to remove the explicit remote.\n",
+ $id,
+ $name,
+ $old,
+ $new);
queryfx(
$conn_w,
'UPDATE %T SET details = %s WHERE id = %d',
$table->getTableName(),
$new_details,
$id);
}
$table->saveTransaction();
-echo "Done.\n";
+echo pht('Done.')."\n";
diff --git a/resources/sql/patches/098.heraldruletypemigration.php b/resources/sql/patches/098.heraldruletypemigration.php
index 0efc8c7e9..896f2353f 100644
--- a/resources/sql/patches/098.heraldruletypemigration.php
+++ b/resources/sql/patches/098.heraldruletypemigration.php
@@ -1,51 +1,51 @@
<?php
-echo "Checking for rules that can be converted to 'personal'. ";
+echo pht("Checking for rules that can be converted to 'personal'.")."\n";
$table = new HeraldRule();
$table->openTransaction();
$table->beginReadLocking();
$rules = $table->loadAll();
foreach ($rules as $rule) {
if ($rule->getRuleType() !== HeraldRuleTypeConfig::RULE_TYPE_PERSONAL) {
$actions = $rule->loadActions();
$can_be_personal = true;
foreach ($actions as $action) {
$target = $action->getTarget();
if (is_array($target)) {
if (count($target) > 1) {
$can_be_personal = false;
break;
} else {
$targetPHID = head($target);
if ($targetPHID !== $rule->getAuthorPHID()) {
$can_be_personal = false;
break;
}
}
} else if ($target) {
if ($target !== $rule->getAuthorPHID()) {
$can_be_personal = false;
break;
}
}
}
if ($can_be_personal) {
$rule->setRuleType(HeraldRuleTypeConfig::RULE_TYPE_PERSONAL);
queryfx(
$rule->establishConnection('w'),
'UPDATE %T SET ruleType = %s WHERE id = %d',
$rule->getTableName(),
$rule->getRuleType(),
$rule->getID());
- echo "Setting rule '".$rule->getName()."' to personal. ";
+ echo pht("Setting rule '%s' to personal.", $rule->getName())."\n";
}
}
}
$table->endReadLocking();
$table->saveTransaction();
-echo "Done.\n";
+echo pht('Done.')."\n";
diff --git a/resources/sql/patches/102.heraldcleanup.php b/resources/sql/patches/102.heraldcleanup.php
index 24bf382c0..5b885bd67 100644
--- a/resources/sql/patches/102.heraldcleanup.php
+++ b/resources/sql/patches/102.heraldcleanup.php
@@ -1,39 +1,39 @@
<?php
-echo "Cleaning up old Herald rule applied rows...\n";
+echo pht('Cleaning up old Herald rule applied rows...')."\n";
$table = new HeraldRule();
$table->openTransaction();
$table->beginReadLocking();
$rules = $table->loadAll();
foreach ($rules as $key => $rule) {
$first_policy = HeraldRepetitionPolicyConfig::toInt(
HeraldRepetitionPolicyConfig::FIRST);
if ($rule->getRepetitionPolicy() != $first_policy) {
unset($rules[$key]);
}
}
$conn_w = $table->establishConnection('w');
$clause = '';
if ($rules) {
$clause = qsprintf(
$conn_w,
'WHERE ruleID NOT IN (%Ld)',
mpull($rules, 'getID'));
}
-echo 'This may take a moment';
+echo pht('This may take a moment')."\n";
do {
queryfx(
$conn_w,
'DELETE FROM %T %Q LIMIT 1000',
HeraldRule::TABLE_RULE_APPLIED,
$clause);
echo '.';
} while ($conn_w->getAffectedRows());
$table->endReadLocking();
$table->saveTransaction();
-echo "\nDone.\n";
+echo "\n".pht('Done.')."\n";
diff --git a/resources/sql/patches/111.commitauditmigration.php b/resources/sql/patches/111.commitauditmigration.php
index 489a07d78..935317b57 100644
--- a/resources/sql/patches/111.commitauditmigration.php
+++ b/resources/sql/patches/111.commitauditmigration.php
@@ -1,58 +1,58 @@
<?php
-echo "Updating old commit authors...\n";
+echo pht('Updating old commit authors...')."\n";
$table = new PhabricatorRepositoryCommit();
$table->openTransaction();
$conn = $table->establishConnection('w');
$data = new PhabricatorRepositoryCommitData();
$commits = queryfx_all(
$conn,
'SELECT c.id id, c.authorPHID authorPHID, d.commitDetails details
FROM %T c JOIN %T d ON d.commitID = c.id
WHERE c.authorPHID IS NULL
FOR UPDATE',
$table->getTableName(),
$data->getTableName());
foreach ($commits as $commit) {
$id = $commit['id'];
$details = json_decode($commit['details'], true);
$author_phid = idx($details, 'authorPHID');
if ($author_phid) {
queryfx(
$conn,
'UPDATE %T SET authorPHID = %s WHERE id = %d',
$table->getTableName(),
$author_phid,
$id);
echo "#{$id}\n";
}
}
$table->saveTransaction();
-echo "Done.\n";
+echo pht('Done.')."\n";
-echo "Updating old commit mailKeys...\n";
+echo pht('Updating old commit %s...', 'mailKeys')."\n";
$table->openTransaction();
$commits = queryfx_all(
$conn,
'SELECT id FROM %T WHERE mailKey = %s FOR UPDATE',
$table->getTableName(),
'');
foreach ($commits as $commit) {
$id = $commit['id'];
queryfx(
$conn,
'UPDATE %T SET mailKey = %s WHERE id = %d',
$table->getTableName(),
Filesystem::readRandomCharacters(20),
$id);
echo "#{$id}\n";
}
$table->saveTransaction();
-echo "Done.\n";
+echo pht('Done.')."\n";
diff --git a/resources/sql/patches/131.migraterevisionquery.php b/resources/sql/patches/131.migraterevisionquery.php
index cf505b629..316c7cdee 100644
--- a/resources/sql/patches/131.migraterevisionquery.php
+++ b/resources/sql/patches/131.migraterevisionquery.php
@@ -1,35 +1,35 @@
<?php
$table = new DifferentialRevision();
$table->openTransaction();
$table->beginReadLocking();
$conn_w = $table->establishConnection('w');
-echo 'Migrating revisions';
+echo pht('Migrating revisions')."\n";
do {
$revisions = $table->loadAllWhere('branchName IS NULL LIMIT 1000');
foreach ($revisions as $revision) {
echo '.';
$diff = $revision->loadActiveDiff();
if (!$diff) {
continue;
}
$branch_name = $diff->getBranch();
$arc_project_phid = $diff->getArcanistProjectPHID();
queryfx(
$conn_w,
'UPDATE %T SET branchName = %s, arcanistProjectPHID = %s WHERE id = %d',
$table->getTableName(),
$branch_name,
$arc_project_phid,
$revision->getID());
}
} while (count($revisions) == 1000);
$table->endReadLocking();
$table->saveTransaction();
-echo "\nDone.\n";
+echo "\n".pht('Done.')."\n";
diff --git a/resources/sql/patches/20121209.xmacromigrate.php b/resources/sql/patches/20121209.xmacromigrate.php
index df52cb317..bdd9fc127 100644
--- a/resources/sql/patches/20121209.xmacromigrate.php
+++ b/resources/sql/patches/20121209.xmacromigrate.php
@@ -1,23 +1,23 @@
<?php
-echo 'Giving image macros PHIDs';
+echo pht('Giving image macros PHIDs');
$table = new PhabricatorFileImageMacro();
$table->openTransaction();
foreach (new LiskMigrationIterator($table) as $macro) {
if ($macro->getPHID()) {
continue;
}
echo '.';
queryfx(
$macro->establishConnection('w'),
'UPDATE %T SET phid = %s WHERE id = %d',
$macro->getTableName(),
$macro->generatePHID(),
$macro->getID());
}
$table->saveTransaction();
-echo "\nDone.\n";
+echo "\n".pht('Done.')."\n";
diff --git a/resources/sql/patches/20130201.revisionunsubscribed.php b/resources/sql/patches/20130201.revisionunsubscribed.php
index 0f01e1d12..904fe1cc8 100644
--- a/resources/sql/patches/20130201.revisionunsubscribed.php
+++ b/resources/sql/patches/20130201.revisionunsubscribed.php
@@ -1,32 +1,32 @@
<?php
-echo "Migrating Differential unsubscribed users to edges...\n";
+echo pht('Migrating Differential unsubscribed users to edges...')."\n";
$table = new DifferentialRevision();
$table->openTransaction();
// We couldn't use new LiskMigrationIterator($table) because the $unsubscribed
// property gets deleted.
$revs = queryfx_all(
$table->establishConnection('w'),
'SELECT id, phid, unsubscribed FROM differential_revision');
foreach ($revs as $rev) {
echo '.';
$unsubscribed = json_decode($rev['unsubscribed']);
if (!$unsubscribed) {
continue;
}
$editor = new PhabricatorEdgeEditor();
foreach ($unsubscribed as $user_phid => $_) {
$editor->addEdge(
$rev['phid'],
PhabricatorObjectHasUnsubscriberEdgeType::EDGECONST ,
$user_phid);
}
$editor->save();
}
$table->saveTransaction();
-echo "Done.\n";
+echo pht('Done.')."\n";
diff --git a/resources/sql/patches/20130218.updatechannelid.php b/resources/sql/patches/20130218.updatechannelid.php
index b8b6d8c41..cf60544a3 100644
--- a/resources/sql/patches/20130218.updatechannelid.php
+++ b/resources/sql/patches/20130218.updatechannelid.php
@@ -1,64 +1,64 @@
<?php
-echo "Updating channel IDs of previous chatlog events...\n";
+echo pht('Updating channel IDs of previous chatlog events...')."\n";
$event_table = new PhabricatorChatLogEvent();
$channel_table = new PhabricatorChatLogChannel();
$event_table->openTransaction();
$channel_table->openTransaction();
$event_table->beginReadLocking();
$channel_table->beginReadLocking();
$events = new LiskMigrationIterator($event_table);
$conn_w = $channel_table->establishConnection('w');
foreach ($events as $event) {
if ($event->getChannelID()) {
continue;
}
$event_row = queryfx_one(
$conn_w,
'SELECT channel FROM %T WHERE id = %d',
$event->getTableName(),
$event->getID());
$event_channel = $event_row['channel'];
$matched = queryfx_one(
$conn_w,
'SELECT * FROM %T WHERE
channelName = %s AND serviceName = %s AND serviceType = %s',
$channel_table->getTableName(),
$event_channel,
'',
'');
if (!$matched) {
$matched = id(new PhabricatorChatLogChannel())
->setChannelName($event_channel)
->setServiceType('')
->setServiceName('')
->setViewPolicy(PhabricatorPolicies::POLICY_USER)
->setEditPolicy(PhabricatorPolicies::POLICY_USER)
->save();
$matched_id = $matched->getID();
} else {
$matched_id = $matched['id'];
}
queryfx(
$event->establishConnection('w'),
'UPDATE %T SET channelID = %d WHERE id = %d',
$event->getTableName(),
$matched_id,
$event->getID());
}
$event_table->endReadLocking();
$channel_table->endReadLocking();
$event_table->saveTransaction();
$channel_table->saveTransaction();
-echo "\nDone.\n";
+echo "\n".pht('Done.')."\n";
diff --git a/resources/sql/patches/20130219.commitsummarymig.php b/resources/sql/patches/20130219.commitsummarymig.php
index 85ed6e34f..60bdd1542 100644
--- a/resources/sql/patches/20130219.commitsummarymig.php
+++ b/resources/sql/patches/20130219.commitsummarymig.php
@@ -1,31 +1,31 @@
<?php
-echo "Backfilling commit summaries...\n";
+echo pht('Backfilling commit summaries...')."\n";
$table = new PhabricatorRepositoryCommit();
$conn_w = $table->establishConnection('w');
$commits = new LiskMigrationIterator($table);
foreach ($commits as $commit) {
- echo 'Filling Commit #'.$commit->getID()."\n";
+ echo pht('Filling Commit #%d', $commit->getID())."\n";
if (strlen($commit->getSummary())) {
continue;
}
$data = $commit->loadOneRelative(
new PhabricatorRepositoryCommitData(),
'commitID');
if (!$data) {
continue;
}
queryfx(
$conn_w,
'UPDATE %T SET summary = %s WHERE id = %d',
$commit->getTableName(),
$data->getSummary(),
$commit->getID());
}
-echo "Done.\n";
+echo pht('Done.')."\n";
diff --git a/resources/sql/patches/20130403.conpherencecachemig.php b/resources/sql/patches/20130403.conpherencecachemig.php
index ffc50006c..cad9fc626 100644
--- a/resources/sql/patches/20130403.conpherencecachemig.php
+++ b/resources/sql/patches/20130403.conpherencecachemig.php
@@ -1,64 +1,65 @@
<?php
-echo "Migrating data from conpherence transactions to conpherence 'cache'...\n";
+echo pht(
+ "Migrating data from conpherence transactions to conpherence 'cache'...\n");
$table = new ConpherenceThread();
$table->openTransaction();
$conn_w = $table->establishConnection('w');
$participant_table = new ConpherenceParticipant();
$conpherences = new LiskMigrationIterator($table);
foreach ($conpherences as $conpherence) {
- echo 'Migrating conpherence #'.$conpherence->getID()."\n";
+ echo pht('Migrating conpherence #%d', $conpherence->getID())."\n";
$participants = id(new ConpherenceParticipant())
->loadAllWhere('conpherencePHID = %s', $conpherence->getPHID());
$transactions = id(new ConpherenceTransaction())
->loadAllWhere('objectPHID = %s', $conpherence->getPHID());
$participation_hash = mgroup($participants, 'getBehindTransactionPHID');
$message_count = 0;
$participants_to_cache = array();
foreach ($transactions as $transaction) {
$participants_to_cache[] = $transaction->getAuthorPHID();
if ($transaction->getTransactionType() ==
PhabricatorTransactions::TYPE_COMMENT) {
$message_count++;
}
$participants_to_update = idx(
$participation_hash,
$transaction->getPHID(),
array());
if ($participants_to_update) {
queryfx(
$conn_w,
'UPDATE %T SET seenMessageCount = %d '.
'WHERE conpherencePHID = %s AND participantPHID IN (%Ls)',
$participant_table->getTableName(),
$message_count,
$conpherence->getPHID(),
mpull($participants_to_update, 'getParticipantPHID'));
}
}
$participants_to_cache = array_slice(
array_unique(array_reverse($participants_to_cache)),
0,
10);
queryfx(
$conn_w,
'UPDATE %T '.
'SET recentParticipantPHIDs = %s, '.
'messageCount = %d '.
'WHERE phid = %s',
$table->getTableName(),
json_encode($participants_to_cache),
$message_count,
$conpherence->getPHID());
}
$table->saveTransaction();
-echo "\nDone.\n";
+echo "\n".pht('Done.')."\n";
diff --git a/resources/sql/patches/20130409.commitdrev.php b/resources/sql/patches/20130409.commitdrev.php
index d3a2faefb..fb556f184 100644
--- a/resources/sql/patches/20130409.commitdrev.php
+++ b/resources/sql/patches/20130409.commitdrev.php
@@ -1,33 +1,33 @@
<?php
-echo "Migrating differential.revisionPHID to edges...\n";
+echo pht('Migrating %s to edges...', 'differential.revisionPHID')."\n";
$commit_table = new PhabricatorRepositoryCommit();
$data_table = new PhabricatorRepositoryCommitData();
$editor = new PhabricatorEdgeEditor();
$commit_table->establishConnection('w');
$edges = 0;
foreach (new LiskMigrationIterator($commit_table) as $commit) {
$data = $commit->loadOneRelative($data_table, 'commitID');
if (!$data) {
continue;
}
$revision_phid = $data->getCommitDetail('differential.revisionPHID');
if (!$revision_phid) {
continue;
}
$commit_drev = DiffusionCommitHasRevisionEdgeType::EDGECONST;
$editor->addEdge($commit->getPHID(), $commit_drev, $revision_phid);
$edges++;
if ($edges % 256 == 0) {
echo '.';
$editor->save();
$editor = new PhabricatorEdgeEditor();
}
}
echo '.';
$editor->save();
-echo "\nDone.\n";
+echo "\n".pht('Done.')."\n";
diff --git a/resources/sql/patches/20130502.countdownrevamp2.php b/resources/sql/patches/20130502.countdownrevamp2.php
index 6f7e41e1d..04d998dab 100644
--- a/resources/sql/patches/20130502.countdownrevamp2.php
+++ b/resources/sql/patches/20130502.countdownrevamp2.php
@@ -1,23 +1,23 @@
<?php
-echo 'Giving countdowns PHIDs';
+echo pht('Giving countdowns PHIDs');
$table = new PhabricatorCountdown();
$table->openTransaction();
foreach (new LiskMigrationIterator($table) as $countdown) {
if ($countdown->getPHID()) {
continue;
}
echo '.';
queryfx(
$countdown->establishConnection('w'),
'UPDATE %T SET phid = %s WHERE id = %d',
$countdown->getTableName(),
$countdown->generatePHID(),
$countdown->getID());
}
$table->saveTransaction();
-echo "\nDone.\n";
+echo "\n".pht('Done.')."\n";
diff --git a/resources/sql/patches/20130507.releephrqmailkeypop.php b/resources/sql/patches/20130507.releephrqmailkeypop.php
index 4acf3a538..016381dc5 100644
--- a/resources/sql/patches/20130507.releephrqmailkeypop.php
+++ b/resources/sql/patches/20130507.releephrqmailkeypop.php
@@ -1,27 +1,27 @@
<?php
-echo "Populating Releeph requests with mail keys...\n";
+echo pht('Populating Releeph requests with mail keys...')."\n";
$table = new ReleephRequest();
$table->openTransaction();
// From ponder-mailkey-populate.php...
foreach (new LiskMigrationIterator($table) as $rq) {
$id = $rq->getID();
echo "RQ{$id}: ";
if (!$rq->getMailKey()) {
queryfx(
$rq->establishConnection('w'),
'UPDATE %T SET mailKey = %s WHERE id = %d',
$rq->getTableName(),
Filesystem::readRandomCharacters(20),
$id);
- echo "Generated Key\n";
+ echo pht('Generated Key')."\n";
} else {
echo "-\n";
}
}
$table->saveTransaction();
-echo "Done.\n";
+echo pht('Done.')."\n";
diff --git a/resources/sql/patches/20130529.macroauthormig.php b/resources/sql/patches/20130529.macroauthormig.php
index 89f10e29c..f642f392c 100644
--- a/resources/sql/patches/20130529.macroauthormig.php
+++ b/resources/sql/patches/20130529.macroauthormig.php
@@ -1,39 +1,39 @@
<?php
-echo "Migrating macro authors...\n";
+echo pht('Migrating macro authors...')."\n";
foreach (new LiskMigrationIterator(new PhabricatorFileImageMacro()) as $macro) {
- echo "Macro #".$macro->getID()."\n";
+ echo pht('Macro #%d', $macro->getID())."\n";
if ($macro->getAuthorPHID()) {
// Already have an author; skip it.
continue;
}
if (!$macro->getFilePHID()) {
// No valid file; skip it.
continue;
}
$file = id(new PhabricatorFile())->loadOneWhere(
'phid = %s',
$macro->getFilePHID());
if (!$file) {
// Couldn't load the file; skip it.
continue;
}
if (!$file->getAuthorPHID()) {
// File has no author; skip it.
continue;
}
queryfx(
$macro->establishConnection('w'),
'UPDATE %T SET authorPHID = %s WHERE id = %d',
$macro->getTableName(),
$file->getAuthorPHID(),
$macro->getID());
}
-echo "Done.\n";
+echo pht('Done.')."\n";
diff --git a/resources/sql/patches/20130611.migrateoauth.php b/resources/sql/patches/20130611.migrateoauth.php
index 468ee78ff..3622b2772 100644
--- a/resources/sql/patches/20130611.migrateoauth.php
+++ b/resources/sql/patches/20130611.migrateoauth.php
@@ -1,66 +1,66 @@
<?php
// NOTE: We aren't using PhabricatorUserOAuthInfo anywhere here because it is
// getting nuked in a future diff.
$table = new PhabricatorUser();
$table_name = 'user_oauthinfo';
$conn_w = $table->establishConnection('w');
$xaccount = new PhabricatorExternalAccount();
-echo "Migrating OAuth to ExternalAccount...\n";
+echo pht('Migrating OAuth to %s...', 'ExternalAccount')."\n";
$domain_map = array(
'disqus' => 'disqus.com',
'facebook' => 'facebook.com',
'github' => 'github.com',
'google' => 'google.com',
);
try {
$phabricator_oauth_uri = new PhutilURI(
PhabricatorEnv::getEnvConfig('phabricator.oauth-uri'));
$domain_map['phabricator'] = $phabricator_oauth_uri->getDomain();
} catch (Exception $ex) {
// Ignore; this likely indicates that we have removed `phabricator.oauth-uri`
// in some future diff.
}
$rows = queryfx_all(
$conn_w,
'SELECT * FROM user_oauthinfo');
foreach ($rows as $row) {
- echo "Migrating row ID #".$row['id'].".\n";
+ echo pht('Migrating row ID #%d.', $row['id'])."\n";
$user = id(new PhabricatorUser())->loadOneWhere(
'id = %d',
$row['userID']);
if (!$user) {
- echo "Bad user ID!\n";
+ echo pht('Bad user ID!')."\n";
continue;
}
$domain = idx($domain_map, $row['oauthProvider']);
if (empty($domain)) {
- echo "Unknown OAuth provider!\n";
+ echo pht('Unknown OAuth provider!')."\n";
continue;
}
$xaccount = id(new PhabricatorExternalAccount())
->setUserPHID($user->getPHID())
->setAccountType($row['oauthProvider'])
->setAccountDomain($domain)
->setAccountID($row['oauthUID'])
->setAccountURI($row['accountURI'])
->setUsername($row['accountName'])
->setDateCreated($row['dateCreated']);
try {
$xaccount->save();
} catch (Exception $ex) {
phlog($ex);
}
}
-echo "Done.\n";
+echo pht('Done.')."\n";
diff --git a/resources/sql/patches/20130611.nukeldap.php b/resources/sql/patches/20130611.nukeldap.php
index ee97bb64a..3f225cfa8 100644
--- a/resources/sql/patches/20130611.nukeldap.php
+++ b/resources/sql/patches/20130611.nukeldap.php
@@ -1,41 +1,41 @@
<?php
// NOTE: We aren't using PhabricatorUserLDAPInfo anywhere here because it is
// being nuked by this change
$table = new PhabricatorUser();
$table_name = 'user_ldapinfo';
$conn_w = $table->establishConnection('w');
$xaccount = new PhabricatorExternalAccount();
-echo "Migrating LDAP to ExternalAccount...\n";
+echo pht('Migrating LDAP to %s...', 'ExternalAccount')."\n";
$rows = queryfx_all($conn_w, 'SELECT * FROM %T', $table_name);
foreach ($rows as $row) {
- echo "Migrating row ID #".$row['id'].".\n";
+ echo pht('Migrating row ID #%d.', $row['id'])."\n";
$user = id(new PhabricatorUser())->loadOneWhere(
'id = %d',
$row['userID']);
if (!$user) {
- echo "Bad user ID!\n";
+ echo pht('Bad user ID!')."\n";
continue;
}
$xaccount = id(new PhabricatorExternalAccount())
->setUserPHID($user->getPHID())
->setAccountType('ldap')
->setAccountDomain('self')
->setAccountID($row['ldapUsername'])
->setUsername($row['ldapUsername'])
->setDateCreated($row['dateCreated']);
try {
$xaccount->save();
} catch (Exception $ex) {
phlog($ex);
}
}
-echo "Done.\n";
+echo pht('Done.')."\n";
diff --git a/resources/sql/patches/20130619.authconf.php b/resources/sql/patches/20130619.authconf.php
index 0b5673552..ab0378ee6 100644
--- a/resources/sql/patches/20130619.authconf.php
+++ b/resources/sql/patches/20130619.authconf.php
@@ -1,164 +1,164 @@
<?php
$config_map = array(
'PhabricatorLDAPAuthProvider' => array(
'enabled' => 'ldap.auth-enabled',
'registration' => true,
'type' => 'ldap',
'domain' => 'self',
),
'PhabricatorAuthProviderOAuthDisqus' => array(
'enabled' => 'disqus.auth-enabled',
'registration' => 'disqus.registration-enabled',
'permanent' => 'disqus.auth-permanent',
'oauth.id' => 'disqus.application-id',
'oauth.secret' => 'disqus.application-secret',
'type' => 'disqus',
'domain' => 'disqus.com',
),
'PhabricatorFacebookAuthProvider' => array(
'enabled' => 'facebook.auth-enabled',
'registration' => 'facebook.registration-enabled',
'permanent' => 'facebook.auth-permanent',
'oauth.id' => 'facebook.application-id',
'oauth.secret' => 'facebook.application-secret',
'type' => 'facebook',
'domain' => 'facebook.com',
),
'PhabricatorAuthProviderOAuthGitHub' => array(
'enabled' => 'github.auth-enabled',
'registration' => 'github.registration-enabled',
'permanent' => 'github.auth-permanent',
'oauth.id' => 'github.application-id',
'oauth.secret' => 'github.application-secret',
'type' => 'github',
'domain' => 'github.com',
),
'PhabricatorAuthProviderOAuthGoogle' => array(
'enabled' => 'google.auth-enabled',
'registration' => 'google.registration-enabled',
'permanent' => 'google.auth-permanent',
'oauth.id' => 'google.application-id',
'oauth.secret' => 'google.application-secret',
'type' => 'google',
'domain' => 'google.com',
),
'PhabricatorPasswordAuthProvider' => array(
'enabled' => 'auth.password-auth-enabled',
'enabled-default' => false,
'registration' => false,
'type' => 'password',
'domain' => 'self',
),
);
foreach ($config_map as $provider_class => $spec) {
$enabled_key = idx($spec, 'enabled');
$enabled_default = idx($spec, 'enabled-default', false);
$enabled = PhabricatorEnv::getEnvConfigIfExists(
$enabled_key,
$enabled_default);
if (!$enabled) {
- echo pht("Skipping %s (not enabled).\n", $provider_class);
+ echo pht('Skipping %s (not enabled).', $provider_class)."\n";
// This provider was not previously enabled, so we can skip migrating it.
continue;
} else {
- echo pht("Migrating %s...\n", $provider_class);
+ echo pht('Migrating %s...', $provider_class)."\n";
}
$registration_key = idx($spec, 'registration');
if ($registration_key === true) {
$registration = 1;
} else if ($registration_key === false) {
$registration = 0;
} else {
$registration = (int)PhabricatorEnv::getEnvConfigIfExists(
$registration_key,
true);
}
$unlink_key = idx($spec, 'permanent');
if (!$unlink_key) {
$unlink = 1;
} else {
$unlink = (int)(!PhabricatorEnv::getEnvConfigIfExists($unlink_key));
}
$config = id(new PhabricatorAuthProviderConfig())
->setIsEnabled(1)
->setShouldAllowLogin(1)
->setShouldAllowRegistration($registration)
->setShouldAllowLink(1)
->setShouldAllowUnlink($unlink)
->setProviderType(idx($spec, 'type'))
->setProviderDomain(idx($spec, 'domain'))
->setProviderClass($provider_class);
if (isset($spec['oauth.id'])) {
$config->setProperty(
PhabricatorAuthProviderOAuth::PROPERTY_APP_ID,
PhabricatorEnv::getEnvConfigIfExists(idx($spec, 'oauth.id')));
$config->setProperty(
PhabricatorAuthProviderOAuth::PROPERTY_APP_SECRET,
PhabricatorEnv::getEnvConfigIfExists(idx($spec, 'oauth.secret')));
}
switch ($provider_class) {
case 'PhabricatorFacebookAuthProvider':
$config->setProperty(
PhabricatorFacebookAuthProvider::KEY_REQUIRE_SECURE,
(int)PhabricatorEnv::getEnvConfigIfExists(
'facebook.require-https-auth'));
break;
case 'PhabricatorLDAPAuthProvider':
$ldap_map = array(
PhabricatorLDAPAuthProvider::KEY_HOSTNAME
=> 'ldap.hostname',
PhabricatorLDAPAuthProvider::KEY_PORT
=> 'ldap.port',
PhabricatorLDAPAuthProvider::KEY_DISTINGUISHED_NAME
=> 'ldap.base_dn',
PhabricatorLDAPAuthProvider::KEY_SEARCH_ATTRIBUTES
=> 'ldap.search_attribute',
PhabricatorLDAPAuthProvider::KEY_USERNAME_ATTRIBUTE
=> 'ldap.username-attribute',
PhabricatorLDAPAuthProvider::KEY_REALNAME_ATTRIBUTES
=> 'ldap.real_name_attributes',
PhabricatorLDAPAuthProvider::KEY_VERSION
=> 'ldap.version',
PhabricatorLDAPAuthProvider::KEY_REFERRALS
=> 'ldap.referrals',
PhabricatorLDAPAuthProvider::KEY_START_TLS
=> 'ldap.start-tls',
PhabricatorLDAPAuthProvider::KEY_ANONYMOUS_USERNAME
=> 'ldap.anonymous-user-name',
PhabricatorLDAPAuthProvider::KEY_ANONYMOUS_PASSWORD
=> 'ldap.anonymous-user-password',
// Update the old "search first" setting to the newer but similar
// "always search" setting.
PhabricatorLDAPAuthProvider::KEY_ALWAYS_SEARCH
=> 'ldap.search-first',
PhabricatorLDAPAuthProvider::KEY_ACTIVEDIRECTORY_DOMAIN
=> 'ldap.activedirectory_domain',
);
$defaults = array(
'ldap.version' => 3,
'ldap.port' => 389,
);
foreach ($ldap_map as $pkey => $ckey) {
$default = idx($defaults, $ckey);
$config->setProperty(
$pkey,
PhabricatorEnv::getEnvConfigIfExists($ckey, $default));
}
break;
}
$config->save();
}
-echo "Done.\n";
+echo pht('Done.')."\n";
diff --git a/resources/sql/patches/20130703.legalpaddocdenorm.php b/resources/sql/patches/20130703.legalpaddocdenorm.php
index 9cb6ef197..61056ceb2 100644
--- a/resources/sql/patches/20130703.legalpaddocdenorm.php
+++ b/resources/sql/patches/20130703.legalpaddocdenorm.php
@@ -1,46 +1,48 @@
<?php
-echo 'Populating Legalpad Documents with ',
- "titles, recentContributorPHIDs, and contributorCounts...\n";
+echo pht(
+ "Populating Legalpad Documents with titles, %s, and %s...\n",
+ 'recentContributorPHIDs',
+ 'contributorCounts');
$table = new LegalpadDocument();
$table->openTransaction();
foreach (new LiskMigrationIterator($table) as $document) {
$updated = false;
$id = $document->getID();
- echo "Document {$id}: ";
+ echo pht('Document %d: ', $id);
if (!$document->getTitle()) {
$document_body = id(new LegalpadDocumentBody())
->loadOneWhere('phid = %s', $document->getDocumentBodyPHID());
$title = $document_body->getTitle();
$document->setTitle($title);
$updated = true;
- echo "Added title: $title\n";
+ echo pht('Added title: %s', $title)."\n";
} else {
echo "-\n";
}
if (!$document->getContributorCount() ||
!$document->getRecentContributorPHIDs()) {
$updated = true;
$type = PhabricatorObjectHasContributorEdgeType::EDGECONST;
$contributors = PhabricatorEdgeQuery::loadDestinationPHIDs(
$document->getPHID(),
$type);
$document->setRecentContributorPHIDs(array_slice($contributors, 0, 3));
- echo "Added recent contributor phids.\n";
+ echo pht('Added recent contributor PHIDs.')."\n";
$document->setContributorCount(count($contributors));
- echo "Added contributor count.\n";
+ echo pht('Added contributor count.')."\n";
}
if (!$updated) {
echo "-\n";
continue;
}
$document->save();
}
$table->saveTransaction();
-echo "Done.\n";
+echo pht('Done.')."\n";
diff --git a/resources/sql/patches/20130711.pholioimageobsolete.php b/resources/sql/patches/20130711.pholioimageobsolete.php
index eae9a479d..a0f805c69 100644
--- a/resources/sql/patches/20130711.pholioimageobsolete.php
+++ b/resources/sql/patches/20130711.pholioimageobsolete.php
@@ -1,23 +1,23 @@
<?php
-echo 'Giving pholio images PHIDs';
+echo pht('Giving Pholio images PHIDs');
$table = new PholioImage();
$table->openTransaction();
foreach (new LiskMigrationIterator($table) as $image) {
if ($image->getPHID()) {
continue;
}
echo '.';
queryfx(
$image->establishConnection('w'),
'UPDATE %T SET phid = %s WHERE id = %d',
$image->getTableName(),
$image->generatePHID(),
$image->getID());
}
$table->saveTransaction();
-echo "\nDone.\n";
+echo "\n".pht('Done.')."\n";
diff --git a/resources/sql/patches/20130711.trimrealnames.php b/resources/sql/patches/20130711.trimrealnames.php
index c422565b3..a5d2ef2c4 100644
--- a/resources/sql/patches/20130711.trimrealnames.php
+++ b/resources/sql/patches/20130711.trimrealnames.php
@@ -1,26 +1,26 @@
<?php
$table = new PhabricatorUser();
$conn_w = $table->establishConnection('w');
-echo "Trimming trailing whitespace from user real names...\n";
+echo pht('Trimming trailing whitespace from user real names...')."\n";
foreach (new LiskMigrationIterator($table) as $user) {
$id = $user->getID();
$real = $user->getRealName();
$trim = rtrim($real);
if ($trim == $real) {
- echo "User {$id} is already trim.\n";
+ echo pht('User %d is already trim.', $id)."\n";
continue;
}
- echo "Trimming user {$id} from '{$real}' to '{$trim}'.\n";
+ echo pht("Trimming user %d from '%s' to '%s'.", $id, $real, $trim)."\n";
qsprintf(
$conn_w,
'UPDATE %T SET realName = %s WHERE id = %d',
$table->getTableName(),
$real,
$id);
}
-echo "Done.\n";
+echo pht('Done.')."\n";
diff --git a/resources/sql/patches/20130715.votecomments.php b/resources/sql/patches/20130715.votecomments.php
index c60b13bb3..0d540596b 100644
--- a/resources/sql/patches/20130715.votecomments.php
+++ b/resources/sql/patches/20130715.votecomments.php
@@ -1,101 +1,101 @@
<?php
-echo "Moving Slowvote comments to transactions...\n";
+echo pht('Moving Slowvote comments to transactions...')."\n";
$viewer = PhabricatorUser::getOmnipotentUser();
$table_xaction = new PhabricatorSlowvoteTransaction();
$table_comment = new PhabricatorSlowvoteTransactionComment();
$conn_w = $table_xaction->establishConnection('w');
$comments = new LiskRawMigrationIterator($conn_w, 'slowvote_comment');
$conn_w->openTransaction();
foreach ($comments as $comment) {
$id = $comment['id'];
$poll_id = $comment['pollID'];
$author_phid = $comment['authorPHID'];
$text = $comment['commentText'];
$date_created = $comment['dateCreated'];
$date_modified = $comment['dateModified'];
- echo "Migrating comment {$id}.\n";
+ echo pht('Migrating comment %d.', $id)."\n";
$poll = id(new PhabricatorSlowvoteQuery())
->setViewer($viewer)
->withIDs(array($poll_id))
->executeOne();
if (!$poll) {
- echo "No poll.\n";
+ echo pht('No poll.')."\n";
continue;
}
$user = id(new PhabricatorPeopleQuery())
->setViewer($viewer)
->withPHIDs(array($author_phid))
->executeOne();
if (!$user) {
- echo "No user.\n";
+ echo pht('No user.')."\n";
continue;
}
$comment_phid = PhabricatorPHID::generateNewPHID(
PhabricatorPHIDConstants::PHID_TYPE_XCMT);
$xaction_phid = PhabricatorPHID::generateNewPHID(
PhabricatorApplicationTransactionTransactionPHIDType::TYPECONST,
PhabricatorSlowvotePollPHIDType::TYPECONST);
$source = PhabricatorContentSource::newForSource(
PhabricatorContentSource::SOURCE_LEGACY,
array())->serialize();
queryfx(
$conn_w,
'INSERT INTO %T (phid, transactionPHID, authorPHID, viewPolicy, editPolicy,
commentVersion, content, contentSource, isDeleted,
dateCreated, dateModified)
VALUES (%s, %s, %s, %s, %s,
%d, %s, %s, %d,
%d, %d)',
$table_comment->getTableName(),
$comment_phid,
$xaction_phid,
$user->getPHID(),
PhabricatorPolicies::POLICY_PUBLIC,
$user->getPHID(),
1,
$text,
$source,
0,
$date_created,
$date_modified);
queryfx(
$conn_w,
'INSERT INTO %T (phid, authorPHID, objectPHID, viewPolicy, editPolicy,
commentPHID, commentVersion, transactionType, oldValue, newValue,
contentSource, metadata, dateCreated, dateModified)
VALUES (%s, %s, %s, %s, %s,
%s, %d, %s, %s, %s,
%s, %s, %d, %d)',
$table_xaction->getTableName(),
$xaction_phid,
$user->getPHID(),
$poll->getPHID(),
PhabricatorPolicies::POLICY_PUBLIC,
$user->getPHID(),
$comment_phid,
1,
PhabricatorTransactions::TYPE_COMMENT,
null,
null,
$source,
'{}',
$date_created,
$date_modified);
}
$conn_w->saveTransaction();
-echo "Done.\n";
+echo pht('Done.')."\n";
diff --git a/resources/sql/patches/20130716.archivememberlessprojects.php b/resources/sql/patches/20130716.archivememberlessprojects.php
index 259c564ce..bd5671cba 100644
--- a/resources/sql/patches/20130716.archivememberlessprojects.php
+++ b/resources/sql/patches/20130716.archivememberlessprojects.php
@@ -1,39 +1,38 @@
<?php
-echo "Archiving projects with no members...\n";
+echo pht('Archiving projects with no members...')."\n";
$table = new PhabricatorProject();
$table->openTransaction();
foreach (new LiskMigrationIterator($table) as $project) {
-
$members = PhabricatorEdgeQuery::loadDestinationPHIDs(
$project->getPHID(),
PhabricatorProjectProjectHasMemberEdgeType::EDGECONST);
if (count($members)) {
- echo sprintf(
+ echo pht(
'Project "%s" has %d members; skipping.',
$project->getName(),
count($members)), "\n";
continue;
}
if ($project->getStatus() == PhabricatorProjectStatus::STATUS_ARCHIVED) {
- echo sprintf(
+ echo pht(
'Project "%s" already archived; skipping.',
$project->getName()), "\n";
continue;
}
- echo sprintf('Archiving project "%s"...', $project->getName()), "\n";
+ echo pht('Archiving project "%s"...', $project->getName())."\n";
queryfx(
$table->establishConnection('w'),
'UPDATE %T SET status = %s WHERE id = %d',
$table->getTableName(),
PhabricatorProjectStatus::STATUS_ARCHIVED,
$project->getID());
}
$table->saveTransaction();
-echo "\nDone.\n";
+echo "\n".pht('Done.')."\n";
diff --git a/resources/sql/patches/20130728.ponderunique.php b/resources/sql/patches/20130728.ponderunique.php
index 94a45ae78..2facd4e3f 100644
--- a/resources/sql/patches/20130728.ponderunique.php
+++ b/resources/sql/patches/20130728.ponderunique.php
@@ -1,58 +1,58 @@
<?php
$map = array();
-echo "Merging duplicate answers by authors...\n";
+echo pht('Merging duplicate answers by authors...')."\n";
$atable = new PonderAnswer();
$conn_w = $atable->establishConnection('w');
$conn_w->openTransaction();
$answers = new LiskMigrationIterator(new PonderAnswer());
foreach ($answers as $answer) {
$aid = $answer->getID();
$qid = $answer->getQuestionID();
$author_phid = $answer->getAuthorPHID();
- echo "Processing answer ID #{$aid}...\n";
+ echo pht('Processing answer ID #%d...', $aid)."\n";
if (empty($map[$qid][$author_phid])) {
- echo "Answer is unique.\n";
+ echo pht('Answer is unique.')."\n";
$map[$qid][$author_phid] = $answer;
continue;
} else {
- echo "Merging answer.\n";
+ echo pht('Merging answer.')."\n";
$target = $map[$qid][$author_phid];
queryfx(
$conn_w,
'UPDATE %T SET content = %s WHERE id = %d',
$target->getTableName(),
$target->getContent().
"\n\n".
"---".
"\n\n".
"> (This content was automatically merged from another answer by the ".
"same author.)".
"\n\n".
$answer->getContent(),
$target->getID());
queryfx(
$conn_w,
'DELETE FROM %T WHERE id = %d',
$target->getTableName(),
$answer->getID());
queryfx(
$conn_w,
'UPDATE %T SET targetPHID = %s WHERE targetPHID = %s',
'ponder_comment',
$target->getPHID(),
$answer->getPHID());
}
}
$conn_w->saveTransaction();
-echo "Done.\n";
+echo pht('Done.')."\n";
diff --git a/resources/sql/patches/20130728.ponderxcomment.php b/resources/sql/patches/20130728.ponderxcomment.php
index 1fb65ade7..92e858f95 100644
--- a/resources/sql/patches/20130728.ponderxcomment.php
+++ b/resources/sql/patches/20130728.ponderxcomment.php
@@ -1,86 +1,86 @@
<?php
$qtable = new PonderQuestionTransaction();
$atable = new PonderAnswerTransaction();
$conn_w = $qtable->establishConnection('w');
$conn_w->openTransaction();
-echo "Migrating Ponder comments to ApplicationTransactions...\n";
+echo pht('Migrating Ponder comments to %s...', 'ApplicationTransactions')."\n";
$rows = new LiskRawMigrationIterator($conn_w, 'ponder_comment');
foreach ($rows as $row) {
$id = $row['id'];
- echo "Migrating {$id}...\n";
+ echo pht('Migrating %d...', $id)."\n";
$type = phid_get_type($row['targetPHID']);
switch ($type) {
case PonderQuestionPHIDType::TYPECONST:
$table_obj = $qtable;
$comment_obj = new PonderQuestionTransactionComment();
break;
case PonderAnswerPHIDType::TYPECONST:
$table_obj = $atable;
$comment_obj = new PonderAnswerTransactionComment();
break;
}
$comment_phid = PhabricatorPHID::generateNewPHID(
PhabricatorApplicationTransactionTransactionPHIDType::TYPECONST,
$type);
$xaction_phid = PhabricatorPHID::generateNewPHID(
PhabricatorApplicationTransactionTransactionPHIDType::TYPECONST,
$type);
queryfx(
$conn_w,
'INSERT INTO %T (phid, transactionPHID, authorPHID, viewPolicy, editPolicy,
commentVersion, content, contentSource, isDeleted, dateCreated,
dateModified)
VALUES (%s, %s, %s, %s, %s, %d, %s, %s, %d, %d, %d)',
$comment_obj->getTableName(),
$comment_phid,
$xaction_phid,
$row['authorPHID'],
'public',
$row['authorPHID'],
1,
$row['content'],
PhabricatorContentSource::newForSource(
PhabricatorContentSource::SOURCE_LEGACY,
array())->serialize(),
0,
$row['dateCreated'],
$row['dateModified']);
queryfx(
$conn_w,
'INSERT INTO %T (phid, authorPHID, objectPHID, viewPolicy, editPolicy,
commentPHID, commentVersion, transactionType, oldValue, newValue,
contentSource, metadata, dateCreated, dateModified)
VALUES (%s, %s, %s, %s, %s, %s, %d, %s, %ns, %ns, %s, %s, %d, %d)',
$table_obj->getTableName(),
$xaction_phid,
$row['authorPHID'],
$row['targetPHID'],
'public',
$row['authorPHID'],
$comment_phid,
1,
PhabricatorTransactions::TYPE_COMMENT,
'null',
'null',
PhabricatorContentSource::newForSource(
PhabricatorContentSource::SOURCE_LEGACY,
array())->serialize(),
'[]',
$row['dateCreated'],
$row['dateModified']);
}
$conn_w->saveTransaction();
-echo "Done.\n";
+echo pht('Done.')."\n";
diff --git a/resources/sql/patches/20130801.pastexactions.php b/resources/sql/patches/20130801.pastexactions.php
index 52d5a081d..1977eb984 100644
--- a/resources/sql/patches/20130801.pastexactions.php
+++ b/resources/sql/patches/20130801.pastexactions.php
@@ -1,48 +1,48 @@
<?php
$table = new PhabricatorPaste();
$x_table = new PhabricatorPasteTransaction();
$conn_w = $table->establishConnection('w');
$conn_w->openTransaction();
-echo "Adding transactions for existing paste objects...\n";
+echo pht('Adding transactions for existing paste objects...')."\n";
$rows = new LiskRawMigrationIterator($conn_w, 'pastebin_paste');
foreach ($rows as $row) {
$id = $row['id'];
- echo "Adding transactions for paste id {$id}...\n";
+ echo pht('Adding transactions for paste id %d...', $id)."\n";
$xaction_phid = PhabricatorPHID::generateNewPHID(
PhabricatorApplicationTransactionTransactionPHIDType::TYPECONST);
queryfx(
$conn_w,
'INSERT INTO %T (phid, authorPHID, objectPHID, viewPolicy, editPolicy,
transactionType, oldValue, newValue,
contentSource, metadata, dateCreated, dateModified,
commentVersion)
VALUES (%s, %s, %s, %s, %s, %s, %ns, %ns, %s, %s, %d, %d, %d)',
$x_table->getTableName(),
$xaction_phid,
$row['authorPHID'],
$row['phid'],
'public',
$row['authorPHID'],
PhabricatorPasteTransaction::TYPE_CONTENT,
'null',
$row['filePHID'],
PhabricatorContentSource::newForSource(
PhabricatorContentSource::SOURCE_LEGACY,
array())->serialize(),
'[]',
$row['dateCreated'],
$row['dateCreated'],
0);
}
$conn_w->saveTransaction();
-echo "Done.\n";
+echo pht('Done.')."\n";
diff --git a/resources/sql/patches/20130802.heraldphids.php b/resources/sql/patches/20130802.heraldphids.php
index 814bbddef..726a335d1 100644
--- a/resources/sql/patches/20130802.heraldphids.php
+++ b/resources/sql/patches/20130802.heraldphids.php
@@ -1,24 +1,24 @@
<?php
$table = new HeraldRule();
$conn_w = $table->establishConnection('w');
-echo "Assigning PHIDs to Herald Rules...\n";
+echo pht('Assigning PHIDs to Herald Rules...')."\n";
foreach (new LiskMigrationIterator(new HeraldRule()) as $rule) {
$id = $rule->getID();
- echo "Rule {$id}.\n";
+ echo pht('Rule %d.', $id)."\n";
if ($rule->getPHID()) {
continue;
}
queryfx(
$conn_w,
'UPDATE %T SET phid = %s WHERE id = %d',
$table->getTableName(),
PhabricatorPHID::generateNewPHID(HeraldRulePHIDType::TYPECONST),
$rule->getID());
}
-echo "Done.\n";
+echo pht('Done.')."\n";
diff --git a/resources/sql/patches/20130805.pastemailkeypop.php b/resources/sql/patches/20130805.pastemailkeypop.php
index 5354453b6..bb2d51ded 100644
--- a/resources/sql/patches/20130805.pastemailkeypop.php
+++ b/resources/sql/patches/20130805.pastemailkeypop.php
@@ -1,27 +1,27 @@
<?php
-echo "Populating pastes with mail keys...\n";
+echo pht('Populating pastes with mail keys...')."\n";
$table = new PhabricatorPaste();
$table->openTransaction();
$conn_w = $table->establishConnection('w');
foreach (new LiskMigrationIterator($table) as $paste) {
$id = $paste->getID();
echo "P{$id}: ";
if (!$paste->getMailKey()) {
queryfx(
$conn_w,
'UPDATE %T SET mailKey = %s WHERE id = %d',
$paste->getTableName(),
Filesystem::readRandomCharacters(20),
$id);
- echo "Generated Key\n";
+ echo pht('Generated Key')."\n";
} else {
echo "-\n";
}
}
$table->saveTransaction();
-echo "Done.\n";
+echo pht('Done.')."\n";
diff --git a/resources/sql/patches/20130820.file-mailkey-populate.php b/resources/sql/patches/20130820.file-mailkey-populate.php
index de1a920d9..ba4d6d160 100644
--- a/resources/sql/patches/20130820.file-mailkey-populate.php
+++ b/resources/sql/patches/20130820.file-mailkey-populate.php
@@ -1,38 +1,38 @@
<?php
-echo "Populating Phabricator files with mail keys xactions...\n";
+echo pht('Populating Phabricator files with mail keys xactions...')."\n";
$table = new PhabricatorFile();
$table_name = $table->getTableName();
$conn_w = $table->establishConnection('w');
$conn_w->openTransaction();
$sql = array();
foreach (new LiskRawMigrationIterator($conn_w, 'file') as $row) {
// NOTE: MySQL requires that the INSERT specify all columns which don't
// have default values when configured in strict mode. This query will
// never actually insert rows, but we need to hand it values anyway.
$sql[] = qsprintf(
$conn_w,
'(%d, %s, 0, 0, 0, 0, 0, 0, 0, 0)',
$row['id'],
Filesystem::readRandomCharacters(20));
}
if ($sql) {
foreach (PhabricatorLiskDAO::chunkSQL($sql, ', ') as $chunk) {
queryfx(
$conn_w,
'INSERT INTO %T
(id, mailKey, phid, byteSize, storageEngine, storageFormat,
storageHandle, dateCreated, dateModified, metadata) VALUES %Q '.
'ON DUPLICATE KEY UPDATE mailKey = VALUES(mailKey)',
$table_name,
$chunk);
}
}
$table->saveTransaction();
-echo "Done.\n";
+echo pht('Done.')."\n";
diff --git a/resources/sql/patches/20130915.maniphestmigrate.php b/resources/sql/patches/20130915.maniphestmigrate.php
index a9013662e..c1c4bcaf5 100644
--- a/resources/sql/patches/20130915.maniphestmigrate.php
+++ b/resources/sql/patches/20130915.maniphestmigrate.php
@@ -1,25 +1,25 @@
<?php
$conn_w = id(new ManiphestTask())->establishConnection('w');
$table_name = id(new ManiphestCustomFieldStorage())->getTableName();
$rows = new LiskRawMigrationIterator($conn_w, 'maniphest_taskauxiliarystorage');
-echo "Migrating custom storage for Maniphest fields...\n";
+echo pht('Migrating custom storage for Maniphest fields...')."\n";
foreach ($rows as $row) {
$phid = $row['taskPHID'];
$name = $row['name'];
- echo "Migrating {$phid} / {$name}...\n";
+ echo pht('Migrating %s / %s...', $phid, $name)."\n";
queryfx(
$conn_w,
'INSERT IGNORE INTO %T (objectPHID, fieldIndex, fieldValue)
VALUES (%s, %s, %s)',
$table_name,
$phid,
PhabricatorHash::digestForIndex('std:maniphest:'.$name),
$row['value']);
}
-echo "Done.\n";
+echo pht('Done.')."\n";
diff --git a/resources/sql/patches/20130919.mfieldconf.php b/resources/sql/patches/20130919.mfieldconf.php
index 099e7c081..c79521acb 100644
--- a/resources/sql/patches/20130919.mfieldconf.php
+++ b/resources/sql/patches/20130919.mfieldconf.php
@@ -1,66 +1,66 @@
<?php
-echo "Migrating Maniphest custom field configuration...\n";
+echo pht('Migrating Maniphest custom field configuration...')."\n";
$old_key = 'maniphest.custom-fields';
$new_key = 'maniphest.custom-field-definitions';
if (PhabricatorEnv::getEnvConfig($new_key)) {
- echo "Skipping migration, new data is already set.\n";
+ echo pht('Skipping migration, new data is already set.')."\n";
return;
}
$old = PhabricatorEnv::getEnvConfigIfExists($old_key);
if (!$old) {
- echo "Skipping migration, old data does not exist.\n";
+ echo pht('Skipping migration, old data does not exist.')."\n";
return;
}
$new = array();
foreach ($old as $field_key => $spec) {
$new_spec = array();
foreach ($spec as $key => $value) {
switch ($key) {
case 'label':
$new_spec['name'] = $value;
break;
case 'required':
case 'default':
case 'caption':
case 'options':
$new_spec[$key] = $value;
break;
case 'checkbox-label':
$new_spec['strings']['edit.checkbox'] = $value;
break;
case 'checkbox-value':
$new_spec['strings']['view.yes'] = $value;
break;
case 'type':
switch ($value) {
case 'string':
$value = 'text';
break;
case 'user':
$value = 'users';
$new_spec['limit'] = 1;
break;
}
$new_spec['type'] = $value;
break;
case 'copy':
$new_spec['copy'] = $value;
break;
}
}
$new[$field_key] = $new_spec;
}
PhabricatorConfigEntry::loadConfigEntry($new_key)
->setIsDeleted(0)
->setValue($new)
->save();
-echo "Done.\n";
+echo pht('Done.')."\n";
diff --git a/resources/sql/patches/20130921.xmigratemaniphest.php b/resources/sql/patches/20130921.xmigratemaniphest.php
index 24614867f..2f7f5952c 100644
--- a/resources/sql/patches/20130921.xmigratemaniphest.php
+++ b/resources/sql/patches/20130921.xmigratemaniphest.php
@@ -1,148 +1,148 @@
<?php
$task_table = new ManiphestTask();
$conn_w = $task_table->establishConnection('w');
$rows = new LiskRawMigrationIterator($conn_w, 'maniphest_transaction');
$conn_w->openTransaction();
// NOTE: These were the correct table names at the time of this patch.
$xaction_table_name = 'maniphest_transactionpro';
$comment_table_name = 'maniphest_transaction_comment';
foreach ($rows as $row) {
$row_id = $row['id'];
$task_id = $row['taskID'];
- echo "Migrating row {$row_id} (T{$task_id})...\n";
+ echo pht('Migrating row %d (%s)...', $row_id, "T{$task_id}")."\n";
$task_row = queryfx_one(
$conn_w,
'SELECT phid FROM %T WHERE id = %d',
$task_table->getTableName(),
$task_id);
if (!$task_row) {
- echo "Skipping, no such task.\n";
+ echo pht('Skipping, no such task.')."\n";
continue;
}
$task_phid = $task_row['phid'];
$has_comment = strlen(trim($row['comments']));
$xaction_type = $row['transactionType'];
$xaction_old = $row['oldValue'];
$xaction_new = $row['newValue'];
$xaction_source = idx($row, 'contentSource', '');
$xaction_meta = $row['metadata'];
// Convert "aux" (auxiliary field) transactions to proper CustomField
// transactions. The formats are very similar, except that the type constant
// is different and the auxiliary key should be prefixed.
if ($xaction_type == 'aux') {
$xaction_meta = @json_decode($xaction_meta, true);
$xaction_meta = nonempty($xaction_meta, array());
$xaction_type = PhabricatorTransactions::TYPE_CUSTOMFIELD;
$aux_key = idx($xaction_meta, 'aux:key');
if (!preg_match('/^std:maniphest:/', $aux_key)) {
$aux_key = 'std:maniphest:'.$aux_key;
}
$xaction_meta = array(
'customfield:key' => $aux_key,
);
$xaction_meta = json_encode($xaction_meta);
}
// If this transaction did something other than just leaving a comment,
// insert a new transaction for that action. If there was a comment (or
// a comment in addition to an action) we'll insert that below.
if ($row['transactionType'] != 'comment') {
$xaction_phid = PhabricatorPHID::generateNewPHID(
PhabricatorApplicationTransactionTransactionPHIDType::TYPECONST,
ManiphestTaskPHIDType::TYPECONST);
queryfx(
$conn_w,
'INSERT INTO %T (phid, authorPHID, objectPHID, viewPolicy, editPolicy,
commentPHID, commentVersion, transactionType, oldValue, newValue,
contentSource, metadata, dateCreated, dateModified)
VALUES (%s, %s, %s, %s, %s, %s, %d, %s, %ns, %ns, %s, %s, %d, %d)',
$xaction_table_name,
$xaction_phid,
$row['authorPHID'],
$task_phid,
'public',
$row['authorPHID'],
null,
0,
$xaction_type,
$xaction_old,
$xaction_new,
$xaction_source,
$xaction_meta,
$row['dateCreated'],
$row['dateModified']);
}
// Now, if the old transaction has a comment, we insert an explicit new
// transaction for it.
if ($has_comment) {
$comment_phid = PhabricatorPHID::generateNewPHID(
PhabricatorPHIDConstants::PHID_TYPE_XCMT,
ManiphestTaskPHIDType::TYPECONST);
$comment_version = 1;
$comment_xaction_phid = PhabricatorPHID::generateNewPHID(
PhabricatorApplicationTransactionTransactionPHIDType::TYPECONST,
ManiphestTaskPHIDType::TYPECONST);
// Insert the comment data.
queryfx(
$conn_w,
'INSERT INTO %T (phid, transactionPHID, authorPHID, viewPolicy,
editPolicy, commentVersion, content, contentSource, isDeleted,
dateCreated, dateModified)
VALUES (%s, %s, %s, %s, %s, %d, %s, %s, %d, %d, %d)',
$comment_table_name,
$comment_phid,
$comment_xaction_phid,
$row['authorPHID'],
'public',
$row['authorPHID'],
$comment_version,
$row['comments'],
$xaction_source,
0,
$row['dateCreated'],
$row['dateModified']);
queryfx(
$conn_w,
'INSERT INTO %T (phid, authorPHID, objectPHID, viewPolicy, editPolicy,
commentPHID, commentVersion, transactionType, oldValue, newValue,
contentSource, metadata, dateCreated, dateModified)
VALUES (%s, %s, %s, %s, %s, %s, %d, %s, %ns, %ns, %s, %s, %d, %d)',
$xaction_table_name,
$comment_xaction_phid,
$row['authorPHID'],
$task_phid,
'public',
$row['authorPHID'],
$comment_phid,
$comment_version,
PhabricatorTransactions::TYPE_COMMENT,
$xaction_old,
$xaction_new,
$xaction_source,
$xaction_meta,
$row['dateCreated'],
$row['dateModified']);
}
}
$conn_w->saveTransaction();
-echo "Done.\n";
+echo pht('Done.')."\n";
diff --git a/resources/sql/patches/20130926.dinline.php b/resources/sql/patches/20130926.dinline.php
index 36632d910..f2d9e9f20 100644
--- a/resources/sql/patches/20130926.dinline.php
+++ b/resources/sql/patches/20130926.dinline.php
@@ -1,90 +1,90 @@
<?php
$revision_table = new DifferentialRevision();
$conn_w = $revision_table->establishConnection('w');
$conn_w->openTransaction();
$src_table = 'differential_inlinecomment';
$dst_table = 'differential_transaction_comment';
-echo "Migrating Differential inline comments to new format...\n";
+echo pht('Migrating Differential inline comments to new format...')."\n";
$content_source = PhabricatorContentSource::newForSource(
PhabricatorContentSource::SOURCE_LEGACY,
array())->serialize();
$rows = new LiskRawMigrationIterator($conn_w, $src_table);
foreach ($rows as $row) {
$id = $row['id'];
$revision_id = $row['revisionID'];
- echo "Migrating inline #{$id} (D{$revision_id})...\n";
+ echo pht('Migrating inline #%d (%s)...', $id, "D{$revision_id}")."\n";
$revision_row = queryfx_one(
$conn_w,
'SELECT phid FROM %T WHERE id = %d',
$revision_table->getTableName(),
$revision_id);
if (!$revision_row) {
continue;
}
$revision_phid = $revision_row['phid'];
if ($row['commentID']) {
$xaction_phid = PhabricatorPHID::generateNewPHID(
PhabricatorApplicationTransactionTransactionPHIDType::TYPECONST,
DifferentialRevisionPHIDType::TYPECONST);
} else {
$xaction_phid = null;
}
$comment_phid = PhabricatorPHID::generateNewPHID(
PhabricatorPHIDConstants::PHID_TYPE_XCMT,
DifferentialRevisionPHIDType::TYPECONST);
queryfx(
$conn_w,
'INSERT IGNORE INTO %T
(id, phid, transactionPHID, authorPHID, viewPolicy, editPolicy,
commentVersion, content, contentSource, isDeleted,
dateCreated, dateModified, revisionPHID, changesetID,
isNewFile, lineNumber, lineLength, hasReplies, legacyCommentID)
VALUES (%d, %s, %ns, %s, %s, %s,
%d, %s, %s, %d,
%d, %d, %s, %nd,
%d, %d, %d, %d, %nd)',
$dst_table,
// id, phid, transactionPHID, authorPHID, viewPolicy, editPolicy
$row['id'],
$comment_phid,
$xaction_phid,
$row['authorPHID'],
'public',
$row['authorPHID'],
// commentVersion, content, contentSource, isDeleted
1,
$row['content'],
$content_source,
0,
// dateCreated, dateModified, revisionPHID, changesetID
$row['dateCreated'],
$row['dateModified'],
$revision_phid,
$row['changesetID'],
// isNewFile, lineNumber, lineLength, hasReplies, legacyCommentID
$row['isNewFile'],
$row['lineNumber'],
$row['lineLength'],
0,
$row['commentID']);
}
$conn_w->saveTransaction();
-echo "Done.\n";
+echo pht('Done.')."\n";
diff --git a/resources/sql/patches/20131004.dxreviewers.php b/resources/sql/patches/20131004.dxreviewers.php
index b117048a1..4f853f5dd 100644
--- a/resources/sql/patches/20131004.dxreviewers.php
+++ b/resources/sql/patches/20131004.dxreviewers.php
@@ -1,51 +1,51 @@
<?php
$table = new DifferentialRevision();
$conn_w = $table->establishConnection('w');
// NOTE: We migrate by revision because the relationship table doesn't have
// an "id" column.
foreach (new LiskMigrationIterator($table) as $revision) {
$revision_id = $revision->getID();
$revision_phid = $revision->getPHID();
- echo "Migrating reviewers for D{$revision_id}...\n";
+ echo pht('Migrating reviewers for %s...', "D{$revision_id}")."\n";
$reviewer_phids = queryfx_all(
$conn_w,
'SELECT objectPHID FROM %T WHERE revisionID = %d
AND relation = %s ORDER BY sequence',
'differential_relationship',
$revision_id,
'revw');
$reviewer_phids = ipull($reviewer_phids, 'objectPHID');
if (!$reviewer_phids) {
continue;
}
$editor = new PhabricatorEdgeEditor();
foreach ($reviewer_phids as $dst) {
if (phid_get_type($dst) == PhabricatorPHIDConstants::PHID_TYPE_UNKNOWN) {
// At least one old install ran into some issues here. Skip the row if we
// can't figure out what the destination PHID is. See here:
// https://github.com/phacility/phabricator/pull/507
continue;
}
$editor->addEdge(
$revision_phid,
DifferentialRevisionHasReviewerEdgeType::EDGECONST,
$dst,
array(
'data' => array(
'status' => DifferentialReviewerStatus::STATUS_ADDED,
),
));
}
$editor->save();
}
-echo "Done.\n";
+echo pht('Done.')."\n";
diff --git a/resources/sql/patches/20131020.pxactionmig.php b/resources/sql/patches/20131020.pxactionmig.php
index eb7c706e1..7bf4416cd 100644
--- a/resources/sql/patches/20131020.pxactionmig.php
+++ b/resources/sql/patches/20131020.pxactionmig.php
@@ -1,92 +1,92 @@
<?php
$project_table = new PhabricatorProject();
$conn_w = $project_table->establishConnection('w');
$conn_w->openTransaction();
$src_table = 'project_legacytransaction';
$dst_table = 'project_transaction';
-echo "Migrating Project transactions to new format...\n";
+echo pht('Migrating Project transactions to new format...')."\n";
$content_source = PhabricatorContentSource::newForSource(
PhabricatorContentSource::SOURCE_LEGACY,
array())->serialize();
$rows = new LiskRawMigrationIterator($conn_w, $src_table);
foreach ($rows as $row) {
$id = $row['id'];
$project_id = $row['projectID'];
- echo "Migrating transaction #{$id} (Project {$project_id})...\n";
+ echo pht('Migrating transaction #%d (Project %d)...', $id, $project_id)."\n";
$project_row = queryfx_one(
$conn_w,
'SELECT phid FROM %T WHERE id = %d',
$project_table->getTableName(),
$project_id);
if (!$project_row) {
continue;
}
$project_phid = $project_row['phid'];
$type_map = array(
'name' => PhabricatorProjectTransaction::TYPE_NAME,
'members' => PhabricatorProjectTransaction::TYPE_MEMBERS,
'status' => PhabricatorProjectTransaction::TYPE_STATUS,
'canview' => PhabricatorTransactions::TYPE_VIEW_POLICY,
'canedit' => PhabricatorTransactions::TYPE_EDIT_POLICY,
'canjoin' => PhabricatorTransactions::TYPE_JOIN_POLICY,
);
$new_type = idx($type_map, $row['transactionType']);
if (!$new_type) {
continue;
}
$xaction_phid = PhabricatorPHID::generateNewPHID(
PhabricatorApplicationTransactionTransactionPHIDType::TYPECONST,
PhabricatorProjectProjectPHIDType::TYPECONST);
queryfx(
$conn_w,
'INSERT IGNORE INTO %T
(phid, authorPHID, objectPHID,
viewPolicy, editPolicy, commentPHID, commentVersion, transactionType,
oldValue, newValue, contentSource, metadata,
dateCreated, dateModified)
VALUES
(%s, %s, %s,
%s, %s, %ns, %d, %s,
%s, %s, %s, %s,
%d, %d)',
$dst_table,
// PHID, Author, Object
$xaction_phid,
$row['authorPHID'],
$project_phid,
// View, Edit, Comment, Version, Type
'public',
$row['authorPHID'],
null,
0,
$new_type,
// Old, New, Source, Meta,
$row['oldValue'],
$row['newValue'],
$content_source,
'{}',
// Created, Modified
$row['dateCreated'],
$row['dateModified']);
}
$conn_w->saveTransaction();
-echo "Done.\n";
+echo pht('Done.')."\n";
diff --git a/resources/sql/patches/20131106.diffphid.2.mig.php b/resources/sql/patches/20131106.diffphid.2.mig.php
index b7f502f47..67fd14aad 100644
--- a/resources/sql/patches/20131106.diffphid.2.mig.php
+++ b/resources/sql/patches/20131106.diffphid.2.mig.php
@@ -1,47 +1,47 @@
<?php
$diff_table = new DifferentialDiff();
$conn_w = $diff_table->establishConnection('w');
$size = 1000;
$row_iter = id(new LiskMigrationIterator($diff_table))->setPageSize($size);
$chunk_iter = new PhutilChunkedIterator($row_iter, $size);
foreach ($chunk_iter as $chunk) {
$sql = array();
foreach ($chunk as $diff) {
$id = $diff->getID();
- echo "Migrating diff ID {$id}...\n";
+ echo pht('Migrating diff ID %d...', $id)."\n";
$phid = $diff->getPHID();
if (strlen($phid)) {
continue;
}
$type_diff = DifferentialDiffPHIDType::TYPECONST;
$new_phid = PhabricatorPHID::generateNewPHID($type_diff);
$sql[] = qsprintf(
$conn_w,
'(%d, %s)',
$id,
$new_phid);
}
if (!$sql) {
continue;
}
foreach (PhabricatorLiskDAO::chunkSQL($sql, ', ') as $sql_chunk) {
queryfx(
$conn_w,
'INSERT IGNORE INTO %T (id, phid) VALUES %Q
ON DUPLICATE KEY UPDATE phid = VALUES(phid)',
$diff_table->getTableName(),
$sql_chunk);
}
}
-echo "Done.\n";
+echo pht('Done.')."\n";
diff --git a/resources/sql/patches/20131112.userverified.2.mig.php b/resources/sql/patches/20131112.userverified.2.mig.php
index 381fc966a..a8c231ec2 100644
--- a/resources/sql/patches/20131112.userverified.2.mig.php
+++ b/resources/sql/patches/20131112.userverified.2.mig.php
@@ -1,33 +1,33 @@
<?php
$table = new PhabricatorUser();
$conn_w = $table->establishConnection('w');
foreach (new LiskMigrationIterator($table) as $user) {
$username = $user->getUsername();
- echo "Migrating {$username}...\n";
+ echo pht('Migrating %s...', $username)."\n";
if ($user->getIsEmailVerified()) {
// Email already verified.
continue;
}
$primary = $user->loadPrimaryEmail();
if (!$primary) {
// No primary email.
continue;
}
if (!$primary->getIsVerified()) {
// Primary email not verified.
continue;
}
// Primary email is verified, so mark the account as verified.
queryfx(
$conn_w,
'UPDATE %T SET isEmailVerified = 1 WHERE id = %d',
$table->getTableName(),
$user->getID());
}
-echo "Done.\n";
+echo pht('Done.')."\n";
diff --git a/resources/sql/patches/20131118.ownerorder.php b/resources/sql/patches/20131118.ownerorder.php
index 35433f8e1..a3a565994 100644
--- a/resources/sql/patches/20131118.ownerorder.php
+++ b/resources/sql/patches/20131118.ownerorder.php
@@ -1,42 +1,42 @@
<?php
$table = new ManiphestTask();
$conn_w = $table->establishConnection('w');
$user_table = new PhabricatorUser();
$user_conn = $user_table->establishConnection('r');
foreach (new LiskMigrationIterator($table) as $task) {
$id = $task->getID();
- echo "Checking task T{$id}...\n";
+ echo pht('Checking task %s...', "T{$id}")."\n";
$owner_phid = $task->getOwnerPHID();
if (!$owner_phid && !$task->getOwnerOrdering()) {
// No owner and no ordering; we're all set.
continue;
}
$owner_row = queryfx_one(
$user_conn,
'SELECT * FROM %T WHERE phid = %s',
$user_table->getTableName(),
$owner_phid);
if ($owner_row) {
$value = $owner_row['userName'];
} else {
$value = null;
}
if ($value !== $task->getOwnerOrdering()) {
queryfx(
$conn_w,
'UPDATE %T SET ownerOrdering = %ns WHERE id = %d',
$table->getTableName(),
$value,
$task->getID());
}
}
-echo "Done.\n";
+echo pht('Done.')."\n";
diff --git a/resources/sql/patches/20131121.repocredentials.2.mig.php b/resources/sql/patches/20131121.repocredentials.2.mig.php
index acb7f2f01..3953b8416 100644
--- a/resources/sql/patches/20131121.repocredentials.2.mig.php
+++ b/resources/sql/patches/20131121.repocredentials.2.mig.php
@@ -1,139 +1,139 @@
<?php
$table = new PhabricatorRepository();
$conn_w = $table->establishConnection('w');
$viewer = PhabricatorUser::getOmnipotentUser();
$map = array();
foreach (new LiskMigrationIterator($table) as $repository) {
$callsign = $repository->getCallsign();
- echo "Examining repository {$callsign}...\n";
+ echo pht('Examining repository %s...', $callsign)."\n";
if ($repository->getCredentialPHID()) {
- echo "...already has a Credential.\n";
+ echo pht('...already has a Credential.')."\n";
continue;
}
$raw_uri = $repository->getRemoteURI();
if (!$raw_uri) {
- echo "...no remote URI.\n";
+ echo pht('...no remote URI.')."\n";
continue;
}
$uri = new PhutilURI($raw_uri);
$proto = strtolower($uri->getProtocol());
if ($proto == 'http' || $proto == 'https' || $proto == 'svn') {
$username = $repository->getDetail('http-login');
$secret = $repository->getDetail('http-pass');
$type = PassphraseCredentialTypePassword::CREDENTIAL_TYPE;
} else {
$username = $repository->getDetail('ssh-login');
if (!$username) {
// If there's no explicit username, check for one in the URI. This is
// possible with older repositories.
$username = $uri->getUser();
if (!$username) {
// Also check for a Git/SCP-style URI.
$git_uri = new PhutilGitURI($raw_uri);
$username = $git_uri->getUser();
}
}
$file = $repository->getDetail('ssh-keyfile');
if ($file) {
$secret = $file;
$type = PassphraseCredentialTypeSSHPrivateKeyFile::CREDENTIAL_TYPE;
} else {
$secret = $repository->getDetail('ssh-key');
$type = PassphraseCredentialTypeSSHPrivateKeyText::CREDENTIAL_TYPE;
}
}
if (!$username || !$secret) {
- echo "...no credentials set.\n";
+ echo pht('...no credentials set.')."\n";
continue;
}
$map[$type][$username][$secret][] = $repository;
- echo "...will migrate.\n";
+ echo pht('...will migrate.')."\n";
}
$passphrase = new PassphraseSecret();
$passphrase->openTransaction();
$table->openTransaction();
foreach ($map as $credential_type => $credential_usernames) {
$type = PassphraseCredentialType::getTypeByConstant($credential_type);
foreach ($credential_usernames as $username => $credential_secrets) {
foreach ($credential_secrets as $secret_plaintext => $repositories) {
$callsigns = mpull($repositories, 'getCallsign');
$signs = implode(', ', $callsigns);
$name = pht(
'Migrated Repository Credential (%s)',
id(new PhutilUTF8StringTruncator())
->setMaximumGlyphs(128)
->truncateString($signs));
- echo "Creating: {$name}...\n";
+ echo pht('Creating: %s...', $name)."\n";
$secret = id(new PassphraseSecret())
->setSecretData($secret_plaintext)
->save();
$secret_id = $secret->getID();
$credential = PassphraseCredential::initializeNewCredential($viewer)
->setCredentialType($type->getCredentialType())
->setProvidesType($type->getProvidesType())
->setViewPolicy(PhabricatorPolicies::POLICY_ADMIN)
->setEditPolicy(PhabricatorPolicies::POLICY_ADMIN)
->setName($name)
->setUsername($username)
->setSecretID($secret_id);
$credential->setPHID($credential->generatePHID());
queryfx(
$credential->establishConnection('w'),
'INSERT INTO %T (name, credentialType, providesType, viewPolicy,
editPolicy, description, username, secretID, isDestroyed,
phid, dateCreated, dateModified)
VALUES (%s, %s, %s, %s, %s, %s, %s, %d, %d, %s, %d, %d)',
$credential->getTableName(),
$credential->getName(),
$credential->getCredentialType(),
$credential->getProvidesType(),
$credential->getViewPolicy(),
$credential->getEditPolicy(),
$credential->getDescription(),
$credential->getUsername(),
$credential->getSecretID(),
$credential->getIsDestroyed(),
$credential->getPHID(),
time(),
time());
foreach ($repositories as $repository) {
queryfx(
$conn_w,
'UPDATE %T SET credentialPHID = %s WHERE id = %d',
$table->getTableName(),
$credential->getPHID(),
$repository->getID());
$edge_type = PhabricatorObjectUsesCredentialsEdgeType::EDGECONST;
id(new PhabricatorEdgeEditor())
->addEdge($repository->getPHID(), $edge_type, $credential->getPHID())
->save();
}
}
}
}
$table->saveTransaction();
$passphrase->saveTransaction();
-echo "Done.\n";
+echo pht('Done.')."\n";
diff --git a/resources/sql/patches/20131205.buildstepordermig.php b/resources/sql/patches/20131205.buildstepordermig.php
index b17724252..2216fe50b 100644
--- a/resources/sql/patches/20131205.buildstepordermig.php
+++ b/resources/sql/patches/20131205.buildstepordermig.php
@@ -1,41 +1,41 @@
<?php
$table = new HarbormasterBuildPlan();
$conn_w = $table->establishConnection('w');
$viewer = PhabricatorUser::getOmnipotentUser();
// Since HarbormasterBuildStepQuery has been updated to handle the
// correct order, we can't use the built in database access.
foreach (new LiskMigrationIterator($table) as $plan) {
$planname = $plan->getName();
- echo "Migrating steps in {$planname}...\n";
+ echo pht('Migrating steps in %s...', $planname)."\n";
$rows = queryfx_all(
$conn_w,
'SELECT id, sequence FROM harbormaster_buildstep '.
'WHERE buildPlanPHID = %s '.
'ORDER BY id ASC',
$plan->getPHID());
$sequence = 1;
foreach ($rows as $row) {
$id = $row['id'];
$existing = $row['sequence'];
if ($existing != 0) {
- echo " - {$id} (already migrated)...\n";
+ echo " - ".pht('%d (already migrated)...', $id)."\n";
continue;
}
- echo " - {$id} to position {$sequence}...\n";
+ echo " - ".pht('%d to position %s...', $id, $sequence)."\n";
queryfx(
$conn_w,
'UPDATE harbormaster_buildstep '.
'SET sequence = %d '.
'WHERE id = %d',
$sequence,
$id);
$sequence++;
}
}
-echo "Done.\n";
+echo pht('Done.')."\n";
diff --git a/resources/sql/patches/20131217.pushlogphid.2.mig.php b/resources/sql/patches/20131217.pushlogphid.2.mig.php
index edda8d346..5b253e228 100644
--- a/resources/sql/patches/20131217.pushlogphid.2.mig.php
+++ b/resources/sql/patches/20131217.pushlogphid.2.mig.php
@@ -1,20 +1,20 @@
<?php
$table = new PhabricatorRepositoryPushLog();
$conn_w = $table->establishConnection('w');
-echo "Assigning PHIDs to push logs...\n";
+echo pht('Assigning PHIDs to push logs...')."\n";
$logs = new LiskMigrationIterator($table);
foreach ($logs as $log) {
$id = $log->getID();
- echo "Updating {$id}...\n";
+ echo pht('Updating %s...', $id)."\n";
queryfx(
$conn_w,
'UPDATE %T SET phid = %s WHERE id = %d',
$table->getTableName(),
$log->generatePHID(),
$id);
}
-echo "Done.\n";
+echo pht('Done.')."\n";
diff --git a/resources/sql/patches/emailtableport.php b/resources/sql/patches/emailtableport.php
index ba46e7091..d70f3341f 100644
--- a/resources/sql/patches/emailtableport.php
+++ b/resources/sql/patches/emailtableport.php
@@ -1,34 +1,34 @@
<?php
-echo "Migrating user emails...\n";
+echo pht('Migrating user emails...')."\n";
$table = new PhabricatorUser();
$table->openTransaction();
$conn = $table->establishConnection('w');
$emails = queryfx_all(
$conn,
'SELECT phid, email FROM %T LOCK IN SHARE MODE',
$table->getTableName());
$emails = ipull($emails, 'email', 'phid');
$etable = new PhabricatorUserEmail();
foreach ($emails as $phid => $email) {
// NOTE: Grandfather all existing email in as primary / verified. We generate
// verification codes because they are used for password resets, etc.
- echo "Migrating '{$phid}'...\n";
+ echo pht("Migrating '%s'...", $phid)."\n";
queryfx(
$conn,
'INSERT INTO %T (userPHID, address, verificationCode, isVerified, isPrimary)
VALUES (%s, %s, %s, 1, 1)',
$etable->getTableName(),
$phid,
$email,
Filesystem::readRandomCharacters(24));
}
$table->saveTransaction();
-echo "Done.\n";
+echo pht('Done.')."\n";
diff --git a/resources/sql/patches/legalpad-mailkey-populate.php b/resources/sql/patches/legalpad-mailkey-populate.php
index e754ab275..8aa2c593d 100644
--- a/resources/sql/patches/legalpad-mailkey-populate.php
+++ b/resources/sql/patches/legalpad-mailkey-populate.php
@@ -1,25 +1,25 @@
<?php
-echo "Populating Legalpad Documents with mail keys...\n";
+echo pht('Populating Legalpad Documents with mail keys...')."\n";
$table = new LegalpadDocument();
$table->openTransaction();
foreach (new LiskMigrationIterator($table) as $document) {
$id = $document->getID();
- echo "Document {$id}: ";
+ echo pht('Document %s: ', $id);
if (!$document->getMailKey()) {
queryfx(
$document->establishConnection('w'),
'UPDATE %T SET mailKey = %s WHERE id = %d',
$document->getTableName(),
Filesystem::readRandomCharacters(20),
$id);
- echo "Generated Key\n";
+ echo pht('Generated Key')."\n";
} else {
echo "-\n";
}
}
$table->saveTransaction();
-echo "Done.\n";
+echo pht('Done.')."\n";
diff --git a/resources/sql/patches/liskcounters.php b/resources/sql/patches/liskcounters.php
index 489747575..198ed1374 100644
--- a/resources/sql/patches/liskcounters.php
+++ b/resources/sql/patches/liskcounters.php
@@ -1,40 +1,40 @@
<?php
-// Switch PhabricatorWorkerActiveTask from autoincrement IDs to counter IDs.
+// Switch PhabricatorWorkerActiveTask from auto-increment IDs to counter IDs.
// Set the initial counter ID to be larger than any known task ID.
$active_table = new PhabricatorWorkerActiveTask();
$archive_table = new PhabricatorWorkerArchiveTask();
$old_table = 'worker_task';
$conn_w = $active_table->establishConnection('w');
$active_auto = head(queryfx_one(
$conn_w,
'SELECT auto_increment FROM information_schema.tables
WHERE table_name = %s
AND table_schema = DATABASE()',
$old_table));
$active_max = head(queryfx_one(
$conn_w,
'SELECT MAX(id) FROM %T',
$old_table));
$archive_max = head(queryfx_one(
$conn_w,
'SELECT MAX(id) FROM %T',
$archive_table->getTableName()));
$initial_counter = max((int)$active_auto, (int)$active_max, (int)$archive_max);
queryfx(
$conn_w,
'INSERT INTO %T (counterName, counterValue)
VALUES (%s, %d)
ON DUPLICATE KEY UPDATE counterValue = %d',
LiskDAO::COUNTER_TABLE_NAME,
$old_table,
$initial_counter + 1,
$initial_counter + 1);
diff --git a/resources/sql/patches/migrate-differential-dependencies.php b/resources/sql/patches/migrate-differential-dependencies.php
index e84a7755a..c65165f9e 100644
--- a/resources/sql/patches/migrate-differential-dependencies.php
+++ b/resources/sql/patches/migrate-differential-dependencies.php
@@ -1,29 +1,29 @@
<?php
-echo "Migrating differential dependencies to edges...\n";
+echo pht('Migrating differential dependencies to edges...')."\n";
$table = new DifferentialRevision();
$table->openTransaction();
foreach (new LiskMigrationIterator($table) as $rev) {
$id = $rev->getID();
- echo "Revision {$id}: ";
+ echo pht('Revision %d: ', $id);
$deps = $rev->getAttachedPHIDs(DifferentialRevisionPHIDType::TYPECONST);
if (!$deps) {
echo "-\n";
continue;
}
$editor = new PhabricatorEdgeEditor();
foreach ($deps as $dep) {
$editor->addEdge(
$rev->getPHID(),
DifferentialRevisionDependsOnRevisionEdgeType::EDGECONST,
$dep);
}
$editor->save();
- echo "OKAY\n";
+ echo pht('OKAY')."\n";
}
$table->saveTransaction();
-echo "Done.\n";
+echo pht('Done.')."\n";
diff --git a/resources/sql/patches/migrate-maniphest-dependencies.php b/resources/sql/patches/migrate-maniphest-dependencies.php
index 394c98a95..074018264 100644
--- a/resources/sql/patches/migrate-maniphest-dependencies.php
+++ b/resources/sql/patches/migrate-maniphest-dependencies.php
@@ -1,29 +1,29 @@
<?php
-echo "Migrating task dependencies to edges...\n";
+echo pht('Migrating task dependencies to edges...')."\n";
$table = new ManiphestTask();
$table->openTransaction();
foreach (new LiskMigrationIterator($table) as $task) {
$id = $task->getID();
- echo "Task {$id}: ";
+ echo pht('Task %d: ', $id);
$deps = $task->getAttachedPHIDs(ManiphestTaskPHIDType::TYPECONST);
if (!$deps) {
echo "-\n";
continue;
}
$editor = new PhabricatorEdgeEditor();
foreach ($deps as $dep) {
$editor->addEdge(
$task->getPHID(),
ManiphestTaskDependsOnTaskEdgeType::EDGECONST,
$dep);
}
$editor->save();
- echo "OKAY\n";
+ echo pht('OKAY')."\n";
}
$table->saveTransaction();
-echo "Done.\n";
+echo pht('Done.')."\n";
diff --git a/resources/sql/patches/migrate-maniphest-revisions.php b/resources/sql/patches/migrate-maniphest-revisions.php
index 0658cfda6..2a8f8061b 100644
--- a/resources/sql/patches/migrate-maniphest-revisions.php
+++ b/resources/sql/patches/migrate-maniphest-revisions.php
@@ -1,28 +1,28 @@
<?php
-echo "Migrating task revisions to edges...\n";
+echo pht('Migrating task revisions to edges...')."\n";
$table = new ManiphestTask();
$table->establishConnection('w');
foreach (new LiskMigrationIterator($table) as $task) {
$id = $task->getID();
- echo "Task {$id}: ";
+ echo pht('Task %d: ', $id);
$revs = $task->getAttachedPHIDs(DifferentialRevisionPHIDType::TYPECONST);
if (!$revs) {
echo "-\n";
continue;
}
$editor = new PhabricatorEdgeEditor();
foreach ($revs as $rev) {
$editor->addEdge(
$task->getPHID(),
ManiphestTaskHasRevisionEdgeType::EDGECONST,
$rev);
}
$editor->save();
- echo "OKAY\n";
+ echo pht('OKAY')."\n";
}
-echo "Done.\n";
+echo pht('Done.')."\n";
diff --git a/resources/sql/patches/migrate-project-edges.php b/resources/sql/patches/migrate-project-edges.php
index 855b52b14..277cf8b15 100644
--- a/resources/sql/patches/migrate-project-edges.php
+++ b/resources/sql/patches/migrate-project-edges.php
@@ -1,35 +1,35 @@
<?php
-echo "Migrating project members to edges...\n";
+echo pht('Migrating project members to edges...')."\n";
$table = new PhabricatorProject();
$table->establishConnection('w');
foreach (new LiskMigrationIterator($table) as $proj) {
$id = $proj->getID();
- echo "Project {$id}: ";
+ echo pht('Project %d: ', $id);
$members = queryfx_all(
$proj->establishConnection('w'),
'SELECT userPHID FROM %T WHERE projectPHID = %s',
'project_affiliation',
$proj->getPHID());
if (!$members) {
echo "-\n";
continue;
}
$members = ipull($members, 'userPHID');
$editor = new PhabricatorEdgeEditor();
foreach ($members as $user_phid) {
$editor->addEdge(
$proj->getPHID(),
PhabricatorProjectProjectHasMemberEdgeType::EDGECONST,
$user_phid);
}
$editor->save();
- echo "OKAY\n";
+ echo pht('OKAY')."\n";
}
-echo "Done.\n";
+echo pht('Done.')."\n";
diff --git a/resources/sql/patches/ponder-mailkey-populate.php b/resources/sql/patches/ponder-mailkey-populate.php
index bdae8f83a..5d2c77fff 100644
--- a/resources/sql/patches/ponder-mailkey-populate.php
+++ b/resources/sql/patches/ponder-mailkey-populate.php
@@ -1,25 +1,25 @@
<?php
-echo "Populating Questions with mail keys...\n";
+echo pht('Populating Questions with mail keys...')."\n";
$table = new PonderQuestion();
$table->openTransaction();
foreach (new LiskMigrationIterator($table) as $question) {
$id = $question->getID();
- echo "Question {$id}: ";
+ echo pht('Question %d: ', $id);
if (!$question->getMailKey()) {
queryfx(
$question->establishConnection('w'),
'UPDATE %T SET mailKey = %s WHERE id = %d',
$question->getTableName(),
Filesystem::readRandomCharacters(20),
$id);
- echo "Generated Key\n";
+ echo pht('Generated Key')."\n";
} else {
echo "-\n";
}
}
$table->saveTransaction();
-echo "Done.\n";
+echo pht('Done.')."\n";
diff --git a/scripts/__init_script__.php b/scripts/__init_script__.php
index c0a198a14..57ada96e6 100644
--- a/scripts/__init_script__.php
+++ b/scripts/__init_script__.php
@@ -1,24 +1,24 @@
<?php
function init_phabricator_script() {
error_reporting(E_ALL | E_STRICT);
ini_set('display_errors', 1);
$include_path = ini_get('include_path');
ini_set(
'include_path',
$include_path.PATH_SEPARATOR.dirname(__FILE__).'/../../');
@include_once 'libphutil/scripts/__init_script__.php';
if (!@constant('__LIBPHUTIL__')) {
echo "ERROR: Unable to load libphutil. Update your PHP 'include_path' to ".
- "include the parent directory of libphutil/.\n";
+ "include the parent directory of libphutil/.\n";
exit(1);
}
phutil_load_library('arcanist/src');
phutil_load_library(dirname(__FILE__).'/../src/');
PhabricatorEnv::initializeScriptEnvironment();
}
init_phabricator_script();
diff --git a/scripts/almanac/manage_almanac.php b/scripts/almanac/manage_almanac.php
index e18b67d74..933d1b722 100755
--- a/scripts/almanac/manage_almanac.php
+++ b/scripts/almanac/manage_almanac.php
@@ -1,21 +1,21 @@
#!/usr/bin/env php
<?php
$root = dirname(dirname(dirname(__FILE__)));
require_once $root.'/scripts/__init_script__.php';
$args = new PhutilArgumentParser($argv);
-$args->setTagline('manage host directory');
+$args->setTagline(pht('manage host directory'));
$args->setSynopsis(<<<EOSYNOPSIS
**almanac** __commmand__ [__options__]
Manage Almanac stuff. NEW AND EXPERIMENTAL.
EOSYNOPSIS
);
$args->parseStandardArguments();
$workflows = id(new PhutilSymbolLoader())
->setAncestorClass('AlmanacManagementWorkflow')
->loadObjects();
$workflows[] = new PhutilHelpArgumentWorkflow();
$args->parseWorkflows($workflows);
diff --git a/scripts/aphront/aphrontpath.php b/scripts/aphront/aphrontpath.php
index 04a051212..09cc04eee 100755
--- a/scripts/aphront/aphrontpath.php
+++ b/scripts/aphront/aphrontpath.php
@@ -1,26 +1,28 @@
#!/usr/bin/env php
<?php
$root = dirname(dirname(dirname(__FILE__)));
require_once $root.'/scripts/__init_script__.php';
if ($argc !== 2 || $argv[1] === '--help') {
- echo "Usage: aphrontpath.php <url>\n";
- echo "Purpose: Print controller which will process passed <url>.\n";
+ echo pht('Usage: %s', 'aphrontpath.php <url>')."\n";
+ echo pht(
+ "Purpose: Print controller which will process passed %s.\n",
+ '<url>');
exit(1);
}
$url = parse_url($argv[1]);
$path = '/'.(isset($url['path']) ? ltrim($url['path'], '/') : '');
$config_key = 'aphront.default-application-configuration-class';
$application = PhabricatorEnv::newObjectFromConfig($config_key);
$application->setRequest(new AphrontRequest('', $path));
list($controller) = $application->buildControllerForPath($path);
if (!$controller && substr($path, -1) !== '/') {
list($controller) = $application->buildControllerForPath($path.'/');
}
if ($controller) {
echo get_class($controller)."\n";
}
diff --git a/scripts/cache/manage_cache.php b/scripts/cache/manage_cache.php
index c50efca60..80e8e8081 100755
--- a/scripts/cache/manage_cache.php
+++ b/scripts/cache/manage_cache.php
@@ -1,21 +1,21 @@
#!/usr/bin/env php
<?php
$root = dirname(dirname(dirname(__FILE__)));
require_once $root.'/scripts/__init_script__.php';
$args = new PhutilArgumentParser($argv);
-$args->setTagline('manage cache');
+$args->setTagline(pht('manage cache'));
$args->setSynopsis(<<<EOSYNOPSIS
**cache** __command__ [__options__]
Manage Phabricator caches.
EOSYNOPSIS
);
$args->parseStandardArguments();
$workflows = id(new PhutilSymbolLoader())
->setAncestorClass('PhabricatorCacheManagementWorkflow')
->loadObjects();
$workflows[] = new PhutilHelpArgumentWorkflow();
$args->parseWorkflows($workflows);
diff --git a/scripts/celerity/generate_sprites.php b/scripts/celerity/generate_sprites.php
index 31ad4684c..836167cfa 100755
--- a/scripts/celerity/generate_sprites.php
+++ b/scripts/celerity/generate_sprites.php
@@ -1,84 +1,86 @@
#!/usr/bin/env php
<?php
require_once dirname(dirname(__FILE__)).'/__init_script__.php';
$args = new PhutilArgumentParser($argv);
-$args->setTagline('regenerate CSS sprite sheets');
+$args->setTagline(pht('regenerate CSS sprite sheets'));
$args->setSynopsis(<<<EOHELP
**sprites**
Rebuild CSS sprite sheets.
EOHELP
);
$args->parseStandardArguments();
$args->parse(
array(
array(
'name' => 'force',
- 'help' => 'Force regeneration even if sources have not changed.',
+ 'help' => pht('Force regeneration even if sources have not changed.'),
),
));
$root = dirname(phutil_get_library_root('phabricator'));
$webroot = $root.'/webroot/rsrc';
$webroot = Filesystem::readablePath($webroot);
$generator = new CeleritySpriteGenerator();
$sheets = array(
'menu' => $generator->buildMenuSheet(),
'tokens' => $generator->buildTokenSheet(),
'gradient' => $generator->buildGradientSheet(),
'main-header' => $generator->buildMainHeaderSheet(),
'login' => $generator->buildLoginSheet(),
'projects' => $generator->buildProjectsSheet(),
);
list($err) = exec_manual('optipng');
if ($err) {
$have_optipng = false;
echo phutil_console_format(
- "<bg:red> WARNING </bg> `optipng` not found in PATH.\n".
- "Sprites will not be optimized! Install `optipng`!\n");
+ "<bg:red> %s </bg> %s\n%s\n",
+ pht('WARNING'),
+ pht('`%s` not found in PATH.', 'optipng'),
+ pht('Sprites will not be optimized! Install `%s`!', 'optipng'));
} else {
$have_optipng = true;
}
foreach ($sheets as $name => $sheet) {
$sheet->setBasePath($root);
$manifest_path = $root.'/resources/sprite/manifest/'.$name.'.json';
if (!$args->getArg('force')) {
if (Filesystem::pathExists($manifest_path)) {
$data = Filesystem::readFile($manifest_path);
$data = phutil_json_decode($data);
if (!$sheet->needsRegeneration($data)) {
continue;
}
}
}
$sheet
->generateCSS($webroot."/css/sprite-{$name}.css")
->generateManifest($root."/resources/sprite/manifest/{$name}.json");
foreach ($sheet->getScales() as $scale) {
if ($scale == 1) {
$sheet_name = "sprite-{$name}.png";
} else {
$sheet_name = "sprite-{$name}-X{$scale}.png";
}
$full_path = "{$webroot}/image/{$sheet_name}";
$sheet->generateImage($full_path, $scale);
if ($have_optipng) {
- echo "Optimizing...\n";
+ echo pht('Optimizing...')."\n";
phutil_passthru('optipng -o7 -clobber %s', $full_path);
}
}
}
-echo "Done.\n";
+echo pht('Done.')."\n";
diff --git a/scripts/daemon/manage_daemons.php b/scripts/daemon/manage_daemons.php
index 2d4b58dbb..087c925b8 100755
--- a/scripts/daemon/manage_daemons.php
+++ b/scripts/daemon/manage_daemons.php
@@ -1,23 +1,23 @@
#!/usr/bin/env php
<?php
$root = dirname(dirname(dirname(__FILE__)));
require_once $root.'/scripts/__init_script__.php';
PhabricatorDaemonManagementWorkflow::requireExtensions();
$args = new PhutilArgumentParser($argv);
-$args->setTagline('manage daemons');
+$args->setTagline(pht('manage daemons'));
$args->setSynopsis(<<<EOSYNOPSIS
**phd** __command__ [__options__]
Manage Phabricator daemons.
EOSYNOPSIS
);
$args->parseStandardArguments();
$workflows = id(new PhutilSymbolLoader())
->setAncestorClass('PhabricatorDaemonManagementWorkflow')
->loadObjects();
$workflows[] = new PhutilHelpArgumentWorkflow();
$args->parseWorkflows($workflows);
diff --git a/scripts/diviner/diviner.php b/scripts/diviner/diviner.php
index fe9c4e1de..03d15274f 100755
--- a/scripts/diviner/diviner.php
+++ b/scripts/diviner/diviner.php
@@ -1,21 +1,21 @@
#!/usr/bin/env php
<?php
$root = dirname(dirname(dirname(__FILE__)));
require_once $root.'/scripts/__init_script__.php';
$args = new PhutilArgumentParser($argv);
-$args->setTagline('documentation generator');
+$args->setTagline(pht('documentation generator'));
$args->setSynopsis(<<<EOHELP
**diviner** __command__ [__options__]
Generate documentation.
EOHELP
);
$args->parseStandardArguments();
$workflows = id(new PhutilSymbolLoader())
->setAncestorClass('DivinerWorkflow')
->loadObjects();
$workflows[] = new PhutilHelpArgumentWorkflow();
$args->parseWorkflows($workflows);
diff --git a/scripts/drydock/drydock_control.php b/scripts/drydock/drydock_control.php
index 0a01b91fa..57ad87758 100755
--- a/scripts/drydock/drydock_control.php
+++ b/scripts/drydock/drydock_control.php
@@ -1,21 +1,21 @@
#!/usr/bin/env php
<?php
$root = dirname(dirname(dirname(__FILE__)));
require_once $root.'/scripts/__init_script__.php';
$args = new PhutilArgumentParser($argv);
-$args->setTagline('manage drydock software resources');
+$args->setTagline(pht('manage drydock software resources'));
$args->setSynopsis(<<<EOSYNOPSIS
**drydock** __commmand__ [__options__]
Manage Drydock stuff. NEW AND EXPERIMENTAL.
EOSYNOPSIS
);
$args->parseStandardArguments();
$workflows = id(new PhutilSymbolLoader())
->setAncestorClass('DrydockManagementWorkflow')
->loadObjects();
$workflows[] = new PhutilHelpArgumentWorkflow();
$args->parseWorkflows($workflows);
diff --git a/scripts/fact/manage_facts.php b/scripts/fact/manage_facts.php
index 973e5bfa2..8a351dd1a 100755
--- a/scripts/fact/manage_facts.php
+++ b/scripts/fact/manage_facts.php
@@ -1,22 +1,22 @@
#!/usr/bin/env php
<?php
$root = dirname(dirname(dirname(__FILE__)));
require_once $root.'/scripts/__init_script__.php';
$args = new PhutilArgumentParser($argv);
-$args->setTagline('manage fact configuration');
+$args->setTagline(pht('manage fact configuration'));
$args->setSynopsis(<<<EOSYNOPSIS
**fact** __command__ [__options__]
Manage and debug Phabricator data extraction, storage and
configuration used to compute statistics.
EOSYNOPSIS
);
$args->parseStandardArguments();
$workflows = id(new PhutilSymbolLoader())
->setAncestorClass('PhabricatorFactManagementWorkflow')
->loadObjects();
$workflows[] = new PhutilHelpArgumentWorkflow();
$args->parseWorkflows($workflows);
diff --git a/scripts/files/manage_files.php b/scripts/files/manage_files.php
index 661d65ea3..edd947467 100755
--- a/scripts/files/manage_files.php
+++ b/scripts/files/manage_files.php
@@ -1,21 +1,21 @@
#!/usr/bin/env php
<?php
$root = dirname(dirname(dirname(__FILE__)));
require_once $root.'/scripts/__init_script__.php';
$args = new PhutilArgumentParser($argv);
-$args->setTagline('manage files');
+$args->setTagline(pht('manage files'));
$args->setSynopsis(<<<EOSYNOPSIS
**files** __command__ [__options__]
Manage Phabricator file storage.
EOSYNOPSIS
);
$args->parseStandardArguments();
$workflows = id(new PhutilSymbolLoader())
->setAncestorClass('PhabricatorFilesManagementWorkflow')
->loadObjects();
$workflows[] = new PhutilHelpArgumentWorkflow();
$args->parseWorkflows($workflows);
diff --git a/scripts/lipsum/manage_lipsum.php b/scripts/lipsum/manage_lipsum.php
index 3a7c0a23a..610e6e9aa 100755
--- a/scripts/lipsum/manage_lipsum.php
+++ b/scripts/lipsum/manage_lipsum.php
@@ -1,21 +1,21 @@
#!/usr/bin/env php
<?php
$root = dirname(dirname(dirname(__FILE__)));
require_once $root.'/scripts/__init_script__.php';
$args = new PhutilArgumentParser($argv);
-$args->setTagline('manage lipsum');
+$args->setTagline(pht('manage lipsum'));
$args->setSynopsis(<<<EOSYNOPSIS
**lipsum** __command__ [__options__]
Manage Phabricator Test Data Generator.
EOSYNOPSIS
);
$args->parseStandardArguments();
$workflows = id(new PhutilSymbolLoader())
->setAncestorClass('PhabricatorLipsumManagementWorkflow')
->loadObjects();
$workflows[] = new PhutilHelpArgumentWorkflow();
$args->parseWorkflows($workflows);
diff --git a/scripts/mail/mail_handler.php b/scripts/mail/mail_handler.php
index 0341a0e46..2ff23adb0 100755
--- a/scripts/mail/mail_handler.php
+++ b/scripts/mail/mail_handler.php
@@ -1,95 +1,95 @@
#!/usr/bin/env php
<?php
// NOTE: This script is very oldschool and takes the environment as an argument.
// Some day, we could take a shot at cleaning this up.
if ($argc > 1) {
foreach (array_slice($argv, 1) as $arg) {
if (!preg_match('/^-/', $arg)) {
$_SERVER['PHABRICATOR_ENV'] = $arg;
break;
}
}
}
$root = dirname(dirname(dirname(__FILE__)));
require_once $root.'/scripts/__init_script__.php';
require_once $root.'/externals/mimemailparser/MimeMailParser.class.php';
$args = new PhutilArgumentParser($argv);
$args->parseStandardArguments();
$args->parse(
array(
array(
'name' => 'process-duplicates',
'help' => pht(
"Process this message, even if it's a duplicate of another message. ".
"This is mostly useful when debugging issues with mail routing."),
),
array(
'name' => 'env',
'wildcard' => true,
),
));
$parser = new MimeMailParser();
$parser->setText(file_get_contents('php://stdin'));
$text_body = $parser->getMessageBody('text');
$text_body_headers = $parser->getMessageBodyHeaders('text');
$content_type = idx($text_body_headers, 'content-type');
if (
!phutil_is_utf8($text_body) &&
(preg_match('/charset="(.*?)"/', $content_type, $matches) ||
preg_match('/charset=(\S+)/', $content_type, $matches))
) {
$text_body = phutil_utf8_convert($text_body, 'UTF-8', $matches[1]);
}
$headers = $parser->getHeaders();
$headers['subject'] = iconv_mime_decode($headers['subject'], 0, 'UTF-8');
$headers['from'] = iconv_mime_decode($headers['from'], 0, 'UTF-8');
if ($args->getArg('process-duplicates')) {
$headers['message-id'] = Filesystem::readRandomCharacters(64);
}
$received = new PhabricatorMetaMTAReceivedMail();
$received->setHeaders($headers);
$received->setBodies(array(
'text' => $text_body,
'html' => $parser->getMessageBody('html'),
));
$attachments = array();
foreach ($parser->getAttachments() as $attachment) {
if (preg_match('@text/(plain|html)@', $attachment->getContentType()) &&
$attachment->getContentDisposition() == 'inline') {
// If this is an "inline" attachment with some sort of text content-type,
// do not treat it as a file for attachment. MimeMailParser already picked
// it up in the getMessageBody() call above. We still want to treat 'inline'
// attachments with other content types (e.g., images) as attachments.
continue;
}
$file = PhabricatorFile::newFromFileData(
$attachment->getContent(),
array(
'name' => $attachment->getFilename(),
'viewPolicy' => PhabricatorPolicies::POLICY_NOONE,
));
$attachments[] = $file->getPHID();
}
try {
$received->setAttachments($attachments);
$received->save();
$received->processReceivedMail();
} catch (Exception $e) {
$received
- ->setMessage('EXCEPTION: '.$e->getMessage())
+ ->setMessage(pht('EXCEPTION: %s', $e->getMessage()))
->save();
throw $e;
}
diff --git a/scripts/mail/manage_mail.php b/scripts/mail/manage_mail.php
index 982256517..7f73303fd 100755
--- a/scripts/mail/manage_mail.php
+++ b/scripts/mail/manage_mail.php
@@ -1,21 +1,21 @@
#!/usr/bin/env php
<?php
$root = dirname(dirname(dirname(__FILE__)));
require_once $root.'/scripts/__init_script__.php';
$args = new PhutilArgumentParser($argv);
-$args->setTagline('manage mail');
+$args->setTagline(pht('manage mail'));
$args->setSynopsis(<<<EOSYNOPSIS
**mail** __command__ [__options__]
Manage Phabricator mail stuff.
EOSYNOPSIS
);
$args->parseStandardArguments();
$workflows = id(new PhutilSymbolLoader())
->setAncestorClass('PhabricatorMailManagementWorkflow')
->loadObjects();
$workflows[] = new PhutilHelpArgumentWorkflow();
$args->parseWorkflows($workflows);
diff --git a/scripts/repository/commit_hook.php b/scripts/repository/commit_hook.php
index 6d756dddd..56e828ab7 100755
--- a/scripts/repository/commit_hook.php
+++ b/scripts/repository/commit_hook.php
@@ -1,169 +1,171 @@
#!/usr/bin/env php
<?php
// NOTE: This script will sometimes emit a warning like this on startup:
//
// No entry for terminal type "unknown";
// using dumb terminal settings.
//
// This can be fixed by adding "TERM=dumb" to the shebang line, but doing so
// causes some systems to hang mysteriously. See T7119.
// Commit hooks execute in an unusual context where the environment may be
// unavailable, particularly in SVN. The first parameter to this script is
// either a bare repository identifier ("X"), or a repository identifier
// followed by an instance identifier ("X:instance"). If we have an instance
// identifier, unpack it into the environment before we start up. This allows
// subclasses of PhabricatorConfigSiteSource to read it and build an instance
// environment.
if ($argc > 1) {
$context = $argv[1];
$context = explode(':', $context, 2);
$argv[1] = $context[0];
if (count($context) > 1) {
$_ENV['PHABRICATOR_INSTANCE'] = $context[1];
putenv('PHABRICATOR_INSTANCE='.$context[1]);
}
}
$root = dirname(dirname(dirname(__FILE__)));
require_once $root.'/scripts/__init_script__.php';
if ($argc < 2) {
throw new Exception(pht('usage: commit-hook <callsign>'));
}
$engine = new DiffusionCommitHookEngine();
$repository = id(new PhabricatorRepositoryQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withCallsigns(array($argv[1]))
->needProjectPHIDs(true)
->executeOne();
if (!$repository) {
throw new Exception(pht('No such repository "%s"!', $argv[1]));
}
if (!$repository->isHosted()) {
// This should be redundant, but double check just in case.
throw new Exception(pht('Repository "%s" is not hosted!', $argv[1]));
}
$engine->setRepository($repository);
// Figure out which user is writing the commit.
if ($repository->isGit() || $repository->isHg()) {
$username = getenv(DiffusionCommitHookEngine::ENV_USER);
if (!strlen($username)) {
throw new Exception(
- pht('usage: %s should be defined!', DiffusionCommitHookEngine::ENV_USER));
+ pht(
+ 'Usage: %s should be defined!',
+ DiffusionCommitHookEngine::ENV_USER));
}
if ($repository->isHg()) {
// We respond to several different hooks in Mercurial.
$engine->setMercurialHook($argv[2]);
}
} else if ($repository->isSVN()) {
// NOTE: In Subversion, the entire environment gets wiped so we can't read
// DiffusionCommitHookEngine::ENV_USER. Instead, we've set "--tunnel-user" to
// specify the correct user; read this user out of the commit log.
if ($argc < 4) {
throw new Exception(pht('usage: commit-hook <callsign> <repo> <txn>'));
}
$svn_repo = $argv[2];
$svn_txn = $argv[3];
list($username) = execx('svnlook author -t %s %s', $svn_txn, $svn_repo);
$username = rtrim($username, "\n");
$engine->setSubversionTransactionInfo($svn_txn, $svn_repo);
} else {
throw new Exception(pht('Unknown repository type.'));
}
$user = id(new PhabricatorPeopleQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withUsernames(array($username))
->executeOne();
if (!$user) {
throw new Exception(pht('No such user "%s"!', $username));
}
$engine->setViewer($user);
// Read stdin for the hook engine.
if ($repository->isHg()) {
// Mercurial leaves stdin open, so we can't just read it until EOF.
$stdin = '';
} else {
// Git and Subversion write data into stdin and then close it. Read the
// data.
$stdin = @file_get_contents('php://stdin');
if ($stdin === false) {
throw new Exception(pht('Failed to read stdin!'));
}
}
$engine->setStdin($stdin);
$engine->setOriginalArgv(array_slice($argv, 2));
$remote_address = getenv(DiffusionCommitHookEngine::ENV_REMOTE_ADDRESS);
if (strlen($remote_address)) {
$engine->setRemoteAddress($remote_address);
}
$remote_protocol = getenv(DiffusionCommitHookEngine::ENV_REMOTE_PROTOCOL);
if (strlen($remote_protocol)) {
$engine->setRemoteProtocol($remote_protocol);
}
try {
$err = $engine->execute();
} catch (DiffusionCommitHookRejectException $ex) {
$console = PhutilConsole::getConsole();
if (PhabricatorEnv::getEnvConfig('phabricator.serious-business')) {
$preamble = pht('*** PUSH REJECTED BY COMMIT HOOK ***');
} else {
$preamble = pht(<<<EOTXT
+---------------------------------------------------------------+
| * * * PUSH REJECTED BY EVIL DRAGON BUREAUCRATS * * * |
+---------------------------------------------------------------+
\
\ ^ /^
\ / \ // \
\ |\___/| / \// .\
\ /V V \__ / // | \ \ *----*
/ / \/_/ // | \ \ \ |
@___@` \/_ // | \ \ \/\ \
0/0/| \/_ // | \ \ \ \
0/0/0/0/| \/// | \ \ | |
0/0/0/0/0/_|_ / ( // | \ _\ | /
0/0/0/0/0/0/`/,_ _ _/ ) ; -. | _ _\.-~ / /
,-} _ *-.|.-~-. .~ ~
\ \__/ `/\ / ~-. _ .-~ /
\____(Oo) *. } { /
( (--) .----~-.\ \-` .~
//__\\\\ \ DENIED! ///.----..< \ _ -~
// \\\\ ///-._ _ _ _ _ _ _{^ - - - - ~
EOTXT
);
}
$console->writeErr("%s\n\n", $preamble);
$console->writeErr("%s\n\n", $ex->getMessage());
$err = 1;
}
exit($err);
diff --git a/scripts/repository/manage_repositories.php b/scripts/repository/manage_repositories.php
index b3ebac084..ae0395e66 100755
--- a/scripts/repository/manage_repositories.php
+++ b/scripts/repository/manage_repositories.php
@@ -1,22 +1,22 @@
#!/usr/bin/env php
<?php
$root = dirname(dirname(dirname(__FILE__)));
require_once $root.'/scripts/__init_script__.php';
$args = new PhutilArgumentParser($argv);
-$args->setTagline('manage repositories');
+$args->setTagline(pht('manage repositories'));
$args->setSynopsis(<<<EOSYNOPSIS
**repository** __command__ [__options__]
Manage and debug Phabricator repository configuration, tracking,
discovery and import.
EOSYNOPSIS
);
$args->parseStandardArguments();
$workflows = id(new PhutilSymbolLoader())
->setAncestorClass('PhabricatorRepositoryManagementWorkflow')
->loadObjects();
$workflows[] = new PhutilHelpArgumentWorkflow();
$args->parseWorkflows($workflows);
diff --git a/scripts/repository/rebuild_summaries.php b/scripts/repository/rebuild_summaries.php
index a98a6f01b..de3d5acca 100755
--- a/scripts/repository/rebuild_summaries.php
+++ b/scripts/repository/rebuild_summaries.php
@@ -1,53 +1,53 @@
#!/usr/bin/env php
<?php
$root = dirname(dirname(dirname(__FILE__)));
require_once $root.'/scripts/__init_script__.php';
$commit = new PhabricatorRepositoryCommit();
$conn_w = id(new PhabricatorRepository())->establishConnection('w');
$sizes = queryfx_all(
$conn_w,
'SELECT repositoryID, count(*) N FROM %T GROUP BY repositoryID',
$commit->getTableName());
$sizes = ipull($sizes, 'N', 'repositoryID');
$maxes = queryfx_all(
$conn_w,
'SELECT repositoryID, max(epoch) maxEpoch FROM %T GROUP BY repositoryID',
$commit->getTableName());
$maxes = ipull($maxes, 'maxEpoch', 'repositoryID');
$repository_ids = array_keys($sizes + $maxes);
-echo 'Updating '.count($repository_ids).' repositories';
+echo pht('Updating %d repositories', count($repository_ids));
foreach ($repository_ids as $repository_id) {
$last_commit = queryfx_one(
$conn_w,
'SELECT id FROM %T WHERE repositoryID = %d AND epoch = %d LIMIT 1',
$commit->getTableName(),
$repository_id,
idx($maxes, $repository_id, 0));
if ($last_commit) {
$last_commit = $last_commit['id'];
} else {
$last_commit = 0;
}
queryfx(
$conn_w,
'INSERT INTO %T (repositoryID, lastCommitID, size, epoch)
VALUES (%d, %d, %d, %d) ON DUPLICATE KEY UPDATE
lastCommitID = VALUES(lastCommitID),
size = VALUES(size),
epoch = VALUES(epoch)',
PhabricatorRepository::TABLE_SUMMARY,
$repository_id,
$last_commit,
idx($sizes, $repository_id, 0),
idx($maxes, $repository_id, 0));
echo '.';
}
-echo "\ndone.\n";
+echo "\n".pht('Done.')."\n";
diff --git a/scripts/repository/save_lint.php b/scripts/repository/save_lint.php
index d9bf921e9..fde2ab8fe 100755
--- a/scripts/repository/save_lint.php
+++ b/scripts/repository/save_lint.php
@@ -1,61 +1,61 @@
#!/usr/bin/env php
<?php
require_once dirname(__FILE__).'/../__init_script__.php';
$synopsis = <<<EOT
**save_lint.php**
Discover lint problems and save them to database so that they can
be displayed in Diffusion.
EOT;
$args = id(new PhutilArgumentParser($argv))
- ->setTagline('save lint errors to database')
+ ->setTagline(pht('save lint errors to database'))
->setSynopsis($synopsis)
->parseStandardArguments()
->parse(array(
array(
'name' => 'all',
'help' => pht(
'Discover problems in the whole repository instead of just changes '.
'since the last run.'),
),
array(
'name' => 'arc',
'param' => 'path',
'default' => 'arc',
'help' => pht('Path to Arcanist executable.'),
),
array(
'name' => 'severity',
'param' => 'string',
'default' => ArcanistLintSeverity::SEVERITY_ADVICE,
'help' => pht(
'Minimum severity, one of %s constants.',
'ArcanistLintSeverity'),
),
array(
'name' => 'chunk-size',
'param' => 'number',
'default' => 256,
'help' => pht('Number of paths passed to `%s` at once.', 'arc'),
),
array(
'name' => 'blame',
'help' => pht(
'Assign lint errors to authors who last modified the line.'),
),
));
echo pht('Saving lint errors to database...')."\n";
$count = id(new DiffusionLintSaveRunner())
->setAll($args->getArg('all', false))
->setArc($args->getArg('arc'))
->setSeverity($args->getArg('severity'))
->setChunkSize($args->getArg('chunk-size'))
->setNeedsBlame($args->getArg('blame'))
->run('.');
echo "\n".pht('Processed %d files.', $count)."\n";
diff --git a/scripts/search/manage_search.php b/scripts/search/manage_search.php
index b07ff54ba..4b119f28c 100755
--- a/scripts/search/manage_search.php
+++ b/scripts/search/manage_search.php
@@ -1,21 +1,21 @@
#!/usr/bin/env php
<?php
$root = dirname(dirname(dirname(__FILE__)));
require_once $root.'/scripts/__init_script__.php';
$args = new PhutilArgumentParser($argv);
-$args->setTagline('manage search');
+$args->setTagline(pht('manage search'));
$args->setSynopsis(<<<EOSYNOPSIS
**search** __command__ [__options__]
Manage Phabricator search index.
EOSYNOPSIS
);
$args->parseStandardArguments();
$workflows = id(new PhutilSymbolLoader())
->setAncestorClass('PhabricatorSearchManagementWorkflow')
->loadObjects();
$workflows[] = new PhutilHelpArgumentWorkflow();
$args->parseWorkflows($workflows);
diff --git a/scripts/setup/manage_audit.php b/scripts/setup/manage_audit.php
index 9a085c694..26bd3044e 100755
--- a/scripts/setup/manage_audit.php
+++ b/scripts/setup/manage_audit.php
@@ -1,21 +1,21 @@
#!/usr/bin/env php
<?php
$root = dirname(dirname(dirname(__FILE__)));
require_once $root.'/scripts/__init_script__.php';
$args = new PhutilArgumentParser($argv);
-$args->setTagline('manage audits');
+$args->setTagline(pht('manage audits'));
$args->setSynopsis(<<<EOSYNOPSIS
**audit** __command__ [__options__]
Manage Phabricator audits.
EOSYNOPSIS
);
$args->parseStandardArguments();
$workflows = id(new PhutilSymbolLoader())
->setAncestorClass('PhabricatorAuditManagementWorkflow')
->loadObjects();
$workflows[] = new PhutilHelpArgumentWorkflow();
$args->parseWorkflows($workflows);
diff --git a/scripts/setup/manage_auth.php b/scripts/setup/manage_auth.php
index a674c92b0..cdd1fe377 100755
--- a/scripts/setup/manage_auth.php
+++ b/scripts/setup/manage_auth.php
@@ -1,21 +1,21 @@
#!/usr/bin/env php
<?php
$root = dirname(dirname(dirname(__FILE__)));
require_once $root.'/scripts/__init_script__.php';
$args = new PhutilArgumentParser($argv);
-$args->setTagline('manage authentication');
+$args->setTagline(pht('manage authentication'));
$args->setSynopsis(<<<EOSYNOPSIS
**auth** __command__ [__options__]
Manage Phabricator authentication configuration.
EOSYNOPSIS
);
$args->parseStandardArguments();
$workflows = id(new PhutilSymbolLoader())
->setAncestorClass('PhabricatorAuthManagementWorkflow')
->loadObjects();
$workflows[] = new PhutilHelpArgumentWorkflow();
$args->parseWorkflows($workflows);
diff --git a/scripts/setup/manage_celerity.php b/scripts/setup/manage_celerity.php
index 4a559711c..268a7abaa 100755
--- a/scripts/setup/manage_celerity.php
+++ b/scripts/setup/manage_celerity.php
@@ -1,21 +1,21 @@
#!/usr/bin/env php
<?php
$root = dirname(dirname(dirname(__FILE__)));
require_once $root.'/scripts/__init_script__.php';
$args = new PhutilArgumentParser($argv);
-$args->setTagline('manage celerity');
+$args->setTagline(pht('manage celerity'));
$args->setSynopsis(<<<EOSYNOPSIS
**celerity** __command__ [__options__]
Manage static resources.
EOSYNOPSIS
);
$args->parseStandardArguments();
$workflows = id(new PhutilSymbolLoader())
->setAncestorClass('CelerityManagementWorkflow')
->loadObjects();
$workflows[] = new PhutilHelpArgumentWorkflow();
$args->parseWorkflows($workflows);
diff --git a/scripts/setup/manage_config.php b/scripts/setup/manage_config.php
index 5c07e7293..996c851a3 100755
--- a/scripts/setup/manage_config.php
+++ b/scripts/setup/manage_config.php
@@ -1,21 +1,21 @@
#!/usr/bin/env php
<?php
$root = dirname(dirname(dirname(__FILE__)));
require_once $root.'/scripts/__init_script__.php';
$args = new PhutilArgumentParser($argv);
-$args->setTagline('manage configuration');
+$args->setTagline(pht('manage configuration'));
$args->setSynopsis(<<<EOSYNOPSIS
**config** __command__ [__options__]
Manage Phabricator configuration.
EOSYNOPSIS
);
$args->parseStandardArguments();
$workflows = id(new PhutilSymbolLoader())
->setAncestorClass('PhabricatorConfigManagementWorkflow')
->loadObjects();
$workflows[] = new PhutilHelpArgumentWorkflow();
$args->parseWorkflows($workflows);
diff --git a/scripts/setup/manage_feed.php b/scripts/setup/manage_feed.php
index 81d2cb502..0afea5d01 100755
--- a/scripts/setup/manage_feed.php
+++ b/scripts/setup/manage_feed.php
@@ -1,21 +1,21 @@
#!/usr/bin/env php
<?php
$root = dirname(dirname(dirname(__FILE__)));
require_once $root.'/scripts/__init_script__.php';
$args = new PhutilArgumentParser($argv);
-$args->setTagline('manage feed');
+$args->setTagline(pht('manage feed'));
$args->setSynopsis(<<<EOSYNOPSIS
**feed** __command__ [__options__]
Test and debug feed events.
EOSYNOPSIS
);
$args->parseStandardArguments();
$workflows = id(new PhutilSymbolLoader())
->setAncestorClass('PhabricatorFeedManagementWorkflow')
->loadObjects();
$workflows[] = new PhutilHelpArgumentWorkflow();
$args->parseWorkflows($workflows);
diff --git a/scripts/setup/manage_harbormaster.php b/scripts/setup/manage_harbormaster.php
index 1903bdd90..1c8683893 100755
--- a/scripts/setup/manage_harbormaster.php
+++ b/scripts/setup/manage_harbormaster.php
@@ -1,21 +1,21 @@
#!/usr/bin/env php
<?php
$root = dirname(dirname(dirname(__FILE__)));
require_once $root.'/scripts/__init_script__.php';
$args = new PhutilArgumentParser($argv);
-$args->setTagline('manage Harbormaster');
+$args->setTagline(pht('manage Harbormaster'));
$args->setSynopsis(<<<EOSYNOPSIS
**harbormaster** __command__ [__options__]
Manage and debug Harbormaster.
EOSYNOPSIS
);
$args->parseStandardArguments();
$workflows = id(new PhutilSymbolLoader())
->setAncestorClass('HarbormasterManagementWorkflow')
->loadObjects();
$workflows[] = new PhutilHelpArgumentWorkflow();
$args->parseWorkflows($workflows);
diff --git a/scripts/setup/manage_hunks.php b/scripts/setup/manage_hunks.php
index c4750ece7..bff11ca72 100755
--- a/scripts/setup/manage_hunks.php
+++ b/scripts/setup/manage_hunks.php
@@ -1,21 +1,21 @@
#!/usr/bin/env php
<?php
$root = dirname(dirname(dirname(__FILE__)));
require_once $root.'/scripts/__init_script__.php';
$args = new PhutilArgumentParser($argv);
-$args->setTagline('manage hunks');
+$args->setTagline(pht('manage hunks'));
$args->setSynopsis(<<<EOSYNOPSIS
**hunks** __command__ [__options__]
Manage Differential hunk storage.
EOSYNOPSIS
);
$args->parseStandardArguments();
$workflows = id(new PhutilSymbolLoader())
->setAncestorClass('PhabricatorHunksManagementWorkflow')
->loadObjects();
$workflows[] = new PhutilHelpArgumentWorkflow();
$args->parseWorkflows($workflows);
diff --git a/scripts/setup/manage_i18n.php b/scripts/setup/manage_i18n.php
index 77a76728b..bbbbdcb29 100755
--- a/scripts/setup/manage_i18n.php
+++ b/scripts/setup/manage_i18n.php
@@ -1,21 +1,21 @@
#!/usr/bin/env php
<?php
$root = dirname(dirname(dirname(__FILE__)));
require_once $root.'/scripts/__init_script__.php';
$args = new PhutilArgumentParser($argv);
-$args->setTagline('manage internationalization');
+$args->setTagline(pht('manage internationalization'));
$args->setSynopsis(<<<EOSYNOPSIS
**i18n** __command__ [__options__]
Manage translations and internationalization.
EOSYNOPSIS
);
$args->parseStandardArguments();
$workflows = id(new PhutilSymbolLoader())
->setAncestorClass('PhabricatorInternationalizationManagementWorkflow')
->loadObjects();
$workflows[] = new PhutilHelpArgumentWorkflow();
$args->parseWorkflows($workflows);
diff --git a/scripts/setup/manage_phortune.php b/scripts/setup/manage_phortune.php
index 45d8c6257..81eacd4dc 100755
--- a/scripts/setup/manage_phortune.php
+++ b/scripts/setup/manage_phortune.php
@@ -1,21 +1,21 @@
#!/usr/bin/env php
<?php
$root = dirname(dirname(dirname(__FILE__)));
require_once $root.'/scripts/__init_script__.php';
$args = new PhutilArgumentParser($argv);
-$args->setTagline('manage billing');
+$args->setTagline(pht('manage billing'));
$args->setSynopsis(<<<EOSYNOPSIS
**phortune** __command__ [__options__]
Manage billing.
EOSYNOPSIS
);
$args->parseStandardArguments();
$workflows = id(new PhutilSymbolLoader())
->setAncestorClass('PhabricatorPhortuneManagementWorkflow')
->loadObjects();
$workflows[] = new PhutilHelpArgumentWorkflow();
$args->parseWorkflows($workflows);
diff --git a/scripts/setup/manage_policy.php b/scripts/setup/manage_policy.php
index a9ab27731..034f498c0 100755
--- a/scripts/setup/manage_policy.php
+++ b/scripts/setup/manage_policy.php
@@ -1,21 +1,21 @@
#!/usr/bin/env php
<?php
$root = dirname(dirname(dirname(__FILE__)));
require_once $root.'/scripts/__init_script__.php';
$args = new PhutilArgumentParser($argv);
-$args->setTagline('manage policies');
+$args->setTagline(pht('manage policies'));
$args->setSynopsis(<<<EOSYNOPSIS
**policy** __command__ [__options__]
Administrative tool for reviewing and editing policies.
EOSYNOPSIS
);
$args->parseStandardArguments();
$workflows = id(new PhutilSymbolLoader())
->setAncestorClass('PhabricatorPolicyManagementWorkflow')
->loadObjects();
$workflows[] = new PhutilHelpArgumentWorkflow();
$args->parseWorkflows($workflows);
diff --git a/scripts/setup/manage_remove.php b/scripts/setup/manage_remove.php
index db7dd1926..581cae238 100755
--- a/scripts/setup/manage_remove.php
+++ b/scripts/setup/manage_remove.php
@@ -1,21 +1,21 @@
#!/usr/bin/env php
<?php
$root = dirname(dirname(dirname(__FILE__)));
require_once $root.'/scripts/__init_script__.php';
$args = new PhutilArgumentParser($argv);
-$args->setTagline('remove objects');
+$args->setTagline(pht('remove objects'));
$args->setSynopsis(<<<EOSYNOPSIS
**remove** __command__ [__options__]
Administrative tool for destroying objects permanently.
EOSYNOPSIS
);
$args->parseStandardArguments();
$workflows = id(new PhutilSymbolLoader())
->setAncestorClass('PhabricatorSystemRemoveWorkflow')
->loadObjects();
$workflows[] = new PhutilHelpArgumentWorkflow();
$args->parseWorkflows($workflows);
diff --git a/scripts/setup/manage_trigger.php b/scripts/setup/manage_trigger.php
index 7624c1f1d..558015fb3 100755
--- a/scripts/setup/manage_trigger.php
+++ b/scripts/setup/manage_trigger.php
@@ -1,21 +1,21 @@
#!/usr/bin/env php
<?php
$root = dirname(dirname(dirname(__FILE__)));
require_once $root.'/scripts/__init_script__.php';
$args = new PhutilArgumentParser($argv);
-$args->setTagline('manage triggers');
+$args->setTagline(pht('manage triggers'));
$args->setSynopsis(<<<EOSYNOPSIS
**trigger** __command__ [__options__]
Manage event triggers.
EOSYNOPSIS
);
$args->parseStandardArguments();
$workflows = id(new PhutilSymbolLoader())
->setAncestorClass('PhabricatorWorkerTriggerManagementWorkflow')
->loadObjects();
$workflows[] = new PhutilHelpArgumentWorkflow();
$args->parseWorkflows($workflows);
diff --git a/scripts/setup/manage_worker.php b/scripts/setup/manage_worker.php
index 05dcf085b..111c66d3e 100755
--- a/scripts/setup/manage_worker.php
+++ b/scripts/setup/manage_worker.php
@@ -1,21 +1,21 @@
#!/usr/bin/env php
<?php
$root = dirname(dirname(dirname(__FILE__)));
require_once $root.'/scripts/__init_script__.php';
$args = new PhutilArgumentParser($argv);
-$args->setTagline('manage task queue');
+$args->setTagline(pht('manage task queue'));
$args->setSynopsis(<<<EOSYNOPSIS
**worker** __command__ [__options__]
Manage the task queue.
EOSYNOPSIS
);
$args->parseStandardArguments();
$workflows = id(new PhutilSymbolLoader())
->setAncestorClass('PhabricatorWorkerManagementWorkflow')
->loadObjects();
$workflows[] = new PhutilHelpArgumentWorkflow();
$args->parseWorkflows($workflows);
diff --git a/scripts/sms/manage_sms.php b/scripts/sms/manage_sms.php
index 6e0539a1a..a66f66040 100755
--- a/scripts/sms/manage_sms.php
+++ b/scripts/sms/manage_sms.php
@@ -1,21 +1,21 @@
#!/usr/bin/env php
<?php
$root = dirname(dirname(dirname(__FILE__)));
require_once $root.'/scripts/__init_script__.php';
$args = new PhutilArgumentParser($argv);
-$args->setTagline('manage SMS');
+$args->setTagline(pht('manage SMS'));
$args->setSynopsis(<<<EOSYNOPSIS
**sms** __command__ [__options__]
Manage Phabricator SMS stuff.
EOSYNOPSIS
);
$args->parseStandardArguments();
$workflows = id(new PhutilSymbolLoader())
->setAncestorClass('PhabricatorSMSManagementWorkflow')
->loadObjects();
$workflows[] = new PhutilHelpArgumentWorkflow();
$args->parseWorkflows($workflows);
diff --git a/scripts/sql/manage_storage.php b/scripts/sql/manage_storage.php
index 32c3bd3f1..1225e21a8 100755
--- a/scripts/sql/manage_storage.php
+++ b/scripts/sql/manage_storage.php
@@ -1,171 +1,176 @@
#!/usr/bin/env php
<?php
$root = dirname(dirname(dirname(__FILE__)));
require_once $root.'/scripts/__init_script__.php';
$args = new PhutilArgumentParser($argv);
-$args->setTagline('manage Phabricator storage and schemata');
+$args->setTagline(pht('manage Phabricator storage and schemata'));
$args->setSynopsis(<<<EOHELP
**storage** __workflow__ [__options__]
Manage Phabricator database storage and schema versioning.
**storage** upgrade
Initialize or upgrade Phabricator storage.
**storage** upgrade --user __root__ --password __hunter2__
Use administrative credentials for schema changes.
EOHELP
);
$args->parseStandardArguments();
$conf = PhabricatorEnv::newObjectFromConfig(
'mysql.configuration-provider',
array($dao = null, 'w'));
$default_user = $conf->getUser();
$default_host = $conf->getHost();
$default_port = $conf->getPort();
$default_namespace = PhabricatorLiskDAO::getDefaultStorageNamespace();
try {
$args->parsePartial(
array(
array(
'name' => 'force',
'short' => 'f',
- 'help' => 'Do not prompt before performing dangerous operations.',
+ 'help' => pht(
+ 'Do not prompt before performing dangerous operations.'),
),
array(
'name' => 'user',
'short' => 'u',
'param' => 'username',
'default' => $default_user,
- 'help' => "Connect with __username__ instead of the configured ".
- "default ('{$default_user}').",
+ 'help' => pht(
+ "Connect with __username__ instead of the configured default ('%s').",
+ $default_user),
),
array(
'name' => 'password',
'short' => 'p',
'param' => 'password',
- 'help' => 'Use __password__ instead of the configured default.',
+ 'help' => pht('Use __password__ instead of the configured default.'),
),
array(
'name' => 'namespace',
'param' => 'name',
'default' => $default_namespace,
- 'help' => "Use namespace __namespace__ instead of the configured ".
- "default ('{$default_namespace}'). This is an advanced ".
- "feature used by unit tests; you should not normally ".
- "use this flag.",
+ 'help' => pht(
+ "Use namespace __namespace__ instead of the configured ".
+ "default ('%s'). This is an advanced feature used by unit tests; ".
+ "you should not normally use this flag.",
+ $default_namespace),
),
array(
'name' => 'dryrun',
- 'help' => 'Do not actually change anything, just show what would be '.
- 'changed.',
+ 'help' => pht(
+ 'Do not actually change anything, just show what would be changed.'),
),
array(
'name' => 'disable-utf8mb4',
'help' => pht(
'Disable utf8mb4, even if the database supports it. This is an '.
'advanced feature used for testing changes to Phabricator; you '.
'should not normally use this flag.'),
),
));
} catch (PhutilArgumentUsageException $ex) {
$args->printUsageException($ex);
exit(77);
}
// First, test that the Phabricator configuration is set up correctly. After
// we know this works we'll test any administrative credentials specifically.
$test_api = new PhabricatorStorageManagementAPI();
$test_api->setUser($default_user);
$test_api->setHost($default_host);
$test_api->setPort($default_port);
$test_api->setPassword($conf->getPassword());
$test_api->setNamespace($args->getArg('namespace'));
try {
queryfx(
$test_api->getConn(null),
'SELECT 1');
} catch (AphrontQueryException $ex) {
$message = phutil_console_format(
+ "**%s**\n\n%s\n\n%s\n\n%s\n\n**%s**: %s\n",
+ pht('MySQL Credentials Not Configured'),
pht(
- "**MySQL Credentials Not Configured**\n\n".
- "Unable to connect to MySQL using the configured credentials. ".
- "You must configure standard credentials before you can upgrade ".
- "storage. Run these commands to set up credentials:\n".
- "\n".
- " phabricator/ $ ./bin/config set mysql.host __host__\n".
- " phabricator/ $ ./bin/config set mysql.user __username__\n".
- " phabricator/ $ ./bin/config set mysql.pass __password__\n".
- "\n".
- "These standard credentials are separate from any administrative ".
- "credentials provided to this command with __--user__ or ".
- "__--password__, and must be configured correctly before you can ".
- "proceed.\n".
- "\n".
- "**Raw MySQL Error**: %s\n",
- $ex->getMessage()));
+ 'Unable to connect to MySQL using the configured credentials. '.
+ 'You must configure standard credentials before you can upgrade '.
+ 'storage. Run these commands to set up credentials:'),
+ " phabricator/ $ ./bin/config set mysql.host __host__\n".
+ " phabricator/ $ ./bin/config set mysql.user __username__\n".
+ " phabricator/ $ ./bin/config set mysql.pass __password__",
+ pht(
+ 'These standard credentials are separate from any administrative '.
+ 'credentials provided to this command with __%s__ or '.
+ '__%s__, and must be configured correctly before you can proceed.',
+ '--user',
+ '--password'),
+ pht('Raw MySQL Error'),
+ $ex->getMessage());
echo phutil_console_wrap($message);
exit(1);
}
if ($args->getArg('password') === null) {
// This is already a PhutilOpaqueEnvelope.
$password = $conf->getPassword();
} else {
// Put this in a PhutilOpaqueEnvelope.
$password = new PhutilOpaqueEnvelope($args->getArg('password'));
PhabricatorEnv::overrideConfig('mysql.pass', $args->getArg('password'));
}
$api = new PhabricatorStorageManagementAPI();
$api->setUser($args->getArg('user'));
PhabricatorEnv::overrideConfig('mysql.user', $args->getArg('user'));
$api->setHost($default_host);
$api->setPort($default_port);
$api->setPassword($password);
$api->setNamespace($args->getArg('namespace'));
$api->setDisableUTF8MB4($args->getArg('disable-utf8mb4'));
try {
queryfx(
$api->getConn(null),
'SELECT 1');
} catch (AphrontQueryException $ex) {
$message = phutil_console_format(
+ "**%s**\n\n%s\n\n**%s**: %s\n",
+ pht('Bad Administrative Credentials'),
pht(
- "**Bad Administrative Credentials**\n\n".
- "Unable to connnect to MySQL using the administrative credentials ".
- "provided with the __--user__ and __--password__ flags. Check that ".
- "you have entered them correctly.\n".
- "\n".
- "**Raw MySQL Error**: %s\n",
- $ex->getMessage()));
+ 'Unable to connect to MySQL using the administrative credentials '.
+ 'provided with the __%s__ and __%s__ flags. Check that '.
+ 'you have entered them correctly.',
+ '--user',
+ '--password'),
+ pht('Raw MySQL Error'),
+ $ex->getMessage());
echo phutil_console_wrap($message);
exit(1);
}
$workflows = id(new PhutilSymbolLoader())
->setAncestorClass('PhabricatorStorageManagementWorkflow')
->loadObjects();
$patches = PhabricatorSQLPatchList::buildAllPatches();
foreach ($workflows as $workflow) {
$workflow->setAPI($api);
$workflow->setPatches($patches);
}
$workflows[] = new PhutilHelpArgumentWorkflow();
$args->parseWorkflows($workflows);
diff --git a/scripts/ssh/ssh-exec.php b/scripts/ssh/ssh-exec.php
index 93b808be9..0eb9b4a3a 100755
--- a/scripts/ssh/ssh-exec.php
+++ b/scripts/ssh/ssh-exec.php
@@ -1,290 +1,297 @@
#!/usr/bin/env php
<?php
$ssh_start_time = microtime(true);
$root = dirname(dirname(dirname(__FILE__)));
require_once $root.'/scripts/__init_script__.php';
$ssh_log = PhabricatorSSHLog::getLog();
$args = new PhutilArgumentParser($argv);
-$args->setTagline('execute SSH requests');
+$args->setTagline(pht('execute SSH requests'));
$args->setSynopsis(<<<EOSYNOPSIS
**ssh-exec** --phabricator-ssh-user __user__ [--ssh-command __commmand__]
**ssh-exec** --phabricator-ssh-device __device__ [--ssh-command __commmand__]
Execute authenticated SSH requests. This script is normally invoked
via SSHD, but can be invoked manually for testing.
EOSYNOPSIS
);
$args->parseStandardArguments();
$args->parse(
array(
array(
'name' => 'phabricator-ssh-user',
'param' => 'username',
'help' => pht(
'If the request authenticated with a user key, the name of the '.
'user.'),
),
array(
'name' => 'phabricator-ssh-device',
'param' => 'name',
'help' => pht(
'If the request authenticated with a device key, the name of the '.
'device.'),
),
array(
'name' => 'phabricator-ssh-key',
'param' => 'id',
'help' => pht(
'The ID of the SSH key which authenticated this request. This is '.
'used to allow logs to report when specific keys were used, to make '.
'it easier to manage credentials.'),
),
array(
'name' => 'ssh-command',
'param' => 'command',
'help' => pht(
'Provide a command to execute. This makes testing this script '.
'easier. When running normally, the command is read from the '.
- 'environment (SSH_ORIGINAL_COMMAND), which is populated by sshd.'),
+ 'environment (%s), which is populated by sshd.',
+ 'SSH_ORIGINAL_COMMAND'),
),
));
try {
$remote_address = null;
$ssh_client = getenv('SSH_CLIENT');
if ($ssh_client) {
// This has the format "<ip> <remote-port> <local-port>". Grab the IP.
$remote_address = head(explode(' ', $ssh_client));
$ssh_log->setData(
array(
'r' => $remote_address,
));
}
$key_id = $args->getArg('phabricator-ssh-key');
if ($key_id) {
$ssh_log->setData(
array(
'k' => $key_id,
));
}
$user_name = $args->getArg('phabricator-ssh-user');
$device_name = $args->getArg('phabricator-ssh-device');
$user = null;
$device = null;
$is_cluster_request = false;
if ($user_name && $device_name) {
throw new Exception(
pht(
- 'The --phabricator-ssh-user and --phabricator-ssh-device flags are '.
- 'mutually exclusive. You can not authenticate as both a user ("%s") '.
- 'and a device ("%s"). Specify one or the other, but not both.',
+ 'The %s and %s flags are mutually exclusive. You can not '.
+ 'authenticate as both a user ("%s") and a device ("%s"). '.
+ 'Specify one or the other, but not both.',
+ '--phabricator-ssh-user',
+ '--phabricator-ssh-device',
$user_name,
$device_name));
} else if (strlen($user_name)) {
$user = id(new PhabricatorPeopleQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withUsernames(array($user_name))
->executeOne();
if (!$user) {
throw new Exception(
pht(
'Invalid username ("%s"). There is no user with this username.',
$user_name));
}
} else if (strlen($device_name)) {
if (!$remote_address) {
throw new Exception(
pht(
- 'Unable to identify remote address from the SSH_CLIENT environment '.
+ 'Unable to identify remote address from the %s environment '.
'variable. Device authentication is accepted only from trusted '.
- 'sources.'));
+ 'sources.',
+ 'SSH_CLIENT'));
}
if (!PhabricatorEnv::isClusterAddress($remote_address)) {
throw new Exception(
pht(
'This request originates from outside of the Phabricator cluster '.
'address range. Requests signed with a trusted device key must '.
'originate from trusted hosts.'));
}
$device = id(new AlmanacDeviceQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withNames(array($device_name))
->executeOne();
if (!$device) {
throw new Exception(
pht(
'Invalid device name ("%s"). There is no device with this name.',
$device->getName()));
}
// We're authenticated as a device, but we're going to read the user out of
// the command below.
$is_cluster_request = true;
} else {
throw new Exception(
pht(
- 'This script must be invoked with either the --phabricator-ssh-user '.
- 'or --phabricator-ssh-device flag.'));
+ 'This script must be invoked with either the %s or %s flag.',
+ '--phabricator-ssh-user',
+ '--phabricator-ssh-device'));
}
if ($args->getArg('ssh-command')) {
$original_command = $args->getArg('ssh-command');
} else {
$original_command = getenv('SSH_ORIGINAL_COMMAND');
}
$original_argv = id(new PhutilShellLexer())
->splitArguments($original_command);
if ($device) {
$act_as_name = array_shift($original_argv);
if (!preg_match('/^@/', $act_as_name)) {
throw new Exception(
pht(
'Commands executed by devices must identify an acting user in the '.
'first command argument. This request was not constructed '.
'properly.'));
}
$act_as_name = substr($act_as_name, 1);
$user = id(new PhabricatorPeopleQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withUsernames(array($act_as_name))
->executeOne();
if (!$user) {
throw new Exception(
pht(
'Device request identifies an acting user with an invalid '.
'username ("%s"). There is no user with this username.',
$act_as_name));
}
}
$ssh_log->setData(
array(
'u' => $user->getUsername(),
'P' => $user->getPHID(),
));
if (!$user->isUserActivated()) {
throw new Exception(
pht(
'Your account ("%s") is not activated. Visit the web interface '.
'for more information.',
$user->getUsername()));
}
$workflows = id(new PhutilSymbolLoader())
->setAncestorClass('PhabricatorSSHWorkflow')
->loadObjects();
$workflow_names = mpull($workflows, 'getName', 'getName');
if (!$original_argv) {
throw new Exception(
pht(
"Welcome to Phabricator.\n\n".
"You are logged in as %s.\n\n".
"You haven't specified a command to run. This means you're requesting ".
"an interactive shell, but Phabricator does not provide an ".
"interactive shell over SSH.\n\n".
- "Usually, you should run a command like `git clone` or `hg push` ".
+ "Usually, you should run a command like `%s` or `%s` ".
"rather than connecting directly with SSH.\n\n".
"Supported commands are: %s.",
$user->getUsername(),
+ 'git clone',
+ 'hg push',
implode(', ', $workflow_names)));
}
$log_argv = implode(' ', $original_argv);
$log_argv = id(new PhutilUTF8StringTruncator())
->setMaximumCodepoints(128)
->truncateString($log_argv);
$ssh_log->setData(
array(
'C' => $original_argv[0],
'U' => $log_argv,
));
$command = head($original_argv);
$parseable_argv = $original_argv;
array_unshift($parseable_argv, 'phabricator-ssh-exec');
$parsed_args = new PhutilArgumentParser($parseable_argv);
if (empty($workflow_names[$command])) {
- throw new Exception('Invalid command.');
+ throw new Exception(pht('Invalid command.'));
}
$workflow = $parsed_args->parseWorkflows($workflows);
$workflow->setUser($user);
$workflow->setOriginalArguments($original_argv);
$workflow->setIsClusterRequest($is_cluster_request);
$sock_stdin = fopen('php://stdin', 'r');
if (!$sock_stdin) {
- throw new Exception('Unable to open stdin.');
+ throw new Exception(pht('Unable to open stdin.'));
}
$sock_stdout = fopen('php://stdout', 'w');
if (!$sock_stdout) {
- throw new Exception('Unable to open stdout.');
+ throw new Exception(pht('Unable to open stdout.'));
}
$sock_stderr = fopen('php://stderr', 'w');
if (!$sock_stderr) {
- throw new Exception('Unable to open stderr.');
+ throw new Exception(pht('Unable to open stderr.'));
}
$socket_channel = new PhutilSocketChannel(
$sock_stdin,
$sock_stdout);
$error_channel = new PhutilSocketChannel(null, $sock_stderr);
$metrics_channel = new PhutilMetricsChannel($socket_channel);
$workflow->setIOChannel($metrics_channel);
$workflow->setErrorChannel($error_channel);
$rethrow = null;
try {
$err = $workflow->execute($parsed_args);
$metrics_channel->flush();
$error_channel->flush();
} catch (Exception $ex) {
$rethrow = $ex;
}
// Always write this if we got as far as building a metrics channel.
$ssh_log->setData(
array(
'i' => $metrics_channel->getBytesRead(),
'o' => $metrics_channel->getBytesWritten(),
));
if ($rethrow) {
throw $rethrow;
}
} catch (Exception $ex) {
fwrite(STDERR, "phabricator-ssh-exec: ".$ex->getMessage()."\n");
$err = 1;
}
$ssh_log->setData(
array(
'c' => $err,
'T' => (int)(1000000 * (microtime(true) - $ssh_start_time)),
));
exit($err);
diff --git a/scripts/symbols/import_repository_symbols.php b/scripts/symbols/import_repository_symbols.php
index 14a83dda0..94cf601a3 100755
--- a/scripts/symbols/import_repository_symbols.php
+++ b/scripts/symbols/import_repository_symbols.php
@@ -1,229 +1,229 @@
#!/usr/bin/env php
<?php
$root = dirname(dirname(dirname(__FILE__)));
require_once $root.'/scripts/__init_script__.php';
$args = new PhutilArgumentParser($argv);
$args->setSynopsis(<<<EOSYNOPSIS
**import_repository_symbols.php** [__options__] __callsign__ < symbols
Import repository symbols (symbols are read from stdin).
EOSYNOPSIS
);
$args->parseStandardArguments();
$args->parse(
array(
array(
'name' => 'no-purge',
'help' => pht(
'Do not clear all symbols for this repository before '.
'uploading new symbols. Useful for incremental updating.'),
),
array(
'name' => 'ignore-errors',
'help' => pht(
"If a line can't be parsed, ignore that line and ".
"continue instead of exiting."),
),
array(
'name' => 'max-transaction',
'param' => 'num-syms',
'default' => '100000',
'help' => pht(
'Maximum number of symbols that should '.
'be part of a single transaction.'),
),
array(
'name' => 'callsign',
'wildcard' => true,
),
));
$callsigns = $args->getArg('callsign');
if (count($callsigns) !== 1) {
$args->printHelpAndExit();
}
$callsign = head($callsigns);
$repository = id(new PhabricatorRepositoryQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withCallsigns($callsigns)
->executeOne();
if (!$repository) {
echo pht("Repository '%s' does not exist.", $callsign);
exit(1);
}
if (!function_exists('posix_isatty') || posix_isatty(STDIN)) {
echo pht('Parsing input from stdin...'), "\n";
}
$input = file_get_contents('php://stdin');
$input = trim($input);
$input = explode("\n", $input);
function commit_symbols(
array $symbols,
PhabricatorRepository $repository,
$no_purge) {
echo pht('Looking up path IDs...'), "\n";
$path_map =
PhabricatorRepositoryCommitChangeParserWorker::lookupOrCreatePaths(
ipull($symbols, 'path'));
$symbol = new PhabricatorRepositorySymbol();
$conn_w = $symbol->establishConnection('w');
echo pht('Preparing queries...'), "\n";
$sql = array();
foreach ($symbols as $dict) {
$sql[] = qsprintf(
$conn_w,
'(%s, %s, %s, %s, %s, %d, %d)',
$repository->getPHID(),
$dict['ctxt'],
$dict['name'],
$dict['type'],
$dict['lang'],
$dict['line'],
$path_map[$dict['path']]);
}
if (!$no_purge) {
echo pht('Purging old symbols...'), "\n";
queryfx(
$conn_w,
'DELETE FROM %T WHERE repositoryPHID = %s',
$symbol->getTableName(),
$repository->getPHID());
}
echo pht('Loading %s symbols...', new PhutilNumber(count($sql))), "\n";
foreach (array_chunk($sql, 128) as $chunk) {
queryfx(
$conn_w,
'INSERT INTO %T
(repositoryPHID, symbolContext, symbolName, symbolType,
symbolLanguage, lineNumber, pathID) VALUES %Q',
$symbol->getTableName(),
implode(', ', $chunk));
}
}
function check_string_value($value, $field_name, $line_no, $max_length) {
if (strlen($value) > $max_length) {
throw new Exception(
pht(
"%s '%s' defined on line #%d is too long, ".
"maximum %s length is %d characters.",
$field_name,
$value,
$line_no,
$field_name,
$max_length));
}
if (!phutil_is_utf8_with_only_bmp_characters($value)) {
throw new Exception(
pht(
"%s '%s' defined on line #%d is not a valid ".
"UTF-8 string, it should contain only UTF-8 characters.",
$field_name,
$value,
$line_no));
}
}
$no_purge = $args->getArg('no-purge');
$symbols = array();
foreach ($input as $key => $line) {
try {
$line_no = $key + 1;
$matches = null;
$ok = preg_match(
'/^((?P<context>[^ ]+)? )?(?P<name>[^ ]+) (?P<type>[^ ]+) '.
'(?P<lang>[^ ]+) (?P<line>\d+) (?P<path>.*)$/',
$line,
$matches);
if (!$ok) {
throw new Exception(
pht(
"Line #%d of input is invalid. Expected five or six space-delimited ".
"fields: maybe symbol context, symbol name, symbol type, symbol ".
"language, line number, path. For example:\n\n%s\n\n".
"Actual line was:\n\n%s",
$line_no,
'idx function php 13 /path/to/some/file.php',
$line));
}
if (empty($matches['context'])) {
$matches['context'] = '';
}
$context = $matches['context'];
$name = $matches['name'];
$type = $matches['type'];
$lang = $matches['lang'];
$line_number = $matches['line'];
$path = $matches['path'];
- check_string_value($context, 'Symbol context', $line_no, 128);
- check_string_value($name, 'Symbol name', $line_no, 128);
- check_string_value($type, 'Symbol type', $line_no, 12);
- check_string_value($lang, 'Symbol language', $line_no, 32);
- check_string_value($path, 'Path', $line_no, 512);
+ check_string_value($context, pht('Symbol context'), $line_no, 128);
+ check_string_value($name, pht('Symbol name'), $line_no, 128);
+ check_string_value($type, pht('Symbol type'), $line_no, 12);
+ check_string_value($lang, pht('Symbol language'), $line_no, 32);
+ check_string_value($path, pht('Path'), $line_no, 512);
if (!strlen($path) || $path[0] != '/') {
throw new Exception(
pht(
"Path '%s' defined on line #%d is invalid. Paths should begin with ".
"'%s' and specify a path from the root of the project, like '%s'.",
$path,
$line_no,
'/',
'/src/utils/utils.php'));
}
$symbols[] = array(
'ctxt' => $context,
'name' => $name,
'type' => $type,
'lang' => $lang,
'line' => $line_number,
'path' => $path,
);
} catch (Exception $e) {
if ($args->getArg('ignore-errors')) {
continue;
} else {
throw $e;
}
}
if (count ($symbols) >= $args->getArg('max-transaction')) {
try {
echo pht(
"Committing %s symbols...\n",
new PhutilNumber($args->getArg('max-transaction')));
commit_symbols($symbols, $repository, $no_purge);
$no_purge = true;
unset($symbols);
$symbols = array();
} catch (Exception $e) {
if ($args->getArg('ignore-errors')) {
continue;
} else {
throw $e;
}
}
}
}
if (count($symbols)) {
commit_symbols($symbols, $repository, $no_purge);
}
-echo pht('Done.'), "\n";
+echo pht('Done.')."\n";
diff --git a/scripts/user/account_admin.php b/scripts/user/account_admin.php
index 3d18c16ac..dc1cb5019 100755
--- a/scripts/user/account_admin.php
+++ b/scripts/user/account_admin.php
@@ -1,226 +1,228 @@
#!/usr/bin/env php
<?php
$root = dirname(dirname(dirname(__FILE__)));
require_once $root.'/scripts/__init_script__.php';
$table = new PhabricatorUser();
$any_user = queryfx_one(
$table->establishConnection('r'),
'SELECT * FROM %T LIMIT 1',
$table->getTableName());
$is_first_user = (!$any_user);
if ($is_first_user) {
echo pht(
- "WARNING\n\n".
- "You're about to create the first account on this install. Normally, you ".
- "should use the web interface to create the first account, not this ".
- "script.\n\n".
- "If you use the web interface, it will drop you into a nice UI workflow ".
- "which gives you more help setting up your install. If you create an ".
- "account with this script instead, you will skip the setup help and you ".
- "will not be able to access it later.");
+ "WARNING\n\n".
+ "You're about to create the first account on this install. Normally, ".
+ "you should use the web interface to create the first account, not ".
+ "this script.\n\n".
+ "If you use the web interface, it will drop you into a nice UI workflow ".
+ "which gives you more help setting up your install. If you create an ".
+ "account with this script instead, you will skip the setup help and you ".
+ "will not be able to access it later.");
if (!phutil_console_confirm(pht('Skip easy setup and create account?'))) {
echo pht('Cancelled.')."\n";
exit(1);
}
}
-echo 'Enter a username to create a new account or edit an existing account.';
+echo pht(
+ 'Enter a username to create a new account or edit an existing account.');
-$username = phutil_console_prompt('Enter a username:');
+$username = phutil_console_prompt(pht('Enter a username:'));
if (!strlen($username)) {
- echo "Cancelled.\n";
+ echo pht('Cancelled.')."\n";
exit(1);
}
if (!PhabricatorUser::validateUsername($username)) {
$valid = PhabricatorUser::describeValidUsername();
- echo "The username '{$username}' is invalid. {$valid}\n";
+ echo pht("The username '%s' is invalid. %s", $username, $valid)."\n";
exit(1);
}
$user = id(new PhabricatorUser())->loadOneWhere(
'username = %s',
$username);
if (!$user) {
$original = new PhabricatorUser();
- echo "There is no existing user account '{$username}'.\n";
+ echo pht("There is no existing user account '%s'.", $username)."\n";
$ok = phutil_console_confirm(
- "Do you want to create a new '{$username}' account?",
+ pht("Do you want to create a new '%s' account?", $username),
$default_no = false);
if (!$ok) {
- echo "Cancelled.\n";
+ echo pht('Cancelled.')."\n";
exit(1);
}
$user = new PhabricatorUser();
$user->setUsername($username);
$is_new = true;
} else {
$original = clone $user;
- echo "There is an existing user account '{$username}'.\n";
+ echo pht("There is an existing user account '%s'.", $username)."\n";
$ok = phutil_console_confirm(
- "Do you want to edit the existing '{$username}' account?",
+ pht("Do you want to edit the existing '%s' account?", $username),
$default_no = false);
if (!$ok) {
- echo "Cancelled.\n";
+ echo pht('Cancelled.')."\n";
exit(1);
}
$is_new = false;
}
$user_realname = $user->getRealName();
if (strlen($user_realname)) {
- $realname_prompt = ' ['.$user_realname.']';
+ $realname_prompt = ' ['.$user_realname.']:';
} else {
- $realname_prompt = '';
+ $realname_prompt = ':';
}
$realname = nonempty(
- phutil_console_prompt("Enter user real name{$realname_prompt}:"),
+ phutil_console_prompt(pht('Enter user real name').$realname_prompt),
$user_realname);
$user->setRealName($realname);
// When creating a new user we prompt for an email address; when editing an
// existing user we just skip this because it would be quite involved to provide
// a reasonable CLI interface for editing multiple addresses and managing email
// verification and primary addresses.
$create_email = null;
if ($is_new) {
do {
- $email = phutil_console_prompt('Enter user email address:');
+ $email = phutil_console_prompt(pht('Enter user email address:'));
$duplicate = id(new PhabricatorUserEmail())->loadOneWhere(
'address = %s',
$email);
if ($duplicate) {
- echo "ERROR: There is already a user with that email address. ".
- "Each user must have a unique email address.\n";
+ echo pht(
+ "ERROR: There is already a user with that email address. ".
+ "Each user must have a unique email address.\n");
} else {
break;
}
} while (true);
$create_email = $email;
}
$changed_pass = false;
// This disables local echo, so the user's password is not shown as they type
// it.
phutil_passthru('stty -echo');
$password = phutil_console_prompt(
- 'Enter a password for this user [blank to leave unchanged]:');
+ pht('Enter a password for this user [blank to leave unchanged]:'));
phutil_passthru('stty echo');
if (strlen($password)) {
$changed_pass = $password;
}
$is_system_agent = $user->getIsSystemAgent();
$set_system_agent = phutil_console_confirm(
- 'Is this user a bot/script?',
+ pht('Is this user a bot/script?'),
$default_no = !$is_system_agent);
$verify_email = null;
$set_verified = false;
// Allow administrators to verify primary email addresses at this time in edit
// scenarios. (Create will work just fine from here as we auto-verify email
// on create.)
if (!$is_new) {
$verify_email = $user->loadPrimaryEmail();
if (!$verify_email->getIsVerified()) {
$set_verified = phutil_console_confirm(
- 'Should the primary email address be verified?',
+ pht('Should the primary email address be verified?'),
$default_no = true);
} else {
- // already verified so let's not make a fuss
+ // Already verified so let's not make a fuss.
$verify_email = null;
}
}
$is_admin = $user->getIsAdmin();
$set_admin = phutil_console_confirm(
- 'Should this user be an administrator?',
+ pht('Should this user be an administrator?'),
$default_no = !$is_admin);
-echo "\n\nACCOUNT SUMMARY\n\n";
+echo "\n\n".pht('ACCOUNT SUMMARY')."\n\n";
$tpl = "%12s %-30s %-30s\n";
-printf($tpl, null, 'OLD VALUE', 'NEW VALUE');
-printf($tpl, 'Username', $original->getUsername(), $user->getUsername());
-printf($tpl, 'Real Name', $original->getRealName(), $user->getRealName());
+printf($tpl, null, pht('OLD VALUE'), pht('NEW VALUE'));
+printf($tpl, pht('Username'), $original->getUsername(), $user->getUsername());
+printf($tpl, pht('Real Name'), $original->getRealName(), $user->getRealName());
if ($is_new) {
- printf($tpl, 'Email', '', $create_email);
+ printf($tpl, pht('Email'), '', $create_email);
}
-printf($tpl, 'Password', null,
+printf($tpl, pht('Password'), null,
($changed_pass !== false)
- ? 'Updated'
- : 'Unchanged');
+ ? pht('Updated')
+ : pht('Unchanged'));
printf(
$tpl,
- 'Bot/Script',
+ pht('Bot/Script'),
$original->getIsSystemAgent() ? 'Y' : 'N',
$set_system_agent ? 'Y' : 'N');
if ($verify_email) {
printf(
$tpl,
- 'Verify Email',
+ pht('Verify Email'),
$verify_email->getIsVerified() ? 'Y' : 'N',
$set_verified ? 'Y' : 'N');
}
printf(
$tpl,
- 'Admin',
+ pht('Admin'),
$original->getIsAdmin() ? 'Y' : 'N',
$set_admin ? 'Y' : 'N');
echo "\n";
-if (!phutil_console_confirm('Save these changes?', $default_no = false)) {
- echo "Cancelled.\n";
+if (!phutil_console_confirm(pht('Save these changes?'), $default_no = false)) {
+ echo pht('Cancelled.')."\n";
exit(1);
}
$user->openTransaction();
$editor = new PhabricatorUserEditor();
// TODO: This is wrong, but we have a chicken-and-egg problem when you use
// this script to create the first user.
$editor->setActor($user);
if ($is_new) {
$email = id(new PhabricatorUserEmail())
->setAddress($create_email)
->setIsVerified(1);
// Unconditionally approve new accounts created from the CLI.
$user->setIsApproved(1);
$editor->createNewUser($user, $email);
} else {
if ($verify_email) {
$user->setIsEmailVerified(1);
$verify_email->setIsVerified($set_verified ? 1 : 0);
}
$editor->updateUser($user, $verify_email);
}
$editor->makeAdminUser($user, $set_admin);
$editor->makeSystemAgentUser($user, $set_system_agent);
if ($changed_pass !== false) {
$envelope = new PhutilOpaqueEnvelope($changed_pass);
$editor->changePassword($user, $envelope);
}
$user->saveTransaction();
-echo "Saved changes.\n";
+echo pht('Saved changes.')."\n";
diff --git a/scripts/user/add_user.php b/scripts/user/add_user.php
index f2cdac396..4c598e47e 100755
--- a/scripts/user/add_user.php
+++ b/scripts/user/add_user.php
@@ -1,57 +1,68 @@
#!/usr/bin/env php
<?php
$root = dirname(dirname(dirname(__FILE__)));
require_once $root.'/scripts/__init_script__.php';
if ($argc !== 5) {
- echo "usage: add_user.php <username> <email> <realname> <admin_user>\n";
+ echo pht(
+ "Usage: %s\n",
+ 'add_user.php <username> <email> <realname> <admin_user>');
exit(1);
}
$username = $argv[1];
$email = $argv[2];
$realname = $argv[3];
$admin = $argv[4];
$admin = id(new PhabricatorUser())->loadOneWhere(
'username = %s',
$argv[4]);
if (!$admin) {
throw new Exception(
- 'Admin user must be the username of a valid Phabricator account, used '.
- 'to send the new user a welcome email.');
+ pht(
+ 'Admin user must be the username of a valid Phabricator account, used '.
+ 'to send the new user a welcome email.'));
}
$existing_user = id(new PhabricatorUser())->loadOneWhere(
'username = %s',
$username);
if ($existing_user) {
throw new Exception(
- "There is already a user with the username '{$username}'!");
+ pht(
+ "There is already a user with the username '%s'!",
+ $username));
}
$existing_email = id(new PhabricatorUserEmail())->loadOneWhere(
'address = %s',
$email);
if ($existing_email) {
throw new Exception(
- "There is already a user with the email '{$email}'!");
+ pht(
+ "There is already a user with the email '%s'!",
+ $email));
}
$user = new PhabricatorUser();
$user->setUsername($username);
$user->setRealname($realname);
$user->setIsApproved(1);
$email_object = id(new PhabricatorUserEmail())
->setAddress($email)
->setIsVerified(1);
id(new PhabricatorUserEditor())
->setActor($admin)
->createNewUser($user, $email_object);
$user->sendWelcomeEmail($admin);
-echo "Created user '{$username}' (realname='{$realname}', email='{$email}').\n";
+echo pht(
+ "Created user '%s' (realname='%s', email='%s').\n",
+ $username,
+ $realname,
+ $email);
diff --git a/scripts/util/add_macro.php b/scripts/util/add_macro.php
index cbfe8480f..03566cfb7 100755
--- a/scripts/util/add_macro.php
+++ b/scripts/util/add_macro.php
@@ -1,65 +1,65 @@
#!/usr/bin/env php
<?php
$root = dirname(dirname(dirname(__FILE__)));
require_once $root.'/scripts/__init_script__.php';
$args = new PhutilArgumentParser($argv);
-$args->setTagline('load files as image macros');
+$args->setTagline(pht('load files as image macros'));
$args->setSynopsis(<<<EOHELP
**add_macro.php** __image__ [--as __name__]
Add an image macro. This can be useful for importing a large number
of macros.
EOHELP
);
$args->parseStandardArguments();
$args->parse(
array(
array(
'name' => 'as',
'param' => 'name',
- 'help' => 'Use a specific name instead of the first part of the image '.
- 'name.',
+ 'help' => pht(
+ 'Use a specific name instead of the first part of the image name.'),
),
array(
'name' => 'more',
'wildcard' => true,
),
));
$more = $args->getArg('more');
if (count($more) !== 1) {
$args->printHelpAndExit();
}
$path = head($more);
$data = Filesystem::readFile($path);
$name = $args->getArg('as');
if ($name === null) {
$name = head(explode('.', basename($path)));
}
$existing = id(new PhabricatorFileImageMacro())->loadOneWhere(
'name = %s',
$name);
if ($existing) {
- throw new Exception("A macro already exists with the name '{$name}'!");
+ throw new Exception(pht("A macro already exists with the name '%s'!", $name));
}
$file = PhabricatorFile::newFromFileData(
$data,
array(
'name' => basename($path),
'canCDN' => true,
));
$macro = id(new PhabricatorFileImageMacro())
->setFilePHID($file->getPHID())
->setName($name)
->save();
$id = $file->getID();
-echo "Added macro '{$name}' (F{$id}).\n";
+echo pht("Added macro '%s' (%s).", $name, "F{$id}")."\n";
diff --git a/scripts/util/emit_test_event.php b/scripts/util/emit_test_event.php
index 500e8390e..c8502d0fe 100755
--- a/scripts/util/emit_test_event.php
+++ b/scripts/util/emit_test_event.php
@@ -1,41 +1,41 @@
#!/usr/bin/env php
<?php
$root = dirname(dirname(dirname(__FILE__)));
require_once $root.'/scripts/__init_script__.php';
$args = new PhutilArgumentParser($argv);
-$args->setTagline('emit a test event');
+$args->setTagline(pht('emit a test event'));
$args->setSynopsis(<<<EOHELP
**emit_test_event.php** [--listen listener] ...
Emit a test event after installing any specified __listener__s.
EOHELP
);
$args->parseStandardArguments();
$args->parse(
array(
array(
'name' => 'listen',
'param' => 'listener',
'repeat' => true,
),
));
$console = PhutilConsole::getConsole();
foreach ($args->getArg('listen') as $listener) {
- $console->writeOut("Installing '%s'...\n", $listener);
+ $console->writeOut("%s\n", pht("Installing '%s'...", $listener));
newv($listener, array())->register();
}
-$console->writeOut("Emitting event...\n");
+$console->writeOut("%s\n", pht('Emitting event...'));
PhutilEventEngine::dispatchEvent(
new PhabricatorEvent(
PhabricatorEventType::TYPE_TEST_DIDRUNTEST,
array(
'time' => time(),
)));
-$console->writeOut("Done.\n");
+$console->writeOut("%s\n", pht('Done.'));
exit(0);
diff --git a/src/__tests__/PhabricatorCelerityTestCase.php b/src/__tests__/PhabricatorCelerityTestCase.php
index 957664f00..a91b21450 100644
--- a/src/__tests__/PhabricatorCelerityTestCase.php
+++ b/src/__tests__/PhabricatorCelerityTestCase.php
@@ -1,35 +1,36 @@
<?php
final class PhabricatorCelerityTestCase extends PhabricatorTestCase {
/**
* This is more of an acceptance test case instead of a unit test. It verifies
* that the Celerity map is up-to-date.
*/
public function testCelerityMaps() {
$resources_map = CelerityPhysicalResources::getAll();
foreach ($resources_map as $resources) {
$old_map = new CelerityResourceMap($resources);
$new_map = id(new CelerityResourceMapGenerator($resources))
->generate();
// Don't actually compare these values with assertEqual(), since the diff
// isn't helpful and is often enormously huge.
$maps_are_identical =
($new_map->getNameMap() === $old_map->getNameMap()) &&
($new_map->getSymbolMap() === $old_map->getSymbolMap()) &&
($new_map->getRequiresMap() === $old_map->getRequiresMap()) &&
($new_map->getPackageMap() === $old_map->getPackageMap());
$this->assertTrue(
$maps_are_identical,
pht(
'When this test fails, it means the Celerity resource map is out '.
- 'of date. Run `bin/celerity map` to rebuild it.'));
+ 'of date. Run `%s` to rebuild it.',
+ 'bin/celerity map'));
}
}
}
diff --git a/src/__tests__/PhabricatorInfrastructureTestCase.php b/src/__tests__/PhabricatorInfrastructureTestCase.php
index 896a506ed..19d034eb3 100644
--- a/src/__tests__/PhabricatorInfrastructureTestCase.php
+++ b/src/__tests__/PhabricatorInfrastructureTestCase.php
@@ -1,42 +1,42 @@
<?php
final class PhabricatorInfrastructureTestCase extends PhabricatorTestCase {
protected function getPhabricatorTestCaseConfiguration() {
return array(
self::PHABRICATOR_TESTCONFIG_BUILD_STORAGE_FIXTURES => true,
);
}
public function testApplicationsInstalled() {
$all = PhabricatorApplication::getAllApplications();
$installed = PhabricatorApplication::getAllInstalledApplications();
$this->assertEqual(
count($all),
count($installed),
- 'In test cases, all applications should default to installed.');
+ pht('In test cases, all applications should default to installed.'));
}
public function testRejectMySQLNonUTF8Queries() {
$table = new HarbormasterScratchTable();
$conn_r = $table->establishConnection('w');
$snowman = "\xE2\x98\x83";
$invalid = "\xE6\x9D";
qsprintf($conn_r, 'SELECT %B', $snowman);
qsprintf($conn_r, 'SELECT %s', $snowman);
qsprintf($conn_r, 'SELECT %B', $invalid);
$caught = null;
try {
qsprintf($conn_r, 'SELECT %s', $invalid);
} catch (AphrontCharacterSetQueryException $ex) {
$caught = $ex;
}
$this->assertTrue($caught instanceof AphrontCharacterSetQueryException);
}
}
diff --git a/src/aphront/AphrontRequest.php b/src/aphront/AphrontRequest.php
index 4aa52c1fc..f4a1ee2cf 100644
--- a/src/aphront/AphrontRequest.php
+++ b/src/aphront/AphrontRequest.php
@@ -1,763 +1,766 @@
<?php
/**
* @task data Accessing Request Data
* @task cookie Managing Cookies
* @task cluster Working With a Phabricator Cluster
*/
final class AphrontRequest {
// NOTE: These magic request-type parameters are automatically included in
// certain requests (e.g., by phabricator_form(), JX.Request,
// JX.Workflow, and ConduitClient) and help us figure out what sort of
// response the client expects.
const TYPE_AJAX = '__ajax__';
const TYPE_FORM = '__form__';
const TYPE_CONDUIT = '__conduit__';
const TYPE_WORKFLOW = '__wflow__';
const TYPE_CONTINUE = '__continue__';
const TYPE_PREVIEW = '__preview__';
const TYPE_HISEC = '__hisec__';
const TYPE_QUICKSAND = '__quicksand__';
private $host;
private $path;
private $requestData;
private $user;
private $applicationConfiguration;
private $uriData;
public function __construct($host, $path) {
$this->host = $host;
$this->path = $path;
}
public function setURIMap(array $uri_data) {
$this->uriData = $uri_data;
return $this;
}
public function getURIMap() {
return $this->uriData;
}
public function getURIData($key, $default = null) {
return idx($this->uriData, $key, $default);
}
public function setApplicationConfiguration(
$application_configuration) {
$this->applicationConfiguration = $application_configuration;
return $this;
}
public function getApplicationConfiguration() {
return $this->applicationConfiguration;
}
public function setPath($path) {
$this->path = $path;
return $this;
}
public function getPath() {
return $this->path;
}
public function getHost() {
// The "Host" header may include a port number, or may be a malicious
// header in the form "realdomain.com:ignored@evil.com". Invoke the full
// parser to extract the real domain correctly. See here for coverage of
// a similar issue in Django:
//
// https://www.djangoproject.com/weblog/2012/oct/17/security/
$uri = new PhutilURI('http://'.$this->host);
return $uri->getDomain();
}
/* -( Accessing Request Data )--------------------------------------------- */
/**
* @task data
*/
public function setRequestData(array $request_data) {
$this->requestData = $request_data;
return $this;
}
/**
* @task data
*/
public function getRequestData() {
return $this->requestData;
}
/**
* @task data
*/
public function getInt($name, $default = null) {
if (isset($this->requestData[$name])) {
return (int)$this->requestData[$name];
} else {
return $default;
}
}
/**
* @task data
*/
public function getBool($name, $default = null) {
if (isset($this->requestData[$name])) {
if ($this->requestData[$name] === 'true') {
return true;
} else if ($this->requestData[$name] === 'false') {
return false;
} else {
return (bool)$this->requestData[$name];
}
} else {
return $default;
}
}
/**
* @task data
*/
public function getStr($name, $default = null) {
if (isset($this->requestData[$name])) {
$str = (string)$this->requestData[$name];
// Normalize newline craziness.
$str = str_replace(
array("\r\n", "\r"),
array("\n", "\n"),
$str);
return $str;
} else {
return $default;
}
}
/**
* @task data
*/
public function getArr($name, $default = array()) {
if (isset($this->requestData[$name]) &&
is_array($this->requestData[$name])) {
return $this->requestData[$name];
} else {
return $default;
}
}
/**
* @task data
*/
public function getStrList($name, $default = array()) {
if (!isset($this->requestData[$name])) {
return $default;
}
$list = $this->getStr($name);
$list = preg_split('/[\s,]+/', $list, $limit = -1, PREG_SPLIT_NO_EMPTY);
return $list;
}
/**
* @task data
*/
public function getExists($name) {
return array_key_exists($name, $this->requestData);
}
public function getFileExists($name) {
return isset($_FILES[$name]) &&
(idx($_FILES[$name], 'error') !== UPLOAD_ERR_NO_FILE);
}
public function isHTTPGet() {
return ($_SERVER['REQUEST_METHOD'] == 'GET');
}
public function isHTTPPost() {
return ($_SERVER['REQUEST_METHOD'] == 'POST');
}
public function isAjax() {
return $this->getExists(self::TYPE_AJAX) && !$this->isQuicksand();
}
public function isWorkflow() {
return $this->getExists(self::TYPE_WORKFLOW) && !$this->isQuicksand();
}
public function isQuicksand() {
return $this->getExists(self::TYPE_QUICKSAND);
}
public function isConduit() {
return $this->getExists(self::TYPE_CONDUIT);
}
public static function getCSRFTokenName() {
return '__csrf__';
}
public static function getCSRFHeaderName() {
return 'X-Phabricator-Csrf';
}
public function validateCSRF() {
$token_name = self::getCSRFTokenName();
$token = $this->getStr($token_name);
// No token in the request, check the HTTP header which is added for Ajax
// requests.
if (empty($token)) {
$token = self::getHTTPHeader(self::getCSRFHeaderName());
}
$valid = $this->getUser()->validateCSRFToken($token);
if (!$valid) {
// Add some diagnostic details so we can figure out if some CSRF issues
// are JS problems or people accessing Ajax URIs directly with their
// browsers.
$more_info = array();
if ($this->isAjax()) {
$more_info[] = pht('This was an Ajax request.');
} else {
$more_info[] = pht('This was a Web request.');
}
if ($token) {
$more_info[] = pht('This request had an invalid CSRF token.');
} else {
$more_info[] = pht('This request had no CSRF token.');
}
// Give a more detailed explanation of how to avoid the exception
// in developer mode.
if (PhabricatorEnv::getEnvConfig('phabricator.developer-mode')) {
// TODO: Clean this up, see T1921.
- $more_info[] =
- "To avoid this error, use phabricator_form() to construct forms. ".
- "If you are already using phabricator_form(), make sure the form ".
- "'action' uses a relative URI (i.e., begins with a '/'). Forms ".
- "using absolute URIs do not include CSRF tokens, to prevent ".
- "leaking tokens to external sites.\n\n".
- "If this page performs writes which do not require CSRF ".
- "protection (usually, filling caches or logging), you can use ".
- "AphrontWriteGuard::beginScopedUnguardedWrites() to temporarily ".
- "bypass CSRF protection while writing. You should use this only ".
- "for writes which can not be protected with normal CSRF ".
+ $more_info[] = pht(
+ "To avoid this error, use %s to construct forms. If you are already ".
+ "using %s, make sure the form 'action' uses a relative URI (i.e., ".
+ "begins with a '%s'). Forms using absolute URIs do not include CSRF ".
+ "tokens, to prevent leaking tokens to external sites.\n\n".
+ "If this page performs writes which do not require CSRF protection ".
+ "(usually, filling caches or logging), you can use %s to ".
+ "temporarily bypass CSRF protection while writing. You should use ".
+ "this only for writes which can not be protected with normal CSRF ".
"mechanisms.\n\n".
- "Some UI elements (like PhabricatorActionListView) also have ".
- "methods which will allow you to render links as forms (like ".
- "setRenderAsForm(true)).";
+ "Some UI elements (like %s) also have methods which will allow you ".
+ "to render links as forms (like %s).",
+ 'phabricator_form()',
+ 'phabricator_form()',
+ '/',
+ 'AphrontWriteGuard::beginScopedUnguardedWrites()',
+ 'PhabricatorActionListView',
+ 'setRenderAsForm(true)');
}
// This should only be able to happen if you load a form, pull your
// internet for 6 hours, and then reconnect and immediately submit,
// but give the user some indication of what happened since the workflow
// is incredibly confusing otherwise.
throw new AphrontCSRFException(
pht(
- "You are trying to save some data to Phabricator, but the request ".
- "your browser made included an incorrect token. Reload the page ".
- "and try again. You may need to clear your cookies.\n\n%s",
- implode("\n", $more_info)));
+ 'You are trying to save some data to Phabricator, but the request '.
+ 'your browser made included an incorrect token. Reload the page '.
+ 'and try again. You may need to clear your cookies.')."\n\n".
+ implode("\n", $more_info));
}
return true;
}
public function isFormPost() {
$post = $this->getExists(self::TYPE_FORM) &&
!$this->getExists(self::TYPE_HISEC) &&
$this->isHTTPPost();
if (!$post) {
return false;
}
return $this->validateCSRF();
}
public function isFormOrHisecPost() {
$post = $this->getExists(self::TYPE_FORM) &&
$this->isHTTPPost();
if (!$post) {
return false;
}
return $this->validateCSRF();
}
public function setCookiePrefix($prefix) {
$this->cookiePrefix = $prefix;
return $this;
}
private function getPrefixedCookieName($name) {
if (strlen($this->cookiePrefix)) {
return $this->cookiePrefix.'_'.$name;
} else {
return $name;
}
}
public function getCookie($name, $default = null) {
$name = $this->getPrefixedCookieName($name);
$value = idx($_COOKIE, $name, $default);
// Internally, PHP deletes cookies by setting them to the value 'deleted'
// with an expiration date in the past.
// At least in Safari, the browser may send this cookie anyway in some
// circumstances. After logging out, the 302'd GET to /login/ consistently
// includes deleted cookies on my local install. If a cookie value is
// literally 'deleted', pretend it does not exist.
if ($value === 'deleted') {
return null;
}
return $value;
}
public function clearCookie($name) {
$this->setCookieWithExpiration($name, '', time() - (60 * 60 * 24 * 30));
unset($_COOKIE[$name]);
}
/**
* Get the domain which cookies should be set on for this request, or null
* if the request does not correspond to a valid cookie domain.
*
* @return PhutilURI|null Domain URI, or null if no valid domain exists.
*
* @task cookie
*/
private function getCookieDomainURI() {
if (PhabricatorEnv::getEnvConfig('security.require-https') &&
!$this->isHTTPS()) {
return null;
}
$host = $this->getHost();
// If there's no base domain configured, just use whatever the request
// domain is. This makes setup easier, and we'll tell administrators to
// configure a base domain during the setup process.
$base_uri = PhabricatorEnv::getEnvConfig('phabricator.base-uri');
if (!strlen($base_uri)) {
return new PhutilURI('http://'.$host.'/');
}
$alternates = PhabricatorEnv::getEnvConfig('phabricator.allowed-uris');
$allowed_uris = array_merge(
array($base_uri),
$alternates);
foreach ($allowed_uris as $allowed_uri) {
$uri = new PhutilURI($allowed_uri);
if ($uri->getDomain() == $host) {
return $uri;
}
}
return null;
}
/**
* Determine if security policy rules will allow cookies to be set when
* responding to the request.
*
* @return bool True if setCookie() will succeed. If this method returns
* false, setCookie() will throw.
*
* @task cookie
*/
public function canSetCookies() {
return (bool)$this->getCookieDomainURI();
}
/**
* Set a cookie which does not expire for a long time.
*
* To set a temporary cookie, see @{method:setTemporaryCookie}.
*
* @param string Cookie name.
* @param string Cookie value.
* @return this
* @task cookie
*/
public function setCookie($name, $value) {
$far_future = time() + (60 * 60 * 24 * 365 * 5);
return $this->setCookieWithExpiration($name, $value, $far_future);
}
/**
* Set a cookie which expires soon.
*
* To set a durable cookie, see @{method:setCookie}.
*
* @param string Cookie name.
* @param string Cookie value.
* @return this
* @task cookie
*/
public function setTemporaryCookie($name, $value) {
return $this->setCookieWithExpiration($name, $value, 0);
}
/**
* Set a cookie with a given expiration policy.
*
* @param string Cookie name.
* @param string Cookie value.
* @param int Epoch timestamp for cookie expiration.
* @return this
* @task cookie
*/
private function setCookieWithExpiration(
$name,
$value,
$expire) {
$is_secure = false;
$base_domain_uri = $this->getCookieDomainURI();
if (!$base_domain_uri) {
$configured_as = PhabricatorEnv::getEnvConfig('phabricator.base-uri');
$accessed_as = $this->getHost();
throw new Exception(
pht(
'This Phabricator install is configured as "%s", but you are '.
'using the domain name "%s" to access a page which is trying to '.
'set a cookie. Acccess Phabricator on the configured primary '.
'domain or a configured alternate domain. Phabricator will not '.
'set cookies on other domains for security reasons.',
$configured_as,
$accessed_as));
}
$base_domain = $base_domain_uri->getDomain();
$is_secure = ($base_domain_uri->getProtocol() == 'https');
$name = $this->getPrefixedCookieName($name);
if (php_sapi_name() == 'cli') {
// Do nothing, to avoid triggering "Cannot modify header information"
// warnings.
// TODO: This is effectively a test for whether we're running in a unit
// test or not. Move this actual call to HTTPSink?
} else {
setcookie(
$name,
$value,
$expire,
$path = '/',
$base_domain,
$is_secure,
$http_only = true);
}
$_COOKIE[$name] = $value;
return $this;
}
public function setUser($user) {
$this->user = $user;
return $this;
}
public function getUser() {
return $this->user;
}
public function getViewer() {
return $this->user;
}
public function getRequestURI() {
$get = $_GET;
unset($get['__path__']);
$path = phutil_escape_uri($this->getPath());
return id(new PhutilURI($path))->setQueryParams($get);
}
public function isDialogFormPost() {
return $this->isFormPost() && $this->getStr('__dialog__');
}
public function getRemoteAddr() {
return $_SERVER['REMOTE_ADDR'];
}
public function isHTTPS() {
if (empty($_SERVER['HTTPS'])) {
return false;
}
if (!strcasecmp($_SERVER['HTTPS'], 'off')) {
return false;
}
return true;
}
public function isContinueRequest() {
return $this->isFormPost() && $this->getStr('__continue__');
}
public function isPreviewRequest() {
return $this->isFormPost() && $this->getStr('__preview__');
}
/**
* Get application request parameters in a flattened form suitable for
* inclusion in an HTTP request, excluding parameters with special meanings.
* This is primarily useful if you want to ask the user for more input and
* then resubmit their request.
*
* @return dict<string, string> Original request parameters.
*/
public function getPassthroughRequestParameters($include_quicksand = false) {
return self::flattenData(
$this->getPassthroughRequestData($include_quicksand));
}
/**
* Get request data other than "magic" parameters.
*
* @return dict<string, wild> Request data, with magic filtered out.
*/
public function getPassthroughRequestData($include_quicksand = false) {
$data = $this->getRequestData();
// Remove magic parameters like __dialog__ and __ajax__.
foreach ($data as $key => $value) {
if ($include_quicksand && $key == self::TYPE_QUICKSAND) {
continue;
}
if (!strncmp($key, '__', 2)) {
unset($data[$key]);
}
}
return $data;
}
/**
* Flatten an array of key-value pairs (possibly including arrays as values)
* into a list of key-value pairs suitable for submitting via HTTP request
* (with arrays flattened).
*
* @param dict<string, wild> Data to flatten.
* @return dict<string, string> Flat data suitable for inclusion in an HTTP
* request.
*/
public static function flattenData(array $data) {
$result = array();
foreach ($data as $key => $value) {
if (is_array($value)) {
foreach (self::flattenData($value) as $fkey => $fvalue) {
$fkey = '['.preg_replace('/(?=\[)|$/', ']', $fkey, $limit = 1);
$result[$key.$fkey] = $fvalue;
}
} else {
$result[$key] = (string)$value;
}
}
ksort($result);
return $result;
}
/**
* Read the value of an HTTP header from `$_SERVER`, or a similar datasource.
*
* This function accepts a canonical header name, like `"Accept-Encoding"`,
* and looks up the appropriate value in `$_SERVER` (in this case,
* `"HTTP_ACCEPT_ENCODING"`).
*
* @param string Canonical header name, like `"Accept-Encoding"`.
* @param wild Default value to return if header is not present.
* @param array? Read this instead of `$_SERVER`.
* @return string|wild Header value if present, or `$default` if not.
*/
public static function getHTTPHeader($name, $default = null, $data = null) {
// PHP mangles HTTP headers by uppercasing them and replacing hyphens with
// underscores, then prepending 'HTTP_'.
$php_index = strtoupper($name);
$php_index = str_replace('-', '_', $php_index);
$try_names = array();
$try_names[] = 'HTTP_'.$php_index;
if ($php_index == 'CONTENT_TYPE' || $php_index == 'CONTENT_LENGTH') {
// These headers may be available under alternate names. See
// http://www.php.net/manual/en/reserved.variables.server.php#110763
$try_names[] = $php_index;
}
if ($data === null) {
$data = $_SERVER;
}
foreach ($try_names as $try_name) {
if (array_key_exists($try_name, $data)) {
return $data[$try_name];
}
}
return $default;
}
/* -( Working With a Phabricator Cluster )--------------------------------- */
/**
* Is this a proxied request originating from within the Phabricator cluster?
*
* IMPORTANT: This means the request is dangerous!
*
* These requests are **more dangerous** than normal requests (they can not
* be safely proxied, because proxying them may cause a loop). Cluster
* requests are not guaranteed to come from a trusted source, and should
* never be treated as safer than normal requests. They are strictly less
* safe.
*/
public function isProxiedClusterRequest() {
return (bool)self::getHTTPHeader('X-Phabricator-Cluster');
}
/**
* Build a new @{class:HTTPSFuture} which proxies this request to another
* node in the cluster.
*
* IMPORTANT: This is very dangerous!
*
* The future forwards authentication information present in the request.
* Proxied requests must only be sent to trusted hosts. (We attempt to
* enforce this.)
*
* This is not a general-purpose proxying method; it is a specialized
* method with niche applications and severe security implications.
*
* @param string URI identifying the host we are proxying the request to.
* @return HTTPSFuture New proxy future.
*
* @phutil-external-symbol class PhabricatorStartup
*/
public function newClusterProxyFuture($uri) {
$uri = new PhutilURI($uri);
$domain = $uri->getDomain();
$ip = gethostbyname($domain);
if (!$ip) {
throw new Exception(
pht(
'Unable to resolve domain "%s"!',
$domain));
}
if (!PhabricatorEnv::isClusterAddress($ip)) {
throw new Exception(
pht(
'Refusing to proxy a request to IP address ("%s") which is not '.
'in the cluster address block (this address was derived by '.
'resolving the domain "%s").',
$ip,
$domain));
}
$uri->setPath($this->getPath());
$uri->setQueryParams(self::flattenData($_GET));
$input = PhabricatorStartup::getRawInput();
$future = id(new HTTPSFuture($uri))
->addHeader('Host', self::getHost())
->addHeader('X-Phabricator-Cluster', true)
->setMethod($_SERVER['REQUEST_METHOD'])
->write($input);
if (isset($_SERVER['PHP_AUTH_USER'])) {
$future->setHTTPBasicAuthCredentials(
$_SERVER['PHP_AUTH_USER'],
new PhutilOpaqueEnvelope(idx($_SERVER, 'PHP_AUTH_PW', '')));
}
$headers = array();
$seen = array();
// NOTE: apache_request_headers() might provide a nicer way to do this,
// but isn't available under FCGI until PHP 5.4.0.
foreach ($_SERVER as $key => $value) {
if (preg_match('/^HTTP_/', $key)) {
// Unmangle the header as best we can.
$key = str_replace('_', ' ', $key);
$key = strtolower($key);
$key = ucwords($key);
$key = str_replace(' ', '-', $key);
$headers[] = array($key, $value);
$seen[$key] = true;
}
}
// In some situations, this may not be mapped into the HTTP_X constants.
// CONTENT_LENGTH is similarly affected, but we trust cURL to take care
// of that if it matters, since we're handing off a request body.
if (empty($seen['Content-Type'])) {
if (isset($_SERVER['CONTENT_TYPE'])) {
$headers[] = array('Content-Type', $_SERVER['CONTENT_TYPE']);
}
}
foreach ($headers as $header) {
list($key, $value) = $header;
switch ($key) {
case 'Host':
case 'Authorization':
// Don't forward these headers, we've already handled them elsewhere.
unset($headers[$key]);
break;
default:
break;
}
}
foreach ($headers as $header) {
list($key, $value) = $header;
$future->addHeader($key, $value);
}
return $future;
}
}
diff --git a/src/aphront/__tests__/AphrontRequestTestCase.php b/src/aphront/__tests__/AphrontRequestTestCase.php
index 151343ef0..88fe97761 100644
--- a/src/aphront/__tests__/AphrontRequestTestCase.php
+++ b/src/aphront/__tests__/AphrontRequestTestCase.php
@@ -1,151 +1,151 @@
<?php
final class AphrontRequestTestCase extends PhabricatorTestCase {
public function testRequestDataAccess() {
$r = new AphrontRequest('example.com', '/');
$r->setRequestData(
array(
'str_empty' => '',
'str' => 'derp',
'str_true' => 'true',
'str_false' => 'false',
'zero' => '0',
'one' => '1',
'arr_empty' => array(),
'arr_num' => array(1, 2, 3),
'comma' => ',',
'comma_1' => 'a, b',
'comma_2' => ' ,a ,, b ,,,, ,, ',
'comma_3' => '0',
'comma_4' => 'a, a, b, a',
'comma_5' => "a\nb, c\n\nd\n\n\n,\n",
));
$this->assertEqual(1, $r->getInt('one'));
$this->assertEqual(0, $r->getInt('zero'));
$this->assertEqual(null, $r->getInt('does-not-exist'));
$this->assertEqual(0, $r->getInt('str_empty'));
$this->assertEqual(true, $r->getBool('one'));
$this->assertEqual(false, $r->getBool('zero'));
$this->assertEqual(true, $r->getBool('str_true'));
$this->assertEqual(false, $r->getBool('str_false'));
$this->assertEqual(true, $r->getBool('str'));
$this->assertEqual(null, $r->getBool('does-not-exist'));
$this->assertEqual(false, $r->getBool('str_empty'));
$this->assertEqual('derp', $r->getStr('str'));
$this->assertEqual('', $r->getStr('str_empty'));
$this->assertEqual(null, $r->getStr('does-not-exist'));
$this->assertEqual(array(), $r->getArr('arr_empty'));
$this->assertEqual(array(1, 2, 3), $r->getArr('arr_num'));
$this->assertEqual(null, $r->getArr('str_empty', null));
$this->assertEqual(null, $r->getArr('str_true', null));
$this->assertEqual(null, $r->getArr('does-not-exist', null));
$this->assertEqual(array(), $r->getArr('does-not-exist'));
$this->assertEqual(array(), $r->getStrList('comma'));
$this->assertEqual(array('a', 'b'), $r->getStrList('comma_1'));
$this->assertEqual(array('a', 'b'), $r->getStrList('comma_2'));
$this->assertEqual(array('0'), $r->getStrList('comma_3'));
$this->assertEqual(array('a', 'a', 'b', 'a'), $r->getStrList('comma_4'));
$this->assertEqual(array('a', 'b', 'c', 'd'), $r->getStrList('comma_5'));
$this->assertEqual(array(), $r->getStrList('does-not-exist'));
$this->assertEqual(null, $r->getStrList('does-not-exist', null));
$this->assertEqual(true, $r->getExists('str'));
$this->assertEqual(false, $r->getExists('does-not-exist'));
}
public function testHostAttacks() {
static $tests = array(
'domain.com' => 'domain.com',
'domain.com:80' => 'domain.com',
'evil.com:evil.com@real.com' => 'real.com',
'evil.com:evil.com@real.com:80' => 'real.com',
);
foreach ($tests as $input => $expect) {
$r = new AphrontRequest($input, '/');
$this->assertEqual(
$expect,
$r->getHost(),
- 'Host: '.$input);
+ pht('Host: %s', $input));
}
}
public function testFlattenRequestData() {
$test_cases = array(
array(
'a' => 'a',
'b' => '1',
'c' => '',
),
array(
'a' => 'a',
'b' => '1',
'c' => '',
),
array(
'x' => array(
0 => 'a',
1 => 'b',
2 => 'c',
),
),
array(
'x[0]' => 'a',
'x[1]' => 'b',
'x[2]' => 'c',
),
array(
'x' => array(
'y' => array(
'z' => array(
40 => 'A',
50 => 'B',
'C' => 60,
),
),
),
),
array(
'x[y][z][40]' => 'A',
'x[y][z][50]' => 'B',
'x[y][z][C]' => '60',
),
);
for ($ii = 0; $ii < count($test_cases); $ii += 2) {
$input = $test_cases[$ii];
$expect = $test_cases[$ii + 1];
$this->assertEqual($expect, AphrontRequest::flattenData($input));
}
}
public function testGetHTTPHeader() {
$server_data = array(
'HTTP_ACCEPT_ENCODING' => 'duck/quack',
'CONTENT_TYPE' => 'cow/moo',
);
$this->assertEqual(
'duck/quack',
AphrontRequest::getHTTPHeader('AcCePt-EncOdING', null, $server_data));
$this->assertEqual(
'cow/moo',
AphrontRequest::getHTTPHeader('cONTent-TyPE', null, $server_data));
$this->assertEqual(
null,
AphrontRequest::getHTTPHeader('Pie-Flavor', null, $server_data));
}
}
diff --git a/src/aphront/configuration/AphrontApplicationConfiguration.php b/src/aphront/configuration/AphrontApplicationConfiguration.php
index d828b9a8c..a3821f18b 100644
--- a/src/aphront/configuration/AphrontApplicationConfiguration.php
+++ b/src/aphront/configuration/AphrontApplicationConfiguration.php
@@ -1,508 +1,512 @@
<?php
/**
* @task routing URI Routing
*/
abstract class AphrontApplicationConfiguration {
private $request;
private $host;
private $path;
private $console;
abstract public function getApplicationName();
abstract public function buildRequest();
abstract public function build404Controller();
abstract public function buildRedirectController($uri, $external);
final public function setRequest(AphrontRequest $request) {
$this->request = $request;
return $this;
}
final public function getRequest() {
return $this->request;
}
final public function getConsole() {
return $this->console;
}
final public function setConsole($console) {
$this->console = $console;
return $this;
}
final public function setHost($host) {
$this->host = $host;
return $this;
}
final public function getHost() {
return $this->host;
}
final public function setPath($path) {
$this->path = $path;
return $this;
}
final public function getPath() {
return $this->path;
}
public function willBuildRequest() {}
/**
* @phutil-external-symbol class PhabricatorStartup
*/
public static function runHTTPRequest(AphrontHTTPSink $sink) {
$multimeter = MultimeterControl::newInstance();
$multimeter->setEventContext('<http-init>');
$multimeter->setEventViewer('<none>');
// Build a no-op write guard for the setup phase. We'll replace this with a
// real write guard later on, but we need to survive setup and build a
// request object first.
$write_guard = new AphrontWriteGuard('id');
PhabricatorEnv::initializeWebEnvironment();
$multimeter->setSampleRate(
PhabricatorEnv::getEnvConfig('debug.sample-rate'));
$debug_time_limit = PhabricatorEnv::getEnvConfig('debug.time-limit');
if ($debug_time_limit) {
PhabricatorStartup::setDebugTimeLimit($debug_time_limit);
}
// This is the earliest we can get away with this, we need env config first.
PhabricatorAccessLog::init();
$access_log = PhabricatorAccessLog::getLog();
PhabricatorStartup::setGlobal('log.access', $access_log);
$access_log->setData(
array(
'R' => AphrontRequest::getHTTPHeader('Referer', '-'),
'r' => idx($_SERVER, 'REMOTE_ADDR', '-'),
'M' => idx($_SERVER, 'REQUEST_METHOD', '-'),
));
DarkConsoleXHProfPluginAPI::hookProfiler();
DarkConsoleErrorLogPluginAPI::registerErrorHandler();
$response = PhabricatorSetupCheck::willProcessRequest();
if ($response) {
PhabricatorStartup::endOutputCapture();
$sink->writeResponse($response);
return;
}
$host = AphrontRequest::getHTTPHeader('Host');
$path = $_REQUEST['__path__'];
switch ($host) {
default:
$config_key = 'aphront.default-application-configuration-class';
$application = PhabricatorEnv::newObjectFromConfig($config_key);
break;
}
$application->setHost($host);
$application->setPath($path);
$application->willBuildRequest();
$request = $application->buildRequest();
// Now that we have a request, convert the write guard into one which
// actually checks CSRF tokens.
$write_guard->dispose();
$write_guard = new AphrontWriteGuard(array($request, 'validateCSRF'));
// Build the server URI implied by the request headers. If an administrator
// has not configured "phabricator.base-uri" yet, we'll use this to generate
// links.
$request_protocol = ($request->isHTTPS() ? 'https' : 'http');
$request_base_uri = "{$request_protocol}://{$host}/";
PhabricatorEnv::setRequestBaseURI($request_base_uri);
$access_log->setData(
array(
'U' => (string)$request->getRequestURI()->getPath(),
));
$processing_exception = null;
try {
$response = $application->processRequest(
$request,
$access_log,
$sink,
$multimeter);
$response_code = $response->getHTTPResponseCode();
} catch (Exception $ex) {
$processing_exception = $ex;
$response_code = 500;
}
$write_guard->dispose();
$access_log->setData(
array(
'c' => $response_code,
'T' => PhabricatorStartup::getMicrosecondsSinceStart(),
));
$multimeter->newEvent(
MultimeterEvent::TYPE_REQUEST_TIME,
$multimeter->getEventContext(),
PhabricatorStartup::getMicrosecondsSinceStart());
$access_log->write();
$multimeter->saveEvents();
DarkConsoleXHProfPluginAPI::saveProfilerSample($access_log);
// Add points to the rate limits for this request.
if (isset($_SERVER['REMOTE_ADDR'])) {
$user_ip = $_SERVER['REMOTE_ADDR'];
// The base score for a request allows users to make 30 requests per
// minute.
$score = (1000 / 30);
// If the user was logged in, let them make more requests.
if ($request->getUser() && $request->getUser()->getPHID()) {
$score = $score / 5;
}
PhabricatorStartup::addRateLimitScore($user_ip, $score);
}
if ($processing_exception) {
throw $processing_exception;
}
}
public function processRequest(
AphrontRequest $request,
PhutilDeferredLog $access_log,
AphrontHTTPSink $sink,
MultimeterControl $multimeter) {
$this->setRequest($request);
list($controller, $uri_data) = $this->buildController();
$controller_class = get_class($controller);
$access_log->setData(
array(
'C' => $controller_class,
));
$multimeter->setEventContext('web.'.$controller_class);
$request->setURIMap($uri_data);
$controller->setRequest($request);
// If execution throws an exception and then trying to render that
// exception throws another exception, we want to show the original
// exception, as it is likely the root cause of the rendering exception.
$original_exception = null;
try {
$response = $controller->willBeginExecution();
if ($request->getUser() && $request->getUser()->getPHID()) {
$access_log->setData(
array(
'u' => $request->getUser()->getUserName(),
'P' => $request->getUser()->getPHID(),
));
$multimeter->setEventViewer('user.'.$request->getUser()->getPHID());
}
if (!$response) {
$controller->willProcessRequest($uri_data);
$response = $controller->handleRequest($request);
}
} catch (Exception $ex) {
$original_exception = $ex;
$response = $this->handleException($ex);
}
try {
$response = $controller->didProcessRequest($response);
$response = $this->willSendResponse($response, $controller);
$response->setRequest($request);
$unexpected_output = PhabricatorStartup::endOutputCapture();
if ($unexpected_output) {
$unexpected_output = pht(
"Unexpected output:\n\n%s",
$unexpected_output);
phlog($unexpected_output);
if ($response instanceof AphrontWebpageResponse) {
echo phutil_tag(
'div',
array('style' =>
'background: #eeddff;'.
'white-space: pre-wrap;'.
'z-index: 200000;'.
'position: relative;'.
'padding: 8px;'.
'font-family: monospace',
),
$unexpected_output);
}
}
$sink->writeResponse($response);
} catch (Exception $ex) {
if ($original_exception) {
throw $original_exception;
}
throw $ex;
}
return $response;
}
/* -( URI Routing )-------------------------------------------------------- */
/**
* Using builtin and application routes, build the appropriate
* @{class:AphrontController} class for the request. To route a request, we
* first test if the HTTP_HOST is configured as a valid Phabricator URI. If
* it isn't, we do a special check to see if it's a custom domain for a blog
* in the Phame application and if that fails we error. Otherwise, we test
* against all application routes from installed
* @{class:PhabricatorApplication}s.
*
* If we match a route, we construct the controller it points at, build it,
* and return it.
*
* If we fail to match a route, but the current path is missing a trailing
* "/", we try routing the same path with a trailing "/" and do a redirect
* if that has a valid route. The idea is to canoncalize URIs for consistency,
* but avoid breaking noncanonical URIs that we can easily salvage.
*
* NOTE: We only redirect on GET. On POST, we'd drop parameters and most
* likely mutate the request implicitly, and a bad POST usually indicates a
* programming error rather than a sloppy typist.
*
* If the failing path already has a trailing "/", or we can't route the
* version with a "/", we call @{method:build404Controller}, which build a
* fallback @{class:AphrontController}.
*
* @return pair<AphrontController,dict> Controller and dictionary of request
* parameters.
* @task routing
*/
final public function buildController() {
$request = $this->getRequest();
// If we're configured to operate in cluster mode, reject requests which
// were not received on a cluster interface.
//
// For example, a host may have an internal address like "170.0.0.1", and
// also have a public address like "51.23.95.16". Assuming the cluster
// is configured on a range like "170.0.0.0/16", we want to reject the
// requests received on the public interface.
//
// Ideally, nodes in a cluster should only be listening on internal
// interfaces, but they may be configured in such a way that they also
// listen on external interfaces, since this is easy to forget about or
// get wrong. As a broad security measure, reject requests received on any
// interfaces which aren't on the whitelist.
$cluster_addresses = PhabricatorEnv::getEnvConfig('cluster.addresses');
if ($cluster_addresses) {
$server_addr = idx($_SERVER, 'SERVER_ADDR');
if (!$server_addr) {
if (php_sapi_name() == 'cli') {
// This is a command line script (probably something like a unit
// test) so it's fine that we don't have SERVER_ADDR defined.
} else {
throw new AphrontUsageException(
- pht('No SERVER_ADDR'),
+ pht('No %s', 'SERVER_ADDR'),
pht(
'Phabricator is configured to operate in cluster mode, but '.
- 'SERVER_ADDR is not defined in the request context. Your '.
- 'webserver configuration needs to forward SERVER_ADDR to '.
- 'PHP so Phabricator can reject requests received on '.
- 'external interfaces.'));
+ '%s is not defined in the request context. Your webserver '.
+ 'configuration needs to forward %s to PHP so Phabricator can '.
+ 'reject requests received on external interfaces.',
+ 'SERVER_ADDR',
+ 'SERVER_ADDR'));
}
} else {
if (!PhabricatorEnv::isClusterAddress($server_addr)) {
throw new AphrontUsageException(
pht('External Interface'),
pht(
'Phabricator is configured in cluster mode and the address '.
'this request was received on ("%s") is not whitelisted as '.
'a cluster address.',
$server_addr));
}
}
}
if (PhabricatorEnv::getEnvConfig('security.require-https')) {
if (!$request->isHTTPS()) {
$https_uri = $request->getRequestURI();
$https_uri->setDomain($request->getHost());
$https_uri->setProtocol('https');
// In this scenario, we'll be redirecting to HTTPS using an absolute
// URI, so we need to permit an external redirect.
return $this->buildRedirectController($https_uri, true);
}
}
$path = $request->getPath();
$host = $request->getHost();
$base_uri = PhabricatorEnv::getEnvConfig('phabricator.base-uri');
$prod_uri = PhabricatorEnv::getEnvConfig('phabricator.production-uri');
$file_uri = PhabricatorEnv::getEnvConfig(
'security.alternate-file-domain');
$allowed_uris = PhabricatorEnv::getEnvConfig('phabricator.allowed-uris');
$uris = array_merge(
array(
$base_uri,
$prod_uri,
),
$allowed_uris);
$cdn_routes = array(
'/res/',
'/file/data/',
'/file/xform/',
'/phame/r/',
);
$host_match = false;
foreach ($uris as $uri) {
if ($host === id(new PhutilURI($uri))->getDomain()) {
$host_match = true;
break;
}
}
if (!$host_match) {
if ($host === id(new PhutilURI($file_uri))->getDomain()) {
foreach ($cdn_routes as $route) {
if (strncmp($path, $route, strlen($route)) == 0) {
$host_match = true;
break;
}
}
}
}
// NOTE: If the base URI isn't defined yet, don't activate alternate
// domains.
if ($base_uri && !$host_match) {
try {
$blog = id(new PhameBlogQuery())
->setViewer(new PhabricatorUser())
->withDomain($host)
->executeOne();
} catch (PhabricatorPolicyException $ex) {
throw new Exception(
- 'This blog is not visible to logged out users, so it can not be '.
- 'visited from a custom domain.');
+ pht(
+ 'This blog is not visible to logged out users, so it can not be '.
+ 'visited from a custom domain.'));
}
if (!$blog) {
if ($prod_uri && $prod_uri != $base_uri) {
- $prod_str = ' or '.$prod_uri;
+ $prod_str = pht('%s or %s', $base_uri, $prod_uri);
} else {
- $prod_str = '';
+ $prod_str = $base_uri;
}
throw new Exception(
- 'Specified domain '.$host.' is not configured for Phabricator '.
- 'requests. Please use '.$base_uri.$prod_str.' to visit this instance.'
- );
+ pht(
+ 'Specified domain %s is not configured for Phabricator '.
+ 'requests. Please use %s to visit this instance.',
+ $host,
+ $prod_str));
}
// TODO: Make this more flexible and modular so any application can
// do crazy stuff here if it wants.
$path = '/phame/live/'.$blog->getID().'/'.$path;
}
list($controller, $uri_data) = $this->buildControllerForPath($path);
if (!$controller) {
if (!preg_match('@/$@', $path)) {
// If we failed to match anything but don't have a trailing slash, try
// to add a trailing slash and issue a redirect if that resolves.
list($controller, $uri_data) = $this->buildControllerForPath($path.'/');
// NOTE: For POST, just 404 instead of redirecting, since the redirect
// will be a GET without parameters.
if ($controller && !$request->isHTTPPost()) {
$slash_uri = $request->getRequestURI()->setPath($path.'/');
$external = strlen($request->getRequestURI()->getDomain());
return $this->buildRedirectController($slash_uri, $external);
}
}
return $this->build404Controller();
}
return array($controller, $uri_data);
}
/**
* Map a specific path to the corresponding controller. For a description
* of routing, see @{method:buildController}.
*
* @return pair<AphrontController,dict> Controller and dictionary of request
* parameters.
* @task routing
*/
final public function buildControllerForPath($path) {
$maps = array();
$applications = PhabricatorApplication::getAllInstalledApplications();
foreach ($applications as $application) {
$maps[] = array($application, $application->getRoutes());
}
$current_application = null;
$controller_class = null;
foreach ($maps as $map_info) {
list($application, $map) = $map_info;
$mapper = new AphrontURIMapper($map);
list($controller_class, $uri_data) = $mapper->mapPath($path);
if ($controller_class) {
if ($application) {
$current_application = $application;
}
break;
}
}
if (!$controller_class) {
return array(null, null);
}
$request = $this->getRequest();
$controller = newv($controller_class, array());
if ($current_application) {
$controller->setCurrentApplication($current_application);
}
return array($controller, $uri_data);
}
}
diff --git a/src/aphront/configuration/AphrontDefaultApplicationConfiguration.php b/src/aphront/configuration/AphrontDefaultApplicationConfiguration.php
index 063ee365d..3d5bfffd2 100644
--- a/src/aphront/configuration/AphrontDefaultApplicationConfiguration.php
+++ b/src/aphront/configuration/AphrontDefaultApplicationConfiguration.php
@@ -1,291 +1,290 @@
<?php
/**
* NOTE: Do not extend this!
*
* @concrete-extensible
*/
class AphrontDefaultApplicationConfiguration
extends AphrontApplicationConfiguration {
public function __construct() {}
public function getApplicationName() {
return 'aphront-default';
}
/**
* @phutil-external-symbol class PhabricatorStartup
*/
public function buildRequest() {
$parser = new PhutilQueryStringParser();
$data = array();
// If the request has "multipart/form-data" content, we can't use
// PhutilQueryStringParser to parse it, and the raw data supposedly is not
// available anyway (according to the PHP documentation, "php://input" is
// not available for "multipart/form-data" requests). However, it is
// available at least some of the time (see T3673), so double check that
// we aren't trying to parse data we won't be able to parse correctly by
// examining the Content-Type header.
$content_type = idx($_SERVER, 'CONTENT_TYPE');
$is_form_data = preg_match('@^multipart/form-data@i', $content_type);
$raw_input = PhabricatorStartup::getRawInput();
if (strlen($raw_input) && !$is_form_data) {
$data += $parser->parseQueryString($raw_input);
} else if ($_POST) {
$data += $_POST;
}
$data += $parser->parseQueryString(idx($_SERVER, 'QUERY_STRING', ''));
$cookie_prefix = PhabricatorEnv::getEnvConfig('phabricator.cookie-prefix');
$request = new AphrontRequest($this->getHost(), $this->getPath());
$request->setRequestData($data);
$request->setApplicationConfiguration($this);
$request->setCookiePrefix($cookie_prefix);
return $request;
}
public function handleException(Exception $ex) {
$request = $this->getRequest();
// For Conduit requests, return a Conduit response.
if ($request->isConduit()) {
$response = new ConduitAPIResponse();
$response->setErrorCode(get_class($ex));
$response->setErrorInfo($ex->getMessage());
return id(new AphrontJSONResponse())
->setAddJSONShield(false)
->setContent($response->toDictionary());
}
// For non-workflow requests, return a Ajax response.
if ($request->isAjax() && !$request->isWorkflow()) {
// Log these; they don't get shown on the client and can be difficult
// to debug.
phlog($ex);
$response = new AphrontAjaxResponse();
$response->setError(
array(
'code' => get_class($ex),
'info' => $ex->getMessage(),
));
return $response;
}
$user = $request->getUser();
if (!$user) {
// If we hit an exception very early, we won't have a user.
$user = new PhabricatorUser();
}
if ($ex instanceof PhabricatorSystemActionRateLimitException) {
$dialog = id(new AphrontDialogView())
->setTitle(pht('Slow Down!'))
->setUser($user)
->setErrors(array(pht('You are being rate limited.')))
->appendParagraph($ex->getMessage())
->appendParagraph($ex->getRateExplanation())
->addCancelButton('/', pht('Okaaaaaaaaaaaaaay...'));
$response = new AphrontDialogResponse();
$response->setDialog($dialog);
return $response;
}
if ($ex instanceof PhabricatorAuthHighSecurityRequiredException) {
$form = id(new PhabricatorAuthSessionEngine())->renderHighSecurityForm(
$ex->getFactors(),
$ex->getFactorValidationResults(),
$user,
$request);
$dialog = id(new AphrontDialogView())
->setUser($user)
->setTitle(pht('Entering High Security'))
->setShortTitle(pht('Security Checkpoint'))
->setWidth(AphrontDialogView::WIDTH_FORM)
->addHiddenInput(AphrontRequest::TYPE_HISEC, true)
->setErrors(
array(
pht(
'You are taking an action which requires you to enter '.
'high security.'),
))
->appendParagraph(
pht(
'High security mode helps protect your account from security '.
'threats, like session theft or someone messing with your stuff '.
'while you\'re grabbing a coffee. To enter high security mode, '.
'confirm your credentials.'))
->appendChild($form->buildLayoutView())
->appendParagraph(
pht(
'Your account will remain in high security mode for a short '.
'period of time. When you are finished taking sensitive '.
'actions, you should leave high security.'))
->setSubmitURI($request->getPath())
->addCancelButton($ex->getCancelURI())
->addSubmitButton(pht('Enter High Security'));
$request_parameters = $request->getPassthroughRequestParameters(
$respect_quicksand = true);
foreach ($request_parameters as $key => $value) {
$dialog->addHiddenInput($key, $value);
}
$response = new AphrontDialogResponse();
$response->setDialog($dialog);
return $response;
}
if ($ex instanceof PhabricatorPolicyException) {
if (!$user->isLoggedIn()) {
// If the user isn't logged in, just give them a login form. This is
// probably a generally more useful response than a policy dialog that
// they have to click through to get a login form.
//
// Possibly we should add a header here like "you need to login to see
// the thing you are trying to look at".
$login_controller = new PhabricatorAuthStartController();
$login_controller->setRequest($request);
$auth_app_class = 'PhabricatorAuthApplication';
$auth_app = PhabricatorApplication::getByClass($auth_app_class);
$login_controller->setCurrentApplication($auth_app);
return $login_controller->handleRequest($request);
}
$list = $ex->getMoreInfo();
foreach ($list as $key => $item) {
$list[$key] = phutil_tag('li', array(), $item);
}
if ($list) {
$list = phutil_tag('ul', array(), $list);
}
$content = array(
phutil_tag(
'div',
array(
'class' => 'aphront-policy-rejection',
),
$ex->getRejection()),
phutil_tag(
'div',
array(
'class' => 'aphront-capability-details',
),
pht('Users with the "%s" capability:', $ex->getCapabilityName())),
$list,
);
$dialog = new AphrontDialogView();
$dialog
->setTitle($ex->getTitle())
->setClass('aphront-access-dialog')
->setUser($user)
->appendChild($content);
if ($this->getRequest()->isAjax()) {
$dialog->addCancelButton('/', pht('Close'));
} else {
$dialog->addCancelButton('/', pht('OK'));
}
$response = new AphrontDialogResponse();
$response->setDialog($dialog);
return $response;
}
if ($ex instanceof AphrontUsageException) {
$error = new PHUIInfoView();
$error->setTitle($ex->getTitle());
$error->appendChild($ex->getMessage());
$view = new PhabricatorStandardPageView();
$view->setRequest($this->getRequest());
$view->appendChild($error);
$response = new AphrontWebpageResponse();
$response->setContent($view->render());
$response->setHTTPResponseCode(500);
return $response;
}
// Always log the unhandled exception.
phlog($ex);
$class = get_class($ex);
$message = $ex->getMessage();
if ($ex instanceof AphrontSchemaQueryException) {
- $message .=
- "\n\n".
+ $message .= "\n\n".pht(
"NOTE: This usually indicates that the MySQL schema has not been ".
- "properly upgraded. Run 'bin/storage upgrade' to ensure your ".
- "schema is up to date.";
+ "properly upgraded. Run '%s' to ensure your schema is up to date.",
+ 'bin/storage upgrade');
}
if (PhabricatorEnv::getEnvConfig('phabricator.developer-mode')) {
$trace = id(new AphrontStackTraceView())
->setUser($user)
->setTrace($ex->getTrace());
} else {
$trace = null;
}
$content = phutil_tag(
'div',
array('class' => 'aphront-unhandled-exception'),
array(
phutil_tag('div', array('class' => 'exception-message'), $message),
$trace,
));
$dialog = new AphrontDialogView();
$dialog
- ->setTitle('Unhandled Exception ("'.$class.'")')
+ ->setTitle(pht('Unhandled Exception ("%s")', $class))
->setClass('aphront-exception-dialog')
->setUser($user)
->appendChild($content);
if ($this->getRequest()->isAjax()) {
- $dialog->addCancelButton('/', 'Close');
+ $dialog->addCancelButton('/', pht('Close'));
}
$response = new AphrontDialogResponse();
$response->setDialog($dialog);
$response->setHTTPResponseCode(500);
return $response;
}
public function willSendResponse(AphrontResponse $response) {
return $response;
}
public function build404Controller() {
return array(new Phabricator404Controller(), array());
}
public function buildRedirectController($uri, $external) {
return array(
new PhabricatorRedirectController(),
array(
'uri' => $uri,
'external' => $external,
),
);
}
}
diff --git a/src/aphront/response/Aphront404Response.php b/src/aphront/response/Aphront404Response.php
index 85dec3055..1284cb62e 100644
--- a/src/aphront/response/Aphront404Response.php
+++ b/src/aphront/response/Aphront404Response.php
@@ -1,30 +1,31 @@
<?php
final class Aphront404Response extends AphrontHTMLResponse {
public function getHTTPResponseCode() {
return 404;
}
public function buildResponseString() {
$request = $this->getRequest();
$user = $request->getUser();
$dialog = id(new AphrontDialogView())
->setUser($user)
->setTitle(pht('404 Not Found'))
->addCancelButton('/', pht('Focus'))
- ->appendParagraph(pht(
- 'Do not dwell in the past, do not dream of the future, '.
- 'concentrate the mind on the present moment.'));
+ ->appendParagraph(
+ pht(
+ 'Do not dwell in the past, do not dream of the future, '.
+ 'concentrate the mind on the present moment.'));
$view = id(new PhabricatorStandardPageView())
- ->setTitle('404 Not Found')
+ ->setTitle(pht('404 Not Found'))
->setRequest($request)
->setDeviceReady(true)
->appendChild($dialog);
return $view->render();
}
}
diff --git a/src/aphront/sink/AphrontHTTPSink.php b/src/aphront/sink/AphrontHTTPSink.php
index 72bf4244f..6ed2edff9 100644
--- a/src/aphront/sink/AphrontHTTPSink.php
+++ b/src/aphront/sink/AphrontHTTPSink.php
@@ -1,132 +1,134 @@
<?php
/**
* Abstract class which wraps some sort of output mechanism for HTTP responses.
* Normally this is just @{class:AphrontPHPHTTPSink}, which uses "echo" and
* "header()" to emit responses.
*
* Mostly, this class allows us to do install security or metrics hooks in the
* output pipeline.
*
* @task write Writing Response Components
* @task emit Emitting the Response
*/
abstract class AphrontHTTPSink {
/* -( Writing Response Components )---------------------------------------- */
/**
* Write an HTTP status code to the output.
*
* @param int Numeric HTTP status code.
* @return void
*/
final public function writeHTTPStatus($code, $message = '') {
if (!preg_match('/^\d{3}$/', $code)) {
- throw new Exception("Malformed HTTP status code '{$code}'!");
+ throw new Exception(pht("Malformed HTTP status code '%s'!", $code));
}
$code = (int)$code;
$this->emitHTTPStatus($code, $message);
}
/**
* Write HTTP headers to the output.
*
* @param list<pair> List of <name, value> pairs.
* @return void
*/
final public function writeHeaders(array $headers) {
foreach ($headers as $header) {
if (!is_array($header) || count($header) !== 2) {
- throw new Exception('Malformed header.');
+ throw new Exception(pht('Malformed header.'));
}
list($name, $value) = $header;
if (strpos($name, ':') !== false) {
throw new Exception(
- 'Declining to emit response with malformed HTTP header name: '.
- $name);
+ pht(
+ 'Declining to emit response with malformed HTTP header name: %s',
+ $name));
}
// Attackers may perform an "HTTP response splitting" attack by making
// the application emit certain types of headers containing newlines:
//
// http://en.wikipedia.org/wiki/HTTP_response_splitting
//
// PHP has built-in protections against HTTP response-splitting, but they
// are of dubious trustworthiness:
//
// http://news.php.net/php.internals/57655
if (preg_match('/[\r\n\0]/', $name.$value)) {
throw new Exception(
- "Declining to emit response with unsafe HTTP header: ".
- "<'".$name."', '".$value."'>.");
+ pht(
+ 'Declining to emit response with unsafe HTTP header: %s',
+ "<'".$name."', '".$value."'>."));
}
}
foreach ($headers as $header) {
list($name, $value) = $header;
$this->emitHeader($name, $value);
}
}
/**
* Write HTTP body data to the output.
*
* @param string Body data.
* @return void
*/
final public function writeData($data) {
$this->emitData($data);
}
/**
* Write an entire @{class:AphrontResponse} to the output.
*
* @param AphrontResponse The response object to write.
* @return void
*/
final public function writeResponse(AphrontResponse $response) {
// Build the content iterator first, in case it throws. Ideally, we'd
// prefer to handle exceptions before we emit the response status or any
// HTTP headers.
$data = $response->getContentIterator();
$all_headers = array_merge(
$response->getHeaders(),
$response->getCacheHeaders());
$this->writeHTTPStatus(
$response->getHTTPResponseCode(),
$response->getHTTPResponseMessage());
$this->writeHeaders($all_headers);
$abort = false;
foreach ($data as $block) {
if (!$this->isWritable()) {
$abort = true;
break;
}
$this->writeData($block);
}
$response->didCompleteWrite($abort);
}
/* -( Emitting the Response )---------------------------------------------- */
abstract protected function emitHTTPStatus($code, $message = '');
abstract protected function emitHeader($name, $value);
abstract protected function emitData($data);
abstract protected function isWritable();
}
diff --git a/src/applications/almanac/controller/AlmanacConsoleController.php b/src/applications/almanac/controller/AlmanacConsoleController.php
index 60fa4e010..156bd14b7 100644
--- a/src/applications/almanac/controller/AlmanacConsoleController.php
+++ b/src/applications/almanac/controller/AlmanacConsoleController.php
@@ -1,60 +1,54 @@
<?php
final class AlmanacConsoleController extends AlmanacController {
public function shouldAllowPublic() {
return true;
}
public function handleRequest(AphrontRequest $request) {
$viewer = $request->getViewer();
$menu = id(new PHUIObjectItemListView())
->setUser($viewer)
->setStackable(true);
$menu->addItem(
id(new PHUIObjectItemView())
->setHeader(pht('Services'))
->setHref($this->getApplicationURI('service/'))
->setFontIcon('fa-plug')
- ->addAttribute(
- pht(
- 'Manage Almanac services.')));
+ ->addAttribute(pht('Manage Almanac services.')));
$menu->addItem(
id(new PHUIObjectItemView())
->setHeader(pht('Devices'))
->setHref($this->getApplicationURI('device/'))
->setFontIcon('fa-server')
- ->addAttribute(
- pht(
- 'Manage Almanac devices.')));
+ ->addAttribute(pht('Manage Almanac devices.')));
$menu->addItem(
id(new PHUIObjectItemView())
->setHeader(pht('Networks'))
->setHref($this->getApplicationURI('network/'))
->setFontIcon('fa-globe')
- ->addAttribute(
- pht(
- 'Manage Almanac networks.')));
+ ->addAttribute(pht('Manage Almanac networks.')));
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb(pht('Console'));
$box = id(new PHUIObjectBoxView())
->setHeaderText('Console')
->appendChild($menu);
return $this->buildApplicationPage(
array(
$crumbs,
$box,
),
array(
'title' => pht('Almanac Console'),
));
}
}
diff --git a/src/applications/almanac/controller/AlmanacServiceEditController.php b/src/applications/almanac/controller/AlmanacServiceEditController.php
index 13f31430e..be3fe0200 100644
--- a/src/applications/almanac/controller/AlmanacServiceEditController.php
+++ b/src/applications/almanac/controller/AlmanacServiceEditController.php
@@ -1,257 +1,253 @@
<?php
final class AlmanacServiceEditController
extends AlmanacServiceController {
public function handleRequest(AphrontRequest $request) {
$viewer = $request->getViewer();
$list_uri = $this->getApplicationURI('service/');
$id = $request->getURIData('id');
if ($id) {
$service = id(new AlmanacServiceQuery())
->setViewer($viewer)
->withIDs(array($id))
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
if (!$service) {
return new Aphront404Response();
}
$is_new = false;
$service_uri = $service->getURI();
$cancel_uri = $service_uri;
$title = pht('Edit Service');
$save_button = pht('Save Changes');
} else {
$cancel_uri = $list_uri;
$this->requireApplicationCapability(
AlmanacCreateServicesCapability::CAPABILITY);
$service_class = $request->getStr('serviceClass');
$service_types = AlmanacServiceType::getAllServiceTypes();
if (empty($service_types[$service_class])) {
return $this->buildServiceTypeResponse($service_types, $cancel_uri);
}
$service_type = $service_types[$service_class];
if ($service_type->isClusterServiceType()) {
$this->requireApplicationCapability(
AlmanacCreateClusterServicesCapability::CAPABILITY);
}
$service = AlmanacService::initializeNewService();
$service->setServiceClass($service_class);
$service->attachServiceType($service_type);
$is_new = true;
$title = pht('Create Service');
$save_button = pht('Create Service');
}
$v_name = $service->getName();
$e_name = true;
$validation_exception = null;
if ($is_new) {
$v_projects = array();
} else {
$v_projects = PhabricatorEdgeQuery::loadDestinationPHIDs(
$service->getPHID(),
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST);
$v_projects = array_reverse($v_projects);
}
if ($request->isFormPost() && $request->getStr('edit')) {
$v_name = $request->getStr('name');
$v_view = $request->getStr('viewPolicy');
$v_edit = $request->getStr('editPolicy');
$v_projects = $request->getArr('projects');
$type_name = AlmanacServiceTransaction::TYPE_NAME;
$type_view = PhabricatorTransactions::TYPE_VIEW_POLICY;
$type_edit = PhabricatorTransactions::TYPE_EDIT_POLICY;
$xactions = array();
$xactions[] = id(new AlmanacServiceTransaction())
->setTransactionType($type_name)
->setNewValue($v_name);
$xactions[] = id(new AlmanacServiceTransaction())
->setTransactionType($type_view)
->setNewValue($v_view);
$xactions[] = id(new AlmanacServiceTransaction())
->setTransactionType($type_edit)
->setNewValue($v_edit);
$proj_edge_type = PhabricatorProjectObjectHasProjectEdgeType::EDGECONST;
$xactions[] = id(new AlmanacServiceTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
->setMetadataValue('edge:type', $proj_edge_type)
->setNewValue(array('=' => array_fuse($v_projects)));
$editor = id(new AlmanacServiceEditor())
->setActor($viewer)
->setContentSourceFromRequest($request)
->setContinueOnNoEffect(true);
try {
$editor->applyTransactions($service, $xactions);
$service_uri = $service->getURI();
return id(new AphrontRedirectResponse())->setURI($service_uri);
} catch (PhabricatorApplicationTransactionValidationException $ex) {
$validation_exception = $ex;
$e_name = $ex->getShortMessage($type_name);
$service->setViewPolicy($v_view);
$service->setEditPolicy($v_edit);
}
}
$policies = id(new PhabricatorPolicyQuery())
->setViewer($viewer)
->setObject($service)
->execute();
$form = id(new AphrontFormView())
->setUser($viewer)
->addHiddenInput('edit', true)
->addHiddenInput('serviceClass', $service->getServiceClass())
->appendChild(
id(new AphrontFormTextControl())
->setLabel(pht('Name'))
->setName('name')
->setValue($v_name)
->setError($e_name))
->appendChild(
id(new AphrontFormPolicyControl())
->setName('viewPolicy')
->setPolicyObject($service)
->setCapability(PhabricatorPolicyCapability::CAN_VIEW)
->setPolicies($policies))
->appendChild(
id(new AphrontFormPolicyControl())
->setName('editPolicy')
->setPolicyObject($service)
->setCapability(PhabricatorPolicyCapability::CAN_EDIT)
->setPolicies($policies))
->appendControl(
id(new AphrontFormTokenizerControl())
->setLabel(pht('Projects'))
->setName('projects')
->setValue($v_projects)
->setDatasource(new PhabricatorProjectDatasource()))
->appendChild(
id(new AphrontFormSubmitControl())
->addCancelButton($cancel_uri)
->setValue($save_button));
$box = id(new PHUIObjectBoxView())
->setValidationException($validation_exception)
->setHeaderText($title)
->appendChild($form);
$crumbs = $this->buildApplicationCrumbs();
if ($is_new) {
$crumbs->addTextCrumb(pht('Create Service'));
} else {
$crumbs->addTextCrumb($service->getName(), $service_uri);
$crumbs->addTextCrumb(pht('Edit'));
}
return $this->buildApplicationPage(
array(
$crumbs,
$box,
),
array(
'title' => $title,
));
}
private function buildServiceTypeResponse(array $service_types, $cancel_uri) {
$request = $this->getRequest();
$viewer = $this->getViewer();
$e_service = null;
$errors = array();
if ($request->isFormPost()) {
$e_service = pht('Required');
$errors[] = pht(
'To create a new service, you must select a service type.');
}
list($can_cluster, $cluster_link) = $this->explainApplicationCapability(
AlmanacCreateClusterServicesCapability::CAPABILITY,
pht('You have permission to create cluster services.'),
pht('You do not have permission to create new cluster services.'));
$type_control = id(new AphrontFormRadioButtonControl())
->setLabel(pht('Service Type'))
->setName('serviceClass')
->setError($e_service);
foreach ($service_types as $service_type) {
$is_cluster = $service_type->isClusterServiceType();
$is_disabled = ($is_cluster && !$can_cluster);
if ($is_cluster) {
$extra = $cluster_link;
} else {
$extra = null;
}
$type_control->addButton(
get_class($service_type),
$service_type->getServiceTypeName(),
array(
$service_type->getServiceTypeDescription(),
$extra,
),
$is_disabled ? 'disabled' : null,
$is_disabled);
}
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb(pht('Create Service'));
$title = pht('Choose Service Type');
$form = id(new AphrontFormView())
->setUser($viewer)
->appendChild($type_control)
->appendChild(
id(new AphrontFormSubmitControl())
->setValue(pht('Continue'))
->addCancelButton($cancel_uri));
$box = id(new PHUIObjectBoxView())
->setFormErrors($errors)
->setHeaderText($title)
->appendChild($form);
return $this->buildApplicationPage(
array(
$crumbs,
$box,
),
array(
'title' => $title,
));
}
-
-
-
-
}
diff --git a/src/applications/aphlict/management/PhabricatorAphlictManagementStatusWorkflow.php b/src/applications/aphlict/management/PhabricatorAphlictManagementStatusWorkflow.php
index 145a5fcff..85f80bfad 100644
--- a/src/applications/aphlict/management/PhabricatorAphlictManagementStatusWorkflow.php
+++ b/src/applications/aphlict/management/PhabricatorAphlictManagementStatusWorkflow.php
@@ -1,26 +1,26 @@
<?php
final class PhabricatorAphlictManagementStatusWorkflow
extends PhabricatorAphlictManagementWorkflow {
protected function didConstruct() {
$this
->setName('status')
->setSynopsis(pht('Show the status of the notifications server.'))
->setArguments(array());
}
public function execute(PhutilArgumentParser $args) {
$console = PhutilConsole::getConsole();
$pid = $this->getPID();
if (!$pid) {
- $console->writeErr(pht("Aphlict is not running.\n"));
+ $console->writeErr("%s\n", pht('Aphlict is not running.'));
return 1;
}
- $console->writeOut(pht("Aphlict (%s) is running.\n", $pid));
+ $console->writeOut("%s\n", pht('Aphlict (%s) is running.', $pid));
return 0;
}
}
diff --git a/src/applications/aphlict/management/PhabricatorAphlictManagementWorkflow.php b/src/applications/aphlict/management/PhabricatorAphlictManagementWorkflow.php
index 1c8d855a8..d5d751363 100644
--- a/src/applications/aphlict/management/PhabricatorAphlictManagementWorkflow.php
+++ b/src/applications/aphlict/management/PhabricatorAphlictManagementWorkflow.php
@@ -1,314 +1,330 @@
<?php
abstract class PhabricatorAphlictManagementWorkflow
extends PhabricatorManagementWorkflow {
private $debug = false;
private $clientHost;
private $clientPort;
protected function didConstruct() {
$this
->setArguments(
array(
array(
'name' => 'client-host',
'param' => 'hostname',
'help' => pht('Hostname to bind to for the client server.'),
),
array(
'name' => 'client-port',
'param' => 'port',
'help' => pht('Port to bind to for the client server.'),
),
));
}
public function execute(PhutilArgumentParser $args) {
$this->clientHost = $args->getArg('client-host');
$this->clientPort = $args->getArg('client-port');
return 0;
}
final public function getPIDPath() {
$path = PhabricatorEnv::getEnvConfig('notification.pidfile');
try {
$dir = dirname($path);
if (!Filesystem::pathExists($dir)) {
Filesystem::createDirectory($dir, 0755, true);
}
} catch (FilesystemException $ex) {
throw new Exception(
pht(
"Failed to create '%s'. You should manually create this directory.",
$dir));
}
return $path;
}
final public function getLogPath() {
$path = PhabricatorEnv::getEnvConfig('notification.log');
try {
$dir = dirname($path);
if (!Filesystem::pathExists($dir)) {
Filesystem::createDirectory($dir, 0755, true);
}
} catch (FilesystemException $ex) {
throw new Exception(
pht(
"Failed to create '%s'. You should manually create this directory.",
$dir));
}
return $path;
}
final public function getPID() {
$pid = null;
if (Filesystem::pathExists($this->getPIDPath())) {
$pid = (int)Filesystem::readFile($this->getPIDPath());
}
return $pid;
}
final public function cleanup($signo = '?') {
global $g_future;
if ($g_future) {
$g_future->resolveKill();
$g_future = null;
}
Filesystem::remove($this->getPIDPath());
exit(1);
}
protected final function setDebug($debug) {
$this->debug = $debug;
}
public static function requireExtensions() {
self::mustHaveExtension('pcntl');
self::mustHaveExtension('posix');
}
private static function mustHaveExtension($ext) {
if (!extension_loaded($ext)) {
- echo "ERROR: The PHP extension '{$ext}' is not installed. You must ".
- "install it to run aphlict on this machine.\n";
+ echo pht(
+ "ERROR: The PHP extension '%s' is not installed. You must ".
+ "install it to run Aphlict on this machine.",
+ $ext)."\n";
exit(1);
}
$extension = new ReflectionExtension($ext);
foreach ($extension->getFunctions() as $function) {
$function = $function->name;
if (!function_exists($function)) {
- echo "ERROR: The PHP function {$function}() is disabled. You must ".
- "enable it to run aphlict on this machine.\n";
+ echo pht(
+ 'ERROR: The PHP function %s is disabled. You must '.
+ 'enable it to run Aphlict on this machine.',
+ $function.'()')."\n";
exit(1);
}
}
}
final protected function willLaunch() {
$console = PhutilConsole::getConsole();
$pid = $this->getPID();
if ($pid) {
throw new PhutilArgumentUsageException(
pht(
'Unable to start notifications server because it is already '.
- 'running. Use `aphlict restart` to restart it.'));
+ 'running. Use `%s` to restart it.',
+ 'aphlict restart'));
}
if (posix_getuid() == 0) {
throw new PhutilArgumentUsageException(
pht(
// TODO: Update this message after a while.
'The notification server should not be run as root. It no '.
'longer requires access to privileged ports.'));
}
// Make sure we can write to the PID file.
if (!$this->debug) {
Filesystem::writeFile($this->getPIDPath(), '');
}
// First, start the server in configuration test mode with --test. This
// will let us error explicitly if there are missing modules, before we
// fork and lose access to the console.
$test_argv = $this->getServerArgv();
$test_argv[] = '--test=true';
execx(
'%s %s %Ls',
$this->getNodeBinary(),
$this->getAphlictScriptPath(),
$test_argv);
}
private function getServerArgv() {
$ssl_key = PhabricatorEnv::getEnvConfig('notification.ssl-key');
$ssl_cert = PhabricatorEnv::getEnvConfig('notification.ssl-cert');
$server_uri = PhabricatorEnv::getEnvConfig('notification.server-uri');
$server_uri = new PhutilURI($server_uri);
$client_uri = PhabricatorEnv::getEnvConfig('notification.client-uri');
$client_uri = new PhutilURI($client_uri);
$log = $this->getLogPath();
$server_argv = array();
$server_argv[] = '--client-port='.coalesce(
$this->clientPort,
$client_uri->getPort());
$server_argv[] = '--admin-port='.$server_uri->getPort();
$server_argv[] = '--admin-host='.$server_uri->getDomain();
if ($ssl_key) {
$server_argv[] = '--ssl-key='.$ssl_key;
}
if ($ssl_cert) {
$server_argv[] = '--ssl-cert='.$ssl_cert;
}
$server_argv[] = '--log='.$log;
if ($this->clientHost) {
$server_argv[] = '--client-host='.$this->clientHost;
}
return $server_argv;
}
private function getAphlictScriptPath() {
$root = dirname(phutil_get_library_root('phabricator'));
return $root.'/support/aphlict/server/aphlict_server.js';
}
final protected function launch() {
$console = PhutilConsole::getConsole();
if ($this->debug) {
- $console->writeOut(pht("Starting Aphlict server in foreground...\n"));
+ $console->writeOut(
+ "%s\n",
+ pht('Starting Aphlict server in foreground...'));
} else {
Filesystem::writeFile($this->getPIDPath(), getmypid());
}
$command = csprintf(
'%s %s %Ls',
$this->getNodeBinary(),
$this->getAphlictScriptPath(),
$this->getServerArgv());
if (!$this->debug) {
declare(ticks = 1);
pcntl_signal(SIGINT, array($this, 'cleanup'));
pcntl_signal(SIGTERM, array($this, 'cleanup'));
}
register_shutdown_function(array($this, 'cleanup'));
if ($this->debug) {
- $console->writeOut("Launching server:\n\n $ ".$command."\n\n");
+ $console->writeOut(
+ "%s\n\n $ %s\n\n",
+ pht('Launching server:'),
+ $command);
$err = phutil_passthru('%C', $command);
- $console->writeOut(">>> Server exited!\n");
+ $console->writeOut(">>> %s\n", pht('Server exited!'));
exit($err);
} else {
while (true) {
global $g_future;
$g_future = new ExecFuture('exec %C', $command);
$g_future->resolve();
// If the server exited, wait a couple of seconds and restart it.
unset($g_future);
sleep(2);
}
}
}
/* -( Commands )----------------------------------------------------------- */
final protected function executeStartCommand() {
$console = PhutilConsole::getConsole();
$this->willLaunch();
$pid = pcntl_fork();
if ($pid < 0) {
- throw new Exception('Failed to fork()!');
+ throw new Exception(
+ pht(
+ 'Failed to %s!',
+ 'fork()'));
} else if ($pid) {
- $console->writeErr(pht("Aphlict Server started.\n"));
+ $console->writeErr("%s\n", pht('Aphlict Server started.'));
exit(0);
}
// When we fork, the child process will inherit its parent's set of open
// file descriptors. If the parent process of bin/aphlict is waiting for
// bin/aphlict's file descriptors to close, it will be stuck waiting on
// the daemonized process. (This happens if e.g. bin/aphlict is started
// in another script using passthru().)
fclose(STDOUT);
fclose(STDERR);
$this->launch();
return 0;
}
final protected function executeStopCommand() {
$console = PhutilConsole::getConsole();
$pid = $this->getPID();
if (!$pid) {
- $console->writeErr(pht("Aphlict is not running.\n"));
+ $console->writeErr("%s\n", pht('Aphlict is not running.'));
return 0;
}
- $console->writeErr(pht("Stopping Aphlict Server (%s)...\n", $pid));
+ $console->writeErr("%s\n", pht('Stopping Aphlict Server (%s)...', $pid));
posix_kill($pid, SIGINT);
$start = time();
do {
if (!PhabricatorDaemonReference::isProcessRunning($pid)) {
$console->writeOut(
"%s\n",
pht('Aphlict Server (%s) exited normally.', $pid));
$pid = null;
break;
}
usleep(100000);
} while (time() < $start + 5);
if ($pid) {
- $console->writeErr(pht('Sending %s a SIGKILL.', $pid)."\n");
+ $console->writeErr("%s\n", pht('Sending %s a SIGKILL.', $pid));
posix_kill($pid, SIGKILL);
unset($pid);
}
Filesystem::remove($this->getPIDPath());
return 0;
}
private function getNodeBinary() {
if (Filesystem::binaryExists('nodejs')) {
return 'nodejs';
}
if (Filesystem::binaryExists('node')) {
return 'node';
}
throw new PhutilArgumentUsageException(
pht(
- 'No `nodejs` or `node` binary was found in $PATH. You must install '.
- 'Node.js to start the Aphlict server.'));
+ 'No `%s` or `%s` binary was found in %s. You must install '.
+ 'Node.js to start the Aphlict server.',
+ 'nodejs',
+ 'node',
+ '$PATH'));
}
}
diff --git a/src/applications/aphlict/query/AphlictDropdownDataQuery.php b/src/applications/aphlict/query/AphlictDropdownDataQuery.php
index 91ce3ad41..4f360f28c 100644
--- a/src/applications/aphlict/query/AphlictDropdownDataQuery.php
+++ b/src/applications/aphlict/query/AphlictDropdownDataQuery.php
@@ -1,103 +1,103 @@
<?php
final class AphlictDropdownDataQuery {
private $viewer;
private $notificationData;
private $conpherenceData;
public function setViewer(PhabricatorUser $viewer) {
$this->viewer = $viewer;
return $this;
}
public function getViewer() {
return $this->viewer;
}
private function setNotificationData(array $data) {
$this->notificationData = $data;
return $this;
}
public function getNotificationData() {
if ($this->notificationData === null) {
- throw new Exception('You must execute() first!');
+ throw new Exception(pht('You must %s first!', 'execute()'));
}
return $this->notificationData;
}
private function setConpherenceData(array $data) {
$this->conpherenceData = $data;
return $this;
}
public function getConpherenceData() {
if ($this->conpherenceData === null) {
- throw new Exception('You must execute() first!');
+ throw new Exception(pht('You must %s first!', 'execute()'));
}
return $this->conpherenceData;
}
public function execute() {
$viewer = $this->getViewer();
$conpherence_app = 'PhabricatorConpherenceApplication';
$is_c_installed = PhabricatorApplication::isClassInstalledForViewer(
$conpherence_app,
$viewer);
$raw_message_count_number = null;
$message_count_number = null;
if ($is_c_installed) {
$unread_status = ConpherenceParticipationStatus::BEHIND;
$unread = id(new ConpherenceParticipantCountQuery())
->withParticipantPHIDs(array($viewer->getPHID()))
->withParticipationStatus($unread_status)
->execute();
$raw_message_count_number = idx($unread, $viewer->getPHID(), 0);
$message_count_number = $this->formatNumber($raw_message_count_number);
}
$conpherence_data = array(
'isInstalled' => $is_c_installed,
'countType' => 'messages',
'count' => $message_count_number,
'rawCount' => $raw_message_count_number,
);
$this->setConpherenceData($conpherence_data);
$notification_app = 'PhabricatorNotificationsApplication';
$is_n_installed = PhabricatorApplication::isClassInstalledForViewer(
$notification_app,
$viewer);
$notification_count_number = null;
$raw_notification_count_number = null;
if ($is_n_installed) {
$raw_notification_count_number =
id(new PhabricatorFeedStoryNotification())
->countUnread($viewer);
$notification_count_number = $this->formatNumber(
$raw_notification_count_number);
}
$notification_data = array(
'isInstalled' => $is_n_installed,
'countType' => 'notifications',
'count' => $notification_count_number,
'rawCount' => $raw_notification_count_number,
);
$this->setNotificationData($notification_data);
return array(
$notification_app => $this->getNotificationData(),
$conpherence_app => $this->getConpherenceData(),
);
}
private function formatNumber($number) {
$formatted = $number;
if ($number > 999) {
$formatted = "\xE2\x88\x9E";
}
return $formatted;
}
}
diff --git a/src/applications/arcanist/conduit/ArcanistProjectInfoConduitAPIMethod.php b/src/applications/arcanist/conduit/ArcanistProjectInfoConduitAPIMethod.php
index 677c4985a..ee5cbbff2 100644
--- a/src/applications/arcanist/conduit/ArcanistProjectInfoConduitAPIMethod.php
+++ b/src/applications/arcanist/conduit/ArcanistProjectInfoConduitAPIMethod.php
@@ -1,70 +1,70 @@
<?php
final class ArcanistProjectInfoConduitAPIMethod
extends ArcanistConduitAPIMethod {
public function getAPIMethodName() {
return 'arcanist.projectinfo';
}
public function getMethodDescription() {
- return 'Get information about Arcanist projects.';
+ return pht('Get information about Arcanist projects.');
}
protected function defineParamTypes() {
return array(
'name' => 'required string',
);
}
protected function defineReturnType() {
return 'nonempty dict';
}
protected function defineErrorTypes() {
return array(
- 'ERR-BAD-ARCANIST-PROJECT' => 'No such project exists.',
+ 'ERR-BAD-ARCANIST-PROJECT' => pht('No such project exists.'),
);
}
protected function execute(ConduitAPIRequest $request) {
$name = $request->getValue('name');
$project = id(new PhabricatorRepositoryArcanistProject())->loadOneWhere(
'name = %s',
$name);
if (!$project) {
throw new ConduitException('ERR-BAD-ARCANIST-PROJECT');
}
$repository = null;
if ($project->getRepositoryID()) {
$repository = id(new PhabricatorRepositoryQuery())
->setViewer($request->getUser())
->withIDs(array($project->getRepositoryID()))
->executeOne();
}
$repository_phid = null;
$tracked = false;
$encoding = null;
$dictionary = array();
if ($repository) {
$repository_phid = $repository->getPHID();
$tracked = $repository->isTracked();
$encoding = $repository->getDetail('encoding');
$dictionary = $repository->toDictionary();
}
return array(
'name' => $project->getName(),
'phid' => $project->getPHID(),
'repositoryPHID' => $repository_phid,
'tracked' => $tracked,
'encoding' => $encoding,
'repository' => $dictionary,
);
}
}
diff --git a/src/applications/audit/conduit/AuditQueryConduitAPIMethod.php b/src/applications/audit/conduit/AuditQueryConduitAPIMethod.php
index d5a02810f..2fc9ca47e 100644
--- a/src/applications/audit/conduit/AuditQueryConduitAPIMethod.php
+++ b/src/applications/audit/conduit/AuditQueryConduitAPIMethod.php
@@ -1,91 +1,91 @@
<?php
final class AuditQueryConduitAPIMethod extends AuditConduitAPIMethod {
public function getAPIMethodName() {
return 'audit.query';
}
public function getMethodDescription() {
- return 'Query audit requests.';
+ return pht('Query audit requests.');
}
protected function defineParamTypes() {
$statuses = array(
DiffusionCommitQuery::AUDIT_STATUS_ANY,
DiffusionCommitQuery::AUDIT_STATUS_OPEN,
DiffusionCommitQuery::AUDIT_STATUS_CONCERN,
DiffusionCommitQuery::AUDIT_STATUS_ACCEPTED,
DiffusionCommitQuery::AUDIT_STATUS_PARTIAL,
);
$status_const = $this->formatStringConstants($statuses);
return array(
'auditorPHIDs' => 'optional list<phid>',
'commitPHIDs' => 'optional list<phid>',
'status' => ('optional '.$status_const.
' (default = "audit-status-any")'),
'offset' => 'optional int',
'limit' => 'optional int (default = 100)',
);
}
protected function defineReturnType() {
return 'list<dict>';
}
protected function execute(ConduitAPIRequest $request) {
$query = id(new DiffusionCommitQuery())
->setViewer($request->getUser());
$auditor_phids = $request->getValue('auditorPHIDs', array());
if ($auditor_phids) {
$query->withAuditorPHIDs($auditor_phids);
}
$commit_phids = $request->getValue('commitPHIDs', array());
if ($commit_phids) {
$query->withPHIDs($commit_phids);
}
$status = $request->getValue(
'status',
DiffusionCommitQuery::AUDIT_STATUS_ANY);
$query->withAuditStatus($status);
// NOTE: These affect the number of commits identified, which is sort of
// reasonable but means the method may return an arbitrary number of
// actual audit requests.
$query->setOffset($request->getValue('offset', 0));
$query->setLimit($request->getValue('limit', 100));
$commits = $query->execute();
$auditor_map = array_fuse($auditor_phids);
$results = array();
foreach ($commits as $commit) {
$requests = $commit->getAudits();
foreach ($requests as $request) {
// If this audit isn't triggered for one of the requested PHIDs,
// skip it.
if ($auditor_map && empty($auditor_map[$request->getAuditorPHID()])) {
continue;
}
$results[] = array(
'id' => $request->getID(),
'commitPHID' => $request->getCommitPHID(),
'auditorPHID' => $request->getAuditorPHID(),
'reasons' => $request->getAuditReasons(),
'status' => $request->getAuditStatus(),
);
}
}
return $results;
}
}
diff --git a/src/applications/audit/constants/PhabricatorAuditActionConstants.php b/src/applications/audit/constants/PhabricatorAuditActionConstants.php
index 95dd8cdbf..afecd0bf8 100644
--- a/src/applications/audit/constants/PhabricatorAuditActionConstants.php
+++ b/src/applications/audit/constants/PhabricatorAuditActionConstants.php
@@ -1,47 +1,47 @@
<?php
final class PhabricatorAuditActionConstants {
const CONCERN = 'concern';
const ACCEPT = 'accept';
const COMMENT = 'comment';
const RESIGN = 'resign';
const CLOSE = 'close';
const ADD_CCS = 'add_ccs';
const ADD_AUDITORS = 'add_auditors';
const INLINE = 'audit:inline';
const ACTION = 'audit:action';
public static function getActionNameMap() {
$map = array(
- self::COMMENT => pht('Comment'),
- self::CONCERN => pht("Raise Concern \xE2\x9C\x98"),
- self::ACCEPT => pht("Accept Commit \xE2\x9C\x94"),
- self::RESIGN => pht('Resign from Audit'),
- self::CLOSE => pht('Close Audit'),
- self::ADD_CCS => pht('Add CCs'),
+ self::COMMENT => pht('Comment'),
+ self::CONCERN => pht("Raise Concern \xE2\x9C\x98"),
+ self::ACCEPT => pht("Accept Commit \xE2\x9C\x94"),
+ self::RESIGN => pht('Resign from Audit'),
+ self::CLOSE => pht('Close Audit'),
+ self::ADD_CCS => pht('Add CCs'),
self::ADD_AUDITORS => pht('Add Auditors'),
);
return $map;
}
public static function getActionName($constant) {
$map = self::getActionNameMap();
- return idx($map, $constant, 'Unknown');
+ return idx($map, $constant, pht('Unknown'));
}
public static function getActionPastTenseVerb($action) {
- static $map = array(
- self::COMMENT => 'commented on',
- self::CONCERN => 'raised a concern with',
- self::ACCEPT => 'accepted',
- self::RESIGN => 'resigned from',
- self::CLOSE => 'closed',
- self::ADD_CCS => 'added CCs to',
- self::ADD_AUDITORS => 'added auditors to',
+ $map = array(
+ self::COMMENT => pht('commented on'),
+ self::CONCERN => pht('raised a concern with'),
+ self::ACCEPT => pht('accepted'),
+ self::RESIGN => pht('resigned from'),
+ self::CLOSE => pht('closed'),
+ self::ADD_CCS => pht('added CCs to'),
+ self::ADD_AUDITORS => pht('added auditors to'),
);
- return idx($map, $action, 'updated');
+ return idx($map, $action, pht('updated'));
}
}
diff --git a/src/applications/audit/constants/PhabricatorAuditCommitStatusConstants.php b/src/applications/audit/constants/PhabricatorAuditCommitStatusConstants.php
index aef3c7e22..1bc07aaa5 100644
--- a/src/applications/audit/constants/PhabricatorAuditCommitStatusConstants.php
+++ b/src/applications/audit/constants/PhabricatorAuditCommitStatusConstants.php
@@ -1,53 +1,53 @@
<?php
final class PhabricatorAuditCommitStatusConstants {
const NONE = 0;
const NEEDS_AUDIT = 1;
const CONCERN_RAISED = 2;
const PARTIALLY_AUDITED = 3;
const FULLY_AUDITED = 4;
public static function getStatusNameMap() {
$map = array(
self::NONE => pht('None'),
self::NEEDS_AUDIT => pht('Audit Required'),
self::CONCERN_RAISED => pht('Concern Raised'),
self::PARTIALLY_AUDITED => pht('Partially Audited'),
self::FULLY_AUDITED => pht('Audited'),
);
return $map;
}
public static function getStatusName($code) {
- return idx(self::getStatusNameMap(), $code, 'Unknown');
+ return idx(self::getStatusNameMap(), $code, pht('Unknown'));
}
public static function getOpenStatusConstants() {
return array(
self::CONCERN_RAISED,
self::NEEDS_AUDIT,
);
}
public static function getStatusColor($code) {
switch ($code) {
case self::CONCERN_RAISED:
$color = 'red';
break;
case self::NEEDS_AUDIT:
case self::PARTIALLY_AUDITED:
$color = 'orange';
break;
case self::FULLY_AUDITED:
$color = 'green';
break;
default:
$color = null;
break;
}
return $color;
}
}
diff --git a/src/applications/audit/editor/PhabricatorAuditEditor.php b/src/applications/audit/editor/PhabricatorAuditEditor.php
index b2fd9d84b..24bc342f0 100644
--- a/src/applications/audit/editor/PhabricatorAuditEditor.php
+++ b/src/applications/audit/editor/PhabricatorAuditEditor.php
@@ -1,986 +1,986 @@
<?php
final class PhabricatorAuditEditor
extends PhabricatorApplicationTransactionEditor {
const MAX_FILES_SHOWN_IN_EMAIL = 1000;
private $auditReasonMap = array();
private $affectedFiles;
private $rawPatch;
private $didExpandInlineState;
public function addAuditReason($phid, $reason) {
if (!isset($this->auditReasonMap[$phid])) {
$this->auditReasonMap[$phid] = array();
}
$this->auditReasonMap[$phid][] = $reason;
return $this;
}
private function getAuditReasons($phid) {
if (isset($this->auditReasonMap[$phid])) {
return $this->auditReasonMap[$phid];
}
if ($this->getIsHeraldEditor()) {
$name = 'herald';
} else {
$name = $this->getActor()->getUsername();
}
- return array('Added by '.$name.'.');
+ return array(pht('Added by %s.', $name));
}
public function setRawPatch($patch) {
$this->rawPatch = $patch;
return $this;
}
public function getRawPatch() {
return $this->rawPatch;
}
public function getEditorApplicationClass() {
return 'PhabricatorAuditApplication';
}
public function getEditorObjectsDescription() {
return pht('Audits');
}
public function getTransactionTypes() {
$types = parent::getTransactionTypes();
$types[] = PhabricatorTransactions::TYPE_COMMENT;
$types[] = PhabricatorTransactions::TYPE_EDGE;
$types[] = PhabricatorTransactions::TYPE_INLINESTATE;
$types[] = PhabricatorAuditTransaction::TYPE_COMMIT;
// TODO: These will get modernized eventually, but that can happen one
// at a time later on.
$types[] = PhabricatorAuditActionConstants::ACTION;
$types[] = PhabricatorAuditActionConstants::INLINE;
$types[] = PhabricatorAuditActionConstants::ADD_AUDITORS;
return $types;
}
protected function transactionHasEffect(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorAuditActionConstants::INLINE:
return $xaction->hasComment();
}
return parent::transactionHasEffect($object, $xaction);
}
protected function getCustomTransactionOldValue(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorAuditActionConstants::ACTION:
case PhabricatorAuditActionConstants::INLINE:
case PhabricatorAuditTransaction::TYPE_COMMIT:
return null;
case PhabricatorAuditActionConstants::ADD_AUDITORS:
// TODO: For now, just record the added PHIDs. Eventually, turn these
// into real edge transactions, probably?
return array();
}
return parent::getCustomTransactionOldValue($object, $xaction);
}
protected function getCustomTransactionNewValue(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorAuditActionConstants::ACTION:
case PhabricatorAuditActionConstants::INLINE:
case PhabricatorAuditActionConstants::ADD_AUDITORS:
case PhabricatorAuditTransaction::TYPE_COMMIT:
return $xaction->getNewValue();
}
return parent::getCustomTransactionNewValue($object, $xaction);
}
protected function applyCustomInternalTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorAuditActionConstants::ACTION:
case PhabricatorAuditActionConstants::INLINE:
case PhabricatorAuditActionConstants::ADD_AUDITORS:
case PhabricatorAuditTransaction::TYPE_COMMIT:
return;
}
return parent::applyCustomInternalTransaction($object, $xaction);
}
protected function applyCustomExternalTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorAuditActionConstants::ACTION:
case PhabricatorAuditTransaction::TYPE_COMMIT:
return;
case PhabricatorAuditActionConstants::INLINE:
$reply = $xaction->getComment()->getReplyToComment();
if ($reply && !$reply->getHasReplies()) {
$reply->setHasReplies(1)->save();
}
return;
case PhabricatorAuditActionConstants::ADD_AUDITORS:
$new = $xaction->getNewValue();
if (!is_array($new)) {
$new = array();
}
$old = $xaction->getOldValue();
if (!is_array($old)) {
$old = array();
}
$add = array_diff_key($new, $old);
$actor = $this->requireActor();
$requests = $object->getAudits();
$requests = mpull($requests, null, 'getAuditorPHID');
foreach ($add as $phid) {
if (isset($requests[$phid])) {
continue;
}
if ($this->getIsHeraldEditor()) {
$audit_requested = $xaction->getMetadataValue('auditStatus');
$audit_reason_map = $xaction->getMetadataValue('auditReasonMap');
$audit_reason = $audit_reason_map[$phid];
} else {
$audit_requested = PhabricatorAuditStatusConstants::AUDIT_REQUESTED;
$audit_reason = $this->getAuditReasons($phid);
}
$requests[] = id (new PhabricatorRepositoryAuditRequest())
->setCommitPHID($object->getPHID())
->setAuditorPHID($phid)
->setAuditStatus($audit_requested)
->setAuditReasons($audit_reason)
->save();
}
$object->attachAudits($requests);
return;
}
return parent::applyCustomExternalTransaction($object, $xaction);
}
protected function applyBuiltinExternalTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorTransactions::TYPE_INLINESTATE:
$table = new PhabricatorAuditTransactionComment();
$conn_w = $table->establishConnection('w');
foreach ($xaction->getNewValue() as $phid => $state) {
queryfx(
$conn_w,
'UPDATE %T SET fixedState = %s WHERE phid = %s',
$table->getTableName(),
$state,
$phid);
}
break;
}
return parent::applyBuiltinExternalTransaction($object, $xaction);
}
protected function applyFinalEffects(
PhabricatorLiskDAO $object,
array $xactions) {
// Load auditors explicitly; we may not have them if the caller was a
// generic piece of infrastructure.
$commit = id(new DiffusionCommitQuery())
->setViewer($this->requireActor())
->withIDs(array($object->getID()))
->needAuditRequests(true)
->executeOne();
if (!$commit) {
throw new Exception(
pht('Failed to load commit during transaction finalization!'));
}
$object->attachAudits($commit->getAudits());
$status_concerned = PhabricatorAuditStatusConstants::CONCERNED;
$status_closed = PhabricatorAuditStatusConstants::CLOSED;
$status_resigned = PhabricatorAuditStatusConstants::RESIGNED;
$status_accepted = PhabricatorAuditStatusConstants::ACCEPTED;
$status_concerned = PhabricatorAuditStatusConstants::CONCERNED;
$actor_phid = $this->getActingAsPHID();
$actor_is_author = ($object->getAuthorPHID()) &&
($actor_phid == $object->getAuthorPHID());
$import_status_flag = null;
foreach ($xactions as $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorAuditTransaction::TYPE_COMMIT:
$import_status_flag = PhabricatorRepositoryCommit::IMPORTED_HERALD;
break;
case PhabricatorAuditActionConstants::ACTION:
$new = $xaction->getNewValue();
switch ($new) {
case PhabricatorAuditActionConstants::CLOSE:
// "Close" means wipe out all the concerns.
$requests = $object->getAudits();
foreach ($requests as $request) {
if ($request->getAuditStatus() == $status_concerned) {
$request
->setAuditStatus($status_closed)
->save();
}
}
break;
case PhabricatorAuditActionConstants::RESIGN:
$requests = $object->getAudits();
$requests = mpull($requests, null, 'getAuditorPHID');
$actor_request = idx($requests, $actor_phid);
// If the actor doesn't currently have a relationship to the
// commit, add one explicitly. For example, this allows members
// of a project to resign from a commit and have it drop out of
// their queue.
if (!$actor_request) {
$actor_request = id(new PhabricatorRepositoryAuditRequest())
->setCommitPHID($object->getPHID())
->setAuditorPHID($actor_phid);
$requests[] = $actor_request;
$object->attachAudits($requests);
}
$actor_request
->setAuditStatus($status_resigned)
->save();
break;
case PhabricatorAuditActionConstants::ACCEPT:
case PhabricatorAuditActionConstants::CONCERN:
if ($new == PhabricatorAuditActionConstants::ACCEPT) {
$new_status = $status_accepted;
} else {
$new_status = $status_concerned;
}
$requests = $object->getAudits();
$requests = mpull($requests, null, 'getAuditorPHID');
// Figure out which requests the actor has authority over: these
// are user requests where they are the auditor, and packages
// and projects they are a member of.
if ($actor_is_author) {
// When modifying your own commits, you act only on behalf of
// yourself, not your packages/projects -- the idea being that
// you can't accept your own commits.
$authority_phids = array($actor_phid);
} else {
$authority_phids =
PhabricatorAuditCommentEditor::loadAuditPHIDsForUser(
$this->requireActor());
}
$authority = array_select_keys(
$requests,
$authority_phids);
if (!$authority) {
// If the actor has no authority over any existing requests,
// create a new request for them.
$actor_request = id(new PhabricatorRepositoryAuditRequest())
->setCommitPHID($object->getPHID())
->setAuditorPHID($actor_phid)
->setAuditStatus($new_status)
->save();
$requests[$actor_phid] = $actor_request;
$object->attachAudits($requests);
} else {
// Otherwise, update the audit status of the existing requests.
foreach ($authority as $request) {
$request
->setAuditStatus($new_status)
->save();
}
}
break;
}
break;
}
}
$requests = $object->getAudits();
$object->updateAuditStatus($requests);
$object->save();
if ($import_status_flag) {
$object->writeImportStatusFlag($import_status_flag);
}
return $xactions;
}
protected function expandTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
$xactions = parent::expandTransaction($object, $xaction);
switch ($xaction->getTransactionType()) {
case PhabricatorAuditTransaction::TYPE_COMMIT:
$request = $this->createAuditRequestTransactionFromCommitMessage(
$object);
if ($request) {
$xactions[] = $request;
$this->setUnmentionablePHIDMap($request->getNewValue());
}
break;
default:
break;
}
if (!$this->didExpandInlineState) {
switch ($xaction->getTransactionType()) {
case PhabricatorTransactions::TYPE_COMMENT:
case PhabricatorAuditActionConstants::ACTION:
$this->didExpandInlineState = true;
$actor_phid = $this->getActingAsPHID();
$actor_is_author = ($object->getAuthorPHID() == $actor_phid);
if (!$actor_is_author) {
break;
}
$state_map = PhabricatorTransactions::getInlineStateMap();
$inlines = id(new DiffusionDiffInlineCommentQuery())
->setViewer($this->getActor())
->withCommitPHIDs(array($object->getPHID()))
->withFixedStates(array_keys($state_map))
->execute();
if (!$inlines) {
break;
}
$old_value = mpull($inlines, 'getFixedState', 'getPHID');
$new_value = array();
foreach ($old_value as $key => $state) {
$new_value[$key] = $state_map[$state];
}
$xactions[] = id(new PhabricatorAuditTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_INLINESTATE)
->setIgnoreOnNoEffect(true)
->setOldValue($old_value)
->setNewValue($new_value);
break;
}
}
return $xactions;
}
private function createAuditRequestTransactionFromCommitMessage(
PhabricatorRepositoryCommit $commit) {
$data = $commit->getCommitData();
$message = $data->getCommitMessage();
$matches = null;
if (!preg_match('/^Auditors?:\s*(.*)$/im', $message, $matches)) {
return array();
}
$phids = id(new PhabricatorObjectListQuery())
->setViewer($this->getActor())
->setAllowPartialResults(true)
->setAllowedTypes(
array(
PhabricatorPeopleUserPHIDType::TYPECONST,
PhabricatorProjectProjectPHIDType::TYPECONST,
))
->setObjectList($matches[1])
->execute();
if (!$phids) {
return array();
}
foreach ($phids as $phid) {
- $this->addAuditReason($phid, 'Requested by Author');
+ $this->addAuditReason($phid, pht('Requested by Author'));
}
return id(new PhabricatorAuditTransaction())
->setTransactionType(PhabricatorAuditActionConstants::ADD_AUDITORS)
->setNewValue(array_fuse($phids));
}
protected function sortTransactions(array $xactions) {
$xactions = parent::sortTransactions($xactions);
$head = array();
$tail = array();
foreach ($xactions as $xaction) {
$type = $xaction->getTransactionType();
if ($type == PhabricatorAuditActionConstants::INLINE) {
$tail[] = $xaction;
} else {
$head[] = $xaction;
}
}
return array_values(array_merge($head, $tail));
}
protected function validateTransaction(
PhabricatorLiskDAO $object,
$type,
array $xactions) {
$errors = parent::validateTransaction($object, $type, $xactions);
foreach ($xactions as $xaction) {
switch ($type) {
case PhabricatorAuditActionConstants::ACTION:
$error = $this->validateAuditAction(
$object,
$type,
$xaction,
$xaction->getNewValue());
if ($error) {
$errors[] = new PhabricatorApplicationTransactionValidationError(
$type,
pht('Invalid'),
$error,
$xaction);
}
break;
}
}
return $errors;
}
private function validateAuditAction(
PhabricatorLiskDAO $object,
$type,
PhabricatorAuditTransaction $xaction,
$action) {
$can_author_close_key = 'audit.can-author-close-audit';
$can_author_close = PhabricatorEnv::getEnvConfig($can_author_close_key);
$actor_is_author = ($object->getAuthorPHID()) &&
($object->getAuthorPHID() == $this->getActingAsPHID());
switch ($action) {
case PhabricatorAuditActionConstants::CLOSE:
if (!$actor_is_author) {
return pht(
'You can not close this audit because you are not the author '.
'of the commit.');
}
if (!$can_author_close) {
return pht(
'You can not close this audit because "%s" is disabled in '.
'the Phabricator configuration.',
$can_author_close_key);
}
break;
}
return null;
}
protected function supportsSearch() {
return true;
}
protected function expandCustomRemarkupBlockTransactions(
PhabricatorLiskDAO $object,
array $xactions,
$blocks,
PhutilMarkupEngine $engine) {
// we are only really trying to find unmentionable phids here...
// don't bother with this outside initial commit (i.e. create)
// transaction
$is_commit = false;
foreach ($xactions as $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorAuditTransaction::TYPE_COMMIT:
$is_commit = true;
break;
}
}
// "result" is always an array....
$result = array();
if (!$is_commit) {
return $result;
}
$flat_blocks = array_mergev($blocks);
$huge_block = implode("\n\n", $flat_blocks);
$phid_map = array();
$phid_map[] = $this->getUnmentionablePHIDMap();
$monograms = array();
$task_refs = id(new ManiphestCustomFieldStatusParser())
->parseCorpus($huge_block);
foreach ($task_refs as $match) {
foreach ($match['monograms'] as $monogram) {
$monograms[] = $monogram;
}
}
$rev_refs = id(new DifferentialCustomFieldDependsOnParser())
->parseCorpus($huge_block);
foreach ($rev_refs as $match) {
foreach ($match['monograms'] as $monogram) {
$monograms[] = $monogram;
}
}
$objects = id(new PhabricatorObjectQuery())
->setViewer($this->getActor())
->withNames($monograms)
->execute();
$phid_map[] = mpull($objects, 'getPHID', 'getPHID');
$phid_map = array_mergev($phid_map);
$this->setUnmentionablePHIDMap($phid_map);
return $result;
}
protected function buildReplyHandler(PhabricatorLiskDAO $object) {
$reply_handler = new PhabricatorAuditReplyHandler();
$reply_handler->setMailReceiver($object);
return $reply_handler;
}
protected function getMailSubjectPrefix() {
return PhabricatorEnv::getEnvConfig('metamta.diffusion.subject-prefix');
}
protected function getMailThreadID(PhabricatorLiskDAO $object) {
// For backward compatibility, use this legacy thread ID.
return 'diffusion-audit-'.$object->getPHID();
}
protected function buildMailTemplate(PhabricatorLiskDAO $object) {
$identifier = $object->getCommitIdentifier();
$repository = $object->getRepository();
$monogram = $repository->getMonogram();
$summary = $object->getSummary();
$name = $repository->formatCommitName($identifier);
$subject = "{$name}: {$summary}";
$thread_topic = "Commit {$monogram}{$identifier}";
$template = id(new PhabricatorMetaMTAMail())
->setSubject($subject)
->addHeader('Thread-Topic', $thread_topic);
$this->attachPatch(
$template,
$object);
return $template;
}
protected function getMailTo(PhabricatorLiskDAO $object) {
$phids = array();
if ($object->getAuthorPHID()) {
$phids[] = $object->getAuthorPHID();
}
$status_resigned = PhabricatorAuditStatusConstants::RESIGNED;
foreach ($object->getAudits() as $audit) {
if ($audit->getAuditStatus() != $status_resigned) {
$phids[] = $audit->getAuditorPHID();
}
}
return $phids;
}
protected function buildMailBody(
PhabricatorLiskDAO $object,
array $xactions) {
$body = parent::buildMailBody($object, $xactions);
$type_inline = PhabricatorAuditActionConstants::INLINE;
$type_push = PhabricatorAuditTransaction::TYPE_COMMIT;
$is_commit = false;
$inlines = array();
foreach ($xactions as $xaction) {
if ($xaction->getTransactionType() == $type_inline) {
$inlines[] = $xaction;
}
if ($xaction->getTransactionType() == $type_push) {
$is_commit = true;
}
}
if ($inlines) {
$body->addTextSection(
pht('INLINE COMMENTS'),
$this->renderInlineCommentsForMail($object, $inlines));
}
if ($is_commit) {
$data = $object->getCommitData();
$body->addTextSection(pht('AFFECTED FILES'), $this->affectedFiles);
$this->inlinePatch(
$body,
$object);
}
// Reload the commit to pull commit data.
$commit = id(new DiffusionCommitQuery())
->setViewer($this->requireActor())
->withIDs(array($object->getID()))
->needCommitData(true)
->executeOne();
$data = $commit->getCommitData();
$user_phids = array();
$author_phid = $commit->getAuthorPHID();
if ($author_phid) {
$user_phids[$author_phid][] = pht('Author');
}
$committer_phid = $data->getCommitDetail('committerPHID');
if ($committer_phid && ($committer_phid != $author_phid)) {
$user_phids[$committer_phid][] = pht('Committer');
}
// we loaded this in applyFinalEffects
$audit_requests = $object->getAudits();
$auditor_phids = mpull($audit_requests, 'getAuditorPHID');
foreach ($auditor_phids as $auditor_phid) {
$user_phids[$auditor_phid][] = pht('Auditor');
}
// TODO: It would be nice to show pusher here too, but that information
// is a little tricky to get at right now.
if ($user_phids) {
$handle_phids = array_keys($user_phids);
$handles = id(new PhabricatorHandleQuery())
->setViewer($this->requireActor())
->withPHIDs($handle_phids)
->execute();
$user_info = array();
foreach ($user_phids as $phid => $roles) {
$user_info[] = pht(
'%s (%s)',
$handles[$phid]->getName(),
implode(', ', $roles));
}
$body->addTextSection(
pht('USERS'),
implode("\n", $user_info));
}
$monogram = $object->getRepository()->formatCommitName(
$object->getCommitIdentifier());
$body->addLinkSection(
pht('COMMIT'),
PhabricatorEnv::getProductionURI('/'.$monogram));
return $body;
}
private function attachPatch(
PhabricatorMetaMTAMail $template,
PhabricatorRepositoryCommit $commit) {
if (!$this->getRawPatch()) {
return;
}
$attach_key = 'metamta.diffusion.attach-patches';
$attach_patches = PhabricatorEnv::getEnvConfig($attach_key);
if (!$attach_patches) {
return;
}
$repository = $commit->getRepository();
$encoding = $repository->getDetail('encoding', 'UTF-8');
$raw_patch = $this->getRawPatch();
$commit_name = $repository->formatCommitName(
$commit->getCommitIdentifier());
$template->addAttachment(
new PhabricatorMetaMTAAttachment(
$raw_patch,
$commit_name.'.patch',
'text/x-patch; charset='.$encoding));
}
private function inlinePatch(
PhabricatorMetaMTAMailBody $body,
PhabricatorRepositoryCommit $commit) {
if (!$this->getRawPatch()) {
return;
}
$inline_key = 'metamta.diffusion.inline-patches';
$inline_patches = PhabricatorEnv::getEnvConfig($inline_key);
if (!$inline_patches) {
return;
}
$repository = $commit->getRepository();
$raw_patch = $this->getRawPatch();
$result = null;
$len = substr_count($raw_patch, "\n");
if ($len <= $inline_patches) {
// We send email as utf8, so we need to convert the text to utf8 if
// we can.
$encoding = $repository->getDetail('encoding', 'UTF-8');
if ($encoding) {
$raw_patch = phutil_utf8_convert($raw_patch, 'UTF-8', $encoding);
}
$result = phutil_utf8ize($raw_patch);
}
if ($result) {
$result = "PATCH\n\n{$result}\n";
}
$body->addRawSection($result);
}
private function renderInlineCommentsForMail(
PhabricatorLiskDAO $object,
array $inline_xactions) {
$inlines = mpull($inline_xactions, 'getComment');
$block = array();
$path_map = id(new DiffusionPathQuery())
->withPathIDs(mpull($inlines, 'getPathID'))
->execute();
$path_map = ipull($path_map, 'path', 'id');
foreach ($inlines as $inline) {
$path = idx($path_map, $inline->getPathID());
if ($path === null) {
continue;
}
$start = $inline->getLineNumber();
$len = $inline->getLineLength();
if ($len) {
$range = $start.'-'.($start + $len);
} else {
$range = $start;
}
$content = $inline->getContent();
$block[] = "{$path}:{$range} {$content}";
}
return implode("\n", $block);
}
public function getMailTagsMap() {
return array(
PhabricatorAuditTransaction::MAILTAG_COMMIT =>
pht('A commit is created.'),
PhabricatorAuditTransaction::MAILTAG_ACTION_CONCERN =>
pht('A commit has a concerned raised against it.'),
PhabricatorAuditTransaction::MAILTAG_ACTION_ACCEPT =>
pht('A commit is accepted.'),
PhabricatorAuditTransaction::MAILTAG_ACTION_RESIGN =>
pht('A commit has an auditor resign.'),
PhabricatorAuditTransaction::MAILTAG_ACTION_CLOSE =>
pht('A commit is closed.'),
PhabricatorAuditTransaction::MAILTAG_ADD_AUDITORS =>
pht('A commit has auditors added.'),
PhabricatorAuditTransaction::MAILTAG_ADD_CCS =>
pht("A commit's subscribers change."),
PhabricatorAuditTransaction::MAILTAG_PROJECTS =>
pht("A commit's projects change."),
PhabricatorAuditTransaction::MAILTAG_COMMENT =>
pht('Someone comments on a commit.'),
PhabricatorAuditTransaction::MAILTAG_OTHER =>
pht('Other commit activity not listed above occurs.'),
);
}
protected function shouldApplyHeraldRules(
PhabricatorLiskDAO $object,
array $xactions) {
foreach ($xactions as $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorAuditTransaction::TYPE_COMMIT:
$repository = $object->getRepository();
if (!$repository->shouldPublish()) {
return false;
}
return true;
default:
break;
}
}
return parent::shouldApplyHeraldRules($object, $xactions);
}
protected function buildHeraldAdapter(
PhabricatorLiskDAO $object,
array $xactions) {
return id(new HeraldCommitAdapter())
->setCommit($object);
}
protected function didApplyHeraldRules(
PhabricatorLiskDAO $object,
HeraldAdapter $adapter,
HeraldTranscript $transcript) {
$xactions = array();
$audit_phids = $adapter->getAuditMap();
foreach ($audit_phids as $phid => $rule_ids) {
foreach ($rule_ids as $rule_id) {
$this->addAuditReason(
$phid,
pht(
'%s Triggered Audit',
"H{$rule_id}"));
}
}
if ($audit_phids) {
$xactions[] = id(new PhabricatorAuditTransaction())
->setTransactionType(PhabricatorAuditActionConstants::ADD_AUDITORS)
->setNewValue(array_fuse(array_keys($audit_phids)))
->setMetadataValue(
'auditStatus',
PhabricatorAuditStatusConstants::AUDIT_REQUIRED)
->setMetadataValue(
'auditReasonMap', $this->auditReasonMap);
}
$cc_phids = $adapter->getAddCCMap();
$add_ccs = array('+' => array());
foreach ($cc_phids as $phid => $rule_ids) {
$add_ccs['+'][$phid] = $phid;
}
$xactions[] = id(new PhabricatorAuditTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS)
->setNewValue($add_ccs);
HarbormasterBuildable::applyBuildPlans(
$object->getPHID(),
$object->getRepository()->getPHID(),
$adapter->getBuildPlans());
$limit = self::MAX_FILES_SHOWN_IN_EMAIL;
$files = $adapter->loadAffectedPaths();
sort($files);
if (count($files) > $limit) {
array_splice($files, $limit);
$files[] = pht(
'(This commit affected more than %d files. Only %d are shown here '.
'and additional ones are truncated.)',
$limit,
$limit);
}
$this->affectedFiles = implode("\n", $files);
return $xactions;
}
private function isCommitMostlyImported(PhabricatorLiskDAO $object) {
$has_message = PhabricatorRepositoryCommit::IMPORTED_MESSAGE;
$has_changes = PhabricatorRepositoryCommit::IMPORTED_CHANGE;
// Don't publish feed stories or email about events which occur during
// import. In particular, this affects tasks being attached when they are
// closed by "Fixes Txxxx" in a commit message. See T5851.
$mask = ($has_message | $has_changes);
return $object->isPartiallyImported($mask);
}
private function shouldPublishRepositoryActivity(
PhabricatorLiskDAO $object,
array $xactions) {
// not every code path loads the repository so tread carefully
// TODO: They should, and then we should simplify this.
$repository = $object->getRepository($assert_attached = false);
if ($repository != PhabricatorLiskDAO::ATTACHABLE) {
if (!$repository->shouldPublish()) {
return false;
}
}
return $this->isCommitMostlyImported($object);
}
protected function shouldSendMail(
PhabricatorLiskDAO $object,
array $xactions) {
return $this->shouldPublishRepositoryActivity($object, $xactions);
}
protected function shouldEnableMentions(
PhabricatorLiskDAO $object,
array $xactions) {
return $this->shouldPublishRepositoryActivity($object, $xactions);
}
protected function shouldPublishFeedStory(
PhabricatorLiskDAO $object,
array $xactions) {
return $this->shouldPublishRepositoryActivity($object, $xactions);
}
}
diff --git a/src/applications/audit/mail/PhabricatorAuditMailReceiver.php b/src/applications/audit/mail/PhabricatorAuditMailReceiver.php
index 2e0a2725a..29b174826 100644
--- a/src/applications/audit/mail/PhabricatorAuditMailReceiver.php
+++ b/src/applications/audit/mail/PhabricatorAuditMailReceiver.php
@@ -1,28 +1,28 @@
<?php
final class PhabricatorAuditMailReceiver extends PhabricatorObjectMailReceiver {
public function isEnabled() {
- $app_class = 'PhabricatorAuditApplication';
- return PhabricatorApplication::isClassInstalled($app_class);
+ return PhabricatorApplication::isClassInstalled(
+ 'PhabricatorAuditApplication');
}
protected function getObjectPattern() {
return 'C[1-9]\d*';
}
protected function loadObject($pattern, PhabricatorUser $viewer) {
$id = (int)trim($pattern, 'C');
return id(new DiffusionCommitQuery())
->setViewer($viewer)
->withIDs(array($id))
->needAuditRequests(true)
->executeOne();
}
protected function getTransactionReplyHandler() {
return new PhabricatorAuditReplyHandler();
}
}
diff --git a/src/applications/audit/mail/PhabricatorAuditReplyHandler.php b/src/applications/audit/mail/PhabricatorAuditReplyHandler.php
index 6aafbd772..c1eb562bf 100644
--- a/src/applications/audit/mail/PhabricatorAuditReplyHandler.php
+++ b/src/applications/audit/mail/PhabricatorAuditReplyHandler.php
@@ -1,18 +1,21 @@
<?php
final class PhabricatorAuditReplyHandler
extends PhabricatorApplicationTransactionReplyHandler {
public function validateMailReceiver($mail_receiver) {
if (!($mail_receiver instanceof PhabricatorRepositoryCommit)) {
- throw new Exception('Mail receiver is not a commit!');
+ throw new Exception(
+ pht(
+ 'Mail receiver is not a %s!',
+ 'PhabricatorRepositoryCommit'));
}
}
public function getObjectPrefix() {
// TODO: This conflicts with Countdown and will probably need to be
// changed eventually.
return 'C';
}
}
diff --git a/src/applications/audit/management/PhabricatorAuditManagementDeleteWorkflow.php b/src/applications/audit/management/PhabricatorAuditManagementDeleteWorkflow.php
index 2d9438dd4..fa9dbff63 100644
--- a/src/applications/audit/management/PhabricatorAuditManagementDeleteWorkflow.php
+++ b/src/applications/audit/management/PhabricatorAuditManagementDeleteWorkflow.php
@@ -1,283 +1,287 @@
<?php
final class PhabricatorAuditManagementDeleteWorkflow
extends PhabricatorAuditManagementWorkflow {
protected function didConstruct() {
$this
->setName('delete')
->setExamples('**delete** [--dry-run] ...')
- ->setSynopsis('Delete audit requests matching parameters.')
+ ->setSynopsis(pht('Delete audit requests matching parameters.'))
->setArguments(
array(
array(
'name' => 'dry-run',
- 'help' => 'Show what would be deleted, but do not actually delete '.
- 'anything.',
+ 'help' => pht(
+ 'Show what would be deleted, but do not actually delete '.
+ 'anything.'),
),
array(
'name' => 'users',
'param' => 'names',
- 'help' => 'Select only audits by a given list of users.',
+ 'help' => pht('Select only audits by a given list of users.'),
),
array(
'name' => 'repositories',
'param' => 'repos',
- 'help' => 'Select only audits in a given list of repositories.',
+ 'help' => pht(
+ 'Select only audits in a given list of repositories.'),
),
array(
'name' => 'commits',
'param' => 'commits',
- 'help' => 'Select only audits for the given commits.',
+ 'help' => pht('Select only audits for the given commits.'),
),
array(
'name' => 'min-commit-date',
'param' => 'date',
- 'help' => 'Select only audits for commits on or after the given '.
- 'date.',
+ 'help' => pht(
+ 'Select only audits for commits on or after the given date.'),
),
array(
'name' => 'max-commit-date',
'param' => 'date',
- 'help' => 'Select only audits for commits on or before the given '.
- 'date.',
+ 'help' => pht(
+ 'Select only audits for commits on or before the given date.'),
),
array(
'name' => 'status',
'param' => 'status',
- 'help' => 'Select only audits in the given status. By default, '.
- 'only open audits are selected.',
+ 'help' => pht(
+ 'Select only audits in the given status. By default, '.
+ 'only open audits are selected.'),
),
array(
'name' => 'ids',
'param' => 'ids',
- 'help' => 'Select only audits with the given IDs.',
+ 'help' => pht('Select only audits with the given IDs.'),
),
));
}
public function execute(PhutilArgumentParser $args) {
$viewer = $this->getViewer();
$users = $this->loadUsers($args->getArg('users'));
$repos = $this->loadRepos($args->getArg('repositories'));
$commits = $this->loadCommits($args->getArg('commits'));
$ids = $this->parseList($args->getArg('ids'));
$status = $args->getArg('status');
if (!$status) {
$status = DiffusionCommitQuery::AUDIT_STATUS_OPEN;
}
$min_date = $this->loadDate($args->getArg('min-commit-date'));
$max_date = $this->loadDate($args->getArg('max-commit-date'));
if ($min_date && $max_date && ($min_date > $max_date)) {
throw new PhutilArgumentUsageException(
- 'Specified max date must come after specified min date.');
+ pht('Specified maximum date must come after specified minimum date.'));
}
$is_dry_run = $args->getArg('dry-run');
$query = id(new DiffusionCommitQuery())
->setViewer($this->getViewer())
->needAuditRequests(true);
if ($status) {
$query->withAuditStatus($status);
}
$id_map = array();
if ($ids) {
$id_map = array_fuse($ids);
$query->withAuditIDs($ids);
}
if ($repos) {
$query->withRepositoryIDs(mpull($repos, 'getID'));
}
$auditor_map = array();
if ($users) {
$auditor_map = array_fuse(mpull($users, 'getPHID'));
$query->withAuditorPHIDs($auditor_map);
}
if ($commits) {
$query->withPHIDs(mpull($commits, 'getPHID'));
}
$commits = $query->execute();
$commits = mpull($commits, null, 'getPHID');
$audits = array();
foreach ($commits as $commit) {
$commit_audits = $commit->getAudits();
foreach ($commit_audits as $key => $audit) {
if ($id_map && empty($id_map[$audit->getID()])) {
unset($commit_audits[$key]);
continue;
}
if ($auditor_map && empty($auditor_map[$audit->getAuditorPHID()])) {
unset($commit_audits[$key]);
continue;
}
if ($min_date && $commit->getEpoch() < $min_date) {
unset($commit_audits[$key]);
continue;
}
if ($max_date && $commit->getEpoch() > $max_date) {
unset($commit_audits[$key]);
continue;
}
}
$audits[] = $commit_audits;
}
$audits = array_mergev($audits);
$console = PhutilConsole::getConsole();
if (!$audits) {
$console->writeErr("%s\n", pht('No audits match the query.'));
return 0;
}
$handles = id(new PhabricatorHandleQuery())
->setViewer($this->getViewer())
->withPHIDs(mpull($audits, 'getAuditorPHID'))
->execute();
foreach ($audits as $audit) {
$commit = $commits[$audit->getCommitPHID()];
$console->writeOut(
"%s\n",
sprintf(
'%10d %-16s %-16s %s: %s',
$audit->getID(),
$handles[$audit->getAuditorPHID()]->getName(),
PhabricatorAuditStatusConstants::getStatusName(
$audit->getAuditStatus()),
$commit->getRepository()->formatCommitName(
$commit->getCommitIdentifier()),
trim($commit->getSummary())));
}
if (!$is_dry_run) {
$message = pht(
'Really delete these %d audit(s)? They will be permanently deleted '.
'and can not be recovered.',
count($audits));
if ($console->confirm($message)) {
foreach ($audits as $audit) {
$id = $audit->getID();
$console->writeOut("%s\n", pht('Deleting audit %d...', $id));
$audit->delete();
}
}
}
return 0;
}
private function loadUsers($users) {
$users = $this->parseList($users);
if (!$users) {
return null;
}
$objects = id(new PhabricatorPeopleQuery())
->setViewer($this->getViewer())
->withUsernames($users)
->execute();
$objects = mpull($objects, null, 'getUsername');
foreach ($users as $name) {
if (empty($objects[$name])) {
throw new PhutilArgumentUsageException(
pht('No such user with username "%s"!', $name));
}
}
return $objects;
}
private function parseList($list) {
$list = preg_split('/\s*,\s*/', $list);
foreach ($list as $key => $item) {
$list[$key] = trim($item);
}
foreach ($list as $key => $item) {
if (!strlen($item)) {
unset($list[$key]);
}
}
return $list;
}
private function loadRepos($callsigns) {
$callsigns = $this->parseList($callsigns);
if (!$callsigns) {
return null;
}
$repos = id(new PhabricatorRepositoryQuery())
->setViewer($this->getViewer())
->withCallsigns($callsigns)
->execute();
$repos = mpull($repos, null, 'getCallsign');
foreach ($callsigns as $sign) {
if (empty($repos[$sign])) {
throw new PhutilArgumentUsageException(
pht('No such repository with callsign "%s"!', $sign));
}
}
return $repos;
}
private function loadDate($date) {
if (!$date) {
return null;
}
$epoch = strtotime($date);
if (!$epoch || $epoch < 1) {
throw new PhutilArgumentUsageException(
pht(
- 'Unable to parse date "%s". Use a format like "2000-01-01".',
- $date));
+ 'Unable to parse date "%s". Use a format like "%s".',
+ $date,
+ '2000-01-01'));
}
return $epoch;
}
private function loadCommits($commits) {
$names = $this->parseList($commits);
if (!$names) {
return null;
}
$query = id(new DiffusionCommitQuery())
->setViewer($this->getViewer())
->withIdentifiers($names);
$commits = $query->execute();
$map = $query->getIdentifierMap();
foreach ($names as $name) {
if (empty($map[$name])) {
throw new PhutilArgumentUsageException(
pht('No such commit "%s"!', $name));
}
}
return $commits;
}
}
diff --git a/src/applications/audit/view/PhabricatorAuditListView.php b/src/applications/audit/view/PhabricatorAuditListView.php
index 96844c5b4..75aa58db5 100644
--- a/src/applications/audit/view/PhabricatorAuditListView.php
+++ b/src/applications/audit/view/PhabricatorAuditListView.php
@@ -1,176 +1,180 @@
<?php
final class PhabricatorAuditListView extends AphrontView {
private $commits;
private $handles;
private $authorityPHIDs = array();
private $noDataString;
private $highlightedAudits;
public function setHandles(array $handles) {
assert_instances_of($handles, 'PhabricatorObjectHandle');
$this->handles = $handles;
return $this;
}
public function setAuthorityPHIDs(array $phids) {
$this->authorityPHIDs = $phids;
return $this;
}
public function setNoDataString($no_data_string) {
$this->noDataString = $no_data_string;
return $this;
}
public function getNoDataString() {
return $this->noDataString;
}
/**
* These commits should have both commit data and audit requests attached.
*/
public function setCommits(array $commits) {
assert_instances_of($commits, 'PhabricatorRepositoryCommit');
$this->commits = mpull($commits, null, 'getPHID');
return $this;
}
public function getCommits() {
return $this->commits;
}
public function getRequiredHandlePHIDs() {
$phids = array();
$commits = $this->getCommits();
foreach ($commits as $commit) {
$phids[$commit->getPHID()] = true;
$phids[$commit->getAuthorPHID()] = true;
$audits = $commit->getAudits();
foreach ($audits as $audit) {
$phids[$audit->getAuditorPHID()] = true;
}
}
return array_keys($phids);
}
private function getHandle($phid) {
$handle = idx($this->handles, $phid);
if (!$handle) {
- throw new Exception("No handle for '{$phid}'!");
+ throw new Exception(pht("No handle for '%s'!", $phid));
}
return $handle;
}
private function getCommitDescription($phid) {
if ($this->commits === null) {
return pht('(Unknown Commit)');
}
$commit = idx($this->commits, $phid);
if (!$commit) {
return pht('(Unknown Commit)');
}
$summary = $commit->getCommitData()->getSummary();
if (strlen($summary)) {
return $summary;
}
// No summary, so either this is still impoting or just has an empty
// commit message.
if (!$commit->isImported()) {
return pht('(Importing Commit...)');
} else {
return pht('(Untitled Commit)');
}
}
public function render() {
$list = $this->buildList();
$list->setFlush(true);
return $list->render();
}
public function buildList() {
$user = $this->getUser();
if (!$user) {
- throw new Exception('you must setUser() before buildList()!');
+ throw new Exception(
+ pht(
+ 'You must %s before %s!',
+ 'setUser()',
+ __FUNCTION__.'()'));
}
$rowc = array();
$list = new PHUIObjectItemListView();
foreach ($this->commits as $commit) {
$commit_phid = $commit->getPHID();
$commit_handle = $this->getHandle($commit_phid);
$committed = null;
$commit_name = $commit_handle->getName();
$commit_link = $commit_handle->getURI();
$commit_desc = $this->getCommitDescription($commit_phid);
$committed = phabricator_datetime($commit->getEpoch(), $user);
$audits = mpull($commit->getAudits(), null, 'getAuditorPHID');
$auditors = array();
$reasons = array();
foreach ($audits as $audit) {
$auditor_phid = $audit->getAuditorPHID();
$auditors[$auditor_phid] =
$this->getHandle($auditor_phid)->renderLink();
}
$auditors = phutil_implode_html(', ', $auditors);
$authority_audits = array_select_keys($audits, $this->authorityPHIDs);
if ($authority_audits) {
$audit = reset($authority_audits);
} else {
$audit = reset($audits);
}
if ($audit) {
$reasons = $audit->getAuditReasons();
$reasons = phutil_implode_html(', ', $reasons);
$status_code = $audit->getAuditStatus();
$status_text =
PhabricatorAuditStatusConstants::getStatusName($status_code);
$status_color =
PhabricatorAuditStatusConstants::getStatusColor($status_code);
} else {
$reasons = null;
$status_text = null;
$status_color = null;
}
$author_phid = $commit->getAuthorPHID();
if ($author_phid) {
$author_name = $this->getHandle($author_phid)->renderLink();
} else {
$author_name = $commit->getCommitData()->getAuthorName();
}
$item = id(new PHUIObjectItemView())
->setObjectName($commit_name)
->setHeader($commit_desc)
->setHref($commit_link)
->setBarColor($status_color)
->addAttribute($status_text)
->addAttribute($reasons)
->addIcon('none', $committed)
->setSubHead(pht('Author: %s', $author_name));
if (!empty($auditors)) {
$item->addByLine(pht('Auditors: %s', $auditors));
}
$list->addItem($item);
}
if ($this->noDataString) {
$list->setNoDataString($this->noDataString);
}
return $list;
}
}
diff --git a/src/applications/audit/view/PhabricatorAuditTransactionView.php b/src/applications/audit/view/PhabricatorAuditTransactionView.php
index 64a2892ff..305ea19c5 100644
--- a/src/applications/audit/view/PhabricatorAuditTransactionView.php
+++ b/src/applications/audit/view/PhabricatorAuditTransactionView.php
@@ -1,124 +1,124 @@
<?php
final class PhabricatorAuditTransactionView
extends PhabricatorApplicationTransactionView {
private $pathMap;
public function setPathMap(array $path_map) {
$this->pathMap = $path_map;
return $this;
}
public function getPathMap() {
return $this->pathMap;
}
// TODO: This shares a lot of code with Differential and Pholio and should
// probably be merged up.
protected function shouldGroupTransactions(
PhabricatorApplicationTransaction $u,
PhabricatorApplicationTransaction $v) {
if ($u->getAuthorPHID() != $v->getAuthorPHID()) {
// Don't group transactions by different authors.
return false;
}
if (($v->getDateCreated() - $u->getDateCreated()) > 60) {
// Don't group if transactions that happened more than 60s apart.
return false;
}
switch ($u->getTransactionType()) {
case PhabricatorTransactions::TYPE_COMMENT:
case PhabricatorAuditActionConstants::INLINE:
break;
default:
return false;
}
switch ($v->getTransactionType()) {
case PhabricatorAuditActionConstants::INLINE:
return true;
}
return parent::shouldGroupTransactions($u, $v);
}
protected function renderTransactionContent(
PhabricatorApplicationTransaction $xaction) {
$out = array();
$type_inline = PhabricatorAuditActionConstants::INLINE;
$group = $xaction->getTransactionGroup();
if ($xaction->getTransactionType() == $type_inline) {
array_unshift($group, $xaction);
} else {
$out[] = parent::renderTransactionContent($xaction);
}
if (!$group) {
return $out;
}
$inlines = array();
foreach ($group as $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorAuditActionConstants::INLINE:
$inlines[] = $xaction;
break;
default:
- throw new Exception('Unknown grouped transaction type!');
+ throw new Exception(pht('Unknown grouped transaction type!'));
}
}
if ($inlines) {
// TODO: This should do something similar to sortAndGroupInlines() to get
// a stable ordering.
$inlines_by_path = array();
foreach ($inlines as $key => $inline) {
$comment = $inline->getComment();
if (!$comment) {
// TODO: Migrate these away? They probably do not exist on normal
// non-development installs.
unset($inlines[$key]);
continue;
}
$path_id = $comment->getPathID();
$inlines_by_path[$path_id][] = $inline;
}
$inline_view = new PhabricatorInlineSummaryView();
foreach ($inlines_by_path as $path_id => $group) {
$path = idx($this->pathMap, $path_id);
if ($path === null) {
continue;
}
$items = array();
foreach ($group as $inline) {
$comment = $inline->getComment();
$item = array(
'id' => $comment->getID(),
'line' => $comment->getLineNumber(),
'length' => $comment->getLineLength(),
'content' => parent::renderTransactionContent($inline),
);
$items[] = $item;
}
$inline_view->addCommentGroup($path, $items);
}
$out[] = $inline_view;
}
return $out;
}
}
diff --git a/src/applications/auth/conduit/PhabricatorAuthConduitAPIMethod.php b/src/applications/auth/conduit/PhabricatorAuthConduitAPIMethod.php
index 07d2a4b15..34bedec0b 100644
--- a/src/applications/auth/conduit/PhabricatorAuthConduitAPIMethod.php
+++ b/src/applications/auth/conduit/PhabricatorAuthConduitAPIMethod.php
@@ -1,19 +1,17 @@
<?php
abstract class PhabricatorAuthConduitAPIMethod extends ConduitAPIMethod {
final public function getApplication() {
- return PhabricatorApplication::getByClass(
- 'PhabricatorAuthApplication');
+ return PhabricatorApplication::getByClass('PhabricatorAuthApplication');
}
public function getMethodStatus() {
return self::METHOD_STATUS_UNSTABLE;
}
public function getMethodStatusDescription() {
- return pht(
- 'These methods are recently introduced and subject to change.');
+ return pht('These methods are recently introduced and subject to change.');
}
}
diff --git a/src/applications/auth/controller/PhabricatorAuthLoginController.php b/src/applications/auth/controller/PhabricatorAuthLoginController.php
index f00eff32c..e3cbeaa2c 100644
--- a/src/applications/auth/controller/PhabricatorAuthLoginController.php
+++ b/src/applications/auth/controller/PhabricatorAuthLoginController.php
@@ -1,252 +1,254 @@
<?php
final class PhabricatorAuthLoginController
extends PhabricatorAuthController {
private $providerKey;
private $extraURIData;
private $provider;
public function shouldRequireLogin() {
return false;
}
public function shouldAllowRestrictedParameter($parameter_name) {
// Whitelist the OAuth 'code' parameter.
if ($parameter_name == 'code') {
return true;
}
return parent::shouldAllowRestrictedParameter($parameter_name);
}
public function willProcessRequest(array $data) {
$this->providerKey = $data['pkey'];
$this->extraURIData = idx($data, 'extra');
}
public function getExtraURIData() {
return $this->extraURIData;
}
public function processRequest() {
$request = $this->getRequest();
$viewer = $request->getUser();
$response = $this->loadProvider();
if ($response) {
return $response;
}
$provider = $this->provider;
try {
list($account, $response) = $provider->processLoginRequest($this);
} catch (PhutilAuthUserAbortedException $ex) {
if ($viewer->isLoggedIn()) {
// If a logged-in user cancels, take them back to the external accounts
// panel.
$next_uri = '/settings/panel/external/';
} else {
// If a logged-out user cancels, take them back to the auth start page.
$next_uri = '/';
}
// User explicitly hit "Cancel".
$dialog = id(new AphrontDialogView())
->setUser($viewer)
->setTitle(pht('Authentication Canceled'))
->appendChild(
pht('You canceled authentication.'))
->addCancelButton($next_uri, pht('Continue'));
return id(new AphrontDialogResponse())->setDialog($dialog);
}
if ($response) {
return $response;
}
if (!$account) {
throw new Exception(
- 'Auth provider failed to load an account from processLoginRequest()!');
+ pht(
+ 'Auth provider failed to load an account from %s!',
+ 'processLoginRequest()'));
}
if ($account->getUserPHID()) {
// The account is already attached to a Phabricator user, so this is
// either a login or a bad account link request.
if (!$viewer->isLoggedIn()) {
if ($provider->shouldAllowLogin()) {
return $this->processLoginUser($account);
} else {
return $this->renderError(
pht(
'The external account ("%s") you just authenticated with is '.
'not configured to allow logins on this Phabricator install. '.
'An administrator may have recently disabled it.',
$provider->getProviderName()));
}
} else if ($viewer->getPHID() == $account->getUserPHID()) {
// This is either an attempt to re-link an existing and already
// linked account (which is silly) or a refresh of an external account
// (e.g., an OAuth account).
return id(new AphrontRedirectResponse())
->setURI('/settings/panel/external/');
} else {
return $this->renderError(
pht(
'The external account ("%s") you just used to login is already '.
'associated with another Phabricator user account. Login to the '.
'other Phabricator account and unlink the external account before '.
'linking it to a new Phabricator account.',
$provider->getProviderName()));
}
} else {
// The account is not yet attached to a Phabricator user, so this is
// either a registration or an account link request.
if (!$viewer->isLoggedIn()) {
if ($provider->shouldAllowRegistration()) {
return $this->processRegisterUser($account);
} else {
return $this->renderError(
pht(
'The external account ("%s") you just authenticated with is '.
'not configured to allow registration on this Phabricator '.
'install. An administrator may have recently disabled it.',
$provider->getProviderName()));
}
} else {
if ($provider->shouldAllowAccountLink()) {
return $this->processLinkUser($account);
} else {
return $this->renderError(
pht(
'The external account ("%s") you just authenticated with is '.
'not configured to allow account linking on this Phabricator '.
'install. An administrator may have recently disabled it.',
$provider->getProviderName()));
}
}
}
// This should be unreachable, but fail explicitly if we get here somehow.
return new Aphront400Response();
}
private function processLoginUser(PhabricatorExternalAccount $account) {
$user = id(new PhabricatorUser())->loadOneWhere(
'phid = %s',
$account->getUserPHID());
if (!$user) {
return $this->renderError(
pht(
'The external account you just logged in with is not associated '.
'with a valid Phabricator user.'));
}
return $this->loginUser($user);
}
private function processRegisterUser(PhabricatorExternalAccount $account) {
$account_secret = $account->getAccountSecret();
$register_uri = $this->getApplicationURI('register/'.$account_secret.'/');
return $this->setAccountKeyAndContinue($account, $register_uri);
}
private function processLinkUser(PhabricatorExternalAccount $account) {
$account_secret = $account->getAccountSecret();
$confirm_uri = $this->getApplicationURI('confirmlink/'.$account_secret.'/');
return $this->setAccountKeyAndContinue($account, $confirm_uri);
}
private function setAccountKeyAndContinue(
PhabricatorExternalAccount $account,
$next_uri) {
if ($account->getUserPHID()) {
- throw new Exception('Account is already registered or linked.');
+ throw new Exception(pht('Account is already registered or linked.'));
}
// Regenerate the registration secret key, set it on the external account,
// set a cookie on the user's machine, and redirect them to registration.
// See PhabricatorAuthRegisterController for discussion of the registration
// key.
$registration_key = Filesystem::readRandomCharacters(32);
$account->setProperty(
'registrationKey',
PhabricatorHash::digest($registration_key));
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$account->save();
unset($unguarded);
$this->getRequest()->setTemporaryCookie(
PhabricatorCookies::COOKIE_REGISTRATION,
$registration_key);
return id(new AphrontRedirectResponse())->setURI($next_uri);
}
private function loadProvider() {
$provider = PhabricatorAuthProvider::getEnabledProviderByKey(
$this->providerKey);
if (!$provider) {
return $this->renderError(
pht(
'The account you are attempting to login with uses a nonexistent '.
'or disabled authentication provider (with key "%s"). An '.
'administrator may have recently disabled this provider.',
$this->providerKey));
}
$this->provider = $provider;
return null;
}
protected function renderError($message) {
return $this->renderErrorPage(
pht('Login Failed'),
array($message));
}
public function buildProviderPageResponse(
PhabricatorAuthProvider $provider,
$content) {
$crumbs = $this->buildApplicationCrumbs();
$crumbs->setBorder(true);
if ($this->getRequest()->getUser()->isLoggedIn()) {
$crumbs->addTextCrumb(pht('Link Account'), $provider->getSettingsURI());
} else {
$crumbs->addTextCrumb(pht('Login'), $this->getApplicationURI('start/'));
}
$crumbs->addTextCrumb($provider->getProviderName());
return $this->buildApplicationPage(
array(
$crumbs,
$content,
),
array(
'title' => pht('Login'),
));
}
public function buildProviderErrorResponse(
PhabricatorAuthProvider $provider,
$message) {
$message = pht(
'Authentication provider ("%s") encountered an error during login. %s',
$provider->getProviderName(),
$message);
return $this->renderError($message);
}
}
diff --git a/src/applications/auth/controller/PhabricatorAuthNeedsMultiFactorController.php b/src/applications/auth/controller/PhabricatorAuthNeedsMultiFactorController.php
index 57516ac6b..975355ec9 100644
--- a/src/applications/auth/controller/PhabricatorAuthNeedsMultiFactorController.php
+++ b/src/applications/auth/controller/PhabricatorAuthNeedsMultiFactorController.php
@@ -1,91 +1,91 @@
<?php
final class PhabricatorAuthNeedsMultiFactorController
extends PhabricatorAuthController {
public function shouldRequireMultiFactorEnrollment() {
// Users need access to this controller in order to enroll in multi-factor
// auth.
return false;
}
public function processRequest() {
$request = $this->getRequest();
$viewer = $request->getUser();
$panel = id(new PhabricatorMultiFactorSettingsPanel())
->setUser($viewer)
->setViewer($viewer)
->setOverrideURI($this->getApplicationURI('/multifactor/'))
->processRequest($request);
if ($panel instanceof AphrontResponse) {
return $panel;
}
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb(pht('Add Multi-Factor Auth'));
$viewer->updateMultiFactorEnrollment();
if (!$viewer->getIsEnrolledInMultiFactor()) {
$help = id(new PHUIInfoView())
->setTitle(pht('Add Multi-Factor Authentication To Your Account'))
->setSeverity(PHUIInfoView::SEVERITY_WARNING)
->setErrors(
array(
pht(
'Before you can use Phabricator, you need to add multi-factor '.
'authentication to your account.'),
pht(
'Multi-factor authentication helps secure your account by '.
'making it more difficult for attackers to gain access or '.
- 'take senstive actions.'),
+ 'take sensitive actions.'),
pht(
'To learn more about multi-factor authentication, click the '.
'%s button below.',
phutil_tag('strong', array(), pht('Help'))),
pht(
'To add an authentication factor, click the %s button below.',
phutil_tag('strong', array(), pht('Add Authentication Factor'))),
pht(
'To continue, add at least one authentication factor to your '.
'account.'),
));
} else {
$help = id(new PHUIInfoView())
->setTitle(pht('Multi-Factor Authentication Configured'))
->setSeverity(PHUIInfoView::SEVERITY_NOTICE)
->setErrors(
array(
pht(
'You have successfully configured multi-factor authentication '.
'for your account.'),
pht(
'You can make adjustments from the Settings panel later.'),
pht(
'When you are ready, %s.',
phutil_tag(
'strong',
array(),
phutil_tag(
'a',
array(
'href' => '/',
),
pht('continue to Phabricator')))),
));
}
return $this->buildApplicationPage(
array(
$crumbs,
$help,
$panel,
),
array(
'title' => pht('Add Multi-Factor Authentication'),
));
}
}
diff --git a/src/applications/auth/controller/PhabricatorAuthRegisterController.php b/src/applications/auth/controller/PhabricatorAuthRegisterController.php
index d27a64448..934134514 100644
--- a/src/applications/auth/controller/PhabricatorAuthRegisterController.php
+++ b/src/applications/auth/controller/PhabricatorAuthRegisterController.php
@@ -1,657 +1,656 @@
<?php
final class PhabricatorAuthRegisterController
extends PhabricatorAuthController {
private $accountKey;
public function shouldRequireLogin() {
return false;
}
public function willProcessRequest(array $data) {
$this->accountKey = idx($data, 'akey');
}
public function processRequest() {
$request = $this->getRequest();
if ($request->getUser()->isLoggedIn()) {
return $this->renderError(pht('You are already logged in.'));
}
$is_setup = false;
if (strlen($this->accountKey)) {
$result = $this->loadAccountForRegistrationOrLinking($this->accountKey);
list($account, $provider, $response) = $result;
$is_default = false;
} else if ($this->isFirstTimeSetup()) {
list($account, $provider, $response) = $this->loadSetupAccount();
$is_default = true;
$is_setup = true;
} else {
list($account, $provider, $response) = $this->loadDefaultAccount();
$is_default = true;
}
if ($response) {
return $response;
}
$invite = $this->loadInvite();
if (!$provider->shouldAllowRegistration()) {
if ($invite) {
// If the user has an invite, we allow them to register with any
// provider, even a login-only provider.
} else {
// TODO: This is a routine error if you click "Login" on an external
// auth source which doesn't allow registration. The error should be
// more tailored.
return $this->renderError(
pht(
'The account you are attempting to register with uses an '.
'authentication provider ("%s") which does not allow '.
'registration. An administrator may have recently disabled '.
'registration with this provider.',
$provider->getProviderName()));
}
}
$user = new PhabricatorUser();
$default_username = $account->getUsername();
$default_realname = $account->getRealName();
$default_email = $account->getEmail();
if ($invite) {
$default_email = $invite->getEmailAddress();
}
if (!PhabricatorUserEmail::isValidAddress($default_email)) {
$default_email = null;
}
if ($default_email !== null) {
// We should bypass policy here becase e.g. limiting an application use
// to a subset of users should not allow the others to overwrite
// configured application emails
$application_email = id(new PhabricatorMetaMTAApplicationEmailQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withAddresses(array($default_email))
->executeOne();
if ($application_email) {
$default_email = null;
}
}
if ($default_email !== null) {
// If the account source provided an email, but it's not allowed by
// the configuration, roadblock the user. Previously, we let the user
// pick a valid email address instead, but this does not align well with
// user expectation and it's not clear the cases it enables are valuable.
// See discussion in T3472.
if (!PhabricatorUserEmail::isAllowedAddress($default_email)) {
return $this->renderError(
array(
pht(
'The account you are attempting to register with has an invalid '.
'email address (%s). This Phabricator install only allows '.
'registration with specific email addresses:',
$default_email),
phutil_tag('br'),
phutil_tag('br'),
PhabricatorUserEmail::describeAllowedAddresses(),
));
}
// If the account source provided an email, but another account already
// has that email, just pretend we didn't get an email.
// TODO: See T3472.
if ($default_email !== null) {
$same_email = id(new PhabricatorUserEmail())->loadOneWhere(
'address = %s',
$default_email);
if ($same_email) {
if ($invite) {
// We're allowing this to continue. The fact that we loaded the
// invite means that the address is nonprimary and unverified and
// we're OK to steal it.
} else {
$default_email = null;
}
}
}
}
$profile = id(new PhabricatorRegistrationProfile())
->setDefaultUsername($default_username)
->setDefaultEmail($default_email)
->setDefaultRealName($default_realname)
->setCanEditUsername(true)
->setCanEditEmail(($default_email === null))
->setCanEditRealName(true)
->setShouldVerifyEmail(false);
$event_type = PhabricatorEventType::TYPE_AUTH_WILLREGISTERUSER;
$event_data = array(
'account' => $account,
'profile' => $profile,
);
$event = id(new PhabricatorEvent($event_type, $event_data))
->setUser($user);
PhutilEventEngine::dispatchEvent($event);
$default_username = $profile->getDefaultUsername();
$default_email = $profile->getDefaultEmail();
$default_realname = $profile->getDefaultRealName();
$can_edit_username = $profile->getCanEditUsername();
$can_edit_email = $profile->getCanEditEmail();
$can_edit_realname = $profile->getCanEditRealName();
$must_set_password = $provider->shouldRequireRegistrationPassword();
$can_edit_anything = $profile->getCanEditAnything() || $must_set_password;
$force_verify = $profile->getShouldVerifyEmail();
// Automatically verify the administrator's email address during first-time
// setup.
if ($is_setup) {
$force_verify = true;
}
$value_username = $default_username;
$value_realname = $default_realname;
$value_email = $default_email;
$value_password = null;
$errors = array();
$require_real_name = PhabricatorEnv::getEnvConfig('user.require-real-name');
$e_username = strlen($value_username) ? null : true;
$e_realname = $require_real_name ? true : null;
$e_email = strlen($value_email) ? null : true;
$e_password = true;
$e_captcha = true;
$skip_captcha = false;
if ($invite) {
// If the user is accepting an invite, assume they're trustworthy enough
// that we don't need to CAPTCHA them.
$skip_captcha = true;
}
$min_len = PhabricatorEnv::getEnvConfig('account.minimum-password-length');
$min_len = (int)$min_len;
$from_invite = $request->getStr('invite');
if ($from_invite && $can_edit_username) {
$value_username = $request->getStr('username');
$e_username = null;
}
if (($request->isFormPost() || !$can_edit_anything) && !$from_invite) {
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
if ($must_set_password && !$skip_captcha) {
$e_captcha = pht('Again');
$captcha_ok = AphrontFormRecaptchaControl::processCaptcha($request);
if (!$captcha_ok) {
$errors[] = pht('Captcha response is incorrect, try again.');
$e_captcha = pht('Invalid');
}
}
if ($can_edit_username) {
$value_username = $request->getStr('username');
if (!strlen($value_username)) {
$e_username = pht('Required');
$errors[] = pht('Username is required.');
} else if (!PhabricatorUser::validateUsername($value_username)) {
$e_username = pht('Invalid');
$errors[] = PhabricatorUser::describeValidUsername();
} else {
$e_username = null;
}
}
if ($must_set_password) {
$value_password = $request->getStr('password');
$value_confirm = $request->getStr('confirm');
if (!strlen($value_password)) {
$e_password = pht('Required');
$errors[] = pht('You must choose a password.');
} else if ($value_password !== $value_confirm) {
$e_password = pht('No Match');
$errors[] = pht('Password and confirmation must match.');
} else if (strlen($value_password) < $min_len) {
$e_password = pht('Too Short');
$errors[] = pht(
'Password is too short (must be at least %d characters long).',
$min_len);
} else if (
PhabricatorCommonPasswords::isCommonPassword($value_password)) {
$e_password = pht('Very Weak');
$errors[] = pht(
'Password is pathologically weak. This password is one of the '.
'most common passwords in use, and is extremely easy for '.
'attackers to guess. You must choose a stronger password.');
} else {
$e_password = null;
}
}
if ($can_edit_email) {
$value_email = $request->getStr('email');
if (!strlen($value_email)) {
$e_email = pht('Required');
$errors[] = pht('Email is required.');
} else if (!PhabricatorUserEmail::isValidAddress($value_email)) {
$e_email = pht('Invalid');
$errors[] = PhabricatorUserEmail::describeValidAddresses();
} else if (!PhabricatorUserEmail::isAllowedAddress($value_email)) {
$e_email = pht('Disallowed');
$errors[] = PhabricatorUserEmail::describeAllowedAddresses();
} else {
$e_email = null;
}
}
if ($can_edit_realname) {
$value_realname = $request->getStr('realName');
if (!strlen($value_realname) && $require_real_name) {
$e_realname = pht('Required');
$errors[] = pht('Real name is required.');
} else {
$e_realname = null;
}
}
if (!$errors) {
$image = $this->loadProfilePicture($account);
if ($image) {
$user->setProfileImagePHID($image->getPHID());
}
try {
$verify_email = false;
if ($force_verify) {
$verify_email = true;
}
if ($value_email === $default_email) {
if ($account->getEmailVerified()) {
$verify_email = true;
}
if ($provider->shouldTrustEmails()) {
$verify_email = true;
}
if ($invite) {
$verify_email = true;
}
}
$email_obj = null;
if ($invite) {
// If we have a valid invite, this email may exist but be
// nonprimary and unverified, so we'll reassign it.
$email_obj = id(new PhabricatorUserEmail())->loadOneWhere(
'address = %s',
$value_email);
}
if (!$email_obj) {
$email_obj = id(new PhabricatorUserEmail())
->setAddress($value_email);
}
$email_obj->setIsVerified((int)$verify_email);
$user->setUsername($value_username);
$user->setRealname($value_realname);
if ($is_setup) {
$must_approve = false;
} else if ($invite) {
$must_approve = false;
} else {
$must_approve = PhabricatorEnv::getEnvConfig(
'auth.require-approval');
}
if ($must_approve) {
$user->setIsApproved(0);
} else {
$user->setIsApproved(1);
}
if ($invite) {
$allow_reassign_email = true;
} else {
$allow_reassign_email = false;
}
$user->openTransaction();
$editor = id(new PhabricatorUserEditor())
->setActor($user);
$editor->createNewUser($user, $email_obj, $allow_reassign_email);
if ($must_set_password) {
$envelope = new PhutilOpaqueEnvelope($value_password);
$editor->changePassword($user, $envelope);
}
if ($is_setup) {
$editor->makeAdminUser($user, true);
}
$account->setUserPHID($user->getPHID());
$provider->willRegisterAccount($account);
$account->save();
$user->saveTransaction();
if (!$email_obj->getIsVerified()) {
$email_obj->sendVerificationEmail($user);
}
if ($must_approve) {
$this->sendWaitingForApprovalEmail($user);
}
if ($invite) {
$invite->setAcceptedByPHID($user->getPHID())->save();
}
return $this->loginUser($user);
} catch (AphrontDuplicateKeyQueryException $exception) {
$same_username = id(new PhabricatorUser())->loadOneWhere(
'userName = %s',
$user->getUserName());
$same_email = id(new PhabricatorUserEmail())->loadOneWhere(
'address = %s',
$value_email);
if ($same_username) {
$e_username = pht('Duplicate');
$errors[] = pht('Another user already has that username.');
}
if ($same_email) {
// TODO: See T3340.
$e_email = pht('Duplicate');
$errors[] = pht('Another user already has that email.');
}
if (!$same_username && !$same_email) {
throw $exception;
}
}
}
unset($unguarded);
}
$form = id(new AphrontFormView())
->setUser($request->getUser());
if (!$is_default) {
$form->appendChild(
id(new AphrontFormMarkupControl())
->setLabel(pht('External Account'))
->setValue(
id(new PhabricatorAuthAccountView())
->setUser($request->getUser())
->setExternalAccount($account)
->setAuthProvider($provider)));
}
if ($can_edit_username) {
$form->appendChild(
id(new AphrontFormTextControl())
->setLabel(pht('Phabricator Username'))
->setName('username')
->setValue($value_username)
->setError($e_username));
} else {
$form->appendChild(
id(new AphrontFormMarkupControl())
->setLabel(pht('Phabricator Username'))
->setValue($value_username)
->setError($e_username));
}
if ($can_edit_realname) {
$form->appendChild(
id(new AphrontFormTextControl())
->setLabel(pht('Real Name'))
->setName('realName')
->setValue($value_realname)
->setError($e_realname));
}
if ($must_set_password) {
$form->appendChild(
id(new AphrontFormPasswordControl())
->setLabel(pht('Password'))
->setName('password')
->setError($e_password));
$form->appendChild(
id(new AphrontFormPasswordControl())
->setLabel(pht('Confirm Password'))
->setName('confirm')
->setError($e_password)
->setCaption(
$min_len
? pht('Minimum length of %d characters.', $min_len)
: null));
}
if ($can_edit_email) {
$form->appendChild(
id(new AphrontFormTextControl())
->setLabel(pht('Email'))
->setName('email')
->setValue($value_email)
->setCaption(PhabricatorUserEmail::describeAllowedAddresses())
->setError($e_email));
}
if ($must_set_password && !$skip_captcha) {
$form->appendChild(
id(new AphrontFormRecaptchaControl())
->setLabel(pht('Captcha'))
->setError($e_captcha));
}
$submit = id(new AphrontFormSubmitControl());
if ($is_setup) {
$submit
->setValue(pht('Create Admin Account'));
} else {
$submit
->addCancelButton($this->getApplicationURI('start/'))
->setValue(pht('Register Phabricator Account'));
}
$form->appendChild($submit);
$crumbs = $this->buildApplicationCrumbs();
if ($is_setup) {
$crumbs->addTextCrumb(pht('Setup Admin Account'));
$title = pht('Welcome to Phabricator');
} else {
$crumbs->addTextCrumb(pht('Register'));
$crumbs->addTextCrumb($provider->getProviderName());
$title = pht('Phabricator Registration');
}
$welcome_view = null;
if ($is_setup) {
$welcome_view = id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_NOTICE)
->setTitle(pht('Welcome to Phabricator'))
->appendChild(
pht(
'Installation is complete. Register your administrator account '.
'below to log in. You will be able to configure options and add '.
'other authentication mechanisms (like LDAP or OAuth) later on.'));
}
$object_box = id(new PHUIObjectBoxView())
->setHeaderText($title)
->setForm($form)
->setFormErrors($errors);
$invite_header = null;
if ($invite) {
$invite_header = $this->renderInviteHeader($invite);
}
return $this->buildApplicationPage(
array(
$crumbs,
$welcome_view,
$invite_header,
$object_box,
),
array(
'title' => $title,
));
}
private function loadDefaultAccount() {
$providers = PhabricatorAuthProvider::getAllEnabledProviders();
$account = null;
$provider = null;
$response = null;
foreach ($providers as $key => $candidate_provider) {
if (!$candidate_provider->shouldAllowRegistration()) {
unset($providers[$key]);
continue;
}
if (!$candidate_provider->isDefaultRegistrationProvider()) {
unset($providers[$key]);
}
}
if (!$providers) {
$response = $this->renderError(
pht(
'There are no configured default registration providers.'));
return array($account, $provider, $response);
} else if (count($providers) > 1) {
$response = $this->renderError(
- pht(
- 'There are too many configured default registration providers.'));
+ pht('There are too many configured default registration providers.'));
return array($account, $provider, $response);
}
$provider = head($providers);
$account = $provider->getDefaultExternalAccount();
return array($account, $provider, $response);
}
private function loadSetupAccount() {
$provider = new PhabricatorPasswordAuthProvider();
$provider->attachProviderConfig(
id(new PhabricatorAuthProviderConfig())
->setShouldAllowRegistration(1)
->setShouldAllowLogin(1)
->setIsEnabled(true));
$account = $provider->getDefaultExternalAccount();
$response = null;
return array($account, $provider, $response);
}
private function loadProfilePicture(PhabricatorExternalAccount $account) {
$phid = $account->getProfileImagePHID();
if (!$phid) {
return null;
}
// NOTE: Use of omnipotent user is okay here because the registering user
// can not control the field value, and we can't use their user object to
// do meaningful policy checks anyway since they have not registered yet.
// Reaching this means the user holds the account secret key and the
// registration secret key, and thus has permission to view the image.
$file = id(new PhabricatorFileQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withPHIDs(array($phid))
->executeOne();
if (!$file) {
return null;
}
$xform = PhabricatorFileTransform::getTransformByKey(
PhabricatorFileThumbnailTransform::TRANSFORM_PROFILE);
return $xform->executeTransform($file);
}
protected function renderError($message) {
return $this->renderErrorPage(
pht('Registration Failed'),
array($message));
}
private function sendWaitingForApprovalEmail(PhabricatorUser $user) {
$title = '[Phabricator] '.pht(
'New User "%s" Awaiting Approval',
$user->getUsername());
$body = new PhabricatorMetaMTAMailBody();
$body->addRawSection(
pht(
'Newly registered user "%s" is awaiting account approval by an '.
'administrator.',
$user->getUsername()));
$body->addLinkSection(
pht('APPROVAL QUEUE'),
PhabricatorEnv::getProductionURI(
'/people/query/approval/'));
$body->addLinkSection(
pht('DISABLE APPROVAL QUEUE'),
PhabricatorEnv::getProductionURI(
'/config/edit/auth.require-approval/'));
$admins = id(new PhabricatorPeopleQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withIsAdmin(true)
->execute();
if (!$admins) {
return;
}
$mail = id(new PhabricatorMetaMTAMail())
->addTos(mpull($admins, 'getPHID'))
->setSubject($title)
->setBody($body->render())
->saveAndSend();
}
}
diff --git a/src/applications/auth/controller/PhabricatorAuthSSHKeyGenerateController.php b/src/applications/auth/controller/PhabricatorAuthSSHKeyGenerateController.php
index d055db514..2cb6dc81e 100644
--- a/src/applications/auth/controller/PhabricatorAuthSSHKeyGenerateController.php
+++ b/src/applications/auth/controller/PhabricatorAuthSSHKeyGenerateController.php
@@ -1,92 +1,91 @@
<?php
final class PhabricatorAuthSSHKeyGenerateController
extends PhabricatorAuthSSHKeyController {
public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
$key = $this->newKeyForObjectPHID($request->getStr('objectPHID'));
if (!$key) {
return new Aphront404Response();
}
$cancel_uri = $key->getObject()->getSSHPublicKeyManagementURI($viewer);
$token = id(new PhabricatorAuthSessionEngine())->requireHighSecuritySession(
$viewer,
$request,
$cancel_uri);
if ($request->isFormPost()) {
$default_name = $key->getObject()->getSSHKeyDefaultName();
$keys = PhabricatorSSHKeyGenerator::generateKeypair();
list($public_key, $private_key) = $keys;
$file = PhabricatorFile::buildFromFileDataOrHash(
$private_key,
array(
'name' => $default_name.'.key',
'ttl' => time() + (60 * 10),
'viewPolicy' => $viewer->getPHID(),
));
$public_key = PhabricatorAuthSSHPublicKey::newFromRawKey($public_key);
$type = $public_key->getType();
$body = $public_key->getBody();
$key
->setName($default_name)
->setKeyType($type)
->setKeyBody($body)
->setKeyComment(pht('Generated'))
->save();
// NOTE: We're disabling workflow on submit so the download works. We're
// disabling workflow on cancel so the page reloads, showing the new
// key.
return $this->newDialog()
->setTitle(pht('Download Private Key'))
->setDisableWorkflowOnCancel(true)
->setDisableWorkflowOnSubmit(true)
->setSubmitURI($file->getDownloadURI())
->appendParagraph(
pht(
- 'A keypair has been generated, and the public key has been '.
- 'added as a recognized key. Use the button below to download '.
- 'the private key.'))
+ 'A keypair has been generated, and the public key has been '.
+ 'added as a recognized key. Use the button below to download '.
+ 'the private key.'))
->appendParagraph(
pht(
'After you download the private key, it will be destroyed. '.
'You will not be able to retrieve it if you lose your copy.'))
->addSubmitButton(pht('Download Private Key'))
->addCancelButton($cancel_uri, pht('Done'));
}
try {
PhabricatorSSHKeyGenerator::assertCanGenerateKeypair();
return $this->newDialog()
->setTitle(pht('Generate New Keypair'))
->addHiddenInput('objectPHID', $key->getObject()->getPHID())
->appendParagraph(
pht(
'This workflow will generate a new SSH keypair, add the public '.
'key, and let you download the private key.'))
->appendParagraph(
- pht(
- 'Phabricator will not retain a copy of the private key.'))
+ pht('Phabricator will not retain a copy of the private key.'))
->addSubmitButton(pht('Generate New Keypair'))
->addCancelButton($cancel_uri);
} catch (Exception $ex) {
return $this->newDialog()
->setTitle(pht('Unable to Generate Keys'))
->appendParagraph($ex->getMessage())
->addCancelButton($cancel_uri);
}
}
}
diff --git a/src/applications/auth/controller/PhabricatorAuthStartController.php b/src/applications/auth/controller/PhabricatorAuthStartController.php
index 7ea10690e..f9b9f7cb4 100644
--- a/src/applications/auth/controller/PhabricatorAuthStartController.php
+++ b/src/applications/auth/controller/PhabricatorAuthStartController.php
@@ -1,243 +1,243 @@
<?php
final class PhabricatorAuthStartController
extends PhabricatorAuthController {
public function shouldRequireLogin() {
return false;
}
public function handleRequest(AphrontRequest $request) {
$viewer = $request->getUser();
if ($viewer->isLoggedIn()) {
// Kick the user home if they are already logged in.
return id(new AphrontRedirectResponse())->setURI('/');
}
if ($request->isAjax()) {
return $this->processAjaxRequest();
}
if ($request->isConduit()) {
return $this->processConduitRequest();
}
// If the user gets this far, they aren't logged in, so if they have a
// user session token we can conclude that it's invalid: if it was valid,
// they'd have been logged in above and never made it here. Try to clear
// it and warn the user they may need to nuke their cookies.
$session_token = $request->getCookie(PhabricatorCookies::COOKIE_SESSION);
if (strlen($session_token)) {
$kind = PhabricatorAuthSessionEngine::getSessionKindFromToken(
$session_token);
switch ($kind) {
case PhabricatorAuthSessionEngine::KIND_ANONYMOUS:
// If this is an anonymous session. It's expected that they won't
// be logged in, so we can just continue.
break;
default:
// The session cookie is invalid, so clear it.
$request->clearCookie(PhabricatorCookies::COOKIE_USERNAME);
$request->clearCookie(PhabricatorCookies::COOKIE_SESSION);
return $this->renderError(
pht(
'Your login session is invalid. Try reloading the page and '.
'logging in again. If that does not work, clear your browser '.
'cookies.'));
}
}
$providers = PhabricatorAuthProvider::getAllEnabledProviders();
foreach ($providers as $key => $provider) {
if (!$provider->shouldAllowLogin()) {
unset($providers[$key]);
}
}
if (!$providers) {
if ($this->isFirstTimeSetup()) {
// If this is a fresh install, let the user register their admin
// account.
return id(new AphrontRedirectResponse())
->setURI($this->getApplicationURI('/register/'));
}
return $this->renderError(
pht(
'This Phabricator install is not configured with any enabled '.
'authentication providers which can be used to log in. If you '.
'have accidentally locked yourself out by disabling all providers, '.
- 'you can use `phabricator/bin/auth recover <username>` to '.
- 'recover access to an administrative account.'));
+ 'you can use `%s` to recover access to an administrative account.'.
+ 'phabricator/bin/auth recover <username>'));
}
$next_uri = $request->getStr('next');
if (!strlen($next_uri)) {
if ($this->getDelegatingController()) {
// Only set a next URI from the request path if this controller was
// delegated to, which happens when a user tries to view a page which
// requires them to login.
// If this controller handled the request directly, we're on the main
// login page, and never want to redirect the user back here after they
// login.
$next_uri = (string)$this->getRequest()->getRequestURI();
}
}
if (!$request->isFormPost()) {
if (strlen($next_uri)) {
PhabricatorCookies::setNextURICookie($request, $next_uri);
}
PhabricatorCookies::setClientIDCookie($request);
}
if (!$request->getURIData('loggedout') && count($providers) == 1) {
$auto_login_provider = head($providers);
$auto_login_config = $auto_login_provider->getProviderConfig();
if ($auto_login_provider instanceof PhabricatorPhabricatorAuthProvider &&
$auto_login_config->getShouldAutoLogin()) {
$auto_login_adapter = $provider->getAdapter();
$auto_login_adapter->setState($provider->getAuthCSRFCode($request));
return id(new AphrontRedirectResponse())
->setIsExternal(true)
->setURI($provider->getAdapter()->getAuthenticateURI());
}
}
$invite = $this->loadInvite();
$not_buttons = array();
$are_buttons = array();
$providers = msort($providers, 'getLoginOrder');
foreach ($providers as $provider) {
if ($invite) {
$form = $provider->buildInviteForm($this);
} else {
$form = $provider->buildLoginForm($this);
}
if ($provider->isLoginFormAButton()) {
$are_buttons[] = $form;
} else {
$not_buttons[] = $form;
}
}
$out = array();
$out[] = $not_buttons;
if ($are_buttons) {
require_celerity_resource('auth-css');
foreach ($are_buttons as $key => $button) {
$are_buttons[$key] = phutil_tag(
'div',
array(
'class' => 'phabricator-login-button mmb',
),
$button);
}
// If we only have one button, add a second pretend button so that we
// always have two columns. This makes it easier to get the alignments
// looking reasonable.
if (count($are_buttons) == 1) {
$are_buttons[] = null;
}
$button_columns = id(new AphrontMultiColumnView())
->setFluidLayout(true);
$are_buttons = array_chunk($are_buttons, ceil(count($are_buttons) / 2));
foreach ($are_buttons as $column) {
$button_columns->addColumn($column);
}
$out[] = phutil_tag(
'div',
array(
'class' => 'phabricator-login-buttons',
),
$button_columns);
}
$login_message = PhabricatorEnv::getEnvConfig('auth.login-message');
$login_message = phutil_safe_html($login_message);
$invite_message = null;
if ($invite) {
$invite_message = $this->renderInviteHeader($invite);
}
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb(pht('Login'));
$crumbs->setBorder(true);
return $this->buildApplicationPage(
array(
$crumbs,
$login_message,
$invite_message,
$out,
),
array(
'title' => pht('Login to Phabricator'),
));
}
private function processAjaxRequest() {
$request = $this->getRequest();
$viewer = $request->getUser();
// We end up here if the user clicks a workflow link that they need to
// login to use. We give them a dialog saying "You need to login...".
if ($request->isDialogFormPost()) {
return id(new AphrontRedirectResponse())->setURI(
$request->getRequestURI());
}
$dialog = new AphrontDialogView();
$dialog->setUser($viewer);
$dialog->setTitle(pht('Login Required'));
$dialog->appendChild(pht('You must login to continue.'));
$dialog->addSubmitButton(pht('Login'));
$dialog->addCancelButton('/');
return id(new AphrontDialogResponse())->setDialog($dialog);
}
private function processConduitRequest() {
$request = $this->getRequest();
$viewer = $request->getUser();
// A common source of errors in Conduit client configuration is getting
// the request path wrong. The client will end up here, so make some
// effort to give them a comprehensible error message.
$request_path = $this->getRequest()->getPath();
$conduit_path = '/api/<method>';
$example_path = '/api/conduit.ping';
$message = pht(
'ERROR: You are making a Conduit API request to "%s", but the correct '.
'HTTP request path to use in order to access a COnduit method is "%s" '.
'(for example, "%s"). Check your configuration.',
$request_path,
$conduit_path,
$example_path);
return id(new AphrontPlainTextResponse())->setContent($message);
}
protected function renderError($message) {
return $this->renderErrorPage(
pht('Authentication Failure'),
array($message));
}
}
diff --git a/src/applications/auth/controller/PhabricatorEmailLoginController.php b/src/applications/auth/controller/PhabricatorEmailLoginController.php
index d21ec5a1b..9db360d51 100644
--- a/src/applications/auth/controller/PhabricatorEmailLoginController.php
+++ b/src/applications/auth/controller/PhabricatorEmailLoginController.php
@@ -1,172 +1,166 @@
<?php
final class PhabricatorEmailLoginController
extends PhabricatorAuthController {
public function shouldRequireLogin() {
return false;
}
public function processRequest() {
$request = $this->getRequest();
if (!PhabricatorPasswordAuthProvider::getPasswordProvider()) {
return new Aphront400Response();
}
$e_email = true;
$e_captcha = true;
$errors = array();
$is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business');
if ($request->isFormPost()) {
$e_email = null;
$e_captcha = pht('Again');
$captcha_ok = AphrontFormRecaptchaControl::processCaptcha($request);
if (!$captcha_ok) {
$errors[] = pht('Captcha response is incorrect, try again.');
$e_captcha = pht('Invalid');
}
$email = $request->getStr('email');
if (!strlen($email)) {
$errors[] = pht('You must provide an email address.');
$e_email = pht('Required');
}
if (!$errors) {
// NOTE: Don't validate the email unless the captcha is good; this makes
// it expensive to fish for valid email addresses while giving the user
// a better error if they goof their email.
$target_email = id(new PhabricatorUserEmail())->loadOneWhere(
'address = %s',
$email);
$target_user = null;
if ($target_email) {
$target_user = id(new PhabricatorUser())->loadOneWhere(
'phid = %s',
$target_email->getUserPHID());
}
if (!$target_user) {
$errors[] =
pht('There is no account associated with that email address.');
$e_email = pht('Invalid');
}
// If this address is unverified, only send a reset link to it if
// the account has no verified addresses. This prevents an opportunistic
// attacker from compromising an account if a user adds an email
// address but mistypes it and doesn't notice.
// (For a newly created account, all the addresses may be unverified,
// which is why we'll send to an unverified address in that case.)
if ($target_email && !$target_email->getIsVerified()) {
$verified_addresses = id(new PhabricatorUserEmail())->loadAllWhere(
'userPHID = %s AND isVerified = 1',
$target_email->getUserPHID());
if ($verified_addresses) {
$errors[] = pht(
- 'That email addess is not verified. You can only send '.
+ 'That email address is not verified. You can only send '.
'password reset links to a verified address.');
$e_email = pht('Unverified');
}
}
if (!$errors) {
$engine = new PhabricatorAuthSessionEngine();
$uri = $engine->getOneTimeLoginURI(
$target_user,
null,
PhabricatorAuthSessionEngine::ONETIME_RESET);
if ($is_serious) {
- $body = <<<EOBODY
-You can use this link to reset your Phabricator password:
-
- {$uri}
-
-EOBODY;
+ $body = pht(
+ "You can use this link to reset your Phabricator password:".
+ "\n\n %s\n",
+ $uri);
} else {
- $body = <<<EOBODY
-Condolences on forgetting your password. You can use this link to reset it:
-
- {$uri}
+ $body = pht(
+ "Condolences on forgetting your password. You can use this ".
+ "link to reset it:\n\n".
+ " %s\n\n".
+ "After you set a new password, consider writing it down on a ".
+ "sticky note and attaching it to your monitor so you don't ".
+ "forget again! Choosing a very short, easy-to-remember password ".
+ "like \"cat\" or \"1234\" might also help.\n\n".
+ "Best Wishes,\nPhabricator\n",
+ $uri);
-After you set a new password, consider writing it down on a sticky note and
-attaching it to your monitor so you don't forget again! Choosing a very short,
-easy-to-remember password like "cat" or "1234" might also help.
-
-Best Wishes,
-Phabricator
-
-EOBODY;
}
$mail = id(new PhabricatorMetaMTAMail())
->setSubject(pht('[Phabricator] Password Reset'))
->setForceDelivery(true)
->addRawTos(array($target_email->getAddress()))
->setBody($body)
->saveAndSend();
return $this->newDialog()
->setTitle(pht('Check Your Email'))
->setShortTitle(pht('Email Sent'))
->appendParagraph(
pht('An email has been sent with a link you can use to login.'))
->addCancelButton('/', pht('Done'));
}
}
-
}
$error_view = null;
if ($errors) {
$error_view = new PHUIInfoView();
$error_view->setErrors($errors);
}
$email_auth = new PHUIFormLayoutView();
$email_auth->appendChild($error_view);
$email_auth
->setUser($request->getUser())
->setFullWidth(true)
->appendChild(
id(new AphrontFormTextControl())
->setLabel(pht('Email'))
->setName('email')
->setValue($request->getStr('email'))
->setError($e_email))
->appendChild(
id(new AphrontFormRecaptchaControl())
->setLabel(pht('Captcha'))
->setError($e_captcha));
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb(pht('Reset Password'));
$dialog = new AphrontDialogView();
$dialog->setUser($request->getUser());
- $dialog->setTitle(pht(
- 'Forgot Password / Email Login'));
+ $dialog->setTitle(pht('Forgot Password / Email Login'));
$dialog->appendChild($email_auth);
$dialog->addSubmitButton(pht('Send Email'));
$dialog->setSubmitURI('/login/email/');
return $this->buildApplicationPage(
array(
$crumbs,
$dialog,
),
array(
'title' => pht('Forgot Password'),
));
}
}
diff --git a/src/applications/auth/factor/__tests__/PhabricatorTOTPAuthFactorTestCase.php b/src/applications/auth/factor/__tests__/PhabricatorTOTPAuthFactorTestCase.php
index 464c0b8e5..284db4cde 100644
--- a/src/applications/auth/factor/__tests__/PhabricatorTOTPAuthFactorTestCase.php
+++ b/src/applications/auth/factor/__tests__/PhabricatorTOTPAuthFactorTestCase.php
@@ -1,44 +1,42 @@
<?php
final class PhabricatorTOTPAuthFactorTestCase extends PhabricatorTestCase {
public function testTOTPCodeGeneration() {
$tests = array(
array(
'AAAABBBBCCCCDDDD',
46620383,
'724492',
),
array(
'AAAABBBBCCCCDDDD',
46620390,
'935803',
),
array(
'Z3RFWEFJN233R23P',
46620398,
'273030',
),
// This is testing the case where the code has leading zeroes.
array(
'Z3RFWEFJN233R23W',
46620399,
'072346',
),
);
foreach ($tests as $test) {
list($key, $time, $code) = $test;
$this->assertEqual(
$code,
PhabricatorTOTPAuthFactor::getTOTPCode(
new PhutilOpaqueEnvelope($key),
$time));
}
}
-
-
}
diff --git a/src/applications/auth/management/PhabricatorAuthManagementCachePKCS8Workflow.php b/src/applications/auth/management/PhabricatorAuthManagementCachePKCS8Workflow.php
index a10b3ea64..22f993cee 100644
--- a/src/applications/auth/management/PhabricatorAuthManagementCachePKCS8Workflow.php
+++ b/src/applications/auth/management/PhabricatorAuthManagementCachePKCS8Workflow.php
@@ -1,96 +1,98 @@
<?php
final class PhabricatorAuthManagementCachePKCS8Workflow
extends PhabricatorAuthManagementWorkflow {
protected function didConstruct() {
$this
->setName('cache-pkcs8')
->setExamples('**cache-pkcs8** --public __keyfile__ --pkcs8 __keyfile__')
->setSynopsis(
pht(
'Cache the PKCS8 format of a public key. When developing on OSX, '.
'this can be used to work around issues with ssh-keygen. Use '.
- '`ssh-keygen -e -m PKCS8 -f key.pub` to generate a PKCS8 key to '.
- 'feed to this command.'))
+ '`%s` to generate a PKCS8 key to feed to this command.',
+ 'ssh-keygen -e -m PKCS8 -f key.pub'))
->setArguments(
array(
array(
'name' => 'public',
'param' => 'keyfile',
'help' => pht('Path to public keyfile.'),
),
array(
'name' => 'pkcs8',
'param' => 'keyfile',
'help' => pht('Path to corresponding PKCS8 key.'),
),
));
}
public function execute(PhutilArgumentParser $args) {
$console = PhutilConsole::getConsole();
$public_keyfile = $args->getArg('public');
if (!strlen($public_keyfile)) {
throw new PhutilArgumentUsageException(
pht(
- 'You must specify the path to a public keyfile with --public.'));
+ 'You must specify the path to a public keyfile with %s.',
+ '--public'));
}
if (!Filesystem::pathExists($public_keyfile)) {
throw new PhutilArgumentUsageException(
pht(
'Specified public keyfile "%s" does not exist!',
$public_keyfile));
}
$public_key = Filesystem::readFile($public_keyfile);
$pkcs8_keyfile = $args->getArg('pkcs8');
if (!strlen($pkcs8_keyfile)) {
throw new PhutilArgumentUsageException(
pht(
- 'You must specify the path to a pkcs8 keyfile with --pkc8s.'));
+ 'You must specify the path to a pkcs8 keyfile with %s.',
+ '--pkc8s'));
}
if (!Filesystem::pathExists($pkcs8_keyfile)) {
throw new PhutilArgumentUsageException(
pht(
'Specified pkcs8 keyfile "%s" does not exist!',
$pkcs8_keyfile));
}
$pkcs8_key = Filesystem::readFile($pkcs8_keyfile);
$warning = pht(
'Adding a PKCS8 keyfile to the cache can be very dangerous. If the '.
'PKCS8 file really encodes a different public key than the one '.
'specified, an attacker could use it to gain unauthorized access.'.
"\n\n".
'Generally, you should use this option only in a development '.
'environment where ssh-keygen is broken and it is inconvenient to '.
'fix it, and only if you are certain you understand the risks. You '.
'should never cache a PKCS8 file you did not generate yourself.');
$console->writeOut(
"%s\n",
phutil_console_wrap($warning));
$prompt = pht('Really trust this PKCS8 keyfile?');
if (!phutil_console_confirm($prompt)) {
throw new PhutilArgumentUsageException(
pht('Aborted workflow.'));
}
$key = PhabricatorAuthSSHPublicKey::newFromRawKey($public_key);
$key->forcePopulatePKCS8Cache($pkcs8_key);
$console->writeOut(
"%s\n",
pht('Cached PKCS8 key for public key.'));
return 0;
}
}
diff --git a/src/applications/auth/management/PhabricatorAuthManagementLDAPWorkflow.php b/src/applications/auth/management/PhabricatorAuthManagementLDAPWorkflow.php
index e7afcc48d..725d2dfc4 100644
--- a/src/applications/auth/management/PhabricatorAuthManagementLDAPWorkflow.php
+++ b/src/applications/auth/management/PhabricatorAuthManagementLDAPWorkflow.php
@@ -1,69 +1,69 @@
<?php
final class PhabricatorAuthManagementLDAPWorkflow
extends PhabricatorAuthManagementWorkflow {
protected function didConstruct() {
$this
->setName('ldap')
->setExamples('**ldap**')
->setSynopsis(
pht('Analyze and diagnose issues with LDAP configuration.'));
}
public function execute(PhutilArgumentParser $args) {
$console = PhutilConsole::getConsole();
$console->getServer()->setEnableLog(true);
PhabricatorLDAPAuthProvider::assertLDAPExtensionInstalled();
$provider = PhabricatorLDAPAuthProvider::getLDAPProvider();
if (!$provider) {
$console->writeOut(
"%s\n",
- 'The LDAP authentication provider is not enabled.');
+ pht('The LDAP authentication provider is not enabled.'));
exit(1);
}
if (!function_exists('ldap_connect')) {
$console->writeOut(
"%s\n",
- 'The LDAP extension is not enabled.');
+ pht('The LDAP extension is not enabled.'));
exit(1);
}
$adapter = $provider->getAdapter();
$console->writeOut("%s\n", pht('Enter LDAP Credentials'));
- $username = phutil_console_prompt('LDAP Username: ');
+ $username = phutil_console_prompt(pht('LDAP Username: '));
if (!strlen($username)) {
throw new PhutilArgumentUsageException(
pht('You must enter an LDAP username.'));
}
phutil_passthru('stty -echo');
- $password = phutil_console_prompt('LDAP Password: ');
+ $password = phutil_console_prompt(pht('LDAP Password: '));
phutil_passthru('stty echo');
if (!strlen($password)) {
throw new PhutilArgumentUsageException(
pht('You must enter an LDAP password.'));
}
$adapter->setLoginUsername($username);
$adapter->setLoginPassword(new PhutilOpaqueEnvelope($password));
$console->writeOut("\n");
$console->writeOut("%s\n", pht('Connecting to LDAP...'));
$account_id = $adapter->getAccountID();
if ($account_id) {
$console->writeOut("%s\n", pht('Found LDAP Account: %s', $account_id));
} else {
$console->writeOut("%s\n", pht('Unable to find LDAP account!'));
}
return 0;
}
}
diff --git a/src/applications/auth/management/PhabricatorAuthManagementRecoverWorkflow.php b/src/applications/auth/management/PhabricatorAuthManagementRecoverWorkflow.php
index fe918adee..ad843d56c 100644
--- a/src/applications/auth/management/PhabricatorAuthManagementRecoverWorkflow.php
+++ b/src/applications/auth/management/PhabricatorAuthManagementRecoverWorkflow.php
@@ -1,96 +1,99 @@
<?php
final class PhabricatorAuthManagementRecoverWorkflow
extends PhabricatorAuthManagementWorkflow {
protected function didConstruct() {
$this
->setName('recover')
->setExamples('**recover** __username__')
->setSynopsis(
- 'Recover access to an administrative account if you have locked '.
- 'yourself out of Phabricator.')
+ pht(
+ 'Recover access to an administrative account if you have locked '.
+ 'yourself out of Phabricator.'))
->setArguments(
array(
'username' => array(
'name' => 'username',
'wildcard' => true,
),
));
}
public function execute(PhutilArgumentParser $args) {
$can_recover = id(new PhabricatorPeopleQuery())
->setViewer($this->getViewer())
->withIsAdmin(true)
->execute();
if (!$can_recover) {
throw new PhutilArgumentUsageException(
pht(
'This Phabricator installation has no recoverable administrator '.
- 'accounts. You can use `bin/accountadmin` to create a new '.
- 'administrator account or make an existing user an administrator.'));
+ 'accounts. You can use `%s` to create a new administrator '.
+ 'account or make an existing user an administrator.',
+ 'bin/accountadmin'));
}
$can_recover = mpull($can_recover, 'getUsername');
sort($can_recover);
$can_recover = implode(', ', $can_recover);
$usernames = $args->getArg('username');
if (!$usernames) {
throw new PhutilArgumentUsageException(
pht('You must specify the username of the account to recover.'));
} else if (count($usernames) > 1) {
throw new PhutilArgumentUsageException(
pht('You can only recover the username for one account.'));
}
$username = head($usernames);
$user = id(new PhabricatorPeopleQuery())
->setViewer($this->getViewer())
->withUsernames(array($username))
->executeOne();
if (!$user) {
throw new PhutilArgumentUsageException(
pht(
'No such user "%s". Recoverable administrator accounts are: %s.',
$username,
$can_recover));
}
if (!$user->getIsAdmin()) {
throw new PhutilArgumentUsageException(
pht(
'You can only recover administrator accounts, but %s is not an '.
'administrator. Recoverable administrator accounts are: %s.',
$username,
$can_recover));
}
$engine = new PhabricatorAuthSessionEngine();
$onetime_uri = $engine->getOneTimeLoginURI(
$user,
null,
PhabricatorAuthSessionEngine::ONETIME_RECOVER);
$console = PhutilConsole::getConsole();
$console->writeOut(
pht(
'Use this link to recover access to the "%s" account from the web '.
'interface:',
$username));
$console->writeOut("\n\n");
$console->writeOut(' %s', $onetime_uri);
$console->writeOut("\n\n");
$console->writeOut(
+ "%s\n",
pht(
'After logging in, you can use the "Auth" application to add or '.
'restore authentication providers and allow normal logins to '.
- 'succeed.')."\n");
+ 'succeed.'));
return 0;
}
}
diff --git a/src/applications/auth/management/PhabricatorAuthManagementRefreshWorkflow.php b/src/applications/auth/management/PhabricatorAuthManagementRefreshWorkflow.php
index b05b87f5e..23c0a109c 100644
--- a/src/applications/auth/management/PhabricatorAuthManagementRefreshWorkflow.php
+++ b/src/applications/auth/management/PhabricatorAuthManagementRefreshWorkflow.php
@@ -1,156 +1,156 @@
<?php
final class PhabricatorAuthManagementRefreshWorkflow
extends PhabricatorAuthManagementWorkflow {
protected function didConstruct() {
$this
->setName('refresh')
->setExamples('**refresh**')
->setSynopsis(
pht(
'Refresh OAuth access tokens. This is primarily useful for '.
'development and debugging.'))
->setArguments(
array(
array(
'name' => 'user',
'param' => 'user',
- 'help' => 'Refresh tokens for a given user.',
+ 'help' => pht('Refresh tokens for a given user.'),
),
array(
'name' => 'type',
'param' => 'provider',
- 'help' => 'Refresh tokens for a given provider type.',
+ 'help' => pht('Refresh tokens for a given provider type.'),
),
array(
'name' => 'domain',
'param' => 'domain',
- 'help' => 'Refresh tokens for a given domain.',
+ 'help' => pht('Refresh tokens for a given domain.'),
),
));
}
public function execute(PhutilArgumentParser $args) {
$console = PhutilConsole::getConsole();
$viewer = $this->getViewer();
$query = id(new PhabricatorExternalAccountQuery())
->setViewer($viewer)
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
));
$username = $args->getArg('user');
if (strlen($username)) {
$user = id(new PhabricatorPeopleQuery())
->setViewer($viewer)
->withUsernames(array($username))
->executeOne();
if ($user) {
$query->withUserPHIDs(array($user->getPHID()));
} else {
throw new PhutilArgumentUsageException(
pht('No such user "%s"!', $username));
}
}
$type = $args->getArg('type');
if (strlen($type)) {
$query->withAccountTypes(array($type));
}
$domain = $args->getArg('domain');
if (strlen($domain)) {
$query->withAccountDomains(array($domain));
}
$accounts = $query->execute();
if (!$accounts) {
throw new PhutilArgumentUsageException(
pht('No accounts match the arguments!'));
} else {
$console->writeOut(
"%s\n",
pht(
'Found %s account(s) to refresh.',
new PhutilNumber(count($accounts))));
}
$providers = PhabricatorAuthProvider::getAllEnabledProviders();
foreach ($accounts as $account) {
$console->writeOut(
"%s\n",
pht(
'Refreshing account #%d (%s/%s).',
$account->getID(),
$account->getAccountType(),
$account->getAccountDomain()));
$key = $account->getProviderKey();
if (empty($providers[$key])) {
$console->writeOut(
"> %s\n",
pht('Skipping, provider is not enabled or does not exist.'));
continue;
}
$provider = $providers[$key];
if (!($provider instanceof PhabricatorOAuth2AuthProvider)) {
$console->writeOut(
"> %s\n",
pht('Skipping, provider is not an OAuth2 provider.'));
continue;
}
$adapter = $provider->getAdapter();
if (!$adapter->supportsTokenRefresh()) {
$console->writeOut(
"> %s\n",
pht('Skipping, provider does not support token refresh.'));
continue;
}
$refresh_token = $account->getProperty('oauth.token.refresh');
if (!$refresh_token) {
$console->writeOut(
"> %s\n",
pht('Skipping, provider has no stored refresh token.'));
continue;
}
$console->writeOut(
"+ %s\n",
pht(
'Refreshing token, current token expires in %s seconds.',
new PhutilNumber(
$account->getProperty('oauth.token.access.expires') - time())));
$token = $provider->getOAuthAccessToken($account, $force_refresh = true);
if (!$token) {
$console->writeOut(
"* %s\n",
pht('Unable to refresh token!'));
continue;
}
$console->writeOut(
"+ %s\n",
pht(
'Refreshed token, new token expires in %s seconds.',
new PhutilNumber(
$account->getProperty('oauth.token.access.expires') - time())));
}
$console->writeOut("%s\n", pht('Done.'));
return 0;
}
}
diff --git a/src/applications/auth/management/PhabricatorAuthManagementStripWorkflow.php b/src/applications/auth/management/PhabricatorAuthManagementStripWorkflow.php
index f6d057b57..f25d05301 100644
--- a/src/applications/auth/management/PhabricatorAuthManagementStripWorkflow.php
+++ b/src/applications/auth/management/PhabricatorAuthManagementStripWorkflow.php
@@ -1,173 +1,175 @@
<?php
final class PhabricatorAuthManagementStripWorkflow
extends PhabricatorAuthManagementWorkflow {
protected function didConstruct() {
$this
->setName('strip')
->setExamples('**strip** [--user username] [--type type]')
- ->setSynopsis(
- pht(
- 'Remove multi-factor authentication from an account.'))
+ ->setSynopsis(pht('Remove multi-factor authentication from an account.'))
->setArguments(
array(
array(
'name' => 'user',
'param' => 'username',
'repeat' => true,
'help' => pht('Strip factors from specified users.'),
),
array(
'name' => 'all-users',
'help' => pht('Strip factors from all users.'),
),
array(
'name' => 'type',
'param' => 'factortype',
'repeat' => true,
'help' => pht('Strip a specific factor type.'),
),
array(
'name' => 'all-types',
'help' => pht('Strip all factors, regardless of type.'),
),
array(
'name' => 'force',
'help' => pht('Strip factors without prompting.'),
),
array(
'name' => 'dry-run',
'help' => pht('Show factors, but do not strip them.'),
),
));
}
public function execute(PhutilArgumentParser $args) {
$usernames = $args->getArg('user');
$all_users = $args->getArg('all-users');
if ($usernames && $all_users) {
throw new PhutilArgumentUsageException(
pht(
- 'Specify either specific users with --user, or all users with '.
- '--all-users, but not both.'));
+ 'Specify either specific users with %s, or all users with '.
+ '%s, but not both.',
+ '--user',
+ '--all-users'));
} else if (!$usernames && !$all_users) {
throw new PhutilArgumentUsageException(
pht(
- 'Use --user to specify which user to strip factors from, or '.
- '--all-users to strip factors from all users.'));
+ 'Use %s to specify which user to strip factors from, or '.
+ '%s to strip factors from all users.',
+ '--user',
+ '--all-users'));
} else if ($usernames) {
$users = id(new PhabricatorPeopleQuery())
->setViewer($this->getViewer())
->withUsernames($usernames)
->execute();
$users_by_username = mpull($users, null, 'getUsername');
foreach ($usernames as $username) {
if (empty($users_by_username[$username])) {
throw new PhutilArgumentUsageException(
pht(
'No user exists with username "%s".',
$username));
}
}
} else {
$users = null;
}
$types = $args->getArg('type');
$all_types = $args->getArg('all-types');
if ($types && $all_types) {
throw new PhutilArgumentUsageException(
pht(
'Specify either specific factors with --type, or all factors with '.
'--all-types, but not both.'));
} else if (!$types && !$all_types) {
throw new PhutilArgumentUsageException(
pht(
'Use --type to specify which factor to strip, or --all-types to '.
'strip all factors. Use `auth list-factors` to show the available '.
'factor types.'));
}
if ($users && $types) {
$factors = id(new PhabricatorAuthFactorConfig())->loadAllWhere(
'userPHID IN (%Ls) AND factorKey IN (%Ls)',
mpull($users, 'getPHID'),
$types);
} else if ($users) {
$factors = id(new PhabricatorAuthFactorConfig())->loadAllWhere(
'userPHID IN (%Ls)',
mpull($users, 'getPHID'));
} else if ($types) {
$factors = id(new PhabricatorAuthFactorConfig())->loadAllWhere(
'factorKey IN (%Ls)',
$types);
} else {
$factors = id(new PhabricatorAuthFactorConfig())->loadAll();
}
if (!$factors) {
throw new PhutilArgumentUsageException(
pht('There are no matching factors to strip.'));
}
$handles = id(new PhabricatorHandleQuery())
->setViewer($this->getViewer())
->withPHIDs(mpull($factors, 'getUserPHID'))
->execute();
$console = PhutilConsole::getConsole();
$console->writeOut("%s\n\n", pht('These auth factors will be stripped:'));
foreach ($factors as $factor) {
$impl = $factor->getImplementation();
$console->writeOut(
" %s\t%s\t%s\n",
$handles[$factor->getUserPHID()]->getName(),
$factor->getFactorKey(),
($impl
? $impl->getFactorName()
: '?'));
}
$is_dry_run = $args->getArg('dry-run');
if ($is_dry_run) {
$console->writeOut(
"\n%s\n",
pht('End of dry run.'));
return 0;
}
$force = $args->getArg('force');
if (!$force) {
if (!$console->confirm(pht('Strip these authentication factors?'))) {
throw new PhutilArgumentUsageException(
pht('User aborted the workflow.'));
}
}
$console->writeOut("%s\n", pht('Stripping authentication factors...'));
foreach ($factors as $factor) {
$user = id(new PhabricatorPeopleQuery())
->setViewer($this->getViewer())
->withPHIDs(array($factor->getUserPHID()))
->executeOne();
$factor->delete();
if ($user) {
$user->updateMultiFactorEnrollment();
}
}
$console->writeOut("%s\n", pht('Done.'));
return 0;
}
}
diff --git a/src/applications/auth/management/PhabricatorAuthManagementTrustOAuthClientWorkflow.php b/src/applications/auth/management/PhabricatorAuthManagementTrustOAuthClientWorkflow.php
index 229218d3b..ee5e50b38 100644
--- a/src/applications/auth/management/PhabricatorAuthManagementTrustOAuthClientWorkflow.php
+++ b/src/applications/auth/management/PhabricatorAuthManagementTrustOAuthClientWorkflow.php
@@ -1,63 +1,64 @@
<?php
final class PhabricatorAuthManagementTrustOAuthClientWorkflow
extends PhabricatorAuthManagementWorkflow {
protected function didConstruct() {
$this
->setName('trust-oauth-client')
->setExamples('**trust-oauth-client** [--id client_id]')
->setSynopsis(
pht(
'Set Phabricator to trust an OAuth client. Phabricator '.
'redirects to trusted OAuth clients that users have authorized '.
'without user intervention.'))
->setArguments(
array(
array(
'name' => 'id',
'param' => 'id',
'help' => pht('The id of the OAuth client.'),
),
));
}
public function execute(PhutilArgumentParser $args) {
$id = $args->getArg('id');
if (!$id) {
throw new PhutilArgumentUsageException(
pht(
- 'Specify an OAuth client id with --id.'));
+ 'Specify an OAuth client id with %s.',
+ '--id'));
}
$client = id(new PhabricatorOAuthServerClientQuery())
->setViewer($this->getViewer())
->withIDs(array($id))
->executeOne();
if (!$client) {
throw new PhutilArgumentUsageException(
pht(
'Failed to find an OAuth client with id %s.', $id));
}
if ($client->getIsTrusted()) {
throw new PhutilArgumentUsageException(
pht(
'Phabricator already trusts OAuth client "%s".',
$client->getName()));
}
$client->setIsTrusted(1);
$client->save();
$console = PhutilConsole::getConsole();
$console->writeOut(
"%s\n",
pht(
'Updated; Phabricator trusts OAuth client %s.',
$client->getName()));
}
}
diff --git a/src/applications/auth/management/PhabricatorAuthManagementUntrustOAuthClientWorkflow.php b/src/applications/auth/management/PhabricatorAuthManagementUntrustOAuthClientWorkflow.php
index 344e4b886..a0b3e02fa 100644
--- a/src/applications/auth/management/PhabricatorAuthManagementUntrustOAuthClientWorkflow.php
+++ b/src/applications/auth/management/PhabricatorAuthManagementUntrustOAuthClientWorkflow.php
@@ -1,63 +1,64 @@
<?php
final class PhabricatorAuthManagementUntrustOAuthClientWorkflow
extends PhabricatorAuthManagementWorkflow {
protected function didConstruct() {
$this
->setName('untrust-oauth-client')
->setExamples('**untrust-oauth-client** [--id client_id]')
->setSynopsis(
pht(
'Set Phabricator to not trust an OAuth client. Phabricator '.
'redirects to trusted OAuth clients that users have authorized '.
'without user intervention.'))
->setArguments(
array(
array(
'name' => 'id',
'param' => 'id',
'help' => pht('The id of the OAuth client.'),
),
));
}
public function execute(PhutilArgumentParser $args) {
$id = $args->getArg('id');
if (!$id) {
throw new PhutilArgumentUsageException(
pht(
- 'Specify an OAuth client id with --id.'));
+ 'Specify an OAuth client ID with %s.',
+ '--id'));
}
$client = id(new PhabricatorOAuthServerClientQuery())
->setViewer($this->getViewer())
->withIDs(array($id))
->executeOne();
if (!$client) {
throw new PhutilArgumentUsageException(
pht(
- 'Failed to find an OAuth client with id %s.', $id));
+ 'Failed to find an OAuth client with ID %s.', $id));
}
if (!$client->getIsTrusted()) {
throw new PhutilArgumentUsageException(
pht(
'Phabricator already does not trust OAuth client "%s".',
$client->getName()));
}
$client->setIsTrusted(0);
$client->save();
$console = PhutilConsole::getConsole();
$console->writeOut(
"%s\n",
pht(
'Updated; Phabricator does not trust OAuth client %s.',
$client->getName()));
}
}
diff --git a/src/applications/auth/provider/PhabricatorAuthProvider.php b/src/applications/auth/provider/PhabricatorAuthProvider.php
index 5ffb18b41..d495a3091 100644
--- a/src/applications/auth/provider/PhabricatorAuthProvider.php
+++ b/src/applications/auth/provider/PhabricatorAuthProvider.php
@@ -1,497 +1,501 @@
<?php
abstract class PhabricatorAuthProvider {
private $providerConfig;
public function attachProviderConfig(PhabricatorAuthProviderConfig $config) {
$this->providerConfig = $config;
return $this;
}
public function hasProviderConfig() {
return (bool)$this->providerConfig;
}
public function getProviderConfig() {
if ($this->providerConfig === null) {
throw new PhutilInvalidStateException('attachProviderConfig');
}
return $this->providerConfig;
}
public function getConfigurationHelp() {
return null;
}
public function getDefaultProviderConfig() {
return id(new PhabricatorAuthProviderConfig())
->setProviderClass(get_class($this))
->setIsEnabled(1)
->setShouldAllowLogin(1)
->setShouldAllowRegistration(1)
->setShouldAllowLink(1)
->setShouldAllowUnlink(1);
}
public function getNameForCreate() {
return $this->getProviderName();
}
public function getDescriptionForCreate() {
return null;
}
public function getProviderKey() {
return $this->getAdapter()->getAdapterKey();
}
public function getProviderType() {
return $this->getAdapter()->getAdapterType();
}
public function getProviderDomain() {
return $this->getAdapter()->getAdapterDomain();
}
public static function getAllBaseProviders() {
static $providers;
if ($providers === null) {
$objects = id(new PhutilSymbolLoader())
->setAncestorClass(__CLASS__)
->loadObjects();
$providers = $objects;
}
return $providers;
}
public static function getAllProviders() {
static $providers;
if ($providers === null) {
$objects = self::getAllBaseProviders();
$configs = id(new PhabricatorAuthProviderConfigQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->execute();
$providers = array();
foreach ($configs as $config) {
if (!isset($objects[$config->getProviderClass()])) {
// This configuration is for a provider which is not installed.
continue;
}
$object = clone $objects[$config->getProviderClass()];
$object->attachProviderConfig($config);
$key = $object->getProviderKey();
if (isset($providers[$key])) {
throw new Exception(
pht(
"Two authentication providers use the same provider key ".
"('%s'). Each provider must be identified by a unique key.",
$key));
}
$providers[$key] = $object;
}
}
return $providers;
}
public static function getAllEnabledProviders() {
$providers = self::getAllProviders();
foreach ($providers as $key => $provider) {
if (!$provider->isEnabled()) {
unset($providers[$key]);
}
}
return $providers;
}
public static function getEnabledProviderByKey($provider_key) {
return idx(self::getAllEnabledProviders(), $provider_key);
}
abstract public function getProviderName();
abstract public function getAdapter();
public function isEnabled() {
return $this->getProviderConfig()->getIsEnabled();
}
public function shouldAllowLogin() {
return $this->getProviderConfig()->getShouldAllowLogin();
}
public function shouldAllowRegistration() {
return $this->getProviderConfig()->getShouldAllowRegistration();
}
public function shouldAllowAccountLink() {
return $this->getProviderConfig()->getShouldAllowLink();
}
public function shouldAllowAccountUnlink() {
return $this->getProviderConfig()->getShouldAllowUnlink();
}
public function shouldTrustEmails() {
return $this->shouldAllowEmailTrustConfiguration() &&
$this->getProviderConfig()->getShouldTrustEmails();
}
/**
* Should we allow the adapter to be marked as "trusted". This is true for
* all adapters except those that allow the user to type in emails (see
* @{class:PhabricatorPasswordAuthProvider}).
*/
public function shouldAllowEmailTrustConfiguration() {
return true;
}
public function buildLoginForm(PhabricatorAuthStartController $controller) {
return $this->renderLoginForm($controller->getRequest(), $mode = 'start');
}
public function buildInviteForm(PhabricatorAuthStartController $controller) {
return $this->renderLoginForm($controller->getRequest(), $mode = 'invite');
}
abstract public function processLoginRequest(
PhabricatorAuthLoginController $controller);
public function buildLinkForm(PhabricatorAuthLinkController $controller) {
return $this->renderLoginForm($controller->getRequest(), $mode = 'link');
}
public function shouldAllowAccountRefresh() {
return true;
}
public function buildRefreshForm(
PhabricatorAuthLinkController $controller) {
return $this->renderLoginForm($controller->getRequest(), $mode = 'refresh');
}
protected function renderLoginForm(AphrontRequest $request, $mode) {
throw new PhutilMethodNotImplementedException();
}
public function createProviders() {
return array($this);
}
protected function willSaveAccount(PhabricatorExternalAccount $account) {
return;
}
public function willRegisterAccount(PhabricatorExternalAccount $account) {
return;
}
protected function loadOrCreateAccount($account_id) {
if (!strlen($account_id)) {
- throw new Exception('loadOrCreateAccount(...): empty account ID!');
+ throw new Exception(pht('Empty account ID!'));
}
$adapter = $this->getAdapter();
$adapter_class = get_class($adapter);
if (!strlen($adapter->getAdapterType())) {
throw new Exception(
- "AuthAdapter (of class '{$adapter_class}') has an invalid ".
- "implementation: no adapter type.");
+ pht(
+ "AuthAdapter (of class '%s') has an invalid implementation: ".
+ "no adapter type.",
+ $adapter_class));
}
if (!strlen($adapter->getAdapterDomain())) {
throw new Exception(
- "AuthAdapter (of class '{$adapter_class}') has an invalid ".
- "implementation: no adapter domain.");
+ pht(
+ "AuthAdapter (of class '%s') has an invalid implementation: ".
+ "no adapter domain.",
+ $adapter_class));
}
$account = id(new PhabricatorExternalAccount())->loadOneWhere(
'accountType = %s AND accountDomain = %s AND accountID = %s',
$adapter->getAdapterType(),
$adapter->getAdapterDomain(),
$account_id);
if (!$account) {
$account = id(new PhabricatorExternalAccount())
->setAccountType($adapter->getAdapterType())
->setAccountDomain($adapter->getAdapterDomain())
->setAccountID($account_id);
}
$account->setUsername($adapter->getAccountName());
$account->setRealName($adapter->getAccountRealName());
$account->setEmail($adapter->getAccountEmail());
$account->setAccountURI($adapter->getAccountURI());
$account->setProfileImagePHID(null);
$image_uri = $adapter->getAccountImageURI();
if ($image_uri) {
try {
$name = PhabricatorSlug::normalize($this->getProviderName());
$name = $name.'-profile.jpg';
// TODO: If the image has not changed, we do not need to make a new
// file entry for it, but there's no convenient way to do this with
// PhabricatorFile right now. The storage will get shared, so the impact
// here is negligible.
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$image_file = PhabricatorFile::newFromFileDownload(
$image_uri,
array(
'name' => $name,
'viewPolicy' => PhabricatorPolicies::POLICY_NOONE,
));
if ($image_file->isViewableImage()) {
$image_file
->setViewPolicy(PhabricatorPolicies::getMostOpenPolicy())
->setCanCDN(true)
->save();
$account->setProfileImagePHID($image_file->getPHID());
} else {
$image_file->delete();
}
unset($unguarded);
} catch (Exception $ex) {
// Log this but proceed, it's not especially important that we
// be able to pull profile images.
phlog($ex);
}
}
$this->willSaveAccount($account);
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$account->save();
unset($unguarded);
return $account;
}
public function getLoginURI() {
$app = PhabricatorApplication::getByClass('PhabricatorAuthApplication');
return $app->getApplicationURI('/login/'.$this->getProviderKey().'/');
}
public function getSettingsURI() {
return '/settings/panel/external/';
}
public function getStartURI() {
$app = PhabricatorApplication::getByClass('PhabricatorAuthApplication');
$uri = $app->getApplicationURI('/start/');
return $uri;
}
public function isDefaultRegistrationProvider() {
return false;
}
public function shouldRequireRegistrationPassword() {
return false;
}
public function getDefaultExternalAccount() {
throw new PhutilMethodNotImplementedException();
}
public function getLoginOrder() {
return '500-'.$this->getProviderName();
}
protected function getLoginIcon() {
return 'Generic';
}
public function isLoginFormAButton() {
return false;
}
public function renderConfigPropertyTransactionTitle(
PhabricatorAuthProviderConfigTransaction $xaction) {
return null;
}
public function readFormValuesFromProvider() {
return array();
}
public function readFormValuesFromRequest(AphrontRequest $request) {
return array();
}
public function processEditForm(
AphrontRequest $request,
array $values) {
$errors = array();
$issues = array();
return array($errors, $issues, $values);
}
public function extendEditForm(
AphrontRequest $request,
AphrontFormView $form,
array $values,
array $issues) {
return;
}
public function willRenderLinkedAccount(
PhabricatorUser $viewer,
PHUIObjectItemView $item,
PhabricatorExternalAccount $account) {
$account_view = id(new PhabricatorAuthAccountView())
->setExternalAccount($account)
->setAuthProvider($this);
$item->appendChild(
phutil_tag(
'div',
array(
'class' => 'mmr mml mst mmb',
),
$account_view));
}
/**
* Return true to use a two-step configuration (setup, configure) instead of
* the default single-step configuration. In practice, this means that
* creating a new provider instance will redirect back to the edit page
* instead of the provider list.
*
* @return bool True if this provider uses two-step configuration.
*/
public function hasSetupStep() {
return false;
}
/**
* Render a standard login/register button element.
*
* The `$attributes` parameter takes these keys:
*
* - `uri`: URI the button should take the user to when clicked.
* - `method`: Optional HTTP method the button should use, defaults to GET.
*
* @param AphrontRequest HTTP request.
* @param string Request mode string.
* @param map Additional parameters, see above.
* @return wild Login button.
*/
protected function renderStandardLoginButton(
AphrontRequest $request,
$mode,
array $attributes = array()) {
PhutilTypeSpec::checkMap(
$attributes,
array(
'method' => 'optional string',
'uri' => 'string',
'sigil' => 'optional string',
));
$viewer = $request->getUser();
$adapter = $this->getAdapter();
if ($mode == 'link') {
$button_text = pht('Link External Account');
} else if ($mode == 'refresh') {
$button_text = pht('Refresh Account Link');
} else if ($mode == 'invite') {
$button_text = pht('Register Account');
} else if ($this->shouldAllowRegistration()) {
$button_text = pht('Login or Register');
} else {
$button_text = pht('Login');
}
$icon = id(new PHUIIconView())
->setSpriteSheet(PHUIIconView::SPRITE_LOGIN)
->setSpriteIcon($this->getLoginIcon());
$button = id(new PHUIButtonView())
->setSize(PHUIButtonView::BIG)
->setColor(PHUIButtonView::GREY)
->setIcon($icon)
->setText($button_text)
->setSubtext($this->getProviderName());
$uri = $attributes['uri'];
$uri = new PhutilURI($uri);
$params = $uri->getQueryParams();
$uri->setQueryParams(array());
$content = array($button);
foreach ($params as $key => $value) {
$content[] = phutil_tag(
'input',
array(
'type' => 'hidden',
'name' => $key,
'value' => $value,
));
}
return phabricator_form(
$viewer,
array(
'method' => idx($attributes, 'method', 'GET'),
'action' => (string)$uri,
'sigil' => idx($attributes, 'sigil'),
),
$content);
}
public function renderConfigurationFooter() {
return null;
}
public function getAuthCSRFCode(AphrontRequest $request) {
$phcid = $request->getCookie(PhabricatorCookies::COOKIE_CLIENTID);
if (!strlen($phcid)) {
throw new Exception(
pht(
'Your browser did not submit a "%s" cookie with client state '.
'information in the request. Check that cookies are enabled. '.
'If this problem persists, you may need to clear your cookies.',
PhabricatorCookies::COOKIE_CLIENTID));
}
return PhabricatorHash::digest($phcid);
}
protected function verifyAuthCSRFCode(AphrontRequest $request, $actual) {
$expect = $this->getAuthCSRFCode($request);
if (!strlen($actual)) {
throw new Exception(
pht(
'The authentication provider did not return a client state '.
'parameter in its response, but one was expected. If this '.
'problem persists, you may need to clear your cookies.'));
}
if ($actual !== $expect) {
throw new Exception(
pht(
'The authentication provider did not return the correct client '.
'state parameter in its response. If this problem persists, you may '.
'need to clear your cookies.'));
}
}
}
diff --git a/src/applications/auth/provider/PhabricatorLDAPAuthProvider.php b/src/applications/auth/provider/PhabricatorLDAPAuthProvider.php
index 111d62601..013cd2173 100644
--- a/src/applications/auth/provider/PhabricatorLDAPAuthProvider.php
+++ b/src/applications/auth/provider/PhabricatorLDAPAuthProvider.php
@@ -1,491 +1,492 @@
<?php
final class PhabricatorLDAPAuthProvider extends PhabricatorAuthProvider {
private $adapter;
public function getProviderName() {
return pht('LDAP');
}
public function getDescriptionForCreate() {
return pht(
'Configure a connection to an LDAP server so that users can use their '.
'LDAP credentials to log in to Phabricator.');
}
public function getDefaultProviderConfig() {
return parent::getDefaultProviderConfig()
->setProperty(self::KEY_PORT, 389)
->setProperty(self::KEY_VERSION, 3);
}
public function getAdapter() {
if (!$this->adapter) {
$conf = $this->getProviderConfig();
$realname_attributes = $conf->getProperty(self::KEY_REALNAME_ATTRIBUTES);
if (!is_array($realname_attributes)) {
$realname_attributes = array();
}
$search_attributes = $conf->getProperty(self::KEY_SEARCH_ATTRIBUTES);
$search_attributes = phutil_split_lines($search_attributes, false);
$search_attributes = array_filter($search_attributes);
$adapter = id(new PhutilLDAPAuthAdapter())
->setHostname(
$conf->getProperty(self::KEY_HOSTNAME))
->setPort(
$conf->getProperty(self::KEY_PORT))
->setBaseDistinguishedName(
$conf->getProperty(self::KEY_DISTINGUISHED_NAME))
->setSearchAttributes($search_attributes)
->setUsernameAttribute(
$conf->getProperty(self::KEY_USERNAME_ATTRIBUTE))
->setRealNameAttributes($realname_attributes)
->setLDAPVersion(
$conf->getProperty(self::KEY_VERSION))
->setLDAPReferrals(
$conf->getProperty(self::KEY_REFERRALS))
->setLDAPStartTLS(
$conf->getProperty(self::KEY_START_TLS))
->setAlwaysSearch($conf->getProperty(self::KEY_ALWAYS_SEARCH))
->setAnonymousUsername(
$conf->getProperty(self::KEY_ANONYMOUS_USERNAME))
->setAnonymousPassword(
new PhutilOpaqueEnvelope(
$conf->getProperty(self::KEY_ANONYMOUS_PASSWORD)))
->setActiveDirectoryDomain(
$conf->getProperty(self::KEY_ACTIVEDIRECTORY_DOMAIN));
$this->adapter = $adapter;
}
return $this->adapter;
}
protected function renderLoginForm(AphrontRequest $request, $mode) {
$viewer = $request->getUser();
$dialog = id(new AphrontDialogView())
->setSubmitURI($this->getLoginURI())
->setUser($viewer);
if ($mode == 'link') {
$dialog->setTitle(pht('Link LDAP Account'));
$dialog->addSubmitButton(pht('Link Accounts'));
$dialog->addCancelButton($this->getSettingsURI());
} else if ($mode == 'refresh') {
$dialog->setTitle(pht('Refresh LDAP Account'));
$dialog->addSubmitButton(pht('Refresh Account'));
$dialog->addCancelButton($this->getSettingsURI());
} else {
if ($this->shouldAllowRegistration()) {
$dialog->setTitle(pht('Login or Register with LDAP'));
$dialog->addSubmitButton(pht('Login or Register'));
} else {
$dialog->setTitle(pht('Login with LDAP'));
$dialog->addSubmitButton(pht('Login'));
}
if ($mode == 'login') {
$dialog->addCancelButton($this->getStartURI());
}
}
$v_user = $request->getStr('ldap_username');
$e_user = null;
$e_pass = null;
$errors = array();
if ($request->isHTTPPost()) {
// NOTE: This is intentionally vague so as not to disclose whether a
// given username exists.
$e_user = pht('Invalid');
$e_pass = pht('Invalid');
$errors[] = pht('Username or password are incorrect.');
}
$form = id(new PHUIFormLayoutView())
->setUser($viewer)
->setFullWidth(true)
->appendChild(
id(new AphrontFormTextControl())
- ->setLabel('LDAP Username')
+ ->setLabel(pht('LDAP Username'))
->setName('ldap_username')
->setValue($v_user)
->setError($e_user))
->appendChild(
id(new AphrontFormPasswordControl())
- ->setLabel('LDAP Password')
+ ->setLabel(pht('LDAP Password'))
->setName('ldap_password')
->setError($e_pass));
if ($errors) {
$errors = id(new PHUIInfoView())->setErrors($errors);
}
$dialog->appendChild($errors);
$dialog->appendChild($form);
return $dialog;
}
public function processLoginRequest(
PhabricatorAuthLoginController $controller) {
$request = $controller->getRequest();
$viewer = $request->getUser();
$response = null;
$account = null;
$username = $request->getStr('ldap_username');
$password = $request->getStr('ldap_password');
$has_password = strlen($password);
$password = new PhutilOpaqueEnvelope($password);
if (!strlen($username) || !$has_password) {
$response = $controller->buildProviderPageResponse(
$this,
$this->renderLoginForm($request, 'login'));
return array($account, $response);
}
if ($request->isFormPost()) {
try {
if (strlen($username) && $has_password) {
$adapter = $this->getAdapter();
$adapter->setLoginUsername($username);
$adapter->setLoginPassword($password);
// TODO: This calls ldap_bind() eventually, which dumps cleartext
// passwords to the error log. See note in PhutilLDAPAuthAdapter.
// See T3351.
DarkConsoleErrorLogPluginAPI::enableDiscardMode();
$account_id = $adapter->getAccountID();
DarkConsoleErrorLogPluginAPI::disableDiscardMode();
} else {
- throw new Exception('Username and password are required!');
+ throw new Exception(pht('Username and password are required!'));
}
} catch (PhutilAuthCredentialException $ex) {
$response = $controller->buildProviderPageResponse(
$this,
$this->renderLoginForm($request, 'login'));
return array($account, $response);
} catch (Exception $ex) {
// TODO: Make this cleaner.
throw $ex;
}
}
return array($this->loadOrCreateAccount($account_id), $response);
}
const KEY_HOSTNAME = 'ldap:host';
const KEY_PORT = 'ldap:port';
const KEY_DISTINGUISHED_NAME = 'ldap:dn';
const KEY_SEARCH_ATTRIBUTES = 'ldap:search-attribute';
const KEY_USERNAME_ATTRIBUTE = 'ldap:username-attribute';
const KEY_REALNAME_ATTRIBUTES = 'ldap:realname-attributes';
const KEY_VERSION = 'ldap:version';
const KEY_REFERRALS = 'ldap:referrals';
const KEY_START_TLS = 'ldap:start-tls';
const KEY_ANONYMOUS_USERNAME = 'ldap:anoynmous-username';
const KEY_ANONYMOUS_PASSWORD = 'ldap:anonymous-password';
const KEY_ALWAYS_SEARCH = 'ldap:always-search';
const KEY_ACTIVEDIRECTORY_DOMAIN = 'ldap:activedirectory-domain';
private function getPropertyKeys() {
return array_keys($this->getPropertyLabels());
}
private function getPropertyLabels() {
return array(
self::KEY_HOSTNAME => pht('LDAP Hostname'),
self::KEY_PORT => pht('LDAP Port'),
self::KEY_DISTINGUISHED_NAME => pht('Base Distinguished Name'),
self::KEY_SEARCH_ATTRIBUTES => pht('Search Attributes'),
self::KEY_ALWAYS_SEARCH => pht('Always Search'),
self::KEY_ANONYMOUS_USERNAME => pht('Anonymous Username'),
self::KEY_ANONYMOUS_PASSWORD => pht('Anonymous Password'),
self::KEY_USERNAME_ATTRIBUTE => pht('Username Attribute'),
self::KEY_REALNAME_ATTRIBUTES => pht('Realname Attributes'),
self::KEY_VERSION => pht('LDAP Version'),
self::KEY_REFERRALS => pht('Enable Referrals'),
self::KEY_START_TLS => pht('Use TLS'),
self::KEY_ACTIVEDIRECTORY_DOMAIN => pht('ActiveDirectory Domain'),
);
}
public function readFormValuesFromProvider() {
$properties = array();
foreach ($this->getPropertyLabels() as $key => $ignored) {
$properties[$key] = $this->getProviderConfig()->getProperty($key);
}
return $properties;
}
public function readFormValuesFromRequest(AphrontRequest $request) {
$values = array();
foreach ($this->getPropertyKeys() as $key) {
switch ($key) {
case self::KEY_REALNAME_ATTRIBUTES:
$values[$key] = $request->getStrList($key, array());
break;
default:
$values[$key] = $request->getStr($key);
break;
}
}
return $values;
}
public function processEditForm(
AphrontRequest $request,
array $values) {
$errors = array();
$issues = array();
return array($errors, $issues, $values);
}
public static function assertLDAPExtensionInstalled() {
if (!function_exists('ldap_bind')) {
throw new Exception(
pht(
'Before you can set up or use LDAP, you need to install the PHP '.
'LDAP extension. It is not currently installed, so PHP can not '.
'talk to LDAP. Usually you can install it with '.
- '`yum install php-ldap`, `apt-get install php5-ldap`, or a '.
- 'similar package manager command.'));
+ '`%s`, `%s`, or a similar package manager command.',
+ 'yum install php-ldap',
+ 'apt-get install php5-ldap'));
}
}
public function extendEditForm(
AphrontRequest $request,
AphrontFormView $form,
array $values,
array $issues) {
self::assertLDAPExtensionInstalled();
$labels = $this->getPropertyLabels();
$captions = array(
self::KEY_HOSTNAME =>
pht('Example: %s%sFor LDAPS, use: %s',
phutil_tag('tt', array(), pht('ldap.example.com')),
phutil_tag('br'),
phutil_tag('tt', array(), pht('ldaps://ldaps.example.com/'))),
self::KEY_DISTINGUISHED_NAME =>
pht('Example: %s',
phutil_tag('tt', array(), pht('ou=People, dc=example, dc=com'))),
self::KEY_USERNAME_ATTRIBUTE =>
pht('Example: %s',
phutil_tag('tt', array(), pht('sn'))),
self::KEY_REALNAME_ATTRIBUTES =>
pht('Example: %s',
phutil_tag('tt', array(), pht('firstname, lastname'))),
self::KEY_REFERRALS =>
pht('Follow referrals. Disable this for Windows AD 2003.'),
self::KEY_START_TLS =>
pht('Start TLS after binding to the LDAP server.'),
self::KEY_ALWAYS_SEARCH =>
pht('Always bind and search, even without a username and password.'),
);
$types = array(
self::KEY_REFERRALS => 'checkbox',
self::KEY_START_TLS => 'checkbox',
self::KEY_SEARCH_ATTRIBUTES => 'textarea',
self::KEY_REALNAME_ATTRIBUTES => 'list',
self::KEY_ANONYMOUS_PASSWORD => 'password',
self::KEY_ALWAYS_SEARCH => 'checkbox',
);
$instructions = array(
self::KEY_SEARCH_ATTRIBUTES => pht(
"When a user types their LDAP username and password into Phabricator, ".
"Phabricator can either bind to LDAP with those credentials directly ".
"(which is simpler, but not as powerful) or bind to LDAP with ".
"anonymous credentials, then search for record matching the supplied ".
"credentials (which is more complicated, but more powerful).\n\n".
"For many installs, direct binding is sufficient. However, you may ".
"want to search first if:\n\n".
" - You want users to be able to login with either their username ".
" or their email address.\n".
" - The login/username is not part of the distinguished name in ".
" your LDAP records.\n".
" - You want to restrict logins to a subset of users (like only ".
" those in certain departments).\n".
" - Your LDAP server is configured in some other way that prevents ".
" direct binding from working correctly.\n\n".
"**To bind directly**, enter the LDAP attribute corresponding to the ".
"login name into the **Search Attributes** box below. Often, this is ".
"something like `sn` or `uid`. This is the simplest configuration, ".
"but will only work if the username is part of the distinguished ".
"name, and won't let you apply complex restrictions to logins.\n\n".
" lang=text,name=Simple Direct Binding\n".
" sn\n\n".
"**To search first**, provide an anonymous username and password ".
"below (or check the **Always Search** checkbox), then enter one ".
"or more search queries into this field, one per line. ".
"After binding, these queries will be used to identify the ".
"record associated with the login name the user typed.\n\n".
"Searches will be tried in order until a matching record is found. ".
"Each query can be a simple attribute name (like `sn` or `mail`), ".
"which will search for a matching record, or it can be a complex ".
"query that uses the string `\${login}` to represent the login ".
"name.\n\n".
"A common simple configuration is just an attribute name, like ".
"`sn`, which will work the same way direct binding works:\n\n".
" lang=text,name=Simple Example\n".
" sn\n\n".
"A slightly more complex configuration might let the user login with ".
"either their login name or email address:\n\n".
" lang=text,name=Match Several Attributes\n".
" mail\n".
" sn\n\n".
"If your LDAP directory is more complex, or you want to perform ".
"sophisticated filtering, you can use more complex queries. Depending ".
"on your directory structure, this example might allow users to login ".
"with either their email address or username, but only if they're in ".
"specific departments:\n\n".
" lang=text,name=Complex Example\n".
" (&(mail=\${login})(|(departmentNumber=1)(departmentNumber=2)))\n".
" (&(sn=\${login})(|(departmentNumber=1)(departmentNumber=2)))\n\n".
"All of the attribute names used here are just examples: your LDAP ".
"server may use different attribute names."),
self::KEY_ALWAYS_SEARCH => pht(
'To search for an LDAP record before authenticating, either check '.
'the **Always Search** checkbox or enter an anonymous '.
'username and password to use to perform the search.'),
self::KEY_USERNAME_ATTRIBUTE => pht(
'Optionally, specify a username attribute to use to prefill usernames '.
'when registering a new account. This is purely cosmetic and does not '.
'affect the login process, but you can configure it to make sure '.
'users get the same default username as their LDAP username, so '.
'usernames remain consistent across systems.'),
self::KEY_REALNAME_ATTRIBUTES => pht(
'Optionally, specify one or more comma-separated attributes to use to '.
'prefill the "Real Name" field when registering a new account. This '.
'is purely cosmetic and does not affect the login process, but can '.
'make registration a little easier.'),
);
foreach ($labels as $key => $label) {
$caption = idx($captions, $key);
$type = idx($types, $key);
$value = idx($values, $key);
$control = null;
switch ($type) {
case 'checkbox':
$control = id(new AphrontFormCheckboxControl())
->addCheckbox(
$key,
1,
hsprintf('<strong>%s:</strong> %s', $label, $caption),
$value);
break;
case 'list':
$control = id(new AphrontFormTextControl())
->setName($key)
->setLabel($label)
->setCaption($caption)
->setValue($value ? implode(', ', $value) : null);
break;
case 'password':
$control = id(new AphrontFormPasswordControl())
->setName($key)
->setLabel($label)
->setCaption($caption)
->setDisableAutocomplete(true)
->setValue($value);
break;
case 'textarea':
$control = id(new AphrontFormTextAreaControl())
->setName($key)
->setLabel($label)
->setCaption($caption)
->setValue($value);
break;
default:
$control = id(new AphrontFormTextControl())
->setName($key)
->setLabel($label)
->setCaption($caption)
->setValue($value);
break;
}
$instruction_text = idx($instructions, $key);
if (strlen($instruction_text)) {
$form->appendRemarkupInstructions($instruction_text);
}
$form->appendChild($control);
}
}
public function renderConfigPropertyTransactionTitle(
PhabricatorAuthProviderConfigTransaction $xaction) {
$author_phid = $xaction->getAuthorPHID();
$old = $xaction->getOldValue();
$new = $xaction->getNewValue();
$key = $xaction->getMetadataValue(
PhabricatorAuthProviderConfigTransaction::PROPERTY_KEY);
$labels = $this->getPropertyLabels();
if (isset($labels[$key])) {
$label = $labels[$key];
$mask = false;
switch ($key) {
case self::KEY_ANONYMOUS_PASSWORD:
$mask = true;
break;
}
if ($mask) {
return pht(
'%s updated the "%s" value.',
$xaction->renderHandleLink($author_phid),
$label);
}
if ($old === null || $old === '') {
return pht(
'%s set the "%s" value to "%s".',
$xaction->renderHandleLink($author_phid),
$label,
$new);
} else {
return pht(
'%s changed the "%s" value from "%s" to "%s".',
$xaction->renderHandleLink($author_phid),
$label,
$old,
$new);
}
}
return parent::renderConfigPropertyTransactionTitle($xaction);
}
public static function getLDAPProvider() {
$providers = self::getAllEnabledProviders();
foreach ($providers as $provider) {
if ($provider instanceof PhabricatorLDAPAuthProvider) {
return $provider;
}
}
return null;
}
}
diff --git a/src/applications/auth/provider/PhabricatorOAuth1AuthProvider.php b/src/applications/auth/provider/PhabricatorOAuth1AuthProvider.php
index 745232a78..a22707b6c 100644
--- a/src/applications/auth/provider/PhabricatorOAuth1AuthProvider.php
+++ b/src/applications/auth/provider/PhabricatorOAuth1AuthProvider.php
@@ -1,277 +1,277 @@
<?php
abstract class PhabricatorOAuth1AuthProvider
extends PhabricatorOAuthAuthProvider {
protected $adapter;
const PROPERTY_CONSUMER_KEY = 'oauth1:consumer:key';
const PROPERTY_CONSUMER_SECRET = 'oauth1:consumer:secret';
const PROPERTY_PRIVATE_KEY = 'oauth1:private:key';
const TEMPORARY_TOKEN_TYPE = 'oauth1:request:secret';
protected function getIDKey() {
return self::PROPERTY_CONSUMER_KEY;
}
protected function getSecretKey() {
return self::PROPERTY_CONSUMER_SECRET;
}
protected function configureAdapter(PhutilOAuth1AuthAdapter $adapter) {
$config = $this->getProviderConfig();
$adapter->setConsumerKey($config->getProperty(self::PROPERTY_CONSUMER_KEY));
$secret = $config->getProperty(self::PROPERTY_CONSUMER_SECRET);
if (strlen($secret)) {
$adapter->setConsumerSecret(new PhutilOpaqueEnvelope($secret));
}
$adapter->setCallbackURI(PhabricatorEnv::getURI($this->getLoginURI()));
return $adapter;
}
protected function renderLoginForm(AphrontRequest $request, $mode) {
$attributes = array(
'method' => 'POST',
'uri' => $this->getLoginURI(),
);
return $this->renderStandardLoginButton($request, $mode, $attributes);
}
public function processLoginRequest(
PhabricatorAuthLoginController $controller) {
$request = $controller->getRequest();
$adapter = $this->getAdapter();
$account = null;
$response = null;
if ($request->isHTTPPost()) {
// Add a CSRF code to the callback URI, which we'll verify when
// performing the login.
$client_code = $this->getAuthCSRFCode($request);
$callback_uri = $adapter->getCallbackURI();
$callback_uri = $callback_uri.$client_code.'/';
$adapter->setCallbackURI($callback_uri);
$uri = $adapter->getClientRedirectURI();
$this->saveHandshakeTokenSecret(
$client_code,
$adapter->getTokenSecret());
$response = id(new AphrontRedirectResponse())
->setIsExternal(true)
->setURI($uri);
return array($account, $response);
}
$denied = $request->getStr('denied');
if (strlen($denied)) {
// Twitter indicates that the user cancelled the login attempt by
// returning "denied" as a parameter.
throw new PhutilAuthUserAbortedException();
}
// NOTE: You can get here via GET, this should probably be a bit more
// user friendly.
$this->verifyAuthCSRFCode($request, $controller->getExtraURIData());
$token = $request->getStr('oauth_token');
$verifier = $request->getStr('oauth_verifier');
if (!$token) {
- throw new Exception("Expected 'oauth_token' in request!");
+ throw new Exception(pht("Expected '%s' in request!", 'oauth_token'));
}
if (!$verifier) {
- throw new Exception("Expected 'oauth_verifier' in request!");
+ throw new Exception(pht("Expected '%s' in request!", 'oauth_verifier'));
}
$adapter->setToken($token);
$adapter->setVerifier($verifier);
$client_code = $this->getAuthCSRFCode($request);
$token_secret = $this->loadHandshakeTokenSecret($client_code);
$adapter->setTokenSecret($token_secret);
// NOTE: As a side effect, this will cause the OAuth adapter to request
// an access token.
try {
$account_id = $adapter->getAccountID();
} catch (Exception $ex) {
// TODO: Handle this in a more user-friendly way.
throw $ex;
}
if (!strlen($account_id)) {
$response = $controller->buildProviderErrorResponse(
$this,
pht(
'The OAuth provider failed to retrieve an account ID.'));
return array($account, $response);
}
return array($this->loadOrCreateAccount($account_id), $response);
}
public function processEditForm(
AphrontRequest $request,
array $values) {
$key_ckey = self::PROPERTY_CONSUMER_KEY;
$key_csecret = self::PROPERTY_CONSUMER_SECRET;
return $this->processOAuthEditForm(
$request,
$values,
pht('Consumer key is required.'),
pht('Consumer secret is required.'));
}
public function extendEditForm(
AphrontRequest $request,
AphrontFormView $form,
array $values,
array $issues) {
return $this->extendOAuthEditForm(
$request,
$form,
$values,
$issues,
pht('OAuth Consumer Key'),
pht('OAuth Consumer Secret'));
}
public function renderConfigPropertyTransactionTitle(
PhabricatorAuthProviderConfigTransaction $xaction) {
$author_phid = $xaction->getAuthorPHID();
$old = $xaction->getOldValue();
$new = $xaction->getNewValue();
$key = $xaction->getMetadataValue(
PhabricatorAuthProviderConfigTransaction::PROPERTY_KEY);
switch ($key) {
case self::PROPERTY_CONSUMER_KEY:
if (strlen($old)) {
return pht(
'%s updated the OAuth consumer key for this provider from '.
'"%s" to "%s".',
$xaction->renderHandleLink($author_phid),
$old,
$new);
} else {
return pht(
'%s set the OAuth consumer key for this provider to '.
'"%s".',
$xaction->renderHandleLink($author_phid),
$new);
}
case self::PROPERTY_CONSUMER_SECRET:
if (strlen($old)) {
return pht(
'%s updated the OAuth consumer secret for this provider.',
$xaction->renderHandleLink($author_phid));
} else {
return pht(
'%s set the OAuth consumer secret for this provider.',
$xaction->renderHandleLink($author_phid));
}
}
return parent::renderConfigPropertyTransactionTitle($xaction);
}
protected function synchronizeOAuthAccount(
PhabricatorExternalAccount $account) {
$adapter = $this->getAdapter();
$oauth_token = $adapter->getToken();
$oauth_token_secret = $adapter->getTokenSecret();
$account->setProperty('oauth1.token', $oauth_token);
$account->setProperty('oauth1.token.secret', $oauth_token_secret);
}
public function willRenderLinkedAccount(
PhabricatorUser $viewer,
PHUIObjectItemView $item,
PhabricatorExternalAccount $account) {
$item->addAttribute(pht('OAuth1 Account'));
parent::willRenderLinkedAccount($viewer, $item, $account);
}
/* -( Temporary Secrets )-------------------------------------------------- */
private function saveHandshakeTokenSecret($client_code, $secret) {
$key = $this->getHandshakeTokenKeyFromClientCode($client_code);
$type = $this->getTemporaryTokenType(self::TEMPORARY_TOKEN_TYPE);
// Wipe out an existing token, if one exists.
$token = id(new PhabricatorAuthTemporaryTokenQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withObjectPHIDs(array($key))
->withTokenTypes(array($type))
->executeOne();
if ($token) {
$token->delete();
}
// Save the new secret.
id(new PhabricatorAuthTemporaryToken())
->setObjectPHID($key)
->setTokenType($type)
->setTokenExpires(time() + phutil_units('1 hour in seconds'))
->setTokenCode($secret)
->save();
}
private function loadHandshakeTokenSecret($client_code) {
$key = $this->getHandshakeTokenKeyFromClientCode($client_code);
$type = $this->getTemporaryTokenType(self::TEMPORARY_TOKEN_TYPE);
$token = id(new PhabricatorAuthTemporaryTokenQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withObjectPHIDs(array($key))
->withTokenTypes(array($type))
->withExpired(false)
->executeOne();
if (!$token) {
throw new Exception(
pht(
'Unable to load your OAuth1 token secret from storage. It may '.
'have expired. Try authenticating again.'));
}
return $token->getTokenCode();
}
private function getTemporaryTokenType($core_type) {
// Namespace the type so that multiple providers don't step on each
// others' toes if a user starts Mediawiki and Bitbucket auth at the
// same time.
return $core_type.':'.$this->getProviderConfig()->getID();
}
private function getHandshakeTokenKeyFromClientCode($client_code) {
// NOTE: This is very slightly coersive since the TemporaryToken table
// expects an "objectPHID" as an identifier, but nothing about the storage
// is bound to PHIDs.
return 'oauth1:secret/'.$client_code;
}
}
diff --git a/src/applications/auth/provider/PhabricatorOAuth2AuthProvider.php b/src/applications/auth/provider/PhabricatorOAuth2AuthProvider.php
index 4f1f1e8fe..aff636784 100644
--- a/src/applications/auth/provider/PhabricatorOAuth2AuthProvider.php
+++ b/src/applications/auth/provider/PhabricatorOAuth2AuthProvider.php
@@ -1,277 +1,276 @@
<?php
abstract class PhabricatorOAuth2AuthProvider
extends PhabricatorOAuthAuthProvider {
const PROPERTY_APP_ID = 'oauth:app:id';
const PROPERTY_APP_SECRET = 'oauth:app:secret';
protected function getIDKey() {
return self::PROPERTY_APP_ID;
}
protected function getSecretKey() {
return self::PROPERTY_APP_SECRET;
}
protected function configureAdapter(PhutilOAuthAuthAdapter $adapter) {
$config = $this->getProviderConfig();
$adapter->setClientID($config->getProperty(self::PROPERTY_APP_ID));
$adapter->setClientSecret(
new PhutilOpaqueEnvelope(
$config->getProperty(self::PROPERTY_APP_SECRET)));
$adapter->setRedirectURI(PhabricatorEnv::getURI($this->getLoginURI()));
return $adapter;
}
protected function renderLoginForm(AphrontRequest $request, $mode) {
$adapter = $this->getAdapter();
$adapter->setState($this->getAuthCSRFCode($request));
$scope = $request->getStr('scope');
if ($scope) {
$adapter->setScope($scope);
}
$attributes = array(
'method' => 'GET',
'uri' => $adapter->getAuthenticateURI(),
);
return $this->renderStandardLoginButton($request, $mode, $attributes);
}
public function processLoginRequest(
PhabricatorAuthLoginController $controller) {
$request = $controller->getRequest();
$adapter = $this->getAdapter();
$account = null;
$response = null;
$error = $request->getStr('error');
if ($error) {
$response = $controller->buildProviderErrorResponse(
$this,
pht(
'The OAuth provider returned an error: %s',
$error));
return array($account, $response);
}
$this->verifyAuthCSRFCode($request, $request->getStr('state'));
$code = $request->getStr('code');
if (!strlen($code)) {
$response = $controller->buildProviderErrorResponse(
$this,
pht(
'The OAuth provider did not return a "code" parameter in its '.
'response.'));
return array($account, $response);
}
$adapter->setCode($code);
// NOTE: As a side effect, this will cause the OAuth adapter to request
// an access token.
try {
$account_id = $adapter->getAccountID();
} catch (Exception $ex) {
// TODO: Handle this in a more user-friendly way.
throw $ex;
}
if (!strlen($account_id)) {
$response = $controller->buildProviderErrorResponse(
$this,
pht(
'The OAuth provider failed to retrieve an account ID.'));
return array($account, $response);
}
return array($this->loadOrCreateAccount($account_id), $response);
}
public function processEditForm(
AphrontRequest $request,
array $values) {
return $this->processOAuthEditForm(
$request,
$values,
pht('Application ID is required.'),
pht('Application secret is required.'));
}
public function extendEditForm(
AphrontRequest $request,
AphrontFormView $form,
array $values,
array $issues) {
return $this->extendOAuthEditForm(
$request,
$form,
$values,
$issues,
pht('OAuth App ID'),
pht('OAuth App Secret'));
}
public function renderConfigPropertyTransactionTitle(
PhabricatorAuthProviderConfigTransaction $xaction) {
$author_phid = $xaction->getAuthorPHID();
$old = $xaction->getOldValue();
$new = $xaction->getNewValue();
$key = $xaction->getMetadataValue(
PhabricatorAuthProviderConfigTransaction::PROPERTY_KEY);
switch ($key) {
case self::PROPERTY_APP_ID:
if (strlen($old)) {
return pht(
'%s updated the OAuth application ID for this provider from '.
'"%s" to "%s".',
$xaction->renderHandleLink($author_phid),
$old,
$new);
} else {
return pht(
'%s set the OAuth application ID for this provider to '.
'"%s".',
$xaction->renderHandleLink($author_phid),
$new);
}
case self::PROPERTY_APP_SECRET:
if (strlen($old)) {
return pht(
'%s updated the OAuth application secret for this provider.',
$xaction->renderHandleLink($author_phid));
} else {
return pht(
'%s set the OAuth application secret for this provider.',
$xaction->renderHandleLink($author_phid));
}
case self::PROPERTY_NOTE:
if (strlen($old)) {
return pht(
'%s updated the OAuth application notes for this provider.',
$xaction->renderHandleLink($author_phid));
} else {
return pht(
'%s set the OAuth application notes for this provider.',
$xaction->renderHandleLink($author_phid));
}
}
return parent::renderConfigPropertyTransactionTitle($xaction);
}
protected function synchronizeOAuthAccount(
PhabricatorExternalAccount $account) {
$adapter = $this->getAdapter();
$oauth_token = $adapter->getAccessToken();
$account->setProperty('oauth.token.access', $oauth_token);
if ($adapter->supportsTokenRefresh()) {
$refresh_token = $adapter->getRefreshToken();
$account->setProperty('oauth.token.refresh', $refresh_token);
} else {
$account->setProperty('oauth.token.refresh', null);
}
$expires = $adapter->getAccessTokenExpires();
$account->setProperty('oauth.token.access.expires', $expires);
}
public function getOAuthAccessToken(
PhabricatorExternalAccount $account,
$force_refresh = false) {
if ($account->getProviderKey() !== $this->getProviderKey()) {
- throw new Exception('Account does not match provider!');
+ throw new Exception(pht('Account does not match provider!'));
}
if (!$force_refresh) {
$access_expires = $account->getProperty('oauth.token.access.expires');
$access_token = $account->getProperty('oauth.token.access');
// Don't return a token with fewer than this many seconds remaining until
// it expires.
$shortest_token = 60;
if ($access_token) {
if ($access_expires === null ||
$access_expires > (time() + $shortest_token)) {
return $access_token;
}
}
}
$refresh_token = $account->getProperty('oauth.token.refresh');
if ($refresh_token) {
$adapter = $this->getAdapter();
if ($adapter->supportsTokenRefresh()) {
$adapter->refreshAccessToken($refresh_token);
$this->synchronizeOAuthAccount($account);
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$account->save();
unset($unguarded);
return $account->getProperty('oauth.token.access');
}
}
return null;
}
public function willRenderLinkedAccount(
PhabricatorUser $viewer,
PHUIObjectItemView $item,
PhabricatorExternalAccount $account) {
// Get a valid token, possibly refreshing it. If we're unable to refresh
// it, render a message to that effect. The user may be able to repair the
// link by manually reconnecting.
$is_invalid = false;
try {
$oauth_token = $this->getOAuthAccessToken($account);
} catch (Exception $ex) {
$oauth_token = null;
$is_invalid = true;
}
$item->addAttribute(pht('OAuth2 Account'));
if ($oauth_token) {
$oauth_expires = $account->getProperty('oauth.token.access.expires');
if ($oauth_expires) {
$item->addAttribute(
pht(
'Active OAuth Token (Expires: %s)',
phabricator_datetime($oauth_expires, $viewer)));
} else {
$item->addAttribute(
- pht(
- 'Active OAuth Token'));
+ pht('Active OAuth Token'));
}
} else if ($is_invalid) {
$item->addAttribute(pht('Invalid OAuth Access Token'));
} else {
$item->addAttribute(pht('No OAuth Access Token'));
}
parent::willRenderLinkedAccount($viewer, $item, $account);
}
}
diff --git a/src/applications/auth/provider/PhabricatorOAuthAuthProvider.php b/src/applications/auth/provider/PhabricatorOAuthAuthProvider.php
index 8f7eae8aa..df76c655f 100644
--- a/src/applications/auth/provider/PhabricatorOAuthAuthProvider.php
+++ b/src/applications/auth/provider/PhabricatorOAuthAuthProvider.php
@@ -1,169 +1,170 @@
<?php
abstract class PhabricatorOAuthAuthProvider extends PhabricatorAuthProvider {
const PROPERTY_NOTE = 'oauth:app:note';
protected $adapter;
abstract protected function newOAuthAdapter();
abstract protected function getIDKey();
abstract protected function getSecretKey();
public function getDescriptionForCreate() {
return pht('Configure %s OAuth.', $this->getProviderName());
}
public function getAdapter() {
if (!$this->adapter) {
$adapter = $this->newOAuthAdapter();
$this->adapter = $adapter;
$this->configureAdapter($adapter);
}
return $this->adapter;
}
public function isLoginFormAButton() {
return true;
}
public function readFormValuesFromProvider() {
$config = $this->getProviderConfig();
$id = $config->getProperty($this->getIDKey());
$secret = $config->getProperty($this->getSecretKey());
$note = $config->getProperty(self::PROPERTY_NOTE);
return array(
$this->getIDKey() => $id,
$this->getSecretKey() => $secret,
self::PROPERTY_NOTE => $note,
);
}
public function readFormValuesFromRequest(AphrontRequest $request) {
return array(
$this->getIDKey() => $request->getStr($this->getIDKey()),
$this->getSecretKey() => $request->getStr($this->getSecretKey()),
self::PROPERTY_NOTE => $request->getStr(self::PROPERTY_NOTE),
);
}
protected function processOAuthEditForm(
AphrontRequest $request,
array $values,
$id_error,
$secret_error) {
$errors = array();
$issues = array();
$key_id = $this->getIDKey();
$key_secret = $this->getSecretKey();
if (!strlen($values[$key_id])) {
$errors[] = $id_error;
$issues[$key_id] = pht('Required');
}
if (!strlen($values[$key_secret])) {
$errors[] = $secret_error;
$issues[$key_secret] = pht('Required');
}
// If the user has not changed the secret, don't update it (that is,
// don't cause a bunch of "****" to be written to the database).
if (preg_match('/^[*]+$/', $values[$key_secret])) {
unset($values[$key_secret]);
}
return array($errors, $issues, $values);
}
public function getConfigurationHelp() {
$help = $this->getProviderConfigurationHelp();
return $help."\n\n".
- pht('Use the **OAuth App Notes** field to record details about which '.
- 'account the external application is registered under.');
+ pht(
+ 'Use the **OAuth App Notes** field to record details about which '.
+ 'account the external application is registered under.');
}
abstract protected function getProviderConfigurationHelp();
protected function extendOAuthEditForm(
AphrontRequest $request,
AphrontFormView $form,
array $values,
array $issues,
$id_label,
$secret_label) {
$key_id = $this->getIDKey();
$key_secret = $this->getSecretKey();
$key_note = self::PROPERTY_NOTE;
$v_id = $values[$key_id];
$v_secret = $values[$key_secret];
if ($v_secret) {
$v_secret = str_repeat('*', strlen($v_secret));
}
$v_note = $values[$key_note];
$e_id = idx($issues, $key_id, $request->isFormPost() ? null : true);
$e_secret = idx($issues, $key_secret, $request->isFormPost() ? null : true);
$form
->appendChild(
id(new AphrontFormTextControl())
->setLabel($id_label)
->setName($key_id)
->setValue($v_id)
->setError($e_id))
->appendChild(
id(new AphrontFormPasswordControl())
->setLabel($secret_label)
->setDisableAutocomplete(true)
->setName($key_secret)
->setValue($v_secret)
->setError($e_secret))
->appendChild(
id(new AphrontFormTextAreaControl())
->setLabel(pht('OAuth App Notes'))
->setHeight(AphrontFormTextAreaControl::HEIGHT_VERY_SHORT)
->setName($key_note)
->setValue($v_note));
}
public function renderConfigPropertyTransactionTitle(
PhabricatorAuthProviderConfigTransaction $xaction) {
$author_phid = $xaction->getAuthorPHID();
$old = $xaction->getOldValue();
$new = $xaction->getNewValue();
$key = $xaction->getMetadataValue(
PhabricatorAuthProviderConfigTransaction::PROPERTY_KEY);
switch ($key) {
case self::PROPERTY_NOTE:
if (strlen($old)) {
return pht(
'%s updated the OAuth application notes for this provider.',
$xaction->renderHandleLink($author_phid));
} else {
return pht(
'%s set the OAuth application notes for this provider.',
$xaction->renderHandleLink($author_phid));
}
}
return parent::renderConfigPropertyTransactionTitle($xaction);
}
protected function willSaveAccount(PhabricatorExternalAccount $account) {
parent::willSaveAccount($account);
$this->synchronizeOAuthAccount($account);
}
abstract protected function synchronizeOAuthAccount(
PhabricatorExternalAccount $account);
}
diff --git a/src/applications/auth/provider/PhabricatorPasswordAuthProvider.php b/src/applications/auth/provider/PhabricatorPasswordAuthProvider.php
index 6307446c8..dbe2203d8 100644
--- a/src/applications/auth/provider/PhabricatorPasswordAuthProvider.php
+++ b/src/applications/auth/provider/PhabricatorPasswordAuthProvider.php
@@ -1,378 +1,380 @@
<?php
final class PhabricatorPasswordAuthProvider extends PhabricatorAuthProvider {
private $adapter;
public function getProviderName() {
return pht('Username/Password');
}
public function getConfigurationHelp() {
return pht(
"(WARNING) Examine the table below for information on how password ".
"hashes will be stored in the database.\n\n".
"(NOTE) You can select a minimum password length by setting ".
- "`account.minimum-password-length` in configuration.");
+ "`%s` in configuration.",
+ 'account.minimum-password-length');
}
public function renderConfigurationFooter() {
$hashers = PhabricatorPasswordHasher::getAllHashers();
$hashers = msort($hashers, 'getStrength');
$hashers = array_reverse($hashers);
$yes = phutil_tag(
'strong',
array(
'style' => 'color: #009900',
),
pht('Yes'));
$no = phutil_tag(
'strong',
array(
'style' => 'color: #990000',
),
pht('Not Installed'));
$best_hasher_name = null;
try {
$best_hasher = PhabricatorPasswordHasher::getBestHasher();
$best_hasher_name = $best_hasher->getHashName();
} catch (PhabricatorPasswordHasherUnavailableException $ex) {
// There are no suitable hashers. The user might be able to enable some,
// so we don't want to fatal here. We'll fatal when users try to actually
// use this stuff if it isn't fixed before then. Until then, we just
// don't highlight a row. In practice, at least one hasher should always
// be available.
}
$rows = array();
$rowc = array();
foreach ($hashers as $hasher) {
$is_installed = $hasher->canHashPasswords();
$rows[] = array(
$hasher->getHumanReadableName(),
$hasher->getHashName(),
$hasher->getHumanReadableStrength(),
($is_installed ? $yes : $no),
($is_installed ? null : $hasher->getInstallInstructions()),
);
$rowc[] = ($best_hasher_name == $hasher->getHashName())
? 'highlighted'
: null;
}
$table = new AphrontTableView($rows);
$table->setRowClasses($rowc);
$table->setHeaders(
array(
pht('Algorithm'),
pht('Name'),
pht('Strength'),
pht('Installed'),
pht('Install Instructions'),
));
$table->setColumnClasses(
array(
'',
'',
'',
'',
'wide',
));
$header = id(new PHUIHeaderView())
->setHeader(pht('Password Hash Algorithms'))
->setSubheader(
pht(
'Stronger algorithms are listed first. The highlighted algorithm '.
'will be used when storing new hashes. Older hashes will be '.
'upgraded to the best algorithm over time.'));
return id(new PHUIObjectBoxView())
->setHeader($header)
->appendChild($table);
}
public function getDescriptionForCreate() {
return pht(
'Allow users to login or register using a username and password.');
}
public function getAdapter() {
if (!$this->adapter) {
$adapter = new PhutilEmptyAuthAdapter();
$adapter->setAdapterType('password');
$adapter->setAdapterDomain('self');
$this->adapter = $adapter;
}
return $this->adapter;
}
public function getLoginOrder() {
// Make sure username/password appears first if it is enabled.
return '100-'.$this->getProviderName();
}
public function shouldAllowAccountLink() {
return false;
}
public function shouldAllowAccountUnlink() {
return false;
}
public function isDefaultRegistrationProvider() {
return true;
}
public function buildLoginForm(
PhabricatorAuthStartController $controller) {
$request = $controller->getRequest();
return $this->renderPasswordLoginForm($request);
}
public function buildInviteForm(
PhabricatorAuthStartController $controller) {
$request = $controller->getRequest();
$viewer = $request->getViewer();
$form = id(new AphrontFormView())
->setUser($viewer)
->addHiddenInput('invite', true)
->appendChild(
id(new AphrontFormTextControl())
->setLabel(pht('Username'))
->setName('username'));
$dialog = id(new AphrontDialogView())
->setUser($viewer)
->setTitle(pht('Register an Account'))
->appendForm($form)
->setSubmitURI('/auth/register/')
->addSubmitButton(pht('Continue'));
return $dialog;
}
public function buildLinkForm(
PhabricatorAuthLinkController $controller) {
- throw new Exception("Password providers can't be linked.");
+ throw new Exception(pht("Password providers can't be linked."));
}
private function renderPasswordLoginForm(
AphrontRequest $request,
$require_captcha = false,
$captcha_valid = false) {
$viewer = $request->getUser();
$dialog = id(new AphrontDialogView())
->setSubmitURI($this->getLoginURI())
->setUser($viewer)
->setTitle(pht('Login to Phabricator'))
->addSubmitButton(pht('Login'));
if ($this->shouldAllowRegistration()) {
$dialog->addCancelButton(
'/auth/register/',
pht('Register New Account'));
}
$dialog->addFooter(
phutil_tag(
'a',
array(
'href' => '/login/email/',
),
pht('Forgot your password?')));
$v_user = nonempty(
$request->getStr('username'),
$request->getCookie(PhabricatorCookies::COOKIE_USERNAME));
$e_user = null;
$e_pass = null;
$e_captcha = null;
$errors = array();
if ($require_captcha && !$captcha_valid) {
if (AphrontFormRecaptchaControl::hasCaptchaResponse($request)) {
$e_captcha = pht('Invalid');
$errors[] = pht('CAPTCHA was not entered correctly.');
} else {
$e_captcha = pht('Required');
- $errors[] = pht('Too many login failures recently. You must '.
- 'submit a CAPTCHA with your login request.');
+ $errors[] = pht(
+ 'Too many login failures recently. You must '.
+ 'submit a CAPTCHA with your login request.');
}
} else if ($request->isHTTPPost()) {
// NOTE: This is intentionally vague so as not to disclose whether a
// given username or email is registered.
$e_user = pht('Invalid');
$e_pass = pht('Invalid');
$errors[] = pht('Username or password are incorrect.');
}
if ($errors) {
$errors = id(new PHUIInfoView())->setErrors($errors);
}
$form = id(new PHUIFormLayoutView())
->setFullWidth(true)
->appendChild($errors)
->appendChild(
id(new AphrontFormTextControl())
- ->setLabel('Username or Email')
+ ->setLabel(pht('Username or Email'))
->setName('username')
->setValue($v_user)
->setError($e_user))
->appendChild(
id(new AphrontFormPasswordControl())
- ->setLabel('Password')
+ ->setLabel(pht('Password'))
->setName('password')
->setError($e_pass));
if ($require_captcha) {
$form->appendChild(
id(new AphrontFormRecaptchaControl())
->setError($e_captcha));
}
$dialog->appendChild($form);
return $dialog;
}
public function processLoginRequest(
PhabricatorAuthLoginController $controller) {
$request = $controller->getRequest();
$viewer = $request->getUser();
$require_captcha = false;
$captcha_valid = false;
if (AphrontFormRecaptchaControl::isRecaptchaEnabled()) {
$failed_attempts = PhabricatorUserLog::loadRecentEventsFromThisIP(
PhabricatorUserLog::ACTION_LOGIN_FAILURE,
60 * 15);
if (count($failed_attempts) > 5) {
$require_captcha = true;
$captcha_valid = AphrontFormRecaptchaControl::processCaptcha($request);
}
}
$response = null;
$account = null;
$log_user = null;
if ($request->isFormPost()) {
if (!$require_captcha || $captcha_valid) {
$username_or_email = $request->getStr('username');
if (strlen($username_or_email)) {
$user = id(new PhabricatorUser())->loadOneWhere(
'username = %s',
$username_or_email);
if (!$user) {
$user = PhabricatorUser::loadOneWithEmailAddress(
$username_or_email);
}
if ($user) {
$envelope = new PhutilOpaqueEnvelope($request->getStr('password'));
if ($user->comparePassword($envelope)) {
$account = $this->loadOrCreateAccount($user->getPHID());
$log_user = $user;
// If the user's password is stored using a less-than-optimal
// hash, upgrade them to the strongest available hash.
$hash_envelope = new PhutilOpaqueEnvelope(
$user->getPasswordHash());
if (PhabricatorPasswordHasher::canUpgradeHash($hash_envelope)) {
$user->setPassword($envelope);
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$user->save();
unset($unguarded);
}
}
}
}
}
}
if (!$account) {
if ($request->isFormPost()) {
$log = PhabricatorUserLog::initializeNewLog(
null,
$log_user ? $log_user->getPHID() : null,
PhabricatorUserLog::ACTION_LOGIN_FAILURE);
$log->save();
}
$request->clearCookie(PhabricatorCookies::COOKIE_USERNAME);
$response = $controller->buildProviderPageResponse(
$this,
$this->renderPasswordLoginForm(
$request,
$require_captcha,
$captcha_valid));
}
return array($account, $response);
}
public function shouldRequireRegistrationPassword() {
return true;
}
public function getDefaultExternalAccount() {
$adapter = $this->getAdapter();
return id(new PhabricatorExternalAccount())
->setAccountType($adapter->getAdapterType())
->setAccountDomain($adapter->getAdapterDomain());
}
protected function willSaveAccount(PhabricatorExternalAccount $account) {
parent::willSaveAccount($account);
$account->setUserPHID($account->getAccountID());
}
public function willRegisterAccount(PhabricatorExternalAccount $account) {
parent::willRegisterAccount($account);
$account->setAccountID($account->getUserPHID());
}
public static function getPasswordProvider() {
$providers = self::getAllEnabledProviders();
foreach ($providers as $provider) {
if ($provider instanceof PhabricatorPasswordAuthProvider) {
return $provider;
}
}
return null;
}
public function willRenderLinkedAccount(
PhabricatorUser $viewer,
PHUIObjectItemView $item,
PhabricatorExternalAccount $account) {
return;
}
public function shouldAllowAccountRefresh() {
return false;
}
public function shouldAllowEmailTrustConfiguration() {
return false;
}
}
diff --git a/src/applications/auth/provider/PhabricatorPersonaAuthProvider.php b/src/applications/auth/provider/PhabricatorPersonaAuthProvider.php
index 843c55a36..e88b46687 100644
--- a/src/applications/auth/provider/PhabricatorPersonaAuthProvider.php
+++ b/src/applications/auth/provider/PhabricatorPersonaAuthProvider.php
@@ -1,82 +1,81 @@
<?php
final class PhabricatorPersonaAuthProvider extends PhabricatorAuthProvider {
private $adapter;
public function getProviderName() {
return pht('Persona');
}
public function getDescriptionForCreate() {
- return pht(
- 'Allow users to login or register using Mozilla Persona.');
+ return pht('Allow users to login or register using Mozilla Persona.');
}
public function getAdapter() {
if (!$this->adapter) {
$adapter = new PhutilPersonaAuthAdapter();
$this->adapter = $adapter;
}
return $this->adapter;
}
protected function renderLoginForm(
AphrontRequest $request,
$mode) {
Javelin::initBehavior(
'persona-login',
array(
'loginURI' => PhabricatorEnv::getURI($this->getLoginURI()),
));
return $this->renderStandardLoginButton(
$request,
$mode,
array(
'uri' => $this->getLoginURI(),
'sigil' => 'persona-login-form',
));
}
public function isLoginFormAButton() {
return true;
}
public function processLoginRequest(
PhabricatorAuthLoginController $controller) {
$request = $controller->getRequest();
$adapter = $this->getAdapter();
$account = null;
$response = null;
if (!$request->isAjax()) {
- throw new Exception('Expected this request to come via Ajax.');
+ throw new Exception(pht('Expected this request to come via Ajax.'));
}
$assertion = $request->getStr('assertion');
if (!$assertion) {
- throw new Exception('Expected identity assertion.');
+ throw new Exception(pht('Expected identity assertion.'));
}
$adapter->setAssertion($assertion);
$adapter->setAudience(PhabricatorEnv::getURI('/'));
try {
$account_id = $adapter->getAccountID();
} catch (Exception $ex) {
// TODO: Handle this in a more user-friendly way.
throw $ex;
}
return array($this->loadOrCreateAccount($account_id), $response);
}
protected function getLoginIcon() {
return 'Persona';
}
}
diff --git a/src/applications/auth/provider/PhabricatorPhabricatorAuthProvider.php b/src/applications/auth/provider/PhabricatorPhabricatorAuthProvider.php
index b27c895e8..8ea5e71f4 100644
--- a/src/applications/auth/provider/PhabricatorPhabricatorAuthProvider.php
+++ b/src/applications/auth/provider/PhabricatorPhabricatorAuthProvider.php
@@ -1,203 +1,204 @@
<?php
final class PhabricatorPhabricatorAuthProvider
extends PhabricatorOAuth2AuthProvider {
const PROPERTY_PHABRICATOR_NAME = 'oauth2:phabricator:name';
const PROPERTY_PHABRICATOR_URI = 'oauth2:phabricator:uri';
public function getProviderName() {
return pht('Phabricator');
}
public function getConfigurationHelp() {
if ($this->isCreate()) {
return pht(
"**Step 1 of 2 - Name Phabricator OAuth Instance**\n\n".
'Choose a permanent name for the OAuth server instance of '.
'Phabricator. //This// instance of Phabricator uses this name '.
'internally to keep track of the OAuth server instance of '.
'Phabricator, in case the URL changes later.');
}
return parent::getConfigurationHelp();
}
protected function getProviderConfigurationHelp() {
$config = $this->getProviderConfig();
$base_uri = rtrim(
$config->getProperty(self::PROPERTY_PHABRICATOR_URI), '/');
$login_uri = PhabricatorEnv::getURI($this->getLoginURI());
return pht(
"**Step 2 of 2 - Configure Phabricator OAuth Instance**\n\n".
"To configure Phabricator OAuth, create a new application here:".
"\n\n".
"%s/oauthserver/client/create/".
"\n\n".
"When creating your application, use these settings:".
"\n\n".
" - **Redirect URI:** Set this to: `%s`".
"\n\n".
"After completing configuration, copy the **Client ID** and ".
"**Client Secret** to the fields above. (You may need to generate the ".
"client secret by clicking 'New Secret' first.)",
$base_uri,
$login_uri);
}
protected function newOAuthAdapter() {
$config = $this->getProviderConfig();
return id(new PhutilPhabricatorAuthAdapter())
->setAdapterDomain($config->getProviderDomain())
->setPhabricatorBaseURI(
$config->getProperty(self::PROPERTY_PHABRICATOR_URI));
}
protected function getLoginIcon() {
return 'Phabricator';
}
private function isCreate() {
return !$this->getProviderConfig()->getID();
}
public function readFormValuesFromProvider() {
$config = $this->getProviderConfig();
$uri = $config->getProperty(self::PROPERTY_PHABRICATOR_URI);
return parent::readFormValuesFromProvider() + array(
self::PROPERTY_PHABRICATOR_NAME => $this->getProviderDomain(),
self::PROPERTY_PHABRICATOR_URI => $uri,
);
}
public function readFormValuesFromRequest(AphrontRequest $request) {
$is_setup = $this->isCreate();
if ($is_setup) {
$parent_values = array();
$name = $request->getStr(self::PROPERTY_PHABRICATOR_NAME);
} else {
$parent_values = parent::readFormValuesFromRequest($request);
$name = $this->getProviderDomain();
}
return $parent_values + array(
self::PROPERTY_PHABRICATOR_NAME => $name,
self::PROPERTY_PHABRICATOR_URI =>
$request->getStr(self::PROPERTY_PHABRICATOR_URI),
);
}
public function processEditForm(
AphrontRequest $request,
array $values) {
$is_setup = $this->isCreate();
if (!$is_setup) {
list($errors, $issues, $values) =
parent::processEditForm($request, $values);
} else {
$errors = array();
$issues = array();
}
$key_name = self::PROPERTY_PHABRICATOR_NAME;
$key_uri = self::PROPERTY_PHABRICATOR_URI;
if (!strlen($values[$key_name])) {
$errors[] = pht('Phabricator instance name is required.');
$issues[$key_name] = pht('Required');
} else if (!preg_match('/^[a-z0-9.]+\z/', $values[$key_name])) {
$errors[] = pht(
'Phabricator instance name must contain only lowercase letters, '.
'digits, and periods.');
$issues[$key_name] = pht('Invalid');
}
if (!strlen($values[$key_uri])) {
$errors[] = pht('Phabricator base URI is required.');
$issues[$key_uri] = pht('Required');
} else {
$uri = new PhutilURI($values[$key_uri]);
if (!$uri->getProtocol()) {
$errors[] = pht(
- 'Phabricator base URI should include protocol (like "https://").');
+ 'Phabricator base URI should include protocol (like "%s").',
+ 'https://');
$issues[$key_uri] = pht('Invalid');
}
}
if (!$errors && $is_setup) {
$config = $this->getProviderConfig();
$config->setProviderDomain($values[$key_name]);
}
return array($errors, $issues, $values);
}
public function extendEditForm(
AphrontRequest $request,
AphrontFormView $form,
array $values,
array $issues) {
$is_setup = $this->isCreate();
$e_required = $request->isFormPost() ? null : true;
$v_name = $values[self::PROPERTY_PHABRICATOR_NAME];
if ($is_setup) {
$e_name = idx($issues, self::PROPERTY_PHABRICATOR_NAME, $e_required);
} else {
$e_name = null;
}
$v_uri = $values[self::PROPERTY_PHABRICATOR_URI];
$e_uri = idx($issues, self::PROPERTY_PHABRICATOR_URI, $e_required);
if ($is_setup) {
$form
->appendChild(
id(new AphrontFormTextControl())
->setLabel(pht('Phabricator Instance Name'))
->setValue($v_name)
->setName(self::PROPERTY_PHABRICATOR_NAME)
->setError($e_name)
->setCaption(pht(
'Use lowercase letters, digits, and periods. For example: %s',
phutil_tag(
'tt',
array(),
'`phabricator.oauthserver`'))));
} else {
$form
->appendChild(
id(new AphrontFormStaticControl())
->setLabel(pht('Phabricator Instance Name'))
->setValue($v_name));
}
$form
->appendChild(
id(new AphrontFormTextControl())
->setLabel(pht('Phabricator Base URI'))
->setValue($v_uri)
->setName(self::PROPERTY_PHABRICATOR_URI)
->setCaption(
pht(
'The URI where the OAuth server instance of Phabricator is '.
'installed. For example: %s',
phutil_tag('tt', array(), 'https://phabricator.mycompany.com/')))
->setError($e_uri));
if (!$is_setup) {
parent::extendEditForm($request, $form, $values, $issues);
}
}
public function hasSetupStep() {
return true;
}
}
diff --git a/src/applications/auth/query/PhabricatorAuthProviderConfigQuery.php b/src/applications/auth/query/PhabricatorAuthProviderConfigQuery.php
index 028108c2c..44e591329 100644
--- a/src/applications/auth/query/PhabricatorAuthProviderConfigQuery.php
+++ b/src/applications/auth/query/PhabricatorAuthProviderConfigQuery.php
@@ -1,103 +1,103 @@
<?php
final class PhabricatorAuthProviderConfigQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $ids;
private $phids;
private $providerClasses;
const STATUS_ALL = 'status:all';
const STATUS_ENABLED = 'status:enabled';
private $status = self::STATUS_ALL;
public function withPHIDs(array $phids) {
$this->phids = $phids;
return $this;
}
public function withIDs(array $ids) {
$this->ids = $ids;
return $this;
}
public function withStatus($status) {
$this->status = $status;
return $this;
}
public function withProviderClasses(array $classes) {
$this->providerClasses = $classes;
return $this;
}
public static function getStatusOptions() {
return array(
self::STATUS_ALL => pht('All Providers'),
self::STATUS_ENABLED => pht('Enabled Providers'),
);
}
protected function loadPage() {
$table = new PhabricatorAuthProviderConfig();
$conn_r = $table->establishConnection('r');
$data = queryfx_all(
$conn_r,
'SELECT * FROM %T %Q %Q %Q',
$table->getTableName(),
$this->buildWhereClause($conn_r),
$this->buildOrderClause($conn_r),
$this->buildLimitClause($conn_r));
return $table->loadAllFromArray($data);
}
protected function buildWhereClause(AphrontDatabaseConnection $conn_r) {
$where = array();
if ($this->ids) {
$where[] = qsprintf(
$conn_r,
'id IN (%Ld)',
$this->ids);
}
if ($this->phids) {
$where[] = qsprintf(
$conn_r,
'phid IN (%Ls)',
$this->phids);
}
if ($this->providerClasses) {
$where[] = qsprintf(
$conn_r,
'providerClass IN (%Ls)',
$this->providerClasses);
}
$status = $this->status;
switch ($status) {
case self::STATUS_ALL:
break;
case self::STATUS_ENABLED:
$where[] = qsprintf(
$conn_r,
'isEnabled = 1');
break;
default:
- throw new Exception("Unknown status '{$status}'!");
+ throw new Exception(pht("Unknown status '%s'!", $status));
}
$where[] = $this->buildPagingClause($conn_r);
return $this->formatWhereClause($where);
}
public function getQueryApplicationClass() {
return 'PhabricatorAuthApplication';
}
}
diff --git a/src/applications/auth/sshkey/PhabricatorAuthSSHPublicKey.php b/src/applications/auth/sshkey/PhabricatorAuthSSHPublicKey.php
index b30e16db7..d8660b195 100644
--- a/src/applications/auth/sshkey/PhabricatorAuthSSHPublicKey.php
+++ b/src/applications/auth/sshkey/PhabricatorAuthSSHPublicKey.php
@@ -1,156 +1,157 @@
<?php
/**
* Data structure representing a raw public key.
*/
final class PhabricatorAuthSSHPublicKey extends Phobject {
private $type;
private $body;
private $comment;
private function __construct() {
// <internal>
}
public static function newFromStoredKey(PhabricatorAuthSSHKey $key) {
$public_key = new PhabricatorAuthSSHPublicKey();
$public_key->type = $key->getKeyType();
$public_key->body = $key->getKeyBody();
$public_key->comment = $key->getKeyComment();
return $public_key;
}
public static function newFromRawKey($entire_key) {
$entire_key = trim($entire_key);
if (!strlen($entire_key)) {
throw new Exception(pht('No public key was provided.'));
}
$parts = str_replace("\n", '', $entire_key);
// The third field (the comment) can have spaces in it, so split this
// into a maximum of three parts.
$parts = preg_split('/\s+/', $parts, 3);
if (preg_match('/private\s*key/i', $entire_key)) {
// Try to give the user a better error message if it looks like
// they uploaded a private key.
throw new Exception(pht('Provide a public key, not a private key!'));
}
switch (count($parts)) {
case 1:
throw new Exception(
pht('Provided public key is not properly formatted.'));
case 2:
// Add an empty comment part.
$parts[] = '';
break;
case 3:
// This is the expected case.
break;
}
list($type, $body, $comment) = $parts;
$recognized_keys = array(
'ssh-dsa',
'ssh-dss',
'ssh-rsa',
'ssh-ed25519',
'ecdsa-sha2-nistp256',
'ecdsa-sha2-nistp384',
'ecdsa-sha2-nistp521',
);
if (!in_array($type, $recognized_keys)) {
$type_list = implode(', ', $recognized_keys);
throw new Exception(
pht(
'Public key type should be one of: %s',
$type_list));
}
$public_key = new PhabricatorAuthSSHPublicKey();
$public_key->type = $type;
$public_key->body = $body;
$public_key->comment = $comment;
return $public_key;
}
public function getType() {
return $this->type;
}
public function getBody() {
return $this->body;
}
public function getComment() {
return $this->comment;
}
public function getHash() {
$body = $this->getBody();
$body = trim($body);
$body = rtrim($body, '=');
return PhabricatorHash::digestForIndex($body);
}
public function getEntireKey() {
$key = $this->type.' '.$this->body;
if (strlen($this->comment)) {
$key = $key.' '.$this->comment;
}
return $key;
}
public function toPKCS8() {
$entire_key = $this->getEntireKey();
$cache_key = $this->getPKCS8CacheKey($entire_key);
$cache = PhabricatorCaches::getImmutableCache();
$pkcs8_key = $cache->getKey($cache_key);
if ($pkcs8_key) {
return $pkcs8_key;
}
$tmp = new TempFile();
Filesystem::writeFile($tmp, $this->getEntireKey());
try {
list($pkcs8_key) = execx(
'ssh-keygen -e -m PKCS8 -f %s',
$tmp);
} catch (CommandException $ex) {
unset($tmp);
throw new PhutilProxyException(
pht(
'Failed to convert public key into PKCS8 format. If you are '.
- 'developing on OSX, you may be able to use `bin/auth cache-pkcs8` '.
+ 'developing on OSX, you may be able to use `%s` '.
'to work around this issue. %s',
+ 'bin/auth cache-pkcs8',
$ex->getMessage()),
$ex);
}
unset($tmp);
$cache->setKey($cache_key, $pkcs8_key);
return $pkcs8_key;
}
public function forcePopulatePKCS8Cache($pkcs8_key) {
$entire_key = $this->getEntireKey();
$cache_key = $this->getPKCS8CacheKey($entire_key);
$cache = PhabricatorCaches::getImmutableCache();
$cache->setKey($cache_key, $pkcs8_key);
}
private function getPKCS8CacheKey($entire_key) {
return 'pkcs8:'.PhabricatorHash::digestForIndex($entire_key);
}
}
diff --git a/src/applications/base/PhabricatorApplication.php b/src/applications/base/PhabricatorApplication.php
index 295d4db18..e97e4d605 100644
--- a/src/applications/base/PhabricatorApplication.php
+++ b/src/applications/base/PhabricatorApplication.php
@@ -1,589 +1,589 @@
<?php
/**
* @task info Application Information
* @task ui UI Integration
* @task uri URI Routing
* @task mail Email integration
* @task fact Fact Integration
* @task meta Application Management
*/
abstract class PhabricatorApplication implements PhabricatorPolicyInterface {
const MAX_STATUS_ITEMS = 100;
const GROUP_CORE = 'core';
const GROUP_UTILITIES = 'util';
const GROUP_ADMIN = 'admin';
const GROUP_DEVELOPER = 'developer';
public static function getApplicationGroups() {
return array(
self::GROUP_CORE => pht('Core Applications'),
self::GROUP_UTILITIES => pht('Utilities'),
self::GROUP_ADMIN => pht('Administration'),
self::GROUP_DEVELOPER => pht('Developer Tools'),
);
}
/* -( Application Information )-------------------------------------------- */
public abstract function getName();
public function getShortDescription() {
- return $this->getName().' Application';
+ return pht('%s Application', $this->getName());
}
public function isInstalled() {
if (!$this->canUninstall()) {
return true;
}
$prototypes = PhabricatorEnv::getEnvConfig('phabricator.show-prototypes');
if (!$prototypes && $this->isPrototype()) {
return false;
}
$uninstalled = PhabricatorEnv::getEnvConfig(
'phabricator.uninstalled-applications');
return empty($uninstalled[get_class($this)]);
}
public function isPrototype() {
return false;
}
/**
* Return `true` if this application should never appear in application lists
* in the UI. Primarily intended for unit test applications or other
* pseudo-applications.
*
* Few applications should be unlisted. For most applications, use
* @{method:isLaunchable} to hide them from main launch views instead.
*
* @return bool True to remove application from UI lists.
*/
public function isUnlisted() {
return false;
}
/**
* Return `true` if this application is a normal application with a base
* URI and a web interface.
*
* Launchable applications can be pinned to the home page, and show up in the
* "Launcher" view of the Applications application. Making an application
* unlauncahble prevents pinning and hides it from this view.
*
* Usually, an application should be marked unlaunchable if:
*
* - it is available on every page anyway (like search); or
* - it does not have a web interface (like subscriptions); or
* - it is still pre-release and being intentionally buried.
*
* To hide applications more completely, use @{method:isUnlisted}.
*
* @return bool True if the application is launchable.
*/
public function isLaunchable() {
return true;
}
/**
* Return `true` if this application should be pinned by default.
*
* Users who have not yet set preferences see a default list of applications.
*
* @param PhabricatorUser User viewing the pinned application list.
* @return bool True if this application should be pinned by default.
*/
public function isPinnedByDefault(PhabricatorUser $viewer) {
return false;
}
/**
* Returns true if an application is first-party (developed by Phacility)
* and false otherwise.
*
* @return bool True if this application is developed by Phacility.
*/
final public function isFirstParty() {
$where = id(new ReflectionClass($this))->getFileName();
$root = phutil_get_library_root('phabricator');
if (!Filesystem::isDescendant($where, $root)) {
return false;
}
if (Filesystem::isDescendant($where, $root.'/extensions')) {
return false;
}
return true;
}
public function canUninstall() {
return true;
}
public function getPHID() {
return 'PHID-APPS-'.get_class($this);
}
public function getTypeaheadURI() {
return $this->isLaunchable() ? $this->getBaseURI() : null;
}
public function getBaseURI() {
return null;
}
public function getApplicationURI($path = '') {
return $this->getBaseURI().ltrim($path, '/');
}
public function getIconURI() {
return null;
}
public function getFontIcon() {
return 'fa-puzzle-piece';
}
public function getApplicationOrder() {
return PHP_INT_MAX;
}
public function getApplicationGroup() {
return self::GROUP_CORE;
}
public function getTitleGlyph() {
return null;
}
public function getHelpMenuItems(PhabricatorUser $viewer) {
$items = array();
$articles = $this->getHelpDocumentationArticles($viewer);
if ($articles) {
$items[] = id(new PHUIListItemView())
->setType(PHUIListItemView::TYPE_LABEL)
->setName(pht('%s Documentation', $this->getName()));
foreach ($articles as $article) {
$item = id(new PHUIListItemView())
->setName($article['name'])
->setIcon('fa-book')
->setHref($article['href']);
$items[] = $item;
}
}
$command_specs = $this->getMailCommandObjects();
if ($command_specs) {
$items[] = id(new PHUIListItemView())
->setType(PHUIListItemView::TYPE_LABEL)
->setName(pht('Email Help'));
foreach ($command_specs as $key => $spec) {
$object = $spec['object'];
$class = get_class($this);
$href = '/applications/mailcommands/'.$class.'/'.$key.'/';
$item = id(new PHUIListItemView())
->setName($spec['name'])
->setIcon('fa-envelope-o')
->setHref($href);
$items[] = $item;
}
}
return $items;
}
public function getHelpDocumentationArticles(PhabricatorUser $viewer) {
return array();
}
public function getOverview() {
return null;
}
public function getEventListeners() {
return array();
}
public function getRemarkupRules() {
return array();
}
public function getQuicksandURIPatternBlacklist() {
return array();
}
public function getMailCommandObjects() {
return array();
}
/* -( URI Routing )-------------------------------------------------------- */
public function getRoutes() {
return array();
}
/* -( Email Integration )-------------------------------------------------- */
public function supportsEmailIntegration() {
return false;
}
protected function getInboundEmailSupportLink() {
return PhabricatorEnv::getDocLink('Configuring Inbound Email');
}
public function getAppEmailBlurb() {
- throw new Exception('Not Implemented.');
+ throw new PhutilMethodNotImplementedException();
}
/* -( Fact Integration )--------------------------------------------------- */
public function getFactObjectsForAnalysis() {
return array();
}
/* -( UI Integration )----------------------------------------------------- */
/**
* Render status elements (like "3 Waiting Reviews") for application list
* views. These provide a way to alert users to new or pending action items
* in applications.
*
* @param PhabricatorUser Viewing user.
* @return list<PhabricatorApplicationStatusView> Application status elements.
* @task ui
*/
public function loadStatus(PhabricatorUser $user) {
return array();
}
/**
* @return string
* @task ui
*/
public static function formatStatusCount(
$count,
$limit_string = '%s',
$base_string = '%d') {
if ($count == self::MAX_STATUS_ITEMS) {
$count_str = pht($limit_string, ($count - 1).'+');
} else {
$count_str = pht($base_string, $count);
}
return $count_str;
}
/**
* You can provide an optional piece of flavor text for the application. This
* is currently rendered in application launch views if the application has no
* status elements.
*
* @return string|null Flavor text.
* @task ui
*/
public function getFlavorText() {
return null;
}
/**
* Build items for the main menu.
*
* @param PhabricatorUser The viewing user.
* @param AphrontController The current controller. May be null for special
* pages like 404, exception handlers, etc.
* @return list<PHUIListItemView> List of menu items.
* @task ui
*/
public function buildMainMenuItems(
PhabricatorUser $user,
PhabricatorController $controller = null) {
return array();
}
/**
* Build extra items for the main menu. Generally, this is used to render
* static dropdowns.
*
* @param PhabricatorUser The viewing user.
* @param AphrontController The current controller. May be null for special
* pages like 404, exception handlers, etc.
* @return view List of menu items.
* @task ui
*/
public function buildMainMenuExtraNodes(
PhabricatorUser $viewer,
PhabricatorController $controller = null) {
return array();
}
/**
* Build items for the "quick create" menu.
*
* @param PhabricatorUser The viewing user.
* @return list<PHUIListItemView> List of menu items.
*/
public function getQuickCreateItems(PhabricatorUser $viewer) {
return array();
}
/* -( Application Management )--------------------------------------------- */
public static function getByClass($class_name) {
$selected = null;
$applications = self::getAllApplications();
foreach ($applications as $application) {
if (get_class($application) == $class_name) {
$selected = $application;
break;
}
}
if (!$selected) {
- throw new Exception("No application '{$class_name}'!");
+ throw new Exception(pht("No application '%s'!", $class_name));
}
return $selected;
}
public static function getAllApplications() {
static $applications;
if ($applications === null) {
$apps = id(new PhutilSymbolLoader())
->setAncestorClass(__CLASS__)
->loadObjects();
// Reorder the applications into "application order". Notably, this
// ensures their event handlers register in application order.
$apps = msort($apps, 'getApplicationOrder');
$apps = mgroup($apps, 'getApplicationGroup');
$group_order = array_keys(self::getApplicationGroups());
$apps = array_select_keys($apps, $group_order) + $apps;
$apps = array_mergev($apps);
$applications = $apps;
}
return $applications;
}
public static function getAllInstalledApplications() {
$all_applications = self::getAllApplications();
$apps = array();
foreach ($all_applications as $app) {
if (!$app->isInstalled()) {
continue;
}
$apps[] = $app;
}
return $apps;
}
/**
* Determine if an application is installed, by application class name.
*
* To check if an application is installed //and// available to a particular
* viewer, user @{method:isClassInstalledForViewer}.
*
* @param string Application class name.
* @return bool True if the class is installed.
* @task meta
*/
public static function isClassInstalled($class) {
return self::getByClass($class)->isInstalled();
}
/**
* Determine if an application is installed and available to a viewer, by
* application class name.
*
* To check if an application is installed at all, use
* @{method:isClassInstalled}.
*
* @param string Application class name.
* @param PhabricatorUser Viewing user.
* @return bool True if the class is installed for the viewer.
* @task meta
*/
public static function isClassInstalledForViewer(
$class,
PhabricatorUser $viewer) {
if (!self::isClassInstalled($class)) {
return false;
}
return PhabricatorPolicyFilter::hasCapability(
$viewer,
self::getByClass($class),
PhabricatorPolicyCapability::CAN_VIEW);
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array_merge(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
),
array_keys($this->getCustomCapabilities()));
}
public function getPolicy($capability) {
$default = $this->getCustomPolicySetting($capability);
if ($default) {
return $default;
}
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
return PhabricatorPolicies::getMostOpenPolicy();
case PhabricatorPolicyCapability::CAN_EDIT:
return PhabricatorPolicies::POLICY_ADMIN;
default:
$spec = $this->getCustomCapabilitySpecification($capability);
return idx($spec, 'default', PhabricatorPolicies::POLICY_USER);
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return false;
}
public function describeAutomaticCapability($capability) {
return null;
}
/* -( Policies )----------------------------------------------------------- */
protected function getCustomCapabilities() {
return array();
}
private function getCustomPolicySetting($capability) {
if (!$this->isCapabilityEditable($capability)) {
return null;
}
$policy_locked = PhabricatorEnv::getEnvConfig('policy.locked');
if (isset($policy_locked[$capability])) {
return $policy_locked[$capability];
}
$config = PhabricatorEnv::getEnvConfig('phabricator.application-settings');
$app = idx($config, $this->getPHID());
if (!$app) {
return null;
}
$policy = idx($app, 'policy');
if (!$policy) {
return null;
}
return idx($policy, $capability);
}
private function getCustomCapabilitySpecification($capability) {
$custom = $this->getCustomCapabilities();
if (!isset($custom[$capability])) {
- throw new Exception("Unknown capability '{$capability}'!");
+ throw new Exception(pht("Unknown capability '%s'!", $capability));
}
return $custom[$capability];
}
public function getCapabilityLabel($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
return pht('Can Use Application');
case PhabricatorPolicyCapability::CAN_EDIT:
return pht('Can Configure Application');
}
$capobj = PhabricatorPolicyCapability::getCapabilityByKey($capability);
if ($capobj) {
return $capobj->getCapabilityName();
}
return null;
}
public function isCapabilityEditable($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
return $this->canUninstall();
case PhabricatorPolicyCapability::CAN_EDIT:
return false;
default:
$spec = $this->getCustomCapabilitySpecification($capability);
return idx($spec, 'edit', true);
}
}
public function getCapabilityCaption($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
if (!$this->canUninstall()) {
return pht(
'This application is required for Phabricator to operate, so all '.
'users must have access to it.');
} else {
return null;
}
case PhabricatorPolicyCapability::CAN_EDIT:
return null;
default:
$spec = $this->getCustomCapabilitySpecification($capability);
return idx($spec, 'caption');
}
}
public function getApplicationSearchDocumentTypes() {
return array();
}
}
diff --git a/src/applications/base/controller/PhabricatorController.php b/src/applications/base/controller/PhabricatorController.php
index e2b899ab6..04bb633d4 100644
--- a/src/applications/base/controller/PhabricatorController.php
+++ b/src/applications/base/controller/PhabricatorController.php
@@ -1,605 +1,605 @@
<?php
abstract class PhabricatorController extends AphrontController {
private $handles;
private $extraQuicksandConfig = array();
public function shouldRequireLogin() {
return true;
}
public function shouldRequireAdmin() {
return false;
}
public function shouldRequireEnabledUser() {
return true;
}
public function shouldAllowPublic() {
return false;
}
public function shouldAllowPartialSessions() {
return false;
}
public function shouldRequireEmailVerification() {
return PhabricatorUserEmail::isEmailVerificationRequired();
}
public function shouldAllowRestrictedParameter($parameter_name) {
return false;
}
public function shouldRequireMultiFactorEnrollment() {
if (!$this->shouldRequireLogin()) {
return false;
}
if (!$this->shouldRequireEnabledUser()) {
return false;
}
if ($this->shouldAllowPartialSessions()) {
return false;
}
$user = $this->getRequest()->getUser();
if (!$user->getIsStandardUser()) {
return false;
}
return PhabricatorEnv::getEnvConfig('security.require-multi-factor-auth');
}
public function shouldAllowLegallyNonCompliantUsers() {
return false;
}
public function isGlobalDragAndDropUploadEnabled() {
return false;
}
public function addExtraQuicksandConfig($config) {
$this->extraQuicksandConfig += $config;
return $this;
}
private function getExtraQuicksandConfig() {
return $this->extraQuicksandConfig;
}
public function willBeginExecution() {
$request = $this->getRequest();
if ($request->getUser()) {
// NOTE: Unit tests can set a user explicitly. Normal requests are not
// permitted to do this.
PhabricatorTestCase::assertExecutingUnitTests();
$user = $request->getUser();
} else {
$user = new PhabricatorUser();
$session_engine = new PhabricatorAuthSessionEngine();
$phsid = $request->getCookie(PhabricatorCookies::COOKIE_SESSION);
if (strlen($phsid)) {
$session_user = $session_engine->loadUserForSession(
PhabricatorAuthSession::TYPE_WEB,
$phsid);
if ($session_user) {
$user = $session_user;
}
} else {
// If the client doesn't have a session token, generate an anonymous
// session. This is used to provide CSRF protection to logged-out users.
$phsid = $session_engine->establishSession(
PhabricatorAuthSession::TYPE_WEB,
null,
$partial = false);
// This may be a resource request, in which case we just don't set
// the cookie.
if ($request->canSetCookies()) {
$request->setCookie(PhabricatorCookies::COOKIE_SESSION, $phsid);
}
}
if (!$user->isLoggedIn()) {
$user->attachAlternateCSRFString(PhabricatorHash::digest($phsid));
}
$request->setUser($user);
}
$locale_code = $user->getTranslation();
if ($locale_code) {
PhabricatorEnv::setLocaleCode($locale_code);
}
$preferences = $user->loadPreferences();
if (PhabricatorEnv::getEnvConfig('darkconsole.enabled')) {
$dark_console = PhabricatorUserPreferences::PREFERENCE_DARK_CONSOLE;
if ($preferences->getPreference($dark_console) ||
PhabricatorEnv::getEnvConfig('darkconsole.always-on')) {
$console = new DarkConsoleCore();
$request->getApplicationConfiguration()->setConsole($console);
}
}
// NOTE: We want to set up the user first so we can render a real page
// here, but fire this before any real logic.
$restricted = array(
'code',
);
foreach ($restricted as $parameter) {
if ($request->getExists($parameter)) {
if (!$this->shouldAllowRestrictedParameter($parameter)) {
throw new Exception(
pht(
'Request includes restricted parameter "%s", but this '.
'controller ("%s") does not whitelist it. Refusing to '.
'serve this request because it might be part of a redirection '.
'attack.',
$parameter,
get_class($this)));
}
}
}
if ($this->shouldRequireEnabledUser()) {
if ($user->isLoggedIn() && !$user->getIsApproved()) {
$controller = new PhabricatorAuthNeedsApprovalController();
return $this->delegateToController($controller);
}
if ($user->getIsDisabled()) {
$controller = new PhabricatorDisabledUserController();
return $this->delegateToController($controller);
}
}
$event = new PhabricatorEvent(
PhabricatorEventType::TYPE_CONTROLLER_CHECKREQUEST,
array(
'request' => $request,
'controller' => $this,
));
$event->setUser($user);
PhutilEventEngine::dispatchEvent($event);
$checker_controller = $event->getValue('controller');
if ($checker_controller != $this) {
return $this->delegateToController($checker_controller);
}
$auth_class = 'PhabricatorAuthApplication';
$auth_application = PhabricatorApplication::getByClass($auth_class);
// Require partial sessions to finish login before doing anything.
if (!$this->shouldAllowPartialSessions()) {
if ($user->hasSession() &&
$user->getSession()->getIsPartial()) {
$login_controller = new PhabricatorAuthFinishController();
$this->setCurrentApplication($auth_application);
return $this->delegateToController($login_controller);
}
}
// Check if the user needs to configure MFA.
$need_mfa = $this->shouldRequireMultiFactorEnrollment();
$have_mfa = $user->getIsEnrolledInMultiFactor();
if ($need_mfa && !$have_mfa) {
// Check if the cache is just out of date. Otherwise, roadblock the user
// and require MFA enrollment.
$user->updateMultiFactorEnrollment();
if (!$user->getIsEnrolledInMultiFactor()) {
$mfa_controller = new PhabricatorAuthNeedsMultiFactorController();
$this->setCurrentApplication($auth_application);
return $this->delegateToController($mfa_controller);
}
}
if ($this->shouldRequireLogin()) {
// This actually means we need either:
// - a valid user, or a public controller; and
// - permission to see the application.
$allow_public = $this->shouldAllowPublic() &&
PhabricatorEnv::getEnvConfig('policy.allow-public');
// If this controller isn't public, and the user isn't logged in, require
// login.
if (!$allow_public && !$user->isLoggedIn()) {
$login_controller = new PhabricatorAuthStartController();
$this->setCurrentApplication($auth_application);
return $this->delegateToController($login_controller);
}
if ($user->isLoggedIn()) {
if ($this->shouldRequireEmailVerification()) {
if (!$user->getIsEmailVerified()) {
$controller = new PhabricatorMustVerifyEmailController();
$this->setCurrentApplication($auth_application);
return $this->delegateToController($controller);
}
}
}
// If the user doesn't have access to the application, don't let them use
// any of its controllers. We query the application in order to generate
// a policy exception if the viewer doesn't have permission.
$application = $this->getCurrentApplication();
if ($application) {
id(new PhabricatorApplicationQuery())
->setViewer($user)
->withPHIDs(array($application->getPHID()))
->executeOne();
}
}
if (!$this->shouldAllowLegallyNonCompliantUsers()) {
$legalpad_class = 'PhabricatorLegalpadApplication';
$legalpad = id(new PhabricatorApplicationQuery())
->setViewer($user)
->withClasses(array($legalpad_class))
->withInstalled(true)
->execute();
$legalpad = head($legalpad);
$doc_query = id(new LegalpadDocumentQuery())
->setViewer($user)
->withSignatureRequired(1)
->needViewerSignatures(true);
if ($user->hasSession() &&
!$user->getSession()->getIsPartial() &&
!$user->getSession()->getSignedLegalpadDocuments() &&
$user->isLoggedIn() &&
$legalpad) {
$sign_docs = $doc_query->execute();
$must_sign_docs = array();
foreach ($sign_docs as $sign_doc) {
if (!$sign_doc->getUserSignature($user->getPHID())) {
$must_sign_docs[] = $sign_doc;
}
}
if ($must_sign_docs) {
$controller = new LegalpadDocumentSignController();
$this->getRequest()->setURIMap(array(
'id' => head($must_sign_docs)->getID(),
));
$this->setCurrentApplication($legalpad);
return $this->delegateToController($controller);
} else {
$engine = id(new PhabricatorAuthSessionEngine())
->signLegalpadDocuments($user, $sign_docs);
}
}
}
// NOTE: We do this last so that users get a login page instead of a 403
// if they need to login.
if ($this->shouldRequireAdmin() && !$user->getIsAdmin()) {
return new Aphront403Response();
}
}
public function buildStandardPageView() {
$view = new PhabricatorStandardPageView();
$view->setRequest($this->getRequest());
$view->setController($this);
return $view;
}
public function buildStandardPageResponse($view, array $data) {
$page = $this->buildStandardPageView();
$page->appendChild($view);
return $this->buildPageResponse($page);
}
private function buildPageResponse($page) {
if ($this->getRequest()->isQuicksand()) {
$response = id(new AphrontAjaxResponse())
->setContent($page->renderForQuicksand(
$this->getExtraQuicksandConfig()));
} else {
$response = id(new AphrontWebpageResponse())
->setContent($page->render());
}
return $response;
}
public function getApplicationURI($path = '') {
if (!$this->getCurrentApplication()) {
- throw new Exception('No application!');
+ throw new Exception(pht('No application!'));
}
return $this->getCurrentApplication()->getApplicationURI($path);
}
public function buildApplicationPage($view, array $options) {
$page = $this->buildStandardPageView();
$title = PhabricatorEnv::getEnvConfig('phabricator.serious-business') ?
'Phabricator' :
pht('Bacon Ice Cream for Breakfast');
$application = $this->getCurrentApplication();
$page->setTitle(idx($options, 'title', $title));
if ($application) {
$page->setApplicationName($application->getName());
if ($application->getTitleGlyph()) {
$page->setGlyph($application->getTitleGlyph());
}
}
if (!($view instanceof AphrontSideNavFilterView)) {
$nav = new AphrontSideNavFilterView();
$nav->appendChild($view);
$view = $nav;
}
$user = $this->getRequest()->getUser();
$view->setUser($user);
$page->appendChild($view);
$object_phids = idx($options, 'pageObjects', array());
if ($object_phids) {
$page->appendPageObjects($object_phids);
foreach ($object_phids as $object_phid) {
PhabricatorFeedStoryNotification::updateObjectNotificationViews(
$user,
$object_phid);
}
}
if (idx($options, 'device', true)) {
$page->setDeviceReady(true);
}
$page->setShowFooter(idx($options, 'showFooter', true));
$page->setShowChrome(idx($options, 'chrome', true));
$application_menu = $this->buildApplicationMenu();
if ($application_menu) {
$page->setApplicationMenu($application_menu);
}
return $this->buildPageResponse($page);
}
public function didProcessRequest($response) {
// If a bare DialogView is returned, wrap it in a DialogResponse.
if ($response instanceof AphrontDialogView) {
$response = id(new AphrontDialogResponse())->setDialog($response);
}
$request = $this->getRequest();
$response->setRequest($request);
$seen = array();
while ($response instanceof AphrontProxyResponse) {
$hash = spl_object_hash($response);
if (isset($seen[$hash])) {
$seen[] = get_class($response);
throw new Exception(
- 'Cycle while reducing proxy responses: '.
- implode(' -> ', $seen));
+ pht('Cycle while reducing proxy responses: %s',
+ implode(' -> ', $seen)));
}
$seen[$hash] = get_class($response);
$response = $response->reduceProxyResponse();
}
if ($response instanceof AphrontDialogResponse) {
if (!$request->isAjax() && !$request->isQuicksand()) {
$dialog = $response->getDialog();
$title = $dialog->getTitle();
$short = $dialog->getShortTitle();
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb(coalesce($short, $title));
$page_content = array(
$crumbs,
$response->buildResponseString(),
);
$view = id(new PhabricatorStandardPageView())
->setRequest($request)
->setController($this)
->setDeviceReady(true)
->setTitle($title)
->appendChild($page_content);
$response = id(new AphrontWebpageResponse())
->setContent($view->render())
->setHTTPResponseCode($response->getHTTPResponseCode());
} else {
$response->getDialog()->setIsStandalone(true);
return id(new AphrontAjaxResponse())
->setContent(array(
'dialog' => $response->buildResponseString(),
));
}
} else if ($response instanceof AphrontRedirectResponse) {
if ($request->isAjax() || $request->isQuicksand()) {
return id(new AphrontAjaxResponse())
->setContent(
array(
'redirect' => $response->getURI(),
));
}
}
return $response;
}
/**
* WARNING: Do not call this in new code.
*
* @deprecated See "Handles Technical Documentation".
*/
protected function loadViewerHandles(array $phids) {
return id(new PhabricatorHandleQuery())
->setViewer($this->getRequest()->getUser())
->withPHIDs($phids)
->execute();
}
public function buildApplicationMenu() {
return null;
}
protected function buildApplicationCrumbs() {
$crumbs = array();
$application = $this->getCurrentApplication();
if ($application) {
$icon = $application->getFontIcon();
if (!$icon) {
$icon = 'fa-puzzle';
}
$crumbs[] = id(new PHUICrumbView())
->setHref($this->getApplicationURI())
->setName($application->getName())
->setIcon($icon);
}
$view = new PHUICrumbsView();
foreach ($crumbs as $crumb) {
$view->addCrumb($crumb);
}
return $view;
}
protected function hasApplicationCapability($capability) {
return PhabricatorPolicyFilter::hasCapability(
$this->getRequest()->getUser(),
$this->getCurrentApplication(),
$capability);
}
protected function requireApplicationCapability($capability) {
PhabricatorPolicyFilter::requireCapability(
$this->getRequest()->getUser(),
$this->getCurrentApplication(),
$capability);
}
protected function explainApplicationCapability(
$capability,
$positive_message,
$negative_message) {
$can_act = $this->hasApplicationCapability($capability);
if ($can_act) {
$message = $positive_message;
$icon_name = 'fa-play-circle-o lightgreytext';
} else {
$message = $negative_message;
$icon_name = 'fa-lock';
}
$icon = id(new PHUIIconView())
->setIconFont($icon_name);
require_celerity_resource('policy-css');
$phid = $this->getCurrentApplication()->getPHID();
$explain_uri = "/policy/explain/{$phid}/{$capability}/";
$message = phutil_tag(
'div',
array(
'class' => 'policy-capability-explanation',
),
array(
$icon,
javelin_tag(
'a',
array(
'href' => $explain_uri,
'sigil' => 'workflow',
),
$message),
));
return array($can_act, $message);
}
public function getDefaultResourceSource() {
return 'phabricator';
}
/**
* Create a new @{class:AphrontDialogView} with defaults filled in.
*
* @return AphrontDialogView New dialog.
*/
public function newDialog() {
$submit_uri = new PhutilURI($this->getRequest()->getRequestURI());
$submit_uri = $submit_uri->getPath();
return id(new AphrontDialogView())
->setUser($this->getRequest()->getUser())
->setSubmitURI($submit_uri);
}
protected function buildTransactionTimeline(
PhabricatorApplicationTransactionInterface $object,
PhabricatorApplicationTransactionQuery $query,
PhabricatorMarkupEngine $engine = null,
$render_data = array()) {
$viewer = $this->getRequest()->getUser();
$xaction = $object->getApplicationTransactionTemplate();
$view = $xaction->getApplicationTransactionViewObject();
$pager = id(new AphrontCursorPagerView())
->readFromRequest($this->getRequest())
->setURI(new PhutilURI(
'/transactions/showolder/'.$object->getPHID().'/'));
$xactions = $query
->setViewer($viewer)
->withObjectPHIDs(array($object->getPHID()))
->needComments(true)
->executeWithCursorPager($pager);
$xactions = array_reverse($xactions);
if ($engine) {
foreach ($xactions as $xaction) {
if ($xaction->getComment()) {
$engine->addObject(
$xaction->getComment(),
PhabricatorApplicationTransactionComment::MARKUP_FIELD_COMMENT);
}
}
$engine->process();
$view->setMarkupEngine($engine);
}
$timeline = $view
->setUser($viewer)
->setObjectPHID($object->getPHID())
->setTransactions($xactions)
->setPager($pager)
->setRenderData($render_data)
->setQuoteTargetID($this->getRequest()->getStr('quoteTargetID'))
->setQuoteRef($this->getRequest()->getStr('quoteRef'));
$object->willRenderTimeline($timeline, $this->getRequest());
return $timeline;
}
}
diff --git a/src/applications/base/controller/__tests__/PhabricatorAccessControlTestCase.php b/src/applications/base/controller/__tests__/PhabricatorAccessControlTestCase.php
index 3791d7462..560e0c95c 100644
--- a/src/applications/base/controller/__tests__/PhabricatorAccessControlTestCase.php
+++ b/src/applications/base/controller/__tests__/PhabricatorAccessControlTestCase.php
@@ -1,281 +1,281 @@
<?php
final class PhabricatorAccessControlTestCase extends PhabricatorTestCase {
protected function getPhabricatorTestCaseConfiguration() {
return array(
self::PHABRICATOR_TESTCONFIG_BUILD_STORAGE_FIXTURES => true,
);
}
public function testControllerAccessControls() {
$root = dirname(phutil_get_library_root('phabricator'));
require_once $root.'/support/PhabricatorStartup.php';
$application_configuration = new AphrontDefaultApplicationConfiguration();
$host = 'meow.example.com';
$_SERVER['REQUEST_METHOD'] = 'GET';
$request = id(new AphrontRequest($host, '/'))
->setApplicationConfiguration($application_configuration)
->setRequestData(array());
$controller = new PhabricatorTestController();
$controller->setRequest($request);
$u_public = id(new PhabricatorUser())
->setUsername('public');
$u_unverified = $this->generateNewTestUser()
->setUsername('unverified')
->save();
$u_unverified->setIsEmailVerified(0)->save();
$u_normal = $this->generateNewTestUser()
->setUsername('normal')
->save();
$u_disabled = $this->generateNewTestUser()
->setIsDisabled(true)
->setUsername('disabled')
->save();
$u_admin = $this->generateNewTestUser()
->setIsAdmin(true)
->setUsername('admin')
->save();
$u_notapproved = $this->generateNewTestUser()
->setIsApproved(0)
->setUsername('notapproved')
->save();
$env = PhabricatorEnv::beginScopedEnv();
$env->overrideEnvConfig('phabricator.base-uri', 'http://'.$host);
$env->overrideEnvConfig('policy.allow-public', false);
$env->overrideEnvConfig('auth.require-email-verification', false);
$env->overrideEnvConfig('auth.email-domains', array());
$env->overrideEnvConfig('security.require-multi-factor-auth', false);
// Test standard defaults.
$this->checkAccess(
- 'Default',
+ pht('Default'),
id(clone $controller),
$request,
array(
$u_normal,
$u_admin,
$u_unverified,
),
array(
$u_public,
$u_disabled,
$u_notapproved,
));
// Test email verification.
$env->overrideEnvConfig('auth.require-email-verification', true);
$this->checkAccess(
- 'Email Verification Required',
+ pht('Email Verification Required'),
id(clone $controller),
$request,
array(
$u_normal,
$u_admin,
),
array(
$u_unverified,
$u_public,
$u_disabled,
$u_notapproved,
));
$this->checkAccess(
- 'Email Verification Required, With Exception',
+ pht('Email Verification Required, With Exception'),
id(clone $controller)->setConfig('email', false),
$request,
array(
$u_normal,
$u_admin,
$u_unverified,
),
array(
$u_public,
$u_disabled,
$u_notapproved,
));
$env->overrideEnvConfig('auth.require-email-verification', false);
// Test admin access.
$this->checkAccess(
- 'Admin Required',
+ pht('Admin Required'),
id(clone $controller)->setConfig('admin', true),
$request,
array(
$u_admin,
),
array(
$u_normal,
$u_unverified,
$u_public,
$u_disabled,
$u_notapproved,
));
// Test disabled access.
$this->checkAccess(
- 'Allow Disabled',
+ pht('Allow Disabled'),
id(clone $controller)->setConfig('enabled', false),
$request,
array(
$u_normal,
$u_unverified,
$u_admin,
$u_disabled,
$u_notapproved,
),
array(
$u_public,
));
// Test no login required.
$this->checkAccess(
- 'No Login Required',
+ pht('No Login Required'),
id(clone $controller)->setConfig('login', false),
$request,
array(
$u_normal,
$u_unverified,
$u_admin,
$u_public,
),
array(
$u_disabled,
$u_notapproved,
));
// Test public access.
$this->checkAccess(
- 'Public Access',
+ pht('Public Access'),
id(clone $controller)->setConfig('public', true),
$request,
array(
$u_normal,
$u_unverified,
$u_admin,
),
array(
$u_disabled,
$u_public,
));
$env->overrideEnvConfig('policy.allow-public', true);
$this->checkAccess(
- 'Public + configured',
+ pht('Public + configured'),
id(clone $controller)->setConfig('public', true),
$request,
array(
$u_normal,
$u_unverified,
$u_admin,
$u_public,
),
array(
$u_disabled,
$u_notapproved,
));
$env->overrideEnvConfig('policy.allow-public', false);
$app = PhabricatorApplication::getByClass('PhabricatorTestApplication');
$app->reset();
$app->setPolicy(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicies::POLICY_NOONE);
$app_controller = id(clone $controller)->setCurrentApplication($app);
$this->checkAccess(
- 'Application Controller',
+ pht('Application Controller'),
$app_controller,
$request,
array(
),
array(
$u_normal,
$u_unverified,
$u_admin,
$u_public,
$u_disabled,
$u_notapproved,
));
$this->checkAccess(
- 'Application Controller',
+ pht('Application Controller'),
id(clone $app_controller)->setConfig('login', false),
$request,
array(
$u_normal,
$u_unverified,
$u_admin,
$u_public,
),
array(
$u_disabled,
$u_notapproved,
));
}
private function checkAccess(
$label,
$controller,
$request,
array $yes,
array $no) {
foreach ($yes as $user) {
$request->setUser($user);
$uname = $user->getUsername();
try {
$result = id(clone $controller)->willBeginExecution();
} catch (Exception $ex) {
$result = $ex;
}
$this->assertTrue(
($result === null),
- "Expect user '{$uname}' to be allowed access to '{$label}'.");
+ pht("Expect user '%s' to be allowed access to '%s'.", $uname, $label));
}
foreach ($no as $user) {
$request->setUser($user);
$uname = $user->getUsername();
try {
$result = id(clone $controller)->willBeginExecution();
} catch (Exception $ex) {
$result = $ex;
}
$this->assertFalse(
($result === null),
- "Expect user '{$uname}' to be denied access to '{$label}'.");
+ pht("Expect user '%s' to be denied access to '%s'.", $uname, $label));
}
}
}
diff --git a/src/applications/cache/PhabricatorCaches.php b/src/applications/cache/PhabricatorCaches.php
index 4e3af7a6a..1cddba9fb 100644
--- a/src/applications/cache/PhabricatorCaches.php
+++ b/src/applications/cache/PhabricatorCaches.php
@@ -1,347 +1,349 @@
<?php
/**
* @task immutable Immutable Cache
* @task setup Setup Cache
* @task compress Compression
*/
final class PhabricatorCaches {
public static function getNamespace() {
return PhabricatorEnv::getEnvConfig('phabricator.cache-namespace');
}
private static function newStackFromCaches(array $caches) {
$caches = self::addNamespaceToCaches($caches);
$caches = self::addProfilerToCaches($caches);
return id(new PhutilKeyValueCacheStack())
->setCaches($caches);
}
/* -( Local Cache )-------------------------------------------------------- */
/**
* Gets an immutable cache stack.
*
* This stack trades mutability away for improved performance. Normally, it is
* APC + DB.
*
* In the general case with multiple web frontends, this stack can not be
* cleared, so it is only appropriate for use if the value of a given key is
* permanent and immutable.
*
* @return PhutilKeyValueCacheStack Best immutable stack available.
* @task immutable
*/
public static function getImmutableCache() {
static $cache;
if (!$cache) {
$caches = self::buildImmutableCaches();
$cache = self::newStackFromCaches($caches);
}
return $cache;
}
/**
* Build the immutable cache stack.
*
* @return list<PhutilKeyValueCache> List of caches.
* @task immutable
*/
private static function buildImmutableCaches() {
$caches = array();
$apc = new PhutilAPCKeyValueCache();
if ($apc->isAvailable()) {
$caches[] = $apc;
}
$caches[] = new PhabricatorKeyValueDatabaseCache();
return $caches;
}
/* -( Repository Graph Cache )--------------------------------------------- */
public static function getRepositoryGraphL1Cache() {
static $cache;
if (!$cache) {
$caches = self::buildRepositoryGraphL1Caches();
$cache = self::newStackFromCaches($caches);
}
return $cache;
}
private static function buildRepositoryGraphL1Caches() {
$caches = array();
$request = new PhutilInRequestKeyValueCache();
$request->setLimit(32);
$caches[] = $request;
$apc = new PhutilAPCKeyValueCache();
if ($apc->isAvailable()) {
$caches[] = $apc;
}
return $caches;
}
public static function getRepositoryGraphL2Cache() {
static $cache;
if (!$cache) {
$caches = self::buildRepositoryGraphL2Caches();
$cache = self::newStackFromCaches($caches);
}
return $cache;
}
private static function buildRepositoryGraphL2Caches() {
$caches = array();
$caches[] = new PhabricatorKeyValueDatabaseCache();
return $caches;
}
/* -( Setup Cache )-------------------------------------------------------- */
/**
* Highly specialized cache for performing setup checks. We use this cache
* to determine if we need to run expensive setup checks when the page
* loads. Without it, we would need to run these checks every time.
*
* Normally, this cache is just APC. In the absence of APC, this cache
* degrades into a slow, quirky on-disk cache.
*
* NOTE: Do not use this cache for anything else! It is not a general-purpose
* cache!
*
* @return PhutilKeyValueCacheStack Most qualified available cache stack.
* @task setup
*/
public static function getSetupCache() {
static $cache;
if (!$cache) {
$caches = self::buildSetupCaches();
$cache = self::newStackFromCaches($caches);
}
return $cache;
}
/**
* @task setup
*/
private static function buildSetupCaches() {
// In most cases, we should have APC. This is an ideal cache for our
// purposes -- it's fast and empties on server restart.
$apc = new PhutilAPCKeyValueCache();
if ($apc->isAvailable()) {
return array($apc);
}
// If we don't have APC, build a poor approximation on disk. This is still
// much better than nothing; some setup steps are quite slow.
$disk_path = self::getSetupCacheDiskCachePath();
if ($disk_path) {
$disk = new PhutilOnDiskKeyValueCache();
$disk->setCacheFile($disk_path);
$disk->setWait(0.1);
if ($disk->isAvailable()) {
return array($disk);
}
}
return array();
}
/**
* @task setup
*/
private static function getSetupCacheDiskCachePath() {
// The difficulty here is in choosing a path which will change on server
// restart (we MUST have this property), but as rarely as possible
// otherwise (we desire this property to give the cache the best hit rate
// we can).
// In some setups, the parent PID is more stable and longer-lived that the
// PID (e.g., under apache, our PID will be a worker while the ppid will
// be the main httpd process). If we're confident we're running under such
// a setup, we can try to use the PPID as the basis for our cache instead
// of our own PID.
$use_ppid = false;
switch (php_sapi_name()) {
case 'cli-server':
// This is the PHP5.4+ built-in webserver. We should use the pid
// (the server), not the ppid (probably a shell or something).
$use_ppid = false;
break;
case 'fpm-fcgi':
// We should be safe to use PPID here.
$use_ppid = true;
break;
case 'apache2handler':
// We're definitely safe to use the PPID.
$use_ppid = true;
break;
}
$pid_basis = getmypid();
if ($use_ppid) {
if (function_exists('posix_getppid')) {
$parent_pid = posix_getppid();
// On most systems, normal processes can never have PIDs lower than 100,
// so something likely went wrong if we we get one of these.
if ($parent_pid > 100) {
$pid_basis = $parent_pid;
}
}
}
// If possible, we also want to know when the process launched, so we can
// drop the cache if a process restarts but gets the same PID an earlier
// process had. "/proc" is not available everywhere (e.g., not on OSX), but
// check if we have it.
$epoch_basis = null;
$stat = @stat("/proc/{$pid_basis}");
if ($stat !== false) {
$epoch_basis = $stat['ctime'];
}
$tmp_dir = sys_get_temp_dir();
$tmp_path = $tmp_dir.DIRECTORY_SEPARATOR.'phabricator-setup';
if (!file_exists($tmp_path)) {
@mkdir($tmp_path);
}
$is_ok = self::testTemporaryDirectory($tmp_path);
if (!$is_ok) {
$tmp_path = $tmp_dir;
$is_ok = self::testTemporaryDirectory($tmp_path);
if (!$is_ok) {
// We can't find anywhere to write the cache, so just bail.
return null;
}
}
$tmp_name = 'setup-'.$pid_basis;
if ($epoch_basis) {
$tmp_name .= '.'.$epoch_basis;
}
$tmp_name .= '.cache';
return $tmp_path.DIRECTORY_SEPARATOR.$tmp_name;
}
/**
* @task setup
*/
private static function testTemporaryDirectory($dir) {
if (!@file_exists($dir)) {
return false;
}
if (!@is_dir($dir)) {
return false;
}
if (!@is_writable($dir)) {
return false;
}
return true;
}
private static function addProfilerToCaches(array $caches) {
foreach ($caches as $key => $cache) {
$pcache = new PhutilKeyValueCacheProfiler($cache);
$pcache->setProfiler(PhutilServiceProfiler::getInstance());
$caches[$key] = $pcache;
}
return $caches;
}
private static function addNamespaceToCaches(array $caches) {
$namespace = self::getNamespace();
if (!$namespace) {
return $caches;
}
foreach ($caches as $key => $cache) {
$ncache = new PhutilKeyValueCacheNamespace($cache);
$ncache->setNamespace($namespace);
$caches[$key] = $ncache;
}
return $caches;
}
/**
* Deflate a value, if deflation is available and has an impact.
*
* If the value is larger than 1KB, we have `gzdeflate()`, we successfully
* can deflate it, and it benefits from deflation, we deflate it. Otherwise
* we leave it as-is.
*
* Data can later be inflated with @{method:inflateData}.
*
* @param string String to attempt to deflate.
* @return string|null Deflated string, or null if it was not deflated.
* @task compress
*/
public static function maybeDeflateData($value) {
$len = strlen($value);
if ($len <= 1024) {
return null;
}
if (!function_exists('gzdeflate')) {
return null;
}
$deflated = gzdeflate($value);
if ($deflated === false) {
return null;
}
$deflated_len = strlen($deflated);
if ($deflated_len >= ($len / 2)) {
return null;
}
return $deflated;
}
/**
* Inflate data previously deflated by @{method:maybeDeflateData}.
*
* @param string Deflated data, from @{method:maybeDeflateData}.
* @return string Original, uncompressed data.
* @task compress
*/
public static function inflateData($value) {
if (!function_exists('gzinflate')) {
throw new Exception(
- pht('gzinflate() is not available; unable to read deflated data!'));
+ pht(
+ '%s is not available; unable to read deflated data!',
+ 'gzinflate()'));
}
$value = gzinflate($value);
if ($value === false) {
throw new Exception(pht('Failed to inflate data!'));
}
return $value;
}
}
diff --git a/src/applications/cache/PhabricatorKeyValueDatabaseCache.php b/src/applications/cache/PhabricatorKeyValueDatabaseCache.php
index 5bdf60f66..85bc57f8b 100644
--- a/src/applications/cache/PhabricatorKeyValueDatabaseCache.php
+++ b/src/applications/cache/PhabricatorKeyValueDatabaseCache.php
@@ -1,170 +1,170 @@
<?php
final class PhabricatorKeyValueDatabaseCache
extends PhutilKeyValueCache {
const CACHE_FORMAT_RAW = 'raw';
const CACHE_FORMAT_DEFLATE = 'deflate';
public function setKeys(array $keys, $ttl = null) {
if ($keys) {
$map = $this->digestKeys(array_keys($keys));
$conn_w = $this->establishConnection('w');
$sql = array();
foreach ($map as $key => $hash) {
$value = $keys[$key];
list($format, $storage_value) = $this->willWriteValue($key, $value);
$sql[] = qsprintf(
$conn_w,
'(%s, %s, %s, %B, %d, %nd)',
$hash,
$key,
$format,
$storage_value,
time(),
$ttl ? (time() + $ttl) : null);
}
$guard = AphrontWriteGuard::beginScopedUnguardedWrites();
foreach (PhabricatorLiskDAO::chunkSQL($sql) as $chunk) {
queryfx(
$conn_w,
'INSERT INTO %T
(cacheKeyHash, cacheKey, cacheFormat, cacheData,
cacheCreated, cacheExpires) VALUES %Q
ON DUPLICATE KEY UPDATE
cacheKey = VALUES(cacheKey),
cacheFormat = VALUES(cacheFormat),
cacheData = VALUES(cacheData),
cacheCreated = VALUES(cacheCreated),
cacheExpires = VALUES(cacheExpires)',
$this->getTableName(),
$chunk);
}
unset($guard);
}
return $this;
}
public function getKeys(array $keys) {
$results = array();
if ($keys) {
$map = $this->digestKeys($keys);
$rows = queryfx_all(
$this->establishConnection('r'),
'SELECT * FROM %T WHERE cacheKeyHash IN (%Ls)',
$this->getTableName(),
$map);
$rows = ipull($rows, null, 'cacheKey');
foreach ($keys as $key) {
if (empty($rows[$key])) {
continue;
}
$row = $rows[$key];
if ($row['cacheExpires'] && ($row['cacheExpires'] < time())) {
continue;
}
try {
$results[$key] = $this->didReadValue(
$row['cacheFormat'],
$row['cacheData']);
} catch (Exception $ex) {
// Treat this as a cache miss.
phlog($ex);
}
}
}
return $results;
}
public function deleteKeys(array $keys) {
if ($keys) {
$map = $this->digestKeys($keys);
queryfx(
$this->establishConnection('w'),
'DELETE FROM %T WHERE cacheKeyHash IN (%Ls)',
$this->getTableName(),
$keys);
}
return $this;
}
public function destroyCache() {
queryfx(
$this->establishConnection('w'),
'DELETE FROM %T',
$this->getTableName());
return $this;
}
/* -( Raw Cache Access )--------------------------------------------------- */
public function establishConnection($mode) {
// TODO: This is the only concrete table we have on the database right
// now.
return id(new PhabricatorMarkupCache())->establishConnection($mode);
}
public function getTableName() {
return 'cache_general';
}
/* -( Implementation )----------------------------------------------------- */
private function digestKeys(array $keys) {
$map = array();
foreach ($keys as $key) {
$map[$key] = PhabricatorHash::digestForIndex($key);
}
return $map;
}
private function willWriteValue($key, $value) {
if (!is_string($value)) {
- throw new Exception('Only strings may be written to the DB cache!');
+ throw new Exception(pht('Only strings may be written to the DB cache!'));
}
static $can_deflate;
if ($can_deflate === null) {
$can_deflate = function_exists('gzdeflate') &&
PhabricatorEnv::getEnvConfig('cache.enable-deflate');
}
if ($can_deflate) {
$deflated = PhabricatorCaches::maybeDeflateData($value);
if ($deflated !== null) {
return array(self::CACHE_FORMAT_DEFLATE, $deflated);
}
}
return array(self::CACHE_FORMAT_RAW, $value);
}
private function didReadValue($format, $value) {
switch ($format) {
case self::CACHE_FORMAT_RAW:
return $value;
case self::CACHE_FORMAT_DEFLATE:
return PhabricatorCaches::inflateData($value);
default:
- throw new Exception('Unknown cache format.');
+ throw new Exception(pht('Unknown cache format.'));
}
}
}
diff --git a/src/applications/cache/management/PhabricatorCacheManagementPurgeWorkflow.php b/src/applications/cache/management/PhabricatorCacheManagementPurgeWorkflow.php
index 2d6de653b..2523a1e33 100644
--- a/src/applications/cache/management/PhabricatorCacheManagementPurgeWorkflow.php
+++ b/src/applications/cache/management/PhabricatorCacheManagementPurgeWorkflow.php
@@ -1,99 +1,102 @@
<?php
final class PhabricatorCacheManagementPurgeWorkflow
extends PhabricatorCacheManagementWorkflow {
protected function didConstruct() {
$this
->setName('purge')
- ->setSynopsis('Drop data from caches.')
+ ->setSynopsis(pht('Drop data from caches.'))
->setArguments(
array(
array(
'name' => 'purge-all',
- 'help' => 'Purge all caches.',
+ 'help' => pht('Purge all caches.'),
),
array(
'name' => 'purge-remarkup',
- 'help' => 'Purge the remarkup cache.',
+ 'help' => pht('Purge the remarkup cache.'),
),
array(
'name' => 'purge-changeset',
- 'help' => 'Purge the Differential changeset cache.',
+ 'help' => pht('Purge the Differential changeset cache.'),
),
array(
'name' => 'purge-general',
- 'help' => 'Purge the general cache.',
+ 'help' => pht('Purge the general cache.'),
),
));
}
public function execute(PhutilArgumentParser $args) {
$console = PhutilConsole::getConsole();
$purge_all = $args->getArg('purge-all');
$purge = array(
'remarkup' => $purge_all || $args->getArg('purge-remarkup'),
'changeset' => $purge_all || $args->getArg('purge-changeset'),
'general' => $purge_all || $args->getArg('purge-general'),
);
if (!array_filter($purge)) {
$list = array();
foreach ($purge as $key => $ignored) {
$list[] = "'--purge-".$key."'";
}
throw new PhutilArgumentUsageException(
- "Specify which cache or caches to purge, or use '--purge-all'. ".
- "Available caches are: ".implode(', ', $list).". Use '--help' ".
- "for more information.");
+ pht(
+ "Specify which cache or caches to purge, or use '%s'. Available ".
+ "caches are: %s. Use '%s' for more information.",
+ '--purge-all',
+ implode(', ', $list),
+ '--help'));
}
if ($purge['remarkup']) {
- $console->writeOut('Purging remarkup cache...');
+ $console->writeOut(pht('Purging remarkup cache...'));
$this->purgeRemarkupCache();
- $console->writeOut("done.\n");
+ $console->writeOut("%s\n", pht('Done.'));
}
if ($purge['changeset']) {
- $console->writeOut('Purging changeset cache...');
+ $console->writeOut(pht('Purging changeset cache...'));
$this->purgeChangesetCache();
- $console->writeOut("done.\n");
+ $console->writeOut("%s\n", pht('Done.'));
}
if ($purge['general']) {
- $console->writeOut('Purging general cache...');
+ $console->writeOut(pht('Purging general cache...'));
$this->purgeGeneralCache();
- $console->writeOut("done.\n");
+ $console->writeOut("%s\n", pht('Done.'));
}
}
private function purgeRemarkupCache() {
$conn_w = id(new PhabricatorMarkupCache())->establishConnection('w');
queryfx(
$conn_w,
'TRUNCATE TABLE %T',
id(new PhabricatorMarkupCache())->getTableName());
}
private function purgeChangesetCache() {
$conn_w = id(new DifferentialChangeset())->establishConnection('w');
queryfx(
$conn_w,
'TRUNCATE TABLE %T',
DifferentialChangeset::TABLE_CACHE);
}
private function purgeGeneralCache() {
$conn_w = id(new PhabricatorMarkupCache())->establishConnection('w');
queryfx(
$conn_w,
'TRUNCATE TABLE %T',
'cache_general');
}
}
diff --git a/src/applications/cache/spec/PhabricatorCacheSpec.php b/src/applications/cache/spec/PhabricatorCacheSpec.php
index 31645ea97..1688cb07f 100644
--- a/src/applications/cache/spec/PhabricatorCacheSpec.php
+++ b/src/applications/cache/spec/PhabricatorCacheSpec.php
@@ -1,111 +1,111 @@
<?php
abstract class PhabricatorCacheSpec extends Phobject {
private $name;
private $isEnabled = false;
private $version;
private $issues = array();
private $usedMemory = 0;
private $totalMemory = 0;
private $entryCount = null;
public function setName($name) {
$this->name = $name;
return $this;
}
public function getName() {
return $this->name;
}
public function setIsEnabled($is_enabled) {
$this->isEnabled = $is_enabled;
return $this;
}
public function getIsEnabled() {
return $this->isEnabled;
}
public function setVersion($version) {
$this->version = $version;
return $this;
}
public function getVersion() {
return $this->version;
}
protected function newIssue($key) {
$issue = id(new PhabricatorSetupIssue())
->setIssueKey($key);
$this->issues[$key] = $issue;
return $issue;
}
public function getIssues() {
return $this->issues;
}
public function setUsedMemory($used_memory) {
$this->usedMemory = $used_memory;
return $this;
}
public function getUsedMemory() {
return $this->usedMemory;
}
public function setTotalMemory($total_memory) {
$this->totalMemory = $total_memory;
return $this;
}
public function getTotalMemory() {
return $this->totalMemory;
}
public function setEntryCount($entry_count) {
$this->entryCount = $entry_count;
return $this;
}
public function getEntryCount() {
return $this->entryCount;
}
protected function raiseInstallAPCIssue() {
$message = pht(
"Installing the PHP extension 'APC' (Alternative PHP Cache) will ".
"dramatically improve performance. Note that APC versions 3.1.14 and ".
"3.1.15 are broken; 3.1.13 is recommended instead.");
return $this
->newIssue('extension.apc')
->setShortName(pht('APC'))
->setName(pht("PHP Extension 'APC' Not Installed"))
->setMessage($message)
->addPHPExtension('apc');
}
protected function raiseEnableAPCIssue() {
$summary = pht('Enabling APC/APCu will improve performance.');
$message = pht(
'The APC or APCu PHP extensions are installed, but not enabled in your '.
'PHP configuration. Enabling these extensions will improve Phabricator '.
- 'performance. Edit the "apc.enabled" setting to enable these '.
- 'extensions.');
+ 'performance. Edit the "%s" setting to enable these extensions.',
+ 'apc.enabled');
return $this
->newIssue('extension.apc.enabled')
->setShortName(pht('APC/APCu Disabled'))
->setName(pht('APC/APCu Extensions Not Enabled'))
->setSummary($summary)
->setMessage($message)
->addPHPConfig('apc.enabled');
}
}
diff --git a/src/applications/cache/spec/PhabricatorOpcodeCacheSpec.php b/src/applications/cache/spec/PhabricatorOpcodeCacheSpec.php
index dd270fa2a..c129fb0fa 100644
--- a/src/applications/cache/spec/PhabricatorOpcodeCacheSpec.php
+++ b/src/applications/cache/spec/PhabricatorOpcodeCacheSpec.php
@@ -1,198 +1,203 @@
<?php
final class PhabricatorOpcodeCacheSpec extends PhabricatorCacheSpec {
public static function getActiveCacheSpec() {
$spec = new PhabricatorOpcodeCacheSpec();
// NOTE: If APCu is installed, it reports that APC is installed.
if (extension_loaded('apc') && !extension_loaded('apcu')) {
$spec->initAPCSpec();
} else if (extension_loaded('Zend OPcache')) {
$spec->initOpcacheSpec();
} else {
$spec->initNoneSpec();
}
return $spec;
}
private function initAPCSpec() {
$this
->setName(pht('APC'))
->setVersion(phpversion('apc'));
if (ini_get('apc.enabled')) {
$this->setIsEnabled(true);
$mem = apc_sma_info();
$this->setTotalMemory($mem['num_seg'] * $mem['seg_size']);
$info = apc_cache_info();
$this->setUsedMemory($info['mem_size']);
$write_lock = ini_get('apc.write_lock');
$slam_defense = ini_get('apc.slam_defense');
if (!$write_lock || $slam_defense) {
- $summary = pht(
- 'Adjust APC settings to quiet unnecessary errors.');
+ $summary = pht('Adjust APC settings to quiet unnecessary errors.');
$message = pht(
'Some versions of APC may emit unnecessary errors into the '.
'error log under the current APC settings. To resolve this, '.
- 'enable "apc.write_lock" and disable "apc.slam_defense" in '.
- 'your PHP configuration.');
+ 'enable "%s" and disable "%s" in your PHP configuration.',
+ 'apc.write_lock',
+ 'apc.slam_defense');
$this
->newIssue('extension.apc.write-lock')
->setShortName(pht('Noisy APC'))
->setName(pht('APC Has Noisy Configuration'))
->setSummary($summary)
->setMessage($message)
->addPHPConfig('apc.write_lock')
->addPHPConfig('apc.slam_defense');
}
$is_dev = PhabricatorEnv::getEnvConfig('phabricator.developer-mode');
$is_stat_enabled = ini_get('apc.stat');
if ($is_stat_enabled && !$is_dev) {
$summary = pht(
- '"apc.stat" is currently enabled, but should probably be disabled.');
+ '"%s" is currently enabled, but should probably be disabled.',
+ 'apc.stat');
$message = pht(
- 'The "apc.stat" setting is currently enabled in your PHP '.
- 'configuration. In production mode, "apc.stat" should be '.
- 'disabled. This will improve performance slightly.');
+ 'The "%s" setting is currently enabled in your PHP configuration. '.
+ 'In production mode, "%s" should be disabled. '.
+ 'This will improve performance slightly.',
+ 'apc.stat',
+ 'apc.stat');
$this
->newIssue('extension.apc.stat-enabled')
- ->setShortName(pht('"apc.stat" Enabled'))
- ->setName(pht('"apc.stat" Enabled in Production'))
+ ->setShortName(pht('"%s" Enabled', 'apc.stat'))
+ ->setName(pht('"%s" Enabled in Production', 'apc.stat'))
->setSummary($summary)
->setMessage($message)
->addPHPConfig('apc.stat')
->addPhabricatorConfig('phabricator.developer-mode');
} else if (!$is_stat_enabled && $is_dev) {
$summary = pht(
- '"apc.stat" is currently disabled, but should probably be enabled.');
+ '"%s" is currently disabled, but should probably be enabled.',
+ 'apc.stat');
$message = pht(
- 'The "apc.stat" setting is currently disabled in your PHP '.
- 'configuration, but Phabricator is running in development mode. '.
- 'This option should normally be enabled in development so you do '.
- 'not need to restart your webserver after making changes to the '.
- 'code.');
+ 'The "%s" setting is currently disabled in your PHP configuration, '.
+ 'but Phabricator is running in development mode. This option should '.
+ 'normally be enabled in development so you do not need to restart '.
+ 'your webserver after making changes to the code.',
+ 'apc.stat');
$this
->newIssue('extension.apc.stat-disabled')
- ->setShortName(pht('"apc.stat" Disabled'))
- ->setName(pht('"apc.stat" Disabled in Development'))
+ ->setShortName(pht('"%s" Disabled', 'apc.stat'))
+ ->setName(pht('"%s" Disabled in Development', 'apc.stat'))
->setSummary($summary)
->setMessage($message)
->addPHPConfig('apc.stat')
->addPhabricatorConfig('phabricator.developer-mode');
}
} else {
$this->setIsEnabled(false);
$this->raiseEnableAPCIssue();
}
}
private function initOpcacheSpec() {
$this
->setName(pht('Zend OPcache'))
->setVersion(phpversion('Zend OPcache'));
if (ini_get('opcache.enable')) {
$this->setIsEnabled(true);
$status = opcache_get_status();
$memory = $status['memory_usage'];
$mem_used = $memory['used_memory'];
$mem_free = $memory['free_memory'];
$mem_junk = $memory['wasted_memory'];
$this->setUsedMemory($mem_used + $mem_junk);
$this->setTotalMemory($mem_used + $mem_junk + $mem_free);
$this->setEntryCount($status['opcache_statistics']['num_cached_keys']);
$is_dev = PhabricatorEnv::getEnvConfig('phabricator.developer-mode');
$validate = ini_get('opcache.validate_timestamps');
$freq = ini_get('opcache.revalidate_freq');
if ($is_dev && (!$validate || $freq)) {
$summary = pht(
'OPcache is not configured properly for development.');
$message = pht(
'In development, OPcache should be configured to always reload '.
'code so the webserver does not need to be restarted after making '.
- 'changes. To do this, enable "opcache.validate_timestamps" and '.
- 'set "opcache.revalidate_freq" to 0.');
+ 'changes. To do this, enable "%s" and set "%s" to 0.',
+ 'opcache.validate_timestamps',
+ 'opcache.revalidate_freq');
$this
->newIssue('extension.opcache.devmode')
->setShortName(pht('OPcache Config'))
->setName(pht('OPCache Not Configured for Development'))
->setSummary($summary)
->setMessage($message)
->addPHPConfig('opcache.validate_timestamps')
->addPHPConfig('opcache.revalidate_freq')
->addPhabricatorConfig('phabricator.developer-mode');
} else if (!$is_dev && $validate) {
- $summary = pht(
- 'OPcache is not configured ideally for production.');
+ $summary = pht('OPcache is not configured ideally for production.');
$message = pht(
'In production, OPcache should be configured to never '.
'revalidate code. This will slightly improve performance. '.
- 'To do this, disable "opcache.validate_timestamps" in your PHP '.
- 'configuration.');
+ 'To do this, disable "%s" in your PHP configuration.',
+ 'opcache.validate_timestamps');
$this
->newIssue('extension.opcache.production')
->setShortName(pht('OPcache Config'))
->setName(pht('OPcache Not Configured for Production'))
->setSummary($summary)
->setMessage($message)
->addPHPConfig('opcache.validate_timestamps')
->addPhabricatorConfig('phabricator.developer-mode');
}
} else {
$this->setIsEnabled(false);
$summary = pht('Enabling OPcache will dramatically improve performance.');
$message = pht(
'The PHP "Zend OPcache" extension is installed, but not enabled in '.
'your PHP configuration. Enabling it will dramatically improve '.
- 'Phabricator performance. Edit the "opcache.enable" setting to '.
- 'enable the extension.');
+ 'Phabricator performance. Edit the "%s" setting to '.
+ 'enable the extension.',
+ 'opcache.enable');
$this->newIssue('extension.opcache.enable')
->setShortName(pht('OPcache Disabled'))
->setName(pht('Zend OPcache Not Enabled'))
->setSummary($summary)
->setMessage($message)
->addPHPConfig('opcache.enable');
}
}
private function initNoneSpec() {
if (version_compare(phpversion(), '5.5', '>=')) {
$message = pht(
'Installing the "Zend OPcache" extension will dramatically improve '.
'performance.');
$this
->newIssue('extension.opcache')
->setShortName(pht('OPcache'))
->setName(pht('Zend OPcache Not Installed'))
->setMessage($message)
->addPHPExtension('Zend OPcache');
} else {
$this->raiseInstallAPCIssue();
}
}
}
diff --git a/src/applications/calendar/mail/PhabricatorCalendarReplyHandler.php b/src/applications/calendar/mail/PhabricatorCalendarReplyHandler.php
index 1d60e1294..23639f1b8 100644
--- a/src/applications/calendar/mail/PhabricatorCalendarReplyHandler.php
+++ b/src/applications/calendar/mail/PhabricatorCalendarReplyHandler.php
@@ -1,15 +1,18 @@
<?php
final class PhabricatorCalendarReplyHandler
extends PhabricatorApplicationTransactionReplyHandler {
public function validateMailReceiver($mail_receiver) {
if (!($mail_receiver instanceof PhabricatorCalendarEvent)) {
- throw new Exception('Mail receiver is not a PhabricatorCalendarEvent!');
+ throw new Exception(
+ pht(
+ 'Mail receiver is not a %s!',
+ 'PhabricatorCalendarEvent'));
}
}
public function getObjectPrefix() {
return 'E';
}
}
diff --git a/src/applications/calendar/storage/__tests__/PhabricatorCalendarHolidayTestCase.php b/src/applications/calendar/storage/__tests__/PhabricatorCalendarHolidayTestCase.php
index 40ec7f0aa..3e92a1958 100644
--- a/src/applications/calendar/storage/__tests__/PhabricatorCalendarHolidayTestCase.php
+++ b/src/applications/calendar/storage/__tests__/PhabricatorCalendarHolidayTestCase.php
@@ -1,39 +1,39 @@
<?php
final class PhabricatorCalendarHolidayTestCase extends PhabricatorTestCase {
protected function getPhabricatorTestCaseConfiguration() {
return array(
self::PHABRICATOR_TESTCONFIG_BUILD_STORAGE_FIXTURES => true,
);
}
protected function willRunTests() {
parent::willRunTests();
id(new PhabricatorCalendarHoliday())
->setDay('2012-01-02')
- ->setName('International Testing Day')
+ ->setName(pht('International Testing Day'))
->save();
}
public function testNthBusinessDay() {
$map = array(
array('2011-12-30', 1, '2012-01-03'),
array('2012-01-01', 1, '2012-01-03'),
array('2012-01-01', 0, '2012-01-01'),
array('2012-01-01', -1, '2011-12-30'),
array('2012-01-04', -1, '2012-01-03'),
);
foreach ($map as $val) {
list($date, $n, $expect) = $val;
$actual = PhabricatorCalendarHoliday::getNthBusinessDay(
strtotime($date),
$n);
$this->assertEqual(
$expect,
date('Y-m-d', $actual),
- "{$n} business days since '{$date}'");
+ pht("%d business days since '%s'", $n, $date));
}
}
}
diff --git a/src/applications/calendar/view/AphrontCalendarEventView.php b/src/applications/calendar/view/AphrontCalendarEventView.php
index eb80a3ce4..fadf5eb88 100644
--- a/src/applications/calendar/view/AphrontCalendarEventView.php
+++ b/src/applications/calendar/view/AphrontCalendarEventView.php
@@ -1,113 +1,113 @@
<?php
final class AphrontCalendarEventView extends AphrontView {
private $userPHID;
private $name;
private $epochStart;
private $epochEnd;
private $description;
private $eventID;
private $viewerIsInvited;
private $uri;
private $isAllDay;
private $icon;
public function setURI($uri) {
$this->uri = $uri;
return $this;
}
public function getURI() {
return $this->uri;
}
public function setEventID($event_id) {
$this->eventID = $event_id;
return $this;
}
public function getEventID() {
return $this->eventID;
}
public function setViewerIsInvited($viewer_is_invited) {
$this->viewerIsInvited = $viewer_is_invited;
return $this;
}
public function getViewerIsInvited() {
return $this->viewerIsInvited;
}
public function setUserPHID($user_phid) {
$this->userPHID = $user_phid;
return $this;
}
public function getUserPHID() {
return $this->userPHID;
}
public function setName($name) {
$this->name = $name;
return $this;
}
public function setEpochRange($start, $end) {
$this->epochStart = $start;
$this->epochEnd = $end;
return $this;
}
public function getEpochStart() {
return $this->epochStart;
}
public function getEpochEnd() {
return $this->epochEnd;
}
public function getName() {
return $this->name;
}
public function setDescription($description) {
$this->description = $description;
return $this;
}
public function getDescription() {
return $this->description;
}
public function setIsAllDay($is_all_day) {
$this->isAllDay = $is_all_day;
return $this;
}
public function getIsAllDay() {
return $this->isAllDay;
}
public function setIcon($icon) {
$this->icon = $icon;
return $this;
}
public function getIcon() {
return $this->icon;
}
public function getMultiDay() {
$nextday = strtotime('12:00 AM Tomorrow', $this->getEpochStart());
if ($this->getEpochEnd() > $nextday) {
return true;
}
return false;
}
public function render() {
- throw new Exception('Events are only rendered indirectly.');
+ throw new Exception(pht('Events are only rendered indirectly.'));
}
}
diff --git a/src/applications/celerity/CelerityResourceMap.php b/src/applications/celerity/CelerityResourceMap.php
index 5b0c374da..7e5e00594 100644
--- a/src/applications/celerity/CelerityResourceMap.php
+++ b/src/applications/celerity/CelerityResourceMap.php
@@ -1,259 +1,260 @@
<?php
/**
* Interface to the static resource map, which is a graph of available
* resources, resource dependencies, and packaging information. You generally do
* not need to invoke it directly; instead, you call higher-level Celerity APIs
* and it uses the resource map to satisfy your requests.
*/
final class CelerityResourceMap {
private static $instances = array();
private $resources;
private $symbolMap;
private $requiresMap;
private $packageMap;
private $nameMap;
private $hashMap;
public function __construct(CelerityResources $resources) {
$this->resources = $resources;
$map = $resources->loadMap();
$this->symbolMap = idx($map, 'symbols', array());
$this->requiresMap = idx($map, 'requires', array());
$this->packageMap = idx($map, 'packages', array());
$this->nameMap = idx($map, 'names', array());
// We derive these reverse maps at runtime.
$this->hashMap = array_flip($this->nameMap);
$this->componentMap = array();
foreach ($this->packageMap as $package_name => $symbols) {
foreach ($symbols as $symbol) {
$this->componentMap[$symbol] = $package_name;
}
}
}
public static function getNamedInstance($name) {
if (empty(self::$instances[$name])) {
$resources_list = CelerityPhysicalResources::getAll();
if (empty($resources_list[$name])) {
throw new Exception(
pht(
- 'No resource source exists with name "%s"!', $name));
+ 'No resource source exists with name "%s"!',
+ $name));
}
$instance = new CelerityResourceMap($resources_list[$name]);
self::$instances[$name] = $instance;
}
return self::$instances[$name];
}
public function getNameMap() {
return $this->nameMap;
}
public function getSymbolMap() {
return $this->symbolMap;
}
public function getRequiresMap() {
return $this->requiresMap;
}
public function getPackageMap() {
return $this->packageMap;
}
public function getPackagedNamesForSymbols(array $symbols) {
$resolved = $this->resolveResources($symbols);
return $this->packageResources($resolved);
}
private function resolveResources(array $symbols) {
$map = array();
foreach ($symbols as $symbol) {
if (!empty($map[$symbol])) {
continue;
}
$this->resolveResource($map, $symbol);
}
return $map;
}
private function resolveResource(array &$map, $symbol) {
if (empty($this->symbolMap[$symbol])) {
throw new Exception(
pht(
'Attempting to resolve unknown resource, "%s".',
$symbol));
}
$hash = $this->symbolMap[$symbol];
$map[$symbol] = $hash;
if (isset($this->requiresMap[$hash])) {
$requires = $this->requiresMap[$hash];
} else {
$requires = array();
}
foreach ($requires as $required_symbol) {
if (!empty($map[$required_symbol])) {
continue;
}
$this->resolveResource($map, $required_symbol);
}
}
private function packageResources(array $resolved_map) {
$packaged = array();
$handled = array();
foreach ($resolved_map as $symbol => $hash) {
if (isset($handled[$symbol])) {
continue;
}
if (empty($this->componentMap[$symbol])) {
$packaged[] = $this->hashMap[$hash];
} else {
$package_name = $this->componentMap[$symbol];
$packaged[] = $package_name;
$package_symbols = $this->packageMap[$package_name];
foreach ($package_symbols as $package_symbol) {
$handled[$package_symbol] = true;
}
}
}
return $packaged;
}
public function getResourceDataForName($resource_name) {
return $this->resources->getResourceData($resource_name);
}
public function getResourceNamesForPackageName($package_name) {
$package_symbols = idx($this->packageMap, $package_name);
if (!$package_symbols) {
return null;
}
$resource_names = array();
foreach ($package_symbols as $symbol) {
$resource_names[] = $this->hashMap[$this->symbolMap[$symbol]];
}
return $resource_names;
}
/**
* Get the epoch timestamp of the last modification time of a symbol.
*
* @param string Resource symbol to lookup.
* @return int Epoch timestamp of last resource modification.
*/
public function getModifiedTimeForName($name) {
if ($this->isPackageResource($name)) {
$names = array();
foreach ($this->packageMap[$name] as $symbol) {
$names[] = $this->getResourceNameForSymbol($symbol);
}
} else {
$names = array($name);
}
$mtime = 0;
foreach ($names as $name) {
$mtime = max($mtime, $this->resources->getResourceModifiedTime($name));
}
return $mtime;
}
/**
* Return the absolute URI for the resource associated with a symbol. This
* method is fairly low-level and ignores packaging.
*
* @param string Resource symbol to lookup.
* @return string|null Resource URI, or null if the symbol is unknown.
*/
public function getURIForSymbol($symbol) {
$hash = idx($this->symbolMap, $symbol);
return $this->getURIForHash($hash);
}
/**
* Return the absolute URI for the resource associated with a resource name.
* This method is fairly low-level and ignores packaging.
*
* @param string Resource name to lookup.
* @return string|null Resource URI, or null if the name is unknown.
*/
public function getURIForName($name) {
$hash = idx($this->nameMap, $name);
return $this->getURIForHash($hash);
}
/**
* Return the absolute URI for a resource, identified by hash.
* This method is fairly low-level and ignores packaging.
*
* @param string Resource hash to lookup.
* @return string|null Resource URI, or null if the hash is unknown.
*/
private function getURIForHash($hash) {
if ($hash === null) {
return null;
}
return $this->resources->getResourceURI($hash, $this->hashMap[$hash]);
}
/**
* Return the resource symbols required by a named resource.
*
* @param string Resource name to lookup.
* @return list<string>|null List of required symbols, or null if the name
* is unknown.
*/
public function getRequiredSymbolsForName($name) {
$hash = idx($this->nameMap, $name);
if ($hash === null) {
return null;
}
return idx($this->requiresMap, $hash, array());
}
/**
* Return the resource name for a given symbol.
*
* @param string Resource symbol to lookup.
* @return string|null Resource name, or null if the symbol is unknown.
*/
public function getResourceNameForSymbol($symbol) {
$hash = idx($this->symbolMap, $symbol);
return idx($this->hashMap, $hash);
}
public function isPackageResource($name) {
return isset($this->packageMap[$name]);
}
public function getResourceTypeForName($name) {
return $this->resources->getResourceType($name);
}
}
diff --git a/src/applications/celerity/CelerityResourceMapGenerator.php b/src/applications/celerity/CelerityResourceMapGenerator.php
index 36092e497..b9c4ddaa3 100644
--- a/src/applications/celerity/CelerityResourceMapGenerator.php
+++ b/src/applications/celerity/CelerityResourceMapGenerator.php
@@ -1,361 +1,367 @@
<?php
final class CelerityResourceMapGenerator {
private $debug = false;
private $resources;
private $nameMap = array();
private $symbolMap = array();
private $requiresMap = array();
private $packageMap = array();
public function __construct(CelerityPhysicalResources $resources) {
$this->resources = $resources;
}
public function getNameMap() {
return $this->nameMap;
}
public function getSymbolMap() {
return $this->symbolMap;
}
public function getRequiresMap() {
return $this->requiresMap;
}
public function getPackageMap() {
return $this->packageMap;
}
public function setDebug($debug) {
$this->debug = $debug;
return $this;
}
protected function log($message) {
if ($this->debug) {
$console = PhutilConsole::getConsole();
$console->writeErr("%s\n", $message);
}
}
public function generate() {
$binary_map = $this->rebuildBinaryResources($this->resources);
$this->log(pht('Found %d binary resources.', count($binary_map)));
$xformer = id(new CelerityResourceTransformer())
->setMinify(false)
->setRawURIMap(ipull($binary_map, 'uri'));
$text_map = $this->rebuildTextResources($this->resources, $xformer);
$this->log(pht('Found %d text resources.', count($text_map)));
$resource_graph = array();
$requires_map = array();
$symbol_map = array();
foreach ($text_map as $name => $info) {
if (isset($info['provides'])) {
$symbol_map[$info['provides']] = $info['hash'];
// We only need to check for cycles and add this to the requires map
// if it actually requires anything.
if (!empty($info['requires'])) {
$resource_graph[$info['provides']] = $info['requires'];
$requires_map[$info['hash']] = $info['requires'];
}
}
}
$this->detectGraphCycles($resource_graph);
$name_map = ipull($binary_map, 'hash') + ipull($text_map, 'hash');
$hash_map = array_flip($name_map);
$package_map = $this->rebuildPackages(
$this->resources,
$symbol_map,
$hash_map);
$this->log(pht('Found %d packages.', count($package_map)));
$component_map = array();
foreach ($package_map as $package_name => $package_info) {
foreach ($package_info['symbols'] as $symbol) {
$component_map[$symbol] = $package_name;
}
}
$name_map = $this->mergeNameMaps(
array(
array(pht('Binary'), ipull($binary_map, 'hash')),
array(pht('Text'), ipull($text_map, 'hash')),
array(pht('Package'), ipull($package_map, 'hash')),
));
$package_map = ipull($package_map, 'symbols');
ksort($name_map, SORT_STRING);
ksort($symbol_map, SORT_STRING);
ksort($requires_map, SORT_STRING);
ksort($package_map, SORT_STRING);
$this->nameMap = $name_map;
$this->symbolMap = $symbol_map;
$this->requiresMap = $requires_map;
$this->packageMap = $package_map;
return $this;
}
public function write() {
$map_content = $this->formatMapContent(array(
'names' => $this->getNameMap(),
'symbols' => $this->getSymbolMap(),
'requires' => $this->getRequiresMap(),
'packages' => $this->getPackageMap(),
));
$map_path = $this->resources->getPathToMap();
$this->log(pht('Writing map "%s".', Filesystem::readablePath($map_path)));
Filesystem::writeFile($map_path, $map_content);
return $this;
}
private function formatMapContent(array $data) {
$content = phutil_var_export($data);
$generated = '@'.'generated';
return <<<EOFILE
<?php
/**
* This file is automatically generated. Use 'bin/celerity map' to rebuild it.
*
* {$generated}
*/
return {$content};
EOFILE;
}
/**
* Find binary resources (like PNG and SWF) and return information about
* them.
*
* @param CelerityPhysicalResources Resource map to find binary resources for.
* @return map<string, map<string, string>> Resource information map.
*/
private function rebuildBinaryResources(
CelerityPhysicalResources $resources) {
$binary_map = $resources->findBinaryResources();
$result_map = array();
foreach ($binary_map as $name => $data_hash) {
$hash = $resources->getCelerityHash($data_hash.$name);
$result_map[$name] = array(
'hash' => $hash,
'uri' => $resources->getResourceURI($hash, $name),
);
}
return $result_map;
}
/**
* Find text resources (like JS and CSS) and return information about them.
*
* @param CelerityPhysicalResources Resource map to find text resources for.
* @param CelerityResourceTransformer Configured resource transformer.
* @return map<string, map<string, string>> Resource information map.
*/
private function rebuildTextResources(
CelerityPhysicalResources $resources,
CelerityResourceTransformer $xformer) {
$text_map = $resources->findTextResources();
$result_map = array();
foreach ($text_map as $name => $data_hash) {
$raw_data = $resources->getResourceData($name);
$xformed_data = $xformer->transformResource($name, $raw_data);
$data_hash = $resources->getCelerityHash($xformed_data);
$hash = $resources->getCelerityHash($data_hash.$name);
list($provides, $requires) = $this->getProvidesAndRequires(
$name,
$raw_data);
$result_map[$name] = array(
'hash' => $hash,
);
if ($provides !== null) {
$result_map[$name] += array(
'provides' => $provides,
'requires' => $requires,
);
}
}
return $result_map;
}
/**
* Parse the `@provides` and `@requires` symbols out of a text resource, like
* JS or CSS.
*
* @param string Resource name.
* @param string Resource data.
* @return pair<string|null, list<string>|null> The `@provides` symbol and
* the list of `@requires` symbols. If the resource is not part of the
* dependency graph, both are null.
*/
private function getProvidesAndRequires($name, $data) {
$parser = new PhutilDocblockParser();
$matches = array();
$ok = preg_match('@/[*][*].*?[*]/@s', $data, $matches);
if (!$ok) {
throw new Exception(
pht(
'Resource "%s" does not have a header doc comment. Encode '.
'dependency data in a header docblock.',
$name));
}
list($description, $metadata) = $parser->parse($matches[0]);
$provides = preg_split('/\s+/', trim(idx($metadata, 'provides')));
$requires = preg_split('/\s+/', trim(idx($metadata, 'requires')));
$provides = array_filter($provides);
$requires = array_filter($requires);
if (!$provides) {
// Tests and documentation-only JS is permitted to @provide no targets.
return array(null, null);
}
if (count($provides) > 1) {
throw new Exception(
- pht('Resource "%s" must @provide at most one Celerity target.', $name));
+ pht(
+ 'Resource "%s" must %s at most one Celerity target.',
+ $name,
+ '@provide'));
}
return array(head($provides), $requires);
}
/**
* Check for dependency cycles in the resource graph. Raises an exception if
* a cycle is detected.
*
* @param map<string, list<string>> Map of `@provides` symbols to their
* `@requires` symbols.
* @return void
*/
private function detectGraphCycles(array $nodes) {
$graph = id(new CelerityResourceGraph())
->addNodes($nodes)
->setResourceGraph($nodes)
->loadGraph();
foreach ($nodes as $provides => $requires) {
$cycle = $graph->detectCycles($provides);
if ($cycle) {
throw new Exception(
- pht('Cycle detected in resource graph: %s', implode(' > ', $cycle)));
+ pht(
+ 'Cycle detected in resource graph: %s',
+ implode(' > ', $cycle)));
}
}
}
/**
* Build package specifications for a given resource source.
*
* @param CelerityPhysicalResources Resource source to rebuild.
* @param map<string, string> Map of `@provides` to hashes.
* @param map<string, string> Map of hashes to resource names.
* @return map<string, map<string, string>> Package information maps.
*/
private function rebuildPackages(
CelerityPhysicalResources $resources,
array $symbol_map,
array $reverse_map) {
$package_map = array();
$package_spec = $resources->getResourcePackages();
foreach ($package_spec as $package_name => $package_symbols) {
$type = null;
$hashes = array();
foreach ($package_symbols as $symbol) {
$symbol_hash = idx($symbol_map, $symbol);
if ($symbol_hash === null) {
throw new Exception(
pht(
'Package specification for "%s" includes "%s", but that symbol '.
- 'is not @provided by any resource.',
+ 'is not %s by any resource.',
$package_name,
- $symbol));
+ $symbol,
+ '@provided'));
}
$resource_name = $reverse_map[$symbol_hash];
$resource_type = $resources->getResourceType($resource_name);
if ($type === null) {
$type = $resource_type;
} else if ($type !== $resource_type) {
throw new Exception(
pht(
'Package specification for "%s" includes resources of multiple '.
'types (%s, %s). Each package may only contain one type of '.
'resource.',
$package_name,
$type,
$resource_type));
}
$hashes[] = $symbol.':'.$symbol_hash;
}
$hash = $resources->getCelerityHash(implode("\n", $hashes));
$package_map[$package_name] = array(
'hash' => $hash,
'symbols' => $package_symbols,
);
}
return $package_map;
}
private function mergeNameMaps(array $maps) {
$result = array();
$origin = array();
foreach ($maps as $map) {
list($map_name, $data) = $map;
foreach ($data as $name => $hash) {
if (empty($result[$name])) {
$result[$name] = $hash;
$origin[$name] = $map_name;
} else {
$old = $origin[$name];
$new = $map_name;
throw new Exception(
pht(
'Resource source defines two resources with the same name, '.
'"%s". One is defined in the "%s" map; the other in the "%s" '.
'map. Each resource must have a unique name.',
$name,
$old,
$new));
}
}
}
return $result;
}
}
diff --git a/src/applications/celerity/CelerityResourceTransformer.php b/src/applications/celerity/CelerityResourceTransformer.php
index 72880c8ce..ebfae6a8d 100644
--- a/src/applications/celerity/CelerityResourceTransformer.php
+++ b/src/applications/celerity/CelerityResourceTransformer.php
@@ -1,395 +1,398 @@
<?php
final class CelerityResourceTransformer {
private $minify;
private $rawURIMap;
private $celerityMap;
private $translateURICallback;
private $currentPath;
public function setTranslateURICallback($translate_uricallback) {
$this->translateURICallback = $translate_uricallback;
return $this;
}
public function setMinify($minify) {
$this->minify = $minify;
return $this;
}
public function setCelerityMap(CelerityResourceMap $celerity_map) {
$this->celerityMap = $celerity_map;
return $this;
}
public function setRawURIMap(array $raw_urimap) {
$this->rawURIMap = $raw_urimap;
return $this;
}
public function getRawURIMap() {
return $this->rawURIMap;
}
/**
* @phutil-external-symbol function jsShrink
*/
public function transformResource($path, $data) {
$type = self::getResourceType($path);
switch ($type) {
case 'css':
$data = $this->replaceCSSPrintRules($path, $data);
$data = $this->replaceCSSVariables($path, $data);
$data = preg_replace_callback(
'@url\s*\((\s*[\'"]?.*?)\)@s',
nonempty(
$this->translateURICallback,
array($this, 'translateResourceURI')),
$data);
break;
}
if (!$this->minify) {
return $data;
}
// Some resources won't survive minification (like Raphael.js), and are
// marked so as not to be minified.
if (strpos($data, '@'.'do-not-minify') !== false) {
return $data;
}
switch ($type) {
case 'css':
// Remove comments.
$data = preg_replace('@/\*.*?\*/@s', '', $data);
// Remove whitespace around symbols.
$data = preg_replace('@\s*([{}:;,])\s*@', '\1', $data);
// Remove unnecessary semicolons.
$data = preg_replace('@;}@', '}', $data);
// Replace #rrggbb with #rgb when possible.
$data = preg_replace(
'@#([a-f0-9])\1([a-f0-9])\2([a-f0-9])\3@i',
'#\1\2\3',
$data);
$data = trim($data);
break;
case 'js':
// If `jsxmin` is available, use it. jsxmin is the Javelin minifier and
// produces the smallest output, but is complicated to build.
if (Filesystem::binaryExists('jsxmin')) {
$future = new ExecFuture('jsxmin __DEV__:0');
$future->write($data);
list($err, $result) = $future->resolve();
if (!$err) {
$data = $result;
break;
}
}
// If `jsxmin` is not available, use `JsShrink`, which doesn't compress
// quite as well but is always available.
$root = dirname(phutil_get_library_root('phabricator'));
require_once $root.'/externals/JsShrink/jsShrink.php';
$data = jsShrink($data);
break;
}
return $data;
}
public static function getResourceType($path) {
return last(explode('.', $path));
}
public function translateResourceURI(array $matches) {
$uri = trim($matches[1], "'\" \r\t\n");
$tail = '';
// If the resource URI has a query string or anchor, strip it off before
// we go looking for the resource. We'll stitch it back on later. This
// primarily affects FontAwesome.
$parts = preg_split('/(?=[?#])/', $uri, 2);
if (count($parts) == 2) {
$uri = $parts[0];
$tail = $parts[1];
}
$alternatives = array_unique(
array(
$uri,
ltrim($uri, '/'),
));
foreach ($alternatives as $alternative) {
if ($this->rawURIMap !== null) {
if (isset($this->rawURIMap[$alternative])) {
$uri = $this->rawURIMap[$alternative];
break;
}
}
if ($this->celerityMap) {
$resource_uri = $this->celerityMap->getURIForName($alternative);
if ($resource_uri) {
// Check if we can use a data URI for this resource. If not, just
// use a normal Celerity URI.
$data_uri = $this->generateDataURI($alternative);
if ($data_uri) {
$uri = $data_uri;
} else {
$uri = $resource_uri;
}
break;
}
}
}
return 'url('.$uri.$tail.')';
}
private function replaceCSSVariables($path, $data) {
$this->currentPath = $path;
return preg_replace_callback(
'/{\$([^}]+)}/',
array($this, 'replaceCSSVariable'),
$data);
}
private function replaceCSSPrintRules($path, $data) {
$this->currentPath = $path;
return preg_replace_callback(
'/!print\s+(.+?{.+?})/s',
array($this, 'replaceCSSPrintRule'),
$data);
}
public static function getCSSVariableMap() {
return array(
// Fonts
'basefont' => "13px/1.231 'Segoe UI', 'Segoe UI Web Regular', ".
"'Segoe UI Symbol', 'Helvetica Neue', Helvetica, Arial, sans-serif",
// Drop Shadow
'dropshadow' => '0 1px 6px rgba(0, 0, 0, .25)',
// Base Colors
'red' => '#c0392b',
'lightred' => '#f4dddb',
'orange' => '#e67e22',
'lightorange' => '#f7e2d4',
'yellow' => '#f1c40f',
'lightyellow' => '#fdf5d4',
'green' => '#139543',
'lightgreen' => '#d7eddf',
'blue' => '#2980b9',
'lightblue' => '#daeaf3',
'sky' => '#3498db',
'lightsky' => '#ddeef9',
'indigo' => '#6e5cb6',
'lightindigo' => '#eae6f7',
'pink' => '#da49be',
'lightpink' => '#fbeaf8',
'violet' => '#8e44ad',
'lightviolet' => '#ecdff1',
'charcoal' => '#4b4d51',
'backdrop' => '#dadee7',
'hovergrey' => '#c5cbcf',
'hoverblue' => '#eceff5',
'hoverborder' => '#dfe1e9',
'hoverselectedgrey' => '#bbc4ca',
'hoverselectedblue' => '#e6e9ee',
// Base Greys
'lightgreyborder' => '#C7CCD9',
'greyborder' => '#A1A6B0',
'darkgreyborder' => '#676A70',
'lightgreytext' => '#92969D',
'greytext' => '#74777D',
'darkgreytext' => '#4B4D51',
'lightgreybackground' => '#F7F7F7',
'greybackground' => '#EBECEE',
'darkgreybackground' => '#DFE0E2',
// Base Blues
'thinblueborder' => '#DDE8EF',
'lightblueborder' => '#BFCFDA',
'blueborder' => '#8C98B8',
'darkblueborder' => '#626E82',
'lightbluebackground' => '#F8F9FC',
'bluebackground' => '#DAE7FF',
'lightbluetext' => '#8C98B8',
'bluetext' => '#6B748C',
'darkbluetext' => '#464C5C',
// Base Greens
'lightgreenborder' => '#bfdac1',
'greenborder' => '#8cb89c',
'greentext' => '#3e6d35',
'lightgreenbackground' => '#e6f2e4',
// Base Red
'lightredborder' => '#f4c6c6',
'redborder' => '#eb9797',
'redtext' => '#802b2b',
'lightredbackground' => '#f5e1e1',
// Base Violet
'lightvioletborder' => '#cfbddb',
'violetborder' => '#b589ba',
'violettext' => '#603c73',
'lightvioletbackground' => '#e9dfee',
// Shades are a more muted set of our base colors
// better suited to blending into other UIs.
// Shade Red
'sh-lightredborder' => '#efcfcf',
'sh-redborder' => '#d1abab',
'sh-redicon' => '#c85a5a',
'sh-redtext' => '#a53737',
'sh-redbackground' => '#f7e6e6',
// Shade Orange
'sh-lightorangeborder' => '#f8dcc3',
'sh-orangeborder' => '#dbb99e',
'sh-orangeicon' => '#e78331',
'sh-orangetext' => '#ba6016',
'sh-orangebackground' => '#fbede1',
// Shade Yellow
'sh-lightyellowborder' => '#e9dbcd',
'sh-yellowborder' => '#c9b8a8',
'sh-yellowicon' => '#9b946e',
'sh-yellowtext' => '#726f56',
'sh-yellowbackground' => '#fdf3da',
// Shade Green
'sh-lightgreenborder' => '#c6e6c7',
'sh-greenborder' => '#a0c4a1',
'sh-greenicon' => '#4ca74e',
'sh-greentext' => '#326d34',
'sh-greenbackground' => '#ddefdd',
// Shade Blue
'sh-lightblueborder' => '#cfdbe3',
'sh-blueborder' => '#a7b5bf',
'sh-blueicon' => '#6b748c',
'sh-bluetext' => '#464c5c',
'sh-bluebackground' => '#dee7f8',
// Shade Indigo
'sh-lightindigoborder' => '#d1c9ee',
'sh-indigoborder' => '#bcb4da',
'sh-indigoicon' => '#8672d4',
'sh-indigotext' => '#6e5cb6',
'sh-indigobackground' => '#eae6f7',
// Shade Violet
'sh-lightvioletborder' => '#e0d1e7',
'sh-violetborder' => '#bcabc5',
'sh-violeticon' => '#9260ad',
'sh-violettext' => '#69427f',
'sh-violetbackground' => '#efe8f3',
// Shade Pink
'sh-lightpinkborder' => '#f6d5ef',
'sh-pinkborder' => '#d5aecd',
'sh-pinkicon' => '#e26fcb',
'sh-pinktext' => '#da49be',
'sh-pinkbackground' => '#fbeaf8',
// Shade Grey
'sh-lightgreyborder' => '#d8d8d8',
'sh-greyborder' => '#b2b2b2',
'sh-greyicon' => '#757575',
'sh-greytext' => '#555555',
'sh-greybackground' => '#e7e7e7',
// Shade Disabled
'sh-lightdisabledborder' => '#e5e5e5',
'sh-disabledborder' => '#cbcbcb',
'sh-disabledicon' => '#bababa',
'sh-disabledtext' => '#a6a6a6',
'sh-disabledbackground' => '#f3f3f3',
);
}
public function replaceCSSVariable($matches) {
static $map;
if (!$map) {
$map = self::getCSSVariableMap();
}
$var_name = $matches[1];
if (empty($map[$var_name])) {
$path = $this->currentPath;
throw new Exception(
- "CSS file '{$path}' has unknown variable '{$var_name}'.");
+ pht(
+ "CSS file '%s' has unknown variable '%s'.",
+ $path,
+ $var_name));
}
return $map[$var_name];
}
public function replaceCSSPrintRule($matches) {
$rule = $matches[1];
$rules = array();
$rules[] = '.printable '.$rule;
$rules[] = "@media print {\n ".str_replace("\n", "\n ", $rule)."\n}\n";
return implode("\n\n", $rules);
}
/**
* Attempt to generate a data URI for a resource. We'll generate a data URI
* if the resource is a valid resource of an appropriate type, and is
* small enough. Otherwise, this method will return `null` and we'll end up
* using a normal URI instead.
*
* @param string Resource name to attempt to generate a data URI for.
* @return string|null Data URI, or null if we declined to generate one.
*/
private function generateDataURI($resource_name) {
$ext = last(explode('.', $resource_name));
switch ($ext) {
case 'png':
$type = 'image/png';
break;
case 'gif':
$type = 'image/gif';
break;
case 'jpg':
$type = 'image/jpeg';
break;
default:
return null;
}
// In IE8, 32KB is the maximum supported URI length.
$maximum_data_size = (1024 * 32);
$data = $this->celerityMap->getResourceDataForName($resource_name);
if (strlen($data) >= $maximum_data_size) {
// If the data is already too large on its own, just bail before
// encoding it.
return null;
}
$uri = 'data:'.$type.';base64,'.base64_encode($data);
if (strlen($uri) >= $maximum_data_size) {
return null;
}
return $uri;
}
}
diff --git a/src/applications/celerity/CeleritySpriteGenerator.php b/src/applications/celerity/CeleritySpriteGenerator.php
index a42fbfa4d..e25e1050a 100644
--- a/src/applications/celerity/CeleritySpriteGenerator.php
+++ b/src/applications/celerity/CeleritySpriteGenerator.php
@@ -1,292 +1,295 @@
<?php
final class CeleritySpriteGenerator {
public function buildMenuSheet() {
$sprites = array();
$sources = array(
'logo' => array(
'x' => 96,
'y' => 40,
'css' => '.phabricator-main-menu-logo',
),
'eye' => array(
'x' => 40,
'y' => 40,
'css' => '.phabricator-main-menu-eye',
),
);
$scales = array(
'1x' => 1,
'2x' => 2,
);
$template = new PhutilSprite();
foreach ($sources as $name => $spec) {
$sprite = id(clone $template)
->setName($name)
->setSourceSize($spec['x'], $spec['y'])
->setTargetCSS($spec['css']);
foreach ($scales as $scale_name => $scale) {
$path = 'menu_'.$scale_name.'/'.$name.'.png';
$path = $this->getPath($path);
$sprite->setSourceFile($path, $scale);
}
$sprites[] = $sprite;
}
$sheet = $this->buildSheet('menu', true);
$sheet->setScales($scales);
foreach ($sprites as $sprite) {
$sheet->addSprite($sprite);
}
return $sheet;
}
public function buildTokenSheet() {
$icons = $this->getDirectoryList('tokens_1x');
$scales = array(
'1x' => 1,
'2x' => 2,
);
$template = id(new PhutilSprite())
->setSourceSize(16, 16);
$sprites = array();
$prefix = 'tokens_';
foreach ($icons as $icon) {
$sprite = id(clone $template)
->setName('tokens-'.$icon)
->setTargetCSS('.tokens-'.$icon);
foreach ($scales as $scale_key => $scale) {
$path = $this->getPath($prefix.$scale_key.'/'.$icon.'.png');
$sprite->setSourceFile($path, $scale);
}
$sprites[] = $sprite;
}
$sheet = $this->buildSheet('tokens', true);
$sheet->setScales($scales);
foreach ($sprites as $sprite) {
$sheet->addSprite($sprite);
}
return $sheet;
}
public function buildProjectsSheet() {
$icons = $this->getDirectoryList('projects_1x');
$scales = array(
'1x' => 1,
'2x' => 2,
);
$template = id(new PhutilSprite())
->setSourceSize(50, 50);
$sprites = array();
$prefix = 'projects-';
foreach ($icons as $icon) {
$sprite = id(clone $template)
->setName($prefix.$icon)
->setTargetCSS('.'.$prefix.$icon);
foreach ($scales as $scale_key => $scale) {
$path = $this->getPath('projects_'.$scale_key.'/'.$icon.'.png');
$sprite->setSourceFile($path, $scale);
}
$sprites[] = $sprite;
}
$sheet = $this->buildSheet('projects', true);
$sheet->setScales($scales);
foreach ($sprites as $sprite) {
$sheet->addSprite($sprite);
}
return $sheet;
}
public function buildLoginSheet() {
$icons = $this->getDirectoryList('login_1x');
$scales = array(
'1x' => 1,
'2x' => 2,
);
$template = id(new PhutilSprite())
->setSourceSize(34, 34);
$sprites = array();
$prefix = 'login_';
foreach ($icons as $icon) {
$sprite = id(clone $template)
->setName('login-'.$icon)
->setTargetCSS('.login-'.$icon);
foreach ($scales as $scale_key => $scale) {
$path = $this->getPath($prefix.$scale_key.'/'.$icon.'.png');
$sprite->setSourceFile($path, $scale);
}
$sprites[] = $sprite;
}
$sheet = $this->buildSheet('login', true);
$sheet->setScales($scales);
foreach ($sprites as $sprite) {
$sheet->addSprite($sprite);
}
return $sheet;
}
public function buildGradientSheet() {
$gradients = $this->getDirectoryList('gradients');
$template = new PhutilSprite();
$unusual_heights = array(
'breadcrumbs' => 31,
'grey-header' => 70,
'dark-grey-header' => 70,
'lightblue-header' => 240,
'lightgreen-header' => 240,
'lightviolet-header' => 240,
'lightred-header' => 240,
);
$sprites = array();
foreach ($gradients as $gradient) {
$path = $this->getPath('gradients/'.$gradient.'.png');
$sprite = id(clone $template)
->setName('gradient-'.$gradient)
->setSourceFile($path)
->setTargetCSS('.gradient-'.$gradient);
$sprite->setSourceSize(4, idx($unusual_heights, $gradient, 26));
$sprites[] = $sprite;
}
$sheet = $this->buildSheet(
'gradient',
false,
PhutilSpriteSheet::TYPE_REPEAT_X);
foreach ($sprites as $sprite) {
$sheet->addSprite($sprite);
}
return $sheet;
}
public function buildMainHeaderSheet() {
$gradients = $this->getDirectoryList('main_header');
$template = new PhutilSprite();
$sprites = array();
foreach ($gradients as $gradient) {
$path = $this->getPath('main_header/'.$gradient.'.png');
$sprite = id(clone $template)
->setName('main-header-'.$gradient)
->setSourceFile($path)
->setTargetCSS('.main-header-'.$gradient);
$sprite->setSourceSize(6, 44);
$sprites[] = $sprite;
}
$sheet = $this->buildSheet('main-header',
false,
PhutilSpriteSheet::TYPE_REPEAT_X);
foreach ($sprites as $sprite) {
$sheet->addSprite($sprite);
}
return $sheet;
}
private function getPath($to_path = null) {
$root = dirname(phutil_get_library_root('phabricator'));
return $root.'/resources/sprite/'.$to_path;
}
private function getDirectoryList($dir) {
$path = $this->getPath($dir);
$result = array();
$images = Filesystem::listDirectory($path, $include_hidden = false);
foreach ($images as $image) {
if (!preg_match('/\.png$/', $image)) {
throw new Exception(
- "Expected file '{$image}' in '{$path}' to be a sprite source ".
- "ending in '.png'.");
+ pht(
+ "Expected file '%s' in '%s' to be a sprite source ending in '%s'.",
+ $image,
+ $path,
+ '.png'));
}
$result[] = substr($image, 0, -4);
}
return $result;
}
private function buildSheet(
$name,
$has_retina,
$type = null,
$extra_css = '') {
$sheet = new PhutilSpriteSheet();
$at = '@';
switch ($type) {
case PhutilSpriteSheet::TYPE_STANDARD:
default:
$type = PhutilSpriteSheet::TYPE_STANDARD;
$repeat_rule = 'no-repeat';
break;
case PhutilSpriteSheet::TYPE_REPEAT_X:
$repeat_rule = 'repeat-x';
break;
case PhutilSpriteSheet::TYPE_REPEAT_Y:
$repeat_rule = 'repeat-y';
break;
}
$retina_rules = null;
if ($has_retina) {
$retina_rules = <<<EOCSS
@media
only screen and (min-device-pixel-ratio: 1.5),
only screen and (-webkit-min-device-pixel-ratio: 1.5) {
.sprite-{$name}{$extra_css} {
background-image: url(/rsrc/image/sprite-{$name}-X2.png);
background-size: {X}px {Y}px;
}
}
EOCSS;
}
$sheet->setSheetType($type);
$sheet->setCSSHeader(<<<EOCSS
/**
* @provides sprite-{$name}-css
* {$at}generated
*/
.sprite-{$name}{$extra_css} {
background-image: url(/rsrc/image/sprite-{$name}.png);
background-repeat: {$repeat_rule};
}
{$retina_rules}
EOCSS
);
return $sheet;
}
}
diff --git a/src/applications/celerity/CelerityStaticResourceResponse.php b/src/applications/celerity/CelerityStaticResourceResponse.php
index 7864515e1..cfb586602 100644
--- a/src/applications/celerity/CelerityStaticResourceResponse.php
+++ b/src/applications/celerity/CelerityStaticResourceResponse.php
@@ -1,316 +1,321 @@
<?php
/**
* Tracks and resolves dependencies the page declares with
* @{function:require_celerity_resource}, and then builds appropriate HTML or
* Ajax responses.
*/
final class CelerityStaticResourceResponse {
private $symbols = array();
private $needsResolve = true;
private $resolved;
private $packaged;
private $metadata = array();
private $metadataBlock = 0;
private $behaviors = array();
private $hasRendered = array();
public function __construct() {
if (isset($_REQUEST['__metablock__'])) {
$this->metadataBlock = (int)$_REQUEST['__metablock__'];
}
}
public function addMetadata($metadata) {
$id = count($this->metadata);
$this->metadata[$id] = $metadata;
return $this->metadataBlock.'_'.$id;
}
public function getMetadataBlock() {
return $this->metadataBlock;
}
/**
* Register a behavior for initialization.
*
* NOTE: If `$config` is empty, a behavior will execute only once even if it
* is initialized multiple times. If `$config` is nonempty, the behavior will
* be invoked once for each configuration.
*/
public function initBehavior(
$behavior,
array $config = array(),
$source_name = null) {
$this->requireResource('javelin-behavior-'.$behavior, $source_name);
if (empty($this->behaviors[$behavior])) {
$this->behaviors[$behavior] = array();
}
if ($config) {
$this->behaviors[$behavior][] = $config;
}
return $this;
}
public function requireResource($symbol, $source_name) {
if (isset($this->symbols[$source_name][$symbol])) {
return $this;
}
// Verify that the resource exists.
$map = CelerityResourceMap::getNamedInstance($source_name);
$name = $map->getResourceNameForSymbol($symbol);
if ($name === null) {
throw new Exception(
pht(
'No resource with symbol "%s" exists in source "%s"!',
$symbol,
$source_name));
}
$this->symbols[$source_name][$symbol] = true;
$this->needsResolve = true;
return $this;
}
private function resolveResources() {
if ($this->needsResolve) {
$this->packaged = array();
foreach ($this->symbols as $source_name => $symbols_map) {
$symbols = array_keys($symbols_map);
$map = CelerityResourceMap::getNamedInstance($source_name);
$packaged = $map->getPackagedNamesForSymbols($symbols);
$this->packaged[$source_name] = $packaged;
}
$this->needsResolve = false;
}
return $this;
}
public function renderSingleResource($symbol, $source_name) {
$map = CelerityResourceMap::getNamedInstance($source_name);
$packaged = $map->getPackagedNamesForSymbols(array($symbol));
return $this->renderPackagedResources($map, $packaged);
}
public function renderResourcesOfType($type) {
$this->resolveResources();
$result = array();
foreach ($this->packaged as $source_name => $resource_names) {
$map = CelerityResourceMap::getNamedInstance($source_name);
$resources_of_type = array();
foreach ($resource_names as $resource_name) {
$resource_type = $map->getResourceTypeForName($resource_name);
if ($resource_type == $type) {
$resources_of_type[] = $resource_name;
}
}
$result[] = $this->renderPackagedResources($map, $resources_of_type);
}
return phutil_implode_html('', $result);
}
private function renderPackagedResources(
CelerityResourceMap $map,
array $resources) {
$output = array();
foreach ($resources as $name) {
if (isset($this->hasRendered[$name])) {
continue;
}
$this->hasRendered[$name] = true;
$output[] = $this->renderResource($map, $name);
}
return $output;
}
private function renderResource(
CelerityResourceMap $map,
$name) {
$uri = $this->getURI($map, $name);
$type = $map->getResourceTypeForName($name);
$multimeter = MultimeterControl::getInstance();
if ($multimeter) {
$event_type = MultimeterEvent::TYPE_STATIC_RESOURCE;
$multimeter->newEvent($event_type, 'rsrc.'.$name, 1);
}
switch ($type) {
case 'css':
return phutil_tag(
'link',
array(
'rel' => 'stylesheet',
'type' => 'text/css',
'href' => $uri,
));
case 'js':
return phutil_tag(
'script',
array(
'type' => 'text/javascript',
'src' => $uri,
),
'');
}
throw new Exception(
pht(
'Unable to render resource "%s", which has unknown type "%s".',
$name,
$type));
}
public function renderHTMLFooter() {
$data = array();
if ($this->metadata) {
$json_metadata = AphrontResponse::encodeJSONForHTTPResponse(
$this->metadata);
$this->metadata = array();
} else {
$json_metadata = '{}';
}
// Even if there is no metadata on the page, Javelin uses the mergeData()
// call to start dispatching the event queue.
$data[] = 'JX.Stratcom.mergeData('.$this->metadataBlock.', '.
$json_metadata.');';
$onload = array();
if ($this->behaviors) {
$behaviors = $this->behaviors;
$this->behaviors = array();
$higher_priority_names = array(
'refresh-csrf',
'aphront-basic-tokenizer',
'dark-console',
'history-install',
);
$higher_priority_behaviors = array_select_keys(
$behaviors,
$higher_priority_names);
foreach ($higher_priority_names as $name) {
unset($behaviors[$name]);
}
$behavior_groups = array(
$higher_priority_behaviors,
$behaviors,
);
foreach ($behavior_groups as $group) {
if (!$group) {
continue;
}
$group_json = AphrontResponse::encodeJSONForHTTPResponse(
$group);
$onload[] = 'JX.initBehaviors('.$group_json.')';
}
}
if ($onload) {
foreach ($onload as $func) {
$data[] = 'JX.onload(function(){'.$func.'});';
}
}
if ($data) {
$data = implode("\n", $data);
return self::renderInlineScript($data);
} else {
return '';
}
}
public static function renderInlineScript($data) {
if (stripos($data, '</script>') !== false) {
throw new Exception(
- 'Literal </script> is not allowed inside inline script.');
+ pht(
+ 'Literal %s is not allowed inside inline script.',
+ '</script>'));
}
if (strpos($data, '<!') !== false) {
- throw new Exception('Literal <! is not allowed inside inline script.');
+ throw new Exception(
+ pht(
+ 'Literal %s is not allowed inside inline script.',
+ '<!'));
}
// We don't use <![CDATA[ ]]> because it is ignored by HTML parsers. We
// would need to send the document with XHTML content type.
return phutil_tag(
'script',
array('type' => 'text/javascript'),
phutil_safe_html($data));
}
public function buildAjaxResponse($payload, $error = null) {
$response = array(
'error' => $error,
'payload' => $payload,
);
if ($this->metadata) {
$response['javelin_metadata'] = $this->metadata;
$this->metadata = array();
}
if ($this->behaviors) {
$response['javelin_behaviors'] = $this->behaviors;
$this->behaviors = array();
}
$this->resolveResources();
$resources = array();
foreach ($this->packaged as $source_name => $resource_names) {
$map = CelerityResourceMap::getNamedInstance($source_name);
foreach ($resource_names as $resource_name) {
$resources[] = $this->getURI($map, $resource_name);
}
}
if ($resources) {
$response['javelin_resources'] = $resources;
}
return $response;
}
public function getURI(
CelerityResourceMap $map,
$name,
$use_primary_domain = false) {
$uri = $map->getURIForName($name);
// In developer mode, we dump file modification times into the URI. When a
// page is reloaded in the browser, any resources brought in by Ajax calls
// do not trigger revalidation, so without this it's very difficult to get
// changes to Ajaxed-in CSS to work (you must clear your cache or rerun
// the map script). In production, we can assume the map script gets run
// after changes, and safely skip this.
if (PhabricatorEnv::getEnvConfig('phabricator.developer-mode')) {
$mtime = $map->getModifiedTimeForName($name);
$uri = preg_replace('@^/res/@', '/res/'.$mtime.'T/', $uri);
}
if ($use_primary_domain) {
return PhabricatorEnv::getURI($uri);
} else {
return PhabricatorEnv::getCDNURI($uri);
}
}
}
diff --git a/src/applications/celerity/controller/CelerityResourceController.php b/src/applications/celerity/controller/CelerityResourceController.php
index c91f9297c..717e20f9f 100644
--- a/src/applications/celerity/controller/CelerityResourceController.php
+++ b/src/applications/celerity/controller/CelerityResourceController.php
@@ -1,171 +1,171 @@
<?php
abstract class CelerityResourceController extends PhabricatorController {
protected function buildResourceTransformer() {
return null;
}
public function shouldRequireLogin() {
return false;
}
public function shouldRequireEnabledUser() {
return false;
}
public function shouldAllowPartialSessions() {
return true;
}
public function shouldAllowLegallyNonCompliantUsers() {
return true;
}
abstract public function getCelerityResourceMap();
protected function serveResource($path, $package_hash = null) {
// Sanity checking to keep this from exposing anything sensitive, since it
// ultimately boils down to disk reads.
if (preg_match('@(//|\.\.)@', $path)) {
return new Aphront400Response();
}
$type = CelerityResourceTransformer::getResourceType($path);
$type_map = self::getSupportedResourceTypes();
if (empty($type_map[$type])) {
- throw new Exception('Only static resources may be served.');
+ throw new Exception(pht('Only static resources may be served.'));
}
$dev_mode = PhabricatorEnv::getEnvConfig('phabricator.developer-mode');
if (AphrontRequest::getHTTPHeader('If-Modified-Since') && !$dev_mode) {
// Return a "304 Not Modified". We don't care about the value of this
// field since we never change what resource is served by a given URI.
return $this->makeResponseCacheable(new Aphront304Response());
}
$is_cacheable = (!$dev_mode) &&
$this->isCacheableResourceType($type);
$cache = null;
$data = null;
if ($is_cacheable) {
$cache = PhabricatorCaches::getImmutableCache();
$request_path = $this->getRequest()->getPath();
$cache_key = $this->getCacheKey($request_path);
$data = $cache->getKey($cache_key);
}
if ($data === null) {
$map = $this->getCelerityResourceMap();
if ($map->isPackageResource($path)) {
$resource_names = $map->getResourceNamesForPackageName($path);
if (!$resource_names) {
return new Aphront404Response();
}
try {
$data = array();
foreach ($resource_names as $resource_name) {
$data[] = $map->getResourceDataForName($resource_name);
}
$data = implode("\n\n", $data);
} catch (Exception $ex) {
return new Aphront404Response();
}
} else {
try {
$data = $map->getResourceDataForName($path);
} catch (Exception $ex) {
return new Aphront404Response();
}
}
$xformer = $this->buildResourceTransformer();
if ($xformer) {
$data = $xformer->transformResource($path, $data);
}
if ($cache) {
$cache->setKey($cache_key, $data);
}
}
$response = new AphrontFileResponse();
$response->setContent($data);
$response->setMimeType($type_map[$type]);
// NOTE: This is a piece of magic required to make WOFF fonts work in
// Firefox and IE. Possibly we should generalize this more.
$cross_origin_types = array(
'woff' => true,
'woff2' => true,
'eot' => true,
);
if (isset($cross_origin_types[$type])) {
// We could be more tailored here, but it's not currently trivial to
// generate a comprehensive list of valid origins (an install may have
// arbitrarily many Phame blogs, for example), and we lose nothing by
// allowing access from anywhere.
$response->addAllowOrigin('*');
}
return $this->makeResponseCacheable($response);
}
public static function getSupportedResourceTypes() {
return array(
'css' => 'text/css; charset=utf-8',
'js' => 'text/javascript; charset=utf-8',
'png' => 'image/png',
'gif' => 'image/gif',
'jpg' => 'image/jpeg',
'swf' => 'application/x-shockwave-flash',
'woff' => 'font/woff',
'woff2' => 'font/woff2',
'eot' => 'font/eot',
'ttf' => 'font/ttf',
'mp3' => 'audio/mpeg',
);
}
private function makeResponseCacheable(AphrontResponse $response) {
$response->setCacheDurationInSeconds(60 * 60 * 24 * 30);
$response->setLastModified(time());
return $response;
}
/**
* Is it appropriate to cache the data for this resource type in the fast
* immutable cache?
*
* Generally, text resources (which are small, and expensive to process)
* are cached, while other types of resources (which are large, and cheap
* to process) are not.
*
* @param string Resource type.
* @return bool True to enable caching.
*/
private function isCacheableResourceType($type) {
$types = array(
'js' => true,
'css' => true,
);
return isset($types[$type]);
}
private function getCacheKey($path) {
return 'celerity:'.$path;
}
}
diff --git a/src/applications/chatlog/conduit/ChatLogQueryConduitAPIMethod.php b/src/applications/chatlog/conduit/ChatLogQueryConduitAPIMethod.php
index 8270ced92..434a4671e 100644
--- a/src/applications/chatlog/conduit/ChatLogQueryConduitAPIMethod.php
+++ b/src/applications/chatlog/conduit/ChatLogQueryConduitAPIMethod.php
@@ -1,59 +1,59 @@
<?php
final class ChatLogQueryConduitAPIMethod extends ChatLogConduitAPIMethod {
public function getAPIMethodName() {
return 'chatlog.query';
}
public function getMethodStatus() {
return self::METHOD_STATUS_UNSTABLE;
}
public function getMethodDescription() {
- return 'Retrieve chatter.';
+ return pht('Retrieve chatter.');
}
protected function defineParamTypes() {
return array(
'channels' => 'optional list<string>',
'limit' => 'optional int (default = 100)',
);
}
protected function defineReturnType() {
return 'nonempty list<dict>';
}
protected function execute(ConduitAPIRequest $request) {
$query = new PhabricatorChatLogQuery();
$channel_ids = $request->getValue('channelIDs');
if ($channel_ids) {
$query->withChannelIDs($channel_ids);
}
$limit = $request->getValue('limit');
if (!$limit) {
$limit = 100;
}
$query->setLimit($limit);
$logs = $query->execute();
$results = array();
foreach ($logs as $log) {
$results[] = array(
'channelID' => $log->getChannelID(),
'epoch' => $log->getEpoch(),
'author' => $log->getAuthor(),
'type' => $log->getType(),
'message' => $log->getMessage(),
'loggedByPHID' => $log->getLoggedByPHID(),
);
}
return $results;
}
}
diff --git a/src/applications/chatlog/conduit/ChatLogRecordConduitAPIMethod.php b/src/applications/chatlog/conduit/ChatLogRecordConduitAPIMethod.php
index ca94b91dc..fe972222a 100644
--- a/src/applications/chatlog/conduit/ChatLogRecordConduitAPIMethod.php
+++ b/src/applications/chatlog/conduit/ChatLogRecordConduitAPIMethod.php
@@ -1,72 +1,72 @@
<?php
final class ChatLogRecordConduitAPIMethod extends ChatLogConduitAPIMethod {
public function getAPIMethodName() {
return 'chatlog.record';
}
public function getMethodStatus() {
return self::METHOD_STATUS_UNSTABLE;
}
public function getMethodDescription() {
- return 'Record chatter.';
+ return pht('Record chatter.');
}
protected function defineParamTypes() {
return array(
'logs' => 'required list<dict>',
);
}
protected function defineReturnType() {
return 'list<id>';
}
protected function execute(ConduitAPIRequest $request) {
$logs = $request->getValue('logs');
if (!is_array($logs)) {
$logs = array();
}
$template = new PhabricatorChatLogEvent();
$template->setLoggedByPHID($request->getUser()->getPHID());
$objs = array();
foreach ($logs as $log) {
$channel_name = idx($log, 'channel');
$service_name = idx($log, 'serviceName');
$service_type = idx($log, 'serviceType');
$channel = id(new PhabricatorChatLogChannel())->loadOneWhere(
'channelName = %s AND serviceName = %s AND serviceType = %s',
$channel_name,
$service_name,
$service_type);
if (!$channel) {
$channel = id(new PhabricatorChatLogChannel())
->setChannelName($channel_name)
->setserviceName($service_name)
->setServiceType($service_type)
->setViewPolicy(PhabricatorPolicies::POLICY_USER)
->setEditPolicy(PhabricatorPolicies::POLICY_USER)
->save();
}
$obj = clone $template;
$obj->setChannelID($channel->getID());
$obj->setType(idx($log, 'type'));
$obj->setAuthor(idx($log, 'author'));
$obj->setEpoch(idx($log, 'epoch'));
$obj->setMessage(idx($log, 'message'));
$obj->save();
$objs[] = $obj;
}
return array_values(mpull($objs, 'getID'));
}
}
diff --git a/src/applications/chatlog/controller/PhabricatorChatLogChannelListController.php b/src/applications/chatlog/controller/PhabricatorChatLogChannelListController.php
index c557b2bc2..3b4892bfe 100644
--- a/src/applications/chatlog/controller/PhabricatorChatLogChannelListController.php
+++ b/src/applications/chatlog/controller/PhabricatorChatLogChannelListController.php
@@ -1,41 +1,41 @@
<?php
final class PhabricatorChatLogChannelListController
extends PhabricatorChatLogController {
public function shouldAllowPublic() {
return true;
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$channels = id(new PhabricatorChatLogChannelQuery())
- ->setViewer($user)
- ->execute();
+ ->setViewer($user)
+ ->execute();
$list = new PHUIObjectItemListView();
foreach ($channels as $channel) {
$item = id(new PHUIObjectItemView())
->setHeader($channel->getChannelName())
->setHref('/chatlog/channel/'.$channel->getID().'/')
->addAttribute($channel->getServiceName())
->addAttribute($channel->getServiceType());
$list->addItem($item);
}
$crumbs = $this
->buildApplicationCrumbs()
->addTextCrumb(pht('Channel List'), $this->getApplicationURI());
return $this->buildApplicationPage(
array(
$crumbs,
$list,
),
array(
'title' => pht('Channel List'),
));
}
}
diff --git a/src/applications/chatlog/controller/PhabricatorChatLogChannelLogController.php b/src/applications/chatlog/controller/PhabricatorChatLogChannelLogController.php
index c7d2dc990..327024562 100644
--- a/src/applications/chatlog/controller/PhabricatorChatLogChannelLogController.php
+++ b/src/applications/chatlog/controller/PhabricatorChatLogChannelLogController.php
@@ -1,334 +1,334 @@
<?php
final class PhabricatorChatLogChannelLogController
extends PhabricatorChatLogController {
private $channelID;
public function shouldAllowPublic() {
return true;
}
public function willProcessRequest(array $data) {
$this->channelID = $data['channelID'];
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$uri = clone $request->getRequestURI();
$uri->setQueryParams(array());
$pager = new AphrontCursorPagerView();
$pager->setURI($uri);
$pager->setPageSize(250);
$query = id(new PhabricatorChatLogQuery())
->setViewer($user)
->withChannelIDs(array($this->channelID));
$channel = id(new PhabricatorChatLogChannelQuery())
- ->setViewer($user)
- ->withIDs(array($this->channelID))
- ->executeOne();
+ ->setViewer($user)
+ ->withIDs(array($this->channelID))
+ ->executeOne();
if (!$channel) {
return new Aphront404Response();
}
list($after, $before, $map) = $this->getPagingParameters($request, $query);
$pager->setAfterID($after);
$pager->setBeforeID($before);
$logs = $query->executeWithCursorPager($pager);
// Show chat logs oldest-first.
$logs = array_reverse($logs);
// Divide all the logs into blocks, where a block is the same author saying
// several things in a row. A block ends when another user speaks, or when
// two minutes pass without the author speaking.
$blocks = array();
$block = null;
$last_author = null;
$last_epoch = null;
foreach ($logs as $log) {
$this_author = $log->getAuthor();
$this_epoch = $log->getEpoch();
// Decide whether we should start a new block or not.
$new_block = ($this_author !== $last_author) ||
($this_epoch - (60 * 2) > $last_epoch);
if ($new_block) {
if ($block) {
$blocks[] = $block;
}
$block = array(
'id' => $log->getID(),
'epoch' => $this_epoch,
'author' => $this_author,
'logs' => array($log),
);
} else {
$block['logs'][] = $log;
}
$last_author = $this_author;
$last_epoch = $this_epoch;
}
if ($block) {
$blocks[] = $block;
}
// Figure out CSS classes for the blocks. We alternate colors between
// lines, and highlight the entire block which contains the target ID or
// date, if applicable.
foreach ($blocks as $key => $block) {
$classes = array();
if ($key % 2) {
$classes[] = 'alternate';
}
$ids = mpull($block['logs'], 'getID', 'getID');
if (array_intersect_key($ids, $map)) {
$classes[] = 'highlight';
}
$blocks[$key]['class'] = $classes ? implode(' ', $classes) : null;
}
require_celerity_resource('phabricator-chatlog-css');
$out = array();
foreach ($blocks as $block) {
$author = $block['author'];
$author = id(new PhutilUTF8StringTruncator())
->setMaximumGlyphs(18)
->truncateString($author);
$author = phutil_tag('td', array('class' => 'author'), $author);
$href = $uri->alter('at', $block['id']);
$timestamp = $block['epoch'];
$timestamp = phabricator_datetime($timestamp, $user);
$timestamp = phutil_tag(
'a',
array(
'href' => $href,
'class' => 'timestamp',
),
$timestamp);
$message = mpull($block['logs'], 'getMessage');
$message = implode("\n", $message);
$message = phutil_tag(
'td',
array(
'class' => 'message',
),
array(
$timestamp,
$message,
));
$out[] = phutil_tag(
'tr',
array(
'class' => $block['class'],
),
array(
$author,
$message,
));
}
$links = array();
$first_uri = $pager->getFirstPageURI();
if ($first_uri) {
$links[] = phutil_tag(
'a',
array(
'href' => $first_uri,
),
"\xC2\xAB ".pht('Newest'));
}
$prev_uri = $pager->getPrevPageURI();
if ($prev_uri) {
$links[] = phutil_tag(
'a',
array(
'href' => $prev_uri,
),
"\xE2\x80\xB9 ".pht('Newer'));
}
$next_uri = $pager->getNextPageURI();
if ($next_uri) {
$links[] = phutil_tag(
'a',
array(
'href' => $next_uri,
),
pht('Older')." \xE2\x80\xBA");
}
$pager_top = phutil_tag(
'div',
array('class' => 'phabricator-chat-log-pager-top'),
$links);
$pager_bottom = phutil_tag(
'div',
array('class' => 'phabricator-chat-log-pager-bottom'),
$links);
$crumbs = $this
->buildApplicationCrumbs()
->setBorder(true)
->addTextCrumb($channel->getChannelName(), $uri);
$form = id(new AphrontFormView())
->setUser($user)
->setMethod('GET')
->setAction($uri)
->appendChild(
id(new AphrontFormTextControl())
->setLabel(pht('Date'))
->setName('date')
->setValue($request->getStr('date')))
->appendChild(
id(new AphrontFormSubmitControl())
->setValue(pht('Jump')));
$filter = new AphrontListFilterView();
$filter->appendChild($form);
$table = phutil_tag(
'table',
array(
'class' => 'phabricator-chat-log',
),
$out);
$log = phutil_tag(
'div',
array(
'class' => 'phabricator-chat-log-panel',
),
$table);
$jump_link = phutil_tag(
'a',
array(
'href' => '#latest',
),
pht('Jump to Bottom')." \xE2\x96\xBE");
$jump = phutil_tag(
'div',
array(
'class' => 'phabricator-chat-log-jump',
),
$jump_link);
$jump_target = phutil_tag(
'div',
array(
'id' => 'latest',
));
$content = phutil_tag(
'div',
array(
'class' => 'phabricator-chat-log-wrap',
),
array(
$jump,
$pager_top,
$log,
$jump_target,
$pager_bottom,
));
return $this->buildApplicationPage(
array(
$crumbs,
$filter,
$content,
),
array(
'title' => pht('Channel Log'),
));
}
/**
* From request parameters, figure out where we should jump to in the log.
* We jump to either a date or log ID, but load a few lines of context before
* it so the user can see the nearby conversation.
*/
private function getPagingParameters(
AphrontRequest $request,
PhabricatorChatLogQuery $query) {
$user = $request->getUser();
$at_id = $request->getInt('at');
$at_date = $request->getStr('date');
$context_log = null;
$map = array();
$query = clone $query;
$query->setLimit(8);
if ($at_id) {
// Jump to the log in question, and load a few lines of context before
// it.
$context_logs = $query
->setAfterID($at_id)
->execute();
$context_log = last($context_logs);
$map = array(
$at_id => true,
);
} else if ($at_date) {
$timestamp = PhabricatorTime::parseLocalTime($at_date, $user);
if ($timestamp) {
$context_logs = $query
->withMaximumEpoch($timestamp)
->execute();
$context_log = last($context_logs);
$target_log = head($context_logs);
if ($target_log) {
$map = array(
$target_log->getID() => true,
);
}
}
}
if ($context_log) {
$after = null;
$before = $context_log->getID() - 1;
} else {
$after = $request->getInt('after');
$before = $request->getInt('before');
}
return array($after, $before, $map);
}
}
diff --git a/src/applications/conduit/call/__tests__/ConduitCallTestCase.php b/src/applications/conduit/call/__tests__/ConduitCallTestCase.php
index a5cc190d6..b62ddcef1 100644
--- a/src/applications/conduit/call/__tests__/ConduitCallTestCase.php
+++ b/src/applications/conduit/call/__tests__/ConduitCallTestCase.php
@@ -1,26 +1,28 @@
<?php
final class ConduitCallTestCase extends PhabricatorTestCase {
public function testConduitPing() {
$call = new ConduitCall('conduit.ping', array());
$result = $call->execute();
$this->assertFalse(empty($result));
}
public function testConduitAuth() {
$call = new ConduitCall('user.whoami', array(), true);
$caught = null;
try {
$result = $call->execute();
} catch (ConduitException $ex) {
$caught = $ex;
}
$this->assertTrue(
($caught instanceof ConduitException),
- 'user.whoami should require authentication');
+ pht(
+ '%s should require authentication.',
+ 'user.whoami'));
}
}
diff --git a/src/applications/conduit/controller/PhabricatorConduitAPIController.php b/src/applications/conduit/controller/PhabricatorConduitAPIController.php
index cdc14da27..cee47949d 100644
--- a/src/applications/conduit/controller/PhabricatorConduitAPIController.php
+++ b/src/applications/conduit/controller/PhabricatorConduitAPIController.php
@@ -1,696 +1,702 @@
<?php
final class PhabricatorConduitAPIController
extends PhabricatorConduitController {
public function shouldRequireLogin() {
return false;
}
private $method;
public function willProcessRequest(array $data) {
$this->method = $data['method'];
return $this;
}
public function processRequest() {
$time_start = microtime(true);
$request = $this->getRequest();
$method = $this->method;
$api_request = null;
$method_implementation = null;
$log = new PhabricatorConduitMethodCallLog();
$log->setMethod($method);
$metadata = array();
$multimeter = MultimeterControl::getInstance();
if ($multimeter) {
$multimeter->setEventContext('api.'.$method);
}
try {
list($metadata, $params) = $this->decodeConduitParams($request, $method);
$call = new ConduitCall($method, $params);
$method_implementation = $call->getMethodImplementation();
$result = null;
// TODO: The relationship between ConduitAPIRequest and ConduitCall is a
// little odd here and could probably be improved. Specifically, the
// APIRequest is a sub-object of the Call, which does not parallel the
// role of AphrontRequest (which is an indepenent object).
// In particular, the setUser() and getUser() existing independently on
// the Call and APIRequest is very awkward.
$api_request = $call->getAPIRequest();
$allow_unguarded_writes = false;
$auth_error = null;
$conduit_username = '-';
if ($call->shouldRequireAuthentication()) {
$metadata['scope'] = $call->getRequiredScope();
$auth_error = $this->authenticateUser($api_request, $metadata);
// If we've explicitly authenticated the user here and either done
// CSRF validation or are using a non-web authentication mechanism.
$allow_unguarded_writes = true;
if (isset($metadata['actAsUser'])) {
$this->actAsUser($api_request, $metadata['actAsUser']);
}
if ($auth_error === null) {
$conduit_user = $api_request->getUser();
if ($conduit_user && $conduit_user->getPHID()) {
$conduit_username = $conduit_user->getUsername();
}
$call->setUser($api_request->getUser());
}
}
$access_log = PhabricatorAccessLog::getLog();
if ($access_log) {
$access_log->setData(
array(
'u' => $conduit_username,
'm' => $method,
));
}
if ($call->shouldAllowUnguardedWrites()) {
$allow_unguarded_writes = true;
}
if ($auth_error === null) {
if ($allow_unguarded_writes) {
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
}
try {
$result = $call->execute();
$error_code = null;
$error_info = null;
} catch (ConduitException $ex) {
$result = null;
$error_code = $ex->getMessage();
if ($ex->getErrorDescription()) {
$error_info = $ex->getErrorDescription();
} else {
$error_info = $call->getErrorDescription($error_code);
}
}
if ($allow_unguarded_writes) {
unset($unguarded);
}
} else {
list($error_code, $error_info) = $auth_error;
}
} catch (Exception $ex) {
if (!($ex instanceof ConduitMethodNotFoundException)) {
phlog($ex);
}
$result = null;
$error_code = ($ex instanceof ConduitException
? 'ERR-CONDUIT-CALL'
: 'ERR-CONDUIT-CORE');
$error_info = $ex->getMessage();
}
$time_end = microtime(true);
$connection_id = null;
if (idx($metadata, 'connectionID')) {
$connection_id = $metadata['connectionID'];
} else if (($method == 'conduit.connect') && $result) {
$connection_id = idx($result, 'connectionID');
}
$log
->setCallerPHID(
isset($conduit_user)
? $conduit_user->getPHID()
: null)
->setConnectionID($connection_id)
->setError((string)$error_code)
->setDuration(1000000 * ($time_end - $time_start));
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$log->save();
unset($unguarded);
$response = id(new ConduitAPIResponse())
->setResult($result)
->setErrorCode($error_code)
->setErrorInfo($error_info);
switch ($request->getStr('output')) {
case 'human':
return $this->buildHumanReadableResponse(
$method,
$api_request,
$response->toDictionary(),
$method_implementation);
case 'json':
default:
return id(new AphrontJSONResponse())
->setAddJSONShield(false)
->setContent($response->toDictionary());
}
}
/**
* Change the api request user to the user that we want to act as.
* Only admins can use actAsUser
*
* @param ConduitAPIRequest Request being executed.
* @param string The username of the user we want to act as
*/
private function actAsUser(
ConduitAPIRequest $api_request,
$user_name) {
$config_key = 'security.allow-conduit-act-as-user';
if (!PhabricatorEnv::getEnvConfig($config_key)) {
- throw new Exception('security.allow-conduit-act-as-user is disabled');
+ throw new Exception(pht('%s is disabled.', $config_key));
}
if (!$api_request->getUser()->getIsAdmin()) {
- throw new Exception('Only administrators can use actAsUser');
+ throw new Exception(
+ pht(
+ 'Only administrators can use %s.',
+ __FUNCTION__));
}
$user = id(new PhabricatorUser())->loadOneWhere(
'userName = %s',
$user_name);
if (!$user) {
throw new Exception(
- "The actAsUser username '{$user_name}' is not a valid user."
- );
+ pht(
+ "The %s username '%s' is not a valid user.",
+ __FUNCTION__,
+ $user_name));
}
$api_request->setUser($user);
}
/**
* Authenticate the client making the request to a Phabricator user account.
*
* @param ConduitAPIRequest Request being executed.
* @param dict Request metadata.
* @return null|pair Null to indicate successful authentication, or
* an error code and error message pair.
*/
private function authenticateUser(
ConduitAPIRequest $api_request,
array $metadata) {
$request = $this->getRequest();
if ($request->getUser()->getPHID()) {
$request->validateCSRF();
return $this->validateAuthenticatedUser(
$api_request,
$request->getUser());
}
$auth_type = idx($metadata, 'auth.type');
if ($auth_type === ConduitClient::AUTH_ASYMMETRIC) {
$host = idx($metadata, 'auth.host');
if (!$host) {
return array(
'ERR-INVALID-AUTH',
pht(
- 'Request is missing required "auth.host" parameter.'),
+ 'Request is missing required "%s" parameter.',
+ 'auth.host'),
);
}
// TODO: Validate that we are the host!
$raw_key = idx($metadata, 'auth.key');
$public_key = PhabricatorAuthSSHPublicKey::newFromRawKey($raw_key);
$ssl_public_key = $public_key->toPKCS8();
// First, verify the signature.
try {
$protocol_data = $metadata;
// TODO: We should stop writing this into the protocol data when
// processing a request.
unset($protocol_data['scope']);
ConduitClient::verifySignature(
$this->method,
$api_request->getAllParameters(),
$protocol_data,
$ssl_public_key);
} catch (Exception $ex) {
return array(
'ERR-INVALID-AUTH',
pht(
'Signature verification failure. %s',
$ex->getMessage()),
);
}
// If the signature is valid, find the user or device which is
// associated with this public key.
$stored_key = id(new PhabricatorAuthSSHKeyQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withKeys(array($public_key))
->executeOne();
if (!$stored_key) {
return array(
'ERR-INVALID-AUTH',
- pht(
- 'No user or device is associated with that public key.'),
+ pht('No user or device is associated with that public key.'),
);
}
$object = $stored_key->getObject();
if ($object instanceof PhabricatorUser) {
$user = $object;
} else {
if (!$stored_key->getIsTrusted()) {
return array(
'ERR-INVALID-AUTH',
pht(
'The key which signed this request is not trusted. Only '.
'trusted keys can be used to sign API calls.'),
);
}
if (!PhabricatorEnv::isClusterRemoteAddress()) {
return array(
'ERR-INVALID-AUTH',
pht(
'This request originates from outside of the Phabricator '.
'cluster address range. Requests signed with trusted '.
'device keys must originate from within the cluster.'),
);
}
$user = PhabricatorUser::getOmnipotentUser();
// Flag this as an intracluster request.
$api_request->setIsClusterRequest(true);
}
return $this->validateAuthenticatedUser(
$api_request,
$user);
} else if ($auth_type === null) {
// No specified authentication type, continue with other authentication
// methods below.
} else {
return array(
'ERR-INVALID-AUTH',
pht(
- 'Provided "auth.type" ("%s") is not recognized.',
+ 'Provided "%s" ("%s") is not recognized.',
+ 'auth.type',
$auth_type),
);
}
$token_string = idx($metadata, 'token');
if (strlen($token_string)) {
if (strlen($token_string) != 32) {
return array(
'ERR-INVALID-AUTH',
pht(
'API token "%s" has the wrong length. API tokens should be '.
'32 characters long.',
$token_string),
);
}
$type = head(explode('-', $token_string));
$valid_types = PhabricatorConduitToken::getAllTokenTypes();
$valid_types = array_fuse($valid_types);
if (empty($valid_types[$type])) {
return array(
'ERR-INVALID-AUTH',
pht(
'API token "%s" has the wrong format. API tokens should be '.
'32 characters long and begin with one of these prefixes: %s.',
$token_string,
implode(', ', $valid_types)),
);
}
$token = id(new PhabricatorConduitTokenQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withTokens(array($token_string))
->withExpired(false)
->executeOne();
if (!$token) {
$token = id(new PhabricatorConduitTokenQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withTokens(array($token_string))
->withExpired(true)
->executeOne();
if ($token) {
return array(
'ERR-INVALID-AUTH',
pht(
'API token "%s" was previously valid, but has expired.',
$token_string),
);
} else {
return array(
'ERR-INVALID-AUTH',
pht(
'API token "%s" is not valid.',
$token_string),
);
}
}
// If this is a "cli-" token, it expires shortly after it is generated
// by default. Once it is actually used, we extend its lifetime and make
// it permanent. This allows stray tokens to get cleaned up automatically
// if they aren't being used.
if ($token->getTokenType() == PhabricatorConduitToken::TYPE_COMMANDLINE) {
if ($token->getExpires()) {
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$token->setExpires(null);
$token->save();
unset($unguarded);
}
}
// If this is a "clr-" token, Phabricator must be configured in cluster
// mode and the remote address must be a cluster node.
if ($token->getTokenType() == PhabricatorConduitToken::TYPE_CLUSTER) {
if (!PhabricatorEnv::isClusterRemoteAddress()) {
return array(
'ERR-INVALID-AUTH',
pht(
'This request originates from outside of the Phabricator '.
'cluster address range. Requests signed with cluster API '.
'tokens must originate from within the cluster.'),
);
}
// Flag this as an intracluster request.
$api_request->setIsClusterRequest(true);
}
$user = $token->getObject();
if (!($user instanceof PhabricatorUser)) {
return array(
'ERR-INVALID-AUTH',
- pht(
- 'API token is not associated with a valid user.'),
+ pht('API token is not associated with a valid user.'),
);
}
return $this->validateAuthenticatedUser(
$api_request,
$user);
}
// handle oauth
$access_token = idx($metadata, 'access_token');
$method_scope = idx($metadata, 'scope');
if ($access_token &&
$method_scope != PhabricatorOAuthServerScope::SCOPE_NOT_ACCESSIBLE) {
$token = id(new PhabricatorOAuthServerAccessToken())
- ->loadOneWhere('token = %s',
- $access_token);
+ ->loadOneWhere('token = %s', $access_token);
if (!$token) {
return array(
'ERR-INVALID-AUTH',
- 'Access token does not exist.',
+ pht('Access token does not exist.'),
);
}
$oauth_server = new PhabricatorOAuthServer();
$valid = $oauth_server->validateAccessToken($token,
$method_scope);
if (!$valid) {
return array(
'ERR-INVALID-AUTH',
- 'Access token is invalid.',
+ pht('Access token is invalid.'),
);
}
// valid token, so let's log in the user!
$user_phid = $token->getUserPHID();
$user = id(new PhabricatorUser())
- ->loadOneWhere('phid = %s',
- $user_phid);
+ ->loadOneWhere('phid = %s', $user_phid);
if (!$user) {
return array(
'ERR-INVALID-AUTH',
- 'Access token is for invalid user.',
+ pht('Access token is for invalid user.'),
);
}
return $this->validateAuthenticatedUser(
$api_request,
$user);
}
// Handle sessionless auth.
// TODO: This is super messy.
// TODO: Remove this in favor of token-based auth.
if (isset($metadata['authUser'])) {
$user = id(new PhabricatorUser())->loadOneWhere(
'userName = %s',
$metadata['authUser']);
if (!$user) {
return array(
'ERR-INVALID-AUTH',
- 'Authentication is invalid.',
+ pht('Authentication is invalid.'),
);
}
$token = idx($metadata, 'authToken');
$signature = idx($metadata, 'authSignature');
$certificate = $user->getConduitCertificate();
if (sha1($token.$certificate) !== $signature) {
return array(
'ERR-INVALID-AUTH',
- 'Authentication is invalid.',
+ pht('Authentication is invalid.'),
);
}
return $this->validateAuthenticatedUser(
$api_request,
$user);
}
// Handle session-based auth.
// TODO: Remove this in favor of token-based auth.
$session_key = idx($metadata, 'sessionKey');
if (!$session_key) {
return array(
'ERR-INVALID-SESSION',
- 'Session key is not present.',
+ pht('Session key is not present.'),
);
}
$user = id(new PhabricatorAuthSessionEngine())
->loadUserForSession(PhabricatorAuthSession::TYPE_CONDUIT, $session_key);
if (!$user) {
return array(
'ERR-INVALID-SESSION',
- 'Session key is invalid.',
+ pht('Session key is invalid.'),
);
}
return $this->validateAuthenticatedUser(
$api_request,
$user);
}
private function validateAuthenticatedUser(
ConduitAPIRequest $request,
PhabricatorUser $user) {
if (!$user->isUserActivated()) {
return array(
'ERR-USER-DISABLED',
pht('User account is not activated.'),
);
}
$request->setUser($user);
return null;
}
private function buildHumanReadableResponse(
$method,
ConduitAPIRequest $request = null,
$result = null,
ConduitAPIMethod $method_implementation = null) {
$param_rows = array();
$param_rows[] = array('Method', $this->renderAPIValue($method));
if ($request) {
foreach ($request->getAllParameters() as $key => $value) {
$param_rows[] = array(
$key,
$this->renderAPIValue($value),
);
}
}
$param_table = new AphrontTableView($param_rows);
$param_table->setColumnClasses(
array(
'header',
'wide',
));
$result_rows = array();
foreach ($result as $key => $value) {
$result_rows[] = array(
$key,
$this->renderAPIValue($value),
);
}
$result_table = new AphrontTableView($result_rows);
$result_table->setColumnClasses(
array(
'header',
'wide',
));
$param_panel = new PHUIObjectBoxView();
$param_panel->setHeaderText(pht('Method Parameters'));
$param_panel->appendChild($param_table);
$result_panel = new PHUIObjectBoxView();
$result_panel->setHeaderText(pht('Method Result'));
$result_panel->appendChild($result_table);
$method_uri = $this->getApplicationURI('method/'.$method.'/');
$crumbs = $this->buildApplicationCrumbs()
->addTextCrumb($method, $method_uri)
->addTextCrumb(pht('Call'));
$example_panel = null;
if ($request && $method_implementation) {
$params = $request->getAllParameters();
$example_panel = $this->renderExampleBox(
$method_implementation,
$params);
}
return $this->buildApplicationPage(
array(
$crumbs,
$param_panel,
$result_panel,
$example_panel,
),
array(
'title' => pht('Method Call Result'),
));
}
private function renderAPIValue($value) {
$json = new PhutilJSON();
if (is_array($value)) {
$value = $json->encodeFormatted($value);
}
$value = phutil_tag(
'pre',
array('style' => 'white-space: pre-wrap;'),
$value);
return $value;
}
private function decodeConduitParams(
AphrontRequest $request,
$method) {
// Look for parameters from the Conduit API Console, which are encoded
// as HTTP POST parameters in an array, e.g.:
//
// params[name]=value&params[name2]=value2
//
// The fields are individually JSON encoded, since we require users to
// enter JSON so that we avoid type ambiguity.
$params = $request->getArr('params', null);
if ($params !== null) {
foreach ($params as $key => $value) {
if ($value == '') {
// Interpret empty string null (e.g., the user didn't type anything
// into the box).
$value = 'null';
}
$decoded_value = json_decode($value, true);
if ($decoded_value === null && strtolower($value) != 'null') {
// When json_decode() fails, it returns null. This almost certainly
// indicates that a user was using the web UI and didn't put quotes
// around a string value. We can either do what we think they meant
// (treat it as a string) or fail. For now, err on the side of
// caution and fail. In the future, if we make the Conduit API
// actually do type checking, it might be reasonable to treat it as
// a string if the parameter type is string.
throw new Exception(
- "The value for parameter '{$key}' is not valid JSON. All ".
- "parameters must be encoded as JSON values, including strings ".
- "(which means you need to surround them in double quotes). ".
- "Check your syntax. Value was: {$value}");
+ pht(
+ "The value for parameter '%s' is not valid JSON. All ".
+ "parameters must be encoded as JSON values, including strings ".
+ "(which means you need to surround them in double quotes). ".
+ "Check your syntax. Value was: %s.",
+ $key,
+ $value));
}
$params[$key] = $decoded_value;
}
$metadata = idx($params, '__conduit__', array());
unset($params['__conduit__']);
return array($metadata, $params);
}
// Otherwise, look for a single parameter called 'params' which has the
// entire param dictionary JSON encoded.
$params_json = $request->getStr('params');
if (strlen($params_json)) {
$params = null;
try {
$params = phutil_json_decode($params_json);
} catch (PhutilJSONParserException $ex) {
throw new PhutilProxyException(
pht(
- "Invalid parameter information was passed to method '%s'",
+ "Invalid parameter information was passed to method '%s'.",
$method),
$ex);
}
$metadata = idx($params, '__conduit__', array());
unset($params['__conduit__']);
return array($metadata, $params);
}
// If we do not have `params`, assume this is a simple HTTP request with
// HTTP key-value pairs.
$params = array();
$metadata = array();
foreach ($request->getPassthroughRequestData() as $key => $value) {
$meta_key = ConduitAPIMethod::getParameterMetadataKey($key);
if ($meta_key !== null) {
$metadata[$meta_key] = $value;
} else {
$params[$key] = $value;
}
}
return array($metadata, $params);
}
}
diff --git a/src/applications/conduit/controller/PhabricatorConduitConsoleController.php b/src/applications/conduit/controller/PhabricatorConduitConsoleController.php
index 5c0ccfe6f..73e32b49f 100644
--- a/src/applications/conduit/controller/PhabricatorConduitConsoleController.php
+++ b/src/applications/conduit/controller/PhabricatorConduitConsoleController.php
@@ -1,210 +1,211 @@
<?php
final class PhabricatorConduitConsoleController
extends PhabricatorConduitController {
public function shouldAllowPublic() {
return true;
}
public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
$method_name = $request->getURIData('method');
$method = id(new PhabricatorConduitMethodQuery())
->setViewer($viewer)
->withMethods(array($method_name))
->executeOne();
if (!$method) {
return new Aphront404Response();
}
$call_uri = '/api/'.$method->getAPIMethodName();
$status = $method->getMethodStatus();
$reason = $method->getMethodStatusDescription();
$errors = array();
switch ($status) {
case ConduitAPIMethod::METHOD_STATUS_DEPRECATED:
$reason = nonempty($reason, pht('This method is deprecated.'));
$errors[] = pht('Deprecated Method: %s', $reason);
break;
case ConduitAPIMethod::METHOD_STATUS_UNSTABLE:
$reason = nonempty(
$reason,
pht(
'This method is new and unstable. Its interface is subject '.
'to change.'));
$errors[] = pht('Unstable Method: %s', $reason);
break;
}
$form = id(new AphrontFormView())
->setAction($call_uri)
->setUser($request->getUser())
->appendRemarkupInstructions(
pht(
'Enter parameters using **JSON**. For instance, to enter a '.
- 'list, type: `["apple", "banana", "cherry"]`'));
+ 'list, type: `%s`',
+ '["apple", "banana", "cherry"]'));
$params = $method->getParamTypes();
foreach ($params as $param => $desc) {
$form->appendChild(
id(new AphrontFormTextControl())
->setLabel($param)
->setName("params[{$param}]")
->setCaption($desc));
}
$must_login = !$viewer->isLoggedIn() &&
$method->shouldRequireAuthentication();
if ($must_login) {
$errors[] = pht(
'Login Required: This method requires authentication. You must '.
'log in before you can make calls to it.');
} else {
$form
->appendChild(
id(new AphrontFormSelectControl())
- ->setLabel('Output Format')
+ ->setLabel(pht('Output Format'))
->setName('output')
->setOptions(
array(
- 'human' => 'Human Readable',
- 'json' => 'JSON',
+ 'human' => pht('Human Readable'),
+ 'json' => pht('JSON'),
)))
->appendChild(
id(new AphrontFormSubmitControl())
->addCancelButton($this->getApplicationURI())
->setValue(pht('Call Method')));
}
$header = id(new PHUIHeaderView())
->setUser($viewer)
->setHeader($method->getAPIMethodName());
$form_box = id(new PHUIObjectBoxView())
->setHeaderText(pht('Call Method'))
->appendChild($form);
$content = array();
$properties = $this->buildMethodProperties($method);
$info_box = id(new PHUIObjectBoxView())
->setHeaderText(pht('API Method: %s', $method->getAPIMethodName()))
->setFormErrors($errors)
->appendChild($properties);
$content[] = $info_box;
$content[] = $form_box;
$content[] = $this->renderExampleBox($method, null);
$query = $method->newQueryObject();
if ($query) {
$orders = $query->getBuiltinOrders();
$rows = array();
foreach ($orders as $key => $order) {
$rows[] = array(
$key,
$order['name'],
implode(', ', $order['vector']),
);
}
$table = id(new AphrontTableView($rows))
->setHeaders(
array(
pht('Key'),
pht('Description'),
pht('Columns'),
))
->setColumnClasses(
array(
'pri',
'',
'wide',
));
$content[] = id(new PHUIObjectBoxView())
->setHeaderText(pht('Builtin Orders'))
->appendChild($table);
$columns = $query->getOrderableColumns();
$rows = array();
foreach ($columns as $key => $column) {
$rows[] = array(
$key,
idx($column, 'unique') ? pht('Yes') : pht('No'),
);
}
$table = id(new AphrontTableView($rows))
->setHeaders(
array(
pht('Key'),
pht('Unique'),
))
->setColumnClasses(
array(
'pri',
'wide',
));
$content[] = id(new PHUIObjectBoxView())
->setHeaderText(pht('Column Orders'))
->appendChild($table);
}
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb($method->getAPIMethodName());
return $this->buildApplicationPage(
array(
$crumbs,
$content,
),
array(
'title' => $method->getAPIMethodName(),
));
}
private function buildMethodProperties(ConduitAPIMethod $method) {
$viewer = $this->getViewer();
$view = id(new PHUIPropertyListView());
$view->addProperty(
pht('Returns'),
$method->getReturnType());
$error_types = $method->getErrorTypes();
$error_types['ERR-CONDUIT-CORE'] = pht('See error message for details.');
$error_description = array();
foreach ($error_types as $error => $meaning) {
$error_description[] = hsprintf(
'<li><strong>%s:</strong> %s</li>',
$error,
$meaning);
}
$error_description = phutil_tag('ul', array(), $error_description);
$view->addProperty(
pht('Errors'),
$error_description);
$description = $method->getMethodDescription();
$description = PhabricatorMarkupEngine::renderOneObject(
id(new PhabricatorMarkupOneOff())->setContent($description),
'default',
$viewer);
$view->addSectionHeader(pht('Description'));
$view->addTextContent($description);
return $view;
}
}
diff --git a/src/applications/conduit/controller/PhabricatorConduitLogController.php b/src/applications/conduit/controller/PhabricatorConduitLogController.php
index d8da59237..2effa1815 100644
--- a/src/applications/conduit/controller/PhabricatorConduitLogController.php
+++ b/src/applications/conduit/controller/PhabricatorConduitLogController.php
@@ -1,131 +1,131 @@
<?php
final class PhabricatorConduitLogController
extends PhabricatorConduitController {
public function processRequest() {
$request = $this->getRequest();
$viewer = $request->getUser();
$conn_table = new PhabricatorConduitConnectionLog();
$call_table = new PhabricatorConduitMethodCallLog();
$conn_r = $call_table->establishConnection('r');
$pager = new AphrontCursorPagerView();
$pager->readFromRequest($request);
$pager->setPageSize(500);
$query = id(new PhabricatorConduitLogQuery())
->setViewer($viewer);
$methods = $request->getStrList('methods');
if ($methods) {
$query->withMethods($methods);
}
$calls = $query->executeWithCursorPager($pager);
$conn_ids = array_filter(mpull($calls, 'getConnectionID'));
$conns = array();
if ($conn_ids) {
$conns = $conn_table->loadAllWhere(
'id IN (%Ld)',
$conn_ids);
}
$table = $this->renderCallTable($calls, $conns);
$box = id(new PHUIObjectBoxView())
->setHeaderText(pht('Call Logs'))
->appendChild($table);
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb(pht('Call Logs'));
return $this->buildApplicationPage(
array(
$crumbs,
$box,
$pager,
),
array(
'title' => pht('Conduit Logs'),
));
}
private function renderCallTable(array $calls, array $conns) {
assert_instances_of($calls, 'PhabricatorConduitMethodCallLog');
assert_instances_of($conns, 'PhabricatorConduitConnectionLog');
$viewer = $this->getRequest()->getUser();
$methods = id(new PhabricatorConduitMethodQuery())
->setViewer($viewer)
->execute();
$methods = mpull($methods, null, 'getAPIMethodName');
$rows = array();
foreach ($calls as $call) {
$conn = idx($conns, $call->getConnectionID());
if ($conn) {
$name = $conn->getUserName();
- $client = ' (via '.$conn->getClient().')';
+ $client = ' '.pht('(via %s)', $conn->getClient());
} else {
$name = null;
$client = null;
}
$method = idx($methods, $call->getMethod());
if ($method) {
switch ($method->getMethodStatus()) {
case ConduitAPIMethod::METHOD_STATUS_STABLE:
$status = null;
break;
case ConduitAPIMethod::METHOD_STATUS_UNSTABLE:
$status = pht('Unstable');
break;
case ConduitAPIMethod::METHOD_STATUS_DEPRECATED:
$status = pht('Deprecated');
break;
}
} else {
$status = pht('Unknown');
}
$rows[] = array(
$call->getConnectionID(),
$name,
array($call->getMethod(), $client),
$status,
$call->getError(),
- number_format($call->getDuration()).' us',
+ pht('%d us', number_format($call->getDuration())),
phabricator_datetime($call->getDateCreated(), $viewer),
);
}
$table = id(new AphrontTableView($rows));
$table->setHeaders(
array(
pht('Connection'),
pht('User'),
pht('Method'),
pht('Status'),
pht('Error'),
pht('Duration'),
pht('Date'),
));
$table->setColumnClasses(
array(
'',
'',
'wide',
'',
'',
'n',
'right',
));
return $table;
}
}
diff --git a/src/applications/conduit/method/ConduitAPIMethod.php b/src/applications/conduit/method/ConduitAPIMethod.php
index f6d464cfc..0643ca72d 100644
--- a/src/applications/conduit/method/ConduitAPIMethod.php
+++ b/src/applications/conduit/method/ConduitAPIMethod.php
@@ -1,375 +1,378 @@
<?php
/**
* @task status Method Status
* @task pager Paging Results
*/
abstract class ConduitAPIMethod
extends Phobject
implements PhabricatorPolicyInterface {
const METHOD_STATUS_STABLE = 'stable';
const METHOD_STATUS_UNSTABLE = 'unstable';
const METHOD_STATUS_DEPRECATED = 'deprecated';
abstract public function getMethodDescription();
abstract protected function defineParamTypes();
abstract protected function defineReturnType();
protected function defineErrorTypes() {
return array();
}
abstract protected function execute(ConduitAPIRequest $request);
public function __construct() {}
public function getParamTypes() {
$types = $this->defineParamTypes();
$query = $this->newQueryObject();
if ($query) {
$types['order'] = 'order';
$types += $this->getPagerParamTypes();
}
return $types;
}
public function getReturnType() {
return $this->defineReturnType();
}
public function getErrorTypes() {
return $this->defineErrorTypes();
}
/**
* This is mostly for compatibility with
* @{class:PhabricatorCursorPagedPolicyAwareQuery}.
*/
public function getID() {
return $this->getAPIMethodName();
}
/**
* Get the status for this method (e.g., stable, unstable or deprecated).
* Should return a METHOD_STATUS_* constant. By default, methods are
* "stable".
*
* @return const METHOD_STATUS_* constant.
* @task status
*/
public function getMethodStatus() {
return self::METHOD_STATUS_STABLE;
}
/**
* Optional description to supplement the method status. In particular, if
* a method is deprecated, you can return a string here describing the reason
* for deprecation and stable alternatives.
*
* @return string|null Description of the method status, if available.
* @task status
*/
public function getMethodStatusDescription() {
return null;
}
public function getErrorDescription($error_code) {
- return idx($this->getErrorTypes(), $error_code, 'Unknown Error');
+ return idx($this->getErrorTypes(), $error_code, pht('Unknown Error'));
}
public function getRequiredScope() {
// by default, conduit methods are not accessible via OAuth
return PhabricatorOAuthServerScope::SCOPE_NOT_ACCESSIBLE;
}
public function executeMethod(ConduitAPIRequest $request) {
return $this->execute($request);
}
public abstract function getAPIMethodName();
/**
* Return a key which sorts methods by application name, then method status,
* then method name.
*/
public function getSortOrder() {
$name = $this->getAPIMethodName();
$map = array(
self::METHOD_STATUS_STABLE => 0,
self::METHOD_STATUS_UNSTABLE => 1,
self::METHOD_STATUS_DEPRECATED => 2,
);
$ord = idx($map, $this->getMethodStatus(), 0);
list($head, $tail) = explode('.', $name, 2);
return "{$head}.{$ord}.{$tail}";
}
public function getApplicationName() {
return head(explode('.', $this->getAPIMethodName(), 2));
}
public static function getConduitMethod($method_name) {
static $method_map = null;
if ($method_map === null) {
$methods = id(new PhutilSymbolLoader())
->setAncestorClass(__CLASS__)
->loadObjects();
foreach ($methods as $method) {
$name = $method->getAPIMethodName();
if (empty($method_map[$name])) {
$method_map[$name] = $method;
continue;
}
$orig_class = get_class($method_map[$name]);
$this_class = get_class($method);
throw new Exception(
- "Two Conduit API method classes ({$orig_class}, {$this_class}) ".
- "both have the same method name ({$name}). API methods ".
- "must have unique method names.");
+ pht(
+ 'Two Conduit API method classes (%s, %s) both have the same '.
+ 'method name (%s). API methods must have unique method names.',
+ $orig_class,
+ $this_class,
+ $name));
}
}
return idx($method_map, $method_name);
}
public function shouldRequireAuthentication() {
return true;
}
public function shouldAllowPublic() {
return false;
}
public function shouldAllowUnguardedWrites() {
return false;
}
/**
* Optionally, return a @{class:PhabricatorApplication} which this call is
* part of. The call will be disabled when the application is uninstalled.
*
* @return PhabricatorApplication|null Related application.
*/
public function getApplication() {
return null;
}
protected function formatStringConstants($constants) {
foreach ($constants as $key => $value) {
$constants[$key] = '"'.$value.'"';
}
$constants = implode(', ', $constants);
return 'string-constant<'.$constants.'>';
}
public static function getParameterMetadataKey($key) {
if (strncmp($key, 'api.', 4) === 0) {
// All keys passed beginning with "api." are always metadata keys.
return substr($key, 4);
} else {
switch ($key) {
// These are real keys which always belong to request metadata.
case 'access_token':
case 'scope':
case 'output':
// This is not a real metadata key; it is included here only to
// prevent Conduit methods from defining it.
case '__conduit__':
// This is prevented globally as a blanket defense against OAuth
// redirection attacks. It is included here to stop Conduit methods
// from defining it.
case 'code':
// This is not a real metadata key, but the presence of this
// parameter triggers an alternate request decoding pathway.
case 'params':
return $key;
}
}
return null;
}
/* -( Paging Results )----------------------------------------------------- */
/**
* @task pager
*/
protected function getPagerParamTypes() {
return array(
'before' => 'optional string',
'after' => 'optional string',
'limit' => 'optional int (default = 100)',
);
}
/**
* @task pager
*/
protected function newPager(ConduitAPIRequest $request) {
$limit = $request->getValue('limit', 100);
$limit = min(1000, $limit);
$limit = max(1, $limit);
$pager = id(new AphrontCursorPagerView())
->setPageSize($limit);
$before_id = $request->getValue('before');
if ($before_id !== null) {
$pager->setBeforeID($before_id);
}
$after_id = $request->getValue('after');
if ($after_id !== null) {
$pager->setAfterID($after_id);
}
return $pager;
}
/**
* @task pager
*/
protected function addPagerResults(
array $results,
AphrontCursorPagerView $pager) {
$results['cursor'] = array(
'limit' => $pager->getPageSize(),
'after' => $pager->getNextPageID(),
'before' => $pager->getPrevPageID(),
);
return $results;
}
/* -( Implementing Query Methods )----------------------------------------- */
public function newQueryObject() {
return null;
}
protected function newQueryForRequest(ConduitAPIRequest $request) {
$query = $this->newQueryObject();
if (!$query) {
throw new Exception(
pht(
'You can not call newQueryFromRequest() in this method ("%s") '.
'because it does not implement newQueryObject().',
get_class($this)));
}
if (!($query instanceof PhabricatorCursorPagedPolicyAwareQuery)) {
throw new Exception(
pht(
'Call to method newQueryObject() did not return an object of class '.
'"%s".',
'PhabricatorCursorPagedPolicyAwareQuery'));
}
$query->setViewer($request->getUser());
$order = $request->getValue('order');
if ($order !== null) {
if (is_scalar($order)) {
$query->setOrder($order);
} else {
$query->setOrderVector($order);
}
}
return $query;
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getPHID() {
return null;
}
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
);
}
public function getPolicy($capability) {
// Application methods get application visibility; other methods get open
// visibility.
$application = $this->getApplication();
if ($application) {
return $application->getPolicy($capability);
}
return PhabricatorPolicies::getMostOpenPolicy();
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
if (!$this->shouldRequireAuthentication()) {
// Make unauthenticated methods universally visible.
return true;
}
return false;
}
public function describeAutomaticCapability($capability) {
return null;
}
protected function hasApplicationCapability(
$capability,
PhabricatorUser $viewer) {
$application = $this->getApplication();
if (!$application) {
return false;
}
return PhabricatorPolicyFilter::hasCapability(
$viewer,
$application,
$capability);
}
protected function requireApplicationCapability(
$capability,
PhabricatorUser $viewer) {
$application = $this->getApplication();
if (!$application) {
return;
}
PhabricatorPolicyFilter::requireCapability(
$viewer,
$this->getApplication(),
$capability);
}
}
diff --git a/src/applications/conduit/method/ConduitConnectConduitAPIMethod.php b/src/applications/conduit/method/ConduitConnectConduitAPIMethod.php
index 12f08f863..a6ec09ae0 100644
--- a/src/applications/conduit/method/ConduitConnectConduitAPIMethod.php
+++ b/src/applications/conduit/method/ConduitConnectConduitAPIMethod.php
@@ -1,159 +1,163 @@
<?php
final class ConduitConnectConduitAPIMethod extends ConduitAPIMethod {
public function getAPIMethodName() {
return 'conduit.connect';
}
public function shouldRequireAuthentication() {
return false;
}
public function shouldAllowUnguardedWrites() {
return true;
}
public function getMethodDescription() {
- return 'Connect a session-based client.';
+ return pht('Connect a session-based client.');
}
protected function defineParamTypes() {
return array(
'client' => 'required string',
'clientVersion' => 'required int',
'clientDescription' => 'optional string',
'user' => 'optional string',
'authToken' => 'optional int',
'authSignature' => 'optional string',
'host' => 'deprecated',
);
}
protected function defineReturnType() {
return 'dict<string, any>';
}
protected function defineErrorTypes() {
return array(
- 'ERR-BAD-VERSION' =>
+ 'ERR-BAD-VERSION' => pht(
'Client/server version mismatch. Upgrade your server or downgrade '.
- 'your client.',
- 'NEW-ARC-VERSION' =>
- 'Client/server version mismatch. Upgrade your client.',
- 'ERR-UNKNOWN-CLIENT' =>
- 'Client is unknown.',
- 'ERR-INVALID-USER' =>
- 'The username you are attempting to authenticate with is not valid.',
- 'ERR-INVALID-CERTIFICATE' =>
- 'Your authentication certificate for this server is invalid.',
- 'ERR-INVALID-TOKEN' =>
+ 'your client.'),
+ 'NEW-ARC-VERSION' => pht(
+ 'Client/server version mismatch. Upgrade your client.'),
+ 'ERR-UNKNOWN-CLIENT' => pht('Client is unknown.'),
+ 'ERR-INVALID-USER' => pht(
+ 'The username you are attempting to authenticate with is not valid.'),
+ 'ERR-INVALID-CERTIFICATE' => pht(
+ 'Your authentication certificate for this server is invalid.'),
+ 'ERR-INVALID-TOKEN' => pht(
"The challenge token you are authenticating with is outside of the ".
"allowed time range. Either your system clock is out of whack or ".
- "you're executing a replay attack.",
- 'ERR-NO-CERTIFICATE' => 'This server requires authentication.',
+ "you're executing a replay attack."),
+ 'ERR-NO-CERTIFICATE' => pht('This server requires authentication.'),
);
}
protected function execute(ConduitAPIRequest $request) {
$client = $request->getValue('client');
$client_version = (int)$request->getValue('clientVersion');
$client_description = (string)$request->getValue('clientDescription');
$client_description = id(new PhutilUTF8StringTruncator())
->setMaximumBytes(255)
->truncateString($client_description);
$username = (string)$request->getValue('user');
// Log the connection, regardless of the outcome of checks below.
$connection = new PhabricatorConduitConnectionLog();
$connection->setClient($client);
$connection->setClientVersion($client_version);
$connection->setClientDescription($client_description);
$connection->setUsername($username);
$connection->save();
switch ($client) {
case 'arc':
$server_version = 6;
$supported_versions = array(
$server_version => true,
// Client version 5 introduced "user.query" call
4 => true,
// Client version 6 introduced "diffusion.getlintmessages" call
5 => true,
);
if (empty($supported_versions[$client_version])) {
if ($server_version < $client_version) {
$ex = new ConduitException('ERR-BAD-VERSION');
$ex->setErrorDescription(
- "Your 'arc' client version is '{$client_version}', which ".
- "is newer than the server version, '{$server_version}'. ".
- "Upgrade your Phabricator install.");
+ pht(
+ "Your '%s' client version is '%d', which is newer than the ".
+ "server version, '%d'. Upgrade your Phabricator install.",
+ 'arc',
+ $client_version,
+ $server_version));
} else {
$ex = new ConduitException('NEW-ARC-VERSION');
$ex->setErrorDescription(
- "A new version of arc is available! You need to upgrade ".
- "to connect to this server (you are running version ".
- "{$client_version}, the server is running version ".
- "{$server_version}).");
+ pht(
+ 'A new version of arc is available! You need to upgrade '.
+ 'to connect to this server (you are running version '.
+ '%d, the server is running version %d).',
+ $client_version,
+ $server_version));
}
throw $ex;
}
break;
default:
// Allow new clients by default.
break;
}
$token = $request->getValue('authToken');
$signature = $request->getValue('authSignature');
$user = id(new PhabricatorUser())->loadOneWhere('username = %s', $username);
if (!$user) {
throw new ConduitException('ERR-INVALID-USER');
}
$session_key = null;
if ($token && $signature) {
$threshold = 60 * 15;
$now = time();
if (abs($token - $now) > $threshold) {
throw id(new ConduitException('ERR-INVALID-TOKEN'))
->setErrorDescription(
pht(
'The request you submitted is signed with a timestamp, but that '.
'timestamp is not within %s of the current time. The '.
'signed timestamp is %s (%s), and the current server time is '.
'%s (%s). This is a difference of %s seconds, but the '.
'timestamp must differ from the server time by no more than '.
'%s seconds. Your client or server clock may not be set '.
'correctly.',
phutil_format_relative_time($threshold),
$token,
date('r', $token),
$now,
date('r', $now),
($token - $now),
$threshold));
}
$valid = sha1($token.$user->getConduitCertificate());
if ($valid != $signature) {
throw new ConduitException('ERR-INVALID-CERTIFICATE');
}
$session_key = id(new PhabricatorAuthSessionEngine())->establishSession(
PhabricatorAuthSession::TYPE_CONDUIT,
$user->getPHID(),
$partial = false);
} else {
throw new ConduitException('ERR-NO-CERTIFICATE');
}
return array(
'connectionID' => $connection->getID(),
'sessionKey' => $session_key,
'userPHID' => $user->getPHID(),
);
}
}
diff --git a/src/applications/conduit/method/ConduitGetCertificateConduitAPIMethod.php b/src/applications/conduit/method/ConduitGetCertificateConduitAPIMethod.php
index be1461079..6248b5a6b 100644
--- a/src/applications/conduit/method/ConduitGetCertificateConduitAPIMethod.php
+++ b/src/applications/conduit/method/ConduitGetCertificateConduitAPIMethod.php
@@ -1,92 +1,92 @@
<?php
final class ConduitGetCertificateConduitAPIMethod extends ConduitAPIMethod {
public function getAPIMethodName() {
return 'conduit.getcertificate';
}
public function shouldRequireAuthentication() {
return false;
}
public function shouldAllowUnguardedWrites() {
// This method performs logging and is on the authentication pathway.
return true;
}
public function getMethodDescription() {
- return 'Retrieve certificate information for a user.';
+ return pht('Retrieve certificate information for a user.');
}
protected function defineParamTypes() {
return array(
'token' => 'required string',
'host' => 'required string',
);
}
protected function defineReturnType() {
return 'dict<string, any>';
}
protected function defineErrorTypes() {
return array(
- 'ERR-BAD-TOKEN' => 'Token does not exist or has expired.',
- 'ERR-RATE-LIMIT' =>
+ 'ERR-BAD-TOKEN' => pht('Token does not exist or has expired.'),
+ 'ERR-RATE-LIMIT' => pht(
'You have made too many invalid token requests recently. Wait before '.
- 'making more.',
+ 'making more.'),
);
}
protected function execute(ConduitAPIRequest $request) {
$failed_attempts = PhabricatorUserLog::loadRecentEventsFromThisIP(
PhabricatorUserLog::ACTION_CONDUIT_CERTIFICATE_FAILURE,
60 * 5);
if (count($failed_attempts) > 5) {
$this->logFailure($request);
throw new ConduitException('ERR-RATE-LIMIT');
}
$token = $request->getValue('token');
$info = id(new PhabricatorConduitCertificateToken())->loadOneWhere(
'token = %s',
trim($token));
if (!$info || $info->getDateCreated() < time() - (60 * 15)) {
$this->logFailure($request, $info);
throw new ConduitException('ERR-BAD-TOKEN');
} else {
$log = PhabricatorUserLog::initializeNewLog(
$request->getUser(),
$info->getUserPHID(),
PhabricatorUserLog::ACTION_CONDUIT_CERTIFICATE)
->save();
}
$user = id(new PhabricatorUser())->loadOneWhere(
'phid = %s',
$info->getUserPHID());
if (!$user) {
- throw new Exception('Certificate token points to an invalid user!');
+ throw new Exception(pht('Certificate token points to an invalid user!'));
}
return array(
'username' => $user->getUserName(),
'certificate' => $user->getConduitCertificate(),
);
}
private function logFailure(
ConduitAPIRequest $request,
PhabricatorConduitCertificateToken $info = null) {
$log = PhabricatorUserLog::initializeNewLog(
$request->getUser(),
$info ? $info->getUserPHID() : '-',
PhabricatorUserLog::ACTION_CONDUIT_CERTIFICATE_FAILURE)
->save();
}
}
diff --git a/src/applications/conduit/method/ConduitPingConduitAPIMethod.php b/src/applications/conduit/method/ConduitPingConduitAPIMethod.php
index f3c502def..9e0c1d4ff 100644
--- a/src/applications/conduit/method/ConduitPingConduitAPIMethod.php
+++ b/src/applications/conduit/method/ConduitPingConduitAPIMethod.php
@@ -1,29 +1,29 @@
<?php
final class ConduitPingConduitAPIMethod extends ConduitAPIMethod {
public function getAPIMethodName() {
return 'conduit.ping';
}
public function shouldRequireAuthentication() {
return false;
}
public function getMethodDescription() {
- return 'Basic ping for monitoring or a health-check.';
+ return pht('Basic ping for monitoring or a health-check.');
}
protected function defineParamTypes() {
return array();
}
protected function defineReturnType() {
return 'string';
}
protected function execute(ConduitAPIRequest $request) {
return php_uname('n');
}
}
diff --git a/src/applications/conduit/method/ConduitQueryConduitAPIMethod.php b/src/applications/conduit/method/ConduitQueryConduitAPIMethod.php
index f5162a04f..6cd9283b4 100644
--- a/src/applications/conduit/method/ConduitQueryConduitAPIMethod.php
+++ b/src/applications/conduit/method/ConduitQueryConduitAPIMethod.php
@@ -1,39 +1,39 @@
<?php
final class ConduitQueryConduitAPIMethod extends ConduitAPIMethod {
public function getAPIMethodName() {
return 'conduit.query';
}
public function getMethodDescription() {
- return 'Returns the parameters of the Conduit methods.';
+ return pht('Returns the parameters of the Conduit methods.');
}
protected function defineParamTypes() {
return array();
}
protected function defineReturnType() {
return 'dict<dict>';
}
protected function execute(ConduitAPIRequest $request) {
$classes = id(new PhutilSymbolLoader())
->setAncestorClass('ConduitAPIMethod')
->setType('class')
->loadObjects();
$names_to_params = array();
foreach ($classes as $class) {
$names_to_params[$class->getAPIMethodName()] = array(
'description' => $class->getMethodDescription(),
'params' => $class->getParamTypes(),
'return' => $class->getReturnType(),
);
}
return $names_to_params;
}
}
diff --git a/src/applications/conduit/protocol/ConduitAPIRequest.php b/src/applications/conduit/protocol/ConduitAPIRequest.php
index 7e5a8ae51..fe8af34b6 100644
--- a/src/applications/conduit/protocol/ConduitAPIRequest.php
+++ b/src/applications/conduit/protocol/ConduitAPIRequest.php
@@ -1,55 +1,56 @@
<?php
final class ConduitAPIRequest {
protected $params;
private $user;
private $isClusterRequest = false;
public function __construct(array $params) {
$this->params = $params;
}
public function getValue($key, $default = null) {
return coalesce(idx($this->params, $key), $default);
}
public function getAllParameters() {
return $this->params;
}
public function setUser(PhabricatorUser $user) {
$this->user = $user;
return $this;
}
/**
* Retrieve the authentic identity of the user making the request. If a
* method requires authentication (the default) the user object will always
* be available. If a method does not require authentication (i.e., overrides
* shouldRequireAuthentication() to return false) the user object will NEVER
* be available.
*
* @return PhabricatorUser Authentic user, available ONLY if the method
* requires authentication.
*/
public function getUser() {
if (!$this->user) {
throw new Exception(
- 'You can not access the user inside the implementation of a Conduit '.
- 'method which does not require authentication (as per '.
- 'shouldRequireAuthentication()).');
+ pht(
+ 'You can not access the user inside the implementation of a Conduit '.
+ 'method which does not require authentication (as per %s).',
+ 'shouldRequireAuthentication()'));
}
return $this->user;
}
public function setIsClusterRequest($is_cluster_request) {
$this->isClusterRequest = $is_cluster_request;
return $this;
}
public function getIsClusterRequest() {
return $this->isClusterRequest;
}
}
diff --git a/src/applications/conduit/query/PhabricatorConduitSearchEngine.php b/src/applications/conduit/query/PhabricatorConduitSearchEngine.php
index 0ed7a95b4..eece3e5c1 100644
--- a/src/applications/conduit/query/PhabricatorConduitSearchEngine.php
+++ b/src/applications/conduit/query/PhabricatorConduitSearchEngine.php
@@ -1,200 +1,201 @@
<?php
final class PhabricatorConduitSearchEngine
extends PhabricatorApplicationSearchEngine {
public function getResultTypeDescription() {
return pht('Conduit Methods');
}
public function getApplicationClassName() {
return 'PhabricatorConduitApplication';
}
public function getPageSize(PhabricatorSavedQuery $saved) {
return PHP_INT_MAX - 1;
}
public function buildSavedQueryFromRequest(AphrontRequest $request) {
$saved = new PhabricatorSavedQuery();
$saved->setParameter('isStable', $request->getStr('isStable'));
$saved->setParameter('isUnstable', $request->getStr('isUnstable'));
$saved->setParameter('isDeprecated', $request->getStr('isDeprecated'));
$saved->setParameter(
'applicationNames',
$request->getStrList('applicationNames'));
$saved->setParameter('nameContains', $request->getStr('nameContains'));
return $saved;
}
public function buildQueryFromSavedQuery(PhabricatorSavedQuery $saved) {
$query = id(new PhabricatorConduitMethodQuery());
$query->withIsStable($saved->getParameter('isStable'));
$query->withIsUnstable($saved->getParameter('isUnstable'));
$query->withIsDeprecated($saved->getParameter('isDeprecated'));
$names = $saved->getParameter('applicationNames', array());
if ($names) {
$query->withApplicationNames($names);
}
$contains = $saved->getParameter('nameContains');
if (strlen($contains)) {
$query->withNameContains($contains);
}
return $query;
}
public function buildSearchForm(
AphrontFormView $form,
PhabricatorSavedQuery $saved) {
$form
->appendChild(
id(new AphrontFormTextControl())
- ->setLabel('Name Contains')
+ ->setLabel(pht('Name Contains'))
->setName('nameContains')
->setValue($saved->getParameter('nameContains')));
$names = $saved->getParameter('applicationNames', array());
$form
->appendChild(
id(new AphrontFormTextControl())
- ->setLabel('Applications')
+ ->setLabel(pht('Applications'))
->setName('applicationNames')
->setValue(implode(', ', $names))
- ->setCaption(pht(
- 'Example: %s',
- phutil_tag('tt', array(), 'differential, paste'))));
+ ->setCaption(
+ pht(
+ 'Example: %s',
+ phutil_tag('tt', array(), 'differential, paste'))));
$is_stable = $saved->getParameter('isStable');
$is_unstable = $saved->getParameter('isUnstable');
$is_deprecated = $saved->getParameter('isDeprecated');
$form
->appendChild(
id(new AphrontFormCheckboxControl())
->setLabel('Stability')
->addCheckbox(
'isStable',
1,
hsprintf(
'<strong>%s</strong>: %s',
pht('Stable Methods'),
pht('Show established API methods with stable interfaces.')),
$is_stable)
->addCheckbox(
'isUnstable',
1,
hsprintf(
'<strong>%s</strong>: %s',
pht('Unstable Methods'),
pht('Show new methods which are subject to change.')),
$is_unstable)
->addCheckbox(
'isDeprecated',
1,
hsprintf(
'<strong>%s</strong>: %s',
pht('Deprecated Methods'),
pht(
'Show old methods which will be deleted in a future '.
'version of Phabricator.')),
$is_deprecated));
}
protected function getURI($path) {
return '/conduit/'.$path;
}
protected function getBuiltinQueryNames() {
return array(
'modern' => pht('Modern Methods'),
'all' => pht('All Methods'),
);
}
public function buildSavedQueryFromBuiltin($query_key) {
$query = $this->newSavedQuery();
$query->setQueryKey($query_key);
switch ($query_key) {
case 'modern':
return $query
->setParameter('isStable', true)
->setParameter('isUnstable', true);
case 'all':
return $query
->setParameter('isStable', true)
->setParameter('isUnstable', true)
->setParameter('isDeprecated', true);
}
return parent::buildSavedQueryFromBuiltin($query_key);
}
protected function renderResultList(
array $methods,
PhabricatorSavedQuery $query,
array $handles) {
assert_instances_of($methods, 'ConduitAPIMethod');
$viewer = $this->requireViewer();
$out = array();
$last = null;
$list = null;
foreach ($methods as $method) {
$app = $method->getApplicationName();
if ($app !== $last) {
$last = $app;
if ($list) {
$out[] = $list;
}
$list = id(new PHUIObjectItemListView());
$app_object = $method->getApplication();
if ($app_object) {
$app_name = $app_object->getName();
} else {
$app_name = $app;
}
}
$method_name = $method->getAPIMethodName();
$item = id(new PHUIObjectItemView())
->setHeader($method_name)
->setHref($this->getApplicationURI('method/'.$method_name.'/'))
->addAttribute($method->getMethodDescription());
switch ($method->getMethodStatus()) {
case ConduitAPIMethod::METHOD_STATUS_STABLE:
break;
case ConduitAPIMethod::METHOD_STATUS_UNSTABLE:
$item->addIcon('warning-grey', pht('Unstable'));
$item->setBarColor('yellow');
break;
case ConduitAPIMethod::METHOD_STATUS_DEPRECATED:
$item->addIcon('warning', pht('Deprecated'));
$item->setBarColor('red');
break;
}
$list->addItem($item);
}
if ($list) {
$out[] = $list;
}
return $out;
}
}
diff --git a/src/applications/conduit/ssh/ConduitSSHWorkflow.php b/src/applications/conduit/ssh/ConduitSSHWorkflow.php
index aed7004d7..6589fac32 100644
--- a/src/applications/conduit/ssh/ConduitSSHWorkflow.php
+++ b/src/applications/conduit/ssh/ConduitSSHWorkflow.php
@@ -1,87 +1,87 @@
<?php
final class ConduitSSHWorkflow extends PhabricatorSSHWorkflow {
protected function didConstruct() {
$this->setName('conduit');
$this->setArguments(
array(
array(
'name' => 'method',
'wildcard' => true,
),
));
}
public function execute(PhutilArgumentParser $args) {
$time_start = microtime(true);
$methodv = $args->getArg('method');
if (!$methodv) {
- throw new Exception('No Conduit method provided.');
+ throw new Exception(pht('No Conduit method provided.'));
} else if (count($methodv) > 1) {
- throw new Exception('Too many Conduit methods provided.');
+ throw new Exception(pht('Too many Conduit methods provided.'));
}
$method = head($methodv);
$json = $this->readAllInput();
$raw_params = null;
try {
$raw_params = phutil_json_decode($json);
} catch (PhutilJSONParserException $ex) {
throw new PhutilProxyException(
pht('Invalid JSON input.'),
$ex);
}
$params = idx($raw_params, 'params', '[]');
$params = phutil_json_decode($params);
$metadata = idx($params, '__conduit__', array());
unset($params['__conduit__']);
$call = null;
$error_code = null;
$error_info = null;
try {
$call = new ConduitCall($method, $params);
$call->setUser($this->getUser());
$result = $call->execute();
} catch (ConduitException $ex) {
$result = null;
$error_code = $ex->getMessage();
if ($ex->getErrorDescription()) {
$error_info = $ex->getErrorDescription();
} else if ($call) {
$error_info = $call->getErrorDescription($error_code);
}
}
$response = id(new ConduitAPIResponse())
->setResult($result)
->setErrorCode($error_code)
->setErrorInfo($error_info);
$json_out = json_encode($response->toDictionary());
$json_out = $json_out."\n";
$this->getIOChannel()->write($json_out);
// NOTE: Flush here so we can get an accurate result for the duration,
// if the response is large and the receiver is slow to read it.
$this->getIOChannel()->flush();
$time_end = microtime(true);
$connection_id = idx($metadata, 'connectionID');
$log = id(new PhabricatorConduitMethodCallLog())
->setCallerPHID($this->getUser()->getPHID())
->setConnectionID($connection_id)
->setMethod($method)
->setError((string)$error_code)
->setDuration(1000000 * ($time_end - $time_start))
->save();
}
}
diff --git a/src/applications/config/check/PhabricatorAuthSetupCheck.php b/src/applications/config/check/PhabricatorAuthSetupCheck.php
index 5d4f73186..0295a85b2 100644
--- a/src/applications/config/check/PhabricatorAuthSetupCheck.php
+++ b/src/applications/config/check/PhabricatorAuthSetupCheck.php
@@ -1,45 +1,40 @@
<?php
final class PhabricatorAuthSetupCheck extends PhabricatorSetupCheck {
public function getDefaultGroup() {
return self::GROUP_IMPORTANT;
}
protected function executeChecks() {
// NOTE: We're not actually building these providers. Building providers
// can require additional configuration to be present (e.g., to build
// redirect and login URIs using `phabricator.base-uri`) and it won't
// necessarily be available when running setup checks.
// Since this check is only meant as a hint to new administrators about
// steps they should take, we don't need to be thorough about checking
// that providers are enabled, available, correctly configured, etc. As
// long as they've created some kind of provider in the auth app before,
// they know that it exists and don't need the hint to go check it out.
$configs = id(new PhabricatorAuthProviderConfigQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->execute();
if (!$configs) {
$message = pht(
'You have not configured any authentication providers yet. You '.
'should add a provider (like username/password, LDAP, or GitHub '.
'OAuth) so users can register and log in. You can add and configure '.
- 'providers %s.',
- phutil_tag(
- 'a',
- array(
- 'href' => '/auth/',
- ),
- pht('using the "Auth" application')));
+ 'providers using the [[%s | "Auth" application]].',
+ '/auth/');
$this
->newIssue('auth.noproviders')
->setShortName(pht('No Auth Providers'))
->setName(pht('No Authentication Providers Configured'))
->setMessage($message);
}
}
}
diff --git a/src/applications/config/check/PhabricatorBinariesSetupCheck.php b/src/applications/config/check/PhabricatorBinariesSetupCheck.php
index 87887d4a9..59dc3a78a 100644
--- a/src/applications/config/check/PhabricatorBinariesSetupCheck.php
+++ b/src/applications/config/check/PhabricatorBinariesSetupCheck.php
@@ -1,277 +1,278 @@
<?php
final class PhabricatorBinariesSetupCheck extends PhabricatorSetupCheck {
public function getDefaultGroup() {
return self::GROUP_OTHER;
}
protected function executeChecks() {
-
if (phutil_is_windows()) {
$bin_name = 'where';
} else {
$bin_name = 'which';
}
if (!Filesystem::binaryExists($bin_name)) {
$message = pht(
"Without '%s', Phabricator can not test for the availability ".
"of other binaries.",
$bin_name);
$this->raiseWarning($bin_name, $message);
// We need to return here if we can't find the 'which' / 'where' binary
// because the other tests won't be valid.
return;
}
if (!Filesystem::binaryExists('diff')) {
$message = pht(
- "Without 'diff', Phabricator will not be able to generate or render ".
- "diffs in multiple applications.");
+ "Without '%s', Phabricator will not be able to generate or render ".
+ "diffs in multiple applications.",
+ 'diff');
$this->raiseWarning('diff', $message);
} else {
$tmp_a = new TempFile();
$tmp_b = new TempFile();
$tmp_c = new TempFile();
Filesystem::writeFile($tmp_a, 'A');
Filesystem::writeFile($tmp_b, 'A');
Filesystem::writeFile($tmp_c, 'B');
list($err) = exec_manual('diff %s %s', $tmp_a, $tmp_b);
if ($err) {
$this->newIssue('bin.diff.same')
- ->setName(pht("Unexpected 'diff' Behavior"))
+ ->setName(pht("Unexpected '%s' Behavior", 'diff'))
->setMessage(
pht(
- "The 'diff' binary on this system has unexpected behavior: ".
+ "The '%s' binary on this system has unexpected behavior: ".
"it was expected to exit without an error code when passed ".
"identical files, but exited with code %d.",
+ 'diff',
$err));
}
list($err) = exec_manual('diff %s %s', $tmp_a, $tmp_c);
if (!$err) {
$this->newIssue('bin.diff.diff')
->setName(pht("Unexpected 'diff' Behavior"))
->setMessage(
pht(
- "The 'diff' binary on this system has unexpected behavior: ".
+ "The '%s' binary on this system has unexpected behavior: ".
"it was expected to exit with a nonzero error code when passed ".
- "differing files, but did not."));
+ "differing files, but did not.",
+ 'diff'));
}
}
$table = new PhabricatorRepository();
$vcses = queryfx_all(
$table->establishConnection('r'),
'SELECT DISTINCT versionControlSystem FROM %T',
$table->getTableName());
foreach ($vcses as $vcs) {
switch ($vcs['versionControlSystem']) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
$binary = 'git';
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
$binary = 'svn';
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
$binary = 'hg';
break;
default:
$binary = null;
break;
}
if (!$binary) {
continue;
}
if (!Filesystem::binaryExists($binary)) {
$message = pht(
'You have at least one repository configured which uses this '.
'version control system. It will not work without the VCS binary.');
$this->raiseWarning($binary, $message);
}
$version = null;
switch ($binary) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
$minimum_version = null;
$bad_versions = array();
list($err, $stdout, $stderr) = exec_manual('git --version');
$version = trim(substr($stdout, strlen('git version ')));
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
$minimum_version = '1.5';
$bad_versions = array(
'1.7.1' => pht(
'This version of Subversion has a bug where `%s` does not work '.
'for files added in rN (Subversion issue #2873), fixed in 1.7.2.',
'svn diff -c N'),
);
list($err, $stdout, $stderr) = exec_manual('svn --version --quiet');
$version = trim($stdout);
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
$minimum_version = '1.9';
$bad_versions = array(
'2.1' => pht(
'This version of Mercurial returns a bad exit code '.
'after a successful pull.'),
'2.2' => pht(
'This version of Mercurial has a significant memory leak, fixed '.
'in 2.2.1. Pushing fails with this version as well; see %s.',
'T3046#54922'),
);
$version = PhabricatorRepositoryVersion::getMercurialVersion();
break;
}
if ($version === null) {
$this->raiseUnknownVersionWarning($binary);
} else {
if ($minimum_version &&
version_compare($version, $minimum_version, '<')) {
$this->raiseMinimumVersionWarning(
$binary,
$minimum_version,
$version);
}
foreach ($bad_versions as $bad_version => $details) {
if ($bad_version === $version) {
$this->raiseBadVersionWarning(
$binary,
$bad_version);
}
}
}
}
}
private function raiseWarning($bin, $message) {
if (phutil_is_windows()) {
$preamble = pht(
"The '%s' binary could not be found. Set the webserver's %s ".
"environmental variable to include the directory where it resides, or ".
"add that directory to '%s' in the Phabricator configuration.",
$bin,
'PATH',
'environment.append-paths');
} else {
$preamble = pht(
"The '%s' binary could not be found. Symlink it into '%s', or set the ".
"webserver's %s environmental variable to include the directory where ".
"it resides, or add that directory to '%s' in the Phabricator ".
"configuration.",
$bin,
'phabricator/support/bin/',
'PATH',
'environment.append-paths');
}
$this->newIssue('bin.'.$bin)
->setShortName(pht("'%s' Missing", $bin))
->setName(pht("Missing '%s' Binary", $bin))
->setSummary(
pht("The '%s' binary could not be located or executed.", $bin))
->setMessage($preamble.' '.$message)
->addPhabricatorConfig('environment.append-paths');
}
private function raiseUnknownVersionWarning($binary) {
$summary = pht(
'Unable to determine the version number of "%s".',
$binary);
$message = pht(
'Unable to determine the version number of "%s". Usually, this means '.
'the program changed its version format string recently and Phabricator '.
'does not know how to parse the new one yet, but might indicate that '.
'you have a very old (or broken) binary.'.
"\n\n".
'Because we can not determine the version number, checks against '.
'minimum and known-bad versions will be skipped, so we might fail '.
'to detect an incompatible binary.'.
"\n\n".
'You may be able to resolve this issue by updating Phabricator, since '.
'a newer version of Phabricator is likely to be able to parse the '.
'newer version string.'.
"\n\n".
'If updating Phabricator does not fix this, you can report the issue '.
'to the upstream so we can adjust the parser.'.
"\n\n".
'If you are confident you have a recent version of "%s" installed and '.
'working correctly, it is usually safe to ignore this warning.',
$binary,
$binary);
$this->newIssue('bin.'.$binary.'.unknown-version')
->setShortName(pht("Unknown '%s' Version", $binary))
->setName(pht("Unknown '%s' Version", $binary))
->setSummary($summary)
->setMessage($message)
->addLink(
PhabricatorEnv::getDoclink('Contributing Bug Reports'),
pht('Report this Issue to the Upstream'));
}
private function raiseMinimumVersionWarning(
$binary,
$minimum_version,
$version) {
switch ($binary) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
$summary = pht(
"The '%s' binary is version %s and Phabricator requires version ".
"%s or higher.",
$binary,
$version,
$minimum_version);
$message = pht(
"Please upgrade the '%s' binary to a more modern version.",
$binary);
$this->newIssue('bin.'.$binary)
->setShortName(pht("Unsupported '%s' Version", $binary))
->setName(pht("Unsupported '%s' Version", $binary))
->setSummary($summary)
->setMessage($summary.' '.$message);
break;
}
}
private function raiseBadVersionWarning($binary, $bad_version) {
-
switch ($binary) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
$summary = pht(
"The '%s' binary is version %s which has bugs that break ".
"Phabricator.",
$binary,
$bad_version);
$message = pht(
"Please upgrade the '%s' binary to a more modern version.",
$binary);
$this->newIssue('bin.'.$binary)
->setShortName(pht("Unsupported '%s' Version", $binary))
->setName(pht("Unsupported '%s' Version", $binary))
->setSummary($summary)
->setMessage($summary.' '.$message);
break;
}
}
}
diff --git a/src/applications/config/check/PhabricatorDaemonsSetupCheck.php b/src/applications/config/check/PhabricatorDaemonsSetupCheck.php
index a6b9ef848..3b4b33513 100644
--- a/src/applications/config/check/PhabricatorDaemonsSetupCheck.php
+++ b/src/applications/config/check/PhabricatorDaemonsSetupCheck.php
@@ -1,194 +1,198 @@
<?php
final class PhabricatorDaemonsSetupCheck extends PhabricatorSetupCheck {
public function getDefaultGroup() {
return self::GROUP_IMPORTANT;
}
protected function executeChecks() {
$task_daemon = id(new PhabricatorDaemonLogQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withStatus(PhabricatorDaemonLogQuery::STATUS_RUNNING)
->withDaemonClasses(array('PhabricatorTaskmasterDaemon'))
->setLimit(1)
->execute();
if (!$task_daemon) {
- $doc_href = PhabricatorEnv::getDocLink(
- 'Managing Daemons with phd');
+ $doc_href = PhabricatorEnv::getDocLink('Managing Daemons with phd');
$summary = pht(
'You must start the Phabricator daemons to send email, rebuild '.
'search indexes, and do other background processing.');
$message = pht(
'The Phabricator daemons are not running, so Phabricator will not '.
'be able to perform background processing (including sending email, '.
'rebuilding search indexes, importing commits, cleaning up old data, '.
'and running builds).'.
"\n\n".
'Use %s to start daemons. See %s for more information.',
phutil_tag('tt', array(), 'bin/phd start'),
phutil_tag(
'a',
array(
'href' => $doc_href,
'target' => '_blank',
),
pht('Managing Daemons with phd')));
$this->newIssue('daemons.not-running')
->setShortName(pht('Daemons Not Running'))
->setName(pht('Phabricator Daemons Are Not Running'))
->setSummary($summary)
->setMessage($message)
->addCommand('phabricator/ $ ./bin/phd start');
}
$phd_user = PhabricatorEnv::getEnvConfig('phd.user');
$environment_hash = PhabricatorEnv::calculateEnvironmentHash();
$all_daemons = id(new PhabricatorDaemonLogQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withStatus(PhabricatorDaemonLogQuery::STATUS_ALIVE)
->execute();
foreach ($all_daemons as $daemon) {
if ($phd_user) {
if ($daemon->getRunningAsUser() != $phd_user) {
- $doc_href = PhabricatorEnv::getDocLink(
- 'Managing Daemons with phd');
+ $doc_href = PhabricatorEnv::getDocLink('Managing Daemons with phd');
$summary = pht(
'At least one daemon is currently running as a different '.
- 'user than configured in the Phabricator phd.user setting');
+ 'user than configured in the Phabricator %s setting',
+ 'phd.user');
$message = pht(
'A daemon is running as user %s while the Phabricator config '.
- 'specifies phd.user to be %s.'.
+ 'specifies %s to be %s.'.
"\n\n".
- 'Either adjust phd.user to match %s or start '.
+ 'Either adjust %s to match %s or start '.
'the daemons as the correct user. '.
"\n\n".
- 'phd Daemons will try to '.
- 'use sudo to start as the configured user. '.
- 'Make sure that the user who starts phd has the correct '.
- 'sudo permissions to start phd daemons as %s',
+ '%s Daemons will try to use %s to start as the configured user. '.
+ 'Make sure that the user who starts %s has the correct '.
+ 'sudo permissions to start %s daemons as %s',
+ 'phd.user',
+ 'phd.user',
+ 'phd',
+ 'sudo',
+ 'phd',
+ 'phd',
phutil_tag('tt', array(), $daemon->getRunningAsUser()),
phutil_tag('tt', array(), $phd_user),
phutil_tag('tt', array(), $daemon->getRunningAsUser()),
phutil_tag('tt', array(), $phd_user));
$this->newIssue('daemons.run-as-different-user')
->setName(pht('Daemons are running as the wrong user'))
->setSummary($summary)
->setMessage($message)
->addCommand('phabricator/ $ ./bin/phd restart');
}
}
if ($daemon->getEnvHash() != $environment_hash) {
$doc_href = PhabricatorEnv::getDocLink(
'Managing Daemons with phd');
$summary = pht(
'At least one daemon is currently running with different '.
'configuration than the Phabricator web application.');
$list_section = null;
$env_info = $daemon->getEnvInfo();
if ($env_info) {
$issues = PhabricatorEnv::compareEnvironmentInfo(
PhabricatorEnv::calculateEnvironmentInfo(),
$env_info);
if ($issues) {
foreach ($issues as $key => $issue) {
$issues[$key] = phutil_tag('li', array(), $issue);
}
$list_section = array(
pht(
'The configurations differ in the following %s way(s):',
new PhutilNumber(count($issues))),
phutil_tag(
'ul',
array(),
$issues),
);
}
}
$message = pht(
'At least one daemon is currently running with a different '.
'configuration (config checksum %s) than the web application '.
'(config checksum %s).'.
"\n\n%s".
'This usually means that you have just made a configuration change '.
'from the web UI, but have not yet restarted the daemons. You '.
'need to restart the daemons after making configuration changes '.
'so they will pick up the new values: until you do, they will '.
'continue operating with the old settings.'.
"\n\n".
'(If you plan to make more changes, you can restart the daemons '.
'once after you finish making all of your changes.)'.
"\n\n".
'Use %s to restart daemons. You can find a list of running daemons '.
'in the %s, which will also help you identify which daemon (or '.
'daemons) have divergent configuration. For more information about '.
'managing the daemons, see %s in the documentation.'.
"\n\n".
'This can also happen if you use the %s environmental variable to '.
'choose a configuration file, but the daemons run with a different '.
'value than the web application. If restarting the daemons does '.
'not resolve this issue and you use %s to select configuration, '.
'check that it is set consistently.'.
"\n\n".
'A third possible cause is that you run several machines, and '.
'the %s configuration file differs between them. This file is '.
'updated when you edit configuration from the CLI with %s. If '.
'restarting the daemons does not resolve this issue and you '.
'run multiple machines, check that all machines have identical '.
'%s configuration files.'.
"\n\n".
'This issue is not severe, but usually indicates that something '.
'is not configured the way you expect, and may cause the daemons '.
'to exhibit different behavior than the web application does.',
phutil_tag('tt', array(), substr($daemon->getEnvHash(), 0, 12)),
phutil_tag('tt', array(), substr($environment_hash, 0, 12)),
$list_section,
phutil_tag('tt', array(), 'bin/phd restart'),
phutil_tag(
'a',
array(
'href' => '/daemon/',
'target' => '_blank',
),
pht('Daemon Console')),
phutil_tag(
'a',
array(
'href' => $doc_href,
'target' => '_blank',
),
pht('Managing Daemons with phd')),
phutil_tag('tt', array(), 'PHABRICATOR_ENV'),
phutil_tag('tt', array(), 'PHABRICATOR_ENV'),
phutil_tag('tt', array(), 'phabricator/conf/local/local.json'),
phutil_tag('tt', array(), 'bin/config'),
phutil_tag('tt', array(), 'phabricator/conf/local/local.json'));
$this->newIssue('daemons.need-restarting')
->setName(pht('Daemons and Web Have Different Config'))
->setSummary($summary)
->setMessage($message)
->addCommand('phabricator/ $ ./bin/phd restart');
break;
}
}
}
}
diff --git a/src/applications/config/check/PhabricatorDatabaseSetupCheck.php b/src/applications/config/check/PhabricatorDatabaseSetupCheck.php
index 3f7af1bdb..4ee0a75c6 100644
--- a/src/applications/config/check/PhabricatorDatabaseSetupCheck.php
+++ b/src/applications/config/check/PhabricatorDatabaseSetupCheck.php
@@ -1,146 +1,149 @@
<?php
final class PhabricatorDatabaseSetupCheck extends PhabricatorSetupCheck {
public function getDefaultGroup() {
return self::GROUP_IMPORTANT;
}
public function getExecutionOrder() {
// This must run after basic PHP checks, but before most other checks.
return 0.5;
}
protected function executeChecks() {
$conf = PhabricatorEnv::newObjectFromConfig('mysql.configuration-provider');
$conn_user = $conf->getUser();
$conn_pass = $conf->getPassword();
$conn_host = $conf->getHost();
$conn_port = $conf->getPort();
ini_set('mysql.connect_timeout', 2);
$config = array(
'user' => $conn_user,
'pass' => $conn_pass,
'host' => $conn_host,
'port' => $conn_port,
'database' => null,
);
$conn_raw = PhabricatorEnv::newObjectFromConfig(
'mysql.implementation',
array($config));
try {
queryfx($conn_raw, 'SELECT 1');
} catch (AphrontConnectionQueryException $ex) {
$message = pht(
"Unable to connect to MySQL!\n\n".
"%s\n\n".
"Make sure Phabricator and MySQL are correctly configured.",
$ex->getMessage());
$this->newIssue('mysql.connect')
->setName(pht('Can Not Connect to MySQL'))
->setMessage($message)
->setIsFatal(true)
->addRelatedPhabricatorConfig('mysql.host')
->addRelatedPhabricatorConfig('mysql.port')
->addRelatedPhabricatorConfig('mysql.user')
->addRelatedPhabricatorConfig('mysql.pass');
return;
}
$engines = queryfx_all($conn_raw, 'SHOW ENGINES');
$engines = ipull($engines, 'Support', 'Engine');
$innodb = idx($engines, 'InnoDB');
if ($innodb != 'YES' && $innodb != 'DEFAULT') {
$message = pht(
"The 'InnoDB' engine is not available in MySQL. Enable InnoDB in ".
"your MySQL configuration.".
"\n\n".
"(If you aleady created tables, MySQL incorrectly used some other ".
"engine to create them. You need to convert them or drop and ".
"reinitialize them.)");
$this->newIssue('mysql.innodb')
->setName(pht('MySQL InnoDB Engine Not Available'))
->setMessage($message)
->setIsFatal(true);
return;
}
$namespace = PhabricatorEnv::getEnvConfig('storage.default-namespace');
$databases = queryfx_all($conn_raw, 'SHOW DATABASES');
$databases = ipull($databases, 'Database', 'Database');
if (empty($databases[$namespace.'_meta_data'])) {
$message = pht(
- 'Run the storage upgrade script to setup Phabricator\'s database '.
- 'schema.');
+ "Run the storage upgrade script to setup Phabricator's database ".
+ "schema.");
$this->newIssue('storage.upgrade')
->setName(pht('Setup MySQL Schema'))
->setMessage($message)
->setIsFatal(true)
->addCommand(hsprintf('<tt>phabricator/ $</tt> ./bin/storage upgrade'));
} else {
$config['database'] = $namespace.'_meta_data';
$conn_meta = PhabricatorEnv::newObjectFromConfig(
'mysql.implementation',
array($config));
$applied = queryfx_all($conn_meta, 'SELECT patch FROM patch_status');
$applied = ipull($applied, 'patch', 'patch');
$all = PhabricatorSQLPatchList::buildAllPatches();
$diff = array_diff_key($all, $applied);
if ($diff) {
$this->newIssue('storage.patch')
->setName(pht('Upgrade MySQL Schema'))
- ->setMessage(pht(
- "Run the storage upgrade script to upgrade Phabricator's database ".
- "schema. Missing patches:<br />%s<br />",
- phutil_implode_html(phutil_tag('br'), array_keys($diff))))
+ ->setMessage(
+ pht(
+ "Run the storage upgrade script to upgrade Phabricator's ".
+ "database schema. Missing patches:<br />%s<br />",
+ phutil_implode_html(phutil_tag('br'), array_keys($diff))))
->addCommand(
hsprintf('<tt>phabricator/ $</tt> ./bin/storage upgrade'));
}
}
$host = PhabricatorEnv::getEnvConfig('mysql.host');
$matches = null;
if (preg_match('/^([^:]+):(\d+)$/', $host, $matches)) {
$host = $matches[1];
$port = $matches[2];
$this->newIssue('storage.mysql.hostport')
->setName(pht('Deprecated mysql.host Format'))
->setSummary(
pht(
- 'Move port information from `mysql.host` to `mysql.port` in your '.
- 'config.'))
+ 'Move port information from `%s` to `%s` in your config.',
+ 'mysql.host',
+ 'mysql.port'))
->setMessage(
pht(
- 'Your `mysql.host` configuration contains a port number, but '.
- 'this usage is deprecated. Instead, put the port number in '.
- '`mysql.port`.'))
+ 'Your `%s` configuration contains a port number, but this usage '.
+ 'is deprecated. Instead, put the port number in `%s`.',
+ 'mysql.host',
+ 'mysql.port'))
->addPhabricatorConfig('mysql.host')
->addPhabricatorConfig('mysql.port')
->addCommand(
hsprintf(
'<tt>phabricator/ $</tt> ./bin/config set mysql.host %s',
$host))
->addCommand(
hsprintf(
'<tt>phabricator/ $</tt> ./bin/config set mysql.port %s',
$port));
}
}
}
diff --git a/src/applications/config/check/PhabricatorExtraConfigSetupCheck.php b/src/applications/config/check/PhabricatorExtraConfigSetupCheck.php
index a88dff0f2..b3c294b3e 100644
--- a/src/applications/config/check/PhabricatorExtraConfigSetupCheck.php
+++ b/src/applications/config/check/PhabricatorExtraConfigSetupCheck.php
@@ -1,272 +1,278 @@
<?php
final class PhabricatorExtraConfigSetupCheck extends PhabricatorSetupCheck {
public function getDefaultGroup() {
return self::GROUP_OTHER;
}
protected function executeChecks() {
$ancient_config = self::getAncientConfig();
$all_keys = PhabricatorEnv::getAllConfigKeys();
$all_keys = array_keys($all_keys);
sort($all_keys);
$defined_keys = PhabricatorApplicationConfigOptions::loadAllOptions();
foreach ($all_keys as $key) {
if (isset($defined_keys[$key])) {
continue;
}
if (isset($ancient_config[$key])) {
$summary = pht(
'This option has been removed. You may delete it at your '.
'convenience.');
$message = pht(
"The configuration option '%s' has been removed. You may delete ".
"it at your convenience.".
"\n\n%s",
$key,
$ancient_config[$key]);
$short = pht('Obsolete Config');
$name = pht('Obsolete Configuration Option "%s"', $key);
} else {
$summary = pht('This option is not recognized. It may be misspelled.');
$message = pht(
"The configuration option '%s' is not recognized. It may be ".
"misspelled, or it might have existed in an older version of ".
"Phabricator. It has no effect, and should be corrected or deleted.",
$key);
$short = pht('Unknown Config');
$name = pht('Unknown Configuration Option "%s"', $key);
}
$issue = $this->newIssue('config.unknown.'.$key)
->setShortName($short)
->setName($name)
->setSummary($summary);
$stack = PhabricatorEnv::getConfigSourceStack();
$stack = $stack->getStack();
$found = array();
$found_local = false;
$found_database = false;
foreach ($stack as $source_key => $source) {
$value = $source->getKeys(array($key));
if ($value) {
$found[] = $source->getName();
if ($source instanceof PhabricatorConfigDatabaseSource) {
$found_database = true;
}
if ($source instanceof PhabricatorConfigLocalSource) {
$found_local = true;
}
}
}
$message = $message."\n\n".pht(
'This configuration value is defined in these %d '.
'configuration source(s): %s.',
count($found),
implode(', ', $found));
$issue->setMessage($message);
if ($found_local) {
$command = csprintf('phabricator/ $ ./bin/config delete %s', $key);
$issue->addCommand($command);
}
if ($found_database) {
$issue->addPhabricatorConfig($key);
}
}
}
/**
* Return a map of deleted config options. Keys are option keys; values are
* explanations of what happened to the option.
*/
public static function getAncientConfig() {
$reason_auth = pht(
'This option has been migrated to the "Auth" application. Your old '.
'configuration is still in effect, but now stored in "Auth" instead of '.
'configuration. Going forward, you can manage authentication from '.
'the web UI.');
$auth_config = array(
'controller.oauth-registration',
'auth.password-auth-enabled',
'facebook.auth-enabled',
'facebook.registration-enabled',
'facebook.auth-permanent',
'facebook.application-id',
'facebook.application-secret',
'facebook.require-https-auth',
'github.auth-enabled',
'github.registration-enabled',
'github.auth-permanent',
'github.application-id',
'github.application-secret',
'google.auth-enabled',
'google.registration-enabled',
'google.auth-permanent',
'google.application-id',
'google.application-secret',
'ldap.auth-enabled',
'ldap.hostname',
'ldap.port',
'ldap.base_dn',
'ldap.search_attribute',
'ldap.search-first',
'ldap.username-attribute',
'ldap.real_name_attributes',
'ldap.activedirectory_domain',
'ldap.version',
'ldap.referrals',
'ldap.anonymous-user-name',
'ldap.anonymous-user-password',
'ldap.start-tls',
'disqus.auth-enabled',
'disqus.registration-enabled',
'disqus.auth-permanent',
'disqus.application-id',
'disqus.application-secret',
'phabricator.oauth-uri',
'phabricator.auth-enabled',
'phabricator.registration-enabled',
'phabricator.auth-permanent',
'phabricator.application-id',
'phabricator.application-secret',
);
$ancient_config = array_fill_keys($auth_config, $reason_auth);
$markup_reason = pht(
'Custom remarkup rules are now added by subclassing '.
- 'PhabricatorRemarkupCustomInlineRule or '.
- 'PhabricatorRemarkupCustomBlockRule.');
+ '%s or %s.',
+ 'PhabricatorRemarkupCustomInlineRule',
+ 'PhabricatorRemarkupCustomBlockRule');
$session_reason = pht(
'Sessions now expire and are garbage collected rather than having an '.
'arbitrary concurrency limit.');
$differential_field_reason = pht(
'All Differential fields are now managed through the configuration '.
'option "%s". Use that option to configure which fields are shown.',
'differential.fields');
$reply_domain_reason = pht(
'Individual application reply handler domains have been removed. '.
'Configure a reply domain with "%s".',
'metamta.reply-handler-domain');
$reply_handler_reason = pht(
'Reply handlers can no longer be overridden with configuration.');
$monospace_reason = pht(
'Phabricator no longer supports global customization of monospaced '.
'fonts.');
$public_mail_reason = pht(
'Inbound mail addresses are now configured for each application '.
'in the Applications tool.');
$ancient_config += array(
'phid.external-loaders' =>
pht(
- 'External loaders have been replaced. Extend `PhabricatorPHIDType` '.
- 'to implement new PHID and handle types.'),
+ 'External loaders have been replaced. Extend `%s` '.
+ 'to implement new PHID and handle types.',
+ 'PhabricatorPHIDType'),
'maniphest.custom-task-extensions-class' =>
pht(
- 'Maniphest fields are now loaded automatically. You can configure '.
- 'them with `maniphest.fields`.'),
+ 'Maniphest fields are now loaded automatically. '.
+ 'You can configure them with `%s`.',
+ 'maniphest.fields'),
'maniphest.custom-fields' =>
pht(
- 'Maniphest fields are now defined in '.
- '`maniphest.custom-field-definitions`. Existing definitions have '.
- 'been migrated.'),
+ 'Maniphest fields are now defined in `%s`. '.
+ 'Existing definitions have been migrated.',
+ 'maniphest.custom-field-definitions'),
'differential.custom-remarkup-rules' => $markup_reason,
'differential.custom-remarkup-block-rules' => $markup_reason,
'auth.sshkeys.enabled' => pht(
'SSH keys are now actually useful, so they are always enabled.'),
'differential.anonymous-access' => pht(
- 'Phabricator now has meaningful global access controls. See '.
- '`policy.allow-public`.'),
+ 'Phabricator now has meaningful global access controls. See `%s`.',
+ 'policy.allow-public'),
'celerity.resource-path' => pht(
'An alternate resource map is no longer supported. Instead, use '.
'multiple maps. See T4222.'),
'metamta.send-immediately' => pht(
'Mail is now always delivered by the daemons.'),
'auth.sessions.conduit' => $session_reason,
'auth.sessions.web' => $session_reason,
'tokenizer.ondemand' => pht(
'Phabricator now manages typeahead strategies automatically.'),
'differential.revision-custom-detail-renderer' => pht(
'Obsolete; use standard rendering events instead.'),
'differential.show-host-field' => $differential_field_reason,
'differential.show-test-plan-field' => $differential_field_reason,
'differential.field-selector' => $differential_field_reason,
'phabricator.show-beta-applications' => pht(
- 'This option has been renamed to `phabricator.show-prototypes` '.
- 'to emphasize the unfinished nature of many prototype applications. '.
- 'Your existing setting has been migrated.'),
+ 'This option has been renamed to `%s` to emphasize the '.
+ 'unfinished nature of many prototype applications. '.
+ 'Your existing setting has been migrated.',
+ 'phabricator.show-prototypes'),
'notification.user' => pht(
'The notification server no longer requires root permissions. Start '.
'the server as the user you want it to run under.'),
'notification.debug' => pht(
'Notifications no longer have a dedicated debugging mode.'),
'translation.provider' => pht(
'The translation implementation has changed and providers are no '.
'longer used or supported.'),
'config.mask' => pht(
- 'Use `config.hide` instead of this option.'),
+ 'Use `%s` instead of this option.',
+ 'config.hide'),
'phd.start-taskmasters' => pht(
'Taskmasters now use an autoscaling pool. You can configure the '.
- 'pool size with `phd.taskmasters`.'),
+ 'pool size with `%s`.',
+ 'phd.taskmasters'),
'storage.engine-selector' => pht(
'Phabricator now automatically discovers available storage engines '.
'at runtime.'),
'storage.upload-size-limit' => pht(
'Phabricator now supports arbitrarily large files. Consult the '.
'documentation for configuration details.'),
'security.allow-outbound-http' => pht(
- 'This option has been replaced with the more granular option '.
- '`security.outbound-blacklist`.'),
+ 'This option has been replaced with the more granular option `%s`.',
+ 'security.outbound-blacklist'),
'metamta.reply.show-hints' => pht(
'Phabricator no longer shows reply hints in mail.'),
'metamta.differential.reply-handler-domain' => $reply_domain_reason,
'metamta.diffusion.reply-handler-domain' => $reply_domain_reason,
'metamta.macro.reply-handler-domain' => $reply_domain_reason,
'metamta.maniphest.reply-handler-domain' => $reply_domain_reason,
'metamta.pholio.reply-handler-domain' => $reply_domain_reason,
'metamta.diffusion.reply-handler' => $reply_handler_reason,
'metamta.differential.reply-handler' => $reply_handler_reason,
'metamta.maniphest.reply-handler' => $reply_handler_reason,
'metamta.package.reply-handler' => $reply_handler_reason,
'metamta.precedence-bulk' => pht(
'Phabricator now always sends transaction mail with '.
'"Precedence: bulk" to improve deliverability.'),
'style.monospace' => $monospace_reason,
'style.monospace.windows' => $monospace_reason,
'search.engine-selector' => pht(
'Phabricator now automatically discovers available search engines '.
'at runtime.'),
'metamta.files.public-create-email' => $public_mail_reason,
'metamta.maniphest.public-create-email' => $public_mail_reason,
'metamta.maniphest.default-public-author' => $public_mail_reason,
'metamta.paste.public-create-email' => $public_mail_reason,
);
return $ancient_config;
}
}
diff --git a/src/applications/config/check/PhabricatorFileinfoSetupCheck.php b/src/applications/config/check/PhabricatorFileinfoSetupCheck.php
index 4f5013c03..543fc4fb7 100644
--- a/src/applications/config/check/PhabricatorFileinfoSetupCheck.php
+++ b/src/applications/config/check/PhabricatorFileinfoSetupCheck.php
@@ -1,21 +1,23 @@
<?php
final class PhabricatorFileinfoSetupCheck extends PhabricatorSetupCheck {
public function getDefaultGroup() {
return self::GROUP_OTHER;
}
protected function executeChecks() {
if (!extension_loaded('fileinfo')) {
$message = pht(
- "The 'fileinfo' extension is not installed. Without 'fileinfo', ".
+ "The '%s' extension is not installed. Without '%s', ".
"support, Phabricator may not be able to determine the MIME types ".
- "of uploaded files.");
+ "of uploaded files.",
+ 'fileinfo',
+ 'fileinfo');
$this->newIssue('extension.fileinfo')
- ->setName(pht("Missing 'fileinfo' Extension"))
+ ->setName(pht("Missing '%s' Extension", 'fileinfo'))
->setMessage($message);
}
}
}
diff --git a/src/applications/config/check/PhabricatorGDSetupCheck.php b/src/applications/config/check/PhabricatorGDSetupCheck.php
index f738715bc..750702c70 100644
--- a/src/applications/config/check/PhabricatorGDSetupCheck.php
+++ b/src/applications/config/check/PhabricatorGDSetupCheck.php
@@ -1,52 +1,57 @@
<?php
final class PhabricatorGDSetupCheck extends PhabricatorSetupCheck {
public function getDefaultGroup() {
return self::GROUP_OTHER;
}
protected function executeChecks() {
if (!extension_loaded('gd')) {
$message = pht(
- "The 'gd' extension is not installed. Without 'gd', support, ".
+ "The '%s' extension is not installed. Without '%s', support, ".
"Phabricator will not be able to process or resize images ".
- "(for example, to generate thumbnails). Install or enable 'gd'.");
+ "(for example, to generate thumbnails). Install or enable '%s'.",
+ 'gd',
+ 'gd',
+ 'gd');
$this->newIssue('extension.gd')
- ->setName(pht("Missing 'gd' Extension"))
+ ->setName(pht("Missing '%s' Extension", 'gd'))
->setMessage($message);
} else {
$image_type_map = array(
'imagecreatefrompng' => 'PNG',
'imagecreatefromgif' => 'GIF',
'imagecreatefromjpeg' => 'JPEG',
);
$have = array();
foreach ($image_type_map as $function => $image_type) {
if (function_exists($function)) {
$have[] = $image_type;
}
}
$missing = array_diff($image_type_map, $have);
if ($missing) {
$missing = implode(', ', $missing);
$have = implode(', ', $have);
$message = pht(
- "The 'gd' extension has support for only some image types. ".
+ "The '%s' extension has support for only some image types. ".
"Phabricator will be unable to process images of the missing ".
- "types until you build 'gd' with support for them. ".
+ "types until you build '%s' with support for them. ".
"Supported types: %s. Missing types: %s.",
+ 'gd',
+ 'gd',
$have,
$missing);
$this->newIssue('extension.gd.support')
- ->setName(pht("Partial 'gd' Support"))
+ ->setName(pht("Partial '%s' Support", 'gd'))
->setMessage($message);
}
}
}
}
diff --git a/src/applications/config/check/PhabricatorImagemagickSetupCheck.php b/src/applications/config/check/PhabricatorImagemagickSetupCheck.php
index 55c538f3a..a69781132 100644
--- a/src/applications/config/check/PhabricatorImagemagickSetupCheck.php
+++ b/src/applications/config/check/PhabricatorImagemagickSetupCheck.php
@@ -1,27 +1,29 @@
<?php
final class PhabricatorImagemagickSetupCheck extends PhabricatorSetupCheck {
public function getDefaultGroup() {
return self::GROUP_OTHER;
}
protected function executeChecks() {
$imagemagick = PhabricatorEnv::getEnvConfig('files.enable-imagemagick');
if ($imagemagick) {
if (!Filesystem::binaryExists('convert')) {
$message = pht(
- 'You have enabled Imagemagick in your config, but the \'convert\' '.
- 'binary is not in the webserver\'s $PATH. Disable imagemagick '.
- 'or make it available to the webserver.');
+ "You have enabled Imagemagick in your config, but the '%s' ".
+ "binary is not in the webserver's %s. Disable imagemagick ".
+ "or make it available to the webserver.",
+ 'convert',
+ '$PATH');
$this->newIssue('files.enable-imagemagick')
->setName(pht(
- "'convert' binary not found or Imagemagick is not installed."))
+ "'%s' binary not found or Imagemagick is not installed.", 'convert'))
->setMessage($message)
->addRelatedPhabricatorConfig('files.enable-imagemagick')
->addPhabricatorConfig('environment.append-paths');
}
}
}
}
diff --git a/src/applications/config/check/PhabricatorMySQLSetupCheck.php b/src/applications/config/check/PhabricatorMySQLSetupCheck.php
index a5178cc9e..75ecc0623 100644
--- a/src/applications/config/check/PhabricatorMySQLSetupCheck.php
+++ b/src/applications/config/check/PhabricatorMySQLSetupCheck.php
@@ -1,348 +1,349 @@
<?php
final class PhabricatorMySQLSetupCheck extends PhabricatorSetupCheck {
public function getDefaultGroup() {
return self::GROUP_MYSQL;
}
public static function loadRawConfigValue($key) {
$conn_raw = id(new PhabricatorUser())->establishConnection('w');
try {
$value = queryfx_one($conn_raw, 'SELECT @@%Q', $key);
$value = $value['@@'.$key];
} catch (AphrontQueryException $ex) {
$value = null;
}
return $value;
}
protected function executeChecks() {
$max_allowed_packet = self::loadRawConfigValue('max_allowed_packet');
// This primarily supports setting the filesize limit for MySQL to 8MB,
// which may produce a >16MB packet after escaping.
$recommended_minimum = (32 * 1024 * 1024);
if ($max_allowed_packet < $recommended_minimum) {
$message = pht(
- "MySQL is configured with a small 'max_allowed_packet' (%d), ".
+ "MySQL is configured with a small '%s' (%d), ".
"which may cause some large writes to fail. Strongly consider raising ".
"this to at least %d in your MySQL configuration.",
+ 'max_allowed_packet',
$max_allowed_packet,
$recommended_minimum);
$this->newIssue('mysql.max_allowed_packet')
- ->setName(pht('Small MySQL "max_allowed_packet"'))
+ ->setName(pht('Small MySQL "%s"', 'max_allowed_packet'))
->setMessage($message)
->addMySQLConfig('max_allowed_packet');
}
$modes = self::loadRawConfigValue('sql_mode');
$modes = explode(',', $modes);
if (!in_array('STRICT_ALL_TABLES', $modes)) {
$summary = pht(
'MySQL is not in strict mode, but using strict mode is strongly '.
'encouraged.');
$message = pht(
"On your MySQL instance, the global %s is not set to %s. ".
"It is strongly encouraged that you enable this mode when running ".
"Phabricator.\n\n".
"By default MySQL will silently ignore some types of errors, which ".
"can cause data loss and raise security concerns. Enabling strict ".
"mode makes MySQL raise an explicit error instead, and prevents this ".
"entire class of problems from doing any damage.\n\n".
"You can find more information about this mode (and how to configure ".
"it) in the MySQL manual. Usually, it is sufficient to add this to ".
"your %s file (in the %s section) and then restart %s:\n\n".
"%s\n".
"(Note that if you run other applications against the same database, ".
"they may not work in strict mode. Be careful about enabling it in ".
"these cases.)",
phutil_tag('tt', array(), 'sql_mode'),
phutil_tag('tt', array(), 'STRICT_ALL_TABLES'),
phutil_tag('tt', array(), 'my.cnf'),
phutil_tag('tt', array(), '[mysqld]'),
phutil_tag('tt', array(), 'mysqld'),
phutil_tag('pre', array(), 'sql_mode=STRICT_ALL_TABLES'));
$this->newIssue('mysql.mode')
- ->setName(pht('MySQL STRICT_ALL_TABLES Mode Not Set'))
+ ->setName(pht('MySQL %s Mode Not Set', 'STRICT_ALL_TABLES'))
->setSummary($summary)
->setMessage($message)
->addMySQLConfig('sql_mode');
}
if (in_array('ONLY_FULL_GROUP_BY', $modes)) {
$summary = pht(
'MySQL is in ONLY_FULL_GROUP_BY mode, but using this mode is strongly '.
'discouraged.');
$message = pht(
"On your MySQL instance, the global %s is set to %s. ".
"It is strongly encouraged that you disable this mode when running ".
"Phabricator.\n\n".
"With %s enabled, MySQL rejects queries for which the select list ".
"or (as of MySQL 5.0.23) %s list refer to nonaggregated columns ".
"that are not named in the %s clause. More importantly, Phabricator ".
"does not work properly with this mode enabled.\n\n".
"You can find more information about this mode (and how to configure ".
"it) in the MySQL manual. Usually, it is sufficient to change the %s ".
"in your %s file (in the %s section) and then restart %s:\n\n".
"%s\n".
"(Note that if you run other applications against the same database, ".
"they may not work with %s. Be careful about enabling ".
"it in these cases and consider migrating Phabricator to a different ".
"database.)",
phutil_tag('tt', array(), 'sql_mode'),
phutil_tag('tt', array(), 'ONLY_FULL_GROUP_BY'),
phutil_tag('tt', array(), 'ONLY_FULL_GROUP_BY'),
phutil_tag('tt', array(), 'HAVING'),
phutil_tag('tt', array(), 'GROUP BY'),
phutil_tag('tt', array(), 'sql_mode'),
phutil_tag('tt', array(), 'my.cnf'),
phutil_tag('tt', array(), '[mysqld]'),
phutil_tag('tt', array(), 'mysqld'),
phutil_tag('pre', array(), 'sql_mode=STRICT_ALL_TABLES'),
phutil_tag('tt', array(), 'ONLY_FULL_GROUP_BY'));
$this->newIssue('mysql.mode')
- ->setName(pht('MySQL ONLY_FULL_GROUP_BY Mode Set'))
+ ->setName(pht('MySQL %s Mode Set', 'ONLY_FULL_GROUP_BY'))
->setSummary($summary)
->setMessage($message)
->addMySQLConfig('sql_mode');
}
$stopword_file = self::loadRawConfigValue('ft_stopword_file');
if ($this->shouldUseMySQLSearchEngine()) {
if ($stopword_file === null) {
$summary = pht(
'Your version of MySQL does not support configuration of a '.
'stopword file. You will not be able to find search results for '.
'common words.');
$message = pht(
"Your MySQL instance does not support the %s option. You will not ".
"be able to find search results for common words. You can gain ".
"access to this option by upgrading MySQL to a more recent ".
"version.\n\n".
"You can ignore this warning if you plan to configure ElasticSearch ".
"later, or aren't concerned about searching for common words.",
phutil_tag('tt', array(), 'ft_stopword_file'));
$this->newIssue('mysql.ft_stopword_file')
- ->setName(pht('MySQL ft_stopword_file Not Supported'))
+ ->setName(pht('MySQL %s Not Supported', 'ft_stopword_file'))
->setSummary($summary)
->setMessage($message)
->addMySQLConfig('ft_stopword_file');
} else if ($stopword_file == '(built-in)') {
$root = dirname(phutil_get_library_root('phabricator'));
$stopword_path = $root.'/resources/sql/stopwords.txt';
$stopword_path = Filesystem::resolvePath($stopword_path);
$namespace = PhabricatorEnv::getEnvConfig('storage.default-namespace');
$summary = pht(
'MySQL is using a default stopword file, which will prevent '.
'searching for many common words.');
$message = pht(
"Your MySQL instance is using the builtin stopword file for ".
"building search indexes. This can make Phabricator's search ".
"feature less useful.\n\n".
"Stopwords are common words which are not indexed and thus can not ".
"be searched for. The default stopword file has about 500 words, ".
"including various words which you are likely to wish to search ".
"for, such as 'various', 'likely', 'wish', and 'zero'.\n\n".
"To make search more useful, you can use an alternate stopword ".
"file with fewer words. Alternatively, if you aren't concerned ".
"about searching for common words, you can ignore this warning. ".
"If you later plan to configure ElasticSearch, you can also ignore ".
"this warning: this stopword file only affects MySQL fulltext ".
"indexes.\n\n".
"To choose a different stopword file, add this to your %s file ".
"(in the %s section) and then restart %s:\n\n".
"%s\n".
"(You can also use a different file if you prefer. The file ".
"suggested above has about 50 of the most common English words.)\n\n".
"Finally, run this command to rebuild indexes using the new ".
"rules:\n\n".
"%s",
phutil_tag('tt', array(), 'my.cnf'),
phutil_tag('tt', array(), '[mysqld]'),
phutil_tag('tt', array(), 'mysqld'),
phutil_tag('pre', array(), 'ft_stopword_file='.$stopword_path),
phutil_tag(
'pre',
array(),
"mysql> REPAIR TABLE {$namespace}_search.search_documentfield;"));
$this->newIssue('mysql.ft_stopword_file')
->setName(pht('MySQL is Using Default Stopword File'))
->setSummary($summary)
->setMessage($message)
->addMySQLConfig('ft_stopword_file');
}
}
$min_len = self::loadRawConfigValue('ft_min_word_len');
if ($min_len >= 4) {
if ($this->shouldUseMySQLSearchEngine()) {
$namespace = PhabricatorEnv::getEnvConfig('storage.default-namespace');
$summary = pht(
'MySQL is configured to only index words with at least %d '.
'characters.',
$min_len);
$message = pht(
"Your MySQL instance is configured to use the default minimum word ".
"length when building search indexes, which is 4. This means words ".
"which are only 3 characters long will not be indexed and can not ".
"be searched for.\n\n".
"For example, you will not be able to find search results for words ".
"like 'SMS', 'web', or 'DOS'.\n\n".
"You can change this setting to 3 to allow these words to be ".
"indexed. Alternatively, you can ignore this warning if you are ".
"not concerned about searching for 3-letter words. If you later ".
"plan to configure ElasticSearch, you can also ignore this warning: ".
"only MySQL fulltext search is affected.\n\n".
"To reduce the minimum word length to 3, add this to your %s file ".
"(in the %s section) and then restart %s:\n\n".
"%s\n".
"Finally, run this command to rebuild indexes using the new ".
"rules:\n\n".
"%s",
phutil_tag('tt', array(), 'my.cnf'),
phutil_tag('tt', array(), '[mysqld]'),
phutil_tag('tt', array(), 'mysqld'),
phutil_tag('pre', array(), 'ft_min_word_len=3'),
phutil_tag(
'pre',
array(),
"mysql> REPAIR TABLE {$namespace}_search.search_documentfield;"));
$this->newIssue('mysql.ft_min_word_len')
->setName(pht('MySQL is Using Default Minimum Word Length'))
->setSummary($summary)
->setMessage($message)
->addMySQLConfig('ft_min_word_len');
}
}
$bool_syntax = self::loadRawConfigValue('ft_boolean_syntax');
if ($bool_syntax != ' |-><()~*:""&^') {
if ($this->shouldUseMySQLSearchEngine()) {
$summary = pht(
'MySQL is configured to search on fulltext indexes using "OR" by '.
'default. Using "AND" is usually the desired behaviour.');
$message = pht(
"Your MySQL instance is configured to use the default Boolean ".
"search syntax when using fulltext indexes. This means searching ".
"for 'search words' will yield the query 'search OR words' ".
"instead of the desired 'search AND words'.\n\n".
"This might produce unexpected search results. \n\n".
"You can change this setting to a more sensible default. ".
"Alternatively, you can ignore this warning if ".
"using 'OR' is the desired behaviour. If you later plan ".
"to configure ElasticSearch, you can also ignore this warning: ".
"only MySQL fulltext search is affected.\n\n".
"To change this setting, add this to your %s file ".
"(in the %s section) and then restart %s:\n\n".
"%s\n",
phutil_tag('tt', array(), 'my.cnf'),
phutil_tag('tt', array(), '[mysqld]'),
phutil_tag('tt', array(), 'mysqld'),
phutil_tag('pre', array(), 'ft_boolean_syntax=\' |-><()~*:""&^\''));
$this->newIssue('mysql.ft_boolean_syntax')
->setName(pht('MySQL is Using the Default Boolean Syntax'))
->setSummary($summary)
->setMessage($message)
->addMySQLConfig('ft_boolean_syntax');
}
}
$innodb_pool = self::loadRawConfigValue('innodb_buffer_pool_size');
$innodb_bytes = phutil_parse_bytes($innodb_pool);
$innodb_readable = phutil_format_bytes($innodb_bytes);
// This is arbitrary and just trying to detect values that the user
// probably didn't set themselves. The Mac OS X default is 128MB and
// 40% of an AWS EC2 Micro instance is 245MB, so keeping it somewhere
// between those two values seems like a reasonable approximation.
$minimum_readable = '225MB';
$minimum_bytes = phutil_parse_bytes($minimum_readable);
if ($innodb_bytes < $minimum_bytes) {
$summary = pht(
'MySQL is configured with a very small innodb_buffer_pool_size, '.
'which may impact performance.');
$message = pht(
"Your MySQL instance is configured with a very small %s (%s). ".
"This may cause poor database performance and lock exhaustion.\n\n".
"There are no hard-and-fast rules to setting an appropriate value, ".
"but a reasonable starting point for a standard install is something ".
"like 40%% of the total memory on the machine. For example, if you ".
"have 4GB of RAM on the machine you have installed Phabricator on, ".
"you might set this value to %s.\n\n".
"You can read more about this option in the MySQL documentation to ".
"help you make a decision about how to configure it for your use ".
"case. There are no concerns specific to Phabricator which make it ".
"different from normal workloads with respect to this setting.\n\n".
"To adjust the setting, add something like this to your %s file (in ".
"the %s section), replacing %s with an appropriate value for your ".
"host and use case. Then restart %s:\n\n".
"%s\n".
"If you're satisfied with the current setting, you can safely ".
"ignore this setup warning.",
phutil_tag('tt', array(), 'innodb_buffer_pool_size'),
phutil_tag('tt', array(), $innodb_readable),
phutil_tag('tt', array(), '1600M'),
phutil_tag('tt', array(), 'my.cnf'),
phutil_tag('tt', array(), '[mysqld]'),
phutil_tag('tt', array(), '1600M'),
phutil_tag('tt', array(), 'mysqld'),
phutil_tag('pre', array(), 'innodb_buffer_pool_size=1600M'));
$this->newIssue('mysql.innodb_buffer_pool_size')
->setName(pht('MySQL May Run Slowly'))
->setSummary($summary)
->setMessage($message)
->addMySQLConfig('innodb_buffer_pool_size');
}
$ok = PhabricatorStorageManagementAPI::isCharacterSetAvailableOnConnection(
'utf8mb4',
id(new PhabricatorUser())->establishConnection('w'));
if (!$ok) {
$summary = pht(
'You are using an old version of MySQL, and should upgrade.');
$message = pht(
'You are using an old version of MySQL which has poor unicode '.
'support (it does not support the "utf8mb4" collation set). You will '.
'encounter limitations when working with some unicode data.'.
"\n\n".
'We strongly recommend you upgrade to MySQL 5.5 or newer.');
$this->newIssue('mysql.utf8mb4')
->setName(pht('Old MySQL Version'))
->setSummary($summary)
->setMessage($message);
}
}
protected function shouldUseMySQLSearchEngine() {
$search_engine = PhabricatorSearchEngine::loadEngine();
return $search_engine instanceof PhabricatorMySQLSearchEngine;
}
}
diff --git a/src/applications/config/check/PhabricatorPHPConfigSetupCheck.php b/src/applications/config/check/PhabricatorPHPConfigSetupCheck.php
index 57d97226a..03bbb3c52 100644
--- a/src/applications/config/check/PhabricatorPHPConfigSetupCheck.php
+++ b/src/applications/config/check/PhabricatorPHPConfigSetupCheck.php
@@ -1,185 +1,184 @@
<?php
final class PhabricatorPHPConfigSetupCheck extends PhabricatorSetupCheck {
public function getDefaultGroup() {
return self::GROUP_PHP;
}
public function getExecutionOrder() {
return 0;
}
protected function executeChecks() {
$safe_mode = ini_get('safe_mode');
if ($safe_mode) {
$message = pht(
- "You have 'safe_mode' enabled in your PHP configuration, but ".
- "Phabricator will not run in safe mode. Safe mode has been deprecated ".
- "in PHP 5.3 and removed in PHP 5.4.".
- "\n\n".
- "Disable safe mode to continue.");
+ "You have '%s' enabled in your PHP configuration, but Phabricator ".
+ "will not run in safe mode. Safe mode has been deprecated in PHP 5.3 ".
+ "and removed in PHP 5.4.\n\nDisable safe mode to continue.",
+ 'safe_mode');
$this->newIssue('php.safe_mode')
->setIsFatal(true)
- ->setName(pht('Disable PHP safe_mode'))
+ ->setName(pht('Disable PHP %s', 'safe_mode'))
->setMessage($message)
->addPHPConfig('safe_mode');
return;
}
// Check for `disable_functions` or `disable_classes`. Although it's
// possible to disable a bunch of functions (say, `array_change_key_case()`)
// and classes and still have Phabricator work fine, it's unreasonably
// difficult for us to be sure we'll even survive setup if these options
// are enabled. Phabricator needs access to the most dangerous functions,
// so there is no reasonable configuration value here which actually
// provides a benefit while guaranteeing Phabricator will run properly.
$disable_options = array('disable_functions', 'disable_classes');
foreach ($disable_options as $disable_option) {
$disable_value = ini_get($disable_option);
if ($disable_value) {
// By default Debian installs the pcntl extension but disables all of
// its functions using configuration. Whitelist disabling these
// functions so that Debian PHP works out of the box (we do not need to
// call these functions from the web UI). This is pretty ridiculous but
// it's not the users' fault and they haven't done anything crazy to
// get here, so don't make them pay for Debian's unusual choices.
// See: http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=605571
$fatal = true;
if ($disable_option == 'disable_functions') {
$functions = preg_split('/[, ]+/', $disable_value);
$functions = array_filter($functions);
foreach ($functions as $k => $function) {
if (preg_match('/^pcntl_/', $function)) {
unset($functions[$k]);
}
}
if (!$functions) {
$fatal = false;
}
}
if ($fatal) {
$message = pht(
"You have '%s' enabled in your PHP configuration.\n\n".
"This option is not compatible with Phabricator. Remove ".
"'%s' from your configuration to continue.",
$disable_option,
$disable_option);
$this->newIssue('php.'.$disable_option)
->setIsFatal(true)
->setName(pht('Remove PHP %s', $disable_option))
->setMessage($message)
->addPHPConfig($disable_option);
}
}
}
$overload_option = 'mbstring.func_overload';
$func_overload = ini_get($overload_option);
if ($func_overload) {
$message = pht(
"You have '%s' enabled in your PHP configuration.\n\n".
"This option is not compatible with Phabricator. Disable ".
"'%s' in your PHP configuration to continue.",
$overload_option,
$overload_option);
$this->newIssue('php'.$overload_option)
->setIsFatal(true)
->setName(pht('Disable PHP %s', $overload_option))
->setMessage($message)
->addPHPConfig($overload_option);
}
$open_basedir = ini_get('open_basedir');
if ($open_basedir) {
// 'open_basedir' restricts which files we're allowed to access with
// file operations. This might be okay -- we don't need to write to
// arbitrary places in the filesystem -- but we need to access certain
// resources. This setting is unlikely to be providing any real measure
// of security so warn even if things look OK.
$failures = array();
try {
$open_libphutil = class_exists('Future');
} catch (Exception $ex) {
$failures[] = $ex->getMessage();
}
try {
$open_arcanist = class_exists('ArcanistDiffParser');
} catch (Exception $ex) {
$failures[] = $ex->getMessage();
}
$open_urandom = false;
try {
Filesystem::readRandomBytes(1);
$open_urandom = true;
} catch (FilesystemException $ex) {
$failures[] = $ex->getMessage();
}
try {
$tmp = new TempFile();
file_put_contents($tmp, '.');
$open_tmp = @fopen((string)$tmp, 'r');
if (!$open_tmp) {
$failures[] = pht(
"Unable to read temporary file '%s'.",
(string)$tmp);
}
} catch (Exception $ex) {
$message = $ex->getMessage();
$dir = sys_get_temp_dir();
$failures[] = pht(
"Unable to open temp files from '%s': %s",
$dir,
$message);
}
$issue = $this->newIssue('php.open_basedir')
- ->setName(pht('Disable PHP open_basedir'))
+ ->setName(pht('Disable PHP %s', 'open_basedir'))
->addPHPConfig('open_basedir');
if ($failures) {
$message = pht(
- "Your server is configured with 'open_basedir', which prevents ".
- "Phabricator from opening files it requires access to.".
- "\n\n".
- "Disable this setting to continue.".
- "\n\n".
- "Failures:\n\n%s",
+ "Your server is configured with '%s', which prevents Phabricator ".
+ "from opening files it requires access to.\n\n".
+ "Disable this setting to continue.\n\nFailures:\n\n%s",
+ 'open_basedir',
implode("\n\n", $failures));
$issue
->setIsFatal(true)
->setMessage($message);
return;
} else {
$summary = pht(
- "You have 'open_basedir' configured in your PHP settings, which ".
- "may cause some features to fail.");
+ "You have '%s' configured in your PHP settings, which ".
+ "may cause some features to fail.",
+ 'open_basedir');
$message = pht(
- "You have 'open_basedir' configured in your PHP settings. Although ".
- "this setting appears permissive enough that Phabricator will ".
- "work properly, you may still run into problems because of it.".
- "\n\n".
- "Consider disabling 'open_basedir'.");
+ "You have '%s' configured in your PHP settings. Although this ".
+ "setting appears permissive enough that Phabricator will work ".
+ "properly, you may still run into problems because of it.\n\n".
+ "Consider disabling '%s'.",
+ 'open_basedir',
+ 'open_basedir');
$issue
->setSummary($summary)
->setMessage($message);
}
}
}
}
diff --git a/src/applications/config/check/PhabricatorPathSetupCheck.php b/src/applications/config/check/PhabricatorPathSetupCheck.php
index 4833e4861..618c81abb 100644
--- a/src/applications/config/check/PhabricatorPathSetupCheck.php
+++ b/src/applications/config/check/PhabricatorPathSetupCheck.php
@@ -1,129 +1,136 @@
<?php
final class PhabricatorPathSetupCheck extends PhabricatorSetupCheck {
public function getDefaultGroup() {
return self::GROUP_OTHER;
}
protected function executeChecks() {
// NOTE: We've already appended `environment.append-paths`, so we don't
// need to explicitly check for it.
$path = getenv('PATH');
if (!$path) {
$summary = pht(
- 'The environmental variable $PATH is empty. Phabricator will not '.
- 'be able to execute some commands.');
+ 'The environmental variable %s is empty. Phabricator will not '.
+ 'be able to execute some commands.',
+ '$PATH');
$message = pht(
- 'The environmental variable $PATH is empty. Phabricator needs to '.
- 'execute some system commands, like `svn`, `git`, `hg`, and `diff`. '.
- 'To execute these commands, the binaries must be available in the '.
- 'webserver\'s $PATH. You can set additional paths in Phabricator '.
- 'configuration.');
+ "The environmental variable %s is empty. Phabricator needs to execute ".
+ "some system commands, like `%s`, `%s`, `%s`, and `%s`. To execute ".
+ "these commands, the binaries must be available in the webserver's ".
+ "%s. You can set additional paths in Phabricator configuration.",
+ '$PATH',
+ 'svn',
+ 'git',
+ 'hg',
+ 'diff',
+ '$PATH');
$this
->newIssue('config.environment.append-paths')
- ->setName(pht('$PATH Not Set'))
+ ->setName(pht('%s Not Set', '$PATH'))
->setSummary($summary)
->setMessage($message)
->addPhabricatorConfig('environment.append-paths');
// Bail on checks below.
return;
}
// Users are remarkably industrious at misconfiguring software. Try to
// catch mistaken configuration of PATH.
$path_parts = explode(PATH_SEPARATOR, $path);
$bad_paths = array();
foreach ($path_parts as $path_part) {
if (!strlen($path_part)) {
continue;
}
$message = null;
$not_exists = false;
foreach (Filesystem::walkToRoot($path_part) as $part) {
if (!Filesystem::pathExists($part)) {
$not_exists = $part;
// Walk up so we can tell if this is a readability issue or not.
continue;
} else if (!is_dir(Filesystem::resolvePath($part))) {
$message = pht(
"The PATH component '%s' (which resolves as the absolute path ".
"'%s') is not usable because '%s' is not a directory.",
$path_part,
Filesystem::resolvePath($path_part),
$part);
} else if (!is_readable($part)) {
$message = pht(
"The PATH component '%s' (which resolves as the absolute path ".
"'%s') is not usable because '%s' is not readable.",
$path_part,
Filesystem::resolvePath($path_part),
$part);
} else if ($not_exists) {
$message = pht(
"The PATH component '%s' (which resolves as the absolute path ".
"'%s') is not usable because '%s' does not exist.",
$path_part,
Filesystem::resolvePath($path_part),
$not_exists);
} else {
// Everything seems good.
break;
}
if ($message !== null) {
break;
}
}
if ($message === null) {
if (!phutil_is_windows() && !@file_exists($path_part.'/.')) {
$message = pht(
"The PATH component '%s' (which resolves as the absolute path ".
- "'%s') is not usable because it is not traversable (its '+x' ".
+ "'%s') is not usable because it is not traversable (its '%s' ".
"permission bit is not set).",
$path_part,
- Filesystem::resolvePath($path_part));
+ Filesystem::resolvePath($path_part),
+ '+x');
}
}
if ($message !== null) {
$bad_paths[$path_part] = $message;
}
}
if ($bad_paths) {
foreach ($bad_paths as $path_part => $message) {
$digest = substr(PhabricatorHash::digest($path_part), 0, 8);
$this
->newIssue('config.PATH.'.$digest)
->setName(pht('$PATH Component Unusable'))
->setSummary(
pht(
'A component of the configured PATH can not be used by '.
'the webserver: %s',
$path_part))
->setMessage(
pht(
"The configured PATH includes a component which is not usable. ".
"Phabricator will be unable to find or execute binaries located ".
"here:".
"\n\n".
"%s".
"\n\n".
"The user that the webserver runs as must be able to read all ".
"the directories in PATH in order to make use of them.",
$message))
->addPhabricatorConfig('environment.append-paths');
}
}
}
}
diff --git a/src/applications/config/check/PhabricatorPygmentSetupCheck.php b/src/applications/config/check/PhabricatorPygmentSetupCheck.php
index 26f1f4633..228eb7a33 100644
--- a/src/applications/config/check/PhabricatorPygmentSetupCheck.php
+++ b/src/applications/config/check/PhabricatorPygmentSetupCheck.php
@@ -1,73 +1,83 @@
<?php
final class PhabricatorPygmentSetupCheck extends PhabricatorSetupCheck {
public function getDefaultGroup() {
return self::GROUP_OTHER;
}
protected function executeChecks() {
$pygment = PhabricatorEnv::getEnvConfig('pygments.enabled');
if ($pygment) {
if (!Filesystem::binaryExists('pygmentize')) {
$summary = pht(
- 'You enabled pygments but the pygmentize script is not '.
- 'actually available, your $PATH is probably broken.');
+ 'You enabled pygments but the %s script is not '.
+ 'actually available, your %s is probably broken.',
+ 'pygmentize',
+ '$PATH');
$message = pht(
- 'The environmental variable $PATH does not contain '.
- 'pygmentize. You have enabled pygments, which requires '.
- 'pygmentize to be available in your $PATH variable.');
+ 'The environmental variable %s does not contain %s. '.
+ 'You have enabled pygments, which requires '.
+ '%s to be available in your %s variable.',
+ '$PATH',
+ 'pygmentize',
+ 'pygmentize',
+ '$PATH');
$this
->newIssue('pygments.enabled')
- ->setName(pht('pygmentize Not Found'))
+ ->setName(pht('%s Not Found', 'pygmentize'))
->setSummary($summary)
->setMessage($message)
->addRelatedPhabricatorConfig('pygments.enabled')
->addPhabricatorConfig('environment.append-paths');
} else {
list($err) = exec_manual('pygmentize -h');
if ($err) {
$summary = pht(
- 'You have enabled pygments and the pygmentize script is '.
- 'available, but does not seem to work.');
+ 'You have enabled pygments and the %s script is '.
+ 'available, but does not seem to work.',
+ 'pygmentize');
$message = pht(
- 'Phabricator has %s available in $PATH, but the binary '.
+ 'Phabricator has %s available in %s, but the binary '.
'exited with an error code when run as %s. Check that it is '.
'installed correctly.',
+ phutil_tag('tt', array(), '$PATH'),
phutil_tag('tt', array(), 'pygmentize'),
phutil_tag('tt', array(), 'pygmentize -h'));
$this
->newIssue('pygments.failed')
- ->setName(pht('pygmentize Not Working'))
+ ->setName(pht('%s Not Working', 'pygmentize'))
->setSummary($summary)
->setMessage($message)
->addRelatedPhabricatorConfig('pygments.enabled')
->addPhabricatorConfig('environment.append-paths');
}
}
} else {
- $summary = pht('Pygments should be installed and enabled '.
+ $summary = pht(
+ 'Pygments should be installed and enabled '.
'to provide advanced syntax highlighting.');
- $message = pht('Phabricator can highlight a few languages by default, '.
+ $message = pht(
+ 'Phabricator can highlight a few languages by default, '.
'but installing and enabling Pygments (a third-party highlighting '.
- 'tool) will add syntax highlighting for many more languages. '."\n\n".
+ "tool) will add syntax highlighting for many more languages. \n\n".
'For instructions on installing and enabling Pygments, see the '.
'%s configuration option.'."\n\n".
'If you do not want to install Pygments, you can ignore this issue.',
phutil_tag('tt', array(), 'pygments.enabled'));
$this
->newIssue('pygments.noenabled')
->setName(pht('Install Pygments to Improve Syntax Highlighting'))
->setSummary($summary)
->setMessage($message)
->addRelatedPhabricatorConfig('pygments.enabled');
}
}
}
diff --git a/src/applications/config/check/PhabricatorSetupCheck.php b/src/applications/config/check/PhabricatorSetupCheck.php
index a689d1494..beb52de4d 100644
--- a/src/applications/config/check/PhabricatorSetupCheck.php
+++ b/src/applications/config/check/PhabricatorSetupCheck.php
@@ -1,170 +1,172 @@
<?php
abstract class PhabricatorSetupCheck {
private $issues;
abstract protected function executeChecks();
const GROUP_OTHER = 'other';
const GROUP_MYSQL = 'mysql';
const GROUP_PHP = 'php';
const GROUP_IMPORTANT = 'important';
public function getExecutionOrder() {
return 1;
}
final protected function newIssue($key) {
$issue = id(new PhabricatorSetupIssue())
->setIssueKey($key);
$this->issues[$key] = $issue;
if ($this->getDefaultGroup()) {
$issue->setGroup($this->getDefaultGroup());
}
return $issue;
}
final public function getIssues() {
return $this->issues;
}
protected function addIssue(PhabricatorSetupIssue $issue) {
$this->issues[$issue->getIssueKey()] = $issue;
return $this;
}
public function getDefaultGroup() {
return null;
}
final public function runSetupChecks() {
$this->issues = array();
$this->executeChecks();
}
final public static function getOpenSetupIssueKeys() {
$cache = PhabricatorCaches::getSetupCache();
return $cache->getKey('phabricator.setup.issue-keys');
}
final public static function setOpenSetupIssueKeys(array $keys) {
$cache = PhabricatorCaches::getSetupCache();
$cache->setKey('phabricator.setup.issue-keys', $keys);
}
final public static function getUnignoredIssueKeys(array $all_issues) {
assert_instances_of($all_issues, 'PhabricatorSetupIssue');
$keys = array();
foreach ($all_issues as $issue) {
if (!$issue->getIsIgnored()) {
$keys[] = $issue->getIssueKey();
}
}
return $keys;
}
final public static function getConfigNeedsRepair() {
$cache = PhabricatorCaches::getSetupCache();
return $cache->getKey('phabricator.setup.needs-repair');
}
final public static function setConfigNeedsRepair($needs_repair) {
$cache = PhabricatorCaches::getSetupCache();
$cache->setKey('phabricator.setup.needs-repair', $needs_repair);
}
final public static function deleteSetupCheckCache() {
$cache = PhabricatorCaches::getSetupCache();
$cache->deleteKeys(
array(
'phabricator.setup.needs-repair',
'phabricator.setup.issue-keys',
));
}
final public static function willProcessRequest() {
$issue_keys = self::getOpenSetupIssueKeys();
if ($issue_keys === null) {
$issues = self::runAllChecks();
foreach ($issues as $issue) {
if ($issue->getIsFatal()) {
$view = id(new PhabricatorSetupIssueView())
->setIssue($issue);
return id(new PhabricatorConfigResponse())
->setView($view);
}
}
self::setOpenSetupIssueKeys(self::getUnignoredIssueKeys($issues));
}
// Try to repair configuration unless we have a clean bill of health on it.
// We need to keep doing this on every page load until all the problems
// are fixed, which is why it's separate from setup checks (which run
// once per restart).
$needs_repair = self::getConfigNeedsRepair();
if ($needs_repair !== false) {
$needs_repair = self::repairConfig();
self::setConfigNeedsRepair($needs_repair);
}
}
final public static function runAllChecks() {
$symbols = id(new PhutilSymbolLoader())
->setAncestorClass(__CLASS__)
->setConcreteOnly(true)
->selectAndLoadSymbols();
$checks = array();
foreach ($symbols as $symbol) {
$checks[] = newv($symbol['name'], array());
}
$checks = msort($checks, 'getExecutionOrder');
$issues = array();
foreach ($checks as $check) {
$check->runSetupChecks();
foreach ($check->getIssues() as $key => $issue) {
if (isset($issues[$key])) {
throw new Exception(
- "Two setup checks raised an issue with key '{$key}'!");
+ pht(
+ "Two setup checks raised an issue with key '%s'!",
+ $key));
}
$issues[$key] = $issue;
if ($issue->getIsFatal()) {
break 2;
}
}
}
- foreach (PhabricatorEnv::getEnvConfig('config.ignore-issues')
- as $ignorable => $derp) {
+ $ignore_issues = PhabricatorEnv::getEnvConfig('config.ignore-issues');
+ foreach ($ignore_issues as $ignorable => $derp) {
if (isset($issues[$ignorable])) {
$issues[$ignorable]->setIsIgnored(true);
}
}
return $issues;
}
final public static function repairConfig() {
$needs_repair = false;
$options = PhabricatorApplicationConfigOptions::loadAllOptions();
foreach ($options as $option) {
try {
$option->getGroup()->validateOption(
$option,
PhabricatorEnv::getEnvConfig($option->getKey()));
} catch (PhabricatorConfigValidationException $ex) {
PhabricatorEnv::repairConfig($option->getKey(), $option->getDefault());
$needs_repair = true;
}
}
return $needs_repair;
}
}
diff --git a/src/applications/config/check/PhabricatorTimezoneSetupCheck.php b/src/applications/config/check/PhabricatorTimezoneSetupCheck.php
index 5fcf74a29..bf087c933 100644
--- a/src/applications/config/check/PhabricatorTimezoneSetupCheck.php
+++ b/src/applications/config/check/PhabricatorTimezoneSetupCheck.php
@@ -1,55 +1,57 @@
<?php
final class PhabricatorTimezoneSetupCheck extends PhabricatorSetupCheck {
public function getDefaultGroup() {
return self::GROUP_OTHER;
}
protected function executeChecks() {
$php_value = ini_get('date.timezone');
if ($php_value) {
$old = date_default_timezone_get();
$ok = @date_default_timezone_set($php_value);
date_default_timezone_set($old);
if (!$ok) {
$message = pht(
'Your PHP configuration configuration selects an invalid timezone. '.
'Select a valid timezone.');
$this
->newIssue('php.date.timezone')
->setShortName(pht('PHP Timezone'))
->setName(pht('PHP Timezone Invalid'))
->setMessage($message)
->addPHPConfig('date.timezone');
}
}
$timezone = nonempty(
PhabricatorEnv::getEnvConfig('phabricator.timezone'),
ini_get('date.timezone'));
if ($timezone) {
return;
}
$summary = pht(
'Without a configured timezone, PHP will emit warnings when working '.
'with dates, and dates and times may not display correctly.');
$message = pht(
"Your configuration fails to specify a server timezone. You can either ".
- "set the PHP configuration value 'date.timezone' or the Phabricator ".
- "configuration value 'phabricator.timezone' to specify one.");
+ "set the PHP configuration value '%s' or the Phabricator ".
+ "configuration value '%s' to specify one.",
+ 'date.timezone',
+ 'phabricator.timezone');
$this
->newIssue('config.timezone')
->setShortName(pht('Timezone'))
->setName(pht('Server Timezone Not Configured'))
->setSummary($summary)
->setMessage($message)
->addPHPConfig('date.timezone')
->addPhabricatorConfig('phabricator.timezone');
}
}
diff --git a/src/applications/config/controller/PhabricatorConfigIgnoreController.php b/src/applications/config/controller/PhabricatorConfigIgnoreController.php
index a13603553..ba634dec1 100644
--- a/src/applications/config/controller/PhabricatorConfigIgnoreController.php
+++ b/src/applications/config/controller/PhabricatorConfigIgnoreController.php
@@ -1,68 +1,68 @@
<?php
final class PhabricatorConfigIgnoreController
extends PhabricatorConfigController {
private $verb;
private $issue;
public function willProcessRequest(array $data) {
$this->verb = $data['verb'];
$this->issue = $data['key'];
}
public function processRequest() {
$request = $this->getRequest();
$issue_uri = $this->getApplicationURI('issue/'.$this->issue.'/');
if ($request->isDialogFormPost()) {
$this->manageApplication();
return id(new AphrontRedirectResponse())->setURI($issue_uri);
}
if ($this->verb == 'ignore') {
$title = pht('Really ignore this setup issue?');
$submit_title = pht('Ignore');
$body = pht(
"You can ignore an issue if you don't want to fix it, or plan to ".
"fix it later. Ignored issues won't appear on every page but will ".
"still be shown in the list of open issues.");
} else if ($this->verb == 'unignore') {
$title = pht('Unignore this setup issue?');
$submit_title = pht('Unignore');
$body = pht(
'This issue will no longer be suppressed, and will return to its '.
'rightful place as a global setup warning.');
} else {
- throw new Exception('Unrecognized verb: '.$this->verb);
+ throw new Exception(pht('Unrecognized verb: %s', $this->verb));
}
$dialog = id(new AphrontDialogView())
->setUser($request->getUser())
->setTitle($title)
->appendChild($body)
->addSubmitButton($submit_title)
->addCancelButton($issue_uri);
return id(new AphrontDialogResponse())->setDialog($dialog);
}
public function manageApplication() {
$key = 'config.ignore-issues';
$config_entry = PhabricatorConfigEntry::loadConfigEntry($key);
$list = $config_entry->getValue();
if (isset($list[$this->issue])) {
unset($list[$this->issue]);
} else {
$list[$this->issue] = true;
}
PhabricatorConfigEditor::storeNewValue(
$this->getRequest()->getUser(),
$config_entry,
$list,
PhabricatorContentSource::newFromRequest($this->getRequest()));
}
}
diff --git a/src/applications/config/custom/PhabricatorCustomHeaderConfigType.php b/src/applications/config/custom/PhabricatorCustomHeaderConfigType.php
index b738aa6aa..fee8ba5a4 100644
--- a/src/applications/config/custom/PhabricatorCustomHeaderConfigType.php
+++ b/src/applications/config/custom/PhabricatorCustomHeaderConfigType.php
@@ -1,42 +1,48 @@
<?php
final class PhabricatorCustomHeaderConfigType
extends PhabricatorConfigOptionType {
public function validateOption(PhabricatorConfigOption $option, $value) {
if (phid_get_type($value) != PhabricatorFileFilePHIDType::TYPECONST) {
- throw new Exception(pht(
- '%s is not a valid file phid.', $value));
+ throw new Exception(
+ pht(
+ '%s is not a valid file PHID.',
+ $value));
}
$file = id(new PhabricatorFileQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withPHIDs(array($value))
->executeOne();
if (!$file) {
- throw new Exception(pht(
- '%s is not a valid file phid.', $value));
+ throw new Exception(
+ pht(
+ '%s is not a valid file PHID.',
+ $value));
}
$most_open_policy = PhabricatorPolicies::getMostOpenPolicy();
if ($file->getViewPolicy() != $most_open_policy) {
- throw new Exception(pht(
- 'Specified file %s has policy "%s" but should have policy "%s".',
- $value,
- $file->getViewPolicy(),
- $most_open_policy));
+ throw new Exception(
+ pht(
+ 'Specified file %s has policy "%s" but should have policy "%s".',
+ $value,
+ $file->getViewPolicy(),
+ $most_open_policy));
}
if (!$file->isViewableImage()) {
- throw new Exception(pht(
- 'Specified file %s is not a viewable image.',
- $value));
+ throw new Exception(
+ pht(
+ 'Specified file %s is not a viewable image.',
+ $value));
}
}
public static function getExampleConfig() {
$config = 'PHID-FILE-abcd1234abcd1234abcd';
return $config;
}
}
diff --git a/src/applications/config/management/PhabricatorConfigManagementDeleteWorkflow.php b/src/applications/config/management/PhabricatorConfigManagementDeleteWorkflow.php
index 8f8cea083..24b0f48c2 100644
--- a/src/applications/config/management/PhabricatorConfigManagementDeleteWorkflow.php
+++ b/src/applications/config/management/PhabricatorConfigManagementDeleteWorkflow.php
@@ -1,70 +1,73 @@
<?php
final class PhabricatorConfigManagementDeleteWorkflow
extends PhabricatorConfigManagementWorkflow {
protected function didConstruct() {
$this
->setName('delete')
->setExamples('**delete** __key__')
->setSynopsis(pht('Delete a local configuration value.'))
->setArguments(
array(
array(
'name' => 'database',
- 'help' => pht('Delete configuration in the database instead of '.
- 'in local configuration.'),
+ 'help' => pht(
+ 'Delete configuration in the database instead of '.
+ 'in local configuration.'),
),
array(
'name' => 'args',
'wildcard' => true,
),
));
}
public function execute(PhutilArgumentParser $args) {
$console = PhutilConsole::getConsole();
$argv = $args->getArg('args');
if (count($argv) == 0) {
- throw new PhutilArgumentUsageException(pht(
- 'Specify a configuration key to delete.'));
+ throw new PhutilArgumentUsageException(
+ pht('Specify a configuration key to delete.'));
}
$key = $argv[0];
if (count($argv) > 1) {
- throw new PhutilArgumentUsageException(pht(
- 'Too many arguments: expected one key.'));
+ throw new PhutilArgumentUsageException(
+ pht('Too many arguments: expected one key.'));
}
$use_database = $args->getArg('database');
if ($use_database) {
$config = new PhabricatorConfigDatabaseSource('default');
$config_type = 'database';
} else {
$config = new PhabricatorConfigLocalSource();
$config_type = 'local';
}
$values = $config->getKeys(array($key));
if (!$values) {
- throw new PhutilArgumentUsageException(pht(
- "Configuration key '%s' is not set in %s configuration!",
- $key,
- $config_type));
+ throw new PhutilArgumentUsageException(
+ pht(
+ "Configuration key '%s' is not set in %s configuration!",
+ $key,
+ $config_type));
}
if ($use_database) {
$config_entry = PhabricatorConfigEntry::loadConfigEntry($key);
$config_entry->setIsDeleted(1);
$config_entry->save();
} else {
$config->deleteKeys(array($key));
}
$console->writeOut(
- pht("Deleted '%s' from %s configuration.", $key, $config_type)."\n");
+ "%s\n",
+ pht("Deleted '%s' from %s configuration.", $key, $config_type));
}
}
diff --git a/src/applications/config/management/PhabricatorConfigManagementGetWorkflow.php b/src/applications/config/management/PhabricatorConfigManagementGetWorkflow.php
index 1924cc479..800ffe594 100644
--- a/src/applications/config/management/PhabricatorConfigManagementGetWorkflow.php
+++ b/src/applications/config/management/PhabricatorConfigManagementGetWorkflow.php
@@ -1,107 +1,109 @@
<?php
final class PhabricatorConfigManagementGetWorkflow
extends PhabricatorConfigManagementWorkflow {
protected function didConstruct() {
$this
->setName('get')
->setExamples('**get** __key__')
- ->setSynopsis('Get a local configuration value.')
+ ->setSynopsis(pht('Get a local configuration value.'))
->setArguments(
array(
array(
'name' => 'args',
'wildcard' => true,
),
));
}
public function execute(PhutilArgumentParser $args) {
$console = PhutilConsole::getConsole();
$argv = $args->getArg('args');
if (count($argv) == 0) {
throw new PhutilArgumentUsageException(
- 'Specify a configuration key to get.');
+ pht('Specify a configuration key to get.'));
}
$key = $argv[0];
if (count($argv) > 1) {
throw new PhutilArgumentUsageException(
- 'Too many arguments: expected one key.');
+ pht('Too many arguments: expected one key.'));
}
$options = PhabricatorApplicationConfigOptions::loadAllOptions();
if (empty($options[$key])) {
throw new PhutilArgumentUsageException(
- "No such configuration key '{$key}'! Use `config list` to list all ".
- "keys.");
+ pht(
+ "No such configuration key '%s'! Use `%s` to list all keys.",
+ $key,
+ 'config list'));
}
$values = array();
$config = new PhabricatorConfigLocalSource();
$local_value = $config->getKeys(array($key));
if (empty($local_value)) {
$values['local'] = array(
'key' => $key,
'value' => null,
'status' => 'unset',
'errorInfo' => null,
);
} else {
$values['local'] = array(
'key' => $key,
'value' => reset($local_value),
'status' => 'set',
'errorInfo' => null,
);
}
try {
$database_config = new PhabricatorConfigDatabaseSource('default');
$database_value = $database_config->getKeys(array($key));
if (empty($database_value)) {
$values['database'] = array(
'key' => $key,
'value' => null,
'status' => 'unset',
'errorInfo' => null,
);
} else {
$values['database'] = array(
'key' => $key,
'value' => reset($database_value),
'status' => 'set',
'errorInfo' => null,
);
}
} catch (Exception $e) {
$values['database'] = array(
'key' => $key,
'value' => null,
'status' => 'error',
'errorInfo' => pht('Database source is not configured properly'),
);
}
$result = array();
foreach ($values as $source => $value) {
$result[] = array(
'key' => $value['key'],
'source' => $source,
'value' => $value['value'],
'status' => $value['status'],
'errorInfo' => $value['errorInfo'],
);
}
$result = array(
'config' => $result,
);
$json = new PhutilJSON();
$console->writeOut($json->encodeFormatted($result));
}
}
diff --git a/src/applications/config/management/PhabricatorConfigManagementListWorkflow.php b/src/applications/config/management/PhabricatorConfigManagementListWorkflow.php
index 10bd6202b..ea9922e4d 100644
--- a/src/applications/config/management/PhabricatorConfigManagementListWorkflow.php
+++ b/src/applications/config/management/PhabricatorConfigManagementListWorkflow.php
@@ -1,25 +1,25 @@
<?php
final class PhabricatorConfigManagementListWorkflow
extends PhabricatorConfigManagementWorkflow {
protected function didConstruct() {
$this
->setName('list')
->setExamples('**list**')
- ->setSynopsis('List all configuration keys.');
+ ->setSynopsis(pht('List all configuration keys.'));
}
public function execute(PhutilArgumentParser $args) {
$options = PhabricatorApplicationConfigOptions::loadAllOptions();
ksort($options);
$console = PhutilConsole::getConsole();
foreach ($options as $option) {
$console->writeOut($option->getKey()."\n");
}
return 0;
}
}
diff --git a/src/applications/config/management/PhabricatorConfigManagementMigrateWorkflow.php b/src/applications/config/management/PhabricatorConfigManagementMigrateWorkflow.php
index 1f992d003..67b5733dc 100644
--- a/src/applications/config/management/PhabricatorConfigManagementMigrateWorkflow.php
+++ b/src/applications/config/management/PhabricatorConfigManagementMigrateWorkflow.php
@@ -1,74 +1,80 @@
<?php
final class PhabricatorConfigManagementMigrateWorkflow
extends PhabricatorConfigManagementWorkflow {
protected function didConstruct() {
$this
->setName('migrate')
->setExamples('**migrate**')
->setSynopsis(pht(
'Migrate file-based configuration to more modern storage.'));
}
public function execute(PhutilArgumentParser $args) {
$console = PhutilConsole::getConsole();
$key_count = 0;
$options = PhabricatorApplicationConfigOptions::loadAllOptions();
$local_config = new PhabricatorConfigLocalSource();
$database_config = new PhabricatorConfigDatabaseSource('default');
$config_sources = PhabricatorEnv::getConfigSourceStack()->getStack();
$console->writeOut(
- pht('Migrating file-based config to more modern config...')."\n");
+ "%s\n",
+ pht('Migrating file-based config to more modern config...'));
foreach ($config_sources as $config_source) {
if (!($config_source instanceof PhabricatorConfigFileSource)) {
$console->writeOut(
- pht('Skipping config of source type %s...',
- get_class($config_source))."\n");
+ "%s\n",
+ pht(
+ 'Skipping config of source type %s...',
+ get_class($config_source)));
continue;
}
- $console->writeOut(pht('Migrating file source...')."\n");
+ $console->writeOut("%s\n", pht('Migrating file source...'));
$all_keys = $config_source->getAllKeys();
foreach ($all_keys as $key => $value) {
$option = idx($options, $key);
if (!$option) {
- $console->writeOut(pht('Skipping obsolete option: %s', $key)."\n");
+ $console->writeOut("%s\n", pht('Skipping obsolete option: %s', $key));
continue;
}
$in_local = $local_config->getKeys(array($option->getKey()));
if ($in_local) {
- $console->writeOut(pht(
- 'Skipping option "%s"; already in local config.', $key)."\n");
+ $console->writeOut(
+ "%s\n",
+ pht('Skipping option "%s"; already in local config.', $key));
continue;
}
$is_locked = $option->getLocked();
if ($is_locked) {
$local_config->setKeys(array($option->getKey() => $value));
$key_count++;
- $console->writeOut(pht(
- 'Migrated option "%s" from file to local config.', $key)."\n");
+ $console->writeOut(
+ "%s\n",
+ pht('Migrated option "%s" from file to local config.', $key));
} else {
$in_database = $database_config->getKeys(array($option->getKey()));
if ($in_database) {
- $console->writeOut(pht(
- 'Skipping option "%s"; already in database config.', $key)."\n");
+ $console->writeOut(
+ "%s\n",
+ pht('Skipping option "%s"; already in database config.', $key));
continue;
} else {
$config_entry = PhabricatorConfigEntry::loadConfigEntry($key);
$config_entry->setValue($value);
$config_entry->save();
$key_count++;
- $console->writeOut(pht(
- 'Migrated option "%s" from file to database config.', $key)."\n");
+ $console->writeOut(
+ "%s\n",
+ pht('Migrated option "%s" from file to database config.', $key));
}
}
}
}
- $console->writeOut(pht(
- 'Done. Migrated %d keys.', $key_count)."\n");
+ $console->writeOut("%s\n", pht('Done. Migrated %d keys.', $key_count));
return 0;
}
}
diff --git a/src/applications/config/management/PhabricatorConfigManagementSetWorkflow.php b/src/applications/config/management/PhabricatorConfigManagementSetWorkflow.php
index 0da633226..1da30408e 100644
--- a/src/applications/config/management/PhabricatorConfigManagementSetWorkflow.php
+++ b/src/applications/config/management/PhabricatorConfigManagementSetWorkflow.php
@@ -1,151 +1,162 @@
<?php
final class PhabricatorConfigManagementSetWorkflow
extends PhabricatorConfigManagementWorkflow {
protected function didConstruct() {
$this
->setName('set')
->setExamples('**set** __key__ __value__')
->setSynopsis(pht('Set a local configuration value.'))
->setArguments(
array(
array(
'name' => 'database',
- 'help' => pht('Update configuration in the database instead of '.
- 'in local configuration.'),
+ 'help' => pht(
+ 'Update configuration in the database instead of '.
+ 'in local configuration.'),
),
array(
'name' => 'args',
'wildcard' => true,
),
));
}
public function execute(PhutilArgumentParser $args) {
$console = PhutilConsole::getConsole();
$argv = $args->getArg('args');
if (count($argv) == 0) {
- throw new PhutilArgumentUsageException(pht(
- 'Specify a configuration key and a value to set it to.'));
+ throw new PhutilArgumentUsageException(
+ pht('Specify a configuration key and a value to set it to.'));
}
$key = $argv[0];
if (count($argv) == 1) {
- throw new PhutilArgumentUsageException(pht(
- "Specify a value to set the key '%s' to.",
- $key));
+ throw new PhutilArgumentUsageException(
+ pht(
+ "Specify a value to set the key '%s' to.",
+ $key));
}
$value = $argv[1];
if (count($argv) > 2) {
- throw new PhutilArgumentUsageException(pht(
- 'Too many arguments: expected one key and one value.'));
+ throw new PhutilArgumentUsageException(
+ pht(
+ 'Too many arguments: expected one key and one value.'));
}
$options = PhabricatorApplicationConfigOptions::loadAllOptions();
if (empty($options[$key])) {
- throw new PhutilArgumentUsageException(pht(
- "No such configuration key '%s'! Use `config list` to list all ".
- "keys.",
- $key));
+ throw new PhutilArgumentUsageException(
+ pht(
+ "No such configuration key '%s'! Use `%s` to list all keys.",
+ $key,
+ 'config list'));
}
$option = $options[$key];
$type = $option->getType();
switch ($type) {
case 'string':
case 'class':
case 'enum':
$value = (string)$value;
break;
case 'int':
if (!ctype_digit($value)) {
- throw new PhutilArgumentUsageException(pht(
- "Config key '%s' is of type '%s'. Specify an integer.",
- $key,
- $type));
+ throw new PhutilArgumentUsageException(
+ pht(
+ "Config key '%s' is of type '%s'. Specify an integer.",
+ $key,
+ $type));
}
$value = (int)$value;
break;
case 'bool':
if ($value == 'true') {
$value = true;
} else if ($value == 'false') {
$value = false;
} else {
- throw new PhutilArgumentUsageException(pht(
- "Config key '%s' is of type '%s'. ".
- "Specify 'true' or 'false'.",
- $key,
- $type));
+ throw new PhutilArgumentUsageException(
+ pht(
+ "Config key '%s' is of type '%s'. Specify '%s' or '%s'.",
+ $key,
+ $type,
+ 'true',
+ 'false'));
}
break;
default:
$value = json_decode($value, true);
if (!is_array($value)) {
switch ($type) {
case 'set':
- $message = pht(
- "Config key '%s' is of type '%s'. Specify it in JSON. ".
- "For example:\n\n".
- ' ./bin/config set \'{"value1": true, "value2": true}\''.
- "\n",
- $key,
- $type);
+ $message = sprintf(
+ "%s%s\n\n %s\n",
+ pht(
+ "Config key '%s' is of type '%s'. Specify it in JSON.",
+ $key,
+ $type),
+ pht('For example:'),
+ './bin/config set \'{"value1": true, "value2": true}\'');
break;
default:
if (preg_match('/^list</', $type)) {
- $message = pht(
- "Config key '%s' is of type '%s'. Specify it in JSON. ".
- "For example:\n\n".
- ' ./bin/config set \'["a", "b", "c"]\''.
- "\n",
- $key,
- $type);
+ $message = sprintf(
+ "%s%s\n\n %s\n",
+ pht(
+ "Config key '%s' is of type '%s'. Specify it in JSON.",
+ $key,
+ $type),
+ pht('For example:'),
+ './bin/config set \'["a", "b", "c"]\'');
} else {
$message = pht(
'Config key "%s" is of type "%s". Specify it in JSON.',
$key,
$type);
}
break;
}
throw new PhutilArgumentUsageException($message);
}
break;
}
$use_database = $args->getArg('database');
if ($option->getLocked() && $use_database) {
- throw new PhutilArgumentUsageException(pht(
- "Config key '%s' is locked and can only be set in local ".
- 'configuration.',
- $key));
+ throw new PhutilArgumentUsageException(
+ pht(
+ "Config key '%s' is locked and can only be set in local ".
+ "configuration.",
+ $key));
}
try {
$option->getGroup()->validateOption($option, $value);
} catch (PhabricatorConfigValidationException $validation) {
// Convert this into a usage exception so we don't dump a stack trace.
throw new PhutilArgumentUsageException($validation->getMessage());
}
if ($use_database) {
$config_type = 'database';
$config_entry = PhabricatorConfigEntry::loadConfigEntry($key);
$config_entry->setValue($value);
$config_entry->save();
} else {
$config_type = 'local';
id(new PhabricatorConfigLocalSource())
->setKeys(array($key => $value));
}
$console->writeOut(
- pht("Set '%s' in %s configuration.", $key, $config_type)."\n");
+ "%s\n",
+ pht("Set '%s' in %s configuration.", $key, $config_type));
}
}
diff --git a/src/applications/config/option/PhabricatorAWSConfigOptions.php b/src/applications/config/option/PhabricatorAWSConfigOptions.php
index 1a360ffc1..b191e0e40 100644
--- a/src/applications/config/option/PhabricatorAWSConfigOptions.php
+++ b/src/applications/config/option/PhabricatorAWSConfigOptions.php
@@ -1,53 +1,53 @@
<?php
final class PhabricatorAWSConfigOptions
extends PhabricatorApplicationConfigOptions {
public function getName() {
return pht('Amazon Web Services');
}
public function getDescription() {
return pht('Configure integration with AWS (EC2, SES, S3, etc).');
}
public function getFontIcon() {
return 'fa-server';
}
public function getGroup() {
return 'core';
}
public function getOptions() {
return array(
$this->newOption('amazon-ses.access-key', 'string', null)
->setLocked(true)
->setDescription(pht('Access key for Amazon SES.')),
$this->newOption('amazon-ses.secret-key', 'string', null)
->setHidden(true)
->setDescription(pht('Secret key for Amazon SES.')),
$this->newOption('amazon-s3.access-key', 'string', null)
->setLocked(true)
->setDescription(pht('Access key for Amazon S3.')),
$this->newOption('amazon-s3.secret-key', 'string', null)
->setHidden(true)
->setDescription(pht('Secret key for Amazon S3.')),
$this->newOption('amazon-s3.endpoint', 'string', null)
->setLocked(true)
->setDescription(
pht(
'Explicit S3 endpoint to use. Leave empty to have Phabricator '.
'select and endpoint. Normally, you do not need to set this.'))
- ->addExample(null, 'Use default endpoint')
- ->addExample('s3.amazon.com', 'Use specific endpoint'),
+ ->addExample(null, pht('Use default endpoint'))
+ ->addExample('s3.amazon.com', pht('Use specific endpoint')),
$this->newOption('amazon-ec2.access-key', 'string', null)
->setLocked(true)
->setDescription(pht('Access key for Amazon EC2.')),
$this->newOption('amazon-ec2.secret-key', 'string', null)
->setHidden(true)
->setDescription(pht('Secret key for Amazon EC2.')),
);
}
}
diff --git a/src/applications/config/option/PhabricatorAccessLogConfigOptions.php b/src/applications/config/option/PhabricatorAccessLogConfigOptions.php
index c186fc355..5bc55f64a 100644
--- a/src/applications/config/option/PhabricatorAccessLogConfigOptions.php
+++ b/src/applications/config/option/PhabricatorAccessLogConfigOptions.php
@@ -1,134 +1,135 @@
<?php
final class PhabricatorAccessLogConfigOptions
extends PhabricatorApplicationConfigOptions {
public function getName() {
return pht('Access Logs');
}
public function getDescription() {
return pht('Configure the access logs, which log HTTP/SSH requests.');
}
public function getFontIcon() {
return 'fa-list';
}
public function getGroup() {
return 'core';
}
public function getOptions() {
$common_map = array(
'C' => pht('The controller or workflow which handled the request.'),
'c' => pht('The HTTP response code or process exit code.'),
'D' => pht('The request date.'),
'e' => pht('Epoch timestamp.'),
'h' => pht("The webserver's host name."),
'p' => pht('The PID of the server process.'),
'r' => pht('The remote IP.'),
'T' => pht('The request duration, in microseconds.'),
'U' => pht('The request path, or request target.'),
'm' => pht('For conduit, the Conduit method which was invoked.'),
'u' => pht('The logged-in username, if one is logged in.'),
'P' => pht('The logged-in user PHID, if one is logged in.'),
'i' => pht('Request input, in bytes.'),
'o' => pht('Request output, in bytes.'),
);
$http_map = $common_map + array(
'R' => pht('The HTTP referrer.'),
'M' => pht('The HTTP method.'),
);
$ssh_map = $common_map + array(
's' => pht('The system user.'),
'S' => pht('The system sudo user.'),
'k' => pht('ID of the SSH key used to authenticate the request.'),
);
$http_desc = pht(
- 'Format for the HTTP access log. Use {{log.access.path}} to set the '.
- 'path. Available variables are:');
+ 'Format for the HTTP access log. Use `%s` to set the path. '.
+ 'Available variables are:',
+ 'log.access.path');
$http_desc .= "\n\n";
$http_desc .= $this->renderMapHelp($http_map);
$ssh_desc = pht(
- 'Format for the SSH access log. Use {{log.ssh.path}} to set the '.
- 'path. Available variables are:');
+ 'Format for the SSH access log. Use %s to set the path. '.
+ 'Available variables are:',
+ 'log.ssh.path');
$ssh_desc .= "\n\n";
$ssh_desc .= $this->renderMapHelp($ssh_map);
return array(
$this->newOption('log.access.path', 'string', null)
->setLocked(true)
->setSummary(pht('Access log location.'))
->setDescription(
pht(
"To enable the Phabricator access log, specify a path. The ".
- "access log can provide more detailed information about ".
"Phabricator access than normal HTTP access logs (for instance, ".
"it can show logged-in users, controllers, and other application ".
"data).\n\n".
"If not set, no log will be written."))
->addExample(
null,
pht('Disable access log.'))
->addExample(
'/var/log/phabricator/access.log',
pht('Write access log here.')),
$this->newOption(
'log.access.format',
// NOTE: This is 'wild' intead of 'string' so "\t" and such can be
// specified.
'wild',
"[%D]\t%p\t%h\t%r\t%u\t%C\t%m\t%U\t%R\t%c\t%T")
->setLocked(true)
->setSummary(pht('Access log format.'))
->setDescription($http_desc),
$this->newOption('log.ssh.path', 'string', null)
->setLocked(true)
->setSummary(pht('SSH log location.'))
->setDescription(
pht(
"To enable the Phabricator SSH log, specify a path. The ".
"access log can provide more detailed information about SSH ".
"access than a normal SSH log (for instance, it can show ".
"logged-in users, commands, and other application data).\n\n".
"If not set, no log will be written."))
->addExample(
null,
pht('Disable SSH log.'))
->addExample(
'/var/log/phabricator/ssh.log',
pht('Write SSH log here.')),
$this->newOption(
'log.ssh.format',
'wild',
"[%D]\t%p\t%h\t%r\t%s\t%S\t%u\t%C\t%U\t%c\t%T\t%i\t%o")
->setLocked(true)
->setSummary(pht('SSH log format.'))
->setDescription($ssh_desc),
);
}
private function renderMapHelp(array $map) {
$desc = '';
foreach ($map as $key => $kdesc) {
$desc .= " - `%".$key."` ".$kdesc."\n";
}
$desc .= "\n";
$desc .= pht(
"If a variable isn't available (for example, %%m appears in the file ".
"format but the request is not a Conduit request), it will be rendered ".
"as '-'");
$desc .= "\n\n";
$desc .= pht(
"Note that the default format is subject to change in the future, so ".
"if you rely on the log's format, specify it explicitly.");
return $desc;
}
}
diff --git a/src/applications/config/option/PhabricatorApplicationConfigOptions.php b/src/applications/config/option/PhabricatorApplicationConfigOptions.php
index 7ac1df3e1..d99cdf310 100644
--- a/src/applications/config/option/PhabricatorApplicationConfigOptions.php
+++ b/src/applications/config/option/PhabricatorApplicationConfigOptions.php
@@ -1,249 +1,249 @@
<?php
abstract class PhabricatorApplicationConfigOptions extends Phobject {
abstract public function getName();
abstract public function getDescription();
abstract public function getGroup();
abstract public function getOptions();
public function getFontIcon() {
return 'fa-sliders';
}
public function validateOption(PhabricatorConfigOption $option, $value) {
if ($value === $option->getDefault()) {
return;
}
if ($value === null) {
return;
}
if ($option->isCustomType()) {
return $option->getCustomObject()->validateOption($option, $value);
}
switch ($option->getType()) {
case 'bool':
if ($value !== true &&
$value !== false) {
throw new PhabricatorConfigValidationException(
pht(
"Option '%s' is of type bool, but value is not true or false.",
$option->getKey()));
}
break;
case 'int':
if (!is_int($value)) {
throw new PhabricatorConfigValidationException(
pht(
"Option '%s' is of type int, but value is not an integer.",
$option->getKey()));
}
break;
case 'string':
if (!is_string($value)) {
throw new PhabricatorConfigValidationException(
pht(
"Option '%s' is of type string, but value is not a string.",
$option->getKey()));
}
break;
case 'class':
$symbols = id(new PhutilSymbolLoader())
->setType('class')
->setAncestorClass($option->getBaseClass())
->setConcreteOnly(true)
->selectSymbolsWithoutLoading();
$names = ipull($symbols, 'name', 'name');
if (empty($names[$value])) {
throw new PhabricatorConfigValidationException(
pht(
"Option '%s' value must name a class extending '%s'.",
$option->getKey(),
$option->getBaseClass()));
}
break;
case 'set':
$valid = true;
if (!is_array($value)) {
throw new PhabricatorConfigValidationException(
pht(
"Option '%s' must be a set, but value is not an array.",
$option->getKey()));
}
foreach ($value as $v) {
if ($v !== true) {
throw new PhabricatorConfigValidationException(
pht(
"Option '%s' must be a set, but array contains values other ".
"than 'true'.",
$option->getKey()));
}
}
break;
case 'list<regex>':
$valid = true;
if (!is_array($value)) {
throw new PhabricatorConfigValidationException(
pht(
"Option '%s' must be a list of regular expressions, but value ".
"is not an array.",
$option->getKey()));
}
if ($value && array_keys($value) != range(0, count($value) - 1)) {
throw new PhabricatorConfigValidationException(
pht(
"Option '%s' must be a list of regular expressions, but the ".
"value is a map with unnatural keys.",
$option->getKey()));
}
foreach ($value as $v) {
$ok = @preg_match($v, '');
if ($ok === false) {
throw new PhabricatorConfigValidationException(
pht(
"Option '%s' must be a list of regular expressions, but the ".
"value '%s' is not a valid regular expression.",
$option->getKey(),
$v));
}
}
break;
case 'list<string>':
$valid = true;
if (!is_array($value)) {
throw new PhabricatorConfigValidationException(
pht(
"Option '%s' must be a list of strings, but value is not ".
"an array.",
$option->getKey()));
}
if ($value && array_keys($value) != range(0, count($value) - 1)) {
throw new PhabricatorConfigValidationException(
pht(
"Option '%s' must be a list of strings, but the value is a ".
"map with unnatural keys.",
$option->getKey()));
}
foreach ($value as $v) {
if (!is_string($v)) {
throw new PhabricatorConfigValidationException(
pht(
"Option '%s' must be a list of strings, but it contains one ".
"or more non-strings.",
$option->getKey()));
}
}
break;
case 'wild':
default:
break;
}
$this->didValidateOption($option, $value);
}
protected function didValidateOption(
PhabricatorConfigOption $option,
$value) {
// Hook for subclasses to do complex validation.
return;
}
/**
* Hook to render additional hints based on, e.g., the viewing user, request,
* or other context. For example, this is used to show workspace IDs when
* configuring `asana.workspace-id`.
*
* @param PhabricatorConfigOption Option being rendered.
* @param AphrontRequest Active request.
* @return wild Additional contextual description
* information.
*/
public function renderContextualDescription(
PhabricatorConfigOption $option,
AphrontRequest $request) {
return null;
}
public function getKey() {
$class = get_class($this);
$matches = null;
if (preg_match('/^Phabricator(.*)ConfigOptions$/', $class, $matches)) {
return strtolower($matches[1]);
}
return strtolower(get_class($this));
}
final protected function newOption($key, $type, $default) {
return id(new PhabricatorConfigOption())
->setKey($key)
->setType($type)
->setDefault($default)
->setGroup($this);
}
final public static function loadAll($external_only = false) {
$symbols = id(new PhutilSymbolLoader())
->setAncestorClass(__CLASS__)
->setConcreteOnly(true)
->selectAndLoadSymbols();
$groups = array();
foreach ($symbols as $symbol) {
if ($external_only && $symbol['library'] == 'phabricator') {
continue;
}
$obj = newv($symbol['name'], array());
$key = $obj->getKey();
if (isset($groups[$key])) {
$pclass = get_class($groups[$key]);
$nclass = $symbol['name'];
throw new Exception(
pht(
"Multiple %s subclasses have the same key ('%s'): %s, %s.",
__CLASS__,
$key,
$pclass,
$nclass));
}
$groups[$key] = $obj;
}
return $groups;
}
final public static function loadAllOptions($external_only = false) {
$groups = self::loadAll($external_only);
$options = array();
foreach ($groups as $group) {
foreach ($group->getOptions() as $option) {
$key = $option->getKey();
if (isset($options[$key])) {
throw new Exception(
pht(
- "Mulitple % subclasses contain an option named '%s'!",
+ "Mulitple %s subclasses contain an option named '%s'!",
__CLASS__,
$key));
}
$options[$key] = $option;
}
}
return $options;
}
/**
* Deformat a HEREDOC for use in remarkup by converting line breaks to
* spaces.
*/
final protected function deformat($string) {
return preg_replace('/(?<=\S)\n(?=\S)/', ' ', $string);
}
}
diff --git a/src/applications/config/option/PhabricatorAuthenticationConfigOptions.php b/src/applications/config/option/PhabricatorAuthenticationConfigOptions.php
index dc7996a3e..a965d16f4 100644
--- a/src/applications/config/option/PhabricatorAuthenticationConfigOptions.php
+++ b/src/applications/config/option/PhabricatorAuthenticationConfigOptions.php
@@ -1,110 +1,109 @@
<?php
final class PhabricatorAuthenticationConfigOptions
extends PhabricatorApplicationConfigOptions {
public function getName() {
return pht('Authentication');
}
public function getDescription() {
return pht('Options relating to authentication.');
}
public function getFontIcon() {
return 'fa-key';
}
public function getGroup() {
return 'core';
}
public function getOptions() {
return array(
$this->newOption('auth.require-email-verification', 'bool', false)
->setBoolOptions(
array(
pht('Require email verification'),
pht("Don't require email verification"),
))
->setSummary(
pht('Require email verification before a user can log in.'))
->setDescription(
pht(
'If true, email addresses must be verified (by clicking a link '.
'in an email) before a user can login. By default, verification '.
'is optional unless {{auth.email-domains}} is nonempty.')),
$this->newOption('auth.require-approval', 'bool', true)
->setBoolOptions(
array(
pht('Require Administrators to Approve Accounts'),
pht("Don't Require Manual Approval"),
))
->setSummary(
pht('Require administrators to approve new accounts.'))
->setDescription(
pht(
"Newly registered Phabricator accounts can either be placed ".
"into a manual approval queue for administrative review, or ".
"automatically activated immediately. The approval queue is ".
"enabled by default because it gives you greater control over ".
"who can register an account and access Phabricator.\n\n".
"If your install is completely public, or on a VPN, or users can ".
"only register with a trusted provider like LDAP, or you've ".
"otherwise configured Phabricator to prevent unauthorized ".
"registration, you can disable the queue to reduce administrative ".
"overhead.\n\n".
"NOTE: Before you disable the queue, make sure ".
- "{{auth.email-domains}} is configured correctly for your ".
- "install!")),
+ "{{auth.email-domains}} is configured correctly ".
+ "for your install!")),
$this->newOption('auth.email-domains', 'list<string>', array())
->setSummary(pht('Only allow registration from particular domains.'))
->setDescription(
pht(
"You can restrict allowed email addresses to certain domains ".
"(like `yourcompany.com`) by setting a list of allowed domains ".
"here.\n\nUsers will only be allowed to register using email ".
"addresses at one of the domains, and will only be able to add ".
"new email addresses for these domains. If you configure this, ".
"it implies {{auth.require-email-verification}}.\n\n".
"You should omit the `@` from domains. Note that the domain must ".
"match exactly. If you allow `yourcompany.com`, that permits ".
"`joe@yourcompany.com` but rejects `joe@mail.yourcompany.com`."))
->addExample(
"yourcompany.com\nmail.yourcompany.com",
pht('Valid Setting')),
$this->newOption('auth.login-message', 'string', null)
->setLocked(true)
->setSummary(pht('A block of HTML displayed on the login screen.'))
->setDescription(
pht(
"You can provide an arbitrary block of HTML here, which will ".
"appear on the login screen. Normally, you'd use this to provide ".
"login or registration instructions to users.")),
$this->newOption('account.editable', 'bool', true)
->setBoolOptions(
array(
pht('Allow editing'),
pht('Prevent editing'),
))
->setSummary(
pht(
- 'Determines whether or not basic account information is '.
- 'editable.'))
+ 'Determines whether or not basic account information is editable.'))
->setDescription(
pht(
'Is basic account information (email, real name, profile '.
'picture) editable? If you set up Phabricator to automatically '.
'synchronize account information from some other authoritative '.
'system, you can disable this to ensure information remains '.
'consistent across both systems.')),
$this->newOption('account.minimum-password-length', 'int', 8)
->setSummary(pht('Minimum password length.'))
->setDescription(
pht(
'When users set or reset a password, it must have at least this '.
'many characters.')),
);
}
}
diff --git a/src/applications/config/option/PhabricatorClusterConfigOptions.php b/src/applications/config/option/PhabricatorClusterConfigOptions.php
index f6e2849e4..8684d9823 100644
--- a/src/applications/config/option/PhabricatorClusterConfigOptions.php
+++ b/src/applications/config/option/PhabricatorClusterConfigOptions.php
@@ -1,78 +1,79 @@
<?php
final class PhabricatorClusterConfigOptions
extends PhabricatorApplicationConfigOptions {
public function getName() {
return pht('Cluster Setup');
}
public function getDescription() {
return pht('Configure Phabricator to run on a cluster of hosts.');
}
public function getFontIcon() {
return 'fa-sitemap';
}
public function getGroup() {
return 'core';
}
public function getOptions() {
return array(
$this->newOption('cluster.addresses', 'list<string>', array())
->setLocked(true)
->setSummary(pht('Address ranges of cluster hosts.'))
->setDescription(
pht(
'To allow Phabricator nodes to communicate with other nodes '.
'in the cluster, provide an address whitelist of hosts that '.
'are part of the cluster.'.
"\n\n".
'Hosts on this whitelist are permitted to use special cluster '.
'mechanisms to authenticate requests. By default, these '.
'mechanisms are disabled.'.
"\n\n".
'Define a list of CIDR blocks which whitelist all hosts in the '.
'cluster. See the examples below for details.',
"\n\n".
'When cluster addresses are defined, Phabricator hosts will also '.
'reject requests to interfaces which are not whitelisted.'))
->addExample(
array(
'23.24.25.80/32',
'23.24.25.81/32',
),
pht('Whitelist Specific Addresses'))
->addExample(
array(
'1.2.3.0/24',
),
pht('Whitelist 1.2.3.*'))
->addExample(
array(
'1.2.0.0/16',
),
pht('Whitelist 1.2.*.*'))
->addExample(
array(
'0.0.0.0/0',
),
pht('Allow Any Host (Insecure!)')),
$this->newOption('cluster.instance', 'string', null)
->setLocked(true)
->setSummary(pht('Instance identifier for multi-tenant clusters.'))
->setDescription(
pht(
'WARNING: This is a very advanced option, and only useful for '.
'hosting providers running multi-tenant clusters.'.
"\n\n".
'If you provide an instance identifier here (normally by '.
- 'injecting it with a `PhabricatorConfigSiteSource`), Phabricator '.
- 'will pass it to subprocesses and commit hooks in the '.
- '`PHABRICATOR_INSTANCE` environmental variable.')),
+ 'injecting it with a `%s`), Phabricator will pass it to '.
+ 'subprocesses and commit hooks in the `%s` environmental variable.',
+ 'PhabricatorConfigSiteSource',
+ 'PHABRICATOR_INSTANCE')),
);
}
}
diff --git a/src/applications/config/option/PhabricatorConfigOption.php b/src/applications/config/option/PhabricatorConfigOption.php
index e5c977361..e1d71416e 100644
--- a/src/applications/config/option/PhabricatorConfigOption.php
+++ b/src/applications/config/option/PhabricatorConfigOption.php
@@ -1,238 +1,240 @@
<?php
final class PhabricatorConfigOption
extends Phobject
implements PhabricatorMarkupInterface {
private $key;
private $default;
private $summary;
private $description;
private $type;
private $boolOptions;
private $enumOptions;
private $group;
private $examples;
private $locked;
private $lockedMessage;
private $hidden;
private $baseClass;
private $customData;
private $customObject;
public function setBaseClass($base_class) {
$this->baseClass = $base_class;
return $this;
}
public function getBaseClass() {
return $this->baseClass;
}
public function setHidden($hidden) {
$this->hidden = $hidden;
return $this;
}
public function getHidden() {
if ($this->hidden) {
return true;
}
return idx(
PhabricatorEnv::getEnvConfig('config.hide'),
$this->getKey(),
false);
}
public function setLocked($locked) {
$this->locked = $locked;
return $this;
}
public function getLocked() {
if ($this->locked) {
return true;
}
if ($this->getHidden()) {
return true;
}
return idx(
PhabricatorEnv::getEnvConfig('config.lock'),
$this->getKey(),
false);
}
public function setLockedMessage($message) {
$this->lockedMessage = $message;
return $this;
}
public function getLockedMessage() {
if ($this->lockedMessage !== null) {
return $this->lockedMessage;
}
return pht(
'This configuration is locked and can not be edited from the web '.
- 'interface. Use `./bin/config` in `phabricator/` to edit it.');
+ 'interface. Use `%s` in `%s` to edit it.',
+ './bin/config',
+ 'phabricator/');
}
public function addExample($value, $description) {
$this->examples[] = array($value, $description);
return $this;
}
public function getExamples() {
return $this->examples;
}
public function setGroup(PhabricatorApplicationConfigOptions $group) {
$this->group = $group;
return $this;
}
public function getGroup() {
return $this->group;
}
public function setBoolOptions(array $options) {
$this->boolOptions = $options;
return $this;
}
public function getBoolOptions() {
if ($this->boolOptions) {
return $this->boolOptions;
}
return array(
pht('True'),
pht('False'),
);
}
public function setEnumOptions(array $options) {
$this->enumOptions = $options;
return $this;
}
public function getEnumOptions() {
if ($this->enumOptions) {
return $this->enumOptions;
}
throw new PhutilInvalidStateException('setEnumOptions');
}
public function setKey($key) {
$this->key = $key;
return $this;
}
public function getKey() {
return $this->key;
}
public function setDefault($default) {
$this->default = $default;
return $this;
}
public function getDefault() {
return $this->default;
}
public function setSummary($summary) {
$this->summary = $summary;
return $this;
}
public function getSummary() {
if (empty($this->summary)) {
return $this->getDescription();
}
return $this->summary;
}
public function setDescription($description) {
$this->description = $description;
return $this;
}
public function getDescription() {
return $this->description;
}
public function setType($type) {
$this->type = $type;
return $this;
}
public function getType() {
return $this->type;
}
public function isCustomType() {
return !strncmp($this->getType(), 'custom:', 7);
}
public function getCustomObject() {
if (!$this->customObject) {
if (!$this->isCustomType()) {
- throw new Exception('This option does not have a custom type!');
+ throw new Exception(pht('This option does not have a custom type!'));
}
$this->customObject = newv(substr($this->getType(), 7), array());
}
return $this->customObject;
}
public function getCustomData() {
return $this->customData;
}
public function setCustomData($data) {
$this->customData = $data;
return $this;
}
/* -( PhabricatorMarkupInterface )----------------------------------------- */
public function getMarkupFieldKey($field) {
return $this->getKey().':'.$field;
}
public function newMarkupEngine($field) {
return PhabricatorMarkupEngine::newMarkupEngine(array());
}
public function getMarkupText($field) {
switch ($field) {
case 'description':
$text = $this->getDescription();
break;
case 'summary':
$text = $this->getSummary();
break;
}
// TODO: We should probably implement this as a real Markup rule, but
// markup rules are a bit of a mess right now and it doesn't hurt us to
// fake this.
$text = preg_replace(
'/{{([^}]+)}}/',
'[[/config/edit/\\1/ | \\1]]',
$text);
return $text;
}
public function didMarkupText($field, $output, PhutilMarkupEngine $engine) {
return $output;
}
public function shouldUseMarkupCache($field) {
return false;
}
}
diff --git a/src/applications/config/option/PhabricatorCoreConfigOptions.php b/src/applications/config/option/PhabricatorCoreConfigOptions.php
index b5df38633..7477bae5b 100644
--- a/src/applications/config/option/PhabricatorCoreConfigOptions.php
+++ b/src/applications/config/option/PhabricatorCoreConfigOptions.php
@@ -1,315 +1,324 @@
<?php
final class PhabricatorCoreConfigOptions
extends PhabricatorApplicationConfigOptions {
public function getName() {
return pht('Core');
}
public function getDescription() {
return pht('Configure core options, including URIs.');
}
public function getFontIcon() {
return 'fa-bullseye';
}
public function getGroup() {
return 'core';
}
public function getOptions() {
if (phutil_is_windows()) {
$paths = array();
} else {
$paths = array(
'/bin',
'/usr/bin',
'/usr/local/bin',
);
}
$path = getenv('PATH');
$proto_doc_href = PhabricatorEnv::getDoclink(
'User Guide: Prototype Applications');
$proto_doc_name = pht('User Guide: Prototype Applications');
$applications_app_href = '/applications/';
return array(
$this->newOption('phabricator.base-uri', 'string', null)
->setLocked(true)
->setSummary(pht('URI where Phabricator is installed.'))
->setDescription(
pht(
'Set the URI where Phabricator is installed. Setting this '.
'improves security by preventing cookies from being set on other '.
'domains, and allows daemons to send emails with links that have '.
'the correct domain.'))
->addExample('http://phabricator.example.com/', pht('Valid Setting')),
$this->newOption('phabricator.production-uri', 'string', null)
->setSummary(
pht('Primary install URI, for multi-environment installs.'))
->setDescription(
pht(
'If you have multiple Phabricator environments (like a '.
'development/staging environment for working on testing '.
'Phabricator, and a production environment for deploying it), '.
'set the production environment URI here so that emails and other '.
'durable URIs will always generate with links pointing at the '.
- 'production environment. If unset, defaults to '.
- '{{phabricator.base-uri}}. Most installs do not need to set '.
- 'this option.'))
+ 'production environment. If unset, defaults to `%s`. Most '.
+ 'installs do not need to set this option.',
+ 'phabricator.base-uri'))
->addExample('http://phabricator.example.com/', pht('Valid Setting')),
$this->newOption('phabricator.allowed-uris', 'list<string>', array())
->setLocked(true)
->setSummary(pht('Alternative URIs that can access Phabricator.'))
->setDescription(
pht(
"These alternative URIs will be able to access 'normal' pages ".
- "on your Phabricator install. Other features such as OAuth ".
- "won't work. The major use case for this is moving installs ".
- "across domains."))
+ "on your Phabricator install. Other features such as OAuth ".
+ "won't work. The major use case for this is moving installs ".
+ "across domains."))
->addExample(
"http://phabricator2.example.com/\n".
"http://phabricator3.example.com/",
pht('Valid Setting')),
$this->newOption('phabricator.timezone', 'string', null)
->setSummary(
pht('The timezone Phabricator should use.'))
->setDescription(
pht(
"PHP requires that you set a timezone in your php.ini before ".
"using date functions, or it will emit a warning. If this isn't ".
"possible (for instance, because you are using HPHP) you can set ".
- "some valid constant for date_default_timezone_set() here and ".
- "Phabricator will set it on your behalf, silencing the warning."))
+ "some valid constant for %s here and Phabricator will set it on ".
+ "your behalf, silencing the warning.",
+ 'date_default_timezone_set()'))
->addExample('America/New_York', pht('US East (EDT)'))
->addExample('America/Chicago', pht('US Central (CDT)'))
->addExample('America/Boise', pht('US Mountain (MDT)'))
->addExample('America/Los_Angeles', pht('US West (PDT)')),
$this->newOption('phabricator.cookie-prefix', 'string', null)
->setLocked(true)
->setSummary(
- pht('Set a string Phabricator should use to prefix '.
- 'cookie names.'))
+ pht(
+ 'Set a string Phabricator should use to prefix cookie names.'))
->setDescription(
pht(
'Cookies set for x.com are also sent for y.x.com. Assuming '.
'Phabricator instances are running on both domains, this will '.
'create a collision preventing you from logging in.'))
- ->addExample('dev', pht('Prefix cookie with "dev"')),
+ ->addExample('dev', pht('Prefix cookie with "%s"', 'dev')),
$this->newOption('phabricator.show-prototypes', 'bool', false)
->setLocked(true)
->setBoolOptions(
array(
pht('Enable Prototypes'),
pht('Disable Prototypes'),
))
->setSummary(
pht(
'Install applications which are still under development.'))
->setDescription(
pht(
"IMPORTANT: The upstream does not provide support for prototype ".
"applications.".
"\n\n".
"Phabricator includes prototype applications which are in an ".
"**early stage of development**. By default, prototype ".
"applications are not installed, because they are often not yet ".
"developed enough to be generally usable. You can enable ".
"this option to install them if you're developing Phabricator ".
"or are interested in previewing upcoming features.".
"\n\n".
"To learn more about prototypes, see [[ %s | %s ]].".
"\n\n".
"After enabling prototypes, you can selectively uninstall them ".
"(like normal applications).",
$proto_doc_href,
$proto_doc_name)),
$this->newOption('phabricator.serious-business', 'bool', false)
->setBoolOptions(
array(
pht('Serious business'),
pht('Shenanigans'), // That should be interesting to translate. :P
))
->setSummary(
pht('Allows you to remove levity and jokes from the UI.'))
->setDescription(
pht(
'By default, Phabricator includes some flavor text in the UI, '.
'like a prompt to "Weigh In" rather than "Add Comment" in '.
'Maniphest. If you\'d prefer more traditional UI strings like '.
'"Add Comment", you can set this flag to disable most of the '.
'extra flavor.')),
$this->newOption('remarkup.ignored-object-names', 'string', '/^(Q|V)\d$/')
->setSummary(
pht('Text values that match this regex and are also object names '.
'will not be linked.'))
->setDescription(
pht(
'By default, Phabricator links object names in Remarkup fields '.
'to the corresponding object. This regex can be used to modify '.
'this behavior; object names that match this regex will not be '.
'linked.')),
$this->newOption('environment.append-paths', 'list<string>', $paths)
->setSummary(
- pht('These paths get appended to your \$PATH envrionment variable.'))
+ pht(
+ 'These paths get appended to your %s environment variable.',
+ '$PATH'))
->setDescription(
pht(
"Phabricator occasionally shells out to other binaries on the ".
- "server. An example of this is the `pygmentize` command, used ".
- "to syntax-highlight code written in languages other than PHP. ".
- "By default, it is assumed that these binaries are in the \$PATH ".
- "of the user running Phabricator (normally 'apache', 'httpd', or ".
- "'nobody'). Here you can add extra directories to the \$PATH ".
+ "server. An example of this is the `%s` command, used to ".
+ "syntax-highlight code written in languages other than PHP. By ".
+ "default, it is assumed that these binaries are in the %s of the ".
+ "user running Phabricator (normally 'apache', 'httpd', or ".
+ "'nobody'). Here you can add extra directories to the %s ".
"environment variable, for when these binaries are in ".
"non-standard locations.\n\n".
- "Note that you can also put binaries in ".
- "`phabricator/support/bin/` (for example, by symlinking them).\n\n".
+ "Note that you can also put binaries in `%s` (for example, by ".
+ "symlinking them).\n\n".
"The current value of PATH after configuration is applied is:\n\n".
" lang=text\n".
- " %s", $path))
+ " %s",
+ '$PATH',
+ '$PATH',
+ 'phabricator/support/bin/',
+ $path,
+ 'pygmentize'))
->setLocked(true)
->addExample('/usr/local/bin', pht('Add One Path'))
->addExample("/usr/bin\n/usr/local/bin", pht('Add Multiple Paths')),
$this->newOption('config.lock', 'set', array())
->setLocked(true)
->setDescription(pht('Additional configuration options to lock.')),
$this->newOption('config.hide', 'set', array())
->setLocked(true)
->setDescription(pht('Additional configuration options to hide.')),
$this->newOption('config.ignore-issues', 'set', array())
->setLocked(true)
->setDescription(pht('Setup issues to ignore.')),
$this->newOption('phabricator.env', 'string', null)
->setLocked(true)
->setDescription(pht('Internal.')),
$this->newOption('test.value', 'wild', null)
->setLocked(true)
->setDescription(pht('Unit test value.')),
$this->newOption('phabricator.uninstalled-applications', 'set', array())
->setLocked(true)
->setLockedMessage(pht(
'Use the %s to manage installed applications.',
phutil_tag(
'a',
array(
'href' => $applications_app_href,
),
pht('Applications application'))))
->setDescription(
- pht('Array containing list of Uninstalled applications.')),
+ pht('Array containing list of uninstalled applications.')),
$this->newOption('phabricator.application-settings', 'wild', array())
->setLocked(true)
->setDescription(
pht('Customized settings for Phabricator applications.')),
$this->newOption('welcome.html', 'string', null)
->setLocked(true)
->setDescription(
pht('Custom HTML to show on the main Phabricator dashboard.')),
$this->newOption('phabricator.cache-namespace', 'string', 'phabricator')
->setLocked(true)
->setDescription(pht('Cache namespace.')),
$this->newOption('phabricator.allow-email-users', 'bool', false)
->setBoolOptions(
array(
pht('Allow'),
pht('Disallow'),
))
->setDescription(
pht('Allow non-members to interact with tasks over email.')),
$this->newOption('phabricator.silent', 'bool', false)
->setLocked(true)
->setBoolOptions(
array(
pht('Run Silently'),
pht('Run Normally'),
))
->setSummary(pht('Stop Phabricator from sending any email, etc.'))
->setDescription(
pht(
'This option allows you to stop Phabricator from sending '.
'any data to external services. Among other things, it will '.
'disable email, SMS, repository mirroring, and HTTP hooks.'.
"\n\n".
'This option is intended to allow a Phabricator instance to '.
'be exported, copied, imported, and run in a test environment '.
'without impacting users. For example, if you are migrating '.
'to new hardware, you could perform a test migration first, '.
'make sure things work, and then do a production cutover '.
'later with higher confidence and less disruption. Without '.
'this flag, users would receive duplicate email during the '.
'time the test instance and old production instance were '.
'both in operation.')),
);
}
protected function didValidateOption(
PhabricatorConfigOption $option,
$value) {
$key = $option->getKey();
if ($key == 'phabricator.base-uri' ||
$key == 'phabricator.production-uri') {
$uri = new PhutilURI($value);
$protocol = $uri->getProtocol();
if ($protocol !== 'http' && $protocol !== 'https') {
throw new PhabricatorConfigValidationException(
pht(
"Config option '%s' is invalid. The URI must start with ".
- "'http://' or 'https://'.",
+ "%s' or '%s'.",
+ 'http://',
+ 'https://',
$key));
}
$domain = $uri->getDomain();
if (strpos($domain, '.') === false) {
throw new PhabricatorConfigValidationException(
pht(
- "Config option '%s' is invalid. The URI must contain a dot ('.'), ".
- "like 'http://example.com/', not just a bare name like ".
- "'http://example/'. Some web browsers will not set cookies on ".
- "domains with no TLD.",
+ "Config option '%s' is invalid. The URI must contain a dot ".
+ "('%s'), like '%s', not just a bare name like '%s'. Some web ".
+ "browsers will not set cookies on domains with no TLD.",
+ '.',
+ 'http://example.com/',
+ 'http://example/',
$key));
}
$path = $uri->getPath();
if ($path !== '' && $path !== '/') {
throw new PhabricatorConfigValidationException(
pht(
"Config option '%s' is invalid. The URI must NOT have a path, ".
- "e.g. 'http://phabricator.example.com/' is OK, but ".
- "'http://example.com/phabricator/' is not. Phabricator must be ".
- "installed on an entire domain; it can not be installed on a ".
- "path.",
- $key));
+ "e.g. '%s' is OK, but '%s' is not. Phabricator must be installed ".
+ "on an entire domain; it can not be installed on a path.",
+ $key,
+ 'http://phabricator.example.com/',
+ 'http://example.com/phabricator/'));
}
}
if ($key === 'phabricator.timezone') {
$old = date_default_timezone_get();
$ok = @date_default_timezone_set($value);
@date_default_timezone_set($old);
if (!$ok) {
throw new PhabricatorConfigValidationException(
pht(
"Config option '%s' is invalid. The timezone identifier must ".
- "be a valid timezone identifier recognized by PHP, like ".
- "'America/Los_Angeles'. You can find a list of valid identifiers ".
- "here: %s",
+ "be a valid timezone identifier recognized by PHP, like '%s'. "."
+ You can find a list of valid identifiers here: %s",
$key,
+ 'America/Los_Angeles',
'http://php.net/manual/timezones.php'));
}
}
-
-
-
}
}
diff --git a/src/applications/config/option/PhabricatorDeveloperConfigOptions.php b/src/applications/config/option/PhabricatorDeveloperConfigOptions.php
index c2063ec87..37cf752f6 100644
--- a/src/applications/config/option/PhabricatorDeveloperConfigOptions.php
+++ b/src/applications/config/option/PhabricatorDeveloperConfigOptions.php
@@ -1,181 +1,183 @@
<?php
final class PhabricatorDeveloperConfigOptions
extends PhabricatorApplicationConfigOptions {
public function getName() {
return pht('Developer / Debugging');
}
public function getDescription() {
return pht('Options for Phabricator developers, including debugging.');
}
public function getFontIcon() {
return 'fa-bug';
}
public function getGroup() {
return 'core';
}
public function getOptions() {
return array(
$this->newOption('darkconsole.enabled', 'bool', false)
->setBoolOptions(
array(
pht('Enable DarkConsole'),
pht('Disable DarkConsole'),
))
->setSummary(pht("Enable Phabricator's debugging console."))
->setDescription(
pht(
"DarkConsole is a development and profiling tool built into ".
"Phabricator's web interface. You should leave it disabled unless ".
"you are developing or debugging Phabricator.\n\n".
"Once you activate DarkConsole for the install, **you need to ".
"enable it for your account before it will actually appear on ".
"pages.** You can do this in Settings > Developer Settings.\n\n".
"DarkConsole exposes potentially sensitive data (like queries, ".
"stack traces, and configuration) so you generally should not ".
"turn it on in production.")),
$this->newOption('darkconsole.always-on', 'bool', false)
->setBoolOptions(
array(
pht('Always Activate DarkConsole'),
pht('Require DarkConsole Activation'),
))
->setSummary(pht('Activate DarkConsole on every page.'))
->setDescription(
pht(
"This option allows you to enable DarkConsole on every page, ".
"even for logged-out users. This is only really useful if you ".
"need to debug something on a logged-out page. You should not ".
"enable this option in production.\n\n".
- "You must enable DarkConsole by setting {{darkconsole.enabled}} ".
- "before this option will have any effect.")),
+ "You must enable DarkConsole by setting '%s' ".
+ "before this option will have any effect.",
+ 'darkconsole.enabled')),
$this->newOption('debug.time-limit', 'int', null)
->setSummary(
pht(
'Limit page execution time to debug hangs.'))
->setDescription(
pht(
"This option can help debug pages which are taking a very ".
"long time (more than 30 seconds) to render.\n\n".
"If a page is slow to render (but taking less than 30 seconds), ".
"the best tools to use to figure out why it is slow are usually ".
"the DarkConsole service call profiler and XHProf.\n\n".
"However, if a request takes a very long time to return, some ".
"components (like Apache, nginx, or PHP itself) may abort the ".
"request before it finishes. This can prevent you from using ".
"profiling tools to understand page performance in detail.\n\n".
"In these cases, you can use this option to force the page to ".
"abort after a smaller number of seconds (for example, 10), and ".
"dump a useful stack trace. This can provide useful information ".
"about why a page is hanging.\n\n".
"To use this option, set it to a small number (like 10), and ".
"reload a hanging page. The page should exit after 10 seconds ".
"and give you a stack trace.\n\n".
"You should turn this option off (set it to 0) when you are ".
"done with it. Leaving it on creates a small amount of overhead ".
"for all requests, even if they do not hit the time limit.")),
$this->newOption('debug.stop-on-redirect', 'bool', false)
->setBoolOptions(
array(
pht('Stop Before HTTP Redirect'),
pht('Use Normal HTTP Redirects'),
))
->setSummary(
pht(
'Confirm before redirecting so DarkConsole can be examined.'))
->setDescription(
pht(
'Normally, Phabricator issues HTTP redirects after a successful '.
'POST. This can make it difficult to debug things which happen '.
'while processing the POST, because service and profiling '.
'information are lost. By setting this configuration option, '.
'Phabricator will show a page instead of automatically '.
'redirecting, allowing you to examine service and profiling '.
'information. It also makes the UX awful, so you should only '.
'enable it when debugging.')),
$this->newOption('debug.profile-rate', 'int', 0)
->addExample(0, pht('No profiling'))
->addExample(1, pht('Profile every request (slow)'))
->addExample(1000, pht('Profile 0.1%% of all requests'))
->setSummary(pht('Automatically profile some percentage of pages.'))
->setDescription(
pht(
"Normally, Phabricator profiles pages only when explicitly ".
"requested via DarkConsole. However, it may be useful to profile ".
"some pages automatically.\n\n".
"Set this option to a positive integer N to profile 1 / N pages ".
"automatically. For example, setting it to 1 will profile every ".
"page, while setting it to 1000 will profile 1 page per 1000 ".
"requests (i.e., 0.1%% of requests).\n\n".
"Since profiling is slow and generates a lot of data, you should ".
"set this to 0 in production (to disable it) or to a large number ".
"(to collect a few samples, if you're interested in having some ".
"data to look at eventually). In development, it may be useful to ".
"set it to 1 in order to debug performance problems.\n\n".
"NOTE: You must install XHProf for profiling to work.")),
$this->newOption('debug.sample-rate', 'int', 1000)
->setLocked(true)
->addExample(0, pht('No performance sampling.'))
->addExample(1, pht('Sample every request (slow).'))
->addExample(1000, pht('Sample 0.1%% of requests.'))
->setSummary(pht('Automatically sample some fraction of requests.'))
->setDescription(
pht(
"The Multimeter application collects performance samples. You ".
"can use this data to help you understand what Phabricator is ".
"spending time and resources doing, and to identify problematic ".
"access patterns.".
"\n\n".
"This option controls how frequently sampling activates. Set it ".
"to some positive integer N to sample every 1 / N pages.".
"\n\n".
"For most installs, the default value (1 sample per 1000 pages) ".
"should collect enough data to be useful without requiring much ".
"storage or meaningfully impacting performance. If you're ".
"investigating performance issues, you can adjust the rate ".
"in order to collect more data.")),
$this->newOption('phabricator.developer-mode', 'bool', false)
->setBoolOptions(
array(
pht('Enable developer mode'),
pht('Disable developer mode'),
))
->setSummary(pht('Enable verbose error reporting and disk reads.'))
->setDescription(
pht(
'This option enables verbose error reporting (stack traces, '.
'error callouts) and forces disk reads of static assets on '.
'every reload.')),
$this->newOption('celerity.minify', 'bool', true)
->setBoolOptions(
array(
pht('Minify static resources.'),
pht("Don't minify static resources."),
))
->setSummary(pht('Minify static Celerity resources.'))
->setDescription(
pht(
'Minify static resources by removing whitespace and comments. You '.
'should enable this in production, but disable it in '.
'development.')),
$this->newOption('cache.enable-deflate', 'bool', true)
->setBoolOptions(
array(
pht('Enable deflate compression'),
pht('Disable deflate compression'),
))
->setSummary(
- pht('Toggle gzdeflate()-based compression for some caches.'))
+ pht('Toggle %s-based compression for some caches.', 'gzdeflate()'))
->setDescription(
pht(
- 'Set this to false to disable the use of gzdeflate()-based '.
+ 'Set this to false to disable the use of %s-based '.
'compression in some caches. This may give you less performant '.
- '(but more debuggable) caching.')),
+ '(but more debuggable) caching.',
+ 'gzdeflate()')),
);
}
}
diff --git a/src/applications/config/option/PhabricatorMailgunConfigOptions.php b/src/applications/config/option/PhabricatorMailgunConfigOptions.php
index 3baf4e679..aebcba672 100644
--- a/src/applications/config/option/PhabricatorMailgunConfigOptions.php
+++ b/src/applications/config/option/PhabricatorMailgunConfigOptions.php
@@ -1,37 +1,38 @@
<?php
final class PhabricatorMailgunConfigOptions
extends PhabricatorApplicationConfigOptions {
public function getName() {
return pht('Integration with Mailgun');
}
public function getDescription() {
return pht('Configure Mailgun integration.');
}
public function getFontIcon() {
return 'fa-send-o';
}
public function getGroup() {
return 'core';
}
public function getOptions() {
return array(
$this->newOption('mailgun.domain', 'string', null)
->setLocked(true)
->setDescription(
pht(
- 'Mailgun domain name. See https://mailgun.com/cp/domains'))
- ->addExample('mycompany.com', 'Use specific domain'),
+ 'Mailgun domain name. See %s.',
+ 'https://mailgun.com/cp/domains'))
+ ->addExample('mycompany.com', pht('Use specific domain')),
$this->newOption('mailgun.api-key', 'string', null)
->setHidden(true)
->setDescription(pht('Mailgun API key.')),
);
}
}
diff --git a/src/applications/config/option/PhabricatorMetaMTAConfigOptions.php b/src/applications/config/option/PhabricatorMetaMTAConfigOptions.php
index d190f1b39..55c8c5689 100644
--- a/src/applications/config/option/PhabricatorMetaMTAConfigOptions.php
+++ b/src/applications/config/option/PhabricatorMetaMTAConfigOptions.php
@@ -1,344 +1,347 @@
<?php
final class PhabricatorMetaMTAConfigOptions
extends PhabricatorApplicationConfigOptions {
public function getName() {
return pht('Mail');
}
public function getDescription() {
return pht('Configure Mail.');
}
public function getFontIcon() {
return 'fa-send';
}
public function getGroup() {
return 'core';
}
public function getOptions() {
$send_as_user_desc = $this->deformat(pht(<<<EODOC
When a user takes an action which generates an email notification (like
commenting on a Differential revision), Phabricator can either send that mail
"From" the user's email address (like "alincoln@logcabin.com") or "From" the
-'metamta.default-address' address.
+'%s' address.
The user experience is generally better if Phabricator uses the user's real
address as the "From" since the messages are easier to organize when they appear
in mail clients, but this will only work if the server is authorized to send
email on behalf of the "From" domain. Practically, this means:
- If you are doing an install for Example Corp and all the users will have
corporate @corp.example.com addresses and any hosts Phabricator is running
on are authorized to send email from corp.example.com, you can enable this
to make the user experience a little better.
- If you are doing an install for an open source project and your users will
be registering via Facebook and using personal email addresses, you probably
should not enable this or all of your outgoing email might vanish into SFP
blackholes.
- If your install is anything else, you're safer leaving this off, at least
initially, since the risk in turning it on is that your outgoing mail will
never arrive.
EODOC
-));
+ ,
+ 'metamta.default-address'));
$one_mail_per_recipient_desc = $this->deformat(pht(<<<EODOC
When a message is sent to multiple recipients (for example, several reviewers on
a code review), Phabricator can either deliver one email to everyone (e.g., "To:
alincoln, usgrant, htaft") or separate emails to each user (e.g., "To:
alincoln", "To: usgrant", "To: htaft"). The major advantages and disadvantages
of each approach are:
- One mail to everyone:
- Recipients can see To/Cc at a glance.
- If you use mailing lists, you won't get duplicate mail if you're
a normal recipient and also Cc'd on a mailing list.
- Getting threading to work properly is harder, and probably requires
making mail less useful by turning off options.
- Sometimes people will "Reply All" and everyone will get two mails,
one from the user and one from Phabricator turning their mail into
a comment.
- Not supported with a private reply-to address.
- Mails are sent in the server default translation.
- One mail to each user:
- Recipients need to look in the mail body to see To/Cc.
- If you use mailing lists, recipients may sometimes get duplicate
mail.
- Getting threading to work properly is easier, and threading settings
can be customzied by each user.
- "Reply All" no longer spams all other users.
- Required if private reply-to addresses are configured.
- Mails are sent in the language of user preference.
In the code, splitting one outbound email into one-per-recipient is sometimes
referred to as "multiplexing".
EODOC
));
$herald_hints_description = $this->deformat(pht(<<<EODOC
You can disable the Herald hints in email if users prefer smaller messages.
These are the links under the header "WHY DID I GET THIS EMAIL?". If you set
this to `false`, they will not appear in any mail. Users can still navigate to
the links via the web interface.
EODOC
));
$reply_hints_description = $this->deformat(pht(<<<EODOC
You can disable the hints under "REPLY HANDLER ACTIONS" if users prefer
smaller messages. The actions themselves will still work properly.
EODOC
));
$recipient_hints_description = $this->deformat(pht(<<<EODOC
You can disable the "To:" and "Cc:" footers in mail if users prefer smaller
messages.
EODOC
));
$bulk_description = $this->deformat(pht(<<<EODOC
If this option is enabled, Phabricator will add a "Precedence: bulk" header to
transactional mail (e.g., Differential, Maniphest and Herald notifications).
This may improve the behavior of some auto-responder software and prevent it
from replying. However, it may also cause deliverability issues -- notably, you
currently can not send this header via Amazon SES, and enabling this option with
SES will prevent delivery of any affected mail.
EODOC
));
$email_preferences_description = $this->deformat(pht(<<<EODOC
You can disable the email preference link in emails if users prefer smaller
emails.
EODOC
));
$re_prefix_description = $this->deformat(pht(<<<EODOC
Mail.app on OS X Lion won't respect threading headers unless the subject is
prefixed with "Re:". If you enable this option, Phabricator will add "Re:" to
the subject line of all mail which is expected to thread. If you've set
'metamta.one-mail-per-recipient', users can override this setting in their
preferences.
EODOC
));
$vary_subjects_description = $this->deformat(pht(<<<EODOC
If true, allow MetaMTA to change mail subjects to put text like '[Accepted]' and
'[Commented]' in them. This makes subjects more useful, but might break
-threading on some clients. If you've set 'metamta.one-mail-per-recipient', users
-can override this setting in their preferences.
+threading on some clients. If you've set '%s', users can override this setting
+in their preferences.
EODOC
-));
+ ,
+ 'metamta.one-mail-per-recipient'));
$reply_to_description = $this->deformat(pht(<<<EODOC
-If you enable {{metamta.public-replies}}, Phabricator uses "From" to
-authenticate users. You can additionally enable this setting to try to
-authenticate with 'Reply-To'. Note that this is completely spoofable and
-insecure (any user can set any 'Reply-To' address) but depending on the nature
-of your install or other deliverability conditions this might be okay.
-Generally, you can't do much more by spoofing Reply-To than be annoying (you can
-write but not read content). But this is still **COMPLETELY INSECURE**.
+If you enable `%s`, Phabricator uses "From" to authenticate users. You can
+additionally enable this setting to try to authenticate with 'Reply-To'. Note
+that this is completely spoofable and insecure (any user can set any 'Reply-To'
+address) but depending on the nature of your install or other deliverability
+conditions this might be okay. Generally, you can't do much more by spoofing
+Reply-To than be annoying (you can write but not read content). But this is
+still **COMPLETELY INSECURE**.
EODOC
-));
+ ,
+ 'metamta.public-replies'));
$adapter_description = $this->deformat(pht(<<<EODOC
Adapter class to use to transmit mail to the MTA. The default uses
PHPMailerLite, which will invoke "sendmail". This is appropriate if sendmail
actually works on your host, but if you haven't configured mail it may not be so
great. A number of other mailers are available (e.g., SES, SendGrid, SMTP,
custom mailers), consult "Configuring Outbound Email" in the documentation for
details.
EODOC
));
$placeholder_description = $this->deformat(pht(<<<EODOC
When sending a message that has no To recipient (i.e. all recipients are CC'd,
for example when multiplexing mail), set the To field to the following value. If
no value is set, messages with no To will have their CCs upgraded to To.
EODOC
));
$public_replies_description = $this->deformat(pht(<<<EODOC
By default, Phabricator generates unique reply-to addresses and sends a separate
email to each recipient when you enable reply handling. This is more secure than
using "From" to establish user identity, but can mean users may receive multiple
emails when they are on mailing lists. Instead, you can use a single, non-unique
reply to address and authenticate users based on the "From" address by setting
this to 'true'. This trades away a little bit of security for convenience, but
it's reasonable in many installs. Object interactions are still protected using
hashes in the single public email address, so objects can not be replied to
blindly.
EODOC
));
$single_description = $this->deformat(pht(<<<EODOC
If you want to use a single mailbox for Phabricator reply mail, you can use this
and set a common prefix for reply addresses generated by Phabricator. It will
make use of the fact that a mail-address such as
`phabricator+D123+1hjk213h@example.com` will be delivered to the `phabricator`
user's mailbox. Set this to the left part of the email address and it will be
prepended to all generated reply addresses.
For example, if you want to use `phabricator@example.com`, this should be set
to `phabricator`.
EODOC
));
$address_description = $this->deformat(pht(<<<EODOC
When email is sent, what format should Phabricator use for user's email
addresses? Valid values are:
- `short`: 'gwashington <gwashington@example.com>'
- `real`: 'George Washington <gwashington@example.com>'
- `full`: 'gwashington (George Washington) <gwashington@example.com>'
The default is `full`.
EODOC
));
return array(
$this->newOption(
'metamta.default-address',
'string',
'noreply@phabricator.example.com')
->setDescription(pht('Default "From" address.')),
$this->newOption(
'metamta.domain',
'string',
'phabricator.example.com')
->setDescription(pht('Domain used to generate Message-IDs.')),
$this->newOption(
'metamta.mail-adapter',
'class',
'PhabricatorMailImplementationPHPMailerLiteAdapter')
->setBaseClass('PhabricatorMailImplementationAdapter')
->setSummary(pht('Control how mail is sent.'))
->setDescription($adapter_description),
$this->newOption(
'metamta.one-mail-per-recipient',
'bool',
true)
->setBoolOptions(
array(
pht('Send Mail To Each Recipient'),
pht('Send Mail To All Recipients'),
))
->setSummary(
pht(
'Controls whether Phabricator sends one email with multiple '.
'recipients in the "To:" line, or multiple emails, each with a '.
'single recipient in the "To:" line.'))
->setDescription($one_mail_per_recipient_desc),
$this->newOption('metamta.can-send-as-user', 'bool', false)
->setBoolOptions(
array(
pht('Send as User Taking Action'),
pht('Send as Phabricator'),
))
->setSummary(
pht(
'Controls whether Phabricator sends email "From" users.'))
->setDescription($send_as_user_desc),
$this->newOption(
'metamta.reply-handler-domain',
'string',
null)
->setLocked(true)
->setDescription(pht('Domain used for reply email addresses.'))
->addExample('phabricator.example.com', ''),
$this->newOption('metamta.herald.show-hints', 'bool', true)
->setBoolOptions(
array(
pht('Show Herald Hints'),
pht('No Herald Hints'),
))
->setSummary(pht('Show hints about Herald rules in email.'))
->setDescription($herald_hints_description),
$this->newOption('metamta.recipients.show-hints', 'bool', true)
->setBoolOptions(
array(
pht('Show Recipient Hints'),
pht('No Recipient Hints'),
))
->setSummary(pht('Show "To:" and "Cc:" footer hints in email.'))
->setDescription($recipient_hints_description),
$this->newOption('metamta.email-preferences', 'bool', true)
->setBoolOptions(
array(
pht('Show Email Preferences Link'),
pht('No Email Preferences Link'),
))
->setSummary(pht('Show email preferences link in email.'))
->setDescription($email_preferences_description),
$this->newOption('metamta.re-prefix', 'bool', false)
->setBoolOptions(
array(
pht('Force "Re:" Subject Prefix'),
pht('No "Re:" Subject Prefix'),
))
->setSummary(pht('Control "Re:" subject prefix, for Mail.app.'))
->setDescription($re_prefix_description),
$this->newOption('metamta.vary-subjects', 'bool', true)
->setBoolOptions(
array(
pht('Allow Varied Subjects'),
pht('Always Use the Same Thread Subject'),
))
->setSummary(pht('Control subject variance, for some mail clients.'))
->setDescription($vary_subjects_description),
$this->newOption('metamta.insecure-auth-with-reply-to', 'bool', false)
->setBoolOptions(
array(
pht('Allow Insecure Reply-To Auth'),
pht('Disallow Reply-To Auth'),
))
->setSummary(pht('Trust "Reply-To" headers for authentication.'))
->setDescription($reply_to_description),
$this->newOption('metamta.placeholder-to-recipient', 'string', null)
->setSummary(pht('Placeholder for mail with only CCs.'))
->setDescription($placeholder_description),
$this->newOption('metamta.public-replies', 'bool', false)
->setBoolOptions(
array(
pht('Use Public Replies (Less Secure)'),
pht('Use Private Replies (More Secure)'),
))
->setSummary(
pht(
'Phabricator can use less-secure but mailing list friendly public '.
'reply addresses.'))
->setDescription($public_replies_description),
$this->newOption('metamta.single-reply-handler-prefix', 'string', null)
->setSummary(
pht('Allow Phabricator to use a single mailbox for all replies.'))
->setDescription($single_description),
$this->newOption('metamta.user-address-format', 'enum', 'full')
->setEnumOptions(
array(
'short' => 'short',
'real' => 'real',
'full' => 'full',
))
->setSummary(pht('Control how Phabricator renders user names in mail.'))
->setDescription($address_description)
->addExample('gwashington <gwashington@example.com>', 'short')
->addExample('George Washington <gwashington@example.com>', 'real')
->addExample(
'gwashington (George Washington) <gwashington@example.com>',
'full'),
$this->newOption('metamta.email-body-limit', 'int', 524288)
->setDescription(
pht(
'You can set a limit for the maximum byte size of outbound mail. '.
'Mail which is larger than this limit will be truncated before '.
'being sent. This can be useful if your MTA rejects mail which '.
'exceeds some limit (this is reasonably common). Specify a value '.
'in bytes.'))
->setSummary(pht('Global cap for size of generated emails (bytes).'))
->addExample(524288, pht('Truncate at 512KB'))
->addExample(1048576, pht('Truncate at 1MB')),
);
}
}
diff --git a/src/applications/config/option/PhabricatorMySQLConfigOptions.php b/src/applications/config/option/PhabricatorMySQLConfigOptions.php
index b1e25ab41..e3cd480e2 100644
--- a/src/applications/config/option/PhabricatorMySQLConfigOptions.php
+++ b/src/applications/config/option/PhabricatorMySQLConfigOptions.php
@@ -1,87 +1,87 @@
<?php
final class PhabricatorMySQLConfigOptions
extends PhabricatorApplicationConfigOptions {
public function getName() {
return pht('MySQL');
}
public function getDescription() {
return pht('Database configuration.');
}
public function getFontIcon() {
return 'fa-database';
}
public function getGroup() {
return 'core';
}
public function getOptions() {
return array(
$this->newOption('mysql.host', 'string', 'localhost')
->setLocked(true)
->setDescription(
pht('MySQL database hostname.'))
->addExample('localhost', pht('MySQL on this machine'))
->addExample('db.example.com:3300', pht('Nonstandard port')),
$this->newOption('mysql.user', 'string', 'root')
->setLocked(true)
->setDescription(
pht('MySQL username to use when connecting to the database.')),
$this->newOption('mysql.pass', 'string', null)
->setHidden(true)
->setDescription(
pht('MySQL password to use when connecting to the database.')),
$this->newOption(
'mysql.configuration-provider',
'class',
'DefaultDatabaseConfigurationProvider')
->setLocked(true)
->setBaseClass('DatabaseConfigurationProvider')
->setSummary(
pht('Configure database configuration class.'))
->setDescription(
pht(
'Phabricator chooses which database to connect to through a '.
'swappable configuration provider. You almost certainly do not '.
'need to change this.')),
$this->newOption(
'mysql.implementation',
'class',
(extension_loaded('mysqli')
? 'AphrontMySQLiDatabaseConnection'
: 'AphrontMySQLDatabaseConnection'))
->setLocked(true)
->setBaseClass('AphrontMySQLDatabaseConnectionBase')
->setSummary(
pht('Configure database connection class.'))
->setDescription(
pht(
'Phabricator connects to MySQL through a swappable abstraction '.
'layer. You can choose an alternate implementation by setting '.
'this option. To provide your own implementation, extend '.
- '`AphrontMySQLDatabaseConnectionBase`. It is very unlikely that '.
- 'you need to change this.')),
+ '`%s`. It is very unlikely that you need to change this.',
+ 'AphrontMySQLDatabaseConnectionBase')),
$this->newOption('storage.default-namespace', 'string', 'phabricator')
->setLocked(true)
->setSummary(
pht('The namespace that Phabricator databases should use.'))
->setDescription(
pht(
"Phabricator puts databases in a namespace, which defaults to ".
"'phabricator' -- for instance, the Differential database is ".
"named 'phabricator_differential' by default. You can change ".
"this namespace if you want. Normally, you should not do this ".
"unless you are developing Phabricator and using namespaces to ".
"separate multiple sandbox datasets.")),
$this->newOption('mysql.port', 'string', null)
->setLocked(true)
->setDescription(
pht('MySQL port to use when connecting to the database.')),
);
}
}
diff --git a/src/applications/config/option/PhabricatorPHDConfigOptions.php b/src/applications/config/option/PhabricatorPHDConfigOptions.php
index b07e62244..59cb8ea72 100644
--- a/src/applications/config/option/PhabricatorPHDConfigOptions.php
+++ b/src/applications/config/option/PhabricatorPHDConfigOptions.php
@@ -1,84 +1,86 @@
<?php
final class PhabricatorPHDConfigOptions
extends PhabricatorApplicationConfigOptions {
public function getName() {
return pht('Daemons');
}
public function getDescription() {
return pht('Options relating to PHD (daemons).');
}
public function getFontIcon() {
return 'fa-pied-piper-alt';
}
public function getGroup() {
return 'core';
}
public function getOptions() {
return array(
$this->newOption('phd.pid-directory', 'string', '/var/tmp/phd/pid')
->setDescription(
- pht(
- 'Directory that phd should use to track running daemons.')),
+ pht('Directory that phd should use to track running daemons.')),
$this->newOption('phd.log-directory', 'string', '/var/tmp/phd/log')
->setDescription(
- pht(
- 'Directory that the daemons should use to store log files.')),
+ pht('Directory that the daemons should use to store log files.')),
$this->newOption('phd.taskmasters', 'int', 4)
->setSummary(pht('Maximum taskmaster daemon pool size.'))
->setDescription(
pht(
'Maximum number of taskmaster daemons to run at once. Raising '.
'this can increase the maximum throughput of the task queue. The '.
'pool will automatically scale down when unutilized.')),
$this->newOption('phd.verbose', 'bool', false)
->setBoolOptions(
array(
pht('Verbose mode'),
pht('Normal mode'),
))
->setSummary(pht("Launch daemons in 'verbose' mode by default."))
->setDescription(
pht(
"Launch daemons in 'verbose' mode by default. This creates a lot ".
"of output, but can help debug issues. Daemons launched in debug ".
- "mode with 'phd debug' are always launched in verbose mode. See ".
- "also 'phd.trace'.")),
+ "mode with '%s' are always launched in verbose mode. ".
+ "See also '%s'.",
+ 'phd debug',
+ 'phd.trace')),
$this->newOption('phd.user', 'string', null)
->setLocked(true)
->setSummary(pht('System user to run daemons as.'))
->setDescription(
pht(
'Specify a system user to run the daemons as. Primarily, this '.
'user will own the working copies of any repositories that '.
'Phabricator imports or manages. This option is new and '.
'experimental.')),
$this->newOption('phd.trace', 'bool', false)
->setBoolOptions(
array(
pht('Trace mode'),
pht('Normal mode'),
))
->setSummary(pht("Launch daemons in 'trace' mode by default."))
->setDescription(
pht(
"Launch daemons in 'trace' mode by default. This creates an ".
"ENORMOUS amount of output, but can help debug issues. Daemons ".
- "launched in debug mode with 'phd debug' are always launched in ".
- "trace mode. See also 'phd.verbose'.")),
+ "launched in debug mode with '%s' are always launched in ".
+ "trace mode. See also '%s'.",
+ 'phd debug',
+ 'phd.verbose')),
$this->newOption('phd.variant-config', 'list<string>', array())
->setDescription(
pht(
'Specify config keys that can safely vary between the web tier '.
'and the daemons. Primarily, this is a way to suppress the '.
'"Daemons and Web Have Different Config" setup issue on a per '.
'config key basis.')),
);
}
}
diff --git a/src/applications/config/option/PhabricatorPHPMailerConfigOptions.php b/src/applications/config/option/PhabricatorPHPMailerConfigOptions.php
index fbe98175d..b2d5c91e9 100644
--- a/src/applications/config/option/PhabricatorPHPMailerConfigOptions.php
+++ b/src/applications/config/option/PhabricatorPHPMailerConfigOptions.php
@@ -1,75 +1,78 @@
<?php
final class PhabricatorPHPMailerConfigOptions
extends PhabricatorApplicationConfigOptions {
public function getName() {
return pht('PHPMailer');
}
public function getDescription() {
return pht('Configure PHPMailer.');
}
public function getFontIcon() {
return 'fa-send-o';
}
public function getGroup() {
return 'core';
}
public function getOptions() {
return array(
$this->newOption('phpmailer.mailer', 'string', 'smtp')
->setLocked(true)
->setSummary(pht('Configure mailer used by PHPMailer.'))
->setDescription(
pht(
"If you're using PHPMailer to send email, provide the mailer and ".
"options here. PHPMailer is much more enormous than ".
"PHPMailerLite, and provides more mailers and greater enormity. ".
"You need it when you want to use SMTP instead of sendmail as the ".
"mailer.")),
$this->newOption('phpmailer.smtp-host', 'string', null)
->setLocked(true)
->setDescription(pht('Host for SMTP.')),
$this->newOption('phpmailer.smtp-port', 'int', 25)
->setLocked(true)
->setDescription(pht('Port for SMTP.')),
// TODO: Implement "enum"? Valid values are empty, 'tls', or 'ssl'.
$this->newOption('phpmailer.smtp-protocol', 'string', null)
->setLocked(true)
->setSummary(pht('Configure TLS or SSL for SMTP.'))
->setDescription(
pht(
- "Using PHPMailer with SMTP, you can set this to one of 'tls' or ".
- "'ssl' to use TLS or SSL, respectively. Leave it blank for ".
- "vanilla SMTP. If you're sending via Gmail, set it to 'ssl'.")),
+ "Using PHPMailer with SMTP, you can set this to one of '%s' or ".
+ "'%s' to use TLS or SSL, respectively. Leave it blank for ".
+ "vanilla SMTP. If you're sending via Gmail, set it to '%s'.",
+ 'tls',
+ 'ssl',
+ 'ssl')),
$this->newOption('phpmailer.smtp-user', 'string', null)
->setLocked(true)
->setDescription(pht('Username for SMTP.')),
$this->newOption('phpmailer.smtp-password', 'string', null)
->setHidden(true)
->setDescription(pht('Password for SMTP.')),
$this->newOption('phpmailer.smtp-encoding', 'string', '8bit')
->setSummary(pht('Configure how mail is encoded.'))
->setDescription(
pht(
"Mail is normally encoded in `8bit`, which works correctly with ".
"most MTAs. However, some MTAs do not work well with this ".
"encoding. If you're having trouble with mail being mangled or ".
"arriving with too many or too few newlines, you may try ".
"adjusting this setting.\n\n".
"Supported values are `8bit` (default), `quoted-printable`, ".
"`7bit`, `binary` and `base64`.\n\n".
"The settings in the table below may work well.\n\n".
"| MTA | Setting | Notes\n".
"|-----|---------|------\n".
"| SendGrid via SMTP | `quoted-printable` | Double newlines under ".
"`8bit`.\n".
"| All Other MTAs | `8bit` | Default setting.")),
);
}
}
diff --git a/src/applications/config/option/PhabricatorSMSConfigOptions.php b/src/applications/config/option/PhabricatorSMSConfigOptions.php
index 973f5db13..33b75c263 100644
--- a/src/applications/config/option/PhabricatorSMSConfigOptions.php
+++ b/src/applications/config/option/PhabricatorSMSConfigOptions.php
@@ -1,62 +1,60 @@
<?php
final class PhabricatorSMSConfigOptions
extends PhabricatorApplicationConfigOptions {
public function getName() {
return pht('SMS');
}
public function getDescription() {
return pht('Configure SMS.');
}
public function getFontIcon() {
return 'fa-mobile';
}
public function getGroup() {
return 'core';
}
public function getOptions() {
- $adapter_description = $this->deformat(pht(<<<EODOC
-Adapter class to use to transmit SMS to an external provider. A given external
-provider will most likely need more configuration which will most likely
-require registration and payment for the service.
-EODOC
- ));
+ $adapter_description = pht(
+ 'Adapter class to use to transmit SMS to an external provider. A given '.
+ 'external provider will most likely need more configuration which will '.
+ 'most likely require registration and payment for the service.');
return array(
$this->newOption(
'sms.default-sender',
'string',
null)
->setDescription(pht('Default "from" number.'))
->addExample('8675309', 'Jenny still has this number')
->addExample('18005555555', 'Maybe not a real number'),
$this->newOption(
'sms.default-adapter',
'class',
null)
->setBaseClass('PhabricatorSMSImplementationAdapter')
- ->setSummary(pht('Control how sms is sent.'))
+ ->setSummary(pht('Control how SMS is sent.'))
->setDescription($adapter_description),
$this->newOption(
'twilio.account-sid',
'string',
null)
->setDescription(pht('Account ID on Twilio service.'))
->setLocked(true)
->addExample('gf5kzccfn2sfknpnadvz7kokv6nz5v', pht('30 characters')),
$this->newOption(
'twilio.auth-token',
'string',
null)
->setDescription(pht('Authorization token from Twilio service.'))
->setHidden(true)
->addExample('f3jsi4i67wiwt6w54hf2zwvy3fjf5h', pht('30 characters')),
);
}
}
diff --git a/src/applications/config/option/PhabricatorSecurityConfigOptions.php b/src/applications/config/option/PhabricatorSecurityConfigOptions.php
index d8a8c85a9..63e43b308 100644
--- a/src/applications/config/option/PhabricatorSecurityConfigOptions.php
+++ b/src/applications/config/option/PhabricatorSecurityConfigOptions.php
@@ -1,341 +1,346 @@
<?php
final class PhabricatorSecurityConfigOptions
extends PhabricatorApplicationConfigOptions {
public function getName() {
return pht('Security');
}
public function getDescription() {
return pht('Security options.');
}
public function getFontIcon() {
return 'fa-lock';
}
public function getGroup() {
return 'core';
}
public function getOptions() {
$support_href = PhabricatorEnv::getDoclink('Give Feedback! Get Support!');
$doc_href = PhabricatorEnv::getDoclink('Configuring a File Domain');
$doc_name = pht('Configuration Guide: Configuring a File Domain');
// This is all of the IANA special/reserved blocks in IPv4 space.
$default_address_blacklist = array(
'0.0.0.0/8',
'10.0.0.0/8',
'100.64.0.0/10',
'127.0.0.0/8',
'169.254.0.0/16',
'172.16.0.0/12',
'192.0.0.0/24',
'192.0.2.0/24',
'192.88.99.0/24',
'192.168.0.0/16',
'198.18.0.0/15',
'198.51.100.0/24',
'203.0.113.0/24',
'224.0.0.0/4',
'240.0.0.0/4',
'255.255.255.255/32',
);
return array(
$this->newOption('security.alternate-file-domain', 'string', null)
->setLocked(true)
->setSummary(pht('Alternate domain to serve files from.'))
->setDescription(
pht(
'By default, Phabricator serves files from the same domain '.
'the application is served from. This is convenient, but '.
'presents a security risk.'.
"\n\n".
'You should configure a CDN or alternate file domain to mitigate '.
'this risk. Configuring a CDN will also improve performance. See '.
'[[ %s | %s ]] for instructions.',
$doc_href,
$doc_name))
->addExample('https://files.phabcdn.net/', pht('Valid Setting')),
$this->newOption(
'security.hmac-key',
'string',
'[D\t~Y7eNmnQGJ;rnH6aF;m2!vJ8@v8C=Cs:aQS\.Qw')
->setHidden(true)
->setSummary(
pht('Key for HMAC digests.'))
->setDescription(
pht(
'Default key for HMAC digests where the key is not important '.
'(i.e., the hash itself is secret). You can change this if you '.
'want (to any other string), but doing so will break existing '.
'sessions and CSRF tokens.')),
$this->newOption('security.require-https', 'bool', false)
->setLocked(true)
->setSummary(
pht('Force users to connect via HTTPS instead of HTTP.'))
->setDescription(
pht(
"If the web server responds to both HTTP and HTTPS requests but ".
"you want users to connect with only HTTPS, you can set this ".
"to true to make Phabricator redirect HTTP requests to HTTPS.\n\n".
"Normally, you should just configure your server not to accept ".
"HTTP traffic, but this setting may be useful if you originally ".
"used HTTP and have now switched to HTTPS but don't want to ".
"break old links, or if your webserver sits behind a load ".
"balancer which terminates HTTPS connections and you can not ".
"reasonably configure more granular behavior there.\n\n".
"IMPORTANT: Phabricator determines if a request is HTTPS or not ".
- "by examining the PHP \$_SERVER['HTTPS'] variable. If you run ".
+ "by examining the PHP `%s` variable. If you run ".
"Apache/mod_php this will probably be set correctly for you ".
"automatically, but if you run Phabricator as CGI/FCGI (e.g., ".
"through nginx or lighttpd), you need to configure your web ".
"server so that it passes the value correctly based on the ".
- "connection type."))
+ "connection type.",
+ "\$_SERVER['HTTPS']"))
->setBoolOptions(
array(
pht('Force HTTPS'),
pht('Allow HTTP'),
)),
$this->newOption('security.require-multi-factor-auth', 'bool', false)
->setLocked(true)
->setSummary(
pht('Require all users to configure multi-factor authentication.'))
->setDescription(
pht(
'By default, Phabricator allows users to add multi-factor '.
'authentication to their accounts, but does not require it. '.
'By enabling this option, you can force all users to add '.
'at least one authentication factor before they can use their '.
'accounts.'))
->setBoolOptions(
array(
pht('Multi-Factor Required'),
pht('Multi-Factor Optional'),
)),
$this->newOption(
'phabricator.csrf-key',
'string',
'0b7ec0592e0a2829d8b71df2fa269b2c6172eca3')
->setHidden(true)
->setSummary(
pht('Hashed with other inputs to generate CSRF tokens.'))
->setDescription(
pht(
'This is hashed with other inputs to generate CSRF tokens. If '.
'you want, you can change it to some other string which is '.
'unique to your install. This will make your install more secure '.
'in a vague, mostly theoretical way. But it will take you like 3 '.
'seconds of mashing on your keyboard to set it up so you might '.
'as well.')),
$this->newOption(
'phabricator.mail-key',
'string',
'5ce3e7e8787f6e40dfae861da315a5cdf1018f12')
->setHidden(true)
->setSummary(
pht('Hashed with other inputs to generate mail tokens.'))
->setDescription(
pht(
"This is hashed with other inputs to generate mail tokens. If ".
"you want, you can change it to some other string which is ".
"unique to your install. In particular, you will want to do ".
"this if you accidentally send a bunch of mail somewhere you ".
"shouldn't have, to invalidate all old reply-to addresses.")),
$this->newOption(
'uri.allowed-protocols',
'set',
array(
'http' => true,
'https' => true,
'mailto' => true,
))
->setSummary(
pht('Determines which URI protocols are auto-linked.'))
->setDescription(
pht(
"When users write comments which have URIs, they'll be ".
"automatically linked if the protocol appears in this set. This ".
"whitelist is primarily to prevent security issues like ".
- "javascript:// URIs."))
+ "%s URIs.",
+ 'javascript://'))
->addExample("http\nhttps", pht('Valid Setting'))
->setLocked(true),
$this->newOption(
'uri.allowed-editor-protocols',
'set',
array(
'http' => true,
'https' => true,
// This handler is installed by Textmate.
'txmt' => true,
// This handler is for MacVim.
'mvim' => true,
// Unofficial handler for Vim.
'vim' => true,
// Unofficial handler for Sublime.
'subl' => true,
// Unofficial handler for Emacs.
'emacs' => true,
// This isn't a standard handler installed by an application, but
// is a reasonable name for a user-installed handler.
'editor' => true,
))
->setSummary(pht('Whitelists editor protocols for "Open in Editor".'))
->setDescription(
pht(
"Users can configure a URI pattern to open files in a text ".
"editor. The URI must use a protocol on this whitelist.\n\n".
"(If you use an editor which defines a protocol not on this ".
"list, [[ %s | let us know ]] and we'll update the defaults.)",
$support_href))
->setLocked(true),
$this->newOption(
'celerity.resource-hash',
'string',
'd9455ea150622ee044f7931dabfa52aa')
->setSummary(
pht('An input to the hash function when building resource hashes.'))
->setDescription(
pht(
'This value is an input to the hash function when building '.
'resource hashes. It has no security value, but if you '.
'accidentally poison user caches (by pushing a bad patch or '.
'having something go wrong with a CDN, e.g.) you can change this '.
'to something else and rebuild the Celerity map to break user '.
'caches. Unless you are doing Celerity development, it is '.
'exceptionally unlikely that you need to modify this.')),
$this->newOption('remarkup.enable-embedded-youtube', 'bool', false)
->setBoolOptions(
array(
pht('Embed YouTube videos'),
pht("Don't embed YouTube videos"),
))
->setSummary(
pht('Determines whether or not YouTube videos get embedded.'))
->setDescription(
pht(
- "If you enable this, linked YouTube videos will be embeded ".
+ "If you enable this, linked YouTube videos will be embedded ".
"inline. This has mild security implications (you'll leak ".
"referrers to YouTube) and is pretty silly (but sort of ".
"awesome).")),
$this->newOption(
'security.outbound-blacklist',
'list<string>',
$default_address_blacklist)
->setLocked(true)
->setSummary(
pht(
'Blacklist subnets to prevent user-initiated outbound '.
'requests.'))
->setDescription(
pht(
'Phabricator users can make requests to other services from '.
'the Phabricator host in some circumstances (for example, by '.
'creating a repository with a remote URL or having Phabricator '.
'fetch an image from a remote server).'.
"\n\n".
'This may represent a security vulnerability if services on '.
'the same subnet will accept commands or reveal private '.
'information over unauthenticated HTTP GET, based on the source '.
'IP address. In particular, all hosts in EC2 have access to '.
'such a service.'.
"\n\n".
'This option defines a list of netblocks which Phabricator '.
'will decline to connect to. Generally, you should list all '.
'private IP space here.'))
->addExample(array('0.0.0.0/0'), pht('No Outbound Requests')),
$this->newOption('security.strict-transport-security', 'bool', false)
->setLocked(true)
->setBoolOptions(
array(
pht('Use HSTS'),
pht('Do Not Use HSTS'),
))
->setSummary(pht('Enable HTTP Strict Transport Security (HSTS).'))
->setDescription(
pht(
'HTTP Strict Transport Security (HSTS) sends a header which '.
'instructs browsers that the site should only be accessed '.
'over HTTPS, never HTTP. This defuses an attack where an '.
'adversary gains access to your network, then proxies requests '.
'through an unsecured link.'.
"\n\n".
'Do not enable this option if you serve (or plan to ever serve) '.
'unsecured content over plain HTTP. It is very difficult to '.
'undo this change once users\' browsers have accepted the '.
'setting.')),
$this->newOption('security.allow-conduit-act-as-user', 'bool', false)
->setBoolOptions(
array(
pht('Allow'),
pht('Disallow'),
))
->setLocked(true)
->setSummary(
pht('Allow administrators to use the Conduit API as other users.'))
->setDescription(
pht(
'DEPRECATED - if you enable this, you are allowing '.
'administrators to act as any user via the Conduit API. '.
'Enabling this is not advised as it introduces a huge policy '.
'violation and has been obsoleted in functionality.')),
);
}
protected function didValidateOption(
PhabricatorConfigOption $option,
$value) {
$key = $option->getKey();
if ($key == 'security.alternate-file-domain') {
$uri = new PhutilURI($value);
$protocol = $uri->getProtocol();
if ($protocol !== 'http' && $protocol !== 'https') {
throw new PhabricatorConfigValidationException(
pht(
"Config option '%s' is invalid. The URI must start with ".
- "'http://' or 'https://'.",
- $key));
+ "'%s' or '%s'.",
+ $key,
+ 'http://',
+ 'https://'));
}
$domain = $uri->getDomain();
if (strpos($domain, '.') === false) {
throw new PhabricatorConfigValidationException(
pht(
"Config option '%s' is invalid. The URI must contain a dot ('.'), ".
- "like 'http://example.com/', not just a bare name like ".
- "'http://example/'. Some web browsers will not set cookies on ".
- "domains with no TLD.",
- $key));
+ "like '%s', not just a bare name like '%s'. ".
+ "Some web browsers will not set cookies on domains with no TLD.",
+ $key,
+ 'http://example.com/',
+ 'http://example/'));
}
$path = $uri->getPath();
if ($path !== '' && $path !== '/') {
throw new PhabricatorConfigValidationException(
pht(
"Config option '%s' is invalid. The URI must NOT have a path, ".
- "e.g. 'http://phabricator.example.com/' is OK, but ".
- "'http://example.com/phabricator/' is not. Phabricator must be ".
- "installed on an entire domain; it can not be installed on a ".
- "path.",
- $key));
+ "e.g. '%s' is OK, but '%s' is not. Phabricator must be installed ".
+ "on an entire domain; it can not be installed on a path.",
+ $key,
+ 'http://phabricator.example.com/',
+ 'http://example.com/phabricator/'));
}
}
}
}
diff --git a/src/applications/config/option/PhabricatorSyntaxHighlightingConfigOptions.php b/src/applications/config/option/PhabricatorSyntaxHighlightingConfigOptions.php
index 90c1e8790..c2511b0f8 100644
--- a/src/applications/config/option/PhabricatorSyntaxHighlightingConfigOptions.php
+++ b/src/applications/config/option/PhabricatorSyntaxHighlightingConfigOptions.php
@@ -1,143 +1,143 @@
<?php
final class PhabricatorSyntaxHighlightingConfigOptions
extends PhabricatorApplicationConfigOptions {
public function getName() {
return pht('Syntax Highlighting');
}
public function getDescription() {
return pht('Options relating to syntax highlighting source code.');
}
public function getFontIcon() {
return 'fa-code';
}
public function getGroup() {
return 'core';
}
public function getOptions() {
$caches_href = PhabricatorEnv::getDocLink('Managing Caches');
return array(
$this->newOption(
'syntax-highlighter.engine',
'class',
'PhutilDefaultSyntaxHighlighterEngine')
->setBaseClass('PhutilSyntaxHighlighterEngine')
->setSummary(pht('Default non-pygments syntax highlighter engine.'))
->setDescription(
pht(
'Phabricator can highlight PHP by default and use Pygments for '.
'other languages if enabled. You can provide a custom '.
- 'highlighter engine by extending class '.
- 'PhutilSyntaxHighlighterEngine.')),
+ 'highlighter engine by extending class %s.',
+ 'PhutilSyntaxHighlighterEngine')),
$this->newOption('pygments.enabled', 'bool', false)
->setSummary(
pht('Should Phabricator use Pygments to highlight code?'))
->setBoolOptions(
array(
pht('Use Pygments'),
pht('Do Not Use Pygments'),
))
->setDescription(
pht(
'Phabricator supports syntax highlighting a few languages by '.
'default, but you can install Pygments (a third-party syntax '.
'highlighting tool) to provide support for many more languages.'.
"\n\n".
'To install Pygments, visit '.
'[[ http://pygments.org | pygments.org ]] and follow the '.
'download and install instructions.'.
"\n\n".
'Once Pygments is installed, enable this option '.
'(`pygments.enabled`) to make Phabricator use Pygments when '.
'highlighting source code.'.
"\n\n".
'After you install and enable Pygments, newly created source '.
'code (like diffs and pastes) should highlight correctly. '.
'You may need to clear Phabricator\'s caches to get previously '.
'existing source code to highlight. For instructions on '.
'managing caches, see [[ %s | Managing Caches ]].',
$caches_href)),
$this->newOption(
'pygments.dropdown-choices',
'wild',
array(
'apacheconf' => 'Apache Configuration',
'bash' => 'Bash Scripting',
'brainfuck' => 'Brainf*ck',
'c' => 'C',
'coffee-script' => 'CoffeeScript',
'cpp' => 'C++',
'css' => 'CSS',
'd' => 'D',
'diff' => 'Diff',
'django' => 'Django Templating',
'erb' => 'Embedded Ruby/ERB',
'erlang' => 'Erlang',
'go' => 'Golang',
'groovy' => 'Groovy',
'haskell' => 'Haskell',
'html' => 'HTML',
'invisible' => 'Invisible',
'java' => 'Java',
'js' => 'Javascript',
'json' => 'JSON',
'mysql' => 'MySQL',
'objc' => 'Objective-C',
'perl' => 'Perl',
'php' => 'PHP',
'puppet' => 'Puppet',
'rest' => 'reStructuredText',
'text' => 'Plain Text',
'python' => 'Python',
'rainbow' => 'Rainbow',
'remarkup' => 'Remarkup',
'ruby' => 'Ruby',
'xml' => 'XML',
'yaml' => 'YAML',
))
->setSummary(
pht('Set the language list which appears in dropdowns.'))
->setDescription(
pht(
'In places that we display a dropdown to syntax-highlight code, '.
'this is where that list is defined.')),
$this->newOption(
'syntax.filemap',
'wild',
array(
'@\.arcconfig$@' => 'js',
'@\.arclint$@' => 'js',
'@\.divinerconfig$@' => 'js',
))
->setSummary(
pht('Override what language files (based on filename) highlight as.'))
->setDescription(
pht(
'This is an override list of regular expressions which allows '.
'you to choose what language files are highlighted as. If your '.
'projects have certain rules about filenames or use unusual or '.
'ambiguous language extensions, you can create a mapping here. '.
'This is an ordered dictionary of regular expressions which will '.
'be tested against the filename. They should map to either an '.
'explicit language as a string value, or a numeric index into '.
'the captured groups as an integer.'))
- ->addExample('{"@\\.xyz$@": "php"}', pht('Highlight *.xyz as PHP.'))
+ ->addExample('{"@\\.xyz$@": "php"}', pht('Highlight %s as PHP.', '*.xyz'))
->addExample(
'{"@/httpd\\.conf@": "apacheconf"}',
pht('Highlight httpd.conf as "apacheconf".'))
->addExample(
'{"@\\.([^.]+)\\.bak$@": 1}',
pht(
"Treat all '*.x.bak' file as '.x'. NOTE: We map to capturing group ".
"1 by specifying the mapping as '1'")),
);
}
}
diff --git a/src/applications/config/option/PhabricatorTranslationsConfigOptions.php b/src/applications/config/option/PhabricatorTranslationsConfigOptions.php
index 007c61d19..aa7b9a2b3 100644
--- a/src/applications/config/option/PhabricatorTranslationsConfigOptions.php
+++ b/src/applications/config/option/PhabricatorTranslationsConfigOptions.php
@@ -1,38 +1,38 @@
<?php
final class PhabricatorTranslationsConfigOptions
extends PhabricatorApplicationConfigOptions {
public function getName() {
return pht('Translations');
}
public function getDescription() {
return pht('Options relating to translations.');
}
public function getFontIcon() {
return 'fa-globe';
}
public function getGroup() {
return 'core';
}
public function getOptions() {
return array(
$this->newOption('translation.override', 'wild', array())
->setSummary(pht('Override translations.'))
->setDescription(
pht(
- "You can use 'translation.override' if you don't want to create ".
- "a full translation to give users an option for switching to it ".
- "and you just want to override some strings in the default ".
- "translation."))
+ "You can use '%s' if you don't want to create a full translation ".
+ "to give users an option for switching to it and you just want to ".
+ "override some strings in the default translation.",
+ 'translation.override'))
->addExample(
'{"some string": "my alternative"}',
pht('Valid Setting')),
);
}
}
diff --git a/src/applications/config/option/PhabricatorUIConfigOptions.php b/src/applications/config/option/PhabricatorUIConfigOptions.php
index 04896e1a2..8b84a6b4b 100644
--- a/src/applications/config/option/PhabricatorUIConfigOptions.php
+++ b/src/applications/config/option/PhabricatorUIConfigOptions.php
@@ -1,100 +1,99 @@
<?php
final class PhabricatorUIConfigOptions
extends PhabricatorApplicationConfigOptions {
public function getName() {
return pht('User Interface');
}
public function getDescription() {
return pht('Configure the Phabricator UI, including colors.');
}
public function getFontIcon() {
return 'fa-magnet';
}
public function getGroup() {
return 'core';
}
public function getOptions() {
$manifest = PHUIIconView::getSheetManifest('main-header');
$custom_header_example =
PhabricatorCustomHeaderConfigType::getExampleConfig();
$experimental_link = 'https://secure.phabricator.com/T4214';
$options = array();
foreach (array_keys($manifest) as $sprite_name) {
$key = substr($sprite_name, strlen('main-header-'));
$options[$key] = $key;
}
$example = <<<EOJSON
[
{
"name" : "Copyright 2199 Examplecorp"
},
{
"name" : "Privacy Policy",
"href" : "http://www.example.org/privacy/"
},
{
"name" : "Terms and Conditions",
"href" : "http://www.example.org/terms/"
}
]
EOJSON;
return array(
$this->newOption('ui.header-color', 'enum', 'dark')
->setDescription(
- pht(
- 'Sets the color of the main header.'))
+ pht('Sets the color of the main header.'))
->setEnumOptions($options),
$this->newOption('ui.footer-items', 'list<wild>', array())
->setSummary(
pht(
'Allows you to add footer links on most pages.'))
->setDescription(
pht(
"Allows you to add a footer with links in it to most ".
"pages. You might want to use these links to point at legal ".
"information or an about page.\n\n".
"Specify a list of dictionaries. Each dictionary describes ".
"a footer item. These keys are supported:\n\n".
" - `name` The name of the item.\n".
" - `href` Optionally, the link target of the item. You can ".
" omit this if you just want a piece of text, like a copyright ".
" notice."))
->addExample($example, pht('Basic Example')),
$this->newOption(
'ui.custom-header',
'custom:PhabricatorCustomHeaderConfigType',
null)
->setSummary(
pht('Customize the Phabricator logo.'))
->setDescription(
pht('You can customize the Phabricator logo by specifying the '.
'phid for a viewable image you have uploaded to Phabricator '.
'via the [[ /file/ | Files application]]. This image should '.
'be:'."\n".
' - 192px X 80px; while not enforced, images with these '.
'dimensions will look best across devices.'."\n".
' - have view policy public if [[ '.
'/config/edit/policy.allow-public | `policy.allow-public`]] '.
'is true and otherwise view policy user; mismatches in these '.
'policy settings will result in a broken logo for some users.'.
"\n\n".
'You should restart your webserver after updating this value '.
'to see this change take effect.'.
"\n\n".
'As this feature is experimental, please read [[ %s | T4214 ]] '.
'for up to date information.',
$experimental_link))
->addExample($custom_header_example, pht('Valid Config')),
);
}
}
diff --git a/src/applications/conpherence/conduit/ConpherenceQueryThreadConduitAPIMethod.php b/src/applications/conpherence/conduit/ConpherenceQueryThreadConduitAPIMethod.php
index 986e8e520..cd5fa1bff 100644
--- a/src/applications/conpherence/conduit/ConpherenceQueryThreadConduitAPIMethod.php
+++ b/src/applications/conpherence/conduit/ConpherenceQueryThreadConduitAPIMethod.php
@@ -1,83 +1,83 @@
<?php
final class ConpherenceQueryThreadConduitAPIMethod
extends ConpherenceConduitAPIMethod {
public function getAPIMethodName() {
return 'conpherence.querythread';
}
public function getMethodDescription() {
return pht(
- 'Query for conpherence threads for the logged in user. '.
- 'You can query by ids or phids for specific conpherence threads. '.
- 'Otherwise, specify limit and offset to query the most recently '.
- 'updated conpherences for the logged in user.');
+ 'Query for Conpherence threads for the logged in user. You can query '.
+ 'by IDs or PHIDs for specific Conpherence threads. Otherwise, specify '.
+ 'limit and offset to query the most recently updated Conpherences for '.
+ 'the logged in user.');
}
protected function defineParamTypes() {
return array(
'ids' => 'optional array<int>',
'phids' => 'optional array<phids>',
'limit' => 'optional int',
'offset' => 'optional int',
);
}
protected function defineReturnType() {
return 'nonempty dict';
}
protected function execute(ConduitAPIRequest $request) {
$user = $request->getUser();
$ids = $request->getValue('ids', array());
$phids = $request->getValue('phids', array());
$limit = $request->getValue('limit');
$offset = $request->getValue('offset');
$query = id(new ConpherenceThreadQuery())
->setViewer($user)
->needParticipantCache(true)
->needFilePHIDs(true);
if ($ids) {
$conpherences = $query
->withIDs($ids)
->setLimit($limit)
->setOffset($offset)
->execute();
} else if ($phids) {
$conpherences = $query
->withPHIDs($phids)
->setLimit($limit)
->setOffset($offset)
->execute();
} else {
$participation = id(new ConpherenceParticipantQuery())
->withParticipantPHIDs(array($user->getPHID()))
->setLimit($limit)
->setOffset($offset)
->execute();
$conpherence_phids = array_keys($participation);
$query->withPHIDs($conpherence_phids);
$conpherences = $query->execute();
$conpherences = array_select_keys($conpherences, $conpherence_phids);
}
$data = array();
foreach ($conpherences as $conpherence) {
$id = $conpherence->getID();
$data[$id] = array(
'conpherenceID' => $id,
'conpherencePHID' => $conpherence->getPHID(),
'conpherenceTitle' => $conpherence->getTitle(),
'messageCount' => $conpherence->getMessageCount(),
'recentParticipantPHIDs' => $conpherence->getRecentParticipantPHIDs(),
'filePHIDs' => $conpherence->getFilePHIDs(),
'conpherenceURI' => $this->getConpherenceURI($conpherence),
);
}
return $data;
}
}
diff --git a/src/applications/conpherence/conduit/ConpherenceQueryTransactionConduitAPIMethod.php b/src/applications/conpherence/conduit/ConpherenceQueryTransactionConduitAPIMethod.php
index 958748a68..e014dc742 100644
--- a/src/applications/conpherence/conduit/ConpherenceQueryTransactionConduitAPIMethod.php
+++ b/src/applications/conpherence/conduit/ConpherenceQueryTransactionConduitAPIMethod.php
@@ -1,97 +1,97 @@
<?php
final class ConpherenceQueryTransactionConduitAPIMethod
extends ConpherenceConduitAPIMethod {
public function getAPIMethodName() {
return 'conpherence.querytransaction';
}
public function getMethodDescription() {
return pht(
'Query for transactions for the logged in user within a specific '.
- 'conpherence thread. You can specify the thread by id or phid. '.
+ 'Conpherence thread. You can specify the thread by ID or PHID. '.
'Otherwise, specify limit and offset to query the most recent '.
- 'transactions within the conpherence for the logged in user.');
+ 'transactions within the Conpherence for the logged in user.');
}
protected function defineParamTypes() {
return array(
'threadID' => 'optional int',
'threadPHID' => 'optional phid',
'limit' => 'optional int',
'offset' => 'optional int',
);
}
protected function defineReturnType() {
return 'nonempty dict';
}
protected function defineErrorTypes() {
return array(
'ERR_USAGE_NO_THREAD_ID' => pht(
- 'You must specify a thread id or thread phid to query transactions '.
+ 'You must specify a thread id or thread PHID to query transactions '.
'from.'),
);
}
protected function execute(ConduitAPIRequest $request) {
$user = $request->getUser();
$thread_id = $request->getValue('threadID');
$thread_phid = $request->getValue('threadPHID');
$limit = $request->getValue('limit');
$offset = $request->getValue('offset');
$query = id(new ConpherenceThreadQuery())
->setViewer($user);
if ($thread_id) {
$query->withIDs(array($thread_id));
} else if ($thread_phid) {
$query->withPHIDs(array($thread_phid));
} else {
throw new ConduitException('ERR_USAGE_NO_THREAD_ID');
}
$conpherence = $query->executeOne();
$query = id(new ConpherenceTransactionQuery())
->setViewer($user)
->withObjectPHIDs(array($conpherence->getPHID()))
->setLimit($limit)
->setOffset($offset);
$transactions = $query->execute();
$data = array();
foreach ($transactions as $transaction) {
$comment = null;
$comment_obj = $transaction->getComment();
if ($comment_obj) {
$comment = $comment_obj->getContent();
}
$title = null;
$title_obj = $transaction->getTitle();
if ($title_obj) {
$title = $title_obj->getHTMLContent();
}
$id = $transaction->getID();
$data[$id] = array(
'transactionID' => $id,
'transactionType' => $transaction->getTransactionType(),
'transactionTitle' => $title,
'transactionComment' => $comment,
'transactionOldValue' => $transaction->getOldValue(),
'transactionNewValue' => $transaction->getNewValue(),
'transactionMetadata' => $transaction->getMetadata(),
'authorPHID' => $transaction->getAuthorPHID(),
'dateCreated' => $transaction->getDateCreated(),
'conpherenceID' => $conpherence->getID(),
'conpherencePHID' => $conpherence->getPHID(),
);
}
return $data;
}
}
diff --git a/src/applications/conpherence/controller/ConpherenceUpdateController.php b/src/applications/conpherence/controller/ConpherenceUpdateController.php
index 8d7db7691..57df50c82 100644
--- a/src/applications/conpherence/controller/ConpherenceUpdateController.php
+++ b/src/applications/conpherence/controller/ConpherenceUpdateController.php
@@ -1,569 +1,569 @@
<?php
final class ConpherenceUpdateController
extends ConpherenceController {
public function handleRequest(AphrontRequest $request) {
$user = $request->getUser();
$conpherence_id = $request->getURIData('id');
if (!$conpherence_id) {
return new Aphront404Response();
}
$need_participants = false;
$needed_capabilities = array(PhabricatorPolicyCapability::CAN_VIEW);
$action = $request->getStr('action', ConpherenceUpdateActions::METADATA);
switch ($action) {
case ConpherenceUpdateActions::REMOVE_PERSON:
$person_phid = $request->getStr('remove_person');
if ($person_phid != $user->getPHID()) {
$needed_capabilities[] = PhabricatorPolicyCapability::CAN_EDIT;
}
break;
case ConpherenceUpdateActions::ADD_PERSON:
case ConpherenceUpdateActions::METADATA:
$needed_capabilities[] = PhabricatorPolicyCapability::CAN_EDIT;
break;
case ConpherenceUpdateActions::JOIN_ROOM:
$needed_capabilities[] = PhabricatorPolicyCapability::CAN_JOIN;
break;
case ConpherenceUpdateActions::NOTIFICATIONS:
$need_participants = true;
break;
case ConpherenceUpdateActions::LOAD:
break;
}
$conpherence = id(new ConpherenceThreadQuery())
->setViewer($user)
->withIDs(array($conpherence_id))
->needFilePHIDs(true)
->needOrigPics(true)
->needCropPics(true)
->needParticipants($need_participants)
->requireCapabilities($needed_capabilities)
->executeOne();
$latest_transaction_id = null;
$response_mode = $request->isAjax() ? 'ajax' : 'redirect';
$error_view = null;
$e_file = array();
$errors = array();
$delete_draft = false;
$xactions = array();
if ($request->isFormPost() || ($action == ConpherenceUpdateActions::LOAD)) {
$editor = id(new ConpherenceEditor())
->setContinueOnNoEffect($request->isContinueRequest())
->setContentSourceFromRequest($request)
->setActor($user);
switch ($action) {
case ConpherenceUpdateActions::DRAFT:
$draft = PhabricatorDraft::newFromUserAndKey(
$user,
$conpherence->getPHID());
$draft->setDraft($request->getStr('text'));
$draft->replaceOrDelete();
return new AphrontAjaxResponse();
case ConpherenceUpdateActions::JOIN_ROOM:
$xactions[] = id(new ConpherenceTransaction())
->setTransactionType(
ConpherenceTransactionType::TYPE_PARTICIPANTS)
->setNewValue(array('+' => array($user->getPHID())));
$delete_draft = true;
$message = $request->getStr('text');
if ($message) {
$message_xactions = $editor->generateTransactionsFromText(
$user,
$conpherence,
$message);
$xactions = array_merge($xactions, $message_xactions);
}
// for now, just redirect back to the conpherence so everything
// will work okay...!
$response_mode = 'redirect';
break;
case ConpherenceUpdateActions::MESSAGE:
$message = $request->getStr('text');
if (strlen($message)) {
$xactions = $editor->generateTransactionsFromText(
$user,
$conpherence,
$message);
$delete_draft = true;
} else {
$action = ConpherenceUpdateActions::LOAD;
$updated = false;
$response_mode = 'ajax';
}
break;
case ConpherenceUpdateActions::ADD_PERSON:
$person_phids = $request->getArr('add_person');
if (!empty($person_phids)) {
$xactions[] = id(new ConpherenceTransaction())
->setTransactionType(
ConpherenceTransactionType::TYPE_PARTICIPANTS)
->setNewValue(array('+' => $person_phids));
}
break;
case ConpherenceUpdateActions::REMOVE_PERSON:
if (!$request->isContinueRequest()) {
// do nothing; we'll display a confirmation dialogue instead
break;
}
$person_phid = $request->getStr('remove_person');
if ($person_phid && $person_phid == $user->getPHID()) {
$xactions[] = id(new ConpherenceTransaction())
->setTransactionType(
ConpherenceTransactionType::TYPE_PARTICIPANTS)
->setNewValue(array('-' => array($person_phid)));
$response_mode = 'go-home';
}
break;
case ConpherenceUpdateActions::NOTIFICATIONS:
$notifications = $request->getStr('notifications');
$participant = $conpherence->getParticipantIfExists($user->getPHID());
if (!$participant) {
return id(new Aphront404Response());
}
$participant->setSettings(array('notifications' => $notifications));
$participant->save();
$result = pht(
'Updated notification settings to "%s".',
ConpherenceSettings::getHumanString($notifications));
return id(new AphrontAjaxResponse())
->setContent($result);
break;
case ConpherenceUpdateActions::METADATA:
$top = $request->getInt('image_y');
$left = $request->getInt('image_x');
$file_id = $request->getInt('file_id');
$title = $request->getStr('title');
if ($file_id) {
$orig_file = id(new PhabricatorFileQuery())
->setViewer($user)
->withIDs(array($file_id))
->executeOne();
$xactions[] = id(new ConpherenceTransaction())
->setTransactionType(ConpherenceTransactionType::TYPE_PICTURE)
->setNewValue($orig_file);
$okay = $orig_file->isTransformableImage();
if ($okay) {
$xformer = new PhabricatorImageTransformer();
$crop_file = $xformer->executeConpherenceTransform(
$orig_file,
0,
0,
ConpherenceImageData::CROP_WIDTH,
ConpherenceImageData::CROP_HEIGHT);
$xactions[] = id(new ConpherenceTransaction())
->setTransactionType(
ConpherenceTransactionType::TYPE_PICTURE_CROP)
->setNewValue($crop_file->getPHID());
}
$response_mode = 'redirect';
}
// all other metadata updates are continue requests
if (!$request->isContinueRequest()) {
break;
}
if ($top !== null || $left !== null) {
$file = $conpherence->getImage(ConpherenceImageData::SIZE_ORIG);
$xformer = new PhabricatorImageTransformer();
$xformed = $xformer->executeConpherenceTransform(
$file,
$top,
$left,
ConpherenceImageData::CROP_WIDTH,
ConpherenceImageData::CROP_HEIGHT);
$image_phid = $xformed->getPHID();
$xactions[] = id(new ConpherenceTransaction())
->setTransactionType(
ConpherenceTransactionType::TYPE_PICTURE_CROP)
->setNewValue($image_phid);
}
$title = $request->getStr('title');
$xactions[] = id(new ConpherenceTransaction())
->setTransactionType(ConpherenceTransactionType::TYPE_TITLE)
->setNewValue($title);
if ($conpherence->getIsRoom()) {
$xactions[] = id(new ConpherenceTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_VIEW_POLICY)
->setNewValue($request->getStr('viewPolicy'));
$xactions[] = id(new ConpherenceTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_EDIT_POLICY)
->setNewValue($request->getStr('editPolicy'));
$xactions[] = id(new ConpherenceTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_JOIN_POLICY)
->setNewValue($request->getStr('joinPolicy'));
}
if (!$request->getExists('force_ajax')) {
$response_mode = 'redirect';
}
break;
case ConpherenceUpdateActions::LOAD:
$updated = false;
$response_mode = 'ajax';
break;
default:
- throw new Exception('Unknown action: '.$action);
+ throw new Exception(pht('Unknown action: %s', $action));
break;
}
if ($xactions) {
try {
$xactions = $editor->applyTransactions($conpherence, $xactions);
if ($delete_draft) {
$draft = PhabricatorDraft::newFromUserAndKey(
$user,
$conpherence->getPHID());
$draft->delete();
}
} catch (PhabricatorApplicationTransactionNoEffectException $ex) {
return id(new PhabricatorApplicationTransactionNoEffectResponse())
->setCancelURI($this->getApplicationURI($conpherence_id.'/'))
->setException($ex);
}
// xactions had no effect...!
if (empty($xactions)) {
$errors[] = pht(
'That was a non-update. Try cancel.');
}
}
if ($xactions || ($action == ConpherenceUpdateActions::LOAD)) {
switch ($response_mode) {
case 'ajax':
$latest_transaction_id = $request->getInt('latest_transaction_id');
$content = $this->loadAndRenderUpdates(
$action,
$conpherence_id,
$latest_transaction_id);
return id(new AphrontAjaxResponse())
->setContent($content);
break;
case 'go-home':
return id(new AphrontRedirectResponse())
->setURI($this->getApplicationURI());
break;
case 'redirect':
default:
return id(new AphrontRedirectResponse())
->setURI('/'.$conpherence->getMonogram());
break;
}
}
}
if ($errors) {
$error_view = id(new PHUIInfoView())
->setErrors($errors);
}
switch ($action) {
case ConpherenceUpdateActions::ADD_PERSON:
$dialogue = $this->renderAddPersonDialogue($conpherence);
break;
case ConpherenceUpdateActions::REMOVE_PERSON:
$dialogue = $this->renderRemovePersonDialogue($conpherence);
break;
case ConpherenceUpdateActions::METADATA:
default:
$dialogue = $this->renderMetadataDialogue($conpherence, $error_view);
break;
}
return id(new AphrontDialogResponse())
->setDialog($dialogue
->setUser($user)
->setWidth(AphrontDialogView::WIDTH_FORM)
->setSubmitURI($this->getApplicationURI('update/'.$conpherence_id.'/'))
->addSubmitButton()
->addCancelButton($this->getApplicationURI($conpherence->getID().'/')));
}
private function renderAddPersonDialogue(
ConpherenceThread $conpherence) {
$request = $this->getRequest();
$user = $request->getUser();
$add_person = $request->getStr('add_person');
$form = id(new AphrontFormView())
->setUser($user)
->setFullWidth(true)
->appendControl(
id(new AphrontFormTokenizerControl())
->setName('add_person')
->setUser($user)
->setDatasource(new PhabricatorPeopleDatasource()));
require_celerity_resource('conpherence-update-css');
$view = id(new AphrontDialogView())
->setTitle(pht('Add Participants'))
->addHiddenInput('action', 'add_person')
->addHiddenInput(
'latest_transaction_id',
$request->getInt('latest_transaction_id'))
->appendForm($form);
if ($request->getExists('minimal_display')) {
$view->addHiddenInput('minimal_display', true);
}
return $view;
}
private function renderRemovePersonDialogue(
ConpherenceThread $conpherence) {
$request = $this->getRequest();
$user = $request->getUser();
$remove_person = $request->getStr('remove_person');
$participants = $conpherence->getParticipants();
if ($conpherence->getIsRoom()) {
$message = pht(
'Are you sure you want to remove yourself from this room?');
} else {
$message = pht(
'Are you sure you want to remove yourself from this thread?');
if (count($participants) == 1) {
$message .= pht(
'The thread will be inaccessible forever and ever.');
} else {
$message .= pht(
'Someone else in the thread can add you back later.');
}
}
$body = phutil_tag(
'p',
array(
),
$message);
require_celerity_resource('conpherence-update-css');
return id(new AphrontDialogView())
->setTitle(pht('Remove Participants'))
->addHiddenInput('action', 'remove_person')
->addHiddenInput('remove_person', $remove_person)
->addHiddenInput(
'latest_transaction_id',
$request->getInt('latest_transaction_id'))
->addHiddenInput('__continue__', true)
->appendChild($body);
}
private function renderMetadataDialogue(
ConpherenceThread $conpherence,
$error_view) {
$request = $this->getRequest();
$user = $request->getUser();
$form = id(new PHUIFormLayoutView())
->appendChild($error_view)
->appendChild(
id(new AphrontFormTextControl())
->setLabel(pht('Title'))
->setName('title')
->setValue($conpherence->getTitle()));
$nopic = $this->getRequest()->getExists('nopic');
$image = $conpherence->getImage(ConpherenceImageData::SIZE_ORIG);
if ($nopic) {
// do not render any pic related controls
} else if ($image) {
$crop_uri = $conpherence->loadImageURI(ConpherenceImageData::SIZE_CROP);
$form
->appendChild(
id(new AphrontFormMarkupControl())
->setLabel(pht('Image'))
->setValue(phutil_tag(
'img',
array(
'src' => $crop_uri,
))))
->appendChild(
id(new ConpherencePicCropControl())
->setLabel(pht('Crop Image'))
->setValue($image))
->appendChild(
id(new ConpherenceFormDragAndDropUploadControl())
->setLabel(pht('Change Image')));
} else {
$form
->appendChild(
id(new ConpherenceFormDragAndDropUploadControl())
->setLabel(pht('Image')));
}
if ($conpherence->getIsRoom()) {
$title = pht('Update Room');
$policies = id(new PhabricatorPolicyQuery())
->setViewer($user)
->setObject($conpherence)
->execute();
$form->appendChild(
id(new AphrontFormPolicyControl())
->setName('viewPolicy')
->setPolicyObject($conpherence)
->setCapability(PhabricatorPolicyCapability::CAN_VIEW)
->setPolicies($policies))
->appendChild(
id(new AphrontFormPolicyControl())
->setName('editPolicy')
->setPolicyObject($conpherence)
->setCapability(PhabricatorPolicyCapability::CAN_EDIT)
->setPolicies($policies))
->appendChild(
id(new AphrontFormPolicyControl())
->setName('joinPolicy')
->setPolicyObject($conpherence)
->setCapability(PhabricatorPolicyCapability::CAN_JOIN)
->setPolicies($policies));
} else {
$title = pht('Update Thread');
}
require_celerity_resource('conpherence-update-css');
$view = id(new AphrontDialogView())
->setTitle($title)
->addHiddenInput('action', 'metadata')
->addHiddenInput(
'latest_transaction_id',
$request->getInt('latest_transaction_id'))
->addHiddenInput('__continue__', true)
->appendChild($form);
if ($request->getExists('minimal_display')) {
$view->addHiddenInput('minimal_display', true);
}
if ($request->getExists('force_ajax')) {
$view->addHiddenInput('force_ajax', true);
}
return $view;
}
private function loadAndRenderUpdates(
$action,
$conpherence_id,
$latest_transaction_id) {
$minimal_display = $this->getRequest()->getExists('minimal_display');
$need_widget_data = false;
$need_transactions = false;
$need_participant_cache = true;
switch ($action) {
case ConpherenceUpdateActions::METADATA:
case ConpherenceUpdateActions::LOAD:
$need_transactions = true;
break;
case ConpherenceUpdateActions::MESSAGE:
case ConpherenceUpdateActions::ADD_PERSON:
$need_transactions = true;
$need_widget_data = !$minimal_display;
break;
case ConpherenceUpdateActions::REMOVE_PERSON:
case ConpherenceUpdateActions::NOTIFICATIONS:
default:
break;
}
$user = $this->getRequest()->getUser();
$conpherence = id(new ConpherenceThreadQuery())
->setViewer($user)
->setAfterTransactionID($latest_transaction_id)
->needCropPics(true)
->needParticipantCache($need_participant_cache)
->needWidgetData($need_widget_data)
->needTransactions($need_transactions)
->withIDs(array($conpherence_id))
->executeOne();
$non_update = false;
if ($need_transactions && $conpherence->getTransactions()) {
$data = ConpherenceTransactionRenderer::renderTransactions(
$user,
$conpherence,
!$minimal_display);
$participant_obj = $conpherence->getParticipant($user->getPHID());
$participant_obj->markUpToDate($conpherence, $data['latest_transaction']);
} else if ($need_transactions) {
$non_update = true;
$data = array();
} else {
$data = array();
}
$rendered_transactions = idx($data, 'transactions');
$new_latest_transaction_id = idx($data, 'latest_transaction_id');
$widget_uri = $this->getApplicationURI('update/'.$conpherence->getID().'/');
$nav_item = null;
$header = null;
$people_widget = null;
$file_widget = null;
if (!$minimal_display) {
switch ($action) {
case ConpherenceUpdateActions::METADATA:
$policy_objects = id(new PhabricatorPolicyQuery())
->setViewer($user)
->setObject($conpherence)
->execute();
$header = $this->buildHeaderPaneContent(
$conpherence,
$policy_objects);
$header = hsprintf('%s', $header);
$nav_item = id(new ConpherenceThreadListView())
->setUser($user)
->setBaseURI($this->getApplicationURI())
->renderSingleThread($conpherence);
$nav_item = hsprintf('%s', $nav_item);
break;
case ConpherenceUpdateActions::MESSAGE:
$file_widget = id(new ConpherenceFileWidgetView())
->setUser($this->getRequest()->getUser())
->setConpherence($conpherence)
->setUpdateURI($widget_uri);
$file_widget = hsprintf('%s', $file_widget->render());
break;
case ConpherenceUpdateActions::ADD_PERSON:
$people_widget = id(new ConpherencePeopleWidgetView())
->setUser($user)
->setConpherence($conpherence)
->setUpdateURI($widget_uri);
$people_widget = hsprintf('%s', $people_widget->render());
break;
case ConpherenceUpdateActions::REMOVE_PERSON:
case ConpherenceUpdateActions::NOTIFICATIONS:
default:
break;
}
}
$data = $conpherence->getDisplayData($user);
$dropdown_query = id(new AphlictDropdownDataQuery())
->setViewer($user);
$dropdown_query->execute();
$content = array(
'non_update' => $non_update,
'transactions' => hsprintf('%s', $rendered_transactions),
'conpherence_title' => (string)$data['title'],
'latest_transaction_id' => $new_latest_transaction_id,
'nav_item' => $nav_item,
'conpherence_phid' => $conpherence->getPHID(),
'header' => $header,
'file_widget' => $file_widget,
'people_widget' => $people_widget,
'aphlictDropdownData' => array(
$dropdown_query->getNotificationData(),
$dropdown_query->getConpherenceData(),
),
);
return $content;
}
}
diff --git a/src/applications/conpherence/mail/ConpherenceReplyHandler.php b/src/applications/conpherence/mail/ConpherenceReplyHandler.php
index a26c9d3cc..61f925e15 100644
--- a/src/applications/conpherence/mail/ConpherenceReplyHandler.php
+++ b/src/applications/conpherence/mail/ConpherenceReplyHandler.php
@@ -1,82 +1,85 @@
<?php
final class ConpherenceReplyHandler extends PhabricatorMailReplyHandler {
private $mailAddedParticipantPHIDs;
public function setMailAddedParticipantPHIDs(array $phids) {
$this->mailAddedParticipantPHIDs = $phids;
return $this;
}
public function getMailAddedParticipantPHIDs() {
return $this->mailAddedParticipantPHIDs;
}
public function validateMailReceiver($mail_receiver) {
if (!($mail_receiver instanceof ConpherenceThread)) {
- throw new Exception('Mail receiver is not a ConpherenceThread!');
+ throw new Exception(
+ pht(
+ 'Mail receiver is not a %s!', '
+ ConpherenceThread'));
}
}
public function getPrivateReplyHandlerEmailAddress(
PhabricatorObjectHandle $handle) {
return $this->getDefaultPrivateReplyHandlerEmailAddress($handle, 'Z');
}
public function getPublicReplyHandlerEmailAddress() {
return $this->getDefaultPublicReplyHandlerEmailAddress('Z');
}
protected function receiveEmail(PhabricatorMetaMTAReceivedMail $mail) {
$conpherence = $this->getMailReceiver();
$user = $this->getActor();
if (!$conpherence->getPHID()) {
$conpherence
->attachParticipants(array())
->attachFilePHIDs(array());
} else {
$edge_type = PhabricatorObjectHasFileEdgeType::EDGECONST;
$file_phids = PhabricatorEdgeQuery::loadDestinationPHIDs(
$conpherence->getPHID(),
$edge_type);
$conpherence->attachFilePHIDs($file_phids);
$participants = id(new ConpherenceParticipant())
->loadAllWhere('conpherencePHID = %s', $conpherence->getPHID());
$participants = mpull($participants, null, 'getParticipantPHID');
$conpherence->attachParticipants($participants);
}
$content_source = PhabricatorContentSource::newForSource(
PhabricatorContentSource::SOURCE_EMAIL,
array(
'id' => $mail->getID(),
));
$editor = id(new ConpherenceEditor())
->setActor($user)
->setContentSource($content_source)
->setParentMessageID($mail->getMessageID());
$body = $mail->getCleanTextBody();
$body = $this->enhanceBodyWithAttachments($body, $mail->getAttachments());
$xactions = array();
if ($this->getMailAddedParticipantPHIDs()) {
$xactions[] = id(new ConpherenceTransaction())
->setTransactionType(ConpherenceTransactionType::TYPE_PARTICIPANTS)
->setNewValue(array('+' => $this->getMailAddedParticipantPHIDs()));
}
$xactions = array_merge(
$xactions,
$editor->generateTransactionsFromText(
$user,
$conpherence,
$body));
$editor->applyTransactions($conpherence, $xactions);
return $conpherence;
}
}
diff --git a/src/applications/console/plugin/DarkConsoleErrorLogPlugin.php b/src/applications/console/plugin/DarkConsoleErrorLogPlugin.php
index 513f4b990..591904a85 100644
--- a/src/applications/console/plugin/DarkConsoleErrorLogPlugin.php
+++ b/src/applications/console/plugin/DarkConsoleErrorLogPlugin.php
@@ -1,99 +1,99 @@
<?php
final class DarkConsoleErrorLogPlugin extends DarkConsolePlugin {
public function getName() {
$count = count($this->getData());
if ($count) {
return pht('Error Log (%d)', $count);
}
return pht('Error Log');
}
public function getOrder() {
return 0;
}
public function getColor() {
if (count($this->getData())) {
return '#ff0000';
}
return null;
}
public function getDescription() {
return pht('Shows errors and warnings.');
}
public function generateData() {
return DarkConsoleErrorLogPluginAPI::getErrors();
}
public function renderPanel() {
$data = $this->getData();
$rows = array();
$details = array();
foreach ($data as $index => $row) {
$file = $row['file'];
$line = $row['line'];
$tag = phutil_tag(
'a',
array(
'onclick' => jsprintf('show_details(%d)', $index),
),
$row['str'].' at ['.basename($file).':'.$line.']');
$rows[] = array($tag);
$details[] = hsprintf(
'<div class="dark-console-panel-error-details" id="row-details-%s">'.
"%s\nStack trace:\n",
$index,
$row['details']);
foreach ($row['trace'] as $key => $entry) {
$line = '';
if (isset($entry['class'])) {
$line .= $entry['class'].'::';
}
$line .= idx($entry, 'function', '');
$href = null;
if (isset($entry['file'])) {
$line .= ' called at ['.$entry['file'].':'.$entry['line'].']';
try {
$user = $this->getRequest()->getUser();
$href = $user->loadEditorLink($entry['file'], $entry['line'], '');
} catch (Exception $ex) {
// The database can be inaccessible.
}
}
$details[] = phutil_tag(
'a',
array(
'href' => $href,
),
$line);
$details[] = "\n";
}
$details[] = hsprintf('</div>');
}
$table = new AphrontTableView($rows);
$table->setClassName('error-log');
- $table->setHeaders(array('Error'));
- $table->setNoDataString('No errors.');
+ $table->setHeaders(array(pht('Error')));
+ $table->setNoDataString(pht('No errors.'));
return phutil_tag(
'div',
array(),
array(
phutil_tag('div', array(), $table->render()),
phutil_tag('pre', array('class' => 'PhabricatorMonospaced'), $details),
));
}
}
diff --git a/src/applications/console/plugin/DarkConsoleEventPlugin.php b/src/applications/console/plugin/DarkConsoleEventPlugin.php
index 02dc4e8b7..070227c12 100644
--- a/src/applications/console/plugin/DarkConsoleEventPlugin.php
+++ b/src/applications/console/plugin/DarkConsoleEventPlugin.php
@@ -1,95 +1,95 @@
<?php
final class DarkConsoleEventPlugin extends DarkConsolePlugin {
public function getName() {
- return 'Events';
+ return pht('Events');
}
public function getDescription() {
- return 'Information about Phabricator events and event listeners.';
+ return pht('Information about Phabricator events and event listeners.');
}
public function generateData() {
$listeners = PhutilEventEngine::getInstance()->getAllListeners();
foreach ($listeners as $key => $listener) {
$listeners[$key] = array(
'id' => $listener->getListenerID(),
'class' => get_class($listener),
);
}
$events = DarkConsoleEventPluginAPI::getEvents();
foreach ($events as $key => $event) {
$events[$key] = array(
'type' => $event->getType(),
'stopped' => $event->isStopped(),
);
}
return array(
'listeners' => $listeners,
'events' => $events,
);
}
public function renderPanel() {
$data = $this->getData();
$out = array();
$out[] = phutil_tag(
'div',
array('class' => 'dark-console-panel-header'),
phutil_tag('h1', array(), pht('Registered Event Listeners')));
$rows = array();
foreach ($data['listeners'] as $listener) {
$rows[] = array($listener['id'], $listener['class']);
}
$table = new AphrontTableView($rows);
$table->setHeaders(
array(
- 'Internal ID',
- 'Listener Class',
+ pht('Internal ID'),
+ pht('Listener Class'),
));
$table->setColumnClasses(
array(
'',
'wide',
));
$out[] = $table->render();
$out[] = phutil_tag(
'div',
array('class' => 'dark-console-panel-header'),
phutil_tag('h1', array(), pht('Event Log')));
$rows = array();
foreach ($data['events'] as $event) {
$rows[] = array(
$event['type'],
- $event['stopped'] ? 'STOPPED' : null,
+ $event['stopped'] ? pht('STOPPED') : null,
);
}
$table = new AphrontTableView($rows);
$table->setColumnClasses(
array(
'wide',
));
$table->setHeaders(
array(
- 'Event Type',
- 'Stopped',
+ pht('Event Type'),
+ pht('Stopped'),
));
$out[] = $table->render();
return phutil_implode_html("\n", $out);
}
}
diff --git a/src/applications/console/plugin/DarkConsoleRequestPlugin.php b/src/applications/console/plugin/DarkConsoleRequestPlugin.php
index b354b328c..833390313 100644
--- a/src/applications/console/plugin/DarkConsoleRequestPlugin.php
+++ b/src/applications/console/plugin/DarkConsoleRequestPlugin.php
@@ -1,76 +1,79 @@
<?php
final class DarkConsoleRequestPlugin extends DarkConsolePlugin {
public function getName() {
- return 'Request';
+ return pht('Request');
}
public function getDescription() {
- return 'Information about $_REQUEST and $_SERVER.';
+ return pht(
+ 'Information about %s and %s.',
+ '$_REQUEST',
+ '$_SERVER');
}
public function generateData() {
return array(
'Request' => $_REQUEST,
'Server' => $_SERVER,
);
}
public function renderPanel() {
$data = $this->getData();
$sections = array(
'Basics' => array(
'Machine' => php_uname('n'),
),
);
// NOTE: This may not be present for some SAPIs, like php-fpm.
if (!empty($data['Server']['SERVER_ADDR'])) {
$addr = $data['Server']['SERVER_ADDR'];
$sections['Basics']['Host'] = $addr;
$sections['Basics']['Hostname'] = @gethostbyaddr($addr);
}
$sections = array_merge($sections, $data);
$mask = array(
'HTTP_COOKIE' => true,
'HTTP_X_PHABRICATOR_CSRF' => true,
);
$out = array();
foreach ($sections as $header => $map) {
$rows = array();
foreach ($map as $key => $value) {
if (isset($mask[$key])) {
$rows[] = array(
$key,
- phutil_tag('em', array(), '(Masked)'),
+ phutil_tag('em', array(), pht('(Masked)')),
);
} else {
$rows[] = array(
$key,
(is_array($value) ? json_encode($value) : $value),
);
}
}
$table = new AphrontTableView($rows);
$table->setHeaders(
array(
$header,
null,
));
$table->setColumnClasses(
array(
'header',
'wide wrap',
));
$out[] = $table->render();
}
return phutil_implode_html("\n", $out);
}
}
diff --git a/src/applications/console/plugin/DarkConsoleServicesPlugin.php b/src/applications/console/plugin/DarkConsoleServicesPlugin.php
index 7f33f9ba6..6a59c7e12 100644
--- a/src/applications/console/plugin/DarkConsoleServicesPlugin.php
+++ b/src/applications/console/plugin/DarkConsoleServicesPlugin.php
@@ -1,292 +1,292 @@
<?php
final class DarkConsoleServicesPlugin extends DarkConsolePlugin {
protected $observations;
public function getName() {
- return 'Services';
+ return pht('Services');
}
public function getDescription() {
- return 'Information about services.';
+ return pht('Information about services.');
}
public static function getQueryAnalyzerHeader() {
return 'X-Phabricator-QueryAnalyzer';
}
public static function isQueryAnalyzerRequested() {
if (!empty($_REQUEST['__analyze__'])) {
return true;
}
$header = AphrontRequest::getHTTPHeader(self::getQueryAnalyzerHeader());
if ($header) {
return true;
}
return false;
}
/**
* @phutil-external-symbol class PhabricatorStartup
*/
public function generateData() {
$should_analyze = self::isQueryAnalyzerRequested();
$log = PhutilServiceProfiler::getInstance()->getServiceCallLog();
foreach ($log as $key => $entry) {
$config = idx($entry, 'config', array());
unset($log[$key]['config']);
if (!$should_analyze) {
$log[$key]['explain'] = array(
'sev' => 7,
'size' => null,
- 'reason' => 'Disabled',
+ 'reason' => pht('Disabled'),
);
// Query analysis is disabled for this request, so don't do any of it.
continue;
}
if ($entry['type'] != 'query') {
continue;
}
// For each SELECT query, go issue an EXPLAIN on it so we can flag stuff
// causing table scans, etc.
if (preg_match('/^\s*SELECT\b/i', $entry['query'])) {
$conn = PhabricatorEnv::newObjectFromConfig(
'mysql.implementation',
array($entry['config']));
try {
$explain = queryfx_all(
$conn,
'EXPLAIN %Q',
$entry['query']);
$badness = 0;
$size = 1;
$reason = null;
foreach ($explain as $table) {
$size *= (int)$table['rows'];
switch ($table['type']) {
case 'index':
$cur_badness = 1;
$cur_reason = 'Index';
break;
case 'const':
$cur_badness = 1;
$cur_reason = 'Const';
break;
case 'eq_ref';
$cur_badness = 2;
$cur_reason = 'EqRef';
break;
case 'range':
$cur_badness = 3;
$cur_reason = 'Range';
break;
case 'ref':
$cur_badness = 3;
$cur_reason = 'Ref';
break;
case 'fulltext':
$cur_badness = 3;
$cur_reason = 'Fulltext';
break;
case 'ALL':
if (preg_match('/Using where/', $table['Extra'])) {
if ($table['rows'] < 256 && !empty($table['possible_keys'])) {
$cur_badness = 2;
- $cur_reason = 'Small Table Scan';
+ $cur_reason = pht('Small Table Scan');
} else {
$cur_badness = 6;
- $cur_reason = 'TABLE SCAN!';
+ $cur_reason = pht('TABLE SCAN!');
}
} else {
$cur_badness = 3;
- $cur_reason = 'Whole Table';
+ $cur_reason = pht('Whole Table');
}
break;
default:
if (preg_match('/No tables used/i', $table['Extra'])) {
$cur_badness = 1;
- $cur_reason = 'No Tables';
+ $cur_reason = pht('No Tables');
} else if (preg_match('/Impossible/i', $table['Extra'])) {
$cur_badness = 1;
- $cur_reason = 'Empty';
+ $cur_reason = pht('Empty');
} else {
$cur_badness = 4;
- $cur_reason = "Can't Analyze";
+ $cur_reason = pht("Can't Analyze");
}
break;
}
if ($cur_badness > $badness) {
$badness = $cur_badness;
$reason = $cur_reason;
}
}
$log[$key]['explain'] = array(
'sev' => $badness,
'size' => $size,
'reason' => $reason,
);
} catch (Exception $ex) {
$log[$key]['explain'] = array(
'sev' => 5,
'size' => null,
'reason' => $ex->getMessage(),
);
}
}
}
return array(
'start' => PhabricatorStartup::getStartTime(),
'end' => microtime(true),
'log' => $log,
'analyzeURI' => (string)$this
->getRequestURI()
->alter('__analyze__', true),
'didAnalyze' => $should_analyze,
);
}
public function renderPanel() {
$data = $this->getData();
$log = $data['log'];
$results = array();
$results[] = phutil_tag(
'div',
array('class' => 'dark-console-panel-header'),
array(
phutil_tag(
'a',
array(
'href' => $data['analyzeURI'],
'class' => $data['didAnalyze'] ? 'disabled button' : 'green button',
),
pht('Analyze Query Plans')),
phutil_tag('h1', array(), pht('Calls to External Services')),
phutil_tag('div', array('style' => 'clear: both;')),
));
$page_total = $data['end'] - $data['start'];
$totals = array();
$counts = array();
foreach ($log as $row) {
$totals[$row['type']] = idx($totals, $row['type'], 0) + $row['duration'];
$counts[$row['type']] = idx($counts, $row['type'], 0) + 1;
}
$totals['All Services'] = array_sum($totals);
$counts['All Services'] = array_sum($counts);
$totals['Entire Page'] = $page_total;
$counts['Entire Page'] = 0;
$summary = array();
foreach ($totals as $type => $total) {
$summary[] = array(
$type,
number_format($counts[$type]),
- number_format((int)(1000000 * $totals[$type])).' us',
+ pht('%d us', number_format((int)(1000000 * $totals[$type]))),
sprintf('%.1f%%', 100 * $totals[$type] / $page_total),
);
}
$summary_table = new AphrontTableView($summary);
$summary_table->setColumnClasses(
array(
'',
'n',
'n',
'wide',
));
$summary_table->setHeaders(
array(
- 'Type',
- 'Count',
- 'Total Cost',
- 'Page Weight',
+ pht('Type'),
+ pht('Count'),
+ pht('Total Cost'),
+ pht('Page Weight'),
));
$results[] = $summary_table->render();
$rows = array();
foreach ($log as $row) {
$analysis = null;
switch ($row['type']) {
case 'query':
$info = $row['query'];
$info = wordwrap($info, 128, "\n", true);
if (!empty($row['explain'])) {
$analysis = phutil_tag(
'span',
array(
'class' => 'explain-sev-'.$row['explain']['sev'],
),
$row['explain']['reason']);
}
break;
case 'connect':
$info = $row['host'].':'.$row['database'];
break;
case 'exec':
$info = $row['command'];
break;
case 's3':
case 'conduit':
$info = $row['method'];
break;
case 'http':
$info = $row['uri'];
break;
default:
$info = '-';
break;
}
$rows[] = array(
$row['type'],
- '+'.number_format(1000 * ($row['begin'] - $data['start'])).' ms',
- number_format(1000000 * $row['duration']).' us',
+ pht('+%d ms', number_format(1000 * ($row['begin'] - $data['start']))),
+ pht('%d us', number_format(1000000 * $row['duration'])),
$info,
$analysis,
);
}
$table = new AphrontTableView($rows);
$table->setColumnClasses(
array(
null,
'n',
'n',
'wide',
'',
));
$table->setHeaders(
array(
- 'Event',
- 'Start',
- 'Duration',
- 'Details',
- 'Analysis',
+ pht('Event'),
+ pht('Start'),
+ pht('Duration'),
+ pht('Details'),
+ pht('Analysis'),
));
$results[] = $table->render();
return phutil_implode_html("\n", $results);
}
}
diff --git a/src/applications/console/plugin/DarkConsoleXHProfPlugin.php b/src/applications/console/plugin/DarkConsoleXHProfPlugin.php
index 2a0d22f77..f751cc31a 100644
--- a/src/applications/console/plugin/DarkConsoleXHProfPlugin.php
+++ b/src/applications/console/plugin/DarkConsoleXHProfPlugin.php
@@ -1,107 +1,107 @@
<?php
final class DarkConsoleXHProfPlugin extends DarkConsolePlugin {
protected $profileFilePHID;
public function getName() {
- return 'XHProf';
+ return pht('XHProf');
}
public function getColor() {
$data = $this->getData();
if ($data['profileFilePHID']) {
return '#ff00ff';
}
return null;
}
public function getDescription() {
- return 'Provides detailed PHP profiling information through XHProf.';
+ return pht('Provides detailed PHP profiling information through XHProf.');
}
public function generateData() {
return array(
'profileFilePHID' => $this->profileFilePHID,
'profileURI' => (string)$this
->getRequestURI()
->alter('__profile__', 'page'),
);
}
public function getXHProfRunID() {
return $this->profileFilePHID;
}
public function renderPanel() {
$data = $this->getData();
$run = $data['profileFilePHID'];
$profile_uri = $data['profileURI'];
if (!DarkConsoleXHProfPluginAPI::isProfilerAvailable()) {
$href = PhabricatorEnv::getDoclink('Installation Guide');
$install_guide = phutil_tag(
'a',
array(
'href' => $href,
'class' => 'bright-link',
),
- 'Installation Guide');
+ pht('Installation Guide'));
return hsprintf(
- '<div class="dark-console-no-content">'.
+ '<div class="dark-console-no-content">%s</div>',
+ pht(
'The "xhprof" PHP extension is not available. Install xhprof '.
'to enable the XHProf console plugin. You can find instructions in '.
- 'the %s.'.
- '</div>',
- $install_guide);
+ 'the %s.',
+ $install_guide));
}
$result = array();
$header = phutil_tag(
'div',
array('class' => 'dark-console-panel-header'),
array(
phutil_tag(
'a',
array(
'href' => $profile_uri,
'class' => $run ? 'disabled button' : 'green button',
),
pht('Profile Page')),
phutil_tag('h1', array(), pht('XHProf Profiler')),
));
$result[] = $header;
if ($run) {
$result[] = phutil_tag(
'a',
array(
'href' => "/xhprof/profile/$run/",
'class' => 'bright-link',
'style' => 'float: right; margin: 1em 2em 0 0; font-weight: bold;',
'target' => '_blank',
),
pht('Profile Permalink'));
$result[] = phutil_tag(
'iframe',
array('src' => "/xhprof/profile/$run/?frame=true"));
} else {
$result[] = phutil_tag(
'div',
array('class' => 'dark-console-no-content'),
pht(
'Profiling was not enabled for this page. Use the button above '.
'to enable it.'));
}
return phutil_implode_html("\n", $result);
}
public function willShutdown() {
$this->profileFilePHID = DarkConsoleXHProfPluginAPI::getProfileFilePHID();
}
}
diff --git a/src/applications/console/plugin/errorlog/DarkConsoleErrorLogPluginAPI.php b/src/applications/console/plugin/errorlog/DarkConsoleErrorLogPluginAPI.php
index 5d9723cc4..3067472cf 100644
--- a/src/applications/console/plugin/errorlog/DarkConsoleErrorLogPluginAPI.php
+++ b/src/applications/console/plugin/errorlog/DarkConsoleErrorLogPluginAPI.php
@@ -1,75 +1,75 @@
<?php
final class DarkConsoleErrorLogPluginAPI {
private static $errors = array();
private static $discardMode = false;
public static function registerErrorHandler() {
// NOTE: This forces PhutilReadableSerializer to load, so that we are
// able to handle errors which fire from inside autoloaders (PHP will not
// reenter autoloaders).
PhutilReadableSerializer::printableValue(null);
PhutilErrorHandler::setErrorListener(
array(__CLASS__, 'handleErrors'));
}
public static function enableDiscardMode() {
self::$discardMode = true;
}
public static function disableDiscardMode() {
self::$discardMode = false;
}
public static function getErrors() {
return self::$errors;
}
public static function handleErrors($event, $value, $metadata) {
if (self::$discardMode) {
return;
}
switch ($event) {
case PhutilErrorHandler::EXCEPTION:
// $value is of type Exception
self::$errors[] = array(
'details' => $value->getMessage(),
'event' => $event,
'file' => $value->getFile(),
'line' => $value->getLine(),
'str' => $value->getMessage(),
'trace' => $metadata['trace'],
);
break;
case PhutilErrorHandler::ERROR:
// $value is a simple string
self::$errors[] = array(
'details' => $value,
'event' => $event,
'file' => $metadata['file'],
'line' => $metadata['line'],
'str' => $value,
'trace' => $metadata['trace'],
);
break;
case PhutilErrorHandler::PHLOG:
// $value can be anything
self::$errors[] = array(
'details' => PhutilReadableSerializer::printShallow($value, 3),
'event' => $event,
'file' => $metadata['file'],
'line' => $metadata['line'],
'str' => PhutilReadableSerializer::printShort($value),
'trace' => $metadata['trace'],
);
break;
default:
- error_log('Unknown event : '.$event);
+ error_log(pht('Unknown event: %s', $event));
break;
}
}
}
diff --git a/src/applications/countdown/controller/PhabricatorCountdownDeleteController.php b/src/applications/countdown/controller/PhabricatorCountdownDeleteController.php
index d57534a42..5dfbd1814 100644
--- a/src/applications/countdown/controller/PhabricatorCountdownDeleteController.php
+++ b/src/applications/countdown/controller/PhabricatorCountdownDeleteController.php
@@ -1,50 +1,51 @@
<?php
final class PhabricatorCountdownDeleteController
extends PhabricatorCountdownController {
private $id;
public function willProcessRequest(array $data) {
$this->id = $data['id'];
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$countdown = id(new PhabricatorCountdownQuery())
->setViewer($user)
->withIDs(array($this->id))
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
if (!$countdown) {
return new Aphront404Response();
}
if ($request->isFormPost()) {
$countdown->delete();
return id(new AphrontRedirectResponse())
->setURI('/countdown/');
}
- $inst = pht('Are you sure you want to delete the countdown %s?',
- $countdown->getTitle());
+ $inst = pht(
+ 'Are you sure you want to delete the countdown %s?',
+ $countdown->getTitle());
$dialog = new AphrontDialogView();
$dialog->setUser($request->getUser());
$dialog->setTitle(pht('Really delete this countdown?'));
$dialog->appendChild(phutil_tag('p', array(), $inst));
$dialog->addSubmitButton(pht('Delete'));
$dialog->addCancelButton('/countdown/');
$dialog->setSubmitURI($request->getPath());
return id(new AphrontDialogResponse())->setDialog($dialog);
}
}
diff --git a/src/applications/daemon/controller/PhabricatorDaemonConsoleController.php b/src/applications/daemon/controller/PhabricatorDaemonConsoleController.php
index cd549a684..76d98bda5 100644
--- a/src/applications/daemon/controller/PhabricatorDaemonConsoleController.php
+++ b/src/applications/daemon/controller/PhabricatorDaemonConsoleController.php
@@ -1,272 +1,272 @@
<?php
final class PhabricatorDaemonConsoleController
extends PhabricatorDaemonController {
public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
$window_start = (time() - (60 * 15));
// Assume daemons spend about 250ms second in overhead per task acquiring
// leases and doing other bookkeeping. This is probably an over-estimation,
// but we'd rather show that utilization is too high than too low.
$lease_overhead = 0.250;
$completed = id(new PhabricatorWorkerArchiveTaskQuery())
->withDateModifiedSince($window_start)
->execute();
$failed = id(new PhabricatorWorkerActiveTask())->loadAllWhere(
'failureTime > %d',
$window_start);
$usage_total = 0;
$usage_start = PHP_INT_MAX;
$completed_info = array();
foreach ($completed as $completed_task) {
$class = $completed_task->getTaskClass();
if (empty($completed_info[$class])) {
$completed_info[$class] = array(
'n' => 0,
'duration' => 0,
);
}
$completed_info[$class]['n']++;
$duration = $completed_task->getDuration();
$completed_info[$class]['duration'] += $duration;
// NOTE: Duration is in microseconds, but we're just using seconds to
// compute utilization.
$usage_total += $lease_overhead + ($duration / 1000000);
$usage_start = min($usage_start, $completed_task->getDateModified());
}
$completed_info = isort($completed_info, 'n');
$rows = array();
foreach ($completed_info as $class => $info) {
$rows[] = array(
$class,
number_format($info['n']),
- number_format((int)($info['duration'] / $info['n'])).' us',
+ pht('%d us', number_format((int)($info['duration'] / $info['n']))),
);
}
if ($failed) {
// Add the time it takes to restart the daemons. This includes a guess
// about other overhead of 2X.
$restart_delay = PhutilDaemonHandle::getWaitBeforeRestart();
$usage_total += $restart_delay * count($failed) * 2;
foreach ($failed as $failed_task) {
$usage_start = min($usage_start, $failed_task->getFailureTime());
}
$rows[] = array(
phutil_tag('em', array(), pht('Temporary Failures')),
count($failed),
null,
);
}
$logs = id(new PhabricatorDaemonLogQuery())
->setViewer($viewer)
->withStatus(PhabricatorDaemonLogQuery::STATUS_ALIVE)
->setAllowStatusWrites(true)
->execute();
$taskmasters = 0;
foreach ($logs as $log) {
if ($log->getDaemon() == 'PhabricatorTaskmasterDaemon') {
$taskmasters++;
}
}
if ($taskmasters && $usage_total) {
// Total number of wall-time seconds the daemons have been running since
// the oldest event. For very short times round up to 15s so we don't
// render any ridiculous numbers if you reload the page immediately after
// restarting the daemons.
$available_time = $taskmasters * max(15, (time() - $usage_start));
// Percentage of those wall-time seconds we can account for, which the
// daemons spent doing work:
$used_time = ($usage_total / $available_time);
$rows[] = array(
phutil_tag('em', array(), pht('Queue Utilization (Approximate)')),
sprintf('%.1f%%', 100 * $used_time),
null,
);
}
$completed_table = new AphrontTableView($rows);
$completed_table->setNoDataString(
pht('No tasks have completed in the last 15 minutes.'));
$completed_table->setHeaders(
array(
pht('Class'),
pht('Count'),
pht('Avg'),
));
$completed_table->setColumnClasses(
array(
'wide',
'n',
'n',
));
$completed_panel = new PHUIObjectBoxView();
$completed_panel->setHeaderText(
pht('Recently Completed Tasks (Last 15m)'));
$completed_panel->appendChild($completed_table);
$daemon_table = new PhabricatorDaemonLogListView();
$daemon_table->setUser($viewer);
$daemon_table->setDaemonLogs($logs);
$daemon_panel = new PHUIObjectBoxView();
$daemon_panel->setHeaderText(pht('Active Daemons'));
$daemon_panel->appendChild($daemon_table);
$tasks = id(new PhabricatorWorkerLeaseQuery())
->setSkipLease(true)
->withLeasedTasks(true)
->setLimit(100)
->execute();
$tasks_table = id(new PhabricatorDaemonTasksTableView())
->setTasks($tasks)
->setNoDataString(pht('No tasks are leased by workers.'));
$leased_panel = id(new PHUIObjectBoxView())
->setHeaderText(pht('Leased Tasks'))
->appendChild($tasks_table);
$task_table = new PhabricatorWorkerActiveTask();
$queued = queryfx_all(
$task_table->establishConnection('r'),
'SELECT taskClass, count(*) N FROM %T GROUP BY taskClass
ORDER BY N DESC',
$task_table->getTableName());
$rows = array();
foreach ($queued as $row) {
$rows[] = array(
$row['taskClass'],
number_format($row['N']),
);
}
$queued_table = new AphrontTableView($rows);
$queued_table->setHeaders(
array(
pht('Class'),
pht('Count'),
));
$queued_table->setColumnClasses(
array(
'wide',
'n',
));
$queued_table->setNoDataString(pht('Task queue is empty.'));
$queued_panel = new PHUIObjectBoxView();
$queued_panel->setHeaderText(pht('Queued Tasks'));
$queued_panel->appendChild($queued_table);
$upcoming = id(new PhabricatorWorkerLeaseQuery())
->setLimit(10)
->setSkipLease(true)
->execute();
$upcoming_panel = id(new PHUIObjectBoxView())
->setHeaderText(pht('Next In Queue'))
->appendChild(
id(new PhabricatorDaemonTasksTableView())
->setTasks($upcoming)
->setNoDataString(pht('Task queue is empty.')));
$triggers = id(new PhabricatorWorkerTriggerQuery())
->setViewer($viewer)
->setOrder(PhabricatorWorkerTriggerQuery::ORDER_EXECUTION)
->needEvents(true)
->setLimit(10)
->execute();
$triggers_table = $this->buildTriggersTable($triggers);
$triggers_panel = id(new PHUIObjectBoxView())
->setHeaderText(pht('Upcoming Triggers'))
->appendChild($triggers_table);
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb(pht('Console'));
$nav = $this->buildSideNavView();
$nav->selectFilter('/');
$nav->appendChild(
array(
$crumbs,
$completed_panel,
$daemon_panel,
$queued_panel,
$leased_panel,
$upcoming_panel,
$triggers_panel,
));
return $this->buildApplicationPage(
$nav,
array(
'title' => pht('Console'),
'device' => false,
));
}
private function buildTriggersTable(array $triggers) {
$viewer = $this->getViewer();
$rows = array();
foreach ($triggers as $trigger) {
$event = $trigger->getEvent();
if ($event) {
$last_epoch = $event->getLastEventEpoch();
$next_epoch = $event->getNextEventEpoch();
} else {
$last_epoch = null;
$next_epoch = null;
}
$rows[] = array(
$trigger->getID(),
$trigger->getClockClass(),
$trigger->getActionClass(),
$last_epoch ? phabricator_datetime($last_epoch, $viewer) : null,
$next_epoch ? phabricator_datetime($next_epoch, $viewer) : null,
);
}
return id(new AphrontTableView($rows))
->setNoDataString(pht('There are no upcoming event triggers.'))
->setHeaders(
array(
pht('ID'),
pht('Clock'),
pht('Action'),
pht('Last'),
pht('Next'),
))
->setColumnClasses(
array(
'',
'',
'wide',
'date',
'date',
));
}
}
diff --git a/src/applications/daemon/controller/PhabricatorDaemonLogViewController.php b/src/applications/daemon/controller/PhabricatorDaemonLogViewController.php
index 17d998fe7..baab0528e 100644
--- a/src/applications/daemon/controller/PhabricatorDaemonLogViewController.php
+++ b/src/applications/daemon/controller/PhabricatorDaemonLogViewController.php
@@ -1,199 +1,197 @@
<?php
final class PhabricatorDaemonLogViewController
extends PhabricatorDaemonController {
private $id;
public function willProcessRequest(array $data) {
$this->id = $data['id'];
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$log = id(new PhabricatorDaemonLogQuery())
->setViewer($user)
->withIDs(array($this->id))
->setAllowStatusWrites(true)
->executeOne();
if (!$log) {
return new Aphront404Response();
}
$events = id(new PhabricatorDaemonLogEvent())->loadAllWhere(
'logID = %d ORDER BY id DESC LIMIT 1000',
$log->getID());
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb(pht('Daemon %s', $log->getID()));
$header = id(new PHUIHeaderView())
->setHeader($log->getDaemon());
$tag = id(new PHUITagView())
->setType(PHUITagView::TYPE_STATE);
$status = $log->getStatus();
switch ($status) {
case PhabricatorDaemonLog::STATUS_UNKNOWN:
$tag->setBackgroundColor(PHUITagView::COLOR_ORANGE);
$tag->setName(pht('Unknown'));
break;
case PhabricatorDaemonLog::STATUS_RUNNING:
$tag->setBackgroundColor(PHUITagView::COLOR_GREEN);
$tag->setName(pht('Running'));
break;
case PhabricatorDaemonLog::STATUS_DEAD:
$tag->setBackgroundColor(PHUITagView::COLOR_RED);
$tag->setName(pht('Dead'));
break;
case PhabricatorDaemonLog::STATUS_WAIT:
$tag->setBackgroundColor(PHUITagView::COLOR_BLUE);
$tag->setName(pht('Waiting'));
break;
case PhabricatorDaemonLog::STATUS_EXITING:
$tag->setBackgroundColor(PHUITagView::COLOR_YELLOW);
$tag->setName(pht('Exiting'));
break;
case PhabricatorDaemonLog::STATUS_EXITED:
$tag->setBackgroundColor(PHUITagView::COLOR_GREY);
$tag->setName(pht('Exited'));
break;
}
$header->addTag($tag);
$env_hash = PhabricatorEnv::calculateEnvironmentHash();
if ($log->getEnvHash() != $env_hash) {
$tag = id(new PHUITagView())
->setType(PHUITagView::TYPE_STATE)
->setBackgroundColor(PHUITagView::COLOR_YELLOW)
->setName(pht('Stale Config'));
$header->addTag($tag);
}
$properties = $this->buildPropertyListView($log);
$event_view = id(new PhabricatorDaemonLogEventsView())
->setUser($user)
->setEvents($events);
$event_panel = new PHUIObjectBoxView();
$event_panel->setHeaderText(pht('Events'));
$event_panel->appendChild($event_view);
$object_box = id(new PHUIObjectBoxView())
->setHeader($header)
->addPropertyList($properties);
return $this->buildApplicationPage(
array(
$crumbs,
$object_box,
$event_panel,
),
array(
'title' => pht('Daemon Log'),
));
}
private function buildPropertyListView(PhabricatorDaemonLog $daemon) {
$request = $this->getRequest();
$viewer = $request->getUser();
$view = id(new PHUIPropertyListView())
->setUser($viewer);
$id = $daemon->getID();
$c_epoch = $daemon->getDateCreated();
$u_epoch = $daemon->getDateModified();
$unknown_time = PhabricatorDaemonLogQuery::getTimeUntilUnknown();
$dead_time = PhabricatorDaemonLogQuery::getTimeUntilDead();
$wait_time = PhutilDaemonHandle::getWaitBeforeRestart();
$details = null;
$status = $daemon->getStatus();
switch ($status) {
case PhabricatorDaemonLog::STATUS_RUNNING:
$details = pht(
'This daemon is running normally and reported a status update '.
'recently (within %s).',
phutil_format_relative_time($unknown_time));
break;
case PhabricatorDaemonLog::STATUS_UNKNOWN:
$details = pht(
'This daemon has not reported a status update recently (within %s). '.
'It may have exited abruptly. After %s, it will be presumed dead.',
phutil_format_relative_time($unknown_time),
phutil_format_relative_time($dead_time));
break;
case PhabricatorDaemonLog::STATUS_DEAD:
$details = pht(
'This daemon did not report a status update for %s. It is '.
'presumed dead. Usually, this indicates that the daemon was '.
'killed or otherwise exited abruptly with an error. You may '.
'need to restart it.',
phutil_format_relative_time($dead_time));
break;
case PhabricatorDaemonLog::STATUS_WAIT:
$details = pht(
'This daemon is running normally and reported a status update '.
'recently (within %s). However, it encountered an error while '.
'doing work and is waiting a little while (%s) to resume '.
'processing. After encountering an error, daemons wait before '.
'resuming work to avoid overloading services.',
phutil_format_relative_time($unknown_time),
phutil_format_relative_time($wait_time));
break;
case PhabricatorDaemonLog::STATUS_EXITING:
- $details = pht(
- 'This daemon is shutting down gracefully.');
+ $details = pht('This daemon is shutting down gracefully.');
break;
case PhabricatorDaemonLog::STATUS_EXITED:
- $details = pht(
- 'This daemon exited normally and is no longer running.');
+ $details = pht('This daemon exited normally and is no longer running.');
break;
}
$view->addProperty(pht('Status Details'), $details);
$view->addProperty(pht('Daemon Class'), $daemon->getDaemon());
$view->addProperty(pht('Host'), $daemon->getHost());
$view->addProperty(pht('PID'), $daemon->getPID());
$view->addProperty(pht('Running as'), $daemon->getRunningAsUser());
$view->addProperty(pht('Started'), phabricator_datetime($c_epoch, $viewer));
$view->addProperty(
pht('Seen'),
pht(
'%s ago (%s)',
phutil_format_relative_time(time() - $u_epoch),
phabricator_datetime($u_epoch, $viewer)));
$argv = $daemon->getArgv();
if (is_array($argv)) {
$argv = implode("\n", $argv);
}
$view->addProperty(
pht('Argv'),
phutil_tag(
'textarea',
array(
'style' => 'width: 100%; height: 12em;',
),
$argv));
$view->addProperty(
pht('View Full Logs'),
phutil_tag(
'tt',
array(),
"phabricator/ $ ./bin/phd log --id {$id}"));
return $view;
}
}
diff --git a/src/applications/daemon/controller/PhabricatorWorkerTaskDetailController.php b/src/applications/daemon/controller/PhabricatorWorkerTaskDetailController.php
index c7254de18..57951ccb9 100644
--- a/src/applications/daemon/controller/PhabricatorWorkerTaskDetailController.php
+++ b/src/applications/daemon/controller/PhabricatorWorkerTaskDetailController.php
@@ -1,224 +1,224 @@
<?php
final class PhabricatorWorkerTaskDetailController
extends PhabricatorDaemonController {
private $id;
public function willProcessRequest(array $data) {
$this->id = $data['id'];
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$task = id(new PhabricatorWorkerActiveTask())->load($this->id);
if (!$task) {
$tasks = id(new PhabricatorWorkerArchiveTaskQuery())
->withIDs(array($this->id))
->execute();
$task = reset($tasks);
}
if (!$task) {
$title = pht('Task Does Not Exist');
$error_view = new PHUIInfoView();
$error_view->setTitle(pht('No Such Task'));
$error_view->appendChild(phutil_tag(
'p',
array(),
pht('This task may have recently been garbage collected.')));
$error_view->setSeverity(PHUIInfoView::SEVERITY_NODATA);
$content = $error_view;
} else {
$title = pht('Task %d', $task->getID());
$header = id(new PHUIHeaderView())
->setHeader(pht('Task %d (%s)',
$task->getID(),
$task->getTaskClass()));
$properties = $this->buildPropertyListView($task);
$object_box = id(new PHUIObjectBoxView())
->setHeader($header)
->addPropertyList($properties);
$retry_head = id(new PHUIHeaderView())
->setHeader(pht('Retries'));
$retry_info = $this->buildRetryListView($task);
$retry_box = id(new PHUIObjectBoxView())
->setHeader($retry_head)
->addPropertyList($retry_info);
$content = array(
$object_box,
$retry_box,
);
}
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb($title);
return $this->buildApplicationPage(
array(
$crumbs,
$content,
),
array(
'title' => $title,
));
}
private function buildPropertyListView(
PhabricatorWorkerTask $task) {
$viewer = $this->getRequest()->getUser();
$view = new PHUIPropertyListView();
if ($task->isArchived()) {
switch ($task->getResult()) {
case PhabricatorWorkerArchiveTask::RESULT_SUCCESS:
$status = pht('Complete');
break;
case PhabricatorWorkerArchiveTask::RESULT_FAILURE:
$status = pht('Failed');
break;
case PhabricatorWorkerArchiveTask::RESULT_CANCELLED:
$status = pht('Cancelled');
break;
default:
- throw new Exception('Unknown task status!');
+ throw new Exception(pht('Unknown task status!'));
}
} else {
$status = pht('Queued');
}
$view->addProperty(
pht('Task Status'),
$status);
$view->addProperty(
pht('Task Class'),
$task->getTaskClass());
if ($task->getLeaseExpires()) {
if ($task->getLeaseExpires() > time()) {
$lease_status = pht('Leased');
} else {
$lease_status = pht('Lease Expired');
}
} else {
$lease_status = phutil_tag('em', array(), pht('Not Leased'));
}
$view->addProperty(
pht('Lease Status'),
$lease_status);
$view->addProperty(
pht('Lease Owner'),
$task->getLeaseOwner()
? $task->getLeaseOwner()
: phutil_tag('em', array(), pht('None')));
if ($task->getLeaseExpires() && $task->getLeaseOwner()) {
$expires = ($task->getLeaseExpires() - time());
$expires = phutil_format_relative_time_detailed($expires);
} else {
$expires = phutil_tag('em', array(), pht('None'));
}
$view->addProperty(
pht('Lease Expires'),
$expires);
if ($task->isArchived()) {
- $duration = number_format($task->getDuration()).' us';
+ $duration = pht('%d us', number_format($task->getDuration()));
} else {
$duration = phutil_tag('em', array(), pht('Not Completed'));
}
$view->addProperty(
pht('Duration'),
$duration);
$data = id(new PhabricatorWorkerTaskData())->load($task->getDataID());
$task->setData($data->getData());
$worker = $task->getWorkerInstance();
$data = $worker->renderForDisplay($viewer);
$view->addProperty(
pht('Data'),
$data);
return $view;
}
private function buildRetryListView(PhabricatorWorkerTask $task) {
$view = new PHUIPropertyListView();
$data = id(new PhabricatorWorkerTaskData())->load($task->getDataID());
$task->setData($data->getData());
$worker = $task->getWorkerInstance();
$view->addProperty(
pht('Failure Count'),
$task->getFailureCount());
$retry_count = $worker->getMaximumRetryCount();
if ($retry_count === null) {
$max_retries = phutil_tag('em', array(), pht('Retries Forever'));
$retry_count = INF;
} else {
$max_retries = $retry_count;
}
$view->addProperty(
pht('Maximum Retries'),
$max_retries);
$projection = clone $task;
$projection->makeEphemeral();
$next = array();
for ($ii = $task->getFailureCount(); $ii < $retry_count; $ii++) {
$projection->setFailureCount($ii);
$next[] = $worker->getWaitBeforeRetry($projection);
if (count($next) > 10) {
break;
}
}
if ($next) {
$cumulative = 0;
foreach ($next as $key => $duration) {
if ($duration === null) {
$duration = 60;
}
$cumulative += $duration;
$next[$key] = phutil_format_relative_time($cumulative);
}
if ($ii != $retry_count) {
$next[] = '...';
}
$retries_in = implode(', ', $next);
} else {
$retries_in = pht('No More Retries');
}
$view->addProperty(
pht('Retries After'),
$retries_in);
return $view;
}
}
diff --git a/src/applications/daemon/management/PhabricatorDaemonManagementDebugWorkflow.php b/src/applications/daemon/management/PhabricatorDaemonManagementDebugWorkflow.php
index 23d5a54ee..e2023ae14 100644
--- a/src/applications/daemon/management/PhabricatorDaemonManagementDebugWorkflow.php
+++ b/src/applications/daemon/management/PhabricatorDaemonManagementDebugWorkflow.php
@@ -1,64 +1,66 @@
<?php
final class PhabricatorDaemonManagementDebugWorkflow
extends PhabricatorDaemonManagementWorkflow {
public function shouldParsePartial() {
return true;
}
protected function didConstruct() {
$this
->setName('debug')
->setExamples('**debug** __daemon__')
->setSynopsis(
pht(
'Start __daemon__ in the foreground and print large volumes of '.
'diagnostic information to the console.'))
->setArguments(
array(
array(
'name' => 'argv',
'wildcard' => true,
),
array(
'name' => 'as-current-user',
- 'help' => 'Run the daemon as the current user '.
- 'instead of the configured phd.user',
+ 'help' => pht(
+ 'Run the daemon as the current user '.
+ 'instead of the configured %s',
+ 'phd.user'),
),
array(
'name' => 'autoscale',
'help' => pht('Put the daemon in an autoscale group.'),
),
));
}
public function execute(PhutilArgumentParser $args) {
$argv = $args->getArg('argv');
$run_as_current_user = $args->getArg('as-current-user');
if (!$argv) {
throw new PhutilArgumentUsageException(
pht('You must specify which daemon to debug.'));
}
$config = array();
$config['class'] = array_shift($argv);
$config['argv'] = $argv;
if ($args->getArg('autoscale')) {
$config['autoscale'] = array(
'group' => 'debug',
);
}
return $this->launchDaemons(
array(
$config,
),
$is_debug = true,
$run_as_current_user);
}
}
diff --git a/src/applications/daemon/management/PhabricatorDaemonManagementLogWorkflow.php b/src/applications/daemon/management/PhabricatorDaemonManagementLogWorkflow.php
index b32c1439f..394b4735a 100644
--- a/src/applications/daemon/management/PhabricatorDaemonManagementLogWorkflow.php
+++ b/src/applications/daemon/management/PhabricatorDaemonManagementLogWorkflow.php
@@ -1,102 +1,102 @@
<?php
final class PhabricatorDaemonManagementLogWorkflow
extends PhabricatorDaemonManagementWorkflow {
protected function didConstruct() {
$this
->setName('log')
->setExamples('**log** [__options__]')
->setSynopsis(
pht(
'Print the logs for all daemons, or some daemon(s) identified by '.
'ID. You can get the ID for a daemon from the Daemon Console in '.
'the web interface.'))
->setArguments(
array(
array(
'name' => 'id',
'param' => 'id',
- 'help' => 'Show logs for daemon(s) with given ID(s).',
+ 'help' => pht('Show logs for daemon(s) with given ID(s).'),
'repeat' => true,
),
array(
'name' => 'limit',
'param' => 'N',
'default' => 100,
- 'help' => 'Show a specific number of log messages '.
- '(default 100).',
+ 'help' => pht(
+ 'Show a specific number of log messages (default 100).'),
),
));
}
public function execute(PhutilArgumentParser $args) {
$query = id(new PhabricatorDaemonLogQuery())
->setViewer($this->getViewer())
->setAllowStatusWrites(true);
$ids = $args->getArg('id');
if ($ids) {
$query->withIDs($ids);
}
$daemons = $query->execute();
if (!$daemons) {
if ($ids) {
throw new PhutilArgumentUsageException(
pht('No daemon(s) with id(s) "%s" exist!', implode(', ', $ids)));
} else {
throw new PhutilArgumentUsageException(
pht('No daemons are running.'));
}
}
$console = PhutilConsole::getConsole();
$limit = $args->getArg('limit');
$logs = id(new PhabricatorDaemonLogEvent())->loadAllWhere(
'logID IN (%Ld) ORDER BY id DESC LIMIT %d',
mpull($daemons, 'getID'),
$limit);
$logs = array_reverse($logs);
$lines = array();
foreach ($logs as $log) {
$text_lines = phutil_split_lines($log->getMessage(), $retain = false);
foreach ($text_lines as $line) {
$lines[] = array(
'id' => $log->getLogID(),
'type' => $log->getLogType(),
'date' => $log->getEpoch(),
'data' => $line,
);
}
}
// Each log message may be several lines. Limit the number of lines we
// output so that `--limit 123` means "show 123 lines", which is the most
// easily understandable behavior.
$lines = array_slice($lines, -$limit);
foreach ($lines as $line) {
$id = $line['id'];
$type = $line['type'];
$data = $line['data'];
$date = date('r', $line['date']);
$console->writeOut(
"%s\n",
- sprintf(
+ pht(
'Daemon %d %s [%s] %s',
$id,
$type,
$date,
$data));
}
return 0;
}
}
diff --git a/src/applications/daemon/management/PhabricatorDaemonManagementReloadWorkflow.php b/src/applications/daemon/management/PhabricatorDaemonManagementReloadWorkflow.php
index 4a7d9a764..fa70231a0 100644
--- a/src/applications/daemon/management/PhabricatorDaemonManagementReloadWorkflow.php
+++ b/src/applications/daemon/management/PhabricatorDaemonManagementReloadWorkflow.php
@@ -1,27 +1,28 @@
<?php
final class PhabricatorDaemonManagementReloadWorkflow
extends PhabricatorDaemonManagementWorkflow {
protected function didConstruct() {
$this
->setName('reload')
->setSynopsis(
pht(
'Gracefully restart daemon processes in-place to pick up changes '.
'to source. This will not disrupt running jobs. This is an '.
- 'advanced workflow; most installs should use __phd restart__.'))
+ 'advanced workflow; most installs should use __%s__.',
+ 'phd restart'))
->setArguments(
array(
array(
'name' => 'pids',
'wildcard' => true,
),
));
}
public function execute(PhutilArgumentParser $args) {
return $this->executeReloadCommand($args->getArg('pids'));
}
}
diff --git a/src/applications/daemon/management/PhabricatorDaemonManagementRestartWorkflow.php b/src/applications/daemon/management/PhabricatorDaemonManagementRestartWorkflow.php
index 5d8d6df89..adb6490f0 100644
--- a/src/applications/daemon/management/PhabricatorDaemonManagementRestartWorkflow.php
+++ b/src/applications/daemon/management/PhabricatorDaemonManagementRestartWorkflow.php
@@ -1,56 +1,54 @@
<?php
final class PhabricatorDaemonManagementRestartWorkflow
extends PhabricatorDaemonManagementWorkflow {
protected function didConstruct() {
$this
->setName('restart')
- ->setSynopsis(
- pht(
- 'Stop, then start the standard daemon loadout.'))
+ ->setSynopsis(pht('Stop, then start the standard daemon loadout.'))
->setArguments(
array(
array(
'name' => 'graceful',
'param' => 'seconds',
'help' => pht(
'Grace period for daemons to attempt a clean shutdown, in '.
'seconds. Defaults to __15__ seconds.'),
'default' => 15,
),
array(
'name' => 'gently',
'help' => pht(
'Ignore running processes that look like daemons but do not '.
'have corresponding PID files.'),
),
array(
'name' => 'force',
'help' => pht(
'Also stop running processes that look like daemons but do '.
'not have corresponding PID files.'),
),
$this->getAutoscaleReserveArgument(),
));
}
public function execute(PhutilArgumentParser $args) {
$err = $this->executeStopCommand(
array(),
array(
'graceful' => $args->getArg('graceful'),
'force' => $args->getArg('force'),
'gently' => $args->getArg('gently'),
));
if ($err) {
return $err;
}
return $this->executeStartCommand(
array(
'reserve' => (float)$args->getArg('autoscale-reserve', 0.0),
));
}
}
diff --git a/src/applications/daemon/management/PhabricatorDaemonManagementStartWorkflow.php b/src/applications/daemon/management/PhabricatorDaemonManagementStartWorkflow.php
index d489dca88..9a807357a 100644
--- a/src/applications/daemon/management/PhabricatorDaemonManagementStartWorkflow.php
+++ b/src/applications/daemon/management/PhabricatorDaemonManagementStartWorkflow.php
@@ -1,40 +1,41 @@
<?php
final class PhabricatorDaemonManagementStartWorkflow
extends PhabricatorDaemonManagementWorkflow {
protected function didConstruct() {
$this
->setName('start')
->setSynopsis(
pht(
'Start the standard configured collection of Phabricator daemons. '.
- 'This is appropriate for most installs. Use **phd launch** to '.
- 'customize which daemons are launched.'))
+ 'This is appropriate for most installs. Use **%s** to '.
+ 'customize which daemons are launched.',
+ 'phd launch'))
->setArguments(
array(
array(
'name' => 'keep-leases',
'help' => pht(
- 'By default, **phd start** will free all task leases held by '.
- 'the daemons. With this flag, this step will be skipped.'),
+ 'By default, **%s** will free all task leases held by '.
+ 'the daemons. With this flag, this step will be skipped.',
+ 'phd start'),
),
array(
'name' => 'force',
- 'help' => pht(
- 'Start daemons even if daemons are already running.'),
+ 'help' => pht('Start daemons even if daemons are already running.'),
),
$this->getAutoscaleReserveArgument(),
));
}
public function execute(PhutilArgumentParser $args) {
return $this->executeStartCommand(
array(
'keep-leases' => $args->getArg('keep-leases'),
'force' => $args->getArg('force'),
'reserve' => (float)$args->getArg('autoscale-reserve', 0.0),
));
}
}
diff --git a/src/applications/daemon/management/PhabricatorDaemonManagementStatusWorkflow.php b/src/applications/daemon/management/PhabricatorDaemonManagementStatusWorkflow.php
index 9fc3cc016..343e42ba6 100644
--- a/src/applications/daemon/management/PhabricatorDaemonManagementStatusWorkflow.php
+++ b/src/applications/daemon/management/PhabricatorDaemonManagementStatusWorkflow.php
@@ -1,106 +1,106 @@
<?php
final class PhabricatorDaemonManagementStatusWorkflow
extends PhabricatorDaemonManagementWorkflow {
protected function didConstruct() {
$this
->setName('status')
->setSynopsis(pht('Show status of running daemons.'))
->setArguments(
array(
array(
'name' => 'local',
'help' => pht('Show only local daemons.'),
),
));
}
public function execute(PhutilArgumentParser $args) {
$console = PhutilConsole::getConsole();
if ($args->getArg('local')) {
$daemons = $this->loadRunningDaemons();
} else {
$daemons = $this->loadAllRunningDaemons();
}
if (!$daemons) {
$console->writeErr(
"%s\n",
pht('There are no running Phabricator daemons.'));
return 1;
}
$status = 0;
$table = id(new PhutilConsoleTable())
->addColumns(array(
'id' => array(
'title' => pht('Log'),
),
'daemonID' => array(
'title' => pht('Daemon'),
),
'host' => array(
'title' => pht('Host'),
),
'pid' => array(
'title' => pht('Overseer'),
),
'started' => array(
'title' => pht('Started'),
),
'daemon' => array(
'title' => pht('Class'),
),
'argv' => array(
'title' => pht('Arguments'),
),
));
foreach ($daemons as $daemon) {
if ($daemon instanceof PhabricatorDaemonLog) {
$table->addRow(array(
'id' => $daemon->getID(),
'daemonID' => $daemon->getDaemonID(),
'host' => $daemon->getHost(),
'pid' => $daemon->getPID(),
'started' => date('M j Y, g:i:s A', $daemon->getDateCreated()),
'daemon' => $daemon->getDaemon(),
'argv' => csprintf('%LR', $daemon->getExplicitArgv()),
));
} else if ($daemon instanceof PhabricatorDaemonReference) {
$name = $daemon->getName();
if (!$daemon->isRunning()) {
$daemon->updateStatus(PhabricatorDaemonLog::STATUS_DEAD);
$status = 2;
- $name = '<DEAD> '.$name;
+ $name = pht('<DEAD> %s', $name);
}
$daemon_log = $daemon->getDaemonLog();
$id = null;
$daemon_id = null;
if ($daemon_log) {
$id = $daemon_log->getID();
$daemon_id = $daemon_log->getDaemonID();
}
$table->addRow(array(
'id' => $id,
'daemonID' => $daemon_id,
'host' => 'localhost',
'pid' => $daemon->getPID(),
'started' => $daemon->getEpochStarted()
? date('M j Y, g:i:s A', $daemon->getEpochStarted())
: null,
'daemon' => $name,
'argv' => csprintf('%LR', $daemon->getArgv()),
));
}
}
$table->draw();
}
}
diff --git a/src/applications/daemon/management/PhabricatorDaemonManagementStopWorkflow.php b/src/applications/daemon/management/PhabricatorDaemonManagementStopWorkflow.php
index e57c617c8..c54a7e9fe 100644
--- a/src/applications/daemon/management/PhabricatorDaemonManagementStopWorkflow.php
+++ b/src/applications/daemon/management/PhabricatorDaemonManagementStopWorkflow.php
@@ -1,52 +1,53 @@
<?php
final class PhabricatorDaemonManagementStopWorkflow
extends PhabricatorDaemonManagementWorkflow {
protected function didConstruct() {
$this
->setName('stop')
->setSynopsis(
pht(
'Stop all running daemons, or specific daemons identified by PIDs. '.
- 'Use **phd status** to find PIDs.'))
+ 'Use **%s** to find PIDs.',
+ 'phd status'))
->setArguments(
array(
array(
'name' => 'graceful',
'param' => 'seconds',
'help' => pht(
'Grace period for daemons to attempt a clean shutdown, in '.
'seconds. Defaults to __15__ seconds.'),
'default' => 15,
),
array(
'name' => 'force',
'help' => pht(
'Also stop running processes that look like daemons but do '.
'not have corresponding PID files.'),
),
array(
'name' => 'gently',
'help' => pht(
'Ignore running processes that look like daemons but do not '.
'have corresponding PID files.'),
),
array(
'name' => 'pids',
'wildcard' => true,
),
));
}
public function execute(PhutilArgumentParser $args) {
return $this->executeStopCommand(
$args->getArg('pids'),
array(
'graceful' => $args->getArg('graceful'),
'force' => $args->getArg('force'),
'gently' => $args->getArg('gently'),
));
}
}
diff --git a/src/applications/daemon/management/PhabricatorDaemonManagementWorkflow.php b/src/applications/daemon/management/PhabricatorDaemonManagementWorkflow.php
index def781b4a..63303d6fb 100644
--- a/src/applications/daemon/management/PhabricatorDaemonManagementWorkflow.php
+++ b/src/applications/daemon/management/PhabricatorDaemonManagementWorkflow.php
@@ -1,655 +1,679 @@
<?php
abstract class PhabricatorDaemonManagementWorkflow
extends PhabricatorManagementWorkflow {
private $runDaemonsAsUser = null;
protected final function loadAvailableDaemonClasses() {
$loader = new PhutilSymbolLoader();
return $loader
->setAncestorClass('PhutilDaemon')
->setConcreteOnly(true)
->selectSymbolsWithoutLoading();
}
protected final function getPIDDirectory() {
$path = PhabricatorEnv::getEnvConfig('phd.pid-directory');
return $this->getControlDirectory($path);
}
protected final function getLogDirectory() {
$path = PhabricatorEnv::getEnvConfig('phd.log-directory');
return $this->getControlDirectory($path);
}
private function getControlDirectory($path) {
if (!Filesystem::pathExists($path)) {
list($err) = exec_manual('mkdir -p %s', $path);
if ($err) {
throw new Exception(
- "phd requires the directory '{$path}' to exist, but it does not ".
- "exist and could not be created. Create this directory or update ".
- "'phd.pid-directory' / 'phd.log-directory' in your configuration ".
- "to point to an existing directory.");
+ pht(
+ "%s requires the directory '%s' to exist, but it does not exist ".
+ "and could not be created. Create this directory or update ".
+ "'%s' / '%s' in your configuration to point to an existing ".
+ "directory.",
+ 'phd',
+ $path,
+ 'phd.pid-directory',
+ 'phd.log-directory'));
}
}
return $path;
}
protected final function loadRunningDaemons() {
$daemons = array();
$pid_dir = $this->getPIDDirectory();
$pid_files = Filesystem::listDirectory($pid_dir);
foreach ($pid_files as $pid_file) {
$path = $pid_dir.'/'.$pid_file;
$daemons[] = PhabricatorDaemonReference::loadReferencesFromFile($path);
}
return array_mergev($daemons);
}
protected final function loadAllRunningDaemons() {
$local_daemons = $this->loadRunningDaemons();
$local_ids = array();
foreach ($local_daemons as $daemon) {
$daemon_log = $daemon->getDaemonLog();
if ($daemon_log) {
$local_ids[] = $daemon_log->getID();
}
}
$daemon_query = id(new PhabricatorDaemonLogQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withStatus(PhabricatorDaemonLogQuery::STATUS_ALIVE);
if ($local_ids) {
$daemon_query->withoutIDs($local_ids);
}
$remote_daemons = $daemon_query->execute();
return array_merge($local_daemons, $remote_daemons);
}
private function findDaemonClass($substring) {
$symbols = $this->loadAvailableDaemonClasses();
$symbols = ipull($symbols, 'name');
$match = array();
foreach ($symbols as $symbol) {
if (stripos($symbol, $substring) !== false) {
if (strtolower($symbol) == strtolower($substring)) {
$match = array($symbol);
break;
} else {
$match[] = $symbol;
}
}
}
if (count($match) == 0) {
throw new PhutilArgumentUsageException(
pht(
- "No daemons match '%s'! Use 'phd list' for a list of available ".
- "daemons.",
- $substring));
+ "No daemons match '%s'! Use '%s' for a list of available daemons.",
+ $substring,
+ 'phd list'));
} else if (count($match) > 1) {
throw new PhutilArgumentUsageException(
pht(
"Specify a daemon unambiguously. Multiple daemons match '%s': %s.",
$substring,
implode(', ', $match)));
}
return head($match);
}
protected final function launchDaemons(
array $daemons,
$debug,
$run_as_current_user = false) {
// Convert any shorthand classnames like "taskmaster" into proper class
// names.
foreach ($daemons as $key => $daemon) {
$class = $this->findDaemonClass($daemon['class']);
$daemons[$key]['class'] = $class;
}
$console = PhutilConsole::getConsole();
if (!$run_as_current_user) {
// Check if the script is started as the correct user
$phd_user = PhabricatorEnv::getEnvConfig('phd.user');
$current_user = posix_getpwuid(posix_geteuid());
$current_user = $current_user['name'];
if ($phd_user && $phd_user != $current_user) {
if ($debug) {
- throw new PhutilArgumentUsageException(pht(
- 'You are trying to run a daemon as a nonstandard user, '.
- 'and `phd` was not able to `sudo` to the correct user. '."\n".
- 'Phabricator is configured to run daemons as "%s", '.
- 'but the current user is "%s". '."\n".
- 'Use `sudo` to run as a different user, pass `--as-current-user` '.
- 'to ignore this warning, or edit `phd.user` '.
- 'to change the configuration.', $phd_user, $current_user));
+ throw new PhutilArgumentUsageException(
+ pht(
+ "You are trying to run a daemon as a nonstandard user, ".
+ "and `%s` was not able to `%s` to the correct user. \n".
+ 'Phabricator is configured to run daemons as "%s", '.
+ 'but the current user is "%s". '."\n".
+ 'Use `%s` to run as a different user, pass `%s` to ignore this '.
+ 'warning, or edit `%s` to change the configuration.',
+ 'phd',
+ 'sudo',
+ $phd_user,
+ $current_user,
+ 'sudo',
+ '--as-current-user',
+ 'phd.user'));
} else {
$this->runDaemonsAsUser = $phd_user;
$console->writeOut(pht('Starting daemons as %s', $phd_user)."\n");
}
}
}
$this->printLaunchingDaemons($daemons, $debug);
$flags = array();
if ($debug || PhabricatorEnv::getEnvConfig('phd.trace')) {
$flags[] = '--trace';
}
if ($debug || PhabricatorEnv::getEnvConfig('phd.verbose')) {
$flags[] = '--verbose';
}
$instance = PhabricatorEnv::getEnvConfig('cluster.instance');
if ($instance) {
$flags[] = '-l';
$flags[] = $instance;
}
$config = array();
if (!$debug) {
$config['daemonize'] = true;
}
if (!$debug) {
$config['log'] = $this->getLogDirectory().'/daemons.log';
}
$pid_dir = $this->getPIDDirectory();
// TODO: This should be a much better user experience.
Filesystem::assertExists($pid_dir);
Filesystem::assertIsDirectory($pid_dir);
Filesystem::assertWritable($pid_dir);
$config['piddir'] = $pid_dir;
$config['daemons'] = $daemons;
$command = csprintf('./phd-daemon %Ls', $flags);
$phabricator_root = dirname(phutil_get_library_root('phabricator'));
$daemon_script_dir = $phabricator_root.'/scripts/daemon/';
if ($debug) {
// Don't terminate when the user sends ^C; it will be sent to the
// subprocess which will terminate normally.
pcntl_signal(
SIGINT,
array(__CLASS__, 'ignoreSignal'));
echo "\n phabricator/scripts/daemon/ \$ {$command}\n\n";
$tempfile = new TempFile('daemon.config');
Filesystem::writeFile($tempfile, json_encode($config));
phutil_passthru(
'(cd %s && exec %C < %s)',
$daemon_script_dir,
$command,
$tempfile);
} else {
try {
$this->executeDaemonLaunchCommand(
$command,
$daemon_script_dir,
$config,
$this->runDaemonsAsUser);
} catch (Exception $e) {
// Retry without sudo
- $console->writeOut(pht(
- "sudo command failed. Starting daemon as current user\n"));
+ $console->writeOut(
+ "%s\n",
+ pht('sudo command failed. Starting daemon as current user.'));
$this->executeDaemonLaunchCommand(
$command,
$daemon_script_dir,
$config);
}
}
}
private function executeDaemonLaunchCommand(
$command,
$daemon_script_dir,
array $config,
$run_as_user = null) {
$is_sudo = false;
if ($run_as_user) {
// If anything else besides sudo should be
// supported then insert it here (runuser, su, ...)
$command = csprintf(
'sudo -En -u %s -- %C',
$run_as_user,
$command);
$is_sudo = true;
}
$future = new ExecFuture('exec %C', $command);
// Play games to keep 'ps' looking reasonable.
$future->setCWD($daemon_script_dir);
$future->write(json_encode($config));
list($stdout, $stderr) = $future->resolvex();
if ($is_sudo) {
// On OSX, `sudo -n` exits 0 when the user does not have permission to
// switch accounts without a password. This is not consistent with
// sudo on Linux, and seems buggy/broken. Check for this by string
// matching the output.
if (preg_match('/sudo: a password is required/', $stderr)) {
throw new Exception(
pht(
'sudo exited with a zero exit code, but emitted output '.
'consistent with failure under OSX.'));
}
}
}
public static function ignoreSignal($signo) {
return;
}
public static function requireExtensions() {
self::mustHaveExtension('pcntl');
self::mustHaveExtension('posix');
}
private static function mustHaveExtension($ext) {
if (!extension_loaded($ext)) {
- echo "ERROR: The PHP extension '{$ext}' is not installed. You must ".
- "install it to run daemons on this machine.\n";
+ echo pht(
+ "ERROR: The PHP extension '%s' is not installed. You must ".
+ "install it to run daemons on this machine.\n",
+ $ext);
exit(1);
}
$extension = new ReflectionExtension($ext);
foreach ($extension->getFunctions() as $function) {
$function = $function->name;
if (!function_exists($function)) {
- echo "ERROR: The PHP function {$function}() is disabled. You must ".
- "enable it to run daemons on this machine.\n";
+ echo pht(
+ "ERROR: The PHP function %s is disabled. You must ".
+ "enable it to run daemons on this machine.\n",
+ $function.'()');
exit(1);
}
}
}
/* -( Commands )----------------------------------------------------------- */
protected final function executeStartCommand(array $options) {
PhutilTypeSpec::checkMap(
$options,
array(
'keep-leases' => 'optional bool',
'force' => 'optional bool',
'reserve' => 'optional float',
));
$console = PhutilConsole::getConsole();
if (!idx($options, 'force')) {
$running = $this->loadRunningDaemons();
// This may include daemons which were launched but which are no longer
// running; check that we actually have active daemons before failing.
foreach ($running as $daemon) {
if ($daemon->isRunning()) {
$message = pht(
"phd start: Unable to start daemons because daemons are already ".
"running.\n\n".
- "You can view running daemons with 'phd status'.\n".
- "You can stop running daemons with 'phd stop'.\n".
- "You can use 'phd restart' to stop all daemons before starting ".
+ "You can view running daemons with '%s'.\n".
+ "You can stop running daemons with '%s'.\n".
+ "You can use '%s' to stop all daemons before starting ".
"new daemons.\n".
- "You can force daemons to start anyway with --force.");
+ "You can force daemons to start anyway with %s.",
+ 'phd status',
+ 'phd stop',
+ 'phd restart',
+ '--force');
$console->writeErr("%s\n", $message);
exit(1);
}
}
}
if (idx($options, 'keep-leases')) {
$console->writeErr("%s\n", pht('Not touching active task queue leases.'));
} else {
$console->writeErr("%s\n", pht('Freeing active task leases...'));
$count = $this->freeActiveLeases();
$console->writeErr(
"%s\n",
pht('Freed %s task lease(s).', new PhutilNumber($count)));
}
$daemons = array(
array(
'class' => 'PhabricatorRepositoryPullLocalDaemon',
),
array(
'class' => 'PhabricatorTriggerDaemon',
),
array(
'class' => 'PhabricatorTaskmasterDaemon',
'autoscale' => array(
'group' => 'task',
'pool' => PhabricatorEnv::getEnvConfig('phd.taskmasters'),
'reserve' => idx($options, 'reserve', 0),
),
),
);
$this->launchDaemons($daemons, $is_debug = false);
- $console->writeErr(pht('Done.')."\n");
+ $console->writeErr("%s\n", pht('Done.'));
return 0;
}
protected final function executeStopCommand(
array $pids,
array $options) {
$console = PhutilConsole::getConsole();
$grace_period = idx($options, 'graceful', 15);
$force = idx($options, 'force');
$gently = idx($options, 'gently');
if ($gently && $force) {
throw new PhutilArgumentUsageException(
pht(
- 'You can not specify conflicting options --gently and --force '.
- 'together.'));
+ 'You can not specify conflicting options %s and %s together.',
+ '--gently',
+ '--force'));
}
$daemons = $this->loadRunningDaemons();
if (!$daemons) {
$survivors = array();
if (!$pids && !$gently) {
$survivors = $this->processRogueDaemons(
$grace_period,
$warn = true,
$force);
}
if (!$survivors) {
- $console->writeErr(pht(
- 'There are no running Phabricator daemons.')."\n");
+ $console->writeErr(
+ "%s\n",
+ pht('There are no running Phabricator daemons.'));
}
return 0;
}
$stop_pids = $this->selectDaemonPIDs($daemons, $pids);
if (!$stop_pids) {
- $console->writeErr(pht('No daemons to kill.')."\n");
+ $console->writeErr("%s\n", pht('No daemons to kill.'));
return 0;
}
$survivors = $this->sendStopSignals($stop_pids, $grace_period);
// Try to clean up PID files for daemons we killed.
$remove = array();
foreach ($daemons as $daemon) {
$pid = $daemon->getPID();
if (empty($stop_pids[$pid])) {
// We did not try to stop this overseer.
continue;
}
if (isset($survivors[$pid])) {
// We weren't able to stop this overseer.
continue;
}
if (!$daemon->getPIDFile()) {
// We don't know where the PID file is.
continue;
}
$remove[] = $daemon->getPIDFile();
}
foreach (array_unique($remove) as $remove_file) {
Filesystem::remove($remove_file);
}
if (!$gently) {
$this->processRogueDaemons($grace_period, !$pids, $force);
}
return 0;
}
protected final function executeReloadCommand(array $pids) {
$console = PhutilConsole::getConsole();
$daemons = $this->loadRunningDaemons();
if (!$daemons) {
$console->writeErr(
"%s\n",
pht('There are no running daemons to reload.'));
return 0;
}
$reload_pids = $this->selectDaemonPIDs($daemons, $pids);
if (!$reload_pids) {
$console->writeErr(
"%s\n",
pht('No daemons to reload.'));
return 0;
}
foreach ($reload_pids as $pid) {
$console->writeOut(
"%s\n",
pht('Reloading process %d...', $pid));
posix_kill($pid, SIGHUP);
}
return 0;
}
private function processRogueDaemons($grace_period, $warn, $force_stop) {
$console = PhutilConsole::getConsole();
$rogue_daemons = PhutilDaemonOverseer::findRunningDaemons();
if ($rogue_daemons) {
if ($force_stop) {
$rogue_pids = ipull($rogue_daemons, 'pid');
$survivors = $this->sendStopSignals($rogue_pids, $grace_period);
if ($survivors) {
$console->writeErr(
+ "%s\n",
pht(
'Unable to stop processes running without PID files. '.
- 'Try running this command again with sudo.')."\n");
+ 'Try running this command again with sudo.'));
}
} else if ($warn) {
- $console->writeErr($this->getForceStopHint($rogue_daemons)."\n");
+ $console->writeErr("%s\n", $this->getForceStopHint($rogue_daemons));
}
}
return $rogue_daemons;
}
private function getForceStopHint($rogue_daemons) {
$debug_output = '';
foreach ($rogue_daemons as $rogue) {
$debug_output .= $rogue['pid'].' '.$rogue['command']."\n";
}
return pht(
- 'There are processes running that look like Phabricator daemons but '.
- 'have no corresponding PID files:'."\n\n".'%s'."\n\n".
- 'Stop these processes by re-running this command with the --force '.
- 'parameter.',
- $debug_output);
+ "There are processes running that look like Phabricator daemons but ".
+ "have no corresponding PID files:\n\n%s\n\n".
+ "Stop these processes by re-running this command with the %s parameter.",
+ $debug_output,
+ '--force');
}
private function sendStopSignals($pids, $grace_period) {
// If we're doing a graceful shutdown, try SIGINT first.
if ($grace_period) {
$pids = $this->sendSignal($pids, SIGINT, $grace_period);
}
// If we still have daemons, SIGTERM them.
if ($pids) {
$pids = $this->sendSignal($pids, SIGTERM, 15);
}
// If the overseer is still alive, SIGKILL it.
if ($pids) {
$pids = $this->sendSignal($pids, SIGKILL, 0);
}
return $pids;
}
private function sendSignal(array $pids, $signo, $wait) {
$console = PhutilConsole::getConsole();
$pids = array_fuse($pids);
foreach ($pids as $key => $pid) {
if (!$pid) {
// NOTE: We must have a PID to signal a daemon, since sending a signal
// to PID 0 kills this process.
unset($pids[$key]);
continue;
}
switch ($signo) {
case SIGINT:
$message = pht('Interrupting process %d...', $pid);
break;
case SIGTERM:
$message = pht('Terminating process %d...', $pid);
break;
case SIGKILL:
$message = pht('Killing process %d...', $pid);
break;
}
$console->writeOut("%s\n", $message);
posix_kill($pid, $signo);
}
if ($wait) {
$start = PhabricatorTime::getNow();
do {
foreach ($pids as $key => $pid) {
if (!PhabricatorDaemonReference::isProcessRunning($pid)) {
$console->writeOut(pht('Process %d exited.', $pid)."\n");
unset($pids[$key]);
}
}
if (empty($pids)) {
break;
}
usleep(100000);
} while (PhabricatorTime::getNow() < $start + $wait);
}
return $pids;
}
private function freeActiveLeases() {
$task_table = id(new PhabricatorWorkerActiveTask());
$conn_w = $task_table->establishConnection('w');
queryfx(
$conn_w,
'UPDATE %T SET leaseExpires = UNIX_TIMESTAMP()
WHERE leaseExpires > UNIX_TIMESTAMP()',
$task_table->getTableName());
return $conn_w->getAffectedRows();
}
private function printLaunchingDaemons(array $daemons, $debug) {
$console = PhutilConsole::getConsole();
if ($debug) {
$console->writeOut(pht('Launching daemons (in debug mode):'));
} else {
$console->writeOut(pht('Launching daemons:'));
}
$log_dir = $this->getLogDirectory().'/daemons.log';
$console->writeOut(
"\n%s\n\n",
pht('(Logs will appear in "%s".)', $log_dir));
foreach ($daemons as $daemon) {
$is_autoscale = isset($daemon['autoscale']['group']);
if ($is_autoscale) {
$autoscale = $daemon['autoscale'];
foreach ($autoscale as $key => $value) {
$autoscale[$key] = $key.'='.$value;
}
$autoscale = implode(', ', $autoscale);
$autoscale = pht('(Autoscaling: %s)', $autoscale);
} else {
$autoscale = pht('(Static)');
}
$console->writeOut(
" %s %s\n",
$daemon['class'],
$autoscale,
implode(' ', idx($daemon, 'argv', array())));
}
$console->writeOut("\n");
}
protected function getAutoscaleReserveArgument() {
return array(
'name' => 'autoscale-reserve',
'param' => 'ratio',
'help' => pht(
'Specify a proportion of machine memory which must be free '.
'before autoscale pools will grow. For example, a value of 0.25 '.
'means that pools will not grow unless the machine has at least '.
'25%%%% of its RAM free.'),
);
}
private function selectDaemonPIDs(array $daemons, array $pids) {
$console = PhutilConsole::getConsole();
$running_pids = array_fuse(mpull($daemons, 'getPID'));
if (!$pids) {
$select_pids = $running_pids;
} else {
// We were given a PID or set of PIDs to kill.
$select_pids = array();
foreach ($pids as $key => $pid) {
if (!preg_match('/^\d+$/', $pid)) {
$console->writeErr(pht("PID '%s' is not a valid PID.", $pid)."\n");
continue;
} else if (empty($running_pids[$pid])) {
$console->writeErr(
"%s\n",
pht(
'PID "%d" is not a known Phabricator daemon PID.',
$pid));
continue;
} else {
$select_pids[$pid] = $pid;
}
}
}
return $select_pids;
}
}
diff --git a/src/applications/daemon/view/PhabricatorDaemonLogEventsView.php b/src/applications/daemon/view/PhabricatorDaemonLogEventsView.php
index b06ebdd7d..4c0caa6fd 100644
--- a/src/applications/daemon/view/PhabricatorDaemonLogEventsView.php
+++ b/src/applications/daemon/view/PhabricatorDaemonLogEventsView.php
@@ -1,134 +1,134 @@
<?php
final class PhabricatorDaemonLogEventsView extends AphrontView {
private $events;
private $combinedLog;
private $showFullMessage;
public function setShowFullMessage($show_full_message) {
$this->showFullMessage = $show_full_message;
return $this;
}
public function setEvents(array $events) {
assert_instances_of($events, 'PhabricatorDaemonLogEvent');
$this->events = $events;
return $this;
}
public function setCombinedLog($is_combined) {
$this->combinedLog = $is_combined;
return $this;
}
public function render() {
$rows = array();
if (!$this->user) {
throw new PhutilInvalidStateException('setUser');
}
foreach ($this->events as $event) {
// Limit display log size. If a daemon gets stuck in an output loop this
// page can be like >100MB if we don't truncate stuff. Try to do cheap
// line-based truncation first, and fall back to expensive UTF-8 character
// truncation if that doesn't get things short enough.
$message = $event->getMessage();
$more = null;
if (!$this->showFullMessage) {
$more_lines = null;
$more_chars = null;
$line_limit = 12;
if (substr_count($message, "\n") > $line_limit) {
$message = explode("\n", $message);
$more_lines = count($message) - $line_limit;
$message = array_slice($message, 0, $line_limit);
$message = implode("\n", $message);
}
$char_limit = 8192;
if (strlen($message) > $char_limit) {
$message = phutil_utf8v($message);
$more_chars = count($message) - $char_limit;
$message = array_slice($message, 0, $char_limit);
$message = implode('', $message);
}
if ($more_chars) {
$more = new PhutilNumber($more_chars);
$more = pht('Show %d more character(s)...', $more);
} else if ($more_lines) {
$more = new PhutilNumber($more_lines);
$more = pht('Show %d more line(s)...', $more);
}
if ($more) {
$id = $event->getID();
$more = array(
"\n...\n",
phutil_tag(
'a',
array(
'href' => "/daemon/event/{$id}/",
),
$more),
);
}
}
$row = array(
$event->getLogType(),
phabricator_date($event->getEpoch(), $this->user),
phabricator_time($event->getEpoch(), $this->user),
array(
$message,
$more,
),
);
if ($this->combinedLog) {
array_unshift(
$row,
phutil_tag(
'a',
array(
'href' => '/daemon/log/'.$event->getLogID().'/',
),
- 'Daemon '.$event->getLogID()));
+ pht('Daemon %s', $event->getLogID())));
}
$rows[] = $row;
}
$classes = array(
'',
'',
'right',
'wide prewrap',
);
$headers = array(
'Type',
'Date',
'Time',
'Message',
);
if ($this->combinedLog) {
array_unshift($classes, 'pri');
array_unshift($headers, 'Daemon');
}
$log_table = new AphrontTableView($rows);
$log_table->setHeaders($headers);
$log_table->setColumnClasses($classes);
return $log_table->render();
}
}
diff --git a/src/applications/dashboard/controller/PhabricatorDashboardEditController.php b/src/applications/dashboard/controller/PhabricatorDashboardEditController.php
index cad74f7a5..26ddd2870 100644
--- a/src/applications/dashboard/controller/PhabricatorDashboardEditController.php
+++ b/src/applications/dashboard/controller/PhabricatorDashboardEditController.php
@@ -1,345 +1,343 @@
<?php
final class PhabricatorDashboardEditController
extends PhabricatorDashboardController {
private $id;
public function willProcessRequest(array $data) {
$this->id = idx($data, 'id');
}
public function processRequest() {
$request = $this->getRequest();
$viewer = $request->getUser();
if ($this->id) {
$dashboard = id(new PhabricatorDashboardQuery())
->setViewer($viewer)
->withIDs(array($this->id))
->needPanels(true)
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
if (!$dashboard) {
return new Aphront404Response();
}
$is_new = false;
} else {
if (!$request->getStr('edit')) {
if ($request->isFormPost()) {
switch ($request->getStr('template')) {
case 'empty':
break;
default:
return $this->processBuildTemplateRequest($request);
}
} else {
return $this->processTemplateRequest($request);
}
}
$dashboard = PhabricatorDashboard::initializeNewDashboard($viewer);
$is_new = true;
}
$crumbs = $this->buildApplicationCrumbs();
if ($is_new) {
$title = pht('Create Dashboard');
$header = pht('Create Dashboard');
$button = pht('Create Dashboard');
$cancel_uri = $this->getApplicationURI();
- $crumbs->addTextCrumb('Create Dashboard');
+ $crumbs->addTextCrumb(pht('Create Dashboard'));
} else {
$id = $dashboard->getID();
$cancel_uri = $this->getApplicationURI('manage/'.$id.'/');
$title = pht('Edit Dashboard %d', $dashboard->getID());
$header = pht('Edit Dashboard "%s"', $dashboard->getName());
$button = pht('Save Changes');
$crumbs->addTextCrumb(pht('Dashboard %d', $id), $cancel_uri);
$crumbs->addTextCrumb(pht('Edit'));
}
$v_name = $dashboard->getName();
$v_layout_mode = $dashboard->getLayoutConfigObject()->getLayoutMode();
$e_name = true;
$validation_exception = null;
if ($request->isFormPost() && $request->getStr('edit')) {
$v_name = $request->getStr('name');
$v_layout_mode = $request->getStr('layout_mode');
$v_view_policy = $request->getStr('viewPolicy');
$v_edit_policy = $request->getStr('editPolicy');
$xactions = array();
$type_name = PhabricatorDashboardTransaction::TYPE_NAME;
$type_layout_mode = PhabricatorDashboardTransaction::TYPE_LAYOUT_MODE;
$type_view_policy = PhabricatorTransactions::TYPE_VIEW_POLICY;
$type_edit_policy = PhabricatorTransactions::TYPE_EDIT_POLICY;
$xactions[] = id(new PhabricatorDashboardTransaction())
->setTransactionType($type_name)
->setNewValue($v_name);
$xactions[] = id(new PhabricatorDashboardTransaction())
->setTransactionType($type_layout_mode)
->setNewValue($v_layout_mode);
$xactions[] = id(new PhabricatorDashboardTransaction())
->setTransactionType($type_view_policy)
->setNewValue($v_view_policy);
$xactions[] = id(new PhabricatorDashboardTransaction())
->setTransactionType($type_edit_policy)
->setNewValue($v_edit_policy);
try {
$editor = id(new PhabricatorDashboardTransactionEditor())
->setActor($viewer)
->setContinueOnNoEffect(true)
->setContentSourceFromRequest($request)
->applyTransactions($dashboard, $xactions);
$uri = $this->getApplicationURI('manage/'.$dashboard->getID().'/');
return id(new AphrontRedirectResponse())->setURI($uri);
} catch (PhabricatorApplicationTransactionValidationException $ex) {
$validation_exception = $ex;
$e_name = $validation_exception->getShortMessage($type_name);
$dashboard->setViewPolicy($v_view_policy);
$dashboard->setEditPolicy($v_edit_policy);
}
}
$policies = id(new PhabricatorPolicyQuery())
->setViewer($viewer)
->setObject($dashboard)
->execute();
$layout_mode_options =
PhabricatorDashboardLayoutConfig::getLayoutModeSelectOptions();
$form = id(new AphrontFormView())
->setUser($viewer)
->addHiddenInput('edit', true)
->appendChild(
id(new AphrontFormTextControl())
->setLabel(pht('Name'))
->setName('name')
->setValue($v_name)
->setError($e_name))
->appendChild(
id(new AphrontFormPolicyControl())
->setName('viewPolicy')
->setPolicyObject($dashboard)
->setCapability(PhabricatorPolicyCapability::CAN_VIEW)
->setPolicies($policies))
->appendChild(
id(new AphrontFormPolicyControl())
->setName('editPolicy')
->setPolicyObject($dashboard)
->setCapability(PhabricatorPolicyCapability::CAN_EDIT)
->setPolicies($policies))
->appendChild(
id(new AphrontFormSelectControl())
->setLabel(pht('Layout Mode'))
->setName('layout_mode')
->setValue($v_layout_mode)
->setOptions($layout_mode_options))
->appendChild(
id(new AphrontFormSubmitControl())
->setValue($button)
->addCancelButton($cancel_uri));
$box = id(new PHUIObjectBoxView())
->setHeaderText($header)
->setForm($form)
->setValidationException($validation_exception);
return $this->buildApplicationPage(
array(
$crumbs,
$box,
),
array(
'title' => $title,
));
}
private function processTemplateRequest(AphrontRequest $request) {
$viewer = $request->getUser();
$template_control = id(new AphrontFormRadioButtonControl())
->setName(pht('template'))
->setValue($request->getStr('template', 'empty'))
->addButton(
'empty',
pht('Empty'),
pht('Start with a blank canvas.'))
->addButton(
'simple',
pht('Simple Template'),
pht(
'Start with a simple dashboard with a welcome message, a feed of '.
'recent events, and a few starter panels.'));
$form = id(new AphrontFormView())
->setUser($viewer)
->appendRemarkupInstructions(
pht('Choose a dashboard template to start with.'))
->appendChild($template_control);
return $this->newDialog()
->setTitle(pht('Create Dashboard'))
->setWidth(AphrontDialogView::WIDTH_FORM)
->appendChild($form->buildLayoutView())
->addCancelButton('/dashboard/')
->addSubmitButton(pht('Continue'));
}
private function processBuildTemplateRequest(AphrontRequest $request) {
$viewer = $request->getUser();
$template = $request->getStr('template');
$bare_panel = PhabricatorDashboardPanel::initializeNewPanel($viewer);
$panel_phids = array();
switch ($template) {
case 'simple':
$v_name = pht('New Simple Dashboard');
$welcome_panel = $this->newPanel(
$request,
$viewer,
'text',
pht('Welcome'),
array(
'text' => pht(
"This is a simple template dashboard. You can edit this panel ".
"to change this text and replace it with a welcome message, or ".
"leave this placeholder text as-is to give your dashboard a ".
- "rustic, authentic feel.".
- "\n\n".
+ "rustic, authentic feel.\n\n".
"You can drag, remove, add, and edit panels to customize the ".
- "rest of this dashboard to show the information you want.".
- "\n\n".
+ "rest of this dashboard to show the information you want.\n\n".
"To install this dashboard on the home page, use the ".
"**Install Dashboard** action link above."),
));
$panel_phids[] = $welcome_panel->getPHID();
$feed_panel = $this->newPanel(
$request,
$viewer,
'query',
pht('Recent Activity'),
array(
'class' => 'PhabricatorFeedSearchEngine',
'key' => 'all',
));
$panel_phids[] = $feed_panel->getPHID();
$task_panel = $this->newPanel(
$request,
$viewer,
'query',
pht('Recent Tasks'),
array(
'class' => 'ManiphestTaskSearchEngine',
'key' => 'all',
));
$panel_phids[] = $task_panel->getPHID();
$commit_panel = $this->newPanel(
$request,
$viewer,
'query',
pht('Recent Commits'),
array(
'class' => 'PhabricatorCommitSearchEngine',
'key' => 'all',
));
$panel_phids[] = $commit_panel->getPHID();
$mode_2_and_1 = PhabricatorDashboardLayoutConfig::MODE_THIRDS_AND_THIRD;
$layout = id(new PhabricatorDashboardLayoutConfig())
->setLayoutMode($mode_2_and_1)
->setPanelLocation(0, $welcome_panel->getPHID())
->setPanelLocation(0, $task_panel->getPHID())
->setPanelLocation(0, $commit_panel->getPHID())
->setPanelLocation(1, $feed_panel->getPHID());
break;
default:
throw new Exception(pht('Unknown dashboard template %s!', $template));
}
// Create the dashboard.
$dashboard = PhabricatorDashboard::initializeNewDashboard($viewer)
->setLayoutConfigFromObject($layout);
$xactions = array();
$xactions[] = id(new PhabricatorDashboardTransaction())
->setTransactionType(PhabricatorDashboardTransaction::TYPE_NAME)
->setNewValue($v_name);
$xactions[] = id(new PhabricatorDashboardTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
->setMetadataValue(
'edge:type',
PhabricatorDashboardDashboardHasPanelEdgeType::EDGECONST)
->setNewValue(
array(
'+' => array_fuse($panel_phids),
));
$editor = id(new PhabricatorDashboardTransactionEditor())
->setActor($viewer)
->setContinueOnNoEffect(true)
->setContentSourceFromRequest($request)
->applyTransactions($dashboard, $xactions);
$manage_uri = $this->getApplicationURI('manage/'.$dashboard->getID().'/');
return id(new AphrontRedirectResponse())
->setURI($manage_uri);
}
private function newPanel(
AphrontRequest $request,
PhabricatorUser $viewer,
$type,
$name,
array $properties) {
$panel = PhabricatorDashboardPanel::initializeNewPanel($viewer)
->setPanelType($type)
->setProperties($properties);
$xactions = array();
$xactions[] = id(new PhabricatorDashboardPanelTransaction())
->setTransactionType(PhabricatorDashboardPanelTransaction::TYPE_NAME)
->setNewValue($name);
$editor = id(new PhabricatorDashboardPanelTransactionEditor())
->setActor($viewer)
->setContinueOnNoEffect(true)
->setContentSourceFromRequest($request)
->applyTransactions($panel, $xactions);
return $panel;
}
}
diff --git a/src/applications/dashboard/layoutconfig/PhabricatorDashboardLayoutConfig.php b/src/applications/dashboard/layoutconfig/PhabricatorDashboardLayoutConfig.php
index af4a947c8..9909515f0 100644
--- a/src/applications/dashboard/layoutconfig/PhabricatorDashboardLayoutConfig.php
+++ b/src/applications/dashboard/layoutconfig/PhabricatorDashboardLayoutConfig.php
@@ -1,164 +1,164 @@
<?php
final class PhabricatorDashboardLayoutConfig {
const MODE_FULL = 'layout-mode-full';
const MODE_HALF_AND_HALF = 'layout-mode-half-and-half';
const MODE_THIRD_AND_THIRDS = 'layout-mode-third-and-thirds';
const MODE_THIRDS_AND_THIRD = 'layout-mode-thirds-and-third';
private $layoutMode = self::MODE_FULL;
private $panelLocations = array();
public function setLayoutMode($mode) {
$this->layoutMode = $mode;
return $this;
}
public function getLayoutMode() {
return $this->layoutMode;
}
public function setPanelLocation($which_column, $panel_phid) {
$this->panelLocations[$which_column][] = $panel_phid;
return $this;
}
public function setPanelLocations(array $locations) {
$this->panelLocations = $locations;
return $this;
}
public function getPanelLocations() {
return $this->panelLocations;
}
public function replacePanel($old_phid, $new_phid) {
$locations = $this->getPanelLocations();
foreach ($locations as $column => $panel_phids) {
foreach ($panel_phids as $key => $panel_phid) {
if ($panel_phid == $old_phid) {
$locations[$column][$key] = $new_phid;
}
}
}
return $this->setPanelLocations($locations);
}
public function removePanel($panel_phid) {
$panel_location_grid = $this->getPanelLocations();
foreach ($panel_location_grid as $column => $panel_columns) {
$found_old_column = array_search($panel_phid, $panel_columns);
if ($found_old_column !== false) {
$new_panel_columns = $panel_columns;
array_splice(
$new_panel_columns,
$found_old_column,
1,
array());
$panel_location_grid[$column] = $new_panel_columns;
break;
}
}
$this->setPanelLocations($panel_location_grid);
}
public function getDefaultPanelLocations() {
switch ($this->getLayoutMode()) {
case self::MODE_HALF_AND_HALF:
case self::MODE_THIRD_AND_THIRDS:
case self::MODE_THIRDS_AND_THIRD:
$locations = array(array(), array());
break;
case self::MODE_FULL:
default:
$locations = array(array());
break;
}
return $locations;
}
public function getColumnClass($column_index, $grippable = false) {
switch ($this->getLayoutMode()) {
case self::MODE_HALF_AND_HALF:
$class = 'half';
break;
case self::MODE_THIRD_AND_THIRDS:
if ($column_index) {
$class = 'thirds';
} else {
$class = 'third';
}
break;
case self::MODE_THIRDS_AND_THIRD:
if ($column_index) {
$class = 'third';
} else {
$class = 'thirds';
}
break;
case self::MODE_FULL:
default:
$class = null;
break;
}
if ($grippable) {
$class .= ' grippable';
}
return $class;
}
public function isMultiColumnLayout() {
return $this->getLayoutMode() != self::MODE_FULL;
}
public function getColumnSelectOptions() {
$options = array();
switch ($this->getLayoutMode()) {
case self::MODE_HALF_AND_HALF:
case self::MODE_THIRD_AND_THIRDS:
case self::MODE_THIRDS_AND_THIRD:
return array(
0 => pht('Left'),
1 => pht('Right'),
);
break;
case self::MODE_FULL:
- throw new Exception('There is only one column in mode full.');
+ throw new Exception(pht('There is only one column in mode full.'));
break;
default:
- throw new Exception('Unknown layout mode!');
+ throw new Exception(pht('Unknown layout mode!'));
break;
}
return $options;
}
public static function getLayoutModeSelectOptions() {
return array(
self::MODE_FULL => pht('One full-width column'),
self::MODE_HALF_AND_HALF => pht('Two columns, 1/2 and 1/2'),
self::MODE_THIRD_AND_THIRDS => pht('Two columns, 1/3 and 2/3'),
self::MODE_THIRDS_AND_THIRD => pht('Two columns, 2/3 and 1/3'),
);
}
public static function newFromDictionary(array $dict) {
$layout_config = id(new PhabricatorDashboardLayoutConfig())
->setLayoutMode(idx($dict, 'layoutMode', self::MODE_FULL));
$layout_config->setPanelLocations(idx(
$dict,
'panelLocations',
$layout_config->getDefaultPanelLocations()));
return $layout_config;
}
public function toDictionary() {
return array(
'layoutMode' => $this->getLayoutMode(),
'panelLocations' => $this->getPanelLocations(),
);
}
}
diff --git a/src/applications/differential/__tests__/DifferentialParseRenderTestCase.php b/src/applications/differential/__tests__/DifferentialParseRenderTestCase.php
index 9805cc725..c278ff0c6 100644
--- a/src/applications/differential/__tests__/DifferentialParseRenderTestCase.php
+++ b/src/applications/differential/__tests__/DifferentialParseRenderTestCase.php
@@ -1,122 +1,121 @@
<?php
final class DifferentialParseRenderTestCase extends PhabricatorTestCase {
private function getTestDataDirectory() {
return dirname(__FILE__).'/data/';
}
public function testParseRender() {
$dir = $this->getTestDataDirectory();
foreach (Filesystem::listDirectory($dir, $show_hidden = false) as $file) {
if (!preg_match('/\.diff$/', $file)) {
continue;
}
$data = Filesystem::readFile($dir.$file);
$opt_file = $dir.$file.'.options';
if (Filesystem::pathExists($opt_file)) {
$options = Filesystem::readFile($opt_file);
try {
$options = phutil_json_decode($options);
} catch (PhutilJSONParserException $ex) {
throw new PhutilProxyException(
pht('Invalid options file: %s.', $opt_file),
$ex);
}
} else {
$options = array();
}
foreach (array('one', 'two') as $type) {
$this->runParser($type, $data, $file, 'expect');
$this->runParser($type, $data, $file, 'unshielded');
$this->runParser($type, $data, $file, 'whitespace');
}
}
}
private function runParser($type, $data, $file, $extension) {
$dir = $this->getTestDataDirectory();
$test_file = $dir.$file.'.'.$type.'.'.$extension;
if (!Filesystem::pathExists($test_file)) {
return;
}
$unshielded = false;
$whitespace = false;
switch ($extension) {
case 'unshielded':
$unshielded = true;
break;
case 'whitespace';
$unshielded = true;
$whitespace = true;
break;
}
$parsers = $this->buildChangesetParsers($type, $data, $file);
$actual = $this->renderParsers($parsers, $unshielded, $whitespace);
$expect = Filesystem::readFile($test_file);
$this->assertEqual($expect, $actual, basename($test_file));
}
private function renderParsers(array $parsers, $unshield, $whitespace) {
$result = array();
foreach ($parsers as $parser) {
if ($unshield) {
$s_range = 0;
$e_range = 0xFFFF;
} else {
$s_range = null;
$e_range = null;
}
if ($whitespace) {
$parser->setWhitespaceMode(
DifferentialChangesetParser::WHITESPACE_SHOW_ALL);
}
$result[] = $parser->render($s_range, $e_range, array());
}
return implode(str_repeat('~', 80)."\n", $result);
}
private function buildChangesetParsers($type, $data, $file) {
$parser = new ArcanistDiffParser();
$changes = $parser->parseDiff($data);
$diff = DifferentialDiff::newFromRawChanges(
PhabricatorUser::getOmnipotentUser(),
$changes);
$changesets = $diff->getChangesets();
$engine = new PhabricatorMarkupEngine();
$engine->setViewer(new PhabricatorUser());
$parsers = array();
foreach ($changesets as $changeset) {
$cparser = new DifferentialChangesetParser();
$cparser->setUser(new PhabricatorUser());
$cparser->setDisableCache(true);
$cparser->setChangeset($changeset);
$cparser->setMarkupEngine($engine);
if ($type == 'one') {
$cparser->setRenderer(new DifferentialChangesetOneUpTestRenderer());
} else if ($type == 'two') {
$cparser->setRenderer(new DifferentialChangesetTwoUpTestRenderer());
} else {
- throw new Exception(
- pht('Unknown renderer type "%s"!', $type));
+ throw new Exception(pht('Unknown renderer type "%s"!', $type));
}
$parsers[] = $cparser;
}
return $parsers;
}
}
diff --git a/src/applications/differential/application/PhabricatorDifferentialApplication.php b/src/applications/differential/application/PhabricatorDifferentialApplication.php
index 1aca32345..7bbe13342 100644
--- a/src/applications/differential/application/PhabricatorDifferentialApplication.php
+++ b/src/applications/differential/application/PhabricatorDifferentialApplication.php
@@ -1,214 +1,212 @@
<?php
final class PhabricatorDifferentialApplication extends PhabricatorApplication {
public function getBaseURI() {
return '/differential/';
}
public function getName() {
return pht('Differential');
}
public function getShortDescription() {
return pht('Review Code');
}
public function getFontIcon() {
return 'fa-cog';
}
public function isPinnedByDefault(PhabricatorUser $viewer) {
return true;
}
public function getHelpDocumentationArticles(PhabricatorUser $viewer) {
return array(
array(
'name' => pht('Differential User Guide'),
'href' => PhabricatorEnv::getDoclink('Differential User Guide'),
),
);
}
public function getFactObjectsForAnalysis() {
return array(
new DifferentialRevision(),
);
}
public function getTitleGlyph() {
return "\xE2\x9A\x99";
}
public function getEventListeners() {
return array(
new DifferentialActionMenuEventListener(),
new DifferentialHovercardEventListener(),
new DifferentialLandingActionMenuEventListener(),
);
}
public function getOverview() {
- return pht(<<<EOTEXT
-Differential is a **code review application** which allows engineers to review,
-discuss and approve changes to software.
-EOTEXT
-);
+ return pht(
+ 'Differential is a **code review application** which allows '.
+ 'engineers to review, discuss and approve changes to software.');
}
public function getRoutes() {
return array(
'/D(?P<id>[1-9]\d*)' => 'DifferentialRevisionViewController',
'/differential/' => array(
'(?:query/(?P<queryKey>[^/]+)/)?'
=> 'DifferentialRevisionListController',
'diff/' => array(
'(?P<id>[1-9]\d*)/' => 'DifferentialDiffViewController',
'create/' => 'DifferentialDiffCreateController',
),
'changeset/' => 'DifferentialChangesetViewController',
'revision/' => array(
'edit/(?:(?P<id>[1-9]\d*)/)?'
=> 'DifferentialRevisionEditController',
'land/(?:(?P<id>[1-9]\d*))/(?P<strategy>[^/]+)/'
=> 'DifferentialRevisionLandController',
'closedetails/(?P<phid>[^/]+)/'
=> 'DifferentialRevisionCloseDetailsController',
'update/(?P<revisionID>[1-9]\d*)/'
=> 'DifferentialDiffCreateController',
),
'comment/' => array(
'preview/(?P<id>[1-9]\d*)/' => 'DifferentialCommentPreviewController',
'save/(?P<id>[1-9]\d*)/' => 'DifferentialCommentSaveController',
'inline/' => array(
'preview/(?P<id>[1-9]\d*)/'
=> 'DifferentialInlineCommentPreviewController',
'edit/(?P<id>[1-9]\d*)/'
=> 'DifferentialInlineCommentEditController',
),
),
'preview/' => 'PhabricatorMarkupPreviewController',
),
);
}
public function getApplicationOrder() {
return 0.100;
}
public function getRemarkupRules() {
return array(
new DifferentialRemarkupRule(),
);
}
public function loadStatus(PhabricatorUser $user) {
$revisions = id(new DifferentialRevisionQuery())
->setViewer($user)
->withResponsibleUsers(array($user->getPHID()))
->withStatus(DifferentialRevisionQuery::STATUS_OPEN)
->needRelationships(true)
->setLimit(self::MAX_STATUS_ITEMS)
->execute();
$status = array();
if (count($revisions) == self::MAX_STATUS_ITEMS) {
$all_count = count($revisions);
$all_count_str = self::formatStatusCount(
$all_count,
'%s Active Reviews',
'%d Active Review(s)');
$type = PhabricatorApplicationStatusView::TYPE_WARNING;
$status[] = id(new PhabricatorApplicationStatusView())
->setType($type)
->setText($all_count_str)
->setCount($all_count);
} else {
list($blocking, $active, $waiting) =
DifferentialRevisionQuery::splitResponsible(
$revisions,
array($user->getPHID()));
$blocking = count($blocking);
$blocking_str = self::formatStatusCount(
$blocking,
'%s Reviews Blocking Others',
'%d Review(s) Blocking Others');
$type = PhabricatorApplicationStatusView::TYPE_NEEDS_ATTENTION;
$status[] = id(new PhabricatorApplicationStatusView())
->setType($type)
->setText($blocking_str)
->setCount($blocking);
$active = count($active);
$active_str = self::formatStatusCount(
$active,
'%s Reviews Need Attention',
'%d Review(s) Need Attention');
$type = PhabricatorApplicationStatusView::TYPE_WARNING;
$status[] = id(new PhabricatorApplicationStatusView())
->setType($type)
->setText($active_str)
->setCount($active);
$waiting = count($waiting);
$waiting_str = self::formatStatusCount(
$waiting,
'%s Reviews Waiting on Others',
'%d Review(s) Waiting on Others');
$type = PhabricatorApplicationStatusView::TYPE_INFO;
$status[] = id(new PhabricatorApplicationStatusView())
->setType($type)
->setText($waiting_str)
->setCount($waiting);
}
return $status;
}
public function supportsEmailIntegration() {
return true;
}
public function getAppEmailBlurb() {
return pht(
'Send email to these addresses to create revisions. The body of the '.
'message and / or one or more attachments should be the output of a '.
'"diff" command. %s',
phutil_tag(
'a',
array(
'href' => $this->getInboundEmailSupportLink(),
),
pht('Learn More')));
}
protected function getCustomCapabilities() {
return array(
DifferentialDefaultViewCapability::CAPABILITY => array(
'caption' => pht('Default view policy for newly created revisions.'),
),
);
}
public function getMailCommandObjects() {
return array(
'revision' => array(
'name' => pht('Email Commands: Revisions'),
'header' => pht('Interacting with Differential Revisions'),
'object' => new DifferentialRevision(),
'summary' => pht(
'This page documents the commands you can use to interact with '.
'revisions in Differential.'),
),
);
}
public function getApplicationSearchDocumentTypes() {
return array(
DifferentialRevisionPHIDType::TYPECONST,
);
}
}
diff --git a/src/applications/differential/conduit/DifferentialCloseConduitAPIMethod.php b/src/applications/differential/conduit/DifferentialCloseConduitAPIMethod.php
index 58fed6cf6..bd097c4b2 100644
--- a/src/applications/differential/conduit/DifferentialCloseConduitAPIMethod.php
+++ b/src/applications/differential/conduit/DifferentialCloseConduitAPIMethod.php
@@ -1,63 +1,63 @@
<?php
final class DifferentialCloseConduitAPIMethod
extends DifferentialConduitAPIMethod {
public function getAPIMethodName() {
return 'differential.close';
}
public function getMethodDescription() {
return pht('Close a Differential revision.');
}
protected function defineParamTypes() {
return array(
'revisionID' => 'required int',
);
}
protected function defineReturnType() {
return 'void';
}
protected function defineErrorTypes() {
return array(
- 'ERR_NOT_FOUND' => 'Revision was not found.',
+ 'ERR_NOT_FOUND' => pht('Revision was not found.'),
);
}
protected function execute(ConduitAPIRequest $request) {
$viewer = $request->getUser();
$id = $request->getValue('revisionID');
$revision = id(new DifferentialRevisionQuery())
->withIDs(array($id))
->setViewer($viewer)
->needReviewerStatus(true)
->executeOne();
if (!$revision) {
throw new ConduitException('ERR_NOT_FOUND');
}
$xactions = array();
$xactions[] = id(new DifferentialTransaction())
->setTransactionType(DifferentialTransaction::TYPE_ACTION)
->setNewValue(DifferentialAction::ACTION_CLOSE);
$content_source = PhabricatorContentSource::newForSource(
PhabricatorContentSource::SOURCE_CONDUIT,
array());
$editor = id(new DifferentialTransactionEditor())
->setActor($viewer)
->setContentSourceFromConduitRequest($request)
->setContinueOnMissingFields(true)
->setContinueOnNoEffect(true);
$editor->applyTransactions($revision, $xactions);
return;
}
}
diff --git a/src/applications/differential/conduit/DifferentialCreateCommentConduitAPIMethod.php b/src/applications/differential/conduit/DifferentialCreateCommentConduitAPIMethod.php
index fe1d57857..c5d2bdb9d 100644
--- a/src/applications/differential/conduit/DifferentialCreateCommentConduitAPIMethod.php
+++ b/src/applications/differential/conduit/DifferentialCreateCommentConduitAPIMethod.php
@@ -1,92 +1,92 @@
<?php
final class DifferentialCreateCommentConduitAPIMethod
extends DifferentialConduitAPIMethod {
public function getAPIMethodName() {
return 'differential.createcomment';
}
public function getMethodDescription() {
return pht('Add a comment to a Differential revision.');
}
protected function defineParamTypes() {
return array(
'revision_id' => 'required revisionid',
'message' => 'optional string',
'action' => 'optional string',
'silent' => 'optional bool',
'attach_inlines' => 'optional bool',
);
}
protected function defineReturnType() {
return 'nonempty dict';
}
protected function defineErrorTypes() {
return array(
- 'ERR_BAD_REVISION' => 'Bad revision ID.',
+ 'ERR_BAD_REVISION' => pht('Bad revision ID.'),
);
}
protected function execute(ConduitAPIRequest $request) {
$viewer = $request->getUser();
$revision = id(new DifferentialRevisionQuery())
->setViewer($viewer)
->withIDs(array($request->getValue('revision_id')))
->needReviewerStatus(true)
->needReviewerAuthority(true)
->executeOne();
if (!$revision) {
throw new ConduitException('ERR_BAD_REVISION');
}
$xactions = array();
$action = $request->getValue('action');
if ($action && ($action != 'comment') && ($action != 'none')) {
$xactions[] = id(new DifferentialTransaction())
->setTransactionType(DifferentialTransaction::TYPE_ACTION)
->setNewValue($action);
}
$content = $request->getValue('message');
if (strlen($content)) {
$xactions[] = id(new DifferentialTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_COMMENT)
->attachComment(
id(new DifferentialTransactionComment())
->setContent($content));
}
if ($request->getValue('attach_inlines')) {
$type_inline = DifferentialTransaction::TYPE_INLINE;
$inlines = DifferentialTransactionQuery::loadUnsubmittedInlineComments(
$viewer,
$revision);
foreach ($inlines as $inline) {
$xactions[] = id(new DifferentialTransaction())
->setTransactionType($type_inline)
->attachComment($inline);
}
}
$editor = id(new DifferentialTransactionEditor())
->setActor($viewer)
->setDisableEmail($request->getValue('silent'))
->setContentSourceFromConduitRequest($request)
->setContinueOnNoEffect(true)
->setContinueOnMissingFields(true);
$editor->applyTransactions($revision, $xactions);
return array(
'revisionid' => $revision->getID(),
'uri' => PhabricatorEnv::getURI('/D'.$revision->getID()),
);
}
}
diff --git a/src/applications/differential/conduit/DifferentialCreateDiffConduitAPIMethod.php b/src/applications/differential/conduit/DifferentialCreateDiffConduitAPIMethod.php
index 8839eee6d..91b5ca865 100644
--- a/src/applications/differential/conduit/DifferentialCreateDiffConduitAPIMethod.php
+++ b/src/applications/differential/conduit/DifferentialCreateDiffConduitAPIMethod.php
@@ -1,167 +1,167 @@
<?php
final class DifferentialCreateDiffConduitAPIMethod
extends DifferentialConduitAPIMethod {
public function getAPIMethodName() {
return 'differential.creatediff';
}
public function getMethodDescription() {
- return 'Create a new Differential diff.';
+ return pht('Create a new Differential diff.');
}
protected function defineParamTypes() {
$vcs_const = $this->formatStringConstants(
array(
'svn',
'git',
'hg',
));
$status_const = $this->formatStringConstants(
array(
'none',
'skip',
'okay',
'warn',
'fail',
'postponed',
));
return array(
'changes' => 'required list<dict>',
'sourceMachine' => 'required string',
'sourcePath' => 'required string',
'branch' => 'required string',
'bookmark' => 'optional string',
'sourceControlSystem' => 'required '.$vcs_const,
'sourceControlPath' => 'required string',
'sourceControlBaseRevision' => 'required string',
'creationMethod' => 'optional string',
'arcanistProject' => 'deprecated',
'lintStatus' => 'required '.$status_const,
'unitStatus' => 'required '.$status_const,
'repositoryPHID' => 'optional phid',
'parentRevisionID' => 'deprecated',
'authorPHID' => 'deprecated',
'repositoryUUID' => 'deprecated',
);
}
protected function defineReturnType() {
return 'nonempty dict';
}
protected function execute(ConduitAPIRequest $request) {
$viewer = $request->getUser();
$change_data = $request->getValue('changes');
$changes = array();
foreach ($change_data as $dict) {
$changes[] = ArcanistDiffChange::newFromDictionary($dict);
}
$diff = DifferentialDiff::newFromRawChanges($viewer, $changes);
// TODO: Remove repository UUID eventually; for now continue writing
// the UUID. Note that we'll overwrite it below if we identify a
// repository, and `arc` no longer sends it. This stuff is retained for
// backward compatibility.
$repository_uuid = $request->getValue('repositoryUUID');
$repository_phid = $request->getValue('repositoryPHID');
if ($repository_phid) {
$repository = id(new PhabricatorRepositoryQuery())
->setViewer($viewer)
->withPHIDs(array($repository_phid))
->executeOne();
if ($repository) {
$repository_phid = $repository->getPHID();
$repository_uuid = $repository->getUUID();
}
}
switch ($request->getValue('lintStatus')) {
case 'skip':
$lint_status = DifferentialLintStatus::LINT_SKIP;
break;
case 'okay':
$lint_status = DifferentialLintStatus::LINT_OKAY;
break;
case 'warn':
$lint_status = DifferentialLintStatus::LINT_WARN;
break;
case 'fail':
$lint_status = DifferentialLintStatus::LINT_FAIL;
break;
case 'postponed':
$lint_status = DifferentialLintStatus::LINT_POSTPONED;
break;
case 'none':
default:
$lint_status = DifferentialLintStatus::LINT_NONE;
break;
}
switch ($request->getValue('unitStatus')) {
case 'skip':
$unit_status = DifferentialUnitStatus::UNIT_SKIP;
break;
case 'okay':
$unit_status = DifferentialUnitStatus::UNIT_OKAY;
break;
case 'warn':
$unit_status = DifferentialUnitStatus::UNIT_WARN;
break;
case 'fail':
$unit_status = DifferentialUnitStatus::UNIT_FAIL;
break;
case 'postponed':
$unit_status = DifferentialUnitStatus::UNIT_POSTPONED;
break;
case 'none':
default:
$unit_status = DifferentialUnitStatus::UNIT_NONE;
break;
}
$diff_data_dict = array(
'sourcePath' => $request->getValue('sourcePath'),
'sourceMachine' => $request->getValue('sourceMachine'),
'branch' => $request->getValue('branch'),
'creationMethod' => $request->getValue('creationMethod'),
'authorPHID' => $viewer->getPHID(),
'bookmark' => $request->getValue('bookmark'),
'repositoryUUID' => $repository_uuid,
'repositoryPHID' => $repository_phid,
'sourceControlSystem' => $request->getValue('sourceControlSystem'),
'sourceControlPath' => $request->getValue('sourceControlPath'),
'sourceControlBaseRevision' =>
$request->getValue('sourceControlBaseRevision'),
'lintStatus' => $lint_status,
'unitStatus' => $unit_status,
);
$xactions = array(id(new DifferentialTransaction())
->setTransactionType(DifferentialDiffTransaction::TYPE_DIFF_CREATE)
->setNewValue($diff_data_dict),
);
id(new DifferentialDiffEditor())
->setActor($viewer)
->setContentSourceFromConduitRequest($request)
->setContinueOnNoEffect(true)
->applyTransactions($diff, $xactions);
$path = '/differential/diff/'.$diff->getID().'/';
$uri = PhabricatorEnv::getURI($path);
return array(
'diffid' => $diff->getID(),
'uri' => $uri,
);
}
}
diff --git a/src/applications/differential/conduit/DifferentialCreateInlineConduitAPIMethod.php b/src/applications/differential/conduit/DifferentialCreateInlineConduitAPIMethod.php
index 98a567b0b..644738555 100644
--- a/src/applications/differential/conduit/DifferentialCreateInlineConduitAPIMethod.php
+++ b/src/applications/differential/conduit/DifferentialCreateInlineConduitAPIMethod.php
@@ -1,109 +1,114 @@
<?php
final class DifferentialCreateInlineConduitAPIMethod
extends DifferentialConduitAPIMethod {
public function getAPIMethodName() {
return 'differential.createinline';
}
public function getMethodDescription() {
- return 'Add an inline comment to a Differential revision.';
+ return pht('Add an inline comment to a Differential revision.');
}
protected function defineParamTypes() {
return array(
'revisionID' => 'optional revisionid',
'diffID' => 'optional diffid',
'filePath' => 'required string',
'isNewFile' => 'required bool',
'lineNumber' => 'required int',
'lineLength' => 'optional int',
'content' => 'required string',
);
}
protected function defineReturnType() {
return 'nonempty dict';
}
protected function defineErrorTypes() {
return array(
- 'ERR-BAD-REVISION' => 'Bad revision ID.',
- 'ERR-BAD-DIFF' => 'Bad diff ID, or diff does not belong to revision.',
- 'ERR-NEED-DIFF' => 'Neither revision ID nor diff ID was provided.',
- 'ERR-NEED-FILE' => 'A file path was not provided.',
- 'ERR-BAD-FILE' => "Requested file doesn't exist in this revision.",
+ 'ERR-BAD-REVISION' => pht(
+ 'Bad revision ID.'),
+ 'ERR-BAD-DIFF' => pht(
+ 'Bad diff ID, or diff does not belong to revision.'),
+ 'ERR-NEED-DIFF' => pht(
+ 'Neither revision ID nor diff ID was provided.'),
+ 'ERR-NEED-FILE' => pht(
+ 'A file path was not provided.'),
+ 'ERR-BAD-FILE' => pht(
+ "Requested file doesn't exist in this revision."),
);
}
protected function execute(ConduitAPIRequest $request) {
$rid = $request->getValue('revisionID');
$did = $request->getValue('diffID');
if ($rid) {
// Given both a revision and a diff, check that they match.
// Given only a revision, find the active diff.
$revision = id(new DifferentialRevisionQuery())
->setViewer($request->getUser())
->withIDs(array($rid))
->executeOne();
if (!$revision) {
throw new ConduitException('ERR-BAD-REVISION');
}
if (!$did) { // did not!
$diff = $revision->loadActiveDiff();
$did = $diff->getID();
} else { // did too!
$diff = id(new DifferentialDiff())->load($did);
if (!$diff || $diff->getRevisionID() != $rid) {
throw new ConduitException('ERR-BAD-DIFF');
}
}
} else if ($did) {
// Given only a diff, find the parent revision.
$diff = id(new DifferentialDiff())->load($did);
if (!$diff) {
throw new ConduitException('ERR-BAD-DIFF');
}
$rid = $diff->getRevisionID();
} else {
// Given neither, bail.
throw new ConduitException('ERR-NEED-DIFF');
}
$file = $request->getValue('filePath');
if (!$file) {
throw new ConduitException('ERR-NEED-FILE');
}
$changes = id(new DifferentialChangeset())->loadAllWhere(
'diffID = %d',
$did);
$cid = null;
foreach ($changes as $id => $change) {
if ($file == $change->getFilename()) {
$cid = $id;
}
}
if ($cid == null) {
throw new ConduitException('ERR-BAD-FILE');
}
$inline = id(new DifferentialInlineComment())
->setRevisionID($rid)
->setChangesetID($cid)
->setAuthorPHID($request->getUser()->getPHID())
->setContent($request->getValue('content'))
->setIsNewFile($request->getValue('isNewFile'))
->setLineNumber($request->getValue('lineNumber'))
->setLineLength($request->getValue('lineLength', 0))
->save();
// Load everything again, just to be safe.
$changeset = id(new DifferentialChangeset())
->load($inline->getChangesetID());
return $this->buildInlineInfoDictionary($inline, $changeset);
}
}
diff --git a/src/applications/differential/conduit/DifferentialCreateRevisionConduitAPIMethod.php b/src/applications/differential/conduit/DifferentialCreateRevisionConduitAPIMethod.php
index de0c3e02d..95b4309c7 100644
--- a/src/applications/differential/conduit/DifferentialCreateRevisionConduitAPIMethod.php
+++ b/src/applications/differential/conduit/DifferentialCreateRevisionConduitAPIMethod.php
@@ -1,61 +1,61 @@
<?php
final class DifferentialCreateRevisionConduitAPIMethod
extends DifferentialConduitAPIMethod {
public function getAPIMethodName() {
return 'differential.createrevision';
}
public function getMethodDescription() {
return pht('Create a new Differential revision.');
}
protected function defineParamTypes() {
return array(
// TODO: Arcanist passes this; prevent fatals after D4191 until Conduit
// version 7 or newer.
'user' => 'ignored',
'diffid' => 'required diffid',
'fields' => 'required dict',
);
}
protected function defineReturnType() {
return 'nonempty dict';
}
protected function defineErrorTypes() {
return array(
- 'ERR_BAD_DIFF' => 'Bad diff ID.',
+ 'ERR_BAD_DIFF' => pht('Bad diff ID.'),
);
}
protected function execute(ConduitAPIRequest $request) {
$viewer = $request->getUser();
$diff = id(new DifferentialDiffQuery())
->setViewer($viewer)
->withIDs(array($request->getValue('diffid')))
->executeOne();
if (!$diff) {
throw new ConduitException('ERR_BAD_DIFF');
}
$revision = DifferentialRevision::initializeNewRevision($viewer);
$revision->attachReviewerStatus(array());
$this->applyFieldEdit(
$request,
$revision,
$diff,
$request->getValue('fields', array()),
$message = null);
return array(
'revisionid' => $revision->getID(),
'uri' => PhabricatorEnv::getURI('/D'.$revision->getID()),
);
}
}
diff --git a/src/applications/differential/conduit/DifferentialFindConduitAPIMethod.php b/src/applications/differential/conduit/DifferentialFindConduitAPIMethod.php
index 56cb0f08d..92dce067f 100644
--- a/src/applications/differential/conduit/DifferentialFindConduitAPIMethod.php
+++ b/src/applications/differential/conduit/DifferentialFindConduitAPIMethod.php
@@ -1,101 +1,101 @@
<?php
final class DifferentialFindConduitAPIMethod
extends DifferentialConduitAPIMethod {
public function getAPIMethodName() {
return 'differential.find';
}
public function getMethodStatus() {
return self::METHOD_STATUS_DEPRECATED;
}
public function getMethodStatusDescription() {
- return "Replaced by 'differential.query'.";
+ return pht("Replaced by '%s'.", 'differential.query');
}
public function getMethodDescription() {
- return 'Query Differential revisions which match certain criteria.';
+ return pht('Query Differential revisions which match certain criteria.');
}
protected function defineParamTypes() {
$types = array(
'open',
'committable',
'revision-ids',
'phids',
);
return array(
'query' => 'required '.$this->formatStringConstants($types),
'guids' => 'required nonempty list<guids>',
);
}
protected function defineReturnType() {
return 'nonempty list<dict>';
}
protected function execute(ConduitAPIRequest $request) {
$type = $request->getValue('query');
$guids = $request->getValue('guids');
$results = array();
if (!$guids) {
return $results;
}
$query = id(new DifferentialRevisionQuery())
->setViewer($request->getUser());
switch ($type) {
case 'open':
$query
->withStatus(DifferentialRevisionQuery::STATUS_OPEN)
->withAuthors($guids);
break;
case 'committable':
$query
->withStatus(DifferentialRevisionQuery::STATUS_ACCEPTED)
->withAuthors($guids);
break;
case 'revision-ids':
$query
->withIDs($guids);
break;
case 'owned':
$query->withAuthors($guids);
break;
case 'phids':
$query
->withPHIDs($guids);
break;
}
$revisions = $query->execute();
foreach ($revisions as $revision) {
$diff = $revision->loadActiveDiff();
if (!$diff) {
continue;
}
$id = $revision->getID();
$results[] = array(
'id' => $id,
'phid' => $revision->getPHID(),
'name' => $revision->getTitle(),
'uri' => PhabricatorEnv::getProductionURI('/D'.$id),
'dateCreated' => $revision->getDateCreated(),
'authorPHID' => $revision->getAuthorPHID(),
'statusName' =>
ArcanistDifferentialRevisionStatus::getNameForRevisionStatus(
$revision->getStatus()),
'sourcePath' => $diff->getSourcePath(),
);
}
return $results;
}
}
diff --git a/src/applications/differential/conduit/DifferentialFinishPostponedLintersConduitAPIMethod.php b/src/applications/differential/conduit/DifferentialFinishPostponedLintersConduitAPIMethod.php
index 093f06e11..f53340f06 100644
--- a/src/applications/differential/conduit/DifferentialFinishPostponedLintersConduitAPIMethod.php
+++ b/src/applications/differential/conduit/DifferentialFinishPostponedLintersConduitAPIMethod.php
@@ -1,118 +1,118 @@
<?php
final class DifferentialFinishPostponedLintersConduitAPIMethod
extends DifferentialConduitAPIMethod {
public function getAPIMethodName() {
return 'differential.finishpostponedlinters';
}
public function getMethodDescription() {
- return 'Update diff with new lint messages and mark postponed '.
- 'linters as finished.';
+ return pht(
+ 'Update diff with new lint messages and mark postponed '.
+ 'linters as finished.');
}
protected function defineParamTypes() {
return array(
'diffID' => 'required diffID',
'linters' => 'required dict',
);
}
protected function defineReturnType() {
return 'void';
}
protected function defineErrorTypes() {
return array(
- 'ERR-BAD-DIFF' => 'Bad diff ID.',
- 'ERR-BAD-LINTER' => 'No postponed linter by the given name',
- 'ERR-NO-LINT' => 'No postponed lint field available in diff',
+ 'ERR-BAD-DIFF' => pht('Bad diff ID.'),
+ 'ERR-BAD-LINTER' => pht('No postponed linter by the given name.'),
+ 'ERR-NO-LINT' => pht('No postponed lint field available in diff.'),
);
}
protected function execute(ConduitAPIRequest $request) {
-
$diff_id = $request->getValue('diffID');
$linter_map = $request->getValue('linters');
$diff = id(new DifferentialDiffQuery())
->setViewer($request->getUser())
->withIDs(array($diff_id))
->executeOne();
if (!$diff) {
throw new ConduitException('ERR-BAD-DIFF');
}
// Extract the finished linters and messages from the linter map.
$finished_linters = array_keys($linter_map);
$new_messages = array();
foreach ($linter_map as $linter => $messages) {
$new_messages = array_merge($new_messages, $messages);
}
// Load the postponed linters attached to this diff.
$postponed_linters_property = id(
new DifferentialDiffProperty())->loadOneWhere(
'diffID = %d AND name = %s',
$diff_id,
'arc:lint-postponed');
if ($postponed_linters_property) {
$postponed_linters = $postponed_linters_property->getData();
} else {
$postponed_linters = array();
}
foreach ($finished_linters as $linter) {
if (!in_array($linter, $postponed_linters)) {
throw new ConduitException('ERR-BAD-LINTER');
}
}
foreach ($postponed_linters as $idx => $linter) {
if (in_array($linter, $finished_linters)) {
unset($postponed_linters[$idx]);
}
}
// Load the lint messages currenty attached to the diff. If this
// diff property doesn't exist, create it.
$messages_property = id(new DifferentialDiffProperty())->loadOneWhere(
'diffID = %d AND name = %s',
$diff_id,
'arc:lint');
if ($messages_property) {
$messages = $messages_property->getData();
} else {
$messages = array();
}
// Add new lint messages, removing duplicates.
foreach ($new_messages as $new_message) {
if (!in_array($new_message, $messages)) {
$messages[] = $new_message;
}
}
// Use setdiffproperty to update the postponed linters and messages,
// as these will also update the lint status correctly.
$call = new ConduitCall(
'differential.setdiffproperty',
array(
'diff_id' => $diff_id,
'name' => 'arc:lint',
'data' => json_encode($messages),
));
$call->setUser($request->getUser());
$call->execute();
$call = new ConduitCall(
'differential.setdiffproperty',
array(
'diff_id' => $diff_id,
'name' => 'arc:lint-postponed',
'data' => json_encode($postponed_linters),
));
$call->setUser($request->getUser());
$call->execute();
}
}
diff --git a/src/applications/differential/conduit/DifferentialGetAllDiffsConduitAPIMethod.php b/src/applications/differential/conduit/DifferentialGetAllDiffsConduitAPIMethod.php
index c4e2d3521..187e1aaaf 100644
--- a/src/applications/differential/conduit/DifferentialGetAllDiffsConduitAPIMethod.php
+++ b/src/applications/differential/conduit/DifferentialGetAllDiffsConduitAPIMethod.php
@@ -1,56 +1,57 @@
<?php
final class DifferentialGetAllDiffsConduitAPIMethod
extends DifferentialConduitAPIMethod {
public function getAPIMethodName() {
return 'differential.getalldiffs';
}
public function getMethodStatus() {
return self::METHOD_STATUS_DEPRECATED;
}
public function getMethodStatusDescription() {
return pht(
- 'This method has been deprecated in favor of differential.querydiffs.');
+ 'This method has been deprecated in favor of %s.',
+ 'differential.querydiffs');
}
public function getMethodDescription() {
- return 'Load all diffs for given revisions from Differential.';
+ return pht('Load all diffs for given revisions from Differential.');
}
protected function defineParamTypes() {
return array(
'revision_ids' => 'required list<int>',
);
}
protected function defineReturnType() {
return 'dict';
}
protected function execute(ConduitAPIRequest $request) {
$results = array();
$revision_ids = $request->getValue('revision_ids');
if (!$revision_ids) {
return $results;
}
$diffs = id(new DifferentialDiffQuery())
->setViewer($request->getUser())
->withRevisionIDs($revision_ids)
->execute();
foreach ($diffs as $diff) {
$results[] = array(
'revision_id' => $diff->getRevisionID(),
'diff_id' => $diff->getID(),
);
}
return $results;
}
}
diff --git a/src/applications/differential/conduit/DifferentialGetCommitMessageConduitAPIMethod.php b/src/applications/differential/conduit/DifferentialGetCommitMessageConduitAPIMethod.php
index 8d7e71c03..288a8c4ca 100644
--- a/src/applications/differential/conduit/DifferentialGetCommitMessageConduitAPIMethod.php
+++ b/src/applications/differential/conduit/DifferentialGetCommitMessageConduitAPIMethod.php
@@ -1,198 +1,198 @@
<?php
final class DifferentialGetCommitMessageConduitAPIMethod
extends DifferentialConduitAPIMethod {
public function getAPIMethodName() {
return 'differential.getcommitmessage';
}
public function getMethodDescription() {
- return 'Retrieve Differential commit messages or message templates.';
+ return pht('Retrieve Differential commit messages or message templates.');
}
protected function defineParamTypes() {
$edit_types = array('edit', 'create');
return array(
'revision_id' => 'optional revision_id',
'fields' => 'optional dict<string, wild>',
'edit' => 'optional '.$this->formatStringConstants($edit_types),
);
}
protected function defineReturnType() {
return 'nonempty string';
}
protected function defineErrorTypes() {
return array(
- 'ERR_NOT_FOUND' => 'Revision was not found.',
+ 'ERR_NOT_FOUND' => pht('Revision was not found.'),
);
}
protected function execute(ConduitAPIRequest $request) {
$id = $request->getValue('revision_id');
$viewer = $request->getUser();
if ($id) {
$revision = id(new DifferentialRevisionQuery())
->withIDs(array($id))
->setViewer($viewer)
->needReviewerStatus(true)
->needActiveDiffs(true)
->executeOne();
if (!$revision) {
throw new ConduitException('ERR_NOT_FOUND');
}
} else {
$revision = DifferentialRevision::initializeNewRevision($viewer);
$revision->attachReviewerStatus(array());
$revision->attachActiveDiff(null);
}
$is_edit = $request->getValue('edit');
$is_create = ($is_edit == 'create');
$field_list = PhabricatorCustomField::getObjectFields(
$revision,
($is_edit
? DifferentialCustomField::ROLE_COMMITMESSAGEEDIT
: DifferentialCustomField::ROLE_COMMITMESSAGE));
$field_list
->setViewer($viewer)
->readFieldsFromStorage($revision);
$field_map = mpull($field_list->getFields(), null, 'getFieldKeyForConduit');
if ($is_edit) {
$fields = $request->getValue('fields', array());
foreach ($fields as $field => $value) {
$custom_field = idx($field_map, $field);
if (!$custom_field) {
// Just ignore this, these workflows don't make strong distictions
// about field editability on the client side.
continue;
}
if ($is_create ||
$custom_field->shouldOverwriteWhenCommitMessageIsEdited()) {
$custom_field->readValueFromCommitMessage($value);
}
}
}
$phids = array();
foreach ($field_list->getFields() as $key => $field) {
$field_phids = $field->getRequiredHandlePHIDsForCommitMessage();
if (!is_array($field_phids)) {
throw new Exception(
pht(
'Custom field "%s" was expected to return an array of handle '.
'PHIDs required for commit message rendering, but returned "%s" '.
'instead.',
$field->getFieldKey(),
gettype($field_phids)));
}
$phids[$key] = $field_phids;
}
$all_phids = array_mergev($phids);
if ($all_phids) {
$all_handles = id(new PhabricatorHandleQuery())
->setViewer($viewer)
->withPHIDs($all_phids)
->execute();
} else {
$all_handles = array();
}
$key_title = id(new DifferentialTitleField())->getFieldKey();
$default_title = DifferentialTitleField::getDefaultTitle();
$commit_message = array();
foreach ($field_list->getFields() as $key => $field) {
$handles = array_select_keys($all_handles, $phids[$key]);
$label = $field->renderCommitMessageLabel();
$value = $field->renderCommitMessageValue($handles);
if (!is_string($value) && !is_null($value)) {
throw new Exception(
pht(
'Custom field "%s" was expected to render a string or null value, '.
'but rendered a "%s" instead.',
$field->getFieldKey(),
gettype($value)));
}
$is_title = ($key == $key_title);
if (!strlen($value)) {
if ($is_title) {
$commit_message[] = $default_title;
} else {
if ($is_edit && $field->shouldAppearInCommitMessageTemplate()) {
$commit_message[] = $label.': ';
}
}
} else {
if ($is_title) {
$commit_message[] = $value;
} else {
$value = str_replace(
array("\r\n", "\r"),
array("\n", "\n"),
$value);
if (strpos($value, "\n") !== false || substr($value, 0, 2) === ' ') {
$commit_message[] = "{$label}:\n{$value}";
} else {
$commit_message[] = "{$label}: {$value}";
}
}
}
}
if ($is_edit) {
$tip = $this->getProTip($field_list);
if ($tip !== null) {
$commit_message[] = "\n".$tip;
}
}
$commit_message = implode("\n\n", $commit_message);
return $commit_message;
}
private function getProTip() {
// Any field can provide tips, whether it normally appears on commit
// messages or not.
$field_list = PhabricatorCustomField::getObjectFields(
new DifferentialRevision(),
PhabricatorCustomField::ROLE_DEFAULT);
$tips = array();
foreach ($field_list->getFields() as $key => $field) {
$tips[] = $field->getProTips();
}
$tips = array_mergev($tips);
if (!$tips) {
return null;
}
shuffle($tips);
$tip = pht('Tip: %s', head($tips));
$tip = wordwrap($tip, 78, "\n", true);
$lines = explode("\n", $tip);
foreach ($lines as $key => $line) {
$lines[$key] = '# '.$line;
}
return implode("\n", $lines);
}
}
diff --git a/src/applications/differential/conduit/DifferentialGetCommitPathsConduitAPIMethod.php b/src/applications/differential/conduit/DifferentialGetCommitPathsConduitAPIMethod.php
index a025cd498..bdd33b60f 100644
--- a/src/applications/differential/conduit/DifferentialGetCommitPathsConduitAPIMethod.php
+++ b/src/applications/differential/conduit/DifferentialGetCommitPathsConduitAPIMethod.php
@@ -1,56 +1,57 @@
<?php
final class DifferentialGetCommitPathsConduitAPIMethod
extends DifferentialConduitAPIMethod {
public function getAPIMethodName() {
return 'differential.getcommitpaths';
}
public function getMethodDescription() {
- return 'Query which paths should be included when committing a '.
- 'Differential revision.';
+ return pht(
+ 'Query which paths should be included when committing a '.
+ 'Differential revision.');
}
protected function defineParamTypes() {
return array(
'revision_id' => 'required int',
);
}
protected function defineReturnType() {
return 'nonempty list<string>';
}
protected function defineErrorTypes() {
return array(
- 'ERR_NOT_FOUND' => 'No such revision exists.',
+ 'ERR_NOT_FOUND' => pht('No such revision exists.'),
);
}
protected function execute(ConduitAPIRequest $request) {
$id = $request->getValue('revision_id');
$revision = id(new DifferentialRevisionQuery())
->setViewer($request->getUser())
->withIDs(array($id))
->executeOne();
if (!$revision) {
throw new ConduitException('ERR_NOT_FOUND');
}
$paths = array();
$diff = id(new DifferentialDiff())->loadOneWhere(
'revisionID = %d ORDER BY id DESC limit 1',
$revision->getID());
$diff->attachChangesets($diff->loadChangesets());
foreach ($diff->getChangesets() as $changeset) {
$paths[] = $changeset->getFilename();
}
return $paths;
}
}
diff --git a/src/applications/differential/conduit/DifferentialGetDiffConduitAPIMethod.php b/src/applications/differential/conduit/DifferentialGetDiffConduitAPIMethod.php
index 92d9807dd..a47c9c616 100644
--- a/src/applications/differential/conduit/DifferentialGetDiffConduitAPIMethod.php
+++ b/src/applications/differential/conduit/DifferentialGetDiffConduitAPIMethod.php
@@ -1,82 +1,82 @@
<?php
final class DifferentialGetDiffConduitAPIMethod
extends DifferentialConduitAPIMethod {
public function getAPIMethodName() {
return 'differential.getdiff';
}
public function shouldAllowPublic() {
return true;
}
public function getMethodStatus() {
return self::METHOD_STATUS_DEPRECATED;
}
public function getMethodStatusDescription() {
return pht(
'This method has been deprecated in favor of %s.',
'differential.querydiffs');
}
public function getMethodDescription() {
return pht(
- 'Load the content of a diff from Differential by revision id '.
- 'or diff id.');
+ 'Load the content of a diff from Differential by revision ID '.
+ 'or diff ID.');
}
protected function defineParamTypes() {
return array(
'revision_id' => 'optional id',
'diff_id' => 'optional id',
);
}
protected function defineReturnType() {
return 'nonempty dict';
}
protected function defineErrorTypes() {
return array(
- 'ERR_BAD_DIFF' => 'No such diff exists.',
+ 'ERR_BAD_DIFF' => pht('No such diff exists.'),
);
}
protected function execute(ConduitAPIRequest $request) {
$diff_id = $request->getValue('diff_id');
// If we have a revision ID, we need the most recent diff. Figure that out
// without loading all the attached data.
$revision_id = $request->getValue('revision_id');
if ($revision_id) {
$diffs = id(new DifferentialDiffQuery())
->setViewer($request->getUser())
->withRevisionIDs(array($revision_id))
->execute();
if ($diffs) {
$diff_id = head($diffs)->getID();
} else {
throw new ConduitException('ERR_BAD_DIFF');
}
}
$diff = null;
if ($diff_id) {
$diff = id(new DifferentialDiffQuery())
->setViewer($request->getUser())
->withIDs(array($diff_id))
->needChangesets(true)
->executeOne();
}
if (!$diff) {
throw new ConduitException('ERR_BAD_DIFF');
}
return $diff->getDiffDict();
}
}
diff --git a/src/applications/differential/conduit/DifferentialGetRevisionCommentsConduitAPIMethod.php b/src/applications/differential/conduit/DifferentialGetRevisionCommentsConduitAPIMethod.php
index 6a6770d3e..1ea23e33a 100644
--- a/src/applications/differential/conduit/DifferentialGetRevisionCommentsConduitAPIMethod.php
+++ b/src/applications/differential/conduit/DifferentialGetRevisionCommentsConduitAPIMethod.php
@@ -1,89 +1,89 @@
<?php
final class DifferentialGetRevisionCommentsConduitAPIMethod
extends DifferentialConduitAPIMethod {
public function getAPIMethodName() {
return 'differential.getrevisioncomments';
}
public function getMethodStatus() {
return self::METHOD_STATUS_DEPRECATED;
}
public function getMethodStatusDescription() {
return pht('Obsolete and doomed, see T2222.');
}
public function getMethodDescription() {
- return 'Retrieve Differential Revision Comments.';
+ return pht('Retrieve Differential Revision Comments.');
}
protected function defineParamTypes() {
return array(
'ids' => 'required list<int>',
'inlines' => 'optional bool (deprecated)',
);
}
protected function defineReturnType() {
return 'nonempty list<dict<string, wild>>';
}
protected function execute(ConduitAPIRequest $request) {
$viewer = $request->getUser();
$results = array();
$revision_ids = $request->getValue('ids');
if (!$revision_ids) {
return $results;
}
$revisions = id(new DifferentialRevisionQuery())
->setViewer($viewer)
->withIDs($revision_ids)
->execute();
if (!$revisions) {
return $results;
}
$xactions = id(new DifferentialTransactionQuery())
->setViewer($viewer)
->withObjectPHIDs(mpull($revisions, 'getPHID'))
->execute();
$revisions = mpull($revisions, null, 'getPHID');
foreach ($xactions as $xaction) {
$revision = idx($revisions, $xaction->getObjectPHID());
if (!$revision) {
continue;
}
$type = $xaction->getTransactionType();
if ($type == DifferentialTransaction::TYPE_ACTION) {
$action = $xaction->getNewValue();
} else if ($type == PhabricatorTransactions::TYPE_COMMENT) {
$action = 'comment';
} else {
$action = 'none';
}
$result = array(
'revisionID' => $revision->getID(),
'action' => $action,
'authorPHID' => $xaction->getAuthorPHID(),
'dateCreated' => $xaction->getDateCreated(),
'content' => ($xaction->hasComment()
? $xaction->getComment()->getContent()
: null),
);
$results[$revision->getID()][] = $result;
}
return $results;
}
}
diff --git a/src/applications/differential/conduit/DifferentialQueryConduitAPIMethod.php b/src/applications/differential/conduit/DifferentialQueryConduitAPIMethod.php
index 305b071be..2bf43d295 100644
--- a/src/applications/differential/conduit/DifferentialQueryConduitAPIMethod.php
+++ b/src/applications/differential/conduit/DifferentialQueryConduitAPIMethod.php
@@ -1,231 +1,235 @@
<?php
final class DifferentialQueryConduitAPIMethod
extends DifferentialConduitAPIMethod {
public function getAPIMethodName() {
return 'differential.query';
}
public function getMethodDescription() {
- return 'Query Differential revisions which match certain criteria.';
+ return pht('Query Differential revisions which match certain criteria.');
}
protected function defineParamTypes() {
$hash_types = ArcanistDifferentialRevisionHash::getTypes();
$hash_const = $this->formatStringConstants($hash_types);
$status_types = array(
DifferentialRevisionQuery::STATUS_ANY,
DifferentialRevisionQuery::STATUS_OPEN,
DifferentialRevisionQuery::STATUS_ACCEPTED,
DifferentialRevisionQuery::STATUS_CLOSED,
);
$status_const = $this->formatStringConstants($status_types);
$order_types = array(
DifferentialRevisionQuery::ORDER_MODIFIED,
DifferentialRevisionQuery::ORDER_CREATED,
);
$order_const = $this->formatStringConstants($order_types);
return array(
'authors' => 'optional list<phid>',
'ccs' => 'optional list<phid>',
'reviewers' => 'optional list<phid>',
'paths' => 'optional list<pair<callsign, path>>',
'commitHashes' => 'optional list<pair<'.$hash_const.', string>>',
'status' => 'optional '.$status_const,
'order' => 'optional '.$order_const,
'limit' => 'optional uint',
'offset' => 'optional uint',
'ids' => 'optional list<uint>',
'phids' => 'optional list<phid>',
'subscribers' => 'optional list<phid>',
'responsibleUsers' => 'optional list<phid>',
'branches' => 'optional list<string>',
);
}
protected function defineReturnType() {
return 'list<dict>';
}
protected function defineErrorTypes() {
return array(
- 'ERR-INVALID-PARAMETER' => 'Missing or malformed parameter.',
+ 'ERR-INVALID-PARAMETER' => pht('Missing or malformed parameter.'),
);
}
protected function execute(ConduitAPIRequest $request) {
$authors = $request->getValue('authors');
$ccs = $request->getValue('ccs');
$reviewers = $request->getValue('reviewers');
$status = $request->getValue('status');
$order = $request->getValue('order');
$path_pairs = $request->getValue('paths');
$commit_hashes = $request->getValue('commitHashes');
$limit = $request->getValue('limit');
$offset = $request->getValue('offset');
$ids = $request->getValue('ids');
$phids = $request->getValue('phids');
$subscribers = $request->getValue('subscribers');
$responsible_users = $request->getValue('responsibleUsers');
$branches = $request->getValue('branches');
$query = id(new DifferentialRevisionQuery())
->setViewer($request->getUser());
if ($authors) {
$query->withAuthors($authors);
}
if ($ccs) {
$query->withCCs($ccs);
}
if ($reviewers) {
$query->withReviewers($reviewers);
}
if ($path_pairs) {
$paths = array();
foreach ($path_pairs as $pair) {
list($callsign, $path) = $pair;
$paths[] = $path;
}
$path_map = id(new DiffusionPathIDQuery($paths))->loadPathIDs();
if (count($path_map) != count($paths)) {
$unknown_paths = array();
foreach ($paths as $p) {
if (!idx($path_map, $p)) {
$unknown_paths[] = $p;
}
}
throw id(new ConduitException('ERR-INVALID-PARAMETER'))
->setErrorDescription(
- 'Unknown paths: '.implode(', ', $unknown_paths));
+ pht(
+ 'Unknown paths: %s',
+ implode(', ', $unknown_paths)));
}
$repos = array();
foreach ($path_pairs as $pair) {
list($callsign, $path) = $pair;
if (!idx($repos, $callsign)) {
$repos[$callsign] = id(new PhabricatorRepositoryQuery())
->setViewer($request->getUser())
->withCallsigns(array($callsign))
->executeOne();
if (!$repos[$callsign]) {
throw id(new ConduitException('ERR-INVALID-PARAMETER'))
->setErrorDescription(
- 'Unknown repo callsign: '.$callsign);
+ pht(
+ 'Unknown repo callsign: %s',
+ $callsign));
}
}
$repo = $repos[$callsign];
$query->withPath($repo->getID(), idx($path_map, $path));
}
}
if ($commit_hashes) {
$hash_types = ArcanistDifferentialRevisionHash::getTypes();
foreach ($commit_hashes as $info) {
list($type, $hash) = $info;
if (empty($type) ||
!in_array($type, $hash_types) ||
empty($hash)) {
throw new ConduitException('ERR-INVALID-PARAMETER');
}
}
$query->withCommitHashes($commit_hashes);
}
if ($status) {
$query->withStatus($status);
}
if ($order) {
$query->setOrder($order);
}
if ($limit) {
$query->setLimit($limit);
}
if ($offset) {
$query->setOffset($offset);
}
if ($ids) {
$query->withIDs($ids);
}
if ($phids) {
$query->withPHIDs($phids);
}
if ($responsible_users) {
$query->withResponsibleUsers($responsible_users);
}
if ($subscribers) {
$query->withCCs($subscribers);
}
if ($branches) {
$query->withBranches($branches);
}
$query->needRelationships(true);
$query->needCommitPHIDs(true);
$query->needDiffIDs(true);
$query->needActiveDiffs(true);
$query->needHashes(true);
$revisions = $query->execute();
$field_data = $this->loadCustomFieldsForRevisions(
$request->getUser(),
$revisions);
$results = array();
foreach ($revisions as $revision) {
$diff = $revision->getActiveDiff();
if (!$diff) {
continue;
}
$id = $revision->getID();
$phid = $revision->getPHID();
$result = array(
'id' => $id,
'phid' => $phid,
'title' => $revision->getTitle(),
'uri' => PhabricatorEnv::getProductionURI('/D'.$id),
'dateCreated' => $revision->getDateCreated(),
'dateModified' => $revision->getDateModified(),
'authorPHID' => $revision->getAuthorPHID(),
'status' => $revision->getStatus(),
'statusName' =>
ArcanistDifferentialRevisionStatus::getNameForRevisionStatus(
$revision->getStatus()),
'branch' => $diff->getBranch(),
'summary' => $revision->getSummary(),
'testPlan' => $revision->getTestPlan(),
'lineCount' => $revision->getLineCount(),
'activeDiffPHID' => $diff->getPHID(),
'diffs' => $revision->getDiffIDs(),
'commits' => $revision->getCommitPHIDs(),
'reviewers' => array_values($revision->getReviewers()),
'ccs' => array_values($revision->getCCPHIDs()),
'hashes' => $revision->getHashes(),
'auxiliary' => idx($field_data, $phid, array()),
'repositoryPHID' => $diff->getRepositoryPHID(),
);
// TODO: This is a hacky way to put permissions on this field until we
// have first-class support, see T838.
if ($revision->getAuthorPHID() == $request->getUser()->getPHID()) {
$result['sourcePath'] = $diff->getSourcePath();
}
$results[] = $result;
}
return $results;
}
}
diff --git a/src/applications/differential/conduit/DifferentialSetDiffPropertyConduitAPIMethod.php b/src/applications/differential/conduit/DifferentialSetDiffPropertyConduitAPIMethod.php
index d79be1363..f9db11e3f 100644
--- a/src/applications/differential/conduit/DifferentialSetDiffPropertyConduitAPIMethod.php
+++ b/src/applications/differential/conduit/DifferentialSetDiffPropertyConduitAPIMethod.php
@@ -1,115 +1,115 @@
<?php
final class DifferentialSetDiffPropertyConduitAPIMethod
extends DifferentialConduitAPIMethod {
public function getAPIMethodName() {
return 'differential.setdiffproperty';
}
public function getMethodDescription() {
- return 'Attach properties to Differential diffs.';
+ return pht('Attach properties to Differential diffs.');
}
protected function defineParamTypes() {
return array(
'diff_id' => 'required diff_id',
'name' => 'required string',
'data' => 'required string',
);
}
protected function defineReturnType() {
return 'void';
}
protected function defineErrorTypes() {
return array(
- 'ERR_NOT_FOUND' => 'Diff was not found.',
+ 'ERR_NOT_FOUND' => pht('Diff was not found.'),
);
}
private static function updateLintStatus($diff_id) {
$diff = id(new DifferentialDiff())->load($diff_id);
if (!$diff) {
throw new ConduitException('ERR_NOT_FOUND');
}
// Load the postponed linters attached to this diff.
$postponed_linters_property = id(
new DifferentialDiffProperty())->loadOneWhere(
'diffID = %d AND name = %s',
$diff_id,
'arc:lint-postponed');
if ($postponed_linters_property) {
$postponed_linters = $postponed_linters_property->getData();
} else {
$postponed_linters = array();
}
// Load the lint messages currenty attached to the diff
$messages_property = id(new DifferentialDiffProperty())->loadOneWhere(
'diffID = %d AND name = %s',
$diff_id,
'arc:lint');
if ($messages_property) {
$results = $messages_property->getData();
} else {
$results = array();
}
$has_error = false;
$has_warning = false;
foreach ($results as $result) {
if ($result['severity'] === ArcanistLintSeverity::SEVERITY_ERROR) {
$has_error = true;
break;
} else if ($result['severity'] ===
ArcanistLintSeverity::SEVERITY_WARNING) {
$has_warning = true;
}
}
if ($has_error) {
$diff->setLintStatus(DifferentialLintStatus::LINT_FAIL);
} else if ($has_warning) {
$diff->setLintStatus(DifferentialLintStatus::LINT_WARN);
} else if (!empty($postponed_linters)) {
$diff->setLintStatus(DifferentialLintStatus::LINT_POSTPONED);
} else if ($diff->getLintStatus() != DifferentialLintStatus::LINT_SKIP) {
$diff->setLintStatus(DifferentialLintStatus::LINT_OKAY);
}
$diff->save();
}
protected function execute(ConduitAPIRequest $request) {
$diff_id = $request->getValue('diff_id');
$name = $request->getValue('name');
$data = json_decode($request->getValue('data'), true);
self::updateDiffProperty($diff_id, $name, $data);
if ($name === 'arc:lint' || $name == 'arc:lint-postponed') {
self::updateLintStatus($diff_id);
}
return;
}
private static function updateDiffProperty($diff_id, $name, $data) {
$property = id(new DifferentialDiffProperty())->loadOneWhere(
'diffID = %d AND name = %s',
$diff_id,
$name);
if (!$property) {
$property = new DifferentialDiffProperty();
$property->setDiffID($diff_id);
$property->setName($name);
}
$property->setData($data);
$property->save();
return $property;
}
}
diff --git a/src/applications/differential/conduit/DifferentialUpdateRevisionConduitAPIMethod.php b/src/applications/differential/conduit/DifferentialUpdateRevisionConduitAPIMethod.php
index e8a34fd13..232dbf79b 100644
--- a/src/applications/differential/conduit/DifferentialUpdateRevisionConduitAPIMethod.php
+++ b/src/applications/differential/conduit/DifferentialUpdateRevisionConduitAPIMethod.php
@@ -1,79 +1,79 @@
<?php
final class DifferentialUpdateRevisionConduitAPIMethod
extends DifferentialConduitAPIMethod {
public function getAPIMethodName() {
return 'differential.updaterevision';
}
public function getMethodDescription() {
return pht('Update a Differential revision.');
}
protected function defineParamTypes() {
return array(
'id' => 'required revisionid',
'diffid' => 'required diffid',
'fields' => 'required dict',
'message' => 'required string',
);
}
protected function defineReturnType() {
return 'nonempty dict';
}
protected function defineErrorTypes() {
return array(
- 'ERR_BAD_DIFF' => 'Bad diff ID.',
- 'ERR_BAD_REVISION' => 'Bad revision ID.',
- 'ERR_WRONG_USER' => 'You are not the author of this revision.',
- 'ERR_CLOSED' => 'This revision has already been closed.',
+ 'ERR_BAD_DIFF' => pht('Bad diff ID.'),
+ 'ERR_BAD_REVISION' => pht('Bad revision ID.'),
+ 'ERR_WRONG_USER' => pht('You are not the author of this revision.'),
+ 'ERR_CLOSED' => pht('This revision has already been closed.'),
);
}
protected function execute(ConduitAPIRequest $request) {
$viewer = $request->getUser();
$diff = id(new DifferentialDiffQuery())
->setViewer($viewer)
->withIDs(array($request->getValue('diffid')))
->executeOne();
if (!$diff) {
throw new ConduitException('ERR_BAD_DIFF');
}
$revision = id(new DifferentialRevisionQuery())
->setViewer($request->getUser())
->withIDs(array($request->getValue('id')))
->needReviewerStatus(true)
->needActiveDiffs(true)
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
if (!$revision) {
throw new ConduitException('ERR_BAD_REVISION');
}
if ($revision->getStatus() == ArcanistDifferentialRevisionStatus::CLOSED) {
throw new ConduitException('ERR_CLOSED');
}
$this->applyFieldEdit(
$request,
$revision,
$diff,
$request->getValue('fields', array()),
$request->getValue('message'));
return array(
'revisionid' => $revision->getID(),
'uri' => PhabricatorEnv::getURI('/D'.$revision->getID()),
);
}
}
diff --git a/src/applications/differential/conduit/DifferentialUpdateUnitResultsConduitAPIMethod.php b/src/applications/differential/conduit/DifferentialUpdateUnitResultsConduitAPIMethod.php
index 0f0c14a6b..7b1a25066 100644
--- a/src/applications/differential/conduit/DifferentialUpdateUnitResultsConduitAPIMethod.php
+++ b/src/applications/differential/conduit/DifferentialUpdateUnitResultsConduitAPIMethod.php
@@ -1,155 +1,154 @@
<?php
final class DifferentialUpdateUnitResultsConduitAPIMethod
extends DifferentialConduitAPIMethod {
public function getAPIMethodName() {
return 'differential.updateunitresults';
}
public function getMethodDescription() {
- return 'Update arc unit results for a postponed test.';
+ return pht('Update arc unit results for a postponed test.');
}
protected function defineParamTypes() {
return array(
'diff_id' => 'required diff_id',
'file' => 'required string',
'name' => 'required string',
'link' => 'optional string',
'result' => 'required string',
'message' => 'required string',
'coverage' => 'optional map<string, string>',
);
}
protected function defineReturnType() {
return 'void';
}
protected function defineErrorTypes() {
return array(
- 'ERR_BAD_DIFF' => 'Bad diff ID.',
- 'ERR_NO_RESULTS' => 'Could not find the postponed test',
+ 'ERR_BAD_DIFF' => pht('Bad diff ID.'),
+ 'ERR_NO_RESULTS' => pht('Could not find the postponed test'),
);
}
protected function execute(ConduitAPIRequest $request) {
-
$diff_id = $request->getValue('diff_id');
if (!$diff_id) {
throw new ConduitException('ERR_BAD_DIFF');
}
$file = $request->getValue('file');
$name = $request->getValue('name');
$link = $request->getValue('link');
$message = $request->getValue('message');
$result = $request->getValue('result');
$coverage = $request->getValue('coverage', array());
$diff_property = id(new DifferentialDiffProperty())->loadOneWhere(
'diffID = %d AND name = %s',
$diff_id,
'arc:unit');
if (!$diff_property) {
throw new ConduitException('ERR_NO_RESULTS');
}
$diff = id(new DifferentialDiffQuery())
->setViewer($request->getUser())
->withIDs(array($diff_id))
->executeOne();
$unit_results = $diff_property->getData();
$postponed_count = 0;
$unit_status = null;
// If the test result already exists, then update it with
// the new info.
foreach ($unit_results as &$unit_result) {
if ($unit_result['name'] === $name ||
$unit_result['name'] === $file ||
$unit_result['name'] === $diff->getSourcePath().$file) {
$unit_result['name'] = $name;
$unit_result['link'] = $link;
$unit_result['file'] = $file;
$unit_result['result'] = $result;
$unit_result['userdata'] = $message;
$unit_result['coverage'] = $coverage;
$unit_status = $result;
break;
}
}
unset($unit_result);
// If the test result doesn't exist, just add it.
if (!$unit_status) {
$unit_result = array();
$unit_result['file'] = $file;
$unit_result['name'] = $name;
$unit_result['link'] = $link;
$unit_result['result'] = $result;
$unit_result['userdata'] = $message;
$unit_result['coverage'] = $coverage;
$unit_status = $result;
$unit_results[] = $unit_result;
}
unset($unit_result);
$diff_property->setData($unit_results);
$diff_property->save();
// Map external unit test status to internal overall diff status
$status_codes =
array(
DifferentialUnitTestResult::RESULT_PASS =>
DifferentialUnitStatus::UNIT_OKAY,
DifferentialUnitTestResult::RESULT_UNSOUND =>
DifferentialUnitStatus::UNIT_WARN,
DifferentialUnitTestResult::RESULT_FAIL =>
DifferentialUnitStatus::UNIT_FAIL,
DifferentialUnitTestResult::RESULT_BROKEN =>
DifferentialUnitStatus::UNIT_FAIL,
DifferentialUnitTestResult::RESULT_SKIP =>
DifferentialUnitStatus::UNIT_OKAY,
DifferentialUnitTestResult::RESULT_POSTPONED =>
DifferentialUnitStatus::UNIT_POSTPONED,
);
// These are the relative priorities for the unit test results
$status_codes_priority =
array(
DifferentialUnitStatus::UNIT_OKAY => 1,
DifferentialUnitStatus::UNIT_WARN => 2,
DifferentialUnitStatus::UNIT_POSTPONED => 3,
DifferentialUnitStatus::UNIT_FAIL => 4,
);
// Walk the now-current list of status codes to find the overall diff
// status
$final_diff_status = DifferentialUnitStatus::UNIT_NONE;
foreach ($unit_results as $unit_result) {
// Convert the text result into a diff unit status value
$status_code = idx($status_codes,
$unit_result['result'],
DifferentialUnitStatus::UNIT_NONE);
// Convert the unit status into a relative value
$diff_status_priority = idx($status_codes_priority, $status_code, 0);
// If the relative value of this result is "more bad" than previous
// results, use it as the new final diff status
if ($diff_status_priority > idx($status_codes_priority,
$final_diff_status, 0)) {
$final_diff_status = $status_code;
}
}
// Update our unit test result status with the final value
$diff->setUnitStatus($final_diff_status);
$diff->save();
}
}
diff --git a/src/applications/differential/config/PhabricatorDifferentialConfigOptions.php b/src/applications/differential/config/PhabricatorDifferentialConfigOptions.php
index 6169d0df8..2c8f4e0bd 100644
--- a/src/applications/differential/config/PhabricatorDifferentialConfigOptions.php
+++ b/src/applications/differential/config/PhabricatorDifferentialConfigOptions.php
@@ -1,304 +1,306 @@
<?php
final class PhabricatorDifferentialConfigOptions
extends PhabricatorApplicationConfigOptions {
public function getName() {
return pht('Differential');
}
public function getDescription() {
return pht('Configure Differential code review.');
}
public function getFontIcon() {
return 'fa-cog';
}
public function getGroup() {
return 'apps';
}
public function getOptions() {
$caches_href = PhabricatorEnv::getDoclink('Managing Caches');
$custom_field_type = 'custom:PhabricatorCustomFieldConfigOptionType';
$fields = array(
new DifferentialTitleField(),
new DifferentialSummaryField(),
new DifferentialTestPlanField(),
new DifferentialAuthorField(),
new DifferentialReviewersField(),
new DifferentialProjectReviewersField(),
new DifferentialReviewedByField(),
new DifferentialSubscribersField(),
new DifferentialRepositoryField(),
new DifferentialLintField(),
new DifferentialProjectsField(),
new DifferentialUnitField(),
new DifferentialViewPolicyField(),
new DifferentialEditPolicyField(),
new DifferentialDependsOnField(),
new DifferentialDependenciesField(),
new DifferentialManiphestTasksField(),
new DifferentialCommitsField(),
new DifferentialJIRAIssuesField(),
new DifferentialAsanaRepresentationField(),
new DifferentialChangesSinceLastUpdateField(),
new DifferentialBranchField(),
new DifferentialBlameRevisionField(),
new DifferentialPathField(),
new DifferentialHostField(),
new DifferentialRevertPlanField(),
new DifferentialApplyPatchField(),
new DifferentialRevisionIDField(),
);
$default_fields = array();
foreach ($fields as $field) {
$default_fields[$field->getFieldKey()] = array(
'disabled' => $field->shouldDisableByDefault(),
);
}
return array(
$this->newOption(
'differential.fields',
$custom_field_type,
$default_fields)
->setCustomData(
id(new DifferentialRevision())->getCustomFieldBaseClass())
->setDescription(
pht(
"Select and reorder revision fields.\n\n".
"NOTE: This feature is under active development and subject ".
"to change.")),
$this->newOption(
'differential.whitespace-matters',
'list<regex>',
array(
'/\.py$/',
'/\.l?hs$/',
))
->setDescription(
pht(
"List of file regexps where whitespace is meaningful and should ".
"not use 'ignore-all' by default")),
$this->newOption('differential.require-test-plan-field', 'bool', true)
->setBoolOptions(
array(
pht("Require 'Test Plan' field"),
pht("Make 'Test Plan' field optional"),
))
->setSummary(pht('Require "Test Plan" field?'))
->setDescription(
pht(
"Differential has a required 'Test Plan' field by default. You ".
"can make it optional by setting this to false. You can also ".
"completely remove it above, if you prefer.")),
$this->newOption('differential.enable-email-accept', 'bool', false)
->setBoolOptions(
array(
pht('Enable Email "!accept" Action'),
pht('Disable Email "!accept" Action'),
))
->setSummary(pht('Enable or disable "!accept" action via email.'))
->setDescription(
pht(
'If inbound email is configured, users can interact with '.
'revisions by using "!actions" in email replies (for example, '.
'"!resign" or "!rethink"). However, by default, users may not '.
'"!accept" revisions via email: email authentication can be '.
'configured to be very weak, and email "!accept" is kind of '.
'sketchy and implies the revision may not actually be receiving '.
'thorough review. You can enable "!accept" by setting this '.
'option to true.')),
$this->newOption('differential.generated-paths', 'list<regex>', array())
->setSummary(pht('File regexps to treat as automatically generated.'))
->setDescription(
pht(
'List of file regexps that should be treated as if they are '.
'generated by an automatic process, and thus be hidden by '.
'default in Differential.'.
"\n\n".
'NOTE: This property is cached, so you will need to purge the '.
'cache after making changes if you want the new configuration '.
'to affect existing revisions. For instructions, see '.
'**[[ %s | Managing Caches ]]** in the documentation.',
$caches_href))
->addExample("/config\.h$/\n#/autobuilt/#", pht('Valid Setting')),
$this->newOption('differential.sticky-accept', 'bool', true)
->setBoolOptions(
array(
pht('Accepts persist across updates'),
pht('Accepts are reset by updates'),
))
->setSummary(
pht('Should "Accepted" revisions remain "Accepted" after updates?'))
->setDescription(
pht(
'Normally, when revisions that have been "Accepted" are updated, '.
'they remain "Accepted". This allows reviewers to suggest minor '.
'alterations when accepting, and encourages authors to update '.
'if they make minor changes in response to this feedback.'.
"\n\n".
'If you want updates to always require re-review, you can disable '.
'the "stickiness" of the "Accepted" status with this option. '.
'This may make the process for minor changes much more burdensome '.
'to both authors and reviewers.')),
$this->newOption('differential.allow-self-accept', 'bool', false)
->setBoolOptions(
array(
pht('Allow self-accept'),
pht('Disallow self-accept'),
))
->setSummary(pht('Allows users to accept their own revisions.'))
->setDescription(
pht(
"If you set this to true, users can accept their own revisions. ".
"This action is disabled by default because it's most likely not ".
"a behavior you want, but it proves useful if you are working ".
"alone on a project and want to make use of all of ".
"differential's features.")),
$this->newOption('differential.always-allow-close', 'bool', false)
->setBoolOptions(
array(
pht('Allow any user'),
pht('Restrict to submitter'),
))
->setSummary(pht('Allows any user to close accepted revisions.'))
->setDescription(
pht(
'If you set this to true, any user can close any revision so '.
'long as it has been accepted. This can be useful depending on '.
'your development model. For example, github-style pull requests '.
'where the reviewer is often the actual committer can benefit '.
'from turning this option to true. If false, only the submitter '.
'can close a revision.')),
$this->newOption('differential.always-allow-abandon', 'bool', false)
->setBoolOptions(
array(
pht('Allow any user'),
pht('Restrict to submitter'),
))
->setSummary(pht('Allows any user to abandon revisions.'))
->setDescription(
pht(
'If you set this to true, any user can abandon any revision. If '.
'false, only the submitter can abandon a revision.')),
$this->newOption('differential.allow-reopen', 'bool', false)
->setBoolOptions(
array(
pht('Enable reopen'),
pht('Disable reopen'),
))
->setSummary(pht('Allows any user to reopen a closed revision.'))
->setDescription(
- pht('If you set this to true, any user can reopen a revision so '.
- 'long as it has been closed. This can be useful if a revision '.
- 'is accidentally closed or if a developer changes his or her '.
- 'mind after closing a revision. If it is false, reopening '.
- 'is not allowed.')),
+ pht(
+ 'If you set this to true, any user can reopen a revision so '.
+ 'long as it has been closed. This can be useful if a revision '.
+ 'is accidentally closed or if a developer changes his or her '.
+ 'mind after closing a revision. If it is false, reopening '.
+ 'is not allowed.')),
$this->newOption('differential.close-on-accept', 'bool', false)
->setBoolOptions(
array(
pht('Treat Accepted Revisions as "Closed"'),
pht('Treat Accepted Revisions as "Open"'),
))
->setSummary(pht('Allows "Accepted" to act as a closed status.'))
->setDescription(
pht(
'Normally, Differential revisions remain on the dashboard when '.
'they are "Accepted", and the author then commits the changes '.
'to "Close" the revision and move it off the dashboard.'.
"\n\n".
'If you have an unusual workflow where Differential is used for '.
'post-commit review (normally called "Audit", elsewhere in '.
'Phabricator), you can set this flag to treat the "Accepted" '.
'state as a "Closed" state and end the review workflow early.'.
"\n\n".
'This sort of workflow is very unusual. Very few installs should '.
'need to change this option.')),
$this->newOption('differential.days-fresh', 'int', 1)
->setSummary(
pht(
"For how many business days should a revision be considered ".
"'fresh'?"))
->setDescription(
pht(
'Revisions newer than this number of days are marked as fresh in '.
'Action Required and Revisions Waiting on You views. Only work '.
'days (not weekends and holidays) are included. Set to 0 to '.
'disable this feature.')),
$this->newOption('differential.days-stale', 'int', 3)
->setSummary(
pht("After this many days, a revision will be considered 'stale'."))
->setDescription(
pht(
- "Similar to `differential.days-fresh` but marks stale revisions. ".
- "If the revision is even older than it is when marked as 'old'.")),
+ "Similar to `%s` but marks stale revisions. ".
+ "If the revision is even older than it is when marked as 'old'.",
+ 'differential.days-fresh')),
$this->newOption(
'metamta.differential.subject-prefix',
'string',
'[Differential]')
->setDescription(pht('Subject prefix for Differential mail.')),
$this->newOption(
'metamta.differential.attach-patches',
'bool',
false)
->setBoolOptions(
array(
pht('Attach Patches'),
pht('Do Not Attach Patches'),
))
->setSummary(pht('Attach patches to email, as text attachments.'))
->setDescription(
pht(
'If you set this to true, Phabricator will attach patches to '.
'Differential mail (as text attachments). This will not work if '.
'you are using SendGrid as your mail adapter.')),
$this->newOption(
'metamta.differential.inline-patches',
'int',
0)
->setSummary(pht('Inline patches in email, as body text.'))
->setDescription(
pht(
"To include patches inline in email bodies, set this to a ".
"positive integer. Patches will be inlined if they are at most ".
"that many lines. For instance, a value of 100 means 'inline ".
"patches if they are no longer than 100 lines'. By default, ".
"patches are not inlined.")),
// TODO: Implement 'enum'? Options are 'unified' or 'git'.
$this->newOption(
'metamta.differential.patch-format',
'string',
'unified')
->setDescription(
pht("Format for inlined or attached patches: 'git' or 'unified'.")),
$this->newOption(
'metamta.differential.unified-comment-context',
'bool',
false)
->setBoolOptions(
array(
pht('Show context'),
pht('Do not show context'),
))
->setSummary(pht('Show diff context around inline comments in email.'))
->setDescription(
pht(
'Normally, inline comments in emails are shown with a file and '.
'line but without any diff context. Enabling this option adds '.
'diff context and the comment thread.')),
);
}
}
diff --git a/src/applications/differential/constants/DifferentialAction.php b/src/applications/differential/constants/DifferentialAction.php
index 48a263c45..5b5fa9eb7 100644
--- a/src/applications/differential/constants/DifferentialAction.php
+++ b/src/applications/differential/constants/DifferentialAction.php
@@ -1,138 +1,155 @@
<?php
final class DifferentialAction {
const ACTION_CLOSE = 'commit';
const ACTION_COMMENT = 'none';
const ACTION_ACCEPT = 'accept';
const ACTION_REJECT = 'reject';
const ACTION_RETHINK = 'rethink';
const ACTION_ABANDON = 'abandon';
const ACTION_REQUEST = 'request_review';
const ACTION_RECLAIM = 'reclaim';
const ACTION_UPDATE = 'update';
const ACTION_RESIGN = 'resign';
const ACTION_SUMMARIZE = 'summarize';
const ACTION_TESTPLAN = 'testplan';
const ACTION_CREATE = 'create';
const ACTION_ADDREVIEWERS = 'add_reviewers';
const ACTION_ADDCCS = 'add_ccs';
const ACTION_CLAIM = 'claim';
const ACTION_REOPEN = 'reopen';
public static function getBasicStoryText($action, $author_name) {
switch ($action) {
case self::ACTION_COMMENT:
- $title = pht('%s commented on this revision.',
+ $title = pht(
+ '%s commented on this revision.',
$author_name);
break;
case self::ACTION_ACCEPT:
- $title = pht('%s accepted this revision.',
+ $title = pht(
+ '%s accepted this revision.',
$author_name);
break;
case self::ACTION_REJECT:
- $title = pht('%s requested changes to this revision.',
+ $title = pht(
+ '%s requested changes to this revision.',
$author_name);
break;
case self::ACTION_RETHINK:
- $title = pht('%s planned changes to this revision.',
+ $title = pht(
+ '%s planned changes to this revision.',
$author_name);
break;
case self::ACTION_ABANDON:
- $title = pht('%s abandoned this revision.',
+ $title = pht(
+ '%s abandoned this revision.',
$author_name);
break;
case self::ACTION_CLOSE:
- $title = pht('%s closed this revision.',
+ $title = pht(
+ '%s closed this revision.',
$author_name);
break;
case self::ACTION_REQUEST:
- $title = pht('%s requested a review of this revision.',
+ $title = pht(
+ '%s requested a review of this revision.',
$author_name);
break;
case self::ACTION_RECLAIM:
- $title = pht('%s reclaimed this revision.',
+ $title = pht(
+ '%s reclaimed this revision.',
$author_name);
break;
case self::ACTION_UPDATE:
- $title = pht('%s updated this revision.',
+ $title = pht(
+ '%s updated this revision.',
$author_name);
break;
case self::ACTION_RESIGN:
- $title = pht('%s resigned from this revision.',
+ $title = pht(
+ '%s resigned from this revision.',
$author_name);
break;
case self::ACTION_SUMMARIZE:
- $title = pht('%s summarized this revision.',
+ $title = pht(
+ '%s summarized this revision.',
$author_name);
break;
case self::ACTION_TESTPLAN:
- $title = pht('%s explained the test plan for this revision.',
+ $title = pht(
+ '%s explained the test plan for this revision.',
$author_name);
break;
case self::ACTION_CREATE:
- $title = pht('%s created this revision.',
+ $title = pht(
+ '%s created this revision.',
$author_name);
break;
case self::ACTION_ADDREVIEWERS:
- $title = pht('%s added reviewers to this revision.',
+ $title = pht(
+ '%s added reviewers to this revision.',
$author_name);
break;
case self::ACTION_ADDCCS:
- $title = pht('%s added CCs to this revision.',
+ $title = pht(
+ '%s added CCs to this revision.',
$author_name);
break;
case self::ACTION_CLAIM:
- $title = pht('%s commandeered this revision.',
+ $title = pht(
+ '%s commandeered this revision.',
$author_name);
break;
case self::ACTION_REOPEN:
- $title = pht('%s reopened this revision.',
+ $title = pht(
+ '%s reopened this revision.',
$author_name);
break;
case DifferentialTransaction::TYPE_INLINE:
$title = pht(
'%s added an inline comment.',
$author_name);
break;
default:
$title = pht('Ghosts happened to this revision.');
break;
}
return $title;
}
public static function getActionVerb($action) {
$verbs = array(
self::ACTION_COMMENT => pht('Comment'),
self::ACTION_ACCEPT => pht("Accept Revision \xE2\x9C\x94"),
self::ACTION_REJECT => pht("Request Changes \xE2\x9C\x98"),
self::ACTION_RETHINK => pht("Plan Changes \xE2\x9C\x98"),
self::ACTION_ABANDON => pht('Abandon Revision'),
self::ACTION_REQUEST => pht('Request Review'),
self::ACTION_RECLAIM => pht('Reclaim Revision'),
self::ACTION_RESIGN => pht('Resign as Reviewer'),
self::ACTION_ADDREVIEWERS => pht('Add Reviewers'),
self::ACTION_ADDCCS => pht('Add Subscribers'),
self::ACTION_CLOSE => pht('Close Revision'),
self::ACTION_CLAIM => pht('Commandeer Revision'),
self::ACTION_REOPEN => pht('Reopen'),
);
if (!empty($verbs[$action])) {
return $verbs[$action];
} else {
- return 'brazenly '.$action;
+ return pht('brazenly %s', $action);
}
}
public static function allowReviewers($action) {
if ($action == self::ACTION_ADDREVIEWERS ||
$action == self::ACTION_REQUEST ||
$action == self::ACTION_RESIGN) {
return true;
}
return false;
}
}
diff --git a/src/applications/differential/constants/DifferentialChangeType.php b/src/applications/differential/constants/DifferentialChangeType.php
index 2ce539216..7e037d29b 100644
--- a/src/applications/differential/constants/DifferentialChangeType.php
+++ b/src/applications/differential/constants/DifferentialChangeType.php
@@ -1,111 +1,111 @@
<?php
final class DifferentialChangeType {
const TYPE_ADD = 1;
const TYPE_CHANGE = 2;
const TYPE_DELETE = 3;
const TYPE_MOVE_AWAY = 4;
const TYPE_COPY_AWAY = 5;
const TYPE_MOVE_HERE = 6;
const TYPE_COPY_HERE = 7;
const TYPE_MULTICOPY = 8;
const TYPE_MESSAGE = 9;
const TYPE_CHILD = 10;
const FILE_TEXT = 1;
const FILE_IMAGE = 2;
const FILE_BINARY = 3;
const FILE_DIRECTORY = 4;
const FILE_SYMLINK = 5;
const FILE_DELETED = 6;
const FILE_NORMAL = 7;
const FILE_SUBMODULE = 8;
public static function getSummaryCharacterForChangeType($type) {
static $types = array(
self::TYPE_ADD => 'A',
self::TYPE_CHANGE => 'M',
self::TYPE_DELETE => 'D',
self::TYPE_MOVE_AWAY => 'V',
self::TYPE_COPY_AWAY => 'P',
self::TYPE_MOVE_HERE => 'V',
self::TYPE_COPY_HERE => 'P',
self::TYPE_MULTICOPY => 'P',
self::TYPE_MESSAGE => 'Q',
self::TYPE_CHILD => '@',
);
return idx($types, coalesce($type, '?'), '~');
}
public static function getShortNameForFileType($type) {
static $names = array(
self::FILE_TEXT => null,
self::FILE_DIRECTORY => 'dir',
self::FILE_IMAGE => 'img',
self::FILE_BINARY => 'bin',
self::FILE_SYMLINK => 'sym',
self::FILE_SUBMODULE => 'sub',
);
return idx($names, coalesce($type, '?'), '???');
}
public static function isOldLocationChangeType($type) {
static $types = array(
self::TYPE_MOVE_AWAY => true,
self::TYPE_COPY_AWAY => true,
self::TYPE_MULTICOPY => true,
);
return isset($types[$type]);
}
public static function isNewLocationChangeType($type) {
static $types = array(
self::TYPE_MOVE_HERE => true,
self::TYPE_COPY_HERE => true,
);
return isset($types[$type]);
}
public static function isDeleteChangeType($type) {
static $types = array(
self::TYPE_DELETE => true,
self::TYPE_MOVE_AWAY => true,
self::TYPE_MULTICOPY => true,
);
return isset($types[$type]);
}
public static function isCreateChangeType($type) {
static $types = array(
self::TYPE_ADD => true,
self::TYPE_COPY_HERE => true,
self::TYPE_MOVE_HERE => true,
);
return isset($types[$type]);
}
public static function isModifyChangeType($type) {
static $types = array(
self::TYPE_CHANGE => true,
);
return isset($types[$type]);
}
public static function getFullNameForChangeType($type) {
$types = array(
self::TYPE_ADD => pht('Added'),
self::TYPE_CHANGE => pht('Modified'),
self::TYPE_DELETE => pht('Deleted'),
self::TYPE_MOVE_AWAY => pht('Moved Away'),
self::TYPE_COPY_AWAY => pht('Copied Away'),
self::TYPE_MOVE_HERE => pht('Moved Here'),
self::TYPE_COPY_HERE => pht('Copied Here'),
self::TYPE_MULTICOPY => pht('Deleted After Multiple Copy'),
self::TYPE_MESSAGE => pht('Commit Message'),
self::TYPE_CHILD => pht('Contents Modified'),
);
- return idx($types, coalesce($type, '?'), 'Unknown');
+ return idx($types, coalesce($type, '?'), pht('Unknown'));
}
}
diff --git a/src/applications/differential/controller/DifferentialChangesetViewController.php b/src/applications/differential/controller/DifferentialChangesetViewController.php
index abc19ab83..0638578f9 100644
--- a/src/applications/differential/controller/DifferentialChangesetViewController.php
+++ b/src/applications/differential/controller/DifferentialChangesetViewController.php
@@ -1,387 +1,387 @@
<?php
final class DifferentialChangesetViewController extends DifferentialController {
public function shouldAllowPublic() {
return true;
}
public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
$rendering_reference = $request->getStr('ref');
$parts = explode('/', $rendering_reference);
if (count($parts) == 2) {
list($id, $vs) = $parts;
} else {
$id = $parts[0];
$vs = 0;
}
$id = (int)$id;
$vs = (int)$vs;
$load_ids = array($id);
if ($vs && ($vs != -1)) {
$load_ids[] = $vs;
}
$changesets = id(new DifferentialChangesetQuery())
->setViewer($viewer)
->withIDs($load_ids)
->needHunks(true)
->execute();
$changesets = mpull($changesets, null, 'getID');
$changeset = idx($changesets, $id);
if (!$changeset) {
return new Aphront404Response();
}
$vs_changeset = null;
if ($vs && ($vs != -1)) {
$vs_changeset = idx($changesets, $vs);
if (!$vs_changeset) {
return new Aphront404Response();
}
}
$view = $request->getStr('view');
if ($view) {
$phid = idx($changeset->getMetadata(), "$view:binary-phid");
if ($phid) {
return id(new AphrontRedirectResponse())->setURI("/file/info/$phid/");
}
switch ($view) {
case 'new':
return $this->buildRawFileResponse($changeset, $is_new = true);
case 'old':
if ($vs_changeset) {
return $this->buildRawFileResponse($vs_changeset, $is_new = true);
}
return $this->buildRawFileResponse($changeset, $is_new = false);
default:
return new Aphront400Response();
}
}
$old = array();
$new = array();
if (!$vs) {
$right = $changeset;
$left = null;
$right_source = $right->getID();
$right_new = true;
$left_source = $right->getID();
$left_new = false;
$render_cache_key = $right->getID();
$old[] = $changeset;
$new[] = $changeset;
} else if ($vs == -1) {
$right = null;
$left = $changeset;
$right_source = $left->getID();
$right_new = false;
$left_source = $left->getID();
$left_new = true;
$render_cache_key = null;
$old[] = $changeset;
$new[] = $changeset;
} else {
$right = $changeset;
$left = $vs_changeset;
$right_source = $right->getID();
$right_new = true;
$left_source = $left->getID();
$left_new = true;
$render_cache_key = null;
$new[] = $left;
$new[] = $right;
}
if ($left) {
$left_data = $left->makeNewFile();
if ($right) {
$right_data = $right->makeNewFile();
} else {
$right_data = $left->makeOldFile();
}
$engine = new PhabricatorDifferenceEngine();
$synthetic = $engine->generateChangesetFromFileContent(
$left_data,
$right_data);
$choice = clone nonempty($left, $right);
$choice->attachHunks($synthetic->getHunks());
$changeset = $choice;
}
$coverage = null;
if ($right && $right->getDiffID()) {
$unit = id(new DifferentialDiffProperty())->loadOneWhere(
'diffID = %d AND name = %s',
$right->getDiffID(),
'arc:unit');
if ($unit) {
$coverage = array();
foreach ($unit->getData() as $result) {
$result_coverage = idx($result, 'coverage');
if (!$result_coverage) {
continue;
}
$file_coverage = idx($result_coverage, $right->getFileName());
if (!$file_coverage) {
continue;
}
$coverage[] = $file_coverage;
}
$coverage = ArcanistUnitTestResult::mergeCoverage($coverage);
}
}
$spec = $request->getStr('range');
list($range_s, $range_e, $mask) =
DifferentialChangesetParser::parseRangeSpecification($spec);
$parser = id(new DifferentialChangesetParser())
->setCoverage($coverage)
->setChangeset($changeset)
->setRenderingReference($rendering_reference)
->setRenderCacheKey($render_cache_key)
->setRightSideCommentMapping($right_source, $right_new)
->setLeftSideCommentMapping($left_source, $left_new);
$parser->readParametersFromRequest($request);
if ($left && $right) {
$parser->setOriginals($left, $right);
}
$diff = $changeset->getDiff();
$revision_id = $diff->getRevisionID();
$can_mark = false;
$object_owner_phid = null;
$revision = null;
if ($revision_id) {
$revision = id(new DifferentialRevisionQuery())
->setViewer($viewer)
->withIDs(array($revision_id))
->executeOne();
if ($revision) {
$can_mark = ($revision->getAuthorPHID() == $viewer->getPHID());
$object_owner_phid = $revision->getAuthorPHID();
}
}
// Load both left-side and right-side inline comments.
if ($revision) {
$query = id(new DifferentialInlineCommentQuery())
->setViewer($viewer)
->withRevisionPHIDs(array($revision->getPHID()));
$inlines = $query->execute();
$inlines = $query->adjustInlinesForChangesets(
$inlines,
$old,
$new,
$revision);
} else {
$inlines = array();
}
if ($left_new) {
$inlines = array_merge(
$inlines,
$this->buildLintInlineComments($left));
}
if ($right_new) {
$inlines = array_merge(
$inlines,
$this->buildLintInlineComments($right));
}
$phids = array();
foreach ($inlines as $inline) {
$parser->parseInlineComment($inline);
if ($inline->getAuthorPHID()) {
$phids[$inline->getAuthorPHID()] = true;
}
}
$phids = array_keys($phids);
$handles = $this->loadViewerHandles($phids);
$parser->setHandles($handles);
$engine = new PhabricatorMarkupEngine();
$engine->setViewer($viewer);
foreach ($inlines as $inline) {
$engine->addObject(
$inline,
PhabricatorInlineCommentInterface::MARKUP_FIELD_BODY);
}
$engine->process();
$parser
->setUser($viewer)
->setMarkupEngine($engine)
->setShowEditAndReplyLinks(true)
->setCanMarkDone($can_mark)
->setObjectOwnerPHID($object_owner_phid)
->setRange($range_s, $range_e)
->setMask($mask);
if ($request->isAjax()) {
$mcov = $parser->renderModifiedCoverage();
$coverage = array(
'differential-mcoverage-'.md5($changeset->getFilename()) => $mcov,
);
return id(new PhabricatorChangesetResponse())
->setRenderedChangeset($parser->renderChangeset())
->setCoverage($coverage)
->setUndoTemplates($parser->getRenderer()->renderUndoTemplates());
}
$detail = id(new DifferentialChangesetListView())
->setUser($this->getViewer())
->setChangesets(array($changeset))
->setVisibleChangesets(array($changeset))
->setRenderingReferences(array($rendering_reference))
->setRenderURI('/differential/changeset/')
->setDiff($diff)
->setTitle(pht('Standalone View'))
->setParser($parser);
if ($revision_id) {
$detail->setInlineCommentControllerURI(
'/differential/comment/inline/edit/'.$revision_id.'/');
}
$crumbs = $this->buildApplicationCrumbs();
if ($revision_id) {
$crumbs->addTextCrumb('D'.$revision_id, '/D'.$revision_id);
}
$diff_id = $diff->getID();
if ($diff_id) {
$crumbs->addTextCrumb(
pht('Diff %d', $diff_id),
$this->getApplicationURI('diff/'.$diff_id));
}
$crumbs->addTextCrumb($changeset->getDisplayFilename());
return $this->buildApplicationPage(
array(
$crumbs,
$detail,
),
array(
'title' => pht('Changeset View'),
'device' => false,
));
}
private function buildRawFileResponse(
DifferentialChangeset $changeset,
$is_new) {
$viewer = $this->getViewer();
if ($is_new) {
$key = 'raw:new:phid';
} else {
$key = 'raw:old:phid';
}
$metadata = $changeset->getMetadata();
$file = null;
$phid = idx($metadata, $key);
if ($phid) {
$file = id(new PhabricatorFileQuery())
->setViewer($viewer)
->withPHIDs(array($phid))
->execute();
if ($file) {
$file = head($file);
}
}
if (!$file) {
// This is just building a cache of the changeset content in the file
// tool, and is safe to run on a read pathway.
$unguard = AphrontWriteGuard::beginScopedUnguardedWrites();
if ($is_new) {
$data = $changeset->makeNewFile();
} else {
$data = $changeset->makeOldFile();
}
$file = PhabricatorFile::newFromFileData(
$data,
array(
'name' => $changeset->getFilename(),
'mime-type' => 'text/plain',
));
$metadata[$key] = $file->getPHID();
$changeset->setMetadata($metadata);
$changeset->save();
unset($unguard);
}
return $file->getRedirectResponse();
}
private function buildLintInlineComments($changeset) {
$lint = id(new DifferentialDiffProperty())->loadOneWhere(
'diffID = %d AND name = %s',
$changeset->getDiffID(),
'arc:lint');
if (!$lint) {
return array();
}
$lint = $lint->getData();
$inlines = array();
foreach ($lint as $msg) {
if ($msg['path'] != $changeset->getFilename()) {
continue;
}
$inline = new DifferentialInlineComment();
$inline->setChangesetID($changeset->getID());
$inline->setIsNewFile(1);
- $inline->setSyntheticAuthor('Lint: '.$msg['name']);
+ $inline->setSyntheticAuthor(pht('Lint: %s', $msg['name']));
$inline->setLineNumber($msg['line']);
$inline->setLineLength(0);
$inline->setContent('%%%'.$msg['description'].'%%%');
$inlines[] = $inline;
}
return $inlines;
}
}
diff --git a/src/applications/differential/controller/DifferentialDiffCreateController.php b/src/applications/differential/controller/DifferentialDiffCreateController.php
index 7781f2202..1eb4f88b1 100644
--- a/src/applications/differential/controller/DifferentialDiffCreateController.php
+++ b/src/applications/differential/controller/DifferentialDiffCreateController.php
@@ -1,210 +1,210 @@
<?php
final class DifferentialDiffCreateController extends DifferentialController {
public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
// If we're on the "Update Diff" workflow, load the revision we're going
// to update.
$revision = null;
$revision_id = $request->getURIData('revisionID');
if ($revision_id) {
$revision = id(new DifferentialRevisionQuery())
->setViewer($viewer)
->withIDs(array($revision_id))
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
if (!$revision) {
return new Aphront404Response();
}
}
$diff = null;
// This object is just for policy stuff
$diff_object = DifferentialDiff::initializeNewDiff($viewer);
$repository_phid = null;
$errors = array();
$e_diff = null;
$e_file = null;
$validation_exception = null;
if ($request->isFormPost()) {
$repository_tokenizer = $request->getArr(
id(new DifferentialRepositoryField())->getFieldKey());
if ($repository_tokenizer) {
$repository_phid = reset($repository_tokenizer);
}
if ($request->getFileExists('diff-file')) {
$diff = PhabricatorFile::readUploadedFileData($_FILES['diff-file']);
} else {
$diff = $request->getStr('diff');
}
if (!strlen($diff)) {
$errors[] = pht(
'You can not create an empty diff. Paste a diff or upload a '.
'file containing a diff.');
$e_diff = pht('Required');
$e_file = pht('Required');
}
if (!$errors) {
try {
$call = new ConduitCall(
'differential.createrawdiff',
array(
'diff' => $diff,
'repositoryPHID' => $repository_phid,
'viewPolicy' => $request->getStr('viewPolicy'),
));
$call->setUser($viewer);
$result = $call->execute();
$diff_id = $result['id'];
$uri = $this->getApplicationURI("diff/{$diff_id}/");
$uri = new PhutilURI($uri);
if ($revision) {
$uri->setQueryParam('revisionID', $revision->getID());
}
return id(new AphrontRedirectResponse())->setURI($uri);
} catch (PhabricatorApplicationTransactionValidationException $ex) {
$validation_exception = $ex;
}
}
}
$form = new AphrontFormView();
$arcanist_href = PhabricatorEnv::getDoclink('Arcanist User Guide');
$arcanist_link = phutil_tag(
'a',
array(
'href' => $arcanist_href,
'target' => '_blank',
),
- 'Learn More');
+ pht('Learn More'));
$cancel_uri = $this->getApplicationURI();
$policies = id(new PhabricatorPolicyQuery())
->setViewer($viewer)
->setObject($diff_object)
->execute();
$info_view = null;
if (!$request->isFormPost()) {
$info_view = id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_NOTICE)
->setErrors(
array(
array(
pht(
'The best way to create a diff is to use the Arcanist '.
'command-line tool.'),
' ',
$arcanist_link,
),
pht(
'You can also paste a diff below, or upload a file '.
'containing a diff (for example, from %s, %s or %s).',
phutil_tag('tt', array(), 'svn diff'),
phutil_tag('tt', array(), 'git diff'),
phutil_tag('tt', array(), 'hg diff --git')),
));
}
if ($revision) {
$title = pht('Update Diff');
$header = pht('Update Diff');
$button = pht('Continue');
} else {
$title = pht('Create Diff');
$header = pht('Create New Diff');
$button = pht('Create Diff');
}
$form
->setEncType('multipart/form-data')
->setUser($viewer);
if ($revision) {
$form->appendChild(
id(new AphrontFormMarkupControl())
->setLabel(pht('Updating Revision'))
->setValue($viewer->renderHandle($revision->getPHID())));
}
if ($repository_phid) {
$repository_value = array($repository_phid);
} else {
$repository_value = array();
}
$form
->appendChild(
id(new AphrontFormTextAreaControl())
->setLabel(pht('Raw Diff'))
->setName('diff')
->setValue($diff)
->setHeight(AphrontFormTextAreaControl::HEIGHT_VERY_TALL)
->setError($e_diff))
->appendChild(
id(new AphrontFormFileControl())
->setLabel(pht('Raw Diff From File'))
->setName('diff-file')
->setError($e_file))
->appendControl(
id(new AphrontFormTokenizerControl())
->setName(id(new DifferentialRepositoryField())->getFieldKey())
->setLabel(pht('Repository'))
->setDatasource(new DiffusionRepositoryDatasource())
->setValue($repository_value)
->setLimit(1))
->appendChild(
id(new AphrontFormPolicyControl())
->setUser($viewer)
->setName('viewPolicy')
->setPolicyObject($diff_object)
->setPolicies($policies)
->setCapability(PhabricatorPolicyCapability::CAN_VIEW))
->appendChild(
id(new AphrontFormSubmitControl())
->addCancelButton($cancel_uri)
->setValue($button));
$form_box = id(new PHUIObjectBoxView())
->setHeaderText($header)
->setValidationException($validation_exception)
->setForm($form)
->setFormErrors($errors);
if ($info_view) {
$form_box->setInfoView($info_view);
}
$crumbs = $this->buildApplicationCrumbs();
if ($revision) {
$crumbs->addTextCrumb(
$revision->getMonogram(),
'/'.$revision->getMonogram());
}
$crumbs->addTextCrumb($title);
return $this->buildApplicationPage(
array(
$crumbs,
$form_box,
),
array(
'title' => $title,
));
}
}
diff --git a/src/applications/differential/controller/DifferentialInlineCommentEditController.php b/src/applications/differential/controller/DifferentialInlineCommentEditController.php
index dcb48361d..13aac1776 100644
--- a/src/applications/differential/controller/DifferentialInlineCommentEditController.php
+++ b/src/applications/differential/controller/DifferentialInlineCommentEditController.php
@@ -1,155 +1,155 @@
<?php
final class DifferentialInlineCommentEditController
extends PhabricatorInlineCommentController {
private function getRevisionID() {
return $this->getRequest()->getURIData('id');
}
private function loadRevision() {
$viewer = $this->getViewer();
$revision_id = $this->getRevisionID();
$revision = id(new DifferentialRevisionQuery())
->setViewer($viewer)
->withIDs(array($revision_id))
->executeOne();
if (!$revision) {
throw new Exception(pht('Invalid revision ID "%s".', $revision_id));
}
return $revision;
}
protected function createComment() {
// Verify revision and changeset correspond to actual objects.
$changeset_id = $this->getChangesetID();
$revision = $this->loadRevision();
if (!id(new DifferentialChangeset())->load($changeset_id)) {
- throw new Exception('Invalid changeset ID!');
+ throw new Exception(pht('Invalid changeset ID!'));
}
return id(new DifferentialInlineComment())
->setRevision($revision)
->setChangesetID($changeset_id);
}
protected function loadComment($id) {
return id(new DifferentialInlineCommentQuery())
->setViewer($this->getViewer())
->withIDs(array($id))
->withDeletedDrafts(true)
->executeOne();
}
protected function loadCommentByPHID($phid) {
return id(new DifferentialInlineCommentQuery())
->setViewer($this->getViewer())
->withPHIDs(array($phid))
->withDeletedDrafts(true)
->executeOne();
}
protected function loadCommentForEdit($id) {
$request = $this->getRequest();
$user = $request->getUser();
$inline = $this->loadComment($id);
if (!$this->canEditInlineComment($user, $inline)) {
- throw new Exception('That comment is not editable!');
+ throw new Exception(pht('That comment is not editable!'));
}
return $inline;
}
protected function loadCommentForDone($id) {
$request = $this->getRequest();
$viewer = $request->getUser();
$inline = $this->loadComment($id);
if (!$inline) {
throw new Exception(pht('Unable to load inline "%d".', $id));
}
$changeset = id(new DifferentialChangesetQuery())
->setViewer($viewer)
->withIDs(array($inline->getChangesetID()))
->executeOne();
if (!$changeset) {
throw new Exception(pht('Unable to load changeset.'));
}
$diff = id(new DifferentialDiffQuery())
->setViewer($viewer)
->withIDs(array($changeset->getDiffID()))
->executeOne();
if (!$diff) {
throw new Exception(pht('Unable to load diff.'));
}
$revision = id(new DifferentialRevisionQuery())
->setViewer($viewer)
->withIDs(array($diff->getRevisionID()))
->executeOne();
if (!$revision) {
throw new Exception(pht('Unable to load revision.'));
}
if ($revision->getAuthorPHID() !== $viewer->getPHID()) {
throw new Exception(pht('You are not the revision owner.'));
}
return $inline;
}
private function canEditInlineComment(
PhabricatorUser $user,
DifferentialInlineComment $inline) {
// Only the author may edit a comment.
if ($inline->getAuthorPHID() != $user->getPHID()) {
return false;
}
// Saved comments may not be edited, for now, although the schema now
// supports it.
if (!$inline->isDraft()) {
return false;
}
// Inline must be attached to the active revision.
if ($inline->getRevisionID() != $this->getRevisionID()) {
return false;
}
return true;
}
protected function deleteComment(PhabricatorInlineCommentInterface $inline) {
$inline->openTransaction();
DifferentialDraft::deleteHasDraft(
$inline->getAuthorPHID(),
$inline->getRevisionPHID(),
$inline->getPHID());
$inline->delete();
$inline->saveTransaction();
}
protected function saveComment(PhabricatorInlineCommentInterface $inline) {
$inline->openTransaction();
$inline->save();
DifferentialDraft::markHasDraft(
$inline->getAuthorPHID(),
$inline->getRevisionPHID(),
$inline->getPHID());
$inline->saveTransaction();
}
protected function loadObjectOwnerPHID(
PhabricatorInlineCommentInterface $inline) {
return $this->loadRevision()->getAuthorPHID();
}
}
diff --git a/src/applications/differential/controller/DifferentialRevisionCloseDetailsController.php b/src/applications/differential/controller/DifferentialRevisionCloseDetailsController.php
index 0f797d94d..25051bcbb 100644
--- a/src/applications/differential/controller/DifferentialRevisionCloseDetailsController.php
+++ b/src/applications/differential/controller/DifferentialRevisionCloseDetailsController.php
@@ -1,115 +1,117 @@
<?php
final class DifferentialRevisionCloseDetailsController
extends DifferentialController {
private $phid;
public function willProcessRequest(array $data) {
$this->phid = idx($data, 'phid');
}
public function processRequest() {
$request = $this->getRequest();
$viewer = $request->getUser();
$xaction_phid = $this->phid;
$xaction = id(new PhabricatorObjectQuery())
->withPHIDs(array($xaction_phid))
->setViewer($viewer)
->executeOne();
if (!$xaction) {
return new Aphront404Response();
}
$obj_phid = $xaction->getObjectPHID();
$obj_handle = id(new PhabricatorHandleQuery())
->setViewer($viewer)
->withPHIDs(array($obj_phid))
->executeOne();
$body = $this->getRevisionMatchExplanation(
$xaction->getMetadataValue('revisionMatchData'),
$obj_handle);
$dialog = id(new AphrontDialogView())
->setUser($viewer)
->setTitle(pht('Commit Close Explanation'))
->appendParagraph($body)
->addCancelButton($obj_handle->getURI());
return id(new AphrontDialogResponse())->setDialog($dialog);
}
private function getRevisionMatchExplanation(
$revision_match_data,
PhabricatorObjectHandle $obj_handle) {
if (!$revision_match_data) {
return pht(
'This commit was made before this feature was built and thus this '.
'information is unavailable.');
}
$body_why = array();
if ($revision_match_data['usedURI']) {
return pht(
- 'We found a "Differential Revision" field with value "%s" in the '.
- 'commit message, and the domain on the URI matches this install, so '.
+ 'We found a "%s" field with value "%s" in the commit message, '.
+ 'and the domain on the URI matches this install, so '.
'we linked this commit to %s.',
+ 'Differential Revision',
$revision_match_data['foundURI'],
phutil_tag(
'a',
array(
'href' => $obj_handle->getURI(),
),
$obj_handle->getName()));
} else if ($revision_match_data['foundURI']) {
$body_why[] = pht(
- 'We found a "Differential Revision" field with value "%s" in the '.
- 'commit message, but the domain on this URI did not match the '.
- 'configured domain for this install, "%s", so we ignored it under '.
+ 'We found a "%s" field with value "%s" in the commit message, '.
+ 'but the domain on this URI did not match the configured '.
+ 'domain for this install, "%s", so we ignored it under '.
'the assumption that it refers to some third-party revision.',
+ 'Differential Revision',
$revision_match_data['foundURI'],
$revision_match_data['validDomain']);
} else {
$body_why[] = pht(
- 'We didn\'t find a "Differential Revision" field in the commit '.
- 'message.');
+ 'We didn\'t find a "%s" field in the commit message.',
+ 'Differential Revision');
}
switch ($revision_match_data['matchHashType']) {
case ArcanistDifferentialRevisionHash::HASH_GIT_TREE:
$hash_info = true;
$hash_type = 'tree';
break;
case ArcanistDifferentialRevisionHash::HASH_GIT_COMMIT:
case ArcanistDifferentialRevisionHash::HASH_MERCURIAL_COMMIT:
$hash_info = true;
$hash_type = 'commit';
break;
default:
$hash_info = false;
break;
}
if ($hash_info) {
$diff_link = phutil_tag(
'a',
array(
'href' => $obj_handle->getURI(),
),
$obj_handle->getName());
$body_why = pht(
'This commit and the active diff of %s had the same %s hash '.
'(%s) so we linked this commit to %s.',
$diff_link,
$hash_type,
$revision_match_data['matchHashValue'],
$diff_link);
}
return phutil_implode_html("\n", $body_why);
}
}
diff --git a/src/applications/differential/controller/DifferentialRevisionEditController.php b/src/applications/differential/controller/DifferentialRevisionEditController.php
index ea2669345..1c664f07d 100644
--- a/src/applications/differential/controller/DifferentialRevisionEditController.php
+++ b/src/applications/differential/controller/DifferentialRevisionEditController.php
@@ -1,210 +1,211 @@
<?php
final class DifferentialRevisionEditController
extends DifferentialController {
private $id;
public function willProcessRequest(array $data) {
$this->id = idx($data, 'id');
}
public function processRequest() {
$request = $this->getRequest();
$viewer = $request->getUser();
if (!$this->id) {
$this->id = $request->getInt('revisionID');
}
if ($this->id) {
$revision = id(new DifferentialRevisionQuery())
->setViewer($viewer)
->withIDs(array($this->id))
->needRelationships(true)
->needReviewerStatus(true)
->needActiveDiffs(true)
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
if (!$revision) {
return new Aphront404Response();
}
} else {
$revision = DifferentialRevision::initializeNewRevision($viewer);
$revision->attachReviewerStatus(array());
}
$diff_id = $request->getInt('diffID');
if ($diff_id) {
$diff = id(new DifferentialDiffQuery())
->setViewer($viewer)
->withIDs(array($diff_id))
->executeOne();
if (!$diff) {
return new Aphront404Response();
}
if ($diff->getRevisionID()) {
// TODO: Redirect?
- throw new Exception('This diff is already attached to a revision!');
+ throw new Exception(
+ pht('This diff is already attached to a revision!'));
}
} else {
$diff = null;
}
if (!$diff) {
if (!$revision->getID()) {
throw new Exception(
pht('You can not create a new revision without a diff!'));
}
} else {
// TODO: It would be nice to show the diff being attached in the UI.
}
$field_list = PhabricatorCustomField::getObjectFields(
$revision,
PhabricatorCustomField::ROLE_EDIT);
$field_list
->setViewer($viewer)
->readFieldsFromStorage($revision);
if ($request->getStr('viaDiffView') && $diff) {
$repo_key = id(new DifferentialRepositoryField())->getFieldKey();
$repository_field = idx(
$field_list->getFields(),
$repo_key);
if ($repository_field) {
$repository_field->setValue($request->getStr($repo_key));
}
$view_policy_key = id(new DifferentialViewPolicyField())->getFieldKey();
$view_policy_field = idx(
$field_list->getFields(),
$view_policy_key);
if ($view_policy_field) {
$view_policy_field->setValue($diff->getViewPolicy());
}
}
$validation_exception = null;
if ($request->isFormPost() && !$request->getStr('viaDiffView')) {
$editor = id(new DifferentialTransactionEditor())
->setActor($viewer)
->setContentSourceFromRequest($request)
->setContinueOnNoEffect(true);
$xactions = $field_list->buildFieldTransactionsFromRequest(
new DifferentialTransaction(),
$request);
if ($diff) {
$repository_phid = null;
$repository_tokenizer = $request->getArr(
id(new DifferentialRepositoryField())->getFieldKey());
if ($repository_tokenizer) {
$repository_phid = reset($repository_tokenizer);
}
$xactions[] = id(new DifferentialTransaction())
->setTransactionType(DifferentialTransaction::TYPE_UPDATE)
->setNewValue($diff->getPHID());
$editor->setRepositoryPHIDOverride($repository_phid);
}
$comments = $request->getStr('comments');
if (strlen($comments)) {
$xactions[] = id(new DifferentialTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_COMMENT)
->attachComment(
id(new DifferentialTransactionComment())
->setContent($comments));
}
try {
$editor->applyTransactions($revision, $xactions);
$revision_uri = '/D'.$revision->getID();
return id(new AphrontRedirectResponse())->setURI($revision_uri);
} catch (PhabricatorApplicationTransactionValidationException $ex) {
$validation_exception = $ex;
}
}
$form = new AphrontFormView();
$form->setUser($request->getUser());
if ($diff) {
$form->addHiddenInput('diffID', $diff->getID());
}
if ($revision->getID()) {
$form->setAction('/differential/revision/edit/'.$revision->getID().'/');
} else {
$form->setAction('/differential/revision/edit/');
}
if ($diff && $revision->getID()) {
$form
->appendChild(
id(new AphrontFormTextAreaControl())
->setLabel(pht('Comments'))
->setName('comments')
->setCaption(pht("Explain what's new in this diff."))
->setValue($request->getStr('comments')))
->appendChild(
id(new AphrontFormSubmitControl())
->setValue(pht('Save')))
->appendChild(
id(new AphrontFormDividerControl()));
}
$field_list->appendFieldsToForm($form);
$submit = id(new AphrontFormSubmitControl())
->setValue('Save');
if ($diff) {
$submit->addCancelButton('/differential/diff/'.$diff->getID().'/');
} else {
$submit->addCancelButton('/D'.$revision->getID());
}
$form->appendChild($submit);
$crumbs = $this->buildApplicationCrumbs();
if ($revision->getID()) {
if ($diff) {
$title = pht('Update Differential Revision');
$crumbs->addTextCrumb(
'D'.$revision->getID(),
'/differential/diff/'.$diff->getID().'/');
} else {
$title = pht('Edit Differential Revision');
$crumbs->addTextCrumb(
'D'.$revision->getID(),
'/D'.$revision->getID());
}
} else {
$title = pht('Create New Differential Revision');
}
$form_box = id(new PHUIObjectBoxView())
->setHeaderText($title)
->setValidationException($validation_exception)
->setForm($form);
$crumbs->addTextCrumb($title);
return $this->buildApplicationPage(
array(
$crumbs,
$form_box,
),
array(
'title' => $title,
));
}
}
diff --git a/src/applications/differential/controller/DifferentialRevisionLandController.php b/src/applications/differential/controller/DifferentialRevisionLandController.php
index 1c8ed0e9d..e19762b44 100644
--- a/src/applications/differential/controller/DifferentialRevisionLandController.php
+++ b/src/applications/differential/controller/DifferentialRevisionLandController.php
@@ -1,163 +1,165 @@
<?php
final class DifferentialRevisionLandController extends DifferentialController {
private $revisionID;
private $strategyClass;
private $pushStrategy;
public function willProcessRequest(array $data) {
$this->revisionID = $data['id'];
$this->strategyClass = $data['strategy'];
}
public function processRequest() {
$request = $this->getRequest();
$viewer = $request->getUser();
$revision_id = $this->revisionID;
$revision = id(new DifferentialRevisionQuery())
->withIDs(array($revision_id))
->setViewer($viewer)
->executeOne();
if (!$revision) {
return new Aphront404Response();
}
if (is_subclass_of($this->strategyClass, 'DifferentialLandingStrategy')) {
$this->pushStrategy = newv($this->strategyClass, array());
} else {
throw new Exception(
- "Strategy type must be a valid class name and must subclass ".
- "DifferentialLandingStrategy. ".
- "'{$this->strategyClass}' is not a subclass of ".
- "DifferentialLandingStrategy.");
+ pht(
+ "Strategy type must be a valid class name and must subclass ".
+ "%s. '%s' is not a subclass of %s",
+ 'DifferentialLandingStrategy',
+ $this->strategyClass,
+ 'DifferentialLandingStrategy'));
}
if ($request->isDialogFormPost()) {
$response = null;
$text = '';
try {
$response = $this->attemptLand($revision, $request);
$title = pht('Success!');
$text = pht('Revision was successfully landed.');
} catch (Exception $ex) {
$title = pht('Failed to land revision');
if ($ex instanceof PhutilProxyException) {
$text = hsprintf(
'%s:<br><pre>%s</pre>',
$ex->getMessage(),
$ex->getPreviousException()->getMessage());
} else {
$text = phutil_tag('pre', array(), $ex->getMessage());
}
$text = id(new PHUIInfoView())
->appendChild($text);
}
if ($response instanceof AphrontDialogView) {
$dialog = $response;
} else {
$dialog = id(new AphrontDialogView())
->setUser($viewer)
->setTitle($title)
->appendChild(phutil_tag('p', array(), $text))
->addCancelButton('/D'.$revision_id, pht('Done'));
}
return id(new AphrontDialogResponse())->setDialog($dialog);
}
$is_disabled = $this->pushStrategy->isActionDisabled(
$viewer,
$revision,
$revision->getRepository());
if ($is_disabled) {
if (is_string($is_disabled)) {
$explain = $is_disabled;
} else {
$explain = pht('This action is not currently enabled.');
}
$dialog = id(new AphrontDialogView())
->setUser($viewer)
->setTitle(pht("Can't land revision"))
->appendChild($explain)
->addCancelButton('/D'.$revision_id);
return id(new AphrontDialogResponse())->setDialog($dialog);
}
$prompt = hsprintf('%s<br><br>%s',
pht(
'This will squash and rebase revision %s, and push it to '.
- 'the default / master branch.',
+ 'the default / master branch.',
$revision_id),
pht('It is an experimental feature and may not work.'));
$dialog = id(new AphrontDialogView())
->setUser($viewer)
->setTitle(pht('Land Revision %s?', $revision_id))
->appendChild($prompt)
->setSubmitURI($request->getRequestURI())
->addSubmitButton(pht('Land it!'))
->addCancelButton('/D'.$revision_id);
return id(new AphrontDialogResponse())->setDialog($dialog);
}
private function attemptLand($revision, $request) {
$status = $revision->getStatus();
if ($status != ArcanistDifferentialRevisionStatus::ACCEPTED) {
- throw new Exception('Only Accepted revisions can be landed.');
+ throw new Exception(pht('Only Accepted revisions can be landed.'));
}
$repository = $revision->getRepository();
if ($repository === null) {
- throw new Exception('revision is not attached to a repository.');
+ throw new Exception(pht('Revision is not attached to a repository.'));
}
$can_push = PhabricatorPolicyFilter::hasCapability(
$request->getUser(),
$repository,
DiffusionPushCapability::CAPABILITY);
if (!$can_push) {
throw new Exception(
pht('You do not have permission to push to this repository.'));
}
$lock = $this->lockRepository($repository);
try {
$response = $this->pushStrategy->processLandRequest(
$request,
$revision,
$repository);
} catch (Exception $e) {
$lock->unlock();
throw $e;
}
$lock->unlock();
$looksoon = new ConduitCall(
'diffusion.looksoon',
array(
'callsigns' => array($repository->getCallsign()),
));
$looksoon->setUser($request->getUser());
$looksoon->execute();
return $response;
}
private function lockRepository($repository) {
$lock_name = __CLASS__.':'.($repository->getCallsign());
$lock = PhabricatorGlobalLock::newLock($lock_name);
$lock->lock();
return $lock;
}
}
diff --git a/src/applications/differential/controller/DifferentialRevisionViewController.php b/src/applications/differential/controller/DifferentialRevisionViewController.php
index cdefdd65d..312d4a7f4 100644
--- a/src/applications/differential/controller/DifferentialRevisionViewController.php
+++ b/src/applications/differential/controller/DifferentialRevisionViewController.php
@@ -1,976 +1,976 @@
<?php
final class DifferentialRevisionViewController extends DifferentialController {
private $revisionID;
public function shouldAllowPublic() {
return true;
}
public function willProcessRequest(array $data) {
$this->revisionID = $data['id'];
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$viewer_is_anonymous = !$user->isLoggedIn();
$revision = id(new DifferentialRevisionQuery())
->withIDs(array($this->revisionID))
->setViewer($request->getUser())
->needRelationships(true)
->needReviewerStatus(true)
->needReviewerAuthority(true)
->executeOne();
if (!$revision) {
return new Aphront404Response();
}
$diffs = id(new DifferentialDiffQuery())
->setViewer($request->getUser())
->withRevisionIDs(array($this->revisionID))
->execute();
$diffs = array_reverse($diffs, $preserve_keys = true);
if (!$diffs) {
throw new Exception(
- 'This revision has no diffs. Something has gone quite wrong.');
+ pht('This revision has no diffs. Something has gone quite wrong.'));
}
$revision->attachActiveDiff(last($diffs));
$diff_vs = $request->getInt('vs');
$target_id = $request->getInt('id');
$target = idx($diffs, $target_id, end($diffs));
$target_manual = $target;
if (!$target_id) {
foreach ($diffs as $diff) {
if ($diff->getCreationMethod() != 'commit') {
$target_manual = $diff;
}
}
}
if (empty($diffs[$diff_vs])) {
$diff_vs = null;
}
$repository = null;
$repository_phid = $target->getRepositoryPHID();
if ($repository_phid) {
if ($repository_phid == $revision->getRepositoryPHID()) {
$repository = $revision->getRepository();
} else {
$repository = id(new PhabricatorRepositoryQuery())
->setViewer($user)
->withPHIDs(array($repository_phid))
->executeOne();
}
}
list($changesets, $vs_map, $vs_changesets, $rendering_references) =
$this->loadChangesetsAndVsMap(
$target,
idx($diffs, $diff_vs),
$repository);
if ($request->getExists('download')) {
return $this->buildRawDiffResponse(
$revision,
$changesets,
$vs_changesets,
$vs_map,
$repository);
}
$map = $vs_map;
if (!$map) {
$map = array_fill_keys(array_keys($changesets), 0);
}
$old_ids = array();
$new_ids = array();
foreach ($map as $id => $vs) {
if ($vs <= 0) {
$old_ids[] = $id;
$new_ids[] = $id;
} else {
$new_ids[] = $id;
$new_ids[] = $vs;
}
}
$props = id(new DifferentialDiffProperty())->loadAllWhere(
'diffID = %d',
$target_manual->getID());
$props = mpull($props, 'getData', 'getName');
$object_phids = array_merge(
$revision->getReviewers(),
$revision->getCCPHIDs(),
$revision->loadCommitPHIDs(),
array(
$revision->getAuthorPHID(),
$user->getPHID(),
));
foreach ($revision->getAttached() as $type => $phids) {
foreach ($phids as $phid => $info) {
$object_phids[] = $phid;
}
}
$field_list = PhabricatorCustomField::getObjectFields(
$revision,
PhabricatorCustomField::ROLE_VIEW);
$field_list->setViewer($user);
$field_list->readFieldsFromStorage($revision);
$warning_handle_map = array();
foreach ($field_list->getFields() as $key => $field) {
$req = $field->getRequiredHandlePHIDsForRevisionHeaderWarnings();
foreach ($req as $phid) {
$warning_handle_map[$key][] = $phid;
$object_phids[] = $phid;
}
}
$handles = $this->loadViewerHandles($object_phids);
$request_uri = $request->getRequestURI();
$limit = 100;
$large = $request->getStr('large');
if (count($changesets) > $limit && !$large) {
$count = count($changesets);
$warning = new PHUIInfoView();
- $warning->setTitle('Very Large Diff');
+ $warning->setTitle(pht('Very Large Diff'));
$warning->setSeverity(PHUIInfoView::SEVERITY_WARNING);
$warning->appendChild(hsprintf(
'%s <strong>%s</strong>',
pht(
'This diff is very large and affects %s files. '.
'You may load each file individually or ',
new PhutilNumber($count)),
phutil_tag(
'a',
array(
'class' => 'button grey',
'href' => $request_uri
->alter('large', 'true')
->setFragment('toc'),
),
pht('Show All Files Inline'))));
$warning = $warning->render();
$old = array_select_keys($changesets, $old_ids);
$new = array_select_keys($changesets, $new_ids);
$query = id(new DifferentialInlineCommentQuery())
->setViewer($user)
->withRevisionPHIDs(array($revision->getPHID()));
$inlines = $query->execute();
$inlines = $query->adjustInlinesForChangesets(
$inlines,
$old,
$new,
$revision);
$visible_changesets = array();
foreach ($inlines as $inline) {
$changeset_id = $inline->getChangesetID();
if (isset($changesets[$changeset_id])) {
$visible_changesets[$changeset_id] = $changesets[$changeset_id];
}
}
if (!empty($props['arc:lint'])) {
$changeset_paths = mpull($changesets, null, 'getFilename');
foreach ($props['arc:lint'] as $lint) {
$changeset = idx($changeset_paths, $lint['path']);
if ($changeset) {
$visible_changesets[$changeset->getID()] = $changeset;
}
}
}
} else {
$warning = null;
$visible_changesets = $changesets;
}
// TODO: This should be in a DiffQuery or similar.
$need_props = array();
foreach ($field_list->getFields() as $field) {
foreach ($field->getRequiredDiffPropertiesForRevisionView() as $prop) {
$need_props[$prop] = $prop;
}
}
if ($need_props) {
$prop_diff = $revision->getActiveDiff();
$load_props = id(new DifferentialDiffProperty())->loadAllWhere(
'diffID = %d AND name IN (%Ls)',
$prop_diff->getID(),
$need_props);
$load_props = mpull($load_props, 'getData', 'getName');
foreach ($need_props as $need) {
$prop_diff->attachProperty($need, idx($load_props, $need));
}
}
$commit_hashes = mpull($diffs, 'getSourceControlBaseRevision');
$local_commits = idx($props, 'local:commits', array());
foreach ($local_commits as $local_commit) {
$commit_hashes[] = idx($local_commit, 'tree');
$commit_hashes[] = idx($local_commit, 'local');
}
$commit_hashes = array_unique(array_filter($commit_hashes));
if ($commit_hashes) {
$commits_for_links = id(new DiffusionCommitQuery())
->setViewer($user)
->withIdentifiers($commit_hashes)
->execute();
$commits_for_links = mpull(
$commits_for_links,
null,
'getCommitIdentifier');
} else {
$commits_for_links = array();
}
$revision_detail = id(new DifferentialRevisionDetailView())
->setUser($user)
->setRevision($revision)
->setDiff(end($diffs))
->setCustomFields($field_list)
->setURI($request->getRequestURI());
$actions = $this->getRevisionActions($revision);
$whitespace = $request->getStr(
'whitespace',
DifferentialChangesetParser::WHITESPACE_IGNORE_MOST);
$repository = $revision->getRepository();
if ($repository) {
$symbol_indexes = $this->buildSymbolIndexes(
$repository,
$visible_changesets);
} else {
$symbol_indexes = array();
}
$revision_detail->setActions($actions);
$revision_detail->setUser($user);
$revision_detail_box = $revision_detail->render();
$revision_warnings = $this->buildRevisionWarnings(
$revision,
$field_list,
$warning_handle_map,
$handles);
if ($revision_warnings) {
$revision_warnings = id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_WARNING)
->setErrors($revision_warnings);
$revision_detail_box->setInfoView($revision_warnings);
}
$comment_view = $this->buildTransactions(
$revision,
$diff_vs ? $diffs[$diff_vs] : $target,
$target,
$old_ids,
$new_ids);
if (!$viewer_is_anonymous) {
$comment_view->setQuoteRef('D'.$revision->getID());
$comment_view->setQuoteTargetID('comment-content');
}
$wrap_id = celerity_generate_unique_node_id();
$comment_view = phutil_tag(
'div',
array(
'id' => $wrap_id,
),
$comment_view);
$changeset_view = new DifferentialChangesetListView();
$changeset_view->setChangesets($changesets);
$changeset_view->setVisibleChangesets($visible_changesets);
if (!$viewer_is_anonymous) {
$changeset_view->setInlineCommentControllerURI(
'/differential/comment/inline/edit/'.$revision->getID().'/');
}
$changeset_view->setStandaloneURI('/differential/changeset/');
$changeset_view->setRawFileURIs(
'/differential/changeset/?view=old',
'/differential/changeset/?view=new');
$changeset_view->setUser($user);
$changeset_view->setDiff($target);
$changeset_view->setRenderingReferences($rendering_references);
$changeset_view->setVsMap($vs_map);
$changeset_view->setWhitespace($whitespace);
if ($repository) {
$changeset_view->setRepository($repository);
}
$changeset_view->setSymbolIndexes($symbol_indexes);
- $changeset_view->setTitle('Diff '.$target->getID());
+ $changeset_view->setTitle(pht('Diff %s', $target->getID()));
$diff_history = id(new DifferentialRevisionUpdateHistoryView())
->setUser($user)
->setDiffs($diffs)
->setSelectedVersusDiffID($diff_vs)
->setSelectedDiffID($target->getID())
->setSelectedWhitespace($whitespace)
->setCommitsForLinks($commits_for_links);
$local_view = id(new DifferentialLocalCommitsView())
->setUser($user)
->setLocalCommits(idx($props, 'local:commits'))
->setCommitsForLinks($commits_for_links);
if ($repository) {
$other_revisions = $this->loadOtherRevisions(
$changesets,
$target,
$repository);
} else {
$other_revisions = array();
}
$other_view = null;
if ($other_revisions) {
$other_view = $this->renderOtherRevisions($other_revisions);
}
$toc_view = new DifferentialDiffTableOfContentsView();
$toc_view->setChangesets($changesets);
$toc_view->setVisibleChangesets($visible_changesets);
$toc_view->setRenderingReferences($rendering_references);
$toc_view->setUnitTestData(idx($props, 'arc:unit', array()));
if ($repository) {
$toc_view->setRepository($repository);
}
$toc_view->setDiff($target);
$toc_view->setUser($user);
$toc_view->setRevisionID($revision->getID());
$toc_view->setWhitespace($whitespace);
$comment_form = null;
if (!$viewer_is_anonymous) {
$draft = id(new PhabricatorDraft())->loadOneWhere(
'authorPHID = %s AND draftKey = %s',
$user->getPHID(),
'differential-comment-'.$revision->getID());
$reviewers = array();
$ccs = array();
if ($draft) {
$reviewers = idx($draft->getMetadata(), 'reviewers', array());
$ccs = idx($draft->getMetadata(), 'ccs', array());
if ($reviewers || $ccs) {
$handles = $this->loadViewerHandles(array_merge($reviewers, $ccs));
$reviewers = array_select_keys($handles, $reviewers);
$ccs = array_select_keys($handles, $ccs);
}
}
$comment_form = new DifferentialAddCommentView();
$comment_form->setRevision($revision);
$review_warnings = array();
foreach ($field_list->getFields() as $field) {
$review_warnings[] = $field->getWarningsForDetailView();
}
$review_warnings = array_mergev($review_warnings);
if ($review_warnings) {
$review_warnings_panel = id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_WARNING)
->setErrors($review_warnings);
$comment_form->setInfoView($review_warnings_panel);
}
$comment_form->setActions($this->getRevisionCommentActions($revision));
$action_uri = $this->getApplicationURI(
'comment/save/'.$revision->getID().'/');
$comment_form->setActionURI($action_uri);
$comment_form->setUser($user);
$comment_form->setDraft($draft);
$comment_form->setReviewers(mpull($reviewers, 'getFullName', 'getPHID'));
$comment_form->setCCs(mpull($ccs, 'getFullName', 'getPHID'));
// TODO: This just makes the "Z" key work. Generalize this and remove
// it at some point.
$comment_form = phutil_tag(
'div',
array(
'class' => 'differential-add-comment-panel',
),
$comment_form);
}
$pane_id = celerity_generate_unique_node_id();
Javelin::initBehavior(
'differential-keyboard-navigation',
array(
'haunt' => $pane_id,
));
Javelin::initBehavior('differential-user-select');
$page_pane = id(new DifferentialPrimaryPaneView())
->setID($pane_id)
->appendChild($comment_view);
$signatures = DifferentialRequiredSignaturesField::loadForRevision(
$revision);
$missing_signatures = false;
foreach ($signatures as $phid => $signed) {
if (!$signed) {
$missing_signatures = true;
}
}
if ($missing_signatures) {
$signature_message = id(new PHUIInfoView())
->setErrors(
array(
array(
phutil_tag('strong', array(), pht('Content Hidden:')),
' ',
pht(
'The content of this revision is hidden until the author has '.
'signed all of the required legal agreements.'),
),
));
$page_pane->appendChild($signature_message);
} else {
$page_pane->appendChild(
array(
$diff_history,
$warning,
$local_view,
$toc_view,
$other_view,
$changeset_view,
));
}
if ($comment_form) {
$page_pane->appendChild($comment_form);
} else {
// TODO: For now, just use this to get "Login to Comment".
$page_pane->appendChild(
id(new PhabricatorApplicationTransactionCommentView())
->setUser($user)
->setRequestURI($request->getRequestURI()));
}
$object_id = 'D'.$revision->getID();
$content = array(
$revision_detail_box,
$page_pane,
);
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb($object_id, '/'.$object_id);
$prefs = $user->loadPreferences();
$pref_filetree = PhabricatorUserPreferences::PREFERENCE_DIFF_FILETREE;
if ($prefs->getPreference($pref_filetree)) {
$collapsed = $prefs->getPreference(
PhabricatorUserPreferences::PREFERENCE_NAV_COLLAPSED,
false);
$nav = id(new DifferentialChangesetFileTreeSideNavBuilder())
->setTitle('D'.$revision->getID())
->setBaseURI(new PhutilURI('/D'.$revision->getID()))
->setCollapsed((bool)$collapsed)
->build($changesets);
$nav->appendChild($content);
$nav->setCrumbs($crumbs);
$content = $nav;
} else {
array_unshift($content, $crumbs);
}
return $this->buildApplicationPage(
$content,
array(
'title' => $object_id.' '.$revision->getTitle(),
'pageObjects' => array($revision->getPHID()),
));
}
private function getRevisionActions(DifferentialRevision $revision) {
$viewer = $this->getRequest()->getUser();
$revision_id = $revision->getID();
$revision_phid = $revision->getPHID();
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$revision,
PhabricatorPolicyCapability::CAN_EDIT);
$actions = array();
$actions[] = id(new PhabricatorActionView())
->setIcon('fa-pencil')
->setHref("/differential/revision/edit/{$revision_id}/")
->setName(pht('Edit Revision'))
->setDisabled(!$can_edit)
->setWorkflow(!$can_edit);
$actions[] = id(new PhabricatorActionView())
->setIcon('fa-upload')
->setHref("/differential/revision/update/{$revision_id}/")
->setName(pht('Update Diff'))
->setDisabled(!$can_edit)
->setWorkflow(!$can_edit);
$this->requireResource('phabricator-object-selector-css');
$this->requireResource('javelin-behavior-phabricator-object-selector');
$actions[] = id(new PhabricatorActionView())
->setIcon('fa-link')
->setName(pht('Edit Dependencies'))
->setHref("/search/attach/{$revision_phid}/DREV/dependencies/")
->setWorkflow(true)
->setDisabled(!$can_edit);
$maniphest = 'PhabricatorManiphestApplication';
if (PhabricatorApplication::isClassInstalled($maniphest)) {
$actions[] = id(new PhabricatorActionView())
->setIcon('fa-anchor')
->setName(pht('Edit Maniphest Tasks'))
->setHref("/search/attach/{$revision_phid}/TASK/")
->setWorkflow(true)
->setDisabled(!$can_edit);
}
$request_uri = $this->getRequest()->getRequestURI();
$actions[] = id(new PhabricatorActionView())
->setIcon('fa-download')
->setName(pht('Download Raw Diff'))
->setHref($request_uri->alter('download', 'true'));
return $actions;
}
private function getRevisionCommentActions(DifferentialRevision $revision) {
$actions = array(
DifferentialAction::ACTION_COMMENT => true,
);
$viewer = $this->getRequest()->getUser();
$viewer_phid = $viewer->getPHID();
$viewer_is_owner = ($viewer_phid == $revision->getAuthorPHID());
$viewer_is_reviewer = in_array($viewer_phid, $revision->getReviewers());
$status = $revision->getStatus();
$viewer_has_accepted = false;
$viewer_has_rejected = false;
$status_accepted = DifferentialReviewerStatus::STATUS_ACCEPTED;
$status_rejected = DifferentialReviewerStatus::STATUS_REJECTED;
foreach ($revision->getReviewerStatus() as $reviewer) {
if ($reviewer->getReviewerPHID() == $viewer_phid) {
if ($reviewer->getStatus() == $status_accepted) {
$viewer_has_accepted = true;
}
if ($reviewer->getStatus() == $status_rejected) {
$viewer_has_rejected = true;
}
break;
}
}
$allow_self_accept = PhabricatorEnv::getEnvConfig(
'differential.allow-self-accept');
$always_allow_abandon = PhabricatorEnv::getEnvConfig(
'differential.always-allow-abandon');
$always_allow_close = PhabricatorEnv::getEnvConfig(
'differential.always-allow-close');
$allow_reopen = PhabricatorEnv::getEnvConfig(
'differential.allow-reopen');
if ($viewer_is_owner) {
switch ($status) {
case ArcanistDifferentialRevisionStatus::NEEDS_REVIEW:
$actions[DifferentialAction::ACTION_ACCEPT] = $allow_self_accept;
$actions[DifferentialAction::ACTION_ABANDON] = true;
$actions[DifferentialAction::ACTION_RETHINK] = true;
break;
case ArcanistDifferentialRevisionStatus::NEEDS_REVISION:
case ArcanistDifferentialRevisionStatus::CHANGES_PLANNED:
$actions[DifferentialAction::ACTION_ACCEPT] = $allow_self_accept;
$actions[DifferentialAction::ACTION_ABANDON] = true;
$actions[DifferentialAction::ACTION_REQUEST] = true;
break;
case ArcanistDifferentialRevisionStatus::ACCEPTED:
$actions[DifferentialAction::ACTION_ABANDON] = true;
$actions[DifferentialAction::ACTION_REQUEST] = true;
$actions[DifferentialAction::ACTION_RETHINK] = true;
$actions[DifferentialAction::ACTION_CLOSE] = true;
break;
case ArcanistDifferentialRevisionStatus::CLOSED:
break;
case ArcanistDifferentialRevisionStatus::ABANDONED:
$actions[DifferentialAction::ACTION_RECLAIM] = true;
break;
}
} else {
switch ($status) {
case ArcanistDifferentialRevisionStatus::NEEDS_REVIEW:
$actions[DifferentialAction::ACTION_ABANDON] = $always_allow_abandon;
$actions[DifferentialAction::ACTION_ACCEPT] = true;
$actions[DifferentialAction::ACTION_REJECT] = true;
$actions[DifferentialAction::ACTION_RESIGN] = $viewer_is_reviewer;
break;
case ArcanistDifferentialRevisionStatus::NEEDS_REVISION:
case ArcanistDifferentialRevisionStatus::CHANGES_PLANNED:
$actions[DifferentialAction::ACTION_ABANDON] = $always_allow_abandon;
$actions[DifferentialAction::ACTION_ACCEPT] = true;
$actions[DifferentialAction::ACTION_REJECT] = !$viewer_has_rejected;
$actions[DifferentialAction::ACTION_RESIGN] = $viewer_is_reviewer;
break;
case ArcanistDifferentialRevisionStatus::ACCEPTED:
$actions[DifferentialAction::ACTION_ABANDON] = $always_allow_abandon;
$actions[DifferentialAction::ACTION_ACCEPT] = !$viewer_has_accepted;
$actions[DifferentialAction::ACTION_REJECT] = true;
$actions[DifferentialAction::ACTION_RESIGN] = $viewer_is_reviewer;
break;
case ArcanistDifferentialRevisionStatus::CLOSED:
case ArcanistDifferentialRevisionStatus::ABANDONED:
break;
}
if ($status != ArcanistDifferentialRevisionStatus::CLOSED) {
$actions[DifferentialAction::ACTION_CLAIM] = true;
$actions[DifferentialAction::ACTION_CLOSE] = $always_allow_close;
}
}
$actions[DifferentialAction::ACTION_ADDREVIEWERS] = true;
$actions[DifferentialAction::ACTION_ADDCCS] = true;
$actions[DifferentialAction::ACTION_REOPEN] = $allow_reopen &&
($status == ArcanistDifferentialRevisionStatus::CLOSED);
$actions = array_keys(array_filter($actions));
$actions_dict = array();
foreach ($actions as $action) {
$actions_dict[$action] = DifferentialAction::getActionVerb($action);
}
return $actions_dict;
}
private function loadChangesetsAndVsMap(
DifferentialDiff $target,
DifferentialDiff $diff_vs = null,
PhabricatorRepository $repository = null) {
$load_diffs = array($target);
if ($diff_vs) {
$load_diffs[] = $diff_vs;
}
$raw_changesets = id(new DifferentialChangesetQuery())
->setViewer($this->getRequest()->getUser())
->withDiffs($load_diffs)
->execute();
$changeset_groups = mgroup($raw_changesets, 'getDiffID');
$changesets = idx($changeset_groups, $target->getID(), array());
$changesets = mpull($changesets, null, 'getID');
$refs = array();
$vs_map = array();
$vs_changesets = array();
if ($diff_vs) {
$vs_id = $diff_vs->getID();
$vs_changesets_path_map = array();
foreach (idx($changeset_groups, $vs_id, array()) as $changeset) {
$path = $changeset->getAbsoluteRepositoryPath($repository, $diff_vs);
$vs_changesets_path_map[$path] = $changeset;
$vs_changesets[$changeset->getID()] = $changeset;
}
foreach ($changesets as $key => $changeset) {
$path = $changeset->getAbsoluteRepositoryPath($repository, $target);
if (isset($vs_changesets_path_map[$path])) {
$vs_map[$changeset->getID()] =
$vs_changesets_path_map[$path]->getID();
$refs[$changeset->getID()] =
$changeset->getID().'/'.$vs_changesets_path_map[$path]->getID();
unset($vs_changesets_path_map[$path]);
} else {
$refs[$changeset->getID()] = $changeset->getID();
}
}
foreach ($vs_changesets_path_map as $path => $changeset) {
$changesets[$changeset->getID()] = $changeset;
$vs_map[$changeset->getID()] = -1;
$refs[$changeset->getID()] = $changeset->getID().'/-1';
}
} else {
foreach ($changesets as $changeset) {
$refs[$changeset->getID()] = $changeset->getID();
}
}
$changesets = msort($changesets, 'getSortKey');
return array($changesets, $vs_map, $vs_changesets, $refs);
}
private function buildSymbolIndexes(
PhabricatorRepository $repository,
array $visible_changesets) {
assert_instances_of($visible_changesets, 'DifferentialChangeset');
$engine = PhabricatorSyntaxHighlighter::newEngine();
$langs = $repository->getSymbolLanguages();
$langs = nonempty($langs, array());
$sources = $repository->getSymbolSources();
$sources = nonempty($sources, array());
$symbol_indexes = array();
if ($langs && $sources) {
$have_symbols = id(new DiffusionSymbolQuery())
->existsSymbolsInRepository($repository->getPHID());
if (!$have_symbols) {
return $symbol_indexes;
}
}
$repository_phids = array_merge(
array($repository->getPHID()),
$sources);
$indexed_langs = array_fill_keys($langs, true);
foreach ($visible_changesets as $key => $changeset) {
$lang = $engine->getLanguageFromFilename($changeset->getFilename());
if (empty($indexed_langs) || isset($indexed_langs[$lang])) {
$symbol_indexes[$key] = array(
'lang' => $lang,
'repositories' => $repository_phids,
);
}
}
return $symbol_indexes;
}
private function loadOtherRevisions(
array $changesets,
DifferentialDiff $target,
PhabricatorRepository $repository) {
assert_instances_of($changesets, 'DifferentialChangeset');
$paths = array();
foreach ($changesets as $changeset) {
$paths[] = $changeset->getAbsoluteRepositoryPath(
$repository,
$target);
}
if (!$paths) {
return array();
}
$path_map = id(new DiffusionPathIDQuery($paths))->loadPathIDs();
if (!$path_map) {
return array();
}
$recent = (PhabricatorTime::getNow() - phutil_units('30 days in seconds'));
$query = id(new DifferentialRevisionQuery())
->setViewer($this->getRequest()->getUser())
->withStatus(DifferentialRevisionQuery::STATUS_OPEN)
->withUpdatedEpochBetween($recent, null)
->setOrder(DifferentialRevisionQuery::ORDER_MODIFIED)
->setLimit(10)
->needFlags(true)
->needDrafts(true)
->needRelationships(true);
foreach ($path_map as $path => $path_id) {
$query->withPath($repository->getID(), $path_id);
}
$results = $query->execute();
// Strip out *this* revision.
foreach ($results as $key => $result) {
if ($result->getID() == $this->revisionID) {
unset($results[$key]);
}
}
return $results;
}
private function renderOtherRevisions(array $revisions) {
assert_instances_of($revisions, 'DifferentialRevision');
$viewer = $this->getViewer();
$header = id(new PHUIHeaderView())
->setHeader(pht('Similar Open Revisions'))
->setSubheader(
pht('Recently updated open revisions affecting the same files.'));
$view = id(new DifferentialRevisionListView())
->setHeader($header)
->setRevisions($revisions)
->setUser($viewer);
$phids = $view->getRequiredHandlePHIDs();
$handles = $this->loadViewerHandles($phids);
$view->setHandles($handles);
return $view;
}
/**
* Note this code is somewhat similar to the buildPatch method in
* @{class:DifferentialReviewRequestMail}.
*
* @return @{class:AphrontRedirectResponse}
*/
private function buildRawDiffResponse(
DifferentialRevision $revision,
array $changesets,
array $vs_changesets,
array $vs_map,
PhabricatorRepository $repository = null) {
assert_instances_of($changesets, 'DifferentialChangeset');
assert_instances_of($vs_changesets, 'DifferentialChangeset');
$viewer = $this->getRequest()->getUser();
id(new DifferentialHunkQuery())
->setViewer($viewer)
->withChangesets($changesets)
->needAttachToChangesets(true)
->execute();
$diff = new DifferentialDiff();
$diff->attachChangesets($changesets);
$raw_changes = $diff->buildChangesList();
$changes = array();
foreach ($raw_changes as $changedict) {
$changes[] = ArcanistDiffChange::newFromDictionary($changedict);
}
$loader = id(new PhabricatorFileBundleLoader())
->setViewer($viewer);
$bundle = ArcanistBundle::newFromChanges($changes);
$bundle->setLoadFileDataCallback(array($loader, 'loadFileData'));
$vcs = $repository ? $repository->getVersionControlSystem() : null;
switch ($vcs) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
$raw_diff = $bundle->toGitPatch();
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
default:
$raw_diff = $bundle->toUnifiedDiff();
break;
}
$request_uri = $this->getRequest()->getRequestURI();
// this ends up being something like
// D123.diff
// or the verbose
// D123.vs123.id123.whitespaceignore-all.diff
// lame but nice to include these options
$file_name = ltrim($request_uri->getPath(), '/').'.';
foreach ($request_uri->getQueryParams() as $key => $value) {
if ($key == 'download') {
continue;
}
$file_name .= $key.$value.'.';
}
$file_name .= 'diff';
$file = PhabricatorFile::buildFromFileDataOrHash(
$raw_diff,
array(
'name' => $file_name,
'ttl' => (60 * 60 * 24),
'viewPolicy' => PhabricatorPolicies::POLICY_NOONE,
));
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$file->attachToObject($revision->getPHID());
unset($unguarded);
return $file->getRedirectResponse();
}
private function buildTransactions(
DifferentialRevision $revision,
DifferentialDiff $left_diff,
DifferentialDiff $right_diff,
array $old_ids,
array $new_ids) {
$timeline = $this->buildTransactionTimeline(
$revision,
new DifferentialTransactionQuery(),
$engine = null,
array(
'left' => $left_diff->getID(),
'right' => $right_diff->getID(),
'old' => implode(',', $old_ids),
'new' => implode(',', $new_ids),
));
return $timeline;
}
private function buildRevisionWarnings(
DifferentialRevision $revision,
PhabricatorCustomFieldList $field_list,
array $warning_handle_map,
array $handles) {
$warnings = array();
foreach ($field_list->getFields() as $key => $field) {
$phids = idx($warning_handle_map, $key, array());
$field_handles = array_select_keys($handles, $phids);
$field_warnings = $field->getWarningsForRevisionHeader($field_handles);
foreach ($field_warnings as $warning) {
$warnings[] = $warning;
}
}
return $warnings;
}
}
diff --git a/src/applications/differential/customfield/DifferentialBranchField.php b/src/applications/differential/customfield/DifferentialBranchField.php
index 5601d6662..35ed809f1 100644
--- a/src/applications/differential/customfield/DifferentialBranchField.php
+++ b/src/applications/differential/customfield/DifferentialBranchField.php
@@ -1,79 +1,80 @@
<?php
final class DifferentialBranchField
extends DifferentialCustomField {
public function getFieldKey() {
return 'differential:branch';
}
public function getFieldName() {
return pht('Branch');
}
public function getFieldDescription() {
return pht('Shows the branch a diff came from.');
}
public function shouldAppearInPropertyView() {
return true;
}
public function renderPropertyViewLabel() {
return $this->getFieldName();
}
public function renderPropertyViewValue(array $handles) {
return $this->getBranchDescription($this->getObject()->getActiveDiff());
}
private function getBranchDescription(DifferentialDiff $diff) {
$branch = $diff->getBranch();
$bookmark = $diff->getBookmark();
if (strlen($branch) && strlen($bookmark)) {
return pht('%s (bookmark) on %s (branch)', $bookmark, $branch);
} else if (strlen($bookmark)) {
return pht('%s (bookmark)', $bookmark);
} else if (strlen($branch)) {
return $branch;
} else {
return null;
}
}
public function getProTips() {
return array(
pht(
- 'In Git and Mercurial, use a branch like "T123" to automatically '.
- 'associate changes with the corresponding task.'),
+ 'In Git and Mercurial, use a branch like "%s" to automatically '.
+ 'associate changes with the corresponding task.',
+ 'T123'),
);
}
public function shouldAppearInTransactionMail() {
return true;
}
public function updateTransactionMailBody(
PhabricatorMetaMTAMailBody $body,
PhabricatorApplicationTransactionEditor $editor,
array $xactions) {
$status_accepted = ArcanistDifferentialRevisionStatus::ACCEPTED;
// Show the "BRANCH" section only if there's a new diff or the revision
// is "Accepted".
if ((!$editor->getDiffUpdateTransaction($xactions)) &&
($this->getObject()->getStatus() != $status_accepted)) {
return;
}
$branch = $this->getBranchDescription($this->getObject()->getActiveDiff());
if ($branch === null) {
return;
}
$body->addTextSection(pht('BRANCH'), $branch);
}
}
diff --git a/src/applications/differential/customfield/DifferentialConflictsField.php b/src/applications/differential/customfield/DifferentialConflictsField.php
index ac5c75326..590d9fd3a 100644
--- a/src/applications/differential/customfield/DifferentialConflictsField.php
+++ b/src/applications/differential/customfield/DifferentialConflictsField.php
@@ -1,45 +1,45 @@
<?php
/**
* This field doesn't do anything, it just parses the "Conflicts:" field which
* `git` can insert after a merge, so we don't squish the field value into
* some other field.
*/
final class DifferentialConflictsField
extends DifferentialCustomField {
public function getFieldKey() {
return 'differential:conflicts';
}
public function getFieldKeyForConduit() {
return 'conflicts';
}
public function getFieldName() {
return pht('Conflicts');
}
public function getFieldDescription() {
return pht(
- 'Parses the "Conflicts" field which Git can inject into commit '.
- 'messages.');
+ 'Parses the "%s" field which Git can inject into commit messages.',
+ 'Conflicts');
}
public function canDisableField() {
return false;
}
public function shouldAppearInCommitMessage() {
return true;
}
public function shouldAllowEditInCommitMessage() {
return false;
}
public function renderCommitMessageValue(array $handles) {
return null;
}
}
diff --git a/src/applications/differential/customfield/DifferentialDependsOnField.php b/src/applications/differential/customfield/DifferentialDependsOnField.php
index 555cb0368..17156fe24 100644
--- a/src/applications/differential/customfield/DifferentialDependsOnField.php
+++ b/src/applications/differential/customfield/DifferentialDependsOnField.php
@@ -1,62 +1,63 @@
<?php
final class DifferentialDependsOnField
extends DifferentialCustomField {
public function getFieldKey() {
return 'differential:depends-on';
}
public function getFieldKeyForConduit() {
return 'phabricator:depends-on';
}
public function getFieldName() {
return pht('Depends On');
}
public function canDisableField() {
return false;
}
public function getFieldDescription() {
return pht('Lists revisions this one depends on.');
}
public function shouldAppearInPropertyView() {
return true;
}
public function renderPropertyViewLabel() {
return $this->getFieldName();
}
public function getRequiredHandlePHIDsForPropertyView() {
return PhabricatorEdgeQuery::loadDestinationPHIDs(
$this->getObject()->getPHID(),
DifferentialRevisionDependsOnRevisionEdgeType::EDGECONST);
}
public function renderPropertyViewValue(array $handles) {
return $this->renderHandleList($handles);
}
public function getProTips() {
return array(
pht(
'Create a dependency between revisions by writing '.
- '"Depends on D123" in your summary.'),
+ '"%s" in your summary.',
+ 'Depends on D123'),
);
}
public function shouldAppearInConduitDictionary() {
return true;
}
public function getConduitDictionaryValue() {
return PhabricatorEdgeQuery::loadDestinationPHIDs(
$this->getObject()->getPHID(),
DifferentialRevisionDependsOnRevisionEdgeType::EDGECONST);
}
}
diff --git a/src/applications/differential/customfield/DifferentialGitSVNIDField.php b/src/applications/differential/customfield/DifferentialGitSVNIDField.php
index a671e10bd..de45c54c7 100644
--- a/src/applications/differential/customfield/DifferentialGitSVNIDField.php
+++ b/src/applications/differential/customfield/DifferentialGitSVNIDField.php
@@ -1,45 +1,45 @@
<?php
/**
* This field doesn't do anything, it just parses the "git-svn-id" field which
* `git svn` inserts into commit messages so that we don't end up mangling
* some other field.
*/
final class DifferentialGitSVNIDField
extends DifferentialCustomField {
public function getFieldKey() {
return 'differential:git-svn-id';
}
public function getFieldKeyForConduit() {
return 'gitSVNID';
}
public function getFieldName() {
return pht('git-svn-id');
}
public function getFieldDescription() {
return pht(
- 'Parses the "git-svn-id" field which Git/SVN can inject into commit '.
- 'messages.');
+ 'Parses the "%s" field which Git/SVN can inject into commit messages.',
+ 'git-svn-id');
}
public function canDisableField() {
return false;
}
public function shouldAppearInCommitMessage() {
return true;
}
public function shouldAllowEditInCommitMessage() {
return false;
}
public function renderCommitMessageValue(array $handles) {
return null;
}
}
diff --git a/src/applications/differential/customfield/DifferentialLintField.php b/src/applications/differential/customfield/DifferentialLintField.php
index 56b7ec3ae..c43034dd0 100644
--- a/src/applications/differential/customfield/DifferentialLintField.php
+++ b/src/applications/differential/customfield/DifferentialLintField.php
@@ -1,260 +1,264 @@
<?php
final class DifferentialLintField
extends DifferentialCustomField {
public function getFieldKey() {
return 'differential:lint';
}
public function getFieldName() {
return pht('Lint');
}
public function getFieldDescription() {
return pht('Shows lint results.');
}
public function shouldAppearInPropertyView() {
return true;
}
public function renderPropertyViewLabel() {
return $this->getFieldName();
}
public function getRequiredDiffPropertiesForRevisionView() {
return array(
'arc:lint',
'arc:lint-excuse',
'arc:lint-postponed',
);
}
public function renderPropertyViewValue(array $handles) {
$diff = $this->getObject()->getActiveDiff();
$path_changesets = mpull($diff->loadChangesets(), 'getID', 'getFilename');
$lstar = DifferentialRevisionUpdateHistoryView::renderDiffLintStar($diff);
$lmsg = DifferentialRevisionUpdateHistoryView::getDiffLintMessage($diff);
$ldata = $diff->getProperty('arc:lint');
$ltail = null;
$rows = array();
$rows[] = array(
'style' => 'star',
'name' => $lstar,
'value' => $lmsg,
'show' => true,
);
$excuse = $diff->getProperty('arc:lint-excuse');
if ($excuse) {
$rows[] = array(
'style' => 'excuse',
'name' => 'Excuse',
'value' => phutil_escape_html_newlines($excuse),
'show' => true,
);
}
$show_limit = 10;
$hidden = array();
if ($ldata) {
$ldata = igroup($ldata, 'path');
foreach ($ldata as $path => $messages) {
$rows[] = array(
'style' => 'section',
'name' => $path,
'show' => $show_limit,
);
foreach ($messages as $message) {
$path = idx($message, 'path');
$line = idx($message, 'line');
$code = idx($message, 'code');
$severity = idx($message, 'severity');
$name = idx($message, 'name');
$description = idx($message, 'description');
- $line_link = 'line '.intval($line);
+ $line_link = pht('line %d', intval($line));
if (isset($path_changesets[$path])) {
$href = '#C'.$path_changesets[$path].'NL'.max(1, $line);
// TODO: We are always showing the active diff
// if ($diff->getID() != $this->getDiff()->getID()) {
// $href = '/D'.$diff->getRevisionID().'?id='.$diff->getID().$href;
// }
$line_link = phutil_tag(
'a',
array(
'href' => $href,
),
$line_link);
}
if ($show_limit) {
--$show_limit;
$show = true;
} else {
$show = false;
if (empty($hidden[$severity])) {
$hidden[$severity] = 0;
}
$hidden[$severity]++;
}
$rows[] = array(
'style' => $this->getSeverityStyle($severity),
'name' => ucwords($severity),
'value' => hsprintf(
'(%s) %s at %s',
$code,
$name,
$line_link),
'show' => $show,
);
if (!empty($message['locations'])) {
$locations = array();
foreach ($message['locations'] as $location) {
$other_line = idx($location, 'line');
$locations[] =
idx($location, 'path', $path).
($other_line ? ":{$other_line}" : '');
}
- $description .= "\nOther locations: ".implode(', ', $locations);
+ $description .= "\n".pht(
+ 'Other locations: %s',
+ implode(', ', $locations));
}
if (strlen($description)) {
$rows[] = array(
'style' => 'details',
'value' => phutil_escape_html_newlines($description),
'show' => false,
);
if (empty($hidden['details'])) {
$hidden['details'] = 0;
}
$hidden['details']++;
}
}
}
}
$postponed = $diff->getProperty('arc:lint-postponed');
if ($postponed) {
foreach ($postponed as $linter) {
$rows[] = array(
'style' => $this->getPostponedStyle(),
'name' => 'Postponed',
'value' => $linter,
'show' => false,
);
if (empty($hidden['postponed'])) {
$hidden['postponed'] = 0;
}
$hidden['postponed']++;
}
}
$show_string = $this->renderShowString($hidden);
$view = new DifferentialResultsTableView();
$view->setRows($rows);
$view->setShowMoreString($show_string);
return $view->render();
}
private function getSeverityStyle($severity) {
$map = array(
ArcanistLintSeverity::SEVERITY_ERROR => 'red',
ArcanistLintSeverity::SEVERITY_WARNING => 'yellow',
ArcanistLintSeverity::SEVERITY_AUTOFIX => 'yellow',
ArcanistLintSeverity::SEVERITY_ADVICE => 'yellow',
);
return idx($map, $severity);
}
private function getPostponedStyle() {
return 'blue';
}
private function renderShowString(array $hidden) {
if (!$hidden) {
return null;
}
// Reorder hidden things by severity.
$hidden = array_select_keys(
$hidden,
array(
ArcanistLintSeverity::SEVERITY_ERROR,
ArcanistLintSeverity::SEVERITY_WARNING,
ArcanistLintSeverity::SEVERITY_AUTOFIX,
ArcanistLintSeverity::SEVERITY_ADVICE,
'details',
'postponed',
)) + $hidden;
$show = array();
foreach ($hidden as $key => $value) {
switch ($key) {
case ArcanistLintSeverity::SEVERITY_ERROR:
$show[] = pht('%d Error(s)', $value);
break;
case ArcanistLintSeverity::SEVERITY_WARNING:
$show[] = pht('%d Warning(s)', $value);
break;
case ArcanistLintSeverity::SEVERITY_AUTOFIX:
$show[] = pht('%d Auto-Fix(es)', $value);
break;
case ArcanistLintSeverity::SEVERITY_ADVICE:
$show[] = pht('%d Advice(s)', $value);
break;
case 'details':
$show[] = pht('%d Detail(s)', $value);
break;
case 'postponed':
$show[] = pht('%d Postponed', $value);
break;
default:
$show[] = $value;
break;
}
}
- return 'Show Full Lint Results ('.implode(', ', $show).')';
+ return pht(
+ 'Show Full Lint Results (%s)',
+ implode(', ', $show));
}
public function getWarningsForDetailView() {
$status = $this->getObject()->getActiveDiff()->getLintStatus();
if ($status < DifferentialLintStatus::LINT_WARN) {
return array();
}
if ($status == DifferentialLintStatus::LINT_AUTO_SKIP) {
return array();
}
$warnings = array();
if ($status == DifferentialLintStatus::LINT_SKIP) {
$warnings[] = pht(
'Lint was skipped when generating these changes.');
} else if ($status == DifferentialLintStatus::LINT_POSTPONED) {
$warnings[] = pht(
'Background linting has not finished executing on these changes.');
} else {
$warnings[] = pht('These changes have lint problems.');
}
return $warnings;
}
}
diff --git a/src/applications/differential/customfield/DifferentialManiphestTasksField.php b/src/applications/differential/customfield/DifferentialManiphestTasksField.php
index b13cfcc30..56803f60c 100644
--- a/src/applications/differential/customfield/DifferentialManiphestTasksField.php
+++ b/src/applications/differential/customfield/DifferentialManiphestTasksField.php
@@ -1,114 +1,115 @@
<?php
final class DifferentialManiphestTasksField
extends DifferentialCoreCustomField {
public function getFieldKey() {
return 'differential:maniphest-tasks';
}
public function getFieldKeyForConduit() {
return 'maniphestTaskPHIDs';
}
public function canDisableField() {
return false;
}
public function shouldAppearInEditView() {
return false;
}
public function getFieldName() {
return pht('Maniphest Tasks');
}
public function getFieldDescription() {
return pht('Lists associated tasks.');
}
public function shouldAppearInPropertyView() {
return true;
}
public function renderPropertyViewLabel() {
return $this->getFieldName();
}
protected function readValueFromRevision(DifferentialRevision $revision) {
if (!$revision->getPHID()) {
return array();
}
return PhabricatorEdgeQuery::loadDestinationPHIDs(
$revision->getPHID(),
DifferentialRevisionHasTaskEdgeType::EDGECONST);
}
public function getApplicationTransactionType() {
return PhabricatorTransactions::TYPE_EDGE;
}
public function getApplicationTransactionMetadata() {
return array(
'edge:type' => DifferentialRevisionHasTaskEdgeType::EDGECONST,
);
}
public function getNewValueForApplicationTransactions() {
$edges = array();
foreach ($this->getValue() as $phid) {
$edges[$phid] = $phid;
}
return array('=' => $edges);
}
public function getRequiredHandlePHIDsForPropertyView() {
return $this->getValue();
}
public function renderPropertyViewValue(array $handles) {
return $this->renderHandleList($handles);
}
public function shouldAppearInCommitMessage() {
return true;
}
public function shouldAllowEditInCommitMessage() {
return true;
}
public function getCommitMessageLabels() {
return array(
'Maniphest Task',
'Maniphest Tasks',
);
}
public function parseValueFromCommitMessage($value) {
return $this->parseObjectList(
$value,
array(
ManiphestTaskPHIDType::TYPECONST,
));
}
public function getRequiredHandlePHIDsForCommitMessage() {
return $this->getRequiredHandlePHIDsForPropertyView();
}
public function renderCommitMessageValue(array $handles) {
return $this->renderObjectList($handles);
}
public function getProTips() {
return array(
pht(
- 'Write "Fixes T123" in your summary to automatically close the '.
- 'corresponding task when this change lands.'),
+ 'Write "%s" in your summary to automatically close the '.
+ 'corresponding task when this change lands.',
+ 'Fixes T123'),
);
}
}
diff --git a/src/applications/differential/customfield/DifferentialProjectReviewersField.php b/src/applications/differential/customfield/DifferentialProjectReviewersField.php
index aff5644a5..1eea4f51b 100644
--- a/src/applications/differential/customfield/DifferentialProjectReviewersField.php
+++ b/src/applications/differential/customfield/DifferentialProjectReviewersField.php
@@ -1,68 +1,69 @@
<?php
final class DifferentialProjectReviewersField
extends DifferentialCustomField {
public function getFieldKey() {
return 'differential:project-reviewers';
}
public function getFieldName() {
return pht('Project Reviewers');
}
public function getFieldDescription() {
return pht('Display project reviewers.');
}
public function shouldAppearInPropertyView() {
return true;
}
public function canDisableField() {
return false;
}
public function renderPropertyViewLabel() {
return $this->getFieldName();
}
public function getRequiredHandlePHIDsForPropertyView() {
return mpull($this->getProjectReviewers(), 'getReviewerPHID');
}
public function renderPropertyViewValue(array $handles) {
$reviewers = $this->getProjectReviewers();
if (!$reviewers) {
return null;
}
$view = id(new DifferentialReviewersView())
->setUser($this->getViewer())
->setReviewers($reviewers)
->setHandles($handles);
// TODO: Active diff stuff.
return $view;
}
private function getProjectReviewers() {
$reviewers = array();
foreach ($this->getObject()->getReviewerStatus() as $reviewer) {
if (!$reviewer->isUser()) {
$reviewers[] = $reviewer;
}
}
return $reviewers;
}
public function getProTips() {
return array(
pht(
'You can add a project as a subscriber or reviewer by writing '.
- '"#projectname" in the appropriate field.'),
+ '"%s" in the appropriate field.',
+ '#projectname'),
);
}
}
diff --git a/src/applications/differential/customfield/DifferentialRepositoryField.php b/src/applications/differential/customfield/DifferentialRepositoryField.php
index 76f40d095..fc6c88194 100644
--- a/src/applications/differential/customfield/DifferentialRepositoryField.php
+++ b/src/applications/differential/customfield/DifferentialRepositoryField.php
@@ -1,162 +1,163 @@
<?php
final class DifferentialRepositoryField
extends DifferentialCoreCustomField {
public function getFieldKey() {
return 'differential:repository';
}
public function getFieldName() {
return pht('Repository');
}
public function getFieldDescription() {
return pht('Associates a revision with a repository.');
}
protected function readValueFromRevision(
DifferentialRevision $revision) {
return $revision->getRepositoryPHID();
}
protected function writeValueToRevision(
DifferentialRevision $revision,
$value) {
$revision->setRepositoryPHID($value);
}
public function readValueFromRequest(AphrontRequest $request) {
$phids = $request->getArr($this->getFieldKey());
$first = head($phids);
$this->setValue(nonempty($first, null));
}
public function renderEditControl(array $handles) {
if ($this->getValue()) {
$value = array($this->getValue());
} else {
$value = array();
}
return id(new AphrontFormTokenizerControl())
->setUser($this->getViewer())
->setName($this->getFieldKey())
->setDatasource(new DiffusionRepositoryDatasource())
->setValue($value)
->setError($this->getFieldError())
->setLabel($this->getFieldName())
->setLimit(1);
}
public function getApplicationTransactionRequiredHandlePHIDs(
PhabricatorApplicationTransaction $xaction) {
$old = $xaction->getOldValue();
$new = $xaction->getNewValue();
$phids = array();
if ($old) {
$phids[] = $old;
}
if ($new) {
$phids[] = $new;
}
return $phids;
}
public function getApplicationTransactionTitle(
PhabricatorApplicationTransaction $xaction) {
$author_phid = $xaction->getAuthorPHID();
$old = $xaction->getOldValue();
$new = $xaction->getNewValue();
if ($old && $new) {
return pht(
'%s changed the repository for this revision from %s to %s.',
$xaction->renderHandleLink($author_phid),
$xaction->renderHandleLink($old),
$xaction->renderHandleLink($new));
} else if ($new) {
return pht(
'%s set the repository for this revision to %s.',
$xaction->renderHandleLink($author_phid),
$xaction->renderHandleLink($new));
} else {
return pht(
'%s removed %s as the repository for this revision.',
$xaction->renderHandleLink($author_phid),
$xaction->renderHandleLink($old));
}
}
public function getApplicationTransactionTitleForFeed(
PhabricatorApplicationTransaction $xaction) {
$object_phid = $xaction->getObjectPHID();
$author_phid = $xaction->getAuthorPHID();
$old = $xaction->getOldValue();
$new = $xaction->getNewValue();
if ($old && $new) {
return pht(
'%s updated the repository for %s from %s to %s.',
$xaction->renderHandleLink($author_phid),
$xaction->renderHandleLink($object_phid),
$xaction->renderHandleLink($old),
$xaction->renderHandleLink($new));
} else if ($new) {
return pht(
'%s set the repository for %s to %s.',
$xaction->renderHandleLink($author_phid),
$xaction->renderHandleLink($object_phid),
$xaction->renderHandleLink($new));
} else {
return pht(
'%s removed the repository for %s. (Repository was %s.)',
$xaction->renderHandleLink($author_phid),
$xaction->renderHandleLink($object_phid),
$xaction->renderHandleLink($old));
}
}
public function shouldAppearInPropertyView() {
return true;
}
public function renderPropertyViewLabel() {
return $this->getFieldName();
}
public function getRequiredHandlePHIDsForPropertyView() {
$repository_phid = $this->getObject()->getRepositoryPHID();
if ($repository_phid) {
return array($repository_phid);
}
return array();
}
public function renderPropertyViewValue(array $handles) {
return $this->renderHandleList($handles);
}
public function shouldAppearInTransactionMail() {
return true;
}
public function updateTransactionMailBody(
PhabricatorMetaMTAMailBody $body,
PhabricatorApplicationTransactionEditor $editor,
array $xactions) {
$repository = $this->getObject()->getRepository();
if ($repository === null) {
return;
}
- $body->addTextSection(pht('REPOSITORY'),
+ $body->addTextSection(
+ pht('REPOSITORY'),
$repository->getMonogram().' '.$repository->getName());
}
}
diff --git a/src/applications/differential/customfield/DifferentialRevisionIDField.php b/src/applications/differential/customfield/DifferentialRevisionIDField.php
index bb54951dc..9d23cf765 100644
--- a/src/applications/differential/customfield/DifferentialRevisionIDField.php
+++ b/src/applications/differential/customfield/DifferentialRevisionIDField.php
@@ -1,86 +1,85 @@
<?php
final class DifferentialRevisionIDField
extends DifferentialCustomField {
private $revisionID;
public function getFieldKey() {
return 'differential:revision-id';
}
public function getFieldKeyForConduit() {
return 'revisionID';
}
public function getFieldName() {
return pht('Differential Revision');
}
public function getFieldDescription() {
return pht(
- 'Ties commits to revisions and provides a permananent link between '.
- 'them.');
+ 'Ties commits to revisions and provides a permanent link between them.');
}
public function canDisableField() {
return false;
}
public function shouldAppearInCommitMessage() {
return true;
}
public function parseValueFromCommitMessage($value) {
// If the value is just "D123" or similar, parse the ID from it directly.
$value = trim($value);
$matches = null;
if (preg_match('/^[dD]([1-9]\d*)\z/', $value, $matches)) {
return (int)$matches[1];
}
// Otherwise, try to extract a URI value.
return self::parseRevisionIDFromURI($value);
}
public function renderCommitMessageValue(array $handles) {
$id = coalesce($this->revisionID, $this->getObject()->getID());
if (!$id) {
return null;
}
return PhabricatorEnv::getProductionURI('/D'.$id);
}
public function readValueFromCommitMessage($value) {
$this->revisionID = $value;
}
private static function parseRevisionIDFromURI($uri_string) {
$uri = new PhutilURI($uri_string);
$path = $uri->getPath();
$matches = null;
if (preg_match('#^/D(\d+)$#', $path, $matches)) {
$id = (int)$matches[1];
$prod_uri = new PhutilURI(PhabricatorEnv::getProductionURI('/D'.$id));
// Make sure the URI is the same as our URI. Basically, we want to ignore
// commits from other Phabricator installs.
if ($uri->getDomain() == $prod_uri->getDomain()) {
return $id;
}
$allowed_uris = PhabricatorEnv::getAllowedURIs('/D'.$id);
foreach ($allowed_uris as $allowed_uri) {
if ($uri_string == $allowed_uri) {
return $id;
}
}
}
return null;
}
}
diff --git a/src/applications/differential/customfield/DifferentialUnitField.php b/src/applications/differential/customfield/DifferentialUnitField.php
index cf183bf38..2cc86d266 100644
--- a/src/applications/differential/customfield/DifferentialUnitField.php
+++ b/src/applications/differential/customfield/DifferentialUnitField.php
@@ -1,234 +1,236 @@
<?php
final class DifferentialUnitField
extends DifferentialCustomField {
public function getFieldKey() {
return 'differential:unit';
}
public function getFieldName() {
return pht('Unit');
}
public function getFieldDescription() {
return pht('Shows unit test results.');
}
public function shouldAppearInPropertyView() {
return true;
}
public function renderPropertyViewLabel() {
return $this->getFieldName();
}
public function getRequiredDiffPropertiesForRevisionView() {
return array(
'arc:unit',
'arc:unit-excuse',
);
}
public function renderPropertyViewValue(array $handles) {
$diff = $this->getObject()->getActiveDiff();
$ustar = DifferentialRevisionUpdateHistoryView::renderDiffUnitStar($diff);
$umsg = DifferentialRevisionUpdateHistoryView::getDiffUnitMessage($diff);
$rows = array();
$rows[] = array(
'style' => 'star',
'name' => $ustar,
'value' => $umsg,
'show' => true,
);
$excuse = $diff->getProperty('arc:unit-excuse');
if ($excuse) {
$rows[] = array(
'style' => 'excuse',
- 'name' => 'Excuse',
+ 'name' => pht('Excuse'),
'value' => phutil_escape_html_newlines($excuse),
'show' => true,
);
}
$show_limit = 10;
$hidden = array();
$udata = $diff->getProperty('arc:unit');
if ($udata) {
$sort_map = array(
ArcanistUnitTestResult::RESULT_BROKEN => 0,
ArcanistUnitTestResult::RESULT_FAIL => 1,
ArcanistUnitTestResult::RESULT_UNSOUND => 2,
ArcanistUnitTestResult::RESULT_SKIP => 3,
ArcanistUnitTestResult::RESULT_POSTPONED => 4,
ArcanistUnitTestResult::RESULT_PASS => 5,
);
foreach ($udata as $key => $test) {
$udata[$key]['sort'] = idx($sort_map, idx($test, 'result'));
}
$udata = isort($udata, 'sort');
$engine = new PhabricatorMarkupEngine();
$engine->setViewer($this->getViewer());
$markup_objects = array();
foreach ($udata as $key => $test) {
$userdata = idx($test, 'userdata');
if ($userdata) {
if ($userdata !== false) {
$userdata = str_replace("\000", '', $userdata);
}
$markup_object = id(new PhabricatorMarkupOneOff())
- ->setContent($userdata)
- ->setPreserveLinebreaks(true);
+ ->setContent($userdata)
+ ->setPreserveLinebreaks(true);
$engine->addObject($markup_object, 'default');
$markup_objects[$key] = $markup_object;
}
}
$engine->process();
foreach ($udata as $key => $test) {
$result = idx($test, 'result');
$default_hide = false;
switch ($result) {
case ArcanistUnitTestResult::RESULT_POSTPONED:
case ArcanistUnitTestResult::RESULT_PASS:
$default_hide = true;
break;
}
if ($show_limit && !$default_hide) {
--$show_limit;
$show = true;
} else {
$show = false;
if (empty($hidden[$result])) {
$hidden[$result] = 0;
}
$hidden[$result]++;
}
$value = idx($test, 'name');
$namespace = idx($test, 'namespace');
if ($namespace) {
$value = $namespace.'::'.$value;
}
if (!empty($test['link'])) {
$value = phutil_tag(
'a',
array(
'href' => $test['link'],
'target' => '_blank',
),
$value);
}
$rows[] = array(
'style' => $this->getResultStyle($result),
'name' => ucwords($result),
'value' => $value,
'show' => $show,
);
if (isset($markup_objects[$key])) {
$rows[] = array(
'style' => 'details',
'value' => $engine->getOutput($markup_objects[$key], 'default'),
'show' => false,
);
if (empty($hidden['details'])) {
$hidden['details'] = 0;
}
$hidden['details']++;
}
}
}
$show_string = $this->renderShowString($hidden);
$view = new DifferentialResultsTableView();
$view->setRows($rows);
$view->setShowMoreString($show_string);
return $view->render();
}
private function getResultStyle($result) {
$map = array(
ArcanistUnitTestResult::RESULT_PASS => 'green',
ArcanistUnitTestResult::RESULT_FAIL => 'red',
ArcanistUnitTestResult::RESULT_SKIP => 'blue',
ArcanistUnitTestResult::RESULT_BROKEN => 'red',
ArcanistUnitTestResult::RESULT_UNSOUND => 'yellow',
ArcanistUnitTestResult::RESULT_POSTPONED => 'blue',
);
return idx($map, $result);
}
private function renderShowString(array $hidden) {
if (!$hidden) {
return null;
}
// Reorder hidden things by severity.
$hidden = array_select_keys(
$hidden,
array(
ArcanistUnitTestResult::RESULT_BROKEN,
ArcanistUnitTestResult::RESULT_FAIL,
ArcanistUnitTestResult::RESULT_UNSOUND,
ArcanistUnitTestResult::RESULT_SKIP,
ArcanistUnitTestResult::RESULT_POSTPONED,
ArcanistUnitTestResult::RESULT_PASS,
'details',
)) + $hidden;
$noun = array(
- ArcanistUnitTestResult::RESULT_BROKEN => 'Broken',
- ArcanistUnitTestResult::RESULT_FAIL => 'Failed',
- ArcanistUnitTestResult::RESULT_UNSOUND => 'Unsound',
- ArcanistUnitTestResult::RESULT_SKIP => 'Skipped',
- ArcanistUnitTestResult::RESULT_POSTPONED => 'Postponed',
- ArcanistUnitTestResult::RESULT_PASS => 'Passed',
+ ArcanistUnitTestResult::RESULT_BROKEN => pht('Broken'),
+ ArcanistUnitTestResult::RESULT_FAIL => pht('Failed'),
+ ArcanistUnitTestResult::RESULT_UNSOUND => pht('Unsound'),
+ ArcanistUnitTestResult::RESULT_SKIP => pht('Skipped'),
+ ArcanistUnitTestResult::RESULT_POSTPONED => pht('Postponed'),
+ ArcanistUnitTestResult::RESULT_PASS => pht('Passed'),
);
$show = array();
foreach ($hidden as $key => $value) {
if ($key == 'details') {
$show[] = pht('%d Detail(s)', $value);
} else {
$show[] = $value.' '.idx($noun, $key);
}
}
- return 'Show Full Unit Results ('.implode(', ', $show).')';
+ return pht(
+ 'Show Full Unit Results (%s)',
+ implode(', ', $show));
}
public function getWarningsForDetailView() {
$status = $this->getObject()->getActiveDiff()->getUnitStatus();
$warnings = array();
if ($status < DifferentialUnitStatus::UNIT_WARN) {
// Don't show any warnings.
} else if ($status == DifferentialUnitStatus::UNIT_AUTO_SKIP) {
// Don't show any warnings.
} else if ($status == DifferentialUnitStatus::UNIT_POSTPONED) {
$warnings[] = pht(
'Background tests have not finished executing on these changes.');
} else if ($status == DifferentialUnitStatus::UNIT_SKIP) {
$warnings[] = pht(
'Unit tests were skipped when generating these changes.');
} else {
$warnings[] = pht('These changes have unit test problems.');
}
return $warnings;
}
}
diff --git a/src/applications/differential/editor/DifferentialTransactionEditor.php b/src/applications/differential/editor/DifferentialTransactionEditor.php
index 0a02dec20..6a43f35d5 100644
--- a/src/applications/differential/editor/DifferentialTransactionEditor.php
+++ b/src/applications/differential/editor/DifferentialTransactionEditor.php
@@ -1,1906 +1,1905 @@
<?php
final class DifferentialTransactionEditor
extends PhabricatorApplicationTransactionEditor {
private $changedPriorToCommitURI;
private $isCloseByCommit;
private $repositoryPHIDOverride = false;
private $didExpandInlineState = false;
public function getEditorApplicationClass() {
return 'PhabricatorDifferentialApplication';
}
public function getEditorObjectsDescription() {
return pht('Differential Revisions');
}
public function getDiffUpdateTransaction(array $xactions) {
$type_update = DifferentialTransaction::TYPE_UPDATE;
foreach ($xactions as $xaction) {
if ($xaction->getTransactionType() == $type_update) {
return $xaction;
}
}
return null;
}
public function setIsCloseByCommit($is_close_by_commit) {
$this->isCloseByCommit = $is_close_by_commit;
return $this;
}
public function getIsCloseByCommit() {
return $this->isCloseByCommit;
}
public function setChangedPriorToCommitURI($uri) {
$this->changedPriorToCommitURI = $uri;
return $this;
}
public function getChangedPriorToCommitURI() {
return $this->changedPriorToCommitURI;
}
public function setRepositoryPHIDOverride($phid_or_null) {
$this->repositoryPHIDOverride = $phid_or_null;
return $this;
}
public function getTransactionTypes() {
$types = parent::getTransactionTypes();
$types[] = PhabricatorTransactions::TYPE_COMMENT;
$types[] = PhabricatorTransactions::TYPE_VIEW_POLICY;
$types[] = PhabricatorTransactions::TYPE_EDIT_POLICY;
$types[] = DifferentialTransaction::TYPE_ACTION;
$types[] = DifferentialTransaction::TYPE_INLINE;
$types[] = DifferentialTransaction::TYPE_STATUS;
$types[] = DifferentialTransaction::TYPE_UPDATE;
return $types;
}
protected function getCustomTransactionOldValue(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case DifferentialTransaction::TYPE_ACTION:
return null;
case DifferentialTransaction::TYPE_INLINE:
return null;
case DifferentialTransaction::TYPE_UPDATE:
if ($this->getIsNewObject()) {
return null;
} else {
return $object->getActiveDiff()->getPHID();
}
}
return parent::getCustomTransactionOldValue($object, $xaction);
}
protected function getCustomTransactionNewValue(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case DifferentialTransaction::TYPE_ACTION:
case DifferentialTransaction::TYPE_UPDATE:
return $xaction->getNewValue();
case DifferentialTransaction::TYPE_INLINE:
return null;
}
return parent::getCustomTransactionNewValue($object, $xaction);
}
protected function transactionHasEffect(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
$actor_phid = $this->getActingAsPHID();
switch ($xaction->getTransactionType()) {
case DifferentialTransaction::TYPE_INLINE:
return $xaction->hasComment();
case DifferentialTransaction::TYPE_ACTION:
$status_closed = ArcanistDifferentialRevisionStatus::CLOSED;
$status_abandoned = ArcanistDifferentialRevisionStatus::ABANDONED;
$status_review = ArcanistDifferentialRevisionStatus::NEEDS_REVIEW;
$status_revision = ArcanistDifferentialRevisionStatus::NEEDS_REVISION;
$status_plan = ArcanistDifferentialRevisionStatus::CHANGES_PLANNED;
$action_type = $xaction->getNewValue();
switch ($action_type) {
case DifferentialAction::ACTION_ACCEPT:
case DifferentialAction::ACTION_REJECT:
if ($action_type == DifferentialAction::ACTION_ACCEPT) {
$new_status = DifferentialReviewerStatus::STATUS_ACCEPTED;
} else {
$new_status = DifferentialReviewerStatus::STATUS_REJECTED;
}
$actor = $this->getActor();
// These transactions can cause effects in two ways: by altering the
// status of an existing reviewer; or by adding the actor as a new
// reviewer.
$will_add_reviewer = true;
foreach ($object->getReviewerStatus() as $reviewer) {
if ($reviewer->hasAuthority($actor)) {
if ($reviewer->getStatus() != $new_status) {
return true;
}
}
if ($reviewer->getReviewerPHID() == $actor_phid) {
$will_add_reviwer = false;
}
}
return $will_add_reviewer;
case DifferentialAction::ACTION_CLOSE:
return ($object->getStatus() != $status_closed);
case DifferentialAction::ACTION_ABANDON:
return ($object->getStatus() != $status_abandoned);
case DifferentialAction::ACTION_RECLAIM:
return ($object->getStatus() == $status_abandoned);
case DifferentialAction::ACTION_REOPEN:
return ($object->getStatus() == $status_closed);
case DifferentialAction::ACTION_RETHINK:
return ($object->getStatus() != $status_plan);
case DifferentialAction::ACTION_REQUEST:
return ($object->getStatus() != $status_review);
case DifferentialAction::ACTION_RESIGN:
foreach ($object->getReviewerStatus() as $reviewer) {
if ($reviewer->getReviewerPHID() == $actor_phid) {
return true;
}
}
return false;
case DifferentialAction::ACTION_CLAIM:
return ($actor_phid != $object->getAuthorPHID());
}
}
return parent::transactionHasEffect($object, $xaction);
}
protected function applyCustomInternalTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
$status_review = ArcanistDifferentialRevisionStatus::NEEDS_REVIEW;
$status_revision = ArcanistDifferentialRevisionStatus::NEEDS_REVISION;
$status_plan = ArcanistDifferentialRevisionStatus::CHANGES_PLANNED;
$status_abandoned = ArcanistDifferentialRevisionStatus::ABANDONED;
switch ($xaction->getTransactionType()) {
case DifferentialTransaction::TYPE_INLINE:
return;
case DifferentialTransaction::TYPE_UPDATE:
if (!$this->getIsCloseByCommit()) {
switch ($object->getStatus()) {
case $status_revision:
case $status_plan:
case $status_abandoned:
$object->setStatus($status_review);
break;
}
}
$diff = $this->requireDiff($xaction->getNewValue());
$object->setLineCount($diff->getLineCount());
if ($this->repositoryPHIDOverride !== false) {
$object->setRepositoryPHID($this->repositoryPHIDOverride);
} else {
$object->setRepositoryPHID($diff->getRepositoryPHID());
}
$object->attachActiveDiff($diff);
// TODO: Update the `diffPHID` once we add that.
return;
case DifferentialTransaction::TYPE_ACTION:
switch ($xaction->getNewValue()) {
case DifferentialAction::ACTION_RESIGN:
case DifferentialAction::ACTION_ACCEPT:
case DifferentialAction::ACTION_REJECT:
// These have no direct effects, and affect review status only
// indirectly by altering reviewers with TYPE_EDGE transactions.
return;
case DifferentialAction::ACTION_ABANDON:
$object->setStatus(ArcanistDifferentialRevisionStatus::ABANDONED);
return;
case DifferentialAction::ACTION_RETHINK:
$object->setStatus($status_plan);
return;
case DifferentialAction::ACTION_RECLAIM:
$object->setStatus($status_review);
return;
case DifferentialAction::ACTION_REOPEN:
$object->setStatus($status_review);
return;
case DifferentialAction::ACTION_REQUEST:
$object->setStatus($status_review);
return;
case DifferentialAction::ACTION_CLOSE:
$object->setStatus(ArcanistDifferentialRevisionStatus::CLOSED);
return;
case DifferentialAction::ACTION_CLAIM:
$object->setAuthorPHID($this->getActingAsPHID());
return;
default:
throw new Exception(
pht(
'Differential action "%s" is not a valid action!',
$xaction->getNewValue()));
}
break;
}
return parent::applyCustomInternalTransaction($object, $xaction);
}
protected function expandTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
$results = parent::expandTransaction($object, $xaction);
$actor = $this->getActor();
$actor_phid = $this->getActingAsPHID();
$type_edge = PhabricatorTransactions::TYPE_EDGE;
$status_plan = ArcanistDifferentialRevisionStatus::CHANGES_PLANNED;
$edge_reviewer = DifferentialRevisionHasReviewerEdgeType::EDGECONST;
$edge_ref_task = DifferentialRevisionHasTaskEdgeType::EDGECONST;
$is_sticky_accept = PhabricatorEnv::getEnvConfig(
'differential.sticky-accept');
$downgrade_rejects = false;
$downgrade_accepts = false;
if ($this->getIsCloseByCommit()) {
// Never downgrade reviewers when we're closing a revision after a
// commit.
} else {
switch ($xaction->getTransactionType()) {
case DifferentialTransaction::TYPE_UPDATE:
$downgrade_rejects = true;
if (!$is_sticky_accept) {
// If "sticky accept" is disabled, also downgrade the accepts.
$downgrade_accepts = true;
}
break;
case DifferentialTransaction::TYPE_ACTION:
switch ($xaction->getNewValue()) {
case DifferentialAction::ACTION_REQUEST:
$downgrade_rejects = true;
if ((!$is_sticky_accept) ||
($object->getStatus() != $status_plan)) {
// If the old state isn't "changes planned", downgrade the
// accepts. This exception allows an accepted revision to
// go through Plan Changes -> Request Review to return to
// "accepted" if the author didn't update the revision.
$downgrade_accepts = true;
}
break;
}
break;
}
}
$new_accept = DifferentialReviewerStatus::STATUS_ACCEPTED;
$new_reject = DifferentialReviewerStatus::STATUS_REJECTED;
$old_accept = DifferentialReviewerStatus::STATUS_ACCEPTED_OLDER;
$old_reject = DifferentialReviewerStatus::STATUS_REJECTED_OLDER;
if ($downgrade_rejects || $downgrade_accepts) {
// When a revision is updated, change all "reject" to "rejected older
// revision". This means we won't immediately push the update back into
// "needs review", but outstanding rejects will still block it from
// moving to "accepted".
// We also do this for "Request Review", even though the diff is not
// updated directly. Essentially, this acts like an update which doesn't
// actually change the diff text.
$edits = array();
foreach ($object->getReviewerStatus() as $reviewer) {
if ($downgrade_rejects) {
if ($reviewer->getStatus() == $new_reject) {
$edits[$reviewer->getReviewerPHID()] = array(
'data' => array(
'status' => $old_reject,
),
);
}
}
if ($downgrade_accepts) {
if ($reviewer->getStatus() == $new_accept) {
$edits[$reviewer->getReviewerPHID()] = array(
'data' => array(
'status' => $old_accept,
),
);
}
}
}
if ($edits) {
$results[] = id(new DifferentialTransaction())
->setTransactionType($type_edge)
->setMetadataValue('edge:type', $edge_reviewer)
->setIgnoreOnNoEffect(true)
->setNewValue(array('+' => $edits));
}
}
switch ($xaction->getTransactionType()) {
case DifferentialTransaction::TYPE_UPDATE:
if ($this->getIsCloseByCommit()) {
// Don't bother with any of this if this update is a side effect of
// commit detection.
break;
}
// When a revision is updated and the diff comes from a branch named
// "T123" or similar, automatically associate the commit with the
// task that the branch names.
$maniphest = 'PhabricatorManiphestApplication';
if (PhabricatorApplication::isClassInstalled($maniphest)) {
$diff = $this->requireDiff($xaction->getNewValue());
$branch = $diff->getBranch();
// No "$", to allow for branches like T123_demo.
$match = null;
if (preg_match('/^T(\d+)/i', $branch, $match)) {
$task_id = $match[1];
$tasks = id(new ManiphestTaskQuery())
->setViewer($this->getActor())
->withIDs(array($task_id))
->execute();
if ($tasks) {
$task = head($tasks);
$task_phid = $task->getPHID();
$results[] = id(new DifferentialTransaction())
->setTransactionType($type_edge)
->setMetadataValue('edge:type', $edge_ref_task)
->setIgnoreOnNoEffect(true)
->setNewValue(array('+' => array($task_phid => $task_phid)));
}
}
}
break;
case PhabricatorTransactions::TYPE_COMMENT:
// When a user leaves a comment, upgrade their reviewer status from
// "added" to "commented" if they're also a reviewer. We may further
// upgrade this based on other actions in the transaction group.
$status_added = DifferentialReviewerStatus::STATUS_ADDED;
$status_commented = DifferentialReviewerStatus::STATUS_COMMENTED;
$data = array(
'status' => $status_commented,
);
$edits = array();
foreach ($object->getReviewerStatus() as $reviewer) {
if ($reviewer->getReviewerPHID() == $actor_phid) {
if ($reviewer->getStatus() == $status_added) {
$edits[$actor_phid] = array(
'data' => $data,
);
}
}
}
if ($edits) {
$results[] = id(new DifferentialTransaction())
->setTransactionType($type_edge)
->setMetadataValue('edge:type', $edge_reviewer)
->setIgnoreOnNoEffect(true)
->setNewValue(array('+' => $edits));
}
break;
case DifferentialTransaction::TYPE_ACTION:
$action_type = $xaction->getNewValue();
switch ($action_type) {
case DifferentialAction::ACTION_ACCEPT:
case DifferentialAction::ACTION_REJECT:
if ($action_type == DifferentialAction::ACTION_ACCEPT) {
$data = array(
'status' => DifferentialReviewerStatus::STATUS_ACCEPTED,
);
} else {
$data = array(
'status' => DifferentialReviewerStatus::STATUS_REJECTED,
);
}
$edits = array();
foreach ($object->getReviewerStatus() as $reviewer) {
if ($reviewer->hasAuthority($actor)) {
$edits[$reviewer->getReviewerPHID()] = array(
'data' => $data,
);
}
}
// Also either update or add the actor themselves as a reviewer.
$edits[$actor_phid] = array(
'data' => $data,
);
$results[] = id(new DifferentialTransaction())
->setTransactionType($type_edge)
->setMetadataValue('edge:type', $edge_reviewer)
->setIgnoreOnNoEffect(true)
->setNewValue(array('+' => $edits));
break;
case DifferentialAction::ACTION_CLAIM:
// If the user is commandeering, add the previous owner as a
// reviewer and remove the actor.
$edits = array(
'-' => array(
$actor_phid => $actor_phid,
),
);
$owner_phid = $object->getAuthorPHID();
if ($owner_phid) {
$reviewer = new DifferentialReviewer(
$owner_phid,
array(
'status' => DifferentialReviewerStatus::STATUS_ADDED,
));
$edits['+'] = array(
$owner_phid => array(
'data' => $reviewer->getEdgeData(),
),
);
}
// NOTE: We're setting setIsCommandeerSideEffect() on this because
// normally you can't add a revision's author as a reviewer, but
// this action swaps them after validation executes.
$results[] = id(new DifferentialTransaction())
->setTransactionType($type_edge)
->setMetadataValue('edge:type', $edge_reviewer)
->setIgnoreOnNoEffect(true)
->setIsCommandeerSideEffect(true)
->setNewValue($edits);
break;
case DifferentialAction::ACTION_RESIGN:
// If the user is resigning, add a separate reviewer edit
// transaction which removes them as a reviewer.
$results[] = id(new DifferentialTransaction())
->setTransactionType($type_edge)
->setMetadataValue('edge:type', $edge_reviewer)
->setIgnoreOnNoEffect(true)
->setNewValue(
array(
'-' => array(
$actor_phid => $actor_phid,
),
));
break;
}
break;
}
if (!$this->didExpandInlineState) {
switch ($xaction->getTransactionType()) {
case PhabricatorTransactions::TYPE_COMMENT:
case DifferentialTransaction::TYPE_ACTION:
case DifferentialTransaction::TYPE_UPDATE:
case DifferentialTransaction::TYPE_INLINE:
$this->didExpandInlineState = true;
$actor_phid = $this->getActingAsPHID();
$actor_is_author = ($object->getAuthorPHID() == $actor_phid);
if (!$actor_is_author) {
break;
}
$state_map = PhabricatorTransactions::getInlineStateMap();
$inlines = id(new DifferentialDiffInlineCommentQuery())
->setViewer($this->getActor())
->withRevisionPHIDs(array($object->getPHID()))
->withFixedStates(array_keys($state_map))
->execute();
if (!$inlines) {
break;
}
$old_value = mpull($inlines, 'getFixedState', 'getPHID');
$new_value = array();
foreach ($old_value as $key => $state) {
$new_value[$key] = $state_map[$state];
}
$results[] = id(new DifferentialTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_INLINESTATE)
->setIgnoreOnNoEffect(true)
->setOldValue($old_value)
->setNewValue($new_value);
break;
}
}
return $results;
}
protected function applyCustomExternalTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case DifferentialTransaction::TYPE_ACTION:
return;
case DifferentialTransaction::TYPE_INLINE:
$reply = $xaction->getComment()->getReplyToComment();
if ($reply && !$reply->getHasReplies()) {
$reply->setHasReplies(1)->save();
}
return;
case DifferentialTransaction::TYPE_UPDATE:
// Now that we're inside the transaction, do a final check.
$diff = $this->requireDiff($xaction->getNewValue());
// TODO: It would be slightly cleaner to just revalidate this
// transaction somehow using the same validation code, but that's
// not easy to do at the moment.
$revision_id = $diff->getRevisionID();
if ($revision_id && ($revision_id != $object->getID())) {
throw new Exception(
pht(
'Diff is already attached to another revision. You lost '.
'a race?'));
}
$diff->setRevisionID($object->getID());
$diff->save();
return;
}
return parent::applyCustomExternalTransaction($object, $xaction);
}
protected function applyBuiltinExternalTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorTransactions::TYPE_INLINESTATE:
$table = new DifferentialTransactionComment();
$conn_w = $table->establishConnection('w');
foreach ($xaction->getNewValue() as $phid => $state) {
queryfx(
$conn_w,
'UPDATE %T SET fixedState = %s WHERE phid = %s',
$table->getTableName(),
$state,
$phid);
}
break;
}
return parent::applyBuiltinExternalTransaction($object, $xaction);
}
protected function mergeEdgeData($type, array $u, array $v) {
$result = parent::mergeEdgeData($type, $u, $v);
switch ($type) {
case DifferentialRevisionHasReviewerEdgeType::EDGECONST:
// When the same reviewer has their status updated by multiple
// transactions, we want the strongest status to win. An example of
// this is when a user adds a comment and also accepts a revision which
// they are a reviewer on. The comment creates a "commented" status,
// while the accept creates an "accepted" status. Since accept is
// stronger, it should win and persist.
$u_status = idx($u, 'status');
$v_status = idx($v, 'status');
$u_str = DifferentialReviewerStatus::getStatusStrength($u_status);
$v_str = DifferentialReviewerStatus::getStatusStrength($v_status);
if ($u_str > $v_str) {
$result['status'] = $u_status;
} else {
$result['status'] = $v_status;
}
break;
}
return $result;
}
protected function applyFinalEffects(
PhabricatorLiskDAO $object,
array $xactions) {
// Load the most up-to-date version of the revision and its reviewers,
// so we don't need to try to deduce the state of reviewers by examining
// all the changes made by the transactions. Then, update the reviewers
// on the object to make sure we're acting on the current reviewer set
// (and, for example, sending mail to the right people).
$new_revision = id(new DifferentialRevisionQuery())
->setViewer($this->getActor())
->needReviewerStatus(true)
->needActiveDiffs(true)
->withIDs(array($object->getID()))
->executeOne();
if (!$new_revision) {
throw new Exception(
pht('Failed to load revision from transaction finalization.'));
}
$object->attachReviewerStatus($new_revision->getReviewerStatus());
$object->attachActiveDiff($new_revision->getActiveDiff());
$object->attachRepository($new_revision->getRepository());
foreach ($xactions as $xaction) {
switch ($xaction->getTransactionType()) {
case DifferentialTransaction::TYPE_UPDATE:
$diff = $this->requireDiff($xaction->getNewValue(), true);
// Update these denormalized index tables when we attach a new
// diff to a revision.
$this->updateRevisionHashTable($object, $diff);
$this->updateAffectedPathTable($object, $diff);
break;
}
}
$status_accepted = ArcanistDifferentialRevisionStatus::ACCEPTED;
$status_revision = ArcanistDifferentialRevisionStatus::NEEDS_REVISION;
$status_review = ArcanistDifferentialRevisionStatus::NEEDS_REVIEW;
$old_status = $object->getStatus();
switch ($old_status) {
case $status_accepted:
case $status_revision:
case $status_review:
// Try to move a revision to "accepted". We look for:
//
// - at least one accepting reviewer who is a user; and
// - no rejects; and
// - no rejects of older diffs; and
// - no blocking reviewers.
$has_accepting_user = false;
$has_rejecting_reviewer = false;
$has_rejecting_older_reviewer = false;
$has_blocking_reviewer = false;
foreach ($object->getReviewerStatus() as $reviewer) {
$reviewer_status = $reviewer->getStatus();
switch ($reviewer_status) {
case DifferentialReviewerStatus::STATUS_REJECTED:
$has_rejecting_reviewer = true;
break;
case DifferentialReviewerStatus::STATUS_REJECTED_OLDER:
$has_rejecting_older_reviewer = true;
break;
case DifferentialReviewerStatus::STATUS_BLOCKING:
$has_blocking_reviewer = true;
break;
case DifferentialReviewerStatus::STATUS_ACCEPTED:
if ($reviewer->isUser()) {
$has_accepting_user = true;
}
break;
}
}
$new_status = null;
if ($has_accepting_user &&
!$has_rejecting_reviewer &&
!$has_rejecting_older_reviewer &&
!$has_blocking_reviewer) {
$new_status = $status_accepted;
} else if ($has_rejecting_reviewer) {
// This isn't accepted, and there's at least one rejecting reviewer,
// so the revision needs changes. This usually happens after a
// "reject".
$new_status = $status_revision;
} else if ($old_status == $status_accepted) {
// This revision was accepted, but it no longer satisfies the
// conditions for acceptance. This usually happens after an accepting
// reviewer resigns or is removed.
$new_status = $status_review;
}
if ($new_status !== null && ($new_status != $old_status)) {
$xaction = id(new DifferentialTransaction())
->setTransactionType(DifferentialTransaction::TYPE_STATUS)
->setOldValue($old_status)
->setNewValue($new_status);
$xaction = $this->populateTransaction($object, $xaction)->save();
$xactions[] = $xaction;
$object->setStatus($new_status)->save();
}
break;
default:
// Revisions can't transition out of other statuses (like closed or
// abandoned) as a side effect of reviewer status changes.
break;
}
return $xactions;
}
protected function validateTransaction(
PhabricatorLiskDAO $object,
$type,
array $xactions) {
$errors = parent::validateTransaction($object, $type, $xactions);
$config_self_accept_key = 'differential.allow-self-accept';
$allow_self_accept = PhabricatorEnv::getEnvConfig($config_self_accept_key);
foreach ($xactions as $xaction) {
switch ($type) {
case PhabricatorTransactions::TYPE_EDGE:
switch ($xaction->getMetadataValue('edge:type')) {
case DifferentialRevisionHasReviewerEdgeType::EDGECONST:
// Prevent the author from becoming a reviewer.
// NOTE: This is pretty gross, but this restriction is unusual.
// If we end up with too much more of this, we should try to clean
// this up -- maybe by moving validation to after transactions
// are adjusted (so we can just examine the final value) or adding
// a second phase there?
$author_phid = $object->getAuthorPHID();
$new = $xaction->getNewValue();
$add = idx($new, '+', array());
$eq = idx($new, '=', array());
$phids = array_keys($add + $eq);
foreach ($phids as $phid) {
if (($phid == $author_phid) &&
!$allow_self_accept &&
!$xaction->getIsCommandeerSideEffect()) {
$errors[] =
new PhabricatorApplicationTransactionValidationError(
$type,
pht('Invalid'),
pht('The author of a revision can not be a reviewer.'),
$xaction);
}
}
break;
}
break;
case DifferentialTransaction::TYPE_UPDATE:
$diff = $this->loadDiff($xaction->getNewValue());
if (!$diff) {
$errors[] = new PhabricatorApplicationTransactionValidationError(
$type,
pht('Invalid'),
pht('The specified diff does not exist.'),
$xaction);
} else if (($diff->getRevisionID()) &&
($diff->getRevisionID() != $object->getID())) {
$errors[] = new PhabricatorApplicationTransactionValidationError(
$type,
pht('Invalid'),
pht(
'You can not update this revision to the specified diff, '.
'because the diff is already attached to another revision.'),
$xaction);
}
break;
case DifferentialTransaction::TYPE_ACTION:
$error = $this->validateDifferentialAction(
$object,
$type,
$xaction,
$xaction->getNewValue());
if ($error) {
$errors[] = new PhabricatorApplicationTransactionValidationError(
$type,
pht('Invalid'),
$error,
$xaction);
}
break;
}
}
return $errors;
}
private function validateDifferentialAction(
DifferentialRevision $revision,
$type,
DifferentialTransaction $xaction,
$action) {
$author_phid = $revision->getAuthorPHID();
$actor_phid = $this->getActingAsPHID();
$actor_is_author = ($author_phid == $actor_phid);
$config_abandon_key = 'differential.always-allow-abandon';
$always_allow_abandon = PhabricatorEnv::getEnvConfig($config_abandon_key);
$config_close_key = 'differential.always-allow-close';
$always_allow_close = PhabricatorEnv::getEnvConfig($config_close_key);
$config_reopen_key = 'differential.allow-reopen';
$allow_reopen = PhabricatorEnv::getEnvConfig($config_reopen_key);
$config_self_accept_key = 'differential.allow-self-accept';
$allow_self_accept = PhabricatorEnv::getEnvConfig($config_self_accept_key);
$revision_status = $revision->getStatus();
$status_accepted = ArcanistDifferentialRevisionStatus::ACCEPTED;
$status_abandoned = ArcanistDifferentialRevisionStatus::ABANDONED;
$status_closed = ArcanistDifferentialRevisionStatus::CLOSED;
switch ($action) {
case DifferentialAction::ACTION_ACCEPT:
if ($actor_is_author && !$allow_self_accept) {
return pht(
'You can not accept this revision because you are the owner.');
}
if ($revision_status == $status_abandoned) {
return pht(
'You can not accept this revision because it has been '.
'abandoned.');
}
if ($revision_status == $status_closed) {
return pht(
'You can not accept this revision because it has already been '.
'closed.');
}
// TODO: It would be nice to make this generic at some point.
$signatures = DifferentialRequiredSignaturesField::loadForRevision(
$revision);
foreach ($signatures as $phid => $signed) {
if (!$signed) {
return pht(
'You can not accept this revision because the author has '.
'not signed all of the required legal documents.');
}
}
break;
case DifferentialAction::ACTION_REJECT:
if ($actor_is_author) {
- return pht(
- 'You can not request changes to your own revision.');
+ return pht('You can not request changes to your own revision.');
}
if ($revision_status == $status_abandoned) {
return pht(
'You can not request changes to this revision because it has been '.
'abandoned.');
}
if ($revision_status == $status_closed) {
return pht(
'You can not request changes to this revision because it has '.
'already been closed.');
}
break;
case DifferentialAction::ACTION_RESIGN:
// You can always resign from a revision if you're a reviewer. If you
// aren't, this is a no-op rather than invalid.
break;
case DifferentialAction::ACTION_CLAIM:
// You can claim a revision if you're not the owner. If you are, this
// is a no-op rather than invalid.
if ($revision_status == $status_closed) {
return pht(
'You can not commandeer this revision because it has already been '.
'closed.');
}
break;
case DifferentialAction::ACTION_ABANDON:
if (!$actor_is_author && !$always_allow_abandon) {
return pht(
'You can not abandon this revision because you do not own it. '.
'You can only abandon revisions you own.');
}
if ($revision_status == $status_closed) {
return pht(
'You can not abandon this revision because it has already been '.
'closed.');
}
// NOTE: Abandons of already-abandoned revisions are treated as no-op
// instead of invalid. Other abandons are OK.
break;
case DifferentialAction::ACTION_RECLAIM:
if (!$actor_is_author) {
return pht(
'You can not reclaim this revision because you do not own '.
'it. You can only reclaim revisions you own.');
}
if ($revision_status == $status_closed) {
return pht(
'You can not reclaim this revision because it has already been '.
'closed.');
}
// NOTE: Reclaims of other non-abandoned revisions are treated as no-op
// instead of invalid.
break;
case DifferentialAction::ACTION_REOPEN:
if (!$allow_reopen) {
return pht(
'The reopen action is not enabled on this Phabricator install. '.
'Adjust your configuration to enable it.');
}
// NOTE: If the revision is not closed, this is caught as a no-op
// instead of an invalid transaction.
break;
case DifferentialAction::ACTION_RETHINK:
if (!$actor_is_author) {
return pht(
'You can not plan changes to this revision because you do not '.
'own it. To plan changes to a revision, you must be its owner.');
}
switch ($revision_status) {
case ArcanistDifferentialRevisionStatus::ACCEPTED:
case ArcanistDifferentialRevisionStatus::NEEDS_REVISION:
case ArcanistDifferentialRevisionStatus::NEEDS_REVIEW:
// These are OK.
break;
case ArcanistDifferentialRevisionStatus::CHANGES_PLANNED:
// Let this through, it's a no-op.
break;
case ArcanistDifferentialRevisionStatus::ABANDONED:
return pht(
'You can not plan changes to this revision because it has '.
'been abandoned.');
case ArcanistDifferentialRevisionStatus::CLOSED:
return pht(
'You can not plan changes to this revision because it has '.
'already been closed.');
default:
throw new Exception(
pht(
'Encountered unexpected revision status ("%s") when '.
'validating "%s" action.',
$revision_status,
$action));
}
break;
case DifferentialAction::ACTION_REQUEST:
if (!$actor_is_author) {
return pht(
'You can not request review of this revision because you do '.
'not own it. To request review of a revision, you must be its '.
'owner.');
}
switch ($revision_status) {
case ArcanistDifferentialRevisionStatus::ACCEPTED:
case ArcanistDifferentialRevisionStatus::NEEDS_REVISION:
case ArcanistDifferentialRevisionStatus::CHANGES_PLANNED:
// These are OK.
break;
case ArcanistDifferentialRevisionStatus::NEEDS_REVIEW:
// This will be caught as "no effect" later on.
break;
case ArcanistDifferentialRevisionStatus::ABANDONED:
return pht(
'You can not request review of this revision because it has '.
'been abandoned. Instead, reclaim it.');
case ArcanistDifferentialRevisionStatus::CLOSED:
return pht(
'You can not request review of this revision because it has '.
'already been closed.');
default:
throw new Exception(
pht(
'Encountered unexpected revision status ("%s") when '.
'validating "%s" action.',
$revision_status,
$action));
}
break;
case DifferentialAction::ACTION_CLOSE:
// We force revisions closed when we discover a corresponding commit.
// In this case, revisions are allowed to transition to closed from
// any state. This is an automated action taken by the daemons.
if (!$this->getIsCloseByCommit()) {
if (!$actor_is_author && !$always_allow_close) {
return pht(
'You can not close this revision because you do not own it. To '.
'close a revision, you must be its owner.');
}
if ($revision_status != $status_accepted) {
return pht(
'You can not close this revision because it has not been '.
'accepted. You can only close accepted revisions.');
}
}
break;
}
return null;
}
protected function sortTransactions(array $xactions) {
$xactions = parent::sortTransactions($xactions);
$head = array();
$tail = array();
foreach ($xactions as $xaction) {
$type = $xaction->getTransactionType();
if ($type == DifferentialTransaction::TYPE_INLINE) {
$tail[] = $xaction;
} else {
$head[] = $xaction;
}
}
return array_values(array_merge($head, $tail));
}
protected function requireCapabilities(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {}
return parent::requireCapabilities($object, $xaction);
}
protected function shouldPublishFeedStory(
PhabricatorLiskDAO $object,
array $xactions) {
return true;
}
protected function shouldSendMail(
PhabricatorLiskDAO $object,
array $xactions) {
return true;
}
protected function getMailTo(PhabricatorLiskDAO $object) {
$phids = array();
$phids[] = $object->getAuthorPHID();
foreach ($object->getReviewerStatus() as $reviewer) {
$phids[] = $reviewer->getReviewerPHID();
}
return $phids;
}
protected function getMailAction(
PhabricatorLiskDAO $object,
array $xactions) {
$action = parent::getMailAction($object, $xactions);
$strongest = $this->getStrongestAction($object, $xactions);
switch ($strongest->getTransactionType()) {
case DifferentialTransaction::TYPE_UPDATE:
$count = new PhutilNumber($object->getLineCount());
$action = pht('%s, %s line(s)', $action, $count);
break;
}
return $action;
}
protected function getMailSubjectPrefix() {
return PhabricatorEnv::getEnvConfig('metamta.differential.subject-prefix');
}
protected function getMailThreadID(PhabricatorLiskDAO $object) {
// This is nonstandard, but retains threading with older messages.
$phid = $object->getPHID();
return "differential-rev-{$phid}-req";
}
protected function buildReplyHandler(PhabricatorLiskDAO $object) {
return id(new DifferentialReplyHandler())
->setMailReceiver($object);
}
protected function buildMailTemplate(PhabricatorLiskDAO $object) {
$id = $object->getID();
$title = $object->getTitle();
$original_title = $object->getOriginalTitle();
$subject = "D{$id}: {$title}";
$thread_topic = "D{$id}: {$original_title}";
return id(new PhabricatorMetaMTAMail())
->setSubject($subject)
->addHeader('Thread-Topic', $thread_topic);
}
protected function buildMailBody(
PhabricatorLiskDAO $object,
array $xactions) {
$body = new PhabricatorMetaMTAMailBody();
$body->setViewer($this->requireActor());
$this->addHeadersAndCommentsToMailBody($body, $xactions);
$type_inline = DifferentialTransaction::TYPE_INLINE;
$inlines = array();
foreach ($xactions as $xaction) {
if ($xaction->getTransactionType() == $type_inline) {
$inlines[] = $xaction;
}
}
if ($inlines) {
$body->addTextSection(
pht('INLINE COMMENTS'),
$this->renderInlineCommentsForMail($object, $inlines));
}
$changed_uri = $this->getChangedPriorToCommitURI();
if ($changed_uri) {
$body->addLinkSection(
pht('CHANGED PRIOR TO COMMIT'),
$changed_uri);
}
$this->addCustomFieldsToMailBody($body, $object, $xactions);
$body->addLinkSection(
pht('REVISION DETAIL'),
PhabricatorEnv::getProductionURI('/D'.$object->getID()));
$update_xaction = null;
foreach ($xactions as $xaction) {
switch ($xaction->getTransactionType()) {
case DifferentialTransaction::TYPE_UPDATE:
$update_xaction = $xaction;
break;
}
}
if ($update_xaction) {
$diff = $this->requireDiff($update_xaction->getNewValue(), true);
$body->addTextSection(
pht('AFFECTED FILES'),
$this->renderAffectedFilesForMail($diff));
$config_key_inline = 'metamta.differential.inline-patches';
$config_inline = PhabricatorEnv::getEnvConfig($config_key_inline);
$config_key_attach = 'metamta.differential.attach-patches';
$config_attach = PhabricatorEnv::getEnvConfig($config_key_attach);
if ($config_inline || $config_attach) {
$patch_section = $this->renderPatchForMail($diff);
$lines = count(phutil_split_lines($patch_section->getPlaintext()));
if ($config_inline && ($lines <= $config_inline)) {
$body->addTextSection(
pht('CHANGE DETAILS'),
$patch_section);
}
if ($config_attach) {
$name = pht('D%s.%s.patch', $object->getID(), $diff->getID());
$mime_type = 'text/x-patch; charset=utf-8';
$body->addAttachment(
new PhabricatorMetaMTAAttachment(
$patch_section->getPlaintext(), $name, $mime_type));
}
}
}
return $body;
}
public function getMailTagsMap() {
return array(
MetaMTANotificationType::TYPE_DIFFERENTIAL_REVIEW_REQUEST =>
pht('A revision is created.'),
MetaMTANotificationType::TYPE_DIFFERENTIAL_UPDATED =>
pht('A revision is updated.'),
MetaMTANotificationType::TYPE_DIFFERENTIAL_COMMENT =>
pht('Someone comments on a revision.'),
MetaMTANotificationType::TYPE_DIFFERENTIAL_CLOSED =>
pht('A revision is closed.'),
MetaMTANotificationType::TYPE_DIFFERENTIAL_REVIEWERS =>
pht("A revision's reviewers change."),
MetaMTANotificationType::TYPE_DIFFERENTIAL_CC =>
pht("A revision's CCs change."),
MetaMTANotificationType::TYPE_DIFFERENTIAL_OTHER =>
pht('Other revision activity not listed above occurs.'),
);
}
protected function supportsSearch() {
return true;
}
protected function extractFilePHIDsFromCustomTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {}
return parent::extractFilePHIDsFromCustomTransaction($object, $xaction);
}
protected function expandCustomRemarkupBlockTransactions(
PhabricatorLiskDAO $object,
array $xactions,
$blocks,
PhutilMarkupEngine $engine) {
$flat_blocks = array_mergev($blocks);
$huge_block = implode("\n\n", $flat_blocks);
$task_map = array();
$task_refs = id(new ManiphestCustomFieldStatusParser())
->parseCorpus($huge_block);
foreach ($task_refs as $match) {
foreach ($match['monograms'] as $monogram) {
$task_id = (int)trim($monogram, 'tT');
$task_map[$task_id] = true;
}
}
$rev_map = array();
$rev_refs = id(new DifferentialCustomFieldDependsOnParser())
->parseCorpus($huge_block);
foreach ($rev_refs as $match) {
foreach ($match['monograms'] as $monogram) {
$rev_id = (int)trim($monogram, 'dD');
$rev_map[$rev_id] = true;
}
}
$edges = array();
$task_phids = array();
$rev_phids = array();
if ($task_map) {
$tasks = id(new ManiphestTaskQuery())
->setViewer($this->getActor())
->withIDs(array_keys($task_map))
->execute();
if ($tasks) {
$task_phids = mpull($tasks, 'getPHID', 'getPHID');
$edge_related = DifferentialRevisionHasTaskEdgeType::EDGECONST;
$edges[$edge_related] = $task_phids;
}
}
if ($rev_map) {
$revs = id(new DifferentialRevisionQuery())
->setViewer($this->getActor())
->withIDs(array_keys($rev_map))
->execute();
$rev_phids = mpull($revs, 'getPHID', 'getPHID');
// NOTE: Skip any write attempts if a user cleverly implies a revision
// depends upon itself.
unset($rev_phids[$object->getPHID()]);
if ($revs) {
$depends = DifferentialRevisionDependsOnRevisionEdgeType::EDGECONST;
$edges[$depends] = $rev_phids;
}
}
$this->setUnmentionablePHIDMap(array_merge($task_phids, $rev_phids));
$result = array();
foreach ($edges as $type => $specs) {
$result[] = id(new DifferentialTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
->setMetadataValue('edge:type', $type)
->setNewValue(array('+' => $specs));
}
return $result;
}
protected function indentForMail(array $lines) {
$indented = array();
foreach ($lines as $line) {
$indented[] = '> '.$line;
}
return $indented;
}
protected function nestCommentHistory(
DifferentialTransactionComment $comment, array $comments_by_line_number,
array $users_by_phid) {
$nested = array();
$previous_comments = $comments_by_line_number[$comment->getChangesetID()]
[$comment->getLineNumber()];
foreach ($previous_comments as $previous_comment) {
if ($previous_comment->getID() >= $comment->getID()) {
break;
}
$nested = $this->indentForMail(
array_merge(
$nested,
explode("\n", $previous_comment->getContent())));
$user = idx($users_by_phid, $previous_comment->getAuthorPHID(), null);
if ($user) {
array_unshift($nested, pht('%s wrote:', $user->getUserName()));
}
}
$nested = array_merge($nested, explode("\n", $comment->getContent()));
return implode("\n", $nested);
}
private function renderInlineCommentsForMail(
PhabricatorLiskDAO $object,
array $inlines) {
$context_key = 'metamta.differential.unified-comment-context';
$show_context = PhabricatorEnv::getEnvConfig($context_key);
$changeset_ids = array();
$line_numbers_by_changeset = array();
foreach ($inlines as $inline) {
$id = $inline->getComment()->getChangesetID();
$changeset_ids[$id] = $id;
$line_numbers_by_changeset[$id][] =
$inline->getComment()->getLineNumber();
}
$changesets = id(new DifferentialChangesetQuery())
->setViewer($this->getActor())
->withIDs($changeset_ids)
->needHunks(true)
->execute();
$inline_groups = DifferentialTransactionComment::sortAndGroupInlines(
$inlines,
$changesets);
if ($show_context) {
$hunk_parser = new DifferentialHunkParser();
$table = new DifferentialTransactionComment();
$conn_r = $table->establishConnection('r');
$queries = array();
foreach ($line_numbers_by_changeset as $id => $line_numbers) {
$queries[] = qsprintf(
$conn_r,
'(changesetID = %d AND lineNumber IN (%Ld))',
$id, $line_numbers);
}
$all_comments = id(new DifferentialTransactionComment())->loadAllWhere(
'transactionPHID IS NOT NULL AND (%Q)', implode(' OR ', $queries));
$comments_by_line_number = array();
foreach ($all_comments as $comment) {
$comments_by_line_number
[$comment->getChangesetID()]
[$comment->getLineNumber()]
[$comment->getID()] = $comment;
}
$author_phids = mpull($all_comments, 'getAuthorPHID');
$authors = id(new PhabricatorPeopleQuery())
->setViewer($this->getActor())
->withPHIDs($author_phids)
->execute();
$authors_by_phid = mpull($authors, null, 'getPHID');
}
$section = new PhabricatorMetaMTAMailSection();
foreach ($inline_groups as $changeset_id => $group) {
$changeset = idx($changesets, $changeset_id);
if (!$changeset) {
continue;
}
foreach ($group as $inline) {
$comment = $inline->getComment();
$file = $changeset->getFilename();
$start = $comment->getLineNumber();
$len = $comment->getLineLength();
if ($len) {
$range = $start.'-'.($start + $len);
} else {
$range = $start;
}
$inline_content = $comment->getContent();
if (!$show_context) {
$section->addFragment("{$file}:{$range} {$inline_content}");
} else {
$patch = $hunk_parser->makeContextDiff(
$changeset->getHunks(),
$comment->getIsNewFile(),
$comment->getLineNumber(),
$comment->getLineLength(),
1);
$nested_comments = $this->nestCommentHistory(
$inline->getComment(), $comments_by_line_number, $authors_by_phid);
- $section->addFragment('================')
- ->addFragment('Comment at: '.$file.':'.$range)
- ->addPlaintextFragment($patch)
- ->addHTMLFragment($this->renderPatchHTMLForMail($patch))
- ->addFragment('----------------')
- ->addFragment($nested_comments)
- ->addFragment(null);
+ $section
+ ->addFragment('================')
+ ->addFragment(pht('Comment at: %s:%s', $file, $range))
+ ->addPlaintextFragment($patch)
+ ->addHTMLFragment($this->renderPatchHTMLForMail($patch))
+ ->addFragment('----------------')
+ ->addFragment($nested_comments)
+ ->addFragment(null);
}
}
}
return $section;
}
private function loadDiff($phid, $need_changesets = false) {
$query = id(new DifferentialDiffQuery())
->withPHIDs(array($phid))
->setViewer($this->getActor());
if ($need_changesets) {
$query->needChangesets(true);
}
return $query->executeOne();
}
private function requireDiff($phid, $need_changesets = false) {
$diff = $this->loadDiff($phid, $need_changesets);
if (!$diff) {
throw new Exception(pht('Diff "%s" does not exist!', $phid));
}
return $diff;
}
/* -( Herald Integration )------------------------------------------------- */
protected function shouldApplyHeraldRules(
PhabricatorLiskDAO $object,
array $xactions) {
if ($this->getIsNewObject()) {
return true;
}
foreach ($xactions as $xaction) {
switch ($xaction->getTransactionType()) {
case DifferentialTransaction::TYPE_UPDATE:
if (!$this->getIsCloseByCommit()) {
return true;
}
break;
case DifferentialTransaction::TYPE_ACTION:
switch ($xaction->getNewValue()) {
case DifferentialAction::ACTION_CLAIM:
// When users commandeer revisions, we may need to trigger
// signatures or author-based rules.
return true;
}
break;
}
}
return parent::shouldApplyHeraldRules($object, $xactions);
}
protected function buildHeraldAdapter(
PhabricatorLiskDAO $object,
array $xactions) {
$unsubscribed_phids = PhabricatorEdgeQuery::loadDestinationPHIDs(
$object->getPHID(),
PhabricatorObjectHasUnsubscriberEdgeType::EDGECONST);
$subscribed_phids = PhabricatorSubscribersQuery::loadSubscribersForPHID(
$object->getPHID());
$revision = id(new DifferentialRevisionQuery())
->setViewer($this->getActor())
->withPHIDs(array($object->getPHID()))
->needActiveDiffs(true)
->needReviewerStatus(true)
->executeOne();
if (!$revision) {
throw new Exception(
- pht(
- 'Failed to load revision for Herald adapter construction!'));
+ pht('Failed to load revision for Herald adapter construction!'));
}
$adapter = HeraldDifferentialRevisionAdapter::newLegacyAdapter(
$revision,
$revision->getActiveDiff());
$reviewers = $revision->getReviewerStatus();
$reviewer_phids = mpull($reviewers, 'getReviewerPHID');
$adapter->setExplicitCCs($subscribed_phids);
$adapter->setExplicitReviewers($reviewer_phids);
$adapter->setForbiddenCCs($unsubscribed_phids);
return $adapter;
}
protected function didApplyHeraldRules(
PhabricatorLiskDAO $object,
HeraldAdapter $adapter,
HeraldTranscript $transcript) {
$xactions = array();
// Build a transaction to adjust CCs.
$ccs = array(
'+' => array_keys($adapter->getCCsAddedByHerald()),
'-' => array_keys($adapter->getCCsRemovedByHerald()),
);
$value = array();
foreach ($ccs as $type => $phids) {
foreach ($phids as $phid) {
$value[$type][$phid] = $phid;
}
}
if ($value) {
$xactions[] = id(new DifferentialTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS)
->setNewValue($value);
}
// Build a transaction to adjust reviewers.
$reviewers = array(
DifferentialReviewerStatus::STATUS_ADDED =>
array_keys($adapter->getReviewersAddedByHerald()),
DifferentialReviewerStatus::STATUS_BLOCKING =>
array_keys($adapter->getBlockingReviewersAddedByHerald()),
);
$old_reviewers = $object->getReviewerStatus();
$old_reviewers = mpull($old_reviewers, null, 'getReviewerPHID');
$value = array();
foreach ($reviewers as $status => $phids) {
foreach ($phids as $phid) {
if ($phid == $object->getAuthorPHID()) {
// Don't try to add the revision's author as a reviewer, since this
// isn't valid and doesn't make sense.
continue;
}
// If the target is already a reviewer, don't try to change anything
// if their current status is at least as strong as the new status.
// For example, don't downgrade an "Accepted" to a "Blocking Reviewer".
$old_reviewer = idx($old_reviewers, $phid);
if ($old_reviewer) {
$old_status = $old_reviewer->getStatus();
$old_strength = DifferentialReviewerStatus::getStatusStrength(
$old_status);
$new_strength = DifferentialReviewerStatus::getStatusStrength(
$status);
if ($new_strength <= $old_strength) {
continue;
}
}
$value['+'][$phid] = array(
'data' => array(
'status' => $status,
),
);
}
}
if ($value) {
$edge_reviewer = DifferentialRevisionHasReviewerEdgeType::EDGECONST;
$xactions[] = id(new DifferentialTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
->setMetadataValue('edge:type', $edge_reviewer)
->setNewValue($value);
}
// Require legalpad document signatures.
$legal_phids = $adapter->getRequiredSignatureDocumentPHIDs();
if ($legal_phids) {
// We only require signatures of documents which have not already
// been signed. In general, this reduces the amount of churn that
// signature rules cause.
$signatures = id(new LegalpadDocumentSignatureQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withDocumentPHIDs($legal_phids)
->withSignerPHIDs(array($object->getAuthorPHID()))
->execute();
$signed_phids = mpull($signatures, 'getDocumentPHID');
$legal_phids = array_diff($legal_phids, $signed_phids);
// If we still have something to trigger, add the edges.
if ($legal_phids) {
$edge_legal = LegalpadObjectNeedsSignatureEdgeType::EDGECONST;
$xactions[] = id(new DifferentialTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
->setMetadataValue('edge:type', $edge_legal)
->setNewValue(
array(
'+' => array_fuse($legal_phids),
));
}
}
// Apply build plans.
HarbormasterBuildable::applyBuildPlans(
$adapter->getDiff()->getPHID(),
$adapter->getPHID(),
$adapter->getBuildPlans());
return $xactions;
}
/**
* Update the table which links Differential revisions to paths they affect,
* so Diffusion can efficiently find pending revisions for a given file.
*/
private function updateAffectedPathTable(
DifferentialRevision $revision,
DifferentialDiff $diff) {
$repository = $revision->getRepository();
if (!$repository) {
// The repository where the code lives is untracked.
return;
}
$path_prefix = null;
$local_root = $diff->getSourceControlPath();
if ($local_root) {
// We're in a working copy which supports subdirectory checkouts (e.g.,
// SVN) so we need to figure out what prefix we should add to each path
// (e.g., trunk/projects/example/) to get the absolute path from the
// root of the repository. DVCS systems like Git and Mercurial are not
// affected.
// Normalize both paths and check if the repository root is a prefix of
// the local root. If so, throw it away. Note that this correctly handles
// the case where the remote path is "/".
$local_root = id(new PhutilURI($local_root))->getPath();
$local_root = rtrim($local_root, '/');
$repo_root = id(new PhutilURI($repository->getRemoteURI()))->getPath();
$repo_root = rtrim($repo_root, '/');
if (!strncmp($repo_root, $local_root, strlen($repo_root))) {
$path_prefix = substr($local_root, strlen($repo_root));
}
}
$changesets = $diff->getChangesets();
$paths = array();
foreach ($changesets as $changeset) {
$paths[] = $path_prefix.'/'.$changeset->getFilename();
}
// Mark this as also touching all parent paths, so you can see all pending
// changes to any file within a directory.
$all_paths = array();
foreach ($paths as $local) {
foreach (DiffusionPathIDQuery::expandPathToRoot($local) as $path) {
$all_paths[$path] = true;
}
}
$all_paths = array_keys($all_paths);
$path_ids =
PhabricatorRepositoryCommitChangeParserWorker::lookupOrCreatePaths(
$all_paths);
$table = new DifferentialAffectedPath();
$conn_w = $table->establishConnection('w');
$sql = array();
foreach ($path_ids as $path_id) {
$sql[] = qsprintf(
$conn_w,
'(%d, %d, %d, %d)',
$repository->getID(),
$path_id,
time(),
$revision->getID());
}
queryfx(
$conn_w,
'DELETE FROM %T WHERE revisionID = %d',
$table->getTableName(),
$revision->getID());
foreach (array_chunk($sql, 256) as $chunk) {
queryfx(
$conn_w,
'INSERT INTO %T (repositoryID, pathID, epoch, revisionID) VALUES %Q',
$table->getTableName(),
implode(', ', $chunk));
}
}
/**
* Update the table connecting revisions to DVCS local hashes, so we can
* identify revisions by commit/tree hashes.
*/
private function updateRevisionHashTable(
DifferentialRevision $revision,
DifferentialDiff $diff) {
$vcs = $diff->getSourceControlSystem();
if ($vcs == DifferentialRevisionControlSystem::SVN) {
// Subversion has no local commit or tree hash information, so we don't
// have to do anything.
return;
}
$property = id(new DifferentialDiffProperty())->loadOneWhere(
'diffID = %d AND name = %s',
$diff->getID(),
'local:commits');
if (!$property) {
return;
}
$hashes = array();
$data = $property->getData();
switch ($vcs) {
case DifferentialRevisionControlSystem::GIT:
foreach ($data as $commit) {
$hashes[] = array(
ArcanistDifferentialRevisionHash::HASH_GIT_COMMIT,
$commit['commit'],
);
$hashes[] = array(
ArcanistDifferentialRevisionHash::HASH_GIT_TREE,
$commit['tree'],
);
}
break;
case DifferentialRevisionControlSystem::MERCURIAL:
foreach ($data as $commit) {
$hashes[] = array(
ArcanistDifferentialRevisionHash::HASH_MERCURIAL_COMMIT,
$commit['rev'],
);
}
break;
}
$conn_w = $revision->establishConnection('w');
$sql = array();
foreach ($hashes as $info) {
list($type, $hash) = $info;
$sql[] = qsprintf(
$conn_w,
'(%d, %s, %s)',
$revision->getID(),
$type,
$hash);
}
queryfx(
$conn_w,
'DELETE FROM %T WHERE revisionID = %d',
ArcanistDifferentialRevisionHash::TABLE_NAME,
$revision->getID());
if ($sql) {
queryfx(
$conn_w,
'INSERT INTO %T (revisionID, type, hash) VALUES %Q',
ArcanistDifferentialRevisionHash::TABLE_NAME,
implode(', ', $sql));
}
}
private function renderAffectedFilesForMail(DifferentialDiff $diff) {
$changesets = $diff->getChangesets();
$filenames = mpull($changesets, 'getDisplayFilename');
sort($filenames);
$count = count($filenames);
$max = 250;
if ($count > $max) {
$filenames = array_slice($filenames, 0, $max);
$filenames[] = pht('(%d more files...)', ($count - $max));
}
return implode("\n", $filenames);
}
private function renderPatchHTMLForMail($patch) {
return phutil_tag('pre',
array('style' => 'font-family: monospace;'), $patch);
}
private function renderPatchForMail(DifferentialDiff $diff) {
$format = PhabricatorEnv::getEnvConfig('metamta.differential.patch-format');
$patch = id(new DifferentialRawDiffRenderer())
->setViewer($this->getActor())
->setFormat($format)
->setChangesets($diff->getChangesets())
->buildPatch();
$section = new PhabricatorMetaMTAMailSection();
$section->addHTMLFragment($this->renderPatchHTMLForMail($patch));
$section->addPlaintextFragment($patch);
return $section;
}
}
diff --git a/src/applications/differential/landing/DifferentialGitHubLandingStrategy.php b/src/applications/differential/landing/DifferentialGitHubLandingStrategy.php
index 7160743bb..c805083cd 100644
--- a/src/applications/differential/landing/DifferentialGitHubLandingStrategy.php
+++ b/src/applications/differential/landing/DifferentialGitHubLandingStrategy.php
@@ -1,177 +1,181 @@
<?php
final class DifferentialGitHubLandingStrategy
extends DifferentialLandingStrategy {
private $account;
private $provider;
public function processLandRequest(
AphrontRequest $request,
DifferentialRevision $revision,
PhabricatorRepository $repository) {
$viewer = $request->getUser();
$this->init($viewer, $repository);
$workspace = $this->getGitWorkspace($repository);
try {
id(new DifferentialHostedGitLandingStrategy())
->commitRevisionToWorkspace($revision, $workspace, $viewer);
} catch (Exception $e) {
- throw new PhutilProxyException('Failed to commit patch', $e);
+ throw new PhutilProxyException(pht('Failed to commit patch.'), $e);
}
try {
$this->pushWorkspaceRepository($repository, $workspace);
} catch (Exception $e) {
// If it's a permission problem, we know more than git.
$dialog = $this->verifyRemotePermissions($viewer, $revision, $repository);
if ($dialog) {
return $dialog;
}
// Else, throw what git said.
- throw new PhutilProxyException('Failed to push changes upstream', $e);
+ throw new PhutilProxyException(
+ pht('Failed to push changes upstream.'),
+ $e);
}
}
/**
* Returns PhabricatorActionView or an array of PhabricatorActionView or null.
*/
public function createMenuItem(
PhabricatorUser $viewer,
DifferentialRevision $revision,
PhabricatorRepository $repository) {
$vcs = $repository->getVersionControlSystem();
if ($vcs !== PhabricatorRepositoryType::REPOSITORY_TYPE_GIT) {
return;
}
if ($repository->isHosted()) {
return;
}
try {
// These throw when failing.
$this->init($viewer, $repository);
$this->findGitHubRepo($repository);
} catch (Exception $e) {
return;
}
return $this->createActionView($revision, pht('Land to GitHub'))
->setIcon('fa-cloud-upload');
}
public function pushWorkspaceRepository(
PhabricatorRepository $repository,
ArcanistRepositoryAPI $workspace) {
$token = $this->getAccessToken();
$github_repo = $this->findGitHubRepo($repository);
$remote = urisprintf(
'https://%s:x-oauth-basic@%s/%s.git',
$token,
$this->provider->getProviderDomain(),
$github_repo);
$workspace->execxLocal(
'push %P HEAD:master',
new PhutilOpaqueEnvelope($remote));
}
private function init($viewer, $repository) {
$repo_uri = $repository->getRemoteURIObject();
$repo_domain = $repo_uri->getDomain();
$this->account = id(new PhabricatorExternalAccountQuery())
->setViewer($viewer)
->withUserPHIDs(array($viewer->getPHID()))
->withAccountTypes(array('github'))
->withAccountDomains(array($repo_domain))
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
if (!$this->account) {
throw new Exception(
- "No matching GitHub account found for {$repo_domain}.");
+ pht('No matching GitHub account found for %s.', $repo_domain));
}
$this->provider = PhabricatorAuthProvider::getEnabledProviderByKey(
$this->account->getProviderKey());
if (!$this->provider) {
- throw new Exception("GitHub provider for {$repo_domain} is not enabled.");
+ throw new Exception(
+ pht('GitHub provider for %s is not enabled.', $repo_domain));
}
}
private function findGitHubRepo(PhabricatorRepository $repository) {
$repo_uri = $repository->getRemoteURIObject();
$repo_path = $repo_uri->getPath();
if (substr($repo_path, -4) == '.git') {
$repo_path = substr($repo_path, 0, -4);
}
$repo_path = ltrim($repo_path, '/');
return $repo_path;
}
private function getAccessToken() {
return $this->provider->getOAuthAccessToken($this->account);
}
private function verifyRemotePermissions($viewer, $revision, $repository) {
$github_user = $this->account->getUsername();
$github_repo = $this->findGitHubRepo($repository);
$uri = urisprintf(
'https://api.github.com/repos/%s/collaborators/%s',
$github_repo,
$github_user);
$uri = new PhutilURI($uri);
$uri->setQueryParam('access_token', $this->getAccessToken());
list($status, $body, $headers) = id(new HTTPSFuture($uri))->resolve();
// Likely status codes:
// 204 No Content: Has permissions. Token might be too weak.
// 404 Not Found: Not a collaborator.
// 401 Unauthorized: Token is bad/revoked.
$no_permission = ($status->getStatusCode() == 404);
if ($no_permission) {
throw new Exception(
- "You don't have permission to push to this repository. \n".
- "Push permissions for this repository are managed on GitHub.");
+ pht(
+ "You don't have permission to push to this repository. ".
+ "Push permissions for this repository are managed on GitHub."));
}
$scopes = BaseHTTPFuture::getHeader($headers, 'X-OAuth-Scopes');
if (strpos($scopes, 'public_repo') === false) {
$provider_key = $this->provider->getProviderKey();
$refresh_token_uri = new PhutilURI("/auth/refresh/{$provider_key}/");
$refresh_token_uri->setQueryParam('scope', 'public_repo');
return id(new AphrontDialogView())
->setUser($viewer)
->setTitle(pht('Stronger token needed'))
->appendChild(pht(
- 'In order to complete this action, you need a '.
- 'stronger GitHub token.'))
+ 'In order to complete this action, you need a '.
+ 'stronger GitHub token.'))
->setSubmitURI($refresh_token_uri)
->addCancelButton('/D'.$revision->getId())
->setDisableWorkflowOnSubmit(true)
->addSubmitButton(pht('Refresh Account Link'));
}
}
}
diff --git a/src/applications/differential/landing/DifferentialHostedGitLandingStrategy.php b/src/applications/differential/landing/DifferentialHostedGitLandingStrategy.php
index 68f0980db..cc0cf9799 100644
--- a/src/applications/differential/landing/DifferentialHostedGitLandingStrategy.php
+++ b/src/applications/differential/landing/DifferentialHostedGitLandingStrategy.php
@@ -1,123 +1,127 @@
<?php
final class DifferentialHostedGitLandingStrategy
extends DifferentialLandingStrategy {
public function processLandRequest(
AphrontRequest $request,
DifferentialRevision $revision,
PhabricatorRepository $repository) {
$viewer = $request->getUser();
$workspace = $this->getGitWorkspace($repository);
try {
$this->commitRevisionToWorkspace($revision, $workspace, $viewer);
} catch (Exception $e) {
- throw new PhutilProxyException('Failed to commit patch', $e);
+ throw new PhutilProxyException(
+ pht('Failed to commit patch.'),
+ $e);
}
try {
$this->pushWorkspaceRepository($repository, $workspace, $viewer);
} catch (Exception $e) {
- throw new PhutilProxyException('Failed to push changes upstream', $e);
+ throw new PhutilProxyException(
+ pht('Failed to push changes upstream.'),
+ $e);
}
}
public function commitRevisionToWorkspace(
DifferentialRevision $revision,
ArcanistRepositoryAPI $workspace,
PhabricatorUser $user) {
$diff_id = $revision->loadActiveDiff()->getID();
$call = new ConduitCall(
'differential.getrawdiff',
array(
'diffID' => $diff_id,
));
$call->setUser($user);
$raw_diff = $call->execute();
$missing_binary =
"\nindex "
."0000000000000000000000000000000000000000.."
."0000000000000000000000000000000000000000\n";
if (strpos($raw_diff, $missing_binary) !== false) {
- throw new Exception('Patch is missing content for a binary file');
+ throw new Exception(pht('Patch is missing content for a binary file'));
}
$future = $workspace->execFutureLocal('apply --index -');
$future->write($raw_diff);
$future->resolvex();
$workspace->reloadWorkingCopy();
$call = new ConduitCall(
'differential.getcommitmessage',
array(
'revision_id' => $revision->getID(),
));
$call->setUser($user);
$message = $call->execute();
$author = id(new PhabricatorUser())->loadOneWhere(
'phid = %s',
$revision->getAuthorPHID());
$author_string = sprintf(
'%s <%s>',
$author->getRealName(),
$author->loadPrimaryEmailAddress());
$author_date = $revision->getDateCreated();
$workspace->execxLocal(
'-c user.name=%s -c user.email=%s '.
'commit --date=%s --author=%s '.
'--message=%s',
// -c will set the 'committer'
$user->getRealName(),
$user->loadPrimaryEmailAddress(),
$author_date,
$author_string,
$message);
}
public function pushWorkspaceRepository(
PhabricatorRepository $repository,
ArcanistRepositoryAPI $workspace,
PhabricatorUser $user) {
$workspace->execxLocal('push origin HEAD:master');
}
public function createMenuItem(
PhabricatorUser $viewer,
DifferentialRevision $revision,
PhabricatorRepository $repository) {
$vcs = $repository->getVersionControlSystem();
if ($vcs !== PhabricatorRepositoryType::REPOSITORY_TYPE_GIT) {
return;
}
if (!$repository->isHosted()) {
return;
}
if (!$repository->isWorkingCopyBare()) {
return;
}
// TODO: This temporarily disables this action, because it doesn't work
// and is confusing to users. If you want to use it, comment out this line
// for now and we'll provide real support eventually.
return;
return $this->createActionView(
$revision,
pht('Land to Hosted Repository'));
}
}
diff --git a/src/applications/differential/landing/DifferentialHostedMercurialLandingStrategy.php b/src/applications/differential/landing/DifferentialHostedMercurialLandingStrategy.php
index 0303be62a..38f716095 100644
--- a/src/applications/differential/landing/DifferentialHostedMercurialLandingStrategy.php
+++ b/src/applications/differential/landing/DifferentialHostedMercurialLandingStrategy.php
@@ -1,104 +1,106 @@
<?php
final class DifferentialHostedMercurialLandingStrategy
extends DifferentialLandingStrategy {
public function processLandRequest(
AphrontRequest $request,
DifferentialRevision $revision,
PhabricatorRepository $repository) {
$viewer = $request->getUser();
$workspace = $this->getMercurialWorkspace($repository);
try {
$this->commitRevisionToWorkspace($revision, $workspace, $viewer);
} catch (Exception $e) {
- throw new PhutilProxyException('Failed to commit patch', $e);
+ throw new PhutilProxyException(pht('Failed to commit patch.'), $e);
}
try {
$this->pushWorkspaceRepository($repository, $workspace, $viewer);
} catch (Exception $e) {
- throw new PhutilProxyException('Failed to push changes upstream', $e);
+ throw new PhutilProxyException(
+ pht('Failed to push changes upstream.'),
+ $e);
}
}
public function commitRevisionToWorkspace(
DifferentialRevision $revision,
ArcanistRepositoryAPI $workspace,
PhabricatorUser $user) {
$diff_id = $revision->loadActiveDiff()->getID();
$call = new ConduitCall(
'differential.getrawdiff',
array(
'diffID' => $diff_id,
));
$call->setUser($user);
$raw_diff = $call->execute();
$future = $workspace->execFutureLocal('patch --no-commit -');
$future->write($raw_diff);
$future->resolvex();
$workspace->reloadWorkingCopy();
$call = new ConduitCall(
'differential.getcommitmessage',
array(
'revision_id' => $revision->getID(),
));
$call->setUser($user);
$message = $call->execute();
$author = id(new PhabricatorUser())->loadOneWhere(
'phid = %s',
$revision->getAuthorPHID());
$author_string = sprintf(
'%s <%s>',
$author->getRealName(),
$author->loadPrimaryEmailAddress());
$author_date = $revision->getDateCreated();
$workspace->execxLocal(
'commit --date=%s --user=%s '.
'--message=%s',
$author_date.' 0',
$author_string,
$message);
}
public function pushWorkspaceRepository(
PhabricatorRepository $repository,
ArcanistRepositoryAPI $workspace,
PhabricatorUser $user) {
$workspace->execxLocal('push -b default');
}
public function createMenuItem(
PhabricatorUser $viewer,
DifferentialRevision $revision,
PhabricatorRepository $repository) {
$vcs = $repository->getVersionControlSystem();
if ($vcs !== PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL) {
return;
}
if (!$repository->isHosted()) {
return;
}
return $this->createActionView(
$revision,
pht('Land to Hosted Repository'));
}
}
diff --git a/src/applications/differential/landing/DifferentialLandingStrategy.php b/src/applications/differential/landing/DifferentialLandingStrategy.php
index c62ca2ce2..d404511ed 100644
--- a/src/applications/differential/landing/DifferentialLandingStrategy.php
+++ b/src/applications/differential/landing/DifferentialLandingStrategy.php
@@ -1,83 +1,87 @@
<?php
abstract class DifferentialLandingStrategy {
public abstract function processLandRequest(
AphrontRequest $request,
DifferentialRevision $revision,
PhabricatorRepository $repository);
/**
* @return PhabricatorActionView or null.
*/
public abstract function createMenuItem(
PhabricatorUser $viewer,
DifferentialRevision $revision,
PhabricatorRepository $repository);
/**
* @return PhabricatorActionView which can be attached to the revision view.
*/
protected function createActionView($revision, $name) {
$strategy = get_class($this);
$revision_id = $revision->getId();
return id(new PhabricatorActionView())
->setRenderAsForm(true)
->setWorkflow(true)
->setName($name)
->setHref("/differential/revision/land/{$revision_id}/{$strategy}/");
}
/**
* Check if this action should be disabled, and explain why.
*
* By default, this method checks for push permissions, and for the
* revision being Accepted.
*
* @return False for "not disabled"; human-readable text explaining why, if
* it is disabled.
*/
public function isActionDisabled(
PhabricatorUser $viewer,
DifferentialRevision $revision,
PhabricatorRepository $repository) {
$status = $revision->getStatus();
if ($status != ArcanistDifferentialRevisionStatus::ACCEPTED) {
return pht('Only Accepted revisions can be landed.');
}
if (!PhabricatorPolicyFilter::hasCapability(
$viewer,
$repository,
DiffusionPushCapability::CAPABILITY)) {
return pht('You do not have permissions to push to this repository.');
}
return false;
}
/**
* Might break if repository is not Git.
*/
protected function getGitWorkspace(PhabricatorRepository $repository) {
try {
return DifferentialGetWorkingCopy::getCleanGitWorkspace($repository);
} catch (Exception $e) {
- throw new PhutilProxyException('Failed to allocate a workspace', $e);
+ throw new PhutilProxyException(
+ pht('Failed to allocate a workspace.'),
+ $e);
}
}
/**
* Might break if repository is not Mercurial.
*/
protected function getMercurialWorkspace(PhabricatorRepository $repository) {
try {
return DifferentialGetWorkingCopy::getCleanMercurialWorkspace(
$repository);
} catch (Exception $e) {
- throw new PhutilProxyException('Failed to allocate a workspace', $e);
+ throw new PhutilProxyException(
+ pht('Failed to allocate a workspace.'),
+ $e);
}
}
}
diff --git a/src/applications/differential/lipsum/PhabricatorDifferentialRevisionTestDataGenerator.php b/src/applications/differential/lipsum/PhabricatorDifferentialRevisionTestDataGenerator.php
index db34f1841..ec346f26a 100644
--- a/src/applications/differential/lipsum/PhabricatorDifferentialRevisionTestDataGenerator.php
+++ b/src/applications/differential/lipsum/PhabricatorDifferentialRevisionTestDataGenerator.php
@@ -1,104 +1,104 @@
<?php
final class PhabricatorDifferentialRevisionTestDataGenerator
extends PhabricatorTestDataGenerator {
public function generate() {
$author = $this->loadPhabrictorUser();
$revision = DifferentialRevision::initializeNewRevision($author);
$revision->attachReviewerStatus(array());
$revision->attachActiveDiff(null);
// This could be a bit richer and more formal than it is.
$revision->setTitle($this->generateTitle());
$revision->setSummary($this->generateDescription());
$revision->setTestPlan($this->generateDescription());
$diff = $this->generateDiff($author);
$xactions = array();
$xactions[] = id(new DifferentialTransaction())
->setTransactionType(DifferentialTransaction::TYPE_UPDATE)
->setNewValue($diff->getPHID());
$content_source = PhabricatorContentSource::newForSource(
PhabricatorContentSource::SOURCE_LIPSUM,
array());
id(new DifferentialTransactionEditor())
->setActor($author)
->setContentSource($content_source)
->applyTransactions($revision, $xactions);
return $revision;
}
public function getCCPHIDs() {
$ccs = array();
for ($i = 0; $i < rand(1, 4);$i++) {
$ccs[] = $this->loadPhabrictorUserPHID();
}
return $ccs;
}
public function generateDiff($author) {
$paste_generator = new PhabricatorPasteTestDataGenerator();
$languages = $paste_generator->supportedLanguages;
$lang = array_rand($languages);
$code = $paste_generator->generateContent($lang);
$altcode = $paste_generator->generateContent($lang);
$newcode = $this->randomlyModify($code, $altcode);
$diff = id(new PhabricatorDifferenceEngine())
->generateRawDiffFromFileContent($code, $newcode);
$call = new ConduitCall(
- 'differential.createrawdiff',
- array(
- 'diff' => $diff,
- ));
+ 'differential.createrawdiff',
+ array(
+ 'diff' => $diff,
+ ));
$call->setUser($author);
$result = $call->execute();
$thediff = id(new DifferentialDiff())->load(
$result['id']);
$thediff->setDescription($this->generateTitle())->save();
return $thediff;
}
public function generateDescription() {
return id(new PhutilLipsumContextFreeGrammar())
->generate(10, 20);
}
public function generateTitle() {
return id(new PhutilLipsumContextFreeGrammar())
->generate();
}
public function randomlyModify($code, $altcode) {
$codearr = explode("\n", $code);
$altcodearr = explode("\n", $altcode);
$no_lines_to_delete = rand(1,
min(count($codearr) - 2, 5));
$randomlines = array_rand($codearr,
count($codearr) - $no_lines_to_delete);
$newcode = array();
foreach ($randomlines as $lineno) {
$newcode[] = $codearr[$lineno];
}
$newlines_count = rand(2,
min(count($codearr) - 2, count($altcodearr) - 2, 5));
$randomlines_orig = array_rand($codearr, $newlines_count);
$randomlines_new = array_rand($altcodearr, $newlines_count);
$newcode2 = array();
$c = 0;
for ($i = 0; $i < count($newcode);$i++) {
$newcode2[] = $newcode[$i];
if (in_array($i, $randomlines_orig)) {
$newcode2[] = $altcodearr[$randomlines_new[$c++]];
}
}
return implode($newcode2, "\n");
}
}
diff --git a/src/applications/differential/mail/DifferentialCreateMailReceiver.php b/src/applications/differential/mail/DifferentialCreateMailReceiver.php
index d54faffc4..c3198d364 100644
--- a/src/applications/differential/mail/DifferentialCreateMailReceiver.php
+++ b/src/applications/differential/mail/DifferentialCreateMailReceiver.php
@@ -1,124 +1,124 @@
<?php
final class DifferentialCreateMailReceiver extends PhabricatorMailReceiver {
public function isEnabled() {
- $app_class = 'PhabricatorDifferentialApplication';
- return PhabricatorApplication::isClassInstalled($app_class);
+ return PhabricatorApplication::isClassInstalled(
+ 'PhabricatorDifferentialApplication');
}
public function canAcceptMail(PhabricatorMetaMTAReceivedMail $mail) {
$differential_app = new PhabricatorDifferentialApplication();
return $this->canAcceptApplicationMail($differential_app, $mail);
}
protected function processReceivedMail(
PhabricatorMetaMTAReceivedMail $mail,
PhabricatorUser $sender) {
$attachments = $mail->getAttachments();
$files = array();
$errors = array();
if ($attachments) {
$files = id(new PhabricatorFileQuery())
->setViewer($sender)
->withPHIDs($attachments)
->execute();
foreach ($files as $index => $file) {
if ($file->getMimeType() != 'text/plain') {
$errors[] = pht(
'Could not parse file %s; only files with mimetype text/plain '.
'can be parsed via email.',
$file->getName());
unset($files[$index]);
}
}
}
$diffs = array();
foreach ($files as $file) {
$call = new ConduitCall(
'differential.createrawdiff',
array(
'diff' => $file->loadFileData(),
));
$call->setUser($sender);
try {
$result = $call->execute();
$diffs[$file->getName()] = $result['uri'];
} catch (Exception $e) {
$errors[] = pht(
'Could not parse attachment %s; only attachments (and mail bodies) '.
'generated via "diff" commands can be parsed.',
$file->getName());
}
}
$body = $mail->getCleanTextBody();
if ($body) {
$call = new ConduitCall(
'differential.createrawdiff',
array(
'diff' => $body,
));
$call->setUser($sender);
try {
$result = $call->execute();
$diffs[pht('Mail Body')] = $result['uri'];
} catch (Exception $e) {
$errors[] = pht(
'Could not parse mail body; only mail bodies (and attachments) '.
'generated via "diff" commands can be parsed.');
}
}
$subject_prefix =
PhabricatorEnv::getEnvConfig('metamta.differential.subject-prefix');
if (count($diffs)) {
$subject = pht(
'You successfully created %d diff(s).',
count($diffs));
} else {
$subject = pht(
'Diff creation failed; see body for %s error(s).',
new PhutilNumber(count($errors)));
}
$body = new PhabricatorMetaMTAMailBody();
$body->addRawSection($subject);
if (count($diffs)) {
$text_body = '';
$html_body = array();
$body_label = pht('%s DIFF LINK(S)', new PhutilNumber(count($diffs)));
foreach ($diffs as $filename => $diff_uri) {
$text_body .= $filename.': '.$diff_uri."\n";
$html_body[] = phutil_tag(
'a',
array(
'href' => $diff_uri,
),
$filename);
$html_body[] = phutil_tag('br');
}
$body->addTextSection($body_label, $text_body);
$body->addHTMLSection($body_label, $html_body);
}
if (count($errors)) {
$body_section = new PhabricatorMetaMTAMailSection();
$body_label = pht('%s ERROR(S)', new PhutilNumber(count($errors)));
foreach ($errors as $error) {
$body_section->addFragment($error);
}
$body->addTextSection($body_label, $body_section);
}
id(new PhabricatorMetaMTAMail())
->addTos(array($sender->getPHID()))
->setSubject($subject)
->setSubjectPrefix($subject_prefix)
->setFrom($sender->getPHID())
->setBody($body->render())
->saveAndSend();
}
}
diff --git a/src/applications/differential/mail/DifferentialReplyHandler.php b/src/applications/differential/mail/DifferentialReplyHandler.php
index 660fe7e03..b2d8d0566 100644
--- a/src/applications/differential/mail/DifferentialReplyHandler.php
+++ b/src/applications/differential/mail/DifferentialReplyHandler.php
@@ -1,16 +1,16 @@
<?php
final class DifferentialReplyHandler
extends PhabricatorApplicationTransactionReplyHandler {
public function validateMailReceiver($mail_receiver) {
if (!($mail_receiver instanceof DifferentialRevision)) {
- throw new Exception('Receiver is not a DifferentialRevision!');
+ throw new Exception(pht('Receiver is not a %s!', 'DifferentialRevision'));
}
}
public function getObjectPrefix() {
return 'D';
}
}
diff --git a/src/applications/differential/mail/DifferentialRevisionMailReceiver.php b/src/applications/differential/mail/DifferentialRevisionMailReceiver.php
index b211a89ea..6b27c5a37 100644
--- a/src/applications/differential/mail/DifferentialRevisionMailReceiver.php
+++ b/src/applications/differential/mail/DifferentialRevisionMailReceiver.php
@@ -1,31 +1,31 @@
<?php
final class DifferentialRevisionMailReceiver
extends PhabricatorObjectMailReceiver {
public function isEnabled() {
- $app_class = 'PhabricatorDifferentialApplication';
- return PhabricatorApplication::isClassInstalled($app_class);
+ return PhabricatorApplication::isClassInstalled(
+ 'PhabricatorDifferentialApplication');
}
protected function getObjectPattern() {
return 'D[1-9]\d*';
}
protected function loadObject($pattern, PhabricatorUser $viewer) {
$id = (int)trim($pattern, 'D');
return id(new DifferentialRevisionQuery())
->setViewer($viewer)
->withIDs(array($id))
->needReviewerStatus(true)
->needReviewerAuthority(true)
->needActiveDiffs(true)
->executeOne();
}
protected function getTransactionReplyHandler() {
return new DifferentialReplyHandler();
}
}
diff --git a/src/applications/differential/parser/DifferentialChangesetParser.php b/src/applications/differential/parser/DifferentialChangesetParser.php
index 82c0a4751..bca4098ca 100644
--- a/src/applications/differential/parser/DifferentialChangesetParser.php
+++ b/src/applications/differential/parser/DifferentialChangesetParser.php
@@ -1,1569 +1,1570 @@
<?php
final class DifferentialChangesetParser {
const HIGHLIGHT_BYTE_LIMIT = 262144;
protected $visible = array();
protected $new = array();
protected $old = array();
protected $intra = array();
protected $newRender = null;
protected $oldRender = null;
protected $filename = null;
protected $hunkStartLines = array();
protected $comments = array();
protected $specialAttributes = array();
protected $changeset;
protected $whitespaceMode = null;
protected $renderCacheKey = null;
private $handles = array();
private $user;
private $leftSideChangesetID;
private $leftSideAttachesToNewFile;
private $rightSideChangesetID;
private $rightSideAttachesToNewFile;
private $originalLeft;
private $originalRight;
private $renderingReference;
private $isSubparser;
private $isTopLevel;
private $coverage;
private $markupEngine;
private $highlightErrors;
private $disableCache;
private $renderer;
private $characterEncoding;
private $highlightAs;
private $highlightingDisabled;
private $showEditAndReplyLinks = true;
private $canMarkDone;
private $objectOwnerPHID;
private $rangeStart;
private $rangeEnd;
private $mask;
public function setRange($start, $end) {
$this->rangeStart = $start;
$this->rangeEnd = $end;
return $this;
}
public function setMask(array $mask) {
$this->mask = $mask;
return $this;
}
public function renderChangeset() {
return $this->render($this->rangeStart, $this->rangeEnd, $this->mask);
}
public function setShowEditAndReplyLinks($bool) {
$this->showEditAndReplyLinks = $bool;
return $this;
}
public function getShowEditAndReplyLinks() {
return $this->showEditAndReplyLinks;
}
public function setHighlightAs($highlight_as) {
$this->highlightAs = $highlight_as;
return $this;
}
public function getHighlightAs() {
return $this->highlightAs;
}
public function setCharacterEncoding($character_encoding) {
$this->characterEncoding = $character_encoding;
return $this;
}
public function getCharacterEncoding() {
return $this->characterEncoding;
}
public function setRenderer(DifferentialChangesetRenderer $renderer) {
$this->renderer = $renderer;
return $this;
}
public function getRenderer() {
if (!$this->renderer) {
return new DifferentialChangesetTwoUpRenderer();
}
return $this->renderer;
}
public function setDisableCache($disable_cache) {
$this->disableCache = $disable_cache;
return $this;
}
public function getDisableCache() {
return $this->disableCache;
}
public function setCanMarkDone($can_mark_done) {
$this->canMarkDone = $can_mark_done;
return $this;
}
public function getCanMarkDone() {
return $this->canMarkDone;
}
public function setObjectOwnerPHID($phid) {
$this->objectOwnerPHID = $phid;
return $this;
}
public function getObjectOwnerPHID() {
return $this->objectOwnerPHID;
}
public static function getDefaultRendererForViewer(PhabricatorUser $viewer) {
$prefs = $viewer->loadPreferences();
$pref_unified = PhabricatorUserPreferences::PREFERENCE_DIFF_UNIFIED;
if ($prefs->getPreference($pref_unified) == 'unified') {
return '1up';
}
return null;
}
public function readParametersFromRequest(AphrontRequest $request) {
$this->setWhitespaceMode($request->getStr('whitespace'));
$this->setCharacterEncoding($request->getStr('encoding'));
$this->setHighlightAs($request->getStr('highlight'));
$renderer = null;
// If the viewer prefers unified diffs, always set the renderer to unified.
// Otherwise, we leave it unspecified and the client will choose a
// renderer based on the screen size.
if ($request->getStr('renderer')) {
$renderer = $request->getStr('renderer');
} else {
$renderer = self::getDefaultRendererForViewer($request->getViewer());
}
switch ($renderer) {
case '1up':
$this->setRenderer(new DifferentialChangesetOneUpRenderer());
break;
default:
$this->setRenderer(new DifferentialChangesetTwoUpRenderer());
break;
}
return $this;
}
const CACHE_VERSION = 11;
const CACHE_MAX_SIZE = 8e6;
const ATTR_GENERATED = 'attr:generated';
const ATTR_DELETED = 'attr:deleted';
const ATTR_UNCHANGED = 'attr:unchanged';
const ATTR_WHITELINES = 'attr:white';
const ATTR_MOVEAWAY = 'attr:moveaway';
const LINES_CONTEXT = 8;
const WHITESPACE_SHOW_ALL = 'show-all';
const WHITESPACE_IGNORE_TRAILING = 'ignore-trailing';
const WHITESPACE_IGNORE_MOST = 'ignore-most';
const WHITESPACE_IGNORE_ALL = 'ignore-all';
public function setOldLines(array $lines) {
$this->old = $lines;
return $this;
}
public function setNewLines(array $lines) {
$this->new = $lines;
return $this;
}
public function setSpecialAttributes(array $attributes) {
$this->specialAttributes = $attributes;
return $this;
}
public function setIntraLineDiffs(array $diffs) {
$this->intra = $diffs;
return $this;
}
public function setVisibileLinesMask(array $mask) {
$this->visible = $mask;
return $this;
}
/**
* Configure which Changeset comments added to the right side of the visible
* diff will be attached to. The ID must be the ID of a real Differential
* Changeset.
*
* The complexity here is that we may show an arbitrary side of an arbitrary
* changeset as either the left or right part of a diff. This method allows
* the left and right halves of the displayed diff to be correctly mapped to
* storage changesets.
*
* @param id The Differential Changeset ID that comments added to the right
* side of the visible diff should be attached to.
* @param bool If true, attach new comments to the right side of the storage
* changeset. Note that this may be false, if the left side of
* some storage changeset is being shown as the right side of
* a display diff.
* @return this
*/
public function setRightSideCommentMapping($id, $is_new) {
$this->rightSideChangesetID = $id;
$this->rightSideAttachesToNewFile = $is_new;
return $this;
}
/**
* See setRightSideCommentMapping(), but this sets information for the left
* side of the display diff.
*/
public function setLeftSideCommentMapping($id, $is_new) {
$this->leftSideChangesetID = $id;
$this->leftSideAttachesToNewFile = $is_new;
return $this;
}
public function setOriginals(
DifferentialChangeset $left,
DifferentialChangeset $right) {
$this->originalLeft = $left;
$this->originalRight = $right;
}
public function diffOriginals() {
$engine = new PhabricatorDifferenceEngine();
$changeset = $engine->generateChangesetFromFileContent(
implode('', mpull($this->originalLeft->getHunks(), 'getChanges')),
implode('', mpull($this->originalRight->getHunks(), 'getChanges')));
$parser = new DifferentialHunkParser();
return $parser->parseHunksForHighlightMasks(
$changeset->getHunks(),
$this->originalLeft->getHunks(),
$this->originalRight->getHunks());
}
/**
* Set a key for identifying this changeset in the render cache. If set, the
* parser will attempt to use the changeset render cache, which can improve
* performance for frequently-viewed changesets.
*
* By default, there is no render cache key and parsers do not use the cache.
* This is appropriate for rarely-viewed changesets.
*
* NOTE: Currently, this key must be a valid Differential Changeset ID.
*
* @param string Key for identifying this changeset in the render cache.
* @return this
*/
public function setRenderCacheKey($key) {
$this->renderCacheKey = $key;
return $this;
}
private function getRenderCacheKey() {
return $this->renderCacheKey;
}
public function setChangeset(DifferentialChangeset $changeset) {
$this->changeset = $changeset;
$this->setFilename($changeset->getFilename());
return $this;
}
public function setWhitespaceMode($whitespace_mode) {
$this->whitespaceMode = $whitespace_mode;
return $this;
}
public function setRenderingReference($ref) {
$this->renderingReference = $ref;
return $this;
}
private function getRenderingReference() {
return $this->renderingReference;
}
public function getChangeset() {
return $this->changeset;
}
public function setFilename($filename) {
$this->filename = $filename;
return $this;
}
public function setHandles(array $handles) {
assert_instances_of($handles, 'PhabricatorObjectHandle');
$this->handles = $handles;
return $this;
}
public function setMarkupEngine(PhabricatorMarkupEngine $engine) {
$this->markupEngine = $engine;
return $this;
}
public function setUser(PhabricatorUser $user) {
$this->user = $user;
return $this;
}
public function getUser() {
return $this->user;
}
public function setCoverage($coverage) {
$this->coverage = $coverage;
return $this;
}
private function getCoverage() {
return $this->coverage;
}
public function parseInlineComment(
PhabricatorInlineCommentInterface $comment) {
// Parse only comments which are actually visible.
if ($this->isCommentVisibleOnRenderedDiff($comment)) {
$this->comments[] = $comment;
}
return $this;
}
private function loadCache() {
$render_cache_key = $this->getRenderCacheKey();
if (!$render_cache_key) {
return false;
}
$data = null;
$changeset = new DifferentialChangeset();
$conn_r = $changeset->establishConnection('r');
$data = queryfx_one(
$conn_r,
'SELECT * FROM %T WHERE id = %d',
$changeset->getTableName().'_parse_cache',
$render_cache_key);
if (!$data) {
return false;
}
if ($data['cache'][0] == '{') {
// This is likely an old-style JSON cache which we will not be able to
// deserialize.
return false;
}
$data = unserialize($data['cache']);
if (!is_array($data) || !$data) {
return false;
}
foreach (self::getCacheableProperties() as $cache_key) {
if (!array_key_exists($cache_key, $data)) {
// If we're missing a cache key, assume we're looking at an old cache
// and ignore it.
return false;
}
}
if ($data['cacheVersion'] !== self::CACHE_VERSION) {
return false;
}
// Someone displays contents of a partially cached shielded file.
if (!isset($data['newRender']) && (!$this->isTopLevel || $this->comments)) {
return false;
}
unset($data['cacheVersion'], $data['cacheHost']);
$cache_prop = array_select_keys($data, self::getCacheableProperties());
foreach ($cache_prop as $cache_key => $v) {
$this->$cache_key = $v;
}
return true;
}
protected static function getCacheableProperties() {
return array(
'visible',
'new',
'old',
'intra',
'newRender',
'oldRender',
'specialAttributes',
'hunkStartLines',
'cacheVersion',
'cacheHost',
'highlightingDisabled',
);
}
public function saveCache() {
if ($this->highlightErrors) {
return false;
}
$render_cache_key = $this->getRenderCacheKey();
if (!$render_cache_key) {
return false;
}
$cache = array();
foreach (self::getCacheableProperties() as $cache_key) {
switch ($cache_key) {
case 'cacheVersion':
$cache[$cache_key] = self::CACHE_VERSION;
break;
case 'cacheHost':
$cache[$cache_key] = php_uname('n');
break;
default:
$cache[$cache_key] = $this->$cache_key;
break;
}
}
$cache = serialize($cache);
// We don't want to waste too much space by a single changeset.
if (strlen($cache) > self::CACHE_MAX_SIZE) {
return;
}
$changeset = new DifferentialChangeset();
$conn_w = $changeset->establishConnection('w');
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
try {
queryfx(
$conn_w,
'INSERT INTO %T (id, cache, dateCreated) VALUES (%d, %B, %d)
ON DUPLICATE KEY UPDATE cache = VALUES(cache)',
DifferentialChangeset::TABLE_CACHE,
$render_cache_key,
$cache,
time());
} catch (AphrontQueryException $ex) {
// Ignore these exceptions. A common cause is that the cache is
// larger than 'max_allowed_packet', in which case we're better off
// not writing it.
// TODO: It would be nice to tailor this more narrowly.
}
unset($unguarded);
}
private function markGenerated($new_corpus_block = '') {
$generated_guess = (strpos($new_corpus_block, '@'.'generated') !== false);
if (!$generated_guess) {
$generated_path_regexps = PhabricatorEnv::getEnvConfig(
'differential.generated-paths');
foreach ($generated_path_regexps as $regexp) {
if (preg_match($regexp, $this->changeset->getFilename())) {
$generated_guess = true;
break;
}
}
}
$event = new PhabricatorEvent(
PhabricatorEventType::TYPE_DIFFERENTIAL_WILLMARKGENERATED,
array(
'corpus' => $new_corpus_block,
'is_generated' => $generated_guess,
)
);
PhutilEventEngine::dispatchEvent($event);
$generated = $event->getValue('is_generated');
$this->specialAttributes[self::ATTR_GENERATED] = $generated;
}
public function isGenerated() {
return idx($this->specialAttributes, self::ATTR_GENERATED, false);
}
public function isDeleted() {
return idx($this->specialAttributes, self::ATTR_DELETED, false);
}
public function isUnchanged() {
return idx($this->specialAttributes, self::ATTR_UNCHANGED, false);
}
public function isWhitespaceOnly() {
return idx($this->specialAttributes, self::ATTR_WHITELINES, false);
}
public function isMoveAway() {
return idx($this->specialAttributes, self::ATTR_MOVEAWAY, false);
}
private function applyIntraline(&$render, $intra, $corpus) {
foreach ($render as $key => $text) {
if (isset($intra[$key])) {
$render[$key] = ArcanistDiffUtils::applyIntralineDiff(
$text,
$intra[$key]);
}
}
}
private function getHighlightFuture($corpus) {
$language = $this->highlightAs;
if (!$language) {
$language = $this->highlightEngine->getLanguageFromFilename(
$this->filename);
if (($language != 'txt') &&
(strlen($corpus) > self::HIGHLIGHT_BYTE_LIMIT)) {
$this->highlightingDisabled = true;
$language = 'txt';
}
}
return $this->highlightEngine->getHighlightFuture(
$language,
$corpus);
}
protected function processHighlightedSource($data, $result) {
$result_lines = phutil_split_lines($result);
foreach ($data as $key => $info) {
if (!$info) {
unset($result_lines[$key]);
}
}
return $result_lines;
}
private function tryCacheStuff() {
$whitespace_mode = $this->whitespaceMode;
switch ($whitespace_mode) {
case self::WHITESPACE_SHOW_ALL:
case self::WHITESPACE_IGNORE_TRAILING:
case self::WHITESPACE_IGNORE_ALL:
break;
default:
$whitespace_mode = self::WHITESPACE_IGNORE_MOST;
break;
}
$skip_cache = ($whitespace_mode != self::WHITESPACE_IGNORE_MOST);
if ($this->disableCache) {
$skip_cache = true;
}
if ($this->characterEncoding) {
$skip_cache = true;
}
if ($this->highlightAs) {
$skip_cache = true;
}
$this->whitespaceMode = $whitespace_mode;
$changeset = $this->changeset;
if ($changeset->getFileType() != DifferentialChangeType::FILE_TEXT &&
$changeset->getFileType() != DifferentialChangeType::FILE_SYMLINK) {
$this->markGenerated();
} else {
if ($skip_cache || !$this->loadCache()) {
$this->process();
if (!$skip_cache) {
$this->saveCache();
}
}
}
}
private function process() {
$whitespace_mode = $this->whitespaceMode;
$changeset = $this->changeset;
$ignore_all = (($whitespace_mode == self::WHITESPACE_IGNORE_MOST) ||
($whitespace_mode == self::WHITESPACE_IGNORE_ALL));
$force_ignore = ($whitespace_mode == self::WHITESPACE_IGNORE_ALL);
if (!$force_ignore) {
if ($ignore_all && $changeset->getWhitespaceMatters()) {
$ignore_all = false;
}
}
// The "ignore all whitespace" algorithm depends on rediffing the
// files, and we currently need complete representations of both
// files to do anything reasonable. If we only have parts of the files,
// don't use the "ignore all" algorithm.
if ($ignore_all) {
$hunks = $changeset->getHunks();
if (count($hunks) !== 1) {
$ignore_all = false;
} else {
$first_hunk = reset($hunks);
if ($first_hunk->getOldOffset() != 1 ||
$first_hunk->getNewOffset() != 1) {
$ignore_all = false;
}
}
}
if ($ignore_all) {
$old_file = $changeset->makeOldFile();
$new_file = $changeset->makeNewFile();
if ($old_file == $new_file) {
// If the old and new files are exactly identical, the synthetic
// diff below will give us nonsense and whitespace modes are
// irrelevant anyway. This occurs when you, e.g., copy a file onto
// itself in Subversion (see T271).
$ignore_all = false;
}
}
$hunk_parser = new DifferentialHunkParser();
$hunk_parser->setWhitespaceMode($whitespace_mode);
$hunk_parser->parseHunksForLineData($changeset->getHunks());
// Depending on the whitespace mode, we may need to compute a different
// set of changes than the set of changes in the hunk data (specificaly,
// we might want to consider changed lines which have only whitespace
// changes as unchanged).
if ($ignore_all) {
$engine = new PhabricatorDifferenceEngine();
$engine->setIgnoreWhitespace(true);
$no_whitespace_changeset = $engine->generateChangesetFromFileContent(
$old_file,
$new_file);
$type_parser = new DifferentialHunkParser();
$type_parser->parseHunksForLineData($no_whitespace_changeset->getHunks());
$hunk_parser->setOldLineTypeMap($type_parser->getOldLineTypeMap());
$hunk_parser->setNewLineTypeMap($type_parser->getNewLineTypeMap());
}
$hunk_parser->reparseHunksForSpecialAttributes();
$unchanged = false;
if (!$hunk_parser->getHasAnyChanges()) {
$filetype = $this->changeset->getFileType();
if ($filetype == DifferentialChangeType::FILE_TEXT ||
$filetype == DifferentialChangeType::FILE_SYMLINK) {
$unchanged = true;
}
}
$moveaway = false;
$changetype = $this->changeset->getChangeType();
if ($changetype == DifferentialChangeType::TYPE_MOVE_AWAY) {
$moveaway = true;
}
$this->setSpecialAttributes(array(
self::ATTR_UNCHANGED => $unchanged,
self::ATTR_DELETED => $hunk_parser->getIsDeleted(),
self::ATTR_WHITELINES => !$hunk_parser->getHasTextChanges(),
self::ATTR_MOVEAWAY => $moveaway,
));
$hunk_parser->generateIntraLineDiffs();
$hunk_parser->generateVisibileLinesMask();
$this->setOldLines($hunk_parser->getOldLines());
$this->setNewLines($hunk_parser->getNewLines());
$this->setIntraLineDiffs($hunk_parser->getIntraLineDiffs());
$this->setVisibileLinesMask($hunk_parser->getVisibleLinesMask());
$this->hunkStartLines = $hunk_parser->getHunkStartLines(
$changeset->getHunks());
$new_corpus = $hunk_parser->getNewCorpus();
$new_corpus_block = implode('', $new_corpus);
$this->markGenerated($new_corpus_block);
if ($this->isTopLevel &&
!$this->comments &&
($this->isGenerated() ||
$this->isUnchanged() ||
$this->isDeleted())) {
return;
}
$old_corpus = $hunk_parser->getOldCorpus();
$old_corpus_block = implode('', $old_corpus);
$old_future = $this->getHighlightFuture($old_corpus_block);
$new_future = $this->getHighlightFuture($new_corpus_block);
$futures = array(
'old' => $old_future,
'new' => $new_future,
);
$corpus_blocks = array(
'old' => $old_corpus_block,
'new' => $new_corpus_block,
);
$this->highlightErrors = false;
foreach (new FutureIterator($futures) as $key => $future) {
try {
try {
$highlighted = $future->resolve();
} catch (PhutilSyntaxHighlighterException $ex) {
$this->highlightErrors = true;
$highlighted = id(new PhutilDefaultSyntaxHighlighter())
->getHighlightFuture($corpus_blocks[$key])
->resolve();
}
switch ($key) {
case 'old':
$this->oldRender = $this->processHighlightedSource(
$this->old,
$highlighted);
break;
case 'new':
$this->newRender = $this->processHighlightedSource(
$this->new,
$highlighted);
break;
}
} catch (Exception $ex) {
phlog($ex);
throw $ex;
}
}
$this->applyIntraline(
$this->oldRender,
ipull($this->intra, 0),
$old_corpus);
$this->applyIntraline(
$this->newRender,
ipull($this->intra, 1),
$new_corpus);
}
private function shouldRenderPropertyChangeHeader($changeset) {
if (!$this->isTopLevel) {
// We render properties only at top level; otherwise we get multiple
// copies of them when a user clicks "Show More".
return false;
}
return true;
}
public function render(
$range_start = null,
$range_len = null,
$mask_force = array()) {
// "Top level" renders are initial requests for the whole file, versus
// requests for a specific range generated by clicking "show more". We
// generate property changes and "shield" UI elements only for toplevel
// requests.
$this->isTopLevel = (($range_start === null) && ($range_len === null));
$this->highlightEngine = PhabricatorSyntaxHighlighter::newEngine();
$encoding = null;
if ($this->characterEncoding) {
// We are forcing this changeset to be interpreted with a specific
// character encoding, so force all the hunks into that encoding and
// propagate it to the renderer.
$encoding = $this->characterEncoding;
foreach ($this->changeset->getHunks() as $hunk) {
$hunk->forceEncoding($this->characterEncoding);
}
} else {
// We're just using the default, so tell the renderer what that is
// (by reading the encoding from the first hunk).
foreach ($this->changeset->getHunks() as $hunk) {
$encoding = $hunk->getDataEncoding();
break;
}
}
$this->tryCacheStuff();
$render_pch = $this->shouldRenderPropertyChangeHeader($this->changeset);
$rows = max(
count($this->old),
count($this->new));
$renderer = $this->getRenderer()
->setUser($this->getUser())
->setChangeset($this->changeset)
->setRenderPropertyChangeHeader($render_pch)
->setIsTopLevel($this->isTopLevel)
->setOldRender($this->oldRender)
->setNewRender($this->newRender)
->setHunkStartLines($this->hunkStartLines)
->setOldChangesetID($this->leftSideChangesetID)
->setNewChangesetID($this->rightSideChangesetID)
->setOldAttachesToNewFile($this->leftSideAttachesToNewFile)
->setNewAttachesToNewFile($this->rightSideAttachesToNewFile)
->setCodeCoverage($this->getCoverage())
->setRenderingReference($this->getRenderingReference())
->setMarkupEngine($this->markupEngine)
->setHandles($this->handles)
->setOldLines($this->old)
->setNewLines($this->new)
->setOriginalCharacterEncoding($encoding)
->setShowEditAndReplyLinks($this->getShowEditAndReplyLinks())
->setCanMarkDone($this->getCanMarkDone())
->setObjectOwnerPHID($this->getObjectOwnerPHID())
->setHighlightingDisabled($this->highlightingDisabled);
$shield = null;
if ($this->isTopLevel && !$this->comments) {
if ($this->isGenerated()) {
$shield = $renderer->renderShield(
pht(
'This file contains generated code, which does not normally '.
'need to be reviewed.'));
} else if ($this->isMoveAway()) {
// We put an empty shield on these files. Normally, they do not have
// any diff content anyway. However, if they come through `arc`, they
// may have content. We don't want to show it (it's not useful) and
// we bailed out of fully processing it earlier anyway.
// We could show a message like "this file was moved", but we show
// that as a change header anyway, so it would be redundant. Instead,
// just render an empty shield to skip rendering the diff body.
$shield = '';
} else if ($this->isUnchanged()) {
$type = 'text';
if (!$rows) {
// NOTE: Normally, diffs which don't change files do not include
// file content (for example, if you "chmod +x" a file and then
// run "git show", the file content is not available). Similarly,
// if you move a file from A to B without changing it, diffs normally
// do not show the file content. In some cases `arc` is able to
// synthetically generate content for these diffs, but for raw diffs
// we'll never have it so we need to be prepared to not render a link.
$type = 'none';
}
$type_add = DifferentialChangeType::TYPE_ADD;
if ($this->changeset->getChangeType() == $type_add) {
// Although the generic message is sort of accurate in a technical
// sense, this more-tailored message is less confusing.
$shield = $renderer->renderShield(
pht('This is an empty file.'),
$type);
} else {
$shield = $renderer->renderShield(
pht('The contents of this file were not changed.'),
$type);
}
} else if ($this->isWhitespaceOnly()) {
$shield = $renderer->renderShield(
pht('This file was changed only by adding or removing whitespace.'),
'whitespace');
} else if ($this->isDeleted()) {
$shield = $renderer->renderShield(
pht('This file was completely deleted.'));
} else if ($this->changeset->getAffectedLineCount() > 2500) {
$lines = number_format($this->changeset->getAffectedLineCount());
$shield = $renderer->renderShield(
pht(
'This file has a very large number of changes (%s lines).',
$lines));
}
}
if ($shield !== null) {
return $renderer->renderChangesetTable($shield);
}
// This request should render the "undershield" headers if it's a top-level
// request which made it this far (indicating the changeset has no shield)
// or it's a request with no mask information (indicating it's the request
// that removes the rendering shield). Possibly, this second class of
// request might need to be made more explicit.
$is_undershield = (empty($mask_force) || $this->isTopLevel);
$renderer->setIsUndershield($is_undershield);
$old_comments = array();
$new_comments = array();
$old_mask = array();
$new_mask = array();
$feedback_mask = array();
if ($this->comments) {
// If there are any comments which appear in sections of the file which
// we don't have, we're going to move them backwards to the closest
// earlier line. Two cases where this may happen are:
//
// - Porting ghost comments forward into a file which was mostly
// deleted.
// - Porting ghost comments forward from a full-context diff to a
// partial-context diff.
list($old_backmap, $new_backmap) = $this->buildLineBackmaps();
foreach ($this->comments as $comment) {
$new_side = $this->isCommentOnRightSideWhenDisplayed($comment);
$line = $comment->getLineNumber();
if ($new_side) {
$back_line = $new_backmap[$line];
} else {
$back_line = $old_backmap[$line];
}
if ($back_line != $line) {
// TODO: This should probably be cleaner, but just be simple and
// obvious for now.
$ghost = $comment->getIsGhost();
if ($ghost) {
$moved = pht(
'This comment originally appeared on line %s, but that line '.
'does not exist in this version of the diff. It has been '.
'moved backward to the nearest line.',
new PhutilNumber($line));
$ghost['reason'] = $ghost['reason']."\n\n".$moved;
$comment->setIsGhost($ghost);
}
$comment->setLineNumber($back_line);
$comment->setLineLength(0);
}
$start = max($comment->getLineNumber() - self::LINES_CONTEXT, 0);
$end = $comment->getLineNumber() +
$comment->getLineLength() +
self::LINES_CONTEXT;
for ($ii = $start; $ii <= $end; $ii++) {
if ($new_side) {
$new_mask[$ii] = true;
} else {
$old_mask[$ii] = true;
}
}
}
foreach ($this->old as $ii => $old) {
if (isset($old['line']) && isset($old_mask[$old['line']])) {
$feedback_mask[$ii] = true;
}
}
foreach ($this->new as $ii => $new) {
if (isset($new['line']) && isset($new_mask[$new['line']])) {
$feedback_mask[$ii] = true;
}
}
$this->comments = msort($this->comments, 'getID');
foreach ($this->comments as $comment) {
$final = $comment->getLineNumber() +
$comment->getLineLength();
$final = max(1, $final);
if ($this->isCommentOnRightSideWhenDisplayed($comment)) {
$new_comments[$final][] = $comment;
} else {
$old_comments[$final][] = $comment;
}
}
}
$renderer
->setOldComments($old_comments)
->setNewComments($new_comments);
switch ($this->changeset->getFileType()) {
case DifferentialChangeType::FILE_IMAGE:
$old = null;
$new = null;
// TODO: Improve the architectural issue as discussed in D955
// https://secure.phabricator.com/D955
$reference = $this->getRenderingReference();
$parts = explode('/', $reference);
if (count($parts) == 2) {
list($id, $vs) = $parts;
} else {
$id = $parts[0];
$vs = 0;
}
$id = (int)$id;
$vs = (int)$vs;
if (!$vs) {
$metadata = $this->changeset->getMetadata();
$data = idx($metadata, 'attachment-data');
$old_phid = idx($metadata, 'old:binary-phid');
$new_phid = idx($metadata, 'new:binary-phid');
} else {
$vs_changeset = id(new DifferentialChangeset())->load($vs);
$old_phid = null;
$new_phid = null;
// TODO: This is spooky, see D6851
if ($vs_changeset) {
$vs_metadata = $vs_changeset->getMetadata();
$old_phid = idx($vs_metadata, 'new:binary-phid');
}
$changeset = id(new DifferentialChangeset())->load($id);
if ($changeset) {
$metadata = $changeset->getMetadata();
$new_phid = idx($metadata, 'new:binary-phid');
}
}
if ($old_phid || $new_phid) {
// grab the files, (micro) optimization for 1 query not 2
$file_phids = array();
if ($old_phid) {
$file_phids[] = $old_phid;
}
if ($new_phid) {
$file_phids[] = $new_phid;
}
$files = id(new PhabricatorFileQuery())
->setViewer($this->getUser())
->withPHIDs($file_phids)
->execute();
foreach ($files as $file) {
if (empty($file)) {
continue;
}
if ($file->getPHID() == $old_phid) {
$old = $file;
} else if ($file->getPHID() == $new_phid) {
$new = $file;
}
}
}
$renderer->attachOldFile($old);
$renderer->attachNewFile($new);
return $renderer->renderFileChange($old, $new, $id, $vs);
case DifferentialChangeType::FILE_DIRECTORY:
case DifferentialChangeType::FILE_BINARY:
$output = $renderer->renderChangesetTable(null);
return $output;
}
if ($this->originalLeft && $this->originalRight) {
list($highlight_old, $highlight_new) = $this->diffOriginals();
$highlight_old = array_flip($highlight_old);
$highlight_new = array_flip($highlight_new);
$renderer
->setHighlightOld($highlight_old)
->setHighlightNew($highlight_new);
}
$renderer
->setOriginalOld($this->originalLeft)
->setOriginalNew($this->originalRight);
if ($range_start === null) {
$range_start = 0;
}
if ($range_len === null) {
$range_len = $rows;
}
$range_len = min($range_len, $rows - $range_start);
list($gaps, $mask, $depths) = $this->calculateGapsMaskAndDepths(
$mask_force,
$feedback_mask,
$range_start,
$range_len);
$renderer
->setGaps($gaps)
->setMask($mask)
->setDepths($depths);
$html = $renderer->renderTextChange(
$range_start,
$range_len,
$rows);
return $renderer->renderChangesetTable($html);
}
/**
* This function calculates a lot of stuff we need to know to display
* the diff:
*
* Gaps - compute gaps in the visible display diff, where we will render
* "Show more context" spacers. If a gap is smaller than the context size,
* we just display it. Otherwise, we record it into $gaps and will render a
* "show more context" element instead of diff text below. A given $gap
* is a tuple of $gap_line_number_start and $gap_length.
*
* Mask - compute the actual lines that need to be shown (because they
* are near changes lines, near inline comments, or the request has
* explicitly asked for them, i.e. resulting from the user clicking
* "show more"). The $mask returned is a sparesely populated dictionary
* of $visible_line_number => true.
*
* Depths - compute how indented any given line is. The $depths returned
* is a sparesely populated dictionary of $visible_line_number => $depth.
*
* This function also has the side effect of modifying member variable
* new such that tabs are normalized to spaces for each line of the diff.
*
* @return array($gaps, $mask, $depths)
*/
- private function calculateGapsMaskAndDepths($mask_force,
- $feedback_mask,
- $range_start,
- $range_len) {
+ private function calculateGapsMaskAndDepths(
+ $mask_force,
+ $feedback_mask,
+ $range_start,
+ $range_len) {
// Calculate gaps and mask first
$gaps = array();
$gap_start = 0;
$in_gap = false;
$base_mask = $this->visible + $mask_force + $feedback_mask;
$base_mask[$range_start + $range_len] = true;
for ($ii = $range_start; $ii <= $range_start + $range_len; $ii++) {
if (isset($base_mask[$ii])) {
if ($in_gap) {
$gap_length = $ii - $gap_start;
if ($gap_length <= self::LINES_CONTEXT) {
for ($jj = $gap_start; $jj <= $gap_start + $gap_length; $jj++) {
$base_mask[$jj] = true;
}
} else {
$gaps[] = array($gap_start, $gap_length);
}
$in_gap = false;
}
} else {
if (!$in_gap) {
$gap_start = $ii;
$in_gap = true;
}
}
}
$gaps = array_reverse($gaps);
$mask = $base_mask;
// Time to calculate depth.
// We need to go backwards to properly indent whitespace in this code:
//
// 0: class C {
// 1:
// 1: function f() {
// 2:
// 2: return;
// 1:
// 1: }
// 0:
// 0: }
//
$depths = array();
$last_depth = 0;
$range_end = $range_start + $range_len;
if (!isset($this->new[$range_end])) {
$range_end--;
}
for ($ii = $range_end; $ii >= $range_start; $ii--) {
// We need to expand tabs to process mixed indenting and to round
// correctly later.
$line = str_replace("\t", ' ', $this->new[$ii]['text']);
$trimmed = ltrim($line);
if ($trimmed != '') {
// We round down to flatten "/**" and " *".
$last_depth = floor((strlen($line) - strlen($trimmed)) / 2);
}
$depths[$ii] = $last_depth;
}
return array($gaps, $mask, $depths);
}
/**
* Determine if an inline comment will appear on the rendered diff,
* taking into consideration which halves of which changesets will actually
* be shown.
*
* @param PhabricatorInlineCommentInterface Comment to test for visibility.
* @return bool True if the comment is visible on the rendered diff.
*/
private function isCommentVisibleOnRenderedDiff(
PhabricatorInlineCommentInterface $comment) {
$changeset_id = $comment->getChangesetID();
$is_new = $comment->getIsNewFile();
if ($changeset_id == $this->rightSideChangesetID &&
$is_new == $this->rightSideAttachesToNewFile) {
return true;
}
if ($changeset_id == $this->leftSideChangesetID &&
$is_new == $this->leftSideAttachesToNewFile) {
return true;
}
return false;
}
/**
* Determine if a comment will appear on the right side of the display diff.
* Note that the comment must appear somewhere on the rendered changeset, as
* per isCommentVisibleOnRenderedDiff().
*
* @param PhabricatorInlineCommentInterface Comment to test for display
* location.
* @return bool True for right, false for left.
*/
private function isCommentOnRightSideWhenDisplayed(
PhabricatorInlineCommentInterface $comment) {
if (!$this->isCommentVisibleOnRenderedDiff($comment)) {
- throw new Exception('Comment is not visible on changeset!');
+ throw new Exception(pht('Comment is not visible on changeset!'));
}
$changeset_id = $comment->getChangesetID();
$is_new = $comment->getIsNewFile();
if ($changeset_id == $this->rightSideChangesetID &&
$is_new == $this->rightSideAttachesToNewFile) {
return true;
}
return false;
}
/**
* Parse the 'range' specification that this class and the client-side JS
* emit to indicate that a user clicked "Show more..." on a diff. Generally,
* use is something like this:
*
* $spec = $request->getStr('range');
* $parsed = DifferentialChangesetParser::parseRangeSpecification($spec);
* list($start, $end, $mask) = $parsed;
* $parser->render($start, $end, $mask);
*
* @param string Range specification, indicating the range of the diff that
* should be rendered.
* @return tuple List of <start, end, mask> suitable for passing to
* @{method:render}.
*/
public static function parseRangeSpecification($spec) {
$range_s = null;
$range_e = null;
$mask = array();
if ($spec) {
$match = null;
if (preg_match('@^(\d+)-(\d+)(?:/(\d+)-(\d+))?$@', $spec, $match)) {
$range_s = (int)$match[1];
$range_e = (int)$match[2];
if (count($match) > 3) {
$start = (int)$match[3];
$len = (int)$match[4];
for ($ii = $start; $ii < $start + $len; $ii++) {
$mask[$ii] = true;
}
}
}
}
return array($range_s, $range_e, $mask);
}
/**
* Render "modified coverage" information; test coverage on modified lines.
* This synthesizes diff information with unit test information into a useful
* indicator of how well tested a change is.
*/
public function renderModifiedCoverage() {
$na = phutil_tag('em', array(), '-');
$coverage = $this->getCoverage();
if (!$coverage) {
return $na;
}
$covered = 0;
$not_covered = 0;
foreach ($this->new as $k => $new) {
if (!$new['line']) {
continue;
}
if (!$new['type']) {
continue;
}
if (empty($coverage[$new['line'] - 1])) {
continue;
}
switch ($coverage[$new['line'] - 1]) {
case 'C':
$covered++;
break;
case 'U':
$not_covered++;
break;
}
}
if (!$covered && !$not_covered) {
return $na;
}
return sprintf('%d%%', 100 * ($covered / ($covered + $not_covered)));
}
public function detectCopiedCode(
array $changesets,
$min_width = 30,
$min_lines = 3) {
assert_instances_of($changesets, 'DifferentialChangeset');
$map = array();
$files = array();
$types = array();
foreach ($changesets as $changeset) {
$file = $changeset->getFilename();
foreach ($changeset->getHunks() as $hunk) {
$lines = $hunk->getStructuredOldFile();
foreach ($lines as $line => $info) {
$type = $info['type'];
if ($type == '\\') {
continue;
}
$types[$file][$line] = $type;
$text = $info['text'];
$text = trim($text);
$files[$file][$line] = $text;
if (strlen($text) >= $min_width) {
$map[$text][] = array($file, $line);
}
}
}
}
foreach ($changesets as $changeset) {
$copies = array();
foreach ($changeset->getHunks() as $hunk) {
$added = $hunk->getStructuredNewFile();
$atype = array();
foreach ($added as $line => $info) {
$atype[$line] = $info['type'];
$added[$line] = trim($info['text']);
}
$skip_lines = 0;
foreach ($added as $line => $code) {
if ($skip_lines) {
// We're skipping lines that we already processed because we
// extended a block above them downward to include them.
$skip_lines--;
continue;
}
if ($atype[$line] !== '+') {
// This line hasn't been changed in the new file, so don't try
// to figure out where it came from.
continue;
}
if (empty($map[$code])) {
// This line was too short to trigger copy/move detection.
continue;
}
if (count($map[$code]) > 16) {
// If there are a large number of identical lines in this diff,
// don't try to figure out where this block came from: the analysis
// is O(N^2), since we need to compare every line against every
// other line. Even if we arrive at a result, it is unlikely to be
// meaningful. See T5041.
continue;
}
$best_length = 0;
// Explore all candidates.
foreach ($map[$code] as $val) {
list($file, $orig_line) = $val;
$length = 1;
// Search backward and forward to find all of the adjacent lines
// which match.
foreach (array(-1, 1) as $direction) {
$offset = $direction;
while (true) {
if (isset($copies[$line + $offset])) {
// If we run into a block above us which we've already
// attributed to a move or copy from elsewhere, stop
// looking.
break;
}
if (!isset($added[$line + $offset])) {
// If we've run off the beginning or end of the new file,
// stop looking.
break;
}
if (!isset($files[$file][$orig_line + $offset])) {
// If we've run off the beginning or end of the original
// file, we also stop looking.
break;
}
$old = $files[$file][$orig_line + $offset];
$new = $added[$line + $offset];
if ($old !== $new) {
// If the old line doesn't match the new line, stop
// looking.
break;
}
$length++;
$offset += $direction;
}
}
if ($length < $best_length) {
// If we already know of a better source (more matching lines)
// for this move/copy, stick with that one. We prefer long
// copies/moves which match a lot of context over short ones.
continue;
}
if ($length == $best_length) {
if (idx($types[$file], $orig_line) != '-') {
// If we already know of an equally good source (same number
// of matching lines) and this isn't a move, stick with the
// other one. We prefer moves over copies.
continue;
}
}
$best_length = $length;
// ($offset - 1) contains number of forward matching lines.
$best_offset = $offset - 1;
$best_file = $file;
$best_line = $orig_line;
}
$file = ($best_file == $changeset->getFilename() ? '' : $best_file);
for ($i = $best_length; $i--; ) {
$type = idx($types[$best_file], $best_line + $best_offset - $i);
$copies[$line + $best_offset - $i] = ($best_length < $min_lines
? array() // Ignore short blocks.
: array($file, $best_line + $best_offset - $i, $type));
}
$skip_lines = $best_offset;
}
}
$copies = array_filter($copies);
if ($copies) {
$metadata = $changeset->getMetadata();
$metadata['copy:lines'] = $copies;
$changeset->setMetadata($metadata);
}
}
return $changesets;
}
/**
* Build maps from lines comments appear on to actual lines.
*/
private function buildLineBackmaps() {
$old_back = array();
$new_back = array();
foreach ($this->old as $ii => $old) {
$old_back[$old['line']] = $old['line'];
}
foreach ($this->new as $ii => $new) {
$new_back[$new['line']] = $new['line'];
}
$max_old_line = 0;
$max_new_line = 0;
foreach ($this->comments as $comment) {
if ($this->isCommentOnRightSideWhenDisplayed($comment)) {
$max_new_line = max($max_new_line, $comment->getLineNumber());
} else {
$max_old_line = max($max_old_line, $comment->getLineNumber());
}
}
$cursor = 1;
for ($ii = 1; $ii <= $max_old_line; $ii++) {
if (empty($old_back[$ii])) {
$old_back[$ii] = $cursor;
} else {
$cursor = $old_back[$ii];
}
}
$cursor = 1;
for ($ii = 1; $ii <= $max_new_line; $ii++) {
if (empty($new_back[$ii])) {
$new_back[$ii] = $cursor;
} else {
$cursor = $new_back[$ii];
}
}
return array($old_back, $new_back);
}
}
diff --git a/src/applications/differential/parser/DifferentialCommitMessageParser.php b/src/applications/differential/parser/DifferentialCommitMessageParser.php
index 5e3c14aa3..00d2fe2c3 100644
--- a/src/applications/differential/parser/DifferentialCommitMessageParser.php
+++ b/src/applications/differential/parser/DifferentialCommitMessageParser.php
@@ -1,214 +1,216 @@
<?php
/**
* Parses commit messages (containing relatively freeform text with textual
* field labels) into a dictionary of fields.
*
* $parser = id(new DifferentialCommitMessageParser())
* ->setLabelMap($label_map)
* ->setTitleKey($key_title)
* ->setSummaryKey($key_summary);
*
* $fields = $parser->parseCorpus($corpus);
* $errors = $parser->getErrors();
*
* This is used by Differential to parse messages entered from the command line.
*
* @task config Configuring the Parser
* @task parse Parsing Messages
* @task support Support Methods
* @task internal Internals
*/
final class DifferentialCommitMessageParser {
private $labelMap;
private $titleKey;
private $summaryKey;
private $errors;
/* -( Configuring the Parser )--------------------------------------------- */
/**
* @task config
*/
public function setLabelMap(array $label_map) {
$this->labelMap = $label_map;
return $this;
}
/**
* @task config
*/
public function setTitleKey($title_key) {
$this->titleKey = $title_key;
return $this;
}
/**
* @task config
*/
public function setSummaryKey($summary_key) {
$this->summaryKey = $summary_key;
return $this;
}
/* -( Parsing Messages )--------------------------------------------------- */
/**
* @task parse
*/
public function parseCorpus($corpus) {
$this->errors = array();
$label_map = $this->labelMap;
$key_title = $this->titleKey;
$key_summary = $this->summaryKey;
if (!$key_title || !$key_summary || ($label_map === null)) {
throw new Exception(
pht(
- 'Expected labelMap, summaryKey and titleKey to be set before '.
- 'parsing a corpus.'));
+ 'Expected %s, %s and %s to be set before parsing a corpus.',
+ 'labelMap',
+ 'summaryKey',
+ 'titleKey'));
}
$label_regexp = $this->buildLabelRegexp($label_map);
// NOTE: We're special casing things here to make the "Title:" label
// optional in the message.
$field = $key_title;
$seen = array();
$lines = explode("\n", trim($corpus));
$field_map = array();
foreach ($lines as $key => $line) {
$match = null;
if (preg_match($label_regexp, $line, $match)) {
$lines[$key] = trim($match['text']);
$field = $label_map[self::normalizeFieldLabel($match['field'])];
if (!empty($seen[$field])) {
$this->errors[] = pht(
'Field "%s" occurs twice in commit message!',
$field);
}
$seen[$field] = true;
}
$field_map[$key] = $field;
}
$fields = array();
foreach ($lines as $key => $line) {
$fields[$field_map[$key]][] = $line;
}
// This is a piece of special-cased magic which allows you to omit the
// field labels for "title" and "summary". If the user enters a large block
// of text at the beginning of the commit message with an empty line in it,
// treat everything before the blank line as "title" and everything after
// as "summary".
if (isset($fields[$key_title]) && empty($fields[$key_summary])) {
$lines = $fields[$key_title];
for ($ii = 0; $ii < count($lines); $ii++) {
if (strlen(trim($lines[$ii])) == 0) {
break;
}
}
if ($ii != count($lines)) {
$fields[$key_title] = array_slice($lines, 0, $ii);
$summary = array_slice($lines, $ii);
if (strlen(trim(implode("\n", $summary)))) {
$fields[$key_summary] = $summary;
}
}
}
// Implode all the lines back into chunks of text.
foreach ($fields as $name => $lines) {
$data = rtrim(implode("\n", $lines));
$data = ltrim($data, "\n");
$fields[$name] = $data;
}
// This is another piece of special-cased magic which allows you to
// enter a ridiculously long title, or just type a big block of stream
// of consciousness text, and have some sort of reasonable result conjured
// from it.
if (isset($fields[$key_title])) {
$terminal = '...';
$title = $fields[$key_title];
$short = id(new PhutilUTF8StringTruncator())
->setMaximumBytes(250)
->setTerminator($terminal)
->truncateString($title);
if ($short != $title) {
// If we shortened the title, split the rest into the summary, so
// we end up with a title like:
//
// Title title tile title title...
//
// ...and a summary like:
//
// ...title title title.
//
// Summary summary summary summary.
$summary = idx($fields, $key_summary, '');
$offset = strlen($short) - strlen($terminal);
$remainder = ltrim(substr($fields[$key_title], $offset));
$summary = '...'.$remainder."\n\n".$summary;
$summary = rtrim($summary, "\n");
$fields[$key_title] = $short;
$fields[$key_summary] = $summary;
}
}
return $fields;
}
/**
* @task parse
*/
public function getErrors() {
return $this->errors;
}
/* -( Support Methods )---------------------------------------------------- */
/**
* @task support
*/
public static function normalizeFieldLabel($label) {
return phutil_utf8_strtolower($label);
}
/* -( Internals )---------------------------------------------------------- */
/**
* @task internal
*/
private function buildLabelRegexp(array $label_map) {
$field_labels = array_keys($label_map);
foreach ($field_labels as $key => $label) {
$field_labels[$key] = preg_quote($label, '/');
}
$field_labels = implode('|', $field_labels);
$field_pattern = '/^(?P<field>'.$field_labels.'):(?P<text>.*)$/i';
return $field_pattern;
}
}
diff --git a/src/applications/differential/parser/DifferentialHunkParser.php b/src/applications/differential/parser/DifferentialHunkParser.php
index e381ca834..d49f4e1e2 100644
--- a/src/applications/differential/parser/DifferentialHunkParser.php
+++ b/src/applications/differential/parser/DifferentialHunkParser.php
@@ -1,682 +1,675 @@
<?php
final class DifferentialHunkParser {
private $oldLines;
private $newLines;
private $intraLineDiffs;
private $visibleLinesMask;
private $whitespaceMode;
/**
* Get a map of lines on which hunks start, other than line 1. This
* datastructure is used to determine when to render "Context not available."
* in diffs with multiple hunks.
*
* @return dict<int, bool> Map of lines where hunks start, other than line 1.
*/
public function getHunkStartLines(array $hunks) {
assert_instances_of($hunks, 'DifferentialHunk');
$map = array();
foreach ($hunks as $hunk) {
$line = $hunk->getOldOffset();
if ($line > 1) {
$map[$line] = true;
}
}
return $map;
}
private function setVisibleLinesMask($mask) {
$this->visibleLinesMask = $mask;
return $this;
}
public function getVisibleLinesMask() {
if ($this->visibleLinesMask === null) {
- throw new Exception(
- 'You must generateVisibileLinesMask before accessing this data.'
- );
+ throw new PhutilInvalidStateException('generateVisibileLinesMask');
}
return $this->visibleLinesMask;
}
private function setIntraLineDiffs($intra_line_diffs) {
$this->intraLineDiffs = $intra_line_diffs;
return $this;
}
public function getIntraLineDiffs() {
if ($this->intraLineDiffs === null) {
- throw new Exception(
- 'You must generateIntraLineDiffs before accessing this data.'
- );
+ throw new PhutilInvalidStateException('generateIntraLineDiffs');
}
return $this->intraLineDiffs;
}
private function setNewLines($new_lines) {
$this->newLines = $new_lines;
return $this;
}
public function getNewLines() {
if ($this->newLines === null) {
- throw new Exception(
- 'You must parseHunksForLineData before accessing this data.'
- );
+ throw new PhutilInvalidStateException('parseHunksForLineData');
}
return $this->newLines;
}
private function setOldLines($old_lines) {
$this->oldLines = $old_lines;
return $this;
}
public function getOldLines() {
if ($this->oldLines === null) {
- throw new Exception(
- 'You must parseHunksForLineData before accessing this data.'
- );
+ throw new PhutilInvalidStateException('parseHunksForLineData');
}
return $this->oldLines;
}
public function getOldLineTypeMap() {
$map = array();
$old = $this->getOldLines();
foreach ($old as $o) {
if (!$o) {
continue;
}
$map[$o['line']] = $o['type'];
}
return $map;
}
public function setOldLineTypeMap(array $map) {
$lines = $this->getOldLines();
foreach ($lines as $key => $data) {
$lines[$key]['type'] = idx($map, $data['line']);
}
$this->oldLines = $lines;
return $this;
}
public function getNewLineTypeMap() {
$map = array();
$new = $this->getNewLines();
foreach ($new as $n) {
if (!$n) {
continue;
}
$map[$n['line']] = $n['type'];
}
return $map;
}
public function setNewLineTypeMap(array $map) {
$lines = $this->getNewLines();
foreach ($lines as $key => $data) {
$lines[$key]['type'] = idx($map, $data['line']);
}
$this->newLines = $lines;
return $this;
}
public function setWhitespaceMode($white_space_mode) {
$this->whitespaceMode = $white_space_mode;
return $this;
}
private function getWhitespaceMode() {
if ($this->whitespaceMode === null) {
throw new Exception(
- 'You must setWhitespaceMode before accessing this data.'
- );
+ pht(
+ 'You must %s before accessing this data.',
+ 'setWhitespaceMode'));
}
return $this->whitespaceMode;
}
public function getIsDeleted() {
foreach ($this->getNewLines() as $line) {
if ($line) {
// At least one new line, so the entire file wasn't deleted.
return false;
}
}
foreach ($this->getOldLines() as $line) {
if ($line) {
// No new lines, at least one old line; the entire file was deleted.
return true;
}
}
// This is an empty file.
return false;
}
/**
* Returns true if the hunks change any text, not just whitespace.
*/
public function getHasTextChanges() {
return $this->getHasChanges('text');
}
/**
* Returns true if the hunks change anything, including whitespace.
*/
public function getHasAnyChanges() {
return $this->getHasChanges('any');
}
private function getHasChanges($filter) {
if ($filter !== 'any' && $filter !== 'text') {
- throw new Exception("Unknown change filter '{$filter}'.");
+ throw new Exception(pht("Unknown change filter '%s'.", $filter));
}
$old = $this->getOldLines();
$new = $this->getNewLines();
$is_any = ($filter === 'any');
foreach ($old as $key => $o) {
$n = $new[$key];
if ($o === null || $n === null) {
// One side is missing, and it's impossible for both sides to be null,
// so the other side must have something, and thus the two sides are
// different and the file has been changed under any type of filter.
return true;
}
if ($o['type'] !== $n['type']) {
// The types are different, so either the underlying text is actually
// different or whatever whitespace rules we're using consider them
// different.
return true;
}
if ($o['text'] !== $n['text']) {
if ($is_any) {
// The text is different, so there's a change.
return true;
} else if (trim($o['text']) !== trim($n['text'])) {
return true;
}
}
}
// No changes anywhere in the file.
return false;
}
/**
* This function takes advantage of the parsing work done in
* @{method:parseHunksForLineData} and continues the struggle to hammer this
* data into something we can display to a user.
*
* In particular, this function re-parses the hunks to make them equivalent
* in length for easy rendering, adding `null` as necessary to pad the
* length.
*
* Anyhoo, this function is not particularly well-named but I try.
*
* NOTE: this function must be called after
* @{method:parseHunksForLineData}.
*/
public function reparseHunksForSpecialAttributes() {
$rebuild_old = array();
$rebuild_new = array();
$old_lines = array_reverse($this->getOldLines());
$new_lines = array_reverse($this->getNewLines());
while (count($old_lines) || count($new_lines)) {
$old_line_data = array_pop($old_lines);
$new_line_data = array_pop($new_lines);
if ($old_line_data) {
$o_type = $old_line_data['type'];
} else {
$o_type = null;
}
if ($new_line_data) {
$n_type = $new_line_data['type'];
} else {
$n_type = null;
}
// This line does not exist in the new file.
if (($o_type != null) && ($n_type == null)) {
$rebuild_old[] = $old_line_data;
$rebuild_new[] = null;
if ($new_line_data) {
array_push($new_lines, $new_line_data);
}
continue;
}
// This line does not exist in the old file.
if (($n_type != null) && ($o_type == null)) {
$rebuild_old[] = null;
$rebuild_new[] = $new_line_data;
if ($old_line_data) {
array_push($old_lines, $old_line_data);
}
continue;
}
$rebuild_old[] = $old_line_data;
$rebuild_new[] = $new_line_data;
}
$this->setOldLines($rebuild_old);
$this->setNewLines($rebuild_new);
$this->updateChangeTypesForWhitespaceMode();
return $this;
}
private function updateChangeTypesForWhitespaceMode() {
$mode = $this->getWhitespaceMode();
$mode_show_all = DifferentialChangesetParser::WHITESPACE_SHOW_ALL;
if ($mode === $mode_show_all) {
// If we're showing all whitespace, we don't need to perform any updates.
return;
}
$mode_trailing = DifferentialChangesetParser::WHITESPACE_IGNORE_TRAILING;
$is_trailing = ($mode === $mode_trailing);
$new = $this->getNewLines();
$old = $this->getOldLines();
foreach ($old as $key => $o) {
$n = $new[$key];
if (!$o || !$n) {
continue;
}
if ($is_trailing) {
// In "trailing" mode, we need to identify lines which are marked
// changed but differ only by trailing whitespace. We mark these lines
// unchanged.
if ($o['type'] != $n['type']) {
if (rtrim($o['text']) === rtrim($n['text'])) {
$old[$key]['type'] = null;
$new[$key]['type'] = null;
}
}
} else {
// In "ignore most" and "ignore all" modes, we need to identify lines
// which are marked unchanged but have internal whitespace changes.
// We want to ignore leading and trailing whitespace changes only, not
// internal whitespace changes (`diff` doesn't have a mode for this, so
// we have to fix it here). If the text is marked unchanged but the
// old and new text differs by internal space, mark the lines changed.
if ($o['type'] === null && $n['type'] === null) {
if ($o['text'] !== $n['text']) {
if (trim($o['text']) !== trim($n['text'])) {
$old[$key]['type'] = '-';
$new[$key]['type'] = '+';
}
}
}
}
}
$this->setOldLines($old);
$this->setNewLines($new);
return $this;
}
public function generateIntraLineDiffs() {
$old = $this->getOldLines();
$new = $this->getNewLines();
$diffs = array();
foreach ($old as $key => $o) {
$n = $new[$key];
if (!$o || !$n) {
continue;
}
if ($o['type'] != $n['type']) {
$diffs[$key] = ArcanistDiffUtils::generateIntralineDiff(
$o['text'],
$n['text']);
}
}
$this->setIntraLineDiffs($diffs);
return $this;
}
public function generateVisibileLinesMask() {
$lines_context = DifferentialChangesetParser::LINES_CONTEXT;
$old = $this->getOldLines();
$new = $this->getNewLines();
$max_length = max(count($old), count($new));
$visible = false;
$last = 0;
$mask = array();
for ($cursor = -$lines_context; $cursor < $max_length; $cursor++) {
$offset = $cursor + $lines_context;
if ((isset($old[$offset]) && $old[$offset]['type']) ||
(isset($new[$offset]) && $new[$offset]['type'])) {
$visible = true;
$last = $offset;
} else if ($cursor > $last + $lines_context) {
$visible = false;
}
if ($visible && $cursor > 0) {
$mask[$cursor] = 1;
}
}
$this->setVisibleLinesMask($mask);
return $this;
}
public function getOldCorpus() {
return $this->getCorpus($this->getOldLines());
}
public function getNewCorpus() {
return $this->getCorpus($this->getNewLines());
}
private function getCorpus(array $lines) {
$corpus = array();
foreach ($lines as $l) {
if ($l['type'] != '\\') {
if ($l['text'] === null) {
// There's no text on this side of the diff, but insert a placeholder
// newline so the highlighted line numbers match up.
$corpus[] = "\n";
} else {
$corpus[] = $l['text'];
}
}
}
return $corpus;
}
public function parseHunksForLineData(array $hunks) {
assert_instances_of($hunks, 'DifferentialHunk');
$old_lines = array();
$new_lines = array();
foreach ($hunks as $hunk) {
$lines = $hunk->getSplitLines();
$line_type_map = array();
$line_text = array();
foreach ($lines as $line_index => $line) {
if (isset($line[0])) {
$char = $line[0];
switch ($char) {
case ' ':
$line_type_map[$line_index] = null;
$line_text[$line_index] = substr($line, 1);
break;
case "\r":
case "\n":
// NOTE: Normally, the first character is a space, plus, minus or
// backslash, but it may be a newline if it used to be a space and
// trailing whitespace has been stripped via email transmission or
// some similar mechanism. In these cases, we essentially pretend
// the missing space is still there.
$line_type_map[$line_index] = null;
$line_text[$line_index] = $line;
break;
case '+':
case '-':
case '\\':
$line_type_map[$line_index] = $char;
$line_text[$line_index] = substr($line, 1);
break;
default:
throw new Exception(
pht(
'Unexpected leading character "%s" at line index %s!',
$char,
$line_index));
}
} else {
$line_type_map[$line_index] = null;
$line_text[$line_index] = '';
}
}
$old_line = $hunk->getOldOffset();
$new_line = $hunk->getNewOffset();
$num_lines = count($lines);
for ($cursor = 0; $cursor < $num_lines; $cursor++) {
$type = $line_type_map[$cursor];
$data = array(
'type' => $type,
'text' => $line_text[$cursor],
'line' => $new_line,
);
if ($type == '\\') {
$type = $line_type_map[$cursor - 1];
$data['text'] = ltrim($data['text']);
}
switch ($type) {
case '+':
$new_lines[] = $data;
++$new_line;
break;
case '-':
$data['line'] = $old_line;
$old_lines[] = $data;
++$old_line;
break;
default:
$new_lines[] = $data;
$data['line'] = $old_line;
$old_lines[] = $data;
++$new_line;
++$old_line;
break;
}
}
}
$this->setOldLines($old_lines);
$this->setNewLines($new_lines);
return $this;
}
public function parseHunksForHighlightMasks(
array $changeset_hunks,
array $old_hunks,
array $new_hunks) {
assert_instances_of($changeset_hunks, 'DifferentialHunk');
assert_instances_of($old_hunks, 'DifferentialHunk');
assert_instances_of($new_hunks, 'DifferentialHunk');
// Put changes side by side.
$olds = array();
$news = array();
$olds_cursor = -1;
$news_cursor = -1;
foreach ($changeset_hunks as $hunk) {
$n_old = $hunk->getOldOffset();
$n_new = $hunk->getNewOffset();
$changes = $hunk->getSplitLines();
foreach ($changes as $line) {
$diff_type = $line[0]; // Change type in diff of diffs.
$orig_type = $line[1]; // Change type in the original diff.
if ($diff_type == ' ') {
// Use the same key for lines that are next to each other.
if ($olds_cursor > $news_cursor) {
$key = $olds_cursor + 1;
} else {
$key = $news_cursor + 1;
}
$olds[$key] = null;
$news[$key] = null;
$olds_cursor = $key;
$news_cursor = $key;
} else if ($diff_type == '-') {
$olds[] = array($n_old, $orig_type);
$olds_cursor++;
} else if ($diff_type == '+') {
$news[] = array($n_new, $orig_type);
$news_cursor++;
}
if (($diff_type == '-' || $diff_type == ' ') && $orig_type != '-') {
$n_old++;
}
if (($diff_type == '+' || $diff_type == ' ') && $orig_type != '-') {
$n_new++;
}
}
}
$offsets_old = $this->computeOffsets($old_hunks);
$offsets_new = $this->computeOffsets($new_hunks);
// Highlight lines that were added on each side or removed on the other
// side.
$highlight_old = array();
$highlight_new = array();
$last = max(last_key($olds), last_key($news));
for ($i = 0; $i <= $last; $i++) {
if (isset($olds[$i])) {
list($n, $type) = $olds[$i];
if ($type == '+' ||
($type == ' ' && isset($news[$i]) && $news[$i][1] != ' ')) {
$highlight_old[] = $offsets_old[$n];
}
}
if (isset($news[$i])) {
list($n, $type) = $news[$i];
if ($type == '+' ||
($type == ' ' && isset($olds[$i]) && $olds[$i][1] != ' ')) {
$highlight_new[] = $offsets_new[$n];
}
}
}
return array($highlight_old, $highlight_new);
}
public function makeContextDiff(
array $hunks,
$is_new,
$line_number,
$line_length,
$add_context) {
assert_instances_of($hunks, 'DifferentialHunk');
$context = array();
if ($is_new) {
$prefix = '+';
} else {
$prefix = '-';
}
foreach ($hunks as $hunk) {
if ($is_new) {
$offset = $hunk->getNewOffset();
$length = $hunk->getNewLen();
} else {
$offset = $hunk->getOldOffset();
$length = $hunk->getOldLen();
}
$start = $line_number - $offset;
$end = $start + $line_length;
// We need to go in if $start == $length, because the last line
// might be a "\No newline at end of file" marker, which we want
// to show if the additional context is > 0.
if ($start <= $length && $end >= 0) {
$start = $start - $add_context;
$end = $end + $add_context;
$hunk_content = array();
$hunk_pos = array( '-' => 0, '+' => 0 );
$hunk_offset = array( '-' => null, '+' => null );
$hunk_last = array( '-' => null, '+' => null );
foreach (explode("\n", $hunk->getChanges()) as $line) {
$in_common = strncmp($line, ' ', 1) === 0;
$in_old = strncmp($line, '-', 1) === 0 || $in_common;
$in_new = strncmp($line, '+', 1) === 0 || $in_common;
$in_selected = strncmp($line, $prefix, 1) === 0;
$skip = !$in_selected && !$in_common;
if ($hunk_pos[$prefix] <= $end) {
if ($start <= $hunk_pos[$prefix]) {
if (!$skip || ($hunk_pos[$prefix] != $start &&
$hunk_pos[$prefix] != $end)) {
if ($in_old) {
if ($hunk_offset['-'] === null) {
$hunk_offset['-'] = $hunk_pos['-'];
}
$hunk_last['-'] = $hunk_pos['-'];
}
if ($in_new) {
if ($hunk_offset['+'] === null) {
$hunk_offset['+'] = $hunk_pos['+'];
}
$hunk_last['+'] = $hunk_pos['+'];
}
$hunk_content[] = $line;
}
}
if ($in_old) { ++$hunk_pos['-']; }
if ($in_new) { ++$hunk_pos['+']; }
}
}
if ($hunk_offset['-'] !== null || $hunk_offset['+'] !== null) {
$header = '@@';
if ($hunk_offset['-'] !== null) {
$header .= ' -'.($hunk->getOldOffset() + $hunk_offset['-']).
','.($hunk_last['-'] - $hunk_offset['-'] + 1);
}
if ($hunk_offset['+'] !== null) {
$header .= ' +'.($hunk->getNewOffset() + $hunk_offset['+']).
','.($hunk_last['+'] - $hunk_offset['+'] + 1);
}
$header .= ' @@';
$context[] = $header;
$context[] = implode("\n", $hunk_content);
}
}
}
return implode("\n", $context);
}
private function computeOffsets(array $hunks) {
assert_instances_of($hunks, 'DifferentialHunk');
$offsets = array();
$n = 1;
foreach ($hunks as $hunk) {
$new_length = $hunk->getNewLen();
$new_offset = $hunk->getNewOffset();
for ($i = 0; $i < $new_length; $i++) {
$offsets[$n] = $new_offset + $i;
$n++;
}
}
return $offsets;
}
}
diff --git a/src/applications/differential/parser/__tests__/DifferentialCommitMessageParserTestCase.php b/src/applications/differential/parser/__tests__/DifferentialCommitMessageParserTestCase.php
index 8396237bc..669cfed22 100644
--- a/src/applications/differential/parser/__tests__/DifferentialCommitMessageParserTestCase.php
+++ b/src/applications/differential/parser/__tests__/DifferentialCommitMessageParserTestCase.php
@@ -1,58 +1,59 @@
<?php
final class DifferentialCommitMessageParserTestCase
extends PhabricatorTestCase {
public function testDifferentialCommitMessageParser() {
$dir = dirname(__FILE__).'/messages/';
$list = Filesystem::listDirectory($dir, $include_hidden = false);
foreach ($list as $file) {
if (!preg_match('/.txt$/', $file)) {
continue;
}
$data = Filesystem::readFile($dir.$file);
$divider = "~~~~~~~~~~\n";
$parts = explode($divider, $data);
if (count($parts) !== 4) {
throw new Exception(
pht(
'Expected test file "%s" to contain four parts (message, fields, '.
- 'output, errors) divided by "~~~~~~~~~~".',
- $file));
+ 'output, errors) divided by "%s".',
+ $file,
+ '~~~~~~~~~~'));
}
list($message, $fields, $output, $errors) = $parts;
$fields = phutil_json_decode($fields);
$output = phutil_json_decode($output);
$errors = phutil_json_decode($errors);
$parser = id(new DifferentialCommitMessageParser())
->setLabelMap($fields)
->setTitleKey('title')
->setSummaryKey('summary');
$result_output = $parser->parseCorpus($message);
$result_errors = $parser->getErrors();
$this->assertEqual($output, $result_output);
$this->assertEqual($errors, $result_errors);
}
}
public function testDifferentialCommitMessageParserNormalization() {
$map = array(
'Test Plan' => 'test plan',
'REVIEWERS' => 'reviewers',
'sUmmArY' => 'summary',
);
foreach ($map as $input => $expect) {
$this->assertEqual(
$expect,
DifferentialCommitMessageParser::normalizeFieldLabel($input),
pht('Field normalization of label "%s".', $input));
}
}
}
diff --git a/src/applications/differential/parser/__tests__/DifferentialHunkParserTestCase.php b/src/applications/differential/parser/__tests__/DifferentialHunkParserTestCase.php
index 45c7ac922..d1d2501b9 100644
--- a/src/applications/differential/parser/__tests__/DifferentialHunkParserTestCase.php
+++ b/src/applications/differential/parser/__tests__/DifferentialHunkParserTestCase.php
@@ -1,291 +1,291 @@
<?php
final class DifferentialHunkParserTestCase extends PhabricatorTestCase {
private function createComment() {
$comment = new DifferentialInlineComment();
return $comment;
}
private function createHunk(
$old_offset,
$old_len,
$new_offset,
$new_len,
$changes) {
$hunk = id(new DifferentialModernHunk())
->setOldOffset($old_offset)
->setOldLen($old_len)
->setNewOffset($new_offset)
->setNewLen($new_len)
->setChanges($changes);
return $hunk;
}
// Returns a change that consists of a single hunk, starting at line 1.
private function createSingleChange($old_lines, $new_lines, $changes) {
return array(
0 => $this->createHunk(1, $old_lines, 1, $new_lines, $changes),
);
}
private function createHunksFromFile($name) {
$data = Filesystem::readFile(dirname(__FILE__).'/data/'.$name);
$parser = new ArcanistDiffParser();
$changes = $parser->parseDiff($data);
if (count($changes) !== 1) {
- throw new Exception("Expected 1 changeset for '{$name}'!");
+ throw new Exception(pht("Expected 1 changeset for '%s'!", $name));
}
$diff = DifferentialDiff::newFromRawChanges(
PhabricatorUser::getOmnipotentUser(),
$changes);
return head($diff->getChangesets())->getHunks();
}
public function testOneLineOldComment() {
$parser = new DifferentialHunkParser();
$hunks = $this->createSingleChange(1, 0, '-a');
$context = $parser->makeContextDiff(
$hunks,
0,
1,
0,
0);
$this->assertEqual("@@ -1,1 @@\n-a", $context);
}
public function testOneLineNewComment() {
$parser = new DifferentialHunkParser();
$hunks = $this->createSingleChange(0, 1, '+a');
$context = $parser->makeContextDiff(
$hunks,
1,
1,
0,
0);
$this->assertEqual("@@ +1,1 @@\n+a", $context);
}
public function testCannotFindContext() {
$parser = new DifferentialHunkParser();
$hunks = $this->createSingleChange(0, 1, '+a');
$context = $parser->makeContextDiff(
$hunks,
1,
2,
0,
0);
$this->assertEqual('', $context);
}
public function testOverlapFromStartOfHunk() {
$parser = new DifferentialHunkParser();
$hunks = array(
0 => $this->createHunk(23, 2, 42, 2, " 1\n 2"),
);
$context = $parser->makeContextDiff(
$hunks,
1,
41,
1,
0);
$this->assertEqual("@@ -23,1 +42,1 @@\n 1", $context);
}
public function testOverlapAfterEndOfHunk() {
$parser = new DifferentialHunkParser();
$hunks = array(
0 => $this->createHunk(23, 2, 42, 2, " 1\n 2"),
);
$context = $parser->makeContextDiff(
$hunks,
1,
43,
1,
0);
$this->assertEqual("@@ -24,1 +43,1 @@\n 2", $context);
}
public function testInclusionOfNewFileInOldCommentFromStart() {
$parser = new DifferentialHunkParser();
$hunks = $this->createSingleChange(2, 3,
- "+n1\n".
- " e1/2\n".
- "-o2\n".
- "+n3\n");
+ "+n1\n".
+ " e1/2\n".
+ "-o2\n".
+ "+n3\n");
$context = $parser->makeContextDiff(
$hunks,
0,
1,
1,
0);
$this->assertEqual(
- "@@ -1,2 +2,1 @@\n".
- " e1/2\n".
- "-o2", $context);
+ "@@ -1,2 +2,1 @@\n".
+ " e1/2\n".
+ "-o2", $context);
}
public function testInclusionOfOldFileInNewCommentFromStart() {
$parser = new DifferentialHunkParser();
$hunks = $this->createSingleChange(2, 2,
- "-o1\n".
- " e2/1\n".
- "-o3\n".
- "+n2\n");
+ "-o1\n".
+ " e2/1\n".
+ "-o3\n".
+ "+n2\n");
$context = $parser->makeContextDiff(
$hunks,
1,
1,
1,
0);
$this->assertEqual(
- "@@ -2,1 +1,2 @@\n".
- " e2/1\n".
- "+n2", $context);
+ "@@ -2,1 +1,2 @@\n".
+ " e2/1\n".
+ "+n2", $context);
}
public function testNoNewlineAtEndOfFile() {
$parser = new DifferentialHunkParser();
$hunks = $this->createSingleChange(0, 1,
- "+a\n".
- "\\No newline at end of file");
+ "+a\n".
+ "\\No newline at end of file");
// Note that this only works with additional context.
$context = $parser->makeContextDiff(
$hunks,
1,
2,
0,
1);
$this->assertEqual(
- "@@ +1,1 @@\n".
- "+a\n".
- "\\No newline at end of file", $context);
+ "@@ +1,1 @@\n".
+ "+a\n".
+ "\\No newline at end of file", $context);
}
public function testMultiLineNewComment() {
$parser = new DifferentialHunkParser();
$hunks = $this->createSingleChange(7, 7,
- " e1\n".
- " e2\n".
- "-o3\n".
- "-o4\n".
- "+n3\n".
- " e5/4\n".
- " e6/5\n".
- "+n6\n".
- " e7\n");
+ " e1\n".
+ " e2\n".
+ "-o3\n".
+ "-o4\n".
+ "+n3\n".
+ " e5/4\n".
+ " e6/5\n".
+ "+n6\n".
+ " e7\n");
$context = $parser->makeContextDiff(
$hunks,
1,
2,
4,
0);
$this->assertEqual(
- "@@ -2,5 +2,5 @@\n".
- " e2\n".
- "-o3\n".
- "-o4\n".
- "+n3\n".
- " e5/4\n".
- " e6/5\n".
- "+n6", $context);
+ "@@ -2,5 +2,5 @@\n".
+ " e2\n".
+ "-o3\n".
+ "-o4\n".
+ "+n3\n".
+ " e5/4\n".
+ " e6/5\n".
+ "+n6", $context);
}
public function testMultiLineOldComment() {
$parser = new DifferentialHunkParser();
$hunks = $this->createSingleChange(7, 7,
- " e1\n".
- " e2\n".
- "-o3\n".
- "-o4\n".
- "+n3\n".
- " e5/4\n".
- " e6/5\n".
- "+n6\n".
- " e7\n");
+ " e1\n".
+ " e2\n".
+ "-o3\n".
+ "-o4\n".
+ "+n3\n".
+ " e5/4\n".
+ " e6/5\n".
+ "+n6\n".
+ " e7\n");
$context = $parser->makeContextDiff(
$hunks,
0,
2,
4,
0);
$this->assertEqual(
- "@@ -2,5 +2,4 @@\n".
- " e2\n".
- "-o3\n".
- "-o4\n".
- "+n3\n".
- " e5/4\n".
- " e6/5", $context);
+ "@@ -2,5 +2,4 @@\n".
+ " e2\n".
+ "-o3\n".
+ "-o4\n".
+ "+n3\n".
+ " e5/4\n".
+ " e6/5", $context);
}
public function testInclusionOfNewFileInOldCommentFromStartWithContext() {
$parser = new DifferentialHunkParser();
$hunks = $this->createSingleChange(2, 3,
- "+n1\n".
- " e1/2\n".
- "-o2\n".
- "+n3\n");
+ "+n1\n".
+ " e1/2\n".
+ "-o2\n".
+ "+n3\n");
$context = $parser->makeContextDiff(
$hunks,
0,
1,
1,
1);
$this->assertEqual(
- "@@ -1,2 +1,2 @@\n".
- "+n1\n".
- " e1/2\n".
- "-o2", $context);
+ "@@ -1,2 +1,2 @@\n".
+ "+n1\n".
+ " e1/2\n".
+ "-o2", $context);
}
public function testInclusionOfOldFileInNewCommentFromStartWithContext() {
$parser = new DifferentialHunkParser();
$hunks = $this->createSingleChange(2, 2,
- "-o1\n".
- " e2/1\n".
- "-o3\n".
- "+n2\n");
+ "-o1\n".
+ " e2/1\n".
+ "-o3\n".
+ "+n2\n");
$context = $parser->makeContextDiff(
$hunks,
1,
1,
1,
1);
$this->assertEqual(
- "@@ -1,3 +1,2 @@\n".
- "-o1\n".
- " e2/1\n".
- "-o3\n".
- "+n2", $context);
+ "@@ -1,3 +1,2 @@\n".
+ "-o1\n".
+ " e2/1\n".
+ "-o3\n".
+ "+n2", $context);
}
public function testMissingContext() {
$tests = array(
'missing_context.diff' => array(
4 => true,
),
'missing_context_2.diff' => array(
5 => true,
),
'missing_context_3.diff' => array(
4 => true,
13 => true,
),
);
foreach ($tests as $name => $expect) {
$hunks = $this->createHunksFromFile($name);
$parser = new DifferentialHunkParser();
$actual = $parser->getHunkStartLines($hunks);
$this->assertEqual($expect, $actual, $name);
}
}
}
diff --git a/src/applications/differential/query/DifferentialHunkQuery.php b/src/applications/differential/query/DifferentialHunkQuery.php
index 3c9f89881..09beb0b6f 100644
--- a/src/applications/differential/query/DifferentialHunkQuery.php
+++ b/src/applications/differential/query/DifferentialHunkQuery.php
@@ -1,120 +1,122 @@
<?php
final class DifferentialHunkQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $changesets;
private $shouldAttachToChangesets;
public function withChangesets(array $changesets) {
assert_instances_of($changesets, 'DifferentialChangeset');
$this->changesets = $changesets;
return $this;
}
public function needAttachToChangesets($attach) {
$this->shouldAttachToChangesets = $attach;
return $this;
}
protected function willExecute() {
// If we fail to load any hunks at all (for example, because all of
// the requested changesets are directories or empty files and have no
// hunks) we'll never call didFilterPage(), and thus never have an
// opportunity to attach hunks. Attach empty hunk lists now so that we
// end up with the right result.
if ($this->shouldAttachToChangesets) {
foreach ($this->changesets as $changeset) {
$changeset->attachHunks(array());
}
}
}
protected function loadPage() {
$all_results = array();
// Load modern hunks.
$table = new DifferentialModernHunk();
$conn_r = $table->establishConnection('r');
$modern_data = queryfx_all(
$conn_r,
'SELECT * FROM %T %Q %Q %Q',
$table->getTableName(),
$this->buildWhereClause($conn_r),
$this->buildOrderClause($conn_r),
$this->buildLimitClause($conn_r));
$modern_results = $table->loadAllFromArray($modern_data);
// Now, load legacy hunks.
$table = new DifferentialLegacyHunk();
$conn_r = $table->establishConnection('r');
$legacy_data = queryfx_all(
$conn_r,
'SELECT * FROM %T %Q %Q %Q',
$table->getTableName(),
$this->buildWhereClause($conn_r),
$this->buildOrderClause($conn_r),
$this->buildLimitClause($conn_r));
$legacy_results = $table->loadAllFromArray($legacy_data);
// Strip all the IDs off since they're not unique and nothing should be
// using them.
return array_values(array_merge($legacy_results, $modern_results));
}
protected function willFilterPage(array $hunks) {
$changesets = mpull($this->changesets, null, 'getID');
foreach ($hunks as $key => $hunk) {
$changeset = idx($changesets, $hunk->getChangesetID());
if (!$changeset) {
unset($hunks[$key]);
}
$hunk->attachChangeset($changeset);
}
return $hunks;
}
protected function didFilterPage(array $hunks) {
if ($this->shouldAttachToChangesets) {
$hunk_groups = mgroup($hunks, 'getChangesetID');
foreach ($this->changesets as $changeset) {
$hunks = idx($hunk_groups, $changeset->getID(), array());
$changeset->attachHunks($hunks);
}
}
return $hunks;
}
protected function buildWhereClause(AphrontDatabaseConnection $conn_r) {
$where = array();
if (!$this->changesets) {
throw new Exception(
- pht('You must load hunks via changesets, with withChangesets()!'));
+ pht(
+ 'You must load hunks via changesets, with %s!',
+ 'withChangesets()'));
}
$where[] = qsprintf(
$conn_r,
'changesetID IN (%Ld)',
mpull($this->changesets, 'getID'));
$where[] = $this->buildPagingClause($conn_r);
return $this->formatWhereClause($where);
}
public function getQueryApplicationClass() {
return 'PhabricatorDifferentialApplication';
}
protected function getDefaultOrderVector() {
// TODO: Do we need this?
return array('-id');
}
}
diff --git a/src/applications/differential/query/DifferentialRevisionQuery.php b/src/applications/differential/query/DifferentialRevisionQuery.php
index d49ef3488..69e9477e0 100644
--- a/src/applications/differential/query/DifferentialRevisionQuery.php
+++ b/src/applications/differential/query/DifferentialRevisionQuery.php
@@ -1,1152 +1,1152 @@
<?php
/**
* Flexible query API for Differential revisions. Example:
*
* // Load open revisions
* $revisions = id(new DifferentialRevisionQuery())
* ->withStatus(DifferentialRevisionQuery::STATUS_OPEN)
* ->execute();
*
* @task config Query Configuration
* @task exec Query Execution
* @task internal Internals
*/
final class DifferentialRevisionQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $pathIDs = array();
private $status = 'status-any';
const STATUS_ANY = 'status-any';
const STATUS_OPEN = 'status-open';
const STATUS_ACCEPTED = 'status-accepted';
const STATUS_NEEDS_REVIEW = 'status-needs-review';
const STATUS_NEEDS_REVISION = 'status-needs-revision';
const STATUS_CLOSED = 'status-closed';
const STATUS_ABANDONED = 'status-abandoned';
private $authors = array();
private $draftAuthors = array();
private $ccs = array();
private $reviewers = array();
private $revIDs = array();
private $commitHashes = array();
private $commitPHIDs = array();
private $phids = array();
private $responsibles = array();
private $branches = array();
private $repositoryPHIDs;
private $updatedEpochMin;
private $updatedEpochMax;
const ORDER_MODIFIED = 'order-modified';
const ORDER_CREATED = 'order-created';
private $needRelationships = false;
private $needActiveDiffs = false;
private $needDiffIDs = false;
private $needCommitPHIDs = false;
private $needHashes = false;
private $needReviewerStatus = false;
private $needReviewerAuthority;
private $needDrafts;
private $needFlags;
private $buildingGlobalOrder;
/* -( Query Configuration )------------------------------------------------ */
/**
* Filter results to revisions which affect a Diffusion path ID in a given
* repository. You can call this multiple times to select revisions for
* several paths.
*
* @param int Diffusion repository ID.
* @param int Diffusion path ID.
* @return this
* @task config
*/
public function withPath($repository_id, $path_id) {
$this->pathIDs[] = array(
'repositoryID' => $repository_id,
'pathID' => $path_id,
);
return $this;
}
/**
* Filter results to revisions authored by one of the given PHIDs. Calling
* this function will clear anything set by previous calls to
* @{method:withAuthors}.
*
* @param array List of PHIDs of authors
* @return this
* @task config
*/
public function withAuthors(array $author_phids) {
$this->authors = $author_phids;
return $this;
}
/**
* Filter results to revisions with comments authored by the given PHIDs.
*
* @param array List of PHIDs of authors
* @return this
* @task config
*/
public function withDraftRepliesByAuthors(array $author_phids) {
$this->draftAuthors = $author_phids;
return $this;
}
/**
* Filter results to revisions which CC one of the listed people. Calling this
* function will clear anything set by previous calls to @{method:withCCs}.
*
* @param array List of PHIDs of subscribers.
* @return this
* @task config
*/
public function withCCs(array $cc_phids) {
$this->ccs = $cc_phids;
return $this;
}
/**
* Filter results to revisions that have one of the provided PHIDs as
* reviewers. Calling this function will clear anything set by previous calls
* to @{method:withReviewers}.
*
* @param array List of PHIDs of reviewers
* @return this
* @task config
*/
public function withReviewers(array $reviewer_phids) {
$this->reviewers = $reviewer_phids;
return $this;
}
/**
* Filter results to revisions that have one of the provided commit hashes.
* Calling this function will clear anything set by previous calls to
* @{method:withCommitHashes}.
*
* @param array List of pairs <Class
* ArcanistDifferentialRevisionHash::HASH_$type constant,
* hash>
* @return this
* @task config
*/
public function withCommitHashes(array $commit_hashes) {
$this->commitHashes = $commit_hashes;
return $this;
}
/**
* Filter results to revisions that have one of the provided PHIDs as
* commits. Calling this function will clear anything set by previous calls
* to @{method:withCommitPHIDs}.
*
* @param array List of PHIDs of commits
* @return this
* @task config
*/
public function withCommitPHIDs(array $commit_phids) {
$this->commitPHIDs = $commit_phids;
return $this;
}
/**
* Filter results to revisions with a given status. Provide a class constant,
* such as `DifferentialRevisionQuery::STATUS_OPEN`.
*
* @param const Class STATUS constant, like STATUS_OPEN.
* @return this
* @task config
*/
public function withStatus($status_constant) {
$this->status = $status_constant;
return $this;
}
/**
* Filter results to revisions on given branches.
*
* @param list List of branch names.
* @return this
* @task config
*/
public function withBranches(array $branches) {
$this->branches = $branches;
return $this;
}
/**
* Filter results to only return revisions whose ids are in the given set.
*
* @param array List of revision ids
* @return this
* @task config
*/
public function withIDs(array $ids) {
$this->revIDs = $ids;
return $this;
}
/**
* Filter results to only return revisions whose PHIDs are in the given set.
*
* @param array List of revision PHIDs
* @return this
* @task config
*/
public function withPHIDs(array $phids) {
$this->phids = $phids;
return $this;
}
/**
* Given a set of users, filter results to return only revisions they are
* responsible for (i.e., they are either authors or reviewers).
*
* @param array List of user PHIDs.
* @return this
* @task config
*/
public function withResponsibleUsers(array $responsible_phids) {
$this->responsibles = $responsible_phids;
return $this;
}
public function withRepositoryPHIDs(array $repository_phids) {
$this->repositoryPHIDs = $repository_phids;
return $this;
}
public function withUpdatedEpochBetween($min, $max) {
$this->updatedEpochMin = $min;
$this->updatedEpochMax = $max;
return $this;
}
/**
* Set result ordering. Provide a class constant, such as
* `DifferentialRevisionQuery::ORDER_CREATED`.
*
* @task config
*/
public function setOrder($order_constant) {
switch ($order_constant) {
case self::ORDER_CREATED:
$this->setOrderVector(array('id'));
break;
case self::ORDER_MODIFIED:
$this->setOrderVector(array('updated', 'id'));
break;
default:
throw new Exception(pht('Unknown order "%s".', $order_constant));
}
return $this;
}
/**
* Set whether or not the query will load and attach relationships.
*
* @param bool True to load and attach relationships.
* @return this
* @task config
*/
public function needRelationships($need_relationships) {
$this->needRelationships = $need_relationships;
return $this;
}
/**
* Set whether or not the query should load the active diff for each
* revision.
*
* @param bool True to load and attach diffs.
* @return this
* @task config
*/
public function needActiveDiffs($need_active_diffs) {
$this->needActiveDiffs = $need_active_diffs;
return $this;
}
/**
* Set whether or not the query should load the associated commit PHIDs for
* each revision.
*
* @param bool True to load and attach diffs.
* @return this
* @task config
*/
public function needCommitPHIDs($need_commit_phids) {
$this->needCommitPHIDs = $need_commit_phids;
return $this;
}
/**
* Set whether or not the query should load associated diff IDs for each
* revision.
*
* @param bool True to load and attach diff IDs.
* @return this
* @task config
*/
public function needDiffIDs($need_diff_ids) {
$this->needDiffIDs = $need_diff_ids;
return $this;
}
/**
* Set whether or not the query should load associated commit hashes for each
* revision.
*
* @param bool True to load and attach commit hashes.
* @return this
* @task config
*/
public function needHashes($need_hashes) {
$this->needHashes = $need_hashes;
return $this;
}
/**
* Set whether or not the query should load associated reviewer status.
*
* @param bool True to load and attach reviewers.
* @return this
* @task config
*/
public function needReviewerStatus($need_reviewer_status) {
$this->needReviewerStatus = $need_reviewer_status;
return $this;
}
/**
* Request information about the viewer's authority to act on behalf of each
* reviewer. In particular, they have authority to act on behalf of projects
* they are a member of.
*
* @param bool True to load and attach authority.
* @return this
* @task config
*/
public function needReviewerAuthority($need_reviewer_authority) {
$this->needReviewerAuthority = $need_reviewer_authority;
return $this;
}
public function needFlags($need_flags) {
$this->needFlags = $need_flags;
return $this;
}
public function needDrafts($need_drafts) {
$this->needDrafts = $need_drafts;
return $this;
}
/* -( Query Execution )---------------------------------------------------- */
/**
* Execute the query as configured, returning matching
* @{class:DifferentialRevision} objects.
*
* @return list List of matching DifferentialRevision objects.
* @task exec
*/
protected function loadPage() {
$table = new DifferentialRevision();
$conn_r = $table->establishConnection('r');
$data = $this->loadData();
return $table->loadAllFromArray($data);
}
protected function willFilterPage(array $revisions) {
$viewer = $this->getViewer();
$repository_phids = mpull($revisions, 'getRepositoryPHID');
$repository_phids = array_filter($repository_phids);
$repositories = array();
if ($repository_phids) {
$repositories = id(new PhabricatorRepositoryQuery())
->setViewer($this->getViewer())
->withPHIDs($repository_phids)
->execute();
$repositories = mpull($repositories, null, 'getPHID');
}
// If a revision is associated with a repository:
//
// - the viewer must be able to see the repository; or
// - the viewer must have an automatic view capability.
//
// In the latter case, we'll load the revision but not load the repository.
$can_view = PhabricatorPolicyCapability::CAN_VIEW;
foreach ($revisions as $key => $revision) {
$repo_phid = $revision->getRepositoryPHID();
if (!$repo_phid) {
// The revision has no associated repository. Attach `null` and move on.
$revision->attachRepository(null);
continue;
}
$repository = idx($repositories, $repo_phid);
if ($repository) {
// The revision has an associated repository, and the viewer can see
// it. Attach it and move on.
$revision->attachRepository($repository);
continue;
}
if ($revision->hasAutomaticCapability($can_view, $viewer)) {
// The revision has an associated repository which the viewer can not
// see, but the viewer has an automatic capability on this revision.
// Load the revision without attaching a repository.
$revision->attachRepository(null);
continue;
}
if ($this->getViewer()->isOmnipotent()) {
// The viewer is omnipotent. Allow the revision to load even without
// a repository.
$revision->attachRepository(null);
continue;
}
// The revision has an associated repository, and the viewer can't see
// it, and the viewer has no special capabilities. Filter out this
// revision.
$this->didRejectResult($revision);
unset($revisions[$key]);
}
if (!$revisions) {
return array();
}
$table = new DifferentialRevision();
$conn_r = $table->establishConnection('r');
if ($this->needRelationships) {
$this->loadRelationships($conn_r, $revisions);
}
if ($this->needCommitPHIDs) {
$this->loadCommitPHIDs($conn_r, $revisions);
}
$need_active = $this->needActiveDiffs;
$need_ids = $need_active || $this->needDiffIDs;
if ($need_ids) {
$this->loadDiffIDs($conn_r, $revisions);
}
if ($need_active) {
$this->loadActiveDiffs($conn_r, $revisions);
}
if ($this->needHashes) {
$this->loadHashes($conn_r, $revisions);
}
if ($this->needReviewerStatus || $this->needReviewerAuthority) {
$this->loadReviewers($conn_r, $revisions);
}
return $revisions;
}
protected function didFilterPage(array $revisions) {
$viewer = $this->getViewer();
if ($this->needFlags) {
$flags = id(new PhabricatorFlagQuery())
->setViewer($viewer)
->withOwnerPHIDs(array($viewer->getPHID()))
->withObjectPHIDs(mpull($revisions, 'getPHID'))
->execute();
$flags = mpull($flags, null, 'getObjectPHID');
foreach ($revisions as $revision) {
$revision->attachFlag(
$viewer,
idx($flags, $revision->getPHID()));
}
}
if ($this->needDrafts) {
$drafts = id(new DifferentialDraft())->loadAllWhere(
'authorPHID = %s AND objectPHID IN (%Ls)',
$viewer->getPHID(),
mpull($revisions, 'getPHID'));
$drafts = mgroup($drafts, 'getObjectPHID');
foreach ($revisions as $revision) {
$revision->attachDrafts(
$viewer,
idx($drafts, $revision->getPHID(), array()));
}
}
return $revisions;
}
private function loadData() {
$table = new DifferentialRevision();
$conn_r = $table->establishConnection('r');
$selects = array();
// NOTE: If the query includes "responsiblePHIDs", we execute it as a
// UNION of revisions they own and revisions they're reviewing. This has
// much better performance than doing it with JOIN/WHERE.
if ($this->responsibles) {
$basic_authors = $this->authors;
$basic_reviewers = $this->reviewers;
$authority_projects = id(new PhabricatorProjectQuery())
->setViewer($this->getViewer())
->withMemberPHIDs($this->responsibles)
->execute();
$authority_phids = mpull($authority_projects, 'getPHID');
try {
// Build the query where the responsible users are authors.
$this->authors = array_merge($basic_authors, $this->responsibles);
$this->reviewers = $basic_reviewers;
$selects[] = $this->buildSelectStatement($conn_r);
// Build the query where the responsible users are reviewers, or
// projects they are members of are reviewers.
$this->authors = $basic_authors;
$this->reviewers = array_merge(
$basic_reviewers,
$this->responsibles,
$authority_phids);
$selects[] = $this->buildSelectStatement($conn_r);
// Put everything back like it was.
$this->authors = $basic_authors;
$this->reviewers = $basic_reviewers;
} catch (Exception $ex) {
$this->authors = $basic_authors;
$this->reviewers = $basic_reviewers;
throw $ex;
}
} else {
$selects[] = $this->buildSelectStatement($conn_r);
}
if (count($selects) > 1) {
$this->buildingGlobalOrder = true;
$query = qsprintf(
$conn_r,
'%Q %Q %Q',
implode(' UNION DISTINCT ', $selects),
$this->buildOrderClause($conn_r),
$this->buildLimitClause($conn_r));
} else {
$query = head($selects);
}
return queryfx_all($conn_r, '%Q', $query);
}
private function buildSelectStatement(AphrontDatabaseConnection $conn_r) {
$table = new DifferentialRevision();
$select = $this->buildSelectClause($conn_r);
$from = qsprintf(
$conn_r,
'FROM %T r',
$table->getTableName());
$joins = $this->buildJoinsClause($conn_r);
$where = $this->buildWhereClause($conn_r);
$group_by = $this->buildGroupByClause($conn_r);
$having = $this->buildHavingClause($conn_r);
$this->buildingGlobalOrder = false;
$order_by = $this->buildOrderClause($conn_r);
$limit = $this->buildLimitClause($conn_r);
return qsprintf(
$conn_r,
'(%Q %Q %Q %Q %Q %Q %Q %Q)',
$select,
$from,
$joins,
$where,
$group_by,
$having,
$order_by,
$limit);
}
/* -( Internals )---------------------------------------------------------- */
/**
* @task internal
*/
private function buildJoinsClause($conn_r) {
$joins = array();
if ($this->pathIDs) {
$path_table = new DifferentialAffectedPath();
$joins[] = qsprintf(
$conn_r,
'JOIN %T p ON p.revisionID = r.id',
$path_table->getTableName());
}
if ($this->commitHashes) {
$joins[] = qsprintf(
$conn_r,
'JOIN %T hash_rel ON hash_rel.revisionID = r.id',
ArcanistDifferentialRevisionHash::TABLE_NAME);
}
if ($this->ccs) {
$joins[] = qsprintf(
$conn_r,
'JOIN %T e_ccs ON e_ccs.src = r.phid '.
'AND e_ccs.type = %s '.
'AND e_ccs.dst in (%Ls)',
PhabricatorEdgeConfig::TABLE_NAME_EDGE,
PhabricatorObjectHasSubscriberEdgeType::EDGECONST,
$this->ccs);
}
if ($this->reviewers) {
$joins[] = qsprintf(
$conn_r,
'JOIN %T e_reviewers ON e_reviewers.src = r.phid '.
'AND e_reviewers.type = %s '.
'AND e_reviewers.dst in (%Ls)',
PhabricatorEdgeConfig::TABLE_NAME_EDGE,
DifferentialRevisionHasReviewerEdgeType::EDGECONST,
$this->reviewers);
}
if ($this->draftAuthors) {
$differential_draft = new DifferentialDraft();
$joins[] = qsprintf(
$conn_r,
'JOIN %T has_draft ON has_draft.objectPHID = r.phid '.
'AND has_draft.authorPHID IN (%Ls)',
$differential_draft->getTableName(),
$this->draftAuthors);
}
if ($this->commitPHIDs) {
$joins[] = qsprintf(
$conn_r,
'JOIN %T commits ON commits.revisionID = r.id',
DifferentialRevision::TABLE_COMMIT);
}
$joins[] = $this->buildJoinClauseParts($conn_r);
return $this->formatJoinClause($joins);
}
/**
* @task internal
*/
protected function buildWhereClause(AphrontDatabaseConnection $conn_r) {
$where = array();
if ($this->pathIDs) {
$path_clauses = array();
$repo_info = igroup($this->pathIDs, 'repositoryID');
foreach ($repo_info as $repository_id => $paths) {
$path_clauses[] = qsprintf(
$conn_r,
'(p.repositoryID = %d AND p.pathID IN (%Ld))',
$repository_id,
ipull($paths, 'pathID'));
}
$path_clauses = '('.implode(' OR ', $path_clauses).')';
$where[] = $path_clauses;
}
if ($this->authors) {
$where[] = qsprintf(
$conn_r,
'r.authorPHID IN (%Ls)',
$this->authors);
}
if ($this->revIDs) {
$where[] = qsprintf(
$conn_r,
'r.id IN (%Ld)',
$this->revIDs);
}
if ($this->repositoryPHIDs) {
$where[] = qsprintf(
$conn_r,
'r.repositoryPHID IN (%Ls)',
$this->repositoryPHIDs);
}
if ($this->commitHashes) {
$hash_clauses = array();
foreach ($this->commitHashes as $info) {
list($type, $hash) = $info;
$hash_clauses[] = qsprintf(
$conn_r,
'(hash_rel.type = %s AND hash_rel.hash = %s)',
$type,
$hash);
}
$hash_clauses = '('.implode(' OR ', $hash_clauses).')';
$where[] = $hash_clauses;
}
if ($this->commitPHIDs) {
$where[] = qsprintf(
$conn_r,
'commits.commitPHID IN (%Ls)',
$this->commitPHIDs);
}
if ($this->phids) {
$where[] = qsprintf(
$conn_r,
'r.phid IN (%Ls)',
$this->phids);
}
if ($this->branches) {
$where[] = qsprintf(
$conn_r,
'r.branchName in (%Ls)',
$this->branches);
}
if ($this->updatedEpochMin !== null) {
$where[] = qsprintf(
$conn_r,
'r.dateModified >= %d',
$this->updatedEpochMin);
}
if ($this->updatedEpochMax !== null) {
$where[] = qsprintf(
$conn_r,
'r.dateModified <= %d',
$this->updatedEpochMax);
}
switch ($this->status) {
case self::STATUS_ANY:
break;
case self::STATUS_OPEN:
$where[] = qsprintf(
$conn_r,
'r.status IN (%Ld)',
DifferentialRevisionStatus::getOpenStatuses());
break;
case self::STATUS_NEEDS_REVIEW:
$where[] = qsprintf(
$conn_r,
'r.status IN (%Ld)',
array(
ArcanistDifferentialRevisionStatus::NEEDS_REVIEW,
));
break;
case self::STATUS_NEEDS_REVISION:
$where[] = qsprintf(
$conn_r,
'r.status IN (%Ld)',
array(
ArcanistDifferentialRevisionStatus::NEEDS_REVISION,
));
break;
case self::STATUS_ACCEPTED:
$where[] = qsprintf(
$conn_r,
'r.status IN (%Ld)',
array(
ArcanistDifferentialRevisionStatus::ACCEPTED,
));
break;
case self::STATUS_CLOSED:
$where[] = qsprintf(
$conn_r,
'r.status IN (%Ld)',
DifferentialRevisionStatus::getClosedStatuses());
break;
case self::STATUS_ABANDONED:
$where[] = qsprintf(
$conn_r,
'r.status IN (%Ld)',
array(
ArcanistDifferentialRevisionStatus::ABANDONED,
));
break;
default:
throw new Exception(
- "Unknown revision status filter constant '{$this->status}'!");
+ pht("Unknown revision status filter constant '%s'!", $this->status));
}
$where[] = $this->buildWhereClauseParts($conn_r);
return $this->formatWhereClause($where);
}
/**
* @task internal
*/
private function buildGroupByClause($conn_r) {
$join_triggers = array_merge(
$this->pathIDs,
$this->ccs,
$this->reviewers);
$needs_distinct = (count($join_triggers) > 1);
if ($needs_distinct) {
return 'GROUP BY r.id';
} else {
return '';
}
}
protected function getDefaultOrderVector() {
return array('updated', 'id');
}
public function getOrderableColumns() {
$primary = ($this->buildingGlobalOrder ? null : 'r');
return array(
'id' => array(
'table' => $primary,
'column' => 'id',
'type' => 'int',
'unique' => true,
),
'updated' => array(
'table' => $primary,
'column' => 'dateModified',
'type' => 'int',
),
);
}
protected function getPagingValueMap($cursor, array $keys) {
$revision = $this->loadCursorObject($cursor);
return array(
'id' => $revision->getID(),
'updated' => $revision->getDateModified(),
);
}
private function loadRelationships($conn_r, array $revisions) {
assert_instances_of($revisions, 'DifferentialRevision');
$type_reviewer = DifferentialRevisionHasReviewerEdgeType::EDGECONST;
$type_subscriber = PhabricatorObjectHasSubscriberEdgeType::EDGECONST;
$edges = id(new PhabricatorEdgeQuery())
->withSourcePHIDs(mpull($revisions, 'getPHID'))
->withEdgeTypes(array($type_reviewer, $type_subscriber))
->setOrder(PhabricatorEdgeQuery::ORDER_OLDEST_FIRST)
->execute();
$type_map = array(
DifferentialRevision::RELATION_REVIEWER => $type_reviewer,
DifferentialRevision::RELATION_SUBSCRIBED => $type_subscriber,
);
foreach ($revisions as $revision) {
$data = array();
foreach ($type_map as $rel_type => $edge_type) {
$revision_edges = $edges[$revision->getPHID()][$edge_type];
foreach ($revision_edges as $dst_phid => $edge_data) {
$data[] = array(
'relation' => $rel_type,
'objectPHID' => $dst_phid,
'reasonPHID' => null,
);
}
}
$revision->attachRelationships($data);
}
}
private function loadCommitPHIDs($conn_r, array $revisions) {
assert_instances_of($revisions, 'DifferentialRevision');
$commit_phids = queryfx_all(
$conn_r,
'SELECT * FROM %T WHERE revisionID IN (%Ld)',
DifferentialRevision::TABLE_COMMIT,
mpull($revisions, 'getID'));
$commit_phids = igroup($commit_phids, 'revisionID');
foreach ($revisions as $revision) {
$phids = idx($commit_phids, $revision->getID(), array());
$phids = ipull($phids, 'commitPHID');
$revision->attachCommitPHIDs($phids);
}
}
private function loadDiffIDs($conn_r, array $revisions) {
assert_instances_of($revisions, 'DifferentialRevision');
$diff_table = new DifferentialDiff();
$diff_ids = queryfx_all(
$conn_r,
'SELECT revisionID, id FROM %T WHERE revisionID IN (%Ld)
ORDER BY id DESC',
$diff_table->getTableName(),
mpull($revisions, 'getID'));
$diff_ids = igroup($diff_ids, 'revisionID');
foreach ($revisions as $revision) {
$ids = idx($diff_ids, $revision->getID(), array());
$ids = ipull($ids, 'id');
$revision->attachDiffIDs($ids);
}
}
private function loadActiveDiffs($conn_r, array $revisions) {
assert_instances_of($revisions, 'DifferentialRevision');
$diff_table = new DifferentialDiff();
$load_ids = array();
foreach ($revisions as $revision) {
$diffs = $revision->getDiffIDs();
if ($diffs) {
$load_ids[] = max($diffs);
}
}
$active_diffs = array();
if ($load_ids) {
$active_diffs = $diff_table->loadAllWhere(
'id IN (%Ld)',
$load_ids);
}
$active_diffs = mpull($active_diffs, null, 'getRevisionID');
foreach ($revisions as $revision) {
$revision->attachActiveDiff(idx($active_diffs, $revision->getID()));
}
}
private function loadHashes(
AphrontDatabaseConnection $conn_r,
array $revisions) {
assert_instances_of($revisions, 'DifferentialRevision');
$data = queryfx_all(
$conn_r,
'SELECT * FROM %T WHERE revisionID IN (%Ld)',
'differential_revisionhash',
mpull($revisions, 'getID'));
$data = igroup($data, 'revisionID');
foreach ($revisions as $revision) {
$hashes = idx($data, $revision->getID(), array());
$list = array();
foreach ($hashes as $hash) {
$list[] = array($hash['type'], $hash['hash']);
}
$revision->attachHashes($list);
}
}
private function loadReviewers(
AphrontDatabaseConnection $conn_r,
array $revisions) {
assert_instances_of($revisions, 'DifferentialRevision');
$edge_type = DifferentialRevisionHasReviewerEdgeType::EDGECONST;
$edges = id(new PhabricatorEdgeQuery())
->withSourcePHIDs(mpull($revisions, 'getPHID'))
->withEdgeTypes(array($edge_type))
->needEdgeData(true)
->setOrder(PhabricatorEdgeQuery::ORDER_OLDEST_FIRST)
->execute();
$viewer = $this->getViewer();
$viewer_phid = $viewer->getPHID();
$allow_key = 'differential.allow-self-accept';
$allow_self = PhabricatorEnv::getEnvConfig($allow_key);
// Figure out which of these reviewers the viewer has authority to act as.
if ($this->needReviewerAuthority && $viewer_phid) {
$authority = $this->loadReviewerAuthority(
$revisions,
$edges,
$allow_self);
}
foreach ($revisions as $revision) {
$revision_edges = $edges[$revision->getPHID()][$edge_type];
$reviewers = array();
foreach ($revision_edges as $reviewer_phid => $edge) {
$reviewer = new DifferentialReviewer($reviewer_phid, $edge['data']);
if ($this->needReviewerAuthority) {
if (!$viewer_phid) {
// Logged-out users never have authority.
$has_authority = false;
} else if ((!$allow_self) &&
($revision->getAuthorPHID() == $viewer_phid)) {
// The author can never have authority unless we allow self-accept.
$has_authority = false;
} else {
- // Otherwise, look up whether th viewer has authority.
+ // Otherwise, look up whether the viewer has authority.
$has_authority = isset($authority[$reviewer_phid]);
}
$reviewer->attachAuthority($viewer, $has_authority);
}
$reviewers[$reviewer_phid] = $reviewer;
}
$revision->attachReviewerStatus($reviewers);
}
}
public static function splitResponsible(array $revisions, array $user_phids) {
$blocking = array();
$active = array();
$waiting = array();
$status_review = ArcanistDifferentialRevisionStatus::NEEDS_REVIEW;
// Bucket revisions into $blocking (revisions where you are blocking
// others), $active (revisions you need to do something about) and $waiting
// (revisions you're waiting on someone else to do something about).
foreach ($revisions as $revision) {
$needs_review = ($revision->getStatus() == $status_review);
$filter_is_author = in_array($revision->getAuthorPHID(), $user_phids);
if (!$revision->getReviewers()) {
$needs_review = false;
$author_is_reviewer = false;
} else {
$author_is_reviewer = in_array(
$revision->getAuthorPHID(),
$revision->getReviewers());
}
// If exactly one of "needs review" and "the user is the author" is
// true, the user needs to act on it. Otherwise, they're waiting on
// it.
if ($needs_review ^ $filter_is_author) {
if ($needs_review) {
array_unshift($blocking, $revision);
} else {
$active[] = $revision;
}
// User is author **and** reviewer. An exotic but configurable workflow.
// User needs to act on it double.
} else if ($needs_review && $author_is_reviewer) {
array_unshift($blocking, $revision);
$active[] = $revision;
} else {
$waiting[] = $revision;
}
}
return array($blocking, $active, $waiting);
}
private function loadReviewerAuthority(
array $revisions,
array $edges,
$allow_self) {
$revision_map = mpull($revisions, null, 'getPHID');
$viewer_phid = $this->getViewer()->getPHID();
// Find all the project reviewers which the user may have authority over.
$project_phids = array();
$project_type = PhabricatorProjectProjectPHIDType::TYPECONST;
$edge_type = DifferentialRevisionHasReviewerEdgeType::EDGECONST;
foreach ($edges as $src => $types) {
if (!$allow_self) {
if ($revision_map[$src]->getAuthorPHID() == $viewer_phid) {
// If self-review isn't permitted, the user will never have
// authority over projects on revisions they authored because you
// can't accept your own revisions, so we don't need to load any
// data about these reviewers.
continue;
}
}
$edge_data = idx($types, $edge_type, array());
foreach ($edge_data as $dst => $data) {
if (phid_get_type($dst) == $project_type) {
$project_phids[] = $dst;
}
}
}
// Now, figure out which of these projects the viewer is actually a
// member of.
$project_authority = array();
if ($project_phids) {
$project_authority = id(new PhabricatorProjectQuery())
->setViewer($this->getViewer())
->withPHIDs($project_phids)
->withMemberPHIDs(array($viewer_phid))
->execute();
$project_authority = mpull($project_authority, 'getPHID');
}
// Finally, the viewer has authority over themselves.
return array(
$viewer_phid => true,
) + array_fuse($project_authority);
}
public function getQueryApplicationClass() {
return 'PhabricatorDifferentialApplication';
}
protected function getPrimaryTableAlias() {
return 'r';
}
}
diff --git a/src/applications/differential/render/DifferentialChangesetHTMLRenderer.php b/src/applications/differential/render/DifferentialChangesetHTMLRenderer.php
index ce1a35f36..30acc71c8 100644
--- a/src/applications/differential/render/DifferentialChangesetHTMLRenderer.php
+++ b/src/applications/differential/render/DifferentialChangesetHTMLRenderer.php
@@ -1,616 +1,619 @@
<?php
abstract class DifferentialChangesetHTMLRenderer
extends DifferentialChangesetRenderer {
public static function getHTMLRendererByKey($key) {
switch ($key) {
case '1up':
return new DifferentialChangesetOneUpRenderer();
case '2up':
default:
return new DifferentialChangesetTwoUpRenderer();
}
throw new Exception(pht('Unknown HTML renderer "%s"!', $key));
}
abstract protected function getRendererTableClass();
abstract public function getRowScaffoldForInline(
PHUIDiffInlineCommentView $view);
protected function renderChangeTypeHeader($force) {
$changeset = $this->getChangeset();
$change = $changeset->getChangeType();
$file = $changeset->getFileType();
$messages = array();
switch ($change) {
case DifferentialChangeType::TYPE_ADD:
switch ($file) {
case DifferentialChangeType::FILE_TEXT:
$messages[] = pht('This file was added.');
break;
case DifferentialChangeType::FILE_IMAGE:
$messages[] = pht('This image was added.');
break;
case DifferentialChangeType::FILE_DIRECTORY:
$messages[] = pht('This directory was added.');
break;
case DifferentialChangeType::FILE_BINARY:
$messages[] = pht('This binary file was added.');
break;
case DifferentialChangeType::FILE_SYMLINK:
$messages[] = pht('This symlink was added.');
break;
case DifferentialChangeType::FILE_SUBMODULE:
$messages[] = pht('This submodule was added.');
break;
}
break;
case DifferentialChangeType::TYPE_DELETE:
switch ($file) {
case DifferentialChangeType::FILE_TEXT:
$messages[] = pht('This file was deleted.');
break;
case DifferentialChangeType::FILE_IMAGE:
$messages[] = pht('This image was deleted.');
break;
case DifferentialChangeType::FILE_DIRECTORY:
$messages[] = pht('This directory was deleted.');
break;
case DifferentialChangeType::FILE_BINARY:
$messages[] = pht('This binary file was deleted.');
break;
case DifferentialChangeType::FILE_SYMLINK:
$messages[] = pht('This symlink was deleted.');
break;
case DifferentialChangeType::FILE_SUBMODULE:
$messages[] = pht('This submodule was deleted.');
break;
}
break;
case DifferentialChangeType::TYPE_MOVE_HERE:
$from = phutil_tag('strong', array(), $changeset->getOldFile());
switch ($file) {
case DifferentialChangeType::FILE_TEXT:
$messages[] = pht('This file was moved from %s.', $from);
break;
case DifferentialChangeType::FILE_IMAGE:
$messages[] = pht('This image was moved from %s.', $from);
break;
case DifferentialChangeType::FILE_DIRECTORY:
$messages[] = pht('This directory was moved from %s.', $from);
break;
case DifferentialChangeType::FILE_BINARY:
$messages[] = pht('This binary file was moved from %s.', $from);
break;
case DifferentialChangeType::FILE_SYMLINK:
$messages[] = pht('This symlink was moved from %s.', $from);
break;
case DifferentialChangeType::FILE_SUBMODULE:
$messages[] = pht('This submodule was moved from %s.', $from);
break;
}
break;
case DifferentialChangeType::TYPE_COPY_HERE:
$from = phutil_tag('strong', array(), $changeset->getOldFile());
switch ($file) {
case DifferentialChangeType::FILE_TEXT:
$messages[] = pht('This file was copied from %s.', $from);
break;
case DifferentialChangeType::FILE_IMAGE:
$messages[] = pht('This image was copied from %s.', $from);
break;
case DifferentialChangeType::FILE_DIRECTORY:
$messages[] = pht('This directory was copied from %s.', $from);
break;
case DifferentialChangeType::FILE_BINARY:
$messages[] = pht('This binary file was copied from %s.', $from);
break;
case DifferentialChangeType::FILE_SYMLINK:
$messages[] = pht('This symlink was copied from %s.', $from);
break;
case DifferentialChangeType::FILE_SUBMODULE:
$messages[] = pht('This submodule was copied from %s.', $from);
break;
}
break;
case DifferentialChangeType::TYPE_MOVE_AWAY:
$paths = phutil_tag(
'strong',
array(),
implode(', ', $changeset->getAwayPaths()));
switch ($file) {
case DifferentialChangeType::FILE_TEXT:
$messages[] = pht('This file was moved to %s.', $paths);
break;
case DifferentialChangeType::FILE_IMAGE:
$messages[] = pht('This image was moved to %s.', $paths);
break;
case DifferentialChangeType::FILE_DIRECTORY:
$messages[] = pht('This directory was moved to %s.', $paths);
break;
case DifferentialChangeType::FILE_BINARY:
$messages[] = pht('This binary file was moved to %s.', $paths);
break;
case DifferentialChangeType::FILE_SYMLINK:
$messages[] = pht('This symlink was moved to %s.', $paths);
break;
case DifferentialChangeType::FILE_SUBMODULE:
$messages[] = pht('This submodule was moved to %s.', $paths);
break;
}
break;
case DifferentialChangeType::TYPE_COPY_AWAY:
$paths = phutil_tag(
'strong',
array(),
implode(', ', $changeset->getAwayPaths()));
switch ($file) {
case DifferentialChangeType::FILE_TEXT:
$messages[] = pht('This file was copied to %s.', $paths);
break;
case DifferentialChangeType::FILE_IMAGE:
$messages[] = pht('This image was copied to %s.', $paths);
break;
case DifferentialChangeType::FILE_DIRECTORY:
$messages[] = pht('This directory was copied to %s.', $paths);
break;
case DifferentialChangeType::FILE_BINARY:
$messages[] = pht('This binary file was copied to %s.', $paths);
break;
case DifferentialChangeType::FILE_SYMLINK:
$messages[] = pht('This symlink was copied to %s.', $paths);
break;
case DifferentialChangeType::FILE_SUBMODULE:
$messages[] = pht('This submodule was copied to %s.', $paths);
break;
}
break;
case DifferentialChangeType::TYPE_MULTICOPY:
$paths = phutil_tag(
'strong',
array(),
implode(', ', $changeset->getAwayPaths()));
switch ($file) {
case DifferentialChangeType::FILE_TEXT:
$messages[] = pht(
'This file was deleted after being copied to %s.',
$paths);
break;
case DifferentialChangeType::FILE_IMAGE:
$messages[] = pht(
'This image was deleted after being copied to %s.',
$paths);
break;
case DifferentialChangeType::FILE_DIRECTORY:
$messages[] = pht(
'This directory was deleted after being copied to %s.',
$paths);
break;
case DifferentialChangeType::FILE_BINARY:
$messages[] = pht(
'This binary file was deleted after being copied to %s.',
$paths);
break;
case DifferentialChangeType::FILE_SYMLINK:
$messages[] = pht(
'This symlink was deleted after being copied to %s.',
$paths);
break;
case DifferentialChangeType::FILE_SUBMODULE:
$messages[] = pht(
'This submodule was deleted after being copied to %s.',
$paths);
break;
}
break;
default:
switch ($file) {
case DifferentialChangeType::FILE_TEXT:
// This is the default case, so we only render this header if
// forced to since it's not very useful.
if ($force) {
$messages[] = pht('This file was not modified.');
}
break;
case DifferentialChangeType::FILE_IMAGE:
$messages[] = pht('This is an image.');
break;
case DifferentialChangeType::FILE_DIRECTORY:
$messages[] = pht('This is a directory.');
break;
case DifferentialChangeType::FILE_BINARY:
$messages[] = pht('This is a binary file.');
break;
case DifferentialChangeType::FILE_SYMLINK:
$messages[] = pht('This is a symlink.');
break;
case DifferentialChangeType::FILE_SUBMODULE:
$messages[] = pht('This is a submodule.');
break;
}
break;
}
return $this->formatHeaderMessages($messages);
}
protected function renderUndershieldHeader() {
$messages = array();
$changeset = $this->getChangeset();
$file = $changeset->getFileType();
// If this is a text file with at least one hunk, we may have converted
// the text encoding. In this case, show a note.
$show_encoding = ($file == DifferentialChangeType::FILE_TEXT) &&
($changeset->getHunks());
if ($show_encoding) {
$encoding = $this->getOriginalCharacterEncoding();
if ($encoding != 'utf8') {
if ($encoding) {
$messages[] = pht(
'This file was converted from %s for display.',
phutil_tag('strong', array(), $encoding));
} else {
- $messages[] = pht(
- 'This file uses an unknown character encoding.');
+ $messages[] = pht('This file uses an unknown character encoding.');
}
}
}
if ($this->getHighlightingDisabled()) {
$messages[] = pht(
'This file is larger than %s, so syntax highlighting is '.
'disabled by default.',
phutil_format_bytes(DifferentialChangesetParser::HIGHLIGHT_BYTE_LIMIT));
}
return $this->formatHeaderMessages($messages);
}
private function formatHeaderMessages(array $messages) {
if (!$messages) {
return null;
}
foreach ($messages as $key => $message) {
$messages[$key] = phutil_tag('li', array(), $message);
}
return phutil_tag(
'ul',
array(
'class' => 'differential-meta-notice',
),
$messages);
}
protected function renderPropertyChangeHeader() {
$changeset = $this->getChangeset();
list($old, $new) = $this->getChangesetProperties($changeset);
// If we don't have any property changes, don't render this table.
if ($old === $new) {
return null;
}
$keys = array_keys($old + $new);
sort($keys);
$key_map = array(
'unix:filemode' => pht('File Mode'),
'file:dimensions' => pht('Image Dimensions'),
'file:mimetype' => pht('MIME Type'),
'file:size' => pht('File Size'),
);
$rows = array();
foreach ($keys as $key) {
$oval = idx($old, $key);
$nval = idx($new, $key);
if ($oval !== $nval) {
if ($oval === null) {
$oval = phutil_tag('em', array(), 'null');
} else {
$oval = phutil_escape_html_newlines($oval);
}
if ($nval === null) {
$nval = phutil_tag('em', array(), 'null');
} else {
$nval = phutil_escape_html_newlines($nval);
}
$readable_key = idx($key_map, $key, $key);
$row = array(
$readable_key,
$oval,
$nval,
);
$rows[] = $row;
}
}
$classes = array('', 'oval', 'nval');
$headers = array(
pht('Property'),
pht('Old Value'),
pht('New Value'),
);
$table = id(new AphrontTableView($rows))
->setHeaders($headers)
->setColumnClasses($classes);
return phutil_tag(
'div',
array(
'class' => 'differential-property-table',
),
$table);
}
public function renderShield($message, $force = 'default') {
$end = count($this->getOldLines());
$reference = $this->getRenderingReference();
if ($force !== 'text' &&
$force !== 'whitespace' &&
$force !== 'none' &&
$force !== 'default') {
- throw new Exception("Invalid 'force' parameter '{$force}'!");
+ throw new Exception(
+ pht(
+ "Invalid '%s' parameter '%s'!",
+ 'force',
+ $force));
}
$range = "0-{$end}";
if ($force == 'text') {
// If we're forcing text, force the whole file to be rendered.
$range = "{$range}/0-{$end}";
}
$meta = array(
'ref' => $reference,
'range' => $range,
);
if ($force == 'whitespace') {
$meta['whitespace'] = DifferentialChangesetParser::WHITESPACE_SHOW_ALL;
}
$content = array();
$content[] = $message;
if ($force !== 'none') {
$content[] = ' ';
$content[] = javelin_tag(
'a',
array(
'mustcapture' => true,
'sigil' => 'show-more',
'class' => 'complete',
'href' => '#',
'meta' => $meta,
),
pht('Show File Contents'));
}
return $this->wrapChangeInTable(
javelin_tag(
'tr',
array(
'sigil' => 'context-target',
),
phutil_tag(
'td',
array(
'class' => 'differential-shield',
'colspan' => 6,
),
$content)));
}
abstract protected function renderColgroup();
protected function wrapChangeInTable($content) {
if (!$content) {
return null;
}
$classes = array();
$classes[] = 'differential-diff';
$classes[] = 'remarkup-code';
$classes[] = 'PhabricatorMonospaced';
$classes[] = $this->getRendererTableClass();
return javelin_tag(
'table',
array(
'class' => implode(' ', $classes),
'sigil' => 'differential-diff',
),
array(
$this->renderColgroup(),
$content,
));
}
protected function buildInlineComment(
PhabricatorInlineCommentInterface $comment,
$on_right = false) {
$user = $this->getUser();
$edit = $user &&
($comment->getAuthorPHID() == $user->getPHID()) &&
($comment->isDraft())
&& $this->getShowEditAndReplyLinks();
$allow_reply = (bool)$user && $this->getShowEditAndReplyLinks();
$allow_done = !$comment->isDraft() && $this->getCanMarkDone();
return id(new PHUIDiffInlineCommentDetailView())
->setUser($user)
->setInlineComment($comment)
->setIsOnRight($on_right)
->setHandles($this->getHandles())
->setMarkupEngine($this->getMarkupEngine())
->setEditable($edit)
->setAllowReply($allow_reply)
->setCanMarkDone($allow_done)
->setObjectOwnerPHID($this->getObjectOwnerPHID());
}
/**
* Build links which users can click to show more context in a changeset.
*
* @param int Beginning of the line range to build links for.
* @param int Length of the line range to build links for.
* @param int Total number of lines in the changeset.
* @return markup Rendered links.
*/
protected function renderShowContextLinks($top, $len, $changeset_length) {
$block_size = 20;
$end = ($top + $len) - $block_size;
// If this is a large block, such that the "top" and "bottom" ranges are
// non-overlapping, we'll provide options to show the top, bottom or entire
// block. For smaller blocks, we only provide an option to show the entire
// block, since it would be silly to show the bottom 20 lines of a 25-line
// block.
$is_large_block = ($len > ($block_size * 2));
$links = array();
if ($is_large_block) {
$is_first_block = ($top == 0);
if ($is_first_block) {
$text = pht('Show First %d Line(s)', $block_size);
} else {
$text = pht("\xE2\x96\xB2 Show %d Line(s)", $block_size);
}
$links[] = $this->renderShowContextLink(
false,
"{$top}-{$len}/{$top}-20",
$text);
}
$links[] = $this->renderShowContextLink(
true,
"{$top}-{$len}/{$top}-{$len}",
pht('Show All %d Line(s)', $len));
if ($is_large_block) {
$is_last_block = (($top + $len) >= $changeset_length);
if ($is_last_block) {
$text = pht('Show Last %d Line(s)', $block_size);
} else {
- $text = pht("\xE2\x96\xBC Show %d Line(s)", $block_size);
+ $text = "\xE2\x96\xBC ".pht('Show %d Line(s)', $block_size);
}
$links[] = $this->renderShowContextLink(
false,
"{$top}-{$len}/{$end}-20",
$text);
}
return phutil_implode_html(" \xE2\x80\xA2 ", $links);
}
/**
* Build a link that shows more context in a changeset.
*
* See @{method:renderShowContextLinks}.
*
* @param bool Does this link show all context when clicked?
* @param string Range specification for lines to show.
* @param string Text of the link.
* @return markup Rendered link.
*/
private function renderShowContextLink($is_all, $range, $text) {
$reference = $this->getRenderingReference();
return javelin_tag(
'a',
array(
'href' => '#',
'mustcapture' => true,
'sigil' => 'show-more',
'meta' => array(
'type' => ($is_all ? 'all' : null),
'range' => $range,
),
),
$text);
}
/**
* Build the prefixes for line IDs used to track inline comments.
*
* @return pair<wild, wild> Left and right prefixes.
*/
protected function getLineIDPrefixes() {
// These look like "C123NL45", which means the line is line 45 on the
// "new" side of the file in changeset 123.
// The "C" stands for "changeset", and is followed by a changeset ID.
// "N" stands for "new" and means the comment should attach to the new file
// when stored. "O" stands for "old" and means the comment should attach to
// the old file. These are important because either the old or new part
// of a file may appear on the left or right side of the diff in the
// diff-of-diffs view.
// The "L" stands for "line" and is followed by the line number.
if ($this->getOldChangesetID()) {
$left_prefix = array();
$left_prefix[] = 'C';
$left_prefix[] = $this->getOldChangesetID();
$left_prefix[] = $this->getOldAttachesToNewFile() ? 'N' : 'O';
$left_prefix[] = 'L';
$left_prefix = implode('', $left_prefix);
} else {
$left_prefix = null;
}
if ($this->getNewChangesetID()) {
$right_prefix = array();
$right_prefix[] = 'C';
$right_prefix[] = $this->getNewChangesetID();
$right_prefix[] = $this->getNewAttachesToNewFile() ? 'N' : 'O';
$right_prefix[] = 'L';
$right_prefix = implode('', $right_prefix);
} else {
$right_prefix = null;
}
return array($left_prefix, $right_prefix);
}
protected function renderImageStage(PhabricatorFile $file) {
return phutil_tag(
'div',
array(
'class' => 'differential-image-stage',
),
phutil_tag(
'img',
array(
'src' => $file->getBestURI(),
)));
}
}
diff --git a/src/applications/differential/render/DifferentialChangesetRenderer.php b/src/applications/differential/render/DifferentialChangesetRenderer.php
index 22255ece7..461ce668b 100644
--- a/src/applications/differential/render/DifferentialChangesetRenderer.php
+++ b/src/applications/differential/render/DifferentialChangesetRenderer.php
@@ -1,667 +1,667 @@
<?php
abstract class DifferentialChangesetRenderer {
private $user;
private $changeset;
private $renderingReference;
private $renderPropertyChangeHeader;
private $isTopLevel;
private $isUndershield;
private $hunkStartLines;
private $oldLines;
private $newLines;
private $oldComments;
private $newComments;
private $oldChangesetID;
private $newChangesetID;
private $oldAttachesToNewFile;
private $newAttachesToNewFile;
private $highlightOld = array();
private $highlightNew = array();
private $codeCoverage;
private $handles;
private $markupEngine;
private $oldRender;
private $newRender;
private $originalOld;
private $originalNew;
private $gaps;
private $mask;
private $depths;
private $originalCharacterEncoding;
private $showEditAndReplyLinks;
private $canMarkDone;
private $objectOwnerPHID;
private $highlightingDisabled;
private $oldFile = false;
private $newFile = false;
abstract public function getRendererKey();
public function setShowEditAndReplyLinks($bool) {
$this->showEditAndReplyLinks = $bool;
return $this;
}
public function getShowEditAndReplyLinks() {
return $this->showEditAndReplyLinks;
}
public function setHighlightingDisabled($highlighting_disabled) {
$this->highlightingDisabled = $highlighting_disabled;
return $this;
}
public function getHighlightingDisabled() {
return $this->highlightingDisabled;
}
public function setOriginalCharacterEncoding($original_character_encoding) {
$this->originalCharacterEncoding = $original_character_encoding;
return $this;
}
public function getOriginalCharacterEncoding() {
return $this->originalCharacterEncoding;
}
public function setIsUndershield($is_undershield) {
$this->isUndershield = $is_undershield;
return $this;
}
public function getIsUndershield() {
return $this->isUndershield;
}
public function setDepths($depths) {
$this->depths = $depths;
return $this;
}
protected function getDepths() {
return $this->depths;
}
public function setMask($mask) {
$this->mask = $mask;
return $this;
}
protected function getMask() {
return $this->mask;
}
public function setGaps($gaps) {
$this->gaps = $gaps;
return $this;
}
protected function getGaps() {
return $this->gaps;
}
public function attachOldFile(PhabricatorFile $old = null) {
$this->oldFile = $old;
return $this;
}
public function getOldFile() {
if ($this->oldFile === false) {
throw new PhabricatorDataNotAttachedException($this);
}
return $this->oldFile;
}
public function hasOldFile() {
return (bool)$this->oldFile;
}
public function attachNewFile(PhabricatorFile $new = null) {
$this->newFile = $new;
return $this;
}
public function getNewFile() {
if ($this->newFile === false) {
throw new PhabricatorDataNotAttachedException($this);
}
return $this->newFile;
}
public function hasNewFile() {
return (bool)$this->newFile;
}
public function setOriginalNew($original_new) {
$this->originalNew = $original_new;
return $this;
}
protected function getOriginalNew() {
return $this->originalNew;
}
public function setOriginalOld($original_old) {
$this->originalOld = $original_old;
return $this;
}
protected function getOriginalOld() {
return $this->originalOld;
}
public function setNewRender($new_render) {
$this->newRender = $new_render;
return $this;
}
protected function getNewRender() {
return $this->newRender;
}
public function setOldRender($old_render) {
$this->oldRender = $old_render;
return $this;
}
protected function getOldRender() {
return $this->oldRender;
}
public function setMarkupEngine(PhabricatorMarkupEngine $markup_engine) {
$this->markupEngine = $markup_engine;
return $this;
}
public function getMarkupEngine() {
return $this->markupEngine;
}
public function setHandles(array $handles) {
assert_instances_of($handles, 'PhabricatorObjectHandle');
$this->handles = $handles;
return $this;
}
protected function getHandles() {
return $this->handles;
}
public function setCodeCoverage($code_coverage) {
$this->codeCoverage = $code_coverage;
return $this;
}
protected function getCodeCoverage() {
return $this->codeCoverage;
}
public function setHighlightNew($highlight_new) {
$this->highlightNew = $highlight_new;
return $this;
}
protected function getHighlightNew() {
return $this->highlightNew;
}
public function setHighlightOld($highlight_old) {
$this->highlightOld = $highlight_old;
return $this;
}
protected function getHighlightOld() {
return $this->highlightOld;
}
public function setNewAttachesToNewFile($attaches) {
$this->newAttachesToNewFile = $attaches;
return $this;
}
protected function getNewAttachesToNewFile() {
return $this->newAttachesToNewFile;
}
public function setOldAttachesToNewFile($attaches) {
$this->oldAttachesToNewFile = $attaches;
return $this;
}
protected function getOldAttachesToNewFile() {
return $this->oldAttachesToNewFile;
}
public function setNewChangesetID($new_changeset_id) {
$this->newChangesetID = $new_changeset_id;
return $this;
}
protected function getNewChangesetID() {
return $this->newChangesetID;
}
public function setOldChangesetID($old_changeset_id) {
$this->oldChangesetID = $old_changeset_id;
return $this;
}
protected function getOldChangesetID() {
return $this->oldChangesetID;
}
public function setNewComments(array $new_comments) {
foreach ($new_comments as $line_number => $comments) {
assert_instances_of($comments, 'PhabricatorInlineCommentInterface');
}
$this->newComments = $new_comments;
return $this;
}
protected function getNewComments() {
return $this->newComments;
}
public function setOldComments(array $old_comments) {
foreach ($old_comments as $line_number => $comments) {
assert_instances_of($comments, 'PhabricatorInlineCommentInterface');
}
$this->oldComments = $old_comments;
return $this;
}
protected function getOldComments() {
return $this->oldComments;
}
public function setNewLines(array $new_lines) {
$this->newLines = $new_lines;
return $this;
}
protected function getNewLines() {
return $this->newLines;
}
public function setOldLines(array $old_lines) {
$this->oldLines = $old_lines;
return $this;
}
protected function getOldLines() {
return $this->oldLines;
}
public function setHunkStartLines(array $hunk_start_lines) {
$this->hunkStartLines = $hunk_start_lines;
return $this;
}
protected function getHunkStartLines() {
return $this->hunkStartLines;
}
public function setUser(PhabricatorUser $user) {
$this->user = $user;
return $this;
}
protected function getUser() {
return $this->user;
}
public function setChangeset(DifferentialChangeset $changeset) {
$this->changeset = $changeset;
return $this;
}
protected function getChangeset() {
return $this->changeset;
}
public function setRenderingReference($rendering_reference) {
$this->renderingReference = $rendering_reference;
return $this;
}
protected function getRenderingReference() {
return $this->renderingReference;
}
public function setRenderPropertyChangeHeader($should_render) {
$this->renderPropertyChangeHeader = $should_render;
return $this;
}
private function shouldRenderPropertyChangeHeader() {
return $this->renderPropertyChangeHeader;
}
public function setIsTopLevel($is) {
$this->isTopLevel = $is;
return $this;
}
private function getIsTopLevel() {
return $this->isTopLevel;
}
public function setCanMarkDone($can_mark_done) {
$this->canMarkDone = $can_mark_done;
return $this;
}
public function getCanMarkDone() {
return $this->canMarkDone;
}
public function setObjectOwnerPHID($phid) {
$this->objectOwnerPHID = $phid;
return $this;
}
public function getObjectOwnerPHID() {
return $this->objectOwnerPHID;
}
final public function renderChangesetTable($content) {
$props = null;
if ($this->shouldRenderPropertyChangeHeader()) {
$props = $this->renderPropertyChangeHeader();
}
$notice = null;
if ($this->getIsTopLevel()) {
$force = (!$content && !$props);
$notice = $this->renderChangeTypeHeader($force);
}
$undershield = null;
if ($this->getIsUndershield()) {
$undershield = $this->renderUndershieldHeader();
}
$result = $notice.$props.$undershield.$content;
// TODO: Let the user customize their tab width / display style.
// TODO: We should possibly post-process "\r" as well.
// TODO: Both these steps should happen earlier.
$result = str_replace("\t", ' ', $result);
return phutil_safe_html($result);
}
abstract public function isOneUpRenderer();
abstract public function renderTextChange(
$range_start,
$range_len,
$rows);
abstract public function renderFileChange(
$old = null,
$new = null,
$id = 0,
$vs = 0);
abstract protected function renderChangeTypeHeader($force);
abstract protected function renderUndershieldHeader();
protected function didRenderChangesetTableContents($contents) {
return $contents;
}
/**
* Render a "shield" over the diff, with a message like "This file is
* generated and does not need to be reviewed." or "This file was completely
* deleted." This UI element hides unimportant text so the reviewer doesn't
* need to scroll past it.
*
* The shield includes a link to view the underlying content. This link
* may force certain rendering modes when the link is clicked:
*
* - `"default"`: Render the diff normally, as though it was not
* shielded. This is the default and appropriate if the underlying
* diff is a normal change, but was hidden for reasons of not being
* important (e.g., generated code).
* - `"text"`: Force the text to be shown. This is probably only relevant
* when a file is not changed.
* - `"whitespace"`: Force the text to be shown, and the diff to be
* rendered with all whitespace shown. This is probably only relevant
* when a file is changed only by altering whitespace.
* - `"none"`: Don't show the link (e.g., text not available).
*
* @param string Message explaining why the diff is hidden.
* @param string|null Force mode, see above.
* @return string Shield markup.
*/
abstract public function renderShield($message, $force = 'default');
abstract protected function renderPropertyChangeHeader();
protected function buildPrimitives($range_start, $range_len) {
$primitives = array();
$hunk_starts = $this->getHunkStartLines();
$mask = $this->getMask();
$gaps = $this->getGaps();
$old = $this->getOldLines();
$new = $this->getNewLines();
$old_render = $this->getOldRender();
$new_render = $this->getNewRender();
$old_comments = $this->getOldComments();
$new_comments = $this->getNewComments();
$size = count($old);
for ($ii = $range_start; $ii < $range_start + $range_len; $ii++) {
if (empty($mask[$ii])) {
list($top, $len) = array_pop($gaps);
$primitives[] = array(
'type' => 'context',
'top' => $top,
'len' => $len,
);
$ii += ($len - 1);
continue;
}
$ospec = array(
'type' => 'old',
'htype' => null,
'cursor' => $ii,
'line' => null,
'oline' => null,
'render' => null,
);
$nspec = array(
'type' => 'new',
'htype' => null,
'cursor' => $ii,
'line' => null,
'oline' => null,
'render' => null,
'copy' => null,
'coverage' => null,
);
if (isset($old[$ii])) {
$ospec['line'] = (int)$old[$ii]['line'];
$nspec['oline'] = (int)$old[$ii]['line'];
$ospec['htype'] = $old[$ii]['type'];
if (isset($old_render[$ii])) {
$ospec['render'] = $old_render[$ii];
}
}
if (isset($new[$ii])) {
$nspec['line'] = (int)$new[$ii]['line'];
$ospec['oline'] = (int)$new[$ii]['line'];
$nspec['htype'] = $new[$ii]['type'];
if (isset($new_render[$ii])) {
$nspec['render'] = $new_render[$ii];
}
}
if (isset($hunk_starts[$ospec['line']])) {
$primitives[] = array(
'type' => 'no-context',
);
}
$primitives[] = $ospec;
$primitives[] = $nspec;
if ($ospec['line'] !== null && isset($old_comments[$ospec['line']])) {
foreach ($old_comments[$ospec['line']] as $comment) {
$primitives[] = array(
'type' => 'inline',
'comment' => $comment,
'right' => false,
);
}
}
if ($nspec['line'] !== null && isset($new_comments[$nspec['line']])) {
foreach ($new_comments[$nspec['line']] as $comment) {
$primitives[] = array(
'type' => 'inline',
'comment' => $comment,
'right' => true,
);
}
}
if ($hunk_starts && ($ii == $size - 1)) {
$primitives[] = array(
'type' => 'no-context',
);
}
}
if ($this->isOneUpRenderer()) {
$primitives = $this->processPrimitivesForOneUp($primitives);
}
return $primitives;
}
private function processPrimitivesForOneUp(array $primitives) {
// Primitives come out of buildPrimitives() in two-up format, because it
// is the most general, flexible format. To put them into one-up format,
// we need to filter and reorder them. In particular:
//
// - We discard unchanged lines in the old file; in one-up format, we
// render them only once.
// - We group contiguous blocks of old-modified and new-modified lines, so
// they render in "block of old, block of new" order instead of
// alternating old and new lines.
$out = array();
$old_buf = array();
$new_buf = array();
foreach ($primitives as $primitive) {
$type = $primitive['type'];
if ($type == 'old') {
if (!$primitive['htype']) {
// This is a line which appears in both the old file and the new
// file, or the spacer corresponding to a line added in the new file.
// Ignore it when rendering a one-up diff.
continue;
}
$old_buf[] = $primitive;
} else if ($type == 'new') {
if ($primitive['line'] === null) {
// This is an empty spacer corresponding to a line removed from the
// old file. Ignore it when rendering a one-up diff.
continue;
}
if (!$primitive['htype']) {
// If this line is the same in both versions of the file, put it in
// the old line buffer. This makes sure inlines on old, unchanged
// lines end up in the right place.
// First, we need to flush the line buffers if they're not empty.
if ($old_buf) {
$out[] = $old_buf;
$old_buf = array();
}
if ($new_buf) {
$out[] = $new_buf;
$new_buf = array();
}
$old_buf[] = $primitive;
} else {
$new_buf[] = $primitive;
}
} else if ($type == 'context' || $type == 'no-context') {
$out[] = $old_buf;
$out[] = $new_buf;
$old_buf = array();
$new_buf = array();
$out[] = array($primitive);
} else if ($type == 'inline') {
// If this inline is on the left side, put it after the old lines.
if (!$primitive['right']) {
$out[] = $old_buf;
$out[] = array($primitive);
$old_buf = array();
} else {
$out[] = $old_buf;
$out[] = $new_buf;
$out[] = array($primitive);
$old_buf = array();
$new_buf = array();
}
} else {
- throw new Exception("Unknown primitive type '{$primitive}'!");
+ throw new Exception(pht("Unknown primitive type '%s'!", $primitive));
}
}
$out[] = $old_buf;
$out[] = $new_buf;
$out = array_mergev($out);
return $out;
}
protected function getChangesetProperties($changeset) {
$old = $changeset->getOldProperties();
$new = $changeset->getNewProperties();
// When adding files, don't show the uninteresting 644 filemode change.
if ($changeset->getChangeType() == DifferentialChangeType::TYPE_ADD &&
$new == array('unix:filemode' => '100644')) {
unset($new['unix:filemode']);
}
// Likewise when removing files.
if ($changeset->getChangeType() == DifferentialChangeType::TYPE_DELETE &&
$old == array('unix:filemode' => '100644')) {
unset($old['unix:filemode']);
}
if ($this->hasOldFile()) {
$file = $this->getOldFile();
if ($file->getImageWidth()) {
$dimensions = $file->getImageWidth().'x'.$file->getImageHeight();
$old['file:dimensions'] = $dimensions;
}
$old['file:mimetype'] = $file->getMimeType();
$old['file:size'] = phutil_format_bytes($file->getByteSize());
}
if ($this->hasNewFile()) {
$file = $this->getNewFile();
if ($file->getImageWidth()) {
$dimensions = $file->getImageWidth().'x'.$file->getImageHeight();
$new['file:dimensions'] = $dimensions;
}
$new['file:mimetype'] = $file->getMimeType();
$new['file:size'] = phutil_format_bytes($file->getByteSize());
}
return array($old, $new);
}
public function renderUndoTemplates() {
$views = array(
'l' => id(new PHUIDiffInlineCommentUndoView())->setIsOnRight(false),
'r' => id(new PHUIDiffInlineCommentUndoView())->setIsOnRight(true),
);
foreach ($views as $key => $view) {
$scaffold = $this->getRowScaffoldForInline($view);
$views[$key] = id(new PHUIDiffInlineCommentTableScaffold())
->addRowScaffold($scaffold);
}
return $views;
}
}
diff --git a/src/applications/differential/render/DifferentialChangesetTwoUpRenderer.php b/src/applications/differential/render/DifferentialChangesetTwoUpRenderer.php
index 3eb019066..181e3d130 100644
--- a/src/applications/differential/render/DifferentialChangesetTwoUpRenderer.php
+++ b/src/applications/differential/render/DifferentialChangesetTwoUpRenderer.php
@@ -1,373 +1,375 @@
<?php
final class DifferentialChangesetTwoUpRenderer
extends DifferentialChangesetHTMLRenderer {
public function isOneUpRenderer() {
return false;
}
protected function getRendererTableClass() {
return 'diff-2up';
}
public function getRendererKey() {
return '2up';
}
protected function renderColgroup() {
return phutil_tag('colgroup', array(), array(
phutil_tag('col', array('class' => 'num')),
phutil_tag('col', array('class' => 'left')),
phutil_tag('col', array('class' => 'num')),
phutil_tag('col', array('class' => 'copy')),
phutil_tag('col', array('class' => 'right')),
phutil_tag('col', array('class' => 'cov')),
));
}
public function renderTextChange(
$range_start,
$range_len,
$rows) {
$hunk_starts = $this->getHunkStartLines();
$context_not_available = null;
if ($hunk_starts) {
$context_not_available = javelin_tag(
'tr',
array(
'sigil' => 'context-target',
),
phutil_tag(
'td',
array(
'colspan' => 6,
'class' => 'show-more',
),
pht('Context not available.')));
}
$html = array();
$old_lines = $this->getOldLines();
$new_lines = $this->getNewLines();
$gaps = $this->getGaps();
$reference = $this->getRenderingReference();
list($left_prefix, $right_prefix) = $this->getLineIDPrefixes();
$changeset = $this->getChangeset();
$copy_lines = idx($changeset->getMetadata(), 'copy:lines', array());
$highlight_old = $this->getHighlightOld();
$highlight_new = $this->getHighlightNew();
$old_render = $this->getOldRender();
$new_render = $this->getNewRender();
$original_left = $this->getOriginalOld();
$original_right = $this->getOriginalNew();
$depths = $this->getDepths();
$mask = $this->getMask();
for ($ii = $range_start; $ii < $range_start + $range_len; $ii++) {
if (empty($mask[$ii])) {
// If we aren't going to show this line, we've just entered a gap.
// Pop information about the next gap off the $gaps stack and render
// an appropriate "Show more context" element. This branch eventually
// increments $ii by the entire size of the gap and then continues
// the loop.
$gap = array_pop($gaps);
$top = $gap[0];
$len = $gap[1];
$contents = $this->renderShowContextLinks($top, $len, $rows);
$is_last_block = false;
if ($ii + $len >= $rows) {
$is_last_block = true;
}
$context = null;
$context_line = null;
if (!$is_last_block && $depths[$ii + $len]) {
for ($l = $ii + $len - 1; $l >= $ii; $l--) {
$line = $new_lines[$l]['text'];
if ($depths[$l] < $depths[$ii + $len] && trim($line) != '') {
$context = $new_render[$l];
$context_line = $new_lines[$l]['line'];
break;
}
}
}
$container = javelin_tag(
'tr',
array(
'sigil' => 'context-target',
),
array(
phutil_tag(
'td',
array(
'colspan' => 2,
'class' => 'show-more',
),
$contents),
phutil_tag(
'th',
array(
'class' => 'show-context-line',
),
$context_line ? (int)$context_line : null),
phutil_tag(
'td',
array(
'colspan' => 3,
'class' => 'show-context',
),
// TODO: [HTML] Escaping model here isn't ideal.
phutil_safe_html($context)),
));
$html[] = $container;
$ii += ($len - 1);
continue;
}
$o_num = null;
$o_classes = '';
$o_text = null;
if (isset($old_lines[$ii])) {
$o_num = $old_lines[$ii]['line'];
$o_text = isset($old_render[$ii]) ? $old_render[$ii] : null;
if ($old_lines[$ii]['type']) {
if ($old_lines[$ii]['type'] == '\\') {
$o_text = $old_lines[$ii]['text'];
$o_class = 'comment';
} else if ($original_left && !isset($highlight_old[$o_num])) {
$o_class = 'old-rebase';
} else if (empty($new_lines[$ii])) {
$o_class = 'old old-full';
} else {
$o_class = 'old';
}
$o_classes = $o_class;
}
}
$n_copy = hsprintf('<td class="copy" />');
$n_cov = null;
$n_colspan = 2;
$n_classes = '';
$n_num = null;
$n_text = null;
if (isset($new_lines[$ii])) {
$n_num = $new_lines[$ii]['line'];
$n_text = isset($new_render[$ii]) ? $new_render[$ii] : null;
$coverage = $this->getCodeCoverage();
if ($coverage !== null) {
if (empty($coverage[$n_num - 1])) {
$cov_class = 'N';
} else {
$cov_class = $coverage[$n_num - 1];
}
$cov_class = 'cov-'.$cov_class;
$n_cov = phutil_tag('td', array('class' => "cov {$cov_class}"));
$n_colspan--;
}
if ($new_lines[$ii]['type']) {
if ($new_lines[$ii]['type'] == '\\') {
$n_text = $new_lines[$ii]['text'];
$n_class = 'comment';
} else if ($original_right && !isset($highlight_new[$n_num])) {
$n_class = 'new-rebase';
} else if (empty($old_lines[$ii])) {
$n_class = 'new new-full';
} else {
$n_class = 'new';
}
$n_classes = $n_class;
if ($new_lines[$ii]['type'] == '\\' || !isset($copy_lines[$n_num])) {
$n_copy = phutil_tag('td', array('class' => "copy {$n_class}"));
} else {
list($orig_file, $orig_line, $orig_type) = $copy_lines[$n_num];
$title = ($orig_type == '-' ? 'Moved' : 'Copied').' from ';
if ($orig_file == '') {
$title .= "line {$orig_line}";
} else {
$title .=
basename($orig_file).
":{$orig_line} in dir ".
dirname('/'.$orig_file);
}
$class = ($orig_type == '-' ? 'new-move' : 'new-copy');
$n_copy = javelin_tag(
'td',
array(
'meta' => array(
'msg' => $title,
),
'class' => 'copy '.$class,
),
'');
}
}
}
if (isset($hunk_starts[$o_num])) {
$html[] = $context_not_available;
}
if ($o_num && $left_prefix) {
$o_id = $left_prefix.$o_num;
} else {
$o_id = null;
}
if ($n_num && $right_prefix) {
$n_id = $right_prefix.$n_num;
} else {
$n_id = null;
}
// NOTE: This is a unicode zero-width space, which we use as a hint when
// intercepting 'copy' events to make sure sensible text ends up on the
// clipboard. See the 'phabricator-oncopy' behavior.
$zero_space = "\xE2\x80\x8B";
$html[] = phutil_tag('tr', array(), array(
phutil_tag('th', array('id' => $o_id), $o_num),
phutil_tag('td', array('class' => $o_classes), $o_text),
phutil_tag('th', array('id' => $n_id), $n_num),
$n_copy,
phutil_tag(
'td',
array('class' => $n_classes, 'colspan' => $n_colspan),
array(
phutil_tag('span', array('class' => 'zwsp'), $zero_space),
$n_text,
)),
$n_cov,
));
if ($context_not_available && ($ii == $rows - 1)) {
$html[] = $context_not_available;
}
$old_comments = $this->getOldComments();
$new_comments = $this->getNewComments();
if ($o_num && isset($old_comments[$o_num])) {
foreach ($old_comments[$o_num] as $comment) {
$inline = $this->buildInlineComment(
$comment,
$on_right = false);
$scaffold = $this->getRowScaffoldForInline($inline);
if ($n_num && isset($new_comments[$n_num])) {
foreach ($new_comments[$n_num] as $key => $new_comment) {
if ($comment->isCompatible($new_comment)) {
$companion = $this->buildInlineComment(
$new_comment,
$on_right = true);
$scaffold->addInlineView($companion);
unset($new_comments[$n_num][$key]);
break;
}
}
}
$html[] = $scaffold;
}
}
if ($n_num && isset($new_comments[$n_num])) {
foreach ($new_comments[$n_num] as $comment) {
$inline = $this->buildInlineComment(
$comment,
$on_right = true);
$html[] = $this->getRowScaffoldForInline($inline);
}
}
}
return $this->wrapChangeInTable(phutil_implode_html('', $html));
}
- public function renderFileChange($old_file = null,
- $new_file = null,
- $id = 0,
- $vs = 0) {
+ public function renderFileChange(
+ $old_file = null,
+ $new_file = null,
+ $id = 0,
+ $vs = 0) {
+
$old = null;
if ($old_file) {
$old = $this->renderImageStage($old_file);
}
$new = null;
if ($new_file) {
$new = $this->renderImageStage($new_file);
}
$html_old = array();
$html_new = array();
foreach ($this->getOldComments() as $on_line => $comment_group) {
foreach ($comment_group as $comment) {
$inline = $this->buildInlineComment(
$comment,
$on_right = false);
$html_old[] = $this->getRowScaffoldForInline($inline);
}
}
foreach ($this->getNewComments() as $lin_line => $comment_group) {
foreach ($comment_group as $comment) {
$inline = $this->buildInlineComment(
$comment,
$on_right = true);
$html_new[] = $this->getRowScaffoldForInline($inline);
}
}
if (!$old) {
$th_old = phutil_tag('th', array());
} else {
$th_old = phutil_tag('th', array('id' => "C{$vs}OL1"), 1);
}
if (!$new) {
$th_new = phutil_tag('th', array());
} else {
$th_new = phutil_tag('th', array('id' => "C{$id}OL1"), 1);
}
$output = hsprintf(
'<tr class="differential-image-diff">'.
'%s'.
'<td class="differential-old-image">%s</td>'.
'%s'.
'<td class="differential-new-image" colspan="3">%s</td>'.
'</tr>'.
'%s'.
'%s',
$th_old,
$old,
$th_new,
$new,
phutil_implode_html('', $html_old),
phutil_implode_html('', $html_new));
$output = $this->wrapChangeInTable($output);
return $this->renderChangesetTable($output);
}
public function getRowScaffoldForInline(PHUIDiffInlineCommentView $view) {
return id(new PHUIDiffTwoUpInlineCommentRowScaffold())
->addInlineView($view);
}
}
diff --git a/src/applications/differential/search/DifferentialSearchIndexer.php b/src/applications/differential/search/DifferentialSearchIndexer.php
index 66a89a6fb..997650312 100644
--- a/src/applications/differential/search/DifferentialSearchIndexer.php
+++ b/src/applications/differential/search/DifferentialSearchIndexer.php
@@ -1,81 +1,81 @@
<?php
final class DifferentialSearchIndexer
extends PhabricatorSearchDocumentIndexer {
public function getIndexableObject() {
return new DifferentialRevision();
}
protected function loadDocumentByPHID($phid) {
$object = id(new DifferentialRevisionQuery())
->setViewer($this->getViewer())
->withPHIDs(array($phid))
->needReviewerStatus(true)
->executeOne();
if (!$object) {
- throw new Exception("Unable to load object by phid '{$phid}'!");
+ throw new Exception(pht("Unable to load object by PHID '%s'!", $phid));
}
return $object;
}
protected function buildAbstractDocumentByPHID($phid) {
$rev = $this->loadDocumentByPHID($phid);
$doc = new PhabricatorSearchAbstractDocument();
$doc->setPHID($rev->getPHID());
$doc->setDocumentType(DifferentialRevisionPHIDType::TYPECONST);
$doc->setDocumentTitle($rev->getTitle());
$doc->setDocumentCreated($rev->getDateCreated());
$doc->setDocumentModified($rev->getDateModified());
$doc->addRelationship(
PhabricatorSearchRelationship::RELATIONSHIP_AUTHOR,
$rev->getAuthorPHID(),
PhabricatorPeopleUserPHIDType::TYPECONST,
$rev->getDateCreated());
$doc->addRelationship(
$rev->isClosed()
? PhabricatorSearchRelationship::RELATIONSHIP_CLOSED
: PhabricatorSearchRelationship::RELATIONSHIP_OPEN,
$rev->getPHID(),
DifferentialRevisionPHIDType::TYPECONST,
time());
$this->indexTransactions(
$doc,
new DifferentialTransactionQuery(),
array($rev->getPHID()));
// If a revision needs review, the owners are the reviewers. Otherwise, the
// owner is the author (e.g., accepted, rejected, closed).
if ($rev->getStatus() == ArcanistDifferentialRevisionStatus::NEEDS_REVIEW) {
$reviewers = $rev->getReviewerStatus();
$reviewers = mpull($reviewers, 'getReviewerPHID', 'getReviewerPHID');
if ($reviewers) {
foreach ($reviewers as $phid) {
$doc->addRelationship(
PhabricatorSearchRelationship::RELATIONSHIP_OWNER,
$phid,
PhabricatorPeopleUserPHIDType::TYPECONST,
$rev->getDateModified()); // Bogus timestamp.
}
} else {
$doc->addRelationship(
PhabricatorSearchRelationship::RELATIONSHIP_UNOWNED,
$rev->getPHID(),
PhabricatorPeopleUserPHIDType::TYPECONST,
$rev->getDateModified()); // Bogus timestamp.
}
} else {
$doc->addRelationship(
PhabricatorSearchRelationship::RELATIONSHIP_OWNER,
$rev->getAuthorPHID(),
PhabricatorPHIDConstants::PHID_TYPE_VOID,
$rev->getDateCreated());
}
return $doc;
}
}
diff --git a/src/applications/differential/storage/DifferentialReviewer.php b/src/applications/differential/storage/DifferentialReviewer.php
index b5a339efb..e3b1e0ac2 100644
--- a/src/applications/differential/storage/DifferentialReviewer.php
+++ b/src/applications/differential/storage/DifferentialReviewer.php
@@ -1,56 +1,56 @@
<?php
final class DifferentialReviewer {
private $reviewerPHID;
private $status;
private $diffID;
private $authority = array();
public function __construct($reviewer_phid, array $edge_data) {
$this->reviewerPHID = $reviewer_phid;
$this->status = idx($edge_data, 'status');
$this->diffID = idx($edge_data, 'diff');
}
public function getReviewerPHID() {
return $this->reviewerPHID;
}
public function getStatus() {
return $this->status;
}
public function getDiffID() {
return $this->diffID;
}
public function isUser() {
$user_type = PhabricatorPeopleUserPHIDType::TYPECONST;
return (phid_get_type($this->getReviewerPHID()) == $user_type);
}
public function attachAuthority(PhabricatorUser $user, $has_authority) {
$this->authority[$user->getPHID()] = $has_authority;
return $this;
}
public function hasAuthority(PhabricatorUser $viewer) {
// It would be nice to use assertAttachedKey() here, but we don't extend
// PhabricatorLiskDAO, and faking that seems sketchy.
$viewer_phid = $viewer->getPHID();
if (!array_key_exists($viewer_phid, $this->authority)) {
- throw new Exception('You must attachAuthority() first!');
+ throw new Exception(pht('You must %s first!', 'attachAuthority()'));
}
return $this->authority[$viewer_phid];
}
public function getEdgeData() {
return array(
'status' => $this->status,
'diffID' => $this->diffID,
);
}
}
diff --git a/src/applications/differential/storage/DifferentialRevision.php b/src/applications/differential/storage/DifferentialRevision.php
index e6baa678e..763081488 100644
--- a/src/applications/differential/storage/DifferentialRevision.php
+++ b/src/applications/differential/storage/DifferentialRevision.php
@@ -1,581 +1,580 @@
<?php
final class DifferentialRevision extends DifferentialDAO
implements
PhabricatorTokenReceiverInterface,
PhabricatorPolicyInterface,
PhabricatorFlaggableInterface,
PhrequentTrackableInterface,
HarbormasterBuildableInterface,
PhabricatorSubscribableInterface,
PhabricatorCustomFieldInterface,
PhabricatorApplicationTransactionInterface,
PhabricatorMentionableInterface,
PhabricatorDestructibleInterface,
PhabricatorProjectInterface {
protected $title = '';
protected $originalTitle;
protected $status;
protected $summary = '';
protected $testPlan = '';
protected $authorPHID;
protected $lastReviewerPHID;
protected $lineCount = 0;
protected $attached = array();
protected $mailKey;
protected $branchName;
protected $arcanistProjectPHID;
protected $repositoryPHID;
protected $viewPolicy = PhabricatorPolicies::POLICY_USER;
protected $editPolicy = PhabricatorPolicies::POLICY_USER;
private $relationships = self::ATTACHABLE;
private $commits = self::ATTACHABLE;
private $activeDiff = self::ATTACHABLE;
private $diffIDs = self::ATTACHABLE;
private $hashes = self::ATTACHABLE;
private $repository = self::ATTACHABLE;
private $reviewerStatus = self::ATTACHABLE;
private $customFields = self::ATTACHABLE;
private $drafts = array();
private $flags = array();
const TABLE_COMMIT = 'differential_commit';
const RELATION_REVIEWER = 'revw';
const RELATION_SUBSCRIBED = 'subd';
public static function initializeNewRevision(PhabricatorUser $actor) {
$app = id(new PhabricatorApplicationQuery())
->setViewer($actor)
->withClasses(array('PhabricatorDifferentialApplication'))
->executeOne();
$view_policy = $app->getPolicy(
DifferentialDefaultViewCapability::CAPABILITY);
return id(new DifferentialRevision())
->setViewPolicy($view_policy)
->setAuthorPHID($actor->getPHID())
->attachRelationships(array())
->setStatus(ArcanistDifferentialRevisionStatus::NEEDS_REVIEW);
}
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'attached' => self::SERIALIZATION_JSON,
'unsubscribed' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'title' => 'text255',
'originalTitle' => 'text255',
'status' => 'text32',
'summary' => 'text',
'testPlan' => 'text',
'authorPHID' => 'phid?',
'lastReviewerPHID' => 'phid?',
'lineCount' => 'uint32?',
'mailKey' => 'bytes40',
'branchName' => 'text255?',
'arcanistProjectPHID' => 'phid?',
'repositoryPHID' => 'phid?',
),
self::CONFIG_KEY_SCHEMA => array(
'key_phid' => null,
'phid' => array(
'columns' => array('phid'),
'unique' => true,
),
'authorPHID' => array(
'columns' => array('authorPHID', 'status'),
),
'repositoryPHID' => array(
'columns' => array('repositoryPHID'),
),
),
) + parent::getConfiguration();
}
public function getMonogram() {
$id = $this->getID();
return "D{$id}";
}
public function setTitle($title) {
$this->title = $title;
if (!$this->getID()) {
$this->originalTitle = $title;
}
return $this;
}
public function loadIDsByCommitPHIDs($phids) {
if (!$phids) {
return array();
}
$revision_ids = queryfx_all(
$this->establishConnection('r'),
'SELECT * FROM %T WHERE commitPHID IN (%Ls)',
self::TABLE_COMMIT,
$phids);
return ipull($revision_ids, 'revisionID', 'commitPHID');
}
public function loadCommitPHIDs() {
if (!$this->getID()) {
return ($this->commits = array());
}
$commits = queryfx_all(
$this->establishConnection('r'),
'SELECT commitPHID FROM %T WHERE revisionID = %d',
self::TABLE_COMMIT,
$this->getID());
$commits = ipull($commits, 'commitPHID');
return ($this->commits = $commits);
}
public function getCommitPHIDs() {
return $this->assertAttached($this->commits);
}
public function getActiveDiff() {
// TODO: Because it's currently technically possible to create a revision
// without an associated diff, we allow an attached-but-null active diff.
// It would be good to get rid of this once we make diff-attaching
// transactional.
return $this->assertAttached($this->activeDiff);
}
public function attachActiveDiff($diff) {
$this->activeDiff = $diff;
return $this;
}
public function getDiffIDs() {
return $this->assertAttached($this->diffIDs);
}
public function attachDiffIDs(array $ids) {
rsort($ids);
$this->diffIDs = array_values($ids);
return $this;
}
public function attachCommitPHIDs(array $phids) {
$this->commits = array_values($phids);
return $this;
}
public function getAttachedPHIDs($type) {
return array_keys(idx($this->attached, $type, array()));
}
public function setAttachedPHIDs($type, array $phids) {
$this->attached[$type] = array_fill_keys($phids, array());
return $this;
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
DifferentialRevisionPHIDType::TYPECONST);
}
public function loadActiveDiff() {
return id(new DifferentialDiff())->loadOneWhere(
'revisionID = %d ORDER BY id DESC LIMIT 1',
$this->getID());
}
public function save() {
if (!$this->getMailKey()) {
$this->mailKey = Filesystem::readRandomCharacters(40);
}
return parent::save();
}
public function loadRelationships() {
if (!$this->getID()) {
$this->relationships = array();
return;
}
$data = array();
$subscriber_phids = PhabricatorEdgeQuery::loadDestinationPHIDs(
$this->getPHID(),
PhabricatorObjectHasSubscriberEdgeType::EDGECONST);
$subscriber_phids = array_reverse($subscriber_phids);
foreach ($subscriber_phids as $phid) {
$data[] = array(
'relation' => self::RELATION_SUBSCRIBED,
'objectPHID' => $phid,
'reasonPHID' => null,
);
}
$reviewer_phids = PhabricatorEdgeQuery::loadDestinationPHIDs(
$this->getPHID(),
DifferentialRevisionHasReviewerEdgeType::EDGECONST);
$reviewer_phids = array_reverse($reviewer_phids);
foreach ($reviewer_phids as $phid) {
$data[] = array(
'relation' => self::RELATION_REVIEWER,
'objectPHID' => $phid,
'reasonPHID' => null,
);
}
return $this->attachRelationships($data);
}
public function attachRelationships(array $relationships) {
$this->relationships = igroup($relationships, 'relation');
return $this;
}
public function getReviewers() {
return $this->getRelatedPHIDs(self::RELATION_REVIEWER);
}
public function getCCPHIDs() {
return $this->getRelatedPHIDs(self::RELATION_SUBSCRIBED);
}
private function getRelatedPHIDs($relation) {
$this->assertAttached($this->relationships);
return ipull($this->getRawRelations($relation), 'objectPHID');
}
public function getRawRelations($relation) {
return idx($this->relationships, $relation, array());
}
public function getPrimaryReviewer() {
$reviewers = $this->getReviewers();
$last = $this->lastReviewerPHID;
if (!$last || !in_array($last, $reviewers)) {
return head($this->getReviewers());
}
return $last;
}
public function getHashes() {
return $this->assertAttached($this->hashes);
}
public function attachHashes(array $hashes) {
$this->hashes = $hashes;
return $this;
}
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
return $this->getViewPolicy();
case PhabricatorPolicyCapability::CAN_EDIT:
return $this->getEditPolicy();
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $user) {
// A revision's author (which effectively means "owner" after we added
// commandeering) can always view and edit it.
$author_phid = $this->getAuthorPHID();
if ($author_phid) {
if ($user->getPHID() == $author_phid) {
return true;
}
}
return false;
}
public function describeAutomaticCapability($capability) {
$description = array(
pht('The owner of a revision can always view and edit it.'),
);
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
- $description[] = pht(
- "A revision's reviewers can always view it.");
+ $description[] = pht("A revision's reviewers can always view it.");
$description[] = pht(
'If a revision belongs to a repository, other users must be able '.
'to view the repository in order to view the revision.');
break;
}
return $description;
}
public function getUsersToNotifyOfTokenGiven() {
return array(
$this->getAuthorPHID(),
);
}
public function getReviewerStatus() {
return $this->assertAttached($this->reviewerStatus);
}
public function attachReviewerStatus(array $reviewers) {
assert_instances_of($reviewers, 'DifferentialReviewer');
$this->reviewerStatus = $reviewers;
return $this;
}
public function getRepository() {
return $this->assertAttached($this->repository);
}
public function attachRepository(PhabricatorRepository $repository = null) {
$this->repository = $repository;
return $this;
}
public function isClosed() {
return DifferentialRevisionStatus::isClosedStatus($this->getStatus());
}
public function getFlag(PhabricatorUser $viewer) {
return $this->assertAttachedKey($this->flags, $viewer->getPHID());
}
public function attachFlag(
PhabricatorUser $viewer,
PhabricatorFlag $flag = null) {
$this->flags[$viewer->getPHID()] = $flag;
return $this;
}
public function getDrafts(PhabricatorUser $viewer) {
return $this->assertAttachedKey($this->drafts, $viewer->getPHID());
}
public function attachDrafts(PhabricatorUser $viewer, array $drafts) {
$this->drafts[$viewer->getPHID()] = $drafts;
return $this;
}
/* -( HarbormasterBuildableInterface )------------------------------------- */
public function getHarbormasterBuildablePHID() {
return $this->loadActiveDiff()->getPHID();
}
public function getHarbormasterContainerPHID() {
return $this->getPHID();
}
public function getBuildVariables() {
return array();
}
public function getAvailableBuildVariables() {
return array();
}
/* -( PhabricatorSubscribableInterface )----------------------------------- */
public function isAutomaticallySubscribed($phid) {
if ($phid == $this->getAuthorPHID()) {
return true;
}
// TODO: This only happens when adding or removing CCs, and is safe from a
// policy perspective, but the subscription pathway should have some
// opportunity to load this data properly. For now, this is the only case
// where implicit subscription is not an intrinsic property of the object.
if ($this->reviewerStatus == self::ATTACHABLE) {
$reviewers = id(new DifferentialRevisionQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withPHIDs(array($this->getPHID()))
->needReviewerStatus(true)
->executeOne()
->getReviewerStatus();
} else {
$reviewers = $this->getReviewerStatus();
}
foreach ($reviewers as $reviewer) {
if ($reviewer->getReviewerPHID() == $phid) {
return true;
}
}
return false;
}
public function shouldShowSubscribersProperty() {
return true;
}
public function shouldAllowSubscription($phid) {
return true;
}
/* -( PhabricatorCustomFieldInterface )------------------------------------ */
public function getCustomFieldSpecificationForRole($role) {
return PhabricatorEnv::getEnvConfig('differential.fields');
}
public function getCustomFieldBaseClass() {
return 'DifferentialCustomField';
}
public function getCustomFields() {
return $this->assertAttached($this->customFields);
}
public function attachCustomFields(PhabricatorCustomFieldAttachment $fields) {
$this->customFields = $fields;
return $this;
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new DifferentialTransactionEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new DifferentialTransaction();
}
public function willRenderTimeline(
PhabricatorApplicationTransactionView $timeline,
AphrontRequest $request) {
$viewer = $request->getViewer();
$render_data = $timeline->getRenderData();
$left = $request->getInt('left', idx($render_data, 'left'));
$right = $request->getInt('right', idx($render_data, 'right'));
$diffs = id(new DifferentialDiffQuery())
->setViewer($request->getUser())
->withIDs(array($left, $right))
->execute();
$diffs = mpull($diffs, null, 'getID');
$left_diff = $diffs[$left];
$right_diff = $diffs[$right];
$old_ids = $request->getStr('old', idx($render_data, 'old'));
$new_ids = $request->getStr('new', idx($render_data, 'new'));
$old_ids = array_filter(explode(',', $old_ids));
$new_ids = array_filter(explode(',', $new_ids));
$type_inline = DifferentialTransaction::TYPE_INLINE;
$changeset_ids = array_merge($old_ids, $new_ids);
$inlines = array();
foreach ($timeline->getTransactions() as $xaction) {
if ($xaction->getTransactionType() == $type_inline) {
$inlines[] = $xaction->getComment();
$changeset_ids[] = $xaction->getComment()->getChangesetID();
}
}
if ($changeset_ids) {
$changesets = id(new DifferentialChangesetQuery())
->setViewer($request->getUser())
->withIDs($changeset_ids)
->execute();
$changesets = mpull($changesets, null, 'getID');
} else {
$changesets = array();
}
foreach ($inlines as $key => $inline) {
$inlines[$key] = DifferentialInlineComment::newFromModernComment(
$inline);
}
$query = id(new DifferentialInlineCommentQuery())
->setViewer($viewer);
// NOTE: This is a bit sketchy: this method adjusts the inlines as a
// side effect, which means it will ultimately adjust the transaction
// comments and affect timeline rendering.
$query->adjustInlinesForChangesets(
$inlines,
array_select_keys($changesets, $old_ids),
array_select_keys($changesets, $new_ids),
$this);
return $timeline
->setChangesets($changesets)
->setRevision($this)
->setLeftDiff($left_diff)
->setRightDiff($right_diff);
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$this->openTransaction();
$diffs = id(new DifferentialDiffQuery())
->setViewer($engine->getViewer())
->withRevisionIDs(array($this->getID()))
->execute();
foreach ($diffs as $diff) {
$engine->destroyObject($diff);
}
$conn_w = $this->establishConnection('w');
queryfx(
$conn_w,
'DELETE FROM %T WHERE revisionID = %d',
self::TABLE_COMMIT,
$this->getID());
// we have to do paths a little differentally as they do not have
// an id or phid column for delete() to act on
$dummy_path = new DifferentialAffectedPath();
queryfx(
$conn_w,
'DELETE FROM %T WHERE revisionID = %d',
$dummy_path->getTableName(),
$this->getID());
$this->delete();
$this->saveTransaction();
}
}
diff --git a/src/applications/differential/storage/DifferentialTransaction.php b/src/applications/differential/storage/DifferentialTransaction.php
index 2857d2427..23f6a45c2 100644
--- a/src/applications/differential/storage/DifferentialTransaction.php
+++ b/src/applications/differential/storage/DifferentialTransaction.php
@@ -1,665 +1,660 @@
<?php
final class DifferentialTransaction extends PhabricatorApplicationTransaction {
private $isCommandeerSideEffect;
public function setIsCommandeerSideEffect($is_side_effect) {
$this->isCommandeerSideEffect = $is_side_effect;
return $this;
}
public function getIsCommandeerSideEffect() {
return $this->isCommandeerSideEffect;
}
const TYPE_INLINE = 'differential:inline';
const TYPE_UPDATE = 'differential:update';
const TYPE_ACTION = 'differential:action';
const TYPE_STATUS = 'differential:status';
public function getApplicationName() {
return 'differential';
}
public function getApplicationTransactionType() {
return DifferentialRevisionPHIDType::TYPECONST;
}
public function getApplicationTransactionCommentObject() {
return new DifferentialTransactionComment();
}
public function getApplicationTransactionViewObject() {
return new DifferentialTransactionView();
}
public function shouldHide() {
$old = $this->getOldValue();
$new = $this->getNewValue();
switch ($this->getTransactionType()) {
case self::TYPE_UPDATE:
// Older versions of this transaction have an ID for the new value,
// and/or do not record the old value. Only hide the transaction if
// the new value is a PHID, indicating that this is a newer style
// transaction.
if ($old === null) {
if (phid_get_type($new) == DifferentialDiffPHIDType::TYPECONST) {
return true;
}
}
break;
case PhabricatorTransactions::TYPE_EDGE:
$add = array_diff_key($new, $old);
$rem = array_diff_key($old, $new);
// Hide metadata-only edge transactions. These correspond to users
// accepting or rejecting revisions, but the change is always explicit
// because of the TYPE_ACTION transaction. Rendering these transactions
// just creates clutter.
if (!$add && !$rem) {
return true;
}
break;
}
return parent::shouldHide();
}
public function isInlineCommentTransaction() {
switch ($this->getTransactionType()) {
case self::TYPE_INLINE:
return true;
}
return parent::isInlineCommentTransaction();
}
public function getRequiredHandlePHIDs() {
$phids = parent::getRequiredHandlePHIDs();
$old = $this->getOldValue();
$new = $this->getNewValue();
switch ($this->getTransactionType()) {
case self::TYPE_ACTION:
if ($new == DifferentialAction::ACTION_CLOSE &&
$this->getMetadataValue('isCommitClose')) {
$phids[] = $this->getMetadataValue('commitPHID');
if ($this->getMetadataValue('committerPHID')) {
$phids[] = $this->getMetadataValue('committerPHID');
}
if ($this->getMetadataValue('authorPHID')) {
$phids[] = $this->getMetadataValue('authorPHID');
}
}
break;
case self::TYPE_UPDATE:
if ($new) {
$phids[] = $new;
}
break;
}
return $phids;
}
public function getActionStrength() {
switch ($this->getTransactionType()) {
case self::TYPE_ACTION:
return 3;
case self::TYPE_UPDATE:
return 2;
}
return parent::getActionStrength();
}
public function getActionName() {
switch ($this->getTransactionType()) {
case self::TYPE_INLINE:
return pht('Commented On');
case self::TYPE_UPDATE:
$old = $this->getOldValue();
if ($old === null) {
return pht('Request');
} else {
return pht('Updated');
}
case self::TYPE_ACTION:
$map = array(
DifferentialAction::ACTION_ACCEPT => pht('Accepted'),
DifferentialAction::ACTION_REJECT => pht('Requested Changes To'),
DifferentialAction::ACTION_RETHINK => pht('Planned Changes To'),
DifferentialAction::ACTION_ABANDON => pht('Abandoned'),
DifferentialAction::ACTION_CLOSE => pht('Closed'),
DifferentialAction::ACTION_REQUEST => pht('Requested A Review Of'),
DifferentialAction::ACTION_RESIGN => pht('Resigned From'),
DifferentialAction::ACTION_ADDREVIEWERS => pht('Added Reviewers'),
DifferentialAction::ACTION_CLAIM => pht('Commandeered'),
DifferentialAction::ACTION_REOPEN => pht('Reopened'),
);
$name = idx($map, $this->getNewValue());
if ($name !== null) {
return $name;
}
break;
}
return parent::getActionName();
}
public function getMailTags() {
$tags = array();
switch ($this->getTransactionType()) {
case PhabricatorTransactions::TYPE_SUBSCRIBERS;
$tags[] = MetaMTANotificationType::TYPE_DIFFERENTIAL_CC;
break;
case self::TYPE_ACTION:
switch ($this->getNewValue()) {
case DifferentialAction::ACTION_CLOSE:
$tags[] = MetaMTANotificationType::TYPE_DIFFERENTIAL_CLOSED;
break;
}
break;
case self::TYPE_UPDATE:
$old = $this->getOldValue();
if ($old === null) {
$tags[] = MetaMTANotificationType::TYPE_DIFFERENTIAL_REVIEW_REQUEST;
} else {
$tags[] = MetaMTANotificationType::TYPE_DIFFERENTIAL_UPDATED;
}
break;
case PhabricatorTransactions::TYPE_EDGE:
switch ($this->getMetadataValue('edge:type')) {
case DifferentialRevisionHasReviewerEdgeType::EDGECONST:
$tags[] = MetaMTANotificationType::TYPE_DIFFERENTIAL_REVIEWERS;
break;
}
break;
case PhabricatorTransactions::TYPE_COMMENT:
case self::TYPE_INLINE:
$tags[] = MetaMTANotificationType::TYPE_DIFFERENTIAL_COMMENT;
break;
}
if (!$tags) {
$tags[] = MetaMTANotificationType::TYPE_DIFFERENTIAL_OTHER;
}
return $tags;
}
public function getTitle() {
$author_phid = $this->getAuthorPHID();
$author_handle = $this->renderHandleLink($author_phid);
$old = $this->getOldValue();
$new = $this->getNewValue();
switch ($this->getTransactionType()) {
case self::TYPE_INLINE:
return pht(
'%s added inline comments.',
$author_handle);
case self::TYPE_UPDATE:
if ($this->getMetadataValue('isCommitUpdate')) {
return pht(
'This revision was automatically updated to reflect the '.
'committed changes.');
} else if ($new) {
// TODO: Migrate to PHIDs and use handles here?
if (phid_get_type($new) == DifferentialDiffPHIDType::TYPECONST) {
return pht(
'%s updated this revision to %s.',
$author_handle,
$this->renderHandleLink($new));
} else {
return pht(
'%s updated this revision.',
$author_handle);
}
} else {
return pht(
'%s updated this revision.',
$author_handle);
}
case self::TYPE_ACTION:
switch ($new) {
case DifferentialAction::ACTION_CLOSE:
if (!$this->getMetadataValue('isCommitClose')) {
return DifferentialAction::getBasicStoryText(
$new,
$author_handle);
}
$commit_name = $this->renderHandleLink(
$this->getMetadataValue('commitPHID'));
$committer_phid = $this->getMetadataValue('committerPHID');
$author_phid = $this->getMetadataValue('authorPHID');
if ($this->getHandleIfExists($committer_phid)) {
$committer_name = $this->renderHandleLink($committer_phid);
} else {
$committer_name = $this->getMetadataValue('committerName');
}
if ($this->getHandleIfExists($author_phid)) {
$author_name = $this->renderHandleLink($author_phid);
} else {
$author_name = $this->getMetadataValue('authorName');
}
if ($committer_name && ($committer_name != $author_name)) {
return pht(
'Closed by commit %s (authored by %s, committed by %s).',
$commit_name,
$author_name,
$committer_name);
} else {
return pht(
'Closed by commit %s (authored by %s).',
$commit_name,
$author_name);
}
break;
default:
return DifferentialAction::getBasicStoryText($new, $author_handle);
}
break;
case self::TYPE_STATUS:
switch ($this->getNewValue()) {
case ArcanistDifferentialRevisionStatus::ACCEPTED:
- return pht(
- 'This revision is now accepted and ready to land.');
+ return pht('This revision is now accepted and ready to land.');
case ArcanistDifferentialRevisionStatus::NEEDS_REVISION:
- return pht(
- 'This revision now requires changes to proceed.');
+ return pht('This revision now requires changes to proceed.');
case ArcanistDifferentialRevisionStatus::NEEDS_REVIEW:
- return pht(
- 'This revision now requires review to proceed.');
+ return pht('This revision now requires review to proceed.');
}
}
return parent::getTitle();
}
public function renderExtraInformationLink() {
if ($this->getMetadataValue('revisionMatchData')) {
$details_href =
'/differential/revision/closedetails/'.$this->getPHID().'/';
$details_link = javelin_tag(
'a',
array(
'href' => $details_href,
'sigil' => 'workflow',
),
pht('Explain Why'));
return $details_link;
}
return parent::renderExtraInformationLink();
}
public function getTitleForFeed() {
$author_phid = $this->getAuthorPHID();
$object_phid = $this->getObjectPHID();
$old = $this->getOldValue();
$new = $this->getNewValue();
$author_link = $this->renderHandleLink($author_phid);
$object_link = $this->renderHandleLink($object_phid);
switch ($this->getTransactionType()) {
case self::TYPE_INLINE:
return pht(
'%s added inline comments to %s.',
$author_link,
$object_link);
case self::TYPE_UPDATE:
return pht(
'%s updated the diff for %s.',
$author_link,
$object_link);
case self::TYPE_ACTION:
switch ($new) {
case DifferentialAction::ACTION_ACCEPT:
return pht(
'%s accepted %s.',
$author_link,
$object_link);
case DifferentialAction::ACTION_REJECT:
return pht(
'%s requested changes to %s.',
$author_link,
$object_link);
case DifferentialAction::ACTION_RETHINK:
return pht(
'%s planned changes to %s.',
$author_link,
$object_link);
case DifferentialAction::ACTION_ABANDON:
return pht(
'%s abandoned %s.',
$author_link,
$object_link);
case DifferentialAction::ACTION_CLOSE:
if (!$this->getMetadataValue('isCommitClose')) {
return pht(
'%s closed %s.',
$author_link,
$object_link);
} else {
$commit_name = $this->renderHandleLink(
$this->getMetadataValue('commitPHID'));
$committer_phid = $this->getMetadataValue('committerPHID');
$author_phid = $this->getMetadataValue('authorPHID');
if ($this->getHandleIfExists($committer_phid)) {
$committer_name = $this->renderHandleLink($committer_phid);
} else {
$committer_name = $this->getMetadataValue('committerName');
}
if ($this->getHandleIfExists($author_phid)) {
$author_name = $this->renderHandleLink($author_phid);
} else {
$author_name = $this->getMetadataValue('authorName');
}
// Check if the committer and author are the same. They're the
// same if both resolved and are the same user, or if neither
// resolved and the text is identical.
if ($committer_phid && $author_phid) {
$same_author = ($committer_phid == $author_phid);
} else if (!$committer_phid && !$author_phid) {
$same_author = ($committer_name == $author_name);
} else {
$same_author = false;
}
if ($committer_name && !$same_author) {
return pht(
'%s closed %s by committing %s (authored by %s).',
$author_link,
$object_link,
$commit_name,
$author_name);
} else {
return pht(
'%s closed %s by committing %s.',
$author_link,
$object_link,
$commit_name);
}
}
break;
case DifferentialAction::ACTION_REQUEST:
return pht(
'%s requested review of %s.',
$author_link,
$object_link);
case DifferentialAction::ACTION_RECLAIM:
return pht(
'%s reclaimed %s.',
$author_link,
$object_link);
case DifferentialAction::ACTION_RESIGN:
return pht(
'%s resigned from %s.',
$author_link,
$object_link);
case DifferentialAction::ACTION_CLAIM:
return pht(
'%s commandeered %s.',
$author_link,
$object_link);
case DifferentialAction::ACTION_REOPEN:
return pht(
'%s reopened %s.',
$author_link,
$object_link);
}
break;
case self::TYPE_STATUS:
switch ($this->getNewValue()) {
case ArcanistDifferentialRevisionStatus::ACCEPTED:
return pht(
'%s is now accepted and ready to land.',
$object_link);
case ArcanistDifferentialRevisionStatus::NEEDS_REVISION:
return pht(
'%s now requires changes to proceed.',
$object_link);
case ArcanistDifferentialRevisionStatus::NEEDS_REVIEW:
return pht(
'%s now requires review to proceed.',
$object_link);
}
}
return parent::getTitleForFeed();
}
public function getIcon() {
switch ($this->getTransactionType()) {
case self::TYPE_INLINE:
return 'fa-comment';
case self::TYPE_UPDATE:
return 'fa-refresh';
case self::TYPE_STATUS:
switch ($this->getNewValue()) {
case ArcanistDifferentialRevisionStatus::ACCEPTED:
return 'fa-check';
case ArcanistDifferentialRevisionStatus::NEEDS_REVISION:
return 'fa-times';
case ArcanistDifferentialRevisionStatus::NEEDS_REVIEW:
return 'fa-undo';
}
break;
case self::TYPE_ACTION:
switch ($this->getNewValue()) {
case DifferentialAction::ACTION_CLOSE:
return 'fa-check';
case DifferentialAction::ACTION_ACCEPT:
return 'fa-check-circle-o';
case DifferentialAction::ACTION_REJECT:
return 'fa-times-circle-o';
case DifferentialAction::ACTION_ABANDON:
return 'fa-plane';
case DifferentialAction::ACTION_RETHINK:
return 'fa-headphones';
case DifferentialAction::ACTION_REQUEST:
return 'fa-refresh';
case DifferentialAction::ACTION_RECLAIM:
case DifferentialAction::ACTION_REOPEN:
return 'fa-bullhorn';
case DifferentialAction::ACTION_RESIGN:
return 'fa-flag';
case DifferentialAction::ACTION_CLAIM:
return 'fa-flag';
}
case PhabricatorTransactions::TYPE_EDGE:
switch ($this->getMetadataValue('edge:type')) {
case DifferentialRevisionHasReviewerEdgeType::EDGECONST:
return 'fa-user';
}
}
return parent::getIcon();
}
public function shouldDisplayGroupWith(array $group) {
// Never group status changes with other types of actions, they're indirect
// and don't make sense when combined with direct actions.
$type_status = self::TYPE_STATUS;
if ($this->getTransactionType() == $type_status) {
return false;
}
foreach ($group as $xaction) {
if ($xaction->getTransactionType() == $type_status) {
return false;
}
}
return parent::shouldDisplayGroupWith($group);
}
public function getColor() {
switch ($this->getTransactionType()) {
case self::TYPE_UPDATE:
return PhabricatorTransactions::COLOR_SKY;
case self::TYPE_STATUS:
switch ($this->getNewValue()) {
case ArcanistDifferentialRevisionStatus::ACCEPTED:
return PhabricatorTransactions::COLOR_GREEN;
case ArcanistDifferentialRevisionStatus::NEEDS_REVISION:
return PhabricatorTransactions::COLOR_RED;
case ArcanistDifferentialRevisionStatus::NEEDS_REVIEW:
return PhabricatorTransactions::COLOR_ORANGE;
}
break;
case self::TYPE_ACTION:
switch ($this->getNewValue()) {
case DifferentialAction::ACTION_CLOSE:
return PhabricatorTransactions::COLOR_INDIGO;
case DifferentialAction::ACTION_ACCEPT:
return PhabricatorTransactions::COLOR_GREEN;
case DifferentialAction::ACTION_REJECT:
return PhabricatorTransactions::COLOR_RED;
case DifferentialAction::ACTION_ABANDON:
return PhabricatorTransactions::COLOR_INDIGO;
case DifferentialAction::ACTION_RETHINK:
return PhabricatorTransactions::COLOR_RED;
case DifferentialAction::ACTION_REQUEST:
return PhabricatorTransactions::COLOR_SKY;
case DifferentialAction::ACTION_RECLAIM:
return PhabricatorTransactions::COLOR_SKY;
case DifferentialAction::ACTION_REOPEN:
return PhabricatorTransactions::COLOR_SKY;
case DifferentialAction::ACTION_RESIGN:
return PhabricatorTransactions::COLOR_ORANGE;
case DifferentialAction::ACTION_CLAIM:
return PhabricatorTransactions::COLOR_YELLOW;
}
}
return parent::getColor();
}
public function getNoEffectDescription() {
switch ($this->getTransactionType()) {
case PhabricatorTransactions::TYPE_EDGE:
switch ($this->getMetadataValue('edge:type')) {
case DifferentialRevisionHasReviewerEdgeType::EDGECONST:
return pht(
'The reviewers you are trying to add are already reviewing '.
'this revision.');
}
break;
case self::TYPE_ACTION:
switch ($this->getNewValue()) {
case DifferentialAction::ACTION_CLOSE:
return pht('This revision is already closed.');
case DifferentialAction::ACTION_ABANDON:
return pht('This revision has already been abandoned.');
case DifferentialAction::ACTION_RECLAIM:
return pht(
'You can not reclaim this revision because his revision is '.
'not abandoned.');
case DifferentialAction::ACTION_REOPEN:
return pht(
'You can not reopen this revision because this revision is '.
'not closed.');
case DifferentialAction::ACTION_RETHINK:
return pht('This revision already requires changes.');
case DifferentialAction::ACTION_REQUEST:
return pht('Review is already requested for this revision.');
case DifferentialAction::ACTION_RESIGN:
return pht(
'You can not resign from this revision because you are not '.
'a reviewer.');
case DifferentialAction::ACTION_CLAIM:
return pht(
'You can not commandeer this revision because you already own '.
'it.');
case DifferentialAction::ACTION_ACCEPT:
- return pht(
- 'You have already accepted this revision.');
+ return pht('You have already accepted this revision.');
case DifferentialAction::ACTION_REJECT:
- return pht(
- 'You have already requested changes to this revision.');
+ return pht('You have already requested changes to this revision.');
}
break;
}
return parent::getNoEffectDescription();
}
public function renderAsTextForDoorkeeper(
DoorkeeperFeedStoryPublisher $publisher,
PhabricatorFeedStory $story,
array $xactions) {
$body = parent::renderAsTextForDoorkeeper($publisher, $story, $xactions);
$inlines = array();
foreach ($xactions as $xaction) {
if ($xaction->getTransactionType() == self::TYPE_INLINE) {
$inlines[] = $xaction;
}
}
// TODO: This is a bit gross, but far less bad than it used to be. It
// could be further cleaned up at some point.
if ($inlines) {
$engine = PhabricatorMarkupEngine::newMarkupEngine(array())
->setConfig('viewer', new PhabricatorUser())
->setMode(PhutilRemarkupEngine::MODE_TEXT);
$body .= "\n\n";
$body .= pht('Inline Comments');
$body .= "\n";
$changeset_ids = array();
foreach ($inlines as $inline) {
$changeset_ids[] = $inline->getComment()->getChangesetID();
}
$changesets = id(new DifferentialChangeset())->loadAllWhere(
'id IN (%Ld)',
$changeset_ids);
foreach ($inlines as $inline) {
$comment = $inline->getComment();
$changeset = idx($changesets, $comment->getChangesetID());
if (!$changeset) {
continue;
}
$filename = $changeset->getDisplayFilename();
$linenumber = $comment->getLineNumber();
$inline_text = $engine->markupText($comment->getContent());
$inline_text = rtrim($inline_text);
$body .= "{$filename}:{$linenumber} {$inline_text}\n";
}
}
return $body;
}
}
diff --git a/src/applications/differential/view/DifferentialDiffTableOfContentsView.php b/src/applications/differential/view/DifferentialDiffTableOfContentsView.php
index 94b9907ec..953444a19 100644
--- a/src/applications/differential/view/DifferentialDiffTableOfContentsView.php
+++ b/src/applications/differential/view/DifferentialDiffTableOfContentsView.php
@@ -1,325 +1,325 @@
<?php
final class DifferentialDiffTableOfContentsView extends AphrontView {
private $changesets = array();
private $visibleChangesets = array();
private $references = array();
private $repository;
private $diff;
private $renderURI = '/differential/changeset/';
private $revisionID;
private $whitespace;
private $unitTestData;
public function setChangesets($changesets) {
$this->changesets = $changesets;
return $this;
}
public function setVisibleChangesets($visible_changesets) {
$this->visibleChangesets = $visible_changesets;
return $this;
}
public function setRenderingReferences(array $references) {
$this->references = $references;
return $this;
}
public function setRepository(PhabricatorRepository $repository) {
$this->repository = $repository;
return $this;
}
public function setDiff(DifferentialDiff $diff) {
$this->diff = $diff;
return $this;
}
public function setUnitTestData($unit_test_data) {
$this->unitTestData = $unit_test_data;
return $this;
}
public function setRevisionID($revision_id) {
$this->revisionID = $revision_id;
return $this;
}
public function setWhitespace($whitespace) {
$this->whitespace = $whitespace;
return $this;
}
public function render() {
$this->requireResource('differential-core-view-css');
$this->requireResource('differential-table-of-contents-css');
$rows = array();
$coverage = array();
if ($this->unitTestData) {
$coverage_by_file = array();
foreach ($this->unitTestData as $result) {
$test_coverage = idx($result, 'coverage');
if (!$test_coverage) {
continue;
}
foreach ($test_coverage as $file => $results) {
$coverage_by_file[$file][] = $results;
}
}
foreach ($coverage_by_file as $file => $coverages) {
$coverage[$file] = ArcanistUnitTestResult::mergeCoverage($coverages);
}
}
$changesets = $this->changesets;
$paths = array();
foreach ($changesets as $id => $changeset) {
$type = $changeset->getChangeType();
$ftype = $changeset->getFileType();
$ref = idx($this->references, $id);
$display_file = $changeset->getDisplayFilename();
$meta = null;
if (DifferentialChangeType::isOldLocationChangeType($type)) {
$away = $changeset->getAwayPaths();
if (count($away) > 1) {
$meta = array();
if ($type == DifferentialChangeType::TYPE_MULTICOPY) {
$meta[] = pht('Deleted after being copied to multiple locations:');
} else {
$meta[] = pht('Copied to multiple locations:');
}
foreach ($away as $path) {
$meta[] = $path;
}
$meta = phutil_implode_html(phutil_tag('br'), $meta);
} else {
if ($type == DifferentialChangeType::TYPE_MOVE_AWAY) {
$display_file = $this->renderRename(
$display_file,
reset($away),
"\xE2\x86\x92");
} else {
$meta = pht('Copied to %s', reset($away));
}
}
} else if ($type == DifferentialChangeType::TYPE_MOVE_HERE) {
$old_file = $changeset->getOldFile();
$display_file = $this->renderRename(
$display_file,
$old_file,
"\xE2\x86\x90");
} else if ($type == DifferentialChangeType::TYPE_COPY_HERE) {
$meta = pht('Copied from %s', $changeset->getOldFile());
}
$link = $this->renderChangesetLink($changeset, $ref, $display_file);
$line_count = $changeset->getAffectedLineCount();
if ($line_count == 0) {
$lines = '';
} else {
$lines = ' '.pht('(%d line(s))', $line_count);
}
$char = DifferentialChangeType::getSummaryCharacterForChangeType($type);
$chartitle = DifferentialChangeType::getFullNameForChangeType($type);
$desc = DifferentialChangeType::getShortNameForFileType($ftype);
if ($desc) {
$desc = '('.$desc.')';
}
$pchar =
($changeset->getOldProperties() === $changeset->getNewProperties())
? ''
: phutil_tag(
- 'span',
- array('title' => pht('Properties Changed')),
- 'M');
+ 'span',
+ array('title' => pht('Properties Changed')),
+ 'M');
$fname = $changeset->getFilename();
$cov = $this->renderCoverage($coverage, $fname);
if ($cov === null) {
$mcov = $cov = phutil_tag('em', array(), '-');
} else {
$mcov = phutil_tag(
'div',
array(
'id' => 'differential-mcoverage-'.md5($fname),
'class' => 'differential-mcoverage-loading',
),
(isset($this->visibleChangesets[$id]) ?
pht('Loading...') : pht('?')));
}
if ($meta) {
$meta = phutil_tag(
'div',
array(
'class' => 'differential-toc-meta',
),
$meta);
}
if ($this->diff && $this->repository) {
$paths[] =
$changeset->getAbsoluteRepositoryPath($this->repository, $this->diff);
}
$rows[] = array(
$char,
$pchar,
$desc,
array($link, $lines, $meta),
$cov,
$mcov,
);
}
$editor_link = null;
if ($paths && $this->user) {
$editor_link = $this->user->loadEditorLink(
$paths,
1, // line number
$this->repository->getCallsign());
if ($editor_link) {
$editor_link =
phutil_tag(
'a',
array(
'href' => $editor_link,
'class' => 'button differential-toc-edit-all',
),
pht('Open All in Editor'));
}
}
$reveal_link = javelin_tag(
'a',
array(
'sigil' => 'differential-reveal-all',
'mustcapture' => true,
'class' => 'button differential-toc-reveal-all',
),
pht('Show All Context'));
$buttons = phutil_tag(
'div',
array(
'class' => 'differential-toc-buttons grouped',
),
array(
$editor_link,
$reveal_link,
));
$table = id(new AphrontTableView($rows));
$table->setHeaders(
array(
'',
'',
'',
pht('Path'),
pht('Coverage (All)'),
pht('Coverage (Touched)'),
));
$table->setColumnClasses(
array(
'differential-toc-char center',
'differential-toc-prop center',
'differential-toc-ftype center',
'differential-toc-file wide',
'differential-toc-cov',
'differential-toc-cov',
));
$table->setDeviceVisibility(
array(
true,
true,
true,
true,
false,
false,
));
$anchor = id(new PhabricatorAnchorView())
- ->setAnchorName('toc')
- ->setNavigationMarker(true);
+ ->setAnchorName('toc')
+ ->setNavigationMarker(true);
return id(new PHUIObjectBoxView())
->setHeaderText(pht('Table of Contents'))
->appendChild($anchor)
->appendChild($table)
->appendChild($buttons);
}
private function renderRename($display_file, $other_file, $arrow) {
$old = explode('/', $display_file);
$new = explode('/', $other_file);
$start = count($old);
foreach ($old as $index => $part) {
if (!isset($new[$index]) || $part != $new[$index]) {
$start = $index;
break;
}
}
$end = count($old);
foreach (array_reverse($old) as $from_end => $part) {
$index = count($new) - $from_end - 1;
if (!isset($new[$index]) || $part != $new[$index]) {
$end = $from_end;
break;
}
}
$rename =
'{'.
implode('/', array_slice($old, $start, count($old) - $end - $start)).
' '.$arrow.' '.
implode('/', array_slice($new, $start, count($new) - $end - $start)).
'}';
array_splice($new, $start, count($new) - $end - $start, $rename);
return implode('/', $new);
}
private function renderCoverage(array $coverage, $file) {
$info = idx($coverage, $file);
if (!$info) {
return null;
}
$not_covered = substr_count($info, 'U');
$covered = substr_count($info, 'C');
if (!$not_covered && !$covered) {
return null;
}
return sprintf('%d%%', 100 * ($covered / ($covered + $not_covered)));
}
private function renderChangesetLink(
DifferentialChangeset $changeset,
$ref,
$display_file) {
return javelin_tag(
'a',
array(
'href' => '#'.$changeset->getAnchorName(),
'sigil' => 'differential-load',
'meta' => array(
'id' => 'diff-'.$changeset->getAnchorName(),
),
),
$display_file);
}
}
diff --git a/src/applications/differential/view/DifferentialRevisionUpdateHistoryView.php b/src/applications/differential/view/DifferentialRevisionUpdateHistoryView.php
index 4f99c19c7..6f8ee6db9 100644
--- a/src/applications/differential/view/DifferentialRevisionUpdateHistoryView.php
+++ b/src/applications/differential/view/DifferentialRevisionUpdateHistoryView.php
@@ -1,439 +1,439 @@
<?php
final class DifferentialRevisionUpdateHistoryView extends AphrontView {
private $diffs = array();
private $selectedVersusDiffID;
private $selectedDiffID;
private $selectedWhitespace;
private $commitsForLinks = array();
public function setDiffs(array $diffs) {
assert_instances_of($diffs, 'DifferentialDiff');
$this->diffs = $diffs;
return $this;
}
public function setSelectedVersusDiffID($id) {
$this->selectedVersusDiffID = $id;
return $this;
}
public function setSelectedDiffID($id) {
$this->selectedDiffID = $id;
return $this;
}
public function setSelectedWhitespace($whitespace) {
$this->selectedWhitespace = $whitespace;
return $this;
}
public function setCommitsForLinks(array $commits) {
assert_instances_of($commits, 'PhabricatorRepositoryCommit');
$this->commitsForLinks = $commits;
return $this;
}
public function render() {
$this->requireResource('differential-core-view-css');
$this->requireResource('differential-revision-history-css');
$data = array(
array(
'name' => 'Base',
'id' => null,
'desc' => 'Base',
'age' => null,
'obj' => null,
),
);
$seq = 0;
foreach ($this->diffs as $diff) {
$data[] = array(
'name' => 'Diff '.(++$seq),
'id' => $diff->getID(),
'desc' => $diff->getDescription(),
'age' => $diff->getDateCreated(),
'obj' => $diff,
);
}
$max_id = $diff->getID();
$revision_id = $diff->getRevisionID();
$idx = 0;
$rows = array();
$disable = false;
$radios = array();
$last_base = null;
$rowc = array();
foreach ($data as $row) {
$diff = $row['obj'];
$name = $row['name'];
$id = $row['id'];
$old_class = false;
$new_class = false;
if ($id) {
$new_checked = ($this->selectedDiffID == $id);
$new = javelin_tag(
'input',
array(
'type' => 'radio',
'name' => 'id',
'value' => $id,
'checked' => $new_checked ? 'checked' : null,
'sigil' => 'differential-new-radio',
));
if ($new_checked) {
$new_class = true;
$disable = true;
}
$new = phutil_tag(
'div',
array(
'class' => 'differential-update-history-radio',
),
$new);
} else {
$new = null;
}
if ($max_id != $id) {
$uniq = celerity_generate_unique_node_id();
$old_checked = ($this->selectedVersusDiffID == $id);
$old = phutil_tag(
'input',
array(
'type' => 'radio',
'name' => 'vs',
'value' => $id,
'id' => $uniq,
'checked' => $old_checked ? 'checked' : null,
'disabled' => $disable ? 'disabled' : null,
));
$radios[] = $uniq;
if ($old_checked) {
$old_class = true;
}
$old = phutil_tag(
'div',
array(
'class' => 'differential-update-history-radio',
),
$old);
} else {
$old = null;
}
$desc = $row['desc'];
if ($row['age']) {
$age = phabricator_datetime($row['age'], $this->getUser());
} else {
$age = null;
}
if ($diff) {
$lint = self::renderDiffLintStar($row['obj']);
$lint = phutil_tag(
'div',
array(
'class' => 'lintunit-star',
'title' => self::getDiffLintMessage($diff),
),
$lint);
$unit = self::renderDiffUnitStar($row['obj']);
$unit = phutil_tag(
'div',
array(
'class' => 'lintunit-star',
'title' => self::getDiffUnitMessage($diff),
),
$unit);
$base = $this->renderBaseRevision($diff);
} else {
$lint = null;
$unit = null;
$base = null;
}
if ($last_base !== null && $base !== $last_base) {
// TODO: Render some kind of notice about rebases.
}
$last_base = $base;
if ($revision_id) {
$id_link = phutil_tag(
'a',
array(
'href' => '/D'.$revision_id.'?id='.$id,
),
$id);
} else {
$id_link = phutil_tag(
'a',
array(
'href' => '/differential/diff/'.$id.'/',
),
$id);
}
$rows[] = array(
$name,
$id_link,
$base,
$desc,
$age,
$lint,
$unit,
$old,
$new,
);
$classes = array();
if ($old_class) {
$classes[] = 'differential-update-history-old-now';
}
if ($new_class) {
$classes[] = 'differential-update-history-new-now';
}
$rowc[] = nonempty(implode(' ', $classes), null);
}
Javelin::initBehavior(
'differential-diff-radios',
array(
'radios' => $radios,
));
$options = array(
DifferentialChangesetParser::WHITESPACE_IGNORE_ALL => pht('Ignore All'),
DifferentialChangesetParser::WHITESPACE_IGNORE_MOST => pht('Ignore Most'),
DifferentialChangesetParser::WHITESPACE_IGNORE_TRAILING =>
pht('Ignore Trailing'),
DifferentialChangesetParser::WHITESPACE_SHOW_ALL => pht('Show All'),
);
foreach ($options as $value => $label) {
$options[$value] = phutil_tag(
'option',
array(
'value' => $value,
'selected' => ($value == $this->selectedWhitespace)
? 'selected'
: null,
),
$label);
}
$select = phutil_tag('select', array('name' => 'whitespace'), $options);
$table = id(new AphrontTableView($rows));
$table->setHeaders(
array(
pht('Diff'),
pht('ID'),
pht('Base'),
pht('Description'),
pht('Created'),
pht('Lint'),
pht('Unit'),
'',
'',
));
$table->setColumnClasses(
array(
'pri',
'',
'',
'wide',
'date',
'center',
'center',
'center differential-update-history-old',
'center differential-update-history-new',
));
$table->setRowClasses($rowc);
$table->setDeviceVisibility(
array(
true,
true,
false,
true,
false,
false,
false,
true,
true,
));
$show_diff = phutil_tag(
'div',
array(
'class' => 'differential-update-history-footer',
),
array(
phutil_tag(
'label',
array(),
array(
pht('Whitespace Changes:'),
$select,
)),
phutil_tag(
'button',
array(),
pht('Show Diff')),
));
$content = phabricator_form(
$this->getUser(),
array(
'action' => '#toc',
),
array(
$table,
$show_diff,
));
return id(new PHUIObjectBoxView())
->setHeaderText(pht('Revision Update History'))
->setFlush(true)
->appendChild($content);
}
const STAR_NONE = 'none';
const STAR_OKAY = 'okay';
const STAR_WARN = 'warn';
const STAR_FAIL = 'fail';
const STAR_SKIP = 'skip';
public static function renderDiffLintStar(DifferentialDiff $diff) {
static $map = array(
DifferentialLintStatus::LINT_NONE => self::STAR_NONE,
DifferentialLintStatus::LINT_OKAY => self::STAR_OKAY,
DifferentialLintStatus::LINT_WARN => self::STAR_WARN,
DifferentialLintStatus::LINT_FAIL => self::STAR_FAIL,
DifferentialLintStatus::LINT_SKIP => self::STAR_SKIP,
DifferentialLintStatus::LINT_AUTO_SKIP => self::STAR_SKIP,
DifferentialLintStatus::LINT_POSTPONED => self::STAR_SKIP,
);
$star = idx($map, $diff->getLintStatus(), self::STAR_FAIL);
return self::renderDiffStar($star);
}
public static function renderDiffUnitStar(DifferentialDiff $diff) {
static $map = array(
DifferentialUnitStatus::UNIT_NONE => self::STAR_NONE,
DifferentialUnitStatus::UNIT_OKAY => self::STAR_OKAY,
DifferentialUnitStatus::UNIT_WARN => self::STAR_WARN,
DifferentialUnitStatus::UNIT_FAIL => self::STAR_FAIL,
DifferentialUnitStatus::UNIT_SKIP => self::STAR_SKIP,
DifferentialUnitStatus::UNIT_AUTO_SKIP => self::STAR_SKIP,
DifferentialUnitStatus::UNIT_POSTPONED => self::STAR_SKIP,
);
$star = idx($map, $diff->getUnitStatus(), self::STAR_FAIL);
return self::renderDiffStar($star);
}
public static function getDiffLintMessage(DifferentialDiff $diff) {
switch ($diff->getLintStatus()) {
case DifferentialLintStatus::LINT_NONE:
return pht('No Linters Available');
case DifferentialLintStatus::LINT_OKAY:
return pht('Lint OK');
case DifferentialLintStatus::LINT_WARN:
return pht('Lint Warnings');
case DifferentialLintStatus::LINT_FAIL:
return pht('Lint Errors');
case DifferentialLintStatus::LINT_SKIP:
return pht('Lint Skipped');
case DifferentialLintStatus::LINT_AUTO_SKIP:
return pht('Automatic diff as part of commit; lint not applicable.');
case DifferentialLintStatus::LINT_POSTPONED:
return pht('Lint Postponed');
}
- return '???';
+ return pht('Unknown');
}
public static function getDiffUnitMessage(DifferentialDiff $diff) {
switch ($diff->getUnitStatus()) {
case DifferentialUnitStatus::UNIT_NONE:
return pht('No Unit Test Coverage');
case DifferentialUnitStatus::UNIT_OKAY:
return pht('Unit Tests OK');
case DifferentialUnitStatus::UNIT_WARN:
return pht('Unit Test Warnings');
case DifferentialUnitStatus::UNIT_FAIL:
return pht('Unit Test Errors');
case DifferentialUnitStatus::UNIT_SKIP:
return pht('Unit Tests Skipped');
case DifferentialUnitStatus::UNIT_AUTO_SKIP:
return pht(
'Automatic diff as part of commit; unit tests not applicable.');
case DifferentialUnitStatus::UNIT_POSTPONED:
return pht('Unit Tests Postponed');
}
- return '???';
+ return pht('Unknown');
}
private static function renderDiffStar($star) {
$class = 'diff-star-'.$star;
return phutil_tag(
'span',
array('class' => $class),
"\xE2\x98\x85");
}
private function renderBaseRevision(DifferentialDiff $diff) {
switch ($diff->getSourceControlSystem()) {
case 'git':
$base = $diff->getSourceControlBaseRevision();
if (strpos($base, '@') === false) {
$label = substr($base, 0, 7);
} else {
// The diff is from git-svn
$base = explode('@', $base);
$base = last($base);
$label = $base;
}
break;
case 'svn':
$base = $diff->getSourceControlBaseRevision();
$base = explode('@', $base);
$base = last($base);
$label = $base;
break;
default:
$label = null;
break;
}
$link = null;
if ($label) {
$commit_for_link = idx(
$this->commitsForLinks,
$diff->getSourceControlBaseRevision());
if ($commit_for_link) {
$link = phutil_tag(
'a',
array('href' => $commit_for_link->getURI()),
$label);
} else {
$link = $label;
}
}
return $link;
}
}
diff --git a/src/applications/differential/view/DifferentialTransactionView.php b/src/applications/differential/view/DifferentialTransactionView.php
index 3312f717c..ea0b2bb89 100644
--- a/src/applications/differential/view/DifferentialTransactionView.php
+++ b/src/applications/differential/view/DifferentialTransactionView.php
@@ -1,168 +1,168 @@
<?php
final class DifferentialTransactionView
extends PhabricatorApplicationTransactionView {
private $changesets;
private $revision;
private $rightDiff;
private $leftDiff;
public function setLeftDiff(DifferentialDiff $left_diff) {
$this->leftDiff = $left_diff;
return $this;
}
public function getLeftDiff() {
return $this->leftDiff;
}
public function setRightDiff(DifferentialDiff $right_diff) {
$this->rightDiff = $right_diff;
return $this;
}
public function getRightDiff() {
return $this->rightDiff;
}
public function setRevision(DifferentialRevision $revision) {
$this->revision = $revision;
return $this;
}
public function getRevision() {
return $this->revision;
}
public function setChangesets(array $changesets) {
assert_instances_of($changesets, 'DifferentialChangeset');
$this->changesets = $changesets;
return $this;
}
public function getChangesets() {
return $this->changesets;
}
// TODO: There's a whole lot of code duplication between this and
// PholioTransactionView to handle inlines. Merge this into the core? Some of
// it can probably be shared, while other parts are trickier.
protected function shouldGroupTransactions(
PhabricatorApplicationTransaction $u,
PhabricatorApplicationTransaction $v) {
if ($u->getAuthorPHID() != $v->getAuthorPHID()) {
// Don't group transactions by different authors.
return false;
}
if (($v->getDateCreated() - $u->getDateCreated()) > 60) {
// Don't group if transactions that happened more than 60s apart.
return false;
}
switch ($u->getTransactionType()) {
case PhabricatorTransactions::TYPE_COMMENT:
case DifferentialTransaction::TYPE_INLINE:
break;
default:
return false;
}
switch ($v->getTransactionType()) {
case DifferentialTransaction::TYPE_INLINE:
return true;
}
return parent::shouldGroupTransactions($u, $v);
}
protected function renderTransactionContent(
PhabricatorApplicationTransaction $xaction) {
$out = array();
$type_inline = DifferentialTransaction::TYPE_INLINE;
$group = $xaction->getTransactionGroup();
if ($xaction->getTransactionType() == $type_inline) {
array_unshift($group, $xaction);
} else {
$out[] = parent::renderTransactionContent($xaction);
}
if (!$group) {
return $out;
}
$inlines = array();
foreach ($group as $xaction) {
switch ($xaction->getTransactionType()) {
case DifferentialTransaction::TYPE_INLINE:
$inlines[] = $xaction;
break;
default:
- throw new Exception('Unknown grouped transaction type!');
+ throw new Exception(pht('Unknown grouped transaction type!'));
}
}
if ($inlines) {
$inline_view = new PhabricatorInlineSummaryView();
$changesets = $this->getChangesets();
$inline_groups = DifferentialTransactionComment::sortAndGroupInlines(
$inlines,
$changesets);
foreach ($inline_groups as $changeset_id => $group) {
$changeset = $changesets[$changeset_id];
$items = array();
foreach ($group as $inline) {
$comment = $inline->getComment();
$item = array(
'id' => $comment->getID(),
'line' => $comment->getLineNumber(),
'length' => $comment->getLineLength(),
'content' => parent::renderTransactionContent($inline),
);
$changeset_diff_id = $changeset->getDiffID();
if ($comment->getIsNewFile()) {
$visible_diff_id = $this->getRightDiff()->getID();
} else {
$visible_diff_id = $this->getLeftDiff()->getID();
}
// TODO: We still get one edge case wrong here, when we have a
// versus diff and the file didn't exist in the old version. The
// comment is visible because we show the left side of the target
// diff when there's no corresponding file in the versus diff, but
// we incorrectly link it off-page.
$is_visible = ($changeset_diff_id == $visible_diff_id);
if (!$is_visible) {
$revision_id = $this->getRevision()->getID();
$comment_id = $comment->getID();
$item['href'] =
'/D'.$revision_id.
'?id='.$changeset_diff_id.
'#inline-'.$comment_id;
$item['where'] = pht('(On Diff #%d)', $changeset_diff_id);
}
$items[] = $item;
}
$inline_view->addCommentGroup(
$changeset->getFilename(),
$items);
}
$out[] = $inline_view;
}
return $out;
}
}
diff --git a/src/applications/diffusion/DiffusionLintSaveRunner.php b/src/applications/diffusion/DiffusionLintSaveRunner.php
index 629c88792..7b652cf5f 100644
--- a/src/applications/diffusion/DiffusionLintSaveRunner.php
+++ b/src/applications/diffusion/DiffusionLintSaveRunner.php
@@ -1,306 +1,306 @@
<?php
final class DiffusionLintSaveRunner {
private $arc = 'arc';
private $severity = ArcanistLintSeverity::SEVERITY_ADVICE;
private $all = false;
private $chunkSize = 256;
private $needsBlame = false;
private $svnRoot;
private $lintCommit;
private $branch;
private $conn;
private $deletes = array();
private $inserts = array();
private $blame = array();
public function setArc($path) {
$this->arc = $path;
return $this;
}
public function setSeverity($string) {
$this->severity = $string;
return $this;
}
public function setAll($bool) {
$this->all = $bool;
return $this;
}
public function setChunkSize($number) {
$this->chunkSize = $number;
return $this;
}
public function setNeedsBlame($boolean) {
$this->needsBlame = $boolean;
return $this;
}
public function run($dir) {
$working_copy = ArcanistWorkingCopyIdentity::newFromPath($dir);
$configuration_manager = new ArcanistConfigurationManager();
$configuration_manager->setWorkingCopyIdentity($working_copy);
$api = ArcanistRepositoryAPI::newAPIFromConfigurationManager(
$configuration_manager);
$this->svnRoot = id(new PhutilURI($api->getSourceControlPath()))->getPath();
if ($api instanceof ArcanistGitAPI) {
$svn_fetch = $api->getGitConfig('svn-remote.svn.fetch');
list($this->svnRoot) = explode(':', $svn_fetch);
if ($this->svnRoot != '') {
$this->svnRoot = '/'.$this->svnRoot;
}
}
$callsign = $configuration_manager->getConfigFromAnySource(
'repository.callsign');
$uuid = $api->getRepositoryUUID();
$remote_uri = $api->getRemoteURI();
$repository_query = id(new PhabricatorRepositoryQuery())
->setViewer(PhabricatorUser::getOmnipotentUser());
if ($callsign) {
$repository_query->withCallsigns(array($callsign));
} else if ($uuid) {
$repository_query->withUUIDs(array($uuid));
} else if ($remote_uri) {
$repository_query->withRemoteURIs(array($remote_uri));
}
$repository = $repository_query->executeOne();
$branch_name = $api->getBranchName();
if (!$repository) {
throw new Exception(pht('No repository was found.'));
}
$this->branch = PhabricatorRepositoryBranch::loadOrCreateBranch(
$repository->getID(),
$branch_name);
$this->conn = $this->branch->establishConnection('w');
$this->lintCommit = null;
if (!$this->all) {
$this->lintCommit = $this->branch->getLintCommit();
}
if ($this->lintCommit) {
try {
$commit = $this->lintCommit;
if ($this->svnRoot) {
$commit = $api->getCanonicalRevisionName('@'.$commit);
}
$all_files = $api->getChangedFiles($commit);
} catch (ArcanistCapabilityNotSupportedException $ex) {
$this->lintCommit = null;
}
}
if (!$this->lintCommit) {
$where = ($this->svnRoot
? qsprintf($this->conn, 'AND path LIKE %>', $this->svnRoot.'/')
: '');
queryfx(
$this->conn,
'DELETE FROM %T WHERE branchID = %d %Q',
PhabricatorRepository::TABLE_LINTMESSAGE,
$this->branch->getID(),
$where);
$all_files = $api->getAllFiles();
}
$count = 0;
$files = array();
foreach ($all_files as $file => $val) {
$count++;
if (!$this->lintCommit) {
$file = $val;
} else {
$this->deletes[] = $this->svnRoot.'/'.$file;
if ($val & ArcanistRepositoryAPI::FLAG_DELETED) {
continue;
}
}
$files[$file] = $file;
if (count($files) >= $this->chunkSize) {
$this->runArcLint($files);
$files = array();
}
}
$this->runArcLint($files);
$this->saveLintMessages();
$this->lintCommit = $api->getUnderlyingWorkingCopyRevision();
$this->branch->setLintCommit($this->lintCommit);
$this->branch->save();
if ($this->blame) {
$this->blameAuthors();
$this->blame = array();
}
return $count;
}
private function runArcLint(array $files) {
if (!$files) {
return;
}
echo '.';
try {
$future = new ExecFuture(
'%C lint --severity %s --output json %Ls',
$this->arc,
$this->severity,
$files);
foreach (new LinesOfALargeExecFuture($future) as $json) {
$paths = null;
try {
$paths = phutil_json_decode($json);
} catch (PhutilJSONParserException $ex) {
- fprintf(STDERR, pht("Invalid JSON: %s\n", $json));
+ fprintf(STDERR, pht('Invalid JSON: %s', $json)."\n");
continue;
}
foreach ($paths as $path => $messages) {
if (!isset($files[$path])) {
continue;
}
foreach ($messages as $message) {
$line = idx($message, 'line', 0);
$this->inserts[] = qsprintf(
$this->conn,
'(%d, %s, %d, %s, %s, %s, %s)',
$this->branch->getID(),
$this->svnRoot.'/'.$path,
$line,
idx($message, 'code', ''),
idx($message, 'severity', ''),
idx($message, 'name', ''),
idx($message, 'description', ''));
if ($line && $this->needsBlame) {
$this->blame[$path][$line] = true;
}
}
if (count($this->deletes) >= 1024 || count($this->inserts) >= 256) {
$this->saveLintMessages();
}
}
}
} catch (Exception $ex) {
fprintf(STDERR, $ex->getMessage()."\n");
}
}
private function saveLintMessages() {
$this->conn->openTransaction();
foreach (array_chunk($this->deletes, 1024) as $paths) {
queryfx(
$this->conn,
'DELETE FROM %T WHERE branchID = %d AND path IN (%Ls)',
PhabricatorRepository::TABLE_LINTMESSAGE,
$this->branch->getID(),
$paths);
}
foreach (array_chunk($this->inserts, 256) as $values) {
queryfx(
$this->conn,
'INSERT INTO %T
(branchID, path, line, code, severity, name, description)
VALUES %Q',
PhabricatorRepository::TABLE_LINTMESSAGE,
implode(', ', $values));
}
$this->conn->saveTransaction();
$this->deletes = array();
$this->inserts = array();
}
private function blameAuthors() {
$repository = id(new PhabricatorRepositoryQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withIDs(array($this->branch->getRepositoryID()))
->executeOne();
$queries = array();
$futures = array();
foreach ($this->blame as $path => $lines) {
$drequest = DiffusionRequest::newFromDictionary(array(
'user' => PhabricatorUser::getOmnipotentUser(),
'repository' => $repository,
'branch' => $this->branch->getName(),
'path' => $path,
'commit' => $this->lintCommit,
));
$query = DiffusionFileContentQuery::newFromDiffusionRequest($drequest)
->setNeedsBlame(true);
$queries[$path] = $query;
$futures[$path] = $query->getFileContentFuture();
}
$authors = array();
$futures = id(new FutureIterator($futures))
->limit(8);
foreach ($futures as $path => $future) {
$queries[$path]->loadFileContentFromFuture($future);
list(, $rev_list, $blame_dict) = $queries[$path]->getBlameData();
foreach (array_keys($this->blame[$path]) as $line) {
$commit_identifier = $rev_list[$line - 1];
$author = idx($blame_dict[$commit_identifier], 'authorPHID');
if ($author) {
$authors[$author][$path][] = $line;
}
}
}
if ($authors) {
$this->conn->openTransaction();
foreach ($authors as $author => $paths) {
$where = array();
foreach ($paths as $path => $lines) {
$where[] = qsprintf(
$this->conn,
'(path = %s AND line IN (%Ld))',
$this->svnRoot.'/'.$path,
$lines);
}
queryfx(
$this->conn,
'UPDATE %T SET authorPHID = %s WHERE %Q',
PhabricatorRepository::TABLE_LINTMESSAGE,
$author,
implode(' OR ', $where));
}
$this->conn->saveTransaction();
}
}
}
diff --git a/src/applications/diffusion/conduit/DiffusionBrowseQueryConduitAPIMethod.php b/src/applications/diffusion/conduit/DiffusionBrowseQueryConduitAPIMethod.php
index f8854ac0f..e2d3a941b 100644
--- a/src/applications/diffusion/conduit/DiffusionBrowseQueryConduitAPIMethod.php
+++ b/src/applications/diffusion/conduit/DiffusionBrowseQueryConduitAPIMethod.php
@@ -1,469 +1,469 @@
<?php
final class DiffusionBrowseQueryConduitAPIMethod
extends DiffusionQueryConduitAPIMethod {
public function getAPIMethodName() {
return 'diffusion.browsequery';
}
public function getMethodDescription() {
- return
+ return pht(
'File(s) information for a repository at an (optional) path and '.
- '(optional) commit.';
+ '(optional) commit.');
}
protected function defineReturnType() {
return 'array';
}
protected function defineCustomParamTypes() {
return array(
'path' => 'optional string',
'commit' => 'optional string',
'needValidityOnly' => 'optional bool',
);
}
protected function getResult(ConduitAPIRequest $request) {
$result = parent::getResult($request);
return $result->toDictionary();
}
protected function getGitResult(ConduitAPIRequest $request) {
$drequest = $this->getDiffusionRequest();
$repository = $drequest->getRepository();
$path = $request->getValue('path');
$commit = $request->getValue('commit');
$result = $this->getEmptyResultSet();
if ($path == '') {
// Fast path to improve the performance of the repository view; we know
// the root is always a tree at any commit and always exists.
$stdout = 'tree';
} else {
try {
list($stdout) = $repository->execxLocalCommand(
'cat-file -t %s:%s',
$commit,
$path);
} catch (CommandException $e) {
$stderr = $e->getStdErr();
if (preg_match('/^fatal: Not a valid object name/', $stderr)) {
// Grab two logs, since the first one is when the object was deleted.
list($stdout) = $repository->execxLocalCommand(
'log -n2 --format="%%H" %s -- %s',
$commit,
$path);
$stdout = trim($stdout);
if ($stdout) {
$commits = explode("\n", $stdout);
$result
->setReasonForEmptyResultSet(
DiffusionBrowseResultSet::REASON_IS_DELETED)
->setDeletedAtCommit(idx($commits, 0))
->setExistedAtCommit(idx($commits, 1));
return $result;
}
$result->setReasonForEmptyResultSet(
DiffusionBrowseResultSet::REASON_IS_NONEXISTENT);
return $result;
} else {
throw $e;
}
}
}
if (trim($stdout) == 'blob') {
$result->setReasonForEmptyResultSet(
DiffusionBrowseResultSet::REASON_IS_FILE);
return $result;
}
$result->setIsValidResults(true);
if ($this->shouldOnlyTestValidity($request)) {
return $result;
}
list($stdout) = $repository->execxLocalCommand(
'ls-tree -z -l %s:%s',
$commit,
$path);
$submodules = array();
if (strlen($path)) {
$prefix = rtrim($path, '/').'/';
} else {
$prefix = '';
}
$results = array();
foreach (explode("\0", rtrim($stdout)) as $line) {
// NOTE: Limit to 5 components so we parse filenames with spaces in them
// correctly.
// NOTE: The output uses a mixture of tabs and one-or-more spaces to
// delimit fields.
$parts = preg_split('/\s+/', $line, 5);
if (count($parts) < 5) {
throw new Exception(
pht(
'Expected "<mode> <type> <hash> <size>\t<name>", for ls-tree of '.
'"%s:%s", got: %s',
$commit,
$path,
$line));
}
list($mode, $type, $hash, $size, $name) = $parts;
$path_result = new DiffusionRepositoryPath();
if ($type == 'tree') {
$file_type = DifferentialChangeType::FILE_DIRECTORY;
} else if ($type == 'commit') {
$file_type = DifferentialChangeType::FILE_SUBMODULE;
$submodules[] = $path_result;
} else {
$mode = intval($mode, 8);
if (($mode & 0120000) == 0120000) {
$file_type = DifferentialChangeType::FILE_SYMLINK;
} else {
$file_type = DifferentialChangeType::FILE_NORMAL;
}
}
$path_result->setFullPath($prefix.$name);
$path_result->setPath($name);
$path_result->setHash($hash);
$path_result->setFileType($file_type);
$path_result->setFileSize($size);
$results[] = $path_result;
}
// If we identified submodules, lookup the module info at this commit to
// find their source URIs.
if ($submodules) {
// NOTE: We need to read the file out of git and write it to a temporary
// location because "git config -f" doesn't accept a "commit:path"-style
// argument.
// NOTE: This file may not exist, e.g. because the commit author removed
// it when they added the submodule. See T1448. If it's not present, just
// show the submodule without enriching it. If ".gitmodules" was removed
// it seems to partially break submodules, but the repository as a whole
// continues to work fine and we've seen at least two cases of this in
// the wild.
list($err, $contents) = $repository->execLocalCommand(
'cat-file blob %s:.gitmodules',
$commit);
if (!$err) {
$tmp = new TempFile();
Filesystem::writeFile($tmp, $contents);
list($module_info) = $repository->execxLocalCommand(
'config -l -f %s',
$tmp);
$dict = array();
$lines = explode("\n", trim($module_info));
foreach ($lines as $line) {
list($key, $value) = explode('=', $line, 2);
$parts = explode('.', $key);
$dict[$key] = $value;
}
foreach ($submodules as $path) {
$full_path = $path->getFullPath();
$key = 'submodule.'.$full_path.'.url';
if (isset($dict[$key])) {
$path->setExternalURI($dict[$key]);
}
}
}
}
return $result->setPaths($results);
}
protected function getMercurialResult(ConduitAPIRequest $request) {
$drequest = $this->getDiffusionRequest();
$repository = $drequest->getRepository();
$path = $request->getValue('path');
$commit = $request->getValue('commit');
$result = $this->getEmptyResultSet();
$entire_manifest = id(new DiffusionLowLevelMercurialPathsQuery())
->setRepository($repository)
->withCommit($commit)
->withPath($path)
->execute();
$results = array();
$match_against = trim($path, '/');
$match_len = strlen($match_against);
// For the root, don't trim. For other paths, trim the "/" after we match.
// We need this because Mercurial's canonical paths have no leading "/",
// but ours do.
$trim_len = $match_len ? $match_len + 1 : 0;
foreach ($entire_manifest as $path) {
if (strncmp($path, $match_against, $match_len)) {
continue;
}
if (!strlen($path)) {
continue;
}
$remainder = substr($path, $trim_len);
if (!strlen($remainder)) {
// There is a file with this exact name in the manifest, so clearly
// it's a file.
$result->setReasonForEmptyResultSet(
DiffusionBrowseResultSet::REASON_IS_FILE);
return $result;
}
$parts = explode('/', $remainder);
if (count($parts) == 1) {
$type = DifferentialChangeType::FILE_NORMAL;
} else {
$type = DifferentialChangeType::FILE_DIRECTORY;
}
$results[reset($parts)] = $type;
}
foreach ($results as $key => $type) {
$path_result = new DiffusionRepositoryPath();
$path_result->setPath($key);
$path_result->setFileType($type);
$path_result->setFullPath(ltrim($match_against.'/', '/').$key);
$results[$key] = $path_result;
}
$valid_results = true;
if (empty($results)) {
// TODO: Detect "deleted" by issuing "hg log"?
$result->setReasonForEmptyResultSet(
DiffusionBrowseResultSet::REASON_IS_NONEXISTENT);
$valid_results = false;
}
return $result
->setPaths($results)
->setIsValidResults($valid_results);
}
protected function getSVNResult(ConduitAPIRequest $request) {
$drequest = $this->getDiffusionRequest();
$repository = $drequest->getRepository();
$path = $request->getValue('path');
$commit = $request->getValue('commit');
$result = $this->getEmptyResultSet();
$subpath = $repository->getDetail('svn-subpath');
if ($subpath && strncmp($subpath, $path, strlen($subpath))) {
// If we have a subpath and the path isn't a child of it, it (almost
// certainly) won't exist since we don't track commits which affect
// it. (Even if it exists, return a consistent result.)
$result->setReasonForEmptyResultSet(
DiffusionBrowseResultSet::REASON_IS_UNTRACKED_PARENT);
return $result;
}
$conn_r = $repository->establishConnection('r');
$parent_path = DiffusionPathIDQuery::getParentPath($path);
$path_query = new DiffusionPathIDQuery(
array(
$path,
$parent_path,
));
$path_map = $path_query->loadPathIDs();
$path_id = $path_map[$path];
$parent_path_id = $path_map[$parent_path];
if (empty($path_id)) {
$result->setReasonForEmptyResultSet(
DiffusionBrowseResultSet::REASON_IS_NONEXISTENT);
return $result;
}
if ($commit) {
$slice_clause = 'AND svnCommit <= '.(int)$commit;
} else {
$slice_clause = '';
}
$index = queryfx_all(
$conn_r,
'SELECT pathID, max(svnCommit) maxCommit FROM %T WHERE
repositoryID = %d AND parentID = %d
%Q GROUP BY pathID',
PhabricatorRepository::TABLE_FILESYSTEM,
$repository->getID(),
$path_id,
$slice_clause);
if (!$index) {
if ($path == '/') {
$result->setReasonForEmptyResultSet(
DiffusionBrowseResultSet::REASON_IS_EMPTY);
} else {
// NOTE: The parent path ID is included so this query can take
// advantage of the table's primary key; it is uniquely determined by
// the pathID but if we don't do the lookup ourselves MySQL doesn't have
// the information it needs to avoid a table scan.
$reasons = queryfx_all(
$conn_r,
'SELECT * FROM %T WHERE repositoryID = %d
AND parentID = %d
AND pathID = %d
%Q ORDER BY svnCommit DESC LIMIT 2',
PhabricatorRepository::TABLE_FILESYSTEM,
$repository->getID(),
$parent_path_id,
$path_id,
$slice_clause);
$reason = reset($reasons);
if (!$reason) {
$result->setReasonForEmptyResultSet(
DiffusionBrowseResultSet::REASON_IS_NONEXISTENT);
} else {
$file_type = $reason['fileType'];
if (empty($reason['existed'])) {
$result->setReasonForEmptyResultSet(
DiffusionBrowseResultSet::REASON_IS_DELETED);
$result->setDeletedAtCommit($reason['svnCommit']);
if (!empty($reasons[1])) {
$result->setExistedAtCommit($reasons[1]['svnCommit']);
}
} else if ($file_type == DifferentialChangeType::FILE_DIRECTORY) {
$result->setReasonForEmptyResultSet(
DiffusionBrowseResultSet::REASON_IS_EMPTY);
} else {
$result->setReasonForEmptyResultSet(
DiffusionBrowseResultSet::REASON_IS_FILE);
}
}
}
return $result;
}
$result->setIsValidResults(true);
if ($this->shouldOnlyTestValidity($request)) {
return $result;
}
$sql = array();
foreach ($index as $row) {
$sql[] =
'(pathID = '.(int)$row['pathID'].' AND '.
'svnCommit = '.(int)$row['maxCommit'].')';
}
$browse = queryfx_all(
$conn_r,
'SELECT *, p.path pathName
FROM %T f JOIN %T p ON f.pathID = p.id
WHERE repositoryID = %d
AND parentID = %d
AND existed = 1
AND (%Q)
ORDER BY pathName',
PhabricatorRepository::TABLE_FILESYSTEM,
PhabricatorRepository::TABLE_PATH,
$repository->getID(),
$path_id,
implode(' OR ', $sql));
$loadable_commits = array();
foreach ($browse as $key => $file) {
// We need to strip out directories because we don't store last-modified
// in the filesystem table.
if ($file['fileType'] != DifferentialChangeType::FILE_DIRECTORY) {
$loadable_commits[] = $file['svnCommit'];
$browse[$key]['hasCommit'] = true;
}
}
$commits = array();
$commit_data = array();
if ($loadable_commits) {
// NOTE: Even though these are integers, use '%Ls' because MySQL doesn't
// use the second part of the key otherwise!
$commits = id(new PhabricatorRepositoryCommit())->loadAllWhere(
'repositoryID = %d AND commitIdentifier IN (%Ls)',
$repository->getID(),
$loadable_commits);
$commits = mpull($commits, null, 'getCommitIdentifier');
if ($commits) {
$commit_data = id(new PhabricatorRepositoryCommitData())->loadAllWhere(
'commitID in (%Ld)',
mpull($commits, 'getID'));
$commit_data = mpull($commit_data, null, 'getCommitID');
} else {
$commit_data = array();
}
}
$path_normal = DiffusionPathIDQuery::normalizePath($path);
$results = array();
foreach ($browse as $file) {
$full_path = $file['pathName'];
$file_path = ltrim(substr($full_path, strlen($path_normal)), '/');
$full_path = ltrim($full_path, '/');
$result_path = new DiffusionRepositoryPath();
$result_path->setPath($file_path);
$result_path->setFullPath($full_path);
// $result_path->setHash($hash);
$result_path->setFileType($file['fileType']);
// $result_path->setFileSize($size);
if (!empty($file['hasCommit'])) {
$commit = idx($commits, $file['svnCommit']);
if ($commit) {
$data = idx($commit_data, $commit->getID());
$result_path->setLastModifiedCommit($commit);
$result_path->setLastCommitData($data);
}
}
$results[] = $result_path;
}
if (empty($results)) {
$result->setReasonForEmptyResultSet(
DiffusionBrowseResultSet::REASON_IS_EMPTY);
}
return $result->setPaths($results);
}
private function getEmptyResultSet() {
return id(new DiffusionBrowseResultSet())
->setPaths(array())
->setReasonForEmptyResultSet(null)
->setIsValidResults(false);
}
private function shouldOnlyTestValidity(ConduitAPIRequest $request) {
return $request->getValue('needValidityOnly', false);
}
}
diff --git a/src/applications/diffusion/conduit/DiffusionCreateCommentConduitAPIMethod.php b/src/applications/diffusion/conduit/DiffusionCreateCommentConduitAPIMethod.php
index 8d978e3f2..5543b759d 100644
--- a/src/applications/diffusion/conduit/DiffusionCreateCommentConduitAPIMethod.php
+++ b/src/applications/diffusion/conduit/DiffusionCreateCommentConduitAPIMethod.php
@@ -1,100 +1,106 @@
<?php
final class DiffusionCreateCommentConduitAPIMethod
extends DiffusionConduitAPIMethod {
public function getAPIMethodName() {
return 'diffusion.createcomment';
}
public function getMethodStatus() {
return self::METHOD_STATUS_DEPRECATED;
}
public function getMethodDescription() {
- return 'Add a comment to a Diffusion commit. By specifying an action of '.
- '"concern", "accept", "resign", or "close", auditing actions can '.
- 'be triggered. Defaults to "comment".';
+ return pht(
+ 'Add a comment to a Diffusion commit. By specifying an action '.
+ 'of "%s", "%s", "%s", or "%s", auditing actions can '.
+ 'be triggered. Defaults to "%s".',
+ 'concern',
+ 'accept',
+ 'resign',
+ 'close',
+ 'comment');
}
protected function defineParamTypes() {
return array(
'phid' => 'required string',
'action' => 'optional string',
'message' => 'required string',
'silent' => 'optional bool',
);
}
protected function defineReturnType() {
return 'bool';
}
protected function defineErrorTypes() {
return array(
- 'ERR_BAD_COMMIT' => 'No commit found with that PHID',
- 'ERR_BAD_ACTION' => 'Invalid action type',
- 'ERR_MISSING_MESSAGE' => 'Message is required',
+ 'ERR_BAD_COMMIT' => pht('No commit found with that PHID.'),
+ 'ERR_BAD_ACTION' => pht('Invalid action type.'),
+ 'ERR_MISSING_MESSAGE' => pht('Message is required.'),
);
}
protected function execute(ConduitAPIRequest $request) {
$commit_phid = $request->getValue('phid');
$commit = id(new DiffusionCommitQuery())
->setViewer($request->getUser())
->withPHIDs(array($commit_phid))
->needAuditRequests(true)
->executeOne();
if (!$commit) {
throw new ConduitException('ERR_BAD_COMMIT');
}
$message = trim($request->getValue('message'));
if (!$message) {
throw new ConduitException('ERR_MISSING_MESSAGE');
}
$action = $request->getValue('action');
if (!$action) {
$action = PhabricatorAuditActionConstants::COMMENT;
}
// Disallow ADD_CCS, ADD_AUDITORS forever.
if (!in_array($action, array(
PhabricatorAuditActionConstants::CONCERN,
PhabricatorAuditActionConstants::ACCEPT,
PhabricatorAuditActionConstants::COMMENT,
PhabricatorAuditActionConstants::RESIGN,
PhabricatorAuditActionConstants::CLOSE,
))) {
throw new ConduitException('ERR_BAD_ACTION');
}
$xactions = array();
if ($action != PhabricatorAuditActionConstants::COMMENT) {
$xactions[] = id(new PhabricatorAuditTransaction())
->setTransactionType(PhabricatorAuditActionConstants::ACTION)
->setNewValue($action);
}
if (strlen($message)) {
$xactions[] = id(new PhabricatorAuditTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_COMMENT)
->attachComment(
id(new PhabricatorAuditTransactionComment())
->setCommitPHID($commit->getPHID())
->setContent($message));
}
id(new PhabricatorAuditEditor())
->setActor($request->getUser())
->setContentSourceFromConduitRequest($request)
->setDisableEmail($request->getValue('silent'))
->setContinueOnMissingFields(true)
->applyTransactions($commit, $xactions);
return true;
}
}
diff --git a/src/applications/diffusion/conduit/DiffusionDiffQueryConduitAPIMethod.php b/src/applications/diffusion/conduit/DiffusionDiffQueryConduitAPIMethod.php
index 0ec00bc76..19295f2b2 100644
--- a/src/applications/diffusion/conduit/DiffusionDiffQueryConduitAPIMethod.php
+++ b/src/applications/diffusion/conduit/DiffusionDiffQueryConduitAPIMethod.php
@@ -1,239 +1,239 @@
<?php
final class DiffusionDiffQueryConduitAPIMethod
extends DiffusionQueryConduitAPIMethod {
private $effectiveCommit;
public function getAPIMethodName() {
return 'diffusion.diffquery';
}
public function getMethodDescription() {
- return
+ return pht(
'Get diff information from a repository for a specific path at an '.
- '(optional) commit.';
+ '(optional) commit.');
}
protected function defineReturnType() {
return 'array';
}
protected function defineCustomParamTypes() {
return array(
'path' => 'required string',
'commit' => 'optional string',
);
}
protected function getResult(ConduitAPIRequest $request) {
$result = parent::getResult($request);
return array(
'changes' => mpull($result, 'toDictionary'),
'effectiveCommit' => $this->getEffectiveCommit($request),
);
}
protected function getGitResult(ConduitAPIRequest $request) {
return $this->getGitOrMercurialResult($request);
}
protected function getMercurialResult(ConduitAPIRequest $request) {
return $this->getGitOrMercurialResult($request);
}
/**
* NOTE: We have to work particularly hard for SVN as compared to other VCS.
* That's okay but means this shares little code with the other VCS.
*/
protected function getSVNResult(ConduitAPIRequest $request) {
$drequest = $this->getDiffusionRequest();
$repository = $drequest->getRepository();
$effective_commit = $this->getEffectiveCommit($request);
if (!$effective_commit) {
return $this->getEmptyResult();
}
$drequest = clone $drequest;
$drequest->updateSymbolicCommit($effective_commit);
$path_change_query = DiffusionPathChangeQuery::newFromDiffusionRequest(
$drequest);
$path_changes = $path_change_query->loadChanges();
$path = null;
foreach ($path_changes as $change) {
if ($change->getPath() == $drequest->getPath()) {
$path = $change;
}
}
if (!$path) {
return $this->getEmptyResult();
}
$change_type = $path->getChangeType();
switch ($change_type) {
case DifferentialChangeType::TYPE_MULTICOPY:
case DifferentialChangeType::TYPE_DELETE:
if ($path->getTargetPath()) {
$old = array(
$path->getTargetPath(),
$path->getTargetCommitIdentifier(),
);
} else {
$old = array($path->getPath(), $path->getCommitIdentifier() - 1);
}
$old_name = $path->getPath();
$new_name = '';
$new = null;
break;
case DifferentialChangeType::TYPE_ADD:
$old = null;
$new = array($path->getPath(), $path->getCommitIdentifier());
$old_name = '';
$new_name = $path->getPath();
break;
case DifferentialChangeType::TYPE_MOVE_HERE:
case DifferentialChangeType::TYPE_COPY_HERE:
$old = array(
$path->getTargetPath(),
$path->getTargetCommitIdentifier(),
);
$new = array($path->getPath(), $path->getCommitIdentifier());
$old_name = $path->getTargetPath();
$new_name = $path->getPath();
break;
case DifferentialChangeType::TYPE_MOVE_AWAY:
$old = array(
$path->getPath(),
$path->getCommitIdentifier() - 1,
);
$old_name = $path->getPath();
$new_name = null;
$new = null;
break;
default:
$old = array($path->getPath(), $path->getCommitIdentifier() - 1);
$new = array($path->getPath(), $path->getCommitIdentifier());
$old_name = $path->getPath();
$new_name = $path->getPath();
break;
}
$futures = array(
'old' => $this->buildSVNContentFuture($old),
'new' => $this->buildSVNContentFuture($new),
);
$futures = array_filter($futures);
foreach (new FutureIterator($futures) as $key => $future) {
$stdout = '';
try {
list($stdout) = $future->resolvex();
} catch (CommandException $e) {
if ($path->getFileType() != DifferentialChangeType::FILE_DIRECTORY) {
throw $e;
}
}
$futures[$key] = $stdout;
}
$old_data = idx($futures, 'old', '');
$new_data = idx($futures, 'new', '');
$engine = new PhabricatorDifferenceEngine();
$engine->setOldName($old_name);
$engine->setNewName($new_name);
$raw_diff = $engine->generateRawDiffFromFileContent($old_data, $new_data);
$arcanist_changes = DiffusionPathChange::convertToArcanistChanges(
$path_changes);
$parser = $this->getDefaultParser();
$parser->setChanges($arcanist_changes);
$parser->forcePath($path->getPath());
$changes = $parser->parseDiff($raw_diff);
$change = $changes[$path->getPath()];
return array($change);
}
private function getEffectiveCommit(ConduitAPIRequest $request) {
if ($this->effectiveCommit === null) {
$drequest = $this->getDiffusionRequest();
$path = $drequest->getPath();
$result = DiffusionQuery::callConduitWithDiffusionRequest(
$request->getUser(),
$drequest,
'diffusion.lastmodifiedquery',
array(
'paths' => array($path => $drequest->getStableCommit()),
));
$this->effectiveCommit = idx($result, $path);
}
return $this->effectiveCommit;
}
private function buildSVNContentFuture($spec) {
if (!$spec) {
return null;
}
$drequest = $this->getDiffusionRequest();
$repository = $drequest->getRepository();
list($ref, $rev) = $spec;
return $repository->getRemoteCommandFuture(
'cat %s',
$repository->getSubversionPathURI($ref, $rev));
}
private function getGitOrMercurialResult(ConduitAPIRequest $request) {
$drequest = $this->getDiffusionRequest();
$repository = $drequest->getRepository();
$effective_commit = $this->getEffectiveCommit($request);
if (!$effective_commit) {
return $this->getEmptyResult(1);
}
$raw_query = DiffusionRawDiffQuery::newFromDiffusionRequest($drequest)
->setAnchorCommit($effective_commit);
$raw_diff = $raw_query->loadRawDiff();
if (!$raw_diff) {
return $this->getEmptyResult(2);
}
$parser = $this->getDefaultParser();
$changes = $parser->parseDiff($raw_diff);
return $changes;
}
private function getDefaultParser() {
$drequest = $this->getDiffusionRequest();
$repository = $drequest->getRepository();
$parser = new ArcanistDiffParser();
$try_encoding = $repository->getDetail('encoding');
if ($try_encoding) {
$parser->setTryEncoding($try_encoding);
}
$parser->setDetectBinaryFiles(true);
return $parser;
}
private function getEmptyResult() {
return array();
}
}
diff --git a/src/applications/diffusion/conduit/DiffusionExistsQueryConduitAPIMethod.php b/src/applications/diffusion/conduit/DiffusionExistsQueryConduitAPIMethod.php
index d0d5453a3..3c6d09ebd 100644
--- a/src/applications/diffusion/conduit/DiffusionExistsQueryConduitAPIMethod.php
+++ b/src/applications/diffusion/conduit/DiffusionExistsQueryConduitAPIMethod.php
@@ -1,57 +1,57 @@
<?php
final class DiffusionExistsQueryConduitAPIMethod
extends DiffusionQueryConduitAPIMethod {
public function getAPIMethodName() {
return 'diffusion.existsquery';
}
public function getMethodDescription() {
- return 'Determine if code exists in a version control system.';
+ return pht('Determine if code exists in a version control system.');
}
protected function defineReturnType() {
return 'bool';
}
protected function defineCustomParamTypes() {
return array(
'commit' => 'required string',
);
}
protected function getGitResult(ConduitAPIRequest $request) {
$repository = $this->getDiffusionRequest()->getRepository();
$commit = $request->getValue('commit');
list($err, $merge_base) = $repository->execLocalCommand(
'cat-file -t %s',
$commit);
return !$err;
}
protected function getSVNResult(ConduitAPIRequest $request) {
$repository = $this->getDiffusionRequest()->getRepository();
$commit = $request->getValue('commit');
list($info) = $repository->execxRemoteCommand(
'info %s',
$repository->getRemoteURI());
$exists = false;
$matches = null;
if (preg_match('/^Revision: (\d+)$/m', $info, $matches)) {
$base_revision = $matches[1];
$exists = $base_revision >= $commit;
}
return $exists;
}
protected function getMercurialResult(ConduitAPIRequest $request) {
$repository = $this->getDiffusionRequest()->getRepository();
$commit = $request->getValue('commit');
list($err, $stdout) = $repository->execLocalCommand(
'id --rev %s',
$commit);
- return !$err;
+ return !$err;
}
}
diff --git a/src/applications/diffusion/conduit/DiffusionFileContentQueryConduitAPIMethod.php b/src/applications/diffusion/conduit/DiffusionFileContentQueryConduitAPIMethod.php
index 9204a84bb..671ec3ef6 100644
--- a/src/applications/diffusion/conduit/DiffusionFileContentQueryConduitAPIMethod.php
+++ b/src/applications/diffusion/conduit/DiffusionFileContentQueryConduitAPIMethod.php
@@ -1,47 +1,47 @@
<?php
final class DiffusionFileContentQueryConduitAPIMethod
extends DiffusionQueryConduitAPIMethod {
public function getAPIMethodName() {
return 'diffusion.filecontentquery';
}
public function getMethodDescription() {
- return 'Retrieve file content from a repository.';
+ return pht('Retrieve file content from a repository.');
}
protected function defineReturnType() {
return 'array';
}
protected function defineCustomParamTypes() {
return array(
'path' => 'required string',
'commit' => 'required string',
'needsBlame' => 'optional bool',
);
}
protected function getResult(ConduitAPIRequest $request) {
$drequest = $this->getDiffusionRequest();
$needs_blame = $request->getValue('needsBlame');
$file_query = DiffusionFileContentQuery::newFromDiffusionRequest(
$drequest);
$file_query
->setViewer($request->getUser())
->setNeedsBlame($needs_blame);
$file_content = $file_query->loadFileContent();
if ($needs_blame) {
list($text_list, $rev_list, $blame_dict) = $file_query->getBlameData();
} else {
$text_list = $rev_list = $blame_dict = array();
}
$file_content
->setBlameDict($blame_dict)
->setRevList($rev_list)
->setTextList($text_list);
return $file_content->toDictionary();
}
}
diff --git a/src/applications/diffusion/conduit/DiffusionGetCommitsConduitAPIMethod.php b/src/applications/diffusion/conduit/DiffusionGetCommitsConduitAPIMethod.php
index cdc89807c..15dd1f53f 100644
--- a/src/applications/diffusion/conduit/DiffusionGetCommitsConduitAPIMethod.php
+++ b/src/applications/diffusion/conduit/DiffusionGetCommitsConduitAPIMethod.php
@@ -1,293 +1,293 @@
<?php
final class DiffusionGetCommitsConduitAPIMethod
extends DiffusionConduitAPIMethod {
public function getAPIMethodName() {
return 'diffusion.getcommits';
}
public function getMethodDescription() {
return pht('Retrieve Diffusion commit information.');
}
public function getMethodStatus() {
return self::METHOD_STATUS_DEPRECATED;
}
public function getMethodStatusDescription() {
- return pht('Obsoleted by diffusion.querycommits.');
+ return pht('Obsoleted by %s.', 'diffusion.querycommits');
}
protected function defineParamTypes() {
return array(
'commits' => 'required list<string>',
);
}
protected function defineReturnType() {
return 'nonempty list<dict<string, wild>>';
}
protected function execute(ConduitAPIRequest $request) {
$results = array();
$commits = $request->getValue('commits');
$commits = array_fill_keys($commits, array());
foreach ($commits as $name => $info) {
$matches = null;
if (!preg_match('/^r([A-Z]+)([0-9a-f]+)\z/', $name, $matches)) {
$results[$name] = array(
'error' => 'ERR-UNPARSEABLE',
);
unset($commits[$name]);
continue;
}
$commits[$name] = array(
'callsign' => $matches[1],
'commitIdentifier' => $matches[2],
);
}
if (!$commits) {
return $results;
}
$callsigns = ipull($commits, 'callsign');
$callsigns = array_unique($callsigns);
$repos = id(new PhabricatorRepositoryQuery())
->setViewer($request->getUser())
->withCallsigns($callsigns)
->execute();
$repos = mpull($repos, null, 'getCallsign');
foreach ($commits as $name => $info) {
$repo = idx($repos, $info['callsign']);
if (!$repo) {
$results[$name] = $info + array(
'error' => 'ERR-UNKNOWN-REPOSITORY',
);
unset($commits[$name]);
continue;
}
$commits[$name] += array(
'repositoryPHID' => $repo->getPHID(),
'repositoryID' => $repo->getID(),
);
}
if (!$commits) {
return $results;
}
// Execute a complicated query to figure out the primary commit information
// for each referenced commit.
$cdata = $this->queryCommitInformation($commits, $repos);
// We've built the queries so that each row also has the identifier we used
// to select it, which might be a git prefix rather than a full identifier.
$ref_map = ipull($cdata, 'commitIdentifier', 'commitRef');
$cobjs = id(new PhabricatorRepositoryCommit())->loadAllFromArray($cdata);
$cobjs = mgroup($cobjs, 'getRepositoryID', 'getCommitIdentifier');
foreach ($commits as $name => $commit) {
// Expand short git names into full identifiers. For SVN this map is just
// the identity.
$full_identifier = idx($ref_map, $commit['commitIdentifier']);
$repo_id = $commit['repositoryID'];
unset($commits[$name]['repositoryID']);
if (empty($full_identifier) ||
empty($cobjs[$commit['repositoryID']][$full_identifier])) {
$results[$name] = $commit + array(
'error' => 'ERR-UNKNOWN-COMMIT',
);
unset($commits[$name]);
continue;
}
$cobj_arr = $cobjs[$commit['repositoryID']][$full_identifier];
$cobj = head($cobj_arr);
$commits[$name] += array(
'epoch' => $cobj->getEpoch(),
'commitPHID' => $cobj->getPHID(),
'commitID' => $cobj->getID(),
);
// Upgrade git short references into full commit identifiers.
$identifier = $cobj->getCommitIdentifier();
$commits[$name]['commitIdentifier'] = $identifier;
$callsign = $commits[$name]['callsign'];
$uri = "/r{$callsign}{$identifier}";
$commits[$name]['uri'] = PhabricatorEnv::getProductionURI($uri);
}
if (!$commits) {
return $results;
}
$commits = $this->addRepositoryCommitDataInformation($commits);
$commits = $this->addDifferentialInformation($commits, $request);
$commits = $this->addManiphestInformation($commits);
foreach ($commits as $name => $commit) {
$results[$name] = $commit;
}
return $results;
}
/**
* Retrieve primary commit information for all referenced commits.
*/
private function queryCommitInformation(array $commits, array $repos) {
assert_instances_of($repos, 'PhabricatorRepository');
$conn_r = id(new PhabricatorRepositoryCommit())->establishConnection('r');
$repos = mpull($repos, null, 'getID');
$groups = array();
foreach ($commits as $name => $commit) {
$groups[$commit['repositoryID']][] = $commit['commitIdentifier'];
}
// NOTE: MySQL goes crazy and does a massive table scan if we build a more
// sensible version of this query. Make sure the query plan is OK if you
// attempt to reduce the craziness here. METANOTE: The addition of prefix
// selection for Git further complicates matters.
$query = array();
$commit_table = id(new PhabricatorRepositoryCommit())->getTableName();
foreach ($groups as $repository_id => $identifiers) {
$vcs = $repos[$repository_id]->getVersionControlSystem();
$is_git = ($vcs == PhabricatorRepositoryType::REPOSITORY_TYPE_GIT);
if ($is_git) {
foreach ($identifiers as $identifier) {
if (strlen($identifier) < 7) {
// Don't bother with silly stuff like 'rX2', which will select
// 1/16th of all commits. Note that with length 7 we'll still get
// collisions in repositories at the tens-of-thousands-of-commits
// scale.
continue;
}
$query[] = qsprintf(
$conn_r,
'SELECT %T.*, %s commitRef
FROM %T WHERE repositoryID = %d
AND commitIdentifier LIKE %>',
$commit_table,
$identifier,
$commit_table,
$repository_id,
$identifier);
}
} else {
$query[] = qsprintf(
$conn_r,
'SELECT %T.*, commitIdentifier commitRef
FROM %T WHERE repositoryID = %d
AND commitIdentifier IN (%Ls)',
$commit_table,
$commit_table,
$repository_id,
$identifiers);
}
}
return queryfx_all(
$conn_r,
'%Q',
implode(' UNION ALL ', $query));
}
/**
* Enhance the commit list with RepositoryCommitData information.
*/
private function addRepositoryCommitDataInformation(array $commits) {
$commit_ids = ipull($commits, 'commitID');
$data = id(new PhabricatorRepositoryCommitData())->loadAllWhere(
'commitID in (%Ld)',
$commit_ids);
$data = mpull($data, null, 'getCommitID');
foreach ($commits as $name => $commit) {
if (isset($data[$commit['commitID']])) {
$dobj = $data[$commit['commitID']];
$commits[$name] += array(
'commitMessage' => $dobj->getCommitMessage(),
'commitDetails' => $dobj->getCommitDetails(),
);
}
// Remove this information so we don't expose it via the API since
// external services shouldn't be storing internal Commit IDs.
unset($commits[$name]['commitID']);
}
return $commits;
}
/**
* Enhance the commit list with Differential information.
*/
private function addDifferentialInformation(
array $commits,
ConduitAPIRequest $request) {
$commit_phids = ipull($commits, 'commitPHID');
$revisions = id(new DifferentialRevisionQuery())
->setViewer($request->getUser())
->withCommitPHIDs($commit_phids)
->needCommitPHIDs(true)
->execute();
$rev_phid_commit_phids_map = mpull($revisions, 'getCommitPHIDs', 'getPHID');
$revisions = mpull($revisions, null, 'getPHID');
foreach ($rev_phid_commit_phids_map as $rev_phid => $commit_phids) {
foreach ($commits as $name => $commit) {
$commit_phid = $commit['commitPHID'];
if (in_array($commit_phid, $commit_phids)) {
$revision = $revisions[$rev_phid];
$commits[$name] += array(
'differentialRevisionID' => 'D'.$revision->getID(),
'differentialRevisionPHID' => $revision->getPHID(),
);
}
}
}
return $commits;
}
/**
* Enhances the commits list with Maniphest information.
*/
private function addManiphestInformation(array $commits) {
$task_type = DiffusionCommitHasTaskEdgeType::EDGECONST;
$commit_phids = ipull($commits, 'commitPHID');
$edge_query = id(new PhabricatorEdgeQuery())
->withSourcePHIDs($commit_phids)
->withEdgeTypes(array($task_type));
$edges = $edge_query->execute();
foreach ($commits as $name => $commit) {
$task_phids = $edge_query->getDestinationPHIDs(
array($commit['commitPHID']),
array($task_type));
$commits[$name] += array(
'taskPHIDs' => $task_phids,
);
}
return $commits;
}
}
diff --git a/src/applications/diffusion/conduit/DiffusionGetRecentCommitsByPathConduitAPIMethod.php b/src/applications/diffusion/conduit/DiffusionGetRecentCommitsByPathConduitAPIMethod.php
index 89e40c69b..002805ac4 100644
--- a/src/applications/diffusion/conduit/DiffusionGetRecentCommitsByPathConduitAPIMethod.php
+++ b/src/applications/diffusion/conduit/DiffusionGetRecentCommitsByPathConduitAPIMethod.php
@@ -1,65 +1,66 @@
<?php
final class DiffusionGetRecentCommitsByPathConduitAPIMethod
extends DiffusionConduitAPIMethod {
const DEFAULT_LIMIT = 10;
public function getAPIMethodName() {
return 'diffusion.getrecentcommitsbypath';
}
public function getMethodDescription() {
- return 'Get commit identifiers for recent commits affecting a given path.';
+ return pht(
+ 'Get commit identifiers for recent commits affecting a given path.');
}
protected function defineParamTypes() {
return array(
'callsign' => 'required string',
'path' => 'required string',
'branch' => 'optional string',
'limit' => 'optional int',
);
}
protected function defineReturnType() {
return 'nonempty list<string>';
}
protected function execute(ConduitAPIRequest $request) {
$drequest = DiffusionRequest::newFromDictionary(
array(
'user' => $request->getUser(),
'callsign' => $request->getValue('callsign'),
'path' => $request->getValue('path'),
'branch' => $request->getValue('branch'),
));
$limit = nonempty(
$request->getValue('limit'),
self::DEFAULT_LIMIT);
$history_result = DiffusionQuery::callConduitWithDiffusionRequest(
$request->getUser(),
$drequest,
'diffusion.historyquery',
array(
'commit' => $drequest->getCommit(),
'path' => $drequest->getPath(),
'offset' => 0,
'limit' => $limit,
'needDirectChanges' => true,
'needChildChanges' => true,
));
$history = DiffusionPathChange::newFromConduit(
$history_result['pathChanges']);
$raw_commit_identifiers = mpull($history, 'getCommitIdentifier');
$result = array();
foreach ($raw_commit_identifiers as $id) {
$result[] = 'r'.$request->getValue('callsign').$id;
}
return $result;
}
}
diff --git a/src/applications/diffusion/conduit/DiffusionHistoryQueryConduitAPIMethod.php b/src/applications/diffusion/conduit/DiffusionHistoryQueryConduitAPIMethod.php
index 939a47ab6..dcc8f56e8 100644
--- a/src/applications/diffusion/conduit/DiffusionHistoryQueryConduitAPIMethod.php
+++ b/src/applications/diffusion/conduit/DiffusionHistoryQueryConduitAPIMethod.php
@@ -1,269 +1,270 @@
<?php
final class DiffusionHistoryQueryConduitAPIMethod
extends DiffusionQueryConduitAPIMethod {
private $parents = array();
public function getAPIMethodName() {
return 'diffusion.historyquery';
}
public function getMethodDescription() {
- return 'Returns history information for a repository at a specific '.
- 'commit and path.';
+ return pht(
+ 'Returns history information for a repository at a specific '.
+ 'commit and path.');
}
protected function defineReturnType() {
return 'array';
}
protected function defineCustomParamTypes() {
return array(
'commit' => 'required string',
'path' => 'required string',
'offset' => 'required int',
'limit' => 'required int',
'needDirectChanges' => 'optional bool',
'needChildChanges' => 'optional bool',
);
}
protected function getResult(ConduitAPIRequest $request) {
$path_changes = parent::getResult($request);
return array(
'pathChanges' => mpull($path_changes, 'toDictionary'),
'parents' => $this->parents,
);
}
protected function getGitResult(ConduitAPIRequest $request) {
$drequest = $this->getDiffusionRequest();
$repository = $drequest->getRepository();
$commit_hash = $request->getValue('commit');
$path = $request->getValue('path');
$offset = $request->getValue('offset');
$limit = $request->getValue('limit');
list($stdout) = $repository->execxLocalCommand(
'log '.
'--skip=%d '.
'-n %d '.
'--pretty=format:%s '.
'%s -- %C',
$offset,
$limit,
'%H:%P',
$commit_hash,
// Git omits merge commits if the path is provided, even if it is empty.
(strlen($path) ? csprintf('%s', $path) : ''));
$lines = explode("\n", trim($stdout));
$lines = array_filter($lines);
if (!$lines) {
return array();
}
$hash_list = array();
$parent_map = array();
foreach ($lines as $line) {
list($hash, $parents) = explode(':', $line);
$hash_list[] = $hash;
$parent_map[$hash] = preg_split('/\s+/', $parents);
}
$this->parents = $parent_map;
return DiffusionQuery::loadHistoryForCommitIdentifiers(
$hash_list,
$drequest);
}
protected function getMercurialResult(ConduitAPIRequest $request) {
$drequest = $this->getDiffusionRequest();
$repository = $drequest->getRepository();
$commit_hash = $request->getValue('commit');
$path = $request->getValue('path');
$offset = $request->getValue('offset');
$limit = $request->getValue('limit');
$path = DiffusionPathIDQuery::normalizePath($path);
$path = ltrim($path, '/');
// NOTE: Older versions of Mercurial give different results for these
// commands (see T1268):
//
// $ hg log -- ''
// $ hg log
//
// All versions of Mercurial give different results for these commands
// (merge commits are excluded with the "." version):
//
// $ hg log -- .
// $ hg log
//
// If we don't have a path component in the query, omit it from the command
// entirely to avoid these inconsistencies.
// NOTE: When viewing the history of a file, we don't use "-b", because
// Mercurial stops history at the branchpoint but we're interested in all
// ancestors. When viewing history of a branch, we do use "-b", and thus
// stop history (this is more consistent with the Mercurial worldview of
// branches).
if (strlen($path)) {
$path_arg = csprintf('-- %s', $path);
$branch_arg = '';
} else {
$path_arg = '';
// NOTE: --branch used to be called --only-branch; use -b for
// compatibility.
$branch_arg = csprintf('-b %s', $drequest->getBranch());
}
list($stdout) = $repository->execxLocalCommand(
'log --debug --template %s --limit %d %C --rev %s %C',
'{node};{parents}\\n',
($offset + $limit), // No '--skip' in Mercurial.
$branch_arg,
hgsprintf('reverse(ancestors(%s))', $commit_hash),
$path_arg);
$stdout = PhabricatorRepository::filterMercurialDebugOutput($stdout);
$lines = explode("\n", trim($stdout));
$lines = array_slice($lines, $offset);
$hash_list = array();
$parent_map = array();
$last = null;
foreach (array_reverse($lines) as $line) {
list($hash, $parents) = explode(';', $line);
$parents = trim($parents);
if (!$parents) {
if ($last === null) {
$parent_map[$hash] = array('...');
} else {
$parent_map[$hash] = array($last);
}
} else {
$parents = preg_split('/\s+/', $parents);
foreach ($parents as $parent) {
list($plocal, $phash) = explode(':', $parent);
if (!preg_match('/^0+$/', $phash)) {
$parent_map[$hash][] = $phash;
}
}
// This may happen for the zeroth commit in repository, both hashes
// are "000000000...".
if (empty($parent_map[$hash])) {
$parent_map[$hash] = array('...');
}
}
// The rendering code expects the first commit to be "mainline", like
// Git. Flip the order so it does the right thing.
$parent_map[$hash] = array_reverse($parent_map[$hash]);
$hash_list[] = $hash;
$last = $hash;
}
$hash_list = array_reverse($hash_list);
$this->parents = $parent_map;
return DiffusionQuery::loadHistoryForCommitIdentifiers(
$hash_list,
$drequest);
}
protected function getSVNResult(ConduitAPIRequest $request) {
$drequest = $this->getDiffusionRequest();
$repository = $drequest->getRepository();
$commit = $request->getValue('commit');
$path = $request->getValue('path');
$offset = $request->getValue('offset');
$limit = $request->getValue('limit');
$need_direct_changes = $request->getValue('needDirectChanges');
$need_child_changes = $request->getValue('needChildChanges');
$conn_r = $repository->establishConnection('r');
$paths = queryfx_all(
$conn_r,
'SELECT id, path FROM %T WHERE pathHash IN (%Ls)',
PhabricatorRepository::TABLE_PATH,
array(md5('/'.trim($path, '/'))));
$paths = ipull($paths, 'id', 'path');
$path_id = idx($paths, '/'.trim($path, '/'));
if (!$path_id) {
return array();
}
$filter_query = '';
if ($need_direct_changes) {
if ($need_child_changes) {
$type = DifferentialChangeType::TYPE_CHILD;
$filter_query = 'AND (isDirect = 1 OR changeType = '.$type.')';
} else {
$filter_query = 'AND (isDirect = 1)';
}
}
$history_data = queryfx_all(
$conn_r,
'SELECT * FROM %T WHERE repositoryID = %d AND pathID = %d
AND commitSequence <= %d
%Q
ORDER BY commitSequence DESC
LIMIT %d, %d',
PhabricatorRepository::TABLE_PATHCHANGE,
$repository->getID(),
$path_id,
$commit ? $commit : 0x7FFFFFFF,
$filter_query,
$offset,
$limit);
$commits = array();
$commit_data = array();
$commit_ids = ipull($history_data, 'commitID');
if ($commit_ids) {
$commits = id(new PhabricatorRepositoryCommit())->loadAllWhere(
'id IN (%Ld)',
$commit_ids);
if ($commits) {
$commit_data = id(new PhabricatorRepositoryCommitData())->loadAllWhere(
'commitID in (%Ld)',
$commit_ids);
$commit_data = mpull($commit_data, null, 'getCommitID');
}
}
$history = array();
foreach ($history_data as $row) {
$item = new DiffusionPathChange();
$commit = idx($commits, $row['commitID']);
if ($commit) {
$item->setCommit($commit);
$item->setCommitIdentifier($commit->getCommitIdentifier());
$data = idx($commit_data, $commit->getID());
if ($data) {
$item->setCommitData($data);
}
}
$item->setChangeType($row['changeType']);
$item->setFileType($row['fileType']);
$history[] = $item;
}
return $history;
}
}
diff --git a/src/applications/diffusion/conduit/DiffusionMergedCommitsQueryConduitAPIMethod.php b/src/applications/diffusion/conduit/DiffusionMergedCommitsQueryConduitAPIMethod.php
index fd618a291..9d2d6caad 100644
--- a/src/applications/diffusion/conduit/DiffusionMergedCommitsQueryConduitAPIMethod.php
+++ b/src/applications/diffusion/conduit/DiffusionMergedCommitsQueryConduitAPIMethod.php
@@ -1,111 +1,111 @@
<?php
final class DiffusionMergedCommitsQueryConduitAPIMethod
extends DiffusionQueryConduitAPIMethod {
public function getAPIMethodName() {
return 'diffusion.mergedcommitsquery';
}
public function getMethodDescription() {
- return
- 'Merged commit information for a specific commit in a repository.';
+ return pht(
+ 'Merged commit information for a specific commit in a repository.');
}
protected function defineReturnType() {
return 'array';
}
protected function defineCustomParamTypes() {
return array(
'commit' => 'required string',
'limit' => 'optional int',
);
}
private function getLimit(ConduitAPIRequest $request) {
// TODO: Paginate this sensibly at some point.
return $request->getValue('limit', 4096);
}
protected function getGitResult(ConduitAPIRequest $request) {
$drequest = $this->getDiffusionRequest();
$repository = $drequest->getRepository();
$commit = $request->getValue('commit');
$limit = $this->getLimit($request);
list($parents) = $repository->execxLocalCommand(
'log -n 1 --format=%s %s',
'%P',
$commit);
$parents = preg_split('/\s+/', trim($parents));
if (count($parents) < 2) {
// This is not a merge commit, so it doesn't merge anything.
return array();
}
// Get all of the commits which are not reachable from the first parent.
// These are the commits this change merges.
$first_parent = head($parents);
list($logs) = $repository->execxLocalCommand(
'log -n %d --format=%s %s %s --',
// NOTE: "+ 1" accounts for the merge commit itself.
$limit + 1,
'%H',
$commit,
'^'.$first_parent);
$hashes = explode("\n", trim($logs));
// Remove the merge commit.
$hashes = array_diff($hashes, array($commit));
$history = DiffusionQuery::loadHistoryForCommitIdentifiers(
$hashes,
$drequest);
return mpull($history, 'toDictionary');
}
protected function getMercurialResult(ConduitAPIRequest $request) {
$drequest = $this->getDiffusionRequest();
$repository = $drequest->getRepository();
$commit = $request->getValue('commit');
$limit = $this->getLimit($request);
list($parents) = $repository->execxLocalCommand(
'parents --template=%s --rev %s',
'{node}\\n',
$commit);
$parents = explode("\n", trim($parents));
if (count($parents) < 2) {
// Not a merge commit.
return array();
}
// NOTE: In Git, the first parent is the "mainline". In Mercurial, the
// second parent is the "mainline" (the way 'git merge' and 'hg merge'
// work is also reversed).
$last_parent = last($parents);
list($logs) = $repository->execxLocalCommand(
'log --template=%s --follow --limit %d --rev %s:0 --prune %s --',
'{node}\\n',
$limit + 1,
$commit,
$last_parent);
$hashes = explode("\n", trim($logs));
// Remove the merge commit.
$hashes = array_diff($hashes, array($commit));
$history = DiffusionQuery::loadHistoryForCommitIdentifiers(
$hashes,
$drequest);
return mpull($history, 'toDictionary');
}
}
diff --git a/src/applications/diffusion/conduit/DiffusionQueryConduitAPIMethod.php b/src/applications/diffusion/conduit/DiffusionQueryConduitAPIMethod.php
index 22c4adad4..e9f5681f9 100644
--- a/src/applications/diffusion/conduit/DiffusionQueryConduitAPIMethod.php
+++ b/src/applications/diffusion/conduit/DiffusionQueryConduitAPIMethod.php
@@ -1,159 +1,159 @@
<?php
abstract class DiffusionQueryConduitAPIMethod
extends DiffusionConduitAPIMethod {
public function shouldAllowPublic() {
return true;
}
public function getMethodStatus() {
return self::METHOD_STATUS_UNSTABLE;
}
public function getMethodStatusDescription() {
return pht(
- 'See T2784 - migrating diffusion working copy calls to conduit methods. '.
+ 'See T2784 - migrating Diffusion working copy calls to conduit methods. '.
'Until that task is completed (and possibly after) these methods are '.
'unstable.');
}
private $diffusionRequest;
private $repository;
protected function setDiffusionRequest(DiffusionRequest $request) {
$this->diffusionRequest = $request;
return $this;
}
protected function getDiffusionRequest() {
return $this->diffusionRequest;
}
protected function getRepository(ConduitAPIRequest $request) {
return $this->getDiffusionRequest()->getRepository();
}
final protected function defineErrorTypes() {
return $this->defineCustomErrorTypes() +
array(
'ERR-UNKNOWN-REPOSITORY' =>
pht('There is no repository with that callsign.'),
'ERR-UNKNOWN-VCS-TYPE' =>
pht('Unknown repository VCS type.'),
'ERR-UNSUPPORTED-VCS' =>
pht('VCS is not supported for this method.'),
);
}
/**
* Subclasses should override this to specify custom error types.
*/
protected function defineCustomErrorTypes() {
return array();
}
final protected function defineParamTypes() {
return $this->defineCustomParamTypes() +
array(
'callsign' => 'required string',
'branch' => 'optional string',
);
}
/**
* Subclasses should override this to specify custom param types.
*/
protected function defineCustomParamTypes() {
return array();
}
/**
* Subclasses should override these methods with the proper result for the
* pertinent version control system, e.g. getGitResult for Git.
*
* If the result is not supported for that VCS, do not implement it. e.g.
* Subversion (SVN) does not support branches.
*/
protected function getGitResult(ConduitAPIRequest $request) {
throw new ConduitException('ERR-UNSUPPORTED-VCS');
}
protected function getSVNResult(ConduitAPIRequest $request) {
throw new ConduitException('ERR-UNSUPPORTED-VCS');
}
protected function getMercurialResult(ConduitAPIRequest $request) {
throw new ConduitException('ERR-UNSUPPORTED-VCS');
}
/**
* This method is final because most queries will need to construct a
* @{class:DiffusionRequest} and use it. Consolidating this codepath and
* enforcing @{method:getDiffusionRequest} works when we need it is good.
*
* @{method:getResult} should be overridden by subclasses as necessary, e.g.
* there is a common operation across all version control systems that
* should occur after @{method:getResult}, like formatting a timestamp.
*/
final protected function execute(ConduitAPIRequest $request) {
$drequest = DiffusionRequest::newFromDictionary(
array(
'user' => $request->getUser(),
'callsign' => $request->getValue('callsign'),
'branch' => $request->getValue('branch'),
'path' => $request->getValue('path'),
'commit' => $request->getValue('commit'),
));
// Figure out whether we're going to handle this request on this device,
// or proxy it to another node in the cluster.
// If this is a cluster request and we need to proxy, we'll explode here
// to prevent infinite recursion.
$is_cluster_request = $request->getIsClusterRequest();
$repository = $drequest->getRepository();
$client = $repository->newConduitClient(
$request->getUser(),
$is_cluster_request);
if ($client) {
// We're proxying, so just make an intracluster call.
return $client->callMethodSynchronous(
$this->getAPIMethodName(),
$request->getAllParameters());
} else {
// We pass this flag on to prevent proxying of any other Conduit calls
// which we need to make in order to respond to this one. Although we
// could safely proxy them, we take a big performance hit in the common
// case, and doing more proxying wouldn't exercise any additional code so
// we wouldn't gain a testability/predictability benefit.
$drequest->setIsClusterRequest($is_cluster_request);
$this->setDiffusionRequest($drequest);
return $this->getResult($request);
}
}
protected function getResult(ConduitAPIRequest $request) {
$repository = $this->getRepository($request);
$result = null;
switch ($repository->getVersionControlSystem()) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
$result = $this->getGitResult($request);
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
$result = $this->getMercurialResult($request);
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
$result = $this->getSVNResult($request);
break;
default:
throw new ConduitException('ERR-UNKNOWN-VCS-TYPE');
break;
}
return $result;
}
}
diff --git a/src/applications/diffusion/conduit/DiffusionRawDiffQueryConduitAPIMethod.php b/src/applications/diffusion/conduit/DiffusionRawDiffQueryConduitAPIMethod.php
index a7e217d2a..282dc7eb9 100644
--- a/src/applications/diffusion/conduit/DiffusionRawDiffQueryConduitAPIMethod.php
+++ b/src/applications/diffusion/conduit/DiffusionRawDiffQueryConduitAPIMethod.php
@@ -1,59 +1,59 @@
<?php
final class DiffusionRawDiffQueryConduitAPIMethod
extends DiffusionQueryConduitAPIMethod {
public function getAPIMethodName() {
return 'diffusion.rawdiffquery';
}
public function getMethodDescription() {
- return
+ return pht(
'Get raw diff information from a repository for a specific commit at an '.
- '(optional) path.';
+ '(optional) path.');
}
protected function defineReturnType() {
return 'string';
}
protected function defineCustomParamTypes() {
return array(
'commit' => 'required string',
'path' => 'optional string',
'timeout' => 'optional int',
'byteLimit' => 'optional int',
'linesOfContext' => 'optional int',
'againstCommit' => 'optional string',
);
}
protected function getResult(ConduitAPIRequest $request) {
$drequest = $this->getDiffusionRequest();
$raw_query = DiffusionRawDiffQuery::newFromDiffusionRequest($drequest);
$timeout = $request->getValue('timeout');
if ($timeout !== null) {
$raw_query->setTimeout($timeout);
}
$lines_of_context = $request->getValue('linesOfContext');
if ($lines_of_context !== null) {
$raw_query->setLinesOfContext($lines_of_context);
}
$against_commit = $request->getValue('againstCommit');
if ($against_commit !== null) {
$raw_query->setAgainstCommit($against_commit);
}
$byte_limit = $request->getValue('byteLimit');
if ($byte_limit !== null) {
$raw_query->setByteLimit($byte_limit);
}
return $raw_query->loadRawDiff();
}
}
diff --git a/src/applications/diffusion/conduit/DiffusionRefsQueryConduitAPIMethod.php b/src/applications/diffusion/conduit/DiffusionRefsQueryConduitAPIMethod.php
index 2d025e9d4..00bb28b38 100644
--- a/src/applications/diffusion/conduit/DiffusionRefsQueryConduitAPIMethod.php
+++ b/src/applications/diffusion/conduit/DiffusionRefsQueryConduitAPIMethod.php
@@ -1,59 +1,59 @@
<?php
final class DiffusionRefsQueryConduitAPIMethod
extends DiffusionQueryConduitAPIMethod {
public function getAPIMethodName() {
return 'diffusion.refsquery';
}
public function getMethodDescription() {
- return
- 'Query a git repository for ref information at a specific commit.';
+ return pht(
+ 'Query a git repository for ref information at a specific commit.');
}
protected function defineReturnType() {
return 'array';
}
protected function defineCustomParamTypes() {
return array(
'commit' => 'required string',
);
}
protected function getGitResult(ConduitAPIRequest $request) {
$drequest = $this->getDiffusionRequest();
$repository = $drequest->getRepository();
$commit = $request->getValue('commit');
list($stdout) = $repository->execxLocalCommand(
'log --format=%s -n 1 %s --',
'%d',
$commit);
// %d, gives a weird output format
// similar to (remote/one, remote/two, remote/three)
$refs = trim($stdout, "() \n");
if (!$refs) {
return array();
}
$refs = explode(',', $refs);
$refs = array_map('trim', $refs);
$ref_links = array();
foreach ($refs as $ref) {
$ref_links[] = array(
'ref' => $ref,
'href' => $drequest->generateURI(
array(
'action' => 'browse',
'branch' => $ref,
)),
);
}
return $ref_links;
}
}
diff --git a/src/applications/diffusion/conduit/DiffusionSearchQueryConduitAPIMethod.php b/src/applications/diffusion/conduit/DiffusionSearchQueryConduitAPIMethod.php
index 5cf91a10f..16b9762e1 100644
--- a/src/applications/diffusion/conduit/DiffusionSearchQueryConduitAPIMethod.php
+++ b/src/applications/diffusion/conduit/DiffusionSearchQueryConduitAPIMethod.php
@@ -1,118 +1,118 @@
<?php
final class DiffusionSearchQueryConduitAPIMethod
extends DiffusionQueryConduitAPIMethod {
public function getAPIMethodName() {
return 'diffusion.searchquery';
}
public function getMethodDescription() {
- return 'Search (grep) a repository at a specific path and commit.';
+ return pht('Search (grep) a repository at a specific path and commit.');
}
protected function defineReturnType() {
return 'array';
}
protected function defineCustomParamTypes() {
return array(
'path' => 'required string',
'commit' => 'optional string',
'grep' => 'required string',
'limit' => 'optional int',
'offset' => 'optional int',
);
}
protected function defineCustomErrorTypes() {
return array(
- 'ERR-GREP-COMMAND' => 'Grep command failed.',
+ 'ERR-GREP-COMMAND' => pht('Grep command failed.'),
);
}
protected function getResult(ConduitAPIRequest $request) {
try {
$results = parent::getResult($request);
} catch (CommandException $ex) {
throw id(new ConduitException('ERR-GREP-COMMAND'))
->setErrorDescription($ex->getStderr());
}
$offset = $request->getValue('offset');
$results = array_slice($results, $offset);
return $results;
}
protected function getGitResult(ConduitAPIRequest $request) {
$drequest = $this->getDiffusionRequest();
$path = $drequest->getPath();
$grep = $request->getValue('grep');
$repository = $drequest->getRepository();
$limit = $request->getValue('limit');
$offset = $request->getValue('offset');
$results = array();
$future = $repository->getLocalCommandFuture(
// NOTE: --perl-regexp is available only with libpcre compiled in.
'grep --extended-regexp --null -n --no-color -e %s %s -- %s',
$grep,
$drequest->getStableCommit(),
$path);
$binary_pattern = '/Binary file [^:]*:(.+) matches/';
$lines = new LinesOfALargeExecFuture($future);
foreach ($lines as $line) {
$result = null;
if (preg_match('/[^:]*:(.+)\0(.+)\0(.*)/', $line, $result)) {
$results[] = array_slice($result, 1);
} else if (preg_match($binary_pattern, $line, $result)) {
list(, $path) = $result;
$results[] = array($path, null, pht('Binary file'));
} else {
$results[] = array(null, null, $line);
}
if (count($results) >= $offset + $limit) {
break;
}
}
unset($lines);
return $results;
}
protected function getMercurialResult(ConduitAPIRequest $request) {
$drequest = $this->getDiffusionRequest();
$path = $drequest->getPath();
$grep = $request->getValue('grep');
$repository = $drequest->getRepository();
$limit = $request->getValue('limit');
$offset = $request->getValue('offset');
$results = array();
$future = $repository->getLocalCommandFuture(
'grep --rev %s --print0 --line-number %s %s',
hgsprintf('ancestors(%s)', $drequest->getStableCommit()),
$grep,
$path);
$lines = id(new LinesOfALargeExecFuture($future))->setDelimiter("\0");
$parts = array();
foreach ($lines as $line) {
$parts[] = $line;
if (count($parts) == 4) {
list($path, $char_offset, $line, $string) = $parts;
$results[] = array($path, $line, $string);
if (count($results) >= $offset + $limit) {
break;
}
$parts = array();
}
}
unset($lines);
return $results;
}
}
diff --git a/src/applications/diffusion/config/PhabricatorDiffusionConfigOptions.php b/src/applications/diffusion/config/PhabricatorDiffusionConfigOptions.php
index 644556cd6..28a9e1b67 100644
--- a/src/applications/diffusion/config/PhabricatorDiffusionConfigOptions.php
+++ b/src/applications/diffusion/config/PhabricatorDiffusionConfigOptions.php
@@ -1,127 +1,130 @@
<?php
final class PhabricatorDiffusionConfigOptions
extends PhabricatorApplicationConfigOptions {
public function getName() {
return pht('Diffusion');
}
public function getDescription() {
return pht('Configure Diffusion repository browsing.');
}
public function getFontIcon() {
return 'fa-code';
}
public function getGroup() {
return 'apps';
}
public function getOptions() {
return array(
$this->newOption(
'metamta.diffusion.subject-prefix',
'string',
'[Diffusion]')
->setDescription(pht('Subject prefix for Diffusion mail.')),
$this->newOption(
'metamta.diffusion.attach-patches',
'bool',
false)
->setBoolOptions(
array(
pht('Attach Patches'),
pht('Do Not Attach Patches'),
))
- ->setDescription(pht(
- 'Set this to true if you want patches to be attached to commit '.
- 'notifications from Diffusion.')),
+ ->setDescription(
+ pht(
+ 'Set this to true if you want patches to be attached to commit '.
+ 'notifications from Diffusion.')),
$this->newOption('metamta.diffusion.inline-patches', 'int', 0)
->setSummary(pht('Include patches in Diffusion mail as body text.'))
->setDescription(
pht(
'To include patches in Diffusion email bodies, set this to a '.
'positive integer. Patches will be inlined if they are at most '.
'that many lines. By default, patches are not inlined.')),
$this->newOption('metamta.diffusion.byte-limit', 'int', 1024 * 1024)
->setDescription(pht('Hard byte limit on including patches in email.')),
$this->newOption('metamta.diffusion.time-limit', 'int', 60)
->setDescription(pht('Hard time limit on generating patches.')),
$this->newOption(
'audit.can-author-close-audit',
'bool',
false)
->setBoolOptions(
array(
pht('Enable Closing Audits'),
pht('Disable Closing Audits'),
))
->setDescription(pht('Controls whether Author can Close Audits.')),
$this->newOption('bugtraq.url', 'string', null)
->addExample('https://bugs.php.net/%BUGID%', pht('PHP bugs'))
->addExample('/%BUGID%', pht('Local Maniphest URL'))
- ->setDescription(pht(
- 'URL of external bug tracker used by Diffusion. %s will be '.
+ ->setDescription(
+ pht(
+ 'URL of external bug tracker used by Diffusion. %s will be '.
'substituted by the bug ID.',
- '%BUGID%')),
+ '%BUGID%')),
$this->newOption('bugtraq.logregex', 'list<regex>', array())
->addExample(array('/\B#([1-9]\d*)\b/'), pht('Issue #123'))
->addExample(
array('/[Ii]ssues?:?(\s*,?\s*#\d+)+/', '/(\d+)/'),
pht('Issue #123, #456'))
->addExample(array('/(?<!#)\b(T[1-9]\d*)\b/'), pht('Task T123'))
->addExample('/[A-Z]{2,}-\d+/', pht('JIRA-1234'))
- ->setDescription(pht(
- 'Regular expression to link external bug tracker. See '.
+ ->setDescription(
+ pht(
+ 'Regular expression to link external bug tracker. See '.
'http://tortoisesvn.net/docs/release/TortoiseSVN_en/'.
'tsvn-dug-bugtracker.html for further explanation.')),
$this->newOption('diffusion.allow-http-auth', 'bool', false)
->setBoolOptions(
array(
pht('Allow HTTP Basic Auth'),
pht('Disable HTTP Basic Auth'),
))
->setSummary(pht('Enable HTTP Basic Auth for repositories.'))
->setDescription(
pht(
"Phabricator can serve repositories over HTTP, using HTTP basic ".
"auth.\n\n".
"Because HTTP basic auth is less secure than SSH auth, it is ".
"disabled by default. You can enable it here if you'd like to use ".
"it anyway. There's nothing fundamentally insecure about it as ".
"long as Phabricator uses HTTPS, but it presents a much lower ".
"barrier to attackers than SSH does.\n\n".
"Consider using SSH for authenticated access to repositories ".
"instead of HTTP.")),
$this->newOption('diffusion.ssh-user', 'string', null)
->setLocked(true)
->setSummary(pht('Login username for SSH connections to repositories.'))
->setDescription(
pht(
'When constructing clone URIs to show to users, Diffusion will '.
'fill in this login username. If you have configured a VCS user '.
'like `git`, you should provide it here.')),
$this->newOption('diffusion.ssh-port', 'int', null)
->setLocked(true)
->setSummary(pht('Port for SSH connections to repositories.'))
->setDescription(
pht(
'When constructing clone URIs to show to users, Diffusion by '.
'default will not display a port assuming the default for your '.
'VCS. Explicitly declare when running on a non-standard port.')),
$this->newOption('diffusion.ssh-host', 'string', null)
->setLocked(true)
->setSummary(pht('Host for SSH connections to repositories.'))
->setDescription(
pht(
'If you accept Phabricator SSH traffic on a different host '.
'from web traffic (for example, if you use different SSH and '.
'web load balancers), you can set the SSH hostname here. This '.
'is an advanced option.')),
);
}
}
diff --git a/src/applications/diffusion/controller/DiffusionBrowseDirectoryController.php b/src/applications/diffusion/controller/DiffusionBrowseDirectoryController.php
index 833e10864..0d54aba08 100644
--- a/src/applications/diffusion/controller/DiffusionBrowseDirectoryController.php
+++ b/src/applications/diffusion/controller/DiffusionBrowseDirectoryController.php
@@ -1,106 +1,108 @@
<?php
final class DiffusionBrowseDirectoryController
extends DiffusionBrowseController {
private $browseQueryResults;
public function setBrowseQueryResults(DiffusionBrowseResultSet $results) {
$this->browseQueryResults = $results;
return $this;
}
public function getBrowseQueryResults() {
return $this->browseQueryResults;
}
protected function processDiffusionRequest(AphrontRequest $request) {
$drequest = $this->diffusionRequest;
$results = $this->getBrowseQueryResults();
$reason = $results->getReasonForEmptyResultSet();
$content = array();
$actions = $this->buildActionView($drequest);
$properties = $this->buildPropertyView($drequest, $actions);
$object_box = id(new PHUIObjectBoxView())
->setHeader($this->buildHeaderView($drequest))
->addPropertyList($properties);
$content[] = $object_box;
$content[] = $this->renderSearchForm($collapsed = true);
if (!$results->isValidResults()) {
$empty_result = new DiffusionEmptyResultView();
$empty_result->setDiffusionRequest($drequest);
$empty_result->setDiffusionBrowseResultSet($results);
$empty_result->setView($request->getStr('view'));
$content[] = $empty_result;
} else {
$phids = array();
foreach ($results->getPaths() as $result) {
$data = $result->getLastCommitData();
if ($data) {
if ($data->getCommitDetail('authorPHID')) {
$phids[$data->getCommitDetail('authorPHID')] = true;
}
}
}
$phids = array_keys($phids);
$handles = $this->loadViewerHandles($phids);
$browse_table = new DiffusionBrowseTableView();
$browse_table->setDiffusionRequest($drequest);
$browse_table->setHandles($handles);
$browse_table->setPaths($results->getPaths());
$browse_table->setUser($request->getUser());
$browse_panel = new PHUIObjectBoxView();
$browse_panel->setHeaderText($drequest->getPath(), '/');
$browse_panel->appendChild($browse_table);
$content[] = $browse_panel;
}
$content[] = $this->buildOpenRevisions();
$readme_path = $results->getReadmePath();
if ($readme_path) {
$readme_content = $this->callConduitWithDiffusionRequest(
'diffusion.filecontentquery',
array(
'path' => $readme_path,
'commit' => $drequest->getStableCommit(),
));
if ($readme_content) {
$content[] = id(new DiffusionReadmeView())
->setUser($this->getViewer())
->setPath($readme_path)
->setContent($readme_content['corpus']);
}
}
$crumbs = $this->buildCrumbs(
array(
'branch' => true,
'path' => true,
'view' => 'browse',
));
return $this->buildApplicationPage(
array(
$crumbs,
$content,
),
array(
'title' => array(
nonempty(basename($drequest->getPath()), '/'),
- $drequest->getRepository()->getCallsign().' Repository',
+ pht(
+ '%s Repository',
+ $drequest->getRepository()->getCallsign()),
),
));
}
}
diff --git a/src/applications/diffusion/controller/DiffusionBrowseFileController.php b/src/applications/diffusion/controller/DiffusionBrowseFileController.php
index 6fcda177a..c55d217bf 100644
--- a/src/applications/diffusion/controller/DiffusionBrowseFileController.php
+++ b/src/applications/diffusion/controller/DiffusionBrowseFileController.php
@@ -1,1099 +1,1102 @@
<?php
final class DiffusionBrowseFileController extends DiffusionBrowseController {
private $lintCommit;
private $lintMessages;
private $coverage;
protected function processDiffusionRequest(AphrontRequest $request) {
$drequest = $this->getDiffusionRequest();
$viewer = $request->getUser();
$before = $request->getStr('before');
if ($before) {
return $this->buildBeforeResponse($before);
}
$path = $drequest->getPath();
$preferences = $viewer->loadPreferences();
$show_blame = $request->getBool(
'blame',
$preferences->getPreference(
PhabricatorUserPreferences::PREFERENCE_DIFFUSION_BLAME,
false));
$show_color = $request->getBool(
'color',
$preferences->getPreference(
PhabricatorUserPreferences::PREFERENCE_DIFFUSION_COLOR,
true));
$view = $request->getStr('view');
if ($request->isFormPost() && $view != 'raw' && $viewer->isLoggedIn()) {
$preferences->setPreference(
PhabricatorUserPreferences::PREFERENCE_DIFFUSION_BLAME,
$show_blame);
$preferences->setPreference(
PhabricatorUserPreferences::PREFERENCE_DIFFUSION_COLOR,
$show_color);
$preferences->save();
$uri = $request->getRequestURI()
->alter('blame', null)
->alter('color', null);
return id(new AphrontRedirectResponse())->setURI($uri);
}
// We need the blame information if blame is on and we're building plain
// text, or blame is on and this is an Ajax request. If blame is on and
// this is a colorized request, we don't show blame at first (we ajax it
// in afterward) so we don't need to query for it.
$needs_blame = ($show_blame && !$show_color) ||
($show_blame && $request->isAjax());
$file_content = DiffusionFileContent::newFromConduit(
$this->callConduitWithDiffusionRequest(
'diffusion.filecontentquery',
array(
'commit' => $drequest->getCommit(),
'path' => $drequest->getPath(),
'needsBlame' => $needs_blame,
)));
$data = $file_content->getCorpus();
if ($view === 'raw') {
return $this->buildRawResponse($path, $data);
}
$this->loadLintMessages();
$this->coverage = $drequest->loadCoverage();
$binary_uri = null;
if (ArcanistDiffUtils::isHeuristicBinaryFile($data)) {
$file = $this->loadFileForData($path, $data);
$file_uri = $file->getBestURI();
if ($file->isViewableImage()) {
$corpus = $this->buildImageCorpus($file_uri);
} else {
$corpus = $this->buildBinaryCorpus($file_uri, $data);
$binary_uri = $file_uri;
}
} else {
// Build the content of the file.
$corpus = $this->buildCorpus(
$show_blame,
$show_color,
$file_content,
$needs_blame,
$drequest,
$path,
$data);
}
if ($request->isAjax()) {
return id(new AphrontAjaxResponse())->setContent($corpus);
}
require_celerity_resource('diffusion-source-css');
// Render the page.
$view = $this->buildActionView($drequest);
$action_list = $this->enrichActionView(
$view,
$drequest,
$show_blame,
$show_color);
$properties = $this->buildPropertyView($drequest, $action_list);
$object_box = id(new PHUIObjectBoxView())
->setHeader($this->buildHeaderView($drequest))
->addPropertyList($properties);
$content = array();
$content[] = $object_box;
$follow = $request->getStr('follow');
if ($follow) {
$notice = new PHUIInfoView();
$notice->setSeverity(PHUIInfoView::SEVERITY_WARNING);
$notice->setTitle(pht('Unable to Continue'));
switch ($follow) {
case 'first':
$notice->appendChild(
- pht('Unable to continue tracing the history of this file because '.
- 'this commit is the first commit in the repository.'));
+ pht(
+ 'Unable to continue tracing the history of this file because '.
+ 'this commit is the first commit in the repository.'));
break;
case 'created':
$notice->appendChild(
- pht('Unable to continue tracing the history of this file because '.
- 'this commit created the file.'));
+ pht(
+ 'Unable to continue tracing the history of this file because '.
+ 'this commit created the file.'));
break;
}
$content[] = $notice;
}
$renamed = $request->getStr('renamed');
if ($renamed) {
$notice = new PHUIInfoView();
$notice->setSeverity(PHUIInfoView::SEVERITY_NOTICE);
$notice->setTitle(pht('File Renamed'));
$notice->appendChild(
- pht("File history passes through a rename from '%s' to '%s'.",
+ pht(
+ "File history passes through a rename from '%s' to '%s'.",
$drequest->getPath(), $renamed));
$content[] = $notice;
}
$content[] = $corpus;
$content[] = $this->buildOpenRevisions();
$crumbs = $this->buildCrumbs(
array(
'branch' => true,
'path' => true,
'view' => 'browse',
));
$basename = basename($this->getDiffusionRequest()->getPath());
return $this->buildApplicationPage(
array(
$crumbs,
$content,
),
array(
'title' => $basename,
));
}
private function loadLintMessages() {
$drequest = $this->getDiffusionRequest();
$branch = $drequest->loadBranch();
if (!$branch || !$branch->getLintCommit()) {
return;
}
$this->lintCommit = $branch->getLintCommit();
$conn = id(new PhabricatorRepository())->establishConnection('r');
$where = '';
if ($drequest->getLint()) {
$where = qsprintf(
$conn,
'AND code = %s',
$drequest->getLint());
}
$this->lintMessages = queryfx_all(
$conn,
'SELECT * FROM %T WHERE branchID = %d %Q AND path = %s',
PhabricatorRepository::TABLE_LINTMESSAGE,
$branch->getID(),
$where,
'/'.$drequest->getPath());
}
private function buildCorpus(
$show_blame,
$show_color,
DiffusionFileContent $file_content,
$needs_blame,
DiffusionRequest $drequest,
$path,
$data) {
if (!$show_color) {
$style =
'border: none; width: 100%; height: 80em; font-family: monospace';
if (!$show_blame) {
$corpus = phutil_tag(
'textarea',
array(
'style' => $style,
),
$file_content->getCorpus());
} else {
$text_list = $file_content->getTextList();
$rev_list = $file_content->getRevList();
$blame_dict = $file_content->getBlameDict();
$rows = array();
foreach ($text_list as $k => $line) {
$rev = $rev_list[$k];
$author = $blame_dict[$rev]['author'];
$rows[] =
sprintf('%-10s %-20s %s', substr($rev, 0, 7), $author, $line);
}
$corpus = phutil_tag(
'textarea',
array(
'style' => $style,
),
implode("\n", $rows));
}
} else {
require_celerity_resource('syntax-highlighting-css');
$text_list = $file_content->getTextList();
$rev_list = $file_content->getRevList();
$blame_dict = $file_content->getBlameDict();
$text_list = implode("\n", $text_list);
$text_list = PhabricatorSyntaxHighlighter::highlightWithFilename(
$path,
$text_list);
$text_list = explode("\n", $text_list);
$rows = $this->buildDisplayRows($text_list, $rev_list, $blame_dict,
$needs_blame, $drequest, $show_blame, $show_color);
$corpus_table = javelin_tag(
'table',
array(
'class' => 'diffusion-source remarkup-code PhabricatorMonospaced',
'sigil' => 'phabricator-source',
),
$rows);
if ($this->getRequest()->isAjax()) {
return $corpus_table;
}
$id = celerity_generate_unique_node_id();
$repo = $drequest->getRepository();
$symbol_repos = nonempty($repo->getSymbolSources(), array());
$symbol_repos[] = $repo;
$lang = last(explode('.', $drequest->getPath()));
$repo_languages = $repo->getSymbolLanguages();
$repo_languages = nonempty($repo_languages, array());
$repo_languages = array_fill_keys($repo_languages, true);
$needs_symbols = true;
if ($repo_languages && $symbol_repos) {
$have_symbols = id(new DiffusionSymbolQuery())
->existsSymbolsInRepository($repo->getPHID());
if (!$have_symbols) {
$needs_symbols = false;
}
}
if ($needs_symbols && $repo_languages) {
$needs_symbols = isset($repo_languages[$lang]);
}
if ($needs_symbols) {
Javelin::initBehavior(
'repository-crossreference',
array(
'container' => $id,
'lang' => $lang,
'repositories' => $symbol_repos,
));
}
$corpus = phutil_tag(
'div',
array(
'id' => $id,
),
$corpus_table);
Javelin::initBehavior('load-blame', array('id' => $id));
}
$edit = $this->renderEditButton();
$file = $this->renderFileButton();
$header = id(new PHUIHeaderView())
->setHeader(pht('File Contents'))
->addActionLink($edit)
->addActionLink($file);
$corpus = id(new PHUIObjectBoxView())
->setHeader($header)
->appendChild($corpus);
return $corpus;
}
private function enrichActionView(
PhabricatorActionListView $view,
DiffusionRequest $drequest,
$show_blame,
$show_color) {
$viewer = $this->getRequest()->getUser();
$base_uri = $this->getRequest()->getRequestURI();
$view->addAction(
id(new PhabricatorActionView())
->setName(pht('Show Last Change'))
->setHref(
$drequest->generateURI(
array(
'action' => 'change',
)))
->setIcon('fa-backward'));
if ($show_blame) {
$blame_text = pht('Disable Blame');
$blame_icon = 'fa-exclamation-circle lightgreytext';
$blame_value = 0;
} else {
$blame_text = pht('Enable Blame');
$blame_icon = 'fa-exclamation-circle';
$blame_value = 1;
}
$view->addAction(
id(new PhabricatorActionView())
->setName($blame_text)
->setHref($base_uri->alter('blame', $blame_value))
->setIcon($blame_icon)
->setUser($viewer)
->setRenderAsForm($viewer->isLoggedIn()));
if ($show_color) {
$highlight_text = pht('Disable Highlighting');
$highlight_icon = 'fa-star-o grey';
$highlight_value = 0;
} else {
$highlight_text = pht('Enable Highlighting');
$highlight_icon = 'fa-star';
$highlight_value = 1;
}
$view->addAction(
id(new PhabricatorActionView())
->setName($highlight_text)
->setHref($base_uri->alter('color', $highlight_value))
->setIcon($highlight_icon)
->setUser($viewer)
->setRenderAsForm($viewer->isLoggedIn()));
$href = null;
if ($this->getRequest()->getStr('lint') !== null) {
$lint_text = pht('Hide %d Lint Message(s)', count($this->lintMessages));
$href = $base_uri->alter('lint', null);
} else if ($this->lintCommit === null) {
$lint_text = pht('Lint not Available');
} else {
$lint_text = pht(
'Show %d Lint Message(s)',
count($this->lintMessages));
$href = $this->getDiffusionRequest()->generateURI(array(
'action' => 'browse',
'commit' => $this->lintCommit,
))->alter('lint', '');
}
$view->addAction(
id(new PhabricatorActionView())
->setName($lint_text)
->setHref($href)
->setIcon('fa-exclamation-triangle')
->setDisabled(!$href));
return $view;
}
private function renderEditButton() {
$request = $this->getRequest();
$user = $request->getUser();
$drequest = $this->getDiffusionRequest();
$repository = $drequest->getRepository();
$path = $drequest->getPath();
$line = nonempty((int)$drequest->getLine(), 1);
$callsign = $repository->getCallsign();
$editor_link = $user->loadEditorLink($path, $line, $callsign);
$template = $user->loadEditorLink($path, '%l', $callsign);
$icon_edit = id(new PHUIIconView())
->setIconFont('fa-pencil');
$button = id(new PHUIButtonView())
->setTag('a')
->setText(pht('Open in Editor'))
->setHref($editor_link)
->setIcon($icon_edit)
->setID('editor_link')
->setMetadata(array('link_template' => $template))
->setDisabled(!$editor_link);
return $button;
}
private function renderFileButton($file_uri = null) {
$base_uri = $this->getRequest()->getRequestURI();
if ($file_uri) {
$text = pht('Download Raw File');
$href = $file_uri;
$icon = 'fa-download';
} else {
$text = pht('View Raw File');
$href = $base_uri->alter('view', 'raw');
$icon = 'fa-file-text';
}
$iconview = id(new PHUIIconView())
->setIconFont($icon);
$button = id(new PHUIButtonView())
->setTag('a')
->setText($text)
->setHref($href)
->setIcon($iconview);
return $button;
}
private function buildDisplayRows(
array $text_list,
array $rev_list,
array $blame_dict,
$needs_blame,
DiffusionRequest $drequest,
$show_blame,
$show_color) {
$handles = array();
if ($blame_dict) {
$epoch_list = ipull(ifilter($blame_dict, 'epoch'), 'epoch');
$epoch_min = min($epoch_list);
$epoch_max = max($epoch_list);
$epoch_range = ($epoch_max - $epoch_min) + 1;
$author_phids = ipull(ifilter($blame_dict, 'authorPHID'), 'authorPHID');
$handles = $this->loadViewerHandles($author_phids);
}
$line_arr = array();
$line_str = $drequest->getLine();
$ranges = explode(',', $line_str);
foreach ($ranges as $range) {
if (strpos($range, '-') !== false) {
list($min, $max) = explode('-', $range, 2);
$line_arr[] = array(
'min' => min($min, $max),
'max' => max($min, $max),
);
} else if (strlen($range)) {
$line_arr[] = array(
'min' => $range,
'max' => $range,
);
}
}
$display = array();
$line_number = 1;
$last_rev = null;
$color = null;
foreach ($text_list as $k => $line) {
$display_line = array(
'epoch' => null,
'commit' => null,
'author' => null,
'target' => null,
'highlighted' => null,
'line' => $line_number,
'data' => $line,
);
if ($show_blame) {
// If the line's rev is same as the line above, show empty content
// with same color; otherwise generate blame info. The newer a change
// is, the more saturated the color.
$rev = idx($rev_list, $k, $last_rev);
if ($last_rev == $rev) {
$display_line['color'] = $color;
} else {
$blame = $blame_dict[$rev];
if (!isset($blame['epoch'])) {
$color = '#ffd'; // Render as warning.
} else {
$color_ratio = ($blame['epoch'] - $epoch_min) / $epoch_range;
$color_value = 0xE6 * (1.0 - $color_ratio);
$color = sprintf(
'#%02x%02x%02x',
$color_value,
0xF6,
$color_value);
}
$display_line['epoch'] = idx($blame, 'epoch');
$display_line['color'] = $color;
$display_line['commit'] = $rev;
$author_phid = idx($blame, 'authorPHID');
if ($author_phid && $handles[$author_phid]) {
$author_link = $handles[$author_phid]->renderLink();
} else {
$author_link = $blame['author'];
}
$display_line['author'] = $author_link;
$last_rev = $rev;
}
}
if ($line_arr) {
if ($line_number == $line_arr[0]['min']) {
$display_line['target'] = true;
}
foreach ($line_arr as $range) {
if ($line_number >= $range['min'] &&
$line_number <= $range['max']) {
$display_line['highlighted'] = true;
}
}
}
$display[] = $display_line;
++$line_number;
}
$request = $this->getRequest();
$viewer = $request->getUser();
$commits = array_filter(ipull($display, 'commit'));
if ($commits) {
$commits = id(new DiffusionCommitQuery())
->setViewer($viewer)
->withRepository($drequest->getRepository())
->withIdentifiers($commits)
->execute();
$commits = mpull($commits, null, 'getCommitIdentifier');
}
$revision_ids = id(new DifferentialRevision())
->loadIDsByCommitPHIDs(mpull($commits, 'getPHID'));
$revisions = array();
if ($revision_ids) {
$revisions = id(new DifferentialRevisionQuery())
->setViewer($viewer)
->withIDs($revision_ids)
->execute();
}
$phids = array();
foreach ($commits as $commit) {
if ($commit->getAuthorPHID()) {
$phids[] = $commit->getAuthorPHID();
}
}
foreach ($revisions as $revision) {
if ($revision->getAuthorPHID()) {
$phids[] = $revision->getAuthorPHID();
}
}
$handles = $this->loadViewerHandles($phids);
Javelin::initBehavior('phabricator-oncopy', array());
$engine = null;
$inlines = array();
if ($this->getRequest()->getStr('lint') !== null && $this->lintMessages) {
$engine = new PhabricatorMarkupEngine();
$engine->setViewer($viewer);
foreach ($this->lintMessages as $message) {
$inline = id(new PhabricatorAuditInlineComment())
->setSyntheticAuthor(
ArcanistLintSeverity::getStringForSeverity($message['severity']).
' '.$message['code'].' ('.$message['name'].')')
->setLineNumber($message['line'])
->setContent($message['description']);
$inlines[$message['line']][] = $inline;
$engine->addObject(
$inline,
PhabricatorInlineCommentInterface::MARKUP_FIELD_BODY);
}
$engine->process();
require_celerity_resource('differential-changeset-view-css');
}
$rows = $this->renderInlines(
idx($inlines, 0, array()),
$show_blame,
(bool)$this->coverage,
$engine);
foreach ($display as $line) {
$line_href = $drequest->generateURI(
array(
'action' => 'browse',
'line' => $line['line'],
'stable' => true,
));
$blame = array();
$style = null;
if (array_key_exists('color', $line)) {
if ($line['color']) {
$style = 'background: '.$line['color'].';';
}
$before_link = null;
$commit_link = null;
$revision_link = null;
if (idx($line, 'commit')) {
$commit = $line['commit'];
if (idx($commits, $commit)) {
$tooltip = $this->renderCommitTooltip(
$commits[$commit],
$handles,
$line['author']);
} else {
$tooltip = null;
}
Javelin::initBehavior('phabricator-tooltips', array());
require_celerity_resource('aphront-tooltip-css');
$commit_link = javelin_tag(
'a',
array(
'href' => $drequest->generateURI(
array(
'action' => 'commit',
'commit' => $line['commit'],
)),
'sigil' => 'has-tooltip',
'meta' => array(
'tip' => $tooltip,
'align' => 'E',
'size' => 600,
),
),
id(new PhutilUTF8StringTruncator())
->setMaximumGlyphs(9)
->setTerminator('')
->truncateString($line['commit']));
$revision_id = null;
if (idx($commits, $commit)) {
$revision_id = idx($revision_ids, $commits[$commit]->getPHID());
}
if ($revision_id) {
$revision = idx($revisions, $revision_id);
if ($revision) {
$tooltip = $this->renderRevisionTooltip($revision, $handles);
$revision_link = javelin_tag(
'a',
array(
'href' => '/D'.$revision->getID(),
'sigil' => 'has-tooltip',
'meta' => array(
'tip' => $tooltip,
'align' => 'E',
'size' => 600,
),
),
'D'.$revision->getID());
}
}
$uri = $line_href->alter('before', $commit);
$before_link = javelin_tag(
'a',
array(
'href' => $uri->setQueryParam('view', 'blame'),
'sigil' => 'has-tooltip',
'meta' => array(
'tip' => pht('Skip Past This Commit'),
'align' => 'E',
'size' => 300,
),
),
"\xC2\xAB");
}
$blame[] = phutil_tag(
'th',
array(
'class' => 'diffusion-blame-link',
),
$before_link);
$object_links = array();
$object_links[] = $commit_link;
if ($revision_link) {
$object_links[] = phutil_tag('span', array(), '/');
$object_links[] = $revision_link;
}
$blame[] = phutil_tag(
'th',
array(
'class' => 'diffusion-rev-link',
),
$object_links);
}
$line_link = phutil_tag(
'a',
array(
'href' => $line_href,
'style' => $style,
),
$line['line']);
$blame[] = javelin_tag(
'th',
array(
'class' => 'diffusion-line-link',
'sigil' => 'phabricator-source-line',
'style' => $style,
),
$line_link);
Javelin::initBehavior('phabricator-line-linker');
if ($line['target']) {
Javelin::initBehavior(
'diffusion-jump-to',
array(
'target' => 'scroll_target',
));
$anchor_text = phutil_tag(
'a',
array(
'id' => 'scroll_target',
),
'');
} else {
$anchor_text = null;
}
$blame[] = phutil_tag(
'td',
array(
),
array(
$anchor_text,
// NOTE: See phabricator-oncopy behavior.
"\xE2\x80\x8B",
// TODO: [HTML] Not ideal.
phutil_safe_html(str_replace("\t", ' ', $line['data'])),
));
if ($this->coverage) {
require_celerity_resource('differential-changeset-view-css');
$cov_index = $line['line'] - 1;
if (isset($this->coverage[$cov_index])) {
$cov_class = $this->coverage[$cov_index];
} else {
$cov_class = 'N';
}
$blame[] = phutil_tag(
'td',
array(
'class' => 'cov cov-'.$cov_class,
),
'');
}
$rows[] = phutil_tag(
'tr',
array(
'class' => ($line['highlighted'] ?
'phabricator-source-highlight' :
null),
),
$blame);
$cur_inlines = $this->renderInlines(
idx($inlines, $line['line'], array()),
$show_blame,
$this->coverage,
$engine);
foreach ($cur_inlines as $cur_inline) {
$rows[] = $cur_inline;
}
}
return $rows;
}
private function renderInlines(
array $inlines,
$needs_blame,
$has_coverage,
$engine) {
$rows = array();
foreach ($inlines as $inline) {
// TODO: This should use modern scaffolding code.
$inline_view = id(new PHUIDiffInlineCommentDetailView())
->setUser($this->getViewer())
->setMarkupEngine($engine)
->setInlineComment($inline)
->render();
$row = array_fill(0, ($needs_blame ? 3 : 1), phutil_tag('th'));
$row[] = phutil_tag('td', array(), $inline_view);
if ($has_coverage) {
$row[] = phutil_tag(
'td',
array(
'class' => 'cov cov-I',
));
}
$rows[] = phutil_tag('tr', array('class' => 'inline'), $row);
}
return $rows;
}
private function loadFileForData($path, $data) {
$file = PhabricatorFile::buildFromFileDataOrHash(
$data,
array(
'name' => basename($path),
'ttl' => time() + 60 * 60 * 24,
'viewPolicy' => PhabricatorPolicies::POLICY_NOONE,
));
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$file->attachToObject(
$this->getDiffusionRequest()->getRepository()->getPHID());
unset($unguarded);
return $file;
}
private function buildRawResponse($path, $data) {
$file = $this->loadFileForData($path, $data);
return $file->getRedirectResponse();
}
private function buildImageCorpus($file_uri) {
$properties = new PHUIPropertyListView();
$properties->addImageContent(
phutil_tag(
'img',
array(
'src' => $file_uri,
)));
$file = $this->renderFileButton($file_uri);
$header = id(new PHUIHeaderView())
->setHeader(pht('Image'))
->addActionLink($file);
return id(new PHUIObjectBoxView())
->setHeader($header)
->addPropertyList($properties);
}
private function buildBinaryCorpus($file_uri, $data) {
$size = new PhutilNumber(strlen($data));
$text = pht('This is a binary file. It is %s byte(s) in length.', $size);
$text = id(new PHUIBoxView())
->addPadding(PHUI::PADDING_LARGE)
->appendChild($text);
$file = $this->renderFileButton($file_uri);
$header = id(new PHUIHeaderView())
->setHeader(pht('Details'))
->addActionLink($file);
$box = id(new PHUIObjectBoxView())
->setHeader($header)
->appendChild($text);
return $box;
}
private function buildBeforeResponse($before) {
$request = $this->getRequest();
$drequest = $this->getDiffusionRequest();
// NOTE: We need to get the grandparent so we can capture filename changes
// in the parent.
$parent = $this->loadParentCommitOf($before);
$old_filename = null;
$was_created = false;
if ($parent) {
$grandparent = $this->loadParentCommitOf($parent);
if ($grandparent) {
$rename_query = new DiffusionRenameHistoryQuery();
$rename_query->setRequest($drequest);
$rename_query->setOldCommit($grandparent);
$rename_query->setViewer($request->getUser());
$old_filename = $rename_query->loadOldFilename();
$was_created = $rename_query->getWasCreated();
}
}
$follow = null;
if ($was_created) {
// If the file was created in history, that means older commits won't
// have it. Since we know it existed at 'before', it must have been
// created then; jump there.
$target_commit = $before;
$follow = 'created';
} else if ($parent) {
// If we found a parent, jump to it. This is the normal case.
$target_commit = $parent;
} else {
// If there's no parent, this was probably created in the initial commit?
// And the "was_created" check will fail because we can't identify the
// grandparent. Keep the user at 'before'.
$target_commit = $before;
$follow = 'first';
}
$path = $drequest->getPath();
$renamed = null;
if ($old_filename !== null &&
$old_filename !== '/'.$path) {
$renamed = $path;
$path = $old_filename;
}
$line = null;
// If there's a follow error, drop the line so the user sees the message.
if (!$follow) {
$line = $this->getBeforeLineNumber($target_commit);
}
$before_uri = $drequest->generateURI(
array(
'action' => 'browse',
'commit' => $target_commit,
'line' => $line,
'path' => $path,
));
$before_uri->setQueryParams($request->getRequestURI()->getQueryParams());
$before_uri = $before_uri->alter('before', null);
$before_uri = $before_uri->alter('renamed', $renamed);
$before_uri = $before_uri->alter('follow', $follow);
return id(new AphrontRedirectResponse())->setURI($before_uri);
}
private function getBeforeLineNumber($target_commit) {
$drequest = $this->getDiffusionRequest();
$line = $drequest->getLine();
if (!$line) {
return null;
}
$raw_diff = $this->callConduitWithDiffusionRequest(
'diffusion.rawdiffquery',
array(
'commit' => $drequest->getCommit(),
'path' => $drequest->getPath(),
'againstCommit' => $target_commit,
));
$old_line = 0;
$new_line = 0;
foreach (explode("\n", $raw_diff) as $text) {
if ($text[0] == '-' || $text[0] == ' ') {
$old_line++;
}
if ($text[0] == '+' || $text[0] == ' ') {
$new_line++;
}
if ($new_line == $line) {
return $old_line;
}
}
// We didn't find the target line.
return $line;
}
private function loadParentCommitOf($commit) {
$drequest = $this->getDiffusionRequest();
$user = $this->getRequest()->getUser();
$before_req = DiffusionRequest::newFromDictionary(
array(
'user' => $user,
'repository' => $drequest->getRepository(),
'commit' => $commit,
));
$parents = DiffusionQuery::callConduitWithDiffusionRequest(
$user,
$before_req,
'diffusion.commitparentsquery',
array(
'commit' => $commit,
));
return head($parents);
}
private function renderRevisionTooltip(
DifferentialRevision $revision,
array $handles) {
$viewer = $this->getRequest()->getUser();
$date = phabricator_date($revision->getDateModified(), $viewer);
$id = $revision->getID();
$title = $revision->getTitle();
$header = "D{$id} {$title}";
$author = $handles[$revision->getAuthorPHID()]->getName();
return "{$header}\n{$date} \xC2\xB7 {$author}";
}
private function renderCommitTooltip(
PhabricatorRepositoryCommit $commit,
array $handles,
$author) {
$viewer = $this->getRequest()->getUser();
$date = phabricator_date($commit->getEpoch(), $viewer);
$summary = trim($commit->getSummary());
if ($commit->getAuthorPHID()) {
$author = $handles[$commit->getAuthorPHID()]->getName();
}
return "{$summary}\n{$date} \xC2\xB7 {$author}";
}
}
diff --git a/src/applications/diffusion/controller/DiffusionBrowseSearchController.php b/src/applications/diffusion/controller/DiffusionBrowseSearchController.php
index ac0f74e79..550fe3877 100644
--- a/src/applications/diffusion/controller/DiffusionBrowseSearchController.php
+++ b/src/applications/diffusion/controller/DiffusionBrowseSearchController.php
@@ -1,231 +1,233 @@
<?php
final class DiffusionBrowseSearchController extends DiffusionBrowseController {
protected function processDiffusionRequest(AphrontRequest $request) {
$drequest = $this->diffusionRequest;
$actions = $this->buildActionView($drequest);
$properties = $this->buildPropertyView($drequest, $actions);
$object_box = id(new PHUIObjectBoxView())
->setHeader($this->buildHeaderView($drequest))
->addPropertyList($properties);
$content = array();
$content[] = $object_box;
$content[] = $this->renderSearchForm($collapsed = false);
$content[] = $this->renderSearchResults();
$crumbs = $this->buildCrumbs(
array(
'branch' => true,
'path' => true,
'view' => 'browse',
));
return $this->buildApplicationPage(
array(
$crumbs,
$content,
),
array(
'title' => array(
nonempty(basename($drequest->getPath()), '/'),
- $drequest->getRepository()->getCallsign().' Repository',
+ pht(
+ '%s Repository',
+ $drequest->getRepository()->getCallsign()),
),
));
}
private function renderSearchResults() {
$drequest = $this->getDiffusionRequest();
$repository = $drequest->getRepository();
$results = array();
$limit = 100;
$page = $this->getRequest()->getInt('page', 0);
$pager = new AphrontPagerView();
$pager->setPageSize($limit);
$pager->setOffset($page);
$pager->setURI($this->getRequest()->getRequestURI(), 'page');
$search_mode = null;
switch ($repository->getVersionControlSystem()) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
$results = array();
break;
default:
if (strlen($this->getRequest()->getStr('grep'))) {
$search_mode = 'grep';
$query_string = $this->getRequest()->getStr('grep');
$results = $this->callConduitWithDiffusionRequest(
'diffusion.searchquery',
array(
'grep' => $query_string,
'commit' => $drequest->getStableCommit(),
'path' => $drequest->getPath(),
'limit' => $limit + 1,
'offset' => $page,
));
} else { // Filename search.
$search_mode = 'find';
$query_string = $this->getRequest()->getStr('find');
$results = $this->callConduitWithDiffusionRequest(
'diffusion.querypaths',
array(
'pattern' => $query_string,
'commit' => $drequest->getStableCommit(),
'path' => $drequest->getPath(),
'limit' => $limit + 1,
'offset' => $page,
));
}
break;
}
$results = $pager->sliceResults($results);
if ($search_mode == 'grep') {
$table = $this->renderGrepResults($results, $query_string);
$header = pht(
'File content matching "%s" under "%s"',
$query_string,
nonempty($drequest->getPath(), '/'));
} else {
$table = $this->renderFindResults($results);
$header = pht(
'Paths matching "%s" under "%s"',
$query_string,
nonempty($drequest->getPath(), '/'));
}
$box = id(new PHUIObjectBoxView())
->setHeaderText($header)
->appendChild($table);
$pager_box = id(new PHUIBoxView())
->addMargin(PHUI::MARGIN_LARGE)
->appendChild($pager);
return array($box, $pager_box);
}
private function renderGrepResults(array $results, $pattern) {
$drequest = $this->getDiffusionRequest();
require_celerity_resource('phabricator-search-results-css');
$rows = array();
foreach ($results as $result) {
list($path, $line, $string) = $result;
$href = $drequest->generateURI(array(
'action' => 'browse',
'path' => $path,
'line' => $line,
));
$matches = null;
$count = @preg_match_all(
'('.$pattern.')u',
$string,
$matches,
PREG_OFFSET_CAPTURE);
if (!$count) {
$output = ltrim($string);
} else {
$output = array();
$cursor = 0;
$length = strlen($string);
foreach ($matches[0] as $match) {
$offset = $match[1];
if ($cursor != $offset) {
$output[] = array(
'text' => substr($string, $cursor, $offset),
'highlight' => false,
);
}
$output[] = array(
'text' => $match[0],
'highlight' => true,
);
$cursor = $offset + strlen($match[0]);
}
if ($cursor != $length) {
$output[] = array(
'text' => substr($string, $cursor),
'highlight' => false,
);
}
if ($output) {
$output[0]['text'] = ltrim($output[0]['text']);
}
foreach ($output as $key => $segment) {
if ($segment['highlight']) {
$output[$key] = phutil_tag('strong', array(), $segment['text']);
} else {
$output[$key] = $segment['text'];
}
}
}
$string = phutil_tag(
'pre',
array('class' => 'PhabricatorMonospaced phui-source-fragment'),
$output);
$path = Filesystem::readablePath($path, $drequest->getPath());
$rows[] = array(
phutil_tag('a', array('href' => $href), $path),
$line,
$string,
);
}
$table = id(new AphrontTableView($rows))
->setClassName('remarkup-code')
->setHeaders(array(pht('Path'), pht('Line'), pht('String')))
->setColumnClasses(array('', 'n', 'wide'))
->setNoDataString(
pht(
'The pattern you searched for was not found in the content of any '.
'files.'));
return $table;
}
private function renderFindResults(array $results) {
$drequest = $this->getDiffusionRequest();
$rows = array();
foreach ($results as $result) {
$href = $drequest->generateURI(array(
'action' => 'browse',
'path' => $result,
));
$readable = Filesystem::readablePath($result, $drequest->getPath());
$rows[] = array(
phutil_tag('a', array('href' => $href), $readable),
);
}
$table = id(new AphrontTableView($rows))
->setHeaders(array(pht('Path')))
->setColumnClasses(array('wide'))
->setNoDataString(
pht(
'The pattern you searched for did not match the names of any '.
'files.'));
return $table;
}
}
diff --git a/src/applications/diffusion/controller/DiffusionCommitBranchesController.php b/src/applications/diffusion/controller/DiffusionCommitBranchesController.php
index 0be218fac..4f12e1765 100644
--- a/src/applications/diffusion/controller/DiffusionCommitBranchesController.php
+++ b/src/applications/diffusion/controller/DiffusionCommitBranchesController.php
@@ -1,44 +1,44 @@
<?php
final class DiffusionCommitBranchesController extends DiffusionController {
public function shouldAllowPublic() {
return true;
}
protected function processDiffusionRequest(AphrontRequest $request) {
$drequest = $this->getDiffusionRequest();
$repository = $drequest->getRepository();
switch ($repository->getVersionControlSystem()) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
$branches = array();
break;
default:
$branches = $this->callConduitWithDiffusionRequest(
'diffusion.branchquery',
array(
'contains' => $drequest->getCommit(),
));
break;
}
$branches = DiffusionRepositoryRef::loadAllFromDictionaries($branches);
$branch_links = array();
foreach ($branches as $branch) {
$branch_links[] = phutil_tag(
'a',
array(
'href' => $drequest->generateURI(
array(
'action' => 'browse',
'branch' => $branch->getShortName(),
)),
),
$branch->getShortName());
}
return id(new AphrontAjaxResponse())
- ->setContent($branch_links ? implode(', ', $branch_links) : 'None');
+ ->setContent($branch_links ? implode(', ', $branch_links) : pht('None'));
}
}
diff --git a/src/applications/diffusion/controller/DiffusionCommitController.php b/src/applications/diffusion/controller/DiffusionCommitController.php
index 1126cb659..b30cfb068 100644
--- a/src/applications/diffusion/controller/DiffusionCommitController.php
+++ b/src/applications/diffusion/controller/DiffusionCommitController.php
@@ -1,1082 +1,1084 @@
<?php
final class DiffusionCommitController extends DiffusionController {
const CHANGES_LIMIT = 100;
private $auditAuthorityPHIDs;
private $highlightedAudits;
public function shouldAllowPublic() {
return true;
}
protected function shouldLoadDiffusionRequest() {
return false;
}
protected function processDiffusionRequest(AphrontRequest $request) {
$user = $request->getUser();
// This controller doesn't use blob/path stuff, just pass the dictionary
// in directly instead of using the AphrontRequest parsing mechanism.
$data = $request->getURIMap();
$data['user'] = $user;
$drequest = DiffusionRequest::newFromDictionary($data);
$this->diffusionRequest = $drequest;
if ($request->getStr('diff')) {
return $this->buildRawDiffResponse($drequest);
}
$repository = $drequest->getRepository();
$callsign = $repository->getCallsign();
$content = array();
$commit = id(new DiffusionCommitQuery())
->setViewer($request->getUser())
->withRepository($repository)
->withIdentifiers(array($drequest->getCommit()))
->needCommitData(true)
->needAuditRequests(true)
->executeOne();
$crumbs = $this->buildCrumbs(array(
'commit' => true,
));
if (!$commit) {
$exists = $this->callConduitWithDiffusionRequest(
'diffusion.existsquery',
array('commit' => $drequest->getCommit()));
if (!$exists) {
return new Aphront404Response();
}
$error = id(new PHUIInfoView())
->setTitle(pht('Commit Still Parsing'))
->appendChild(
pht(
'Failed to load the commit because the commit has not been '.
'parsed yet.'));
return $this->buildApplicationPage(
array(
$crumbs,
$error,
),
array(
'title' => pht('Commit Still Parsing'),
));
}
$audit_requests = $commit->getAudits();
$this->auditAuthorityPHIDs =
PhabricatorAuditCommentEditor::loadAuditPHIDsForUser($user);
$commit_data = $commit->getCommitData();
$is_foreign = $commit_data->getCommitDetail('foreign-svn-stub');
$changesets = null;
if ($is_foreign) {
$subpath = $commit_data->getCommitDetail('svn-subpath');
$error_panel = new PHUIInfoView();
$error_panel->setTitle(pht('Commit Not Tracked'));
$error_panel->setSeverity(PHUIInfoView::SEVERITY_WARNING);
$error_panel->appendChild(
- pht("This Diffusion repository is configured to track only one ".
- "subdirectory of the entire Subversion repository, and this commit ".
- "didn't affect the tracked subdirectory ('%s'), so no ".
- "information is available.", $subpath));
+ pht(
+ "This Diffusion repository is configured to track only one ".
+ "subdirectory of the entire Subversion repository, and this commit ".
+ "didn't affect the tracked subdirectory ('%s'), so no ".
+ "information is available.",
+ $subpath));
$content[] = $error_panel;
} else {
$engine = PhabricatorMarkupEngine::newDifferentialMarkupEngine();
$engine->setConfig('viewer', $user);
require_celerity_resource('phabricator-remarkup-css');
$parents = $this->callConduitWithDiffusionRequest(
'diffusion.commitparentsquery',
array('commit' => $drequest->getCommit()));
if ($parents) {
$parents = id(new DiffusionCommitQuery())
->setViewer($user)
->withRepository($repository)
->withIdentifiers($parents)
->execute();
}
$headsup_view = id(new PHUIHeaderView())
->setHeader(nonempty($commit->getSummary(), pht('Commit Detail')));
$headsup_actions = $this->renderHeadsupActionList($commit, $repository);
$commit_properties = $this->loadCommitProperties(
$commit,
$commit_data,
$parents,
$audit_requests);
$property_list = id(new PHUIPropertyListView())
->setHasKeyboardShortcuts(true)
->setUser($user)
->setObject($commit);
foreach ($commit_properties as $key => $value) {
$property_list->addProperty($key, $value);
}
$message = $commit_data->getCommitMessage();
$revision = $commit->getCommitIdentifier();
$message = $this->linkBugtraq($message);
$message = $engine->markupText($message);
$property_list->invokeWillRenderEvent();
$property_list->setActionList($headsup_actions);
$detail_list = new PHUIPropertyListView();
$detail_list->addSectionHeader(
pht('Description'),
PHUIPropertyListView::ICON_SUMMARY);
$detail_list->addTextContent(
phutil_tag(
'div',
array(
'class' => 'diffusion-commit-message phabricator-remarkup',
),
$message));
$object_box = id(new PHUIObjectBoxView())
->setHeader($headsup_view)
->addPropertyList($property_list)
->addPropertyList($detail_list);
$content[] = $object_box;
}
$content[] = $this->buildComments($commit);
$hard_limit = 1000;
if ($commit->isImported()) {
$change_query = DiffusionPathChangeQuery::newFromDiffusionRequest(
$drequest);
$change_query->setLimit($hard_limit + 1);
$changes = $change_query->loadChanges();
} else {
$changes = array();
}
$was_limited = (count($changes) > $hard_limit);
if ($was_limited) {
$changes = array_slice($changes, 0, $hard_limit);
}
$content[] = $this->buildMergesTable($commit);
$highlighted_audits = $commit->getAuthorityAudits(
$user,
$this->auditAuthorityPHIDs);
$owners_paths = array();
if ($highlighted_audits) {
$packages = id(new PhabricatorOwnersPackage())->loadAllWhere(
'phid IN (%Ls)',
mpull($highlighted_audits, 'getAuditorPHID'));
if ($packages) {
$owners_paths = id(new PhabricatorOwnersPath())->loadAllWhere(
'repositoryPHID = %s AND packageID IN (%Ld)',
$repository->getPHID(),
mpull($packages, 'getID'));
}
}
$change_table = new DiffusionCommitChangeTableView();
$change_table->setDiffusionRequest($drequest);
$change_table->setPathChanges($changes);
$change_table->setOwnersPaths($owners_paths);
$count = count($changes);
$bad_commit = null;
if ($count == 0) {
$bad_commit = queryfx_one(
id(new PhabricatorRepository())->establishConnection('r'),
'SELECT * FROM %T WHERE fullCommitName = %s',
PhabricatorRepository::TABLE_BADCOMMIT,
'r'.$callsign.$commit->getCommitIdentifier());
}
if ($bad_commit) {
$content[] = $this->renderStatusMessage(
pht('Bad Commit'),
$bad_commit['description']);
} else if ($is_foreign) {
// Don't render anything else.
} else if (!$commit->isImported()) {
$content[] = $this->renderStatusMessage(
pht('Still Importing...'),
pht(
'This commit is still importing. Changes will be visible once '.
'the import finishes.'));
} else if (!count($changes)) {
$content[] = $this->renderStatusMessage(
pht('Empty Commit'),
pht(
'This commit is empty and does not affect any paths.'));
} else if ($was_limited) {
$content[] = $this->renderStatusMessage(
pht('Enormous Commit'),
pht(
'This commit is enormous, and affects more than %d files. '.
'Changes are not shown.',
$hard_limit));
} else {
// The user has clicked "Show All Changes", and we should show all the
// changes inline even if there are more than the soft limit.
$show_all_details = $request->getBool('show_all');
$change_panel = new PHUIObjectBoxView();
$header = new PHUIHeaderView();
- $header->setHeader('Changes ('.number_format($count).')');
+ $header->setHeader(pht('Changes (%d', number_format($count)));
$change_panel->setID('toc');
if ($count > self::CHANGES_LIMIT && !$show_all_details) {
$icon = id(new PHUIIconView())
->setIconFont('fa-files-o');
$button = id(new PHUIButtonView())
->setText(pht('Show All Changes'))
->setHref('?show_all=true')
->setTag('a')
->setIcon($icon);
$warning_view = id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_WARNING)
- ->setTitle('Very Large Commit')
+ ->setTitle(pht('Very Large Commit'))
->appendChild(
pht('This commit is very large. Load each file individually.'));
$change_panel->setInfoView($warning_view);
$header->addActionLink($button);
}
$change_panel->appendChild($change_table);
$change_panel->setHeader($header);
$content[] = $change_panel;
$changesets = DiffusionPathChange::convertToDifferentialChangesets(
$user,
$changes);
$vcs = $repository->getVersionControlSystem();
switch ($vcs) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
$vcs_supports_directory_changes = true;
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
$vcs_supports_directory_changes = false;
break;
default:
- throw new Exception('Unknown VCS.');
+ throw new Exception(pht('Unknown VCS.'));
}
$references = array();
foreach ($changesets as $key => $changeset) {
$file_type = $changeset->getFileType();
if ($file_type == DifferentialChangeType::FILE_DIRECTORY) {
if (!$vcs_supports_directory_changes) {
unset($changesets[$key]);
continue;
}
}
$references[$key] = $drequest->generateURI(
array(
'action' => 'rendering-ref',
'path' => $changeset->getFilename(),
));
}
// TODO: Some parts of the views still rely on properties of the
// DifferentialChangeset. Make the objects ephemeral to make sure we don't
// accidentally save them, and then set their ID to the appropriate ID for
// this application (the path IDs).
$path_ids = array_flip(mpull($changes, 'getPath'));
foreach ($changesets as $changeset) {
$changeset->makeEphemeral();
$changeset->setID($path_ids[$changeset->getFilename()]);
}
if ($count <= self::CHANGES_LIMIT || $show_all_details) {
$visible_changesets = $changesets;
} else {
$visible_changesets = array();
$inlines = PhabricatorAuditInlineComment::loadDraftAndPublishedComments(
$user,
$commit->getPHID());
$path_ids = mpull($inlines, null, 'getPathID');
foreach ($changesets as $key => $changeset) {
if (array_key_exists($changeset->getID(), $path_ids)) {
$visible_changesets[$key] = $changeset;
}
}
}
$change_list_title = DiffusionView::nameCommit(
$repository,
$commit->getCommitIdentifier());
$change_list = new DifferentialChangesetListView();
$change_list->setTitle($change_list_title);
$change_list->setChangesets($changesets);
$change_list->setVisibleChangesets($visible_changesets);
$change_list->setRenderingReferences($references);
$change_list->setRenderURI('/diffusion/'.$callsign.'/diff/');
$change_list->setRepository($repository);
$change_list->setUser($user);
// TODO: Try to setBranch() to something reasonable here?
$change_list->setStandaloneURI(
'/diffusion/'.$callsign.'/diff/');
$change_list->setRawFileURIs(
// TODO: Implement this, somewhat tricky if there's an octopus merge
// or whatever?
null,
'/diffusion/'.$callsign.'/diff/?view=r');
$change_list->setInlineCommentControllerURI(
'/diffusion/inline/edit/'.phutil_escape_uri($commit->getPHID()).'/');
$change_references = array();
foreach ($changesets as $key => $changeset) {
$change_references[$changeset->getID()] = $references[$key];
}
$change_table->setRenderingReferences($change_references);
$content[] = $change_list->render();
}
$content[] = $this->renderAddCommentPanel($commit, $audit_requests);
$commit_id = 'r'.$callsign.$commit->getCommitIdentifier();
$short_name = DiffusionView::nameCommit(
$repository,
$commit->getCommitIdentifier());
$prefs = $user->loadPreferences();
$pref_filetree = PhabricatorUserPreferences::PREFERENCE_DIFF_FILETREE;
$pref_collapse = PhabricatorUserPreferences::PREFERENCE_NAV_COLLAPSED;
$show_filetree = $prefs->getPreference($pref_filetree);
$collapsed = $prefs->getPreference($pref_collapse);
if ($changesets && $show_filetree) {
$nav = id(new DifferentialChangesetFileTreeSideNavBuilder())
->setTitle($short_name)
->setBaseURI(new PhutilURI('/'.$commit_id))
->build($changesets)
->setCrumbs($crumbs)
->setCollapsed((bool)$collapsed)
->appendChild($content);
$content = $nav;
} else {
$content = array($crumbs, $content);
}
return $this->buildApplicationPage(
$content,
array(
'title' => $commit_id,
'pageObjects' => array($commit->getPHID()),
));
}
private function loadCommitProperties(
PhabricatorRepositoryCommit $commit,
PhabricatorRepositoryCommitData $data,
array $parents,
array $audit_requests) {
assert_instances_of($parents, 'PhabricatorRepositoryCommit');
$viewer = $this->getRequest()->getUser();
$commit_phid = $commit->getPHID();
$drequest = $this->getDiffusionRequest();
$repository = $drequest->getRepository();
$edge_query = id(new PhabricatorEdgeQuery())
->withSourcePHIDs(array($commit_phid))
->withEdgeTypes(array(
DiffusionCommitHasTaskEdgeType::EDGECONST,
DiffusionCommitHasRevisionEdgeType::EDGECONST,
DiffusionCommitRevertsCommitEdgeType::EDGECONST,
DiffusionCommitRevertedByCommitEdgeType::EDGECONST,
));
$edges = $edge_query->execute();
$task_phids = array_keys(
$edges[$commit_phid][DiffusionCommitHasTaskEdgeType::EDGECONST]);
$revision_phid = key(
$edges[$commit_phid][DiffusionCommitHasRevisionEdgeType::EDGECONST]);
$reverts_phids = array_keys(
$edges[$commit_phid][DiffusionCommitRevertsCommitEdgeType::EDGECONST]);
$reverted_by_phids = array_keys(
$edges[$commit_phid][DiffusionCommitRevertedByCommitEdgeType::EDGECONST]);
$phids = $edge_query->getDestinationPHIDs(array($commit_phid));
if ($data->getCommitDetail('authorPHID')) {
$phids[] = $data->getCommitDetail('authorPHID');
}
if ($data->getCommitDetail('reviewerPHID')) {
$phids[] = $data->getCommitDetail('reviewerPHID');
}
if ($data->getCommitDetail('committerPHID')) {
$phids[] = $data->getCommitDetail('committerPHID');
}
if ($parents) {
foreach ($parents as $parent) {
$phids[] = $parent->getPHID();
}
}
// NOTE: We should never normally have more than a single push log, but
// it can occur naturally if a commit is pushed, then the branch it was
// on is deleted, then the commit is pushed again (or through other similar
// chains of events). This should be rare, but does not indicate a bug
// or data issue.
// NOTE: We never query push logs in SVN because the commiter is always
// the pusher and the commit time is always the push time; the push log
// is redundant and we save a query by skipping it.
$push_logs = array();
if ($repository->isHosted() && !$repository->isSVN()) {
$push_logs = id(new PhabricatorRepositoryPushLogQuery())
->setViewer($viewer)
->withRepositoryPHIDs(array($repository->getPHID()))
->withNewRefs(array($commit->getCommitIdentifier()))
->withRefTypes(array(PhabricatorRepositoryPushLog::REFTYPE_COMMIT))
->execute();
foreach ($push_logs as $log) {
$phids[] = $log->getPusherPHID();
}
}
$handles = array();
if ($phids) {
$handles = $this->loadViewerHandles($phids);
}
$props = array();
if ($commit->getAuditStatus()) {
$status = PhabricatorAuditCommitStatusConstants::getStatusName(
$commit->getAuditStatus());
$tag = id(new PHUITagView())
->setType(PHUITagView::TYPE_STATE)
->setName($status);
switch ($commit->getAuditStatus()) {
case PhabricatorAuditCommitStatusConstants::NEEDS_AUDIT:
$tag->setBackgroundColor(PHUITagView::COLOR_ORANGE);
break;
case PhabricatorAuditCommitStatusConstants::CONCERN_RAISED:
$tag->setBackgroundColor(PHUITagView::COLOR_RED);
break;
case PhabricatorAuditCommitStatusConstants::PARTIALLY_AUDITED:
$tag->setBackgroundColor(PHUITagView::COLOR_BLUE);
break;
case PhabricatorAuditCommitStatusConstants::FULLY_AUDITED:
$tag->setBackgroundColor(PHUITagView::COLOR_GREEN);
break;
}
$props['Status'] = $tag;
}
if ($audit_requests) {
$user_requests = array();
$other_requests = array();
foreach ($audit_requests as $audit_request) {
if ($audit_request->isUser()) {
$user_requests[] = $audit_request;
} else {
$other_requests[] = $audit_request;
}
}
if ($user_requests) {
$props['Auditors'] = $this->renderAuditStatusView(
$user_requests);
}
if ($other_requests) {
$props['Project/Package Auditors'] = $this->renderAuditStatusView(
$other_requests);
}
}
$author_phid = $data->getCommitDetail('authorPHID');
$author_name = $data->getAuthorName();
if (!$repository->isSVN()) {
$authored_info = id(new PHUIStatusItemView());
// TODO: In Git, a distinct authorship date is available. When present,
// we should show it here.
if ($author_phid) {
$authored_info->setTarget($handles[$author_phid]->renderLink());
} else if (strlen($author_name)) {
$authored_info->setTarget($author_name);
}
$props['Authored'] = id(new PHUIStatusListView())
->addItem($authored_info);
}
$committed_info = id(new PHUIStatusItemView())
->setNote(phabricator_datetime($commit->getEpoch(), $viewer));
$committer_phid = $data->getCommitDetail('committerPHID');
$committer_name = $data->getCommitDetail('committer');
if ($committer_phid) {
$committed_info->setTarget($handles[$committer_phid]->renderLink());
} else if (strlen($committer_name)) {
$committed_info->setTarget($committer_name);
} else if ($author_phid) {
$committed_info->setTarget($handles[$author_phid]->renderLink());
} else if (strlen($author_name)) {
$committed_info->setTarget($author_name);
}
$props['Committed'] = id(new PHUIStatusListView())
->addItem($committed_info);
if ($push_logs) {
$pushed_list = new PHUIStatusListView();
foreach ($push_logs as $push_log) {
$pushed_item = id(new PHUIStatusItemView())
->setTarget($handles[$push_log->getPusherPHID()]->renderLink())
->setNote(phabricator_datetime($push_log->getEpoch(), $viewer));
$pushed_list->addItem($pushed_item);
}
$props['Pushed'] = $pushed_list;
}
$reviewer_phid = $data->getCommitDetail('reviewerPHID');
if ($reviewer_phid) {
$props['Reviewer'] = $handles[$reviewer_phid]->renderLink();
}
if ($revision_phid) {
$props['Differential Revision'] = $handles[$revision_phid]->renderLink();
}
if ($parents) {
$parent_links = array();
foreach ($parents as $parent) {
$parent_links[] = $handles[$parent->getPHID()]->renderLink();
}
$props['Parents'] = phutil_implode_html(" \xC2\xB7 ", $parent_links);
}
$props['Branches'] = phutil_tag(
'span',
array(
'id' => 'commit-branches',
),
pht('Unknown'));
$props['Tags'] = phutil_tag(
'span',
array(
'id' => 'commit-tags',
),
pht('Unknown'));
$callsign = $repository->getCallsign();
$root = '/diffusion/'.$callsign.'/commit/'.$commit->getCommitIdentifier();
Javelin::initBehavior(
'diffusion-commit-branches',
array(
$root.'/branches/' => 'commit-branches',
$root.'/tags/' => 'commit-tags',
));
$refs = $this->buildRefs($drequest);
if ($refs) {
$props['References'] = $refs;
}
if ($reverts_phids) {
$props[pht('Reverts')] = $viewer->renderHandleList($reverts_phids);
}
if ($reverted_by_phids) {
$props[pht('Reverted By')] = $viewer->renderHandleList(
$reverted_by_phids);
}
if ($task_phids) {
$task_list = array();
foreach ($task_phids as $phid) {
$task_list[] = $handles[$phid]->renderLink();
}
$task_list = phutil_implode_html(phutil_tag('br'), $task_list);
$props['Tasks'] = $task_list;
}
return $props;
}
private function buildComments(PhabricatorRepositoryCommit $commit) {
$timeline = $this->buildTransactionTimeline(
$commit,
new PhabricatorAuditTransactionQuery());
$commit->willRenderTimeline($timeline, $this->getRequest());
return $timeline;
}
private function renderAddCommentPanel(
PhabricatorRepositoryCommit $commit,
array $audit_requests) {
assert_instances_of($audit_requests, 'PhabricatorRepositoryAuditRequest');
$request = $this->getRequest();
$user = $request->getUser();
if (!$user->isLoggedIn()) {
return id(new PhabricatorApplicationTransactionCommentView())
->setUser($user)
->setRequestURI($request->getRequestURI());
}
$is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business');
$pane_id = celerity_generate_unique_node_id();
Javelin::initBehavior(
'differential-keyboard-navigation',
array(
'haunt' => $pane_id,
));
$draft = id(new PhabricatorDraft())->loadOneWhere(
'authorPHID = %s AND draftKey = %s',
$user->getPHID(),
'diffusion-audit-'.$commit->getID());
if ($draft) {
$draft = $draft->getDraft();
} else {
$draft = null;
}
$actions = $this->getAuditActions($commit, $audit_requests);
$mailable_source = new PhabricatorMetaMTAMailableDatasource();
$auditor_source = new DiffusionAuditorDatasource();
$form = id(new AphrontFormView())
->setUser($user)
->setAction('/audit/addcomment/')
->addHiddenInput('commit', $commit->getPHID())
->appendChild(
id(new AphrontFormSelectControl())
->setLabel(pht('Action'))
->setName('action')
->setID('audit-action')
->setOptions($actions))
->appendControl(
id(new AphrontFormTokenizerControl())
->setLabel(pht('Add Auditors'))
->setName('auditors')
->setControlID('add-auditors')
->setControlStyle('display: none')
->setID('add-auditors-tokenizer')
->setDisableBehavior(true)
->setDatasource($auditor_source))
->appendControl(
id(new AphrontFormTokenizerControl())
->setLabel(pht('Add CCs'))
->setName('ccs')
->setControlID('add-ccs')
->setControlStyle('display: none')
->setID('add-ccs-tokenizer')
->setDisableBehavior(true)
->setDatasource($mailable_source))
->appendChild(
id(new PhabricatorRemarkupControl())
->setLabel(pht('Comments'))
->setName('content')
->setValue($draft)
->setID('audit-content')
->setUser($user))
->appendChild(
id(new AphrontFormSubmitControl())
->setValue(pht('Submit')));
$header = new PHUIHeaderView();
$header->setHeader(
$is_serious ? pht('Audit Commit') : pht('Creative Accounting'));
Javelin::initBehavior(
'differential-add-reviewers-and-ccs',
array(
'dynamic' => array(
'add-auditors-tokenizer' => array(
'actions' => array('add_auditors' => 1),
'src' => $auditor_source->getDatasourceURI(),
'row' => 'add-auditors',
'placeholder' => $auditor_source->getPlaceholderText(),
),
'add-ccs-tokenizer' => array(
'actions' => array('add_ccs' => 1),
'src' => $mailable_source->getDatasourceURI(),
'row' => 'add-ccs',
'placeholder' => $mailable_source->getPlaceholderText(),
),
),
'select' => 'audit-action',
));
Javelin::initBehavior('differential-feedback-preview', array(
'uri' => '/audit/preview/'.$commit->getID().'/',
'preview' => 'audit-preview',
'content' => 'audit-content',
'action' => 'audit-action',
'previewTokenizers' => array(
'auditors' => 'add-auditors-tokenizer',
'ccs' => 'add-ccs-tokenizer',
),
'inline' => 'inline-comment-preview',
'inlineuri' => '/diffusion/inline/preview/'.$commit->getPHID().'/',
));
$loading = phutil_tag_div(
'aphront-panel-preview-loading-text',
pht('Loading preview...'));
$preview_panel = phutil_tag_div(
'aphront-panel-preview aphront-panel-flush',
array(
phutil_tag('div', array('id' => 'audit-preview'), $loading),
phutil_tag('div', array('id' => 'inline-comment-preview')),
));
// TODO: This is pretty awkward, unify the CSS between Diffusion and
// Differential better.
require_celerity_resource('differential-core-view-css');
$anchor = id(new PhabricatorAnchorView())
->setAnchorName('comment')
->setNavigationMarker(true)
->render();
$comment_box = id(new PHUIObjectBoxView())
->setHeader($header)
->appendChild($form);
return phutil_tag(
'div',
array(
'id' => $pane_id,
),
phutil_tag_div(
'differential-add-comment-panel',
array($anchor, $comment_box, $preview_panel)));
}
/**
* Return a map of available audit actions for rendering into a <select />.
* This shows the user valid actions, and does not show nonsense/invalid
* actions (like closing an already-closed commit, or resigning from a commit
* you have no association with).
*/
private function getAuditActions(
PhabricatorRepositoryCommit $commit,
array $audit_requests) {
assert_instances_of($audit_requests, 'PhabricatorRepositoryAuditRequest');
$user = $this->getRequest()->getUser();
$user_is_author = ($commit->getAuthorPHID() == $user->getPHID());
$user_request = null;
foreach ($audit_requests as $audit_request) {
if ($audit_request->getAuditorPHID() == $user->getPHID()) {
$user_request = $audit_request;
break;
}
}
$actions = array();
$actions[PhabricatorAuditActionConstants::COMMENT] = true;
$actions[PhabricatorAuditActionConstants::ADD_CCS] = true;
$actions[PhabricatorAuditActionConstants::ADD_AUDITORS] = true;
// We allow you to accept your own commits. A use case here is that you
// notice an issue with your own commit and "Raise Concern" as an indicator
// to other auditors that you're on top of the issue, then later resolve it
// and "Accept". You can not accept on behalf of projects or packages,
// however.
$actions[PhabricatorAuditActionConstants::ACCEPT] = true;
$actions[PhabricatorAuditActionConstants::CONCERN] = true;
// To resign, a user must have authority on some request and not be the
// commit's author.
if (!$user_is_author) {
$may_resign = false;
$authority_map = array_fill_keys($this->auditAuthorityPHIDs, true);
foreach ($audit_requests as $request) {
if (empty($authority_map[$request->getAuditorPHID()])) {
continue;
}
$may_resign = true;
break;
}
// If the user has already resigned, don't show "Resign...".
$status_resigned = PhabricatorAuditStatusConstants::RESIGNED;
if ($user_request) {
if ($user_request->getAuditStatus() == $status_resigned) {
$may_resign = false;
}
}
if ($may_resign) {
$actions[PhabricatorAuditActionConstants::RESIGN] = true;
}
}
$status_concern = PhabricatorAuditCommitStatusConstants::CONCERN_RAISED;
$concern_raised = ($commit->getAuditStatus() == $status_concern);
$can_close_option = PhabricatorEnv::getEnvConfig(
'audit.can-author-close-audit');
if ($can_close_option && $user_is_author && $concern_raised) {
$actions[PhabricatorAuditActionConstants::CLOSE] = true;
}
foreach ($actions as $constant => $ignored) {
$actions[$constant] =
PhabricatorAuditActionConstants::getActionName($constant);
}
return $actions;
}
private function buildMergesTable(PhabricatorRepositoryCommit $commit) {
$drequest = $this->getDiffusionRequest();
$repository = $drequest->getRepository();
$vcs = $repository->getVersionControlSystem();
switch ($vcs) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
// These aren't supported under SVN.
return null;
}
$limit = 50;
$merges = $this->callConduitWithDiffusionRequest(
'diffusion.mergedcommitsquery',
array(
'commit' => $drequest->getCommit(),
'limit' => $limit + 1,
));
if (!$merges) {
return null;
}
$merges = DiffusionPathChange::newFromConduit($merges);
$caption = null;
if (count($merges) > $limit) {
$merges = array_slice($merges, 0, $limit);
$caption = new PHUIInfoView();
$caption->setSeverity(PHUIInfoView::SEVERITY_NOTICE);
$caption->appendChild(
pht(
- 'This commit merges a very large number of changes. Only the first '.
- '%s are shown.',
+ 'This commit merges a very large number of changes. '.
+ 'Only the first %s are shown.',
new PhutilNumber($limit)));
}
$history_table = new DiffusionHistoryTableView();
$history_table->setUser($this->getRequest()->getUser());
$history_table->setDiffusionRequest($drequest);
$history_table->setHistory($merges);
$history_table->loadRevisions();
$phids = $history_table->getRequiredHandlePHIDs();
$handles = $this->loadViewerHandles($phids);
$history_table->setHandles($handles);
$panel = new PHUIObjectBoxView();
$panel->setHeaderText(pht('Merged Changes'));
$panel->appendChild($history_table);
if ($caption) {
$panel->setInfoView($caption);
}
return $panel;
}
private function renderHeadsupActionList(
PhabricatorRepositoryCommit $commit,
PhabricatorRepository $repository) {
$request = $this->getRequest();
$user = $request->getUser();
$actions = id(new PhabricatorActionListView())
->setUser($user)
->setObject($commit)
->setObjectURI($request->getRequestURI());
$can_edit = PhabricatorPolicyFilter::hasCapability(
$user,
$commit,
PhabricatorPolicyCapability::CAN_EDIT);
$uri = '/diffusion/'.$repository->getCallsign().'/commit/'.
$commit->getCommitIdentifier().'/edit/';
$action = id(new PhabricatorActionView())
->setName(pht('Edit Commit'))
->setHref($uri)
->setIcon('fa-pencil')
->setDisabled(!$can_edit)
->setWorkflow(!$can_edit);
$actions->addAction($action);
require_celerity_resource('phabricator-object-selector-css');
require_celerity_resource('javelin-behavior-phabricator-object-selector');
$maniphest = 'PhabricatorManiphestApplication';
if (PhabricatorApplication::isClassInstalled($maniphest)) {
$action = id(new PhabricatorActionView())
->setName(pht('Edit Maniphest Tasks'))
->setIcon('fa-anchor')
->setHref('/search/attach/'.$commit->getPHID().'/TASK/edge/')
->setWorkflow(true)
->setDisabled(!$can_edit);
$actions->addAction($action);
}
$action = id(new PhabricatorActionView())
->setName(pht('Download Raw Diff'))
->setHref($request->getRequestURI()->alter('diff', true))
->setIcon('fa-download');
$actions->addAction($action);
return $actions;
}
private function buildRefs(DiffusionRequest $request) {
// this is git-only, so save a conduit round trip and just get out of
// here if the repository isn't git
$type_git = PhabricatorRepositoryType::REPOSITORY_TYPE_GIT;
$repository = $request->getRepository();
if ($repository->getVersionControlSystem() != $type_git) {
return null;
}
$results = $this->callConduitWithDiffusionRequest(
'diffusion.refsquery',
array('commit' => $request->getCommit()));
$ref_links = array();
foreach ($results as $ref_data) {
$ref_links[] = phutil_tag('a',
array('href' => $ref_data['href']),
$ref_data['ref']);
}
return phutil_implode_html(', ', $ref_links);
}
private function buildRawDiffResponse(DiffusionRequest $drequest) {
$raw_diff = $this->callConduitWithDiffusionRequest(
'diffusion.rawdiffquery',
array(
'commit' => $drequest->getCommit(),
'path' => $drequest->getPath(),
));
$file = PhabricatorFile::buildFromFileDataOrHash(
$raw_diff,
array(
'name' => $drequest->getCommit().'.diff',
'ttl' => (60 * 60 * 24),
'viewPolicy' => PhabricatorPolicies::POLICY_NOONE,
));
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$file->attachToObject($drequest->getRepository()->getPHID());
unset($unguarded);
return $file->getRedirectResponse();
}
private function renderAuditStatusView(array $audit_requests) {
assert_instances_of($audit_requests, 'PhabricatorRepositoryAuditRequest');
$viewer = $this->getViewer();
$authority_map = array_fill_keys($this->auditAuthorityPHIDs, true);
$view = new PHUIStatusListView();
foreach ($audit_requests as $request) {
$code = $request->getAuditStatus();
$item = new PHUIStatusItemView();
$item->setIcon(
PhabricatorAuditStatusConstants::getStatusIcon($code),
PhabricatorAuditStatusConstants::getStatusColor($code),
PhabricatorAuditStatusConstants::getStatusName($code));
$note = array();
foreach ($request->getAuditReasons() as $reason) {
$note[] = phutil_tag('div', array(), $reason);
}
$item->setNote($note);
$auditor_phid = $request->getAuditorPHID();
$target = $viewer->renderHandle($auditor_phid);
$item->setTarget($target);
if (isset($authority_map[$auditor_phid])) {
$item->setHighlighted(true);
}
$view->addItem($item);
}
return $view;
}
private function linkBugtraq($corpus) {
$url = PhabricatorEnv::getEnvConfig('bugtraq.url');
if (!strlen($url)) {
return $corpus;
}
$regexes = PhabricatorEnv::getEnvConfig('bugtraq.logregex');
if (!$regexes) {
return $corpus;
}
$parser = id(new PhutilBugtraqParser())
->setBugtraqPattern("[[ {$url} | %BUGID% ]]")
->setBugtraqCaptureExpression(array_shift($regexes));
$select = array_shift($regexes);
if ($select) {
$parser->setBugtraqSelectExpression($select);
}
return $parser->processCorpus($corpus);
}
}
diff --git a/src/applications/diffusion/controller/DiffusionController.php b/src/applications/diffusion/controller/DiffusionController.php
index 0b3e97a09..262223c6f 100644
--- a/src/applications/diffusion/controller/DiffusionController.php
+++ b/src/applications/diffusion/controller/DiffusionController.php
@@ -1,252 +1,252 @@
<?php
abstract class DiffusionController extends PhabricatorController {
protected $diffusionRequest;
public function setDiffusionRequest(DiffusionRequest $request) {
$this->diffusionRequest = $request;
return $this;
}
protected function getDiffusionRequest() {
if (!$this->diffusionRequest) {
- throw new Exception('No Diffusion request object!');
+ throw new Exception(pht('No Diffusion request object!'));
}
return $this->diffusionRequest;
}
public function willBeginExecution() {
$request = $this->getRequest();
// Check if this is a VCS request, e.g. from "git clone", "hg clone", or
// "svn checkout". If it is, we jump off into repository serving code to
// process the request.
if (DiffusionServeController::isVCSRequest($request)) {
$serve_controller = id(new DiffusionServeController())
->setCurrentApplication($this->getCurrentApplication());
return $this->delegateToController($serve_controller);
}
return parent::willBeginExecution();
}
protected function shouldLoadDiffusionRequest() {
return true;
}
final public function handleRequest(AphrontRequest $request) {
if ($request->getURIData('callsign') &&
$this->shouldLoadDiffusionRequest()) {
try {
$drequest = DiffusionRequest::newFromAphrontRequestDictionary(
$request->getURIMap(),
$request);
} catch (Exception $ex) {
return id(new Aphront404Response())
->setRequest($request);
}
$this->setDiffusionRequest($drequest);
}
return $this->processDiffusionRequest($request);
}
abstract protected function processDiffusionRequest(AphrontRequest $request);
public function buildCrumbs(array $spec = array()) {
$crumbs = $this->buildApplicationCrumbs();
$crumb_list = $this->buildCrumbList($spec);
foreach ($crumb_list as $crumb) {
$crumbs->addCrumb($crumb);
}
return $crumbs;
}
private function buildCrumbList(array $spec = array()) {
$spec = $spec + array(
'commit' => null,
'tags' => null,
'branches' => null,
'view' => null,
);
$crumb_list = array();
// On the home page, we don't have a DiffusionRequest.
if ($this->diffusionRequest) {
$drequest = $this->getDiffusionRequest();
$repository = $drequest->getRepository();
} else {
$drequest = null;
$repository = null;
}
if (!$repository) {
return $crumb_list;
}
$callsign = $repository->getCallsign();
$repository_name = $repository->getName();
if (!$spec['commit'] && !$spec['tags'] && !$spec['branches']) {
$branch_name = $drequest->getBranch();
if ($branch_name) {
$repository_name .= ' ('.$branch_name.')';
}
}
$crumb = id(new PHUICrumbView())
->setName($repository_name);
if (!$spec['view'] && !$spec['commit'] &&
!$spec['tags'] && !$spec['branches']) {
$crumb_list[] = $crumb;
return $crumb_list;
}
$crumb->setHref(
$drequest->generateURI(
array(
'action' => 'branch',
'path' => '/',
)));
$crumb_list[] = $crumb;
$stable_commit = $drequest->getStableCommit();
if ($spec['tags']) {
$crumb = new PHUICrumbView();
if ($spec['commit']) {
$crumb->setName(
pht('Tags for %s', 'r'.$callsign.$stable_commit));
$crumb->setHref($drequest->generateURI(
array(
'action' => 'commit',
'commit' => $drequest->getStableCommit(),
)));
} else {
$crumb->setName(pht('Tags'));
}
$crumb_list[] = $crumb;
return $crumb_list;
}
if ($spec['branches']) {
$crumb = id(new PHUICrumbView())
->setName(pht('Branches'));
$crumb_list[] = $crumb;
return $crumb_list;
}
if ($spec['commit']) {
$crumb = id(new PHUICrumbView())
->setName("r{$callsign}{$stable_commit}")
->setHref("r{$callsign}{$stable_commit}");
$crumb_list[] = $crumb;
return $crumb_list;
}
$crumb = new PHUICrumbView();
$view = $spec['view'];
switch ($view) {
case 'history':
$view_name = pht('History');
break;
case 'browse':
$view_name = pht('Browse');
break;
case 'lint':
$view_name = pht('Lint');
break;
case 'change':
$view_name = pht('Change');
break;
}
$crumb = id(new PHUICrumbView())
->setName($view_name);
$crumb_list[] = $crumb;
return $crumb_list;
}
protected function callConduitWithDiffusionRequest(
$method,
array $params = array()) {
$user = $this->getRequest()->getUser();
$drequest = $this->getDiffusionRequest();
return DiffusionQuery::callConduitWithDiffusionRequest(
$user,
$drequest,
$method,
$params);
}
protected function getRepositoryControllerURI(
PhabricatorRepository $repository,
$path) {
return $this->getApplicationURI($repository->getCallsign().'/'.$path);
}
protected function renderPathLinks(DiffusionRequest $drequest, $action) {
$path = $drequest->getPath();
$path_parts = array_filter(explode('/', trim($path, '/')));
$divider = phutil_tag(
'span',
array(
'class' => 'phui-header-divider',
),
'/');
$links = array();
if ($path_parts) {
$links[] = phutil_tag(
'a',
array(
'href' => $drequest->generateURI(
array(
'action' => $action,
'path' => '',
)),
),
'r'.$drequest->getRepository()->getCallsign());
$links[] = $divider;
$accum = '';
$last_key = last_key($path_parts);
foreach ($path_parts as $key => $part) {
$accum .= '/'.$part;
if ($key === $last_key) {
$links[] = $part;
} else {
$links[] = phutil_tag(
'a',
array(
'href' => $drequest->generateURI(
array(
'action' => $action,
'path' => $accum.'/',
)),
),
$part);
$links[] = $divider;
}
}
} else {
$links[] = 'r'.$drequest->getRepository()->getCallsign();
$links[] = $divider;
}
return $links;
}
protected function renderStatusMessage($title, $body) {
return id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_WARNING)
->setTitle($title)
->appendChild($body);
}
}
diff --git a/src/applications/diffusion/controller/DiffusionExternalController.php b/src/applications/diffusion/controller/DiffusionExternalController.php
index c48553842..4ab698b07 100644
--- a/src/applications/diffusion/controller/DiffusionExternalController.php
+++ b/src/applications/diffusion/controller/DiffusionExternalController.php
@@ -1,145 +1,147 @@
<?php
final class DiffusionExternalController extends DiffusionController {
public function shouldAllowPublic() {
return true;
}
protected function shouldLoadDiffusionRequest() {
return false;
}
protected function processDiffusionRequest(AphrontRequest $request) {
$uri = $request->getStr('uri');
$id = $request->getStr('id');
$repositories = id(new PhabricatorRepositoryQuery())
->setViewer($request->getUser())
->execute();
if ($uri) {
$uri_path = id(new PhutilURI($uri))->getPath();
$matches = array();
// Try to figure out which tracked repository this external lives in by
// comparing repository metadata. We look for an exact match, but accept
// a partial match.
foreach ($repositories as $key => $repository) {
$remote_uri = new PhutilURI($repository->getRemoteURI());
if ($remote_uri->getPath() == $uri_path) {
$matches[$key] = 1;
}
if ($repository->getPublicCloneURI() == $uri) {
$matches[$key] = 2;
}
if ($repository->getRemoteURI() == $uri) {
$matches[$key] = 3;
}
}
arsort($matches);
$best_match = head_key($matches);
if ($best_match) {
$repository = $repositories[$best_match];
$redirect = DiffusionRequest::generateDiffusionURI(
array(
'action' => 'browse',
'callsign' => $repository->getCallsign(),
'branch' => $repository->getDefaultBranch(),
'commit' => $id,
));
return id(new AphrontRedirectResponse())->setURI($redirect);
}
}
// TODO: This is a rare query but does a table scan, add a key?
$commits = id(new PhabricatorRepositoryCommit())->loadAllWhere(
'commitIdentifier = %s',
$id);
if (empty($commits)) {
$desc = null;
if ($uri) {
$desc = $uri.', at ';
}
$desc .= $id;
$content = id(new PHUIInfoView())
->setTitle(pht('Unknown External'))
->setSeverity(PHUIInfoView::SEVERITY_WARNING)
->appendChild(phutil_tag(
'p',
array(),
- pht('This external (%s) does not appear in any tracked '.
- 'repository. It may exist in an untracked repository that '.
- 'Diffusion does not know about.', $desc)));
+ pht(
+ 'This external (%s) does not appear in any tracked '.
+ 'repository. It may exist in an untracked repository that '.
+ 'Diffusion does not know about.',
+ $desc)));
} else if (count($commits) == 1) {
$commit = head($commits);
$repo = $repositories[$commit->getRepositoryID()];
$redirect = DiffusionRequest::generateDiffusionURI(
array(
'action' => 'browse',
'callsign' => $repo->getCallsign(),
'branch' => $repo->getDefaultBranch(),
'commit' => $commit->getCommitIdentifier(),
));
return id(new AphrontRedirectResponse())->setURI($redirect);
} else {
$rows = array();
foreach ($commits as $commit) {
$repo = $repositories[$commit->getRepositoryID()];
$href = DiffusionRequest::generateDiffusionURI(
array(
'action' => 'browse',
'callsign' => $repo->getCallsign(),
'branch' => $repo->getDefaultBranch(),
'commit' => $commit->getCommitIdentifier(),
));
$rows[] = array(
phutil_tag(
'a',
array(
'href' => $href,
),
'r'.$repo->getCallsign().$commit->getCommitIdentifier()),
$commit->loadCommitData()->getSummary(),
);
}
$table = new AphrontTableView($rows);
$table->setHeaders(
array(
pht('Commit'),
pht('Description'),
));
$table->setColumnClasses(
array(
'pri',
'wide',
));
$caption = id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_NOTICE)
->appendChild(
pht('This external reference matches multiple known commits.'));
$content = new PHUIObjectBoxView();
$content->setHeaderText(pht('Multiple Matching Commits'));
$content->setInfoView($caption);
$content->appendChild($table);
}
return $this->buildApplicationPage(
$content,
array(
'title' => pht('Unresolvable External'),
));
}
}
diff --git a/src/applications/diffusion/controller/DiffusionHistoryController.php b/src/applications/diffusion/controller/DiffusionHistoryController.php
index 36667c5df..887e4cbd2 100644
--- a/src/applications/diffusion/controller/DiffusionHistoryController.php
+++ b/src/applications/diffusion/controller/DiffusionHistoryController.php
@@ -1,173 +1,171 @@
<?php
final class DiffusionHistoryController extends DiffusionController {
public function shouldAllowPublic() {
return true;
}
protected function processDiffusionRequest(AphrontRequest $request) {
$drequest = $this->diffusionRequest;
$viewer = $request->getUser();
$repository = $drequest->getRepository();
$page_size = $request->getInt('pagesize', 100);
$offset = $request->getInt('offset', 0);
$params = array(
'commit' => $drequest->getCommit(),
'path' => $drequest->getPath(),
'offset' => $offset,
'limit' => $page_size + 1,
);
if (!$request->getBool('copies')) {
$params['needDirectChanges'] = true;
$params['needChildChanges'] = true;
}
$history_results = $this->callConduitWithDiffusionRequest(
'diffusion.historyquery',
$params);
$history = DiffusionPathChange::newFromConduit(
$history_results['pathChanges']);
$pager = new AphrontPagerView();
$pager->setPageSize($page_size);
$pager->setOffset($offset);
$history = $pager->sliceResults($history);
$pager->setURI($request->getRequestURI(), 'offset');
$show_graph = !strlen($drequest->getPath());
$content = array();
$history_table = new DiffusionHistoryTableView();
$history_table->setUser($request->getUser());
$history_table->setDiffusionRequest($drequest);
$history_table->setHistory($history);
$history_table->loadRevisions();
$phids = $history_table->getRequiredHandlePHIDs();
$handles = $this->loadViewerHandles($phids);
$history_table->setHandles($handles);
if ($show_graph) {
$history_table->setParents($history_results['parents']);
$history_table->setIsHead($offset == 0);
}
$history_panel = new PHUIObjectBoxView();
$history_panel->setHeaderText(pht('History'));
$history_panel->appendChild($history_table);
$content[] = $history_panel;
$header = id(new PHUIHeaderView())
->setUser($viewer)
->setPolicyObject($repository)
->setHeader($this->renderPathLinks($drequest, $mode = 'history'));
$actions = $this->buildActionView($drequest);
$properties = $this->buildPropertyView($drequest, $actions);
$object_box = id(new PHUIObjectBoxView())
->setHeader($header)
->addPropertyList($properties);
$crumbs = $this->buildCrumbs(
array(
'branch' => true,
'path' => true,
'view' => 'history',
));
return $this->buildApplicationPage(
array(
$crumbs,
$object_box,
$content,
$pager,
),
array(
'title' => array(
pht('History'),
pht('%s Repository', $drequest->getRepository()->getCallsign()),
),
));
}
private function buildActionView(DiffusionRequest $drequest) {
$viewer = $this->getRequest()->getUser();
$view = id(new PhabricatorActionListView())
->setUser($viewer);
$browse_uri = $drequest->generateURI(
array(
'action' => 'browse',
));
$view->addAction(
id(new PhabricatorActionView())
->setName(pht('Browse Content'))
->setHref($browse_uri)
->setIcon('fa-files-o'));
// TODO: Sometimes we do have a change view, we need to look at the most
// recent history entry to figure it out.
$request = $this->getRequest();
if ($request->getBool('copies')) {
$branch_name = pht('Hide Copies/Branches');
$branch_uri = $request->getRequestURI()
->alter('offset', null)
->alter('copies', null);
} else {
$branch_name = pht('Show Copies/Branches');
$branch_uri = $request->getRequestURI()
->alter('offset', null)
->alter('copies', true);
}
$view->addAction(
id(new PhabricatorActionView())
->setName($branch_name)
->setIcon('fa-code-fork')
->setHref($branch_uri));
return $view;
}
protected function buildPropertyView(
DiffusionRequest $drequest,
PhabricatorActionListView $actions) {
$viewer = $this->getRequest()->getUser();
$view = id(new PHUIPropertyListView())
->setUser($viewer)
->setActionList($actions);
$stable_commit = $drequest->getStableCommit();
$callsign = $drequest->getRepository()->getCallsign();
$view->addProperty(
pht('Commit'),
phutil_tag(
'a',
array(
'href' => $drequest->generateURI(
array(
'action' => 'commit',
'commit' => $stable_commit,
)),
),
$drequest->getRepository()->formatCommitName($stable_commit)));
return $view;
}
-
-
}
diff --git a/src/applications/diffusion/controller/DiffusionInlineCommentController.php b/src/applications/diffusion/controller/DiffusionInlineCommentController.php
index fb3d87167..09f5f06ad 100644
--- a/src/applications/diffusion/controller/DiffusionInlineCommentController.php
+++ b/src/applications/diffusion/controller/DiffusionInlineCommentController.php
@@ -1,124 +1,124 @@
<?php
final class DiffusionInlineCommentController
extends PhabricatorInlineCommentController {
private function getCommitPHID() {
return $this->getRequest()->getURIData('phid');
}
private function loadCommit() {
$viewer = $this->getViewer();
$commit_phid = $this->getCommitPHID();
$commit = id(new DiffusionCommitQuery())
->setViewer($viewer)
->withPHIDs(array($commit_phid))
->executeOne();
if (!$commit) {
throw new Exception(pht('Invalid commit PHID "%s"!', $commit_phid));
}
return $commit;
}
protected function createComment() {
$commit = $this->loadCommit();
// TODO: Write a real PathQuery object?
$path_id = $this->getChangesetID();
$path = queryfx_one(
id(new PhabricatorRepository())->establishConnection('r'),
'SELECT path FROM %T WHERE id = %d',
PhabricatorRepository::TABLE_PATH,
$path_id);
if (!$path) {
- throw new Exception('Invalid path ID!');
+ throw new Exception(pht('Invalid path ID!'));
}
return id(new PhabricatorAuditInlineComment())
->setCommitPHID($commit->getPHID())
->setPathID($path_id);
}
protected function loadComment($id) {
return PhabricatorAuditInlineComment::loadID($id);
}
protected function loadCommentByPHID($phid) {
return PhabricatorAuditInlineComment::loadPHID($phid);
}
protected function loadCommentForEdit($id) {
$request = $this->getRequest();
$user = $request->getUser();
$inline = $this->loadComment($id);
if (!$this->canEditInlineComment($user, $inline)) {
- throw new Exception('That comment is not editable!');
+ throw new Exception(pht('That comment is not editable!'));
}
return $inline;
}
protected function loadCommentForDone($id) {
$request = $this->getRequest();
$viewer = $request->getUser();
$inline = $this->loadComment($id);
if (!$inline) {
throw new Exception(pht('Failed to load comment "%d".', $id));
}
$commit = id(new DiffusionCommitQuery())
->setViewer($viewer)
->withPHIDs(array($inline->getCommitPHID()))
->executeOne();
if (!$commit) {
throw new Exception(pht('Failed to load commit.'));
}
if ((!$commit->getAuthorPHID()) ||
($commit->getAuthorPHID() != $viewer->getPHID())) {
throw new Exception(pht('You can not mark this comment as complete.'));
}
return $inline;
}
private function canEditInlineComment(
PhabricatorUser $user,
PhabricatorAuditInlineComment $inline) {
// Only the author may edit a comment.
if ($inline->getAuthorPHID() != $user->getPHID()) {
return false;
}
// Saved comments may not be edited.
if ($inline->getAuditCommentID()) {
return false;
}
// Inline must be attached to the active revision.
if ($inline->getCommitPHID() != $this->getCommitPHID()) {
return false;
}
return true;
}
protected function deleteComment(PhabricatorInlineCommentInterface $inline) {
return $inline->delete();
}
protected function saveComment(PhabricatorInlineCommentInterface $inline) {
return $inline->save();
}
protected function loadObjectOwnerPHID(
PhabricatorInlineCommentInterface $inline) {
return $this->loadCommit()->getAuthorPHID();
}
}
diff --git a/src/applications/diffusion/controller/DiffusionPathValidateController.php b/src/applications/diffusion/controller/DiffusionPathValidateController.php
index 1b839aab4..d54ddd7af 100644
--- a/src/applications/diffusion/controller/DiffusionPathValidateController.php
+++ b/src/applications/diffusion/controller/DiffusionPathValidateController.php
@@ -1,71 +1,71 @@
<?php
final class DiffusionPathValidateController extends DiffusionController {
protected function shouldLoadDiffusionRequest() {
return false;
}
protected function processDiffusionRequest(AphrontRequest $request) {
$repository_phid = $request->getStr('repositoryPHID');
$repository = id(new PhabricatorRepositoryQuery())
->setViewer($request->getUser())
->withPHIDs(array($repository_phid))
->executeOne();
if (!$repository) {
return new Aphront400Response();
}
$path = $request->getStr('path');
$path = ltrim($path, '/');
$drequest = DiffusionRequest::newFromDictionary(
array(
'user' => $request->getUser(),
'repository' => $repository,
'path' => $path,
));
$this->setDiffusionRequest($drequest);
$browse_results = DiffusionBrowseResultSet::newFromConduit(
$this->callConduitWithDiffusionRequest(
'diffusion.browsequery',
array(
'path' => $drequest->getPath(),
'commit' => $drequest->getCommit(),
'needValidityOnly' => true,
)));
$valid = $browse_results->isValidResults();
if (!$valid) {
switch ($browse_results->getReasonForEmptyResultSet()) {
case DiffusionBrowseResultSet::REASON_IS_FILE:
$valid = true;
break;
case DiffusionBrowseResultSet::REASON_IS_EMPTY:
$valid = true;
break;
}
}
$output = array(
'valid' => (bool)$valid,
);
if (!$valid) {
$branch = $drequest->getBranch();
if ($branch) {
$message = pht('Not found in %s', $branch);
} else {
- $message = pht('Not found at HEAD');
+ $message = pht('Not found at %s', 'HEAD');
}
} else {
$message = pht('OK');
}
$output['message'] = $message;
return id(new AphrontAjaxResponse())->setContent($output);
}
}
diff --git a/src/applications/diffusion/controller/DiffusionRepositoryController.php b/src/applications/diffusion/controller/DiffusionRepositoryController.php
index 7eb99b709..ff81c02ab 100644
--- a/src/applications/diffusion/controller/DiffusionRepositoryController.php
+++ b/src/applications/diffusion/controller/DiffusionRepositoryController.php
@@ -1,742 +1,741 @@
<?php
final class DiffusionRepositoryController extends DiffusionController {
public function shouldAllowPublic() {
return true;
}
protected function processDiffusionRequest(AphrontRequest $request) {
$viewer = $request->getUser();
$drequest = $this->getDiffusionRequest();
$repository = $drequest->getRepository();
$content = array();
$crumbs = $this->buildCrumbs();
$content[] = $crumbs;
$content[] = $this->buildPropertiesTable($drequest->getRepository());
// Before we do any work, make sure we're looking at a some content: we're
// on a valid branch, and the repository is not empty.
$page_has_content = false;
$empty_title = null;
$empty_message = null;
// If this VCS supports branches, check that the selected branch actually
// exists.
if ($drequest->supportsBranches()) {
// NOTE: Mercurial may have multiple branch heads with the same name.
$ref_cursors = id(new PhabricatorRepositoryRefCursorQuery())
->setViewer($viewer)
->withRepositoryPHIDs(array($repository->getPHID()))
->withRefTypes(array(PhabricatorRepositoryRefCursor::TYPE_BRANCH))
->withRefNames(array($drequest->getBranch()))
->execute();
if ($ref_cursors) {
// This is a valid branch, so we necessarily have some content.
$page_has_content = true;
} else {
$empty_title = pht('No Such Branch');
$empty_message = pht(
'There is no branch named "%s" in this repository.',
$drequest->getBranch());
}
}
// If we didn't find any branches, check if there are any commits at all.
// This can tailor the message for empty repositories.
if (!$page_has_content) {
$any_commit = id(new DiffusionCommitQuery())
->setViewer($viewer)
->withRepository($repository)
->setLimit(1)
->execute();
if ($any_commit) {
if (!$drequest->supportsBranches()) {
$page_has_content = true;
}
} else {
$empty_title = pht('Empty Repository');
- $empty_message = pht(
- 'This repository does not have any commits yet.');
+ $empty_message = pht('This repository does not have any commits yet.');
}
}
if ($page_has_content) {
$content[] = $this->buildNormalContent($drequest);
} else {
$content[] = id(new PHUIInfoView())
->setTitle($empty_title)
->setSeverity(PHUIInfoView::SEVERITY_WARNING)
->setErrors(array($empty_message));
}
return $this->buildApplicationPage(
$content,
array(
'title' => $drequest->getRepository()->getName(),
));
}
private function buildNormalContent(DiffusionRequest $drequest) {
$repository = $drequest->getRepository();
$phids = array();
$content = array();
try {
$history_results = $this->callConduitWithDiffusionRequest(
'diffusion.historyquery',
array(
'commit' => $drequest->getCommit(),
'path' => $drequest->getPath(),
'offset' => 0,
'limit' => 15,
));
$history = DiffusionPathChange::newFromConduit(
$history_results['pathChanges']);
foreach ($history as $item) {
$data = $item->getCommitData();
if ($data) {
if ($data->getCommitDetail('authorPHID')) {
$phids[$data->getCommitDetail('authorPHID')] = true;
}
if ($data->getCommitDetail('committerPHID')) {
$phids[$data->getCommitDetail('committerPHID')] = true;
}
}
}
$history_exception = null;
} catch (Exception $ex) {
$history_results = null;
$history = null;
$history_exception = $ex;
}
try {
$browse_results = DiffusionBrowseResultSet::newFromConduit(
$this->callConduitWithDiffusionRequest(
'diffusion.browsequery',
array(
'path' => $drequest->getPath(),
'commit' => $drequest->getCommit(),
)));
$browse_paths = $browse_results->getPaths();
foreach ($browse_paths as $item) {
$data = $item->getLastCommitData();
if ($data) {
if ($data->getCommitDetail('authorPHID')) {
$phids[$data->getCommitDetail('authorPHID')] = true;
}
if ($data->getCommitDetail('committerPHID')) {
$phids[$data->getCommitDetail('committerPHID')] = true;
}
}
}
$browse_exception = null;
} catch (Exception $ex) {
$browse_results = null;
$browse_paths = null;
$browse_exception = $ex;
}
$phids = array_keys($phids);
$handles = $this->loadViewerHandles($phids);
$readme = null;
if ($browse_results) {
$readme_path = $browse_results->getReadmePath();
if ($readme_path) {
$readme_content = $this->callConduitWithDiffusionRequest(
'diffusion.filecontentquery',
array(
'path' => $readme_path,
'commit' => $drequest->getStableCommit(),
));
if ($readme_content) {
$readme = id(new DiffusionReadmeView())
->setUser($this->getViewer())
->setPath($readme_path)
->setContent($readme_content['corpus']);
}
}
}
$content[] = $this->buildBrowseTable(
$browse_results,
$browse_paths,
$browse_exception,
$handles);
$content[] = $this->buildHistoryTable(
$history_results,
$history,
$history_exception,
$handles);
try {
$content[] = $this->buildTagListTable($drequest);
} catch (Exception $ex) {
if (!$repository->isImporting()) {
$content[] = $this->renderStatusMessage(
pht('Unable to Load Tags'),
$ex->getMessage());
}
}
try {
$content[] = $this->buildBranchListTable($drequest);
} catch (Exception $ex) {
if (!$repository->isImporting()) {
$content[] = $this->renderStatusMessage(
pht('Unable to Load Branches'),
$ex->getMessage());
}
}
if ($readme) {
$content[] = $readme;
}
return $content;
}
private function buildPropertiesTable(PhabricatorRepository $repository) {
$user = $this->getRequest()->getUser();
$header = id(new PHUIHeaderView())
->setHeader($repository->getName())
->setUser($user)
->setPolicyObject($repository);
if (!$repository->isTracked()) {
$header->setStatus('fa-ban', 'dark', pht('Inactive'));
} else if ($repository->isImporting()) {
$header->setStatus('fa-clock-o', 'indigo', pht('Importing...'));
} else {
$header->setStatus('fa-check', 'bluegrey', pht('Active'));
}
$actions = $this->buildActionList($repository);
$view = id(new PHUIPropertyListView())
->setUser($user);
$project_phids = PhabricatorEdgeQuery::loadDestinationPHIDs(
$repository->getPHID(),
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST);
if ($project_phids) {
$view->addProperty(
pht('Projects'),
$user->renderHandleList($project_phids));
}
if ($repository->isHosted()) {
$ssh_uri = $repository->getSSHCloneURIObject();
if ($ssh_uri) {
$clone_uri = $this->renderCloneCommand(
$repository,
$ssh_uri,
$repository->getServeOverSSH(),
'/settings/panel/ssh/');
$view->addProperty(
$repository->isSVN()
? pht('Checkout (SSH)')
: pht('Clone (SSH)'),
$clone_uri);
}
$http_uri = $repository->getHTTPCloneURIObject();
if ($http_uri) {
$clone_uri = $this->renderCloneCommand(
$repository,
$http_uri,
$repository->getServeOverHTTP(),
PhabricatorEnv::getEnvConfig('diffusion.allow-http-auth')
? '/settings/panel/vcspassword/'
: null);
$view->addProperty(
$repository->isSVN()
? pht('Checkout (HTTP)')
: pht('Clone (HTTP)'),
$clone_uri);
}
} else {
switch ($repository->getVersionControlSystem()) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
$view->addProperty(
pht('Clone'),
$this->renderCloneCommand(
$repository,
$repository->getPublicCloneURI()));
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
$view->addProperty(
pht('Checkout'),
$this->renderCloneCommand(
$repository,
$repository->getPublicCloneURI()));
break;
}
}
$description = $repository->getDetail('description');
if (strlen($description)) {
$description = PhabricatorMarkupEngine::renderOneObject(
$repository,
'description',
$user);
$view->addSectionHeader(pht('Description'));
$view->addTextContent($description);
}
$view->setActionList($actions);
$box = id(new PHUIObjectBoxView())
->setHeader($header)
->addPropertyList($view);
$info = null;
$drequest = $this->getDiffusionRequest();
if ($drequest->getRefAlternatives()) {
$message = array(
pht(
'The ref "%s" is ambiguous in this repository.',
$drequest->getBranch()),
' ',
phutil_tag(
'a',
array(
'href' => $drequest->generateURI(
array(
'action' => 'refs',
)),
),
pht('View Alternatives')),
);
$messages = array($message);
$info = id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_WARNING)
->setErrors(array($message));
$box->setInfoView($info);
}
return $box;
}
private function buildBranchListTable(DiffusionRequest $drequest) {
$viewer = $this->getRequest()->getUser();
if ($drequest->getBranch() === null) {
return null;
}
$limit = 15;
$branches = $this->callConduitWithDiffusionRequest(
'diffusion.branchquery',
array(
'closed' => false,
'limit' => $limit + 1,
));
if (!$branches) {
return null;
}
$more_branches = (count($branches) > $limit);
$branches = array_slice($branches, 0, $limit);
$branches = DiffusionRepositoryRef::loadAllFromDictionaries($branches);
$commits = id(new DiffusionCommitQuery())
->setViewer($viewer)
->withIdentifiers(mpull($branches, 'getCommitIdentifier'))
->withRepository($drequest->getRepository())
->execute();
$table = id(new DiffusionBranchTableView())
->setUser($viewer)
->setDiffusionRequest($drequest)
->setBranches($branches)
->setCommits($commits);
$panel = new PHUIObjectBoxView();
$header = new PHUIHeaderView();
$header->setHeader(pht('Branches'));
if ($more_branches) {
$header->setSubHeader(pht('Showing %d branches.', $limit));
}
$icon = id(new PHUIIconView())
->setIconFont('fa-code-fork');
$button = new PHUIButtonView();
$button->setText(pht('Show All Branches'));
$button->setTag('a');
$button->setIcon($icon);
$button->setHref($drequest->generateURI(
- array(
- 'action' => 'branches',
- )));
+ array(
+ 'action' => 'branches',
+ )));
$header->addActionLink($button);
$panel->setHeader($header);
$panel->appendChild($table);
return $panel;
}
private function buildTagListTable(DiffusionRequest $drequest) {
$viewer = $this->getRequest()->getUser();
$repository = $drequest->getRepository();
switch ($repository->getVersionControlSystem()) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
// no tags in SVN
return null;
}
$tag_limit = 15;
$tags = array();
$tags = DiffusionRepositoryTag::newFromConduit(
$this->callConduitWithDiffusionRequest(
'diffusion.tagsquery',
array(
// On the home page, we want to find tags on any branch.
'commit' => null,
'limit' => $tag_limit + 1,
)));
if (!$tags) {
return null;
}
$more_tags = (count($tags) > $tag_limit);
$tags = array_slice($tags, 0, $tag_limit);
$commits = id(new DiffusionCommitQuery())
->setViewer($viewer)
->withIdentifiers(mpull($tags, 'getCommitIdentifier'))
->withRepository($repository)
->needCommitData(true)
->execute();
$view = id(new DiffusionTagListView())
->setUser($viewer)
->setDiffusionRequest($drequest)
->setTags($tags)
->setCommits($commits);
$phids = $view->getRequiredHandlePHIDs();
$handles = $this->loadViewerHandles($phids);
$view->setHandles($handles);
$panel = new PHUIObjectBoxView();
$header = new PHUIHeaderView();
$header->setHeader(pht('Tags'));
if ($more_tags) {
$header->setSubHeader(
pht('Showing the %d most recent tags.', $tag_limit));
}
$icon = id(new PHUIIconView())
->setIconFont('fa-tag');
$button = new PHUIButtonView();
$button->setText(pht('Show All Tags'));
$button->setTag('a');
$button->setIcon($icon);
$button->setHref($drequest->generateURI(
array(
'action' => 'tags',
)));
$header->addActionLink($button);
$panel->setHeader($header);
$panel->appendChild($view);
return $panel;
}
private function buildActionList(PhabricatorRepository $repository) {
$viewer = $this->getRequest()->getUser();
$view_uri = $this->getApplicationURI($repository->getCallsign().'/');
$edit_uri = $this->getApplicationURI($repository->getCallsign().'/edit/');
$view = id(new PhabricatorActionListView())
->setUser($viewer)
->setObject($repository)
->setObjectURI($view_uri);
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$repository,
PhabricatorPolicyCapability::CAN_EDIT);
$view->addAction(
id(new PhabricatorActionView())
->setName(pht('Edit Repository'))
->setIcon('fa-pencil')
->setHref($edit_uri)
->setWorkflow(!$can_edit)
->setDisabled(!$can_edit));
if ($repository->isHosted()) {
$callsign = $repository->getCallsign();
$push_uri = $this->getApplicationURI(
'pushlog/?repositories=r'.$callsign);
$view->addAction(
id(new PhabricatorActionView())
->setName(pht('View Push Logs'))
->setIcon('fa-list-alt')
->setHref($push_uri));
}
return $view;
}
private function buildHistoryTable(
$history_results,
$history,
$history_exception,
array $handles) {
$request = $this->getRequest();
$viewer = $request->getUser();
$drequest = $this->getDiffusionRequest();
$repository = $drequest->getRepository();
if ($history_exception) {
if ($repository->isImporting()) {
return $this->renderStatusMessage(
pht('Still Importing...'),
pht(
'This repository is still importing. History is not yet '.
'available.'));
} else {
return $this->renderStatusMessage(
pht('Unable to Retrieve History'),
$history_exception->getMessage());
}
}
$history_table = id(new DiffusionHistoryTableView())
->setUser($viewer)
->setDiffusionRequest($drequest)
->setHandles($handles)
->setHistory($history);
// TODO: Super sketchy.
$history_table->loadRevisions();
if ($history_results) {
$history_table->setParents($history_results['parents']);
}
$history_table->setIsHead(true);
$callsign = $drequest->getRepository()->getCallsign();
$icon = id(new PHUIIconView())
->setIconFont('fa-list-alt');
$button = id(new PHUIButtonView())
->setText(pht('View Full History'))
->setHref($drequest->generateURI(
array(
'action' => 'history',
)))
->setTag('a')
->setIcon($icon);
$panel = new PHUIObjectBoxView();
$header = id(new PHUIHeaderView())
->setHeader(pht('Recent Commits'))
->addActionLink($button);
$panel->setHeader($header);
$panel->appendChild($history_table);
return $panel;
}
private function buildBrowseTable(
$browse_results,
$browse_paths,
$browse_exception,
array $handles) {
require_celerity_resource('diffusion-icons-css');
$request = $this->getRequest();
$viewer = $request->getUser();
$drequest = $this->getDiffusionRequest();
$repository = $drequest->getRepository();
if ($browse_exception) {
if ($repository->isImporting()) {
// The history table renders a useful message.
return null;
} else {
return $this->renderStatusMessage(
pht('Unable to Retrieve Paths'),
$browse_exception->getMessage());
}
}
$browse_table = id(new DiffusionBrowseTableView())
->setUser($viewer)
->setDiffusionRequest($drequest)
->setHandles($handles);
if ($browse_paths) {
$browse_table->setPaths($browse_paths);
} else {
$browse_table->setPaths(array());
}
$browse_uri = $drequest->generateURI(array('action' => 'browse'));
$browse_panel = new PHUIObjectBoxView();
$header = id(new PHUIHeaderView())
->setHeader(pht('Repository'));
$icon = id(new PHUIIconView())
->setIconFont('fa-folder-open');
$button = new PHUIButtonView();
$button->setText(pht('Browse Repository'));
$button->setTag('a');
$button->setIcon($icon);
$button->setHref($browse_uri);
$header->addActionLink($button);
$browse_panel->setHeader($header);
if ($repository->canUsePathTree()) {
Javelin::initBehavior(
'diffusion-locate-file',
array(
'controlID' => 'locate-control',
'inputID' => 'locate-input',
'browseBaseURI' => (string)$drequest->generateURI(
array(
'action' => 'browse',
)),
'uri' => (string)$drequest->generateURI(
array(
'action' => 'pathtree',
)),
));
$form = id(new AphrontFormView())
->setUser($viewer)
->appendChild(
id(new AphrontFormTypeaheadControl())
->setHardpointID('locate-control')
->setID('locate-input')
->setLabel(pht('Locate File')));
$form_box = id(new PHUIBoxView())
->addClass('diffusion-locate-file-view')
->appendChild($form->buildLayoutView());
$browse_panel->appendChild($form_box);
}
$browse_panel->appendChild($browse_table);
return $browse_panel;
}
private function renderCloneCommand(
PhabricatorRepository $repository,
$uri,
$serve_mode = null,
$manage_uri = null) {
require_celerity_resource('diffusion-icons-css');
Javelin::initBehavior('select-on-click');
switch ($repository->getVersionControlSystem()) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
$command = csprintf(
'git clone %R',
$uri);
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
$command = csprintf(
'hg clone %R',
$uri);
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
if ($repository->isHosted()) {
$command = csprintf(
'svn checkout %R %R',
$uri,
$repository->getCloneName());
} else {
$command = csprintf(
'svn checkout %R',
$uri);
}
break;
}
$input = javelin_tag(
'input',
array(
'type' => 'text',
'value' => (string)$command,
'class' => 'diffusion-clone-uri',
'sigil' => 'select-on-click',
'readonly' => 'true',
));
$extras = array();
if ($serve_mode) {
if ($serve_mode === PhabricatorRepository::SERVE_READONLY) {
$extras[] = pht('(Read Only)');
}
}
if ($manage_uri) {
if ($this->getRequest()->getUser()->isLoggedIn()) {
$extras[] = phutil_tag(
'a',
array(
'href' => $manage_uri,
),
pht('Manage Credentials'));
}
}
if ($extras) {
$extras = phutil_implode_html(' ', $extras);
$extras = phutil_tag(
'div',
array(
'class' => 'diffusion-clone-extras',
),
$extras);
}
return array($input, $extras);
}
}
diff --git a/src/applications/diffusion/controller/DiffusionRepositoryCreateController.php b/src/applications/diffusion/controller/DiffusionRepositoryCreateController.php
index d077d031f..269523915 100644
--- a/src/applications/diffusion/controller/DiffusionRepositoryCreateController.php
+++ b/src/applications/diffusion/controller/DiffusionRepositoryCreateController.php
@@ -1,904 +1,903 @@
<?php
final class DiffusionRepositoryCreateController
extends DiffusionRepositoryEditController {
private $edit;
private $repository;
protected function processDiffusionRequest(AphrontRequest $request) {
$viewer = $request->getUser();
$this->edit = $request->getURIData('edit');
// NOTE: We can end up here via either "Create Repository", or via
// "Import Repository", or via "Edit Remote", or via "Edit Policies". In
// the latter two cases, we show only a few of the pages.
$repository = null;
$service = null;
switch ($this->edit) {
case 'remote':
case 'policy':
$repository = $this->getDiffusionRequest()->getRepository();
// Make sure we have CAN_EDIT.
PhabricatorPolicyFilter::requireCapability(
$viewer,
$repository,
PhabricatorPolicyCapability::CAN_EDIT);
$this->setRepository($repository);
$cancel_uri = $this->getRepositoryControllerURI($repository, 'edit/');
break;
case 'import':
case 'create':
$this->requireApplicationCapability(
DiffusionCreateRepositoriesCapability::CAPABILITY);
// Pick a random open service to allocate this repository on, if any
// exist. If there are no services, we aren't in cluster mode and
// will allocate locally. If there are services but none permit
// allocations, we fail.
$services = id(new AlmanacServiceQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withServiceClasses(
array(
'AlmanacClusterRepositoryServiceType',
))
->execute();
if ($services) {
// Filter out services which do not permit new allocations.
foreach ($services as $key => $possible_service) {
if ($possible_service->getAlmanacPropertyValue('closed')) {
unset($services[$key]);
}
}
if (!$services) {
throw new Exception(
pht(
'This install is configured in cluster mode, but all '.
'available repository cluster services are closed to new '.
'allocations. At least one service must be open to allow '.
'new allocations to take place.'));
}
shuffle($services);
$service = head($services);
}
$cancel_uri = $this->getApplicationURI('new/');
break;
default:
- throw new Exception('Invalid edit operation!');
+ throw new Exception(pht('Invalid edit operation!'));
}
$form = id(new PHUIPagedFormView())
->setUser($viewer)
->setCancelURI($cancel_uri);
switch ($this->edit) {
case 'remote':
$title = pht('Edit Remote');
$form
->addPage('remote-uri', $this->buildRemoteURIPage())
->addPage('auth', $this->buildAuthPage());
break;
case 'policy':
$title = pht('Edit Policies');
$form
->addPage('policy', $this->buildPolicyPage());
break;
case 'create':
$title = pht('Create Repository');
$form
->addPage('vcs', $this->buildVCSPage())
->addPage('name', $this->buildNamePage())
->addPage('policy', $this->buildPolicyPage())
->addPage('done', $this->buildDonePage());
break;
case 'import':
$title = pht('Import Repository');
$form
->addPage('vcs', $this->buildVCSPage())
->addPage('name', $this->buildNamePage())
->addPage('remote-uri', $this->buildRemoteURIPage())
->addPage('auth', $this->buildAuthPage())
->addPage('policy', $this->buildPolicyPage())
->addPage('done', $this->buildDonePage());
break;
}
if ($request->isFormPost()) {
$form->readFromRequest($request);
if ($form->isComplete()) {
$is_create = ($this->edit === 'import' || $this->edit === 'create');
$is_auth = ($this->edit == 'import' || $this->edit == 'remote');
$is_policy = ($this->edit != 'remote');
$is_init = ($this->edit == 'create');
if ($is_create) {
$repository = PhabricatorRepository::initializeNewRepository(
$viewer);
}
$template = id(new PhabricatorRepositoryTransaction());
$type_name = PhabricatorRepositoryTransaction::TYPE_NAME;
$type_vcs = PhabricatorRepositoryTransaction::TYPE_VCS;
$type_activate = PhabricatorRepositoryTransaction::TYPE_ACTIVATE;
$type_local_path = PhabricatorRepositoryTransaction::TYPE_LOCAL_PATH;
$type_remote_uri = PhabricatorRepositoryTransaction::TYPE_REMOTE_URI;
$type_hosting = PhabricatorRepositoryTransaction::TYPE_HOSTING;
$type_http = PhabricatorRepositoryTransaction::TYPE_PROTOCOL_HTTP;
$type_ssh = PhabricatorRepositoryTransaction::TYPE_PROTOCOL_SSH;
$type_credential = PhabricatorRepositoryTransaction::TYPE_CREDENTIAL;
$type_view = PhabricatorTransactions::TYPE_VIEW_POLICY;
$type_edit = PhabricatorTransactions::TYPE_EDIT_POLICY;
$type_push = PhabricatorRepositoryTransaction::TYPE_PUSH_POLICY;
$type_service = PhabricatorRepositoryTransaction::TYPE_SERVICE;
$xactions = array();
// If we're creating a new repository, set all this core stuff.
if ($is_create) {
$callsign = $form->getPage('name')
->getControl('callsign')->getValue();
// We must set this to a unique value to save the repository
// initially, and it's immutable, so we don't bother using
// transactions to apply this change.
$repository->setCallsign($callsign);
$xactions[] = id(clone $template)
->setTransactionType($type_name)
->setNewValue(
$form->getPage('name')->getControl('name')->getValue());
$xactions[] = id(clone $template)
->setTransactionType($type_vcs)
->setNewValue(
$form->getPage('vcs')->getControl('vcs')->getValue());
$activate = $form->getPage('done')
->getControl('activate')->getValue();
$xactions[] = id(clone $template)
->setTransactionType($type_activate)
->setNewValue(($activate == 'start'));
if ($service) {
$xactions[] = id(clone $template)
->setTransactionType($type_service)
->setNewValue($service->getPHID());
}
$default_local_path = PhabricatorEnv::getEnvConfig(
'repository.default-local-path');
$default_local_path = rtrim($default_local_path, '/');
$default_local_path = $default_local_path.'/'.$callsign.'/';
$xactions[] = id(clone $template)
->setTransactionType($type_local_path)
->setNewValue($default_local_path);
}
if ($is_init) {
$xactions[] = id(clone $template)
->setTransactionType($type_hosting)
->setNewValue(true);
$vcs = $form->getPage('vcs')->getControl('vcs')->getValue();
if ($vcs != PhabricatorRepositoryType::REPOSITORY_TYPE_SVN) {
if (PhabricatorEnv::getEnvConfig('diffusion.allow-http-auth')) {
$v_http_mode = PhabricatorRepository::SERVE_READWRITE;
} else {
$v_http_mode = PhabricatorRepository::SERVE_OFF;
}
$xactions[] = id(clone $template)
->setTransactionType($type_http)
->setNewValue($v_http_mode);
}
if (PhabricatorEnv::getEnvConfig('diffusion.ssh-user')) {
$v_ssh_mode = PhabricatorRepository::SERVE_READWRITE;
} else {
$v_ssh_mode = PhabricatorRepository::SERVE_OFF;
}
$xactions[] = id(clone $template)
->setTransactionType($type_ssh)
->setNewValue($v_ssh_mode);
}
if ($is_auth) {
$xactions[] = id(clone $template)
->setTransactionType($type_remote_uri)
->setNewValue(
$form->getPage('remote-uri')->getControl('remoteURI')
->getValue());
$xactions[] = id(clone $template)
->setTransactionType($type_credential)
->setNewValue(
$form->getPage('auth')->getControl('credential')->getValue());
}
if ($is_policy) {
$xactions[] = id(clone $template)
->setTransactionType($type_view)
->setNewValue(
$form->getPage('policy')->getControl('viewPolicy')->getValue());
$xactions[] = id(clone $template)
->setTransactionType($type_edit)
->setNewValue(
$form->getPage('policy')->getControl('editPolicy')->getValue());
if ($is_init || $repository->isHosted()) {
$xactions[] = id(clone $template)
->setTransactionType($type_push)
->setNewValue(
$form->getPage('policy')->getControl('pushPolicy')->getValue());
}
}
id(new PhabricatorRepositoryEditor())
->setContinueOnNoEffect(true)
->setContentSourceFromRequest($request)
->setActor($viewer)
->applyTransactions($repository, $xactions);
$repo_uri = $this->getRepositoryControllerURI($repository, 'edit/');
return id(new AphrontRedirectResponse())->setURI($repo_uri);
}
} else {
$dict = array();
if ($repository) {
$dict = array(
'remoteURI' => $repository->getRemoteURI(),
'credential' => $repository->getCredentialPHID(),
'viewPolicy' => $repository->getViewPolicy(),
'editPolicy' => $repository->getEditPolicy(),
'pushPolicy' => $repository->getPushPolicy(),
);
}
$form->readFromObject($dict);
}
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb($title);
return $this->buildApplicationPage(
array(
$crumbs,
$form,
),
array(
'title' => $title,
));
}
/* -( Page: VCS Type )----------------------------------------------------- */
private function buildVCSPage() {
$is_import = ($this->edit == 'import');
if ($is_import) {
$git_str = pht(
'Import a Git repository (for example, a repository hosted '.
'on GitHub).');
$hg_str = pht(
'Import a Mercurial repository (for example, a repository '.
'hosted on Bitbucket).');
$svn_str = pht('Import a Subversion repository.');
} else {
$git_str = pht('Create a new, empty Git repository.');
$hg_str = pht('Create a new, empty Mercurial repository.');
$svn_str = pht('Create a new, empty Subversion repository.');
}
$control = id(new AphrontFormRadioButtonControl())
->setName('vcs')
->setLabel(pht('Type'))
->addButton(
PhabricatorRepositoryType::REPOSITORY_TYPE_GIT,
pht('Git'),
$git_str)
->addButton(
PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL,
pht('Mercurial'),
$hg_str)
->addButton(
PhabricatorRepositoryType::REPOSITORY_TYPE_SVN,
pht('Subversion'),
$svn_str);
if ($is_import) {
$control->addButton(
PhabricatorRepositoryType::REPOSITORY_TYPE_PERFORCE,
pht('Perforce'),
pht(
'Perforce is not directly supported, but you can import '.
'a Perforce repository as a Git repository using %s.',
phutil_tag(
'a',
array(
'href' =>
'http://www.perforce.com/product/components/git-fusion',
'target' => '_blank',
),
pht('Perforce Git Fusion'))),
'disabled',
$disabled = true);
}
return id(new PHUIFormPageView())
->setPageName(pht('Repository Type'))
->setUser($this->getRequest()->getUser())
->setValidateFormPageCallback(array($this, 'validateVCSPage'))
->addControl($control);
}
public function validateVCSPage(PHUIFormPageView $page) {
$valid = array(
PhabricatorRepositoryType::REPOSITORY_TYPE_GIT => true,
PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL => true,
PhabricatorRepositoryType::REPOSITORY_TYPE_SVN => true,
);
$c_vcs = $page->getControl('vcs');
$v_vcs = $c_vcs->getValue();
if (!$v_vcs) {
$c_vcs->setError(pht('Required'));
$page->addPageError(
pht('You must select a version control system.'));
} else if (empty($valid[$v_vcs])) {
$c_vcs->setError(pht('Invalid'));
$page->addPageError(
pht('You must select a valid version control system.'));
}
return $c_vcs->isValid();
}
/* -( Page: Name and Callsign )-------------------------------------------- */
private function buildNamePage() {
return id(new PHUIFormPageView())
->setUser($this->getRequest()->getUser())
->setPageName(pht('Repository Name and Location'))
->setValidateFormPageCallback(array($this, 'validateNamePage'))
->addRemarkupInstructions(
pht(
'**Choose a human-readable name for this repository**, like '.
'"CompanyName Mobile App" or "CompanyName Backend Server". You '.
'can change this later.'))
->addControl(
id(new AphrontFormTextControl())
->setName('name')
->setLabel(pht('Name'))
->setCaption(pht('Human-readable repository name.')))
->addRemarkupInstructions(
pht(
'**Choose a "Callsign" for the repository.** This is a short, '.
'unique string which identifies commits elsewhere in Phabricator. '.
'For example, you might use `M` for your mobile app repository '.
'and `B` for your backend repository.'.
"\n\n".
'**Callsigns must be UPPERCASE**, and can not be edited after the '.
'repository is created. Generally, you should choose short '.
'callsigns.'))
->addControl(
id(new AphrontFormTextControl())
->setName('callsign')
->setLabel(pht('Callsign'))
->setCaption(pht('Short UPPERCASE identifier.')));
}
public function validateNamePage(PHUIFormPageView $page) {
$c_name = $page->getControl('name');
$v_name = $c_name->getValue();
if (!strlen($v_name)) {
$c_name->setError(pht('Required'));
$page->addPageError(
pht('You must choose a name for this repository.'));
}
$c_call = $page->getControl('callsign');
$v_call = $c_call->getValue();
if (!strlen($v_call)) {
$c_call->setError(pht('Required'));
$page->addPageError(
pht('You must choose a callsign for this repository.'));
} else if (!preg_match('/^[A-Z]+\z/', $v_call)) {
$c_call->setError(pht('Invalid'));
$page->addPageError(
pht('The callsign must contain only UPPERCASE letters.'));
} else {
$exists = false;
try {
$repo = id(new PhabricatorRepositoryQuery())
->setViewer($this->getRequest()->getUser())
->withCallsigns(array($v_call))
->executeOne();
$exists = (bool)$repo;
} catch (PhabricatorPolicyException $ex) {
$exists = true;
}
if ($exists) {
$c_call->setError(pht('Not Unique'));
$page->addPageError(
pht(
'Another repository already uses that callsign. You must choose '.
'a unique callsign.'));
}
}
return $c_name->isValid() &&
$c_call->isValid();
}
/* -( Page: Remote URI )--------------------------------------------------- */
private function buildRemoteURIPage() {
return id(new PHUIFormPageView())
->setUser($this->getRequest()->getUser())
->setPageName(pht('Repository Remote URI'))
->setValidateFormPageCallback(array($this, 'validateRemoteURIPage'))
->setAdjustFormPageCallback(array($this, 'adjustRemoteURIPage'))
->addControl(
id(new AphrontFormTextControl())
->setName('remoteURI'));
}
public function adjustRemoteURIPage(PHUIFormPageView $page) {
$form = $page->getForm();
$is_git = false;
$is_svn = false;
$is_mercurial = false;
if ($this->getRepository()) {
$vcs = $this->getRepository()->getVersionControlSystem();
} else {
$vcs = $form->getPage('vcs')->getControl('vcs')->getValue();
}
switch ($vcs) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
$is_git = true;
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
$is_svn = true;
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
$is_mercurial = true;
break;
default:
- throw new Exception('Unsupported VCS!');
+ throw new Exception(pht('Unsupported VCS!'));
}
$has_local = ($is_git || $is_mercurial);
if ($is_git) {
$uri_label = pht('Remote URI');
$instructions = pht(
'Enter the URI to clone this Git repository from. It should usually '.
'look like one of these examples:'.
"\n\n".
"| Example Git Remote URIs |\n".
"| ----------------------- |\n".
"| `git@github.com:example/example.git` |\n".
"| `ssh://user@host.com/git/example.git` |\n".
"| `https://example.com/repository.git` |\n");
} else if ($is_mercurial) {
$uri_label = pht('Remote URI');
$instructions = pht(
'Enter the URI to clone this Mercurial repository from. It should '.
'usually look like one of these examples:'.
"\n\n".
"| Example Mercurial Remote URIs |\n".
"| ----------------------- |\n".
"| `ssh://hg@bitbucket.org/example/repository` |\n".
"| `https://bitbucket.org/example/repository` |\n");
} else if ($is_svn) {
$uri_label = pht('Repository Root');
$instructions = pht(
'Enter the **Repository Root** for this Subversion repository. '.
'You can figure this out by running `svn info` in a working copy '.
'and looking at the value in the `Repository Root` field. It '.
'should be a URI and will usually look like these:'.
"\n\n".
"| Example Subversion Repository Root URIs |\n".
"| ------------------------------ |\n".
"| `http://svn.example.org/svnroot/` |\n".
"| `svn+ssh://svn.example.com/svnroot/` |\n".
"| `svn://svn.example.net/svnroot/` |\n".
"\n\n".
"You **MUST** specify the root of the repository, not a ".
"subdirectory. (If you want to import only part of a Subversion ".
"repository, use the //Import Only// option at the end of this ".
"workflow.)");
} else {
- throw new Exception('Unsupported VCS!');
+ throw new Exception(pht('Unsupported VCS!'));
}
$page->addRemarkupInstructions($instructions, 'remoteURI');
$page->getControl('remoteURI')->setLabel($uri_label);
}
public function validateRemoteURIPage(PHUIFormPageView $page) {
$c_remote = $page->getControl('remoteURI');
$v_remote = $c_remote->getValue();
if (!strlen($v_remote)) {
$c_remote->setError(pht('Required'));
$page->addPageError(
pht('You must specify a URI.'));
} else {
try {
PhabricatorRepository::assertValidRemoteURI($v_remote);
} catch (Exception $ex) {
$c_remote->setError(pht('Invalid'));
$page->addPageError($ex->getMessage());
}
}
return $c_remote->isValid();
}
/* -( Page: Authentication )----------------------------------------------- */
public function buildAuthPage() {
return id(new PHUIFormPageView())
->setPageName(pht('Authentication'))
->setUser($this->getRequest()->getUser())
->setAdjustFormPageCallback(array($this, 'adjustAuthPage'))
->addControl(
id(new PassphraseCredentialControl())
->setName('credential'));
}
public function adjustAuthPage($page) {
$form = $page->getForm();
if ($this->getRepository()) {
$vcs = $this->getRepository()->getVersionControlSystem();
} else {
$vcs = $form->getPage('vcs')->getControl('vcs')->getValue();
}
$remote_uri = $form->getPage('remote-uri')
->getControl('remoteURI')
->getValue();
$proto = PhabricatorRepository::getRemoteURIProtocol($remote_uri);
$remote_user = $this->getRemoteURIUser($remote_uri);
$c_credential = $page->getControl('credential');
$c_credential->setDefaultUsername($remote_user);
if ($this->isSSHProtocol($proto)) {
$c_credential->setLabel(pht('SSH Key'));
$c_credential->setCredentialType(
PassphraseCredentialTypeSSHPrivateKeyText::CREDENTIAL_TYPE);
$provides_type = PassphraseCredentialTypeSSHPrivateKey::PROVIDES_TYPE;
$page->addRemarkupInstructions(
pht(
'Choose or add the SSH credentials to use to connect to the the '.
'repository hosted at:'.
"\n\n".
" lang=text\n".
" %s",
$remote_uri),
'credential');
} else if ($this->isUsernamePasswordProtocol($proto)) {
$c_credential->setLabel(pht('Password'));
$c_credential->setAllowNull(true);
$c_credential->setCredentialType(
PassphraseCredentialTypePassword::CREDENTIAL_TYPE);
$provides_type = PassphraseCredentialTypePassword::PROVIDES_TYPE;
$page->addRemarkupInstructions(
pht(
'Choose the username and password used to connect to the '.
'repository hosted at:'.
"\n\n".
" lang=text\n".
" %s".
"\n\n".
"If this repository does not require a username or password, ".
"you can continue to the next step.",
$remote_uri),
'credential');
} else {
- throw new Exception('Unknown URI protocol!');
+ throw new Exception(pht('Unknown URI protocol!'));
}
if ($provides_type) {
$viewer = $this->getRequest()->getUser();
$options = id(new PassphraseCredentialQuery())
->setViewer($viewer)
->withIsDestroyed(false)
->withProvidesTypes(array($provides_type))
->execute();
$c_credential->setOptions($options);
}
}
public function validateAuthPage(PHUIFormPageView $page) {
$form = $page->getForm();
$remote_uri = $form->getPage('remote')->getControl('remoteURI')->getValue();
$proto = $this->getRemoteURIProtocol($remote_uri);
$c_credential = $page->getControl('credential');
$v_credential = $c_credential->getValue();
// NOTE: We're using the omnipotent user here because the viewer might be
// editing a repository they're allowed to edit which uses a credential they
// are not allowed to see. This is fine, as long as they don't change it.
$credential = id(new PassphraseCredentialQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withPHIDs(array($v_credential))
->executeOne();
if ($this->isSSHProtocol($proto)) {
if (!$credential) {
$c_credential->setError(pht('Required'));
$page->addPageError(
pht('You must choose an SSH credential to connect over SSH.'));
}
$ssh_type = PassphraseCredentialTypeSSHPrivateKey::PROVIDES_TYPE;
if ($credential->getProvidesType() !== $ssh_type) {
$c_credential->setError(pht('Invalid'));
$page->addPageError(
pht(
'You must choose an SSH credential, not some other type '.
'of credential.'));
}
} else if ($this->isUsernamePasswordProtocol($proto)) {
if ($credential) {
$password_type = PassphraseCredentialTypePassword::PROVIDES_TYPE;
if ($credential->getProvidesType() !== $password_type) {
$c_credential->setError(pht('Invalid'));
$page->addPageError(
pht(
'You must choose a username/password credential, not some other '.
'type of credential.'));
}
}
return $c_credential->isValid();
} else {
return true;
}
}
/* -( Page: Policy )------------------------------------------------------- */
private function buildPolicyPage() {
$viewer = $this->getRequest()->getUser();
if ($this->getRepository()) {
$repository = $this->getRepository();
} else {
$repository = PhabricatorRepository::initializeNewRepository($viewer);
}
$policies = id(new PhabricatorPolicyQuery())
->setViewer($viewer)
->setObject($repository)
->execute();
$view_policy = id(new AphrontFormPolicyControl())
->setUser($viewer)
->setCapability(PhabricatorPolicyCapability::CAN_VIEW)
->setPolicyObject($repository)
->setPolicies($policies)
->setName('viewPolicy');
$edit_policy = id(new AphrontFormPolicyControl())
->setUser($viewer)
->setCapability(PhabricatorPolicyCapability::CAN_EDIT)
->setPolicyObject($repository)
->setPolicies($policies)
->setName('editPolicy');
$push_policy = id(new AphrontFormPolicyControl())
->setUser($viewer)
->setCapability(DiffusionPushCapability::CAPABILITY)
->setPolicyObject($repository)
->setPolicies($policies)
->setName('pushPolicy');
return id(new PHUIFormPageView())
->setPageName(pht('Policies'))
->setValidateFormPageCallback(array($this, 'validatePolicyPage'))
->setAdjustFormPageCallback(array($this, 'adjustPolicyPage'))
->setUser($viewer)
->addRemarkupInstructions(
- pht(
- 'Select access policies for this repository.'))
+ pht('Select access policies for this repository.'))
->addControl($view_policy)
->addControl($edit_policy)
->addControl($push_policy);
}
public function adjustPolicyPage(PHUIFormPageView $page) {
if ($this->getRepository()) {
$repository = $this->getRepository();
$show_push = $repository->isHosted();
} else {
$show_push = ($this->edit == 'create');
}
if (!$show_push) {
$c_push = $page->getControl('pushPolicy');
$c_push->setHidden(true);
}
}
public function validatePolicyPage(PHUIFormPageView $page) {
$form = $page->getForm();
$viewer = $this->getRequest()->getUser();
$c_view = $page->getControl('viewPolicy');
$c_edit = $page->getControl('editPolicy');
$c_push = $page->getControl('pushPolicy');
$v_view = $c_view->getValue();
$v_edit = $c_edit->getValue();
$v_push = $c_push->getValue();
if ($this->getRepository()) {
$repository = $this->getRepository();
} else {
$repository = PhabricatorRepository::initializeNewRepository($viewer);
}
$proxy = clone $repository;
$proxy->setViewPolicy($v_view);
$proxy->setEditPolicy($v_edit);
$can_view = PhabricatorPolicyFilter::hasCapability(
$viewer,
$proxy,
PhabricatorPolicyCapability::CAN_VIEW);
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$proxy,
PhabricatorPolicyCapability::CAN_EDIT);
if (!$can_view) {
$c_view->setError(pht('Invalid'));
$page->addPageError(
pht(
'You can not use the selected policy, because you would be unable '.
'to see the repository.'));
}
if (!$can_edit) {
$c_edit->setError(pht('Invalid'));
$page->addPageError(
pht(
'You can not use the selected edit policy, because you would be '.
'unable to edit the repository.'));
}
return $c_view->isValid() &&
$c_edit->isValid();
}
/* -( Page: Done )--------------------------------------------------------- */
private function buildDonePage() {
$is_create = ($this->edit == 'create');
if ($is_create) {
$now_label = pht('Create Repository Now');
$now_caption = pht(
'Create the repository right away. This will create the repository '.
'using default settings.');
$wait_label = pht('Configure More Options First');
$wait_caption = pht(
'Configure more options before creating the repository. '.
'This will let you fine-tune settings. You can create the repository '.
'whenever you are ready.');
} else {
$now_label = pht('Start Import Now');
$now_caption = pht(
'Start importing the repository right away. This will import '.
'the entire repository using default settings.');
$wait_label = pht('Configure More Options First');
$wait_caption = pht(
'Configure more options before beginning the repository '.
'import. This will let you fine-tune settings. You can '.
'start the import whenever you are ready.');
}
return id(new PHUIFormPageView())
->setPageName(pht('Repository Ready!'))
->setValidateFormPageCallback(array($this, 'validateDonePage'))
->setUser($this->getRequest()->getUser())
->addControl(
id(new AphrontFormRadioButtonControl())
->setName('activate')
->setLabel(pht('Start Now'))
->addButton(
'start',
$now_label,
$now_caption)
->addButton(
'wait',
$wait_label,
$wait_caption));
}
public function validateDonePage(PHUIFormPageView $page) {
$c_activate = $page->getControl('activate');
$v_activate = $c_activate->getValue();
if ($v_activate != 'start' && $v_activate != 'wait') {
$c_activate->setError(pht('Required'));
$page->addPageError(
pht('Make a choice about repository activation.'));
}
return $c_activate->isValid();
}
/* -( Internal )----------------------------------------------------------- */
private function getRemoteURIUser($raw_uri) {
$uri = new PhutilURI($raw_uri);
if ($uri->getUser()) {
return $uri->getUser();
}
$git_uri = new PhutilGitURI($raw_uri);
if (strlen($git_uri->getDomain()) && strlen($git_uri->getPath())) {
return $git_uri->getUser();
}
return null;
}
private function isSSHProtocol($proto) {
return ($proto == 'git' || $proto == 'ssh' || $proto == 'svn+ssh');
}
private function isUsernamePasswordProtocol($proto) {
return ($proto == 'http' || $proto == 'https' || $proto == 'svn');
}
private function setRepository(PhabricatorRepository $repository) {
$this->repository = $repository;
return $this;
}
private function getRepository() {
return $this->repository;
}
}
diff --git a/src/applications/diffusion/controller/DiffusionRepositoryEditActionsController.php b/src/applications/diffusion/controller/DiffusionRepositoryEditActionsController.php
index 3c149e8ce..2b0bc674b 100644
--- a/src/applications/diffusion/controller/DiffusionRepositoryEditActionsController.php
+++ b/src/applications/diffusion/controller/DiffusionRepositoryEditActionsController.php
@@ -1,123 +1,122 @@
<?php
final class DiffusionRepositoryEditActionsController
extends DiffusionRepositoryEditController {
protected function processDiffusionRequest(AphrontRequest $request) {
$viewer = $request->getUser();
$drequest = $this->diffusionRequest;
$repository = $drequest->getRepository();
$repository = id(new PhabricatorRepositoryQuery())
->setViewer($viewer)
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->withIDs(array($repository->getID()))
->executeOne();
if (!$repository) {
return new Aphront404Response();
}
$edit_uri = $this->getRepositoryControllerURI($repository, 'edit/');
// NOTE: We're inverting these here, because the storage is silly.
$v_notify = !$repository->getHumanReadableDetail('herald-disabled');
$v_autoclose = !$repository->getHumanReadableDetail('disable-autoclose');
if ($request->isFormPost()) {
$v_notify = $request->getBool('notify');
$v_autoclose = $request->getBool('autoclose');
$xactions = array();
$template = id(new PhabricatorRepositoryTransaction());
$type_notify = PhabricatorRepositoryTransaction::TYPE_NOTIFY;
$type_autoclose = PhabricatorRepositoryTransaction::TYPE_AUTOCLOSE;
$xactions[] = id(clone $template)
->setTransactionType($type_notify)
->setNewValue($v_notify);
$xactions[] = id(clone $template)
->setTransactionType($type_autoclose)
->setNewValue($v_autoclose);
id(new PhabricatorRepositoryEditor())
->setContinueOnNoEffect(true)
->setContentSourceFromRequest($request)
->setActor($viewer)
->applyTransactions($repository, $xactions);
return id(new AphrontRedirectResponse())->setURI($edit_uri);
}
$content = array();
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb(pht('Edit Actions'));
$title = pht('Edit Actions (%s)', $repository->getName());
$policies = id(new PhabricatorPolicyQuery())
->setViewer($viewer)
->setObject($repository)
->execute();
$form = id(new AphrontFormView())
->setUser($viewer)
->appendRemarkupInstructions(
pht(
"Normally, Phabricator publishes notifications when it discovers ".
"new commits. You can disable publishing for this repository by ".
"turning off **Notify/Publish**. This will disable notifications, ".
"feed, and Herald (including audits and build plans) for this ".
- "repository.".
- "\n\n".
+ "repository.\n\n".
"When Phabricator discovers a new commit, it can automatically ".
"close associated revisions and tasks. If you don't want ".
"Phabricator to close objects when it discovers new commits in ".
"this repository, you can disable **Autoclose**."))
->appendChild(
id(new AphrontFormSelectControl())
->setName('notify')
->setLabel(pht('Notify/Publish'))
->setValue((int)$v_notify)
->setOptions(
array(
1 => pht('Enable Notifications, Feed and Herald'),
0 => pht('Disable Notifications, Feed and Herald'),
)))
->appendChild(
id(new AphrontFormSelectControl())
->setName('autoclose')
->setLabel(pht('Autoclose'))
->setValue((int)$v_autoclose)
->setOptions(
array(
1 => pht('Enable Autoclose'),
0 => pht('Disable Autoclose'),
)))
->appendChild(
id(new AphrontFormSubmitControl())
->setValue(pht('Save Actions'))
->addCancelButton($edit_uri));
$form_box = id(new PHUIObjectBoxView())
->setHeaderText($title)
->setForm($form);
return $this->buildApplicationPage(
array(
$crumbs,
$form_box,
),
array(
'title' => $title,
));
}
}
diff --git a/src/applications/diffusion/controller/DiffusionRepositoryEditBasicController.php b/src/applications/diffusion/controller/DiffusionRepositoryEditBasicController.php
index 986f46cec..80771f65c 100644
--- a/src/applications/diffusion/controller/DiffusionRepositoryEditBasicController.php
+++ b/src/applications/diffusion/controller/DiffusionRepositoryEditBasicController.php
@@ -1,170 +1,176 @@
<?php
final class DiffusionRepositoryEditBasicController
extends DiffusionRepositoryEditController {
protected function processDiffusionRequest(AphrontRequest $request) {
$user = $request->getUser();
$drequest = $this->diffusionRequest;
$repository = $drequest->getRepository();
$repository = id(new PhabricatorRepositoryQuery())
->setViewer($user)
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->needProjectPHIDs(true)
->withIDs(array($repository->getID()))
->executeOne();
if (!$repository) {
return new Aphront404Response();
}
$edit_uri = $this->getRepositoryControllerURI($repository, 'edit/');
$v_name = $repository->getName();
$v_desc = $repository->getDetail('description');
$v_clone_name = $repository->getDetail('clone-name');
$e_name = true;
$errors = array();
if ($request->isFormPost()) {
$v_name = $request->getStr('name');
$v_desc = $request->getStr('description');
$v_projects = $request->getArr('projectPHIDs');
if ($repository->isHosted()) {
$v_clone_name = $request->getStr('cloneName');
}
if (!strlen($v_name)) {
$e_name = pht('Required');
$errors[] = pht('Repository name is required.');
} else {
$e_name = null;
}
if (!$errors) {
$xactions = array();
$template = id(new PhabricatorRepositoryTransaction());
$type_name = PhabricatorRepositoryTransaction::TYPE_NAME;
$type_desc = PhabricatorRepositoryTransaction::TYPE_DESCRIPTION;
$type_edge = PhabricatorTransactions::TYPE_EDGE;
$type_clone_name = PhabricatorRepositoryTransaction::TYPE_CLONE_NAME;
$xactions[] = id(clone $template)
->setTransactionType($type_name)
->setNewValue($v_name);
$xactions[] = id(clone $template)
->setTransactionType($type_desc)
->setNewValue($v_desc);
$xactions[] = id(clone $template)
->setTransactionType($type_clone_name)
->setNewValue($v_clone_name);
$xactions[] = id(clone $template)
->setTransactionType($type_edge)
->setMetadataValue(
'edge:type',
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST)
->setNewValue(
array(
'=' => array_fuse($v_projects),
));
id(new PhabricatorRepositoryEditor())
->setContinueOnNoEffect(true)
->setContentSourceFromRequest($request)
->setActor($user)
->applyTransactions($repository, $xactions);
return id(new AphrontRedirectResponse())->setURI($edit_uri);
}
}
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb(pht('Edit Basics'));
$title = pht('Edit %s', $repository->getName());
$form = id(new AphrontFormView())
->setUser($user)
->appendChild(
id(new AphrontFormTextControl())
->setName('name')
->setLabel(pht('Name'))
->setValue($v_name)
->setError($e_name));
if ($repository->isHosted()) {
$form
->appendChild(
id(new AphrontFormTextControl())
->setName('cloneName')
->setLabel(pht('Clone/Checkout As'))
->setValue($v_clone_name)
->setCaption(
pht(
'Optional directory name to use when cloning or checking out '.
'this repository.')));
}
$form
->appendChild(
id(new PhabricatorRemarkupControl())
->setUser($user)
->setName('description')
->setLabel(pht('Description'))
->setValue($v_desc))
->appendControl(
id(new AphrontFormTokenizerControl())
->setDatasource(new PhabricatorProjectDatasource())
->setName('projectPHIDs')
->setLabel(pht('Projects'))
->setValue($repository->getProjectPHIDs()))
->appendChild(
id(new AphrontFormSubmitControl())
->setValue(pht('Save'))
->addCancelButton($edit_uri))
->appendChild(id(new PHUIFormDividerControl()))
->appendRemarkupInstructions($this->getReadmeInstructions());
$object_box = id(new PHUIObjectBoxView())
->setHeaderText($title)
->setForm($form)
->setFormErrors($errors);
return $this->buildApplicationPage(
array(
$crumbs,
$object_box,
),
array(
'title' => $title,
));
}
private function getReadmeInstructions() {
return pht(<<<EOTEXT
-You can also create a `README` file at the repository root (or in any
+You can also create a `%s` file at the repository root (or in any
subdirectory) to provide information about the repository. These formats are
supported:
-| File Name | Rendered As... |
-|-----------------|----------------|
-| `README` | Plain Text |
-| `README.txt` | Plain Text |
-| `README.remarkup` | Remarkup |
-| `README.md` | Remarkup |
-| `README.rainbow` | \xC2\xA1Fiesta! |
+| File Name | Rendered As... |
+|-----------|-----------------|
+| `%s` | Plain Text |
+| `%s` | Plain Text |
+| `%s` | Remarkup |
+| `%s` | Remarkup |
+| `%s` | \xC2\xA1Fiesta! |
EOTEXT
-);
+ ,
+ 'README',
+ 'README',
+ 'README.txt',
+ 'README.remarkup',
+ 'README.md',
+ 'README.rainbow');
}
}
diff --git a/src/applications/diffusion/controller/DiffusionRepositoryEditBranchesController.php b/src/applications/diffusion/controller/DiffusionRepositoryEditBranchesController.php
index 2d0a7e53f..a175fba94 100644
--- a/src/applications/diffusion/controller/DiffusionRepositoryEditBranchesController.php
+++ b/src/applications/diffusion/controller/DiffusionRepositoryEditBranchesController.php
@@ -1,251 +1,251 @@
<?php
final class DiffusionRepositoryEditBranchesController
extends DiffusionRepositoryEditController {
protected function processDiffusionRequest(AphrontRequest $request) {
$request = $this->getRequest();
$viewer = $request->getUser();
$drequest = $this->diffusionRequest;
$repository = $drequest->getRepository();
$repository = id(new PhabricatorRepositoryQuery())
->setViewer($viewer)
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->withIDs(array($repository->getID()))
->executeOne();
if (!$repository) {
return new Aphront404Response();
}
$is_git = false;
$is_hg = false;
switch ($repository->getVersionControlSystem()) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
$is_git = true;
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
$is_hg = true;
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
throw new Exception(
pht('Subversion does not support branches!'));
default:
throw new Exception(
pht('Repository has unknown version control system!'));
}
$edit_uri = $this->getRepositoryControllerURI($repository, 'edit/');
$v_default = $repository->getHumanReadableDetail('default-branch');
$v_track = $repository->getDetail(
'branch-filter',
array());
$v_track = array_keys($v_track);
$v_autoclose = $repository->getDetail(
'close-commits-filter',
array());
$v_autoclose = array_keys($v_autoclose);
$e_track = null;
$e_autoclose = null;
$validation_exception = null;
if ($request->isFormPost()) {
$v_default = $request->getStr('default');
$v_track = $this->processBranches($request->getStr('track'));
if (!$is_hg) {
$v_autoclose = $this->processBranches($request->getStr('autoclose'));
}
$xactions = array();
$template = id(new PhabricatorRepositoryTransaction());
$type_default = PhabricatorRepositoryTransaction::TYPE_DEFAULT_BRANCH;
$type_track = PhabricatorRepositoryTransaction::TYPE_TRACK_ONLY;
$type_autoclose = PhabricatorRepositoryTransaction::TYPE_AUTOCLOSE_ONLY;
$xactions[] = id(clone $template)
->setTransactionType($type_default)
->setNewValue($v_default);
$xactions[] = id(clone $template)
->setTransactionType($type_track)
->setNewValue($v_track);
if (!$is_hg) {
$xactions[] = id(clone $template)
->setTransactionType($type_autoclose)
->setNewValue($v_autoclose);
}
$editor = id(new PhabricatorRepositoryEditor())
->setContinueOnNoEffect(true)
->setContentSourceFromRequest($request)
->setActor($viewer);
try {
$editor->applyTransactions($repository, $xactions);
return id(new AphrontRedirectResponse())->setURI($edit_uri);
} catch (PhabricatorApplicationTransactionValidationException $ex) {
$validation_exception = $ex;
$e_track = $validation_exception->getShortMessage($type_track);
$e_autoclose = $validation_exception->getShortMessage($type_autoclose);
}
}
$content = array();
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb(pht('Edit Branches'));
$title = pht('Edit Branches (%s)', $repository->getName());
$policies = id(new PhabricatorPolicyQuery())
->setViewer($viewer)
->setObject($repository)
->execute();
$rows = array();
$rows[] = array(
array(
'master',
),
pht('Select only master.'),
);
$rows[] = array(
array(
'master',
'develop',
'release',
),
- pht('Select master, develop, and release.'),
+ pht('Select %s, %s, and %s.', 'master', 'develop', 'release'),
);
$rows[] = array(
array(
'master',
'regexp(/^release-/)',
),
- pht('Select master, and all branches which start with "release-".'),
+ pht('Select master, and all branches which start with "%s".', 'release-'),
);
$rows[] = array(
array(
'regexp(/^(?!temp-)/)',
),
- pht('Select all branches which do not start with "temp-".'),
+ pht('Select all branches which do not start with "%s".', 'temp-'),
);
foreach ($rows as $k => $row) {
$rows[$k][0] = phutil_tag(
'pre',
array(),
implode("\n", $row[0]));
}
$example_table = id(new AphrontTableView($rows))
->setHeaders(
array(
pht('Example'),
pht('Effect'),
))
->setColumnClasses(
array(
'',
'wide',
));
$v_track = implode("\n", $v_track);
$v_autoclose = implode("\n", $v_autoclose);
$form = id(new AphrontFormView())
->setUser($viewer)
->appendRemarkupInstructions(
- pht(
- 'You can choose a **Default Branch** for viewing this repository.'))
+ pht('You can choose a **Default Branch** for viewing this repository.'))
->appendChild(
id(new AphrontFormTextControl())
->setName('default')
->setLabel(pht('Default Branch'))
->setValue($v_default))
->appendRemarkupInstructions(
pht(
'If you want to import only some branches into Diffusion, you can '.
'list them in **Track Only**. Other branches will be ignored. If '.
'you do not specify any branches, all branches are tracked.'));
if (!$is_hg) {
$form->appendRemarkupInstructions(
pht(
'If you have **Autoclose** enabled for this repository, Phabricator '.
'can close tasks and revisions when corresponding commits are '.
'pushed to the repository. If you want to autoclose objects only '.
'when commits appear on specific branches, you can list those '.
'branches in **Autoclose Only**. By default, all tracked branches '.
'will autoclose objects.'));
}
$form
->appendRemarkupInstructions(
pht(
'When specifying branches, you should enter one branch name per '.
'line. You can use regular expressions to match branches by '.
- 'wrapping an expression in `regexp(...)`. For example:'))
+ 'wrapping an expression in `%s`. For example:',
+ 'regexp(...)'))
->appendChild(
id(new AphrontFormMarkupControl())
->setValue($example_table))
->appendChild(
id(new AphrontFormTextAreaControl())
->setName('track')
->setLabel(pht('Track Only'))
->setError($e_track)
->setValue($v_track));
if (!$is_hg) {
$form->appendChild(
id(new AphrontFormTextAreaControl())
->setName('autoclose')
->setLabel(pht('Autoclose Only'))
->setError($e_autoclose)
->setValue($v_autoclose));
}
$form->appendChild(
id(new AphrontFormSubmitControl())
->setValue(pht('Save Branches'))
->addCancelButton($edit_uri));
$form_box = id(new PHUIObjectBoxView())
->setValidationException($validation_exception)
->setHeaderText($title)
->setForm($form);
return $this->buildApplicationPage(
array(
$crumbs,
$form_box,
),
array(
'title' => $title,
));
}
private function processBranches($string) {
$lines = phutil_split_lines($string, $retain_endings = false);
foreach ($lines as $key => $line) {
$lines[$key] = trim($line);
if (!strlen($lines[$key])) {
unset($lines[$key]);
}
}
return array_values($lines);
}
}
diff --git a/src/applications/diffusion/controller/DiffusionRepositoryEditDeleteController.php b/src/applications/diffusion/controller/DiffusionRepositoryEditDeleteController.php
index 34a843165..f39708fe1 100644
--- a/src/applications/diffusion/controller/DiffusionRepositoryEditDeleteController.php
+++ b/src/applications/diffusion/controller/DiffusionRepositoryEditDeleteController.php
@@ -1,57 +1,58 @@
<?php
final class DiffusionRepositoryEditDeleteController
extends DiffusionRepositoryEditController {
protected function processDiffusionRequest(AphrontRequest $request) {
$viewer = $request->getUser();
$drequest = $this->diffusionRequest;
$repository = $drequest->getRepository();
$repository = id(new PhabricatorRepositoryQuery())
->setViewer($viewer)
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->withIDs(array($repository->getID()))
->executeOne();
if (!$repository) {
return new Aphront404Response();
}
$edit_uri = $this->getRepositoryControllerURI($repository, 'edit/');
$dialog = new AphrontDialogView();
$text_1 = pht(
'If you really want to delete the repository, run this command from '.
'the command line:');
$command = csprintf(
'phabricator/ $ ./bin/remove destroy %R',
$repository->getMonogram());
- $text_2 = pht('Repositories touch many objects and as such deletes are '.
- 'prohibitively expensive to run from the web UI.');
+ $text_2 = pht(
+ 'Repositories touch many objects and as such deletes are '.
+ 'prohibitively expensive to run from the web UI.');
$body = phutil_tag(
'div',
array(
'class' => 'phabricator-remarkup',
),
array(
phutil_tag('p', array(), $text_1),
phutil_tag('p', array(),
phutil_tag('tt', array(), $command)),
phutil_tag('p', array(), $text_2),
));
$dialog = id(new AphrontDialogView())
->setUser($request->getUser())
->setTitle(pht('Really want to delete the repository?'))
->appendChild($body)
->addCancelButton($edit_uri, pht('Okay'));
return id(new AphrontDialogResponse())->setDialog($dialog);
}
}
diff --git a/src/applications/diffusion/controller/DiffusionRepositoryEditEncodingController.php b/src/applications/diffusion/controller/DiffusionRepositoryEditEncodingController.php
index 1abbad652..f71516e6c 100644
--- a/src/applications/diffusion/controller/DiffusionRepositoryEditEncodingController.php
+++ b/src/applications/diffusion/controller/DiffusionRepositoryEditEncodingController.php
@@ -1,111 +1,110 @@
<?php
final class DiffusionRepositoryEditEncodingController
extends DiffusionRepositoryEditController {
protected function processDiffusionRequest(AphrontRequest $request) {
$user = $request->getUser();
$drequest = $this->diffusionRequest;
$repository = $drequest->getRepository();
$repository = id(new PhabricatorRepositoryQuery())
->setViewer($user)
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->withIDs(array($repository->getID()))
->executeOne();
if (!$repository) {
return new Aphront404Response();
}
$edit_uri = $this->getRepositoryControllerURI($repository, 'edit/');
$v_encoding = $repository->getDetail('encoding');
$e_encoding = null;
$errors = array();
if ($request->isFormPost()) {
$v_encoding = $request->getStr('encoding');
if (!$errors) {
$xactions = array();
$template = id(new PhabricatorRepositoryTransaction());
$type_encoding = PhabricatorRepositoryTransaction::TYPE_ENCODING;
$xactions[] = id(clone $template)
->setTransactionType($type_encoding)
->setNewValue($v_encoding);
try {
id(new PhabricatorRepositoryEditor())
->setContinueOnNoEffect(true)
->setContentSourceFromRequest($request)
->setActor($user)
->applyTransactions($repository, $xactions);
return id(new AphrontRedirectResponse())->setURI($edit_uri);
} catch (Exception $ex) {
$errors[] = $ex->getMessage();
}
}
}
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb(pht('Edit Encoding'));
$title = pht('Edit %s', $repository->getName());
$form = id(new AphrontFormView())
->setUser($user)
->appendRemarkupInstructions($this->getEncodingInstructions())
->appendChild(
id(new AphrontFormTextControl())
->setName('encoding')
->setLabel(pht('Text Encoding'))
->setValue($v_encoding)
->setError($e_encoding))
->appendChild(
id(new AphrontFormSubmitControl())
->setValue(pht('Save Encoding'))
->addCancelButton($edit_uri));
$object_box = id(new PHUIObjectBoxView())
->setHeaderText($title)
->setForm($form)
->setFormErrors($errors);
return $this->buildApplicationPage(
array(
$crumbs,
$object_box,
),
array(
'title' => $title,
));
}
private function getEncodingInstructions() {
return pht(<<<EOT
If source code in this repository uses a character encoding other than UTF-8
(for example, `ISO-8859-1`), specify it here.
**Normally, you can leave this field blank.** If your source code is written in
ASCII or UTF-8, everything will work correctly.
Source files will be translated from the specified encoding to UTF-8 when they
are read from the repository, before they are displayed in Diffusion.
See [[%s | UTF-8 and Character Encoding]] for more information on how
Phabricator handles text encodings.
EOT
,
- PhabricatorEnv::getDoclink(
- 'User Guide: UTF-8 and Character Encoding'));
+ PhabricatorEnv::getDoclink('User Guide: UTF-8 and Character Encoding'));
}
}
diff --git a/src/applications/diffusion/controller/DiffusionRepositoryEditMainController.php b/src/applications/diffusion/controller/DiffusionRepositoryEditMainController.php
index a5ad77f87..905021ab2 100644
--- a/src/applications/diffusion/controller/DiffusionRepositoryEditMainController.php
+++ b/src/applications/diffusion/controller/DiffusionRepositoryEditMainController.php
@@ -1,1255 +1,1256 @@
<?php
final class DiffusionRepositoryEditMainController
extends DiffusionRepositoryEditController {
protected function processDiffusionRequest(AphrontRequest $request) {
$viewer = $request->getUser();
$drequest = $this->diffusionRequest;
$repository = $drequest->getRepository();
PhabricatorPolicyFilter::requireCapability(
$viewer,
$repository,
PhabricatorPolicyCapability::CAN_EDIT);
$is_svn = false;
$is_git = false;
$is_hg = false;
switch ($repository->getVersionControlSystem()) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
$is_git = true;
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
$is_svn = true;
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
$is_hg = true;
break;
}
$has_branches = ($is_git || $is_hg);
$has_local = $repository->usesLocalWorkingCopy();
$crumbs = $this->buildApplicationCrumbs($is_main = true);
$title = pht('Edit %s', $repository->getName());
$header = id(new PHUIHeaderView())
->setHeader($title);
if ($repository->isTracked()) {
$header->setStatus('fa-check', 'bluegrey', pht('Active'));
} else {
$header->setStatus('fa-ban', 'dark', pht('Inactive'));
}
$basic_actions = $this->buildBasicActions($repository);
$basic_properties =
$this->buildBasicProperties($repository, $basic_actions);
$policy_actions = $this->buildPolicyActions($repository);
$policy_properties =
$this->buildPolicyProperties($repository, $policy_actions);
$remote_properties = null;
if (!$repository->isHosted()) {
$remote_properties = $this->buildRemoteProperties(
$repository,
$this->buildRemoteActions($repository));
}
$encoding_actions = $this->buildEncodingActions($repository);
$encoding_properties =
$this->buildEncodingProperties($repository, $encoding_actions);
$symbols_actions = $this->buildSymbolsActions($repository);
$symbols_properties =
$this->buildSymbolsProperties($repository, $symbols_actions);
$hosting_properties = $this->buildHostingProperties(
$repository,
$this->buildHostingActions($repository));
$branches_properties = null;
if ($has_branches) {
$branches_properties = $this->buildBranchesProperties(
$repository,
$this->buildBranchesActions($repository));
}
$subversion_properties = null;
if ($is_svn) {
$subversion_properties = $this->buildSubversionProperties(
$repository,
$this->buildSubversionActions($repository));
}
$storage_properties = null;
if ($has_local) {
$storage_properties = $this->buildStorageProperties(
$repository,
$this->buildStorageActions($repository));
}
$actions_properties = $this->buildActionsProperties(
$repository,
$this->buildActionsActions($repository));
$timeline = $this->buildTransactionTimeline(
$repository,
new PhabricatorRepositoryTransactionQuery());
$timeline->setShouldTerminate(true);
$boxes = array();
$boxes[] = id(new PHUIObjectBoxView())
->setHeader($header)
->addPropertyList($basic_properties);
$boxes[] = id(new PHUIObjectBoxView())
->setHeaderText(pht('Policies'))
->addPropertyList($policy_properties);
$boxes[] = id(new PHUIObjectBoxView())
->setHeaderText(pht('Hosting'))
->addPropertyList($hosting_properties);
if ($repository->canMirror()) {
$mirror_actions = $this->buildMirrorActions($repository);
$mirror_properties = $this->buildMirrorProperties(
$repository,
$mirror_actions);
$mirrors = id(new PhabricatorRepositoryMirrorQuery())
->setViewer($viewer)
->withRepositoryPHIDs(array($repository->getPHID()))
->execute();
$mirror_list = $this->buildMirrorList($repository, $mirrors);
$boxes[] = id(new PhabricatorAnchorView())->setAnchorName('mirrors');
$mirror_info = array();
if (PhabricatorEnv::getEnvConfig('phabricator.silent')) {
$mirror_info[] = pht(
'Phabricator is running in silent mode, so changes will not '.
'be pushed to mirrors.');
}
$boxes[] = id(new PHUIObjectBoxView())
->setFormErrors($mirror_info)
->setHeaderText(pht('Mirrors'))
->addPropertyList($mirror_properties);
$boxes[] = $mirror_list;
}
if ($remote_properties) {
$boxes[] = id(new PHUIObjectBoxView())
->setHeaderText(pht('Remote'))
->addPropertyList($remote_properties);
}
if ($storage_properties) {
$boxes[] = id(new PHUIObjectBoxView())
->setHeaderText(pht('Storage'))
->addPropertyList($storage_properties);
}
$boxes[] = id(new PHUIObjectBoxView())
->setHeaderText(pht('Text Encoding'))
->addPropertyList($encoding_properties);
$boxes[] = id(new PHUIObjectBoxView())
->setHeaderText(pht('Symbols'))
->addPropertyList($symbols_properties);
if ($branches_properties) {
$boxes[] = id(new PHUIObjectBoxView())
->setHeaderText(pht('Branches'))
->addPropertyList($branches_properties);
}
if ($subversion_properties) {
$boxes[] = id(new PHUIObjectBoxView())
->setHeaderText(pht('Subversion'))
->addPropertyList($subversion_properties);
}
$boxes[] = id(new PHUIObjectBoxView())
->setHeaderText(pht('Actions'))
->addPropertyList($actions_properties);
return $this->buildApplicationPage(
array(
$crumbs,
$boxes,
$timeline,
),
array(
'title' => $title,
));
}
private function buildBasicActions(PhabricatorRepository $repository) {
$viewer = $this->getRequest()->getUser();
$view = id(new PhabricatorActionListView())
->setObjectURI($this->getRequest()->getRequestURI())
->setUser($viewer);
$edit = id(new PhabricatorActionView())
->setIcon('fa-pencil')
->setName(pht('Edit Basic Information'))
->setHref($this->getRepositoryControllerURI($repository, 'edit/basic/'));
$view->addAction($edit);
$edit = id(new PhabricatorActionView())
->setIcon('fa-refresh')
->setName(pht('Update Now'))
->setWorkflow(true)
->setHref(
$this->getRepositoryControllerURI($repository, 'edit/update/'));
$view->addAction($edit);
$activate = id(new PhabricatorActionView())
->setHref(
$this->getRepositoryControllerURI($repository, 'edit/activate/'))
->setWorkflow(true);
if ($repository->isTracked()) {
$activate
->setIcon('fa-pause')
->setName(pht('Deactivate Repository'));
} else {
$activate
->setIcon('fa-play')
->setName(pht('Activate Repository'));
}
$view->addAction($activate);
$view->addAction(
id(new PhabricatorActionView())
->setName(pht('Delete Repository'))
->setIcon('fa-times')
->setHref(
$this->getRepositoryControllerURI($repository, 'edit/delete/'))
->setDisabled(true)
->setWorkflow(true));
return $view;
}
private function buildBasicProperties(
PhabricatorRepository $repository,
PhabricatorActionListView $actions) {
$viewer = $this->getRequest()->getUser();
$view = id(new PHUIPropertyListView())
->setUser($viewer)
->setActionList($actions);
$type = PhabricatorRepositoryType::getNameForRepositoryType(
$repository->getVersionControlSystem());
$view->addProperty(pht('Type'), $type);
$view->addProperty(pht('Callsign'), $repository->getCallsign());
$clone_name = $repository->getDetail('clone-name');
if ($repository->isHosted()) {
$view->addProperty(
pht('Clone/Checkout As'),
$clone_name
? $clone_name.'/'
: phutil_tag('em', array(), $repository->getCloneName().'/'));
}
$project_phids = PhabricatorEdgeQuery::loadDestinationPHIDs(
$repository->getPHID(),
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST);
if ($project_phids) {
$project_text = $viewer->renderHandleList($project_phids);
} else {
$project_text = phutil_tag('em', array(), pht('None'));
}
$view->addProperty(
pht('Projects'),
$project_text);
$view->addProperty(
pht('Status'),
$this->buildRepositoryStatus($repository));
$view->addProperty(
pht('Update Frequency'),
$this->buildRepositoryUpdateInterval($repository));
$description = $repository->getDetail('description');
$view->addSectionHeader(pht('Description'));
if (!strlen($description)) {
$description = phutil_tag('em', array(), pht('No description provided.'));
} else {
$description = PhabricatorMarkupEngine::renderOneObject(
$repository,
'description',
$viewer);
}
$view->addTextContent($description);
return $view;
}
private function buildEncodingActions(PhabricatorRepository $repository) {
$viewer = $this->getRequest()->getUser();
$view = id(new PhabricatorActionListView())
->setObjectURI($this->getRequest()->getRequestURI())
->setUser($viewer);
$edit = id(new PhabricatorActionView())
->setIcon('fa-pencil')
->setName(pht('Edit Text Encoding'))
->setHref(
$this->getRepositoryControllerURI($repository, 'edit/encoding/'));
$view->addAction($edit);
return $view;
}
private function buildEncodingProperties(
PhabricatorRepository $repository,
PhabricatorActionListView $actions) {
$viewer = $this->getRequest()->getUser();
$view = id(new PHUIPropertyListView())
->setUser($viewer)
->setActionList($actions);
$encoding = $repository->getDetail('encoding');
if (!$encoding) {
$encoding = phutil_tag('em', array(), pht('Use Default (UTF-8)'));
}
$view->addProperty(pht('Encoding'), $encoding);
return $view;
}
private function buildPolicyActions(PhabricatorRepository $repository) {
$viewer = $this->getRequest()->getUser();
$view = id(new PhabricatorActionListView())
->setObjectURI($this->getRequest()->getRequestURI())
->setUser($viewer);
$edit = id(new PhabricatorActionView())
->setIcon('fa-pencil')
->setName(pht('Edit Policies'))
->setHref(
$this->getRepositoryControllerURI($repository, 'edit/policy/'));
$view->addAction($edit);
return $view;
}
private function buildPolicyProperties(
PhabricatorRepository $repository,
PhabricatorActionListView $actions) {
$viewer = $this->getRequest()->getUser();
$view = id(new PHUIPropertyListView())
->setUser($viewer)
->setActionList($actions);
$descriptions = PhabricatorPolicyQuery::renderPolicyDescriptions(
$viewer,
$repository);
$view->addProperty(
pht('Visible To'),
$descriptions[PhabricatorPolicyCapability::CAN_VIEW]);
$view->addProperty(
pht('Editable By'),
$descriptions[PhabricatorPolicyCapability::CAN_EDIT]);
$pushable = $repository->isHosted()
? $descriptions[DiffusionPushCapability::CAPABILITY]
: phutil_tag('em', array(), pht('Not a Hosted Repository'));
$view->addProperty(pht('Pushable By'), $pushable);
return $view;
}
private function buildBranchesActions(PhabricatorRepository $repository) {
$viewer = $this->getRequest()->getUser();
$view = id(new PhabricatorActionListView())
->setObjectURI($this->getRequest()->getRequestURI())
->setUser($viewer);
$edit = id(new PhabricatorActionView())
->setIcon('fa-pencil')
->setName(pht('Edit Branches'))
->setHref(
$this->getRepositoryControllerURI($repository, 'edit/branches/'));
$view->addAction($edit);
return $view;
}
private function buildBranchesProperties(
PhabricatorRepository $repository,
PhabricatorActionListView $actions) {
$viewer = $this->getRequest()->getUser();
$view = id(new PHUIPropertyListView())
->setUser($viewer)
->setActionList($actions);
$default_branch = nonempty(
$repository->getHumanReadableDetail('default-branch'),
phutil_tag('em', array(), $repository->getDefaultBranch()));
$view->addProperty(pht('Default Branch'), $default_branch);
$track_only = nonempty(
$repository->getHumanReadableDetail('branch-filter', array()),
phutil_tag('em', array(), pht('Track All Branches')));
$view->addProperty(pht('Track Only'), $track_only);
$autoclose_only = nonempty(
$repository->getHumanReadableDetail('close-commits-filter', array()),
phutil_tag('em', array(), pht('Autoclose On All Branches')));
if ($repository->getDetail('disable-autoclose')) {
$autoclose_only = phutil_tag('em', array(), pht('Disabled'));
}
$view->addProperty(pht('Autoclose Only'), $autoclose_only);
return $view;
}
private function buildSubversionActions(PhabricatorRepository $repository) {
$viewer = $this->getRequest()->getUser();
$view = id(new PhabricatorActionListView())
->setObjectURI($this->getRequest()->getRequestURI())
->setUser($viewer);
$edit = id(new PhabricatorActionView())
->setIcon('fa-pencil')
->setName(pht('Edit Subversion Info'))
->setHref(
$this->getRepositoryControllerURI($repository, 'edit/subversion/'));
$view->addAction($edit);
return $view;
}
private function buildSubversionProperties(
PhabricatorRepository $repository,
PhabricatorActionListView $actions) {
$viewer = $this->getRequest()->getUser();
$view = id(new PHUIPropertyListView())
->setUser($viewer)
->setActionList($actions);
$svn_uuid = nonempty(
$repository->getUUID(),
phutil_tag('em', array(), pht('Not Configured')));
$view->addProperty(pht('Subversion UUID'), $svn_uuid);
$svn_subpath = nonempty(
$repository->getHumanReadableDetail('svn-subpath'),
phutil_tag('em', array(), pht('Import Entire Repository')));
$view->addProperty(pht('Import Only'), $svn_subpath);
return $view;
}
private function buildActionsActions(PhabricatorRepository $repository) {
$viewer = $this->getRequest()->getUser();
$view = id(new PhabricatorActionListView())
->setObjectURI($this->getRequest()->getRequestURI())
->setUser($viewer);
$edit = id(new PhabricatorActionView())
->setIcon('fa-pencil')
->setName(pht('Edit Actions'))
->setHref(
$this->getRepositoryControllerURI($repository, 'edit/actions/'));
$view->addAction($edit);
return $view;
}
private function buildActionsProperties(
PhabricatorRepository $repository,
PhabricatorActionListView $actions) {
$viewer = $this->getRequest()->getUser();
$view = id(new PHUIPropertyListView())
->setUser($viewer)
->setActionList($actions);
$notify = $repository->getDetail('herald-disabled')
? pht('Off')
: pht('On');
$notify = phutil_tag('em', array(), $notify);
$view->addProperty(pht('Publish/Notify'), $notify);
$autoclose = $repository->getDetail('disable-autoclose')
? pht('Off')
: pht('On');
$autoclose = phutil_tag('em', array(), $autoclose);
$view->addProperty(pht('Autoclose'), $autoclose);
return $view;
}
private function buildRemoteActions(PhabricatorRepository $repository) {
$viewer = $this->getRequest()->getUser();
$view = id(new PhabricatorActionListView())
->setObjectURI($this->getRequest()->getRequestURI())
->setUser($viewer);
$edit = id(new PhabricatorActionView())
->setIcon('fa-pencil')
->setName(pht('Edit Remote'))
->setHref(
$this->getRepositoryControllerURI($repository, 'edit/remote/'));
$view->addAction($edit);
return $view;
}
private function buildRemoteProperties(
PhabricatorRepository $repository,
PhabricatorActionListView $actions) {
$viewer = $this->getRequest()->getUser();
$view = id(new PHUIPropertyListView())
->setUser($viewer)
->setActionList($actions);
$view->addProperty(
pht('Remote URI'),
$repository->getHumanReadableDetail('remote-uri'));
$credential_phid = $repository->getCredentialPHID();
if ($credential_phid) {
$view->addProperty(
pht('Credential'),
$viewer->renderHandle($credential_phid));
}
return $view;
}
private function buildStorageActions(PhabricatorRepository $repository) {
$viewer = $this->getRequest()->getUser();
$view = id(new PhabricatorActionListView())
->setObjectURI($this->getRequest()->getRequestURI())
->setUser($viewer);
$edit = id(new PhabricatorActionView())
->setIcon('fa-pencil')
->setName(pht('Edit Storage'))
->setHref(
$this->getRepositoryControllerURI($repository, 'edit/storage/'));
$view->addAction($edit);
return $view;
}
private function buildStorageProperties(
PhabricatorRepository $repository,
PhabricatorActionListView $actions) {
$viewer = $this->getRequest()->getUser();
$view = id(new PHUIPropertyListView())
->setUser($viewer)
->setActionList($actions);
$service_phid = $repository->getAlmanacServicePHID();
if ($service_phid) {
$v_service = $viewer->renderHandle($service_phid);
} else {
$v_service = phutil_tag(
'em',
array(),
pht('Local'));
}
$view->addProperty(
pht('Storage Service'),
$v_service);
$view->addProperty(
pht('Storage Path'),
$repository->getHumanReadableDetail('local-path'));
return $view;
}
private function buildHostingActions(PhabricatorRepository $repository) {
$user = $this->getRequest()->getUser();
$view = id(new PhabricatorActionListView())
->setObjectURI($this->getRequest()->getRequestURI())
->setUser($user);
$edit = id(new PhabricatorActionView())
->setIcon('fa-pencil')
->setName(pht('Edit Hosting'))
->setHref(
$this->getRepositoryControllerURI($repository, 'edit/hosting/'));
$view->addAction($edit);
if ($repository->canAllowDangerousChanges()) {
if ($repository->shouldAllowDangerousChanges()) {
$changes = id(new PhabricatorActionView())
->setIcon('fa-shield')
->setName(pht('Prevent Dangerous Changes'))
->setHref(
$this->getRepositoryControllerURI($repository, 'edit/dangerous/'))
->setWorkflow(true);
} else {
$changes = id(new PhabricatorActionView())
->setIcon('fa-bullseye')
->setName(pht('Allow Dangerous Changes'))
->setHref(
$this->getRepositoryControllerURI($repository, 'edit/dangerous/'))
->setWorkflow(true);
}
$view->addAction($changes);
}
return $view;
}
private function buildHostingProperties(
PhabricatorRepository $repository,
PhabricatorActionListView $actions) {
$user = $this->getRequest()->getUser();
$view = id(new PHUIPropertyListView())
->setUser($user)
->setActionList($actions);
$hosting = $repository->isHosted()
? pht('Hosted on Phabricator')
: pht('Hosted Elsewhere');
$view->addProperty(pht('Hosting'), phutil_tag('em', array(), $hosting));
$view->addProperty(
pht('Serve over HTTP'),
phutil_tag(
'em',
array(),
PhabricatorRepository::getProtocolAvailabilityName(
$repository->getServeOverHTTP())));
$view->addProperty(
pht('Serve over SSH'),
phutil_tag(
'em',
array(),
PhabricatorRepository::getProtocolAvailabilityName(
$repository->getServeOverSSH())));
if ($repository->canAllowDangerousChanges()) {
if ($repository->shouldAllowDangerousChanges()) {
$description = pht('Allowed');
} else {
$description = pht('Not Allowed');
}
$view->addProperty(
pht('Dangerous Changes'),
$description);
}
return $view;
}
private function buildRepositoryStatus(
PhabricatorRepository $repository) {
$viewer = $this->getRequest()->getUser();
$is_cluster = $repository->getAlmanacServicePHID();
$view = new PHUIStatusListView();
$messages = id(new PhabricatorRepositoryStatusMessage())
->loadAllWhere('repositoryID = %d', $repository->getID());
$messages = mpull($messages, null, 'getStatusType');
if ($repository->isTracked()) {
$view->addItem(
id(new PHUIStatusItemView())
->setIcon(PHUIStatusItemView::ICON_ACCEPT, 'green')
->setTarget(pht('Repository Active')));
} else {
$view->addItem(
id(new PHUIStatusItemView())
->setIcon(PHUIStatusItemView::ICON_WARNING, 'bluegrey')
->setTarget(pht('Repository Inactive'))
->setNote(
pht('Activate this repository to begin or resume import.')));
return $view;
}
$binaries = array();
$svnlook_check = false;
switch ($repository->getVersionControlSystem()) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
$binaries[] = 'git';
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
$binaries[] = 'svn';
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
$binaries[] = 'hg';
break;
}
if ($repository->isHosted()) {
if ($repository->getServeOverHTTP() != PhabricatorRepository::SERVE_OFF) {
switch ($repository->getVersionControlSystem()) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
$binaries[] = 'git-http-backend';
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
$binaries[] = 'svnserve';
$binaries[] = 'svnadmin';
$binaries[] = 'svnlook';
$svnlook_check = true;
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
$binaries[] = 'hg';
break;
}
}
if ($repository->getServeOverSSH() != PhabricatorRepository::SERVE_OFF) {
switch ($repository->getVersionControlSystem()) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
$binaries[] = 'git-receive-pack';
$binaries[] = 'git-upload-pack';
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
$binaries[] = 'svnserve';
$binaries[] = 'svnadmin';
$binaries[] = 'svnlook';
$svnlook_check = true;
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
$binaries[] = 'hg';
break;
}
}
}
$binaries = array_unique($binaries);
if (!$is_cluster) {
// We're only checking for binaries if we aren't running with a cluster
// configuration. In theory, we could check for binaries on the
// repository host machine, but we'd need to make this more complicated
// to do that.
foreach ($binaries as $binary) {
$where = Filesystem::resolveBinary($binary);
if (!$where) {
$view->addItem(
id(new PHUIStatusItemView())
->setIcon(PHUIStatusItemView::ICON_WARNING, 'red')
->setTarget(
pht('Missing Binary %s', phutil_tag('tt', array(), $binary)))
->setNote(pht(
"Unable to find this binary in the webserver's PATH. You may ".
"need to configure %s.",
$this->getEnvConfigLink())));
} else {
$view->addItem(
id(new PHUIStatusItemView())
->setIcon(PHUIStatusItemView::ICON_ACCEPT, 'green')
->setTarget(
pht('Found Binary %s', phutil_tag('tt', array(), $binary)))
->setNote(phutil_tag('tt', array(), $where)));
}
}
// This gets checked generically above. However, for svn commit hooks, we
// need this to be in environment.append-paths because subversion strips
// PATH.
if ($svnlook_check) {
$where = Filesystem::resolveBinary('svnlook');
if ($where) {
$path = substr($where, 0, strlen($where) - strlen('svnlook'));
$dirs = PhabricatorEnv::getEnvConfig('environment.append-paths');
$in_path = false;
foreach ($dirs as $dir) {
if (Filesystem::isDescendant($path, $dir)) {
$in_path = true;
break;
}
}
if (!$in_path) {
$view->addItem(
id(new PHUIStatusItemView())
->setIcon(PHUIStatusItemView::ICON_WARNING, 'red')
->setTarget(
pht('Missing Binary %s', phutil_tag('tt', array(), $binary)))
->setNote(pht(
- 'Unable to find this binary in `environment.append-paths`. '.
+ 'Unable to find this binary in `%s`. '.
'You need to configure %s and include %s.',
+ 'environment.append-paths',
$this->getEnvConfigLink(),
$path)));
}
}
}
}
$doc_href = PhabricatorEnv::getDocLink('Managing Daemons with phd');
$daemon_instructions = pht(
'Use %s to start daemons. See %s.',
phutil_tag('tt', array(), 'bin/phd start'),
phutil_tag(
'a',
array(
'href' => $doc_href,
),
pht('Managing Daemons with phd')));
$pull_daemon = id(new PhabricatorDaemonLogQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withStatus(PhabricatorDaemonLogQuery::STATUS_ALIVE)
->withDaemonClasses(array('PhabricatorRepositoryPullLocalDaemon'))
->setLimit(1)
->execute();
if ($pull_daemon) {
// TODO: In a cluster environment, we need a daemon on this repository's
// host, specifically, and we aren't checking for that right now. This
// is a reasonable proxy for things being more-or-less correctly set up,
// though.
$view->addItem(
id(new PHUIStatusItemView())
->setIcon(PHUIStatusItemView::ICON_ACCEPT, 'green')
->setTarget(pht('Pull Daemon Running')));
} else {
$view->addItem(
id(new PHUIStatusItemView())
->setIcon(PHUIStatusItemView::ICON_WARNING, 'red')
->setTarget(pht('Pull Daemon Not Running'))
->setNote($daemon_instructions));
}
$task_daemon = id(new PhabricatorDaemonLogQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withStatus(PhabricatorDaemonLogQuery::STATUS_ALIVE)
->withDaemonClasses(array('PhabricatorTaskmasterDaemon'))
->setLimit(1)
->execute();
if ($task_daemon) {
$view->addItem(
id(new PHUIStatusItemView())
->setIcon(PHUIStatusItemView::ICON_ACCEPT, 'green')
->setTarget(pht('Task Daemon Running')));
} else {
$view->addItem(
id(new PHUIStatusItemView())
->setIcon(PHUIStatusItemView::ICON_WARNING, 'red')
->setTarget(pht('Task Daemon Not Running'))
->setNote($daemon_instructions));
}
if ($is_cluster) {
// Just omit this status check for now in cluster environments. We
// could make a service call and pull it from the repository host
// eventually.
} else if ($repository->usesLocalWorkingCopy()) {
$local_parent = dirname($repository->getLocalPath());
if (Filesystem::pathExists($local_parent)) {
$view->addItem(
id(new PHUIStatusItemView())
->setIcon(PHUIStatusItemView::ICON_ACCEPT, 'green')
->setTarget(pht('Storage Directory OK'))
->setNote(phutil_tag('tt', array(), $local_parent)));
} else {
$view->addItem(
id(new PHUIStatusItemView())
->setIcon(PHUIStatusItemView::ICON_WARNING, 'red')
->setTarget(pht('No Storage Directory'))
->setNote(
pht(
'Storage directory %s does not exist, or is not readable by '.
'the webserver. Create this directory or make it readable.',
phutil_tag('tt', array(), $local_parent))));
return $view;
}
$local_path = $repository->getLocalPath();
$message = idx($messages, PhabricatorRepositoryStatusMessage::TYPE_INIT);
if ($message) {
switch ($message->getStatusCode()) {
case PhabricatorRepositoryStatusMessage::CODE_ERROR:
$view->addItem(
id(new PHUIStatusItemView())
->setIcon(PHUIStatusItemView::ICON_WARNING, 'red')
->setTarget(pht('Initialization Error'))
->setNote($message->getParameter('message')));
return $view;
case PhabricatorRepositoryStatusMessage::CODE_OKAY:
if (Filesystem::pathExists($local_path)) {
$view->addItem(
id(new PHUIStatusItemView())
->setIcon(PHUIStatusItemView::ICON_ACCEPT, 'green')
->setTarget(pht('Working Copy OK'))
->setNote(phutil_tag('tt', array(), $local_path)));
} else {
$view->addItem(
id(new PHUIStatusItemView())
->setIcon(PHUIStatusItemView::ICON_WARNING, 'red')
->setTarget(pht('Working Copy Error'))
->setNote(
pht(
'Working copy %s has been deleted, or is not '.
'readable by the webserver. Make this directory '.
'readable. If it has been deleted, the daemons should '.
'restore it automatically.',
phutil_tag('tt', array(), $local_path))));
return $view;
}
break;
case PhabricatorRepositoryStatusMessage::CODE_WORKING:
$view->addItem(
id(new PHUIStatusItemView())
->setIcon(PHUIStatusItemView::ICON_CLOCK, 'green')
->setTarget(pht('Initializing Working Copy'))
->setNote(pht('Daemons are initializing the working copy.')));
return $view;
default:
$view->addItem(
id(new PHUIStatusItemView())
->setIcon(PHUIStatusItemView::ICON_WARNING, 'red')
->setTarget(pht('Unknown Init Status'))
->setNote($message->getStatusCode()));
return $view;
}
} else {
$view->addItem(
id(new PHUIStatusItemView())
->setIcon(PHUIStatusItemView::ICON_CLOCK, 'orange')
->setTarget(pht('No Working Copy Yet'))
->setNote(
pht('Waiting for daemons to build a working copy.')));
return $view;
}
}
$message = idx($messages, PhabricatorRepositoryStatusMessage::TYPE_FETCH);
if ($message) {
switch ($message->getStatusCode()) {
case PhabricatorRepositoryStatusMessage::CODE_ERROR:
$message = $message->getParameter('message');
$suggestion = null;
if (preg_match('/Permission denied \(publickey\)./', $message)) {
$suggestion = pht(
'Public Key Error: This error usually indicates that the '.
'keypair you have configured does not have permission to '.
'access the repository.');
}
$message = phutil_escape_html_newlines($message);
if ($suggestion !== null) {
$message = array(
phutil_tag('strong', array(), $suggestion),
phutil_tag('br'),
phutil_tag('br'),
phutil_tag('em', array(), pht('Raw Error')),
phutil_tag('br'),
$message,
);
}
$view->addItem(
id(new PHUIStatusItemView())
->setIcon(PHUIStatusItemView::ICON_WARNING, 'red')
->setTarget(pht('Update Error'))
->setNote($message));
return $view;
case PhabricatorRepositoryStatusMessage::CODE_OKAY:
$ago = (PhabricatorTime::getNow() - $message->getEpoch());
$view->addItem(
id(new PHUIStatusItemView())
->setIcon(PHUIStatusItemView::ICON_ACCEPT, 'green')
->setTarget(pht('Updates OK'))
->setNote(
pht(
'Last updated %s (%s ago).',
phabricator_datetime($message->getEpoch(), $viewer),
phutil_format_relative_time_detailed($ago))));
break;
}
} else {
$view->addItem(
id(new PHUIStatusItemView())
->setIcon(PHUIStatusItemView::ICON_CLOCK, 'orange')
->setTarget(pht('Waiting For Update'))
->setNote(
pht('Waiting for daemons to read updates.')));
}
if ($repository->isImporting()) {
$progress = queryfx_all(
$repository->establishConnection('r'),
'SELECT importStatus, count(*) N FROM %T WHERE repositoryID = %d
GROUP BY importStatus',
id(new PhabricatorRepositoryCommit())->getTableName(),
$repository->getID());
$done = 0;
$total = 0;
foreach ($progress as $row) {
$total += $row['N'] * 4;
$status = $row['importStatus'];
if ($status & PhabricatorRepositoryCommit::IMPORTED_MESSAGE) {
$done += $row['N'];
}
if ($status & PhabricatorRepositoryCommit::IMPORTED_CHANGE) {
$done += $row['N'];
}
if ($status & PhabricatorRepositoryCommit::IMPORTED_OWNERS) {
$done += $row['N'];
}
if ($status & PhabricatorRepositoryCommit::IMPORTED_HERALD) {
$done += $row['N'];
}
}
if ($total) {
$percentage = 100 * ($done / $total);
} else {
$percentage = 0;
}
// Cap this at "99.99%", because it's confusing to users when the actual
// fraction is "99.996%" and it rounds up to "100.00%".
if ($percentage > 99.99) {
$percentage = 99.99;
}
$percentage = sprintf('%.2f%%', $percentage);
$view->addItem(
id(new PHUIStatusItemView())
->setIcon(PHUIStatusItemView::ICON_CLOCK, 'green')
->setTarget(pht('Importing'))
->setNote(
pht('%s Complete', $percentage)));
} else {
$view->addItem(
id(new PHUIStatusItemView())
->setIcon(PHUIStatusItemView::ICON_ACCEPT, 'green')
->setTarget(pht('Fully Imported')));
}
if (idx($messages, PhabricatorRepositoryStatusMessage::TYPE_NEEDS_UPDATE)) {
$view->addItem(
id(new PHUIStatusItemView())
->setIcon(PHUIStatusItemView::ICON_UP, 'indigo')
->setTarget(pht('Prioritized'))
->setNote(pht('This repository will be updated soon!')));
}
return $view;
}
private function buildRepositoryUpdateInterval(
PhabricatorRepository $repository) {
$smart_wait = $repository->loadUpdateInterval();
$doc_href = PhabricatorEnv::getDoclink(
'Diffusion User Guide: Repository Updates');
return array(
phutil_format_relative_time_detailed($smart_wait),
" \xC2\xB7 ",
phutil_tag(
'a',
array(
'href' => $doc_href,
'target' => '_blank',
),
pht('Learn More')),
);
}
private function buildMirrorActions(
PhabricatorRepository $repository) {
$viewer = $this->getRequest()->getUser();
$mirror_actions = id(new PhabricatorActionListView())
->setObjectURI($this->getRequest()->getRequestURI())
->setUser($viewer);
$new_mirror_uri = $this->getRepositoryControllerURI(
$repository,
'mirror/edit/');
$mirror_actions->addAction(
id(new PhabricatorActionView())
->setName(pht('Add Mirror'))
->setIcon('fa-plus')
->setHref($new_mirror_uri)
->setWorkflow(true));
return $mirror_actions;
}
private function buildMirrorProperties(
PhabricatorRepository $repository,
PhabricatorActionListView $actions) {
$viewer = $this->getRequest()->getUser();
$mirror_properties = id(new PHUIPropertyListView())
->setUser($viewer)
->setActionList($actions);
$mirror_properties->addProperty(
'',
phutil_tag(
'em',
array(),
pht('Automatically push changes into other remotes.')));
return $mirror_properties;
}
private function buildMirrorList(
PhabricatorRepository $repository,
array $mirrors) {
assert_instances_of($mirrors, 'PhabricatorRepositoryMirror');
$mirror_list = id(new PHUIObjectItemListView())
->setNoDataString(pht('This repository has no configured mirrors.'));
foreach ($mirrors as $mirror) {
$item = id(new PHUIObjectItemView())
->setHeader($mirror->getRemoteURI());
$edit_uri = $this->getRepositoryControllerURI(
$repository,
'mirror/edit/'.$mirror->getID().'/');
$delete_uri = $this->getRepositoryControllerURI(
$repository,
'mirror/delete/'.$mirror->getID().'/');
$item->addAction(
id(new PHUIListItemView())
->setIcon('fa-pencil')
->setHref($edit_uri)
->setWorkflow(true));
$item->addAction(
id(new PHUIListItemView())
->setIcon('fa-times')
->setHref($delete_uri)
->setWorkflow(true));
$mirror_list->addItem($item);
}
return $mirror_list;
}
private function buildSymbolsActions(PhabricatorRepository $repository) {
$viewer = $this->getRequest()->getUser();
$view = id(new PhabricatorActionListView())
->setObjectURI($this->getRequest()->getRequestURI())
->setUser($viewer);
$edit = id(new PhabricatorActionView())
->setIcon('fa-pencil')
->setName(pht('Edit Symbols'))
->setHref(
$this->getRepositoryControllerURI($repository, 'edit/symbol/'));
$view->addAction($edit);
return $view;
}
private function buildSymbolsProperties(
PhabricatorRepository $repository,
PhabricatorActionListView $actions) {
$viewer = $this->getRequest()->getUser();
$view = id(new PHUIPropertyListView())
->setUser($viewer)
->setActionList($actions);
$languages = $repository->getSymbolLanguages();
if ($languages) {
$languages = implode(', ', $languages);
} else {
$languages = phutil_tag('em', array(), pht('Any'));
}
$view->addProperty(pht('Languages'), $languages);
$sources = $repository->getSymbolSources();
if ($sources) {
$handles = $viewer->loadHandles($sources);
$sources = $handles->renderList();
} else {
$sources = phutil_tag('em', array(), pht('This Repository Only'));
}
$view->addProperty(pht('Use Symbols From'), $sources);
return $view;
}
private function getEnvConfigLink() {
$config_href = '/config/edit/environment.append-paths/';
return phutil_tag(
'a',
array(
'href' => $config_href,
),
'environment.append-paths');
}
}
diff --git a/src/applications/diffusion/controller/DiffusionRepositoryEditStorageController.php b/src/applications/diffusion/controller/DiffusionRepositoryEditStorageController.php
index 07da9a644..345464a00 100644
--- a/src/applications/diffusion/controller/DiffusionRepositoryEditStorageController.php
+++ b/src/applications/diffusion/controller/DiffusionRepositoryEditStorageController.php
@@ -1,83 +1,84 @@
<?php
final class DiffusionRepositoryEditStorageController
extends DiffusionRepositoryEditController {
protected function processDiffusionRequest(AphrontRequest $request) {
$user = $request->getUser();
$drequest = $this->diffusionRequest;
$repository = $drequest->getRepository();
$repository = id(new PhabricatorRepositoryQuery())
->setViewer($user)
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->withIDs(array($repository->getID()))
->executeOne();
if (!$repository) {
return new Aphront404Response();
}
$edit_uri = $this->getRepositoryControllerURI($repository, 'edit/');
$v_local = $repository->getHumanReadableDetail('local-path');
$errors = array();
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb(pht('Edit Storage'));
$title = pht('Edit %s', $repository->getName());
$service_phid = $repository->getAlmanacServicePHID();
if ($service_phid) {
$handles = $this->loadViewerHandles(array($service_phid));
$v_service = $handles[$service_phid]->renderLink();
} else {
$v_service = phutil_tag(
'em',
array(),
pht('Local'));
}
$form = id(new AphrontFormView())
->setUser($user)
->appendChild(
id(new AphrontFormMarkupControl())
->setLabel(pht('Storage Service'))
->setValue($v_service))
->appendChild(
id(new AphrontFormMarkupControl())
->setName('local')
->setLabel(pht('Storage Path'))
->setValue($v_local))
->appendRemarkupInstructions(
pht(
"You can not adjust the local path for this repository from the ".
- "web interface. To edit it, run this command:\n\n".
- " phabricator/ $ ./bin/repository edit %s --as %s --local-path ...",
- $repository->getCallsign(),
- $user->getUsername()))
+ "web interface. To edit it, run this command:\n\n %s",
+ sprintf(
+ 'phabricator/ $ ./bin/repository edit %s --as %s --local-path ...',
+ $repository->getCallsign(),
+ $user->getUsername())))
->appendChild(
id(new AphrontFormSubmitControl())
->addCancelButton($edit_uri, pht('Done')));
$object_box = id(new PHUIObjectBoxView())
->setHeaderText($title)
->setForm($form)
->setFormErrors($errors);
return $this->buildApplicationPage(
array(
$crumbs,
$object_box,
),
array(
'title' => $title,
));
}
}
diff --git a/src/applications/diffusion/controller/DiffusionRepositoryEditSubversionController.php b/src/applications/diffusion/controller/DiffusionRepositoryEditSubversionController.php
index 83cda151d..b125f26ca 100644
--- a/src/applications/diffusion/controller/DiffusionRepositoryEditSubversionController.php
+++ b/src/applications/diffusion/controller/DiffusionRepositoryEditSubversionController.php
@@ -1,121 +1,121 @@
<?php
final class DiffusionRepositoryEditSubversionController
extends DiffusionRepositoryEditController {
protected function processDiffusionRequest(AphrontRequest $request) {
$viewer = $request->getUser();
$drequest = $this->diffusionRequest;
$repository = $drequest->getRepository();
$repository = id(new PhabricatorRepositoryQuery())
->setViewer($viewer)
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->withIDs(array($repository->getID()))
->executeOne();
if (!$repository) {
return new Aphront404Response();
}
switch ($repository->getVersionControlSystem()) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
throw new Exception(
pht('Git and Mercurial do not support editing SVN properties!'));
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
break;
default:
throw new Exception(
pht('Repository has unknown version control system!'));
}
$edit_uri = $this->getRepositoryControllerURI($repository, 'edit/');
$v_subpath = $repository->getHumanReadableDetail('svn-subpath');
$v_uuid = $repository->getUUID();
if ($request->isFormPost()) {
$v_subpath = $request->getStr('subpath');
$v_uuid = $request->getStr('uuid');
$xactions = array();
$template = id(new PhabricatorRepositoryTransaction());
$type_subpath = PhabricatorRepositoryTransaction::TYPE_SVN_SUBPATH;
$type_uuid = PhabricatorRepositoryTransaction::TYPE_UUID;
$xactions[] = id(clone $template)
->setTransactionType($type_subpath)
->setNewValue($v_subpath);
$xactions[] = id(clone $template)
->setTransactionType($type_uuid)
->setNewValue($v_uuid);
id(new PhabricatorRepositoryEditor())
->setContinueOnNoEffect(true)
->setContentSourceFromRequest($request)
->setActor($viewer)
->applyTransactions($repository, $xactions);
return id(new AphrontRedirectResponse())->setURI($edit_uri);
}
$content = array();
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb(pht('Edit Subversion Info'));
$title = pht('Edit Subversion Info (%s)', $repository->getName());
$policies = id(new PhabricatorPolicyQuery())
->setViewer($viewer)
->setObject($repository)
->execute();
$form = id(new AphrontFormView())
->setUser($viewer)
->appendRemarkupInstructions(
pht(
"You can set the **Repository UUID**, which will help Phabriactor ".
"provide better context in some cases. You can find the UUID of a ".
- "repository by running `svn info`.".
- "\n\n".
+ "repository by running `%s`.\n\n".
"If you want to import only part of a repository, like `trunk/`, ".
"you can set a path in **Import Only**. Phabricator will ignore ".
- "commits which do not affect this path."))
+ "commits which do not affect this path.",
+ 'svn info'))
->appendChild(
id(new AphrontFormTextControl())
->setName('uuid')
->setLabel(pht('Repository UUID'))
->setValue($v_uuid))
->appendChild(
id(new AphrontFormTextControl())
->setName('subpath')
->setLabel(pht('Import Only'))
->setValue($v_subpath))
->appendChild(
id(new AphrontFormSubmitControl())
->setValue(pht('Save Subversion Info'))
->addCancelButton($edit_uri));
$form_box = id(new PHUIObjectBoxView())
->setHeaderText($title)
->setForm($form);
return $this->buildApplicationPage(
array(
$crumbs,
$form_box,
),
array(
'title' => $title,
));
}
}
diff --git a/src/applications/diffusion/controller/DiffusionRepositoryNewController.php b/src/applications/diffusion/controller/DiffusionRepositoryNewController.php
index efab74543..f7fac7b18 100644
--- a/src/applications/diffusion/controller/DiffusionRepositoryNewController.php
+++ b/src/applications/diffusion/controller/DiffusionRepositoryNewController.php
@@ -1,83 +1,83 @@
<?php
final class DiffusionRepositoryNewController extends DiffusionController {
protected function processDiffusionRequest(AphrontRequest $request) {
$viewer = $request->getUser();
$this->requireApplicationCapability(
DiffusionCreateRepositoriesCapability::CAPABILITY);
if ($request->isFormPost()) {
if ($request->getStr('type')) {
switch ($request->getStr('type')) {
case 'create':
$uri = $this->getApplicationURI('create/');
break;
case 'import':
default:
$uri = $this->getApplicationURI('import/');
break;
}
return id(new AphrontRedirectResponse())->setURI($uri);
}
}
$doc_href = PhabricatorEnv::getDoclink(
'Diffusion User Guide: Repository Hosting');
$doc_link = phutil_tag(
'a',
array(
'href' => $doc_href,
'target' => '_blank',
),
pht('Diffusion User Guide: Repository Hosting'));
$form = id(new AphrontFormView())
->setUser($viewer)
->appendChild(
id(new AphrontFormRadioButtonControl())
->setName('type')
->addButton(
'create',
pht('Create a New Hosted Repository'),
array(
pht(
'Create a new, empty repository which Phabricator will host. '.
'For instructions on configuring repository hosting, see %s.',
$doc_link),
))
->addButton(
'import',
pht('Import an Existing External Repository'),
pht(
- 'Import a repository hosted somewhere else, like GitHub, '.
- 'Bitbucket, or your organization\'s existing servers. '.
- 'Phabricator will read changes from the repository but will '.
- 'not host or manage it. The authoritative master version of '.
- 'the repository will stay where it is now.')))
+ "Import a repository hosted somewhere else, like GitHub, ".
+ "Bitbucket, or your organization's existing servers. ".
+ "Phabricator will read changes from the repository but will ".
+ "not host or manage it. The authoritative master version of ".
+ "the repository will stay where it is now.")))
->appendChild(
id(new AphrontFormSubmitControl())
->setValue(pht('Continue'))
->addCancelButton($this->getApplicationURI()));
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb(pht('New Repository'));
$form_box = id(new PHUIObjectBoxView())
->setHeaderText(pht('Create or Import Repository'))
->setForm($form);
return $this->buildApplicationPage(
array(
$crumbs,
$form_box,
),
array(
'title' => pht('New Repository'),
));
}
}
diff --git a/src/applications/diffusion/controller/DiffusionServeController.php b/src/applications/diffusion/controller/DiffusionServeController.php
index 2b82bde43..a76acd704 100644
--- a/src/applications/diffusion/controller/DiffusionServeController.php
+++ b/src/applications/diffusion/controller/DiffusionServeController.php
@@ -1,640 +1,648 @@
<?php
final class DiffusionServeController extends DiffusionController {
protected function shouldLoadDiffusionRequest() {
return false;
}
public static function isVCSRequest(AphrontRequest $request) {
if (!self::getCallsign($request)) {
return null;
}
$content_type = $request->getHTTPHeader('Content-Type');
$user_agent = idx($_SERVER, 'HTTP_USER_AGENT');
$vcs = null;
if ($request->getExists('service')) {
$service = $request->getStr('service');
// We get this initially for `info/refs`.
// Git also gives us a User-Agent like "git/1.8.2.3".
$vcs = PhabricatorRepositoryType::REPOSITORY_TYPE_GIT;
} else if (strncmp($user_agent, 'git/', 4) === 0) {
$vcs = PhabricatorRepositoryType::REPOSITORY_TYPE_GIT;
} else if ($content_type == 'application/x-git-upload-pack-request') {
// We get this for `git-upload-pack`.
$vcs = PhabricatorRepositoryType::REPOSITORY_TYPE_GIT;
} else if ($content_type == 'application/x-git-receive-pack-request') {
// We get this for `git-receive-pack`.
$vcs = PhabricatorRepositoryType::REPOSITORY_TYPE_GIT;
} else if ($request->getExists('cmd')) {
// Mercurial also sends an Accept header like
// "application/mercurial-0.1", and a User-Agent like
// "mercurial/proto-1.0".
$vcs = PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL;
} else {
// Subversion also sends an initial OPTIONS request (vs GET/POST), and
// has a User-Agent like "SVN/1.8.3 (x86_64-apple-darwin11.4.2)
// serf/1.3.2".
$dav = $request->getHTTPHeader('DAV');
$dav = new PhutilURI($dav);
if ($dav->getDomain() === 'subversion.tigris.org') {
$vcs = PhabricatorRepositoryType::REPOSITORY_TYPE_SVN;
}
}
return $vcs;
}
private static function getCallsign(AphrontRequest $request) {
$uri = $request->getRequestURI();
$regex = '@^/diffusion/(?P<callsign>[A-Z]+)(/|$)@';
$matches = null;
if (!preg_match($regex, (string)$uri, $matches)) {
return null;
}
return $matches['callsign'];
}
protected function processDiffusionRequest(AphrontRequest $request) {
$callsign = self::getCallsign($request);
// If authentication credentials have been provided, try to find a user
// that actually matches those credentials.
if (isset($_SERVER['PHP_AUTH_USER']) && isset($_SERVER['PHP_AUTH_PW'])) {
$username = $_SERVER['PHP_AUTH_USER'];
$password = new PhutilOpaqueEnvelope($_SERVER['PHP_AUTH_PW']);
$viewer = $this->authenticateHTTPRepositoryUser($username, $password);
if (!$viewer) {
return new PhabricatorVCSResponse(
403,
pht('Invalid credentials.'));
}
} else {
// User hasn't provided credentials, which means we count them as
// being "not logged in".
$viewer = new PhabricatorUser();
}
$allow_public = PhabricatorEnv::getEnvConfig('policy.allow-public');
$allow_auth = PhabricatorEnv::getEnvConfig('diffusion.allow-http-auth');
if (!$allow_public) {
if (!$viewer->isLoggedIn()) {
if ($allow_auth) {
return new PhabricatorVCSResponse(
401,
pht('You must log in to access repositories.'));
} else {
return new PhabricatorVCSResponse(
403,
pht('Public and authenticated HTTP access are both forbidden.'));
}
}
}
try {
$repository = id(new PhabricatorRepositoryQuery())
->setViewer($viewer)
->withCallsigns(array($callsign))
->executeOne();
if (!$repository) {
return new PhabricatorVCSResponse(
404,
pht('No such repository exists.'));
}
} catch (PhabricatorPolicyException $ex) {
if ($viewer->isLoggedIn()) {
return new PhabricatorVCSResponse(
403,
pht('You do not have permission to access this repository.'));
} else {
if ($allow_auth) {
return new PhabricatorVCSResponse(
401,
pht('You must log in to access this repository.'));
} else {
return new PhabricatorVCSResponse(
403,
pht(
'This repository requires authentication, which is forbidden '.
'over HTTP.'));
}
}
}
if (!$repository->isTracked()) {
return new PhabricatorVCSResponse(
403,
pht('This repository is inactive.'));
}
$is_push = !$this->isReadOnlyRequest($repository);
switch ($repository->getServeOverHTTP()) {
case PhabricatorRepository::SERVE_READONLY:
if ($is_push) {
return new PhabricatorVCSResponse(
403,
pht('This repository is read-only over HTTP.'));
}
break;
case PhabricatorRepository::SERVE_READWRITE:
if ($is_push) {
$can_push = PhabricatorPolicyFilter::hasCapability(
$viewer,
$repository,
DiffusionPushCapability::CAPABILITY);
if (!$can_push) {
if ($viewer->isLoggedIn()) {
return new PhabricatorVCSResponse(
403,
pht('You do not have permission to push to this repository.'));
} else {
if ($allow_auth) {
return new PhabricatorVCSResponse(
401,
pht('You must log in to push to this repository.'));
} else {
return new PhabricatorVCSResponse(
403,
pht(
'Pushing to this repository requires authentication, '.
'which is forbidden over HTTP.'));
}
}
}
}
break;
case PhabricatorRepository::SERVE_OFF:
default:
return new PhabricatorVCSResponse(
403,
pht('This repository is not available over HTTP.'));
}
$vcs_type = $repository->getVersionControlSystem();
$req_type = $this->isVCSRequest($request);
if ($vcs_type != $req_type) {
switch ($req_type) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
$result = new PhabricatorVCSResponse(
500,
pht('This is not a Git repository.'));
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
$result = new PhabricatorVCSResponse(
500,
pht('This is not a Mercurial repository.'));
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
$result = new PhabricatorVCSResponse(
500,
pht('This is not a Subversion repository.'));
break;
default:
$result = new PhabricatorVCSResponse(
500,
pht('Unknown request type.'));
break;
}
} else {
switch ($vcs_type) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
$result = $this->serveVCSRequest($repository, $viewer);
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
$result = new PhabricatorVCSResponse(
500,
pht(
'Phabricator does not support HTTP access to Subversion '.
'repositories.'));
break;
default:
$result = new PhabricatorVCSResponse(
500,
pht('Unknown version control system.'));
break;
}
}
$code = $result->getHTTPResponseCode();
if ($is_push && ($code == 200)) {
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$repository->writeStatusMessage(
PhabricatorRepositoryStatusMessage::TYPE_NEEDS_UPDATE,
PhabricatorRepositoryStatusMessage::CODE_OKAY);
unset($unguarded);
}
return $result;
}
private function serveVCSRequest(
PhabricatorRepository $repository,
PhabricatorUser $viewer) {
// If this repository is hosted on a service, we need to proxy the request
// to a host which can serve it.
$is_cluster_request = $this->getRequest()->isProxiedClusterRequest();
$uri = $repository->getAlmanacServiceURI(
$viewer,
$is_cluster_request,
array(
'http',
'https',
));
if ($uri) {
$future = $this->getRequest()->newClusterProxyFuture($uri);
return id(new AphrontHTTPProxyResponse())
->setHTTPFuture($future);
}
// Otherwise, we're going to handle the request locally.
$vcs_type = $repository->getVersionControlSystem();
switch ($vcs_type) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
$result = $this->serveGitRequest($repository, $viewer);
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
$result = $this->serveMercurialRequest($repository, $viewer);
break;
}
return $result;
}
private function isReadOnlyRequest(
PhabricatorRepository $repository) {
$request = $this->getRequest();
$method = $_SERVER['REQUEST_METHOD'];
// TODO: This implementation is safe by default, but very incomplete.
switch ($repository->getVersionControlSystem()) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
$service = $request->getStr('service');
$path = $this->getRequestDirectoryPath($repository);
// NOTE: Service names are the reverse of what you might expect, as they
// are from the point of view of the server. The main read service is
// "git-upload-pack", and the main write service is "git-receive-pack".
if ($method == 'GET' &&
$path == '/info/refs' &&
$service == 'git-upload-pack') {
return true;
}
if ($path == '/git-upload-pack') {
return true;
}
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
$cmd = $request->getStr('cmd');
if ($cmd == 'batch') {
$cmds = idx($this->getMercurialArguments(), 'cmds');
return DiffusionMercurialWireProtocol::isReadOnlyBatchCommand($cmds);
}
return DiffusionMercurialWireProtocol::isReadOnlyCommand($cmd);
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
break;
}
return false;
}
/**
* @phutil-external-symbol class PhabricatorStartup
*/
private function serveGitRequest(
PhabricatorRepository $repository,
PhabricatorUser $viewer) {
$request = $this->getRequest();
$request_path = $this->getRequestDirectoryPath($repository);
$repository_root = $repository->getLocalPath();
// Rebuild the query string to strip `__magic__` parameters and prevent
// issues where we might interpret inputs like "service=read&service=write"
// differently than the server does and pass it an unsafe command.
// NOTE: This does not use getPassthroughRequestParameters() because
// that code is HTTP-method agnostic and will encode POST data.
$query_data = $_GET;
foreach ($query_data as $key => $value) {
if (!strncmp($key, '__', 2)) {
unset($query_data[$key]);
}
}
$query_string = http_build_query($query_data, '', '&');
// We're about to wipe out PATH with the rest of the environment, so
// resolve the binary first.
$bin = Filesystem::resolveBinary('git-http-backend');
if (!$bin) {
- throw new Exception('Unable to find `git-http-backend` in PATH!');
+ throw new Exception(
+ pht(
+ 'Unable to find `%s` in %s!',
+ 'git-http-backend',
+ '$PATH'));
}
$env = array(
'REQUEST_METHOD' => $_SERVER['REQUEST_METHOD'],
'QUERY_STRING' => $query_string,
'CONTENT_TYPE' => $request->getHTTPHeader('Content-Type'),
'HTTP_CONTENT_ENCODING' => $request->getHTTPHeader('Content-Encoding'),
'REMOTE_ADDR' => $_SERVER['REMOTE_ADDR'],
'GIT_PROJECT_ROOT' => $repository_root,
'GIT_HTTP_EXPORT_ALL' => '1',
'PATH_INFO' => $request_path,
'REMOTE_USER' => $viewer->getUsername(),
// TODO: Set these correctly.
// GIT_COMMITTER_NAME
// GIT_COMMITTER_EMAIL
) + $this->getCommonEnvironment($viewer);
$input = PhabricatorStartup::getRawInput();
$command = csprintf('%s', $bin);
$command = PhabricatorDaemon::sudoCommandAsDaemonUser($command);
list($err, $stdout, $stderr) = id(new ExecFuture('%C', $command))
->setEnv($env, true)
->write($input)
->resolve();
if ($err) {
if ($this->isValidGitShallowCloneResponse($stdout, $stderr)) {
// Ignore the error if the response passes this special check for
// validity.
$err = 0;
}
}
if ($err) {
return new PhabricatorVCSResponse(
500,
pht('Error %d: %s', $err, $stderr));
}
return id(new DiffusionGitResponse())->setGitData($stdout);
}
private function getRequestDirectoryPath(PhabricatorRepository $repository) {
$request = $this->getRequest();
$request_path = $request->getRequestURI()->getPath();
$base_path = preg_replace('@^/diffusion/[A-Z]+@', '', $request_path);
// For Git repositories, strip an optional directory component if it
// isn't the name of a known Git resource. This allows users to clone
// repositories as "/diffusion/X/anything.git", for example.
if ($repository->isGit()) {
$known = array(
'info',
'git-upload-pack',
'git-receive-pack',
);
foreach ($known as $key => $path) {
$known[$key] = preg_quote($path, '@');
}
$known = implode('|', $known);
if (preg_match('@^/([^/]+)/('.$known.')(/|$)@', $base_path)) {
$base_path = preg_replace('@^/([^/]+)@', '', $base_path);
}
}
return $base_path;
}
private function authenticateHTTPRepositoryUser(
$username,
PhutilOpaqueEnvelope $password) {
if (!PhabricatorEnv::getEnvConfig('diffusion.allow-http-auth')) {
// No HTTP auth permitted.
return null;
}
if (!strlen($username)) {
// No username.
return null;
}
if (!strlen($password->openEnvelope())) {
// No password.
return null;
}
$user = id(new PhabricatorPeopleQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withUsernames(array($username))
->executeOne();
if (!$user) {
// Username doesn't match anything.
return null;
}
if (!$user->isUserActivated()) {
// User is not activated.
return null;
}
$password_entry = id(new PhabricatorRepositoryVCSPassword())
->loadOneWhere('userPHID = %s', $user->getPHID());
if (!$password_entry) {
// User doesn't have a password set.
return null;
}
if (!$password_entry->comparePassword($password, $user)) {
// Password doesn't match.
return null;
}
// If the user's password is stored using a less-than-optimal hash, upgrade
// them to the strongest available hash.
$hash_envelope = new PhutilOpaqueEnvelope(
$password_entry->getPasswordHash());
if (PhabricatorPasswordHasher::canUpgradeHash($hash_envelope)) {
$password_entry->setPassword($password, $user);
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$password_entry->save();
unset($unguarded);
}
return $user;
}
private function serveMercurialRequest(
PhabricatorRepository $repository,
PhabricatorUser $viewer) {
$request = $this->getRequest();
$bin = Filesystem::resolveBinary('hg');
if (!$bin) {
- throw new Exception('Unable to find `hg` in PATH!');
+ throw new Exception(
+ pht(
+ 'Unable to find `%s` in %s!',
+ 'hg',
+ '$PATH'));
}
$env = $this->getCommonEnvironment($viewer);
$input = PhabricatorStartup::getRawInput();
$cmd = $request->getStr('cmd');
$args = $this->getMercurialArguments();
$args = $this->formatMercurialArguments($cmd, $args);
if (strlen($input)) {
$input = strlen($input)."\n".$input."0\n";
}
$command = csprintf('%s serve --stdio', $bin);
$command = PhabricatorDaemon::sudoCommandAsDaemonUser($command);
list($err, $stdout, $stderr) = id(new ExecFuture('%C', $command))
->setEnv($env, true)
->setCWD($repository->getLocalPath())
->write("{$cmd}\n{$args}{$input}")
->resolve();
if ($err) {
return new PhabricatorVCSResponse(
500,
pht('Error %d: %s', $err, $stderr));
}
if ($cmd == 'getbundle' ||
$cmd == 'changegroup' ||
$cmd == 'changegroupsubset') {
// We're not completely sure that "changegroup" and "changegroupsubset"
// actually work, they're for very old Mercurial.
$body = gzcompress($stdout);
} else if ($cmd == 'unbundle') {
// This includes diagnostic information and anything echoed by commit
// hooks. We ignore `stdout` since it just has protocol garbage, and
// substitute `stderr`.
$body = strlen($stderr)."\n".$stderr;
} else {
list($length, $body) = explode("\n", $stdout, 2);
}
return id(new DiffusionMercurialResponse())->setContent($body);
}
private function getMercurialArguments() {
// Mercurial sends arguments in HTTP headers. "Why?", you might wonder,
// "Why would you do this?".
$args_raw = array();
for ($ii = 1;; $ii++) {
$header = 'HTTP_X_HGARG_'.$ii;
if (!array_key_exists($header, $_SERVER)) {
break;
}
$args_raw[] = $_SERVER[$header];
}
$args_raw = implode('', $args_raw);
return id(new PhutilQueryStringParser())
->parseQueryString($args_raw);
}
private function formatMercurialArguments($command, array $arguments) {
$spec = DiffusionMercurialWireProtocol::getCommandArgs($command);
$out = array();
// Mercurial takes normal arguments like this:
//
// name <length(value)>
// value
$has_star = false;
foreach ($spec as $arg_key) {
if ($arg_key == '*') {
$has_star = true;
continue;
}
if (isset($arguments[$arg_key])) {
$value = $arguments[$arg_key];
$size = strlen($value);
$out[] = "{$arg_key} {$size}\n{$value}";
unset($arguments[$arg_key]);
}
}
if ($has_star) {
// Mercurial takes arguments for variable argument lists roughly like
// this:
//
// * <count(args)>
// argname1 <length(argvalue1)>
// argvalue1
// argname2 <length(argvalue2)>
// argvalue2
$count = count($arguments);
$out[] = "* {$count}\n";
foreach ($arguments as $key => $value) {
if (in_array($key, $spec)) {
// We already added this argument above, so skip it.
continue;
}
$size = strlen($value);
$out[] = "{$key} {$size}\n{$value}";
}
}
return implode('', $out);
}
private function isValidGitShallowCloneResponse($stdout, $stderr) {
// If you execute `git clone --depth N ...`, git sends a request which
// `git-http-backend` responds to by emitting valid output and then exiting
// with a failure code and an error message. If we ignore this error,
// everything works.
// This is a pretty funky fix: it would be nice to more precisely detect
// that a request is a `--depth N` clone request, but we don't have any code
// to decode protocol frames yet. Instead, look for reasonable evidence
// in the error and output that we're looking at a `--depth` clone.
// For evidence this isn't completely crazy, see:
// https://github.com/schacon/grack/pull/7
$stdout_regexp = '(^Content-Type: application/x-git-upload-pack-result)m';
$stderr_regexp = '(The remote end hung up unexpectedly)';
$has_pack = preg_match($stdout_regexp, $stdout);
$is_hangup = preg_match($stderr_regexp, $stderr);
return $has_pack && $is_hangup;
}
private function getCommonEnvironment(PhabricatorUser $viewer) {
$remote_addr = $this->getRequest()->getRemoteAddr();
return array(
DiffusionCommitHookEngine::ENV_USER => $viewer->getUsername(),
DiffusionCommitHookEngine::ENV_REMOTE_ADDRESS => $remote_addr,
DiffusionCommitHookEngine::ENV_REMOTE_PROTOCOL => 'http',
);
}
}
diff --git a/src/applications/diffusion/data/DiffusionGitBranch.php b/src/applications/diffusion/data/DiffusionGitBranch.php
index 925ede568..8f69645b7 100644
--- a/src/applications/diffusion/data/DiffusionGitBranch.php
+++ b/src/applications/diffusion/data/DiffusionGitBranch.php
@@ -1,102 +1,110 @@
<?php
final class DiffusionGitBranch {
const DEFAULT_GIT_REMOTE = 'origin';
/**
* Parse the output of 'git branch -r --verbose --no-abbrev' or similar into
* a map. For instance:
*
* array(
* 'origin/master' => '99a9c082f9a1b68c7264e26b9e552484a5ae5f25',
* );
*
* If you specify $only_this_remote, branches will be filtered to only those
* on the given remote, **and the remote name will be stripped**. For example:
*
* array(
* 'master' => '99a9c082f9a1b68c7264e26b9e552484a5ae5f25',
* );
*
* @param string stdout of git branch command.
* @param string Filter branches to those on a specific remote.
* @return map Map of 'branch' or 'remote/branch' to hash at HEAD.
*/
public static function parseRemoteBranchOutput(
$stdout,
$only_this_remote = null) {
$map = array();
$lines = array_filter(explode("\n", $stdout));
foreach ($lines as $line) {
$matches = null;
if (preg_match('/^ (\S+)\s+-> (\S+)$/', $line, $matches)) {
// This is a line like:
//
// origin/HEAD -> origin/master
//
// ...which we don't currently do anything interesting with, although
// in theory we could use it to automatically choose the default
// branch.
continue;
}
if (!preg_match('/^ *(\S+)\s+([a-z0-9]{40})/', $line, $matches)) {
- throw new Exception("Failed to parse {$line}!");
+ throw new Exception(
+ pht(
+ 'Failed to parse %s!',
+ $line));
}
$remote_branch = $matches[1];
$branch_head = $matches[2];
if (strpos($remote_branch, 'HEAD') !== false) {
// let's assume that no one will call their remote or branch HEAD
continue;
}
if ($only_this_remote) {
$matches = null;
if (!preg_match('#^([^/]+)/(.*)$#', $remote_branch, $matches)) {
throw new Exception(
- "Failed to parse remote branch '{$remote_branch}'!");
+ pht(
+ "Failed to parse remote branch '%s'!",
+ $remote_branch));
}
$remote_name = $matches[1];
$branch_name = $matches[2];
if ($remote_name != $only_this_remote) {
continue;
}
$map[$branch_name] = $branch_head;
} else {
$map[$remote_branch] = $branch_head;
}
}
return $map;
}
/**
* As above, but with no `-r`. Used for bare repositories.
*/
public static function parseLocalBranchOutput($stdout) {
$map = array();
$lines = array_filter(explode("\n", $stdout));
$regex = '/^[* ]*(\(no branch\)|\S+)\s+([a-z0-9]{40})/';
foreach ($lines as $line) {
$matches = null;
if (!preg_match($regex, $line, $matches)) {
- throw new Exception("Failed to parse {$line}!");
+ throw new Exception(
+ pht(
+ 'Failed to parse %s!',
+ $line));
}
$branch = $matches[1];
$branch_head = $matches[2];
if ($branch == '(no branch)') {
continue;
}
$map[$branch] = $branch_head;
}
return $map;
}
}
diff --git a/src/applications/diffusion/data/DiffusionRepositoryTag.php b/src/applications/diffusion/data/DiffusionRepositoryTag.php
index 12e71185b..555e08270 100644
--- a/src/applications/diffusion/data/DiffusionRepositoryTag.php
+++ b/src/applications/diffusion/data/DiffusionRepositoryTag.php
@@ -1,117 +1,117 @@
<?php
final class DiffusionRepositoryTag {
private $author;
private $epoch;
private $commitIdentifier;
private $name;
private $description;
private $type;
private $message = false;
public function setType($type) {
$this->type = $type;
return $this;
}
public function getType() {
return $this->type;
}
public function setDescription($description) {
$this->description = $description;
return $this;
}
public function getDescription() {
return $this->description;
}
public function setName($name) {
$this->name = $name;
return $this;
}
public function getName() {
return $this->name;
}
public function setCommitIdentifier($commit_identifier) {
$this->commitIdentifier = $commit_identifier;
return $this;
}
public function getCommitIdentifier() {
return $this->commitIdentifier;
}
public function setEpoch($epoch) {
$this->epoch = $epoch;
return $this;
}
public function getEpoch() {
return $this->epoch;
}
public function setAuthor($author) {
$this->author = $author;
return $this;
}
public function getAuthor() {
return $this->author;
}
public function attachMessage($message) {
$this->message = $message;
return $this;
}
public function getMessage() {
if ($this->message === false) {
- throw new Exception('Message is not attached!');
+ throw new Exception(pht('Message is not attached!'));
}
return $this->message;
}
public function toDictionary() {
$dict = array(
'author' => $this->getAuthor(),
'epoch' => $this->getEpoch(),
'commitIdentifier' => $this->getCommitIdentifier(),
'name' => $this->getName(),
'description' => $this->getDescription(),
'type' => $this->getType(),
);
if ($this->message !== false) {
$dict['message'] = $this->message;
}
return $dict;
}
public static function newFromConduit(array $dicts) {
$tags = array();
foreach ($dicts as $dict) {
$tag = id(new DiffusionRepositoryTag())
->setAuthor($dict['author'])
->setEpoch($dict['epoch'])
->setCommitIdentifier($dict['commitIdentifier'])
->setName($dict['name'])
->setDescription($dict['description'])
->setType($dict['type']);
if (array_key_exists('message', $dict)) {
$tag->attachMessage($dict['message']);
}
$tags[] = $tag;
}
return $tags;
}
}
diff --git a/src/applications/diffusion/engine/DiffusionCommitHookEngine.php b/src/applications/diffusion/engine/DiffusionCommitHookEngine.php
index 13c7127e0..f84eb13bd 100644
--- a/src/applications/diffusion/engine/DiffusionCommitHookEngine.php
+++ b/src/applications/diffusion/engine/DiffusionCommitHookEngine.php
@@ -1,1258 +1,1261 @@
<?php
/**
* @task config Configuring the Hook Engine
* @task hook Hook Execution
* @task git Git Hooks
* @task hg Mercurial Hooks
* @task svn Subversion Hooks
* @task internal Internals
*/
final class DiffusionCommitHookEngine extends Phobject {
const ENV_USER = 'PHABRICATOR_USER';
const ENV_REMOTE_ADDRESS = 'PHABRICATOR_REMOTE_ADDRESS';
const ENV_REMOTE_PROTOCOL = 'PHABRICATOR_REMOTE_PROTOCOL';
const EMPTY_HASH = '0000000000000000000000000000000000000000';
private $viewer;
private $repository;
private $stdin;
private $originalArgv;
private $subversionTransaction;
private $subversionRepository;
private $remoteAddress;
private $remoteProtocol;
private $transactionKey;
private $mercurialHook;
private $mercurialCommits = array();
private $gitCommits = array();
private $heraldViewerProjects;
private $rejectCode = PhabricatorRepositoryPushLog::REJECT_BROKEN;
private $rejectDetails;
private $emailPHIDs = array();
/* -( Config )------------------------------------------------------------- */
public function setRemoteProtocol($remote_protocol) {
$this->remoteProtocol = $remote_protocol;
return $this;
}
public function getRemoteProtocol() {
return $this->remoteProtocol;
}
public function setRemoteAddress($remote_address) {
$this->remoteAddress = $remote_address;
return $this;
}
public function getRemoteAddress() {
return $this->remoteAddress;
}
private function getRemoteAddressForLog() {
// If whatever we have here isn't a valid IPv4 address, just store `null`.
// Older versions of PHP return `-1` on failure instead of `false`.
$remote_address = $this->getRemoteAddress();
$remote_address = max(0, ip2long($remote_address));
$remote_address = nonempty($remote_address, null);
return $remote_address;
}
public function setSubversionTransactionInfo($transaction, $repository) {
$this->subversionTransaction = $transaction;
$this->subversionRepository = $repository;
return $this;
}
public function setStdin($stdin) {
$this->stdin = $stdin;
return $this;
}
public function getStdin() {
return $this->stdin;
}
public function setOriginalArgv(array $original_argv) {
$this->originalArgv = $original_argv;
return $this;
}
public function getOriginalArgv() {
return $this->originalArgv;
}
public function setRepository(PhabricatorRepository $repository) {
$this->repository = $repository;
return $this;
}
public function getRepository() {
return $this->repository;
}
public function setViewer(PhabricatorUser $viewer) {
$this->viewer = $viewer;
return $this;
}
public function getViewer() {
return $this->viewer;
}
public function setMercurialHook($mercurial_hook) {
$this->mercurialHook = $mercurial_hook;
return $this;
}
public function getMercurialHook() {
return $this->mercurialHook;
}
/* -( Hook Execution )----------------------------------------------------- */
public function execute() {
$ref_updates = $this->findRefUpdates();
$all_updates = $ref_updates;
$caught = null;
try {
try {
$this->rejectDangerousChanges($ref_updates);
} catch (DiffusionCommitHookRejectException $ex) {
// If we're rejecting dangerous changes, flag everything that we've
// seen as rejected so it's clear that none of it was accepted.
$this->rejectCode = PhabricatorRepositoryPushLog::REJECT_DANGEROUS;
throw $ex;
}
$this->applyHeraldRefRules($ref_updates, $all_updates);
$content_updates = $this->findContentUpdates($ref_updates);
$all_updates = array_merge($all_updates, $content_updates);
$this->applyHeraldContentRules($content_updates, $all_updates);
// Run custom scripts in `hook.d/` directories.
$this->applyCustomHooks($all_updates);
// If we make it this far, we're accepting these changes. Mark all the
// logs as accepted.
$this->rejectCode = PhabricatorRepositoryPushLog::REJECT_ACCEPT;
} catch (Exception $ex) {
// We'll throw this again in a minute, but we want to save all the logs
// first.
$caught = $ex;
}
// Save all the logs no matter what the outcome was.
$event = $this->newPushEvent();
$event->setRejectCode($this->rejectCode);
$event->setRejectDetails($this->rejectDetails);
$event->openTransaction();
$event->save();
foreach ($all_updates as $update) {
$update->setPushEventPHID($event->getPHID());
$update->save();
}
$event->saveTransaction();
if ($caught) {
throw $caught;
}
// If this went through cleanly, detect pushes which are actually imports
// of an existing repository rather than an addition of new commits. If
// this push is importing a bunch of stuff, set the importing flag on
// the repository. It will be cleared once we fully process everything.
if ($this->isInitialImport($all_updates)) {
$repository = $this->getRepository();
$repository->openTransaction();
$repository->beginReadLocking();
$repository = $repository->reload();
$repository->setDetail('importing', true);
$repository->save();
$repository->endReadLocking();
$repository->saveTransaction();
}
if ($this->emailPHIDs) {
// If Herald rules triggered email to users, queue a worker to send the
// mail. We do this out-of-process so that we block pushes as briefly
// as possible.
// (We do need to pull some commit info here because the commit objects
// may not exist yet when this worker runs, which could be immediately.)
PhabricatorWorker::scheduleTask(
'PhabricatorRepositoryPushMailWorker',
array(
'eventPHID' => $event->getPHID(),
'emailPHIDs' => array_values($this->emailPHIDs),
'info' => $this->loadCommitInfoForWorker($all_updates),
),
array(
'priority' => PhabricatorWorker::PRIORITY_ALERTS,
));
}
return 0;
}
private function findRefUpdates() {
$type = $this->getRepository()->getVersionControlSystem();
switch ($type) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
return $this->findGitRefUpdates();
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
return $this->findMercurialRefUpdates();
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
return $this->findSubversionRefUpdates();
default:
throw new Exception(pht('Unsupported repository type "%s"!', $type));
}
}
private function rejectDangerousChanges(array $ref_updates) {
assert_instances_of($ref_updates, 'PhabricatorRepositoryPushLog');
$repository = $this->getRepository();
if ($repository->shouldAllowDangerousChanges()) {
return;
}
$flag_dangerous = PhabricatorRepositoryPushLog::CHANGEFLAG_DANGEROUS;
foreach ($ref_updates as $ref_update) {
if (!$ref_update->hasChangeFlags($flag_dangerous)) {
// This is not a dangerous change.
continue;
}
// We either have a branch deletion or a non fast-forward branch update.
// Format a message and reject the push.
$message = pht(
"DANGEROUS CHANGE: %s\n".
"Dangerous change protection is enabled for this repository.\n".
"Edit the repository configuration before making dangerous changes.",
$ref_update->getDangerousChangeDescription());
throw new DiffusionCommitHookRejectException($message);
}
}
private function findContentUpdates(array $ref_updates) {
assert_instances_of($ref_updates, 'PhabricatorRepositoryPushLog');
$type = $this->getRepository()->getVersionControlSystem();
switch ($type) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
return $this->findGitContentUpdates($ref_updates);
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
return $this->findMercurialContentUpdates($ref_updates);
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
return $this->findSubversionContentUpdates($ref_updates);
default:
throw new Exception(pht('Unsupported repository type "%s"!', $type));
}
}
/* -( Herald )------------------------------------------------------------- */
private function applyHeraldRefRules(
array $ref_updates,
array $all_updates) {
$this->applyHeraldRules(
$ref_updates,
new HeraldPreCommitRefAdapter(),
$all_updates);
}
private function applyHeraldContentRules(
array $content_updates,
array $all_updates) {
$this->applyHeraldRules(
$content_updates,
new HeraldPreCommitContentAdapter(),
$all_updates);
}
private function applyHeraldRules(
array $updates,
HeraldAdapter $adapter_template,
array $all_updates) {
if (!$updates) {
return;
}
$adapter_template->setHookEngine($this);
$engine = new HeraldEngine();
$rules = null;
$blocking_effect = null;
$blocked_update = null;
foreach ($updates as $update) {
$adapter = id(clone $adapter_template)
->setPushLog($update);
if ($rules === null) {
$rules = $engine->loadRulesForAdapter($adapter);
}
$effects = $engine->applyRules($rules, $adapter);
$engine->applyEffects($effects, $adapter, $rules);
$xscript = $engine->getTranscript();
// Store any PHIDs we want to send email to for later.
foreach ($adapter->getEmailPHIDs() as $email_phid) {
$this->emailPHIDs[$email_phid] = $email_phid;
}
if ($blocking_effect === null) {
foreach ($effects as $effect) {
if ($effect->getAction() == HeraldAdapter::ACTION_BLOCK) {
$blocking_effect = $effect;
$blocked_update = $update;
break;
}
}
}
}
if ($blocking_effect) {
$rule = $blocking_effect->getRule();
$this->rejectCode = PhabricatorRepositoryPushLog::REJECT_HERALD;
$this->rejectDetails = $rule->getPHID();
$message = $blocking_effect->getTarget();
if (!strlen($message)) {
$message = pht('(None.)');
}
$blocked_ref_name = coalesce(
$blocked_update->getRefName(),
$blocked_update->getRefNewShort());
$blocked_name = $blocked_update->getRefType().'/'.$blocked_ref_name;
throw new DiffusionCommitHookRejectException(
pht(
"This push was rejected by Herald push rule %s.\n".
"Change: %s\n".
" Rule: %s\n".
"Reason: %s",
$rule->getMonogram(),
$blocked_name,
$rule->getName(),
$message));
}
}
public function loadViewerProjectPHIDsForHerald() {
// This just caches the viewer's projects so we don't need to load them
// over and over again when applying Herald rules.
if ($this->heraldViewerProjects === null) {
$this->heraldViewerProjects = id(new PhabricatorProjectQuery())
->setViewer($this->getViewer())
->withMemberPHIDs(array($this->getViewer()->getPHID()))
->execute();
}
return mpull($this->heraldViewerProjects, 'getPHID');
}
/* -( Git )---------------------------------------------------------------- */
private function findGitRefUpdates() {
$ref_updates = array();
// First, parse stdin, which lists all the ref changes. The input looks
// like this:
//
// <old hash> <new hash> <ref>
$stdin = $this->getStdin();
$lines = phutil_split_lines($stdin, $retain_endings = false);
foreach ($lines as $line) {
$parts = explode(' ', $line, 3);
if (count($parts) != 3) {
throw new Exception(pht('Expected "old new ref", got "%s".', $line));
}
$ref_old = $parts[0];
$ref_new = $parts[1];
$ref_raw = $parts[2];
if (preg_match('(^refs/heads/)', $ref_raw)) {
$ref_type = PhabricatorRepositoryPushLog::REFTYPE_BRANCH;
$ref_raw = substr($ref_raw, strlen('refs/heads/'));
} else if (preg_match('(^refs/tags/)', $ref_raw)) {
$ref_type = PhabricatorRepositoryPushLog::REFTYPE_TAG;
$ref_raw = substr($ref_raw, strlen('refs/tags/'));
} else {
throw new Exception(
pht(
"Unable to identify the reftype of '%s'. Rejecting push.",
$ref_raw));
}
$ref_update = $this->newPushLog()
->setRefType($ref_type)
->setRefName($ref_raw)
->setRefOld($ref_old)
->setRefNew($ref_new);
$ref_updates[] = $ref_update;
}
$this->findGitMergeBases($ref_updates);
$this->findGitChangeFlags($ref_updates);
return $ref_updates;
}
private function findGitMergeBases(array $ref_updates) {
assert_instances_of($ref_updates, 'PhabricatorRepositoryPushLog');
$futures = array();
foreach ($ref_updates as $key => $ref_update) {
// If the old hash is "00000...", the ref is being created (either a new
// branch, or a new tag). If the new hash is "00000...", the ref is being
// deleted. If both are nonempty, the ref is being updated. For updates,
// we'll figure out the `merge-base` of the old and new objects here. This
// lets us reject non-FF changes cheaply; later, we'll figure out exactly
// which commits are new.
$ref_old = $ref_update->getRefOld();
$ref_new = $ref_update->getRefNew();
if (($ref_old === self::EMPTY_HASH) ||
($ref_new === self::EMPTY_HASH)) {
continue;
}
$futures[$key] = $this->getRepository()->getLocalCommandFuture(
'merge-base %s %s',
$ref_old,
$ref_new);
}
$futures = id(new FutureIterator($futures))
->limit(8);
foreach ($futures as $key => $future) {
// If 'old' and 'new' have no common ancestors (for example, a force push
// which completely rewrites a ref), `git merge-base` will exit with
// an error and no output. It would be nice to find a positive test
// for this instead, but I couldn't immediately come up with one. See
// T4224. Assume this means there are no ancestors.
list($err, $stdout) = $future->resolve();
if ($err) {
$merge_base = null;
} else {
$merge_base = rtrim($stdout, "\n");
}
$ref_update = $ref_updates[$key];
$ref_update->setMergeBase($merge_base);
}
return $ref_updates;
}
private function findGitChangeFlags(array $ref_updates) {
assert_instances_of($ref_updates, 'PhabricatorRepositoryPushLog');
foreach ($ref_updates as $key => $ref_update) {
$ref_old = $ref_update->getRefOld();
$ref_new = $ref_update->getRefNew();
$ref_type = $ref_update->getRefType();
$ref_flags = 0;
$dangerous = null;
if (($ref_old === self::EMPTY_HASH) && ($ref_new === self::EMPTY_HASH)) {
// This happens if you try to delete a tag or branch which does not
// exist by pushing directly to the ref. Git will warn about it but
// allow it. Just call it a delete, without flagging it as dangerous.
$ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_DELETE;
} else if ($ref_old === self::EMPTY_HASH) {
$ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_ADD;
} else if ($ref_new === self::EMPTY_HASH) {
$ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_DELETE;
if ($ref_type == PhabricatorRepositoryPushLog::REFTYPE_BRANCH) {
$ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_DANGEROUS;
$dangerous = pht(
"The change you're attempting to push deletes the branch '%s'.",
$ref_update->getRefName());
}
} else {
$merge_base = $ref_update->getMergeBase();
if ($merge_base == $ref_old) {
// This is a fast-forward update to an existing branch.
// These are safe.
$ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_APPEND;
} else {
$ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_REWRITE;
// For now, we don't consider deleting or moving tags to be a
// "dangerous" update. It's way harder to get wrong and should be easy
// to recover from once we have better logging. Only add the dangerous
// flag if this ref is a branch.
if ($ref_type == PhabricatorRepositoryPushLog::REFTYPE_BRANCH) {
$ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_DANGEROUS;
$dangerous = pht(
"The change you're attempting to push updates the branch '%s' ".
"from '%s' to '%s', but this is not a fast-forward. Pushes ".
"which rewrite published branch history are dangerous.",
$ref_update->getRefName(),
$ref_update->getRefOldShort(),
$ref_update->getRefNewShort());
}
}
}
$ref_update->setChangeFlags($ref_flags);
if ($dangerous !== null) {
$ref_update->attachDangerousChangeDescription($dangerous);
}
}
return $ref_updates;
}
private function findGitContentUpdates(array $ref_updates) {
$flag_delete = PhabricatorRepositoryPushLog::CHANGEFLAG_DELETE;
$futures = array();
foreach ($ref_updates as $key => $ref_update) {
if ($ref_update->hasChangeFlags($flag_delete)) {
// Deleting a branch or tag can never create any new commits.
continue;
}
// NOTE: This piece of magic finds all new commits, by walking backward
// from the new value to the value of *any* existing ref in the
// repository. Particularly, this will cover the cases of a new branch, a
// completely moved tag, etc.
$futures[$key] = $this->getRepository()->getLocalCommandFuture(
'log --format=%s %s --not --all',
'%H',
$ref_update->getRefNew());
}
$content_updates = array();
$futures = id(new FutureIterator($futures))
->limit(8);
foreach ($futures as $key => $future) {
list($stdout) = $future->resolvex();
if (!strlen(trim($stdout))) {
// This change doesn't have any new commits. One common case of this
// is creating a new tag which points at an existing commit.
continue;
}
$commits = phutil_split_lines($stdout, $retain_newlines = false);
// If we're looking at a branch, mark all of the new commits as on that
// branch. It's only possible for these commits to be on updated branches,
// since any other branch heads are necessarily behind them.
$branch_name = null;
$ref_update = $ref_updates[$key];
$type_branch = PhabricatorRepositoryPushLog::REFTYPE_BRANCH;
if ($ref_update->getRefType() == $type_branch) {
$branch_name = $ref_update->getRefName();
}
foreach ($commits as $commit) {
if ($branch_name) {
$this->gitCommits[$commit][] = $branch_name;
}
$content_updates[$commit] = $this->newPushLog()
->setRefType(PhabricatorRepositoryPushLog::REFTYPE_COMMIT)
->setRefNew($commit)
->setChangeFlags(PhabricatorRepositoryPushLog::CHANGEFLAG_ADD);
}
}
return $content_updates;
}
/* -( Custom )------------------------------------------------------------- */
private function applyCustomHooks(array $updates) {
$args = $this->getOriginalArgv();
$stdin = $this->getStdin();
$console = PhutilConsole::getConsole();
$env = array(
'PHABRICATOR_REPOSITORY' => $this->getRepository()->getCallsign(),
self::ENV_USER => $this->getViewer()->getUsername(),
self::ENV_REMOTE_PROTOCOL => $this->getRemoteProtocol(),
self::ENV_REMOTE_ADDRESS => $this->getRemoteAddress(),
);
$directories = $this->getRepository()->getHookDirectories();
foreach ($directories as $directory) {
$hooks = $this->getExecutablesInDirectory($directory);
sort($hooks);
foreach ($hooks as $hook) {
// NOTE: We're explicitly running the hooks in sequential order to
// make this more predictable.
$future = id(new ExecFuture('%s %Ls', $hook, $args))
->setEnv($env, $wipe_process_env = false)
->write($stdin);
list($err, $stdout, $stderr) = $future->resolve();
if (!$err) {
// This hook ran OK, but echo its output in case there was something
// informative.
$console->writeOut('%s', $stdout);
$console->writeErr('%s', $stderr);
continue;
}
$this->rejectCode = PhabricatorRepositoryPushLog::REJECT_EXTERNAL;
$this->rejectDetails = basename($hook);
throw new DiffusionCommitHookRejectException(
pht(
"This push was rejected by custom hook script '%s':\n\n%s%s",
basename($hook),
$stdout,
$stderr));
}
}
}
private function getExecutablesInDirectory($directory) {
$executables = array();
if (!Filesystem::pathExists($directory)) {
return $executables;
}
foreach (Filesystem::listDirectory($directory) as $path) {
$full_path = $directory.DIRECTORY_SEPARATOR.$path;
if (!is_executable($full_path)) {
// Don't include non-executable files.
continue;
}
if (basename($full_path) == 'README') {
// Don't include README, even if it is marked as executable. It almost
// certainly got caught in the crossfire of a sweeping `chmod`, since
// users do this with some frequency.
continue;
}
$executables[] = $full_path;
}
return $executables;
}
/* -( Mercurial )---------------------------------------------------------- */
private function findMercurialRefUpdates() {
$hook = $this->getMercurialHook();
switch ($hook) {
case 'pretxnchangegroup':
return $this->findMercurialChangegroupRefUpdates();
case 'prepushkey':
return $this->findMercurialPushKeyRefUpdates();
default:
throw new Exception(pht('Unrecognized hook "%s"!', $hook));
}
}
private function findMercurialChangegroupRefUpdates() {
$hg_node = getenv('HG_NODE');
if (!$hg_node) {
- throw new Exception(pht('Expected HG_NODE in environment!'));
+ throw new Exception(
+ pht(
+ 'Expected %s in environment!',
+ 'HG_NODE'));
}
// NOTE: We need to make sure this is passed to subprocesses, or they won't
// be able to see new commits. Mercurial uses this as a marker to determine
// whether the pending changes are visible or not.
$_ENV['HG_PENDING'] = getenv('HG_PENDING');
$repository = $this->getRepository();
$futures = array();
foreach (array('old', 'new') as $key) {
$futures[$key] = $repository->getLocalCommandFuture(
'heads --template %s',
'{node}\1{branch}\2');
}
// Wipe HG_PENDING out of the old environment so we see the pre-commit
// state of the repository.
$futures['old']->updateEnv('HG_PENDING', null);
$futures['commits'] = $repository->getLocalCommandFuture(
'log --rev %s --template %s',
hgsprintf('%s:%s', $hg_node, 'tip'),
'{node}\1{branch}\2');
// Resolve all of the futures now. We don't need the 'commits' future yet,
// but it simplifies the logic to just get it out of the way.
foreach (new FutureIterator($futures) as $future) {
$future->resolve();
}
list($commit_raw) = $futures['commits']->resolvex();
$commit_map = $this->parseMercurialCommits($commit_raw);
$this->mercurialCommits = $commit_map;
// NOTE: `hg heads` exits with an error code and no output if the repository
// has no heads. Most commonly this happens on a new repository. We know
// we can run `hg` successfully since the `hg log` above didn't error, so
// just ignore the error code.
list($err, $old_raw) = $futures['old']->resolve();
$old_refs = $this->parseMercurialHeads($old_raw);
list($err, $new_raw) = $futures['new']->resolve();
$new_refs = $this->parseMercurialHeads($new_raw);
$all_refs = array_keys($old_refs + $new_refs);
$ref_updates = array();
foreach ($all_refs as $ref) {
$old_heads = idx($old_refs, $ref, array());
$new_heads = idx($new_refs, $ref, array());
sort($old_heads);
sort($new_heads);
if (!$old_heads && !$new_heads) {
// This should never be possible, as it makes no sense. Explode.
throw new Exception(
pht(
'Mercurial repository has no new or old heads for branch "%s" '.
'after push. This makes no sense; rejecting change.',
$ref));
}
if ($old_heads === $new_heads) {
// No changes to this branch, so skip it.
continue;
}
$stray_heads = array();
if ($old_heads && !$new_heads) {
// This is a branch deletion with "--close-branch".
$head_map = array();
foreach ($old_heads as $old_head) {
$head_map[$old_head] = array(self::EMPTY_HASH);
}
} else if (count($old_heads) > 1) {
// HORRIBLE: In Mercurial, branches can have multiple heads. If the
// old branch had multiple heads, we need to figure out which new
// heads descend from which old heads, so we can tell whether you're
// actively creating new heads (dangerous) or just working in a
// repository that's already full of garbage (strongly discouraged but
// not as inherently dangerous). These cases should be very uncommon.
// NOTE: We're only looking for heads on the same branch. The old
// tip of the branch may be the branchpoint for other branches, but that
// is OK.
$dfutures = array();
foreach ($old_heads as $old_head) {
$dfutures[$old_head] = $repository->getLocalCommandFuture(
'log --branch %s --rev %s --template %s',
$ref,
hgsprintf('(descendants(%s) and head())', $old_head),
'{node}\1');
}
$head_map = array();
foreach (new FutureIterator($dfutures) as $future_head => $dfuture) {
list($stdout) = $dfuture->resolvex();
$descendant_heads = array_filter(explode("\1", $stdout));
if ($descendant_heads) {
// This old head has at least one descendant in the push.
$head_map[$future_head] = $descendant_heads;
} else {
// This old head has no descendants, so it is being deleted.
$head_map[$future_head] = array(self::EMPTY_HASH);
}
}
// Now, find all the new stray heads this push creates, if any. These
// are new heads which do not descend from the old heads.
$seen = array_fuse(array_mergev($head_map));
foreach ($new_heads as $new_head) {
if ($new_head === self::EMPTY_HASH) {
// If a branch head is being deleted, don't insert it as an add.
continue;
}
if (empty($seen[$new_head])) {
$head_map[self::EMPTY_HASH][] = $new_head;
}
}
} else if ($old_heads) {
$head_map[head($old_heads)] = $new_heads;
} else {
$head_map[self::EMPTY_HASH] = $new_heads;
}
foreach ($head_map as $old_head => $child_heads) {
foreach ($child_heads as $new_head) {
if ($new_head === $old_head) {
continue;
}
$ref_flags = 0;
$dangerous = null;
if ($old_head == self::EMPTY_HASH) {
$ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_ADD;
} else {
$ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_APPEND;
}
$deletes_existing_head = ($new_head == self::EMPTY_HASH);
$splits_existing_head = (count($child_heads) > 1);
$creates_duplicate_head = ($old_head == self::EMPTY_HASH) &&
(count($head_map) > 1);
if ($splits_existing_head || $creates_duplicate_head) {
$readable_child_heads = array();
foreach ($child_heads as $child_head) {
$readable_child_heads[] = substr($child_head, 0, 12);
}
$ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_DANGEROUS;
if ($splits_existing_head) {
// We're splitting an existing head into two or more heads.
// This is dangerous, and a super bad idea. Note that we're only
// raising this if you're actively splitting a branch head. If a
// head split in the past, we don't consider appends to it
// to be dangerous.
$dangerous = pht(
"The change you're attempting to push splits the head of ".
"branch '%s' into multiple heads: %s. This is inadvisable ".
"and dangerous.",
$ref,
implode(', ', $readable_child_heads));
} else {
// We're adding a second (or more) head to a branch. The new
// head is not a descendant of any old head.
$dangerous = pht(
"The change you're attempting to push creates new, divergent ".
"heads for the branch '%s': %s. This is inadvisable and ".
"dangerous.",
$ref,
implode(', ', $readable_child_heads));
}
}
if ($deletes_existing_head) {
// TODO: Somewhere in here we should be setting CHANGEFLAG_REWRITE
// if we are also creating at least one other head to replace
// this one.
// NOTE: In Git, this is a dangerous change, but it is not dangerous
// in Mercurial. Mercurial branches are version controlled, and
// Mercurial does not prompt you for any special flags when pushing
// a `--close-branch` commit by default.
$ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_DELETE;
}
$ref_update = $this->newPushLog()
->setRefType(PhabricatorRepositoryPushLog::REFTYPE_BRANCH)
->setRefName($ref)
->setRefOld($old_head)
->setRefNew($new_head)
->setChangeFlags($ref_flags);
if ($dangerous !== null) {
$ref_update->attachDangerousChangeDescription($dangerous);
}
$ref_updates[] = $ref_update;
}
}
}
return $ref_updates;
}
private function findMercurialPushKeyRefUpdates() {
$key_namespace = getenv('HG_NAMESPACE');
if ($key_namespace === 'phases') {
// Mercurial changes commit phases as part of normal push operations. We
// just ignore these, as they don't seem to represent anything
// interesting.
return array();
}
$key_name = getenv('HG_KEY');
$key_old = getenv('HG_OLD');
if (!strlen($key_old)) {
$key_old = null;
}
$key_new = getenv('HG_NEW');
if (!strlen($key_new)) {
$key_new = null;
}
if ($key_namespace !== 'bookmarks') {
throw new Exception(
pht(
"Unknown Mercurial key namespace '%s', with key '%s' (%s -> %s). ".
"Rejecting push.",
$key_namespace,
$key_name,
coalesce($key_old, pht('null')),
coalesce($key_new, pht('null'))));
}
if ($key_old === $key_new) {
// We get a callback when the bookmark doesn't change. Just ignore this,
// as it's a no-op.
return array();
}
$ref_flags = 0;
$merge_base = null;
if ($key_old === null) {
$ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_ADD;
} else if ($key_new === null) {
$ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_DELETE;
} else {
list($merge_base_raw) = $this->getRepository()->execxLocalCommand(
'log --template %s --rev %s',
'{node}',
hgsprintf('ancestor(%s, %s)', $key_old, $key_new));
if (strlen(trim($merge_base_raw))) {
$merge_base = trim($merge_base_raw);
}
if ($merge_base && ($merge_base === $key_old)) {
$ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_APPEND;
} else {
$ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_REWRITE;
}
}
$ref_update = $this->newPushLog()
->setRefType(PhabricatorRepositoryPushLog::REFTYPE_BOOKMARK)
->setRefName($key_name)
->setRefOld(coalesce($key_old, self::EMPTY_HASH))
->setRefNew(coalesce($key_new, self::EMPTY_HASH))
->setChangeFlags($ref_flags);
return array($ref_update);
}
private function findMercurialContentUpdates(array $ref_updates) {
$content_updates = array();
foreach ($this->mercurialCommits as $commit => $branches) {
$content_updates[$commit] = $this->newPushLog()
->setRefType(PhabricatorRepositoryPushLog::REFTYPE_COMMIT)
->setRefNew($commit)
->setChangeFlags(PhabricatorRepositoryPushLog::CHANGEFLAG_ADD);
}
return $content_updates;
}
private function parseMercurialCommits($raw) {
$commits_lines = explode("\2", $raw);
$commits_lines = array_filter($commits_lines);
$commit_map = array();
foreach ($commits_lines as $commit_line) {
list($node, $branch) = explode("\1", $commit_line);
$commit_map[$node] = array($branch);
}
return $commit_map;
}
private function parseMercurialHeads($raw) {
$heads_map = $this->parseMercurialCommits($raw);
$heads = array();
foreach ($heads_map as $commit => $branches) {
foreach ($branches as $branch) {
$heads[$branch][] = $commit;
}
}
return $heads;
}
/* -( Subversion )--------------------------------------------------------- */
private function findSubversionRefUpdates() {
// Subversion doesn't have any kind of mutable ref metadata.
return array();
}
private function findSubversionContentUpdates(array $ref_updates) {
list($youngest) = execx(
'svnlook youngest %s',
$this->subversionRepository);
$ref_new = (int)$youngest + 1;
$ref_flags = 0;
$ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_ADD;
$ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_APPEND;
$ref_content = $this->newPushLog()
->setRefType(PhabricatorRepositoryPushLog::REFTYPE_COMMIT)
->setRefNew($ref_new)
->setChangeFlags($ref_flags);
return array($ref_content);
}
/* -( Internals )---------------------------------------------------------- */
private function newPushLog() {
// NOTE: We generate PHIDs up front so the Herald transcripts can pick them
// up.
$phid = id(new PhabricatorRepositoryPushLog())->generatePHID();
return PhabricatorRepositoryPushLog::initializeNewLog($this->getViewer())
->setPHID($phid)
->setRepositoryPHID($this->getRepository()->getPHID())
->attachRepository($this->getRepository())
->setEpoch(time());
}
private function newPushEvent() {
$viewer = $this->getViewer();
return PhabricatorRepositoryPushEvent::initializeNewEvent($viewer)
->setRepositoryPHID($this->getRepository()->getPHID())
->setRemoteAddress($this->getRemoteAddressForLog())
->setRemoteProtocol($this->getRemoteProtocol())
->setEpoch(time());
}
public function loadChangesetsForCommit($identifier) {
$byte_limit = HeraldCommitAdapter::getEnormousByteLimit();
$time_limit = HeraldCommitAdapter::getEnormousTimeLimit();
$vcs = $this->getRepository()->getVersionControlSystem();
switch ($vcs) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
// For git and hg, we can use normal commands.
$drequest = DiffusionRequest::newFromDictionary(
array(
'repository' => $this->getRepository(),
'user' => $this->getViewer(),
'commit' => $identifier,
));
$raw_diff = DiffusionRawDiffQuery::newFromDiffusionRequest($drequest)
->setTimeout($time_limit)
->setByteLimit($byte_limit)
->setLinesOfContext(0)
->loadRawDiff();
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
// TODO: This diff has 3 lines of context, which produces slightly
// incorrect "added file content" and "removed file content" results.
// This may also choke on binaries, but "svnlook diff" does not support
// the "--diff-cmd" flag.
// For subversion, we need to use `svnlook`.
$future = new ExecFuture(
'svnlook diff -t %s %s',
$this->subversionTransaction,
$this->subversionRepository);
$future->setTimeout($time_limit);
$future->setStdoutSizeLimit($byte_limit);
$future->setStderrSizeLimit($byte_limit);
list($raw_diff) = $future->resolvex();
break;
default:
throw new Exception(pht("Unknown VCS '%s!'", $vcs));
}
if (strlen($raw_diff) >= $byte_limit) {
throw new Exception(
pht(
'The raw text of this change is enormous (larger than %d '.
'bytes). Herald can not process it.',
$byte_limit));
}
if (!strlen($raw_diff)) {
// If the commit is actually empty, just return no changesets.
return array();
}
$parser = new ArcanistDiffParser();
$changes = $parser->parseDiff($raw_diff);
$diff = DifferentialDiff::newEphemeralFromRawChanges(
$changes);
return $diff->getChangesets();
}
public function loadCommitRefForCommit($identifier) {
$repository = $this->getRepository();
$vcs = $repository->getVersionControlSystem();
switch ($vcs) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
return id(new DiffusionLowLevelCommitQuery())
->setRepository($repository)
->withIdentifier($identifier)
->execute();
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
// For subversion, we need to use `svnlook`.
list($message) = execx(
'svnlook log -t %s %s',
$this->subversionTransaction,
$this->subversionRepository);
return id(new DiffusionCommitRef())
->setMessage($message);
break;
default:
throw new Exception(pht("Unknown VCS '%s!'", $vcs));
}
}
public function loadBranches($identifier) {
$repository = $this->getRepository();
$vcs = $repository->getVersionControlSystem();
switch ($vcs) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
return idx($this->gitCommits, $identifier, array());
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
// NOTE: This will be "the branch the commit was made to", not
// "a list of all branch heads which descend from the commit".
// This is consistent with Mercurial, but possibly confusing.
return idx($this->mercurialCommits, $identifier, array());
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
// Subversion doesn't have branches.
return array();
}
}
private function loadCommitInfoForWorker(array $all_updates) {
$type_commit = PhabricatorRepositoryPushLog::REFTYPE_COMMIT;
$map = array();
foreach ($all_updates as $update) {
if ($update->getRefType() != $type_commit) {
continue;
}
$map[$update->getRefNew()] = array();
}
foreach ($map as $identifier => $info) {
$ref = $this->loadCommitRefForCommit($identifier);
$map[$identifier] += array(
'summary' => $ref->getSummary(),
'branches' => $this->loadBranches($identifier),
);
}
return $map;
}
private function isInitialImport(array $all_updates) {
$repository = $this->getRepository();
$vcs = $repository->getVersionControlSystem();
switch ($vcs) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
// There is no meaningful way to import history into Subversion by
// pushing.
return false;
default:
break;
}
// Now, apply a heuristic to guess whether this is a normal commit or
// an initial import. We guess something is an initial import if:
//
// - the repository is currently empty; and
// - it pushes more than 7 commits at once.
//
// The number "7" is chosen arbitrarily as seeming reasonable. We could
// also look at author data (do the commits come from multiple different
// authors?) and commit date data (is the oldest commit more than 48 hours
// old), but we don't have immediate access to those and this simple
// heruistic might be good enough.
$commit_count = 0;
$type_commit = PhabricatorRepositoryPushLog::REFTYPE_COMMIT;
foreach ($all_updates as $update) {
if ($update->getRefType() != $type_commit) {
continue;
}
$commit_count++;
}
if ($commit_count <= 7) {
// If this pushes a very small number of commits, assume it's an
// initial commit or stack of a few initial commits.
return false;
}
$any_commits = id(new DiffusionCommitQuery())
->setViewer($this->getViewer())
->withRepository($repository)
->setLimit(1)
->execute();
if ($any_commits) {
// If the repository already has commits, this isn't an import.
return false;
}
return true;
}
}
diff --git a/src/applications/diffusion/exception/DiffusionSetupException.php b/src/applications/diffusion/exception/DiffusionSetupException.php
index eecf7a81f..0ce5994b7 100644
--- a/src/applications/diffusion/exception/DiffusionSetupException.php
+++ b/src/applications/diffusion/exception/DiffusionSetupException.php
@@ -1,9 +1,9 @@
<?php
final class DiffusionSetupException extends AphrontUsageException {
public function __construct($message) {
- parent::__construct('Diffusion Setup Exception', $message);
+ parent::__construct(pht('Diffusion Setup Exception'), $message);
}
}
diff --git a/src/applications/diffusion/protocol/DiffusionMercurialWireProtocol.php b/src/applications/diffusion/protocol/DiffusionMercurialWireProtocol.php
index 578196ff6..6d5257365 100644
--- a/src/applications/diffusion/protocol/DiffusionMercurialWireProtocol.php
+++ b/src/applications/diffusion/protocol/DiffusionMercurialWireProtocol.php
@@ -1,102 +1,102 @@
<?php
final class DiffusionMercurialWireProtocol {
public static function getCommandArgs($command) {
// We need to enumerate all of the Mercurial wire commands because the
// argument encoding varies based on the command. "Why?", you might ask,
// "Why would you do this?".
$commands = array(
'batch' => array('cmds', '*'),
'between' => array('pairs'),
'branchmap' => array(),
'branches' => array('nodes'),
'capabilities' => array(),
'changegroup' => array('roots'),
'changegroupsubset' => array('bases heads'),
'debugwireargs' => array('one two *'),
'getbundle' => array('*'),
'heads' => array(),
'hello' => array(),
'known' => array('nodes', '*'),
'listkeys' => array('namespace'),
'lookup' => array('key'),
'pushkey' => array('namespace', 'key', 'old', 'new'),
'stream_out' => array(''),
'unbundle' => array('heads'),
);
if (!isset($commands[$command])) {
- throw new Exception("Unknown Mercurial command '{$command}!");
+ throw new Exception(pht("Unknown Mercurial command '%s!", $command));
}
return $commands[$command];
}
public static function isReadOnlyCommand($command) {
$read_only = array(
'between' => true,
'branchmap' => true,
'branches' => true,
'capabilities' => true,
'changegroup' => true,
'changegroupsubset' => true,
'debugwireargs' => true,
'getbundle' => true,
'heads' => true,
'hello' => true,
'known' => true,
'listkeys' => true,
'lookup' => true,
'stream_out' => true,
);
// Notably, the write commands are "pushkey" and "unbundle". The
// "batch" command is theoretically read only, but we require explicit
// analysis of the actual commands.
return isset($read_only[$command]);
}
public static function isReadOnlyBatchCommand($cmds) {
if (!strlen($cmds)) {
// We expect a "batch" command to always have a "cmds" string, so err
// on the side of caution and throw if we don't get any data here. This
// either indicates a mangled command from the client or a programming
// error in our code.
- throw new Exception("Expected nonempty 'cmds' specification!");
+ throw new Exception(pht("Expected nonempty '%s' specification!", 'cmds'));
}
// For "batch" we get a "cmds" argument like:
//
// heads ;known nodes=
//
// We need to examine the commands (here, "heads" and "known") to make sure
// they're all read-only.
// NOTE: Mercurial has some code to escape semicolons, but it does not
// actually function for command separation. For example, these two batch
// commands will produce completely different results (the former will run
// the lookup; the latter will fail with a parser error):
//
// lookup key=a:xb;lookup key=z* 0
// lookup key=a:;b;lookup key=z* 0
// ^
// |
// +-- Note semicolon.
//
// So just split unconditionally.
$cmds = explode(';', $cmds);
foreach ($cmds as $sub_cmd) {
$name = head(explode(' ', $sub_cmd, 2));
if (!self::isReadOnlyCommand($name)) {
return false;
}
}
return true;
}
}
diff --git a/src/applications/diffusion/protocol/DiffusionSubversionWireProtocol.php b/src/applications/diffusion/protocol/DiffusionSubversionWireProtocol.php
index e5645859a..1cb0f107a 100644
--- a/src/applications/diffusion/protocol/DiffusionSubversionWireProtocol.php
+++ b/src/applications/diffusion/protocol/DiffusionSubversionWireProtocol.php
@@ -1,192 +1,197 @@
<?php
final class DiffusionSubversionWireProtocol extends Phobject {
private $buffer = '';
private $state = 'item';
private $expectBytes = 0;
private $byteBuffer = '';
private $stack = array();
private $list = array();
private $raw = '';
private function pushList() {
$this->stack[] = $this->list;
$this->list = array();
}
private function popList() {
$list = $this->list;
$this->list = array_pop($this->stack);
return $list;
}
private function pushItem($item, $type) {
$this->list[] = array(
'type' => $type,
'value' => $item,
);
}
public function writeData($data) {
$this->buffer .= $data;
$messages = array();
while (true) {
if ($this->state == 'item') {
$match = null;
$result = null;
$buf = $this->buffer;
if (preg_match('/^([a-z][a-z0-9-]*)\s/i', $buf, $match)) {
$this->pushItem($match[1], 'word');
} else if (preg_match('/^(\d+)\s/', $buf, $match)) {
$this->pushItem((int)$match[1], 'number');
} else if (preg_match('/^(\d+):/', $buf, $match)) {
// NOTE: The "+ 1" includes the space after the string.
$this->expectBytes = (int)$match[1] + 1;
$this->state = 'bytes';
} else if (preg_match('/^(\\()\s/', $buf, $match)) {
$this->pushList();
} else if (preg_match('/^(\\))\s/', $buf, $match)) {
$list = $this->popList();
if ($this->stack) {
$this->pushItem($list, 'list');
} else {
$result = $list;
}
} else {
$match = false;
}
if ($match !== false) {
$this->raw .= substr($this->buffer, 0, strlen($match[0]));
$this->buffer = substr($this->buffer, strlen($match[0]));
if ($result !== null) {
$messages[] = array(
'structure' => $list,
'raw' => $this->raw,
);
$this->raw = '';
}
} else {
// No matches yet, wait for more data.
break;
}
} else if ($this->state == 'bytes') {
$new_data = substr($this->buffer, 0, $this->expectBytes);
if (!strlen($new_data)) {
// No more bytes available yet, wait for more data.
break;
}
$this->buffer = substr($this->buffer, strlen($new_data));
$this->expectBytes -= strlen($new_data);
$this->raw .= $new_data;
$this->byteBuffer .= $new_data;
if (!$this->expectBytes) {
$this->state = 'byte-space';
// Strip off the terminal space.
$this->pushItem(substr($this->byteBuffer, 0, -1), 'string');
$this->byteBuffer = '';
$this->state = 'item';
}
} else {
- throw new Exception("Invalid state '{$this->state}'!");
+ throw new Exception(pht("Invalid state '%s'!", $this->state));
}
}
return $messages;
}
/**
* Convert a parsed command struct into a wire protocol string.
*/
public function serializeStruct(array $struct) {
$out = array();
$out[] = '( ';
foreach ($struct as $item) {
$value = $item['value'];
$type = $item['type'];
switch ($type) {
case 'word':
$out[] = $value;
break;
case 'number':
$out[] = $value;
break;
case 'string':
$out[] = strlen($value).':'.$value;
break;
case 'list':
$out[] = self::serializeStruct($value);
break;
default:
- throw new Exception("Unknown SVN wire protocol structure '{$type}'!");
+ throw new Exception(
+ pht(
+ "Unknown SVN wire protocol structure '%s'!",
+ $type));
}
if ($type != 'list') {
$out[] = ' ';
}
}
$out[] = ') ';
return implode('', $out);
}
public function isReadOnlyCommand(array $struct) {
if (empty($struct[0]['type']) || ($struct[0]['type'] != 'word')) {
// This isn't what we expect; fail defensively.
throw new Exception(
- pht("Unexpected command structure, expected '( word ... )'."));
+ pht(
+ "Unexpected command structure, expected '%s'.",
+ '( word ... )'));
}
switch ($struct[0]['value']) {
// Authentication command set.
case 'EXTERNAL':
// The "Main" command set. Some of the commands in this command set are
// mutation commands, and are omitted from this list.
case 'reparent':
case 'get-latest-rev':
case 'get-dated-rev':
case 'rev-proplist':
case 'rev-prop':
case 'get-file':
case 'get-dir':
case 'check-path':
case 'stat':
case 'update':
case 'get-mergeinfo':
case 'switch':
case 'status':
case 'diff':
case 'log':
case 'get-file-revs':
case 'get-locations':
// The "Report" command set. These are not actually mutation
// operations, they just define a request for information.
case 'set-path':
case 'delete-path':
case 'link-path':
case 'finish-report':
case 'abort-report':
// These are used to report command results.
case 'success':
case 'failure':
// If we get here, we've matched some known read-only command.
return true;
default:
// Anything else isn't a known read-only command, so require write
// access to use it.
break;
}
return false;
}
}
diff --git a/src/applications/diffusion/query/DiffusionCachedResolveRefsQuery.php b/src/applications/diffusion/query/DiffusionCachedResolveRefsQuery.php
index 8190a035f..55e86ce87 100644
--- a/src/applications/diffusion/query/DiffusionCachedResolveRefsQuery.php
+++ b/src/applications/diffusion/query/DiffusionCachedResolveRefsQuery.php
@@ -1,185 +1,185 @@
<?php
/**
* Resolves references into canonical, stable commit identifiers by examining
* database caches.
*
* This is a counterpart to @{class:DiffusionLowLevelResolveRefsQuery}. This
* query offers fast resolution, but can not resolve everything that the
* low-level query can.
*
* This class can resolve the most common refs (commits, branches, tags) and
* can do so cheapy (by examining the database, without needing to make calls
* to the VCS or the service host).
*/
final class DiffusionCachedResolveRefsQuery
extends DiffusionLowLevelQuery {
private $refs;
private $types;
public function withRefs(array $refs) {
$this->refs = $refs;
return $this;
}
public function withTypes(array $types) {
$this->types = $types;
return $this;
}
protected function executeQuery() {
if (!$this->refs) {
return array();
}
switch ($this->getRepository()->getVersionControlSystem()) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
$result = $this->resolveGitAndMercurialRefs();
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
$result = $this->resolveSubversionRefs();
break;
default:
- throw new Exception('Unsupported repository type!');
+ throw new Exception(pht('Unsupported repository type!'));
}
if ($this->types !== null) {
$result = $this->filterRefsByType($result, $this->types);
}
return $result;
}
/**
* Resolve refs in Git and Mercurial repositories.
*
* We can resolve commit hashes from the commits table, and branch and tag
* names from the refcursor table.
*/
private function resolveGitAndMercurialRefs() {
$repository = $this->getRepository();
$conn_r = $repository->establishConnection('r');
$results = array();
$prefixes = array();
foreach ($this->refs as $ref) {
// We require refs to look like hashes and be at least 4 characters
// long. This is similar to the behavior of git.
if (preg_match('/^[a-f0-9]{4,}$/', $ref)) {
$prefixes[] = qsprintf(
$conn_r,
'(commitIdentifier LIKE %>)',
$ref);
}
}
if ($prefixes) {
$commits = queryfx_all(
$conn_r,
'SELECT commitIdentifier FROM %T
WHERE repositoryID = %s AND %Q',
id(new PhabricatorRepositoryCommit())->getTableName(),
$repository->getID(),
implode(' OR ', $prefixes));
foreach ($commits as $commit) {
$hash = $commit['commitIdentifier'];
foreach ($this->refs as $ref) {
if (!strncmp($hash, $ref, strlen($ref))) {
$results[$ref][] = array(
'type' => 'commit',
'identifier' => $hash,
);
}
}
}
}
$name_hashes = array();
foreach ($this->refs as $ref) {
$name_hashes[PhabricatorHash::digestForIndex($ref)] = $ref;
}
$cursors = queryfx_all(
$conn_r,
'SELECT refNameHash, refType, commitIdentifier, isClosed FROM %T
WHERE repositoryPHID = %s AND refNameHash IN (%Ls)',
id(new PhabricatorRepositoryRefCursor())->getTableName(),
$repository->getPHID(),
array_keys($name_hashes));
foreach ($cursors as $cursor) {
if (isset($name_hashes[$cursor['refNameHash']])) {
$results[$name_hashes[$cursor['refNameHash']]][] = array(
'type' => $cursor['refType'],
'identifier' => $cursor['commitIdentifier'],
'closed' => (bool)$cursor['isClosed'],
);
// TODO: In Git, we don't store (and thus don't return) the hash
// of the tag itself. It would be vaguely nice to do this.
}
}
return $results;
}
/**
* Resolve refs in Subversion repositories.
*
* We can resolve all numeric identifiers and the keyword `HEAD`.
*/
private function resolveSubversionRefs() {
$repository = $this->getRepository();
$max_commit = id(new PhabricatorRepositoryCommit())
->loadOneWhere(
'repositoryID = %d ORDER BY epoch DESC, id DESC LIMIT 1',
$repository->getID());
if (!$max_commit) {
// This repository is empty or hasn't parsed yet, so none of the refs are
// going to resolve.
return array();
}
$max_commit_id = (int)$max_commit->getCommitIdentifier();
$results = array();
foreach ($this->refs as $ref) {
if ($ref == 'HEAD') {
// Resolve "HEAD" to mean "the most recent commit".
$results[$ref][] = array(
'type' => 'commit',
'identifier' => $max_commit_id,
);
continue;
}
if (!preg_match('/^\d+$/', $ref)) {
// This ref is non-numeric, so it doesn't resolve to anything.
continue;
}
// Resolve other commits if we can deduce their existence.
// TODO: When we import only part of a repository, we won't necessarily
// have all of the smaller commits. Should we fail to resolve them here
// for repositories with a subpath? It might let us simplify other things
// elsewhere.
if ((int)$ref <= $max_commit_id) {
$results[$ref][] = array(
'type' => 'commit',
'identifier' => (int)$ref,
);
}
}
return $results;
}
}
diff --git a/src/applications/diffusion/query/DiffusionCommitQuery.php b/src/applications/diffusion/query/DiffusionCommitQuery.php
index 23f886154..93efb1df1 100644
--- a/src/applications/diffusion/query/DiffusionCommitQuery.php
+++ b/src/applications/diffusion/query/DiffusionCommitQuery.php
@@ -1,566 +1,570 @@
<?php
final class DiffusionCommitQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $ids;
private $phids;
private $authorPHIDs;
private $defaultRepository;
private $identifiers;
private $repositoryIDs;
private $repositoryPHIDs;
private $identifierMap;
private $needAuditRequests;
private $auditIDs;
private $auditorPHIDs;
private $auditAwaitingUser;
private $auditStatus;
const AUDIT_STATUS_ANY = 'audit-status-any';
const AUDIT_STATUS_OPEN = 'audit-status-open';
const AUDIT_STATUS_CONCERN = 'audit-status-concern';
const AUDIT_STATUS_ACCEPTED = 'audit-status-accepted';
const AUDIT_STATUS_PARTIAL = 'audit-status-partial';
private $needCommitData;
public function withIDs(array $ids) {
$this->ids = $ids;
return $this;
}
public function withPHIDs(array $phids) {
$this->phids = $phids;
return $this;
}
public function withAuthorPHIDs(array $phids) {
$this->authorPHIDs = $phids;
return $this;
}
/**
* Load commits by partial or full identifiers, e.g. "rXab82393", "rX1234",
* or "a9caf12". When an identifier matches multiple commits, they will all
* be returned; callers should be prepared to deal with more results than
* they queried for.
*/
public function withIdentifiers(array $identifiers) {
$this->identifiers = $identifiers;
return $this;
}
/**
* Look up commits in a specific repository. This is a shorthand for calling
* @{method:withDefaultRepository} and @{method:withRepositoryIDs}.
*/
public function withRepository(PhabricatorRepository $repository) {
$this->withDefaultRepository($repository);
$this->withRepositoryIDs(array($repository->getID()));
return $this;
}
/**
* Look up commits in a specific repository. Prefer
* @{method:withRepositoryIDs}; the underyling table is keyed by ID such
* that this method requires a separate initial query to map PHID to ID.
*/
public function withRepositoryPHIDs(array $phids) {
$this->repositoryPHIDs = $phids;
}
/**
* If a default repository is provided, ambiguous commit identifiers will
* be assumed to belong to the default repository.
*
* For example, "r123" appearing in a commit message in repository X is
* likely to be unambiguously "rX123". Normally the reference would be
* considered ambiguous, but if you provide a default repository it will
* be correctly resolved.
*/
public function withDefaultRepository(PhabricatorRepository $repository) {
$this->defaultRepository = $repository;
return $this;
}
public function withRepositoryIDs(array $repository_ids) {
$this->repositoryIDs = $repository_ids;
return $this;
}
public function needCommitData($need) {
$this->needCommitData = $need;
return $this;
}
public function needAuditRequests($need) {
$this->needAuditRequests = $need;
return $this;
}
/**
* Returns true if we should join the audit table, either because we're
* interested in the information if it's available or because matching rows
* must always have it.
*/
private function shouldJoinAudits() {
return $this->auditStatus ||
$this->rowsMustHaveAudits();
}
/**
* Return true if we should `JOIN` (vs `LEFT JOIN`) the audit table, because
* matching commits will always have audit rows.
*/
private function rowsMustHaveAudits() {
return
$this->auditIDs ||
$this->auditorPHIDs ||
$this->auditAwaitingUser;
}
public function withAuditIDs(array $ids) {
$this->auditIDs = $ids;
return $this;
}
public function withAuditorPHIDs(array $auditor_phids) {
$this->auditorPHIDs = $auditor_phids;
return $this;
}
public function withAuditAwaitingUser(PhabricatorUser $user) {
$this->auditAwaitingUser = $user;
return $this;
}
public function withAuditStatus($status) {
$this->auditStatus = $status;
return $this;
}
public function getIdentifierMap() {
if ($this->identifierMap === null) {
throw new Exception(
- 'You must execute() the query before accessing the identifier map.');
+ pht(
+ 'You must %s the query before accessing the identifier map.',
+ 'execute()'));
}
return $this->identifierMap;
}
protected function getPrimaryTableAlias() {
return 'commit';
}
protected function willExecute() {
if ($this->identifierMap === null) {
$this->identifierMap = array();
}
}
protected function loadPage() {
$table = new PhabricatorRepositoryCommit();
$conn_r = $table->establishConnection('r');
$data = queryfx_all(
$conn_r,
'SELECT commit.* FROM %T commit %Q %Q %Q %Q %Q',
$table->getTableName(),
$this->buildJoinClause($conn_r),
$this->buildWhereClause($conn_r),
$this->buildGroupClause($conn_r),
$this->buildOrderClause($conn_r),
$this->buildLimitClause($conn_r));
return $table->loadAllFromArray($data);
}
protected function willFilterPage(array $commits) {
$repository_ids = mpull($commits, 'getRepositoryID', 'getRepositoryID');
$repos = id(new PhabricatorRepositoryQuery())
->setViewer($this->getViewer())
->withIDs($repository_ids)
->execute();
$min_qualified = PhabricatorRepository::MINIMUM_QUALIFIED_HASH;
$result = array();
foreach ($commits as $key => $commit) {
$repo = idx($repos, $commit->getRepositoryID());
if ($repo) {
$commit->attachRepository($repo);
} else {
unset($commits[$key]);
continue;
}
// Build the identifierMap
if ($this->identifiers !== null) {
$ids = array_fuse($this->identifiers);
$prefixes = array(
'r'.$commit->getRepository()->getCallsign(),
'r'.$commit->getRepository()->getCallsign().':',
'R'.$commit->getRepository()->getID().':',
'', // No prefix is valid too and will only match the commitIdentifier
);
$suffix = $commit->getCommitIdentifier();
if ($commit->getRepository()->isSVN()) {
foreach ($prefixes as $prefix) {
if (isset($ids[$prefix.$suffix])) {
$result[$prefix.$suffix][] = $commit;
}
}
} else {
// This awkward construction is so we can link the commits up in O(N)
// time instead of O(N^2).
for ($ii = $min_qualified; $ii <= strlen($suffix); $ii++) {
$part = substr($suffix, 0, $ii);
foreach ($prefixes as $prefix) {
if (isset($ids[$prefix.$part])) {
$result[$prefix.$part][] = $commit;
}
}
}
}
}
}
if ($result) {
foreach ($result as $identifier => $matching_commits) {
if (count($matching_commits) == 1) {
$result[$identifier] = head($matching_commits);
} else {
// This reference is ambiguous (it matches more than one commit) so
// don't link it.
unset($result[$identifier]);
}
}
$this->identifierMap += $result;
}
return $commits;
}
protected function didFilterPage(array $commits) {
if ($this->needCommitData) {
$data = id(new PhabricatorRepositoryCommitData())->loadAllWhere(
'commitID in (%Ld)',
mpull($commits, 'getID'));
$data = mpull($data, null, 'getCommitID');
foreach ($commits as $commit) {
$commit_data = idx($data, $commit->getID());
if (!$commit_data) {
$commit_data = new PhabricatorRepositoryCommitData();
}
$commit->attachCommitData($commit_data);
}
}
// TODO: This should just be `needAuditRequests`, not `shouldJoinAudits()`,
// but leave that for a future diff.
if ($this->needAuditRequests || $this->shouldJoinAudits()) {
$requests = id(new PhabricatorRepositoryAuditRequest())->loadAllWhere(
'commitPHID IN (%Ls)',
mpull($commits, 'getPHID'));
$requests = mgroup($requests, 'getCommitPHID');
foreach ($commits as $commit) {
$audit_requests = idx($requests, $commit->getPHID(), array());
$commit->attachAudits($audit_requests);
foreach ($audit_requests as $audit_request) {
$audit_request->attachCommit($commit);
}
}
}
return $commits;
}
protected function buildWhereClause(AphrontDatabaseConnection $conn_r) {
$where = array();
if ($this->repositoryPHIDs !== null) {
$map_repositories = id (new PhabricatorRepositoryQuery())
->setViewer($this->getViewer())
->withPHIDs($this->repositoryPHIDs)
->execute();
if (!$map_repositories) {
throw new PhabricatorEmptyQueryException();
}
$repository_ids = mpull($map_repositories, 'getID');
if ($this->repositoryIDs !== null) {
$repository_ids = array_merge($repository_ids, $this->repositoryIDs);
}
$this->withRepositoryIDs($repository_ids);
}
if ($this->ids !== null) {
$where[] = qsprintf(
$conn_r,
'commit.id IN (%Ld)',
$this->ids);
}
if ($this->phids !== null) {
$where[] = qsprintf(
$conn_r,
'commit.phid IN (%Ls)',
$this->phids);
}
if ($this->repositoryIDs !== null) {
$where[] = qsprintf(
$conn_r,
'commit.repositoryID IN (%Ld)',
$this->repositoryIDs);
}
if ($this->authorPHIDs !== null) {
$where[] = qsprintf(
$conn_r,
'commit.authorPHID IN (%Ls)',
$this->authorPHIDs);
}
if ($this->identifiers !== null) {
$min_unqualified = PhabricatorRepository::MINIMUM_UNQUALIFIED_HASH;
$min_qualified = PhabricatorRepository::MINIMUM_QUALIFIED_HASH;
$refs = array();
$bare = array();
foreach ($this->identifiers as $identifier) {
$matches = null;
preg_match('/^(?:[rR]([A-Z]+:?|[0-9]+:))?(.*)$/',
$identifier, $matches);
$repo = nonempty(rtrim($matches[1], ':'), null);
$commit_identifier = nonempty($matches[2], null);
if ($repo === null) {
if ($this->defaultRepository) {
$repo = $this->defaultRepository->getCallsign();
}
}
if ($repo === null) {
if (strlen($commit_identifier) < $min_unqualified) {
continue;
}
$bare[] = $commit_identifier;
} else {
$refs[] = array(
'callsign' => $repo,
'identifier' => $commit_identifier,
);
}
}
$sql = array();
foreach ($bare as $identifier) {
$sql[] = qsprintf(
$conn_r,
'(commit.commitIdentifier LIKE %> AND '.
'LENGTH(commit.commitIdentifier) = 40)',
$identifier);
}
if ($refs) {
$callsigns = ipull($refs, 'callsign');
$repos = id(new PhabricatorRepositoryQuery())
->setViewer($this->getViewer())
->withIdentifiers($callsigns);
$repos->execute();
$repos = $repos->getIdentifierMap();
foreach ($refs as $key => $ref) {
$repo = idx($repos, $ref['callsign']);
if (!$repo) {
continue;
}
if ($repo->isSVN()) {
if (!ctype_digit($ref['identifier'])) {
continue;
}
$sql[] = qsprintf(
$conn_r,
'(commit.repositoryID = %d AND commit.commitIdentifier = %s)',
$repo->getID(),
// NOTE: Because the 'commitIdentifier' column is a string, MySQL
// ignores the index if we hand it an integer. Hand it a string.
// See T3377.
(int)$ref['identifier']);
} else {
if (strlen($ref['identifier']) < $min_qualified) {
continue;
}
$sql[] = qsprintf(
$conn_r,
'(commit.repositoryID = %d AND commit.commitIdentifier LIKE %>)',
$repo->getID(),
$ref['identifier']);
}
}
}
if (!$sql) {
// If we discarded all possible identifiers (e.g., they all referenced
// bogus repositories or were all too short), make sure the query finds
// nothing.
throw new PhabricatorEmptyQueryException(
pht('No commit identifiers.'));
}
$where[] = '('.implode(' OR ', $sql).')';
}
if ($this->auditIDs !== null) {
$where[] = qsprintf(
$conn_r,
'audit.id IN (%Ld)',
$this->auditIDs);
}
if ($this->auditorPHIDs !== null) {
$where[] = qsprintf(
$conn_r,
'audit.auditorPHID IN (%Ls)',
$this->auditorPHIDs);
}
if ($this->auditAwaitingUser) {
$awaiting_user_phid = $this->auditAwaitingUser->getPHID();
// Exclude package and project audits associated with commits where
// the user is the author.
$where[] = qsprintf(
$conn_r,
'(commit.authorPHID IS NULL OR commit.authorPHID != %s)
OR (audit.auditorPHID = %s)',
$awaiting_user_phid,
$awaiting_user_phid);
}
$status = $this->auditStatus;
if ($status !== null) {
switch ($status) {
case self::AUDIT_STATUS_PARTIAL:
$where[] = qsprintf(
$conn_r,
'commit.auditStatus = %d',
PhabricatorAuditCommitStatusConstants::PARTIALLY_AUDITED);
break;
case self::AUDIT_STATUS_ACCEPTED:
$where[] = qsprintf(
$conn_r,
'commit.auditStatus = %d',
PhabricatorAuditCommitStatusConstants::FULLY_AUDITED);
break;
case self::AUDIT_STATUS_CONCERN:
$where[] = qsprintf(
$conn_r,
'audit.auditStatus = %s',
PhabricatorAuditStatusConstants::CONCERNED);
break;
case self::AUDIT_STATUS_OPEN:
$where[] = qsprintf(
$conn_r,
'audit.auditStatus in (%Ls)',
PhabricatorAuditStatusConstants::getOpenStatusConstants());
if ($this->auditAwaitingUser) {
$where[] = qsprintf(
$conn_r,
'awaiting.auditStatus IS NULL OR awaiting.auditStatus != %s',
PhabricatorAuditStatusConstants::RESIGNED);
}
break;
case self::AUDIT_STATUS_ANY:
break;
default:
$valid = array(
self::AUDIT_STATUS_ANY,
self::AUDIT_STATUS_OPEN,
self::AUDIT_STATUS_CONCERN,
self::AUDIT_STATUS_ACCEPTED,
self::AUDIT_STATUS_PARTIAL,
);
throw new Exception(
- "Unknown audit status '{$status}'! Valid statuses are: ".
- implode(', ', $valid));
+ pht(
+ "Unknown audit status '%s'! Valid statuses are: %s.",
+ $status,
+ implode(', ', $valid)));
}
}
$where[] = $this->buildPagingClause($conn_r);
return $this->formatWhereClause($where);
}
protected function didFilterResults(array $filtered) {
if ($this->identifierMap) {
foreach ($this->identifierMap as $name => $commit) {
if (isset($filtered[$commit->getPHID()])) {
unset($this->identifierMap[$name]);
}
}
}
}
protected function buildJoinClause(AphrontDatabaseConnection $conn_r) {
$joins = array();
$audit_request = new PhabricatorRepositoryAuditRequest();
if ($this->shouldJoinAudits()) {
$joins[] = qsprintf(
$conn_r,
'%Q %T audit ON commit.phid = audit.commitPHID',
($this->rowsMustHaveAudits() ? 'JOIN' : 'LEFT JOIN'),
$audit_request->getTableName());
}
if ($this->auditAwaitingUser) {
// Join the request table on the awaiting user's requests, so we can
// filter out package and project requests which the user has resigned
// from.
$joins[] = qsprintf(
$conn_r,
'LEFT JOIN %T awaiting ON audit.commitPHID = awaiting.commitPHID AND
awaiting.auditorPHID = %s',
$audit_request->getTableName(),
$this->auditAwaitingUser->getPHID());
}
if ($joins) {
return implode(' ', $joins);
} else {
return '';
}
}
protected function buildGroupClause(AphrontDatabaseConnection $conn_r) {
$should_group = $this->shouldJoinAudits();
// TODO: Currently, the audit table is missing a unique key, so we may
// require a GROUP BY if we perform this join. See T1768. This can be
// removed once the table has the key.
if ($this->auditAwaitingUser) {
$should_group = true;
}
if ($should_group) {
return 'GROUP BY commit.id';
} else {
return '';
}
}
public function getQueryApplicationClass() {
return 'PhabricatorDiffusionApplication';
}
}
diff --git a/src/applications/diffusion/query/DiffusionQuery.php b/src/applications/diffusion/query/DiffusionQuery.php
index 24f58583b..a180a61f5 100644
--- a/src/applications/diffusion/query/DiffusionQuery.php
+++ b/src/applications/diffusion/query/DiffusionQuery.php
@@ -1,217 +1,217 @@
<?php
abstract class DiffusionQuery extends PhabricatorQuery {
private $request;
final protected function __construct() {
// <protected>
}
protected static function newQueryObject(
$base_class,
DiffusionRequest $request) {
$repository = $request->getRepository();
$obj = self::initQueryObject($base_class, $repository);
$obj->request = $request;
return $obj;
}
final protected static function initQueryObject(
$base_class,
PhabricatorRepository $repository) {
$map = array(
PhabricatorRepositoryType::REPOSITORY_TYPE_GIT => 'Git',
PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL => 'Mercurial',
PhabricatorRepositoryType::REPOSITORY_TYPE_SVN => 'Svn',
);
$name = idx($map, $repository->getVersionControlSystem());
if (!$name) {
- throw new Exception('Unsupported VCS!');
+ throw new Exception(pht('Unsupported VCS!'));
}
$class = str_replace('Diffusion', 'Diffusion'.$name, $base_class);
$obj = new $class();
return $obj;
}
final protected function getRequest() {
return $this->request;
}
final public static function callConduitWithDiffusionRequest(
PhabricatorUser $user,
DiffusionRequest $drequest,
$method,
array $params = array()) {
$repository = $drequest->getRepository();
$core_params = array(
'callsign' => $repository->getCallsign(),
);
if ($drequest->getBranch() !== null) {
$core_params['branch'] = $drequest->getBranch();
}
// If the method we're calling doesn't actually take some of the implicit
// parameters we derive from the DiffusionRequest, omit them.
$method_object = ConduitAPIMethod::getConduitMethod($method);
$method_params = $method_object->getParamTypes();
foreach ($core_params as $key => $value) {
if (empty($method_params[$key])) {
unset($core_params[$key]);
}
}
$params = $params + $core_params;
$client = $repository->newConduitClient(
$user,
$drequest->getIsClusterRequest());
if (!$client) {
return id(new ConduitCall($method, $params))
->setUser($user)
->execute();
} else {
return $client->callMethodSynchronous($method, $params);
}
}
public function execute() {
return $this->executeQuery();
}
abstract protected function executeQuery();
/* -( Query Utilities )---------------------------------------------------- */
final public static function loadCommitsByIdentifiers(
array $identifiers,
DiffusionRequest $drequest) {
if (!$identifiers) {
return array();
}
$commits = array();
$commit_data = array();
$repository = $drequest->getRepository();
$commits = id(new PhabricatorRepositoryCommit())->loadAllWhere(
'repositoryID = %d AND commitIdentifier IN (%Ls)',
$repository->getID(),
$identifiers);
$commits = mpull($commits, null, 'getCommitIdentifier');
// Build empty commit objects for every commit, so we can show unparsed
// commits in history views (as "Importing") instead of not showing them.
// This makes the process of importing and parsing commits clearer to the
// user.
$commit_list = array();
foreach ($identifiers as $identifier) {
$commit_obj = idx($commits, $identifier);
if (!$commit_obj) {
$commit_obj = new PhabricatorRepositoryCommit();
$commit_obj->setRepositoryID($repository->getID());
$commit_obj->setCommitIdentifier($identifier);
$commit_obj->makeEphemeral();
}
$commit_list[$identifier] = $commit_obj;
}
$commits = $commit_list;
$commit_ids = array_filter(mpull($commits, 'getID'));
if ($commit_ids) {
$commit_data = id(new PhabricatorRepositoryCommitData())->loadAllWhere(
'commitID in (%Ld)',
$commit_ids);
$commit_data = mpull($commit_data, null, 'getCommitID');
}
foreach ($commits as $commit) {
if (!$commit->getID()) {
continue;
}
if (idx($commit_data, $commit->getID())) {
$commit->attachCommitData($commit_data[$commit->getID()]);
}
}
return $commits;
}
final public static function loadHistoryForCommitIdentifiers(
array $identifiers,
DiffusionRequest $drequest) {
if (!$identifiers) {
return array();
}
$repository = $drequest->getRepository();
$commits = self::loadCommitsByIdentifiers($identifiers, $drequest);
if (!$commits) {
return array();
}
$path = $drequest->getPath();
$conn_r = $repository->establishConnection('r');
$path_normal = DiffusionPathIDQuery::normalizePath($path);
$paths = queryfx_all(
$conn_r,
'SELECT id, path FROM %T WHERE pathHash IN (%Ls)',
PhabricatorRepository::TABLE_PATH,
array(md5($path_normal)));
$paths = ipull($paths, 'id', 'path');
$path_id = idx($paths, $path_normal);
$commit_ids = array_filter(mpull($commits, 'getID'));
$path_changes = array();
if ($path_id && $commit_ids) {
$path_changes = queryfx_all(
$conn_r,
'SELECT * FROM %T WHERE commitID IN (%Ld) AND pathID = %d',
PhabricatorRepository::TABLE_PATHCHANGE,
$commit_ids,
$path_id);
$path_changes = ipull($path_changes, null, 'commitID');
}
$history = array();
foreach ($identifiers as $identifier) {
$item = new DiffusionPathChange();
$item->setCommitIdentifier($identifier);
$commit = idx($commits, $identifier);
if ($commit) {
$item->setCommit($commit);
try {
$item->setCommitData($commit->getCommitData());
} catch (Exception $ex) {
// Ignore, commit just doesn't have data.
}
$change = idx($path_changes, $commit->getID());
if ($change) {
$item->setChangeType($change['changeType']);
$item->setFileType($change['fileType']);
}
}
$history[] = $item;
}
return $history;
}
}
diff --git a/src/applications/diffusion/query/filecontent/DiffusionGitFileContentQuery.php b/src/applications/diffusion/query/filecontent/DiffusionGitFileContentQuery.php
index ec0729d45..67c636994 100644
--- a/src/applications/diffusion/query/filecontent/DiffusionGitFileContentQuery.php
+++ b/src/applications/diffusion/query/filecontent/DiffusionGitFileContentQuery.php
@@ -1,56 +1,58 @@
<?php
final class DiffusionGitFileContentQuery extends DiffusionFileContentQuery {
public function getFileContentFuture() {
$drequest = $this->getRequest();
$repository = $drequest->getRepository();
$path = $drequest->getPath();
$commit = $drequest->getCommit();
if ($this->getNeedsBlame()) {
return $repository->getLocalCommandFuture(
'--no-pager blame -c -l --date=short %s -- %s',
$commit,
$path);
} else {
return $repository->getLocalCommandFuture(
'cat-file blob %s:%s',
$commit,
$path);
}
}
protected function executeQueryFromFuture(Future $future) {
list($corpus) = $future->resolvex();
$file_content = new DiffusionFileContent();
$file_content->setCorpus($corpus);
return $file_content;
}
protected function tokenizeLine($line) {
return self::match($line);
}
public static function match($line) {
$m = array();
// sample lines:
//
// d1b4fcdd2a7c8c0f8cbdd01ca839d992135424dc
// ( hzhao 2009-05-01 202)function print();
//
// 8220d5d54f6d5d5552a636576cbe9c35f15b65b2
// (Andrew Gallagher 2010-12-03 324)
// // Add the lines for trailing context
- preg_match('/^\s*?(\S+?)\s*\(\s*(.*?)\s+\d{4}-\d{2}-\d{2}\s+\d+\)(.*)?$/',
- $line, $m);
+ preg_match(
+ '/^\s*?(\S+?)\s*\(\s*(.*?)\s+\d{4}-\d{2}-\d{2}\s+\d+\)(.*)?$/',
+ $line,
+ $m);
$rev_id = $m[1];
$author = $m[2];
$text = idx($m, 3);
return array($rev_id, $author, $text);
}
}
diff --git a/src/applications/diffusion/query/filecontent/DiffusionSvnFileContentQuery.php b/src/applications/diffusion/query/filecontent/DiffusionSvnFileContentQuery.php
index d87db3ec8..be7487969 100644
--- a/src/applications/diffusion/query/filecontent/DiffusionSvnFileContentQuery.php
+++ b/src/applications/diffusion/query/filecontent/DiffusionSvnFileContentQuery.php
@@ -1,55 +1,56 @@
<?php
final class DiffusionSvnFileContentQuery extends DiffusionFileContentQuery {
public function getFileContentFuture() {
$drequest = $this->getRequest();
$repository = $drequest->getRepository();
$path = $drequest->getPath();
$commit = $drequest->getCommit();
return $repository->getRemoteCommandFuture(
'%C %s',
$this->getNeedsBlame() ? 'blame --force' : 'cat',
$repository->getSubversionPathURI($path, $commit));
}
protected function executeQueryFromFuture(Future $future) {
try {
list($corpus) = $future->resolvex();
} catch (CommandException $ex) {
$stderr = $ex->getStdErr();
if (preg_match('/path not found$/', trim($stderr))) {
// TODO: Improve user experience for this. One way to end up here
// is to have the parser behind and look at a file which was recently
// nuked; Diffusion will think it still exists and try to grab content
// at HEAD.
throw new Exception(
- 'Failed to retrieve file content from Subversion. The file may '.
- 'have been recently deleted, or the Diffusion cache may be out of '.
- 'date.');
+ pht(
+ 'Failed to retrieve file content from Subversion. The file may '.
+ 'have been recently deleted, or the Diffusion cache may be out of '.
+ 'date.'));
} else {
throw $ex;
}
}
$file_content = new DiffusionFileContent();
$file_content->setCorpus($corpus);
return $file_content;
}
protected function tokenizeLine($line) {
// sample line:
// 347498 yliu function print();
$m = array();
preg_match('/^\s*(\d+)\s+(\S+)(?: (.*))?$/', $line, $m);
$rev_id = $m[1];
$author = $m[2];
$text = idx($m, 3);
return array($rev_id, $author, $text);
}
}
diff --git a/src/applications/diffusion/query/lowlevel/DiffusionLowLevelCommitQuery.php b/src/applications/diffusion/query/lowlevel/DiffusionLowLevelCommitQuery.php
index 4aaf85d93..43862ce45 100644
--- a/src/applications/diffusion/query/lowlevel/DiffusionLowLevelCommitQuery.php
+++ b/src/applications/diffusion/query/lowlevel/DiffusionLowLevelCommitQuery.php
@@ -1,160 +1,159 @@
<?php
/**
* Populate a @{class:DiffusionCommitRef} with information about a specific
* commit in a repository. This is a low-level query which talks directly to
* the underlying VCS.
*/
final class DiffusionLowLevelCommitQuery
extends DiffusionLowLevelQuery {
private $identifier;
public function withIdentifier($identifier) {
$this->identifier = $identifier;
return $this;
}
protected function executeQuery() {
if (!strlen($this->identifier)) {
- throw new Exception(
- pht('You must provide an identifier with withIdentifier()!'));
+ throw new PhutilInvalidStateException('withIdentifier');
}
$type = $this->getRepository()->getVersionControlSystem();
switch ($type) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
$result = $this->loadGitCommitRef();
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
$result = $this->loadMercurialCommitRef();
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
$result = $this->loadSubversionCommitRef();
break;
default:
throw new Exception(pht('Unsupported repository type "%s"!', $type));
}
return $result;
}
private function loadGitCommitRef() {
$repository = $this->getRepository();
// NOTE: %B was introduced somewhat recently in git's history, so pull
// commit message information with %s and %b instead.
// Even though we pass --encoding here, git doesn't always succeed, so
// we try a little harder, since git *does* tell us what the actual encoding
// is correctly (unless it doesn't; encoding is sometimes empty).
list($info) = $repository->execxLocalCommand(
'log -n 1 --encoding=%s --format=%s %s --',
'UTF-8',
implode(
'%x00',
array('%e', '%cn', '%ce', '%an', '%ae', '%T', '%s%n%n%b')),
$this->identifier);
$parts = explode("\0", $info);
$encoding = array_shift($parts);
foreach ($parts as $key => $part) {
if ($encoding) {
$part = phutil_utf8_convert($part, 'UTF-8', $encoding);
}
$parts[$key] = phutil_utf8ize($part);
if (!strlen($parts[$key])) {
$parts[$key] = null;
}
}
$hashes = array(
id(new DiffusionCommitHash())
->setHashType(ArcanistDifferentialRevisionHash::HASH_GIT_COMMIT)
->setHashValue($this->identifier),
id(new DiffusionCommitHash())
->setHashType(ArcanistDifferentialRevisionHash::HASH_GIT_TREE)
->setHashValue($parts[4]),
);
return id(new DiffusionCommitRef())
->setCommitterName($parts[0])
->setCommitterEmail($parts[1])
->setAuthorName($parts[2])
->setAuthorEmail($parts[3])
->setHashes($hashes)
->setMessage($parts[5]);
}
private function loadMercurialCommitRef() {
$repository = $this->getRepository();
list($stdout) = $repository->execxLocalCommand(
'log --template %s --rev %s',
'{author}\\n{desc}',
hgsprintf('%s', $this->identifier));
list($author, $message) = explode("\n", $stdout, 2);
$author = phutil_utf8ize($author);
$message = phutil_utf8ize($message);
list($author_name, $author_email) = $this->splitUserIdentifier($author);
$hashes = array(
id(new DiffusionCommitHash())
->setHashType(ArcanistDifferentialRevisionHash::HASH_MERCURIAL_COMMIT)
->setHashValue($this->identifier),
);
return id(new DiffusionCommitRef())
->setAuthorName($author_name)
->setAuthorEmail($author_email)
->setMessage($message)
->setHashes($hashes);
}
private function loadSubversionCommitRef() {
$repository = $this->getRepository();
list($xml) = $repository->execxRemoteCommand(
'log --xml --limit 1 %s',
$repository->getSubversionPathURI(null, $this->identifier));
// Subversion may send us back commit messages which won't parse because
// they have non UTF-8 garbage in them. Slam them into valid UTF-8.
$xml = phutil_utf8ize($xml);
$log = new SimpleXMLElement($xml);
$entry = $log->logentry[0];
$author = (string)$entry->author;
$message = (string)$entry->msg;
list($author_name, $author_email) = $this->splitUserIdentifier($author);
// No hashes in Subversion.
$hashes = array();
return id(new DiffusionCommitRef())
->setAuthorName($author_name)
->setAuthorEmail($author_email)
->setMessage($message)
->setHashes($hashes);
}
private function splitUserIdentifier($user) {
$email = new PhutilEmailAddress($user);
if ($email->getDisplayName() || $email->getDomainName()) {
$user_name = $email->getDisplayName();
$user_email = $email->getAddress();
} else {
$user_name = $email->getAddress();
$user_email = null;
}
return array($user_name, $user_email);
}
}
diff --git a/src/applications/diffusion/query/lowlevel/DiffusionLowLevelParentsQuery.php b/src/applications/diffusion/query/lowlevel/DiffusionLowLevelParentsQuery.php
index 2b264bc35..60ee4b5fe 100644
--- a/src/applications/diffusion/query/lowlevel/DiffusionLowLevelParentsQuery.php
+++ b/src/applications/diffusion/query/lowlevel/DiffusionLowLevelParentsQuery.php
@@ -1,84 +1,83 @@
<?php
final class DiffusionLowLevelParentsQuery
extends DiffusionLowLevelQuery {
private $identifier;
public function withIdentifier($identifier) {
$this->identifier = $identifier;
return $this;
}
protected function executeQuery() {
if (!strlen($this->identifier)) {
- throw new Exception(
- pht('You must provide an identifier with withIdentifier()!'));
+ throw new PhutilInvalidStateException('withIdentifier');
}
$type = $this->getRepository()->getVersionControlSystem();
switch ($type) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
$result = $this->loadGitParents();
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
$result = $this->loadMercurialParents();
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
$result = $this->loadSubversionParents();
break;
default:
throw new Exception(pht('Unsupported repository type "%s"!', $type));
}
return $result;
}
private function loadGitParents() {
$repository = $this->getRepository();
list($stdout) = $repository->execxLocalCommand(
'log -n 1 --format=%s %s',
'%P',
$this->identifier);
return preg_split('/\s+/', trim($stdout));
}
private function loadMercurialParents() {
$repository = $this->getRepository();
list($stdout) = $repository->execxLocalCommand(
'log --debug --limit 1 --template={parents} --rev %s',
$this->identifier);
$stdout = PhabricatorRepository::filterMercurialDebugOutput($stdout);
$hashes = preg_split('/\s+/', trim($stdout));
foreach ($hashes as $key => $value) {
// Mercurial parents look like "23:ad9f769d6f786fad9f76d9a" -- we want
// to strip out the local rev part.
list($local, $global) = explode(':', $value);
$hashes[$key] = $global;
// With --debug we get 40-character hashes but also get the "000000..."
// hash for missing parents; ignore it.
if (preg_match('/^0+$/', $global)) {
unset($hashes[$key]);
}
}
return $hashes;
}
private function loadSubversionParents() {
$n = (int)$this->identifier;
if ($n > 1) {
$ids = array($n - 1);
} else {
$ids = array();
}
return $ids;
}
}
diff --git a/src/applications/diffusion/query/lowlevel/DiffusionLowLevelResolveRefsQuery.php b/src/applications/diffusion/query/lowlevel/DiffusionLowLevelResolveRefsQuery.php
index 3926dbf39..53dc6b30d 100644
--- a/src/applications/diffusion/query/lowlevel/DiffusionLowLevelResolveRefsQuery.php
+++ b/src/applications/diffusion/query/lowlevel/DiffusionLowLevelResolveRefsQuery.php
@@ -1,285 +1,298 @@
<?php
/**
* Resolves references (like short commit names, branch names, tag names, etc.)
* into canonical, stable commit identifiers. This query works for all
* repository types.
*
* This query will always resolve refs which can be resolved, but may need to
* perform VCS operations. A faster (but less complete) counterpart query is
* available in @{class:DiffusionCachedResolveRefsQuery}; that query can
* resolve most refs without VCS operations.
*/
final class DiffusionLowLevelResolveRefsQuery
extends DiffusionLowLevelQuery {
private $refs;
private $types;
public function withRefs(array $refs) {
$this->refs = $refs;
return $this;
}
public function withTypes(array $types) {
$this->types = $types;
return $this;
}
protected function executeQuery() {
if (!$this->refs) {
return array();
}
switch ($this->getRepository()->getVersionControlSystem()) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
$result = $this->resolveGitRefs();
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
$result = $this->resolveMercurialRefs();
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
$result = $this->resolveSubversionRefs();
break;
default:
- throw new Exception('Unsupported repository type!');
+ throw new Exception(pht('Unsupported repository type!'));
}
if ($this->types !== null) {
$result = $this->filterRefsByType($result, $this->types);
}
return $result;
}
private function resolveGitRefs() {
$repository = $this->getRepository();
$unresolved = array_fuse($this->refs);
$results = array();
// First, resolve branches and tags.
$ref_map = id(new DiffusionLowLevelGitRefQuery())
->setRepository($repository)
->withIsTag(true)
->withIsOriginBranch(true)
->execute();
$ref_map = mgroup($ref_map, 'getShortName');
$tag_prefix = 'refs/tags/';
foreach ($unresolved as $ref) {
if (empty($ref_map[$ref])) {
continue;
}
foreach ($ref_map[$ref] as $result) {
$fields = $result->getRawFields();
$objectname = idx($fields, 'refname');
if (!strncmp($objectname, $tag_prefix, strlen($tag_prefix))) {
$type = 'tag';
} else {
$type = 'branch';
}
$info = array(
'type' => $type,
'identifier' => $result->getCommitIdentifier(),
);
if ($type == 'tag') {
$alternate = idx($fields, 'objectname');
if ($alternate) {
$info['alternate'] = $alternate;
}
}
$results[$ref][] = $info;
}
unset($unresolved[$ref]);
}
// If we resolved everything, we're done.
if (!$unresolved) {
return $results;
}
// Try to resolve anything else. This stuff either doesn't exist or is
// some ref like "HEAD^^^".
$future = $repository->getLocalCommandFuture('cat-file --batch-check');
$future->write(implode("\n", $unresolved));
list($stdout) = $future->resolvex();
$lines = explode("\n", rtrim($stdout, "\n"));
if (count($lines) !== count($unresolved)) {
- throw new Exception('Unexpected line count from `git cat-file`!');
+ throw new Exception(
+ pht(
+ 'Unexpected line count from `%s`!',
+ 'git cat-file'));
}
$hits = array();
$tags = array();
$lines = array_combine($unresolved, $lines);
foreach ($lines as $ref => $line) {
$parts = explode(' ', $line);
if (count($parts) < 2) {
- throw new Exception("Failed to parse `git cat-file` output: {$line}");
+ throw new Exception(
+ pht(
+ 'Failed to parse `%s` output: %s',
+ 'git cat-file',
+ $line));
}
list($identifier, $type) = $parts;
if ($type == 'missing') {
// This is either an ambiguous reference which resolves to several
// objects, or an invalid reference. For now, always treat it as
// invalid. It would be nice to resolve all possibilities for
// ambiguous references at some point, although the strategy for doing
// so isn't clear to me.
continue;
}
switch ($type) {
case 'commit':
break;
case 'tag':
$tags[] = $identifier;
break;
default:
throw new Exception(
- "Unexpected object type from `git cat-file`: {$line}");
+ pht(
+ 'Unexpected object type from `%s`: %s',
+ 'git cat-file',
+ $line));
}
$hits[] = array(
'ref' => $ref,
'type' => $type,
'identifier' => $identifier,
);
}
$tag_map = array();
if ($tags) {
// If some of the refs were tags, just load every tag in order to figure
// out which commits they map to. This might be somewhat inefficient in
// repositories with a huge number of tags.
$tag_refs = id(new DiffusionLowLevelGitRefQuery())
->setRepository($repository)
->withIsTag(true)
->executeQuery();
foreach ($tag_refs as $tag_ref) {
$tag_map[$tag_ref->getShortName()] = $tag_ref->getCommitIdentifier();
}
}
$results = array();
foreach ($hits as $hit) {
$type = $hit['type'];
$ref = $hit['ref'];
$alternate = null;
if ($type == 'tag') {
$alternate = $identifier;
$identifier = idx($tag_map, $ref);
if (!$identifier) {
- throw new Exception("Failed to look up tag '{$ref}'!");
+ throw new Exception(
+ pht(
+ "Failed to look up tag '%s'!",
+ $ref));
}
}
$result = array(
'type' => $type,
'identifier' => $identifier,
);
if ($alternate !== null) {
$result['alternate'] = $alternate;
}
$results[$ref][] = $result;
}
return $results;
}
private function resolveMercurialRefs() {
$repository = $this->getRepository();
// First, pull all of the branch heads in the repository. Doing this in
// bulk is much faster than querying each individual head if we're
// checking even a small number of refs.
$branches = id(new DiffusionLowLevelMercurialBranchesQuery())
->setRepository($repository)
->executeQuery();
$branches = mgroup($branches, 'getShortName');
$results = array();
$unresolved = $this->refs;
foreach ($unresolved as $key => $ref) {
if (empty($branches[$ref])) {
continue;
}
foreach ($branches[$ref] as $branch) {
$fields = $branch->getRawFields();
$results[$ref][] = array(
'type' => 'branch',
'identifier' => $branch->getCommitIdentifier(),
'closed' => idx($fields, 'closed', false),
);
}
unset($unresolved[$key]);
}
if (!$unresolved) {
return $results;
}
// If we still have unresolved refs (which might be things like "tip"),
// try to resolve them individually.
$futures = array();
foreach ($unresolved as $ref) {
$futures[$ref] = $repository->getLocalCommandFuture(
'log --template=%s --rev %s',
'{node}',
hgsprintf('%s', $ref));
}
foreach (new FutureIterator($futures) as $ref => $future) {
try {
list($stdout) = $future->resolvex();
} catch (CommandException $ex) {
if (preg_match('/ambiguous identifier/', $ex->getStdErr())) {
// This indicates that the ref ambiguously matched several things.
// Eventually, it would be nice to return all of them, but it is
// unclear how to best do that. For now, treat it as a miss instead.
continue;
}
if (preg_match('/unknown revision/', $ex->getStdErr())) {
// No matches for this ref.
continue;
}
throw $ex;
}
// It doesn't look like we can figure out the type (commit/branch/rev)
// from this output very easily. For now, just call everything a commit.
$type = 'commit';
$results[$ref][] = array(
'type' => $type,
'identifier' => trim($stdout),
);
}
return $results;
}
private function resolveSubversionRefs() {
// We don't have any VCS logic for Subversion, so just use the cached
// query.
return id(new DiffusionCachedResolveRefsQuery())
->setRepository($this->getRepository())
->withRefs($this->refs)
->execute();
}
}
diff --git a/src/applications/diffusion/query/pathid/__tests__/DiffusionPathQueryTestCase.php b/src/applications/diffusion/query/pathid/__tests__/DiffusionPathQueryTestCase.php
index b0c618376..a3ab67e60 100644
--- a/src/applications/diffusion/query/pathid/__tests__/DiffusionPathQueryTestCase.php
+++ b/src/applications/diffusion/query/pathid/__tests__/DiffusionPathQueryTestCase.php
@@ -1,42 +1,42 @@
<?php
final class DiffusionPathQueryTestCase extends PhabricatorTestCase {
public function testParentEdgeCases() {
$this->assertEqual(
'/',
DiffusionPathIDQuery::getParentPath('/'),
- 'Parent of /');
+ pht('Parent of %s', '/'));
$this->assertEqual(
'/',
DiffusionPathIDQuery::getParentPath('x.txt'),
- 'Parent of x.txt');
+ pht('Parent of %s', 'x.txt'));
$this->assertEqual(
'/a',
DiffusionPathIDQuery::getParentPath('/a/b'),
- 'Parent of /a/b');
+ pht('Parent of %s', '/a/b'));
$this->assertEqual(
'/a',
DiffusionPathIDQuery::getParentPath('/a///b'),
- 'Parent of /a///b');
+ pht('Parent of %s', '/a///b'));
}
public function testExpandEdgeCases() {
$this->assertEqual(
array('/'),
DiffusionPathIDQuery::expandPathToRoot('/'));
$this->assertEqual(
array('/'),
DiffusionPathIDQuery::expandPathToRoot('//'));
$this->assertEqual(
array('/a/b', '/a', '/'),
DiffusionPathIDQuery::expandPathToRoot('/a/b'));
$this->assertEqual(
array('/a/b', '/a', '/'),
DiffusionPathIDQuery::expandPathToRoot('/a//b'));
$this->assertEqual(
array('/a/b', '/a', '/'),
DiffusionPathIDQuery::expandPathToRoot('a/b'));
}
}
diff --git a/src/applications/diffusion/request/DiffusionGitRequest.php b/src/applications/diffusion/request/DiffusionGitRequest.php
index 48d076150..6e30886ee 100644
--- a/src/applications/diffusion/request/DiffusionGitRequest.php
+++ b/src/applications/diffusion/request/DiffusionGitRequest.php
@@ -1,23 +1,23 @@
<?php
final class DiffusionGitRequest extends DiffusionRequest {
public function supportsBranches() {
return true;
}
protected function isStableCommit($symbol) {
return preg_match('/^[a-f0-9]{40}\z/', $symbol);
}
public function getBranch() {
if ($this->branch) {
return $this->branch;
}
if ($this->repository) {
return $this->repository->getDefaultBranch();
}
- throw new Exception('Unable to determine branch!');
+ throw new Exception(pht('Unable to determine branch!'));
}
}
diff --git a/src/applications/diffusion/request/DiffusionMercurialRequest.php b/src/applications/diffusion/request/DiffusionMercurialRequest.php
index 5966ffaaa..a43cb089d 100644
--- a/src/applications/diffusion/request/DiffusionMercurialRequest.php
+++ b/src/applications/diffusion/request/DiffusionMercurialRequest.php
@@ -1,25 +1,25 @@
<?php
final class DiffusionMercurialRequest extends DiffusionRequest {
public function supportsBranches() {
return true;
}
protected function isStableCommit($symbol) {
return preg_match('/^[a-f0-9]{40}\z/', $symbol);
}
public function getBranch() {
if ($this->branch) {
return $this->branch;
}
if ($this->repository) {
return $this->repository->getDefaultBranch();
}
- throw new Exception('Unable to determine branch!');
+ throw new Exception(pht('Unable to determine branch!'));
}
}
diff --git a/src/applications/diffusion/request/DiffusionRequest.php b/src/applications/diffusion/request/DiffusionRequest.php
index e7296e9cc..2ed8e1f88 100644
--- a/src/applications/diffusion/request/DiffusionRequest.php
+++ b/src/applications/diffusion/request/DiffusionRequest.php
@@ -1,834 +1,855 @@
<?php
/**
* Contains logic to parse Diffusion requests, which have a complicated URI
* structure.
*
* @task new Creating Requests
* @task uri Managing Diffusion URIs
*/
abstract class DiffusionRequest {
protected $callsign;
protected $path;
protected $line;
protected $branch;
protected $lint;
protected $symbolicCommit;
protected $symbolicType;
protected $stableCommit;
protected $repository;
protected $repositoryCommit;
protected $repositoryCommitData;
private $isClusterRequest = false;
private $initFromConduit = true;
private $user;
private $branchObject = false;
private $refAlternatives;
abstract public function supportsBranches();
abstract protected function isStableCommit($symbol);
protected function didInitialize() {
return null;
}
/* -( Creating Requests )-------------------------------------------------- */
/**
* Create a new synthetic request from a parameter dictionary. If you need
* a @{class:DiffusionRequest} object in order to issue a DiffusionQuery, you
* can use this method to build one.
*
* Parameters are:
*
* - `callsign` Repository callsign. Provide this or `repository`.
* - `user` Viewing user. Required if `callsign` is provided.
* - `repository` Repository object. Provide this or `callsign`.
* - `branch` Optional, branch name.
* - `path` Optional, file path.
* - `commit` Optional, commit identifier.
* - `line` Optional, line range.
*
* @param map See documentation.
* @return DiffusionRequest New request object.
* @task new
*/
final public static function newFromDictionary(array $data) {
if (isset($data['repository']) && isset($data['callsign'])) {
throw new Exception(
- "Specify 'repository' or 'callsign', but not both.");
+ pht(
+ "Specify '%s' or '%s', but not both.",
+ 'repository',
+ 'callsign'));
} else if (!isset($data['repository']) && !isset($data['callsign'])) {
throw new Exception(
- "One of 'repository' and 'callsign' is required.");
+ pht(
+ "One of '%s' and '%s' is required.",
+ 'repository',
+ 'callsign'));
} else if (isset($data['callsign']) && empty($data['user'])) {
throw new Exception(
- "Parameter 'user' is required if 'callsign' is provided.");
+ pht(
+ "Parameter '%s' is required if '%s' is provided.",
+ 'user',
+ 'callsign'));
}
if (isset($data['repository'])) {
$object = self::newFromRepository($data['repository']);
} else {
$object = self::newFromCallsign($data['callsign'], $data['user']);
}
$object->initializeFromDictionary($data);
return $object;
}
/**
* Create a new request from an Aphront request dictionary. This is an
* internal method that you generally should not call directly; instead,
* call @{method:newFromDictionary}.
*
* @param map Map of Aphront request data.
* @return DiffusionRequest New request object.
* @task new
*/
final public static function newFromAphrontRequestDictionary(
array $data,
AphrontRequest $request) {
$callsign = phutil_unescape_uri_path_component(idx($data, 'callsign'));
$object = self::newFromCallsign($callsign, $request->getUser());
$use_branches = $object->supportsBranches();
if (isset($data['dblob'])) {
$parsed = self::parseRequestBlob(idx($data, 'dblob'), $use_branches);
} else {
$parsed = array(
'commit' => idx($data, 'commit'),
'path' => idx($data, 'path'),
'line' => idx($data, 'line'),
'branch' => idx($data, 'branch'),
);
}
$object->setUser($request->getUser());
$object->initializeFromDictionary($parsed);
$object->lint = $request->getStr('lint');
return $object;
}
/**
* Internal.
*
* @task new
*/
final private function __construct() {
// <private>
}
/**
* Internal. Use @{method:newFromDictionary}, not this method.
*
* @param string Repository callsign.
* @param PhabricatorUser Viewing user.
* @return DiffusionRequest New request object.
* @task new
*/
final private static function newFromCallsign(
$callsign,
PhabricatorUser $viewer) {
$repository = id(new PhabricatorRepositoryQuery())
->setViewer($viewer)
->withCallsigns(array($callsign))
->executeOne();
if (!$repository) {
- throw new Exception("No such repository '{$callsign}'.");
+ throw new Exception(pht("No such repository '%s'.", $callsign));
}
return self::newFromRepository($repository);
}
/**
* Internal. Use @{method:newFromDictionary}, not this method.
*
* @param PhabricatorRepository Repository object.
* @return DiffusionRequest New request object.
* @task new
*/
final private static function newFromRepository(
PhabricatorRepository $repository) {
$map = array(
PhabricatorRepositoryType::REPOSITORY_TYPE_GIT => 'DiffusionGitRequest',
PhabricatorRepositoryType::REPOSITORY_TYPE_SVN => 'DiffusionSvnRequest',
PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL =>
'DiffusionMercurialRequest',
);
$class = idx($map, $repository->getVersionControlSystem());
if (!$class) {
- throw new Exception('Unknown version control system!');
+ throw new Exception(pht('Unknown version control system!'));
}
$object = new $class();
$object->repository = $repository;
$object->callsign = $repository->getCallsign();
return $object;
}
/**
* Internal. Use @{method:newFromDictionary}, not this method.
*
* @param map Map of parsed data.
* @return void
* @task new
*/
final private function initializeFromDictionary(array $data) {
$this->path = idx($data, 'path');
$this->line = idx($data, 'line');
$this->initFromConduit = idx($data, 'initFromConduit', true);
$this->symbolicCommit = idx($data, 'commit');
if ($this->supportsBranches()) {
$this->branch = idx($data, 'branch');
}
if (!$this->getUser()) {
$user = idx($data, 'user');
if (!$user) {
throw new Exception(
- 'You must provide a PhabricatorUser in the dictionary!');
+ pht(
+ 'You must provide a %s in the dictionary!',
+ 'PhabricatorUser'));
}
$this->setUser($user);
}
$this->didInitialize();
}
final public function setUser(PhabricatorUser $user) {
$this->user = $user;
return $this;
}
final public function getUser() {
return $this->user;
}
public function getRepository() {
return $this->repository;
}
public function getCallsign() {
return $this->callsign;
}
public function setPath($path) {
$this->path = $path;
return $this;
}
public function getPath() {
return $this->path;
}
public function getLine() {
return $this->line;
}
public function getCommit() {
// TODO: Probably remove all of this.
if ($this->getSymbolicCommit() !== null) {
return $this->getSymbolicCommit();
}
return $this->getStableCommit();
}
/**
* Get the symbolic commit associated with this request.
*
* A symbolic commit may be a commit hash, an abbreviated commit hash, a
* branch name, a tag name, or an expression like "HEAD^^^". The symbolic
* commit may also be absent.
*
* This method always returns the symbol present in the original request,
* in unmodified form.
*
* See also @{method:getStableCommit}.
*
* @return string|null Symbolic commit, if one was present in the request.
*/
public function getSymbolicCommit() {
return $this->symbolicCommit;
}
/**
* Modify the request to move the symbolic commit elsewhere.
*
* @param string New symbolic commit.
* @return this
*/
public function updateSymbolicCommit($symbol) {
$this->symbolicCommit = $symbol;
$this->symbolicType = null;
$this->stableCommit = null;
return $this;
}
/**
* Get the ref type (`commit` or `tag`) of the location associated with this
* request.
*
* If a symbolic commit is present in the request, this method identifies
* the type of the symbol. Otherwise, it identifies the type of symbol of
* the location the request is implicitly associated with. This will probably
* always be `commit`.
*
* @return string Symbolic commit type (`commit` or `tag`).
*/
public function getSymbolicType() {
if ($this->symbolicType === null) {
// As a side effect, this resolves the symbolic type.
$this->getStableCommit();
}
return $this->symbolicType;
}
/**
* Retrieve the stable, permanent commit name identifying the repository
* location associated with this request.
*
* This returns a non-symbolic identifier for the current commit: in Git and
* Mercurial, a 40-character SHA1; in SVN, a revision number.
*
* See also @{method:getSymbolicCommit}.
*
* @return string Stable commit name, like a git hash or SVN revision. Not
* a symbolic commit reference.
*/
public function getStableCommit() {
if (!$this->stableCommit) {
if ($this->isStableCommit($this->symbolicCommit)) {
$this->stableCommit = $this->symbolicCommit;
$this->symbolicType = 'commit';
} else {
$this->queryStableCommit();
}
}
return $this->stableCommit;
}
public function getBranch() {
return $this->branch;
}
public function getLint() {
return $this->lint;
}
protected function getArcanistBranch() {
return $this->getBranch();
}
public function loadBranch() {
// TODO: Get rid of this and do real Queries on real objects.
if ($this->branchObject === false) {
$this->branchObject = PhabricatorRepositoryBranch::loadBranch(
$this->getRepository()->getID(),
$this->getArcanistBranch());
}
return $this->branchObject;
}
public function loadCoverage() {
// TODO: This should also die.
$branch = $this->loadBranch();
if (!$branch) {
return;
}
$path = $this->getPath();
$path_map = id(new DiffusionPathIDQuery(array($path)))->loadPathIDs();
$coverage_row = queryfx_one(
id(new PhabricatorRepository())->establishConnection('r'),
'SELECT * FROM %T WHERE branchID = %d AND pathID = %d
ORDER BY commitID DESC LIMIT 1',
'repository_coverage',
$branch->getID(),
$path_map[$path]);
if (!$coverage_row) {
return null;
}
return idx($coverage_row, 'coverage');
}
public function loadCommit() {
if (empty($this->repositoryCommit)) {
$repository = $this->getRepository();
$commit = id(new DiffusionCommitQuery())
->setViewer($this->getUser())
->withRepository($repository)
->withIdentifiers(array($this->getStableCommit()))
->executeOne();
if ($commit) {
$commit->attachRepository($repository);
}
$this->repositoryCommit = $commit;
}
return $this->repositoryCommit;
}
public function loadCommitData() {
if (empty($this->repositoryCommitData)) {
$commit = $this->loadCommit();
$data = id(new PhabricatorRepositoryCommitData())->loadOneWhere(
'commitID = %d',
$commit->getID());
if (!$data) {
$data = new PhabricatorRepositoryCommitData();
$data->setCommitMessage(
- '(This commit has not been fully parsed yet.)');
+ pht('(This commit has not been fully parsed yet.)'));
}
$this->repositoryCommitData = $data;
}
return $this->repositoryCommitData;
}
/* -( Managing Diffusion URIs )-------------------------------------------- */
/**
* Generate a Diffusion URI using this request to provide defaults. See
* @{method:generateDiffusionURI} for details. This method is the same, but
* preserves the request parameters if they are not overridden.
*
* @param map See @{method:generateDiffusionURI}.
* @return PhutilURI Generated URI.
* @task uri
*/
public function generateURI(array $params) {
if (empty($params['stable'])) {
$default_commit = $this->getSymbolicCommit();
} else {
$default_commit = $this->getStableCommit();
}
$defaults = array(
'callsign' => $this->getCallsign(),
'path' => $this->getPath(),
'branch' => $this->getBranch(),
'commit' => $default_commit,
'lint' => idx($params, 'lint', $this->getLint()),
);
foreach ($defaults as $key => $val) {
if (!isset($params[$key])) { // Overwrite NULL.
$params[$key] = $val;
}
}
return self::generateDiffusionURI($params);
}
/**
* Generate a Diffusion URI from a parameter map. Applies the correct encoding
* and formatting to the URI. Parameters are:
*
* - `action` One of `history`, `browse`, `change`, `lastmodified`,
* `branch`, `tags`, `branches`, or `revision-ref`. The action specified
* by the URI.
* - `callsign` Repository callsign.
* - `branch` Optional if action is not `branch`, branch name.
* - `path` Optional, path to file.
* - `commit` Optional, commit identifier.
* - `line` Optional, line range.
* - `lint` Optional, lint code.
* - `params` Optional, query parameters.
*
* The function generates the specified URI and returns it.
*
* @param map See documentation.
* @return PhutilURI Generated URI.
* @task uri
*/
public static function generateDiffusionURI(array $params) {
$action = idx($params, 'action');
$callsign = idx($params, 'callsign');
$path = idx($params, 'path');
$branch = idx($params, 'branch');
$commit = idx($params, 'commit');
$line = idx($params, 'line');
if (strlen($callsign)) {
$callsign = phutil_escape_uri_path_component($callsign).'/';
}
if (strlen($branch)) {
$branch = phutil_escape_uri_path_component($branch).'/';
}
if (strlen($path)) {
$path = ltrim($path, '/');
$path = str_replace(array(';', '$'), array(';;', '$$'), $path);
$path = phutil_escape_uri($path);
}
$path = "{$branch}{$path}";
if (strlen($commit)) {
$commit = str_replace('$', '$$', $commit);
$commit = ';'.phutil_escape_uri($commit);
}
if (strlen($line)) {
$line = '$'.phutil_escape_uri($line);
}
$req_callsign = false;
$req_branch = false;
$req_commit = false;
switch ($action) {
case 'history':
case 'browse':
case 'change':
case 'lastmodified':
case 'tags':
case 'branches':
case 'lint':
case 'refs':
$req_callsign = true;
break;
case 'branch':
$req_callsign = true;
$req_branch = true;
break;
case 'commit':
$req_callsign = true;
$req_commit = true;
break;
}
if ($req_callsign && !strlen($callsign)) {
throw new Exception(
- "Diffusion URI action '{$action}' requires callsign!");
+ pht(
+ "Diffusion URI action '%s' requires callsign!",
+ $action));
}
if ($req_commit && !strlen($commit)) {
throw new Exception(
- "Diffusion URI action '{$action}' requires commit!");
+ pht(
+ "Diffusion URI action '%s' requires commit!",
+ $action));
}
switch ($action) {
case 'change':
case 'history':
case 'browse':
case 'lastmodified':
case 'tags':
case 'branches':
case 'lint':
case 'pathtree':
case 'refs':
$uri = "/diffusion/{$callsign}{$action}/{$path}{$commit}{$line}";
break;
case 'branch':
if (strlen($path)) {
$uri = "/diffusion/{$callsign}repository/{$path}";
} else {
$uri = "/diffusion/{$callsign}";
}
break;
case 'external':
$commit = ltrim($commit, ';');
$uri = "/diffusion/external/{$commit}/";
break;
case 'rendering-ref':
// This isn't a real URI per se, it's passed as a query parameter to
// the ajax changeset stuff but then we parse it back out as though
// it came from a URI.
$uri = rawurldecode("{$path}{$commit}");
break;
case 'commit':
$commit = ltrim($commit, ';');
$callsign = rtrim($callsign, '/');
$uri = "/r{$callsign}{$commit}";
break;
default:
- throw new Exception("Unknown Diffusion URI action '{$action}'!");
+ throw new Exception(pht("Unknown Diffusion URI action '%s'!", $action));
}
if ($action == 'rendering-ref') {
return $uri;
}
$uri = new PhutilURI($uri);
if (isset($params['lint'])) {
$params['params'] = idx($params, 'params', array()) + array(
'lint' => $params['lint'],
);
}
if (idx($params, 'params')) {
$uri->setQueryParams($params['params']);
}
return $uri;
}
/**
* Internal. Public only for unit tests.
*
* Parse the request URI into components.
*
* @param string URI blob.
* @param bool True if this VCS supports branches.
* @return map Parsed URI.
*
* @task uri
*/
public static function parseRequestBlob($blob, $supports_branches) {
$result = array(
'branch' => null,
'path' => null,
'commit' => null,
'line' => null,
);
$matches = null;
if ($supports_branches) {
// Consume the front part of the URI, up to the first "/". This is the
// path-component encoded branch name.
if (preg_match('@^([^/]+)/@', $blob, $matches)) {
$result['branch'] = phutil_unescape_uri_path_component($matches[1]);
$blob = substr($blob, strlen($matches[1]) + 1);
}
}
// Consume the back part of the URI, up to the first "$". Use a negative
// lookbehind to prevent matching '$$'. We double the '$' symbol when
// encoding so that files with names like "money/$100" will survive.
$pattern = '@(?:(?:^|[^$])(?:[$][$])*)[$]([\d-,]+)$@';
if (preg_match($pattern, $blob, $matches)) {
$result['line'] = $matches[1];
$blob = substr($blob, 0, -(strlen($matches[1]) + 1));
}
// We've consumed the line number if it exists, so unescape "$" in the
// rest of the string.
$blob = str_replace('$$', '$', $blob);
// Consume the commit name, stopping on ';;'. We allow any character to
// appear in commits names, as they can sometimes be symbolic names (like
// tag names or refs).
if (preg_match('@(?:(?:^|[^;])(?:;;)*);([^;].*)$@', $blob, $matches)) {
$result['commit'] = $matches[1];
$blob = substr($blob, 0, -(strlen($matches[1]) + 1));
}
// We've consumed the commit if it exists, so unescape ";" in the rest
// of the string.
$blob = str_replace(';;', ';', $blob);
if (strlen($blob)) {
$result['path'] = $blob;
}
$parts = explode('/', $result['path']);
foreach ($parts as $part) {
// Prevent any hyjinx since we're ultimately shipping this to the
// filesystem under a lot of workflows.
if ($part == '..') {
- throw new Exception('Invalid path URI.');
+ throw new Exception(pht('Invalid path URI.'));
}
}
return $result;
}
/**
* Check that the working copy of the repository is present and readable.
*
* @param string Path to the working copy.
*/
protected function validateWorkingCopy($path) {
if (!is_readable(dirname($path))) {
$this->raisePermissionException();
}
if (!Filesystem::pathExists($path)) {
$this->raiseCloneException();
}
}
protected function raisePermissionException() {
$host = php_uname('n');
$callsign = $this->getRepository()->getCallsign();
throw new DiffusionSetupException(
- "The clone of this repository ('{$callsign}') on the local machine ".
- "('{$host}') could not be read. Ensure that the repository is in a ".
- "location where the web server has read permissions.");
+ pht(
+ "The clone of this repository ('%s') on the local machine ('%s') ".
+ "could not be read. Ensure that the repository is in a ".
+ "location where the web server has read permissions.",
+ $callsign,
+ $host));
}
protected function raiseCloneException() {
$host = php_uname('n');
$callsign = $this->getRepository()->getCallsign();
throw new DiffusionSetupException(
- "The working copy for this repository ('{$callsign}') hasn't been ".
- "cloned yet on this machine ('{$host}'). Make sure you've started the ".
- "Phabricator daemons. If this problem persists for longer than a clone ".
- "should take, check the daemon logs (in the Daemon Console) to see if ".
- "there were errors cloning the repository. Consult the 'Diffusion User ".
- "Guide' in the documentation for help setting up repositories.");
+ pht(
+ "The working copy for this repository ('%s') hasn't been cloned yet ".
+ "on this machine ('%s'). Make sure you've started the Phabricator ".
+ "daemons. If this problem persists for longer than a clone should ".
+ "take, check the daemon logs (in the Daemon Console) to see if there ".
+ "were errors cloning the repository. Consult the 'Diffusion User ".
+ "Guide' in the documentation for help setting up repositories.",
+ $callsign,
+ $host));
}
private function queryStableCommit() {
$types = array();
if ($this->symbolicCommit) {
$ref = $this->symbolicCommit;
} else {
if ($this->supportsBranches()) {
$ref = $this->getBranch();
$types = array(
PhabricatorRepositoryRefCursor::TYPE_BRANCH,
);
} else {
$ref = 'HEAD';
}
}
$results = $this->resolveRefs(array($ref), $types);
$matches = idx($results, $ref, array());
if (!$matches) {
$message = pht(
'Ref "%s" does not exist in this repository.',
$ref);
throw id(new DiffusionRefNotFoundException($message))
->setRef($ref);
}
if (count($matches) > 1) {
$match = $this->chooseBestRefMatch($ref, $matches);
} else {
$match = head($matches);
}
$this->stableCommit = $match['identifier'];
$this->symbolicType = $match['type'];
}
public function getRefAlternatives() {
// Make sure we've resolved the reference into a stable commit first.
try {
$this->getStableCommit();
} catch (DiffusionRefNotFoundException $ex) {
// If we have a bad reference, just return the empty set of
// alternatives.
}
return $this->refAlternatives;
}
private function chooseBestRefMatch($ref, array $results) {
// First, filter out less-desirable matches.
$candidates = array();
foreach ($results as $result) {
// Exclude closed heads.
if ($result['type'] == 'branch') {
if (idx($result, 'closed')) {
continue;
}
}
$candidates[] = $result;
}
// If we filtered everything, undo the filtering.
if (!$candidates) {
$candidates = $results;
}
// TODO: Do a better job of selecting the best match?
$match = head($candidates);
// After choosing the best alternative, save all the alternatives so the
// UI can show them to the user.
if (count($candidates) > 1) {
$this->refAlternatives = $candidates;
}
return $match;
}
private function resolveRefs(array $refs, array $types) {
// First, try to resolve refs from fast cache sources.
$cached_query = id(new DiffusionCachedResolveRefsQuery())
->setRepository($this->getRepository())
->withRefs($refs);
if ($types) {
$cached_query->withTypes($types);
}
$cached_results = $cached_query->execute();
// Throw away all the refs we resolved. Hopefully, we'll throw away
// everything here.
foreach ($refs as $key => $ref) {
if (isset($cached_results[$ref])) {
unset($refs[$key]);
}
}
// If we couldn't pull everything out of the cache, execute the underlying
// VCS operation.
if ($refs) {
$vcs_results = DiffusionQuery::callConduitWithDiffusionRequest(
$this->getUser(),
$this,
'diffusion.resolverefs',
array(
'types' => $types,
'refs' => $refs,
));
} else {
$vcs_results = array();
}
return $vcs_results + $cached_results;
}
public function setIsClusterRequest($is_cluster_request) {
$this->isClusterRequest = $is_cluster_request;
return $this;
}
public function getIsClusterRequest() {
return $this->isClusterRequest;
}
}
diff --git a/src/applications/diffusion/request/__tests__/DiffusionURITestCase.php b/src/applications/diffusion/request/__tests__/DiffusionURITestCase.php
index a6c9eeb27..43a2bd340 100644
--- a/src/applications/diffusion/request/__tests__/DiffusionURITestCase.php
+++ b/src/applications/diffusion/request/__tests__/DiffusionURITestCase.php
@@ -1,150 +1,150 @@
<?php
final class DiffusionURITestCase extends PhutilTestCase {
public function testBlobDecode() {
$map = array(
// This is a basic blob.
'branch/path.ext;abc$3' => array(
'branch' => 'branch',
'path' => 'path.ext',
'commit' => 'abc',
'line' => '3',
),
'branch/path.ext$3' => array(
'branch' => 'branch',
'path' => 'path.ext',
'line' => '3',
),
'branch/money;;/$$100' => array(
'branch' => 'branch',
'path' => 'money;/$100',
),
'a%252Fb/' => array(
'branch' => 'a/b',
),
'branch/path/;Version-1_0_0' => array(
'branch' => 'branch',
'path' => 'path/',
'commit' => 'Version-1_0_0',
),
'branch/path/;$$moneytag$$' => array(
'branch' => 'branch',
'path' => 'path/',
'commit' => '$moneytag$',
),
'branch/path/semicolon;;;;;$$;;semicolon;;$$$$$100' => array(
'branch' => 'branch',
'path' => 'path/semicolon;;',
'commit' => '$;;semicolon;;$$',
'line' => '100',
),
'branch/path.ext;abc$3-5,7-12,14' => array(
'branch' => 'branch',
'path' => 'path.ext',
'commit' => 'abc',
'line' => '3-5,7-12,14',
),
);
foreach ($map as $input => $expect) {
// Simulate decode effect of the webserver.
$input = rawurldecode($input);
$expect = $expect + array(
'branch' => null,
'path' => null,
'commit' => null,
'line' => null,
);
$expect = array_select_keys(
$expect,
array('branch', 'path', 'commit', 'line'));
$actual = $this->parseBlob($input);
$this->assertEqual(
$expect,
$actual,
- "Parsing '{$input}'");
+ pht("Parsing '%s'", $input));
}
}
public function testBlobDecodeFail() {
$this->tryTestCaseMap(
array(
'branch/path/../../../secrets/secrets.key' => false,
),
array($this, 'parseBlob'));
}
public function parseBlob($blob) {
return DiffusionRequest::parseRequestBlob(
$blob,
$supports_branches = true);
}
public function testURIGeneration() {
$map = array(
'/diffusion/A/browse/branch/path.ext;abc$1' => array(
'action' => 'browse',
'callsign' => 'A',
'branch' => 'branch',
'path' => 'path.ext',
'commit' => 'abc',
'line' => '1',
),
'/diffusion/A/browse/a%252Fb/path.ext' => array(
'action' => 'browse',
'callsign' => 'A',
'branch' => 'a/b',
'path' => 'path.ext',
),
'/diffusion/A/browse/%2B/%20%21' => array(
'action' => 'browse',
'callsign' => 'A',
'path' => '+/ !',
),
'/diffusion/A/browse/money/%24%24100$2' => array(
'action' => 'browse',
'callsign' => 'A',
'path' => 'money/$100',
'line' => '2',
),
'/diffusion/A/browse/path/to/file.ext?view=things' => array(
'action' => 'browse',
'callsign' => 'A',
'path' => 'path/to/file.ext',
'params' => array(
'view' => 'things',
),
),
'/diffusion/A/repository/master/' => array(
'action' => 'branch',
'callsign' => 'A',
'branch' => 'master',
),
'path/to/file.ext;abc' => array(
'action' => 'rendering-ref',
'path' => 'path/to/file.ext',
'commit' => 'abc',
),
'/diffusion/A/browse/branch/path.ext$3-5%2C7-12%2C14' => array(
'action' => 'browse',
'callsign' => 'A',
'branch' => 'branch',
'path' => 'path.ext',
'line' => '3-5,7-12,14',
),
);
foreach ($map as $expect => $input) {
$actual = DiffusionRequest::generateDiffusionURI($input);
$this->assertEqual(
$expect,
(string)$actual);
}
}
}
diff --git a/src/applications/diffusion/ssh/DiffusionMercurialServeSSHWorkflow.php b/src/applications/diffusion/ssh/DiffusionMercurialServeSSHWorkflow.php
index b6c22b3a7..51ffdeb3b 100644
--- a/src/applications/diffusion/ssh/DiffusionMercurialServeSSHWorkflow.php
+++ b/src/applications/diffusion/ssh/DiffusionMercurialServeSSHWorkflow.php
@@ -1,114 +1,114 @@
<?php
final class DiffusionMercurialServeSSHWorkflow
extends DiffusionMercurialSSHWorkflow {
protected $didSeeWrite;
protected function didConstruct() {
$this->setName('hg');
$this->setArguments(
array(
array(
'name' => 'repository',
'short' => 'R',
'param' => 'repo',
),
array(
'name' => 'stdio',
),
array(
'name' => 'command',
'wildcard' => true,
),
));
}
protected function identifyRepository() {
$args = $this->getArgs();
$path = $args->getArg('repository');
return $this->loadRepositoryWithPath($path);
}
protected function executeRepositoryOperations() {
$repository = $this->getRepository();
$args = $this->getArgs();
if (!$args->getArg('stdio')) {
- throw new Exception('Expected `hg ... --stdio`!');
+ throw new Exception(pht('Expected `%s`!', 'hg ... --stdio'));
}
if ($args->getArg('command') !== array('serve')) {
- throw new Exception('Expected `hg ... serve`!');
+ throw new Exception(pht('Expected `%s`!', 'hg ... serve'));
}
if ($this->shouldProxy()) {
$command = $this->getProxyCommand();
} else {
$command = csprintf(
'hg -R %s serve --stdio',
$repository->getLocalPath());
}
$command = PhabricatorDaemon::sudoCommandAsDaemonUser($command);
$future = id(new ExecFuture('%C', $command))
->setEnv($this->getEnvironment());
$io_channel = $this->getIOChannel();
$protocol_channel = new DiffusionMercurialWireClientSSHProtocolChannel(
$io_channel);
$err = id($this->newPassthruCommand())
->setIOChannel($protocol_channel)
->setCommandChannelFromExecFuture($future)
->setWillWriteCallback(array($this, 'willWriteMessageCallback'))
->execute();
// TODO: It's apparently technically possible to communicate errors to
// Mercurial over SSH by writing a special "\n<error>\n-\n" string. However,
// my attempt to implement that resulted in Mercurial closing the socket and
// then hanging, without showing the error. This might be an issue on our
// side (we need to close our half of the socket?), or maybe the code
// for this in Mercurial doesn't actually work, or maybe something else
// is afoot. At some point, we should look into doing this more cleanly.
// For now, when we, e.g., reject writes for policy reasons, the user will
// see "abort: unexpected response: empty string" after the diagnostically
// useful, e.g., "remote: This repository is read-only over SSH." message.
if (!$err && $this->didSeeWrite) {
$repository->writeStatusMessage(
PhabricatorRepositoryStatusMessage::TYPE_NEEDS_UPDATE,
PhabricatorRepositoryStatusMessage::CODE_OKAY);
}
return $err;
}
public function willWriteMessageCallback(
PhabricatorSSHPassthruCommand $command,
$message) {
$command = $message['command'];
// Check if this is a readonly command.
$is_readonly = false;
if ($command == 'batch') {
$cmds = idx($message['arguments'], 'cmds');
if (DiffusionMercurialWireProtocol::isReadOnlyBatchCommand($cmds)) {
$is_readonly = true;
}
} else if (DiffusionMercurialWireProtocol::isReadOnlyCommand($command)) {
$is_readonly = true;
}
if (!$is_readonly) {
$this->requireWriteAccess();
$this->didSeeWrite = true;
}
// If we're good, return the raw message data.
return $message['raw'];
}
}
diff --git a/src/applications/diffusion/ssh/DiffusionMercurialWireClientSSHProtocolChannel.php b/src/applications/diffusion/ssh/DiffusionMercurialWireClientSSHProtocolChannel.php
index e82a62a25..5150ef935 100644
--- a/src/applications/diffusion/ssh/DiffusionMercurialWireClientSSHProtocolChannel.php
+++ b/src/applications/diffusion/ssh/DiffusionMercurialWireClientSSHProtocolChannel.php
@@ -1,217 +1,217 @@
<?php
final class DiffusionMercurialWireClientSSHProtocolChannel
extends PhutilProtocolChannel {
private $buffer = '';
private $state = 'command';
private $expectArgumentCount;
private $argumentName;
private $expectBytes;
private $command;
private $arguments;
private $raw;
protected function encodeMessage($message) {
return $message;
}
private function initializeState($last_command = null) {
if ($last_command == 'unbundle') {
$this->command = '<raw-data>';
$this->state = 'data-length';
} else {
$this->state = 'command';
}
$this->expectArgumentCount = null;
$this->expectBytes = null;
$this->command = null;
$this->argumentName = null;
$this->arguments = array();
$this->raw = '';
}
private function readProtocolLine() {
$pos = strpos($this->buffer, "\n");
if ($pos === false) {
return null;
}
$line = substr($this->buffer, 0, $pos);
$this->raw .= $line."\n";
$this->buffer = substr($this->buffer, $pos + 1);
return $line;
}
private function readProtocolBytes() {
if (strlen($this->buffer) < $this->expectBytes) {
return null;
}
$bytes = substr($this->buffer, 0, $this->expectBytes);
$this->raw .= $bytes;
$this->buffer = substr($this->buffer, $this->expectBytes);
return $bytes;
}
private function newMessageAndResetState() {
$message = array(
'command' => $this->command,
'arguments' => $this->arguments,
'raw' => $this->raw,
);
$this->initializeState($this->command);
return $message;
}
private function newDataMessage($bytes) {
$message = array(
'command' => '<raw-data>',
'raw' => strlen($bytes)."\n".$bytes,
);
return $message;
}
protected function decodeStream($data) {
$this->buffer .= $data;
$out = array();
$messages = array();
while (true) {
if ($this->state == 'command') {
$this->initializeState();
// We're reading a command. It looks like:
//
// <command>
$line = $this->readProtocolLine();
if ($line === null) {
break;
}
$this->command = $line;
$this->state = 'arguments';
} else if ($this->state == 'arguments') {
// Check if we're still waiting for arguments.
$args = DiffusionMercurialWireProtocol::getCommandArgs($this->command);
$have = array_select_keys($this->arguments, $args);
if (count($have) == count($args)) {
// We have all the arguments. Emit a message and read the next
// command.
$messages[] = $this->newMessageAndResetState();
} else {
// We're still reading arguments. They can either look like:
//
// <name> <length(value)>
// <value>
// ...
//
// ...or like this:
//
// * <count>
// <name1> <length(value1)>
// <value1>
// ...
$line = $this->readProtocolLine();
if ($line === null) {
break;
}
list($arg, $size) = explode(' ', $line, 2);
$size = (int)$size;
if ($arg != '*') {
$this->expectBytes = $size;
$this->argumentName = $arg;
$this->state = 'value';
} else {
$this->arguments['*'] = array();
$this->expectArgumentCount = $size;
$this->state = 'argv';
}
}
} else if ($this->state == 'value' || $this->state == 'argv-value') {
// We're reading the value of an argument. We just need to wait for
// the right number of bytes to show up.
$bytes = $this->readProtocolBytes();
if ($bytes === null) {
break;
}
if ($this->state == 'argv-value') {
$this->arguments['*'][$this->argumentName] = $bytes;
$this->state = 'argv';
} else {
$this->arguments[$this->argumentName] = $bytes;
$this->state = 'arguments';
}
} else if ($this->state == 'argv') {
// We're reading a variable number of arguments. We need to wait for
// the arguments to arrive.
if ($this->expectArgumentCount) {
$line = $this->readProtocolLine();
if ($line === null) {
break;
}
list($arg, $size) = explode(' ', $line, 2);
$size = (int)$size;
$this->expectBytes = $size;
$this->argumentName = $arg;
$this->state = 'argv-value';
$this->expectArgumentCount--;
} else {
$this->state = 'arguments';
}
} else if ($this->state == 'data-length') {
$line = $this->readProtocolLine();
if ($line === null) {
break;
}
$this->expectBytes = (int)$line;
if (!$this->expectBytes) {
$messages[] = $this->newDataMessage('');
$this->initializeState();
} else {
$this->state = 'data-bytes';
}
} else if ($this->state == 'data-bytes') {
$bytes = substr($this->buffer, 0, $this->expectBytes);
$this->buffer = substr($this->buffer, strlen($bytes));
$this->expectBytes -= strlen($bytes);
$messages[] = $this->newDataMessage($bytes);
if (!$this->expectBytes) {
// We've finished reading this chunk, so go read the next chunk.
$this->state = 'data-length';
} else {
// We're waiting for more data, and have read everything available
// to us so far.
break;
}
} else {
- throw new Exception("Bad parser state '{$this->state}'!");
+ throw new Exception(pht("Bad parser state '%s'!", $this->state));
}
}
return $messages;
}
}
diff --git a/src/applications/diffusion/ssh/DiffusionSSHWorkflow.php b/src/applications/diffusion/ssh/DiffusionSSHWorkflow.php
index 5190f8d3d..2b2c82077 100644
--- a/src/applications/diffusion/ssh/DiffusionSSHWorkflow.php
+++ b/src/applications/diffusion/ssh/DiffusionSSHWorkflow.php
@@ -1,232 +1,231 @@
<?php
abstract class DiffusionSSHWorkflow extends PhabricatorSSHWorkflow {
private $args;
private $repository;
private $hasWriteAccess;
private $proxyURI;
public function getRepository() {
if (!$this->repository) {
throw new Exception(pht('Repository is not available yet!'));
}
return $this->repository;
}
private function setRepository(PhabricatorRepository $repository) {
$this->repository = $repository;
return $this;
}
public function getArgs() {
return $this->args;
}
public function getEnvironment() {
$env = array(
DiffusionCommitHookEngine::ENV_USER => $this->getUser()->getUsername(),
DiffusionCommitHookEngine::ENV_REMOTE_PROTOCOL => 'ssh',
);
$ssh_client = getenv('SSH_CLIENT');
if ($ssh_client) {
// This has the format "<ip> <remote-port> <local-port>". Grab the IP.
$remote_address = head(explode(' ', $ssh_client));
$env[DiffusionCommitHookEngine::ENV_REMOTE_ADDRESS] = $remote_address;
}
return $env;
}
/**
* Identify and load the affected repository.
*/
abstract protected function identifyRepository();
abstract protected function executeRepositoryOperations();
protected function writeError($message) {
$this->getErrorChannel()->write($message);
return $this;
}
protected function shouldProxy() {
return (bool)$this->proxyURI;
}
protected function getProxyCommand() {
$uri = new PhutilURI($this->proxyURI);
$username = PhabricatorEnv::getEnvConfig('cluster.instance');
if (!strlen($username)) {
$username = PhabricatorEnv::getEnvConfig('diffusion.ssh-user');
if (!strlen($username)) {
throw new Exception(
pht(
'Unable to determine the username to connect with when trying '.
'to proxy an SSH request within the Phabricator cluster.'));
}
}
$port = $uri->getPort();
$host = $uri->getDomain();
$key_path = AlmanacKeys::getKeyPath('device.key');
if (!Filesystem::pathExists($key_path)) {
throw new Exception(
pht(
'Unable to proxy this SSH request within the cluster: this device '.
'is not registered and has a missing device key (expected to '.
'find key at "%s").',
$key_path));
}
$options = array();
$options[] = '-o';
$options[] = 'StrictHostKeyChecking=no';
$options[] = '-o';
$options[] = 'UserKnownHostsFile=/dev/null';
// This is suppressing "added <address> to the list of known hosts"
// messages, which are confusing and irrelevant when they arise from
// proxied requests. It might also be suppressing lots of useful errors,
// of course. Ideally, we would enforce host keys eventually.
$options[] = '-o';
$options[] = 'LogLevel=quiet';
// NOTE: We prefix the command with "@username", which the far end of the
// connection will parse in order to act as the specified user. This
// behavior is only available to cluster requests signed by a trusted
// device key.
return csprintf(
'ssh %Ls -l %s -i %s -p %s %s -- %s %Ls',
$options,
$username,
$key_path,
$port,
$host,
'@'.$this->getUser()->getUsername(),
$this->getOriginalArguments());
}
final public function execute(PhutilArgumentParser $args) {
$this->args = $args;
$viewer = $this->getUser();
$have_diffusion = PhabricatorApplication::isClassInstalledForViewer(
'PhabricatorDiffusionApplication',
$viewer);
if (!$have_diffusion) {
throw new Exception(
pht(
'You do not have permission to access the Diffusion application, '.
'so you can not interact with repositories over SSH.'));
}
$repository = $this->identifyRepository();
$this->setRepository($repository);
$is_cluster_request = $this->getIsClusterRequest();
$uri = $repository->getAlmanacServiceURI(
$viewer,
$is_cluster_request,
array(
'ssh',
));
if ($uri) {
$this->proxyURI = $uri;
}
try {
return $this->executeRepositoryOperations();
} catch (Exception $ex) {
$this->writeError(get_class($ex).': '.$ex->getMessage());
return 1;
}
}
protected function loadRepositoryWithPath($path) {
$viewer = $this->getUser();
$regex = '@^/?diffusion/(?P<callsign>[A-Z]+)(?:/|\z)@';
$matches = null;
if (!preg_match($regex, $path, $matches)) {
throw new Exception(
pht(
- 'Unrecognized repository path "%s". Expected a path like '.
- '"%s".',
+ 'Unrecognized repository path "%s". Expected a path like "%s".',
$path,
'/diffusion/X/'));
}
$callsign = $matches[1];
$repository = id(new PhabricatorRepositoryQuery())
->setViewer($viewer)
->withCallsigns(array($callsign))
->executeOne();
if (!$repository) {
throw new Exception(
pht('No repository "%s" exists!', $callsign));
}
switch ($repository->getServeOverSSH()) {
case PhabricatorRepository::SERVE_READONLY:
case PhabricatorRepository::SERVE_READWRITE:
// If we have read or read/write access, proceed for now. We will
// check write access when the user actually issues a write command.
break;
case PhabricatorRepository::SERVE_OFF:
default:
throw new Exception(
pht('This repository is not available over SSH.'));
}
return $repository;
}
protected function requireWriteAccess($protocol_command = null) {
if ($this->hasWriteAccess === true) {
return;
}
$repository = $this->getRepository();
$viewer = $this->getUser();
switch ($repository->getServeOverSSH()) {
case PhabricatorRepository::SERVE_READONLY:
if ($protocol_command !== null) {
throw new Exception(
pht(
'This repository is read-only over SSH (tried to execute '.
'protocol command "%s").',
$protocol_command));
} else {
throw new Exception(
pht('This repository is read-only over SSH.'));
}
break;
case PhabricatorRepository::SERVE_READWRITE:
$can_push = PhabricatorPolicyFilter::hasCapability(
$viewer,
$repository,
DiffusionPushCapability::CAPABILITY);
if (!$can_push) {
throw new Exception(
pht('You do not have permission to push to this repository.'));
}
break;
case PhabricatorRepository::SERVE_OFF:
default:
// This shouldn't be reachable because we don't get this far if the
// repository isn't enabled, but kick them out anyway.
throw new Exception(
pht('This repository is not available over SSH.'));
}
$this->hasWriteAccess = true;
return $this->hasWriteAccess;
}
}
diff --git a/src/applications/diffusion/ssh/DiffusionSubversionServeSSHWorkflow.php b/src/applications/diffusion/ssh/DiffusionSubversionServeSSHWorkflow.php
index 4c5fd6740..4d085302c 100644
--- a/src/applications/diffusion/ssh/DiffusionSubversionServeSSHWorkflow.php
+++ b/src/applications/diffusion/ssh/DiffusionSubversionServeSSHWorkflow.php
@@ -1,430 +1,433 @@
<?php
/**
* This protocol has a good spec here:
*
* http://svn.apache.org/repos/asf/subversion/trunk/subversion/libsvn_ra_svn/protocol
*
*/
final class DiffusionSubversionServeSSHWorkflow
extends DiffusionSubversionSSHWorkflow {
private $didSeeWrite;
private $inProtocol;
private $outProtocol;
private $inSeenGreeting;
private $outPhaseCount = 0;
private $internalBaseURI;
private $externalBaseURI;
private $peekBuffer;
private $command;
private function getCommand() {
return $this->command;
}
protected function didConstruct() {
$this->setName('svnserve');
$this->setArguments(
array(
array(
'name' => 'tunnel',
'short' => 't',
),
));
}
protected function identifyRepository() {
// NOTE: In SVN, we need to read the first few protocol frames before we
// can determine which repository the user is trying to access. We're
// going to peek at the data on the wire to identify the repository.
$io_channel = $this->getIOChannel();
// Before the client will send us the first protocol frame, we need to send
// it a connection frame with server capabilities. To figure out the
// correct frame we're going to start `svnserve`, read the frame from it,
// send it to the client, then kill the subprocess.
// TODO: This is pretty inelegant and the protocol frame will change very
// rarely. We could cache it if we can find a reasonable way to dirty the
// cache.
$command = csprintf('svnserve -t');
$command = PhabricatorDaemon::sudoCommandAsDaemonUser($command);
$future = new ExecFuture('%C', $command);
$exec_channel = new PhutilExecChannel($future);
$exec_protocol = new DiffusionSubversionWireProtocol();
while (true) {
PhutilChannel::waitForAny(array($exec_channel));
$exec_channel->update();
$exec_message = $exec_channel->read();
if ($exec_message !== null) {
$messages = $exec_protocol->writeData($exec_message);
if ($messages) {
$message = head($messages);
$raw = $message['raw'];
// Write the greeting frame to the client.
$io_channel->write($raw);
// Kill the subprocess.
$future->resolveKill();
break;
}
}
if (!$exec_channel->isOpenForReading()) {
throw new Exception(
pht(
- 'svnserve subprocess exited before emitting a protocol frame.'));
+ '%s subprocess exited before emitting a protocol frame.',
+ 'svnserve'));
}
}
$io_protocol = new DiffusionSubversionWireProtocol();
while (true) {
PhutilChannel::waitForAny(array($io_channel));
$io_channel->update();
$in_message = $io_channel->read();
if ($in_message !== null) {
$this->peekBuffer .= $in_message;
if (strlen($this->peekBuffer) > (1024 * 1024)) {
throw new Exception(
pht(
'Client transmitted more than 1MB of data without transmitting '.
'a recognizable protocol frame.'));
}
$messages = $io_protocol->writeData($in_message);
if ($messages) {
$message = head($messages);
$struct = $message['structure'];
// This is the:
//
// ( version ( cap1 ... ) url ... )
//
// The `url` allows us to identify the repository.
$uri = $struct[2]['value'];
$path = $this->getPathFromSubversionURI($uri);
return $this->loadRepositoryWithPath($path);
}
}
if (!$io_channel->isOpenForReading()) {
throw new Exception(
pht(
'Client closed connection before sending a complete protocol '.
'frame.'));
}
// If the client has disconnected, kill the subprocess and bail.
if (!$io_channel->isOpenForWriting()) {
throw new Exception(
pht(
'Client closed connection before receiving response.'));
}
}
}
protected function executeRepositoryOperations() {
$repository = $this->getRepository();
$args = $this->getArgs();
if (!$args->getArg('tunnel')) {
- throw new Exception('Expected `svnserve -t`!');
+ throw new Exception(pht('Expected `%s`!', 'svnserve -t'));
}
if ($this->shouldProxy()) {
$command = $this->getProxyCommand();
} else {
$command = csprintf(
'svnserve -t --tunnel-user=%s',
$this->getUser()->getUsername());
}
$command = PhabricatorDaemon::sudoCommandAsDaemonUser($command);
$future = new ExecFuture('%C', $command);
$this->inProtocol = new DiffusionSubversionWireProtocol();
$this->outProtocol = new DiffusionSubversionWireProtocol();
$this->command = id($this->newPassthruCommand())
->setIOChannel($this->getIOChannel())
->setCommandChannelFromExecFuture($future)
->setWillWriteCallback(array($this, 'willWriteMessageCallback'))
->setWillReadCallback(array($this, 'willReadMessageCallback'));
$this->command->setPauseIOReads(true);
$err = $this->command->execute();
if (!$err && $this->didSeeWrite) {
$this->getRepository()->writeStatusMessage(
PhabricatorRepositoryStatusMessage::TYPE_NEEDS_UPDATE,
PhabricatorRepositoryStatusMessage::CODE_OKAY);
}
return $err;
}
public function willWriteMessageCallback(
PhabricatorSSHPassthruCommand $command,
$message) {
$proto = $this->inProtocol;
$messages = $proto->writeData($message);
$result = array();
foreach ($messages as $message) {
$message_raw = $message['raw'];
$struct = $message['structure'];
if (!$this->inSeenGreeting) {
$this->inSeenGreeting = true;
// The first message the client sends looks like:
//
// ( version ( cap1 ... ) url ... )
//
// We want to grab the URL, load the repository, make sure it exists and
// is accessible, and then replace it with the location of the
// repository on disk.
$uri = $struct[2]['value'];
$struct[2]['value'] = $this->makeInternalURI($uri);
$message_raw = $proto->serializeStruct($struct);
} else if (isset($struct[0]) && $struct[0]['type'] == 'word') {
if (!$proto->isReadOnlyCommand($struct)) {
$this->didSeeWrite = true;
$this->requireWriteAccess($struct[0]['value']);
}
// Several other commands also pass in URLs. We need to translate
// all of these into the internal representation; this also makes sure
// they're valid and accessible.
switch ($struct[0]['value']) {
case 'reparent':
// ( reparent ( url ) )
$struct[1]['value'][0]['value'] = $this->makeInternalURI(
$struct[1]['value'][0]['value']);
$message_raw = $proto->serializeStruct($struct);
break;
case 'switch':
// ( switch ( ( rev ) target recurse url ... ) )
$struct[1]['value'][3]['value'] = $this->makeInternalURI(
$struct[1]['value'][3]['value']);
$message_raw = $proto->serializeStruct($struct);
break;
case 'diff':
// ( diff ( ( rev ) target recurse ignore-ancestry url ... ) )
$struct[1]['value'][4]['value'] = $this->makeInternalURI(
$struct[1]['value'][4]['value']);
$message_raw = $proto->serializeStruct($struct);
break;
case 'add-file':
case 'add-dir':
// ( add-file ( path dir-token file-token [ copy-path copy-rev ] ) )
// ( add-dir ( path parent child [ copy-path copy-rev ] ) )
if (isset($struct[1]['value'][3]['value'][0]['value'])) {
$copy_from = $struct[1]['value'][3]['value'][0]['value'];
$copy_from = $this->makeInternalURI($copy_from);
$struct[1]['value'][3]['value'][0]['value'] = $copy_from;
}
$message_raw = $proto->serializeStruct($struct);
break;
}
}
$result[] = $message_raw;
}
if (!$result) {
return null;
}
return implode('', $result);
}
public function willReadMessageCallback(
PhabricatorSSHPassthruCommand $command,
$message) {
$proto = $this->outProtocol;
$messages = $proto->writeData($message);
$result = array();
foreach ($messages as $message) {
$message_raw = $message['raw'];
$struct = $message['structure'];
if (isset($struct[0]) && ($struct[0]['type'] == 'word')) {
if ($struct[0]['value'] == 'success') {
switch ($this->outPhaseCount) {
case 0:
// This is the "greeting", which announces capabilities.
// We already sent this when we were figuring out which
// repository this request is for, so we aren't going to send
// it again.
// Instead, we're going to replay the client's response (which
// we also already read).
$command = $this->getCommand();
$command->writeIORead($this->peekBuffer);
$command->setPauseIOReads(false);
$message_raw = null;
break;
case 1:
// This responds to the client greeting, and announces auth.
break;
case 2:
// This responds to auth, which should be trivial over SSH.
break;
case 3:
// This contains the URI of the repository. We need to edit it;
// if it does not match what the client requested it will reject
// the response.
$struct[1]['value'][1]['value'] = $this->makeExternalURI(
$struct[1]['value'][1]['value']);
$message_raw = $proto->serializeStruct($struct);
break;
default:
// We don't care about other protocol frames.
break;
}
$this->outPhaseCount++;
} else if ($struct[0]['value'] == 'failure') {
// Find any error messages which include the internal URI, and
// replace the text with the external URI.
foreach ($struct[1]['value'] as $key => $error) {
$code = $error['value'][0]['value'];
$message = $error['value'][1]['value'];
$message = str_replace(
$this->internalBaseURI,
$this->externalBaseURI,
$message);
// Derp derp derp derp derp. The structure looks like this:
// ( failure ( ( code message ... ) ... ) )
$struct[1]['value'][$key]['value'][1]['value'] = $message;
}
$message_raw = $proto->serializeStruct($struct);
}
}
if ($message_raw !== null) {
$result[] = $message_raw;
}
}
if (!$result) {
return null;
}
return implode('', $result);
}
private function getPathFromSubversionURI($uri_string) {
$uri = new PhutilURI($uri_string);
$proto = $uri->getProtocol();
if ($proto !== 'svn+ssh') {
throw new Exception(
pht(
- 'Protocol for URI "%s" MUST be "svn+ssh".',
- $uri_string));
+ 'Protocol for URI "%s" MUST be "%s".',
+ $uri_string,
+ 'svn+ssh'));
}
$path = $uri->getPath();
// Subversion presumably deals with this, but make sure there's nothing
// sketchy going on with the URI.
if (preg_match('(/\\.\\./)', $path)) {
throw new Exception(
pht(
- 'String "/../" is invalid in path specification "%s".',
+ 'String "%s" is invalid in path specification "%s".',
+ '/../',
$uri_string));
}
$path = $this->normalizeSVNPath($path);
return $path;
}
private function makeInternalURI($uri_string) {
$uri = new PhutilURI($uri_string);
$repository = $this->getRepository();
$path = $this->getPathFromSubversionURI($uri_string);
$path = preg_replace(
'(^/diffusion/[A-Z]+)',
rtrim($repository->getLocalPath(), '/'),
$path);
if (preg_match('(^/diffusion/[A-Z]+/\z)', $path)) {
$path = rtrim($path, '/');
}
// NOTE: We are intentionally NOT removing username information from the
// URI. Subversion retains it over the course of the request and considers
// two repositories with different username identifiers to be distinct and
// incompatible.
$uri->setPath($path);
// If this is happening during the handshake, these are the base URIs for
// the request.
if ($this->externalBaseURI === null) {
$pre = (string)id(clone $uri)->setPath('');
$external_path = '/diffusion/'.$repository->getCallsign();
$external_path = $this->normalizeSVNPath($external_path);
$this->externalBaseURI = $pre.$external_path;
$internal_path = rtrim($repository->getLocalPath(), '/');
$internal_path = $this->normalizeSVNPath($internal_path);
$this->internalBaseURI = $pre.$internal_path;
}
return (string)$uri;
}
private function makeExternalURI($uri) {
$internal = $this->internalBaseURI;
$external = $this->externalBaseURI;
if (strncmp($uri, $internal, strlen($internal)) === 0) {
$uri = $external.substr($uri, strlen($internal));
}
return $uri;
}
private function normalizeSVNPath($path) {
// Subversion normalizes redundant slashes internally, so normalize them
// here as well to make sure things match up.
$path = preg_replace('(/+)', '/', $path);
return $path;
}
}
diff --git a/src/applications/diffusion/ssh/__tests__/DiffusionMercurialWireSSHTestCase.php b/src/applications/diffusion/ssh/__tests__/DiffusionMercurialWireSSHTestCase.php
index 694f4cd11..0154805dd 100644
--- a/src/applications/diffusion/ssh/__tests__/DiffusionMercurialWireSSHTestCase.php
+++ b/src/applications/diffusion/ssh/__tests__/DiffusionMercurialWireSSHTestCase.php
@@ -1,56 +1,56 @@
<?php
final class DiffusionMercurialWireSSHTestCase extends PhabricatorTestCase {
public function testMercurialClientWireProtocolParser() {
$data = dirname(__FILE__).'/hgwiredata/';
$dir = Filesystem::listDirectory($data, $include_hidden = false);
foreach ($dir as $file) {
$raw = Filesystem::readFile($data.$file);
$raw = explode("\n~~~~~~~~~~\n", $raw, 2);
$this->assertEqual(2, count($raw));
$expect = phutil_json_decode($raw[1]);
$this->assertTrue(is_array($expect), $file);
$this->assertParserResult($expect, $raw[0], $file);
}
}
private function assertParserResult(array $expect, $input, $file) {
list($x, $y) = PhutilSocketChannel::newChannelPair();
$xp = new DiffusionMercurialWireClientSSHProtocolChannel($x);
$y->write($input);
$y->flush();
$y->closeWriteChannel();
$messages = array();
for ($ii = 0; $ii < count($expect); $ii++) {
try {
$messages[] = $xp->waitForMessage();
} catch (Exception $ex) {
// This is probably the parser not producing as many messages as
// we expect. Log the exception, but continue to the assertion below
// since that will often be easier to diagnose.
phlog($ex);
break;
}
}
$this->assertEqual($expect, $messages, $file);
// Now, make sure the channel doesn't have *more* messages than we expect.
// Specifically, it should throw when we try to read another message.
$caught = null;
try {
$xp->waitForMessage();
} catch (Exception $ex) {
$caught = $ex;
}
$this->assertTrue(
($caught instanceof Exception),
- "No extra messages for '{$file}'.");
+ pht("No extra messages for '%s'.", $file));
}
}
diff --git a/src/applications/diffusion/view/DiffusionEmptyResultView.php b/src/applications/diffusion/view/DiffusionEmptyResultView.php
index 798fc7cf0..74a131e9d 100644
--- a/src/applications/diffusion/view/DiffusionEmptyResultView.php
+++ b/src/applications/diffusion/view/DiffusionEmptyResultView.php
@@ -1,87 +1,88 @@
<?php
final class DiffusionEmptyResultView extends DiffusionView {
private $browseResultSet;
private $view;
public function setDiffusionBrowseResultSet(DiffusionBrowseResultSet $set) {
$this->browseResultSet = $set;
return $this;
}
public function setView($view) {
$this->view = $view;
return $this;
}
public function render() {
$drequest = $this->getDiffusionRequest();
$commit = $drequest->getCommit();
$callsign = $drequest->getRepository()->getCallsign();
if ($commit) {
$commit = "r{$callsign}{$commit}";
} else {
$commit = 'HEAD';
}
$reason = $this->browseResultSet->getReasonForEmptyResultSet();
switch ($reason) {
case DiffusionBrowseResultSet::REASON_IS_NONEXISTENT:
$title = pht('Path Does Not Exist');
// TODO: Under git, this error message should be more specific. It
// may exist on some other branch.
$body = pht('This path does not exist anywhere.');
$severity = PHUIInfoView::SEVERITY_ERROR;
break;
case DiffusionBrowseResultSet::REASON_IS_EMPTY:
$title = pht('Empty Directory');
$body = pht("This path was an empty directory at %s.\n", $commit);
$severity = PHUIInfoView::SEVERITY_NOTICE;
break;
case DiffusionBrowseResultSet::REASON_IS_DELETED:
$deleted = $this->browseResultSet->getDeletedAtCommit();
$existed = $this->browseResultSet->getExistedAtCommit();
$browse = $this->linkBrowse(
$drequest->getPath(),
array(
'text' => 'existed',
'commit' => $existed,
'params' => array('view' => $this->view),
));
$title = pht('Path Was Deleted');
$body = pht(
'This path does not exist at %s. It was deleted in %s and last %s '.
- 'at %s.',
+ 'at %s.',
$commit,
self::linkCommit($drequest->getRepository(), $deleted),
$browse,
"r{$callsign}{$existed}");
$severity = PHUIInfoView::SEVERITY_WARNING;
break;
case DiffusionBrowseResultSet::REASON_IS_UNTRACKED_PARENT:
$subdir = $drequest->getRepository()->getDetail('svn-subpath');
$title = pht('Directory Not Tracked');
$body =
- pht("This repository is configured to track only one subdirectory ".
- "of the entire repository ('%s'), ".
- "but you aren't looking at something in that subdirectory, so no ".
- "information is available.", $subdir);
+ pht(
+ "This repository is configured to track only one subdirectory ".
+ "of the entire repository ('%s'), but you aren't looking at ".
+ "something in that subdirectory, so no information is available.",
+ $subdir);
$severity = PHUIInfoView::SEVERITY_WARNING;
break;
default:
- throw new Exception("Unknown failure reason: $reason");
+ throw new Exception(pht('Unknown failure reason: %s', $reason));
}
$error_view = new PHUIInfoView();
$error_view->setSeverity($severity);
$error_view->setTitle($title);
$error_view->appendChild(phutil_tag('p', array(), $body));
return $error_view->render();
}
}
diff --git a/src/applications/diffusion/view/DiffusionView.php b/src/applications/diffusion/view/DiffusionView.php
index 44613f05c..0bc51f834 100644
--- a/src/applications/diffusion/view/DiffusionView.php
+++ b/src/applications/diffusion/view/DiffusionView.php
@@ -1,167 +1,170 @@
<?php
abstract class DiffusionView extends AphrontView {
private $diffusionRequest;
final public function setDiffusionRequest(DiffusionRequest $request) {
$this->diffusionRequest = $request;
return $this;
}
final public function getDiffusionRequest() {
return $this->diffusionRequest;
}
- final public function linkChange($change_type, $file_type, $path = null,
- $commit_identifier = null) {
+ final public function linkChange(
+ $change_type,
+ $file_type,
+ $path = null,
+ $commit_identifier = null) {
$text = DifferentialChangeType::getFullNameForChangeType($change_type);
if ($change_type == DifferentialChangeType::TYPE_CHILD) {
// TODO: Don't link COPY_AWAY without a direct change.
return $text;
}
if ($file_type == DifferentialChangeType::FILE_DIRECTORY) {
return $text;
}
$href = $this->getDiffusionRequest()->generateURI(
array(
'action' => 'change',
'path' => $path,
'commit' => $commit_identifier,
));
return phutil_tag(
'a',
array(
'href' => $href,
),
$text);
}
final public function linkHistory($path) {
$href = $this->getDiffusionRequest()->generateURI(
array(
'action' => 'history',
'path' => $path,
));
return phutil_tag(
'a',
array(
'href' => $href,
),
pht('History'));
}
final public function linkBrowse($path, array $details = array()) {
$href = $this->getDiffusionRequest()->generateURI(
$details + array(
'action' => 'browse',
'path' => $path,
));
if (isset($details['text'])) {
$text = $details['text'];
} else {
$text = pht('Browse');
}
return phutil_tag(
'a',
array(
'href' => $href,
),
$text);
}
final public function linkExternal($hash, $uri, $text) {
$href = id(new PhutilURI('/diffusion/external/'))
->setQueryParams(
array(
'uri' => $uri,
'id' => $hash,
));
return phutil_tag(
'a',
array(
'href' => $href,
),
$text);
}
final public static function nameCommit(
PhabricatorRepository $repository,
$commit) {
switch ($repository->getVersionControlSystem()) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
$commit_name = substr($commit, 0, 12);
break;
default:
$commit_name = $commit;
break;
}
$callsign = $repository->getCallsign();
return "r{$callsign}{$commit_name}";
}
final public static function linkCommit(
PhabricatorRepository $repository,
$commit,
$summary = '') {
$commit_name = self::nameCommit($repository, $commit);
$callsign = $repository->getCallsign();
if (strlen($summary)) {
$commit_name .= ': '.$summary;
}
return phutil_tag(
'a',
array(
'href' => "/r{$callsign}{$commit}",
),
$commit_name);
}
final public static function linkRevision($id) {
if (!$id) {
return null;
}
return phutil_tag(
'a',
array(
'href' => "/D{$id}",
),
"D{$id}");
}
final public static function renderName($name) {
$email = new PhutilEmailAddress($name);
if ($email->getDisplayName() && $email->getDomainName()) {
Javelin::initBehavior('phabricator-tooltips', array());
require_celerity_resource('aphront-tooltip-css');
return javelin_tag(
'span',
array(
'sigil' => 'has-tooltip',
'meta' => array(
'tip' => $email->getAddress(),
'align' => 'E',
'size' => 'auto',
),
),
$email->getDisplayName());
}
return hsprintf('%s', $name);
}
}
diff --git a/src/applications/diviner/atom/DivinerAtom.php b/src/applications/diviner/atom/DivinerAtom.php
index ef1a079a4..adeac3680 100644
--- a/src/applications/diviner/atom/DivinerAtom.php
+++ b/src/applications/diviner/atom/DivinerAtom.php
@@ -1,435 +1,435 @@
<?php
final class DivinerAtom {
const TYPE_ARTICLE = 'article';
const TYPE_CLASS = 'class';
const TYPE_FILE = 'file';
const TYPE_FUNCTION = 'function';
const TYPE_INTERFACE = 'interface';
const TYPE_METHOD = 'method';
private $type;
private $name;
private $file;
private $line;
private $hash;
private $contentRaw;
private $length;
private $language;
private $docblockRaw;
private $docblockText;
private $docblockMeta;
private $warnings = array();
private $parent;
private $parentHash;
private $children = array();
private $childHashes = array();
private $context;
private $extends = array();
private $links = array();
private $book;
private $properties = array();
/**
* Returns a sorting key which imposes an unambiguous, stable order on atoms.
*/
public function getSortKey() {
return implode(
"\0",
array(
$this->getBook(),
$this->getType(),
$this->getContext(),
$this->getName(),
$this->getFile(),
sprintf('%08', $this->getLine()),
));
}
public function setBook($book) {
$this->book = $book;
return $this;
}
public function getBook() {
return $this->book;
}
public function setContext($context) {
$this->context = $context;
return $this;
}
public function getContext() {
return $this->context;
}
public static function getAtomSerializationVersion() {
return 2;
}
public function addWarning($warning) {
$this->warnings[] = $warning;
return $this;
}
public function getWarnings() {
return $this->warnings;
}
public function setDocblockRaw($docblock_raw) {
$this->docblockRaw = $docblock_raw;
$parser = new PhutilDocblockParser();
list($text, $meta) = $parser->parse($docblock_raw);
$this->docblockText = $text;
$this->docblockMeta = $meta;
return $this;
}
public function getDocblockRaw() {
return $this->docblockRaw;
}
public function getDocblockText() {
if ($this->docblockText === null) {
throw new PhutilInvalidStateException('setDocblockRaw');
}
return $this->docblockText;
}
public function getDocblockMeta() {
if ($this->docblockMeta === null) {
throw new PhutilInvalidStateException('setDocblockRaw');
}
return $this->docblockMeta;
}
public function getDocblockMetaValue($key, $default = null) {
$meta = $this->getDocblockMeta();
return idx($meta, $key, $default);
}
public function setDocblockMetaValue($key, $value) {
$meta = $this->getDocblockMeta();
$meta[$key] = $value;
$this->docblockMeta = $meta;
return $this;
}
public function setType($type) {
$this->type = $type;
return $this;
}
public function getType() {
return $this->type;
}
public function setName($name) {
$this->name = $name;
return $this;
}
public function getName() {
return $this->name;
}
public function setFile($file) {
$this->file = $file;
return $this;
}
public function getFile() {
return $this->file;
}
public function setLine($line) {
$this->line = $line;
return $this;
}
public function getLine() {
return $this->line;
}
public function setContentRaw($content_raw) {
$this->contentRaw = $content_raw;
return $this;
}
public function getContentRaw() {
return $this->contentRaw;
}
public function setHash($hash) {
$this->hash = $hash;
return $this;
}
public function addLink(DivinerAtomRef $ref) {
$this->links[] = $ref;
return $this;
}
public function addExtends(DivinerAtomRef $ref) {
$this->extends[] = $ref;
return $this;
}
public function getLinkDictionaries() {
return mpull($this->links, 'toDictionary');
}
public function getExtendsDictionaries() {
return mpull($this->extends, 'toDictionary');
}
public function getExtends() {
return $this->extends;
}
public function getHash() {
if ($this->hash) {
return $this->hash;
}
$parts = array(
$this->getBook(),
$this->getType(),
$this->getName(),
$this->getFile(),
$this->getLine(),
$this->getLength(),
$this->getLanguage(),
$this->getContentRaw(),
$this->getDocblockRaw(),
$this->getProperties(),
$this->getChildHashes(),
mpull($this->extends, 'toHash'),
mpull($this->links, 'toHash'),
);
$this->hash = md5(serialize($parts)).'N';
return $this->hash;
}
public function setLength($length) {
$this->length = $length;
return $this;
}
public function getLength() {
return $this->length;
}
public function setLanguage($language) {
$this->language = $language;
return $this;
}
public function getLanguage() {
return $this->language;
}
public function addChildHash($child_hash) {
$this->childHashes[] = $child_hash;
return $this;
}
public function getChildHashes() {
if (!$this->childHashes && $this->children) {
$this->childHashes = mpull($this->children, 'getHash');
}
return $this->childHashes;
}
public function setParentHash($parent_hash) {
if ($this->parentHash) {
throw new Exception(pht('Atom already has a parent!'));
}
$this->parentHash = $parent_hash;
return $this;
}
public function hasParent() {
return $this->parent || $this->parentHash;
}
public function setParent(DivinerAtom $atom) {
if ($this->parentHash) {
throw new Exception(pht('Parent hash has already been computed!'));
}
$this->parent = $atom;
return $this;
}
public function getParentHash() {
if ($this->parent && !$this->parentHash) {
$this->parentHash = $this->parent->getHash();
}
return $this->parentHash;
}
public function addChild(DivinerAtom $atom) {
if ($this->childHashes) {
throw new Exception(pht('Child hashes have already been computed!'));
}
$atom->setParent($this);
$this->children[] = $atom;
return $this;
}
public function getURI() {
$parts = array();
$parts[] = phutil_escape_uri_path_component($this->getType());
if ($this->getContext()) {
$parts[] = phutil_escape_uri_path_component($this->getContext());
}
$parts[] = phutil_escape_uri_path_component($this->getName());
$parts[] = null;
return implode('/', $parts);
}
public function toDictionary() {
// NOTE: If you change this format, bump the format version in
// @{method:getAtomSerializationVersion}.
return array(
'book' => $this->getBook(),
'type' => $this->getType(),
'name' => $this->getName(),
'file' => $this->getFile(),
'line' => $this->getLine(),
'hash' => $this->getHash(),
'uri' => $this->getURI(),
'length' => $this->getLength(),
'context' => $this->getContext(),
'language' => $this->getLanguage(),
'docblockRaw' => $this->getDocblockRaw(),
'warnings' => $this->getWarnings(),
'parentHash' => $this->getParentHash(),
'childHashes' => $this->getChildHashes(),
'extends' => $this->getExtendsDictionaries(),
'links' => $this->getLinkDictionaries(),
'ref' => $this->getRef()->toDictionary(),
'properties' => $this->getProperties(),
);
}
public function getRef() {
$title = null;
if ($this->docblockMeta) {
$title = $this->getDocblockMetaValue('title');
}
return id(new DivinerAtomRef())
->setBook($this->getBook())
->setContext($this->getContext())
->setType($this->getType())
->setName($this->getName())
->setTitle($title)
->setGroup($this->getProperty('group'));
}
public static function newFromDictionary(array $dictionary) {
$atom = id(new DivinerAtom())
->setBook(idx($dictionary, 'book'))
->setType(idx($dictionary, 'type'))
->setName(idx($dictionary, 'name'))
->setFile(idx($dictionary, 'file'))
->setLine(idx($dictionary, 'line'))
->setHash(idx($dictionary, 'hash'))
->setLength(idx($dictionary, 'length'))
->setContext(idx($dictionary, 'context'))
->setLanguage(idx($dictionary, 'language'))
->setParentHash(idx($dictionary, 'parentHash'))
->setDocblockRaw(idx($dictionary, 'docblockRaw'))
->setProperties(idx($dictionary, 'properties'));
foreach (idx($dictionary, 'warnings', array()) as $warning) {
$atom->addWarning($warning);
}
foreach (idx($dictionary, 'childHashes', array()) as $child) {
$atom->addChildHash($child);
}
foreach (idx($dictionary, 'extends', array()) as $extends) {
$atom->addExtends(DivinerAtomRef::newFromDictionary($extends));
}
return $atom;
}
public function getProperty($key, $default = null) {
return idx($this->properties, $key, $default);
}
public function setProperty($key, $value) {
$this->properties[$key] = $value;
}
public function getProperties() {
return $this->properties;
}
public function setProperties(array $properties) {
$this->properties = $properties;
return $this;
}
public static function getThisAtomIsNotDocumentedString($type) {
switch ($type) {
case self::TYPE_ARTICLE:
return pht('This article is not documented.');
case self::TYPE_CLASS:
return pht('This class is not documented.');
case self::TYPE_FILE:
return pht('This file is not documented.');
case self::TYPE_FUNCTION:
return pht('This function is not documented.');
case self::TYPE_INTERFACE:
return pht('This interface is not documented.');
case self::TYPE_METHOD:
return pht('This method is not documented.');
default:
- phlog("Need translation for '{$type}'.");
+ phlog(pht("Need translation for '%s'.", $type));
return pht('This %s is not documented.', $type);
}
}
public static function getAllTypes() {
return array(
self::TYPE_ARTICLE,
self::TYPE_CLASS,
self::TYPE_FILE,
self::TYPE_FUNCTION,
self::TYPE_INTERFACE,
self::TYPE_METHOD,
);
}
public static function getAtomTypeNameString($type) {
switch ($type) {
case self::TYPE_ARTICLE:
return pht('Article');
case self::TYPE_CLASS:
return pht('Class');
case self::TYPE_FILE:
return pht('File');
case self::TYPE_FUNCTION:
return pht('Function');
case self::TYPE_INTERFACE:
return pht('Interface');
case self::TYPE_METHOD:
return pht('Method');
default:
- phlog("Need translation for '{$type}'.");
+ phlog(pht("Need translation for '%s'.", $type));
return ucwords($type);
}
}
}
diff --git a/src/applications/diviner/atomizer/DivinerArticleAtomizer.php b/src/applications/diviner/atomizer/DivinerArticleAtomizer.php
index 62f568d3e..e973a4944 100644
--- a/src/applications/diviner/atomizer/DivinerArticleAtomizer.php
+++ b/src/applications/diviner/atomizer/DivinerArticleAtomizer.php
@@ -1,35 +1,35 @@
<?php
final class DivinerArticleAtomizer extends DivinerAtomizer {
protected function executeAtomize($file_name, $file_data) {
$atom = $this->newAtom(DivinerAtom::TYPE_ARTICLE)
->setLine(1)
->setLength(count(explode("\n", $file_data)))
->setLanguage('human');
$block = "/**\n".str_replace("\n", "\n * ", $file_data)."\n */";
$atom->setDocblockRaw($block);
$meta = $atom->getDocblockMeta();
$title = idx($meta, 'title');
if (!strlen($title)) {
$title = pht('Untitled Article "%s"', basename($file_name));
- $atom->addWarning('Article has no @title!');
+ $atom->addWarning(pht('Article has no %s!', '@title'));
$atom->setDocblockMetaValue('title', $title);
}
// If the article has no @name, use the filename after stripping any
// extension.
$name = idx($meta, 'name');
if (!$name) {
$name = basename($file_name);
$name = preg_replace('/\\.[^.]+$/', '', $name);
}
$atom->setName($name);
return array($atom);
}
}
diff --git a/src/applications/doorkeeper/worker/DoorkeeperAsanaFeedWorker.php b/src/applications/doorkeeper/worker/DoorkeeperAsanaFeedWorker.php
index 7cd7af0b3..1d293956b 100644
--- a/src/applications/doorkeeper/worker/DoorkeeperAsanaFeedWorker.php
+++ b/src/applications/doorkeeper/worker/DoorkeeperAsanaFeedWorker.php
@@ -1,699 +1,709 @@
<?php
/**
* Publishes tasks representing work that needs to be done into Asana, and
* updates the tasks as the corresponding Phabricator objects are updated.
*/
final class DoorkeeperAsanaFeedWorker extends DoorkeeperFeedWorker {
private $provider;
/* -( Publishing Stories )------------------------------------------------- */
/**
* This worker is enabled when an Asana workspace ID is configured with
* `asana.workspace-id`.
*/
public function isEnabled() {
return (bool)$this->getWorkspaceID();
}
/**
* Publish stories into Asana using the Asana API.
*/
protected function publishFeedStory() {
$story = $this->getFeedStory();
$data = $story->getStoryData();
$viewer = $this->getViewer();
$provider = $this->getProvider();
$workspace_id = $this->getWorkspaceID();
$object = $this->getStoryObject();
$src_phid = $object->getPHID();
$publisher = $this->getPublisher();
// Figure out all the users related to the object. Users go into one of
// four buckets:
//
// - Owner: the owner of the object. This user becomes the assigned owner
// of the parent task.
// - Active: users who are responsible for the object and need to act on
// it. For example, reviewers of a "needs review" revision.
// - Passive: users who are responsible for the object, but do not need
// to act on it right now. For example, reviewers of a "needs revision"
// revision.
// - Follow: users who are following the object; generally CCs.
$owner_phid = $publisher->getOwnerPHID($object);
$active_phids = $publisher->getActiveUserPHIDs($object);
$passive_phids = $publisher->getPassiveUserPHIDs($object);
$follow_phids = $publisher->getCCUserPHIDs($object);
$all_phids = array();
$all_phids = array_merge(
array($owner_phid),
$active_phids,
$passive_phids,
$follow_phids);
$all_phids = array_unique(array_filter($all_phids));
$phid_aid_map = $this->lookupAsanaUserIDs($all_phids);
if (!$phid_aid_map) {
throw new PhabricatorWorkerPermanentFailureException(
- 'No related users have linked Asana accounts.');
+ pht('No related users have linked Asana accounts.'));
}
$owner_asana_id = idx($phid_aid_map, $owner_phid);
$all_asana_ids = array_select_keys($phid_aid_map, $all_phids);
$all_asana_ids = array_values($all_asana_ids);
// Even if the actor isn't a reviewer, etc., try to use their account so
// we can post in the correct voice. If we miss, we'll try all the other
// related users.
$try_users = array_merge(
array($data->getAuthorPHID()),
array_keys($phid_aid_map));
$try_users = array_filter($try_users);
$access_info = $this->findAnyValidAsanaAccessToken($try_users);
list($possessed_user, $possessed_asana_id, $oauth_token) = $access_info;
if (!$oauth_token) {
throw new PhabricatorWorkerPermanentFailureException(
- 'Unable to find any Asana user with valid credentials to '.
- 'pull an OAuth token out of.');
+ pht(
+ 'Unable to find any Asana user with valid credentials to '.
+ 'pull an OAuth token out of.'));
}
$etype_main = PhabricatorObjectHasAsanaTaskEdgeType::EDGECONST;
$etype_sub = PhabricatorObjectHasAsanaSubtaskEdgeType::EDGECONST;
$equery = id(new PhabricatorEdgeQuery())
->withSourcePHIDs(array($src_phid))
->withEdgeTypes(
array(
$etype_main,
$etype_sub,
))
->needEdgeData(true);
$edges = $equery->execute();
$main_edge = head($edges[$src_phid][$etype_main]);
$main_data = $this->getAsanaTaskData($object) + array(
'assignee' => $owner_asana_id,
);
$projects = $this->getAsanaProjectIDs();
$extra_data = array();
if ($main_edge) {
$extra_data = $main_edge['data'];
$refs = id(new DoorkeeperImportEngine())
->setViewer($possessed_user)
->withPHIDs(array($main_edge['dst']))
->execute();
$parent_ref = head($refs);
if (!$parent_ref) {
throw new PhabricatorWorkerPermanentFailureException(
- 'DoorkeeperExternalObject could not be loaded.');
+ pht('%s could not be loaded.', 'DoorkeeperExternalObject'));
}
if ($parent_ref->getSyncFailed()) {
throw new Exception(
- 'Synchronization of parent task from Asana failed!');
+ pht('Synchronization of parent task from Asana failed!'));
} else if (!$parent_ref->getIsVisible()) {
- $this->log("Skipping main task update, object is no longer visible.\n");
+ $this->log(
+ "%s\n",
+ pht('Skipping main task update, object is no longer visible.'));
$extra_data['gone'] = true;
} else {
$edge_cursor = idx($main_edge['data'], 'cursor', 0);
// TODO: This probably breaks, very rarely, on 32-bit systems.
if ($edge_cursor <= $story->getChronologicalKey()) {
- $this->log("Updating main task.\n");
+ $this->log("%s\n", pht('Updating main task.'));
$task_id = $parent_ref->getObjectID();
$this->makeAsanaAPICall(
$oauth_token,
'tasks/'.$parent_ref->getObjectID(),
'PUT',
$main_data);
} else {
$this->log(
- "Skipping main task update, cursor is ahead of the story.\n");
+ "%s\n",
+ pht('Skipping main task update, cursor is ahead of the story.'));
}
}
} else {
// If there are no followers (CCs), and no active or passive users
// (reviewers or auditors), and we haven't synchronized the object before,
// don't synchronize the object.
if (!$active_phids && !$passive_phids && !$follow_phids) {
- $this->log("Object has no followers or active/passive users.\n");
+ $this->log(
+ "%s\n",
+ pht('Object has no followers or active/passive users.'));
return;
}
$parent = $this->makeAsanaAPICall(
$oauth_token,
'tasks',
'POST',
array(
'workspace' => $workspace_id,
'projects' => $projects,
// NOTE: We initially create parent tasks in the "Later" state but
// don't update it afterward, even if the corresponding object
// becomes actionable. The expectation is that users will prioritize
// tasks in responses to notifications of state changes, and that
// we should not overwrite their choices.
'assignee_status' => 'later',
) + $main_data);
$parent_ref = $this->newRefFromResult(
DoorkeeperBridgeAsana::OBJTYPE_TASK,
$parent);
$extra_data = array(
'workspace' => $workspace_id,
);
}
// Synchronize main task followers.
$task_id = $parent_ref->getObjectID();
// Reviewers are added as followers of the parent task silently, because
// they receive a notification when they are assigned as the owner of their
// subtask, so the follow notification is redundant / non-actionable.
$silent_followers = array_select_keys($phid_aid_map, $active_phids) +
array_select_keys($phid_aid_map, $passive_phids);
$silent_followers = array_values($silent_followers);
// CCs are added as followers of the parent task with normal notifications,
// since they won't get a secondary subtask notification.
$noisy_followers = array_select_keys($phid_aid_map, $follow_phids);
$noisy_followers = array_values($noisy_followers);
// To synchronize follower data, just add all the followers. The task might
// have additional followers, but we can't really tell how they got there:
// were they CC'd and then unsubscribed, or did they manually follow the
// task? Assume the latter since it's easier and less destructive and the
// former is rare. To be fully consistent, we should enumerate followers
// and remove unknown followers, but that's a fair amount of work for little
// benefit, and creates a wider window for race conditions.
// Add the silent followers first so that a user who is both a reviewer and
// a CC gets silently added and then implicitly skipped by then noisy add.
// They will get a subtask notification.
// We only do this if the task still exists.
if (empty($extra_data['gone'])) {
$this->addFollowers($oauth_token, $task_id, $silent_followers, true);
$this->addFollowers($oauth_token, $task_id, $noisy_followers);
// We're also going to synchronize project data here.
$this->addProjects($oauth_token, $task_id, $projects);
}
$dst_phid = $parent_ref->getExternalObject()->getPHID();
// Update the main edge.
$edge_data = array(
'cursor' => $story->getChronologicalKey(),
) + $extra_data;
$edge_options = array(
'data' => $edge_data,
);
id(new PhabricatorEdgeEditor())
->addEdge($src_phid, $etype_main, $dst_phid, $edge_options)
->save();
if (!$parent_ref->getIsVisible()) {
throw new PhabricatorWorkerPermanentFailureException(
- 'DoorkeeperExternalObject has no visible object on the other side; '.
- 'this likely indicates the Asana task has been deleted.');
+ pht(
+ '%s has no visible object on the other side; this '.
+ 'likely indicates the Asana task has been deleted.',
+ 'DoorkeeperExternalObject'));
}
// Now, handle the subtasks.
$sub_editor = new PhabricatorEdgeEditor();
// First, find all the object references in Phabricator for tasks that we
// know about and import their objects from Asana.
$sub_edges = $edges[$src_phid][$etype_sub];
$sub_refs = array();
$subtask_data = $this->getAsanaSubtaskData($object);
$have_phids = array();
if ($sub_edges) {
$refs = id(new DoorkeeperImportEngine())
->setViewer($possessed_user)
->withPHIDs(array_keys($sub_edges))
->execute();
foreach ($refs as $ref) {
if ($ref->getSyncFailed()) {
throw new Exception(
- 'Synchronization of child task from Asana failed!');
+ pht('Synchronization of child task from Asana failed!'));
}
if (!$ref->getIsVisible()) {
$ref->getExternalObject()->delete();
continue;
}
$have_phids[$ref->getExternalObject()->getPHID()] = $ref;
}
}
// Remove any edges in Phabricator which don't have valid tasks in Asana.
// These are likely tasks which have been deleted. We're going to respawn
// them.
foreach ($sub_edges as $sub_phid => $sub_edge) {
if (isset($have_phids[$sub_phid])) {
continue;
}
$this->log(
- "Removing subtask edge to %s, foreign object is not visible.\n",
- $sub_phid);
+ "%s\n",
+ pht(
+ 'Removing subtask edge to %s, foreign object is not visible.',
+ $sub_phid));
$sub_editor->removeEdge($src_phid, $etype_sub, $sub_phid);
unset($sub_edges[$sub_phid]);
}
// For each active or passive user, we're looking for an existing, valid
// task. If we find one we're going to update it; if we don't, we'll
// create one. We ignore extra subtasks that we didn't create (we gain
// nothing by deleting them and might be nuking something important) and
// ignore subtasks which have been moved across workspaces or replanted
// under new parents (this stuff is too edge-casey to bother checking for
// and complicated to fix, as it needs extra API calls). However, we do
// clean up subtasks we created whose owners are no longer associated
// with the object.
$subtask_states = array_fill_keys($active_phids, false) +
array_fill_keys($passive_phids, true);
// Continue with only those users who have Asana credentials.
$subtask_states = array_select_keys(
$subtask_states,
array_keys($phid_aid_map));
$need_subtasks = $subtask_states;
$user_to_ref_map = array();
$nuke_refs = array();
foreach ($sub_edges as $sub_phid => $sub_edge) {
$user_phid = idx($sub_edge['data'], 'userPHID');
if (isset($need_subtasks[$user_phid])) {
unset($need_subtasks[$user_phid]);
$user_to_ref_map[$user_phid] = $have_phids[$sub_phid];
} else {
// This user isn't associated with the object anymore, so get rid
// of their task and edge.
$nuke_refs[$sub_phid] = $have_phids[$sub_phid];
}
}
// These are tasks we know about but which are no longer relevant -- for
// example, because a user has been removed as a reviewer. Remove them and
// their edges.
foreach ($nuke_refs as $sub_phid => $ref) {
$sub_editor->removeEdge($src_phid, $etype_sub, $sub_phid);
$this->makeAsanaAPICall(
$oauth_token,
'tasks/'.$ref->getObjectID(),
'DELETE',
array());
$ref->getExternalObject()->delete();
}
// For each user that we don't have a subtask for, create a new subtask.
foreach ($need_subtasks as $user_phid => $is_completed) {
$subtask = $this->makeAsanaAPICall(
$oauth_token,
'tasks',
'POST',
$subtask_data + array(
'assignee' => $phid_aid_map[$user_phid],
'completed' => $is_completed,
'parent' => $parent_ref->getObjectID(),
));
$subtask_ref = $this->newRefFromResult(
DoorkeeperBridgeAsana::OBJTYPE_TASK,
$subtask);
$user_to_ref_map[$user_phid] = $subtask_ref;
// We don't need to synchronize this subtask's state because we just
// set it when we created it.
unset($subtask_states[$user_phid]);
// Add an edge to track this subtask.
$sub_editor->addEdge(
$src_phid,
$etype_sub,
$subtask_ref->getExternalObject()->getPHID(),
array(
'data' => array(
'userPHID' => $user_phid,
),
));
}
// Synchronize all the previously-existing subtasks.
foreach ($subtask_states as $user_phid => $is_completed) {
$this->makeAsanaAPICall(
$oauth_token,
'tasks/'.$user_to_ref_map[$user_phid]->getObjectID(),
'PUT',
$subtask_data + array(
'assignee' => $phid_aid_map[$user_phid],
'completed' => $is_completed,
));
}
foreach ($user_to_ref_map as $user_phid => $ref) {
// For each subtask, if the acting user isn't the same user as the subtask
// owner, remove the acting user as a follower. Currently, the acting user
// will be added as a follower only when they create the task, but this
// may change in the future (e.g., closing the task may also mark them
// as a follower). Wipe every subtask to be sure. The intent here is to
// leave only the owner as a follower so that the acting user doesn't
// receive notifications about changes to subtask state. Note that
// removing followers is silent in all cases in Asana and never produces
// any kind of notification, so this isn't self-defeating.
if ($user_phid != $possessed_user->getPHID()) {
$this->makeAsanaAPICall(
$oauth_token,
'tasks/'.$ref->getObjectID().'/removeFollowers',
'POST',
array(
'followers' => array($possessed_asana_id),
));
}
}
// Update edges on our side.
$sub_editor->save();
// Don't publish the "create" story, since pushing the object into Asana
// naturally generates a notification which effectively serves the same
// purpose as the "create" story. Similarly, "close" stories generate a
// close notification.
if (!$publisher->isStoryAboutObjectCreation($object) &&
!$publisher->isStoryAboutObjectClosure($object)) {
// Post the feed story itself to the main Asana task. We do this last
// because everything else is idempotent, so this is the only effect we
// can't safely run more than once.
$text = $publisher
->setRenderWithImpliedContext(true)
->getStoryText($object);
$this->makeAsanaAPICall(
$oauth_token,
'tasks/'.$parent_ref->getObjectID().'/stories',
'POST',
array(
'text' => $text,
));
}
}
/* -( Internals )---------------------------------------------------------- */
private function getWorkspaceID() {
return PhabricatorEnv::getEnvConfig('asana.workspace-id');
}
private function getProvider() {
if (!$this->provider) {
$provider = PhabricatorAsanaAuthProvider::getAsanaProvider();
if (!$provider) {
throw new PhabricatorWorkerPermanentFailureException(
- 'No Asana provider configured.');
+ pht('No Asana provider configured.'));
}
$this->provider = $provider;
}
return $this->provider;
}
private function getAsanaTaskData($object) {
$publisher = $this->getPublisher();
$title = $publisher->getObjectTitle($object);
$uri = $publisher->getObjectURI($object);
$description = $publisher->getObjectDescription($object);
$is_completed = $publisher->isObjectClosed($object);
$notes = array(
$description,
$uri,
$this->getSynchronizationWarning(),
);
$notes = implode("\n\n", $notes);
return array(
'name' => $title,
'notes' => $notes,
'completed' => $is_completed,
);
}
private function getAsanaSubtaskData($object) {
$publisher = $this->getPublisher();
$title = $publisher->getResponsibilityTitle($object);
$uri = $publisher->getObjectURI($object);
$description = $publisher->getObjectDescription($object);
$notes = array(
$description,
$uri,
$this->getSynchronizationWarning(),
);
$notes = implode("\n\n", $notes);
return array(
'name' => $title,
'notes' => $notes,
);
}
private function getSynchronizationWarning() {
- return
+ return pht(
"\xE2\x9A\xA0 DO NOT EDIT THIS TASK \xE2\x9A\xA0\n".
"\xE2\x98\xA0 Your changes will not be reflected in Phabricator.\n".
"\xE2\x98\xA0 Your changes will be destroyed the next time state ".
- "is synchronized.";
+ "is synchronized.");
}
private function lookupAsanaUserIDs($all_phids) {
$phid_map = array();
$all_phids = array_unique(array_filter($all_phids));
if (!$all_phids) {
return $phid_map;
}
$provider = PhabricatorAsanaAuthProvider::getAsanaProvider();
$accounts = id(new PhabricatorExternalAccountQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withUserPHIDs($all_phids)
->withAccountTypes(array($provider->getProviderType()))
->withAccountDomains(array($provider->getProviderDomain()))
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->execute();
foreach ($accounts as $account) {
$phid_map[$account->getUserPHID()] = $account->getAccountID();
}
// Put this back in input order.
$phid_map = array_select_keys($phid_map, $all_phids);
return $phid_map;
}
private function findAnyValidAsanaAccessToken(array $user_phids) {
if (!$user_phids) {
return array(null, null, null);
}
$provider = $this->getProvider();
$viewer = $this->getViewer();
$accounts = id(new PhabricatorExternalAccountQuery())
->setViewer($viewer)
->withUserPHIDs($user_phids)
->withAccountTypes(array($provider->getProviderType()))
->withAccountDomains(array($provider->getProviderDomain()))
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->execute();
// Reorder accounts in the original order.
// TODO: This needs to be adjusted if/when we allow you to link multiple
// accounts.
$accounts = mpull($accounts, null, 'getUserPHID');
$accounts = array_select_keys($accounts, $user_phids);
$workspace_id = $this->getWorkspaceID();
foreach ($accounts as $account) {
// Get a token if possible.
$token = $provider->getOAuthAccessToken($account);
if (!$token) {
continue;
}
// Verify we can actually make a call with the token, and that the user
// has access to the workspace in question.
try {
id(new PhutilAsanaFuture())
->setAccessToken($token)
->setRawAsanaQuery("workspaces/{$workspace_id}")
->resolve();
} catch (Exception $ex) {
// This token didn't make it through; try the next account.
continue;
}
$user = id(new PhabricatorPeopleQuery())
->setViewer($viewer)
->withPHIDs(array($account->getUserPHID()))
->executeOne();
if ($user) {
return array($user, $account->getAccountID(), $token);
}
}
return array(null, null, null);
}
private function makeAsanaAPICall($token, $action, $method, array $params) {
foreach ($params as $key => $value) {
if ($value === null) {
unset($params[$key]);
} else if (is_array($value)) {
unset($params[$key]);
foreach ($value as $skey => $svalue) {
$params[$key.'['.$skey.']'] = $svalue;
}
}
}
return id(new PhutilAsanaFuture())
->setAccessToken($token)
->setMethod($method)
->setRawAsanaQuery($action, $params)
->resolve();
}
private function newRefFromResult($type, $result) {
$ref = id(new DoorkeeperObjectRef())
->setApplicationType(DoorkeeperBridgeAsana::APPTYPE_ASANA)
->setApplicationDomain(DoorkeeperBridgeAsana::APPDOMAIN_ASANA)
->setObjectType($type)
->setObjectID($result['id'])
->setIsVisible(true);
$xobj = $ref->newExternalObject();
$ref->attachExternalObject($xobj);
$bridge = new DoorkeeperBridgeAsana();
$bridge->fillObjectFromData($xobj, $result);
$xobj->save();
return $ref;
}
private function addFollowers(
$oauth_token,
$task_id,
array $followers,
$silent = false) {
if (!$followers) {
return;
}
$data = array(
'followers' => $followers,
);
// NOTE: This uses a currently-undocumented API feature to suppress the
// follow notifications.
if ($silent) {
$data['silent'] = true;
}
$this->makeAsanaAPICall(
$oauth_token,
"tasks/{$task_id}/addFollowers",
'POST',
$data);
}
private function getAsanaProjectIDs() {
$project_ids = array();
$publisher = $this->getPublisher();
$config = PhabricatorEnv::getEnvConfig('asana.project-ids');
if (is_array($config)) {
$ids = idx($config, get_class($publisher));
if (is_array($ids)) {
foreach ($ids as $id) {
if (is_scalar($id)) {
$project_ids[] = $id;
}
}
}
}
return $project_ids;
}
private function addProjects(
$oauth_token,
$task_id,
array $project_ids) {
foreach ($project_ids as $project_id) {
$data = array('project' => $project_id);
$this->makeAsanaAPICall(
$oauth_token,
"tasks/{$task_id}/addProject",
'POST',
$data);
}
}
}
diff --git a/src/applications/doorkeeper/worker/DoorkeeperFeedWorker.php b/src/applications/doorkeeper/worker/DoorkeeperFeedWorker.php
index a280e85fb..d015b1979 100644
--- a/src/applications/doorkeeper/worker/DoorkeeperFeedWorker.php
+++ b/src/applications/doorkeeper/worker/DoorkeeperFeedWorker.php
@@ -1,201 +1,203 @@
<?php
/**
* Publish events (like comments on a revision) to external objects which are
* linked through Doorkeeper (like a linked JIRA or Asana task).
*
* These workers are invoked by feed infrastructure during normal task queue
* operations. They read feed stories and publish information about them to
* external systems, generally mirroring comments and updates in Phabricator
* into remote systems by making API calls.
*
* @task publish Publishing Stories
* @task context Story Context
* @task internal Internals
*/
abstract class DoorkeeperFeedWorker extends FeedPushWorker {
private $publisher;
private $feedStory;
private $storyObject;
/* -( Publishing Stories )------------------------------------------------- */
/**
* Actually publish the feed story. Subclasses will generally make API calls
* to publish some version of the story into external systems.
*
* @return void
* @task publish
*/
abstract protected function publishFeedStory();
/**
* Enable or disable the worker. Normally, this checks configuration to
* see if Phabricator is linked to applicable external systems.
*
* @return bool True if this worker should try to publish stories.
* @task publish
*/
abstract public function isEnabled();
/* -( Story Context )------------------------------------------------------ */
/**
* Get the @{class:PhabricatorFeedStory} that should be published.
*
* @return PhabricatorFeedStory The story to publish.
* @task context
*/
protected function getFeedStory() {
if (!$this->feedStory) {
$story = $this->loadFeedStory();
$this->feedStory = $story;
}
return $this->feedStory;
}
/**
* Get the viewer for the act of publishing.
*
* NOTE: Publishing currently uses the omnipotent viewer because it depends
* on loading external accounts. Possibly we should tailor this. See T3732.
* Using the actor for most operations might make more sense.
*
* @return PhabricatorUser Viewer.
* @task context
*/
protected function getViewer() {
return PhabricatorUser::getOmnipotentUser();
}
/**
* Get the @{class:DoorkeeperFeedStoryPublisher} which handles this object.
*
* @return DoorkeeperFeedStoryPublisher Object publisher.
* @task context
*/
protected function getPublisher() {
return $this->publisher;
}
/**
* Get the primary object the story is about, like a
* @{class:DifferentialRevision} or @{class:ManiphestTask}.
*
* @return object Object which the story is about.
* @task context
*/
protected function getStoryObject() {
if (!$this->storyObject) {
$story = $this->getFeedStory();
try {
$object = $story->getPrimaryObject();
} catch (Exception $ex) {
throw new PhabricatorWorkerPermanentFailureException(
$ex->getMessage());
}
$this->storyObject = $object;
}
return $this->storyObject;
}
/* -( Internals )---------------------------------------------------------- */
/**
* Load the @{class:DoorkeeperFeedStoryPublisher} which corresponds to this
* object. Publishers provide a common API for pushing object updates into
* foreign systems.
*
* @return DoorkeeperFeedStoryPublisher Publisher for the story's object.
* @task internal
*/
private function loadPublisher() {
$story = $this->getFeedStory();
$viewer = $this->getViewer();
$object = $this->getStoryObject();
$publishers = id(new PhutilSymbolLoader())
->setAncestorClass('DoorkeeperFeedStoryPublisher')
->loadObjects();
foreach ($publishers as $publisher) {
if (!$publisher->canPublishStory($story, $object)) {
continue;
}
$publisher
->setViewer($viewer)
->setFeedStory($story);
$object = $publisher->willPublishStory($object);
$this->storyObject = $object;
$this->publisher = $publisher;
break;
}
return $this->publisher;
}
/* -( Inherited )---------------------------------------------------------- */
/**
* Doorkeeper workers set up some context, then call
* @{method:publishFeedStory}.
*/
final protected function doWork() {
if (PhabricatorEnv::getEnvConfig('phabricator.silent')) {
- $this->log(pht('Phabricator is running in silent mode.'));
+ $this->log("%s\n", pht('Phabricator is running in silent mode.'));
return;
}
if (!$this->isEnabled()) {
- $this->log("Doorkeeper worker '%s' is not enabled.\n", get_class($this));
+ $this->log(
+ "%s\n",
+ pht("Doorkeeper worker '%s' is not enabled.", get_class($this)));
return;
}
$publisher = $this->loadPublisher();
if (!$publisher) {
- $this->log("Story is about an unsupported object type.\n");
+ $this->log("%s\n", pht('Story is about an unsupported object type.'));
return;
} else {
- $this->log("Using publisher '%s'.\n", get_class($publisher));
+ $this->log("%s\n", pht("Using publisher '%s'.", get_class($publisher)));
}
$this->publishFeedStory();
}
/**
* By default, Doorkeeper workers perform a small number of retries with
* exponential backoff. A consideration in this policy is that many of these
* workers are laden with side effects.
*/
public function getMaximumRetryCount() {
return 4;
}
/**
* See @{method:getMaximumRetryCount} for a description of Doorkeeper
* retry defaults.
*/
public function getWaitBeforeRetry(PhabricatorWorkerTask $task) {
$count = $task->getFailureCount();
return (5 * 60) * pow(8, $count);
}
}
diff --git a/src/applications/doorkeeper/worker/DoorkeeperJIRAFeedWorker.php b/src/applications/doorkeeper/worker/DoorkeeperJIRAFeedWorker.php
index 3334a3cd7..7c6f15cf0 100644
--- a/src/applications/doorkeeper/worker/DoorkeeperJIRAFeedWorker.php
+++ b/src/applications/doorkeeper/worker/DoorkeeperJIRAFeedWorker.php
@@ -1,174 +1,182 @@
<?php
/**
* Publishes feed stories into JIRA, using the "JIRA Issues" field to identify
* linked issues.
*/
final class DoorkeeperJIRAFeedWorker extends DoorkeeperFeedWorker {
private $provider;
/* -( Publishing Stories )------------------------------------------------- */
/**
* This worker is enabled when a JIRA authentication provider is active.
*/
public function isEnabled() {
return (bool)PhabricatorJIRAAuthProvider::getJIRAProvider();
}
/**
* Publishes stories into JIRA using the JIRA API.
*/
protected function publishFeedStory() {
$story = $this->getFeedStory();
$viewer = $this->getViewer();
$provider = $this->getProvider();
$object = $this->getStoryObject();
$publisher = $this->getPublisher();
$jira_issue_phids = PhabricatorEdgeQuery::loadDestinationPHIDs(
$object->getPHID(),
PhabricatorJiraIssueHasObjectEdgeType::EDGECONST);
if (!$jira_issue_phids) {
- $this->log("Story is about an object with no linked JIRA issues.\n");
+ $this->log(
+ "%s\n",
+ pht('Story is about an object with no linked JIRA issues.'));
return;
}
$xobjs = id(new DoorkeeperExternalObjectQuery())
->setViewer($viewer)
->withPHIDs($jira_issue_phids)
->execute();
if (!$xobjs) {
- $this->log("Story object has no corresponding external JIRA objects.\n");
+ $this->log(
+ "%s\n",
+ pht('Story object has no corresponding external JIRA objects.'));
return;
}
$try_users = $this->findUsersToPossess();
if (!$try_users) {
- $this->log("No users to act on linked JIRA objects.\n");
+ $this->log(
+ "%s\n",
+ pht('No users to act on linked JIRA objects.'));
return;
}
$story_text = $this->renderStoryText();
$xobjs = mgroup($xobjs, 'getApplicationDomain');
foreach ($xobjs as $domain => $xobj_list) {
$accounts = id(new PhabricatorExternalAccountQuery())
->setViewer($viewer)
->withUserPHIDs($try_users)
->withAccountTypes(array($provider->getProviderType()))
->withAccountDomains(array($domain))
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->execute();
// Reorder accounts in the original order.
// TODO: This needs to be adjusted if/when we allow you to link multiple
// accounts.
$accounts = mpull($accounts, null, 'getUserPHID');
$accounts = array_select_keys($accounts, $try_users);
foreach ($xobj_list as $xobj) {
foreach ($accounts as $account) {
try {
$provider->newJIRAFuture(
$account,
'rest/api/2/issue/'.$xobj->getObjectID().'/comment',
'POST',
array(
'body' => $story_text,
))->resolveJSON();
break;
} catch (HTTPFutureResponseStatus $ex) {
phlog($ex);
$this->log(
- "Failed to update object %s using user %s.\n",
- $xobj->getObjectID(),
- $account->getUserPHID());
+ "%s\n",
+ pht(
+ 'Failed to update object %s using user %s.',
+ $xobj->getObjectID(),
+ $account->getUserPHID()));
}
}
}
}
}
/* -( Internals )---------------------------------------------------------- */
/**
* Get the active JIRA provider.
*
* @return PhabricatorJIRAAuthProvider Active JIRA auth provider.
* @task internal
*/
private function getProvider() {
if (!$this->provider) {
$provider = PhabricatorJIRAAuthProvider::getJIRAProvider();
if (!$provider) {
throw new PhabricatorWorkerPermanentFailureException(
- 'No JIRA provider configured.');
+ pht('No JIRA provider configured.'));
}
$this->provider = $provider;
}
return $this->provider;
}
/**
* Get a list of users to act as when publishing into JIRA.
*
* @return list<phid> Candidate user PHIDs to act as when publishing this
* story.
* @task internal
*/
private function findUsersToPossess() {
$object = $this->getStoryObject();
$publisher = $this->getPublisher();
$data = $this->getFeedStory()->getStoryData();
// Figure out all the users related to the object. Users go into one of
// four buckets. For JIRA integration, we don't care about which bucket
// a user is in, since we just want to publish an update to linked objects.
$owner_phid = $publisher->getOwnerPHID($object);
$active_phids = $publisher->getActiveUserPHIDs($object);
$passive_phids = $publisher->getPassiveUserPHIDs($object);
$follow_phids = $publisher->getCCUserPHIDs($object);
$all_phids = array_merge(
array($owner_phid),
$active_phids,
$passive_phids,
$follow_phids);
$all_phids = array_unique(array_filter($all_phids));
// Even if the actor isn't a reviewer, etc., try to use their account so
// we can post in the correct voice. If we miss, we'll try all the other
// related users.
$try_users = array_merge(
array($data->getAuthorPHID()),
$all_phids);
$try_users = array_filter($try_users);
return $try_users;
}
private function renderStoryText() {
$object = $this->getStoryObject();
$publisher = $this->getPublisher();
$text = $publisher->getStoryText($object);
$uri = $publisher->getObjectURI($object);
return $text."\n\n".$uri;
}
}
diff --git a/src/applications/drydock/blueprint/DrydockBlueprintImplementation.php b/src/applications/drydock/blueprint/DrydockBlueprintImplementation.php
index 223d97454..226815a64 100644
--- a/src/applications/drydock/blueprint/DrydockBlueprintImplementation.php
+++ b/src/applications/drydock/blueprint/DrydockBlueprintImplementation.php
@@ -1,469 +1,476 @@
<?php
/**
* @task lease Lease Acquisition
* @task resource Resource Allocation
* @task log Logging
*/
abstract class DrydockBlueprintImplementation {
private $activeResource;
private $activeLease;
private $instance;
abstract public function getType();
abstract public function getInterface(
DrydockResource $resource,
DrydockLease $lease,
$type);
abstract public function isEnabled();
abstract public function getBlueprintName();
abstract public function getDescription();
public function getBlueprintClass() {
return get_class($this);
}
protected function loadLease($lease_id) {
// TODO: Get rid of this?
$query = id(new DrydockLeaseQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withIDs(array($lease_id))
->execute();
$lease = idx($query, $lease_id);
if (!$lease) {
- throw new Exception("No such lease '{$lease_id}'!");
+ throw new Exception(pht("No such lease '%d'!", $lease_id));
}
return $lease;
}
protected function getInstance() {
if (!$this->instance) {
throw new Exception(
- 'Attach the blueprint instance to the implementation.');
+ pht('Attach the blueprint instance to the implementation.'));
}
return $this->instance;
}
public function attachInstance(DrydockBlueprint $instance) {
$this->instance = $instance;
return $this;
}
public function getFieldSpecifications() {
return array();
}
public function getDetail($key, $default = null) {
return $this->getInstance()->getDetail($key, $default);
}
/* -( Lease Acquisition )-------------------------------------------------- */
/**
* @task lease
*/
final public function filterResource(
DrydockResource $resource,
DrydockLease $lease) {
$scope = $this->pushActiveScope($resource, $lease);
return $this->canAllocateLease($resource, $lease);
}
/**
* Enforce basic checks on lease/resource compatibility. Allows resources to
* reject leases if they are incompatible, even if the resource types match.
*
* For example, if a resource represents a 32-bit host, this method might
* reject leases that need a 64-bit host. If a resource represents a working
* copy of repository "X", this method might reject leases which need a
* working copy of repository "Y". Generally, although the main types of
* a lease and resource may match (e.g., both "host"), it may not actually be
* possible to satisfy the lease with a specific resource.
*
* This method generally should not enforce limits or perform capacity
* checks. Perform those in @{method:shouldAllocateLease} instead. It also
* should not perform actual acquisition of the lease; perform that in
* @{method:executeAcquireLease} instead.
*
* @param DrydockResource Candidiate resource to allocate the lease on.
* @param DrydockLease Pending lease that wants to allocate here.
* @return bool True if the resource and lease are compatible.
* @task lease
*/
abstract protected function canAllocateLease(
DrydockResource $resource,
DrydockLease $lease);
/**
* @task lease
*/
final public function allocateLease(
DrydockResource $resource,
DrydockLease $lease) {
$scope = $this->pushActiveScope($resource, $lease);
- $this->log('Trying to Allocate Lease');
+ $this->log(pht('Trying to Allocate Lease'));
$lease->setStatus(DrydockLeaseStatus::STATUS_ACQUIRING);
$lease->setResourceID($resource->getID());
$lease->attachResource($resource);
$ephemeral_lease = id(clone $lease)->makeEphemeral();
$allocated = false;
$allocation_exception = null;
$resource->openTransaction();
$resource->beginReadLocking();
$resource->reload();
// TODO: Policy stuff.
$other_leases = id(new DrydockLease())->loadAllWhere(
'status IN (%Ld) AND resourceID = %d',
array(
DrydockLeaseStatus::STATUS_ACQUIRING,
DrydockLeaseStatus::STATUS_ACTIVE,
),
$resource->getID());
try {
$allocated = $this->shouldAllocateLease(
$resource,
$ephemeral_lease,
$other_leases);
} catch (Exception $ex) {
$allocation_exception = $ex;
}
if ($allocated) {
$lease->save();
}
$resource->endReadLocking();
if ($allocated) {
$resource->saveTransaction();
$this->log('Allocated Lease');
} else {
$resource->killTransaction();
- $this->log('Failed to Allocate Lease');
+ $this->log(pht('Failed to Allocate Lease'));
}
if ($allocation_exception) {
$this->logException($allocation_exception);
}
return $allocated;
}
/**
* Enforce lease limits on resources. Allows resources to reject leases if
* they would become over-allocated by accepting them.
*
* For example, if a resource represents disk space, this method might check
* how much space the lease is asking for (say, 200MB) and how much space is
* left unallocated on the resource. It could grant the lease (return true)
* if it has enough remaining space (more than 200MB), and reject the lease
* (return false) if it does not (less than 200MB).
*
* A resource might also allow only exclusive leases. In this case it could
* accept a new lease (return true) if there are no active leases, or reject
* the new lease (return false) if there any other leases.
*
* A lock is held on the resource while this method executes to prevent
* multiple processes from allocating leases on the resource simultaneously.
* However, this means you should implement the method as cheaply as possible.
* In particular, do not perform any actual acquisition or setup in this
* method.
*
* If allocation is permitted, the lease will be moved to `ACQUIRING` status
* and @{method:executeAcquireLease} will be called to actually perform
* acquisition.
*
* General compatibility checks unrelated to resource limits and capacity are
* better implemented in @{method:canAllocateLease}, which serves as a
* cheap filter before lock acquisition.
*
* @param DrydockResource Candidate resource to allocate the lease on.
* @param DrydockLease Pending lease that wants to allocate here.
* @param list<DrydockLease> Other allocated and acquired leases on the
* resource. The implementation can inspect them
* to verify it can safely add the new lease.
* @return bool True to allocate the lease on the resource;
* false to reject it.
* @task lease
*/
abstract protected function shouldAllocateLease(
DrydockResource $resource,
DrydockLease $lease,
array $other_leases);
/**
* @task lease
*/
final public function acquireLease(
DrydockResource $resource,
DrydockLease $lease) {
$scope = $this->pushActiveScope($resource, $lease);
- $this->log('Acquiring Lease');
+ $this->log(pht('Acquiring Lease'));
$lease->setStatus(DrydockLeaseStatus::STATUS_ACTIVE);
$lease->setResourceID($resource->getID());
$lease->attachResource($resource);
$ephemeral_lease = id(clone $lease)->makeEphemeral();
try {
$this->executeAcquireLease($resource, $ephemeral_lease);
} catch (Exception $ex) {
$this->logException($ex);
throw $ex;
}
$lease->setAttributes($ephemeral_lease->getAttributes());
$lease->save();
- $this->log('Acquired Lease');
+ $this->log(pht('Acquired Lease'));
}
/**
* Acquire and activate an allocated lease. Allows resources to peform setup
* as leases are brought online.
*
* Following a successful call to @{method:canAllocateLease}, a lease is moved
* to `ACQUIRING` status and this method is called after resource locks are
* released. Nothing is locked while this method executes; the implementation
* is free to perform expensive operations like writing files and directories,
* executing commands, etc.
*
* After this method executes, the lease status is moved to `ACTIVE` and the
* original leasee may access it.
*
* If acquisition fails, throw an exception.
*
* @param DrydockResource Resource to acquire a lease on.
* @param DrydockLease Lease to acquire.
* @return void
*/
abstract protected function executeAcquireLease(
DrydockResource $resource,
DrydockLease $lease);
final public function releaseLease(
DrydockResource $resource,
DrydockLease $lease) {
$scope = $this->pushActiveScope(null, $lease);
$released = false;
$lease->openTransaction();
$lease->beginReadLocking();
$lease->reload();
if ($lease->getStatus() == DrydockLeaseStatus::STATUS_ACTIVE) {
$lease->setStatus(DrydockLeaseStatus::STATUS_RELEASED);
$lease->save();
$released = true;
}
$lease->endReadLocking();
$lease->saveTransaction();
if (!$released) {
- throw new Exception('Unable to release lease: lease not active!');
+ throw new Exception(pht('Unable to release lease: lease not active!'));
}
}
/* -( Resource Allocation )------------------------------------------------ */
public function canAllocateMoreResources(array $pool) {
return true;
}
abstract protected function executeAllocateResource(DrydockLease $lease);
final public function allocateResource(DrydockLease $lease) {
$scope = $this->pushActiveScope(null, $lease);
$this->log(
pht(
"Blueprint '%s': Allocating Resource for '%s'",
$this->getBlueprintClass(),
$lease->getLeaseName()));
try {
$resource = $this->executeAllocateResource($lease);
$this->validateAllocatedResource($resource);
} catch (Exception $ex) {
$this->logException($ex);
throw $ex;
}
return $resource;
}
/* -( Logging )------------------------------------------------------------ */
/**
* @task log
*/
protected function logException(Exception $ex) {
$this->log($ex->getMessage());
}
/**
* @task log
*/
protected function log($message) {
self::writeLog(
$this->activeResource,
$this->activeLease,
$message);
}
/**
* @task log
*/
public static function writeLog(
DrydockResource $resource = null,
DrydockLease $lease = null,
$message = null) {
$log = id(new DrydockLog())
->setEpoch(time())
->setMessage($message);
if ($resource) {
$log->setResourceID($resource->getID());
}
if ($lease) {
$log->setLeaseID($lease->getID());
}
$log->save();
}
public static function getAllBlueprintImplementations() {
static $list = null;
if ($list === null) {
$blueprints = id(new PhutilSymbolLoader())
->setType('class')
->setAncestorClass(__CLASS__)
->setConcreteOnly(true)
->selectAndLoadSymbols();
$list = ipull($blueprints, 'name', 'name');
foreach ($list as $class_name => $ignored) {
$list[$class_name] = newv($class_name, array());
}
}
return $list;
}
public static function getAllBlueprintImplementationsForResource($type) {
static $groups = null;
if ($groups === null) {
$groups = mgroup(self::getAllBlueprintImplementations(), 'getType');
}
return idx($groups, $type, array());
}
public static function getNamedImplementation($class) {
return idx(self::getAllBlueprintImplementations(), $class);
}
protected function newResourceTemplate($name) {
$resource = id(new DrydockResource())
->setBlueprintPHID($this->getInstance()->getPHID())
->setBlueprintClass($this->getBlueprintClass())
->setType($this->getType())
->setStatus(DrydockResourceStatus::STATUS_PENDING)
->setName($name)
->save();
$this->activeResource = $resource;
$this->log(
pht(
"Blueprint '%s': Created New Template",
$this->getBlueprintClass()));
return $resource;
}
/**
* Sanity checks that the blueprint is implemented properly.
*/
private function validateAllocatedResource($resource) {
$blueprint = $this->getBlueprintClass();
if (!($resource instanceof DrydockResource)) {
throw new Exception(
- "Blueprint '{$blueprint}' is not properly implemented: ".
- "executeAllocateResource() must return an object of type ".
- "DrydockResource or throw, but returned something else.");
+ pht(
+ "Blueprint '%s' is not properly implemented: %s must return an ".
+ "object of type %s or throw, but returned something else.",
+ $blueprint,
+ 'executeAllocateResource()',
+ 'DrydockResource'));
}
$current_status = $resource->getStatus();
$req_status = DrydockResourceStatus::STATUS_OPEN;
if ($current_status != $req_status) {
$current_name = DrydockResourceStatus::getNameForStatus($current_status);
$req_name = DrydockResourceStatus::getNameForStatus($req_status);
throw new Exception(
- "Blueprint '{$blueprint}' is not properly implemented: ".
- "executeAllocateResource() must return a DrydockResource with ".
- "status '{$req_name}', but returned one with status ".
- "'{$current_name}'.");
+ pht(
+ "Blueprint '%s' is not properly implemented: %s must return a %s ".
+ "with status '%s', but returned one with status '%s'.",
+ $blueprint,
+ 'executeAllocateResource()',
+ 'DrydockResource',
+ $req_name,
+ $current_name));
}
}
private function pushActiveScope(
DrydockResource $resource = null,
DrydockLease $lease = null) {
if (($this->activeResource !== null) ||
($this->activeLease !== null)) {
- throw new Exception('There is already an active resource or lease!');
+ throw new Exception(pht('There is already an active resource or lease!'));
}
$this->activeResource = $resource;
$this->activeLease = $lease;
return new DrydockBlueprintScopeGuard($this);
}
public function popActiveScope() {
$this->activeResource = null;
$this->activeLease = null;
}
}
diff --git a/src/applications/drydock/blueprint/DrydockPreallocatedHostBlueprintImplementation.php b/src/applications/drydock/blueprint/DrydockPreallocatedHostBlueprintImplementation.php
index e256ed4bb..af6a9e5f2 100644
--- a/src/applications/drydock/blueprint/DrydockPreallocatedHostBlueprintImplementation.php
+++ b/src/applications/drydock/blueprint/DrydockPreallocatedHostBlueprintImplementation.php
@@ -1,126 +1,129 @@
<?php
final class DrydockPreallocatedHostBlueprintImplementation
extends DrydockBlueprintImplementation {
public function isEnabled() {
return true;
}
public function getBlueprintName() {
return pht('Preallocated Remote Hosts');
}
public function getDescription() {
return pht('Allows Drydock to run on specific remote hosts you configure.');
}
public function canAllocateMoreResources(array $pool) {
return false;
}
protected function executeAllocateResource(DrydockLease $lease) {
- throw new Exception("Preallocated hosts can't be dynamically allocated.");
+ throw new Exception(
+ pht("Preallocated hosts can't be dynamically allocated."));
}
protected function canAllocateLease(
DrydockResource $resource,
DrydockLease $lease) {
return
$lease->getAttribute('platform') === $resource->getAttribute('platform');
}
protected function shouldAllocateLease(
DrydockResource $resource,
DrydockLease $lease,
array $other_leases) {
return true;
}
protected function executeAcquireLease(
DrydockResource $resource,
DrydockLease $lease) {
// Because preallocated resources are manually created, we should verify
// we have all the information we need.
PhutilTypeSpec::checkMap(
$resource->getAttributesForTypeSpec(
array('platform', 'host', 'port', 'credential', 'path')),
array(
'platform' => 'string',
'host' => 'string',
'port' => 'string', // Value is a string from the command line
'credential' => 'string',
'path' => 'string',
));
$v_platform = $resource->getAttribute('platform');
$v_path = $resource->getAttribute('path');
// Similar to DrydockLocalHostBlueprint, we create a folder
// on the remote host that the lease can use.
$lease_id = $lease->getID();
// Can't use DIRECTORY_SEPERATOR here because that is relevant to
// the platform we're currently running on, not the platform we are
// remoting to.
$separator = '/';
if ($v_platform === 'windows') {
$separator = '\\';
}
// Clean up the directory path a little.
$base_path = rtrim($v_path, '/');
$base_path = rtrim($base_path, '\\');
$full_path = $base_path.$separator.$lease_id;
$cmd = $lease->getInterface('command');
if ($v_platform !== 'windows') {
$cmd->execx('mkdir %s', $full_path);
} else {
// Windows is terrible. The mkdir command doesn't even support putting
// the path in quotes. IN QUOTES. ARGUHRGHUGHHGG!! Do some terribly
// inaccurate sanity checking since we can't safely escape the path.
if (preg_match('/^[A-Z]\\:\\\\[a-zA-Z0-9\\\\\\ ]/', $full_path) === 0) {
throw new Exception(
- 'Unsafe path detected for Windows platform: "'.$full_path.'".');
+ pht(
+ 'Unsafe path detected for Windows platform: "%s".',
+ $full_path));
}
$cmd->execx('mkdir %C', $full_path);
}
$lease->setAttribute('path', $full_path);
}
public function getType() {
return 'host';
}
public function getInterface(
DrydockResource $resource,
DrydockLease $lease,
$type) {
switch ($type) {
case 'command':
return id(new DrydockSSHCommandInterface())
->setConfiguration(array(
'host' => $resource->getAttribute('host'),
'port' => $resource->getAttribute('port'),
'credential' => $resource->getAttribute('credential'),
'platform' => $resource->getAttribute('platform'),
))
->setWorkingDirectory($lease->getAttribute('path'));
case 'filesystem':
return id(new DrydockSFTPFilesystemInterface())
->setConfiguration(array(
'host' => $resource->getAttribute('host'),
'port' => $resource->getAttribute('port'),
'credential' => $resource->getAttribute('credential'),
));
}
- throw new Exception("No interface of type '{$type}'.");
+ throw new Exception(pht("No interface of type '%s'.", $type));
}
}
diff --git a/src/applications/drydock/blueprint/DrydockWorkingCopyBlueprintImplementation.php b/src/applications/drydock/blueprint/DrydockWorkingCopyBlueprintImplementation.php
index 3f6adb19b..54d2b3b0f 100644
--- a/src/applications/drydock/blueprint/DrydockWorkingCopyBlueprintImplementation.php
+++ b/src/applications/drydock/blueprint/DrydockWorkingCopyBlueprintImplementation.php
@@ -1,114 +1,118 @@
<?php
final class DrydockWorkingCopyBlueprintImplementation
extends DrydockBlueprintImplementation {
public function isEnabled() {
return true;
}
public function getBlueprintName() {
return pht('Working Copy');
}
public function getDescription() {
return pht('Allows Drydock to check out working copies of repositories.');
}
protected function canAllocateLease(
DrydockResource $resource,
DrydockLease $lease) {
$resource_repo = $resource->getAttribute('repositoryID');
$lease_repo = $lease->getAttribute('repositoryID');
return ($resource_repo && $lease_repo && ($resource_repo == $lease_repo));
}
protected function shouldAllocateLease(
DrydockResource $resource,
DrydockLease $lease,
array $other_leases) {
return !$other_leases;
}
protected function executeAllocateResource(DrydockLease $lease) {
$repository_id = $lease->getAttribute('repositoryID');
if (!$repository_id) {
throw new Exception(
- "Lease is missing required 'repositoryID' attribute.");
+ pht(
+ "Lease is missing required '%s' attribute.",
+ 'repositoryID'));
}
$repository = id(new PhabricatorRepositoryQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withIDs(array($repository_id))
->executeOne();
if (!$repository) {
throw new Exception(
- "Repository '{$repository_id}' does not exist!");
+ pht(
+ "Repository '%s' does not exist!",
+ $repository_id));
}
switch ($repository->getVersionControlSystem()) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
break;
default:
- throw new Exception('Unsupported VCS!');
+ throw new Exception(pht('Unsupported VCS!'));
}
// TODO: Policy stuff here too.
$host_lease = id(new DrydockLease())
->setResourceType('host')
->waitUntilActive();
$path = $host_lease->getAttribute('path').$repository->getCallsign();
$this->log(
pht('Cloning %s into %s....', $repository->getCallsign(), $path));
$cmd = $host_lease->getInterface('command');
$cmd->execx(
'git clone --origin origin %P %s',
$repository->getRemoteURIEnvelope(),
$path);
$this->log(pht('Complete.'));
$resource = $this->newResourceTemplate(
'Working Copy ('.$repository->getCallsign().')');
$resource->setStatus(DrydockResourceStatus::STATUS_OPEN);
$resource->setAttribute('lease.host', $host_lease->getID());
$resource->setAttribute('path', $path);
$resource->setAttribute('repositoryID', $repository->getID());
$resource->save();
return $resource;
}
protected function executeAcquireLease(
DrydockResource $resource,
DrydockLease $lease) {
return;
}
public function getType() {
return 'working-copy';
}
public function getInterface(
DrydockResource $resource,
DrydockLease $lease,
$type) {
switch ($type) {
case 'command':
return $this
->loadLease($resource->getAttribute('lease.host'))
->getInterface($type);
}
- throw new Exception("No interface of type '{$type}'.");
+ throw new Exception(pht("No interface of type '%s'.", $type));
}
}
diff --git a/src/applications/drydock/constants/DrydockResourceStatus.php b/src/applications/drydock/constants/DrydockResourceStatus.php
index 7d11d93cb..d653138fa 100644
--- a/src/applications/drydock/constants/DrydockResourceStatus.php
+++ b/src/applications/drydock/constants/DrydockResourceStatus.php
@@ -1,33 +1,33 @@
<?php
final class DrydockResourceStatus extends DrydockConstants {
const STATUS_PENDING = 0;
const STATUS_OPEN = 1;
const STATUS_CLOSED = 2;
const STATUS_BROKEN = 3;
const STATUS_DESTROYED = 4;
public static function getNameForStatus($status) {
$map = array(
self::STATUS_PENDING => pht('Pending'),
self::STATUS_OPEN => pht('Open'),
self::STATUS_CLOSED => pht('Closed'),
self::STATUS_BROKEN => pht('Broken'),
self::STATUS_DESTROYED => pht('Destroyed'),
);
- return idx($map, $status, 'Unknown');
+ return idx($map, $status, pht('Unknown'));
}
public static function getAllStatuses() {
return array(
self::STATUS_PENDING,
self::STATUS_OPEN,
self::STATUS_CLOSED,
self::STATUS_BROKEN,
self::STATUS_DESTROYED,
);
}
}
diff --git a/src/applications/drydock/controller/DrydockConsoleController.php b/src/applications/drydock/controller/DrydockConsoleController.php
index 28647c67a..923e43a44 100644
--- a/src/applications/drydock/controller/DrydockConsoleController.php
+++ b/src/applications/drydock/controller/DrydockConsoleController.php
@@ -1,79 +1,74 @@
<?php
final class DrydockConsoleController extends DrydockController {
public function shouldAllowPublic() {
return true;
}
public function buildSideNavView() {
$nav = new AphrontSideNavFilterView();
$nav->setBaseURI(new PhutilURI($this->getApplicationURI()));
// These are only used on mobile.
$nav->addFilter('blueprint', pht('Blueprints'));
$nav->addFilter('resource', pht('Resources'));
$nav->addFilter('lease', pht('Leases'));
$nav->addFilter('log', pht('Logs'));
$nav->selectFilter(null);
return $nav;
}
public function processRequest() {
$request = $this->getRequest();
$viewer = $request->getUser();
$menu = id(new PHUIObjectItemListView())
->setUser($viewer);
$menu->addItem(
id(new PHUIObjectItemView())
->setHeader(pht('Blueprints'))
->setHref($this->getApplicationURI('blueprint/'))
->addAttribute(
pht(
'Configure blueprints so Drydock can build resources, like '.
'hosts and working copies.')));
$menu->addItem(
id(new PHUIObjectItemView())
->setHeader(pht('Resources'))
->setHref($this->getApplicationURI('resource/'))
->addAttribute(
- pht(
- 'View and manage resources Drydock has built, like hosts.')));
+ pht('View and manage resources Drydock has built, like hosts.')));
$menu->addItem(
id(new PHUIObjectItemView())
->setHeader(pht('Leases'))
->setHref($this->getApplicationURI('lease/'))
- ->addAttribute(
- pht(
- 'Manage leases on resources.')));
+ ->addAttribute(pht('Manage leases on resources.')));
$menu->addItem(
id(new PHUIObjectItemView())
->setHeader(pht('Logs'))
->setHref($this->getApplicationURI('log/'))
- ->addAttribute(
- pht(
- 'View logs.')));
+ ->addAttribute(pht('View logs.')));
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb(pht('Console'));
return $this->buildApplicationPage(
array(
$crumbs,
$menu,
),
array(
'title' => pht('Drydock Console'),
));
}
}
diff --git a/src/applications/drydock/controller/DrydockLeaseReleaseController.php b/src/applications/drydock/controller/DrydockLeaseReleaseController.php
index 580e0660e..1779edf1b 100644
--- a/src/applications/drydock/controller/DrydockLeaseReleaseController.php
+++ b/src/applications/drydock/controller/DrydockLeaseReleaseController.php
@@ -1,58 +1,65 @@
<?php
final class DrydockLeaseReleaseController extends DrydockLeaseController {
private $id;
public function willProcessRequest(array $data) {
$this->id = $data['id'];
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$lease = id(new DrydockLeaseQuery())
->setViewer($user)
->withIDs(array($this->id))
->executeOne();
if (!$lease) {
return new Aphront404Response();
}
$lease_uri = '/lease/'.$lease->getID().'/';
$lease_uri = $this->getApplicationURI($lease_uri);
if ($lease->getStatus() != DrydockLeaseStatus::STATUS_ACTIVE) {
$dialog = id(new AphrontDialogView())
->setUser($user)
->setTitle(pht('Lease Not Active'))
- ->appendChild(phutil_tag('p', array(), pht(
- 'You can only release "active" leases.')))
+ ->appendChild(
+ phutil_tag(
+ 'p',
+ array(),
+ pht('You can only release "active" leases.')))
->addCancelButton($lease_uri);
return id(new AphrontDialogResponse())->setDialog($dialog);
}
if (!$request->isDialogFormPost()) {
$dialog = id(new AphrontDialogView())
->setUser($user)
->setTitle(pht('Really release lease?'))
- ->appendChild(phutil_tag('p', array(), pht(
- 'Releasing a lease may cause trouble for the lease holder and '.
- 'trigger cleanup of the underlying resource. It can not be '.
- 'undone. Continue?')))
+ ->appendChild(
+ phutil_tag(
+ 'p',
+ array(),
+ pht(
+ 'Releasing a lease may cause trouble for the lease holder and '.
+ 'trigger cleanup of the underlying resource. It can not be '.
+ 'undone. Continue?')))
->addSubmitButton(pht('Release Lease'))
->addCancelButton($lease_uri);
return id(new AphrontDialogResponse())->setDialog($dialog);
}
$resource = $lease->getResource();
$blueprint = $resource->getBlueprint();
$blueprint->releaseLease($resource, $lease);
return id(new AphrontReloadResponse())->setURI($lease_uri);
}
}
diff --git a/src/applications/drydock/controller/DrydockResourceViewController.php b/src/applications/drydock/controller/DrydockResourceViewController.php
index 2a534a4d9..0070b9cf5 100644
--- a/src/applications/drydock/controller/DrydockResourceViewController.php
+++ b/src/applications/drydock/controller/DrydockResourceViewController.php
@@ -1,135 +1,135 @@
<?php
final class DrydockResourceViewController extends DrydockResourceController {
private $id;
public function willProcessRequest(array $data) {
$this->id = $data['id'];
}
public function processRequest() {
$request = $this->getRequest();
$viewer = $request->getUser();
$resource = id(new DrydockResourceQuery())
->setViewer($viewer)
->withIDs(array($this->id))
->executeOne();
if (!$resource) {
return new Aphront404Response();
}
- $title = 'Resource '.$resource->getID().' '.$resource->getName();
+ $title = pht('Resource %s %s', $resource->getID(), $resource->getName());
$header = id(new PHUIHeaderView())
->setHeader($title);
$actions = $this->buildActionListView($resource);
$properties = $this->buildPropertyListView($resource, $actions);
$resource_uri = 'resource/'.$resource->getID().'/';
$resource_uri = $this->getApplicationURI($resource_uri);
$leases = id(new DrydockLeaseQuery())
->setViewer($viewer)
->withResourceIDs(array($resource->getID()))
->execute();
$lease_list = id(new DrydockLeaseListView())
->setUser($viewer)
->setLeases($leases)
->render();
$lease_list->setNoDataString(pht('This resource has no leases.'));
$pager = new AphrontPagerView();
$pager->setURI(new PhutilURI($resource_uri), 'offset');
$pager->setOffset($request->getInt('offset'));
$logs = id(new DrydockLogQuery())
->setViewer($viewer)
->withResourceIDs(array($resource->getID()))
->executeWithOffsetPager($pager);
$log_table = id(new DrydockLogListView())
->setUser($viewer)
->setLogs($logs)
->render();
$log_table->appendChild($pager);
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb(pht('Resource %d', $resource->getID()));
$object_box = id(new PHUIObjectBoxView())
->setHeader($header)
->addPropertyList($properties);
return $this->buildApplicationPage(
array(
$crumbs,
$object_box,
$lease_list,
$log_table,
),
array(
'title' => $title,
));
}
private function buildActionListView(DrydockResource $resource) {
$view = id(new PhabricatorActionListView())
->setUser($this->getRequest()->getUser())
->setObjectURI($this->getRequest()->getRequestURI())
->setObject($resource);
$can_close = ($resource->getStatus() == DrydockResourceStatus::STATUS_OPEN);
$uri = '/resource/'.$resource->getID().'/close/';
$uri = $this->getApplicationURI($uri);
$view->addAction(
id(new PhabricatorActionView())
->setHref($uri)
->setName(pht('Close Resource'))
->setIcon('fa-times')
->setWorkflow(true)
->setDisabled(!$can_close));
return $view;
}
private function buildPropertyListView(
DrydockResource $resource,
PhabricatorActionListView $actions) {
$view = new PHUIPropertyListView();
$view->setActionList($actions);
$status = $resource->getStatus();
$status = DrydockResourceStatus::getNameForStatus($status);
$view->addProperty(
pht('Status'),
$status);
$view->addProperty(
pht('Resource Type'),
$resource->getType());
// TODO: Load handle.
$view->addProperty(
pht('Blueprint'),
$resource->getBlueprintPHID());
$attributes = $resource->getAttributes();
if ($attributes) {
$view->addSectionHeader(pht('Attributes'));
foreach ($attributes as $key => $value) {
$view->addProperty($key, $value);
}
}
return $view;
}
}
diff --git a/src/applications/drydock/interface/command/DrydockSSHCommandInterface.php b/src/applications/drydock/interface/command/DrydockSSHCommandInterface.php
index 1ec2c124f..e691e0ecf 100644
--- a/src/applications/drydock/interface/command/DrydockSSHCommandInterface.php
+++ b/src/applications/drydock/interface/command/DrydockSSHCommandInterface.php
@@ -1,106 +1,107 @@
<?php
final class DrydockSSHCommandInterface extends DrydockCommandInterface {
private $passphraseSSHKey;
private $connectTimeout;
private function openCredentialsIfNotOpen() {
if ($this->passphraseSSHKey !== null) {
return;
}
$credential = id(new PassphraseCredentialQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withIDs(array($this->getConfig('credential')))
->needSecrets(true)
->executeOne();
if ($credential === null) {
- throw new Exception(pht(
- 'There is no credential with ID %d.',
- $this->getConfig('credential')));
+ throw new Exception(
+ pht(
+ 'There is no credential with ID %d.',
+ $this->getConfig('credential')));
}
if ($credential->getProvidesType() !==
PassphraseCredentialTypeSSHPrivateKey::PROVIDES_TYPE) {
- throw new Exception('Only private key credentials are supported.');
+ throw new Exception(pht('Only private key credentials are supported.'));
}
$this->passphraseSSHKey = PassphraseSSHKey::loadFromPHID(
$credential->getPHID(),
PhabricatorUser::getOmnipotentUser());
}
public function setConnectTimeout($timeout) {
$this->connectTimeout = $timeout;
return $this;
}
public function getExecFuture($command) {
$this->openCredentialsIfNotOpen();
$argv = func_get_args();
if ($this->getConfig('platform') === 'windows') {
// Handle Windows by executing the command under PowerShell.
$command = id(new PhutilCommandString($argv))
->setEscapingMode(PhutilCommandString::MODE_POWERSHELL);
$change_directory = '';
if ($this->getWorkingDirectory() !== null) {
$change_directory .= 'cd '.$this->getWorkingDirectory();
}
$script = <<<EOF
$change_directory
$command
if (\$LastExitCode -ne 0) {
exit \$LastExitCode
}
EOF;
// When Microsoft says "Unicode" they don't mean UTF-8.
$script = mb_convert_encoding($script, 'UTF-16LE');
$script = base64_encode($script);
$powershell =
'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe';
$powershell .=
' -ExecutionPolicy Bypass'.
' -NonInteractive'.
' -InputFormat Text'.
' -OutputFormat Text'.
' -EncodedCommand '.$script;
$full_command = $powershell;
} else {
// Handle UNIX by executing under the native shell.
$argv = $this->applyWorkingDirectoryToArgv($argv);
$full_command = call_user_func_array('csprintf', $argv);
}
$command_timeout = '';
if ($this->connectTimeout !== null) {
$command_timeout = csprintf(
'-o %s',
'ConnectTimeout='.$this->connectTimeout);
}
return new ExecFuture(
'ssh '.
'-o LogLevel=quiet '.
'-o StrictHostKeyChecking=no '.
'-o UserKnownHostsFile=/dev/null '.
'-o BatchMode=yes '.
'%C -p %s -i %P %P@%s -- %s',
$command_timeout,
$this->getConfig('port'),
$this->passphraseSSHKey->getKeyfileEnvelope(),
$this->passphraseSSHKey->getUsernameEnvelope(),
$this->getConfig('host'),
$full_command);
}
}
diff --git a/src/applications/drydock/interface/filesystem/DrydockSFTPFilesystemInterface.php b/src/applications/drydock/interface/filesystem/DrydockSFTPFilesystemInterface.php
index 6d6265ac6..fe7d2d1cd 100644
--- a/src/applications/drydock/interface/filesystem/DrydockSFTPFilesystemInterface.php
+++ b/src/applications/drydock/interface/filesystem/DrydockSFTPFilesystemInterface.php
@@ -1,65 +1,65 @@
<?php
final class DrydockSFTPFilesystemInterface extends DrydockFilesystemInterface {
private $passphraseSSHKey;
private function openCredentialsIfNotOpen() {
if ($this->passphraseSSHKey !== null) {
return;
}
$credential = id(new PassphraseCredentialQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withIDs(array($this->getConfig('credential')))
->needSecrets(true)
->executeOne();
if ($credential->getProvidesType() !==
PassphraseCredentialTypeSSHPrivateKey::PROVIDES_TYPE) {
- throw new Exception('Only private key credentials are supported.');
+ throw new Exception(pht('Only private key credentials are supported.'));
}
$this->passphraseSSHKey = PassphraseSSHKey::loadFromPHID(
$credential->getPHID(),
PhabricatorUser::getOmnipotentUser());
}
private function getExecFuture($path) {
$this->openCredentialsIfNotOpen();
return new ExecFuture(
'sftp -o "StrictHostKeyChecking no" -P %s -i %P %P@%s',
$this->getConfig('port'),
$this->passphraseSSHKey->getKeyfileEnvelope(),
$this->passphraseSSHKey->getUsernameEnvelope(),
$this->getConfig('host'));
}
public function readFile($path) {
$target = new TempFile();
$future = $this->getExecFuture($path);
$future->write(csprintf('get %s %s', $path, $target));
$future->resolvex();
return Filesystem::readFile($target);
}
public function saveFile($path, $name) {
$data = $this->readFile($path);
$file = PhabricatorFile::newFromFileData(
$data,
array('name' => $name));
$file->setName($name);
$file->save();
return $file;
}
public function writeFile($path, $data) {
$source = new TempFile();
Filesystem::writeFile($source, $data);
$future = $this->getExecFuture($path);
$future->write(csprintf('put %s %s', $source, $path));
$future->resolvex();
}
}
diff --git a/src/applications/drydock/management/DrydockManagementCloseWorkflow.php b/src/applications/drydock/management/DrydockManagementCloseWorkflow.php
index 550b441e8..f20b0e692 100644
--- a/src/applications/drydock/management/DrydockManagementCloseWorkflow.php
+++ b/src/applications/drydock/management/DrydockManagementCloseWorkflow.php
@@ -1,49 +1,49 @@
<?php
final class DrydockManagementCloseWorkflow
extends DrydockManagementWorkflow {
protected function didConstruct() {
$this
->setName('close')
- ->setSynopsis('Close a resource.')
+ ->setSynopsis(pht('Close a resource.'))
->setArguments(
array(
array(
'name' => 'ids',
'wildcard' => true,
),
));
}
public function execute(PhutilArgumentParser $args) {
$console = PhutilConsole::getConsole();
$ids = $args->getArg('ids');
if (!$ids) {
throw new PhutilArgumentUsageException(
- 'Specify one or more resource IDs to close.');
+ pht('Specify one or more resource IDs to close.'));
}
$viewer = $this->getViewer();
$resources = id(new DrydockResourceQuery())
->setViewer($viewer)
->withIDs($ids)
->execute();
foreach ($ids as $id) {
$resource = idx($resources, $id);
if (!$resource) {
- $console->writeErr("Resource %d does not exist!\n", $id);
+ $console->writeErr("%s\n", pht('Resource %d does not exist!', $id));
} else if ($resource->getStatus() != DrydockResourceStatus::STATUS_OPEN) {
- $console->writeErr("Resource %d is not 'open'!\n", $id);
+ $console->writeErr("%s\n", pht("Resource %d is not 'open'!", $id));
} else {
$resource->closeResource();
- $console->writeErr("Closed resource %d.\n", $id);
+ $console->writeErr("%s\n", pht('Closed resource %d.', $id));
}
}
}
}
diff --git a/src/applications/drydock/management/DrydockManagementCreateResourceWorkflow.php b/src/applications/drydock/management/DrydockManagementCreateResourceWorkflow.php
index 937a714ea..8c67aaa90 100644
--- a/src/applications/drydock/management/DrydockManagementCreateResourceWorkflow.php
+++ b/src/applications/drydock/management/DrydockManagementCreateResourceWorkflow.php
@@ -1,77 +1,81 @@
<?php
final class DrydockManagementCreateResourceWorkflow
extends DrydockManagementWorkflow {
protected function didConstruct() {
$this
->setName('create-resource')
- ->setSynopsis('Create a resource manually.')
+ ->setSynopsis(pht('Create a resource manually.'))
->setArguments(
array(
array(
'name' => 'name',
'param' => 'resource_name',
- 'help' => 'Resource name.',
+ 'help' => pht('Resource name.'),
),
array(
'name' => 'blueprint',
'param' => 'blueprint_id',
- 'help' => 'Blueprint ID.',
+ 'help' => pht('Blueprint ID.'),
),
array(
'name' => 'attributes',
'param' => 'name=value,...',
- 'help' => 'Resource attributes.',
+ 'help' => pht('Resource attributes.'),
),
));
}
public function execute(PhutilArgumentParser $args) {
$console = PhutilConsole::getConsole();
$resource_name = $args->getArg('name');
if (!$resource_name) {
throw new PhutilArgumentUsageException(
- 'Specify a resource name with `--name`.');
+ pht(
+ 'Specify a resource name with `%s`.',
+ '--name'));
}
$blueprint_id = $args->getArg('blueprint');
if (!$blueprint_id) {
throw new PhutilArgumentUsageException(
- 'Specify a blueprint ID with `--blueprint`.');
+ pht(
+ 'Specify a blueprint ID with `%s`.',
+ '--blueprint'));
}
$attributes = $args->getArg('attributes');
if ($attributes) {
$options = new PhutilSimpleOptions();
$options->setCaseSensitive(true);
$attributes = $options->parse($attributes);
}
$viewer = $this->getViewer();
$blueprint = id(new DrydockBlueprintQuery())
->setViewer($viewer)
->withIDs(array($blueprint_id))
->executeOne();
if (!$blueprint) {
throw new PhutilArgumentUsageException(
- 'Specified blueprint does not exist.');
+ pht('Specified blueprint does not exist.'));
}
$resource = id(new DrydockResource())
->setBlueprintPHID($blueprint->getPHID())
->setType($blueprint->getImplementation()->getType())
->setName($resource_name)
->setStatus(DrydockResourceStatus::STATUS_OPEN);
if ($attributes) {
$resource->setAttributes($attributes);
}
$resource->save();
- $console->writeOut("Created Resource %s\n", $resource->getID());
+ $console->writeOut("%s\n", pht('Created Resource %s', $resource->getID()));
return 0;
}
}
diff --git a/src/applications/drydock/management/DrydockManagementLeaseWorkflow.php b/src/applications/drydock/management/DrydockManagementLeaseWorkflow.php
index 529376fe0..238177ae6 100644
--- a/src/applications/drydock/management/DrydockManagementLeaseWorkflow.php
+++ b/src/applications/drydock/management/DrydockManagementLeaseWorkflow.php
@@ -1,56 +1,58 @@
<?php
final class DrydockManagementLeaseWorkflow
extends DrydockManagementWorkflow {
protected function didConstruct() {
$this
->setName('lease')
- ->setSynopsis('Lease a resource.')
+ ->setSynopsis(pht('Lease a resource.'))
->setArguments(
array(
array(
'name' => 'type',
'param' => 'resource_type',
- 'help' => 'Resource type.',
+ 'help' => pht('Resource type.'),
),
array(
'name' => 'attributes',
'param' => 'name=value,...',
- 'help' => 'Resource specficiation.',
+ 'help' => pht('Resource specficiation.'),
),
));
}
public function execute(PhutilArgumentParser $args) {
$console = PhutilConsole::getConsole();
$resource_type = $args->getArg('type');
if (!$resource_type) {
throw new PhutilArgumentUsageException(
- 'Specify a resource type with `--type`.');
+ pht(
+ 'Specify a resource type with `%s`.',
+ '--type'));
}
$attributes = $args->getArg('attributes');
if ($attributes) {
$options = new PhutilSimpleOptions();
$options->setCaseSensitive(true);
$attributes = $options->parse($attributes);
}
PhabricatorWorker::setRunAllTasksInProcess(true);
$lease = id(new DrydockLease())
->setResourceType($resource_type);
if ($attributes) {
$lease->setAttributes($attributes);
}
$lease
->queueForActivation()
->waitUntilActive();
- $console->writeOut("Acquired Lease %s\n", $lease->getID());
+ $console->writeOut("%s\n", pht('Acquired Lease %s', $lease->getID()));
return 0;
}
}
diff --git a/src/applications/drydock/management/DrydockManagementReleaseWorkflow.php b/src/applications/drydock/management/DrydockManagementReleaseWorkflow.php
index b59204695..616a5deb7 100644
--- a/src/applications/drydock/management/DrydockManagementReleaseWorkflow.php
+++ b/src/applications/drydock/management/DrydockManagementReleaseWorkflow.php
@@ -1,52 +1,52 @@
<?php
final class DrydockManagementReleaseWorkflow
extends DrydockManagementWorkflow {
protected function didConstruct() {
$this
->setName('release')
- ->setSynopsis('Release a lease.')
+ ->setSynopsis(pht('Release a lease.'))
->setArguments(
array(
array(
'name' => 'ids',
'wildcard' => true,
),
));
}
public function execute(PhutilArgumentParser $args) {
$console = PhutilConsole::getConsole();
$ids = $args->getArg('ids');
if (!$ids) {
throw new PhutilArgumentUsageException(
- 'Specify one or more lease IDs to release.');
+ pht('Specify one or more lease IDs to release.'));
}
$viewer = $this->getViewer();
$leases = id(new DrydockLeaseQuery())
->setViewer($viewer)
->withIDs($ids)
->execute();
foreach ($ids as $id) {
$lease = idx($leases, $id);
if (!$lease) {
- $console->writeErr("Lease %d does not exist!\n", $id);
+ $console->writeErr("%s\n", pht('Lease %d does not exist!', $id));
} else if ($lease->getStatus() != DrydockLeaseStatus::STATUS_ACTIVE) {
- $console->writeErr("Lease %d is not 'active'!\n", $id);
+ $console->writeErr("%s\n", pht("Lease %d is not 'active'!", $id));
} else {
$resource = $lease->getResource();
$blueprint = $resource->getBlueprint();
$blueprint->releaseLease($resource, $lease);
- $console->writeErr("Released lease %d.\n", $id);
+ $console->writeErr("%s\n", pht('Released lease %d.', $id));
}
}
}
}
diff --git a/src/applications/drydock/storage/DrydockBlueprint.php b/src/applications/drydock/storage/DrydockBlueprint.php
index af258e0b1..bad3f9a2d 100644
--- a/src/applications/drydock/storage/DrydockBlueprint.php
+++ b/src/applications/drydock/storage/DrydockBlueprint.php
@@ -1,152 +1,153 @@
<?php
final class DrydockBlueprint extends DrydockDAO
implements
PhabricatorApplicationTransactionInterface,
PhabricatorPolicyInterface,
PhabricatorCustomFieldInterface {
protected $className;
protected $blueprintName;
protected $viewPolicy;
protected $editPolicy;
protected $details = array();
private $implementation = self::ATTACHABLE;
private $customFields = self::ATTACHABLE;
public static function initializeNewBlueprint(PhabricatorUser $actor) {
$app = id(new PhabricatorApplicationQuery())
->setViewer($actor)
->withClasses(array('PhabricatorDrydockApplication'))
->executeOne();
$view_policy = $app->getPolicy(
DrydockDefaultViewCapability::CAPABILITY);
$edit_policy = $app->getPolicy(
DrydockDefaultEditCapability::CAPABILITY);
return id(new DrydockBlueprint())
->setViewPolicy($view_policy)
->setEditPolicy($edit_policy)
->setBlueprintName('');
}
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'details' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'className' => 'text255',
'blueprintName' => 'text255',
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
DrydockBlueprintPHIDType::TYPECONST);
}
public function getImplementation() {
$class = $this->className;
$implementations =
DrydockBlueprintImplementation::getAllBlueprintImplementations();
if (!isset($implementations[$class])) {
throw new Exception(
- "Invalid class name for blueprint (got '".$class."')");
+ pht(
+ "Invalid class name for blueprint (got '%s')",
+ $class));
}
return id(new $class())->attachInstance($this);
}
public function attachImplementation(DrydockBlueprintImplementation $impl) {
$this->implementation = $impl;
return $this;
}
public function getDetail($key, $default = null) {
return idx($this->details, $key, $default);
}
public function setDetail($key, $value) {
$this->details[$key] = $value;
return $this;
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new DrydockBlueprintEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new DrydockBlueprintTransaction();
}
public function willRenderTimeline(
PhabricatorApplicationTransactionView $timeline,
AphrontRequest $request) {
return $timeline;
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
return $this->getViewPolicy();
case PhabricatorPolicyCapability::CAN_EDIT:
return $this->getEditPolicy();
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return false;
}
public function describeAutomaticCapability($capability) {
return null;
}
/* -( PhabricatorCustomFieldInterface )------------------------------------ */
public function getCustomFieldSpecificationForRole($role) {
return array();
}
public function getCustomFieldBaseClass() {
return 'DrydockBlueprintCustomField';
}
public function getCustomFields() {
return $this->assertAttached($this->customFields);
}
public function attachCustomFields(PhabricatorCustomFieldAttachment $fields) {
$this->customFields = $fields;
return $this;
}
-
}
diff --git a/src/applications/drydock/storage/DrydockLease.php b/src/applications/drydock/storage/DrydockLease.php
index 252c92206..39fa59330 100644
--- a/src/applications/drydock/storage/DrydockLease.php
+++ b/src/applications/drydock/storage/DrydockLease.php
@@ -1,229 +1,230 @@
<?php
final class DrydockLease extends DrydockDAO
implements PhabricatorPolicyInterface {
protected $resourceID;
protected $resourceType;
protected $until;
protected $ownerPHID;
protected $attributes = array();
protected $status = DrydockLeaseStatus::STATUS_PENDING;
protected $taskID;
private $resource = self::ATTACHABLE;
private $releaseOnDestruction;
/**
* Flag this lease to be released when its destructor is called. This is
* mostly useful if you have a script which acquires, uses, and then releases
* a lease, as you don't need to explicitly handle exceptions to properly
* release the lease.
*/
public function releaseOnDestruction() {
$this->releaseOnDestruction = true;
return $this;
}
public function __destruct() {
if ($this->releaseOnDestruction) {
if ($this->isActive()) {
$this->release();
}
}
}
public function getLeaseName() {
return pht('Lease %d', $this->getID());
}
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'attributes' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'status' => 'uint32',
'until' => 'epoch?',
'resourceType' => 'text128',
'taskID' => 'id?',
'ownerPHID' => 'phid?',
'resourceID' => 'id?',
),
self::CONFIG_KEY_SCHEMA => array(
'key_phid' => null,
'phid' => array(
'columns' => array('phid'),
'unique' => true,
),
),
) + parent::getConfiguration();
}
public function setAttribute($key, $value) {
$this->attributes[$key] = $value;
return $this;
}
public function getAttribute($key, $default = null) {
return idx($this->attributes, $key, $default);
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(DrydockLeasePHIDType::TYPECONST);
}
public function getInterface($type) {
return $this->getResource()->getInterface($this, $type);
}
public function getResource() {
return $this->assertAttached($this->resource);
}
public function attachResource(DrydockResource $resource = null) {
$this->resource = $resource;
return $this;
}
public function hasAttachedResource() {
return ($this->resource !== null);
}
public function loadResource() {
return id(new DrydockResource())->loadOneWhere(
'id = %d',
$this->getResourceID());
}
public function queueForActivation() {
if ($this->getID()) {
throw new Exception(
- 'Only new leases may be queued for activation!');
+ pht('Only new leases may be queued for activation!'));
}
$this->setStatus(DrydockLeaseStatus::STATUS_PENDING);
$this->save();
$task = PhabricatorWorker::scheduleTask(
'DrydockAllocatorWorker',
$this->getID());
// NOTE: Scheduling the task might execute it in-process, if we're running
// from a CLI script. Reload the lease to make sure we have the most
// up-to-date information. Normally, this has no effect.
$this->reload();
$this->setTaskID($task->getID());
$this->save();
return $this;
}
public function release() {
$this->assertActive();
$this->setStatus(DrydockLeaseStatus::STATUS_RELEASED);
$this->save();
$this->resource = null;
return $this;
}
public function isActive() {
switch ($this->status) {
case DrydockLeaseStatus::STATUS_ACTIVE:
case DrydockLeaseStatus::STATUS_ACQUIRING:
return true;
}
return false;
}
private function assertActive() {
if (!$this->isActive()) {
throw new Exception(
- 'Lease is not active! You can not interact with resources through '.
- 'an inactive lease.');
+ pht(
+ 'Lease is not active! You can not interact with resources through '.
+ 'an inactive lease.'));
}
}
public static function waitForLeases(array $leases) {
assert_instances_of($leases, __CLASS__);
$task_ids = array_filter(mpull($leases, 'getTaskID'));
PhabricatorWorker::waitForTasks($task_ids);
$unresolved = $leases;
while (true) {
foreach ($unresolved as $key => $lease) {
$lease->reload();
switch ($lease->getStatus()) {
case DrydockLeaseStatus::STATUS_ACTIVE:
unset($unresolved[$key]);
break;
case DrydockLeaseStatus::STATUS_RELEASED:
- throw new Exception('Lease has already been released!');
+ throw new Exception(pht('Lease has already been released!'));
case DrydockLeaseStatus::STATUS_EXPIRED:
- throw new Exception('Lease has already expired!');
+ throw new Exception(pht('Lease has already expired!'));
case DrydockLeaseStatus::STATUS_BROKEN:
- throw new Exception('Lease has been broken!');
+ throw new Exception(pht('Lease has been broken!'));
case DrydockLeaseStatus::STATUS_PENDING:
case DrydockLeaseStatus::STATUS_ACQUIRING:
break;
default:
- throw new Exception('Unknown status??');
+ throw new Exception(pht('Unknown status??'));
}
}
if ($unresolved) {
sleep(1);
} else {
break;
}
}
foreach ($leases as $lease) {
$lease->attachResource($lease->loadResource());
}
}
public function waitUntilActive() {
if (!$this->getID()) {
$this->queueForActivation();
}
self::waitForLeases(array($this));
return $this;
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
);
}
public function getPolicy($capability) {
if ($this->getResource()) {
return $this->getResource()->getPolicy($capability);
}
return PhabricatorPolicies::getMostOpenPolicy();
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
if ($this->getResource()) {
return $this->getResource()->hasAutomaticCapability($capability, $viewer);
}
return false;
}
public function describeAutomaticCapability($capability) {
return pht('Leases inherit policies from the resources they lease.');
}
}
diff --git a/src/applications/drydock/worker/DrydockAllocatorWorker.php b/src/applications/drydock/worker/DrydockAllocatorWorker.php
index 6d8e740e1..f9a647a3a 100644
--- a/src/applications/drydock/worker/DrydockAllocatorWorker.php
+++ b/src/applications/drydock/worker/DrydockAllocatorWorker.php
@@ -1,185 +1,187 @@
<?php
final class DrydockAllocatorWorker extends PhabricatorWorker {
private $lease;
public function getRequiredLeaseTime() {
return 3600 * 24;
}
public function getMaximumRetryCount() {
// TODO: Allow Drydock allocations to retry. For now, every failure is
// permanent and most of them are because I am bad at programming, so fail
// fast rather than ending up in limbo.
return 0;
}
private function loadLease() {
if (empty($this->lease)) {
$lease = id(new DrydockLeaseQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withIDs(array($this->getTaskData()))
->executeOne();
if (!$lease) {
throw new PhabricatorWorkerPermanentFailureException(
pht('No such lease %d!', $this->getTaskData()));
}
$this->lease = $lease;
}
return $this->lease;
}
private function logToDrydock($message) {
DrydockBlueprintImplementation::writeLog(
null,
$this->loadLease(),
$message);
}
protected function doWork() {
$lease = $this->loadLease();
- $this->logToDrydock('Allocating Lease');
+ $this->logToDrydock(pht('Allocating Lease'));
try {
$this->allocateLease($lease);
} catch (Exception $ex) {
// TODO: We should really do this when archiving the task, if we've
// suffered a permanent failure. But we don't have hooks for that yet
// and always fail after the first retry right now, so this is
// functionally equivalent.
$lease->reload();
if ($lease->getStatus() == DrydockLeaseStatus::STATUS_PENDING) {
$lease->setStatus(DrydockLeaseStatus::STATUS_BROKEN);
$lease->save();
}
throw $ex;
}
}
private function loadAllBlueprints() {
$viewer = PhabricatorUser::getOmnipotentUser();
$instances = id(new DrydockBlueprintQuery())
->setViewer($viewer)
->execute();
$blueprints = array();
foreach ($instances as $instance) {
$blueprints[$instance->getPHID()] = $instance;
}
return $blueprints;
}
private function allocateLease(DrydockLease $lease) {
$type = $lease->getResourceType();
$blueprints = $this->loadAllBlueprints();
// TODO: Policy stuff.
$pool = id(new DrydockResource())->loadAllWhere(
'type = %s AND status = %s',
$lease->getResourceType(),
DrydockResourceStatus::STATUS_OPEN);
$this->logToDrydock(
pht('Found %d Open Resource(s)', count($pool)));
$candidates = array();
foreach ($pool as $key => $candidate) {
if (!isset($blueprints[$candidate->getBlueprintPHID()])) {
unset($pool[$key]);
continue;
}
$blueprint = $blueprints[$candidate->getBlueprintPHID()];
$implementation = $blueprint->getImplementation();
if ($implementation->filterResource($candidate, $lease)) {
$candidates[] = $candidate;
}
}
$this->logToDrydock(pht('%d Open Resource(s) Remain', count($candidates)));
$resource = null;
if ($candidates) {
shuffle($candidates);
foreach ($candidates as $candidate_resource) {
$blueprint = $blueprints[$candidate_resource->getBlueprintPHID()]
->getImplementation();
if ($blueprint->allocateLease($candidate_resource, $lease)) {
$resource = $candidate_resource;
break;
}
}
}
if (!$resource) {
$blueprints = DrydockBlueprintImplementation
::getAllBlueprintImplementationsForResource($type);
$this->logToDrydock(
pht('Found %d Blueprints', count($blueprints)));
foreach ($blueprints as $key => $candidate_blueprint) {
if (!$candidate_blueprint->isEnabled()) {
unset($blueprints[$key]);
continue;
}
}
$this->logToDrydock(
pht('%d Blueprints Enabled', count($blueprints)));
foreach ($blueprints as $key => $candidate_blueprint) {
if (!$candidate_blueprint->canAllocateMoreResources($pool)) {
unset($blueprints[$key]);
continue;
}
}
$this->logToDrydock(
pht('%d Blueprints Can Allocate', count($blueprints)));
if (!$blueprints) {
$lease->setStatus(DrydockLeaseStatus::STATUS_BROKEN);
$lease->save();
$this->logToDrydock(
- "There are no resources of type '{$type}' available, and no ".
- "blueprints which can allocate new ones.");
+ pht(
+ "There are no resources of type '%s' available, and no ".
+ "blueprints which can allocate new ones.",
+ $type));
return;
}
// TODO: Rank intelligently.
shuffle($blueprints);
$blueprint = head($blueprints);
$resource = $blueprint->allocateResource($lease);
if (!$blueprint->allocateLease($resource, $lease)) {
// TODO: This "should" happen only if we lost a race with another lease,
// which happened to acquire this resource immediately after we
// allocated it. In this case, the right behavior is to retry
// immediately. However, other things like a blueprint allocating a
// resource it can't actually allocate the lease on might be happening
// too, in which case we'd just allocate infinite resources. Probably
// what we should do is test for an active or allocated lease and retry
// if we find one (although it might have already been released by now)
// and fail really hard ("your configuration is a huge broken mess")
// otherwise. But just throw for now since this stuff is all edge-casey.
// Alternatively we could bring resources up in a "BESPOKE" status
// and then switch them to "OPEN" only after the allocating lease gets
// its grubby mitts on the resource. This might make more sense but
// is a bit messy.
- throw new Exception('Lost an allocation race?');
+ throw new Exception(pht('Lost an allocation race?'));
}
}
$blueprint = $resource->getBlueprint();
$blueprint->acquireLease($resource, $lease);
}
}
diff --git a/src/applications/fact/controller/PhabricatorFactChartController.php b/src/applications/fact/controller/PhabricatorFactChartController.php
index 9b6a46b32..92718f47f 100644
--- a/src/applications/fact/controller/PhabricatorFactChartController.php
+++ b/src/applications/fact/controller/PhabricatorFactChartController.php
@@ -1,96 +1,96 @@
<?php
final class PhabricatorFactChartController extends PhabricatorFactController {
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$table = new PhabricatorFactRaw();
$conn_r = $table->establishConnection('r');
$table_name = $table->getTableName();
$series = $request->getStr('y1');
$specs = PhabricatorFactSpec::newSpecsForFactTypes(
PhabricatorFactEngine::loadAllEngines(),
array($series));
$spec = idx($specs, $series);
$data = queryfx_all(
$conn_r,
'SELECT valueX, epoch FROM %T WHERE factType = %s ORDER BY epoch ASC',
$table_name,
$series);
$points = array();
$sum = 0;
foreach ($data as $key => $row) {
$sum += (int)$row['valueX'];
$points[(int)$row['epoch']] = $sum;
}
if (!$points) {
// NOTE: Raphael crashes Safari if you hand it series with no points.
- throw new Exception('No data to show!');
+ throw new Exception(pht('No data to show!'));
}
// Limit amount of data passed to browser.
$count = count($points);
$limit = 2000;
if ($count > $limit) {
$i = 0;
$every = ceil($count / $limit);
foreach ($points as $epoch => $sum) {
$i++;
if ($i % $every && $i != $count) {
unset($points[$epoch]);
}
}
}
$x = array_keys($points);
$y = array_values($points);
$id = celerity_generate_unique_node_id();
$chart = phutil_tag(
'div',
array(
'id' => $id,
'style' => 'border: 1px solid #6f6f6f; '.
'margin: 1em 2em; '.
'background: #ffffff; '.
'height: 400px; ',
),
'');
require_celerity_resource('raphael-core');
require_celerity_resource('raphael-g');
require_celerity_resource('raphael-g-line');
Javelin::initBehavior('line-chart', array(
'hardpoint' => $id,
'x' => array($x),
'y' => array($y),
'xformat' => 'epoch',
'colors' => array('#0000ff'),
));
$panel = new PHUIObjectBoxView();
$panel->setHeaderText(pht('Count of %s', $spec->getName()));
$panel->appendChild($chart);
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb(pht('Chart'));
return $this->buildApplicationPage(
array(
$crumbs,
$panel,
),
array(
'title' => pht('Chart'),
));
}
}
diff --git a/src/applications/fact/daemon/PhabricatorFactDaemon.php b/src/applications/fact/daemon/PhabricatorFactDaemon.php
index 130bdcde1..d60a77de6 100644
--- a/src/applications/fact/daemon/PhabricatorFactDaemon.php
+++ b/src/applications/fact/daemon/PhabricatorFactDaemon.php
@@ -1,205 +1,205 @@
<?php
final class PhabricatorFactDaemon extends PhabricatorDaemon {
private $engines;
const RAW_FACT_BUFFER_LIMIT = 128;
protected function run() {
$this->setEngines(PhabricatorFactEngine::loadAllEngines());
while (!$this->shouldExit()) {
$iterators = $this->getAllApplicationIterators();
foreach ($iterators as $iterator_name => $iterator) {
$this->processIteratorWithCursor($iterator_name, $iterator);
}
$this->processAggregates();
- $this->log('Zzz...');
+ $this->log(pht('Zzz...'));
$this->sleep(60 * 5);
}
}
public static function getAllApplicationIterators() {
$apps = PhabricatorApplication::getAllInstalledApplications();
$iterators = array();
foreach ($apps as $app) {
foreach ($app->getFactObjectsForAnalysis() as $object) {
$iterator = new PhabricatorFactUpdateIterator($object);
$iterators[get_class($object)] = $iterator;
}
}
return $iterators;
}
public function processIteratorWithCursor($iterator_name, $iterator) {
- $this->log("Processing cursor '{$iterator_name}'.");
+ $this->log(pht("Processing cursor '%s'.", $iterator_name));
$cursor = id(new PhabricatorFactCursor())->loadOneWhere(
'name = %s',
$iterator_name);
if (!$cursor) {
$cursor = new PhabricatorFactCursor();
$cursor->setName($iterator_name);
$position = null;
} else {
$position = $cursor->getPosition();
}
if ($position) {
$iterator->setPosition($position);
}
$new_cursor_position = $this->processIterator($iterator);
if ($new_cursor_position) {
$cursor->setPosition($new_cursor_position);
$cursor->save();
}
}
public function setEngines(array $engines) {
assert_instances_of($engines, 'PhabricatorFactEngine');
$this->engines = $engines;
return $this;
}
public function processIterator($iterator) {
$result = null;
$raw_facts = array();
foreach ($iterator as $key => $object) {
$phid = $object->getPHID();
- $this->log("Processing {$phid}...");
+ $this->log(pht('Processing %s...', $phid));
$raw_facts[$phid] = $this->computeRawFacts($object);
if (count($raw_facts) > self::RAW_FACT_BUFFER_LIMIT) {
$this->updateRawFacts($raw_facts);
$raw_facts = array();
}
$result = $key;
}
if ($raw_facts) {
$this->updateRawFacts($raw_facts);
$raw_facts = array();
}
return $result;
}
public function processAggregates() {
- $this->log('Processing aggregates.');
+ $this->log(pht('Processing aggregates.'));
$facts = $this->computeAggregateFacts();
$this->updateAggregateFacts($facts);
}
private function computeAggregateFacts() {
$facts = array();
foreach ($this->engines as $engine) {
if (!$engine->shouldComputeAggregateFacts()) {
continue;
}
$facts[] = $engine->computeAggregateFacts();
}
return array_mergev($facts);
}
private function computeRawFacts(PhabricatorLiskDAO $object) {
$facts = array();
foreach ($this->engines as $engine) {
if (!$engine->shouldComputeRawFactsForObject($object)) {
continue;
}
$facts[] = $engine->computeRawFactsForObject($object);
}
return array_mergev($facts);
}
private function updateRawFacts(array $map) {
foreach ($map as $phid => $facts) {
assert_instances_of($facts, 'PhabricatorFactRaw');
}
$phids = array_keys($map);
if (!$phids) {
return;
}
$table = new PhabricatorFactRaw();
$conn = $table->establishConnection('w');
$table_name = $table->getTableName();
$sql = array();
foreach ($map as $phid => $facts) {
foreach ($facts as $fact) {
$sql[] = qsprintf(
$conn,
'(%s, %s, %s, %d, %d, %d)',
$fact->getFactType(),
$fact->getObjectPHID(),
$fact->getObjectA(),
$fact->getValueX(),
$fact->getValueY(),
$fact->getEpoch());
}
}
$table->openTransaction();
queryfx(
$conn,
'DELETE FROM %T WHERE objectPHID IN (%Ls)',
$table_name,
$phids);
if ($sql) {
foreach (array_chunk($sql, 256) as $chunk) {
queryfx(
$conn,
'INSERT INTO %T
(factType, objectPHID, objectA, valueX, valueY, epoch)
VALUES %Q',
$table_name,
implode(', ', $chunk));
}
}
$table->saveTransaction();
}
private function updateAggregateFacts(array $facts) {
if (!$facts) {
return;
}
$table = new PhabricatorFactAggregate();
$conn = $table->establishConnection('w');
$table_name = $table->getTableName();
$sql = array();
foreach ($facts as $fact) {
$sql[] = qsprintf(
$conn,
'(%s, %s, %d)',
$fact->getFactType(),
$fact->getObjectPHID(),
$fact->getValueX());
}
foreach (array_chunk($sql, 256) as $chunk) {
queryfx(
$conn,
'INSERT INTO %T (factType, objectPHID, valueX) VALUES %Q
ON DUPLICATE KEY UPDATE valueX = VALUES(valueX)',
$table_name,
implode(', ', $chunk));
}
}
}
diff --git a/src/applications/fact/engine/PhabricatorFactCountEngine.php b/src/applications/fact/engine/PhabricatorFactCountEngine.php
index 133e49926..f24068646 100644
--- a/src/applications/fact/engine/PhabricatorFactCountEngine.php
+++ b/src/applications/fact/engine/PhabricatorFactCountEngine.php
@@ -1,86 +1,86 @@
<?php
/**
* Simple fact engine which counts objects.
*/
final class PhabricatorFactCountEngine extends PhabricatorFactEngine {
public function getFactSpecs(array $fact_types) {
$results = array();
foreach ($fact_types as $type) {
if (!strncmp($type, '+N:', 3)) {
if ($type == '+N:*') {
- $name = 'Total Objects';
+ $name = pht('Total Objects');
} else {
- $name = 'Total Objects of type '.substr($type, 3);
+ $name = pht('Total Objects of type %s', substr($type, 3));
}
$results[] = id(new PhabricatorFactSimpleSpec($type))
->setName($name)
->setUnit(PhabricatorFactSimpleSpec::UNIT_COUNT);
}
if (!strncmp($type, 'N:', 2)) {
if ($type == 'N:*') {
- $name = 'Objects';
+ $name = pht('Objects');
} else {
- $name = 'Objects of type '.substr($type, 2);
+ $name = pht('Objects of type %s', substr($type, 2));
}
$results[] = id(new PhabricatorFactSimpleSpec($type))
->setName($name)
->setUnit(PhabricatorFactSimpleSpec::UNIT_COUNT);
}
}
return $results;
}
public function shouldComputeRawFactsForObject(PhabricatorLiskDAO $object) {
return true;
}
public function computeRawFactsForObject(PhabricatorLiskDAO $object) {
$facts = array();
$phid = $object->getPHID();
$type = phid_get_type($phid);
foreach (array('N:*', 'N:'.$type) as $fact_type) {
$facts[] = id(new PhabricatorFactRaw())
->setFactType($fact_type)
->setObjectPHID($phid)
->setValueX(1)
->setEpoch($object->getDateCreated());
}
return $facts;
}
public function shouldComputeAggregateFacts() {
return true;
}
public function computeAggregateFacts() {
$table = new PhabricatorFactRaw();
$table_name = $table->getTableName();
$conn = $table->establishConnection('r');
$counts = queryfx_all(
$conn,
'SELECT factType, SUM(valueX) N FROM %T WHERE factType LIKE %>
GROUP BY factType',
$table_name,
'N:');
$facts = array();
foreach ($counts as $count) {
$facts[] = id(new PhabricatorFactAggregate())
->setFactType('+'.$count['factType'])
->setValueX($count['N']);
}
return $facts;
}
}
diff --git a/src/applications/fact/engine/PhabricatorFactLastUpdatedEngine.php b/src/applications/fact/engine/PhabricatorFactLastUpdatedEngine.php
index 39e8781d5..5ea99e623 100644
--- a/src/applications/fact/engine/PhabricatorFactLastUpdatedEngine.php
+++ b/src/applications/fact/engine/PhabricatorFactLastUpdatedEngine.php
@@ -1,34 +1,34 @@
<?php
/**
* Engine that records the time facts were last updated.
*/
final class PhabricatorFactLastUpdatedEngine extends PhabricatorFactEngine {
public function getFactSpecs(array $fact_types) {
$results = array();
foreach ($fact_types as $type) {
if ($type == 'updated') {
$results[] = id(new PhabricatorFactSimpleSpec($type))
- ->setName('Facts Last Updated')
+ ->setName(pht('Facts Last Updated'))
->setUnit(PhabricatorFactSimpleSpec::UNIT_EPOCH);
}
}
return $results;
}
public function shouldComputeAggregateFacts() {
return true;
}
public function computeAggregateFacts() {
$facts = array();
$facts[] = id(new PhabricatorFactAggregate())
->setFactType('updated')
->setValueX(time());
return $facts;
}
}
diff --git a/src/applications/fact/extract/PhabricatorFactUpdateIterator.php b/src/applications/fact/extract/PhabricatorFactUpdateIterator.php
index 302026098..df77f4dd3 100644
--- a/src/applications/fact/extract/PhabricatorFactUpdateIterator.php
+++ b/src/applications/fact/extract/PhabricatorFactUpdateIterator.php
@@ -1,96 +1,96 @@
<?php
/**
* Iterate over objects by update time in a stable way. This iterator only works
- * for "normal" Lisk objects: objects with an autoincrement ID and a
+ * for "normal" Lisk objects: objects with an auto-increment ID and a
* dateModified column.
*/
final class PhabricatorFactUpdateIterator extends PhutilBufferedIterator {
private $cursor;
private $object;
private $position;
private $ignoreUpdatesDuration = 15;
private $set;
public function __construct(LiskDAO $object) {
$this->set = new LiskDAOSet();
$this->object = $object->putInSet($this->set);
}
public function setPosition($position) {
$this->position = $position;
return $this;
}
protected function didRewind() {
$this->cursor = $this->position;
}
protected function getCursorFromObject($object) {
if ($object->hasProperty('dateModified')) {
return $object->getDateModified().':'.$object->getID();
} else {
return $object->getID();
}
}
public function key() {
return $this->getCursorFromObject($this->current());
}
protected function loadPage() {
$this->set->clearSet();
if ($this->object->hasProperty('dateModified')) {
if ($this->cursor) {
list($after_epoch, $after_id) = explode(':', $this->cursor);
} else {
$after_epoch = 0;
$after_id = 0;
}
// NOTE: We ignore recent updates because once we process an update we'll
// never process rows behind it again. We need to read only rows which
// we're sure no new rows will be inserted behind. If we read a row that
// was updated on the current second, another update later on in this
// second could affect an object with a lower ID, and we'd skip that
// update. To avoid this, just ignore any rows which have been updated in
// the last few seconds. This also reduces the amount of work we need to
// do if an object is repeatedly updated; we will just look at the end
// state without processing the intermediate states. Finally, this gives
// us reasonable protections against clock skew between the machine the
// daemon is running on and any machines performing writes.
$page = $this->object->loadAllWhere(
'((dateModified > %d) OR (dateModified = %d AND id > %d))
AND (dateModified < %d - %d)
ORDER BY dateModified ASC, id ASC LIMIT %d',
$after_epoch,
$after_epoch,
$after_id,
time(),
$this->ignoreUpdatesDuration,
$this->getPageSize());
} else {
if ($this->cursor) {
$after_id = $this->cursor;
} else {
$after_id = 0;
}
$page = $this->object->loadAllWhere(
'id > %d ORDER BY id ASC LIMIT %d',
$after_id,
$this->getPageSize());
}
if ($page) {
$this->cursor = $this->getCursorFromObject(end($page));
}
return $page;
}
}
diff --git a/src/applications/fact/management/PhabricatorFactManagementAnalyzeWorkflow.php b/src/applications/fact/management/PhabricatorFactManagementAnalyzeWorkflow.php
index 461f87379..6b316f8c7 100644
--- a/src/applications/fact/management/PhabricatorFactManagementAnalyzeWorkflow.php
+++ b/src/applications/fact/management/PhabricatorFactManagementAnalyzeWorkflow.php
@@ -1,68 +1,68 @@
<?php
final class PhabricatorFactManagementAnalyzeWorkflow
extends PhabricatorFactManagementWorkflow {
protected function didConstruct() {
$this
->setName('analyze')
->setSynopsis(pht('Manually invoke fact analyzers.'))
->setArguments(
array(
array(
'name' => 'iterator',
'param' => 'name',
'repeat' => true,
- 'help' => 'Process only iterator __name__.',
+ 'help' => pht('Process only iterator __name__.'),
),
array(
'name' => 'all',
- 'help' => 'Analyze from the beginning, ignoring cursors.',
+ 'help' => pht('Analyze from the beginning, ignoring cursors.'),
),
array(
'name' => 'skip-aggregates',
- 'help' => 'Skip analysis of aggreate facts.',
+ 'help' => pht('Skip analysis of aggregate facts.'),
),
));
}
public function execute(PhutilArgumentParser $args) {
$console = PhutilConsole::getConsole();
$daemon = new PhabricatorFactDaemon(array());
$daemon->setVerbose(true);
$daemon->setEngines(PhabricatorFactEngine::loadAllEngines());
$iterators = PhabricatorFactDaemon::getAllApplicationIterators();
$selected = $args->getArg('iterator');
if ($selected) {
$use = array();
foreach ($selected as $iterator_name) {
if (isset($iterators[$iterator_name])) {
$use[$iterator_name] = $iterators[$iterator_name];
} else {
$console->writeErr(
"%s\n",
pht("Iterator '%s' does not exist.", $iterator_name));
}
}
$iterators = $use;
}
foreach ($iterators as $iterator_name => $iterator) {
if ($args->getArg('all')) {
$daemon->processIterator($iterator);
} else {
$daemon->processIteratorWithCursor($iterator_name, $iterator);
}
}
if (!$args->getArg('skip-aggregates')) {
$daemon->processAggregates();
}
return 0;
}
}
diff --git a/src/applications/fact/management/PhabricatorFactManagementCursorsWorkflow.php b/src/applications/fact/management/PhabricatorFactManagementCursorsWorkflow.php
index 590967bd3..74f35e3cc 100644
--- a/src/applications/fact/management/PhabricatorFactManagementCursorsWorkflow.php
+++ b/src/applications/fact/management/PhabricatorFactManagementCursorsWorkflow.php
@@ -1,66 +1,66 @@
<?php
final class PhabricatorFactManagementCursorsWorkflow
extends PhabricatorFactManagementWorkflow {
protected function didConstruct() {
$this
->setName('cursors')
->setSynopsis(pht('Show a list of fact iterators and cursors.'))
->setExamples(
"**cursors**\n".
"**cursors** --reset __cursor__")
->setArguments(
array(
array(
'name' => 'reset',
'param' => 'cursor',
'repeat' => true,
- 'help' => 'Reset cursor __cursor__.',
+ 'help' => pht('Reset cursor __cursor__.'),
),
));
}
public function execute(PhutilArgumentParser $args) {
$console = PhutilConsole::getConsole();
$reset = $args->getArg('reset');
if ($reset) {
foreach ($reset as $name) {
$cursor = id(new PhabricatorFactCursor())->loadOneWhere(
'name = %s',
$name);
if ($cursor) {
$console->writeOut("%s\n", pht('Resetting cursor %s...', $name));
$cursor->delete();
} else {
$console->writeErr(
"%s\n",
pht('Cursor %s does not exist or is already reset.', $name));
}
}
return 0;
}
$iterator_map = PhabricatorFactDaemon::getAllApplicationIterators();
if (!$iterator_map) {
$console->writeErr("%s\n", pht('No cursors.'));
return 0;
}
$cursors = id(new PhabricatorFactCursor())->loadAllWhere(
'name IN (%Ls)',
array_keys($iterator_map));
$cursors = mpull($cursors, 'getPosition', 'getName');
foreach ($iterator_map as $iterator_name => $iterator) {
$console->writeOut(
"%s (%s)\n",
$iterator_name,
idx($cursors, $iterator_name, 'start'));
}
return 0;
}
}
diff --git a/src/applications/fact/spec/PhabricatorFactSpec.php b/src/applications/fact/spec/PhabricatorFactSpec.php
index a0af2420c..a9646b246 100644
--- a/src/applications/fact/spec/PhabricatorFactSpec.php
+++ b/src/applications/fact/spec/PhabricatorFactSpec.php
@@ -1,52 +1,53 @@
<?php
abstract class PhabricatorFactSpec {
const UNIT_COUNT = 'unit-count';
const UNIT_EPOCH = 'unit-epoch';
public static function newSpecsForFactTypes(
array $engines,
array $fact_types) {
assert_instances_of($engines, 'PhabricatorFactEngine');
$map = array();
foreach ($engines as $engine) {
$specs = $engine->getFactSpecs($fact_types);
$specs = mpull($specs, null, 'getType');
$map += $specs;
}
foreach ($fact_types as $type) {
if (empty($map[$type])) {
$map[$type] = new PhabricatorFactSimpleSpec($type);
}
}
return $map;
}
abstract public function getType();
public function getUnit() {
return null;
}
public function getName() {
- $type = $this->getType();
- return "Fact ({$type})";
+ return pht(
+ 'Fact (%s)',
+ $this->getType());
}
public function formatValueForDisplay(PhabricatorUser $user, $value) {
$unit = $this->getUnit();
switch ($unit) {
case self::UNIT_COUNT:
return number_format($value);
case self::UNIT_EPOCH:
return phabricator_datetime($value, $user);
default:
return $value;
}
}
}
diff --git a/src/applications/feed/PhabricatorFeedStoryPublisher.php b/src/applications/feed/PhabricatorFeedStoryPublisher.php
index 79b3b6cd5..4e91be852 100644
--- a/src/applications/feed/PhabricatorFeedStoryPublisher.php
+++ b/src/applications/feed/PhabricatorFeedStoryPublisher.php
@@ -1,288 +1,299 @@
<?php
final class PhabricatorFeedStoryPublisher {
private $relatedPHIDs;
private $storyType;
private $storyData;
private $storyTime;
private $storyAuthorPHID;
private $primaryObjectPHID;
private $subscribedPHIDs = array();
private $mailRecipientPHIDs = array();
private $notifyAuthor;
private $mailTags = array();
public function setMailTags(array $mail_tags) {
$this->mailTags = $mail_tags;
return $this;
}
public function getMailTags() {
return $this->mailTags;
}
public function setNotifyAuthor($notify_author) {
$this->notifyAuthor = $notify_author;
return $this;
}
public function getNotifyAuthor() {
return $this->notifyAuthor;
}
public function setRelatedPHIDs(array $phids) {
$this->relatedPHIDs = $phids;
return $this;
}
public function setSubscribedPHIDs(array $phids) {
$this->subscribedPHIDs = $phids;
return $this;
}
public function setPrimaryObjectPHID($phid) {
$this->primaryObjectPHID = $phid;
return $this;
}
public function setStoryType($story_type) {
$this->storyType = $story_type;
return $this;
}
public function setStoryData(array $data) {
$this->storyData = $data;
return $this;
}
public function setStoryTime($time) {
$this->storyTime = $time;
return $this;
}
public function setStoryAuthorPHID($phid) {
$this->storyAuthorPHID = $phid;
return $this;
}
public function setMailRecipientPHIDs(array $phids) {
$this->mailRecipientPHIDs = $phids;
return $this;
}
public function publish() {
$class = $this->storyType;
if (!$class) {
- throw new Exception('Call setStoryType() before publishing!');
+ throw new Exception(
+ pht(
+ 'Call %s before publishing!',
+ 'setStoryType()'));
}
if (!class_exists($class)) {
throw new Exception(
- "Story type must be a valid class name and must subclass ".
- "PhabricatorFeedStory. ".
- "'{$class}' is not a loadable class.");
+ pht(
+ "Story type must be a valid class name and must subclass %s. ".
+ "'%s' is not a loadable class.",
+ 'PhabricatorFeedStory',
+ $class));
}
if (!is_subclass_of($class, 'PhabricatorFeedStory')) {
throw new Exception(
- "Story type must be a valid class name and must subclass ".
- "PhabricatorFeedStory. ".
- "'{$class}' is not a subclass of PhabricatorFeedStory.");
+ pht(
+ "Story type must be a valid class name and must subclass %s. ".
+ "'%s' is not a subclass of %s.",
+ 'PhabricatorFeedStory',
+ $class,
+ 'PhabricatorFeedStory'));
}
$chrono_key = $this->generateChronologicalKey();
$story = new PhabricatorFeedStoryData();
$story->setStoryType($this->storyType);
$story->setStoryData($this->storyData);
$story->setAuthorPHID((string)$this->storyAuthorPHID);
$story->setChronologicalKey($chrono_key);
$story->save();
if ($this->relatedPHIDs) {
$ref = new PhabricatorFeedStoryReference();
$sql = array();
$conn = $ref->establishConnection('w');
foreach (array_unique($this->relatedPHIDs) as $phid) {
$sql[] = qsprintf(
$conn,
'(%s, %s)',
$phid,
$chrono_key);
}
queryfx(
$conn,
'INSERT INTO %T (objectPHID, chronologicalKey) VALUES %Q',
$ref->getTableName(),
implode(', ', $sql));
}
$subscribed_phids = $this->subscribedPHIDs;
if ($subscribed_phids) {
$subscribed_phids = $this->filterSubscribedPHIDs($subscribed_phids);
$this->insertNotifications($chrono_key, $subscribed_phids);
$this->sendNotification($chrono_key, $subscribed_phids);
}
PhabricatorWorker::scheduleTask(
'FeedPublisherWorker',
array(
'key' => $chrono_key,
));
return $story;
}
private function insertNotifications($chrono_key, array $subscribed_phids) {
if (!$this->primaryObjectPHID) {
throw new Exception(
- 'You must call setPrimaryObjectPHID() if you setSubscribedPHIDs()!');
+ pht(
+ 'You must call %s if you %s!',
+ 'setPrimaryObjectPHID()',
+ 'setSubscribedPHIDs()'));
}
$notif = new PhabricatorFeedStoryNotification();
$sql = array();
$conn = $notif->establishConnection('w');
$will_receive_mail = array_fill_keys($this->mailRecipientPHIDs, true);
foreach (array_unique($subscribed_phids) as $user_phid) {
if (isset($will_receive_mail[$user_phid])) {
$mark_read = 1;
} else {
$mark_read = 0;
}
$sql[] = qsprintf(
$conn,
'(%s, %s, %s, %d)',
$this->primaryObjectPHID,
$user_phid,
$chrono_key,
$mark_read);
}
if ($sql) {
queryfx(
$conn,
'INSERT INTO %T '.
'(primaryObjectPHID, userPHID, chronologicalKey, hasViewed) '.
'VALUES %Q',
$notif->getTableName(),
implode(', ', $sql));
}
}
private function sendNotification($chrono_key, array $subscribed_phids) {
$data = array(
'key' => (string)$chrono_key,
'type' => 'notification',
'subscribers' => $subscribed_phids,
);
PhabricatorNotificationClient::tryToPostMessage($data);
}
/**
* Remove PHIDs who should not receive notifications from a subscriber list.
*
* @param list<phid> List of potential subscribers.
* @return list<phid> List of actual subscribers.
*/
private function filterSubscribedPHIDs(array $phids) {
$phids = $this->expandRecipients($phids);
$tags = $this->getMailTags();
if ($tags) {
$all_prefs = id(new PhabricatorUserPreferences())->loadAllWhere(
'userPHID in (%Ls)',
$phids);
$all_prefs = mpull($all_prefs, null, 'getUserPHID');
}
$pref_default = PhabricatorUserPreferences::MAILTAG_PREFERENCE_EMAIL;
$pref_ignore = PhabricatorUserPreferences::MAILTAG_PREFERENCE_IGNORE;
$keep = array();
foreach ($phids as $phid) {
if (($phid == $this->storyAuthorPHID) && !$this->getNotifyAuthor()) {
continue;
}
if ($tags && isset($all_prefs[$phid])) {
$mailtags = $all_prefs[$phid]->getPreference(
PhabricatorUserPreferences::PREFERENCE_MAILTAGS,
array());
$notify = false;
foreach ($tags as $tag) {
// If this is set to "email" or "notify", notify the user.
if ((int)idx($mailtags, $tag, $pref_default) != $pref_ignore) {
$notify = true;
break;
}
}
if (!$notify) {
continue;
}
}
$keep[] = $phid;
}
return array_values(array_unique($keep));
}
private function expandRecipients(array $phids) {
return id(new PhabricatorMetaMTAMemberQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withPHIDs($phids)
->executeExpansion();
}
/**
* We generate a unique chronological key for each story type because we want
* to be able to page through the stream with a cursor (i.e., select stories
* after ID = X) so we can efficiently perform filtering after selecting data,
* and multiple stories with the same ID make this cumbersome without putting
* a bunch of logic in the client. We could use the primary key, but that
* would prevent publishing stories which happened in the past. Since it's
* potentially useful to do that (e.g., if you're importing another data
* source) build a unique key for each story which has chronological ordering.
*
* @return string A unique, time-ordered key which identifies the story.
*/
private function generateChronologicalKey() {
// Use the epoch timestamp for the upper 32 bits of the key. Default to
// the current time if the story doesn't have an explicit timestamp.
$time = nonempty($this->storyTime, time());
// Generate a random number for the lower 32 bits of the key.
$rand = head(unpack('L', Filesystem::readRandomBytes(4)));
// On 32-bit machines, we have to get creative.
if (PHP_INT_SIZE < 8) {
// We're on a 32-bit machine.
if (function_exists('bcadd')) {
// Try to use the 'bc' extension.
return bcadd(bcmul($time, bcpow(2, 32)), $rand);
} else {
// Do the math in MySQL. TODO: If we formalize a bc dependency, get
// rid of this.
$conn_r = id(new PhabricatorFeedStoryData())->establishConnection('r');
$result = queryfx_one(
$conn_r,
'SELECT (%d << 32) + %d as N',
$time,
$rand);
return $result['N'];
}
} else {
// This is a 64 bit machine, so we can just do the math.
return ($time << 32) + $rand;
}
}
}
diff --git a/src/applications/feed/builder/PhabricatorFeedBuilder.php b/src/applications/feed/builder/PhabricatorFeedBuilder.php
index 6f78b0dcf..63ec42077 100644
--- a/src/applications/feed/builder/PhabricatorFeedBuilder.php
+++ b/src/applications/feed/builder/PhabricatorFeedBuilder.php
@@ -1,103 +1,103 @@
<?php
final class PhabricatorFeedBuilder {
private $stories;
private $framed;
private $hovercards = false;
private $noDataString;
public function __construct(array $stories) {
assert_instances_of($stories, 'PhabricatorFeedStory');
$this->stories = $stories;
}
public function setFramed($framed) {
$this->framed = $framed;
return $this;
}
public function setUser(PhabricatorUser $user) {
$this->user = $user;
return $this;
}
public function setShowHovercards($hover) {
$this->hovercards = $hover;
return $this;
}
public function setNoDataString($string) {
$this->noDataString = $string;
return $this;
}
public function buildView() {
if (!$this->user) {
- throw new Exception('Call setUser() before buildView()!');
+ throw new PhutilInvalidStateException('setUser');
}
$user = $this->user;
$stories = $this->stories;
$null_view = new AphrontNullView();
require_celerity_resource('phabricator-feed-css');
$last_date = null;
foreach ($stories as $story) {
$story->setFramed($this->framed);
$story->setHovercard($this->hovercards);
$date = ucfirst(phabricator_relative_date($story->getEpoch(), $user));
if ($date !== $last_date) {
if ($last_date !== null) {
$null_view->appendChild(
phutil_tag_div('phabricator-feed-story-date-separator'));
}
$last_date = $date;
$header = new PHUIActionHeaderView();
$header->setHeaderTitle($date);
$null_view->appendChild($header);
}
try {
$view = $story->renderView();
$view->setUser($user);
$view = $view->render();
} catch (Exception $ex) {
// If rendering failed for any reason, don't fail the entire feed,
// just this one story.
$view = id(new PHUIFeedStoryView())
->setUser($user)
->setChronologicalKey($story->getChronologicalKey())
->setEpoch($story->getEpoch())
->setTitle(
pht('Feed Story Failed to Render (%s)', get_class($story)))
->appendChild(pht('%s: %s', get_class($ex), $ex->getMessage()));
}
$null_view->appendChild($view);
}
if (empty($stories)) {
$nodatastring = pht('No Stories.');
if ($this->noDataString) {
$nodatastring = $this->noDataString;
}
$view = id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_NODATA)
->appendChild($nodatastring);
$null_view->appendChild($view);
}
return id(new AphrontNullView())
->appendChild($null_view->render());
}
}
diff --git a/src/applications/feed/conduit/FeedPublishConduitAPIMethod.php b/src/applications/feed/conduit/FeedPublishConduitAPIMethod.php
index a082e36fe..5f83e73b3 100644
--- a/src/applications/feed/conduit/FeedPublishConduitAPIMethod.php
+++ b/src/applications/feed/conduit/FeedPublishConduitAPIMethod.php
@@ -1,49 +1,49 @@
<?php
final class FeedPublishConduitAPIMethod extends FeedConduitAPIMethod {
public function getAPIMethodName() {
return 'feed.publish';
}
public function getMethodStatus() {
return self::METHOD_STATUS_UNSTABLE;
}
public function getMethodDescription() {
- return 'Publish a story to the feed.';
+ return pht('Publish a story to the feed.');
}
protected function defineParamTypes() {
return array(
'type' => 'required string',
'data' => 'required dict',
'time' => 'optional int',
);
}
protected function defineReturnType() {
return 'nonempty phid';
}
protected function execute(ConduitAPIRequest $request) {
$type = $request->getValue('type');
$data = $request->getValue('data');
$time = $request->getValue('time');
$author_phid = $request->getUser()->getPHID();
$phids = array($author_phid);
$publisher = new PhabricatorFeedStoryPublisher();
$publisher->setStoryType($type);
$publisher->setStoryData($data);
$publisher->setStoryTime($time);
$publisher->setRelatedPHIDs($phids);
$publisher->setStoryAuthorPHID($author_phid);
$data = $publisher->publish();
return $data->getPHID();
}
}
diff --git a/src/applications/feed/conduit/FeedQueryConduitAPIMethod.php b/src/applications/feed/conduit/FeedQueryConduitAPIMethod.php
index 02efb83f7..661b70a7a 100644
--- a/src/applications/feed/conduit/FeedQueryConduitAPIMethod.php
+++ b/src/applications/feed/conduit/FeedQueryConduitAPIMethod.php
@@ -1,145 +1,147 @@
<?php
final class FeedQueryConduitAPIMethod extends FeedConduitAPIMethod {
public function getAPIMethodName() {
return 'feed.query';
}
public function getMethodStatus() {
return self::METHOD_STATUS_UNSTABLE;
}
public function getMethodDescription() {
- return 'Query the feed for stories';
+ return pht('Query the feed for stories');
}
private function getDefaultLimit() {
return 100;
}
protected function defineParamTypes() {
return array(
'filterPHIDs' => 'optional list <phid>',
'limit' => 'optional int (default '.$this->getDefaultLimit().')',
'after' => 'optional int',
'before' => 'optional int',
'view' => 'optional string (data, html, html-summary, text)',
);
}
private function getSupportedViewTypes() {
return array(
- 'html' => 'Full HTML presentation of story',
- 'data' => 'Dictionary with various data of the story',
- 'html-summary' => 'Story contains only the title of the story',
- 'text' => 'Simple one-line plain text representation of story',
+ 'html' => pht('Full HTML presentation of story'),
+ 'data' => pht('Dictionary with various data of the story'),
+ 'html-summary' => pht('Story contains only the title of the story'),
+ 'text' => pht('Simple one-line plain text representation of story'),
);
}
protected function defineErrorTypes() {
$view_types = array_keys($this->getSupportedViewTypes());
$view_types = implode(', ', $view_types);
return array(
'ERR-UNKNOWN-TYPE' =>
- 'Unsupported view type, possibles are: '.$view_types,
+ pht(
+ 'Unsupported view type, possibles are: %s',
+ $view_types),
);
}
protected function defineReturnType() {
return 'nonempty dict';
}
protected function execute(ConduitAPIRequest $request) {
$results = array();
$user = $request->getUser();
$view_type = $request->getValue('view');
if (!$view_type) {
$view_type = 'data';
}
$limit = $request->getValue('limit');
if (!$limit) {
$limit = $this->getDefaultLimit();
}
$filter_phids = $request->getValue('filterPHIDs');
if (!$filter_phids) {
$filter_phids = array();
}
$query = id(new PhabricatorFeedQuery())
->setLimit($limit)
->setFilterPHIDs($filter_phids)
->setViewer($user);
$after = $request->getValue('after');
if (strlen($after)) {
$query->setAfterID($after);
}
$before = $request->getValue('before');
if (strlen($before)) {
$query->setBeforeID($before);
}
$stories = $query->execute();
if ($stories) {
foreach ($stories as $story) {
$story_data = $story->getStoryData();
$data = null;
try {
$view = $story->renderView();
} catch (Exception $ex) {
// When stories fail to render, just fail that story.
phlog($ex);
continue;
}
$view->setEpoch($story->getEpoch());
$view->setUser($user);
switch ($view_type) {
case 'html':
$data = $view->render();
break;
case 'html-summary':
$data = $view->render();
break;
case 'data':
$data = array(
'class' => $story_data->getStoryType(),
'epoch' => $story_data->getEpoch(),
'authorPHID' => $story_data->getAuthorPHID(),
'chronologicalKey' => $story_data->getChronologicalKey(),
'data' => $story_data->getStoryData(),
);
break;
case 'text':
$data = array(
'class' => $story_data->getStoryType(),
'epoch' => $story_data->getEpoch(),
'authorPHID' => $story_data->getAuthorPHID(),
'chronologicalKey' => $story_data->getChronologicalKey(),
'objectPHID' => $story->getPrimaryObjectPHID(),
'text' => $story->renderText(),
);
break;
default:
throw new ConduitException('ERR-UNKNOWN-TYPE');
}
$results[$story_data->getPHID()] = $data;
}
}
return $results;
}
}
diff --git a/src/applications/feed/query/PhabricatorFeedQuery.php b/src/applications/feed/query/PhabricatorFeedQuery.php
index 4ee91859c..b9c7d099a 100644
--- a/src/applications/feed/query/PhabricatorFeedQuery.php
+++ b/src/applications/feed/query/PhabricatorFeedQuery.php
@@ -1,126 +1,127 @@
<?php
final class PhabricatorFeedQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $filterPHIDs;
private $chronologicalKeys;
public function setFilterPHIDs(array $phids) {
$this->filterPHIDs = $phids;
return $this;
}
public function withChronologicalKeys(array $keys) {
$this->chronologicalKeys = $keys;
return $this;
}
protected function loadPage() {
$story_table = new PhabricatorFeedStoryData();
$conn = $story_table->establishConnection('r');
$data = queryfx_all(
$conn,
'SELECT story.* FROM %T story %Q %Q %Q %Q %Q',
$story_table->getTableName(),
$this->buildJoinClause($conn),
$this->buildWhereClause($conn),
$this->buildGroupClause($conn),
$this->buildOrderClause($conn),
$this->buildLimitClause($conn));
return $data;
}
protected function willFilterPage(array $data) {
return PhabricatorFeedStory::loadAllFromRows($data, $this->getViewer());
}
protected function buildJoinClause(AphrontDatabaseConnection $conn_r) {
// NOTE: We perform this join unconditionally (even if we have no filter
// PHIDs) to omit rows which have no story references. These story data
// rows are notifications or realtime alerts.
$ref_table = new PhabricatorFeedStoryReference();
return qsprintf(
$conn_r,
'JOIN %T ref ON ref.chronologicalKey = story.chronologicalKey',
$ref_table->getTableName());
}
protected function buildWhereClause(AphrontDatabaseConnection $conn_r) {
$where = array();
if ($this->filterPHIDs) {
$where[] = qsprintf(
$conn_r,
'ref.objectPHID IN (%Ls)',
$this->filterPHIDs);
}
if ($this->chronologicalKeys) {
// NOTE: We want to use integers in the query so we can take advantage
// of keys, but can't use %d on 32-bit systems. Make sure all the keys
// are integers and then format them raw.
$keys = $this->chronologicalKeys;
foreach ($keys as $key) {
if (!ctype_digit($key)) {
- throw new Exception("Key '{$key}' is not a valid chronological key!");
+ throw new Exception(
+ pht("Key '%s' is not a valid chronological key!", $key));
}
}
$where[] = qsprintf(
$conn_r,
'ref.chronologicalKey IN (%Q)',
implode(', ', $keys));
}
$where[] = $this->buildPagingClause($conn_r);
return $this->formatWhereClause($where);
}
protected function buildGroupClause(AphrontDatabaseConnection $conn_r) {
if ($this->filterPHIDs) {
return qsprintf($conn_r, 'GROUP BY ref.chronologicalKey');
} else {
return qsprintf($conn_r, 'GROUP BY story.chronologicalKey');
}
}
protected function getDefaultOrderVector() {
return array('key');
}
public function getOrderableColumns() {
$table = ($this->filterPHIDs ? 'ref' : 'story');
return array(
'key' => array(
'table' => $table,
'column' => 'chronologicalKey',
'type' => 'int',
'unique' => true,
),
);
}
protected function getPagingValueMap($cursor, array $keys) {
return array(
'key' => $cursor,
);
}
protected function getResultCursor($item) {
if ($item instanceof PhabricatorFeedStory) {
return $item->getChronologicalKey();
}
return $item['chronologicalKey'];
}
public function getQueryApplicationClass() {
return 'PhabricatorFeedApplication';
}
}
diff --git a/src/applications/feed/story/PhabricatorFeedStory.php b/src/applications/feed/story/PhabricatorFeedStory.php
index f94c2601e..253757b8d 100644
--- a/src/applications/feed/story/PhabricatorFeedStory.php
+++ b/src/applications/feed/story/PhabricatorFeedStory.php
@@ -1,532 +1,534 @@
<?php
/**
* Manages rendering and aggregation of a story. A story is an event (like a
* user adding a comment) which may be represented in different forms on
* different channels (like feed, notifications and realtime alerts).
*
* @task load Loading Stories
* @task policy Policy Implementation
*/
abstract class PhabricatorFeedStory
implements
PhabricatorPolicyInterface,
PhabricatorMarkupInterface {
private $data;
private $hasViewed;
private $framed;
private $hovercard = false;
private $renderingTarget = PhabricatorApplicationTransaction::TARGET_HTML;
private $handles = array();
private $objects = array();
private $projectPHIDs = array();
private $markupFieldOutput = array();
/* -( Loading Stories )---------------------------------------------------- */
/**
* Given @{class:PhabricatorFeedStoryData} rows, load them into objects and
* construct appropriate @{class:PhabricatorFeedStory} wrappers for each
* data row.
*
* @param list<dict> List of @{class:PhabricatorFeedStoryData} rows from the
* database.
* @return list<PhabricatorFeedStory> List of @{class:PhabricatorFeedStory}
* objects.
* @task load
*/
public static function loadAllFromRows(array $rows, PhabricatorUser $viewer) {
$stories = array();
$data = id(new PhabricatorFeedStoryData())->loadAllFromArray($rows);
foreach ($data as $story_data) {
$class = $story_data->getStoryType();
try {
$ok =
class_exists($class) &&
is_subclass_of($class, __CLASS__);
} catch (PhutilMissingSymbolException $ex) {
$ok = false;
}
// If the story type isn't a valid class or isn't a subclass of
// PhabricatorFeedStory, decline to load it.
if (!$ok) {
continue;
}
$key = $story_data->getChronologicalKey();
$stories[$key] = newv($class, array($story_data));
}
$object_phids = array();
$key_phids = array();
foreach ($stories as $key => $story) {
$phids = array();
foreach ($story->getRequiredObjectPHIDs() as $phid) {
$phids[$phid] = true;
}
if ($story->getPrimaryObjectPHID()) {
$phids[$story->getPrimaryObjectPHID()] = true;
}
$key_phids[$key] = $phids;
$object_phids += $phids;
}
$objects = id(new PhabricatorObjectQuery())
->setViewer($viewer)
->withPHIDs(array_keys($object_phids))
->execute();
foreach ($key_phids as $key => $phids) {
if (!$phids) {
continue;
}
$story_objects = array_select_keys($objects, array_keys($phids));
if (count($story_objects) != count($phids)) {
// An object this story requires either does not exist or is not visible
// to the user. Decline to render the story.
unset($stories[$key]);
unset($key_phids[$key]);
continue;
}
$stories[$key]->setObjects($story_objects);
}
// If stories are about PhabricatorProjectInterface objects, load the
// projects the objects are a part of so we can render project tags
// on the stories.
$project_phids = array();
foreach ($objects as $object) {
if ($object instanceof PhabricatorProjectInterface) {
$project_phids[$object->getPHID()] = array();
}
}
if ($project_phids) {
$edge_query = id(new PhabricatorEdgeQuery())
->withSourcePHIDs(array_keys($project_phids))
->withEdgeTypes(
array(
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST,
));
$edge_query->execute();
foreach ($project_phids as $phid => $ignored) {
$project_phids[$phid] = $edge_query->getDestinationPHIDs(array($phid));
}
}
$handle_phids = array();
foreach ($stories as $key => $story) {
foreach ($story->getRequiredHandlePHIDs() as $phid) {
$key_phids[$key][$phid] = true;
}
if ($story->getAuthorPHID()) {
$key_phids[$key][$story->getAuthorPHID()] = true;
}
$object_phid = $story->getPrimaryObjectPHID();
$object_project_phids = idx($project_phids, $object_phid, array());
$story->setProjectPHIDs($object_project_phids);
foreach ($object_project_phids as $dst) {
$key_phids[$key][$dst] = true;
}
$handle_phids += $key_phids[$key];
}
$handles = id(new PhabricatorHandleQuery())
->setViewer($viewer)
->withPHIDs(array_keys($handle_phids))
->execute();
foreach ($key_phids as $key => $phids) {
if (!$phids) {
continue;
}
$story_handles = array_select_keys($handles, array_keys($phids));
$stories[$key]->setHandles($story_handles);
}
// Load and process story markup blocks.
$engine = new PhabricatorMarkupEngine();
$engine->setViewer($viewer);
foreach ($stories as $story) {
foreach ($story->getFieldStoryMarkupFields() as $field) {
$engine->addObject($story, $field);
}
}
$engine->process();
foreach ($stories as $story) {
foreach ($story->getFieldStoryMarkupFields() as $field) {
$story->setMarkupFieldOutput(
$field,
$engine->getOutput($story, $field));
}
}
return $stories;
}
public function setMarkupFieldOutput($field, $output) {
$this->markupFieldOutput[$field] = $output;
return $this;
}
public function getMarkupFieldOutput($field) {
if (!array_key_exists($field, $this->markupFieldOutput)) {
throw new Exception(
pht(
'Trying to retrieve markup field key "%s", but this feed story '.
'did not request it be rendered.',
$field));
}
return $this->markupFieldOutput[$field];
}
public function setHovercard($hover) {
$this->hovercard = $hover;
return $this;
}
public function setRenderingTarget($target) {
$this->validateRenderingTarget($target);
$this->renderingTarget = $target;
return $this;
}
public function getRenderingTarget() {
return $this->renderingTarget;
}
private function validateRenderingTarget($target) {
switch ($target) {
case PhabricatorApplicationTransaction::TARGET_HTML:
case PhabricatorApplicationTransaction::TARGET_TEXT:
break;
default:
- throw new Exception('Unknown rendering target: '.$target);
+ throw new Exception(pht('Unknown rendering target: %s', $target));
break;
}
}
public function setObjects(array $objects) {
$this->objects = $objects;
return $this;
}
public function getObject($phid) {
$object = idx($this->objects, $phid);
if (!$object) {
throw new Exception(
- "Story is asking for an object it did not request ('{$phid}')!");
+ pht(
+ "Story is asking for an object it did not request ('%s')!",
+ $phid));
}
return $object;
}
public function getPrimaryObject() {
$phid = $this->getPrimaryObjectPHID();
if (!$phid) {
- throw new Exception('Story has no primary object!');
+ throw new Exception(pht('Story has no primary object!'));
}
return $this->getObject($phid);
}
public function getPrimaryObjectPHID() {
return null;
}
final public function __construct(PhabricatorFeedStoryData $data) {
$this->data = $data;
}
abstract public function renderView();
public function renderAsTextForDoorkeeper(
DoorkeeperFeedStoryPublisher $publisher) {
// TODO: This (and text rendering) should be properly abstract and
// universal. However, this is far less bad than it used to be, and we
// need to clean up more old feed code to really make this reasonable.
return pht(
'(Unable to render story of class %s for Doorkeeper.)',
get_class($this));
}
public function getRequiredHandlePHIDs() {
return array();
}
public function getRequiredObjectPHIDs() {
return array();
}
public function setHasViewed($has_viewed) {
$this->hasViewed = $has_viewed;
return $this;
}
public function getHasViewed() {
return $this->hasViewed;
}
final public function setFramed($framed) {
$this->framed = $framed;
return $this;
}
final public function setHandles(array $handles) {
assert_instances_of($handles, 'PhabricatorObjectHandle');
$this->handles = $handles;
return $this;
}
final protected function getObjects() {
return $this->objects;
}
final protected function getHandles() {
return $this->handles;
}
final protected function getHandle($phid) {
if (isset($this->handles[$phid])) {
if ($this->handles[$phid] instanceof PhabricatorObjectHandle) {
return $this->handles[$phid];
}
}
$handle = new PhabricatorObjectHandle();
$handle->setPHID($phid);
- $handle->setName("Unloaded Object '{$phid}'");
+ $handle->setName(pht("Unloaded Object '%s'", $phid));
return $handle;
}
final public function getStoryData() {
return $this->data;
}
final public function getEpoch() {
return $this->getStoryData()->getEpoch();
}
final public function getChronologicalKey() {
return $this->getStoryData()->getChronologicalKey();
}
final public function getValue($key, $default = null) {
return $this->getStoryData()->getValue($key, $default);
}
final public function getAuthorPHID() {
return $this->getStoryData()->getAuthorPHID();
}
final protected function renderHandleList(array $phids) {
$items = array();
foreach ($phids as $phid) {
$items[] = $this->linkTo($phid);
}
$list = null;
switch ($this->getRenderingTarget()) {
case PhabricatorApplicationTransaction::TARGET_TEXT:
$list = implode(', ', $items);
break;
case PhabricatorApplicationTransaction::TARGET_HTML:
$list = phutil_implode_html(', ', $items);
break;
}
return $list;
}
final protected function linkTo($phid) {
$handle = $this->getHandle($phid);
switch ($this->getRenderingTarget()) {
case PhabricatorApplicationTransaction::TARGET_TEXT:
return $handle->getLinkName();
}
// NOTE: We render our own link here to customize the styling and add
// the '_top' target for framed feeds.
$class = null;
if ($handle->getType() == PhabricatorPeopleUserPHIDType::TYPECONST) {
$class = 'phui-link-person';
}
return javelin_tag(
'a',
array(
'href' => $handle->getURI(),
'target' => $this->framed ? '_top' : null,
'sigil' => $this->hovercard ? 'hovercard' : null,
'meta' => $this->hovercard ? array('hoverPHID' => $phid) : null,
'class' => $class,
),
$handle->getLinkName());
}
final protected function renderString($str) {
switch ($this->getRenderingTarget()) {
case PhabricatorApplicationTransaction::TARGET_TEXT:
return $str;
case PhabricatorApplicationTransaction::TARGET_HTML:
return phutil_tag('strong', array(), $str);
}
}
final public function renderSummary($text, $len = 128) {
if ($len) {
$text = id(new PhutilUTF8StringTruncator())
->setMaximumGlyphs($len)
->truncateString($text);
}
switch ($this->getRenderingTarget()) {
case PhabricatorApplicationTransaction::TARGET_HTML:
$text = phutil_escape_html_newlines($text);
break;
}
return $text;
}
public function getNotificationAggregations() {
return array();
}
protected function newStoryView() {
$view = id(new PHUIFeedStoryView())
->setChronologicalKey($this->getChronologicalKey())
->setEpoch($this->getEpoch())
->setViewed($this->getHasViewed());
$project_phids = $this->getProjectPHIDs();
if ($project_phids) {
$view->setTags($this->renderHandleList($project_phids));
}
return $view;
}
public function setProjectPHIDs(array $phids) {
$this->projectPHIDs = $phids;
return $this;
}
public function getProjectPHIDs() {
return $this->projectPHIDs;
}
public function getFieldStoryMarkupFields() {
return array();
}
/* -( PhabricatorPolicyInterface Implementation )-------------------------- */
public function getPHID() {
return null;
}
/**
* @task policy
*/
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
);
}
/**
* @task policy
*/
public function getPolicy($capability) {
$policy_object = $this->getPrimaryPolicyObject();
if ($policy_object) {
return $policy_object->getPolicy($capability);
}
// TODO: Remove this once all objects are policy-aware. For now, keep
// respecting the `feed.public` setting.
return PhabricatorEnv::getEnvConfig('feed.public')
? PhabricatorPolicies::POLICY_PUBLIC
: PhabricatorPolicies::POLICY_USER;
}
/**
* @task policy
*/
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
$policy_object = $this->getPrimaryPolicyObject();
if ($policy_object) {
return $policy_object->hasAutomaticCapability($capability, $viewer);
}
return false;
}
public function describeAutomaticCapability($capability) {
return null;
}
/**
* Get the policy object this story is about, if such a policy object
* exists.
*
* @return PhabricatorPolicyInterface|null Policy object, if available.
* @task policy
*/
private function getPrimaryPolicyObject() {
$primary_phid = $this->getPrimaryObjectPHID();
if (empty($this->objects[$primary_phid])) {
$object = $this->objects[$primary_phid];
if ($object instanceof PhabricatorPolicyInterface) {
return $object;
}
}
return null;
}
/* -( PhabricatorMarkupInterface Implementation )--------------------------- */
public function getMarkupFieldKey($field) {
return 'feed:'.$this->getChronologicalKey().':'.$field;
}
public function newMarkupEngine($field) {
return PhabricatorMarkupEngine::newMarkupEngine(array());
}
public function getMarkupText($field) {
throw new PhutilMethodNotImplementedException();
}
public function didMarkupText(
$field,
$output,
PhutilMarkupEngine $engine) {
return $output;
}
public function shouldUseMarkupCache($field) {
return true;
}
}
diff --git a/src/applications/feed/worker/FeedPushWorker.php b/src/applications/feed/worker/FeedPushWorker.php
index b406b76ee..90407f6a7 100644
--- a/src/applications/feed/worker/FeedPushWorker.php
+++ b/src/applications/feed/worker/FeedPushWorker.php
@@ -1,22 +1,22 @@
<?php
abstract class FeedPushWorker extends PhabricatorWorker {
protected function loadFeedStory() {
$task_data = $this->getTaskData();
$key = $task_data['key'];
$story = id(new PhabricatorFeedQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withChronologicalKeys(array($key))
->executeOne();
if (!$story) {
throw new PhabricatorWorkerPermanentFailureException(
- 'Feed story does not exist.');
+ pht('Feed story does not exist.'));
}
return $story;
}
}
diff --git a/src/applications/files/application/PhabricatorFilesApplication.php b/src/applications/files/application/PhabricatorFilesApplication.php
index ed4801ace..b80a8e1ae 100644
--- a/src/applications/files/application/PhabricatorFilesApplication.php
+++ b/src/applications/files/application/PhabricatorFilesApplication.php
@@ -1,115 +1,114 @@
<?php
final class PhabricatorFilesApplication extends PhabricatorApplication {
public function getBaseURI() {
return '/file/';
}
public function getName() {
return pht('Files');
}
public function getShortDescription() {
return pht('Store and Share Files');
}
public function getFontIcon() {
return 'fa-file';
}
public function getTitleGlyph() {
return "\xE2\x87\xAA";
}
public function getFlavorText() {
return pht('Blob store for Pokemon pictures.');
}
public function getApplicationGroup() {
return self::GROUP_UTILITIES;
}
public function canUninstall() {
return false;
}
public function getRemarkupRules() {
return array(
new PhabricatorEmbedFileRemarkupRule(),
);
}
public function supportsEmailIntegration() {
return true;
}
public function getAppEmailBlurb() {
return pht(
'Send emails with file attachments to these addresses to upload '.
'files. %s',
phutil_tag(
'a',
array(
'href' => $this->getInboundEmailSupportLink(),
),
pht('Learn More')));
}
protected function getCustomCapabilities() {
return array(
FilesDefaultViewCapability::CAPABILITY => array(
- 'caption' => pht(
- 'Default view policy for newly created files.'),
+ 'caption' => pht('Default view policy for newly created files.'),
),
);
}
public function getRoutes() {
return array(
'/F(?P<id>[1-9]\d*)' => 'PhabricatorFileInfoController',
'/file/' => array(
'(query/(?P<key>[^/]+)/)?' => 'PhabricatorFileListController',
'upload/' => 'PhabricatorFileUploadController',
'dropupload/' => 'PhabricatorFileDropUploadController',
'compose/' => 'PhabricatorFileComposeController',
'comment/(?P<id>[1-9]\d*)/' => 'PhabricatorFileCommentController',
'delete/(?P<id>[1-9]\d*)/' => 'PhabricatorFileDeleteController',
'edit/(?P<id>[1-9]\d*)/' => 'PhabricatorFileEditController',
'info/(?P<phid>[^/]+)/' => 'PhabricatorFileInfoController',
'data/'.
'(?:@(?P<instance>[^/]+)/)?'.
'(?P<key>[^/]+)/'.
'(?P<phid>[^/]+)/'.
'(?:(?P<token>[^/]+)/)?'.
'.*'
=> 'PhabricatorFileDataController',
'proxy/' => 'PhabricatorFileProxyController',
'xform/'.
'(?:@(?P<instance>[^/]+)/)?'.
'(?P<transform>[^/]+)/'.
'(?P<phid>[^/]+)/'.
'(?P<key>[^/]+)/'
=> 'PhabricatorFileTransformController',
'transforms/(?P<id>[1-9]\d*)/' =>
'PhabricatorFileTransformListController',
'uploaddialog/' => 'PhabricatorFileUploadDialogController',
'download/(?P<phid>[^/]+)/' => 'PhabricatorFileDialogController',
),
);
}
public function getMailCommandObjects() {
return array(
'file' => array(
'name' => pht('Email Commands: Files'),
'header' => pht('Interacting with Files'),
'object' => new PhabricatorFile(),
'summary' => pht(
'This page documents the commands you can use to interact with '.
'files.'),
),
);
}
}
diff --git a/src/applications/files/conduit/FileConduitAPIMethod.php b/src/applications/files/conduit/FileConduitAPIMethod.php
index 241930321..420ed65bf 100644
--- a/src/applications/files/conduit/FileConduitAPIMethod.php
+++ b/src/applications/files/conduit/FileConduitAPIMethod.php
@@ -1,120 +1,119 @@
<?php
abstract class FileConduitAPIMethod extends ConduitAPIMethod {
final public function getApplication() {
return PhabricatorApplication::getByClass('PhabricatorFilesApplication');
}
protected function loadFileByPHID(PhabricatorUser $viewer, $file_phid) {
$file = id(new PhabricatorFileQuery())
->setViewer($viewer)
->withPHIDs(array($file_phid))
->executeOne();
if (!$file) {
throw new Exception(pht('No such file "%s"!', $file_phid));
}
return $file;
}
protected function loadFileChunks(
PhabricatorUser $viewer,
PhabricatorFile $file) {
return $this->newChunkQuery($viewer, $file)
->execute();
}
protected function loadFileChunkForUpload(
PhabricatorUser $viewer,
PhabricatorFile $file,
$start,
$end) {
$start = (int)$start;
$end = (int)$end;
$chunks = $this->newChunkQuery($viewer, $file)
->withByteRange($start, $end)
->execute();
if (!$chunks) {
throw new Exception(
pht(
'There are no file data chunks in byte range %d - %d.',
$start,
$end));
}
if (count($chunks) !== 1) {
phlog($chunks);
throw new Exception(
pht(
'There are multiple chunks in byte range %d - %d.',
$start,
$end));
}
$chunk = head($chunks);
if ($chunk->getByteStart() != $start) {
throw new Exception(
pht(
'Chunk start byte is %d, not %d.',
$chunk->getByteStart(),
$start));
}
if ($chunk->getByteEnd() != $end) {
throw new Exception(
pht(
'Chunk end byte is %d, not %d.',
$chunk->getByteEnd(),
$end));
}
if ($chunk->getDataFilePHID()) {
throw new Exception(
- pht(
- 'Chunk has already been uploaded.'));
+ pht('Chunk has already been uploaded.'));
}
return $chunk;
}
protected function decodeBase64($data) {
$data = base64_decode($data, $strict = true);
if ($data === false) {
throw new Exception(pht('Unable to decode base64 data!'));
}
return $data;
}
protected function loadAnyMissingChunk(
PhabricatorUser $viewer,
PhabricatorFile $file) {
return $this->newChunkQuery($viewer, $file)
->withIsComplete(false)
->setLimit(1)
->execute();
}
private function newChunkQuery(
PhabricatorUser $viewer,
PhabricatorFile $file) {
$engine = $file->instantiateStorageEngine();
if (!$engine->isChunkEngine()) {
throw new Exception(
pht(
'File "%s" does not have chunks!',
$file->getPHID()));
}
return id(new PhabricatorFileChunkQuery())
->setViewer($viewer)
->withChunkHandles(array($file->getStorageHandle()));
}
}
diff --git a/src/applications/files/conduit/FileDownloadConduitAPIMethod.php b/src/applications/files/conduit/FileDownloadConduitAPIMethod.php
index 67e9d1f25..3a3867ac9 100644
--- a/src/applications/files/conduit/FileDownloadConduitAPIMethod.php
+++ b/src/applications/files/conduit/FileDownloadConduitAPIMethod.php
@@ -1,43 +1,43 @@
<?php
final class FileDownloadConduitAPIMethod extends FileConduitAPIMethod {
public function getAPIMethodName() {
return 'file.download';
}
public function getMethodDescription() {
- return 'Download a file from the server.';
+ return pht('Download a file from the server.');
}
protected function defineParamTypes() {
return array(
'phid' => 'required phid',
);
}
protected function defineReturnType() {
return 'nonempty base64-bytes';
}
protected function defineErrorTypes() {
return array(
- 'ERR-BAD-PHID' => 'No such file exists.',
+ 'ERR-BAD-PHID' => pht('No such file exists.'),
);
}
protected function execute(ConduitAPIRequest $request) {
$phid = $request->getValue('phid');
$file = id(new PhabricatorFileQuery())
->setViewer($request->getUser())
->withPHIDs(array($phid))
->executeOne();
if (!$file) {
throw new ConduitException('ERR-BAD-PHID');
}
return base64_encode($file->loadFileData());
}
}
diff --git a/src/applications/files/conduit/FileInfoConduitAPIMethod.php b/src/applications/files/conduit/FileInfoConduitAPIMethod.php
index 7b8cecbdf..5f1bb6e93 100644
--- a/src/applications/files/conduit/FileInfoConduitAPIMethod.php
+++ b/src/applications/files/conduit/FileInfoConduitAPIMethod.php
@@ -1,64 +1,64 @@
<?php
final class FileInfoConduitAPIMethod extends FileConduitAPIMethod {
public function getAPIMethodName() {
return 'file.info';
}
public function getMethodDescription() {
- return 'Get information about a file.';
+ return pht('Get information about a file.');
}
protected function defineParamTypes() {
return array(
'phid' => 'optional phid',
'id' => 'optional id',
);
}
protected function defineReturnType() {
return 'nonempty dict';
}
protected function defineErrorTypes() {
return array(
- 'ERR-NOT-FOUND' => 'No such file exists.',
+ 'ERR-NOT-FOUND' => pht('No such file exists.'),
);
}
protected function execute(ConduitAPIRequest $request) {
$phid = $request->getValue('phid');
$id = $request->getValue('id');
$query = id(new PhabricatorFileQuery())
->setViewer($request->getUser());
if ($id) {
$query->withIDs(array($id));
} else {
$query->withPHIDs(array($phid));
}
$file = $query->executeOne();
if (!$file) {
throw new ConduitException('ERR-NOT-FOUND');
}
$uri = $file->getInfoURI();
return array(
'id' => $file->getID(),
'phid' => $file->getPHID(),
'objectName' => 'F'.$file->getID(),
'name' => $file->getName(),
'mimeType' => $file->getMimeType(),
'byteSize' => $file->getByteSize(),
'authorPHID' => $file->getAuthorPHID(),
'dateCreated' => $file->getDateCreated(),
'dateModified' => $file->getDateModified(),
'uri' => PhabricatorEnv::getProductionURI($uri),
);
}
}
diff --git a/src/applications/files/conduit/FileUploadConduitAPIMethod.php b/src/applications/files/conduit/FileUploadConduitAPIMethod.php
index 594b53bcc..ac93b7d39 100644
--- a/src/applications/files/conduit/FileUploadConduitAPIMethod.php
+++ b/src/applications/files/conduit/FileUploadConduitAPIMethod.php
@@ -1,49 +1,49 @@
<?php
final class FileUploadConduitAPIMethod extends FileConduitAPIMethod {
public function getAPIMethodName() {
return 'file.upload';
}
public function getMethodDescription() {
- return 'Upload a file to the server.';
+ return pht('Upload a file to the server.');
}
protected function defineParamTypes() {
return array(
'data_base64' => 'required nonempty base64-bytes',
'name' => 'optional string',
'viewPolicy' => 'optional valid policy string or <phid>',
'canCDN' => 'optional bool',
);
}
protected function defineReturnType() {
return 'nonempty guid';
}
protected function execute(ConduitAPIRequest $request) {
$viewer = $request->getUser();
$name = $request->getValue('name');
$can_cdn = $request->getValue('canCDN');
$view_policy = $request->getValue('viewPolicy');
$data = $request->getValue('data_base64');
$data = $this->decodeBase64($data);
$file = PhabricatorFile::newFromFileData(
$data,
array(
'name' => $name,
'authorPHID' => $viewer->getPHID(),
'viewPolicy' => $view_policy,
'canCDN' => $can_cdn,
'isExplicitUpload' => true,
));
return $file->getPHID();
}
}
diff --git a/src/applications/files/conduit/FileUploadHashConduitAPIMethod.php b/src/applications/files/conduit/FileUploadHashConduitAPIMethod.php
index 2ab13dc35..135e018a2 100644
--- a/src/applications/files/conduit/FileUploadHashConduitAPIMethod.php
+++ b/src/applications/files/conduit/FileUploadHashConduitAPIMethod.php
@@ -1,43 +1,43 @@
<?php
final class FileUploadHashConduitAPIMethod extends FileConduitAPIMethod {
public function getAPIMethodName() {
// TODO: Deprecate this in favor of `file.allocate`.
return 'file.uploadhash';
}
public function getMethodDescription() {
- return 'Upload a file to the server using content hash.';
+ return pht('Upload a file to the server using content hash.');
}
protected function defineParamTypes() {
return array(
'hash' => 'required nonempty string',
'name' => 'required nonempty string',
);
}
protected function defineReturnType() {
return 'phid or null';
}
protected function execute(ConduitAPIRequest $request) {
$hash = $request->getValue('hash');
$name = $request->getValue('name');
$user = $request->getUser();
$file = PhabricatorFile::newFileFromContentHash(
$hash,
array(
'name' => $name,
'authorPHID' => $user->getPHID(),
));
if ($file) {
return $file->getPHID();
}
return $file;
}
}
diff --git a/src/applications/files/config/PhabricatorFilesConfigOptions.php b/src/applications/files/config/PhabricatorFilesConfigOptions.php
index ac62d410f..e9d899f65 100644
--- a/src/applications/files/config/PhabricatorFilesConfigOptions.php
+++ b/src/applications/files/config/PhabricatorFilesConfigOptions.php
@@ -1,180 +1,183 @@
<?php
final class PhabricatorFilesConfigOptions
extends PhabricatorApplicationConfigOptions {
public function getName() {
return pht('Files');
}
public function getDescription() {
return pht('Configure files and file storage.');
}
public function getFontIcon() {
return 'fa-file';
}
public function getGroup() {
return 'apps';
}
public function getOptions() {
$viewable_default = array(
'image/jpeg' => 'image/jpeg',
'image/jpg' => 'image/jpg',
'image/png' => 'image/png',
'image/gif' => 'image/gif',
'text/plain' => 'text/plain; charset=utf-8',
'text/x-diff' => 'text/plain; charset=utf-8',
// ".ico" favicon files, which have mime type diversity. See:
// http://en.wikipedia.org/wiki/ICO_(file_format)#MIME_type
'image/x-ico' => 'image/x-icon',
'image/x-icon' => 'image/x-icon',
'image/vnd.microsoft.icon' => 'image/x-icon',
'audio/x-wav' => 'audio/x-wav',
'application/ogg' => 'application/ogg',
'audio/mpeg' => 'audio/mpeg',
);
$image_default = array(
'image/jpeg' => true,
'image/jpg' => true,
'image/png' => true,
'image/gif' => true,
'image/x-ico' => true,
'image/x-icon' => true,
'image/vnd.microsoft.icon' => true,
);
$audio_default = array(
'audio/x-wav' => true,
'application/ogg' => true,
'audio/mpeg' => true,
);
// largely lifted from http://en.wikipedia.org/wiki/Internet_media_type
$icon_default = array(
// audio file icon
'audio/basic' => 'fa-file-audio-o',
'audio/L24' => 'fa-file-audio-o',
'audio/mp4' => 'fa-file-audio-o',
'audio/mpeg' => 'fa-file-audio-o',
'audio/ogg' => 'fa-file-audio-o',
'audio/vorbis' => 'fa-file-audio-o',
'audio/vnd.rn-realaudio' => 'fa-file-audio-o',
'audio/vnd.wave' => 'fa-file-audio-o',
'audio/webm' => 'fa-file-audio-o',
// movie file icon
'video/mpeg' => 'fa-file-movie-o',
'video/mp4' => 'fa-file-movie-o',
'video/ogg' => 'fa-file-movie-o',
'video/quicktime' => 'fa-file-movie-o',
'video/webm' => 'fa-file-movie-o',
'video/x-matroska' => 'fa-file-movie-o',
'video/x-ms-wmv' => 'fa-file-movie-o',
'video/x-flv' => 'fa-file-movie-o',
// pdf file icon
'application/pdf' => 'fa-file-pdf-o',
// zip file icon
'application/zip' => 'fa-file-zip-o',
// msword icon
'application/msword' => 'fa-file-word-o',
// msexcel
'application/vnd.ms-excel' => 'fa-file-excel-o',
// mspowerpoint
'application/vnd.ms-powerpoint' => 'fa-file-powerpoint-o',
) + array_fill_keys(array_keys($image_default), 'fa-file-image-o');
// NOTE: These options are locked primarily because adding "text/plain"
// as an image MIME type increases SSRF vulnerability by allowing users
// to load text files from remote servers as "images" (see T6755 for
// discussion).
return array(
$this->newOption('files.viewable-mime-types', 'wild', $viewable_default)
->setLocked(true)
->setSummary(
pht('Configure which MIME types are viewable in the browser.'))
->setDescription(
pht(
- 'Configure which uploaded file types may be viewed directly '.
- 'in the browser. Other file types will be downloaded instead '.
- 'of displayed. This is mainly a usability consideration, since '.
- 'browsers tend to freak out when viewing enormous binary files.'.
+ "Configure which uploaded file types may be viewed directly ".
+ "in the browser. Other file types will be downloaded instead ".
+ "of displayed. This is mainly a usability consideration, since ".
+ "browsers tend to freak out when viewing enormous binary files.".
"\n\n".
- 'The keys in this map are vieweable MIME types; the values are '.
- 'the MIME types they are delivered as when they are viewed in '.
- 'the browser.')),
+ "The keys in this map are viewable MIME types; the values are ".
+ "the MIME types they are delivered as when they are viewed in ".
+ "the browser.")),
$this->newOption('files.image-mime-types', 'set', $image_default)
->setLocked(true)
->setSummary(pht('Configure which MIME types are images.'))
->setDescription(
pht(
- 'List of MIME types which can be used as the `src` for an '.
- '`<img />` tag.')),
+ 'List of MIME types which can be used as the `%s` for an `%s` tag.',
+ 'src',
+ '<img />')),
$this->newOption('files.audio-mime-types', 'set', $audio_default)
->setLocked(true)
->setSummary(pht('Configure which MIME types are audio.'))
->setDescription(
pht(
- 'List of MIME types which can be used to render an '.
- '`<audio />` tag.')),
+ 'List of MIME types which can be used to render an `%s` tag.',
+ '<audio />')),
$this->newOption('files.icon-mime-types', 'wild', $icon_default)
->setLocked(true)
->setSummary(pht('Configure which MIME types map to which icons.'))
->setDescription(
pht(
'Map of MIME type to icon name. MIME types which can not be '.
- 'found default to icon `doc_files`.')),
+ 'found default to icon `%s`.',
+ 'doc_files')),
$this->newOption('storage.mysql-engine.max-size', 'int', 1000000)
->setSummary(
pht(
'Configure the largest file which will be put into the MySQL '.
'storage engine.')),
$this->newOption('storage.local-disk.path', 'string', null)
->setLocked(true)
->setSummary(pht('Local storage disk path.'))
->setDescription(
pht(
"Phabricator provides a local disk storage engine, which just ".
"writes files to some directory on local disk. The webserver ".
"must have read/write permissions on this directory. This is ".
"straightforward and suitable for most installs, but will not ".
"scale past one web frontend unless the path is actually an NFS ".
"mount, since you'll end up with some of the files written to ".
"each web frontend and no way for them to share. To use the ".
"local disk storage engine, specify the path to a directory ".
"here. To disable it, specify null.")),
$this->newOption('storage.s3.bucket', 'string', null)
->setSummary(pht('Amazon S3 bucket.'))
->setDescription(
pht(
"Set this to a valid Amazon S3 bucket to store files there. You ".
"must also configure S3 access keys in the 'Amazon Web Services' ".
"group.")),
$this->newOption(
'metamta.files.subject-prefix',
'string',
'[File]')
->setDescription(pht('Subject prefix for Files email.')),
$this->newOption('files.enable-imagemagick', 'bool', false)
->setBoolOptions(
array(
pht('Enable'),
pht('Disable'),
))
->setDescription(
pht(
'This option will use Imagemagick to rescale images, so animated '.
'GIFs can be thumbnailed and set as profile pictures. Imagemagick '.
- 'must be installed and the "convert" binary must be available to '.
- 'the webserver for this to work.')),
+ 'must be installed and the "%s" binary must be available to '.
+ 'the webserver for this to work.',
+ 'convert')),
);
}
}
diff --git a/src/applications/files/controller/PhabricatorFileDeleteController.php b/src/applications/files/controller/PhabricatorFileDeleteController.php
index 17be9f2ff..ed245c5cb 100644
--- a/src/applications/files/controller/PhabricatorFileDeleteController.php
+++ b/src/applications/files/controller/PhabricatorFileDeleteController.php
@@ -1,49 +1,51 @@
<?php
final class PhabricatorFileDeleteController extends PhabricatorFileController {
private $id;
public function willProcessRequest(array $data) {
$this->id = $data['id'];
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$file = id(new PhabricatorFileQuery())
->setViewer($user)
->withIDs(array($this->id))
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
if (!$file) {
return new Aphront404Response();
}
if (($user->getPHID() != $file->getAuthorPHID()) &&
(!$user->getIsAdmin())) {
return new Aphront403Response();
}
if ($request->isFormPost()) {
$file->delete();
return id(new AphrontRedirectResponse())->setURI('/file/');
}
$dialog = new AphrontDialogView();
$dialog->setUser($user);
- $dialog->setTitle('Really delete file?');
+ $dialog->setTitle(pht('Really delete file?'));
$dialog->appendChild(hsprintf(
- "<p>Permanently delete '%s'? This action can not be undone.</p>",
- $file->getName()));
- $dialog->addSubmitButton('Delete');
+ '<p>%s</p>',
+ pht(
+ "Permanently delete '%s'? This action can not be undone.",
+ $file->getName())));
+ $dialog->addSubmitButton(pht('Delete'));
$dialog->addCancelButton($file->getInfoURI());
return id(new AphrontDialogResponse())->setDialog($dialog);
}
}
diff --git a/src/applications/files/engine/PhabricatorLocalDiskFileStorageEngine.php b/src/applications/files/engine/PhabricatorLocalDiskFileStorageEngine.php
index 617247bda..d9f7caf12 100644
--- a/src/applications/files/engine/PhabricatorLocalDiskFileStorageEngine.php
+++ b/src/applications/files/engine/PhabricatorLocalDiskFileStorageEngine.php
@@ -1,132 +1,136 @@
<?php
/**
* Local disk storage engine. Keeps files on local disk. This engine is easy
* to set up, but it doesn't work if you have multiple web frontends!
*
* @task internal Internals
*/
final class PhabricatorLocalDiskFileStorageEngine
extends PhabricatorFileStorageEngine {
/* -( Engine Metadata )---------------------------------------------------- */
/**
* This engine identifies as "local-disk".
*/
public function getEngineIdentifier() {
return 'local-disk';
}
public function getEnginePriority() {
return 5;
}
public function canWriteFiles() {
$path = PhabricatorEnv::getEnvConfig('storage.local-disk.path');
return (bool)strlen($path);
}
/* -( Managing File Data )------------------------------------------------- */
/**
* Write the file data to local disk. Returns the relative path as the
* file data handle.
* @task impl
*/
public function writeFile($data, array $params) {
$root = $this->getLocalDiskFileStorageRoot();
// Generate a random, unique file path like "ab/29/1f918a9ac39201ff". We
// put a couple of subdirectories up front to avoid a situation where we
// have one directory with a zillion files in it, since this is generally
// bad news.
do {
$name = md5(mt_rand());
$name = preg_replace('/^(..)(..)(.*)$/', '\\1/\\2/\\3', $name);
if (!Filesystem::pathExists($root.'/'.$name)) {
break;
}
} while (true);
$parent = $root.'/'.dirname($name);
if (!Filesystem::pathExists($parent)) {
execx('mkdir -p %s', $parent);
}
AphrontWriteGuard::willWrite();
Filesystem::writeFile($root.'/'.$name, $data);
return $name;
}
/**
* Read the file data off local disk.
* @task impl
*/
public function readFile($handle) {
$path = $this->getLocalDiskFileStorageFullPath($handle);
return Filesystem::readFile($path);
}
/**
* Deletes the file from local disk, if it exists.
* @task impl
*/
public function deleteFile($handle) {
$path = $this->getLocalDiskFileStorageFullPath($handle);
if (Filesystem::pathExists($path)) {
AphrontWriteGuard::willWrite();
Filesystem::remove($path);
}
}
/* -( Internals )---------------------------------------------------------- */
/**
* Get the configured local disk path for file storage.
*
* @return string Absolute path to somewhere that files can be stored.
* @task internal
*/
private function getLocalDiskFileStorageRoot() {
$root = PhabricatorEnv::getEnvConfig('storage.local-disk.path');
if (!$root || $root == '/' || $root[0] != '/') {
throw new PhabricatorFileStorageConfigurationException(
- "Malformed local disk storage root. You must provide an absolute ".
- "path, and can not use '/' as the root.");
+ pht(
+ "Malformed local disk storage root. You must provide an absolute ".
+ "path, and can not use '%s' as the root.",
+ '/'));
}
return rtrim($root, '/');
}
/**
* Convert a handle into an absolute local disk path.
*
* @param string File data handle.
* @return string Absolute path to the corresponding file.
* @task internal
*/
private function getLocalDiskFileStorageFullPath($handle) {
// Make sure there's no funny business going on here. Users normally have
// no ability to affect the content of handles, but double-check that
// we're only accessing local storage just in case.
if (!preg_match('@^[a-f0-9]{2}/[a-f0-9]{2}/[a-f0-9]{28}\z@', $handle)) {
throw new Exception(
- "Local disk filesystem handle '{$handle}' is malformed!");
+ pht(
+ "Local disk filesystem handle '%s' is malformed!",
+ $handle));
}
$root = $this->getLocalDiskFileStorageRoot();
return $root.'/'.$handle;
}
}
diff --git a/src/applications/files/engine/PhabricatorMySQLFileStorageEngine.php b/src/applications/files/engine/PhabricatorMySQLFileStorageEngine.php
index b4c46702e..eb49ef78c 100644
--- a/src/applications/files/engine/PhabricatorMySQLFileStorageEngine.php
+++ b/src/applications/files/engine/PhabricatorMySQLFileStorageEngine.php
@@ -1,95 +1,95 @@
<?php
/**
* MySQL blob storage engine. This engine is the easiest to set up but doesn't
* scale very well.
*
* It uses the @{class:PhabricatorFileStorageBlob} to actually access the
* underlying database table.
*
* @task internal Internals
*/
final class PhabricatorMySQLFileStorageEngine
extends PhabricatorFileStorageEngine {
/* -( Engine Metadata )---------------------------------------------------- */
/**
* For historical reasons, this engine identifies as "blob".
*/
public function getEngineIdentifier() {
return 'blob';
}
public function getEnginePriority() {
return 1;
}
public function canWriteFiles() {
return ($this->getFilesizeLimit() > 0);
}
public function hasFilesizeLimit() {
return true;
}
public function getFilesizeLimit() {
return PhabricatorEnv::getEnvConfig('storage.mysql-engine.max-size');
}
/* -( Managing File Data )------------------------------------------------- */
/**
* Write file data into the big blob store table in MySQL. Returns the row
* ID as the file data handle.
*/
public function writeFile($data, array $params) {
$blob = new PhabricatorFileStorageBlob();
$blob->setData($data);
$blob->save();
return $blob->getID();
}
/**
* Load a stored blob from MySQL.
*/
public function readFile($handle) {
return $this->loadFromMySQLFileStorage($handle)->getData();
}
/**
* Delete a blob from MySQL.
*/
public function deleteFile($handle) {
$this->loadFromMySQLFileStorage($handle)->delete();
}
/* -( Internals )---------------------------------------------------------- */
/**
* Load the Lisk object that stores the file data for a handle.
*
* @param string File data handle.
* @return PhabricatorFileStorageBlob Data DAO.
* @task internal
*/
private function loadFromMySQLFileStorage($handle) {
$blob = id(new PhabricatorFileStorageBlob())->load($handle);
if (!$blob) {
- throw new Exception("Unable to load MySQL blob file '{$handle}'!");
+ throw new Exception(pht("Unable to load MySQL blob file '%s'!", $handle));
}
return $blob;
}
}
diff --git a/src/applications/files/engine/PhabricatorS3FileStorageEngine.php b/src/applications/files/engine/PhabricatorS3FileStorageEngine.php
index 93ca00c13..59446c2c1 100644
--- a/src/applications/files/engine/PhabricatorS3FileStorageEngine.php
+++ b/src/applications/files/engine/PhabricatorS3FileStorageEngine.php
@@ -1,174 +1,179 @@
<?php
/**
* Amazon S3 file storage engine. This engine scales well but is relatively
* high-latency since data has to be pulled off S3.
*
* @task internal Internals
*/
final class PhabricatorS3FileStorageEngine
extends PhabricatorFileStorageEngine {
/* -( Engine Metadata )---------------------------------------------------- */
/**
* This engine identifies as `amazon-s3`.
*/
public function getEngineIdentifier() {
return 'amazon-s3';
}
public function getEnginePriority() {
return 100;
}
public function canWriteFiles() {
$bucket = PhabricatorEnv::getEnvConfig('storage.s3.bucket');
$access_key = PhabricatorEnv::getEnvConfig('amazon-s3.access-key');
$secret_key = PhabricatorEnv::getEnvConfig('amazon-s3.secret-key');
return (strlen($bucket) && strlen($access_key) && strlen($secret_key));
}
/* -( Managing File Data )------------------------------------------------- */
/**
* Writes file data into Amazon S3.
*/
public function writeFile($data, array $params) {
$s3 = $this->newS3API();
// Generate a random name for this file. We add some directories to it
// (e.g. 'abcdef123456' becomes 'ab/cd/ef123456') to make large numbers of
// files more browsable with web/debugging tools like the S3 administration
// tool.
$seed = Filesystem::readRandomCharacters(20);
$parts = array();
$parts[] = 'phabricator';
$instance_name = PhabricatorEnv::getEnvConfig('cluster.instance');
if (strlen($instance_name)) {
$parts[] = $instance_name;
}
$parts[] = substr($seed, 0, 2);
$parts[] = substr($seed, 2, 2);
$parts[] = substr($seed, 4);
$name = implode('/', $parts);
AphrontWriteGuard::willWrite();
$profiler = PhutilServiceProfiler::getInstance();
$call_id = $profiler->beginServiceCall(
array(
'type' => 's3',
'method' => 'putObject',
));
$s3->putObject(
$data,
$this->getBucketName(),
$name,
$acl = 'private');
$profiler->endServiceCall($call_id, array());
return $name;
}
/**
* Load a stored blob from Amazon S3.
*/
public function readFile($handle) {
$s3 = $this->newS3API();
$profiler = PhutilServiceProfiler::getInstance();
$call_id = $profiler->beginServiceCall(
array(
'type' => 's3',
'method' => 'getObject',
));
$result = $s3->getObject(
$this->getBucketName(),
$handle);
$profiler->endServiceCall($call_id, array());
// NOTE: The implementation of the API that we're using may respond with
// a successful result that has length 0 and no body property.
if (isset($result->body)) {
return $result->body;
} else {
return '';
}
}
/**
* Delete a blob from Amazon S3.
*/
public function deleteFile($handle) {
AphrontWriteGuard::willWrite();
$s3 = $this->newS3API();
$profiler = PhutilServiceProfiler::getInstance();
$call_id = $profiler->beginServiceCall(
array(
'type' => 's3',
'method' => 'deleteObject',
));
$s3->deleteObject(
$this->getBucketName(),
$handle);
$profiler->endServiceCall($call_id, array());
}
/* -( Internals )---------------------------------------------------------- */
/**
* Retrieve the S3 bucket name.
*
* @task internal
*/
private function getBucketName() {
$bucket = PhabricatorEnv::getEnvConfig('storage.s3.bucket');
if (!$bucket) {
throw new PhabricatorFileStorageConfigurationException(
- "No 'storage.s3.bucket' specified!");
+ pht(
+ "No '%s' specified!",
+ 'storage.s3.bucket'));
}
return $bucket;
}
/**
* Create a new S3 API object.
*
* @task internal
* @phutil-external-symbol class S3
*/
private function newS3API() {
$libroot = dirname(phutil_get_library_root('phabricator'));
require_once $libroot.'/externals/s3/S3.php';
$access_key = PhabricatorEnv::getEnvConfig('amazon-s3.access-key');
$secret_key = PhabricatorEnv::getEnvConfig('amazon-s3.secret-key');
$endpoint = PhabricatorEnv::getEnvConfig('amazon-s3.endpoint');
if (!$access_key || !$secret_key) {
throw new PhabricatorFileStorageConfigurationException(
- "Specify 'amazon-s3.access-key' and 'amazon-s3.secret-key'!");
+ pht(
+ "Specify '%s' and '%s'!",
+ 'amazon-s3.access-key',
+ 'amazon-s3.secret-key'));
}
if ($endpoint !== null) {
$s3 = new S3($access_key, $secret_key, $use_ssl = true, $endpoint);
} else {
$s3 = new S3($access_key, $secret_key, $use_ssl = true);
}
$s3->setExceptions(true);
return $s3;
}
}
diff --git a/src/applications/files/engine/PhabricatorTestStorageEngine.php b/src/applications/files/engine/PhabricatorTestStorageEngine.php
index 243d17904..72e68dc7c 100644
--- a/src/applications/files/engine/PhabricatorTestStorageEngine.php
+++ b/src/applications/files/engine/PhabricatorTestStorageEngine.php
@@ -1,50 +1,50 @@
<?php
/**
* Test storage engine. Does not actually store files. Used for unit tests.
*/
final class PhabricatorTestStorageEngine
extends PhabricatorFileStorageEngine {
private static $storage = array();
private static $nextHandle = 1;
public function getEngineIdentifier() {
return 'unit-test';
}
public function getEnginePriority() {
return 1000;
}
public function isTestEngine() {
return true;
}
public function canWriteFiles() {
return true;
}
public function hasFilesizeLimit() {
return false;
}
public function writeFile($data, array $params) {
AphrontWriteGuard::willWrite();
self::$storage[self::$nextHandle] = $data;
return (string)self::$nextHandle++;
}
public function readFile($handle) {
if (isset(self::$storage[$handle])) {
return self::$storage[$handle];
}
- throw new Exception("No such file with handle '{$handle}'!");
+ throw new Exception(pht("No such file with handle '%s'!", $handle));
}
public function deleteFile($handle) {
AphrontWriteGuard::willWrite();
unset(self::$storage[$handle]);
}
}
diff --git a/src/applications/files/exception/PhabricatorFileUploadException.php b/src/applications/files/exception/PhabricatorFileUploadException.php
index 59d24ed29..095565793 100644
--- a/src/applications/files/exception/PhabricatorFileUploadException.php
+++ b/src/applications/files/exception/PhabricatorFileUploadException.php
@@ -1,28 +1,29 @@
<?php
final class PhabricatorFileUploadException extends Exception {
public function __construct($code) {
$map = array(
- UPLOAD_ERR_INI_SIZE =>
- pht("Uploaded file is too large: current limit is %s. To adjust ".
- "this limit change 'upload_max_filesize' in php.ini.",
- ini_get('upload_max_filesize')),
- UPLOAD_ERR_FORM_SIZE =>
- 'File is too large.',
- UPLOAD_ERR_PARTIAL =>
- 'File was only partially transferred, upload did not complete.',
- UPLOAD_ERR_NO_FILE =>
- 'No file was uploaded.',
- UPLOAD_ERR_NO_TMP_DIR =>
- 'Unable to write file: temporary directory does not exist.',
- UPLOAD_ERR_CANT_WRITE =>
- 'Unable to write file: failed to write to temporary directory.',
- UPLOAD_ERR_EXTENSION =>
- 'Unable to upload: a PHP extension stopped the upload.',
+ 'UPLOAD_ERR_INI_SIZE' => pht(
+ "Uploaded file is too large: current limit is %s. To adjust ".
+ "this limit change '%s' in php.ini.",
+ ini_get('upload_max_filesize'),
+ 'upload_max_filesize'),
+ 'UPLOAD_ERR_FORM_SIZE' => pht(
+ 'File is too large.'),
+ 'UPLOAD_ERR_PARTIAL' => pht(
+ 'File was only partially transferred, upload did not complete.'),
+ 'UPLOAD_ERR_NO_FILE' => pht(
+ 'No file was uploaded.'),
+ 'UPLOAD_ERR_NO_TMP_DIR' => pht(
+ 'Unable to write file: temporary directory does not exist.'),
+ 'UPLOAD_ERR_CANT_WRITE' => pht(
+ 'Unable to write file: failed to write to temporary directory.'),
+ 'UPLOAD_ERR_EXTENSION' => pht(
+ 'Unable to upload: a PHP extension stopped the upload.'),
);
- $message = idx($map, $code, 'Upload failed: unknown error.');
+ $message = idx($map, $code, pht('Upload failed: unknown error.'));
parent::__construct($message, $code);
}
}
diff --git a/src/applications/files/mail/FileCreateMailReceiver.php b/src/applications/files/mail/FileCreateMailReceiver.php
index 9f0fcc157..fa9c6691e 100644
--- a/src/applications/files/mail/FileCreateMailReceiver.php
+++ b/src/applications/files/mail/FileCreateMailReceiver.php
@@ -1,57 +1,58 @@
<?php
final class FileCreateMailReceiver extends PhabricatorMailReceiver {
public function isEnabled() {
$app_class = 'PhabricatorFilesApplication';
return PhabricatorApplication::isClassInstalled($app_class);
}
public function canAcceptMail(PhabricatorMetaMTAReceivedMail $mail) {
$files_app = new PhabricatorFilesApplication();
return $this->canAcceptApplicationMail($files_app, $mail);
}
protected function processReceivedMail(
PhabricatorMetaMTAReceivedMail $mail,
PhabricatorUser $sender) {
$attachment_phids = $mail->getAttachments();
if (empty($attachment_phids)) {
throw new PhabricatorMetaMTAReceivedMailProcessingException(
MetaMTAReceivedMailStatus::STATUS_UNHANDLED_EXCEPTION,
- 'Ignoring email to create files that did not include attachments.');
+ pht(
+ 'Ignoring email to create files that did not include attachments.'));
}
$first_phid = head($attachment_phids);
$mail->setRelatedPHID($first_phid);
$attachment_count = count($attachment_phids);
if ($attachment_count > 1) {
$subject = pht('You successfully uploaded %d files.', $attachment_count);
} else {
$subject = pht('You successfully uploaded a file.');
}
$subject_prefix =
PhabricatorEnv::getEnvConfig('metamta.files.subject-prefix');
$file_uris = array();
foreach ($attachment_phids as $phid) {
$file_uris[] =
PhabricatorEnv::getProductionURI('/file/info/'.$phid.'/');
}
$body = new PhabricatorMetaMTAMailBody();
$body->addRawSection($subject);
$body->addTextSection(pht('FILE LINKS'), implode("\n", $file_uris));
id(new PhabricatorMetaMTAMail())
->addTos(array($sender->getPHID()))
->setSubject($subject)
->setSubjectPrefix($subject_prefix)
->setFrom($sender->getPHID())
->setRelatedPHID($first_phid)
->setBody($body->render())
->saveAndSend();
}
}
diff --git a/src/applications/files/mail/FileMailReceiver.php b/src/applications/files/mail/FileMailReceiver.php
index 05cb788ef..cdad22c5c 100644
--- a/src/applications/files/mail/FileMailReceiver.php
+++ b/src/applications/files/mail/FileMailReceiver.php
@@ -1,27 +1,27 @@
<?php
final class FileMailReceiver extends PhabricatorObjectMailReceiver {
public function isEnabled() {
- $app_class = 'PhabricatorFilesApplication';
- return PhabricatorApplication::isClassInstalled($app_class);
+ return PhabricatorApplication::isClassInstalled(
+ 'PhabricatorFilesApplication');
}
protected function getObjectPattern() {
return 'F[1-9]\d*';
}
protected function loadObject($pattern, PhabricatorUser $viewer) {
$id = (int)trim($pattern, 'F');
return id(new PhabricatorFileQuery())
->setViewer($viewer)
->withIDs(array($id))
->executeOne();
}
protected function getTransactionReplyHandler() {
return new FileReplyHandler();
}
}
diff --git a/src/applications/files/mail/FileReplyHandler.php b/src/applications/files/mail/FileReplyHandler.php
index b21c487e1..7cdfb886c 100644
--- a/src/applications/files/mail/FileReplyHandler.php
+++ b/src/applications/files/mail/FileReplyHandler.php
@@ -1,16 +1,16 @@
<?php
final class FileReplyHandler
extends PhabricatorApplicationTransactionReplyHandler {
public function validateMailReceiver($mail_receiver) {
if (!($mail_receiver instanceof PhabricatorFile)) {
- throw new Exception('Mail receiver is not a PhabricatorFile.');
+ throw new Exception(pht('Mail receiver is not a %s.', 'PhabricatorFile'));
}
}
public function getObjectPrefix() {
return 'F';
}
}
diff --git a/src/applications/files/management/PhabricatorFilesManagementCatWorkflow.php b/src/applications/files/management/PhabricatorFilesManagementCatWorkflow.php
index 896165f58..98efd2dee 100644
--- a/src/applications/files/management/PhabricatorFilesManagementCatWorkflow.php
+++ b/src/applications/files/management/PhabricatorFilesManagementCatWorkflow.php
@@ -1,55 +1,54 @@
<?php
final class PhabricatorFilesManagementCatWorkflow
extends PhabricatorFilesManagementWorkflow {
protected function didConstruct() {
$this
->setName('cat')
- ->setSynopsis(
- pht('Print the contents of a file.'))
+ ->setSynopsis(pht('Print the contents of a file.'))
->setArguments(
array(
array(
'name' => 'begin',
'param' => 'bytes',
'help' => pht('Begin printing at a specific offset.'),
),
array(
'name' => 'end',
'param' => 'bytes',
'help' => pht('End printing at a specific offset.'),
),
array(
'name' => 'names',
'wildcard' => true,
),
));
}
public function execute(PhutilArgumentParser $args) {
$console = PhutilConsole::getConsole();
$names = $args->getArg('names');
if (count($names) > 1) {
throw new PhutilArgumentUsageException(
- pht('Specify exactly one file to print, like "F123".'));
+ pht('Specify exactly one file to print, like "%s".', 'F123'));
} else if (!$names) {
throw new PhutilArgumentUsageException(
- pht('Specify a file to print, like "F123".'));
+ pht('Specify a file to print, like "%s".', 'F123'));
}
$file = head($this->loadFilesWithNames($names));
$begin = $args->getArg('begin');
$end = $args->getArg('end');
$iterator = $file->getFileDataIterator($begin, $end);
foreach ($iterator as $data) {
echo $data;
}
return 0;
}
}
diff --git a/src/applications/files/management/PhabricatorFilesManagementCompactWorkflow.php b/src/applications/files/management/PhabricatorFilesManagementCompactWorkflow.php
index b885f2613..0c8e7153e 100644
--- a/src/applications/files/management/PhabricatorFilesManagementCompactWorkflow.php
+++ b/src/applications/files/management/PhabricatorFilesManagementCompactWorkflow.php
@@ -1,133 +1,134 @@
<?php
final class PhabricatorFilesManagementCompactWorkflow
extends PhabricatorFilesManagementWorkflow {
protected function didConstruct() {
$this
->setName('compact')
->setSynopsis(
pht(
'Merge identical files to share the same storage. In some cases, '.
'this can repair files with missing data.'))
->setArguments(
array(
array(
'name' => 'dry-run',
'help' => pht('Show what would be compacted.'),
),
array(
'name' => 'all',
'help' => pht('Compact all files.'),
),
array(
'name' => 'names',
'wildcard' => true,
),
));
}
public function execute(PhutilArgumentParser $args) {
$console = PhutilConsole::getConsole();
$iterator = $this->buildIterator($args);
if (!$iterator) {
throw new PhutilArgumentUsageException(
pht(
- 'Either specify a list of files to compact, or use `--all` '.
- 'to compact all files.'));
+ 'Either specify a list of files to compact, or use `%s` '.
+ 'to compact all files.',
+ '--all'));
}
$is_dry_run = $args->getArg('dry-run');
foreach ($iterator as $file) {
$monogram = $file->getMonogram();
$hash = $file->getContentHash();
if (!$hash) {
$console->writeOut(
"%s\n",
pht('%s: No content hash.', $monogram));
continue;
}
// Find other files with the same content hash. We're going to point
// them at the data for this file.
$similar_files = id(new PhabricatorFile())->loadAllWhere(
'contentHash = %s AND id != %d AND
(storageEngine != %s OR storageHandle != %s)',
$hash,
$file->getID(),
$file->getStorageEngine(),
$file->getStorageHandle());
if (!$similar_files) {
$console->writeOut(
"%s\n",
pht('%s: No other files with the same content hash.', $monogram));
continue;
}
// Only compact files into this one if we can load the data. This
// prevents us from breaking working files if we're missing some data.
try {
$data = $file->loadFileData();
} catch (Exception $ex) {
$data = null;
}
if ($data === null) {
$console->writeOut(
"%s\n",
pht(
'%s: Unable to load file data; declining to compact.',
$monogram));
continue;
}
foreach ($similar_files as $similar_file) {
if ($is_dry_run) {
$console->writeOut(
"%s\n",
pht(
'%s: Would compact storage with %s.',
$monogram,
$similar_file->getMonogram()));
continue;
}
$console->writeOut(
"%s\n",
pht(
'%s: Compacting storage with %s.',
$monogram,
$similar_file->getMonogram()));
$old_instance = null;
try {
$old_instance = $similar_file->instantiateStorageEngine();
$old_engine = $similar_file->getStorageEngine();
$old_handle = $similar_file->getStorageHandle();
} catch (Exception $ex) {
// If the old stuff is busted, we just won't try to delete the
// old data.
phlog($ex);
}
$similar_file
->setStorageEngine($file->getStorageEngine())
->setStorageHandle($file->getStorageHandle())
->save();
if ($old_instance) {
$similar_file->deleteFileDataIfUnused(
$old_instance,
$old_engine,
$old_handle);
}
}
}
return 0;
}
}
diff --git a/src/applications/files/management/PhabricatorFilesManagementEnginesWorkflow.php b/src/applications/files/management/PhabricatorFilesManagementEnginesWorkflow.php
index 579fd7e3b..ede95a1f2 100644
--- a/src/applications/files/management/PhabricatorFilesManagementEnginesWorkflow.php
+++ b/src/applications/files/management/PhabricatorFilesManagementEnginesWorkflow.php
@@ -1,30 +1,30 @@
<?php
final class PhabricatorFilesManagementEnginesWorkflow
extends PhabricatorFilesManagementWorkflow {
protected function didConstruct() {
$this
->setName('engines')
- ->setSynopsis('List available storage engines.')
+ ->setSynopsis(pht('List available storage engines.'))
->setArguments(array());
}
public function execute(PhutilArgumentParser $args) {
$console = PhutilConsole::getConsole();
$engines = PhabricatorFile::buildAllEngines();
if (!$engines) {
- throw new Exception('No storage engines are available.');
+ throw new Exception(pht('No storage engines are available.'));
}
foreach ($engines as $engine) {
$console->writeOut(
"%s\n",
$engine->getEngineIdentifier());
}
return 0;
}
}
diff --git a/src/applications/files/management/PhabricatorFilesManagementMigrateWorkflow.php b/src/applications/files/management/PhabricatorFilesManagementMigrateWorkflow.php
index 5201465be..ce48d29bb 100644
--- a/src/applications/files/management/PhabricatorFilesManagementMigrateWorkflow.php
+++ b/src/applications/files/management/PhabricatorFilesManagementMigrateWorkflow.php
@@ -1,106 +1,117 @@
<?php
final class PhabricatorFilesManagementMigrateWorkflow
extends PhabricatorFilesManagementWorkflow {
protected function didConstruct() {
$this
->setName('migrate')
- ->setSynopsis('Migrate files between storage engines.')
+ ->setSynopsis(pht('Migrate files between storage engines.'))
->setArguments(
array(
array(
'name' => 'engine',
'param' => 'storage_engine',
- 'help' => 'Migrate to the named storage engine.',
+ 'help' => pht('Migrate to the named storage engine.'),
),
array(
'name' => 'dry-run',
- 'help' => 'Show what would be migrated.',
+ 'help' => pht('Show what would be migrated.'),
),
array(
'name' => 'all',
- 'help' => 'Migrate all files.',
+ 'help' => pht('Migrate all files.'),
),
array(
'name' => 'names',
'wildcard' => true,
),
));
}
public function execute(PhutilArgumentParser $args) {
$console = PhutilConsole::getConsole();
$engine_id = $args->getArg('engine');
if (!$engine_id) {
throw new PhutilArgumentUsageException(
- 'Specify an engine to migrate to with `--engine`. '.
- 'Use `files engines` to get a list of engines.');
+ pht(
+ 'Specify an engine to migrate to with `%s`. '.
+ 'Use `%s` to get a list of engines.',
+ '--engine',
+ 'files engines'));
}
$engine = PhabricatorFile::buildEngine($engine_id);
$iterator = $this->buildIterator($args);
if (!$iterator) {
throw new PhutilArgumentUsageException(
- 'Either specify a list of files to migrate, or use `--all` '.
- 'to migrate all files.');
+ pht(
+ 'Either specify a list of files to migrate, or use `%s` '.
+ 'to migrate all files.',
+ '--all'));
}
$is_dry_run = $args->getArg('dry-run');
$failed = array();
foreach ($iterator as $file) {
$fid = 'F'.$file->getID();
if ($file->getStorageEngine() == $engine_id) {
$console->writeOut(
- "%s: Already stored on '%s'\n",
- $fid,
- $engine_id);
+ "%s\n",
+ pht(
+ "%s: Already stored on '%s'",
+ $fid,
+ $engine_id));
continue;
}
if ($is_dry_run) {
$console->writeOut(
- "%s: Would migrate from '%s' to '%s' (dry run)\n",
- $fid,
- $file->getStorageEngine(),
- $engine_id);
+ "%s\n",
+ pht(
+ "%s: Would migrate from '%s' to '%s' (dry run)",
+ $fid,
+ $file->getStorageEngine(),
+ $engine_id));
continue;
}
$console->writeOut(
- "%s: Migrating from '%s' to '%s'...",
- $fid,
- $file->getStorageEngine(),
- $engine_id);
+ "%s\n",
+ pht(
+ "%s: Migrating from '%s' to '%s'...",
+ $fid,
+ $file->getStorageEngine(),
+ $engine_id));
try {
$file->migrateToEngine($engine);
- $console->writeOut("done.\n");
+ $console->writeOut("%s\n", pht('Done.'));
} catch (Exception $ex) {
- $console->writeOut("failed!\n");
+ $console->writeOut("%s\n", pht('Failed!'));
$console->writeErr("%s\n", (string)$ex);
$failed[] = $file;
}
}
if ($failed) {
- $console->writeOut("**Failures!**\n");
+ $console->writeOut("**%s**\n", pht('Failures!'));
$ids = array();
foreach ($failed as $file) {
$ids[] = 'F'.$file->getID();
}
$console->writeOut("%s\n", implode(', ', $ids));
return 1;
} else {
- $console->writeOut("**Success!**\n");
+ $console->writeOut("**%s**\n", pht('Success!'));
return 0;
}
}
}
diff --git a/src/applications/files/management/PhabricatorFilesManagementPurgeWorkflow.php b/src/applications/files/management/PhabricatorFilesManagementPurgeWorkflow.php
index 3f08c7662..bf2c04a58 100644
--- a/src/applications/files/management/PhabricatorFilesManagementPurgeWorkflow.php
+++ b/src/applications/files/management/PhabricatorFilesManagementPurgeWorkflow.php
@@ -1,69 +1,71 @@
<?php
final class PhabricatorFilesManagementPurgeWorkflow
extends PhabricatorFilesManagementWorkflow {
protected function didConstruct() {
$this
->setName('purge')
- ->setSynopsis('Delete files with missing data.')
+ ->setSynopsis(pht('Delete files with missing data.'))
->setArguments(
array(
array(
'name' => 'all',
- 'help' => 'Update all files.',
+ 'help' => pht('Update all files.'),
),
array(
'name' => 'dry-run',
- 'help' => 'Show what would be updated.',
+ 'help' => pht('Show what would be updated.'),
),
array(
'name' => 'names',
'wildcard' => true,
),
));
}
public function execute(PhutilArgumentParser $args) {
$console = PhutilConsole::getConsole();
$iterator = $this->buildIterator($args);
if (!$iterator) {
throw new PhutilArgumentUsageException(
- 'Either specify a list of files to purge, or use `--all` '.
- 'to purge all files.');
+ pht(
+ 'Either specify a list of files to purge, or use `%s` '.
+ 'to purge all files.',
+ '--all'));
}
$is_dry_run = $args->getArg('dry-run');
foreach ($iterator as $file) {
$fid = 'F'.$file->getID();
try {
$file->loadFileData();
$okay = true;
} catch (Exception $ex) {
$okay = false;
}
if ($okay) {
$console->writeOut(
- "%s: File data is OK, not purging.\n",
- $fid);
+ "%s\n",
+ pht('%s: File data is OK, not purging.', $fid));
} else {
if ($is_dry_run) {
$console->writeOut(
- "%s: Would purge (dry run).\n",
- $fid);
+ "%s\n",
+ pht('%s: Would purge (dry run).', $fid));
} else {
$console->writeOut(
- "%s: Purging.\n",
- $fid);
+ "%s\n",
+ pht('%s: Purging.', $fid));
$file->delete();
}
}
}
return 0;
}
}
diff --git a/src/applications/files/management/PhabricatorFilesManagementRebuildWorkflow.php b/src/applications/files/management/PhabricatorFilesManagementRebuildWorkflow.php
index 95a3ea056..f7fc890ae 100644
--- a/src/applications/files/management/PhabricatorFilesManagementRebuildWorkflow.php
+++ b/src/applications/files/management/PhabricatorFilesManagementRebuildWorkflow.php
@@ -1,149 +1,156 @@
<?php
final class PhabricatorFilesManagementRebuildWorkflow
extends PhabricatorFilesManagementWorkflow {
protected function didConstruct() {
$this
->setName('rebuild')
- ->setSynopsis('Rebuild metadata of old files.')
+ ->setSynopsis(pht('Rebuild metadata of old files.'))
->setArguments(
array(
array(
'name' => 'all',
- 'help' => 'Update all files.',
+ 'help' => pht('Update all files.'),
),
array(
'name' => 'dry-run',
- 'help' => 'Show what would be updated.',
+ 'help' => pht('Show what would be updated.'),
),
array(
'name' => 'rebuild-mime',
- 'help' => 'Rebuild MIME information.',
+ 'help' => pht('Rebuild MIME information.'),
),
array(
'name' => 'rebuild-dimensions',
- 'help' => 'Rebuild image dimension information.',
+ 'help' => pht('Rebuild image dimension information.'),
),
array(
'name' => 'names',
'wildcard' => true,
),
));
}
public function execute(PhutilArgumentParser $args) {
$console = PhutilConsole::getConsole();
$iterator = $this->buildIterator($args);
if (!$iterator) {
throw new PhutilArgumentUsageException(
- 'Either specify a list of files to update, or use `--all` '.
- 'to update all files.');
+ pht(
+ 'Either specify a list of files to update, or use `%s` '.
+ 'to update all files.',
+ '--all'));
}
$update = array(
'mime' => $args->getArg('rebuild-mime'),
'dimensions' => $args->getArg('rebuild-dimensions'),
);
// If the user didn't select anything, rebuild everything.
if (!array_filter($update)) {
foreach ($update as $key => $ignored) {
$update[$key] = true;
}
}
$is_dry_run = $args->getArg('dry-run');
$failed = array();
foreach ($iterator as $file) {
$fid = 'F'.$file->getID();
if ($update['mime']) {
$tmp = new TempFile();
Filesystem::writeFile($tmp, $file->loadFileData());
$new_type = Filesystem::getMimeType($tmp);
if ($new_type == $file->getMimeType()) {
$console->writeOut(
- "%s: Mime type not changed (%s).\n",
- $fid,
- $new_type);
+ "%s\n",
+ pht(
+ '%s: Mime type not changed (%s).',
+ $fid,
+ $new_type));
} else {
if ($is_dry_run) {
$console->writeOut(
- "%s: Would update Mime type: '%s' -> '%s'.\n",
- $fid,
- $file->getMimeType(),
- $new_type);
+ "%s\n",
+ pht(
+ "%s: Would update Mime type: '%s' -> '%s'.",
+ $fid,
+ $file->getMimeType(),
+ $new_type));
} else {
$console->writeOut(
- "%s: Updating Mime type: '%s' -> '%s'.\n",
- $fid,
- $file->getMimeType(),
- $new_type);
+ "%s\n",
+ pht(
+ "%s: Updating Mime type: '%s' -> '%s'.",
+ $fid,
+ $file->getMimeType(),
+ $new_type));
$file->setMimeType($new_type);
$file->save();
}
}
}
if ($update['dimensions']) {
if (!$file->isViewableImage()) {
$console->writeOut(
- "%s: Not an image file.\n",
- $fid);
+ "%s\n",
+ pht('%s: Not an image file.', $fid));
continue;
}
$metadata = $file->getMetadata();
$image_width = idx($metadata, PhabricatorFile::METADATA_IMAGE_WIDTH);
$image_height = idx($metadata, PhabricatorFile::METADATA_IMAGE_HEIGHT);
if ($image_width && $image_height) {
$console->writeOut(
- "%s: Image dimensions already exist.\n",
- $fid);
+ "%s\n",
+ pht('%s: Image dimensions already exist.', $fid));
continue;
}
if ($is_dry_run) {
$console->writeOut(
- "%s: Would update file dimensions (dry run)\n",
- $fid);
+ "%s\n",
+ pht('%s: Would update file dimensions (dry run)', $fid));
continue;
}
$console->writeOut(
- '%s: Updating metadata... ',
- $fid);
+ pht('%s: Updating metadata... ', $fid));
try {
$file->updateDimensions();
- $console->writeOut("done.\n");
+ $console->writeOut("%s\n", pht('Done.'));
} catch (Exception $ex) {
- $console->writeOut("failed!\n");
+ $console->writeOut("%s\n", pht('Failed!'));
$console->writeErr("%s\n", (string)$ex);
$failed[] = $file;
}
}
}
if ($failed) {
- $console->writeOut("**Failures!**\n");
+ $console->writeOut("**%s**\n", pht('Failures!'));
$ids = array();
foreach ($failed as $file) {
$ids[] = 'F'.$file->getID();
}
$console->writeOut("%s\n", implode(', ', $ids));
return 1;
} else {
- $console->writeOut("**Success!**\n");
+ $console->writeOut("**%s**\n", pht('Success!'));
return 0;
}
return 0;
}
}
diff --git a/src/applications/files/management/PhabricatorFilesManagementWorkflow.php b/src/applications/files/management/PhabricatorFilesManagementWorkflow.php
index d0bff01c7..e94fa1d96 100644
--- a/src/applications/files/management/PhabricatorFilesManagementWorkflow.php
+++ b/src/applications/files/management/PhabricatorFilesManagementWorkflow.php
@@ -1,43 +1,47 @@
<?php
abstract class PhabricatorFilesManagementWorkflow
extends PhabricatorManagementWorkflow {
protected function buildIterator(PhutilArgumentParser $args) {
$names = $args->getArg('names');
if ($args->getArg('all')) {
if ($names) {
throw new PhutilArgumentUsageException(
- 'Specify either a list of files or `--all`, but not both.');
+ pht(
+ 'Specify either a list of files or `%s`, but not both.',
+ '--all'));
}
return new LiskMigrationIterator(new PhabricatorFile());
}
if ($names) {
return $this->loadFilesWithNames($names);
}
return null;
}
protected function loadFilesWithNames(array $names) {
$query = id(new PhabricatorObjectQuery())
->setViewer($this->getViewer())
->withNames($names)
->withTypes(array(PhabricatorFileFilePHIDType::TYPECONST));
$query->execute();
$files = $query->getNamedResults();
foreach ($names as $name) {
if (empty($files[$name])) {
throw new PhutilArgumentUsageException(
- "No file '{$name}' exists!");
+ pht(
+ "No file '%s' exists!",
+ $name));
}
}
return array_values($files);
}
}
diff --git a/src/applications/files/markup/PhabricatorEmbedFileRemarkupRule.php b/src/applications/files/markup/PhabricatorEmbedFileRemarkupRule.php
index 5ffb70be7..138465a63 100644
--- a/src/applications/files/markup/PhabricatorEmbedFileRemarkupRule.php
+++ b/src/applications/files/markup/PhabricatorEmbedFileRemarkupRule.php
@@ -1,234 +1,234 @@
<?php
final class PhabricatorEmbedFileRemarkupRule
extends PhabricatorObjectRemarkupRule {
const KEY_EMBED_FILE_PHIDS = 'phabricator.embedded-file-phids';
protected function getObjectNamePrefix() {
return 'F';
}
protected function loadObjects(array $ids) {
$engine = $this->getEngine();
$viewer = $engine->getConfig('viewer');
$objects = id(new PhabricatorFileQuery())
->setViewer($viewer)
->withIDs($ids)
->execute();
$phids_key = self::KEY_EMBED_FILE_PHIDS;
$phids = $engine->getTextMetadata($phids_key, array());
foreach (mpull($objects, 'getPHID') as $phid) {
$phids[] = $phid;
}
$engine->setTextMetadata($phids_key, $phids);
return $objects;
}
protected function renderObjectEmbed(
$object,
PhabricatorObjectHandle $handle,
$options) {
$options = $this->getFileOptions($options) + array(
'name' => $object->getName(),
);
$is_viewable_image = $object->isViewableImage();
$is_audio = $object->isAudio();
$force_link = ($options['layout'] == 'link');
$options['viewable'] = ($is_viewable_image || $is_audio);
if ($is_viewable_image && !$force_link) {
return $this->renderImageFile($object, $handle, $options);
} else if ($is_audio && !$force_link) {
return $this->renderAudioFile($object, $handle, $options);
} else {
return $this->renderFileLink($object, $handle, $options);
}
}
private function getFileOptions($option_string) {
$options = array(
'size' => null,
'layout' => 'left',
'float' => false,
'width' => null,
'height' => null,
'alt' => null,
);
if ($option_string) {
$option_string = trim($option_string, ', ');
$parser = new PhutilSimpleOptions();
$options = $parser->parse($option_string) + $options;
}
return $options;
}
private function renderImageFile(
PhabricatorFile $file,
PhabricatorObjectHandle $handle,
array $options) {
require_celerity_resource('lightbox-attachment-css');
$attrs = array();
$image_class = null;
$use_size = true;
if (!$options['size']) {
$width = $this->parseDimension($options['width']);
$height = $this->parseDimension($options['height']);
if ($width || $height) {
$use_size = false;
$attrs += array(
'src' => $file->getBestURI(),
'width' => $width,
'height' => $height,
);
}
}
if ($use_size) {
switch ((string)$options['size']) {
case 'full':
$attrs += array(
'src' => $file->getBestURI(),
'height' => $file->getImageHeight(),
'width' => $file->getImageWidth(),
);
$image_class = 'phabricator-remarkup-embed-image-full';
break;
case 'thumb':
default:
$preview_key = PhabricatorFileThumbnailTransform::TRANSFORM_PREVIEW;
$xform = PhabricatorFileTransform::getTransformByKey($preview_key);
$attrs['src'] = $file->getURIForTransform($xform);
$dimensions = $xform->getTransformedDimensions($file);
if ($dimensions) {
list($x, $y) = $dimensions;
$attrs['width'] = $x;
$attrs['height'] = $y;
}
$image_class = 'phabricator-remarkup-embed-image';
break;
}
}
if (isset($options['alt'])) {
$attrs['alt'] = $options['alt'];
}
$img = phutil_tag('img', $attrs);
$embed = javelin_tag(
'a',
array(
'href' => $file->getBestURI(),
'class' => $image_class,
'sigil' => 'lightboxable',
'meta' => array(
- 'phid' => $file->getPHID(),
- 'uri' => $file->getBestURI(),
- 'dUri' => $file->getDownloadURI(),
+ 'phid' => $file->getPHID(),
+ 'uri' => $file->getBestURI(),
+ 'dUri' => $file->getDownloadURI(),
'viewable' => true,
),
),
$img);
switch ($options['layout']) {
case 'right':
case 'center':
case 'inline':
case 'left':
$layout_class = 'phabricator-remarkup-embed-layout-'.$options['layout'];
break;
default:
$layout_class = 'phabricator-remarkup-embed-layout-left';
break;
}
if ($options['float']) {
switch ($options['layout']) {
case 'center':
case 'inline':
break;
case 'right':
$layout_class .= ' phabricator-remarkup-embed-float-right';
break;
case 'left':
default:
$layout_class .= ' phabricator-remarkup-embed-float-left';
break;
}
}
return phutil_tag(
($options['layout'] == 'inline' ? 'span' : 'div'),
array(
'class' => $layout_class,
),
$embed);
}
private function renderAudioFile(
PhabricatorFile $file,
PhabricatorObjectHandle $handle,
array $options) {
if (idx($options, 'autoplay')) {
$preload = 'auto';
$autoplay = 'autoplay';
} else {
$preload = 'none';
$autoplay = null;
}
return $this->newTag(
'audio',
array(
'controls' => 'controls',
'preload' => $preload,
'autoplay' => $autoplay,
'loop' => idx($options, 'loop') ? 'loop' : null,
),
$this->newTag(
'source',
array(
'src' => $file->getBestURI(),
'type' => $file->getMimeType(),
)));
}
private function renderFileLink(
PhabricatorFile $file,
PhabricatorObjectHandle $handle,
array $options) {
return id(new PhabricatorFileLinkView())
->setFilePHID($file->getPHID())
->setFileName($this->assertFlatText($options['name']))
->setFileDownloadURI($file->getDownloadURI())
->setFileViewURI($file->getBestURI())
->setFileViewable((bool)$options['viewable']);
}
private function parseDimension($string) {
$string = trim($string);
if (preg_match('/^(?:\d*\\.)?\d+%?$/', $string)) {
return $string;
}
return null;
}
}
diff --git a/src/applications/files/query/PhabricatorFileQuery.php b/src/applications/files/query/PhabricatorFileQuery.php
index aa4e0be16..594dd6098 100644
--- a/src/applications/files/query/PhabricatorFileQuery.php
+++ b/src/applications/files/query/PhabricatorFileQuery.php
@@ -1,342 +1,345 @@
<?php
final class PhabricatorFileQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $ids;
private $phids;
private $authorPHIDs;
private $explicitUploads;
private $transforms;
private $dateCreatedAfter;
private $dateCreatedBefore;
private $contentHashes;
private $minLength;
private $maxLength;
private $names;
private $isPartial;
public function withIDs(array $ids) {
$this->ids = $ids;
return $this;
}
public function withPHIDs(array $phids) {
$this->phids = $phids;
return $this;
}
public function withAuthorPHIDs(array $phids) {
$this->authorPHIDs = $phids;
return $this;
}
public function withDateCreatedBefore($date_created_before) {
$this->dateCreatedBefore = $date_created_before;
return $this;
}
public function withDateCreatedAfter($date_created_after) {
$this->dateCreatedAfter = $date_created_after;
return $this;
}
public function withContentHashes(array $content_hashes) {
$this->contentHashes = $content_hashes;
return $this;
}
/**
* Select files which are transformations of some other file. For example,
* you can use this query to find previously generated thumbnails of an image
* file.
*
* As a parameter, provide a list of transformation specifications. Each
* specification is a dictionary with the keys `originalPHID` and `transform`.
* The `originalPHID` is the PHID of the original file (the file which was
* transformed) and the `transform` is the name of the transform to query
* for. If you pass `true` as the `transform`, all transformations of the
* file will be selected.
*
* For example:
*
* array(
* array(
* 'originalPHID' => 'PHID-FILE-aaaa',
* 'transform' => 'sepia',
* ),
* array(
* 'originalPHID' => 'PHID-FILE-bbbb',
* 'transform' => true,
* ),
* )
*
* This selects the `"sepia"` transformation of the file with PHID
* `PHID-FILE-aaaa` and all transformations of the file with PHID
* `PHID-FILE-bbbb`.
*
* @param list<dict> List of transform specifications, described above.
* @return this
*/
public function withTransforms(array $specs) {
foreach ($specs as $spec) {
if (!is_array($spec) ||
empty($spec['originalPHID']) ||
empty($spec['transform'])) {
throw new Exception(
- "Transform specification must be a dictionary with keys ".
- "'originalPHID' and 'transform'!");
+ pht(
+ "Transform specification must be a dictionary with keys ".
+ "'%s' and '%s'!",
+ 'originalPHID',
+ 'transform'));
}
}
$this->transforms = $specs;
return $this;
}
public function withLengthBetween($min, $max) {
$this->minLength = $min;
$this->maxLength = $max;
return $this;
}
public function withNames(array $names) {
$this->names = $names;
return $this;
}
public function withIsPartial($partial) {
$this->isPartial = $partial;
return $this;
}
public function showOnlyExplicitUploads($explicit_uploads) {
$this->explicitUploads = $explicit_uploads;
return $this;
}
protected function loadPage() {
$table = new PhabricatorFile();
$conn_r = $table->establishConnection('r');
$data = queryfx_all(
$conn_r,
'SELECT f.* FROM %T f %Q %Q %Q %Q',
$table->getTableName(),
$this->buildJoinClause($conn_r),
$this->buildWhereClause($conn_r),
$this->buildOrderClause($conn_r),
$this->buildLimitClause($conn_r));
$files = $table->loadAllFromArray($data);
if (!$files) {
return $files;
}
// We need to load attached objects to perform policy checks for files.
// First, load the edges.
$edge_type = PhabricatorFileHasObjectEdgeType::EDGECONST;
$file_phids = mpull($files, 'getPHID');
$edges = id(new PhabricatorEdgeQuery())
->withSourcePHIDs($file_phids)
->withEdgeTypes(array($edge_type))
->execute();
$object_phids = array();
foreach ($files as $file) {
$phids = array_keys($edges[$file->getPHID()][$edge_type]);
$file->attachObjectPHIDs($phids);
foreach ($phids as $phid) {
$object_phids[$phid] = true;
}
}
// If this file is a transform of another file, load that file too. If you
// can see the original file, you can see the thumbnail.
// TODO: It might be nice to put this directly on PhabricatorFile and remove
// the PhabricatorTransformedFile table, which would be a little simpler.
$xforms = id(new PhabricatorTransformedFile())->loadAllWhere(
'transformedPHID IN (%Ls)',
$file_phids);
$xform_phids = mpull($xforms, 'getOriginalPHID', 'getTransformedPHID');
foreach ($xform_phids as $derived_phid => $original_phid) {
$object_phids[$original_phid] = true;
}
$object_phids = array_keys($object_phids);
// Now, load the objects.
$objects = array();
if ($object_phids) {
// NOTE: We're explicitly turning policy exceptions off, since the rule
// here is "you can see the file if you can see ANY associated object".
// Without this explicit flag, we'll incorrectly throw unless you can
// see ALL associated objects.
$objects = id(new PhabricatorObjectQuery())
->setParentQuery($this)
->setViewer($this->getViewer())
->withPHIDs($object_phids)
->setRaisePolicyExceptions(false)
->execute();
$objects = mpull($objects, null, 'getPHID');
}
foreach ($files as $file) {
$file_objects = array_select_keys($objects, $file->getObjectPHIDs());
$file->attachObjects($file_objects);
}
foreach ($files as $key => $file) {
$original_phid = idx($xform_phids, $file->getPHID());
if ($original_phid == PhabricatorPHIDConstants::PHID_VOID) {
// This is a special case for builtin files, which are handled
// oddly.
$original = null;
} else if ($original_phid) {
$original = idx($objects, $original_phid);
if (!$original) {
// If the viewer can't see the original file, also prevent them from
// seeing the transformed file.
$this->didRejectResult($file);
unset($files[$key]);
continue;
}
} else {
$original = null;
}
$file->attachOriginalFile($original);
}
return $files;
}
protected function buildJoinClause(AphrontDatabaseConnection $conn_r) {
$joins = array();
if ($this->transforms) {
$joins[] = qsprintf(
$conn_r,
'JOIN %T t ON t.transformedPHID = f.phid',
id(new PhabricatorTransformedFile())->getTableName());
}
return implode(' ', $joins);
}
protected function buildWhereClause(AphrontDatabaseConnection $conn_r) {
$where = array();
$where[] = $this->buildPagingClause($conn_r);
if ($this->ids !== null) {
$where[] = qsprintf(
$conn_r,
'f.id IN (%Ld)',
$this->ids);
}
if ($this->phids !== null) {
$where[] = qsprintf(
$conn_r,
'f.phid IN (%Ls)',
$this->phids);
}
if ($this->authorPHIDs !== null) {
$where[] = qsprintf(
$conn_r,
'f.authorPHID IN (%Ls)',
$this->authorPHIDs);
}
if ($this->explicitUploads !== null) {
$where[] = qsprintf(
$conn_r,
'f.isExplicitUpload = true');
}
if ($this->transforms !== null) {
$clauses = array();
foreach ($this->transforms as $transform) {
if ($transform['transform'] === true) {
$clauses[] = qsprintf(
$conn_r,
'(t.originalPHID = %s)',
$transform['originalPHID']);
} else {
$clauses[] = qsprintf(
$conn_r,
'(t.originalPHID = %s AND t.transform = %s)',
$transform['originalPHID'],
$transform['transform']);
}
}
$where[] = qsprintf($conn_r, '(%Q)', implode(') OR (', $clauses));
}
if ($this->dateCreatedAfter !== null) {
$where[] = qsprintf(
$conn_r,
'f.dateCreated >= %d',
$this->dateCreatedAfter);
}
if ($this->dateCreatedBefore !== null) {
$where[] = qsprintf(
$conn_r,
'f.dateCreated <= %d',
$this->dateCreatedBefore);
}
if ($this->contentHashes !== null) {
$where[] = qsprintf(
$conn_r,
'f.contentHash IN (%Ls)',
$this->contentHashes);
}
if ($this->minLength !== null) {
$where[] = qsprintf(
$conn_r,
'byteSize >= %d',
$this->minLength);
}
if ($this->maxLength !== null) {
$where[] = qsprintf(
$conn_r,
'byteSize <= %d',
$this->maxLength);
}
if ($this->names !== null) {
$where[] = qsprintf(
$conn_r,
'name in (%Ls)',
$this->names);
}
if ($this->isPartial !== null) {
$where[] = qsprintf(
$conn_r,
'isPartial = %d',
(int)$this->isPartial);
}
return $this->formatWhereClause($where);
}
protected function getPrimaryTableAlias() {
return 'f';
}
public function getQueryApplicationClass() {
return 'PhabricatorFilesApplication';
}
}
diff --git a/src/applications/files/storage/PhabricatorFile.php b/src/applications/files/storage/PhabricatorFile.php
index c236a2368..d60107ee6 100644
--- a/src/applications/files/storage/PhabricatorFile.php
+++ b/src/applications/files/storage/PhabricatorFile.php
@@ -1,1386 +1,1390 @@
<?php
/**
* Parameters
* ==========
*
* When creating a new file using a method like @{method:newFromFileData}, these
* parameters are supported:
*
* | name | Human readable filename.
* | authorPHID | User PHID of uploader.
* | ttl | Temporary file lifetime, in seconds.
* | viewPolicy | File visibility policy.
* | isExplicitUpload | Used to show users files they explicitly uploaded.
* | canCDN | Allows the file to be cached and delivered over a CDN.
* | mime-type | Optional, explicit file MIME type.
* | builtin | Optional filename, identifies this as a builtin.
*
*/
final class PhabricatorFile extends PhabricatorFileDAO
implements
PhabricatorApplicationTransactionInterface,
PhabricatorTokenReceiverInterface,
PhabricatorSubscribableInterface,
PhabricatorFlaggableInterface,
PhabricatorPolicyInterface,
PhabricatorDestructibleInterface {
const ONETIME_TEMPORARY_TOKEN_TYPE = 'file:onetime';
const STORAGE_FORMAT_RAW = 'raw';
const METADATA_IMAGE_WIDTH = 'width';
const METADATA_IMAGE_HEIGHT = 'height';
const METADATA_CAN_CDN = 'canCDN';
const METADATA_BUILTIN = 'builtin';
const METADATA_PARTIAL = 'partial';
const METADATA_PROFILE = 'profile';
protected $name;
protected $mimeType;
protected $byteSize;
protected $authorPHID;
protected $secretKey;
protected $contentHash;
protected $metadata = array();
protected $mailKey;
protected $storageEngine;
protected $storageFormat;
protected $storageHandle;
protected $ttl;
protected $isExplicitUpload = 1;
protected $viewPolicy = PhabricatorPolicies::POLICY_USER;
protected $isPartial = 0;
private $objects = self::ATTACHABLE;
private $objectPHIDs = self::ATTACHABLE;
private $originalFile = self::ATTACHABLE;
public static function initializeNewFile() {
$app = id(new PhabricatorApplicationQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withClasses(array('PhabricatorFilesApplication'))
->executeOne();
$view_policy = $app->getPolicy(
FilesDefaultViewCapability::CAPABILITY);
return id(new PhabricatorFile())
->setViewPolicy($view_policy)
->setIsPartial(0)
->attachOriginalFile(null)
->attachObjects(array())
->attachObjectPHIDs(array());
}
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'metadata' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'name' => 'text255?',
'mimeType' => 'text255?',
'byteSize' => 'uint64',
'storageEngine' => 'text32',
'storageFormat' => 'text32',
'storageHandle' => 'text255',
'authorPHID' => 'phid?',
'secretKey' => 'bytes20?',
'contentHash' => 'bytes40?',
'ttl' => 'epoch?',
'isExplicitUpload' => 'bool?',
'mailKey' => 'bytes20',
'isPartial' => 'bool',
),
self::CONFIG_KEY_SCHEMA => array(
'key_phid' => null,
'phid' => array(
'columns' => array('phid'),
'unique' => true,
),
'authorPHID' => array(
'columns' => array('authorPHID'),
),
'contentHash' => array(
'columns' => array('contentHash'),
),
'key_ttl' => array(
'columns' => array('ttl'),
),
'key_dateCreated' => array(
'columns' => array('dateCreated'),
),
'key_partial' => array(
'columns' => array('authorPHID', 'isPartial'),
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhabricatorFileFilePHIDType::TYPECONST);
}
public function save() {
if (!$this->getSecretKey()) {
$this->setSecretKey($this->generateSecretKey());
}
if (!$this->getMailKey()) {
$this->setMailKey(Filesystem::readRandomCharacters(20));
}
return parent::save();
}
public function getMonogram() {
return 'F'.$this->getID();
}
public static function readUploadedFileData($spec) {
if (!$spec) {
- throw new Exception('No file was uploaded!');
+ throw new Exception(pht('No file was uploaded!'));
}
$err = idx($spec, 'error');
if ($err) {
throw new PhabricatorFileUploadException($err);
}
$tmp_name = idx($spec, 'tmp_name');
$is_valid = @is_uploaded_file($tmp_name);
if (!$is_valid) {
- throw new Exception('File is not an uploaded file.');
+ throw new Exception(pht('File is not an uploaded file.'));
}
$file_data = Filesystem::readFile($tmp_name);
$file_size = idx($spec, 'size');
if (strlen($file_data) != $file_size) {
- throw new Exception('File size disagrees with uploaded size.');
+ throw new Exception(pht('File size disagrees with uploaded size.'));
}
return $file_data;
}
public static function newFromPHPUpload($spec, array $params = array()) {
$file_data = self::readUploadedFileData($spec);
$file_name = nonempty(
idx($params, 'name'),
idx($spec, 'name'));
$params = array(
'name' => $file_name,
) + $params;
return self::newFromFileData($file_data, $params);
}
public static function newFromXHRUpload($data, array $params = array()) {
return self::newFromFileData($data, $params);
}
/**
* Given a block of data, try to load an existing file with the same content
* if one exists. If it does not, build a new file.
*
* This method is generally used when we have some piece of semi-trusted data
* like a diff or a file from a repository that we want to show to the user.
* We can't just dump it out because it may be dangerous for any number of
* reasons; instead, we need to serve it through the File abstraction so it
* ends up on the CDN domain if one is configured and so on. However, if we
* simply wrote a new file every time we'd potentially end up with a lot
* of redundant data in file storage.
*
* To solve these problems, we use file storage as a cache and reuse the
* same file again if we've previously written it.
*
* NOTE: This method unguards writes.
*
* @param string Raw file data.
* @param dict Dictionary of file information.
*/
public static function buildFromFileDataOrHash(
$data,
array $params = array()) {
$file = id(new PhabricatorFile())->loadOneWhere(
'name = %s AND contentHash = %s LIMIT 1',
idx($params, 'name'),
self::hashFileContent($data));
if (!$file) {
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$file = self::newFromFileData($data, $params);
unset($unguarded);
}
return $file;
}
public static function newFileFromContentHash($hash, array $params) {
// Check to see if a file with same contentHash exist
$file = id(new PhabricatorFile())->loadOneWhere(
'contentHash = %s LIMIT 1',
$hash);
if ($file) {
// copy storageEngine, storageHandle, storageFormat
$copy_of_storage_engine = $file->getStorageEngine();
$copy_of_storage_handle = $file->getStorageHandle();
$copy_of_storage_format = $file->getStorageFormat();
$copy_of_byte_size = $file->getByteSize();
$copy_of_mime_type = $file->getMimeType();
$new_file = self::initializeNewFile();
$new_file->setByteSize($copy_of_byte_size);
$new_file->setContentHash($hash);
$new_file->setStorageEngine($copy_of_storage_engine);
$new_file->setStorageHandle($copy_of_storage_handle);
$new_file->setStorageFormat($copy_of_storage_format);
$new_file->setMimeType($copy_of_mime_type);
$new_file->copyDimensions($file);
$new_file->readPropertiesFromParameters($params);
$new_file->save();
return $new_file;
}
return $file;
}
public static function newChunkedFile(
PhabricatorFileStorageEngine $engine,
$length,
array $params) {
$file = self::initializeNewFile();
$file->setByteSize($length);
// TODO: We might be able to test the first chunk in order to figure
// this out more reliably, since MIME detection usually examines headers.
// However, enormous files are probably always either actually raw data
// or reasonable to treat like raw data.
$file->setMimeType('application/octet-stream');
$chunked_hash = idx($params, 'chunkedHash');
if ($chunked_hash) {
$file->setContentHash($chunked_hash);
} else {
// See PhabricatorChunkedFileStorageEngine::getChunkedHash() for some
// discussion of this.
$seed = Filesystem::readRandomBytes(64);
$hash = PhabricatorChunkedFileStorageEngine::getChunkedHashForInput(
$seed);
$file->setContentHash($hash);
}
$file->setStorageEngine($engine->getEngineIdentifier());
$file->setStorageHandle(PhabricatorFileChunk::newChunkHandle());
$file->setStorageFormat(self::STORAGE_FORMAT_RAW);
$file->setIsPartial(1);
$file->readPropertiesFromParameters($params);
return $file;
}
private static function buildFromFileData($data, array $params = array()) {
if (isset($params['storageEngines'])) {
$engines = $params['storageEngines'];
} else {
$size = strlen($data);
$engines = PhabricatorFileStorageEngine::loadStorageEngines($size);
if (!$engines) {
throw new Exception(
pht(
'No configured storage engine can store this file. See '.
'"Configuring File Storage" in the documentation for '.
'information on configuring storage engines.'));
}
}
assert_instances_of($engines, 'PhabricatorFileStorageEngine');
if (!$engines) {
throw new Exception(pht('No valid storage engines are available!'));
}
$file = self::initializeNewFile();
$data_handle = null;
$engine_identifier = null;
$exceptions = array();
foreach ($engines as $engine) {
$engine_class = get_class($engine);
try {
list($engine_identifier, $data_handle) = $file->writeToEngine(
$engine,
$data,
$params);
// We stored the file somewhere so stop trying to write it to other
// places.
break;
} catch (PhabricatorFileStorageConfigurationException $ex) {
// If an engine is outright misconfigured (or misimplemented), raise
// that immediately since it probably needs attention.
throw $ex;
} catch (Exception $ex) {
phlog($ex);
// If an engine doesn't work, keep trying all the other valid engines
// in case something else works.
$exceptions[$engine_class] = $ex;
}
}
if (!$data_handle) {
throw new PhutilAggregateException(
- 'All storage engines failed to write file:',
+ pht('All storage engines failed to write file:'),
$exceptions);
}
$file->setByteSize(strlen($data));
$file->setContentHash(self::hashFileContent($data));
$file->setStorageEngine($engine_identifier);
$file->setStorageHandle($data_handle);
// TODO: This is probably YAGNI, but allows for us to do encryption or
// compression later if we want.
$file->setStorageFormat(self::STORAGE_FORMAT_RAW);
$file->readPropertiesFromParameters($params);
if (!$file->getMimeType()) {
$tmp = new TempFile();
Filesystem::writeFile($tmp, $data);
$file->setMimeType(Filesystem::getMimeType($tmp));
}
try {
$file->updateDimensions(false);
} catch (Exception $ex) {
// Do nothing
}
$file->save();
return $file;
}
public static function newFromFileData($data, array $params = array()) {
$hash = self::hashFileContent($data);
$file = self::newFileFromContentHash($hash, $params);
if ($file) {
return $file;
}
return self::buildFromFileData($data, $params);
}
public function migrateToEngine(PhabricatorFileStorageEngine $engine) {
if (!$this->getID() || !$this->getStorageHandle()) {
throw new Exception(
- "You can not migrate a file which hasn't yet been saved.");
+ pht("You can not migrate a file which hasn't yet been saved."));
}
$data = $this->loadFileData();
$params = array(
'name' => $this->getName(),
);
list($new_identifier, $new_handle) = $this->writeToEngine(
$engine,
$data,
$params);
$old_engine = $this->instantiateStorageEngine();
$old_identifier = $this->getStorageEngine();
$old_handle = $this->getStorageHandle();
$this->setStorageEngine($new_identifier);
$this->setStorageHandle($new_handle);
$this->save();
$this->deleteFileDataIfUnused(
$old_engine,
$old_identifier,
$old_handle);
return $this;
}
private function writeToEngine(
PhabricatorFileStorageEngine $engine,
$data,
array $params) {
$engine_class = get_class($engine);
$data_handle = $engine->writeFile($data, $params);
if (!$data_handle || strlen($data_handle) > 255) {
// This indicates an improperly implemented storage engine.
throw new PhabricatorFileStorageConfigurationException(
- "Storage engine '{$engine_class}' executed writeFile() but did ".
- "not return a valid handle ('{$data_handle}') to the data: it ".
- "must be nonempty and no longer than 255 characters.");
+ pht(
+ "Storage engine '%s' executed %s but did not return a valid ".
+ "handle ('%s') to the data: it must be nonempty and no longer ".
+ "than 255 characters.",
+ $engine_class,
+ 'writeFile()',
+ $data_handle));
}
$engine_identifier = $engine->getEngineIdentifier();
if (!$engine_identifier || strlen($engine_identifier) > 32) {
throw new PhabricatorFileStorageConfigurationException(
- "Storage engine '{$engine_class}' returned an improper engine ".
- "identifier '{$engine_identifier}': it must be nonempty ".
- "and no longer than 32 characters.");
+ pht(
+ "Storage engine '%s' returned an improper engine identifier '{%s}': ".
+ "it must be nonempty and no longer than 32 characters.",
+ $engine_class,
+ $engine_identifier));
}
return array($engine_identifier, $data_handle);
}
/**
* Download a remote resource over HTTP and save the response body as a file.
*
* This method respects `security.outbound-blacklist`, and protects against
* HTTP redirection (by manually following "Location" headers and verifying
* each destination). It does not protect against DNS rebinding. See
* discussion in T6755.
*/
public static function newFromFileDownload($uri, array $params = array()) {
$timeout = 5;
$redirects = array();
$current = $uri;
while (true) {
try {
if (count($redirects) > 10) {
throw new Exception(
pht('Too many redirects trying to fetch remote URI.'));
}
$resolved = PhabricatorEnv::requireValidRemoteURIForFetch(
$current,
array(
'http',
'https',
));
list($resolved_uri, $resolved_domain) = $resolved;
$current = new PhutilURI($current);
if ($current->getProtocol() == 'http') {
// For HTTP, we can use a pre-resolved URI to defuse DNS rebinding.
$fetch_uri = $resolved_uri;
$fetch_host = $resolved_domain;
} else {
// For HTTPS, we can't: cURL won't verify the SSL certificate if
// the domain has been replaced with an IP. But internal services
// presumably will not have valid certificates for rebindable
// domain names on attacker-controlled domains, so the DNS rebinding
// attack should generally not be possible anyway.
$fetch_uri = $current;
$fetch_host = null;
}
$future = id(new HTTPSFuture($fetch_uri))
->setFollowLocation(false)
->setTimeout($timeout);
if ($fetch_host !== null) {
$future->addHeader('Host', $fetch_host);
}
list($status, $body, $headers) = $future->resolve();
if ($status->isRedirect()) {
// This is an HTTP 3XX status, so look for a "Location" header.
$location = null;
foreach ($headers as $header) {
list($name, $value) = $header;
if (phutil_utf8_strtolower($name) == 'location') {
$location = $value;
break;
}
}
// HTTP 3XX status with no "Location" header, just treat this like
// a normal HTTP error.
if ($location === null) {
throw $status;
}
if (isset($redirects[$location])) {
throw new Exception(
- pht(
- 'Encountered loop while following redirects.'));
+ pht('Encountered loop while following redirects.'));
}
$redirects[$location] = $location;
$current = $location;
// We'll fall off the bottom and go try this URI now.
} else if ($status->isError()) {
// This is something other than an HTTP 2XX or HTTP 3XX status, so
// just bail out.
throw $status;
} else {
// This is HTTP 2XX, so use the the response body to save the
// file data.
$params = $params + array(
'name' => basename($uri),
);
return self::newFromFileData($body, $params);
}
} catch (Exception $ex) {
if ($redirects) {
throw new PhutilProxyException(
pht(
'Failed to fetch remote URI "%s" after following %s redirect(s) '.
'(%s): %s',
$uri,
new PhutilNumber(count($redirects)),
implode(' > ', array_keys($redirects)),
$ex->getMessage()),
$ex);
} else {
throw $ex;
}
}
}
}
public static function normalizeFileName($file_name) {
$pattern = "@[\\x00-\\x19#%&+!~'\$\"\/=\\\\?<> ]+@";
$file_name = preg_replace($pattern, '_', $file_name);
$file_name = preg_replace('@_+@', '_', $file_name);
$file_name = trim($file_name, '_');
$disallowed_filenames = array(
'.' => 'dot',
'..' => 'dotdot',
'' => 'file',
);
$file_name = idx($disallowed_filenames, $file_name, $file_name);
return $file_name;
}
public function delete() {
// We want to delete all the rows which mark this file as the transformation
// of some other file (since we're getting rid of it). We also delete all
// the transformations of this file, so that a user who deletes an image
// doesn't need to separately hunt down and delete a bunch of thumbnails and
// resizes of it.
$outbound_xforms = id(new PhabricatorFileQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withTransforms(
array(
array(
'originalPHID' => $this->getPHID(),
'transform' => true,
),
))
->execute();
foreach ($outbound_xforms as $outbound_xform) {
$outbound_xform->delete();
}
$inbound_xforms = id(new PhabricatorTransformedFile())->loadAllWhere(
'transformedPHID = %s',
$this->getPHID());
$this->openTransaction();
foreach ($inbound_xforms as $inbound_xform) {
$inbound_xform->delete();
}
$ret = parent::delete();
$this->saveTransaction();
$this->deleteFileDataIfUnused(
$this->instantiateStorageEngine(),
$this->getStorageEngine(),
$this->getStorageHandle());
return $ret;
}
/**
* Destroy stored file data if there are no remaining files which reference
* it.
*/
public function deleteFileDataIfUnused(
PhabricatorFileStorageEngine $engine,
$engine_identifier,
$handle) {
// Check to see if any files are using storage.
$usage = id(new PhabricatorFile())->loadAllWhere(
'storageEngine = %s AND storageHandle = %s LIMIT 1',
$engine_identifier,
$handle);
// If there are no files using the storage, destroy the actual storage.
if (!$usage) {
try {
$engine->deleteFile($handle);
} catch (Exception $ex) {
// In the worst case, we're leaving some data stranded in a storage
// engine, which is not a big deal.
phlog($ex);
}
}
}
public static function hashFileContent($data) {
return sha1($data);
}
public function loadFileData() {
$engine = $this->instantiateStorageEngine();
$data = $engine->readFile($this->getStorageHandle());
switch ($this->getStorageFormat()) {
case self::STORAGE_FORMAT_RAW:
$data = $data;
break;
default:
- throw new Exception('Unknown storage format.');
+ throw new Exception(pht('Unknown storage format.'));
}
return $data;
}
/**
* Return an iterable which emits file content bytes.
*
* @param int Offset for the start of data.
* @param int Offset for the end of data.
* @return Iterable Iterable object which emits requested data.
*/
public function getFileDataIterator($begin = null, $end = null) {
$engine = $this->instantiateStorageEngine();
return $engine->getFileDataIterator($this, $begin, $end);
}
public function getViewURI() {
if (!$this->getPHID()) {
throw new Exception(
- 'You must save a file before you can generate a view URI.');
+ pht('You must save a file before you can generate a view URI.'));
}
return $this->getCDNURI(null);
}
private function getCDNURI($token) {
$name = self::normalizeFileName($this->getName());
$name = phutil_escape_uri($name);
$parts = array();
$parts[] = 'file';
$parts[] = 'data';
// If this is an instanced install, add the instance identifier to the URI.
// Instanced configurations behind a CDN may not be able to control the
// request domain used by the CDN (as with AWS CloudFront). Embedding the
// instance identity in the path allows us to distinguish between requests
// originating from different instances but served through the same CDN.
$instance = PhabricatorEnv::getEnvConfig('cluster.instance');
if (strlen($instance)) {
$parts[] = '@'.$instance;
}
$parts[] = $this->getSecretKey();
$parts[] = $this->getPHID();
if ($token) {
$parts[] = $token;
}
$parts[] = $name;
$path = '/'.implode('/', $parts);
// If this file is only partially uploaded, we're just going to return a
// local URI to make sure that Ajax works, since the page is inevitably
// going to give us an error back.
if ($this->getIsPartial()) {
return PhabricatorEnv::getURI($path);
} else {
return PhabricatorEnv::getCDNURI($path);
}
}
/**
* Get the CDN URI for this file, including a one-time-use security token.
*
*/
public function getCDNURIWithToken() {
if (!$this->getPHID()) {
throw new Exception(
- 'You must save a file before you can generate a CDN URI.');
+ pht('You must save a file before you can generate a CDN URI.'));
}
return $this->getCDNURI($this->generateOneTimeToken());
}
public function getInfoURI() {
return '/'.$this->getMonogram();
}
public function getBestURI() {
if ($this->isViewableInBrowser()) {
return $this->getViewURI();
} else {
return $this->getInfoURI();
}
}
public function getDownloadURI() {
$uri = id(new PhutilURI($this->getViewURI()))
->setQueryParam('download', true);
return (string)$uri;
}
public function getURIForTransform(PhabricatorFileTransform $transform) {
return $this->getTransformedURI($transform->getTransformKey());
}
private function getTransformedURI($transform) {
$parts = array();
$parts[] = 'file';
$parts[] = 'xform';
$instance = PhabricatorEnv::getEnvConfig('cluster.instance');
if (strlen($instance)) {
$parts[] = '@'.$instance;
}
$parts[] = $transform;
$parts[] = $this->getPHID();
$parts[] = $this->getSecretKey();
$path = implode('/', $parts);
$path = $path.'/';
return PhabricatorEnv::getCDNURI($path);
}
public function isViewableInBrowser() {
return ($this->getViewableMimeType() !== null);
}
public function isViewableImage() {
if (!$this->isViewableInBrowser()) {
return false;
}
$mime_map = PhabricatorEnv::getEnvConfig('files.image-mime-types');
$mime_type = $this->getMimeType();
return idx($mime_map, $mime_type);
}
public function isAudio() {
if (!$this->isViewableInBrowser()) {
return false;
}
$mime_map = PhabricatorEnv::getEnvConfig('files.audio-mime-types');
$mime_type = $this->getMimeType();
return idx($mime_map, $mime_type);
}
public function isTransformableImage() {
// NOTE: The way the 'gd' extension works in PHP is that you can install it
// with support for only some file types, so it might be able to handle
// PNG but not JPEG. Try to generate thumbnails for whatever we can. Setup
// warns you if you don't have complete support.
$matches = null;
$ok = preg_match(
'@^image/(gif|png|jpe?g)@',
$this->getViewableMimeType(),
$matches);
if (!$ok) {
return false;
}
switch ($matches[1]) {
case 'jpg';
case 'jpeg':
return function_exists('imagejpeg');
break;
case 'png':
return function_exists('imagepng');
break;
case 'gif':
return function_exists('imagegif');
break;
default:
- throw new Exception('Unknown type matched as image MIME type.');
+ throw new Exception(pht('Unknown type matched as image MIME type.'));
}
}
public static function getTransformableImageFormats() {
$supported = array();
if (function_exists('imagejpeg')) {
$supported[] = 'jpg';
}
if (function_exists('imagepng')) {
$supported[] = 'png';
}
if (function_exists('imagegif')) {
$supported[] = 'gif';
}
return $supported;
}
public function instantiateStorageEngine() {
return self::buildEngine($this->getStorageEngine());
}
public static function buildEngine($engine_identifier) {
$engines = self::buildAllEngines();
foreach ($engines as $engine) {
if ($engine->getEngineIdentifier() == $engine_identifier) {
return $engine;
}
}
throw new Exception(
- "Storage engine '{$engine_identifier}' could not be located!");
+ pht(
+ "Storage engine '%s' could not be located!",
+ $engine_identifier));
}
public static function buildAllEngines() {
$engines = id(new PhutilSymbolLoader())
->setType('class')
->setConcreteOnly(true)
->setAncestorClass('PhabricatorFileStorageEngine')
->selectAndLoadSymbols();
$results = array();
foreach ($engines as $engine_class) {
$results[] = newv($engine_class['name'], array());
}
return $results;
}
public function getViewableMimeType() {
$mime_map = PhabricatorEnv::getEnvConfig('files.viewable-mime-types');
$mime_type = $this->getMimeType();
$mime_parts = explode(';', $mime_type);
$mime_type = trim(reset($mime_parts));
return idx($mime_map, $mime_type);
}
public function getDisplayIconForMimeType() {
$mime_map = PhabricatorEnv::getEnvConfig('files.icon-mime-types');
$mime_type = $this->getMimeType();
return idx($mime_map, $mime_type, 'fa-file-o');
}
public function validateSecretKey($key) {
return ($key == $this->getSecretKey());
}
public function generateSecretKey() {
return Filesystem::readRandomCharacters(20);
}
public function updateDimensions($save = true) {
if (!$this->isViewableImage()) {
- throw new Exception(
- 'This file is not a viewable image.');
+ throw new Exception(pht('This file is not a viewable image.'));
}
if (!function_exists('imagecreatefromstring')) {
- throw new Exception(
- 'Cannot retrieve image information.');
+ throw new Exception(pht('Cannot retrieve image information.'));
}
$data = $this->loadFileData();
$img = imagecreatefromstring($data);
if ($img === false) {
- throw new Exception(
- 'Error when decoding image.');
+ throw new Exception(pht('Error when decoding image.'));
}
$this->metadata[self::METADATA_IMAGE_WIDTH] = imagesx($img);
$this->metadata[self::METADATA_IMAGE_HEIGHT] = imagesy($img);
if ($save) {
$this->save();
}
return $this;
}
public function copyDimensions(PhabricatorFile $file) {
$metadata = $file->getMetadata();
$width = idx($metadata, self::METADATA_IMAGE_WIDTH);
if ($width) {
$this->metadata[self::METADATA_IMAGE_WIDTH] = $width;
}
$height = idx($metadata, self::METADATA_IMAGE_HEIGHT);
if ($height) {
$this->metadata[self::METADATA_IMAGE_HEIGHT] = $height;
}
return $this;
}
/**
* Load (or build) the {@class:PhabricatorFile} objects for builtin file
* resources. The builtin mechanism allows files shipped with Phabricator
* to be treated like normal files so that APIs do not need to special case
* things like default images or deleted files.
*
* Builtins are located in `resources/builtin/` and identified by their
* name.
*
* @param PhabricatorUser Viewing user.
* @param list<string> List of builtin file names.
* @return dict<string, PhabricatorFile> Dictionary of named builtins.
*/
public static function loadBuiltins(PhabricatorUser $user, array $names) {
$specs = array();
foreach ($names as $name) {
$specs[] = array(
'originalPHID' => PhabricatorPHIDConstants::PHID_VOID,
'transform' => 'builtin:'.$name,
);
}
// NOTE: Anyone is allowed to access builtin files.
$files = id(new PhabricatorFileQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withTransforms($specs)
->execute();
$files = mpull($files, null, 'getName');
$root = dirname(phutil_get_library_root('phabricator'));
$root = $root.'/resources/builtin/';
$build = array();
foreach ($names as $name) {
if (isset($files[$name])) {
continue;
}
// This is just a sanity check to prevent loading arbitrary files.
if (basename($name) != $name) {
- throw new Exception("Invalid builtin name '{$name}'!");
+ throw new Exception(pht("Invalid builtin name '%s'!", $name));
}
$path = $root.$name;
if (!Filesystem::pathExists($path)) {
- throw new Exception("Builtin '{$path}' does not exist!");
+ throw new Exception(pht("Builtin '%s' does not exist!", $path));
}
$data = Filesystem::readFile($path);
$params = array(
'name' => $name,
'ttl' => time() + (60 * 60 * 24 * 7),
'canCDN' => true,
'builtin' => $name,
);
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$file = self::newFromFileData($data, $params);
$xform = id(new PhabricatorTransformedFile())
->setOriginalPHID(PhabricatorPHIDConstants::PHID_VOID)
->setTransform('builtin:'.$name)
->setTransformedPHID($file->getPHID())
->save();
unset($unguarded);
$file->attachObjectPHIDs(array());
$file->attachObjects(array());
$files[$name] = $file;
}
return $files;
}
/**
* Convenience wrapper for @{method:loadBuiltins}.
*
* @param PhabricatorUser Viewing user.
* @param string Single builtin name to load.
* @return PhabricatorFile Corresponding builtin file.
*/
public static function loadBuiltin(PhabricatorUser $user, $name) {
return idx(self::loadBuiltins($user, array($name)), $name);
}
public function getObjects() {
return $this->assertAttached($this->objects);
}
public function attachObjects(array $objects) {
$this->objects = $objects;
return $this;
}
public function getObjectPHIDs() {
return $this->assertAttached($this->objectPHIDs);
}
public function attachObjectPHIDs(array $object_phids) {
$this->objectPHIDs = $object_phids;
return $this;
}
public function getOriginalFile() {
return $this->assertAttached($this->originalFile);
}
public function attachOriginalFile(PhabricatorFile $file = null) {
$this->originalFile = $file;
return $this;
}
public function getImageHeight() {
if (!$this->isViewableImage()) {
return null;
}
return idx($this->metadata, self::METADATA_IMAGE_HEIGHT);
}
public function getImageWidth() {
if (!$this->isViewableImage()) {
return null;
}
return idx($this->metadata, self::METADATA_IMAGE_WIDTH);
}
public function getCanCDN() {
if (!$this->isViewableImage()) {
return false;
}
return idx($this->metadata, self::METADATA_CAN_CDN);
}
public function setCanCDN($can_cdn) {
$this->metadata[self::METADATA_CAN_CDN] = $can_cdn ? 1 : 0;
return $this;
}
public function isBuiltin() {
return ($this->getBuiltinName() !== null);
}
public function getBuiltinName() {
return idx($this->metadata, self::METADATA_BUILTIN);
}
public function setBuiltinName($name) {
$this->metadata[self::METADATA_BUILTIN] = $name;
return $this;
}
public function getIsProfileImage() {
return idx($this->metadata, self::METADATA_PROFILE);
}
public function setIsProfileImage($value) {
$this->metadata[self::METADATA_PROFILE] = $value;
return $this;
}
protected function generateOneTimeToken() {
$key = Filesystem::readRandomCharacters(16);
// Save the new secret.
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$token = id(new PhabricatorAuthTemporaryToken())
->setObjectPHID($this->getPHID())
->setTokenType(self::ONETIME_TEMPORARY_TOKEN_TYPE)
->setTokenExpires(time() + phutil_units('1 hour in seconds'))
->setTokenCode(PhabricatorHash::digest($key))
->save();
unset($unguarded);
return $key;
}
public function validateOneTimeToken($token_code) {
$token = id(new PhabricatorAuthTemporaryTokenQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withObjectPHIDs(array($this->getPHID()))
->withTokenTypes(array(self::ONETIME_TEMPORARY_TOKEN_TYPE))
->withExpired(false)
->withTokenCodes(array(PhabricatorHash::digest($token_code)))
->executeOne();
return $token;
}
/**
* Write the policy edge between this file and some object.
*
* @param phid Object PHID to attach to.
* @return this
*/
public function attachToObject($phid) {
$edge_type = PhabricatorObjectHasFileEdgeType::EDGECONST;
id(new PhabricatorEdgeEditor())
->addEdge($phid, $edge_type, $this->getPHID())
->save();
return $this;
}
/**
* Remove the policy edge between this file and some object.
*
* @param phid Object PHID to detach from.
* @return this
*/
public function detachFromObject($phid) {
$edge_type = PhabricatorObjectHasFileEdgeType::EDGECONST;
id(new PhabricatorEdgeEditor())
->removeEdge($phid, $edge_type, $this->getPHID())
->save();
return $this;
}
/**
* Configure a newly created file object according to specified parameters.
*
* This method is called both when creating a file from fresh data, and
* when creating a new file which reuses existing storage.
*
* @param map<string, wild> Bag of parameters, see @{class:PhabricatorFile}
* for documentation.
* @return this
*/
private function readPropertiesFromParameters(array $params) {
$file_name = idx($params, 'name');
$this->setName($file_name);
$author_phid = idx($params, 'authorPHID');
$this->setAuthorPHID($author_phid);
$file_ttl = idx($params, 'ttl');
$this->setTtl($file_ttl);
$view_policy = idx($params, 'viewPolicy');
if ($view_policy) {
$this->setViewPolicy($params['viewPolicy']);
}
$is_explicit = (idx($params, 'isExplicitUpload') ? 1 : 0);
$this->setIsExplicitUpload($is_explicit);
$can_cdn = idx($params, 'canCDN');
if ($can_cdn) {
$this->setCanCDN(true);
}
$builtin = idx($params, 'builtin');
if ($builtin) {
$this->setBuiltinName($builtin);
}
$profile = idx($params, 'profile');
if ($profile) {
$this->setIsProfileImage(true);
}
$mime_type = idx($params, 'mime-type');
if ($mime_type) {
$this->setMimeType($mime_type);
}
return $this;
}
public function getRedirectResponse() {
$uri = $this->getBestURI();
// TODO: This is a bit iffy. Sometimes, getBestURI() returns a CDN URI
// (if the file is a viewable image) and sometimes a local URI (if not).
// For now, just detect which one we got and configure the response
// appropriately. In the long run, if this endpoint is served from a CDN
// domain, we can't issue a local redirect to an info URI (which is not
// present on the CDN domain). We probably never actually issue local
// redirects here anyway, since we only ever transform viewable images
// right now.
$is_external = strlen(id(new PhutilURI($uri))->getDomain());
return id(new AphrontRedirectResponse())
->setIsExternal($is_external)
->setURI($uri);
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new PhabricatorFileEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new PhabricatorFileTransaction();
}
public function willRenderTimeline(
PhabricatorApplicationTransactionView $timeline,
AphrontRequest $request) {
return $timeline;
}
/* -( PhabricatorPolicyInterface Implementation )-------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
if ($this->isBuiltin()) {
return PhabricatorPolicies::getMostOpenPolicy();
}
if ($this->getIsProfileImage()) {
return PhabricatorPolicies::getMostOpenPolicy();
}
return $this->getViewPolicy();
case PhabricatorPolicyCapability::CAN_EDIT:
return PhabricatorPolicies::POLICY_NOONE;
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
$viewer_phid = $viewer->getPHID();
if ($viewer_phid) {
if ($this->getAuthorPHID() == $viewer_phid) {
return true;
}
}
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
// If you can see the file this file is a transform of, you can see
// this file.
if ($this->getOriginalFile()) {
return true;
}
// If you can see any object this file is attached to, you can see
// the file.
return (count($this->getObjects()) > 0);
}
return false;
}
public function describeAutomaticCapability($capability) {
$out = array();
$out[] = pht('The user who uploaded a file can always view and edit it.');
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
$out[] = pht(
'Files attached to objects are visible to users who can view '.
'those objects.');
$out[] = pht(
'Thumbnails are visible only to users who can view the original '.
'file.');
break;
}
return $out;
}
/* -( PhabricatorSubscribableInterface Implementation )-------------------- */
public function isAutomaticallySubscribed($phid) {
return ($this->authorPHID == $phid);
}
public function shouldShowSubscribersProperty() {
return true;
}
public function shouldAllowSubscription($phid) {
return true;
}
/* -( PhabricatorTokenReceiverInterface )---------------------------------- */
public function getUsersToNotifyOfTokenGiven() {
return array(
$this->getAuthorPHID(),
);
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$this->openTransaction();
$this->delete();
$this->saveTransaction();
}
}
diff --git a/src/applications/files/view/PhabricatorGlobalUploadTargetView.php b/src/applications/files/view/PhabricatorGlobalUploadTargetView.php
index 3624f2a73..3e0690815 100644
--- a/src/applications/files/view/PhabricatorGlobalUploadTargetView.php
+++ b/src/applications/files/view/PhabricatorGlobalUploadTargetView.php
@@ -1,62 +1,62 @@
<?php
/**
* IMPORTANT: If you use this, make sure to implement
*
* public function isGlobalDragAndDropUploadEnabled() {
* return true;
* }
*
* on the controller(s) that render this class...! This is necessary
* to make sure Quicksand works properly with the javascript in this
* UI.
*/
final class PhabricatorGlobalUploadTargetView extends AphrontView {
private $showIfSupportedID;
public function setShowIfSupportedID($show_if_supported_id) {
$this->showIfSupportedID = $show_if_supported_id;
return $this;
}
public function getShowIfSupportedID() {
return $this->showIfSupportedID;
}
public function render() {
$viewer = $this->getUser();
if (!$viewer->isLoggedIn()) {
return null;
}
$instructions_id = 'phabricator-global-drag-and-drop-upload-instructions';
require_celerity_resource('global-drag-and-drop-css');
// Use the configured default view policy. Drag and drop uploads use
// a more restrictive view policy if we don't specify a policy explicitly,
// as the more restrictive policy is correct for most drop targets (like
// Pholio uploads and Remarkup text areas).
$view_policy = PhabricatorFile::initializeNewFile()->getViewPolicy();
Javelin::initBehavior('global-drag-and-drop', array(
'ifSupported' => $this->showIfSupportedID,
'instructions' => $instructions_id,
'uploadURI' => '/file/dropupload/',
'browseURI' => '/file/query/authored/',
'viewPolicy' => $view_policy,
'chunkThreshold' => PhabricatorFileStorageEngine::getChunkThreshold(),
));
return phutil_tag(
'div',
array(
'id' => $instructions_id,
'class' => 'phabricator-global-upload-instructions',
'style' => 'display: none;',
),
- pht("\xE2\x87\xAA Drop Files to Upload"));
+ "\xE2\x87\xAA ".pht('Drop Files to Upload'));
}
}
diff --git a/src/applications/flag/conduit/FlagDeleteConduitAPIMethod.php b/src/applications/flag/conduit/FlagDeleteConduitAPIMethod.php
index 6634fa776..de6d98d7f 100644
--- a/src/applications/flag/conduit/FlagDeleteConduitAPIMethod.php
+++ b/src/applications/flag/conduit/FlagDeleteConduitAPIMethod.php
@@ -1,60 +1,60 @@
<?php
final class FlagDeleteConduitAPIMethod extends FlagConduitAPIMethod {
public function getAPIMethodName() {
return 'flag.delete';
}
public function getMethodDescription() {
- return 'Clear a flag.';
+ return pht('Clear a flag.');
}
protected function defineParamTypes() {
return array(
'id' => 'optional id',
'objectPHID' => 'optional phid',
);
}
protected function defineReturnType() {
return 'dict | null';
}
protected function defineErrorTypes() {
return array(
- 'ERR_NOT_FOUND' => 'Bad flag ID.',
- 'ERR_WRONG_USER' => 'You are not the creator of this flag.',
- 'ERR_NEED_PARAM' => 'Must pass an id or an objectPHID.',
+ 'ERR_NOT_FOUND' => pht('Bad flag ID.'),
+ 'ERR_WRONG_USER' => pht('You are not the creator of this flag.'),
+ 'ERR_NEED_PARAM' => pht('Must pass an id or an objectPHID.'),
);
}
protected function execute(ConduitAPIRequest $request) {
$id = $request->getValue('id');
$object = $request->getValue('objectPHID');
if ($id) {
$flag = id(new PhabricatorFlag())->load($id);
if (!$flag) {
throw new ConduitException('ERR_NOT_FOUND');
}
if ($flag->getOwnerPHID() != $request->getUser()->getPHID()) {
throw new ConduitException('ERR_WRONG_USER');
}
} else if ($object) {
$flag = id(new PhabricatorFlag())->loadOneWhere(
'objectPHID = %s AND ownerPHID = %s',
$object,
$request->getUser()->getPHID());
if (!$flag) {
return null;
}
} else {
throw new ConduitException('ERR_NEED_PARAM');
}
$this->attachHandleToFlag($flag, $request->getUser());
$ret = $this->buildFlagInfoDictionary($flag);
$flag->delete();
return $ret;
}
}
diff --git a/src/applications/flag/conduit/FlagEditConduitAPIMethod.php b/src/applications/flag/conduit/FlagEditConduitAPIMethod.php
index 5a07cdeb9..3bc0d7e29 100644
--- a/src/applications/flag/conduit/FlagEditConduitAPIMethod.php
+++ b/src/applications/flag/conduit/FlagEditConduitAPIMethod.php
@@ -1,60 +1,60 @@
<?php
final class FlagEditConduitAPIMethod extends FlagConduitAPIMethod {
public function getAPIMethodName() {
return 'flag.edit';
}
public function getMethodDescription() {
- return 'Create or modify a flag.';
+ return pht('Create or modify a flag.');
}
protected function defineParamTypes() {
return array(
'objectPHID' => 'required phid',
'color' => 'optional int',
'note' => 'optional string',
);
}
protected function defineReturnType() {
return 'dict';
}
protected function execute(ConduitAPIRequest $request) {
$user = $request->getUser()->getPHID();
$phid = $request->getValue('objectPHID');
$new = false;
$flag = id(new PhabricatorFlag())->loadOneWhere(
'objectPHID = %s AND ownerPHID = %s',
$phid,
$user);
if ($flag) {
$params = $request->getAllParameters();
if (isset($params['color'])) {
$flag->setColor($params['color']);
}
if (isset($params['note'])) {
$flag->setNote($params['note']);
}
} else {
$default_color = PhabricatorFlagColor::COLOR_BLUE;
$flag = id(new PhabricatorFlag())
->setOwnerPHID($user)
->setType(phid_get_type($phid))
->setObjectPHID($phid)
->setReasonPHID($user)
->setColor($request->getValue('color', $default_color))
->setNote($request->getValue('note', ''));
$new = true;
}
$this->attachHandleToFlag($flag, $request->getUser());
$flag->save();
$ret = $this->buildFlagInfoDictionary($flag);
$ret['new'] = $new;
return $ret;
}
}
diff --git a/src/applications/flag/conduit/FlagQueryConduitAPIMethod.php b/src/applications/flag/conduit/FlagQueryConduitAPIMethod.php
index 486a5afa9..0658093e4 100644
--- a/src/applications/flag/conduit/FlagQueryConduitAPIMethod.php
+++ b/src/applications/flag/conduit/FlagQueryConduitAPIMethod.php
@@ -1,62 +1,62 @@
<?php
final class FlagQueryConduitAPIMethod extends FlagConduitAPIMethod {
public function getAPIMethodName() {
return 'flag.query';
}
public function getMethodDescription() {
- return 'Query flag markers.';
+ return pht('Query flag markers.');
}
protected function defineParamTypes() {
return array(
'ownerPHIDs' => 'optional list<phid>',
'types' => 'optional list<type>',
'objectPHIDs' => 'optional list<phid>',
'offset' => 'optional int',
'limit' => 'optional int (default = 100)',
);
}
protected function defineReturnType() {
return 'list<dict>';
}
protected function execute(ConduitAPIRequest $request) {
$query = new PhabricatorFlagQuery();
$query->setViewer($request->getUser());
$owner_phids = $request->getValue('ownerPHIDs', array());
if ($owner_phids) {
$query->withOwnerPHIDs($owner_phids);
}
$object_phids = $request->getValue('objectPHIDs', array());
if ($object_phids) {
$query->withObjectPHIDs($object_phids);
}
$types = $request->getValue('types', array());
if ($types) {
$query->withTypes($types);
}
$query->needHandles(true);
$query->setOffset($request->getValue('offset', 0));
$query->setLimit($request->getValue('limit', 100));
$flags = $query->execute();
$results = array();
foreach ($flags as $flag) {
$results[] = $this->buildFlagInfoDictionary($flag);
}
return $results;
}
}
diff --git a/src/applications/flag/query/PhabricatorFlagQuery.php b/src/applications/flag/query/PhabricatorFlagQuery.php
index 0b1902f6f..ff154a512 100644
--- a/src/applications/flag/query/PhabricatorFlagQuery.php
+++ b/src/applications/flag/query/PhabricatorFlagQuery.php
@@ -1,165 +1,166 @@
<?php
final class PhabricatorFlagQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
const GROUP_COLOR = 'color';
const GROUP_NONE = 'none';
private $ownerPHIDs;
private $types;
private $objectPHIDs;
private $colors;
private $groupBy = self::GROUP_NONE;
private $needHandles;
private $needObjects;
public function withOwnerPHIDs(array $owner_phids) {
$this->ownerPHIDs = $owner_phids;
return $this;
}
public function withTypes(array $types) {
$this->types = $types;
return $this;
}
public function withObjectPHIDs(array $object_phids) {
$this->objectPHIDs = $object_phids;
return $this;
}
public function withColors(array $colors) {
$this->colors = $colors;
return $this;
}
/**
* NOTE: this is done in PHP and not in MySQL, which means its inappropriate
* for large datasets. Pragmatically, this is fine for user flags which are
* typically well under 100 flags per user.
*/
public function setGroupBy($group) {
$this->groupBy = $group;
return $this;
}
public function needHandles($need) {
$this->needHandles = $need;
return $this;
}
public function needObjects($need) {
$this->needObjects = $need;
return $this;
}
public static function loadUserFlag(PhabricatorUser $user, $object_phid) {
// Specifying the type in the query allows us to use a key.
return id(new PhabricatorFlagQuery())
->setViewer($user)
->withOwnerPHIDs(array($user->getPHID()))
->withTypes(array(phid_get_type($object_phid)))
->withObjectPHIDs(array($object_phid))
->executeOne();
}
protected function loadPage() {
$table = new PhabricatorFlag();
$conn_r = $table->establishConnection('r');
$data = queryfx_all(
$conn_r,
'SELECT * FROM %T flag %Q %Q %Q',
$table->getTableName(),
$this->buildWhereClause($conn_r),
$this->buildOrderClause($conn_r),
$this->buildLimitClause($conn_r));
return $table->loadAllFromArray($data);
}
protected function willFilterPage(array $flags) {
if ($this->needObjects) {
$objects = id(new PhabricatorObjectQuery())
->setViewer($this->getViewer())
->withPHIDs(mpull($flags, 'getObjectPHID'))
->execute();
$objects = mpull($objects, null, 'getPHID');
foreach ($flags as $key => $flag) {
$object = idx($objects, $flag->getObjectPHID());
if ($object) {
$flags[$key]->attachObject($object);
} else {
unset($flags[$key]);
}
}
}
if ($this->needHandles) {
$handles = id(new PhabricatorHandleQuery())
->setViewer($this->getViewer())
->withPHIDs(mpull($flags, 'getObjectPHID'))
->execute();
foreach ($flags as $flag) {
$flag->attachHandle($handles[$flag->getObjectPHID()]);
}
}
switch ($this->groupBy) {
case self::GROUP_COLOR:
$flags = msort($flags, 'getColor');
break;
case self::GROUP_NONE:
break;
default:
- throw new Exception("Unknown groupBy parameter: $this->groupBy");
+ throw new Exception(
+ pht('Unknown groupBy parameter: %s', $this->groupBy));
break;
}
return $flags;
}
protected function buildWhereClause(AphrontDatabaseConnection $conn_r) {
$where = array();
if ($this->ownerPHIDs) {
$where[] = qsprintf(
$conn_r,
'flag.ownerPHID IN (%Ls)',
$this->ownerPHIDs);
}
if ($this->types) {
$where[] = qsprintf(
$conn_r,
'flag.type IN (%Ls)',
$this->types);
}
if ($this->objectPHIDs) {
$where[] = qsprintf(
$conn_r,
'flag.objectPHID IN (%Ls)',
$this->objectPHIDs);
}
if ($this->colors) {
$where[] = qsprintf(
$conn_r,
'flag.color IN (%Ld)',
$this->colors);
}
$where[] = $this->buildPagingClause($conn_r);
return $this->formatWhereClause($where);
}
public function getQueryApplicationClass() {
return 'PhabricatorFlagsApplication';
}
}
diff --git a/src/applications/fund/editor/FundInitiativeEditor.php b/src/applications/fund/editor/FundInitiativeEditor.php
index 679e1b577..fc07c472b 100644
--- a/src/applications/fund/editor/FundInitiativeEditor.php
+++ b/src/applications/fund/editor/FundInitiativeEditor.php
@@ -1,290 +1,290 @@
<?php
final class FundInitiativeEditor
extends PhabricatorApplicationTransactionEditor {
public function getEditorApplicationClass() {
return 'PhabricatorFundApplication';
}
public function getEditorObjectsDescription() {
return pht('Fund Initiatives');
}
public function getTransactionTypes() {
$types = parent::getTransactionTypes();
$types[] = FundInitiativeTransaction::TYPE_NAME;
$types[] = FundInitiativeTransaction::TYPE_DESCRIPTION;
$types[] = FundInitiativeTransaction::TYPE_RISKS;
$types[] = FundInitiativeTransaction::TYPE_STATUS;
$types[] = FundInitiativeTransaction::TYPE_BACKER;
$types[] = FundInitiativeTransaction::TYPE_REFUND;
$types[] = FundInitiativeTransaction::TYPE_MERCHANT;
$types[] = PhabricatorTransactions::TYPE_VIEW_POLICY;
$types[] = PhabricatorTransactions::TYPE_EDIT_POLICY;
return $types;
}
protected function getCustomTransactionOldValue(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case FundInitiativeTransaction::TYPE_NAME:
return $object->getName();
case FundInitiativeTransaction::TYPE_DESCRIPTION:
return $object->getDescription();
case FundInitiativeTransaction::TYPE_RISKS:
return $object->getRisks();
case FundInitiativeTransaction::TYPE_STATUS:
return $object->getStatus();
case FundInitiativeTransaction::TYPE_BACKER:
case FundInitiativeTransaction::TYPE_REFUND:
return null;
case FundInitiativeTransaction::TYPE_MERCHANT:
return $object->getMerchantPHID();
}
return parent::getCustomTransactionOldValue($object, $xaction);
}
protected function getCustomTransactionNewValue(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case FundInitiativeTransaction::TYPE_NAME:
case FundInitiativeTransaction::TYPE_DESCRIPTION:
case FundInitiativeTransaction::TYPE_RISKS:
case FundInitiativeTransaction::TYPE_STATUS:
case FundInitiativeTransaction::TYPE_BACKER:
case FundInitiativeTransaction::TYPE_REFUND:
case FundInitiativeTransaction::TYPE_MERCHANT:
return $xaction->getNewValue();
}
return parent::getCustomTransactionNewValue($object, $xaction);
}
protected function applyCustomInternalTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
$type = $xaction->getTransactionType();
switch ($type) {
case FundInitiativeTransaction::TYPE_NAME:
$object->setName($xaction->getNewValue());
return;
case FundInitiativeTransaction::TYPE_DESCRIPTION:
$object->setDescription($xaction->getNewValue());
return;
case FundInitiativeTransaction::TYPE_RISKS:
$object->setRisks($xaction->getNewValue());
return;
case FundInitiativeTransaction::TYPE_MERCHANT:
$object->setMerchantPHID($xaction->getNewValue());
return;
case FundInitiativeTransaction::TYPE_STATUS:
$object->setStatus($xaction->getNewValue());
return;
case FundInitiativeTransaction::TYPE_BACKER:
case FundInitiativeTransaction::TYPE_REFUND:
$amount = $xaction->getMetadataValue(
FundInitiativeTransaction::PROPERTY_AMOUNT);
$amount = PhortuneCurrency::newFromString($amount);
if ($type == FundInitiativeTransaction::TYPE_REFUND) {
$total = $object->getTotalAsCurrency()->subtract($amount);
} else {
$total = $object->getTotalAsCurrency()->add($amount);
}
$object->setTotalAsCurrency($total);
return;
}
return parent::applyCustomInternalTransaction($object, $xaction);
}
protected function applyCustomExternalTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
$type = $xaction->getTransactionType();
switch ($type) {
case FundInitiativeTransaction::TYPE_NAME:
case FundInitiativeTransaction::TYPE_DESCRIPTION:
case FundInitiativeTransaction::TYPE_RISKS:
case FundInitiativeTransaction::TYPE_STATUS:
case FundInitiativeTransaction::TYPE_MERCHANT:
return;
case FundInitiativeTransaction::TYPE_BACKER:
case FundInitiativeTransaction::TYPE_REFUND:
$backer = id(new FundBackerQuery())
->setViewer($this->requireActor())
->withPHIDs(array($xaction->getNewValue()))
->executeOne();
if (!$backer) {
- throw new Exception(pht('Unable to load FundBacker!'));
+ throw new Exception(pht('Unable to load %s!', 'FundBacker'));
}
$subx = array();
if ($type == FundInitiativeTransaction::TYPE_BACKER) {
$subx[] = id(new FundBackerTransaction())
->setTransactionType(FundBackerTransaction::TYPE_STATUS)
->setNewValue(FundBacker::STATUS_PURCHASED);
} else {
$amount = $xaction->getMetadataValue(
FundInitiativeTransaction::PROPERTY_AMOUNT);
$subx[] = id(new FundBackerTransaction())
->setTransactionType(FundBackerTransaction::TYPE_STATUS)
->setNewValue($amount);
}
$editor = id(new FundBackerEditor())
->setActor($this->requireActor())
->setContentSource($this->getContentSource())
->setContinueOnMissingFields(true)
->setContinueOnNoEffect(true);
$editor->applyTransactions($backer, $subx);
return;
}
return parent::applyCustomExternalTransaction($object, $xaction);
}
protected function validateTransaction(
PhabricatorLiskDAO $object,
$type,
array $xactions) {
$errors = parent::validateTransaction($object, $type, $xactions);
switch ($type) {
case FundInitiativeTransaction::TYPE_NAME:
$missing = $this->validateIsEmptyTextField(
$object->getName(),
$xactions);
if ($missing) {
$error = new PhabricatorApplicationTransactionValidationError(
$type,
pht('Required'),
pht('Initiative name is required.'),
nonempty(last($xactions), null));
$error->setIsMissingFieldError(true);
$errors[] = $error;
}
break;
case FundInitiativeTransaction::TYPE_MERCHANT:
$missing = $this->validateIsEmptyTextField(
$object->getName(),
$xactions);
if ($missing) {
$error = new PhabricatorApplicationTransactionValidationError(
$type,
pht('Required'),
pht('Payable merchant is required.'),
nonempty(last($xactions), null));
$error->setIsMissingFieldError(true);
$errors[] = $error;
} else if ($xactions) {
$merchant_phid = last($xactions)->getNewValue();
// Make sure the actor has permission to edit the merchant they're
// selecting. You aren't allowed to send payments to an account you
// do not control.
$merchants = id(new PhortuneMerchantQuery())
->setViewer($this->requireActor())
->withPHIDs(array($merchant_phid))
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->execute();
if (!$merchants) {
$error = new PhabricatorApplicationTransactionValidationError(
$type,
pht('Invalid'),
pht(
'You must specify a merchant account you control as the '.
'recipient of funds from this initiative.'),
last($xactions));
$errors[] = $error;
}
}
break;
}
return $errors;
}
protected function shouldSendMail(
PhabricatorLiskDAO $object,
array $xactions) {
return true;
}
public function getMailTagsMap() {
return array(
FundInitiativeTransaction::MAILTAG_BACKER =>
pht('Someone backs an initiative.'),
FundInitiativeTransaction::MAILTAG_STATUS =>
pht("An initiative's status changes."),
FundInitiativeTransaction::MAILTAG_OTHER =>
pht('Other initiative activity not listed above occurs.'),
);
}
protected function buildMailTemplate(PhabricatorLiskDAO $object) {
$monogram = $object->getMonogram();
$name = $object->getName();
return id(new PhabricatorMetaMTAMail())
->setSubject("{$monogram}: {$name}")
->addHeader('Thread-Topic', $monogram);
}
protected function buildMailBody(
PhabricatorLiskDAO $object,
array $xactions) {
$body = parent::buildMailBody($object, $xactions);
$body->addLinkSection(
pht('INITIATIVE DETAIL'),
PhabricatorEnv::getProductionURI('/'.$object->getMonogram()));
return $body;
}
protected function getMailTo(PhabricatorLiskDAO $object) {
return array($object->getOwnerPHID());
}
protected function getMailSubjectPrefix() {
return 'Fund';
}
protected function buildReplyHandler(PhabricatorLiskDAO $object) {
return id(new FundInitiativeReplyHandler())
->setMailReceiver($object);
}
protected function shouldPublishFeedStory(
PhabricatorLiskDAO $object,
array $xactions) {
return true;
}
protected function supportsSearch() {
return true;
}
}
diff --git a/src/applications/fund/mail/FundInitiativeReplyHandler.php b/src/applications/fund/mail/FundInitiativeReplyHandler.php
index 4ff1816bb..0c236c239 100644
--- a/src/applications/fund/mail/FundInitiativeReplyHandler.php
+++ b/src/applications/fund/mail/FundInitiativeReplyHandler.php
@@ -1,16 +1,16 @@
<?php
final class FundInitiativeReplyHandler
extends PhabricatorApplicationTransactionReplyHandler {
public function validateMailReceiver($mail_receiver) {
if (!($mail_receiver instanceof FundInitiative)) {
- throw new Exception('Mail receiver is not a FundInitiative!');
+ throw new Exception(pht('Mail receiver is not a %s!', 'FundInitiative'));
}
}
public function getObjectPrefix() {
return 'I';
}
}
diff --git a/src/applications/fund/phortune/FundBackerProduct.php b/src/applications/fund/phortune/FundBackerProduct.php
index cc1320063..79ffc559b 100644
--- a/src/applications/fund/phortune/FundBackerProduct.php
+++ b/src/applications/fund/phortune/FundBackerProduct.php
@@ -1,148 +1,148 @@
<?php
final class FundBackerProduct extends PhortuneProductImplementation {
private $initiativePHID;
private $initiative;
private $viewer;
public function setViewer(PhabricatorUser $viewer) {
$this->viewer = $viewer;
return $this;
}
public function getViewer() {
return $this->viewer;
}
public function getRef() {
return $this->getInitiativePHID();
}
public function getName(PhortuneProduct $product) {
$initiative = $this->getInitiative();
return pht(
'Fund %s %s',
$initiative->getMonogram(),
$initiative->getName());
}
public function getPriceAsCurrency(PhortuneProduct $product) {
return PhortuneCurrency::newEmptyCurrency();
}
public function setInitiativePHID($initiative_phid) {
$this->initiativePHID = $initiative_phid;
return $this;
}
public function getInitiativePHID() {
return $this->initiativePHID;
}
public function setInitiative(FundInitiative $initiative) {
$this->initiative = $initiative;
return $this;
}
public function getInitiative() {
return $this->initiative;
}
public function loadImplementationsForRefs(
PhabricatorUser $viewer,
array $refs) {
$initiatives = id(new FundInitiativeQuery())
->setViewer($viewer)
->withPHIDs($refs)
->execute();
$initiatives = mpull($initiatives, null, 'getPHID');
$objects = array();
foreach ($refs as $ref) {
$object = id(new FundBackerProduct())
->setViewer($viewer)
->setInitiativePHID($ref);
$initiative = idx($initiatives, $ref);
if ($initiative) {
$object->setInitiative($initiative);
}
$objects[] = $object;
}
return $objects;
}
public function didPurchaseProduct(
PhortuneProduct $product,
PhortunePurchase $purchase) {
$viewer = $this->getViewer();
$backer = id(new FundBackerQuery())
->setViewer($viewer)
->withPHIDs(array($purchase->getMetadataValue('backerPHID')))
->executeOne();
if (!$backer) {
- throw new Exception(pht('Unable to load FundBacker!'));
+ throw new Exception(pht('Unable to load %s!', 'FundBacker'));
}
// Load the actual backing user -- they may not be the curent viewer if this
// product purchase is completing from a background worker or a merchant
// action.
$actor = id(new PhabricatorPeopleQuery())
->setViewer($viewer)
->withPHIDs(array($backer->getBackerPHID()))
->executeOne();
$xactions = array();
$xactions[] = id(new FundInitiativeTransaction())
->setTransactionType(FundInitiativeTransaction::TYPE_BACKER)
->setMetadataValue(
FundInitiativeTransaction::PROPERTY_AMOUNT,
$backer->getAmountAsCurrency()->serializeForStorage())
->setNewValue($backer->getPHID());
$editor = id(new FundInitiativeEditor())
->setActor($actor)
->setContentSource($this->getContentSource());
$editor->applyTransactions($this->getInitiative(), $xactions);
}
public function didRefundProduct(
PhortuneProduct $product,
PhortunePurchase $purchase,
PhortuneCurrency $amount) {
$viewer = $this->getViewer();
$backer = id(new FundBackerQuery())
->setViewer($viewer)
->withPHIDs(array($purchase->getMetadataValue('backerPHID')))
->executeOne();
if (!$backer) {
- throw new Exception(pht('Unable to load FundBacker!'));
+ throw new Exception(pht('Unable to load %s!', 'FundBacker'));
}
$xactions = array();
$xactions[] = id(new FundInitiativeTransaction())
->setTransactionType(FundInitiativeTransaction::TYPE_REFUND)
->setMetadataValue(
FundInitiativeTransaction::PROPERTY_AMOUNT,
$amount->serializeForStorage())
->setMetadataValue(
FundInitiativeTransaction::PROPERTY_BACKER,
$backer->getBackerPHID())
->setNewValue($backer->getPHID());
$editor = id(new FundInitiativeEditor())
->setActor($viewer)
->setContentSource($this->getContentSource());
$editor->applyTransactions($this->getInitiative(), $xactions);
}
}
diff --git a/src/applications/fund/search/FundInitiativeIndexer.php b/src/applications/fund/search/FundInitiativeIndexer.php
index a5be18398..7816af04b 100644
--- a/src/applications/fund/search/FundInitiativeIndexer.php
+++ b/src/applications/fund/search/FundInitiativeIndexer.php
@@ -1,58 +1,61 @@
<?php
final class FundInitiativeIndexer
extends PhabricatorSearchDocumentIndexer {
public function getIndexableObject() {
return new FundInitiative();
}
protected function loadDocumentByPHID($phid) {
$object = id(new FundInitiativeQuery())
->setViewer($this->getViewer())
->withPHIDs(array($phid))
->executeOne();
if (!$object) {
- throw new Exception("Unable to load object by phid '{$phid}'!");
+ throw new Exception(
+ pht(
+ "Unable to load object by PHID '%s'!",
+ $phid));
}
return $object;
}
protected function buildAbstractDocumentByPHID($phid) {
$initiative = $this->loadDocumentByPHID($phid);
$doc = id(new PhabricatorSearchAbstractDocument())
->setPHID($initiative->getPHID())
->setDocumentType(FundInitiativePHIDType::TYPECONST)
->setDocumentTitle($initiative->getName())
->setDocumentCreated($initiative->getDateCreated())
->setDocumentModified($initiative->getDateModified());
$doc->addRelationship(
PhabricatorSearchRelationship::RELATIONSHIP_AUTHOR,
$initiative->getOwnerPHID(),
PhabricatorPeopleUserPHIDType::TYPECONST,
$initiative->getDateCreated());
$doc->addRelationship(
PhabricatorSearchRelationship::RELATIONSHIP_OWNER,
$initiative->getOwnerPHID(),
PhabricatorPeopleUserPHIDType::TYPECONST,
$initiative->getDateCreated());
$doc->addRelationship(
$initiative->isClosed()
? PhabricatorSearchRelationship::RELATIONSHIP_CLOSED
: PhabricatorSearchRelationship::RELATIONSHIP_OPEN,
$initiative->getPHID(),
FundInitiativePHIDType::TYPECONST,
time());
$this->indexTransactions(
$doc,
new FundInitiativeTransactionQuery(),
array($initiative->getPHID()));
return $doc;
}
}
diff --git a/src/applications/fund/storage/FundInitiative.php b/src/applications/fund/storage/FundInitiative.php
index ac73401cf..6acc19f5f 100644
--- a/src/applications/fund/storage/FundInitiative.php
+++ b/src/applications/fund/storage/FundInitiative.php
@@ -1,211 +1,210 @@
<?php
final class FundInitiative extends FundDAO
implements
PhabricatorPolicyInterface,
PhabricatorProjectInterface,
PhabricatorApplicationTransactionInterface,
PhabricatorSubscribableInterface,
PhabricatorMentionableInterface,
PhabricatorFlaggableInterface,
PhabricatorTokenReceiverInterface,
PhabricatorDestructibleInterface {
protected $name;
protected $ownerPHID;
protected $merchantPHID;
protected $description;
protected $risks;
protected $viewPolicy;
protected $editPolicy;
protected $status;
protected $totalAsCurrency;
protected $mailKey;
private $projectPHIDs = self::ATTACHABLE;
const STATUS_OPEN = 'open';
const STATUS_CLOSED = 'closed';
public static function getStatusNameMap() {
return array(
self::STATUS_OPEN => pht('Open'),
self::STATUS_CLOSED => pht('Closed'),
);
}
public static function initializeNewInitiative(PhabricatorUser $actor) {
$app = id(new PhabricatorApplicationQuery())
->setViewer($actor)
->withClasses(array('PhabricatorFundApplication'))
->executeOne();
$view_policy = $app->getPolicy(FundDefaultViewCapability::CAPABILITY);
return id(new FundInitiative())
->setOwnerPHID($actor->getPHID())
->setViewPolicy($view_policy)
->setEditPolicy($actor->getPHID())
->setStatus(self::STATUS_OPEN)
->setTotalAsCurrency(PhortuneCurrency::newEmptyCurrency());
}
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_COLUMN_SCHEMA => array(
'name' => 'text255',
'description' => 'text',
'risks' => 'text',
'status' => 'text32',
'merchantPHID' => 'phid?',
'totalAsCurrency' => 'text64',
'mailKey' => 'bytes20',
),
self::CONFIG_APPLICATION_SERIALIZERS => array(
'totalAsCurrency' => new PhortuneCurrencySerializer(),
),
self::CONFIG_KEY_SCHEMA => array(
'key_status' => array(
'columns' => array('status'),
),
'key_owner' => array(
'columns' => array('ownerPHID'),
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(FundInitiativePHIDType::TYPECONST);
}
public function getMonogram() {
return 'I'.$this->getID();
}
public function getProjectPHIDs() {
return $this->assertAttached($this->projectPHIDs);
}
public function attachProjectPHIDs(array $phids) {
$this->projectPHIDs = $phids;
return $this;
}
public function isClosed() {
return ($this->getStatus() == self::STATUS_CLOSED);
}
public function save() {
if (!$this->mailKey) {
$this->mailKey = Filesystem::readRandomCharacters(20);
}
return parent::save();
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
return $this->getViewPolicy();
case PhabricatorPolicyCapability::CAN_EDIT:
return $this->getEditPolicy();
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
if ($viewer->getPHID() == $this->getOwnerPHID()) {
return true;
}
if ($capability == PhabricatorPolicyCapability::CAN_VIEW) {
foreach ($viewer->getAuthorities() as $authority) {
if ($authority instanceof PhortuneMerchant) {
if ($authority->getPHID() == $this->getMerchantPHID()) {
return true;
}
}
}
}
return false;
}
public function describeAutomaticCapability($capability) {
- return pht(
- 'The owner of an initiative can always view and edit it.');
+ return pht('The owner of an initiative can always view and edit it.');
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new FundInitiativeEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new FundInitiativeTransaction();
}
public function willRenderTimeline(
PhabricatorApplicationTransactionView $timeline,
AphrontRequest $request) {
return $timeline;
}
/* -( PhabricatorSubscribableInterface )----------------------------------- */
public function isAutomaticallySubscribed($phid) {
return ($phid == $this->getOwnerPHID());
}
public function shouldShowSubscribersProperty() {
return true;
}
public function shouldAllowSubscription($phid) {
return true;
}
/* -( PhabricatorTokenRecevierInterface )---------------------------------- */
public function getUsersToNotifyOfTokenGiven() {
return array(
$this->getOwnerPHID(),
);
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$this->openTransaction();
$this->delete();
$this->saveTransaction();
}
}
diff --git a/src/applications/harbormaster/controller/HarbormasterBuildActionController.php b/src/applications/harbormaster/controller/HarbormasterBuildActionController.php
index 5b0c84e10..133ba27fa 100644
--- a/src/applications/harbormaster/controller/HarbormasterBuildActionController.php
+++ b/src/applications/harbormaster/controller/HarbormasterBuildActionController.php
@@ -1,155 +1,154 @@
<?php
final class HarbormasterBuildActionController
extends HarbormasterController {
private $id;
private $action;
private $via;
public function willProcessRequest(array $data) {
$this->id = $data['id'];
$this->action = $data['action'];
$this->via = idx($data, 'via');
}
public function processRequest() {
$request = $this->getRequest();
$viewer = $request->getUser();
$command = $this->action;
$build = id(new HarbormasterBuildQuery())
->setViewer($viewer)
->withIDs(array($this->id))
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
if (!$build) {
return new Aphront404Response();
}
switch ($command) {
case HarbormasterBuildCommand::COMMAND_RESTART:
$can_issue = $build->canRestartBuild();
break;
case HarbormasterBuildCommand::COMMAND_STOP:
$can_issue = $build->canStopBuild();
break;
case HarbormasterBuildCommand::COMMAND_RESUME:
$can_issue = $build->canResumeBuild();
break;
default:
return new Aphront400Response();
}
switch ($this->via) {
case 'buildable':
$return_uri = '/'.$build->getBuildable()->getMonogram();
break;
default:
$return_uri = $this->getApplicationURI('/build/'.$build->getID().'/');
break;
}
if ($request->isDialogFormPost() && $can_issue) {
$editor = id(new HarbormasterBuildTransactionEditor())
->setActor($viewer)
->setContentSourceFromRequest($request)
->setContinueOnNoEffect(true)
->setContinueOnMissingFields(true);
$xaction = id(new HarbormasterBuildTransaction())
->setTransactionType(HarbormasterBuildTransaction::TYPE_COMMAND)
->setNewValue($command);
$editor->applyTransactions($build, array($xaction));
return id(new AphrontRedirectResponse())->setURI($return_uri);
}
switch ($command) {
case HarbormasterBuildCommand::COMMAND_RESTART:
if ($can_issue) {
$title = pht('Really restart build?');
$body = pht(
'Progress on this build will be discarded and the build will '.
'restart. Side effects of the build will occur again. Really '.
'restart build?');
$submit = pht('Restart Build');
} else {
$title = pht('Unable to Restart Build');
if ($build->isRestarting()) {
$body = pht(
'This build is already restarting. You can not reissue a '.
'restart command to a restarting build.');
} else {
- $body = pht(
- 'You can not restart this build.');
+ $body = pht('You can not restart this build.');
}
}
break;
case HarbormasterBuildCommand::COMMAND_STOP:
if ($can_issue) {
$title = pht('Really pause build?');
$body = pht(
'If you pause this build, work will halt once the current steps '.
'complete. You can resume the build later.');
$submit = pht('Pause Build');
} else {
$title = pht('Unable to Pause Build');
if ($build->isComplete()) {
$body = pht(
'This build is already complete. You can not pause a completed '.
'build.');
} else if ($build->isStopped()) {
$body = pht(
'This build is already paused. You can not pause a build which '.
'has already been paused.');
} else if ($build->isStopping()) {
$body = pht(
'This build is already pausing. You can not reissue a pause '.
'command to a pausing build.');
} else {
$body = pht(
'This build can not be paused.');
}
}
break;
case HarbormasterBuildCommand::COMMAND_RESUME:
if ($can_issue) {
$title = pht('Really resume build?');
$body = pht(
'Work will continue on the build. Really resume?');
$submit = pht('Resume Build');
} else {
$title = pht('Unable to Resume Build');
if ($build->isResuming()) {
$body = pht(
'This build is already resuming. You can not reissue a resume '.
'command to a resuming build.');
} else if (!$build->isStopped()) {
$body = pht(
'This build is not stopped. You can only resume a stopped '.
'build.');
}
}
break;
}
$dialog = id(new AphrontDialogView())
->setUser($viewer)
->setTitle($title)
->appendChild($body)
->addCancelButton($return_uri);
if ($can_issue) {
$dialog->addSubmitButton($submit);
}
return id(new AphrontDialogResponse())->setDialog($dialog);
}
}
diff --git a/src/applications/harbormaster/controller/HarbormasterBuildViewController.php b/src/applications/harbormaster/controller/HarbormasterBuildViewController.php
index 9fd6b0d2e..4af1f3427 100644
--- a/src/applications/harbormaster/controller/HarbormasterBuildViewController.php
+++ b/src/applications/harbormaster/controller/HarbormasterBuildViewController.php
@@ -1,533 +1,531 @@
<?php
final class HarbormasterBuildViewController
extends HarbormasterController {
private $id;
public function willProcessRequest(array $data) {
$this->id = $data['id'];
}
public function processRequest() {
$request = $this->getRequest();
$viewer = $request->getUser();
$id = $this->id;
$generation = $request->getInt('g');
$build = id(new HarbormasterBuildQuery())
->setViewer($viewer)
->withIDs(array($id))
->executeOne();
if (!$build) {
return new Aphront404Response();
}
require_celerity_resource('harbormaster-css');
$title = pht('Build %d', $id);
$header = id(new PHUIHeaderView())
->setHeader($title)
->setUser($viewer)
->setPolicyObject($build);
if ($build->isRestarting()) {
$header->setStatus('fa-exclamation-triangle', 'red', pht('Restarting'));
} else if ($build->isStopping()) {
$header->setStatus('fa-exclamation-triangle', 'red', pht('Pausing'));
} else if ($build->isResuming()) {
$header->setStatus('fa-exclamation-triangle', 'red', pht('Resuming'));
}
$box = id(new PHUIObjectBoxView())
->setHeader($header);
$actions = $this->buildActionList($build);
$this->buildPropertyLists($box, $build, $actions);
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb(
$build->getBuildable()->getMonogram(),
'/'.$build->getBuildable()->getMonogram());
$crumbs->addTextCrumb($title);
if ($generation === null || $generation > $build->getBuildGeneration() ||
$generation < 0) {
$generation = $build->getBuildGeneration();
}
$build_targets = id(new HarbormasterBuildTargetQuery())
->setViewer($viewer)
->needBuildSteps(true)
->withBuildPHIDs(array($build->getPHID()))
->withBuildGenerations(array($generation))
->execute();
if ($build_targets) {
$messages = id(new HarbormasterBuildMessageQuery())
->setViewer($viewer)
->withBuildTargetPHIDs(mpull($build_targets, 'getPHID'))
->execute();
$messages = mgroup($messages, 'getBuildTargetPHID');
} else {
$messages = array();
}
$targets = array();
foreach ($build_targets as $build_target) {
$header = id(new PHUIHeaderView())
->setHeader($build_target->getName())
->setUser($viewer);
$target_box = id(new PHUIObjectBoxView())
->setHeader($header);
$properties = new PHUIPropertyListView();
$status_view = new PHUIStatusListView();
$item = new PHUIStatusItemView();
$status = $build_target->getTargetStatus();
$status_name =
HarbormasterBuildTarget::getBuildTargetStatusName($status);
$icon = HarbormasterBuildTarget::getBuildTargetStatusIcon($status);
$color = HarbormasterBuildTarget::getBuildTargetStatusColor($status);
$item->setTarget($status_name);
$item->setIcon($icon, $color);
$status_view->addItem($item);
$properties->addProperty(pht('Name'), $build_target->getName());
if ($build_target->getDateStarted() !== null) {
$properties->addProperty(
pht('Started'),
phabricator_datetime($build_target->getDateStarted(), $viewer));
if ($build_target->isComplete()) {
$properties->addProperty(
pht('Completed'),
phabricator_datetime($build_target->getDateCompleted(), $viewer));
$properties->addProperty(
pht('Duration'),
phutil_format_relative_time_detailed(
$build_target->getDateCompleted() -
$build_target->getDateStarted()));
} else {
$properties->addProperty(
pht('Elapsed'),
phutil_format_relative_time_detailed(
time() - $build_target->getDateStarted()));
}
}
$properties->addProperty(pht('Status'), $status_view);
$target_box->addPropertyList($properties, pht('Overview'));
$step = $build_target->getBuildStep();
if ($step) {
$description = $step->getDescription();
if ($description) {
$rendered = PhabricatorMarkupEngine::renderOneObject(
id(new PhabricatorMarkupOneOff())
->setContent($description)
->setPreserveLinebreaks(true),
'default',
$viewer);
$properties->addSectionHeader(pht('Description'));
$properties->addTextContent($rendered);
}
} else {
$target_box->setFormErrors(
array(
pht(
'This build step has since been deleted on the build plan. '.
'Some information may be omitted.'),
));
}
$details = $build_target->getDetails();
if ($details) {
$properties = new PHUIPropertyListView();
foreach ($details as $key => $value) {
$properties->addProperty($key, $value);
}
$target_box->addPropertyList($properties, pht('Configuration'));
}
$variables = $build_target->getVariables();
if ($variables) {
$properties = new PHUIPropertyListView();
foreach ($variables as $key => $value) {
$properties->addProperty($key, $value);
}
$target_box->addPropertyList($properties, pht('Variables'));
}
$artifacts = $this->buildArtifacts($build_target);
if ($artifacts) {
$properties = new PHUIPropertyListView();
$properties->addRawContent($artifacts);
$target_box->addPropertyList($properties, pht('Artifacts'));
}
$build_messages = idx($messages, $build_target->getPHID(), array());
if ($build_messages) {
$properties = new PHUIPropertyListView();
$properties->addRawContent($this->buildMessages($build_messages));
$target_box->addPropertyList($properties, pht('Messages'));
}
$properties = new PHUIPropertyListView();
- $properties->addProperty('Build Target ID', $build_target->getID());
+ $properties->addProperty(pht('Build Target ID'), $build_target->getID());
$target_box->addPropertyList($properties, pht('Metadata'));
$targets[] = $target_box;
$targets[] = $this->buildLog($build, $build_target);
}
$timeline = $this->buildTransactionTimeline(
$build,
new HarbormasterBuildTransactionQuery());
$timeline->setShouldTerminate(true);
return $this->buildApplicationPage(
array(
$crumbs,
$box,
$targets,
$timeline,
),
array(
'title' => $title,
));
}
private function buildArtifacts(
HarbormasterBuildTarget $build_target) {
$request = $this->getRequest();
$viewer = $request->getUser();
$artifacts = id(new HarbormasterBuildArtifactQuery())
->setViewer($viewer)
->withBuildTargetPHIDs(array($build_target->getPHID()))
->execute();
if (count($artifacts) === 0) {
return null;
}
$list = id(new PHUIObjectItemListView())
->setFlush(true);
foreach ($artifacts as $artifact) {
$item = $artifact->getObjectItemView($viewer);
if ($item !== null) {
$list->addItem($item);
}
}
return $list;
}
private function buildLog(
HarbormasterBuild $build,
HarbormasterBuildTarget $build_target) {
$request = $this->getRequest();
$viewer = $request->getUser();
$limit = $request->getInt('l', 25);
$logs = id(new HarbormasterBuildLogQuery())
->setViewer($viewer)
->withBuildTargetPHIDs(array($build_target->getPHID()))
->execute();
$empty_logs = array();
$log_boxes = array();
foreach ($logs as $log) {
$start = 1;
$lines = preg_split("/\r\n|\r|\n/", $log->getLogText());
if ($limit !== 0) {
$start = count($lines) - $limit;
if ($start >= 1) {
$lines = array_slice($lines, -$limit, $limit);
} else {
$start = 1;
}
}
$id = null;
$is_empty = false;
if (count($lines) === 1 && trim($lines[0]) === '') {
// Prevent Harbormaster from showing empty build logs.
$id = celerity_generate_unique_node_id();
$empty_logs[] = $id;
$is_empty = true;
}
$log_view = new ShellLogView();
$log_view->setLines($lines);
$log_view->setStart($start);
$header = id(new PHUIHeaderView())
->setHeader(pht(
'Build Log %d (%s - %s)',
$log->getID(),
$log->getLogSource(),
$log->getLogType()))
->setSubheader($this->createLogHeader($build, $log))
->setUser($viewer);
$log_box = id(new PHUIObjectBoxView())
->setHeader($header)
->setForm($log_view);
if ($is_empty) {
$log_box = phutil_tag(
'div',
array(
'style' => 'display: none',
'id' => $id,
),
$log_box);
}
$log_boxes[] = $log_box;
}
if ($empty_logs) {
$hide_id = celerity_generate_unique_node_id();
Javelin::initBehavior('phabricator-reveal-content');
$expand = phutil_tag(
'div',
array(
'id' => $hide_id,
'class' => 'harbormaster-empty-logs-are-hidden mlr mlt mll',
),
array(
pht(
'%s empty logs are hidden.',
new PhutilNumber(count($empty_logs))),
' ',
javelin_tag(
'a',
array(
'href' => '#',
'sigil' => 'reveal-content',
'meta' => array(
'showIDs' => $empty_logs,
'hideIDs' => array($hide_id),
),
),
pht('Show all logs.')),
));
array_unshift($log_boxes, $expand);
}
return $log_boxes;
}
private function createLogHeader($build, $log) {
$request = $this->getRequest();
$limit = $request->getInt('l', 25);
$lines_25 = $this->getApplicationURI('/build/'.$build->getID().'/?l=25');
$lines_50 = $this->getApplicationURI('/build/'.$build->getID().'/?l=50');
$lines_100 =
$this->getApplicationURI('/build/'.$build->getID().'/?l=100');
$lines_0 = $this->getApplicationURI('/build/'.$build->getID().'/?l=0');
$link_25 = phutil_tag('a', array('href' => $lines_25), pht('25'));
$link_50 = phutil_tag('a', array('href' => $lines_50), pht('50'));
$link_100 = phutil_tag('a', array('href' => $lines_100), pht('100'));
$link_0 = phutil_tag('a', array('href' => $lines_0), pht('Unlimited'));
if ($limit === 25) {
$link_25 = phutil_tag('strong', array(), $link_25);
} else if ($limit === 50) {
$link_50 = phutil_tag('strong', array(), $link_50);
} else if ($limit === 100) {
$link_100 = phutil_tag('strong', array(), $link_100);
} else if ($limit === 0) {
$link_0 = phutil_tag('strong', array(), $link_0);
}
return phutil_tag(
'span',
array(),
array(
$link_25,
' - ',
$link_50,
' - ',
$link_100,
' - ',
$link_0,
' Lines',
));
}
private function buildActionList(HarbormasterBuild $build) {
$request = $this->getRequest();
$viewer = $request->getUser();
$id = $build->getID();
$list = id(new PhabricatorActionListView())
->setUser($viewer)
->setObject($build)
->setObjectURI("/build/{$id}");
$can_restart = $build->canRestartBuild();
$can_stop = $build->canStopBuild();
$can_resume = $build->canResumeBuild();
$list->addAction(
id(new PhabricatorActionView())
->setName(pht('Restart Build'))
->setIcon('fa-repeat')
->setHref($this->getApplicationURI('/build/restart/'.$id.'/'))
->setDisabled(!$can_restart)
->setWorkflow(true));
if ($build->canResumeBuild()) {
$list->addAction(
id(new PhabricatorActionView())
->setName(pht('Resume Build'))
->setIcon('fa-play')
->setHref($this->getApplicationURI('/build/resume/'.$id.'/'))
->setDisabled(!$can_resume)
->setWorkflow(true));
} else {
$list->addAction(
id(new PhabricatorActionView())
->setName(pht('Pause Build'))
->setIcon('fa-pause')
->setHref($this->getApplicationURI('/build/stop/'.$id.'/'))
->setDisabled(!$can_stop)
->setWorkflow(true));
}
return $list;
}
private function buildPropertyLists(
PHUIObjectBoxView $box,
HarbormasterBuild $build,
PhabricatorActionListView $actions) {
$request = $this->getRequest();
$viewer = $request->getUser();
$properties = id(new PHUIPropertyListView())
->setUser($viewer)
->setObject($build)
->setActionList($actions);
$box->addPropertyList($properties);
$handles = id(new PhabricatorHandleQuery())
->setViewer($viewer)
->withPHIDs(array(
$build->getBuildablePHID(),
$build->getBuildPlanPHID(),
))
->execute();
$properties->addProperty(
pht('Buildable'),
$handles[$build->getBuildablePHID()]->renderLink());
$properties->addProperty(
pht('Build Plan'),
$handles[$build->getBuildPlanPHID()]->renderLink());
$properties->addProperty(
pht('Restarts'),
$build->getBuildGeneration());
$properties->addProperty(
pht('Status'),
$this->getStatus($build));
}
private function getStatus(HarbormasterBuild $build) {
$status_view = new PHUIStatusListView();
$item = new PHUIStatusItemView();
if ($build->isStopping()) {
$status_name = pht('Pausing');
$icon = PHUIStatusItemView::ICON_RIGHT;
$color = 'dark';
} else {
$status = $build->getBuildStatus();
$status_name =
HarbormasterBuild::getBuildStatusName($status);
$icon = HarbormasterBuild::getBuildStatusIcon($status);
$color = HarbormasterBuild::getBuildStatusColor($status);
}
$item->setTarget($status_name);
$item->setIcon($icon, $color);
$status_view->addItem($item);
return $status_view;
}
private function buildMessages(array $messages) {
$viewer = $this->getRequest()->getUser();
if ($messages) {
$handles = id(new PhabricatorHandleQuery())
->setViewer($viewer)
->withPHIDs(mpull($messages, 'getAuthorPHID'))
->execute();
} else {
$handles = array();
}
$rows = array();
foreach ($messages as $message) {
$rows[] = array(
$message->getID(),
$handles[$message->getAuthorPHID()]->renderLink(),
$message->getType(),
$message->getIsConsumed() ? pht('Consumed') : null,
phabricator_datetime($message->getDateCreated(), $viewer),
);
}
$table = new AphrontTableView($rows);
$table->setNoDataString(pht('No messages for this build target.'));
$table->setHeaders(
array(
pht('ID'),
pht('From'),
pht('Type'),
pht('Consumed'),
pht('Received'),
));
$table->setColumnClasses(
array(
'',
'',
'wide',
'',
'date',
));
return $table;
}
-
-
}
diff --git a/src/applications/harbormaster/controller/HarbormasterPlanEditController.php b/src/applications/harbormaster/controller/HarbormasterPlanEditController.php
index c291f4e67..8679d9ce4 100644
--- a/src/applications/harbormaster/controller/HarbormasterPlanEditController.php
+++ b/src/applications/harbormaster/controller/HarbormasterPlanEditController.php
@@ -1,114 +1,114 @@
<?php
final class HarbormasterPlanEditController extends HarbormasterPlanController {
private $id;
public function willProcessRequest(array $data) {
$this->id = idx($data, 'id');
}
public function processRequest() {
$request = $this->getRequest();
$viewer = $request->getUser();
$this->requireApplicationCapability(
HarbormasterManagePlansCapability::CAPABILITY);
if ($this->id) {
$plan = id(new HarbormasterBuildPlanQuery())
->setViewer($viewer)
->withIDs(array($this->id))
->executeOne();
if (!$plan) {
return new Aphront404Response();
}
} else {
$plan = HarbormasterBuildPlan::initializeNewBuildPlan($viewer);
}
$e_name = true;
$v_name = $plan->getName();
$validation_exception = null;
if ($request->isFormPost()) {
$xactions = array();
$v_name = $request->getStr('name');
$e_name = null;
$type_name = HarbormasterBuildPlanTransaction::TYPE_NAME;
$xactions[] = id(new HarbormasterBuildPlanTransaction())
->setTransactionType($type_name)
->setNewValue($v_name);
$editor = id(new HarbormasterBuildPlanEditor())
->setActor($viewer)
->setContentSourceFromRequest($request);
try {
$editor->applyTransactions($plan, $xactions);
return id(new AphrontRedirectResponse())
->setURI($this->getApplicationURI('plan/'.$plan->getID().'/'));
} catch (PhabricatorApplicationTransactionValidationException $ex) {
$validation_exception = $ex;
$e_name = $validation_exception->getShortMessage(
HarbormasterBuildPlanTransaction::TYPE_NAME);
}
}
$is_new = (!$plan->getID());
if ($is_new) {
$title = pht('New Build Plan');
$cancel_uri = $this->getApplicationURI();
$save_button = pht('Create Build Plan');
} else {
$id = $plan->getID();
$title = pht('Edit Build Plan');
$cancel_uri = $this->getApplicationURI('plan/'.$plan->getID().'/');
$save_button = pht('Save Build Plan');
}
$form = id(new AphrontFormView())
->setUser($viewer)
->appendChild(
id(new AphrontFormTextControl())
- ->setLabel('Plan Name')
+ ->setLabel(pht('Plan Name'))
->setName('name')
->setError($e_name)
->setValue($v_name));
$form->appendChild(
id(new AphrontFormSubmitControl())
->setValue($save_button)
->addCancelButton($cancel_uri));
$box = id(new PHUIObjectBoxView())
->setHeaderText($title)
->setValidationException($validation_exception)
->setForm($form);
$crumbs = $this->buildApplicationCrumbs();
if ($is_new) {
$crumbs->addTextCrumb(pht('New Build Plan'));
} else {
$id = $plan->getID();
$crumbs->addTextCrumb(
pht('Plan %d', $id),
$this->getApplicationURI("plan/{$id}/"));
$crumbs->addTextCrumb(pht('Edit'));
}
return $this->buildApplicationPage(
array(
$crumbs,
$box,
),
array(
'title' => $title,
));
}
}
diff --git a/src/applications/harbormaster/controller/HarbormasterPlanViewController.php b/src/applications/harbormaster/controller/HarbormasterPlanViewController.php
index c6fd4a115..7447f2ed7 100644
--- a/src/applications/harbormaster/controller/HarbormasterPlanViewController.php
+++ b/src/applications/harbormaster/controller/HarbormasterPlanViewController.php
@@ -1,475 +1,475 @@
<?php
final class HarbormasterPlanViewController extends HarbormasterPlanController {
private $id;
public function willProcessRequest(array $data) {
$this->id = $data['id'];
}
public function processRequest() {
$request = $this->getRequest();
$viewer = $request->getUser();
$id = $this->id;
$plan = id(new HarbormasterBuildPlanQuery())
->setViewer($viewer)
->withIDs(array($id))
->executeOne();
if (!$plan) {
return new Aphront404Response();
}
$timeline = $this->buildTransactionTimeline(
$plan,
new HarbormasterBuildPlanTransactionQuery());
$timeline->setShouldTerminate(true);
$title = pht('Plan %d', $id);
$header = id(new PHUIHeaderView())
->setHeader($title)
->setUser($viewer)
->setPolicyObject($plan);
$box = id(new PHUIObjectBoxView())
->setHeader($header);
$actions = $this->buildActionList($plan);
$this->buildPropertyLists($box, $plan, $actions);
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb(pht('Plan %d', $id));
list($step_list, $has_any_conflicts, $would_deadlock) =
$this->buildStepList($plan);
if ($would_deadlock) {
$box->setFormErrors(
array(
pht(
'This build plan will deadlock when executed, due to '.
'circular dependencies present in the build plan. '.
'Examine the step list and resolve the deadlock.'),
));
} else if ($has_any_conflicts) {
// A deadlocking build will also cause all the artifacts to be
// invalid, so we just skip showing this message if that's the
// case.
$box->setFormErrors(
array(
pht(
'This build plan has conflicts in one or more build steps. '.
'Examine the step list and resolve the listed errors.'),
));
}
return $this->buildApplicationPage(
array(
$crumbs,
$box,
$step_list,
$timeline,
),
array(
'title' => $title,
));
}
private function buildStepList(HarbormasterBuildPlan $plan) {
$request = $this->getRequest();
$viewer = $request->getUser();
$run_order =
HarbormasterBuildGraph::determineDependencyExecution($plan);
$steps = id(new HarbormasterBuildStepQuery())
->setViewer($viewer)
->withBuildPlanPHIDs(array($plan->getPHID()))
->execute();
$steps = mpull($steps, null, 'getPHID');
$can_edit = $this->hasApplicationCapability(
HarbormasterManagePlansCapability::CAPABILITY);
$step_list = id(new PHUIObjectItemListView())
->setUser($viewer)
->setNoDataString(
pht('This build plan does not have any build steps yet.'));
$i = 1;
$last_depth = 0;
$has_any_conflicts = false;
$is_deadlocking = false;
foreach ($run_order as $run_ref) {
$step = $steps[$run_ref['node']->getPHID()];
$depth = $run_ref['depth'] + 1;
if ($last_depth !== $depth) {
$last_depth = $depth;
$i = 1;
} else {
$i++;
}
$implementation = null;
try {
$implementation = $step->getStepImplementation();
} catch (Exception $ex) {
// We can't initialize the implementation. This might be because
// it's been renamed or no longer exists.
$item = id(new PHUIObjectItemView())
->setObjectName(pht('Step %d.%d', $depth, $i))
->setHeader(pht('Unknown Implementation'))
->setBarColor('red')
->addAttribute(pht(
'This step has an invalid implementation (%s).',
$step->getClassName()))
->addAction(
id(new PHUIListItemView())
->setIcon('fa-times')
->addSigil('harbormaster-build-step-delete')
->setWorkflow(true)
->setRenderNameAsTooltip(true)
->setName(pht('Delete'))
->setHref(
$this->getApplicationURI('step/delete/'.$step->getID().'/')));
$step_list->addItem($item);
continue;
}
$item = id(new PHUIObjectItemView())
->setObjectName(pht('Step %d.%d', $depth, $i))
->setHeader($step->getName());
$item->addAttribute($implementation->getDescription());
$step_id = $step->getID();
$edit_uri = $this->getApplicationURI("step/edit/{$step_id}/");
$delete_uri = $this->getApplicationURI("step/delete/{$step_id}/");
if ($can_edit) {
$item->setHref($edit_uri);
}
$item
->setHref($edit_uri)
->addAction(
id(new PHUIListItemView())
->setIcon('fa-times')
->addSigil('harbormaster-build-step-delete')
->setWorkflow(true)
->setDisabled(!$can_edit)
->setHref(
$this->getApplicationURI('step/delete/'.$step->getID().'/')));
$depends = $step->getStepImplementation()->getDependencies($step);
$inputs = $step->getStepImplementation()->getArtifactInputs();
$outputs = $step->getStepImplementation()->getArtifactOutputs();
$has_conflicts = false;
if ($depends || $inputs || $outputs) {
$available_artifacts =
HarbormasterBuildStepImplementation::getAvailableArtifacts(
$plan,
$step,
null);
$available_artifacts = ipull($available_artifacts, 'type');
list($depends_ui, $has_conflicts) = $this->buildDependsOnList(
$depends,
pht('Depends On'),
$steps);
list($inputs_ui, $has_conflicts) = $this->buildArtifactList(
$inputs,
'in',
pht('Input Artifacts'),
$available_artifacts);
list($outputs_ui) = $this->buildArtifactList(
$outputs,
'out',
pht('Output Artifacts'),
array());
$item->appendChild(
phutil_tag(
'div',
array(
'class' => 'harbormaster-artifact-io',
),
array(
$depends_ui,
$inputs_ui,
$outputs_ui,
)));
}
if ($has_conflicts) {
$has_any_conflicts = true;
$item->setBarColor('red');
}
if ($run_ref['cycle']) {
$is_deadlocking = true;
}
if ($is_deadlocking) {
$item->setBarColor('red');
}
$step_list->addItem($item);
}
return array($step_list, $has_any_conflicts, $is_deadlocking);
}
private function buildActionList(HarbormasterBuildPlan $plan) {
$request = $this->getRequest();
$viewer = $request->getUser();
$id = $plan->getID();
$list = id(new PhabricatorActionListView())
->setUser($viewer)
->setObject($plan)
->setObjectURI($this->getApplicationURI("plan/{$id}/"));
$can_edit = $this->hasApplicationCapability(
HarbormasterManagePlansCapability::CAPABILITY);
$list->addAction(
id(new PhabricatorActionView())
->setName(pht('Edit Plan'))
->setHref($this->getApplicationURI("plan/edit/{$id}/"))
->setWorkflow(!$can_edit)
->setDisabled(!$can_edit)
->setIcon('fa-pencil'));
if ($plan->isDisabled()) {
$list->addAction(
id(new PhabricatorActionView())
->setName(pht('Enable Plan'))
->setHref($this->getApplicationURI("plan/disable/{$id}/"))
->setWorkflow(true)
->setDisabled(!$can_edit)
->setIcon('fa-check'));
} else {
$list->addAction(
id(new PhabricatorActionView())
->setName(pht('Disable Plan'))
->setHref($this->getApplicationURI("plan/disable/{$id}/"))
->setWorkflow(true)
->setDisabled(!$can_edit)
->setIcon('fa-ban'));
}
$list->addAction(
id(new PhabricatorActionView())
->setName(pht('Add Build Step'))
->setHref($this->getApplicationURI("step/add/{$id}/"))
->setWorkflow(true)
->setDisabled(!$can_edit)
->setIcon('fa-plus'));
$list->addAction(
id(new PhabricatorActionView())
->setName(pht('Run Plan Manually'))
->setHref($this->getApplicationURI("plan/run/{$id}/"))
->setWorkflow(true)
->setDisabled(!$can_edit)
->setIcon('fa-play-circle'));
return $list;
}
private function buildPropertyLists(
PHUIObjectBoxView $box,
HarbormasterBuildPlan $plan,
PhabricatorActionListView $actions) {
$request = $this->getRequest();
$viewer = $request->getUser();
$properties = id(new PHUIPropertyListView())
->setUser($viewer)
->setObject($plan)
->setActionList($actions);
$box->addPropertyList($properties);
$properties->addProperty(
pht('Created'),
phabricator_datetime($plan->getDateCreated(), $viewer));
}
private function buildArtifactList(
array $artifacts,
$kind,
$name,
array $available_artifacts) {
$has_conflicts = false;
if (!$artifacts) {
return array(null, $has_conflicts);
}
$this->requireResource('harbormaster-css');
$header = phutil_tag(
'div',
array(
'class' => 'harbormaster-artifact-summary-header',
),
$name);
$is_input = ($kind == 'in');
$list = new PHUIStatusListView();
foreach ($artifacts as $artifact) {
$error = null;
$key = idx($artifact, 'key');
if (!strlen($key)) {
$bound = phutil_tag('em', array(), pht('(null)'));
if ($is_input) {
// This is an unbound input. For now, all inputs are always required.
$icon = PHUIStatusItemView::ICON_WARNING;
$color = 'red';
$icon_label = pht('Required Input');
$has_conflicts = true;
$error = pht('This input is required, but not configured.');
} else {
// This is an unnamed output. Outputs do not necessarily need to be
// named.
$icon = PHUIStatusItemView::ICON_OPEN;
$color = 'bluegrey';
$icon_label = pht('Unused Output');
}
} else {
$bound = phutil_tag('strong', array(), $key);
if ($is_input) {
if (isset($available_artifacts[$key])) {
if ($available_artifacts[$key] == idx($artifact, 'type')) {
$icon = PHUIStatusItemView::ICON_ACCEPT;
$color = 'green';
$icon_label = pht('Valid Input');
} else {
$icon = PHUIStatusItemView::ICON_WARNING;
$color = 'red';
$icon_label = pht('Bad Input Type');
$has_conflicts = true;
$error = pht(
'This input is bound to the wrong artifact type. It is bound '.
'to a "%s" artifact, but should be bound to a "%s" artifact.',
$available_artifacts[$key],
idx($artifact, 'type'));
}
} else {
$icon = PHUIStatusItemView::ICON_QUESTION;
$color = 'red';
$icon_label = pht('Unknown Input');
$has_conflicts = true;
$error = pht(
'This input is bound to an artifact ("%s") which does not exist '.
'at this stage in the build process.',
$key);
}
} else {
$icon = PHUIStatusItemView::ICON_DOWN;
$color = 'green';
$icon_label = pht('Valid Output');
}
}
if ($error) {
$note = array(
phutil_tag('strong', array(), pht('ERROR:')),
' ',
$error,
);
} else {
$note = $bound;
}
$list->addItem(
id(new PHUIStatusItemView())
->setIcon($icon, $color, $icon_label)
->setTarget($artifact['name'])
->setNote($note));
}
$ui = array(
$header,
$list,
);
return array($ui, $has_conflicts);
}
private function buildDependsOnList(
array $step_phids,
$name,
array $steps) {
$has_conflicts = false;
if (count($step_phids) === 0) {
return null;
}
$this->requireResource('harbormaster-css');
$steps = mpull($steps, null, 'getPHID');
$header = phutil_tag(
'div',
array(
'class' => 'harbormaster-artifact-summary-header',
),
$name);
$list = new PHUIStatusListView();
foreach ($step_phids as $step_phid) {
$error = null;
if (idx($steps, $step_phid) === null) {
$icon = PHUIStatusItemView::ICON_WARNING;
$color = 'red';
$icon_label = pht('Missing Dependency');
$has_conflicts = true;
$error = pht(
- 'This dependency specifies a build step which doesn\'t exist.');
+ "This dependency specifies a build step which doesn't exist.");
} else {
$bound = phutil_tag(
'strong',
array(),
idx($steps, $step_phid)->getName());
$icon = PHUIStatusItemView::ICON_ACCEPT;
$color = 'green';
$icon_label = pht('Valid Input');
}
if ($error) {
$note = array(
phutil_tag('strong', array(), pht('ERROR:')),
' ',
$error,
);
} else {
$note = $bound;
}
$list->addItem(
id(new PHUIStatusItemView())
->setIcon($icon, $color, $icon_label)
->setTarget(pht('Build Step'))
->setNote($note));
}
$ui = array(
$header,
$list,
);
return array($ui, $has_conflicts);
}
}
diff --git a/src/applications/harbormaster/controller/HarbormasterStepDeleteController.php b/src/applications/harbormaster/controller/HarbormasterStepDeleteController.php
index 228304e73..c0df595fa 100644
--- a/src/applications/harbormaster/controller/HarbormasterStepDeleteController.php
+++ b/src/applications/harbormaster/controller/HarbormasterStepDeleteController.php
@@ -1,51 +1,51 @@
<?php
final class HarbormasterStepDeleteController extends HarbormasterController {
private $id;
public function willProcessRequest(array $data) {
$this->id = $data['id'];
}
public function processRequest() {
$request = $this->getRequest();
$viewer = $request->getUser();
$this->requireApplicationCapability(
HarbormasterManagePlansCapability::CAPABILITY);
$id = $this->id;
$step = id(new HarbormasterBuildStepQuery())
->setViewer($viewer)
->withIDs(array($id))
->executeOne();
if ($step === null) {
- throw new Exception('Build step not found!');
+ throw new Exception(pht('Build step not found!'));
}
$plan_id = $step->getBuildPlan()->getID();
$done_uri = $this->getApplicationURI('plan/'.$plan_id.'/');
if ($request->isDialogFormPost()) {
$step->delete();
return id(new AphrontRedirectResponse())->setURI($done_uri);
}
$dialog = new AphrontDialogView();
$dialog->setTitle(pht('Really Delete Step?'))
->setUser($viewer)
->addSubmitButton(pht('Delete Build Step'))
->addCancelButton($done_uri);
$dialog->appendChild(
phutil_tag(
'p',
array(),
pht(
- 'Are you sure you want to delete this '.
- 'step? This can\'t be undone!')));
+ "Are you sure you want to delete this step? ".
+ "This can't be undone!")));
return id(new AphrontDialogResponse())->setDialog($dialog);
}
}
diff --git a/src/applications/harbormaster/controller/HarbormasterStepEditController.php b/src/applications/harbormaster/controller/HarbormasterStepEditController.php
index ef9289a7c..c753bd218 100644
--- a/src/applications/harbormaster/controller/HarbormasterStepEditController.php
+++ b/src/applications/harbormaster/controller/HarbormasterStepEditController.php
@@ -1,234 +1,235 @@
<?php
final class HarbormasterStepEditController extends HarbormasterController {
private $id;
private $planID;
private $className;
public function willProcessRequest(array $data) {
$this->id = idx($data, 'id');
$this->planID = idx($data, 'plan');
$this->className = idx($data, 'class');
}
public function processRequest() {
$request = $this->getRequest();
$viewer = $request->getUser();
$this->requireApplicationCapability(
HarbormasterManagePlansCapability::CAPABILITY);
if ($this->id) {
$step = id(new HarbormasterBuildStepQuery())
->setViewer($viewer)
->withIDs(array($this->id))
->executeOne();
if (!$step) {
return new Aphront404Response();
}
$plan = $step->getBuildPlan();
$is_new = false;
} else {
$plan = id(new HarbormasterBuildPlanQuery())
->setViewer($viewer)
->withIDs(array($this->planID))
->executeOne();
if (!$plan) {
return new Aphront404Response();
}
$impl = HarbormasterBuildStepImplementation::getImplementation(
$this->className);
if (!$impl) {
return new Aphront404Response();
}
$step = HarbormasterBuildStep::initializeNewStep($viewer)
->setBuildPlanPHID($plan->getPHID())
->setClassName($this->className);
$is_new = true;
}
$plan_uri = $this->getApplicationURI('plan/'.$plan->getID().'/');
$implementation = $step->getStepImplementation();
$field_list = PhabricatorCustomField::getObjectFields(
$step,
PhabricatorCustomField::ROLE_EDIT);
$field_list
->setViewer($viewer)
->readFieldsFromStorage($step);
$e_name = true;
$v_name = $step->getName();
$e_description = true;
$v_description = $step->getDescription();
$e_depends_on = true;
$v_depends_on = $step->getDetail('dependsOn', array());
$errors = array();
$validation_exception = null;
if ($request->isFormPost()) {
$e_name = null;
$v_name = $request->getStr('name');
$e_description = null;
$v_description = $request->getStr('description');
$e_depends_on = null;
$v_depends_on = $request->getArr('dependsOn');
$xactions = $field_list->buildFieldTransactionsFromRequest(
new HarbormasterBuildStepTransaction(),
$request);
$editor = id(new HarbormasterBuildStepEditor())
->setActor($viewer)
->setContinueOnNoEffect(true)
->setContentSourceFromRequest($request);
$name_xaction = id(new HarbormasterBuildStepTransaction())
->setTransactionType(HarbormasterBuildStepTransaction::TYPE_NAME)
->setNewValue($v_name);
array_unshift($xactions, $name_xaction);
$depends_on_xaction = id(new HarbormasterBuildStepTransaction())
->setTransactionType(
HarbormasterBuildStepTransaction::TYPE_DEPENDS_ON)
->setNewValue($v_depends_on);
array_unshift($xactions, $depends_on_xaction);
$description_xaction = id(new HarbormasterBuildStepTransaction())
->setTransactionType(
HarbormasterBuildStepTransaction::TYPE_DESCRIPTION)
->setNewValue($v_description);
array_unshift($xactions, $description_xaction);
if ($is_new) {
// When creating a new step, make sure we have a create transaction
// so we'll apply the transactions even if the step has no
// configurable options.
$create_xaction = id(new HarbormasterBuildStepTransaction())
->setTransactionType(HarbormasterBuildStepTransaction::TYPE_CREATE);
array_unshift($xactions, $create_xaction);
}
try {
$editor->applyTransactions($step, $xactions);
return id(new AphrontRedirectResponse())->setURI($plan_uri);
} catch (PhabricatorApplicationTransactionValidationException $ex) {
$validation_exception = $ex;
}
}
$form = id(new AphrontFormView())
->setUser($viewer)
->appendChild(
id(new AphrontFormTextControl())
->setName('name')
->setLabel(pht('Name'))
->setError($e_name)
->setValue($v_name));
$form
->appendControl(
id(new AphrontFormTokenizerControl())
->setDatasource(id(new HarbormasterBuildDependencyDatasource())
->setParameters(array(
'planPHID' => $plan->getPHID(),
'stepPHID' => $is_new ? null : $step->getPHID(),
)))
->setName('dependsOn')
->setLabel(pht('Depends On'))
->setError($e_depends_on)
->setValue($v_depends_on));
$field_list->appendFieldsToForm($form);
$form
->appendChild(
id(new PhabricatorRemarkupControl())
->setUser($viewer)
->setName('description')
->setLabel(pht('Description'))
->setError($e_description)
->setValue($v_description));
if ($is_new) {
$submit = pht('Create Build Step');
$header = pht('New Step: %s', $implementation->getName());
$crumb = pht('Add Step');
} else {
$submit = pht('Save Build Step');
$header = pht('Edit Step: %s', $implementation->getName());
$crumb = pht('Edit Step');
}
$form->appendChild(
id(new AphrontFormSubmitControl())
->setValue($submit)
->addCancelButton($plan_uri));
$box = id(new PHUIObjectBoxView())
->setHeaderText($header)
->setValidationException($validation_exception)
->setForm($form);
$crumbs = $this->buildApplicationCrumbs();
$id = $plan->getID();
$crumbs->addTextCrumb(pht('Plan %d', $id), $plan_uri);
$crumbs->addTextCrumb($crumb);
$variables = $this->renderBuildVariablesTable();
if ($is_new) {
$xaction_view = null;
$timeline = null;
} else {
$timeline = $this->buildTransactionTimeline(
$step,
new HarbormasterBuildStepTransactionQuery());
$timeline->setShouldTerminate(true);
}
return $this->buildApplicationPage(
array(
$crumbs,
$box,
$variables,
$timeline,
),
array(
'title' => $implementation->getName(),
));
}
private function renderBuildVariablesTable() {
$viewer = $this->getRequest()->getUser();
$variables = HarbormasterBuild::getAvailableBuildVariables();
ksort($variables);
$rows = array();
$rows[] = pht(
- 'The following variables can be used in most fields. To reference '.
- 'a variable, use `${name}` in a field.');
+ 'The following variables can be used in most fields. '.
+ 'To reference a variable, use `%s` in a field.',
+ '${name}');
$rows[] = pht('| Variable | Description |');
$rows[] = '|---|---|';
foreach ($variables as $name => $description) {
$rows[] = '| `'.$name.'` | '.$description.' |';
}
$rows = implode("\n", $rows);
$form = id(new AphrontFormView())
->setUser($viewer)
->appendRemarkupInstructions($rows);
return id(new PHUIObjectBoxView())
->setHeaderText(pht('Build Variables'))
->appendChild($form);
}
}
diff --git a/src/applications/harbormaster/editor/HarbormasterBuildTransactionEditor.php b/src/applications/harbormaster/editor/HarbormasterBuildTransactionEditor.php
index 0d9d26c6a..6c59622f9 100644
--- a/src/applications/harbormaster/editor/HarbormasterBuildTransactionEditor.php
+++ b/src/applications/harbormaster/editor/HarbormasterBuildTransactionEditor.php
@@ -1,114 +1,114 @@
<?php
final class HarbormasterBuildTransactionEditor
extends PhabricatorApplicationTransactionEditor {
public function getEditorApplicationClass() {
return 'PhabricatorHarbormasterApplication';
}
public function getEditorObjectsDescription() {
return pht('Harbormaster Builds');
}
public function getTransactionTypes() {
$types = parent::getTransactionTypes();
$types[] = HarbormasterBuildTransaction::TYPE_CREATE;
$types[] = HarbormasterBuildTransaction::TYPE_COMMAND;
return $types;
}
protected function getCustomTransactionOldValue(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case HarbormasterBuildTransaction::TYPE_CREATE:
case HarbormasterBuildTransaction::TYPE_COMMAND:
return null;
}
return parent::getCustomTransactionOldValue($object, $xaction);
}
protected function getCustomTransactionNewValue(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case HarbormasterBuildTransaction::TYPE_CREATE:
return true;
case HarbormasterBuildTransaction::TYPE_COMMAND:
return $xaction->getNewValue();
}
return parent::getCustomTransactionNewValue($object, $xaction);
}
protected function applyCustomInternalTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case HarbormasterBuildTransaction::TYPE_CREATE:
return;
case HarbormasterBuildTransaction::TYPE_COMMAND:
return $this->executeBuildCommand($object, $xaction);
}
return parent::applyCustomInternalTransaction($object, $xaction);
}
private function executeBuildCommand(
HarbormasterBuild $build,
HarbormasterBuildTransaction $xaction) {
$command = $xaction->getNewValue();
switch ($command) {
case HarbormasterBuildCommand::COMMAND_RESTART:
$issuable = $build->canRestartBuild();
break;
case HarbormasterBuildCommand::COMMAND_STOP:
$issuable = $build->canStopBuild();
break;
case HarbormasterBuildCommand::COMMAND_RESUME:
$issuable = $build->canResumeBuild();
break;
default:
- throw new Exception("Unknown command $command");
+ throw new Exception(pht('Unknown command %s', $command));
}
if (!$issuable) {
return;
}
id(new HarbormasterBuildCommand())
->setAuthorPHID($xaction->getAuthorPHID())
->setTargetPHID($build->getPHID())
->setCommand($command)
->save();
PhabricatorWorker::scheduleTask(
'HarbormasterBuildWorker',
array(
'buildID' => $build->getID(),
));
}
protected function applyCustomExternalTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case HarbormasterBuildTransaction::TYPE_CREATE:
case HarbormasterBuildTransaction::TYPE_COMMAND:
return;
}
return parent::applyCustomExternalTransaction($object, $xaction);
}
}
diff --git a/src/applications/harbormaster/management/HarbormasterManagementBuildWorkflow.php b/src/applications/harbormaster/management/HarbormasterManagementBuildWorkflow.php
index e7d6adb54..86b8596fe 100644
--- a/src/applications/harbormaster/management/HarbormasterManagementBuildWorkflow.php
+++ b/src/applications/harbormaster/management/HarbormasterManagementBuildWorkflow.php
@@ -1,92 +1,94 @@
<?php
final class HarbormasterManagementBuildWorkflow
extends HarbormasterManagementWorkflow {
protected function didConstruct() {
$this
->setName('build')
->setExamples('**build** [__options__] __buildable__ --plan __id__')
->setSynopsis(pht('Run plan __id__ on __buildable__.'))
->setArguments(
array(
array(
- 'name' => 'plan',
- 'param' => 'id',
- 'help' => pht('ID of build plan to run.'),
+ 'name' => 'plan',
+ 'param' => 'id',
+ 'help' => pht('ID of build plan to run.'),
),
array(
'name' => 'buildable',
'wildcard' => true,
),
));
}
public function execute(PhutilArgumentParser $args) {
$viewer = $this->getViewer();
$names = $args->getArg('buildable');
if (count($names) != 1) {
throw new PhutilArgumentUsageException(
pht('Specify exactly one buildable object, by object name.'));
}
$name = head($names);
$buildable = id(new PhabricatorObjectQuery())
->setViewer($viewer)
->withNames($names)
->executeOne();
if (!$buildable) {
throw new PhutilArgumentUsageException(
pht('No such buildable "%s"!', $name));
}
if (!($buildable instanceof HarbormasterBuildableInterface)) {
throw new PhutilArgumentUsageException(
pht('Object "%s" is not a buildable!', $name));
}
$plan_id = $args->getArg('plan');
if (!$plan_id) {
throw new PhutilArgumentUsageException(
- pht('Use --plan to specify a build plan to run.'));
+ pht(
+ 'Use %s to specify a build plan to run.',
+ '--plan'));
}
$plan = id(new HarbormasterBuildPlanQuery())
->setViewer($viewer)
->withIDs(array($plan_id))
->executeOne();
if (!$plan) {
throw new PhutilArgumentUsageException(
pht('Build plan "%s" does not exist.', $plan_id));
}
$console = PhutilConsole::getConsole();
$buildable = HarbormasterBuildable::initializeNewBuildable($viewer)
->setIsManualBuildable(true)
->setBuildablePHID($buildable->getHarbormasterBuildablePHID())
->setContainerPHID($buildable->getHarbormasterContainerPHID())
->save();
$console->writeOut(
"%s\n",
pht(
'Applying plan %s to new buildable %s...',
$plan->getID(),
'B'.$buildable->getID()));
$console->writeOut(
"\n %s\n\n",
PhabricatorEnv::getProductionURI('/B'.$buildable->getID()));
PhabricatorWorker::setRunAllTasksInProcess(true);
$buildable->applyPlan($plan);
$console->writeOut("%s\n", pht('Done.'));
return 0;
}
}
diff --git a/src/applications/harbormaster/step/HarbormasterCommandBuildStepImplementation.php b/src/applications/harbormaster/step/HarbormasterCommandBuildStepImplementation.php
index 14fc65a76..5dbfd8026 100644
--- a/src/applications/harbormaster/step/HarbormasterCommandBuildStepImplementation.php
+++ b/src/applications/harbormaster/step/HarbormasterCommandBuildStepImplementation.php
@@ -1,144 +1,144 @@
<?php
final class HarbormasterCommandBuildStepImplementation
extends HarbormasterBuildStepImplementation {
private $platform;
public function getName() {
return pht('Run Command');
}
public function getGenericDescription() {
return pht('Run a command on Drydock host.');
}
public function getDescription() {
return pht(
'Run command %s on host %s.',
$this->formatSettingForDescription('command'),
$this->formatSettingForDescription('hostartifact'));
}
public function escapeCommand($pattern, array $args) {
array_unshift($args, $pattern);
$mode = PhutilCommandString::MODE_DEFAULT;
if ($this->platform == 'windows') {
$mode = PhutilCommandString::MODE_POWERSHELL;
}
return id(new PhutilCommandString($args))
->setEscapingMode($mode);
}
public function execute(
HarbormasterBuild $build,
HarbormasterBuildTarget $build_target) {
$settings = $this->getSettings();
$variables = $build_target->getVariables();
$artifact = $build->loadArtifact($settings['hostartifact']);
$lease = $artifact->loadDrydockLease();
$this->platform = $lease->getAttribute('platform');
$command = $this->mergeVariables(
array($this, 'escapeCommand'),
$settings['command'],
$variables);
$this->platform = null;
$interface = $lease->getInterface('command');
$future = $interface->getExecFuture('%C', $command);
$log_stdout = $build->createLog($build_target, 'remote', 'stdout');
$log_stderr = $build->createLog($build_target, 'remote', 'stderr');
$start_stdout = $log_stdout->start();
$start_stderr = $log_stderr->start();
$build_update = 5;
// Read the next amount of available output every second.
$futures = new FutureIterator(array($future));
foreach ($futures->setUpdateInterval(1) as $key => $future_iter) {
if ($future_iter === null) {
// Check to see if we should abort.
if ($build_update <= 0) {
$build->reload();
if ($this->shouldAbort($build, $build_target)) {
$future->resolveKill();
throw new HarbormasterBuildAbortedException();
} else {
$build_update = 5;
}
} else {
$build_update -= 1;
}
// Command is still executing.
// Read more data as it is available.
list($stdout, $stderr) = $future->read();
$log_stdout->append($stdout);
$log_stderr->append($stderr);
$future->discardBuffers();
} else {
// Command execution is complete.
// Get the return value so we can log that as well.
list($err) = $future->resolve();
// Retrieve the last few bits of information.
list($stdout, $stderr) = $future->read();
$log_stdout->append($stdout);
$log_stderr->append($stderr);
$future->discardBuffers();
break;
}
}
$log_stdout->finalize($start_stdout);
$log_stderr->finalize($start_stderr);
if ($err) {
throw new HarbormasterBuildFailureException();
}
}
public function getArtifactInputs() {
return array(
array(
'name' => pht('Run on Host'),
'key' => $this->getSetting('hostartifact'),
'type' => HarbormasterBuildArtifact::TYPE_HOST,
),
);
}
public function getFieldSpecifications() {
return array(
'command' => array(
'name' => pht('Command'),
'type' => 'text',
'required' => true,
'caption' => pht(
- 'Under Windows, this is executed under PowerShell.'.
- 'Under UNIX, this is executed using the user\'s shell.'),
+ "Under Windows, this is executed under PowerShell. ".
+ "Under UNIX, this is executed using the user's shell."),
),
'hostartifact' => array(
'name' => pht('Host'),
'type' => 'text',
'required' => true,
),
);
}
}
diff --git a/src/applications/harbormaster/step/HarbormasterWaitForPreviousBuildStepImplementation.php b/src/applications/harbormaster/step/HarbormasterWaitForPreviousBuildStepImplementation.php
index d3e4e9252..288cb1b1f 100644
--- a/src/applications/harbormaster/step/HarbormasterWaitForPreviousBuildStepImplementation.php
+++ b/src/applications/harbormaster/step/HarbormasterWaitForPreviousBuildStepImplementation.php
@@ -1,111 +1,111 @@
<?php
final class HarbormasterWaitForPreviousBuildStepImplementation
extends HarbormasterBuildStepImplementation {
public function getName() {
return pht('Wait for Previous Commits to Build');
}
public function getGenericDescription() {
return pht(
'Wait for previous commits to finish building the current plan '.
'before continuing.');
}
public function execute(
HarbormasterBuild $build,
HarbormasterBuildTarget $build_target) {
// We can only wait when building against commits.
$buildable = $build->getBuildable();
$object = $buildable->getBuildableObject();
if (!($object instanceof PhabricatorRepositoryCommit)) {
return;
}
// Block until all previous builds of the same build plan have
// finished.
$plan = $build->getBuildPlan();
$existing_logs = id(new HarbormasterBuildLogQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withBuildTargetPHIDs(array($build_target->getPHID()))
->execute();
if ($existing_logs) {
$log = head($existing_logs);
} else {
$log = $build->createLog($build_target, 'waiting', 'blockers');
}
$blockers = $this->getBlockers($object, $plan, $build);
if ($blockers) {
$log->start();
- $log->append("Blocked by: ".implode(',', $blockers)."\n");
+ $log->append(pht("Blocked by: %s\n", implode(',', $blockers)));
$log->finalize();
}
if ($blockers) {
throw new PhabricatorWorkerYieldException(15);
}
}
private function getBlockers(
PhabricatorRepositoryCommit $commit,
HarbormasterBuildPlan $plan,
HarbormasterBuild $source) {
$call = new ConduitCall(
'diffusion.commitparentsquery',
array(
'commit' => $commit->getCommitIdentifier(),
'callsign' => $commit->getRepository()->getCallsign(),
));
$call->setUser(PhabricatorUser::getOmnipotentUser());
$parents = $call->execute();
$parents = id(new DiffusionCommitQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withRepository($commit->getRepository())
->withIdentifiers($parents)
->execute();
$blockers = array();
$build_objects = array();
foreach ($parents as $parent) {
if (!$parent->isImported()) {
$blockers[] = pht('Commit %s', $parent->getCommitIdentifier());
} else {
$build_objects[] = $parent->getPHID();
}
}
if ($build_objects) {
$buildables = id(new HarbormasterBuildableQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withBuildablePHIDs($build_objects)
->withManualBuildables(false)
->execute();
$buildable_phids = mpull($buildables, 'getPHID');
if ($buildable_phids) {
$builds = id(new HarbormasterBuildQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withBuildablePHIDs($buildable_phids)
->withBuildPlanPHIDs(array($plan->getPHID()))
->execute();
foreach ($builds as $build) {
if (!$build->isComplete()) {
$blockers[] = pht('Build %d', $build->getID());
}
}
}
}
return $blockers;
}
}
diff --git a/src/applications/harbormaster/storage/build/HarbormasterBuild.php b/src/applications/harbormaster/storage/build/HarbormasterBuild.php
index 38216c5b9..5ca60bb32 100644
--- a/src/applications/harbormaster/storage/build/HarbormasterBuild.php
+++ b/src/applications/harbormaster/storage/build/HarbormasterBuild.php
@@ -1,454 +1,454 @@
<?php
final class HarbormasterBuild extends HarbormasterDAO
implements
PhabricatorApplicationTransactionInterface,
PhabricatorPolicyInterface {
protected $buildablePHID;
protected $buildPlanPHID;
protected $buildStatus;
protected $buildGeneration;
private $buildable = self::ATTACHABLE;
private $buildPlan = self::ATTACHABLE;
private $buildTargets = self::ATTACHABLE;
private $unprocessedCommands = self::ATTACHABLE;
/**
* Not currently being built.
*/
const STATUS_INACTIVE = 'inactive';
/**
* Pending pick up by the Harbormaster daemon.
*/
const STATUS_PENDING = 'pending';
/**
* Current building the buildable.
*/
const STATUS_BUILDING = 'building';
/**
* The build has passed.
*/
const STATUS_PASSED = 'passed';
/**
* The build has failed.
*/
const STATUS_FAILED = 'failed';
/**
* The build encountered an unexpected error.
*/
const STATUS_ERROR = 'error';
/**
* The build has been stopped.
*/
const STATUS_STOPPED = 'stopped';
/**
* The build has been deadlocked.
*/
const STATUS_DEADLOCKED = 'deadlocked';
/**
* Get a human readable name for a build status constant.
*
* @param const Build status constant.
* @return string Human-readable name.
*/
public static function getBuildStatusName($status) {
switch ($status) {
case self::STATUS_INACTIVE:
return pht('Inactive');
case self::STATUS_PENDING:
return pht('Pending');
case self::STATUS_BUILDING:
return pht('Building');
case self::STATUS_PASSED:
return pht('Passed');
case self::STATUS_FAILED:
return pht('Failed');
case self::STATUS_ERROR:
return pht('Unexpected Error');
case self::STATUS_STOPPED:
return pht('Paused');
case self::STATUS_DEADLOCKED:
return pht('Deadlocked');
default:
return pht('Unknown');
}
}
public static function getBuildStatusIcon($status) {
switch ($status) {
case self::STATUS_INACTIVE:
case self::STATUS_PENDING:
return PHUIStatusItemView::ICON_OPEN;
case self::STATUS_BUILDING:
return PHUIStatusItemView::ICON_RIGHT;
case self::STATUS_PASSED:
return PHUIStatusItemView::ICON_ACCEPT;
case self::STATUS_FAILED:
return PHUIStatusItemView::ICON_REJECT;
case self::STATUS_ERROR:
return PHUIStatusItemView::ICON_MINUS;
case self::STATUS_STOPPED:
return PHUIStatusItemView::ICON_MINUS;
case self::STATUS_DEADLOCKED:
return PHUIStatusItemView::ICON_WARNING;
default:
return PHUIStatusItemView::ICON_QUESTION;
}
}
public static function getBuildStatusColor($status) {
switch ($status) {
case self::STATUS_INACTIVE:
return 'dark';
case self::STATUS_PENDING:
case self::STATUS_BUILDING:
return 'blue';
case self::STATUS_PASSED:
return 'green';
case self::STATUS_FAILED:
case self::STATUS_ERROR:
case self::STATUS_DEADLOCKED:
return 'red';
case self::STATUS_STOPPED:
return 'dark';
default:
return 'bluegrey';
}
}
public static function initializeNewBuild(PhabricatorUser $actor) {
return id(new HarbormasterBuild())
->setBuildStatus(self::STATUS_INACTIVE)
->setBuildGeneration(0);
}
public function delete() {
$this->openTransaction();
$this->deleteUnprocessedCommands();
$result = parent::delete();
$this->saveTransaction();
return $result;
}
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_COLUMN_SCHEMA => array(
'buildStatus' => 'text32',
'buildGeneration' => 'uint32',
),
self::CONFIG_KEY_SCHEMA => array(
'key_buildable' => array(
'columns' => array('buildablePHID'),
),
'key_plan' => array(
'columns' => array('buildPlanPHID'),
),
'key_status' => array(
'columns' => array('buildStatus'),
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
HarbormasterBuildPHIDType::TYPECONST);
}
public function attachBuildable(HarbormasterBuildable $buildable) {
$this->buildable = $buildable;
return $this;
}
public function getBuildable() {
return $this->assertAttached($this->buildable);
}
public function getName() {
if ($this->getBuildPlan()) {
return $this->getBuildPlan()->getName();
}
return pht('Build');
}
public function attachBuildPlan(
HarbormasterBuildPlan $build_plan = null) {
$this->buildPlan = $build_plan;
return $this;
}
public function getBuildPlan() {
return $this->assertAttached($this->buildPlan);
}
public function getBuildTargets() {
return $this->assertAttached($this->buildTargets);
}
public function attachBuildTargets(array $targets) {
$this->buildTargets = $targets;
return $this;
}
public function isBuilding() {
return $this->getBuildStatus() === self::STATUS_PENDING ||
$this->getBuildStatus() === self::STATUS_BUILDING;
}
public function createLog(
HarbormasterBuildTarget $build_target,
$log_source,
$log_type) {
$log_source = id(new PhutilUTF8StringTruncator())
->setMaximumBytes(250)
->truncateString($log_source);
$log = HarbormasterBuildLog::initializeNewBuildLog($build_target)
->setLogSource($log_source)
->setLogType($log_type)
->save();
return $log;
}
public function createArtifact(
HarbormasterBuildTarget $build_target,
$artifact_key,
$artifact_type) {
$artifact =
HarbormasterBuildArtifact::initializeNewBuildArtifact($build_target);
$artifact->setArtifactKey(
$this->getPHID(),
$this->getBuildGeneration(),
$artifact_key);
$artifact->setArtifactType($artifact_type);
$artifact->save();
return $artifact;
}
public function loadArtifact($name) {
$artifact = id(new HarbormasterBuildArtifactQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withArtifactKeys(
$this->getPHID(),
$this->getBuildGeneration(),
array($name))
->executeOne();
if ($artifact === null) {
- throw new Exception('Artifact not found!');
+ throw new Exception(pht('Artifact not found!'));
}
return $artifact;
}
public function retrieveVariablesFromBuild() {
$results = array(
'buildable.diff' => null,
'buildable.revision' => null,
'buildable.commit' => null,
'repository.callsign' => null,
'repository.vcs' => null,
'repository.uri' => null,
'step.timestamp' => null,
'build.id' => null,
);
$buildable = $this->getBuildable();
$object = $buildable->getBuildableObject();
$object_variables = $object->getBuildVariables();
$results = $object_variables + $results;
$results['step.timestamp'] = time();
$results['build.id'] = $this->getID();
return $results;
}
public static function getAvailableBuildVariables() {
$objects = id(new PhutilSymbolLoader())
->setAncestorClass('HarbormasterBuildableInterface')
->loadObjects();
$variables = array();
$variables[] = array(
'step.timestamp' => pht('The current UNIX timestamp.'),
'build.id' => pht('The ID of the current build.'),
'target.phid' => pht('The PHID of the current build target.'),
);
foreach ($objects as $object) {
$variables[] = $object->getAvailableBuildVariables();
}
$variables = array_mergev($variables);
return $variables;
}
public function isComplete() {
switch ($this->getBuildStatus()) {
case self::STATUS_PASSED:
case self::STATUS_FAILED:
case self::STATUS_ERROR:
case self::STATUS_STOPPED:
return true;
}
return false;
}
public function isStopped() {
return ($this->getBuildStatus() == self::STATUS_STOPPED);
}
/* -( Build Commands )----------------------------------------------------- */
private function getUnprocessedCommands() {
return $this->assertAttached($this->unprocessedCommands);
}
public function attachUnprocessedCommands(array $commands) {
$this->unprocessedCommands = $commands;
return $this;
}
public function canRestartBuild() {
return !$this->isRestarting();
}
public function canStopBuild() {
return !$this->isComplete() &&
!$this->isStopped() &&
!$this->isStopping();
}
public function canResumeBuild() {
return $this->isStopped() &&
!$this->isResuming();
}
public function isStopping() {
$is_stopping = false;
foreach ($this->getUnprocessedCommands() as $command_object) {
$command = $command_object->getCommand();
switch ($command) {
case HarbormasterBuildCommand::COMMAND_STOP:
$is_stopping = true;
break;
case HarbormasterBuildCommand::COMMAND_RESUME:
case HarbormasterBuildCommand::COMMAND_RESTART:
$is_stopping = false;
break;
}
}
return $is_stopping;
}
public function isResuming() {
$is_resuming = false;
foreach ($this->getUnprocessedCommands() as $command_object) {
$command = $command_object->getCommand();
switch ($command) {
case HarbormasterBuildCommand::COMMAND_RESTART:
case HarbormasterBuildCommand::COMMAND_RESUME:
$is_resuming = true;
break;
case HarbormasterBuildCommand::COMMAND_STOP:
$is_resuming = false;
break;
}
}
return $is_resuming;
}
public function isRestarting() {
$is_restarting = false;
foreach ($this->getUnprocessedCommands() as $command_object) {
$command = $command_object->getCommand();
switch ($command) {
case HarbormasterBuildCommand::COMMAND_RESTART:
$is_restarting = true;
break;
}
}
return $is_restarting;
}
public function deleteUnprocessedCommands() {
foreach ($this->getUnprocessedCommands() as $key => $command_object) {
$command_object->delete();
unset($this->unprocessedCommands[$key]);
}
return $this;
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new HarbormasterBuildTransactionEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new HarbormasterBuildTransaction();
}
public function willRenderTimeline(
PhabricatorApplicationTransactionView $timeline,
AphrontRequest $request) {
return $timeline;
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
return $this->getBuildable()->getPolicy($capability);
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return $this->getBuildable()->hasAutomaticCapability(
$capability,
$viewer);
}
public function describeAutomaticCapability($capability) {
return pht('A build inherits policies from its buildable.');
}
}
diff --git a/src/applications/harbormaster/storage/build/HarbormasterBuildArtifact.php b/src/applications/harbormaster/storage/build/HarbormasterBuildArtifact.php
index e5bcb7c9b..47df26131 100644
--- a/src/applications/harbormaster/storage/build/HarbormasterBuildArtifact.php
+++ b/src/applications/harbormaster/storage/build/HarbormasterBuildArtifact.php
@@ -1,185 +1,188 @@
<?php
final class HarbormasterBuildArtifact extends HarbormasterDAO
implements PhabricatorPolicyInterface {
protected $buildTargetPHID;
protected $artifactType;
protected $artifactIndex;
protected $artifactKey;
protected $artifactData = array();
private $buildTarget = self::ATTACHABLE;
const TYPE_FILE = 'file';
const TYPE_HOST = 'host';
const TYPE_URI = 'uri';
public static function initializeNewBuildArtifact(
HarbormasterBuildTarget $build_target) {
return id(new HarbormasterBuildArtifact())
->setBuildTargetPHID($build_target->getPHID());
}
protected function getConfiguration() {
return array(
self::CONFIG_SERIALIZATION => array(
'artifactData' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'artifactType' => 'text32',
'artifactIndex' => 'bytes12',
'artifactKey' => 'text255',
),
self::CONFIG_KEY_SCHEMA => array(
'key_artifact' => array(
'columns' => array('artifactType', 'artifactIndex'),
'unique' => true,
),
'key_garbagecollect' => array(
'columns' => array('artifactType', 'dateCreated'),
),
),
) + parent::getConfiguration();
}
public function attachBuildTarget(HarbormasterBuildTarget $build_target) {
$this->buildTarget = $build_target;
return $this;
}
public function getBuildTarget() {
return $this->assertAttached($this->buildTarget);
}
public function setArtifactKey($build_phid, $build_gen, $key) {
$this->artifactIndex =
PhabricatorHash::digestForIndex($build_phid.$build_gen.$key);
$this->artifactKey = $key;
return $this;
}
public function getObjectItemView(PhabricatorUser $viewer) {
$data = $this->getArtifactData();
switch ($this->getArtifactType()) {
case self::TYPE_FILE:
$handle = id(new PhabricatorHandleQuery())
->setViewer($viewer)
->withPHIDs($data)
->executeOne();
return id(new PHUIObjectItemView())
->setObjectName(pht('File'))
->setHeader($handle->getFullName())
->setHref($handle->getURI());
case self::TYPE_HOST:
$leases = id(new DrydockLeaseQuery())
->setViewer($viewer)
->withIDs(array($data['drydock-lease']))
->execute();
$lease = $leases[$data['drydock-lease']];
return id(new PHUIObjectItemView())
->setObjectName(pht('Drydock Lease'))
->setHeader($lease->getID())
->setHref('/drydock/lease/'.$lease->getID());
case self::TYPE_URI:
return id(new PHUIObjectItemView())
->setObjectName($data['name'])
->setHeader($data['uri'])
->setHref($data['uri']);
default:
return null;
}
}
public function loadDrydockLease() {
if ($this->getArtifactType() !== self::TYPE_HOST) {
throw new Exception(
- '`loadDrydockLease` may only be called on host artifacts.');
+ pht(
+ '`%s` may only be called on host artifacts.',
+ __FUNCTION__));
}
$data = $this->getArtifactData();
// FIXME: Is there a better way of doing this?
// TODO: Policy stuff, etc.
$lease = id(new DrydockLease())->load(
$data['drydock-lease']);
if ($lease === null) {
- throw new Exception('Associated Drydock lease not found!');
+ throw new Exception(pht('Associated Drydock lease not found!'));
}
$resource = id(new DrydockResource())->load(
$lease->getResourceID());
if ($resource === null) {
- throw new Exception('Associated Drydock resource not found!');
+ throw new Exception(pht('Associated Drydock resource not found!'));
}
$lease->attachResource($resource);
return $lease;
}
public function loadPhabricatorFile() {
if ($this->getArtifactType() !== self::TYPE_FILE) {
throw new Exception(
- '`loadPhabricatorFile` may only be called on file artifacts.');
+ pht(
+ '`%s` may only be called on file artifacts.',
+ __FUNCTION__));
}
$data = $this->getArtifactData();
// The data for TYPE_FILE is an array with a single PHID in it.
$phid = $data['filePHID'];
$file = id(new PhabricatorFileQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withPHIDs(array($phid))
->executeOne();
if ($file === null) {
- throw new Exception('Associated file not found!');
+ throw new Exception(pht('Associated file not found!'));
}
return $file;
}
public function release() {
switch ($this->getArtifactType()) {
case self::TYPE_HOST:
$this->releaseDrydockLease();
break;
}
}
public function releaseDrydockLease() {
$lease = $this->loadDrydockLease();
$resource = $lease->getResource();
$blueprint = $resource->getBlueprint();
if ($lease->isActive()) {
$blueprint->releaseLease($resource, $lease);
}
}
/* -( 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 buildable to see its artifacts.');
+ return pht('Users must be able to see a buildable to see its artifacts.');
}
}
diff --git a/src/applications/harbormaster/storage/build/HarbormasterBuildLog.php b/src/applications/harbormaster/storage/build/HarbormasterBuildLog.php
index a40554cbe..d03f58871 100644
--- a/src/applications/harbormaster/storage/build/HarbormasterBuildLog.php
+++ b/src/applications/harbormaster/storage/build/HarbormasterBuildLog.php
@@ -1,208 +1,210 @@
<?php
final class HarbormasterBuildLog extends HarbormasterDAO
implements PhabricatorPolicyInterface {
protected $buildTargetPHID;
protected $logSource;
protected $logType;
protected $duration;
protected $live;
private $buildTarget = self::ATTACHABLE;
const CHUNK_BYTE_LIMIT = 102400;
/**
* The log is encoded as plain text.
*/
const ENCODING_TEXT = 'text';
public static function initializeNewBuildLog(
HarbormasterBuildTarget $build_target) {
return id(new HarbormasterBuildLog())
->setBuildTargetPHID($build_target->getPHID())
->setDuration(null)
->setLive(0);
}
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
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',
),
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 start() {
if ($this->getLive()) {
- throw new Exception('Live logging has already started for this log.');
+ throw new Exception(
+ pht('Live logging has already started for this log.'));
}
$this->setLive(1);
$this->save();
return time();
}
public function append($content) {
if (!$this->getLive()) {
- throw new Exception('Start logging before appending data to the log.');
+ throw new Exception(
+ pht('Start logging before appending data to the log.'));
}
if (strlen($content) === 0) {
return;
}
// If the length of the content is greater than the chunk size limit,
// then we can never fit the content in a single record. We need to
// split our content out and call append on it for as many parts as there
// are to the content.
if (strlen($content) > self::CHUNK_BYTE_LIMIT) {
$current = $content;
while (strlen($current) > self::CHUNK_BYTE_LIMIT) {
$part = substr($current, 0, self::CHUNK_BYTE_LIMIT);
$current = substr($current, self::CHUNK_BYTE_LIMIT);
$this->append($part);
}
$this->append($current);
return;
}
// Retrieve the size of last chunk from the DB for this log. If the
// chunk is over 500K, then we need to create a new log entry.
$conn = $this->establishConnection('w');
$result = queryfx_all(
$conn,
'SELECT id, size, encoding '.
'FROM harbormaster_buildlogchunk '.
'WHERE logID = %d '.
'ORDER BY id DESC '.
'LIMIT 1',
$this->getID());
if (count($result) === 0 ||
$result[0]['size'] + strlen($content) > self::CHUNK_BYTE_LIMIT ||
$result[0]['encoding'] !== self::ENCODING_TEXT) {
// We must insert a new chunk because the data we are appending
// won't fit into the existing one, or we don't have any existing
// chunk data.
queryfx(
$conn,
'INSERT INTO harbormaster_buildlogchunk '.
'(logID, encoding, size, chunk) '.
'VALUES '.
'(%d, %s, %d, %B)',
$this->getID(),
self::ENCODING_TEXT,
strlen($content),
$content);
} else {
// We have a resulting record that we can append our content onto.
queryfx(
$conn,
'UPDATE harbormaster_buildlogchunk '.
'SET chunk = CONCAT(chunk, %B), size = LENGTH(CONCAT(chunk, %B))'.
'WHERE id = %d',
$content,
$content,
$result[0]['id']);
}
}
public function finalize($start = 0) {
if (!$this->getLive()) {
- throw new Exception('Start logging before finalizing it.');
+ throw new Exception(pht('Start logging before finalizing it.'));
}
// TODO: Encode the log contents in a gzipped format.
$this->reload();
if ($start > 0) {
$this->setDuration(time() - $start);
}
$this->setLive(0);
$this->save();
}
public function getLogText() {
// TODO: This won't cope very well if we're pulling like a 700MB
// log file out of the DB. We should probably implement some sort
// of optional limit parameter so that when we're rendering out only
// 25 lines in the UI, we don't wastefully read in the whole log.
// We have to read our content out of the database and stitch all of
// the log data back together.
$conn = $this->establishConnection('r');
$result = queryfx_all(
$conn,
'SELECT chunk '.
'FROM harbormaster_buildlogchunk '.
'WHERE logID = %d '.
'ORDER BY id ASC',
$this->getID());
$content = '';
foreach ($result as $row) {
$content .= $row['chunk'];
}
return $content;
}
/* -( 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 it\'s build log.');
+ "Users must be able to see a build target to view it's build log.");
}
}
diff --git a/src/applications/herald/adapter/HeraldAdapter.php b/src/applications/herald/adapter/HeraldAdapter.php
index ca52b8da1..300af2c38 100644
--- a/src/applications/herald/adapter/HeraldAdapter.php
+++ b/src/applications/herald/adapter/HeraldAdapter.php
@@ -1,1651 +1,1657 @@
<?php
/**
* @task customfield Custom Field Integration
*/
abstract class HeraldAdapter {
const FIELD_TITLE = 'title';
const FIELD_BODY = 'body';
const FIELD_AUTHOR = 'author';
const FIELD_ASSIGNEE = 'assignee';
const FIELD_REVIEWER = 'reviewer';
const FIELD_REVIEWERS = 'reviewers';
const FIELD_COMMITTER = 'committer';
const FIELD_CC = 'cc';
const FIELD_TAGS = 'tags';
const FIELD_DIFF_FILE = 'diff-file';
const FIELD_DIFF_CONTENT = 'diff-content';
const FIELD_DIFF_ADDED_CONTENT = 'diff-added-content';
const FIELD_DIFF_REMOVED_CONTENT = 'diff-removed-content';
const FIELD_DIFF_ENORMOUS = 'diff-enormous';
const FIELD_REPOSITORY = 'repository';
const FIELD_REPOSITORY_PROJECTS = 'repository-projects';
const FIELD_RULE = 'rule';
const FIELD_AFFECTED_PACKAGE = 'affected-package';
const FIELD_AFFECTED_PACKAGE_OWNER = 'affected-package-owner';
const FIELD_CONTENT_SOURCE = 'contentsource';
const FIELD_ALWAYS = 'always';
const FIELD_AUTHOR_PROJECTS = 'authorprojects';
const FIELD_PROJECTS = 'projects';
const FIELD_PUSHER = 'pusher';
const FIELD_PUSHER_PROJECTS = 'pusher-projects';
const FIELD_DIFFERENTIAL_REVISION = 'differential-revision';
const FIELD_DIFFERENTIAL_REVIEWERS = 'differential-reviewers';
const FIELD_DIFFERENTIAL_CCS = 'differential-ccs';
const FIELD_DIFFERENTIAL_ACCEPTED = 'differential-accepted';
const FIELD_IS_MERGE_COMMIT = 'is-merge-commit';
const FIELD_BRANCHES = 'branches';
const FIELD_AUTHOR_RAW = 'author-raw';
const FIELD_COMMITTER_RAW = 'committer-raw';
const FIELD_IS_NEW_OBJECT = 'new-object';
const FIELD_APPLICATION_EMAIL = 'applicaton-email';
const FIELD_TASK_PRIORITY = 'taskpriority';
const FIELD_TASK_STATUS = 'taskstatus';
const FIELD_ARCANIST_PROJECT = 'arcanist-project';
const FIELD_PUSHER_IS_COMMITTER = 'pusher-is-committer';
const FIELD_PATH = 'path';
const CONDITION_CONTAINS = 'contains';
const CONDITION_NOT_CONTAINS = '!contains';
const CONDITION_IS = 'is';
const CONDITION_IS_NOT = '!is';
const CONDITION_IS_ANY = 'isany';
const CONDITION_IS_NOT_ANY = '!isany';
const CONDITION_INCLUDE_ALL = 'all';
const CONDITION_INCLUDE_ANY = 'any';
const CONDITION_INCLUDE_NONE = 'none';
const CONDITION_IS_ME = 'me';
const CONDITION_IS_NOT_ME = '!me';
const CONDITION_REGEXP = 'regexp';
const CONDITION_RULE = 'conditions';
const CONDITION_NOT_RULE = '!conditions';
const CONDITION_EXISTS = 'exists';
const CONDITION_NOT_EXISTS = '!exists';
const CONDITION_UNCONDITIONALLY = 'unconditionally';
const CONDITION_NEVER = 'never';
const CONDITION_REGEXP_PAIR = 'regexp-pair';
const CONDITION_HAS_BIT = 'bit';
const CONDITION_NOT_BIT = '!bit';
const CONDITION_IS_TRUE = 'true';
const CONDITION_IS_FALSE = 'false';
const ACTION_ADD_CC = 'addcc';
const ACTION_REMOVE_CC = 'remcc';
const ACTION_EMAIL = 'email';
const ACTION_NOTHING = 'nothing';
const ACTION_AUDIT = 'audit';
const ACTION_FLAG = 'flag';
const ACTION_ASSIGN_TASK = 'assigntask';
const ACTION_ADD_PROJECTS = 'addprojects';
const ACTION_REMOVE_PROJECTS = 'removeprojects';
const ACTION_ADD_REVIEWERS = 'addreviewers';
const ACTION_ADD_BLOCKING_REVIEWERS = 'addblockingreviewers';
const ACTION_APPLY_BUILD_PLANS = 'applybuildplans';
const ACTION_BLOCK = 'block';
const ACTION_REQUIRE_SIGNATURE = 'signature';
const VALUE_TEXT = 'text';
const VALUE_NONE = 'none';
const VALUE_EMAIL = 'email';
const VALUE_USER = 'user';
const VALUE_TAG = 'tag';
const VALUE_RULE = 'rule';
const VALUE_REPOSITORY = 'repository';
const VALUE_OWNERS_PACKAGE = 'package';
const VALUE_PROJECT = 'project';
const VALUE_FLAG_COLOR = 'flagcolor';
const VALUE_CONTENT_SOURCE = 'contentsource';
const VALUE_USER_OR_PROJECT = 'userorproject';
const VALUE_BUILD_PLAN = 'buildplan';
const VALUE_TASK_PRIORITY = 'taskpriority';
const VALUE_TASK_STATUS = 'taskstatus';
const VALUE_ARCANIST_PROJECT = 'arcanistprojects';
const VALUE_LEGAL_DOCUMENTS = 'legaldocuments';
const VALUE_APPLICATION_EMAIL = 'applicationemail';
private $contentSource;
private $isNewObject;
private $applicationEmail;
private $customFields = false;
private $customActions = null;
private $queuedTransactions = array();
private $emailPHIDs = array();
private $forcedEmailPHIDs = array();
public function getEmailPHIDs() {
return array_values($this->emailPHIDs);
}
public function getForcedEmailPHIDs() {
return array_values($this->forcedEmailPHIDs);
}
public function getCustomActions() {
if ($this->customActions === null) {
$custom_actions = id(new PhutilSymbolLoader())
->setAncestorClass('HeraldCustomAction')
->loadObjects();
foreach ($custom_actions as $key => $object) {
if (!$object->appliesToAdapter($this)) {
unset($custom_actions[$key]);
}
}
$this->customActions = array();
foreach ($custom_actions as $action) {
$key = $action->getActionKey();
if (array_key_exists($key, $this->customActions)) {
throw new Exception(
- 'More than one Herald custom action implementation '.
- 'handles the action key: \''.$key.'\'.');
+ pht(
+ "More than one Herald custom action implementation ".
+ "handles the action key: '%s'.",
+ $key));
}
$this->customActions[$key] = $action;
}
}
return $this->customActions;
}
public function setContentSource(PhabricatorContentSource $content_source) {
$this->contentSource = $content_source;
return $this;
}
public function getContentSource() {
return $this->contentSource;
}
public function getIsNewObject() {
if (is_bool($this->isNewObject)) {
return $this->isNewObject;
}
- throw new Exception(pht('You must setIsNewObject to a boolean first!'));
+ throw new Exception(
+ pht(
+ 'You must %s to a boolean first!',
+ 'setIsNewObject()'));
}
public function setIsNewObject($new) {
$this->isNewObject = (bool)$new;
return $this;
}
public function setApplicationEmail(
PhabricatorMetaMTAApplicationEmail $email) {
$this->applicationEmail = $email;
return $this;
}
public function getApplicationEmail() {
return $this->applicationEmail;
}
abstract public function getPHID();
abstract public function getHeraldName();
public function getHeraldField($field_name) {
switch ($field_name) {
case self::FIELD_RULE:
return null;
case self::FIELD_CONTENT_SOURCE:
return $this->getContentSource()->getSource();
case self::FIELD_ALWAYS:
return true;
case self::FIELD_IS_NEW_OBJECT:
return $this->getIsNewObject();
case self::FIELD_APPLICATION_EMAIL:
$value = array();
// while there is only one match by implementation, we do set
// comparisons on phids, so return an array with just the phid
if ($this->getApplicationEmail()) {
$value[] = $this->getApplicationEmail()->getPHID();
}
return $value;
default:
if ($this->isHeraldCustomKey($field_name)) {
return $this->getCustomFieldValue($field_name);
}
- throw new Exception(
- "Unknown field '{$field_name}'!");
+ throw new Exception(pht("Unknown field '%s'!", $field_name));
}
}
abstract public function applyHeraldEffects(array $effects);
protected function handleCustomHeraldEffect(HeraldEffect $effect) {
$custom_action = idx($this->getCustomActions(), $effect->getAction());
if ($custom_action !== null) {
return $custom_action->applyEffect(
$this,
$this->getObject(),
$effect);
}
return null;
}
public function isAvailableToUser(PhabricatorUser $viewer) {
$applications = id(new PhabricatorApplicationQuery())
->setViewer($viewer)
->withInstalled(true)
->withClasses(array($this->getAdapterApplicationClass()))
->execute();
return !empty($applications);
}
public function queueTransaction($transaction) {
$this->queuedTransactions[] = $transaction;
}
public function getQueuedTransactions() {
return $this->queuedTransactions;
}
protected function newTransaction() {
$object = $this->newObject();
if (!($object instanceof PhabricatorApplicationTransactionInterface)) {
throw new Exception(
pht(
'Unable to build a new transaction for adapter object; it does '.
'not implement "%s".',
'PhabricatorApplicationTransactionInterface'));
}
return $object->getApplicationTransactionTemplate();
}
/**
* NOTE: You generally should not override this; it exists to support legacy
* adapters which had hard-coded content types.
*/
public function getAdapterContentType() {
return get_class($this);
}
abstract public function getAdapterContentName();
abstract public function getAdapterContentDescription();
abstract public function getAdapterApplicationClass();
abstract public function getObject();
/**
* Return a new characteristic object for this adapter.
*
* The adapter will use this object to test for interfaces, generate
* transactions, and interact with custom fields.
*
* Adapters must return an object from this method to enable custom
* field rules and various implicit actions.
*
* Normally, you'll return an empty version of the adapted object:
*
* return new ApplicationObject();
*
* @return null|object Template object.
*/
protected function newObject() {
return null;
}
public function supportsRuleType($rule_type) {
return false;
}
public function canTriggerOnObject($object) {
return false;
}
public function explainValidTriggerObjects() {
return pht('This adapter can not trigger on objects.');
}
public function getTriggerObjectPHIDs() {
return array($this->getPHID());
}
public function getAdapterSortKey() {
return sprintf(
'%08d%s',
$this->getAdapterSortOrder(),
$this->getAdapterContentName());
}
public function getAdapterSortOrder() {
return 1000;
}
/* -( Fields )------------------------------------------------------------- */
public function getFields() {
$fields = array();
$fields[] = self::FIELD_ALWAYS;
$fields[] = self::FIELD_RULE;
$custom_fields = $this->getCustomFields();
if ($custom_fields) {
foreach ($custom_fields->getFields() as $custom_field) {
$key = $custom_field->getFieldKey();
$fields[] = $this->getHeraldKeyFromCustomKey($key);
}
}
return $fields;
}
public function getFieldNameMap() {
return array(
self::FIELD_TITLE => pht('Title'),
self::FIELD_BODY => pht('Body'),
self::FIELD_AUTHOR => pht('Author'),
self::FIELD_ASSIGNEE => pht('Assignee'),
self::FIELD_COMMITTER => pht('Committer'),
self::FIELD_REVIEWER => pht('Reviewer'),
self::FIELD_REVIEWERS => pht('Reviewers'),
self::FIELD_CC => pht('CCs'),
self::FIELD_TAGS => pht('Tags'),
self::FIELD_DIFF_FILE => pht('Any changed filename'),
self::FIELD_DIFF_CONTENT => pht('Any changed file content'),
self::FIELD_DIFF_ADDED_CONTENT => pht('Any added file content'),
self::FIELD_DIFF_REMOVED_CONTENT => pht('Any removed file content'),
self::FIELD_DIFF_ENORMOUS => pht('Change is enormous'),
self::FIELD_REPOSITORY => pht('Repository'),
self::FIELD_REPOSITORY_PROJECTS => pht('Repository\'s projects'),
self::FIELD_RULE => pht('Another Herald rule'),
self::FIELD_AFFECTED_PACKAGE => pht('Any affected package'),
self::FIELD_AFFECTED_PACKAGE_OWNER =>
pht("Any affected package's owner"),
self::FIELD_CONTENT_SOURCE => pht('Content Source'),
self::FIELD_ALWAYS => pht('Always'),
self::FIELD_AUTHOR_PROJECTS => pht("Author's projects"),
self::FIELD_PROJECTS => pht('Projects'),
self::FIELD_PUSHER => pht('Pusher'),
self::FIELD_PUSHER_PROJECTS => pht("Pusher's projects"),
self::FIELD_DIFFERENTIAL_REVISION => pht('Differential revision'),
self::FIELD_DIFFERENTIAL_REVIEWERS => pht('Differential reviewers'),
self::FIELD_DIFFERENTIAL_CCS => pht('Differential CCs'),
self::FIELD_DIFFERENTIAL_ACCEPTED
=> pht('Accepted Differential revision'),
self::FIELD_IS_MERGE_COMMIT => pht('Commit is a merge'),
self::FIELD_BRANCHES => pht('Commit\'s branches'),
self::FIELD_AUTHOR_RAW => pht('Raw author name'),
self::FIELD_COMMITTER_RAW => pht('Raw committer name'),
self::FIELD_IS_NEW_OBJECT => pht('Is newly created?'),
self::FIELD_APPLICATION_EMAIL => pht('Receiving email address'),
self::FIELD_TASK_PRIORITY => pht('Task priority'),
self::FIELD_TASK_STATUS => pht('Task status'),
self::FIELD_ARCANIST_PROJECT => pht('Arcanist Project'),
self::FIELD_PUSHER_IS_COMMITTER => pht('Pusher same as committer'),
self::FIELD_PATH => pht('Path'),
) + $this->getCustomFieldNameMap();
}
/* -( Conditions )--------------------------------------------------------- */
public function getConditionNameMap() {
return array(
self::CONDITION_CONTAINS => pht('contains'),
self::CONDITION_NOT_CONTAINS => pht('does not contain'),
self::CONDITION_IS => pht('is'),
self::CONDITION_IS_NOT => pht('is not'),
self::CONDITION_IS_ANY => pht('is any of'),
self::CONDITION_IS_TRUE => pht('is true'),
self::CONDITION_IS_FALSE => pht('is false'),
self::CONDITION_IS_NOT_ANY => pht('is not any of'),
self::CONDITION_INCLUDE_ALL => pht('include all of'),
self::CONDITION_INCLUDE_ANY => pht('include any of'),
self::CONDITION_INCLUDE_NONE => pht('do not include'),
self::CONDITION_IS_ME => pht('is myself'),
self::CONDITION_IS_NOT_ME => pht('is not myself'),
self::CONDITION_REGEXP => pht('matches regexp'),
self::CONDITION_RULE => pht('matches:'),
self::CONDITION_NOT_RULE => pht('does not match:'),
self::CONDITION_EXISTS => pht('exists'),
self::CONDITION_NOT_EXISTS => pht('does not exist'),
self::CONDITION_UNCONDITIONALLY => '', // don't show anything!
self::CONDITION_NEVER => '', // don't show anything!
self::CONDITION_REGEXP_PAIR => pht('matches regexp pair'),
self::CONDITION_HAS_BIT => pht('has bit'),
self::CONDITION_NOT_BIT => pht('lacks bit'),
);
}
public function getConditionsForField($field) {
switch ($field) {
case self::FIELD_TITLE:
case self::FIELD_BODY:
case self::FIELD_COMMITTER_RAW:
case self::FIELD_AUTHOR_RAW:
case self::FIELD_PATH:
return array(
self::CONDITION_CONTAINS,
self::CONDITION_NOT_CONTAINS,
self::CONDITION_IS,
self::CONDITION_IS_NOT,
self::CONDITION_REGEXP,
);
case self::FIELD_REVIEWER:
case self::FIELD_PUSHER:
case self::FIELD_TASK_PRIORITY:
case self::FIELD_TASK_STATUS:
case self::FIELD_ARCANIST_PROJECT:
return array(
self::CONDITION_IS_ANY,
self::CONDITION_IS_NOT_ANY,
);
case self::FIELD_REPOSITORY:
case self::FIELD_ASSIGNEE:
case self::FIELD_AUTHOR:
case self::FIELD_COMMITTER:
return array(
self::CONDITION_IS_ANY,
self::CONDITION_IS_NOT_ANY,
self::CONDITION_EXISTS,
self::CONDITION_NOT_EXISTS,
);
case self::FIELD_TAGS:
case self::FIELD_REVIEWERS:
case self::FIELD_CC:
case self::FIELD_AUTHOR_PROJECTS:
case self::FIELD_PROJECTS:
case self::FIELD_AFFECTED_PACKAGE:
case self::FIELD_AFFECTED_PACKAGE_OWNER:
case self::FIELD_PUSHER_PROJECTS:
case self::FIELD_REPOSITORY_PROJECTS:
return array(
self::CONDITION_INCLUDE_ALL,
self::CONDITION_INCLUDE_ANY,
self::CONDITION_INCLUDE_NONE,
self::CONDITION_EXISTS,
self::CONDITION_NOT_EXISTS,
);
case self::FIELD_APPLICATION_EMAIL:
return array(
self::CONDITION_INCLUDE_ANY,
self::CONDITION_INCLUDE_NONE,
self::CONDITION_EXISTS,
self::CONDITION_NOT_EXISTS,
);
case self::FIELD_DIFF_FILE:
case self::FIELD_BRANCHES:
return array(
self::CONDITION_CONTAINS,
self::CONDITION_REGEXP,
);
case self::FIELD_DIFF_CONTENT:
case self::FIELD_DIFF_ADDED_CONTENT:
case self::FIELD_DIFF_REMOVED_CONTENT:
return array(
self::CONDITION_CONTAINS,
self::CONDITION_REGEXP,
self::CONDITION_REGEXP_PAIR,
);
case self::FIELD_RULE:
return array(
self::CONDITION_RULE,
self::CONDITION_NOT_RULE,
);
case self::FIELD_CONTENT_SOURCE:
return array(
self::CONDITION_IS,
self::CONDITION_IS_NOT,
);
case self::FIELD_ALWAYS:
return array(
self::CONDITION_UNCONDITIONALLY,
);
case self::FIELD_DIFFERENTIAL_REVIEWERS:
return array(
self::CONDITION_EXISTS,
self::CONDITION_NOT_EXISTS,
self::CONDITION_INCLUDE_ALL,
self::CONDITION_INCLUDE_ANY,
self::CONDITION_INCLUDE_NONE,
);
case self::FIELD_DIFFERENTIAL_CCS:
return array(
self::CONDITION_INCLUDE_ALL,
self::CONDITION_INCLUDE_ANY,
self::CONDITION_INCLUDE_NONE,
);
case self::FIELD_DIFFERENTIAL_REVISION:
case self::FIELD_DIFFERENTIAL_ACCEPTED:
return array(
self::CONDITION_EXISTS,
self::CONDITION_NOT_EXISTS,
);
case self::FIELD_IS_MERGE_COMMIT:
case self::FIELD_DIFF_ENORMOUS:
case self::FIELD_IS_NEW_OBJECT:
case self::FIELD_PUSHER_IS_COMMITTER:
return array(
self::CONDITION_IS_TRUE,
self::CONDITION_IS_FALSE,
);
default:
if ($this->isHeraldCustomKey($field)) {
return $this->getCustomFieldConditions($field);
}
throw new Exception(
- "This adapter does not define conditions for field '{$field}'!");
+ pht(
+ "This adapter does not define conditions for field '%s'!",
+ $field));
}
}
public function doesConditionMatch(
HeraldEngine $engine,
HeraldRule $rule,
HeraldCondition $condition,
$field_value) {
$condition_type = $condition->getFieldCondition();
$condition_value = $condition->getValue();
switch ($condition_type) {
case self::CONDITION_CONTAINS:
// "Contains" can take an array of strings, as in "Any changed
// filename" for diffs.
foreach ((array)$field_value as $value) {
if (stripos($value, $condition_value) !== false) {
return true;
}
}
return false;
case self::CONDITION_NOT_CONTAINS:
return (stripos($field_value, $condition_value) === false);
case self::CONDITION_IS:
return ($field_value == $condition_value);
case self::CONDITION_IS_NOT:
return ($field_value != $condition_value);
case self::CONDITION_IS_ME:
return ($field_value == $rule->getAuthorPHID());
case self::CONDITION_IS_NOT_ME:
return ($field_value != $rule->getAuthorPHID());
case self::CONDITION_IS_ANY:
if (!is_array($condition_value)) {
throw new HeraldInvalidConditionException(
- 'Expected condition value to be an array.');
+ pht('Expected condition value to be an array.'));
}
$condition_value = array_fuse($condition_value);
return isset($condition_value[$field_value]);
case self::CONDITION_IS_NOT_ANY:
if (!is_array($condition_value)) {
throw new HeraldInvalidConditionException(
- 'Expected condition value to be an array.');
+ pht('Expected condition value to be an array.'));
}
$condition_value = array_fuse($condition_value);
return !isset($condition_value[$field_value]);
case self::CONDITION_INCLUDE_ALL:
if (!is_array($field_value)) {
throw new HeraldInvalidConditionException(
- 'Object produced non-array value!');
+ pht('Object produced non-array value!'));
}
if (!is_array($condition_value)) {
throw new HeraldInvalidConditionException(
- 'Expected condition value to be an array.');
+ pht('Expected condition value to be an array.'));
}
$have = array_select_keys(array_fuse($field_value), $condition_value);
return (count($have) == count($condition_value));
case self::CONDITION_INCLUDE_ANY:
return (bool)array_select_keys(
array_fuse($field_value),
$condition_value);
case self::CONDITION_INCLUDE_NONE:
return !array_select_keys(
array_fuse($field_value),
$condition_value);
case self::CONDITION_EXISTS:
case self::CONDITION_IS_TRUE:
return (bool)$field_value;
case self::CONDITION_NOT_EXISTS:
case self::CONDITION_IS_FALSE:
return !$field_value;
case self::CONDITION_UNCONDITIONALLY:
return (bool)$field_value;
case self::CONDITION_NEVER:
return false;
case self::CONDITION_REGEXP:
foreach ((array)$field_value as $value) {
// We add the 'S' flag because we use the regexp multiple times.
// It shouldn't cause any troubles if the flag is already there
// - /.*/S is evaluated same as /.*/SS.
$result = @preg_match($condition_value.'S', $value);
if ($result === false) {
throw new HeraldInvalidConditionException(
- 'Regular expression is not valid!');
+ pht('Regular expression is not valid!'));
}
if ($result) {
return true;
}
}
return false;
case self::CONDITION_REGEXP_PAIR:
// Match a JSON-encoded pair of regular expressions against a
// dictionary. The first regexp must match the dictionary key, and the
// second regexp must match the dictionary value. If any key/value pair
// in the dictionary matches both regexps, the condition is satisfied.
$regexp_pair = null;
try {
$regexp_pair = phutil_json_decode($condition_value);
} catch (PhutilJSONParserException $ex) {
throw new HeraldInvalidConditionException(
pht('Regular expression pair is not valid JSON!'));
}
if (count($regexp_pair) != 2) {
throw new HeraldInvalidConditionException(
pht('Regular expression pair is not a pair!'));
}
$key_regexp = array_shift($regexp_pair);
$value_regexp = array_shift($regexp_pair);
foreach ((array)$field_value as $key => $value) {
$key_matches = @preg_match($key_regexp, $key);
if ($key_matches === false) {
throw new HeraldInvalidConditionException(
- 'First regular expression is invalid!');
+ pht('First regular expression is invalid!'));
}
if ($key_matches) {
$value_matches = @preg_match($value_regexp, $value);
if ($value_matches === false) {
throw new HeraldInvalidConditionException(
- 'Second regular expression is invalid!');
+ pht('Second regular expression is invalid!'));
}
if ($value_matches) {
return true;
}
}
}
return false;
case self::CONDITION_RULE:
case self::CONDITION_NOT_RULE:
$rule = $engine->getRule($condition_value);
if (!$rule) {
throw new HeraldInvalidConditionException(
- 'Condition references a rule which does not exist!');
+ pht('Condition references a rule which does not exist!'));
}
$is_not = ($condition_type == self::CONDITION_NOT_RULE);
$result = $engine->doesRuleMatch($rule, $this);
if ($is_not) {
$result = !$result;
}
return $result;
case self::CONDITION_HAS_BIT:
return (($condition_value & $field_value) === (int)$condition_value);
case self::CONDITION_NOT_BIT:
return (($condition_value & $field_value) !== (int)$condition_value);
default:
throw new HeraldInvalidConditionException(
- "Unknown condition '{$condition_type}'.");
+ pht("Unknown condition '%s'.", $condition_type));
}
}
public function willSaveCondition(HeraldCondition $condition) {
$condition_type = $condition->getFieldCondition();
$condition_value = $condition->getValue();
switch ($condition_type) {
case self::CONDITION_REGEXP:
$ok = @preg_match($condition_value, '');
if ($ok === false) {
throw new HeraldInvalidConditionException(
pht(
'The regular expression "%s" is not valid. Regular expressions '.
'must have enclosing characters (e.g. "@/path/to/file@", not '.
'"/path/to/file") and be syntactically correct.',
$condition_value));
}
break;
case self::CONDITION_REGEXP_PAIR:
$json = null;
try {
$json = phutil_json_decode($condition_value);
} catch (PhutilJSONParserException $ex) {
throw new HeraldInvalidConditionException(
pht(
'The regular expression pair "%s" is not valid JSON. Enter a '.
'valid JSON array with two elements.',
$condition_value));
}
if (count($json) != 2) {
throw new HeraldInvalidConditionException(
pht(
'The regular expression pair "%s" must have exactly two '.
'elements.',
$condition_value));
}
$key_regexp = array_shift($json);
$val_regexp = array_shift($json);
$key_ok = @preg_match($key_regexp, '');
if ($key_ok === false) {
throw new HeraldInvalidConditionException(
pht(
'The first regexp in the regexp pair, "%s", is not a valid '.
'regexp.',
$key_regexp));
}
$val_ok = @preg_match($val_regexp, '');
if ($val_ok === false) {
throw new HeraldInvalidConditionException(
pht(
'The second regexp in the regexp pair, "%s", is not a valid '.
'regexp.',
$val_regexp));
}
break;
case self::CONDITION_CONTAINS:
case self::CONDITION_NOT_CONTAINS:
case self::CONDITION_IS:
case self::CONDITION_IS_NOT:
case self::CONDITION_IS_ANY:
case self::CONDITION_IS_NOT_ANY:
case self::CONDITION_INCLUDE_ALL:
case self::CONDITION_INCLUDE_ANY:
case self::CONDITION_INCLUDE_NONE:
case self::CONDITION_IS_ME:
case self::CONDITION_IS_NOT_ME:
case self::CONDITION_RULE:
case self::CONDITION_NOT_RULE:
case self::CONDITION_EXISTS:
case self::CONDITION_NOT_EXISTS:
case self::CONDITION_UNCONDITIONALLY:
case self::CONDITION_NEVER:
case self::CONDITION_HAS_BIT:
case self::CONDITION_NOT_BIT:
case self::CONDITION_IS_TRUE:
case self::CONDITION_IS_FALSE:
// No explicit validation for these types, although there probably
// should be in some cases.
break;
default:
throw new HeraldInvalidConditionException(
pht(
'Unknown condition "%s"!',
$condition_type));
}
}
/* -( Actions )------------------------------------------------------------ */
public function getCustomActionsForRuleType($rule_type) {
$results = array();
foreach ($this->getCustomActions() as $custom_action) {
if ($custom_action->appliesToRuleType($rule_type)) {
$results[] = $custom_action;
}
}
return $results;
}
public function getActions($rule_type) {
$custom_actions = $this->getCustomActionsForRuleType($rule_type);
$custom_actions = mpull($custom_actions, 'getActionKey');
$actions = $custom_actions;
$object = $this->newObject();
if (($object instanceof PhabricatorProjectInterface)) {
if ($rule_type == HeraldRuleTypeConfig::RULE_TYPE_GLOBAL) {
$actions[] = self::ACTION_ADD_PROJECTS;
$actions[] = self::ACTION_REMOVE_PROJECTS;
}
}
return $actions;
}
public function getActionNameMap($rule_type) {
switch ($rule_type) {
case HeraldRuleTypeConfig::RULE_TYPE_GLOBAL:
case HeraldRuleTypeConfig::RULE_TYPE_OBJECT:
$standard = array(
self::ACTION_NOTHING => pht('Do nothing'),
self::ACTION_ADD_CC => pht('Add emails to CC'),
self::ACTION_REMOVE_CC => pht('Remove emails from CC'),
self::ACTION_EMAIL => pht('Send an email to'),
self::ACTION_AUDIT => pht('Trigger an Audit by'),
self::ACTION_FLAG => pht('Mark with flag'),
self::ACTION_ASSIGN_TASK => pht('Assign task to'),
self::ACTION_ADD_PROJECTS => pht('Add projects'),
self::ACTION_REMOVE_PROJECTS => pht('Remove projects'),
self::ACTION_ADD_REVIEWERS => pht('Add reviewers'),
self::ACTION_ADD_BLOCKING_REVIEWERS => pht('Add blocking reviewers'),
self::ACTION_APPLY_BUILD_PLANS => pht('Run build plans'),
self::ACTION_REQUIRE_SIGNATURE => pht('Require legal signatures'),
self::ACTION_BLOCK => pht('Block change with message'),
);
break;
case HeraldRuleTypeConfig::RULE_TYPE_PERSONAL:
$standard = array(
self::ACTION_NOTHING => pht('Do nothing'),
self::ACTION_ADD_CC => pht('Add me to CC'),
self::ACTION_REMOVE_CC => pht('Remove me from CC'),
self::ACTION_EMAIL => pht('Send me an email'),
self::ACTION_AUDIT => pht('Trigger an Audit by me'),
self::ACTION_FLAG => pht('Mark with flag'),
self::ACTION_ASSIGN_TASK => pht('Assign task to me'),
self::ACTION_ADD_REVIEWERS => pht('Add me as a reviewer'),
self::ACTION_ADD_BLOCKING_REVIEWERS =>
pht('Add me as a blocking reviewer'),
);
break;
default:
- throw new Exception("Unknown rule type '{$rule_type}'!");
+ throw new Exception(pht("Unknown rule type '%s'!", $rule_type));
}
$custom_actions = $this->getCustomActionsForRuleType($rule_type);
$standard += mpull($custom_actions, 'getActionName', 'getActionKey');
return $standard;
}
public function willSaveAction(
HeraldRule $rule,
HeraldAction $action) {
$target = $action->getTarget();
if (is_array($target)) {
$target = array_keys($target);
}
$author_phid = $rule->getAuthorPHID();
$rule_type = $rule->getRuleType();
if ($rule_type == HeraldRuleTypeConfig::RULE_TYPE_PERSONAL) {
switch ($action->getAction()) {
case self::ACTION_EMAIL:
case self::ACTION_ADD_CC:
case self::ACTION_REMOVE_CC:
case self::ACTION_AUDIT:
case self::ACTION_ASSIGN_TASK:
case self::ACTION_ADD_REVIEWERS:
case self::ACTION_ADD_BLOCKING_REVIEWERS:
// For personal rules, force these actions to target the rule owner.
$target = array($author_phid);
break;
case self::ACTION_FLAG:
// Make sure flag color is valid; set to blue if not.
$color_map = PhabricatorFlagColor::getColorNameMap();
if (empty($color_map[$target])) {
$target = PhabricatorFlagColor::COLOR_BLUE;
}
break;
case self::ACTION_BLOCK:
case self::ACTION_NOTHING:
break;
default:
throw new HeraldInvalidActionException(
pht(
'Unrecognized action type "%s"!',
$action->getAction()));
}
}
$action->setTarget($target);
}
/* -( Values )------------------------------------------------------------- */
public function getValueTypeForFieldAndCondition($field, $condition) {
if ($this->isHeraldCustomKey($field)) {
$value_type = $this->getCustomFieldValueTypeForFieldAndCondition(
$field,
$condition);
if ($value_type !== null) {
return $value_type;
}
}
switch ($condition) {
case self::CONDITION_CONTAINS:
case self::CONDITION_NOT_CONTAINS:
case self::CONDITION_REGEXP:
case self::CONDITION_REGEXP_PAIR:
return self::VALUE_TEXT;
case self::CONDITION_IS:
case self::CONDITION_IS_NOT:
switch ($field) {
case self::FIELD_CONTENT_SOURCE:
return self::VALUE_CONTENT_SOURCE;
default:
return self::VALUE_TEXT;
}
break;
case self::CONDITION_IS_ANY:
case self::CONDITION_IS_NOT_ANY:
switch ($field) {
case self::FIELD_REPOSITORY:
return self::VALUE_REPOSITORY;
case self::FIELD_TASK_PRIORITY:
return self::VALUE_TASK_PRIORITY;
case self::FIELD_TASK_STATUS:
return self::VALUE_TASK_STATUS;
case self::FIELD_ARCANIST_PROJECT:
return self::VALUE_ARCANIST_PROJECT;
default:
return self::VALUE_USER;
}
break;
case self::CONDITION_INCLUDE_ALL:
case self::CONDITION_INCLUDE_ANY:
case self::CONDITION_INCLUDE_NONE:
switch ($field) {
case self::FIELD_REPOSITORY:
return self::VALUE_REPOSITORY;
case self::FIELD_CC:
return self::VALUE_EMAIL;
case self::FIELD_TAGS:
return self::VALUE_TAG;
case self::FIELD_AFFECTED_PACKAGE:
return self::VALUE_OWNERS_PACKAGE;
case self::FIELD_AUTHOR_PROJECTS:
case self::FIELD_PUSHER_PROJECTS:
case self::FIELD_PROJECTS:
case self::FIELD_REPOSITORY_PROJECTS:
return self::VALUE_PROJECT;
case self::FIELD_REVIEWERS:
return self::VALUE_USER_OR_PROJECT;
case self::FIELD_APPLICATION_EMAIL:
return self::VALUE_APPLICATION_EMAIL;
default:
return self::VALUE_USER;
}
break;
case self::CONDITION_IS_ME:
case self::CONDITION_IS_NOT_ME:
case self::CONDITION_EXISTS:
case self::CONDITION_NOT_EXISTS:
case self::CONDITION_UNCONDITIONALLY:
case self::CONDITION_NEVER:
case self::CONDITION_IS_TRUE:
case self::CONDITION_IS_FALSE:
return self::VALUE_NONE;
case self::CONDITION_RULE:
case self::CONDITION_NOT_RULE:
return self::VALUE_RULE;
default:
- throw new Exception("Unknown condition '{$condition}'.");
+ throw new Exception(pht("Unknown condition '%s'.", $condition));
}
}
public function getValueTypeForAction($action, $rule_type) {
$is_personal = ($rule_type == HeraldRuleTypeConfig::RULE_TYPE_PERSONAL);
if ($is_personal) {
switch ($action) {
case self::ACTION_ADD_CC:
case self::ACTION_REMOVE_CC:
case self::ACTION_EMAIL:
case self::ACTION_NOTHING:
case self::ACTION_AUDIT:
case self::ACTION_ASSIGN_TASK:
case self::ACTION_ADD_REVIEWERS:
case self::ACTION_ADD_BLOCKING_REVIEWERS:
return self::VALUE_NONE;
case self::ACTION_FLAG:
return self::VALUE_FLAG_COLOR;
case self::ACTION_ADD_PROJECTS:
case self::ACTION_REMOVE_PROJECTS:
return self::VALUE_PROJECT;
}
} else {
switch ($action) {
case self::ACTION_ADD_CC:
case self::ACTION_REMOVE_CC:
case self::ACTION_EMAIL:
return self::VALUE_EMAIL;
case self::ACTION_NOTHING:
return self::VALUE_NONE;
case self::ACTION_ADD_PROJECTS:
case self::ACTION_REMOVE_PROJECTS:
return self::VALUE_PROJECT;
case self::ACTION_FLAG:
return self::VALUE_FLAG_COLOR;
case self::ACTION_ASSIGN_TASK:
return self::VALUE_USER;
case self::ACTION_AUDIT:
case self::ACTION_ADD_REVIEWERS:
case self::ACTION_ADD_BLOCKING_REVIEWERS:
return self::VALUE_USER_OR_PROJECT;
case self::ACTION_APPLY_BUILD_PLANS:
return self::VALUE_BUILD_PLAN;
case self::ACTION_REQUIRE_SIGNATURE:
return self::VALUE_LEGAL_DOCUMENTS;
case self::ACTION_BLOCK:
return self::VALUE_TEXT;
}
}
$custom_action = idx($this->getCustomActions(), $action);
if ($custom_action !== null) {
return $custom_action->getActionType();
}
- throw new Exception("Unknown or invalid action '".$action."'.");
+ throw new Exception(pht("Unknown or invalid action '%s'.", $action));
}
/* -( Repetition )--------------------------------------------------------- */
public function getRepetitionOptions() {
return array(
HeraldRepetitionPolicyConfig::EVERY,
);
}
public static function getAllAdapters() {
static $adapters;
if (!$adapters) {
$adapters = id(new PhutilSymbolLoader())
->setAncestorClass(__CLASS__)
->loadObjects();
$adapters = msort($adapters, 'getAdapterSortKey');
}
return $adapters;
}
public static function getAdapterForContentType($content_type) {
$adapters = self::getAllAdapters();
foreach ($adapters as $adapter) {
if ($adapter->getAdapterContentType() == $content_type) {
return $adapter;
}
}
throw new Exception(
pht(
'No adapter exists for Herald content type "%s".',
$content_type));
}
public static function getEnabledAdapterMap(PhabricatorUser $viewer) {
$map = array();
$adapters = self::getAllAdapters();
foreach ($adapters as $adapter) {
if (!$adapter->isAvailableToUser($viewer)) {
continue;
}
$type = $adapter->getAdapterContentType();
$name = $adapter->getAdapterContentName();
$map[$type] = $name;
}
return $map;
}
public function renderRuleAsText(
HeraldRule $rule,
PhabricatorHandleList $handles) {
require_celerity_resource('herald-css');
$icon = id(new PHUIIconView())
->setIconFont('fa-chevron-circle-right lightgreytext')
->addClass('herald-list-icon');
if ($rule->getMustMatchAll()) {
$match_text = pht('When all of these conditions are met:');
} else {
$match_text = pht('When any of these conditions are met:');
}
$match_title = phutil_tag(
'p',
array(
'class' => 'herald-list-description',
),
$match_text);
$match_list = array();
foreach ($rule->getConditions() as $condition) {
$match_list[] = phutil_tag(
'div',
array(
'class' => 'herald-list-item',
),
array(
$icon,
$this->renderConditionAsText($condition, $handles),
));
}
$integer_code_for_every = HeraldRepetitionPolicyConfig::toInt(
HeraldRepetitionPolicyConfig::EVERY);
if ($rule->getRepetitionPolicy() == $integer_code_for_every) {
$action_text =
pht('Take these actions every time this rule matches:');
} else {
$action_text =
pht('Take these actions the first time this rule matches:');
}
$action_title = phutil_tag(
'p',
array(
'class' => 'herald-list-description',
),
$action_text);
$action_list = array();
foreach ($rule->getActions() as $action) {
$action_list[] = phutil_tag(
'div',
array(
'class' => 'herald-list-item',
),
array(
$icon,
$this->renderActionAsText($action, $handles),
));
}
return array(
$match_title,
$match_list,
$action_title,
$action_list,
);
}
private function renderConditionAsText(
HeraldCondition $condition,
PhabricatorHandleList $handles) {
$field_type = $condition->getFieldName();
$default = $this->isHeraldCustomKey($field_type)
? pht('(Unknown Custom Field "%s")', $field_type)
: pht('(Unknown Field "%s")', $field_type);
$field_name = idx($this->getFieldNameMap(), $field_type, $default);
$condition_type = $condition->getFieldCondition();
$condition_name = idx($this->getConditionNameMap(), $condition_type);
$value = $this->renderConditionValueAsText($condition, $handles);
return hsprintf(' %s %s %s', $field_name, $condition_name, $value);
}
private function renderActionAsText(
HeraldAction $action,
PhabricatorHandleList $handles) {
$rule_global = HeraldRuleTypeConfig::RULE_TYPE_GLOBAL;
$action_type = $action->getAction();
$action_name = idx($this->getActionNameMap($rule_global), $action_type);
$target = $this->renderActionTargetAsText($action, $handles);
return hsprintf(' %s %s', $action_name, $target);
}
private function renderConditionValueAsText(
HeraldCondition $condition,
PhabricatorHandleList $handles) {
$value = $condition->getValue();
if (!is_array($value)) {
$value = array($value);
}
switch ($condition->getFieldName()) {
case self::FIELD_TASK_PRIORITY:
$priority_map = ManiphestTaskPriority::getTaskPriorityMap();
foreach ($value as $index => $val) {
$name = idx($priority_map, $val);
if ($name) {
$value[$index] = $name;
}
}
break;
case self::FIELD_TASK_STATUS:
$status_map = ManiphestTaskStatus::getTaskStatusMap();
foreach ($value as $index => $val) {
$name = idx($status_map, $val);
if ($name) {
$value[$index] = $name;
}
}
break;
case HeraldPreCommitRefAdapter::FIELD_REF_CHANGE:
$change_map =
PhabricatorRepositoryPushLog::getHeraldChangeFlagConditionOptions();
foreach ($value as $index => $val) {
$name = idx($change_map, $val);
if ($name) {
$value[$index] = $name;
}
}
break;
default:
foreach ($value as $index => $val) {
$handle = $handles->getHandleIfExists($val);
if ($handle) {
$value[$index] = $handle->renderLink();
}
}
break;
}
$value = phutil_implode_html(', ', $value);
return $value;
}
private function renderActionTargetAsText(
HeraldAction $action,
PhabricatorHandleList $handles) {
$target = $action->getTarget();
if (!is_array($target)) {
$target = array($target);
}
foreach ($target as $index => $val) {
switch ($action->getAction()) {
case self::ACTION_FLAG:
$target[$index] = PhabricatorFlagColor::getColorName($val);
break;
default:
$handle = $handles->getHandleIfExists($val);
if ($handle) {
$target[$index] = $handle->renderLink();
}
break;
}
}
$target = phutil_implode_html(', ', $target);
return $target;
}
/**
* Given a @{class:HeraldRule}, this function extracts all the phids that
* we'll want to load as handles later.
*
* This function performs a somewhat hacky approach to figuring out what
* is and is not a phid - try to get the phid type and if the type is
* *not* unknown assume its a valid phid.
*
* Don't try this at home. Use more strongly typed data at home.
*
* Think of the children.
*/
public static function getHandlePHIDs(HeraldRule $rule) {
$phids = array($rule->getAuthorPHID());
foreach ($rule->getConditions() as $condition) {
$value = $condition->getValue();
if (!is_array($value)) {
$value = array($value);
}
foreach ($value as $val) {
if (phid_get_type($val) !=
PhabricatorPHIDConstants::PHID_TYPE_UNKNOWN) {
$phids[] = $val;
}
}
}
foreach ($rule->getActions() as $action) {
$target = $action->getTarget();
if (!is_array($target)) {
$target = array($target);
}
foreach ($target as $val) {
if (phid_get_type($val) !=
PhabricatorPHIDConstants::PHID_TYPE_UNKNOWN) {
$phids[] = $val;
}
}
}
if ($rule->isObjectRule()) {
$phids[] = $rule->getTriggerObjectPHID();
}
return $phids;
}
/* -( Custom Field Integration )------------------------------------------- */
/**
* Returns the prefix used to namespace Herald fields which are based on
* custom fields.
*
* @return string Key prefix.
* @task customfield
*/
private function getCustomKeyPrefix() {
return 'herald.custom/';
}
/**
* Determine if a field key is based on a custom field or a regular internal
* field.
*
* @param string Field key.
* @return bool True if the field key is based on a custom field.
* @task customfield
*/
private function isHeraldCustomKey($key) {
$prefix = $this->getCustomKeyPrefix();
return (strncmp($key, $prefix, strlen($prefix)) == 0);
}
/**
* Convert a custom field key into a Herald field key.
*
* @param string Custom field key.
* @return string Herald field key.
* @task customfield
*/
private function getHeraldKeyFromCustomKey($key) {
return $this->getCustomKeyPrefix().$key;
}
/**
* Get custom fields for this adapter, if appliable. This will either return
* a field list or `null` if the adapted object does not implement custom
* fields or the adapter does not support them.
*
* @return PhabricatorCustomFieldList|null List of fields, or `null`.
* @task customfield
*/
private function getCustomFields() {
if ($this->customFields === false) {
$this->customFields = null;
$template_object = $this->newObject();
if ($template_object instanceof PhabricatorCustomFieldInterface) {
$object = $this->getObject();
if (!$object) {
$object = $template_object;
}
$fields = PhabricatorCustomField::getObjectFields(
$object,
PhabricatorCustomField::ROLE_HERALD);
$fields->setViewer(PhabricatorUser::getOmnipotentUser());
$fields->readFieldsFromStorage($object);
$this->customFields = $fields;
}
}
return $this->customFields;
}
/**
* Get a custom field by Herald field key, or `null` if it does not exist
* or custom fields are not supported.
*
* @param string Herald field key.
* @return PhabricatorCustomField|null Matching field, if it exists.
* @task customfield
*/
private function getCustomField($herald_field_key) {
$fields = $this->getCustomFields();
if (!$fields) {
return null;
}
foreach ($fields->getFields() as $custom_field) {
$key = $custom_field->getFieldKey();
if ($this->getHeraldKeyFromCustomKey($key) == $herald_field_key) {
return $custom_field;
}
}
return null;
}
/**
* Get the field map for custom fields.
*
* @return map<string, string> Map of Herald field keys to field names.
* @task customfield
*/
private function getCustomFieldNameMap() {
$fields = $this->getCustomFields();
if (!$fields) {
return array();
}
$map = array();
foreach ($fields->getFields() as $field) {
$key = $field->getFieldKey();
$name = $field->getHeraldFieldName();
$map[$this->getHeraldKeyFromCustomKey($key)] = $name;
}
return $map;
}
/**
* Get the value for a custom field.
*
* @param string Herald field key.
* @return wild Custom field value.
* @task customfield
*/
private function getCustomFieldValue($field_key) {
$field = $this->getCustomField($field_key);
if (!$field) {
return null;
}
return $field->getHeraldFieldValue();
}
/**
* Get the Herald conditions for a custom field.
*
* @param string Herald field key.
* @return list<const> List of Herald conditions.
* @task customfield
*/
private function getCustomFieldConditions($field_key) {
$field = $this->getCustomField($field_key);
if (!$field) {
return array(
self::CONDITION_NEVER,
);
}
return $field->getHeraldFieldConditions();
}
/**
* Get the Herald value type for a custom field and condition.
*
* @param string Herald field key.
* @param const Herald condition constant.
* @return const|null Herald value type constant, or null to use the default.
* @task customfield
*/
private function getCustomFieldValueTypeForFieldAndCondition(
$field_key,
$condition) {
$field = $this->getCustomField($field_key);
if (!$field) {
return self::VALUE_NONE;
}
return $field->getHeraldFieldValueType($condition);
}
/* -( Applying Effects )--------------------------------------------------- */
/**
* @task apply
*/
protected function applyStandardEffect(HeraldEffect $effect) {
$action = $effect->getAction();
$rule_type = $effect->getRule()->getRuleType();
$supported = $this->getActions($rule_type);
$supported = array_fuse($supported);
if (empty($supported[$action])) {
throw new Exception(
pht(
'Adapter "%s" does not support action "%s" for rule type "%s".',
get_class($this),
$action,
$rule_type));
}
switch ($action) {
case self::ACTION_ADD_PROJECTS:
case self::ACTION_REMOVE_PROJECTS:
return $this->applyProjectsEffect($effect);
case self::ACTION_FLAG:
return $this->applyFlagEffect($effect);
case self::ACTION_EMAIL:
return $this->applyEmailEffect($effect);
default:
break;
}
$result = $this->handleCustomHeraldEffect($effect);
if (!$result) {
throw new Exception(
pht(
'No custom action exists to handle rule action "%s".',
$action));
}
return $result;
}
/**
* @task apply
*/
private function applyProjectsEffect(HeraldEffect $effect) {
if ($effect->getAction() == self::ACTION_ADD_PROJECTS) {
$kind = '+';
} else {
$kind = '-';
}
$project_type = PhabricatorProjectObjectHasProjectEdgeType::EDGECONST;
$project_phids = $effect->getTarget();
$xaction = $this->newTransaction()
->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
->setMetadataValue('edge:type', $project_type)
->setNewValue(
array(
$kind => array_fuse($project_phids),
));
$this->queueTransaction($xaction);
return new HeraldApplyTranscript(
$effect,
true,
pht('Added projects.'));
}
/**
* @task apply
*/
private function applyFlagEffect(HeraldEffect $effect) {
$phid = $this->getPHID();
$color = $effect->getTarget();
$rule = $effect->getRule();
$user = $rule->getAuthor();
$flag = PhabricatorFlagQuery::loadUserFlag($user, $phid);
if ($flag) {
return new HeraldApplyTranscript(
$effect,
false,
pht('Object already flagged.'));
}
$handle = id(new PhabricatorHandleQuery())
->setViewer($user)
->withPHIDs(array($phid))
->executeOne();
$flag = new PhabricatorFlag();
$flag->setOwnerPHID($user->getPHID());
$flag->setType($handle->getType());
$flag->setObjectPHID($handle->getPHID());
// TOOD: Should really be transcript PHID, but it doesn't exist yet.
$flag->setReasonPHID($user->getPHID());
$flag->setColor($color);
$flag->setNote(
pht('Flagged by Herald Rule "%s".', $rule->getName()));
$flag->save();
return new HeraldApplyTranscript(
$effect,
true,
pht('Added flag.'));
}
/**
* @task apply
*/
private function applyEmailEffect(HeraldEffect $effect) {
foreach ($effect->getTarget() as $phid) {
$this->emailPHIDs[$phid] = $phid;
// If this is a personal rule, we'll force delivery of a real email. This
// effect is stronger than notification preferences, so you get an actual
// email even if your preferences are set to "Notify" or "Ignore".
$rule = $effect->getRule();
if ($rule->isPersonalRule()) {
$this->forcedEmailPHIDs[$phid] = $phid;
}
}
return new HeraldApplyTranscript(
$effect,
true,
pht('Added mailable to mail targets.'));
}
}
diff --git a/src/applications/herald/adapter/HeraldCommitAdapter.php b/src/applications/herald/adapter/HeraldCommitAdapter.php
index eeb43c7a9..e26da09ab 100644
--- a/src/applications/herald/adapter/HeraldCommitAdapter.php
+++ b/src/applications/herald/adapter/HeraldCommitAdapter.php
@@ -1,541 +1,541 @@
<?php
final class HeraldCommitAdapter extends HeraldAdapter {
const FIELD_NEED_AUDIT_FOR_PACKAGE = 'need-audit-for-package';
const FIELD_REPOSITORY_AUTOCLOSE_BRANCH = 'repository-autoclose-branch';
protected $diff;
protected $revision;
protected $repository;
protected $commit;
protected $commitData;
private $commitDiff;
protected $addCCPHIDs = array();
protected $auditMap = array();
protected $buildPlans = array();
protected $affectedPaths;
protected $affectedRevision;
protected $affectedPackages;
protected $auditNeededPackages;
public function getAdapterApplicationClass() {
return 'PhabricatorDiffusionApplication';
}
protected function newObject() {
return new PhabricatorRepositoryCommit();
}
public function getObject() {
return $this->commit;
}
public function getAdapterContentType() {
return 'commit';
}
public function getAdapterContentName() {
return pht('Commits');
}
public function getAdapterContentDescription() {
return pht(
"React to new commits appearing in tracked repositories.\n".
"Commit rules can send email, flag commits, trigger audits, ".
"and run build plans.");
}
public function supportsRuleType($rule_type) {
switch ($rule_type) {
case HeraldRuleTypeConfig::RULE_TYPE_GLOBAL:
case HeraldRuleTypeConfig::RULE_TYPE_PERSONAL:
case HeraldRuleTypeConfig::RULE_TYPE_OBJECT:
return true;
default:
return false;
}
}
public function canTriggerOnObject($object) {
if ($object instanceof PhabricatorRepository) {
return true;
}
if ($object instanceof PhabricatorProject) {
return true;
}
return false;
}
public function getTriggerObjectPHIDs() {
return array_merge(
array(
$this->repository->getPHID(),
$this->getPHID(),
),
$this->repository->getProjectPHIDs());
}
public function explainValidTriggerObjects() {
return pht('This rule can trigger for **repositories** and **projects**.');
}
public function getFieldNameMap() {
return array(
self::FIELD_NEED_AUDIT_FOR_PACKAGE =>
pht('Affected packages that need audit'),
self::FIELD_REPOSITORY_AUTOCLOSE_BRANCH
=> pht('Commit is on closing branch'),
) + parent::getFieldNameMap();
}
public function getFields() {
return array_merge(
array(
self::FIELD_BODY,
self::FIELD_AUTHOR,
self::FIELD_COMMITTER,
self::FIELD_REVIEWER,
self::FIELD_REPOSITORY,
self::FIELD_REPOSITORY_PROJECTS,
self::FIELD_DIFF_FILE,
self::FIELD_DIFF_CONTENT,
self::FIELD_DIFF_ADDED_CONTENT,
self::FIELD_DIFF_REMOVED_CONTENT,
self::FIELD_DIFF_ENORMOUS,
self::FIELD_AFFECTED_PACKAGE,
self::FIELD_AFFECTED_PACKAGE_OWNER,
self::FIELD_NEED_AUDIT_FOR_PACKAGE,
self::FIELD_DIFFERENTIAL_REVISION,
self::FIELD_DIFFERENTIAL_ACCEPTED,
self::FIELD_DIFFERENTIAL_REVIEWERS,
self::FIELD_DIFFERENTIAL_CCS,
self::FIELD_BRANCHES,
self::FIELD_REPOSITORY_AUTOCLOSE_BRANCH,
),
parent::getFields());
}
public function getConditionsForField($field) {
switch ($field) {
case self::FIELD_NEED_AUDIT_FOR_PACKAGE:
return array(
self::CONDITION_INCLUDE_ANY,
self::CONDITION_INCLUDE_NONE,
);
case self::FIELD_REPOSITORY_AUTOCLOSE_BRANCH:
return array(
self::CONDITION_UNCONDITIONALLY,
);
}
return parent::getConditionsForField($field);
}
public function getActions($rule_type) {
switch ($rule_type) {
case HeraldRuleTypeConfig::RULE_TYPE_GLOBAL:
case HeraldRuleTypeConfig::RULE_TYPE_OBJECT:
return array_merge(
array(
self::ACTION_ADD_CC,
self::ACTION_EMAIL,
self::ACTION_AUDIT,
self::ACTION_APPLY_BUILD_PLANS,
self::ACTION_NOTHING,
),
parent::getActions($rule_type));
case HeraldRuleTypeConfig::RULE_TYPE_PERSONAL:
return array_merge(
array(
self::ACTION_ADD_CC,
self::ACTION_EMAIL,
self::ACTION_FLAG,
self::ACTION_AUDIT,
self::ACTION_NOTHING,
),
parent::getActions($rule_type));
}
}
public function getValueTypeForFieldAndCondition($field, $condition) {
switch ($field) {
case self::FIELD_DIFFERENTIAL_CCS:
return self::VALUE_EMAIL;
case self::FIELD_NEED_AUDIT_FOR_PACKAGE:
return self::VALUE_OWNERS_PACKAGE;
}
return parent::getValueTypeForFieldAndCondition($field, $condition);
}
public static function newLegacyAdapter(
PhabricatorRepository $repository,
PhabricatorRepositoryCommit $commit,
PhabricatorRepositoryCommitData $commit_data) {
$object = new HeraldCommitAdapter();
$commit->attachRepository($repository);
$object->repository = $repository;
$object->commit = $commit;
$object->commitData = $commit_data;
return $object;
}
public function setCommit(PhabricatorRepositoryCommit $commit) {
$viewer = PhabricatorUser::getOmnipotentUser();
$repository = id(new PhabricatorRepositoryQuery())
->setViewer($viewer)
->withIDs(array($commit->getRepositoryID()))
->needProjectPHIDs(true)
->executeOne();
if (!$repository) {
throw new Exception(pht('Unable to load repository!'));
}
$data = id(new PhabricatorRepositoryCommitData())->loadOneWhere(
'commitID = %d',
$commit->getID());
if (!$data) {
throw new Exception(pht('Unable to load commit data!'));
}
$this->commit = clone $commit;
$this->commit->attachRepository($repository);
$this->commit->attachCommitData($data);
$this->repository = $repository;
$this->commitData = $data;
return $this;
}
public function getPHID() {
return $this->commit->getPHID();
}
public function getAddCCMap() {
return $this->addCCPHIDs;
}
public function getAuditMap() {
return $this->auditMap;
}
public function getBuildPlans() {
return $this->buildPlans;
}
public function getHeraldName() {
return
'r'.
$this->repository->getCallsign().
$this->commit->getCommitIdentifier();
}
public function loadAffectedPaths() {
if ($this->affectedPaths === null) {
$result = PhabricatorOwnerPathQuery::loadAffectedPaths(
$this->repository,
$this->commit,
PhabricatorUser::getOmnipotentUser());
$this->affectedPaths = $result;
}
return $this->affectedPaths;
}
public function loadAffectedPackages() {
if ($this->affectedPackages === null) {
$packages = PhabricatorOwnersPackage::loadAffectedPackages(
$this->repository,
$this->loadAffectedPaths());
$this->affectedPackages = $packages;
}
return $this->affectedPackages;
}
public function loadAuditNeededPackage() {
if ($this->auditNeededPackages === null) {
$status_arr = array(
PhabricatorAuditStatusConstants::AUDIT_REQUIRED,
PhabricatorAuditStatusConstants::CONCERNED,
);
$requests = id(new PhabricatorRepositoryAuditRequest())
->loadAllWhere(
'commitPHID = %s AND auditStatus IN (%Ls)',
$this->commit->getPHID(),
$status_arr);
$packages = mpull($requests, 'getAuditorPHID');
$this->auditNeededPackages = $packages;
}
return $this->auditNeededPackages;
}
public function loadDifferentialRevision() {
if ($this->affectedRevision === null) {
$this->affectedRevision = false;
$data = $this->commitData;
$revision_id = $data->getCommitDetail('differential.revisionID');
if ($revision_id) {
// NOTE: The Herald rule owner might not actually have access to
// the revision, and can control which revision a commit is
// associated with by putting text in the commit message. However,
// the rules they can write against revisions don't actually expose
// anything interesting, so it seems reasonable to load unconditionally
// here.
$revision = id(new DifferentialRevisionQuery())
->withIDs(array($revision_id))
->setViewer(PhabricatorUser::getOmnipotentUser())
->needRelationships(true)
->needReviewerStatus(true)
->executeOne();
if ($revision) {
$this->affectedRevision = $revision;
}
}
}
return $this->affectedRevision;
}
public static function getEnormousByteLimit() {
return 1024 * 1024 * 1024; // 1GB
}
public static function getEnormousTimeLimit() {
return 60 * 15; // 15 Minutes
}
private function loadCommitDiff() {
$drequest = DiffusionRequest::newFromDictionary(
array(
'user' => PhabricatorUser::getOmnipotentUser(),
'repository' => $this->repository,
'commit' => $this->commit->getCommitIdentifier(),
));
$byte_limit = self::getEnormousByteLimit();
$raw = DiffusionQuery::callConduitWithDiffusionRequest(
PhabricatorUser::getOmnipotentUser(),
$drequest,
'diffusion.rawdiffquery',
array(
'commit' => $this->commit->getCommitIdentifier(),
'timeout' => self::getEnormousTimeLimit(),
'byteLimit' => $byte_limit,
'linesOfContext' => 0,
));
if (strlen($raw) >= $byte_limit) {
throw new Exception(
pht(
'The raw text of this change is enormous (larger than %d bytes). '.
'Herald can not process it.',
$byte_limit));
}
$parser = new ArcanistDiffParser();
$changes = $parser->parseDiff($raw);
$diff = DifferentialDiff::newEphemeralFromRawChanges(
$changes);
return $diff;
}
private function getDiffContent($type) {
if ($this->commitDiff === null) {
try {
$this->commitDiff = $this->loadCommitDiff();
} catch (Exception $ex) {
$this->commitDiff = $ex;
phlog($ex);
}
}
if ($this->commitDiff instanceof Exception) {
$ex = $this->commitDiff;
$ex_class = get_class($ex);
$ex_message = pht('Failed to load changes: %s', $ex->getMessage());
return array(
'<'.$ex_class.'>' => $ex_message,
);
}
$changes = $this->commitDiff->getChangesets();
$result = array();
foreach ($changes as $change) {
$lines = array();
foreach ($change->getHunks() as $hunk) {
switch ($type) {
case '-':
$lines[] = $hunk->makeOldFile();
break;
case '+':
$lines[] = $hunk->makeNewFile();
break;
case '*':
$lines[] = $hunk->makeChanges();
break;
default:
- throw new Exception("Unknown content selection '{$type}'!");
+ throw new Exception(pht("Unknown content selection '%s'!", $type));
}
}
$result[$change->getFilename()] = implode("\n", $lines);
}
return $result;
}
public function getHeraldField($field) {
$data = $this->commitData;
switch ($field) {
case self::FIELD_BODY:
return $data->getCommitMessage();
case self::FIELD_AUTHOR:
return $data->getCommitDetail('authorPHID');
case self::FIELD_COMMITTER:
return $data->getCommitDetail('committerPHID');
case self::FIELD_REVIEWER:
return $data->getCommitDetail('reviewerPHID');
case self::FIELD_DIFF_FILE:
return $this->loadAffectedPaths();
case self::FIELD_REPOSITORY:
return $this->repository->getPHID();
case self::FIELD_REPOSITORY_PROJECTS:
return $this->repository->getProjectPHIDs();
case self::FIELD_DIFF_CONTENT:
return $this->getDiffContent('*');
case self::FIELD_DIFF_ADDED_CONTENT:
return $this->getDiffContent('+');
case self::FIELD_DIFF_REMOVED_CONTENT:
return $this->getDiffContent('-');
case self::FIELD_DIFF_ENORMOUS:
$this->getDiffContent('*');
return ($this->commitDiff instanceof Exception);
case self::FIELD_AFFECTED_PACKAGE:
$packages = $this->loadAffectedPackages();
return mpull($packages, 'getPHID');
case self::FIELD_AFFECTED_PACKAGE_OWNER:
$packages = $this->loadAffectedPackages();
$owners = PhabricatorOwnersOwner::loadAllForPackages($packages);
return mpull($owners, 'getUserPHID');
case self::FIELD_NEED_AUDIT_FOR_PACKAGE:
return $this->loadAuditNeededPackage();
case self::FIELD_DIFFERENTIAL_REVISION:
$revision = $this->loadDifferentialRevision();
if (!$revision) {
return null;
}
return $revision->getID();
case self::FIELD_DIFFERENTIAL_ACCEPTED:
$revision = $this->loadDifferentialRevision();
if (!$revision) {
return null;
}
$status = $data->getCommitDetail(
'precommitRevisionStatus',
$revision->getStatus());
switch ($status) {
case ArcanistDifferentialRevisionStatus::ACCEPTED:
case ArcanistDifferentialRevisionStatus::CLOSED:
return $revision->getPHID();
}
return null;
case self::FIELD_DIFFERENTIAL_REVIEWERS:
$revision = $this->loadDifferentialRevision();
if (!$revision) {
return array();
}
return $revision->getReviewers();
case self::FIELD_DIFFERENTIAL_CCS:
$revision = $this->loadDifferentialRevision();
if (!$revision) {
return array();
}
return $revision->getCCPHIDs();
case self::FIELD_BRANCHES:
$params = array(
'callsign' => $this->repository->getCallsign(),
'contains' => $this->commit->getCommitIdentifier(),
);
$result = id(new ConduitCall('diffusion.branchquery', $params))
->setUser(PhabricatorUser::getOmnipotentUser())
->execute();
$refs = DiffusionRepositoryRef::loadAllFromDictionaries($result);
return mpull($refs, 'getShortName');
case self::FIELD_REPOSITORY_AUTOCLOSE_BRANCH:
return $this->repository->shouldAutocloseCommit($this->commit);
}
return parent::getHeraldField($field);
}
public function applyHeraldEffects(array $effects) {
assert_instances_of($effects, 'HeraldEffect');
$result = array();
foreach ($effects as $effect) {
$action = $effect->getAction();
switch ($action) {
case self::ACTION_NOTHING:
$result[] = new HeraldApplyTranscript(
$effect,
true,
pht('Great success at doing nothing.'));
break;
case self::ACTION_ADD_CC:
foreach ($effect->getTarget() as $phid) {
if (empty($this->addCCPHIDs[$phid])) {
$this->addCCPHIDs[$phid] = array();
}
$this->addCCPHIDs[$phid][] = $effect->getRule()->getID();
}
$result[] = new HeraldApplyTranscript(
$effect,
true,
pht('Added address to CC.'));
break;
case self::ACTION_AUDIT:
foreach ($effect->getTarget() as $phid) {
if (empty($this->auditMap[$phid])) {
$this->auditMap[$phid] = array();
}
$this->auditMap[$phid][] = $effect->getRule()->getID();
}
$result[] = new HeraldApplyTranscript(
$effect,
true,
pht('Triggered an audit.'));
break;
case self::ACTION_APPLY_BUILD_PLANS:
foreach ($effect->getTarget() as $phid) {
$this->buildPlans[] = $phid;
}
$result[] = new HeraldApplyTranscript(
$effect,
true,
pht('Applied build plans.'));
break;
default:
$result[] = $this->applyStandardEffect($effect);
break;
}
}
return $result;
}
}
diff --git a/src/applications/herald/adapter/HeraldDifferentialRevisionAdapter.php b/src/applications/herald/adapter/HeraldDifferentialRevisionAdapter.php
index 3168b29f6..ff1468663 100644
--- a/src/applications/herald/adapter/HeraldDifferentialRevisionAdapter.php
+++ b/src/applications/herald/adapter/HeraldDifferentialRevisionAdapter.php
@@ -1,418 +1,420 @@
<?php
final class HeraldDifferentialRevisionAdapter
extends HeraldDifferentialAdapter {
protected $revision;
protected $explicitCCs;
protected $explicitReviewers;
protected $forbiddenCCs;
protected $newCCs = array();
protected $remCCs = array();
protected $addReviewerPHIDs = array();
protected $blockingReviewerPHIDs = array();
protected $buildPlans = array();
protected $requiredSignatureDocumentPHIDs = array();
protected $affectedPackages;
protected $changesets;
private $haveHunks;
public function getAdapterApplicationClass() {
return 'PhabricatorDifferentialApplication';
}
protected function newObject() {
return new DifferentialRevision();
}
public function getObject() {
return $this->revision;
}
public function getDiff() {
return $this->diff;
}
public function getAdapterContentType() {
return 'differential';
}
public function getAdapterContentName() {
return pht('Differential Revisions');
}
public function getAdapterContentDescription() {
return pht(
"React to revisions being created or updated.\n".
"Revision rules can send email, flag revisions, add reviewers, ".
"and run build plans.");
}
public function supportsRuleType($rule_type) {
switch ($rule_type) {
case HeraldRuleTypeConfig::RULE_TYPE_GLOBAL:
case HeraldRuleTypeConfig::RULE_TYPE_PERSONAL:
return true;
case HeraldRuleTypeConfig::RULE_TYPE_OBJECT:
default:
return false;
}
}
public function getFields() {
return array_merge(
array(
self::FIELD_TITLE,
self::FIELD_BODY,
self::FIELD_AUTHOR,
self::FIELD_AUTHOR_PROJECTS,
self::FIELD_REVIEWERS,
self::FIELD_CC,
self::FIELD_REPOSITORY,
self::FIELD_REPOSITORY_PROJECTS,
self::FIELD_DIFF_FILE,
self::FIELD_DIFF_CONTENT,
self::FIELD_DIFF_ADDED_CONTENT,
self::FIELD_DIFF_REMOVED_CONTENT,
self::FIELD_AFFECTED_PACKAGE,
self::FIELD_AFFECTED_PACKAGE_OWNER,
self::FIELD_IS_NEW_OBJECT,
self::FIELD_ARCANIST_PROJECT,
),
parent::getFields());
}
public function getRepetitionOptions() {
return array(
HeraldRepetitionPolicyConfig::EVERY,
HeraldRepetitionPolicyConfig::FIRST,
);
}
public static function newLegacyAdapter(
DifferentialRevision $revision,
DifferentialDiff $diff) {
$object = new HeraldDifferentialRevisionAdapter();
// Reload the revision to pick up relationship information.
$revision = id(new DifferentialRevisionQuery())
->withIDs(array($revision->getID()))
->setViewer(PhabricatorUser::getOmnipotentUser())
->needRelationships(true)
->needReviewerStatus(true)
->executeOne();
$object->revision = $revision;
$object->diff = $diff;
return $object;
}
public function setExplicitCCs($explicit_ccs) {
$this->explicitCCs = $explicit_ccs;
return $this;
}
public function setExplicitReviewers($explicit_reviewers) {
$this->explicitReviewers = $explicit_reviewers;
return $this;
}
public function setForbiddenCCs($forbidden_ccs) {
$this->forbiddenCCs = $forbidden_ccs;
return $this;
}
public function getCCsAddedByHerald() {
return array_diff_key($this->newCCs, $this->remCCs);
}
public function getCCsRemovedByHerald() {
return $this->remCCs;
}
public function getReviewersAddedByHerald() {
return $this->addReviewerPHIDs;
}
public function getBlockingReviewersAddedByHerald() {
return $this->blockingReviewerPHIDs;
}
public function getRequiredSignatureDocumentPHIDs() {
return $this->requiredSignatureDocumentPHIDs;
}
public function getBuildPlans() {
return $this->buildPlans;
}
public function getPHID() {
return $this->revision->getPHID();
}
public function getHeraldName() {
return $this->revision->getTitle();
}
protected function loadChangesets() {
if ($this->changesets === null) {
$this->changesets = $this->diff->loadChangesets();
}
return $this->changesets;
}
protected function loadChangesetsWithHunks() {
$changesets = $this->loadChangesets();
if ($changesets && !$this->haveHunks) {
$this->haveHunks = true;
id(new DifferentialHunkQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withChangesets($changesets)
->needAttachToChangesets(true)
->execute();
}
return $changesets;
}
public function loadAffectedPackages() {
if ($this->affectedPackages === null) {
$this->affectedPackages = array();
$repository = $this->loadRepository();
if ($repository) {
$packages = PhabricatorOwnersPackage::loadAffectedPackages(
$repository,
$this->loadAffectedPaths());
$this->affectedPackages = $packages;
}
}
return $this->affectedPackages;
}
public function getHeraldField($field) {
switch ($field) {
case self::FIELD_TITLE:
return $this->revision->getTitle();
break;
case self::FIELD_BODY:
return $this->revision->getSummary()."\n".
$this->revision->getTestPlan();
break;
case self::FIELD_AUTHOR:
return $this->revision->getAuthorPHID();
break;
case self::FIELD_AUTHOR_PROJECTS:
$author_phid = $this->revision->getAuthorPHID();
if (!$author_phid) {
return array();
}
$projects = id(new PhabricatorProjectQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withMemberPHIDs(array($author_phid))
->execute();
return mpull($projects, 'getPHID');
case self::FIELD_DIFF_FILE:
return $this->loadAffectedPaths();
case self::FIELD_CC:
if (isset($this->explicitCCs)) {
return array_keys($this->explicitCCs);
} else {
return $this->revision->getCCPHIDs();
}
case self::FIELD_REVIEWERS:
if (isset($this->explicitReviewers)) {
return array_keys($this->explicitReviewers);
} else {
return $this->revision->getReviewers();
}
case self::FIELD_REPOSITORY:
$repository = $this->loadRepository();
if (!$repository) {
return null;
}
return $repository->getPHID();
case self::FIELD_REPOSITORY_PROJECTS:
$repository = $this->loadRepository();
if (!$repository) {
return array();
}
return $repository->getProjectPHIDs();
case self::FIELD_DIFF_CONTENT:
return $this->loadContentDictionary();
case self::FIELD_DIFF_ADDED_CONTENT:
return $this->loadAddedContentDictionary();
case self::FIELD_DIFF_REMOVED_CONTENT:
return $this->loadRemovedContentDictionary();
case self::FIELD_AFFECTED_PACKAGE:
$packages = $this->loadAffectedPackages();
return mpull($packages, 'getPHID');
case self::FIELD_AFFECTED_PACKAGE_OWNER:
$packages = $this->loadAffectedPackages();
return PhabricatorOwnersOwner::loadAffiliatedUserPHIDs(
mpull($packages, 'getID'));
case self::FIELD_ARCANIST_PROJECT:
return $this->revision->getArcanistProjectPHID();
}
return parent::getHeraldField($field);
}
public function getActions($rule_type) {
switch ($rule_type) {
case HeraldRuleTypeConfig::RULE_TYPE_GLOBAL:
return array_merge(
array(
self::ACTION_ADD_CC,
self::ACTION_REMOVE_CC,
self::ACTION_EMAIL,
self::ACTION_ADD_REVIEWERS,
self::ACTION_ADD_BLOCKING_REVIEWERS,
self::ACTION_APPLY_BUILD_PLANS,
self::ACTION_REQUIRE_SIGNATURE,
self::ACTION_NOTHING,
),
parent::getActions($rule_type));
case HeraldRuleTypeConfig::RULE_TYPE_PERSONAL:
return array_merge(
array(
self::ACTION_ADD_CC,
self::ACTION_REMOVE_CC,
self::ACTION_EMAIL,
self::ACTION_FLAG,
self::ACTION_ADD_REVIEWERS,
self::ACTION_ADD_BLOCKING_REVIEWERS,
self::ACTION_NOTHING,
),
parent::getActions($rule_type));
}
}
public function applyHeraldEffects(array $effects) {
assert_instances_of($effects, 'HeraldEffect');
$result = array();
if ($this->explicitCCs) {
$effect = new HeraldEffect();
$effect->setAction(self::ACTION_ADD_CC);
$effect->setTarget(array_keys($this->explicitCCs));
$effect->setReason(
- pht('CCs provided explicitly by revision author or carried over '.
+ pht(
+ 'CCs provided explicitly by revision author or carried over '.
'from a previous version of the revision.'));
$result[] = new HeraldApplyTranscript(
$effect,
true,
pht('Added addresses to CC list.'));
}
$forbidden_ccs = array_fill_keys(
nonempty($this->forbiddenCCs, array()),
true);
foreach ($effects as $effect) {
$action = $effect->getAction();
switch ($action) {
case self::ACTION_NOTHING:
$result[] = new HeraldApplyTranscript(
$effect,
true,
pht('OK, did nothing.'));
break;
case self::ACTION_ADD_CC:
$base_target = $effect->getTarget();
$forbidden = array();
foreach ($base_target as $key => $fbid) {
if (isset($forbidden_ccs[$fbid])) {
$forbidden[] = $fbid;
unset($base_target[$key]);
} else {
$this->newCCs[$fbid] = true;
}
}
if ($forbidden) {
$failed = clone $effect;
$failed->setTarget($forbidden);
if ($base_target) {
$effect->setTarget($base_target);
$result[] = new HeraldApplyTranscript(
$effect,
true,
- pht('Added these addresses to CC list. '.
- 'Others could not be added.'));
+ pht(
+ 'Added these addresses to CC list. '.
+ 'Others could not be added.'));
}
$result[] = new HeraldApplyTranscript(
$failed,
false,
pht('CC forbidden, these addresses have unsubscribed.'));
} else {
$result[] = new HeraldApplyTranscript(
$effect,
true,
pht('Added addresses to CC list.'));
}
break;
case self::ACTION_REMOVE_CC:
foreach ($effect->getTarget() as $fbid) {
$this->remCCs[$fbid] = true;
}
$result[] = new HeraldApplyTranscript(
$effect,
true,
pht('Removed addresses from CC list.'));
break;
case self::ACTION_ADD_REVIEWERS:
foreach ($effect->getTarget() as $phid) {
$this->addReviewerPHIDs[$phid] = true;
}
$result[] = new HeraldApplyTranscript(
$effect,
true,
pht('Added reviewers.'));
break;
case self::ACTION_ADD_BLOCKING_REVIEWERS:
// This adds reviewers normally, it just also marks them blocking.
foreach ($effect->getTarget() as $phid) {
$this->addReviewerPHIDs[$phid] = true;
$this->blockingReviewerPHIDs[$phid] = true;
}
$result[] = new HeraldApplyTranscript(
$effect,
true,
pht('Added blocking reviewers.'));
break;
case self::ACTION_APPLY_BUILD_PLANS:
foreach ($effect->getTarget() as $phid) {
$this->buildPlans[] = $phid;
}
$result[] = new HeraldApplyTranscript(
$effect,
true,
pht('Applied build plans.'));
break;
case self::ACTION_REQUIRE_SIGNATURE:
foreach ($effect->getTarget() as $phid) {
$this->requiredSignatureDocumentPHIDs[] = $phid;
}
$result[] = new HeraldApplyTranscript(
$effect,
true,
pht('Required signatures.'));
break;
default:
$result[] = $this->applyStandardEffect($effect);
break;
}
}
return $result;
}
}
diff --git a/src/applications/herald/controller/HeraldRuleController.php b/src/applications/herald/controller/HeraldRuleController.php
index c29040709..f52a849d1 100644
--- a/src/applications/herald/controller/HeraldRuleController.php
+++ b/src/applications/herald/controller/HeraldRuleController.php
@@ -1,683 +1,683 @@
<?php
final class HeraldRuleController extends HeraldController {
private $id;
private $filter;
public function willProcessRequest(array $data) {
$this->id = (int)idx($data, 'id');
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$content_type_map = HeraldAdapter::getEnabledAdapterMap($user);
$rule_type_map = HeraldRuleTypeConfig::getRuleTypeMap();
if ($this->id) {
$id = $this->id;
$rule = id(new HeraldRuleQuery())
->setViewer($user)
->withIDs(array($id))
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
if (!$rule) {
return new Aphront404Response();
}
$cancel_uri = $this->getApplicationURI("rule/{$id}/");
} else {
$rule = new HeraldRule();
$rule->setAuthorPHID($user->getPHID());
$rule->setMustMatchAll(1);
$content_type = $request->getStr('content_type');
$rule->setContentType($content_type);
$rule_type = $request->getStr('rule_type');
if (!isset($rule_type_map[$rule_type])) {
$rule_type = HeraldRuleTypeConfig::RULE_TYPE_PERSONAL;
}
$rule->setRuleType($rule_type);
$adapter = HeraldAdapter::getAdapterForContentType(
$rule->getContentType());
if (!$adapter->supportsRuleType($rule->getRuleType())) {
throw new Exception(
pht(
"This rule's content type does not support the selected rule ".
"type."));
}
if ($rule->isObjectRule()) {
$rule->setTriggerObjectPHID($request->getStr('targetPHID'));
$object = id(new PhabricatorObjectQuery())
->setViewer($user)
->withPHIDs(array($rule->getTriggerObjectPHID()))
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
if (!$object) {
throw new Exception(
pht('No valid object provided for object rule!'));
}
if (!$adapter->canTriggerOnObject($object)) {
throw new Exception(
pht('Object is of wrong type for adapter!'));
}
}
$cancel_uri = $this->getApplicationURI();
}
if ($rule->isGlobalRule()) {
$this->requireApplicationCapability(
HeraldManageGlobalRulesCapability::CAPABILITY);
}
$adapter = HeraldAdapter::getAdapterForContentType($rule->getContentType());
$local_version = id(new HeraldRule())->getConfigVersion();
if ($rule->getConfigVersion() > $local_version) {
throw new Exception(
pht(
'This rule was created with a newer version of Herald. You can not '.
'view or edit it in this older version. Upgrade your Phabricator '.
'deployment.'));
}
// Upgrade rule version to our version, since we might add newly-defined
// conditions, etc.
$rule->setConfigVersion($local_version);
$rule_conditions = $rule->loadConditions();
$rule_actions = $rule->loadActions();
$rule->attachConditions($rule_conditions);
$rule->attachActions($rule_actions);
$e_name = true;
$errors = array();
if ($request->isFormPost() && $request->getStr('save')) {
list($e_name, $errors) = $this->saveRule($adapter, $rule, $request);
if (!$errors) {
$id = $rule->getID();
$uri = $this->getApplicationURI("rule/{$id}/");
return id(new AphrontRedirectResponse())->setURI($uri);
}
}
$must_match_selector = $this->renderMustMatchSelector($rule);
$repetition_selector = $this->renderRepetitionSelector($rule, $adapter);
$handles = $this->loadHandlesForRule($rule);
require_celerity_resource('herald-css');
$content_type_name = $content_type_map[$rule->getContentType()];
$rule_type_name = $rule_type_map[$rule->getRuleType()];
$form = id(new AphrontFormView())
->setUser($user)
->setID('herald-rule-edit-form')
->addHiddenInput('content_type', $rule->getContentType())
->addHiddenInput('rule_type', $rule->getRuleType())
->addHiddenInput('save', 1)
->appendChild(
// Build this explicitly (instead of using addHiddenInput())
// so we can add a sigil to it.
javelin_tag(
'input',
array(
'type' => 'hidden',
'name' => 'rule',
'sigil' => 'rule',
)))
->appendChild(
id(new AphrontFormTextControl())
->setLabel(pht('Rule Name'))
->setName('name')
->setError($e_name)
->setValue($rule->getName()));
$trigger_object_control = false;
if ($rule->isObjectRule()) {
$trigger_object_control = id(new AphrontFormStaticControl())
->setValue(
pht(
'This rule triggers for %s.',
$handles[$rule->getTriggerObjectPHID()]->renderLink()));
}
$form
->appendChild(
id(new AphrontFormMarkupControl())
->setValue(pht(
'This %s rule triggers for %s.',
phutil_tag('strong', array(), $rule_type_name),
phutil_tag('strong', array(), $content_type_name))))
->appendChild($trigger_object_control)
->appendChild(
id(new PHUIFormInsetView())
->setTitle(pht('Conditions'))
->setRightButton(javelin_tag(
'a',
array(
'href' => '#',
'class' => 'button green',
'sigil' => 'create-condition',
'mustcapture' => true,
),
pht('New Condition')))
->setDescription(
pht('When %s these conditions are met:', $must_match_selector))
->setContent(javelin_tag(
'table',
array(
'sigil' => 'rule-conditions',
'class' => 'herald-condition-table',
),
'')))
->appendChild(
id(new PHUIFormInsetView())
->setTitle(pht('Action'))
->setRightButton(javelin_tag(
'a',
array(
'href' => '#',
'class' => 'button green',
'sigil' => 'create-action',
'mustcapture' => true,
),
pht('New Action')))
->setDescription(pht(
'Take these actions %s this rule matches:',
$repetition_selector))
->setContent(javelin_tag(
'table',
array(
'sigil' => 'rule-actions',
'class' => 'herald-action-table',
),
'')))
->appendChild(
id(new AphrontFormSubmitControl())
->setValue(pht('Save Rule'))
->addCancelButton($cancel_uri));
$this->setupEditorBehavior($rule, $handles, $adapter);
$title = $rule->getID()
? pht('Edit Herald Rule')
: pht('Create Herald Rule');
$form_box = id(new PHUIObjectBoxView())
->setHeaderText($title)
->setFormErrors($errors)
->setForm($form);
$crumbs = $this
->buildApplicationCrumbs()
->addTextCrumb($title);
return $this->buildApplicationPage(
array(
$crumbs,
$form_box,
),
array(
'title' => pht('Edit Rule'),
));
}
private function saveRule(HeraldAdapter $adapter, $rule, $request) {
$rule->setName($request->getStr('name'));
$match_all = ($request->getStr('must_match') == 'all');
$rule->setMustMatchAll((int)$match_all);
$repetition_policy_param = $request->getStr('repetition_policy');
$rule->setRepetitionPolicy(
HeraldRepetitionPolicyConfig::toInt($repetition_policy_param));
$e_name = true;
$errors = array();
if (!strlen($rule->getName())) {
$e_name = pht('Required');
$errors[] = pht('Rule must have a name.');
}
$data = null;
try {
$data = phutil_json_decode($request->getStr('rule'));
} catch (PhutilJSONParserException $ex) {
throw new PhutilProxyException(
pht('Failed to decode rule data.'),
$ex);
}
if (!is_array($data) ||
!$data['conditions'] ||
!$data['actions']) {
- throw new Exception('Failed to decode rule data.');
+ throw new Exception(pht('Failed to decode rule data.'));
}
$conditions = array();
foreach ($data['conditions'] as $condition) {
if ($condition === null) {
// We manage this as a sparse array on the client, so may receive
// NULL if conditions have been removed.
continue;
}
$obj = new HeraldCondition();
$obj->setFieldName($condition[0]);
$obj->setFieldCondition($condition[1]);
if (is_array($condition[2])) {
$obj->setValue(array_keys($condition[2]));
} else {
$obj->setValue($condition[2]);
}
try {
$adapter->willSaveCondition($obj);
} catch (HeraldInvalidConditionException $ex) {
$errors[] = $ex->getMessage();
}
$conditions[] = $obj;
}
$actions = array();
foreach ($data['actions'] as $action) {
if ($action === null) {
// Sparse on the client; removals can give us NULLs.
continue;
}
if (!isset($action[1])) {
// Legitimate for any action which doesn't need a target, like
// "Do nothing".
$action[1] = null;
}
$obj = new HeraldAction();
$obj->setAction($action[0]);
$obj->setTarget($action[1]);
try {
$adapter->willSaveAction($rule, $obj);
} catch (HeraldInvalidActionException $ex) {
$errors[] = $ex;
}
$actions[] = $obj;
}
$rule->attachConditions($conditions);
$rule->attachActions($actions);
if (!$errors) {
$edit_action = $rule->getID() ? 'edit' : 'create';
$rule->openTransaction();
$rule->save();
$rule->saveConditions($conditions);
$rule->saveActions($actions);
$rule->saveTransaction();
}
return array($e_name, $errors);
}
private function setupEditorBehavior(
HeraldRule $rule,
array $handles,
HeraldAdapter $adapter) {
$serial_conditions = array(
array('default', 'default', ''),
);
if ($rule->getConditions()) {
$serial_conditions = array();
foreach ($rule->getConditions() as $condition) {
$value = $condition->getValue();
switch ($condition->getFieldName()) {
case HeraldAdapter::FIELD_TASK_PRIORITY:
$value_map = array();
$priority_map = ManiphestTaskPriority::getTaskPriorityMap();
foreach ($value as $priority) {
$value_map[$priority] = idx($priority_map, $priority);
}
$value = $value_map;
break;
case HeraldAdapter::FIELD_TASK_STATUS:
$value_map = array();
$status_map = ManiphestTaskStatus::getTaskStatusMap();
foreach ($value as $status) {
$value_map[$status] = idx($status_map, $status);
}
$value = $value_map;
break;
default:
if (is_array($value)) {
$value_map = array();
foreach ($value as $k => $fbid) {
$value_map[$fbid] = $handles[$fbid]->getName();
}
$value = $value_map;
}
break;
}
$serial_conditions[] = array(
$condition->getFieldName(),
$condition->getFieldCondition(),
$value,
);
}
}
$serial_actions = array(
array('default', ''),
);
if ($rule->getActions()) {
$serial_actions = array();
foreach ($rule->getActions() as $action) {
switch ($action->getAction()) {
case HeraldAdapter::ACTION_FLAG:
case HeraldAdapter::ACTION_BLOCK:
$current_value = $action->getTarget();
break;
default:
if (is_array($action->getTarget())) {
$target_map = array();
foreach ((array)$action->getTarget() as $fbid) {
$target_map[$fbid] = $handles[$fbid]->getName();
}
$current_value = $target_map;
} else {
$current_value = $action->getTarget();
}
break;
}
$serial_actions[] = array(
$action->getAction(),
$current_value,
);
}
}
$all_rules = $this->loadRulesThisRuleMayDependUpon($rule);
$all_rules = mpull($all_rules, 'getName', 'getPHID');
asort($all_rules);
$all_fields = $adapter->getFieldNameMap();
$all_conditions = $adapter->getConditionNameMap();
$all_actions = $adapter->getActionNameMap($rule->getRuleType());
$fields = $adapter->getFields();
$field_map = array_select_keys($all_fields, $fields);
// Populate any fields which exist in the rule but which we don't know the
// names of, so that saving a rule without touching anything doesn't change
// it.
foreach ($rule->getConditions() as $condition) {
if (empty($field_map[$condition->getFieldName()])) {
$field_map[$condition->getFieldName()] = pht('<Unknown Field>');
}
}
$actions = $adapter->getActions($rule->getRuleType());
$action_map = array_select_keys($all_actions, $actions);
$config_info = array();
$config_info['fields'] = $field_map;
$config_info['conditions'] = $all_conditions;
$config_info['actions'] = $action_map;
foreach ($config_info['fields'] as $field => $name) {
$field_conditions = $adapter->getConditionsForField($field);
$config_info['conditionMap'][$field] = $field_conditions;
}
foreach ($config_info['fields'] as $field => $fname) {
foreach ($config_info['conditionMap'][$field] as $condition) {
$value_type = $adapter->getValueTypeForFieldAndCondition(
$field,
$condition);
$config_info['values'][$field][$condition] = $value_type;
}
}
$config_info['rule_type'] = $rule->getRuleType();
foreach ($config_info['actions'] as $action => $name) {
$config_info['targets'][$action] = $adapter->getValueTypeForAction(
$action,
$rule->getRuleType());
}
$changeflag_options =
PhabricatorRepositoryPushLog::getHeraldChangeFlagConditionOptions();
Javelin::initBehavior(
'herald-rule-editor',
array(
'root' => 'herald-rule-edit-form',
'conditions' => (object)$serial_conditions,
'actions' => (object)$serial_actions,
'select' => array(
HeraldAdapter::VALUE_CONTENT_SOURCE => array(
'options' => PhabricatorContentSource::getSourceNameMap(),
'default' => PhabricatorContentSource::SOURCE_WEB,
),
HeraldAdapter::VALUE_FLAG_COLOR => array(
'options' => PhabricatorFlagColor::getColorNameMap(),
'default' => PhabricatorFlagColor::COLOR_BLUE,
),
HeraldPreCommitRefAdapter::VALUE_REF_TYPE => array(
'options' => array(
PhabricatorRepositoryPushLog::REFTYPE_BRANCH
=> pht('branch (git/hg)'),
PhabricatorRepositoryPushLog::REFTYPE_TAG
=> pht('tag (git)'),
PhabricatorRepositoryPushLog::REFTYPE_BOOKMARK
=> pht('bookmark (hg)'),
),
'default' => PhabricatorRepositoryPushLog::REFTYPE_BRANCH,
),
HeraldPreCommitRefAdapter::VALUE_REF_CHANGE => array(
'options' => $changeflag_options,
'default' => PhabricatorRepositoryPushLog::CHANGEFLAG_ADD,
),
),
'template' => $this->buildTokenizerTemplates($handles) + array(
'rules' => $all_rules,
),
'author' => array(
$rule->getAuthorPHID() =>
$handles[$rule->getAuthorPHID()]->getName(),
),
'info' => $config_info,
));
}
private function loadHandlesForRule($rule) {
$phids = array();
foreach ($rule->getActions() as $action) {
if (!is_array($action->getTarget())) {
continue;
}
foreach ($action->getTarget() as $target) {
$target = (array)$target;
foreach ($target as $phid) {
$phids[] = $phid;
}
}
}
foreach ($rule->getConditions() as $condition) {
$value = $condition->getValue();
if (is_array($value)) {
foreach ($value as $phid) {
$phids[] = $phid;
}
}
}
$phids[] = $rule->getAuthorPHID();
if ($rule->isObjectRule()) {
$phids[] = $rule->getTriggerObjectPHID();
}
return $this->loadViewerHandles($phids);
}
/**
* Render the selector for the "When (all of | any of) these conditions are
* met:" element.
*/
private function renderMustMatchSelector($rule) {
return AphrontFormSelectControl::renderSelectTag(
$rule->getMustMatchAll() ? 'all' : 'any',
array(
'all' => pht('all of'),
'any' => pht('any of'),
),
array(
'name' => 'must_match',
));
}
/**
* Render the selector for "Take these actions (every time | only the first
* time) this rule matches..." element.
*/
private function renderRepetitionSelector($rule, HeraldAdapter $adapter) {
$repetition_policy = HeraldRepetitionPolicyConfig::toString(
$rule->getRepetitionPolicy());
$repetition_options = $adapter->getRepetitionOptions();
$repetition_names = HeraldRepetitionPolicyConfig::getMap();
$repetition_map = array_select_keys($repetition_names, $repetition_options);
if (count($repetition_map) < 2) {
return head($repetition_names);
} else {
return AphrontFormSelectControl::renderSelectTag(
$repetition_policy,
$repetition_map,
array(
'name' => 'repetition_policy',
));
}
}
protected function buildTokenizerTemplates(array $handles) {
$template = new AphrontTokenizerTemplateView();
$template = $template->render();
$sources = array(
'repository' => new DiffusionRepositoryDatasource(),
'legaldocuments' => new LegalpadDocumentDatasource(),
'taskpriority' => new ManiphestTaskPriorityDatasource(),
'taskstatus' => new ManiphestTaskStatusDatasource(),
'buildplan' => new HarbormasterBuildPlanDatasource(),
'arcanistprojects' => new DiffusionArcanistProjectDatasource(),
'package' => new PhabricatorOwnersPackageDatasource(),
'project' => new PhabricatorProjectDatasource(),
'user' => new PhabricatorPeopleDatasource(),
'email' => new PhabricatorMetaMTAMailableDatasource(),
'userorproject' => new PhabricatorProjectOrUserDatasource(),
'applicationemail' => new PhabricatorMetaMTAApplicationEmailDatasource(),
);
foreach ($sources as $key => $source) {
$source->setViewer($this->getViewer());
$sources[$key] = array(
'uri' => $source->getDatasourceURI(),
'placeholder' => $source->getPlaceholderText(),
'browseURI' => $source->getBrowseURI(),
);
}
return array(
'source' => $sources,
'username' => $this->getRequest()->getUser()->getUserName(),
'icons' => mpull($handles, 'getTypeIcon', 'getPHID'),
'markup' => $template,
);
}
/**
* Load rules for the "Another Herald rule..." condition dropdown, which
* allows one rule to depend upon the success or failure of another rule.
*/
private function loadRulesThisRuleMayDependUpon(HeraldRule $rule) {
$viewer = $this->getRequest()->getUser();
// Any rule can depend on a global rule.
$all_rules = id(new HeraldRuleQuery())
->setViewer($viewer)
->withRuleTypes(array(HeraldRuleTypeConfig::RULE_TYPE_GLOBAL))
->withContentTypes(array($rule->getContentType()))
->execute();
if ($rule->isObjectRule()) {
// Object rules may depend on other rules for the same object.
$all_rules += id(new HeraldRuleQuery())
->setViewer($viewer)
->withRuleTypes(array(HeraldRuleTypeConfig::RULE_TYPE_OBJECT))
->withContentTypes(array($rule->getContentType()))
->withTriggerObjectPHIDs(array($rule->getTriggerObjectPHID()))
->execute();
}
if ($rule->isPersonalRule()) {
// Personal rules may depend upon your other personal rules.
$all_rules += id(new HeraldRuleQuery())
->setViewer($viewer)
->withRuleTypes(array(HeraldRuleTypeConfig::RULE_TYPE_PERSONAL))
->withContentTypes(array($rule->getContentType()))
->withAuthorPHIDs(array($rule->getAuthorPHID()))
->execute();
}
// mark disabled rules as disabled since they are not useful as such;
// don't filter though to keep edit cases sane / expected
foreach ($all_rules as $current_rule) {
if ($current_rule->getIsDisabled()) {
$current_rule->makeEphemeral();
$current_rule->setName($rule->getName().' '.pht('(Disabled)'));
}
}
// A rule can not depend upon itself.
unset($all_rules[$rule->getID()]);
return $all_rules;
}
}
diff --git a/src/applications/herald/controller/HeraldTestConsoleController.php b/src/applications/herald/controller/HeraldTestConsoleController.php
index 0af51cf67..7cd610f35 100644
--- a/src/applications/herald/controller/HeraldTestConsoleController.php
+++ b/src/applications/herald/controller/HeraldTestConsoleController.php
@@ -1,119 +1,119 @@
<?php
final class HeraldTestConsoleController extends HeraldController {
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$request = $this->getRequest();
$object_name = trim($request->getStr('object_name'));
$e_name = true;
$errors = array();
if ($request->isFormPost()) {
if (!$object_name) {
$e_name = pht('Required');
$errors[] = pht('An object name is required.');
}
if (!$errors) {
$object = id(new PhabricatorObjectQuery())
->setViewer($user)
->withNames(array($object_name))
->executeOne();
if (!$object) {
$e_name = pht('Invalid');
$errors[] = pht('No object exists with that name.');
}
if (!$errors) {
// TODO: Let the adapters claim objects instead.
if ($object instanceof DifferentialRevision) {
$adapter = HeraldDifferentialRevisionAdapter::newLegacyAdapter(
$object,
$object->loadActiveDiff());
} else if ($object instanceof PhabricatorRepositoryCommit) {
$adapter = id(new HeraldCommitAdapter())
->setCommit($object);
} else if ($object instanceof ManiphestTask) {
$adapter = id(new HeraldManiphestTaskAdapter())
->setTask($object);
} else if ($object instanceof PholioMock) {
$adapter = id(new HeraldPholioMockAdapter())
->setMock($object);
} else if ($object instanceof PhrictionDocument) {
$adapter = id(new PhrictionDocumentHeraldAdapter())
->setDocument($object);
} else {
- throw new Exception('Can not build adapter for object!');
+ throw new Exception(pht('Can not build adapter for object!'));
}
$adapter->setIsNewObject(false);
$rules = id(new HeraldRuleQuery())
->setViewer($user)
->withContentTypes(array($adapter->getAdapterContentType()))
->withDisabled(false)
->needConditionsAndActions(true)
->needAppliedToPHIDs(array($object->getPHID()))
->needValidateAuthors(true)
->execute();
$engine = id(new HeraldEngine())
->setDryRun(true);
$effects = $engine->applyRules($rules, $adapter);
$engine->applyEffects($effects, $adapter, $rules);
$xscript = $engine->getTranscript();
return id(new AphrontRedirectResponse())
->setURI('/herald/transcript/'.$xscript->getID().'/');
}
}
}
$form = id(new AphrontFormView())
->setUser($user)
->appendRemarkupInstructions(
pht(
'Enter an object to test rules for, like a Diffusion commit (e.g., '.
'`rX123`) or a Differential revision (e.g., `D123`). You will be '.
'shown the results of a dry run on the object.'))
->appendChild(
id(new AphrontFormTextControl())
->setLabel(pht('Object Name'))
->setName('object_name')
->setError($e_name)
->setValue($object_name))
->appendChild(
id(new AphrontFormSubmitControl())
->setValue(pht('Test Rules')));
$box = id(new PHUIObjectBoxView())
->setHeaderText(pht('Herald Test Console'))
->setFormErrors($errors)
->setForm($form);
$nav = $this->buildSideNavView();
$nav->selectFilter('test');
$nav->appendChild($box);
$crumbs = id($this->buildApplicationCrumbs())
->addTextCrumb(pht('Test Console'));
$nav->setCrumbs($crumbs);
return $this->buildApplicationPage(
$nav,
array(
'title' => pht('Test Console'),
));
}
}
diff --git a/src/applications/herald/controller/HeraldTranscriptController.php b/src/applications/herald/controller/HeraldTranscriptController.php
index d1094437d..f20d620cc 100644
--- a/src/applications/herald/controller/HeraldTranscriptController.php
+++ b/src/applications/herald/controller/HeraldTranscriptController.php
@@ -1,552 +1,555 @@
<?php
final class HeraldTranscriptController extends HeraldController {
const FILTER_AFFECTED = 'affected';
const FILTER_OWNED = 'owned';
const FILTER_ALL = 'all';
private $id;
private $filter;
private $handles;
private $adapter;
public function willProcessRequest(array $data) {
$this->id = $data['id'];
$map = $this->getFilterMap();
$this->filter = idx($data, 'filter');
if (empty($map[$this->filter])) {
$this->filter = self::FILTER_ALL;
}
}
private function getAdapter() {
return $this->adapter;
}
public function processRequest() {
$request = $this->getRequest();
$viewer = $request->getUser();
$xscript = id(new HeraldTranscriptQuery())
->setViewer($viewer)
->withIDs(array($this->id))
->executeOne();
if (!$xscript) {
return new Aphront404Response();
}
require_celerity_resource('herald-test-css');
$nav = $this->buildSideNav();
$object_xscript = $xscript->getObjectTranscript();
if (!$object_xscript) {
$notice = id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_NOTICE)
->setTitle(pht('Old Transcript'))
->appendChild(phutil_tag(
'p',
array(),
pht('Details of this transcript have been garbage collected.')));
$nav->appendChild($notice);
} else {
$map = HeraldAdapter::getEnabledAdapterMap($viewer);
$object_type = $object_xscript->getType();
if (empty($map[$object_type])) {
// TODO: We should filter these out in the Query, but we have to load
// the objectTranscript right now, which is potentially enormous. We
// should denormalize the object type, or move the data into a separate
// table, and then filter this earlier (and thus raise a better error).
// For now, just block access so we don't violate policies.
throw new Exception(
pht('This transcript has an invalid or inaccessible adapter.'));
}
$this->adapter = HeraldAdapter::getAdapterForContentType($object_type);
$filter = $this->getFilterPHIDs();
$this->filterTranscript($xscript, $filter);
$phids = array_merge($filter, $this->getTranscriptPHIDs($xscript));
$phids = array_unique($phids);
$phids = array_filter($phids);
$handles = $this->loadViewerHandles($phids);
$this->handles = $handles;
if ($xscript->getDryRun()) {
$notice = new PHUIInfoView();
$notice->setSeverity(PHUIInfoView::SEVERITY_NOTICE);
$notice->setTitle(pht('Dry Run'));
- $notice->appendChild(pht('This was a dry run to test Herald '.
- 'rules, no actions were executed.'));
+ $notice->appendChild(
+ pht(
+ 'This was a dry run to test Herald rules, '.
+ 'no actions were executed.'));
$nav->appendChild($notice);
}
$warning_panel = $this->buildWarningPanel($xscript);
$nav->appendChild($warning_panel);
$apply_xscript_panel = $this->buildApplyTranscriptPanel(
$xscript);
$nav->appendChild($apply_xscript_panel);
$action_xscript_panel = $this->buildActionTranscriptPanel(
$xscript);
$nav->appendChild($action_xscript_panel);
$object_xscript_panel = $this->buildObjectTranscriptPanel(
$xscript);
$nav->appendChild($object_xscript_panel);
}
$crumbs = id($this->buildApplicationCrumbs())
->addTextCrumb(
pht('Transcripts'),
$this->getApplicationURI('/transcript/'))
->addTextCrumb($xscript->getID());
$nav->setCrumbs($crumbs);
return $this->buildApplicationPage(
$nav,
array(
'title' => pht('Transcript'),
));
}
protected function renderConditionTestValue($condition, $handles) {
switch ($condition->getFieldName()) {
case HeraldAdapter::FIELD_RULE:
$value = array($condition->getTestValue());
break;
default:
$value = $condition->getTestValue();
break;
}
if (!is_scalar($value) && $value !== null) {
foreach ($value as $key => $phid) {
$handle = idx($handles, $phid);
if ($handle) {
$value[$key] = $handle->getName();
} else {
// This shouldn't ever really happen as we are supposed to have
// grabbed handles for everything, but be super liberal in what
// we accept here since we expect all sorts of weird issues as we
// version the system.
- $value[$key] = 'Unknown Object #'.$phid;
+ $value[$key] = pht('Unknown Object #%s', $phid);
}
}
sort($value);
$value = implode(', ', $value);
}
return phutil_tag('span', array('class' => 'condition-test-value'), $value);
}
private function buildSideNav() {
$nav = new AphrontSideNavFilterView();
$nav->setBaseURI(new PhutilURI('/herald/transcript/'.$this->id.'/'));
$items = array();
$filters = $this->getFilterMap();
foreach ($filters as $key => $name) {
$nav->addFilter($key, $name);
}
$nav->selectFilter($this->filter, null);
return $nav;
}
protected function getFilterMap() {
return array(
self::FILTER_ALL => pht('All Rules'),
self::FILTER_OWNED => pht('Rules I Own'),
self::FILTER_AFFECTED => pht('Rules that Affected Me'),
);
}
protected function getFilterPHIDs() {
return array($this->getRequest()->getUser()->getPHID());
}
protected function getTranscriptPHIDs($xscript) {
$phids = array();
$object_xscript = $xscript->getObjectTranscript();
if (!$object_xscript) {
return array();
}
$phids[] = $object_xscript->getPHID();
foreach ($xscript->getApplyTranscripts() as $apply_xscript) {
// TODO: This is total hacks. Add another amazing layer of abstraction.
$target = (array)$apply_xscript->getTarget();
foreach ($target as $phid) {
if ($phid) {
$phids[] = $phid;
}
}
}
foreach ($xscript->getRuleTranscripts() as $rule_xscript) {
$phids[] = $rule_xscript->getRuleOwner();
}
$condition_xscripts = $xscript->getConditionTranscripts();
if ($condition_xscripts) {
$condition_xscripts = call_user_func_array(
'array_merge',
$condition_xscripts);
}
foreach ($condition_xscripts as $condition_xscript) {
switch ($condition_xscript->getFieldName()) {
case HeraldAdapter::FIELD_RULE:
$phids[] = $condition_xscript->getTestValue();
break;
default:
$value = $condition_xscript->getTestValue();
// TODO: Also total hacks.
if (is_array($value)) {
foreach ($value as $phid) {
- if ($phid) { // TODO: Probably need to make sure this
+ if ($phid) {
+ // TODO: Probably need to make sure this
// "looks like" a PHID or decrease the level of hacks here;
// this used to be an is_numeric() check in Facebook land.
$phids[] = $phid;
}
}
}
break;
}
}
return $phids;
}
protected function filterTranscript($xscript, $filter_phids) {
$filter_owned = ($this->filter == self::FILTER_OWNED);
$filter_affected = ($this->filter == self::FILTER_AFFECTED);
if (!$filter_owned && !$filter_affected) {
// No filtering to be done.
return;
}
if (!$xscript->getObjectTranscript()) {
return;
}
$user_phid = $this->getRequest()->getUser()->getPHID();
$keep_apply_xscripts = array();
$keep_rule_xscripts = array();
$filter_phids = array_fill_keys($filter_phids, true);
$rule_xscripts = $xscript->getRuleTranscripts();
foreach ($xscript->getApplyTranscripts() as $id => $apply_xscript) {
$rule_id = $apply_xscript->getRuleID();
if ($filter_owned) {
if (empty($rule_xscripts[$rule_id])) {
// No associated rule so you can't own this effect.
continue;
}
if ($rule_xscripts[$rule_id]->getRuleOwner() != $user_phid) {
continue;
}
} else if ($filter_affected) {
$targets = (array)$apply_xscript->getTarget();
if (!array_select_keys($filter_phids, $targets)) {
continue;
}
}
$keep_apply_xscripts[$id] = true;
if ($rule_id) {
$keep_rule_xscripts[$rule_id] = true;
}
}
foreach ($rule_xscripts as $rule_id => $rule_xscript) {
if ($filter_owned && $rule_xscript->getRuleOwner() == $user_phid) {
$keep_rule_xscripts[$rule_id] = true;
}
}
$xscript->setRuleTranscripts(
array_intersect_key(
$xscript->getRuleTranscripts(),
$keep_rule_xscripts));
$xscript->setApplyTranscripts(
array_intersect_key(
$xscript->getApplyTranscripts(),
$keep_apply_xscripts));
$xscript->setConditionTranscripts(
array_intersect_key(
$xscript->getConditionTranscripts(),
$keep_rule_xscripts));
}
private function buildWarningPanel(HeraldTranscript $xscript) {
$request = $this->getRequest();
$panel = null;
if ($xscript->getObjectTranscript()) {
$handles = $this->handles;
$object_xscript = $xscript->getObjectTranscript();
$handle = $handles[$object_xscript->getPHID()];
if ($handle->getType() ==
PhabricatorRepositoryCommitPHIDType::TYPECONST) {
$commit = id(new DiffusionCommitQuery())
->setViewer($request->getUser())
->withPHIDs(array($handle->getPHID()))
->executeOne();
if ($commit) {
$repository = $commit->getRepository();
if ($repository->isImporting()) {
$title = pht(
'The %s repository is still importing.',
$repository->getMonogram());
$body = pht(
'Herald rules will not trigger until import completes.');
} else if (!$repository->isTracked()) {
$title = pht(
'The %s repository is not tracked.',
$repository->getMonogram());
$body = pht(
'Herald rules will not trigger until tracking is enabled.');
} else {
return $panel;
}
$panel = id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_WARNING)
->setTitle($title)
->appendChild($body);
}
}
}
return $panel;
}
private function buildApplyTranscriptPanel(HeraldTranscript $xscript) {
$handles = $this->handles;
$adapter = $this->getAdapter();
$rule_type_global = HeraldRuleTypeConfig::RULE_TYPE_GLOBAL;
$action_names = $adapter->getActionNameMap($rule_type_global);
$list = new PHUIObjectItemListView();
$list->setStates(true);
$list->setNoDataString(pht('No actions were taken.'));
foreach ($xscript->getApplyTranscripts() as $apply_xscript) {
$target = $apply_xscript->getTarget();
switch ($apply_xscript->getAction()) {
case HeraldAdapter::ACTION_NOTHING:
$target = null;
break;
case HeraldAdapter::ACTION_FLAG:
$target = PhabricatorFlagColor::getColorName($target);
break;
case HeraldAdapter::ACTION_BLOCK:
// Target is a text string.
$target = $target;
break;
default:
if (is_array($target) && $target) {
foreach ($target as $k => $phid) {
if (isset($handles[$phid])) {
$target[$k] = $handles[$phid]->getName();
}
}
$target = implode(', ', $target);
} else if (is_string($target)) {
$target = $target;
} else {
$target = '<empty>';
}
break;
}
$item = new PHUIObjectItemView();
if ($apply_xscript->getApplied()) {
$item->setState(PHUIObjectItemView::STATE_SUCCESS);
} else {
$item->setState(PHUIObjectItemView::STATE_FAIL);
}
$rule = idx($action_names, $apply_xscript->getAction(), pht('Unknown'));
$item->setHeader(pht('%s: %s', $rule, $target));
$item->addAttribute($apply_xscript->getReason());
$item->addAttribute(
pht('Outcome: %s', $apply_xscript->getAppliedReason()));
$list->addItem($item);
}
$box = new PHUIObjectBoxView();
$box->setHeaderText(pht('Actions Taken'));
$box->appendChild($list);
return $box;
}
private function buildActionTranscriptPanel(HeraldTranscript $xscript) {
$action_xscript = mgroup($xscript->getApplyTranscripts(), 'getRuleID');
$adapter = $this->getAdapter();
$field_names = $adapter->getFieldNameMap();
$condition_names = $adapter->getConditionNameMap();
$handles = $this->handles;
$rule_markup = array();
foreach ($xscript->getRuleTranscripts() as $rule_id => $rule) {
$cond_markup = array();
foreach ($xscript->getConditionTranscriptsForRule($rule_id) as $cond) {
if ($cond->getNote()) {
$note = phutil_tag_div('herald-condition-note', $cond->getNote());
} else {
$note = null;
}
if ($cond->getResult()) {
$result = phutil_tag(
'span',
array('class' => 'herald-outcome condition-pass'),
"\xE2\x9C\x93");
} else {
$result = phutil_tag(
'span',
array('class' => 'herald-outcome condition-fail'),
"\xE2\x9C\x98");
}
$cond_markup[] = phutil_tag(
'li',
array(),
pht(
'%s Condition: %s %s %s%s',
$result,
idx($field_names, $cond->getFieldName(), pht('Unknown')),
idx($condition_names, $cond->getCondition(), pht('Unknown')),
$this->renderConditionTestValue($cond, $handles),
$note));
}
if ($rule->getResult()) {
$result = phutil_tag(
'span',
array('class' => 'herald-outcome rule-pass'),
pht('PASS'));
$class = 'herald-rule-pass';
} else {
$result = phutil_tag(
'span',
array('class' => 'herald-outcome rule-fail'),
pht('FAIL'));
$class = 'herald-rule-fail';
}
$cond_markup[] = phutil_tag(
'li',
array(),
array($result, $rule->getReason()));
$user_phid = $this->getRequest()->getUser()->getPHID();
$name = $rule->getRuleName();
$rule_markup[] =
phutil_tag(
'li',
array(
'class' => $class,
),
phutil_tag_div('rule-name', array(
phutil_tag('strong', array(), $name),
' ',
phutil_tag('ul', array(), $cond_markup),
)));
}
$box = null;
if ($rule_markup) {
$box = new PHUIObjectBoxView();
$box->setHeaderText(pht('Rule Details'));
$box->appendChild(phutil_tag(
'ul',
array('class' => 'herald-explain-list'),
$rule_markup));
}
return $box;
}
private function buildObjectTranscriptPanel(HeraldTranscript $xscript) {
$adapter = $this->getAdapter();
$field_names = $adapter->getFieldNameMap();
$object_xscript = $xscript->getObjectTranscript();
$data = array();
if ($object_xscript) {
$phid = $object_xscript->getPHID();
$handles = $this->handles;
$data += array(
pht('Object Name') => $object_xscript->getName(),
pht('Object Type') => $object_xscript->getType(),
pht('Object PHID') => $phid,
pht('Object Link') => $handles[$phid]->renderLink(),
);
}
$data += $xscript->getMetadataMap();
if ($object_xscript) {
foreach ($object_xscript->getFields() as $field => $value) {
$field = idx($field_names, $field, '['.$field.'?]');
$data['Field: '.$field] = $value;
}
}
$rows = array();
foreach ($data as $name => $value) {
if (!($value instanceof PhutilSafeHTML)) {
if (!is_scalar($value) && !is_null($value)) {
$value = implode("\n", $value);
}
if (strlen($value) > 256) {
$value = phutil_tag(
'textarea',
array(
'class' => 'herald-field-value-transcript',
),
$value);
}
}
$rows[] = array($name, $value);
}
$property_list = new PHUIPropertyListView();
$property_list->setStacked(true);
foreach ($rows as $row) {
$property_list->addProperty($row[0], $row[1]);
}
$box = new PHUIObjectBoxView();
$box->setHeaderText(pht('Object Transcript'));
$box->appendChild($property_list);
return $box;
}
}
diff --git a/src/applications/herald/engine/HeraldEngine.php b/src/applications/herald/engine/HeraldEngine.php
index 9774cb866..5f1189fff 100644
--- a/src/applications/herald/engine/HeraldEngine.php
+++ b/src/applications/herald/engine/HeraldEngine.php
@@ -1,438 +1,441 @@
<?php
final class HeraldEngine {
protected $rules = array();
protected $results = array();
protected $stack = array();
protected $activeRule = null;
protected $fieldCache = array();
protected $object = null;
private $dryRun;
public function setDryRun($dry_run) {
$this->dryRun = $dry_run;
return $this;
}
public function getDryRun() {
return $this->dryRun;
}
public function getRule($phid) {
return idx($this->rules, $phid);
}
public function loadRulesForAdapter(HeraldAdapter $adapter) {
return id(new HeraldRuleQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withDisabled(false)
->withContentTypes(array($adapter->getAdapterContentType()))
->needConditionsAndActions(true)
->needAppliedToPHIDs(array($adapter->getPHID()))
->needValidateAuthors(true)
->execute();
}
public static function loadAndApplyRules(HeraldAdapter $adapter) {
$engine = new HeraldEngine();
$rules = $engine->loadRulesForAdapter($adapter);
$effects = $engine->applyRules($rules, $adapter);
$engine->applyEffects($effects, $adapter, $rules);
return $engine->getTranscript();
}
public function applyRules(array $rules, HeraldAdapter $object) {
assert_instances_of($rules, 'HeraldRule');
$t_start = microtime(true);
// Rules execute in a well-defined order: sort them into execution order.
$rules = msort($rules, 'getRuleExecutionOrderSortKey');
$rules = mpull($rules, null, 'getPHID');
$this->transcript = new HeraldTranscript();
$this->transcript->setObjectPHID((string)$object->getPHID());
$this->fieldCache = array();
$this->results = array();
$this->rules = $rules;
$this->object = $object;
$effects = array();
foreach ($rules as $phid => $rule) {
$this->stack = array();
$policy_first = HeraldRepetitionPolicyConfig::FIRST;
$policy_first_int = HeraldRepetitionPolicyConfig::toInt($policy_first);
$is_first_only = ($rule->getRepetitionPolicy() == $policy_first_int);
try {
if (!$this->getDryRun() &&
$is_first_only &&
$rule->getRuleApplied($object->getPHID())) {
// This is not a dry run, and this rule is only supposed to be
// applied a single time, and it's already been applied...
// That means automatic failure.
$xscript = id(new HeraldRuleTranscript())
->setRuleID($rule->getID())
->setResult(false)
->setRuleName($rule->getName())
->setRuleOwner($rule->getAuthorPHID())
->setReason(
- 'This rule is only supposed to be repeated a single time, '.
- 'and it has already been applied.');
+ pht(
+ 'This rule is only supposed to be repeated a single time, '.
+ 'and it has already been applied.'));
$this->transcript->addRuleTranscript($xscript);
$rule_matches = false;
} else {
$rule_matches = $this->doesRuleMatch($rule, $object);
}
} catch (HeraldRecursiveConditionsException $ex) {
$names = array();
foreach ($this->stack as $rule_id => $ignored) {
$names[] = '"'.$rules[$rule_id]->getName().'"';
}
$names = implode(', ', $names);
foreach ($this->stack as $rule_id => $ignored) {
$xscript = new HeraldRuleTranscript();
$xscript->setRuleID($rule_id);
$xscript->setResult(false);
$xscript->setReason(
- "Rules {$names} are recursively dependent upon one another! ".
- "Don't do this! You have formed an unresolvable cycle in the ".
- "dependency graph!");
+ pht(
+ "Rules %s are recursively dependent upon one another! ".
+ "Don't do this! You have formed an unresolvable cycle in the ".
+ "dependency graph!",
+ $names));
$xscript->setRuleName($rules[$rule_id]->getName());
$xscript->setRuleOwner($rules[$rule_id]->getAuthorPHID());
$this->transcript->addRuleTranscript($xscript);
}
$rule_matches = false;
}
$this->results[$phid] = $rule_matches;
if ($rule_matches) {
foreach ($this->getRuleEffects($rule, $object) as $effect) {
$effects[] = $effect;
}
}
}
$object_transcript = new HeraldObjectTranscript();
$object_transcript->setPHID($object->getPHID());
$object_transcript->setName($object->getHeraldName());
$object_transcript->setType($object->getAdapterContentType());
$object_transcript->setFields($this->fieldCache);
$this->transcript->setObjectTranscript($object_transcript);
$t_end = microtime(true);
$this->transcript->setDuration($t_end - $t_start);
return $effects;
}
public function applyEffects(
array $effects,
HeraldAdapter $adapter,
array $rules) {
assert_instances_of($effects, 'HeraldEffect');
assert_instances_of($rules, 'HeraldRule');
$this->transcript->setDryRun((int)$this->getDryRun());
if ($this->getDryRun()) {
$xscripts = array();
foreach ($effects as $effect) {
$xscripts[] = new HeraldApplyTranscript(
$effect,
false,
pht('This was a dry run, so no actions were actually taken.'));
}
} else {
$xscripts = $adapter->applyHeraldEffects($effects);
}
assert_instances_of($xscripts, 'HeraldApplyTranscript');
foreach ($xscripts as $apply_xscript) {
$this->transcript->addApplyTranscript($apply_xscript);
}
// For dry runs, don't mark the rule as having applied to the object.
if ($this->getDryRun()) {
return;
}
$rules = mpull($rules, null, 'getID');
$applied_ids = array();
$first_policy = HeraldRepetitionPolicyConfig::toInt(
HeraldRepetitionPolicyConfig::FIRST);
// Mark all the rules that have had their effects applied as having been
// executed for the current object.
$rule_ids = mpull($xscripts, 'getRuleID');
foreach ($rule_ids as $rule_id) {
if (!$rule_id) {
// Some apply transcripts are purely informational and not associated
// with a rule, e.g. carryover emails from earlier revisions.
continue;
}
$rule = idx($rules, $rule_id);
if (!$rule) {
continue;
}
if ($rule->getRepetitionPolicy() == $first_policy) {
$applied_ids[] = $rule_id;
}
}
if ($applied_ids) {
$conn_w = id(new HeraldRule())->establishConnection('w');
$sql = array();
foreach ($applied_ids as $id) {
$sql[] = qsprintf(
$conn_w,
'(%s, %d)',
$adapter->getPHID(),
$id);
}
queryfx(
$conn_w,
'INSERT IGNORE INTO %T (phid, ruleID) VALUES %Q',
HeraldRule::TABLE_RULE_APPLIED,
implode(', ', $sql));
}
}
public function getTranscript() {
$this->transcript->save();
return $this->transcript;
}
public function doesRuleMatch(
HeraldRule $rule,
HeraldAdapter $object) {
$phid = $rule->getPHID();
if (isset($this->results[$phid])) {
// If we've already evaluated this rule because another rule depends
// on it, we don't need to reevaluate it.
return $this->results[$phid];
}
if (isset($this->stack[$phid])) {
// We've recursed, fail all of the rules on the stack. This happens when
// there's a dependency cycle with "Rule conditions match for rule ..."
// conditions.
foreach ($this->stack as $rule_phid => $ignored) {
$this->results[$rule_phid] = false;
}
throw new HeraldRecursiveConditionsException();
}
$this->stack[$phid] = true;
$all = $rule->getMustMatchAll();
$conditions = $rule->getConditions();
$result = null;
$local_version = id(new HeraldRule())->getConfigVersion();
if ($rule->getConfigVersion() > $local_version) {
$reason = pht(
'Rule could not be processed, it was created with a newer version '.
'of Herald.');
$result = false;
} else if (!$conditions) {
$reason = pht(
'Rule failed automatically because it has no conditions.');
$result = false;
} else if (!$rule->hasValidAuthor()) {
$reason = pht(
'Rule failed automatically because its owner is invalid '.
'or disabled.');
$result = false;
} else if (!$this->canAuthorViewObject($rule, $object)) {
$reason = pht(
'Rule failed automatically because it is a personal rule and its '.
'owner can not see the object.');
$result = false;
} else if (!$this->canRuleApplyToObject($rule, $object)) {
$reason = pht(
'Rule failed automatically because it is an object rule which is '.
'not relevant for this object.');
$result = false;
} else {
foreach ($conditions as $condition) {
$match = $this->doesConditionMatch($rule, $condition, $object);
if (!$all && $match) {
- $reason = 'Any condition matched.';
+ $reason = pht('Any condition matched.');
$result = true;
break;
}
if ($all && !$match) {
- $reason = 'Not all conditions matched.';
+ $reason = pht('Not all conditions matched.');
$result = false;
break;
}
}
if ($result === null) {
if ($all) {
- $reason = 'All conditions matched.';
+ $reason = pht('All conditions matched.');
$result = true;
} else {
- $reason = 'No conditions matched.';
+ $reason = pht('No conditions matched.');
$result = false;
}
}
}
$rule_transcript = new HeraldRuleTranscript();
$rule_transcript->setRuleID($rule->getID());
$rule_transcript->setResult($result);
$rule_transcript->setReason($reason);
$rule_transcript->setRuleName($rule->getName());
$rule_transcript->setRuleOwner($rule->getAuthorPHID());
$this->transcript->addRuleTranscript($rule_transcript);
return $result;
}
protected function doesConditionMatch(
HeraldRule $rule,
HeraldCondition $condition,
HeraldAdapter $object) {
$object_value = $this->getConditionObjectValue($condition, $object);
$test_value = $condition->getValue();
$cond = $condition->getFieldCondition();
$transcript = new HeraldConditionTranscript();
$transcript->setRuleID($rule->getID());
$transcript->setConditionID($condition->getID());
$transcript->setFieldName($condition->getFieldName());
$transcript->setCondition($cond);
$transcript->setTestValue($test_value);
try {
$result = $object->doesConditionMatch(
$this,
$rule,
$condition,
$object_value);
} catch (HeraldInvalidConditionException $ex) {
$result = false;
$transcript->setNote($ex->getMessage());
}
$transcript->setResult($result);
$this->transcript->addConditionTranscript($transcript);
return $result;
}
protected function getConditionObjectValue(
HeraldCondition $condition,
HeraldAdapter $object) {
$field = $condition->getFieldName();
return $this->getObjectFieldValue($field);
}
public function getObjectFieldValue($field) {
if (isset($this->fieldCache[$field])) {
return $this->fieldCache[$field];
}
$result = $this->object->getHeraldField($field);
$this->fieldCache[$field] = $result;
return $result;
}
protected function getRuleEffects(
HeraldRule $rule,
HeraldAdapter $object) {
$effects = array();
foreach ($rule->getActions() as $action) {
$effect = id(new HeraldEffect())
->setObjectPHID($object->getPHID())
->setAction($action->getAction())
->setTarget($action->getTarget())
->setRule($rule);
$name = $rule->getName();
$id = $rule->getID();
$effect->setReason(
pht(
'Conditions were met for %s',
"H{$id} {$name}"));
$effects[] = $effect;
}
return $effects;
}
private function canAuthorViewObject(
HeraldRule $rule,
HeraldAdapter $adapter) {
// Authorship is irrelevant for global rules and object rules.
if ($rule->isGlobalRule() || $rule->isObjectRule()) {
return true;
}
// The author must be able to create rules for the adapter's content type.
// In particular, this means that the application must be installed and
// accessible to the user. For example, if a user writes a Differential
// rule and then loses access to Differential, this disables the rule.
$enabled = HeraldAdapter::getEnabledAdapterMap($rule->getAuthor());
if (empty($enabled[$adapter->getAdapterContentType()])) {
return false;
}
// Finally, the author must be able to see the object itself. You can't
// write a personal rule that CC's you on revisions you wouldn't otherwise
// be able to see, for example.
$object = $adapter->getObject();
return PhabricatorPolicyFilter::hasCapability(
$rule->getAuthor(),
$object,
PhabricatorPolicyCapability::CAN_VIEW);
}
private function canRuleApplyToObject(
HeraldRule $rule,
HeraldAdapter $adapter) {
// Rules which are not object rules can apply to anything.
if (!$rule->isObjectRule()) {
return true;
}
$trigger_phid = $rule->getTriggerObjectPHID();
$object_phids = $adapter->getTriggerObjectPHIDs();
if ($object_phids) {
if (in_array($trigger_phid, $object_phids)) {
return true;
}
}
return false;
}
}
diff --git a/src/applications/herald/query/HeraldTranscriptSearchEngine.php b/src/applications/herald/query/HeraldTranscriptSearchEngine.php
index f6944e121..1fd01c3e0 100644
--- a/src/applications/herald/query/HeraldTranscriptSearchEngine.php
+++ b/src/applications/herald/query/HeraldTranscriptSearchEngine.php
@@ -1,139 +1,139 @@
<?php
final class HeraldTranscriptSearchEngine
extends PhabricatorApplicationSearchEngine {
public function getResultTypeDescription() {
return pht('Herald Transcripts');
}
public function getApplicationClassName() {
return 'PhabricatorHeraldApplication';
}
public function buildSavedQueryFromRequest(AphrontRequest $request) {
$saved = new PhabricatorSavedQuery();
$object_monograms = $request->getStrList('objectMonograms');
$saved->setParameter('objectMonograms', $object_monograms);
$ids = $request->getStrList('ids');
foreach ($ids as $key => $id) {
if (!$id || !is_numeric($id)) {
unset($ids[$key]);
} else {
$ids[$key] = $id;
}
}
$saved->setParameter('ids', $ids);
return $saved;
}
public function buildQueryFromSavedQuery(PhabricatorSavedQuery $saved) {
$query = id(new HeraldTranscriptQuery());
$object_monograms = $saved->getParameter('objectMonograms');
if ($object_monograms) {
$objects = id(new PhabricatorObjectQuery())
->setViewer($this->requireViewer())
->withNames($object_monograms)
->execute();
$query->withObjectPHIDs(mpull($objects, 'getPHID'));
}
$ids = $saved->getParameter('ids');
if ($ids) {
$query->withIDs($ids);
}
return $query;
}
public function buildSearchForm(
AphrontFormView $form,
PhabricatorSavedQuery $saved) {
$object_monograms = $saved->getParameter('objectMonograms', array());
$ids = $saved->getParameter('ids', array());
$form
->appendChild(
id(new AphrontFormTextControl())
->setName('objectMonograms')
->setLabel(pht('Object Monograms'))
->setValue(implode(', ', $object_monograms)))
->appendChild(
id(new AphrontFormTextControl())
->setName('ids')
->setLabel(pht('Transcript IDs'))
->setValue(implode(', ', $ids)));
}
protected function getURI($path) {
return '/herald/transcript/'.$path;
}
protected function getBuiltinQueryNames() {
return array(
'all' => pht('All'),
);
}
public function buildSavedQueryFromBuiltin($query_key) {
$query = $this->newSavedQuery();
$query->setQueryKey($query_key);
$viewer_phid = $this->requireViewer()->getPHID();
switch ($query_key) {
case 'all':
return $query;
}
return parent::buildSavedQueryFromBuiltin($query_key);
}
protected function getRequiredHandlePHIDsForResultList(
array $transcripts,
PhabricatorSavedQuery $query) {
return mpull($transcripts, 'getObjectPHID');
}
protected function renderResultList(
array $transcripts,
PhabricatorSavedQuery $query,
array $handles) {
assert_instances_of($transcripts, 'HeraldTranscript');
$viewer = $this->requireViewer();
$list = new PHUIObjectItemListView();
foreach ($transcripts as $xscript) {
$view_href = phutil_tag(
'a',
array(
'href' => '/herald/transcript/'.$xscript->getID().'/',
),
pht('View Full Transcript'));
$item = new PHUIObjectItemView();
$item->setObjectName($xscript->getID());
$item->setHeader($view_href);
if ($xscript->getDryRun()) {
$item->addAttribute(pht('Dry Run'));
}
$item->addAttribute($handles[$xscript->getObjectPHID()]->renderLink());
$item->addAttribute(
- number_format((int)(1000 * $xscript->getDuration())).' ms');
+ pht('%d ms', number_format((int)(1000 * $xscript->getDuration()))));
$item->addIcon(
'none',
phabricator_datetime($xscript->getTime(), $viewer));
$list->addItem($item);
}
return $list;
}
}
diff --git a/src/applications/herald/storage/HeraldRule.php b/src/applications/herald/storage/HeraldRule.php
index 2d19bc535..0600d2570 100644
--- a/src/applications/herald/storage/HeraldRule.php
+++ b/src/applications/herald/storage/HeraldRule.php
@@ -1,333 +1,333 @@
<?php
final class HeraldRule extends HeraldDAO
implements
PhabricatorApplicationTransactionInterface,
PhabricatorFlaggableInterface,
PhabricatorPolicyInterface,
PhabricatorDestructibleInterface {
const TABLE_RULE_APPLIED = 'herald_ruleapplied';
protected $name;
protected $authorPHID;
protected $contentType;
protected $mustMatchAll;
protected $repetitionPolicy;
protected $ruleType;
protected $isDisabled = 0;
protected $triggerObjectPHID;
protected $configVersion = 38;
// PHIDs for which this rule has been applied
private $ruleApplied = self::ATTACHABLE;
private $validAuthor = self::ATTACHABLE;
private $author = self::ATTACHABLE;
private $conditions;
private $actions;
private $triggerObject = self::ATTACHABLE;
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_COLUMN_SCHEMA => array(
'name' => 'text255',
'contentType' => 'text255',
'mustMatchAll' => 'bool',
'configVersion' => 'uint32',
'ruleType' => 'text32',
'isDisabled' => 'uint32',
'triggerObjectPHID' => 'phid?',
// T6203/NULLABILITY
// This should not be nullable.
'repetitionPolicy' => 'uint32?',
),
self::CONFIG_KEY_SCHEMA => array(
'key_author' => array(
'columns' => array('authorPHID'),
),
'key_ruletype' => array(
'columns' => array('ruleType'),
),
'key_trigger' => array(
'columns' => array('triggerObjectPHID'),
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(HeraldRulePHIDType::TYPECONST);
}
public function getRuleApplied($phid) {
return $this->assertAttachedKey($this->ruleApplied, $phid);
}
public function setRuleApplied($phid, $applied) {
if ($this->ruleApplied === self::ATTACHABLE) {
$this->ruleApplied = array();
}
$this->ruleApplied[$phid] = $applied;
return $this;
}
public function loadConditions() {
if (!$this->getID()) {
return array();
}
return id(new HeraldCondition())->loadAllWhere(
'ruleID = %d',
$this->getID());
}
public function attachConditions(array $conditions) {
assert_instances_of($conditions, 'HeraldCondition');
$this->conditions = $conditions;
return $this;
}
public function getConditions() {
// TODO: validate conditions have been attached.
return $this->conditions;
}
public function loadActions() {
if (!$this->getID()) {
return array();
}
return id(new HeraldAction())->loadAllWhere(
'ruleID = %d',
$this->getID());
}
public function attachActions(array $actions) {
// TODO: validate actions have been attached.
assert_instances_of($actions, 'HeraldAction');
$this->actions = $actions;
return $this;
}
public function getActions() {
return $this->actions;
}
public function saveConditions(array $conditions) {
assert_instances_of($conditions, 'HeraldCondition');
return $this->saveChildren(
id(new HeraldCondition())->getTableName(),
$conditions);
}
public function saveActions(array $actions) {
assert_instances_of($actions, 'HeraldAction');
return $this->saveChildren(
id(new HeraldAction())->getTableName(),
$actions);
}
protected function saveChildren($table_name, array $children) {
assert_instances_of($children, 'HeraldDAO');
if (!$this->getID()) {
- throw new Exception('Save rule before saving children.');
+ throw new PhutilInvalidStateException('save');
}
foreach ($children as $child) {
$child->setRuleID($this->getID());
}
$this->openTransaction();
queryfx(
$this->establishConnection('w'),
'DELETE FROM %T WHERE ruleID = %d',
$table_name,
$this->getID());
foreach ($children as $child) {
$child->save();
}
$this->saveTransaction();
}
public function delete() {
$this->openTransaction();
queryfx(
$this->establishConnection('w'),
'DELETE FROM %T WHERE ruleID = %d',
id(new HeraldCondition())->getTableName(),
$this->getID());
queryfx(
$this->establishConnection('w'),
'DELETE FROM %T WHERE ruleID = %d',
id(new HeraldAction())->getTableName(),
$this->getID());
$result = parent::delete();
$this->saveTransaction();
return $result;
}
public function hasValidAuthor() {
return $this->assertAttached($this->validAuthor);
}
public function attachValidAuthor($valid) {
$this->validAuthor = $valid;
return $this;
}
public function getAuthor() {
return $this->assertAttached($this->author);
}
public function attachAuthor(PhabricatorUser $user) {
$this->author = $user;
return $this;
}
public function isGlobalRule() {
return ($this->getRuleType() === HeraldRuleTypeConfig::RULE_TYPE_GLOBAL);
}
public function isPersonalRule() {
return ($this->getRuleType() === HeraldRuleTypeConfig::RULE_TYPE_PERSONAL);
}
public function isObjectRule() {
return ($this->getRuleType() == HeraldRuleTypeConfig::RULE_TYPE_OBJECT);
}
public function attachTriggerObject($trigger_object) {
$this->triggerObject = $trigger_object;
return $this;
}
public function getTriggerObject() {
return $this->assertAttached($this->triggerObject);
}
/**
* Get a sortable key for rule execution order.
*
* Rules execute in a well-defined order: personal rules first, then object
* rules, then global rules. Within each rule type, rules execute from lowest
* ID to highest ID.
*
* This ordering allows more powerful rules (like global rules) to override
* weaker rules (like personal rules) when multiple rules exist which try to
* affect the same field. Executing from low IDs to high IDs makes
* interactions easier to understand when adding new rules, because the newest
* rules always happen last.
*
* @return string A sortable key for this rule.
*/
public function getRuleExecutionOrderSortKey() {
$rule_type = $this->getRuleType();
switch ($rule_type) {
case HeraldRuleTypeConfig::RULE_TYPE_PERSONAL:
$type_order = 1;
break;
case HeraldRuleTypeConfig::RULE_TYPE_OBJECT:
$type_order = 2;
break;
case HeraldRuleTypeConfig::RULE_TYPE_GLOBAL:
$type_order = 3;
break;
default:
throw new Exception(pht('Unknown rule type "%s"!', $rule_type));
}
return sprintf('~%d%010d', $type_order, $this->getID());
}
public function getMonogram() {
return 'H'.$this->getID();
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new HeraldRuleEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new HeraldRuleTransaction();
}
public function willRenderTimeline(
PhabricatorApplicationTransactionView $timeline,
AphrontRequest $request) {
return $timeline;
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
if ($this->isGlobalRule()) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
return PhabricatorPolicies::POLICY_USER;
case PhabricatorPolicyCapability::CAN_EDIT:
$app = 'PhabricatorHeraldApplication';
$herald = PhabricatorApplication::getByClass($app);
$global = HeraldManageGlobalRulesCapability::CAPABILITY;
return $herald->getPolicy($global);
}
} else if ($this->isObjectRule()) {
return $this->getTriggerObject()->getPolicy($capability);
} else {
return PhabricatorPolicies::POLICY_NOONE;
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
if ($this->isPersonalRule()) {
return ($viewer->getPHID() == $this->getAuthorPHID());
} else {
return false;
}
}
public function describeAutomaticCapability($capability) {
if ($this->isPersonalRule()) {
return pht("A personal rule's owner can always view and edit it.");
} else if ($this->isObjectRule()) {
return pht('Object rules inherit the policies of their objects.');
}
return null;
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$this->openTransaction();
$this->delete();
$this->saveTransaction();
}
}
diff --git a/src/applications/home/controller/PhabricatorHomeMainController.php b/src/applications/home/controller/PhabricatorHomeMainController.php
index 2b3077025..3069ccee7 100644
--- a/src/applications/home/controller/PhabricatorHomeMainController.php
+++ b/src/applications/home/controller/PhabricatorHomeMainController.php
@@ -1,423 +1,421 @@
<?php
final class PhabricatorHomeMainController extends PhabricatorHomeController {
private $minipanels = array();
public function shouldAllowPublic() {
return true;
}
public function isGlobalDragAndDropUploadEnabled() {
return true;
}
public function handleRequest(AphrontRequest $request) {
$user = $request->getUser();
$dashboard = PhabricatorDashboardInstall::getDashboard(
$user,
$user->getPHID(),
get_class($this->getCurrentApplication()));
if (!$dashboard) {
$dashboard = PhabricatorDashboardInstall::getDashboard(
$user,
PhabricatorHomeApplication::DASHBOARD_DEFAULT,
get_class($this->getCurrentApplication()));
}
if ($dashboard) {
$content = id(new PhabricatorDashboardRenderingEngine())
->setViewer($user)
->setDashboard($dashboard)
->renderDashboard();
} else {
$project_query = new PhabricatorProjectQuery();
$project_query->setViewer($user);
$project_query->withMemberPHIDs(array($user->getPHID()));
$projects = $project_query->execute();
$content = $this->buildMainResponse($projects);
}
if (!$request->getURIData('only')) {
$nav = $this->buildNav();
$nav->appendChild(
array(
$content,
id(new PhabricatorGlobalUploadTargetView())->setUser($user),
));
$content = $nav;
}
return $this->buildApplicationPage(
$content,
array(
'title' => 'Phabricator',
));
}
private function buildMainResponse(array $projects) {
assert_instances_of($projects, 'PhabricatorProject');
$viewer = $this->getRequest()->getUser();
$has_maniphest = PhabricatorApplication::isClassInstalledForViewer(
'PhabricatorManiphestApplication',
$viewer);
$has_audit = PhabricatorApplication::isClassInstalledForViewer(
'PhabricatorAuditApplication',
$viewer);
$has_differential = PhabricatorApplication::isClassInstalledForViewer(
'PhabricatorDifferentialApplication',
$viewer);
if ($has_maniphest) {
$unbreak_panel = $this->buildUnbreakNowPanel();
$triage_panel = $this->buildNeedsTriagePanel($projects);
$tasks_panel = $this->buildTasksPanel();
} else {
$unbreak_panel = null;
$triage_panel = null;
$tasks_panel = null;
}
if ($has_audit) {
$audit_panel = $this->buildAuditPanel();
$commit_panel = $this->buildCommitPanel();
} else {
$audit_panel = null;
$commit_panel = null;
}
if (PhabricatorEnv::getEnvConfig('welcome.html') !== null) {
$welcome_panel = $this->buildWelcomePanel();
} else {
$welcome_panel = null;
}
if ($has_differential) {
$revision_panel = $this->buildRevisionPanel();
} else {
$revision_panel = null;
}
require_celerity_resource('homepage-panel-css');
$home = phutil_tag(
'div',
array(
'class' => 'homepage-panel',
),
array(
$welcome_panel,
$unbreak_panel,
$triage_panel,
$revision_panel,
$tasks_panel,
$audit_panel,
$commit_panel,
$this->minipanels,
));
return $home;
}
private function buildUnbreakNowPanel() {
$unbreak_now = PhabricatorEnv::getEnvConfig(
'maniphest.priorities.unbreak-now');
if (!$unbreak_now) {
return null;
}
$user = $this->getRequest()->getUser();
$task_query = id(new ManiphestTaskQuery())
->setViewer($user)
->withStatuses(ManiphestTaskStatus::getOpenStatusConstants())
->withPriorities(array($unbreak_now))
->needProjectPHIDs(true)
->setLimit(10);
$tasks = $task_query->execute();
if (!$tasks) {
return $this->renderMiniPanel(
- 'No "Unbreak Now!" Tasks',
- 'Nothing appears to be critically broken right now.');
+ pht('No "Unbreak Now!" Tasks'),
+ pht('Nothing appears to be critically broken right now.'));
}
$href = urisprintf(
'/maniphest/?statuses=open()&priorities=%s#R',
$unbreak_now);
$title = pht('Unbreak Now!');
$panel = new PHUIObjectBoxView();
$panel->setHeader($this->renderSectionHeader($title, $href));
$panel->appendChild($this->buildTaskListView($tasks));
return $panel;
}
private function buildNeedsTriagePanel(array $projects) {
assert_instances_of($projects, 'PhabricatorProject');
$needs_triage = PhabricatorEnv::getEnvConfig(
'maniphest.priorities.needs-triage');
if (!$needs_triage) {
return null;
}
$user = $this->getRequest()->getUser();
if (!$user->isLoggedIn()) {
return null;
}
if ($projects) {
$task_query = id(new ManiphestTaskQuery())
->setViewer($user)
->withStatuses(ManiphestTaskStatus::getOpenStatusConstants())
->withPriorities(array($needs_triage))
->withEdgeLogicPHIDs(
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST,
PhabricatorQueryConstraint::OPERATOR_OR,
mpull($projects, 'getPHID'))
->needProjectPHIDs(true)
->setLimit(10);
$tasks = $task_query->execute();
} else {
$tasks = array();
}
if (!$tasks) {
return $this->renderMiniPanel(
- 'No "Needs Triage" Tasks',
- hsprintf(
- 'No tasks in <a href="/project/">projects you are a member of</a> '.
- 'need triage.'));
+ pht('No "Needs Triage" Tasks'),
+ pht('No tasks in projects you are a member of need triage.'));
}
$title = pht('Needs Triage');
$href = urisprintf(
'/maniphest/?statuses=open()&priorities=%s&projects=projects(%s)#R',
$needs_triage,
$user->getPHID());
$panel = new PHUIObjectBoxView();
$panel->setHeader($this->renderSectionHeader($title, $href));
$panel->appendChild($this->buildTaskListView($tasks));
return $panel;
}
private function buildRevisionPanel() {
$user = $this->getRequest()->getUser();
$user_phid = $user->getPHID();
$revision_query = id(new DifferentialRevisionQuery())
->setViewer($user)
->withStatus(DifferentialRevisionQuery::STATUS_OPEN)
->withResponsibleUsers(array($user_phid))
->needRelationships(true)
->needFlags(true)
->needDrafts(true);
$revisions = $revision_query->execute();
list($blocking, $active, ) = DifferentialRevisionQuery::splitResponsible(
$revisions,
array($user_phid));
if (!$blocking && !$active) {
return $this->renderMiniPanel(
- 'No Waiting Revisions',
- 'No revisions are waiting on you.');
+ pht('No Waiting Revisions'),
+ pht('No revisions are waiting on you.'));
}
$title = pht('Revisions Waiting on You');
$href = '/differential';
$panel = new PHUIObjectBoxView();
$panel->setHeader($this->renderSectionHeader($title, $href));
$revision_view = id(new DifferentialRevisionListView())
->setHighlightAge(true)
->setRevisions(array_merge($blocking, $active))
->setUser($user);
$phids = array_merge(
array($user_phid),
$revision_view->getRequiredHandlePHIDs());
$handles = $this->loadViewerHandles($phids);
$revision_view->setHandles($handles);
$list_view = $revision_view->render();
$list_view->setFlush(true);
$panel->appendChild($list_view);
return $panel;
}
private function buildWelcomePanel() {
$panel = new PHUIObjectBoxView();
$panel->setHeaderText(pht('Welcome'));
$panel->appendChild(
phutil_safe_html(
PhabricatorEnv::getEnvConfig('welcome.html')));
return $panel;
}
private function buildTasksPanel() {
$user = $this->getRequest()->getUser();
$user_phid = $user->getPHID();
$task_query = id(new ManiphestTaskQuery())
->setViewer($user)
->withStatuses(ManiphestTaskStatus::getOpenStatusConstants())
->setGroupBy(ManiphestTaskQuery::GROUP_PRIORITY)
->withOwners(array($user_phid))
->needProjectPHIDs(true)
->setLimit(10);
$tasks = $task_query->execute();
if (!$tasks) {
return $this->renderMiniPanel(
- 'No Assigned Tasks',
- 'You have no assigned tasks.');
+ pht('No Assigned Tasks'),
+ pht('You have no assigned tasks.'));
}
$title = pht('Assigned Tasks');
$href = '/maniphest/query/assigned/';
$panel = new PHUIObjectBoxView();
$panel->setHeader($this->renderSectionHeader($title, $href));
$panel->appendChild($this->buildTaskListView($tasks));
return $panel;
}
private function buildTaskListView(array $tasks) {
assert_instances_of($tasks, 'ManiphestTask');
$user = $this->getRequest()->getUser();
$phids = array_merge(
array_filter(mpull($tasks, 'getOwnerPHID')),
array_mergev(mpull($tasks, 'getProjectPHIDs')));
$handles = $this->loadViewerHandles($phids);
$view = new ManiphestTaskListView();
$view->setTasks($tasks);
$view->setUser($user);
$view->setHandles($handles);
return $view;
}
private function renderSectionHeader($title, $href) {
$title = phutil_tag(
'a',
array(
'href' => $href,
),
$title);
$header = id(new PHUIHeaderView())
->setHeader($title);
return $header;
}
private function renderMiniPanel($title, $body) {
$panel = new PHUIInfoView();
$panel->setSeverity(PHUIInfoView::SEVERITY_NODATA);
$panel->appendChild(
phutil_tag(
'p',
array(
),
array(
phutil_tag('strong', array(), $title.': '),
$body,
)));
$this->minipanels[] = $panel;
}
public function buildAuditPanel() {
$request = $this->getRequest();
$user = $request->getUser();
$phids = PhabricatorAuditCommentEditor::loadAuditPHIDsForUser($user);
$query = id(new DiffusionCommitQuery())
->setViewer($user)
->withAuditorPHIDs($phids)
->withAuditStatus(DiffusionCommitQuery::AUDIT_STATUS_OPEN)
->withAuditAwaitingUser($user)
->needAuditRequests(true)
->needCommitData(true)
->setLimit(10);
$commits = $query->execute();
if (!$commits) {
return $this->renderMinipanel(
- 'No Audits',
- 'No commits are waiting for you to audit them.');
+ pht('No Audits'),
+ pht('No commits are waiting for you to audit them.'));
}
$view = id(new PhabricatorAuditListView())
->setCommits($commits)
->setUser($user);
$phids = $view->getRequiredHandlePHIDs();
$handles = $this->loadViewerHandles($phids);
$view->setHandles($handles);
$title = pht('Audits');
$href = '/audit/';
$panel = new PHUIObjectBoxView();
$panel->setHeader($this->renderSectionHeader($title, $href));
$panel->appendChild($view);
return $panel;
}
public function buildCommitPanel() {
$request = $this->getRequest();
$user = $request->getUser();
$phids = array($user->getPHID());
$query = id(new DiffusionCommitQuery())
->setViewer($user)
->withAuthorPHIDs($phids)
->withAuditStatus(DiffusionCommitQuery::AUDIT_STATUS_CONCERN)
->needCommitData(true)
->needAuditRequests(true)
->setLimit(10);
$commits = $query->execute();
if (!$commits) {
return $this->renderMinipanel(
- 'No Problem Commits',
- 'No one has raised concerns with your commits.');
+ pht('No Problem Commits'),
+ pht('No one has raised concerns with your commits.'));
}
$view = id(new PhabricatorAuditListView())
->setCommits($commits)
->setUser($user);
$phids = $view->getRequiredHandlePHIDs();
$handles = $this->loadViewerHandles($phids);
$view->setHandles($handles);
$title = pht('Problem Commits');
$href = '/audit/';
$panel = new PHUIObjectBoxView();
$panel->setHeader($this->renderSectionHeader($title, $href));
$panel->appendChild($view);
return $panel;
}
}
diff --git a/src/applications/legalpad/controller/LegalpadDocumentEditController.php b/src/applications/legalpad/controller/LegalpadDocumentEditController.php
index c4bff97f6..908bf8dd5 100644
--- a/src/applications/legalpad/controller/LegalpadDocumentEditController.php
+++ b/src/applications/legalpad/controller/LegalpadDocumentEditController.php
@@ -1,263 +1,262 @@
<?php
final class LegalpadDocumentEditController extends LegalpadController {
public function handleRequest(AphrontRequest $request) {
$user = $request->getUser();
$id = $request->getURIData('id');
if (!$id) {
$is_create = true;
$this->requireApplicationCapability(
LegalpadCreateDocumentsCapability::CAPABILITY);
$document = LegalpadDocument::initializeNewDocument($user);
$body = id(new LegalpadDocumentBody())
->setCreatorPHID($user->getPHID());
$document->attachDocumentBody($body);
$document->setDocumentBodyPHID(PhabricatorPHIDConstants::PHID_VOID);
} else {
$is_create = false;
$document = id(new LegalpadDocumentQuery())
->setViewer($user)
->needDocumentBodies(true)
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->withIDs(array($id))
->executeOne();
if (!$document) {
return new Aphront404Response();
}
}
$e_title = true;
$e_text = true;
$title = $document->getDocumentBody()->getTitle();
$text = $document->getDocumentBody()->getText();
$v_signature_type = $document->getSignatureType();
$v_preamble = $document->getPreamble();
$v_require_signature = $document->getRequireSignature();
$errors = array();
$can_view = null;
$can_edit = null;
if ($request->isFormPost()) {
$xactions = array();
$title = $request->getStr('title');
if (!strlen($title)) {
$e_title = pht('Required');
$errors[] = pht('The document title may not be blank.');
} else {
$xactions[] = id(new LegalpadTransaction())
->setTransactionType(LegalpadTransactionType::TYPE_TITLE)
->setNewValue($title);
}
$text = $request->getStr('text');
if (!strlen($text)) {
$e_text = pht('Required');
$errors[] = pht('The document may not be blank.');
} else {
$xactions[] = id(new LegalpadTransaction())
->setTransactionType(LegalpadTransactionType::TYPE_TEXT)
->setNewValue($text);
}
$can_view = $request->getStr('can_view');
$xactions[] = id(new LegalpadTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_VIEW_POLICY)
->setNewValue($can_view);
$can_edit = $request->getStr('can_edit');
$xactions[] = id(new LegalpadTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_EDIT_POLICY)
->setNewValue($can_edit);
if ($is_create) {
$v_signature_type = $request->getStr('signatureType');
$xactions[] = id(new LegalpadTransaction())
->setTransactionType(LegalpadTransactionType::TYPE_SIGNATURE_TYPE)
->setNewValue($v_signature_type);
}
$v_preamble = $request->getStr('preamble');
$xactions[] = id(new LegalpadTransaction())
->setTransactionType(LegalpadTransactionType::TYPE_PREAMBLE)
->setNewValue($v_preamble);
$v_require_signature = $request->getBool('requireSignature', 0);
if ($v_require_signature) {
if (!$user->getIsAdmin()) {
$errors[] = pht('Only admins may require signature.');
}
$individual = LegalpadDocument::SIGNATURE_TYPE_INDIVIDUAL;
if ($v_signature_type != $individual) {
$errors[] = pht(
'Only documents with signature type "individual" may require '.
'signing to use Phabricator.');
}
}
if ($user->getIsAdmin()) {
$xactions[] = id(new LegalpadTransaction())
->setTransactionType(LegalpadTransactionType::TYPE_REQUIRE_SIGNATURE)
->setNewValue($v_require_signature);
}
if (!$errors) {
$editor = id(new LegalpadDocumentEditor())
->setContentSourceFromRequest($request)
->setContinueOnNoEffect(true)
->setActor($user);
$xactions = $editor->applyTransactions($document, $xactions);
return id(new AphrontRedirectResponse())
->setURI($this->getApplicationURI('view/'.$document->getID()));
}
}
if ($errors) {
// set these to what was specified in the form on post
$document->setViewPolicy($can_view);
$document->setEditPolicy($can_edit);
}
$form = id(new AphrontFormView())
->setUser($user)
->appendChild(
id(new AphrontFormTextControl())
->setID('document-title')
->setLabel(pht('Title'))
->setError($e_title)
->setValue($title)
->setName('title'));
if ($is_create) {
$form->appendChild(
id(new AphrontFormSelectControl())
->setLabel(pht('Who Should Sign?'))
->setName(pht('signatureType'))
->setValue($v_signature_type)
->setOptions(LegalpadDocument::getSignatureTypeMap()));
$show_require = true;
$caption = pht('Applies only to documents individuals sign.');
} else {
$form->appendChild(
id(new AphrontFormMarkupControl())
->setLabel(pht('Who Should Sign?'))
->setValue($document->getSignatureTypeName()));
$individual = LegalpadDocument::SIGNATURE_TYPE_INDIVIDUAL;
$show_require = $document->getSignatureType() == $individual;
$caption = null;
}
if ($show_require) {
$form
->appendChild(
id(new AphrontFormCheckboxControl())
->setDisabled(!$user->getIsAdmin())
->setLabel(pht('Require Signature'))
->addCheckbox(
'requireSignature',
'requireSignature',
- pht(
- 'Should signing this document be required to use Phabricator?'),
+ pht('Should signing this document be required to use Phabricator?'),
$v_require_signature)
->setCaption($caption));
}
$form
->appendChild(
id(new PhabricatorRemarkupControl())
->setUser($user)
->setID('preamble')
->setLabel(pht('Preamble'))
->setValue($v_preamble)
->setName('preamble')
->setCaption(
pht('Optional help text for users signing this document.')))
->appendChild(
id(new PhabricatorRemarkupControl())
->setUser($user)
->setID('document-text')
->setLabel(pht('Document Body'))
->setError($e_text)
->setValue($text)
->setHeight(AphrontFormTextAreaControl::HEIGHT_VERY_TALL)
->setName('text'));
$policies = id(new PhabricatorPolicyQuery())
->setViewer($user)
->setObject($document)
->execute();
$form
->appendChild(
id(new AphrontFormPolicyControl())
->setUser($user)
->setCapability(PhabricatorPolicyCapability::CAN_VIEW)
->setPolicyObject($document)
->setPolicies($policies)
->setName('can_view'))
->appendChild(
id(new AphrontFormPolicyControl())
->setUser($user)
->setCapability(PhabricatorPolicyCapability::CAN_EDIT)
->setPolicyObject($document)
->setPolicies($policies)
->setName('can_edit'));
$crumbs = $this->buildApplicationCrumbs($this->buildSideNav());
$submit = new AphrontFormSubmitControl();
if ($is_create) {
$submit->setValue(pht('Create Document'));
$submit->addCancelButton($this->getApplicationURI());
$title = pht('Create Document');
$short = pht('Create');
} else {
$submit->setValue(pht('Save Document'));
$submit->addCancelButton(
$this->getApplicationURI('view/'.$document->getID()));
$title = pht('Edit Document');
$short = pht('Edit');
$crumbs->addTextCrumb(
$document->getMonogram(),
$this->getApplicationURI('view/'.$document->getID()));
}
$form
->appendChild($submit);
$form_box = id(new PHUIObjectBoxView())
->setHeaderText($title)
->setFormErrors($errors)
->setForm($form);
$crumbs->addTextCrumb($short);
$preview = id(new PHUIRemarkupPreviewPanel())
->setHeader(pht('Document Preview'))
->setPreviewURI($this->getApplicationURI('document/preview/'))
->setControlID('document-text')
->setSkin('document');
return $this->buildApplicationPage(
array(
$crumbs,
$form_box,
$preview,
),
array(
'title' => $title,
));
}
}
diff --git a/src/applications/legalpad/controller/LegalpadDocumentSignController.php b/src/applications/legalpad/controller/LegalpadDocumentSignController.php
index 4cd2f59b4..3e65d1de1 100644
--- a/src/applications/legalpad/controller/LegalpadDocumentSignController.php
+++ b/src/applications/legalpad/controller/LegalpadDocumentSignController.php
@@ -1,692 +1,689 @@
<?php
final class LegalpadDocumentSignController extends LegalpadController {
public function shouldAllowPublic() {
return true;
}
public function shouldAllowLegallyNonCompliantUsers() {
return true;
}
public function handleRequest(AphrontRequest $request) {
$viewer = $request->getUser();
$document = id(new LegalpadDocumentQuery())
->setViewer($viewer)
->withIDs(array($request->getURIData('id')))
->needDocumentBodies(true)
->executeOne();
if (!$document) {
return new Aphront404Response();
}
$information = $this->readSignerInformation(
$document,
$request);
if ($information instanceof AphrontResponse) {
return $information;
}
list($signer_phid, $signature_data) = $information;
$signature = null;
$type_individual = LegalpadDocument::SIGNATURE_TYPE_INDIVIDUAL;
$is_individual = ($document->getSignatureType() == $type_individual);
switch ($document->getSignatureType()) {
case LegalpadDocument::SIGNATURE_TYPE_NONE:
// nothing to sign means this should be true
$has_signed = true;
// this is a status UI element
$signed_status = null;
break;
case LegalpadDocument::SIGNATURE_TYPE_INDIVIDUAL:
if ($signer_phid) {
// TODO: This is odd and should probably be adjusted after
// grey/external accounts work better, but use the omnipotent
// viewer to check for a signature so we can pick up
// anonymous/grey signatures.
$signature = id(new LegalpadDocumentSignatureQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withDocumentPHIDs(array($document->getPHID()))
->withSignerPHIDs(array($signer_phid))
->executeOne();
if ($signature && !$viewer->isLoggedIn()) {
return $this->newDialog()
->setTitle(pht('Already Signed'))
->appendParagraph(pht('You have already signed this document!'))
->addCancelButton('/'.$document->getMonogram(), pht('Okay'));
}
}
$signed_status = null;
if (!$signature) {
$has_signed = false;
$signature = id(new LegalpadDocumentSignature())
->setSignerPHID($signer_phid)
->setDocumentPHID($document->getPHID())
->setDocumentVersion($document->getVersions());
// If the user is logged in, show a notice that they haven't signed.
// If they aren't logged in, we can't be as sure, so don't show
// anything.
if ($viewer->isLoggedIn()) {
$signed_status = id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_WARNING)
->setErrors(
array(
pht('You have not signed this document yet.'),
));
}
} else {
$has_signed = true;
$signature_data = $signature->getSignatureData();
// In this case, we know they've signed.
$signed_at = $signature->getDateCreated();
if ($signature->getIsExemption()) {
$exemption_phid = $signature->getExemptionPHID();
$handles = $this->loadViewerHandles(array($exemption_phid));
$exemption_handle = $handles[$exemption_phid];
$signed_text = pht(
'You do not need to sign this document. '.
'%s added a signature exemption for you on %s.',
$exemption_handle->renderLink(),
phabricator_datetime($signed_at, $viewer));
} else {
$signed_text = pht(
'You signed this document on %s.',
phabricator_datetime($signed_at, $viewer));
}
$signed_status = id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_NOTICE)
->setErrors(array($signed_text));
}
$field_errors = array(
'name' => true,
'email' => true,
'agree' => true,
);
$signature->setSignatureData($signature_data);
break;
case LegalpadDocument::SIGNATURE_TYPE_CORPORATION:
$signature = id(new LegalpadDocumentSignature())
->setDocumentPHID($document->getPHID())
->setDocumentVersion($document->getVersions());
if ($viewer->isLoggedIn()) {
$has_signed = false;
$signed_status = null;
} else {
// This just hides the form.
$has_signed = true;
$login_text = pht(
'This document requires a corporate signatory. You must log in to '.
'accept this document on behalf of a company you represent.');
$signed_status = id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_WARNING)
->setErrors(array($login_text));
}
$field_errors = array(
'name' => true,
'address' => true,
'contact.name' => true,
'email' => true,
);
$signature->setSignatureData($signature_data);
break;
}
$errors = array();
if ($request->isFormOrHisecPost() && !$has_signed) {
// Require two-factor auth to sign legal documents.
if ($viewer->isLoggedIn()) {
$engine = new PhabricatorAuthSessionEngine();
$engine->requireHighSecuritySession(
$viewer,
$request,
'/'.$document->getMonogram());
}
list($form_data, $errors, $field_errors) = $this->readSignatureForm(
$document,
$request);
$signature_data = $form_data + $signature_data;
$signature->setSignatureData($signature_data);
$signature->setSignatureType($document->getSignatureType());
$signature->setSignerName((string)idx($signature_data, 'name'));
$signature->setSignerEmail((string)idx($signature_data, 'email'));
$agree = $request->getExists('agree');
if (!$agree) {
$errors[] = pht(
'You must check "I agree to the terms laid forth above."');
$field_errors['agree'] = pht('Required');
}
if ($viewer->isLoggedIn() && $is_individual) {
$verified = LegalpadDocumentSignature::VERIFIED;
} else {
$verified = LegalpadDocumentSignature::UNVERIFIED;
}
$signature->setVerified($verified);
if (!$errors) {
$signature->save();
// If the viewer is logged in, signing for themselves, send them to
// the document page, which will show that they have signed the
// document. Unless of course they were required to sign the
// document to use Phabricator; in that case try really hard to
// re-direct them to where they wanted to go.
//
// Otherwise, send them to a completion page.
if ($viewer->isLoggedIn() && $is_individual) {
$next_uri = '/'.$document->getMonogram();
if ($document->getRequireSignature()) {
$request_uri = $request->getRequestURI();
$next_uri = (string)$request_uri;
}
} else {
$this->sendVerifySignatureEmail(
$document,
$signature);
$next_uri = $this->getApplicationURI('done/');
}
return id(new AphrontRedirectResponse())->setURI($next_uri);
}
}
$document_body = $document->getDocumentBody();
$engine = id(new PhabricatorMarkupEngine())
->setViewer($viewer);
$engine->addObject(
$document_body,
LegalpadDocumentBody::MARKUP_FIELD_TEXT);
$engine->process();
$document_markup = $engine->getOutput(
$document_body,
LegalpadDocumentBody::MARKUP_FIELD_TEXT);
$title = $document_body->getTitle();
$manage_uri = $this->getApplicationURI('view/'.$document->getID().'/');
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$document,
PhabricatorPolicyCapability::CAN_EDIT);
$header = id(new PHUIHeaderView())
->setHeader($title)
->setUser($viewer)
->setPolicyObject($document)
->setEpoch($document->getDateModified())
->addActionLink(
id(new PHUIButtonView())
->setTag('a')
->setIcon(
id(new PHUIIconView())
->setIconFont('fa-pencil'))
->setText(pht('Manage Document'))
->setHref($manage_uri)
->setDisabled(!$can_edit)
->setWorkflow(!$can_edit));
$preamble = null;
if (strlen($document->getPreamble())) {
$preamble_text = PhabricatorMarkupEngine::renderOneObject(
id(new PhabricatorMarkupOneOff())->setContent(
$document->getPreamble()),
'default',
$viewer);
$preamble = id(new PHUIPropertyListView())
->addSectionHeader(pht('Preamble'))
->addTextContent($preamble_text);
}
$content = id(new PHUIDocumentView())
->addClass('legalpad')
->setHeader($header)
->setFontKit(PHUIDocumentView::FONT_SOURCE_SANS)
->appendChild(
array(
$signed_status,
$preamble,
$document_markup,
));
if (!$has_signed) {
$error_view = null;
if ($errors) {
$error_view = id(new PHUIInfoView())
->setErrors($errors);
}
$signature_form = $this->buildSignatureForm(
$document,
$signature,
$field_errors);
switch ($document->getSignatureType()) {
case LegalpadDocument::SIGNATURE_TYPE_NONE:
$subheader = null;
break;
case LegalpadDocument::SIGNATURE_TYPE_INDIVIDUAL:
case LegalpadDocument::SIGNATURE_TYPE_CORPORATION:
$subheader = id(new PHUIHeaderView())
->setHeader(pht('Agree and Sign Document'))
->setBleedHeader(true);
break;
}
$content->appendChild(
array(
$subheader,
$error_view,
$signature_form,
));
}
$crumbs = $this->buildApplicationCrumbs();
$crumbs->setBorder(true);
$crumbs->addTextCrumb($document->getMonogram());
return $this->buildApplicationPage(
array(
$crumbs,
$content,
),
array(
'title' => $title,
'pageObjects' => array($document->getPHID()),
));
}
private function readSignerInformation(
LegalpadDocument $document,
AphrontRequest $request) {
$viewer = $request->getUser();
$signer_phid = null;
$signature_data = array();
switch ($document->getSignatureType()) {
case LegalpadDocument::SIGNATURE_TYPE_NONE:
break;
case LegalpadDocument::SIGNATURE_TYPE_INDIVIDUAL:
if ($viewer->isLoggedIn()) {
$signer_phid = $viewer->getPHID();
$signature_data = array(
'name' => $viewer->getRealName(),
'email' => $viewer->loadPrimaryEmailAddress(),
);
} else if ($request->isFormPost()) {
$email = new PhutilEmailAddress($request->getStr('email'));
if (strlen($email->getDomainName())) {
$email_obj = id(new PhabricatorUserEmail())
->loadOneWhere('address = %s', $email->getAddress());
if ($email_obj) {
return $this->signInResponse();
}
$external_account = id(new PhabricatorExternalAccountQuery())
->setViewer($viewer)
->withAccountTypes(array('email'))
->withAccountDomains(array($email->getDomainName()))
->withAccountIDs(array($email->getAddress()))
->loadOneOrCreate();
if ($external_account->getUserPHID()) {
return $this->signInResponse();
}
$signer_phid = $external_account->getPHID();
}
}
break;
case LegalpadDocument::SIGNATURE_TYPE_CORPORATION:
$signer_phid = $viewer->getPHID();
if ($signer_phid) {
$signature_data = array(
'contact.name' => $viewer->getRealName(),
'email' => $viewer->loadPrimaryEmailAddress(),
'actorPHID' => $viewer->getPHID(),
);
}
break;
}
return array($signer_phid, $signature_data);
}
private function buildSignatureForm(
LegalpadDocument $document,
LegalpadDocumentSignature $signature,
array $errors) {
$viewer = $this->getRequest()->getUser();
$data = $signature->getSignatureData();
$form = id(new AphrontFormView())
->setUser($viewer);
$signature_type = $document->getSignatureType();
switch ($signature_type) {
case LegalpadDocument::SIGNATURE_TYPE_NONE:
// bail out of here quick
return;
case LegalpadDocument::SIGNATURE_TYPE_INDIVIDUAL:
$this->buildIndividualSignatureForm(
$form,
$document,
$signature,
$errors);
break;
case LegalpadDocument::SIGNATURE_TYPE_CORPORATION:
$this->buildCorporateSignatureForm(
$form,
$document,
$signature,
$errors);
break;
default:
throw new Exception(
pht(
'This document has an unknown signature type ("%s").',
$signature_type));
}
$form
->appendChild(
id(new AphrontFormCheckboxControl())
->setError(idx($errors, 'agree', null))
->addCheckbox(
'agree',
'agree',
pht('I agree to the terms laid forth above.'),
false));
if ($document->getRequireSignature()) {
$cancel_uri = '/logout/';
$cancel_text = pht('Log Out');
} else {
$cancel_uri = $this->getApplicationURI();
$cancel_text = pht('Cancel');
}
$form
->appendChild(
id(new AphrontFormSubmitControl())
->setValue(pht('Sign Document'))
->addCancelButton($cancel_uri, $cancel_text));
return $form;
}
private function buildIndividualSignatureForm(
AphrontFormView $form,
LegalpadDocument $document,
LegalpadDocumentSignature $signature,
array $errors) {
$data = $signature->getSignatureData();
$form
->appendChild(
id(new AphrontFormTextControl())
->setLabel(pht('Name'))
->setValue(idx($data, 'name', ''))
->setName('name')
->setError(idx($errors, 'name', null)));
$viewer = $this->getRequest()->getUser();
if (!$viewer->isLoggedIn()) {
$form->appendChild(
id(new AphrontFormTextControl())
->setLabel(pht('Email'))
->setValue(idx($data, 'email', ''))
->setName('email')
->setError(idx($errors, 'email', null)));
}
return $form;
}
private function buildCorporateSignatureForm(
AphrontFormView $form,
LegalpadDocument $document,
LegalpadDocumentSignature $signature,
array $errors) {
$data = $signature->getSignatureData();
$form
->appendChild(
id(new AphrontFormTextControl())
->setLabel(pht('Company Name'))
->setValue(idx($data, 'name', ''))
->setName('name')
->setError(idx($errors, 'name', null)))
->appendChild(
id(new AphrontFormTextAreaControl())
->setLabel(pht('Company Address'))
->setValue(idx($data, 'address', ''))
->setName('address')
->setError(idx($errors, 'address', null)))
->appendChild(
id(new AphrontFormTextControl())
->setLabel(pht('Contact Name'))
->setValue(idx($data, 'contact.name', ''))
->setName('contact.name')
->setError(idx($errors, 'contact.name', null)))
->appendChild(
id(new AphrontFormTextControl())
->setLabel(pht('Contact Email'))
->setValue(idx($data, 'email', ''))
->setName('email')
->setError(idx($errors, 'email', null)));
return $form;
}
private function readSignatureForm(
LegalpadDocument $document,
AphrontRequest $request) {
$signature_type = $document->getSignatureType();
switch ($signature_type) {
case LegalpadDocument::SIGNATURE_TYPE_INDIVIDUAL:
$result = $this->readIndividualSignatureForm(
$document,
$request);
break;
case LegalpadDocument::SIGNATURE_TYPE_CORPORATION:
$result = $this->readCorporateSignatureForm(
$document,
$request);
break;
default:
throw new Exception(
pht(
'This document has an unknown signature type ("%s").',
$signature_type));
}
return $result;
}
private function readIndividualSignatureForm(
LegalpadDocument $document,
AphrontRequest $request) {
$signature_data = array();
$errors = array();
$field_errors = array();
$name = $request->getStr('name');
if (!strlen($name)) {
$field_errors['name'] = pht('Required');
$errors[] = pht('Name field is required.');
} else {
$field_errors['name'] = null;
}
$signature_data['name'] = $name;
$viewer = $request->getUser();
if ($viewer->isLoggedIn()) {
$email = $viewer->loadPrimaryEmailAddress();
} else {
$email = $request->getStr('email');
$addr_obj = null;
if (!strlen($email)) {
$field_errors['email'] = pht('Required');
$errors[] = pht('Email field is required.');
} else {
$addr_obj = new PhutilEmailAddress($email);
$domain = $addr_obj->getDomainName();
if (!$domain) {
$field_errors['email'] = pht('Invalid');
$errors[] = pht('A valid email is required.');
} else {
$field_errors['email'] = null;
}
}
}
$signature_data['email'] = $email;
return array($signature_data, $errors, $field_errors);
}
private function readCorporateSignatureForm(
LegalpadDocument $document,
AphrontRequest $request) {
$viewer = $request->getUser();
if (!$viewer->isLoggedIn()) {
throw new Exception(
pht(
'You can not sign a document on behalf of a corporation unless '.
'you are logged in.'));
}
$signature_data = array();
$errors = array();
$field_errors = array();
$name = $request->getStr('name');
if (!strlen($name)) {
$field_errors['name'] = pht('Required');
$errors[] = pht('Company name is required.');
} else {
$field_errors['name'] = null;
}
$signature_data['name'] = $name;
$address = $request->getStr('address');
if (!strlen($address)) {
$field_errors['address'] = pht('Required');
$errors[] = pht('Company address is required.');
} else {
$field_errors['address'] = null;
}
$signature_data['address'] = $address;
$contact_name = $request->getStr('contact.name');
if (!strlen($contact_name)) {
$field_errors['contact.name'] = pht('Required');
$errors[] = pht('Contact name is required.');
} else {
$field_errors['contact.name'] = null;
}
$signature_data['contact.name'] = $contact_name;
$email = $request->getStr('email');
$addr_obj = null;
if (!strlen($email)) {
$field_errors['email'] = pht('Required');
$errors[] = pht('Contact email is required.');
} else {
$addr_obj = new PhutilEmailAddress($email);
$domain = $addr_obj->getDomainName();
if (!$domain) {
$field_errors['email'] = pht('Invalid');
$errors[] = pht('A valid email is required.');
} else {
$field_errors['email'] = null;
}
}
$signature_data['email'] = $email;
return array($signature_data, $errors, $field_errors);
}
private function sendVerifySignatureEmail(
LegalpadDocument $doc,
LegalpadDocumentSignature $signature) {
$signature_data = $signature->getSignatureData();
$email = new PhutilEmailAddress($signature_data['email']);
$doc_name = $doc->getTitle();
$doc_link = PhabricatorEnv::getProductionURI('/'.$doc->getMonogram());
$path = $this->getApplicationURI(sprintf(
'/verify/%s/',
$signature->getSecretKey()));
$link = PhabricatorEnv::getProductionURI($path);
$name = idx($signature_data, 'name');
- $body = <<<EOBODY
-{$name}:
-
-This email address was used to sign a Legalpad document in Phabricator:
-
- {$doc_name}
-
-Please verify you own this email address and accept the agreement by clicking
-this link:
-
- {$link}
-
-Your signature is not valid until you complete this verification step.
-
-You can review the document here:
-
- {$doc_link}
-
-EOBODY;
+ $body = pht(
+ "%s:\n\n".
+ "This email address was used to sign a Legalpad document ".
+ "in Phabricator:\n\n".
+ " %s\n\n".
+ "Please verify you own this email address and accept the ".
+ "agreement by clicking this link:\n\n".
+ " %s\n\n".
+ "Your signature is not valid until you complete this ".
+ "verification step.\n\nYou can review the document here:\n\n".
+ " %s\n",
+ $name,
+ $doc_name,
+ $link,
+ $doc_link);
id(new PhabricatorMetaMTAMail())
->addRawTos(array($email->getAddress()))
->setSubject(pht('[Legalpad] Signature Verification'))
->setForceDelivery(true)
->setBody($body)
->setRelatedPHID($signature->getDocumentPHID())
->saveAndSend();
}
private function signInResponse() {
return id(new Aphront403Response())
- ->setForbiddenText(pht(
- 'The email address specified is associated with an account. '.
- 'Please login to that account and sign this document again.'));
+ ->setForbiddenText(
+ pht(
+ 'The email address specified is associated with an account. '.
+ 'Please login to that account and sign this document again.'));
}
}
diff --git a/src/applications/legalpad/controller/LegalpadDocumentSignatureVerificationController.php b/src/applications/legalpad/controller/LegalpadDocumentSignatureVerificationController.php
index 7ad5d1394..a940eb646 100644
--- a/src/applications/legalpad/controller/LegalpadDocumentSignatureVerificationController.php
+++ b/src/applications/legalpad/controller/LegalpadDocumentSignatureVerificationController.php
@@ -1,99 +1,97 @@
<?php
final class LegalpadDocumentSignatureVerificationController
extends LegalpadController {
private $code;
public function shouldAllowPublic() {
return true;
}
public function willProcessRequest(array $data) {
$this->code = $data['code'];
}
public function processRequest() {
$request = $this->getRequest();
$viewer = $request->getUser();
// NOTE: We're using the omnipotent user to handle logged-out signatures
// and corporate signatures.
$signature = id(new LegalpadDocumentSignatureQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withSecretKeys(array($this->code))
->executeOne();
if (!$signature) {
return $this->newDialog()
->setTitle(pht('Unable to Verify Signature'))
->appendParagraph(
pht(
'The signature verification code is incorrect, or the signature '.
'has been invalidated. Make sure you followed the link in the '.
'email correctly.'))
->addCancelButton('/', pht('Rats!'));
}
if ($signature->isVerified()) {
return $this->newDialog()
->setTitle(pht('Signature Already Verified'))
- ->appendParagraph(
- pht(
- 'This signature has already been verified.'))
+ ->appendParagraph(pht('This signature has already been verified.'))
->addCancelButton('/', pht('Okay'));
}
if ($request->isFormPost()) {
$signature
->setVerified(LegalpadDocumentSignature::VERIFIED)
->save();
return $this->newDialog()
->setTitle(pht('Signature Verified'))
->appendParagraph(pht('The signature is now verified.'))
->addCancelButton('/', pht('Okay'));
}
$document_link = phutil_tag(
'a',
array(
'href' => '/'.$signature->getDocument()->getMonogram(),
'target' => '_blank',
),
$signature->getDocument()->getTitle());
$signed_at = phabricator_datetime($signature->getDateCreated(), $viewer);
$name = $signature->getSignerName();
$email = $signature->getSignerEmail();
$form = id(new AphrontFormView())
->setUser($viewer)
->appendRemarkupInstructions(
pht('Please verify this document signature.'))
->appendChild(
id(new AphrontFormMarkupControl())
->setLabel(pht('Document'))
->setValue($document_link))
->appendChild(
id(new AphrontFormMarkupControl())
->setLabel(pht('Signed At'))
->setValue($signed_at))
->appendChild(
id(new AphrontFormMarkupControl())
->setLabel(pht('Name'))
->setValue($name))
->appendChild(
id(new AphrontFormMarkupControl())
->setLabel(pht('Email'))
->setValue($email));
return $this->newDialog()
->setTitle(pht('Verify Signature?'))
->appendChild($form->buildLayoutView())
->addCancelButton('/')
->addSubmitButton(pht('Verify Signature'));
}
}
diff --git a/src/applications/legalpad/mail/LegalpadMailReceiver.php b/src/applications/legalpad/mail/LegalpadMailReceiver.php
index 20296372e..5fe0dfd4e 100644
--- a/src/applications/legalpad/mail/LegalpadMailReceiver.php
+++ b/src/applications/legalpad/mail/LegalpadMailReceiver.php
@@ -1,28 +1,28 @@
<?php
final class LegalpadMailReceiver extends PhabricatorObjectMailReceiver {
public function isEnabled() {
- $app_class = 'PhabricatorLegalpadApplication';
- return PhabricatorApplication::isClassInstalled($app_class);
+ return PhabricatorApplication::isClassInstalled(
+ 'PhabricatorLegalpadApplication');
}
protected function getObjectPattern() {
return 'L[1-9]\d*';
}
protected function loadObject($pattern, PhabricatorUser $viewer) {
$id = (int)trim($pattern, 'L');
return id(new LegalpadDocumentQuery())
->setViewer($viewer)
->withIDs(array($id))
->needDocumentBodies(true)
->executeOne();
}
protected function getTransactionReplyHandler() {
return new LegalpadReplyHandler();
}
}
diff --git a/src/applications/legalpad/mail/LegalpadReplyHandler.php b/src/applications/legalpad/mail/LegalpadReplyHandler.php
index 8063d4ec2..103f5a6cb 100644
--- a/src/applications/legalpad/mail/LegalpadReplyHandler.php
+++ b/src/applications/legalpad/mail/LegalpadReplyHandler.php
@@ -1,16 +1,16 @@
<?php
final class LegalpadReplyHandler
extends PhabricatorApplicationTransactionReplyHandler {
public function validateMailReceiver($mail_receiver) {
if (!($mail_receiver instanceof LegalpadDocument)) {
- throw new Exception('Mail receiver is not a LegalpadDocument!');
+ throw new Exception(pht('Mail receiver is not a LegalpadDocument!'));
}
}
public function getObjectPrefix() {
return 'L';
}
}
diff --git a/src/applications/legalpad/query/LegalpadDocumentSearchEngine.php b/src/applications/legalpad/query/LegalpadDocumentSearchEngine.php
index 84abe3fdd..9f404515b 100644
--- a/src/applications/legalpad/query/LegalpadDocumentSearchEngine.php
+++ b/src/applications/legalpad/query/LegalpadDocumentSearchEngine.php
@@ -1,216 +1,215 @@
<?php
final class LegalpadDocumentSearchEngine
extends PhabricatorApplicationSearchEngine {
public function getResultTypeDescription() {
return pht('Legalpad Documents');
}
public function getApplicationClassName() {
return 'PhabricatorLegalpadApplication';
}
public function buildSavedQueryFromRequest(AphrontRequest $request) {
$saved = new PhabricatorSavedQuery();
$saved->setParameter(
'creatorPHIDs',
$this->readUsersFromRequest($request, 'creators'));
$saved->setParameter(
'contributorPHIDs',
$this->readUsersFromRequest($request, 'contributors'));
$saved->setParameter(
'withViewerSignature',
$request->getBool('withViewerSignature'));
$saved->setParameter('createdStart', $request->getStr('createdStart'));
$saved->setParameter('createdEnd', $request->getStr('createdEnd'));
return $saved;
}
public function buildQueryFromSavedQuery(PhabricatorSavedQuery $saved) {
$query = id(new LegalpadDocumentQuery())
->needViewerSignatures(true);
$creator_phids = $saved->getParameter('creatorPHIDs', array());
if ($creator_phids) {
$query->withCreatorPHIDs($creator_phids);
}
$contributor_phids = $saved->getParameter('contributorPHIDs', array());
if ($contributor_phids) {
$query->withContributorPHIDs($contributor_phids);
}
if ($saved->getParameter('withViewerSignature')) {
$viewer_phid = $this->requireViewer()->getPHID();
if ($viewer_phid) {
$query->withSignerPHIDs(array($viewer_phid));
}
}
$start = $this->parseDateTime($saved->getParameter('createdStart'));
$end = $this->parseDateTime($saved->getParameter('createdEnd'));
if ($start) {
$query->withDateCreatedAfter($start);
}
if ($end) {
$query->withDateCreatedBefore($end);
}
return $query;
}
public function buildSearchForm(
AphrontFormView $form,
PhabricatorSavedQuery $saved_query) {
$creator_phids = $saved_query->getParameter('creatorPHIDs', array());
$contributor_phids = $saved_query->getParameter(
'contributorPHIDs', array());
$viewer_signature = $saved_query->getParameter('withViewerSignature');
if (!$this->requireViewer()->getPHID()) {
$viewer_signature = false;
}
$form
->appendChild(
id(new AphrontFormCheckboxControl())
->addCheckbox(
'withViewerSignature',
1,
pht('Show only documents I have signed.'),
$viewer_signature)
->setDisabled(!$this->requireViewer()->getPHID()))
->appendControl(
id(new AphrontFormTokenizerControl())
->setDatasource(new PhabricatorPeopleDatasource())
->setName('creators')
->setLabel(pht('Creators'))
->setValue($creator_phids))
->appendControl(
id(new AphrontFormTokenizerControl())
->setDatasource(new PhabricatorPeopleDatasource())
->setName('contributors')
->setLabel(pht('Contributors'))
->setValue($contributor_phids));
$this->buildDateRange(
$form,
$saved_query,
'createdStart',
pht('Created After'),
'createdEnd',
pht('Created Before'));
-
}
protected function getURI($path) {
return '/legalpad/'.$path;
}
protected function getBuiltinQueryNames() {
$names = array();
if ($this->requireViewer()->isLoggedIn()) {
$names['signed'] = pht('Signed Documents');
}
$names['all'] = pht('All Documents');
return $names;
}
public function buildSavedQueryFromBuiltin($query_key) {
$query = $this->newSavedQuery();
$query->setQueryKey($query_key);
switch ($query_key) {
case 'signed':
return $query
->setParameter('withViewerSignature', true);
case 'all':
return $query;
}
return parent::buildSavedQueryFromBuiltin($query_key);
}
protected function getRequiredHandlePHIDsForResultList(
array $documents,
PhabricatorSavedQuery $query) {
return array();
}
protected function renderResultList(
array $documents,
PhabricatorSavedQuery $query,
array $handles) {
assert_instances_of($documents, 'LegalpadDocument');
$viewer = $this->requireViewer();
$list = new PHUIObjectItemListView();
$list->setUser($viewer);
foreach ($documents as $document) {
$last_updated = phabricator_date($document->getDateModified(), $viewer);
$title = $document->getTitle();
$item = id(new PHUIObjectItemView())
->setObjectName($document->getMonogram())
->setHeader($title)
->setHref('/'.$document->getMonogram())
->setObject($document);
$no_signatures = LegalpadDocument::SIGNATURE_TYPE_NONE;
if ($document->getSignatureType() == $no_signatures) {
$item->addIcon('none', pht('Not Signable'));
} else {
$type_name = $document->getSignatureTypeName();
$type_icon = $document->getSignatureTypeIcon();
$item->addIcon($type_icon, $type_name);
if ($viewer->getPHID()) {
$signature = $document->getUserSignature($viewer->getPHID());
} else {
$signature = null;
}
if ($signature) {
$item->addAttribute(
array(
id(new PHUIIconView())->setIconFont('fa-check-square-o', 'green'),
' ',
pht(
'Signed on %s',
phabricator_date($signature->getDateCreated(), $viewer)),
));
} else {
$item->addAttribute(
array(
id(new PHUIIconView())->setIconFont('fa-square-o', 'grey'),
' ',
pht('Not Signed'),
));
}
}
$item->addIcon(
'fa-pencil grey',
pht('Version %d (%s)', $document->getVersions(), $last_updated));
$list->addItem($item);
}
return $list;
}
}
diff --git a/src/applications/legalpad/storage/LegalpadDocument.php b/src/applications/legalpad/storage/LegalpadDocument.php
index d750c3689..4c6e04a26 100644
--- a/src/applications/legalpad/storage/LegalpadDocument.php
+++ b/src/applications/legalpad/storage/LegalpadDocument.php
@@ -1,259 +1,258 @@
<?php
final class LegalpadDocument extends LegalpadDAO
implements
PhabricatorPolicyInterface,
PhabricatorSubscribableInterface,
PhabricatorApplicationTransactionInterface,
PhabricatorDestructibleInterface {
protected $title;
protected $contributorCount;
protected $recentContributorPHIDs = array();
protected $creatorPHID;
protected $versions;
protected $documentBodyPHID;
protected $viewPolicy;
protected $editPolicy;
protected $mailKey;
protected $signatureType;
protected $preamble;
protected $requireSignature;
const SIGNATURE_TYPE_NONE = 'none';
const SIGNATURE_TYPE_INDIVIDUAL = 'user';
const SIGNATURE_TYPE_CORPORATION = 'corp';
private $documentBody = self::ATTACHABLE;
private $contributors = self::ATTACHABLE;
private $signatures = self::ATTACHABLE;
private $userSignatures = array();
public static function initializeNewDocument(PhabricatorUser $actor) {
$app = id(new PhabricatorApplicationQuery())
->setViewer($actor)
->withClasses(array('PhabricatorLegalpadApplication'))
->executeOne();
$view_policy = $app->getPolicy(LegalpadDefaultViewCapability::CAPABILITY);
$edit_policy = $app->getPolicy(LegalpadDefaultEditCapability::CAPABILITY);
return id(new LegalpadDocument())
->setVersions(0)
->setCreatorPHID($actor->getPHID())
->setContributorCount(0)
->setRecentContributorPHIDs(array())
->attachSignatures(array())
->setSignatureType(self::SIGNATURE_TYPE_INDIVIDUAL)
->setPreamble('')
->setRequireSignature(0)
->setViewPolicy($view_policy)
->setEditPolicy($edit_policy);
}
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'recentContributorPHIDs' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'title' => 'text255',
'contributorCount' => 'uint32',
'versions' => 'uint32',
'mailKey' => 'bytes20',
'signatureType' => 'text4',
'preamble' => 'text',
'requireSignature' => 'bool',
),
self::CONFIG_KEY_SCHEMA => array(
'key_creator' => array(
'columns' => array('creatorPHID', 'dateModified'),
),
'key_required' => array(
'columns' => array('requireSignature', 'dateModified'),
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhabricatorLegalpadDocumentPHIDType::TYPECONST);
}
public function getDocumentBody() {
return $this->assertAttached($this->documentBody);
}
public function attachDocumentBody(LegalpadDocumentBody $body) {
$this->documentBody = $body;
return $this;
}
public function getContributors() {
return $this->assertAttached($this->contributors);
}
public function attachContributors(array $contributors) {
$this->contributors = $contributors;
return $this;
}
public function getSignatures() {
return $this->assertAttached($this->signatures);
}
public function attachSignatures(array $signatures) {
$this->signatures = $signatures;
return $this;
}
public function save() {
if (!$this->getMailKey()) {
$this->setMailKey(Filesystem::readRandomCharacters(20));
}
return parent::save();
}
public function getMonogram() {
return 'L'.$this->getID();
}
public function getUserSignature($phid) {
return $this->assertAttachedKey($this->userSignatures, $phid);
}
public function attachUserSignature(
$user_phid,
LegalpadDocumentSignature $signature = null) {
$this->userSignatures[$user_phid] = $signature;
return $this;
}
public static function getSignatureTypeMap() {
return array(
self::SIGNATURE_TYPE_INDIVIDUAL => pht('Individuals'),
self::SIGNATURE_TYPE_CORPORATION => pht('Corporations'),
self::SIGNATURE_TYPE_NONE => pht('No One'),
);
}
public function getSignatureTypeName() {
$type = $this->getSignatureType();
return idx(self::getSignatureTypeMap(), $type, $type);
}
public function getSignatureTypeIcon() {
$type = $this->getSignatureType();
$map = array(
self::SIGNATURE_TYPE_NONE => '',
self::SIGNATURE_TYPE_INDIVIDUAL => 'fa-user grey',
self::SIGNATURE_TYPE_CORPORATION => 'fa-building-o grey',
);
return idx($map, $type, 'fa-user grey');
}
/* -( PhabricatorSubscribableInterface )----------------------------------- */
public function isAutomaticallySubscribed($phid) {
return ($this->creatorPHID == $phid);
}
public function shouldShowSubscribersProperty() {
return true;
}
public function shouldAllowSubscription($phid) {
return true;
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
$policy = $this->viewPolicy;
break;
case PhabricatorPolicyCapability::CAN_EDIT:
$policy = $this->editPolicy;
break;
default:
$policy = PhabricatorPolicies::POLICY_NOONE;
break;
}
return $policy;
}
public function hasAutomaticCapability($capability, PhabricatorUser $user) {
return ($user->getPHID() == $this->getCreatorPHID());
}
public function describeAutomaticCapability($capability) {
- return pht(
- 'The author of a document can always view and edit it.');
+ return pht('The author of a document can always view and edit it.');
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new LegalpadDocumentEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new LegalpadTransaction();
}
public function willRenderTimeline(
PhabricatorApplicationTransactionView $timeline,
AphrontRequest $request) {
return $timeline;
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$this->openTransaction();
$this->delete();
$bodies = id(new LegalpadDocumentBody())->loadAllWhere(
'documentPHID = %s',
$this->getPHID());
foreach ($bodies as $body) {
$body->delete();
}
$signatures = id(new LegalpadDocumentSignature())->loadAllWhere(
'documentPHID = %s',
$this->getPHID());
foreach ($signatures as $signature) {
$signature->delete();
}
$this->saveTransaction();
}
}
diff --git a/src/applications/legalpad/storage/LegalpadDocumentBody.php b/src/applications/legalpad/storage/LegalpadDocumentBody.php
index 1533f4165..a55583428 100644
--- a/src/applications/legalpad/storage/LegalpadDocumentBody.php
+++ b/src/applications/legalpad/storage/LegalpadDocumentBody.php
@@ -1,80 +1,80 @@
<?php
final class LegalpadDocumentBody extends LegalpadDAO
implements
PhabricatorMarkupInterface {
const MARKUP_FIELD_TEXT = 'markup:text ';
protected $phid;
protected $creatorPHID;
protected $documentPHID;
protected $version;
protected $title;
protected $text;
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_COLUMN_SCHEMA => array(
'version' => 'uint32',
'title' => 'text255',
'text' => 'text?',
),
self::CONFIG_KEY_SCHEMA => array(
'key_document' => array(
'columns' => array('documentPHID', 'version'),
'unique' => true,
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhabricatorPHIDConstants::PHID_TYPE_LEGB);
}
/* -( PhabricatorMarkupInterface )----------------------------------------- */
public function getMarkupFieldKey($field) {
$hash = PhabricatorHash::digest($this->getMarkupText($field));
return 'LEGB:'.$hash;
}
public function newMarkupEngine($field) {
return PhabricatorMarkupEngine::newMarkupEngine(array());
}
public function getMarkupText($field) {
switch ($field) {
case self::MARKUP_FIELD_TEXT:
$text = $this->getText();
break;
case self::MARKUP_FIELD_TITLE:
$text = $this->getTitle();
break;
default:
- throw new Exception('Unknown field: '.$field);
+ throw new Exception(pht('Unknown field: %s', $field));
break;
}
return $text;
}
public function didMarkupText($field, $output, PhutilMarkupEngine $engine) {
require_celerity_resource('phabricator-remarkup-css');
return phutil_tag(
'div',
array(
'class' => 'phabricator-remarkup',
),
$output);
}
public function shouldUseMarkupCache($field) {
return (bool)$this->getID();
}
}
diff --git a/src/applications/lipsum/generator/PhabricatorTestDataGenerator.php b/src/applications/lipsum/generator/PhabricatorTestDataGenerator.php
index 45eca56c7..9adf6fca7 100644
--- a/src/applications/lipsum/generator/PhabricatorTestDataGenerator.php
+++ b/src/applications/lipsum/generator/PhabricatorTestDataGenerator.php
@@ -1,28 +1,30 @@
<?php
abstract class PhabricatorTestDataGenerator {
public function generate() {
return;
}
public function loadOneRandom($classname) {
try {
return newv($classname, array())
->loadOneWhere('1 = 1 ORDER BY RAND() LIMIT 1');
} catch (PhutilMissingSymbolException $ex) {
throw new PhutilMissingSymbolException(
- 'Unable to load symbol '.$classname.': this class does not exit.');
+ pht(
+ 'Unable to load symbol %s: this class does not exit.',
+ $classname));
}
}
public function loadPhabrictorUserPHID() {
return $this->loadOneRandom('PhabricatorUser')->getPHID();
}
public function loadPhabrictorUser() {
return $this->loadOneRandom('PhabricatorUser');
}
}
diff --git a/src/applications/macro/conduit/MacroCreateMemeConduitAPIMethod.php b/src/applications/macro/conduit/MacroCreateMemeConduitAPIMethod.php
index 308f1c3b3..e63d831a7 100644
--- a/src/applications/macro/conduit/MacroCreateMemeConduitAPIMethod.php
+++ b/src/applications/macro/conduit/MacroCreateMemeConduitAPIMethod.php
@@ -1,57 +1,57 @@
<?php
final class MacroCreateMemeConduitAPIMethod extends MacroConduitAPIMethod {
public function getAPIMethodName() {
return 'macro.creatememe';
}
public function getMethodStatus() {
return self::METHOD_STATUS_UNSTABLE;
}
public function getMethodDescription() {
return pht('Generate a meme.');
}
protected function defineParamTypes() {
return array(
'macroName' => 'string',
'upperText' => 'optional string',
'lowerText' => 'optional string',
);
}
protected function defineReturnType() {
return 'string';
}
protected function defineErrorTypes() {
return array(
- 'ERR-NOT-FOUND' => 'Macro was not found.',
+ 'ERR-NOT-FOUND' => pht('Macro was not found.'),
);
}
protected function execute(ConduitAPIRequest $request) {
$user = $request->getUser();
$macro_name = $request->getValue('macroName');
$upper_text = $request->getValue('upperText');
$lower_text = $request->getValue('lowerText');
$uri = PhabricatorMacroMemeController::generateMacro(
$user,
$macro_name,
$upper_text,
$lower_text);
if (!$uri) {
throw new ConduitException('ERR-NOT-FOUND');
}
return array(
'uri' => $uri,
);
}
}
diff --git a/src/applications/macro/conduit/MacroQueryConduitAPIMethod.php b/src/applications/macro/conduit/MacroQueryConduitAPIMethod.php
index 66cdedeeb..44f54a3c8 100644
--- a/src/applications/macro/conduit/MacroQueryConduitAPIMethod.php
+++ b/src/applications/macro/conduit/MacroQueryConduitAPIMethod.php
@@ -1,79 +1,79 @@
<?php
final class MacroQueryConduitAPIMethod extends MacroConduitAPIMethod {
public function getAPIMethodName() {
return 'macro.query';
}
public function getMethodDescription() {
- return 'Retrieve image macro information.';
+ return pht('Retrieve image macro information.');
}
protected function defineParamTypes() {
return array(
'authorPHIDs' => 'optional list<phid>',
'phids' => 'optional list<phid>',
'ids' => 'optional list<id>',
'names' => 'optional list<string>',
'nameLike' => 'optional string',
);
}
protected function defineReturnType() {
return 'list<dict>';
}
protected function execute(ConduitAPIRequest $request) {
$query = id(new PhabricatorMacroQuery())
->setViewer($request->getUser())
->needFiles(true);
$author_phids = $request->getValue('authorPHIDs');
$phids = $request->getValue('phids');
$ids = $request->getValue('ids');
$name_like = $request->getValue('nameLike');
$names = $request->getValue('names');
if ($author_phids) {
$query->withAuthorPHIDs($author_phids);
}
if ($phids) {
$query->withPHIDs($phids);
}
if ($ids) {
$query->withIDs($ids);
}
if ($name_like) {
$query->withNameLike($name_like);
}
if ($names) {
$query->withNames($names);
}
$macros = $query->execute();
if (!$macros) {
return array();
}
$results = array();
foreach ($macros as $macro) {
$file = $macro->getFile();
$results[$macro->getName()] = array(
'uri' => $file->getBestURI(),
'phid' => $macro->getPHID(),
'authorPHID' => $file->getAuthorPHID(),
'dateCreated' => $file->getDateCreated(),
'filePHID' => $file->getPHID(),
);
}
return $results;
}
}
diff --git a/src/applications/macro/controller/PhabricatorMacroDisableController.php b/src/applications/macro/controller/PhabricatorMacroDisableController.php
index 8afc89df9..b75e67e1f 100644
--- a/src/applications/macro/controller/PhabricatorMacroDisableController.php
+++ b/src/applications/macro/controller/PhabricatorMacroDisableController.php
@@ -1,58 +1,62 @@
<?php
final class PhabricatorMacroDisableController
extends PhabricatorMacroController {
private $id;
public function willProcessRequest(array $data) {
$this->id = $data['id'];
}
public function processRequest() {
$this->requireApplicationCapability(
PhabricatorMacroManageCapability::CAPABILITY);
$request = $this->getRequest();
$user = $request->getUser();
$macro = id(new PhabricatorMacroQuery())
->setViewer($user)
->withIDs(array($this->id))
->executeOne();
if (!$macro) {
return new Aphront404Response();
}
$view_uri = $this->getApplicationURI('/view/'.$this->id.'/');
if ($request->isDialogFormPost() || $macro->getIsDisabled()) {
$xaction = id(new PhabricatorMacroTransaction())
->setTransactionType(PhabricatorMacroTransactionType::TYPE_DISABLED)
->setNewValue($macro->getIsDisabled() ? 0 : 1);
$editor = id(new PhabricatorMacroEditor())
->setActor($user)
->setContentSourceFromRequest($request);
$xactions = $editor->applyTransactions($macro, array($xaction));
return id(new AphrontRedirectResponse())->setURI($view_uri);
}
$dialog = new AphrontDialogView();
$dialog
->setUser($request->getUser())
->setTitle(pht('Really disable macro?'))
- ->appendChild(phutil_tag('p', array(), pht(
- 'Really disable the much-beloved image macro %s? '.
- 'It will be sorely missed.',
- $macro->getName())))
+ ->appendChild(
+ phutil_tag(
+ 'p',
+ array(),
+ pht(
+ 'Really disable the much-beloved image macro %s? '.
+ 'It will be sorely missed.',
+ $macro->getName())))
->setSubmitURI($this->getApplicationURI('/disable/'.$this->id.'/'))
->addSubmitButton(pht('Disable'))
->addCancelButton($view_uri);
return id(new AphrontDialogResponse())->setDialog($dialog);
}
}
diff --git a/src/applications/macro/mail/PhabricatorMacroMailReceiver.php b/src/applications/macro/mail/PhabricatorMacroMailReceiver.php
index 30f562e4b..222524a37 100644
--- a/src/applications/macro/mail/PhabricatorMacroMailReceiver.php
+++ b/src/applications/macro/mail/PhabricatorMacroMailReceiver.php
@@ -1,27 +1,27 @@
<?php
final class PhabricatorMacroMailReceiver extends PhabricatorObjectMailReceiver {
public function isEnabled() {
- $app_class = 'PhabricatorManiphestApplication';
- return PhabricatorApplication::isClassInstalled($app_class);
+ return PhabricatorApplication::isClassInstalled(
+ 'PhabricatorManiphestApplication');
}
protected function getObjectPattern() {
return 'MCRO[1-9]\d*';
}
protected function loadObject($pattern, PhabricatorUser $viewer) {
$id = (int)substr($pattern, 4);
return id(new PhabricatorMacroQuery())
->setViewer($viewer)
->withIDs(array($id))
->executeOne();
}
protected function getTransactionReplyHandler() {
return new PhabricatorMacroReplyHandler();
}
}
diff --git a/src/applications/macro/mail/PhabricatorMacroReplyHandler.php b/src/applications/macro/mail/PhabricatorMacroReplyHandler.php
index 5b0d9ce8a..83b5d0a93 100644
--- a/src/applications/macro/mail/PhabricatorMacroReplyHandler.php
+++ b/src/applications/macro/mail/PhabricatorMacroReplyHandler.php
@@ -1,16 +1,17 @@
<?php
final class PhabricatorMacroReplyHandler
extends PhabricatorApplicationTransactionReplyHandler {
public function validateMailReceiver($mail_receiver) {
if (!($mail_receiver instanceof PhabricatorFileImageMacro)) {
- throw new Exception('Mail receiver is not a PhabricatorFileImageMacro!');
+ throw new Exception(
+ pht('Mail receiver is not a %s!', 'PhabricatorFileImageMacro'));
}
}
public function getObjectPrefix() {
return 'MCRO';
}
}
diff --git a/src/applications/macro/query/PhabricatorMacroQuery.php b/src/applications/macro/query/PhabricatorMacroQuery.php
index 7c7074664..d4ffe4610 100644
--- a/src/applications/macro/query/PhabricatorMacroQuery.php
+++ b/src/applications/macro/query/PhabricatorMacroQuery.php
@@ -1,278 +1,278 @@
<?php
final class PhabricatorMacroQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $ids;
private $phids;
private $authors;
private $names;
private $nameLike;
private $namePrefix;
private $dateCreatedAfter;
private $dateCreatedBefore;
private $flagColor;
private $needFiles;
private $status = 'status-any';
const STATUS_ANY = 'status-any';
const STATUS_ACTIVE = 'status-active';
const STATUS_DISABLED = 'status-disabled';
public static function getStatusOptions() {
return array(
self::STATUS_ACTIVE => pht('Active Macros'),
self::STATUS_DISABLED => pht('Disabled Macros'),
self::STATUS_ANY => pht('Active and Disabled Macros'),
);
}
public static function getFlagColorsOptions() {
$options = array(
'-1' => pht('(No Filtering)'),
'-2' => pht('(Marked With Any Flag)'),
);
foreach (PhabricatorFlagColor::getColorNameMap() as $color => $name) {
$options[$color] = $name;
}
return $options;
}
public function withIDs(array $ids) {
$this->ids = $ids;
return $this;
}
public function withPHIDs(array $phids) {
$this->phids = $phids;
return $this;
}
public function withAuthorPHIDs(array $authors) {
$this->authors = $authors;
return $this;
}
public function withNameLike($name) {
$this->nameLike = $name;
return $this;
}
public function withNames(array $names) {
$this->names = $names;
return $this;
}
public function withNamePrefix($prefix) {
$this->namePrefix = $prefix;
return $this;
}
public function withStatus($status) {
$this->status = $status;
return $this;
}
public function withDateCreatedBefore($date_created_before) {
$this->dateCreatedBefore = $date_created_before;
return $this;
}
public function withDateCreatedAfter($date_created_after) {
$this->dateCreatedAfter = $date_created_after;
return $this;
}
public function withFlagColor($flag_color) {
$this->flagColor = $flag_color;
return $this;
}
public function needFiles($need_files) {
$this->needFiles = $need_files;
return $this;
}
protected function loadPage() {
$macro_table = new PhabricatorFileImageMacro();
$conn = $macro_table->establishConnection('r');
$rows = queryfx_all(
$conn,
'SELECT m.* FROM %T m %Q %Q %Q',
$macro_table->getTableName(),
$this->buildWhereClause($conn),
$this->buildOrderClause($conn),
$this->buildLimitClause($conn));
return $macro_table->loadAllFromArray($rows);
}
protected function buildWhereClause(AphrontDatabaseConnection $conn) {
$where = array();
if ($this->ids) {
$where[] = qsprintf(
$conn,
'm.id IN (%Ld)',
$this->ids);
}
if ($this->phids) {
$where[] = qsprintf(
$conn,
'm.phid IN (%Ls)',
$this->phids);
}
if ($this->authors) {
$where[] = qsprintf(
$conn,
'm.authorPHID IN (%Ls)',
$this->authors);
}
if ($this->nameLike) {
$where[] = qsprintf(
$conn,
'm.name LIKE %~',
$this->nameLike);
}
if ($this->names) {
$where[] = qsprintf(
$conn,
'm.name IN (%Ls)',
$this->names);
}
if (strlen($this->namePrefix)) {
$where[] = qsprintf(
$conn,
'm.name LIKE %>',
$this->namePrefix);
}
switch ($this->status) {
case self::STATUS_ACTIVE:
$where[] = qsprintf(
$conn,
'm.isDisabled = 0');
break;
case self::STATUS_DISABLED:
$where[] = qsprintf(
$conn,
'm.isDisabled = 1');
break;
case self::STATUS_ANY:
break;
default:
- throw new Exception("Unknown status '{$this->status}'!");
+ throw new Exception(pht("Unknown status '%s'!", $this->status));
}
if ($this->dateCreatedAfter) {
$where[] = qsprintf(
$conn,
'm.dateCreated >= %d',
$this->dateCreatedAfter);
}
if ($this->dateCreatedBefore) {
$where[] = qsprintf(
$conn,
'm.dateCreated <= %d',
$this->dateCreatedBefore);
}
if ($this->flagColor != '-1' && $this->flagColor !== null) {
if ($this->flagColor == '-2') {
$flag_colors = array_keys(PhabricatorFlagColor::getColorNameMap());
} else {
$flag_colors = array($this->flagColor);
}
$flags = id(new PhabricatorFlagQuery())
->withOwnerPHIDs(array($this->getViewer()->getPHID()))
->withTypes(array(PhabricatorMacroMacroPHIDType::TYPECONST))
->withColors($flag_colors)
->setViewer($this->getViewer())
->execute();
if (empty($flags)) {
- throw new PhabricatorEmptyQueryException('No matching flags.');
+ throw new PhabricatorEmptyQueryException(pht('No matching flags.'));
} else {
$where[] = qsprintf(
$conn,
'm.phid IN (%Ls)',
mpull($flags, 'getObjectPHID'));
}
}
$where[] = $this->buildPagingClause($conn);
return $this->formatWhereClause($where);
}
protected function didFilterPage(array $macros) {
if ($this->needFiles) {
$file_phids = mpull($macros, 'getFilePHID');
$files = id(new PhabricatorFileQuery())
->setViewer($this->getViewer())
->setParentQuery($this)
->withPHIDs($file_phids)
->execute();
$files = mpull($files, null, 'getPHID');
foreach ($macros as $key => $macro) {
$file = idx($files, $macro->getFilePHID());
if (!$file) {
unset($macros[$key]);
continue;
}
$macro->attachFile($file);
}
}
return $macros;
}
protected function getPrimaryTableAlias() {
return 'm';
}
public function getQueryApplicationClass() {
return 'PhabricatorMacroApplication';
}
public function getOrderableColumns() {
return parent::getOrderableColumns() + array(
'name' => array(
'table' => 'm',
'column' => 'name',
'type' => 'string',
'reverse' => true,
'unique' => true,
),
);
}
protected function getPagingValueMap($cursor, array $keys) {
$macro = $this->loadCursorObject($cursor);
return array(
'id' => $macro->getID(),
'name' => $macro->getName(),
);
}
public function getBuiltinOrders() {
return array(
'name' => array(
'vector' => array('name'),
'name' => pht('Name'),
),
) + parent::getBuiltinOrders();
}
}
diff --git a/src/applications/mailinglists/application/PhabricatorMailingListsApplication.php b/src/applications/mailinglists/application/PhabricatorMailingListsApplication.php
index bd134933b..cf6232199 100644
--- a/src/applications/mailinglists/application/PhabricatorMailingListsApplication.php
+++ b/src/applications/mailinglists/application/PhabricatorMailingListsApplication.php
@@ -1,48 +1,48 @@
<?php
final class PhabricatorMailingListsApplication extends PhabricatorApplication {
public function getName() {
- return 'Mailing Lists';
+ return pht('Mailing Lists');
}
public function getBaseURI() {
return '/mailinglists/';
}
public function getShortDescription() {
- return 'Manage External Lists';
+ return pht('Manage External Lists');
}
public function getFontIcon() {
return 'fa-mail-reply-all';
}
public function getApplicationGroup() {
return self::GROUP_ADMIN;
}
public function getRoutes() {
return array(
'/mailinglists/' => array(
'(?:query/(?P<queryKey>[^/]+)/)?'
=> 'PhabricatorMailingListsListController',
'edit/(?:(?P<id>[1-9]\d*)/)?'
=> 'PhabricatorMailingListsEditController',
),
);
}
public function getTitleGlyph() {
return '@';
}
protected function getCustomCapabilities() {
return array(
PhabricatorMailingListsManageCapability::CAPABILITY => array(
'default' => PhabricatorPolicies::POLICY_ADMIN,
),
);
}
}
diff --git a/src/applications/maniphest/__tests__/ManiphestTaskTestCase.php b/src/applications/maniphest/__tests__/ManiphestTaskTestCase.php
index fc9aa1738..b7fafa83f 100644
--- a/src/applications/maniphest/__tests__/ManiphestTaskTestCase.php
+++ b/src/applications/maniphest/__tests__/ManiphestTaskTestCase.php
@@ -1,221 +1,221 @@
<?php
final class ManiphestTaskTestCase extends PhabricatorTestCase {
protected function getPhabricatorTestCaseConfiguration() {
return array(
self::PHABRICATOR_TESTCONFIG_BUILD_STORAGE_FIXTURES => true,
);
}
public function testTaskReordering() {
$viewer = $this->generateNewTestUser();
- $t1 = $this->newTask($viewer, 'Task 1');
- $t2 = $this->newTask($viewer, 'Task 2');
- $t3 = $this->newTask($viewer, 'Task 3');
+ $t1 = $this->newTask($viewer, pht('Task 1'));
+ $t2 = $this->newTask($viewer, pht('Task 2'));
+ $t3 = $this->newTask($viewer, pht('Task 3'));
$auto_base = min(mpull(array($t1, $t2, $t3), 'getID'));
// Default order should be reverse creation.
$tasks = $this->loadTasks($viewer, $auto_base);
$t1 = $tasks[1];
$t2 = $tasks[2];
$t3 = $tasks[3];
$this->assertEqual(array(3, 2, 1), array_keys($tasks));
// Move T3 to the middle.
$this->moveTask($viewer, $t3, $t2, true);
$tasks = $this->loadTasks($viewer, $auto_base);
$t1 = $tasks[1];
$t2 = $tasks[2];
$t3 = $tasks[3];
$this->assertEqual(array(2, 3, 1), array_keys($tasks));
// Move T3 to the end.
$this->moveTask($viewer, $t3, $t1, true);
$tasks = $this->loadTasks($viewer, $auto_base);
$t1 = $tasks[1];
$t2 = $tasks[2];
$t3 = $tasks[3];
$this->assertEqual(array(2, 1, 3), array_keys($tasks));
// Repeat the move above, there should be no overall change in order.
$this->moveTask($viewer, $t3, $t1, true);
$tasks = $this->loadTasks($viewer, $auto_base);
$t1 = $tasks[1];
$t2 = $tasks[2];
$t3 = $tasks[3];
$this->assertEqual(array(2, 1, 3), array_keys($tasks));
// Move T3 to the first slot in the priority.
$this->movePriority($viewer, $t3, $t3->getPriority(), false);
$tasks = $this->loadTasks($viewer, $auto_base);
$t1 = $tasks[1];
$t2 = $tasks[2];
$t3 = $tasks[3];
$this->assertEqual(array(3, 2, 1), array_keys($tasks));
// Move T3 to the last slot in the priority.
$this->movePriority($viewer, $t3, $t3->getPriority(), true);
$tasks = $this->loadTasks($viewer, $auto_base);
$t1 = $tasks[1];
$t2 = $tasks[2];
$t3 = $tasks[3];
$this->assertEqual(array(2, 1, 3), array_keys($tasks));
// Move T3 before T2.
$this->moveTask($viewer, $t3, $t2, false);
$tasks = $this->loadTasks($viewer, $auto_base);
$t1 = $tasks[1];
$t2 = $tasks[2];
$t3 = $tasks[3];
$this->assertEqual(array(3, 2, 1), array_keys($tasks));
// Move T3 before T1.
$this->moveTask($viewer, $t3, $t1, false);
$tasks = $this->loadTasks($viewer, $auto_base);
$t1 = $tasks[1];
$t2 = $tasks[2];
$t3 = $tasks[3];
$this->assertEqual(array(2, 3, 1), array_keys($tasks));
}
public function testTaskAdjacentBlocks() {
$viewer = $this->generateNewTestUser();
$t = array();
for ($ii = 1; $ii < 10; $ii++) {
- $t[$ii] = $this->newTask($viewer, "Task Block {$ii}");
+ $t[$ii] = $this->newTask($viewer, pht('Task Block %d', $ii));
// This makes sure this test remains meaningful if we begin assigning
// subpriorities when tasks are created.
$t[$ii]->setSubpriority(0)->save();
}
$auto_base = min(mpull($t, 'getID'));
$tasks = $this->loadTasks($viewer, $auto_base);
$this->assertEqual(
array(9, 8, 7, 6, 5, 4, 3, 2, 1),
array_keys($tasks));
$this->moveTask($viewer, $t[9], $t[8], true);
$tasks = $this->loadTasks($viewer, $auto_base);
$this->assertEqual(
array(8, 9, 7, 6, 5, 4, 3, 2, 1),
array_keys($tasks));
// When there is a large block of tasks which all have the same
// subpriority, they should be assigned distinct subpriorities as a
// side effect of having a task moved into the block.
$subpri = mpull($tasks, 'getSubpriority');
$unique_subpri = array_unique($subpri);
$this->assertEqual(
9,
count($subpri),
- 'Expected subpriorities to be distributed.');
+ pht('Expected subpriorities to be distributed.'));
}
private function newTask(PhabricatorUser $viewer, $title) {
$task = ManiphestTask::initializeNewTask($viewer);
$xactions = array();
$xactions[] = id(new ManiphestTransaction())
->setTransactionType(ManiphestTransaction::TYPE_TITLE)
->setNewValue($title);
$this->applyTaskTransactions($viewer, $task, $xactions);
return $task;
}
private function loadTasks(PhabricatorUser $viewer, $auto_base) {
$tasks = id(new ManiphestTaskQuery())
->setViewer($viewer)
->setOrderBy(ManiphestTaskQuery::ORDER_PRIORITY)
->execute();
// NOTE: AUTO_INCREMENT changes survive ROLLBACK, and we can't throw them
// away without committing the current transaction, so we adjust the
// apparent task IDs as though the first one had been ID 1. This makes the
// tests a little easier to understand.
$map = array();
foreach ($tasks as $task) {
$map[($task->getID() - $auto_base) + 1] = $task;
}
return $map;
}
private function moveTask(PhabricatorUser $viewer, $src, $dst, $is_after) {
list($pri, $sub) = ManiphestTransactionEditor::getAdjacentSubpriority(
$dst,
$is_after);
$xactions = array();
$xactions[] = id(new ManiphestTransaction())
->setTransactionType(ManiphestTransaction::TYPE_PRIORITY)
->setNewValue($pri);
$xactions[] = id(new ManiphestTransaction())
->setTransactionType(ManiphestTransaction::TYPE_SUBPRIORITY)
->setNewValue($sub);
return $this->applyTaskTransactions($viewer, $src, $xactions);
}
private function movePriority(
PhabricatorUser $viewer,
$src,
$target_priority,
$is_end) {
list($pri, $sub) = ManiphestTransactionEditor::getEdgeSubpriority(
$target_priority,
$is_end);
$xactions = array();
$xactions[] = id(new ManiphestTransaction())
->setTransactionType(ManiphestTransaction::TYPE_PRIORITY)
->setNewValue($pri);
$xactions[] = id(new ManiphestTransaction())
->setTransactionType(ManiphestTransaction::TYPE_SUBPRIORITY)
->setNewValue($sub);
return $this->applyTaskTransactions($viewer, $src, $xactions);
}
private function applyTaskTransactions(
PhabricatorUser $viewer,
ManiphestTask $task,
array $xactions) {
$content_source = PhabricatorContentSource::newConsoleSource();
$editor = id(new ManiphestTransactionEditor())
->setActor($viewer)
->setContentSource($content_source)
->setContinueOnNoEffect(true)
->applyTransactions($task, $xactions);
return $task;
}
}
diff --git a/src/applications/maniphest/command/ManiphestAssignEmailCommand.php b/src/applications/maniphest/command/ManiphestAssignEmailCommand.php
index 7d98bb003..9d2a51b10 100644
--- a/src/applications/maniphest/command/ManiphestAssignEmailCommand.php
+++ b/src/applications/maniphest/command/ManiphestAssignEmailCommand.php
@@ -1,59 +1,61 @@
<?php
final class ManiphestAssignEmailCommand
extends ManiphestEmailCommand {
public function getCommand() {
return 'assign';
}
public function getCommandSyntax() {
return '**!assign** //username//';
}
public function getCommandSummary() {
return pht('Assign a task to a specific user.');
}
public function getCommandDescription() {
return pht(
- 'To assign a task to another user, provide their username. For example, '.
- 'to assign a task to `alincoln`, write `!assign alincoln`.'.
- "\n\n".
- 'If you omit the username or the username is not valid, this behaves '.
- 'like `!claim` and assigns the task to you instead.');
+ "To assign a task to another user, provide their username. For example, ".
+ "to assign a task to `%s`, write `%s`.\n\n".
+ "If you omit the username or the username is not valid, this behaves ".
+ "like `%s` and assigns the task to you instead.",
+ 'alincoln',
+ '!assign alincoln',
+ '!claim');
}
public function buildTransactions(
PhabricatorUser $viewer,
PhabricatorApplicationTransactionInterface $object,
PhabricatorMetaMTAReceivedMail $mail,
$command,
array $argv) {
$xactions = array();
$assign_to = head($argv);
if ($assign_to) {
$assign_user = id(new PhabricatorPeopleQuery())
->setViewer($viewer)
->withUsernames(array($assign_to))
->executeOne();
if ($assign_user) {
$assign_phid = $assign_user->getPHID();
}
}
// Treat bad "!assign" like "!claim".
if (!$assign_phid) {
$assign_phid = $viewer->getPHID();
}
$xactions[] = $object->getApplicationTransactionTemplate()
->setTransactionType(ManiphestTransaction::TYPE_OWNER)
->setNewValue($assign_phid);
return $xactions;
}
}
diff --git a/src/applications/maniphest/command/ManiphestPriorityEmailCommand.php b/src/applications/maniphest/command/ManiphestPriorityEmailCommand.php
index 04fdc7399..85ed148db 100644
--- a/src/applications/maniphest/command/ManiphestPriorityEmailCommand.php
+++ b/src/applications/maniphest/command/ManiphestPriorityEmailCommand.php
@@ -1,73 +1,73 @@
<?php
final class ManiphestPriorityEmailCommand
extends ManiphestEmailCommand {
public function getCommand() {
return 'priority';
}
public function getCommandSyntax() {
return '**!priority** //priority//';
}
public function getCommandSummary() {
return pht('Change the priority of a task.');
}
public function getCommandDescription() {
$names = ManiphestTaskPriority::getTaskPriorityMap();
$keywords = ManiphestTaskPriority::getTaskPriorityKeywordsMap();
$table = array();
$table[] = '| '.pht('Priority').' | '.pht('Keywords');
$table[] = '|---|---|';
foreach ($keywords as $priority => $words) {
$words = implode(', ', $words);
$table[] = '| '.$names[$priority].' | '.$words;
}
$table = implode("\n", $table);
return pht(
- 'To change the priority of a task, specify the desired priority, like '.
- '`!priority high`. This table shows the configured names for priority '.
- 'levels.'.
+ "To change the priority of a task, specify the desired priority, like ".
+ "`%s`. This table shows the configured names for priority levels.".
"\n\n%s\n\n".
- 'If you specify an invalid priority, the command is ignored. This '.
- 'command has no effect if you do not specify a priority.',
+ "If you specify an invalid priority, the command is ignored. This ".
+ "command has no effect if you do not specify a priority.",
+ '!priority high',
$table);
}
public function buildTransactions(
PhabricatorUser $viewer,
PhabricatorApplicationTransactionInterface $object,
PhabricatorMetaMTAReceivedMail $mail,
$command,
array $argv) {
$xactions = array();
$target = phutil_utf8_strtolower(head($argv));
$priority = null;
$keywords = ManiphestTaskPriority::getTaskPriorityKeywordsMap();
foreach ($keywords as $key => $words) {
foreach ($words as $word) {
if ($word == $target) {
$priority = $key;
break;
}
}
}
if ($priority === null) {
return array();
}
$xactions[] = $object->getApplicationTransactionTemplate()
->setTransactionType(ManiphestTransaction::TYPE_PRIORITY)
->setNewValue($priority);
return $xactions;
}
}
diff --git a/src/applications/maniphest/command/ManiphestStatusEmailCommand.php b/src/applications/maniphest/command/ManiphestStatusEmailCommand.php
index 0e2ebbba7..c9fa07494 100644
--- a/src/applications/maniphest/command/ManiphestStatusEmailCommand.php
+++ b/src/applications/maniphest/command/ManiphestStatusEmailCommand.php
@@ -1,72 +1,72 @@
<?php
final class ManiphestStatusEmailCommand
extends ManiphestEmailCommand {
public function getCommand() {
return 'status';
}
public function getCommandSyntax() {
return '**!status** //status//';
}
public function getCommandSummary() {
return pht('Change the status of a task.');
}
public function getCommandDescription() {
$names = ManiphestTaskStatus::getTaskStatusMap();
$keywords = ManiphestTaskStatus::getTaskStatusKeywordsMap();
$table = array();
$table[] = '| '.pht('Status').' | '.pht('Keywords');
$table[] = '|---|---|';
foreach ($keywords as $status => $words) {
$words = implode(', ', $words);
$table[] = '| '.$names[$status].' | '.$words;
}
$table = implode("\n", $table);
return pht(
- 'To change the status of a task, specify the desired status, like '.
- '`!status invalid`. This table shows the configured names for statuses.'.
- "\n\n%s\n\n".
- 'If you specify an invalid status, the command is ignored. This '.
- 'command has no effect if you do not specify a status.',
+ "To change the status of a task, specify the desired status, like ".
+ "`%s`. This table shows the configured names for statuses.\n\n%s\n\n".
+ "If you specify an invalid status, the command is ignored. This ".
+ "command has no effect if you do not specify a status.",
+ '!status invalid',
$table);
}
public function buildTransactions(
PhabricatorUser $viewer,
PhabricatorApplicationTransactionInterface $object,
PhabricatorMetaMTAReceivedMail $mail,
$command,
array $argv) {
$xactions = array();
$target = phutil_utf8_strtolower(head($argv));
$status = null;
$keywords = ManiphestTaskStatus::getTaskStatusKeywordsMap();
foreach ($keywords as $key => $words) {
foreach ($words as $word) {
if ($word == $target) {
$status = $key;
break;
}
}
}
if ($status === null) {
return array();
}
$xactions[] = $object->getApplicationTransactionTemplate()
->setTransactionType(ManiphestTransaction::TYPE_STATUS)
->setNewValue($status);
return $xactions;
}
}
diff --git a/src/applications/maniphest/conduit/ManiphestConduitAPIMethod.php b/src/applications/maniphest/conduit/ManiphestConduitAPIMethod.php
index 39067f7cb..52257b2cb 100644
--- a/src/applications/maniphest/conduit/ManiphestConduitAPIMethod.php
+++ b/src/applications/maniphest/conduit/ManiphestConduitAPIMethod.php
@@ -1,322 +1,325 @@
<?php
abstract class ManiphestConduitAPIMethod extends ConduitAPIMethod {
final public function getApplication() {
return PhabricatorApplication::getByClass(
'PhabricatorManiphestApplication');
}
protected function defineErrorTypes() {
return array(
- 'ERR-INVALID-PARAMETER' => 'Missing or malformed parameter.',
+ 'ERR-INVALID-PARAMETER' => pht('Missing or malformed parameter.'),
);
}
protected function buildTaskInfoDictionary(ManiphestTask $task) {
$results = $this->buildTaskInfoDictionaries(array($task));
return idx($results, $task->getPHID());
}
protected function getTaskFields($is_new) {
$fields = array();
if (!$is_new) {
$fields += array(
'id' => 'optional int',
'phid' => 'optional int',
);
}
$fields += array(
'title' => $is_new ? 'required string' : 'optional string',
'description' => 'optional string',
'ownerPHID' => 'optional phid',
'viewPolicy' => 'optional phid or policy string',
'editPolicy' => 'optional phid or policy string',
'ccPHIDs' => 'optional list<phid>',
'priority' => 'optional int',
'projectPHIDs' => 'optional list<phid>',
'auxiliary' => 'optional dict',
);
if (!$is_new) {
$fields += array(
'status' => 'optional string',
'comments' => 'optional string',
);
}
return $fields;
}
protected function applyRequest(
ManiphestTask $task,
ConduitAPIRequest $request,
$is_new) {
$changes = array();
if ($is_new) {
$task->setTitle((string)$request->getValue('title'));
$task->setDescription((string)$request->getValue('description'));
$changes[ManiphestTransaction::TYPE_STATUS] =
ManiphestTaskStatus::getDefaultStatus();
$changes[PhabricatorTransactions::TYPE_SUBSCRIBERS] =
array('+' => array($request->getUser()->getPHID()));
} else {
$comments = $request->getValue('comments');
if (!$is_new && $comments !== null) {
$changes[PhabricatorTransactions::TYPE_COMMENT] = null;
}
$title = $request->getValue('title');
if ($title !== null) {
$changes[ManiphestTransaction::TYPE_TITLE] = $title;
}
$desc = $request->getValue('description');
if ($desc !== null) {
$changes[ManiphestTransaction::TYPE_DESCRIPTION] = $desc;
}
$status = $request->getValue('status');
if ($status !== null) {
$valid_statuses = ManiphestTaskStatus::getTaskStatusMap();
if (!isset($valid_statuses[$status])) {
throw id(new ConduitException('ERR-INVALID-PARAMETER'))
- ->setErrorDescription('Status set to invalid value.');
+ ->setErrorDescription(pht('Status set to invalid value.'));
}
$changes[ManiphestTransaction::TYPE_STATUS] = $status;
}
}
$priority = $request->getValue('priority');
if ($priority !== null) {
$valid_priorities = ManiphestTaskPriority::getTaskPriorityMap();
if (!isset($valid_priorities[$priority])) {
throw id(new ConduitException('ERR-INVALID-PARAMETER'))
- ->setErrorDescription('Priority set to invalid value.');
+ ->setErrorDescription(pht('Priority set to invalid value.'));
}
$changes[ManiphestTransaction::TYPE_PRIORITY] = $priority;
}
$owner_phid = $request->getValue('ownerPHID');
if ($owner_phid !== null) {
$this->validatePHIDList(
array($owner_phid),
PhabricatorPeopleUserPHIDType::TYPECONST,
'ownerPHID');
$changes[ManiphestTransaction::TYPE_OWNER] = $owner_phid;
}
$ccs = $request->getValue('ccPHIDs');
if ($ccs !== null) {
$changes[PhabricatorTransactions::TYPE_SUBSCRIBERS] =
array('=' => array_fuse($ccs));
}
$transactions = array();
$view_policy = $request->getValue('viewPolicy');
if ($view_policy !== null) {
$transactions[] = id(new ManiphestTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_VIEW_POLICY)
->setNewValue($view_policy);
}
$edit_policy = $request->getValue('editPolicy');
if ($edit_policy !== null) {
$transactions[] = id(new ManiphestTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_EDIT_POLICY)
->setNewValue($edit_policy);
}
$project_phids = $request->getValue('projectPHIDs');
if ($project_phids !== null) {
$this->validatePHIDList(
$project_phids,
PhabricatorProjectProjectPHIDType::TYPECONST,
'projectPHIDS');
$project_type = PhabricatorProjectObjectHasProjectEdgeType::EDGECONST;
$transactions[] = id(new ManiphestTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
->setMetadataValue('edge:type', $project_type)
->setNewValue(
array(
'=' => array_fuse($project_phids),
));
}
$template = new ManiphestTransaction();
foreach ($changes as $type => $value) {
$transaction = clone $template;
$transaction->setTransactionType($type);
if ($type == PhabricatorTransactions::TYPE_COMMENT) {
$transaction->attachComment(
id(new ManiphestTransactionComment())
->setContent($comments));
} else {
$transaction->setNewValue($value);
}
$transactions[] = $transaction;
}
$field_list = PhabricatorCustomField::getObjectFields(
$task,
PhabricatorCustomField::ROLE_EDIT);
$field_list->readFieldsFromStorage($task);
$auxiliary = $request->getValue('auxiliary');
if ($auxiliary) {
foreach ($field_list->getFields() as $key => $field) {
if (!array_key_exists($key, $auxiliary)) {
continue;
}
$transaction = clone $template;
$transaction->setTransactionType(
PhabricatorTransactions::TYPE_CUSTOMFIELD);
$transaction->setMetadataValue('customfield:key', $key);
$transaction->setOldValue(
$field->getOldValueForApplicationTransactions());
$transaction->setNewValue($auxiliary[$key]);
$transactions[] = $transaction;
}
}
if (!$transactions) {
return;
}
$event = new PhabricatorEvent(
PhabricatorEventType::TYPE_MANIPHEST_WILLEDITTASK,
array(
'task' => $task,
'new' => $is_new,
'transactions' => $transactions,
));
$event->setUser($request->getUser());
$event->setConduitRequest($request);
PhutilEventEngine::dispatchEvent($event);
$task = $event->getValue('task');
$transactions = $event->getValue('transactions');
$content_source = PhabricatorContentSource::newForSource(
PhabricatorContentSource::SOURCE_CONDUIT,
array());
$editor = id(new ManiphestTransactionEditor())
->setActor($request->getUser())
->setContentSource($content_source)
->setContinueOnNoEffect(true);
if (!$is_new) {
$editor->setContinueOnMissingFields(true);
}
$editor->applyTransactions($task, $transactions);
$event = new PhabricatorEvent(
PhabricatorEventType::TYPE_MANIPHEST_DIDEDITTASK,
array(
'task' => $task,
'new' => $is_new,
'transactions' => $transactions,
));
$event->setUser($request->getUser());
$event->setConduitRequest($request);
PhutilEventEngine::dispatchEvent($event);
// reload the task now that we've done all the fun stuff
return id(new ManiphestTaskQuery())
->setViewer($request->getUser())
->withPHIDs(array($task->getPHID()))
->needSubscriberPHIDs(true)
->needProjectPHIDs(true)
->executeOne();
}
protected function buildTaskInfoDictionaries(array $tasks) {
assert_instances_of($tasks, 'ManiphestTask');
if (!$tasks) {
return array();
}
$task_phids = mpull($tasks, 'getPHID');
$all_deps = id(new PhabricatorEdgeQuery())
->withSourcePHIDs($task_phids)
->withEdgeTypes(array(ManiphestTaskDependsOnTaskEdgeType::EDGECONST));
$all_deps->execute();
$result = array();
foreach ($tasks as $task) {
// TODO: Batch this get as CustomField gets cleaned up.
$field_list = PhabricatorCustomField::getObjectFields(
$task,
PhabricatorCustomField::ROLE_EDIT);
$field_list->readFieldsFromStorage($task);
$auxiliary = mpull(
$field_list->getFields(),
'getValueForStorage',
'getFieldKey');
$task_deps = $all_deps->getDestinationPHIDs(
array($task->getPHID()),
array(ManiphestTaskDependsOnTaskEdgeType::EDGECONST));
$result[$task->getPHID()] = array(
'id' => $task->getID(),
'phid' => $task->getPHID(),
'authorPHID' => $task->getAuthorPHID(),
'ownerPHID' => $task->getOwnerPHID(),
'ccPHIDs' => $task->getSubscriberPHIDs(),
'status' => $task->getStatus(),
'statusName' => ManiphestTaskStatus::getTaskStatusName(
$task->getStatus()),
'isClosed' => $task->isClosed(),
'priority' => ManiphestTaskPriority::getTaskPriorityName(
$task->getPriority()),
'priorityColor' => ManiphestTaskPriority::getTaskPriorityColor(
$task->getPriority()),
'title' => $task->getTitle(),
'description' => $task->getDescription(),
'projectPHIDs' => $task->getProjectPHIDs(),
'uri' => PhabricatorEnv::getProductionURI('/T'.$task->getID()),
'auxiliary' => $auxiliary,
'objectName' => 'T'.$task->getID(),
'dateCreated' => $task->getDateCreated(),
'dateModified' => $task->getDateModified(),
'dependsOnTaskPHIDs' => $task_deps,
);
}
return $result;
}
/**
* NOTE: This is a temporary stop gap since its easy to make malformed tasks.
* Long-term, the values set in @{method:defineParamTypes} will be used to
* validate data implicitly within the larger Conduit application.
*
* TODO: Remove this in favor of generalized Conduit hotness.
*/
private function validatePHIDList(array $phid_list, $phid_type, $field) {
$phid_groups = phid_group_by_type($phid_list);
unset($phid_groups[$phid_type]);
if (!empty($phid_groups)) {
throw id(new ConduitException('ERR-INVALID-PARAMETER'))
- ->setErrorDescription('One or more PHIDs were invalid for '.$field.'.');
+ ->setErrorDescription(
+ pht(
+ 'One or more PHIDs were invalid for %s.',
+ $field));
}
return true;
}
}
diff --git a/src/applications/maniphest/conduit/ManiphestCreateTaskConduitAPIMethod.php b/src/applications/maniphest/conduit/ManiphestCreateTaskConduitAPIMethod.php
index ed16f7749..3a404774c 100644
--- a/src/applications/maniphest/conduit/ManiphestCreateTaskConduitAPIMethod.php
+++ b/src/applications/maniphest/conduit/ManiphestCreateTaskConduitAPIMethod.php
@@ -1,36 +1,36 @@
<?php
final class ManiphestCreateTaskConduitAPIMethod
extends ManiphestConduitAPIMethod {
public function getAPIMethodName() {
return 'maniphest.createtask';
}
public function getMethodDescription() {
- return 'Create a new Maniphest task.';
+ return pht('Create a new Maniphest task.');
}
protected function defineParamTypes() {
return $this->getTaskFields($is_new = true);
}
protected function defineReturnType() {
return 'nonempty dict';
}
protected function defineErrorTypes() {
return array(
- 'ERR-INVALID-PARAMETER' => 'Missing or malformed parameter.',
+ 'ERR-INVALID-PARAMETER' => pht('Missing or malformed parameter.'),
);
}
protected function execute(ConduitAPIRequest $request) {
$task = ManiphestTask::initializeNewTask($request->getUser());
$task = $this->applyRequest($task, $request, $is_new = true);
return $this->buildTaskInfoDictionary($task);
}
}
diff --git a/src/applications/maniphest/conduit/ManiphestGetTaskTransactionsConduitAPIMethod.php b/src/applications/maniphest/conduit/ManiphestGetTaskTransactionsConduitAPIMethod.php
index ff51dafd7..4c64a56b3 100644
--- a/src/applications/maniphest/conduit/ManiphestGetTaskTransactionsConduitAPIMethod.php
+++ b/src/applications/maniphest/conduit/ManiphestGetTaskTransactionsConduitAPIMethod.php
@@ -1,75 +1,75 @@
<?php
final class ManiphestGetTaskTransactionsConduitAPIMethod
extends ManiphestConduitAPIMethod {
public function getAPIMethodName() {
return 'maniphest.gettasktransactions';
}
public function getMethodDescription() {
- return 'Retrieve Maniphest Task Transactions.';
+ return pht('Retrieve Maniphest task transactions.');
}
protected function defineParamTypes() {
return array(
'ids' => 'required list<int>',
);
}
protected function defineReturnType() {
return 'nonempty list<dict<string, wild>>';
}
protected function execute(ConduitAPIRequest $request) {
$results = array();
$task_ids = $request->getValue('ids');
if (!$task_ids) {
return $results;
}
$tasks = id(new ManiphestTaskQuery())
->setViewer($request->getUser())
->withIDs($task_ids)
->execute();
$tasks = mpull($tasks, null, 'getPHID');
$transactions = array();
if ($tasks) {
$transactions = id(new ManiphestTransactionQuery())
->setViewer($request->getUser())
->withObjectPHIDs(mpull($tasks, 'getPHID'))
->needComments(true)
->execute();
}
foreach ($transactions as $transaction) {
$task_phid = $transaction->getObjectPHID();
if (empty($tasks[$task_phid])) {
continue;
}
$task_id = $tasks[$task_phid]->getID();
$comments = null;
if ($transaction->hasComment()) {
$comments = $transaction->getComment()->getContent();
}
$results[$task_id][] = array(
'taskID' => $task_id,
'transactionPHID' => $transaction->getPHID(),
'transactionType' => $transaction->getTransactionType(),
'oldValue' => $transaction->getOldValue(),
'newValue' => $transaction->getNewValue(),
'comments' => $comments,
'authorPHID' => $transaction->getAuthorPHID(),
'dateCreated' => $transaction->getDateCreated(),
);
}
return $results;
}
}
diff --git a/src/applications/maniphest/conduit/ManiphestInfoConduitAPIMethod.php b/src/applications/maniphest/conduit/ManiphestInfoConduitAPIMethod.php
index 5915e74fb..367feff41 100644
--- a/src/applications/maniphest/conduit/ManiphestInfoConduitAPIMethod.php
+++ b/src/applications/maniphest/conduit/ManiphestInfoConduitAPIMethod.php
@@ -1,45 +1,45 @@
<?php
final class ManiphestInfoConduitAPIMethod extends ManiphestConduitAPIMethod {
public function getAPIMethodName() {
return 'maniphest.info';
}
public function getMethodDescription() {
- return 'Retrieve information about a Maniphest task, given its id.';
+ return pht('Retrieve information about a Maniphest task, given its ID.');
}
protected function defineParamTypes() {
return array(
'task_id' => 'required id',
);
}
protected function defineReturnType() {
return 'nonempty dict';
}
protected function defineErrorTypes() {
return array(
- 'ERR_BAD_TASK' => 'No such maniphest task exists',
+ 'ERR_BAD_TASK' => pht('No such Maniphest task exists.'),
);
}
protected function execute(ConduitAPIRequest $request) {
$task_id = $request->getValue('task_id');
$task = id(new ManiphestTaskQuery())
->setViewer($request->getUser())
->withIDs(array($task_id))
->needSubscriberPHIDs(true)
->needProjectPHIDs(true)
->executeOne();
if (!$task) {
throw new ConduitException('ERR_BAD_TASK');
}
return $this->buildTaskInfoDictionary($task);
}
}
diff --git a/src/applications/maniphest/conduit/ManiphestQueryConduitAPIMethod.php b/src/applications/maniphest/conduit/ManiphestQueryConduitAPIMethod.php
index 0dc26e58a..8a2321b2d 100644
--- a/src/applications/maniphest/conduit/ManiphestQueryConduitAPIMethod.php
+++ b/src/applications/maniphest/conduit/ManiphestQueryConduitAPIMethod.php
@@ -1,122 +1,122 @@
<?php
final class ManiphestQueryConduitAPIMethod extends ManiphestConduitAPIMethod {
public function getAPIMethodName() {
return 'maniphest.query';
}
public function getMethodDescription() {
- return 'Execute complex searches for Maniphest tasks.';
+ return pht('Execute complex searches for Maniphest tasks.');
}
protected function defineParamTypes() {
$statuses = array(
ManiphestTaskQuery::STATUS_ANY,
ManiphestTaskQuery::STATUS_OPEN,
ManiphestTaskQuery::STATUS_CLOSED,
ManiphestTaskQuery::STATUS_RESOLVED,
ManiphestTaskQuery::STATUS_WONTFIX,
ManiphestTaskQuery::STATUS_INVALID,
ManiphestTaskQuery::STATUS_SPITE,
ManiphestTaskQuery::STATUS_DUPLICATE,
);
$status_const = $this->formatStringConstants($statuses);
$orders = array(
ManiphestTaskQuery::ORDER_PRIORITY,
ManiphestTaskQuery::ORDER_CREATED,
ManiphestTaskQuery::ORDER_MODIFIED,
);
$order_const = $this->formatStringConstants($orders);
return array(
'ids' => 'optional list<uint>',
'phids' => 'optional list<phid>',
'ownerPHIDs' => 'optional list<phid>',
'authorPHIDs' => 'optional list<phid>',
'projectPHIDs' => 'optional list<phid>',
'ccPHIDs' => 'optional list<phid>',
'fullText' => 'optional string',
'status' => 'optional '.$status_const,
'order' => 'optional '.$order_const,
'limit' => 'optional int',
'offset' => 'optional int',
);
}
protected function defineReturnType() {
return 'list';
}
protected function execute(ConduitAPIRequest $request) {
$query = id(new ManiphestTaskQuery())
->setViewer($request->getUser())
->needProjectPHIDs(true)
->needSubscriberPHIDs(true);
$task_ids = $request->getValue('ids');
if ($task_ids) {
$query->withIDs($task_ids);
}
$task_phids = $request->getValue('phids');
if ($task_phids) {
$query->withPHIDs($task_phids);
}
$owners = $request->getValue('ownerPHIDs');
if ($owners) {
$query->withOwners($owners);
}
$authors = $request->getValue('authorPHIDs');
if ($authors) {
$query->withAuthors($authors);
}
$projects = $request->getValue('projectPHIDs');
if ($projects) {
$query->withEdgeLogicPHIDs(
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST,
PhabricatorQueryConstraint::OPERATOR_AND,
$projects);
}
$ccs = $request->getValue('ccPHIDs');
if ($ccs) {
$query->withSubscribers($ccs);
}
$full_text = $request->getValue('fullText');
if ($full_text) {
$query->withFullTextSearch($full_text);
}
$status = $request->getValue('status');
if ($status) {
$query->withStatus($status);
}
$order = $request->getValue('order');
if ($order) {
$query->setOrderBy($order);
}
$limit = $request->getValue('limit');
if ($limit) {
$query->setLimit($limit);
}
$offset = $request->getValue('offset');
if ($offset) {
$query->setOffset($offset);
}
$results = $query->execute();
return $this->buildTaskInfoDictionaries($results);
}
}
diff --git a/src/applications/maniphest/conduit/ManiphestQueryStatusesConduitAPIMethod.php b/src/applications/maniphest/conduit/ManiphestQueryStatusesConduitAPIMethod.php
index 8255b3474..971be8820 100644
--- a/src/applications/maniphest/conduit/ManiphestQueryStatusesConduitAPIMethod.php
+++ b/src/applications/maniphest/conduit/ManiphestQueryStatusesConduitAPIMethod.php
@@ -1,35 +1,36 @@
<?php
final class ManiphestQueryStatusesConduitAPIMethod
extends ManiphestConduitAPIMethod {
public function getAPIMethodName() {
return 'maniphest.querystatuses';
}
public function getMethodDescription() {
- return 'Retrieve information about possible Maniphest Task status values.';
+ return pht(
+ 'Retrieve information about possible Maniphest task status values.');
}
protected function defineParamTypes() {
return array();
}
protected function defineReturnType() {
return 'nonempty dict<string, wild>';
}
protected function execute(ConduitAPIRequest $request) {
$results = array(
'defaultStatus' => ManiphestTaskStatus::getDefaultStatus(),
'defaultClosedStatus' => ManiphestTaskStatus::getDefaultClosedStatus(),
'duplicateStatus' => ManiphestTaskStatus::getDuplicateStatus(),
'openStatuses' => ManiphestTaskStatus::getOpenStatusConstants(),
'closedStatuses' => ManiphestTaskStatus::getClosedStatusConstants(),
'allStatuses' => array_keys(ManiphestTaskStatus::getTaskStatusMap()),
'statusMap' => ManiphestTaskStatus::getTaskStatusMap(),
);
return $results;
}
}
diff --git a/src/applications/maniphest/conduit/ManiphestUpdateConduitAPIMethod.php b/src/applications/maniphest/conduit/ManiphestUpdateConduitAPIMethod.php
index a4a623267..ad659af25 100644
--- a/src/applications/maniphest/conduit/ManiphestUpdateConduitAPIMethod.php
+++ b/src/applications/maniphest/conduit/ManiphestUpdateConduitAPIMethod.php
@@ -1,65 +1,69 @@
<?php
final class ManiphestUpdateConduitAPIMethod extends ManiphestConduitAPIMethod {
public function getAPIMethodName() {
return 'maniphest.update';
}
public function getMethodDescription() {
- return 'Update an existing Maniphest task.';
+ return pht('Update an existing Maniphest task.');
}
protected function defineErrorTypes() {
return array(
- 'ERR-BAD-TASK' => 'No such maniphest task exists.',
- 'ERR-INVALID-PARAMETER' => 'Missing or malformed parameter.',
- 'ERR-NO-EFFECT' => 'Update has no effect.',
+ 'ERR-BAD-TASK' => pht('No such Maniphest task exists.'),
+ 'ERR-INVALID-PARAMETER' => pht('Missing or malformed parameter.'),
+ 'ERR-NO-EFFECT' => pht('Update has no effect.'),
);
}
protected function defineParamTypes() {
return $this->getTaskFields($is_new = false);
}
protected function defineReturnType() {
return 'nonempty dict';
}
protected function execute(ConduitAPIRequest $request) {
$id = $request->getValue('id');
$phid = $request->getValue('phid');
if (($id && $phid) || (!$id && !$phid)) {
- throw new Exception("Specify exactly one of 'id' and 'phid'.");
+ throw new Exception(
+ pht(
+ "Specify exactly one of '%s' and '%s'.",
+ 'id',
+ 'phid'));
}
$query = id (new ManiphestTaskQuery())
->setViewer($request->getUser())
->needSubscriberPHIDs(true)
->needProjectPHIDs(true);
if ($id) {
$query->withIDs(array($id));
} else {
$query->withPHIDs(array($phid));
}
$task = $query->executeOne();
$params = $request->getAllParameters();
unset($params['id']);
unset($params['phid']);
if (call_user_func_array('coalesce', $params) === null) {
throw new ConduitException('ERR-NO-EFFECT');
}
if (!$task) {
throw new ConduitException('ERR-BAD-TASK');
}
$task = $this->applyRequest($task, $request, $is_new = false);
return $this->buildTaskInfoDictionary($task);
}
}
diff --git a/src/applications/maniphest/config/PhabricatorManiphestConfigOptions.php b/src/applications/maniphest/config/PhabricatorManiphestConfigOptions.php
index 04d59c996..a52b84dcb 100644
--- a/src/applications/maniphest/config/PhabricatorManiphestConfigOptions.php
+++ b/src/applications/maniphest/config/PhabricatorManiphestConfigOptions.php
@@ -1,323 +1,327 @@
<?php
final class PhabricatorManiphestConfigOptions
extends PhabricatorApplicationConfigOptions {
public function getName() {
return pht('Maniphest');
}
public function getDescription() {
return pht('Configure Maniphest.');
}
public function getFontIcon() {
return 'fa-anchor';
}
public function getGroup() {
return 'apps';
}
public function getOptions() {
$priority_defaults = array(
100 => array(
'name' => pht('Unbreak Now!'),
'short' => pht('Unbreak!'),
'color' => 'indigo',
'keywords' => array('unbreak'),
),
90 => array(
'name' => pht('Needs Triage'),
'short' => pht('Triage'),
'color' => 'violet',
'keywords' => array('triage'),
),
80 => array(
'name' => pht('High'),
'short' => pht('High'),
'color' => 'red',
'keywords' => array('high'),
),
50 => array(
'name' => pht('Normal'),
'short' => pht('Normal'),
'color' => 'orange',
'keywords' => array('normal'),
),
25 => array(
'name' => pht('Low'),
'short' => pht('Low'),
'color' => 'yellow',
'keywords' => array('low'),
),
0 => array(
'name' => pht('Wishlist'),
'short' => pht('Wish'),
'color' => 'sky',
'keywords' => array('wish', 'wishlist'),
),
);
$status_type = 'custom:ManiphestStatusConfigOptionType';
$status_defaults = array(
'open' => array(
'name' => pht('Open'),
'special' => ManiphestTaskStatus::SPECIAL_DEFAULT,
'prefixes' => array(
'open',
'opens',
'reopen',
'reopens',
),
),
'resolved' => array(
'name' => pht('Resolved'),
'name.full' => pht('Closed, Resolved'),
'closed' => true,
'special' => ManiphestTaskStatus::SPECIAL_CLOSED,
'prefixes' => array(
'closed',
'closes',
'close',
'fix',
'fixes',
'fixed',
'resolve',
'resolves',
'resolved',
),
'suffixes' => array(
'as resolved',
'as fixed',
),
'keywords' => array('closed', 'fixed', 'resolved'),
),
'wontfix' => array(
'name' => pht('Wontfix'),
'name.full' => pht('Closed, Wontfix'),
'closed' => true,
'prefixes' => array(
'wontfix',
'wontfixes',
'wontfixed',
),
'suffixes' => array(
'as wontfix',
),
),
'invalid' => array(
'name' => pht('Invalid'),
'name.full' => pht('Closed, Invalid'),
'closed' => true,
'prefixes' => array(
'invalidate',
'invalidates',
'invalidated',
),
'suffixes' => array(
'as invalid',
),
),
'duplicate' => array(
'name' => pht('Duplicate'),
'name.full' => pht('Closed, Duplicate'),
'transaction.icon' => 'fa-files-o',
'special' => ManiphestTaskStatus::SPECIAL_DUPLICATE,
'closed' => true,
),
'spite' => array(
'name' => pht('Spite'),
'name.full' => pht('Closed, Spite'),
'name.action' => pht('Spited'),
'transaction.icon' => 'fa-thumbs-o-down',
'silly' => true,
'closed' => true,
'prefixes' => array(
'spite',
'spites',
'spited',
),
'suffixes' => array(
'out of spite',
'as spite',
),
),
);
$status_description = $this->deformat(pht(<<<EOTEXT
Allows you to edit, add, or remove the task statuses available in Maniphest,
like "Open", "Resolved" and "Invalid". The configuration should contain a map
of status constants to status specifications (see defaults below for examples).
The constant for each status should be 1-12 characters long and contain only
lowercase letters and digits. Valid examples are "open", "closed", and
"invalid". Users will not normally see these values.
The keys you can provide in a specification are:
- `name` //Required string.// Name of the status, like "Invalid".
- `name.full` //Optional string.// Longer name, like "Closed, Invalid". This
appears on the task detail view in the header.
- `name.action` //Optional string.// Action name for email subjects, like
"Marked Invalid".
- `closed` //Optional bool.// Statuses are either "open" or "closed".
Specifying `true` here will mark the status as closed (like "Resolved" or
"Invalid"). By default, statuses are open.
- `special` //Optional string.// Mark this status as special. The special
statuses are:
- `default` This is the default status for newly created tasks. You must
designate one status as default, and it must be an open status.
- `closed` This is the default status for closed tasks (for example, tasks
closed via the "!close" action in email or via the quick close button in
Maniphest). You must designate one status as the default closed status,
and it must be a closed status.
- `duplicate` This is the status used when tasks are merged into one
another as duplicates. You must designate one status for duplicates,
and it must be a closed status.
- `transaction.icon` //Optional string.// Allows you to choose a different
icon to use for this status when showing status changes in the transaction
log. Please see UIExamples, Icons and Images for a list.
- `transaction.color` //Optional string.// Allows you to choose a different
color to use for this status when showing status changes in the transaction
log.
- `silly` //Optional bool.// Marks this status as silly, and thus wholly
inappropriate for use by serious businesses.
- `prefixes` //Optional list<string>.// Allows you to specify a list of
text prefixes which will trigger a task transition into this status
when mentioned in a commit message. For example, providing "closes" here
will allow users to move tasks to this status by writing `Closes T123` in
commit messages.
- `suffixes` //Optional list<string>.// Allows you to specify a list of
text suffixes which will trigger a task transition into this status
when mentioned in a commit message, after a valid prefix. For example,
providing "as invalid" here will allow users to move tasks
to this status by writing `Closes T123 as invalid`, even if another status
is selected by the "Closes" prefix.
- `keywords` //Optional list<string>.// Allows you to specify a list
of keywords which can be used with `!status` commands in email to select
this status.
Statuses will appear in the UI in the order specified. Note the status marked
`special` as `duplicate` is not settable directly and will not appear in UI
elements, and that any status marked `silly` does not appear if Phabricator
is configured with `phabricator.serious-business` set to true.
Examining the default configuration and examples below will probably be helpful
in understanding these options.
EOTEXT
));
$status_example = array(
'open' => array(
'name' => 'Open',
'special' => 'default',
),
'closed' => array(
'name' => 'Closed',
'special' => 'closed',
'closed' => true,
),
'duplicate' => array(
'name' => 'Duplicate',
'special' => 'duplicate',
'closed' => true,
),
);
$json = new PhutilJSON();
$status_example = $json->encodeFormatted($status_example);
// This is intentionally blank for now, until we can move more Maniphest
// logic to custom fields.
$default_fields = array();
foreach ($default_fields as $key => $enabled) {
$default_fields[$key] = array(
'disabled' => !$enabled,
);
}
$custom_field_type = 'custom:PhabricatorCustomFieldConfigOptionType';
return array(
$this->newOption('maniphest.custom-field-definitions', 'wild', array())
->setSummary(pht('Custom Maniphest fields.'))
->setDescription(
pht(
'Array of custom fields for Maniphest tasks. For details on '.
'adding custom fields to Maniphest, see "Configuring Custom '.
'Fields" in the documentation.'))
->addExample(
'{"mycompany:estimated-hours": {"name": "Estimated Hours", '.
'"type": "int", "caption": "Estimated number of hours this will '.
'take."}}',
pht('Valid Setting')),
$this->newOption('maniphest.fields', $custom_field_type, $default_fields)
->setCustomData(id(new ManiphestTask())->getCustomFieldBaseClass())
->setDescription(pht('Select and reorder task fields.')),
$this->newOption('maniphest.priorities', 'wild', $priority_defaults)
->setSummary(pht('Configure Maniphest priority names.'))
->setDescription(
pht(
'Allows you to edit or override the default priorities available '.
'in Maniphest, like "High", "Normal" and "Low". The configuration '.
'should contain a map of priority constants to priority '.
'specifications (see defaults below for examples).'.
"\n\n".
'The keys you can define for a priority are:'.
"\n\n".
' - `name` Name of the priority.'."\n".
' - `short` Alternate shorter name, used in UIs where there is '.
' not much space available.'."\n".
' - `color` A color for this priority, like "red" or "blue".'.
' - `keywords` An optional list of keywords which can '.
' be used to select this priority when using `!priority` '.
' commands in email.'.
"\n\n".
'You can choose which priority is the default for newly created '.
- 'tasks with `maniphest.default-priority`.')),
+ 'tasks with `%s`.',
+ 'maniphest.default-priority')),
$this->newOption('maniphest.statuses', $status_type, $status_defaults)
->setSummary(pht('Configure Maniphest task statuses.'))
->setDescription($status_description)
->addExample($status_example, pht('Minimal Valid Config')),
$this->newOption('maniphest.default-priority', 'int', 90)
->setSummary(pht('Default task priority for create flows.'))
->setDescription(
pht(
'Choose a default priority for newly created tasks. You can '.
'review and adjust available priorities by using the '.
- '{{maniphest.priorities}} configuration option. The default value '.
- '(`90`) corresponds to the default "Needs Triage" priority.')),
+ '%s configuration option. The default value (`90`) '.
+ 'corresponds to the default "Needs Triage" priority.',
+ 'maniphest.priorities')),
$this->newOption(
'metamta.maniphest.subject-prefix',
'string',
'[Maniphest]')
->setDescription(pht('Subject prefix for Maniphest mail.')),
$this->newOption(
'maniphest.priorities.unbreak-now',
'int',
100)
->setSummary(pht('Priority used to populate "Unbreak Now" on home.'))
->setDescription(
pht(
'Temporary setting. If set, this priority is used to populate the '.
'"Unbreak Now" panel on the home page. You should adjust this if '.
- 'you adjust priorities using `maniphest.priorities`.')),
+ 'you adjust priorities using `%s`.',
+ 'maniphest.priorities')),
$this->newOption(
'maniphest.priorities.needs-triage',
'int',
90)
->setSummary(pht('Priority used to populate "Needs Triage" on home.'))
->setDescription(
pht(
'Temporary setting. If set, this priority is used to populate the '.
'"Needs Triage" panel on the home page. You should adjust this if '.
- 'you adjust priorities using `maniphest.priorities`.')),
+ 'you adjust priorities using `%s`.',
+ 'maniphest.priorities')),
);
}
}
diff --git a/src/applications/maniphest/controller/ManiphestBatchEditController.php b/src/applications/maniphest/controller/ManiphestBatchEditController.php
index 58c8ffce1..a4bf77ef1 100644
--- a/src/applications/maniphest/controller/ManiphestBatchEditController.php
+++ b/src/applications/maniphest/controller/ManiphestBatchEditController.php
@@ -1,406 +1,406 @@
<?php
final class ManiphestBatchEditController extends ManiphestController {
public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
$this->requireApplicationCapability(
ManiphestBulkEditCapability::CAPABILITY);
$project = null;
$board_id = $request->getInt('board');
if ($board_id) {
$project = id(new PhabricatorProjectQuery())
->setViewer($viewer)
->withIDs(array($board_id))
->executeOne();
if (!$project) {
return new Aphront404Response();
}
}
$task_ids = $request->getArr('batch');
if (!$task_ids) {
$task_ids = $request->getStrList('batch');
}
$tasks = id(new ManiphestTaskQuery())
->setViewer($viewer)
->withIDs($task_ids)
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->needSubscriberPHIDs(true)
->needProjectPHIDs(true)
->execute();
if ($project) {
$cancel_uri = '/project/board/'.$project->getID().'/';
$redirect_uri = $cancel_uri;
} else {
$cancel_uri = '/maniphest/';
$redirect_uri = '/maniphest/?ids='.implode(',', mpull($tasks, 'getID'));
}
$actions = $request->getStr('actions');
if ($actions) {
$actions = phutil_json_decode($actions);
}
if ($request->isFormPost() && is_array($actions)) {
foreach ($tasks as $task) {
$field_list = PhabricatorCustomField::getObjectFields(
$task,
PhabricatorCustomField::ROLE_EDIT);
$field_list->readFieldsFromStorage($task);
$xactions = $this->buildTransactions($actions, $task);
if ($xactions) {
// TODO: Set content source to "batch edit".
$editor = id(new ManiphestTransactionEditor())
->setActor($viewer)
->setContentSourceFromRequest($request)
->setContinueOnNoEffect(true)
->setContinueOnMissingFields(true)
->applyTransactions($task, $xactions);
}
}
return id(new AphrontRedirectResponse())->setURI($redirect_uri);
}
$handles = ManiphestTaskListView::loadTaskHandles($viewer, $tasks);
$list = new ManiphestTaskListView();
$list->setTasks($tasks);
$list->setUser($viewer);
$list->setHandles($handles);
$template = new AphrontTokenizerTemplateView();
$template = $template->render();
$projects_source = new PhabricatorProjectDatasource();
$mailable_source = new PhabricatorMetaMTAMailableDatasource();
$mailable_source->setViewer($viewer);
$owner_source = new ManiphestAssigneeDatasource();
$owner_source->setViewer($viewer);
require_celerity_resource('maniphest-batch-editor');
Javelin::initBehavior(
'maniphest-batch-editor',
array(
'root' => 'maniphest-batch-edit-form',
'tokenizerTemplate' => $template,
'sources' => array(
'project' => array(
'src' => $projects_source->getDatasourceURI(),
'placeholder' => $projects_source->getPlaceholderText(),
'browseURI' => $projects_source->getBrowseURI(),
),
'owner' => array(
'src' => $owner_source->getDatasourceURI(),
'placeholder' => $owner_source->getPlaceholderText(),
'browseURI' => $owner_source->getBrowseURI(),
'limit' => 1,
),
'cc' => array(
'src' => $mailable_source->getDatasourceURI(),
'placeholder' => $mailable_source->getPlaceholderText(),
'browseURI' => $mailable_source->getBrowseURI(),
),
),
'input' => 'batch-form-actions',
'priorityMap' => ManiphestTaskPriority::getTaskPriorityMap(),
'statusMap' => ManiphestTaskStatus::getTaskStatusMap(),
));
$form = id(new AphrontFormView())
->setUser($viewer)
->addHiddenInput('board', $board_id)
->setID('maniphest-batch-edit-form');
foreach ($tasks as $task) {
$form->appendChild(
phutil_tag(
'input',
array(
'type' => 'hidden',
'name' => 'batch[]',
'value' => $task->getID(),
)));
}
$form->appendChild(
phutil_tag(
'input',
array(
'type' => 'hidden',
'name' => 'actions',
'id' => 'batch-form-actions',
)));
$form->appendChild(
id(new PHUIFormInsetView())
->setTitle(pht('Actions'))
->setRightButton(javelin_tag(
'a',
array(
'href' => '#',
'class' => 'button green',
'sigil' => 'add-action',
'mustcapture' => true,
),
pht('Add Another Action')))
->setContent(javelin_tag(
'table',
array(
'sigil' => 'maniphest-batch-actions',
'class' => 'maniphest-batch-actions-table',
),
'')))
->appendChild(
id(new AphrontFormSubmitControl())
->setValue(pht('Update Tasks'))
->addCancelButton($cancel_uri));
$title = pht('Batch Editor');
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb($title);
$task_box = id(new PHUIObjectBoxView())
->setHeaderText(pht('Selected Tasks'))
->appendChild($list);
$form_box = id(new PHUIObjectBoxView())
->setHeaderText(pht('Batch Editor'))
->setForm($form);
return $this->buildApplicationPage(
array(
$crumbs,
$task_box,
$form_box,
),
array(
'title' => $title,
));
}
private function buildTransactions($actions, ManiphestTask $task) {
$value_map = array();
$type_map = array(
'add_comment' => PhabricatorTransactions::TYPE_COMMENT,
'assign' => ManiphestTransaction::TYPE_OWNER,
'status' => ManiphestTransaction::TYPE_STATUS,
'priority' => ManiphestTransaction::TYPE_PRIORITY,
'add_project' => PhabricatorTransactions::TYPE_EDGE,
'remove_project' => PhabricatorTransactions::TYPE_EDGE,
'add_ccs' => PhabricatorTransactions::TYPE_SUBSCRIBERS,
'remove_ccs' => PhabricatorTransactions::TYPE_SUBSCRIBERS,
);
$edge_edit_types = array(
'add_project' => true,
'remove_project' => true,
'add_ccs' => true,
'remove_ccs' => true,
);
$xactions = array();
foreach ($actions as $action) {
if (empty($type_map[$action['action']])) {
- throw new Exception("Unknown batch edit action '{$action}'!");
+ throw new Exception(pht("Unknown batch edit action '%s'!", $action));
}
$type = $type_map[$action['action']];
// Figure out the current value, possibly after modifications by other
// batch actions of the same type. For example, if the user chooses to
// "Add Comment" twice, we should add both comments. More notably, if the
// user chooses "Remove Project..." and also "Add Project...", we should
// avoid restoring the removed project in the second transaction.
if (array_key_exists($type, $value_map)) {
$current = $value_map[$type];
} else {
switch ($type) {
case PhabricatorTransactions::TYPE_COMMENT:
$current = null;
break;
case ManiphestTransaction::TYPE_OWNER:
$current = $task->getOwnerPHID();
break;
case ManiphestTransaction::TYPE_STATUS:
$current = $task->getStatus();
break;
case ManiphestTransaction::TYPE_PRIORITY:
$current = $task->getPriority();
break;
case PhabricatorTransactions::TYPE_EDGE:
$current = $task->getProjectPHIDs();
break;
case PhabricatorTransactions::TYPE_SUBSCRIBERS:
$current = $task->getSubscriberPHIDs();
break;
}
}
// Check if the value is meaningful / provided, and normalize it if
// necessary. This discards, e.g., empty comments and empty owner
// changes.
$value = $action['value'];
switch ($type) {
case PhabricatorTransactions::TYPE_COMMENT:
if (!strlen($value)) {
continue 2;
}
break;
case ManiphestTransaction::TYPE_OWNER:
if (empty($value)) {
continue 2;
}
$value = head($value);
$no_owner = PhabricatorPeopleNoOwnerDatasource::FUNCTION_TOKEN;
if ($value === $no_owner) {
$value = null;
}
break;
case PhabricatorTransactions::TYPE_EDGE:
if (empty($value)) {
continue 2;
}
break;
case PhabricatorTransactions::TYPE_SUBSCRIBERS:
if (empty($value)) {
continue 2;
}
break;
}
// If the edit doesn't change anything, go to the next action. This
// check is only valid for changes like "owner", "status", etc, not
// for edge edits, because we should still apply an edit like
// "Remove Projects: A, B" to a task with projects "A, B".
if (empty($edge_edit_types[$action['action']])) {
if ($value == $current) {
continue;
}
}
// Apply the value change; for most edits this is just replacement, but
// some need to merge the current and edited values (add/remove project).
switch ($type) {
case PhabricatorTransactions::TYPE_COMMENT:
if (strlen($current)) {
$value = $current."\n\n".$value;
}
break;
case PhabricatorTransactions::TYPE_EDGE:
$is_remove = $action['action'] == 'remove_project';
$current = array_fill_keys($current, true);
$value = array_fill_keys($value, true);
$new = $current;
$did_something = false;
if ($is_remove) {
foreach ($value as $phid => $ignored) {
if (isset($new[$phid])) {
unset($new[$phid]);
$did_something = true;
}
}
} else {
foreach ($value as $phid => $ignored) {
if (empty($new[$phid])) {
$new[$phid] = true;
$did_something = true;
}
}
}
if (!$did_something) {
continue 2;
}
$value = array_keys($new);
break;
case PhabricatorTransactions::TYPE_SUBSCRIBERS:
$is_remove = $action['action'] == 'remove_ccs';
$current = array_fill_keys($current, true);
$new = array();
$did_something = false;
if ($is_remove) {
foreach ($value as $phid) {
if (isset($current[$phid])) {
$new[$phid] = true;
$did_something = true;
}
}
if ($new) {
$value = array('-' => array_keys($new));
}
} else {
$new = array();
foreach ($value as $phid) {
$new[$phid] = true;
$did_something = true;
}
if ($new) {
$value = array('+' => array_keys($new));
}
}
if (!$did_something) {
continue 2;
}
break;
}
$value_map[$type] = $value;
}
$template = new ManiphestTransaction();
foreach ($value_map as $type => $value) {
$xaction = clone $template;
$xaction->setTransactionType($type);
switch ($type) {
case PhabricatorTransactions::TYPE_COMMENT:
$xaction->attachComment(
id(new ManiphestTransactionComment())
->setContent($value));
break;
case PhabricatorTransactions::TYPE_EDGE:
$project_type = PhabricatorProjectObjectHasProjectEdgeType::EDGECONST;
$xaction
->setMetadataValue('edge:type', $project_type)
->setNewValue(
array(
'=' => array_fuse($value),
));
break;
default:
$xaction->setNewValue($value);
break;
}
$xactions[] = $xaction;
}
return $xactions;
}
}
diff --git a/src/applications/maniphest/controller/ManiphestExportController.php b/src/applications/maniphest/controller/ManiphestExportController.php
index 9c84c3949..6db04a6e3 100644
--- a/src/applications/maniphest/controller/ManiphestExportController.php
+++ b/src/applications/maniphest/controller/ManiphestExportController.php
@@ -1,137 +1,141 @@
<?php
final class ManiphestExportController extends ManiphestController {
private $key;
public function willProcessRequest(array $data) {
$this->key = $data['key'];
return $this;
}
/**
* @phutil-external-symbol class PHPExcel
* @phutil-external-symbol class PHPExcel_IOFactory
* @phutil-external-symbol class PHPExcel_Style_NumberFormat
* @phutil-external-symbol class PHPExcel_Cell_DataType
*/
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$ok = @include_once 'PHPExcel.php';
if (!$ok) {
$dialog = new AphrontDialogView();
$dialog->setUser($user);
$inst1 = pht(
'This system does not have PHPExcel installed. This software '.
'component is required to export tasks to Excel. Have your system '.
'administrator install it from:');
$inst2 = pht(
- 'Your PHP "include_path" needs to be updated to include the '.
- 'PHPExcel Classes directory.');
+ 'Your PHP "%s" needs to be updated to include the '.
+ 'PHPExcel Classes directory.',
+ 'include_path');
$dialog->setTitle(pht('Excel Export Not Configured'));
$dialog->appendChild(hsprintf(
'<p>%s</p>'.
'<br />'.
'<p>'.
'<a href="http://www.phpexcel.net/">http://www.phpexcel.net/</a>'.
'</p>'.
'<br />'.
'<p>%s</p>',
$inst1,
$inst2));
$dialog->addCancelButton('/maniphest/');
return id(new AphrontDialogResponse())->setDialog($dialog);
}
// TODO: PHPExcel has a dependency on the PHP zip extension. We should test
// for that here, since it fatals if we don't have the ZipArchive class.
$saved = id(new PhabricatorSavedQueryQuery())
->setViewer($user)
->withQueryKeys(array($this->key))
->executeOne();
if (!$saved) {
$engine = id(new ManiphestTaskSearchEngine())
->setViewer($user);
if ($engine->isBuiltinQuery($this->key)) {
$saved = $engine->buildSavedQueryFromBuiltin($this->key);
}
if (!$saved) {
return new Aphront404Response();
}
}
$formats = ManiphestExcelFormat::loadAllFormats();
$export_formats = array();
foreach ($formats as $format_class => $format_object) {
$export_formats[$format_class] = $format_object->getName();
}
if (!$request->isDialogFormPost()) {
$dialog = new AphrontDialogView();
$dialog->setUser($user);
$dialog->setTitle(pht('Export Tasks to Excel'));
- $dialog->appendChild(phutil_tag('p', array(), pht(
- 'Do you want to export the query results to Excel?')));
+ $dialog->appendChild(
+ phutil_tag(
+ 'p',
+ array(),
+ pht('Do you want to export the query results to Excel?')));
$form = id(new PHUIFormLayoutView())
->appendChild(
id(new AphrontFormSelectControl())
->setLabel(pht('Format:'))
->setName('excel-format')
->setOptions($export_formats));
$dialog->appendChild($form);
$dialog->addCancelButton('/maniphest/');
$dialog->addSubmitButton(pht('Export to Excel'));
return id(new AphrontDialogResponse())->setDialog($dialog);
}
$format = idx($formats, $request->getStr('excel-format'));
if ($format === null) {
- throw new Exception('Excel format object not found.');
+ throw new Exception(pht('Excel format object not found.'));
}
$saved->makeEphemeral();
$saved->setParameter('limit', PHP_INT_MAX);
$engine = id(new ManiphestTaskSearchEngine())
->setViewer($user);
$query = $engine->buildQueryFromSavedQuery($saved);
$query->setViewer($user);
$tasks = $query->execute();
$all_projects = array_mergev(mpull($tasks, 'getProjectPHIDs'));
$all_assigned = mpull($tasks, 'getOwnerPHID');
$handles = id(new PhabricatorHandleQuery())
->setViewer($user)
->withPHIDs(array_merge($all_projects, $all_assigned))
->execute();
$workbook = new PHPExcel();
$format->buildWorkbook($workbook, $tasks, $handles, $user);
$writer = PHPExcel_IOFactory::createWriter($workbook, 'Excel2007');
ob_start();
$writer->save('php://output');
$data = ob_get_clean();
$mime = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
return id(new AphrontFileResponse())
->setMimeType($mime)
->setDownload($format->getFileName().'.xlsx')
->setContent($data);
}
}
diff --git a/src/applications/maniphest/controller/ManiphestReportController.php b/src/applications/maniphest/controller/ManiphestReportController.php
index 3c6eca054..b03caed54 100644
--- a/src/applications/maniphest/controller/ManiphestReportController.php
+++ b/src/applications/maniphest/controller/ManiphestReportController.php
@@ -1,795 +1,795 @@
<?php
final class ManiphestReportController extends ManiphestController {
private $view;
public function willProcessRequest(array $data) {
$this->view = idx($data, 'view');
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
if ($request->isFormPost()) {
$uri = $request->getRequestURI();
$project = head($request->getArr('set_project'));
$project = nonempty($project, null);
$uri = $uri->alter('project', $project);
$window = $request->getStr('set_window');
$uri = $uri->alter('window', $window);
return id(new AphrontRedirectResponse())->setURI($uri);
}
$nav = new AphrontSideNavFilterView();
$nav->setBaseURI(new PhutilURI('/maniphest/report/'));
$nav->addLabel(pht('Open Tasks'));
$nav->addFilter('user', pht('By User'));
$nav->addFilter('project', pht('By Project'));
$nav->addLabel(pht('Burnup'));
$nav->addFilter('burn', pht('Burnup Rate'));
$this->view = $nav->selectFilter($this->view, 'user');
require_celerity_resource('maniphest-report-css');
switch ($this->view) {
case 'burn':
$core = $this->renderBurn();
break;
case 'user':
case 'project':
$core = $this->renderOpenTasks();
break;
default:
return new Aphront404Response();
}
$nav->appendChild($core);
$nav->setCrumbs(
$this->buildApplicationCrumbs()
->setBorder(true)
->addTextCrumb(pht('Reports')));
return $this->buildApplicationPage(
$nav,
array(
'title' => pht('Maniphest Reports'),
'device' => false,
));
}
public function renderBurn() {
$request = $this->getRequest();
$user = $request->getUser();
$handle = null;
$project_phid = $request->getStr('project');
if ($project_phid) {
$phids = array($project_phid);
$handles = $this->loadViewerHandles($phids);
$handle = $handles[$project_phid];
}
$table = new ManiphestTransaction();
$conn = $table->establishConnection('r');
$joins = '';
if ($project_phid) {
$joins = qsprintf(
$conn,
'JOIN %T t ON x.objectPHID = t.phid
JOIN %T p ON p.src = t.phid AND p.type = %d AND p.dst = %s',
id(new ManiphestTask())->getTableName(),
PhabricatorEdgeConfig::TABLE_NAME_EDGE,
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST,
$project_phid);
}
$data = queryfx_all(
$conn,
'SELECT x.oldValue, x.newValue, x.dateCreated FROM %T x %Q
WHERE transactionType = %s
ORDER BY x.dateCreated ASC',
$table->getTableName(),
$joins,
ManiphestTransaction::TYPE_STATUS);
$stats = array();
$day_buckets = array();
$open_tasks = array();
foreach ($data as $key => $row) {
// NOTE: Hack to avoid json_decode().
$oldv = trim($row['oldValue'], '"');
$newv = trim($row['newValue'], '"');
if ($oldv == 'null') {
$old_is_open = false;
} else {
$old_is_open = ManiphestTaskStatus::isOpenStatus($oldv);
}
$new_is_open = ManiphestTaskStatus::isOpenStatus($newv);
$is_open = ($new_is_open && !$old_is_open);
$is_close = ($old_is_open && !$new_is_open);
$data[$key]['_is_open'] = $is_open;
$data[$key]['_is_close'] = $is_close;
if (!$is_open && !$is_close) {
// This is either some kind of bogus event, or a resolution change
// (e.g., resolved -> invalid). Just skip it.
continue;
}
$day_bucket = phabricator_format_local_time(
$row['dateCreated'],
$user,
'Yz');
$day_buckets[$day_bucket] = $row['dateCreated'];
if (empty($stats[$day_bucket])) {
$stats[$day_bucket] = array(
'open' => 0,
'close' => 0,
);
}
$stats[$day_bucket][$is_close ? 'close' : 'open']++;
}
$template = array(
'open' => 0,
'close' => 0,
);
$rows = array();
$rowc = array();
$last_month = null;
$last_month_epoch = null;
$last_week = null;
$last_week_epoch = null;
$week = null;
$month = null;
$last = last_key($stats) - 1;
$period = $template;
foreach ($stats as $bucket => $info) {
$epoch = $day_buckets[$bucket];
$week_bucket = phabricator_format_local_time(
$epoch,
$user,
'YW');
if ($week_bucket != $last_week) {
if ($week) {
$rows[] = $this->formatBurnRow(
- 'Week of '.phabricator_date($last_week_epoch, $user),
+ pht('Week of %s', phabricator_date($last_week_epoch, $user)),
$week);
$rowc[] = 'week';
}
$week = $template;
$last_week = $week_bucket;
$last_week_epoch = $epoch;
}
$month_bucket = phabricator_format_local_time(
$epoch,
$user,
'Ym');
if ($month_bucket != $last_month) {
if ($month) {
$rows[] = $this->formatBurnRow(
phabricator_format_local_time($last_month_epoch, $user, 'F, Y'),
$month);
$rowc[] = 'month';
}
$month = $template;
$last_month = $month_bucket;
$last_month_epoch = $epoch;
}
$rows[] = $this->formatBurnRow(phabricator_date($epoch, $user), $info);
$rowc[] = null;
$week['open'] += $info['open'];
$week['close'] += $info['close'];
$month['open'] += $info['open'];
$month['close'] += $info['close'];
$period['open'] += $info['open'];
$period['close'] += $info['close'];
}
if ($week) {
$rows[] = $this->formatBurnRow(
pht('Week To Date'),
$week);
$rowc[] = 'week';
}
if ($month) {
$rows[] = $this->formatBurnRow(
pht('Month To Date'),
$month);
$rowc[] = 'month';
}
$rows[] = $this->formatBurnRow(
pht('All Time'),
$period);
$rowc[] = 'aggregate';
$rows = array_reverse($rows);
$rowc = array_reverse($rowc);
$table = new AphrontTableView($rows);
$table->setRowClasses($rowc);
$table->setHeaders(
array(
pht('Period'),
pht('Opened'),
pht('Closed'),
pht('Change'),
));
$table->setColumnClasses(
array(
'right wide',
'n',
'n',
'n',
));
if ($handle) {
$inst = pht(
'NOTE: This table reflects tasks currently in '.
'the project. If a task was opened in the past but added to '.
'the project recently, it is counted on the day it was '.
'opened, not the day it was categorized. If a task was part '.
'of this project in the past but no longer is, it is not '.
'counted at all.');
$header = pht('Task Burn Rate for Project %s', $handle->renderLink());
$caption = phutil_tag('p', array(), $inst);
} else {
$header = pht('Task Burn Rate for All Tasks');
$caption = null;
}
if ($caption) {
$caption = id(new PHUIInfoView())
->appendChild($caption)
->setSeverity(PHUIInfoView::SEVERITY_NOTICE);
}
$panel = new PHUIObjectBoxView();
$panel->setHeaderText($header);
if ($caption) {
$panel->setInfoView($caption);
}
$panel->appendChild($table);
$tokens = array();
if ($handle) {
$tokens = array($handle);
}
$filter = $this->renderReportFilters($tokens, $has_window = false);
$id = celerity_generate_unique_node_id();
$chart = phutil_tag(
'div',
array(
'id' => $id,
'style' => 'border: 1px solid #BFCFDA; '.
'background-color: #fff; '.
'margin: 8px 16px; '.
'height: 400px; ',
),
'');
list($burn_x, $burn_y) = $this->buildSeries($data);
require_celerity_resource('raphael-core');
require_celerity_resource('raphael-g');
require_celerity_resource('raphael-g-line');
Javelin::initBehavior('line-chart', array(
'hardpoint' => $id,
'x' => array(
$burn_x,
),
'y' => array(
$burn_y,
),
'xformat' => 'epoch',
'yformat' => 'int',
));
return array($filter, $chart, $panel);
}
private function renderReportFilters(array $tokens, $has_window) {
$request = $this->getRequest();
$user = $request->getUser();
$form = id(new AphrontFormView())
->setUser($user)
->appendControl(
id(new AphrontFormTokenizerControl())
->setDatasource(new PhabricatorProjectDatasource())
->setLabel(pht('Project'))
->setLimit(1)
->setName('set_project')
// TODO: This is silly, but this is Maniphest reports.
->setValue(mpull($tokens, 'getPHID')));
if ($has_window) {
list($window_str, $ignored, $window_error) = $this->getWindow();
$form
->appendChild(
id(new AphrontFormTextControl())
->setLabel(pht('Recently Means'))
->setName('set_window')
->setCaption(
pht('Configure the cutoff for the "Recently Closed" column.'))
->setValue($window_str)
->setError($window_error));
}
$form
->appendChild(
id(new AphrontFormSubmitControl())
->setValue(pht('Filter By Project')));
$filter = new AphrontListFilterView();
$filter->appendChild($form);
return $filter;
}
private function buildSeries(array $data) {
$out = array();
$counter = 0;
foreach ($data as $row) {
$t = (int)$row['dateCreated'];
if ($row['_is_close']) {
--$counter;
$out[$t] = $counter;
} else if ($row['_is_open']) {
++$counter;
$out[$t] = $counter;
}
}
return array(array_keys($out), array_values($out));
}
private function formatBurnRow($label, $info) {
$delta = $info['open'] - $info['close'];
$fmt = number_format($delta);
if ($delta > 0) {
$fmt = '+'.$fmt;
$fmt = phutil_tag('span', array('class' => 'red'), $fmt);
} else {
$fmt = phutil_tag('span', array('class' => 'green'), $fmt);
}
return array(
$label,
number_format($info['open']),
number_format($info['close']),
$fmt,
);
}
public function renderOpenTasks() {
$request = $this->getRequest();
$user = $request->getUser();
$query = id(new ManiphestTaskQuery())
->setViewer($user)
->withStatuses(ManiphestTaskStatus::getOpenStatusConstants());
switch ($this->view) {
case 'project':
$query->needProjectPHIDs(true);
break;
}
$project_phid = $request->getStr('project');
$project_handle = null;
if ($project_phid) {
$phids = array($project_phid);
$handles = $this->loadViewerHandles($phids);
$project_handle = $handles[$project_phid];
$query->withEdgeLogicPHIDs(
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST,
PhabricatorQueryConstraint::OPERATOR_OR,
$phids);
}
$tasks = $query->execute();
$recently_closed = $this->loadRecentlyClosedTasks();
$date = phabricator_date(time(), $user);
switch ($this->view) {
case 'user':
$result = mgroup($tasks, 'getOwnerPHID');
$leftover = idx($result, '', array());
unset($result['']);
$result_closed = mgroup($recently_closed, 'getOwnerPHID');
$leftover_closed = idx($result_closed, '', array());
unset($result_closed['']);
$base_link = '/maniphest/?assigned=';
$leftover_name = phutil_tag('em', array(), pht('(Up For Grabs)'));
$col_header = pht('User');
$header = pht('Open Tasks by User and Priority (%s)', $date);
break;
case 'project':
$result = array();
$leftover = array();
foreach ($tasks as $task) {
$phids = $task->getProjectPHIDs();
if ($phids) {
foreach ($phids as $project_phid) {
$result[$project_phid][] = $task;
}
} else {
$leftover[] = $task;
}
}
$result_closed = array();
$leftover_closed = array();
foreach ($recently_closed as $task) {
$phids = $task->getProjectPHIDs();
if ($phids) {
foreach ($phids as $project_phid) {
$result_closed[$project_phid][] = $task;
}
} else {
$leftover_closed[] = $task;
}
}
$base_link = '/maniphest/?projects=';
$leftover_name = phutil_tag('em', array(), pht('(No Project)'));
$col_header = pht('Project');
$header = pht('Open Tasks by Project and Priority (%s)', $date);
break;
}
$phids = array_keys($result);
$handles = $this->loadViewerHandles($phids);
$handles = msort($handles, 'getName');
$order = $request->getStr('order', 'name');
list($order, $reverse) = AphrontTableView::parseSort($order);
require_celerity_resource('aphront-tooltip-css');
Javelin::initBehavior('phabricator-tooltips', array());
$rows = array();
$pri_total = array();
foreach (array_merge($handles, array(null)) as $handle) {
if ($handle) {
if (($project_handle) &&
($project_handle->getPHID() == $handle->getPHID())) {
// If filtering by, e.g., "bugs", don't show a "bugs" group.
continue;
}
$tasks = idx($result, $handle->getPHID(), array());
$name = phutil_tag(
'a',
array(
'href' => $base_link.$handle->getPHID(),
),
$handle->getName());
$closed = idx($result_closed, $handle->getPHID(), array());
} else {
$tasks = $leftover;
$name = $leftover_name;
$closed = $leftover_closed;
}
$taskv = $tasks;
$tasks = mgroup($tasks, 'getPriority');
$row = array();
$row[] = $name;
$total = 0;
foreach (ManiphestTaskPriority::getTaskPriorityMap() as $pri => $label) {
$n = count(idx($tasks, $pri, array()));
if ($n == 0) {
$row[] = '-';
} else {
$row[] = number_format($n);
}
$total += $n;
}
$row[] = number_format($total);
list($link, $oldest_all) = $this->renderOldest($taskv);
$row[] = $link;
$normal_or_better = array();
foreach ($taskv as $id => $task) {
// TODO: This is sort of a hard-code for the default "normal" status.
// When reports are more powerful, this should be made more general.
if ($task->getPriority() < 50) {
continue;
}
$normal_or_better[$id] = $task;
}
list($link, $oldest_pri) = $this->renderOldest($normal_or_better);
$row[] = $link;
if ($closed) {
$task_ids = implode(',', mpull($closed, 'getID'));
$row[] = phutil_tag(
'a',
array(
'href' => '/maniphest/?ids='.$task_ids,
'target' => '_blank',
),
number_format(count($closed)));
} else {
$row[] = '-';
}
switch ($order) {
case 'total':
$row['sort'] = $total;
break;
case 'oldest-all':
$row['sort'] = $oldest_all;
break;
case 'oldest-pri':
$row['sort'] = $oldest_pri;
break;
case 'closed':
$row['sort'] = count($closed);
break;
case 'name':
default:
$row['sort'] = $handle ? $handle->getName() : '~';
break;
}
$rows[] = $row;
}
$rows = isort($rows, 'sort');
foreach ($rows as $k => $row) {
unset($rows[$k]['sort']);
}
if ($reverse) {
$rows = array_reverse($rows);
}
$cname = array($col_header);
$cclass = array('pri right wide');
$pri_map = ManiphestTaskPriority::getShortNameMap();
foreach ($pri_map as $pri => $label) {
$cname[] = $label;
$cclass[] = 'n';
}
$cname[] = 'Total';
$cclass[] = 'n';
$cname[] = javelin_tag(
'span',
array(
'sigil' => 'has-tooltip',
'meta' => array(
'tip' => pht('Oldest open task.'),
'size' => 200,
),
),
pht('Oldest (All)'));
$cclass[] = 'n';
$cname[] = javelin_tag(
'span',
array(
'sigil' => 'has-tooltip',
'meta' => array(
- 'tip' => pht('Oldest open task, excluding those with Low or '.
- 'Wishlist priority.'),
+ 'tip' => pht(
+ 'Oldest open task, excluding those with Low or Wishlist priority.'),
'size' => 200,
),
),
pht('Oldest (Pri)'));
$cclass[] = 'n';
list($ignored, $window_epoch) = $this->getWindow();
$edate = phabricator_datetime($window_epoch, $user);
$cname[] = javelin_tag(
'span',
array(
'sigil' => 'has-tooltip',
'meta' => array(
'tip' => pht('Closed after %s', $edate),
'size' => 260,
),
),
pht('Recently Closed'));
$cclass[] = 'n';
$table = new AphrontTableView($rows);
$table->setHeaders($cname);
$table->setColumnClasses($cclass);
$table->makeSortable(
$request->getRequestURI(),
'order',
$order,
$reverse,
array(
'name',
null,
null,
null,
null,
null,
null,
'total',
'oldest-all',
'oldest-pri',
'closed',
));
$panel = new PHUIObjectBoxView();
$panel->setHeaderText($header);
$panel->appendChild($table);
$tokens = array();
if ($project_handle) {
$tokens = array($project_handle);
}
$filter = $this->renderReportFilters($tokens, $has_window = true);
return array($filter, $panel);
}
/**
* Load all the tasks that have been recently closed.
*/
private function loadRecentlyClosedTasks() {
list($ignored, $window_epoch) = $this->getWindow();
$table = new ManiphestTask();
$xtable = new ManiphestTransaction();
$conn_r = $table->establishConnection('r');
// TODO: Gross. This table is not meant to be queried like this. Build
// real stats tables.
$open_status_list = array();
foreach (ManiphestTaskStatus::getOpenStatusConstants() as $constant) {
$open_status_list[] = json_encode((string)$constant);
}
$rows = queryfx_all(
$conn_r,
'SELECT t.id FROM %T t JOIN %T x ON x.objectPHID = t.phid
WHERE t.status NOT IN (%Ls)
AND x.oldValue IN (null, %Ls)
AND x.newValue NOT IN (%Ls)
AND t.dateModified >= %d
AND x.dateCreated >= %d',
$table->getTableName(),
$xtable->getTableName(),
ManiphestTaskStatus::getOpenStatusConstants(),
$open_status_list,
$open_status_list,
$window_epoch,
$window_epoch);
if (!$rows) {
return array();
}
$ids = ipull($rows, 'id');
$query = id(new ManiphestTaskQuery())
->setViewer($this->getRequest()->getUser())
->withIDs($ids);
switch ($this->view) {
case 'project':
$query->needProjectPHIDs(true);
break;
}
return $query->execute();
}
/**
* Parse the "Recently Means" filter into:
*
* - A string representation, like "12 AM 7 days ago" (default);
* - a locale-aware epoch representation; and
* - a possible error.
*/
private function getWindow() {
$request = $this->getRequest();
$user = $request->getUser();
$window_str = $this->getRequest()->getStr('window', '12 AM 7 days ago');
$error = null;
$window_epoch = null;
// Do locale-aware parsing so that the user's timezone is assumed for
// time windows like "3 PM", rather than assuming the server timezone.
$window_epoch = PhabricatorTime::parseLocalTime($window_str, $user);
if (!$window_epoch) {
$error = 'Invalid';
$window_epoch = time() - (60 * 60 * 24 * 7);
}
// If the time ends up in the future, convert it to the corresponding time
// and equal distance in the past. This is so users can type "6 days" (which
// means "6 days from now") and get the behavior of "6 days ago", rather
// than no results (because the window epoch is in the future). This might
// be a little confusing because it casues "tomorrow" to mean "yesterday"
// and "2022" (or whatever) to mean "ten years ago", but these inputs are
// nonsense anyway.
if ($window_epoch > time()) {
$window_epoch = time() - ($window_epoch - time());
}
return array($window_str, $window_epoch, $error);
}
private function renderOldest(array $tasks) {
assert_instances_of($tasks, 'ManiphestTask');
$oldest = null;
foreach ($tasks as $id => $task) {
if (($oldest === null) ||
($task->getDateCreated() < $tasks[$oldest]->getDateCreated())) {
$oldest = $id;
}
}
if ($oldest === null) {
return array('-', 0);
}
$oldest = $tasks[$oldest];
$raw_age = (time() - $oldest->getDateCreated());
$age = number_format($raw_age / (24 * 60 * 60)).' d';
$link = javelin_tag(
'a',
array(
'href' => '/T'.$oldest->getID(),
'sigil' => 'has-tooltip',
'meta' => array(
'tip' => 'T'.$oldest->getID().': '.$oldest->getTitle(),
),
'target' => '_blank',
),
$age);
return array($link, $raw_age);
}
}
diff --git a/src/applications/maniphest/controller/ManiphestTransactionSaveController.php b/src/applications/maniphest/controller/ManiphestTransactionSaveController.php
index f12171a47..f7693d941 100644
--- a/src/applications/maniphest/controller/ManiphestTransactionSaveController.php
+++ b/src/applications/maniphest/controller/ManiphestTransactionSaveController.php
@@ -1,210 +1,210 @@
<?php
final class ManiphestTransactionSaveController extends ManiphestController {
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$task = id(new ManiphestTaskQuery())
->setViewer($user)
->withIDs(array($request->getStr('taskID')))
->needSubscriberPHIDs(true)
->needProjectPHIDs(true)
->executeOne();
if (!$task) {
return new Aphront404Response();
}
$task_uri = '/'.$task->getMonogram();
$transactions = array();
$action = $request->getStr('action');
$implicit_ccs = array();
$explicit_ccs = array();
$transaction = new ManiphestTransaction();
$transaction
->setTransactionType($action);
switch ($action) {
case ManiphestTransaction::TYPE_STATUS:
$transaction->setNewValue($request->getStr('resolution'));
break;
case ManiphestTransaction::TYPE_OWNER:
$assign_to = $request->getArr('assign_to');
$assign_to = reset($assign_to);
$transaction->setNewValue($assign_to);
break;
case PhabricatorTransactions::TYPE_EDGE:
$projects = $request->getArr('projects');
$projects = array_merge($projects, $task->getProjectPHIDs());
$projects = array_filter($projects);
$projects = array_unique($projects);
$project_type = PhabricatorProjectObjectHasProjectEdgeType::EDGECONST;
$transaction
->setMetadataValue('edge:type', $project_type)
->setNewValue(
array(
'+' => array_fuse($projects),
));
break;
case PhabricatorTransactions::TYPE_SUBSCRIBERS:
// Accumulate the new explicit CCs into the array that we'll add in
// the CC transaction later.
$explicit_ccs = $request->getArr('ccs');
// Throw away the primary transaction.
$transaction = null;
break;
case ManiphestTransaction::TYPE_PRIORITY:
$transaction->setNewValue($request->getInt('priority'));
break;
case PhabricatorTransactions::TYPE_COMMENT:
// Nuke this, we're going to create it below.
$transaction = null;
break;
default:
- throw new Exception('unknown action');
+ throw new Exception(pht("Unknown action '%s'!", $action));
}
if ($transaction) {
$transactions[] = $transaction;
}
// When you interact with a task, we add you to the CC list so you get
// further updates, and possibly assign the task to you if you took an
// ownership action (closing it) but it's currently unowned. We also move
// previous owners to CC if ownership changes. Detect all these conditions
// and create side-effect transactions for them.
$implicitly_claimed = false;
if ($action == ManiphestTransaction::TYPE_OWNER) {
if ($task->getOwnerPHID() == $transaction->getNewValue()) {
// If this is actually no-op, don't generate the side effect.
} else {
// Otherwise, when a task is reassigned, move the previous owner to CC.
if ($task->getOwnerPHID()) {
$implicit_ccs[] = $task->getOwnerPHID();
}
}
}
if ($action == ManiphestTransaction::TYPE_STATUS) {
$resolution = $request->getStr('resolution');
if (!$task->getOwnerPHID() &&
ManiphestTaskStatus::isClosedStatus($resolution)) {
// Closing an unassigned task. Assign the user as the owner of
// this task.
$assign = new ManiphestTransaction();
$assign->setTransactionType(ManiphestTransaction::TYPE_OWNER);
$assign->setNewValue($user->getPHID());
$transactions[] = $assign;
$implicitly_claimed = true;
}
}
$user_owns_task = false;
if ($implicitly_claimed) {
$user_owns_task = true;
} else {
if ($action == ManiphestTransaction::TYPE_OWNER) {
if ($transaction->getNewValue() == $user->getPHID()) {
$user_owns_task = true;
}
} else if ($task->getOwnerPHID() == $user->getPHID()) {
$user_owns_task = true;
}
}
if (!$user_owns_task) {
// If we aren't making the user the new task owner and they aren't the
// existing task owner, add them to CC unless they're aleady CC'd.
if (!in_array($user->getPHID(), $task->getSubscriberPHIDs())) {
$implicit_ccs[] = $user->getPHID();
}
}
if ($implicit_ccs || $explicit_ccs) {
// TODO: These implicit CC rules should probably be handled inside the
// Editor, eventually.
$all_ccs = array_fuse($implicit_ccs) + array_fuse($explicit_ccs);
$cc_transaction = id(new ManiphestTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS)
->setNewValue(array('+' => $all_ccs));
if (!$explicit_ccs) {
$cc_transaction->setIgnoreOnNoEffect(true);
}
$transactions[] = $cc_transaction;
}
$comments = $request->getStr('comments');
if (strlen($comments) || !$transactions) {
$transactions[] = id(new ManiphestTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_COMMENT)
->attachComment(
id(new ManiphestTransactionComment())
->setContent($comments));
}
$event = new PhabricatorEvent(
PhabricatorEventType::TYPE_MANIPHEST_WILLEDITTASK,
array(
'task' => $task,
'new' => false,
'transactions' => $transactions,
));
$event->setUser($user);
$event->setAphrontRequest($request);
PhutilEventEngine::dispatchEvent($event);
$task = $event->getValue('task');
$transactions = $event->getValue('transactions');
$editor = id(new ManiphestTransactionEditor())
->setActor($user)
->setContentSourceFromRequest($request)
->setContinueOnMissingFields(true)
->setContinueOnNoEffect($request->isContinueRequest());
try {
$editor->applyTransactions($task, $transactions);
} catch (PhabricatorApplicationTransactionNoEffectException $ex) {
return id(new PhabricatorApplicationTransactionNoEffectResponse())
->setCancelURI($task_uri)
->setException($ex);
}
$draft = id(new PhabricatorDraft())->loadOneWhere(
'authorPHID = %s AND draftKey = %s',
$user->getPHID(),
$task->getPHID());
if ($draft) {
$draft->delete();
}
$event = new PhabricatorEvent(
PhabricatorEventType::TYPE_MANIPHEST_DIDEDITTASK,
array(
'task' => $task,
'new' => false,
'transactions' => $transactions,
));
$event->setUser($user);
$event->setAphrontRequest($request);
PhutilEventEngine::dispatchEvent($event);
return id(new AphrontRedirectResponse())->setURI($task_uri);
}
}
diff --git a/src/applications/maniphest/editor/ManiphestTransactionEditor.php b/src/applications/maniphest/editor/ManiphestTransactionEditor.php
index abc83369b..3609145c7 100644
--- a/src/applications/maniphest/editor/ManiphestTransactionEditor.php
+++ b/src/applications/maniphest/editor/ManiphestTransactionEditor.php
@@ -1,742 +1,746 @@
<?php
final class ManiphestTransactionEditor
extends PhabricatorApplicationTransactionEditor {
public function getEditorApplicationClass() {
return 'PhabricatorManiphestApplication';
}
public function getEditorObjectsDescription() {
return pht('Maniphest Tasks');
}
public function getTransactionTypes() {
$types = parent::getTransactionTypes();
$types[] = PhabricatorTransactions::TYPE_COMMENT;
$types[] = PhabricatorTransactions::TYPE_EDGE;
$types[] = ManiphestTransaction::TYPE_PRIORITY;
$types[] = ManiphestTransaction::TYPE_STATUS;
$types[] = ManiphestTransaction::TYPE_TITLE;
$types[] = ManiphestTransaction::TYPE_DESCRIPTION;
$types[] = ManiphestTransaction::TYPE_OWNER;
$types[] = ManiphestTransaction::TYPE_SUBPRIORITY;
$types[] = ManiphestTransaction::TYPE_PROJECT_COLUMN;
$types[] = ManiphestTransaction::TYPE_MERGED_INTO;
$types[] = ManiphestTransaction::TYPE_MERGED_FROM;
$types[] = ManiphestTransaction::TYPE_UNBLOCK;
$types[] = PhabricatorTransactions::TYPE_VIEW_POLICY;
$types[] = PhabricatorTransactions::TYPE_EDIT_POLICY;
return $types;
}
protected function getCustomTransactionOldValue(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case ManiphestTransaction::TYPE_PRIORITY:
if ($this->getIsNewObject()) {
return null;
}
return (int)$object->getPriority();
case ManiphestTransaction::TYPE_STATUS:
if ($this->getIsNewObject()) {
return null;
}
return $object->getStatus();
case ManiphestTransaction::TYPE_TITLE:
if ($this->getIsNewObject()) {
return null;
}
return $object->getTitle();
case ManiphestTransaction::TYPE_DESCRIPTION:
if ($this->getIsNewObject()) {
return null;
}
return $object->getDescription();
case ManiphestTransaction::TYPE_OWNER:
return nonempty($object->getOwnerPHID(), null);
case ManiphestTransaction::TYPE_PROJECT_COLUMN:
// These are pre-populated.
return $xaction->getOldValue();
case ManiphestTransaction::TYPE_SUBPRIORITY:
return $object->getSubpriority();
case ManiphestTransaction::TYPE_MERGED_INTO:
case ManiphestTransaction::TYPE_MERGED_FROM:
return null;
}
}
protected function getCustomTransactionNewValue(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case ManiphestTransaction::TYPE_PRIORITY:
return (int)$xaction->getNewValue();
case ManiphestTransaction::TYPE_OWNER:
return nonempty($xaction->getNewValue(), null);
case ManiphestTransaction::TYPE_STATUS:
case ManiphestTransaction::TYPE_TITLE:
case ManiphestTransaction::TYPE_DESCRIPTION:
case ManiphestTransaction::TYPE_SUBPRIORITY:
case ManiphestTransaction::TYPE_PROJECT_COLUMN:
case ManiphestTransaction::TYPE_MERGED_INTO:
case ManiphestTransaction::TYPE_MERGED_FROM:
case ManiphestTransaction::TYPE_UNBLOCK:
return $xaction->getNewValue();
}
}
protected function transactionHasEffect(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
$old = $xaction->getOldValue();
$new = $xaction->getNewValue();
switch ($xaction->getTransactionType()) {
case ManiphestTransaction::TYPE_PROJECT_COLUMN:
$new_column_phids = $new['columnPHIDs'];
$old_column_phids = $old['columnPHIDs'];
sort($new_column_phids);
sort($old_column_phids);
return ($old !== $new);
}
return parent::transactionHasEffect($object, $xaction);
}
protected function applyCustomInternalTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case ManiphestTransaction::TYPE_PRIORITY:
return $object->setPriority($xaction->getNewValue());
case ManiphestTransaction::TYPE_STATUS:
return $object->setStatus($xaction->getNewValue());
case ManiphestTransaction::TYPE_TITLE:
return $object->setTitle($xaction->getNewValue());
case ManiphestTransaction::TYPE_DESCRIPTION:
return $object->setDescription($xaction->getNewValue());
case ManiphestTransaction::TYPE_OWNER:
$phid = $xaction->getNewValue();
// Update the "ownerOrdering" column to contain the full name of the
// owner, if the task is assigned.
$handle = null;
if ($phid) {
$handle = id(new PhabricatorHandleQuery())
->setViewer($this->getActor())
->withPHIDs(array($phid))
->executeOne();
}
if ($handle) {
$object->setOwnerOrdering($handle->getName());
} else {
$object->setOwnerOrdering(null);
}
return $object->setOwnerPHID($phid);
case ManiphestTransaction::TYPE_SUBPRIORITY:
$object->setSubpriority($xaction->getNewValue());
return;
case ManiphestTransaction::TYPE_PROJECT_COLUMN:
// these do external (edge) updates
return;
case ManiphestTransaction::TYPE_MERGED_INTO:
$object->setStatus(ManiphestTaskStatus::getDuplicateStatus());
return;
case ManiphestTransaction::TYPE_MERGED_FROM:
return;
}
}
protected function applyCustomExternalTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case ManiphestTransaction::TYPE_PROJECT_COLUMN:
$board_phid = idx($xaction->getNewValue(), 'projectPHID');
if (!$board_phid) {
throw new Exception(
- pht("Expected 'projectPHID' in column transaction."));
+ pht(
+ "Expected '%s' in column transaction.",
+ 'projectPHID'));
}
$old_phids = idx($xaction->getOldValue(), 'columnPHIDs', array());
$new_phids = idx($xaction->getNewValue(), 'columnPHIDs', array());
if (count($new_phids) !== 1) {
throw new Exception(
- pht("Expected exactly one 'columnPHIDs' in column transaction."));
+ pht(
+ "Expected exactly one '%s' in column transaction.",
+ 'columnPHIDs'));
}
$columns = id(new PhabricatorProjectColumnQuery())
->setViewer($this->requireActor())
->withPHIDs($new_phids)
->execute();
$columns = mpull($columns, null, 'getPHID');
$positions = id(new PhabricatorProjectColumnPositionQuery())
->setViewer($this->requireActor())
->withObjectPHIDs(array($object->getPHID()))
->withBoardPHIDs(array($board_phid))
->execute();
$before_phid = idx($xaction->getNewValue(), 'beforePHID');
$after_phid = idx($xaction->getNewValue(), 'afterPHID');
if (!$before_phid && !$after_phid && ($old_phids == $new_phids)) {
// If we are not moving the object between columns and also not
// reordering the position, this is a move on some other order
// (like priority). We can leave the positions untouched and just
// bail, there's no work to be done.
return;
}
// Otherwise, we're either moving between columns or adjusting the
// object's position in the "natural" ordering, so we do need to update
// some rows.
// Remove all existing column positions on the board.
foreach ($positions as $position) {
$position->delete();
}
// Add the new column positions.
foreach ($new_phids as $phid) {
$column = idx($columns, $phid);
if (!$column) {
throw new Exception(
pht('No such column "%s" exists!', $phid));
}
// Load the other object positions in the column. Note that we must
// skip implicit column creation to avoid generating a new position
// if the target column is a backlog column.
$other_positions = id(new PhabricatorProjectColumnPositionQuery())
->setViewer($this->requireActor())
->withColumns(array($column))
->withBoardPHIDs(array($board_phid))
->setSkipImplicitCreate(true)
->execute();
$other_positions = msort($other_positions, 'getOrderingKey');
// Set up the new position object. We're going to figure out the
// right sequence number and then persist this object with that
// sequence number.
$new_position = id(new PhabricatorProjectColumnPosition())
->setBoardPHID($board_phid)
->setColumnPHID($column->getPHID())
->setObjectPHID($object->getPHID());
$updates = array();
$sequence = 0;
// If we're just dropping this into the column without any specific
// position information, put it at the top.
if (!$before_phid && !$after_phid) {
$new_position->setSequence($sequence)->save();
$sequence++;
}
foreach ($other_positions as $position) {
$object_phid = $position->getObjectPHID();
// If this is the object we're moving before and we haven't
// saved yet, insert here.
if (($before_phid == $object_phid) && !$new_position->getID()) {
$new_position->setSequence($sequence)->save();
$sequence++;
}
// This object goes here in the sequence; we might need to update
// the row.
if ($sequence != $position->getSequence()) {
$updates[$position->getID()] = $sequence;
}
$sequence++;
// If this is the object we're moving after and we haven't saved
// yet, insert here.
if (($after_phid == $object_phid) && !$new_position->getID()) {
$new_position->setSequence($sequence)->save();
$sequence++;
}
}
// We should have found a place to put it.
if (!$new_position->getID()) {
throw new Exception(
pht('Unable to find a place to insert object on column!'));
}
// If we changed other objects' column positions, bulk reorder them.
if ($updates) {
$position = new PhabricatorProjectColumnPosition();
$conn_w = $position->establishConnection('w');
$pairs = array();
foreach ($updates as $id => $sequence) {
// This is ugly because MySQL gets upset with us if it is
// configured strictly and we attempt inserts which can't work.
// We'll never actually do these inserts since they'll always
// collide (triggering the ON DUPLICATE KEY logic), so we just
// provide dummy values in order to get there.
$pairs[] = qsprintf(
$conn_w,
'(%d, %d, "", "", "")',
$id,
$sequence);
}
queryfx(
$conn_w,
'INSERT INTO %T (id, sequence, boardPHID, columnPHID, objectPHID)
VALUES %Q ON DUPLICATE KEY UPDATE sequence = VALUES(sequence)',
$position->getTableName(),
implode(', ', $pairs));
}
}
break;
default:
break;
}
}
protected function applyFinalEffects(
PhabricatorLiskDAO $object,
array $xactions) {
// When we change the status of a task, update tasks this tasks blocks
// with a message to the effect of "alincoln resolved blocking task Txxx."
$unblock_xaction = null;
foreach ($xactions as $xaction) {
switch ($xaction->getTransactionType()) {
case ManiphestTransaction::TYPE_STATUS:
$unblock_xaction = $xaction;
break;
}
}
if ($unblock_xaction !== null) {
$blocked_phids = PhabricatorEdgeQuery::loadDestinationPHIDs(
$object->getPHID(),
ManiphestTaskDependedOnByTaskEdgeType::EDGECONST);
if ($blocked_phids) {
// In theory we could apply these through policies, but that seems a
// little bit surprising. For now, use the actor's vision.
$blocked_tasks = id(new ManiphestTaskQuery())
->setViewer($this->getActor())
->withPHIDs($blocked_phids)
->needSubscriberPHIDs(true)
->needProjectPHIDs(true)
->execute();
$old = $unblock_xaction->getOldValue();
$new = $unblock_xaction->getNewValue();
foreach ($blocked_tasks as $blocked_task) {
$unblock_xactions = array();
$unblock_xactions[] = id(new ManiphestTransaction())
->setTransactionType(ManiphestTransaction::TYPE_UNBLOCK)
->setOldValue(array($object->getPHID() => $old))
->setNewValue(array($object->getPHID() => $new));
id(new ManiphestTransactionEditor())
->setActor($this->getActor())
->setActingAsPHID($this->getActingAsPHID())
->setContentSource($this->getContentSource())
->setContinueOnNoEffect(true)
->setContinueOnMissingFields(true)
->applyTransactions($blocked_task, $unblock_xactions);
}
}
}
return $xactions;
}
protected function shouldSendMail(
PhabricatorLiskDAO $object,
array $xactions) {
$xactions = mfilter($xactions, 'shouldHide', true);
return $xactions;
}
protected function getMailSubjectPrefix() {
return PhabricatorEnv::getEnvConfig('metamta.maniphest.subject-prefix');
}
protected function getMailThreadID(PhabricatorLiskDAO $object) {
return 'maniphest-task-'.$object->getPHID();
}
protected function getMailTo(PhabricatorLiskDAO $object) {
$phids = array();
if ($object->getOwnerPHID()) {
$phids[] = $object->getOwnerPHID();
}
$phids[] = $this->getActingAsPHID();
return $phids;
}
public function getMailTagsMap() {
return array(
ManiphestTransaction::MAILTAG_STATUS =>
pht("A task's status changes."),
ManiphestTransaction::MAILTAG_OWNER =>
pht("A task's owner changes."),
ManiphestTransaction::MAILTAG_PRIORITY =>
pht("A task's priority changes."),
ManiphestTransaction::MAILTAG_CC =>
pht("A task's subscribers change."),
ManiphestTransaction::MAILTAG_PROJECTS =>
pht("A task's associated projects change."),
ManiphestTransaction::MAILTAG_UNBLOCK =>
pht('One of the tasks a task is blocked by changes status.'),
ManiphestTransaction::MAILTAG_COLUMN =>
pht('A task is moved between columns on a workboard.'),
ManiphestTransaction::MAILTAG_COMMENT =>
pht('Someone comments on a task.'),
ManiphestTransaction::MAILTAG_OTHER =>
pht('Other task activity not listed above occurs.'),
);
}
protected function buildReplyHandler(PhabricatorLiskDAO $object) {
return id(new ManiphestReplyHandler())
->setMailReceiver($object);
}
protected function buildMailTemplate(PhabricatorLiskDAO $object) {
$id = $object->getID();
$title = $object->getTitle();
return id(new PhabricatorMetaMTAMail())
->setSubject("T{$id}: {$title}")
->addHeader('Thread-Topic', "T{$id}: ".$object->getOriginalTitle());
}
protected function buildMailBody(
PhabricatorLiskDAO $object,
array $xactions) {
$body = parent::buildMailBody($object, $xactions);
if ($this->getIsNewObject()) {
$body->addTextSection(
pht('TASK DESCRIPTION'),
$object->getDescription());
}
$body->addLinkSection(
pht('TASK DETAIL'),
PhabricatorEnv::getProductionURI('/T'.$object->getID()));
$board_phids = array();
$type_column = ManiphestTransaction::TYPE_PROJECT_COLUMN;
foreach ($xactions as $xaction) {
if ($xaction->getTransactionType() == $type_column) {
$new = $xaction->getNewValue();
$project_phid = idx($new, 'projectPHID');
if ($project_phid) {
$board_phids[] = $project_phid;
}
}
}
if ($board_phids) {
$projects = id(new PhabricatorProjectQuery())
->setViewer($this->requireActor())
->withPHIDs($board_phids)
->execute();
foreach ($projects as $project) {
$body->addLinkSection(
pht('WORKBOARD'),
PhabricatorEnv::getProductionURI(
'/project/board/'.$project->getID().'/'));
}
}
return $body;
}
protected function shouldPublishFeedStory(
PhabricatorLiskDAO $object,
array $xactions) {
return $this->shouldSendMail($object, $xactions);
}
protected function supportsSearch() {
return true;
}
protected function shouldApplyHeraldRules(
PhabricatorLiskDAO $object,
array $xactions) {
return true;
}
protected function buildHeraldAdapter(
PhabricatorLiskDAO $object,
array $xactions) {
return id(new HeraldManiphestTaskAdapter())
->setTask($object);
}
protected function didApplyHeraldRules(
PhabricatorLiskDAO $object,
HeraldAdapter $adapter,
HeraldTranscript $transcript) {
$xactions = array();
$cc_phids = $adapter->getCcPHIDs();
if ($cc_phids) {
$xactions[] = id(new ManiphestTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS)
->setNewValue(array('+' => $cc_phids));
}
$assign_phid = $adapter->getAssignPHID();
if ($assign_phid) {
$xactions[] = id(new ManiphestTransaction())
->setTransactionType(ManiphestTransaction::TYPE_OWNER)
->setNewValue($assign_phid);
}
return $xactions;
}
protected function requireCapabilities(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
parent::requireCapabilities($object, $xaction);
$app_capability_map = array(
ManiphestTransaction::TYPE_PRIORITY =>
ManiphestEditPriorityCapability::CAPABILITY,
ManiphestTransaction::TYPE_STATUS =>
ManiphestEditStatusCapability::CAPABILITY,
ManiphestTransaction::TYPE_OWNER =>
ManiphestEditAssignCapability::CAPABILITY,
PhabricatorTransactions::TYPE_EDIT_POLICY =>
ManiphestEditPoliciesCapability::CAPABILITY,
PhabricatorTransactions::TYPE_VIEW_POLICY =>
ManiphestEditPoliciesCapability::CAPABILITY,
);
$transaction_type = $xaction->getTransactionType();
$app_capability = null;
if ($transaction_type == PhabricatorTransactions::TYPE_EDGE) {
switch ($xaction->getMetadataValue('edge:type')) {
case PhabricatorProjectObjectHasProjectEdgeType::EDGECONST:
$app_capability = ManiphestEditProjectsCapability::CAPABILITY;
break;
}
} else {
$app_capability = idx($app_capability_map, $transaction_type);
}
if ($app_capability) {
$app = id(new PhabricatorApplicationQuery())
->setViewer($this->getActor())
->withClasses(array('PhabricatorManiphestApplication'))
->executeOne();
PhabricatorPolicyFilter::requireCapability(
$this->getActor(),
$app,
$app_capability);
}
}
protected function adjustObjectForPolicyChecks(
PhabricatorLiskDAO $object,
array $xactions) {
$copy = parent::adjustObjectForPolicyChecks($object, $xactions);
foreach ($xactions as $xaction) {
switch ($xaction->getTransactionType()) {
case ManiphestTransaction::TYPE_OWNER:
$copy->setOwnerPHID($xaction->getNewValue());
break;
default:
continue;
}
}
return $copy;
}
/**
* Get priorities for moving a task to a new priority.
*/
public static function getEdgeSubpriority(
$priority,
$is_end) {
$query = id(new ManiphestTaskQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withPriorities(array($priority))
->setLimit(1);
if ($is_end) {
$query->setOrderVector(array('-priority', '-subpriority', '-id'));
} else {
$query->setOrderVector(array('priority', 'subpriority', 'id'));
}
$result = $query->executeOne();
$step = (double)(2 << 32);
if ($result) {
$base = $result->getSubpriority();
if ($is_end) {
$sub = ($base - $step);
} else {
$sub = ($base + $step);
}
} else {
$sub = 0;
}
return array($priority, $sub);
}
/**
* Get priorities for moving a task before or after another task.
*/
public static function getAdjacentSubpriority(
ManiphestTask $dst,
$is_after,
$allow_recursion = true) {
$query = id(new ManiphestTaskQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->setOrderBy(ManiphestTaskQuery::ORDER_PRIORITY)
->withPriorities(array($dst->getPriority()))
->setLimit(1);
if ($is_after) {
$query->setAfterID($dst->getID());
} else {
$query->setBeforeID($dst->getID());
}
$adjacent = $query->executeOne();
$base = $dst->getSubpriority();
$step = (double)(2 << 32);
// If we find an adjacent task, we average the two subpriorities and
// return the result.
if ($adjacent) {
$epsilon = 0.01;
// If the adjacent task has a subpriority that is identical or very
// close to the task we're looking at, we're going to move it and all
// tasks with the same subpriority a little farther down the subpriority
// scale.
if ($allow_recursion &&
(abs($adjacent->getSubpriority() - $base) < $epsilon)) {
$conn_w = $adjacent->establishConnection('w');
$min = ($adjacent->getSubpriority() - ($epsilon));
$max = ($adjacent->getSubpriority() + ($epsilon));
// Get all of the tasks with the similar subpriorities to the adjacent
// task, including the adjacent task itself.
$query = id(new ManiphestTaskQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withPriorities(array($adjacent->getPriority()))
->withSubpriorityBetween($min, $max);
if (!$is_after) {
$query->setOrderVector(array('-priority', '-subpriority', '-id'));
} else {
$query->setOrderVector(array('priority', 'subpriority', 'id'));
}
$shift_all = $query->execute();
$shift_last = last($shift_all);
// Select the most extreme subpriority in the result set as the
// base value.
$shift_base = head($shift_all)->getSubpriority();
// Find the subpriority before or after the task at the end of the
// block.
list($shift_pri, $shift_sub) = self::getAdjacentSubpriority(
$shift_last,
$is_after,
$allow_recursion = false);
$delta = ($shift_sub - $shift_base);
$count = count($shift_all);
$shift = array();
$cursor = 1;
foreach ($shift_all as $shift_task) {
$shift_target = $shift_base + (($cursor / $count) * $delta);
$cursor++;
queryfx(
$conn_w,
'UPDATE %T SET subpriority = %f WHERE id = %d',
$adjacent->getTableName(),
$shift_target,
$shift_task->getID());
// If we're shifting the adjacent task, update it.
if ($shift_task->getID() == $adjacent->getID()) {
$adjacent->setSubpriority($shift_target);
}
// If we're shifting the original target task, update the base
// subpriority.
if ($shift_task->getID() == $dst->getID()) {
$base = $shift_target;
}
}
}
$sub = ($adjacent->getSubpriority() + $base) / 2;
} else {
// Otherwise, we take a step away from the target's subpriority and
// use that.
if ($is_after) {
$sub = ($base - $step);
} else {
$sub = ($base + $step);
}
}
return array($dst->getPriority(), $sub);
}
}
diff --git a/src/applications/maniphest/mail/ManiphestCreateMailReceiver.php b/src/applications/maniphest/mail/ManiphestCreateMailReceiver.php
index 2df618c87..0102b1bd7 100644
--- a/src/applications/maniphest/mail/ManiphestCreateMailReceiver.php
+++ b/src/applications/maniphest/mail/ManiphestCreateMailReceiver.php
@@ -1,36 +1,36 @@
<?php
final class ManiphestCreateMailReceiver extends PhabricatorMailReceiver {
public function isEnabled() {
- $app_class = 'PhabricatorManiphestApplication';
- return PhabricatorApplication::isClassInstalled($app_class);
+ return PhabricatorApplication::isClassInstalled(
+ 'PhabricatorManiphestApplication');
}
public function canAcceptMail(PhabricatorMetaMTAReceivedMail $mail) {
$maniphest_app = new PhabricatorManiphestApplication();
return $this->canAcceptApplicationMail($maniphest_app, $mail);
}
protected function processReceivedMail(
PhabricatorMetaMTAReceivedMail $mail,
PhabricatorUser $sender) {
$task = ManiphestTask::initializeNewTask($sender);
$task->setOriginalEmailSource($mail->getHeader('From'));
$handler = new ManiphestReplyHandler();
$handler->setMailReceiver($task);
$handler->setActor($sender);
$handler->setExcludeMailRecipientPHIDs(
$mail->loadExcludeMailRecipientPHIDs());
if ($this->getApplicationEmail()) {
$handler->setApplicationEmail($this->getApplicationEmail());
}
$handler->processEmail($mail);
$mail->setRelatedPHID($task->getPHID());
}
}
diff --git a/src/applications/maniphest/mail/ManiphestReplyHandler.php b/src/applications/maniphest/mail/ManiphestReplyHandler.php
index f95f20311..af96de70d 100644
--- a/src/applications/maniphest/mail/ManiphestReplyHandler.php
+++ b/src/applications/maniphest/mail/ManiphestReplyHandler.php
@@ -1,39 +1,39 @@
<?php
final class ManiphestReplyHandler
extends PhabricatorApplicationTransactionReplyHandler {
public function validateMailReceiver($mail_receiver) {
if (!($mail_receiver instanceof ManiphestTask)) {
- throw new Exception('Mail receiver is not a ManiphestTask!');
+ throw new Exception(pht('Mail receiver is not a %s!', 'ManiphestTask'));
}
}
public function getObjectPrefix() {
return 'T';
}
protected function didReceiveMail(
PhabricatorMetaMTAReceivedMail $mail,
$body) {
$object = $this->getMailReceiver();
$is_new = !$object->getID();
$xactions = array();
if ($is_new) {
$xactions[] = $object->getApplicationTransactionTemplate()
->setTransactionType(ManiphestTransaction::TYPE_TITLE)
->setNewValue(nonempty($mail->getSubject(), pht('Untitled Task')));
$xactions[] = $object->getApplicationTransactionTemplate()
->setTransactionType(ManiphestTransaction::TYPE_DESCRIPTION)
->setNewValue($body);
}
return $xactions;
}
}
diff --git a/src/applications/maniphest/mail/ManiphestTaskMailReceiver.php b/src/applications/maniphest/mail/ManiphestTaskMailReceiver.php
index 124b5d8dc..e69ae8293 100644
--- a/src/applications/maniphest/mail/ManiphestTaskMailReceiver.php
+++ b/src/applications/maniphest/mail/ManiphestTaskMailReceiver.php
@@ -1,31 +1,31 @@
<?php
final class ManiphestTaskMailReceiver extends PhabricatorObjectMailReceiver {
public function isEnabled() {
- $app_class = 'PhabricatorManiphestApplication';
- return PhabricatorApplication::isClassInstalled($app_class);
+ return PhabricatorApplication::isClassInstalled(
+ 'PhabricatorManiphestApplication');
}
protected function getObjectPattern() {
return 'T[1-9]\d*';
}
protected function loadObject($pattern, PhabricatorUser $viewer) {
$id = (int)trim($pattern, 'T');
$results = id(new ManiphestTaskQuery())
->setViewer($viewer)
->withIDs(array($id))
->needSubscriberPHIDs(true)
->needProjectPHIDs(true)
->execute();
return head($results);
}
protected function getTransactionReplyHandler() {
return new ManiphestReplyHandler();
}
}
diff --git a/src/applications/maniphest/query/ManiphestTaskQuery.php b/src/applications/maniphest/query/ManiphestTaskQuery.php
index 4da078c00..09472be2a 100644
--- a/src/applications/maniphest/query/ManiphestTaskQuery.php
+++ b/src/applications/maniphest/query/ManiphestTaskQuery.php
@@ -1,862 +1,862 @@
<?php
/**
* Query tasks by specific criteria. This class uses the higher-performance
* but less-general Maniphest indexes to satisfy queries.
*/
final class ManiphestTaskQuery extends PhabricatorCursorPagedPolicyAwareQuery {
private $taskIDs = array();
private $taskPHIDs = array();
private $authorPHIDs = array();
private $ownerPHIDs = array();
private $noOwner;
private $anyOwner;
private $subscriberPHIDs = array();
private $dateCreatedAfter;
private $dateCreatedBefore;
private $dateModifiedAfter;
private $dateModifiedBefore;
private $subpriorityMin;
private $subpriorityMax;
private $fullTextSearch = '';
private $status = 'status-any';
const STATUS_ANY = 'status-any';
const STATUS_OPEN = 'status-open';
const STATUS_CLOSED = 'status-closed';
const STATUS_RESOLVED = 'status-resolved';
const STATUS_WONTFIX = 'status-wontfix';
const STATUS_INVALID = 'status-invalid';
const STATUS_SPITE = 'status-spite';
const STATUS_DUPLICATE = 'status-duplicate';
private $statuses;
private $priorities;
private $subpriorities;
private $groupBy = 'group-none';
const GROUP_NONE = 'group-none';
const GROUP_PRIORITY = 'group-priority';
const GROUP_OWNER = 'group-owner';
const GROUP_STATUS = 'group-status';
const GROUP_PROJECT = 'group-project';
private $orderBy = 'order-modified';
const ORDER_PRIORITY = 'order-priority';
const ORDER_CREATED = 'order-created';
const ORDER_MODIFIED = 'order-modified';
const ORDER_TITLE = 'order-title';
private $needSubscriberPHIDs;
private $needProjectPHIDs;
private $blockingTasks;
private $blockedTasks;
public function withAuthors(array $authors) {
$this->authorPHIDs = $authors;
return $this;
}
public function withIDs(array $ids) {
$this->taskIDs = $ids;
return $this;
}
public function withPHIDs(array $phids) {
$this->taskPHIDs = $phids;
return $this;
}
public function withOwners(array $owners) {
$no_owner = PhabricatorPeopleNoOwnerDatasource::FUNCTION_TOKEN;
$any_owner = PhabricatorPeopleAnyOwnerDatasource::FUNCTION_TOKEN;
foreach ($owners as $k => $phid) {
if ($phid === $no_owner || $phid === null) {
$this->noOwner = true;
unset($owners[$k]);
break;
}
if ($phid === $any_owner) {
$this->anyOwner = true;
unset($owners[$k]);
break;
}
}
$this->ownerPHIDs = $owners;
return $this;
}
public function withStatus($status) {
$this->status = $status;
return $this;
}
public function withStatuses(array $statuses) {
$this->statuses = $statuses;
return $this;
}
public function withPriorities(array $priorities) {
$this->priorities = $priorities;
return $this;
}
public function withSubpriorities(array $subpriorities) {
$this->subpriorities = $subpriorities;
return $this;
}
public function withSubpriorityBetween($min, $max) {
$this->subpriorityMin = $min;
$this->subpriorityMax = $max;
return $this;
}
public function withSubscribers(array $subscribers) {
$this->subscriberPHIDs = $subscribers;
return $this;
}
public function withFullTextSearch($fulltext_search) {
$this->fullTextSearch = $fulltext_search;
return $this;
}
public function setGroupBy($group) {
$this->groupBy = $group;
return $this;
}
public function setOrderBy($order) {
$this->orderBy = $order;
return $this;
}
/**
* True returns tasks that are blocking other tasks only.
* False returns tasks that are not blocking other tasks only.
* Null returns tasks regardless of blocking status.
*/
public function withBlockingTasks($mode) {
$this->blockingTasks = $mode;
return $this;
}
public function shouldJoinBlockingTasks() {
return $this->blockingTasks !== null;
}
/**
* True returns tasks that are blocked by other tasks only.
* False returns tasks that are not blocked by other tasks only.
* Null returns tasks regardless of blocked by status.
*/
public function withBlockedTasks($mode) {
$this->blockedTasks = $mode;
return $this;
}
public function shouldJoinBlockedTasks() {
return $this->blockedTasks !== null;
}
public function withDateCreatedBefore($date_created_before) {
$this->dateCreatedBefore = $date_created_before;
return $this;
}
public function withDateCreatedAfter($date_created_after) {
$this->dateCreatedAfter = $date_created_after;
return $this;
}
public function withDateModifiedBefore($date_modified_before) {
$this->dateModifiedBefore = $date_modified_before;
return $this;
}
public function withDateModifiedAfter($date_modified_after) {
$this->dateModifiedAfter = $date_modified_after;
return $this;
}
public function needSubscriberPHIDs($bool) {
$this->needSubscriberPHIDs = $bool;
return $this;
}
public function needProjectPHIDs($bool) {
$this->needProjectPHIDs = $bool;
return $this;
}
protected function newResultObject() {
return new ManiphestTask();
}
protected function willExecute() {
// If we already have an order vector, use it as provided.
// TODO: This is a messy hack to make setOrderVector() stronger than
// setPriority().
$vector = $this->getOrderVector();
$keys = mpull(iterator_to_array($vector), 'getOrderKey');
if (array_values($keys) !== array('id')) {
return;
}
$parts = array();
switch ($this->groupBy) {
case self::GROUP_NONE:
break;
case self::GROUP_PRIORITY:
$parts[] = array('priority');
break;
case self::GROUP_OWNER:
$parts[] = array('owner');
break;
case self::GROUP_STATUS:
$parts[] = array('status');
break;
case self::GROUP_PROJECT:
$parts[] = array('project');
break;
}
if ($this->applicationSearchOrders) {
$columns = array();
foreach ($this->applicationSearchOrders as $order) {
$part = 'custom:'.$order['key'];
if ($order['ascending']) {
$part = '-'.$part;
}
$columns[] = $part;
}
$columns[] = 'id';
$parts[] = $columns;
} else {
switch ($this->orderBy) {
case self::ORDER_PRIORITY:
$parts[] = array('priority', 'subpriority', 'id');
break;
case self::ORDER_CREATED:
$parts[] = array('id');
break;
case self::ORDER_MODIFIED:
$parts[] = array('updated', 'id');
break;
case self::ORDER_TITLE:
$parts[] = array('title', 'id');
break;
}
}
$parts = array_mergev($parts);
// We may have a duplicate column if we are both ordering and grouping
// by priority.
$parts = array_unique($parts);
$this->setOrderVector($parts);
}
protected function loadPage() {
$task_dao = new ManiphestTask();
$conn = $task_dao->establishConnection('r');
$where = array();
$where[] = $this->buildTaskIDsWhereClause($conn);
$where[] = $this->buildTaskPHIDsWhereClause($conn);
$where[] = $this->buildStatusWhereClause($conn);
$where[] = $this->buildStatusesWhereClause($conn);
$where[] = $this->buildDependenciesWhereClause($conn);
$where[] = $this->buildAuthorWhereClause($conn);
$where[] = $this->buildOwnerWhereClause($conn);
$where[] = $this->buildFullTextWhereClause($conn);
if ($this->dateCreatedAfter) {
$where[] = qsprintf(
$conn,
'task.dateCreated >= %d',
$this->dateCreatedAfter);
}
if ($this->dateCreatedBefore) {
$where[] = qsprintf(
$conn,
'task.dateCreated <= %d',
$this->dateCreatedBefore);
}
if ($this->dateModifiedAfter) {
$where[] = qsprintf(
$conn,
'task.dateModified >= %d',
$this->dateModifiedAfter);
}
if ($this->dateModifiedBefore) {
$where[] = qsprintf(
$conn,
'task.dateModified <= %d',
$this->dateModifiedBefore);
}
if ($this->priorities) {
$where[] = qsprintf(
$conn,
'task.priority IN (%Ld)',
$this->priorities);
}
if ($this->subpriorities) {
$where[] = qsprintf(
$conn,
'task.subpriority IN (%Lf)',
$this->subpriorities);
}
if ($this->subpriorityMin) {
$where[] = qsprintf(
$conn,
'task.subpriority >= %f',
$this->subpriorityMin);
}
if ($this->subpriorityMax) {
$where[] = qsprintf(
$conn,
'task.subpriority <= %f',
$this->subpriorityMax);
}
$where[] = $this->buildWhereClauseParts($conn);
$where = $this->formatWhereClause($where);
$group_column = '';
switch ($this->groupBy) {
case self::GROUP_PROJECT:
$group_column = qsprintf(
$conn,
', projectGroupName.indexedObjectPHID projectGroupPHID');
break;
}
$rows = queryfx_all(
$conn,
'%Q %Q FROM %T task %Q %Q %Q %Q %Q %Q',
$this->buildSelectClause($conn),
$group_column,
$task_dao->getTableName(),
$this->buildJoinClause($conn),
$where,
$this->buildGroupClause($conn),
$this->buildHavingClause($conn),
$this->buildOrderClause($conn),
$this->buildLimitClause($conn));
switch ($this->groupBy) {
case self::GROUP_PROJECT:
$data = ipull($rows, null, 'id');
break;
default:
$data = $rows;
break;
}
$tasks = $task_dao->loadAllFromArray($data);
switch ($this->groupBy) {
case self::GROUP_PROJECT:
$results = array();
foreach ($rows as $row) {
$task = clone $tasks[$row['id']];
$task->attachGroupByProjectPHID($row['projectGroupPHID']);
$results[] = $task;
}
$tasks = $results;
break;
}
return $tasks;
}
protected function willFilterPage(array $tasks) {
if ($this->groupBy == self::GROUP_PROJECT) {
// We should only return project groups which the user can actually see.
$project_phids = mpull($tasks, 'getGroupByProjectPHID');
$projects = id(new PhabricatorProjectQuery())
->setViewer($this->getViewer())
->withPHIDs($project_phids)
->execute();
$projects = mpull($projects, null, 'getPHID');
foreach ($tasks as $key => $task) {
if (!$task->getGroupByProjectPHID()) {
// This task is either not in any projects, or only in projects
// which we're ignoring because they're being queried for explicitly.
continue;
}
if (empty($projects[$task->getGroupByProjectPHID()])) {
unset($tasks[$key]);
}
}
}
return $tasks;
}
protected function didFilterPage(array $tasks) {
$phids = mpull($tasks, 'getPHID');
if ($this->needProjectPHIDs) {
$edge_query = id(new PhabricatorEdgeQuery())
->withSourcePHIDs($phids)
->withEdgeTypes(
array(
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST,
));
$edge_query->execute();
foreach ($tasks as $task) {
$project_phids = $edge_query->getDestinationPHIDs(
array($task->getPHID()));
$task->attachProjectPHIDs($project_phids);
}
}
if ($this->needSubscriberPHIDs) {
$subscriber_sets = id(new PhabricatorSubscribersQuery())
->withObjectPHIDs($phids)
->execute();
foreach ($tasks as $task) {
$subscribers = idx($subscriber_sets, $task->getPHID(), array());
$task->attachSubscriberPHIDs($subscribers);
}
}
return $tasks;
}
private function buildTaskIDsWhereClause(AphrontDatabaseConnection $conn) {
if (!$this->taskIDs) {
return null;
}
return qsprintf(
$conn,
'task.id in (%Ld)',
$this->taskIDs);
}
private function buildTaskPHIDsWhereClause(AphrontDatabaseConnection $conn) {
if (!$this->taskPHIDs) {
return null;
}
return qsprintf(
$conn,
'task.phid in (%Ls)',
$this->taskPHIDs);
}
private function buildStatusWhereClause(AphrontDatabaseConnection $conn) {
static $map = array(
self::STATUS_RESOLVED => ManiphestTaskStatus::STATUS_CLOSED_RESOLVED,
self::STATUS_WONTFIX => ManiphestTaskStatus::STATUS_CLOSED_WONTFIX,
self::STATUS_INVALID => ManiphestTaskStatus::STATUS_CLOSED_INVALID,
self::STATUS_SPITE => ManiphestTaskStatus::STATUS_CLOSED_SPITE,
self::STATUS_DUPLICATE => ManiphestTaskStatus::STATUS_CLOSED_DUPLICATE,
);
switch ($this->status) {
case self::STATUS_ANY:
return null;
case self::STATUS_OPEN:
return qsprintf(
$conn,
'task.status IN (%Ls)',
ManiphestTaskStatus::getOpenStatusConstants());
case self::STATUS_CLOSED:
return qsprintf(
$conn,
'task.status IN (%Ls)',
ManiphestTaskStatus::getClosedStatusConstants());
default:
$constant = idx($map, $this->status);
if (!$constant) {
- throw new Exception("Unknown status query '{$this->status}'!");
+ throw new Exception(pht("Unknown status query '%s'!", $this->status));
}
return qsprintf(
$conn,
'task.status = %s',
$constant);
}
}
private function buildStatusesWhereClause(AphrontDatabaseConnection $conn) {
if ($this->statuses) {
return qsprintf(
$conn,
'task.status IN (%Ls)',
$this->statuses);
}
return null;
}
private function buildAuthorWhereClause(AphrontDatabaseConnection $conn) {
if (!$this->authorPHIDs) {
return null;
}
return qsprintf(
$conn,
'task.authorPHID in (%Ls)',
$this->authorPHIDs);
}
private function buildOwnerWhereClause(AphrontDatabaseConnection $conn) {
$subclause = array();
if ($this->noOwner) {
$subclause[] = qsprintf(
$conn,
'task.ownerPHID IS NULL');
}
if ($this->anyOwner) {
$subclause[] = qsprintf(
$conn,
'task.ownerPHID IS NOT NULL');
}
if ($this->ownerPHIDs) {
$subclause[] = qsprintf(
$conn,
'task.ownerPHID IN (%Ls)',
$this->ownerPHIDs);
}
if (!$subclause) {
return '';
}
return '('.implode(') OR (', $subclause).')';
}
private function buildFullTextWhereClause(AphrontDatabaseConnection $conn) {
if (!strlen($this->fullTextSearch)) {
return null;
}
// In doing a fulltext search, we first find all the PHIDs that match the
// fulltext search, and then use that to limit the rest of the search
$fulltext_query = id(new PhabricatorSavedQuery())
->setEngineClassName('PhabricatorSearchApplicationSearchEngine')
->setParameter('query', $this->fullTextSearch);
// NOTE: Setting this to something larger than 2^53 will raise errors in
// ElasticSearch, and billions of results won't fit in memory anyway.
$fulltext_query->setParameter('limit', 100000);
$fulltext_query->setParameter('types',
array(ManiphestTaskPHIDType::TYPECONST));
$engine = PhabricatorSearchEngine::loadEngine();
$fulltext_results = $engine->executeSearch($fulltext_query);
if (empty($fulltext_results)) {
$fulltext_results = array(null);
}
return qsprintf(
$conn,
'task.phid IN (%Ls)',
$fulltext_results);
}
private function buildDependenciesWhereClause(
AphrontDatabaseConnection $conn) {
if (!$this->shouldJoinBlockedTasks() &&
!$this->shouldJoinBlockingTasks()) {
return null;
}
$parts = array();
if ($this->blockingTasks === true) {
$parts[] = qsprintf(
$conn,
'blocking.dst IS NOT NULL AND blockingtask.status IN (%Ls)',
ManiphestTaskStatus::getOpenStatusConstants());
} else if ($this->blockingTasks === false) {
$parts[] = qsprintf(
$conn,
'blocking.dst IS NULL OR blockingtask.status NOT IN (%Ls)',
ManiphestTaskStatus::getOpenStatusConstants());
}
if ($this->blockedTasks === true) {
$parts[] = qsprintf(
$conn,
'blocked.dst IS NOT NULL AND blockedtask.status IN (%Ls)',
ManiphestTaskStatus::getOpenStatusConstants());
} else if ($this->blockedTasks === false) {
$parts[] = qsprintf(
$conn,
'blocked.dst IS NULL OR blockedtask.status NOT IN (%Ls)',
ManiphestTaskStatus::getOpenStatusConstants());
}
return '('.implode(') OR (', $parts).')';
}
protected function buildJoinClauseParts(AphrontDatabaseConnection $conn_r) {
$edge_table = PhabricatorEdgeConfig::TABLE_NAME_EDGE;
$joins = array();
if ($this->shouldJoinBlockingTasks()) {
$joins[] = qsprintf(
$conn_r,
'LEFT JOIN %T blocking ON blocking.src = task.phid '.
'AND blocking.type = %d '.
'LEFT JOIN %T blockingtask ON blocking.dst = blockingtask.phid',
$edge_table,
ManiphestTaskDependedOnByTaskEdgeType::EDGECONST,
id(new ManiphestTask())->getTableName());
}
if ($this->shouldJoinBlockedTasks()) {
$joins[] = qsprintf(
$conn_r,
'LEFT JOIN %T blocked ON blocked.src = task.phid '.
'AND blocked.type = %d '.
'LEFT JOIN %T blockedtask ON blocked.dst = blockedtask.phid',
$edge_table,
ManiphestTaskDependsOnTaskEdgeType::EDGECONST,
id(new ManiphestTask())->getTableName());
}
if ($this->subscriberPHIDs) {
$joins[] = qsprintf(
$conn_r,
'JOIN %T e_ccs ON e_ccs.src = task.phid '.
'AND e_ccs.type = %s '.
'AND e_ccs.dst in (%Ls)',
PhabricatorEdgeConfig::TABLE_NAME_EDGE,
PhabricatorObjectHasSubscriberEdgeType::EDGECONST,
$this->subscriberPHIDs);
}
switch ($this->groupBy) {
case self::GROUP_PROJECT:
$ignore_group_phids = $this->getIgnoreGroupedProjectPHIDs();
if ($ignore_group_phids) {
$joins[] = qsprintf(
$conn_r,
'LEFT JOIN %T projectGroup ON task.phid = projectGroup.src
AND projectGroup.type = %d
AND projectGroup.dst NOT IN (%Ls)',
$edge_table,
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST,
$ignore_group_phids);
} else {
$joins[] = qsprintf(
$conn_r,
'LEFT JOIN %T projectGroup ON task.phid = projectGroup.src
AND projectGroup.type = %d',
$edge_table,
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST);
}
$joins[] = qsprintf(
$conn_r,
'LEFT JOIN %T projectGroupName
ON projectGroup.dst = projectGroupName.indexedObjectPHID',
id(new ManiphestNameIndex())->getTableName());
break;
}
$joins[] = parent::buildJoinClauseParts($conn_r);
return $joins;
}
protected function buildGroupClause(AphrontDatabaseConnection $conn_r) {
$joined_multiple_rows = $this->shouldJoinBlockingTasks() ||
$this->shouldJoinBlockedTasks() ||
($this->shouldGroupQueryResultRows());
$joined_project_name = ($this->groupBy == self::GROUP_PROJECT);
// If we're joining multiple rows, we need to group the results by the
// task IDs.
if ($joined_multiple_rows) {
if ($joined_project_name) {
return 'GROUP BY task.phid, projectGroup.dst';
} else {
return 'GROUP BY task.phid';
}
} else {
return '';
}
}
/**
* Return project PHIDs which we should ignore when grouping tasks by
* project. For example, if a user issues a query like:
*
* Tasks in all projects: Frontend, Bugs
*
* ...then we don't show "Frontend" or "Bugs" groups in the result set, since
* they're meaningless as all results are in both groups.
*
* Similarly, for queries like:
*
* Tasks in any projects: Public Relations
*
* ...we ignore the single project, as every result is in that project. (In
* the case that there are several "any" projects, we do not ignore them.)
*
* @return list<phid> Project PHIDs which should be ignored in query
* construction.
*/
private function getIgnoreGroupedProjectPHIDs() {
// Maybe we should also exclude the "OPERATOR_NOT" PHIDs? It won't
// impact the results, but we might end up with a better query plan.
// Investigate this on real data? This is likely very rare.
$edge_types = array(
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST,
);
$phids = array();
$phids[] = $this->getEdgeLogicValues(
$edge_types,
array(
PhabricatorQueryConstraint::OPERATOR_AND,
));
$any = $this->getEdgeLogicValues(
$edge_types,
array(
PhabricatorQueryConstraint::OPERATOR_OR,
));
if (count($any) == 1) {
$phids[] = $any;
}
return array_mergev($phids);
}
protected function getResultCursor($result) {
$id = $result->getID();
if ($this->groupBy == self::GROUP_PROJECT) {
return rtrim($id.'.'.$result->getGroupByProjectPHID(), '.');
}
return $id;
}
public function getOrderableColumns() {
return parent::getOrderableColumns() + array(
'priority' => array(
'table' => 'task',
'column' => 'priority',
'type' => 'int',
),
'owner' => array(
'table' => 'task',
'column' => 'ownerOrdering',
'null' => 'head',
'reverse' => true,
'type' => 'string',
),
'status' => array(
'table' => 'task',
'column' => 'status',
'type' => 'string',
'reverse' => true,
),
'project' => array(
'table' => 'projectGroupName',
'column' => 'indexedObjectName',
'type' => 'string',
'null' => 'head',
'reverse' => true,
),
'title' => array(
'table' => 'task',
'column' => 'title',
'type' => 'string',
'reverse' => true,
),
'subpriority' => array(
'table' => 'task',
'column' => 'subpriority',
'type' => 'float',
),
'updated' => array(
'table' => 'task',
'column' => 'dateModified',
'type' => 'int',
),
);
}
protected function getPagingValueMap($cursor, array $keys) {
$cursor_parts = explode('.', $cursor, 2);
$task_id = $cursor_parts[0];
$group_id = idx($cursor_parts, 1);
$task = $this->loadCursorObject($task_id);
$map = array(
'id' => $task->getID(),
'priority' => $task->getPriority(),
'subpriority' => $task->getSubpriority(),
'owner' => $task->getOwnerOrdering(),
'status' => $task->getStatus(),
'title' => $task->getTitle(),
'updated' => $task->getDateModified(),
);
foreach ($keys as $key) {
switch ($key) {
case 'project':
$value = null;
if ($group_id) {
$paging_projects = id(new PhabricatorProjectQuery())
->setViewer($this->getViewer())
->withPHIDs(array($group_id))
->execute();
if ($paging_projects) {
$value = head($paging_projects)->getName();
}
}
$map[$key] = $value;
break;
}
}
foreach ($keys as $key) {
if ($this->isCustomFieldOrderKey($key)) {
$map += $this->getPagingValueMapForCustomFields($task);
break;
}
}
return $map;
}
protected function getPrimaryTableAlias() {
return 'task';
}
public function getQueryApplicationClass() {
return 'PhabricatorManiphestApplication';
}
}
diff --git a/src/applications/meta/controller/PhabricatorApplicationEmailCommandsController.php b/src/applications/meta/controller/PhabricatorApplicationEmailCommandsController.php
index c739c3969..75dffdee3 100644
--- a/src/applications/meta/controller/PhabricatorApplicationEmailCommandsController.php
+++ b/src/applications/meta/controller/PhabricatorApplicationEmailCommandsController.php
@@ -1,170 +1,155 @@
<?php
final class PhabricatorApplicationEmailCommandsController
extends PhabricatorApplicationsController {
public function shouldAllowPublic() {
return true;
}
public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
$application = $request->getURIData('application');
$selected = id(new PhabricatorApplicationQuery())
->setViewer($viewer)
->withClasses(array($application))
->executeOne();
if (!$selected) {
return new Aphront404Response();
}
$specs = $selected->getMailCommandObjects();
$type = $request->getURIData('type');
if (empty($specs[$type])) {
return new Aphront404Response();
}
$spec = $specs[$type];
$commands = MetaMTAEmailTransactionCommand::getAllCommandsForObject(
$spec['object']);
$commands = msort($commands, 'getCommand');
$content = array();
$content[] = '= '.pht('Mail Commands Overview');
$content[] = pht(
'After configuring Phabricator to process inbound mail, you can '.
'interact with objects (like tasks and revisions) over email. For '.
'information on configuring Phabricator, see '.
'**[[ %s | Configuring Inbound Email ]]**.'.
"\n\n".
'In most cases, you can reply to email you receive from Phabricator '.
'to leave comments. You can also use **mail commands** to take a '.
- 'greater range of actions (like claming a task or requesting changes '.
+ 'greater range of actions (like claiming a task or requesting changes '.
'to a revision) without needing to log in to the web UI.'.
"\n\n".
'Mail commands are keywords which start with an exclamation point, '.
'like `!claim`. Some commands may take parameters, like '.
- '`!assign alincoln`.'.
- "\n\n".
+ "`!assign alincoln`.\n\n".
'To use mail commands, write one command per line at the beginning '.
'or end of your mail message. For example, you could write this in a '.
'reply to task email to claim the task:'.
- "\n\n".
-
- "```\n".
- "!claim\n".
- "\n".
- "I'll take care of this.\n".
- "```\n".
-
- "\n\n".
+ "\n\n```\n!claim\n\nI'll take care of this.\n```\n\n\n".
"When Phabricator receives your mail, it will process any commands ".
"first, then post the remaining message body as a comment. You can ".
"execute multiple commands at once:".
- "\n\n".
-
- "```\n".
- "!assign alincoln\n".
- "!close\n".
- "\n".
- "I just talked to @alincoln, and he showed me that he fixed this.\n".
- "```\n",
+ "\n\n```\n!assign alincoln\n!close\n\nI just talked to @alincoln, ".
+ "and he showed me that he fixed this.\n```\n",
PhabricatorEnv::getDoclink('Configuring Inbound Email'));
$content[] = '= '.$spec['header'];
$content[] = $spec['summary'];
$content[] = '= '.pht('Quick Reference');
$content[] = pht(
'This table summarizes the available mail commands. For details on a '.
'specific command, see the command section below.');
$table = array();
$table[] = '| '.pht('Command').' | '.pht('Summary').' |';
$table[] = '|---|---|';
foreach ($commands as $command) {
$summary = $command->getCommandSummary();
$table[] = '| '.$command->getCommandSyntax().' | '.$summary;
}
$table = implode("\n", $table);
$content[] = $table;
foreach ($commands as $command) {
$content[] = '== !'.$command->getCommand().' ==';
$content[] = $command->getCommandSummary();
$aliases = $command->getCommandAliases();
if ($aliases) {
foreach ($aliases as $key => $alias) {
$aliases[$key] = '!'.$alias;
}
$aliases = implode(', ', $aliases);
} else {
$aliases = '//None//';
}
$syntax = $command->getCommandSyntax();
$table = array();
$table[] = '| '.pht('Property').' | '.pht('Value');
$table[] = '|---|---|';
$table[] = '| **'.pht('Syntax').'** | '.$syntax;
$table[] = '| **'.pht('Aliases').'** | '.$aliases;
$table[] = '| **'.pht('Class').'** | `'.get_class($command).'`';
$table = implode("\n", $table);
$content[] = $table;
$description = $command->getCommandDescription();
if ($description) {
$content[] = $description;
}
}
$content = implode("\n\n", $content);
$title = $spec['name'];
$crumbs = $this->buildApplicationCrumbs();
$this->addApplicationCrumb($crumbs, $selected);
$crumbs->addTextCrumb($title);
$content_box = PhabricatorMarkupEngine::renderOneObject(
id(new PhabricatorMarkupOneOff())->setContent($content),
'default',
$viewer);
$info_view = null;
if (!PhabricatorEnv::getEnvConfig('metamta.reply-handler-domain')) {
$error = pht(
"Phabricator is not currently configured to accept inbound mail. ".
"You won't be able to interact with objects over email until ".
"inbound mail is set up.");
$info_view = id(new PHUIInfoView())
->setErrors(array($error));
}
$header = id(new PHUIHeaderView())
->setHeader($title);
$document = id(new PHUIDocumentView())
->setHeader($header)
->setFontKit(PHUIDocumentView::FONT_SOURCE_SANS)
->appendChild($info_view)
->appendChild($content_box);
return $this->buildApplicationPage(
array(
$crumbs,
$document,
),
array(
'title' => $title,
));
}
}
diff --git a/src/applications/meta/controller/PhabricatorApplicationUninstallController.php b/src/applications/meta/controller/PhabricatorApplicationUninstallController.php
index 744e9598f..eaedb3a06 100644
--- a/src/applications/meta/controller/PhabricatorApplicationUninstallController.php
+++ b/src/applications/meta/controller/PhabricatorApplicationUninstallController.php
@@ -1,117 +1,123 @@
<?php
final class PhabricatorApplicationUninstallController
extends PhabricatorApplicationsController {
private $application;
private $action;
public function shouldRequireAdmin() {
return true;
}
public function willProcessRequest(array $data) {
$this->application = $data['application'];
$this->action = $data['action'];
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$selected = PhabricatorApplication::getByClass($this->application);
if (!$selected) {
return new Aphront404Response();
}
$view_uri = $this->getApplicationURI('view/'.$this->application);
$prototypes_enabled = PhabricatorEnv::getEnvConfig(
'phabricator.show-prototypes');
$dialog = id(new AphrontDialogView())
- ->setUser($user)
- ->addCancelButton($view_uri);
+ ->setUser($user)
+ ->addCancelButton($view_uri);
if ($selected->isPrototype() && !$prototypes_enabled) {
$dialog
->setTitle(pht('Prototypes Not Enabled'))
->appendChild(
pht(
'To manage prototypes, enable them by setting %s in your '.
'Phabricator configuration.',
phutil_tag('tt', array(), 'phabricator.show-prototypes')));
return id(new AphrontDialogResponse())->setDialog($dialog);
}
if ($request->isDialogFormPost()) {
$this->manageApplication();
return id(new AphrontRedirectResponse())->setURI($view_uri);
}
if ($this->action == 'install') {
if ($selected->canUninstall()) {
- $dialog->setTitle('Confirmation')
- ->appendChild(
- 'Install '.$selected->getName().' application?')
- ->addSubmitButton('Install');
+ $dialog
+ ->setTitle('Confirmation')
+ ->appendChild(
+ pht(
+ 'Install %s application?',
+ $selected->getName()))
+ ->addSubmitButton('Install');
} else {
- $dialog->setTitle('Information')
- ->appendChild('You cannot install an installed application.');
+ $dialog
+ ->setTitle('Information')
+ ->appendChild(pht('You cannot install an installed application.'));
}
} else {
if ($selected->canUninstall()) {
$dialog->setTitle(pht('Really Uninstall Application?'));
if ($selected instanceof PhabricatorHomeApplication) {
$dialog
->appendParagraph(
pht(
'Are you absolutely certain you want to uninstall the Home '.
'application?'))
->appendParagraph(
pht(
'This is very unusual and will leave you without any '.
'content on the Phabricator home page. You should only '.
'do this if you are certain you know what you are doing.'))
->addSubmitButton(pht('Completely Break Phabricator'));
} else {
$dialog
->appendParagraph(
pht(
'Really uninstall the %s application?',
$selected->getName()))
->addSubmitButton(pht('Uninstall'));
}
} else {
- $dialog->setTitle('Information')
- ->appendChild(
- 'This application cannot be uninstalled,
- because it is required for Phabricator to work.');
+ $dialog
+ ->setTitle(pht('Information'))
+ ->appendChild(
+ pht(
+ 'This application cannot be uninstalled, '.
+ 'because it is required for Phabricator to work.'));
}
}
return id(new AphrontDialogResponse())->setDialog($dialog);
}
public function manageApplication() {
$key = 'phabricator.uninstalled-applications';
$config_entry = PhabricatorConfigEntry::loadConfigEntry($key);
$list = $config_entry->getValue();
$uninstalled = PhabricatorEnv::getEnvConfig($key);
if (isset($uninstalled[$this->application])) {
unset($list[$this->application]);
} else {
$list[$this->application] = true;
}
PhabricatorConfigEditor::storeNewValue(
$this->getRequest()->getUser(),
$config_entry,
$list,
PhabricatorContentSource::newFromRequest($this->getRequest()));
}
}
diff --git a/src/applications/metamta/adapter/PhabricatorMailImplementationMailgunAdapter.php b/src/applications/metamta/adapter/PhabricatorMailImplementationMailgunAdapter.php
index c812f1ed4..0890ac300 100644
--- a/src/applications/metamta/adapter/PhabricatorMailImplementationMailgunAdapter.php
+++ b/src/applications/metamta/adapter/PhabricatorMailImplementationMailgunAdapter.php
@@ -1,140 +1,143 @@
<?php
/**
* Mail adapter that uses Mailgun's web API to deliver email.
*/
final class PhabricatorMailImplementationMailgunAdapter
extends PhabricatorMailImplementationAdapter {
private $params = array();
private $attachments = array();
public function setFrom($email, $name = '') {
$this->params['from'] = $email;
$this->params['from-name'] = $name;
return $this;
}
public function addReplyTo($email, $name = '') {
if (empty($this->params['reply-to'])) {
$this->params['reply-to'] = array();
}
$this->params['reply-to'][] = "{$name} <{$email}>";
return $this;
}
public function addTos(array $emails) {
foreach ($emails as $email) {
$this->params['tos'][] = $email;
}
return $this;
}
public function addCCs(array $emails) {
foreach ($emails as $email) {
$this->params['ccs'][] = $email;
}
return $this;
}
public function addAttachment($data, $filename, $mimetype) {
$this->attachments[] = array(
'data' => $data,
'name' => $filename,
'type' => $mimetype,
);
return $this;
}
public function addHeader($header_name, $header_value) {
$this->params['headers'][] = array($header_name, $header_value);
return $this;
}
public function setBody($body) {
$this->params['body'] = $body;
return $this;
}
public function setHTMLBody($html_body) {
$this->params['html-body'] = $html_body;
return $this;
}
public function setSubject($subject) {
$this->params['subject'] = $subject;
return $this;
}
public function supportsMessageIDHeader() {
return true;
}
public function send() {
$key = PhabricatorEnv::getEnvConfig('mailgun.api-key');
$domain = PhabricatorEnv::getEnvConfig('mailgun.domain');
$params = array();
$params['to'] = implode(', ', idx($this->params, 'tos', array()));
$params['subject'] = idx($this->params, 'subject');
$params['text'] = idx($this->params, 'body');
if (idx($this->params, 'html-body')) {
$params['html'] = idx($this->params, 'html-body');
}
$from = idx($this->params, 'from');
if (idx($this->params, 'from-name')) {
$params['from'] = "{$this->params['from-name']} <{$from}>";
} else {
$params['from'] = $from;
}
if (idx($this->params, 'reply-to')) {
$replyto = $this->params['reply-to'];
$params['h:reply-to'] = implode(', ', $replyto);
}
if (idx($this->params, 'ccs')) {
$params['cc'] = implode(', ', $this->params['ccs']);
}
foreach (idx($this->params, 'headers', array()) as $header) {
list($name, $value) = $header;
$params['h:'.$name] = $value;
}
$future = new HTTPSFuture(
"https://api:{$key}@api.mailgun.net/v2/{$domain}/messages",
$params);
$future->setMethod('POST');
foreach ($this->attachments as $attachment) {
$future->attachFileData(
'attachment',
$attachment['data'],
$attachment['name'],
$attachment['type']);
}
list($body) = $future->resolvex();
$response = null;
try {
$response = phutil_json_decode($body);
} catch (PhutilJSONParserException $ex) {
throw new PhutilProxyException(
pht('Failed to JSON decode response.'),
$ex);
}
if (!idx($response, 'id')) {
$message = $response['message'];
- throw new Exception("Request failed with errors: {$message}.");
+ throw new Exception(
+ pht(
+ 'Request failed with errors: %s.',
+ $message));
}
return true;
}
}
diff --git a/src/applications/metamta/adapter/PhabricatorMailImplementationTestAdapter.php b/src/applications/metamta/adapter/PhabricatorMailImplementationTestAdapter.php
index 234438698..0ea2af916 100644
--- a/src/applications/metamta/adapter/PhabricatorMailImplementationTestAdapter.php
+++ b/src/applications/metamta/adapter/PhabricatorMailImplementationTestAdapter.php
@@ -1,110 +1,110 @@
<?php
/**
* Mail adapter that doesn't actually send any email, for writing unit tests
* against.
*/
final class PhabricatorMailImplementationTestAdapter
extends PhabricatorMailImplementationAdapter {
private $guts = array();
private $config;
public function __construct(array $config = array()) {
$this->config = $config;
}
public function setFrom($email, $name = '') {
$this->guts['from'] = $email;
$this->guts['from-name'] = $name;
return $this;
}
public function addReplyTo($email, $name = '') {
if (empty($this->guts['reply-to'])) {
$this->guts['reply-to'] = array();
}
$this->guts['reply-to'][] = array(
'email' => $email,
'name' => $name,
);
return $this;
}
public function addTos(array $emails) {
foreach ($emails as $email) {
$this->guts['tos'][] = $email;
}
return $this;
}
public function addCCs(array $emails) {
foreach ($emails as $email) {
$this->guts['ccs'][] = $email;
}
return $this;
}
public function addAttachment($data, $filename, $mimetype) {
$this->guts['attachments'][] = array(
'data' => $data,
'filename' => $filename,
'mimetype' => $mimetype,
);
return $this;
}
public function addHeader($header_name, $header_value) {
$this->guts['headers'][] = array($header_name, $header_value);
return $this;
}
public function setBody($body) {
$this->guts['body'] = $body;
return $this;
}
public function setHTMLBody($html_body) {
$this->guts['html-body'] = $html_body;
return $this;
}
public function setSubject($subject) {
$this->guts['subject'] = $subject;
return $this;
}
public function supportsMessageIDHeader() {
return idx($this->config, 'supportsMessageIDHeader', true);
}
public function send() {
if (!empty($this->guts['fail-permanently'])) {
throw new PhabricatorMetaMTAPermanentFailureException(
- 'Unit Test (Permanent)');
+ pht('Unit Test (Permanent)'));
}
if (!empty($this->guts['fail-temporarily'])) {
throw new Exception(
- 'Unit Test (Temporary)');
+ pht('Unit Test (Temporary)'));
}
$this->guts['did-send'] = true;
return true;
}
public function getGuts() {
return $this->guts;
}
public function setFailPermanently($fail) {
$this->guts['fail-permanently'] = $fail;
return $this;
}
public function setFailTemporarily($fail) {
$this->guts['fail-temporarily'] = $fail;
return $this;
}
}
diff --git a/src/applications/metamta/contentsource/PhabricatorContentSourceView.php b/src/applications/metamta/contentsource/PhabricatorContentSourceView.php
index e1a7b8fda..f159b2059 100644
--- a/src/applications/metamta/contentsource/PhabricatorContentSourceView.php
+++ b/src/applications/metamta/contentsource/PhabricatorContentSourceView.php
@@ -1,32 +1,32 @@
<?php
final class PhabricatorContentSourceView extends AphrontView {
private $contentSource;
public function setContentSource(PhabricatorContentSource $content_source) {
$this->contentSource = $content_source;
return $this;
}
public function render() {
require_celerity_resource('phabricator-content-source-view-css');
$map = PhabricatorContentSource::getSourceNameMap();
$source = $this->contentSource->getSource();
$type = idx($map, $source, null);
if (!$type) {
return null;
}
return phutil_tag(
'span',
array(
'class' => 'phabricator-content-source-view',
),
- "Via {$type}");
+ pht('Via %s', $type));
}
}
diff --git a/src/applications/metamta/controller/PhabricatorMetaMTASendGridReceiveController.php b/src/applications/metamta/controller/PhabricatorMetaMTASendGridReceiveController.php
index 11a0184ff..646d6ef2a 100644
--- a/src/applications/metamta/controller/PhabricatorMetaMTASendGridReceiveController.php
+++ b/src/applications/metamta/controller/PhabricatorMetaMTASendGridReceiveController.php
@@ -1,62 +1,62 @@
<?php
final class PhabricatorMetaMTASendGridReceiveController
extends PhabricatorMetaMTAController {
public function shouldRequireLogin() {
return false;
}
public function processRequest() {
// No CSRF for SendGrid.
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$request = $this->getRequest();
$user = $request->getUser();
$raw_headers = $request->getStr('headers');
$raw_headers = explode("\n", rtrim($raw_headers));
$raw_dict = array();
foreach (array_filter($raw_headers) as $header) {
list($name, $value) = explode(':', $header, 2);
$raw_dict[$name] = ltrim($value);
}
$headers = array(
'to' => $request->getStr('to'),
'from' => $request->getStr('from'),
'subject' => $request->getStr('subject'),
) + $raw_dict;
$received = new PhabricatorMetaMTAReceivedMail();
$received->setHeaders($headers);
$received->setBodies(array(
'text' => $request->getStr('text'),
'html' => $request->getStr('from'),
));
$file_phids = array();
foreach ($_FILES as $file_raw) {
try {
$file = PhabricatorFile::newFromPHPUpload(
$file_raw,
array(
'viewPolicy' => PhabricatorPolicies::POLICY_NOONE,
));
$file_phids[] = $file->getPHID();
} catch (Exception $ex) {
phlog($ex);
}
}
$received->setAttachments($file_phids);
$received->save();
$received->processReceivedMail();
$response = new AphrontWebpageResponse();
- $response->setContent(pht("Got it! Thanks, SendGrid!\n"));
+ $response->setContent(pht('Got it! Thanks, SendGrid!')."\n");
return $response;
}
}
diff --git a/src/applications/metamta/management/PhabricatorMailManagementListInboundWorkflow.php b/src/applications/metamta/management/PhabricatorMailManagementListInboundWorkflow.php
index ed02b4968..02ed82a6b 100644
--- a/src/applications/metamta/management/PhabricatorMailManagementListInboundWorkflow.php
+++ b/src/applications/metamta/management/PhabricatorMailManagementListInboundWorkflow.php
@@ -1,70 +1,71 @@
<?php
final class PhabricatorMailManagementListInboundWorkflow
extends PhabricatorMailManagementWorkflow {
protected function didConstruct() {
$this
->setName('list-inbound')
- ->setSynopsis('List inbound messages received by Phabricator.')
+ ->setSynopsis(pht('List inbound messages received by Phabricator.'))
->setExamples(
'**list-inbound**')
->setArguments(
array(
array(
'name' => 'limit',
'param' => 'N',
'default' => 100,
- 'help' => 'Show a specific number of messages (default 100).',
+ 'help' => pht(
+ 'Show a specific number of messages (default 100).'),
),
));
}
public function execute(PhutilArgumentParser $args) {
$console = PhutilConsole::getConsole();
$viewer = $this->getViewer();
$mails = id(new PhabricatorMetaMTAReceivedMail())->loadAllWhere(
'1 = 1 ORDER BY id DESC LIMIT %d',
$args->getArg('limit'));
if (!$mails) {
$console->writeErr("%s\n", pht('No received mail.'));
return 0;
}
$phids = array_merge(
mpull($mails, 'getRelatedPHID'),
mpull($mails, 'getAuthorPHID'));
$handles = id(new PhabricatorHandleQuery())
->setViewer($viewer)
->withPHIDs($phids)
->execute();
$table = id(new PhutilConsoleTable())
->setShowHeader(false)
- ->addColumn('id', array('title' => 'ID'))
- ->addColumn('author', array('title' => 'Author'))
- ->addColumn('phid', array('title' => 'Related PHID'))
- ->addColumn('subject', array('title' => 'Subject'));
+ ->addColumn('id', array('title' => pht('ID')))
+ ->addColumn('author', array('title' => pht('Author')))
+ ->addColumn('phid', array('title' => pht('Related PHID')))
+ ->addColumn('subject', array('title' => pht('Subject')));
foreach (array_reverse($mails) as $mail) {
$table->addRow(array(
'id' => $mail->getID(),
'author' => $mail->getAuthorPHID()
? $handles[$mail->getAuthorPHID()]->getName()
: '-',
'phid' => $mail->getRelatedPHID()
? $handles[$mail->getRelatedPHID()]->getName()
: '-',
'subject' => $mail->getSubject()
? $mail->getSubject()
: pht('(No subject.)'),
));
}
$table->draw();
return 0;
}
}
diff --git a/src/applications/metamta/management/PhabricatorMailManagementListOutboundWorkflow.php b/src/applications/metamta/management/PhabricatorMailManagementListOutboundWorkflow.php
index 7357993df..366f5bdec 100644
--- a/src/applications/metamta/management/PhabricatorMailManagementListOutboundWorkflow.php
+++ b/src/applications/metamta/management/PhabricatorMailManagementListOutboundWorkflow.php
@@ -1,56 +1,57 @@
<?php
final class PhabricatorMailManagementListOutboundWorkflow
extends PhabricatorMailManagementWorkflow {
protected function didConstruct() {
$this
->setName('list-outbound')
- ->setSynopsis('List outbound messages sent by Phabricator.')
+ ->setSynopsis(pht('List outbound messages sent by Phabricator.'))
->setExamples(
'**list-outbound**')
->setArguments(
array(
array(
'name' => 'limit',
'param' => 'N',
'default' => 100,
- 'help' => 'Show a specific number of messages (default 100).',
+ 'help' => pht(
+ 'Show a specific number of messages (default 100).'),
),
));
}
public function execute(PhutilArgumentParser $args) {
$console = PhutilConsole::getConsole();
$viewer = $this->getViewer();
$mails = id(new PhabricatorMetaMTAMail())->loadAllWhere(
'1 = 1 ORDER BY id DESC LIMIT %d',
$args->getArg('limit'));
if (!$mails) {
$console->writeErr("%s\n", pht('No sent mail.'));
return 0;
}
$table = id(new PhutilConsoleTable())
->setShowHeader(false)
- ->addColumn('id', array('title' => 'ID'))
- ->addColumn('status', array('title' => 'Status'))
- ->addColumn('subject', array('title' => 'Subject'));
+ ->addColumn('id', array('title' => pht('ID')))
+ ->addColumn('status', array('title' => pht('Status')))
+ ->addColumn('subject', array('title' => pht('Subject')));
foreach (array_reverse($mails) as $mail) {
$status = $mail->getStatus();
$table->addRow(array(
'id' => $mail->getID(),
'status' => PhabricatorMetaMTAMail::getReadableStatus($status),
'subject' => $mail->getSubject(),
));
}
$table->draw();
return 0;
}
}
diff --git a/src/applications/metamta/management/PhabricatorMailManagementReceiveTestWorkflow.php b/src/applications/metamta/management/PhabricatorMailManagementReceiveTestWorkflow.php
index 2ee5fdf88..eb4d05d27 100644
--- a/src/applications/metamta/management/PhabricatorMailManagementReceiveTestWorkflow.php
+++ b/src/applications/metamta/management/PhabricatorMailManagementReceiveTestWorkflow.php
@@ -1,163 +1,166 @@
<?php
final class PhabricatorMailManagementReceiveTestWorkflow
extends PhabricatorMailManagementWorkflow {
protected function didConstruct() {
$this
->setName('receive-test')
->setSynopsis(
pht(
'Simulate receiving mail. This is primarily useful if you are '.
'developing new mail receivers.'))
->setExamples(
'**receive-test** --as alincoln --to D123 < body.txt')
->setArguments(
array(
array(
'name' => 'as',
'param' => 'user',
- 'help' => 'Act as the specified user.',
+ 'help' => pht('Act as the specified user.'),
),
array(
'name' => 'from',
'param' => 'email',
- 'help' => 'Simulate mail delivery "From:" the given user.',
+ 'help' => pht('Simulate mail delivery "From:" the given user.'),
),
array(
'name' => 'to',
'param' => 'object',
- 'help' => 'Simulate mail delivery "To:" the given object.',
+ 'help' => pht('Simulate mail delivery "To:" the given object.'),
),
));
}
public function execute(PhutilArgumentParser $args) {
$console = PhutilConsole::getConsole();
$to = $args->getArg('to');
if (!$to) {
throw new PhutilArgumentUsageException(
- "Use '--to' to specify the receiving object or email address.");
+ pht(
+ "Use '%s' to specify the receiving object or email address.",
+ '--to'));
}
$to_application_email = id(new PhabricatorMetaMTAApplicationEmailQuery())
->setViewer($this->getViewer())
->withAddresses(array($to))
->executeOne();
$as = $args->getArg('as');
if (!$as && $to_application_email) {
$default_phid = $to_application_email->getConfigValue(
PhabricatorMetaMTAApplicationEmail::CONFIG_DEFAULT_AUTHOR);
if ($default_phid) {
$default_user = id(new PhabricatorPeopleQuery())
->setViewer($this->getViewer())
->withPHIDs(array($default_phid))
->executeOne();
if ($default_user) {
$as = $default_user->getUsername();
}
}
}
if (!$as) {
throw new PhutilArgumentUsageException(
pht("Use '--as' to specify the acting user."));
}
$user = id(new PhabricatorPeopleQuery())
->setViewer($this->getViewer())
->withUsernames(array($as))
->executeOne();
if (!$user) {
throw new PhutilArgumentUsageException(
pht("No such user '%s' exists.", $as));
}
$from = $args->getArg('from');
if (!$from) {
$from = $user->loadPrimaryEmail()->getAddress();
}
$console->writeErr("%s\n", pht('Reading message body from stdin...'));
$body = file_get_contents('php://stdin');
$received = new PhabricatorMetaMTAReceivedMail();
$header_content = array(
'Message-ID' => Filesystem::readRandomCharacters(12),
'From' => $from,
);
if (preg_match('/.+@.+/', $to)) {
$header_content['to'] = $to;
} else {
// We allow the user to use an object name instead of a real address
// as a convenience. To build the mail, we build a similar message and
// look for a receiver which will accept it.
$pseudohash = PhabricatorObjectMailReceiver::computeMailHash('x', 'y');
$pseudomail = id(new PhabricatorMetaMTAReceivedMail())
->setHeaders(
array(
'to' => $to.'+1+'.$pseudohash,
));
$receivers = id(new PhutilSymbolLoader())
->setAncestorClass('PhabricatorMailReceiver')
->loadObjects();
$receiver = null;
foreach ($receivers as $possible_receiver) {
if (!$possible_receiver->isEnabled()) {
continue;
}
if (!$possible_receiver->canAcceptMail($pseudomail)) {
continue;
}
$receiver = $possible_receiver;
break;
}
if (!$receiver) {
throw new Exception(
pht("No configured mail receiver can accept mail to '%s'.", $to));
}
if (!($receiver instanceof PhabricatorObjectMailReceiver)) {
$class = get_class($receiver);
throw new Exception(
- "Receiver '%s' accepts mail to '%s', but is not a ".
- "subclass of PhabricatorObjectMailReceiver.",
- $class,
- $to);
+ pht(
+ "Receiver '%s' accepts mail to '%s', but is not a ".
+ "subclass of PhabricatorObjectMailReceiver.",
+ $class,
+ $to));
}
$object = $receiver->loadMailReceiverObject($to, $user);
if (!$object) {
throw new Exception(pht("No such object '%s'!", $to));
}
$hash = PhabricatorObjectMailReceiver::computeMailHash(
$object->getMailKey(),
$user->getPHID());
$header_content['to'] = $to.'+'.$user->getID().'+'.$hash.'@test.com';
}
$received->setHeaders($header_content);
$received->setBodies(
array(
'text' => $body,
));
$received->save();
$received->processReceivedMail();
$console->writeErr(
"%s\n\n phabricator/ $ ./bin/mail show-inbound --id %d\n\n",
pht('Mail received! You can view details by running this command:'),
$received->getID());
}
}
diff --git a/src/applications/metamta/management/PhabricatorMailManagementResendWorkflow.php b/src/applications/metamta/management/PhabricatorMailManagementResendWorkflow.php
index c08731efe..7a0932417 100644
--- a/src/applications/metamta/management/PhabricatorMailManagementResendWorkflow.php
+++ b/src/applications/metamta/management/PhabricatorMailManagementResendWorkflow.php
@@ -1,63 +1,68 @@
<?php
final class PhabricatorMailManagementResendWorkflow
extends PhabricatorMailManagementWorkflow {
protected function didConstruct() {
$this
->setName('resend')
- ->setSynopsis('Send mail again.')
+ ->setSynopsis(pht('Send mail again.'))
->setExamples(
'**resend** --id 1 --id 2')
->setArguments(
array(
array(
'name' => 'id',
'param' => 'id',
- 'help' => 'Send mail with a given ID again.',
+ 'help' => pht('Send mail with a given ID again.'),
'repeat' => true,
),
));
}
public function execute(PhutilArgumentParser $args) {
$console = PhutilConsole::getConsole();
$ids = $args->getArg('id');
if (!$ids) {
throw new PhutilArgumentUsageException(
- "Use the '--id' flag to specify one or more messages to resend.");
+ pht(
+ "Use the '%s' flag to specify one or more messages to resend.",
+ '--id'));
}
$messages = id(new PhabricatorMetaMTAMail())->loadAllWhere(
'id IN (%Ld)',
$ids);
if ($ids) {
$ids = array_fuse($ids);
$missing = array_diff_key($ids, $messages);
if ($missing) {
throw new PhutilArgumentUsageException(
- 'Some specified messages do not exist: '.
- implode(', ', array_keys($missing)));
+ pht(
+ 'Some specified messages do not exist: %s',
+ implode(', ', array_keys($missing))));
}
}
foreach ($messages as $message) {
$message->setStatus(PhabricatorMetaMTAMail::STATUS_QUEUE);
$message->save();
$mailer_task = PhabricatorWorker::scheduleTask(
'PhabricatorMetaMTAWorker',
$message->getID(),
array(
'priority' => PhabricatorWorker::PRIORITY_ALERTS,
));
$console->writeOut(
- "Queued message #%d for resend.\n",
- $message->getID());
+ "%s\n",
+ pht(
+ 'Queued message #%d for resend.',
+ $message->getID()));
}
}
}
diff --git a/src/applications/metamta/management/PhabricatorMailManagementSendTestWorkflow.php b/src/applications/metamta/management/PhabricatorMailManagementSendTestWorkflow.php
index d36682e9f..ca1af3a18 100644
--- a/src/applications/metamta/management/PhabricatorMailManagementSendTestWorkflow.php
+++ b/src/applications/metamta/management/PhabricatorMailManagementSendTestWorkflow.php
@@ -1,163 +1,165 @@
<?php
final class PhabricatorMailManagementSendTestWorkflow
extends PhabricatorMailManagementWorkflow {
protected function didConstruct() {
$this
->setName('send-test')
->setSynopsis(
pht(
'Simulate sending mail. This may be useful to test your mail '.
'configuration, or while developing new mail adapters.'))
- ->setExamples(
- '**send-test** --to alincoln --subject hi < body.txt')
+ ->setExamples('**send-test** --to alincoln --subject hi < body.txt')
->setArguments(
array(
array(
'name' => 'from',
'param' => 'user',
- 'help' => 'Send mail from the specified user.',
+ 'help' => pht('Send mail from the specified user.'),
),
array(
'name' => 'to',
'param' => 'user',
- 'help' => 'Send mail "To:" the specified users.',
+ 'help' => pht('Send mail "To:" the specified users.'),
'repeat' => true,
),
array(
'name' => 'cc',
'param' => 'user',
- 'help' => 'Send mail which "Cc:"s the specified users.',
+ 'help' => pht('Send mail which "Cc:"s the specified users.'),
'repeat' => true,
),
array(
'name' => 'subject',
'param' => 'text',
- 'help' => 'Use the provided subject.',
+ 'help' => pht('Use the provided subject.'),
),
array(
'name' => 'tag',
'param' => 'text',
- 'help' => 'Add the given mail tags.',
+ 'help' => pht('Add the given mail tags.'),
'repeat' => true,
),
array(
'name' => 'attach',
'param' => 'file',
- 'help' => 'Attach a file.',
+ 'help' => pht('Attach a file.'),
'repeat' => true,
),
array(
'name' => 'html',
- 'help' => 'Send as HTML mail.',
+ 'help' => pht('Send as HTML mail.'),
),
array(
'name' => 'bulk',
- 'help' => 'Send with bulk headers.',
+ 'help' => pht('Send with bulk headers.'),
),
));
}
public function execute(PhutilArgumentParser $args) {
$console = PhutilConsole::getConsole();
$viewer = $this->getViewer();
$from = $args->getArg('from');
if ($from) {
$user = id(new PhabricatorPeopleQuery())
->setViewer($viewer)
->withUsernames(array($from))
->executeOne();
if (!$user) {
throw new PhutilArgumentUsageException(
pht("No such user '%s' exists.", $from));
}
$from = $user;
}
$tos = $args->getArg('to');
$ccs = $args->getArg('cc');
if (!$tos && !$ccs) {
throw new PhutilArgumentUsageException(
pht(
- 'Specify one or more users to send mail to with `--to` and '.
- '`--cc`.'));
+ 'Specify one or more users to send mail to with `%s` and `%s`.',
+ '--to',
+ '--cc'));
}
$names = array_merge($tos, $ccs);
$users = id(new PhabricatorPeopleQuery())
->setViewer($viewer)
->withUsernames($names)
->execute();
$users = mpull($users, null, 'getUsername');
foreach ($tos as $key => $username) {
if (empty($users[$username])) {
throw new PhutilArgumentUsageException(
pht("No such user '%s' exists.", $username));
}
$tos[$key] = $users[$username]->getPHID();
}
foreach ($ccs as $key => $username) {
if (empty($users[$username])) {
throw new PhutilArgumentUsageException(
pht("No such user '%s' exists.", $username));
}
$ccs[$key] = $users[$username]->getPHID();
}
$subject = $args->getArg('subject');
if ($subject === null) {
$subject = pht('No Subject');
}
$tags = $args->getArg('tag');
$attach = $args->getArg('attach');
$is_bulk = $args->getArg('bulk');
$console->writeErr("%s\n", pht('Reading message body from stdin...'));
$body = file_get_contents('php://stdin');
$mail = id(new PhabricatorMetaMTAMail())
->addTos($tos)
->addCCs($ccs)
->setSubject($subject)
->setBody($body)
->setIsBulk($is_bulk)
->setMailTags($tags);
if ($args->getArg('html')) {
$mail->setBody(
- pht('(This is a placeholder plaintext email body for a test message '.
- 'sent with --html.)'));
+ pht(
+ '(This is a placeholder plaintext email body for a test message '.
+ 'sent with %s.)',
+ '--html'));
$mail->setHTMLBody($body);
} else {
$mail->setBody($body);
}
if ($from) {
$mail->setFrom($from->getPHID());
}
foreach ($attach as $attachment) {
$data = Filesystem::readFile($attachment);
$name = basename($attachment);
$mime = Filesystem::getMimeType($attachment);
$file = new PhabricatorMetaMTAAttachment($data, $name, $mime);
$mail->addAttachment($file);
}
PhabricatorWorker::setRunAllTasksInProcess(true);
$mail->save();
$console->writeErr(
"%s\n\n phabricator/ $ ./bin/mail show-outbound --id %d\n\n",
pht('Mail sent! You can view details by running this command:'),
$mail->getID());
}
}
diff --git a/src/applications/metamta/management/PhabricatorMailManagementShowInboundWorkflow.php b/src/applications/metamta/management/PhabricatorMailManagementShowInboundWorkflow.php
index 7131187fa..b9c0cd7d6 100644
--- a/src/applications/metamta/management/PhabricatorMailManagementShowInboundWorkflow.php
+++ b/src/applications/metamta/management/PhabricatorMailManagementShowInboundWorkflow.php
@@ -1,106 +1,109 @@
<?php
final class PhabricatorMailManagementShowInboundWorkflow
extends PhabricatorMailManagementWorkflow {
protected function didConstruct() {
$this
->setName('show-inbound')
- ->setSynopsis('Show diagnostic details about inbound mail.')
+ ->setSynopsis(pht('Show diagnostic details about inbound mail.'))
->setExamples(
'**show-inbound** --id 1 --id 2')
->setArguments(
array(
array(
'name' => 'id',
'param' => 'id',
- 'help' => 'Show details about inbound mail with given ID.',
+ 'help' => pht('Show details about inbound mail with given ID.'),
'repeat' => true,
),
));
}
public function execute(PhutilArgumentParser $args) {
$console = PhutilConsole::getConsole();
$ids = $args->getArg('id');
if (!$ids) {
throw new PhutilArgumentUsageException(
- "Use the '--id' flag to specify one or more messages to show.");
+ pht(
+ "Use the '%s' flag to specify one or more messages to show.",
+ '--id'));
}
$messages = id(new PhabricatorMetaMTAReceivedMail())->loadAllWhere(
'id IN (%Ld)',
$ids);
if ($ids) {
$ids = array_fuse($ids);
$missing = array_diff_key($ids, $messages);
if ($missing) {
throw new PhutilArgumentUsageException(
- 'Some specified messages do not exist: '.
- implode(', ', array_keys($missing)));
+ pht(
+ 'Some specified messages do not exist: %s',
+ implode(', ', array_keys($missing))));
}
}
$last_key = last_key($messages);
foreach ($messages as $message_key => $message) {
$info = array();
$info[] = pht('PROPERTIES');
$info[] = pht('ID: %d', $message->getID());
$info[] = pht('Status: %s', $message->getStatus());
$info[] = pht('Related PHID: %s', $message->getRelatedPHID());
$info[] = pht('Author PHID: %s', $message->getAuthorPHID());
$info[] = pht('Message ID Hash: %s', $message->getMessageIDHash());
if ($message->getMessage()) {
$info[] = null;
$info[] = pht('MESSAGE');
$info[] = $message->getMessage();
}
$info[] = null;
$info[] = pht('HEADERS');
foreach ($message->getHeaders() as $key => $value) {
if (is_array($value)) {
$value = implode("\n", $value);
}
$info[] = pht('%s: %s', $key, $value);
}
$bodies = $message->getBodies();
$last_body = last_key($bodies);
$info[] = null;
$info[] = pht('BODIES');
foreach ($bodies as $key => $value) {
$info[] = pht('Body "%s"', $key);
$info[] = $value;
if ($key != $last_body) {
$info[] = null;
}
}
$attachments = $message->getAttachments();
$info[] = null;
$info[] = pht('ATTACHMENTS');
if (!$attachments) {
$info[] = pht('No attachments.');
} else {
foreach ($attachments as $attachment) {
$info[] = pht('File PHID: %s', $attachment);
}
}
$console->writeOut("%s\n", implode("\n", $info));
if ($message_key != $last_key) {
$console->writeOut("\n%s\n\n", str_repeat('-', 80));
}
}
}
}
diff --git a/src/applications/metamta/management/PhabricatorMailManagementShowOutboundWorkflow.php b/src/applications/metamta/management/PhabricatorMailManagementShowOutboundWorkflow.php
index 3b798259b..bc8f4ccba 100644
--- a/src/applications/metamta/management/PhabricatorMailManagementShowOutboundWorkflow.php
+++ b/src/applications/metamta/management/PhabricatorMailManagementShowOutboundWorkflow.php
@@ -1,162 +1,165 @@
<?php
final class PhabricatorMailManagementShowOutboundWorkflow
extends PhabricatorMailManagementWorkflow {
protected function didConstruct() {
$this
->setName('show-outbound')
- ->setSynopsis('Show diagnostic details about outbound mail.')
+ ->setSynopsis(pht('Show diagnostic details about outbound mail.'))
->setExamples(
'**show-outbound** --id 1 --id 2')
->setArguments(
array(
array(
'name' => 'id',
'param' => 'id',
- 'help' => 'Show details about outbound mail with given ID.',
+ 'help' => pht('Show details about outbound mail with given ID.'),
'repeat' => true,
),
array(
'name' => 'dump-html',
'help' => pht(
'Dump the HTML body of the mail. You can redirect it to a '.
'file and then open it in a browser.'),
),
));
}
public function execute(PhutilArgumentParser $args) {
$console = PhutilConsole::getConsole();
$ids = $args->getArg('id');
if (!$ids) {
throw new PhutilArgumentUsageException(
- "Use the '--id' flag to specify one or more messages to show.");
+ pht(
+ "Use the '%s' flag to specify one or more messages to show.",
+ '--id'));
}
$messages = id(new PhabricatorMetaMTAMail())->loadAllWhere(
'id IN (%Ld)',
$ids);
if ($ids) {
$ids = array_fuse($ids);
$missing = array_diff_key($ids, $messages);
if ($missing) {
throw new PhutilArgumentUsageException(
- 'Some specified messages do not exist: '.
- implode(', ', array_keys($missing)));
+ pht(
+ 'Some specified messages do not exist: %s',
+ implode(', ', array_keys($missing))));
}
}
$last_key = last_key($messages);
foreach ($messages as $message_key => $message) {
if ($args->getArg('dump-html')) {
$html_body = $message->getHTMLBody();
if (strlen($html_body)) {
$template =
"<!doctype html><html><body>{$html_body}</body></html>";
$console->writeOut("%s\n", $html_body);
} else {
$console->writeErr(
"%s\n",
pht('(This message has no HTML body.)'));
}
continue;
}
$info = array();
$info[] = pht('PROPERTIES');
$info[] = pht('ID: %d', $message->getID());
$info[] = pht('Status: %s', $message->getStatus());
$info[] = pht('Related PHID: %s', $message->getRelatedPHID());
$info[] = pht('Message: %s', $message->getMessage());
$info[] = null;
$info[] = pht('PARAMETERS');
$parameters = $message->getParameters();
foreach ($parameters as $key => $value) {
if ($key == 'body') {
continue;
}
if ($key == 'html-body') {
continue;
}
if ($key == 'headers') {
continue;
}
if ($key == 'attachments') {
continue;
}
if (!is_scalar($value)) {
$value = json_encode($value);
}
$info[] = pht('%s: %s', $key, $value);
}
$info[] = null;
$info[] = pht('HEADERS');
foreach (idx($parameters, 'headers', array()) as $header) {
list($name, $value) = $header;
$info[] = "{$name}: {$value}";
}
$attachments = idx($parameters, 'attachments');
if ($attachments) {
$info[] = null;
$info[] = pht('ATTACHMENTS');
foreach ($attachments as $attachment) {
$info[] = idx($attachment, 'filename', pht('Unnamed File'));
}
}
$actors = $message->loadAllActors();
$actors = array_select_keys(
$actors,
array_merge($message->getToPHIDs(), $message->getCcPHIDs()));
$info[] = null;
$info[] = pht('RECIPIENTS');
foreach ($actors as $actor) {
if ($actor->isDeliverable()) {
$info[] = ' '.coalesce($actor->getName(), $actor->getPHID());
} else {
$info[] = '! '.coalesce($actor->getName(), $actor->getPHID());
}
foreach ($actor->getDeliverabilityReasons() as $reason) {
$desc = PhabricatorMetaMTAActor::getReasonDescription($reason);
$info[] = ' - '.$desc;
}
}
$info[] = null;
$info[] = pht('TEXT BODY');
if (strlen($message->getBody())) {
$info[] = $message->getBody();
} else {
$info[] = pht('(This message has no text body.)');
}
$info[] = null;
$info[] = pht('HTML BODY');
if (strlen($message->getHTMLBody())) {
$info[] = $message->getHTMLBody();
$info[] = null;
} else {
$info[] = pht('(This message has no HTML body.)');
}
$console->writeOut('%s', implode("\n", $info));
if ($message_key != $last_key) {
$console->writeOut("\n%s\n\n", str_repeat('-', 80));
}
}
}
}
diff --git a/src/applications/metamta/receiver/__tests__/PhabricatorMailReceiverTestCase.php b/src/applications/metamta/receiver/__tests__/PhabricatorMailReceiverTestCase.php
index ff7647bc1..f5b6ab578 100644
--- a/src/applications/metamta/receiver/__tests__/PhabricatorMailReceiverTestCase.php
+++ b/src/applications/metamta/receiver/__tests__/PhabricatorMailReceiverTestCase.php
@@ -1,40 +1,40 @@
<?php
final class PhabricatorMailReceiverTestCase extends PhabricatorTestCase {
public function testAddressSimilarity() {
$env = PhabricatorEnv::beginScopedEnv();
$env->overrideEnvConfig('metamta.single-reply-handler-prefix', 'prefix');
$base = 'alincoln@example.com';
$same = array(
'alincoln@example.com',
'"Abrahamn Lincoln" <alincoln@example.com>',
'ALincoln@example.com',
'prefix+alincoln@example.com',
);
foreach ($same as $address) {
$this->assertTrue(
PhabricatorMailReceiver::matchAddresses($base, $address),
"Address {$address}");
}
$diff = array(
'a.lincoln@example.com',
'aluncoln@example.com',
'prefixalincoln@example.com',
'badprefix+alincoln@example.com',
'bad+prefix+alincoln@example.com',
'prefix+alincoln+sufffix@example.com',
);
foreach ($diff as $address) {
$this->assertFalse(
PhabricatorMailReceiver::matchAddresses($base, $address),
- "Address: {$address}");
+ pht('Address: %s', $address));
}
}
}
diff --git a/src/applications/metamta/storage/PhabricatorMetaMTAMail.php b/src/applications/metamta/storage/PhabricatorMetaMTAMail.php
index 05a3f6b56..07129d9e0 100644
--- a/src/applications/metamta/storage/PhabricatorMetaMTAMail.php
+++ b/src/applications/metamta/storage/PhabricatorMetaMTAMail.php
@@ -1,995 +1,997 @@
<?php
/**
* @task recipients Managing Recipients
*/
final class PhabricatorMetaMTAMail extends PhabricatorMetaMTADAO {
const STATUS_QUEUE = 'queued';
const STATUS_SENT = 'sent';
const STATUS_FAIL = 'fail';
const STATUS_VOID = 'void';
const RETRY_DELAY = 5;
protected $parameters;
protected $status;
protected $message;
protected $relatedPHID;
private $recipientExpansionMap;
public function __construct() {
$this->status = self::STATUS_QUEUE;
$this->parameters = array();
parent::__construct();
}
protected function getConfiguration() {
return array(
self::CONFIG_SERIALIZATION => array(
'parameters' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'status' => 'text32',
'relatedPHID' => 'phid?',
// T6203/NULLABILITY
// This should just be empty if there's no body.
'message' => 'text?',
),
self::CONFIG_KEY_SCHEMA => array(
'status' => array(
'columns' => array('status'),
),
'relatedPHID' => array(
'columns' => array('relatedPHID'),
),
'key_created' => array(
'columns' => array('dateCreated'),
),
),
) + parent::getConfiguration();
}
protected function setParam($param, $value) {
$this->parameters[$param] = $value;
return $this;
}
protected function getParam($param, $default = null) {
return idx($this->parameters, $param, $default);
}
/**
* Set tags (@{class:MetaMTANotificationType} constants) which identify the
* content of this mail in a general way. These tags are used to allow users
* to opt out of receiving certain types of mail, like updates when a task's
* projects change.
*
* @param list<const> List of @{class:MetaMTANotificationType} constants.
* @return this
*/
public function setMailTags(array $tags) {
$this->setParam('mailtags', array_unique($tags));
return $this;
}
public function getMailTags() {
return $this->getParam('mailtags', array());
}
/**
* In Gmail, conversations will be broken if you reply to a thread and the
* server sends back a response without referencing your Message-ID, even if
* it references a Message-ID earlier in the thread. To avoid this, use the
* parent email's message ID explicitly if it's available. This overwrites the
* "In-Reply-To" and "References" headers we would otherwise generate. This
* needs to be set whenever an action is triggered by an email message. See
* T251 for more details.
*
* @param string The "Message-ID" of the email which precedes this one.
* @return this
*/
public function setParentMessageID($id) {
$this->setParam('parent-message-id', $id);
return $this;
}
public function getParentMessageID() {
return $this->getParam('parent-message-id');
}
public function getSubject() {
return $this->getParam('subject');
}
public function addTos(array $phids) {
$phids = array_unique($phids);
$this->setParam('to', $phids);
return $this;
}
public function addRawTos(array $raw_email) {
// Strip addresses down to bare emails, since the MailAdapter API currently
// requires we pass it just the address (like `alincoln@logcabin.org`), not
// a full string like `"Abraham Lincoln" <alincoln@logcabin.org>`.
foreach ($raw_email as $key => $email) {
$object = new PhutilEmailAddress($email);
$raw_email[$key] = $object->getAddress();
}
$this->setParam('raw-to', $raw_email);
return $this;
}
public function addCCs(array $phids) {
$phids = array_unique($phids);
$this->setParam('cc', $phids);
return $this;
}
public function setExcludeMailRecipientPHIDs(array $exclude) {
$this->setParam('exclude', $exclude);
return $this;
}
private function getExcludeMailRecipientPHIDs() {
return $this->getParam('exclude', array());
}
public function setForceHeraldMailRecipientPHIDs(array $force) {
$this->setParam('herald-force-recipients', $force);
return $this;
}
private function getForceHeraldMailRecipientPHIDs() {
return $this->getParam('herald-force-recipients', array());
}
public function getTranslation(array $objects) {
$default_translation = PhabricatorEnv::getEnvConfig('translation.provider');
$return = null;
$recipients = array_merge(
idx($this->parameters, 'to', array()),
idx($this->parameters, 'cc', array()));
foreach (array_select_keys($objects, $recipients) as $object) {
$translation = null;
if ($object instanceof PhabricatorUser) {
$translation = $object->getTranslation();
}
if (!$translation) {
$translation = $default_translation;
}
if ($return && $translation != $return) {
return $default_translation;
}
$return = $translation;
}
if (!$return) {
$return = $default_translation;
}
return $return;
}
public function addPHIDHeaders($name, array $phids) {
foreach ($phids as $phid) {
$this->addHeader($name, '<'.$phid.'>');
}
return $this;
}
public function addHeader($name, $value) {
$this->parameters['headers'][] = array($name, $value);
return $this;
}
public function addAttachment(PhabricatorMetaMTAAttachment $attachment) {
$this->parameters['attachments'][] = $attachment->toDictionary();
return $this;
}
public function getAttachments() {
$dicts = $this->getParam('attachments');
$result = array();
foreach ($dicts as $dict) {
$result[] = PhabricatorMetaMTAAttachment::newFromDictionary($dict);
}
return $result;
}
public function setAttachments(array $attachments) {
assert_instances_of($attachments, 'PhabricatorMetaMTAAttachment');
$this->setParam('attachments', mpull($attachments, 'toDictionary'));
return $this;
}
public function setFrom($from) {
$this->setParam('from', $from);
return $this;
}
public function setRawFrom($raw_email, $raw_name) {
$this->setParam('raw-from', array($raw_email, $raw_name));
return $this;
}
public function setReplyTo($reply_to) {
$this->setParam('reply-to', $reply_to);
return $this;
}
public function setSubject($subject) {
$this->setParam('subject', $subject);
return $this;
}
public function setSubjectPrefix($prefix) {
$this->setParam('subject-prefix', $prefix);
return $this;
}
public function setVarySubjectPrefix($prefix) {
$this->setParam('vary-subject-prefix', $prefix);
return $this;
}
public function setBody($body) {
$this->setParam('body', $body);
return $this;
}
public function setHTMLBody($html) {
$this->setParam('html-body', $html);
return $this;
}
public function getBody() {
return $this->getParam('body');
}
public function getHTMLBody() {
return $this->getParam('html-body');
}
public function setIsErrorEmail($is_error) {
$this->setParam('is-error', $is_error);
return $this;
}
public function getIsErrorEmail() {
return $this->getParam('is-error', false);
}
public function getToPHIDs() {
return $this->getParam('to', array());
}
public function getRawToAddresses() {
return $this->getParam('raw-to', array());
}
public function getCcPHIDs() {
return $this->getParam('cc', array());
}
/**
* Force delivery of a message, even if recipients have preferences which
* would otherwise drop the message.
*
* This is primarily intended to let users who don't want any email still
* receive things like password resets.
*
* @param bool True to force delivery despite user preferences.
* @return this
*/
public function setForceDelivery($force) {
$this->setParam('force', $force);
return $this;
}
public function getForceDelivery() {
return $this->getParam('force', false);
}
/**
* Flag that this is an auto-generated bulk message and should have bulk
* headers added to it if appropriate. Broadly, this means some flavor of
* "Precedence: bulk" or similar, but is implementation and configuration
* dependent.
*
* @param bool True if the mail is automated bulk mail.
* @return this
*/
public function setIsBulk($is_bulk) {
$this->setParam('is-bulk', $is_bulk);
return $this;
}
/**
* Use this method to set an ID used for message threading. MetaMTA will
* set appropriate headers (Message-ID, In-Reply-To, References and
* Thread-Index) based on the capabilities of the underlying mailer.
*
* @param string Unique identifier, appropriate for use in a Message-ID,
* In-Reply-To or References headers.
* @param bool If true, indicates this is the first message in the thread.
* @return this
*/
public function setThreadID($thread_id, $is_first_message = false) {
$this->setParam('thread-id', $thread_id);
$this->setParam('is-first-message', $is_first_message);
return $this;
}
/**
* Save a newly created mail to the database. The mail will eventually be
* delivered by the MetaMTA daemon.
*
* @return this
*/
public function saveAndSend() {
return $this->save();
}
public function save() {
if ($this->getID()) {
return parent::save();
}
// NOTE: When mail is sent from CLI scripts that run tasks in-process, we
// may re-enter this method from within scheduleTask(). The implementation
// is intended to avoid anything awkward if we end up reentering this
// method.
$this->openTransaction();
// Save to generate a task ID.
$result = parent::save();
// Queue a task to send this mail.
$mailer_task = PhabricatorWorker::scheduleTask(
'PhabricatorMetaMTAWorker',
$this->getID(),
array(
'priority' => PhabricatorWorker::PRIORITY_ALERTS,
));
$this->saveTransaction();
return $result;
}
public function buildDefaultMailer() {
return PhabricatorEnv::newObjectFromConfig('metamta.mail-adapter');
}
/**
* Attempt to deliver an email immediately, in this process.
*
* @param bool Try to deliver this email even if it has already been
* delivered or is in backoff after a failed delivery attempt.
* @param PhabricatorMailImplementationAdapter Use a specific mail adapter,
* instead of the default.
*
* @return void
*/
public function sendNow(
$force_send = false,
PhabricatorMailImplementationAdapter $mailer = null) {
if ($mailer === null) {
$mailer = $this->buildDefaultMailer();
}
if (!$force_send) {
if ($this->getStatus() != self::STATUS_QUEUE) {
- throw new Exception('Trying to send an already-sent mail!');
+ throw new Exception(pht('Trying to send an already-sent mail!'));
}
}
try {
$params = $this->parameters;
$actors = $this->loadAllActors();
$deliverable_actors = $this->filterDeliverableActors($actors);
$default_from = PhabricatorEnv::getEnvConfig('metamta.default-address');
if (empty($params['from'])) {
$mailer->setFrom($default_from);
}
$is_first = idx($params, 'is-first-message');
unset($params['is-first-message']);
$is_threaded = (bool)idx($params, 'thread-id');
$reply_to_name = idx($params, 'reply-to-name', '');
unset($params['reply-to-name']);
$add_cc = array();
$add_to = array();
// Only try to use preferences if everything is multiplexed, so we
// get consistent behavior.
$use_prefs = self::shouldMultiplexAllMail();
$prefs = null;
if ($use_prefs) {
// If multiplexing is enabled, some recipients will be in "Cc"
// rather than "To". We'll move them to "To" later (or supply a
// dummy "To") but need to look for the recipient in either the
// "To" or "Cc" fields here.
$target_phid = head(idx($params, 'to', array()));
if (!$target_phid) {
$target_phid = head(idx($params, 'cc', array()));
}
if ($target_phid) {
$user = id(new PhabricatorUser())->loadOneWhere(
'phid = %s',
$target_phid);
if ($user) {
$prefs = $user->loadPreferences();
}
}
}
foreach ($params as $key => $value) {
switch ($key) {
case 'raw-from':
list($from_email, $from_name) = $value;
$mailer->setFrom($from_email, $from_name);
break;
case 'from':
$from = $value;
$actor_email = null;
$actor_name = null;
$actor = idx($actors, $from);
if ($actor) {
$actor_email = $actor->getEmailAddress();
$actor_name = $actor->getName();
}
$can_send_as_user = $actor_email &&
PhabricatorEnv::getEnvConfig('metamta.can-send-as-user');
if ($can_send_as_user) {
$mailer->setFrom($actor_email, $actor_name);
} else {
$from_email = coalesce($actor_email, $default_from);
$from_name = coalesce($actor_name, pht('Phabricator'));
if (empty($params['reply-to'])) {
$params['reply-to'] = $from_email;
$params['reply-to-name'] = $from_name;
}
$mailer->setFrom($default_from, $from_name);
}
break;
case 'reply-to':
$mailer->addReplyTo($value, $reply_to_name);
break;
case 'to':
$to_phids = $this->expandRecipients($value);
$to_actors = array_select_keys($deliverable_actors, $to_phids);
$add_to = array_merge(
$add_to,
mpull($to_actors, 'getEmailAddress'));
break;
case 'raw-to':
$add_to = array_merge($add_to, $value);
break;
case 'cc':
$cc_phids = $this->expandRecipients($value);
$cc_actors = array_select_keys($deliverable_actors, $cc_phids);
$add_cc = array_merge(
$add_cc,
mpull($cc_actors, 'getEmailAddress'));
break;
case 'headers':
foreach ($value as $pair) {
list($header_key, $header_value) = $pair;
// NOTE: If we have \n in a header, SES rejects the email.
$header_value = str_replace("\n", ' ', $header_value);
$mailer->addHeader($header_key, $header_value);
}
break;
case 'attachments':
$value = $this->getAttachments();
foreach ($value as $attachment) {
$mailer->addAttachment(
$attachment->getData(),
$attachment->getFilename(),
$attachment->getMimeType());
}
break;
case 'subject':
$subject = array();
if ($is_threaded) {
$add_re = PhabricatorEnv::getEnvConfig('metamta.re-prefix');
if ($prefs) {
$add_re = $prefs->getPreference(
PhabricatorUserPreferences::PREFERENCE_RE_PREFIX,
$add_re);
}
if ($add_re) {
$subject[] = 'Re:';
}
}
$subject[] = trim(idx($params, 'subject-prefix'));
$vary_prefix = idx($params, 'vary-subject-prefix');
if ($vary_prefix != '') {
$use_subject = PhabricatorEnv::getEnvConfig(
'metamta.vary-subjects');
if ($prefs) {
$use_subject = $prefs->getPreference(
PhabricatorUserPreferences::PREFERENCE_VARY_SUBJECT,
$use_subject);
}
if ($use_subject) {
$subject[] = $vary_prefix;
}
}
$subject[] = $value;
$mailer->setSubject(implode(' ', array_filter($subject)));
break;
case 'is-bulk':
if ($value) {
$mailer->addHeader('Precedence', 'bulk');
}
break;
case 'thread-id':
// NOTE: Gmail freaks out about In-Reply-To and References which
// aren't in the form "<string@domain.tld>"; this is also required
// by RFC 2822, although some clients are more liberal in what they
// accept.
$domain = PhabricatorEnv::getEnvConfig('metamta.domain');
$value = '<'.$value.'@'.$domain.'>';
if ($is_first && $mailer->supportsMessageIDHeader()) {
$mailer->addHeader('Message-ID', $value);
} else {
$in_reply_to = $value;
$references = array($value);
$parent_id = $this->getParentMessageID();
if ($parent_id) {
$in_reply_to = $parent_id;
// By RFC 2822, the most immediate parent should appear last
// in the "References" header, so this order is intentional.
$references[] = $parent_id;
}
$references = implode(' ', $references);
$mailer->addHeader('In-Reply-To', $in_reply_to);
$mailer->addHeader('References', $references);
}
$thread_index = $this->generateThreadIndex($value, $is_first);
$mailer->addHeader('Thread-Index', $thread_index);
break;
case 'mailtags':
// Handled below.
break;
case 'subject-prefix':
case 'vary-subject-prefix':
// Handled above.
break;
default:
// Just discard.
}
}
$body = idx($params, 'body', '');
$max = PhabricatorEnv::getEnvConfig('metamta.email-body-limit');
if (strlen($body) > $max) {
$body = id(new PhutilUTF8StringTruncator())
->setMaximumBytes($max)
->truncateString($body);
$body .= "\n";
$body .= pht('(This email was truncated at %d bytes.)', $max);
}
$mailer->setBody($body);
$html_emails = false;
if ($use_prefs && $prefs) {
$html_emails = $prefs->getPreference(
PhabricatorUserPreferences::PREFERENCE_HTML_EMAILS,
$html_emails);
}
if ($html_emails && isset($params['html-body'])) {
$mailer->setHTMLBody($params['html-body']);
}
if (!$add_to && !$add_cc) {
$this->setStatus(self::STATUS_VOID);
$this->setMessage(
- 'Message has no valid recipients: all To/Cc are disabled, invalid, '.
- 'or configured not to receive this mail.');
+ pht(
+ 'Message has no valid recipients: all To/Cc are disabled, '.
+ 'invalid, or configured not to receive this mail.'));
return $this->save();
}
if ($this->getIsErrorEmail()) {
$all_recipients = array_merge($add_to, $add_cc);
if ($this->shouldRateLimitMail($all_recipients)) {
$this->setStatus(self::STATUS_VOID);
$this->setMessage(
pht(
'This is an error email, but one or more recipients have '.
'exceeded the error email rate limit. Declining to deliver '.
'message.'));
return $this->save();
}
}
if (PhabricatorEnv::getEnvConfig('phabricator.silent')) {
$this->setStatus(self::STATUS_VOID);
$this->setMessage(
pht(
- 'Phabricator is running in silent mode. See `phabricator.silent` '.
- 'in the configuration to change this setting.'));
+ 'Phabricator is running in silent mode. See `%s` '.
+ 'in the configuration to change this setting.',
+ 'phabricator.silent'));
return $this->save();
}
$mailer->addHeader('X-Phabricator-Sent-This-Message', 'Yes');
$mailer->addHeader('X-Mail-Transport-Agent', 'MetaMTA');
// Some clients respect this to suppress OOF and other auto-responses.
$mailer->addHeader('X-Auto-Response-Suppress', 'All');
// If the message has mailtags, filter out any recipients who don't want
// to receive this type of mail.
$mailtags = $this->getParam('mailtags');
if ($mailtags) {
$tag_header = array();
foreach ($mailtags as $mailtag) {
$tag_header[] = '<'.$mailtag.'>';
}
$tag_header = implode(', ', $tag_header);
$mailer->addHeader('X-Phabricator-Mail-Tags', $tag_header);
}
// Some mailers require a valid "To:" in order to deliver mail. If we
// don't have any "To:", try to fill it in with a placeholder "To:".
// If that also fails, move the "Cc:" line to "To:".
if (!$add_to) {
$placeholder_key = 'metamta.placeholder-to-recipient';
$placeholder = PhabricatorEnv::getEnvConfig($placeholder_key);
if ($placeholder !== null) {
$add_to = array($placeholder);
} else {
$add_to = $add_cc;
$add_cc = array();
}
}
$add_to = array_unique($add_to);
$add_cc = array_diff(array_unique($add_cc), $add_to);
$mailer->addTos($add_to);
if ($add_cc) {
$mailer->addCCs($add_cc);
}
} catch (Exception $ex) {
$this
->setStatus(self::STATUS_FAIL)
->setMessage($ex->getMessage())
->save();
throw $ex;
}
try {
$ok = $mailer->send();
if (!$ok) {
// TODO: At some point, we should clean this up and make all mailers
// throw.
throw new Exception(
pht('Mail adapter encountered an unexpected, unspecified failure.'));
}
$this->setStatus(self::STATUS_SENT);
$this->save();
return $this;
} catch (PhabricatorMetaMTAPermanentFailureException $ex) {
$this
->setStatus(self::STATUS_FAIL)
->setMessage($ex->getMessage())
->save();
throw $ex;
} catch (Exception $ex) {
$this
->setMessage($ex->getMessage()."\n".$ex->getTraceAsString())
->save();
throw $ex;
}
}
public static function getReadableStatus($status_code) {
- static $readable = array(
- self::STATUS_QUEUE => 'Queued for Delivery',
- self::STATUS_FAIL => 'Delivery Failed',
- self::STATUS_SENT => 'Sent',
- self::STATUS_VOID => 'Void',
+ $readable = array(
+ self::STATUS_QUEUE => pht('Queued for Delivery'),
+ self::STATUS_FAIL => pht('Delivery Failed'),
+ self::STATUS_SENT => pht('Sent'),
+ self::STATUS_VOID => pht('Void'),
);
$status_code = coalesce($status_code, '?');
return idx($readable, $status_code, $status_code);
}
private function generateThreadIndex($seed, $is_first_mail) {
// When threading, Outlook ignores the 'References' and 'In-Reply-To'
// headers that most clients use. Instead, it uses a custom 'Thread-Index'
// header. The format of this header is something like this (from
// camel-exchange-folder.c in Evolution Exchange):
/* A new post to a folder gets a 27-byte-long thread index. (The value
* is apparently unique but meaningless.) Each reply to a post gets a
* 32-byte-long thread index whose first 27 bytes are the same as the
* parent's thread index. Each reply to any of those gets a
* 37-byte-long thread index, etc. The Thread-Index header contains a
* base64 representation of this value.
*/
// The specific implementation uses a 27-byte header for the first email
// a recipient receives, and a random 5-byte suffix (32 bytes total)
// thereafter. This means that all the replies are (incorrectly) siblings,
// but it would be very difficult to keep track of the entire tree and this
// gets us reasonable client behavior.
$base = substr(md5($seed), 0, 27);
if (!$is_first_mail) {
// Not totally sure, but it seems like outlook orders replies by
// thread-index rather than timestamp, so to get these to show up in the
// right order we use the time as the last 4 bytes.
$base .= ' '.pack('N', time());
}
return base64_encode($base);
}
public static function shouldMultiplexAllMail() {
return PhabricatorEnv::getEnvConfig('metamta.one-mail-per-recipient');
}
/* -( Managing Recipients )------------------------------------------------ */
/**
* Get all of the recipients for this mail, after preference filters are
* applied. This list has all objects to whom delivery will be attempted.
*
* Note that this expands recipients into their members, because delivery
* is never directly attempted to aggregate actors like projects.
*
* @return list<phid> A list of all recipients to whom delivery will be
* attempted.
* @task recipients
*/
public function buildRecipientList() {
$actors = $this->loadAllActors();
$actors = $this->filterDeliverableActors($actors);
return mpull($actors, 'getPHID');
}
public function loadAllActors() {
$actor_phids = $this->getAllActorPHIDs();
$actor_phids = $this->expandRecipients($actor_phids);
return $this->loadActors($actor_phids);
}
private function getAllActorPHIDs() {
return array_merge(
array($this->getParam('from')),
$this->getToPHIDs(),
$this->getCcPHIDs());
}
/**
* Expand a list of recipient PHIDs (possibly including aggregate recipients
* like projects) into a deaggregated list of individual recipient PHIDs.
* For example, this will expand project PHIDs into a list of the project's
* members.
*
* @param list<phid> List of recipient PHIDs, possibly including aggregate
* recipients.
* @return list<phid> Deaggregated list of mailable recipients.
*/
private function expandRecipients(array $phids) {
if ($this->recipientExpansionMap === null) {
$all_phids = $this->getAllActorPHIDs();
$this->recipientExpansionMap = id(new PhabricatorMetaMTAMemberQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withPHIDs($all_phids)
->execute();
}
$results = array();
foreach ($phids as $phid) {
foreach ($this->recipientExpansionMap[$phid] as $recipient_phid) {
$results[$recipient_phid] = $recipient_phid;
}
}
return array_keys($results);
}
private function filterDeliverableActors(array $actors) {
assert_instances_of($actors, 'PhabricatorMetaMTAActor');
$deliverable_actors = array();
foreach ($actors as $phid => $actor) {
if ($actor->isDeliverable()) {
$deliverable_actors[$phid] = $actor;
}
}
return $deliverable_actors;
}
private function loadActors(array $actor_phids) {
$actor_phids = array_filter($actor_phids);
$viewer = PhabricatorUser::getOmnipotentUser();
$actors = id(new PhabricatorMetaMTAActorQuery())
->setViewer($viewer)
->withPHIDs($actor_phids)
->execute();
if (!$actors) {
return array();
}
if ($this->getForceDelivery()) {
// If we're forcing delivery, skip all the opt-out checks. We don't
// bother annotating reasoning on the mail in this case because it should
// always be obvious why the mail hit this rule (e.g., it is a password
// reset mail).
foreach ($actors as $actor) {
$actor->setDeliverable(PhabricatorMetaMTAActor::REASON_FORCE);
}
return $actors;
}
// Exclude explicit recipients.
foreach ($this->getExcludeMailRecipientPHIDs() as $phid) {
$actor = idx($actors, $phid);
if (!$actor) {
continue;
}
$actor->setUndeliverable(PhabricatorMetaMTAActor::REASON_RESPONSE);
}
// Before running more rules, save a list of the actors who were
// deliverable before we started running preference-based rules. This stops
// us from trying to send mail to disabled users just because a Herald rule
// added them, for example.
$deliverable = array();
foreach ($actors as $phid => $actor) {
if ($actor->isDeliverable()) {
$deliverable[] = $phid;
}
}
// For the rest of the rules, order matters. We're going to run all the
// possible rules in order from weakest to strongest, and let the strongest
// matching rule win. The weaker rules leave annotations behind which help
// users understand why the mail was routed the way it was.
// Exclude the actor if their preferences are set.
$from_phid = $this->getParam('from');
$from_actor = idx($actors, $from_phid);
if ($from_actor) {
$from_user = id(new PhabricatorPeopleQuery())
->setViewer($viewer)
->withPHIDs(array($from_phid))
->execute();
$from_user = head($from_user);
if ($from_user) {
$pref_key = PhabricatorUserPreferences::PREFERENCE_NO_SELF_MAIL;
$exclude_self = $from_user
->loadPreferences()
->getPreference($pref_key);
if ($exclude_self) {
$from_actor->setUndeliverable(PhabricatorMetaMTAActor::REASON_SELF);
}
}
}
$all_prefs = id(new PhabricatorUserPreferences())->loadAllWhere(
'userPHID in (%Ls)',
$actor_phids);
$all_prefs = mpull($all_prefs, null, 'getUserPHID');
$value_email = PhabricatorUserPreferences::MAILTAG_PREFERENCE_EMAIL;
// Exclude all recipients who have set preferences to not receive this type
// of email (for example, a user who says they don't want emails about task
// CC changes).
$tags = $this->getParam('mailtags');
if ($tags) {
foreach ($all_prefs as $phid => $prefs) {
$user_mailtags = $prefs->getPreference(
PhabricatorUserPreferences::PREFERENCE_MAILTAGS,
array());
// The user must have elected to receive mail for at least one
// of the mailtags.
$send = false;
foreach ($tags as $tag) {
if (((int)idx($user_mailtags, $tag, $value_email)) == $value_email) {
$send = true;
break;
}
}
if (!$send) {
$actors[$phid]->setUndeliverable(
PhabricatorMetaMTAActor::REASON_MAILTAGS);
}
}
}
// If recipients were initially deliverable and were added by "Send me an
// email" Herald rules, annotate them as such and make them deliverable
// again, overriding any changes made by the "self mail" and "mail tags"
// settings.
$force_recipients = $this->getForceHeraldMailRecipientPHIDs();
$force_recipients = array_fuse($force_recipients);
if ($force_recipients) {
foreach ($deliverable as $phid) {
if (isset($force_recipients[$phid])) {
$actors[$phid]->setDeliverable(
PhabricatorMetaMTAActor::REASON_FORCE_HERALD);
}
}
}
// Exclude recipients who don't want any mail. This rule is very strong
// and runs last.
foreach ($all_prefs as $phid => $prefs) {
$exclude = $prefs->getPreference(
PhabricatorUserPreferences::PREFERENCE_NO_MAIL,
false);
if ($exclude) {
$actors[$phid]->setUndeliverable(
PhabricatorMetaMTAActor::REASON_MAIL_DISABLED);
}
}
return $actors;
}
private function shouldRateLimitMail(array $all_recipients) {
try {
PhabricatorSystemActionEngine::willTakeAction(
$all_recipients,
new PhabricatorMetaMTAErrorMailAction(),
1);
return false;
} catch (PhabricatorSystemActionRateLimitException $ex) {
return true;
}
}
}
diff --git a/src/applications/metamta/storage/PhabricatorMetaMTAReceivedMail.php b/src/applications/metamta/storage/PhabricatorMetaMTAReceivedMail.php
index 91634fe57..0f17f08d5 100644
--- a/src/applications/metamta/storage/PhabricatorMetaMTAReceivedMail.php
+++ b/src/applications/metamta/storage/PhabricatorMetaMTAReceivedMail.php
@@ -1,373 +1,373 @@
<?php
final class PhabricatorMetaMTAReceivedMail extends PhabricatorMetaMTADAO {
protected $headers = array();
protected $bodies = array();
protected $attachments = array();
protected $status = '';
protected $relatedPHID;
protected $authorPHID;
protected $message;
protected $messageIDHash = '';
protected function getConfiguration() {
return array(
self::CONFIG_SERIALIZATION => array(
'headers' => self::SERIALIZATION_JSON,
'bodies' => self::SERIALIZATION_JSON,
'attachments' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'relatedPHID' => 'phid?',
'authorPHID' => 'phid?',
'message' => 'text?',
'messageIDHash' => 'bytes12',
'status' => 'text32',
),
self::CONFIG_KEY_SCHEMA => array(
'relatedPHID' => array(
'columns' => array('relatedPHID'),
),
'authorPHID' => array(
'columns' => array('authorPHID'),
),
'key_messageIDHash' => array(
'columns' => array('messageIDHash'),
),
'key_created' => array(
'columns' => array('dateCreated'),
),
),
) + parent::getConfiguration();
}
public function setHeaders(array $headers) {
// Normalize headers to lowercase.
$normalized = array();
foreach ($headers as $name => $value) {
$name = $this->normalizeMailHeaderName($name);
if ($name == 'message-id') {
$this->setMessageIDHash(PhabricatorHash::digestForIndex($value));
}
$normalized[$name] = $value;
}
$this->headers = $normalized;
return $this;
}
public function getHeader($key, $default = null) {
$key = $this->normalizeMailHeaderName($key);
return idx($this->headers, $key, $default);
}
private function normalizeMailHeaderName($name) {
return strtolower($name);
}
public function getMessageID() {
return $this->getHeader('Message-ID');
}
public function getSubject() {
return $this->getHeader('Subject');
}
public function getCCAddresses() {
return $this->getRawEmailAddresses(idx($this->headers, 'cc'));
}
public function getToAddresses() {
return $this->getRawEmailAddresses(idx($this->headers, 'to'));
}
public function loadExcludeMailRecipientPHIDs() {
$addresses = array_merge(
$this->getToAddresses(),
$this->getCCAddresses());
return $this->loadPHIDsFromAddresses($addresses);
}
public function loadCCPHIDs() {
return $this->loadPHIDsFromAddresses($this->getCCAddresses());
}
private function loadPHIDsFromAddresses(array $addresses) {
if (empty($addresses)) {
return array();
}
$users = id(new PhabricatorUserEmail())
->loadAllWhere('address IN (%Ls)', $addresses);
$user_phids = mpull($users, 'getUserPHID');
$mailing_lists = id(new PhabricatorMetaMTAMailingList())
->loadAllWhere('email in (%Ls)', $addresses);
$mailing_list_phids = mpull($mailing_lists, 'getPHID');
return array_merge($user_phids, $mailing_list_phids);
}
public function processReceivedMail() {
try {
$this->dropMailFromPhabricator();
$this->dropMailAlreadyReceived();
$receiver = $this->loadReceiver();
$sender = $receiver->loadSender($this);
$receiver->validateSender($this, $sender);
$this->setAuthorPHID($sender->getPHID());
// Now that we've identified the sender, mark them as the author of
// any attached files.
$attachments = $this->getAttachments();
if ($attachments) {
$files = id(new PhabricatorFileQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withPHIDs($attachments)
->execute();
foreach ($files as $file) {
$file->setAuthorPHID($sender->getPHID())->save();
}
}
$receiver->receiveMail($this, $sender);
} catch (PhabricatorMetaMTAReceivedMailProcessingException $ex) {
switch ($ex->getStatusCode()) {
case MetaMTAReceivedMailStatus::STATUS_DUPLICATE:
case MetaMTAReceivedMailStatus::STATUS_FROM_PHABRICATOR:
// Don't send an error email back in these cases, since they're
// very unlikely to be the sender's fault.
break;
case MetaMTAReceivedMailStatus::STATUS_EMPTY_IGNORED:
// This error is explicitly ignored.
break;
default:
$this->sendExceptionMail($ex);
break;
}
$this
->setStatus($ex->getStatusCode())
->setMessage($ex->getMessage())
->save();
return $this;
} catch (Exception $ex) {
$this->sendExceptionMail($ex);
$this
->setStatus(MetaMTAReceivedMailStatus::STATUS_UNHANDLED_EXCEPTION)
->setMessage(pht('Unhandled Exception: %s', $ex->getMessage()))
->save();
throw $ex;
}
return $this->setMessage('OK')->save();
}
public function getCleanTextBody() {
$body = $this->getRawTextBody();
$parser = new PhabricatorMetaMTAEmailBodyParser();
return $parser->stripTextBody($body);
}
public function parseBody() {
$body = $this->getRawTextBody();
$parser = new PhabricatorMetaMTAEmailBodyParser();
return $parser->parseBody($body);
}
public function getRawTextBody() {
return idx($this->bodies, 'text');
}
/**
* Strip an email address down to the actual user@domain.tld part if
* necessary, since sometimes it will have formatting like
* '"Abraham Lincoln" <alincoln@logcab.in>'.
*/
private function getRawEmailAddress($address) {
$matches = null;
$ok = preg_match('/<(.*)>/', $address, $matches);
if ($ok) {
$address = $matches[1];
}
return $address;
}
private function getRawEmailAddresses($addresses) {
$raw_addresses = array();
foreach (explode(',', $addresses) as $address) {
$raw_addresses[] = $this->getRawEmailAddress($address);
}
return array_filter($raw_addresses);
}
/**
* If Phabricator sent the mail, always drop it immediately. This prevents
* loops where, e.g., the public bug address is also a user email address
* and creating a bug sends them an email, which loops.
*/
private function dropMailFromPhabricator() {
if (!$this->getHeader('x-phabricator-sent-this-message')) {
return;
}
throw new PhabricatorMetaMTAReceivedMailProcessingException(
MetaMTAReceivedMailStatus::STATUS_FROM_PHABRICATOR,
pht(
- "Ignoring email with 'X-Phabricator-Sent-This-Message' header to ".
- "avoid loops."));
+ "Ignoring email with '%s' header to avoid loops.",
+ 'X-Phabricator-Sent-This-Message'));
}
/**
* If this mail has the same message ID as some other mail, and isn't the
* first mail we we received with that message ID, we drop it as a duplicate.
*/
private function dropMailAlreadyReceived() {
$message_id_hash = $this->getMessageIDHash();
if (!$message_id_hash) {
// No message ID hash, so we can't detect duplicates. This should only
// happen with very old messages.
return;
}
$messages = $this->loadAllWhere(
'messageIDHash = %s ORDER BY id ASC LIMIT 2',
$message_id_hash);
$messages_count = count($messages);
if ($messages_count <= 1) {
// If we only have one copy of this message, we're good to process it.
return;
}
$first_message = reset($messages);
if ($first_message->getID() == $this->getID()) {
// If this is the first copy of the message, it is okay to process it.
// We may not have been able to to process it immediately when we received
// it, and could may have received several copies without processing any
// yet.
return;
}
$message = pht(
'Ignoring email with "Message-ID" hash "%s" that has been seen %d '.
'times, including this message.',
$message_id_hash,
$messages_count);
throw new PhabricatorMetaMTAReceivedMailProcessingException(
MetaMTAReceivedMailStatus::STATUS_DUPLICATE,
$message);
}
/**
* Load a concrete instance of the @{class:PhabricatorMailReceiver} which
* accepts this mail, if one exists.
*/
private function loadReceiver() {
$receivers = id(new PhutilSymbolLoader())
->setAncestorClass('PhabricatorMailReceiver')
->loadObjects();
$accept = array();
foreach ($receivers as $key => $receiver) {
if (!$receiver->isEnabled()) {
continue;
}
if ($receiver->canAcceptMail($this)) {
$accept[$key] = $receiver;
}
}
if (!$accept) {
throw new PhabricatorMetaMTAReceivedMailProcessingException(
MetaMTAReceivedMailStatus::STATUS_NO_RECEIVERS,
pht(
'Phabricator can not process this mail because no application '.
'knows how to handle it. Check that the address you sent it to is '.
'correct.'.
"\n\n".
'(No concrete, enabled subclass of PhabricatorMailReceiver can '.
'accept this mail.)'));
}
if (count($accept) > 1) {
$names = implode(', ', array_keys($accept));
throw new PhabricatorMetaMTAReceivedMailProcessingException(
MetaMTAReceivedMailStatus::STATUS_ABUNDANT_RECEIVERS,
pht(
'Phabricator is not able to process this mail because more than '.
'one application is willing to accept it, creating ambiguity. '.
'Mail needs to be accepted by exactly one receiving application.'.
"\n\n".
'Accepting receivers: %s.',
$names));
}
return head($accept);
}
private function sendExceptionMail(Exception $ex) {
$from = $this->getHeader('from');
if (!strlen($from)) {
return;
}
if ($ex instanceof PhabricatorMetaMTAReceivedMailProcessingException) {
$status_code = $ex->getStatusCode();
$status_name = MetaMTAReceivedMailStatus::getHumanReadableName(
$status_code);
$title = pht('Error Processing Mail (%s)', $status_name);
$description = $ex->getMessage();
} else {
$title = pht('Error Processing Mail (%s)', get_class($ex));
$description = pht('%s: %s', get_class($ex), $ex->getMessage());
}
// TODO: Since headers don't necessarily have unique names, this may not
// really be all the headers. It would be nice to pass the raw headers
// through from the upper layers where possible.
$headers = array();
foreach ($this->headers as $key => $value) {
$headers[] = pht('%s: %s', $key, $value);
}
$headers = implode("\n", $headers);
$body = pht(<<<EOBODY
Your email to Phabricator was not processed, because an error occurred while
trying to handle it:
%s
-- Original Message Body -----------------------------------------------------
%s
-- Original Message Headers --------------------------------------------------
%s
EOBODY
,
wordwrap($description, 78),
$this->getRawTextBody(),
$headers);
$mail = id(new PhabricatorMetaMTAMail())
->setIsErrorEmail(true)
->setForceDelivery(true)
->setSubject($title)
->addRawTos(array($from))
->setBody($body)
->saveAndSend();
}
}
diff --git a/src/applications/metamta/storage/__tests__/PhabricatorMetaMTAMailTestCase.php b/src/applications/metamta/storage/__tests__/PhabricatorMetaMTAMailTestCase.php
index 5822b11a9..97369f054 100644
--- a/src/applications/metamta/storage/__tests__/PhabricatorMetaMTAMailTestCase.php
+++ b/src/applications/metamta/storage/__tests__/PhabricatorMetaMTAMailTestCase.php
@@ -1,212 +1,218 @@
<?php
final class PhabricatorMetaMTAMailTestCase extends PhabricatorTestCase {
protected function getPhabricatorTestCaseConfiguration() {
return array(
self::PHABRICATOR_TESTCONFIG_BUILD_STORAGE_FIXTURES => true,
);
}
public function testMailSendFailures() {
$user = $this->generateNewTestUser();
$phid = $user->getPHID();
// Normally, the send should succeed.
$mail = new PhabricatorMetaMTAMail();
$mail->addTos(array($phid));
$mailer = new PhabricatorMailImplementationTestAdapter();
$mail->sendNow($force = true, $mailer);
$this->assertEqual(
PhabricatorMetaMTAMail::STATUS_SENT,
$mail->getStatus());
// When the mailer fails temporarily, the mail should remain queued.
$mail = new PhabricatorMetaMTAMail();
$mail->addTos(array($phid));
$mailer = new PhabricatorMailImplementationTestAdapter();
$mailer->setFailTemporarily(true);
try {
$mail->sendNow($force = true, $mailer);
} catch (Exception $ex) {
// Ignore.
}
$this->assertEqual(
PhabricatorMetaMTAMail::STATUS_QUEUE,
$mail->getStatus());
// When the mailer fails permanently, the mail should be failed.
$mail = new PhabricatorMetaMTAMail();
$mail->addTos(array($phid));
$mailer = new PhabricatorMailImplementationTestAdapter();
$mailer->setFailPermanently(true);
try {
$mail->sendNow($force = true, $mailer);
} catch (Exception $ex) {
// Ignore.
}
$this->assertEqual(
PhabricatorMetaMTAMail::STATUS_FAIL,
$mail->getStatus());
}
public function testRecipients() {
$user = $this->generateNewTestUser();
$phid = $user->getPHID();
$prefs = $user->loadPreferences();
$mailer = new PhabricatorMailImplementationTestAdapter();
$mail = new PhabricatorMetaMTAMail();
$mail->addTos(array($phid));
$this->assertTrue(
in_array($phid, $mail->buildRecipientList()),
- '"To" is a recipient.');
+ pht('"To" is a recipient.'));
// Test that the "No Self Mail" and "No Mail" preferences work correctly.
$mail->setFrom($phid);
$this->assertTrue(
in_array($phid, $mail->buildRecipientList()),
- '"From" does not exclude recipients by default.');
+ pht('"From" does not exclude recipients by default.'));
$prefs->setPreference(
PhabricatorUserPreferences::PREFERENCE_NO_SELF_MAIL,
true);
$prefs->save();
$this->assertFalse(
in_array($phid, $mail->buildRecipientList()),
- '"From" excludes recipients with no-self-mail set.');
+ pht('"From" excludes recipients with no-self-mail set.'));
$prefs->unsetPreference(
PhabricatorUserPreferences::PREFERENCE_NO_SELF_MAIL);
$prefs->save();
$this->assertTrue(
in_array($phid, $mail->buildRecipientList()),
- '"From" does not exclude recipients by default.');
+ pht('"From" does not exclude recipients by default.'));
$prefs->setPreference(
PhabricatorUserPreferences::PREFERENCE_NO_MAIL,
true);
$prefs->save();
$this->assertFalse(
in_array($phid, $mail->buildRecipientList()),
- '"From" excludes recipients with no-mail set.');
+ pht('"From" excludes recipients with no-mail set.'));
$mail->setForceDelivery(true);
$this->assertTrue(
in_array($phid, $mail->buildRecipientList()),
- '"From" includes no-mail recipients when forced.');
+ pht('"From" includes no-mail recipients when forced.'));
$mail->setForceDelivery(false);
$prefs->unsetPreference(
PhabricatorUserPreferences::PREFERENCE_NO_MAIL);
$prefs->save();
$this->assertTrue(
in_array($phid, $mail->buildRecipientList()),
- '"From" does not exclude recipients by default.');
+ pht('"From" does not exclude recipients by default.'));
// Test that explicit exclusion works correctly.
$mail->setExcludeMailRecipientPHIDs(array($phid));
$this->assertFalse(
in_array($phid, $mail->buildRecipientList()),
- 'Explicit exclude excludes recipients.');
+ pht('Explicit exclude excludes recipients.'));
$mail->setExcludeMailRecipientPHIDs(array());
// Test that mail tag preferences exclude recipients.
$prefs->setPreference(
PhabricatorUserPreferences::PREFERENCE_MAILTAGS,
array(
'test-tag' => false,
));
$prefs->save();
$mail->setMailTags(array('test-tag'));
$this->assertFalse(
in_array($phid, $mail->buildRecipientList()),
- 'Tag preference excludes recipients.');
+ pht('Tag preference excludes recipients.'));
$prefs->unsetPreference(PhabricatorUserPreferences::PREFERENCE_MAILTAGS);
$prefs->save();
$this->assertTrue(
in_array($phid, $mail->buildRecipientList()),
'Recipients restored after tag preference removed.');
}
public function testThreadIDHeaders() {
$this->runThreadIDHeadersWithConfiguration(true, true);
$this->runThreadIDHeadersWithConfiguration(true, false);
$this->runThreadIDHeadersWithConfiguration(false, true);
$this->runThreadIDHeadersWithConfiguration(false, false);
}
private function runThreadIDHeadersWithConfiguration(
$supports_message_id,
$is_first_mail) {
$mailer = new PhabricatorMailImplementationTestAdapter(
array(
'supportsMessageIDHeader' => $supports_message_id,
));
$thread_id = '<somethread-12345@somedomain.tld>';
$mail = new PhabricatorMetaMTAMail();
$mail->setThreadID($thread_id, $is_first_mail);
$mail->sendNow($force = true, $mailer);
$guts = $mailer->getGuts();
$dict = ipull($guts['headers'], 1, 0);
if ($is_first_mail && $supports_message_id) {
$expect_message_id = true;
$expect_in_reply_to = false;
$expect_references = false;
} else {
$expect_message_id = false;
$expect_in_reply_to = true;
$expect_references = true;
}
$case = '<message-id = '.($supports_message_id ? 'Y' : 'N').', '.
'first = '.($is_first_mail ? 'Y' : 'N').'>';
$this->assertTrue(
isset($dict['Thread-Index']),
- "Expect Thread-Index header for case {$case}.");
+ pht('Expect Thread-Index header for case %s.', $case));
$this->assertEqual(
$expect_message_id,
isset($dict['Message-ID']),
- "Expectation about existence of Message-ID header for case {$case}.");
+ pht(
+ 'Expectation about existence of Message-ID header for case %s.',
+ $case));
$this->assertEqual(
$expect_in_reply_to,
isset($dict['In-Reply-To']),
- "Expectation about existence of In-Reply-To header for case {$case}.");
+ pht(
+ 'Expectation about existence of In-Reply-To header for case %s.',
+ $case));
$this->assertEqual(
$expect_references,
isset($dict['References']),
- "Expectation about existence of References header for case {$case}.");
+ pht(
+ 'Expectation about existence of References header for case %s.',
+ $case));
}
}
diff --git a/src/applications/notification/controller/PhabricatorNotificationClearController.php b/src/applications/notification/controller/PhabricatorNotificationClearController.php
index 94ee8aa0f..ba781234e 100644
--- a/src/applications/notification/controller/PhabricatorNotificationClearController.php
+++ b/src/applications/notification/controller/PhabricatorNotificationClearController.php
@@ -1,55 +1,54 @@
<?php
final class PhabricatorNotificationClearController
extends PhabricatorNotificationController {
public function processRequest() {
$request = $this->getRequest();
$chrono_key = $request->getStr('chronoKey');
$user = $request->getUser();
if ($request->isDialogFormPost()) {
$table = new PhabricatorFeedStoryNotification();
queryfx(
$table->establishConnection('w'),
'UPDATE %T SET hasViewed = 1 '.
'WHERE userPHID = %s AND hasViewed = 0 and chronologicalKey <= %s',
$table->getTableName(),
$user->getPHID(),
$chrono_key);
return id(new AphrontReloadResponse())
->setURI('/notification/');
}
$dialog = new AphrontDialogView();
$dialog->setUser($user);
$dialog->addCancelButton('/notification/');
if ($chrono_key) {
- $dialog->setTitle('Really mark all notifications as read?');
+ $dialog->setTitle(pht('Really mark all notifications as read?'));
$dialog->addHiddenInput('chronoKey', $chrono_key);
$is_serious =
PhabricatorEnv::getEnvConfig('phabricator.serious-business');
if ($is_serious) {
$dialog->appendChild(
pht(
'All unread notifications will be marked as read. You can not '.
'undo this action.'));
} else {
$dialog->appendChild(
pht(
"You can't ignore your problems forever, you know."));
}
$dialog->addSubmitButton(pht('Mark All Read'));
} else {
- $dialog->setTitle('No notifications to mark as read.');
- $dialog->appendChild(pht(
- 'You have no unread notifications.'));
+ $dialog->setTitle(pht('No notifications to mark as read.'));
+ $dialog->appendChild(pht('You have no unread notifications.'));
}
return id(new AphrontDialogResponse())->setDialog($dialog);
}
}
diff --git a/src/applications/notification/controller/PhabricatorNotificationStatusController.php b/src/applications/notification/controller/PhabricatorNotificationStatusController.php
index 398586e59..2c9e30898 100644
--- a/src/applications/notification/controller/PhabricatorNotificationStatusController.php
+++ b/src/applications/notification/controller/PhabricatorNotificationStatusController.php
@@ -1,84 +1,84 @@
<?php
final class PhabricatorNotificationStatusController
extends PhabricatorNotificationController {
public function processRequest() {
try {
$status = PhabricatorNotificationClient::getServerStatus();
$status = $this->renderServerStatus($status);
} catch (Exception $ex) {
$status = new PHUIInfoView();
- $status->setTitle('Notification Server Issue');
+ $status->setTitle(pht('Notification Server Issue'));
$status->appendChild(hsprintf(
- 'Unable to determine server status. This probably means the server '.
- 'is not in great shape. The specific issue encountered was:'.
- '<br />'.
- '<br />'.
+ '%s<br /><br />'.
'<strong>%s</strong> %s',
+ pht(
+ 'Unable to determine server status. This probably means the server '.
+ 'is not in great shape. The specific issue encountered was:'),
get_class($ex),
phutil_escape_html_newlines($ex->getMessage())));
}
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb(pht('Status'));
return $this->buildApplicationPage(
array(
$crumbs,
$status,
),
array(
'title' => pht('Notification Server Status'),
'device' => false,
));
}
private function renderServerStatus(array $status) {
$rows = array();
foreach ($status as $key => $value) {
switch ($key) {
case 'uptime':
$value /= 1000;
$value = phutil_format_relative_time_detailed($value);
break;
case 'log':
case 'instance':
break;
default:
$value = number_format($value);
break;
}
$rows[] = array($key, $value);
}
$table = new AphrontTableView($rows);
$table->setColumnClasses(
array(
'header',
'wide',
));
$test_icon = id(new PHUIIconView())
->setIconFont('fa-exclamation-triangle');
$test_button = id(new PHUIButtonView())
->setTag('a')
->setWorkflow(true)
->setText(pht('Send Test Notification'))
->setHref($this->getApplicationURI('test/'))
->setIcon($test_icon);
$header = id(new PHUIHeaderView())
->setHeader(pht('Notification Server Status'))
->addActionLink($test_button);
$box = id(new PHUIObjectBoxView())
->setHeader($header)
->appendChild($table);
return $box;
}
}
diff --git a/src/applications/notification/setup/PhabricatorAphlictSetupCheck.php b/src/applications/notification/setup/PhabricatorAphlictSetupCheck.php
index 91096c824..99dba45ec 100644
--- a/src/applications/notification/setup/PhabricatorAphlictSetupCheck.php
+++ b/src/applications/notification/setup/PhabricatorAphlictSetupCheck.php
@@ -1,64 +1,62 @@
<?php
final class PhabricatorAphlictSetupCheck extends PhabricatorSetupCheck {
protected function executeChecks() {
$enabled = PhabricatorEnv::getEnvConfig('notification.enabled');
if (!$enabled) {
// Notifications aren't set up, so just ignore all of these checks.
return;
}
try {
$status = PhabricatorNotificationClient::getServerStatus();
} catch (Exception $ex) {
$message = pht(
- 'Phabricator is configured to use a notification server, but '.
- 'is unable to connect to it. You should resolve this issue or '.
- 'disable the notification server. It may be helpful to double check '.
- 'your configuration or restart the server using the command below.'.
- "\n\n".
- "%s",
+ "Phabricator is configured to use a notification server, but is ".
+ "unable to connect to it. You should resolve this issue or disable ".
+ "the notification server. It may be helpful to double check your ".
+ "configuration or restart the server using the command below.\n\n%s",
phutil_tag(
'pre',
array(),
array(
get_class($ex),
"\n",
$ex->getMessage(),
)));
$this->newIssue('aphlict.connect')
->setShortName(pht('Notification Server Down'))
->setName(pht('Unable to Connect to Notification Server'))
->setMessage($message)
->addRelatedPhabricatorConfig('notification.enabled')
->addRelatedPhabricatorConfig('notification.server-uri')
->addCommand(
pht(
"(To start the server, run this command.)\n%s",
'phabricator/ $ ./bin/aphlict start'));
return;
}
$expect_version = PhabricatorNotificationClient::EXPECT_VERSION;
$have_version = idx($status, 'version', 1);
if ($have_version != $expect_version) {
$message = pht(
'The notification server is out of date. You are running server '.
'version %d, but Phabricator expects version %d. Restart the server '.
'to update it, using the command below:',
$have_version,
$expect_version);
$this->newIssue('aphlict.version')
->setShortName(pht('Notification Server Version'))
->setName(pht('Notification Server Out of Date'))
->setMessage($message)
->addCommand('phabricator/ $ ./bin/aphlict restart');
}
}
}
diff --git a/src/applications/nuance/controller/NuanceQueueViewController.php b/src/applications/nuance/controller/NuanceQueueViewController.php
index d863cda0e..261b52819 100644
--- a/src/applications/nuance/controller/NuanceQueueViewController.php
+++ b/src/applications/nuance/controller/NuanceQueueViewController.php
@@ -1,42 +1,42 @@
<?php
final class NuanceQueueViewController extends NuanceController {
private $queueID;
public function setQueueID($queue_id) {
$this->queueID = $queue_id;
return $this;
}
public function getQueueID() {
return $this->queueID;
}
public function willProcessRequest(array $data) {
$this->setQueueID($data['id']);
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$queue_id = $this->getQueueID();
$queue = id(new NuanceQueueQuery())
->setViewer($user)
->withIDs(array($queue_id))
->executeOne();
if (!$queue) {
return new Aphront404Response();
}
$crumbs = $this->buildApplicationCrumbs();
- $title = 'TODO';
+ $title = pht('TODO');
return $this->buildApplicationPage(
$crumbs,
array(
'title' => $title,
));
}
}
diff --git a/src/applications/nuance/controller/NuanceRequestorEditController.php b/src/applications/nuance/controller/NuanceRequestorEditController.php
index 193865547..42405a227 100644
--- a/src/applications/nuance/controller/NuanceRequestorEditController.php
+++ b/src/applications/nuance/controller/NuanceRequestorEditController.php
@@ -1,50 +1,50 @@
<?php
final class NuanceRequestorEditController extends NuanceController {
private $requestorID;
public function setRequestorID($requestor_id) {
$this->requestorID = $requestor_id;
return $this;
}
public function getRequestorID() {
return $this->requestorID;
}
public function willProcessRequest(array $data) {
$this->setRequestorID(idx($data, 'id'));
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$requestor_id = $this->getRequestorID();
$is_new = !$requestor_id;
if ($is_new) {
$requestor = new NuanceRequestor();
} else {
$requestor = id(new NuanceRequestorQuery())
->setViewer($user)
->withIDs(array($requestor_id))
->executeOne();
}
if (!$requestor) {
return new Aphront404Response();
}
$crumbs = $this->buildApplicationCrumbs();
- $title = 'TODO';
+ $title = pht('TODO');
return $this->buildApplicationPage(
$crumbs,
array(
'title' => $title,
));
}
}
diff --git a/src/applications/nuance/source/NuancePhabricatorFormSourceDefinition.php b/src/applications/nuance/source/NuancePhabricatorFormSourceDefinition.php
index 6ebb07d0f..1d49b17c5 100644
--- a/src/applications/nuance/source/NuancePhabricatorFormSourceDefinition.php
+++ b/src/applications/nuance/source/NuancePhabricatorFormSourceDefinition.php
@@ -1,41 +1,41 @@
<?php
final class NuancePhabricatorFormSourceDefinition
extends NuanceSourceDefinition {
public function getName() {
return pht('Phabricator Form');
}
public function getSourceTypeConstant() {
return 'phabricator-form';
}
public function updateItems() {
return null;
}
protected function augmentEditForm(
AphrontFormView $form,
PhabricatorApplicationTransactionValidationException $ex = null) {
/* TODO - add a box to allow for custom fields to be defined here, so that
- * these NuanceSource objects made from this defintion can be used to
+ * these NuanceSource objects made from this definition can be used to
* capture arbitrary data */
return $form;
}
protected function buildTransactions(AphrontRequest $request) {
$transactions = parent::buildTransactions($request);
// TODO -- as above
return $transactions;
}
public function renderView() {}
public function renderListView() {}
}
diff --git a/src/applications/nuance/source/NuanceSourceDefinition.php b/src/applications/nuance/source/NuanceSourceDefinition.php
index e643daa0b..b2092dfdf 100644
--- a/src/applications/nuance/source/NuanceSourceDefinition.php
+++ b/src/applications/nuance/source/NuanceSourceDefinition.php
@@ -1,262 +1,263 @@
<?php
abstract class NuanceSourceDefinition extends Phobject {
private $actor;
private $sourceObject;
public function setActor(PhabricatorUser $actor) {
$this->actor = $actor;
return $this;
}
public function getActor() {
return $this->actor;
}
public function requireActor() {
$actor = $this->getActor();
if (!$actor) {
- throw new Exception('You must "setActor()" first!');
+ throw new PhutilInvalidStateException('setActor');
}
return $actor;
}
public function setSourceObject(NuanceSource $source) {
$source->setType($this->getSourceTypeConstant());
$this->sourceObject = $source;
return $this;
}
public function getSourceObject() {
return $this->sourceObject;
}
public function requireSourceObject() {
$source = $this->getSourceObject();
if (!$source) {
- throw new Exception('You must "setSourceObject()" first!');
+ throw new PhutilInvalidStateException('setSourceObject');
}
return $source;
}
public static function getSelectOptions() {
$definitions = self::getAllDefinitions();
$options = array();
foreach ($definitions as $definition) {
$key = $definition->getSourceTypeConstant();
$name = $definition->getName();
$options[$key] = $name;
}
return $options;
}
/**
* Gives a @{class:NuanceSourceDefinition} object for a given
* @{class:NuanceSource}. Note you still need to @{method:setActor}
* before the @{class:NuanceSourceDefinition} object will be useful.
*/
public static function getDefinitionForSource(NuanceSource $source) {
$definitions = self::getAllDefinitions();
$map = mpull($definitions, null, 'getSourceTypeConstant');
$definition = $map[$source->getType()];
$definition->setSourceObject($source);
return $definition;
}
public static function getAllDefinitions() {
static $definitions;
if ($definitions === null) {
$objects = id(new PhutilSymbolLoader())
->setAncestorClass(__CLASS__)
->loadObjects();
foreach ($objects as $definition) {
$key = $definition->getSourceTypeConstant();
$name = $definition->getName();
if (isset($definitions[$key])) {
$conflict = $definitions[$key];
- throw new Exception(sprintf(
- 'Defintion %s conflicts with definition %s. This is a programming '.
- 'error.',
- $conflict,
- $name));
+ throw new Exception(
+ pht(
+ 'Definition %s conflicts with definition %s. This is a '.
+ 'programming error.',
+ $conflict,
+ $name));
}
}
$definitions = $objects;
}
return $definitions;
}
/**
* A human readable string like "Twitter" or "Phabricator Form".
*/
abstract public function getName();
/**
* This should be a any VARCHAR(32).
*
* @{method:getAllDefinitions} will throw if you choose a string that
* collides with another @{class:NuanceSourceDefinition} class.
*/
abstract public function getSourceTypeConstant();
/**
* Code to create and update @{class:NuanceItem}s and
* @{class:NuanceRequestor}s via daemons goes here.
*
* If that does not make sense for the @{class:NuanceSource} you are
* defining, simply return null. For example,
* @{class:NuancePhabricatorFormSourceDefinition} since these are one-way
* contact forms.
*/
abstract public function updateItems();
private function loadSourceObjectPolicies(
PhabricatorUser $user,
NuanceSource $source) {
$user = $this->requireActor();
$source = $this->requireSourceObject();
return id(new PhabricatorPolicyQuery())
->setViewer($user)
->setObject($source)
->execute();
}
final public function getEditTitle() {
$source = $this->requireSourceObject();
if ($source->getPHID()) {
$title = pht('Edit "%s" source.', $source->getName());
} else {
$title = pht('Create a new "%s" source.', $this->getName());
}
return $title;
}
final public function buildEditLayout(AphrontRequest $request) {
$actor = $this->requireActor();
$source = $this->requireSourceObject();
$form_errors = array();
$error_messages = array();
$transactions = array();
$validation_exception = null;
if ($request->isFormPost()) {
$transactions = $this->buildTransactions($request);
try {
$editor = id(new NuanceSourceEditor())
->setActor($actor)
->setContentSourceFromRequest($request)
->setContinueOnNoEffect(true)
->applyTransactions($source, $transactions);
return id(new AphrontRedirectResponse())
->setURI($source->getURI());
} catch (PhabricatorApplicationTransactionValidationException $ex) {
$validation_exception = $ex;
}
}
$form = $this->renderEditForm($validation_exception);
$layout = id(new PHUIObjectBoxView())
->setHeaderText($this->getEditTitle())
->setValidationException($validation_exception)
->setFormErrors($error_messages)
->setForm($form);
return $layout;
}
/**
* Code to create a form to edit the @{class:NuanceItem} you are defining.
*
* return @{class:AphrontFormView}
*/
private function renderEditForm(
PhabricatorApplicationTransactionValidationException $ex = null) {
$user = $this->requireActor();
$source = $this->requireSourceObject();
$policies = $this->loadSourceObjectPolicies($user, $source);
$e_name = null;
if ($ex) {
$e_name = $ex->getShortMessage(NuanceSourceTransaction::TYPE_NAME);
}
$form = id(new AphrontFormView())
->setUser($user)
->appendChild(
id(new AphrontFormTextControl())
->setLabel(pht('Name'))
->setName('name')
->setError($e_name)
->setValue($source->getName()))
->appendChild(
id(new AphrontFormSelectControl())
->setLabel(pht('Type'))
->setName('type')
->setOptions(self::getSelectOptions())
->setValue($source->getType()));
$form = $this->augmentEditForm($form, $ex);
$form
->appendChild(
id(new AphrontFormPolicyControl())
->setUser($user)
->setCapability(PhabricatorPolicyCapability::CAN_VIEW)
->setPolicyObject($source)
->setPolicies($policies)
->setName('viewPolicy'))
->appendChild(
id(new AphrontFormPolicyControl())
->setUser($user)
->setCapability(PhabricatorPolicyCapability::CAN_EDIT)
->setPolicyObject($source)
->setPolicies($policies)
->setName('editPolicy'))
->appendChild(
id(new AphrontFormSubmitControl())
->addCancelButton($source->getURI())
->setValue(pht('Save')));
return $form;
}
/**
* return @{class:AphrontFormView}
*/
protected function augmentEditForm(
AphrontFormView $form,
PhabricatorApplicationTransactionValidationException $ex = null) {
return $form;
}
/**
* Hook to build up @{class:PhabricatorTransactions}.
*
* return array $transactions
*/
protected function buildTransactions(AphrontRequest $request) {
$transactions = array();
$transactions[] = id(new NuanceSourceTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_EDIT_POLICY)
->setNewValue($request->getStr('editPolicy'));
$transactions[] = id(new NuanceSourceTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_VIEW_POLICY)
->setNewValue($request->getStr('viewPolicy'));
$transactions[] = id(new NuanceSourceTransaction())
->setTransactionType(NuanceSourceTransaction::TYPE_NAME)
->setNewvalue($request->getStr('name'));
return $transactions;
}
abstract public function renderView();
abstract public function renderListView();
}
diff --git a/src/applications/oauthserver/PhabricatorOAuthServer.php b/src/applications/oauthserver/PhabricatorOAuthServer.php
index b7dc0d630..5f75083c8 100644
--- a/src/applications/oauthserver/PhabricatorOAuthServer.php
+++ b/src/applications/oauthserver/PhabricatorOAuthServer.php
@@ -1,272 +1,272 @@
<?php
/**
* Implements core OAuth 2.0 Server logic.
*
* This class should be used behind business logic that parses input to
* determine pertinent @{class:PhabricatorUser} $user,
* @{class:PhabricatorOAuthServerClient} $client(s),
* @{class:PhabricatorOAuthServerAuthorizationCode} $code(s), and.
* @{class:PhabricatorOAuthServerAccessToken} $token(s).
*
* For an OAuth 2.0 server, there are two main steps:
*
* 1) Authorization - the user authorizes a given client to access the data
* the OAuth 2.0 server protects. Once this is achieved / if it has
* been achived already, the OAuth server sends the client an authorization
* code.
* 2) Access Token - the client should send the authorization code received in
* step 1 along with its id and secret to the OAuth server to receive an
* access token. This access token can later be used to access Phabricator
* data on behalf of the user.
*
* @task auth Authorizing @{class:PhabricatorOAuthServerClient}s and
* generating @{class:PhabricatorOAuthServerAuthorizationCode}s
* @task token Validating @{class:PhabricatorOAuthServerAuthorizationCode}s
* and generating @{class:PhabricatorOAuthServerAccessToken}s
* @task internal Internals
*/
final class PhabricatorOAuthServer {
const AUTHORIZATION_CODE_TIMEOUT = 300;
const ACCESS_TOKEN_TIMEOUT = 3600;
private $user;
private $client;
private function getUser() {
if (!$this->user) {
- throw new Exception('You must setUser before you can getUser!');
+ throw new PhutilInvalidStateException('setUser');
}
return $this->user;
}
public function setUser(PhabricatorUser $user) {
$this->user = $user;
return $this;
}
private function getClient() {
if (!$this->client) {
- throw new Exception('You must setClient before you can getClient!');
+ throw new PhutilInvalidStateException('setClient');
}
return $this->client;
}
public function setClient(PhabricatorOAuthServerClient $client) {
$this->client = $client;
return $this;
}
/**
* @task auth
* @return tuple <bool hasAuthorized, ClientAuthorization or null>
*/
public function userHasAuthorizedClient(array $scope) {
$authorization = id(new PhabricatorOAuthClientAuthorization())->
- loadOneWhere('userPHID = %s AND clientPHID = %s',
- $this->getUser()->getPHID(),
- $this->getClient()->getPHID());
+ loadOneWhere(
+ 'userPHID = %s AND clientPHID = %s',
+ $this->getUser()->getPHID(),
+ $this->getClient()->getPHID());
if (empty($authorization)) {
return array(false, null);
}
if ($scope) {
- $missing_scope = array_diff_key($scope,
- $authorization->getScope());
+ $missing_scope = array_diff_key($scope, $authorization->getScope());
} else {
$missing_scope = false;
}
if ($missing_scope) {
return array(false, $authorization);
}
return array(true, $authorization);
}
/**
* @task auth
*/
public function authorizeClient(array $scope) {
$authorization = new PhabricatorOAuthClientAuthorization();
$authorization->setUserPHID($this->getUser()->getPHID());
$authorization->setClientPHID($this->getClient()->getPHID());
$authorization->setScope($scope);
$authorization->save();
return $authorization;
}
/**
* @task auth
*/
public function generateAuthorizationCode(PhutilURI $redirect_uri) {
$code = Filesystem::readRandomCharacters(32);
$client = $this->getClient();
$authorization_code = new PhabricatorOAuthServerAuthorizationCode();
$authorization_code->setCode($code);
$authorization_code->setClientPHID($client->getPHID());
$authorization_code->setClientSecret($client->getSecret());
$authorization_code->setUserPHID($this->getUser()->getPHID());
$authorization_code->setRedirectURI((string)$redirect_uri);
$authorization_code->save();
return $authorization_code;
}
/**
* @task token
*/
public function generateAccessToken() {
$token = Filesystem::readRandomCharacters(32);
$access_token = new PhabricatorOAuthServerAccessToken();
$access_token->setToken($token);
$access_token->setUserPHID($this->getUser()->getPHID());
$access_token->setClientPHID($this->getClient()->getPHID());
$access_token->save();
return $access_token;
}
/**
* @task token
*/
public function validateAuthorizationCode(
PhabricatorOAuthServerAuthorizationCode $test_code,
PhabricatorOAuthServerAuthorizationCode $valid_code) {
// check that all the meta data matches
if ($test_code->getClientPHID() != $valid_code->getClientPHID()) {
return false;
}
if ($test_code->getClientSecret() != $valid_code->getClientSecret()) {
return false;
}
// check that the authorization code hasn't timed out
$created_time = $test_code->getDateCreated();
$must_be_used_by = $created_time + self::AUTHORIZATION_CODE_TIMEOUT;
return (time() < $must_be_used_by);
}
/**
* @task token
*/
public function validateAccessToken(
PhabricatorOAuthServerAccessToken $token,
$required_scope) {
$created_time = $token->getDateCreated();
$must_be_used_by = $created_time + self::ACCESS_TOKEN_TIMEOUT;
$expired = time() > $must_be_used_by;
$authorization = id(new PhabricatorOAuthClientAuthorization())
- ->loadOneWhere(
- 'userPHID = %s AND clientPHID = %s',
- $token->getUserPHID(),
- $token->getClientPHID());
+ ->loadOneWhere(
+ 'userPHID = %s AND clientPHID = %s',
+ $token->getUserPHID(),
+ $token->getClientPHID());
if (!$authorization) {
return false;
}
$token_scope = $authorization->getScope();
if (!isset($token_scope[$required_scope])) {
return false;
}
$valid = true;
if ($expired) {
$valid = false;
// check if the scope includes "offline_access", which makes the
// token valid despite being expired
if (isset(
$token_scope[PhabricatorOAuthServerScope::SCOPE_OFFLINE_ACCESS])) {
$valid = true;
}
}
return $valid;
}
/**
* See http://tools.ietf.org/html/draft-ietf-oauth-v2-23#section-3.1.2
* for details on what makes a given redirect URI "valid".
*/
public function validateRedirectURI(PhutilURI $uri) {
if (!PhabricatorEnv::isValidRemoteURIForLink($uri)) {
return false;
}
if ($uri->getFragment()) {
return false;
}
if (!$uri->getDomain()) {
return false;
}
return true;
}
/**
* If there's a URI specified in an OAuth request, it must be validated in
* its own right. Further, it must have the same domain, the same path, the
* same port, and (at least) the same query parameters as the primary URI.
*/
public function validateSecondaryRedirectURI(
PhutilURI $secondary_uri,
PhutilURI $primary_uri) {
// The secondary URI must be valid.
if (!$this->validateRedirectURI($secondary_uri)) {
return false;
}
// Both URIs must point at the same domain.
if ($secondary_uri->getDomain() != $primary_uri->getDomain()) {
return false;
}
// Both URIs must have the same path
if ($secondary_uri->getPath() != $primary_uri->getPath()) {
return false;
}
// Both URIs must have the same port
if ($secondary_uri->getPort() != $primary_uri->getPort()) {
return false;
}
// Any query parameters present in the first URI must be exactly present
// in the second URI.
$need_params = $primary_uri->getQueryParams();
$have_params = $secondary_uri->getQueryParams();
foreach ($need_params as $key => $value) {
if (!array_key_exists($key, $have_params)) {
return false;
}
if ((string)$have_params[$key] != (string)$value) {
return false;
}
}
// If the first URI is HTTPS, the second URI must also be HTTPS. This
// defuses an attack where a third party with control over the network
// tricks you into using HTTP to authenticate over a link which is supposed
// to be HTTPS only and sniffs all your token cookies.
if (strtolower($primary_uri->getProtocol()) == 'https') {
if (strtolower($secondary_uri->getProtocol()) != 'https') {
return false;
}
}
return true;
}
}
diff --git a/src/applications/oauthserver/PhabricatorOAuthServerScope.php b/src/applications/oauthserver/PhabricatorOAuthServerScope.php
index 38707fa3c..dcba6b623 100644
--- a/src/applications/oauthserver/PhabricatorOAuthServerScope.php
+++ b/src/applications/oauthserver/PhabricatorOAuthServerScope.php
@@ -1,127 +1,127 @@
<?php
final class PhabricatorOAuthServerScope {
const SCOPE_OFFLINE_ACCESS = 'offline_access';
const SCOPE_WHOAMI = 'whoami';
const SCOPE_NOT_ACCESSIBLE = 'not_accessible';
/*
* Note this does not contain SCOPE_NOT_ACCESSIBLE which is magic
* used to simplify code for data that is not currently accessible
* via OAuth.
*/
static public function getScopesDict() {
return array(
self::SCOPE_OFFLINE_ACCESS => 1,
self::SCOPE_WHOAMI => 1,
);
}
static public function getDefaultScope() {
return self::SCOPE_WHOAMI;
}
static public function getCheckboxControl(
array $current_scopes) {
$have_options = false;
$scopes = self::getScopesDict();
$scope_keys = array_keys($scopes);
sort($scope_keys);
$default_scope = self::getDefaultScope();
$checkboxes = new AphrontFormCheckboxControl();
foreach ($scope_keys as $scope) {
if ($scope == $default_scope) {
continue;
}
if (!isset($current_scopes[$scope])) {
continue;
}
$checkboxes->addCheckbox(
$name = $scope,
$value = 1,
$label = self::getCheckboxLabel($scope),
$checked = isset($current_scopes[$scope]));
$have_options = true;
}
if ($have_options) {
$checkboxes->setLabel(pht('Scope'));
return $checkboxes;
}
return null;
}
static private function getCheckboxLabel($scope) {
$label = null;
switch ($scope) {
case self::SCOPE_OFFLINE_ACCESS:
- $label = 'Make access tokens granted to this client never expire.';
+ $label = pht('Make access tokens granted to this client never expire.');
break;
case self::SCOPE_WHOAMI:
- $label = 'Read access to Conduit method user.whoami.';
+ $label = pht('Read access to Conduit method %s.', 'user.whoami');
break;
}
return $label;
}
static public function getScopesFromRequest(AphrontRequest $request) {
$scopes = self::getScopesDict();
$requested_scopes = array();
foreach ($scopes as $scope => $bit) {
if ($request->getBool($scope)) {
$requested_scopes[$scope] = 1;
}
}
$requested_scopes[self::getDefaultScope()] = 1;
return $requested_scopes;
}
/**
* A scopes list is considered valid if each scope is a known scope
* and each scope is seen only once. Otherwise, the list is invalid.
*/
static public function validateScopesList($scope_list) {
$scopes = explode(' ', $scope_list);
$known_scopes = self::getScopesDict();
$seen_scopes = array();
foreach ($scopes as $scope) {
if (!isset($known_scopes[$scope])) {
return false;
}
if (isset($seen_scopes[$scope])) {
return false;
}
$seen_scopes[$scope] = 1;
}
return true;
}
/**
* A scopes dictionary is considered valid if each key is a known scope.
* Otherwise, the dictionary is invalid.
*/
static public function validateScopesDict($scope_dict) {
$known_scopes = self::getScopesDict();
$unknown_scopes = array_diff_key($scope_dict,
$known_scopes);
return empty($unknown_scopes);
}
/**
* Transforms a space-delimited scopes list into a scopes dict. The list
* should be validated by @{method:validateScopesList} before
* transformation.
*/
static public function scopesListToDict($scope_list) {
$scopes = explode(' ', $scope_list);
return array_fill_keys($scopes, 1);
}
}
diff --git a/src/applications/oauthserver/__tests__/PhabricatorOAuthServerTestCase.php b/src/applications/oauthserver/__tests__/PhabricatorOAuthServerTestCase.php
index 30fea69a9..2fa1778f5 100644
--- a/src/applications/oauthserver/__tests__/PhabricatorOAuthServerTestCase.php
+++ b/src/applications/oauthserver/__tests__/PhabricatorOAuthServerTestCase.php
@@ -1,96 +1,100 @@
<?php
final class PhabricatorOAuthServerTestCase
extends PhabricatorTestCase {
public function testValidateRedirectURI() {
static $map = array(
'http://www.google.com' => true,
'http://www.google.com/' => true,
'http://www.google.com/auth' => true,
'www.google.com' => false,
'http://www.google.com/auth#invalid' => false,
);
$server = new PhabricatorOAuthServer();
foreach ($map as $input => $expected) {
$uri = new PhutilURI($input);
$result = $server->validateRedirectURI($uri);
$this->assertEqual(
$expected,
$result,
- "Validation of redirect URI '{$input}'");
+ pht("Validation of redirect URI '%s'", $input));
}
}
public function testValidateSecondaryRedirectURI() {
$server = new PhabricatorOAuthServer();
$primary_uri = new PhutilURI('http://www.google.com/');
static $test_domain_map = array(
'http://www.google.com' => false,
'http://www.google.com/' => true,
'http://www.google.com/auth' => false,
'http://www.google.com/?auth' => true,
'www.google.com' => false,
'http://www.google.com/auth#invalid' => false,
'http://www.example.com' => false,
);
foreach ($test_domain_map as $input => $expected) {
$uri = new PhutilURI($input);
$this->assertEqual(
$expected,
$server->validateSecondaryRedirectURI($uri, $primary_uri),
- "Validation of redirect URI '{$input}' ".
- "relative to '{$primary_uri}'");
+ pht(
+ "Validation of redirect URI '%s' relative to '%s'",
+ $input,
+ $primary_uri));
}
$primary_uri = new PhutilURI('http://www.google.com/?auth');
static $test_query_map = array(
'http://www.google.com' => false,
'http://www.google.com/' => false,
'http://www.google.com/auth' => false,
'http://www.google.com/?auth' => true,
'http://www.google.com/?auth&stuff' => true,
'http://www.google.com/?stuff' => false,
);
foreach ($test_query_map as $input => $expected) {
$uri = new PhutilURI($input);
$this->assertEqual(
$expected,
$server->validateSecondaryRedirectURI($uri, $primary_uri),
- "Validation of secondary redirect URI '{$input}' ".
- "relative to '{$primary_uri}'");
+ pht(
+ "Validation of secondary redirect URI '%s' relative to '%s'",
+ $input,
+ $primary_uri));
}
$primary_uri = new PhutilURI('https://secure.example.com/');
$tests = array(
'https://secure.example.com/' => true,
'http://secure.example.com/' => false,
);
foreach ($tests as $input => $expected) {
$uri = new PhutilURI($input);
$this->assertEqual(
$expected,
$server->validateSecondaryRedirectURI($uri, $primary_uri),
- "Validation (https): {$input}");
+ pht('Validation (https): %s', $input));
}
$primary_uri = new PhutilURI('http://example.com/?z=2&y=3');
$tests = array(
'http://example.com/?z=2&y=3' => true,
'http://example.com/?y=3&z=2' => true,
'http://example.com/?y=3&z=2&x=1' => true,
'http://example.com/?y=2&z=3' => false,
'http://example.com/?y&x' => false,
'http://example.com/?z=2&x=3' => false,
);
foreach ($tests as $input => $expected) {
$uri = new PhutilURI($input);
$this->assertEqual(
$expected,
$server->validateSecondaryRedirectURI($uri, $primary_uri),
- "Validation (params): {$input}");
+ pht('Validation (params): %s', $input));
}
}
}
diff --git a/src/applications/oauthserver/controller/PhabricatorOAuthServerController.php b/src/applications/oauthserver/controller/PhabricatorOAuthServerController.php
index 1c0dc588b..22388c876 100644
--- a/src/applications/oauthserver/controller/PhabricatorOAuthServerController.php
+++ b/src/applications/oauthserver/controller/PhabricatorOAuthServerController.php
@@ -1,64 +1,60 @@
<?php
abstract class PhabricatorOAuthServerController
extends PhabricatorController {
public function buildStandardPageResponse($view, array $data) {
$user = $this->getRequest()->getUser();
$page = $this->buildStandardPageView();
- $page->setApplicationName('OAuth Server');
+ $page->setApplicationName(pht('OAuth Server'));
$page->setBaseURI('/oauthserver/');
$page->setTitle(idx($data, 'title'));
$nav = new AphrontSideNavFilterView();
$nav->setBaseURI(new PhutilURI('/oauthserver/'));
- $nav->addLabel('Clients');
- $nav->addFilter('client/create',
- 'Create Client');
+ $nav->addLabel(pht('Clients'));
+ $nav->addFilter('client/create', pht('Create Client'));
foreach ($this->getExtraClientFilters() as $filter) {
- $nav->addFilter($filter['url'],
- $filter['label']);
+ $nav->addFilter($filter['url'], $filter['label']);
}
- $nav->addFilter('client',
- 'My Clients');
- $nav->selectFilter($this->getFilter(),
- 'clientauthorization');
+ $nav->addFilter('client', pht('My Clients'));
+ $nav->selectFilter($this->getFilter(), 'clientauthorization');
$nav->appendChild($view);
$page->appendChild($nav);
$response = new AphrontWebpageResponse();
return $response->setContent($page->render());
}
protected function getFilter() {
return 'clientauthorization';
}
protected function getExtraClientFilters() {
return array();
}
protected function getHighlightPHIDs() {
$phids = array();
$request = $this->getRequest();
$edited = $request->getStr('edited');
$new = $request->getStr('new');
if ($edited) {
$phids[$edited] = $edited;
}
if ($new) {
$phids[$new] = $new;
}
return $phids;
}
protected function buildErrorView($error_message) {
$error = new PHUIInfoView();
$error->setSeverity(PHUIInfoView::SEVERITY_ERROR);
$error->setTitle($error_message);
return $error;
}
}
diff --git a/src/applications/oauthserver/controller/PhabricatorOAuthServerTokenController.php b/src/applications/oauthserver/controller/PhabricatorOAuthServerTokenController.php
index 5b3db13fb..aa9b8f71a 100644
--- a/src/applications/oauthserver/controller/PhabricatorOAuthServerTokenController.php
+++ b/src/applications/oauthserver/controller/PhabricatorOAuthServerTokenController.php
@@ -1,138 +1,156 @@
<?php
final class PhabricatorOAuthServerTokenController
extends PhabricatorAuthController {
public function shouldRequireLogin() {
return false;
}
public function shouldAllowRestrictedParameter($parameter_name) {
if ($parameter_name == 'code') {
return true;
}
return parent::shouldAllowRestrictedParameter($parameter_name);
}
public function processRequest() {
$request = $this->getRequest();
$grant_type = $request->getStr('grant_type');
$code = $request->getStr('code');
$redirect_uri = $request->getStr('redirect_uri');
$client_phid = $request->getStr('client_id');
$client_secret = $request->getStr('client_secret');
$response = new PhabricatorOAuthResponse();
$server = new PhabricatorOAuthServer();
if ($grant_type != 'authorization_code') {
$response->setError('unsupported_grant_type');
$response->setErrorDescription(
- 'Only grant_type authorization_code is supported.');
+ pht(
+ 'Only %s %s is supported.',
+ 'grant_type',
+ 'authorization_code'));
return $response;
}
if (!$code) {
$response->setError('invalid_request');
- $response->setErrorDescription(
- 'Required parameter code missing.');
+ $response->setErrorDescription(pht('Required parameter code missing.'));
return $response;
}
if (!$client_phid) {
$response->setError('invalid_request');
$response->setErrorDescription(
- 'Required parameter client_id missing.');
+ pht(
+ 'Required parameter %s missing.',
+ 'client_id'));
return $response;
}
if (!$client_secret) {
$response->setError('invalid_request');
$response->setErrorDescription(
- 'Required parameter client_secret missing.');
+ pht(
+ 'Required parameter %s missing.',
+ 'client_secret'));
return $response;
}
// one giant try / catch around all the exciting database stuff so we
// can return a 'server_error' response if something goes wrong!
try {
$auth_code = id(new PhabricatorOAuthServerAuthorizationCode())
->loadOneWhere('code = %s',
$code);
if (!$auth_code) {
$response->setError('invalid_grant');
$response->setErrorDescription(
- 'Authorization code '.$code.' not found.');
+ pht(
+ 'Authorization code %d not found.',
+ $code));
return $response;
}
// if we have an auth code redirect URI, there must be a redirect_uri
// in the request and it must match the auth code redirect uri *exactly*
$auth_code_redirect_uri = $auth_code->getRedirectURI();
if ($auth_code_redirect_uri) {
$auth_code_redirect_uri = new PhutilURI($auth_code_redirect_uri);
$redirect_uri = new PhutilURI($redirect_uri);
if (!$redirect_uri->getDomain() ||
$redirect_uri != $auth_code_redirect_uri) {
$response->setError('invalid_grant');
$response->setErrorDescription(
- 'Redirect uri in request must exactly match redirect uri '.
- 'from authorization code.');
+ pht(
+ 'Redirect URI in request must exactly match redirect URI '.
+ 'from authorization code.'));
return $response;
}
} else if ($redirect_uri) {
$response->setError('invalid_grant');
$response->setErrorDescription(
- 'Redirect uri in request and no redirect uri in authorization '.
- 'code. The two must exactly match.');
+ pht(
+ 'Redirect URI in request and no redirect URI in authorization '.
+ 'code. The two must exactly match.'));
return $response;
}
$client = id(new PhabricatorOAuthServerClient())
- ->loadOneWhere('phid = %s',
- $client_phid);
+ ->loadOneWhere('phid = %s', $client_phid);
if (!$client) {
$response->setError('invalid_client');
$response->setErrorDescription(
- 'Client with client_id '.$client_phid.' not found.');
+ pht(
+ 'Client with %s %d not found.',
+ 'client_id',
+ $client_phid));
return $response;
}
$server->setClient($client);
$user_phid = $auth_code->getUserPHID();
$user = id(new PhabricatorUser())
->loadOneWhere('phid = %s', $user_phid);
if (!$user) {
$response->setError('invalid_grant');
$response->setErrorDescription(
- 'User with phid '.$user_phid.' not found.');
+ pht(
+ 'User with PHID %d not found.',
+ $user_phid));
return $response;
}
$server->setUser($user);
$test_code = new PhabricatorOAuthServerAuthorizationCode();
$test_code->setClientSecret($client_secret);
$test_code->setClientPHID($client_phid);
- $is_good_code = $server->validateAuthorizationCode($auth_code,
- $test_code);
+ $is_good_code = $server->validateAuthorizationCode(
+ $auth_code,
+ $test_code);
if (!$is_good_code) {
$response->setError('invalid_grant');
$response->setErrorDescription(
- 'Invalid authorization code '.$code.'.');
+ pht(
+ 'Invalid authorization code %d.',
+ $code));
return $response;
}
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$access_token = $server->generateAccessToken();
$auth_code->delete();
unset($unguarded);
$result = array(
'access_token' => $access_token->getToken(),
'token_type' => 'Bearer',
'expires_in' => PhabricatorOAuthServer::ACCESS_TOKEN_TIMEOUT,
);
return $response->setContent($result);
} catch (Exception $e) {
$response->setError('server_error');
$response->setErrorDescription(
- 'The authorization server encountered an unexpected condition '.
- 'which prevented it from fulfilling the request.');
+ pht(
+ 'The authorization server encountered an unexpected condition '.
+ 'which prevented it from fulfilling the request.'));
return $response;
}
}
}
diff --git a/src/applications/oauthserver/controller/client/PhabricatorOAuthClientEditController.php b/src/applications/oauthserver/controller/client/PhabricatorOAuthClientEditController.php
index 1f1b0e387..cd194aa74 100644
--- a/src/applications/oauthserver/controller/client/PhabricatorOAuthClientEditController.php
+++ b/src/applications/oauthserver/controller/client/PhabricatorOAuthClientEditController.php
@@ -1,137 +1,137 @@
<?php
final class PhabricatorOAuthClientEditController
extends PhabricatorOAuthClientController {
public function processRequest() {
$request = $this->getRequest();
$viewer = $request->getUser();
$phid = $this->getClientPHID();
if ($phid) {
$client = id(new PhabricatorOAuthServerClientQuery())
->setViewer($viewer)
->withPHIDs(array($phid))
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
if (!$client) {
return new Aphront404Response();
}
$title = pht('Edit OAuth Application: %s', $client->getName());
$submit_button = pht('Save Application');
$crumb_text = pht('Edit');
$cancel_uri = $client->getViewURI();
$is_new = false;
} else {
$this->requireApplicationCapability(
PhabricatorOAuthServerCreateClientsCapability::CAPABILITY);
$client = PhabricatorOAuthServerClient::initializeNewClient($viewer);
$title = pht('Create OAuth Application');
$submit_button = pht('Create Application');
$crumb_text = pht('Create Application');
$cancel_uri = $this->getApplicationURI();
$is_new = true;
}
$errors = array();
$e_redirect = true;
$e_name = true;
if ($request->isFormPost()) {
$redirect_uri = $request->getStr('redirect_uri');
$client->setName($request->getStr('name'));
$client->setRedirectURI($redirect_uri);
if (!strlen($client->getName())) {
$errors[] = pht('You must choose a name for this OAuth application.');
$e_name = pht('Required');
}
$server = new PhabricatorOAuthServer();
$uri = new PhutilURI($redirect_uri);
if (!$server->validateRedirectURI($uri)) {
$errors[] = pht(
'Redirect URI must be a fully qualified domain name '.
'with no fragments. See %s for more information on the correct '.
'format.',
'http://tools.ietf.org/html/draft-ietf-oauth-v2-23#section-3.1.2');
$e_redirect = pht('Invalid');
}
$client->setViewPolicy($request->getStr('viewPolicy'));
$client->setEditPolicy($request->getStr('editPolicy'));
if (!$errors) {
$client->save();
$view_uri = $client->getViewURI();
return id(new AphrontRedirectResponse())->setURI($view_uri);
}
}
$policies = id(new PhabricatorPolicyQuery())
->setViewer($viewer)
->setObject($client)
->execute();
$form = id(new AphrontFormView())
->setUser($viewer)
->appendChild(
id(new AphrontFormTextControl())
- ->setLabel('Name')
+ ->setLabel(pht('Name'))
->setName('name')
->setValue($client->getName())
->setError($e_name))
->appendChild(
id(new AphrontFormTextControl())
- ->setLabel('Redirect URI')
+ ->setLabel(pht('Redirect URI'))
->setName('redirect_uri')
->setValue($client->getRedirectURI())
->setError($e_redirect))
->appendChild(
id(new AphrontFormPolicyControl())
->setUser($viewer)
->setCapability(PhabricatorPolicyCapability::CAN_VIEW)
->setPolicyObject($client)
->setPolicies($policies)
->setName('viewPolicy'))
->appendChild(
id(new AphrontFormPolicyControl())
->setUser($viewer)
->setCapability(PhabricatorPolicyCapability::CAN_EDIT)
->setPolicyObject($client)
->setPolicies($policies)
->setName('editPolicy'))
->appendChild(
id(new AphrontFormSubmitControl())
->addCancelButton($cancel_uri)
->setValue($submit_button));
$crumbs = $this->buildApplicationCrumbs();
if (!$is_new) {
$crumbs->addTextCrumb(
$client->getName(),
$client->getViewURI());
}
$crumbs->addTextCrumb($crumb_text);
$box = id(new PHUIObjectBoxView())
->setHeaderText($title)
->setFormErrors($errors)
->setForm($form);
return $this->buildApplicationPage(
array(
$crumbs,
$box,
),
array(
'title' => $title,
));
}
}
diff --git a/src/applications/oauthserver/controller/client/PhabricatorOAuthClientSecretController.php b/src/applications/oauthserver/controller/client/PhabricatorOAuthClientSecretController.php
index cdfc53e5b..ceb1379b7 100644
--- a/src/applications/oauthserver/controller/client/PhabricatorOAuthClientSecretController.php
+++ b/src/applications/oauthserver/controller/client/PhabricatorOAuthClientSecretController.php
@@ -1,70 +1,70 @@
<?php
final class PhabricatorOAuthClientSecretController
extends PhabricatorOAuthClientController {
public function handleRequest(AphrontRequest $request) {
$viewer = $request->getUser();
$client = id(new PhabricatorOAuthServerClientQuery())
->setViewer($viewer)
->withPHIDs(array($this->getClientPHID()))
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
if (!$client) {
return new Aphront404Response();
}
$view_uri = $client->getViewURI();
$token = id(new PhabricatorAuthSessionEngine())->requireHighSecuritySession(
$viewer,
$request,
$view_uri);
if ($request->isFormPost()) {
$secret = $client->getSecret();
$body = id(new PHUIFormLayoutView())
->appendChild(
id(new AphrontFormTextAreaControl())
->setLabel(pht('Plaintext'))
->setReadOnly(true)
->setHeight(AphrontFormTextAreaControl::HEIGHT_VERY_SHORT)
->setValue($secret));
$dialog = id(new AphrontDialogView())
->setUser($viewer)
->setWidth(AphrontDialogView::WIDTH_FORM)
->setTitle(pht('Application Secret'))
->appendChild($body)
->addCancelButton($view_uri, pht('Done'));
return id(new AphrontDialogResponse())->setDialog($dialog);
}
$is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business');
if ($is_serious) {
$body = pht(
- 'The secret associated with this oauth application will be shown in '.
+ 'The secret associated with this OAuth application will be shown in '.
'plain text on your screen.');
} else {
$body = pht(
- 'The secret associated with this oauth application will be shown in '.
+ 'The secret associated with this OAuth application will be shown in '.
'plain text on your screen. Before continuing, wrap your arms around '.
'your monitor to create a human shield, keeping it safe from prying '.
'eyes. Protect company secrets!');
}
return $this->newDialog()
->setUser($viewer)
->setTitle(pht('Really show application secret?'))
->appendChild($body)
->addSubmitButton(pht('Show Application Secret'))
->addCancelButton($view_uri);
}
}
diff --git a/src/applications/oauthserver/panel/PhabricatorOAuthServerAuthorizationsSettingsPanel.php b/src/applications/oauthserver/panel/PhabricatorOAuthServerAuthorizationsSettingsPanel.php
index 8504c97ce..2e2cff174 100644
--- a/src/applications/oauthserver/panel/PhabricatorOAuthServerAuthorizationsSettingsPanel.php
+++ b/src/applications/oauthserver/panel/PhabricatorOAuthServerAuthorizationsSettingsPanel.php
@@ -1,143 +1,142 @@
<?php
final class PhabricatorOAuthServerAuthorizationsSettingsPanel
extends PhabricatorSettingsPanel {
public function getPanelKey() {
return 'oauthorizations';
}
public function getPanelName() {
return pht('OAuth Authorizations');
}
public function getPanelGroup() {
return pht('Sessions and Logs');
}
public function isEnabled() {
- $app_name = 'PhabricatorOAuthServerApplication';
- return PhabricatorApplication::isClassInstalled($app_name);
+ return PhabricatorApplication::isClassInstalled(
+ 'PhabricatorOAuthServerApplication');
}
public function processRequest(AphrontRequest $request) {
$viewer = $request->getUser();
// TODO: It would be nice to simply disable this panel, but we can't do
// viewer-based checks for enabled panels right now.
$app_class = 'PhabricatorOAuthServerApplication';
$installed = PhabricatorApplication::isClassInstalledForViewer(
$app_class,
$viewer);
if (!$installed) {
$dialog = id(new AphrontDialogView())
->setUser($viewer)
->setTitle(pht('OAuth Not Available'))
->appendParagraph(
pht('You do not have access to OAuth authorizations.'))
->addCancelButton('/settings/');
return id(new AphrontDialogResponse())->setDialog($dialog);
}
$authorizations = id(new PhabricatorOAuthClientAuthorizationQuery())
->setViewer($viewer)
->withUserPHIDs(array($viewer->getPHID()))
->execute();
$authorizations = mpull($authorizations, null, 'getID');
$panel_uri = $this->getPanelURI();
$revoke = $request->getInt('revoke');
if ($revoke) {
if (empty($authorizations[$revoke])) {
return new Aphront404Response();
}
if ($request->isFormPost()) {
$authorizations[$revoke]->delete();
return id(new AphrontRedirectResponse())->setURI($panel_uri);
}
$dialog = id(new AphrontDialogView())
->setUser($viewer)
->setTitle(pht('Revoke Authorization?'))
->appendParagraph(
pht(
'This application will no longer be able to access Phabricator '.
'on your behalf.'))
->addSubmitButton(pht('Revoke Authorization'))
->addCancelButton($panel_uri);
return id(new AphrontDialogResponse())->setDialog($dialog);
}
$highlight = $request->getInt('id');
$rows = array();
$rowc = array();
foreach ($authorizations as $authorization) {
if ($highlight == $authorization->getID()) {
$rowc[] = 'highlighted';
} else {
$rowc[] = null;
}
$button = javelin_tag(
'a',
array(
'href' => $this->getPanelURI('?revoke='.$authorization->getID()),
'class' => 'small grey button',
'sigil' => 'workflow',
),
pht('Revoke'));
$rows[] = array(
phutil_tag(
'a',
array(
'href' => $authorization->getClient()->getViewURI(),
),
$authorization->getClient()->getName()),
$authorization->getScopeString(),
phabricator_datetime($authorization->getDateCreated(), $viewer),
phabricator_datetime($authorization->getDateModified(), $viewer),
$button,
);
}
$table = new AphrontTableView($rows);
$table->setNoDataString(
- pht(
- "You haven't authorized any OAuth applications."));
+ pht("You haven't authorized any OAuth applications."));
$table->setRowClasses($rowc);
$table->setHeaders(
array(
pht('Application'),
pht('Scope'),
pht('Created'),
pht('Updated'),
null,
));
$table->setColumnClasses(
array(
'pri',
'wide',
'right',
'right',
'action',
));
$header = id(new PHUIHeaderView())
->setHeader(pht('OAuth Application Authorizations'));
$panel = id(new PHUIObjectBoxView())
->setHeader($header)
->appendChild($table);
return $panel;
}
}
diff --git a/src/applications/owners/conduit/OwnersQueryConduitAPIMethod.php b/src/applications/owners/conduit/OwnersQueryConduitAPIMethod.php
index f62c45628..5473ec6e0 100644
--- a/src/applications/owners/conduit/OwnersQueryConduitAPIMethod.php
+++ b/src/applications/owners/conduit/OwnersQueryConduitAPIMethod.php
@@ -1,157 +1,162 @@
<?php
final class OwnersQueryConduitAPIMethod extends OwnersConduitAPIMethod {
public function getAPIMethodName() {
return 'owners.query';
}
public function getMethodDescription() {
- return 'Query for packages by one of the following: repository/path, '.
+ return pht(
+ 'Query for packages by one of the following: repository/path, '.
'packages with a given user or project owner, or packages affiliated '.
'with a user (owned by either the user or a project they are a member '.
- 'of.) You should only provide at most one search query.';
+ 'of.) You should only provide at most one search query.');
}
protected function defineParamTypes() {
return array(
'userOwner' => 'optional string',
'projectOwner' => 'optional string',
'userAffiliated' => 'optional string',
'repositoryCallsign' => 'optional string',
'path' => 'optional string',
);
}
protected function defineReturnType() {
return 'dict<phid -> dict of package info>';
}
protected function defineErrorTypes() {
return array(
- 'ERR-INVALID-USAGE' =>
+ 'ERR-INVALID-USAGE' => pht(
'Provide one of a single owner phid (user/project), a single '.
- 'affiliated user phid (user), or a repository/path.',
- 'ERR-INVALID-PARAMETER' => 'parameter should be a phid',
- 'ERR_REP_NOT_FOUND' => 'The repository callsign is not recognized',
+ 'affiliated user phid (user), or a repository/path.'),
+ 'ERR-INVALID-PARAMETER' => pht('Parameter should be a phid.'),
+ 'ERR_REP_NOT_FOUND' => pht('The repository callsign is not recognized.'),
);
}
protected static function queryAll() {
return id(new PhabricatorOwnersPackage())->loadAll();
}
protected static function queryByOwner($owner) {
$is_valid_phid =
phid_get_type($owner) == PhabricatorPeopleUserPHIDType::TYPECONST ||
phid_get_type($owner) == PhabricatorProjectProjectPHIDType::TYPECONST;
if (!$is_valid_phid) {
throw id(new ConduitException('ERR-INVALID-PARAMETER'))
->setErrorDescription(
- 'Expected user/project PHID for owner, got '.$owner);
+ pht(
+ 'Expected user/project PHID for owner, got %s.',
+ $owner));
}
$owners = id(new PhabricatorOwnersOwner())->loadAllWhere(
'userPHID = %s',
$owner);
$package_ids = mpull($owners, 'getPackageID');
$packages = array();
foreach ($package_ids as $id) {
$packages[] = id(new PhabricatorOwnersPackage())->load($id);
}
return $packages;
}
private static function queryByPath(
PhabricatorUser $viewer,
$repo_callsign,
$path) {
$repository = id(new PhabricatorRepositoryQuery())
->setViewer($viewer)
->withCallsigns(array($repo_callsign))
->executeOne();
if (!$repository) {
throw id(new ConduitException('ERR_REP_NOT_FOUND'))
->setErrorDescription(
- 'Repository callsign '.$repo_callsign.' not recognized');
+ pht(
+ 'Repository callsign %s not recognized',
+ $repo_callsign));
}
if ($path == null) {
return PhabricatorOwnersPackage::loadPackagesForRepository($repository);
} else {
return PhabricatorOwnersPackage::loadOwningPackages(
$repository, $path);
}
}
public static function buildPackageInformationDictionaries($packages) {
assert_instances_of($packages, 'PhabricatorOwnersPackage');
$result = array();
foreach ($packages as $package) {
$p_owners = $package->loadOwners();
$p_paths = $package->loadPaths();
$owners = array_values(mpull($p_owners, 'getUserPHID'));
$paths = array();
foreach ($p_paths as $p) {
$paths[] = array($p->getRepositoryPHID(), $p->getPath());
}
$result[$package->getPHID()] = array(
'phid' => $package->getPHID(),
'name' => $package->getName(),
'description' => $package->getDescription(),
'primaryOwner' => $package->getPrimaryOwnerPHID(),
'owners' => $owners,
'paths' => $paths,
);
}
return $result;
}
protected function execute(ConduitAPIRequest $request) {
$is_owner_query =
($request->getValue('userOwner') ||
$request->getValue('projectOwner')) ?
1 : 0;
$is_affiliated_query = $request->getValue('userAffiliated') ? 1 : 0;
$repo = $request->getValue('repositoryCallsign');
$path = $request->getValue('path');
$is_path_query = $repo ? 1 : 0;
if ($is_owner_query + $is_path_query + $is_affiliated_query === 0) {
// if no search terms are provided, return everything
$packages = self::queryAll();
} else if ($is_owner_query + $is_path_query + $is_affiliated_query > 1) {
// otherwise, exactly one of these should be provided
throw new ConduitException('ERR-INVALID-USAGE');
}
if ($is_affiliated_query) {
$query = id(new PhabricatorOwnersPackageQuery())
->setViewer($request->getUser());
$query->withOwnerPHIDs(array($request->getValue('userAffiliated')));
$packages = $query->execute();
} else if ($is_owner_query) {
$owner = nonempty(
$request->getValue('userOwner'),
$request->getValue('projectOwner'));
$packages = self::queryByOwner($owner);
} else if ($is_path_query) {
$packages = self::queryByPath($request->getUser(), $repo, $path);
}
return self::buildPackageInformationDictionaries($packages);
}
}
diff --git a/src/applications/owners/controller/PhabricatorOwnersDeleteController.php b/src/applications/owners/controller/PhabricatorOwnersDeleteController.php
index 4f14ddf66..7b275b740 100644
--- a/src/applications/owners/controller/PhabricatorOwnersDeleteController.php
+++ b/src/applications/owners/controller/PhabricatorOwnersDeleteController.php
@@ -1,42 +1,44 @@
<?php
final class PhabricatorOwnersDeleteController
extends PhabricatorOwnersController {
private $id;
public function willProcessRequest(array $data) {
$this->id = $data['id'];
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$package = id(new PhabricatorOwnersPackage())->load($this->id);
if (!$package) {
return new Aphront404Response();
}
if ($request->isDialogFormPost()) {
id(new PhabricatorOwnersPackageEditor())
->setActor($user)
->setPackage($package)
->delete();
return id(new AphrontRedirectResponse())->setURI('/owners/');
}
- $text = pht('Are you sure you want to delete the "%s" package? This '.
- 'operation can not be undone.', $package->getName());
+ $text = pht(
+ 'Are you sure you want to delete the "%s" package? This '.
+ 'operation can not be undone.',
+ $package->getName());
$dialog = id(new AphrontDialogView())
->setUser($user)
- ->setTitle('Really delete this package?')
+ ->setTitle(pht('Really delete this package?'))
->appendChild(phutil_tag('p', array(), $text))
->addSubmitButton(pht('Delete'))
->addCancelButton('/owners/package/'.$package->getID().'/')
->setSubmitURI($request->getRequestURI());
return id(new AphrontDialogResponse())->setDialog($dialog);
}
}
diff --git a/src/applications/owners/controller/PhabricatorOwnersEditController.php b/src/applications/owners/controller/PhabricatorOwnersEditController.php
index 34bf51373..3a0baaa06 100644
--- a/src/applications/owners/controller/PhabricatorOwnersEditController.php
+++ b/src/applications/owners/controller/PhabricatorOwnersEditController.php
@@ -1,277 +1,279 @@
<?php
final class PhabricatorOwnersEditController
extends PhabricatorOwnersController {
private $id;
public function willProcessRequest(array $data) {
$this->id = idx($data, 'id');
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
if ($this->id) {
$package = id(new PhabricatorOwnersPackage())->load($this->id);
if (!$package) {
return new Aphront404Response();
}
} else {
$package = new PhabricatorOwnersPackage();
$package->setPrimaryOwnerPHID($user->getPHID());
}
$e_name = true;
$e_primary = true;
$errors = array();
if ($request->isFormPost()) {
$package->setName($request->getStr('name'));
$package->setDescription($request->getStr('description'));
$old_auditing_enabled = $package->getAuditingEnabled();
$package->setAuditingEnabled(
($request->getStr('auditing') === 'enabled')
? 1
: 0);
$primary = $request->getArr('primary');
$primary = reset($primary);
$old_primary = $package->getPrimaryOwnerPHID();
$package->setPrimaryOwnerPHID($primary);
$owners = $request->getArr('owners');
if ($primary) {
array_unshift($owners, $primary);
}
$owners = array_unique($owners);
$paths = $request->getArr('path');
$repos = $request->getArr('repo');
$excludes = $request->getArr('exclude');
$path_refs = array();
for ($ii = 0; $ii < count($paths); $ii++) {
if (empty($paths[$ii]) || empty($repos[$ii])) {
continue;
}
$path_refs[] = array(
'repositoryPHID' => $repos[$ii],
'path' => $paths[$ii],
'excluded' => $excludes[$ii],
);
}
if (!strlen($package->getName())) {
$e_name = pht('Required');
$errors[] = pht('Package name is required.');
} else {
$e_name = null;
}
if (!$package->getPrimaryOwnerPHID()) {
$e_primary = pht('Required');
$errors[] = pht('Package must have a primary owner.');
} else {
$e_primary = null;
}
if (!$path_refs) {
$errors[] = pht('Package must include at least one path.');
}
if (!$errors) {
$package->attachUnsavedOwners($owners);
$package->attachUnsavedPaths($path_refs);
$package->attachOldAuditingEnabled($old_auditing_enabled);
$package->attachOldPrimaryOwnerPHID($old_primary);
try {
id(new PhabricatorOwnersPackageEditor())
->setActor($user)
->setPackage($package)
->save();
return id(new AphrontRedirectResponse())
->setURI('/owners/package/'.$package->getID().'/');
} catch (AphrontDuplicateKeyQueryException $ex) {
$e_name = pht('Duplicate');
$errors[] = pht('Package name must be unique.');
}
}
} else {
$owners = $package->loadOwners();
$owners = mpull($owners, 'getUserPHID');
$paths = $package->loadPaths();
$path_refs = array();
foreach ($paths as $path) {
$path_refs[] = array(
'repositoryPHID' => $path->getRepositoryPHID(),
'path' => $path->getPath(),
'excluded' => $path->getExcluded(),
);
}
}
$primary = $package->getPrimaryOwnerPHID();
if ($primary) {
$value_primary_owner = array($primary);
} else {
$value_primary_owner = array();
}
if ($package->getID()) {
$title = pht('Edit Package');
$side_nav_filter = 'edit/'.$this->id;
} else {
$title = pht('New Package');
$side_nav_filter = 'new';
}
$this->setSideNavFilter($side_nav_filter);
$repos = id(new PhabricatorRepositoryQuery())
->setViewer($user)
->execute();
$default_paths = array();
foreach ($repos as $repo) {
$default_path = $repo->getDetail('default-owners-path');
if ($default_path) {
$default_paths[$repo->getPHID()] = $default_path;
}
}
$repos = mpull($repos, 'getCallsign', 'getPHID');
asort($repos);
$template = new AphrontTypeaheadTemplateView();
$template = $template->render();
Javelin::initBehavior(
'owners-path-editor',
array(
'root' => 'path-editor',
'table' => 'paths',
'add_button' => 'addpath',
'repositories' => $repos,
'input_template' => $template,
'pathRefs' => $path_refs,
'completeURI' => '/diffusion/services/path/complete/',
'validateURI' => '/diffusion/services/path/validate/',
'repositoryDefaultPaths' => $default_paths,
));
require_celerity_resource('owners-path-editor-css');
$cancel_uri = $package->getID()
? '/owners/package/'.$package->getID().'/'
: '/owners/';
$form = id(new AphrontFormView())
->setUser($user)
->appendChild(
id(new AphrontFormTextControl())
->setLabel(pht('Name'))
->setName('name')
->setValue($package->getName())
->setError($e_name))
->appendControl(
id(new AphrontFormTokenizerControl())
->setDatasource(new PhabricatorProjectOrUserDatasource())
->setLabel(pht('Primary Owner'))
->setName('primary')
->setLimit(1)
->setValue($value_primary_owner)
->setError($e_primary))
->appendControl(
id(new AphrontFormTokenizerControl())
->setDatasource(new PhabricatorProjectOrUserDatasource())
->setLabel(pht('Owners'))
->setName('owners')
->setValue($owners))
->appendChild(
id(new AphrontFormSelectControl())
->setName('auditing')
->setLabel(pht('Auditing'))
->setCaption(
- pht('With auditing enabled, all future commits that touch '.
- 'this package will be reviewed to make sure an owner '.
- 'of the package is involved and the commit message has '.
- 'a valid revision, reviewed by, and author.'))
+ pht(
+ 'With auditing enabled, all future commits that touch '.
+ 'this package will be reviewed to make sure an owner '.
+ 'of the package is involved and the commit message has '.
+ 'a valid revision, reviewed by, and author.'))
->setOptions(array(
'disabled' => pht('Disabled'),
'enabled' => pht('Enabled'),
))
->setValue(
$package->getAuditingEnabled()
? 'enabled'
: 'disabled'))
->appendChild(
id(new PHUIFormInsetView())
->setTitle(pht('Paths'))
->addDivAttributes(array('id' => 'path-editor'))
->setRightButton(javelin_tag(
'a',
array(
'href' => '#',
'class' => 'button green',
'sigil' => 'addpath',
'mustcapture' => true,
),
pht('Add New Path')))
->setDescription(
- pht('Specify the files and directories which comprise '.
- 'this package.'))
+ pht(
+ 'Specify the files and directories which comprise '.
+ 'this package.'))
->setContent(javelin_tag(
'table',
array(
'class' => 'owners-path-editor-table',
'sigil' => 'paths',
),
'')))
->appendChild(
id(new AphrontFormTextAreaControl())
->setLabel(pht('Description'))
->setName('description')
->setValue($package->getDescription()))
->appendChild(
id(new AphrontFormSubmitControl())
->addCancelButton($cancel_uri)
->setValue(pht('Save Package')));
$form_box = id(new PHUIObjectBoxView())
->setHeaderText($title)
->setFormErrors($errors)
->setForm($form);
$crumbs = $this->buildApplicationCrumbs();
if ($package->getID()) {
$crumbs->addTextCrumb(pht('Edit %s', $package->getName()));
} else {
$crumbs->addTextCrumb(pht('New Package'));
}
$nav = $this->buildSideNavView();
$nav->appendChild($crumbs);
$nav->appendChild($form_box);
return $this->buildApplicationPage(
array(
$nav,
),
array(
'title' => $title,
));
}
protected function getExtraPackageViews(AphrontSideNavFilterView $view) {
if ($this->id) {
$view->addFilter('edit/'.$this->id, pht('Edit'));
} else {
$view->addFilter('new', pht('New'));
}
}
}
diff --git a/src/applications/owners/mail/OwnersPackageReplyHandler.php b/src/applications/owners/mail/OwnersPackageReplyHandler.php
index b0f397249..0e28ff12b 100644
--- a/src/applications/owners/mail/OwnersPackageReplyHandler.php
+++ b/src/applications/owners/mail/OwnersPackageReplyHandler.php
@@ -1,22 +1,25 @@
<?php
final class OwnersPackageReplyHandler extends PhabricatorMailReplyHandler {
public function validateMailReceiver($mail_receiver) {
if (!($mail_receiver instanceof PhabricatorOwnersPackage)) {
- throw new Exception('Receiver is not a PhabricatorOwnersPackage!');
+ throw new Exception(
+ pht(
+ 'Receiver is not a %s!',
+ 'PhabricatorOwnersPackage'));
}
}
public function getPrivateReplyHandlerEmailAddress(
PhabricatorObjectHandle $handle) {
return null;
}
public function getPublicReplyHandlerEmailAddress() {
return null;
}
protected function receiveEmail(PhabricatorMetaMTAReceivedMail $mail) {
return;
}
}
diff --git a/src/applications/owners/mail/PackageCreateMail.php b/src/applications/owners/mail/PackageCreateMail.php
index 486704afd..7f447d7d8 100644
--- a/src/applications/owners/mail/PackageCreateMail.php
+++ b/src/applications/owners/mail/PackageCreateMail.php
@@ -1,12 +1,12 @@
<?php
final class PackageCreateMail extends PackageMail {
protected function isNewThread() {
return true;
}
protected function getVerb() {
- return 'Created';
+ return pht('Created');
}
}
diff --git a/src/applications/owners/mail/PackageDeleteMail.php b/src/applications/owners/mail/PackageDeleteMail.php
index 8d992b3fb..002fa0498 100644
--- a/src/applications/owners/mail/PackageDeleteMail.php
+++ b/src/applications/owners/mail/PackageDeleteMail.php
@@ -1,13 +1,13 @@
<?php
final class PackageDeleteMail extends PackageMail {
protected function getVerb() {
- return 'Deleted';
+ return pht('Deleted');
}
protected function isNewThread() {
return false;
}
}
diff --git a/src/applications/owners/mail/PackageMail.php b/src/applications/owners/mail/PackageMail.php
index 00a8b196f..683f60812 100644
--- a/src/applications/owners/mail/PackageMail.php
+++ b/src/applications/owners/mail/PackageMail.php
@@ -1,209 +1,212 @@
<?php
abstract class PackageMail extends PhabricatorMail {
protected $package;
protected $handles;
protected $owners;
protected $paths;
protected $mailTo;
public function __construct(PhabricatorOwnersPackage $package) {
$this->package = $package;
}
abstract protected function getVerb();
abstract protected function isNewThread();
final protected function getPackage() {
return $this->package;
}
final protected function getHandles() {
return $this->handles;
}
final protected function getOwners() {
return $this->owners;
}
final protected function getPaths() {
return $this->paths;
}
final protected function getMailTo() {
return $this->mailTo;
}
final protected function renderPackageTitle() {
return $this->getPackage()->getName();
}
final protected function renderRepoSubSection($repository_phid, $paths) {
$handles = $this->getHandles();
$section = array();
- $section[] = ' In repository '.$handles[$repository_phid]->getName().
+ $section[] = ' '.
+ pht('In repository %s', $handles[$repository_phid]->getName()).
' - '.PhabricatorEnv::getProductionURI($handles[$repository_phid]
->getURI());
foreach ($paths as $path => $excluded) {
- $section[] = ' '.($excluded ? 'Excluded' : 'Included').' '.$path;
+ $section[] = ' '.
+ ($excluded ? pht('Excluded') : pht('Included')).' '.$path;
}
return implode("\n", $section);
}
protected function needSend() {
return true;
}
protected function loadData() {
$package = $this->getPackage();
$owners = $package->loadOwners();
$this->owners = $owners;
$owner_phids = mpull($owners, 'getUserPHID');
$primary_owner_phid = $package->getPrimaryOwnerPHID();
$mail_to = $owner_phids;
if (!in_array($primary_owner_phid, $owner_phids)) {
$mail_to[] = $primary_owner_phid;
}
$this->mailTo = $mail_to;
$this->paths = array();
$repository_paths = mgroup($package->loadPaths(), 'getRepositoryPHID');
foreach ($repository_paths as $repository_phid => $paths) {
$this->paths[$repository_phid] = mpull($paths, 'getExcluded', 'getPath');
}
$phids = array_merge(
$this->mailTo,
array($package->getActorPHID()),
array_keys($this->paths));
$this->handles = id(new PhabricatorHandleQuery())
->setViewer($this->getActor())
->withPHIDs($phids)
->execute();
}
final protected function renderSummarySection() {
$package = $this->getPackage();
$handles = $this->getHandles();
$section = array();
$section[] = $handles[$package->getActorPHID()]->getName().' '.
strtolower($this->getVerb()).' '.$this->renderPackageTitle().'.';
$section[] = '';
- $section[] = 'PACKAGE DETAIL';
+ $section[] = pht('PACKAGE DETAIL');
$section[] = ' '.PhabricatorEnv::getProductionURI(
'/owners/package/'.$package->getID().'/');
return implode("\n", $section);
}
protected function renderDescriptionSection() {
- return "PACKAGE DESCRIPTION\n".
- ' '.$this->getPackage()->getDescription();
+ return pht('PACKAGE DESCRIPTION')."\n ".
+ $this->getPackage()->getDescription();
}
protected function renderPrimaryOwnerSection() {
$handles = $this->getHandles();
- return "PRIMARY OWNER\n".
- ' '.$handles[$this->getPackage()->getPrimaryOwnerPHID()]->getName();
+ return pht('PRIMARY OWNER')."\n ".
+ $handles[$this->getPackage()->getPrimaryOwnerPHID()]->getName();
}
protected function renderOwnersSection() {
$handles = $this->getHandles();
$owners = $this->getOwners();
if (!$owners) {
return null;
}
$owners = mpull($owners, 'getUserPHID');
$owners = array_select_keys($handles, $owners);
$owners = mpull($owners, 'getName');
- return "OWNERS\n".
- ' '.implode(', ', $owners);
+ return pht('OWNERS')."\n ".implode(', ', $owners);
}
protected function renderAuditingEnabledSection() {
- return "AUDITING ENABLED STATUS\n".
- ' '.($this->getPackage()->getAuditingEnabled() ? 'Enabled' : 'Disabled');
+ return pht('AUDITING ENABLED STATUS')."\n ".
+ ($this->getPackage()->getAuditingEnabled()
+ ? pht('Enabled')
+ : pht('Disabled'));
}
protected function renderPathsSection() {
$section = array();
- $section[] = 'PATHS';
+ $section[] = pht('PATHS');
foreach ($this->paths as $repository_phid => $paths) {
$section[] = $this->renderRepoSubSection($repository_phid, $paths);
}
return implode("\n", $section);
}
final protected function renderBody() {
$body = array();
$body[] = $this->renderSummarySection();
$body[] = $this->renderDescriptionSection();
$body[] = $this->renderPrimaryOwnerSection();
$body[] = $this->renderOwnersSection();
$body[] = $this->renderAuditingEnabledSection();
$body[] = $this->renderPathsSection();
$body = array_filter($body);
return implode("\n\n", $body)."\n";
}
final public function send() {
$mails = $this->prepareMails();
foreach ($mails as $mail) {
$mail->saveAndSend();
}
}
final public function prepareMails() {
if (!$this->needSend()) {
return array();
}
$this->loadData();
$package = $this->getPackage();
$prefix = PhabricatorEnv::getEnvConfig('metamta.package.subject-prefix');
$verb = $this->getVerb();
$threading = $this->getMailThreading();
list($thread_id, $thread_topic) = $threading;
$template = id(new PhabricatorMetaMTAMail())
->setSubject($this->renderPackageTitle())
->setSubjectPrefix($prefix)
->setVarySubjectPrefix("[{$verb}]")
->setFrom($package->getActorPHID())
->setThreadID($thread_id, $this->isNewThread())
->addHeader('Thread-Topic', $thread_topic)
->setRelatedPHID($package->getPHID())
->setIsBulk(true)
->setBody($this->renderBody());
$reply_handler = $this->newReplyHandler();
$mails = $reply_handler->multiplexMail(
$template,
array_select_keys($this->getHandles(), $this->getMailTo()),
array());
return $mails;
}
private function getMailThreading() {
return array(
'package-'.$this->getPackage()->getPHID(),
'Package '.$this->getPackage()->getOriginalName(),
);
}
private function newReplyHandler() {
$reply_handler = new OwnersPackageReplyHandler();
$reply_handler->setMailReceiver($this->getPackage());
return $reply_handler;
}
}
diff --git a/src/applications/owners/mail/PackageModifyMail.php b/src/applications/owners/mail/PackageModifyMail.php
index 206fece68..cd96fb0c7 100644
--- a/src/applications/owners/mail/PackageModifyMail.php
+++ b/src/applications/owners/mail/PackageModifyMail.php
@@ -1,160 +1,160 @@
<?php
final class PackageModifyMail extends PackageMail {
protected $addOwners;
protected $removeOwners;
protected $allOwners;
protected $touchedRepos;
protected $addPaths;
protected $removePaths;
public function __construct(
PhabricatorOwnersPackage $package,
$add_owners,
$remove_owners,
$all_owners,
$touched_repos,
$add_paths,
$remove_paths) {
$this->package = $package;
$this->addOwners = $add_owners;
$this->removeOwners = $remove_owners;
$this->allOwners = $all_owners;
$this->touchedRepos = $touched_repos;
$this->addPaths = $add_paths;
$this->removePaths = $remove_paths;
}
protected function getVerb() {
- return 'Modified';
+ return pht('Modified');
}
protected function isNewThread() {
return false;
}
protected function needSend() {
$package = $this->getPackage();
if ($package->getOldPrimaryOwnerPHID() !== $package->getPrimaryOwnerPHID()
|| $package->getOldAuditingEnabled() != $package->getAuditingEnabled()
|| $this->addOwners
|| $this->removeOwners
|| $this->addPaths
|| $this->removePaths) {
return true;
} else {
return false;
}
}
protected function loadData() {
$this->mailTo = $this->allOwners;
$phids = array_merge(
$this->allOwners,
$this->touchedRepos,
array(
$this->getPackage()->getActorPHID(),
));
$this->handles = id(new PhabricatorHandleQuery())
->setViewer($this->getActor())
->withPHIDs($phids)
->execute();
}
protected function renderDescriptionSection() {
return null;
}
protected function renderPrimaryOwnerSection() {
$package = $this->getPackage();
$handles = $this->getHandles();
$old_primary_owner_phid = $package->getOldPrimaryOwnerPHID();
$primary_owner_phid = $package->getPrimaryOwnerPHID();
if ($old_primary_owner_phid == $primary_owner_phid) {
return null;
}
$section = array();
- $section[] = 'PRIMARY OWNER CHANGE';
- $section[] = ' Old owner: '.
+ $section[] = pht('PRIMARY OWNER CHANGE');
+ $section[] = ' '.pht('Old owner:').' '.
$handles[$old_primary_owner_phid]->getName();
- $section[] = ' New owner: '.
+ $section[] = ' '.pht('New owner:').' '.
$handles[$primary_owner_phid]->getName();
return implode("\n", $section);
}
protected function renderOwnersSection() {
$section = array();
$add_owners = $this->addOwners;
$remove_owners = $this->removeOwners;
$handles = $this->getHandles();
if ($add_owners) {
$add_owners = array_select_keys($handles, $add_owners);
$add_owners = mpull($add_owners, 'getName');
- $section[] = 'ADDED OWNERS';
+ $section[] = pht('ADDED OWNERS');
$section[] = ' '.implode(', ', $add_owners);
}
if ($remove_owners) {
if ($add_owners) {
$section[] = '';
}
$remove_owners = array_select_keys($handles, $remove_owners);
$remove_owners = mpull($remove_owners, 'getName');
- $section[] = 'REMOVED OWNERS';
+ $section[] = pht('REMOVED OWNERS');
$section[] = ' '.implode(', ', $remove_owners);
}
if ($section) {
return implode("\n", $section);
} else {
return null;
}
}
protected function renderAuditingEnabledSection() {
$package = $this->getPackage();
$old_auditing_enabled = $package->getOldAuditingEnabled();
$auditing_enabled = $package->getAuditingEnabled();
if ($old_auditing_enabled == $auditing_enabled) {
return null;
}
$section = array();
- $section[] = 'AUDITING ENABLED STATUS CHANGE';
- $section[] = ' Old value: '.
- ($old_auditing_enabled ? 'Enabled' : 'Disabled');
- $section[] = ' New value: '.
- ($auditing_enabled ? 'Enabled' : 'Disabled');
+ $section[] = pht('AUDITING ENABLED STATUS CHANGE');
+ $section[] = ' '.pht('Old value:').' '.
+ ($old_auditing_enabled ? pht('Enabled') : pht('Disabled'));
+ $section[] = ' '.pht('New value:').' '.
+ ($auditing_enabled ? pht('Enabled') : pht('Disabled'));
return implode("\n", $section);
}
protected function renderPathsSection() {
$section = array();
if ($this->addPaths) {
- $section[] = 'ADDED PATHS';
+ $section[] = pht('ADDED PATHS');
foreach ($this->addPaths as $repository_phid => $paths) {
$section[] = $this->renderRepoSubSection($repository_phid, $paths);
}
}
if ($this->removePaths) {
if ($this->addPaths) {
$section[] = '';
}
- $section[] = 'REMOVED PATHS';
+ $section[] = pht('REMOVED PATHS');
foreach ($this->removePaths as $repository_phid => $paths) {
$section[] = $this->renderRepoSubSection($repository_phid, $paths);
}
}
return implode("\n", $section);
}
}
diff --git a/src/applications/passphrase/controller/PassphraseCredentialConduitController.php b/src/applications/passphrase/controller/PassphraseCredentialConduitController.php
index cea1aeac6..4c65b5d07 100644
--- a/src/applications/passphrase/controller/PassphraseCredentialConduitController.php
+++ b/src/applications/passphrase/controller/PassphraseCredentialConduitController.php
@@ -1,81 +1,81 @@
<?php
final class PassphraseCredentialConduitController
extends PassphraseController {
private $id;
public function willProcessRequest(array $data) {
$this->id = $data['id'];
}
public function processRequest() {
$request = $this->getRequest();
$viewer = $request->getUser();
$credential = id(new PassphraseCredentialQuery())
->setViewer($viewer)
->withIDs(array($this->id))
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
if (!$credential) {
return new Aphront404Response();
}
$view_uri = '/K'.$credential->getID();
$token = id(new PhabricatorAuthSessionEngine())->requireHighSecuritySession(
$viewer,
$request,
$view_uri);
$type = PassphraseCredentialType::getTypeByConstant(
$credential->getCredentialType());
if (!$type) {
throw new Exception(pht('Credential has invalid type "%s"!', $type));
}
if ($request->isFormPost()) {
$xactions = array();
$xactions[] = id(new PassphraseCredentialTransaction())
->setTransactionType(PassphraseCredentialTransaction::TYPE_CONDUIT)
->setNewValue(!$credential->getAllowConduit());
$editor = id(new PassphraseCredentialTransactionEditor())
->setActor($viewer)
->setContinueOnMissingFields(true)
->setContentSourceFromRequest($request)
->applyTransactions($credential, $xactions);
return id(new AphrontRedirectResponse())->setURI($view_uri);
}
if ($credential->getAllowConduit()) {
return $this->newDialog()
->setTitle(pht('Prevent Conduit access?'))
->appendChild(
pht(
'This credential and its secret will no longer be able '.
- 'to be retrieved using the `passphrase.query` method '.
- 'in Conduit.'))
+ 'to be retrieved using the `%s` method in Conduit.',
+ 'passphrase.query'))
->addSubmitButton(pht('Prevent Conduit Access'))
->addCancelButton($view_uri);
} else {
return $this->newDialog()
->setTitle(pht('Allow Conduit access?'))
->appendChild(
pht(
'This credential will be able to be retrieved via the Conduit '.
- 'API by users who have access to this credential. You should '.
+ 'API by users who have access to this credential. You should '.
'only enable this for credentials which need to be accessed '.
'programmatically (such as from build agents).'))
->addSubmitButton(pht('Allow Conduit Access'))
->addCancelButton($view_uri);
}
}
}
diff --git a/src/applications/passphrase/credentialtype/PassphraseCredentialTypeSSHPrivateKeyText.php b/src/applications/passphrase/credentialtype/PassphraseCredentialTypeSSHPrivateKeyText.php
index dc2b80a85..c197d7eae 100644
--- a/src/applications/passphrase/credentialtype/PassphraseCredentialTypeSSHPrivateKeyText.php
+++ b/src/applications/passphrase/credentialtype/PassphraseCredentialTypeSSHPrivateKeyText.php
@@ -1,66 +1,68 @@
<?php
final class PassphraseCredentialTypeSSHPrivateKeyText
extends PassphraseCredentialTypeSSHPrivateKey {
const CREDENTIAL_TYPE = 'ssh-key-text';
public function getCredentialType() {
return self::CREDENTIAL_TYPE;
}
public function getCredentialTypeName() {
return pht('SSH Private Key');
}
public function getCredentialTypeDescription() {
return pht('Store the plaintext of an SSH private key.');
}
public function getSecretLabel() {
return pht('Private Key');
}
public function shouldShowPasswordField() {
return true;
}
public function getPasswordLabel() {
return pht('Password for Key');
}
public function requiresPassword(PhutilOpaqueEnvelope $secret) {
// According to the internet, this is the canonical test for an SSH private
// key with a password.
return preg_match('/ENCRYPTED/', $secret->openEnvelope());
}
public function decryptSecret(
PhutilOpaqueEnvelope $secret,
PhutilOpaqueEnvelope $password) {
$tmp = new TempFile();
Filesystem::writeFile($tmp, $secret->openEnvelope());
if (!Filesystem::binaryExists('ssh-keygen')) {
throw new Exception(
pht(
- 'Decrypting SSH keys requires the `ssh-keygen` binary, but it '.
- 'is not available in PATH. Either make it available or strip the '.
- 'password fromt his SSH key manually before uploading it.'));
+ 'Decrypting SSH keys requires the `%s` binary, but it '.
+ 'is not available in %s. Either make it available or strip the '.
+ 'password fromt his SSH key manually before uploading it.',
+ 'ssh-keygen',
+ '$PATH'));
}
list($err, $stdout, $stderr) = exec_manual(
'ssh-keygen -p -P %P -N %s -f %s',
$password,
'',
(string)$tmp);
if ($err) {
return null;
} else {
return new PhutilOpaqueEnvelope(Filesystem::readFile($tmp));
}
}
}
diff --git a/src/applications/paste/conduit/PasteCreateConduitAPIMethod.php b/src/applications/paste/conduit/PasteCreateConduitAPIMethod.php
index 0c09109a2..379f5b6e8 100644
--- a/src/applications/paste/conduit/PasteCreateConduitAPIMethod.php
+++ b/src/applications/paste/conduit/PasteCreateConduitAPIMethod.php
@@ -1,76 +1,76 @@
<?php
final class PasteCreateConduitAPIMethod extends PasteConduitAPIMethod {
public function getAPIMethodName() {
return 'paste.create';
}
public function getMethodDescription() {
- return 'Create a new paste.';
+ return pht('Create a new paste.');
}
protected function defineParamTypes() {
return array(
'content' => 'required string',
'title' => 'optional string',
'language' => 'optional string',
);
}
protected function defineReturnType() {
return 'nonempty dict';
}
protected function defineErrorTypes() {
return array(
- 'ERR-NO-PASTE' => 'Paste may not be empty.',
+ 'ERR-NO-PASTE' => pht('Paste may not be empty.'),
);
}
protected function execute(ConduitAPIRequest $request) {
$content = $request->getValue('content');
$title = $request->getValue('title');
$language = $request->getValue('language');
if (!strlen($content)) {
throw new ConduitException('ERR-NO-PASTE');
}
- $title = nonempty($title, 'Masterwork From Distant Lands');
+ $title = nonempty($title, pht('Masterwork From Distant Lands'));
$language = nonempty($language, '');
$viewer = $request->getUser();
$paste = PhabricatorPaste::initializeNewPaste($viewer);
$file = PhabricatorPasteEditor::initializeFileForPaste(
$viewer,
$title,
$content);
$xactions = array();
$xactions[] = id(new PhabricatorPasteTransaction())
->setTransactionType(PhabricatorPasteTransaction::TYPE_CONTENT)
->setNewValue($file->getPHID());
$xactions[] = id(new PhabricatorPasteTransaction())
->setTransactionType(PhabricatorPasteTransaction::TYPE_TITLE)
->setNewValue($title);
$xactions[] = id(new PhabricatorPasteTransaction())
->setTransactionType(PhabricatorPasteTransaction::TYPE_LANGUAGE)
->setNewValue($language);
$editor = id(new PhabricatorPasteEditor())
->setActor($viewer)
->setContentSourceFromConduitRequest($request);
$xactions = $editor->applyTransactions($paste, $xactions);
$paste->attachRawContent($content);
return $this->buildPasteInfoDictionary($paste);
}
}
diff --git a/src/applications/paste/conduit/PasteInfoConduitAPIMethod.php b/src/applications/paste/conduit/PasteInfoConduitAPIMethod.php
index ea39b9c18..e34583e76 100644
--- a/src/applications/paste/conduit/PasteInfoConduitAPIMethod.php
+++ b/src/applications/paste/conduit/PasteInfoConduitAPIMethod.php
@@ -1,50 +1,50 @@
<?php
final class PasteInfoConduitAPIMethod extends PasteConduitAPIMethod {
public function getAPIMethodName() {
return 'paste.info';
}
public function getMethodStatus() {
return self::METHOD_STATUS_DEPRECATED;
}
public function getMethodStatusDescription() {
- return "Replaced by 'paste.query'.";
+ return pht("Replaced by '%s'.", 'paste.query');
}
public function getMethodDescription() {
- return 'Retrieve an array of information about a paste.';
+ return pht('Retrieve an array of information about a paste.');
}
protected function defineParamTypes() {
return array(
'paste_id' => 'required id',
);
}
protected function defineReturnType() {
return 'nonempty dict';
}
protected function defineErrorTypes() {
return array(
- 'ERR_BAD_PASTE' => 'No such paste exists',
+ 'ERR_BAD_PASTE' => pht('No such paste exists.'),
);
}
protected function execute(ConduitAPIRequest $request) {
$paste_id = $request->getValue('paste_id');
$paste = id(new PhabricatorPasteQuery())
->setViewer($request->getUser())
->withIDs(array($paste_id))
->needRawContent(true)
->executeOne();
if (!$paste) {
throw new ConduitException('ERR_BAD_PASTE');
}
return $this->buildPasteInfoDictionary($paste);
}
}
diff --git a/src/applications/paste/conduit/PasteQueryConduitAPIMethod.php b/src/applications/paste/conduit/PasteQueryConduitAPIMethod.php
index b03079245..6f3f87acf 100644
--- a/src/applications/paste/conduit/PasteQueryConduitAPIMethod.php
+++ b/src/applications/paste/conduit/PasteQueryConduitAPIMethod.php
@@ -1,63 +1,63 @@
<?php
final class PasteQueryConduitAPIMethod extends PasteConduitAPIMethod {
public function getAPIMethodName() {
return 'paste.query';
}
public function getMethodDescription() {
- return 'Query Pastes.';
+ return pht('Query Pastes.');
}
protected function defineParamTypes() {
return array(
'ids' => 'optional list<int>',
'phids' => 'optional list<phid>',
'authorPHIDs' => 'optional list<phid>',
'after' => 'optional int',
'limit' => 'optional int, default = 100',
);
}
protected function defineReturnType() {
return 'list<dict>';
}
protected function execute(ConduitAPIRequest $request) {
$query = id(new PhabricatorPasteQuery())
->setViewer($request->getUser())
->needRawContent(true);
if ($request->getValue('ids')) {
$query->withIDs($request->getValue('ids'));
}
if ($request->getValue('phids')) {
$query->withPHIDs($request->getValue('phids'));
}
if ($request->getValue('authorPHIDs')) {
$query->withAuthorPHIDs($request->getValue('authorPHIDs'));
}
if ($request->getValue('after')) {
$query->setAfterID($request->getValue('after'));
}
$limit = $request->getValue('limit', 100);
if ($limit) {
$query->setLimit($limit);
}
$pastes = $query->execute();
$results = array();
foreach ($pastes as $paste) {
$results[$paste->getPHID()] = $this->buildPasteInfoDictionary($paste);
}
return $results;
}
}
diff --git a/src/applications/paste/mail/PasteReplyHandler.php b/src/applications/paste/mail/PasteReplyHandler.php
index b8c498b76..6a58eb64e 100644
--- a/src/applications/paste/mail/PasteReplyHandler.php
+++ b/src/applications/paste/mail/PasteReplyHandler.php
@@ -1,16 +1,17 @@
<?php
final class PasteReplyHandler
extends PhabricatorApplicationTransactionReplyHandler {
public function validateMailReceiver($mail_receiver) {
if (!($mail_receiver instanceof PhabricatorPaste)) {
- throw new Exception('Mail receiver is not a PhabricatorPaste.');
+ throw new Exception(
+ pht('Mail receiver is not a %s.', 'PhabricatorPaste'));
}
}
public function getObjectPrefix() {
return 'P';
}
}
diff --git a/src/applications/people/conduit/UserDisableConduitAPIMethod.php b/src/applications/people/conduit/UserDisableConduitAPIMethod.php
index 6d9850a8a..3512d3f74 100644
--- a/src/applications/people/conduit/UserDisableConduitAPIMethod.php
+++ b/src/applications/people/conduit/UserDisableConduitAPIMethod.php
@@ -1,53 +1,53 @@
<?php
final class UserDisableConduitAPIMethod extends UserConduitAPIMethod {
public function getAPIMethodName() {
return 'user.disable';
}
public function getMethodDescription() {
- return 'Permanently disable specified users (admin only).';
+ return pht('Permanently disable specified users (admin only).');
}
protected function defineParamTypes() {
return array(
'phids' => 'required list<phid>',
);
}
protected function defineReturnType() {
return 'void';
}
protected function defineErrorTypes() {
return array(
- 'ERR-PERMISSIONS' => 'Only admins can call this method.',
- 'ERR-BAD-PHID' => 'Non existent user PHID.',
+ 'ERR-PERMISSIONS' => pht('Only admins can call this method.'),
+ 'ERR-BAD-PHID' => pht('Non existent user PHID.'),
);
}
protected function execute(ConduitAPIRequest $request) {
$actor = $request->getUser();
if (!$actor->getIsAdmin()) {
throw new ConduitException('ERR-PERMISSIONS');
}
$phids = $request->getValue('phids');
$users = id(new PhabricatorUser())->loadAllWhere(
'phid IN (%Ls)',
$phids);
if (count($phids) != count($users)) {
throw new ConduitException('ERR-BAD-PHID');
}
foreach ($users as $user) {
id(new PhabricatorUserEditor())
->setActor($actor)
->disableUser($user, true);
}
}
}
diff --git a/src/applications/people/conduit/UserEnableConduitAPIMethod.php b/src/applications/people/conduit/UserEnableConduitAPIMethod.php
index e29ce3ca6..f7cb117df 100644
--- a/src/applications/people/conduit/UserEnableConduitAPIMethod.php
+++ b/src/applications/people/conduit/UserEnableConduitAPIMethod.php
@@ -1,53 +1,53 @@
<?php
final class UserEnableConduitAPIMethod extends UserConduitAPIMethod {
public function getAPIMethodName() {
return 'user.enable';
}
public function getMethodDescription() {
- return 'Re-enable specified users (admin only).';
+ return pht('Re-enable specified users (admin only).');
}
protected function defineParamTypes() {
return array(
'phids' => 'required list<phid>',
);
}
protected function defineReturnType() {
return 'void';
}
protected function defineErrorTypes() {
return array(
- 'ERR-PERMISSIONS' => 'Only admins can call this method.',
- 'ERR-BAD-PHID' => 'Non existent user PHID.',
+ 'ERR-PERMISSIONS' => pht('Only admins can call this method.'),
+ 'ERR-BAD-PHID' => pht('Non existent user PHID.'),
);
}
protected function execute(ConduitAPIRequest $request) {
$actor = $request->getUser();
if (!$actor->getIsAdmin()) {
throw new ConduitException('ERR-PERMISSIONS');
}
$phids = $request->getValue('phids');
$users = id(new PhabricatorUser())->loadAllWhere(
'phid IN (%Ls)',
$phids);
if (count($phids) != count($users)) {
throw new ConduitException('ERR-BAD-PHID');
}
foreach ($users as $user) {
id(new PhabricatorUserEditor())
->setActor($actor)
->disableUser($user, false);
}
}
}
diff --git a/src/applications/people/conduit/UserFindConduitAPIMethod.php b/src/applications/people/conduit/UserFindConduitAPIMethod.php
index 06f124d3a..2390abad0 100644
--- a/src/applications/people/conduit/UserFindConduitAPIMethod.php
+++ b/src/applications/people/conduit/UserFindConduitAPIMethod.php
@@ -1,40 +1,40 @@
<?php
final class UserFindConduitAPIMethod extends UserConduitAPIMethod {
public function getAPIMethodName() {
return 'user.find';
}
public function getMethodStatus() {
return self::METHOD_STATUS_DEPRECATED;
}
public function getMethodStatusDescription() {
- return pht('Obsoleted by "user.query".');
+ return pht('Obsoleted by "%s".', 'user.query');
}
public function getMethodDescription() {
- return pht('Lookup PHIDs by username. Obsoleted by "user.query".');
+ return pht('Lookup PHIDs by username. Obsoleted by "%s".', 'user.query');
}
protected function defineParamTypes() {
return array(
'aliases' => 'required list<string>',
);
}
protected function defineReturnType() {
return 'nonempty dict<string, phid>';
}
protected function execute(ConduitAPIRequest $request) {
$users = id(new PhabricatorPeopleQuery())
->setViewer($request->getUser())
->withUsernames($request->getValue('aliases', array()))
->execute();
return mpull($users, 'getPHID', 'getUsername');
}
}
diff --git a/src/applications/people/conduit/UserQueryConduitAPIMethod.php b/src/applications/people/conduit/UserQueryConduitAPIMethod.php
index 4a567a015..c42414a6b 100644
--- a/src/applications/people/conduit/UserQueryConduitAPIMethod.php
+++ b/src/applications/people/conduit/UserQueryConduitAPIMethod.php
@@ -1,82 +1,82 @@
<?php
final class UserQueryConduitAPIMethod extends UserConduitAPIMethod {
public function getAPIMethodName() {
return 'user.query';
}
public function getMethodDescription() {
- return 'Query users.';
+ return pht('Query users.');
}
protected function defineParamTypes() {
return array(
'usernames' => 'optional list<string>',
'emails' => 'optional list<string>',
'realnames' => 'optional list<string>',
'phids' => 'optional list<phid>',
'ids' => 'optional list<uint>',
'offset' => 'optional int',
'limit' => 'optional int (default = 100)',
);
}
protected function defineReturnType() {
return 'list<dict>';
}
protected function defineErrorTypes() {
return array(
- 'ERR-INVALID-PARAMETER' => 'Missing or malformed parameter.',
+ 'ERR-INVALID-PARAMETER' => pht('Missing or malformed parameter.'),
);
}
protected function execute(ConduitAPIRequest $request) {
$usernames = $request->getValue('usernames', array());
$emails = $request->getValue('emails', array());
$realnames = $request->getValue('realnames', array());
$phids = $request->getValue('phids', array());
$ids = $request->getValue('ids', array());
$offset = $request->getValue('offset', 0);
$limit = $request->getValue('limit', 100);
$query = id(new PhabricatorPeopleQuery())
->setViewer($request->getUser())
->needProfileImage(true)
->needAvailability(true);
if ($usernames) {
$query->withUsernames($usernames);
}
if ($emails) {
$query->withEmails($emails);
}
if ($realnames) {
$query->withRealnames($realnames);
}
if ($phids) {
$query->withPHIDs($phids);
}
if ($ids) {
$query->withIDs($ids);
}
if ($limit) {
$query->setLimit($limit);
}
if ($offset) {
$query->setOffset($offset);
}
$users = $query->execute();
$results = array();
foreach ($users as $user) {
$results[] = $this->buildUserInformationDictionary(
$user,
$with_email = false,
$with_availability = true);
}
return $results;
}
}
diff --git a/src/applications/people/conduit/UserWhoAmIConduitAPIMethod.php b/src/applications/people/conduit/UserWhoAmIConduitAPIMethod.php
index b646246c5..13884e1e8 100644
--- a/src/applications/people/conduit/UserWhoAmIConduitAPIMethod.php
+++ b/src/applications/people/conduit/UserWhoAmIConduitAPIMethod.php
@@ -1,38 +1,38 @@
<?php
final class UserWhoAmIConduitAPIMethod extends UserConduitAPIMethod {
public function getAPIMethodName() {
return 'user.whoami';
}
public function getMethodDescription() {
- return 'Retrieve information about the logged-in user.';
+ return pht('Retrieve information about the logged-in user.');
}
protected function defineParamTypes() {
return array();
}
protected function defineReturnType() {
return 'nonempty dict<string, wild>';
}
public function getRequiredScope() {
return PhabricatorOAuthServerScope::SCOPE_WHOAMI;
}
protected function execute(ConduitAPIRequest $request) {
$person = id(new PhabricatorPeopleQuery())
->setViewer($request->getUser())
->needProfileImage(true)
->withPHIDs(array($request->getUser()->getPHID()))
->executeOne();
return $this->buildUserInformationDictionary(
$person,
$with_email = true,
$with_availability = false);
}
}
diff --git a/src/applications/people/controller/PhabricatorPeopleApproveController.php b/src/applications/people/controller/PhabricatorPeopleApproveController.php
index bbf69f23f..a906b1565 100644
--- a/src/applications/people/controller/PhabricatorPeopleApproveController.php
+++ b/src/applications/people/controller/PhabricatorPeopleApproveController.php
@@ -1,66 +1,68 @@
<?php
final class PhabricatorPeopleApproveController
extends PhabricatorPeopleController {
private $id;
public function willProcessRequest(array $data) {
$this->id = idx($data, 'id');
}
public function processRequest() {
$request = $this->getRequest();
$admin = $request->getUser();
$user = id(new PhabricatorPeopleQuery())
->setViewer($admin)
->withIDs(array($this->id))
->executeOne();
if (!$user) {
return new Aphront404Response();
}
$done_uri = $this->getApplicationURI('query/approval/');
if ($request->isFormPost()) {
id(new PhabricatorUserEditor())
->setActor($admin)
->approveUser($user, true);
$title = pht(
'Phabricator Account "%s" Approved',
$user->getUsername());
- $body = pht(
- "Your Phabricator account (%s) has been approved by %s. You can ".
- "login here:\n\n %s\n\n",
- $user->getUsername(),
- $admin->getUsername(),
+ $body = sprintf(
+ "%s\n\n %s\n\n",
+ pht(
+ 'Your Phabricator account (%s) has been approved by %s. You can '.
+ 'login here:',
+ $user->getUsername(),
+ $admin->getUsername()),
PhabricatorEnv::getProductionURI('/'));
$mail = id(new PhabricatorMetaMTAMail())
->addTos(array($user->getPHID()))
->addCCs(array($admin->getPHID()))
->setSubject('[Phabricator] '.$title)
->setForceDelivery(true)
->setBody($body)
->saveAndSend();
return id(new AphrontRedirectResponse())->setURI($done_uri);
}
$dialog = id(new AphrontDialogView())
->setUser($admin)
->setTitle(pht('Confirm Approval'))
->appendChild(
pht(
'Allow %s to access this Phabricator install?',
phutil_tag('strong', array(), $user->getUsername())))
->addCancelButton($done_uri)
->addSubmitButton(pht('Approve Account'));
return id(new AphrontDialogResponse())->setDialog($dialog);
}
}
diff --git a/src/applications/people/controller/PhabricatorPeopleDeleteController.php b/src/applications/people/controller/PhabricatorPeopleDeleteController.php
index fcd819e06..e95dd4c64 100644
--- a/src/applications/people/controller/PhabricatorPeopleDeleteController.php
+++ b/src/applications/people/controller/PhabricatorPeopleDeleteController.php
@@ -1,83 +1,80 @@
<?php
final class PhabricatorPeopleDeleteController
extends PhabricatorPeopleController {
private $id;
public function willProcessRequest(array $data) {
$this->id = $data['id'];
}
public function processRequest() {
$request = $this->getRequest();
$admin = $request->getUser();
$user = id(new PhabricatorPeopleQuery())
->setViewer($admin)
->withIDs(array($this->id))
->executeOne();
if (!$user) {
return new Aphront404Response();
}
$profile_uri = '/p/'.$user->getUsername().'/';
if ($user->getPHID() == $admin->getPHID()) {
return $this->buildDeleteSelfResponse($profile_uri);
}
$str1 = pht(
'Be careful when deleting users! This will permanently and '.
'irreversibly destroy this user account.');
$str2 = pht(
'If this user interacted with anything, it is generally better to '.
'disable them, not delete them. If you delete them, it will no longer '.
'be possible to (for example) search for objects they created, and you '.
'will lose other information about their history. Disabling them '.
'instead will prevent them from logging in, but will not destroy any of '.
'their data.');
$str3 = pht(
'It is generally safe to delete newly created users (and test users and '.
'so on), but less safe to delete established users. If possible, '.
'disable them instead.');
- $str4 = pht(
- 'To permanently destroy this user, run this command:');
+ $str4 = pht('To permanently destroy this user, run this command:');
$form = id(new AphrontFormView())
->setUser($admin)
->appendRemarkupInstructions(
pht(
" phabricator/ $ ./bin/remove destroy %s\n",
csprintf('%R', '@'.$user->getUsername())));
return $this->newDialog()
->setWidth(AphrontDialogView::WIDTH_FORM)
->setTitle(pht('Permanently Delete User'))
->setShortTitle(pht('Delete User'))
->appendParagraph($str1)
->appendParagraph($str2)
->appendParagraph($str3)
->appendParagraph($str4)
->appendChild($form->buildLayoutView())
->addCancelButton($profile_uri, pht('Close'));
}
private function buildDeleteSelfResponse($profile_uri) {
return $this->newDialog()
->setTitle(pht('You Shall Journey No Farther'))
->appendParagraph(
pht(
'As you stare into the gaping maw of the abyss, something '.
'holds you back.'))
- ->appendParagraph(
- pht(
- 'You can not delete your own account.'))
+ ->appendParagraph(pht('You can not delete your own account.'))
->addCancelButton($profile_uri, pht('Turn Back'));
}
}
diff --git a/src/applications/people/controller/PhabricatorPeopleDisableController.php b/src/applications/people/controller/PhabricatorPeopleDisableController.php
index a1715dffd..7ef409970 100644
--- a/src/applications/people/controller/PhabricatorPeopleDisableController.php
+++ b/src/applications/people/controller/PhabricatorPeopleDisableController.php
@@ -1,87 +1,86 @@
<?php
final class PhabricatorPeopleDisableController
extends PhabricatorPeopleController {
private $id;
private $via;
public function willProcessRequest(array $data) {
$this->id = $data['id'];
$this->via = $data['via'];
}
public function processRequest() {
$request = $this->getRequest();
$admin = $request->getUser();
$user = id(new PhabricatorPeopleQuery())
->setViewer($admin)
->withIDs(array($this->id))
->executeOne();
if (!$user) {
return new Aphront404Response();
}
// NOTE: We reach this controller via the administrative "Disable User"
// on profiles and also via the "X" action on the approval queue. We do
// things slightly differently depending on the context the actor is in.
$is_disapprove = ($this->via == 'disapprove');
if ($is_disapprove) {
$done_uri = $this->getApplicationURI('query/approval/');
$should_disable = true;
} else {
$done_uri = '/p/'.$user->getUsername().'/';
$should_disable = !$user->getIsDisabled();
}
if ($admin->getPHID() == $user->getPHID()) {
return $this->newDialog()
->setTitle(pht('Something Stays Your Hand'))
->appendParagraph(
pht(
- 'Try as you might, you find you can not disable your '.
- 'own account.'))
+ 'Try as you might, you find you can not disable your own account.'))
->addCancelButton($done_uri, pht('Curses!'));
}
if ($request->isFormPost()) {
id(new PhabricatorUserEditor())
->setActor($admin)
->disableUser($user, $should_disable);
return id(new AphrontRedirectResponse())->setURI($done_uri);
}
if ($should_disable) {
$title = pht('Disable User?');
$short_title = pht('Disable User');
$body = pht(
'Disable %s? They will no longer be able to access Phabricator or '.
'receive email.',
phutil_tag('strong', array(), $user->getUsername()));
$submit = pht('Disable User');
} else {
$title = pht('Enable User?');
$short_title = pht('Enable User');
$body = pht(
'Enable %s? They will be able to access Phabricator and receive '.
'email again.',
phutil_tag('strong', array(), $user->getUsername()));
$submit = pht('Enable User');
}
return $this->newDialog()
->setTitle($title)
->setShortTitle($short_title)
->appendParagraph($body)
->addCancelButton($done_uri)
->addSubmitButton($submit);
}
}
diff --git a/src/applications/people/controller/PhabricatorPeopleFeedController.php b/src/applications/people/controller/PhabricatorPeopleFeedController.php
index 5907ae4a1..64a443cab 100644
--- a/src/applications/people/controller/PhabricatorPeopleFeedController.php
+++ b/src/applications/people/controller/PhabricatorPeopleFeedController.php
@@ -1,60 +1,61 @@
<?php
final class PhabricatorPeopleFeedController
extends PhabricatorPeopleController {
private $username;
public function shouldRequireAdmin() {
return false;
}
public function willProcessRequest(array $data) {
$this->username = idx($data, 'username');
}
public function processRequest() {
require_celerity_resource('phabricator-profile-css');
$viewer = $this->getRequest()->getUser();
$user = id(new PhabricatorPeopleQuery())
->setViewer($viewer)
->withUsernames(array($this->username))
->needProfileImage(true)
->executeOne();
if (!$user) {
return new Aphront404Response();
}
$query = new PhabricatorFeedQuery();
$query->setFilterPHIDs(
array(
$user->getPHID(),
));
$query->setLimit(100);
$query->setViewer($viewer);
$stories = $query->execute();
$builder = new PhabricatorFeedBuilder($stories);
$builder->setUser($viewer);
$builder->setShowHovercards(true);
- $builder->setNoDataString(pht('To begin on such a grand journey, '.
- 'requires but just a single step.'));
+ $builder->setNoDataString(
+ pht(
+ 'To begin on such a grand journey, requires but just a single step.'));
$view = $builder->buildView();
$feed = phutil_tag_div(
'phabricator-project-feed',
$view->render());
$name = $user->getUsername();
$nav = $this->buildIconNavView($user);
$nav->selectFilter("{$name}/feed/");
$nav->appendChild($feed);
return $this->buildApplicationPage(
$nav,
array(
'title' => pht('Feed'),
));
}
}
diff --git a/src/applications/people/controller/PhabricatorPeopleLdapController.php b/src/applications/people/controller/PhabricatorPeopleLdapController.php
index 22cb29be0..6f5fa09f3 100644
--- a/src/applications/people/controller/PhabricatorPeopleLdapController.php
+++ b/src/applications/people/controller/PhabricatorPeopleLdapController.php
@@ -1,216 +1,216 @@
<?php
final class PhabricatorPeopleLdapController
extends PhabricatorPeopleController {
public function handleRequest(AphrontRequest $request) {
$this->requireApplicationCapability(
PeopleCreateUsersCapability::CAPABILITY);
$admin = $request->getUser();
$content = array();
$form = id(new AphrontFormView())
->setAction($request->getRequestURI()
->alter('search', 'true')->alter('import', null))
->setUser($admin)
->appendChild(
id(new AphrontFormTextControl())
->setLabel(pht('LDAP username'))
->setName('username'))
->appendChild(
id(new AphrontFormPasswordControl())
->setDisableAutocomplete(true)
->setLabel(pht('Password'))
->setName('password'))
->appendChild(
id(new AphrontFormTextControl())
->setLabel(pht('LDAP query'))
- ->setCaption(pht('A filter such as (objectClass=*)'))
+ ->setCaption(pht('A filter such as %s.', '(objectClass=*)'))
->setName('query'))
->appendChild(
id(new AphrontFormSubmitControl())
->setValue(pht('Search')));
$panel = id(new PHUIObjectBoxView())
->setHeaderText(pht('Import LDAP Users'))
->setForm($form);
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb(
pht('Import Ldap Users'),
$this->getApplicationURI('/ldap/'));
$nav = $this->buildSideNavView();
$nav->setCrumbs($crumbs);
$nav->selectFilter('ldap');
$nav->appendChild($content);
if ($request->getStr('import')) {
$nav->appendChild($this->processImportRequest($request));
}
$nav->appendChild($panel);
if ($request->getStr('search')) {
$nav->appendChild($this->processSearchRequest($request));
}
return $this->buildApplicationPage(
$nav,
array(
'title' => pht('Import Ldap Users'),
));
}
private function processImportRequest($request) {
$admin = $request->getUser();
$usernames = $request->getArr('usernames');
$emails = $request->getArr('email');
$names = $request->getArr('name');
$notice_view = new PHUIInfoView();
$notice_view->setSeverity(PHUIInfoView::SEVERITY_NOTICE);
$notice_view->setTitle(pht('Import Successful'));
$notice_view->setErrors(array(
pht('Successfully imported users from LDAP'),
));
$list = new PHUIObjectItemListView();
$list->setNoDataString(pht('No users imported?'));
foreach ($usernames as $username) {
$user = new PhabricatorUser();
$user->setUsername($username);
$user->setRealname($names[$username]);
$email_obj = id(new PhabricatorUserEmail())
->setAddress($emails[$username])
->setIsVerified(1);
try {
id(new PhabricatorUserEditor())
->setActor($admin)
->createNewUser($user, $email_obj);
id(new PhabricatorExternalAccount())
->setUserPHID($user->getPHID())
->setAccountType('ldap')
->setAccountDomain('self')
->setAccountID($username)
->save();
$header = pht('Successfully added %s', $username);
$attribute = null;
$color = 'green';
} catch (Exception $ex) {
$header = pht('Failed to add %s', $username);
$attribute = $ex->getMessage();
$color = 'red';
}
$item = id(new PHUIObjectItemView())
->setHeader($header)
->addAttribute($attribute)
->setBarColor($color);
$list->addItem($item);
}
return array(
$notice_view,
$list,
);
}
private function processSearchRequest($request) {
$panel = new PHUIBoxView();
$admin = $request->getUser();
$search = $request->getStr('query');
$ldap_provider = PhabricatorLDAPAuthProvider::getLDAPProvider();
if (!$ldap_provider) {
- throw new Exception('No LDAP provider enabled!');
+ throw new Exception(pht('No LDAP provider enabled!'));
}
$ldap_adapter = $ldap_provider->getAdapter();
$ldap_adapter->setLoginUsername($request->getStr('username'));
$ldap_adapter->setLoginPassword(
new PhutilOpaqueEnvelope($request->getStr('password')));
// This causes us to connect and bind.
// TODO: Clean up this discard mode stuff.
DarkConsoleErrorLogPluginAPI::enableDiscardMode();
$ldap_adapter->getAccountID();
DarkConsoleErrorLogPluginAPI::disableDiscardMode();
$results = $ldap_adapter->searchLDAP('%Q', $search);
foreach ($results as $key => $record) {
$account_id = $ldap_adapter->readLDAPRecordAccountID($record);
if (!$account_id) {
unset($results[$key]);
continue;
}
$info = array(
$account_id,
$ldap_adapter->readLDAPRecordEmail($record),
$ldap_adapter->readLDAPRecordRealName($record),
);
$results[$key] = $info;
$results[$key][] = $this->renderUserInputs($info);
}
$form = id(new AphrontFormView())
->setUser($admin);
$table = new AphrontTableView($results);
$table->setHeaders(
array(
pht('Username'),
pht('Email'),
pht('Real Name'),
pht('Import?'),
));
$form->appendChild($table);
$form->setAction($request->getRequestURI()
->alter('import', 'true')->alter('search', null))
->appendChild(
id(new AphrontFormSubmitControl())
->setValue(pht('Import')));
$panel->appendChild($form);
return $panel;
}
private function renderUserInputs($user) {
$username = $user[0];
return hsprintf(
'%s%s%s',
phutil_tag(
'input',
array(
'type' => 'checkbox',
'name' => 'usernames[]',
'value' => $username,
)),
phutil_tag(
'input',
array(
'type' => 'hidden',
'name' => "email[$username]",
'value' => $user[1],
)),
phutil_tag(
'input',
array(
'type' => 'hidden',
'name' => "name[$username]",
'value' => $user[2],
)));
}
}
diff --git a/src/applications/people/controller/PhabricatorPeopleNewController.php b/src/applications/people/controller/PhabricatorPeopleNewController.php
index 21dce0419..93f9c42fa 100644
--- a/src/applications/people/controller/PhabricatorPeopleNewController.php
+++ b/src/applications/people/controller/PhabricatorPeopleNewController.php
@@ -1,218 +1,216 @@
<?php
final class PhabricatorPeopleNewController
extends PhabricatorPeopleController {
public function handleRequest(AphrontRequest $request) {
$type = $request->getURIData('type');
$admin = $request->getUser();
switch ($type) {
case 'standard':
$this->requireApplicationCapability(
PeopleCreateUsersCapability::CAPABILITY);
$is_bot = false;
break;
case 'bot':
$is_bot = true;
break;
default:
return new Aphront404Response();
}
$user = new PhabricatorUser();
$require_real_name = PhabricatorEnv::getEnvConfig('user.require-real-name');
$e_username = true;
$e_realname = $require_real_name ? true : null;
$e_email = true;
$errors = array();
$welcome_checked = true;
$new_email = null;
if ($request->isFormPost()) {
$welcome_checked = $request->getInt('welcome');
$user->setUsername($request->getStr('username'));
$new_email = $request->getStr('email');
if (!strlen($new_email)) {
$errors[] = pht('Email is required.');
$e_email = pht('Required');
} else if (!PhabricatorUserEmail::isAllowedAddress($new_email)) {
$e_email = pht('Invalid');
$errors[] = PhabricatorUserEmail::describeAllowedAddresses();
} else {
$e_email = null;
}
$user->setRealName($request->getStr('realname'));
if (!strlen($user->getUsername())) {
$errors[] = pht('Username is required.');
$e_username = pht('Required');
} else if (!PhabricatorUser::validateUsername($user->getUsername())) {
$errors[] = PhabricatorUser::describeValidUsername();
$e_username = pht('Invalid');
} else {
$e_username = null;
}
if (!strlen($user->getRealName()) && $require_real_name) {
$errors[] = pht('Real name is required.');
$e_realname = pht('Required');
} else {
$e_realname = null;
}
if (!$errors) {
try {
$email = id(new PhabricatorUserEmail())
->setAddress($new_email)
->setIsVerified(0);
// Automatically approve the user, since an admin is creating them.
$user->setIsApproved(1);
// If the user is a bot, approve their email too.
if ($is_bot) {
$email->setIsVerified(1);
}
id(new PhabricatorUserEditor())
->setActor($admin)
->createNewUser($user, $email);
if ($is_bot) {
id(new PhabricatorUserEditor())
->setActor($admin)
->makeSystemAgentUser($user, true);
}
if ($welcome_checked && !$is_bot) {
$user->sendWelcomeEmail($admin);
}
$response = id(new AphrontRedirectResponse())
->setURI('/p/'.$user->getUsername().'/');
return $response;
} catch (AphrontDuplicateKeyQueryException $ex) {
$errors[] = pht('Username and email must be unique.');
$same_username = id(new PhabricatorUser())
->loadOneWhere('username = %s', $user->getUsername());
$same_email = id(new PhabricatorUserEmail())
->loadOneWhere('address = %s', $new_email);
if ($same_username) {
$e_username = pht('Duplicate');
}
if ($same_email) {
$e_email = pht('Duplicate');
}
}
}
}
$form = id(new AphrontFormView())
->setUser($admin);
if ($is_bot) {
$form->appendRemarkupInstructions(
- pht(
- 'You are creating a new **bot/script** user account.'));
+ pht('You are creating a new **bot/script** user account.'));
} else {
$form->appendRemarkupInstructions(
- pht(
- 'You are creating a new **standard** user account.'));
+ pht('You are creating a new **standard** user account.'));
}
$form
->appendChild(
id(new AphrontFormTextControl())
->setLabel(pht('Username'))
->setName('username')
->setValue($user->getUsername())
->setError($e_username))
->appendChild(
id(new AphrontFormTextControl())
->setLabel(pht('Real Name'))
->setName('realname')
->setValue($user->getRealName())
->setError($e_realname))
->appendChild(
id(new AphrontFormTextControl())
->setLabel(pht('Email'))
->setName('email')
->setValue($new_email)
->setCaption(PhabricatorUserEmail::describeAllowedAddresses())
->setError($e_email));
if (!$is_bot) {
$form->appendChild(
id(new AphrontFormCheckboxControl())
->addCheckbox(
'welcome',
1,
pht('Send "Welcome to Phabricator" email with login instructions.'),
$welcome_checked));
}
$form
->appendChild(
id(new AphrontFormSubmitControl())
->addCancelButton($this->getApplicationURI())
->setValue(pht('Create User')));
if ($is_bot) {
$form
->appendChild(id(new AphrontFormDividerControl()))
->appendRemarkupInstructions(
pht(
'**Why do bot/script accounts need an email address?**'.
"\n\n".
'Although bots do not normally receive email from Phabricator, '.
'they can interact with other systems which require an email '.
'address. Examples include:'.
"\n\n".
" - If the account takes actions which //send// email, we need ".
" an address to use in the //From// header.\n".
" - If the account creates commits, Git and Mercurial require ".
" an email address for authorship.\n".
" - If you send email //to// Phabricator on behalf of the ".
" account, the address can identify the sender.\n".
" - Some internal authentication functions depend on accounts ".
" having an email address.\n".
"\n\n".
"The address will automatically be verified, so you do not need ".
"to be able to receive mail at this address, and can enter some ".
"invalid or nonexistent (but correctly formatted) address like ".
"`bot@yourcompany.com` if you prefer."));
}
$title = pht('Create New User');
$form_box = id(new PHUIObjectBoxView())
->setHeaderText($title)
->setFormErrors($errors)
->setForm($form);
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb($title);
return $this->buildApplicationPage(
array(
$crumbs,
$form_box,
),
array(
'title' => $title,
));
}
}
diff --git a/src/applications/people/controller/PhabricatorPeopleWelcomeController.php b/src/applications/people/controller/PhabricatorPeopleWelcomeController.php
index 7b0131a4b..b3762e67c 100644
--- a/src/applications/people/controller/PhabricatorPeopleWelcomeController.php
+++ b/src/applications/people/controller/PhabricatorPeopleWelcomeController.php
@@ -1,50 +1,48 @@
<?php
final class PhabricatorPeopleWelcomeController
extends PhabricatorPeopleController {
private $id;
public function willProcessRequest(array $data) {
$this->id = $data['id'];
}
public function processRequest() {
$request = $this->getRequest();
$admin = $request->getUser();
$user = id(new PhabricatorPeopleQuery())
->setViewer($admin)
->withIDs(array($this->id))
->executeOne();
if (!$user) {
return new Aphront404Response();
}
$profile_uri = '/p/'.$user->getUsername().'/';
if ($request->isFormPost()) {
$user->sendWelcomeEmail($admin);
return id(new AphrontRedirectResponse())->setURI($profile_uri);
}
return $this->newDialog()
->setTitle(pht('Send Welcome Email'))
->appendParagraph(
pht(
'This will send the user another copy of the "Welcome to '.
'Phabricator" email that users normally receive when their '.
'accounts are created.'))
->appendParagraph(
pht(
'The email contains a link to log in to their account. Sending '.
'another copy of the email can be useful if the original was lost '.
'or never sent.'))
- ->appendParagraph(
- pht(
- 'The email will identify you as the sender.'))
+ ->appendParagraph(pht('The email will identify you as the sender.'))
->addSubmitButton(pht('Send Email'))
->addCancelButton($profile_uri);
}
}
diff --git a/src/applications/people/editor/PhabricatorUserEditor.php b/src/applications/people/editor/PhabricatorUserEditor.php
index 3a3ccdc35..cc34696f2 100644
--- a/src/applications/people/editor/PhabricatorUserEditor.php
+++ b/src/applications/people/editor/PhabricatorUserEditor.php
@@ -1,699 +1,697 @@
<?php
/**
* Editor class for creating and adjusting users. This class guarantees data
* integrity and writes logs when user information changes.
*
* @task config Configuration
* @task edit Creating and Editing Users
* @task role Editing Roles
* @task email Adding, Removing and Changing Email
* @task internal Internals
*/
final class PhabricatorUserEditor extends PhabricatorEditor {
private $logs = array();
/* -( Creating and Editing Users )----------------------------------------- */
/**
* @task edit
*/
public function createNewUser(
PhabricatorUser $user,
PhabricatorUserEmail $email,
$allow_reassign = false) {
if ($user->getID()) {
- throw new Exception('User has already been created!');
+ throw new Exception(pht('User has already been created!'));
}
$is_reassign = false;
if ($email->getID()) {
if ($allow_reassign) {
if ($email->getIsPrimary()) {
throw new Exception(
- pht(
- 'Primary email addresses can not be reassigned.'));
+ pht('Primary email addresses can not be reassigned.'));
}
$is_reassign = true;
} else {
- throw new Exception('Email has already been created!');
+ throw new Exception(pht('Email has already been created!'));
}
}
if (!PhabricatorUser::validateUsername($user->getUsername())) {
$valid = PhabricatorUser::describeValidUsername();
- throw new Exception("Username is invalid! {$valid}");
+ throw new Exception(pht('Username is invalid! %s', $valid));
}
// Always set a new user's email address to primary.
$email->setIsPrimary(1);
// If the primary address is already verified, also set the verified flag
// on the user themselves.
if ($email->getIsVerified()) {
$user->setIsEmailVerified(1);
}
$this->willAddEmail($email);
$user->openTransaction();
try {
$user->save();
$email->setUserPHID($user->getPHID());
$email->save();
} catch (AphrontDuplicateKeyQueryException $ex) {
// We might have written the user but failed to write the email; if
// so, erase the IDs we attached.
$user->setID(null);
$user->setPHID(null);
$user->killTransaction();
throw $ex;
}
$log = PhabricatorUserLog::initializeNewLog(
$this->requireActor(),
$user->getPHID(),
PhabricatorUserLog::ACTION_CREATE);
$log->setNewValue($email->getAddress());
$log->save();
if ($is_reassign) {
$log = PhabricatorUserLog::initializeNewLog(
$this->requireActor(),
$user->getPHID(),
PhabricatorUserLog::ACTION_EMAIL_REASSIGN);
$log->setNewValue($email->getAddress());
$log->save();
}
$user->saveTransaction();
if ($email->getIsVerified()) {
$this->didVerifyEmail($user, $email);
}
return $this;
}
/**
* @task edit
*/
public function updateUser(
PhabricatorUser $user,
PhabricatorUserEmail $email = null) {
+
if (!$user->getID()) {
- throw new Exception('User has not been created yet!');
+ throw new Exception(pht('User has not been created yet!'));
}
$user->openTransaction();
$user->save();
if ($email) {
$email->save();
}
$log = PhabricatorUserLog::initializeNewLog(
$this->requireActor(),
$user->getPHID(),
PhabricatorUserLog::ACTION_EDIT);
$log->save();
$user->saveTransaction();
return $this;
}
/**
* @task edit
*/
public function changePassword(
PhabricatorUser $user,
PhutilOpaqueEnvelope $envelope) {
if (!$user->getID()) {
- throw new Exception('User has not been created yet!');
+ throw new Exception(pht('User has not been created yet!'));
}
$user->openTransaction();
$user->reload();
$user->setPassword($envelope);
$user->save();
$log = PhabricatorUserLog::initializeNewLog(
$this->requireActor(),
$user->getPHID(),
PhabricatorUserLog::ACTION_CHANGE_PASSWORD);
$log->save();
$user->saveTransaction();
}
/**
* @task edit
*/
public function changeUsername(PhabricatorUser $user, $username) {
$actor = $this->requireActor();
if (!$user->getID()) {
- throw new Exception('User has not been created yet!');
+ throw new Exception(pht('User has not been created yet!'));
}
if (!PhabricatorUser::validateUsername($username)) {
$valid = PhabricatorUser::describeValidUsername();
- throw new Exception("Username is invalid! {$valid}");
+ throw new Exception(pht('Username is invalid! %s', $valid));
}
$old_username = $user->getUsername();
$user->openTransaction();
$user->reload();
$user->setUsername($username);
try {
$user->save();
} catch (AphrontDuplicateKeyQueryException $ex) {
$user->setUsername($old_username);
$user->killTransaction();
throw $ex;
}
$log = PhabricatorUserLog::initializeNewLog(
$actor,
$user->getPHID(),
PhabricatorUserLog::ACTION_CHANGE_USERNAME);
$log->setOldValue($old_username);
$log->setNewValue($username);
$log->save();
$user->saveTransaction();
$user->sendUsernameChangeEmail($actor, $old_username);
}
/* -( Editing Roles )------------------------------------------------------ */
/**
* @task role
*/
public function makeAdminUser(PhabricatorUser $user, $admin) {
$actor = $this->requireActor();
if (!$user->getID()) {
- throw new Exception('User has not been created yet!');
+ throw new Exception(pht('User has not been created yet!'));
}
$user->openTransaction();
$user->beginWriteLocking();
$user->reload();
if ($user->getIsAdmin() == $admin) {
$user->endWriteLocking();
$user->killTransaction();
return $this;
}
$log = PhabricatorUserLog::initializeNewLog(
$actor,
$user->getPHID(),
PhabricatorUserLog::ACTION_ADMIN);
$log->setOldValue($user->getIsAdmin());
$log->setNewValue($admin);
$user->setIsAdmin((int)$admin);
$user->save();
$log->save();
$user->endWriteLocking();
$user->saveTransaction();
return $this;
}
/**
* @task role
*/
public function makeSystemAgentUser(PhabricatorUser $user, $system_agent) {
$actor = $this->requireActor();
if (!$user->getID()) {
- throw new Exception('User has not been created yet!');
+ throw new Exception(pht('User has not been created yet!'));
}
$user->openTransaction();
$user->beginWriteLocking();
$user->reload();
if ($user->getIsSystemAgent() == $system_agent) {
$user->endWriteLocking();
$user->killTransaction();
return $this;
}
$log = PhabricatorUserLog::initializeNewLog(
$actor,
$user->getPHID(),
PhabricatorUserLog::ACTION_SYSTEM_AGENT);
$log->setOldValue($user->getIsSystemAgent());
$log->setNewValue($system_agent);
$user->setIsSystemAgent((int)$system_agent);
$user->save();
$log->save();
$user->endWriteLocking();
$user->saveTransaction();
return $this;
}
/**
* @task role
*/
public function disableUser(PhabricatorUser $user, $disable) {
$actor = $this->requireActor();
if (!$user->getID()) {
- throw new Exception('User has not been created yet!');
+ throw new Exception(pht('User has not been created yet!'));
}
$user->openTransaction();
$user->beginWriteLocking();
$user->reload();
if ($user->getIsDisabled() == $disable) {
$user->endWriteLocking();
$user->killTransaction();
return $this;
}
$log = PhabricatorUserLog::initializeNewLog(
$actor,
$user->getPHID(),
PhabricatorUserLog::ACTION_DISABLE);
$log->setOldValue($user->getIsDisabled());
$log->setNewValue($disable);
$user->setIsDisabled((int)$disable);
$user->save();
$log->save();
$user->endWriteLocking();
$user->saveTransaction();
return $this;
}
/**
* @task role
*/
public function approveUser(PhabricatorUser $user, $approve) {
$actor = $this->requireActor();
if (!$user->getID()) {
- throw new Exception('User has not been created yet!');
+ throw new Exception(pht('User has not been created yet!'));
}
$user->openTransaction();
$user->beginWriteLocking();
$user->reload();
if ($user->getIsApproved() == $approve) {
$user->endWriteLocking();
$user->killTransaction();
return $this;
}
$log = PhabricatorUserLog::initializeNewLog(
$actor,
$user->getPHID(),
PhabricatorUserLog::ACTION_APPROVE);
$log->setOldValue($user->getIsApproved());
$log->setNewValue($approve);
$user->setIsApproved($approve);
$user->save();
$log->save();
$user->endWriteLocking();
$user->saveTransaction();
return $this;
}
/* -( Adding, Removing and Changing Email )-------------------------------- */
/**
* @task email
*/
public function addEmail(
PhabricatorUser $user,
PhabricatorUserEmail $email) {
$actor = $this->requireActor();
if (!$user->getID()) {
- throw new Exception('User has not been created yet!');
+ throw new Exception(pht('User has not been created yet!'));
}
if ($email->getID()) {
- throw new Exception('Email has already been created!');
+ throw new Exception(pht('Email has already been created!'));
}
// Use changePrimaryEmail() to change primary email.
$email->setIsPrimary(0);
$email->setUserPHID($user->getPHID());
$this->willAddEmail($email);
$user->openTransaction();
$user->beginWriteLocking();
$user->reload();
try {
$email->save();
} catch (AphrontDuplicateKeyQueryException $ex) {
$user->endWriteLocking();
$user->killTransaction();
throw $ex;
}
$log = PhabricatorUserLog::initializeNewLog(
$actor,
$user->getPHID(),
PhabricatorUserLog::ACTION_EMAIL_ADD);
$log->setNewValue($email->getAddress());
$log->save();
$user->endWriteLocking();
$user->saveTransaction();
return $this;
}
/**
* @task email
*/
public function removeEmail(
PhabricatorUser $user,
PhabricatorUserEmail $email) {
$actor = $this->requireActor();
if (!$user->getID()) {
- throw new Exception('User has not been created yet!');
+ throw new Exception(pht('User has not been created yet!'));
}
if (!$email->getID()) {
- throw new Exception('Email has not been created yet!');
+ throw new Exception(pht('Email has not been created yet!'));
}
$user->openTransaction();
$user->beginWriteLocking();
$user->reload();
$email->reload();
if ($email->getIsPrimary()) {
- throw new Exception("Can't remove primary email!");
+ throw new Exception(pht("Can't remove primary email!"));
}
if ($email->getUserPHID() != $user->getPHID()) {
- throw new Exception('Email not owned by user!');
+ throw new Exception(pht('Email not owned by user!'));
}
$email->delete();
$log = PhabricatorUserLog::initializeNewLog(
$actor,
$user->getPHID(),
PhabricatorUserLog::ACTION_EMAIL_REMOVE);
$log->setOldValue($email->getAddress());
$log->save();
$user->endWriteLocking();
$user->saveTransaction();
$this->revokePasswordResetLinks($user);
return $this;
}
/**
* @task email
*/
public function changePrimaryEmail(
PhabricatorUser $user,
PhabricatorUserEmail $email) {
$actor = $this->requireActor();
if (!$user->getID()) {
- throw new Exception('User has not been created yet!');
+ throw new Exception(pht('User has not been created yet!'));
}
if (!$email->getID()) {
- throw new Exception('Email has not been created yet!');
+ throw new Exception(pht('Email has not been created yet!'));
}
$user->openTransaction();
$user->beginWriteLocking();
$user->reload();
$email->reload();
if ($email->getUserPHID() != $user->getPHID()) {
- throw new Exception('User does not own email!');
+ throw new Exception(pht('User does not own email!'));
}
if ($email->getIsPrimary()) {
- throw new Exception('Email is already primary!');
+ throw new Exception(pht('Email is already primary!'));
}
if (!$email->getIsVerified()) {
- throw new Exception('Email is not verified!');
+ throw new Exception(pht('Email is not verified!'));
}
$old_primary = $user->loadPrimaryEmail();
if ($old_primary) {
$old_primary->setIsPrimary(0);
$old_primary->save();
}
$email->setIsPrimary(1);
$email->save();
$log = PhabricatorUserLog::initializeNewLog(
$actor,
$user->getPHID(),
PhabricatorUserLog::ACTION_EMAIL_PRIMARY);
$log->setOldValue($old_primary ? $old_primary->getAddress() : null);
$log->setNewValue($email->getAddress());
$log->save();
$user->endWriteLocking();
$user->saveTransaction();
if ($old_primary) {
$old_primary->sendOldPrimaryEmail($user, $email);
}
$email->sendNewPrimaryEmail($user);
$this->revokePasswordResetLinks($user);
return $this;
}
/**
* Verify a user's email address.
*
* This verifies an individual email address. If the address is the user's
* primary address and their account was not previously verified, their
* account is marked as email verified.
*
* @task email
*/
public function verifyEmail(
PhabricatorUser $user,
PhabricatorUserEmail $email) {
$actor = $this->requireActor();
if (!$user->getID()) {
- throw new Exception('User has not been created yet!');
+ throw new Exception(pht('User has not been created yet!'));
}
if (!$email->getID()) {
- throw new Exception('Email has not been created yet!');
+ throw new Exception(pht('Email has not been created yet!'));
}
$user->openTransaction();
$user->beginWriteLocking();
$user->reload();
$email->reload();
if ($email->getUserPHID() != $user->getPHID()) {
throw new Exception(pht('User does not own email!'));
}
if (!$email->getIsVerified()) {
$email->setIsVerified(1);
$email->save();
$log = PhabricatorUserLog::initializeNewLog(
$actor,
$user->getPHID(),
PhabricatorUserLog::ACTION_EMAIL_VERIFY);
$log->setNewValue($email->getAddress());
$log->save();
}
if (!$user->getIsEmailVerified()) {
// If the user just verified their primary email address, mark their
// account as email verified.
$user_primary = $user->loadPrimaryEmail();
if ($user_primary->getID() == $email->getID()) {
$user->setIsEmailVerified(1);
$user->save();
}
}
$user->endWriteLocking();
$user->saveTransaction();
$this->didVerifyEmail($user, $email);
}
/**
* Reassign an unverified email address.
*/
public function reassignEmail(
PhabricatorUser $user,
PhabricatorUserEmail $email) {
$actor = $this->requireActor();
if (!$user->getID()) {
throw new Exception(pht('User has not been created yet!'));
}
if (!$email->getID()) {
throw new Exception(pht('Email has not been created yet!'));
}
$user->openTransaction();
$user->beginWriteLocking();
$user->reload();
$email->reload();
$old_user = $email->getUserPHID();
if ($old_user != $user->getPHID()) {
if ($email->getIsVerified()) {
throw new Exception(
- pht(
- 'Verified email addresses can not be reassigned.'));
+ pht('Verified email addresses can not be reassigned.'));
}
if ($email->getIsPrimary()) {
throw new Exception(
- pht(
- 'Primary email addresses can not be reassigned.'));
+ pht('Primary email addresses can not be reassigned.'));
}
$email->setUserPHID($user->getPHID());
$email->save();
$log = PhabricatorUserLog::initializeNewLog(
$actor,
$user->getPHID(),
PhabricatorUserLog::ACTION_EMAIL_REASSIGN);
$log->setNewValue($email->getAddress());
$log->save();
}
$user->endWriteLocking();
$user->saveTransaction();
}
/* -( Internals )---------------------------------------------------------- */
/**
* @task internal
*/
private function willAddEmail(PhabricatorUserEmail $email) {
// Hard check before write to prevent creation of disallowed email
// addresses. Normally, the application does checks and raises more
// user friendly errors for us, but we omit the courtesy checks on some
// pathways like administrative scripts for simplicity.
if (!PhabricatorUserEmail::isValidAddress($email->getAddress())) {
throw new Exception(PhabricatorUserEmail::describeValidAddresses());
}
if (!PhabricatorUserEmail::isAllowedAddress($email->getAddress())) {
throw new Exception(PhabricatorUserEmail::describeAllowedAddresses());
}
$application_email = id(new PhabricatorMetaMTAApplicationEmailQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withAddresses(array($email->getAddress()))
->executeOne();
if ($application_email) {
throw new Exception($application_email->getInUseMessage());
}
}
private function revokePasswordResetLinks(PhabricatorUser $user) {
// Revoke any outstanding password reset links. If an attacker compromises
// an account, changes the email address, and sends themselves a password
// reset link, it could otherwise remain live for a short period of time
// and allow them to compromise the account again later.
PhabricatorAuthTemporaryToken::revokeTokens(
$user,
array($user->getPHID()),
array(
PhabricatorAuthSessionEngine::ONETIME_TEMPORARY_TOKEN_TYPE,
PhabricatorAuthSessionEngine::PASSWORD_TEMPORARY_TOKEN_TYPE,
));
}
private function didVerifyEmail(
PhabricatorUser $user,
PhabricatorUserEmail $email) {
$event_type = PhabricatorEventType::TYPE_AUTH_DIDVERIFYEMAIL;
$event_data = array(
'user' => $user,
'email' => $email,
);
$event = id(new PhabricatorEvent($event_type, $event_data))
->setUser($user);
PhutilEventEngine::dispatchEvent($event);
}
}
diff --git a/src/applications/people/editor/__tests__/PhabricatorUserEditorTestCase.php b/src/applications/people/editor/__tests__/PhabricatorUserEditorTestCase.php
index 6cc6abf37..ef0c58060 100644
--- a/src/applications/people/editor/__tests__/PhabricatorUserEditorTestCase.php
+++ b/src/applications/people/editor/__tests__/PhabricatorUserEditorTestCase.php
@@ -1,90 +1,88 @@
<?php
final class PhabricatorUserEditorTestCase extends PhabricatorTestCase {
protected function getPhabricatorTestCaseConfiguration() {
return array(
self::PHABRICATOR_TESTCONFIG_BUILD_STORAGE_FIXTURES => true,
);
}
public function testRegistrationEmailOK() {
$env = PhabricatorEnv::beginScopedEnv();
$env->overrideEnvConfig('auth.email-domains', array('example.com'));
$this->registerUser(
'PhabricatorUserEditorTestCaseOK',
'PhabricatorUserEditorTest@example.com');
$this->assertTrue(true);
}
public function testRegistrationEmailInvalid() {
$env = PhabricatorEnv::beginScopedEnv();
$env->overrideEnvConfig('auth.email-domains', array('example.com'));
$prefix = str_repeat('a', PhabricatorUserEmail::MAX_ADDRESS_LENGTH);
$email = $prefix.'@evil.com@example.com';
try {
- $this->registerUser(
- 'PhabricatorUserEditorTestCaseInvalid',
- $email);
+ $this->registerUser('PhabricatorUserEditorTestCaseInvalid', $email);
} catch (Exception $ex) {
$caught = $ex;
}
$this->assertTrue($caught instanceof Exception);
}
public function testRegistrationEmailDomain() {
$env = PhabricatorEnv::beginScopedEnv();
$env->overrideEnvConfig('auth.email-domains', array('example.com'));
$caught = null;
try {
$this->registerUser(
'PhabricatorUserEditorTestCaseDomain',
'PhabricatorUserEditorTest@whitehouse.gov');
} catch (Exception $ex) {
$caught = $ex;
}
$this->assertTrue($caught instanceof Exception);
}
public function testRegistrationEmailApplicationEmailCollide() {
$app_email = 'bugs@whitehouse.gov';
$app_email_object =
PhabricatorMetaMTAApplicationEmail::initializeNewAppEmail(
$this->generateNewTestUser());
$app_email_object->setAddress($app_email);
$app_email_object->setApplicationPHID('test');
$app_email_object->save();
$caught = null;
try {
$this->registerUser(
'PhabricatorUserEditorTestCaseDomain',
$app_email);
} catch (Exception $ex) {
$caught = $ex;
}
$this->assertTrue($caught instanceof Exception);
}
private function registerUser($username, $email) {
$user = id(new PhabricatorUser())
->setUsername($username)
->setRealname($username);
$email = id(new PhabricatorUserEmail())
->setAddress($email)
->setIsVerified(0);
id(new PhabricatorUserEditor())
->setActor($user)
->createNewUser($user, $email);
}
}
diff --git a/src/applications/people/storage/PhabricatorUser.php b/src/applications/people/storage/PhabricatorUser.php
index e66a34d9a..d69fa146d 100644
--- a/src/applications/people/storage/PhabricatorUser.php
+++ b/src/applications/people/storage/PhabricatorUser.php
@@ -1,1157 +1,1158 @@
<?php
/**
* @task availability Availability
* @task image-cache Profile Image Cache
* @task factors Multi-Factor Authentication
* @task handles Managing Handles
*/
final class PhabricatorUser
extends PhabricatorUserDAO
implements
PhutilPerson,
PhabricatorPolicyInterface,
PhabricatorCustomFieldInterface,
PhabricatorDestructibleInterface,
PhabricatorSSHPublicKeyInterface {
const SESSION_TABLE = 'phabricator_session';
const NAMETOKEN_TABLE = 'user_nametoken';
const MAXIMUM_USERNAME_LENGTH = 64;
protected $userName;
protected $realName;
protected $sex;
protected $translation;
protected $passwordSalt;
protected $passwordHash;
protected $profileImagePHID;
protected $profileImageCache;
protected $availabilityCache;
protected $availabilityCacheTTL;
protected $timezoneIdentifier = '';
protected $consoleEnabled = 0;
protected $consoleVisible = 0;
protected $consoleTab = '';
protected $conduitCertificate;
protected $isSystemAgent = 0;
protected $isAdmin = 0;
protected $isDisabled = 0;
protected $isEmailVerified = 0;
protected $isApproved = 0;
protected $isEnrolledInMultiFactor = 0;
protected $accountSecret;
private $profileImage = self::ATTACHABLE;
private $profile = null;
private $availability = self::ATTACHABLE;
private $preferences = null;
private $omnipotent = false;
private $customFields = self::ATTACHABLE;
private $alternateCSRFString = self::ATTACHABLE;
private $session = self::ATTACHABLE;
private $authorities = array();
private $handlePool;
protected function readField($field) {
switch ($field) {
case 'timezoneIdentifier':
// If the user hasn't set one, guess the server's time.
return nonempty(
$this->timezoneIdentifier,
date_default_timezone_get());
// Make sure these return booleans.
case 'isAdmin':
return (bool)$this->isAdmin;
case 'isDisabled':
return (bool)$this->isDisabled;
case 'isSystemAgent':
return (bool)$this->isSystemAgent;
case 'isEmailVerified':
return (bool)$this->isEmailVerified;
case 'isApproved':
return (bool)$this->isApproved;
default:
return parent::readField($field);
}
}
/**
* Is this a live account which has passed required approvals? Returns true
* if this is an enabled, verified (if required), approved (if required)
* account, and false otherwise.
*
* @return bool True if this is a standard, usable account.
*/
public function isUserActivated() {
if ($this->isOmnipotent()) {
return true;
}
if ($this->getIsDisabled()) {
return false;
}
if (!$this->getIsApproved()) {
return false;
}
if (PhabricatorUserEmail::isEmailVerificationRequired()) {
if (!$this->getIsEmailVerified()) {
return false;
}
}
return true;
}
/**
* Returns `true` if this is a standard user who is logged in. Returns `false`
* for logged out, anonymous, or external users.
*
* @return bool `true` if the user is a standard user who is logged in with
* a normal session.
*/
public function getIsStandardUser() {
$type_user = PhabricatorPeopleUserPHIDType::TYPECONST;
return $this->getPHID() && (phid_get_type($this->getPHID()) == $type_user);
}
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_COLUMN_SCHEMA => array(
'userName' => 'sort64',
'realName' => 'text128',
'sex' => 'text4?',
'translation' => 'text64?',
'passwordSalt' => 'text32?',
'passwordHash' => 'text128?',
'profileImagePHID' => 'phid?',
'consoleEnabled' => 'bool',
'consoleVisible' => 'bool',
'consoleTab' => 'text64',
'conduitCertificate' => 'text255',
'isSystemAgent' => 'bool',
'isDisabled' => 'bool',
'isAdmin' => 'bool',
'timezoneIdentifier' => 'text255',
'isEmailVerified' => 'uint32',
'isApproved' => 'uint32',
'accountSecret' => 'bytes64',
'isEnrolledInMultiFactor' => 'bool',
'profileImageCache' => 'text255?',
'availabilityCache' => 'text255?',
'availabilityCacheTTL' => 'uint32?',
),
self::CONFIG_KEY_SCHEMA => array(
'key_phid' => null,
'phid' => array(
'columns' => array('phid'),
'unique' => true,
),
'userName' => array(
'columns' => array('userName'),
'unique' => true,
),
'realName' => array(
'columns' => array('realName'),
),
'key_approved' => array(
'columns' => array('isApproved'),
),
),
self::CONFIG_NO_MUTATE => array(
'profileImageCache' => true,
'availabilityCache' => true,
'availabilityCacheTTL' => true,
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhabricatorPeopleUserPHIDType::TYPECONST);
}
public function setPassword(PhutilOpaqueEnvelope $envelope) {
if (!$this->getPHID()) {
throw new Exception(
- 'You can not set a password for an unsaved user because their PHID '.
- 'is a salt component in the password hash.');
+ pht(
+ 'You can not set a password for an unsaved user because their PHID '.
+ 'is a salt component in the password hash.'));
}
if (!strlen($envelope->openEnvelope())) {
$this->setPasswordHash('');
} else {
$this->setPasswordSalt(md5(Filesystem::readRandomBytes(32)));
$hash = $this->hashPassword($envelope);
$this->setPasswordHash($hash->openEnvelope());
}
return $this;
}
// To satisfy PhutilPerson.
public function getSex() {
return $this->sex;
}
public function getMonogram() {
return '@'.$this->getUsername();
}
public function isLoggedIn() {
return !($this->getPHID() === null);
}
public function save() {
if (!$this->getConduitCertificate()) {
$this->setConduitCertificate($this->generateConduitCertificate());
}
if (!strlen($this->getAccountSecret())) {
$this->setAccountSecret(Filesystem::readRandomCharacters(64));
}
$result = parent::save();
if ($this->profile) {
$this->profile->save();
}
$this->updateNameTokens();
id(new PhabricatorSearchIndexer())
->queueDocumentForIndexing($this->getPHID());
return $result;
}
public function attachSession(PhabricatorAuthSession $session) {
$this->session = $session;
return $this;
}
public function getSession() {
return $this->assertAttached($this->session);
}
public function hasSession() {
return ($this->session !== self::ATTACHABLE);
}
private function generateConduitCertificate() {
return Filesystem::readRandomCharacters(255);
}
public function comparePassword(PhutilOpaqueEnvelope $envelope) {
if (!strlen($envelope->openEnvelope())) {
return false;
}
if (!strlen($this->getPasswordHash())) {
return false;
}
return PhabricatorPasswordHasher::comparePassword(
$this->getPasswordHashInput($envelope),
new PhutilOpaqueEnvelope($this->getPasswordHash()));
}
private function getPasswordHashInput(PhutilOpaqueEnvelope $password) {
$input =
$this->getUsername().
$password->openEnvelope().
$this->getPHID().
$this->getPasswordSalt();
return new PhutilOpaqueEnvelope($input);
}
private function hashPassword(PhutilOpaqueEnvelope $password) {
$hasher = PhabricatorPasswordHasher::getBestHasher();
$input_envelope = $this->getPasswordHashInput($password);
return $hasher->getPasswordHashForStorage($input_envelope);
}
const CSRF_CYCLE_FREQUENCY = 3600;
const CSRF_SALT_LENGTH = 8;
const CSRF_TOKEN_LENGTH = 16;
const CSRF_BREACH_PREFIX = 'B@';
const EMAIL_CYCLE_FREQUENCY = 86400;
const EMAIL_TOKEN_LENGTH = 24;
private function getRawCSRFToken($offset = 0) {
return $this->generateToken(
time() + (self::CSRF_CYCLE_FREQUENCY * $offset),
self::CSRF_CYCLE_FREQUENCY,
PhabricatorEnv::getEnvConfig('phabricator.csrf-key'),
self::CSRF_TOKEN_LENGTH);
}
/**
* @phutil-external-symbol class PhabricatorStartup
*/
public function getCSRFToken() {
$salt = PhabricatorStartup::getGlobal('csrf.salt');
if (!$salt) {
$salt = Filesystem::readRandomCharacters(self::CSRF_SALT_LENGTH);
PhabricatorStartup::setGlobal('csrf.salt', $salt);
}
// Generate a token hash to mitigate BREACH attacks against SSL. See
// discussion in T3684.
$token = $this->getRawCSRFToken();
$hash = PhabricatorHash::digest($token, $salt);
return 'B@'.$salt.substr($hash, 0, self::CSRF_TOKEN_LENGTH);
}
public function validateCSRFToken($token) {
$salt = null;
$version = 'plain';
// This is a BREACH-mitigating token. See T3684.
$breach_prefix = self::CSRF_BREACH_PREFIX;
$breach_prelen = strlen($breach_prefix);
if (!strncmp($token, $breach_prefix, $breach_prelen)) {
$version = 'breach';
$salt = substr($token, $breach_prelen, self::CSRF_SALT_LENGTH);
$token = substr($token, $breach_prelen + self::CSRF_SALT_LENGTH);
}
// When the user posts a form, we check that it contains a valid CSRF token.
// Tokens cycle each hour (every CSRF_CYLCE_FREQUENCY seconds) and we accept
// either the current token, the next token (users can submit a "future"
// token if you have two web frontends that have some clock skew) or any of
// the last 6 tokens. This means that pages are valid for up to 7 hours.
// There is also some Javascript which periodically refreshes the CSRF
// tokens on each page, so theoretically pages should be valid indefinitely.
// However, this code may fail to run (if the user loses their internet
// connection, or there's a JS problem, or they don't have JS enabled).
// Choosing the size of the window in which we accept old CSRF tokens is
// an issue of balancing concerns between security and usability. We could
// choose a very narrow (e.g., 1-hour) window to reduce vulnerability to
// attacks using captured CSRF tokens, but it's also more likely that real
// users will be affected by this, e.g. if they close their laptop for an
// hour, open it back up, and try to submit a form before the CSRF refresh
// can kick in. Since the user experience of submitting a form with expired
// CSRF is often quite bad (you basically lose data, or it's a big pain to
// recover at least) and I believe we gain little additional protection
// by keeping the window very short (the overwhelming value here is in
// preventing blind attacks, and most attacks which can capture CSRF tokens
// can also just capture authentication information [sniffing networks]
// or act as the user [xss]) the 7 hour default seems like a reasonable
// balance. Other major platforms have much longer CSRF token lifetimes,
// like Rails (session duration) and Django (forever), which suggests this
// is a reasonable analysis.
$csrf_window = 6;
for ($ii = -$csrf_window; $ii <= 1; $ii++) {
$valid = $this->getRawCSRFToken($ii);
switch ($version) {
// TODO: We can remove this after the BREACH version has been in the
// wild for a while.
case 'plain':
if ($token == $valid) {
return true;
}
break;
case 'breach':
$digest = PhabricatorHash::digest($valid, $salt);
if (substr($digest, 0, self::CSRF_TOKEN_LENGTH) == $token) {
return true;
}
break;
default:
- throw new Exception('Unknown CSRF token format!');
+ throw new Exception(pht('Unknown CSRF token format!'));
}
}
return false;
}
private function generateToken($epoch, $frequency, $key, $len) {
if ($this->getPHID()) {
$vec = $this->getPHID().$this->getAccountSecret();
} else {
$vec = $this->getAlternateCSRFString();
}
if ($this->hasSession()) {
$vec = $vec.$this->getSession()->getSessionKey();
}
$time_block = floor($epoch / $frequency);
$vec = $vec.$key.$time_block;
return substr(PhabricatorHash::digest($vec), 0, $len);
}
public function getUserProfile() {
return $this->assertAttached($this->profile);
}
public function attachUserProfile(PhabricatorUserProfile $profile) {
$this->profile = $profile;
return $this;
}
public function loadUserProfile() {
if ($this->profile) {
return $this->profile;
}
$profile_dao = new PhabricatorUserProfile();
$this->profile = $profile_dao->loadOneWhere('userPHID = %s',
$this->getPHID());
if (!$this->profile) {
$profile_dao->setUserPHID($this->getPHID());
$this->profile = $profile_dao;
}
return $this->profile;
}
public function loadPrimaryEmailAddress() {
$email = $this->loadPrimaryEmail();
if (!$email) {
- throw new Exception('User has no primary email address!');
+ throw new Exception(pht('User has no primary email address!'));
}
return $email->getAddress();
}
public function loadPrimaryEmail() {
return $this->loadOneRelative(
new PhabricatorUserEmail(),
'userPHID',
'getPHID',
'(isPrimary = 1)');
}
public function loadPreferences() {
if ($this->preferences) {
return $this->preferences;
}
$preferences = null;
if ($this->getPHID()) {
$preferences = id(new PhabricatorUserPreferences())->loadOneWhere(
'userPHID = %s',
$this->getPHID());
}
if (!$preferences) {
$preferences = new PhabricatorUserPreferences();
$preferences->setUserPHID($this->getPHID());
$default_dict = array(
PhabricatorUserPreferences::PREFERENCE_TITLES => 'glyph',
PhabricatorUserPreferences::PREFERENCE_EDITOR => '',
PhabricatorUserPreferences::PREFERENCE_MONOSPACED => '',
PhabricatorUserPreferences::PREFERENCE_DARK_CONSOLE => 0,
);
$preferences->setPreferences($default_dict);
}
$this->preferences = $preferences;
return $preferences;
}
public function loadEditorLink($path, $line, $callsign) {
$editor = $this->loadPreferences()->getPreference(
PhabricatorUserPreferences::PREFERENCE_EDITOR);
if (is_array($path)) {
$multiedit = $this->loadPreferences()->getPreference(
PhabricatorUserPreferences::PREFERENCE_MULTIEDIT);
switch ($multiedit) {
case '':
$path = implode(' ', $path);
break;
case 'disable':
return null;
}
}
if (!strlen($editor)) {
return null;
}
$uri = strtr($editor, array(
'%%' => '%',
'%f' => phutil_escape_uri($path),
'%l' => phutil_escape_uri($line),
'%r' => phutil_escape_uri($callsign),
));
// The resulting URI must have an allowed protocol. Otherwise, we'll return
// a link to an error page explaining the misconfiguration.
$ok = PhabricatorHelpEditorProtocolController::hasAllowedProtocol($uri);
if (!$ok) {
return '/help/editorprotocol/';
}
return (string)$uri;
}
public function getAlternateCSRFString() {
return $this->assertAttached($this->alternateCSRFString);
}
public function attachAlternateCSRFString($string) {
$this->alternateCSRFString = $string;
return $this;
}
/**
* Populate the nametoken table, which used to fetch typeahead results. When
* a user types "linc", we want to match "Abraham Lincoln" from on-demand
* typeahead sources. To do this, we need a separate table of name fragments.
*/
public function updateNameTokens() {
$table = self::NAMETOKEN_TABLE;
$conn_w = $this->establishConnection('w');
$tokens = PhabricatorTypeaheadDatasource::tokenizeString(
$this->getUserName().' '.$this->getRealName());
$sql = array();
foreach ($tokens as $token) {
$sql[] = qsprintf(
$conn_w,
'(%d, %s)',
$this->getID(),
$token);
}
queryfx(
$conn_w,
'DELETE FROM %T WHERE userID = %d',
$table,
$this->getID());
if ($sql) {
queryfx(
$conn_w,
'INSERT INTO %T (userID, token) VALUES %Q',
$table,
implode(', ', $sql));
}
}
public function sendWelcomeEmail(PhabricatorUser $admin) {
$admin_username = $admin->getUserName();
$admin_realname = $admin->getRealName();
$user_username = $this->getUserName();
$is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business');
$base_uri = PhabricatorEnv::getProductionURI('/');
$engine = new PhabricatorAuthSessionEngine();
$uri = $engine->getOneTimeLoginURI(
$this,
$this->loadPrimaryEmail(),
PhabricatorAuthSessionEngine::ONETIME_WELCOME);
- $body = <<<EOBODY
-Welcome to Phabricator!
-
-{$admin_username} ({$admin_realname}) has created an account for you.
-
- Username: {$user_username}
-
-To login to Phabricator, follow this link and set a password:
-
- {$uri}
-
-After you have set a password, you can login in the future by going here:
-
- {$base_uri}
-
-EOBODY;
+ $body = pht(
+ "Welcome to Phabricator!\n\n".
+ "%s (%s) has created an account for you.\n\n".
+ " Username: %s\n\n".
+ "To login to Phabricator, follow this link and set a password:\n\n".
+ " %s\n\n".
+ "After you have set a password, you can login in the future by ".
+ "going here:\n\n".
+ " %s\n",
+ $admin_username,
+ $admin_realname,
+ $user_username,
+ $uri,
+ $base_uri);
if (!$is_serious) {
- $body .= <<<EOBODY
-
-Love,
-Phabricator
-
-EOBODY;
+ $body .= sprintf(
+ "\n%s\n",
+ pht("Love,\nPhabricator"));
}
$mail = id(new PhabricatorMetaMTAMail())
->addTos(array($this->getPHID()))
->setForceDelivery(true)
- ->setSubject('[Phabricator] Welcome to Phabricator')
+ ->setSubject(pht('[Phabricator] Welcome to Phabricator'))
->setBody($body)
->saveAndSend();
}
public function sendUsernameChangeEmail(
PhabricatorUser $admin,
$old_username) {
$admin_username = $admin->getUserName();
$admin_realname = $admin->getRealName();
$new_username = $this->getUserName();
$password_instructions = null;
if (PhabricatorPasswordAuthProvider::getPasswordProvider()) {
$engine = new PhabricatorAuthSessionEngine();
$uri = $engine->getOneTimeLoginURI(
$this,
null,
PhabricatorAuthSessionEngine::ONETIME_USERNAME);
- $password_instructions = <<<EOTXT
-If you use a password to login, you'll need to reset it before you can login
-again. You can reset your password by following this link:
-
- {$uri}
-
-And, of course, you'll need to use your new username to login from now on. If
-you use OAuth to login, nothing should change.
-
-EOTXT;
+ $password_instructions = sprintf(
+ "%s\n\n %s\n\n%s\n",
+ pht(
+ "If you use a password to login, you'll need to reset it ".
+ "before you can login again. You can reset your password by ".
+ "following this link:"),
+ $uri,
+ pht(
+ "And, of course, you'll need to use your new username to login ".
+ "from now on. If you use OAuth to login, nothing should change."));
}
- $body = <<<EOBODY
-{$admin_username} ({$admin_realname}) has changed your Phabricator username.
-
- Old Username: {$old_username}
- New Username: {$new_username}
-
-{$password_instructions}
-EOBODY;
+ $body = sprintf(
+ "%s\n\n %s\n %s\n\n%s",
+ pht(
+ '%s (%s) has changed your Phabricator username.',
+ $admin_username,
+ $admin_realname),
+ pht(
+ 'Old Username: %s',
+ $old_username),
+ pht(
+ 'New Username: %s',
+ $new_username),
+ $password_instructions);
$mail = id(new PhabricatorMetaMTAMail())
->addTos(array($this->getPHID()))
->setForceDelivery(true)
- ->setSubject('[Phabricator] Username Changed')
+ ->setSubject(pht('[Phabricator] Username Changed'))
->setBody($body)
->saveAndSend();
}
public static function describeValidUsername() {
return pht(
'Usernames must contain only numbers, letters, period, underscore and '.
'hyphen, and can not end with a period. They must have no more than %d '.
'characters.',
new PhutilNumber(self::MAXIMUM_USERNAME_LENGTH));
}
public static function validateUsername($username) {
// NOTE: If you update this, make sure to update:
//
// - Remarkup rule for @mentions.
// - Routing rule for "/p/username/".
// - Unit tests, obviously.
// - describeValidUsername() method, above.
if (strlen($username) > self::MAXIMUM_USERNAME_LENGTH) {
return false;
}
return (bool)preg_match('/^[a-zA-Z0-9._-]*[a-zA-Z0-9_-]\z/', $username);
}
public static function getDefaultProfileImageURI() {
return celerity_get_resource_uri('/rsrc/image/avatar.png');
}
public function attachProfileImageURI($uri) {
$this->profileImage = $uri;
return $this;
}
public function getProfileImageURI() {
return $this->assertAttached($this->profileImage);
}
public function getFullName() {
if (strlen($this->getRealName())) {
return $this->getUsername().' ('.$this->getRealName().')';
} else {
return $this->getUsername();
}
}
public function getTimeZone() {
return new DateTimeZone($this->getTimezoneIdentifier());
}
public function __toString() {
return $this->getUsername();
}
public static function loadOneWithEmailAddress($address) {
$email = id(new PhabricatorUserEmail())->loadOneWhere(
'address = %s',
$address);
if (!$email) {
return null;
}
return id(new PhabricatorUser())->loadOneWhere(
'phid = %s',
$email->getUserPHID());
}
/**
* Grant a user a source of authority, to let them bypass policy checks they
* could not otherwise.
*/
public function grantAuthority($authority) {
$this->authorities[] = $authority;
return $this;
}
/**
* Get authorities granted to the user.
*/
public function getAuthorities() {
return $this->authorities;
}
/* -( Availability )------------------------------------------------------- */
/**
* @task availability
*/
public function attachAvailability(array $availability) {
$this->availability = $availability;
return $this;
}
/**
* Get the timestamp the user is away until, if they are currently away.
*
* @return int|null Epoch timestamp, or `null` if the user is not away.
* @task availability
*/
public function getAwayUntil() {
$availability = $this->availability;
$this->assertAttached($availability);
if (!$availability) {
return null;
}
return idx($availability, 'until');
}
/**
* Describe the user's availability.
*
* @param PhabricatorUser Viewing user.
* @return string Human-readable description of away status.
* @task availability
*/
public function getAvailabilityDescription(PhabricatorUser $viewer) {
$until = $this->getAwayUntil();
if ($until) {
return pht('Away until %s', phabricator_datetime($until, $viewer));
} else {
return pht('Available');
}
}
/**
* Get cached availability, if present.
*
* @return wild|null Cache data, or null if no cache is available.
* @task availability
*/
public function getAvailabilityCache() {
$now = PhabricatorTime::getNow();
if ($this->availabilityCacheTTL <= $now) {
return null;
}
try {
return phutil_json_decode($this->availabilityCache);
} catch (Exception $ex) {
return null;
}
}
/**
* Write to the availability cache.
*
* @param wild Availability cache data.
* @param int|null Cache TTL.
* @return this
* @task availability
*/
public function writeAvailabilityCache(array $availability, $ttl) {
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
queryfx(
$this->establishConnection('w'),
'UPDATE %T SET availabilityCache = %s, availabilityCacheTTL = %nd
WHERE id = %d',
$this->getTableName(),
json_encode($availability),
$ttl,
$this->getID());
unset($unguarded);
return $this;
}
/* -( Profile Image Cache )------------------------------------------------ */
/**
* Get this user's cached profile image URI.
*
* @return string|null Cached URI, if a URI is cached.
* @task image-cache
*/
public function getProfileImageCache() {
$version = $this->getProfileImageVersion();
$parts = explode(',', $this->profileImageCache, 2);
if (count($parts) !== 2) {
return null;
}
if ($parts[0] !== $version) {
return null;
}
return $parts[1];
}
/**
* Generate a new cache value for this user's profile image.
*
* @return string New cache value.
* @task image-cache
*/
public function writeProfileImageCache($uri) {
$version = $this->getProfileImageVersion();
$cache = "{$version},{$uri}";
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
queryfx(
$this->establishConnection('w'),
'UPDATE %T SET profileImageCache = %s WHERE id = %d',
$this->getTableName(),
$cache,
$this->getID());
unset($unguarded);
}
/**
* Get a version identifier for a user's profile image.
*
* This version will change if the image changes, or if any of the
* environment configuration which goes into generating a URI changes.
*
* @return string Cache version.
* @task image-cache
*/
private function getProfileImageVersion() {
$parts = array(
PhabricatorEnv::getCDNURI('/'),
PhabricatorEnv::getEnvConfig('cluster.instance'),
$this->getProfileImagePHID(),
);
$parts = serialize($parts);
return PhabricatorHash::digestForIndex($parts);
}
/* -( Multi-Factor Authentication )---------------------------------------- */
/**
* Update the flag storing this user's enrollment in multi-factor auth.
*
* With certain settings, we need to check if a user has MFA on every page,
* so we cache MFA enrollment on the user object for performance. Calling this
* method synchronizes the cache by examining enrollment records. After
* updating the cache, use @{method:getIsEnrolledInMultiFactor} to check if
* the user is enrolled.
*
* This method should be called after any changes are made to a given user's
* multi-factor configuration.
*
* @return void
* @task factors
*/
public function updateMultiFactorEnrollment() {
$factors = id(new PhabricatorAuthFactorConfig())->loadAllWhere(
'userPHID = %s',
$this->getPHID());
$enrolled = count($factors) ? 1 : 0;
if ($enrolled !== $this->isEnrolledInMultiFactor) {
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
queryfx(
$this->establishConnection('w'),
'UPDATE %T SET isEnrolledInMultiFactor = %d WHERE id = %d',
$this->getTableName(),
$enrolled,
$this->getID());
unset($unguarded);
$this->isEnrolledInMultiFactor = $enrolled;
}
}
/**
* Check if the user is enrolled in multi-factor authentication.
*
* Enrolled users have one or more multi-factor authentication sources
* attached to their account. For performance, this value is cached. You
* can use @{method:updateMultiFactorEnrollment} to update the cache.
*
* @return bool True if the user is enrolled.
* @task factors
*/
public function getIsEnrolledInMultiFactor() {
return $this->isEnrolledInMultiFactor;
}
/* -( Omnipotence )-------------------------------------------------------- */
/**
* Returns true if this user is omnipotent. Omnipotent users bypass all policy
* checks.
*
* @return bool True if the user bypasses policy checks.
*/
public function isOmnipotent() {
return $this->omnipotent;
}
/**
* Get an omnipotent user object for use in contexts where there is no acting
* user, notably daemons.
*
* @return PhabricatorUser An omnipotent user.
*/
public static function getOmnipotentUser() {
static $user = null;
if (!$user) {
$user = new PhabricatorUser();
$user->omnipotent = true;
$user->makeEphemeral();
}
return $user;
}
/* -( Managing Handles )--------------------------------------------------- */
/**
* Get a @{class:PhabricatorHandleList} which benefits from this viewer's
* internal handle pool.
*
* @param list<phid> List of PHIDs to load.
* @return PhabricatorHandleList Handle list object.
* @task handle
*/
public function loadHandles(array $phids) {
if ($this->handlePool === null) {
$this->handlePool = id(new PhabricatorHandlePool())
->setViewer($this);
}
return $this->handlePool->newHandleList($phids);
}
/**
* Get a @{class:PHUIHandleView} for a single handle.
*
* This benefits from the viewer's internal handle pool.
*
* @param phid PHID to render a handle for.
* @return PHUIHandleView View of the handle.
* @task handle
*/
public function renderHandle($phid) {
return $this->loadHandles(array($phid))->renderHandle($phid);
}
/**
* Get a @{class:PHUIHandleListView} for a list of handles.
*
* This benefits from the viewer's internal handle pool.
*
* @param list<phid> List of PHIDs to render.
* @return PHUIHandleListView View of the handles.
* @task handle
*/
public function renderHandleList(array $phids) {
return $this->loadHandles($phids)->renderList();
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
return PhabricatorPolicies::POLICY_PUBLIC;
case PhabricatorPolicyCapability::CAN_EDIT:
if ($this->getIsSystemAgent()) {
return PhabricatorPolicies::POLICY_ADMIN;
} else {
return PhabricatorPolicies::POLICY_NOONE;
}
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return $this->getPHID() && ($viewer->getPHID() === $this->getPHID());
}
public function describeAutomaticCapability($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_EDIT:
return pht('Only you can edit your information.');
default:
return null;
}
}
/* -( PhabricatorCustomFieldInterface )------------------------------------ */
public function getCustomFieldSpecificationForRole($role) {
return PhabricatorEnv::getEnvConfig('user.fields');
}
public function getCustomFieldBaseClass() {
return 'PhabricatorUserCustomField';
}
public function getCustomFields() {
return $this->assertAttached($this->customFields);
}
public function attachCustomFields(PhabricatorCustomFieldAttachment $fields) {
$this->customFields = $fields;
return $this;
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$this->openTransaction();
$this->delete();
$externals = id(new PhabricatorExternalAccount())->loadAllWhere(
'userPHID = %s',
$this->getPHID());
foreach ($externals as $external) {
$external->delete();
}
$prefs = id(new PhabricatorUserPreferences())->loadAllWhere(
'userPHID = %s',
$this->getPHID());
foreach ($prefs as $pref) {
$pref->delete();
}
$profiles = id(new PhabricatorUserProfile())->loadAllWhere(
'userPHID = %s',
$this->getPHID());
foreach ($profiles as $profile) {
$profile->delete();
}
$keys = id(new PhabricatorAuthSSHKey())->loadAllWhere(
'objectPHID = %s',
$this->getPHID());
foreach ($keys as $key) {
$key->delete();
}
$emails = id(new PhabricatorUserEmail())->loadAllWhere(
'userPHID = %s',
$this->getPHID());
foreach ($emails as $email) {
$email->delete();
}
$sessions = id(new PhabricatorAuthSession())->loadAllWhere(
'userPHID = %s',
$this->getPHID());
foreach ($sessions as $session) {
$session->delete();
}
$factors = id(new PhabricatorAuthFactorConfig())->loadAllWhere(
'userPHID = %s',
$this->getPHID());
foreach ($factors as $factor) {
$factor->delete();
}
$this->saveTransaction();
}
/* -( PhabricatorSSHPublicKeyInterface )----------------------------------- */
public function getSSHPublicKeyManagementURI(PhabricatorUser $viewer) {
if ($viewer->getPHID() == $this->getPHID()) {
// If the viewer is managing their own keys, take them to the normal
// panel.
return '/settings/panel/ssh/';
} else {
// Otherwise, take them to the administrative panel for this user.
return '/settings/'.$this->getID().'/panel/ssh/';
}
}
public function getSSHKeyDefaultName() {
return 'id_rsa_phabricator';
}
}
diff --git a/src/applications/people/storage/PhabricatorUserEmail.php b/src/applications/people/storage/PhabricatorUserEmail.php
index 4ae58130a..42946015d 100644
--- a/src/applications/people/storage/PhabricatorUserEmail.php
+++ b/src/applications/people/storage/PhabricatorUserEmail.php
@@ -1,276 +1,275 @@
<?php
/**
* @task restrictions Domain Restrictions
* @task email Email About Email
*/
final class PhabricatorUserEmail extends PhabricatorUserDAO {
protected $userPHID;
protected $address;
protected $isVerified;
protected $isPrimary;
protected $verificationCode;
const MAX_ADDRESS_LENGTH = 128;
protected function getConfiguration() {
return array(
self::CONFIG_COLUMN_SCHEMA => array(
'address' => 'sort128',
'isVerified' => 'bool',
'isPrimary' => 'bool',
'verificationCode' => 'text64?',
),
self::CONFIG_KEY_SCHEMA => array(
'address' => array(
'columns' => array('address'),
'unique' => true,
),
'userPHID' => array(
'columns' => array('userPHID', 'isPrimary'),
),
),
) + parent::getConfiguration();
}
public function getVerificationURI() {
return '/emailverify/'.$this->getVerificationCode().'/';
}
public function save() {
if (!$this->verificationCode) {
$this->setVerificationCode(Filesystem::readRandomCharacters(24));
}
return parent::save();
}
/* -( Domain Restrictions )------------------------------------------------ */
/**
* @task restrictions
*/
public static function isValidAddress($address) {
if (strlen($address) > self::MAX_ADDRESS_LENGTH) {
return false;
}
// Very roughly validate that this address isn't so mangled that a
// reasonable piece of code might completely misparse it. In particular,
// the major risks are:
//
// - `PhutilEmailAddress` needs to be able to extract the domain portion
// from it.
// - Reasonable mail adapters should be hard-pressed to interpret one
// address as several addresses.
//
// To this end, we're roughly verifying that there's some normal text, an
// "@" symbol, and then some more normal text.
$email_regex = '(^[a-z0-9_+.!-]+@[a-z0-9_+:.-]+\z)i';
if (!preg_match($email_regex, $address)) {
return false;
}
return true;
}
/**
* @task restrictions
*/
public static function describeValidAddresses() {
return pht(
- "Email addresses should be in the form 'user@domain.com'. The maximum ".
- "length of an email address is %d character(s).",
+ "Email addresses should be in the form '%s'. The maximum ".
+ "length of an email address is %s character(s).",
+ 'user@domain.com',
new PhutilNumber(self::MAX_ADDRESS_LENGTH));
}
/**
* @task restrictions
*/
public static function isAllowedAddress($address) {
if (!self::isValidAddress($address)) {
return false;
}
$allowed_domains = PhabricatorEnv::getEnvConfig('auth.email-domains');
if (!$allowed_domains) {
return true;
}
$addr_obj = new PhutilEmailAddress($address);
$domain = $addr_obj->getDomainName();
if (!$domain) {
return false;
}
$lower_domain = phutil_utf8_strtolower($domain);
foreach ($allowed_domains as $allowed_domain) {
$lower_allowed = phutil_utf8_strtolower($allowed_domain);
if ($lower_allowed === $lower_domain) {
return true;
}
}
return false;
}
/**
* @task restrictions
*/
public static function describeAllowedAddresses() {
$domains = PhabricatorEnv::getEnvConfig('auth.email-domains');
if (!$domains) {
return null;
}
if (count($domains) == 1) {
- return 'Email address must be @'.head($domains);
+ return pht('Email address must be @%s', head($domains));
} else {
- return 'Email address must be at one of: '.
- implode(', ', $domains);
+ return pht(
+ 'Email address must be at one of: %s',
+ implode(', ', $domains));
}
}
/**
* Check if this install requires email verification.
*
* @return bool True if email addresses must be verified.
*
* @task restrictions
*/
public static function isEmailVerificationRequired() {
// NOTE: Configuring required email domains implies required verification.
return PhabricatorEnv::getEnvConfig('auth.require-email-verification') ||
PhabricatorEnv::getEnvConfig('auth.email-domains');
}
/* -( Email About Email )-------------------------------------------------- */
/**
* Send a verification email from $user to this address.
*
* @param PhabricatorUser The user sending the verification.
* @return this
* @task email
*/
public function sendVerificationEmail(PhabricatorUser $user) {
$username = $user->getUsername();
$address = $this->getAddress();
$link = PhabricatorEnv::getProductionURI($this->getVerificationURI());
$is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business');
$signature = null;
if (!$is_serious) {
- $signature = <<<EOSIGNATURE
-Get Well Soon,
-Phabricator
-EOSIGNATURE;
+ $signature = pht("Get Well Soon,\nPhabricator");
}
- $body = <<<EOBODY
-Hi {$username},
-
-Please verify that you own this email address ({$address}) by clicking this
-link:
-
- {$link}
-
-{$signature}
-EOBODY;
+ $body = sprintf(
+ "%s\n\n%s\n\n %s\n\n%s",
+ pht('Hi %s', $username),
+ pht(
+ 'Please verify that you own this email address (%s) by '.
+ 'clicking this link:',
+ $address),
+ $link,
+ $signature);
id(new PhabricatorMetaMTAMail())
->addRawTos(array($address))
->setForceDelivery(true)
- ->setSubject('[Phabricator] Email Verification')
+ ->setSubject(pht('[Phabricator] Email Verification'))
->setBody($body)
->setRelatedPHID($user->getPHID())
->saveAndSend();
return $this;
}
/**
* Send a notification email from $user to this address, informing the
* recipient that this is no longer their account's primary address.
*
* @param PhabricatorUser The user sending the notification.
* @param PhabricatorUserEmail New primary email address.
* @return this
* @task email
*/
public function sendOldPrimaryEmail(
PhabricatorUser $user,
PhabricatorUserEmail $new) {
$username = $user->getUsername();
$old_address = $this->getAddress();
$new_address = $new->getAddress();
- $body = <<<EOBODY
-Hi {$username},
-
-This email address ({$old_address}) is no longer your primary email address.
-Going forward, Phabricator will send all email to your new primary email
-address ({$new_address}).
-
-EOBODY;
+ $body = sprintf(
+ "%s\n\n%s\n",
+ pht('Hi %s', $username),
+ pht(
+ 'This email address (%s) is no longer your primary email address. '.
+ 'Going forward, Phabricator will send all email to your new primary '.
+ 'email address (%s).',
+ $old_address,
+ $new_address));
id(new PhabricatorMetaMTAMail())
->addRawTos(array($old_address))
->setForceDelivery(true)
- ->setSubject('[Phabricator] Primary Address Changed')
+ ->setSubject(pht('[Phabricator] Primary Address Changed'))
->setBody($body)
->setFrom($user->getPHID())
->setRelatedPHID($user->getPHID())
->saveAndSend();
}
/**
* Send a notification email from $user to this address, informing the
* recipient that this is now their account's new primary email address.
*
* @param PhabricatorUser The user sending the verification.
* @return this
* @task email
*/
public function sendNewPrimaryEmail(PhabricatorUser $user) {
$username = $user->getUsername();
$new_address = $this->getAddress();
- $body = <<<EOBODY
-Hi {$username},
-
-This is now your primary email address ({$new_address}). Going forward,
-Phabricator will send all email here.
-
-EOBODY;
+ $body = sprintf(
+ "%s\n\n%s\n",
+ pht('Hi %s', $username),
+ pht(
+ 'This is now your primary email address (%s). Going forward, '.
+ 'Phabricator will send all email here.',
+ $new_address));
id(new PhabricatorMetaMTAMail())
->addRawTos(array($new_address))
->setForceDelivery(true)
- ->setSubject('[Phabricator] Primary Address Changed')
+ ->setSubject(pht('[Phabricator] Primary Address Changed'))
->setBody($body)
->setFrom($user->getPHID())
->setRelatedPHID($user->getPHID())
->saveAndSend();
return $this;
}
}
diff --git a/src/applications/people/storage/__tests__/PhabricatorUserTestCase.php b/src/applications/people/storage/__tests__/PhabricatorUserTestCase.php
index 561bcb2d6..a29dd4f20 100644
--- a/src/applications/people/storage/__tests__/PhabricatorUserTestCase.php
+++ b/src/applications/people/storage/__tests__/PhabricatorUserTestCase.php
@@ -1,56 +1,56 @@
<?php
final class PhabricatorUserTestCase extends PhabricatorTestCase {
public function testUsernameValidation() {
$map = array(
'alincoln' => true,
'alincoln69' => true,
'hd3' => true,
'Alincoln' => true,
'a.lincoln' => true,
'alincoln!' => false,
'' => false,
// These are silly, but permitted.
'7' => true,
'0' => true,
'____' => true,
'-' => true,
// These are not permitted because they make capturing @mentions
// ambiguous.
'joe.' => false,
// We can never allow these because they invalidate usernames as tokens
// in commit messages ("Reviewers: alincoln, usgrant"), or as parameters
// in URIs ("/p/alincoln/", "?user=alincoln"), or make them unsafe in
// HTML. Theoretically we escape all the HTML/URI stuff, but these
// restrictions make attacks more difficult and are generally reasonable,
// since usernames like "<^, ,^>" don't seem very important to support.
'<script>' => false,
'a lincoln' => false,
' alincoln' => false,
'alincoln ' => false,
'a,lincoln' => false,
'a&lincoln' => false,
'a/lincoln' => false,
"username\n" => false,
"user\nname" => false,
"\nusername" => false,
"username\r" => false,
"user\rname" => false,
"\rusername" => false,
);
foreach ($map as $name => $expect) {
$this->assertEqual(
$expect,
PhabricatorUser::validateUsername($name),
- "Validity of '{$name}'.");
+ pht("Validity of '%s'.", $name));
}
}
}
diff --git a/src/applications/people/typeahead/PhabricatorPeopleNoOwnerDatasource.php b/src/applications/people/typeahead/PhabricatorPeopleNoOwnerDatasource.php
index 90c887ac0..35f62e31d 100644
--- a/src/applications/people/typeahead/PhabricatorPeopleNoOwnerDatasource.php
+++ b/src/applications/people/typeahead/PhabricatorPeopleNoOwnerDatasource.php
@@ -1,77 +1,75 @@
<?php
final class PhabricatorPeopleNoOwnerDatasource
extends PhabricatorTypeaheadDatasource {
const FUNCTION_TOKEN = 'none()';
public function getBrowseTitle() {
return pht('Browse No Owner');
}
public function getPlaceholderText() {
return pht('Type "none"...');
}
public function getDatasourceApplicationClass() {
return 'PhabricatorPeopleApplication';
}
public function getDatasourceFunctions() {
return array(
'none' => array(
'name' => pht('No Owner'),
'summary' => pht('Find results which are not assigned.'),
'description' => pht(
- 'This function includes results which have no owner. Use a query '.
- 'like this to find unassigned results:'.
- "\n\n".
- '> none()'.
- "\n\n".
- 'If you combine this function with other functions, the query will '.
- 'return results which match the other selectors //or// have no '.
- 'owner. For example, this query will find results which are owned '.
- 'by `alincoln`, and will also find results which have no owner:'.
- "\n\n".
+ "This function includes results which have no owner. Use a query ".
+ "like this to find unassigned results:\n\n%s\n\n".
+ "If you combine this function with other functions, the query will ".
+ "return results which match the other selectors //or// have no ".
+ "owner. For example, this query will find results which are owned ".
+ "by `alincoln`, and will also find results which have no owner:".
+ "\n\n%s",
+ '> none()',
'> alincoln, none()'),
),
);
}
public function loadResults() {
$results = array(
$this->buildNoOwnerResult(),
);
return $this->filterResultsAgainstTokens($results);
}
protected function evaluateFunction($function, array $argv_list) {
$results = array();
foreach ($argv_list as $argv) {
$results[] = self::FUNCTION_TOKEN;
}
return $results;
}
public function renderFunctionTokens($function, array $argv_list) {
$results = array();
foreach ($argv_list as $argv) {
$results[] = PhabricatorTypeaheadTokenView::newFromTypeaheadResult(
$this->buildNoOwnerResult());
}
return $results;
}
private function buildNoOwnerResult() {
$name = pht('No Owner');
return $this->newFunctionResult()
->setName($name.' none')
->setDisplayName($name)
->setIcon('fa-ban')
->setPHID('none()')
->setUnique(true);
}
}
diff --git a/src/applications/people/typeahead/PhabricatorViewerDatasource.php b/src/applications/people/typeahead/PhabricatorViewerDatasource.php
index 5e08a7e70..1c9276ae8 100644
--- a/src/applications/people/typeahead/PhabricatorViewerDatasource.php
+++ b/src/applications/people/typeahead/PhabricatorViewerDatasource.php
@@ -1,81 +1,80 @@
<?php
final class PhabricatorViewerDatasource
extends PhabricatorTypeaheadDatasource {
public function getBrowseTitle() {
return pht('Browse Viewer');
}
public function getPlaceholderText() {
return pht('Type viewer()...');
}
public function getDatasourceApplicationClass() {
return 'PhabricatorPeopleApplication';
}
public function getDatasourceFunctions() {
return array(
'viewer' => array(
'name' => pht('Current Viewer'),
'summary' => pht('Use the current viewing user.'),
'description' => pht(
- 'This function allows you to change the behavior of a query '.
- 'based on who is running it. When you use this function, you will '.
- 'be the current viewer, so it works like you typed your own '.
- 'username.'.
- "\n\n".
- 'However, if you save a query using this function and send it '.
- 'to someone else, it will work like //their// username was the '.
- 'one that was typed. This can be useful for building dashboard '.
- 'panels that always show relevant information to the user who '.
- 'is looking at them.'),
+ "This function allows you to change the behavior of a query ".
+ "based on who is running it. When you use this function, you will ".
+ "be the current viewer, so it works like you typed your own ".
+ "username.\n\n".
+ "However, if you save a query using this function and send it ".
+ "to someone else, it will work like //their// username was the ".
+ "one that was typed. This can be useful for building dashboard ".
+ "panels that always show relevant information to the user who ".
+ "is looking at them."),
),
);
}
public function loadResults() {
if ($this->getViewer()->getPHID()) {
$results = array($this->renderViewerFunctionToken());
} else {
$results = array();
}
return $this->filterResultsAgainstTokens($results);
}
protected function canEvaluateFunction($function) {
if (!$this->getViewer()->getPHID()) {
return false;
}
return parent::canEvaluateFunction($function);
}
protected function evaluateFunction($function, array $argv_list) {
$results = array();
foreach ($argv_list as $argv) {
$results[] = $this->getViewer()->getPHID();
}
return $results;
}
public function renderFunctionTokens($function, array $argv_list) {
$tokens = array();
foreach ($argv_list as $argv) {
$tokens[] = PhabricatorTypeaheadTokenView::newFromTypeaheadResult(
$this->renderViewerFunctionToken());
}
return $tokens;
}
private function renderViewerFunctionToken() {
return $this->newFunctionResult()
->setName(pht('Current Viewer'))
->setPHID('viewer()')
->setIcon('fa-user')
->setUnique(true);
}
}
diff --git a/src/applications/phame/conduit/PhameQueryConduitAPIMethod.php b/src/applications/phame/conduit/PhameQueryConduitAPIMethod.php
index 8b882674a..d82afc2fa 100644
--- a/src/applications/phame/conduit/PhameQueryConduitAPIMethod.php
+++ b/src/applications/phame/conduit/PhameQueryConduitAPIMethod.php
@@ -1,78 +1,78 @@
<?php
final class PhameQueryConduitAPIMethod extends PhameConduitAPIMethod {
public function getAPIMethodName() {
return 'phame.query';
}
public function getMethodDescription() {
- return 'Query phame blogs.';
+ return pht('Query phame blogs.');
}
public function getMethodStatus() {
return self::METHOD_STATUS_UNSTABLE;
}
protected function defineParamTypes() {
return array(
'ids' => 'optional list<int>',
'phids' => 'optional list<phid>',
'after' => 'optional int',
'before' => 'optional int',
'limit' => 'optional int',
);
}
protected function defineReturnType() {
return 'list<dict>';
}
protected function execute(ConduitAPIRequest $request) {
$query = new PhameBlogQuery();
$query->setViewer($request->getUser());
$ids = $request->getValue('ids', array());
if ($ids) {
$query->withIDs($ids);
}
$phids = $request->getValue('phids', array());
if ($phids) {
$query->withPHIDs($phids);
}
$after = $request->getValue('after', null);
if ($after !== null) {
$query->setAfterID($after);
}
$before = $request->getValue('before', null);
if ($before !== null) {
$query->setBeforeID($before);
}
$limit = $request->getValue('limit', null);
if ($limit !== null) {
$query->setLimit($limit);
}
$blogs = $query->execute();
$results = array();
foreach ($blogs as $blog) {
$results[] = array(
'id' => $blog->getID(),
'phid' => $blog->getPHID(),
'name' => $blog->getName(),
'description' => $blog->getDescription(),
'domain' => $blog->getDomain(),
'creatorPHID' => $blog->getCreatorPHID(),
);
}
return $results;
}
}
diff --git a/src/applications/phame/conduit/PhameQueryPostsConduitAPIMethod.php b/src/applications/phame/conduit/PhameQueryPostsConduitAPIMethod.php
index 822c32a8d..06c20c772 100644
--- a/src/applications/phame/conduit/PhameQueryPostsConduitAPIMethod.php
+++ b/src/applications/phame/conduit/PhameQueryPostsConduitAPIMethod.php
@@ -1,103 +1,103 @@
<?php
final class PhameQueryPostsConduitAPIMethod extends PhameConduitAPIMethod {
public function getAPIMethodName() {
return 'phame.queryposts';
}
public function getMethodDescription() {
- return 'Query phame posts.';
+ return pht('Query phame posts.');
}
public function getMethodStatus() {
return self::METHOD_STATUS_UNSTABLE;
}
protected function defineParamTypes() {
return array(
'ids' => 'optional list<int>',
'phids' => 'optional list<phid>',
'blogPHIDs' => 'optional list<phid>',
'bloggerPHIDs' => 'optional list<phid>',
'phameTitles' => 'optional list<string>',
'published' => 'optional bool',
'publishedAfter' => 'optional date',
'before' => 'optional int',
'after' => 'optional int',
'limit' => 'optional int',
);
}
protected function defineReturnType() {
return 'list<dict>';
}
protected function execute(ConduitAPIRequest $request) {
$query = new PhamePostQuery();
$query->setViewer($request->getUser());
$ids = $request->getValue('ids', array());
if ($ids) {
$query->withIDs($ids);
}
$phids = $request->getValue('phids', array());
if ($phids) {
$query->withPHIDs($phids);
}
$blog_phids = $request->getValue('blogPHIDs', array());
if ($blog_phids) {
$query->withBlogPHIDs($blog_phids);
}
$blogger_phids = $request->getValue('bloggerPHIDs', array());
if ($blogger_phids) {
$query->withBloggerPHIDs($blogger_phids);
}
$phame_titles = $request->getValue('phameTitles', array());
if ($phame_titles) {
$query->withPhameTitles($phame_titles);
}
$published = $request->getValue('published', null);
if ($published === true) {
$query->withVisibility(PhamePost::VISIBILITY_PUBLISHED);
} else if ($published === false) {
$query->withVisibility(PhamePost::VISIBILITY_DRAFT);
}
$published_after = $request->getValue('publishedAfter', null);
if ($published_after !== null) {
$query->withPublishedAfter($published_after);
}
$after = $request->getValue('after', null);
if ($after !== null) {
$query->setAfterID($after);
}
$before = $request->getValue('before', null);
if ($before !== null) {
$query->setBeforeID($before);
}
$limit = $request->getValue('limit', null);
if ($limit !== null) {
$query->setLimit($limit);
}
$posts = $query->execute();
$results = array();
foreach ($posts as $post) {
$results[] = $post->toDictionary();
}
return $results;
}
}
diff --git a/src/applications/phame/controller/PhameController.php b/src/applications/phame/controller/PhameController.php
index a9ede9613..5e786e9d4 100644
--- a/src/applications/phame/controller/PhameController.php
+++ b/src/applications/phame/controller/PhameController.php
@@ -1,132 +1,136 @@
<?php
abstract class PhameController extends PhabricatorController {
protected function renderSideNavFilterView() {
$base_uri = new PhutilURI($this->getApplicationURI());
$nav = new AphrontSideNavFilterView();
$nav->setBaseURI($base_uri);
$nav->addLabel(pht('Create'));
$nav->addFilter('post/new', pht('New Post'));
$nav->addFilter('blog/new', pht('New Blog'));
$nav->addLabel(pht('Posts'));
$nav->addFilter('post/draft', pht('My Drafts'));
$nav->addFilter('post', pht('My Posts'));
$nav->addFilter('post/all', pht('All Posts'));
$nav->addLabel(pht('Blogs'));
$nav->addFilter('blog/user', pht('Joinable Blogs'));
$nav->addFilter('blog/all', pht('All Blogs'));
$nav->selectFilter(null);
return $nav;
}
protected function renderPostList(
array $posts,
PhabricatorUser $viewer,
$nodata) {
assert_instances_of($posts, 'PhamePost');
$handle_phids = array();
foreach ($posts as $post) {
$handle_phids[] = $post->getBloggerPHID();
if ($post->getBlog()) {
$handle_phids[] = $post->getBlog()->getPHID();
}
}
$handles = $viewer->loadHandles($handle_phids);
$stories = array();
foreach ($posts as $post) {
$blogger = $handles[$post->getBloggerPHID()]->renderLink();
$blogger_uri = $handles[$post->getBloggerPHID()]->getURI();
$blogger_image = $handles[$post->getBloggerPHID()]->getImageURI();
$blog = null;
if ($post->getBlog()) {
$blog = $handles[$post->getBlog()->getPHID()]->renderLink();
}
$phame_post = '';
if ($post->getBody()) {
$phame_post = PhabricatorMarkupEngine::summarize($post->getBody());
}
$blog_view = $post->getViewURI();
$phame_title = phutil_tag('a', array('href' => $blog_view),
$post->getTitle());
$blogger = phutil_tag('strong', array(), $blogger);
if ($post->isDraft()) {
- $title = pht('%s drafted a blog post on %s.',
- $blogger, $blog);
+ $title = pht(
+ '%s drafted a blog post on %s.',
+ $blogger,
+ $blog);
$title = phutil_tag('em', array(), $title);
} else {
- $title = pht('%s wrote a blog post on %s.',
- $blogger, $blog);
+ $title = pht(
+ '%s wrote a blog post on %s.',
+ $blogger,
+ $blog);
}
$item = id(new PHUIObjectItemView())
->setObject($post)
->setHeader($post->getTitle())
->setHref($this->getApplicationURI('post/view/'.$post->getID().'/'));
$story = id(new PHUIFeedStoryView())
->setTitle($title)
->setImage($blogger_image)
->setImageHref($blogger_uri)
->setAppIcon('fa-star')
->setUser($viewer)
->setPontification($phame_post, $phame_title);
if (PhabricatorPolicyFilter::hasCapability(
$viewer,
$post,
PhabricatorPolicyCapability::CAN_EDIT)) {
$story->addAction(id(new PHUIIconView())
->setHref($this->getApplicationURI('post/edit/'.$post->getID().'/'))
->setIconFont('fa-pencil'));
}
if ($post->getDatePublished()) {
$story->setEpoch($post->getDatePublished());
}
$stories[] = $story;
}
if (empty($stories)) {
return id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_NODATA)
->appendChild($nodata);
}
return $stories;
}
public function buildApplicationMenu() {
return $this->renderSideNavFilterView()->getMenu();
}
protected function buildApplicationCrumbs() {
$crumbs = parent::buildApplicationCrumbs();
$crumbs->addAction(
id(new PHUIListItemView())
->setName(pht('New Blog'))
->setHref($this->getApplicationURI('/blog/new'))
->setIcon('fa-plus-square'));
$crumbs->addAction(
id(new PHUIListItemView())
->setName(pht('New Post'))
->setHref($this->getApplicationURI('/post/new'))
->setIcon('fa-pencil'));
return $crumbs;
}
}
diff --git a/src/applications/phame/controller/blog/PhameBlogEditController.php b/src/applications/phame/controller/blog/PhameBlogEditController.php
index 8b1b3b81e..65eabeb05 100644
--- a/src/applications/phame/controller/blog/PhameBlogEditController.php
+++ b/src/applications/phame/controller/blog/PhameBlogEditController.php
@@ -1,191 +1,191 @@
<?php
final class PhameBlogEditController
extends PhameController {
public function handleRequest(AphrontRequest $request) {
$user = $request->getUser();
$id = $request->getURIData('id');
if ($id) {
$blog = id(new PhameBlogQuery())
->setViewer($user)
->withIDs(array($id))
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
if (!$blog) {
return new Aphront404Response();
}
$submit_button = pht('Save Changes');
$page_title = pht('Edit Blog');
$cancel_uri = $this->getApplicationURI('blog/view/'.$blog->getID().'/');
} else {
$blog = PhameBlog::initializeNewBlog($user);
$submit_button = pht('Create Blog');
$page_title = pht('Create Blog');
$cancel_uri = $this->getApplicationURI();
}
$name = $blog->getName();
$description = $blog->getDescription();
$custom_domain = $blog->getDomain();
$skin = $blog->getSkin();
$can_view = $blog->getViewPolicy();
$can_edit = $blog->getEditPolicy();
$can_join = $blog->getJoinPolicy();
$e_name = true;
$e_custom_domain = null;
$e_view_policy = null;
$validation_exception = null;
if ($request->isFormPost()) {
$name = $request->getStr('name');
$description = $request->getStr('description');
$custom_domain = nonempty($request->getStr('custom_domain'), null);
$skin = $request->getStr('skin');
$can_view = $request->getStr('can_view');
$can_edit = $request->getStr('can_edit');
$can_join = $request->getStr('can_join');
$xactions = array(
id(new PhameBlogTransaction())
->setTransactionType(PhameBlogTransaction::TYPE_NAME)
->setNewValue($name),
id(new PhameBlogTransaction())
->setTransactionType(PhameBlogTransaction::TYPE_DESCRIPTION)
->setNewValue($description),
id(new PhameBlogTransaction())
->setTransactionType(PhameBlogTransaction::TYPE_DOMAIN)
->setNewValue($custom_domain),
id(new PhameBlogTransaction())
->setTransactionType(PhameBlogTransaction::TYPE_SKIN)
->setNewValue($skin),
id(new PhameBlogTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_VIEW_POLICY)
->setNewValue($can_view),
id(new PhameBlogTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_EDIT_POLICY)
->setNewValue($can_edit),
id(new PhameBlogTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_JOIN_POLICY)
->setNewValue($can_join),
);
$editor = id(new PhameBlogEditor())
->setActor($user)
->setContentSourceFromRequest($request)
->setContinueOnNoEffect(true);
try {
$editor->applyTransactions($blog, $xactions);
return id(new AphrontRedirectResponse())
->setURI($this->getApplicationURI('blog/view/'.$blog->getID().'/'));
} catch (PhabricatorApplicationTransactionValidationException $ex) {
$validation_exception = $ex;
$e_name = $validation_exception->getShortMessage(
PhameBlogTransaction::TYPE_NAME);
$e_custom_domain = $validation_exception->getShortMessage(
PhameBlogTransaction::TYPE_DOMAIN);
$e_view_policy = $validation_exception->getShortMessage(
PhabricatorTransactions::TYPE_VIEW_POLICY);
}
}
$policies = id(new PhabricatorPolicyQuery())
->setViewer($user)
->setObject($blog)
->execute();
$skins = PhameSkinSpecification::loadAllSkinSpecifications();
$skins = mpull($skins, 'getName');
$form = id(new AphrontFormView())
->setUser($user)
->appendChild(
id(new AphrontFormTextControl())
->setLabel(pht('Name'))
->setName('name')
->setValue($name)
->setID('blog-name')
->setError($e_name))
->appendChild(
id(new PhabricatorRemarkupControl())
->setUser($user)
->setLabel(pht('Description'))
->setName('description')
->setValue($description)
->setID('blog-description')
->setUser($user)
->setDisableMacros(true))
->appendChild(
id(new AphrontFormPolicyControl())
->setUser($user)
->setCapability(PhabricatorPolicyCapability::CAN_VIEW)
->setPolicyObject($blog)
->setPolicies($policies)
->setError($e_view_policy)
->setValue($can_view)
->setName('can_view'))
->appendChild(
id(new AphrontFormPolicyControl())
->setUser($user)
->setCapability(PhabricatorPolicyCapability::CAN_EDIT)
->setPolicyObject($blog)
->setPolicies($policies)
->setValue($can_edit)
->setName('can_edit'))
->appendChild(
id(new AphrontFormPolicyControl())
->setUser($user)
->setCapability(PhabricatorPolicyCapability::CAN_JOIN)
->setPolicyObject($blog)
->setPolicies($policies)
->setValue($can_join)
->setName('can_join'))
->appendChild(
id(new AphrontFormTextControl())
->setLabel(pht('Custom Domain'))
->setName('custom_domain')
->setValue($custom_domain)
->setCaption(
- pht('Must include at least one dot (.), e.g. blog.example.com'))
+ pht('Must include at least one dot (.), e.g. %s', 'blog.example.com'))
->setError($e_custom_domain))
->appendChild(
id(new AphrontFormSelectControl())
->setLabel(pht('Skin'))
->setName('skin')
->setValue($skin)
->setOptions($skins))
->appendChild(
id(new AphrontFormSubmitControl())
->addCancelButton($cancel_uri)
->setValue($submit_button));
$form_box = id(new PHUIObjectBoxView())
->setHeaderText($page_title)
->setValidationException($validation_exception)
->setForm($form);
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb($page_title, $this->getApplicationURI('blog/new'));
$nav = $this->renderSideNavFilterView();
$nav->selectFilter($id ? null : 'blog/new');
$nav->appendChild(
array(
$crumbs,
$form_box,
));
return $this->buildApplicationPage(
$nav,
array(
'title' => $page_title,
));
}
}
diff --git a/src/applications/phame/controller/blog/PhameBlogListController.php b/src/applications/phame/controller/blog/PhameBlogListController.php
index f9a4aeb2d..9b1620317 100644
--- a/src/applications/phame/controller/blog/PhameBlogListController.php
+++ b/src/applications/phame/controller/blog/PhameBlogListController.php
@@ -1,78 +1,78 @@
<?php
final class PhameBlogListController extends PhameController {
public function handleRequest(AphrontRequest $request) {
$user = $request->getUser();
$nav = $this->renderSideNavFilterView(null);
$filter = $request->getURIData('filter');
$filter = $nav->selectFilter('blog/'.$filter, 'blog/user');
$query = id(new PhameBlogQuery())
->setViewer($user);
switch ($filter) {
case 'blog/all':
$title = pht('All Blogs');
$nodata = pht('No blogs have been created.');
break;
case 'blog/user':
$title = pht('Joinable Blogs');
$nodata = pht('There are no blogs you can contribute to.');
$query->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_JOIN,
));
break;
default:
- throw new Exception("Unknown filter '{$filter}'!");
+ throw new Exception(pht("Unknown filter '%s'!", $filter));
}
$pager = id(new AphrontPagerView())
->setURI($request->getRequestURI(), 'offset')
->setOffset($request->getInt('offset'));
$blogs = $query->executeWithOffsetPager($pager);
$blog_list = $this->renderBlogList($blogs, $user, $nodata);
$blog_list->setPager($pager);
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb($title, $this->getApplicationURI());
$nav->appendChild(
array(
$crumbs,
$blog_list,
));
return $this->buildApplicationPage(
$nav,
array(
'title' => $title,
));
}
private function renderBlogList(
array $blogs,
PhabricatorUser $user,
$nodata) {
$view = new PHUIObjectItemListView();
$view->setNoDataString($nodata);
$view->setUser($user);
foreach ($blogs as $blog) {
$item = id(new PHUIObjectItemView())
->setHeader($blog->getName())
->setHref($this->getApplicationURI('blog/view/'.$blog->getID().'/'))
->setObject($blog);
$view->addItem($item);
}
return $view;
}
}
diff --git a/src/applications/phame/controller/blog/PhameBlogViewController.php b/src/applications/phame/controller/blog/PhameBlogViewController.php
index 76491278b..a9fb9d73a 100644
--- a/src/applications/phame/controller/blog/PhameBlogViewController.php
+++ b/src/applications/phame/controller/blog/PhameBlogViewController.php
@@ -1,179 +1,179 @@
<?php
final class PhameBlogViewController extends PhameController {
public function handleRequest(AphrontRequest $request) {
$user = $request->getUser();
$id = $request->getURIData('id');
$blog = id(new PhameBlogQuery())
->setViewer($user)
->withIDs(array($id))
->executeOne();
if (!$blog) {
return new Aphront404Response();
}
$pager = id(new AphrontCursorPagerView())
->readFromRequest($request);
$posts = id(new PhamePostQuery())
->setViewer($user)
->withBlogPHIDs(array($blog->getPHID()))
->executeWithCursorPager($pager);
$nav = $this->renderSideNavFilterView(null);
$header = id(new PHUIHeaderView())
->setHeader($blog->getName())
->setUser($user)
->setPolicyObject($blog);
$actions = $this->renderActions($blog, $user);
$properties = $this->renderProperties($blog, $user, $actions);
$post_list = $this->renderPostList(
$posts,
$user,
pht('This blog has no visible posts.'));
require_celerity_resource('phame-css');
$post_list = id(new PHUIBoxView())
->addPadding(PHUI::PADDING_LARGE)
->addClass('phame-post-list')
->appendChild($post_list);
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb($blog->getName(), $this->getApplicationURI());
$object_box = id(new PHUIObjectBoxView())
->setHeader($header)
->addPropertyList($properties);
$nav->appendChild(
array(
$crumbs,
$object_box,
$post_list,
));
return $this->buildApplicationPage(
$nav,
array(
'title' => $blog->getName(),
));
}
private function renderProperties(
PhameBlog $blog,
PhabricatorUser $user,
PhabricatorActionListView $actions) {
require_celerity_resource('aphront-tooltip-css');
Javelin::initBehavior('phabricator-tooltips');
$properties = new PHUIPropertyListView();
$properties->setActionList($actions);
$properties->addProperty(
pht('Skin'),
$blog->getSkin());
$properties->addProperty(
pht('Domain'),
$blog->getDomain());
$feed_uri = PhabricatorEnv::getProductionURI(
$this->getApplicationURI('blog/feed/'.$blog->getID().'/'));
$properties->addProperty(
pht('Atom URI'),
javelin_tag('a',
array(
'href' => $feed_uri,
'sigil' => 'has-tooltip',
'meta' => array(
'tip' => pht('Atom URI does not support custom domains.'),
'size' => 320,
),
),
$feed_uri));
$descriptions = PhabricatorPolicyQuery::renderPolicyDescriptions(
$user,
$blog);
$properties->addProperty(
pht('Editable By'),
$descriptions[PhabricatorPolicyCapability::CAN_EDIT]);
$properties->addProperty(
pht('Joinable By'),
$descriptions[PhabricatorPolicyCapability::CAN_JOIN]);
$engine = id(new PhabricatorMarkupEngine())
->setViewer($user)
->addObject($blog, PhameBlog::MARKUP_FIELD_DESCRIPTION)
->process();
$properties->addTextContent(
phutil_tag(
'div',
array(
'class' => 'phabricator-remarkup',
),
$engine->getOutput($blog, PhameBlog::MARKUP_FIELD_DESCRIPTION)));
return $properties;
}
private function renderActions(PhameBlog $blog, PhabricatorUser $user) {
$actions = id(new PhabricatorActionListView())
->setObject($blog)
->setObjectURI($this->getRequest()->getRequestURI())
->setUser($user);
$can_edit = PhabricatorPolicyFilter::hasCapability(
$user,
$blog,
PhabricatorPolicyCapability::CAN_EDIT);
$can_join = PhabricatorPolicyFilter::hasCapability(
$user,
$blog,
PhabricatorPolicyCapability::CAN_JOIN);
$actions->addAction(
id(new PhabricatorActionView())
->setIcon('fa-plus')
->setHref($this->getApplicationURI('post/edit/?blog='.$blog->getID()))
->setName(pht('Write Post'))
->setDisabled(!$can_join)
->setWorkflow(!$can_join));
$actions->addAction(
id(new PhabricatorActionView())
->setUser($user)
->setIcon('fa-globe')
->setHref($blog->getLiveURI())
->setName(pht('View Live')));
$actions->addAction(
id(new PhabricatorActionView())
->setIcon('fa-pencil')
->setHref($this->getApplicationURI('blog/edit/'.$blog->getID().'/'))
- ->setName('Edit Blog')
+ ->setName(pht('Edit Blog'))
->setDisabled(!$can_edit)
->setWorkflow(!$can_edit));
$actions->addAction(
id(new PhabricatorActionView())
->setIcon('fa-times')
->setHref($this->getApplicationURI('blog/delete/'.$blog->getID().'/'))
- ->setName('Delete Blog')
+ ->setName(pht('Delete Blog'))
->setDisabled(!$can_edit)
->setWorkflow(true));
return $actions;
}
}
diff --git a/src/applications/phame/controller/post/PhamePostListController.php b/src/applications/phame/controller/post/PhamePostListController.php
index 23e9c985f..7c8231915 100644
--- a/src/applications/phame/controller/post/PhamePostListController.php
+++ b/src/applications/phame/controller/post/PhamePostListController.php
@@ -1,89 +1,89 @@
<?php
final class PhamePostListController extends PhameController {
private $bloggername;
private $filter;
public function willProcessRequest(array $data) {
$this->filter = idx($data, 'filter', 'blogger');
$this->bloggername = idx($data, 'bloggername');
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$query = id(new PhamePostQuery())
->setViewer($user);
$nav = $this->renderSideNavFilterView();
switch ($this->filter) {
case 'draft':
$query->withBloggerPHIDs(array($user->getPHID()));
$query->withVisibility(PhamePost::VISIBILITY_DRAFT);
$nodata = pht('You have no unpublished drafts.');
$title = pht('Unpublished Drafts');
$nav->selectFilter('post/draft');
break;
case 'blogger':
if ($this->bloggername) {
$blogger = id(new PhabricatorUser())->loadOneWhere(
'username = %s',
$this->bloggername);
if (!$blogger) {
return new Aphront404Response();
}
} else {
$blogger = $user;
}
$query->withBloggerPHIDs(array($blogger->getPHID()));
if ($blogger->getPHID() == $user->getPHID()) {
$nav->selectFilter('post');
$nodata = pht('You have not written any posts.');
} else {
$nodata = pht('%s has not written any posts.', $blogger);
}
$title = pht('Posts By %s', $blogger);
break;
case 'all':
$nodata = pht('There are no visible posts.');
$title = pht('Posts');
$nav->selectFilter('post/all');
break;
default:
- throw new Exception("Unknown filter '{$this->filter}'!");
+ throw new Exception(pht("Unknown filter '%s'!", $this->filter));
}
$pager = id(new AphrontCursorPagerView())
->readFromRequest($request);
$posts = $query->executeWithCursorPager($pager);
require_celerity_resource('phame-css');
$post_list = $this->renderPostList($posts, $user, $nodata);
$post_list = id(new PHUIBoxView())
->addPadding(PHUI::PADDING_LARGE)
->addClass('phame-post-list')
->appendChild($post_list);
$crumbs = $this->buildApplicationCrumbs();
$crumbs->setBorder(true);
$crumbs->addTextCrumb($title, $this->getApplicationURI());
$nav->appendChild(
array(
$crumbs,
$post_list,
));
return $this->buildApplicationPage(
$nav,
array(
'title' => $title,
));
}
}
diff --git a/src/applications/phame/controller/post/PhamePostViewController.php b/src/applications/phame/controller/post/PhamePostViewController.php
index c1f640537..1b2b8c1a3 100644
--- a/src/applications/phame/controller/post/PhamePostViewController.php
+++ b/src/applications/phame/controller/post/PhamePostViewController.php
@@ -1,192 +1,194 @@
<?php
final class PhamePostViewController extends PhameController {
public function handleRequest(AphrontRequest $request) {
$user = $request->getUser();
$post = id(new PhamePostQuery())
->setViewer($user)
->withIDs(array($request->getURIData('id')))
->executeOne();
if (!$post) {
return new Aphront404Response();
}
$nav = $this->renderSideNavFilterView();
$actions = $this->renderActions($post, $user);
$properties = $this->renderProperties($post, $user, $actions);
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb(
$post->getTitle(),
$this->getApplicationURI('post/view/'.$post->getID().'/'));
$nav->appendChild($crumbs);
$header = id(new PHUIHeaderView())
->setHeader($post->getTitle())
->setUser($user)
->setPolicyObject($post);
$object_box = id(new PHUIObjectBoxView())
->setHeader($header)
->addPropertyList($properties);
if ($post->isDraft()) {
$object_box->appendChild(
id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_NOTICE)
->setTitle(pht('Draft Post'))
->appendChild(
- pht('Only you can see this draft until you publish it. '.
- 'Use "Preview / Publish" to publish this post.')));
+ pht(
+ 'Only you can see this draft until you publish it. '.
+ 'Use "Preview / Publish" to publish this post.')));
}
if (!$post->getBlog()) {
$object_box->appendChild(
id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_WARNING)
->setTitle(pht('Not On A Blog'))
->appendChild(
- pht('This post is not associated with a blog (the blog may have '.
- 'been deleted). Use "Move Post" to move it to a new blog.')));
+ pht(
+ 'This post is not associated with a blog (the blog may have '.
+ 'been deleted). Use "Move Post" to move it to a new blog.')));
}
$nav->appendChild(
array(
$object_box,
$this->buildTransactionTimeline(
$post,
new PhamePostTransactionQuery()),
));
return $this->buildApplicationPage(
$nav,
array(
'title' => $post->getTitle(),
));
}
private function renderActions(
PhamePost $post,
PhabricatorUser $user) {
$actions = id(new PhabricatorActionListView())
->setObject($post)
->setObjectURI($this->getRequest()->getRequestURI())
->setUser($user);
$can_edit = PhabricatorPolicyFilter::hasCapability(
$user,
$post,
PhabricatorPolicyCapability::CAN_EDIT);
$id = $post->getID();
$actions->addAction(
id(new PhabricatorActionView())
->setIcon('fa-pencil')
->setHref($this->getApplicationURI('post/edit/'.$id.'/'))
->setName(pht('Edit Post'))
->setDisabled(!$can_edit)
->setWorkflow(!$can_edit));
$actions->addAction(
id(new PhabricatorActionView())
->setIcon('fa-arrows')
->setHref($this->getApplicationURI('post/move/'.$id.'/'))
->setName(pht('Move Post'))
->setDisabled(!$can_edit)
->setWorkflow(!$can_edit));
if ($post->isDraft()) {
$actions->addAction(
id(new PhabricatorActionView())
->setIcon('fa-eye')
->setHref($this->getApplicationURI('post/publish/'.$id.'/'))
->setName(pht('Preview / Publish')));
} else {
$actions->addAction(
id(new PhabricatorActionView())
->setIcon('fa-eye-slash')
->setHref($this->getApplicationURI('post/unpublish/'.$id.'/'))
->setName(pht('Unpublish'))
->setWorkflow(true));
}
$actions->addAction(
id(new PhabricatorActionView())
->setIcon('fa-times')
->setHref($this->getApplicationURI('post/delete/'.$id.'/'))
->setName(pht('Delete Post'))
->setDisabled(!$can_edit)
->setWorkflow(true));
$blog = $post->getBlog();
$can_view_live = $blog && !$post->isDraft();
if ($can_view_live) {
$live_uri = $blog->getLiveURI($post);
} else {
$live_uri = 'post/notlive/'.$post->getID().'/';
$live_uri = $this->getApplicationURI($live_uri);
}
$actions->addAction(
id(new PhabricatorActionView())
->setUser($user)
->setIcon('fa-globe')
->setHref($live_uri)
->setName(pht('View Live'))
->setDisabled(!$can_view_live)
->setWorkflow(!$can_view_live));
return $actions;
}
private function renderProperties(
PhamePost $post,
PhabricatorUser $user,
PhabricatorActionListView $actions) {
$properties = id(new PHUIPropertyListView())
->setUser($user)
->setObject($post)
->setActionList($actions);
$properties->addProperty(
pht('Blog'),
$user->renderHandle($post->getBlogPHID()));
$properties->addProperty(
pht('Blogger'),
$user->renderHandle($post->getBloggerPHID()));
$properties->addProperty(
pht('Published'),
$post->isDraft()
? pht('Draft')
: phabricator_datetime($post->getDatePublished(), $user));
$engine = id(new PhabricatorMarkupEngine())
->setViewer($user)
->addObject($post, PhamePost::MARKUP_FIELD_BODY)
->process();
$properties->invokeWillRenderEvent();
$properties->addTextContent(
phutil_tag(
'div',
array(
'class' => 'phabricator-remarkup',
),
$engine->getOutput($post, PhamePost::MARKUP_FIELD_BODY)));
return $properties;
}
}
diff --git a/src/applications/phame/skins/PhameSkinSpecification.php b/src/applications/phame/skins/PhameSkinSpecification.php
index 506f89a81..7dedf8ab4 100644
--- a/src/applications/phame/skins/PhameSkinSpecification.php
+++ b/src/applications/phame/skins/PhameSkinSpecification.php
@@ -1,193 +1,197 @@
<?php
final class PhameSkinSpecification {
const TYPE_ADVANCED = 'advanced';
const TYPE_BASIC = 'basic';
private $type;
private $rootDirectory;
private $skinClass;
private $phutilLibraries = array();
private $name;
private $config;
public static function loadAllSkinSpecifications() {
static $specs;
if ($specs === null) {
$paths = PhabricatorEnv::getEnvConfig('phame.skins');
$base = dirname(phutil_get_library_root('phabricator'));
$specs = array();
foreach ($paths as $path) {
$path = Filesystem::resolvePath($path, $base);
foreach (Filesystem::listDirectory($path) as $skin_directory) {
$skin_path = $path.DIRECTORY_SEPARATOR.$skin_directory;
if (!is_dir($skin_path)) {
continue;
}
$spec = self::loadSkinSpecification($skin_path);
if (!$spec) {
continue;
}
$name = trim($skin_directory, DIRECTORY_SEPARATOR);
$spec->setName($name);
if (isset($specs[$name])) {
$that_dir = $specs[$name]->getRootDirectory();
$this_dir = $spec->getRootDirectory();
throw new Exception(
- "Two skins have the same name ('{$name}'), in '{$this_dir}' and ".
- "'{$that_dir}'. Rename one or adjust your 'phame.skins' ".
- "configuration.");
+ pht(
+ "Two skins have the same name ('%s'), in '%s' and '%s'. ".
+ "Rename one or adjust your '%s' configuration.",
+ $name,
+ $this_dir,
+ $that_dir,
+ 'phame.skins'));
}
$specs[$name] = $spec;
}
}
}
return $specs;
}
public static function loadOneSkinSpecification($name) {
// Only allow skins which we know to exist to load. This prevents loading
// skins like "../../secrets/evil/".
$all = self::loadAllSkinSpecifications();
if (empty($all[$name])) {
throw new Exception(
pht(
'Blog skin "%s" is not a valid skin!',
$name));
}
$paths = PhabricatorEnv::getEnvConfig('phame.skins');
$base = dirname(phutil_get_library_root('phabricator'));
foreach ($paths as $path) {
$path = Filesystem::resolvePath($path, $base);
$skin_path = $path.DIRECTORY_SEPARATOR.$name;
if (is_dir($skin_path)) {
// Double check that the skin really lives in the skin directory.
if (!Filesystem::isDescendant($skin_path, $path)) {
throw new Exception(
pht(
'Blog skin "%s" is not located in path "%s"!',
$name,
$path));
}
$spec = self::loadSkinSpecification($skin_path);
if ($spec) {
$spec->setName($name);
return $spec;
}
}
}
return null;
}
private static function loadSkinSpecification($path) {
$config_path = $path.DIRECTORY_SEPARATOR.'skin.json';
$config = array();
if (Filesystem::pathExists($config_path)) {
$config = Filesystem::readFile($config_path);
try {
$config = phutil_json_decode($config);
} catch (PhutilJSONParserException $ex) {
throw new PhutilProxyException(
pht(
"Skin configuration file '%s' is not a valid JSON file.",
$config_path),
$ex);
}
$type = idx($config, 'type', self::TYPE_BASIC);
} else {
$type = self::TYPE_BASIC;
}
$spec = new PhameSkinSpecification();
$spec->setRootDirectory($path);
$spec->setConfig($config);
switch ($type) {
case self::TYPE_BASIC:
$spec->setSkinClass('PhameBasicTemplateBlogSkin');
break;
case self::TYPE_ADVANCED:
$spec->setSkinClass($config['class']);
$spec->addPhutilLibrary($path.DIRECTORY_SEPARATOR.'src');
break;
default:
- throw new Exception('Unknown skin type!');
+ throw new Exception(pht('Unknown skin type!'));
}
$spec->setType($type);
return $spec;
}
public function setConfig(array $config) {
$this->config = $config;
return $this;
}
public function getConfig($key, $default = null) {
return idx($this->config, $key, $default);
}
public function setName($name) {
$this->name = $name;
return $this;
}
public function getName() {
return $this->getConfig('name', $this->name);
}
public function setRootDirectory($root_directory) {
$this->rootDirectory = $root_directory;
return $this;
}
public function getRootDirectory() {
return $this->rootDirectory;
}
public function setType($type) {
$this->type = $type;
return $this;
}
public function getType() {
return $this->type;
}
public function setSkinClass($skin_class) {
$this->skinClass = $skin_class;
return $this;
}
public function getSkinClass() {
return $this->skinClass;
}
public function addPhutilLibrary($library) {
$this->phutilLibraries[] = $library;
return $this;
}
public function buildSkin(AphrontRequest $request) {
foreach ($this->phutilLibraries as $library) {
phutil_load_library($library);
}
return newv($this->getSkinClass(), array($request, $this));
}
}
diff --git a/src/applications/phame/storage/PhameBlog.php b/src/applications/phame/storage/PhameBlog.php
index 1c4017fbd..349d40d77 100644
--- a/src/applications/phame/storage/PhameBlog.php
+++ b/src/applications/phame/storage/PhameBlog.php
@@ -1,304 +1,305 @@
<?php
final class PhameBlog extends PhameDAO
implements PhabricatorPolicyInterface, PhabricatorMarkupInterface {
const MARKUP_FIELD_DESCRIPTION = 'markup:description';
const SKIN_DEFAULT = 'oblivious';
protected $name;
protected $description;
protected $domain;
protected $configData;
protected $creatorPHID;
protected $viewPolicy;
protected $editPolicy;
protected $joinPolicy;
static private $requestBlog;
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'configData' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'name' => 'text64',
'description' => 'text',
'domain' => 'text128?',
// T6203/NULLABILITY
// These policies should always be non-null.
'joinPolicy' => 'policy?',
'editPolicy' => 'policy?',
'viewPolicy' => 'policy?',
),
self::CONFIG_KEY_SCHEMA => array(
'key_phid' => null,
'phid' => array(
'columns' => array('phid'),
'unique' => true,
),
'domain' => array(
'columns' => array('domain'),
'unique' => true,
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhabricatorPhameBlogPHIDType::TYPECONST);
}
public static function initializeNewBlog(PhabricatorUser $actor) {
$blog = id(new PhameBlog())
->setCreatorPHID($actor->getPHID())
->setViewPolicy(PhabricatorPolicies::getMostOpenPolicy())
->setEditPolicy(PhabricatorPolicies::POLICY_USER)
->setJoinPolicy(PhabricatorPolicies::POLICY_USER);
return $blog;
}
public function getSkinRenderer(AphrontRequest $request) {
$spec = PhameSkinSpecification::loadOneSkinSpecification(
$this->getSkin());
if (!$spec) {
$spec = PhameSkinSpecification::loadOneSkinSpecification(
self::SKIN_DEFAULT);
}
if (!$spec) {
throw new Exception(
- 'This blog has an invalid skin, and the default skin failed to '.
- 'load.');
+ pht(
+ 'This blog has an invalid skin, and the default skin failed to '.
+ 'load.'));
}
$skin = newv($spec->getSkinClass(), array());
$skin->setRequest($request);
$skin->setSpecification($spec);
return $skin;
}
/**
* Makes sure a given custom blog uri is properly configured in DNS
* to point at this Phabricator instance. If there is an error in
* the configuration, return a string describing the error and how
* to fix it. If there is no error, return an empty string.
*
* @return string
*/
public function validateCustomDomain($custom_domain) {
$example_domain = 'blog.example.com';
$label = pht('Invalid');
// note this "uri" should be pretty busted given the desired input
// so just use it to test if there's a protocol specified
$uri = new PhutilURI($custom_domain);
if ($uri->getProtocol()) {
return array(
$label,
pht(
'The custom domain should not include a protocol. Just provide '.
'the bare domain name (for example, "%s").',
$example_domain),
);
}
if ($uri->getPort()) {
return array(
$label,
pht(
'The custom domain should not include a port number. Just provide '.
'the bare domain name (for example, "%s").',
$example_domain),
);
}
if (strpos($custom_domain, '/') !== false) {
return array(
$label,
pht(
'The custom domain should not specify a path (hosting a Phame '.
'blog at a path is currently not supported). Instead, just provide '.
'the bare domain name (for example, "%s").',
$example_domain),
);
}
if (strpos($custom_domain, '.') === false) {
return array(
$label,
pht(
'The custom domain should contain at least one dot (.) because '.
'some browsers fail to set cookies on domains without a dot. '.
'Instead, use a normal looking domain name like "%s".',
$example_domain),
);
}
if (!PhabricatorEnv::getEnvConfig('policy.allow-public')) {
$href = PhabricatorEnv::getProductionURI(
'/config/edit/policy.allow-public/');
return array(
pht('Fix Configuration'),
pht(
'For custom domains to work, this Phabricator instance must be '.
'configured to allow the public access policy. Configure this '.
'setting %s, or ask an administrator to configure this setting. '.
'The domain can be specified later once this setting has been '.
'changed.',
phutil_tag(
'a',
array('href' => $href),
pht('here'))),
);
}
return null;
}
public function getSkin() {
$config = coalesce($this->getConfigData(), array());
return idx($config, 'skin', self::SKIN_DEFAULT);
}
public function setSkin($skin) {
$config = coalesce($this->getConfigData(), array());
$config['skin'] = $skin;
return $this->setConfigData($config);
}
static public function getSkinOptionsForSelect() {
$classes = id(new PhutilSymbolLoader())
->setAncestorClass('PhameBlogSkin')
->setType('class')
->setConcreteOnly(true)
->selectSymbolsWithoutLoading();
return ipull($classes, 'name', 'name');
}
public static function setRequestBlog(PhameBlog $blog) {
self::$requestBlog = $blog;
}
public static function getRequestBlog() {
return self::$requestBlog;
}
public function getLiveURI(PhamePost $post = null) {
if ($this->getDomain()) {
$base = new PhutilURI('http://'.$this->getDomain().'/');
} else {
$base = '/phame/live/'.$this->getID().'/';
$base = PhabricatorEnv::getURI($base);
}
if ($post) {
$base .= '/post/'.$post->getPhameTitle();
}
return $base;
}
/* -( PhabricatorPolicyInterface Implementation )-------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
PhabricatorPolicyCapability::CAN_JOIN,
);
}
public function getPolicy($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
return $this->getViewPolicy();
case PhabricatorPolicyCapability::CAN_EDIT:
return $this->getEditPolicy();
case PhabricatorPolicyCapability::CAN_JOIN:
return $this->getJoinPolicy();
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $user) {
$can_edit = PhabricatorPolicyCapability::CAN_EDIT;
$can_join = PhabricatorPolicyCapability::CAN_JOIN;
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
// Users who can edit or post to a blog can always view it.
if (PhabricatorPolicyFilter::hasCapability($user, $this, $can_edit)) {
return true;
}
if (PhabricatorPolicyFilter::hasCapability($user, $this, $can_join)) {
return true;
}
break;
case PhabricatorPolicyCapability::CAN_JOIN:
// Users who can edit a blog can always post to it.
if (PhabricatorPolicyFilter::hasCapability($user, $this, $can_edit)) {
return true;
}
break;
}
return false;
}
public function describeAutomaticCapability($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
return pht(
'Users who can edit or post on a blog can always view it.');
case PhabricatorPolicyCapability::CAN_JOIN:
return pht(
'Users who can edit a blog can always post on it.');
}
return null;
}
/* -( PhabricatorMarkupInterface Implementation )-------------------------- */
public function getMarkupFieldKey($field) {
$hash = PhabricatorHash::digest($this->getMarkupText($field));
return $this->getPHID().':'.$field.':'.$hash;
}
public function newMarkupEngine($field) {
return PhabricatorMarkupEngine::newPhameMarkupEngine();
}
public function getMarkupText($field) {
return $this->getDescription();
}
public function didMarkupText(
$field,
$output,
PhutilMarkupEngine $engine) {
return $output;
}
public function shouldUseMarkupCache($field) {
return (bool)$this->getPHID();
}
}
diff --git a/src/applications/phame/storage/PhamePost.php b/src/applications/phame/storage/PhamePost.php
index 8a02a7c56..e44e9615d 100644
--- a/src/applications/phame/storage/PhamePost.php
+++ b/src/applications/phame/storage/PhamePost.php
@@ -1,290 +1,290 @@
<?php
final class PhamePost extends PhameDAO
implements
PhabricatorPolicyInterface,
PhabricatorMarkupInterface,
PhabricatorApplicationTransactionInterface,
PhabricatorTokenReceiverInterface {
const MARKUP_FIELD_BODY = 'markup:body';
const MARKUP_FIELD_SUMMARY = 'markup:summary';
const VISIBILITY_DRAFT = 0;
const VISIBILITY_PUBLISHED = 1;
protected $bloggerPHID;
protected $title;
protected $phameTitle;
protected $body;
protected $visibility;
protected $configData;
protected $datePublished;
protected $blogPHID;
private $blog;
public static function initializePost(
PhabricatorUser $blogger,
PhameBlog $blog) {
$post = id(new PhamePost())
->setBloggerPHID($blogger->getPHID())
->setBlogPHID($blog->getPHID())
->setBlog($blog)
->setDatePublished(0)
->setVisibility(self::VISIBILITY_DRAFT);
return $post;
}
public function setBlog(PhameBlog $blog) {
$this->blog = $blog;
return $this;
}
public function getBlog() {
return $this->blog;
}
public function getViewURI() {
// go for the pretty uri if we can
$domain = ($this->blog ? $this->blog->getDomain() : '');
if ($domain) {
$phame_title = PhabricatorSlug::normalize($this->getPhameTitle());
return 'http://'.$domain.'/post/'.$phame_title;
}
$uri = '/phame/post/view/'.$this->getID().'/';
return PhabricatorEnv::getProductionURI($uri);
}
public function getEditURI() {
return '/phame/post/edit/'.$this->getID().'/';
}
public function isDraft() {
return $this->getVisibility() == self::VISIBILITY_DRAFT;
}
public function getHumanName() {
if ($this->isDraft()) {
$name = 'draft';
} else {
$name = 'post';
}
return $name;
}
public function setCommentsWidget($widget) {
$config_data = $this->getConfigData();
$config_data['comments_widget'] = $widget;
return $this;
}
public function getCommentsWidget() {
$config_data = $this->getConfigData();
if (empty($config_data)) {
return 'none';
}
return idx($config_data, 'comments_widget', 'none');
}
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'configData' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'title' => 'text255',
'phameTitle' => 'sort64',
'visibility' => 'uint32',
// T6203/NULLABILITY
// These seem like they should always be non-null?
'blogPHID' => 'phid?',
'body' => 'text?',
'configData' => 'text?',
// T6203/NULLABILITY
// This one probably should be nullable?
'datePublished' => 'epoch',
),
self::CONFIG_KEY_SCHEMA => array(
'key_phid' => null,
'phid' => array(
'columns' => array('phid'),
'unique' => true,
),
'phameTitle' => array(
'columns' => array('bloggerPHID', 'phameTitle'),
'unique' => true,
),
'bloggerPosts' => array(
'columns' => array(
'bloggerPHID',
'visibility',
'datePublished',
'id',
),
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhabricatorPhamePostPHIDType::TYPECONST);
}
public function toDictionary() {
return array(
'id' => $this->getID(),
'phid' => $this->getPHID(),
'blogPHID' => $this->getBlogPHID(),
'bloggerPHID' => $this->getBloggerPHID(),
'viewURI' => $this->getViewURI(),
'title' => $this->getTitle(),
'phameTitle' => $this->getPhameTitle(),
'body' => $this->getBody(),
'summary' => PhabricatorMarkupEngine::summarize($this->getBody()),
'datePublished' => $this->getDatePublished(),
'published' => !$this->isDraft(),
);
}
public static function getVisibilityOptionsForSelect() {
return array(
- self::VISIBILITY_DRAFT => 'Draft: visible only to me.',
- self::VISIBILITY_PUBLISHED => 'Published: visible to the whole world.',
+ self::VISIBILITY_DRAFT => pht('Draft: visible only to me.'),
+ self::VISIBILITY_PUBLISHED => pht(
+ 'Published: visible to the whole world.'),
);
}
public function getCommentsWidgetOptionsForSelect() {
$current = $this->getCommentsWidget();
$options = array();
if ($current == 'facebook' ||
PhabricatorFacebookAuthProvider::getFacebookApplicationID()) {
$options['facebook'] = 'Facebook';
}
if ($current == 'disqus' ||
PhabricatorEnv::getEnvConfig('disqus.shortname')) {
$options['disqus'] = 'Disqus';
}
$options['none'] = 'None';
return $options;
}
/* -( PhabricatorPolicyInterface Implementation )-------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
// Draft posts are visible only to the author. Published posts are visible
// to whoever the blog is visible to.
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
if (!$this->isDraft() && $this->getBlog()) {
return $this->getBlog()->getViewPolicy();
} else {
return PhabricatorPolicies::POLICY_NOONE;
}
break;
case PhabricatorPolicyCapability::CAN_EDIT:
return PhabricatorPolicies::POLICY_NOONE;
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $user) {
// A blog post's author can always view it, and is the only user allowed
// to edit it.
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
case PhabricatorPolicyCapability::CAN_EDIT:
return ($user->getPHID() == $this->getBloggerPHID());
}
}
public function describeAutomaticCapability($capability) {
- return pht(
- 'The author of a blog post can always view and edit it.');
+ return pht('The author of a blog post can always view and edit it.');
}
/* -( PhabricatorMarkupInterface Implementation )-------------------------- */
public function getMarkupFieldKey($field) {
$hash = PhabricatorHash::digest($this->getMarkupText($field));
return $this->getPHID().':'.$field.':'.$hash;
}
public function newMarkupEngine($field) {
return PhabricatorMarkupEngine::newPhameMarkupEngine();
}
public function getMarkupText($field) {
switch ($field) {
case self::MARKUP_FIELD_BODY:
return $this->getBody();
case self::MARKUP_FIELD_SUMMARY:
return PhabricatorMarkupEngine::summarize($this->getBody());
}
}
public function didMarkupText(
$field,
$output,
PhutilMarkupEngine $engine) {
return $output;
}
public function shouldUseMarkupCache($field) {
return (bool)$this->getPHID();
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new PhamePostEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new PhamePostTransaction();
}
public function willRenderTimeline(
PhabricatorApplicationTransactionView $timeline,
AphrontRequest $request) {
return $timeline;
}
/* -( PhabricatorTokenReceiverInterface )---------------------------------- */
public function getUsersToNotifyOfTokenGiven() {
return array(
$this->getBloggerPHID(),
);
}
}
diff --git a/src/applications/phid/conduit/PHIDInfoConduitAPIMethod.php b/src/applications/phid/conduit/PHIDInfoConduitAPIMethod.php
index 465697c60..76afb1ef5 100644
--- a/src/applications/phid/conduit/PHIDInfoConduitAPIMethod.php
+++ b/src/applications/phid/conduit/PHIDInfoConduitAPIMethod.php
@@ -1,52 +1,52 @@
<?php
final class PHIDInfoConduitAPIMethod extends PHIDConduitAPIMethod {
public function getAPIMethodName() {
return 'phid.info';
}
public function getMethodStatus() {
return self::METHOD_STATUS_DEPRECATED;
}
public function getMethodStatusDescription() {
- return "Replaced by 'phid.query'.";
+ return pht("Replaced by 'phid.query'.");
}
public function getMethodDescription() {
- return 'Retrieve information about an arbitrary PHID.';
+ return pht('Retrieve information about an arbitrary PHID.');
}
protected function defineParamTypes() {
return array(
'phid' => 'required phid',
);
}
protected function defineReturnType() {
return 'nonempty dict<string, wild>';
}
protected function defineErrorTypes() {
return array(
- 'ERR-BAD-PHID' => 'No such object exists.',
+ 'ERR-BAD-PHID' => pht('No such object exists.'),
);
}
protected function execute(ConduitAPIRequest $request) {
$phid = $request->getValue('phid');
$handle = id(new PhabricatorHandleQuery())
->setViewer($request->getUser())
->withPHIDs(array($phid))
->executeOne();
if (!$handle->isComplete()) {
throw new ConduitException('ERR-BAD-PHID');
}
return $this->buildHandleInformationDictionary($handle);
}
}
diff --git a/src/applications/phid/conduit/PHIDLookupConduitAPIMethod.php b/src/applications/phid/conduit/PHIDLookupConduitAPIMethod.php
index 73196daaf..f70afb993 100644
--- a/src/applications/phid/conduit/PHIDLookupConduitAPIMethod.php
+++ b/src/applications/phid/conduit/PHIDLookupConduitAPIMethod.php
@@ -1,47 +1,47 @@
<?php
final class PHIDLookupConduitAPIMethod extends PHIDConduitAPIMethod {
public function getAPIMethodName() {
return 'phid.lookup';
}
public function getMethodDescription() {
- return 'Look up objects by name.';
+ return pht('Look up objects by name.');
}
protected function defineParamTypes() {
return array(
'names' => 'required list<string>',
);
}
protected function defineReturnType() {
return 'nonempty dict<string, wild>';
}
protected function execute(ConduitAPIRequest $request) {
$names = $request->getValue('names');
$query = id(new PhabricatorObjectQuery())
->setViewer($request->getUser())
->withNames($names);
$query->execute();
$name_map = $query->getNamedResults();
$handles = id(new PhabricatorHandleQuery())
->setViewer($request->getUser())
->withPHIDs(mpull($name_map, 'getPHID'))
->execute();
$result = array();
foreach ($name_map as $name => $object) {
$phid = $object->getPHID();
$handle = $handles[$phid];
$result[$name] = $this->buildHandleInformationDictionary($handle);
}
return $result;
}
}
diff --git a/src/applications/phid/conduit/PHIDQueryConduitAPIMethod.php b/src/applications/phid/conduit/PHIDQueryConduitAPIMethod.php
index 69b6fe0ac..420d6a2f9 100644
--- a/src/applications/phid/conduit/PHIDQueryConduitAPIMethod.php
+++ b/src/applications/phid/conduit/PHIDQueryConduitAPIMethod.php
@@ -1,41 +1,41 @@
<?php
final class PHIDQueryConduitAPIMethod extends PHIDConduitAPIMethod {
public function getAPIMethodName() {
return 'phid.query';
}
public function getMethodDescription() {
- return 'Retrieve information about arbitrary PHIDs.';
+ return pht('Retrieve information about arbitrary PHIDs.');
}
protected function defineParamTypes() {
return array(
'phids' => 'required list<phid>',
);
}
protected function defineReturnType() {
return 'nonempty dict<string, wild>';
}
protected function execute(ConduitAPIRequest $request) {
$phids = $request->getValue('phids');
$handles = id(new PhabricatorHandleQuery())
->setViewer($request->getUser())
->withPHIDs($phids)
->execute();
$result = array();
foreach ($handles as $phid => $handle) {
if ($handle->isComplete()) {
$result[$phid] = $this->buildHandleInformationDictionary($handle);
}
}
return $result;
}
}
diff --git a/src/applications/phid/storage/PhabricatorPHID.php b/src/applications/phid/storage/PhabricatorPHID.php
index 43896045b..d3c43bfc4 100644
--- a/src/applications/phid/storage/PhabricatorPHID.php
+++ b/src/applications/phid/storage/PhabricatorPHID.php
@@ -1,27 +1,27 @@
<?php
final class PhabricatorPHID {
protected $phid;
protected $phidType;
protected $ownerPHID;
protected $parentPHID;
public static function generateNewPHID($type, $subtype = null) {
if (!$type) {
- throw new Exception('Can not generate PHID with no type.');
+ throw new Exception(pht('Can not generate PHID with no type.'));
}
if ($subtype === null) {
$uniq_len = 20;
$type_str = "{$type}";
} else {
$uniq_len = 15;
$type_str = "{$type}-{$subtype}";
}
$uniq = Filesystem::readRandomCharacters($uniq_len);
return "PHID-{$type_str}-{$uniq}";
}
}
diff --git a/src/applications/phid/type/PhabricatorPHIDType.php b/src/applications/phid/type/PhabricatorPHIDType.php
index fd64ab7bd..62107233e 100644
--- a/src/applications/phid/type/PhabricatorPHIDType.php
+++ b/src/applications/phid/type/PhabricatorPHIDType.php
@@ -1,235 +1,239 @@
<?php
abstract class PhabricatorPHIDType {
final public function getTypeConstant() {
$class = new ReflectionClass($this);
$const = $class->getConstant('TYPECONST');
if ($const === false) {
throw new Exception(
pht(
- 'PHIDType class "%s" must define an TYPECONST property.',
- get_class($this)));
+ '%s class "%s" must define a %s property.',
+ __CLASS__,
+ get_class($this),
+ 'TYPECONST'));
}
if (!is_string($const) || !preg_match('/^[A-Z]{4}$/', $const)) {
throw new Exception(
pht(
- 'PHIDType class "%s" has an invalid TYPECONST property. PHID '.
+ '%s class "%s" has an invalid %s property. PHID '.
'constants must be a four character uppercase string.',
- get_class($this)));
+ __CLASS__,
+ get_class($this),
+ 'TYPECONST'));
}
return $const;
}
abstract public function getTypeName();
public function newObject() {
return null;
}
public function getTypeIcon() {
// Default to the application icon if the type doesn't specify one.
$application_class = $this->getPHIDTypeApplicationClass();
if ($application_class) {
$application = newv($application_class, array());
return $application->getFontIcon();
}
return null;
}
/**
* Get the class name for the application this type belongs to.
*
* @return string|null Class name of the corresponding application, or null
* if the type is not bound to an application.
*/
public function getPHIDTypeApplicationClass() {
// TODO: Some day this should probably be abstract, but for now it only
// affects global search and there's no real burning need to go classify
// every PHID type.
return null;
}
/**
* Build a @{class:PhabricatorPolicyAwareQuery} to load objects of this type
* by PHID.
*
* If you can not build a single query which satisfies this requirement, you
* can provide a dummy implementation for this method and overload
* @{method:loadObjects} instead.
*
* @param PhabricatorObjectQuery Query being executed.
* @param list<phid> PHIDs to load.
* @return PhabricatorPolicyAwareQuery Query object which loads the
* specified PHIDs when executed.
*/
abstract protected function buildQueryForObjects(
PhabricatorObjectQuery $query,
array $phids);
/**
* Load objects of this type, by PHID. For most PHID types, it is only
* necessary to implement @{method:buildQueryForObjects} to get object
* loading to work.
*
* @param PhabricatorObjectQuery Query being executed.
* @param list<phid> PHIDs to load.
* @return list<wild> Corresponding objects.
*/
public function loadObjects(
PhabricatorObjectQuery $query,
array $phids) {
$object_query = $this->buildQueryForObjects($query, $phids)
->setViewer($query->getViewer())
->setParentQuery($query);
// If the user doesn't have permission to use the application at all,
// just mark all the PHIDs as filtered. This primarily makes these
// objects show up as "Restricted" instead of "Unknown" when loaded as
// handles, which is technically true.
if (!$object_query->canViewerUseQueryApplication()) {
$object_query->addPolicyFilteredPHIDs(array_fuse($phids));
return array();
}
return $object_query->execute();
}
/**
* Populate provided handles with application-specific data, like titles and
* URIs.
*
* NOTE: The `$handles` and `$objects` lists are guaranteed to be nonempty
* and have the same keys: subclasses are expected to load information only
* for handles with visible objects.
*
* Because of this guarantee, a safe implementation will typically look like*
*
* foreach ($handles as $phid => $handle) {
* $object = $objects[$phid];
*
* $handle->setStuff($object->getStuff());
* // ...
* }
*
* In general, an implementation should call `setName()` and `setURI()` on
* each handle at a minimum. See @{class:PhabricatorObjectHandle} for other
* handle properties.
*
* @param PhabricatorHandleQuery Issuing query object.
* @param list<PhabricatorObjectHandle> Handles to populate with data.
* @param list<Object> Objects for these PHIDs loaded by
* @{method:buildQueryForObjects()}.
* @return void
*/
abstract public function loadHandles(
PhabricatorHandleQuery $query,
array $handles,
array $objects);
public function canLoadNamedObject($name) {
return false;
}
public function loadNamedObjects(
PhabricatorObjectQuery $query,
array $names) {
throw new PhutilMethodNotImplementedException();
}
/**
* Get all known PHID types.
*
* To get PHID types a given user has access to, see
* @{method:getAllInstalledTypes}.
*
* @return dict<string, PhabricatorPHIDType> Map of type constants to types.
*/
public static function getAllTypes() {
static $types;
if ($types === null) {
$objects = id(new PhutilSymbolLoader())
->setAncestorClass(__CLASS__)
->loadObjects();
$map = array();
$original = array();
foreach ($objects as $object) {
$type = $object->getTypeConstant();
if (isset($map[$type])) {
$that_class = $original[$type];
$this_class = get_class($object);
throw new Exception(
pht(
"Two %s classes (%s, %s) both handle PHID type '%s'. ".
"A type may be handled by only one class.",
__CLASS__,
$that_class,
$this_class,
$type));
}
$original[$type] = get_class($object);
$map[$type] = $object;
}
$types = $map;
}
return $types;
}
/**
* Get all PHID types of applications installed for a given viewer.
*
* @param PhabricatorUser Viewing user.
* @return dict<string, PhabricatorPHIDType> Map of constants to installed
* types.
*/
public static function getAllInstalledTypes(PhabricatorUser $viewer) {
$all_types = self::getAllTypes();
$installed_types = array();
$app_classes = array();
foreach ($all_types as $key => $type) {
$app_class = $type->getPHIDTypeApplicationClass();
if ($app_class === null) {
// If the PHID type isn't bound to an application, include it as
// installed.
$installed_types[$key] = $type;
continue;
}
// Otherwise, we need to check if this application is installed before
// including the PHID type.
$app_classes[$app_class][$key] = $type;
}
if ($app_classes) {
$apps = id(new PhabricatorApplicationQuery())
->setViewer($viewer)
->withInstalled(true)
->withClasses(array_keys($app_classes))
->execute();
foreach ($apps as $app_class => $app) {
$installed_types += $app_classes[$app_class];
}
}
return $installed_types;
}
}
diff --git a/src/applications/pholio/controller/PholioInlineController.php b/src/applications/pholio/controller/PholioInlineController.php
index 782383cdb..4cc5510b9 100644
--- a/src/applications/pholio/controller/PholioInlineController.php
+++ b/src/applications/pholio/controller/PholioInlineController.php
@@ -1,173 +1,173 @@
<?php
final class PholioInlineController extends PholioController {
private $id;
public function willProcessRequest(array $data) {
$this->id = idx($data, 'id');
}
public function processRequest() {
$request = $this->getRequest();
$viewer = $request->getUser();
if ($this->id) {
$inline = id(new PholioTransactionComment())->load($this->id);
if (!$inline) {
return new Aphront404Response();
}
if ($inline->getTransactionPHID()) {
$mode = 'view';
} else {
if ($inline->getAuthorPHID() == $viewer->getPHID()) {
$mode = 'edit';
} else {
return new Aphront404Response();
}
}
} else {
$mock = id(new PholioMockQuery())
->setViewer($viewer)
->withIDs(array($request->getInt('mockID')))
->executeOne();
if (!$mock) {
return new Aphront404Response();
}
$inline = id(new PholioTransactionComment())
->setImageID($request->getInt('imageID'))
->setX($request->getInt('startX'))
->setY($request->getInt('startY'))
->setCommentVersion(1)
->setAuthorPHID($viewer->getPHID())
->setEditPolicy($viewer->getPHID())
->setViewPolicy(PhabricatorPolicies::POLICY_PUBLIC)
->setContentSourceFromRequest($request)
->setWidth($request->getInt('endX') - $request->getInt('startX'))
->setHeight($request->getInt('endY') - $request->getInt('startY'));
$mode = 'new';
}
$v_content = $inline->getContent();
// TODO: Not correct, but we don't always have a mock right now.
$mock_uri = '/';
if ($mode == 'view') {
require_celerity_resource('pholio-inline-comments-css');
$image = id(new PholioImageQuery())
->setViewer($viewer)
->withIDs(array($inline->getImageID()))
->executeOne();
$handles = $this->loadViewerHandles(array($inline->getAuthorPHID()));
$author_handle = $handles[$inline->getAuthorPHID()];
$file = $image->getFile();
if (!$file->isViewableImage()) {
- throw new Exception('File is not viewable.');
+ throw new Exception(pht('File is not viewable.'));
}
$image_uri = $file->getBestURI();
$thumb = id(new PHUIImageMaskView())
->addClass('mrl')
->setImage($image_uri)
->setDisplayHeight(200)
->setDisplayWidth(498)
->withMask(true)
->centerViewOnPoint(
$inline->getX(), $inline->getY(),
$inline->getHeight(), $inline->getWidth());
$comment_head = phutil_tag(
'div',
array(
'class' => 'pholio-inline-comment-head',
),
$author_handle->renderLink());
$comment_body = phutil_tag(
'div',
array(
'class' => 'pholio-inline-comment-body',
),
PhabricatorMarkupEngine::renderOneObject(
id(new PhabricatorMarkupOneOff())
->setContent($inline->getContent()),
'default',
$viewer));
return $this->newDialog()
->setTitle(pht('Inline Comment'))
->appendChild($thumb)
->appendChild($comment_head)
->appendChild($comment_body)
->addCancelButton($mock_uri, pht('Close'));
}
if ($request->isFormPost()) {
$v_content = $request->getStr('content');
if (strlen($v_content)) {
$inline->setContent($v_content);
$inline->save();
$dictionary = $inline->toDictionary();
} else if ($inline->getID()) {
$inline->delete();
$dictionary = array();
}
return id(new AphrontAjaxResponse())->setContent($dictionary);
}
switch ($mode) {
case 'edit':
$title = pht('Edit Inline Comment');
$submit_text = pht('Save Draft');
break;
case 'new':
$title = pht('New Inline Comment');
$submit_text = pht('Save Draft');
break;
}
$form = id(new AphrontFormView())
->setUser($viewer);
if ($mode == 'new') {
$params = array(
'mockID' => $request->getInt('mockID'),
'imageID' => $request->getInt('imageID'),
'startX' => $request->getInt('startX'),
'startY' => $request->getInt('startY'),
'endX' => $request->getInt('endX'),
'endY' => $request->getInt('endY'),
);
foreach ($params as $key => $value) {
$form->addHiddenInput($key, $value);
}
}
$form
->appendChild(
id(new PhabricatorRemarkupControl())
->setUser($viewer)
->setName('content')
->setLabel(pht('Comment'))
->setValue($v_content));
return $this->newDialog()
->setTitle($title)
->setWidth(AphrontDialogView::WIDTH_FORM)
->appendChild($form->buildLayoutView())
->addCancelButton($mock_uri)
->addSubmitButton($submit_text);
}
}
diff --git a/src/applications/pholio/controller/PholioMockEditController.php b/src/applications/pholio/controller/PholioMockEditController.php
index 7cfa0b35b..17abba6e6 100644
--- a/src/applications/pholio/controller/PholioMockEditController.php
+++ b/src/applications/pholio/controller/PholioMockEditController.php
@@ -1,395 +1,395 @@
<?php
final class PholioMockEditController extends PholioController {
private $id;
public function willProcessRequest(array $data) {
$this->id = idx($data, 'id');
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
if ($this->id) {
$mock = id(new PholioMockQuery())
->setViewer($user)
->needImages(true)
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->withIDs(array($this->id))
->executeOne();
if (!$mock) {
return new Aphront404Response();
}
$title = pht('Edit Mock');
$is_new = false;
$mock_images = $mock->getImages();
$files = mpull($mock_images, 'getFile');
$mock_images = mpull($mock_images, null, 'getFilePHID');
} else {
$mock = PholioMock::initializeNewMock($user);
$title = pht('Create Mock');
$is_new = true;
$files = array();
$mock_images = array();
}
if ($is_new) {
$v_projects = array();
} else {
$v_projects = PhabricatorEdgeQuery::loadDestinationPHIDs(
$mock->getPHID(),
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST);
$v_projects = array_reverse($v_projects);
}
$e_name = true;
$e_images = count($mock_images) ? null : true;
$errors = array();
$posted_mock_images = array();
$v_name = $mock->getName();
$v_desc = $mock->getDescription();
$v_status = $mock->getStatus();
$v_view = $mock->getViewPolicy();
$v_edit = $mock->getEditPolicy();
$v_cc = PhabricatorSubscribersQuery::loadSubscribersForPHID(
$mock->getPHID());
if ($request->isFormPost()) {
$xactions = array();
$type_name = PholioTransactionType::TYPE_NAME;
$type_desc = PholioTransactionType::TYPE_DESCRIPTION;
$type_status = PholioTransactionType::TYPE_STATUS;
$type_view = PhabricatorTransactions::TYPE_VIEW_POLICY;
$type_edit = PhabricatorTransactions::TYPE_EDIT_POLICY;
$type_cc = PhabricatorTransactions::TYPE_SUBSCRIBERS;
$v_name = $request->getStr('name');
$v_desc = $request->getStr('description');
$v_status = $request->getStr('status');
$v_view = $request->getStr('can_view');
$v_edit = $request->getStr('can_edit');
$v_cc = $request->getArr('cc');
$v_projects = $request->getArr('projects');
$mock_xactions = array();
$mock_xactions[$type_name] = $v_name;
$mock_xactions[$type_desc] = $v_desc;
$mock_xactions[$type_status] = $v_status;
$mock_xactions[$type_view] = $v_view;
$mock_xactions[$type_edit] = $v_edit;
$mock_xactions[$type_cc] = array('=' => $v_cc);
if (!strlen($request->getStr('name'))) {
$e_name = 'Required';
$errors[] = pht('You must give the mock a name.');
}
$file_phids = $request->getArr('file_phids');
if ($file_phids) {
$files = id(new PhabricatorFileQuery())
->setViewer($user)
->withPHIDs($file_phids)
->execute();
$files = mpull($files, null, 'getPHID');
$files = array_select_keys($files, $file_phids);
} else {
$files = array();
}
if (!$files) {
$e_images = pht('Required');
$errors[] = pht('You must add at least one image to the mock.');
} else {
$mock->setCoverPHID(head($files)->getPHID());
}
foreach ($mock_xactions as $type => $value) {
$xactions[$type] = id(new PholioTransaction())
->setTransactionType($type)
->setNewValue($value);
}
$order = $request->getStrList('imageOrder');
$sequence_map = array_flip($order);
$replaces = $request->getArr('replaces');
$replaces_map = array_flip($replaces);
/**
* Foreach file posted, check to see whether we are replacing an image,
* adding an image, or simply updating image metadata. Create
* transactions for these cases as appropos.
*/
foreach ($files as $file_phid => $file) {
$replaces_image_phid = null;
if (isset($replaces_map[$file_phid])) {
$old_file_phid = $replaces_map[$file_phid];
if ($old_file_phid != $file_phid) {
$old_image = idx($mock_images, $old_file_phid);
if ($old_image) {
$replaces_image_phid = $old_image->getPHID();
}
}
}
$existing_image = idx($mock_images, $file_phid);
$title = (string)$request->getStr('title_'.$file_phid);
$description = (string)$request->getStr('description_'.$file_phid);
$sequence = $sequence_map[$file_phid];
if ($replaces_image_phid) {
$replace_image = id(new PholioImage())
->setReplacesImagePHID($replaces_image_phid)
->setFilePhid($file_phid)
->attachFile($file)
->setName(strlen($title) ? $title : $file->getName())
->setDescription($description)
->setSequence($sequence);
$xactions[] = id(new PholioTransaction())
->setTransactionType(
PholioTransactionType::TYPE_IMAGE_REPLACE)
->setNewValue($replace_image);
$posted_mock_images[] = $replace_image;
} else if (!$existing_image) { // this is an add
$add_image = id(new PholioImage())
->setFilePhid($file_phid)
->attachFile($file)
->setName(strlen($title) ? $title : $file->getName())
->setDescription($description)
->setSequence($sequence);
$xactions[] = id(new PholioTransaction())
->setTransactionType(PholioTransactionType::TYPE_IMAGE_FILE)
->setNewValue(
array('+' => array($add_image)));
$posted_mock_images[] = $add_image;
} else {
$xactions[] = id(new PholioTransaction())
->setTransactionType(PholioTransactionType::TYPE_IMAGE_NAME)
->setNewValue(
array($existing_image->getPHID() => $title));
$xactions[] = id(new PholioTransaction())
->setTransactionType(
PholioTransactionType::TYPE_IMAGE_DESCRIPTION)
->setNewValue(
array($existing_image->getPHID() => $description));
$xactions[] = id(new PholioTransaction())
->setTransactionType(
PholioTransactionType::TYPE_IMAGE_SEQUENCE)
->setNewValue(
array($existing_image->getPHID() => $sequence));
$posted_mock_images[] = $existing_image;
}
}
foreach ($mock_images as $file_phid => $mock_image) {
if (!isset($files[$file_phid]) && !isset($replaces[$file_phid])) {
// this is an outright delete
$xactions[] = id(new PholioTransaction())
->setTransactionType(PholioTransactionType::TYPE_IMAGE_FILE)
->setNewValue(
array('-' => array($mock_image)));
}
}
if (!$errors) {
$proj_edge_type = PhabricatorProjectObjectHasProjectEdgeType::EDGECONST;
$xactions[] = id(new PholioTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
->setMetadataValue('edge:type', $proj_edge_type)
->setNewValue(array('=' => array_fuse($v_projects)));
$mock->openTransaction();
$editor = id(new PholioMockEditor())
->setContentSourceFromRequest($request)
->setContinueOnNoEffect(true)
->setActor($user);
$xactions = $editor->applyTransactions($mock, $xactions);
$mock->saveTransaction();
return id(new AphrontRedirectResponse())
->setURI('/M'.$mock->getID());
}
}
if ($this->id) {
$submit = id(new AphrontFormSubmitControl())
->addCancelButton('/M'.$this->id)
->setValue(pht('Save'));
} else {
$submit = id(new AphrontFormSubmitControl())
->addCancelButton($this->getApplicationURI())
->setValue(pht('Create'));
}
$policies = id(new PhabricatorPolicyQuery())
->setViewer($user)
->setObject($mock)
->execute();
// NOTE: Make this show up correctly on the rendered form.
$mock->setViewPolicy($v_view);
$mock->setEditPolicy($v_edit);
$image_elements = array();
if ($posted_mock_images) {
$display_mock_images = $posted_mock_images;
} else {
$display_mock_images = $mock_images;
}
foreach ($display_mock_images as $mock_image) {
$image_elements[] = id(new PholioUploadedImageView())
->setUser($user)
->setImage($mock_image)
->setReplacesPHID($mock_image->getFilePHID());
}
$list_id = celerity_generate_unique_node_id();
$drop_id = celerity_generate_unique_node_id();
$order_id = celerity_generate_unique_node_id();
$list_control = phutil_tag(
'div',
array(
'id' => $list_id,
'class' => 'pholio-edit-list',
),
$image_elements);
$drop_control = phutil_tag(
'div',
array(
'id' => $drop_id,
'class' => 'pholio-edit-drop',
),
- 'Drag and drop images here to add them to the mock.');
+ pht('Drag and drop images here to add them to the mock.'));
$order_control = phutil_tag(
'input',
array(
'type' => 'hidden',
'name' => 'imageOrder',
'id' => $order_id,
));
Javelin::initBehavior(
'pholio-mock-edit',
array(
'listID' => $list_id,
'dropID' => $drop_id,
'orderID' => $order_id,
'uploadURI' => '/file/dropupload/',
'renderURI' => $this->getApplicationURI('image/upload/'),
'pht' => array(
'uploading' => pht('Uploading Image...'),
'uploaded' => pht('Upload Complete...'),
'undo' => pht('Undo'),
'removed' => pht('This image will be removed from the mock.'),
),
));
require_celerity_resource('pholio-edit-css');
$form = id(new AphrontFormView())
->setUser($user)
->appendChild($order_control)
->appendChild(
id(new AphrontFormTextControl())
->setName('name')
->setValue($v_name)
->setLabel(pht('Name'))
->setError($e_name))
->appendChild(
id(new PhabricatorRemarkupControl())
->setName('description')
->setValue($v_desc)
->setLabel(pht('Description'))
->setUser($user));
if ($this->id) {
$form->appendChild(
id(new AphrontFormSelectControl())
->setLabel(pht('Status'))
->setName('status')
->setValue($mock->getStatus())
->setOptions($mock->getStatuses()));
} else {
$form->addHiddenInput('status', 'open');
}
$form
->appendControl(
id(new AphrontFormTokenizerControl())
->setLabel(pht('Projects'))
->setName('projects')
->setValue($v_projects)
->setDatasource(new PhabricatorProjectDatasource()))
->appendControl(
id(new AphrontFormTokenizerControl())
->setLabel(pht('CC'))
->setName('cc')
->setValue($v_cc)
->setUser($user)
->setDatasource(new PhabricatorMetaMTAMailableDatasource()))
->appendChild(
id(new AphrontFormPolicyControl())
->setUser($user)
->setCapability(PhabricatorPolicyCapability::CAN_VIEW)
->setPolicyObject($mock)
->setPolicies($policies)
->setName('can_view'))
->appendChild(
id(new AphrontFormPolicyControl())
->setUser($user)
->setCapability(PhabricatorPolicyCapability::CAN_EDIT)
->setPolicyObject($mock)
->setPolicies($policies)
->setName('can_edit'))
->appendChild(
id(new AphrontFormMarkupControl())
->setValue($list_control))
->appendChild(
id(new AphrontFormMarkupControl())
->setValue($drop_control)
->setError($e_images))
->appendChild($submit);
$form_box = id(new PHUIObjectBoxView())
->setHeaderText($title)
->setFormErrors($errors)
->setForm($form);
$crumbs = $this->buildApplicationCrumbs();
if (!$is_new) {
$crumbs->addTextCrumb($mock->getMonogram(), '/'.$mock->getMonogram());
}
$crumbs->addTextCrumb($title);
$content = array(
$crumbs,
$form_box,
);
$this->addExtraQuicksandConfig(
array('mockEditConfig' => true));
return $this->buildApplicationPage(
$content,
array(
'title' => $title,
));
}
}
diff --git a/src/applications/pholio/mail/PholioReplyHandler.php b/src/applications/pholio/mail/PholioReplyHandler.php
index 3f20ea637..1109d901f 100644
--- a/src/applications/pholio/mail/PholioReplyHandler.php
+++ b/src/applications/pholio/mail/PholioReplyHandler.php
@@ -1,16 +1,16 @@
<?php
final class PholioReplyHandler
extends PhabricatorApplicationTransactionReplyHandler {
public function validateMailReceiver($mail_receiver) {
if (!($mail_receiver instanceof PholioMock)) {
- throw new Exception('Mail receiver is not a PholioMock!');
+ throw new Exception(pht('Mail receiver is not a %s!', 'PholioMock'));
}
}
public function getObjectPrefix() {
return 'M';
}
}
diff --git a/src/applications/pholio/view/PholioMockImagesView.php b/src/applications/pholio/view/PholioMockImagesView.php
index c1bcb4623..cf8d0c614 100644
--- a/src/applications/pholio/view/PholioMockImagesView.php
+++ b/src/applications/pholio/view/PholioMockImagesView.php
@@ -1,230 +1,230 @@
<?php
final class PholioMockImagesView extends AphrontView {
private $mock;
private $imageID;
private $requestURI;
private $commentFormID;
private $panelID;
private $viewportID;
private $behaviorConfig;
public function setCommentFormID($comment_form_id) {
$this->commentFormID = $comment_form_id;
return $this;
}
public function getCommentFormID() {
return $this->commentFormID;
}
public function setRequestURI(PhutilURI $request_uri) {
$this->requestURI = $request_uri;
return $this;
}
public function getRequestURI() {
return $this->requestURI;
}
public function setImageID($image_id) {
$this->imageID = $image_id;
return $this;
}
public function getImageID() {
return $this->imageID;
}
public function setMock(PholioMock $mock) {
$this->mock = $mock;
return $this;
}
public function getMock() {
return $this->mock;
}
public function __construct() {
$this->panelID = celerity_generate_unique_node_id();
$this->viewportID = celerity_generate_unique_node_id();
}
public function getBehaviorConfig() {
if (!$this->getMock()) {
- throw new Exception('Call setMock() before getBehaviorConfig()!');
+ throw new PhutilInvalidStateException('setMock');
}
if ($this->behaviorConfig === null) {
$this->behaviorConfig = $this->calculateBehaviorConfig();
}
return $this->behaviorConfig;
}
private function calculateBehaviorConfig() {
$mock = $this->getMock();
// TODO: We could maybe do a better job with tailoring this, which is the
// image shown on the review stage.
$default_name = 'image-100x100.png';
$builtins = PhabricatorFile::loadBuiltins(
$this->getUser(),
array($default_name));
$default = $builtins[$default_name];
$engine = id(new PhabricatorMarkupEngine())
->setViewer($this->getUser());
foreach ($mock->getAllImages() as $image) {
$engine->addObject($image, 'default');
}
$engine->process();
$images = array();
$current_set = 0;
foreach ($mock->getAllImages() as $image) {
$file = $image->getFile();
$metadata = $file->getMetadata();
$x = idx($metadata, PhabricatorFile::METADATA_IMAGE_WIDTH);
$y = idx($metadata, PhabricatorFile::METADATA_IMAGE_HEIGHT);
$is_obs = (bool)$image->getIsObsolete();
if (!$is_obs) {
$current_set++;
}
$history_uri = '/pholio/image/history/'.$image->getID().'/';
$images[] = array(
'id' => $image->getID(),
'fullURI' => $file->getBestURI(),
'stageURI' => ($file->isViewableImage()
? $file->getBestURI()
: $default->getBestURI()),
'pageURI' => $this->getImagePageURI($image, $mock),
'downloadURI' => $file->getDownloadURI(),
'historyURI' => $history_uri,
'width' => $x,
'height' => $y,
'title' => $image->getName(),
'descriptionMarkup' => $engine->getOutput($image, 'default'),
'isObsolete' => (bool)$image->getIsObsolete(),
'isImage' => $file->isViewableImage(),
'isViewable' => $file->isViewableInBrowser(),
);
}
$ids = mpull($mock->getImages(), 'getID');
if ($this->imageID && isset($ids[$this->imageID])) {
$selected_id = $this->imageID;
} else {
$selected_id = head_key($ids);
}
$navsequence = array();
foreach ($mock->getImages() as $image) {
$navsequence[] = $image->getID();
}
$full_icon = array(
javelin_tag('span', array('aural' => true), pht('View Raw File')),
id(new PHUIIconView())->setIconFont('fa-file-image-o'),
);
$download_icon = array(
javelin_tag('span', array('aural' => true), pht('Download File')),
id(new PHUIIconView())->setIconFont('fa-download'),
);
$login_uri = id(new PhutilURI('/login/'))
->setQueryParam('next', (string)$this->getRequestURI());
$config = array(
'mockID' => $mock->getID(),
'panelID' => $this->panelID,
'viewportID' => $this->viewportID,
'commentFormID' => $this->getCommentFormID(),
'images' => $images,
'selectedID' => $selected_id,
'loggedIn' => $this->getUser()->isLoggedIn(),
'logInLink' => (string)$login_uri,
'navsequence' => $navsequence,
'fullIcon' => hsprintf('%s', $full_icon),
'downloadIcon' => hsprintf('%s', $download_icon),
'currentSetSize' => $current_set,
);
return $config;
}
public function render() {
if (!$this->getMock()) {
- throw new Exception('Call setMock() before render()!');
+ throw new PhutilInvalidStateException('setMock');
}
$mock = $this->getMock();
require_celerity_resource('javelin-behavior-pholio-mock-view');
$panel_id = $this->panelID;
$viewport_id = $this->viewportID;
$config = $this->getBehaviorConfig();
Javelin::initBehavior(
'pholio-mock-view',
$this->getBehaviorConfig());
$mockview = '';
$mock_wrapper = javelin_tag(
'div',
array(
'id' => $this->viewportID,
'sigil' => 'mock-viewport',
'class' => 'pholio-mock-image-viewport',
),
'');
$image_header = javelin_tag(
'div',
array(
'id' => 'mock-image-header',
'class' => 'pholio-mock-image-header',
),
'');
$mock_wrapper = javelin_tag(
'div',
array(
'id' => $this->panelID,
'sigil' => 'mock-panel touchable',
'class' => 'pholio-mock-image-panel',
),
array(
$image_header,
$mock_wrapper,
));
$inline_comments_holder = javelin_tag(
'div',
array(
'id' => 'mock-image-description',
'sigil' => 'mock-image-description',
'class' => 'mock-image-description',
),
'');
$mockview[] = phutil_tag(
'div',
array(
'class' => 'pholio-mock-image-container',
'id' => 'pholio-mock-image-container',
),
array($mock_wrapper, $inline_comments_holder));
return $mockview;
}
private function getImagePageURI(PholioImage $image, PholioMock $mock) {
$uri = '/M'.$mock->getID().'/'.$image->getID().'/';
return $uri;
}
}
diff --git a/src/applications/pholio/view/PholioTransactionView.php b/src/applications/pholio/view/PholioTransactionView.php
index 50d764910..cf2f44a3a 100644
--- a/src/applications/pholio/view/PholioTransactionView.php
+++ b/src/applications/pholio/view/PholioTransactionView.php
@@ -1,140 +1,140 @@
<?php
final class PholioTransactionView
extends PhabricatorApplicationTransactionView {
private $mock;
public function setMock($mock) {
$this->mock = $mock;
return $this;
}
public function getMock() {
return $this->mock;
}
protected function shouldGroupTransactions(
PhabricatorApplicationTransaction $u,
PhabricatorApplicationTransaction $v) {
if ($u->getAuthorPHID() != $v->getAuthorPHID()) {
// Don't group transactions by different authors.
return false;
}
if (($v->getDateCreated() - $u->getDateCreated()) > 60) {
// Don't group if transactions happened more than 60s apart.
return false;
}
switch ($u->getTransactionType()) {
case PhabricatorTransactions::TYPE_COMMENT:
case PholioTransactionType::TYPE_INLINE:
break;
default:
return false;
}
switch ($v->getTransactionType()) {
case PholioTransactionType::TYPE_INLINE:
return true;
}
return parent::shouldGroupTransactions($u, $v);
}
protected function renderTransactionContent(
PhabricatorApplicationTransaction $xaction) {
$out = array();
$group = $xaction->getTransactionGroup();
if ($xaction->getTransactionType() == PholioTransactionType::TYPE_INLINE) {
array_unshift($group, $xaction);
} else {
$out[] = parent::renderTransactionContent($xaction);
}
if (!$group) {
return $out;
}
$inlines = array();
foreach ($group as $xaction) {
switch ($xaction->getTransactionType()) {
case PholioTransactionType::TYPE_INLINE:
$inlines[] = $xaction;
break;
default:
- throw new Exception('Unknown grouped transaction type!');
+ throw new Exception(pht('Unknown grouped transaction type!'));
}
}
if ($inlines) {
$icon = id(new PHUIIconView())
->setIconFont('fa-comment bluegrey msr');
$header = phutil_tag(
'div',
array(
'class' => 'phabricator-transaction-subheader',
),
array($icon, pht('Inline Comments')));
$out[] = $header;
foreach ($inlines as $inline) {
if (!$inline->getComment()) {
continue;
}
$out[] = $this->renderInlineContent($inline);
}
}
return $out;
}
private function renderInlineContent(PholioTransaction $inline) {
$comment = $inline->getComment();
$mock = $this->getMock();
$images = $mock->getAllImages();
$images = mpull($images, null, 'getID');
$image = idx($images, $comment->getImageID());
if (!$image) {
- throw new Exception('No image attached!');
+ throw new Exception(pht('No image attached!'));
}
$file = $image->getFile();
if (!$file->isViewableImage()) {
- throw new Exception('File is not viewable.');
+ throw new Exception(pht('File is not viewable.'));
}
$image_uri = $file->getBestURI();
$thumb = id(new PHUIImageMaskView())
->addClass('mrl')
->setImage($image_uri)
->setDisplayHeight(100)
->setDisplayWidth(200)
->withMask(true)
->centerViewOnPoint(
$comment->getX(), $comment->getY(),
$comment->getHeight(), $comment->getWidth());
$link = phutil_tag(
'a',
array(
'href' => '#',
'class' => 'pholio-transaction-inline-image-anchor',
),
$thumb);
$inline_comment = parent::renderTransactionContent($inline);
return phutil_tag(
'div',
array('class' => 'pholio-transaction-inline-comment'),
array($link, $inline_comment));
}
}
diff --git a/src/applications/phortune/control/PhortuneMonthYearExpiryControl.php b/src/applications/phortune/control/PhortuneMonthYearExpiryControl.php
index b9c48cd59..73f26bc26 100644
--- a/src/applications/phortune/control/PhortuneMonthYearExpiryControl.php
+++ b/src/applications/phortune/control/PhortuneMonthYearExpiryControl.php
@@ -1,86 +1,86 @@
<?php
final class PhortuneMonthYearExpiryControl extends AphrontFormControl {
private $monthValue;
private $yearValue;
public function setMonthInputValue($value) {
$this->monthValue = $value;
return $this;
}
private function getMonthInputValue() {
return $this->monthValue;
}
private function getCurrentMonth() {
return phabricator_format_local_time(
time(),
$this->getUser(),
'm');
}
public function setYearInputValue($value) {
$this->yearValue = $value;
return $this;
}
private function getYearInputValue() {
return $this->yearValue;
}
private function getCurrentYear() {
return phabricator_format_local_time(
time(),
$this->getUser(),
'Y');
}
protected function getCustomControlClass() {
return 'aphront-form-control-text';
}
protected function renderInput() {
if (!$this->getUser()) {
- throw new Exception('You must setUser() before render()!');
+ throw new PhutilInvalidStateException('setUser');
}
// represent months like a credit card does
$months = array(
'01' => '01',
'02' => '02',
'03' => '03',
'04' => '04',
'05' => '05',
'06' => '06',
'07' => '07',
'08' => '08',
'09' => '09',
'10' => '10',
'11' => '11',
'12' => '12',
);
$current_year = $this->getCurrentYear();
$years = range($current_year, $current_year + 20);
$years = array_fuse($years);
if ($this->getMonthInputValue()) {
$selected_month = $this->getMonthInputValue();
} else {
$selected_month = $this->getCurrentMonth();
}
$months_sel = AphrontFormSelectControl::renderSelectTag(
$selected_month,
$months,
array(
'sigil' => 'month-input',
));
$years_sel = AphrontFormSelectControl::renderSelectTag(
$this->getYearInputValue(),
$years,
array(
'sigil' => 'year-input',
));
return hsprintf('%s%s', $months_sel, $years_sel);
}
}
diff --git a/src/applications/phortune/controller/PhortuneCartCancelController.php b/src/applications/phortune/controller/PhortuneCartCancelController.php
index b7415c521..3aedceb6b 100644
--- a/src/applications/phortune/controller/PhortuneCartCancelController.php
+++ b/src/applications/phortune/controller/PhortuneCartCancelController.php
@@ -1,215 +1,213 @@
<?php
final class PhortuneCartCancelController
extends PhortuneCartController {
private $id;
private $action;
public function willProcessRequest(array $data) {
$this->id = $data['id'];
$this->action = $data['action'];
}
public function processRequest() {
$request = $this->getRequest();
$viewer = $request->getUser();
$authority = $this->loadMerchantAuthority();
$cart_query = id(new PhortuneCartQuery())
->setViewer($viewer)
->withIDs(array($this->id))
->needPurchases(true);
if ($authority) {
$cart_query->withMerchantPHIDs(array($authority->getPHID()));
}
$cart = $cart_query->executeOne();
if (!$cart) {
return new Aphront404Response();
}
switch ($this->action) {
case 'cancel':
// You must be able to edit the account to cancel an order.
PhabricatorPolicyFilter::requireCapability(
$viewer,
$cart->getAccount(),
PhabricatorPolicyCapability::CAN_EDIT);
$is_refund = false;
break;
case 'refund':
// You must be able to control the merchant to refund an order.
PhabricatorPolicyFilter::requireCapability(
$viewer,
$cart->getMerchant(),
PhabricatorPolicyCapability::CAN_EDIT);
$is_refund = true;
break;
default:
return new Aphront404Response();
}
$cancel_uri = $cart->getDetailURI($authority);
$merchant = $cart->getMerchant();
try {
if ($is_refund) {
$title = pht('Unable to Refund Order');
$cart->assertCanRefundOrder();
} else {
$title = pht('Unable to Cancel Order');
$cart->assertCanCancelOrder();
}
} catch (Exception $ex) {
return $this->newDialog()
->setTitle($title)
->appendChild($ex->getMessage())
->addCancelButton($cancel_uri);
}
$charges = id(new PhortuneChargeQuery())
->setViewer($viewer)
->withCartPHIDs(array($cart->getPHID()))
->withStatuses(
array(
PhortuneCharge::STATUS_HOLD,
PhortuneCharge::STATUS_CHARGED,
))
->execute();
$amounts = mpull($charges, 'getAmountAsCurrency');
$maximum = PhortuneCurrency::newFromList($amounts);
$v_refund = $maximum->formatForDisplay();
$errors = array();
$e_refund = true;
if ($request->isFormPost()) {
if ($is_refund) {
try {
$refund = PhortuneCurrency::newFromUserInput(
$viewer,
$request->getStr('refund'));
$refund->assertInRange('0.00 USD', $maximum->formatForDisplay());
} catch (Exception $ex) {
$errors[] = $ex->getMessage();
$e_refund = pht('Invalid');
}
} else {
$refund = $maximum;
}
if (!$errors) {
$charges = msort($charges, 'getID');
$charges = array_reverse($charges);
if ($charges) {
$providers = id(new PhortunePaymentProviderConfigQuery())
->setViewer($viewer)
->withPHIDs(mpull($charges, 'getProviderPHID'))
->execute();
$providers = mpull($providers, null, 'getPHID');
} else {
$providers = array();
}
foreach ($charges as $charge) {
$refundable = $charge->getAmountRefundableAsCurrency();
if (!$refundable->isPositive()) {
// This charge is a refund, or has already been fully refunded.
continue;
}
if ($refund->isGreaterThan($refundable)) {
$refund_amount = $refundable;
} else {
$refund_amount = $refund;
}
$provider_config = idx($providers, $charge->getProviderPHID());
if (!$provider_config) {
throw new Exception(pht('Unable to load provider for charge!'));
}
$provider = $provider_config->buildProvider();
$refund_charge = $cart->willRefundCharge(
$viewer,
$provider,
$charge,
$refund_amount);
$refunded = false;
try {
$provider->refundCharge($charge, $refund_charge);
$refunded = true;
} catch (Exception $ex) {
phlog($ex);
$cart->didFailRefund($charge, $refund_charge);
}
if ($refunded) {
$cart->didRefundCharge($charge, $refund_charge);
$refund = $refund->subtract($refund_amount);
}
if (!$refund->isPositive()) {
break;
}
}
if ($refund->isPositive()) {
throw new Exception(pht('Unable to refund some charges!'));
}
// TODO: If every HOLD and CHARGING transaction has been fully refunded
// and we're in a HOLD, REVIEW, PURCHASING or CHARGED cart state we
// probably need to kick the cart back to READY here (or maybe kill
// it if it was in REVIEW)?
return id(new AphrontRedirectResponse())->setURI($cancel_uri);
}
}
if ($is_refund) {
$title = pht('Refund Order?');
- $body = pht(
- 'Really refund this order?');
+ $body = pht('Really refund this order?');
$button = pht('Refund Order');
$cancel_text = pht('Cancel');
$form = id(new AphrontFormView())
->setUser($viewer)
->appendChild(
id(new AphrontFormTextControl())
->setName('refund')
->setLabel(pht('Amount'))
->setError($e_refund)
->setValue($v_refund));
$form = $form->buildLayoutView();
} else {
$title = pht('Cancel Order?');
- $body = pht(
- 'Really cancel this order? Any payment will be refunded.');
+ $body = pht('Really cancel this order? Any payment will be refunded.');
$button = pht('Cancel Order');
// Don't give the user a "Cancel" button in response to a "Cancel?"
// prompt, as it's confusing.
$cancel_text = pht('Do Not Cancel Order');
$form = null;
}
return $this->newDialog()
->setTitle($title)
->setErrors($errors)
->appendChild($body)
->appendChild($form)
->addSubmitButton($button)
->addCancelButton($cancel_uri, $cancel_text);
}
}
diff --git a/src/applications/phortune/controller/PhortuneCartCheckoutController.php b/src/applications/phortune/controller/PhortuneCartCheckoutController.php
index b6b928710..2d6ff2342 100644
--- a/src/applications/phortune/controller/PhortuneCartCheckoutController.php
+++ b/src/applications/phortune/controller/PhortuneCartCheckoutController.php
@@ -1,230 +1,230 @@
<?php
final class PhortuneCartCheckoutController
extends PhortuneCartController {
private $id;
public function willProcessRequest(array $data) {
$this->id = $data['id'];
}
public function processRequest() {
$request = $this->getRequest();
$viewer = $request->getUser();
$cart = id(new PhortuneCartQuery())
->setViewer($viewer)
->withIDs(array($this->id))
->needPurchases(true)
->executeOne();
if (!$cart) {
return new Aphront404Response();
}
$cancel_uri = $cart->getCancelURI();
$merchant = $cart->getMerchant();
switch ($cart->getStatus()) {
case PhortuneCart::STATUS_BUILDING:
return $this->newDialog()
->setTitle(pht('Incomplete Cart'))
->appendParagraph(
pht(
'The application that created this cart did not finish putting '.
'products in it. You can not checkout with an incomplete '.
'cart.'))
->addCancelButton($cancel_uri);
case PhortuneCart::STATUS_READY:
// This is the expected, normal state for a cart that's ready for
// checkout.
break;
case PhortuneCart::STATUS_CHARGED:
case PhortuneCart::STATUS_PURCHASING:
case PhortuneCart::STATUS_HOLD:
case PhortuneCart::STATUS_REVIEW:
case PhortuneCart::STATUS_PURCHASED:
// For these states, kick the user to the order page to give them
// information and options.
return id(new AphrontRedirectResponse())->setURI($cart->getDetailURI());
default:
throw new Exception(
pht(
'Unknown cart status "%s"!',
$cart->getStatus()));
}
$account = $cart->getAccount();
$account_uri = $this->getApplicationURI($account->getID().'/');
$methods = id(new PhortunePaymentMethodQuery())
->setViewer($viewer)
->withAccountPHIDs(array($account->getPHID()))
->withMerchantPHIDs(array($merchant->getPHID()))
->withStatuses(array(PhortunePaymentMethod::STATUS_ACTIVE))
->execute();
$e_method = null;
$errors = array();
if ($request->isFormPost()) {
// Require CAN_EDIT on the cart to actually make purchases.
PhabricatorPolicyFilter::requireCapability(
$viewer,
$cart,
PhabricatorPolicyCapability::CAN_EDIT);
$method_id = $request->getInt('paymentMethodID');
$method = idx($methods, $method_id);
if (!$method) {
$e_method = pht('Required');
$errors[] = pht('You must choose a payment method.');
}
if (!$errors) {
$provider = $method->buildPaymentProvider();
$charge = $cart->willApplyCharge($viewer, $provider, $method);
try {
$provider->applyCharge($method, $charge);
} catch (Exception $ex) {
$cart->didFailCharge($charge);
return $this->newDialog()
->setTitle(pht('Charge Failed'))
->appendParagraph(
pht(
'Unable to make payment: %s',
$ex->getMessage()))
->addCancelButton($cart->getCheckoutURI(), pht('Continue'));
}
$cart->didApplyCharge($charge);
$done_uri = $cart->getCheckoutURI();
return id(new AphrontRedirectResponse())->setURI($done_uri);
}
}
$cart_table = $this->buildCartContentTable($cart);
$cart_box = id(new PHUIObjectBoxView())
->setFormErrors($errors)
->setHeaderText(pht('Cart Contents'))
->appendChild($cart_table);
$title = $cart->getName();
if (!$methods) {
$method_control = id(new AphrontFormStaticControl())
->setLabel(pht('Payment Method'))
->setValue(
phutil_tag('em', array(), pht('No payment methods configured.')));
} else {
$method_control = id(new AphrontFormRadioButtonControl())
->setLabel(pht('Payment Method'))
->setName('paymentMethodID')
->setValue($request->getInt('paymentMethodID'));
foreach ($methods as $method) {
$method_control->addButton(
$method->getID(),
$method->getFullDisplayName(),
$method->getDescription());
}
}
$method_control->setError($e_method);
$account_id = $account->getID();
$payment_method_uri = $this->getApplicationURI("{$account_id}/card/new/");
$payment_method_uri = new PhutilURI($payment_method_uri);
$payment_method_uri->setQueryParams(
array(
'merchantID' => $merchant->getID(),
'cartID' => $cart->getID(),
));
$form = id(new AphrontFormView())
->setUser($viewer)
->appendChild($method_control);
$add_providers = $this->loadCreatePaymentMethodProvidersForMerchant(
$merchant);
if ($add_providers) {
$new_method = javelin_tag(
'a',
array(
'class' => 'button grey',
'href' => $payment_method_uri,
),
pht('Add New Payment Method'));
$form->appendChild(
id(new AphrontFormMarkupControl())
->setValue($new_method));
}
if ($methods || $add_providers) {
$submit = id(new AphrontFormSubmitControl())
->setValue(pht('Submit Payment'))
->setDisabled(!$methods);
if ($cart->getCancelURI() !== null) {
$submit->addCancelButton($cart->getCancelURI());
}
$form->appendChild($submit);
}
$provider_form = null;
$pay_providers = $this->loadOneTimePaymentProvidersForMerchant($merchant);
if ($pay_providers) {
$one_time_options = array();
foreach ($pay_providers as $provider) {
$one_time_options[] = $provider->renderOneTimePaymentButton(
$account,
$cart,
$viewer);
}
$one_time_options = phutil_tag(
'div',
array(
'class' => 'phortune-payment-onetime-list',
),
$one_time_options);
$provider_form = new PHUIFormLayoutView();
$provider_form->appendChild(
id(new AphrontFormMarkupControl())
- ->setLabel('Pay With')
+ ->setLabel(pht('Pay With'))
->setValue($one_time_options));
}
$payment_box = id(new PHUIObjectBoxView())
->setHeaderText(pht('Choose Payment Method'))
->appendChild($form)
->appendChild($provider_form);
$description_box = $this->renderCartDescription($cart);
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb(pht('Checkout'));
$crumbs->addTextCrumb($title);
return $this->buildApplicationPage(
array(
$crumbs,
$cart_box,
$description_box,
$payment_box,
),
array(
'title' => $title,
));
}
}
diff --git a/src/applications/phortune/controller/PhortuneMerchantViewController.php b/src/applications/phortune/controller/PhortuneMerchantViewController.php
index c1512e9d3..502659712 100644
--- a/src/applications/phortune/controller/PhortuneMerchantViewController.php
+++ b/src/applications/phortune/controller/PhortuneMerchantViewController.php
@@ -1,301 +1,300 @@
<?php
final class PhortuneMerchantViewController
extends PhortuneMerchantController {
private $id;
public function willProcessRequest(array $data) {
$this->id = $data['id'];
}
public function processRequest() {
$request = $this->getRequest();
$viewer = $request->getUser();
$merchant = id(new PhortuneMerchantQuery())
->setViewer($viewer)
->withIDs(array($this->id))
->executeOne();
if (!$merchant) {
return new Aphront404Response();
}
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb($merchant->getName());
$title = pht(
'Merchant %d %s',
$merchant->getID(),
$merchant->getName());
$header = id(new PHUIHeaderView())
->setObjectName(pht('Merchant %d', $merchant->getID()))
->setHeader($merchant->getName())
->setUser($viewer)
->setPolicyObject($merchant);
$providers = id(new PhortunePaymentProviderConfigQuery())
->setViewer($viewer)
->withMerchantPHIDs(array($merchant->getPHID()))
->execute();
$properties = $this->buildPropertyListView($merchant, $providers);
$actions = $this->buildActionListView($merchant);
$properties->setActionList($actions);
$provider_list = $this->buildProviderList(
$merchant,
$providers);
$box = id(new PHUIObjectBoxView())
->setHeader($header)
->appendChild($properties);
$timeline = $this->buildTransactionTimeline(
$merchant,
new PhortuneMerchantTransactionQuery());
$timeline->setShouldTerminate(true);
return $this->buildApplicationPage(
array(
$crumbs,
$box,
$provider_list,
$timeline,
),
array(
'title' => $title,
));
}
private function buildPropertyListView(
PhortuneMerchant $merchant,
array $providers) {
$viewer = $this->getRequest()->getUser();
$view = id(new PHUIPropertyListView())
->setUser($viewer)
->setObject($merchant);
$status_view = new PHUIStatusListView();
$have_any = false;
$any_test = false;
foreach ($providers as $provider_config) {
$provider = $provider_config->buildProvider();
if ($provider->isEnabled()) {
$have_any = true;
}
if (!$provider->isAcceptingLivePayments()) {
$any_test = true;
}
}
if ($have_any) {
$status_view->addItem(
id(new PHUIStatusItemView())
->setIcon(PHUIStatusItemView::ICON_ACCEPT, 'green')
->setTarget(pht('Accepts Payments'))
->setNote(pht('This merchant can accept payments.')));
if ($any_test) {
$status_view->addItem(
id(new PHUIStatusItemView())
->setIcon(PHUIStatusItemView::ICON_WARNING, 'yellow')
->setTarget(pht('Test Mode'))
->setNote(pht('This merchant is accepting test payments.')));
} else {
$status_view->addItem(
id(new PHUIStatusItemView())
->setIcon(PHUIStatusItemView::ICON_ACCEPT, 'green')
->setTarget(pht('Live Mode'))
->setNote(pht('This merchant is accepting live payments.')));
}
} else if ($providers) {
$status_view->addItem(
id(new PHUIStatusItemView())
->setIcon(PHUIStatusItemView::ICON_REJECT, 'red')
->setTarget(pht('No Enabled Providers'))
->setNote(
pht(
'All of the payment providers for this merchant are '.
'disabled.')));
} else {
$status_view->addItem(
id(new PHUIStatusItemView())
->setIcon(PHUIStatusItemView::ICON_WARNING, 'yellow')
->setTarget(pht('No Providers'))
->setNote(
pht(
'This merchant does not have any payment providers configured '.
'yet, so it can not accept payments. Add a provider.')));
}
$view->addProperty(pht('Status'), $status_view);
$view->addProperty(
pht('Members'),
$viewer->renderHandleList($merchant->getMemberPHIDs()));
$view->invokeWillRenderEvent();
$description = $merchant->getDescription();
if (strlen($description)) {
$description = PhabricatorMarkupEngine::renderOneObject(
id(new PhabricatorMarkupOneOff())->setContent($description),
'default',
$viewer);
$view->addSectionHeader(pht('Description'));
$view->addTextContent($description);
}
return $view;
}
private function buildActionListView(PhortuneMerchant $merchant) {
$viewer = $this->getRequest()->getUser();
$id = $merchant->getID();
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$merchant,
PhabricatorPolicyCapability::CAN_EDIT);
$view = id(new PhabricatorActionListView())
->setUser($viewer)
->setObject($merchant);
$view->addAction(
id(new PhabricatorActionView())
->setName(pht('Edit Merchant'))
->setIcon('fa-pencil')
->setDisabled(!$can_edit)
->setWorkflow(!$can_edit)
->setHref($this->getApplicationURI("merchant/edit/{$id}/")));
$view->addAction(
id(new PhabricatorActionView())
->setName(pht('View Orders'))
->setIcon('fa-shopping-cart')
->setHref($this->getApplicationURI("merchant/orders/{$id}/"))
->setDisabled(!$can_edit)
->setWorkflow(!$can_edit));
$view->addAction(
id(new PhabricatorActionView())
->setName(pht('View Subscriptions'))
->setIcon('fa-moon-o')
->setHref($this->getApplicationURI("merchant/{$id}/subscription/"))
->setDisabled(!$can_edit)
->setWorkflow(!$can_edit));
$view->addAction(
id(new PhabricatorActionView())
->setName(pht('New Invoice'))
->setIcon('fa-fax')
->setHref($this->getApplicationURI("merchant/{$id}/invoice/new/"))
->setDisabled(!$can_edit)
->setWorkflow(!$can_edit));
return $view;
}
private function buildProviderList(
PhortuneMerchant $merchant,
array $providers) {
$viewer = $this->getRequest()->getUser();
$id = $merchant->getID();
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$merchant,
PhabricatorPolicyCapability::CAN_EDIT);
$provider_list = id(new PHUIObjectItemListView())
->setFlush(true)
->setNoDataString(pht('This merchant has no payment providers.'));
foreach ($providers as $provider_config) {
$provider = $provider_config->buildProvider();
$provider_id = $provider_config->getID();
$item = id(new PHUIObjectItemView())
->setHeader($provider->getName());
if ($provider->isEnabled()) {
if ($provider->isAcceptingLivePayments()) {
$item->setBarColor('green');
} else {
$item->setBarColor('yellow');
$item->addIcon('fa-exclamation-triangle', pht('Test Mode'));
}
$item->addAttribute($provider->getConfigureProvidesDescription());
} else {
// Don't show disabled providers to users who can't manage the merchant
// account.
if (!$can_edit) {
continue;
}
$item->setDisabled(true);
$item->addAttribute(
phutil_tag('em', array(), pht('This payment provider is disabled.')));
}
if ($can_edit) {
$edit_uri = $this->getApplicationURI(
"/provider/edit/{$provider_id}/");
$disable_uri = $this->getApplicationURI(
"/provider/disable/{$provider_id}/");
if ($provider->isEnabled()) {
$disable_icon = 'fa-times';
$disable_name = pht('Disable');
} else {
$disable_icon = 'fa-check';
$disable_name = pht('Enable');
}
$item->addAction(
id(new PHUIListItemView())
->setIcon($disable_icon)
->setHref($disable_uri)
->setName($disable_name)
->setWorkflow(true));
$item->addAction(
id(new PHUIListItemView())
->setIcon('fa-pencil')
->setHref($edit_uri)
->setName(pht('Edit')));
}
$provider_list->addItem($item);
}
$add_action = id(new PHUIButtonView())
->setTag('a')
->setHref($this->getApplicationURI('provider/edit/?merchantID='.$id))
->setText(pht('Add Payment Provider'))
->setDisabled(!$can_edit)
->setWorkflow(!$can_edit)
->setIcon(id(new PHUIIconView())->setIconFont('fa-plus'));
$header = id(new PHUIHeaderView())
->setHeader(pht('Payment Providers'))
->addActionLink($add_action);
return id(new PHUIObjectBoxView())
->setHeader($header)
->appendChild($provider_list);
}
-
}
diff --git a/src/applications/phortune/controller/PhortunePaymentMethodCreateController.php b/src/applications/phortune/controller/PhortunePaymentMethodCreateController.php
index 844dd2ed5..6b7e55bb5 100644
--- a/src/applications/phortune/controller/PhortunePaymentMethodCreateController.php
+++ b/src/applications/phortune/controller/PhortunePaymentMethodCreateController.php
@@ -1,275 +1,276 @@
<?php
final class PhortunePaymentMethodCreateController
extends PhortuneController {
private $accountID;
public function willProcessRequest(array $data) {
$this->accountID = $data['accountID'];
}
public function processRequest() {
$request = $this->getRequest();
$viewer = $request->getUser();
$account = id(new PhortuneAccountQuery())
->setViewer($viewer)
->withIDs(array($this->accountID))
->executeOne();
if (!$account) {
return new Aphront404Response();
}
$account_id = $account->getID();
$merchant = id(new PhortuneMerchantQuery())
->setViewer($viewer)
->withIDs(array($request->getInt('merchantID')))
->executeOne();
if (!$merchant) {
return new Aphront404Response();
}
$cart_id = $request->getInt('cartID');
$subscription_id = $request->getInt('subscriptionID');
if ($cart_id) {
$cancel_uri = $this->getApplicationURI("cart/{$cart_id}/checkout/");
} else if ($subscription_id) {
$cancel_uri = $this->getApplicationURI(
"{$account_id}/subscription/edit/{$subscription_id}/");
} else {
$cancel_uri = $this->getApplicationURI($account->getID().'/');
}
$providers = $this->loadCreatePaymentMethodProvidersForMerchant($merchant);
if (!$providers) {
throw new Exception(
- 'There are no payment providers enabled that can add payment '.
- 'methods.');
+ pht(
+ 'There are no payment providers enabled that can add payment '.
+ 'methods.'));
}
if (count($providers) == 1) {
// If there's only one provider, always choose it.
$provider_id = head_key($providers);
} else {
$provider_id = $request->getInt('providerID');
if (empty($providers[$provider_id])) {
$choices = array();
foreach ($providers as $provider) {
$choices[] = $this->renderSelectProvider($provider);
}
$content = phutil_tag(
'div',
array(
'class' => 'phortune-payment-method-list',
),
$choices);
return $this->newDialog()
->setRenderDialogAsDiv(true)
->setTitle(pht('Add Payment Method'))
->appendParagraph(pht('Choose a payment method to add:'))
->appendChild($content)
->addCancelButton($cancel_uri);
}
}
$provider = $providers[$provider_id];
$errors = array();
if ($request->isFormPost() && $request->getBool('isProviderForm')) {
$method = id(new PhortunePaymentMethod())
->setAccountPHID($account->getPHID())
->setAuthorPHID($viewer->getPHID())
->setMerchantPHID($merchant->getPHID())
->setProviderPHID($provider->getProviderConfig()->getPHID())
->setStatus(PhortunePaymentMethod::STATUS_ACTIVE);
if (!$errors) {
$errors = $this->processClientErrors(
$provider,
$request->getStr('errors'));
}
if (!$errors) {
$client_token_raw = $request->getStr('token');
$client_token = null;
try {
$client_token = phutil_json_decode($client_token_raw);
} catch (PhutilJSONParserException $ex) {
$errors[] = pht(
'There was an error decoding token information submitted by the '.
'client. Expected a JSON-encoded token dictionary, received: %s.',
nonempty($client_token_raw, pht('nothing')));
}
if (!$provider->validateCreatePaymentMethodToken($client_token)) {
$errors[] = pht(
'There was an error with the payment token submitted by the '.
'client. Expected a valid dictionary, received: %s.',
$client_token_raw);
}
if (!$errors) {
$errors = $provider->createPaymentMethodFromRequest(
$request,
$method,
$client_token);
}
}
if (!$errors) {
$method->save();
// If we added this method on a cart flow, return to the cart to
// check out.
if ($cart_id) {
$next_uri = $this->getApplicationURI(
"cart/{$cart_id}/checkout/?paymentMethodID=".$method->getID());
} else if ($subscription_id) {
$next_uri = $cancel_uri;
} else {
$account_uri = $this->getApplicationURI($account->getID().'/');
$next_uri = new PhutilURI($account_uri);
$next_uri->setFragment('payment');
}
return id(new AphrontRedirectResponse())->setURI($next_uri);
} else {
$dialog = id(new AphrontDialogView())
->setUser($viewer)
->setTitle(pht('Error Adding Payment Method'))
->appendChild(id(new PHUIInfoView())->setErrors($errors))
->addCancelButton($request->getRequestURI());
return id(new AphrontDialogResponse())->setDialog($dialog);
}
}
$form = $provider->renderCreatePaymentMethodForm($request, $errors);
$form
->setUser($viewer)
->setAction($request->getRequestURI())
->setWorkflow(true)
->addHiddenInput('providerID', $provider_id)
->addHiddenInput('cartID', $request->getInt('cartID'))
->addHiddenInput('subscriptionID', $request->getInt('subscriptionID'))
->addHiddenInput('isProviderForm', true)
->appendChild(
id(new AphrontFormSubmitControl())
->setValue(pht('Add Payment Method'))
->addCancelButton($cancel_uri));
$box = id(new PHUIObjectBoxView())
->setHeaderText($provider->getPaymentMethodDescription())
->setForm($form);
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb(pht('Add Payment Method'));
return $this->buildApplicationPage(
array(
$crumbs,
$box,
),
array(
'title' => $provider->getPaymentMethodDescription(),
));
}
private function renderSelectProvider(
PhortunePaymentProvider $provider) {
$request = $this->getRequest();
$viewer = $request->getUser();
$description = $provider->getPaymentMethodDescription();
$icon_uri = $provider->getPaymentMethodIcon();
$details = $provider->getPaymentMethodProviderDescription();
$this->requireResource('phortune-css');
$icon = id(new PHUIIconView())
->setSpriteSheet(PHUIIconView::SPRITE_LOGIN)
->setSpriteIcon($provider->getPaymentMethodIcon());
$button = id(new PHUIButtonView())
->setSize(PHUIButtonView::BIG)
->setColor(PHUIButtonView::GREY)
->setIcon($icon)
->setText($description)
->setSubtext($details)
->setMetadata(array('disableWorkflow' => true));
$form = id(new AphrontFormView())
->setUser($viewer)
->setAction($request->getRequestURI())
->addHiddenInput('providerID', $provider->getProviderConfig()->getID())
->appendChild($button);
return $form;
}
private function processClientErrors(
PhortunePaymentProvider $provider,
$client_errors_raw) {
$errors = array();
$client_errors = null;
try {
$client_errors = phutil_json_decode($client_errors_raw);
} catch (PhutilJSONParserException $ex) {
$errors[] = pht(
'There was an error decoding error information submitted by the '.
'client. Expected a JSON-encoded list of error codes, received: %s.',
nonempty($client_errors_raw, pht('nothing')));
}
foreach (array_unique($client_errors) as $key => $client_error) {
$client_errors[$key] = $provider->translateCreatePaymentMethodErrorCode(
$client_error);
}
foreach (array_unique($client_errors) as $client_error) {
switch ($client_error) {
case PhortuneErrCode::ERR_CC_INVALID_NUMBER:
$message = pht(
'The card number you entered is not a valid card number. Check '.
'that you entered it correctly.');
break;
case PhortuneErrCode::ERR_CC_INVALID_CVC:
$message = pht(
'The CVC code you entered is not a valid CVC code. Check that '.
'you entered it correctly. The CVC code is a 3-digit or 4-digit '.
'numeric code which usually appears on the back of the card.');
break;
case PhortuneErrCode::ERR_CC_INVALID_EXPIRY:
$message = pht(
'The card expiration date is not a valid expiration date. Check '.
'that you entered it correctly. You can not add an expired card '.
'as a payment method.');
break;
default:
$message = $provider->getCreatePaymentMethodErrorMessage(
$client_error);
if (!$message) {
$message = pht(
"There was an unexpected error ('%s') processing payment ".
"information.",
$client_error);
phlog($message);
}
break;
}
$errors[$client_error] = $message;
}
return $errors;
}
}
diff --git a/src/applications/phortune/controller/PhortuneProviderEditController.php b/src/applications/phortune/controller/PhortuneProviderEditController.php
index da18fdbb4..3ed97b5a4 100644
--- a/src/applications/phortune/controller/PhortuneProviderEditController.php
+++ b/src/applications/phortune/controller/PhortuneProviderEditController.php
@@ -1,292 +1,291 @@
<?php
final class PhortuneProviderEditController
extends PhortuneMerchantController {
private $id;
public function willProcessRequest(array $data) {
$this->id = idx($data, 'id');
}
public function processRequest() {
$request = $this->getRequest();
$viewer = $request->getUser();
if ($this->id) {
$provider_config = id(new PhortunePaymentProviderConfigQuery())
->setViewer($viewer)
->withIDs(array($this->id))
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
if (!$provider_config) {
return new Aphront404Response();
}
$is_new = false;
$is_choose_type = false;
$merchant = $provider_config->getMerchant();
$merchant_id = $merchant->getID();
$cancel_uri = $this->getApplicationURI("merchant/{$merchant_id}/");
} else {
$merchant = id(new PhortuneMerchantQuery())
->setViewer($viewer)
->withIDs(array($request->getStr('merchantID')))
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
if (!$merchant) {
return new Aphront404Response();
}
$merchant_id = $merchant->getID();
$current_providers = id(new PhortunePaymentProviderConfigQuery())
->setViewer($viewer)
->withMerchantPHIDs(array($merchant->getPHID()))
->execute();
$current_map = mgroup($current_providers, 'getProviderClass');
$provider_config = PhortunePaymentProviderConfig::initializeNewProvider(
$merchant);
$is_new = true;
$classes = PhortunePaymentProvider::getAllProviders();
$class = $request->getStr('class');
if (empty($classes[$class]) || isset($current_map[$class])) {
return $this->processChooseClassRequest(
$request,
$merchant,
$current_map);
}
$provider_config->setProviderClass($class);
$cancel_uri = $this->getApplicationURI(
'provider/edit/?merchantID='.$merchant_id);
}
$provider = $provider_config->buildProvider();
if ($is_new) {
$title = pht('Create Payment Provider');
$button_text = pht('Create Provider');
} else {
$title = pht(
'Edit Payment Provider %d %s',
$provider_config->getID(),
$provider->getName());
$button_text = pht('Save Changes');
}
$errors = array();
if ($request->isFormPost() && $request->getStr('edit')) {
$form_values = $provider->readEditFormValuesFromRequest($request);
list($errors, $issues, $xaction_values) = $provider->processEditForm(
$request,
$form_values);
if (!$errors) {
// Find any secret fields which we're about to set to "*******"
// (indicating that the user did not edit the value) and remove them
// from the list of properties to update (so we don't write "******"
// to permanent configuration.
$secrets = $provider->getAllConfigurableSecretProperties();
$secrets = array_fuse($secrets);
foreach ($xaction_values as $key => $value) {
if ($provider->isConfigurationSecret($value)) {
unset($xaction_values[$key]);
}
}
if ($provider->canRunConfigurationTest()) {
$proxy = clone $provider;
$proxy_config = clone $provider_config;
$proxy_config->setMetadata(
$xaction_values + $provider_config->getMetadata());
$proxy->setProviderConfig($proxy_config);
try {
$proxy->runConfigurationTest();
} catch (Exception $ex) {
$errors[] = pht('Unable to connect to payment provider:');
$errors[] = $ex->getMessage();
}
}
if (!$errors) {
$template = id(new PhortunePaymentProviderConfigTransaction())
->setTransactionType(
PhortunePaymentProviderConfigTransaction::TYPE_PROPERTY);
$xactions = array();
$xactions[] = id(new PhortunePaymentProviderConfigTransaction())
->setTransactionType(
PhortunePaymentProviderConfigTransaction::TYPE_CREATE)
->setNewValue(true);
foreach ($xaction_values as $key => $value) {
$xactions[] = id(clone $template)
->setMetadataValue(
PhortunePaymentProviderConfigTransaction::PROPERTY_KEY,
$key)
->setNewValue($value);
}
$editor = id(new PhortunePaymentProviderConfigEditor())
->setActor($viewer)
->setContentSourceFromRequest($request)
->setContinueOnNoEffect(true);
$editor->applyTransactions($provider_config, $xactions);
$merchant_uri = $this->getApplicationURI(
'merchant/'.$merchant->getID().'/');
return id(new AphrontRedirectResponse())->setURI($merchant_uri);
}
}
} else {
$form_values = $provider->readEditFormValuesFromProviderConfig();
$issues = array();
}
$form = id(new AphrontFormView())
->setUser($viewer)
->addHiddenInput('merchantID', $merchant->getID())
->addHiddenInput('class', $provider_config->getProviderClass())
->addHiddenInput('edit', true)
->appendChild(
id(new AphrontFormMarkupControl())
->setLabel(pht('Provider Type'))
->setValue($provider->getName()));
$provider->extendEditForm($request, $form, $form_values, $issues);
$form
->appendChild(
id(new AphrontFormSubmitControl())
->setValue($button_text)
->addCancelButton($cancel_uri))
->appendChild(
id(new AphrontFormDividerControl()))
->appendRemarkupInstructions(
$provider->getConfigureInstructions());
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb($merchant->getName(), $cancel_uri);
if ($is_new) {
$crumbs->addTextCrumb(pht('Add Provider'));
} else {
$crumbs->addTextCrumb(
pht('Edit Provider %d', $provider_config->getID()));
}
$box = id(new PHUIObjectBoxView())
->setFormErrors($errors)
->setHeaderText($title)
->appendChild($form);
return $this->buildApplicationPage(
array(
$crumbs,
$box,
),
array(
'title' => $title,
));
}
private function processChooseClassRequest(
AphrontRequest $request,
PhortuneMerchant $merchant,
array $current_map) {
$viewer = $request->getUser();
$providers = PhortunePaymentProvider::getAllProviders();
$v_class = null;
$errors = array();
if ($request->isFormPost()) {
$v_class = $request->getStr('class');
if (!isset($providers[$v_class])) {
$errors[] = pht('You must select a valid provider type.');
}
}
$merchant_id = $merchant->getID();
$cancel_uri = $this->getApplicationURI("merchant/{$merchant_id}/");
if (!$v_class) {
$v_class = key($providers);
}
$panel_classes = id(new AphrontFormRadioButtonControl())
->setName('class')
->setValue($v_class);
$providers = msort($providers, 'getConfigureName');
foreach ($providers as $class => $provider) {
$disabled = isset($current_map[$class]);
if ($disabled) {
$description = phutil_tag(
'em',
array(),
pht(
'This merchant already has a payment account configured '.
'with this provider.'));
} else {
$description = $provider->getConfigureDescription();
}
$panel_classes->addButton(
$class,
$provider->getConfigureName(),
$description,
null,
$disabled);
}
$form = id(new AphrontFormView())
->setUser($viewer)
->addHiddenInput('merchantID', $merchant->getID())
->appendRemarkupInstructions(
- pht(
- 'Choose the type of payment provider to add:'))
+ pht('Choose the type of payment provider to add:'))
->appendChild($panel_classes)
->appendChild(
id(new AphrontFormSubmitControl())
->setValue(pht('Continue'))
->addCancelButton($cancel_uri));
$title = pht('Add Payment Provider');
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb($merchant->getName(), $cancel_uri);
$crumbs->addTextCrumb($title);
$box = id(new PHUIObjectBoxView())
->setHeaderText($title)
->setFormErrors($errors)
->setForm($form);
return $this->buildApplicationPage(
array(
$crumbs,
$box,
),
array(
'title' => $title,
));
}
}
diff --git a/src/applications/phortune/currency/PhortuneCurrency.php b/src/applications/phortune/currency/PhortuneCurrency.php
index b59d9b144..f69dd362f 100644
--- a/src/applications/phortune/currency/PhortuneCurrency.php
+++ b/src/applications/phortune/currency/PhortuneCurrency.php
@@ -1,239 +1,239 @@
<?php
final class PhortuneCurrency extends Phobject {
private $value;
private $currency;
private function __construct() {
// Intentionally private.
}
public static function getDefaultCurrency() {
return 'USD';
}
public static function newEmptyCurrency() {
return self::newFromString('0.00 USD');
}
public static function newFromUserInput(PhabricatorUser $user, $string) {
// Eventually, this might select a default currency based on user settings.
return self::newFromString($string, self::getDefaultCurrency());
}
public static function newFromString($string, $default = null) {
$matches = null;
$ok = preg_match(
'/^([-$]*(?:\d+)?(?:[.]\d{0,2})?)(?:\s+([A-Z]+))?$/',
trim($string),
$matches);
if (!$ok) {
self::throwFormatException($string);
}
$value = $matches[1];
if (substr_count($value, '-') > 1) {
self::throwFormatException($string);
}
if (substr_count($value, '$') > 1) {
self::throwFormatException($string);
}
$value = str_replace('$', '', $value);
$value = (float)$value;
$value = (int)round(100 * $value);
$currency = idx($matches, 2, $default);
switch ($currency) {
case 'USD':
break;
default:
- throw new Exception("Unsupported currency '{$currency}'!");
+ throw new Exception(pht("Unsupported currency '%s'!", $currency));
}
return self::newFromValueAndCurrency($value, $currency);
}
public static function newFromValueAndCurrency($value, $currency) {
$obj = new PhortuneCurrency();
$obj->value = $value;
$obj->currency = $currency;
return $obj;
}
public static function newFromList(array $list) {
assert_instances_of($list, __CLASS__);
if (!$list) {
return self::newEmptyCurrency();
}
$total = null;
foreach ($list as $item) {
if ($total === null) {
$total = $item;
} else {
$total = $total->add($item);
}
}
return $total;
}
public function formatForDisplay() {
$bare = $this->formatBareValue();
return '$'.$bare.' '.$this->currency;
}
public function serializeForStorage() {
return $this->formatBareValue().' '.$this->currency;
}
public function formatBareValue() {
switch ($this->currency) {
case 'USD':
return sprintf('%.02f', $this->value / 100);
default:
throw new Exception(
pht('Unsupported currency ("%s")!', $this->currency));
}
}
public function getValue() {
return $this->value;
}
public function getCurrency() {
return $this->currency;
}
public function getValueInUSDCents() {
if ($this->currency !== 'USD') {
throw new Exception(pht('Unexpected currency!'));
}
return $this->value;
}
private static function throwFormatException($string) {
- throw new Exception("Invalid currency format ('{$string}').");
+ throw new Exception(pht("Invalid currency format ('%s').", $string));
}
private function throwUnlikeCurrenciesException(PhortuneCurrency $other) {
throw new Exception(
pht(
'Trying to operate on unlike currencies ("%s" and "%s")!',
$this->currency,
$other->currency));
}
public function add(PhortuneCurrency $other) {
if ($this->currency !== $other->currency) {
$this->throwUnlikeCurrenciesException($other);
}
$currency = new PhortuneCurrency();
// TODO: This should check for integer overflows, etc.
$currency->value = $this->value + $other->value;
$currency->currency = $this->currency;
return $currency;
}
public function subtract(PhortuneCurrency $other) {
if ($this->currency !== $other->currency) {
$this->throwUnlikeCurrenciesException($other);
}
$currency = new PhortuneCurrency();
// TODO: This should check for integer overflows, etc.
$currency->value = $this->value - $other->value;
$currency->currency = $this->currency;
return $currency;
}
public function isEqualTo(PhortuneCurrency $other) {
if ($this->currency !== $other->currency) {
$this->throwUnlikeCurrenciesException($other);
}
return ($this->value === $other->value);
}
public function negate() {
$currency = new PhortuneCurrency();
$currency->value = -$this->value;
$currency->currency = $this->currency;
return $currency;
}
public function isPositive() {
return ($this->value > 0);
}
public function isGreaterThan(PhortuneCurrency $other) {
if ($this->currency !== $other->currency) {
$this->throwUnlikeCurrenciesException($other);
}
return $this->value > $other->value;
}
/**
* Assert that a currency value lies within a range.
*
* Throws if the value is not between the minimum and maximum, inclusive.
*
* In particular, currency values can be negative (to represent a debt or
* credit), so checking against zero may be useful to make sure a value
* has the expected sign.
*
* @param string|null Currency string, or null to skip check.
* @param string|null Currency string, or null to skip check.
* @return this
*/
public function assertInRange($minimum, $maximum) {
if ($minimum !== null && $maximum !== null) {
$min = self::newFromString($minimum);
$max = self::newFromString($maximum);
if ($min->value > $max->value) {
throw new Exception(
pht(
'Range (%s - %s) is not valid!',
$min->formatForDisplay(),
$max->formatForDisplay()));
}
}
if ($minimum !== null) {
$min = self::newFromString($minimum);
if ($min->value > $this->value) {
throw new Exception(
pht(
'Minimum allowed amount is %s.',
$min->formatForDisplay()));
}
}
if ($maximum !== null) {
$max = self::newFromString($maximum);
if ($max->value < $this->value) {
throw new Exception(
pht(
'Maximum allowed amount is %s.',
$max->formatForDisplay()));
}
}
return $this;
}
}
diff --git a/src/applications/phortune/currency/PhortuneCurrencySerializer.php b/src/applications/phortune/currency/PhortuneCurrencySerializer.php
index affcb7751..b5f756044 100644
--- a/src/applications/phortune/currency/PhortuneCurrencySerializer.php
+++ b/src/applications/phortune/currency/PhortuneCurrencySerializer.php
@@ -1,20 +1,21 @@
<?php
final class PhortuneCurrencySerializer extends PhabricatorLiskSerializer {
public function willReadValue($value) {
return PhortuneCurrency::newFromString($value);
}
public function willWriteValue($value) {
if (!($value instanceof PhortuneCurrency)) {
throw new Exception(
pht(
'Trying to save object with a currency column, but the column '.
- 'value is not a PhortuneCurrency object.'));
+ 'value is not a %s object.',
+ 'PhortuneCurrency'));
}
return $value->serializeForStorage();
}
}
diff --git a/src/applications/phortune/exception/PhortuneNotImplementedException.php b/src/applications/phortune/exception/PhortuneNotImplementedException.php
index 7912ab30f..eac3b3b7a 100644
--- a/src/applications/phortune/exception/PhortuneNotImplementedException.php
+++ b/src/applications/phortune/exception/PhortuneNotImplementedException.php
@@ -1,11 +1,12 @@
<?php
final class PhortuneNotImplementedException extends Exception {
public function __construct(PhortunePaymentProvider $provider) {
- $class = get_class($provider);
return parent::__construct(
- "Provider '{$class}' does not implement this method.");
+ pht(
+ "Provider '%s' does not implement this method.",
+ get_class($provider)));
}
}
diff --git a/src/applications/phortune/mail/PhortuneCartReplyHandler.php b/src/applications/phortune/mail/PhortuneCartReplyHandler.php
index 4f333d830..9c781fc90 100644
--- a/src/applications/phortune/mail/PhortuneCartReplyHandler.php
+++ b/src/applications/phortune/mail/PhortuneCartReplyHandler.php
@@ -1,16 +1,16 @@
<?php
final class PhortuneCartReplyHandler
extends PhabricatorApplicationTransactionReplyHandler {
public function validateMailReceiver($mail_receiver) {
if (!($mail_receiver instanceof PhortuneCart)) {
- throw new Exception('Mail receiver is not a PhortuneCart!');
+ throw new Exception(pht('Mail receiver is not a %s!', 'PhortuneCart'));
}
}
public function getObjectPrefix() {
return 'CART';
}
}
diff --git a/src/applications/phortune/management/PhabricatorPhortuneManagementInvoiceWorkflow.php b/src/applications/phortune/management/PhabricatorPhortuneManagementInvoiceWorkflow.php
index e1053e74a..673bf812d 100644
--- a/src/applications/phortune/management/PhabricatorPhortuneManagementInvoiceWorkflow.php
+++ b/src/applications/phortune/management/PhabricatorPhortuneManagementInvoiceWorkflow.php
@@ -1,166 +1,175 @@
<?php
final class PhabricatorPhortuneManagementInvoiceWorkflow
extends PhabricatorPhortuneManagementWorkflow {
protected function didConstruct() {
$this
->setName('invoice')
->setSynopsis(
pht(
'Invoices a subscription for a given billing period. This can '.
'charge payment accounts twice.'))
->setArguments(
array(
array(
'name' => 'subscription',
'param' => 'phid',
'help' => pht('Subscription to invoice.'),
),
array(
'name' => 'now',
'param' => 'time',
'help' => pht(
'Bill as though the current time is a specific time.'),
),
array(
'name' => 'last',
'param' => 'time',
'help' => pht('Set the start of the billing period.'),
),
array(
'name' => 'next',
'param' => 'time',
'help' => pht('Set the end of the billing period.'),
),
array(
'name' => 'auto-range',
'help' => pht('Automatically use the current billing period.'),
),
array(
'name' => 'force',
'help' => pht(
'Skip the prompt warning you that this operation is '.
'potentially dangerous.'),
),
));
}
public function execute(PhutilArgumentParser $args) {
$console = PhutilConsole::getConsole();
$viewer = $this->getViewer();
$subscription_phid = $args->getArg('subscription');
if (!$subscription_phid) {
throw new PhutilArgumentUsageException(
pht(
- 'Specify which subscription to invoice with --subscription.'));
+ 'Specify which subscription to invoice with %s.',
+ '--subscription'));
}
$subscription = id(new PhortuneSubscriptionQuery())
->setViewer($viewer)
->withPHIDs(array($subscription_phid))
->needTriggers(true)
->executeOne();
if (!$subscription) {
throw new PhutilArgumentUsageException(
pht(
'Unable to load subscription with PHID "%s".',
$subscription_phid));
}
$now = $args->getArg('now');
$now = $this->parseTimeArgument($now);
if (!$now) {
$now = PhabricatorTime::getNow();
}
$time_guard = PhabricatorTime::pushTime($now, date_default_timezone_get());
$console->writeOut(
"%s\n",
pht(
'Set current time to %s.',
phabricator_datetime(PhabricatorTime::getNow(), $viewer)));
$auto_range = $args->getArg('auto-range');
$last_arg = $args->getArg('last');
$next_arg = $args->getARg('next');
if (!$auto_range && !$last_arg && !$next_arg) {
throw new PhutilArgumentUsageException(
pht(
- 'Specify a billing range with --last and --next, or use '.
- '--auto-range.'));
+ 'Specify a billing range with %s and %s, or use %s.',
+ '--last',
+ '--next',
+ '--auto-range'));
} else if (!$auto_range & (!$last_arg || !$next_arg)) {
throw new PhutilArgumentUsageException(
pht(
- 'When specifying --last or --next, you must specify both arguments '.
- 'to define the beginning and end of the billing range.'));
+ 'When specifying %s or %s, you must specify both arguments '.
+ 'to define the beginning and end of the billing range.',
+ '--last',
+ '--next'));
} else if (!$auto_range && ($last_arg && $next_arg)) {
$last_time = $this->parseTimeArgument($args->getArg('last'));
$next_time = $this->parseTimeArgument($args->getArg('next'));
} else if ($auto_range && ($last_arg || $next_arg)) {
throw new PhutilArgumentUsageException(
pht(
- 'Use either --auto-range or --last and --next to specify the '.
- 'billing range, but not both.'));
+ 'Use either %s or %s and %s to specify the '.
+ 'billing range, but not both.',
+ '--auto-range',
+ '--last',
+ '--next'));
} else {
$trigger = $subscription->getTrigger();
$event = $trigger->getEvent();
if (!$event) {
throw new PhutilArgumentUsageException(
pht(
- 'Unable to calculate --auto-range, this subscription has not been '.
+ 'Unable to calculate %s, this subscription has not been '.
'scheduled for billing yet. Wait for the trigger daemon to '.
- 'schedule the subscription.'));
+ 'schedule the subscription.',
+ '--auto-range'));
}
$last_time = $event->getLastEventEpoch();
$next_time = $event->getNextEventEpoch();
}
$console->writeOut(
"%s\n",
pht(
'Preparing to invoice subscription "%s" from %s to %s.',
$subscription->getSubscriptionName(),
($last_time
? phabricator_datetime($last_time, $viewer)
: pht('subscription creation')),
phabricator_datetime($next_time, $viewer)));
PhabricatorWorker::setRunAllTasksInProcess(true);
if (!$args->getArg('force')) {
$console->writeOut(
"**<bg:yellow> %s </bg>**\n%s\n",
pht('WARNING'),
phutil_console_wrap(
pht(
'Manually invoicing will double bill payment accounts if the '.
'range overlaps an existing or future invoice. This script is '.
'intended for testing and development, and should not be part '.
'of routine billing operations. If you continue, you may '.
'incorrectly overcharge customers.')));
if (!phutil_console_confirm(pht('Really invoice this subscription?'))) {
throw new Exception(pht('Declining to invoice.'));
}
}
PhabricatorWorker::scheduleTask(
'PhortuneSubscriptionWorker',
array(
'subscriptionPHID' => $subscription->getPHID(),
'trigger.last-epoch' => $last_time,
'trigger.this-epoch' => $next_time,
'manual' => true,
),
array(
'objectPHID' => $subscription->getPHID(),
));
return 0;
}
}
diff --git a/src/applications/phortune/provider/PhortunePayPalPaymentProvider.php b/src/applications/phortune/provider/PhortunePayPalPaymentProvider.php
index fcc0d12bf..078141f8a 100644
--- a/src/applications/phortune/provider/PhortunePayPalPaymentProvider.php
+++ b/src/applications/phortune/provider/PhortunePayPalPaymentProvider.php
@@ -1,506 +1,505 @@
<?php
final class PhortunePayPalPaymentProvider extends PhortunePaymentProvider {
const PAYPAL_API_USERNAME = 'paypal.api-username';
const PAYPAL_API_PASSWORD = 'paypal.api-password';
const PAYPAL_API_SIGNATURE = 'paypal.api-signature';
const PAYPAL_MODE = 'paypal.mode';
public function isAcceptingLivePayments() {
$mode = $this->getProviderConfig()->getMetadataValue(self::PAYPAL_MODE);
return ($mode === 'live');
}
public function getName() {
return pht('PayPal');
}
public function getConfigureName() {
return pht('Add PayPal Payments Account');
}
public function getConfigureDescription() {
return pht(
'Allows you to accept various payment instruments with a paypal.com '.
'account.');
}
public function getConfigureProvidesDescription() {
- return pht(
- 'This merchant accepts payments via PayPal.');
+ return pht('This merchant accepts payments via PayPal.');
}
public function getConfigureInstructions() {
return pht(
"To configure PayPal, register or log into an existing account on ".
"[[https://paypal.com | paypal.com]] (for live payments) or ".
"[[https://sandbox.paypal.com | sandbox.paypal.com]] (for test ".
"payments). Once logged in:\n\n".
" - Navigate to {nav Tools > API Access}.\n".
" - Choose **View API Signature**.\n".
" - Copy the **API Username**, **API Password** and **Signature** ".
" into the fields above.\n\n".
"You can select whether the provider operates in test mode or ".
"accepts live payments using the **Mode** dropdown above.\n\n".
"You can either use `sandbox.paypal.com` to retrieve live credentials, ".
"or `paypal.com` to retrieve live credentials.");
}
public function getAllConfigurableProperties() {
return array(
self::PAYPAL_API_USERNAME,
self::PAYPAL_API_PASSWORD,
self::PAYPAL_API_SIGNATURE,
self::PAYPAL_MODE,
);
}
public function getAllConfigurableSecretProperties() {
return array(
self::PAYPAL_API_PASSWORD,
self::PAYPAL_API_SIGNATURE,
);
}
public function processEditForm(
AphrontRequest $request,
array $values) {
$errors = array();
$issues = array();
if (!strlen($values[self::PAYPAL_API_USERNAME])) {
$errors[] = pht('PayPal API Username is required.');
$issues[self::PAYPAL_API_USERNAME] = pht('Required');
}
if (!strlen($values[self::PAYPAL_API_PASSWORD])) {
$errors[] = pht('PayPal API Password is required.');
$issues[self::PAYPAL_API_PASSWORD] = pht('Required');
}
if (!strlen($values[self::PAYPAL_API_SIGNATURE])) {
$errors[] = pht('PayPal API Signature is required.');
$issues[self::PAYPAL_API_SIGNATURE] = pht('Required');
}
if (!strlen($values[self::PAYPAL_MODE])) {
$errors[] = pht('Mode is required.');
$issues[self::PAYPAL_MODE] = pht('Required');
}
return array($errors, $issues, $values);
}
public function extendEditForm(
AphrontRequest $request,
AphrontFormView $form,
array $values,
array $issues) {
$form
->appendChild(
id(new AphrontFormTextControl())
->setName(self::PAYPAL_API_USERNAME)
->setValue($values[self::PAYPAL_API_USERNAME])
->setError(idx($issues, self::PAYPAL_API_USERNAME, true))
->setLabel(pht('Paypal API Username')))
->appendChild(
id(new AphrontFormTextControl())
->setName(self::PAYPAL_API_PASSWORD)
->setValue($values[self::PAYPAL_API_PASSWORD])
->setError(idx($issues, self::PAYPAL_API_PASSWORD, true))
->setLabel(pht('Paypal API Password')))
->appendChild(
id(new AphrontFormTextControl())
->setName(self::PAYPAL_API_SIGNATURE)
->setValue($values[self::PAYPAL_API_SIGNATURE])
->setError(idx($issues, self::PAYPAL_API_SIGNATURE, true))
->setLabel(pht('Paypal API Signature')))
->appendChild(
id(new AphrontFormSelectControl())
->setName(self::PAYPAL_MODE)
->setValue($values[self::PAYPAL_MODE])
->setError(idx($issues, self::PAYPAL_MODE))
->setLabel(pht('Mode'))
->setOptions(
array(
'test' => pht('Test Mode'),
'live' => pht('Live Mode'),
)));
return;
}
public function canRunConfigurationTest() {
return true;
}
public function runConfigurationTest() {
$result = $this
->newPaypalAPICall()
->setRawPayPalQuery('GetBalance', array())
->resolve();
}
public function getPaymentMethodDescription() {
return pht('Credit Card or PayPal Account');
}
public function getPaymentMethodIcon() {
return 'PayPal';
}
public function getPaymentMethodProviderDescription() {
return 'PayPal';
}
protected function executeCharge(
PhortunePaymentMethod $payment_method,
PhortuneCharge $charge) {
throw new Exception('!');
}
protected function executeRefund(
PhortuneCharge $charge,
PhortuneCharge $refund) {
$transaction_id = $charge->getMetadataValue('paypal.transactionID');
if (!$transaction_id) {
throw new Exception(pht('Charge has no transaction ID!'));
}
$refund_amount = $refund->getAmountAsCurrency()->negate();
$refund_currency = $refund_amount->getCurrency();
$refund_value = $refund_amount->formatBareValue();
$params = array(
'TRANSACTIONID' => $transaction_id,
'REFUNDTYPE' => 'Partial',
'AMT' => $refund_value,
'CURRENCYCODE' => $refund_currency,
);
$result = $this
->newPaypalAPICall()
->setRawPayPalQuery('RefundTransaction', $params)
->resolve();
$charge->setMetadataValue(
'paypal.refundID',
$result['REFUNDTRANSACTIONID']);
}
public function updateCharge(PhortuneCharge $charge) {
$transaction_id = $charge->getMetadataValue('paypal.transactionID');
if (!$transaction_id) {
throw new Exception(pht('Charge has no transaction ID!'));
}
$params = array(
'TRANSACTIONID' => $transaction_id,
);
$result = $this
->newPaypalAPICall()
->setRawPayPalQuery('GetTransactionDetails', $params)
->resolve();
$is_charge = false;
$is_fail = false;
switch ($result['PAYMENTSTATUS']) {
case 'Processed':
case 'Completed':
case 'Completed-Funds-Held':
$is_charge = true;
break;
case 'Partially-Refunded':
case 'Refunded':
case 'Reversed':
case 'Canceled-Reversal':
// TODO: Handle these.
return;
case 'In-Progress':
case 'Pending':
// TODO: Also handle these better?
return;
case 'Denied':
case 'Expired':
case 'Failed':
case 'None':
case 'Voided':
default:
$is_fail = true;
break;
}
if ($charge->getStatus() == PhortuneCharge::STATUS_HOLD) {
$cart = $charge->getCart();
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
if ($is_charge) {
$cart->didApplyCharge($charge);
} else if ($is_fail) {
$cart->didFailCharge($charge);
}
unset($unguarded);
}
}
private function getPaypalAPIUsername() {
return $this
->getProviderConfig()
->getMetadataValue(self::PAYPAL_API_USERNAME);
}
private function getPaypalAPIPassword() {
return $this
->getProviderConfig()
->getMetadataValue(self::PAYPAL_API_PASSWORD);
}
private function getPaypalAPISignature() {
return $this
->getProviderConfig()
->getMetadataValue(self::PAYPAL_API_SIGNATURE);
}
/* -( One-Time Payments )-------------------------------------------------- */
public function canProcessOneTimePayments() {
return true;
}
/* -( Controllers )-------------------------------------------------------- */
public function canRespondToControllerAction($action) {
switch ($action) {
case 'checkout':
case 'charge':
case 'cancel':
return true;
}
return parent::canRespondToControllerAction();
}
public function processControllerRequest(
PhortuneProviderActionController $controller,
AphrontRequest $request) {
$viewer = $request->getUser();
$cart = $controller->loadCart($request->getInt('cartID'));
if (!$cart) {
return new Aphront404Response();
}
$charge = $controller->loadActiveCharge($cart);
switch ($controller->getAction()) {
case 'checkout':
if ($charge) {
throw new Exception(pht('Cart is already charging!'));
}
break;
case 'charge':
case 'cancel':
if (!$charge) {
throw new Exception(pht('Cart is not charging yet!'));
}
break;
}
switch ($controller->getAction()) {
case 'checkout':
$return_uri = $this->getControllerURI(
'charge',
array(
'cartID' => $cart->getID(),
));
$cancel_uri = $this->getControllerURI(
'cancel',
array(
'cartID' => $cart->getID(),
));
$price = $cart->getTotalPriceAsCurrency();
$charge = $cart->willApplyCharge($viewer, $this);
$params = array(
'PAYMENTREQUEST_0_AMT' => $price->formatBareValue(),
'PAYMENTREQUEST_0_CURRENCYCODE' => $price->getCurrency(),
'PAYMENTREQUEST_0_PAYMENTACTION' => 'Sale',
'PAYMENTREQUEST_0_CUSTOM' => $charge->getPHID(),
'PAYMENTREQUEST_0_DESC' => $cart->getName(),
'RETURNURL' => $return_uri,
'CANCELURL' => $cancel_uri,
// TODO: This should be cart-dependent if we eventually support
// physical goods.
'NOSHIPPING' => '1',
);
$result = $this
->newPaypalAPICall()
->setRawPayPalQuery('SetExpressCheckout', $params)
->resolve();
$uri = new PhutilURI('https://www.sandbox.paypal.com/cgi-bin/webscr');
$uri->setQueryParams(
array(
'cmd' => '_express-checkout',
'token' => $result['TOKEN'],
));
$cart->setMetadataValue('provider.checkoutURI', (string)$uri);
$cart->save();
$charge->setMetadataValue('paypal.token', $result['TOKEN']);
$charge->save();
return id(new AphrontRedirectResponse())
->setIsExternal(true)
->setURI($uri);
case 'charge':
if ($cart->getStatus() !== PhortuneCart::STATUS_PURCHASING) {
return id(new AphrontRedirectResponse())
->setURI($cart->getCheckoutURI());
}
$token = $request->getStr('token');
$params = array(
'TOKEN' => $token,
);
$result = $this
->newPaypalAPICall()
->setRawPayPalQuery('GetExpressCheckoutDetails', $params)
->resolve();
if ($result['CUSTOM'] !== $charge->getPHID()) {
throw new Exception(
pht('Paypal checkout does not match Phortune charge!'));
}
if ($result['CHECKOUTSTATUS'] !== 'PaymentActionNotInitiated') {
return $controller->newDialog()
->setTitle(pht('Payment Already Processed'))
->appendParagraph(
pht(
'The payment response for this charge attempt has already '.
'been processed.'))
->addCancelButton($cart->getCheckoutURI(), pht('Continue'));
}
$price = $cart->getTotalPriceAsCurrency();
$params = array(
'TOKEN' => $token,
'PAYERID' => $result['PAYERID'],
'PAYMENTREQUEST_0_AMT' => $price->formatBareValue(),
'PAYMENTREQUEST_0_CURRENCYCODE' => $price->getCurrency(),
'PAYMENTREQUEST_0_PAYMENTACTION' => 'Sale',
);
$result = $this
->newPaypalAPICall()
->setRawPayPalQuery('DoExpressCheckoutPayment', $params)
->resolve();
$transaction_id = $result['PAYMENTINFO_0_TRANSACTIONID'];
$success = false;
$hold = false;
switch ($result['PAYMENTINFO_0_PAYMENTSTATUS']) {
case 'Processed':
case 'Completed':
case 'Completed-Funds-Held':
$success = true;
break;
case 'In-Progress':
case 'Pending':
// TODO: We can capture more information about this stuff.
$hold = true;
break;
case 'Denied':
case 'Expired':
case 'Failed':
case 'Partially-Refunded':
case 'Canceled-Reversal':
case 'None':
case 'Refunded':
case 'Reversed':
case 'Voided':
default:
// These are all failure states.
break;
}
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$charge->setMetadataValue('paypal.transactionID', $transaction_id);
$charge->save();
if ($success) {
$cart->didApplyCharge($charge);
$response = id(new AphrontRedirectResponse())->setURI(
$cart->getCheckoutURI());
} else if ($hold) {
$cart->didHoldCharge($charge);
$response = $controller
->newDialog()
->setTitle(pht('Charge On Hold'))
->appendParagraph(
pht('Your charge is on hold, for reasons?'))
->addCancelButton($cart->getCheckoutURI(), pht('Continue'));
} else {
$cart->didFailCharge($charge);
$response = $controller
->newDialog()
->setTitle(pht('Charge Failed'))
->addCancelButton($cart->getCheckoutURI(), pht('Continue'));
}
unset($unguarded);
return $response;
case 'cancel':
if ($cart->getStatus() === PhortuneCart::STATUS_PURCHASING) {
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
// TODO: Since the user cancelled this, we could conceivably just
// throw it away or make it more clear that it's a user cancel.
$cart->didFailCharge($charge);
unset($unguarded);
}
return id(new AphrontRedirectResponse())
->setURI($cart->getCheckoutURI());
}
throw new Exception(
pht('Unsupported action "%s".', $controller->getAction()));
}
private function newPaypalAPICall() {
if ($this->isAcceptingLivePayments()) {
$host = 'https://api-3t.paypal.com/nvp';
} else {
$host = 'https://api-3t.sandbox.paypal.com/nvp';
}
return id(new PhutilPayPalAPIFuture())
->setHost($host)
->setAPIUsername($this->getPaypalAPIUsername())
->setAPIPassword($this->getPaypalAPIPassword())
->setAPISignature($this->getPaypalAPISignature());
}
}
diff --git a/src/applications/phortune/provider/PhortuneStripePaymentProvider.php b/src/applications/phortune/provider/PhortuneStripePaymentProvider.php
index f751e3cbc..1e7c34829 100644
--- a/src/applications/phortune/provider/PhortuneStripePaymentProvider.php
+++ b/src/applications/phortune/provider/PhortuneStripePaymentProvider.php
@@ -1,381 +1,380 @@
<?php
final class PhortuneStripePaymentProvider extends PhortunePaymentProvider {
const STRIPE_PUBLISHABLE_KEY = 'stripe.publishable-key';
const STRIPE_SECRET_KEY = 'stripe.secret-key';
public function isAcceptingLivePayments() {
return preg_match('/_live_/', $this->getPublishableKey());
}
public function getName() {
return pht('Stripe');
}
public function getConfigureName() {
return pht('Add Stripe Payments Account');
}
public function getConfigureDescription() {
return pht(
'Allows you to accept credit or debit card payments with a '.
'stripe.com account.');
}
public function getConfigureProvidesDescription() {
- return pht(
- 'This merchant accepts credit and debit cards via Stripe.');
+ return pht('This merchant accepts credit and debit cards via Stripe.');
}
public function getPaymentMethodDescription() {
return pht('Add Credit or Debit Card (US and Canada)');
}
public function getPaymentMethodIcon() {
return 'Stripe';
}
public function getPaymentMethodProviderDescription() {
return pht('Processed by Stripe');
}
public function getDefaultPaymentMethodDisplayName(
PhortunePaymentMethod $method) {
return pht('Credit/Debit Card');
}
public function getAllConfigurableProperties() {
return array(
self::STRIPE_PUBLISHABLE_KEY,
self::STRIPE_SECRET_KEY,
);
}
public function getAllConfigurableSecretProperties() {
return array(
self::STRIPE_SECRET_KEY,
);
}
public function processEditForm(
AphrontRequest $request,
array $values) {
$errors = array();
$issues = array();
if (!strlen($values[self::STRIPE_SECRET_KEY])) {
$errors[] = pht('Stripe Secret Key is required.');
$issues[self::STRIPE_SECRET_KEY] = pht('Required');
}
if (!strlen($values[self::STRIPE_PUBLISHABLE_KEY])) {
$errors[] = pht('Stripe Publishable Key is required.');
$issues[self::STRIPE_PUBLISHABLE_KEY] = pht('Required');
}
return array($errors, $issues, $values);
}
public function extendEditForm(
AphrontRequest $request,
AphrontFormView $form,
array $values,
array $issues) {
$form
->appendChild(
id(new AphrontFormTextControl())
->setName(self::STRIPE_SECRET_KEY)
->setValue($values[self::STRIPE_SECRET_KEY])
->setError(idx($issues, self::STRIPE_SECRET_KEY, true))
->setLabel(pht('Stripe Secret Key')))
->appendChild(
id(new AphrontFormTextControl())
->setName(self::STRIPE_PUBLISHABLE_KEY)
->setValue($values[self::STRIPE_PUBLISHABLE_KEY])
->setError(idx($issues, self::STRIPE_PUBLISHABLE_KEY, true))
->setLabel(pht('Stripe Publishable Key')));
}
public function getConfigureInstructions() {
return pht(
"To configure Stripe, register or log in to an existing account on ".
"[[https://stripe.com | stripe.com]]. Once logged in:\n\n".
" - Go to {nav icon=user, name=Your Account > Account Settings ".
"> API Keys}\n".
" - Copy the **Secret Key** and **Publishable Key** into the fields ".
"above.\n\n".
"You can either use the test keys to add this provider in test mode, ".
"or the live keys to accept live payments.");
}
public function canRunConfigurationTest() {
return true;
}
public function runConfigurationTest() {
$this->loadStripeAPILibraries();
$secret_key = $this->getSecretKey();
$account = Stripe_Account::retrieve($secret_key);
}
/**
* @phutil-external-symbol class Stripe_Charge
* @phutil-external-symbol class Stripe_CardError
* @phutil-external-symbol class Stripe_Account
*/
protected function executeCharge(
PhortunePaymentMethod $method,
PhortuneCharge $charge) {
$this->loadStripeAPILibraries();
$price = $charge->getAmountAsCurrency();
$secret_key = $this->getSecretKey();
$params = array(
'amount' => $price->getValueInUSDCents(),
'currency' => $price->getCurrency(),
'customer' => $method->getMetadataValue('stripe.customerID'),
'description' => $charge->getPHID(),
'capture' => true,
);
$stripe_charge = Stripe_Charge::create($params, $secret_key);
$id = $stripe_charge->id;
if (!$id) {
- throw new Exception('Stripe charge call did not return an ID!');
+ throw new Exception(pht('Stripe charge call did not return an ID!'));
}
$charge->setMetadataValue('stripe.chargeID', $id);
$charge->save();
}
protected function executeRefund(
PhortuneCharge $charge,
PhortuneCharge $refund) {
$this->loadStripeAPILibraries();
$charge_id = $charge->getMetadataValue('stripe.chargeID');
if (!$charge_id) {
throw new Exception(
pht('Unable to refund charge; no Stripe chargeID!'));
}
$refund_cents = $refund
->getAmountAsCurrency()
->negate()
->getValueInUSDCents();
$secret_key = $this->getSecretKey();
$params = array(
'amount' => $refund_cents,
);
$stripe_charge = Stripe_Charge::retrieve($charge_id, $secret_key);
$stripe_refund = $stripe_charge->refunds->create($params);
$id = $stripe_refund->id;
if (!$id) {
throw new Exception(pht('Stripe refund call did not return an ID!'));
}
$charge->setMetadataValue('stripe.refundID', $id);
$charge->save();
}
public function updateCharge(PhortuneCharge $charge) {
$this->loadStripeAPILibraries();
$charge_id = $charge->getMetadataValue('stripe.chargeID');
if (!$charge_id) {
throw new Exception(
pht('Unable to update charge; no Stripe chargeID!'));
}
$secret_key = $this->getSecretKey();
$stripe_charge = Stripe_Charge::retrieve($charge_id, $secret_key);
// TODO: Deal with disputes / chargebacks / surprising refunds.
}
private function getPublishableKey() {
return $this
->getProviderConfig()
->getMetadataValue(self::STRIPE_PUBLISHABLE_KEY);
}
private function getSecretKey() {
return $this
->getProviderConfig()
->getMetadataValue(self::STRIPE_SECRET_KEY);
}
/* -( Adding Payment Methods )--------------------------------------------- */
public function canCreatePaymentMethods() {
return true;
}
/**
* @phutil-external-symbol class Stripe_Token
* @phutil-external-symbol class Stripe_Customer
*/
public function createPaymentMethodFromRequest(
AphrontRequest $request,
PhortunePaymentMethod $method,
array $token) {
$this->loadStripeAPILibraries();
$errors = array();
$secret_key = $this->getSecretKey();
$stripe_token = $token['stripeCardToken'];
// First, make sure the token is valid.
$info = id(new Stripe_Token())->retrieve($stripe_token, $secret_key);
$account_phid = $method->getAccountPHID();
$author_phid = $method->getAuthorPHID();
$params = array(
'card' => $stripe_token,
'description' => $account_phid.':'.$author_phid,
);
// Then, we need to create a Customer in order to be able to charge
// the card more than once. We create one Customer for each card;
// they do not map to PhortuneAccounts because we allow an account to
// have more than one active card.
$customer = Stripe_Customer::create($params, $secret_key);
$card = $info->card;
$method
->setBrand($card->brand)
->setLastFourDigits($card->last4)
->setExpires($card->exp_year, $card->exp_month)
->setMetadata(
array(
'type' => 'stripe.customer',
'stripe.customerID' => $customer->id,
'stripe.cardToken' => $stripe_token,
));
return $errors;
}
public function renderCreatePaymentMethodForm(
AphrontRequest $request,
array $errors) {
$ccform = id(new PhortuneCreditCardForm())
->setSecurityAssurance(
pht('Payments are processed securely by Stripe.'))
->setUser($request->getUser())
->setErrors($errors)
->addScript('https://js.stripe.com/v2/');
Javelin::initBehavior(
'stripe-payment-form',
array(
'stripePublishableKey' => $this->getPublishableKey(),
'formID' => $ccform->getFormID(),
));
return $ccform->buildForm();
}
private function getStripeShortErrorCode($error_code) {
$prefix = 'cc:stripe:';
if (strncmp($error_code, $prefix, strlen($prefix))) {
return null;
}
return substr($error_code, strlen($prefix));
}
public function validateCreatePaymentMethodToken(array $token) {
return isset($token['stripeCardToken']);
}
public function translateCreatePaymentMethodErrorCode($error_code) {
$short_code = $this->getStripeShortErrorCode($error_code);
if ($short_code) {
static $map = array(
'error:invalid_number' => PhortuneErrCode::ERR_CC_INVALID_NUMBER,
'error:invalid_cvc' => PhortuneErrCode::ERR_CC_INVALID_CVC,
'error:invalid_expiry_month' => PhortuneErrCode::ERR_CC_INVALID_EXPIRY,
'error:invalid_expiry_year' => PhortuneErrCode::ERR_CC_INVALID_EXPIRY,
);
if (isset($map[$short_code])) {
return $map[$short_code];
}
}
return $error_code;
}
/**
* See https://stripe.com/docs/api#errors for more information on possible
* errors.
*/
public function getCreatePaymentMethodErrorMessage($error_code) {
$short_code = $this->getStripeShortErrorCode($error_code);
if (!$short_code) {
return null;
}
switch ($short_code) {
case 'error:incorrect_number':
$error_key = 'number';
$message = pht('Invalid or incorrect credit card number.');
break;
case 'error:incorrect_cvc':
$error_key = 'cvc';
$message = pht('Card CVC is invalid or incorrect.');
break;
$error_key = 'exp';
$message = pht('Card expiration date is invalid or incorrect.');
break;
case 'error:invalid_expiry_month':
case 'error:invalid_expiry_year':
case 'error:invalid_cvc':
case 'error:invalid_number':
// NOTE: These should be translated into Phortune error codes earlier,
// so we don't expect to receive them here. They are listed for clarity
// and completeness. If we encounter one, we treat it as an unknown
// error.
break;
case 'error:invalid_amount':
case 'error:missing':
case 'error:card_declined':
case 'error:expired_card':
case 'error:duplicate_transaction':
case 'error:processing_error':
default:
// NOTE: These errors currently don't recevive a detailed message.
// NOTE: We can also end up here with "http:nnn" messages.
// TODO: At least some of these should have a better message, or be
// translated into common errors above.
break;
}
return null;
}
private function loadStripeAPILibraries() {
$root = dirname(phutil_get_library_root('phabricator'));
require_once $root.'/externals/stripe-php/lib/Stripe.php';
}
}
diff --git a/src/applications/phortune/storage/PhortuneCart.php b/src/applications/phortune/storage/PhortuneCart.php
index 70ecbb21c..22dc27a9e 100644
--- a/src/applications/phortune/storage/PhortuneCart.php
+++ b/src/applications/phortune/storage/PhortuneCart.php
@@ -1,689 +1,694 @@
<?php
final class PhortuneCart extends PhortuneDAO
implements
PhabricatorApplicationTransactionInterface,
PhabricatorPolicyInterface {
const STATUS_BUILDING = 'cart:building';
const STATUS_READY = 'cart:ready';
const STATUS_PURCHASING = 'cart:purchasing';
const STATUS_CHARGED = 'cart:charged';
const STATUS_HOLD = 'cart:hold';
const STATUS_REVIEW = 'cart:review';
const STATUS_PURCHASED = 'cart:purchased';
protected $accountPHID;
protected $authorPHID;
protected $merchantPHID;
protected $subscriptionPHID;
protected $cartClass;
protected $status;
protected $metadata = array();
protected $mailKey;
protected $isInvoice;
private $account = self::ATTACHABLE;
private $purchases = self::ATTACHABLE;
private $implementation = self::ATTACHABLE;
private $merchant = self::ATTACHABLE;
public static function initializeNewCart(
PhabricatorUser $actor,
PhortuneAccount $account,
PhortuneMerchant $merchant) {
$cart = id(new PhortuneCart())
->setAuthorPHID($actor->getPHID())
->setStatus(self::STATUS_BUILDING)
->setAccountPHID($account->getPHID())
->setIsInvoice(0)
->attachAccount($account)
->setMerchantPHID($merchant->getPHID())
->attachMerchant($merchant);
$cart->account = $account;
$cart->purchases = array();
return $cart;
}
public function newPurchase(
PhabricatorUser $actor,
PhortuneProduct $product) {
$purchase = PhortunePurchase::initializeNewPurchase($actor, $product)
->setAccountPHID($this->getAccount()->getPHID())
->setCartPHID($this->getPHID())
->save();
$this->purchases[] = $purchase;
return $purchase;
}
public static function getStatusNameMap() {
return array(
self::STATUS_BUILDING => pht('Building'),
self::STATUS_READY => pht('Ready'),
self::STATUS_PURCHASING => pht('Purchasing'),
self::STATUS_CHARGED => pht('Charged'),
self::STATUS_HOLD => pht('Hold'),
self::STATUS_REVIEW => pht('Review'),
self::STATUS_PURCHASED => pht('Purchased'),
);
}
public static function getNameForStatus($status) {
return idx(self::getStatusNameMap(), $status, $status);
}
public function activateCart() {
$this->openTransaction();
$this->beginReadLocking();
$copy = clone $this;
$copy->reload();
if ($copy->getStatus() !== self::STATUS_BUILDING) {
throw new Exception(
pht(
- 'Cart has wrong status ("%s") to call willApplyCharge().',
- $copy->getStatus()));
+ 'Cart has wrong status ("%s") to call %s.',
+ $copy->getStatus(),
+ 'willApplyCharge()'));
}
$this->setStatus(self::STATUS_READY)->save();
$this->endReadLocking();
$this->saveTransaction();
$this->recordCartTransaction(PhortuneCartTransaction::TYPE_CREATED);
return $this;
}
public function willApplyCharge(
PhabricatorUser $actor,
PhortunePaymentProvider $provider,
PhortunePaymentMethod $method = null) {
$account = $this->getAccount();
$charge = PhortuneCharge::initializeNewCharge()
->setAccountPHID($account->getPHID())
->setCartPHID($this->getPHID())
->setAuthorPHID($actor->getPHID())
->setMerchantPHID($this->getMerchant()->getPHID())
->setProviderPHID($provider->getProviderConfig()->getPHID())
->setAmountAsCurrency($this->getTotalPriceAsCurrency());
if ($method) {
$charge->setPaymentMethodPHID($method->getPHID());
}
$this->openTransaction();
$this->beginReadLocking();
$copy = clone $this;
$copy->reload();
if ($copy->getStatus() !== self::STATUS_READY) {
throw new Exception(
pht(
- 'Cart has wrong status ("%s") to call willApplyCharge(), '.
- 'expected "%s".',
+ 'Cart has wrong status ("%s") to call %s, expected "%s".',
$copy->getStatus(),
+ 'willApplyCharge()',
self::STATUS_READY));
}
$charge->save();
$this->setStatus(self::STATUS_PURCHASING)->save();
$this->endReadLocking();
$this->saveTransaction();
return $charge;
}
public function didHoldCharge(PhortuneCharge $charge) {
$charge->setStatus(PhortuneCharge::STATUS_HOLD);
$this->openTransaction();
$this->beginReadLocking();
$copy = clone $this;
$copy->reload();
if ($copy->getStatus() !== self::STATUS_PURCHASING) {
throw new Exception(
pht(
- 'Cart has wrong status ("%s") to call didHoldCharge(), '.
- 'expected "%s".',
+ 'Cart has wrong status ("%s") to call %s, expected "%s".',
$copy->getStatus(),
+ 'didHoldCharge()',
self::STATUS_PURCHASING));
}
$charge->save();
$this->setStatus(self::STATUS_HOLD)->save();
$this->endReadLocking();
$this->saveTransaction();
$this->recordCartTransaction(PhortuneCartTransaction::TYPE_HOLD);
}
public function didApplyCharge(PhortuneCharge $charge) {
$charge->setStatus(PhortuneCharge::STATUS_CHARGED);
$this->openTransaction();
$this->beginReadLocking();
$copy = clone $this;
$copy->reload();
if (($copy->getStatus() !== self::STATUS_PURCHASING) &&
($copy->getStatus() !== self::STATUS_HOLD)) {
throw new Exception(
pht(
- 'Cart has wrong status ("%s") to call didApplyCharge().',
- $copy->getStatus()));
+ 'Cart has wrong status ("%s") to call %s.',
+ $copy->getStatus(),
+ 'didApplyCharge()'));
}
$charge->save();
$this->setStatus(self::STATUS_CHARGED)->save();
$this->endReadLocking();
$this->saveTransaction();
// TODO: Perform purchase review. Here, we would apply rules to determine
// whether the charge needs manual review (maybe making the decision via
// Herald, configuration, or by examining provider fraud data). For now,
// don't require review.
$needs_review = false;
if ($needs_review) {
$this->willReviewCart();
} else {
$this->didReviewCart();
}
return $this;
}
public function willReviewCart() {
$this->openTransaction();
$this->beginReadLocking();
$copy = clone $this;
$copy->reload();
if (($copy->getStatus() !== self::STATUS_CHARGED)) {
throw new Exception(
pht(
- 'Cart has wrong status ("%s") to call willReviewCart()!',
- $copy->getStatus()));
+ 'Cart has wrong status ("%s") to call %s!',
+ $copy->getStatus(),
+ 'willReviewCart()'));
}
$this->setStatus(self::STATUS_REVIEW)->save();
$this->endReadLocking();
$this->saveTransaction();
$this->recordCartTransaction(PhortuneCartTransaction::TYPE_REVIEW);
return $this;
}
public function didReviewCart() {
$this->openTransaction();
$this->beginReadLocking();
$copy = clone $this;
$copy->reload();
if (($copy->getStatus() !== self::STATUS_CHARGED) &&
($copy->getStatus() !== self::STATUS_REVIEW)) {
throw new Exception(
pht(
- 'Cart has wrong status ("%s") to call didReviewCart()!',
- $copy->getStatus()));
+ 'Cart has wrong status ("%s") to call %s!',
+ $copy->getStatus(),
+ 'didReviewCart()'));
}
foreach ($this->purchases as $purchase) {
$purchase->getProduct()->didPurchaseProduct($purchase);
}
$this->setStatus(self::STATUS_PURCHASED)->save();
$this->endReadLocking();
$this->saveTransaction();
$this->recordCartTransaction(PhortuneCartTransaction::TYPE_PURCHASED);
return $this;
}
public function didFailCharge(PhortuneCharge $charge) {
$charge->setStatus(PhortuneCharge::STATUS_FAILED);
$this->openTransaction();
$this->beginReadLocking();
$copy = clone $this;
$copy->reload();
if (($copy->getStatus() !== self::STATUS_PURCHASING) &&
($copy->getStatus() !== self::STATUS_HOLD)) {
throw new Exception(
pht(
- 'Cart has wrong status ("%s") to call didFailCharge().',
- $copy->getStatus()));
+ 'Cart has wrong status ("%s") to call %s.',
+ $copy->getStatus(),
+ 'didFailCharge()'));
}
$charge->save();
// Move the cart back into STATUS_READY so the user can try
// making the purchase again.
$this->setStatus(self::STATUS_READY)->save();
$this->endReadLocking();
$this->saveTransaction();
return $this;
}
public function willRefundCharge(
PhabricatorUser $actor,
PhortunePaymentProvider $provider,
PhortuneCharge $charge,
PhortuneCurrency $amount) {
if (!$amount->isPositive()) {
throw new Exception(
- pht('Trying to refund nonpositive amount of money!'));
+ pht('Trying to refund non-positive amount of money!'));
}
if ($amount->isGreaterThan($charge->getAmountRefundableAsCurrency())) {
throw new Exception(
pht('Trying to refund more money than remaining on charge!'));
}
if ($charge->getRefundedChargePHID()) {
throw new Exception(
pht('Trying to refund a refund!'));
}
if (($charge->getStatus() !== PhortuneCharge::STATUS_CHARGED) &&
($charge->getStatus() !== PhortuneCharge::STATUS_HOLD)) {
throw new Exception(
pht('Trying to refund an uncharged charge!'));
}
$refund_charge = PhortuneCharge::initializeNewCharge()
->setAccountPHID($this->getAccount()->getPHID())
->setCartPHID($this->getPHID())
->setAuthorPHID($actor->getPHID())
->setMerchantPHID($this->getMerchant()->getPHID())
->setProviderPHID($provider->getProviderConfig()->getPHID())
->setPaymentMethodPHID($charge->getPaymentMethodPHID())
->setRefundedChargePHID($charge->getPHID())
->setAmountAsCurrency($amount->negate());
$charge->openTransaction();
$charge->beginReadLocking();
$copy = clone $charge;
$copy->reload();
if ($copy->getRefundingPHID() !== null) {
throw new Exception(
pht('Trying to refund a charge which is already refunding!'));
}
$refund_charge->save();
$charge->setRefundingPHID($refund_charge->getPHID());
$charge->save();
$charge->endReadLocking();
$charge->saveTransaction();
return $refund_charge;
}
public function didRefundCharge(
PhortuneCharge $charge,
PhortuneCharge $refund) {
$refund->setStatus(PhortuneCharge::STATUS_CHARGED);
$this->openTransaction();
$this->beginReadLocking();
$copy = clone $charge;
$copy->reload();
if ($charge->getRefundingPHID() !== $refund->getPHID()) {
throw new Exception(
pht('Charge is in the wrong refunding state!'));
}
$charge->setRefundingPHID(null);
// NOTE: There's some trickiness here to get the signs right. Both
// these values are positive but the refund has a negative value.
$total_refunded = $charge
->getAmountRefundedAsCurrency()
->add($refund->getAmountAsCurrency()->negate());
$charge->setAmountRefundedAsCurrency($total_refunded);
$charge->save();
$refund->save();
$this->endReadLocking();
$this->saveTransaction();
$amount = $refund->getAmountAsCurrency()->negate();
foreach ($this->purchases as $purchase) {
$purchase->getProduct()->didRefundProduct($purchase, $amount);
}
return $this;
}
public function didFailRefund(
PhortuneCharge $charge,
PhortuneCharge $refund) {
$refund->setStatus(PhortuneCharge::STATUS_FAILED);
$this->openTransaction();
$this->beginReadLocking();
$copy = clone $charge;
$copy->reload();
if ($charge->getRefundingPHID() !== $refund->getPHID()) {
throw new Exception(
pht('Charge is in the wrong refunding state!'));
}
$charge->setRefundingPHID(null);
$charge->save();
$refund->save();
$this->endReadLocking();
$this->saveTransaction();
}
private function recordCartTransaction($type) {
$omnipotent_user = PhabricatorUser::getOmnipotentUser();
$phortune_phid = id(new PhabricatorPhortuneApplication())->getPHID();
$xactions = array();
$xactions[] = id(new PhortuneCartTransaction())
->setTransactionType($type)
->setNewValue(true);
$content_source = PhabricatorContentSource::newForSource(
PhabricatorContentSource::SOURCE_PHORTUNE,
array());
$editor = id(new PhortuneCartEditor())
->setActor($omnipotent_user)
->setActingAsPHID($phortune_phid)
->setContentSource($content_source)
->setContinueOnMissingFields(true)
->setContinueOnNoEffect(true);
$editor->applyTransactions($this, $xactions);
}
public function getName() {
return $this->getImplementation()->getName($this);
}
public function getDoneURI() {
return $this->getImplementation()->getDoneURI($this);
}
public function getDoneActionName() {
return $this->getImplementation()->getDoneActionName($this);
}
public function getCancelURI() {
return $this->getImplementation()->getCancelURI($this);
}
public function getDescription() {
return $this->getImplementation()->getDescription($this);
}
public function getDetailURI(PhortuneMerchant $authority = null) {
if ($authority) {
$prefix = 'merchant/'.$authority->getID().'/';
} else {
$prefix = '';
}
return '/phortune/'.$prefix.'cart/'.$this->getID().'/';
}
public function getCheckoutURI() {
return '/phortune/cart/'.$this->getID().'/checkout/';
}
public function canCancelOrder() {
try {
$this->assertCanCancelOrder();
return true;
} catch (Exception $ex) {
return false;
}
}
public function canRefundOrder() {
try {
$this->assertCanRefundOrder();
return true;
} catch (Exception $ex) {
return false;
}
}
public function assertCanCancelOrder() {
switch ($this->getStatus()) {
case self::STATUS_BUILDING:
throw new Exception(
pht(
'This order can not be cancelled because the application has not '.
'finished building it yet.'));
case self::STATUS_READY:
throw new Exception(
pht(
'This order can not be cancelled because it has not been placed.'));
}
return $this->getImplementation()->assertCanCancelOrder($this);
}
public function assertCanRefundOrder() {
switch ($this->getStatus()) {
case self::STATUS_BUILDING:
throw new Exception(
pht(
'This order can not be refunded because the application has not '.
'finished building it yet.'));
case self::STATUS_READY:
throw new Exception(
pht(
'This order can not be refunded because it has not been placed.'));
}
return $this->getImplementation()->assertCanRefundOrder($this);
}
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'metadata' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'status' => 'text32',
'cartClass' => 'text128',
'mailKey' => 'bytes20',
'subscriptionPHID' => 'phid?',
'isInvoice' => 'bool',
),
self::CONFIG_KEY_SCHEMA => array(
'key_account' => array(
'columns' => array('accountPHID'),
),
'key_merchant' => array(
'columns' => array('merchantPHID'),
),
'key_subscription' => array(
'columns' => array('subscriptionPHID'),
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhortuneCartPHIDType::TYPECONST);
}
public function save() {
if (!$this->getMailKey()) {
$this->setMailKey(Filesystem::readRandomCharacters(20));
}
return parent::save();
}
public function attachPurchases(array $purchases) {
assert_instances_of($purchases, 'PhortunePurchase');
$this->purchases = $purchases;
return $this;
}
public function getPurchases() {
return $this->assertAttached($this->purchases);
}
public function attachAccount(PhortuneAccount $account) {
$this->account = $account;
return $this;
}
public function getAccount() {
return $this->assertAttached($this->account);
}
public function attachMerchant(PhortuneMerchant $merchant) {
$this->merchant = $merchant;
return $this;
}
public function getMerchant() {
return $this->assertAttached($this->merchant);
}
public function attachImplementation(
PhortuneCartImplementation $implementation) {
$this->implementation = $implementation;
return $this;
}
public function getImplementation() {
return $this->assertAttached($this->implementation);
}
public function getTotalPriceAsCurrency() {
$prices = array();
foreach ($this->getPurchases() as $purchase) {
$prices[] = $purchase->getTotalPriceAsCurrency();
}
return PhortuneCurrency::newFromList($prices);
}
public function setMetadataValue($key, $value) {
$this->metadata[$key] = $value;
return $this;
}
public function getMetadataValue($key, $default = null) {
return idx($this->metadata, $key, $default);
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new PhortuneCartEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new PhortuneCartTransaction();
}
public function willRenderTimeline(
PhabricatorApplicationTransactionView $timeline,
AphrontRequest $request) {
return $timeline;
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
// NOTE: Both view and edit use the account's edit policy. We punch a hole
// through this for merchants, below.
return $this
->getAccount()
->getPolicy(PhabricatorPolicyCapability::CAN_EDIT);
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
if ($this->getAccount()->hasAutomaticCapability($capability, $viewer)) {
return true;
}
// If the viewer controls the merchant this order was placed with, they
// can view the order.
if ($capability == PhabricatorPolicyCapability::CAN_VIEW) {
$can_admin = PhabricatorPolicyFilter::hasCapability(
$viewer,
$this->getMerchant(),
PhabricatorPolicyCapability::CAN_EDIT);
if ($can_admin) {
return true;
}
}
return false;
}
public function describeAutomaticCapability($capability) {
return array(
pht('Orders inherit the policies of the associated account.'),
pht('The merchant you placed an order with can review and manage it.'),
);
}
}
diff --git a/src/applications/phortune/view/PhortuneCreditCardForm.php b/src/applications/phortune/view/PhortuneCreditCardForm.php
index 58bf4cda0..956e14499 100644
--- a/src/applications/phortune/view/PhortuneCreditCardForm.php
+++ b/src/applications/phortune/view/PhortuneCreditCardForm.php
@@ -1,122 +1,122 @@
<?php
final class PhortuneCreditCardForm {
private $formID;
private $scripts = array();
private $user;
private $errors = array();
private $cardNumberError;
private $cardCVCError;
private $cardExpirationError;
private $securityAssurance;
public function setSecurityAssurance($security_assurance) {
$this->securityAssurance = $security_assurance;
return $this;
}
public function getSecurityAssurance() {
return $this->securityAssurance;
}
public function setUser(PhabricatorUser $user) {
$this->user = $user;
return $this;
}
public function setErrors(array $errors) {
$this->errors = $errors;
return $this;
}
public function addScript($script_uri) {
$this->scripts[] = $script_uri;
return $this;
}
public function getFormID() {
if (!$this->formID) {
$this->formID = celerity_generate_unique_node_id();
}
return $this->formID;
}
public function buildForm() {
$form_id = $this->getFormID();
require_celerity_resource('phortune-credit-card-form-css');
require_celerity_resource('phortune-credit-card-form');
require_celerity_resource('aphront-tooltip-css');
Javelin::initBehavior('phabricator-tooltips');
$form = new AphrontFormView();
foreach ($this->scripts as $script) {
$form->appendChild(
phutil_tag(
'script',
array(
'type' => 'text/javascript',
'src' => $script,
)));
}
$errors = $this->errors;
$e_number = isset($errors[PhortuneErrCode::ERR_CC_INVALID_NUMBER])
? pht('Invalid')
: null;
$e_cvc = isset($errors[PhortuneErrCode::ERR_CC_INVALID_CVC])
? pht('Invalid')
: null;
$e_expiry = isset($errors[PhortuneErrCode::ERR_CC_INVALID_EXPIRY])
? pht('Invalid')
: null;
$form
->setID($form_id)
->appendChild(
id(new AphrontFormTextControl())
- ->setLabel('Card Number')
+ ->setLabel(pht('Card Number'))
->setDisableAutocomplete(true)
->setSigil('number-input')
->setError($e_number))
->appendChild(
id(new AphrontFormTextControl())
- ->setLabel('CVC')
+ ->setLabel(pht('CVC'))
->setDisableAutocomplete(true)
->addClass('aphront-form-cvc-input')
->setSigil('cvc-input')
->setError($e_cvc))
->appendChild(
id(new PhortuneMonthYearExpiryControl())
- ->setLabel('Expiration')
+ ->setLabel(pht('Expiration'))
->setUser($this->user)
->setError($e_expiry));
$assurance = $this->getSecurityAssurance();
if ($assurance) {
$assurance = phutil_tag(
'div',
array(
'class' => 'phortune-security-assurance',
),
array(
id(new PHUIIconView())
->setIconFont('fa-lock grey'),
' ',
$assurance,
));
$form->appendChild(
id(new AphrontFormMarkupControl())
->setValue($assurance));
}
return $form;
}
}
diff --git a/src/applications/phortune/worker/PhortuneSubscriptionWorker.php b/src/applications/phortune/worker/PhortuneSubscriptionWorker.php
index 747b4b388..96f194852 100644
--- a/src/applications/phortune/worker/PhortuneSubscriptionWorker.php
+++ b/src/applications/phortune/worker/PhortuneSubscriptionWorker.php
@@ -1,224 +1,223 @@
<?php
final class PhortuneSubscriptionWorker extends PhabricatorWorker {
protected function doWork() {
$subscription = $this->loadSubscription();
$range = $this->getBillingPeriodRange($subscription);
list($last_epoch, $next_epoch) = $range;
$should_invoice = $subscription->shouldInvoiceForBillingPeriod(
$last_epoch,
$next_epoch);
if (!$should_invoice) {
return;
}
$currency = $subscription->getCostForBillingPeriodAsCurrency(
$last_epoch,
$next_epoch);
if (!$currency->isPositive()) {
return;
}
$account = $subscription->getAccount();
$merchant = $subscription->getMerchant();
$viewer = PhabricatorUser::getOmnipotentUser();
$product = id(new PhortuneProductQuery())
->setViewer($viewer)
->withClassAndRef('PhortuneSubscriptionProduct', $subscription->getPHID())
->executeOne();
$cart_implementation = id(new PhortuneSubscriptionCart())
->setSubscription($subscription);
// TODO: This isn't really ideal. It would be better to use an application
// actor than the original author of the subscription. In particular, if
// someone initiates a subscription, adds some other account managers, and
// later leaves the company, they'll continue "acting" here indefinitely.
// However, for now, some of the stuff later in the pipeline requires a
// valid actor with a real PHID. The subscription should eventually be
// able to create these invoices "as" the application it is acting on
// behalf of.
$actor = id(new PhabricatorPeopleQuery())
->setViewer($viewer)
->withPHIDs(array($subscription->getAuthorPHID()))
->executeOne();
if (!$actor) {
throw new Exception(pht('Failed to load actor to bill subscription!'));
}
$cart = $account->newCart($actor, $cart_implementation, $merchant);
$purchase = $cart->newPurchase($actor, $product);
$purchase
->setBasePriceAsCurrency($currency)
->setMetadataValue('subscriptionPHID', $subscription->getPHID())
->setMetadataValue('epoch.start', $last_epoch)
->setMetadataValue('epoch.end', $next_epoch)
->save();
$cart
->setSubscriptionPHID($subscription->getPHID())
->setIsInvoice(1)
->save();
$cart->activateCart();
try {
$issues = $this->chargeSubscription($actor, $subscription, $cart);
} catch (Exception $ex) {
$issues = array(
pht(
'There was a technical error while trying to automatically bill '.
'this subscription: %s',
$ex),
);
}
if (!$issues) {
// We're all done; charging the cart sends a billing email as a side
// effect.
return;
}
// We're shoving this through the CartEditor because it has all the logic
// for sending mail about carts. This doesn't really affect the state of
// the cart, but reduces the amount of code duplication.
$xactions = array();
$xactions[] = id(new PhortuneCartTransaction())
->setTransactionType(PhortuneCartTransaction::TYPE_INVOICED)
->setNewValue(true);
$content_source = PhabricatorContentSource::newForSource(
PhabricatorContentSource::SOURCE_PHORTUNE,
array());
$acting_phid = id(new PhabricatorPhortuneApplication())->getPHID();
$editor = id(new PhortuneCartEditor())
->setActor($viewer)
->setActingAsPHID($acting_phid)
->setContentSource($content_source)
->setContinueOnMissingFields(true)
->setInvoiceIssues($issues)
->applyTransactions($cart, $xactions);
}
private function chargeSubscription(
PhabricatorUser $viewer,
PhortuneSubscription $subscription,
PhortuneCart $cart) {
$issues = array();
if (!$subscription->getDefaultPaymentMethodPHID()) {
$issues[] = pht(
'There is no payment method associated with this subscription, so '.
'it could not be billed automatically. Add a default payment method '.
'to enable automatic billing.');
return $issues;
}
$method = id(new PhortunePaymentMethodQuery())
->setViewer($viewer)
->withPHIDs(array($subscription->getDefaultPaymentMethodPHID()))
->executeOne();
if (!$method) {
$issues[] = pht(
'The payment method associated with this subscription is invalid '.
'or out of date, so it could not be automatically billed. Update '.
'the default payment method to enable automatic billing.');
return $issues;
}
$provider = $method->buildPaymentProvider();
$charge = $cart->willApplyCharge($viewer, $provider, $method);
try {
$provider->applyCharge($method, $charge);
} catch (Exception $ex) {
$cart->didFailCharge($charge);
$issues[] = pht(
'Automatic billing failed: %s',
$ex->getMessage());
return $issues;
}
$cart->didApplyCharge($charge);
}
/**
* Load the subscription to generate an invoice for.
*
* @return PhortuneSubscription The subscription to invoice.
*/
private function loadSubscription() {
$viewer = PhabricatorUser::getOmnipotentUser();
$data = $this->getTaskData();
$subscription_phid = idx($data, 'subscriptionPHID');
$subscription = id(new PhortuneSubscriptionQuery())
->setViewer($viewer)
->withPHIDs(array($subscription_phid))
->executeOne();
if (!$subscription) {
throw new PhabricatorWorkerPermanentFailureException(
pht(
'Failed to load subscription with PHID "%s".',
$subscription_phid));
}
return $subscription;
}
/**
* Get the start and end epoch timestamps for this billing period.
*
* @param PhortuneSubscription The subscription being billed.
* @return pair<int, int> Beginning and end of the billing range.
*/
private function getBillingPeriodRange(PhortuneSubscription $subscription) {
$data = $this->getTaskData();
$last_epoch = idx($data, 'trigger.last-epoch');
if (!$last_epoch) {
// If this is the first time the subscription is firing, use the
// creation date as the start of the billing period.
$last_epoch = $subscription->getDateCreated();
}
$this_epoch = idx($data, 'trigger.this-epoch');
if (!$last_epoch || !$this_epoch) {
throw new PhabricatorWorkerPermanentFailureException(
- pht(
- 'Subscription is missing billing period information.'));
+ pht('Subscription is missing billing period information.'));
}
$period_length = ($this_epoch - $last_epoch);
if ($period_length <= 0) {
throw new PhabricatorWorkerPermanentFailureException(
pht(
'Subscription has invalid billing period.'));
}
if (empty($data['manual'])) {
if (PhabricatorTime::getNow() < $this_epoch) {
throw new Exception(
pht(
'Refusing to generate a subscription invoice for a billing period '.
'which ends in the future.'));
}
}
return array($last_epoch, $this_epoch);
}
}
diff --git a/src/applications/phpast/application/PhabricatorPHPASTApplication.php b/src/applications/phpast/application/PhabricatorPHPASTApplication.php
index 8a9efa5a9..3118ed68c 100644
--- a/src/applications/phpast/application/PhabricatorPHPASTApplication.php
+++ b/src/applications/phpast/application/PhabricatorPHPASTApplication.php
@@ -1,47 +1,47 @@
<?php
final class PhabricatorPHPASTApplication extends PhabricatorApplication {
public function getName() {
return pht('PHPAST');
}
public function getBaseURI() {
return '/xhpast/';
}
public function getFontIcon() {
return 'fa-ambulance';
}
public function getShortDescription() {
- return 'Visual PHP Parser';
+ return pht('Visual PHP Parser');
}
public function getTitleGlyph() {
return "\xE2\x96\xA0";
}
public function getApplicationGroup() {
return self::GROUP_DEVELOPER;
}
public function getRoutes() {
return array(
'/xhpast/' => array(
'' => 'PhabricatorXHPASTViewRunController',
'view/(?P<id>[1-9]\d*)/'
=> 'PhabricatorXHPASTViewFrameController',
'frameset/(?P<id>[1-9]\d*)/'
=> 'PhabricatorXHPASTViewFramesetController',
'input/(?P<id>[1-9]\d*)/'
=> 'PhabricatorXHPASTViewInputController',
'tree/(?P<id>[1-9]\d*)/'
=> 'PhabricatorXHPASTViewTreeController',
'stream/(?P<id>[1-9]\d*)/'
=> 'PhabricatorXHPASTViewStreamController',
),
);
}
}
diff --git a/src/applications/phpast/controller/PhabricatorXHPASTViewFrameController.php b/src/applications/phpast/controller/PhabricatorXHPASTViewFrameController.php
index 65ec7731f..8d6e69020 100644
--- a/src/applications/phpast/controller/PhabricatorXHPASTViewFrameController.php
+++ b/src/applications/phpast/controller/PhabricatorXHPASTViewFrameController.php
@@ -1,28 +1,28 @@
<?php
final class PhabricatorXHPASTViewFrameController
extends PhabricatorXHPASTViewController {
private $id;
public function willProcessRequest(array $data) {
$this->id = $data['id'];
}
public function processRequest() {
$id = $this->id;
return $this->buildStandardPageResponse(
phutil_tag(
'iframe',
array(
'src' => '/xhpast/frameset/'.$id.'/',
'frameborder' => '0',
'style' => 'width: 100%; height: 800px;',
'',
)),
array(
- 'title' => 'XHPAST View',
+ 'title' => pht('XHPAST View'),
));
}
}
diff --git a/src/applications/phpast/controller/PhabricatorXHPASTViewPanelController.php b/src/applications/phpast/controller/PhabricatorXHPASTViewPanelController.php
index 53a93b138..ab68948af 100644
--- a/src/applications/phpast/controller/PhabricatorXHPASTViewPanelController.php
+++ b/src/applications/phpast/controller/PhabricatorXHPASTViewPanelController.php
@@ -1,70 +1,70 @@
<?php
abstract class PhabricatorXHPASTViewPanelController
extends PhabricatorXHPASTViewController {
private $id;
private $storageTree;
public function willProcessRequest(array $data) {
$this->id = $data['id'];
$this->storageTree = id(new PhabricatorXHPASTViewParseTree())
->load($this->id);
if (!$this->storageTree) {
- throw new Exception('No such AST!');
+ throw new Exception(pht('No such AST!'));
}
}
protected function getStorageTree() {
return $this->storageTree;
}
protected function buildXHPASTViewPanelResponse($content) {
$content = hsprintf(
'<!DOCTYPE html>'.
'<html>'.
'<head>'.
'<style type="text/css">
body {
white-space: pre;
font: 10px "Monaco";
cursor: pointer;
}
.token {
padding: 2px 4px;
margin: 2px 2px;
border: 1px solid #bbbbbb;
line-height: 24px;
}
ul {
margin: 0 0 0 1em;
padding: 0;
list-style: none;
line-height: 1em;
}
li {
margin: 0;
padding: 0;
}
li span {
background: #dddddd;
padding: 3px 6px;
}
</style>'.
'</head>'.
'<body>%s</body>'.
'</html>',
$content);
$response = new AphrontWebpageResponse();
$response->setFrameable(true);
$response->setContent($content);
return $response;
}
}
diff --git a/src/applications/phpast/controller/PhabricatorXHPASTViewRunController.php b/src/applications/phpast/controller/PhabricatorXHPASTViewRunController.php
index 3e6e2b3fb..b235f5b84 100644
--- a/src/applications/phpast/controller/PhabricatorXHPASTViewRunController.php
+++ b/src/applications/phpast/controller/PhabricatorXHPASTViewRunController.php
@@ -1,57 +1,57 @@
<?php
final class PhabricatorXHPASTViewRunController
extends PhabricatorXHPASTViewController {
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
if ($request->isFormPost()) {
$source = $request->getStr('source');
$future = PhutilXHPASTBinary::getParserFuture($source);
$resolved = $future->resolve();
// This is just to let it throw exceptions if stuff is broken.
$parse_tree = XHPASTTree::newFromDataAndResolvedExecFuture(
$source,
$resolved);
list($err, $stdout, $stderr) = $resolved;
$storage_tree = new PhabricatorXHPASTViewParseTree();
$storage_tree->setInput($source);
$storage_tree->setStdout($stdout);
$storage_tree->setAuthorPHID($user->getPHID());
$storage_tree->save();
return id(new AphrontRedirectResponse())
->setURI('/xhpast/view/'.$storage_tree->getID().'/');
}
$form = id(new AphrontFormView())
->setUser($user)
->appendChild(
id(new AphrontFormTextAreaControl())
- ->setLabel('Source')
+ ->setLabel(pht('Source'))
->setName('source')
->setValue("<?php\n\n")
->setHeight(AphrontFormTextAreaControl::HEIGHT_VERY_TALL))
->appendChild(
id(new AphrontFormSubmitControl())
->setValue('Parse'));
$form_box = id(new PHUIObjectBoxView())
->setHeaderText(pht('Generate XHP AST'))
->setForm($form);
return $this->buildApplicationPage(
$form_box,
array(
'title' => pht('XHPAST View'),
));
}
}
diff --git a/src/applications/phpast/controller/PhabricatorXHPASTViewStreamController.php b/src/applications/phpast/controller/PhabricatorXHPASTViewStreamController.php
index 4931ab29e..4a098f159 100644
--- a/src/applications/phpast/controller/PhabricatorXHPASTViewStreamController.php
+++ b/src/applications/phpast/controller/PhabricatorXHPASTViewStreamController.php
@@ -1,33 +1,33 @@
<?php
final class PhabricatorXHPASTViewStreamController
extends PhabricatorXHPASTViewPanelController {
public function processRequest() {
$storage = $this->getStorageTree();
$input = $storage->getInput();
$stdout = $storage->getStdout();
$tree = XHPASTTree::newFromDataAndResolvedExecFuture(
$input,
array(0, $stdout, ''));
$tokens = array();
foreach ($tree->getRawTokenStream() as $id => $token) {
$seq = $id;
$name = $token->getTypeName();
- $title = "Token {$seq}: {$name}";
+ $title = pht('Token %s: %s', $seq, $name);
$tokens[] = phutil_tag(
'span',
array(
'title' => $title,
'class' => 'token',
),
$token->getValue());
}
return $this->buildXHPASTViewPanelResponse(
phutil_implode_html('', $tokens));
}
}
diff --git a/src/applications/phragment/conduit/PhragmentGetPatchConduitAPIMethod.php b/src/applications/phragment/conduit/PhragmentGetPatchConduitAPIMethod.php
index dedf00e1e..aa9e00800 100644
--- a/src/applications/phragment/conduit/PhragmentGetPatchConduitAPIMethod.php
+++ b/src/applications/phragment/conduit/PhragmentGetPatchConduitAPIMethod.php
@@ -1,189 +1,189 @@
<?php
final class PhragmentGetPatchConduitAPIMethod
extends PhragmentConduitAPIMethod {
public function getAPIMethodName() {
return 'phragment.getpatch';
}
public function getMethodStatus() {
return self::METHOD_STATUS_UNSTABLE;
}
public function getMethodDescription() {
return pht('Retrieve the patches to apply for a given set of files.');
}
protected function defineParamTypes() {
return array(
'path' => 'required string',
'state' => 'required dict<string, string>',
);
}
protected function defineReturnType() {
return 'nonempty dict';
}
protected function defineErrorTypes() {
return array(
- 'ERR_BAD_FRAGMENT' => 'No such fragment exists',
+ 'ERR_BAD_FRAGMENT' => pht('No such fragment exists.'),
);
}
protected function execute(ConduitAPIRequest $request) {
$path = $request->getValue('path');
$state = $request->getValue('state');
// The state is an array mapping file paths to hashes.
$patches = array();
// We need to get all of the mappings (like phragment.getstate) first
// so that we can detect deletions and creations of files.
$fragment = id(new PhragmentFragmentQuery())
->setViewer($request->getUser())
->withPaths(array($path))
->executeOne();
if ($fragment === null) {
throw new ConduitException('ERR_BAD_FRAGMENT');
}
$mappings = $fragment->getFragmentMappings(
$request->getUser(),
$fragment->getPath());
$file_phids = mpull(mpull($mappings, 'getLatestVersion'), 'getFilePHID');
$files = id(new PhabricatorFileQuery())
->setViewer($request->getUser())
->withPHIDs($file_phids)
->execute();
$files = mpull($files, null, 'getPHID');
// Scan all of the files that the caller currently has and iterate
// over that.
foreach ($state as $path => $hash) {
// If $mappings[$path] exists, then the user has the file and it's
// also a fragment.
if (array_key_exists($path, $mappings)) {
$file_phid = $mappings[$path]->getLatestVersion()->getFilePHID();
if ($file_phid !== null) {
// If the file PHID is present, then we need to check the
// hashes to see if they are the same.
$hash_caller = strtolower($state[$path]);
$hash_current = $files[$file_phid]->getContentHash();
if ($hash_caller === $hash_current) {
// The user's version is identical to our version, so
// there is no update needed.
} else {
// The hash differs, and the user needs to update.
$patches[] = array(
'path' => $path,
'fileOld' => null,
'fileNew' => $files[$file_phid],
'hashOld' => $hash_caller,
'hashNew' => $hash_current,
'patchURI' => null,
);
}
} else {
// We have a record of this as a file, but there is no file
// attached to the latest version, so we consider this to be
// a deletion.
$patches[] = array(
'path' => $path,
'fileOld' => null,
'fileNew' => null,
'hashOld' => $hash_caller,
'hashNew' => PhragmentPatchUtil::EMPTY_HASH,
'patchURI' => null,
);
}
} else {
// If $mappings[$path] does not exist, then the user has a file,
// and we have absolutely no record of it what-so-ever (we haven't
// even recorded a deletion). Assuming most applications will store
// some form of data near their own files, this is probably a data
// file relevant for the application that is not versioned, so we
// don't tell the client to do anything with it.
}
}
// Check the remaining files that we know about but the caller has
// not reported.
foreach ($mappings as $path => $child) {
if (array_key_exists($path, $state)) {
// We have already evaluated this above.
} else {
$file_phid = $mappings[$path]->getLatestVersion()->getFilePHID();
if ($file_phid !== null) {
// If the file PHID is present, then this is a new file that
// we know about, but the caller does not. We need to tell
// the caller to create the file.
$hash_current = $files[$file_phid]->getContentHash();
$patches[] = array(
'path' => $path,
'fileOld' => null,
'fileNew' => $files[$file_phid],
'hashOld' => PhragmentPatchUtil::EMPTY_HASH,
'hashNew' => $hash_current,
'patchURI' => null,
);
} else {
// We have a record of deleting this file, and the caller hasn't
// reported it, so they've probably deleted it in a previous
// update.
}
}
}
// Before we can calculate patches, we need to resolve the old versions
// of files so we can draw diffs on them.
$hashes = array();
foreach ($patches as $patch) {
if ($patch['hashOld'] !== PhragmentPatchUtil::EMPTY_HASH) {
$hashes[] = $patch['hashOld'];
}
}
$old_files = array();
if (count($hashes) !== 0) {
$old_files = id(new PhabricatorFileQuery())
->setViewer($request->getUser())
->withContentHashes($hashes)
->execute();
}
$old_files = mpull($old_files, null, 'getContentHash');
foreach ($patches as $key => $patch) {
if ($patch['hashOld'] !== PhragmentPatchUtil::EMPTY_HASH) {
if (array_key_exists($patch['hashOld'], $old_files)) {
$patches[$key]['fileOld'] = $old_files[$patch['hashOld']];
} else {
// We either can't see or can't read the old file.
$patches[$key]['hashOld'] = PhragmentPatchUtil::EMPTY_HASH;
$patches[$key]['fileOld'] = null;
}
}
}
// Now run through all of the patch entries, calculate the patches
// and return the results.
foreach ($patches as $key => $patch) {
$data = PhragmentPatchUtil::calculatePatch(
$patches[$key]['fileOld'],
$patches[$key]['fileNew']);
unset($patches[$key]['fileOld']);
unset($patches[$key]['fileNew']);
$file = PhabricatorFile::buildFromFileDataOrHash(
$data,
array(
'name' => 'patch.dmp',
'ttl' => time() + 60 * 60 * 24,
));
$patches[$key]['patchURI'] = $file->getDownloadURI();
}
return $patches;
}
}
diff --git a/src/applications/phragment/conduit/PhragmentQueryFragmentsConduitAPIMethod.php b/src/applications/phragment/conduit/PhragmentQueryFragmentsConduitAPIMethod.php
index 27ddac1d3..08a1bcba4 100644
--- a/src/applications/phragment/conduit/PhragmentQueryFragmentsConduitAPIMethod.php
+++ b/src/applications/phragment/conduit/PhragmentQueryFragmentsConduitAPIMethod.php
@@ -1,85 +1,85 @@
<?php
final class PhragmentQueryFragmentsConduitAPIMethod
extends PhragmentConduitAPIMethod {
public function getAPIMethodName() {
return 'phragment.queryfragments';
}
public function getMethodStatus() {
return self::METHOD_STATUS_UNSTABLE;
}
public function getMethodDescription() {
return pht('Query fragments based on their paths.');
}
protected function defineParamTypes() {
return array(
'paths' => 'required list<string>',
);
}
protected function defineReturnType() {
return 'nonempty dict';
}
protected function defineErrorTypes() {
return array(
- 'ERR_BAD_FRAGMENT' => 'No such fragment exists',
+ 'ERR_BAD_FRAGMENT' => pht('No such fragment exists.'),
);
}
protected function execute(ConduitAPIRequest $request) {
$paths = $request->getValue('paths');
$fragments = id(new PhragmentFragmentQuery())
->setViewer($request->getUser())
->withPaths($paths)
->execute();
$fragments = mpull($fragments, null, 'getPath');
foreach ($paths as $path) {
if (!array_key_exists($path, $fragments)) {
throw new ConduitException('ERR_BAD_FRAGMENT');
}
}
$results = array();
foreach ($fragments as $path => $fragment) {
$mappings = $fragment->getFragmentMappings(
$request->getUser(),
$fragment->getPath());
$file_phids = mpull(mpull($mappings, 'getLatestVersion'), 'getFilePHID');
$files = id(new PhabricatorFileQuery())
->setViewer($request->getUser())
->withPHIDs($file_phids)
->execute();
$files = mpull($files, null, 'getPHID');
$result = array();
foreach ($mappings as $cpath => $child) {
$file_phid = $child->getLatestVersion()->getFilePHID();
if (!isset($files[$file_phid])) {
// Skip any files we don't have permission to access.
continue;
}
$file = $files[$file_phid];
$cpath = substr($child->getPath(), strlen($fragment->getPath()) + 1);
$result[] = array(
'phid' => $child->getPHID(),
'phidVersion' => $child->getLatestVersionPHID(),
'path' => $cpath,
'hash' => $file->getContentHash(),
'version' => $child->getLatestVersion()->getSequence(),
'uri' => $file->getViewURI(),
);
}
$results[$path] = $result;
}
return $results;
}
}
diff --git a/src/applications/phragment/controller/PhragmentController.php b/src/applications/phragment/controller/PhragmentController.php
index 096ee21a1..a96c25878 100644
--- a/src/applications/phragment/controller/PhragmentController.php
+++ b/src/applications/phragment/controller/PhragmentController.php
@@ -1,227 +1,231 @@
<?php
abstract class PhragmentController extends PhabricatorController {
protected function loadParentFragments($path) {
$components = explode('/', $path);
$combinations = array();
$current = '';
foreach ($components as $component) {
$current .= '/'.$component;
$current = trim($current, '/');
if (trim($current) === '') {
continue;
}
$combinations[] = $current;
}
$fragments = array();
$results = id(new PhragmentFragmentQuery())
->setViewer($this->getRequest()->getUser())
->needLatestVersion(true)
->withPaths($combinations)
->execute();
foreach ($combinations as $combination) {
$found = false;
foreach ($results as $fragment) {
if ($fragment->getPath() === $combination) {
$fragments[] = $fragment;
$found = true;
break;
}
}
if (!$found) {
return null;
}
}
return $fragments;
}
protected function buildApplicationCrumbsWithPath(array $fragments) {
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb('/', '/phragment/');
foreach ($fragments as $parent) {
$crumbs->addTextCrumb(
$parent->getName(),
'/phragment/browse/'.$parent->getPath());
}
return $crumbs;
}
protected function createCurrentFragmentView($fragment, $is_history_view) {
if ($fragment === null) {
return null;
}
$viewer = $this->getRequest()->getUser();
$snapshot_phids = array();
$snapshots = id(new PhragmentSnapshotQuery())
->setViewer($viewer)
->withPrimaryFragmentPHIDs(array($fragment->getPHID()))
->execute();
foreach ($snapshots as $snapshot) {
$snapshot_phids[] = $snapshot->getPHID();
}
$file = null;
$file_uri = null;
if (!$fragment->isDirectory()) {
$file = id(new PhabricatorFileQuery())
->setViewer($viewer)
->withPHIDs(array($fragment->getLatestVersion()->getFilePHID()))
->executeOne();
if ($file !== null) {
$file_uri = $file->getDownloadURI();
}
}
$header = id(new PHUIHeaderView())
->setHeader($fragment->getName())
->setPolicyObject($fragment)
->setUser($viewer);
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$fragment,
PhabricatorPolicyCapability::CAN_EDIT);
$zip_uri = $this->getApplicationURI('zip/'.$fragment->getPath());
$actions = id(new PhabricatorActionListView())
->setUser($viewer)
->setObject($fragment)
->setObjectURI($fragment->getURI());
$actions->addAction(
id(new PhabricatorActionView())
->setName(pht('Download Fragment'))
->setHref($this->isCorrectlyConfigured() ? $file_uri : null)
->setDisabled($file === null || !$this->isCorrectlyConfigured())
->setIcon('fa-download'));
$actions->addAction(
id(new PhabricatorActionView())
->setName(pht('Download Contents as ZIP'))
->setHref($this->isCorrectlyConfigured() ? $zip_uri : null)
->setDisabled(!$this->isCorrectlyConfigured())
->setIcon('fa-floppy-o'));
if (!$fragment->isDirectory()) {
$actions->addAction(
id(new PhabricatorActionView())
->setName(pht('Update Fragment'))
->setHref($this->getApplicationURI('update/'.$fragment->getPath()))
->setDisabled(!$can_edit)
->setWorkflow(!$can_edit)
->setIcon('fa-refresh'));
} else {
$actions->addAction(
id(new PhabricatorActionView())
->setName(pht('Convert to File'))
->setHref($this->getApplicationURI('update/'.$fragment->getPath()))
->setDisabled(!$can_edit)
->setWorkflow(!$can_edit)
->setIcon('fa-file-o'));
}
$actions->addAction(
id(new PhabricatorActionView())
->setName(pht('Set Fragment Policies'))
->setHref($this->getApplicationURI('policy/'.$fragment->getPath()))
->setDisabled(!$can_edit)
->setWorkflow(!$can_edit)
->setIcon('fa-asterisk'));
if ($is_history_view) {
$actions->addAction(
id(new PhabricatorActionView())
->setName(pht('View Child Fragments'))
->setHref($this->getApplicationURI('browse/'.$fragment->getPath()))
->setIcon('fa-search-plus'));
} else {
$actions->addAction(
id(new PhabricatorActionView())
->setName(pht('View History'))
->setHref($this->getApplicationURI('history/'.$fragment->getPath()))
->setIcon('fa-list'));
}
$actions->addAction(
id(new PhabricatorActionView())
->setName(pht('Create Snapshot'))
->setHref($this->getApplicationURI(
'snapshot/create/'.$fragment->getPath()))
->setDisabled(!$can_edit)
->setWorkflow(!$can_edit)
->setIcon('fa-files-o'));
$actions->addAction(
id(new PhabricatorActionView())
->setName(pht('Promote Snapshot to Here'))
->setHref($this->getApplicationURI(
'snapshot/promote/latest/'.$fragment->getPath()))
->setWorkflow(true)
->setDisabled(!$can_edit)
->setIcon('fa-arrow-circle-up'));
$properties = id(new PHUIPropertyListView())
->setUser($viewer)
->setObject($fragment)
->setActionList($actions);
if (!$fragment->isDirectory()) {
if ($fragment->isDeleted()) {
$properties->addProperty(
pht('Type'),
pht('File (Deleted)'));
} else {
$properties->addProperty(
pht('Type'),
pht('File'));
}
$properties->addProperty(
pht('Latest Version'),
$viewer->renderHandle($fragment->getLatestVersionPHID()));
} else {
$properties->addProperty(
pht('Type'),
pht('Directory'));
}
if (count($snapshot_phids) > 0) {
$properties->addProperty(
pht('Snapshots'),
$viewer->renderHandleList($snapshot_phids));
}
return id(new PHUIObjectBoxView())
->setHeader($header)
->addPropertyList($properties);
}
public function renderConfigurationWarningIfRequired() {
$alt = PhabricatorEnv::getEnvConfig('security.alternate-file-domain');
if ($alt === null) {
return id(new PHUIInfoView())
->setTitle(pht('security.alternate-file-domain must be configured!'))
->setSeverity(PHUIInfoView::SEVERITY_ERROR)
- ->appendChild(phutil_tag('p', array(), pht(
- 'Because Phragment generates files (such as ZIP archives and '.
- 'patches) as they are requested, it requires that you configure '.
- 'the `security.alternate-file-domain` option. This option on it\'s '.
- 'own will also provide additional security when serving files '.
- 'across Phabricator.')));
+ ->appendChild(
+ phutil_tag(
+ 'p',
+ array(),
+ pht(
+ "Because Phragment generates files (such as ZIP archives and ".
+ "patches) as they are requested, it requires that you configure ".
+ "the `%s` option. This option on it's own will also provide ".
+ "additional security when serving files across Phabricator.",
+ 'security.alternate-file-domain')));
}
return null;
}
/**
* We use this to disable the download links if the alternate domain is
* not configured correctly. Although the download links will mostly work
* for logged in users without an alternate domain, the behaviour is
* reasonably non-consistent and will deny public users, even if policies
* are configured otherwise (because the Files app does not support showing
* the info page to viewers who are not logged in).
*/
public function isCorrectlyConfigured() {
$alt = PhabricatorEnv::getEnvConfig('security.alternate-file-domain');
return $alt !== null;
}
}
diff --git a/src/applications/phragment/controller/PhragmentCreateController.php b/src/applications/phragment/controller/PhragmentCreateController.php
index d899754a1..2a5b54538 100644
--- a/src/applications/phragment/controller/PhragmentCreateController.php
+++ b/src/applications/phragment/controller/PhragmentCreateController.php
@@ -1,138 +1,138 @@
<?php
final class PhragmentCreateController extends PhragmentController {
private $dblob;
public function willProcessRequest(array $data) {
$this->dblob = idx($data, 'dblob', '');
}
public function processRequest() {
$request = $this->getRequest();
$viewer = $request->getUser();
$parent = null;
$parents = $this->loadParentFragments($this->dblob);
if ($parents === null) {
return new Aphront404Response();
}
if (count($parents) !== 0) {
$parent = idx($parents, count($parents) - 1, null);
}
$parent_path = '';
if ($parent !== null) {
$parent_path = $parent->getPath();
}
$parent_path = trim($parent_path, '/');
$fragment = id(new PhragmentFragment());
$error_view = null;
if ($request->isFormPost()) {
$errors = array();
$v_name = $request->getStr('name');
$v_fileid = $request->getInt('fileID');
$v_viewpolicy = $request->getStr('viewPolicy');
$v_editpolicy = $request->getStr('editPolicy');
if (strpos($v_name, '/') !== false) {
- $errors[] = pht('The fragment name can not contain \'/\'.');
+ $errors[] = pht("The fragment name can not contain '/'.");
}
$file = id(new PhabricatorFileQuery())
->setViewer($viewer)
->withIDs(array($v_fileid))
->executeOne();
if (!$file) {
- $errors[] = pht('The specified file doesn\'t exist.');
+ $errors[] = pht("The specified file doesn't exist.");
}
if (!count($errors)) {
$depth = 1;
if ($parent !== null) {
$depth = $parent->getDepth() + 1;
}
PhragmentFragment::createFromFile(
$viewer,
$file,
trim($parent_path.'/'.$v_name, '/'),
$v_viewpolicy,
$v_editpolicy);
return id(new AphrontRedirectResponse())
->setURI('/phragment/browse/'.trim($parent_path.'/'.$v_name, '/'));
} else {
$error_view = id(new PHUIInfoView())
->setErrors($errors)
->setTitle(pht('Errors while creating fragment'));
}
}
$policies = id(new PhabricatorPolicyQuery())
->setViewer($viewer)
->setObject($fragment)
->execute();
$form = id(new AphrontFormView())
->setUser($viewer)
->appendChild(
id(new AphrontFormTextControl())
->setLabel(pht('Parent Path'))
->setDisabled(true)
->setValue('/'.trim($parent_path.'/', '/')))
->appendChild(
id(new AphrontFormTextControl())
->setLabel(pht('Name'))
->setName('name'))
->appendChild(
id(new AphrontFormTextControl())
->setLabel(pht('File ID'))
->setName('fileID'))
->appendChild(
id(new AphrontFormPolicyControl())
->setUser($viewer)
->setName('viewPolicy')
->setPolicyObject($fragment)
->setPolicies($policies)
->setCapability(PhabricatorPolicyCapability::CAN_VIEW))
->appendChild(
id(new AphrontFormPolicyControl())
->setUser($viewer)
->setName('editPolicy')
->setPolicyObject($fragment)
->setPolicies($policies)
->setCapability(PhabricatorPolicyCapability::CAN_EDIT))
->appendChild(
id(new AphrontFormSubmitControl())
->setValue(pht('Create Fragment'))
->addCancelButton(
$this->getApplicationURI('browse/'.$parent_path)));
$crumbs = $this->buildApplicationCrumbsWithPath($parents);
$crumbs->addTextCrumb(pht('Create Fragment'));
$box = id(new PHUIObjectBoxView())
- ->setHeaderText('Create Fragment')
+ ->setHeaderText(pht('Create Fragment'))
->setForm($form);
if ($error_view) {
$box->setInfoView($error_view);
}
return $this->buildApplicationPage(
array(
$crumbs,
$this->renderConfigurationWarningIfRequired(),
$box,
),
array(
'title' => pht('Create Fragment'),
));
}
}
diff --git a/src/applications/phragment/controller/PhragmentHistoryController.php b/src/applications/phragment/controller/PhragmentHistoryController.php
index 3df7095c9..cb84fd819 100644
--- a/src/applications/phragment/controller/PhragmentHistoryController.php
+++ b/src/applications/phragment/controller/PhragmentHistoryController.php
@@ -1,112 +1,112 @@
<?php
final class PhragmentHistoryController extends PhragmentController {
private $dblob;
public function shouldAllowPublic() {
return true;
}
public function willProcessRequest(array $data) {
$this->dblob = idx($data, 'dblob', '');
}
public function processRequest() {
$request = $this->getRequest();
$viewer = $request->getUser();
$parents = $this->loadParentFragments($this->dblob);
if ($parents === null) {
return new Aphront404Response();
}
$current = idx($parents, count($parents) - 1, null);
$path = $current->getPath();
$crumbs = $this->buildApplicationCrumbsWithPath($parents);
if ($this->hasApplicationCapability(
PhragmentCanCreateCapability::CAPABILITY)) {
$crumbs->addAction(
id(new PHUIListItemView())
->setName(pht('Create Fragment'))
->setHref($this->getApplicationURI('/create/'.$path))
->setIcon('fa-plus-square'));
}
$current_box = $this->createCurrentFragmentView($current, true);
$versions = id(new PhragmentFragmentVersionQuery())
->setViewer($viewer)
->withFragmentPHIDs(array($current->getPHID()))
->execute();
$list = id(new PHUIObjectItemListView())
->setUser($viewer);
$file_phids = mpull($versions, 'getFilePHID');
$files = id(new PhabricatorFileQuery())
->setViewer($viewer)
->withPHIDs($file_phids)
->execute();
$files = mpull($files, null, 'getPHID');
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$current,
PhabricatorPolicyCapability::CAN_EDIT);
$first = true;
foreach ($versions as $version) {
$item = id(new PHUIObjectItemView());
- $item->setHeader('Version '.$version->getSequence());
+ $item->setHeader(pht('Version %s', $version->getSequence()));
$item->setHref($version->getURI());
$item->addAttribute(phabricator_datetime(
$version->getDateCreated(),
$viewer));
if ($version->getFilePHID() === null) {
$item->setDisabled(true);
$item->addAttribute('Deletion');
}
if (!$first && $can_edit) {
$item->addAction(id(new PHUIListItemView())
->setIcon('fa-refresh')
->setRenderNameAsTooltip(true)
->setWorkflow(true)
->setName(pht('Revert to Here'))
->setHref($this->getApplicationURI(
'revert/'.$version->getID().'/'.$current->getPath())));
}
$disabled = !isset($files[$version->getFilePHID()]);
$action = id(new PHUIListItemView())
->setIcon('fa-download')
->setDisabled($disabled || !$this->isCorrectlyConfigured())
->setRenderNameAsTooltip(true)
->setName(pht('Download'));
if (!$disabled && $this->isCorrectlyConfigured()) {
$action->setHref($files[$version->getFilePHID()]
->getDownloadURI($version->getURI()));
}
$item->addAction($action);
$list->addItem($item);
$first = false;
}
return $this->buildApplicationPage(
array(
$crumbs,
$this->renderConfigurationWarningIfRequired(),
$current_box,
$list,
),
array(
'title' => pht('Fragment History'),
));
}
}
diff --git a/src/applications/phragment/controller/PhragmentRevertController.php b/src/applications/phragment/controller/PhragmentRevertController.php
index 84e8cbb71..92da1f27a 100644
--- a/src/applications/phragment/controller/PhragmentRevertController.php
+++ b/src/applications/phragment/controller/PhragmentRevertController.php
@@ -1,87 +1,87 @@
<?php
final class PhragmentRevertController extends PhragmentController {
private $dblob;
private $id;
public function willProcessRequest(array $data) {
$this->dblob = $data['dblob'];
$this->id = $data['id'];
}
public function processRequest() {
$request = $this->getRequest();
$viewer = $request->getUser();
$fragment = id(new PhragmentFragmentQuery())
->setViewer($viewer)
->withPaths(array($this->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($this->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(
- 'The file associated with this version was not found.');
+ 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/'.$this->dblob));
}
return $this->createDialog($fragment, $version);
}
public function createDialog(
PhragmentFragment $fragment,
PhragmentFragmentVersion $version) {
$request = $this->getRequest();
$viewer = $request->getUser();
$dialog = id(new AphrontDialogView())
->setTitle(pht('Really revert this fragment?'))
->setUser($request->getUser())
->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/phragment/controller/PhragmentVersionController.php b/src/applications/phragment/controller/PhragmentVersionController.php
index 9267fdc5b..e2906a745 100644
--- a/src/applications/phragment/controller/PhragmentVersionController.php
+++ b/src/applications/phragment/controller/PhragmentVersionController.php
@@ -1,132 +1,132 @@
<?php
final class PhragmentVersionController extends PhragmentController {
private $id;
public function shouldAllowPublic() {
return true;
}
public function willProcessRequest(array $data) {
$this->id = idx($data, 'id', 0);
}
public function processRequest() {
$request = $this->getRequest();
$viewer = $request->getUser();
$version = id(new PhragmentFragmentVersionQuery())
->setViewer($viewer)
->withIDs(array($this->id))
->executeOne();
if ($version === null) {
return new Aphront404Response();
}
$parents = $this->loadParentFragments($version->getFragment()->getPath());
if ($parents === null) {
return new Aphront404Response();
}
$current = idx($parents, count($parents) - 1, null);
$crumbs = $this->buildApplicationCrumbsWithPath($parents);
$crumbs->addTextCrumb(pht('View Version %d', $version->getSequence()));
$file = id(new PhabricatorFileQuery())
->setViewer($viewer)
->withPHIDs(array($version->getFilePHID()))
->executeOne();
if ($file !== null) {
$file_uri = $file->getDownloadURI();
}
$header = id(new PHUIHeaderView())
->setHeader(pht(
'%s at version %d',
$version->getFragment()->getName(),
$version->getSequence()))
->setPolicyObject($version)
->setUser($viewer);
$actions = id(new PhabricatorActionListView())
->setUser($viewer)
->setObject($version)
->setObjectURI($version->getURI());
$actions->addAction(
id(new PhabricatorActionView())
->setName(pht('Download Version'))
->setDisabled($file === null || !$this->isCorrectlyConfigured())
->setHref($this->isCorrectlyConfigured() ? $file_uri : null)
->setIcon('fa-download'));
$properties = id(new PHUIPropertyListView())
->setUser($viewer)
->setObject($version)
->setActionList($actions);
$properties->addProperty(
pht('File'),
$viewer->renderHandle($version->getFilePHID()));
$box = id(new PHUIObjectBoxView())
->setHeader($header)
->addPropertyList($properties);
return $this->buildApplicationPage(
array(
$crumbs,
$this->renderConfigurationWarningIfRequired(),
$box,
$this->renderPreviousVersionList($version),
),
array(
'title' => pht('View Version'),
));
}
private function renderPreviousVersionList(
PhragmentFragmentVersion $version) {
$request = $this->getRequest();
$viewer = $request->getUser();
$previous_versions = id(new PhragmentFragmentVersionQuery())
->setViewer($viewer)
->withFragmentPHIDs(array($version->getFragmentPHID()))
->withSequenceBefore($version->getSequence())
->execute();
$list = id(new PHUIObjectItemListView())
->setUser($viewer);
foreach ($previous_versions as $previous_version) {
$item = id(new PHUIObjectItemView());
- $item->setHeader('Version '.$previous_version->getSequence());
+ $item->setHeader(pht('Version %s', $previous_version->getSequence()));
$item->setHref($previous_version->getURI());
$item->addAttribute(phabricator_datetime(
$previous_version->getDateCreated(),
$viewer));
$patch_uri = $this->getApplicationURI(
'patch/'.$previous_version->getID().'/'.$version->getID());
$item->addAction(id(new PHUIListItemView())
->setIcon('fa-file-o')
->setName(pht('Get Patch'))
->setHref($this->isCorrectlyConfigured() ? $patch_uri : null)
->setDisabled(!$this->isCorrectlyConfigured()));
$list->addItem($item);
}
$item = id(new PHUIObjectItemView());
- $item->setHeader('Prior to Version 0');
- $item->addAttribute('Prior to any content (empty file)');
+ $item->setHeader(pht('Prior to Version 0'));
+ $item->addAttribute(pht('Prior to any content (empty file)'));
$item->addAction(id(new PHUIListItemView())
->setIcon('fa-file-o')
->setName(pht('Get Patch'))
->setHref($this->getApplicationURI(
'patch/x/'.$version->getID())));
$list->addItem($item);
return $list;
}
}
diff --git a/src/applications/phragment/storage/PhragmentFragment.php b/src/applications/phragment/storage/PhragmentFragment.php
index d58be033c..1ab156e60 100644
--- a/src/applications/phragment/storage/PhragmentFragment.php
+++ b/src/applications/phragment/storage/PhragmentFragment.php
@@ -1,351 +1,352 @@
<?php
final class PhragmentFragment extends PhragmentDAO
implements PhabricatorPolicyInterface {
protected $path;
protected $depth;
protected $latestVersionPHID;
protected $viewPolicy;
protected $editPolicy;
private $latestVersion = self::ATTACHABLE;
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_COLUMN_SCHEMA => array(
'path' => 'text128',
'depth' => 'uint32',
'latestVersionPHID' => 'phid?',
),
self::CONFIG_KEY_SCHEMA => array(
'key_path' => array(
'columns' => array('path'),
'unique' => true,
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhragmentFragmentPHIDType::TYPECONST);
}
public function getURI() {
return '/phragment/browse/'.$this->getPath();
}
public function getName() {
return basename($this->path);
}
public function getFile() {
return $this->assertAttached($this->file);
}
public function attachFile(PhabricatorFile $file) {
return $this->file = $file;
}
public function isDirectory() {
return $this->latestVersionPHID === null;
}
public function isDeleted() {
return $this->getLatestVersion()->getFilePHID() === null;
}
public function getLatestVersion() {
if ($this->latestVersionPHID === null) {
return null;
}
return $this->assertAttached($this->latestVersion);
}
public function attachLatestVersion(PhragmentFragmentVersion $version) {
return $this->latestVersion = $version;
}
/* -( Updating ) --------------------------------------------------------- */
/**
* Create a new fragment from a file.
*/
public static function createFromFile(
PhabricatorUser $viewer,
PhabricatorFile $file = null,
$path = null,
$view_policy = null,
$edit_policy = null) {
$fragment = id(new PhragmentFragment());
$fragment->setPath($path);
$fragment->setDepth(count(explode('/', $path)));
$fragment->setLatestVersionPHID(null);
$fragment->setViewPolicy($view_policy);
$fragment->setEditPolicy($edit_policy);
$fragment->save();
// Directory fragments have no versions associated with them, so we
// just return the fragment at this point.
if ($file === null) {
return $fragment;
}
if ($file->getMimeType() === 'application/zip') {
$fragment->updateFromZIP($viewer, $file);
} else {
$fragment->updateFromFile($viewer, $file);
}
return $fragment;
}
/**
* Set the specified file as the next version for the fragment.
*/
public function updateFromFile(
PhabricatorUser $viewer,
PhabricatorFile $file) {
$existing = id(new PhragmentFragmentVersionQuery())
->setViewer($viewer)
->withFragmentPHIDs(array($this->getPHID()))
->execute();
$sequence = count($existing);
$this->openTransaction();
$version = id(new PhragmentFragmentVersion());
$version->setSequence($sequence);
$version->setFragmentPHID($this->getPHID());
$version->setFilePHID($file->getPHID());
$version->save();
$this->setLatestVersionPHID($version->getPHID());
$this->save();
$this->saveTransaction();
$file->attachToObject($version->getPHID());
}
/**
* Apply the specified ZIP archive onto the fragment, removing
* and creating fragments as needed.
*/
public function updateFromZIP(
PhabricatorUser $viewer,
PhabricatorFile $file) {
if ($file->getMimeType() !== 'application/zip') {
- throw new Exception("File must have mimetype 'application/zip'");
+ throw new Exception(
+ pht("File must have mimetype '%s'.", 'application/zip'));
}
// First apply the ZIP as normal.
$this->updateFromFile($viewer, $file);
// Ensure we have ZIP support.
$zip = null;
try {
$zip = new ZipArchive();
} catch (Exception $e) {
// The server doesn't have php5-zip, so we can't do recursive updates.
return;
}
$temp = new TempFile();
Filesystem::writeFile($temp, $file->loadFileData());
if (!$zip->open($temp)) {
- throw new Exception('Unable to open ZIP');
+ throw new Exception(pht('Unable to open ZIP.'));
}
// Get all of the paths and their data from the ZIP.
$mappings = array();
for ($i = 0; $i < $zip->numFiles; $i++) {
$path = trim($zip->getNameIndex($i), '/');
$stream = $zip->getStream($path);
$data = null;
// If the stream is false, then it is a directory entry. We leave
// $data set to null for directories so we know not to create a
// version entry for them.
if ($stream !== false) {
$data = stream_get_contents($stream);
fclose($stream);
}
$mappings[$path] = $data;
}
// We need to detect any directories that are in the ZIP folder that
// aren't explicitly noted in the ZIP. This can happen if the file
// entries in the ZIP look like:
//
// * something/blah.png
// * something/other.png
// * test.png
//
// Where there is no explicit "something/" entry.
foreach ($mappings as $path_key => $data) {
if ($data === null) {
continue;
}
$directory = dirname($path_key);
while ($directory !== '.') {
if (!array_key_exists($directory, $mappings)) {
$mappings[$directory] = null;
}
if (dirname($directory) === $directory) {
// dirname() will not reduce this directory any further; to
// prevent infinite loop we just break out here.
break;
}
$directory = dirname($directory);
}
}
// Adjust the paths relative to this fragment so we can look existing
// fragments up in the DB.
$base_path = $this->getPath();
$paths = array();
foreach ($mappings as $p => $data) {
$paths[] = $base_path.'/'.$p;
}
// FIXME: What happens when a child exists, but the current user
// can't see it. We're going to create a new child with the exact
// same path and then bad things will happen.
$children = id(new PhragmentFragmentQuery())
->setViewer($viewer)
->needLatestVersion(true)
->withLeadingPath($this->getPath().'/')
->execute();
$children = mpull($children, null, 'getPath');
// Iterate over the existing fragments.
foreach ($children as $full_path => $child) {
$path = substr($full_path, strlen($base_path) + 1);
if (array_key_exists($path, $mappings)) {
if ($child->isDirectory() && $mappings[$path] === null) {
// Don't create a version entry for a directory
// (unless it's been converted into a file).
continue;
}
// The file is being updated.
$file = PhabricatorFile::newFromFileData(
$mappings[$path],
array('name' => basename($path)));
$child->updateFromFile($viewer, $file);
} else {
// The file is being deleted.
$child->deleteFile($viewer);
}
}
// Iterate over the mappings to find new files.
foreach ($mappings as $path => $data) {
if (!array_key_exists($base_path.'/'.$path, $children)) {
// The file is being created. If the data is null,
// then this is explicitly a directory being created.
$file = null;
if ($mappings[$path] !== null) {
$file = PhabricatorFile::newFromFileData(
$mappings[$path],
array('name' => basename($path)));
}
self::createFromFile(
$viewer,
$file,
$base_path.'/'.$path,
$this->getViewPolicy(),
$this->getEditPolicy());
}
}
}
/**
* Delete the contents of the specified fragment.
*/
public function deleteFile(PhabricatorUser $viewer) {
$existing = id(new PhragmentFragmentVersionQuery())
->setViewer($viewer)
->withFragmentPHIDs(array($this->getPHID()))
->execute();
$sequence = count($existing);
$this->openTransaction();
$version = id(new PhragmentFragmentVersion());
$version->setSequence($sequence);
$version->setFragmentPHID($this->getPHID());
$version->setFilePHID(null);
$version->save();
$this->setLatestVersionPHID($version->getPHID());
$this->save();
$this->saveTransaction();
}
/* -( Utility ) ---------------------------------------------------------- */
public function getFragmentMappings(
PhabricatorUser $viewer,
$base_path) {
$children = id(new PhragmentFragmentQuery())
->setViewer($viewer)
->needLatestVersion(true)
->withLeadingPath($this->getPath().'/')
->withDepths(array($this->getDepth() + 1))
->execute();
if (count($children) === 0) {
$path = substr($this->getPath(), strlen($base_path) + 1);
return array($path => $this);
} else {
$mappings = array();
foreach ($children as $child) {
$child_mappings = $child->getFragmentMappings(
$viewer,
$base_path);
foreach ($child_mappings as $key => $value) {
$mappings[$key] = $value;
}
}
return $mappings;
}
}
/* -( Policy Interface )--------------------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
return $this->getViewPolicy();
case PhabricatorPolicyCapability::CAN_EDIT:
return $this->getEditPolicy();
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return false;
}
public function describeAutomaticCapability($capability) {
return null;
}
}
diff --git a/src/applications/phrequent/conduit/PhrequentTrackingConduitAPIMethod.php b/src/applications/phrequent/conduit/PhrequentTrackingConduitAPIMethod.php
index f5391a56f..4f7301954 100644
--- a/src/applications/phrequent/conduit/PhrequentTrackingConduitAPIMethod.php
+++ b/src/applications/phrequent/conduit/PhrequentTrackingConduitAPIMethod.php
@@ -1,45 +1,44 @@
<?php
final class PhrequentTrackingConduitAPIMethod
extends PhrequentConduitAPIMethod {
public function getAPIMethodName() {
return 'phrequent.tracking';
}
public function getMethodDescription() {
- return pht(
- 'Returns current objects being tracked in Phrequent.');
+ return pht('Returns current objects being tracked in Phrequent.');
}
public function getMethodStatus() {
return self::METHOD_STATUS_UNSTABLE;
}
protected function defineParamTypes() {
return array();
}
protected function defineReturnType() {
return 'array';
}
protected function execute(ConduitAPIRequest $request) {
$user = $request->getUser();
$times = id(new PhrequentUserTimeQuery())
->setViewer($user)
->needPreemptingEvents(true)
->withEnded(PhrequentUserTimeQuery::ENDED_NO)
->withUserPHIDs(array($user->getPHID()))
->execute();
$now = time();
$results = id(new PhrequentTimeBlock($times))
->getCurrentWorkStack($now);
return array('data' => $results);
}
}
diff --git a/src/applications/phrequent/controller/PhrequentTrackController.php b/src/applications/phrequent/controller/PhrequentTrackController.php
index fd52bcea1..de0b75324 100644
--- a/src/applications/phrequent/controller/PhrequentTrackController.php
+++ b/src/applications/phrequent/controller/PhrequentTrackController.php
@@ -1,176 +1,174 @@
<?php
final class PhrequentTrackController
extends PhrequentController {
private $verb;
private $phid;
public function willProcessRequest(array $data) {
$this->phid = $data['phid'];
$this->verb = $data['verb'];
}
public function processRequest() {
$request = $this->getRequest();
$viewer = $request->getUser();
$phid = $this->phid;
$handle = id(new PhabricatorHandleQuery())
->setViewer($viewer)
->withPHIDs(array($phid))
->executeOne();
$done_uri = $handle->getURI();
$current_timer = null;
switch ($this->verb) {
case 'start':
$button_text = pht('Start Tracking');
$title_text = pht('Start Tracking Time');
$inner_text = pht('What time did you start working?');
$action_text = pht('Start Timer');
$label_text = pht('Start Time');
break;
case 'stop':
$button_text = pht('Stop Tracking');
$title_text = pht('Stop Tracking Time');
$inner_text = pht('What time did you stop working?');
$action_text = pht('Stop Timer');
$label_text = pht('Stop Time');
$current_timer = id(new PhrequentUserTimeQuery())
->setViewer($viewer)
->withUserPHIDs(array($viewer->getPHID()))
->withObjectPHIDs(array($phid))
->withEnded(PhrequentUserTimeQuery::ENDED_NO)
->executeOne();
if (!$current_timer) {
return $this->newDialog()
->setTitle(pht('Not Tracking Time'))
->appendParagraph(
- pht(
- 'You are not currently tracking time on this object.'))
+ pht('You are not currently tracking time on this object.'))
->addCancelButton($done_uri);
}
break;
default:
return new Aphront404Response();
}
$errors = array();
$v_note = null;
$e_date = null;
$timestamp = AphrontFormDateControlValue::newFromEpoch(
$viewer,
time());
if ($request->isDialogFormPost()) {
$v_note = $request->getStr('note');
$timestamp = AphrontFormDateControlValue::newFromRequest(
$request,
'epoch');
if (!$timestamp->isValid()) {
$errors[] = pht('Please choose a valid date.');
$e_date = pht('Invalid');
} else {
$max_time = PhabricatorTime::getNow();
if ($timestamp->getEpoch() > $max_time) {
if ($this->isStoppingTracking()) {
$errors[] = pht(
'You can not stop tracking time at a future time. Enter the '.
'current time, or a time in the past.');
} else {
$errors[] = pht(
'You can not start tracking time at a future time. Enter the '.
'current time, or a time in the past.');
}
$e_date = pht('Invalid');
}
if ($this->isStoppingTracking()) {
$min_time = $current_timer->getDateStarted();
if ($min_time > $timestamp->getEpoch()) {
- $errors[] = pht(
- 'Stop time must be after start time.');
+ $errors[] = pht('Stop time must be after start time.');
$e_date = pht('Invalid');
}
}
}
if (!$errors) {
$editor = new PhrequentTrackingEditor();
if ($this->isStartingTracking()) {
$editor->startTracking(
$viewer,
$this->phid,
$timestamp->getEpoch());
} else if ($this->isStoppingTracking()) {
$editor->stopTracking(
$viewer,
$this->phid,
$timestamp->getEpoch(),
$v_note);
}
return id(new AphrontRedirectResponse())->setURI($done_uri);
}
}
$dialog = $this->newDialog()
->setTitle($title_text)
->setWidth(AphrontDialogView::WIDTH_FORM)
->setErrors($errors)
->appendParagraph($inner_text);
$form = new PHUIFormLayoutView();
if ($this->isStoppingTracking()) {
$start_time = $current_timer->getDateStarted();
$start_string = pht(
'%s (%s ago)',
phabricator_datetime($start_time, $viewer),
phutil_format_relative_time(PhabricatorTime::getNow() - $start_time));
$form->appendChild(
id(new AphrontFormStaticControl())
->setLabel(pht('Started At'))
->setValue($start_string));
}
$form->appendChild(
id(new AphrontFormDateControl())
->setUser($viewer)
->setName('epoch')
->setLabel($action_text)
->setError($e_date)
->setValue($timestamp));
if ($this->isStoppingTracking()) {
$form->appendChild(
id(new AphrontFormTextControl())
->setLabel(pht('Note'))
->setName('note')
->setValue($v_note));
}
$dialog->appendChild($form);
$dialog->addCancelButton($done_uri);
$dialog->addSubmitButton($action_text);
return $dialog;
}
private function isStartingTracking() {
return $this->verb === 'start';
}
private function isStoppingTracking() {
return $this->verb === 'stop';
}
}
diff --git a/src/applications/phrequent/query/PhrequentUserTimeQuery.php b/src/applications/phrequent/query/PhrequentUserTimeQuery.php
index 61fa97816..d0d1160df 100644
--- a/src/applications/phrequent/query/PhrequentUserTimeQuery.php
+++ b/src/applications/phrequent/query/PhrequentUserTimeQuery.php
@@ -1,333 +1,333 @@
<?php
final class PhrequentUserTimeQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
const ORDER_ID_ASC = 0;
const ORDER_ID_DESC = 1;
const ORDER_STARTED_ASC = 2;
const ORDER_STARTED_DESC = 3;
const ORDER_ENDED_ASC = 4;
const ORDER_ENDED_DESC = 5;
const ENDED_YES = 0;
const ENDED_NO = 1;
const ENDED_ALL = 2;
private $ids;
private $userPHIDs;
private $objectPHIDs;
private $ended = self::ENDED_ALL;
private $needPreemptingEvents;
public function withIDs(array $ids) {
$this->ids = $ids;
return $this;
}
public function withUserPHIDs(array $user_phids) {
$this->userPHIDs = $user_phids;
return $this;
}
public function withObjectPHIDs(array $object_phids) {
$this->objectPHIDs = $object_phids;
return $this;
}
public function withEnded($ended) {
$this->ended = $ended;
return $this;
}
public function setOrder($order) {
switch ($order) {
case self::ORDER_ID_ASC:
$this->setOrderVector(array('-id'));
break;
case self::ORDER_ID_DESC:
$this->setOrderVector(array('id'));
break;
case self::ORDER_STARTED_ASC:
$this->setOrderVector(array('-start', '-id'));
break;
case self::ORDER_STARTED_DESC:
$this->setOrderVector(array('start', 'id'));
break;
case self::ORDER_ENDED_ASC:
$this->setOrderVector(array('-end', '-id'));
break;
case self::ORDER_ENDED_DESC:
$this->setOrderVector(array('end', 'id'));
break;
default:
throw new Exception(pht('Unknown order "%s".', $order));
}
return $this;
}
public function needPreemptingEvents($need_events) {
$this->needPreemptingEvents = $need_events;
return $this;
}
protected function buildWhereClause(AphrontDatabaseConnection $conn) {
$where = array();
if ($this->ids !== null) {
$where[] = qsprintf(
$conn,
'id IN (%Ld)',
$this->ids);
}
if ($this->userPHIDs !== null) {
$where[] = qsprintf(
$conn,
'userPHID IN (%Ls)',
$this->userPHIDs);
}
if ($this->objectPHIDs !== null) {
$where[] = qsprintf(
$conn,
'objectPHID IN (%Ls)',
$this->objectPHIDs);
}
switch ($this->ended) {
case self::ENDED_ALL:
break;
case self::ENDED_YES:
$where[] = qsprintf(
$conn,
'dateEnded IS NOT NULL');
break;
case self::ENDED_NO:
$where[] = qsprintf(
$conn,
'dateEnded IS NULL');
break;
default:
- throw new Exception("Unknown ended '{$this->ended}'!");
+ throw new Exception(pht("Unknown ended '%s'!", $this->ended));
}
$where[] = $this->buildPagingClause($conn);
return $this->formatWhereClause($where);
}
public function getOrderableColumns() {
return parent::getOrderableColumns() + array(
'start' => array(
'column' => 'dateStarted',
'type' => 'int',
),
'end' => array(
'column' => 'dateEnded',
'type' => 'int',
'null' => 'head',
),
);
}
protected function getPagingValueMap($cursor, array $keys) {
$usertime = $this->loadCursorObject($cursor);
return array(
'id' => $usertime->getID(),
'start' => $usertime->getDateStarted(),
'end' => $usertime->getDateEnded(),
);
}
protected function loadPage() {
$usertime = new PhrequentUserTime();
$conn = $usertime->establishConnection('r');
$data = queryfx_all(
$conn,
'SELECT usertime.* FROM %T usertime %Q %Q %Q',
$usertime->getTableName(),
$this->buildWhereClause($conn),
$this->buildOrderClause($conn),
$this->buildLimitClause($conn));
return $usertime->loadAllFromArray($data);
}
protected function didFilterPage(array $page) {
if ($this->needPreemptingEvents) {
$usertime = new PhrequentUserTime();
$conn_r = $usertime->establishConnection('r');
$preempt = array();
foreach ($page as $event) {
$preempt[] = qsprintf(
$conn_r,
'(userPHID = %s AND
(dateStarted BETWEEN %d AND %d) AND
(dateEnded IS NULL OR dateEnded > %d))',
$event->getUserPHID(),
$event->getDateStarted(),
nonempty($event->getDateEnded(), PhabricatorTime::getNow()),
$event->getDateStarted());
}
$preempting_events = queryfx_all(
$conn_r,
'SELECT * FROM %T WHERE %Q ORDER BY dateStarted ASC, id ASC',
$usertime->getTableName(),
implode(' OR ', $preempt));
$preempting_events = $usertime->loadAllFromArray($preempting_events);
$preempting_events = mgroup($preempting_events, 'getUserPHID');
foreach ($page as $event) {
$e_start = $event->getDateStarted();
$e_end = $event->getDateEnded();
$select = array();
$user_events = idx($preempting_events, $event->getUserPHID(), array());
foreach ($user_events as $u_event) {
if ($u_event->getID() == $event->getID()) {
// Don't allow an event to preempt itself.
continue;
}
$u_start = $u_event->getDateStarted();
$u_end = $u_event->getDateEnded();
if ($u_start < $e_start) {
// This event started before our event started, so it's not
// preempting us.
continue;
}
if ($u_start == $e_start) {
if ($u_event->getID() < $event->getID()) {
// This event started at the same time as our event started,
// but has a lower ID, so it's not preempting us.
continue;
}
}
if (($e_end !== null) && ($u_start > $e_end)) {
// Our event has ended, and this event started after it ended.
continue;
}
if (($u_end !== null) && ($u_end < $e_start)) {
// This event ended before our event began.
continue;
}
$select[] = $u_event;
}
$event->attachPreemptingEvents($select);
}
}
return $page;
}
/* -( Helper Functions ) --------------------------------------------------- */
public static function getEndedSearchOptions() {
return array(
self::ENDED_ALL => pht('All'),
self::ENDED_NO => pht('No'),
self::ENDED_YES => pht('Yes'),
);
}
public static function getOrderSearchOptions() {
return array(
self::ORDER_STARTED_ASC => pht('by furthest start date'),
self::ORDER_STARTED_DESC => pht('by nearest start date'),
self::ORDER_ENDED_ASC => pht('by furthest end date'),
self::ORDER_ENDED_DESC => pht('by nearest end date'),
);
}
public static function getUserTotalObjectsTracked(
PhabricatorUser $user,
$limit = PHP_INT_MAX) {
$usertime_dao = new PhrequentUserTime();
$conn = $usertime_dao->establishConnection('r');
$count = queryfx_one(
$conn,
'SELECT COUNT(usertime.id) N FROM %T usertime '.
'WHERE usertime.userPHID = %s '.
'AND usertime.dateEnded IS NULL '.
'LIMIT %d',
$usertime_dao->getTableName(),
$user->getPHID(),
$limit);
return $count['N'];
}
public static function isUserTrackingObject(
PhabricatorUser $user,
$phid) {
$usertime_dao = new PhrequentUserTime();
$conn = $usertime_dao->establishConnection('r');
$count = queryfx_one(
$conn,
'SELECT COUNT(usertime.id) N FROM %T usertime '.
'WHERE usertime.userPHID = %s '.
'AND usertime.objectPHID = %s '.
'AND usertime.dateEnded IS NULL',
$usertime_dao->getTableName(),
$user->getPHID(),
$phid);
return $count['N'] > 0;
}
public static function getUserTimeSpentOnObject(
PhabricatorUser $user,
$phid) {
$usertime_dao = new PhrequentUserTime();
$conn = $usertime_dao->establishConnection('r');
// First calculate all the time spent where the
// usertime blocks have ended.
$sum_ended = queryfx_one(
$conn,
'SELECT SUM(usertime.dateEnded - usertime.dateStarted) N '.
'FROM %T usertime '.
'WHERE usertime.userPHID = %s '.
'AND usertime.objectPHID = %s '.
'AND usertime.dateEnded IS NOT NULL',
$usertime_dao->getTableName(),
$user->getPHID(),
$phid);
// Now calculate the time spent where the usertime
// blocks have not yet ended.
$sum_not_ended = queryfx_one(
$conn,
'SELECT SUM(UNIX_TIMESTAMP() - usertime.dateStarted) N '.
'FROM %T usertime '.
'WHERE usertime.userPHID = %s '.
'AND usertime.objectPHID = %s '.
'AND usertime.dateEnded IS NULL',
$usertime_dao->getTableName(),
$user->getPHID(),
$phid);
return $sum_ended['N'] + $sum_not_ended['N'];
}
public function getQueryApplicationClass() {
return 'PhabricatorPhrequentApplication';
}
}
diff --git a/src/applications/phriction/conduit/PhrictionHistoryConduitAPIMethod.php b/src/applications/phriction/conduit/PhrictionHistoryConduitAPIMethod.php
index 9a4098b5b..454af2934 100644
--- a/src/applications/phriction/conduit/PhrictionHistoryConduitAPIMethod.php
+++ b/src/applications/phriction/conduit/PhrictionHistoryConduitAPIMethod.php
@@ -1,53 +1,53 @@
<?php
final class PhrictionHistoryConduitAPIMethod extends PhrictionConduitAPIMethod {
public function getAPIMethodName() {
return 'phriction.history';
}
public function getMethodDescription() {
return pht('Retrieve history about a Phriction document.');
}
protected function defineParamTypes() {
return array(
'slug' => 'required string',
);
}
protected function defineReturnType() {
return 'nonempty list';
}
protected function defineErrorTypes() {
return array(
- 'ERR-BAD-DOCUMENT' => 'No such document exists.',
+ 'ERR-BAD-DOCUMENT' => pht('No such document exists.'),
);
}
protected function execute(ConduitAPIRequest $request) {
$slug = $request->getValue('slug');
$doc = id(new PhrictionDocumentQuery())
->setViewer($request->getUser())
->withSlugs(array(PhabricatorSlug::normalize($slug)))
->executeOne();
if (!$doc) {
throw new ConduitException('ERR-BAD-DOCUMENT');
}
$content = id(new PhrictionContent())->loadAllWhere(
'documentID = %d ORDER BY version DESC',
$doc->getID());
$results = array();
foreach ($content as $version) {
$results[] = $this->buildDocumentContentDictionary(
$doc,
$version);
}
return $results;
}
}
diff --git a/src/applications/phriction/conduit/PhrictionInfoConduitAPIMethod.php b/src/applications/phriction/conduit/PhrictionInfoConduitAPIMethod.php
index 14e424e83..60d9ece73 100644
--- a/src/applications/phriction/conduit/PhrictionInfoConduitAPIMethod.php
+++ b/src/applications/phriction/conduit/PhrictionInfoConduitAPIMethod.php
@@ -1,46 +1,46 @@
<?php
final class PhrictionInfoConduitAPIMethod extends PhrictionConduitAPIMethod {
public function getAPIMethodName() {
return 'phriction.info';
}
public function getMethodDescription() {
return pht('Retrieve information about a Phriction document.');
}
protected function defineParamTypes() {
return array(
'slug' => 'required string',
);
}
protected function defineReturnType() {
return 'nonempty dict';
}
protected function defineErrorTypes() {
return array(
- 'ERR-BAD-DOCUMENT' => 'No such document exists.',
+ 'ERR-BAD-DOCUMENT' => pht('No such document exists.'),
);
}
protected function execute(ConduitAPIRequest $request) {
$slug = $request->getValue('slug');
$document = id(new PhrictionDocumentQuery())
->setViewer($request->getUser())
->withSlugs(array(PhabricatorSlug::normalize($slug)))
->needContent(true)
->executeOne();
if (!$document) {
throw new ConduitException('ERR-BAD-DOCUMENT');
}
return $this->buildDocumentInfoDictionary(
$document,
$document->getContent());
}
}
diff --git a/src/applications/phriction/constants/PhrictionActionConstants.php b/src/applications/phriction/constants/PhrictionActionConstants.php
index 842962ca9..f39691181 100644
--- a/src/applications/phriction/constants/PhrictionActionConstants.php
+++ b/src/applications/phriction/constants/PhrictionActionConstants.php
@@ -1,23 +1,23 @@
<?php
final class PhrictionActionConstants extends PhrictionConstants {
const ACTION_CREATE = 'create';
const ACTION_EDIT = 'edit';
const ACTION_DELETE = 'delete';
const ACTION_MOVE_AWAY = 'move to';
const ACTION_MOVE_HERE = 'move here';
public static function getActionPastTenseVerb($action) {
- static $map = array(
- self::ACTION_CREATE => 'created',
- self::ACTION_EDIT => 'edited',
- self::ACTION_DELETE => 'deleted',
- self::ACTION_MOVE_AWAY => 'moved',
- self::ACTION_MOVE_HERE => 'moved',
+ $map = array(
+ self::ACTION_CREATE => pht('created'),
+ self::ACTION_EDIT => pht('edited'),
+ self::ACTION_DELETE => pht('deleted'),
+ self::ACTION_MOVE_AWAY => pht('moved'),
+ self::ACTION_MOVE_HERE => pht('moved'),
);
- return idx($map, $action, "brazenly {$action}'d");
+ return idx($map, $action, pht("brazenly %s'd", $action));
}
}
diff --git a/src/applications/phriction/constants/PhrictionChangeType.php b/src/applications/phriction/constants/PhrictionChangeType.php
index 127c3e049..1adf9ea6a 100644
--- a/src/applications/phriction/constants/PhrictionChangeType.php
+++ b/src/applications/phriction/constants/PhrictionChangeType.php
@@ -1,23 +1,23 @@
<?php
final class PhrictionChangeType extends PhrictionConstants {
const CHANGE_EDIT = 0;
const CHANGE_DELETE = 1;
const CHANGE_MOVE_HERE = 2;
const CHANGE_MOVE_AWAY = 3;
const CHANGE_STUB = 4;
public static function getChangeTypeLabel($const) {
- static $map = array(
- self::CHANGE_EDIT => 'Edit',
- self::CHANGE_DELETE => 'Delete',
- self::CHANGE_MOVE_HERE => 'Move Here',
- self::CHANGE_MOVE_AWAY => 'Move Away',
- self::CHANGE_STUB => 'Created through child',
+ $map = array(
+ self::CHANGE_EDIT => pht('Edit'),
+ self::CHANGE_DELETE => pht('Delete'),
+ self::CHANGE_MOVE_HERE => pht('Move Here'),
+ self::CHANGE_MOVE_AWAY => pht('Move Away'),
+ self::CHANGE_STUB => pht('Created through child'),
);
- return idx($map, $const, '???');
+ return idx($map, $const, pht('Unknown'));
}
}
diff --git a/src/applications/phriction/controller/PhrictionDocumentController.php b/src/applications/phriction/controller/PhrictionDocumentController.php
index 1031a2a05..c96cbc57f 100644
--- a/src/applications/phriction/controller/PhrictionDocumentController.php
+++ b/src/applications/phriction/controller/PhrictionDocumentController.php
@@ -1,457 +1,457 @@
<?php
final class PhrictionDocumentController
extends PhrictionController {
private $slug;
public function shouldAllowPublic() {
return true;
}
public function willProcessRequest(array $data) {
$this->slug = $data['slug'];
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$slug = PhabricatorSlug::normalize($this->slug);
if ($slug != $this->slug) {
$uri = PhrictionDocument::getSlugURI($slug);
// Canonicalize pages to their one true URI.
return id(new AphrontRedirectResponse())->setURI($uri);
}
require_celerity_resource('phriction-document-css');
$document = id(new PhrictionDocumentQuery())
->setViewer($user)
->withSlugs(array($slug))
->executeOne();
$version_note = null;
$core_content = '';
$move_notice = '';
$properties = null;
$content = null;
if (!$document) {
$document = PhrictionDocument::initializeNewDocument($user, $slug);
$create_uri = '/phriction/edit/?slug='.$slug;
$notice = new PHUIInfoView();
$notice->setSeverity(PHUIInfoView::SEVERITY_NODATA);
$notice->setTitle(pht('No content here!'));
$notice->appendChild(
pht(
'No document found at %s. You can <strong>'.
'<a href="%s">create a new document here</a></strong>.',
phutil_tag('tt', array(), $slug),
$create_uri));
$core_content = $notice;
$page_title = pht('Page Not Found');
} else {
$version = $request->getInt('v');
if ($version) {
$content = id(new PhrictionContent())->loadOneWhere(
'documentID = %d AND version = %d',
$document->getID(),
$version);
if (!$content) {
return new Aphront404Response();
}
if ($content->getID() != $document->getContentID()) {
$vdate = phabricator_datetime($content->getDateCreated(), $user);
$version_note = new PHUIInfoView();
$version_note->setSeverity(PHUIInfoView::SEVERITY_NOTICE);
$version_note->appendChild(
pht('You are viewing an older version of this document, as it '.
'appeared on %s.', $vdate));
}
} else {
$content = id(new PhrictionContent())->load($document->getContentID());
}
$page_title = $content->getTitle();
$properties = $this
->buildPropertyListView($document, $content, $slug);
$doc_status = $document->getStatus();
$current_status = $content->getChangeType();
if ($current_status == PhrictionChangeType::CHANGE_EDIT ||
$current_status == PhrictionChangeType::CHANGE_MOVE_HERE) {
$core_content = $content->renderContent($user);
} else if ($current_status == PhrictionChangeType::CHANGE_DELETE) {
$notice = new PHUIInfoView();
$notice->setSeverity(PHUIInfoView::SEVERITY_NOTICE);
$notice->setTitle(pht('Document Deleted'));
$notice->appendChild(
pht('This document has been deleted. You can edit it to put new '.
'content here, or use history to revert to an earlier version.'));
$core_content = $notice->render();
} else if ($current_status == PhrictionChangeType::CHANGE_STUB) {
$notice = new PHUIInfoView();
$notice->setSeverity(PHUIInfoView::SEVERITY_NOTICE);
$notice->setTitle(pht('Empty Document'));
$notice->appendChild(
pht('This document is empty. You can edit it to put some proper '.
'content here.'));
$core_content = $notice->render();
} else if ($current_status == PhrictionChangeType::CHANGE_MOVE_AWAY) {
$new_doc_id = $content->getChangeRef();
$slug_uri = null;
// If the new document exists and the viewer can see it, provide a link
// to it. Otherwise, render a generic message.
$new_docs = id(new PhrictionDocumentQuery())
->setViewer($user)
->withIDs(array($new_doc_id))
->execute();
if ($new_docs) {
$new_doc = head($new_docs);
$slug_uri = PhrictionDocument::getSlugURI($new_doc->getSlug());
}
$notice = id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_NOTICE);
if ($slug_uri) {
$notice->appendChild(
phutil_tag(
'p',
array(),
pht(
'This document has been moved to %s. You can edit it to put '.
'new content here, or use history to revert to an earlier '.
'version.',
phutil_tag('a', array('href' => $slug_uri), $slug_uri))));
} else {
$notice->appendChild(
phutil_tag(
'p',
array(),
pht(
'This document has been moved. You can edit it to put new '.
'contne here, or use history to revert to an earlier '.
'version.')));
}
$core_content = $notice->render();
} else {
- throw new Exception("Unknown document status '{$doc_status}'!");
+ throw new Exception(pht("Unknown document status '%s'!", $doc_status));
}
$move_notice = null;
if ($current_status == PhrictionChangeType::CHANGE_MOVE_HERE) {
$from_doc_id = $content->getChangeRef();
$slug_uri = null;
// If the old document exists and is visible, provide a link to it.
$from_docs = id(new PhrictionDocumentQuery())
->setViewer($user)
->withIDs(array($from_doc_id))
->execute();
if ($from_docs) {
$from_doc = head($from_docs);
$slug_uri = PhrictionDocument::getSlugURI($from_doc->getSlug());
}
$move_notice = id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_NOTICE);
if ($slug_uri) {
$move_notice->appendChild(
pht(
'This document was moved from %s.',
phutil_tag('a', array('href' => $slug_uri), $slug_uri)));
} else {
// Render this for consistency, even though it's a bit silly.
$move_notice->appendChild(
pht('This document was moved from elsewhere.'));
}
}
}
$children = $this->renderDocumentChildren($slug);
$actions = $this->buildActionView($user, $document);
$crumbs = $this->buildApplicationCrumbs();
$crumbs->setBorder(true);
$crumb_views = $this->renderBreadcrumbs($slug);
foreach ($crumb_views as $view) {
$crumbs->addCrumb($view);
}
$action_button = id(new PHUIButtonView())
->setTag('a')
->setText(pht('Actions'))
->setHref('#')
->setIconFont('fa-bars')
->addClass('phui-mobile-menu')
->setDropdownMenu($actions);
$header = id(new PHUIHeaderView())
->setUser($user)
->setPolicyObject($document)
->setHeader($page_title)
->addActionLink($action_button);
if ($content) {
$header->setEpoch($content->getDateCreated());
}
$prop_list = null;
if ($properties) {
$prop_list = new PHUIPropertyGroupView();
$prop_list->addPropertyList($properties);
}
$page_content = id(new PHUIDocumentView())
->setFontKit(PHUIDocumentView::FONT_SOURCE_SANS)
->setHeader($header)
->appendChild(
array(
$prop_list,
$version_note,
$move_notice,
$core_content,
));
return $this->buildApplicationPage(
array(
$crumbs->render(),
$page_content,
$children,
),
array(
'pageObjects' => array($document->getPHID()),
'title' => $page_title,
));
}
private function buildPropertyListView(
PhrictionDocument $document,
PhrictionContent $content,
$slug) {
$viewer = $this->getRequest()->getUser();
$view = id(new PHUIPropertyListView())
->setUser($viewer)
->setObject($document);
$view->addProperty(
pht('Last Author'),
$viewer->renderHandle($content->getAuthorPHID()));
return $view;
}
private function buildActionView(
PhabricatorUser $user,
PhrictionDocument $document) {
$can_edit = PhabricatorPolicyFilter::hasCapability(
$user,
$document,
PhabricatorPolicyCapability::CAN_EDIT);
$slug = PhabricatorSlug::normalize($this->slug);
$action_view = id(new PhabricatorActionListView())
->setUser($user)
->setObjectURI($this->getRequest()->getRequestURI())
->setObject($document);
if (!$document->getID()) {
return $action_view->addAction(
id(new PhabricatorActionView())
->setName(pht('Create This Document'))
->setIcon('fa-plus-square')
->setHref('/phriction/edit/?slug='.$slug));
}
$action_view->addAction(
id(new PhabricatorActionView())
->setName(pht('Edit Document'))
->setIcon('fa-pencil')
->setHref('/phriction/edit/'.$document->getID().'/'));
if ($document->getStatus() == PhrictionDocumentStatus::STATUS_EXISTS) {
$action_view->addAction(
id(new PhabricatorActionView())
->setName(pht('Move Document'))
->setIcon('fa-arrows')
->setHref('/phriction/move/'.$document->getID().'/')
->setWorkflow(true));
$action_view->addAction(
id(new PhabricatorActionView())
->setName(pht('Delete Document'))
->setIcon('fa-times')
->setHref('/phriction/delete/'.$document->getID().'/')
->setWorkflow(true));
}
return
$action_view->addAction(
id(new PhabricatorActionView())
->setName(pht('View History'))
->setIcon('fa-list')
->setHref(PhrictionDocument::getSlugURI($slug, 'history')));
}
private function renderDocumentChildren($slug) {
$d_child = PhabricatorSlug::getDepth($slug) + 1;
$d_grandchild = PhabricatorSlug::getDepth($slug) + 2;
$limit = 250;
$query = id(new PhrictionDocumentQuery())
->setViewer($this->getRequest()->getUser())
->withDepths(array($d_child, $d_grandchild))
->withSlugPrefix($slug == '/' ? '' : $slug)
->withStatuses(array(
PhrictionDocumentStatus::STATUS_EXISTS,
PhrictionDocumentStatus::STATUS_STUB,
))
->setLimit($limit)
->setOrder(PhrictionDocumentQuery::ORDER_HIERARCHY)
->needContent(true);
$children = $query->execute();
if (!$children) {
return;
}
// We're going to render in one of three modes to try to accommodate
// different information scales:
//
// - If we found fewer than $limit rows, we know we have all the children
// and grandchildren and there aren't all that many. We can just render
// everything.
// - If we found $limit rows but the results included some grandchildren,
// we just throw them out and render only the children, as we know we
// have them all.
// - If we found $limit rows and the results have no grandchildren, we
// have a ton of children. Render them and then let the user know that
// this is not an exhaustive list.
if (count($children) == $limit) {
$more_children = true;
foreach ($children as $child) {
if ($child->getDepth() == $d_grandchild) {
$more_children = false;
}
}
$show_grandchildren = false;
} else {
$show_grandchildren = true;
$more_children = false;
}
$children_dicts = array();
$grandchildren_dicts = array();
foreach ($children as $key => $child) {
$child_dict = array(
'slug' => $child->getSlug(),
'depth' => $child->getDepth(),
'title' => $child->getContent()->getTitle(),
);
if ($child->getDepth() == $d_child) {
$children_dicts[] = $child_dict;
continue;
} else {
unset($children[$key]);
if ($show_grandchildren) {
$ancestors = PhabricatorSlug::getAncestry($child->getSlug());
$grandchildren_dicts[end($ancestors)][] = $child_dict;
}
}
}
// Fill in any missing children.
$known_slugs = mpull($children, null, 'getSlug');
foreach ($grandchildren_dicts as $slug => $ignored) {
if (empty($known_slugs[$slug])) {
$children_dicts[] = array(
'slug' => $slug,
'depth' => $d_child,
'title' => PhabricatorSlug::getDefaultTitle($slug),
'empty' => true,
);
}
}
$children_dicts = isort($children_dicts, 'title');
$list = array();
foreach ($children_dicts as $child) {
$list[] = hsprintf('<li>');
$list[] = $this->renderChildDocumentLink($child);
$grand = idx($grandchildren_dicts, $child['slug'], array());
if ($grand) {
$list[] = hsprintf('<ul>');
foreach ($grand as $grandchild) {
$list[] = hsprintf('<li>');
$list[] = $this->renderChildDocumentLink($grandchild);
$list[] = hsprintf('</li>');
}
$list[] = hsprintf('</ul>');
}
$list[] = hsprintf('</li>');
}
if ($more_children) {
$list[] = phutil_tag('li', array(), pht('More...'));
}
$content = array(
phutil_tag(
'div',
array(
'class' => 'phriction-children-header '.
'sprite-gradient gradient-lightblue-header',
),
pht('Document Hierarchy')),
phutil_tag(
'div',
array(
'class' => 'phriction-children',
),
phutil_tag('ul', array(), $list)),
);
return id(new PHUIDocumentView())
->appendChild($content);
}
private function renderChildDocumentLink(array $info) {
$title = nonempty($info['title'], pht('(Untitled Document)'));
$item = phutil_tag(
'a',
array(
'href' => PhrictionDocument::getSlugURI($info['slug']),
),
$title);
if (isset($info['empty'])) {
$item = phutil_tag('em', array(), $item);
}
return $item;
}
protected function getDocumentSlug() {
return $this->slug;
}
}
diff --git a/src/applications/phriction/controller/PhrictionEditController.php b/src/applications/phriction/controller/PhrictionEditController.php
index d09afb974..7ee98120b 100644
--- a/src/applications/phriction/controller/PhrictionEditController.php
+++ b/src/applications/phriction/controller/PhrictionEditController.php
@@ -1,289 +1,292 @@
<?php
final class PhrictionEditController
extends PhrictionController {
private $id;
public function willProcessRequest(array $data) {
$this->id = idx($data, 'id');
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$current_version = null;
if ($this->id) {
$document = id(new PhrictionDocumentQuery())
->setViewer($user)
->withIDs(array($this->id))
->needContent(true)
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
if (!$document) {
return new Aphront404Response();
}
$current_version = $document->getContent()->getVersion();
$revert = $request->getInt('revert');
if ($revert) {
$content = id(new PhrictionContent())->loadOneWhere(
'documentID = %d AND version = %d',
$document->getID(),
$revert);
if (!$content) {
return new Aphront404Response();
}
} else {
$content = $document->getContent();
}
} else {
$slug = $request->getStr('slug');
$slug = PhabricatorSlug::normalize($slug);
if (!$slug) {
return new Aphront404Response();
}
$document = id(new PhrictionDocumentQuery())
->setViewer($user)
->withSlugs(array($slug))
->needContent(true)
->executeOne();
if ($document) {
$content = $document->getContent();
$current_version = $content->getVersion();
} else {
$document = PhrictionDocument::initializeNewDocument($user, $slug);
$content = $document->getContent();
}
}
if ($request->getBool('nodraft')) {
$draft = null;
$draft_key = null;
} else {
if ($document->getPHID()) {
$draft_key = $document->getPHID().':'.$content->getVersion();
} else {
$draft_key = 'phriction:'.$content->getSlug();
}
$draft = id(new PhabricatorDraft())->loadOneWhere(
'authorPHID = %s AND draftKey = %s',
$user->getPHID(),
$draft_key);
}
if ($draft &&
strlen($draft->getDraft()) &&
($draft->getDraft() != $content->getContent())) {
$content_text = $draft->getDraft();
$discard = phutil_tag(
'a',
array(
'href' => $request->getRequestURI()->alter('nodraft', true),
),
pht('discard this draft'));
$draft_note = new PHUIInfoView();
$draft_note->setSeverity(PHUIInfoView::SEVERITY_NOTICE);
- $draft_note->setTitle('Recovered Draft');
- $draft_note->appendChild(hsprintf(
- '<p>Showing a saved draft of your edits, you can %s.</p>',
- $discard));
+ $draft_note->setTitle(pht('Recovered Draft'));
+ $draft_note->appendChild(
+ hsprintf(
+ '<p>%s</p>',
+ pht(
+ 'Showing a saved draft of your edits, you can %s.',
+ $discard)));
} else {
$content_text = $content->getContent();
$draft_note = null;
}
require_celerity_resource('phriction-document-css');
$e_title = true;
$e_content = true;
$validation_exception = null;
$notes = null;
$title = $content->getTitle();
$overwrite = false;
if ($request->isFormPost()) {
$title = $request->getStr('title');
$content_text = $request->getStr('content');
$notes = $request->getStr('description');
$current_version = $request->getInt('contentVersion');
$v_view = $request->getStr('viewPolicy');
$v_edit = $request->getStr('editPolicy');
$xactions = array();
$xactions[] = id(new PhrictionTransaction())
->setTransactionType(PhrictionTransaction::TYPE_TITLE)
->setNewValue($title);
$xactions[] = id(new PhrictionTransaction())
->setTransactionType(PhrictionTransaction::TYPE_CONTENT)
->setNewValue($content_text);
$xactions[] = id(new PhrictionTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_VIEW_POLICY)
->setNewValue($v_view);
$xactions[] = id(new PhrictionTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_EDIT_POLICY)
->setNewValue($v_edit);
$editor = id(new PhrictionTransactionEditor())
->setActor($user)
->setContentSourceFromRequest($request)
->setContinueOnNoEffect(true)
->setDescription($notes)
->setProcessContentVersionError(!$request->getBool('overwrite'))
->setContentVersion($current_version);
try {
$editor->applyTransactions($document, $xactions);
if ($draft) {
$draft->delete();
}
$uri = PhrictionDocument::getSlugURI($document->getSlug());
return id(new AphrontRedirectResponse())->setURI($uri);
} catch (PhabricatorApplicationTransactionValidationException $ex) {
$validation_exception = $ex;
$e_title = nonempty(
$ex->getShortMessage(PhrictionTransaction::TYPE_TITLE),
true);
$e_content = nonempty(
$ex->getShortMessage(PhrictionTransaction::TYPE_CONTENT),
true);
// if we're not supposed to process the content version error, then
// overwrite that content...!
if (!$editor->getProcessContentVersionError()) {
$overwrite = true;
}
$document->setViewPolicy($v_view);
$document->setEditPolicy($v_edit);
}
}
if ($document->getID()) {
$panel_header = pht('Edit Phriction Document');
$page_title = pht('Edit Document');
if ($overwrite) {
$submit_button = pht('Overwrite Changes');
} else {
$submit_button = pht('Save Changes');
}
} else {
$panel_header = pht('Create New Phriction Document');
$submit_button = pht('Create Document');
$page_title = pht('Create Document');
}
$uri = $document->getSlug();
$uri = PhrictionDocument::getSlugURI($uri);
$uri = PhabricatorEnv::getProductionURI($uri);
$cancel_uri = PhrictionDocument::getSlugURI($document->getSlug());
$policies = id(new PhabricatorPolicyQuery())
->setViewer($user)
->setObject($document)
->execute();
$view_capability = PhabricatorPolicyCapability::CAN_VIEW;
$edit_capability = PhabricatorPolicyCapability::CAN_EDIT;
$form = id(new AphrontFormView())
->setUser($user)
->addHiddenInput('slug', $document->getSlug())
->addHiddenInput('nodraft', $request->getBool('nodraft'))
->addHiddenInput('contentVersion', $current_version)
->addHiddenInput('overwrite', $overwrite)
->appendChild(
id(new AphrontFormTextControl())
->setLabel(pht('Title'))
->setValue($title)
->setError($e_title)
->setName('title'))
->appendChild(
id(new AphrontFormStaticControl())
->setLabel(pht('URI'))
->setValue($uri))
->appendChild(
id(new PhabricatorRemarkupControl())
->setLabel(pht('Content'))
->setValue($content_text)
->setError($e_content)
->setHeight(AphrontFormTextAreaControl::HEIGHT_VERY_TALL)
->setName('content')
->setID('document-textarea')
->setUser($user))
->appendChild(
id(new AphrontFormPolicyControl())
->setName('viewPolicy')
->setPolicyObject($document)
->setCapability($view_capability)
->setPolicies($policies)
->setCaption(
$document->describeAutomaticCapability($view_capability)))
->appendChild(
id(new AphrontFormPolicyControl())
->setName('editPolicy')
->setPolicyObject($document)
->setCapability($edit_capability)
->setPolicies($policies)
->setCaption(
$document->describeAutomaticCapability($edit_capability)))
->appendChild(
id(new AphrontFormTextControl())
->setLabel(pht('Edit Notes'))
->setValue($notes)
->setError(null)
->setName('description'))
->appendChild(
id(new AphrontFormSubmitControl())
->addCancelButton($cancel_uri)
->setValue($submit_button));
$form_box = id(new PHUIObjectBoxView())
->setHeaderText($panel_header)
->setValidationException($validation_exception)
->setForm($form);
$preview = id(new PHUIRemarkupPreviewPanel())
->setHeader(pht('Document Preview'))
->setPreviewURI('/phriction/preview/')
->setControlID('document-textarea')
->setSkin('document');
$crumbs = $this->buildApplicationCrumbs();
if ($document->getID()) {
$crumbs->addTextCrumb(
$content->getTitle(),
PhrictionDocument::getSlugURI($document->getSlug()));
$crumbs->addTextCrumb(pht('Edit'));
} else {
$crumbs->addTextCrumb(pht('Create'));
}
return $this->buildApplicationPage(
array(
$crumbs,
$draft_note,
$form_box,
$preview,
),
array(
'title' => $page_title,
));
}
}
diff --git a/src/applications/phriction/controller/PhrictionHistoryController.php b/src/applications/phriction/controller/PhrictionHistoryController.php
index e9daa2be4..ef30382b7 100644
--- a/src/applications/phriction/controller/PhrictionHistoryController.php
+++ b/src/applications/phriction/controller/PhrictionHistoryController.php
@@ -1,173 +1,173 @@
<?php
final class PhrictionHistoryController
extends PhrictionController {
private $slug;
public function shouldAllowPublic() {
return true;
}
public function willProcessRequest(array $data) {
$this->slug = $data['slug'];
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$document = id(new PhrictionDocumentQuery())
->setViewer($user)
->withSlugs(array(PhabricatorSlug::normalize($this->slug)))
->needContent(true)
->executeOne();
if (!$document) {
return new Aphront404Response();
}
$current = $document->getContent();
$pager = new AphrontPagerView();
$pager->setOffset($request->getInt('page'));
$pager->setURI($request->getRequestURI(), 'page');
$history = id(new PhrictionContent())->loadAllWhere(
'documentID = %d ORDER BY version DESC LIMIT %d, %d',
$document->getID(),
$pager->getOffset(),
$pager->getPageSize() + 1);
$history = $pager->sliceResults($history);
$author_phids = mpull($history, 'getAuthorPHID');
$handles = $this->loadViewerHandles($author_phids);
$list = new PHUIObjectItemListView();
$list->setFlush(true);
foreach ($history as $content) {
$author = $handles[$content->getAuthorPHID()]->renderLink();
$slug_uri = PhrictionDocument::getSlugURI($document->getSlug());
$version = $content->getVersion();
$diff_uri = new PhutilURI('/phriction/diff/'.$document->getID().'/');
$vs_previous = null;
if ($content->getVersion() != 1) {
$vs_previous = $diff_uri
->alter('l', $content->getVersion() - 1)
->alter('r', $content->getVersion());
}
$vs_head = null;
if ($content->getID() != $document->getContentID()) {
$vs_head = $diff_uri
->alter('l', $content->getVersion())
->alter('r', $current->getVersion());
}
$change_type = PhrictionChangeType::getChangeTypeLabel(
$content->getChangeType());
switch ($content->getChangeType()) {
case PhrictionChangeType::CHANGE_DELETE:
$color = 'red';
break;
case PhrictionChangeType::CHANGE_EDIT:
$color = 'blue';
break;
case PhrictionChangeType::CHANGE_MOVE_HERE:
$color = 'yellow';
break;
case PhrictionChangeType::CHANGE_MOVE_AWAY:
$color = 'orange';
break;
case PhrictionChangeType::CHANGE_STUB:
$color = 'green';
break;
default:
- throw new Exception('Unknown change type!');
+ throw new Exception(pht('Unknown change type!'));
break;
}
$item = id(new PHUIObjectItemView())
->setHeader(pht('%s by %s', $change_type, $author))
->setBarColor($color)
->addAttribute(
phutil_tag(
'a',
array(
'href' => $slug_uri.'?v='.$version,
),
pht('Version %s', $version)))
->addAttribute(pht('%s %s',
phabricator_date($content->getDateCreated(), $user),
phabricator_time($content->getDateCreated(), $user)));
if ($content->getDescription()) {
$item->addAttribute($content->getDescription());
}
if ($vs_previous) {
$item->addIcon(
'fa-reply',
pht('Show Change'),
array(
'href' => $vs_previous,
));
} else {
$item->addIcon(
'fa-reply grey',
phutil_tag('em', array(), pht('No previous change')));
}
if ($vs_head) {
$item->addIcon(
'fa-reply-all',
pht('Show Later Changes'),
array(
'href' => $vs_head,
));
} else {
$item->addIcon(
'fa-reply-all grey',
phutil_tag('em', array(), pht('No later changes')));
}
$list->addItem($item);
}
$crumbs = $this->buildApplicationCrumbs();
$crumb_views = $this->renderBreadcrumbs($document->getSlug());
foreach ($crumb_views as $view) {
$crumbs->addCrumb($view);
}
$crumbs->addTextCrumb(
pht('History'),
PhrictionDocument::getSlugURI($document->getSlug(), 'history'));
$header = new PHUIHeaderView();
$header->setHeader(pht('Document History for %s',
phutil_tag(
'a',
array('href' => PhrictionDocument::getSlugURI($document->getSlug())),
head($history)->getTitle())));
$obj_box = id(new PHUIObjectBoxView())
->setHeader($header)
->appendChild($list)
->appendChild($pager);
return $this->buildApplicationPage(
array(
$crumbs,
$obj_box,
),
array(
'title' => pht('Document History'),
));
}
}
diff --git a/src/applications/phriction/mail/PhrictionReplyHandler.php b/src/applications/phriction/mail/PhrictionReplyHandler.php
index 0cb9e8f79..7929349e6 100644
--- a/src/applications/phriction/mail/PhrictionReplyHandler.php
+++ b/src/applications/phriction/mail/PhrictionReplyHandler.php
@@ -1,16 +1,17 @@
<?php
final class PhrictionReplyHandler
extends PhabricatorApplicationTransactionReplyHandler {
public function validateMailReceiver($mail_receiver) {
if (!($mail_receiver instanceof PhrictionDocument)) {
- throw new Exception('Mail receiver is not a PhrictionDocument!');
+ throw new Exception(
+ pht('Mail receiver is not a %s!', 'PhrictionDocument'));
}
}
public function getObjectPrefix() {
return PhrictionDocumentPHIDType::TYPECONST;
}
}
diff --git a/src/applications/phriction/query/PhrictionDocumentQuery.php b/src/applications/phriction/query/PhrictionDocumentQuery.php
index b4e80af93..63d1764a9 100644
--- a/src/applications/phriction/query/PhrictionDocumentQuery.php
+++ b/src/applications/phriction/query/PhrictionDocumentQuery.php
@@ -1,339 +1,339 @@
<?php
final class PhrictionDocumentQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $ids;
private $phids;
private $slugs;
private $depths;
private $slugPrefix;
private $statuses;
private $needContent;
private $status = 'status-any';
const STATUS_ANY = 'status-any';
const STATUS_OPEN = 'status-open';
const STATUS_NONSTUB = 'status-nonstub';
const ORDER_CREATED = 'order-created';
const ORDER_UPDATED = 'order-updated';
const ORDER_HIERARCHY = 'order-hierarchy';
public function withIDs(array $ids) {
$this->ids = $ids;
return $this;
}
public function withPHIDs(array $phids) {
$this->phids = $phids;
return $this;
}
public function withSlugs(array $slugs) {
$this->slugs = $slugs;
return $this;
}
public function withDepths(array $depths) {
$this->depths = $depths;
return $this;
}
public function withSlugPrefix($slug_prefix) {
$this->slugPrefix = $slug_prefix;
return $this;
}
public function withStatuses(array $statuses) {
$this->statuses = $statuses;
return $this;
}
public function withStatus($status) {
$this->status = $status;
return $this;
}
public function needContent($need_content) {
$this->needContent = $need_content;
return $this;
}
public function setOrder($order) {
switch ($order) {
case self::ORDER_CREATED:
$this->setOrderVector(array('id'));
break;
case self::ORDER_UPDATED:
$this->setOrderVector(array('updated'));
break;
case self::ORDER_HIERARCHY:
$this->setOrderVector(array('depth', 'title', 'updated'));
break;
default:
throw new Exception(pht('Unknown order "%s".', $order));
}
return $this;
}
protected function loadPage() {
$table = new PhrictionDocument();
$conn_r = $table->establishConnection('r');
$rows = queryfx_all(
$conn_r,
'SELECT d.* FROM %T d %Q %Q %Q %Q',
$table->getTableName(),
$this->buildJoinClause($conn_r),
$this->buildWhereClause($conn_r),
$this->buildOrderClause($conn_r),
$this->buildLimitClause($conn_r));
$documents = $table->loadAllFromArray($rows);
if ($documents) {
$ancestor_slugs = array();
foreach ($documents as $key => $document) {
$document_slug = $document->getSlug();
foreach (PhabricatorSlug::getAncestry($document_slug) as $ancestor) {
$ancestor_slugs[$ancestor][] = $key;
}
}
if ($ancestor_slugs) {
$ancestors = queryfx_all(
$conn_r,
'SELECT * FROM %T WHERE slug IN (%Ls)',
$document->getTableName(),
array_keys($ancestor_slugs));
$ancestors = $table->loadAllFromArray($ancestors);
$ancestors = mpull($ancestors, null, 'getSlug');
foreach ($ancestor_slugs as $ancestor_slug => $document_keys) {
$ancestor = idx($ancestors, $ancestor_slug);
foreach ($document_keys as $document_key) {
$documents[$document_key]->attachAncestor(
$ancestor_slug,
$ancestor);
}
}
}
}
return $documents;
}
protected function willFilterPage(array $documents) {
// To view a Phriction document, you must also be able to view all of the
// ancestor documents. Filter out documents which have ancestors that are
// not visible.
$document_map = array();
foreach ($documents as $document) {
$document_map[$document->getSlug()] = $document;
foreach ($document->getAncestors() as $key => $ancestor) {
if ($ancestor) {
$document_map[$key] = $ancestor;
}
}
}
$filtered_map = $this->applyPolicyFilter(
$document_map,
array(PhabricatorPolicyCapability::CAN_VIEW));
// Filter all of the documents where a parent is not visible.
foreach ($documents as $document_key => $document) {
// If the document itself is not visible, filter it.
if (!isset($filtered_map[$document->getSlug()])) {
$this->didRejectResult($documents[$document_key]);
unset($documents[$document_key]);
continue;
}
// If an ancestor exists but is not visible, filter the document.
foreach ($document->getAncestors() as $ancestor_key => $ancestor) {
if (!$ancestor) {
continue;
}
if (!isset($filtered_map[$ancestor_key])) {
$this->didRejectResult($documents[$document_key]);
unset($documents[$document_key]);
break;
}
}
}
if (!$documents) {
return $documents;
}
if ($this->needContent) {
$contents = id(new PhrictionContent())->loadAllWhere(
'id IN (%Ld)',
mpull($documents, 'getContentID'));
foreach ($documents as $key => $document) {
$content_id = $document->getContentID();
if (empty($contents[$content_id])) {
unset($documents[$key]);
continue;
}
$document->attachContent($contents[$content_id]);
}
}
return $documents;
}
protected function buildJoinClause(AphrontDatabaseConnection $conn) {
$join = '';
if ($this->getOrderVector()->containsKey('updated')) {
$content_dao = new PhrictionContent();
$join = qsprintf(
$conn,
'JOIN %T c ON d.contentID = c.id',
$content_dao->getTableName());
}
return $join;
}
protected function buildWhereClause(AphrontDatabaseConnection $conn) {
$where = array();
if ($this->ids) {
$where[] = qsprintf(
$conn,
'd.id IN (%Ld)',
$this->ids);
}
if ($this->phids) {
$where[] = qsprintf(
$conn,
'd.phid IN (%Ls)',
$this->phids);
}
if ($this->slugs) {
$where[] = qsprintf(
$conn,
'd.slug IN (%Ls)',
$this->slugs);
}
if ($this->statuses) {
$where[] = qsprintf(
$conn,
'd.status IN (%Ld)',
$this->statuses);
}
if ($this->slugPrefix) {
$where[] = qsprintf(
$conn,
'd.slug LIKE %>',
$this->slugPrefix);
}
if ($this->depths) {
$where[] = qsprintf(
$conn,
'd.depth IN (%Ld)',
$this->depths);
}
switch ($this->status) {
case self::STATUS_OPEN:
$where[] = qsprintf(
$conn,
'd.status NOT IN (%Ld)',
array(
PhrictionDocumentStatus::STATUS_DELETED,
PhrictionDocumentStatus::STATUS_MOVED,
PhrictionDocumentStatus::STATUS_STUB,
));
break;
case self::STATUS_NONSTUB:
$where[] = qsprintf(
$conn,
'd.status NOT IN (%Ld)',
array(
PhrictionDocumentStatus::STATUS_MOVED,
PhrictionDocumentStatus::STATUS_STUB,
));
break;
case self::STATUS_ANY:
break;
default:
- throw new Exception("Unknown status '{$this->status}'!");
+ throw new Exception(pht("Unknown status '%s'!", $this->status));
}
$where[] = $this->buildPagingClause($conn);
return $this->formatWhereClause($where);
}
public function getOrderableColumns() {
return parent::getOrderableColumns() + array(
'depth' => array(
'table' => 'd',
'column' => 'depth',
'reverse' => true,
'type' => 'int',
),
'title' => array(
'table' => 'c',
'column' => 'title',
'reverse' => true,
'type' => 'string',
),
'updated' => array(
'table' => 'd',
'column' => 'contentID',
'type' => 'int',
'unique' => true,
),
);
}
protected function getPagingValueMap($cursor, array $keys) {
$document = $this->loadCursorObject($cursor);
$map = array(
'id' => $document->getID(),
'depth' => $document->getDepth(),
'updated' => $document->getContentID(),
);
foreach ($keys as $key) {
switch ($key) {
case 'title':
$map[$key] = $document->getContent()->getTitle();
break;
}
}
return $map;
}
protected function willExecuteCursorQuery(
PhabricatorCursorPagedPolicyAwareQuery $query) {
$vector = $this->getOrderVector();
if ($vector->containsKey('title')) {
$query->needContent(true);
}
}
public function getQueryApplicationClass() {
return 'PhabricatorPhrictionApplication';
}
}
diff --git a/src/applications/phriction/storage/PhrictionDocument.php b/src/applications/phriction/storage/PhrictionDocument.php
index 34b3bf6ec..f274d87ad 100644
--- a/src/applications/phriction/storage/PhrictionDocument.php
+++ b/src/applications/phriction/storage/PhrictionDocument.php
@@ -1,255 +1,255 @@
<?php
final class PhrictionDocument extends PhrictionDAO
implements
PhabricatorPolicyInterface,
PhabricatorSubscribableInterface,
PhabricatorFlaggableInterface,
PhabricatorTokenReceiverInterface,
PhabricatorDestructibleInterface,
PhabricatorApplicationTransactionInterface {
protected $slug;
protected $depth;
protected $contentID;
protected $status;
protected $mailKey;
protected $viewPolicy;
protected $editPolicy;
private $contentObject = self::ATTACHABLE;
private $ancestors = array();
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_TIMESTAMPS => false,
self::CONFIG_COLUMN_SCHEMA => array(
'slug' => 'sort128',
'depth' => 'uint32',
'contentID' => 'id?',
'status' => 'uint32',
'mailKey' => 'bytes20',
),
self::CONFIG_KEY_SCHEMA => array(
'key_phid' => null,
'phid' => array(
'columns' => array('phid'),
'unique' => true,
),
'slug' => array(
'columns' => array('slug'),
'unique' => true,
),
'depth' => array(
'columns' => array('depth', 'slug'),
'unique' => true,
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhrictionDocumentPHIDType::TYPECONST);
}
public static function initializeNewDocument(PhabricatorUser $actor, $slug) {
$document = new PhrictionDocument();
$document->setSlug($slug);
$content = new PhrictionContent();
$content->setSlug($slug);
$default_title = PhabricatorSlug::getDefaultTitle($slug);
$content->setTitle($default_title);
$document->attachContent($content);
$parent_doc = null;
$ancestral_slugs = PhabricatorSlug::getAncestry($slug);
if ($ancestral_slugs) {
$parent = end($ancestral_slugs);
$parent_doc = id(new PhrictionDocumentQuery())
->setViewer($actor)
->withSlugs(array($parent))
->executeOne();
}
if ($parent_doc) {
$document->setViewPolicy($parent_doc->getViewPolicy());
$document->setEditPolicy($parent_doc->getEditPolicy());
} else {
$default_view_policy = PhabricatorPolicies::getMostOpenPolicy();
$document->setViewPolicy($default_view_policy);
$document->setEditPolicy(PhabricatorPolicies::POLICY_USER);
}
return $document;
}
public function save() {
if (!$this->getMailKey()) {
$this->setMailKey(Filesystem::readRandomCharacters(20));
}
return parent::save();
}
public static function getSlugURI($slug, $type = 'document') {
static $types = array(
'document' => '/w/',
'history' => '/phriction/history/',
);
if (empty($types[$type])) {
- throw new Exception("Unknown URI type '{$type}'!");
+ throw new Exception(pht("Unknown URI type '%s'!", $type));
}
$prefix = $types[$type];
if ($slug == '/') {
return $prefix;
} else {
// NOTE: The effect here is to escape non-latin characters, since modern
// browsers deal with escaped UTF8 characters in a reasonable way (showing
// the user a readable URI) but older programs may not.
$slug = phutil_escape_uri($slug);
return $prefix.$slug;
}
}
public function setSlug($slug) {
$this->slug = PhabricatorSlug::normalize($slug);
$this->depth = PhabricatorSlug::getDepth($slug);
return $this;
}
public function attachContent(PhrictionContent $content) {
$this->contentObject = $content;
return $this;
}
public function getContent() {
return $this->assertAttached($this->contentObject);
}
public function getAncestors() {
return $this->ancestors;
}
public function getAncestor($slug) {
return $this->assertAttachedKey($this->ancestors, $slug);
}
public function attachAncestor($slug, $ancestor) {
$this->ancestors[$slug] = $ancestor;
return $this;
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
return $this->getViewPolicy();
case PhabricatorPolicyCapability::CAN_EDIT:
return $this->getEditPolicy();
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $user) {
return false;
}
public function describeAutomaticCapability($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
return pht(
'To view a wiki document, you must also be able to view all '.
'of its parents.');
case PhabricatorPolicyCapability::CAN_EDIT:
return pht(
'To edit a wiki document, you must also be able to view all '.
'of its parents.');
}
return null;
}
/* -( PhabricatorSubscribableInterface )----------------------------------- */
public function isAutomaticallySubscribed($phid) {
return false;
}
public function shouldShowSubscribersProperty() {
return true;
}
public function shouldAllowSubscription($phid) {
return true;
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new PhrictionTransactionEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new PhrictionTransaction();
}
public function willRenderTimeline(
PhabricatorApplicationTransactionView $timeline,
AphrontRequest $request) {
return $timeline;
}
/* -( PhabricatorTokenReceiverInterface )---------------------------------- */
public function getUsersToNotifyOfTokenGiven() {
return PhabricatorSubscribersQuery::loadSubscribersForPHID($this->phid);
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$this->openTransaction();
$this->delete();
$contents = id(new PhrictionContent())->loadAllWhere(
'documentID = %d',
$this->getID());
foreach ($contents as $content) {
$content->delete();
}
$this->saveTransaction();
}
}
diff --git a/src/applications/policy/__tests__/PhabricatorPolicyTestCase.php b/src/applications/policy/__tests__/PhabricatorPolicyTestCase.php
index dca771156..327c8a285 100644
--- a/src/applications/policy/__tests__/PhabricatorPolicyTestCase.php
+++ b/src/applications/policy/__tests__/PhabricatorPolicyTestCase.php
@@ -1,335 +1,338 @@
<?php
final class PhabricatorPolicyTestCase extends PhabricatorTestCase {
/**
* Verify that any user can view an object with POLICY_PUBLIC.
*/
public function testPublicPolicyEnabled() {
$env = PhabricatorEnv::beginScopedEnv();
$env->overrideEnvConfig('policy.allow-public', true);
$this->expectVisibility(
$this->buildObject(PhabricatorPolicies::POLICY_PUBLIC),
array(
'public' => true,
'user' => true,
'admin' => true,
),
- 'Public Policy (Enabled in Config)');
+ pht('Public Policy (Enabled in Config)'));
}
/**
* Verify that POLICY_PUBLIC is interpreted as POLICY_USER when public
* policies are disallowed.
*/
public function testPublicPolicyDisabled() {
$env = PhabricatorEnv::beginScopedEnv();
$env->overrideEnvConfig('policy.allow-public', false);
$this->expectVisibility(
$this->buildObject(PhabricatorPolicies::POLICY_PUBLIC),
array(
'public' => false,
'user' => true,
'admin' => true,
),
- 'Public Policy (Disabled in Config)');
+ pht('Public Policy (Disabled in Config)'));
}
/**
* Verify that any logged-in user can view an object with POLICY_USER, but
* logged-out users can not.
*/
public function testUsersPolicy() {
$this->expectVisibility(
$this->buildObject(PhabricatorPolicies::POLICY_USER),
array(
'public' => false,
'user' => true,
'admin' => true,
),
- 'User Policy');
+ pht('User Policy'));
}
/**
* Verify that only administrators can view an object with POLICY_ADMIN.
*/
public function testAdminPolicy() {
$this->expectVisibility(
$this->buildObject(PhabricatorPolicies::POLICY_ADMIN),
array(
'public' => false,
'user' => false,
'admin' => true,
),
- 'Admin Policy');
+ pht('Admin Policy'));
}
/**
* Verify that no one can view an object with POLICY_NOONE.
*/
public function testNoOnePolicy() {
$this->expectVisibility(
$this->buildObject(PhabricatorPolicies::POLICY_NOONE),
array(
'public' => false,
'user' => false,
'admin' => false,
),
- 'No One Policy');
+ pht('No One Policy'));
}
/**
* Test offset-based filtering.
*/
public function testOffsets() {
$results = array(
$this->buildObject(PhabricatorPolicies::POLICY_NOONE),
$this->buildObject(PhabricatorPolicies::POLICY_NOONE),
$this->buildObject(PhabricatorPolicies::POLICY_NOONE),
$this->buildObject(PhabricatorPolicies::POLICY_USER),
$this->buildObject(PhabricatorPolicies::POLICY_USER),
$this->buildObject(PhabricatorPolicies::POLICY_USER),
);
$query = new PhabricatorPolicyAwareTestQuery();
$query->setResults($results);
$query->setViewer($this->buildUser('user'));
$this->assertEqual(
3,
count($query->setLimit(3)->setOffset(0)->execute()),
- 'Invisible objects are ignored.');
+ pht('Invisible objects are ignored.'));
$this->assertEqual(
0,
count($query->setLimit(3)->setOffset(3)->execute()),
- 'Offset pages through visible objects only.');
+ pht('Offset pages through visible objects only.'));
$this->assertEqual(
2,
count($query->setLimit(3)->setOffset(1)->execute()),
- 'Offsets work correctly.');
+ pht('Offsets work correctly.'));
$this->assertEqual(
2,
count($query->setLimit(0)->setOffset(1)->execute()),
- 'Offset with no limit works.');
+ pht('Offset with no limit works.'));
}
/**
* Test limits.
*/
public function testLimits() {
$results = array(
$this->buildObject(PhabricatorPolicies::POLICY_USER),
$this->buildObject(PhabricatorPolicies::POLICY_USER),
$this->buildObject(PhabricatorPolicies::POLICY_USER),
$this->buildObject(PhabricatorPolicies::POLICY_USER),
$this->buildObject(PhabricatorPolicies::POLICY_USER),
$this->buildObject(PhabricatorPolicies::POLICY_USER),
);
$query = new PhabricatorPolicyAwareTestQuery();
$query->setResults($results);
$query->setViewer($this->buildUser('user'));
$this->assertEqual(
3,
count($query->setLimit(3)->setOffset(0)->execute()),
- 'Limits work.');
+ pht('Limits work.'));
$this->assertEqual(
2,
count($query->setLimit(3)->setOffset(4)->execute()),
- 'Limit + offset work.');
+ pht('Limit + offset work.'));
}
/**
* Test that omnipotent users bypass policies.
*/
public function testOmnipotence() {
$results = array(
$this->buildObject(PhabricatorPolicies::POLICY_NOONE),
);
$query = new PhabricatorPolicyAwareTestQuery();
$query->setResults($results);
$query->setViewer(PhabricatorUser::getOmnipotentUser());
$this->assertEqual(
1,
count($query->execute()));
}
/**
* Test that invalid policies reject viewers of all types.
*/
public function testRejectInvalidPolicy() {
$invalid_policy = 'the duck goes quack';
$object = $this->buildObject($invalid_policy);
$this->expectVisibility(
$object = $this->buildObject($invalid_policy),
array(
'public' => false,
'user' => false,
'admin' => false,
),
- 'Invalid Policy');
+ pht('Invalid Policy'));
}
/**
* An omnipotent user should be able to see even objects with invalid
* policies.
*/
public function testInvalidPolicyVisibleByOmnipotentUser() {
$invalid_policy = 'the cow goes moo';
$object = $this->buildObject($invalid_policy);
$results = array(
$object,
);
$query = new PhabricatorPolicyAwareTestQuery();
$query->setResults($results);
$query->setViewer(PhabricatorUser::getOmnipotentUser());
$this->assertEqual(
1,
count($query->execute()));
}
public function testAllQueriesBelongToActualApplications() {
$queries = id(new PhutilSymbolLoader())
->setAncestorClass('PhabricatorPolicyAwareQuery')
->loadObjects();
foreach ($queries as $qclass => $query) {
$class = $query->getQueryApplicationClass();
if (!$class) {
continue;
}
$this->assertTrue(
(bool)PhabricatorApplication::getByClass($class),
- "Application class '{$class}' for query '{$qclass}'");
+ pht(
+ "Application class '%s' for query '%s'.",
+ $class,
+ $qclass));
}
}
public function testMultipleCapabilities() {
$object = new PhabricatorPolicyTestObject();
$object->setCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
));
$object->setPolicies(
array(
PhabricatorPolicyCapability::CAN_VIEW
=> PhabricatorPolicies::POLICY_USER,
PhabricatorPolicyCapability::CAN_EDIT
=> PhabricatorPolicies::POLICY_NOONE,
));
$filter = new PhabricatorPolicyFilter();
$filter->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
));
$filter->setViewer($this->buildUser('user'));
$result = $filter->apply(array($object));
$this->assertEqual(array(), $result);
}
/**
* Test an object for visibility across multiple user specifications.
*/
private function expectVisibility(
PhabricatorPolicyTestObject $object,
array $map,
$description) {
foreach ($map as $spec => $expect) {
$viewer = $this->buildUser($spec);
$query = new PhabricatorPolicyAwareTestQuery();
$query->setResults(array($object));
$query->setViewer($viewer);
$caught = null;
try {
$result = $query->executeOne();
} catch (PhabricatorPolicyException $ex) {
$caught = $ex;
}
if ($expect) {
$this->assertEqual(
$object,
$result,
- "{$description} with user {$spec} should succeed.");
+ pht('%s with user %s should succeed.', $description, $spec));
} else {
$this->assertTrue(
$caught instanceof PhabricatorPolicyException,
- "{$description} with user {$spec} should fail.");
+ pht('%s with user %s should fail.', $description, $spec));
}
}
}
/**
* Build a test object to spec.
*/
private function buildObject($policy) {
$object = new PhabricatorPolicyTestObject();
$object->setCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
));
$object->setPolicies(
array(
PhabricatorPolicyCapability::CAN_VIEW => $policy,
));
return $object;
}
/**
* Build a test user to spec.
*/
private function buildUser($spec) {
$user = new PhabricatorUser();
switch ($spec) {
case 'public':
break;
case 'user':
$user->setPHID(1);
break;
case 'admin':
$user->setPHID(1);
$user->setIsAdmin(true);
break;
default:
- throw new Exception("Unknown user spec '{$spec}'.");
+ throw new Exception(pht("Unknown user spec '%s'.", $spec));
}
return $user;
}
}
diff --git a/src/applications/policy/capability/PhabricatorPolicyCapability.php b/src/applications/policy/capability/PhabricatorPolicyCapability.php
index 346a6fb4e..a9af1fa00 100644
--- a/src/applications/policy/capability/PhabricatorPolicyCapability.php
+++ b/src/applications/policy/capability/PhabricatorPolicyCapability.php
@@ -1,90 +1,94 @@
<?php
abstract class PhabricatorPolicyCapability extends Phobject {
const CAN_VIEW = 'view';
const CAN_EDIT = 'edit';
const CAN_JOIN = 'join';
/**
* Get the unique key identifying this capability. This key must be globally
* unique. Application capabilities should be namespaced. For example:
*
* application.create
*
* @return string Globally unique capability key.
*/
final public function getCapabilityKey() {
$class = new ReflectionClass($this);
$const = $class->getConstant('CAPABILITY');
if ($const === false) {
throw new Exception(
pht(
- 'PolicyCapability class "%s" must define an CAPABILITY property.',
- get_class($this)));
+ '%s class "%s" must define a %s property.',
+ __CLASS__,
+ get_class($this),
+ 'CAPABILITY'));
}
if (!is_string($const)) {
throw new Exception(
pht(
- 'PolicyCapability class "%s" has an invalid CAPABILITY '.
- 'property. Capability constants must be a string.',
- get_class($this)));
+ '%s class "%s" has an invalid %s property. '.
+ 'Capability constants must be a string.',
+ __CLASS__,
+ get_class($this),
+ 'CAPABILITY'));
}
return $const;
}
/**
* Return a human-readable descriptive name for this capability, like
* "Can View".
*
* @return string Human-readable name describing the capability.
*/
abstract public function getCapabilityName();
/**
* Return a human-readable string describing what not having this capability
* prevents the user from doing. For example:
*
* - You do not have permission to edit this object.
* - You do not have permission to create new tasks.
*
* @return string Human-readable name describing what failing a check for this
* capability prevents the user from doing.
*/
public function describeCapabilityRejection() {
return null;
}
/**
* Can this capability be set to "public"? Broadly, this is only appropriate
* for view and view-related policies.
*
* @return bool True to allow the "public" policy. Returns false by default.
*/
public function shouldAllowPublicPolicySetting() {
return false;
}
final public static function getCapabilityByKey($key) {
return idx(self::getCapabilityMap(), $key);
}
final public static function getCapabilityMap() {
static $map;
if ($map === null) {
$capabilities = id(new PhutilSymbolLoader())
->setAncestorClass(__CLASS__)
->loadObjects();
$map = mpull($capabilities, null, 'getCapabilityKey');
}
return $map;
}
}
diff --git a/src/applications/policy/config/PhabricatorPolicyConfigOptions.php b/src/applications/policy/config/PhabricatorPolicyConfigOptions.php
index 2b47ae8d8..1c3698c4c 100644
--- a/src/applications/policy/config/PhabricatorPolicyConfigOptions.php
+++ b/src/applications/policy/config/PhabricatorPolicyConfigOptions.php
@@ -1,71 +1,72 @@
<?php
final class PhabricatorPolicyConfigOptions
extends PhabricatorApplicationConfigOptions {
public function getName() {
return pht('Policy');
}
public function getDescription() {
return pht('Options relating to object visibility.');
}
public function getFontIcon() {
return 'fa-lock';
}
public function getGroup() {
return 'apps';
}
public function getOptions() {
$policy_locked_type = 'custom:PolicyLockOptionType';
$policy_locked_example = array(
'people.create.users' => 'admin',
);
$json = new PhutilJSON();
$policy_locked_example = $json->encodeFormatted($policy_locked_example);
return array(
$this->newOption('policy.allow-public', 'bool', false)
->setBoolOptions(
array(
pht('Allow Public Visibility'),
pht('Require Login'),
))
->setSummary(pht('Allow users to set object visibility to public.'))
->setDescription(
pht(
"Phabricator allows you to set the visibility of objects (like ".
"repositories and tasks) to 'Public', which means **anyone ".
"on the internet can see them, without needing to log in or ".
"have an account**.".
"\n\n".
"This is intended for open source projects. Many installs will ".
"never want to make anything public, so this policy is disabled ".
"by default. You can enable it here, which will let you set the ".
"policy for objects to 'Public'.".
"\n\n".
"Enabling this setting will immediately open up some features, ".
"like the user directory. Anyone on the internet will be able to ".
"access these features.".
"\n\n".
"With this setting disabled, the 'Public' policy is not ".
"available, and the most open policy is 'All Users' (which means ".
"users must have accounts and be logged in to view things).")),
$this->newOption('policy.locked', $policy_locked_type, array())
->setLocked(true)
->setSummary(pht(
'Lock specific application policies so they can not be edited.'))
->setDescription(pht(
'Phabricator has application policies which can dictate whether '.
'users can take certain actions, such as creating new users. '."\n\n".
'This setting allows for "locking" these policies such that no '.
'further edits can be made on a per-policy basis.'))
- ->addExample($policy_locked_example,
- pht('Lock Create User Policy To Admins')),
+ ->addExample(
+ $policy_locked_example,
+ pht('Lock Create User Policy To Admins')),
);
}
}
diff --git a/src/applications/policy/config/PolicyLockOptionType.php b/src/applications/policy/config/PolicyLockOptionType.php
index 3198a927c..e9a51d706 100644
--- a/src/applications/policy/config/PolicyLockOptionType.php
+++ b/src/applications/policy/config/PolicyLockOptionType.php
@@ -1,63 +1,68 @@
<?php
final class PolicyLockOptionType
extends PhabricatorConfigJSONOptionType {
public function validateOption(PhabricatorConfigOption $option, $value) {
$capabilities = id(new PhutilSymbolLoader())
->setAncestorClass('PhabricatorPolicyCapability')
->loadObjects();
$capabilities = mpull($capabilities, null, 'getCapabilityKey');
$policy_phids = array();
foreach ($value as $capability_key => $policy) {
$capability = idx($capabilities, $capability_key);
if (!$capability) {
- throw new Exception(pht(
- 'Capability "%s" does not exist.', $capability_key));
+ throw new Exception(
+ pht(
+ 'Capability "%s" does not exist.',
+ $capability_key));
}
if (phid_get_type($policy) !=
PhabricatorPHIDConstants::PHID_TYPE_UNKNOWN) {
$policy_phids[$policy] = $policy;
} else {
try {
$policy_object = PhabricatorPolicyQuery::getGlobalPolicy($policy);
// this exception is not helpful here as its about global policy;
// throw a better exception
} catch (Exception $ex) {
- throw new Exception(pht(
- 'Capability "%s" has invalid policy "%s".',
- $capability_key,
- $policy));
+ throw new Exception(
+ pht(
+ 'Capability "%s" has invalid policy "%s".',
+ $capability_key,
+ $policy));
}
}
if ($policy == PhabricatorPolicies::POLICY_PUBLIC) {
if (!$capability->shouldAllowPublicPolicySetting()) {
- throw new Exception(pht(
- 'Capability "%s" does not support public policy.',
- $capability_key));
+ throw new Exception(
+ pht(
+ 'Capability "%s" does not support public policy.',
+ $capability_key));
}
}
}
if ($policy_phids) {
$handles = id(new PhabricatorHandleQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withPhids($policy_phids)
->execute();
$handles = mpull($handles, null, 'getPHID');
foreach ($value as $capability_key => $policy) {
$handle = $handles[$policy];
if (!$handle->isComplete()) {
- throw new Exception(pht(
- 'Capability "%s" has invalid policy "%s"; "%s" does not exist.',
- $capability_key,
- $policy,
- $policy));
+ throw new Exception(
+ pht(
+ 'Capability "%s" has invalid policy "%s"; "%s" does not exist.',
+ $capability_key,
+ $policy,
+ $policy));
}
}
}
}
}
diff --git a/src/applications/policy/controller/PhabricatorPolicyEditController.php b/src/applications/policy/controller/PhabricatorPolicyEditController.php
index 4c044a6dc..80b04e4ea 100644
--- a/src/applications/policy/controller/PhabricatorPolicyEditController.php
+++ b/src/applications/policy/controller/PhabricatorPolicyEditController.php
@@ -1,225 +1,224 @@
<?php
final class PhabricatorPolicyEditController
extends PhabricatorPolicyController {
public function handleRequest(AphrontRequest $request) {
$request = $this->getRequest();
$viewer = $request->getUser();
$action_options = array(
PhabricatorPolicy::ACTION_ALLOW => pht('Allow'),
PhabricatorPolicy::ACTION_DENY => pht('Deny'),
);
$rules = id(new PhutilSymbolLoader())
->setAncestorClass('PhabricatorPolicyRule')
->loadObjects();
$rules = msort($rules, 'getRuleOrder');
$default_rule = array(
'action' => head_key($action_options),
'rule' => head_key($rules),
'value' => null,
);
$phid = $request->getURIData('phid');
if ($phid) {
$policies = id(new PhabricatorPolicyQuery())
->setViewer($viewer)
->withPHIDs(array($phid))
->execute();
if (!$policies) {
return new Aphront404Response();
}
$policy = head($policies);
} else {
$policy = id(new PhabricatorPolicy())
->setRules(array($default_rule))
->setDefaultAction(PhabricatorPolicy::ACTION_DENY);
}
$root_id = celerity_generate_unique_node_id();
$default_action = $policy->getDefaultAction();
$rule_data = $policy->getRules();
$errors = array();
if ($request->isFormPost()) {
$data = $request->getStr('rules');
try {
$data = phutil_json_decode($data);
} catch (PhutilJSONParserException $ex) {
throw new PhutilProxyException(
pht('Failed to JSON decode rule data!'),
$ex);
}
$rule_data = array();
foreach ($data as $rule) {
$action = idx($rule, 'action');
switch ($action) {
case 'allow':
case 'deny':
break;
default:
- throw new Exception("Invalid action '{$action}'!");
+ throw new Exception(pht("Invalid action '%s'!", $action));
}
$rule_class = idx($rule, 'rule');
if (empty($rules[$rule_class])) {
- throw new Exception("Invalid rule class '{$rule_class}'!");
+ throw new Exception(pht("Invalid rule class '%s'!", $rule_class));
}
$rule_obj = $rules[$rule_class];
$value = $rule_obj->getValueForStorage(idx($rule, 'value'));
$rule_data[] = array(
'action' => $action,
'rule' => $rule_class,
'value' => $value,
);
}
// Filter out nonsense rules, like a "users" rule without any users
// actually specified.
$valid_rules = array();
foreach ($rule_data as $rule) {
$rule_class = $rule['rule'];
if ($rules[$rule_class]->ruleHasEffect($rule['value'])) {
$valid_rules[] = $rule;
}
}
if (!$valid_rules) {
$errors[] = pht('None of these policy rules have any effect.');
}
// NOTE: Policies are immutable once created, and we always create a new
// policy here. If we didn't, we would need to lock this endpoint down,
// as users could otherwise just go edit the policies of objects with
// custom policies.
if (!$errors) {
$new_policy = new PhabricatorPolicy();
$new_policy->setRules($valid_rules);
$new_policy->setDefaultAction($request->getStr('default'));
$new_policy->save();
$data = array(
'phid' => $new_policy->getPHID(),
'info' => array(
'name' => $new_policy->getName(),
'full' => $new_policy->getName(),
'icon' => $new_policy->getIcon(),
),
);
return id(new AphrontAjaxResponse())->setContent($data);
}
}
// Convert rule values to display format (for example, expanding PHIDs
// into tokens).
foreach ($rule_data as $key => $rule) {
$rule_data[$key]['value'] = $rules[$rule['rule']]->getValueForDisplay(
$viewer,
$rule['value']);
}
$default_select = AphrontFormSelectControl::renderSelectTag(
$default_action,
$action_options,
array(
'name' => 'default',
));
if ($errors) {
$errors = id(new PHUIInfoView())
->setErrors($errors);
}
$form = id(new PHUIFormLayoutView())
->appendChild($errors)
->appendChild(
javelin_tag(
'input',
array(
'type' => 'hidden',
'name' => 'rules',
'sigil' => 'rules',
)))
->appendChild(
id(new PHUIFormInsetView())
->setTitle(pht('Rules'))
->setRightButton(
javelin_tag(
'a',
array(
'href' => '#',
'class' => 'button green',
'sigil' => 'create-rule',
'mustcapture' => true,
),
pht('New Rule')))
- ->setDescription(
- pht('These rules are processed in order.'))
+ ->setDescription(pht('These rules are processed in order.'))
->setContent(javelin_tag(
'table',
array(
'sigil' => 'rules',
'class' => 'policy-rules-table',
),
'')))
->appendChild(
id(new AphrontFormMarkupControl())
->setLabel(pht('If No Rules Match'))
->setValue(pht(
'%s all other users.',
$default_select)));
$form = phutil_tag(
'div',
array(
'id' => $root_id,
),
$form);
$rule_options = mpull($rules, 'getRuleDescription');
$type_map = mpull($rules, 'getValueControlType');
$templates = mpull($rules, 'getValueControlTemplate');
require_celerity_resource('policy-edit-css');
Javelin::initBehavior(
'policy-rule-editor',
array(
'rootID' => $root_id,
'actions' => $action_options,
'rules' => $rule_options,
'types' => $type_map,
'templates' => $templates,
'data' => $rule_data,
'defaultRule' => $default_rule,
));
$title = pht('Custom Policy');
$key = $request->getStr('capability');
if ($key) {
$capability = PhabricatorPolicyCapability::getCapabilityByKey($key);
$title = pht('Custom "%s" Policy', $capability->getCapabilityName());
}
$dialog = id(new AphrontDialogView())
->setWidth(AphrontDialogView::WIDTH_FULL)
->setUser($viewer)
->setTitle($title)
->appendChild($form)
->addSubmitButton(pht('Save Policy'))
->addCancelButton('#');
return id(new AphrontDialogResponse())->setDialog($dialog);
}
}
diff --git a/src/applications/policy/filter/PhabricatorPolicyFilter.php b/src/applications/policy/filter/PhabricatorPolicyFilter.php
index 7373991fd..8d52ddc04 100644
--- a/src/applications/policy/filter/PhabricatorPolicyFilter.php
+++ b/src/applications/policy/filter/PhabricatorPolicyFilter.php
@@ -1,458 +1,461 @@
<?php
final class PhabricatorPolicyFilter {
private $viewer;
private $objects;
private $capabilities;
private $raisePolicyExceptions;
private $userProjects;
private $customPolicies = array();
private $forcedPolicy;
public static function mustRetainCapability(
PhabricatorUser $user,
PhabricatorPolicyInterface $object,
$capability) {
if (!self::hasCapability($user, $object, $capability)) {
throw new Exception(
- "You can not make that edit, because it would remove your ability ".
- "to '{$capability}' the object.");
+ pht(
+ "You can not make that edit, because it would remove your ability ".
+ "to '%s' the object.",
+ $capability));
}
}
public static function requireCapability(
PhabricatorUser $user,
PhabricatorPolicyInterface $object,
$capability) {
$filter = id(new PhabricatorPolicyFilter())
->setViewer($user)
->requireCapabilities(array($capability))
->raisePolicyExceptions(true)
->apply(array($object));
}
/**
* Perform a capability check, acting as though an object had a specific
* policy. This is primarily used to check if a policy is valid (for example,
* to prevent users from editing away their ability to edit an object).
*
* Specifically, a check like this:
*
* PhabricatorPolicyFilter::requireCapabilityWithForcedPolicy(
* $viewer,
* $object,
* PhabricatorPolicyCapability::CAN_EDIT,
* $potential_new_policy);
*
* ...will throw a @{class:PhabricatorPolicyException} if the new policy would
* remove the user's ability to edit the object.
*
* @param PhabricatorUser The viewer to perform a policy check for.
* @param PhabricatorPolicyInterface The object to perform a policy check on.
* @param string Capability to test.
* @param string Perform the test as though the object has this
* policy instead of the policy it actually has.
* @return void
*/
public static function requireCapabilityWithForcedPolicy(
PhabricatorUser $viewer,
PhabricatorPolicyInterface $object,
$capability,
$forced_policy) {
id(new PhabricatorPolicyFilter())
->setViewer($viewer)
->requireCapabilities(array($capability))
->raisePolicyExceptions(true)
->forcePolicy($forced_policy)
->apply(array($object));
}
public static function hasCapability(
PhabricatorUser $user,
PhabricatorPolicyInterface $object,
$capability) {
$filter = new PhabricatorPolicyFilter();
$filter->setViewer($user);
$filter->requireCapabilities(array($capability));
$result = $filter->apply(array($object));
return (count($result) == 1);
}
public function setViewer(PhabricatorUser $user) {
$this->viewer = $user;
return $this;
}
public function requireCapabilities(array $capabilities) {
$this->capabilities = $capabilities;
return $this;
}
public function raisePolicyExceptions($raise) {
$this->raisePolicyExceptions = $raise;
return $this;
}
public function forcePolicy($forced_policy) {
$this->forcedPolicy = $forced_policy;
return $this;
}
public function apply(array $objects) {
assert_instances_of($objects, 'PhabricatorPolicyInterface');
$viewer = $this->viewer;
$capabilities = $this->capabilities;
if (!$viewer || !$capabilities) {
- throw new Exception(
- 'Call setViewer() and requireCapabilities() before apply()!');
+ throw new PhutilInvalidStateException('setViewer', 'requireCapabilities');
}
// If the viewer is omnipotent, short circuit all the checks and just
// return the input unmodified. This is an optimization; we know the
// result already.
if ($viewer->isOmnipotent()) {
return $objects;
}
$filtered = array();
$viewer_phid = $viewer->getPHID();
if (empty($this->userProjects[$viewer_phid])) {
$this->userProjects[$viewer_phid] = array();
}
$need_projects = array();
$need_policies = array();
foreach ($objects as $key => $object) {
$object_capabilities = $object->getCapabilities();
foreach ($capabilities as $capability) {
if (!in_array($capability, $object_capabilities)) {
throw new Exception(
- "Testing for capability '{$capability}' on an object which does ".
- "not have that capability!");
+ pht(
+ "Testing for capability '%s' on an object which does ".
+ "not have that capability!",
+ $capability));
}
$policy = $this->getObjectPolicy($object, $capability);
$type = phid_get_type($policy);
if ($type == PhabricatorProjectProjectPHIDType::TYPECONST) {
$need_projects[$policy] = $policy;
}
if ($type == PhabricatorPolicyPHIDTypePolicy::TYPECONST) {
$need_policies[$policy] = $policy;
}
}
}
if ($need_policies) {
$this->loadCustomPolicies(array_keys($need_policies));
}
// If we need projects, check if any of the projects we need are also the
// objects we're filtering. Because of how project rules work, this is a
// common case.
if ($need_projects) {
foreach ($objects as $object) {
if ($object instanceof PhabricatorProject) {
$project_phid = $object->getPHID();
if (isset($need_projects[$project_phid])) {
$is_member = $object->isUserMember($viewer_phid);
$this->userProjects[$viewer_phid][$project_phid] = $is_member;
unset($need_projects[$project_phid]);
}
}
}
}
if ($need_projects) {
$need_projects = array_unique($need_projects);
// NOTE: We're using the omnipotent user here to avoid a recursive
// descent into madness. We don't actually need to know if the user can
// see these projects or not, since: the check is "user is member of
// project", not "user can see project"; and membership implies
// visibility anyway. Without this, we may load other projects and
// re-enter the policy filter and generally create a huge mess.
$projects = id(new PhabricatorProjectQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withMemberPHIDs(array($viewer->getPHID()))
->withPHIDs($need_projects)
->execute();
foreach ($projects as $project) {
$this->userProjects[$viewer_phid][$project->getPHID()] = true;
}
}
foreach ($objects as $key => $object) {
$object_capabilities = $object->getCapabilities();
foreach ($capabilities as $capability) {
if (!$this->checkCapability($object, $capability)) {
// If we're missing any capability, move on to the next object.
continue 2;
}
}
// If we make it here, we have all of the required capabilities.
$filtered[$key] = $object;
}
return $filtered;
}
private function checkCapability(
PhabricatorPolicyInterface $object,
$capability) {
$policy = $this->getObjectPolicy($object, $capability);
if (!$policy) {
// TODO: Formalize this somehow?
$policy = PhabricatorPolicies::POLICY_USER;
}
if ($policy == PhabricatorPolicies::POLICY_PUBLIC) {
// If the object is set to "public" but that policy is disabled for this
// install, restrict the policy to "user".
if (!PhabricatorEnv::getEnvConfig('policy.allow-public')) {
$policy = PhabricatorPolicies::POLICY_USER;
}
// If the object is set to "public" but the capability is not a public
// capability, restrict the policy to "user".
$capobj = PhabricatorPolicyCapability::getCapabilityByKey($capability);
if (!$capobj || !$capobj->shouldAllowPublicPolicySetting()) {
$policy = PhabricatorPolicies::POLICY_USER;
}
}
$viewer = $this->viewer;
if ($viewer->isOmnipotent()) {
return true;
}
if ($object->hasAutomaticCapability($capability, $viewer)) {
return true;
}
switch ($policy) {
case PhabricatorPolicies::POLICY_PUBLIC:
return true;
case PhabricatorPolicies::POLICY_USER:
if ($viewer->getPHID()) {
return true;
} else {
$this->rejectObject($object, $policy, $capability);
}
break;
case PhabricatorPolicies::POLICY_ADMIN:
if ($viewer->getIsAdmin()) {
return true;
} else {
$this->rejectObject($object, $policy, $capability);
}
break;
case PhabricatorPolicies::POLICY_NOONE:
$this->rejectObject($object, $policy, $capability);
break;
default:
$type = phid_get_type($policy);
if ($type == PhabricatorProjectProjectPHIDType::TYPECONST) {
if (!empty($this->userProjects[$viewer->getPHID()][$policy])) {
return true;
} else {
$this->rejectObject($object, $policy, $capability);
}
} else if ($type == PhabricatorPeopleUserPHIDType::TYPECONST) {
if ($viewer->getPHID() == $policy) {
return true;
} else {
$this->rejectObject($object, $policy, $capability);
}
} else if ($type == PhabricatorPolicyPHIDTypePolicy::TYPECONST) {
if ($this->checkCustomPolicy($policy)) {
return true;
} else {
$this->rejectObject($object, $policy, $capability);
}
} else {
// Reject objects with unknown policies.
$this->rejectObject($object, false, $capability);
}
}
return false;
}
public function rejectObject(
PhabricatorPolicyInterface $object,
$policy,
$capability) {
if (!$this->raisePolicyExceptions) {
return;
}
if ($this->viewer->isOmnipotent()) {
// Never raise policy exceptions for the omnipotent viewer. Although we
// will never normally issue a policy rejection for the omnipotent
// viewer, we can end up here when queries blanket reject objects that
// have failed to load, without distinguishing between nonexistent and
// nonvisible objects.
return;
}
$capobj = PhabricatorPolicyCapability::getCapabilityByKey($capability);
$rejection = null;
if ($capobj) {
$rejection = $capobj->describeCapabilityRejection();
$capability_name = $capobj->getCapabilityName();
} else {
$capability_name = $capability;
}
if (!$rejection) {
// We couldn't find the capability object, or it doesn't provide a
// tailored rejection string.
$rejection = pht(
'You do not have the required capability ("%s") to do whatever you '.
'are trying to do.',
$capability);
}
$more = PhabricatorPolicy::getPolicyExplanation($this->viewer, $policy);
$exceptions = $object->describeAutomaticCapability($capability);
$details = array_filter(array_merge(array($more), (array)$exceptions));
// NOTE: Not every type of policy object has a real PHID; just load an
// empty handle if a real PHID isn't available.
$phid = nonempty($object->getPHID(), PhabricatorPHIDConstants::PHID_VOID);
$handle = id(new PhabricatorHandleQuery())
->setViewer($this->viewer)
->withPHIDs(array($phid))
->executeOne();
$is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business');
if ($is_serious) {
$title = pht(
'Access Denied: %s',
$handle->getObjectName());
} else {
$title = pht(
'You Shall Not Pass: %s',
$handle->getObjectName());
}
$full_message = pht(
'[%s] (%s) %s // %s',
$title,
$capability_name,
$rejection,
implode(' ', $details));
$exception = id(new PhabricatorPolicyException($full_message))
->setTitle($title)
->setRejection($rejection)
->setCapabilityName($capability_name)
->setMoreInfo($details);
throw $exception;
}
private function loadCustomPolicies(array $phids) {
$viewer = $this->viewer;
$viewer_phid = $viewer->getPHID();
$custom_policies = id(new PhabricatorPolicyQuery())
->setViewer($viewer)
->withPHIDs($phids)
->execute();
$custom_policies = mpull($custom_policies, null, 'getPHID');
$classes = array();
$values = array();
foreach ($custom_policies as $policy) {
foreach ($policy->getCustomRuleClasses() as $class) {
$classes[$class] = $class;
$values[$class][] = $policy->getCustomRuleValues($class);
}
}
foreach ($classes as $class => $ignored) {
$object = newv($class, array());
$object->willApplyRules($viewer, array_mergev($values[$class]));
$classes[$class] = $object;
}
foreach ($custom_policies as $policy) {
$policy->attachRuleObjects($classes);
}
if (empty($this->customPolicies[$viewer_phid])) {
$this->customPolicies[$viewer_phid] = array();
}
$this->customPolicies[$viewer->getPHID()] += $custom_policies;
}
private function checkCustomPolicy($policy_phid) {
$viewer = $this->viewer;
$viewer_phid = $viewer->getPHID();
$policy = idx($this->customPolicies[$viewer_phid], $policy_phid);
if (!$policy) {
// Reject, this policy is bogus.
return false;
}
$objects = $policy->getRuleObjects();
$action = null;
foreach ($policy->getRules() as $rule) {
$object = idx($objects, idx($rule, 'rule'));
if (!$object) {
// Reject, this policy has a bogus rule.
return false;
}
// If the user matches this rule, use this action.
if ($object->applyRule($viewer, idx($rule, 'value'))) {
$action = idx($rule, 'action');
break;
}
}
if ($action === null) {
$action = $policy->getDefaultAction();
}
if ($action === PhabricatorPolicy::ACTION_ALLOW) {
return true;
}
return false;
}
private function getObjectPolicy(
PhabricatorPolicyInterface $object,
$capability) {
if ($this->forcedPolicy) {
return $this->forcedPolicy;
} else {
return $object->getPolicy($capability);
}
}
}
diff --git a/src/applications/policy/management/PhabricatorPolicyManagementShowWorkflow.php b/src/applications/policy/management/PhabricatorPolicyManagementShowWorkflow.php
index e6d2741bf..1529d4904 100644
--- a/src/applications/policy/management/PhabricatorPolicyManagementShowWorkflow.php
+++ b/src/applications/policy/management/PhabricatorPolicyManagementShowWorkflow.php
@@ -1,82 +1,80 @@
<?php
final class PhabricatorPolicyManagementShowWorkflow
extends PhabricatorPolicyManagementWorkflow {
protected function didConstruct() {
$this
->setName('show')
- ->setSynopsis('Show policy information about an object.')
- ->setExamples(
- '**show** D123')
+ ->setSynopsis(pht('Show policy information about an object.'))
+ ->setExamples('**show** D123')
->setArguments(
array(
array(
'name' => 'objects',
'wildcard' => true,
),
));
}
public function execute(PhutilArgumentParser $args) {
$console = PhutilConsole::getConsole();
$viewer = $this->getViewer();
$obj_names = $args->getArg('objects');
if (!$obj_names) {
throw new PhutilArgumentUsageException(
- pht(
- 'Specify the name of an object to show policy information for.'));
+ pht('Specify the name of an object to show policy information for.'));
} else if (count($obj_names) > 1) {
throw new PhutilArgumentUsageException(
pht(
'Specify the name of exactly one object to show policy information '.
'for.'));
}
$object = id(new PhabricatorObjectQuery())
->setViewer($viewer)
->withNames($obj_names)
->executeOne();
if (!$object) {
$name = head($obj_names);
throw new PhutilArgumentUsageException(
pht(
"No such object '%s'!",
$name));
}
$handle = id(new PhabricatorHandleQuery())
->setViewer($viewer)
->withPHIDs(array($object->getPHID()))
->executeOne();
$policies = PhabricatorPolicyQuery::loadPolicies(
$viewer,
$object);
$console->writeOut("__%s__\n\n", pht('OBJECT'));
$console->writeOut(" %s\n", $handle->getFullName());
$console->writeOut("\n");
$console->writeOut("__%s__\n\n", pht('CAPABILITIES'));
foreach ($policies as $capability => $policy) {
$console->writeOut(" **%s**\n", $capability);
$console->writeOut(" %s\n", $policy->renderDescription());
$console->writeOut(" %s\n",
PhabricatorPolicy::getPolicyExplanation($viewer, $policy->getPHID()));
$console->writeOut("\n");
$more = (array)$object->describeAutomaticCapability($capability);
if ($more) {
foreach ($more as $line) {
$console->writeOut(" %s\n", $line);
}
$console->writeOut("\n");
}
}
}
}
diff --git a/src/applications/policy/management/PhabricatorPolicyManagementUnlockWorkflow.php b/src/applications/policy/management/PhabricatorPolicyManagementUnlockWorkflow.php
index 22e498452..33f7e209c 100644
--- a/src/applications/policy/management/PhabricatorPolicyManagementUnlockWorkflow.php
+++ b/src/applications/policy/management/PhabricatorPolicyManagementUnlockWorkflow.php
@@ -1,136 +1,132 @@
<?php
final class PhabricatorPolicyManagementUnlockWorkflow
extends PhabricatorPolicyManagementWorkflow {
protected function didConstruct() {
$this
->setName('unlock')
->setSynopsis(
- 'Unlock an object by setting its policies to allow anyone to view '.
- 'and edit it.')
- ->setExamples(
- '**unlock** D123')
+ pht(
+ 'Unlock an object by setting its policies to allow anyone to view '.
+ 'and edit it.'))
+ ->setExamples('**unlock** D123')
->setArguments(
array(
array(
'name' => 'objects',
'wildcard' => true,
),
));
}
public function execute(PhutilArgumentParser $args) {
$console = PhutilConsole::getConsole();
$viewer = $this->getViewer();
$obj_names = $args->getArg('objects');
if (!$obj_names) {
throw new PhutilArgumentUsageException(
- pht(
- 'Specify the name of an object to unlock.'));
+ pht('Specify the name of an object to unlock.'));
} else if (count($obj_names) > 1) {
throw new PhutilArgumentUsageException(
- pht(
- 'Specify the name of exactly one object to unlock.'));
+ pht('Specify the name of exactly one object to unlock.'));
}
$object = id(new PhabricatorObjectQuery())
->setViewer($viewer)
->withNames($obj_names)
->executeOne();
if (!$object) {
$name = head($obj_names);
throw new PhutilArgumentUsageException(
- pht(
- "No such object '%s'!",
- $name));
+ pht("No such object '%s'!", $name));
}
$handle = id(new PhabricatorHandleQuery())
->setViewer($viewer)
->withPHIDs(array($object->getPHID()))
->executeOne();
if ($object instanceof PhabricatorApplication) {
$application = $object;
$console->writeOut(
"%s\n",
pht('Unlocking Application: %s', $handle->getFullName()));
// For applications, we can't unlock them in a normal way and don't want
// to unlock every capability, just view and edit.
$capabilities = array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
$key = 'phabricator.application-settings';
$config_entry = PhabricatorConfigEntry::loadConfigEntry($key);
$value = $config_entry->getValue();
foreach ($capabilities as $capability) {
if ($application->isCapabilityEditable($capability)) {
unset($value[$application->getPHID()]['policy'][$capability]);
}
}
$config_entry->setValue($value);
$config_entry->save();
$console->writeOut("%s\n", pht('Saved application.'));
return 0;
}
$console->writeOut("%s\n", pht('Unlocking: %s', $handle->getFullName()));
$updated = false;
foreach ($object->getCapabilities() as $capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
try {
$object->setViewPolicy(PhabricatorPolicies::POLICY_USER);
$console->writeOut("%s\n", pht('Unlocked view policy.'));
$updated = true;
} catch (Exception $ex) {
$console->writeOut("%s\n", pht('View policy is not mutable.'));
}
break;
case PhabricatorPolicyCapability::CAN_EDIT:
try {
$object->setEditPolicy(PhabricatorPolicies::POLICY_USER);
$console->writeOut("%s\n", pht('Unlocked edit policy.'));
$updated = true;
} catch (Exception $ex) {
$console->writeOut("%s\n", pht('Edit policy is not mutable.'));
}
break;
case PhabricatorPolicyCapability::CAN_JOIN:
try {
$object->setJoinPolicy(PhabricatorPolicies::POLICY_USER);
$console->writeOut("%s\n", pht('Unlocked join policy.'));
$updated = true;
} catch (Exception $ex) {
$console->writeOut("%s\n", pht('Join policy is not mutable.'));
}
break;
}
}
if ($updated) {
$object->save();
$console->writeOut("%s\n", pht('Saved object.'));
} else {
$console->writeOut(
"%s\n",
pht(
'Object has no mutable policies. Try unlocking parent/container '.
'object instead. For example, to gain access to a commit, unlock '.
'the repository it belongs to.'));
}
}
}
diff --git a/src/applications/policy/query/PhabricatorPolicyQuery.php b/src/applications/policy/query/PhabricatorPolicyQuery.php
index 01e2e2bea..6a5c2219d 100644
--- a/src/applications/policy/query/PhabricatorPolicyQuery.php
+++ b/src/applications/policy/query/PhabricatorPolicyQuery.php
@@ -1,235 +1,237 @@
<?php
final class PhabricatorPolicyQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $object;
private $phids;
public function setObject(PhabricatorPolicyInterface $object) {
$this->object = $object;
return $this;
}
public function withPHIDs(array $phids) {
$this->phids = $phids;
return $this;
}
public static function loadPolicies(
PhabricatorUser $viewer,
PhabricatorPolicyInterface $object) {
$results = array();
$map = array();
foreach ($object->getCapabilities() as $capability) {
$map[$capability] = $object->getPolicy($capability);
}
$policies = id(new PhabricatorPolicyQuery())
->setViewer($viewer)
->withPHIDs($map)
->execute();
foreach ($map as $capability => $phid) {
$results[$capability] = $policies[$phid];
}
return $results;
}
public static function renderPolicyDescriptions(
PhabricatorUser $viewer,
PhabricatorPolicyInterface $object,
$icon = false) {
$policies = self::loadPolicies($viewer, $object);
foreach ($policies as $capability => $policy) {
$policies[$capability] = $policy->renderDescription($icon);
}
return $policies;
}
protected function loadPage() {
if ($this->object && $this->phids) {
throw new Exception(
- 'You can not issue a policy query with both setObject() and '.
- 'setPHIDs().');
+ pht(
+ 'You can not issue a policy query with both %s and %s.',
+ 'setObject()',
+ 'setPHIDs()'));
} else if ($this->object) {
$phids = $this->loadObjectPolicyPHIDs();
} else {
$phids = $this->phids;
}
$phids = array_fuse($phids);
$results = array();
// First, load global policies.
foreach ($this->getGlobalPolicies() as $phid => $policy) {
if (isset($phids[$phid])) {
$results[$phid] = $policy;
unset($phids[$phid]);
}
}
// If we still need policies, we're going to have to fetch data. Bucket
// the remaining policies into rule-based policies and handle-based
// policies.
if ($phids) {
$rule_policies = array();
$handle_policies = array();
foreach ($phids as $phid) {
$phid_type = phid_get_type($phid);
if ($phid_type == PhabricatorPolicyPHIDTypePolicy::TYPECONST) {
$rule_policies[$phid] = $phid;
} else {
$handle_policies[$phid] = $phid;
}
}
if ($handle_policies) {
$handles = id(new PhabricatorHandleQuery())
->setViewer($this->getViewer())
->withPHIDs($handle_policies)
->execute();
foreach ($handle_policies as $phid) {
$results[$phid] = PhabricatorPolicy::newFromPolicyAndHandle(
$phid,
$handles[$phid]);
}
}
if ($rule_policies) {
$rules = id(new PhabricatorPolicy())->loadAllWhere(
'phid IN (%Ls)',
$rule_policies);
$results += mpull($rules, null, 'getPHID');
}
}
$results = msort($results, 'getSortKey');
return $results;
}
public static function isGlobalPolicy($policy) {
$global_policies = self::getGlobalPolicies();
if (isset($global_policies[$policy])) {
return true;
}
return false;
}
public static function getGlobalPolicy($policy) {
if (!self::isGlobalPolicy($policy)) {
- throw new Exception("Policy '{$policy}' is not a global policy!");
+ throw new Exception(pht("Policy '%s' is not a global policy!", $policy));
}
return idx(self::getGlobalPolicies(), $policy);
}
private static function getGlobalPolicies() {
static $constants = array(
PhabricatorPolicies::POLICY_PUBLIC,
PhabricatorPolicies::POLICY_USER,
PhabricatorPolicies::POLICY_ADMIN,
PhabricatorPolicies::POLICY_NOONE,
);
$results = array();
foreach ($constants as $constant) {
$results[$constant] = id(new PhabricatorPolicy())
->setType(PhabricatorPolicyType::TYPE_GLOBAL)
->setPHID($constant)
->setName(self::getGlobalPolicyName($constant))
->setShortName(self::getGlobalPolicyShortName($constant))
->makeEphemeral();
}
return $results;
}
private static function getGlobalPolicyName($policy) {
switch ($policy) {
case PhabricatorPolicies::POLICY_PUBLIC:
return pht('Public (No Login Required)');
case PhabricatorPolicies::POLICY_USER:
return pht('All Users');
case PhabricatorPolicies::POLICY_ADMIN:
return pht('Administrators');
case PhabricatorPolicies::POLICY_NOONE:
return pht('No One');
default:
return pht('Unknown Policy');
}
}
private static function getGlobalPolicyShortName($policy) {
switch ($policy) {
case PhabricatorPolicies::POLICY_PUBLIC:
return pht('Public');
default:
return null;
}
}
private function loadObjectPolicyPHIDs() {
$phids = array();
$viewer = $this->getViewer();
if ($viewer->getPHID()) {
$projects = id(new PhabricatorProjectQuery())
->setViewer($viewer)
->withMemberPHIDs(array($viewer->getPHID()))
->execute();
foreach ($projects as $project) {
$phids[] = $project->getPHID();
}
// Include the "current viewer" policy. This improves consistency, but
// is also useful for creating private instances of normally-shared object
// types, like repositories.
$phids[] = $viewer->getPHID();
}
$capabilities = $this->object->getCapabilities();
foreach ($capabilities as $capability) {
$policy = $this->object->getPolicy($capability);
if (!$policy) {
continue;
}
$phids[] = $policy;
}
// If this install doesn't have "Public" enabled, don't include it as an
// option unless the object already has a "Public" policy. In this case we
// retain the policy but enforce it as though it was "All Users".
$show_public = PhabricatorEnv::getEnvConfig('policy.allow-public');
foreach ($this->getGlobalPolicies() as $phid => $policy) {
if ($phid == PhabricatorPolicies::POLICY_PUBLIC) {
if (!$show_public) {
continue;
}
}
$phids[] = $phid;
}
return $phids;
}
protected function shouldDisablePolicyFiltering() {
// Policy filtering of policies is currently perilous and not required by
// the application.
return true;
}
public function getQueryApplicationClass() {
return 'PhabricatorPolicyApplication';
}
}
diff --git a/src/applications/policy/storage/PhabricatorPolicy.php b/src/applications/policy/storage/PhabricatorPolicy.php
index 7c8186124..c83d09ab5 100644
--- a/src/applications/policy/storage/PhabricatorPolicy.php
+++ b/src/applications/policy/storage/PhabricatorPolicy.php
@@ -1,377 +1,381 @@
<?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);
}
if (!$handle) {
throw new Exception(
- "Policy identifier is an object PHID ('{$policy_identifier}'), but no ".
- "object handle was provided. A handle must be provided for object ".
- "policies.");
+ 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(
- "Policy identifier is an object PHID ('{$policy_identifier}'), but ".
- "the provided handle has a different PHID ('{$handle_phid}'). The ".
- "handle must correspond to the policy identifier.");
+ 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);
$policy->setName($handle->getName());
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 getIcon() {
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) {
switch ($policy) {
case PhabricatorPolicies::POLICY_PUBLIC:
return pht('This object is public.');
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 the project "%s" can take this action.',
$handle->getFullName());
} 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('Project: %s', $this->getName());
case PhabricatorPolicyType::TYPE_MASKED:
return pht('Other: %s', $this->getName());
default:
return $this->getName();
}
}
public function renderDescription($icon = false) {
$img = null;
if ($icon) {
$img = id(new PHUIIconView())
->setIconFont($this->getIcon());
}
if ($this->getHref()) {
$desc = javelin_tag(
'a',
array(
'href' => $this->getHref(),
'class' => 'policy-link',
'sigil' => $this->getWorkflow() ? 'workflow' : null,
),
array(
$img,
$this->getName(),
));
} else {
if ($img) {
$desc = array($img, $this->getName());
} else {
$desc = $this->getName();
}
}
switch ($this->getType()) {
case PhabricatorPolicyType::TYPE_PROJECT:
return pht('%s (Project)', $desc);
case PhabricatorPolicyType::TYPE_CUSTOM:
return $desc;
case PhabricatorPolicyType::TYPE_MASKED:
return pht(
'%s (You do not have permission to view policy details.)',
$desc);
default:
return $desc;
}
}
/**
* 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) {
$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);
}
/* -( 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;
}
public function describeAutomaticCapability($capability) {
return null;
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$this->delete();
}
}
diff --git a/src/applications/ponder/application/PhabricatorPonderApplication.php b/src/applications/ponder/application/PhabricatorPonderApplication.php
index e21660301..999580e90 100644
--- a/src/applications/ponder/application/PhabricatorPonderApplication.php
+++ b/src/applications/ponder/application/PhabricatorPonderApplication.php
@@ -1,88 +1,88 @@
<?php
final class PhabricatorPonderApplication extends PhabricatorApplication {
public function getBaseURI() {
return '/ponder/';
}
public function getName() {
return pht('Ponder');
}
public function getShortDescription() {
return pht('Questions and Answers');
}
public function getFontIcon() {
return 'fa-university';
}
public function getFactObjectsForAnalysis() {
return array(
new PonderQuestion(),
);
}
public function getTitleGlyph() {
return "\xE2\x97\xB3";
}
public function loadStatus(PhabricatorUser $user) {
- // replace with "x new unanswered questions" or some such
- // make sure to use self::formatStatusCount and friends...!
+ // Replace with "x new unanswered questions" or some such
+ // make sure to use `self::formatStatusCount` and friends...!
$status = array();
return $status;
}
public function getRemarkupRules() {
return array(
new PonderRemarkupRule(),
);
}
public function isPrototype() {
return true;
}
public function getRoutes() {
return array(
'/Q(?P<id>[1-9]\d*)' => 'PonderQuestionViewController',
'/ponder/' => array(
'(?:query/(?P<queryKey>[^/]+)/)?' => 'PonderQuestionListController',
'answer/add/' => 'PonderAnswerSaveController',
'answer/edit/(?P<id>\d+)/' => 'PonderAnswerEditController',
'answer/comment/(?P<id>\d+)/' => 'PonderAnswerCommentController',
'answer/history/(?P<id>\d+)/' => 'PonderAnswerHistoryController',
'question/edit/(?:(?P<id>\d+)/)?' => 'PonderQuestionEditController',
'question/comment/(?P<id>\d+)/' => 'PonderQuestionCommentController',
'question/history/(?P<id>\d+)/' => 'PonderQuestionHistoryController',
'preview/' => 'PhabricatorMarkupPreviewController',
'question/(?P<status>open|close)/(?P<id>[1-9]\d*)/'
=> 'PonderQuestionStatusController',
'vote/' => 'PonderVoteSaveController',
),
);
}
public function getMailCommandObjects() {
return array(
'question' => array(
'name' => pht('Email Commands: Questions'),
'header' => pht('Interacting with Ponder Questions'),
'object' => new PonderQuestion(),
'summary' => pht(
'This page documents the commands you can use to interact with '.
'questions in Ponder.'),
),
);
}
public function getApplicationSearchDocumentTypes() {
return array(
PonderQuestionPHIDType::TYPECONST,
);
}
}
diff --git a/src/applications/ponder/constants/PonderQuestionStatus.php b/src/applications/ponder/constants/PonderQuestionStatus.php
index 8d2a87823..3877380b3 100644
--- a/src/applications/ponder/constants/PonderQuestionStatus.php
+++ b/src/applications/ponder/constants/PonderQuestionStatus.php
@@ -1,31 +1,31 @@
<?php
final class PonderQuestionStatus extends PonderConstants {
const STATUS_OPEN = 0;
const STATUS_CLOSED = 1;
public static function getQuestionStatusMap() {
return array(
self::STATUS_OPEN => pht('Open'),
self::STATUS_CLOSED => pht('Closed'),
);
}
public static function getQuestionStatusFullName($status) {
$map = array(
self::STATUS_OPEN => pht('Open'),
self::STATUS_CLOSED => pht('Closed by author'),
);
- return idx($map, $status, '???');
+ return idx($map, $status, pht('Unknown'));
}
public static function getQuestionStatusTagColor($status) {
$map = array(
self::STATUS_CLOSED => PHUITagView::COLOR_BLACK,
);
return idx($map, $status);
}
}
diff --git a/src/applications/ponder/controller/PonderAnswerSaveController.php b/src/applications/ponder/controller/PonderAnswerSaveController.php
index b720015ff..53e9367ed 100644
--- a/src/applications/ponder/controller/PonderAnswerSaveController.php
+++ b/src/applications/ponder/controller/PonderAnswerSaveController.php
@@ -1,69 +1,68 @@
<?php
final class PonderAnswerSaveController extends PonderController {
public function processRequest() {
$request = $this->getRequest();
$viewer = $request->getUser();
if (!$request->isFormPost()) {
return new Aphront400Response();
}
$question_id = $request->getInt('question_id');
$question = id(new PonderQuestionQuery())
->setViewer($viewer)
->withIDs(array($question_id))
->needAnswers(true)
->executeOne();
if (!$question) {
return new Aphront404Response();
}
$answer = $request->getStr('answer');
if (!strlen(trim($answer))) {
$dialog = id(new AphrontDialogView())
->setUser($viewer)
->setTitle(pht('Empty Answer'))
->appendChild(
- phutil_tag('p', array(), pht(
- 'Your answer must not be empty.')))
+ phutil_tag('p', array(), pht('Your answer must not be empty.')))
->addCancelButton('/Q'.$question_id);
return id(new AphrontDialogResponse())->setDialog($dialog);
}
$content_source = PhabricatorContentSource::newForSource(
PhabricatorContentSource::SOURCE_WEB,
array(
'ip' => $request->getRemoteAddr(),
));
$res = id(new PonderAnswer())
->setAuthorPHID($viewer->getPHID())
->setQuestionID($question->getID())
->setContent($answer)
->setVoteCount(0)
->setContentSource($content_source);
$xactions = array();
$xactions[] = id(new PonderQuestionTransaction())
->setTransactionType(PonderQuestionTransaction::TYPE_ANSWERS)
->setNewValue(
array(
'+' => array(
array('answer' => $res),
),
));
$editor = id(new PonderQuestionEditor())
->setActor($viewer)
->setContentSourceFromRequest($request);
$editor->applyTransactions($question, $xactions);
return id(new AphrontRedirectResponse())->setURI(
id(new PhutilURI('/Q'.$question->getID())));
}
}
diff --git a/src/applications/ponder/editor/PonderVoteEditor.php b/src/applications/ponder/editor/PonderVoteEditor.php
index cc9f89d9f..2edb18678 100644
--- a/src/applications/ponder/editor/PonderVoteEditor.php
+++ b/src/applications/ponder/editor/PonderVoteEditor.php
@@ -1,77 +1,77 @@
<?php
final class PonderVoteEditor extends PhabricatorEditor {
private $answer;
private $votable;
private $anwer;
private $vote;
public function setAnswer($answer) {
$this->answer = $answer;
return $this;
}
public function setVotable($votable) {
$this->votable = $votable;
return $this;
}
public function setVote($vote) {
$this->vote = $vote;
return $this;
}
public function saveVote() {
$actor = $this->requireActor();
if (!$this->votable) {
- throw new Exception('Must set votable before saving vote');
+ throw new Exception(pht('Must set votable before saving vote.'));
}
$votable = $this->votable;
$newvote = $this->vote;
// prepare vote add, or update if this user is amending an
// earlier vote
$editor = id(new PhabricatorEdgeEditor())
->addEdge(
$actor->getPHID(),
$votable->getUserVoteEdgeType(),
$votable->getVotablePHID(),
array('data' => $newvote))
->removeEdge(
$actor->getPHID(),
$votable->getUserVoteEdgeType(),
$votable->getVotablePHID());
$conn = $votable->establishConnection('w');
$trans = $conn->openTransaction();
$trans->beginReadLocking();
$votable->reload();
$curvote = (int)PhabricatorEdgeQuery::loadSingleEdgeData(
$actor->getPHID(),
$votable->getUserVoteEdgeType(),
$votable->getVotablePHID());
if (!$curvote) {
$curvote = PonderVote::VOTE_NONE;
}
- // adjust votable's score by this much
+ // Adjust votable's score by this much.
$delta = $newvote - $curvote;
queryfx($conn,
'UPDATE %T as t
SET t.`voteCount` = t.`voteCount` + %d
WHERE t.`PHID` = %s',
$votable->getTableName(),
$delta,
$votable->getVotablePHID());
$editor->save();
$trans->endReadLocking();
$trans->saveTransaction();
}
}
diff --git a/src/applications/ponder/mail/PonderQuestionReplyHandler.php b/src/applications/ponder/mail/PonderQuestionReplyHandler.php
index 699d7cd79..5d3978220 100644
--- a/src/applications/ponder/mail/PonderQuestionReplyHandler.php
+++ b/src/applications/ponder/mail/PonderQuestionReplyHandler.php
@@ -1,16 +1,16 @@
<?php
final class PonderQuestionReplyHandler
extends PhabricatorApplicationTransactionReplyHandler {
public function validateMailReceiver($mail_receiver) {
if (!($mail_receiver instanceof PonderQuestion)) {
- throw new Exception('Mail receiver is not a PonderQuestion!');
+ throw new Exception(pht('Mail receiver is not a %s!', 'PonderQuestion'));
}
}
public function getObjectPrefix() {
return 'Q';
}
}
diff --git a/src/applications/ponder/phid/PonderAnswerPHIDType.php b/src/applications/ponder/phid/PonderAnswerPHIDType.php
index c292b10b8..0bb059673 100644
--- a/src/applications/ponder/phid/PonderAnswerPHIDType.php
+++ b/src/applications/ponder/phid/PonderAnswerPHIDType.php
@@ -1,40 +1,40 @@
<?php
final class PonderAnswerPHIDType extends PhabricatorPHIDType {
const TYPECONST = 'ANSW';
public function getTypeName() {
return pht('Ponder Answer');
}
public function newObject() {
return new PonderAnswer();
}
protected function buildQueryForObjects(
PhabricatorObjectQuery $query,
array $phids) {
return id(new PonderAnswerQuery())
->withPHIDs($phids);
}
public function loadHandles(
PhabricatorHandleQuery $query,
array $handles,
array $objects) {
foreach ($handles as $phid => $handle) {
$answer = $objects[$phid];
$id = $answer->getID();
$question = $answer->getQuestion();
$question_title = $question->getFullTitle();
- $handle->setName("{$question_title} (Answer {$id})");
+ $handle->setName(pht('%s (Answer %s)', $question_title, $id));
$handle->setURI($answer->getURI());
}
}
}
diff --git a/src/applications/ponder/query/PonderQuestionQuery.php b/src/applications/ponder/query/PonderQuestionQuery.php
index 51081b320..b63ba60de 100644
--- a/src/applications/ponder/query/PonderQuestionQuery.php
+++ b/src/applications/ponder/query/PonderQuestionQuery.php
@@ -1,182 +1,182 @@
<?php
final class PonderQuestionQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $ids;
private $phids;
private $authorPHIDs;
private $answererPHIDs;
private $status = 'status-any';
const STATUS_ANY = 'status-any';
const STATUS_OPEN = 'status-open';
const STATUS_CLOSED = 'status-closed';
private $needAnswers;
private $needViewerVotes;
public function withIDs(array $ids) {
$this->ids = $ids;
return $this;
}
public function withPHIDs(array $phids) {
$this->phids = $phids;
return $this;
}
public function withAuthorPHIDs(array $phids) {
$this->authorPHIDs = $phids;
return $this;
}
public function withStatus($status) {
$this->status = $status;
return $this;
}
public function withAnswererPHIDs(array $phids) {
$this->answererPHIDs = $phids;
return $this;
}
public function needAnswers($need_answers) {
$this->needAnswers = $need_answers;
return $this;
}
public function needViewerVotes($need_viewer_votes) {
$this->needViewerVotes = $need_viewer_votes;
return $this;
}
protected function buildWhereClause(AphrontDatabaseConnection $conn_r) {
$where = array();
if ($this->ids) {
$where[] = qsprintf(
$conn_r,
'q.id IN (%Ld)',
$this->ids);
}
if ($this->phids) {
$where[] = qsprintf(
$conn_r,
'q.phid IN (%Ls)',
$this->phids);
}
if ($this->authorPHIDs) {
$where[] = qsprintf(
$conn_r,
'q.authorPHID IN (%Ls)',
$this->authorPHIDs);
}
if ($this->status) {
switch ($this->status) {
case self::STATUS_ANY:
break;
case self::STATUS_OPEN:
$where[] = qsprintf(
$conn_r,
'q.status = %d',
PonderQuestionStatus::STATUS_OPEN);
break;
case self::STATUS_CLOSED:
$where[] = qsprintf(
$conn_r,
'q.status = %d',
PonderQuestionStatus::STATUS_CLOSED);
break;
default:
- throw new Exception("Unknown status query '{$this->status}'!");
+ throw new Exception(pht("Unknown status query '%s'!", $this->status));
}
}
$where[] = $this->buildPagingClause($conn_r);
return $this->formatWhereClause($where);
}
protected function loadPage() {
$question = new PonderQuestion();
$conn_r = $question->establishConnection('r');
$data = queryfx_all(
$conn_r,
'SELECT q.* FROM %T q %Q %Q %Q %Q',
$question->getTableName(),
$this->buildJoinsClause($conn_r),
$this->buildWhereClause($conn_r),
$this->buildOrderClause($conn_r),
$this->buildLimitClause($conn_r));
return $question->loadAllFromArray($data);
}
protected function willFilterPage(array $questions) {
if ($this->needAnswers) {
$aquery = id(new PonderAnswerQuery())
->setViewer($this->getViewer())
->setOrderVector(array('-id'))
->withQuestionIDs(mpull($questions, 'getID'));
if ($this->needViewerVotes) {
$aquery->needViewerVotes($this->needViewerVotes);
}
$answers = $aquery->execute();
$answers = mgroup($answers, 'getQuestionID');
foreach ($questions as $question) {
$question_answers = idx($answers, $question->getID(), array());
$question->attachAnswers(mpull($question_answers, null, 'getPHID'));
}
}
if ($this->needViewerVotes) {
$viewer_phid = $this->getViewer()->getPHID();
$etype = PonderQuestionHasVotingUserEdgeType::EDGECONST;
$edges = id(new PhabricatorEdgeQuery())
->withSourcePHIDs(mpull($questions, 'getPHID'))
->withDestinationPHIDs(array($viewer_phid))
->withEdgeTypes(array($etype))
->needEdgeData(true)
->execute();
foreach ($questions as $question) {
$user_edge = idx(
$edges[$question->getPHID()][$etype],
$viewer_phid,
array());
$question->attachUserVote($viewer_phid, idx($user_edge, 'data', 0));
}
}
return $questions;
}
private function buildJoinsClause(AphrontDatabaseConnection $conn_r) {
$joins = array();
if ($this->answererPHIDs) {
$answer_table = new PonderAnswer();
$joins[] = qsprintf(
$conn_r,
'JOIN %T a ON a.questionID = q.id AND a.authorPHID IN (%Ls)',
$answer_table->getTableName(),
$this->answererPHIDs);
}
return implode(' ', $joins);
}
public function getQueryApplicationClass() {
return 'PhabricatorPonderApplication';
}
}
diff --git a/src/applications/ponder/storage/PonderQuestion.php b/src/applications/ponder/storage/PonderQuestion.php
index 8c8dab3f4..1568c5924 100644
--- a/src/applications/ponder/storage/PonderQuestion.php
+++ b/src/applications/ponder/storage/PonderQuestion.php
@@ -1,302 +1,301 @@
<?php
final class PonderQuestion extends PonderDAO
implements
PhabricatorApplicationTransactionInterface,
PhabricatorMarkupInterface,
PonderVotableInterface,
PhabricatorSubscribableInterface,
PhabricatorFlaggableInterface,
PhabricatorPolicyInterface,
PhabricatorTokenReceiverInterface,
PhabricatorProjectInterface,
PhabricatorDestructibleInterface {
const MARKUP_FIELD_CONTENT = 'markup:content';
protected $title;
protected $phid;
protected $authorPHID;
protected $status;
protected $content;
protected $contentSource;
protected $voteCount;
protected $answerCount;
protected $heat;
protected $mailKey;
private $answers;
private $vote;
private $comments;
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_COLUMN_SCHEMA => array(
'title' => 'text255',
'voteCount' => 'sint32',
'status' => 'uint32',
'content' => 'text',
'heat' => 'double',
'answerCount' => 'uint32',
'mailKey' => 'bytes20',
// T6203/NULLABILITY
// This should always exist.
'contentSource' => 'text?',
),
self::CONFIG_KEY_SCHEMA => array(
'key_phid' => null,
'phid' => array(
'columns' => array('phid'),
'unique' => true,
),
'authorPHID' => array(
'columns' => array('authorPHID'),
),
'heat' => array(
'columns' => array('heat'),
),
'status' => array(
'columns' => array('status'),
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(PonderQuestionPHIDType::TYPECONST);
}
public function setContentSource(PhabricatorContentSource $content_source) {
$this->contentSource = $content_source->serialize();
return $this;
}
public function getContentSource() {
return PhabricatorContentSource::newFromSerialized($this->contentSource);
}
public function attachVotes($user_phid) {
$qa_phids = mpull($this->answers, 'getPHID') + array($this->getPHID());
$edges = id(new PhabricatorEdgeQuery())
->withSourcePHIDs(array($user_phid))
->withDestinationPHIDs($qa_phids)
->withEdgeTypes(
array(
PonderVotingUserHasQuestionEdgeType::EDGECONST,
PonderVotingUserHasAnswerEdgeType::EDGECONST,
))
->needEdgeData(true)
->execute();
$question_edge =
$edges[$user_phid][PonderVotingUserHasQuestionEdgeType::EDGECONST];
$answer_edges =
$edges[$user_phid][PonderVotingUserHasAnswerEdgeType::EDGECONST];
$edges = null;
$this->setUserVote(idx($question_edge, $this->getPHID()));
foreach ($this->answers as $answer) {
$answer->setUserVote(idx($answer_edges, $answer->getPHID()));
}
}
public function setUserVote($vote) {
$this->vote = $vote['data'];
if (!$this->vote) {
$this->vote = PonderVote::VOTE_NONE;
}
return $this;
}
public function attachUserVote($user_phid, $vote) {
$this->vote = $vote;
return $this;
}
public function getUserVote() {
return $this->vote;
}
public function setComments($comments) {
$this->comments = $comments;
return $this;
}
public function getComments() {
return $this->comments;
}
public function attachAnswers(array $answers) {
assert_instances_of($answers, 'PonderAnswer');
$this->answers = $answers;
return $this;
}
public function getAnswers() {
return $this->answers;
}
public function getMarkupField() {
return self::MARKUP_FIELD_CONTENT;
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new PonderQuestionEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new PonderQuestionTransaction();
}
public function willRenderTimeline(
PhabricatorApplicationTransactionView $timeline,
AphrontRequest $request) {
return $timeline;
}
// Markup interface
public function getMarkupFieldKey($field) {
$hash = PhabricatorHash::digest($this->getMarkupText($field));
$id = $this->getID();
return "ponder:Q{$id}:{$field}:{$hash}";
}
public function getMarkupText($field) {
return $this->getContent();
}
public function newMarkupEngine($field) {
return PhabricatorMarkupEngine::getEngine();
}
public function didMarkupText(
$field,
$output,
PhutilMarkupEngine $engine) {
return $output;
}
public function shouldUseMarkupCache($field) {
return (bool)$this->getID();
}
// votable interface
public function getUserVoteEdgeType() {
return PonderVotingUserHasQuestionEdgeType::EDGECONST;
}
public function getVotablePHID() {
return $this->getPHID();
}
public function save() {
if (!$this->getMailKey()) {
$this->setMailKey(Filesystem::readRandomCharacters(20));
}
return parent::save();
}
public function getOriginalTitle() {
// TODO: Make this actually save/return the original title.
return $this->getTitle();
}
public function getFullTitle() {
$id = $this->getID();
$title = $this->getTitle();
return "Q{$id}: {$title}";
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
$policy = PhabricatorPolicies::POLICY_NOONE;
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
$policy = PhabricatorPolicies::POLICY_USER;
break;
}
return $policy;
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return ($viewer->getPHID() == $this->getAuthorPHID());
}
public function describeAutomaticCapability($capability) {
- return pht(
- 'The user who asked a question can always view and edit it.');
+ return pht('The user who asked a question can always view and edit it.');
}
/* -( PhabricatorSubscribableInterface )----------------------------------- */
public function isAutomaticallySubscribed($phid) {
return ($phid == $this->getAuthorPHID());
}
public function shouldShowSubscribersProperty() {
return true;
}
public function shouldAllowSubscription($phid) {
return true;
}
/* -( PhabricatorTokenReceiverInterface )---------------------------------- */
public function getUsersToNotifyOfTokenGiven() {
return array(
$this->getAuthorPHID(),
);
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$this->openTransaction();
$answers = id(new PonderAnswer())->loadAllWhere(
'questionID = %d',
$this->getID());
foreach ($answers as $answer) {
$engine->destroyObject($answer);
}
$this->delete();
$this->saveTransaction();
}
}
diff --git a/src/applications/ponder/storage/PonderQuestionTransaction.php b/src/applications/ponder/storage/PonderQuestionTransaction.php
index 5fe3bc266..0e4d07b15 100644
--- a/src/applications/ponder/storage/PonderQuestionTransaction.php
+++ b/src/applications/ponder/storage/PonderQuestionTransaction.php
@@ -1,323 +1,323 @@
<?php
final class PonderQuestionTransaction
extends PhabricatorApplicationTransaction {
const TYPE_TITLE = 'ponder.question:question';
const TYPE_CONTENT = 'ponder.question:content';
const TYPE_ANSWERS = 'ponder.question:answer';
const TYPE_STATUS = 'ponder.question:status';
public function getApplicationName() {
return 'ponder';
}
public function getTableName() {
return 'ponder_questiontransaction';
}
public function getApplicationTransactionType() {
return PonderQuestionPHIDType::TYPECONST;
}
public function getApplicationTransactionCommentObject() {
return new PonderQuestionTransactionComment();
}
public function getRequiredHandlePHIDs() {
$phids = parent::getRequiredHandlePHIDs();
switch ($this->getTransactionType()) {
case self::TYPE_ANSWERS:
$phids[] = $this->getNewAnswerPHID();
$phids[] = $this->getObjectPHID();
break;
}
return $phids;
}
public function getRemarkupBlocks() {
$blocks = parent::getRemarkupBlocks();
switch ($this->getTransactionType()) {
case self::TYPE_CONTENT:
$blocks[] = $this->getNewValue();
break;
}
return $blocks;
}
public function getTitle() {
$author_phid = $this->getAuthorPHID();
$object_phid = $this->getObjectPHID();
$old = $this->getOldValue();
$new = $this->getNewValue();
switch ($this->getTransactionType()) {
case self::TYPE_TITLE:
if ($old === null) {
return pht(
'%s asked this question.',
$this->renderHandleLink($author_phid));
} else {
return pht(
'%s edited the question title from "%s" to "%s".',
$this->renderHandleLink($author_phid),
$old,
$new);
}
case self::TYPE_CONTENT:
return pht(
'%s edited the question description.',
$this->renderHandleLink($author_phid));
case self::TYPE_ANSWERS:
$answer_handle = $this->getHandle($this->getNewAnswerPHID());
$question_handle = $this->getHandle($object_phid);
return pht(
'%s answered %s',
$this->renderHandleLink($author_phid),
$this->renderHandleLink($object_phid));
case self::TYPE_STATUS:
switch ($new) {
case PonderQuestionStatus::STATUS_OPEN:
return pht(
'%s reopened this question.',
$this->renderHandleLink($author_phid));
case PonderQuestionStatus::STATUS_CLOSED:
return pht(
'%s closed this question.',
$this->renderHandleLink($author_phid));
}
}
return parent::getTitle();
}
public function getIcon() {
$old = $this->getOldValue();
$new = $this->getNewValue();
switch ($this->getTransactionType()) {
case self::TYPE_TITLE:
case self::TYPE_CONTENT:
return 'fa-pencil';
case self::TYPE_STATUS:
switch ($new) {
case PonderQuestionStatus::STATUS_OPEN:
return 'fa-check-circle';
case PonderQuestionStatus::STATUS_CLOSED:
return 'fa-minus-circle';
}
case self::TYPE_ANSWERS:
return 'fa-plus';
}
return parent::getIcon();
}
public function getColor() {
$old = $this->getOldValue();
$new = $this->getNewValue();
switch ($this->getTransactionType()) {
case self::TYPE_TITLE:
case self::TYPE_CONTENT:
return PhabricatorTransactions::COLOR_BLUE;
case self::TYPE_ANSWERS:
return PhabricatorTransactions::COLOR_GREEN;
case self::TYPE_STATUS:
switch ($new) {
case PonderQuestionStatus::STATUS_OPEN:
return PhabricatorTransactions::COLOR_GREEN;
case PonderQuestionStatus::STATUS_CLOSED:
return PhabricatorTransactions::COLOR_INDIGO;
}
}
}
public function hasChangeDetails() {
switch ($this->getTransactionType()) {
case self::TYPE_CONTENT:
return true;
}
return parent::hasChangeDetails();
}
public function renderChangeDetails(PhabricatorUser $viewer) {
return $this->renderTextCorpusChangeDetails(
$viewer,
$this->getOldValue(),
$this->getNewValue());
}
public function getActionStrength() {
$old = $this->getOldValue();
$new = $this->getNewValue();
switch ($this->getTransactionType()) {
case self::TYPE_TITLE:
if ($old === null) {
return 3;
}
break;
case self::TYPE_ANSWERS:
return 2;
}
return parent::getActionStrength();
}
public function getActionName() {
$old = $this->getOldValue();
$new = $this->getNewValue();
switch ($this->getTransactionType()) {
case self::TYPE_TITLE:
if ($old === null) {
return pht('Asked');
}
break;
case self::TYPE_ANSWERS:
return pht('Answered');
}
return parent::getActionName();
}
public function shouldHide() {
switch ($this->getTransactionType()) {
case self::TYPE_CONTENT:
if ($this->getOldValue() === null) {
return true;
} else {
return false;
}
break;
}
return parent::shouldHide();
}
public function getTitleForFeed() {
$author_phid = $this->getAuthorPHID();
$object_phid = $this->getObjectPHID();
$old = $this->getOldValue();
$new = $this->getNewValue();
switch ($this->getTransactionType()) {
case self::TYPE_TITLE:
if ($old === null) {
return pht(
'%s asked a question: %s',
$this->renderHandleLink($author_phid),
$this->renderHandleLink($object_phid));
} else {
return pht(
'%s edited the title of %s (was "%s")',
$this->renderHandleLink($author_phid),
$this->renderHandleLink($object_phid),
$old);
}
case self::TYPE_CONTENT:
return pht(
'%s edited the description of %s',
$this->renderHandleLink($author_phid),
$this->renderHandleLink($object_phid));
case self::TYPE_ANSWERS:
$answer_handle = $this->getHandle($this->getNewAnswerPHID());
$question_handle = $this->getHandle($object_phid);
return pht(
'%s answered %s',
$this->renderHandleLink($author_phid),
$answer_handle->renderLink($question_handle->getFullName()));
case self::TYPE_STATUS:
switch ($new) {
case PonderQuestionStatus::STATUS_OPEN:
return pht(
'%s reopened %s',
$this->renderHandleLink($author_phid),
$this->renderHandleLink($object_phid));
case PonderQuestionStatus::STATUS_CLOSED:
return pht(
'%s closed %s',
$this->renderHandleLink($author_phid),
$this->renderHandleLink($object_phid));
}
}
return parent::getTitleForFeed();
}
public function getBodyForFeed(PhabricatorFeedStory $story) {
$new = $this->getNewValue();
$old = $this->getOldValue();
$body = null;
switch ($this->getTransactionType()) {
case self::TYPE_TITLE:
if ($old === null) {
$question = $story->getObject($this->getObjectPHID());
return phutil_escape_html_newlines(
id(new PhutilUTF8StringTruncator())
->setMaximumGlyphs(128)
->truncateString($question->getContent()));
}
break;
case self::TYPE_ANSWERS:
$answer = $this->getNewAnswerObject($story);
if ($answer) {
return phutil_escape_html_newlines(
id(new PhutilUTF8StringTruncator())
->setMaximumGlyphs(128)
->truncateString($answer->getContent()));
}
break;
}
return parent::getBodyForFeed($story);
}
/**
* Currently the application only supports adding answers one at a time.
* This data is stored as a list of phids. Use this function to get the
* new phid.
*/
private function getNewAnswerPHID() {
$new = $this->getNewValue();
$old = $this->getOldValue();
$add = array_diff($new, $old);
if (count($add) != 1) {
throw new Exception(
- 'There should be only one answer added at a time.');
+ pht('There should be only one answer added at a time.'));
}
return reset($add);
}
/**
* Generally, the answer object is only available if the transaction
- * type is self::TYPE_ANSWERS.
+ * type is `self::TYPE_ANSWERS`.
*
* Some stories - notably ones made before D7027 - will be of the more
* generic @{class:PhabricatorApplicationTransactionFeedStory}. These
* poor stories won't have the PonderAnswer loaded, and thus will have
* less cool information.
*/
private function getNewAnswerObject(PhabricatorFeedStory $story) {
if ($story instanceof PonderTransactionFeedStory) {
$answer_phid = $this->getNewAnswerPHID();
if ($answer_phid) {
return $story->getObject($answer_phid);
}
}
return null;
}
}
diff --git a/src/applications/project/conduit/ProjectQueryConduitAPIMethod.php b/src/applications/project/conduit/ProjectQueryConduitAPIMethod.php
index 1f2078ff6..f65578e34 100644
--- a/src/applications/project/conduit/ProjectQueryConduitAPIMethod.php
+++ b/src/applications/project/conduit/ProjectQueryConduitAPIMethod.php
@@ -1,114 +1,114 @@
<?php
final class ProjectQueryConduitAPIMethod extends ProjectConduitAPIMethod {
public function getAPIMethodName() {
return 'project.query';
}
public function getMethodDescription() {
- return 'Execute searches for Projects.';
+ return pht('Execute searches for Projects.');
}
protected function defineParamTypes() {
$statuses = array(
PhabricatorProjectQuery::STATUS_ANY,
PhabricatorProjectQuery::STATUS_OPEN,
PhabricatorProjectQuery::STATUS_CLOSED,
PhabricatorProjectQuery::STATUS_ACTIVE,
PhabricatorProjectQuery::STATUS_ARCHIVED,
);
$status_const = $this->formatStringConstants($statuses);
return array(
'ids' => 'optional list<int>',
'names' => 'optional list<string>',
'phids' => 'optional list<phid>',
'slugs' => 'optional list<string>',
'status' => 'optional '.$status_const,
'members' => 'optional list<phid>',
'limit' => 'optional int',
'offset' => 'optional int',
);
}
protected function defineReturnType() {
return 'list';
}
protected function execute(ConduitAPIRequest $request) {
$query = new PhabricatorProjectQuery();
$query->setViewer($request->getUser());
$query->needMembers(true);
$query->needSlugs(true);
$ids = $request->getValue('ids');
if ($ids) {
$query->withIDs($ids);
}
$names = $request->getValue('names');
if ($names) {
$query->withNames($names);
}
$status = $request->getValue('status');
if ($status) {
$query->withStatus($status);
}
$phids = $request->getValue('phids');
if ($phids) {
$query->withPHIDs($phids);
}
$slugs = $request->getValue('slugs');
if ($slugs) {
$query->withSlugs($slugs);
}
$members = $request->getValue('members');
if ($members) {
$query->withMemberPHIDs($members);
}
$limit = $request->getValue('limit');
if ($limit) {
$query->setLimit($limit);
}
$offset = $request->getValue('offset');
if ($offset) {
$query->setOffset($offset);
}
$pager = $this->newPager($request);
$results = $query->executeWithCursorPager($pager);
$projects = $this->buildProjectInfoDictionaries($results);
// TODO: This is pretty hideous.
$slug_map = array();
if ($slugs) {
foreach ($slugs as $slug) {
$normal = rtrim(PhabricatorSlug::normalize($slug), '/');
foreach ($projects as $project) {
if (in_array($normal, $project['slugs'])) {
$slug_map[$slug] = $project['phid'];
}
}
}
}
$result = array(
'data' => $projects,
'slugMap' => $slug_map,
);
return $this->addPagerResults($result, $pager);
}
}
diff --git a/src/applications/project/editor/__tests__/PhabricatorProjectEditorTestCase.php b/src/applications/project/editor/__tests__/PhabricatorProjectEditorTestCase.php
index 3af09a454..876ed9d0d 100644
--- a/src/applications/project/editor/__tests__/PhabricatorProjectEditorTestCase.php
+++ b/src/applications/project/editor/__tests__/PhabricatorProjectEditorTestCase.php
@@ -1,289 +1,291 @@
<?php
final class PhabricatorProjectEditorTestCase extends PhabricatorTestCase {
protected function getPhabricatorTestCaseConfiguration() {
return array(
self::PHABRICATOR_TESTCONFIG_BUILD_STORAGE_FIXTURES => true,
);
}
public function testViewProject() {
$user = $this->createUser();
$user->save();
$user2 = $this->createUser();
$user2->save();
$proj = $this->createProject($user);
$proj = $this->refreshProject($proj, $user, true);
$this->joinProject($proj, $user);
$proj->setViewPolicy(PhabricatorPolicies::POLICY_USER);
$proj->save();
$can_view = PhabricatorPolicyCapability::CAN_VIEW;
// When the view policy is set to "users", any user can see the project.
$this->assertTrue((bool)$this->refreshProject($proj, $user));
$this->assertTrue((bool)$this->refreshProject($proj, $user2));
// When the view policy is set to "no one", members can still see the
// project.
$proj->setViewPolicy(PhabricatorPolicies::POLICY_NOONE);
$proj->save();
$this->assertTrue((bool)$this->refreshProject($proj, $user));
$this->assertFalse((bool)$this->refreshProject($proj, $user2));
}
public function testEditProject() {
$user = $this->createUser();
$user->save();
$user2 = $this->createUser();
$user2->save();
$proj = $this->createProject($user);
// When edit and view policies are set to "user", anyone can edit.
$proj->setViewPolicy(PhabricatorPolicies::POLICY_USER);
$proj->setEditPolicy(PhabricatorPolicies::POLICY_USER);
$proj->save();
$this->assertTrue($this->attemptProjectEdit($proj, $user));
// When edit policy is set to "no one", no one can edit.
$proj->setEditPolicy(PhabricatorPolicies::POLICY_NOONE);
$proj->save();
$caught = null;
try {
$this->attemptProjectEdit($proj, $user);
} catch (Exception $ex) {
$caught = $ex;
}
$this->assertTrue($caught instanceof Exception);
}
private function attemptProjectEdit(
PhabricatorProject $proj,
PhabricatorUser $user,
$skip_refresh = false) {
$proj = $this->refreshProject($proj, $user, true);
$new_name = $proj->getName().' '.mt_rand();
$xaction = new PhabricatorProjectTransaction();
$xaction->setTransactionType(PhabricatorProjectTransaction::TYPE_NAME);
$xaction->setNewValue($new_name);
$editor = new PhabricatorProjectTransactionEditor();
$editor->setActor($user);
$editor->setContentSource(PhabricatorContentSource::newConsoleSource());
$editor->applyTransactions($proj, array($xaction));
return true;
}
public function testJoinLeaveProject() {
$user = $this->createUser();
$user->save();
$proj = $this->createProjectWithNewAuthor();
$proj = $this->refreshProject($proj, $user, true);
$this->assertTrue(
(bool)$proj,
- 'Assumption that projects are default visible to any user when created.');
+ pht(
+ 'Assumption that projects are default visible '.
+ 'to any user when created.'));
$this->assertFalse(
$proj->isUserMember($user->getPHID()),
- 'Arbitrary user not member of project.');
+ pht('Arbitrary user not member of project.'));
// Join the project.
$this->joinProject($proj, $user);
$proj = $this->refreshProject($proj, $user, true);
$this->assertTrue((bool)$proj);
$this->assertTrue(
$proj->isUserMember($user->getPHID()),
- 'Join works.');
+ pht('Join works.'));
// Join the project again.
$this->joinProject($proj, $user);
$proj = $this->refreshProject($proj, $user, true);
$this->assertTrue((bool)$proj);
$this->assertTrue(
$proj->isUserMember($user->getPHID()),
- 'Joining an already-joined project is a no-op.');
+ pht('Joining an already-joined project is a no-op.'));
// Leave the project.
$this->leaveProject($proj, $user);
$proj = $this->refreshProject($proj, $user, true);
$this->assertTrue((bool)$proj);
$this->assertFalse(
$proj->isUserMember($user->getPHID()),
- 'Leave works.');
+ pht('Leave works.'));
// Leave the project again.
$this->leaveProject($proj, $user);
$proj = $this->refreshProject($proj, $user, true);
$this->assertTrue((bool)$proj);
$this->assertFalse(
$proj->isUserMember($user->getPHID()),
- 'Leaving an already-left project is a no-op.');
+ pht('Leaving an already-left project is a no-op.'));
// If a user can't edit or join a project, joining fails.
$proj->setEditPolicy(PhabricatorPolicies::POLICY_NOONE);
$proj->setJoinPolicy(PhabricatorPolicies::POLICY_NOONE);
$proj->save();
$proj = $this->refreshProject($proj, $user, true);
$caught = null;
try {
$this->joinProject($proj, $user);
} catch (Exception $ex) {
$caught = $ex;
}
$this->assertTrue($ex instanceof Exception);
// If a user can edit a project, they can join.
$proj->setEditPolicy(PhabricatorPolicies::POLICY_USER);
$proj->setJoinPolicy(PhabricatorPolicies::POLICY_NOONE);
$proj->save();
$proj = $this->refreshProject($proj, $user, true);
$this->joinProject($proj, $user);
$proj = $this->refreshProject($proj, $user, true);
$this->assertTrue(
$proj->isUserMember($user->getPHID()),
- 'Join allowed with edit permission.');
+ pht('Join allowed with edit permission.'));
$this->leaveProject($proj, $user);
// If a user can join a project, they can join, even if they can't edit.
$proj->setEditPolicy(PhabricatorPolicies::POLICY_NOONE);
$proj->setJoinPolicy(PhabricatorPolicies::POLICY_USER);
$proj->save();
$proj = $this->refreshProject($proj, $user, true);
$this->joinProject($proj, $user);
$proj = $this->refreshProject($proj, $user, true);
$this->assertTrue(
$proj->isUserMember($user->getPHID()),
- 'Join allowed with join permission.');
+ pht('Join allowed with join permission.'));
// A user can leave a project even if they can't edit it or join.
$proj->setEditPolicy(PhabricatorPolicies::POLICY_NOONE);
$proj->setJoinPolicy(PhabricatorPolicies::POLICY_NOONE);
$proj->save();
$proj = $this->refreshProject($proj, $user, true);
$this->leaveProject($proj, $user);
$proj = $this->refreshProject($proj, $user, true);
$this->assertFalse(
$proj->isUserMember($user->getPHID()),
- 'Leave allowed without any permission.');
+ pht('Leave allowed without any permission.'));
}
private function refreshProject(
PhabricatorProject $project,
PhabricatorUser $viewer,
$need_members = false) {
$results = id(new PhabricatorProjectQuery())
->setViewer($viewer)
->needMembers($need_members)
->withIDs(array($project->getID()))
->execute();
if ($results) {
return head($results);
} else {
return null;
}
}
private function createProject(PhabricatorUser $user) {
$project = PhabricatorProject::initializeNewProject($user);
$project->setName('Test Project '.mt_rand());
$project->save();
return $project;
}
private function createProjectWithNewAuthor() {
$author = $this->createUser();
$author->save();
$project = $this->createProject($author);
return $project;
}
private function createUser() {
$rand = mt_rand();
$user = new PhabricatorUser();
$user->setUsername('unittestuser'.$rand);
$user->setRealName('Unit Test User '.$rand);
return $user;
}
private function joinProject(
PhabricatorProject $project,
PhabricatorUser $user) {
$this->joinOrLeaveProject($project, $user, '+');
}
private function leaveProject(
PhabricatorProject $project,
PhabricatorUser $user) {
$this->joinOrLeaveProject($project, $user, '-');
}
private function joinOrLeaveProject(
PhabricatorProject $project,
PhabricatorUser $user,
$operation) {
$spec = array(
$operation => array($user->getPHID() => $user->getPHID()),
);
$xactions = array();
$xactions[] = id(new PhabricatorProjectTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
->setMetadataValue(
'edge:type',
PhabricatorProjectProjectHasMemberEdgeType::EDGECONST)
->setNewValue($spec);
$editor = id(new PhabricatorProjectTransactionEditor())
->setActor($user)
->setContentSource(PhabricatorContentSource::newConsoleSource())
->setContinueOnNoEffect(true)
->applyTransactions($project, $xactions);
}
}
diff --git a/src/applications/project/mail/ProjectReplyHandler.php b/src/applications/project/mail/ProjectReplyHandler.php
index ac2cf2b6e..7358a3607 100644
--- a/src/applications/project/mail/ProjectReplyHandler.php
+++ b/src/applications/project/mail/ProjectReplyHandler.php
@@ -1,20 +1,21 @@
<?php
final class ProjectReplyHandler
extends PhabricatorApplicationTransactionReplyHandler {
public function validateMailReceiver($mail_receiver) {
if (!($mail_receiver instanceof PhabricatorProject)) {
- throw new Exception('Mail receiver is not a PhabricatorProject.');
+ throw new Exception(
+ pht('Mail receiver is not a %s.', 'PhabricatorProject'));
}
}
public function getObjectPrefix() {
return PhabricatorProjectProjectPHIDType::TYPECONST;
}
protected function shouldCreateCommentFromMailBody() {
return false;
}
}
diff --git a/src/applications/project/query/PhabricatorProjectQuery.php b/src/applications/project/query/PhabricatorProjectQuery.php
index ff3583707..a39313022 100644
--- a/src/applications/project/query/PhabricatorProjectQuery.php
+++ b/src/applications/project/query/PhabricatorProjectQuery.php
@@ -1,393 +1,395 @@
<?php
final class PhabricatorProjectQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $ids;
private $phids;
private $memberPHIDs;
private $slugs;
private $phrictionSlugs;
private $names;
private $nameTokens;
private $icons;
private $colors;
private $status = 'status-any';
const STATUS_ANY = 'status-any';
const STATUS_OPEN = 'status-open';
const STATUS_CLOSED = 'status-closed';
const STATUS_ACTIVE = 'status-active';
const STATUS_ARCHIVED = 'status-archived';
private $needSlugs;
private $needMembers;
private $needWatchers;
private $needImages;
public function withIDs(array $ids) {
$this->ids = $ids;
return $this;
}
public function withPHIDs(array $phids) {
$this->phids = $phids;
return $this;
}
public function withStatus($status) {
$this->status = $status;
return $this;
}
public function withMemberPHIDs(array $member_phids) {
$this->memberPHIDs = $member_phids;
return $this;
}
public function withSlugs(array $slugs) {
$this->slugs = $slugs;
return $this;
}
public function withPhrictionSlugs(array $slugs) {
$this->phrictionSlugs = $slugs;
return $this;
}
public function withNames(array $names) {
$this->names = $names;
return $this;
}
public function withNameTokens(array $tokens) {
$this->nameTokens = array_values($tokens);
return $this;
}
public function withIcons(array $icons) {
$this->icons = $icons;
return $this;
}
public function withColors(array $colors) {
$this->colors = $colors;
return $this;
}
public function needMembers($need_members) {
$this->needMembers = $need_members;
return $this;
}
public function needWatchers($need_watchers) {
$this->needWatchers = $need_watchers;
return $this;
}
public function needImages($need_images) {
$this->needImages = $need_images;
return $this;
}
public function needSlugs($need_slugs) {
$this->needSlugs = $need_slugs;
return $this;
}
protected function getDefaultOrderVector() {
return array('name');
}
public function getOrderableColumns() {
return array(
'name' => array(
'table' => $this->getPrimaryTableAlias(),
'column' => 'name',
'reverse' => true,
'type' => 'string',
'unique' => true,
),
);
}
protected function getPagingValueMap($cursor, array $keys) {
$project = $this->loadCursorObject($cursor);
return array(
'name' => $project->getName(),
);
}
protected function loadPage() {
$table = new PhabricatorProject();
$conn_r = $table->establishConnection('r');
// NOTE: Because visibility checks for projects depend on whether or not
// the user is a project member, we always load their membership. If we're
// loading all members anyway we can piggyback on that; otherwise we
// do an explicit join.
$select_clause = '';
if (!$this->needMembers) {
$select_clause = ', vm.dst viewerIsMember';
}
$data = queryfx_all(
$conn_r,
'SELECT p.* %Q FROM %T p %Q %Q %Q %Q %Q',
$select_clause,
$table->getTableName(),
$this->buildJoinClause($conn_r),
$this->buildWhereClause($conn_r),
$this->buildGroupClause($conn_r),
$this->buildOrderClause($conn_r),
$this->buildLimitClause($conn_r));
$projects = $table->loadAllFromArray($data);
if ($projects) {
$viewer_phid = $this->getViewer()->getPHID();
$project_phids = mpull($projects, 'getPHID');
$member_type = PhabricatorProjectProjectHasMemberEdgeType::EDGECONST;
$watcher_type = PhabricatorObjectHasWatcherEdgeType::EDGECONST;
$need_edge_types = array();
if ($this->needMembers) {
$need_edge_types[] = $member_type;
} else {
foreach ($data as $row) {
$projects[$row['id']]->setIsUserMember(
$viewer_phid,
($row['viewerIsMember'] !== null));
}
}
if ($this->needWatchers) {
$need_edge_types[] = $watcher_type;
}
if ($need_edge_types) {
$edges = id(new PhabricatorEdgeQuery())
->withSourcePHIDs($project_phids)
->withEdgeTypes($need_edge_types)
->execute();
if ($this->needMembers) {
foreach ($projects as $project) {
$phid = $project->getPHID();
$project->attachMemberPHIDs(
array_keys($edges[$phid][$member_type]));
$project->setIsUserMember(
$viewer_phid,
isset($edges[$phid][$member_type][$viewer_phid]));
}
}
if ($this->needWatchers) {
foreach ($projects as $project) {
$phid = $project->getPHID();
$project->attachWatcherPHIDs(
array_keys($edges[$phid][$watcher_type]));
$project->setIsUserWatcher(
$viewer_phid,
isset($edges[$phid][$watcher_type][$viewer_phid]));
}
}
}
}
return $projects;
}
protected function didFilterPage(array $projects) {
if ($this->needImages) {
$default = null;
$file_phids = mpull($projects, 'getProfileImagePHID');
$files = id(new PhabricatorFileQuery())
->setParentQuery($this)
->setViewer($this->getViewer())
->withPHIDs($file_phids)
->execute();
$files = mpull($files, null, 'getPHID');
foreach ($projects as $project) {
$file = idx($files, $project->getProfileImagePHID());
if (!$file) {
if (!$default) {
$default = PhabricatorFile::loadBuiltin(
$this->getViewer(),
'project.png');
}
$file = $default;
}
$project->attachProfileImageFile($file);
}
}
if ($this->needSlugs) {
$slugs = id(new PhabricatorProjectSlug())
->loadAllWhere(
'projectPHID IN (%Ls)',
mpull($projects, 'getPHID'));
$slugs = mgroup($slugs, 'getProjectPHID');
foreach ($projects as $project) {
$project_slugs = idx($slugs, $project->getPHID(), array());
$project->attachSlugs($project_slugs);
}
}
return $projects;
}
protected function buildWhereClause(AphrontDatabaseConnection $conn_r) {
$where = array();
if ($this->status != self::STATUS_ANY) {
switch ($this->status) {
case self::STATUS_OPEN:
case self::STATUS_ACTIVE:
$filter = array(
PhabricatorProjectStatus::STATUS_ACTIVE,
);
break;
case self::STATUS_CLOSED:
case self::STATUS_ARCHIVED:
$filter = array(
PhabricatorProjectStatus::STATUS_ARCHIVED,
);
break;
default:
throw new Exception(
- "Unknown project status '{$this->status}'!");
+ pht(
+ "Unknown project status '%s'!",
+ $this->status));
}
$where[] = qsprintf(
$conn_r,
'status IN (%Ld)',
$filter);
}
if ($this->ids !== null) {
$where[] = qsprintf(
$conn_r,
'id IN (%Ld)',
$this->ids);
}
if ($this->phids !== null) {
$where[] = qsprintf(
$conn_r,
'phid IN (%Ls)',
$this->phids);
}
if ($this->memberPHIDs !== null) {
$where[] = qsprintf(
$conn_r,
'e.dst IN (%Ls)',
$this->memberPHIDs);
}
if ($this->slugs !== null) {
$where[] = qsprintf(
$conn_r,
'slug.slug IN (%Ls)',
$this->slugs);
}
if ($this->phrictionSlugs !== null) {
$where[] = qsprintf(
$conn_r,
'phrictionSlug IN (%Ls)',
$this->phrictionSlugs);
}
if ($this->names !== null) {
$where[] = qsprintf(
$conn_r,
'name IN (%Ls)',
$this->names);
}
if ($this->icons !== null) {
$where[] = qsprintf(
$conn_r,
'icon IN (%Ls)',
$this->icons);
}
if ($this->colors !== null) {
$where[] = qsprintf(
$conn_r,
'color IN (%Ls)',
$this->colors);
}
$where[] = $this->buildPagingClause($conn_r);
return $this->formatWhereClause($where);
}
protected function buildGroupClause(AphrontDatabaseConnection $conn_r) {
if ($this->memberPHIDs || $this->nameTokens) {
return 'GROUP BY p.id';
} else {
return $this->buildApplicationSearchGroupClause($conn_r);
}
}
protected function buildJoinClause(AphrontDatabaseConnection $conn_r) {
$joins = array();
if (!$this->needMembers !== null) {
$joins[] = qsprintf(
$conn_r,
'LEFT JOIN %T vm ON vm.src = p.phid AND vm.type = %d AND vm.dst = %s',
PhabricatorEdgeConfig::TABLE_NAME_EDGE,
PhabricatorProjectProjectHasMemberEdgeType::EDGECONST,
$this->getViewer()->getPHID());
}
if ($this->memberPHIDs !== null) {
$joins[] = qsprintf(
$conn_r,
'JOIN %T e ON e.src = p.phid AND e.type = %d',
PhabricatorEdgeConfig::TABLE_NAME_EDGE,
PhabricatorProjectProjectHasMemberEdgeType::EDGECONST);
}
if ($this->slugs !== null) {
$joins[] = qsprintf(
$conn_r,
'JOIN %T slug on slug.projectPHID = p.phid',
id(new PhabricatorProjectSlug())->getTableName());
}
if ($this->nameTokens !== null) {
foreach ($this->nameTokens as $key => $token) {
$token_table = 'token_'.$key;
$joins[] = qsprintf(
$conn_r,
'JOIN %T %T ON %T.projectID = p.id AND %T.token LIKE %>',
PhabricatorProject::TABLE_DATASOURCE_TOKEN,
$token_table,
$token_table,
$token_table,
$token);
}
}
$joins[] = $this->buildApplicationSearchJoinClause($conn_r);
return implode(' ', $joins);
}
public function getQueryApplicationClass() {
return 'PhabricatorProjectApplication';
}
protected function getPrimaryTableAlias() {
return 'p';
}
}
diff --git a/src/applications/project/remarkup/__tests__/ProjectRemarkupRuleTestCase.php b/src/applications/project/remarkup/__tests__/ProjectRemarkupRuleTestCase.php
index 437b02549..80e809aba 100644
--- a/src/applications/project/remarkup/__tests__/ProjectRemarkupRuleTestCase.php
+++ b/src/applications/project/remarkup/__tests__/ProjectRemarkupRuleTestCase.php
@@ -1,123 +1,123 @@
<?php
final class ProjectRemarkupRuleTestCase extends PhabricatorTestCase {
public function testProjectObjectRemarkup() {
$cases = array(
'I like #ducks.' => array(
'embed' => array(),
'ref' => array(
array(
'offset' => 8,
'id' => 'ducks',
),
),
),
'We should make a post on #blog.example.com tomorrow.' => array(
'embed' => array(),
'ref' => array(
array(
'offset' => 26,
'id' => 'blog.example.com',
),
),
),
'We should make a post on #blog.example.com.' => array(
'embed' => array(),
'ref' => array(
array(
'offset' => 26,
'id' => 'blog.example.com',
),
),
),
'#123' => array(
'embed' => array(),
'ref' => array(),
),
'#security#123' => array(
'embed' => array(),
'ref' => array(
array(
'offset' => 1,
'id' => 'security',
'tail' => '123',
),
),
),
// Don't match a terminal parenthesis. This fixes these constructs in
// natural language.
'There is some documentation (see #guides).' => array(
'embed' => array(),
'ref' => array(
array(
'offset' => 34,
'id' => 'guides',
),
),
),
// Don't match internal parentheses either. This makes the terminal
// parenthesis behavior less arbitrary (otherwise, we match open
// parentheses but not closing parentheses, which is surprising).
'#a(b)c' => array(
'embed' => array(),
'ref' => array(
array(
'offset' => 1,
'id' => 'a',
),
),
),
'#s3' => array(
'embed' => array(),
'ref' => array(
array(
'offset' => 1,
'id' => 's3',
),
),
),
'Is this #urgent?' => array(
'embed' => array(),
'ref' => array(
array(
'offset' => 9,
'id' => 'urgent',
),
),
),
'This is "#urgent".' => array(
'embed' => array(),
'ref' => array(
array(
'offset' => 10,
'id' => 'urgent',
),
),
),
- 'This is \'#urgent\'.' => array(
+ "This is '#urgent'." => array(
'embed' => array(),
'ref' => array(
array(
'offset' => 10,
'id' => 'urgent',
),
),
),
);
foreach ($cases as $input => $expect) {
$rule = new ProjectRemarkupRule();
$matches = $rule->extractReferences($input);
$this->assertEqual($expect, $matches, $input);
}
}
}
diff --git a/src/applications/project/storage/PhabricatorProjectTransaction.php b/src/applications/project/storage/PhabricatorProjectTransaction.php
index 692c41d73..c8698dbe6 100644
--- a/src/applications/project/storage/PhabricatorProjectTransaction.php
+++ b/src/applications/project/storage/PhabricatorProjectTransaction.php
@@ -1,398 +1,398 @@
<?php
final class PhabricatorProjectTransaction
extends PhabricatorApplicationTransaction {
const TYPE_NAME = 'project:name';
const TYPE_SLUGS = 'project:slugs';
const TYPE_STATUS = 'project:status';
const TYPE_IMAGE = 'project:image';
const TYPE_ICON = 'project:icon';
const TYPE_COLOR = 'project:color';
const TYPE_LOCKED = 'project:locked';
// NOTE: This is deprecated, members are just a normal edge now.
const TYPE_MEMBERS = 'project:members';
const MAILTAG_METADATA = 'project-metadata';
const MAILTAG_MEMBERS = 'project-members';
const MAILTAG_WATCHERS = 'project-watchers';
const MAILTAG_OTHER = 'project-other';
public function getApplicationName() {
return 'project';
}
public function getApplicationTransactionType() {
return PhabricatorProjectProjectPHIDType::TYPECONST;
}
public function getRequiredHandlePHIDs() {
$old = $this->getOldValue();
$new = $this->getNewValue();
$req_phids = array();
switch ($this->getTransactionType()) {
case self::TYPE_MEMBERS:
$add = array_diff($new, $old);
$rem = array_diff($old, $new);
$req_phids = array_merge($add, $rem);
break;
case self::TYPE_IMAGE:
$req_phids[] = $old;
$req_phids[] = $new;
break;
}
return array_merge($req_phids, parent::getRequiredHandlePHIDs());
}
public function getColor() {
$old = $this->getOldValue();
$new = $this->getNewValue();
switch ($this->getTransactionType()) {
case self::TYPE_STATUS:
if ($old == 0) {
return 'red';
} else {
return 'green';
}
}
return parent::getColor();
}
public function getIcon() {
$old = $this->getOldValue();
$new = $this->getNewValue();
switch ($this->getTransactionType()) {
case self::TYPE_STATUS:
if ($old == 0) {
return 'fa-ban';
} else {
return 'fa-check';
}
case self::TYPE_LOCKED:
if ($new) {
return 'fa-lock';
} else {
return 'fa-unlock';
}
case self::TYPE_ICON:
return $new;
case self::TYPE_IMAGE:
return 'fa-photo';
case self::TYPE_MEMBERS:
return 'fa-user';
case self::TYPE_SLUGS:
return 'fa-tag';
}
return parent::getIcon();
}
public function getTitle() {
$old = $this->getOldValue();
$new = $this->getNewValue();
$author_handle = $this->renderHandleLink($this->getAuthorPHID());
switch ($this->getTransactionType()) {
case self::TYPE_NAME:
if ($old === null) {
return pht(
'%s created this project.',
$author_handle);
} else {
return pht(
'%s renamed this project from "%s" to "%s".',
$author_handle,
$old,
$new);
}
break;
case self::TYPE_STATUS:
if ($old == 0) {
return pht(
'%s archived this project.',
$author_handle);
} else {
return pht(
'%s activated this project.',
$author_handle);
}
break;
case self::TYPE_IMAGE:
// TODO: Some day, it would be nice to show the images.
if (!$old) {
return pht(
- '%s set this project\'s image to %s.',
+ "%s set this project's image to %s.",
$author_handle,
$this->renderHandleLink($new));
} else if (!$new) {
return pht(
- '%s removed this project\'s image.',
+ "%s removed this project's image.",
$author_handle);
} else {
return pht(
- '%s updated this project\'s image from %s to %s.',
+ "%s updated this project's image from %s to %s.",
$author_handle,
$this->renderHandleLink($old),
$this->renderHandleLink($new));
}
break;
case self::TYPE_ICON:
return pht(
- '%s set this project\'s icon to %s.',
+ "%s set this project's icon to %s.",
$author_handle,
PhabricatorProjectIcon::getLabel($new));
break;
case self::TYPE_COLOR:
return pht(
- '%s set this project\'s color to %s.',
+ "%s set this project's color to %s.",
$author_handle,
PHUITagView::getShadeName($new));
break;
case self::TYPE_LOCKED:
if ($new) {
return pht(
- '%s locked this project\'s membership.',
+ "%s locked this project's membership.",
$author_handle);
} else {
return pht(
- '%s unlocked this project\'s membership.',
+ "%s unlocked this project's membership.",
$author_handle);
}
break;
case self::TYPE_SLUGS:
$add = array_diff($new, $old);
$rem = array_diff($old, $new);
if ($add && $rem) {
return pht(
'%s changed project hashtag(s), added %d: %s; removed %d: %s.',
$author_handle,
count($add),
$this->renderSlugList($add),
count($rem),
$this->renderSlugList($rem));
} else if ($add) {
return pht(
'%s added %d project hashtag(s): %s.',
$author_handle,
count($add),
$this->renderSlugList($add));
} else if ($rem) {
return pht(
'%s removed %d project hashtag(s): %s.',
$author_handle,
count($rem),
$this->renderSlugList($rem));
}
break;
case self::TYPE_MEMBERS:
$add = array_diff($new, $old);
$rem = array_diff($old, $new);
if ($add && $rem) {
return pht(
'%s changed project member(s), added %d: %s; removed %d: %s.',
$author_handle,
count($add),
$this->renderHandleList($add),
count($rem),
$this->renderHandleList($rem));
} else if ($add) {
if (count($add) == 1 && (head($add) == $this->getAuthorPHID())) {
return pht(
'%s joined this project.',
$author_handle);
} else {
return pht(
'%s added %d project member(s): %s.',
$author_handle,
count($add),
$this->renderHandleList($add));
}
} else if ($rem) {
if (count($rem) == 1 && (head($rem) == $this->getAuthorPHID())) {
return pht(
'%s left this project.',
$author_handle);
} else {
return pht(
'%s removed %d project member(s): %s.',
$author_handle,
count($rem),
$this->renderHandleList($rem));
}
}
break;
}
return parent::getTitle();
}
public function getTitleForFeed() {
$author_phid = $this->getAuthorPHID();
$object_phid = $this->getObjectPHID();
$author_handle = $this->renderHandleLink($author_phid);
$object_handle = $this->renderHandleLink($object_phid);
$old = $this->getOldValue();
$new = $this->getNewValue();
switch ($this->getTransactionType()) {
case self::TYPE_NAME:
if ($old === null) {
return pht(
'%s created %s.',
$author_handle,
$object_handle);
} else {
return pht(
'%s renamed %s from "%s" to "%s".',
$author_handle,
$object_handle,
$old,
$new);
}
case self::TYPE_STATUS:
if ($old == 0) {
return pht(
'%s archived %s.',
$author_handle,
$object_handle);
} else {
return pht(
'%s activated %s.',
$author_handle,
$object_handle);
}
case self::TYPE_IMAGE:
// TODO: Some day, it would be nice to show the images.
if (!$old) {
return pht(
'%s set the image for %s to %s.',
$author_handle,
$object_handle,
$this->renderHandleLink($new));
} else if (!$new) {
return pht(
'%s removed the image for %s.',
$author_handle,
$object_handle);
} else {
return pht(
'%s updated the image for %s from %s to %s.',
$author_handle,
$object_handle,
$this->renderHandleLink($old),
$this->renderHandleLink($new));
}
case self::TYPE_ICON:
return pht(
'%s set the icon for %s to %s.',
$author_handle,
$object_handle,
PhabricatorProjectIcon::getLabel($new));
case self::TYPE_COLOR:
return pht(
'%s set the color for %s to %s.',
$author_handle,
$object_handle,
PHUITagView::getShadeName($new));
case self::TYPE_LOCKED:
if ($new) {
return pht(
'%s locked %s membership.',
$author_handle,
$object_handle);
} else {
return pht(
'%s unlocked %s membership.',
$author_handle,
$object_handle);
}
case self::TYPE_SLUGS:
$add = array_diff($new, $old);
$rem = array_diff($old, $new);
if ($add && $rem) {
return pht(
'%s changed %s hashtag(s), added %d: %s; removed %d: %s.',
$author_handle,
$object_handle,
count($add),
$this->renderSlugList($add),
count($rem),
$this->renderSlugList($rem));
} else if ($add) {
return pht(
'%s added %d %s hashtag(s): %s.',
$author_handle,
count($add),
$object_handle,
$this->renderSlugList($add));
} else if ($rem) {
return pht(
'%s removed %d %s hashtag(s): %s.',
$author_handle,
count($rem),
$object_handle,
$this->renderSlugList($rem));
}
}
return parent::getTitleForFeed();
}
public function getMailTags() {
$tags = array();
switch ($this->getTransactionType()) {
case self::TYPE_NAME:
case self::TYPE_SLUGS:
case self::TYPE_IMAGE:
case self::TYPE_ICON:
case self::TYPE_COLOR:
$tags[] = self::MAILTAG_METADATA;
break;
case PhabricatorTransactions::TYPE_EDGE:
$type = $this->getMetadata('edge:type');
$type = head($type);
$type_member = PhabricatorProjectProjectHasMemberEdgeType::EDGECONST;
$type_watcher = PhabricatorObjectHasWatcherEdgeType::EDGECONST;
if ($type == $type_member) {
$tags[] = self::MAILTAG_MEMBERS;
} else if ($type == $type_watcher) {
$tags[] = self::MAILTAG_WATCHERS;
} else {
$tags[] = self::MAILTAG_OTHER;
}
break;
case self::TYPE_STATUS:
case self::TYPE_LOCKED:
default:
$tags[] = self::MAILTAG_OTHER;
break;
}
return $tags;
}
private function renderSlugList($slugs) {
return implode(', ', $slugs);
}
}
diff --git a/src/applications/project/typeahead/PhabricatorProjectLogicalUserDatasource.php b/src/applications/project/typeahead/PhabricatorProjectLogicalUserDatasource.php
index d2cb58a7e..3b02195c2 100644
--- a/src/applications/project/typeahead/PhabricatorProjectLogicalUserDatasource.php
+++ b/src/applications/project/typeahead/PhabricatorProjectLogicalUserDatasource.php
@@ -1,139 +1,138 @@
<?php
final class PhabricatorProjectLogicalUserDatasource
extends PhabricatorTypeaheadCompositeDatasource {
public function getBrowseTitle() {
return pht('Browse User Projects');
}
public function getPlaceholderText() {
return pht('Type projects(<user>)...');
}
public function getDatasourceApplicationClass() {
return 'PhabricatorProjectApplication';
}
public function getComponentDatasources() {
return array(
new PhabricatorPeopleDatasource(),
);
}
public function getDatasourceFunctions() {
return array(
'projects' => array(
'name' => pht('Projects: ...'),
'arguments' => pht('username'),
'summary' => pht("Find results in any of a user's projects."),
'description' => pht(
- 'This function allows you to find results associated with any '.
- 'of the projects a specified user is a member of. For example, '.
- 'this will find results associated with all of the projects '.
- '`alincoln` is a member of:'.
- "\n\n".
- '> projects(alincoln)'.
- "\n\n"),
+ "This function allows you to find results associated with any ".
+ "of the projects a specified user is a member of. For example, ".
+ "this will find results associated with all of the projects ".
+ "`%s` is a member of:\n\n%s\n\n",
+ 'alincoln',
+ '> projects(alincoln)'),
),
);
}
protected function didLoadResults(array $results) {
foreach ($results as $result) {
$result
->setColor(null)
->setTokenType(PhabricatorTypeaheadTokenView::TYPE_FUNCTION)
->setIcon('fa-asterisk')
->setPHID('projects('.$result->getPHID().')')
->setDisplayName(pht("User's Projects: %s", $result->getDisplayName()))
->setName($result->getName().' projects');
}
return $results;
}
protected function evaluateFunction($function, array $argv_list) {
$phids = array();
foreach ($argv_list as $argv) {
$phids[] = head($argv);
}
$phids = $this->resolvePHIDs($phids);
$projects = id(new PhabricatorProjectQuery())
->setViewer($this->getViewer())
->withMemberPHIDs($phids)
->execute();
$results = array();
foreach ($projects as $project) {
$results[] = new PhabricatorQueryConstraint(
PhabricatorQueryConstraint::OPERATOR_OR,
$project->getPHID());
}
return $results;
}
public function renderFunctionTokens($function, array $argv_list) {
$phids = array();
foreach ($argv_list as $argv) {
$phids[] = head($argv);
}
$phids = $this->resolvePHIDs($phids);
$tokens = $this->renderTokens($phids);
foreach ($tokens as $token) {
$token->setColor(null);
if ($token->isInvalid()) {
$token
->setValue(pht("User's Projects: Invalid User"));
} else {
$token
->setIcon('fa-asterisk')
->setTokenType(PhabricatorTypeaheadTokenView::TYPE_FUNCTION)
->setKey('projects('.$token->getKey().')')
->setValue(pht("User's Projects: %s", $token->getValue()));
}
}
return $tokens;
}
private function resolvePHIDs(array $phids) {
// If we have a function like `projects(alincoln)`, try to resolve the
// username first. This won't happen normally, but can be passed in from
// the query string.
// The user might also give us an invalid username. In this case, we
// preserve it and return it in-place so we get an "invalid" token rendered
// in the UI. This shows the user where the issue is and best represents
// the user's input.
$usernames = array();
foreach ($phids as $key => $phid) {
if (phid_get_type($phid) != PhabricatorPeopleUserPHIDType::TYPECONST) {
$usernames[$key] = $phid;
}
}
if ($usernames) {
$users = id(new PhabricatorPeopleQuery())
->setViewer($this->getViewer())
->withUsernames($usernames)
->execute();
$users = mpull($users, null, 'getUsername');
foreach ($usernames as $key => $username) {
$user = idx($users, $username);
if ($user) {
$phids[$key] = $user->getPHID();
}
}
}
return $phids;
}
}
diff --git a/src/applications/project/typeahead/PhabricatorProjectNoProjectsDatasource.php b/src/applications/project/typeahead/PhabricatorProjectNoProjectsDatasource.php
index 731f39efd..156f04605 100644
--- a/src/applications/project/typeahead/PhabricatorProjectNoProjectsDatasource.php
+++ b/src/applications/project/typeahead/PhabricatorProjectNoProjectsDatasource.php
@@ -1,74 +1,73 @@
<?php
final class PhabricatorProjectNoProjectsDatasource
extends PhabricatorTypeaheadDatasource {
public function getBrowseTitle() {
return pht('Browse Not In Any Projects');
}
public function getPlaceholderText() {
return pht('Type "not in any projects"...');
}
public function getDatasourceApplicationClass() {
return 'PhabricatorProjectApplication';
}
public function getDatasourceFunctions() {
return array(
'null' => array(
'name' => pht('Not In Any Projects'),
'summary' => pht('Find results which are not in any projects.'),
'description' => pht(
- 'This function matches results which are not associated with any '.
- 'projects. It is usually most often used to find objects which '.
- 'might have slipped through the cracks and not been organized '.
- 'properly.'.
- "\n\n".
- "> null()"),
+ "This function matches results which are not associated with any ".
+ "projects. It is usually most often used to find objects which ".
+ "might have slipped through the cracks and not been organized ".
+ "properly.\n\n%s",
+ '> null()'),
),
);
}
public function loadResults() {
$results = array(
$this->buildNullResult(),
);
return $this->filterResultsAgainstTokens($results);
}
protected function evaluateFunction($function, array $argv_list) {
$results = array();
foreach ($argv_list as $argv) {
$results[] = new PhabricatorQueryConstraint(
PhabricatorQueryConstraint::OPERATOR_NULL,
'empty');
}
return $results;
}
public function renderFunctionTokens($function, array $argv_list) {
$results = array();
foreach ($argv_list as $argv) {
$results[] = PhabricatorTypeaheadTokenView::newFromTypeaheadResult(
$this->buildNullResult());
}
return $results;
}
private function buildNullResult() {
$name = pht('Not In Any Projects');
return $this->newFunctionResult()
->setUnique(true)
->setPHID('null()')
->setIcon('fa-ban')
->setName('null '.$name)
->setDisplayName($name);
}
}
diff --git a/src/applications/releeph/commitfinder/ReleephCommitFinder.php b/src/applications/releeph/commitfinder/ReleephCommitFinder.php
index 7b017f028..89fd84e19 100644
--- a/src/applications/releeph/commitfinder/ReleephCommitFinder.php
+++ b/src/applications/releeph/commitfinder/ReleephCommitFinder.php
@@ -1,110 +1,120 @@
<?php
final class ReleephCommitFinder {
private $releephProject;
private $user;
private $objectPHID;
public function setUser(PhabricatorUser $user) {
$this->user = $user;
return $this;
}
public function getUser() {
return $this->user;
}
public function setReleephProject(ReleephProject $rp) {
$this->releephProject = $rp;
return $this;
}
public function getRequestedObjectPHID() {
return $this->objectPHID;
}
public function fromPartial($partial_string) {
$this->objectPHID = null;
// Look for diffs
$matches = array();
if (preg_match('/^D([1-9]\d*)$/', $partial_string, $matches)) {
$diff_id = $matches[1];
$diff_rev = id(new DifferentialRevisionQuery())
->setViewer($this->getUser())
->withIDs(array($diff_id))
->needCommitPHIDs(true)
->executeOne();
if (!$diff_rev) {
throw new ReleephCommitFinderException(
- "{$partial_string} does not refer to an existing diff.");
+ pht(
+ '%s does not refer to an existing diff.',
+ $partial_string));
}
$commit_phids = $diff_rev->getCommitPHIDs();
if (!$commit_phids) {
throw new ReleephCommitFinderException(
- "{$partial_string} has no commits associated with it yet.");
+ pht(
+ '%s has no commits associated with it yet.',
+ $partial_string));
}
$this->objectPHID = $diff_rev->getPHID();
$commits = id(new DiffusionCommitQuery())
->setViewer($this->getUser())
->withPHIDs($commit_phids)
->execute();
$commits = msort($commits, 'getEpoch');
return head($commits);
}
// Look for a raw commit number, or r<callsign><commit-number>.
$repository = $this->releephProject->getRepository();
$dr_data = null;
$matches = array();
if (preg_match('/^r(?P<callsign>[A-Z]+)(?P<commit>\w+)$/',
$partial_string, $matches)) {
$callsign = $matches['callsign'];
if ($callsign != $repository->getCallsign()) {
- throw new ReleephCommitFinderException(sprintf(
- '%s is in a different repository to this Releeph project (%s).',
- $partial_string,
- $repository->getCallsign()));
+ throw new ReleephCommitFinderException(
+ pht(
+ '%s is in a different repository to this Releeph project (%s).',
+ $partial_string,
+ $repository->getCallsign()));
} else {
$dr_data = $matches;
}
} else {
$dr_data = array(
'callsign' => $repository->getCallsign(),
'commit' => $partial_string,
);
}
try {
$dr_data['user'] = $this->getUser();
$dr = DiffusionRequest::newFromDictionary($dr_data);
} catch (Exception $ex) {
- $message = "No commit matches {$partial_string}: ".$ex->getMessage();
+ $message = pht(
+ 'No commit matches %s: %s',
+ $partial_string,
+ $ex->getMessage());
throw new ReleephCommitFinderException($message);
}
$phabricator_repository_commit = $dr->loadCommit();
if (!$phabricator_repository_commit) {
throw new ReleephCommitFinderException(
- "The commit {$partial_string} doesn't exist in this repository.");
+ pht(
+ "The commit %s doesn't exist in this repository.",
+ $partial_string));
}
// When requesting a single commit, if it has an associated review we
// imply the review was requested instead. This is always correct for now
// and consistent with the older behavior, although it might not be the
// right rule in the future.
$phids = PhabricatorEdgeQuery::loadDestinationPHIDs(
$phabricator_repository_commit->getPHID(),
DiffusionCommitHasRevisionEdgeType::EDGECONST);
if ($phids) {
$this->objectPHID = head($phids);
}
return $phabricator_repository_commit;
}
}
diff --git a/src/applications/releeph/conduit/ReleephGetBranchesConduitAPIMethod.php b/src/applications/releeph/conduit/ReleephGetBranchesConduitAPIMethod.php
index 59e890010..6e225ec8e 100644
--- a/src/applications/releeph/conduit/ReleephGetBranchesConduitAPIMethod.php
+++ b/src/applications/releeph/conduit/ReleephGetBranchesConduitAPIMethod.php
@@ -1,61 +1,61 @@
<?php
final class ReleephGetBranchesConduitAPIMethod extends ReleephConduitAPIMethod {
public function getAPIMethodName() {
return 'releeph.getbranches';
}
public function getMethodDescription() {
- return 'Return information about all active Releeph branches.';
+ return pht('Return information about all active Releeph branches.');
}
protected function defineParamTypes() {
return array(
);
}
protected function defineReturnType() {
return 'nonempty list<dict<string, wild>>';
}
protected function execute(ConduitAPIRequest $request) {
$results = array();
$projects = id(new ReleephProductQuery())
->setViewer($request->getUser())
->withActive(1)
->execute();
foreach ($projects as $project) {
$repository = $project->getRepository();
$branches = $project->loadRelatives(
id(new ReleephBranch()),
'releephProjectID',
'getID',
'isActive = 1');
foreach ($branches as $branch) {
$full_branch_name = $branch->getName();
$cut_point_commit = $branch->loadOneRelative(
id(new PhabricatorRepositoryCommit()),
'phid',
'getCutPointCommitPHID');
$results[] = array(
'project' => $project->getName(),
'repository' => $repository->getCallsign(),
'branch' => $branch->getBasename(),
'fullBranchName' => $full_branch_name,
'symbolicName' => $branch->getSymbolicName(),
'cutPoint' => $cut_point_commit->getCommitIdentifier(),
);
}
}
return $results;
}
}
diff --git a/src/applications/releeph/conduit/ReleephProjectInfoConduitAPIMethod.php b/src/applications/releeph/conduit/ReleephProjectInfoConduitAPIMethod.php
index f30d1070c..0dc45391c 100644
--- a/src/applications/releeph/conduit/ReleephProjectInfoConduitAPIMethod.php
+++ b/src/applications/releeph/conduit/ReleephProjectInfoConduitAPIMethod.php
@@ -1,98 +1,100 @@
<?php
final class ReleephProjectInfoConduitAPIMethod extends ReleephConduitAPIMethod {
public function getAPIMethodName() {
return 'releeph.projectinfo';
}
public function getMethodDescription() {
- return
+ return pht(
'Fetch information about all Releeph projects '.
- 'for a given Arcanist project.';
+ 'for a given Arcanist project.');
}
protected function defineParamTypes() {
return array(
'arcProjectName' => 'optional string',
);
}
protected function defineReturnType() {
return 'dict<string, wild>';
}
protected function defineErrorTypes() {
return array(
- 'ERR_UNKNOWN_ARC' =>
+ 'ERR_UNKNOWN_ARC' => pht(
"The given Arcanist project name doesn't exist in the ".
- "installation of Phabricator you are accessing.",
+ "installation of Phabricator you are accessing."),
);
}
protected function execute(ConduitAPIRequest $request) {
$arc_project_name = $request->getValue('arcProjectName');
if ($arc_project_name) {
$arc_project = id(new PhabricatorRepositoryArcanistProject())
->loadOneWhere('name = %s', $arc_project_name);
if (!$arc_project) {
throw id(new ConduitException('ERR_UNKNOWN_ARC'))
->setErrorDescription(
- "Unknown Arcanist project '{$arc_project_name}': ".
- "are you using the correct Conduit URI?");
+ pht(
+ "Unknown Arcanist project '%s': ".
+ "are you using the correct Conduit URI?",
+ $arc_project_name));
}
$releeph_projects = id(new ReleephProject())
->loadAllWhere('arcanistProjectID = %d', $arc_project->getID());
} else {
$releeph_projects = id(new ReleephProject())->loadAll();
}
$releeph_projects = mfilter($releeph_projects, 'getIsActive');
$result = array();
foreach ($releeph_projects as $releeph_project) {
$selector = $releeph_project->getReleephFieldSelector();
$fields = $selector->getFieldSpecifications();
$fields_info = array();
foreach ($fields as $field) {
$field->setReleephProject($releeph_project);
if ($field->isEditable()) {
$key = $field->getKeyForConduit();
$fields_info[$key] = array(
'class' => get_class($field),
'name' => $field->getName(),
'key' => $key,
'arcHelp' => $field->renderHelpForArcanist(),
);
}
}
$releeph_branches = mfilter(
id(new ReleephBranch())
->loadAllWhere('releephProjectID = %d', $releeph_project->getID()),
'getIsActive');
$releeph_branches_struct = array();
foreach ($releeph_branches as $branch) {
$releeph_branches_struct[] = array(
'branchName' => $branch->getName(),
'projectName' => $releeph_project->getName(),
'projectPHID' => $releeph_project->getPHID(),
'branchPHID' => $branch->getPHID(),
);
}
$result[] = array(
'projectName' => $releeph_project->getName(),
'projectPHID' => $releeph_project->getPHID(),
'branches' => $releeph_branches_struct,
'fields' => $fields_info,
);
}
return $result;
}
}
diff --git a/src/applications/releeph/conduit/ReleephQueryRequestsConduitAPIMethod.php b/src/applications/releeph/conduit/ReleephQueryRequestsConduitAPIMethod.php
index 5ccc8a967..5cd5032c2 100644
--- a/src/applications/releeph/conduit/ReleephQueryRequestsConduitAPIMethod.php
+++ b/src/applications/releeph/conduit/ReleephQueryRequestsConduitAPIMethod.php
@@ -1,77 +1,77 @@
<?php
final class ReleephQueryRequestsConduitAPIMethod
extends ReleephConduitAPIMethod {
public function getAPIMethodName() {
return 'releeph.queryrequests';
}
public function getMethodDescription() {
- return
- 'Return information about all Releeph requests linked to the given ids.';
+ return pht(
+ 'Return information about all Releeph requests linked to the given ids.');
}
protected function defineParamTypes() {
return array(
'revisionPHIDs' => 'optional list<phid>',
'requestedCommitPHIDs' => 'optional list<phid>',
);
}
protected function defineReturnType() {
return 'dict<string, wild>';
}
protected function execute(ConduitAPIRequest $conduit_request) {
$revision_phids = $conduit_request->getValue('revisionPHIDs');
$requested_commit_phids =
$conduit_request->getValue('requestedCommitPHIDs');
$result = array();
if (!$revision_phids && !$requested_commit_phids) {
return $result;
}
$query = new ReleephRequestQuery();
$query->setViewer($conduit_request->getUser());
if ($revision_phids) {
$query->withRequestedObjectPHIDs($revision_phids);
} else if ($requested_commit_phids) {
$query->withRequestedCommitPHIDs($requested_commit_phids);
}
$releeph_requests = $query->execute();
foreach ($releeph_requests as $releeph_request) {
$branch = $releeph_request->getBranch();
$request_commit_phid = $releeph_request->getRequestCommitPHID();
$object = $releeph_request->getRequestedObject();
if ($object instanceof DifferentialRevision) {
$object_phid = $object->getPHID();
} else {
$object_phid = null;
}
$status = $releeph_request->getStatus();
$status_name = ReleephRequestStatus::getStatusDescriptionFor($status);
$url = PhabricatorEnv::getProductionURI('/RQ'.$releeph_request->getID());
$result[] = array(
'branchBasename' => $branch->getBasename(),
'branchSymbolic' => $branch->getSymbolicName(),
'requestID' => $releeph_request->getID(),
'revisionPHID' => $object_phid,
'status' => $status,
'status_name' => $status_name,
'url' => $url,
);
}
return $result;
}
}
diff --git a/src/applications/releeph/conduit/ReleephRequestConduitAPIMethod.php b/src/applications/releeph/conduit/ReleephRequestConduitAPIMethod.php
index f60e40894..8473c0c7b 100644
--- a/src/applications/releeph/conduit/ReleephRequestConduitAPIMethod.php
+++ b/src/applications/releeph/conduit/ReleephRequestConduitAPIMethod.php
@@ -1,169 +1,172 @@
<?php
final class ReleephRequestConduitAPIMethod extends ReleephConduitAPIMethod {
public function getAPIMethodName() {
return 'releeph.request';
}
public function getMethodDescription() {
- return 'Request a commit or diff to be picked to a branch.';
+ return pht('Request a commit or diff to be picked to a branch.');
}
protected function defineParamTypes() {
return array(
'branchPHID' => 'required string',
'things' => 'required list<string>',
'fields' => 'dict<string, string>',
);
}
protected function defineReturnType() {
return 'dict<string, wild>';
}
protected function defineErrorTypes() {
return array(
- 'ERR_BRANCH' => 'Unknown Releeph branch.',
- 'ERR_FIELD_PARSE' => 'Unable to parse a Releeph field.',
+ 'ERR_BRANCH' => pht('Unknown Releeph branch.'),
+ 'ERR_FIELD_PARSE' => pht('Unable to parse a Releeph field.'),
);
}
protected function execute(ConduitAPIRequest $request) {
$user = $request->getUser();
$viewer_handle = id(new PhabricatorHandleQuery())
->setViewer($user)
->withPHIDs(array($user->getPHID()))
->executeOne();
$branch_phid = $request->getValue('branchPHID');
$releeph_branch = id(new ReleephBranchQuery())
->setViewer($user)
->withPHIDs(array($branch_phid))
->executeOne();
if (!$releeph_branch) {
throw id(new ConduitException('ERR_BRANCH'))->setErrorDescription(
- "No ReleephBranch found with PHID {$branch_phid}!");
+ pht(
+ 'No %s found with PHID %s!',
+ 'ReleephBranch',
+ $branch_phid));
}
$releeph_project = $releeph_branch->getProduct();
// Find the requested commit identifiers
$requested_commits = array();
$requested_object_phids = array();
$things = $request->getValue('things');
$finder = id(new ReleephCommitFinder())
->setUser($user)
->setReleephProject($releeph_project);
foreach ($things as $thing) {
try {
$requested_commits[$thing] = $finder->fromPartial($thing);
$object_phid = $finder->getRequestedObjectPHID();
if (!$object_phid) {
$object_phid = $requested_commits[$thing]->getPHID();
}
$requested_object_phids[$thing] = $object_phid;
} catch (ReleephCommitFinderException $ex) {
throw id(new ConduitException('ERR_NO_MATCHES'))
->setErrorDescription($ex->getMessage());
}
}
$requested_commit_phids = mpull($requested_commits, 'getPHID');
// Find any existing requests that clash on the commit id, for this branch
$existing_releeph_requests = id(new ReleephRequest())->loadAllWhere(
'requestCommitPHID IN (%Ls) AND branchID = %d',
$requested_commit_phids,
$releeph_branch->getID());
$existing_releeph_requests = mpull(
$existing_releeph_requests,
null,
'getRequestCommitPHID');
$selector = $releeph_project->getReleephFieldSelector();
$fields = $selector->getFieldSpecifications();
foreach ($fields as $field) {
$field
->setReleephProject($releeph_project)
->setReleephBranch($releeph_branch);
}
$results = array();
$handles = id(new PhabricatorHandleQuery())
->setViewer($user)
->withPHIDs($requested_commit_phids)
->execute();
foreach ($requested_commits as $thing => $commit) {
$phid = $commit->getPHID();
$name = id($handles[$phid])->getName();
$releeph_request = null;
$existing_releeph_request = idx($existing_releeph_requests, $phid);
if ($existing_releeph_request) {
$releeph_request = $existing_releeph_request;
} else {
$releeph_request = id(new ReleephRequest())
->setRequestUserPHID($user->getPHID())
->setBranchID($releeph_branch->getID())
->setInBranch(0)
->setRequestedObjectPHID($requested_object_phids[$thing]);
$xactions = array();
$xactions[] = id(new ReleephRequestTransaction())
->setTransactionType(ReleephRequestTransaction::TYPE_REQUEST)
->setNewValue($commit->getPHID());
$xactions[] = id(new ReleephRequestTransaction())
->setTransactionType(ReleephRequestTransaction::TYPE_USER_INTENT)
->setMetadataValue('userPHID', $user->getPHID())
->setMetadataValue(
'isAuthoritative',
$releeph_project->isAuthoritative($user))
->setNewValue(ReleephRequest::INTENT_WANT);
foreach ($fields as $field) {
if (!$field->isEditable()) {
continue;
}
$field->setReleephRequest($releeph_request);
try {
$field->setValueFromConduitAPIRequest($request);
} catch (ReleephFieldParseException $ex) {
throw id(new ConduitException('ERR_FIELD_PARSE'))
->setErrorDescription($ex->getMessage());
}
}
$editor = id(new ReleephRequestTransactionalEditor())
->setActor($user)
->setContinueOnNoEffect(true)
->setContentSource(
PhabricatorContentSource::newForSource(
PhabricatorContentSource::SOURCE_CONDUIT,
array()));
$editor->applyTransactions($releeph_request, $xactions);
}
$url = PhabricatorEnv::getProductionURI('/Y'.$releeph_request->getID());
$results[$thing] = array(
'thing' => $thing,
'branch' => $releeph_branch->getDisplayNameWithDetail(),
'commitName' => $name,
'commitID' => $commit->getCommitIdentifier(),
'url' => $url,
'requestID' => $releeph_request->getID(),
'requestor' => $viewer_handle->getName(),
'requestTime' => $releeph_request->getDateCreated(),
'existing' => $existing_releeph_request !== null,
);
}
return $results;
}
}
diff --git a/src/applications/releeph/conduit/work/ReleephWorkCanPushConduitAPIMethod.php b/src/applications/releeph/conduit/work/ReleephWorkCanPushConduitAPIMethod.php
index 339f0ac5e..008620957 100644
--- a/src/applications/releeph/conduit/work/ReleephWorkCanPushConduitAPIMethod.php
+++ b/src/applications/releeph/conduit/work/ReleephWorkCanPushConduitAPIMethod.php
@@ -1,34 +1,34 @@
<?php
final class ReleephWorkCanPushConduitAPIMethod extends ReleephConduitAPIMethod {
public function getAPIMethodName() {
return 'releephwork.canpush';
}
public function getMethodStatus() {
return self::METHOD_STATUS_UNSTABLE;
}
public function getMethodDescription() {
- return 'Return whether the conduit user is allowed to push.';
+ return pht('Return whether the conduit user is allowed to push.');
}
protected function defineParamTypes() {
return array(
'projectPHID' => 'required string',
);
}
protected function defineReturnType() {
return 'bool';
}
protected function execute(ConduitAPIRequest $request) {
$releeph_project = id(new ReleephProject())
->loadOneWhere('phid = %s', $request->getValue('projectPHID'));
$user = $request->getUser();
return $releeph_project->isAuthoritative($user);
}
}
diff --git a/src/applications/releeph/conduit/work/ReleephWorkGetAuthorInfoConduitAPIMethod.php b/src/applications/releeph/conduit/work/ReleephWorkGetAuthorInfoConduitAPIMethod.php
index 08381befb..6db9fd76e 100644
--- a/src/applications/releeph/conduit/work/ReleephWorkGetAuthorInfoConduitAPIMethod.php
+++ b/src/applications/releeph/conduit/work/ReleephWorkGetAuthorInfoConduitAPIMethod.php
@@ -1,44 +1,44 @@
<?php
final class ReleephWorkGetAuthorInfoConduitAPIMethod
extends ReleephConduitAPIMethod {
public function getAPIMethodName() {
return 'releephwork.getauthorinfo';
}
public function getMethodStatus() {
return self::METHOD_STATUS_UNSTABLE;
}
public function getMethodDescription() {
- return 'Return a string to use as the VCS author.';
+ return pht('Return a string to use as the VCS author.');
}
protected function defineParamTypes() {
return array(
'userPHID' => 'required string',
'vcsType' => 'required string',
);
}
protected function defineReturnType() {
return 'nonempty string';
}
protected function execute(ConduitAPIRequest $request) {
$user = id(new PhabricatorUser())
->loadOneWhere('phid = %s', $request->getValue('userPHID'));
$email = $user->loadPrimaryEmailAddress();
if (is_numeric($email)) {
$email = $user->getUserName().'@fb.com';
}
return sprintf(
'%s <%s>',
$user->getRealName(),
$email);
}
}
diff --git a/src/applications/releeph/conduit/work/ReleephWorkGetBranchCommitMessageConduitAPIMethod.php b/src/applications/releeph/conduit/work/ReleephWorkGetBranchCommitMessageConduitAPIMethod.php
index 20d86a003..43bdd0416 100644
--- a/src/applications/releeph/conduit/work/ReleephWorkGetBranchCommitMessageConduitAPIMethod.php
+++ b/src/applications/releeph/conduit/work/ReleephWorkGetBranchCommitMessageConduitAPIMethod.php
@@ -1,100 +1,104 @@
<?php
final class ReleephWorkGetBranchCommitMessageConduitAPIMethod
extends ReleephConduitAPIMethod {
public function getAPIMethodName() {
return 'releephwork.getbranchcommitmessage';
}
public function getMethodStatus() {
return self::METHOD_STATUS_UNSTABLE;
}
public function getMethodDescription() {
- return 'Get a commit message for committing a Releeph branch.';
+ return pht('Get a commit message for committing a Releeph branch.');
}
protected function defineParamTypes() {
return array(
'branchPHID' => 'required string',
);
}
protected function defineReturnType() {
return 'nonempty string';
}
protected function execute(ConduitAPIRequest $request) {
$viewer = $request->getUser();
$branch = id(new ReleephBranchQuery())
->setViewer($viewer)
->withPHIDs(array($request->getValue('branchPHID')))
->executeOne();
$project = $branch->getProduct();
$creator_phid = $branch->getCreatedByUserPHID();
$cut_phid = $branch->getCutPointCommitPHID();
$phids = array(
$branch->getPHID(),
$project->getPHID(),
$creator_phid,
$cut_phid,
);
$handles = id(new PhabricatorHandleQuery())
->setViewer($request->getUser())
->withPHIDs($phids)
->execute();
$h_branch = $handles[$branch->getPHID()];
$h_project = $handles[$project->getPHID()];
// Not as customizable as a ReleephRequest's commit message. It doesn't
// really need to be.
// TODO: Yes it does, see FB-specific stuff below.
$commit_message = array();
$commit_message[] = $h_branch->getFullName();
$commit_message[] = $h_branch->getURI();
- $commit_message[] = 'Cut Point: '.$handles[$cut_phid]->getName();
+ $commit_message[] = pht('Cut Point: %s', $handles[$cut_phid]->getName());
$cut_point_pr_commit = id(new PhabricatorRepositoryCommit())
->loadOneWhere('phid = %s', $cut_phid);
$cut_point_commit_date = strftime(
'%Y-%m-%d %H:%M:%S%z',
$cut_point_pr_commit->getEpoch());
- $commit_message[] = "Cut Point Date: {$cut_point_commit_date}";
+ $commit_message[] = pht('Cut Point Date: %s', $cut_point_commit_date);
- $commit_message[] = 'Created By: '.$handles[$creator_phid]->getName();
+ $commit_message[] = pht(
+ 'Created By: %s',
+ $handles[$creator_phid]->getName());
$project_uri = $project->getURI();
- $commit_message[] = 'Project: '.$h_project->getName().' '.$project_uri;
+ $commit_message[] = pht(
+ 'Project: %s',
+ $h_project->getName().' '.$project_uri);
/**
* Required for 090-limit_new_branch_creations.sh in
* admin/scripts/git/hosting/hooks/update.d (in the E repo):
*
* http://fburl.com/2372545
*
* The commit message must have a line saying:
*
* @new-branch: <branch-name>
*
*/
$repo = $project->getRepository();
switch ($repo->getVersionControlSystem()) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
$commit_message[] = sprintf(
'@new-branch: %s',
$branch->getName());
break;
}
return implode("\n\n", $commit_message);
}
}
diff --git a/src/applications/releeph/conduit/work/ReleephWorkGetBranchConduitAPIMethod.php b/src/applications/releeph/conduit/work/ReleephWorkGetBranchConduitAPIMethod.php
index 5f6f82b17..794171052 100644
--- a/src/applications/releeph/conduit/work/ReleephWorkGetBranchConduitAPIMethod.php
+++ b/src/applications/releeph/conduit/work/ReleephWorkGetBranchConduitAPIMethod.php
@@ -1,57 +1,57 @@
<?php
final class ReleephWorkGetBranchConduitAPIMethod
extends ReleephConduitAPIMethod {
public function getAPIMethodName() {
return 'releephwork.getbranch';
}
public function getMethodStatus() {
return self::METHOD_STATUS_UNSTABLE;
}
public function getMethodDescription() {
- return 'Return information to help checkout / cut a Releeph branch.';
+ return pht('Return information to help checkout / cut a Releeph branch.');
}
protected function defineParamTypes() {
return array(
'branchPHID' => 'required string',
);
}
protected function defineReturnType() {
return 'dict<string, wild>';
}
protected function execute(ConduitAPIRequest $request) {
$branch = id(new ReleephBranchQuery())
->setViewer($request->getUser())
->withPHIDs(array($request->getValue('branchPHID')))
->needCutPointCommits(true)
->executeOne();
$cut_phid = $branch->getCutPointCommitPHID();
$phids = array($cut_phid);
$handles = id(new PhabricatorHandleQuery())
->setViewer($request->getUser())
->withPHIDs($phids)
->execute();
$project = $branch->getProject();
$repo = $project->getRepository();
$commit = $branch->getCutPointCommit();
return array(
'branchName' => $branch->getName(),
'branchPHID' => $branch->getPHID(),
'vcsType' => $repo->getVersionControlSystem(),
'cutCommitID' => $commit->getCommitIdentifier(),
'cutCommitName' => $handles[$cut_phid]->getName(),
'creatorPHID' => $branch->getCreatedByUserPHID(),
'trunk' => $project->getTrunkBranch(),
);
}
}
diff --git a/src/applications/releeph/conduit/work/ReleephWorkGetCommitMessageConduitAPIMethod.php b/src/applications/releeph/conduit/work/ReleephWorkGetCommitMessageConduitAPIMethod.php
index 398f37e7a..7f645c4be 100644
--- a/src/applications/releeph/conduit/work/ReleephWorkGetCommitMessageConduitAPIMethod.php
+++ b/src/applications/releeph/conduit/work/ReleephWorkGetCommitMessageConduitAPIMethod.php
@@ -1,96 +1,96 @@
<?php
final class ReleephWorkGetCommitMessageConduitAPIMethod
extends ReleephConduitAPIMethod {
public function getAPIMethodName() {
return 'releephwork.getcommitmessage';
}
public function getMethodStatus() {
return self::METHOD_STATUS_UNSTABLE;
}
public function getMethodDescription() {
- return
- 'Get commit message components for building '.
- 'a ReleephRequest commit message.';
+ return pht(
+ 'Get commit message components for building a %s commit message.',
+ 'ReleephRequest');
}
protected function defineParamTypes() {
$action_const = $this->formatStringConstants(array('pick', 'revert'));
return array(
'requestPHID' => 'required string',
'action' => 'required '.$action_const,
);
}
protected function defineReturnType() {
return 'dict<string, string>';
}
protected function execute(ConduitAPIRequest $request) {
$viewer = $request->getUser();
$releeph_request = id(new ReleephRequestQuery())
->setViewer($viewer)
->withPHIDs(array($request->getValue('requestPHID')))
->executeOne();
$action = $request->getValue('action');
$title = $releeph_request->getSummaryForDisplay();
$commit_message = array();
$branch = $releeph_request->getBranch();
$project = $branch->getProduct();
$selector = $project->getReleephFieldSelector();
$fields = $selector->getFieldSpecifications();
$fields = $selector->sortFieldsForCommitMessage($fields);
foreach ($fields as $field) {
$field
->setUser($request->getUser())
->setReleephProject($project)
->setReleephBranch($branch)
->setReleephRequest($releeph_request);
$label = null;
$value = null;
switch ($action) {
case 'pick':
if ($field->shouldAppearOnCommitMessage()) {
$label = $field->renderLabelForCommitMessage();
$value = $field->renderValueForCommitMessage();
}
break;
case 'revert':
if ($field->shouldAppearOnRevertMessage()) {
$label = $field->renderLabelForRevertMessage();
$value = $field->renderValueForRevertMessage();
}
break;
}
if ($label && $value) {
if (strpos($value, "\n") !== false ||
substr($value, 0, 2) === ' ') {
$commit_message[] = "{$label}:\n{$value}";
} else {
$commit_message[] = "{$label}: {$value}";
}
}
}
return array(
'title' => $title,
'body' => implode("\n\n", $commit_message),
);
}
}
diff --git a/src/applications/releeph/conduit/work/ReleephWorkNextRequestConduitAPIMethod.php b/src/applications/releeph/conduit/work/ReleephWorkNextRequestConduitAPIMethod.php
index e161faf08..fb8a8ad0d 100644
--- a/src/applications/releeph/conduit/work/ReleephWorkNextRequestConduitAPIMethod.php
+++ b/src/applications/releeph/conduit/work/ReleephWorkNextRequestConduitAPIMethod.php
@@ -1,228 +1,228 @@
<?php
final class ReleephWorkNextRequestConduitAPIMethod
extends ReleephConduitAPIMethod {
private $project;
private $branch;
public function getAPIMethodName() {
return 'releephwork.nextrequest';
}
public function getMethodStatus() {
return self::METHOD_STATUS_UNSTABLE;
}
public function getMethodDescription() {
- return
- 'Return info required to cut a branch, '.
- 'and pick and revert ReleephRequests';
+ return pht(
+ 'Return info required to cut a branch, and pick and revert %s.',
+ 'ReleephRequests');
}
protected function defineParamTypes() {
return array(
'branchPHID' => 'required phid',
'seen' => 'required map<string, bool>',
);
}
protected function defineReturnType() {
return '';
}
protected function defineErrorTypes() {
return array(
- 'ERR-NOT-PUSHER' =>
- 'You are not listed as a pusher for thie Releeph project!',
+ 'ERR-NOT-PUSHER' => pht(
+ 'You are not listed as a pusher for the Releeph project!'),
);
}
protected function execute(ConduitAPIRequest $request) {
$viewer = $request->getUser();
$seen = $request->getValue('seen');
$branch = id(new ReleephBranchQuery())
->setViewer($viewer)
->withPHIDs(array($request->getValue('branchPHID')))
->executeOne();
$project = $branch->getProduct();
$needs_pick = array();
$needs_revert = array();
// Load every request ever made for this branch...?!!!
$releeph_requests = id(new ReleephRequestQuery())
->setViewer($viewer)
->withBranchIDs(array($branch->getID()))
->execute();
foreach ($releeph_requests as $candidate) {
$phid = $candidate->getPHID();
if (idx($seen, $phid)) {
continue;
}
$should = $candidate->shouldBeInBranch();
$in = $candidate->getInBranch();
if ($should && !$in) {
$needs_pick[] = $candidate;
}
if (!$should && $in) {
$needs_revert[] = $candidate;
}
}
/**
* Sort both needs_pick and needs_revert in ascending commit order, as
* discovered by Phabricator (using the `id` column to perform that
* ordering).
*
* This is easy for $needs_pick as the ordinal is stored. It is hard for
* reverts, as we have to look that information up.
*/
$needs_pick = $this->sortPicks($needs_pick);
$needs_revert = $this->sortReverts($needs_revert);
/**
* Do reverts first in reverse order, then the picks in original-commit
* order.
*
* This seems like the correct thing to do, but there may be a better
* algorithm for the releephwork.nextrequest Conduit call that orders
* things better.
*
* We could also button-mash our way through everything that failed (at the
* end of the run) to try failed things again.
*/
$releeph_request = null;
$action = null;
if ($needs_revert) {
$releeph_request = last($needs_revert);
$action = 'revert';
$commit_id = $releeph_request->getCommitIdentifier();
$commit_phid = $releeph_request->getCommitPHID();
} else if ($needs_pick) {
$releeph_request = head($needs_pick);
$action = 'pick';
$commit = $releeph_request->loadPhabricatorRepositoryCommit();
$commit_id = $commit->getCommitIdentifier();
$commit_phid = $commit->getPHID();
} else {
// Return early if there's nothing to do!
return array();
}
// Build the response
$phids = array();
$phids[] = $commit_phid;
$diff_phid = null;
$diff_rev_id = null;
$requested_object = $releeph_request->getRequestedObject();
if ($requested_object instanceof DifferentialRevision) {
$diff_rev = $requested_object;
} else {
$diff_rev = null;
}
if ($diff_rev) {
$diff_phid = $diff_rev->getPHID();
$phids[] = $diff_phid;
$diff_rev_id = $diff_rev->getID();
}
$phids[] = $releeph_request->getPHID();
$handles = id(new PhabricatorHandleQuery())
->setViewer($request->getUser())
->withPHIDs($phids)
->execute();
$diff_name = null;
if ($diff_rev) {
$diff_name = $handles[$diff_phid]->getName();
}
$new_author_phid = null;
if ($diff_rev) {
$new_author_phid = $diff_rev->getAuthorPHID();
} else {
$pr_commit = $releeph_request->loadPhabricatorRepositoryCommit();
if ($pr_commit) {
$new_author_phid = $pr_commit->getAuthorPHID();
}
}
return array(
'requestID' => $releeph_request->getID(),
'requestPHID' => $releeph_request->getPHID(),
'requestName' => $handles[$releeph_request->getPHID()]->getName(),
'requestorPHID' => $releeph_request->getRequestUserPHID(),
'action' => $action,
'diffRevID' => $diff_rev_id,
'diffName' => $diff_name,
'commitIdentifier' => $commit_id,
'commitPHID' => $commit_phid,
'commitName' => $handles[$commit_phid]->getName(),
'needsRevert' => mpull($needs_revert, 'getID'),
'needsPick' => mpull($needs_pick, 'getID'),
'newAuthorPHID' => $new_author_phid,
);
}
private function sortPicks(array $releeph_requests) {
$surrogate = array();
foreach ($releeph_requests as $rq) {
// TODO: it's likely that relying on the `id` column to provide
// trunk-commit-order is thoroughly broken.
$ordinal = (int)$rq->loadPhabricatorRepositoryCommit()->getID();
$surrogate[$ordinal] = $rq;
}
ksort($surrogate);
return $surrogate;
}
/**
* Sort an array of ReleephRequests, that have been picked into a branch, in
* the order in which they were picked to the branch.
*/
private function sortReverts(array $releeph_requests) {
if (!$releeph_requests) {
return array();
}
// ReleephRequests, keyed by <branch-commit-id>
$releeph_requests = mpull($releeph_requests, null, 'getCommitIdentifier');
$commits = id(new PhabricatorRepositoryCommit())
->loadAllWhere(
'commitIdentifier IN (%Ls)',
mpull($releeph_requests, 'getCommitIdentifier'));
// A map of <branch-commit-id> => <branch-commit-ordinal>
$surrogate = mpull($commits, 'getID', 'getCommitIdentifier');
$unparsed = array();
$result = array();
foreach ($releeph_requests as $commit_id => $releeph_request) {
$ordinal = idx($surrogate, $commit_id);
if ($ordinal) {
$result[$ordinal] = $releeph_request;
} else {
$unparsed[] = $releeph_request;
}
}
// Sort $result in ascending order
ksort($result);
// Unparsed commits we'll just have to guess, based on time
$unparsed = msort($unparsed, 'getDateModified');
return array_merge($result, $unparsed);
}
}
diff --git a/src/applications/releeph/conduit/work/ReleephWorkRecordConduitAPIMethod.php b/src/applications/releeph/conduit/work/ReleephWorkRecordConduitAPIMethod.php
index 43e26f2f4..d32249fe6 100644
--- a/src/applications/releeph/conduit/work/ReleephWorkRecordConduitAPIMethod.php
+++ b/src/applications/releeph/conduit/work/ReleephWorkRecordConduitAPIMethod.php
@@ -1,75 +1,76 @@
<?php
final class ReleephWorkRecordConduitAPIMethod
extends ReleephConduitAPIMethod {
public function getAPIMethodName() {
return 'releephwork.record';
}
public function getMethodStatus() {
return self::METHOD_STATUS_UNSTABLE;
}
/**
* Record that a request was committed locally, and is about to be pushed to
* the remote repository.
*
* This lets us mark a ReleephRequest as being in a branch in real time so
* that no one else tries to pick it.
*
* When the daemons discover this commit in the repository with
* DifferentialReleephRequestFieldSpecification, we'll be able to record the
* commit's PHID as well. That process is slow though, and we don't want to
* wait a whole minute before marking something as cleanly picked or
* reverted.
*/
public function getMethodDescription() {
- return 'Record whether we committed a pick or revert '.
- 'to the upstream repository.';
+ return pht(
+ 'Record whether we committed a pick or revert '.
+ 'to the upstream repository.');
}
protected function defineParamTypes() {
$action_const = $this->formatStringConstants(
array(
'pick',
'revert',
));
return array(
'requestPHID' => 'required string',
'action' => 'required '.$action_const,
'commitIdentifier' => 'required string',
);
}
protected function defineReturnType() {
return 'void';
}
protected function execute(ConduitAPIRequest $request) {
$action = $request->getValue('action');
$new_commit_id = $request->getValue('commitIdentifier');
$releeph_request = id(new ReleephRequest())
->loadOneWhere('phid = %s', $request->getValue('requestPHID'));
$xactions = array();
$xactions[] = id(new ReleephRequestTransaction())
->setTransactionType(ReleephRequestTransaction::TYPE_COMMIT)
->setMetadataValue('action', $action)
->setNewValue($new_commit_id);
$editor = id(new ReleephRequestTransactionalEditor())
->setActor($request->getUser())
->setContinueOnNoEffect(true)
->setContentSource(
PhabricatorContentSource::newForSource(
PhabricatorContentSource::SOURCE_CONDUIT,
array()));
$editor->applyTransactions($releeph_request, $xactions);
}
}
diff --git a/src/applications/releeph/conduit/work/ReleephWorkRecordPickStatusConduitAPIMethod.php b/src/applications/releeph/conduit/work/ReleephWorkRecordPickStatusConduitAPIMethod.php
index f9434ab48..ead3d9dd2 100644
--- a/src/applications/releeph/conduit/work/ReleephWorkRecordPickStatusConduitAPIMethod.php
+++ b/src/applications/releeph/conduit/work/ReleephWorkRecordPickStatusConduitAPIMethod.php
@@ -1,83 +1,83 @@
<?php
final class ReleephWorkRecordPickStatusConduitAPIMethod
extends ReleephConduitAPIMethod {
public function getAPIMethodName() {
return 'releephwork.recordpickstatus';
}
public function getMethodStatus() {
return self::METHOD_STATUS_UNSTABLE;
}
public function getMethodDescription() {
- return 'Record whether a pick or revert was successful or not.';
+ return pht('Record whether a pick or revert was successful or not.');
}
protected function defineParamTypes() {
$action_const = $this->formatStringConstants(
array(
'pick',
'revert',
));
return array(
'requestPHID' => 'required string',
'action' => 'required '.$action_const,
'ok' => 'required bool',
'dryRun' => 'optional bool',
'details' => 'optional dict<string, wild>',
);
}
protected function defineReturnType() {
return '';
}
protected function execute(ConduitAPIRequest $request) {
$action = $request->getValue('action');
$ok = $request->getValue('ok');
$dry_run = $request->getValue('dryRun');
$details = $request->getValue('details', array());
switch ($request->getValue('action')) {
case 'pick':
$pick_status = $ok
? ReleephRequest::PICK_OK
: ReleephRequest::PICK_FAILED;
break;
case 'revert':
$pick_status = $ok
? ReleephRequest::REVERT_OK
: ReleephRequest::REVERT_FAILED;
break;
default:
- throw new Exception("Unknown action {$action}!");
+ throw new Exception(pht('Unknown action %s!', $action));
}
$releeph_request = id(new ReleephRequest())
->loadOneWhere('phid = %s', $request->getValue('requestPHID'));
$editor = id(new ReleephRequestTransactionalEditor())
->setActor($request->getUser())
->setContinueOnNoEffect(true)
->setContentSource(
PhabricatorContentSource::newForSource(
PhabricatorContentSource::SOURCE_CONDUIT,
array()));
$xactions = array();
$xactions[] = id(new ReleephRequestTransaction())
->setTransactionType(ReleephRequestTransaction::TYPE_PICK_STATUS)
->setMetadataValue('dryRun', $dry_run)
->setMetadataValue('details', $details)
->setNewValue($pick_status);
$editor->applyTransactions($releeph_request, $xactions);
}
}
diff --git a/src/applications/releeph/controller/branch/ReleephBranchCreateController.php b/src/applications/releeph/controller/branch/ReleephBranchCreateController.php
index 11b2e746a..851724c6d 100644
--- a/src/applications/releeph/controller/branch/ReleephBranchCreateController.php
+++ b/src/applications/releeph/controller/branch/ReleephBranchCreateController.php
@@ -1,131 +1,130 @@
<?php
final class ReleephBranchCreateController extends ReleephProductController {
private $productID;
public function willProcessRequest(array $data) {
$this->productID = $data['projectID'];
}
public function processRequest() {
$request = $this->getRequest();
$viewer = $request->getUser();
$product = id(new ReleephProductQuery())
->setViewer($viewer)
->withIDs(array($this->productID))
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
if (!$product) {
return new Aphront404Response();
}
$this->setProduct($product);
$cut_point = $request->getStr('cutPoint');
$symbolic_name = $request->getStr('symbolicName');
if (!$cut_point) {
$repository = $product->getRepository();
switch ($repository->getVersionControlSystem()) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
$cut_point = $product->getTrunkBranch();
break;
}
}
$e_cut = true;
$errors = array();
$branch_date_control = id(new AphrontFormDateControl())
->setUser($request->getUser())
->setName('templateDate')
->setLabel(pht('Date'))
->setCaption(pht('The date used for filling out the branch template.'))
->setInitialTime(AphrontFormDateControl::TIME_START_OF_DAY);
$branch_date = $branch_date_control->readValueFromRequest($request);
if ($request->isFormPost()) {
$cut_commit = null;
if (!$cut_point) {
$e_cut = pht('Required');
$errors[] = pht('You must give a branch cut point');
} else {
try {
$finder = id(new ReleephCommitFinder())
->setUser($request->getUser())
->setReleephProject($product);
$cut_commit = $finder->fromPartial($cut_point);
} catch (Exception $e) {
$e_cut = pht('Invalid');
$errors[] = $e->getMessage();
}
}
if (!$errors) {
$branch = id(new ReleephBranchEditor())
->setReleephProject($product)
->setActor($request->getUser())
->newBranchFromCommit(
$cut_commit,
$branch_date,
$symbolic_name);
$branch_uri = $this->getApplicationURI('branch/'.$branch->getID());
return id(new AphrontRedirectResponse())
->setURI($branch_uri);
}
}
$product_uri = $this->getProductViewURI($product);
$form = id(new AphrontFormView())
->setUser($request->getUser())
->appendChild(
id(new AphrontFormTextControl())
->setLabel(pht('Symbolic Name'))
->setName('symbolicName')
->setValue($symbolic_name)
- ->setCaption(pht('Mutable alternate name, for easy reference, '.
- '(e.g. "LATEST")')))
+ ->setCaption(pht(
+ 'Mutable alternate name, for easy reference, (e.g. "LATEST")')))
->appendChild(
id(new AphrontFormTextControl())
->setLabel(pht('Cut point'))
->setName('cutPoint')
->setValue($cut_point)
->setError($e_cut)
- ->setCaption(
- pht('A commit ID for your repo type, or a '.
- 'Diffusion ID like "rE123"')))
+ ->setCaption(pht(
+ 'A commit ID for your repo type, or a Diffusion ID like "rE123"')))
->appendChild($branch_date_control)
->appendChild(
id(new AphrontFormSubmitControl())
->setValue(pht('Cut Branch'))
->addCancelButton($product_uri));
$box = id(new PHUIObjectBoxView())
->setHeaderText(pht('New Branch'))
->setFormErrors($errors)
->appendChild($form);
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb(pht('New Branch'));
return $this->buildApplicationPage(
array(
$crumbs,
$box,
),
array(
'title' => pht('New Branch'),
));
}
}
diff --git a/src/applications/releeph/controller/branch/ReleephBranchEditController.php b/src/applications/releeph/controller/branch/ReleephBranchEditController.php
index 2e1e4ccda..000e9535b 100644
--- a/src/applications/releeph/controller/branch/ReleephBranchEditController.php
+++ b/src/applications/releeph/controller/branch/ReleephBranchEditController.php
@@ -1,115 +1,115 @@
<?php
final class ReleephBranchEditController extends ReleephBranchController {
private $branchID;
public function willProcessRequest(array $data) {
$this->branchID = $data['branchID'];
}
public function processRequest() {
$request = $this->getRequest();
$viewer = $request->getUser();
$branch = id(new ReleephBranchQuery())
->setViewer($viewer)
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->withIDs(array($this->branchID))
->executeOne();
if (!$branch) {
return new Aphront404Response();
}
$this->setBranch($branch);
$symbolic_name = $request->getStr(
'symbolicName',
$branch->getSymbolicName());
if ($request->isFormPost()) {
$existing_with_same_symbolic_name =
id(new ReleephBranch())
->loadOneWhere(
'id != %d AND releephProjectID = %d AND symbolicName = %s',
$branch->getID(),
$branch->getReleephProjectID(),
$symbolic_name);
$branch->openTransaction();
$branch
->setSymbolicName($symbolic_name);
if ($existing_with_same_symbolic_name) {
$existing_with_same_symbolic_name
->setSymbolicName(null)
->save();
}
$branch->save();
$branch->saveTransaction();
return id(new AphrontRedirectResponse())
->setURI($this->getBranchViewURI($branch));
}
$phids = array();
$phids[] = $creator_phid = $branch->getCreatedByUserPHID();
$phids[] = $cut_commit_phid = $branch->getCutPointCommitPHID();
$handles = id(new PhabricatorHandleQuery())
->setViewer($request->getUser())
->withPHIDs($phids)
->execute();
$form = id(new AphrontFormView())
->setUser($request->getUser())
->appendChild(
id(new AphrontFormStaticControl())
->setLabel(pht('Branch Name'))
->setValue($branch->getName()))
->appendChild(
id(new AphrontFormMarkupControl())
->setLabel(pht('Cut Point'))
->setValue($handles[$cut_commit_phid]->renderLink()))
->appendChild(
id(new AphrontFormMarkupControl())
->setLabel(pht('Created By'))
->setValue($handles[$creator_phid]->renderLink()))
->appendChild(
id(new AphrontFormTextControl())
->setLabel(pht('Symbolic Name'))
->setName('symbolicName')
->setValue($symbolic_name)
- ->setCaption(pht('Mutable alternate name, for easy reference, '.
- '(e.g. "LATEST")')))
+ ->setCaption(pht(
+ 'Mutable alternate name, for easy reference, (e.g. "LATEST")')))
->appendChild(
id(new AphrontFormSubmitControl())
->addCancelButton($this->getBranchViewURI($branch))
->setValue(pht('Save Branch')));
$title = pht(
'Edit Branch %s',
$branch->getDisplayNameWithDetail());
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb(pht('Edit'));
$box = id(new PHUIObjectBoxView())
->setHeaderText($title)
->appendChild($form);
return $this->buildApplicationPage(
array(
$crumbs,
$box,
),
array(
'title' => $title,
));
}
}
diff --git a/src/applications/releeph/controller/product/ReleephProductCreateController.php b/src/applications/releeph/controller/product/ReleephProductCreateController.php
index 801ec7fe4..9412f292e 100644
--- a/src/applications/releeph/controller/product/ReleephProductCreateController.php
+++ b/src/applications/releeph/controller/product/ReleephProductCreateController.php
@@ -1,169 +1,169 @@
<?php
final class ReleephProductCreateController extends ReleephProductController {
public function processRequest() {
$request = $this->getRequest();
$name = trim($request->getStr('name'));
$trunk_branch = trim($request->getStr('trunkBranch'));
$arc_pr_id = $request->getInt('arcPrID');
$arc_projects = $this->loadArcProjects();
$e_name = true;
$e_trunk_branch = true;
$errors = array();
if ($request->isFormPost()) {
if (!$name) {
$e_name = pht('Required');
$errors[] = pht(
'Your product should have a simple, descriptive name.');
}
if (!$trunk_branch) {
$e_trunk_branch = pht('Required');
$errors[] = pht(
'You must specify which branch you will be picking from.');
}
$arc_project = $arc_projects[$arc_pr_id];
$pr_repository = null;
if ($arc_project->getRepositoryID()) {
$pr_repository = id(new PhabricatorRepositoryQuery())
->setViewer($request->getUser())
->withIDs(array($arc_project->getRepositoryID()))
->executeOne();
}
if (!$errors) {
$releeph_product = id(new ReleephProject())
->setName($name)
->setTrunkBranch($trunk_branch)
->setRepositoryPHID($pr_repository->getPHID())
->setArcanistProjectID($arc_project->getID())
->setCreatedByUserPHID($request->getUser()->getPHID())
->setIsActive(1);
try {
$releeph_product->save();
return id(new AphrontRedirectResponse())
->setURI($releeph_product->getURI());
} catch (AphrontDuplicateKeyQueryException $ex) {
$e_name = pht('Not Unique');
$errors[] = pht('Another product already uses this name.');
}
}
}
$arc_project_options = $this->getArcProjectSelectOptions($arc_projects);
$product_name_input = id(new AphrontFormTextControl())
->setLabel(pht('Name'))
->setDisableAutocomplete(true)
->setName('name')
->setValue($name)
->setError($e_name)
->setCaption(pht('A name like "Thrift" but not "Thrift releases".'));
$arc_project_input = id(new AphrontFormSelectControl())
->setLabel(pht('Arc Project'))
->setName('arcPrID')
->setValue($arc_pr_id)
->setCaption(pht(
- 'If your Arc project isn\'t listed, associate it with a repository %s',
+ "If your Arc project isn't listed, associate it with a repository %s.",
phutil_tag(
'a',
array(
'href' => '/repository/',
'target' => '_blank',
),
'here')))
->setOptions($arc_project_options);
$branch_name_preview = id(new ReleephBranchPreviewView())
->setLabel(pht('Example Branch'))
->addControl('projectName', $product_name_input)
->addControl('arcProjectID', $arc_project_input)
->addStatic('template', '')
->addStatic('isSymbolic', false);
$form = id(new AphrontFormView())
->setUser($request->getUser())
->appendChild($product_name_input)
->appendChild($arc_project_input)
->appendChild(
id(new AphrontFormTextControl())
->setLabel(pht('Trunk'))
->setName('trunkBranch')
->setValue($trunk_branch)
->setError($e_trunk_branch)
- ->setCaption(pht('The development branch, '.
- 'from which requests will be picked.')))
+ ->setCaption(pht(
+ 'The development branch, from which requests will be picked.')))
->appendChild($branch_name_preview)
->appendChild(
id(new AphrontFormSubmitControl())
->addCancelButton('/releeph/project/')
->setValue(pht('Create Release Product')));
$form_box = id(new PHUIObjectBoxView())
->setHeaderText(pht('Create New Product'))
->setFormErrors($errors)
->setForm($form);
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb(pht('New Product'));
return $this->buildApplicationPage(
array(
$crumbs,
$form_box,
),
array(
'title' => pht('Create New Product'),
));
}
private function loadArcProjects() {
$viewer = $this->getRequest()->getUser();
$projects = id(new PhabricatorRepositoryArcanistProjectQuery())
->setViewer($viewer)
->needRepositories(true)
->execute();
$projects = mfilter($projects, 'getRepository');
$projects = msort($projects, 'getName');
return $projects;
}
private function getArcProjectSelectOptions(array $arc_projects) {
assert_instances_of($arc_projects, 'PhabricatorRepositoryArcanistProject');
$repos = mpull($arc_projects, 'getRepository');
$repos = mpull($repos, null, 'getID');
$groups = array();
foreach ($arc_projects as $arc_project) {
$id = $arc_project->getID();
$repo_id = $arc_project->getRepository()->getID();
$groups[$repo_id][$id] = $arc_project->getName();
}
$choices = array();
foreach ($groups as $repo_id => $group) {
$repo_name = $repos[$repo_id]->getName();
$callsign = $repos[$repo_id]->getCallsign();
$name = "r{$callsign} ({$repo_name})";
$choices[$name] = $group;
}
ksort($choices);
return $choices;
}
}
diff --git a/src/applications/releeph/controller/product/ReleephProductEditController.php b/src/applications/releeph/controller/product/ReleephProductEditController.php
index 926f813db..41b405030 100644
--- a/src/applications/releeph/controller/product/ReleephProductEditController.php
+++ b/src/applications/releeph/controller/product/ReleephProductEditController.php
@@ -1,273 +1,273 @@
<?php
final class ReleephProductEditController extends ReleephProductController {
private $productID;
public function willProcessRequest(array $data) {
$this->productID = $data['projectID'];
}
public function processRequest() {
$request = $this->getRequest();
$viewer = $request->getUser();
$product = id(new ReleephProductQuery())
->setViewer($viewer)
->withIDs(array($this->productID))
->needArcanistProjects(true)
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
if (!$product) {
return new Aphront404Response();
}
$this->setProduct($product);
$e_name = true;
$e_trunk_branch = true;
$e_branch_template = false;
$errors = array();
$product_name = $request->getStr('name', $product->getName());
$trunk_branch = $request->getStr('trunkBranch', $product->getTrunkBranch());
$branch_template = $request->getStr('branchTemplate');
if ($branch_template === null) {
$branch_template = $product->getDetail('branchTemplate');
}
$pick_failure_instructions = $request->getStr('pickFailureInstructions',
$product->getDetail('pick_failure_instructions'));
$test_paths = $request->getStr('testPaths');
if ($test_paths !== null) {
$test_paths = array_filter(explode("\n", $test_paths));
} else {
$test_paths = $product->getDetail('testPaths', array());
}
$arc_project_id = $product->getArcanistProjectID();
if ($request->isFormPost()) {
$pusher_phids = $request->getArr('pushers');
if (!$product_name) {
$e_name = pht('Required');
$errors[] =
pht('Your releeph product should have a simple descriptive name.');
}
if (!$trunk_branch) {
$e_trunk_branch = pht('Required');
$errors[] =
pht('You must specify which branch you will be picking from.');
}
$other_releeph_products = id(new ReleephProject())
->loadAllWhere('id != %d', $product->getID());
$other_releeph_product_names = mpull($other_releeph_products,
'getName', 'getID');
if (in_array($product_name, $other_releeph_product_names)) {
$errors[] = pht('Releeph product name %s is already taken',
$product_name);
}
foreach ($test_paths as $test_path) {
$result = @preg_match($test_path, '');
$is_a_valid_regexp = $result !== false;
if (!$is_a_valid_regexp) {
$errors[] = pht('Please provide a valid regular expression: '.
'%s is not valid', $test_path);
}
}
$product
->setName($product_name)
->setTrunkBranch($trunk_branch)
->setDetail('pushers', $pusher_phids)
->setDetail('pick_failure_instructions', $pick_failure_instructions)
->setDetail('branchTemplate', $branch_template)
->setDetail('testPaths', $test_paths);
$fake_commit_handle =
ReleephBranchTemplate::getFakeCommitHandleFor($arc_project_id, $viewer);
if ($branch_template) {
list($branch_name, $template_errors) = id(new ReleephBranchTemplate())
->setCommitHandle($fake_commit_handle)
->setReleephProjectName($product_name)
->interpolate($branch_template);
if ($template_errors) {
$e_branch_template = pht('Whoopsies!');
foreach ($template_errors as $template_error) {
- $errors[] = "Template error: {$template_error}";
+ $errors[] = pht('Template error: %s', $template_error);
}
}
}
if (!$errors) {
$product->save();
return id(new AphrontRedirectResponse())->setURI($product->getURI());
}
}
$pusher_phids = $request->getArr(
'pushers',
$product->getDetail('pushers', array()));
$form = id(new AphrontFormView())
->setUser($request->getUser())
->appendChild(
id(new AphrontFormTextControl())
->setLabel(pht('Name'))
->setName('name')
->setValue($product_name)
->setError($e_name)
->setCaption(pht('A name like "Thrift" but not "Thrift releases".')))
->appendChild(
id(new AphrontFormStaticControl())
->setLabel(pht('Repository'))
->setValue(
$product->getRepository()->getName()))
->appendChild(
id(new AphrontFormStaticControl())
->setLabel(pht('Arc Project'))
->setValue(
$product->getArcanistProject()->getName()))
->appendChild(
id(new AphrontFormStaticControl())
->setLabel(pht('Releeph Project PHID'))
->setValue(
$product->getPHID()))
->appendChild(
id(new AphrontFormTextControl())
->setLabel(pht('Trunk'))
->setValue($trunk_branch)
->setName('trunkBranch')
->setError($e_trunk_branch))
->appendChild(
id(new AphrontFormTextAreaControl())
->setLabel(pht('Pick Instructions'))
->setValue($pick_failure_instructions)
->setName('pickFailureInstructions')
->setCaption(
pht('Instructions for pick failures, which will be used '.
'in emails generated by failed picks')))
->appendChild(
id(new AphrontFormTextAreaControl())
->setLabel(pht('Tests paths'))
->setValue(implode("\n", $test_paths))
->setName('testPaths')
->setCaption(
pht('List of strings that all test files contain in their path '.
'in this project. One string per line. '.
'Examples: \'__tests__\', \'/javatests/\'...')));
$branch_template_input = id(new AphrontFormTextControl())
->setName('branchTemplate')
->setValue($branch_template)
- ->setLabel('Branch Template')
+ ->setLabel(pht('Branch Template'))
->setError($e_branch_template)
->setCaption(
pht("Leave this blank to use your installation's default."));
$branch_template_preview = id(new ReleephBranchPreviewView())
->setLabel(pht('Preview'))
->addControl('template', $branch_template_input)
->addStatic('arcProjectID', $arc_project_id)
->addStatic('isSymbolic', false)
->addStatic('projectName', $product->getName());
$form
->appendControl(
id(new AphrontFormTokenizerControl())
->setLabel(pht('Pushers'))
->setName('pushers')
->setDatasource(new PhabricatorPeopleDatasource())
->setValue($pusher_phids))
->appendChild($branch_template_input)
->appendChild($branch_template_preview)
->appendRemarkupInstructions($this->getBranchHelpText());
$form
->appendChild(
id(new AphrontFormSubmitControl())
->addCancelButton('/releeph/product/')
->setValue(pht('Save')));
$box = id(new PHUIObjectBoxView())
->setHeaderText(pht('Edit Releeph Product'))
->setFormErrors($errors)
->appendChild($form);
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb(pht('Edit Product'));
return $this->buildStandardPageResponse(
array(
$crumbs,
$box,
),
array(
'title' => pht('Edit Releeph Product'),
'device' => true,
));
}
private function getBranchHelpText() {
return <<<EOTEXT
==== Interpolations ====
| Code | Meaning
| ----- | -------
| `%P` | The name of your product, with spaces changed to "-".
| `%p` | Like %P, but all lowercase.
| `%Y` | The four digit year associated with the branch date.
| `%m` | The two digit month.
| `%d` | The two digit day.
| `%v` | The handle of the commit where the branch was cut ("rXYZa4b3c2d1").
| `%V` | The abbreviated commit id where the branch was cut ("a4b3c2d1").
| `%..` | Any other sequence interpreted by `strftime()`.
| `%%` | A literal percent sign.
==== Tips for Branch Templates ====
Use a directory to separate your release branches from other branches:
lang=none
releases/%Y-%M-%d-%v
=> releases/2012-30-16-rHERGE32cd512a52b7
Include a second hierarchy if you share your repository with other products:
lang=none
releases/%P/%p-release-%Y%m%d-%V
=> releases/Tintin/tintin-release-20121116-32cd512a52b7
Keep your branch names simple, avoiding strange punctuation, most of which is
forbidden or escaped anyway:
lang=none, counterexample
releases//..clown-releases..//`date --iso=seconds`-$(sudo halt)
Include the date early in your template, in an order which sorts properly:
lang=none
releases/%Y%m%d-%v
=> releases/20121116-rHERGE32cd512a52b7 (good!)
releases/%V-%m.%d.%Y
=> releases/32cd512a52b7-11.16.2012 (awful!)
EOTEXT;
}
}
diff --git a/src/applications/releeph/controller/request/ReleephRequestActionController.php b/src/applications/releeph/controller/request/ReleephRequestActionController.php
index 64125db5f..bbcc1df2b 100644
--- a/src/applications/releeph/controller/request/ReleephRequestActionController.php
+++ b/src/applications/releeph/controller/request/ReleephRequestActionController.php
@@ -1,130 +1,132 @@
<?php
final class ReleephRequestActionController
extends ReleephRequestController {
private $action;
private $requestID;
public function willProcessRequest(array $data) {
$this->action = $data['action'];
$this->requestID = $data['requestID'];
}
public function processRequest() {
$request = $this->getRequest();
$viewer = $request->getUser();
$request->validateCSRF();
$pull = id(new ReleephRequestQuery())
->setViewer($viewer)
->withIDs(array($this->requestID))
->executeOne();
if (!$pull) {
return new Aphront404Response();
}
$branch = $pull->getBranch();
$product = $branch->getProduct();
$action = $this->action;
$origin_uri = '/'.$pull->getMonogram();
$editor = id(new ReleephRequestTransactionalEditor())
->setActor($viewer)
->setContinueOnNoEffect(true)
->setContentSourceFromRequest($request);
$xactions = array();
switch ($action) {
case 'want':
case 'pass':
static $action_map = array(
'want' => ReleephRequest::INTENT_WANT,
'pass' => ReleephRequest::INTENT_PASS,
);
$intent = $action_map[$action];
$xactions[] = id(new ReleephRequestTransaction())
->setTransactionType(ReleephRequestTransaction::TYPE_USER_INTENT)
->setMetadataValue(
'isAuthoritative',
$product->isAuthoritative($viewer))
->setNewValue($intent);
break;
case 'mark-manually-picked':
case 'mark-manually-reverted':
if (
$pull->getRequestUserPHID() === $viewer->getPHID() ||
$product->isAuthoritative($viewer)) {
// We're all good!
} else {
throw new Exception(
- "Bug! Only pushers or the requestor can manually change a ".
- "request's in-branch status!");
+ pht(
+ "Bug! Only pushers or the requestor can manually change a ".
+ "request's in-branch status!"));
}
if ($action === 'mark-manually-picked') {
$in_branch = 1;
$intent = ReleephRequest::INTENT_WANT;
} else {
$in_branch = 0;
$intent = ReleephRequest::INTENT_PASS;
}
$xactions[] = id(new ReleephRequestTransaction())
->setTransactionType(ReleephRequestTransaction::TYPE_USER_INTENT)
->setMetadataValue('isManual', true)
->setMetadataValue('isAuthoritative', true)
->setNewValue($intent);
$xactions[] = id(new ReleephRequestTransaction())
->setTransactionType(ReleephRequestTransaction::TYPE_MANUAL_IN_BRANCH)
->setNewValue($in_branch);
break;
default:
- throw new Exception("unknown or unimplemented action {$action}");
+ throw new Exception(
+ pht('Unknown or unimplemented action %s.', $action));
}
$editor->applyTransactions($pull, $xactions);
if ($request->getBool('render')) {
$field_list = PhabricatorCustomField::getObjectFields(
$pull,
PhabricatorCustomField::ROLE_VIEW);
$field_list
->setViewer($viewer)
->readFieldsFromStorage($pull);
// TODO: This should be more modern and general.
$engine = id(new PhabricatorMarkupEngine())
->setViewer($viewer);
foreach ($field_list->getFields() as $field) {
if ($field->shouldMarkup()) {
$field->setMarkupEngine($engine);
}
}
$engine->process();
$pull_box = id(new ReleephRequestView())
->setUser($viewer)
->setCustomFields($field_list)
->setPullRequest($pull)
->setIsListView(true);
return id(new AphrontAjaxResponse())->setContent(
array(
'markup' => hsprintf('%s', $pull_box),
));
}
return id(new AphrontRedirectResponse())->setURI($origin_uri);
}
}
diff --git a/src/applications/releeph/controller/request/ReleephRequestDifferentialCreateController.php b/src/applications/releeph/controller/request/ReleephRequestDifferentialCreateController.php
index 8d27ef028..e938fda6c 100644
--- a/src/applications/releeph/controller/request/ReleephRequestDifferentialCreateController.php
+++ b/src/applications/releeph/controller/request/ReleephRequestDifferentialCreateController.php
@@ -1,106 +1,108 @@
<?php
// TODO: After T2222, this is likely unreachable?
final class ReleephRequestDifferentialCreateController
extends ReleephController {
private $revisionID;
private $revision;
public function willProcessRequest(array $data) {
$this->revisionID = $data['diffRevID'];
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$diff_rev = id(new DifferentialRevisionQuery())
->setViewer($user)
->withIDs(array($this->revisionID))
->executeOne();
if (!$diff_rev) {
return new Aphront404Response();
}
$this->revision = $diff_rev;
$arc_project = id(new PhabricatorRepositoryArcanistProject())
->loadOneWhere('phid = %s', $this->revision->getArcanistProjectPHID());
$projects = id(new ReleephProject())->loadAllWhere(
'arcanistProjectID = %d AND isActive = 1',
$arc_project->getID());
if (!$projects) {
- throw new Exception(sprintf(
- "D%d belongs to the '%s' Arcanist project, ".
- "which is not part of any Releeph project!",
- $this->revision->getID(),
- $arc_project->getName()));
+ throw new Exception(
+ pht(
+ "%s belongs to the '%s' Arcanist project, ".
+ "which is not part of any Releeph project!",
+ 'D'.$this->revision->getID(),
+ $arc_project->getName()));
}
$branches = id(new ReleephBranch())->loadAllWhere(
'releephProjectID IN (%Ld) AND isActive = 1',
mpull($projects, 'getID'));
if (!$branches) {
- throw new Exception(sprintf(
- 'D%d could be in the Releeph project(s) %s, '.
+ throw new Exception(pht(
+ '%s could be in the Releeph project(s) %s, '.
'but this project / none of these projects have open branches.',
- $this->revision->getID(),
+ 'D'.$this->revision->getID(),
implode(', ', mpull($projects, 'getName'))));
}
if (count($branches) === 1) {
return id(new AphrontRedirectResponse())
->setURI($this->buildReleephRequestURI(head($branches)));
}
$projects = msort(
mpull($projects, null, 'getID'),
'getName');
$branch_groups = mgroup($branches, 'getReleephProjectID');
require_celerity_resource('releeph-request-differential-create-dialog');
$dialog = id(new AphrontDialogView())
->setUser($user)
->setTitle(pht('Choose Releeph Branch'))
->setClass('releeph-request-differential-create-dialog')
->addCancelButton('/D'.$request->getStr('D'));
$dialog->appendChild(
- pht('This differential revision changes code that is associated '.
- 'with multiple Releeph branches. '.
- 'Please select the branch where you would like this code to be picked.'));
+ pht(
+ 'This differential revision changes code that is associated '.
+ 'with multiple Releeph branches. Please select the branch '.
+ 'where you would like this code to be picked.'));
foreach ($branch_groups as $project_id => $branches) {
$project = idx($projects, $project_id);
$dialog->appendChild(
phutil_tag(
'h1',
array(),
$project->getName()));
$branches = msort($branches, 'getBasename');
foreach ($branches as $branch) {
$uri = $this->buildReleephRequestURI($branch);
$dialog->appendChild(
phutil_tag(
'a',
array(
'href' => $uri,
),
$branch->getDisplayNameWithDetail()));
}
}
return id(new AphrontDialogResponse())
->setDialog($dialog);
}
private function buildReleephRequestURI(ReleephBranch $branch) {
$uri = $branch->getURI('request/');
return id(new PhutilURI($uri))
->setQueryParam('D', $this->revision->getID());
}
}
diff --git a/src/applications/releeph/controller/request/ReleephRequestEditController.php b/src/applications/releeph/controller/request/ReleephRequestEditController.php
index 3c5a80bd9..8888a0107 100644
--- a/src/applications/releeph/controller/request/ReleephRequestEditController.php
+++ b/src/applications/releeph/controller/request/ReleephRequestEditController.php
@@ -1,312 +1,314 @@
<?php
final class ReleephRequestEditController extends ReleephBranchController {
private $requestID;
private $branchID;
public function willProcessRequest(array $data) {
$this->requestID = idx($data, 'requestID');
$this->branchID = idx($data, 'branchID');
}
public function processRequest() {
$request = $this->getRequest();
$viewer = $request->getUser();
if ($this->requestID) {
$pull = id(new ReleephRequestQuery())
->setViewer($viewer)
->withIDs(array($this->requestID))
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
if (!$pull) {
return new Aphront404Response();
}
$branch = $pull->getBranch();
$is_edit = true;
} else {
$branch = id(new ReleephBranchQuery())
->setViewer($viewer)
->withIDs(array($this->branchID))
->executeOne();
if (!$branch) {
return new Aphront404Response();
}
$pull = id(new ReleephRequest())
->setRequestUserPHID($viewer->getPHID())
->setBranchID($branch->getID())
->setInBranch(0)
->attachBranch($branch);
$is_edit = false;
}
$this->setBranch($branch);
$product = $branch->getProduct();
$request_identifier = $request->getStr('requestIdentifierRaw');
$e_request_identifier = true;
// Load all the ReleephFieldSpecifications
$selector = $branch->getProduct()->getReleephFieldSelector();
$fields = $selector->getFieldSpecifications();
foreach ($fields as $field) {
$field
->setReleephProject($product)
->setReleephBranch($branch)
->setReleephRequest($pull);
}
$field_list = PhabricatorCustomField::getObjectFields(
$pull,
PhabricatorCustomField::ROLE_EDIT);
foreach ($field_list->getFields() as $field) {
$field
->setReleephProject($product)
->setReleephBranch($branch)
->setReleephRequest($pull);
}
$field_list->readFieldsFromStorage($pull);
if ($this->branchID) {
$cancel_uri = $this->getApplicationURI('branch/'.$this->branchID.'/');
} else {
$cancel_uri = '/'.$pull->getMonogram();
}
// Make edits
$errors = array();
if ($request->isFormPost()) {
$xactions = array();
// The commit-identifier being requested...
if (!$is_edit) {
if ($request_identifier ===
ReleephRequestTypeaheadControl::PLACEHOLDER) {
- $errors[] = 'No commit ID was provided.';
- $e_request_identifier = 'Required';
+ $errors[] = pht('No commit ID was provided.');
+ $e_request_identifier = pht('Required');
} else {
$pr_commit = null;
$finder = id(new ReleephCommitFinder())
->setUser($viewer)
->setReleephProject($product);
try {
$pr_commit = $finder->fromPartial($request_identifier);
} catch (Exception $e) {
- $e_request_identifier = 'Invalid';
- $errors[] =
- "Request {$request_identifier} is probably not a valid commit";
+ $e_request_identifier = pht('Invalid');
+ $errors[] = pht(
+ 'Request %s is probably not a valid commit.',
+ $request_identifier);
$errors[] = $e->getMessage();
}
if (!$errors) {
$object_phid = $finder->getRequestedObjectPHID();
if (!$object_phid) {
$object_phid = $pr_commit->getPHID();
}
$pull->setRequestedObjectPHID($object_phid);
}
}
if (!$errors) {
$existing = id(new ReleephRequest())
->loadOneWhere('requestCommitPHID = %s AND branchID = %d',
$pr_commit->getPHID(), $branch->getID());
if ($existing) {
return id(new AphrontRedirectResponse())
->setURI('/releeph/request/edit/'.$existing->getID().
- '?existing=1');
+ '?existing=1');
}
$xactions[] = id(new ReleephRequestTransaction())
->setTransactionType(ReleephRequestTransaction::TYPE_REQUEST)
->setNewValue($pr_commit->getPHID());
$xactions[] = id(new ReleephRequestTransaction())
->setTransactionType(ReleephRequestTransaction::TYPE_USER_INTENT)
// To help hide these implicit intents...
->setMetadataValue('isRQCreate', true)
->setMetadataValue('userPHID', $viewer->getPHID())
->setMetadataValue(
'isAuthoritative',
$product->isAuthoritative($viewer))
->setNewValue(ReleephRequest::INTENT_WANT);
}
}
// TODO: This should happen implicitly while building transactions
// instead.
foreach ($field_list->getFields() as $field) {
$field->readValueFromRequest($request);
}
if (!$errors) {
foreach ($fields as $field) {
if ($field->isEditable()) {
try {
$data = $request->getRequestData();
$value = idx($data, $field->getRequiredStorageKey());
$field->validate($value);
$xactions[] = id(new ReleephRequestTransaction())
->setTransactionType(ReleephRequestTransaction::TYPE_EDIT_FIELD)
->setMetadataValue('fieldClass', get_class($field))
->setNewValue($value);
} catch (ReleephFieldParseException $ex) {
$errors[] = $ex->getMessage();
}
}
}
}
if (!$errors) {
$editor = id(new ReleephRequestTransactionalEditor())
->setActor($viewer)
->setContinueOnNoEffect(true)
->setContentSourceFromRequest($request);
$editor->applyTransactions($pull, $xactions);
return id(new AphrontRedirectResponse())->setURI($cancel_uri);
}
}
$handle_phids = array(
$pull->getRequestUserPHID(),
$pull->getRequestCommitPHID(),
);
$handle_phids = array_filter($handle_phids);
if ($handle_phids) {
$handles = id(new PhabricatorHandleQuery())
->setViewer($viewer)
->withPHIDs($handle_phids)
->execute();
} else {
$handles = array();
}
$age_string = '';
if ($is_edit) {
$age_string = phutil_format_relative_time(
time() - $pull->getDateCreated()).' ago';
}
// Warn the user if we've been redirected here because we tried to
// re-request something.
$notice_view = null;
if ($request->getInt('existing')) {
$notice_messages = array(
- 'You are editing an existing pick request!',
- hsprintf(
+ pht('You are editing an existing pick request!'),
+ pht(
'Requested %s by %s',
$age_string,
$handles[$pull->getRequestUserPHID()]->renderLink()),
);
$notice_view = id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_NOTICE)
->setErrors($notice_messages);
}
$form = id(new AphrontFormView())
->setUser($viewer);
if ($is_edit) {
$form
->appendChild(
id(new AphrontFormMarkupControl())
- ->setLabel('Original Commit')
+ ->setLabel(pht('Original Commit'))
->setValue(
$handles[$pull->getRequestCommitPHID()]->renderLink()))
->appendChild(
id(new AphrontFormMarkupControl())
- ->setLabel('Requestor')
+ ->setLabel(pht('Requestor'))
->setValue(hsprintf(
'%s %s',
$handles[$pull->getRequestUserPHID()]->renderLink(),
$age_string)));
} else {
$origin = null;
$diff_rev_id = $request->getStr('D');
if ($diff_rev_id) {
$diff_rev = id(new DifferentialRevisionQuery())
->setViewer($viewer)
->withIDs(array($diff_rev_id))
->executeOne();
$origin = '/D'.$diff_rev->getID();
$title = sprintf(
'D%d: %s',
$diff_rev_id,
$diff_rev->getTitle());
$form
->addHiddenInput('requestIdentifierRaw', 'D'.$diff_rev_id)
->appendChild(
id(new AphrontFormStaticControl())
->setLabel('Diff')
->setValue($title));
} else {
$origin = $branch->getURI();
$repo = $product->getRepository();
$branch_cut_point = id(new PhabricatorRepositoryCommit())
->loadOneWhere(
'phid = %s',
$branch->getCutPointCommitPHID());
$form->appendChild(
id(new ReleephRequestTypeaheadControl())
->setName('requestIdentifierRaw')
- ->setLabel('Commit ID')
+ ->setLabel(pht('Commit ID'))
->setRepo($repo)
->setValue($request_identifier)
->setError($e_request_identifier)
->setStartTime($branch_cut_point->getEpoch())
->setCaption(
- 'Start typing to autocomplete on commit title, '.
- 'or give a Phabricator commit identifier like rFOO1234'));
+ pht(
+ 'Start typing to autocomplete on commit title, '.
+ 'or give a Phabricator commit identifier like rFOO1234.')));
}
}
$field_list->appendFieldsToForm($form);
$crumbs = $this->buildApplicationCrumbs();
if ($is_edit) {
$title = pht('Edit Pull Request');
$submit_name = pht('Save');
$crumbs->addTextCrumb($pull->getMonogram(), '/'.$pull->getMonogram());
$crumbs->addTextCrumb(pht('Edit'));
} else {
$title = pht('Create Pull Request');
$submit_name = pht('Create Pull Request');
$crumbs->addTextCrumb(pht('New Pull Request'));
}
$form->appendChild(
id(new AphrontFormSubmitControl())
- ->addCancelButton($cancel_uri, 'Cancel')
+ ->addCancelButton($cancel_uri, pht('Cancel'))
->setValue($submit_name));
$box = id(new PHUIObjectBoxView())
->setHeaderText($title)
->setFormErrors($errors)
->appendChild($form);
return $this->buildApplicationPage(
array(
$crumbs,
$notice_view,
$box,
),
array(
'title' => $title,
));
}
}
diff --git a/src/applications/releeph/differential/DifferentialReleephRequestFieldSpecification.php b/src/applications/releeph/differential/DifferentialReleephRequestFieldSpecification.php
index 6e635ee4b..33ae0acc9 100644
--- a/src/applications/releeph/differential/DifferentialReleephRequestFieldSpecification.php
+++ b/src/applications/releeph/differential/DifferentialReleephRequestFieldSpecification.php
@@ -1,371 +1,386 @@
<?php
/**
* This DifferentialFieldSpecification exists for two reason:
*
* 1: To parse "Releeph: picks RQ<nn>" headers in commits created by
* arc-releeph so that RQs committed by arc-releeph have real
* PhabricatorRepositoryCommits associated with them (instaed of just the SHA
* of the commit, as seen by the pusher).
*
* 2: If requestors want to commit directly to their release branch, they can
* use this header to (i) indicate on a differential revision that this
* differential revision is for the release branch, and (ii) when they land
* their diff on to the release branch manually, the ReleephRequest is
* automatically updated (instead of having to use the "Mark Manually Picked"
* button.)
*
*/
final class DifferentialReleephRequestFieldSpecification {
// TODO: This class is essentially dead right now, see T2222.
const ACTION_PICKS = 'picks';
const ACTION_REVERTS = 'reverts';
private $releephAction;
private $releephPHIDs = array();
public function getStorageKey() {
return 'releeph:actions';
}
public function getValueForStorage() {
return json_encode(array(
'releephAction' => $this->releephAction,
'releephPHIDs' => $this->releephPHIDs,
));
}
public function setValueFromStorage($json) {
if ($json) {
$dict = phutil_json_decode($json);
$this->releephAction = idx($dict, 'releephAction');
$this->releephPHIDs = idx($dict, 'releephPHIDs');
}
return $this;
}
public function shouldAppearOnRevisionView() {
return true;
}
public function renderLabelForRevisionView() {
return 'Releeph';
}
public function getRequiredHandlePHIDs() {
return mpull($this->loadReleephRequests(), 'getPHID');
}
public function renderValueForRevisionView() {
- static $tense = array(
- self::ACTION_PICKS => array(
- 'future' => 'Will pick',
- 'past' => 'Picked',
- ),
- self::ACTION_REVERTS => array(
- 'future' => 'Will revert',
- 'past' => 'Reverted',
- ),
- );
+ static $tense;
+
+ if ($tense === null) {
+ $tense = array(
+ self::ACTION_PICKS => array(
+ 'future' => pht('Will pick'),
+ 'past' => pht('Picked'),
+ ),
+ self::ACTION_REVERTS => array(
+ 'future' => pht('Will revert'),
+ 'past' => pht('Reverted'),
+ ),
+ );
+ }
$releeph_requests = $this->loadReleephRequests();
if (!$releeph_requests) {
return null;
}
$status = $this->getRevision()->getStatus();
if ($status == ArcanistDifferentialRevisionStatus::CLOSED) {
$verb = $tense[$this->releephAction]['past'];
} else {
$verb = $tense[$this->releephAction]['future'];
}
$parts = hsprintf('%s...', $verb);
foreach ($releeph_requests as $releeph_request) {
$parts->appendHTML(phutil_tag('br'));
$parts->appendHTML(
$this->getHandle($releeph_request->getPHID())->renderLink());
}
return $parts;
}
public function shouldAppearOnCommitMessage() {
return true;
}
public function getCommitMessageKey() {
return 'releephActions';
}
public function setValueFromParsedCommitMessage($dict) {
$this->releephAction = $dict['releephAction'];
$this->releephPHIDs = $dict['releephPHIDs'];
return $this;
}
public function renderValueForCommitMessage($is_edit) {
$releeph_requests = $this->loadReleephRequests();
if (!$releeph_requests) {
return null;
}
$parts = array($this->releephAction);
foreach ($releeph_requests as $releeph_request) {
$parts[] = 'RQ'.$releeph_request->getID();
}
return implode(' ', $parts);
}
/**
* Releeph fields should look like:
*
* Releeph: picks RQ1 RQ2, RQ3
* Releeph: reverts RQ1
*/
public function parseValueFromCommitMessage($value) {
/**
* Releeph commit messages look like this (but with more blank lines,
* omitted here):
*
* Make CaptainHaddock more reasonable
* Releeph: picks RQ1
* Requested By: edward
* Approved By: edward (requestor)
* Request Reason: x
* Summary: Make the Haddock implementation more reasonable.
* Test Plan: none
* Reviewers: user1
*
* Some of these fields are recognized by Differential (e.g. "Requested
* By"). They are folded up into the "Releeph" field, parsed by this
* class. As such $value includes more than just the first-line:
*
* "picks RQ1\n\nRequested By: edward\n\nApproved By: edward (requestor)"
*
* To hack around this, just consider the first line of $value when
* determining what Releeph actions the parsed commit is performing.
*/
$first_line = head(array_filter(explode("\n", $value)));
$tokens = preg_split('/\s*,?\s+/', $first_line);
$raw_action = array_shift($tokens);
$action = strtolower($raw_action);
if (!$action) {
return null;
}
switch ($action) {
case self::ACTION_REVERTS:
case self::ACTION_PICKS:
break;
default:
throw new DifferentialFieldParseException(
- "Commit message contains unknown Releeph action '{$raw_action}'!");
+ pht(
+ "Commit message contains unknown Releeph action '%s'!",
+ $raw_action));
break;
}
$releeph_requests = array();
foreach ($tokens as $token) {
$match = array();
if (!preg_match('/^(?:RQ)?(\d+)$/i', $token, $match)) {
$label = $this->renderLabelForCommitMessage();
throw new DifferentialFieldParseException(
- "Commit message contains unparseable ".
- "Releeph request token '{$token}'!");
+ pht(
+ "Commit message contains unparseable ".
+ "Releeph request token '%s'!",
+ $token));
}
$id = (int)$match[1];
$releeph_request = id(new ReleephRequest())->load($id);
if (!$releeph_request) {
throw new DifferentialFieldParseException(
- "Commit message references non existent releeph request: {$value}!");
+ pht(
+ 'Commit message references non existent Releeph request: %s!',
+ $value));
}
$releeph_requests[] = $releeph_request;
}
if (count($releeph_requests) > 1) {
$rqs_seen = array();
$groups = array();
foreach ($releeph_requests as $releeph_request) {
$releeph_branch = $releeph_request->getBranch();
$branch_name = $releeph_branch->getName();
$rq_id = 'RQ'.$releeph_request->getID();
if (idx($rqs_seen, $rq_id)) {
throw new DifferentialFieldParseException(
- "Commit message refers to {$rq_id} multiple times!");
+ pht(
+ 'Commit message refers to %s multiple times!',
+ $rq_id));
}
$rqs_seen[$rq_id] = true;
if (!isset($groups[$branch_name])) {
$groups[$branch_name] = array();
}
$groups[$branch_name][] = $rq_id;
}
if (count($groups) > 1) {
$lists = array();
foreach ($groups as $branch_name => $rq_ids) {
$lists[] = implode(', ', $rq_ids).' in '.$branch_name;
}
throw new DifferentialFieldParseException(
- 'Commit message references multiple Releeph requests, '.
- 'but the requests are in different branches: '.
- implode('; ', $lists));
+ pht(
+ 'Commit message references multiple Releeph requests, '.
+ 'but the requests are in different branches: %s',
+ implode('; ', $lists)));
}
}
$phids = mpull($releeph_requests, 'getPHID');
$data = array(
'releephAction' => $action,
'releephPHIDs' => $phids,
);
return $data;
}
public function renderLabelForCommitMessage() {
return 'Releeph';
}
public function shouldAppearOnCommitMessageTemplate() {
return false;
}
- public function didParseCommit(PhabricatorRepository $repo,
- PhabricatorRepositoryCommit $commit,
- PhabricatorRepositoryCommitData $data) {
+ public function didParseCommit(
+ PhabricatorRepository $repo,
+ PhabricatorRepositoryCommit $commit,
+ PhabricatorRepositoryCommitData $data) {
// NOTE: This is currently dead code. See T2222.
$releeph_requests = $this->loadReleephRequests();
if (!$releeph_requests) {
return;
}
$releeph_branch = head($releeph_requests)->getBranch();
if (!$this->isCommitOnBranch($repo, $commit, $releeph_branch)) {
return;
}
foreach ($releeph_requests as $releeph_request) {
if ($this->releephAction === self::ACTION_PICKS) {
$action = 'pick';
} else {
$action = 'revert';
}
$actor_phid = coalesce(
$data->getCommitDetail('committerPHID'),
$data->getCommitDetail('authorPHID'));
$actor = id(new PhabricatorUser())
->loadOneWhere('phid = %s', $actor_phid);
$xactions = array();
$xactions[] = id(new ReleephRequestTransaction())
->setTransactionType(ReleephRequestTransaction::TYPE_DISCOVERY)
->setMetadataValue('action', $action)
->setMetadataValue('authorPHID',
$data->getCommitDetail('authorPHID'))
->setMetadataValue('committerPHID',
$data->getCommitDetail('committerPHID'))
->setNewValue($commit->getPHID());
$editor = id(new ReleephRequestTransactionalEditor())
->setActor($actor)
->setContinueOnNoEffect(true)
->setContentSource(
PhabricatorContentSource::newForSource(
PhabricatorContentSource::SOURCE_UNKNOWN,
array()));
$editor->applyTransactions($releeph_request, $xactions);
}
}
private function loadReleephRequests() {
if (!$this->releephPHIDs) {
return array();
}
return id(new ReleephRequestQuery())
->setViewer($this->getViewer())
->withPHIDs($this->releephPHIDs)
->execute();
}
- private function isCommitOnBranch(PhabricatorRepository $repo,
- PhabricatorRepositoryCommit $commit,
- ReleephBranch $releeph_branch) {
+ private function isCommitOnBranch(
+ PhabricatorRepository $repo,
+ PhabricatorRepositoryCommit $commit,
+ ReleephBranch $releeph_branch) {
switch ($repo->getVersionControlSystem()) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
list($output) = $repo->execxLocalCommand(
'branch --all --no-color --contains %s',
$commit->getCommitIdentifier());
$remote_prefix = 'remotes/origin/';
$branches = array();
foreach (array_filter(explode("\n", $output)) as $line) {
$tokens = explode(' ', $line);
$ref = last($tokens);
if (strncmp($ref, $remote_prefix, strlen($remote_prefix)) === 0) {
$branch = substr($ref, strlen($remote_prefix));
$branches[$branch] = $branch;
}
}
return idx($branches, $releeph_branch->getName());
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
$change_query = DiffusionPathChangeQuery::newFromDiffusionRequest(
DiffusionRequest::newFromDictionary(array(
'user' => $this->getUser(),
'repository' => $repo,
'commit' => $commit->getCommitIdentifier(),
)));
$path_changes = $change_query->loadChanges();
$commit_paths = mpull($path_changes, 'getPath');
$branch_path = $releeph_branch->getName();
$in_branch = array();
$ex_branch = array();
foreach ($commit_paths as $path) {
if (strncmp($path, $branch_path, strlen($branch_path)) === 0) {
$in_branch[] = $path;
} else {
$ex_branch[] = $path;
}
}
if ($in_branch && $ex_branch) {
- $error = sprintf(
+ $error = pht(
'CONFUSION: commit %s in %s contains %d path change(s) that were '.
'part of a Releeph branch, but also has %d path change(s) not '.
'part of a Releeph branch!',
$commit->getCommitIdentifier(),
$repo->getCallsign(),
count($in_branch),
count($ex_branch));
phlog($error);
}
return !empty($in_branch);
break;
}
}
}
diff --git a/src/applications/releeph/editor/ReleephBranchEditor.php b/src/applications/releeph/editor/ReleephBranchEditor.php
index e2cb81ce7..0bc084223 100644
--- a/src/applications/releeph/editor/ReleephBranchEditor.php
+++ b/src/applications/releeph/editor/ReleephBranchEditor.php
@@ -1,85 +1,86 @@
<?php
final class ReleephBranchEditor extends PhabricatorEditor {
private $releephProject;
private $releephBranch;
public function setReleephProject(ReleephProject $rp) {
$this->releephProject = $rp;
return $this;
}
public function setReleephBranch(ReleephBranch $branch) {
$this->releephBranch = $branch;
return $this;
}
- public function newBranchFromCommit(PhabricatorRepositoryCommit $cut_point,
- $branch_date,
- $symbolic_name = null) {
+ public function newBranchFromCommit(
+ PhabricatorRepositoryCommit $cut_point,
+ $branch_date,
+ $symbolic_name = null) {
$template = $this->releephProject->getDetail('branchTemplate');
if (!$template) {
$template = ReleephBranchTemplate::getRequiredDefaultTemplate();
}
$cut_point_handle = id(new PhabricatorHandleQuery())
->setViewer($this->requireActor())
->withPHIDs(array($cut_point->getPHID()))
->executeOne();
list($name, $errors) = id(new ReleephBranchTemplate())
->setCommitHandle($cut_point_handle)
->setBranchDate($branch_date)
->setReleephProjectName($this->releephProject->getName())
->interpolate($template);
$basename = last(explode('/', $name));
$table = id(new ReleephBranch());
$transaction = $table->openTransaction();
$branch = id(new ReleephBranch())
->setName($name)
->setBasename($basename)
->setReleephProjectID($this->releephProject->getID())
->setCreatedByUserPHID($this->requireActor()->getPHID())
->setCutPointCommitPHID($cut_point->getPHID())
->setIsActive(1)
->setDetail('branchDate', $branch_date)
->save();
/**
* Steal the symbolic name from any other branch that has it (in this
* project).
*/
if ($symbolic_name) {
$others = id(new ReleephBranch())->loadAllWhere(
'releephProjectID = %d',
$this->releephProject->getID());
foreach ($others as $other) {
if ($other->getSymbolicName() == $symbolic_name) {
$other
->setSymbolicName(null)
->save();
}
}
$branch
->setSymbolicName($symbolic_name)
->save();
}
$table->saveTransaction();
return $branch;
}
// aka "close" and "reopen"
public function changeBranchAccess($is_active) {
$branch = $this->releephBranch;
$branch
->setIsActive((int)$is_active)
->save();
}
}
diff --git a/src/applications/releeph/editor/ReleephRequestTransactionalEditor.php b/src/applications/releeph/editor/ReleephRequestTransactionalEditor.php
index 993079c50..8f6805e66 100644
--- a/src/applications/releeph/editor/ReleephRequestTransactionalEditor.php
+++ b/src/applications/releeph/editor/ReleephRequestTransactionalEditor.php
@@ -1,310 +1,313 @@
<?php
final class ReleephRequestTransactionalEditor
extends PhabricatorApplicationTransactionEditor {
public function getEditorApplicationClass() {
return 'PhabricatorReleephApplication';
}
public function getEditorObjectsDescription() {
return pht('Releeph Requests');
}
public function getTransactionTypes() {
$types = parent::getTransactionTypes();
$types[] = PhabricatorTransactions::TYPE_COMMENT;
$types[] = ReleephRequestTransaction::TYPE_COMMIT;
$types[] = ReleephRequestTransaction::TYPE_DISCOVERY;
$types[] = ReleephRequestTransaction::TYPE_EDIT_FIELD;
$types[] = ReleephRequestTransaction::TYPE_MANUAL_IN_BRANCH;
$types[] = ReleephRequestTransaction::TYPE_PICK_STATUS;
$types[] = ReleephRequestTransaction::TYPE_REQUEST;
$types[] = ReleephRequestTransaction::TYPE_USER_INTENT;
return $types;
}
protected function getCustomTransactionOldValue(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case ReleephRequestTransaction::TYPE_REQUEST:
return $object->getRequestCommitPHID();
case ReleephRequestTransaction::TYPE_EDIT_FIELD:
$field = newv($xaction->getMetadataValue('fieldClass'), array());
$value = $field->setReleephRequest($object)->getValue();
return $value;
case ReleephRequestTransaction::TYPE_USER_INTENT:
$user_phid = $xaction->getAuthorPHID();
return idx($object->getUserIntents(), $user_phid);
case ReleephRequestTransaction::TYPE_PICK_STATUS:
return (int)$object->getPickStatus();
break;
case ReleephRequestTransaction::TYPE_COMMIT:
return $object->getCommitIdentifier();
case ReleephRequestTransaction::TYPE_DISCOVERY:
return $object->getCommitPHID();
case ReleephRequestTransaction::TYPE_MANUAL_IN_BRANCH:
return $object->getInBranch();
}
}
protected function getCustomTransactionNewValue(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case ReleephRequestTransaction::TYPE_REQUEST:
case ReleephRequestTransaction::TYPE_USER_INTENT:
case ReleephRequestTransaction::TYPE_EDIT_FIELD:
case ReleephRequestTransaction::TYPE_PICK_STATUS:
case ReleephRequestTransaction::TYPE_COMMIT:
case ReleephRequestTransaction::TYPE_DISCOVERY:
case ReleephRequestTransaction::TYPE_MANUAL_IN_BRANCH:
return $xaction->getNewValue();
}
}
protected function applyCustomInternalTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
$new = $xaction->getNewValue();
switch ($xaction->getTransactionType()) {
case ReleephRequestTransaction::TYPE_REQUEST:
$object->setRequestCommitPHID($new);
break;
case ReleephRequestTransaction::TYPE_USER_INTENT:
$user_phid = $xaction->getAuthorPHID();
$intents = $object->getUserIntents();
$intents[$user_phid] = $new;
$object->setUserIntents($intents);
break;
case ReleephRequestTransaction::TYPE_EDIT_FIELD:
$field = newv($xaction->getMetadataValue('fieldClass'), array());
$field
->setReleephRequest($object)
->setValue($new);
break;
case ReleephRequestTransaction::TYPE_PICK_STATUS:
$object->setPickStatus($new);
break;
case ReleephRequestTransaction::TYPE_COMMIT:
$this->setInBranchFromAction($object, $xaction);
$object->setCommitIdentifier($new);
break;
case ReleephRequestTransaction::TYPE_DISCOVERY:
$this->setInBranchFromAction($object, $xaction);
$object->setCommitPHID($new);
break;
case ReleephRequestTransaction::TYPE_MANUAL_IN_BRANCH:
$object->setInBranch((int)$new);
break;
}
}
protected function applyCustomExternalTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
return;
}
protected function filterTransactions(
PhabricatorLiskDAO $object,
array $xactions) {
// Remove TYPE_DISCOVERY xactions that are the result of a reparse.
$previously_discovered_commits = array();
$discovery_xactions = idx(
mgroup($xactions, 'getTransactionType'),
ReleephRequestTransaction::TYPE_DISCOVERY);
if ($discovery_xactions) {
$previous_xactions = id(new ReleephRequestTransactionQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withObjectPHIDs(array($object->getPHID()))
->execute();
foreach ($previous_xactions as $xaction) {
if ($xaction->getTransactionType() ===
ReleephRequestTransaction::TYPE_DISCOVERY) {
$commit_phid = $xaction->getNewValue();
$previously_discovered_commits[$commit_phid] = true;
}
}
}
foreach ($xactions as $key => $xaction) {
if ($xaction->getTransactionType() ===
ReleephRequestTransaction::TYPE_DISCOVERY &&
idx($previously_discovered_commits, $xaction->getNewValue())) {
unset($xactions[$key]);
}
}
return parent::filterTransactions($object, $xactions);
}
protected function shouldSendMail(
PhabricatorLiskDAO $object,
array $xactions) {
return true;
}
protected function sendMail(
PhabricatorLiskDAO $object,
array $xactions) {
// Avoid sending emails that only talk about commit discovery.
$types = array_unique(mpull($xactions, 'getTransactionType'));
if ($types === array(ReleephRequestTransaction::TYPE_DISCOVERY)) {
return null;
}
// Don't email people when we discover that something picks or reverts OK.
if ($types === array(ReleephRequestTransaction::TYPE_PICK_STATUS)) {
if (!mfilter($xactions, 'isBoringPickStatus', true /* negate */)) {
// If we effectively call "isInterestingPickStatus" and get nothing...
return null;
}
}
return parent::sendMail($object, $xactions);
}
protected function buildReplyHandler(PhabricatorLiskDAO $object) {
return id(new ReleephRequestReplyHandler())
->setActor($this->getActor())
->setMailReceiver($object);
}
protected function getMailSubjectPrefix() {
return '[Releeph]';
}
protected function buildMailTemplate(PhabricatorLiskDAO $object) {
$id = $object->getID();
$phid = $object->getPHID();
$title = $object->getSummaryForDisplay();
return id(new PhabricatorMetaMTAMail())
->setSubject("RQ{$id}: {$title}")
->addHeader('Thread-Topic', "RQ{$id}: {$phid}");
}
protected function getMailTo(PhabricatorLiskDAO $object) {
$to_phids = array();
$product = $object->getBranch()->getProduct();
foreach ($product->getPushers() as $phid) {
$to_phids[] = $phid;
}
foreach ($object->getUserIntents() as $phid => $intent) {
$to_phids[] = $phid;
}
return $to_phids;
}
protected function getMailCC(PhabricatorLiskDAO $object) {
return array();
}
protected function buildMailBody(
PhabricatorLiskDAO $object,
array $xactions) {
$body = parent::buildMailBody($object, $xactions);
$rq = $object;
$releeph_branch = $rq->getBranch();
$releeph_project = $releeph_branch->getProduct();
/**
* If any of the events we are emailing about were about a pick failure
* (and/or a revert failure?), include pick failure instructions.
*/
$has_pick_failure = false;
foreach ($xactions as $xaction) {
if ($xaction->getTransactionType() ===
ReleephRequestTransaction::TYPE_PICK_STATUS &&
$xaction->getNewValue() === ReleephRequest::PICK_FAILED) {
$has_pick_failure = true;
break;
}
}
if ($has_pick_failure) {
$instructions = $releeph_project->getDetail('pick_failure_instructions');
if ($instructions) {
$body->addTextSection(
pht('PICK FAILURE INSTRUCTIONS'),
$instructions);
}
}
$name = sprintf('RQ%s: %s', $rq->getID(), $rq->getSummaryForDisplay());
$body->addTextSection(
pht('RELEEPH REQUEST'),
$name."\n".
PhabricatorEnv::getProductionURI('/RQ'.$rq->getID()));
$project_and_branch = sprintf(
'%s - %s',
$releeph_project->getName(),
$releeph_branch->getDisplayNameWithDetail());
$body->addTextSection(
pht('RELEEPH BRANCH'),
$project_and_branch."\n".
PhabricatorEnv::getProductionURI($releeph_branch->getURI()));
return $body;
}
private function setInBranchFromAction(
ReleephRequest $rq,
ReleephRequestTransaction $xaction) {
$action = $xaction->getMetadataValue('action');
switch ($action) {
case 'pick':
$rq->setInBranch(1);
break;
case 'revert':
$rq->setInBranch(0);
break;
default:
$id = $rq->getID();
$type = $xaction->getTransactionType();
$new = $xaction->getNewValue();
phlog(
- "Unknown discovery action '{$action}' ".
- "for xaction of type {$type} ".
- "with new value {$new} ".
- "mentioning RQ{$id}!");
+ pht(
+ "Unknown discovery action '%s' for xaction of type %s ".
+ "with new value %s mentioning %s!",
+ $action,
+ $type,
+ $new,
+ 'RQ'.$id));
break;
}
}
}
diff --git a/src/applications/releeph/field/selector/ReleephFieldSelector.php b/src/applications/releeph/field/selector/ReleephFieldSelector.php
index 68a9f4d74..95f22d0fe 100644
--- a/src/applications/releeph/field/selector/ReleephFieldSelector.php
+++ b/src/applications/releeph/field/selector/ReleephFieldSelector.php
@@ -1,45 +1,48 @@
<?php
abstract class ReleephFieldSelector {
final public function __construct() {
// <empty>
}
abstract public function getFieldSpecifications();
public function sortFieldsForCommitMessage(array $fields) {
assert_instances_of($fields, 'ReleephFieldSpecification');
return $fields;
}
protected static function selectFields(array $fields, array $classes) {
assert_instances_of($fields, 'ReleephFieldSpecification');
$map = array();
foreach ($fields as $field) {
$map[get_class($field)] = $field;
}
$result = array();
foreach ($classes as $class) {
$field = idx($map, $class);
if (!$field) {
throw new Exception(
- "Tried to select a in instance of '{$class}' but that field ".
- "is not configured for this project!");
+ pht(
+ "Tried to select a in instance of '%s' but that field ".
+ "is not configured for this project!",
+ $class));
}
if (idx($result, $class)) {
throw new Exception(
- "You have asked to select the field '{$class}' ".
- "more than once!");
+ pht(
+ "You have asked to select the field '%s' more than once!",
+ $class));
}
$result[$class] = $field;
}
return $result;
}
}
diff --git a/src/applications/releeph/field/specification/ReleephDependsOnFieldSpecification.php b/src/applications/releeph/field/specification/ReleephDependsOnFieldSpecification.php
index 832120368..acb5d7d11 100644
--- a/src/applications/releeph/field/specification/ReleephDependsOnFieldSpecification.php
+++ b/src/applications/releeph/field/specification/ReleephDependsOnFieldSpecification.php
@@ -1,33 +1,34 @@
<?php
final class ReleephDependsOnFieldSpecification
extends ReleephFieldSpecification {
+
public function getFieldKey() {
return 'dependsOn';
}
public function getName() {
return pht('Depends On');
}
public function getRequiredHandlePHIDsForPropertyView() {
return $this->getDependentRevisionPHIDs();
}
public function renderPropertyViewValue(array $handles) {
return $this->renderHandleList($handles);
}
private function getDependentRevisionPHIDs() {
$requested_object = $this->getObject()->getRequestedObjectPHID();
if (!($requested_object instanceof DifferentialRevision)) {
return array();
}
$revision = $requested_object;
return PhabricatorEdgeQuery::loadDestinationPHIDs(
$revision->getPHID(),
DifferentialRevisionDependsOnRevisionEdgeType::EDGECONST);
}
}
diff --git a/src/applications/releeph/field/specification/ReleephDiffChurnFieldSpecification.php b/src/applications/releeph/field/specification/ReleephDiffChurnFieldSpecification.php
index 026392a1b..18c3c78c8 100644
--- a/src/applications/releeph/field/specification/ReleephDiffChurnFieldSpecification.php
+++ b/src/applications/releeph/field/specification/ReleephDiffChurnFieldSpecification.php
@@ -1,89 +1,89 @@
<?php
final class ReleephDiffChurnFieldSpecification
extends ReleephFieldSpecification {
const REJECTIONS_WEIGHT = 30;
const COMMENTS_WEIGHT = 7;
const UPDATES_WEIGHT = 10;
const MAX_POINTS = 100;
public function getFieldKey() {
return 'churn';
}
public function getName() {
- return 'Churn';
+ return pht('Churn');
}
public function renderPropertyViewValue(array $handles) {
$requested_object = $this->getObject()->getRequestedObject();
if (!($requested_object instanceof DifferentialRevision)) {
return null;
}
$diff_rev = $requested_object;
$xactions = id(new DifferentialTransactionQuery())
->setViewer($this->getViewer())
->withObjectPHIDs(array($diff_rev->getPHID()))
->execute();
$rejections = 0;
$comments = 0;
$updates = 0;
foreach ($xactions as $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorTransactions::TYPE_COMMENT:
$comments++;
break;
case DifferentialTransaction::TYPE_UPDATE:
$updates++;
break;
case DifferentialTransaction::TYPE_ACTION:
switch ($xaction->getNewValue()) {
case DifferentialAction::ACTION_REJECT:
$rejections++;
break;
}
break;
}
}
$points =
self::REJECTIONS_WEIGHT * $rejections +
self::COMMENTS_WEIGHT * $comments +
self::UPDATES_WEIGHT * $updates;
if ($points === 0) {
$points = 0.15 * self::MAX_POINTS;
- $blurb = 'Silent diff';
+ $blurb = pht('Silent diff');
} else {
$parts = array();
if ($rejections) {
$parts[] = pht('%d rejection(s)', $rejections);
}
if ($comments) {
$parts[] = pht('%d comment(s)', $comments);
}
if ($updates) {
$parts[] = pht('%d update(s)', $updates);
}
if (count($parts) === 0) {
$blurb = '';
} else if (count($parts) === 1) {
$blurb = head($parts);
} else {
$last = array_pop($parts);
- $blurb = implode(', ', $parts).' and '.$last;
+ $blurb = pht('%s and %s', implode(', ', $parts), $last);
}
}
return id(new AphrontProgressBarView())
->setValue($points)
->setMax(self::MAX_POINTS)
->setCaption($blurb)
->render();
}
}
diff --git a/src/applications/releeph/field/specification/ReleephDiffSizeFieldSpecification.php b/src/applications/releeph/field/specification/ReleephDiffSizeFieldSpecification.php
index 671511deb..fe1a39277 100644
--- a/src/applications/releeph/field/specification/ReleephDiffSizeFieldSpecification.php
+++ b/src/applications/releeph/field/specification/ReleephDiffSizeFieldSpecification.php
@@ -1,115 +1,117 @@
<?php
final class ReleephDiffSizeFieldSpecification
extends ReleephFieldSpecification {
const LINES_WEIGHT = 1;
const PATHS_WEIGHT = 30;
const MAX_POINTS = 1000;
public function getFieldKey() {
return 'commit:size';
}
public function getName() {
- return 'Size';
+ return pht('Size');
}
public function renderPropertyViewValue(array $handles) {
$requested_object = $this->getObject()->getRequestedObject();
if (!($requested_object instanceof DifferentialRevision)) {
return null;
}
$diff_rev = $requested_object;
$diffs = $diff_rev->loadRelatives(
new DifferentialDiff(),
'revisionID',
'getID',
'creationMethod <> "commit"');
$all_changesets = array();
$most_recent_changesets = null;
foreach ($diffs as $diff) {
$changesets = $diff->loadRelatives(new DifferentialChangeset(), 'diffID');
$all_changesets += $changesets;
$most_recent_changesets = $changesets;
}
// The score is based on all changesets for all versions of this diff
$all_changes = $this->countLinesAndPaths($all_changesets);
$points =
self::LINES_WEIGHT * $all_changes['code']['lines'] +
self::PATHS_WEIGHT * count($all_changes['code']['paths']);
// The blurb is just based on the most recent version of the diff
$mr_changes = $this->countLinesAndPaths($most_recent_changesets);
$test_tag = '';
if ($mr_changes['tests']['paths']) {
Javelin::initBehavior('phabricator-tooltips');
require_celerity_resource('aphront-tooltip-css');
- $test_blurb =
- pht('%d line(s)', $mr_changes['tests']['lines']).' and '.
- pht('%d path(s)', count($mr_changes['tests']['paths'])).
- " contain changes to test code:\n";
+ $test_blurb = pht(
+ "%d line(s) and %d path(s) contain changes to test code:\n",
+ $mr_changes['tests']['lines'],
+ count($mr_changes['tests']['paths']));
foreach ($mr_changes['tests']['paths'] as $mr_test_path) {
$test_blurb .= pht("%s\n", $mr_test_path);
}
$test_tag = javelin_tag(
'span',
array(
'sigil' => 'has-tooltip',
'meta' => array(
'tip' => $test_blurb,
'align' => 'E',
'size' => 'auto',
),
'style' => '',
),
' + tests');
}
$blurb = hsprintf('%s%s.',
- pht('%d line(s)', $mr_changes['code']['lines']).' and '.
- pht('%d path(s)', count($mr_changes['code']['paths'])).' over '.
- pht('%d diff(s)', count($diffs)),
+ pht(
+ '%d line(s) and %d path(s) over %d diff(s)',
+ $mr_changes['code']['lines'],
+ $mr_changes['code']['paths'],
+ count($diffs)),
$test_tag);
return id(new AphrontProgressBarView())
->setValue($points)
->setMax(self::MAX_POINTS)
->setCaption($blurb)
->render();
}
private function countLinesAndPaths(array $changesets) {
assert_instances_of($changesets, 'DifferentialChangeset');
$lines = 0;
$paths_touched = array();
$test_lines = 0;
$test_paths_touched = array();
foreach ($changesets as $ch) {
if ($this->getReleephProject()->isTestFile($ch->getFilename())) {
$test_lines += $ch->getAddLines() + $ch->getDelLines();
$test_paths_touched[] = $ch->getFilename();
} else {
$lines += $ch->getAddLines() + $ch->getDelLines();
$paths_touched[] = $ch->getFilename();
}
}
return array(
'code' => array(
'lines' => $lines,
'paths' => array_unique($paths_touched),
),
'tests' => array(
'lines' => $test_lines,
'paths' => array_unique($test_paths_touched),
),
);
}
}
diff --git a/src/applications/releeph/field/specification/ReleephFieldSpecification.php b/src/applications/releeph/field/specification/ReleephFieldSpecification.php
index b0017f1b4..c0cb64b05 100644
--- a/src/applications/releeph/field/specification/ReleephFieldSpecification.php
+++ b/src/applications/releeph/field/specification/ReleephFieldSpecification.php
@@ -1,263 +1,262 @@
<?php
abstract class ReleephFieldSpecification
extends PhabricatorCustomField
implements PhabricatorMarkupInterface {
// TODO: This is temporary, until ReleephFieldSpecification is more conformant
// to PhabricatorCustomField.
private $requestValue;
public function readValueFromRequest(AphrontRequest $request) {
$this->requestValue = $request->getStr($this->getRequiredStorageKey());
return $this;
}
public function shouldAppearInPropertyView() {
return true;
}
public function renderPropertyViewLabel() {
return $this->getName();
}
public function renderPropertyViewValue(array $handles) {
$key = $this->getRequiredStorageKey();
$value = $this->getReleephRequest()->getDetail($key);
if ($value === '') {
return null;
}
return $value;
}
abstract public function getName();
/* -( Storage )------------------------------------------------------------ */
public function getStorageKey() {
return null;
}
public function getRequiredStorageKey() {
$key = $this->getStorageKey();
if ($key === null) {
throw new PhabricatorCustomFieldImplementationIncompleteException($this);
}
if (strpos($key, '.') !== false) {
/**
* Storage keys are reused for form controls, and periods in form control
* names break HTML forms.
*/
- throw new Exception(
- "You can't use '.' in storage keys!");
+ throw new Exception(pht("You can't use '%s' in storage keys!", '.'));
}
return $key;
}
public function shouldAppearInEditView() {
return $this->isEditable();
}
final public function isEditable() {
return $this->getStorageKey() !== null;
}
final public function getValue() {
if ($this->requestValue !== null) {
return $this->requestValue;
}
$key = $this->getRequiredStorageKey();
return $this->getReleephRequest()->getDetail($key);
}
final public function setValue($value) {
$key = $this->getRequiredStorageKey();
return $this->getReleephRequest()->setDetail($key, $value);
}
/**
* @throws ReleephFieldParseException, to show an error.
*/
public function validate($value) {
return;
}
/**
* Turn values as they are stored in a ReleephRequest into a text that can be
* rendered as a transactions old/new values.
*/
public function normalizeForTransactionView(
PhabricatorApplicationTransaction $xaction,
$value) {
return $value;
}
/* -( Conduit )------------------------------------------------------------ */
public function getKeyForConduit() {
return $this->getRequiredStorageKey();
}
public function getValueForConduit() {
return $this->getValue();
}
public function setValueFromConduitAPIRequest(ConduitAPIRequest $request) {
$value = idx(
$request->getValue('fields', array()),
$this->getRequiredStorageKey());
$this->validate($value);
$this->setValue($value);
}
/* -( Arcanist )----------------------------------------------------------- */
public function renderHelpForArcanist() {
return '';
}
/* -( Context )------------------------------------------------------------ */
private $releephProject;
private $releephBranch;
private $releephRequest;
private $user;
final public function setReleephProject(ReleephProject $rp) {
$this->releephProject = $rp;
return $this;
}
final public function setReleephBranch(ReleephBranch $rb) {
$this->releephRequest = $rb;
return $this;
}
final public function setReleephRequest(ReleephRequest $rr) {
$this->releephRequest = $rr;
return $this;
}
final public function setUser(PhabricatorUser $user) {
$this->user = $user;
return $this;
}
final public function getReleephProject() {
if (!$this->releephProject) {
return $this->getReleephBranch()->getProduct();
}
return $this->releephProject;
}
final public function getReleephBranch() {
if (!$this->releephBranch) {
return $this->getReleephRequest()->getBranch();
}
return $this->releephBranch;
}
final public function getReleephRequest() {
if (!$this->releephRequest) {
return $this->getObject();
}
return $this->releephRequest;
}
final public function getUser() {
if (!$this->user) {
return $this->getViewer();
}
return $this->user;
}
/* -( Commit Messages )---------------------------------------------------- */
public function shouldAppearOnCommitMessage() {
return false;
}
public function renderLabelForCommitMessage() {
throw new PhabricatorCustomFieldImplementationIncompleteException($this);
}
public function renderValueForCommitMessage() {
throw new PhabricatorCustomFieldImplementationIncompleteException($this);
}
public function shouldAppearOnRevertMessage() {
return false;
}
public function renderLabelForRevertMessage() {
return $this->renderLabelForCommitMessage();
}
public function renderValueForRevertMessage() {
return $this->renderValueForCommitMessage();
}
/* -( Markup Interface )--------------------------------------------------- */
const MARKUP_FIELD_GENERIC = 'releeph:generic-markup-field';
private $engine;
/**
* @{class:ReleephFieldSpecification} implements much of
* @{interface:PhabricatorMarkupInterface} for you. If you return true from
* `shouldMarkup()`, and implement `getMarkupText()` then your text will be
* rendered through the Phabricator markup pipeline.
*
* Output is retrievable with `getMarkupEngineOutput()`.
*/
public function shouldMarkup() {
return false;
}
public function getMarkupText($field) {
throw new PhabricatorCustomFieldImplementationIncompleteException($this);
}
final public function getMarkupEngineOutput() {
return $this->engine->getOutput($this, self::MARKUP_FIELD_GENERIC);
}
final public function setMarkupEngine(PhabricatorMarkupEngine $engine) {
$this->engine = $engine;
$engine->addObject($this, self::MARKUP_FIELD_GENERIC);
return $this;
}
final public function getMarkupFieldKey($field) {
return sprintf(
'%s:%s:%s:%s',
$this->getReleephRequest()->getPHID(),
$this->getStorageKey(),
$field,
PhabricatorHash::digest($this->getMarkupText($field)));
}
final public function newMarkupEngine($field) {
return PhabricatorMarkupEngine::newDifferentialMarkupEngine();
}
final public function didMarkupText(
$field,
$output,
PhutilMarkupEngine $engine) {
return $output;
}
final public function shouldUseMarkupCache($field) {
return true;
}
}
diff --git a/src/applications/releeph/field/specification/ReleephIntentFieldSpecification.php b/src/applications/releeph/field/specification/ReleephIntentFieldSpecification.php
index bae7da14c..4d199f26c 100644
--- a/src/applications/releeph/field/specification/ReleephIntentFieldSpecification.php
+++ b/src/applications/releeph/field/specification/ReleephIntentFieldSpecification.php
@@ -1,142 +1,142 @@
<?php
final class ReleephIntentFieldSpecification
extends ReleephFieldSpecification {
public function getFieldKey() {
return 'intent';
}
public function getName() {
return 'Intent';
}
public function getRequiredHandlePHIDsForPropertyView() {
$pull = $this->getReleephRequest();
$intents = $pull->getUserIntents();
return array_keys($intents);
}
public function renderPropertyViewValue(array $handles) {
$pull = $this->getReleephRequest();
$intents = $pull->getUserIntents();
$product = $this->getReleephProject();
if (!$intents) {
return null;
}
$pushers = array();
$others = array();
foreach ($intents as $phid => $intent) {
if ($product->isAuthoritativePHID($phid)) {
$pushers[$phid] = $intent;
} else {
$others[$phid] = $intent;
}
}
$intents = $pushers + $others;
$view = id(new PHUIStatusListView());
foreach ($intents as $phid => $intent) {
switch ($intent) {
case ReleephRequest::INTENT_WANT:
$icon = PHUIStatusItemView::ICON_ACCEPT;
$color = 'green';
$label = pht('Want');
break;
case ReleephRequest::INTENT_PASS:
$icon = PHUIStatusItemView::ICON_REJECT;
$color = 'red';
$label = pht('Pass');
break;
default:
$icon = PHUIStatusItemView::ICON_QUESTION;
$color = 'bluegrey';
$label = pht('Unknown Intent (%s)', $intent);
break;
}
$target = $handles[$phid]->renderLink();
if ($product->isAuthoritativePHID($phid)) {
$target = phutil_tag('strong', array(), $target);
}
$view->addItem(
id(new PHUIStatusItemView())
->setIcon($icon, $color, $label)
->setTarget($target));
}
return $view;
}
public function shouldAppearOnCommitMessage() {
return true;
}
public function shouldAppearOnRevertMessage() {
return true;
}
public function renderLabelForCommitMessage() {
- return 'Approved By';
+ return pht('Approved By');
}
public function renderLabelForRevertMessage() {
- return 'Rejected By';
+ return pht('Rejected By');
}
public function renderValueForCommitMessage() {
return $this->renderIntentsForCommitMessage(ReleephRequest::INTENT_WANT);
}
public function renderValueForRevertMessage() {
return $this->renderIntentsForCommitMessage(ReleephRequest::INTENT_PASS);
}
private function renderIntentsForCommitMessage($print_intent) {
$intents = $this->getReleephRequest()->getUserIntents();
$requestor = $this->getReleephRequest()->getRequestUserPHID();
$pusher_phids = $this->getReleephProject()->getPushers();
$phids = array_unique($pusher_phids + array_keys($intents));
$handles = id(new PhabricatorHandleQuery())
->setViewer($this->getUser())
->withPHIDs($phids)
->execute();
$tokens = array();
foreach ($phids as $phid) {
$intent = idx($intents, $phid);
if ($intent == $print_intent) {
$name = $handles[$phid]->getName();
$is_pusher = in_array($phid, $pusher_phids);
$is_requestor = $phid == $requestor;
if ($is_pusher) {
if ($is_requestor) {
- $token = "{$name} (pusher and requestor)";
+ $token = pht('%s (pusher and requestor)', $name);
} else {
$token = "{$name} (pusher)";
}
} else {
if ($is_requestor) {
- $token = "{$name} (requestor)";
+ $token = pht('%s (requestor)', $name);
} else {
$token = $name;
}
}
$tokens[] = $token;
}
}
return implode(', ', $tokens);
}
}
diff --git a/src/applications/releeph/field/specification/ReleephLevelFieldSpecification.php b/src/applications/releeph/field/specification/ReleephLevelFieldSpecification.php
index 70ae446f9..a686eb15c 100644
--- a/src/applications/releeph/field/specification/ReleephLevelFieldSpecification.php
+++ b/src/applications/releeph/field/specification/ReleephLevelFieldSpecification.php
@@ -1,132 +1,136 @@
<?php
/**
* Provides a convenient field for storing a set of levels that you can use to
* filter requests on.
*
* Levels are rendered with names and descriptions in the edit UI, and are
* automatically documented via the "arc request" interface.
*
* See ReleephSeverityFieldSpecification for an example.
*/
abstract class ReleephLevelFieldSpecification
extends ReleephFieldSpecification {
private $error;
abstract public function getLevels();
abstract public function getDefaultLevel();
abstract public function getNameForLevel($level);
abstract public function getDescriptionForLevel($level);
public function getStorageKey() {
throw new PhabricatorCustomFieldImplementationIncompleteException($this);
}
public function renderPropertyViewValue(array $handles) {
return $this->getNameForLevel($this->getValue());
}
public function renderEditControl(array $handles) {
$control_name = $this->getRequiredStorageKey();
$all_levels = $this->getLevels();
$level = $this->getValue();
if (!$level) {
$level = $this->getDefaultLevel();
}
$control = id(new AphrontFormRadioButtonControl())
->setLabel('Level')
->setName($control_name)
->setValue($level);
if ($this->error) {
$control->setError($this->error);
} else if ($this->getDefaultLevel()) {
$control->setError(true);
}
foreach ($all_levels as $level) {
$name = $this->getNameForLevel($level);
$description = $this->getDescriptionForLevel($level);
$control->addButton($level, $name, $description);
}
return $control;
}
public function renderHelpForArcanist() {
$text = '';
$levels = $this->getLevels();
$default = $this->getDefaultLevel();
foreach ($levels as $level) {
$name = $this->getNameForLevel($level);
$description = $this->getDescriptionForLevel($level);
$default_marker = ' ';
if ($level === $default) {
$default_marker = '*';
}
$text .= " {$default_marker} **{$name}**\n";
$text .= phutil_console_wrap($description."\n", 8);
}
return $text;
}
public function validate($value) {
if ($value === null) {
$this->error = 'Required';
$label = $this->getName();
throw new ReleephFieldParseException(
$this,
- "You must provide a {$label} level");
+ pht('You must provide a %s level.', $label));
}
$levels = $this->getLevels();
if (!in_array($value, $levels)) {
$label = $this->getName();
throw new ReleephFieldParseException(
$this,
- "Level '{$value}' is not a valid {$label} level in this project.");
+ pht(
+ "Level '%s' is not a valid %s level in this project.",
+ $value,
+ $label));
}
}
public function setValueFromConduitAPIRequest(ConduitAPIRequest $request) {
$key = $this->getRequiredStorageKey();
$label = $this->getName();
$name = idx($request->getValue('fields', array()), $key);
if (!$name) {
$level = $this->getDefaultLevel();
if (!$level) {
throw new ReleephFieldParseException(
$this,
- "No value given for {$label}, ".
- "and no default is given for this level!");
+ pht(
+ 'No value given for %s, and no default is given for this level!',
+ $label));
}
} else {
$level = $this->getLevelByName($name);
}
if (!$level) {
throw new ReleephFieldParseException(
$this,
- "Unknown {$label} level name '{$name}'");
+ pht("Unknown %s level name '%s'", $label, $name));
}
$this->setValue($level);
}
private $nameMap = array();
public function getLevelByName($name) {
// Build this once
if (!$this->nameMap) {
foreach ($this->getLevels() as $level) {
$level_name = $this->getNameForLevel($level);
$this->nameMap[$level_name] = $level;
}
}
return idx($this->nameMap, $name);
}
}
diff --git a/src/applications/releeph/field/specification/ReleephReasonFieldSpecification.php b/src/applications/releeph/field/specification/ReleephReasonFieldSpecification.php
index d8312283c..831e1d60a 100644
--- a/src/applications/releeph/field/specification/ReleephReasonFieldSpecification.php
+++ b/src/applications/releeph/field/specification/ReleephReasonFieldSpecification.php
@@ -1,86 +1,86 @@
<?php
final class ReleephReasonFieldSpecification
extends ReleephFieldSpecification {
public function getFieldKey() {
return 'reason';
}
public function getName() {
- return 'Reason';
+ return pht('Reason');
}
public function getStorageKey() {
return 'reason';
}
public function getStyleForPropertyView() {
return 'block';
}
public function getIconForPropertyView() {
return PHUIPropertyListView::ICON_SUMMARY;
}
public function renderPropertyViewValue(array $handles) {
return phutil_tag(
'div',
array(
'class' => 'phabricator-remarkup',
),
$this->getMarkupEngineOutput());
}
private $error = true;
public function renderEditControl(array $handles) {
return id(new AphrontFormTextAreaControl())
- ->setLabel('Reason')
+ ->setLabel(pht('Reason'))
->setName('reason')
->setError($this->error)
->setValue($this->getValue());
}
public function validate($reason) {
if (!$reason) {
$this->error = 'Required';
throw new ReleephFieldParseException(
$this,
- 'You must give a reason for your request.');
+ pht('You must give a reason for your request.'));
}
}
public function renderHelpForArcanist() {
- $text =
+ $text = pht(
"Fully explain why you are requesting this code be included ".
- "in the next release.\n";
+ "in the next release.\n");
return phutil_console_wrap($text, 8);
}
public function shouldAppearOnCommitMessage() {
return true;
}
public function renderLabelForCommitMessage() {
- return 'Request Reason';
+ return pht('Request Reason');
}
public function renderValueForCommitMessage() {
return $this->getValue();
}
public function shouldMarkup() {
return true;
}
public function getMarkupText($field) {
$reason = $this->getValue();
if ($reason) {
return $reason;
} else {
return '';
}
}
}
diff --git a/src/applications/releeph/field/specification/ReleephRequestorFieldSpecification.php b/src/applications/releeph/field/specification/ReleephRequestorFieldSpecification.php
index 03fb88e95..d479bb802 100644
--- a/src/applications/releeph/field/specification/ReleephRequestorFieldSpecification.php
+++ b/src/applications/releeph/field/specification/ReleephRequestorFieldSpecification.php
@@ -1,50 +1,50 @@
<?php
final class ReleephRequestorFieldSpecification
extends ReleephFieldSpecification {
public function getFieldKey() {
return 'requestor';
}
public function getName() {
- return 'Requestor';
+ return pht('Requestor');
}
public function getRequiredHandlePHIDsForPropertyView() {
$phids = array();
$phid = $this->getReleephRequest()->getRequestUserPHID();
if ($phid) {
$phids[] = $phid;
}
return $phids;
}
public function renderPropertyViewValue(array $handles) {
return $this->renderHandleList($handles);
}
public function shouldAppearOnCommitMessage() {
return true;
}
public function shouldAppearOnRevertMessage() {
return true;
}
public function renderLabelForCommitMessage() {
- return 'Requested By';
+ return pht('Requested By');
}
public function renderValueForCommitMessage() {
$phid = $this->getReleephRequest()->getRequestUserPHID();
$handle = id(new PhabricatorHandleQuery())
->setViewer($this->getUser())
->withPHIDs(array($phid))
->executeOne();
return $handle->getName();
}
}
diff --git a/src/applications/releeph/field/specification/ReleephRevisionFieldSpecification.php b/src/applications/releeph/field/specification/ReleephRevisionFieldSpecification.php
index f9aa6fd2a..45f6bdd7e 100644
--- a/src/applications/releeph/field/specification/ReleephRevisionFieldSpecification.php
+++ b/src/applications/releeph/field/specification/ReleephRevisionFieldSpecification.php
@@ -1,29 +1,29 @@
<?php
final class ReleephRevisionFieldSpecification
extends ReleephFieldSpecification {
public function getFieldKey() {
return 'revision';
}
public function getName() {
- return 'Revision';
+ return pht('Revision');
}
public function getRequiredHandlePHIDsForPropertyView() {
$requested_object = $this->getObject()->getRequestedObjectPHID();
if (!($requested_object instanceof DifferentialRevision)) {
return array();
}
return array(
$requested_object->getPHID(),
);
}
public function renderPropertyViewValue(array $handles) {
return $this->renderHandleList($handles);
}
}
diff --git a/src/applications/releeph/field/specification/ReleephSeverityFieldSpecification.php b/src/applications/releeph/field/specification/ReleephSeverityFieldSpecification.php
index 07fa55de6..18beb35a1 100644
--- a/src/applications/releeph/field/specification/ReleephSeverityFieldSpecification.php
+++ b/src/applications/releeph/field/specification/ReleephSeverityFieldSpecification.php
@@ -1,50 +1,53 @@
<?php
final class ReleephSeverityFieldSpecification
extends ReleephLevelFieldSpecification {
const HOTFIX = 'HOTFIX';
const RELEASE = 'RELEASE';
public function getFieldKey() {
return 'severity';
}
public function getName() {
return 'Severity';
}
public function getStorageKey() {
return 'releeph:severity';
}
public function getLevels() {
return array(
self::HOTFIX,
self::RELEASE,
);
}
public function getDefaultLevel() {
return self::RELEASE;
}
public function getNameForLevel($level) {
static $names = array(
self::HOTFIX => 'HOTFIX',
self::RELEASE => 'RELEASE',
);
return idx($names, $level, $level);
}
public function getDescriptionForLevel($level) {
- static $descriptions = array(
- self::HOTFIX =>
- 'Needs merging and fixing right now.',
- self::RELEASE =>
- 'Required for the currently rolling release.',
- );
+ static $descriptions;
+
+ if ($descriptions === null) {
+ $descriptions = array(
+ self::HOTFIX => pht('Needs merging and fixing right now.'),
+ self::RELEASE => pht('Required for the currently rolling release.'),
+ );
+ }
+
return idx($descriptions, $level);
}
}
diff --git a/src/applications/releeph/field/specification/ReleephSummaryFieldSpecification.php b/src/applications/releeph/field/specification/ReleephSummaryFieldSpecification.php
index a4f1e1bdd..84767bcf6 100644
--- a/src/applications/releeph/field/specification/ReleephSummaryFieldSpecification.php
+++ b/src/applications/releeph/field/specification/ReleephSummaryFieldSpecification.php
@@ -1,53 +1,53 @@
<?php
final class ReleephSummaryFieldSpecification
extends ReleephFieldSpecification {
const MAX_SUMMARY_LENGTH = 60;
public function shouldAppearInPropertyView() {
return false;
}
public function getFieldKey() {
return 'summary';
}
public function getName() {
return 'Summary';
}
public function getStorageKey() {
return 'summary';
}
private $error = false;
public function renderEditControl(array $handles) {
return id(new AphrontFormTextControl())
->setLabel('Summary')
->setName('summary')
->setError($this->error)
->setValue($this->getValue())
- ->setCaption(
- 'Leave this blank to use the original commit title');
+ ->setCaption(pht('Leave this blank to use the original commit title'));
}
public function renderHelpForArcanist() {
- $text =
+ $text = pht(
"A one-line title summarizing this request. ".
- "Leave blank to use the original commit title.\n";
+ 'Leave blank to use the original commit title.')."\n";
return phutil_console_wrap($text, 8);
}
public function validate($summary) {
if ($summary && strlen($summary) > self::MAX_SUMMARY_LENGTH) {
- $this->error = 'Too long!';
+ $this->error = pht('Too long!');
throw new ReleephFieldParseException(
- $this, sprintf(
+ $this,
+ pht(
'Please keep your summary to under %d characters.',
self::MAX_SUMMARY_LENGTH));
}
}
}
diff --git a/src/applications/releeph/mail/ReleephRequestReplyHandler.php b/src/applications/releeph/mail/ReleephRequestReplyHandler.php
index a0a049379..964894194 100644
--- a/src/applications/releeph/mail/ReleephRequestReplyHandler.php
+++ b/src/applications/releeph/mail/ReleephRequestReplyHandler.php
@@ -1,16 +1,16 @@
<?php
final class ReleephRequestReplyHandler
extends PhabricatorApplicationTransactionReplyHandler {
public function validateMailReceiver($mail_receiver) {
if (!($mail_receiver instanceof ReleephRequest)) {
- throw new Exception('Mail receiver is not a ReleephRequest!');
+ throw new Exception(pht('Mail receiver is not a %s!', 'ReleephRequest'));
}
}
public function getObjectPrefix() {
return 'Y';
}
}
diff --git a/src/applications/releeph/query/ReleephBranchQuery.php b/src/applications/releeph/query/ReleephBranchQuery.php
index 201c2653d..9d7c88401 100644
--- a/src/applications/releeph/query/ReleephBranchQuery.php
+++ b/src/applications/releeph/query/ReleephBranchQuery.php
@@ -1,152 +1,152 @@
<?php
final class ReleephBranchQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $ids;
private $phids;
private $productPHIDs;
private $productIDs;
const STATUS_ALL = 'status-all';
const STATUS_OPEN = 'status-open';
private $status = self::STATUS_ALL;
private $needCutPointCommits;
public function withIDs(array $ids) {
$this->ids = $ids;
return $this;
}
public function withPHIDs(array $phids) {
$this->phids = $phids;
return $this;
}
public function needCutPointCommits($need_commits) {
$this->needCutPointCommits = $need_commits;
return $this;
}
public function withStatus($status) {
$this->status = $status;
return $this;
}
public function withProductPHIDs($product_phids) {
$this->productPHIDs = $product_phids;
return $this;
}
protected function loadPage() {
$table = new ReleephBranch();
$conn_r = $table->establishConnection('r');
$data = queryfx_all(
$conn_r,
'SELECT * FROM %T %Q %Q %Q',
$table->getTableName(),
$this->buildWhereClause($conn_r),
$this->buildOrderClause($conn_r),
$this->buildLimitClause($conn_r));
return $table->loadAllFromArray($data);
}
protected function willExecute() {
if ($this->productPHIDs !== null) {
$products = id(new ReleephProductQuery())
->setViewer($this->getViewer())
->withPHIDs($this->productPHIDs)
->execute();
if (!$products) {
throw new PhabricatorEmptyQueryException();
}
$this->productIDs = mpull($products, 'getID');
}
}
protected function willFilterPage(array $branches) {
$project_ids = mpull($branches, 'getReleephProjectID');
$projects = id(new ReleephProductQuery())
->withIDs($project_ids)
->setViewer($this->getViewer())
->execute();
foreach ($branches as $key => $branch) {
$project_id = $project_ids[$key];
if (isset($projects[$project_id])) {
$branch->attachProject($projects[$project_id]);
} else {
unset($branches[$key]);
}
}
if ($this->needCutPointCommits) {
$commit_phids = mpull($branches, 'getCutPointCommitPHID');
$commits = id(new DiffusionCommitQuery())
->setViewer($this->getViewer())
->withPHIDs($commit_phids)
->execute();
$commits = mpull($commits, null, 'getPHID');
foreach ($branches as $branch) {
$commit = idx($commits, $branch->getCutPointCommitPHID());
$branch->attachCutPointCommit($commit);
}
}
return $branches;
}
protected function buildWhereClause(AphrontDatabaseConnection $conn_r) {
$where = array();
if ($this->ids !== null) {
$where[] = qsprintf(
$conn_r,
'id IN (%Ld)',
$this->ids);
}
if ($this->phids !== null) {
$where[] = qsprintf(
$conn_r,
'phid IN (%Ls)',
$this->phids);
}
if ($this->productIDs !== null) {
$where[] = qsprintf(
$conn_r,
'releephProjectID IN (%Ld)',
$this->productIDs);
}
$status = $this->status;
switch ($status) {
case self::STATUS_ALL:
break;
case self::STATUS_OPEN:
$where[] = qsprintf(
$conn_r,
'isActive = 1');
break;
default:
- throw new Exception("Unknown status constant '{$status}'!");
+ throw new Exception(pht("Unknown status constant '%s'!", $status));
}
$where[] = $this->buildPagingClause($conn_r);
return $this->formatWhereClause($where);
}
public function getQueryApplicationClass() {
return 'PhabricatorReleephApplication';
}
}
diff --git a/src/applications/releeph/query/ReleephRequestQuery.php b/src/applications/releeph/query/ReleephRequestQuery.php
index 1a5965ad4..363a3b05d 100644
--- a/src/applications/releeph/query/ReleephRequestQuery.php
+++ b/src/applications/releeph/query/ReleephRequestQuery.php
@@ -1,247 +1,247 @@
<?php
final class ReleephRequestQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $requestedCommitPHIDs;
private $ids;
private $phids;
private $severities;
private $requestorPHIDs;
private $branchIDs;
private $requestedObjectPHIDs;
const STATUS_ALL = 'status-all';
const STATUS_OPEN = 'status-open';
const STATUS_REQUESTED = 'status-requested';
const STATUS_NEEDS_PULL = 'status-needs-pull';
const STATUS_REJECTED = 'status-rejected';
const STATUS_ABANDONED = 'status-abandoned';
const STATUS_PULLED = 'status-pulled';
const STATUS_NEEDS_REVERT = 'status-needs-revert';
const STATUS_REVERTED = 'status-reverted';
private $status = self::STATUS_ALL;
public function withIDs(array $ids) {
$this->ids = $ids;
return $this;
}
public function withPHIDs(array $phids) {
$this->phids = $phids;
return $this;
}
public function withBranchIDs(array $branch_ids) {
$this->branchIDs = $branch_ids;
return $this;
}
public function withStatus($status) {
$this->status = $status;
return $this;
}
public function withRequestedCommitPHIDs(array $requested_commit_phids) {
$this->requestedCommitPHIDs = $requested_commit_phids;
return $this;
}
public function withRequestorPHIDs(array $phids) {
$this->requestorPHIDs = $phids;
return $this;
}
public function withSeverities(array $severities) {
$this->severities = $severities;
return $this;
}
public function withRequestedObjectPHIDs(array $phids) {
$this->requestedObjectPHIDs = $phids;
return $this;
}
protected function loadPage() {
$table = new ReleephRequest();
$conn_r = $table->establishConnection('r');
$data = queryfx_all(
$conn_r,
'SELECT * FROM %T %Q %Q %Q',
$table->getTableName(),
$this->buildWhereClause($conn_r),
$this->buildOrderClause($conn_r),
$this->buildLimitClause($conn_r));
return $table->loadAllFromArray($data);
}
protected function willFilterPage(array $requests) {
// Load requested objects: you must be able to see an object to see
// requests for it.
$object_phids = mpull($requests, 'getRequestedObjectPHID');
$objects = id(new PhabricatorObjectQuery())
->setViewer($this->getViewer())
->setParentQuery($this)
->withPHIDs($object_phids)
->execute();
foreach ($requests as $key => $request) {
$object_phid = $request->getRequestedObjectPHID();
$object = idx($objects, $object_phid);
if (!$object) {
unset($requests[$key]);
continue;
}
$request->attachRequestedObject($object);
}
if ($this->severities) {
$severities = array_fuse($this->severities);
foreach ($requests as $key => $request) {
// NOTE: Facebook uses a custom field here.
if (ReleephDefaultFieldSelector::isFacebook()) {
$severity = $request->getDetail('severity');
} else {
$severity = $request->getDetail('releeph:severity');
}
if (empty($severities[$severity])) {
unset($requests[$key]);
}
}
}
$branch_ids = array_unique(mpull($requests, 'getBranchID'));
$branches = id(new ReleephBranchQuery())
->withIDs($branch_ids)
->setViewer($this->getViewer())
->execute();
$branches = mpull($branches, null, 'getID');
foreach ($requests as $key => $request) {
$branch = idx($branches, $request->getBranchID());
if (!$branch) {
unset($requests[$key]);
continue;
}
$request->attachBranch($branch);
}
// TODO: These should be serviced by the query, but are not currently
// denormalized anywhere. For now, filter them here instead. Note that
// we must perform this filtering *after* querying and attaching branches,
// because request status depends on the product.
$keep_status = array_fuse($this->getKeepStatusConstants());
if ($keep_status) {
foreach ($requests as $key => $request) {
if (empty($keep_status[$request->getStatus()])) {
unset($requests[$key]);
}
}
}
return $requests;
}
protected function buildWhereClause(AphrontDatabaseConnection $conn_r) {
$where = array();
if ($this->ids !== null) {
$where[] = qsprintf(
$conn_r,
'id IN (%Ld)',
$this->ids);
}
if ($this->phids !== null) {
$where[] = qsprintf(
$conn_r,
'phid IN (%Ls)',
$this->phids);
}
if ($this->branchIDs !== null) {
$where[] = qsprintf(
$conn_r,
'branchID IN (%Ld)',
$this->branchIDs);
}
if ($this->requestedCommitPHIDs !== null) {
$where[] = qsprintf(
$conn_r,
'requestCommitPHID IN (%Ls)',
$this->requestedCommitPHIDs);
}
if ($this->requestorPHIDs !== null) {
$where[] = qsprintf(
$conn_r,
'requestUserPHID IN (%Ls)',
$this->requestorPHIDs);
}
if ($this->requestedObjectPHIDs !== null) {
$where[] = qsprintf(
$conn_r,
'requestedObjectPHID IN (%Ls)',
$this->requestedObjectPHIDs);
}
$where[] = $this->buildPagingClause($conn_r);
return $this->formatWhereClause($where);
}
private function getKeepStatusConstants() {
switch ($this->status) {
case self::STATUS_ALL:
return array();
case self::STATUS_OPEN:
return array(
ReleephRequestStatus::STATUS_REQUESTED,
ReleephRequestStatus::STATUS_NEEDS_PICK,
ReleephRequestStatus::STATUS_NEEDS_REVERT,
);
case self::STATUS_REQUESTED:
return array(
ReleephRequestStatus::STATUS_REQUESTED,
);
case self::STATUS_NEEDS_PULL:
return array(
ReleephRequestStatus::STATUS_NEEDS_PICK,
);
case self::STATUS_REJECTED:
return array(
ReleephRequestStatus::STATUS_REJECTED,
);
case self::STATUS_ABANDONED:
return array(
ReleephRequestStatus::STATUS_ABANDONED,
);
case self::STATUS_PULLED:
return array(
ReleephRequestStatus::STATUS_PICKED,
);
case self::STATUS_NEEDS_REVERT:
return array(
ReleephRequestStatus::NEEDS_REVERT,
);
case self::STATUS_REVERTED:
return array(
ReleephRequestStatus::REVERTED,
);
default:
- throw new Exception("Unknown status '{$this->status}'!");
+ throw new Exception(pht("Unknown status '%s'!", $this->status));
}
}
public function getQueryApplicationClass() {
return 'PhabricatorReleephApplication';
}
}
diff --git a/src/applications/releeph/storage/ReleephRequest.php b/src/applications/releeph/storage/ReleephRequest.php
index f2b51b6d8..c071600ec 100644
--- a/src/applications/releeph/storage/ReleephRequest.php
+++ b/src/applications/releeph/storage/ReleephRequest.php
@@ -1,366 +1,366 @@
<?php
final class ReleephRequest extends ReleephDAO
implements
PhabricatorApplicationTransactionInterface,
PhabricatorPolicyInterface,
PhabricatorCustomFieldInterface {
protected $branchID;
protected $requestUserPHID;
protected $details = array();
protected $userIntents = array();
protected $inBranch;
protected $pickStatus;
protected $mailKey;
/**
* The object which is being requested. Normally this is a commit, but it
* might also be a revision. In the future, it could be a repository branch
* or an external object (like a GitHub pull request).
*/
protected $requestedObjectPHID;
// Information about the thing being requested
protected $requestCommitPHID;
// Information about the last commit to the releeph branch
protected $commitIdentifier;
protected $commitPHID;
private $customFields = self::ATTACHABLE;
private $branch = self::ATTACHABLE;
private $requestedObject = self::ATTACHABLE;
/* -( Constants and helper methods )--------------------------------------- */
const INTENT_WANT = 'want';
const INTENT_PASS = 'pass';
const PICK_PENDING = 1; // old
const PICK_FAILED = 2;
const PICK_OK = 3;
const PICK_MANUAL = 4; // old
const REVERT_OK = 5;
const REVERT_FAILED = 6;
public function shouldBeInBranch() {
return
$this->getPusherIntent() == self::INTENT_WANT &&
/**
* We use "!= pass" instead of "== want" in case the requestor intent is
* not present. In other words, only revert if the requestor explicitly
* passed.
*/
$this->getRequestorIntent() != self::INTENT_PASS;
}
/**
* Will return INTENT_WANT if any pusher wants this request, and no pusher
* passes on this request.
*/
public function getPusherIntent() {
$product = $this->getBranch()->getProduct();
if (!$product->getPushers()) {
return self::INTENT_WANT;
}
$found_pusher_want = false;
foreach ($this->userIntents as $phid => $intent) {
if ($product->isAuthoritativePHID($phid)) {
if ($intent == self::INTENT_PASS) {
return self::INTENT_PASS;
}
$found_pusher_want = true;
}
}
if ($found_pusher_want) {
return self::INTENT_WANT;
} else {
return null;
}
}
public function getRequestorIntent() {
return idx($this->userIntents, $this->requestUserPHID);
}
public function getStatus() {
return $this->calculateStatus();
}
public function getMonogram() {
return 'Y'.$this->getID();
}
public function getBranch() {
return $this->assertAttached($this->branch);
}
public function attachBranch(ReleephBranch $branch) {
$this->branch = $branch;
return $this;
}
public function getRequestedObject() {
return $this->assertAttached($this->requestedObject);
}
public function attachRequestedObject($object) {
$this->requestedObject = $object;
return $this;
}
private function calculateStatus() {
if ($this->shouldBeInBranch()) {
if ($this->getInBranch()) {
return ReleephRequestStatus::STATUS_PICKED;
} else {
return ReleephRequestStatus::STATUS_NEEDS_PICK;
}
} else {
if ($this->getInBranch()) {
return ReleephRequestStatus::STATUS_NEEDS_REVERT;
} else {
$intent_pass = self::INTENT_PASS;
$intent_want = self::INTENT_WANT;
$has_been_in_branch = $this->getCommitIdentifier();
// Regardless of why we reverted something, always say reverted if it
// was once in the branch.
if ($has_been_in_branch) {
return ReleephRequestStatus::STATUS_REVERTED;
} else if ($this->getPusherIntent() === $intent_pass) {
// Otherwise, if it has never been in the branch, explicitly say why:
return ReleephRequestStatus::STATUS_REJECTED;
} else if ($this->getRequestorIntent() === $intent_want) {
return ReleephRequestStatus::STATUS_REQUESTED;
} else {
return ReleephRequestStatus::STATUS_ABANDONED;
}
}
}
}
/* -( Lisk mechanics )----------------------------------------------------- */
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'details' => self::SERIALIZATION_JSON,
'userIntents' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'requestCommitPHID' => 'phid?',
'commitIdentifier' => 'text40?',
'commitPHID' => 'phid?',
'pickStatus' => 'uint32?',
'inBranch' => 'bool',
'mailKey' => 'bytes20',
'userIntents' => 'text?',
),
self::CONFIG_KEY_SCHEMA => array(
'key_phid' => null,
'phid' => array(
'columns' => array('phid'),
'unique' => true,
),
'requestIdentifierBranch' => array(
'columns' => array('requestCommitPHID', 'branchID'),
'unique' => true,
),
'branchID' => array(
'columns' => array('branchID'),
),
'key_requestedObject' => array(
'columns' => array('requestedObjectPHID'),
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
ReleephRequestPHIDType::TYPECONST);
}
public function save() {
if (!$this->getMailKey()) {
$this->setMailKey(Filesystem::readRandomCharacters(20));
}
return parent::save();
}
/* -( Helpful accessors )--------------------------------------------------- */
public function getDetail($key, $default = null) {
return idx($this->getDetails(), $key, $default);
}
public function setDetail($key, $value) {
$this->details[$key] = $value;
return $this;
}
/**
* Get the commit PHIDs this request is requesting.
*
* NOTE: For now, this always returns one PHID.
*
* @return list<phid> Commit PHIDs requested by this request.
*/
public function getCommitPHIDs() {
return array(
$this->requestCommitPHID,
);
}
public function getReason() {
// Backward compatibility: reason used to be called comments
$reason = $this->getDetail('reason');
if (!$reason) {
return $this->getDetail('comments');
}
return $reason;
}
/**
* Allow a null summary, and fall back to the title of the commit.
*/
public function getSummaryForDisplay() {
$summary = $this->getDetail('summary');
if (!strlen($summary)) {
$commit = $this->loadPhabricatorRepositoryCommit();
if ($commit) {
$summary = $commit->getSummary();
}
}
if (!strlen($summary)) {
$summary = pht('None');
}
return $summary;
}
/* -( Loading external objects )------------------------------------------- */
public function loadPhabricatorRepositoryCommit() {
return $this->loadOneRelative(
new PhabricatorRepositoryCommit(),
'phid',
'getRequestCommitPHID');
}
public function loadPhabricatorRepositoryCommitData() {
$commit = $this->loadPhabricatorRepositoryCommit();
if ($commit) {
return $commit->loadOneRelative(
new PhabricatorRepositoryCommitData(),
'commitID');
}
}
/* -( State change helpers )----------------------------------------------- */
public function setUserIntent(PhabricatorUser $user, $intent) {
$this->userIntents[$user->getPHID()] = $intent;
return $this;
}
/* -( Migrating to status-less ReleephRequests )--------------------------- */
protected function didReadData() {
if ($this->userIntents === null) {
$this->userIntents = array();
}
}
public function setStatus($value) {
- throw new Exception('`status` is now deprecated!');
+ throw new Exception(pht('`%s` is now deprecated!', 'status'));
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new ReleephRequestTransactionalEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new ReleephRequestTransaction();
}
public function willRenderTimeline(
PhabricatorApplicationTransactionView $timeline,
AphrontRequest $request) {
return $timeline;
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
return $this->getBranch()->getPolicy($capability);
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return $this->getBranch()->hasAutomaticCapability($capability, $viewer);
}
public function describeAutomaticCapability($capability) {
return pht(
'Pull requests have the same policies as the branches they are '.
'requested against.');
}
/* -( PhabricatorCustomFieldInterface )------------------------------------ */
public function getCustomFieldSpecificationForRole($role) {
return PhabricatorEnv::getEnvConfig('releeph.fields');
}
public function getCustomFieldBaseClass() {
return 'ReleephFieldSpecification';
}
public function getCustomFields() {
return $this->assertAttached($this->customFields);
}
public function attachCustomFields(PhabricatorCustomFieldAttachment $fields) {
$this->customFields = $fields;
return $this;
}
}
diff --git a/src/applications/releeph/view/branch/ReleephBranchPreviewView.php b/src/applications/releeph/view/branch/ReleephBranchPreviewView.php
index d81f1f5d0..afb3dffd4 100644
--- a/src/applications/releeph/view/branch/ReleephBranchPreviewView.php
+++ b/src/applications/releeph/view/branch/ReleephBranchPreviewView.php
@@ -1,60 +1,62 @@
<?php
final class ReleephBranchPreviewView extends AphrontFormControl {
private $statics = array();
private $dynamics = array();
public function addControl($param_name, AphrontFormControl $control) {
$celerity_id = celerity_generate_unique_node_id();
$control->setID($celerity_id);
$this->dynamics[$param_name] = $celerity_id;
return $this;
}
public function addStatic($param_name, $value) {
$this->statics[$param_name] = $value;
return $this;
}
protected function getCustomControlClass() {
require_celerity_resource('releeph-preview-branch');
return 'releeph-preview-branch';
}
protected function renderInput() {
static $required_params = array(
'arcProjectID',
'projectName',
'isSymbolic',
'template',
);
$all_params = array_merge($this->statics, $this->dynamics);
foreach ($required_params as $param_name) {
if (idx($all_params, $param_name) === null) {
throw new Exception(
- "'{$param_name}' is not set as either a static or dynamic!");
+ pht(
+ "'%s' is not set as either a static or dynamic!",
+ $param_name));
}
}
$output_id = celerity_generate_unique_node_id();
Javelin::initBehavior('releeph-preview-branch', array(
'uri' => '/releeph/branch/preview/',
'outputID' => $output_id,
'params' => array(
'static' => $this->statics,
'dynamic' => $this->dynamics,
),
));
return phutil_tag(
'div',
array(
'id' => $output_id,
),
'');
}
}
diff --git a/src/applications/releeph/view/branch/ReleephBranchTemplate.php b/src/applications/releeph/view/branch/ReleephBranchTemplate.php
index 030c70f8e..79d19b858 100644
--- a/src/applications/releeph/view/branch/ReleephBranchTemplate.php
+++ b/src/applications/releeph/view/branch/ReleephBranchTemplate.php
@@ -1,201 +1,206 @@
<?php
final class ReleephBranchTemplate {
const KEY = 'releeph.default-branch-template';
public static function getDefaultTemplate() {
return PhabricatorEnv::getEnvConfig(self::KEY);
}
public static function getRequiredDefaultTemplate() {
$template = self::getDefaultTemplate();
if (!$template) {
- throw new Exception(sprintf(
+ throw new Exception(pht(
"Config setting '%s' must be set, ".
"or you must provide a branch-template for each project!",
self::KEY));
}
return $template;
}
public static function getFakeCommitHandleFor(
$arc_project_id,
PhabricatorUser $viewer) {
$arc_project = id(new PhabricatorRepositoryArcanistProject())
->load($arc_project_id);
if (!$arc_project) {
throw new Exception(
- "No Arc project found with id '{$arc_project_id}'!");
+ pht(
+ "No Arc project found with id '%s'!",
+ $arc_project_id));
}
$repository = null;
if ($arc_project->getRepositoryID()) {
$repository = id(new PhabricatorRepositoryQuery())
->setViewer($viewer)
->withIDs(array($arc_project->getRepositoryID()))
->executeOne();
}
$fake_handle = 'SOFAKE';
if ($repository) {
$fake_handle = id(new PhabricatorObjectHandle())
->setName($repository->formatCommitName('100000000000'));
}
return $fake_handle;
}
private $commitHandle;
private $branchDate = null;
private $projectName;
private $isSymbolic;
public function setCommitHandle(PhabricatorObjectHandle $handle) {
$this->commitHandle = $handle;
return $this;
}
public function setBranchDate($branch_date) {
$this->branchDate = $branch_date;
return $this;
}
public function setReleephProjectName($project_name) {
$this->projectName = $project_name;
return $this;
}
public function setSymbolic($is_symbolic) {
$this->isSymbolic = $is_symbolic;
return $this;
}
public function interpolate($template) {
if (!$this->projectName) {
return array('', array());
}
list($name, $name_errors) = $this->interpolateInner(
$template,
$this->isSymbolic);
if ($this->isSymbolic) {
return array($name, $name_errors);
} else {
$validate_errors = $this->validateAsBranchName($name);
$errors = array_merge($name_errors, $validate_errors);
return array($name, $errors);
}
}
/*
* xsprintf() would be useful here, but that's for formatting concrete lists
* of things in a certain way...
*
* animal_printf('%A %A %A', $dog1, $dog2, $dog3);
*
* ...rather than interpolating percent-control-strings like strftime does.
*/
private function interpolateInner($template, $is_symbolic) {
$name = $template;
$errors = array();
$safe_project_name = str_replace(' ', '-', $this->projectName);
$short_commit_id = last(
preg_split('/r[A-Z]+/', $this->commitHandle->getName()));
$interpolations = array();
for ($ii = 0; $ii < strlen($name); $ii++) {
$char = substr($name, $ii, 1);
$prev = null;
if ($ii > 0) {
$prev = substr($name, $ii - 1, 1);
}
$next = substr($name, $ii + 1, 1);
if ($next && $char == '%' && $prev != '%') {
$interpolations[$ii] = $next;
}
}
$variable_interpolations = array();
$reverse_interpolations = $interpolations;
krsort($reverse_interpolations);
if ($this->branchDate) {
$branch_date = $this->branchDate;
} else {
$branch_date = $this->commitHandle->getTimestamp();
}
foreach ($reverse_interpolations as $position => $code) {
$replacement = null;
switch ($code) {
case 'v':
$replacement = $this->commitHandle->getName();
$is_variable = true;
break;
case 'V':
$replacement = $short_commit_id;
$is_variable = true;
break;
case 'P':
$replacement = $safe_project_name;
$is_variable = false;
break;
case 'p':
$replacement = strtolower($safe_project_name);
$is_variable = false;
break;
default:
// Format anything else using strftime()
$replacement = strftime("%{$code}", $branch_date);
$is_variable = true;
break;
}
if ($is_variable) {
$variable_interpolations[] = $code;
}
$name = substr_replace($name, $replacement, $position, 2);
}
if (!$is_symbolic && !$variable_interpolations) {
- $errors[] = "Include additional interpolations that aren't static!";
+ $errors[] = pht("Include additional interpolations that aren't static!");
}
return array($name, $errors);
}
private function validateAsBranchName($name) {
$errors = array();
if (preg_match('{^/}', $name) || preg_match('{/$}', $name)) {
- $errors[] = "Branches cannot begin or end with '/'";
+ $errors[] = pht("Branches cannot begin or end with '%s'", '/');
}
if (preg_match('{//+}', $name)) {
- $errors[] = "Branches cannot contain multiple consective '/'";
+ $errors[] = pht("Branches cannot contain multiple consecutive '%s'", '/');
}
$parts = array_filter(explode('/', $name));
foreach ($parts as $index => $part) {
$part_error = null;
if (preg_match('{^\.}', $part) || preg_match('{\.$}', $part)) {
- $errors[] = "Path components cannot begin or end with '.'";
+ $errors[] = pht("Path components cannot begin or end with '%s'", '.');
} else if (preg_match('{^(?!\w)}', $part)) {
- $errors[] = 'Path components must begin with an alphanumeric';
+ $errors[] = pht('Path components must begin with an alphanumeric.');
} else if (!preg_match('{^\w ([\w-_%\.]* [\w-_%])?$}x', $part)) {
- $errors[] =
+ $errors[] = pht(
"Path components may only contain alphanumerics ".
- "or '-', '_', or '.'";
+ "or '%s', '%s' or '%s'.",
+ '-',
+ '_',
+ '.');
}
}
return $errors;
}
}
diff --git a/src/applications/remarkup/conduit/RemarkupProcessConduitAPIMethod.php b/src/applications/remarkup/conduit/RemarkupProcessConduitAPIMethod.php
index d6c055423..f9169cd08 100644
--- a/src/applications/remarkup/conduit/RemarkupProcessConduitAPIMethod.php
+++ b/src/applications/remarkup/conduit/RemarkupProcessConduitAPIMethod.php
@@ -1,76 +1,76 @@
<?php
final class RemarkupProcessConduitAPIMethod extends ConduitAPIMethod {
public function getAPIMethodName() {
return 'remarkup.process';
}
public function getMethodStatus() {
return self::METHOD_STATUS_UNSTABLE;
}
public function getMethodDescription() {
- return 'Process text through remarkup in phabricator context.';
+ return pht('Process text through remarkup in Phabricator context.');
}
protected function defineReturnType() {
return 'nonempty dict';
}
protected function defineErrorTypes() {
return array(
- 'ERR-NO-CONTENT' => 'Content may not be empty.',
- 'ERR-INVALID-ENGINE' => 'Invalid markup engine.',
+ 'ERR-NO-CONTENT' => pht('Content may not be empty.'),
+ 'ERR-INVALID-ENGINE' => pht('Invalid markup engine.'),
);
}
protected function defineParamTypes() {
$available_contexts = array_keys($this->getEngineContexts());
$available_const = $this->formatStringConstants($available_contexts);
return array(
'context' => 'required '.$available_const,
'contents' => 'required list<string>',
);
}
protected function execute(ConduitAPIRequest $request) {
$contents = $request->getValue('contents');
$context = $request->getValue('context');
$engine_class = idx($this->getEngineContexts(), $context);
if (!$engine_class) {
throw new ConduitException('ERR-INVALID_ENGINE');
}
$engine = PhabricatorMarkupEngine::$engine_class();
$engine->setConfig('viewer', $request->getUser());
$results = array();
foreach ($contents as $content) {
$text = $engine->markupText($content);
if ($text) {
$content = hsprintf('%s', $text)->getHTMLContent();
} else {
$content = '';
}
$results[] = array(
'content' => $content,
);
}
return $results;
}
private function getEngineContexts() {
return array(
'phriction' => 'newPhrictionMarkupEngine',
'maniphest' => 'newManiphestMarkupEngine',
'differential' => 'newDifferentialMarkupEngine',
'phame' => 'newPhameMarkupEngine',
'feed' => 'newFeedMarkupEngine',
'diffusion' => 'newDiffusionMarkupEngine',
);
}
}
diff --git a/src/applications/repository/conduit/RepositoryCreateConduitAPIMethod.php b/src/applications/repository/conduit/RepositoryCreateConduitAPIMethod.php
index 42df1b6d4..ad4b62104 100644
--- a/src/applications/repository/conduit/RepositoryCreateConduitAPIMethod.php
+++ b/src/applications/repository/conduit/RepositoryCreateConduitAPIMethod.php
@@ -1,142 +1,140 @@
<?php
final class RepositoryCreateConduitAPIMethod
extends RepositoryConduitAPIMethod {
public function getAPIMethodName() {
return 'repository.create';
}
public function getMethodStatus() {
return self::METHOD_STATUS_UNSTABLE;
}
public function getMethodStatusDescription() {
- return 'Repository methods are new and subject to change.';
+ return pht('Repository methods are new and subject to change.');
}
public function getMethodDescription() {
return pht('Create a new repository.');
}
protected function defineParamTypes() {
$vcs_const = $this->formatStringConstants(array('git', 'hg', 'svn'));
return array(
'name' => 'required string',
'vcs' => 'required '.$vcs_const,
'callsign' => 'required string',
'description' => 'optional string',
'encoding' => 'optional string',
'tracking' => 'optional bool',
'uri' => 'required string',
'credentialPHID' => 'optional string',
'svnSubpath' => 'optional string',
'branchFilter' => 'optional list<string>',
'closeCommitsFilter' => 'optional list<string>',
'pullFrequency' => 'optional int',
'defaultBranch' => 'optional string',
'heraldEnabled' => 'optional bool, default = true',
'autocloseEnabled' => 'optional bool, default = true',
'svnUUID' => 'optional string',
);
}
protected function defineReturnType() {
return 'nonempty dict';
}
protected function defineErrorTypes() {
return array(
- 'ERR-DUPLICATE' =>
- 'Duplicate repository callsign.',
- 'ERR-BAD-CALLSIGN' =>
- 'Callsign is required and must be ALL UPPERCASE LETTERS.',
- 'ERR-UNKNOWN-REPOSITORY-VCS' =>
- 'Unknown repository VCS type.',
+ 'ERR-DUPLICATE' => pht('Duplicate repository callsign.'),
+ 'ERR-BAD-CALLSIGN' => pht(
+ 'Callsign is required and must be ALL UPPERCASE LETTERS.'),
+ 'ERR-UNKNOWN-REPOSITORY-VCS' => pht('Unknown repository VCS type.'),
);
}
protected function execute(ConduitAPIRequest $request) {
$application = id(new PhabricatorApplicationQuery())
->setViewer($request->getUser())
->withClasses(array('PhabricatorDiffusionApplication'))
->executeOne();
PhabricatorPolicyFilter::requireCapability(
$request->getUser(),
$application,
DiffusionCreateRepositoriesCapability::CAPABILITY);
// TODO: This has some duplication with (and lacks some of the validation
// of) the web workflow; refactor things so they can share more code as this
// stabilizes. Specifically, this should move to transactions since they
// work properly now.
$repository = PhabricatorRepository::initializeNewRepository(
$request->getUser());
$repository->setName($request->getValue('name'));
$callsign = $request->getValue('callsign');
if (!preg_match('/^[A-Z]+\z/', $callsign)) {
throw new ConduitException('ERR-BAD-CALLSIGN');
}
$repository->setCallsign($callsign);
$local_path = PhabricatorEnv::getEnvConfig(
'repository.default-local-path');
$local_path = rtrim($local_path, '/');
$local_path = $local_path.'/'.$callsign.'/';
$vcs = $request->getValue('vcs');
$map = array(
'git' => PhabricatorRepositoryType::REPOSITORY_TYPE_GIT,
'hg' => PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL,
'svn' => PhabricatorRepositoryType::REPOSITORY_TYPE_SVN,
);
if (empty($map[$vcs])) {
throw new ConduitException('ERR-UNKNOWN-REPOSITORY-VCS');
}
$repository->setVersionControlSystem($map[$vcs]);
$repository->setCredentialPHID($request->getValue('credentialPHID'));
$remote_uri = $request->getValue('uri');
PhabricatorRepository::assertValidRemoteURI($remote_uri);
$details = array(
'encoding' => $request->getValue('encoding'),
'description' => $request->getValue('description'),
'tracking-enabled' => (bool)$request->getValue('tracking', true),
'remote-uri' => $remote_uri,
'local-path' => $local_path,
'branch-filter' => array_fill_keys(
$request->getValue('branchFilter', array()),
true),
'close-commits-filter' => array_fill_keys(
$request->getValue('closeCommitsFilter', array()),
true),
'pull-frequency' => $request->getValue('pullFrequency'),
'default-branch' => $request->getValue('defaultBranch'),
'herald-disabled' => !$request->getValue('heraldEnabled', true),
'svn-subpath' => $request->getValue('svnSubpath'),
'disable-autoclose' => !$request->getValue('autocloseEnabled', true),
);
foreach ($details as $key => $value) {
$repository->setDetail($key, $value);
}
try {
$repository->save();
} catch (AphrontDuplicateKeyQueryException $ex) {
throw new ConduitException('ERR-DUPLICATE');
}
return $repository->toDictionary();
}
}
diff --git a/src/applications/repository/controller/PhabricatorRepositoryArcanistProjectDeleteController.php b/src/applications/repository/controller/PhabricatorRepositoryArcanistProjectDeleteController.php
index 4be5e1bfe..04c200889 100644
--- a/src/applications/repository/controller/PhabricatorRepositoryArcanistProjectDeleteController.php
+++ b/src/applications/repository/controller/PhabricatorRepositoryArcanistProjectDeleteController.php
@@ -1,41 +1,42 @@
<?php
final class PhabricatorRepositoryArcanistProjectDeleteController
extends PhabricatorRepositoryController {
private $id;
public function willProcessRequest(array $data) {
$this->id = $data['id'];
}
public function processRequest() {
$arc_project =
id(new PhabricatorRepositoryArcanistProject())->load($this->id);
if (!$arc_project) {
return new Aphront404Response();
}
$request = $this->getRequest();
if ($request->isDialogFormPost()) {
$arc_project->delete();
return id(new AphrontRedirectResponse())->setURI('/repository/');
}
$dialog = new AphrontDialogView();
$dialog
->setUser($request->getUser())
->setTitle(pht('Really delete this arcanist project?'))
- ->appendChild(pht(
- 'Really delete the "%s" arcanist project? '.
+ ->appendChild(
+ pht(
+ 'Really delete the "%s" arcanist project? '.
'This operation can not be undone.',
- $arc_project->getName()))
+ $arc_project->getName()))
->setSubmitURI('/repository/project/delete/'.$this->id.'/')
->addSubmitButton(pht('Delete Arcanist Project'))
->addCancelButton('/repository/');
return id(new AphrontDialogResponse())->setDialog($dialog);
}
}
diff --git a/src/applications/repository/controller/PhabricatorRepositoryArcanistProjectEditController.php b/src/applications/repository/controller/PhabricatorRepositoryArcanistProjectEditController.php
index a43917903..c453ebb05 100644
--- a/src/applications/repository/controller/PhabricatorRepositoryArcanistProjectEditController.php
+++ b/src/applications/repository/controller/PhabricatorRepositoryArcanistProjectEditController.php
@@ -1,85 +1,85 @@
<?php
final class PhabricatorRepositoryArcanistProjectEditController
extends PhabricatorRepositoryController {
private $id;
public function willProcessRequest(array $data) {
$this->id = $data['id'];
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$project = id(new PhabricatorRepositoryArcanistProject())->load($this->id);
if (!$project) {
return new Aphront404Response();
}
$repositories = id(new PhabricatorRepositoryQuery())
->setViewer($user)
->execute();
$repos = array(
0 => 'None',
);
foreach ($repositories as $repository) {
$callsign = $repository->getCallsign();
$name = $repository->getname();
$repos[$repository->getID()] = "r{$callsign} ({$name})";
}
// note "None" will still be first thanks to 'r' prefix
asort($repos);
if ($request->isFormPost()) {
$repo_id = $request->getInt('repository', 0);
if (isset($repos[$repo_id])) {
$project->setRepositoryID($repo_id);
$project->save();
return id(new AphrontRedirectResponse())
->setURI('/repository/');
}
}
$form = id(new AphrontFormView())
->setUser($user)
->appendChild(
id(new AphrontFormStaticControl())
->setLabel(pht('Name'))
->setValue($project->getName()))
->appendChild(
id(new AphrontFormStaticControl())
->setLabel('PHID')
->setValue($project->getPHID()))
->appendChild(
id(new AphrontFormSelectControl())
->setLabel(pht('Repository'))
->setOptions($repos)
->setName('repository')
->setValue($project->getRepositoryID()))
->appendChild(
id(new AphrontFormSubmitControl())
->addCancelButton('/repository/')
- ->setValue('Save'));
+ ->setValue(pht('Save')));
$panel = new PHUIObjectBoxView();
$panel->setHeaderText(pht('Edit Arcanist Project'));
$panel->setForm($form);
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb(pht('Edit Project'));
return $this->buildApplicationPage(
array(
$crumbs,
$panel,
),
array(
'title' => pht('Edit Project'),
));
}
}
diff --git a/src/applications/repository/controller/PhabricatorRepositoryListController.php b/src/applications/repository/controller/PhabricatorRepositoryListController.php
index 56e8dc37d..89d068e28 100644
--- a/src/applications/repository/controller/PhabricatorRepositoryListController.php
+++ b/src/applications/repository/controller/PhabricatorRepositoryListController.php
@@ -1,164 +1,164 @@
<?php
final class PhabricatorRepositoryListController
extends PhabricatorRepositoryController {
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$is_admin = $user->getIsAdmin();
$repos = id(new PhabricatorRepositoryQuery())
->setViewer($user)
->execute();
$repos = msort($repos, 'getName');
$rows = array();
foreach ($repos as $repo) {
if ($repo->isTracked()) {
$diffusion_link = phutil_tag(
'a',
array(
'href' => '/diffusion/'.$repo->getCallsign().'/',
),
pht('View in Diffusion'));
} else {
- $diffusion_link = phutil_tag('em', array(), 'Not Tracked');
+ $diffusion_link = phutil_tag('em', array(), pht('Not Tracked'));
}
$rows[] = array(
$repo->getCallsign(),
$repo->getName(),
PhabricatorRepositoryType::getNameForRepositoryType(
$repo->getVersionControlSystem()),
$diffusion_link,
phutil_tag(
'a',
array(
'class' => 'button small grey',
'href' => '/diffusion/'.$repo->getCallsign().'/edit/',
),
pht('Edit')),
);
}
$table = new AphrontTableView($rows);
$table->setNoDataString(pht('No Repositories'));
$table->setHeaders(
array(
pht('Callsign'),
pht('Repository'),
pht('Type'),
pht('Diffusion'),
'',
));
$table->setColumnClasses(
array(
null,
'wide',
null,
null,
'action',
));
$table->setColumnVisibility(
array(
true,
true,
true,
true,
$is_admin,
));
$panel = new PHUIObjectBoxView();
$header = new PHUIHeaderView();
$header->setHeader(pht('Repositories'));
if ($is_admin) {
$button = id(new PHUIButtonView())
->setTag('a')
->setText(pht('Create New Repository'))
->setHref('/diffusion/new/');
$header->addActionLink($button);
}
$panel->setHeader($header);
$panel->appendChild($table);
$projects = id(new PhabricatorRepositoryArcanistProject())->loadAll();
$rows = array();
foreach ($projects as $project) {
$repo = idx($repos, $project->getRepositoryID());
if ($repo) {
$repo_name = $repo->getName();
} else {
$repo_name = '-';
}
$rows[] = array(
$project->getName(),
$repo_name,
phutil_tag(
'a',
array(
'href' => '/repository/project/edit/'.$project->getID().'/',
'class' => 'button grey small',
),
pht('Edit')),
javelin_tag(
'a',
array(
'href' => '/repository/project/delete/'.$project->getID().'/',
'class' => 'button grey small',
'sigil' => 'workflow',
),
pht('Delete')),
);
}
$project_table = new AphrontTableView($rows);
$project_table->setNoDataString(pht('No Arcanist Projects'));
$project_table->setHeaders(
array(
pht('Project ID'),
pht('Repository'),
'',
'',
));
$project_table->setColumnClasses(
array(
'',
'wide',
'action',
'action',
));
$project_table->setColumnVisibility(
array(
true,
true,
$is_admin,
$is_admin,
));
$project_panel = new PHUIObjectBoxView();
$project_panel->setHeaderText(pht('Arcanist Projects'));
$project_panel->appendChild($project_table);
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb(pht('Repository List'));
return $this->buildApplicationPage(
array(
$crumbs,
$panel,
$project_panel,
),
array(
'title' => pht('Repository List'),
));
}
}
diff --git a/src/applications/repository/daemon/PhabricatorGitGraphStream.php b/src/applications/repository/daemon/PhabricatorGitGraphStream.php
index 911e30cb5..334213048 100644
--- a/src/applications/repository/daemon/PhabricatorGitGraphStream.php
+++ b/src/applications/repository/daemon/PhabricatorGitGraphStream.php
@@ -1,86 +1,89 @@
<?php
final class PhabricatorGitGraphStream
extends PhabricatorRepositoryGraphStream {
private $repository;
private $iterator;
private $parents = array();
private $dates = array();
public function __construct(
PhabricatorRepository $repository,
$start_commit) {
$this->repository = $repository;
$future = $repository->getLocalCommandFuture(
'log --format=%s %s --',
'%H%x01%P%x01%ct',
$start_commit);
$this->iterator = new LinesOfALargeExecFuture($future);
$this->iterator->setDelimiter("\n");
$this->iterator->rewind();
}
public function getParents($commit) {
if (!isset($this->parents[$commit])) {
$this->parseUntil($commit);
}
$parents = $this->parents[$commit];
// NOTE: In Git, it is possible for a commit to list the same parent more
// than once. See T5226. Discard duplicate parents.
return array_unique($parents);
}
public function getCommitDate($commit) {
if (!isset($this->dates[$commit])) {
$this->parseUntil($commit);
}
return $this->dates[$commit];
}
private function parseUntil($commit) {
if ($this->isParsed($commit)) {
return;
}
$gitlog = $this->iterator;
while ($gitlog->valid()) {
$line = $gitlog->current();
$gitlog->next();
$line = trim($line);
if (!strlen($line)) {
break;
}
list($hash, $parents, $epoch) = explode("\1", $line);
if ($parents) {
$parents = explode(' ', $parents);
} else {
// First commit.
$parents = array();
}
$this->dates[$hash] = $epoch;
$this->parents[$hash] = $parents;
if ($this->isParsed($commit)) {
return;
}
}
- throw new Exception("No such commit '{$commit}' in repository!");
+ throw new Exception(
+ pht(
+ "No such commit '%s' in repository!",
+ $commit));
}
private function isParsed($commit) {
return isset($this->dates[$commit]);
}
}
diff --git a/src/applications/repository/daemon/PhabricatorRepositoryPullLocalDaemon.php b/src/applications/repository/daemon/PhabricatorRepositoryPullLocalDaemon.php
index ff96dced3..e2895972d 100644
--- a/src/applications/repository/daemon/PhabricatorRepositoryPullLocalDaemon.php
+++ b/src/applications/repository/daemon/PhabricatorRepositoryPullLocalDaemon.php
@@ -1,429 +1,431 @@
<?php
/**
* Run pull commands on local working copies to keep them up to date. This
* daemon handles all repository types.
*
* By default, the daemon pulls **every** repository. If you want it to be
* responsible for only some repositories, you can launch it with a list of
* PHIDs or callsigns:
*
* ./phd launch repositorypulllocal -- X Q Z
*
* You can also launch a daemon which is responsible for all //but// one or
* more repositories:
*
* ./phd launch repositorypulllocal -- --not A --not B
*
* If you have a very large number of repositories and some aren't being pulled
* as frequently as you'd like, you can either change the pull frequency of
* the less-important repositories to a larger number (so the daemon will skip
* them more often) or launch one daemon for all the less-important repositories
* and one for the more important repositories (or one for each more important
* repository).
*
* @task pull Pulling Repositories
*/
final class PhabricatorRepositoryPullLocalDaemon
extends PhabricatorDaemon {
private $statusMessageCursor = 0;
/* -( Pulling Repositories )----------------------------------------------- */
/**
* @task pull
*/
protected function run() {
$argv = $this->getArgv();
array_unshift($argv, __CLASS__);
$args = new PhutilArgumentParser($argv);
$args->parse(
array(
array(
'name' => 'no-discovery',
- 'help' => 'Pull only, without discovering commits.',
+ 'help' => pht('Pull only, without discovering commits.'),
),
array(
'name' => 'not',
'param' => 'repository',
'repeat' => true,
- 'help' => 'Do not pull __repository__.',
+ 'help' => pht('Do not pull __repository__.'),
),
array(
'name' => 'repositories',
'wildcard' => true,
- 'help' => 'Pull specific __repositories__ instead of all.',
+ 'help' => pht('Pull specific __repositories__ instead of all.'),
),
));
$no_discovery = $args->getArg('no-discovery');
$include = $args->getArg('repositories');
$exclude = $args->getArg('not');
// Each repository has an individual pull frequency; after we pull it,
// wait that long to pull it again. When we start up, try to pull everything
// serially.
$retry_after = array();
$min_sleep = 15;
$max_futures = 4;
$futures = array();
$queue = array();
while (!$this->shouldExit()) {
$pullable = $this->loadPullableRepositories($include, $exclude);
// If any repositories have the NEEDS_UPDATE flag set, pull them
// as soon as possible.
$need_update_messages = $this->loadRepositoryUpdateMessages(true);
foreach ($need_update_messages as $message) {
$repo = idx($pullable, $message->getRepositoryID());
if (!$repo) {
continue;
}
$this->log(
pht(
'Got an update message for repository "%s"!',
$repo->getMonogram()));
$retry_after[$message->getRepositoryID()] = time();
}
// If any repositories were deleted, remove them from the retry timer map
// so we don't end up with a retry timer that never gets updated and
// causes us to sleep for the minimum amount of time.
$retry_after = array_select_keys(
$retry_after,
array_keys($pullable));
// Figure out which repositories we need to queue for an update.
foreach ($pullable as $id => $repository) {
$monogram = $repository->getMonogram();
if (isset($futures[$id])) {
$this->log(pht('Repository "%s" is currently updating.', $monogram));
continue;
}
if (isset($queue[$id])) {
$this->log(pht('Repository "%s" is already queued.', $monogram));
continue;
}
$after = idx($retry_after, $id, 0);
if ($after > time()) {
$this->log(
pht(
'Repository "%s" is not due for an update for %s second(s).',
$monogram,
new PhutilNumber($after - time())));
continue;
}
if (!$after) {
$this->log(
pht(
'Scheduling repository "%s" for an initial update.',
$monogram));
} else {
$this->log(
pht(
'Scheduling repository "%s" for an update (%s seconds overdue).',
$monogram,
new PhutilNumber(time() - $after)));
}
$queue[$id] = $after;
}
// Process repositories in the order they became candidates for updates.
asort($queue);
// Dequeue repositories until we hit maximum parallelism.
while ($queue && (count($futures) < $max_futures)) {
foreach ($queue as $id => $time) {
$repository = idx($pullable, $id);
if (!$repository) {
$this->log(
pht('Repository %s is no longer pullable; skipping.', $id));
unset($queue[$id]);
continue;
}
$monogram = $repository->getMonogram();
$this->log(pht('Starting update for repository "%s".', $monogram));
unset($queue[$id]);
$futures[$id] = $this->buildUpdateFuture(
$repository,
$no_discovery);
break;
}
}
if ($queue) {
$this->log(
pht(
'Not enough process slots to schedule the other %s '.
'repository(s) for updates yet.',
new PhutilNumber(count($queue))));
}
if ($futures) {
$iterator = id(new FutureIterator($futures))
->setUpdateInterval($min_sleep);
foreach ($iterator as $id => $future) {
$this->stillWorking();
if ($future === null) {
$this->log(pht('Waiting for updates to complete...'));
$this->stillWorking();
if ($this->loadRepositoryUpdateMessages()) {
$this->log(pht('Interrupted by pending updates!'));
break;
}
continue;
}
unset($futures[$id]);
$retry_after[$id] = $this->resolveUpdateFuture(
$pullable[$id],
$future,
$min_sleep);
// We have a free slot now, so go try to fill it.
break;
}
// Jump back into prioritization if we had any futures to deal with.
continue;
}
$this->waitForUpdates($min_sleep, $retry_after);
}
}
/**
* @task pull
*/
private function buildUpdateFuture(
PhabricatorRepository $repository,
$no_discovery) {
$bin = dirname(phutil_get_library_root('phabricator')).'/bin/repository';
$flags = array();
if ($no_discovery) {
$flags[] = '--no-discovery';
}
$callsign = $repository->getCallsign();
$future = new ExecFuture('%s update %Ls -- %s', $bin, $flags, $callsign);
// Sometimes, the underlying VCS commands will hang indefinitely. We've
// observed this occasionally with GitHub, and other users have observed
// it with other VCS servers.
// To limit the damage this can cause, kill the update out after a
// reasonable amount of time, under the assumption that it has hung.
// Since it's hard to know what a "reasonable" amount of time is given that
// users may be downloading a repository full of pirated movies over a
// potato, these limits are fairly generous. Repositories exceeding these
// limits can be manually pulled with `bin/repository update X`, which can
// just run for as long as it wants.
if ($repository->isImporting()) {
$timeout = phutil_units('4 hours in seconds');
} else {
$timeout = phutil_units('15 minutes in seconds');
}
$future->setTimeout($timeout);
return $future;
}
/**
* Check for repositories that should be updated immediately.
*
* With the `$consume` flag, an internal cursor will also be incremented so
* that these messages are not returned by subsequent calls.
*
* @param bool Pass `true` to consume these messages, so the process will
* not see them again.
* @return list<wild> Pending update messages.
*
* @task pull
*/
private function loadRepositoryUpdateMessages($consume = false) {
$type_need_update = PhabricatorRepositoryStatusMessage::TYPE_NEEDS_UPDATE;
$messages = id(new PhabricatorRepositoryStatusMessage())->loadAllWhere(
'statusType = %s AND id > %d',
$type_need_update,
$this->statusMessageCursor);
// Keep track of messages we've seen so that we don't load them again.
// If we reload messages, we can get stuck a loop if we have a failing
// repository: we update immediately in response to the message, but do
// not clear the message because the update does not succeed. We then
// immediately retry. Instead, messages are only permitted to trigger
// an immediate update once.
if ($consume) {
foreach ($messages as $message) {
$this->statusMessageCursor = max(
$this->statusMessageCursor,
$message->getID());
}
}
return $messages;
}
/**
* @task pull
*/
private function loadPullableRepositories(array $include, array $exclude) {
$query = id(new PhabricatorRepositoryQuery())
->setViewer($this->getViewer());
if ($include) {
$query->withCallsigns($include);
}
$repositories = $query->execute();
if ($include) {
$by_callsign = mpull($repositories, null, 'getCallsign');
foreach ($include as $name) {
if (empty($by_callsign[$name])) {
throw new Exception(
- "No repository exists with callsign '{$name}'!");
+ pht(
+ "No repository exists with callsign '%s'!",
+ $name));
}
}
}
if ($exclude) {
$exclude = array_fuse($exclude);
foreach ($repositories as $key => $repository) {
if (isset($exclude[$repository->getCallsign()])) {
unset($repositories[$key]);
}
}
}
foreach ($repositories as $key => $repository) {
if (!$repository->isTracked()) {
unset($repositories[$key]);
}
}
// Shuffle the repositories, then re-key the array since shuffle()
// discards keys. This is mostly for startup, we'll use soft priorities
// later.
shuffle($repositories);
$repositories = mpull($repositories, null, 'getID');
return $repositories;
}
/**
* @task pull
*/
private function resolveUpdateFuture(
PhabricatorRepository $repository,
ExecFuture $future,
$min_sleep) {
$monogram = $repository->getMonogram();
$this->log(pht('Resolving update for "%s".', $monogram));
try {
list($stdout, $stderr) = $future->resolvex();
} catch (Exception $ex) {
$proxy = new PhutilProxyException(
pht(
'Error while updating the "%s" repository.',
$repository->getMonogram()),
$ex);
phlog($proxy);
return time() + $min_sleep;
}
if (strlen($stderr)) {
$stderr_msg = pht(
'Unexpected output while updating repository "%s": %s',
$monogram,
$stderr);
phlog($stderr_msg);
}
$smart_wait = $repository->loadUpdateInterval($min_sleep);
$this->log(
pht(
'Based on activity in repository "%s", considering a wait of %s '.
'seconds before update.',
$repository->getMonogram(),
new PhutilNumber($smart_wait)));
return time() + $smart_wait;
}
/**
* Sleep for a short period of time, waiting for update messages from the
*
*
* @task pull
*/
private function waitForUpdates($min_sleep, array $retry_after) {
$this->log(
pht('No repositories need updates right now, sleeping...'));
$sleep_until = time() + $min_sleep;
if ($retry_after) {
$sleep_until = min($sleep_until, min($retry_after));
}
while (($sleep_until - time()) > 0) {
$sleep_duration = ($sleep_until - time());
$this->log(
pht(
'Sleeping for %s more second(s)...',
new PhutilNumber($sleep_duration)));
$this->sleep(1);
if ($this->shouldExit()) {
$this->log(pht('Awakened from sleep by graceful shutdown!'));
return;
}
if ($this->loadRepositoryUpdateMessages()) {
$this->log(pht('Awakened from sleep by pending updates!'));
break;
}
}
}
}
diff --git a/src/applications/repository/engine/PhabricatorRepositoryDiscoveryEngine.php b/src/applications/repository/engine/PhabricatorRepositoryDiscoveryEngine.php
index f404f9c8a..407940e47 100644
--- a/src/applications/repository/engine/PhabricatorRepositoryDiscoveryEngine.php
+++ b/src/applications/repository/engine/PhabricatorRepositoryDiscoveryEngine.php
@@ -1,582 +1,582 @@
<?php
/**
* @task discover Discovering Repositories
* @task svn Discovering Subversion Repositories
* @task git Discovering Git Repositories
* @task hg Discovering Mercurial Repositories
* @task internal Internals
*/
final class PhabricatorRepositoryDiscoveryEngine
extends PhabricatorRepositoryEngine {
private $repairMode;
private $commitCache = array();
private $workingSet = array();
const MAX_COMMIT_CACHE_SIZE = 2048;
/* -( Discovering Repositories )------------------------------------------- */
public function setRepairMode($repair_mode) {
$this->repairMode = $repair_mode;
return $this;
}
public function getRepairMode() {
return $this->repairMode;
}
/**
* @task discovery
*/
public function discoverCommits() {
$repository = $this->getRepository();
$vcs = $repository->getVersionControlSystem();
switch ($vcs) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
$refs = $this->discoverSubversionCommits();
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
$refs = $this->discoverMercurialCommits();
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
$refs = $this->discoverGitCommits();
break;
default:
- throw new Exception("Unknown VCS '{$vcs}'!");
+ throw new Exception(pht("Unknown VCS '%s'!", $vcs));
}
// Clear the working set cache.
$this->workingSet = array();
// Record discovered commits and mark them in the cache.
foreach ($refs as $ref) {
$this->recordCommit(
$repository,
$ref->getIdentifier(),
$ref->getEpoch(),
$ref->getCanCloseImmediately(),
$ref->getParents());
$this->commitCache[$ref->getIdentifier()] = true;
}
return $refs;
}
/* -( Discovering Git Repositories )--------------------------------------- */
/**
* @task git
*/
private function discoverGitCommits() {
$repository = $this->getRepository();
if (!$repository->isHosted()) {
$this->verifyGitOrigin($repository);
}
$branches = id(new DiffusionLowLevelGitRefQuery())
->setRepository($repository)
->withIsOriginBranch(true)
->execute();
if (!$branches) {
// This repository has no branches at all, so we don't need to do
// anything. Generally, this means the repository is empty.
return array();
}
$branches = $this->sortBranches($branches);
$branches = mpull($branches, 'getCommitIdentifier', 'getShortName');
$this->log(
pht(
'Discovering commits in repository %s.',
$repository->getCallsign()));
$this->fillCommitCache(array_values($branches));
$refs = array();
foreach ($branches as $name => $commit) {
$this->log(pht('Examining branch "%s", at "%s".', $name, $commit));
if (!$repository->shouldTrackBranch($name)) {
$this->log(pht('Skipping, branch is untracked.'));
continue;
}
if ($this->isKnownCommit($commit)) {
$this->log(pht('Skipping, HEAD is known.'));
continue;
}
$this->log(pht('Looking for new commits.'));
$branch_refs = $this->discoverStreamAncestry(
new PhabricatorGitGraphStream($repository, $commit),
$commit,
$repository->shouldAutocloseBranch($name));
$this->didDiscoverRefs($branch_refs);
$refs[] = $branch_refs;
}
return array_mergev($refs);
}
/* -( Discovering Subversion Repositories )-------------------------------- */
/**
* @task svn
*/
private function discoverSubversionCommits() {
$repository = $this->getRepository();
if (!$repository->isHosted()) {
$this->verifySubversionRoot($repository);
}
$upper_bound = null;
$limit = 1;
$refs = array();
do {
// Find all the unknown commits on this path. Note that we permit
// importing an SVN subdirectory rather than the entire repository, so
// commits may be nonsequential.
if ($upper_bound === null) {
$at_rev = 'HEAD';
} else {
$at_rev = ($upper_bound - 1);
}
try {
list($xml, $stderr) = $repository->execxRemoteCommand(
'log --xml --quiet --limit %d %s',
$limit,
$repository->getSubversionBaseURI($at_rev));
} catch (CommandException $ex) {
$stderr = $ex->getStdErr();
if (preg_match('/(path|File) not found/', $stderr)) {
// We've gone all the way back through history and this path was not
// affected by earlier commits.
break;
}
throw $ex;
}
$xml = phutil_utf8ize($xml);
$log = new SimpleXMLElement($xml);
foreach ($log->logentry as $entry) {
$identifier = (int)$entry['revision'];
$epoch = (int)strtotime((string)$entry->date[0]);
$refs[$identifier] = id(new PhabricatorRepositoryCommitRef())
->setIdentifier($identifier)
->setEpoch($epoch)
->setCanCloseImmediately(true);
if ($upper_bound === null) {
$upper_bound = $identifier;
} else {
$upper_bound = min($upper_bound, $identifier);
}
}
// Discover 2, 4, 8, ... 256 logs at a time. This allows us to initially
// import large repositories fairly quickly, while pulling only as much
// data as we need in the common case (when we've already imported the
// repository and are just grabbing one commit at a time).
$limit = min($limit * 2, 256);
} while ($upper_bound > 1 && !$this->isKnownCommit($upper_bound));
krsort($refs);
while ($refs && $this->isKnownCommit(last($refs)->getIdentifier())) {
array_pop($refs);
}
$refs = array_reverse($refs);
$this->didDiscoverRefs($refs);
return $refs;
}
private function verifySubversionRoot(PhabricatorRepository $repository) {
list($xml) = $repository->execxRemoteCommand(
'info --xml %s',
$repository->getSubversionPathURI());
$xml = phutil_utf8ize($xml);
$xml = new SimpleXMLElement($xml);
$remote_root = (string)($xml->entry[0]->repository[0]->root[0]);
$expect_root = $repository->getSubversionPathURI();
$normal_type_svn = PhabricatorRepositoryURINormalizer::TYPE_SVN;
$remote_normal = id(new PhabricatorRepositoryURINormalizer(
$normal_type_svn,
$remote_root))->getNormalizedPath();
$expect_normal = id(new PhabricatorRepositoryURINormalizer(
$normal_type_svn,
$expect_root))->getNormalizedPath();
if ($remote_normal != $expect_normal) {
throw new Exception(
pht(
'Repository "%s" does not have a correctly configured remote URI. '.
'The remote URI for a Subversion repository MUST point at the '.
'repository root. The root for this repository is "%s", but the '.
'configured URI is "%s". To resolve this error, set the remote URI '.
'to point at the repository root. If you want to import only part '.
'of a Subversion repository, use the "Import Only" option.',
$repository->getCallsign(),
$remote_root,
$expect_root));
}
}
/* -( Discovering Mercurial Repositories )--------------------------------- */
/**
* @task hg
*/
private function discoverMercurialCommits() {
$repository = $this->getRepository();
$branches = id(new DiffusionLowLevelMercurialBranchesQuery())
->setRepository($repository)
->execute();
$this->fillCommitCache(mpull($branches, 'getCommitIdentifier'));
$refs = array();
foreach ($branches as $branch) {
// NOTE: Mercurial branches may have multiple heads, so the names may
// not be unique.
$name = $branch->getShortName();
$commit = $branch->getCommitIdentifier();
$this->log(pht('Examining branch "%s" head "%s".', $name, $commit));
if (!$repository->shouldTrackBranch($name)) {
$this->log(pht('Skipping, branch is untracked.'));
continue;
}
if ($this->isKnownCommit($commit)) {
$this->log(pht('Skipping, this head is a known commit.'));
continue;
}
$this->log(pht('Looking for new commits.'));
$branch_refs = $this->discoverStreamAncestry(
new PhabricatorMercurialGraphStream($repository, $commit),
$commit,
$close_immediately = true);
$this->didDiscoverRefs($branch_refs);
$refs[] = $branch_refs;
}
return array_mergev($refs);
}
/* -( Internals )---------------------------------------------------------- */
private function discoverStreamAncestry(
PhabricatorRepositoryGraphStream $stream,
$commit,
$close_immediately) {
$discover = array($commit);
$graph = array();
$seen = array();
// Find all the reachable, undiscovered commits. Build a graph of the
// edges.
while ($discover) {
$target = array_pop($discover);
if (empty($graph[$target])) {
$graph[$target] = array();
}
$parents = $stream->getParents($target);
foreach ($parents as $parent) {
if ($this->isKnownCommit($parent)) {
continue;
}
$graph[$target][$parent] = true;
if (empty($seen[$parent])) {
$seen[$parent] = true;
$discover[] = $parent;
}
}
}
// Now, sort them topographically.
$commits = $this->reduceGraph($graph);
$refs = array();
foreach ($commits as $commit) {
$refs[] = id(new PhabricatorRepositoryCommitRef())
->setIdentifier($commit)
->setEpoch($stream->getCommitDate($commit))
->setCanCloseImmediately($close_immediately)
->setParents($stream->getParents($commit));
}
return $refs;
}
private function reduceGraph(array $edges) {
foreach ($edges as $commit => $parents) {
$edges[$commit] = array_keys($parents);
}
$graph = new PhutilDirectedScalarGraph();
$graph->addNodes($edges);
$commits = $graph->getTopographicallySortedNodes();
// NOTE: We want the most ancestral nodes first, so we need to reverse the
// list we get out of AbstractDirectedGraph.
$commits = array_reverse($commits);
return $commits;
}
private function isKnownCommit($identifier) {
if (isset($this->commitCache[$identifier])) {
return true;
}
if (isset($this->workingSet[$identifier])) {
return true;
}
if ($this->repairMode) {
// In repair mode, rediscover the entire repository, ignoring the
// database state. We can hit the local cache above, but if we miss it
// stop the script from going to the database cache.
return false;
}
$this->fillCommitCache(array($identifier));
return isset($this->commitCache[$identifier]);
}
private function fillCommitCache(array $identifiers) {
if (!$identifiers) {
return;
}
$commits = id(new PhabricatorRepositoryCommit())->loadAllWhere(
'repositoryID = %d AND commitIdentifier IN (%Ls)',
$this->getRepository()->getID(),
$identifiers);
foreach ($commits as $commit) {
$this->commitCache[$commit->getCommitIdentifier()] = true;
}
while (count($this->commitCache) > self::MAX_COMMIT_CACHE_SIZE) {
array_shift($this->commitCache);
}
}
/**
* Sort branches so we process closeable branches first. This makes the
* whole import process a little cheaper, since we can close these commits
* the first time through rather than catching them in the refs step.
*
* @task internal
*
* @param list<DiffusionRepositoryRef> List of branch heads.
* @return list<DiffusionRepositoryRef> Sorted list of branch heads.
*/
private function sortBranches(array $branches) {
$repository = $this->getRepository();
$head_branches = array();
$tail_branches = array();
foreach ($branches as $branch) {
$name = $branch->getShortName();
if ($repository->shouldAutocloseBranch($name)) {
$head_branches[] = $branch;
} else {
$tail_branches[] = $branch;
}
}
return array_merge($head_branches, $tail_branches);
}
private function recordCommit(
PhabricatorRepository $repository,
$commit_identifier,
$epoch,
$close_immediately,
array $parents) {
$commit = new PhabricatorRepositoryCommit();
$commit->setRepositoryID($repository->getID());
$commit->setCommitIdentifier($commit_identifier);
$commit->setEpoch($epoch);
if ($close_immediately) {
$commit->setImportStatus(PhabricatorRepositoryCommit::IMPORTED_CLOSEABLE);
}
$data = new PhabricatorRepositoryCommitData();
$conn_w = $repository->establishConnection('w');
try {
// If this commit has parents, look up their IDs. The parent commits
// should always exist already.
$parent_ids = array();
if ($parents) {
$parent_rows = queryfx_all(
$conn_w,
'SELECT id, commitIdentifier FROM %T
WHERE commitIdentifier IN (%Ls) AND repositoryID = %d',
$commit->getTableName(),
$parents,
$repository->getID());
$parent_map = ipull($parent_rows, 'id', 'commitIdentifier');
foreach ($parents as $parent) {
if (empty($parent_map[$parent])) {
throw new Exception(
pht('Unable to identify parent "%s"!', $parent));
}
$parent_ids[] = $parent_map[$parent];
}
} else {
// Write an explicit 0 so we can distinguish between "really no
// parents" and "data not available".
if (!$repository->isSVN()) {
$parent_ids = array(0);
}
}
$commit->openTransaction();
$commit->save();
$data->setCommitID($commit->getID());
$data->save();
foreach ($parent_ids as $parent_id) {
queryfx(
$conn_w,
'INSERT IGNORE INTO %T (childCommitID, parentCommitID)
VALUES (%d, %d)',
PhabricatorRepository::TABLE_PARENTS,
$commit->getID(),
$parent_id);
}
$commit->saveTransaction();
$this->insertTask($repository, $commit);
queryfx(
$conn_w,
'INSERT INTO %T (repositoryID, size, lastCommitID, epoch)
VALUES (%d, 1, %d, %d)
ON DUPLICATE KEY UPDATE
size = size + 1,
lastCommitID =
IF(VALUES(epoch) > epoch, VALUES(lastCommitID), lastCommitID),
epoch = IF(VALUES(epoch) > epoch, VALUES(epoch), epoch)',
PhabricatorRepository::TABLE_SUMMARY,
$repository->getID(),
$commit->getID(),
$epoch);
if ($this->repairMode) {
// Normally, the query should throw a duplicate key exception. If we
// reach this in repair mode, we've actually performed a repair.
$this->log(pht('Repaired commit "%s".', $commit_identifier));
}
PhutilEventEngine::dispatchEvent(
new PhabricatorEvent(
PhabricatorEventType::TYPE_DIFFUSION_DIDDISCOVERCOMMIT,
array(
'repository' => $repository,
'commit' => $commit,
)));
} catch (AphrontDuplicateKeyQueryException $ex) {
$commit->killTransaction();
// Ignore. This can happen because we discover the same new commit
// more than once when looking at history, or because of races or
// data inconsistency or cosmic radiation; in any case, we're still
// in a good state if we ignore the failure.
}
}
private function didDiscoverRefs(array $refs) {
foreach ($refs as $ref) {
$this->workingSet[$ref->getIdentifier()] = true;
}
}
private function insertTask(
PhabricatorRepository $repository,
PhabricatorRepositoryCommit $commit,
$data = array()) {
$vcs = $repository->getVersionControlSystem();
switch ($vcs) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
$class = 'PhabricatorRepositoryGitCommitMessageParserWorker';
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
$class = 'PhabricatorRepositorySvnCommitMessageParserWorker';
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
$class = 'PhabricatorRepositoryMercurialCommitMessageParserWorker';
break;
default:
- throw new Exception("Unknown repository type '{$vcs}'!");
+ throw new Exception(pht("Unknown repository type '%s'!", $vcs));
}
$data['commitID'] = $commit->getID();
PhabricatorWorker::scheduleTask($class, $data);
}
}
diff --git a/src/applications/repository/engine/PhabricatorRepositoryEngine.php b/src/applications/repository/engine/PhabricatorRepositoryEngine.php
index 300fde13e..8d858a3d8 100644
--- a/src/applications/repository/engine/PhabricatorRepositoryEngine.php
+++ b/src/applications/repository/engine/PhabricatorRepositoryEngine.php
@@ -1,162 +1,162 @@
<?php
/**
* @task config Configuring Repository Engines
* @task internal Internals
*/
abstract class PhabricatorRepositoryEngine {
private $repository;
private $verbose;
/**
* @task config
*/
public function setRepository(PhabricatorRepository $repository) {
$this->repository = $repository;
return $this;
}
/**
* @task config
*/
protected function getRepository() {
if ($this->repository === null) {
- throw new Exception('Call setRepository() to provide a repository!');
+ throw new PhutilInvalidStateException('setRepository');
}
return $this->repository;
}
/**
* @task config
*/
public function setVerbose($verbose) {
$this->verbose = $verbose;
return $this;
}
/**
* @task config
*/
public function getVerbose() {
return $this->verbose;
}
public function getViewer() {
return PhabricatorUser::getOmnipotentUser();
}
/**
* Verify that the "origin" remote exists, and points at the correct URI.
*
* This catches or corrects some types of misconfiguration, and also repairs
* an issue where Git 1.7.1 does not create an "origin" for `--bare` clones.
* See T4041.
*
* @param PhabricatorRepository Repository to verify.
* @return void
*/
protected function verifyGitOrigin(PhabricatorRepository $repository) {
list($remotes) = $repository->execxLocalCommand(
'remote show -n origin');
$matches = null;
if (!preg_match('/^\s*Fetch URL:\s*(.*?)\s*$/m', $remotes, $matches)) {
throw new Exception(
"Expected 'Fetch URL' in 'git remote show -n origin'.");
}
$remote_uri = $matches[1];
$expect_remote = $repository->getRemoteURI();
if ($remote_uri == 'origin') {
// If a remote does not exist, git pretends it does and prints out a
// made up remote where the URI is the same as the remote name. This is
// definitely not correct.
// Possibly, we should use `git remote --verbose` instead, which does not
// suffer from this problem (but is a little more complicated to parse).
$valid = false;
$exists = false;
} else {
$normal_type_git = PhabricatorRepositoryURINormalizer::TYPE_GIT;
$remote_normal = id(new PhabricatorRepositoryURINormalizer(
$normal_type_git,
$remote_uri))->getNormalizedPath();
$expect_normal = id(new PhabricatorRepositoryURINormalizer(
$normal_type_git,
$expect_remote))->getNormalizedPath();
$valid = ($remote_normal == $expect_normal);
$exists = true;
}
if (!$valid) {
if (!$exists) {
// If there's no "origin" remote, just create it regardless of how
// strongly we own the working copy. There is almost no conceivable
// scenario in which this could do damage.
$this->log(
pht(
'Remote "origin" does not exist. Creating "origin", with '.
'URI "%s".',
$expect_remote));
$repository->execxLocalCommand(
'remote add origin %P',
$repository->getRemoteURIEnvelope());
// NOTE: This doesn't fetch the origin (it just creates it), so we won't
// know about origin branches until the next "pull" happens. That's fine
// for our purposes, but might impact things in the future.
} else {
if ($repository->canDestroyWorkingCopy()) {
// Bad remote, but we can try to repair it.
$this->log(
pht(
'Remote "origin" exists, but is pointed at the wrong URI, "%s". '.
'Resetting origin URI to "%s.',
$remote_uri,
$expect_remote));
$repository->execxLocalCommand(
'remote set-url origin %P',
$repository->getRemoteURIEnvelope());
} else {
// Bad remote and we aren't comfortable repairing it.
$message = pht(
'Working copy at "%s" has a mismatched origin URI, "%s". '.
'The expected origin URI is "%s". Fix your configuration, or '.
'set the remote URI correctly. To avoid breaking anything, '.
'Phabricator will not automatically fix this.',
$repository->getLocalPath(),
$remote_uri,
$expect_remote);
throw new Exception($message);
}
}
}
}
/**
* @task internal
*/
protected function log($pattern /* ... */) {
if ($this->getVerbose()) {
$console = PhutilConsole::getConsole();
$argv = func_get_args();
array_unshift($argv, "%s\n");
call_user_func_array(array($console, 'writeOut'), $argv);
}
return $this;
}
}
diff --git a/src/applications/repository/engine/PhabricatorRepositoryPullEngine.php b/src/applications/repository/engine/PhabricatorRepositoryPullEngine.php
index ffbbc7034..d72514877 100644
--- a/src/applications/repository/engine/PhabricatorRepositoryPullEngine.php
+++ b/src/applications/repository/engine/PhabricatorRepositoryPullEngine.php
@@ -1,545 +1,558 @@
<?php
/**
* Manages execution of `git pull` and `hg pull` commands for
* @{class:PhabricatorRepository} objects. Used by
* @{class:PhabricatorRepositoryPullLocalDaemon}.
*
* This class also covers initial working copy setup through `git clone`,
* `git init`, `hg clone`, `hg init`, or `svnadmin create`.
*
* @task pull Pulling Working Copies
* @task git Pulling Git Working Copies
* @task hg Pulling Mercurial Working Copies
* @task svn Pulling Subversion Working Copies
* @task internal Internals
*/
final class PhabricatorRepositoryPullEngine
extends PhabricatorRepositoryEngine {
/* -( Pulling Working Copies )--------------------------------------------- */
public function pullRepository() {
$repository = $this->getRepository();
$is_hg = false;
$is_git = false;
$is_svn = false;
$vcs = $repository->getVersionControlSystem();
$callsign = $repository->getCallsign();
switch ($vcs) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
// We never pull a local copy of non-hosted Subversion repositories.
if (!$repository->isHosted()) {
$this->skipPull(
pht(
"Repository '%s' is a non-hosted Subversion repository, which ".
"does not require a local working copy to be pulled.",
$callsign));
return;
}
$is_svn = true;
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
$is_git = true;
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
$is_hg = true;
break;
default:
$this->abortPull(pht('Unknown VCS "%s"!', $vcs));
break;
}
$callsign = $repository->getCallsign();
$local_path = $repository->getLocalPath();
if ($local_path === null) {
$this->abortPull(
pht(
"No local path is configured for repository '%s'.",
$callsign));
}
try {
$dirname = dirname($local_path);
if (!Filesystem::pathExists($dirname)) {
Filesystem::createDirectory($dirname, 0755, $recursive = true);
}
if (!Filesystem::pathExists($local_path)) {
$this->logPull(
pht(
"Creating a new working copy for repository '%s'.",
$callsign));
if ($is_git) {
$this->executeGitCreate();
} else if ($is_hg) {
$this->executeMercurialCreate();
} else {
$this->executeSubversionCreate();
}
} else {
if (!$repository->isHosted()) {
$this->logPull(
pht(
"Updating the working copy for repository '%s'.",
$callsign));
if ($is_git) {
$this->verifyGitOrigin($repository);
$this->executeGitUpdate();
} else if ($is_hg) {
$this->executeMercurialUpdate();
}
}
}
if ($repository->isHosted()) {
if ($is_git) {
$this->installGitHook();
} else if ($is_svn) {
$this->installSubversionHook();
} else if ($is_hg) {
$this->installMercurialHook();
}
foreach ($repository->getHookDirectories() as $directory) {
$this->installHookDirectory($directory);
}
}
} catch (Exception $ex) {
$this->abortPull(
pht('Pull of "%s" failed: %s', $callsign, $ex->getMessage()),
$ex);
}
$this->donePull();
return $this;
}
private function skipPull($message) {
$this->log('%s', $message);
$this->donePull();
}
private function abortPull($message, Exception $ex = null) {
$code_error = PhabricatorRepositoryStatusMessage::CODE_ERROR;
$this->updateRepositoryInitStatus($code_error, $message);
if ($ex) {
throw $ex;
} else {
throw new Exception($message);
}
}
private function logPull($message) {
$code_working = PhabricatorRepositoryStatusMessage::CODE_WORKING;
$this->updateRepositoryInitStatus($code_working, $message);
$this->log('%s', $message);
}
private function donePull() {
$code_okay = PhabricatorRepositoryStatusMessage::CODE_OKAY;
$this->updateRepositoryInitStatus($code_okay);
}
private function updateRepositoryInitStatus($code, $message = null) {
$this->getRepository()->writeStatusMessage(
PhabricatorRepositoryStatusMessage::TYPE_INIT,
$code,
array(
'message' => $message,
));
}
private function installHook($path) {
$this->log('%s', pht('Installing commit hook to "%s"...', $path));
$repository = $this->getRepository();
$identifier = $this->getHookContextIdentifier($repository);
$root = dirname(phutil_get_library_root('phabricator'));
$bin = $root.'/bin/commit-hook';
$full_php_path = Filesystem::resolveBinary('php');
$cmd = csprintf(
'exec %s -f %s -- %s "$@"',
$full_php_path,
$bin,
$identifier);
$hook = "#!/bin/sh\nexport TERM=dumb\n{$cmd}\n";
Filesystem::writeFile($path, $hook);
Filesystem::changePermissions($path, 0755);
}
private function installHookDirectory($path) {
$readme = pht(
"To add custom hook scripts to this repository, add them to this ".
"directory.\n\nPhabricator will run any executables in this directory ".
"after running its own checks, as though they were normal hook ".
"scripts.");
Filesystem::createDirectory($path, 0755);
Filesystem::writeFile($path.'/README', $readme);
}
private function getHookContextIdentifier(PhabricatorRepository $repository) {
$identifier = $repository->getCallsign();
$instance = PhabricatorEnv::getEnvConfig('cluster.instance');
if (strlen($instance)) {
$identifier = "{$identifier}:{$instance}";
}
return $identifier;
}
/* -( Pulling Git Working Copies )----------------------------------------- */
/**
* @task git
*/
private function executeGitCreate() {
$repository = $this->getRepository();
$path = rtrim($repository->getLocalPath(), '/');
if ($repository->isHosted()) {
$repository->execxRemoteCommand(
'init --bare -- %s',
$path);
} else {
$repository->execxRemoteCommand(
'clone --bare -- %P %s',
$repository->getRemoteURIEnvelope(),
$path);
}
}
/**
* @task git
*/
private function executeGitUpdate() {
$repository = $this->getRepository();
list($err, $stdout) = $repository->execLocalCommand(
'rev-parse --show-toplevel');
$message = null;
$path = $repository->getLocalPath();
if ($err) {
// Try to raise a more tailored error message in the more common case
// of the user creating an empty directory. (We could try to remove it,
// but might not be able to, and it's much simpler to raise a good
// message than try to navigate those waters.)
if (is_dir($path)) {
$files = Filesystem::listDirectory($path, $include_hidden = true);
if (!$files) {
- $message =
- "Expected to find a git repository at '{$path}', but there ".
+ $message = pht(
+ "Expected to find a git repository at '%s', but there ".
"is an empty directory there. Remove the directory: the daemon ".
- "will run 'git clone' for you.";
+ "will run '%s' for you.",
+ $path,
+ 'git clone');
} else {
- $message =
- "Expected to find a git repository at '{$path}', but there is ".
+ $message = pht(
+ "Expected to find a git repository at '%s', but there is ".
"a non-repository directory (with other stuff in it) there. Move ".
"or remove this directory (or reconfigure the repository to use a ".
"different directory), and then either clone a repository ".
- "yourself or let the daemon do it.";
+ "yourself or let the daemon do it.",
+ $path);
}
} else if (is_file($path)) {
- $message =
- "Expected to find a git repository at '{$path}', but there is a ".
+ $message = pht(
+ "Expected to find a git repository at '%s', but there is a ".
"file there instead. Remove it and let the daemon clone a ".
- "repository for you.";
+ "repository for you.",
+ $path);
} else {
- $message =
- "Expected to find a git repository at '{$path}', but did not.";
+ $message = pht(
+ "Expected to find a git repository at '%s', but did not.",
+ $path);
}
} else {
$repo_path = rtrim($stdout, "\n");
if (empty($repo_path)) {
// This can mean one of two things: we're in a bare repository, or
// we're inside a git repository inside another git repository. Since
// the first is dramatically more likely now that we perform bare
// clones and I don't have a great way to test for the latter, assume
// we're OK.
} else if (!Filesystem::pathsAreEquivalent($repo_path, $path)) {
$err = true;
- $message =
- "Expected to find repo at '{$path}', but the actual ".
- "git repository root for this directory is '{$repo_path}'. ".
- "Something is misconfigured. The repository's 'Local Path' should ".
- "be set to some place where the daemon can check out a working ".
- "copy, and should not be inside another git repository.";
+ $message = pht(
+ "Expected to find repo at '%s', but the actual git repository root ".
+ "for this directory is '%s'. Something is misconfigured. ".
+ "The repository's 'Local Path' should be set to some place where ".
+ "the daemon can check out a working copy, ".
+ "and should not be inside another git repository.",
+ $path,
+ $repo_path);
}
}
if ($err && $repository->canDestroyWorkingCopy()) {
- phlog("Repository working copy at '{$path}' failed sanity check; ".
- "destroying and re-cloning. {$message}");
+ phlog(
+ pht(
+ "Repository working copy at '%s' failed sanity check; ".
+ "destroying and re-cloning. %s",
+ $path,
+ $message));
Filesystem::remove($path);
$this->executeGitCreate();
} else if ($err) {
throw new Exception($message);
}
$retry = false;
do {
// This is a local command, but needs credentials.
if ($repository->isWorkingCopyBare()) {
// For bare working copies, we need this magic incantation.
$future = $repository->getRemoteCommandFuture(
'fetch origin %s --prune',
'+refs/heads/*:refs/heads/*');
} else {
$future = $repository->getRemoteCommandFuture(
'fetch --all --prune');
}
$future->setCWD($path);
list($err, $stdout, $stderr) = $future->resolve();
if ($err && !$retry && $repository->canDestroyWorkingCopy()) {
$retry = true;
// Fix remote origin url if it doesn't match our configuration
$origin_url = $repository->execLocalCommand(
'config --get remote.origin.url');
$remote_uri = $repository->getRemoteURIEnvelope();
if ($origin_url != $remote_uri->openEnvelope()) {
$repository->execLocalCommand(
'remote set-url origin %P',
$remote_uri);
}
} else if ($err) {
throw new Exception(
- "git fetch failed with error #{$err}:\n".
- "stdout:{$stdout}\n\n".
- "stderr:{$stderr}\n");
+ pht(
+ "git fetch failed with error #%d:\nstdout:%s\n\nstderr:%s\n",
+ $err,
+ $stdout,
+ $stderr));
} else {
$retry = false;
}
} while ($retry);
}
/**
* @task git
*/
private function installGitHook() {
$repository = $this->getRepository();
$root = $repository->getLocalPath();
if ($repository->isWorkingCopyBare()) {
$path = '/hooks/pre-receive';
} else {
$path = '/.git/hooks/pre-receive';
}
$this->installHook($root.$path);
}
/* -( Pulling Mercurial Working Copies )----------------------------------- */
/**
* @task hg
*/
private function executeMercurialCreate() {
$repository = $this->getRepository();
$path = rtrim($repository->getLocalPath(), '/');
if ($repository->isHosted()) {
$repository->execxRemoteCommand(
'init -- %s',
$path);
} else {
$remote = $repository->getRemoteURIEnvelope();
// NOTE: Mercurial prior to 3.2.4 has an severe command injection
// vulnerability. See: <http://bit.ly/19B58E9>
// On vulnerable versions of Mercurial, we refuse to clone remotes which
// contain characters which may be interpreted by the shell.
$hg_version = PhabricatorRepositoryVersion::getMercurialVersion();
$is_vulnerable = version_compare($hg_version, '3.2.4', '<');
if ($is_vulnerable) {
$cleartext = $remote->openEnvelope();
// The use of "%R" here is an attempt to limit collateral damage
// for normal URIs because it isn't clear how long this vulnerability
// has been around for.
$escaped = csprintf('%R', $cleartext);
if ((string)$escaped !== (string)$cleartext) {
throw new Exception(
pht(
'You have an old version of Mercurial (%s) which has a severe '.
'command injection security vulnerability. The remote URI for '.
'this repository (%s) is potentially unsafe. Upgrade Mercurial '.
'to at least 3.2.4 to clone it.',
$hg_version,
$repository->getMonogram()));
}
}
try {
$repository->execxRemoteCommand(
'clone --noupdate -- %P %s',
$remote,
$path);
} catch (Exception $ex) {
$message = $ex->getMessage();
$message = $this->censorMercurialErrorMessage($message);
throw new Exception($message);
}
}
}
/**
* @task hg
*/
private function executeMercurialUpdate() {
$repository = $this->getRepository();
$path = $repository->getLocalPath();
// This is a local command, but needs credentials.
$future = $repository->getRemoteCommandFuture('pull -u');
$future->setCWD($path);
try {
$future->resolvex();
} catch (CommandException $ex) {
$err = $ex->getError();
$stdout = $ex->getStdOut();
// NOTE: Between versions 2.1 and 2.1.1, Mercurial changed the behavior
// of "hg pull" to return 1 in case of a successful pull with no changes.
// This behavior has been reverted, but users who updated between Feb 1,
// 2012 and Mar 1, 2012 will have the erroring version. Do a dumb test
// against stdout to check for this possibility.
// See: https://github.com/phacility/phabricator/issues/101/
// NOTE: Mercurial has translated versions, which translate this error
// string. In a translated version, the string will be something else,
// like "aucun changement trouve". There didn't seem to be an easy way
// to handle this (there are hard ways but this is not a common problem
// and only creates log spam, not application failures). Assume English.
// TODO: Remove this once we're far enough in the future that deployment
// of 2.1 is exceedingly rare?
if ($err == 1 && preg_match('/no changes found/', $stdout)) {
return;
} else {
$message = $ex->getMessage();
$message = $this->censorMercurialErrorMessage($message);
throw new Exception($message);
}
}
}
/**
* Censor response bodies from Mercurial error messages.
*
* When Mercurial attempts to clone an HTTP repository but does not
* receive a response it expects, it emits the response body in the
* command output.
*
* This represents a potential SSRF issue, because an attacker with
* permission to create repositories can create one which points at the
* remote URI for some local service, then read the response from the
* error message. To prevent this, censor response bodies out of error
* messages.
*
* @param string Uncensored Mercurial command output.
* @return string Censored Mercurial command output.
*/
private function censorMercurialErrorMessage($message) {
return preg_replace(
'/^---%<---.*/sm',
pht('<Response body omitted from Mercurial error message.>')."\n",
$message);
}
/**
* @task hg
*/
private function installMercurialHook() {
$repository = $this->getRepository();
$path = $repository->getLocalPath().'/.hg/hgrc';
$identifier = $this->getHookContextIdentifier($repository);
$root = dirname(phutil_get_library_root('phabricator'));
$bin = $root.'/bin/commit-hook';
$data = array();
$data[] = '[hooks]';
// This hook handles normal pushes.
$data[] = csprintf(
'pretxnchangegroup.phabricator = TERM=dumb %s %s %s',
$bin,
$identifier,
'pretxnchangegroup');
// This one handles creating bookmarks.
$data[] = csprintf(
'prepushkey.phabricator = TERM=dumb %s %s %s',
$bin,
$identifier,
'prepushkey');
$data[] = null;
$data = implode("\n", $data);
$this->log('%s', pht('Installing commit hook config to "%s"...', $path));
Filesystem::writeFile($path, $data);
}
/* -( Pulling Subversion Working Copies )---------------------------------- */
/**
* @task svn
*/
private function executeSubversionCreate() {
$repository = $this->getRepository();
$path = rtrim($repository->getLocalPath(), '/');
execx('svnadmin create -- %s', $path);
}
/**
* @task svn
*/
private function installSubversionHook() {
$repository = $this->getRepository();
$root = $repository->getLocalPath();
$path = '/hooks/pre-commit';
$this->installHook($root.$path);
}
}
diff --git a/src/applications/repository/engine/PhabricatorRepositoryRefEngine.php b/src/applications/repository/engine/PhabricatorRepositoryRefEngine.php
index a393a6be6..0eab00b5c 100644
--- a/src/applications/repository/engine/PhabricatorRepositoryRefEngine.php
+++ b/src/applications/repository/engine/PhabricatorRepositoryRefEngine.php
@@ -1,492 +1,492 @@
<?php
/**
* Update the ref cursors for a repository, which track the positions of
* branches, bookmarks, and tags.
*/
final class PhabricatorRepositoryRefEngine
extends PhabricatorRepositoryEngine {
private $newRefs = array();
private $deadRefs = array();
private $closeCommits = array();
private $hasNoCursors;
public function updateRefs() {
$this->newRefs = array();
$this->deadRefs = array();
$this->closeCommits = array();
$repository = $this->getRepository();
$branches_may_close = false;
$vcs = $repository->getVersionControlSystem();
switch ($vcs) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
// No meaningful refs of any type in Subversion.
$branches = array();
$bookmarks = array();
$tags = array();
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
$branches = $this->loadMercurialBranchPositions($repository);
$bookmarks = $this->loadMercurialBookmarkPositions($repository);
$tags = array();
$branches_may_close = true;
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
$branches = $this->loadGitBranchPositions($repository);
$bookmarks = array();
$tags = $this->loadGitTagPositions($repository);
break;
default:
throw new Exception(pht('Unknown VCS "%s"!', $vcs));
}
$maps = array(
PhabricatorRepositoryRefCursor::TYPE_BRANCH => $branches,
PhabricatorRepositoryRefCursor::TYPE_TAG => $tags,
PhabricatorRepositoryRefCursor::TYPE_BOOKMARK => $bookmarks,
);
$all_cursors = id(new PhabricatorRepositoryRefCursorQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withRepositoryPHIDs(array($repository->getPHID()))
->execute();
$cursor_groups = mgroup($all_cursors, 'getRefType');
$this->hasNoCursors = (!$all_cursors);
// Find all the heads of closing refs.
$all_closing_heads = array();
foreach ($all_cursors as $cursor) {
if ($this->shouldCloseRef($cursor->getRefType(), $cursor->getRefName())) {
$all_closing_heads[] = $cursor->getCommitIdentifier();
}
}
$all_closing_heads = array_unique($all_closing_heads);
$all_closing_heads = $this->removeMissingCommits($all_closing_heads);
foreach ($maps as $type => $refs) {
$cursor_group = idx($cursor_groups, $type, array());
$this->updateCursors($cursor_group, $refs, $type, $all_closing_heads);
}
if ($this->closeCommits) {
$this->setCloseFlagOnCommits($this->closeCommits);
}
if ($this->newRefs || $this->deadRefs) {
$repository->openTransaction();
foreach ($this->newRefs as $ref) {
$ref->save();
}
foreach ($this->deadRefs as $ref) {
$ref->delete();
}
$repository->saveTransaction();
$this->newRefs = array();
$this->deadRefs = array();
}
if ($branches && $branches_may_close) {
$this->updateBranchStates($repository, $branches);
}
}
private function updateBranchStates(
PhabricatorRepository $repository,
array $branches) {
assert_instances_of($branches, 'DiffusionRepositoryRef');
$all_cursors = id(new PhabricatorRepositoryRefCursorQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withRepositoryPHIDs(array($repository->getPHID()))
->execute();
$state_map = array();
$type_branch = PhabricatorRepositoryRefCursor::TYPE_BRANCH;
foreach ($all_cursors as $cursor) {
if ($cursor->getRefType() !== $type_branch) {
continue;
}
$raw_name = $cursor->getRefNameRaw();
$hash = $cursor->getCommitIdentifier();
$state_map[$raw_name][$hash] = $cursor;
}
foreach ($branches as $branch) {
$cursor = idx($state_map, $branch->getShortName(), array());
$cursor = idx($cursor, $branch->getCommitIdentifier());
if (!$cursor) {
continue;
}
$fields = $branch->getRawFields();
$cursor_state = (bool)$cursor->getIsClosed();
$branch_state = (bool)idx($fields, 'closed');
if ($cursor_state != $branch_state) {
$cursor->setIsClosed((int)$branch_state)->save();
}
}
}
private function markRefNew(PhabricatorRepositoryRefCursor $cursor) {
$this->newRefs[] = $cursor;
return $this;
}
private function markRefDead(PhabricatorRepositoryRefCursor $cursor) {
$this->deadRefs[] = $cursor;
return $this;
}
private function markCloseCommits(array $identifiers) {
foreach ($identifiers as $identifier) {
$this->closeCommits[$identifier] = $identifier;
}
return $this;
}
/**
* Remove commits which no longer exist in the repository from a list.
*
* After a force push and garbage collection, we may have branch cursors which
* point at commits which no longer exist. This can make commands issued later
* fail. See T5839 for discussion.
*
* @param list<string> List of commit identifiers.
* @return list<string> List with nonexistent identifiers removed.
*/
private function removeMissingCommits(array $identifiers) {
if (!$identifiers) {
return array();
}
$resolved = id(new DiffusionLowLevelResolveRefsQuery())
->setRepository($this->getRepository())
->withRefs($identifiers)
->execute();
foreach ($identifiers as $key => $identifier) {
if (empty($resolved[$identifier])) {
unset($identifiers[$key]);
}
}
return $identifiers;
}
private function updateCursors(
array $cursors,
array $new_refs,
$ref_type,
array $all_closing_heads) {
$repository = $this->getRepository();
// NOTE: Mercurial branches may have multiple branch heads; this logic
// is complex primarily to account for that.
// Group all the cursors by their ref name, like "master". Since Mercurial
// branches may have multiple heads, there could be several cursors with
// the same name.
$cursor_groups = mgroup($cursors, 'getRefNameRaw');
// Group all the new ref values by their name. As above, these groups may
// have multiple members in Mercurial.
$ref_groups = mgroup($new_refs, 'getShortName');
foreach ($ref_groups as $name => $refs) {
$new_commits = mpull($refs, 'getCommitIdentifier', 'getCommitIdentifier');
$ref_cursors = idx($cursor_groups, $name, array());
$old_commits = mpull($ref_cursors, null, 'getCommitIdentifier');
// We're going to delete all the cursors pointing at commits which are
// no longer associated with the refs. This primarily makes the Mercurial
// multiple head case easier, and means that when we update a ref we
// delete the old one and write a new one.
foreach ($ref_cursors as $cursor) {
if (isset($new_commits[$cursor->getCommitIdentifier()])) {
// This ref previously pointed at this commit, and still does.
$this->log(
pht(
'Ref %s "%s" still points at %s.',
$ref_type,
$name,
$cursor->getCommitIdentifier()));
} else {
// This ref previously pointed at this commit, but no longer does.
$this->log(
pht(
'Ref %s "%s" no longer points at %s.',
$ref_type,
$name,
$cursor->getCommitIdentifier()));
// Nuke the obsolete cursor.
$this->markRefDead($cursor);
}
}
// Now, we're going to insert new cursors for all the commits which are
// associated with this ref that don't currently have cursors.
$added_commits = array_diff_key($new_commits, $old_commits);
foreach ($added_commits as $identifier) {
$this->log(
pht(
'Ref %s "%s" now points at %s.',
$ref_type,
$name,
$identifier));
$this->markRefNew(
id(new PhabricatorRepositoryRefCursor())
->setRepositoryPHID($repository->getPHID())
->setRefType($ref_type)
->setRefName($name)
->setCommitIdentifier($identifier));
}
if ($this->shouldCloseRef($ref_type, $name)) {
foreach ($added_commits as $identifier) {
$new_identifiers = $this->loadNewCommitIdentifiers(
$identifier,
$all_closing_heads);
$this->markCloseCommits($new_identifiers);
}
}
}
// Find any cursors for refs which no longer exist. This happens when a
// branch, tag or bookmark is deleted.
foreach ($cursor_groups as $name => $cursor_group) {
if (idx($ref_groups, $name) === null) {
foreach ($cursor_group as $cursor) {
$this->log(
pht(
'Ref %s "%s" no longer exists.',
$cursor->getRefType(),
$cursor->getRefName()));
$this->markRefDead($cursor);
}
}
}
}
private function shouldCloseRef($ref_type, $ref_name) {
if ($ref_type !== PhabricatorRepositoryRefCursor::TYPE_BRANCH) {
return false;
}
if ($this->hasNoCursors) {
// If we don't have any cursors, don't close things. Particularly, this
// corresponds to the case where you've just updated to this code on an
// existing repository: we don't want to requeue message steps for every
// commit on a closeable ref.
return false;
}
return $this->getRepository()->shouldAutocloseBranch($ref_name);
}
/**
* Find all ancestors of a new closing branch head which are not ancestors
* of any old closing branch head.
*/
private function loadNewCommitIdentifiers(
$new_head,
array $all_closing_heads) {
$repository = $this->getRepository();
$vcs = $repository->getVersionControlSystem();
switch ($vcs) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
if ($all_closing_heads) {
$parts = array();
foreach ($all_closing_heads as $head) {
$parts[] = hgsprintf('%s', $head);
}
// See T5896. Mercurial can not parse an "X or Y or ..." rev list
// with more than about 300 items, because it exceeds the maximum
// allowed recursion depth. Split all the heads into chunks of
// 256, and build a query like this:
//
// ((1 or 2 or ... or 255) or (256 or 257 or ... 511))
//
// If we have more than 65535 heads, we'll do that again:
//
// (((1 or ...) or ...) or ((65536 or ...) or ...))
$chunk_size = 256;
while (count($parts) > $chunk_size) {
$chunks = array_chunk($parts, $chunk_size);
foreach ($chunks as $key => $chunk) {
$chunks[$key] = '('.implode(' or ', $chunk).')';
}
$parts = array_values($chunks);
}
$parts = '('.implode(' or ', $parts).')';
list($stdout) = $this->getRepository()->execxLocalCommand(
'log --template %s --rev %s',
'{node}\n',
hgsprintf('%s', $new_head).' - '.$parts);
} else {
list($stdout) = $this->getRepository()->execxLocalCommand(
'log --template %s --rev %s',
'{node}\n',
hgsprintf('%s', $new_head));
}
$stdout = trim($stdout);
if (!strlen($stdout)) {
return array();
}
return phutil_split_lines($stdout, $retain_newlines = false);
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
if ($all_closing_heads) {
list($stdout) = $this->getRepository()->execxLocalCommand(
'log --format=%s %s --not %Ls',
'%H',
$new_head,
$all_closing_heads);
} else {
list($stdout) = $this->getRepository()->execxLocalCommand(
'log --format=%s %s',
'%H',
$new_head);
}
$stdout = trim($stdout);
if (!strlen($stdout)) {
return array();
}
return phutil_split_lines($stdout, $retain_newlines = false);
default:
throw new Exception(pht('Unsupported VCS "%s"!', $vcs));
}
}
/**
* Mark a list of commits as closeable, and queue workers for those commits
* which don't already have the flag.
*/
private function setCloseFlagOnCommits(array $identifiers) {
$repository = $this->getRepository();
$commit_table = new PhabricatorRepositoryCommit();
$conn_w = $commit_table->establishConnection('w');
$vcs = $repository->getVersionControlSystem();
switch ($vcs) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
$class = 'PhabricatorRepositoryGitCommitMessageParserWorker';
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
$class = 'PhabricatorRepositorySvnCommitMessageParserWorker';
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
$class = 'PhabricatorRepositoryMercurialCommitMessageParserWorker';
break;
default:
- throw new Exception("Unknown repository type '{$vcs}'!");
+ throw new Exception(pht("Unknown repository type '%s'!", $vcs));
}
$all_commits = queryfx_all(
$conn_w,
'SELECT id, commitIdentifier, importStatus FROM %T
WHERE repositoryID = %d AND commitIdentifier IN (%Ls)',
$commit_table->getTableName(),
$repository->getID(),
$identifiers);
$closeable_flag = PhabricatorRepositoryCommit::IMPORTED_CLOSEABLE;
$all_commits = ipull($all_commits, null, 'commitIdentifier');
foreach ($identifiers as $identifier) {
$row = idx($all_commits, $identifier);
if (!$row) {
throw new Exception(
pht(
'Commit "%s" has not been discovered yet! Run discovery before '.
'updating refs.',
$identifier));
}
if (!($row['importStatus'] & $closeable_flag)) {
queryfx(
$conn_w,
'UPDATE %T SET importStatus = (importStatus | %d) WHERE id = %d',
$commit_table->getTableName(),
$closeable_flag,
$row['id']);
$data = array(
'commitID' => $row['id'],
'only' => true,
);
PhabricatorWorker::scheduleTask($class, $data);
}
}
}
/* -( Updating Git Refs )-------------------------------------------------- */
/**
* @task git
*/
private function loadGitBranchPositions(PhabricatorRepository $repository) {
return id(new DiffusionLowLevelGitRefQuery())
->setRepository($repository)
->withIsOriginBranch(true)
->execute();
}
/**
* @task git
*/
private function loadGitTagPositions(PhabricatorRepository $repository) {
return id(new DiffusionLowLevelGitRefQuery())
->setRepository($repository)
->withIsTag(true)
->execute();
}
/* -( Updating Mercurial Refs )-------------------------------------------- */
/**
* @task hg
*/
private function loadMercurialBranchPositions(
PhabricatorRepository $repository) {
return id(new DiffusionLowLevelMercurialBranchesQuery())
->setRepository($repository)
->execute();
}
/**
* @task hg
*/
private function loadMercurialBookmarkPositions(
PhabricatorRepository $repository) {
// TODO: Implement support for Mercurial bookmarks.
return array();
}
}
diff --git a/src/applications/repository/engine/__tests__/PhabricatorWorkingCopyDiscoveryTestCase.php b/src/applications/repository/engine/__tests__/PhabricatorWorkingCopyDiscoveryTestCase.php
index 1e9c83da2..f912a551c 100644
--- a/src/applications/repository/engine/__tests__/PhabricatorWorkingCopyDiscoveryTestCase.php
+++ b/src/applications/repository/engine/__tests__/PhabricatorWorkingCopyDiscoveryTestCase.php
@@ -1,54 +1,54 @@
<?php
final class PhabricatorWorkingCopyDiscoveryTestCase
extends PhabricatorWorkingCopyTestCase {
public function testSubversionCommitDiscovery() {
$refs = $this->discoverRefs('ST');
$this->assertEqual(
array(
1368319433,
1368319448,
),
mpull($refs, 'getEpoch'),
- 'Commit Epochs');
+ pht('Commit Epochs'));
}
public function testMercurialCommitDiscovery() {
$this->requireBinaryForTest('hg');
$refs = $this->discoverRefs('HT');
$this->assertEqual(
array(
'4a110ae879f473f2e82ffd032475caedd6cdba91',
),
mpull($refs, 'getIdentifier'));
}
public function testGitCommitDiscovery() {
$refs = $this->discoverRefs('GT');
$this->assertEqual(
array(
'763d4ab372445551c95fb5cccd1a7a223f5b2ac8',
),
mpull($refs, 'getIdentifier'));
}
private function discoverRefs($callsign) {
$repo = $this->buildPulledRepository($callsign);
$engine = id(new PhabricatorRepositoryDiscoveryEngine())
->setRepository($repo);
$refs = $engine->discoverCommits($repo);
// The next time through, these should be cached as already discovered.
$new_refs = $engine->discoverCommits($repo);
$this->assertEqual(array(), $new_refs);
return $refs;
}
}
diff --git a/src/applications/repository/engine/__tests__/PhabricatorWorkingCopyTestCase.php b/src/applications/repository/engine/__tests__/PhabricatorWorkingCopyTestCase.php
index 40412e45d..5a9be9921 100644
--- a/src/applications/repository/engine/__tests__/PhabricatorWorkingCopyTestCase.php
+++ b/src/applications/repository/engine/__tests__/PhabricatorWorkingCopyTestCase.php
@@ -1,101 +1,108 @@
<?php
abstract class PhabricatorWorkingCopyTestCase extends PhabricatorTestCase {
private $dirs = array();
protected function getPhabricatorTestCaseConfiguration() {
return array(
self::PHABRICATOR_TESTCONFIG_BUILD_STORAGE_FIXTURES => true,
);
}
protected function buildBareRepository($callsign) {
$existing_repository = id(new PhabricatorRepositoryQuery())
->withCallsigns(array($callsign))
->setViewer(PhabricatorUser::getOmnipotentUser())
->executeOne();
if ($existing_repository) {
$existing_repository->delete();
}
$data_dir = dirname(__FILE__).'/data/';
$types = array(
'svn' => PhabricatorRepositoryType::REPOSITORY_TYPE_SVN,
'hg' => PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL,
'git' => PhabricatorRepositoryType::REPOSITORY_TYPE_GIT,
);
$hits = array();
foreach ($types as $type => $const) {
$path = $data_dir.$callsign.'.'.$type.'.tgz';
if (Filesystem::pathExists($path)) {
$hits[$const] = $path;
}
}
if (!$hits) {
throw new Exception(
- "No test data for callsign '{$callsign}'. Expected an archive ".
- "like '{$callsign}.git.tgz' in '{$data_dir}'.");
+ pht(
+ "No test data for callsign '%s'. Expected an archive ".
+ "like '%s' in '%s'.",
+ $callsign,
+ "{$callsign}.git.tgz",
+ $data_dir));
}
if (count($hits) > 1) {
throw new Exception(
- "Expected exactly one archive matching callsign '{$callsign}', ".
- "found too many: ".implode(', ', $hits));
+ pht(
+ "Expected exactly one archive matching callsign '%s', ".
+ "found too many: %s",
+ $callsign,
+ implode(', ', $hits)));
}
$path = head($hits);
$vcs_type = head_key($hits);
$dir = PhutilDirectoryFixture::newFromArchive($path);
$local = new TempFile('.ignore');
$user = $this->generateNewTestUser();
$repo = PhabricatorRepository::initializeNewRepository($user)
->setCallsign($callsign)
->setName(pht('Test Repo "%s"', $callsign))
->setVersionControlSystem($vcs_type)
->setDetail('local-path', dirname($local).'/'.$callsign)
->setDetail('remote-uri', 'file://'.$dir->getPath().'/');
$this->didConstructRepository($repo);
$repo->save();
$repo->makeEphemeral();
// Keep the disk resources around until we exit.
$this->dirs[] = $dir;
$this->dirs[] = $local;
return $repo;
}
protected function didConstructRepository(PhabricatorRepository $repository) {
return;
}
protected function buildPulledRepository($callsign) {
$repository = $this->buildBareRepository($callsign);
id(new PhabricatorRepositoryPullEngine())
->setRepository($repository)
->pullRepository();
return $repository;
}
protected function buildDiscoveredRepository($callsign) {
$repository = $this->buildPulledRepository($callsign);
id(new PhabricatorRepositoryDiscoveryEngine())
->setRepository($repository)
->discoverCommits();
return $repository;
}
}
diff --git a/src/applications/repository/graphcache/PhabricatorRepositoryGraphCache.php b/src/applications/repository/graphcache/PhabricatorRepositoryGraphCache.php
index f1e5aff4e..5c3081a7b 100644
--- a/src/applications/repository/graphcache/PhabricatorRepositoryGraphCache.php
+++ b/src/applications/repository/graphcache/PhabricatorRepositoryGraphCache.php
@@ -1,409 +1,409 @@
<?php
/**
* Given a commit and a path, efficiently determine the most recent ancestor
* commit where the path was touched.
*
* In Git and Mercurial, log operations with a path are relatively slow. For
* example:
*
* git log -n1 <commit> -- <path>
*
* ...routinely takes several hundred milliseconds, and equivalent requests
* often take longer in Mercurial.
*
* Unfortunately, this operation is fundamental to rendering a repository for
* the web, and essentially everything else that's slow can be reduced to this
* plus some trivial work afterward. Making this fast is desirable and powerful,
* and allows us to make other things fast by expressing them in terms of this
* query.
*
* Because the query is fundamentally a graph query, it isn't easy to express
* in a reasonable way in MySQL, and we can't do round trips to the server to
* walk the graph without incurring huge performance penalties.
*
* However, the total amount of data in the graph is relatively small. By
* caching it in chunks and keeping it in APC, we can reasonably load and walk
* the graph in PHP quickly.
*
* For more context, see T2683.
*
* Structure of the Cache
* ======================
*
* The cache divides commits into buckets (see @{method:getBucketSize}). To
* walk the graph, we pull a commit's bucket. The bucket is a map from commit
* IDs to a list of parents and changed paths, separated by `null`. For
* example, a bucket might look like this:
*
* array(
* 1 => array(0, null, 17, 18),
* 2 => array(1, null, 4),
* // ...
* )
*
* This means that commit ID 1 has parent commit 0 (a special value meaning
* no parents) and affected path IDs 17 and 18. Commit ID 2 has parent commit 1,
* and affected path 4.
*
* This data structure attempts to balance compactness, ease of construction,
* simplicity of cache semantics, and lookup performance. In the average case,
* it appears to do a reasonable job at this.
*
* @task query Querying the Graph Cache
* @task cache Cache Internals
*/
final class PhabricatorRepositoryGraphCache {
private $rebuiltKeys = array();
/* -( Querying the Graph Cache )------------------------------------------- */
/**
* Search the graph cache for the most modification to a path.
*
* @param int The commit ID to search ancestors of.
* @param int The path ID to search for changes to.
* @param float Maximum number of seconds to spend trying to satisfy this
* query using the graph cache. By default, `0.5` (500ms).
* @return mixed Commit ID, or `null` if no ancestors exist, or `false` if
* the graph cache was unable to determine the answer.
* @task query
*/
public function loadLastModifiedCommitID($commit_id, $path_id, $time = 0.5) {
$commit_id = (int)$commit_id;
$path_id = (int)$path_id;
$bucket_data = null;
$data_key = null;
$seen = array();
$t_start = microtime(true);
$iterations = 0;
while (true) {
$bucket_key = $this->getBucketKey($commit_id);
if (($data_key != $bucket_key) || $bucket_data === null) {
$bucket_data = $this->getBucketData($bucket_key);
$data_key = $bucket_key;
}
if (empty($bucket_data[$commit_id])) {
// Rebuild the cache bucket, since the commit might be a very recent
// one that we'll pick up by rebuilding.
$bucket_data = $this->getBucketData($bucket_key, $bucket_data);
if (empty($bucket_data[$commit_id])) {
// A rebuild didn't help. This can occur legitimately if the commit
// is new and hasn't parsed yet.
return false;
}
// Otherwise, the rebuild gave us the data, so we can keep going.
}
// Sanity check so we can survive and recover from bad data.
if (isset($seen[$commit_id])) {
- phlog(pht('Unexpected infinite loop in RepositoryGraphCache!'));
+ phlog(pht('Unexpected infinite loop in %s!', __CLASS__));
return false;
} else {
$seen[$commit_id] = true;
}
// `$data` is a list: the commit's parent IDs, followed by `null`,
// followed by the modified paths in ascending order. We figure out the
// first parent first, then check if the path was touched. If the path
// was touched, this is the commit we're after. If not, walk backward
// in the tree.
$items = $bucket_data[$commit_id];
$size = count($items);
// Walk past the parent information.
$parent_id = null;
for ($ii = 0;; ++$ii) {
if ($items[$ii] === null) {
break;
}
if ($parent_id === null) {
$parent_id = $items[$ii];
}
}
// Look for a modification to the path.
for (; $ii < $size; ++$ii) {
$item = $items[$ii];
if ($item > $path_id) {
break;
}
if ($item === $path_id) {
return $commit_id;
}
}
if ($parent_id) {
$commit_id = $parent_id;
// Periodically check if we've spent too long looking for a result
// in the cache, and return so we can fall back to a VCS operation. This
// keeps us from having a degenerate worst case if, e.g., the cache
// is cold and we need to inspect a very large number of blocks
// to satisfy the query.
if (((++$iterations) % 64) === 0) {
$t_end = microtime(true);
if (($t_end - $t_start) > $time) {
return false;
}
}
continue;
}
// If we have an explicit 0, that means this commit really has no parents.
// Usually, it is the first commit in the repository.
if ($parent_id === 0) {
return null;
}
// If we didn't find a parent, the parent data isn't available. We fail
// to find an answer in the cache and fall back to querying the VCS.
return false;
}
}
/* -( Cache Internals )---------------------------------------------------- */
/**
* Get the bucket key for a given commit ID.
*
* @param int Commit ID.
* @return int Bucket key.
* @task cache
*/
private function getBucketKey($commit_id) {
return (int)floor($commit_id / $this->getBucketSize());
}
/**
* Get the cache key for a given bucket key (from @{method:getBucketKey}).
*
* @param int Bucket key.
* @return string Cache key.
* @task cache
*/
private function getBucketCacheKey($bucket_key) {
static $prefix;
if ($prefix === null) {
$self = get_class($this);
$size = $this->getBucketSize();
$prefix = "{$self}:{$size}:2:";
}
return $prefix.$bucket_key;
}
/**
* Get the number of items per bucket.
*
* @return int Number of items to store per bucket.
* @task cache
*/
private function getBucketSize() {
return 4096;
}
/**
* Retrieve or build a graph cache bucket from the cache.
*
* Normally, this operates as a readthrough cache call. It can also be used
* to force a cache update by passing the existing data to `$rebuild_data`.
*
* @param int Bucket key, from @{method:getBucketKey}.
* @param mixed Current data, to force a cache rebuild of this bucket.
* @return array Data from the cache.
* @task cache
*/
private function getBucketData($bucket_key, $rebuild_data = null) {
$cache_key = $this->getBucketCacheKey($bucket_key);
// TODO: This cache stuff could be handled more gracefully, but the
// database cache currently requires values to be strings and needs
// some tweaking to support this as part of a stack. Our cache semantics
// here are also unusual (not purely readthrough) because this cache is
// appendable.
$cache_level1 = PhabricatorCaches::getRepositoryGraphL1Cache();
$cache_level2 = PhabricatorCaches::getRepositoryGraphL2Cache();
if ($rebuild_data === null) {
$bucket_data = $cache_level1->getKey($cache_key);
if ($bucket_data) {
return $bucket_data;
}
$bucket_data = $cache_level2->getKey($cache_key);
if ($bucket_data) {
$unserialized = @unserialize($bucket_data);
if ($unserialized) {
// Fill APC if we got a database hit but missed in APC.
$cache_level1->setKey($cache_key, $unserialized);
return $unserialized;
}
}
}
if (!is_array($rebuild_data)) {
$rebuild_data = array();
}
$bucket_data = $this->rebuildBucket($bucket_key, $rebuild_data);
// Don't bother writing the data if we didn't update anything.
if ($bucket_data !== $rebuild_data) {
$cache_level2->setKey($cache_key, serialize($bucket_data));
$cache_level1->setKey($cache_key, $bucket_data);
}
return $bucket_data;
}
/**
* Rebuild a cache bucket, amending existing data if available.
*
* @param int Bucket key, from @{method:getBucketKey}.
* @param array Existing bucket data.
* @return array Rebuilt bucket data.
* @task cache
*/
private function rebuildBucket($bucket_key, array $current_data) {
// First, check if we've already rebuilt this bucket. In some cases (like
// browsing a repository at some commit) it's common to issue many lookups
// against one commit. If that commit has been discovered but not yet
// fully imported, we'll repeatedly attempt to rebuild the bucket. If the
// first rebuild did not work, subsequent rebuilds are very unlikely to
// have any effect. We can just skip the rebuild in these cases.
if (isset($this->rebuiltKeys[$bucket_key])) {
return $current_data;
} else {
$this->rebuiltKeys[$bucket_key] = true;
}
$bucket_min = ($bucket_key * $this->getBucketSize());
$bucket_max = ($bucket_min + $this->getBucketSize()) - 1;
// We need to reload all of the commits in the bucket because there is
// no guarantee that they'll get parsed in order, so we can fill large
// commit IDs before small ones. Later on, we'll ignore the commits we
// already know about.
$table_commit = new PhabricatorRepositoryCommit();
$table_repository = new PhabricatorRepository();
$conn_r = $table_commit->establishConnection('r');
// Find all the Git and Mercurial commits in the block which have completed
// change import. We can't fill the cache accurately for commits which have
// not completed change import, so just pretend we don't know about them.
// In these cases, we will will ultimately fall back to VCS queries.
$commit_rows = queryfx_all(
$conn_r,
'SELECT c.id FROM %T c
JOIN %T r ON c.repositoryID = r.id AND r.versionControlSystem IN (%Ls)
WHERE c.id BETWEEN %d AND %d
AND (c.importStatus & %d) = %d',
$table_commit->getTableName(),
$table_repository->getTableName(),
array(
PhabricatorRepositoryType::REPOSITORY_TYPE_GIT,
PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL,
),
$bucket_min,
$bucket_max,
PhabricatorRepositoryCommit::IMPORTED_CHANGE,
PhabricatorRepositoryCommit::IMPORTED_CHANGE);
// If we don't have any data, just return the existing data.
if (!$commit_rows) {
return $current_data;
}
// Remove the commits we already have data for. We don't need to rebuild
// these. If there's nothing left, return the existing data.
$commit_ids = ipull($commit_rows, 'id', 'id');
$commit_ids = array_diff_key($commit_ids, $current_data);
if (!$commit_ids) {
return $current_data;
}
// Find all the path changes for the new commits.
$path_changes = queryfx_all(
$conn_r,
'SELECT commitID, pathID FROM %T
WHERE commitID IN (%Ld)
AND (isDirect = 1 OR changeType = %d)',
PhabricatorRepository::TABLE_PATHCHANGE,
$commit_ids,
DifferentialChangeType::TYPE_CHILD);
$path_changes = igroup($path_changes, 'commitID');
// Find all the parents for the new commits.
$parents = queryfx_all(
$conn_r,
'SELECT childCommitID, parentCommitID FROM %T
WHERE childCommitID IN (%Ld)
ORDER BY id ASC',
PhabricatorRepository::TABLE_PARENTS,
$commit_ids);
$parents = igroup($parents, 'childCommitID');
// Build the actual data for the cache.
foreach ($commit_ids as $commit_id) {
$parent_ids = array();
if (!empty($parents[$commit_id])) {
foreach ($parents[$commit_id] as $row) {
$parent_ids[] = (int)$row['parentCommitID'];
}
} else {
// We expect all rows to have parents (commits with no parents get
// an explicit "0" placeholder). If we're in an older repository, the
// parent information might not have been populated yet. Decline to fill
// the cache if we don't have the parent information, since the fill
// will be incorrect.
continue;
}
if (isset($path_changes[$commit_id])) {
$path_ids = $path_changes[$commit_id];
foreach ($path_ids as $key => $path_id) {
$path_ids[$key] = (int)$path_id['pathID'];
}
sort($path_ids);
} else {
$path_ids = array();
}
$value = $parent_ids;
$value[] = null;
foreach ($path_ids as $path_id) {
$value[] = $path_id;
}
$current_data[$commit_id] = $value;
}
return $current_data;
}
}
diff --git a/src/applications/repository/management/PhabricatorRepositoryManagementCacheWorkflow.php b/src/applications/repository/management/PhabricatorRepositoryManagementCacheWorkflow.php
index b34e430ff..a78dff1a7 100644
--- a/src/applications/repository/management/PhabricatorRepositoryManagementCacheWorkflow.php
+++ b/src/applications/repository/management/PhabricatorRepositoryManagementCacheWorkflow.php
@@ -1,92 +1,94 @@
<?php
final class PhabricatorRepositoryManagementCacheWorkflow
extends PhabricatorRepositoryManagementWorkflow {
protected function didConstruct() {
$this
->setName('cache')
->setExamples(
'**cache** [__options__] --commit __commit__ --path __path__')
->setSynopsis(pht('Manage the repository graph cache.'))
->setArguments(
array(
array(
'name' => 'commit',
'param' => 'commit',
'help' => pht('Specify a commit to look up.'),
),
array(
'name' => 'path',
'param' => 'path',
'help' => pht('Specify a path to look up.'),
),
));
}
public function execute(PhutilArgumentParser $args) {
$commit_name = $args->getArg('commit');
if ($commit_name === null) {
throw new PhutilArgumentUsageException(
- pht('Specify a commit to look up with `--commit`.'));
+ pht(
+ 'Specify a commit to look up with `%s`.',
+ '--commit'));
}
$commit = $this->loadNamedCommit($commit_name);
$path_name = $args->getArg('path');
if ($path_name === null) {
throw new PhutilArgumentUsageException(
- pht('Specify a path to look up with `--path`.'));
+ pht(
+ 'Specify a path to look up with `%s`.',
+ '--path'));
}
$path_map = id(new DiffusionPathIDQuery(array($path_name)))
->loadPathIDs();
if (empty($path_map[$path_name])) {
throw new PhutilArgumentUsageException(
pht('Path "%s" is not known to Phabricator.', $path_name));
}
$path_id = $path_map[$path_name];
$graph_cache = new PhabricatorRepositoryGraphCache();
$t_start = microtime(true);
$cache_result = $graph_cache->loadLastModifiedCommitID(
$commit->getID(),
$path_id);
$t_end = microtime(true);
$console = PhutilConsole::getConsole();
$console->writeOut(
"%s\n",
pht('Query took %s ms.', new PhutilNumber(1000 * ($t_end - $t_start))));
if ($cache_result === false) {
- $console->writeOut(
- "%s\n",
- pht('Not found in graph cache.'));
+ $console->writeOut("%s\n", pht('Not found in graph cache.'));
} else if ($cache_result === null) {
$console->writeOut(
"%s\n",
pht('Path not modified in any ancestor commit.'));
} else {
$last = id(new DiffusionCommitQuery())
->setViewer($this->getViewer())
->withIDs(array($cache_result))
->executeOne();
if (!$last) {
throw new Exception(pht('Cache returned bogus result!'));
}
$console->writeOut(
"%s\n",
pht(
'Path was last changed at %s.',
$commit->getRepository()->formatCommitName(
$last->getcommitIdentifier())));
}
return 0;
}
}
diff --git a/src/applications/repository/management/PhabricatorRepositoryManagementDiscoverWorkflow.php b/src/applications/repository/management/PhabricatorRepositoryManagementDiscoverWorkflow.php
index 66d9a22fe..526348d15 100644
--- a/src/applications/repository/management/PhabricatorRepositoryManagementDiscoverWorkflow.php
+++ b/src/applications/repository/management/PhabricatorRepositoryManagementDiscoverWorkflow.php
@@ -1,53 +1,55 @@
<?php
final class PhabricatorRepositoryManagementDiscoverWorkflow
extends PhabricatorRepositoryManagementWorkflow {
protected function didConstruct() {
$this
->setName('discover')
->setExamples('**discover** [__options__] __repository__ ...')
- ->setSynopsis('Discover __repository__, named by callsign.')
+ ->setSynopsis(pht('Discover __repository__, named by callsign.'))
->setArguments(
array(
array(
'name' => 'verbose',
- 'help' => 'Show additional debugging information.',
+ 'help' => pht('Show additional debugging information.'),
),
array(
'name' => 'repair',
- 'help' => 'Repair a repository with gaps in commit '.
- 'history.',
+ 'help' => pht(
+ 'Repair a repository with gaps in commit history.'),
),
array(
'name' => 'repos',
'wildcard' => true,
),
));
}
public function execute(PhutilArgumentParser $args) {
$repos = $this->loadRepositories($args, 'repos');
if (!$repos) {
throw new PhutilArgumentUsageException(
- 'Specify one or more repositories to discover, by callsign.');
+ pht('Specify one or more repositories to discover, by callsign.'));
}
$console = PhutilConsole::getConsole();
foreach ($repos as $repo) {
- $console->writeOut("Discovering '%s'...\n", $repo->getCallsign());
+ $console->writeOut(
+ "%s\n",
+ pht("Discovering '%s'...", $repo->getCallsign()));
id(new PhabricatorRepositoryDiscoveryEngine())
->setRepository($repo)
->setVerbose($args->getArg('verbose'))
->setRepairMode($args->getArg('repair'))
->discoverCommits();
}
- $console->writeOut("Done.\n");
+ $console->writeOut("%s\n", pht('Done.'));
return 0;
}
}
diff --git a/src/applications/repository/management/PhabricatorRepositoryManagementEditWorkflow.php b/src/applications/repository/management/PhabricatorRepositoryManagementEditWorkflow.php
index ac7a7c6b0..d42ecf952 100644
--- a/src/applications/repository/management/PhabricatorRepositoryManagementEditWorkflow.php
+++ b/src/applications/repository/management/PhabricatorRepositoryManagementEditWorkflow.php
@@ -1,96 +1,98 @@
<?php
final class PhabricatorRepositoryManagementEditWorkflow
extends PhabricatorRepositoryManagementWorkflow {
protected function didConstruct() {
$this
->setName('edit')
->setExamples('**edit** --as __username__ __repository__ ...')
- ->setSynopsis('Edit __repository__, named by callsign.')
+ ->setSynopsis(pht('Edit __repository__, named by callsign.'))
->setArguments(
array(
array(
'name' => 'repos',
'wildcard' => true,
),
array(
'name' => 'as',
'param' => 'user',
- 'help' => 'Edit as user.',
+ 'help' => pht('Edit as user.'),
),
array(
'name' => 'local-path',
'param' => 'path',
- 'help' => 'Edit the local path.',
+ 'help' => pht('Edit the local path.'),
),
));
}
public function execute(PhutilArgumentParser $args) {
$repos = $this->loadRepositories($args, 'repos');
if (!$repos) {
throw new PhutilArgumentUsageException(
- 'Specify one or more repositories to edit, by callsign.');
+ pht('Specify one or more repositories to edit, by callsign.'));
}
$console = PhutilConsole::getConsole();
// TODO: It would be nice to just take this action as "Administrator" or
// similar, since that would make it easier to use this script, harder to
// impersonate users, and more clear to viewers what happened. However,
// the omnipotent user doesn't have a PHID right now, can't be loaded,
// doesn't have a handle, etc. Adding all of that is fairly involved, and
// I want to wait for stronger use cases first.
$username = $args->getArg('as');
if (!$username) {
throw new PhutilArgumentUsageException(
- pht('Specify a user to edit as with --as <username>.'));
+ pht(
+ 'Specify a user to edit as with %s.',
+ '--as <username>'));
}
$actor = id(new PhabricatorPeopleQuery())
->setViewer($this->getViewer())
->withUsernames(array($username))
->executeOne();
if (!$actor) {
throw new PhutilArgumentUsageException(
pht("No such user '%s'!", $username));
}
foreach ($repos as $repo) {
- $console->writeOut("Editing '%s'...\n", $repo->getCallsign());
+ $console->writeOut("%s\n", pht("Editing '%s'...", $repo->getCallsign()));
$xactions = array();
$type_local_path = PhabricatorRepositoryTransaction::TYPE_LOCAL_PATH;
if ($args->getArg('local-path')) {
$xactions[] = id(new PhabricatorRepositoryTransaction())
->setTransactionType($type_local_path)
->setNewValue($args->getArg('local-path'));
}
if (!$xactions) {
throw new PhutilArgumentUsageException(
pht('Specify one or more fields to edit!'));
}
$content_source = PhabricatorContentSource::newConsoleSource();
$editor = id(new PhabricatorRepositoryEditor())
->setActor($actor)
->setContentSource($content_source)
->setContinueOnNoEffect(true)
->setContinueOnMissingFields(true)
->applyTransactions($repo, $xactions);
}
- $console->writeOut("Done.\n");
+ $console->writeOut("%s\n", pht('Done.'));
return 0;
}
}
diff --git a/src/applications/repository/management/PhabricatorRepositoryManagementImportingWorkflow.php b/src/applications/repository/management/PhabricatorRepositoryManagementImportingWorkflow.php
index d895b07ae..7fabffe7b 100644
--- a/src/applications/repository/management/PhabricatorRepositoryManagementImportingWorkflow.php
+++ b/src/applications/repository/management/PhabricatorRepositoryManagementImportingWorkflow.php
@@ -1,87 +1,89 @@
<?php
final class PhabricatorRepositoryManagementImportingWorkflow
extends PhabricatorRepositoryManagementWorkflow {
protected function didConstruct() {
$this
->setName('importing')
->setExamples('**importing** __repository__ ...')
->setSynopsis(
- 'Show commits in __repository__, named by callsign, which are still '.
- 'importing.')
+ pht(
+ 'Show commits in __repository__, named by callsign, which are '.
+ 'still importing.'))
->setArguments(
array(
array(
'name' => 'simple',
- 'help' => 'Show simpler output.',
+ 'help' => pht('Show simpler output.'),
),
array(
'name' => 'repos',
'wildcard' => true,
),
));
}
public function execute(PhutilArgumentParser $args) {
$repos = $this->loadRepositories($args, 'repos');
if (!$repos) {
throw new PhutilArgumentUsageException(
- 'Specify one or more repositories to find importing commits for, '.
- 'by callsign.');
+ pht(
+ 'Specify one or more repositories to find importing commits for, '.
+ 'by callsign.'));
}
$repos = mpull($repos, null, 'getID');
$table = new PhabricatorRepositoryCommit();
$conn_r = $table->establishConnection('r');
$rows = queryfx_all(
$conn_r,
'SELECT repositoryID, commitIdentifier, importStatus FROM %T
WHERE repositoryID IN (%Ld) AND (importStatus & %d) != %d',
$table->getTableName(),
array_keys($repos),
PhabricatorRepositoryCommit::IMPORTED_ALL,
PhabricatorRepositoryCommit::IMPORTED_ALL);
$console = PhutilConsole::getConsole();
if ($rows) {
foreach ($rows as $row) {
$repo = $repos[$row['repositoryID']];
$identifier = $row['commitIdentifier'];
$console->writeOut('%s', 'r'.$repo->getCallsign().$identifier);
if (!$args->getArg('simple')) {
$status = $row['importStatus'];
$need = array();
if (!($status & PhabricatorRepositoryCommit::IMPORTED_MESSAGE)) {
$need[] = 'Message';
}
if (!($status & PhabricatorRepositoryCommit::IMPORTED_CHANGE)) {
$need[] = 'Change';
}
if (!($status & PhabricatorRepositoryCommit::IMPORTED_OWNERS)) {
$need[] = 'Owners';
}
if (!($status & PhabricatorRepositoryCommit::IMPORTED_HERALD)) {
$need[] = 'Herald';
}
$console->writeOut(' %s', implode(', ', $need));
}
$console->writeOut("\n");
}
} else {
$console->writeErr(
"%s\n",
pht('No importing commits found.'));
}
return 0;
}
}
diff --git a/src/applications/repository/management/PhabricatorRepositoryManagementListWorkflow.php b/src/applications/repository/management/PhabricatorRepositoryManagementListWorkflow.php
index 9d584af73..a66314298 100644
--- a/src/applications/repository/management/PhabricatorRepositoryManagementListWorkflow.php
+++ b/src/applications/repository/management/PhabricatorRepositoryManagementListWorkflow.php
@@ -1,30 +1,30 @@
<?php
final class PhabricatorRepositoryManagementListWorkflow
extends PhabricatorRepositoryManagementWorkflow {
protected function didConstruct() {
$this
->setName('list')
- ->setSynopsis('Show a list of repositories.')
+ ->setSynopsis(pht('Show a list of repositories.'))
->setArguments(array());
}
public function execute(PhutilArgumentParser $args) {
$console = PhutilConsole::getConsole();
$repos = id(new PhabricatorRepositoryQuery())
->setViewer($this->getViewer())
->execute();
if ($repos) {
foreach ($repos as $repo) {
$console->writeOut("%s\n", $repo->getCallsign());
}
} else {
- $console->writeErr("%s\n", 'There are no repositories.');
+ $console->writeErr("%s\n", pht('There are no repositories.'));
}
return 0;
}
}
diff --git a/src/applications/repository/management/PhabricatorRepositoryManagementLookupUsersWorkflow.php b/src/applications/repository/management/PhabricatorRepositoryManagementLookupUsersWorkflow.php
index e4f03a4d1..949fd9614 100644
--- a/src/applications/repository/management/PhabricatorRepositoryManagementLookupUsersWorkflow.php
+++ b/src/applications/repository/management/PhabricatorRepositoryManagementLookupUsersWorkflow.php
@@ -1,114 +1,115 @@
<?php
final class PhabricatorRepositoryManagementLookupUsersWorkflow
extends PhabricatorRepositoryManagementWorkflow {
protected function didConstruct() {
$this
->setName('lookup-users')
->setExamples('**lookup-users** __commit__ ...')
- ->setSynopsis('Resolve user accounts for users attached to __commit__.')
+ ->setSynopsis(
+ pht('Resolve user accounts for users attached to __commit__.'))
->setArguments(
array(
array(
'name' => 'commits',
'wildcard' => true,
),
));
}
public function execute(PhutilArgumentParser $args) {
$commits = $this->loadCommits($args, 'commits');
if (!$commits) {
throw new PhutilArgumentUsageException(
- 'Specify one or more commits to resolve users for.');
+ pht('Specify one or more commits to resolve users for.'));
}
$console = PhutilConsole::getConsole();
foreach ($commits as $commit) {
$repo = $commit->getRepository();
$name = $repo->formatCommitName($commit->getCommitIdentifier());
$console->writeOut(
"%s\n",
pht('Examining commit %s...', $name));
$refs_raw = DiffusionQuery::callConduitWithDiffusionRequest(
$this->getViewer(),
DiffusionRequest::newFromDictionary(
array(
'repository' => $repo,
'user' => $this->getViewer(),
)),
'diffusion.querycommits',
array(
'phids' => array($commit->getPHID()),
'bypassCache' => true,
));
if (empty($refs_raw['data'])) {
throw new Exception(
pht(
'Unable to retrieve details for commit "%s"!',
$commit->getPHID()));
}
$ref = DiffusionCommitRef::newFromConduitResult(head($refs_raw['data']));
$author = $ref->getAuthor();
$console->writeOut(
"%s\n",
pht('Raw author string: %s', coalesce($author, 'null')));
if ($author !== null) {
$handle = $this->resolveUser($commit, $author);
if ($handle) {
$console->writeOut(
"%s\n",
pht('Phabricator user: %s', $handle->getFullName()));
} else {
$console->writeOut(
"%s\n",
pht('Unable to resolve a corresponding Phabricator user.'));
}
}
$committer = $ref->getCommitter();
$console->writeOut(
"%s\n",
pht('Raw committer string: %s', coalesce($committer, 'null')));
if ($committer !== null) {
$handle = $this->resolveUser($commit, $committer);
if ($handle) {
$console->writeOut(
"%s\n",
pht('Phabricator user: %s', $handle->getFullName()));
} else {
$console->writeOut(
"%s\n",
pht('Unable to resolve a corresponding Phabricator user.'));
}
}
}
return 0;
}
private function resolveUser(PhabricatorRepositoryCommit $commit, $name) {
$phid = id(new DiffusionResolveUserQuery())
->withCommit($commit)
->withName($name)
->execute();
if (!$phid) {
return null;
}
return id(new PhabricatorHandleQuery())
->setViewer($this->getViewer())
->withPHIDs(array($phid))
->executeOne();
}
}
diff --git a/src/applications/repository/management/PhabricatorRepositoryManagementMarkImportedWorkflow.php b/src/applications/repository/management/PhabricatorRepositoryManagementMarkImportedWorkflow.php
index 411a3e10c..fb14261df 100644
--- a/src/applications/repository/management/PhabricatorRepositoryManagementMarkImportedWorkflow.php
+++ b/src/applications/repository/management/PhabricatorRepositoryManagementMarkImportedWorkflow.php
@@ -1,67 +1,67 @@
<?php
final class PhabricatorRepositoryManagementMarkImportedWorkflow
extends PhabricatorRepositoryManagementWorkflow {
protected function didConstruct() {
$this
->setName('mark-imported')
->setExamples('**mark-imported** __repository__ ...')
- ->setSynopsis('Mark __repository__, named by callsign, as imported.')
+ ->setSynopsis(pht('Mark __repository__, named by callsign, as imported.'))
->setArguments(
array(
array(
'name' => 'mark-not-imported',
- 'help' => 'Instead, mark repositories as NOT imported.',
+ 'help' => pht('Instead, mark repositories as NOT imported.'),
),
array(
'name' => 'repos',
'wildcard' => true,
),
));
}
public function execute(PhutilArgumentParser $args) {
$repos = $this->loadRepositories($args, 'repos');
if (!$repos) {
throw new PhutilArgumentUsageException(
- 'Specify one or more repositories to mark imported, by callsign.');
+ pht('Specify one or more repositories to mark imported, by callsign.'));
}
$new_importing_value = (bool)$args->getArg('mark-not-imported');
$console = PhutilConsole::getConsole();
foreach ($repos as $repo) {
$callsign = $repo->getCallsign();
if ($repo->isImporting() && $new_importing_value) {
$console->writeOut(
"%s\n",
pht("Repository '%s' is already importing.", $callsign));
} else if (!$repo->isImporting() && !$new_importing_value) {
$console->writeOut(
"%s\n",
pht("Repository '%s' is already imported.", $callsign));
} else {
if ($new_importing_value) {
$console->writeOut(
"%s\n",
pht("Marking repository '%s' as importing.", $callsign));
} else {
$console->writeOut(
"%s\n",
pht("Marking repository '%s' as imported.", $callsign));
}
$repo->setDetail('importing', $new_importing_value);
$repo->save();
}
}
- $console->writeOut("Done.\n");
+ $console->writeOut("%s\n", pht('Done.'));
return 0;
}
}
diff --git a/src/applications/repository/management/PhabricatorRepositoryManagementMirrorWorkflow.php b/src/applications/repository/management/PhabricatorRepositoryManagementMirrorWorkflow.php
index 243ae640a..4a4c73255 100644
--- a/src/applications/repository/management/PhabricatorRepositoryManagementMirrorWorkflow.php
+++ b/src/applications/repository/management/PhabricatorRepositoryManagementMirrorWorkflow.php
@@ -1,52 +1,51 @@
<?php
final class PhabricatorRepositoryManagementMirrorWorkflow
extends PhabricatorRepositoryManagementWorkflow {
protected function didConstruct() {
$this
->setName('mirror')
->setExamples('**mirror** [__options__] __repository__ ...')
->setSynopsis(
pht('Push __repository__, named by callsign, to mirrors.'))
->setArguments(
array(
array(
'name' => 'verbose',
'help' => pht('Show additional debugging information.'),
),
array(
'name' => 'repos',
'wildcard' => true,
),
));
}
public function execute(PhutilArgumentParser $args) {
$repos = $this->loadRepositories($args, 'repos');
if (!$repos) {
throw new PhutilArgumentUsageException(
pht(
- 'Specify one or more repositories to push to mirrors, by '.
- 'callsign.'));
+ 'Specify one or more repositories to push to mirrors, by callsign.'));
}
$console = PhutilConsole::getConsole();
foreach ($repos as $repo) {
$console->writeOut(
"%s\n",
pht('Pushing "%s" to mirrors...', $repo->getCallsign()));
$engine = id(new PhabricatorRepositoryMirrorEngine())
->setRepository($repo)
->setVerbose($args->getArg('verbose'))
->pushToMirrors();
}
- $console->writeOut("Done.\n");
+ $console->writeOut('%s\b', pht('Done.'));
return 0;
}
}
diff --git a/src/applications/repository/management/PhabricatorRepositoryManagementPullWorkflow.php b/src/applications/repository/management/PhabricatorRepositoryManagementPullWorkflow.php
index 4cc760561..b58c56d46 100644
--- a/src/applications/repository/management/PhabricatorRepositoryManagementPullWorkflow.php
+++ b/src/applications/repository/management/PhabricatorRepositoryManagementPullWorkflow.php
@@ -1,47 +1,47 @@
<?php
final class PhabricatorRepositoryManagementPullWorkflow
extends PhabricatorRepositoryManagementWorkflow {
protected function didConstruct() {
$this
->setName('pull')
->setExamples('**pull** __repository__ ...')
- ->setSynopsis('Pull __repository__, named by callsign.')
+ ->setSynopsis(pht('Pull __repository__, named by callsign.'))
->setArguments(
array(
array(
- 'name' => 'verbose',
- 'help' => 'Show additional debugging information.',
+ 'name' => 'verbose',
+ 'help' => pht('Show additional debugging information.'),
),
array(
- 'name' => 'repos',
- 'wildcard' => true,
+ 'name' => 'repos',
+ 'wildcard' => true,
),
));
}
public function execute(PhutilArgumentParser $args) {
$repos = $this->loadRepositories($args, 'repos');
if (!$repos) {
throw new PhutilArgumentUsageException(
- 'Specify one or more repositories to pull, by callsign.');
+ pht('Specify one or more repositories to pull, by callsign.'));
}
$console = PhutilConsole::getConsole();
foreach ($repos as $repo) {
- $console->writeOut("Pulling '%s'...\n", $repo->getCallsign());
+ $console->writeOut("%s\n", pht("Pulling '%s'...", $repo->getCallsign()));
id(new PhabricatorRepositoryPullEngine())
->setRepository($repo)
->setVerbose($args->getArg('verbose'))
->pullRepository();
}
- $console->writeOut("Done.\n");
+ $console->writeOut("%s\n", pht('Done.'));
return 0;
}
}
diff --git a/src/applications/repository/management/PhabricatorRepositoryManagementRefsWorkflow.php b/src/applications/repository/management/PhabricatorRepositoryManagementRefsWorkflow.php
index 77eb9608f..5d5dd169e 100644
--- a/src/applications/repository/management/PhabricatorRepositoryManagementRefsWorkflow.php
+++ b/src/applications/repository/management/PhabricatorRepositoryManagementRefsWorkflow.php
@@ -1,49 +1,51 @@
<?php
final class PhabricatorRepositoryManagementRefsWorkflow
extends PhabricatorRepositoryManagementWorkflow {
protected function didConstruct() {
$this
->setName('refs')
->setExamples('**refs** [__options__] __repository__ ...')
- ->setSynopsis('Update refs in __repository__, named by callsign.')
+ ->setSynopsis(pht('Update refs in __repository__, named by callsign.'))
->setArguments(
array(
array(
'name' => 'verbose',
- 'help' => 'Show additional debugging information.',
+ 'help' => pht('Show additional debugging information.'),
),
array(
'name' => 'repos',
'wildcard' => true,
),
));
}
public function execute(PhutilArgumentParser $args) {
$repos = $this->loadRepositories($args, 'repos');
if (!$repos) {
throw new PhutilArgumentUsageException(
pht(
'Specify one or more repositories to update refs for, '.
'by callsign.'));
}
$console = PhutilConsole::getConsole();
foreach ($repos as $repo) {
- $console->writeOut("Updating refs in '%s'...\n", $repo->getCallsign());
+ $console->writeOut(
+ "%s\n",
+ pht("Updating refs in '%s'...", $repo->getCallsign()));
$engine = id(new PhabricatorRepositoryRefEngine())
->setRepository($repo)
->setVerbose($args->getArg('verbose'))
->updateRefs();
}
- $console->writeOut("Done.\n");
+ $console->writeOut("%s\n", pht('Done.'));
return 0;
}
}
diff --git a/src/applications/repository/management/PhabricatorRepositoryManagementReparseWorkflow.php b/src/applications/repository/management/PhabricatorRepositoryManagementReparseWorkflow.php
index 9f3aa2715..275577ea3 100644
--- a/src/applications/repository/management/PhabricatorRepositoryManagementReparseWorkflow.php
+++ b/src/applications/repository/management/PhabricatorRepositoryManagementReparseWorkflow.php
@@ -1,309 +1,320 @@
<?php
final class PhabricatorRepositoryManagementReparseWorkflow
extends PhabricatorRepositoryManagementWorkflow {
protected function didConstruct() {
$this
->setName('reparse')
->setExamples('**reparse** [options] __repository__')
- ->setSynopsis(pht(
- '**reparse** __what__ __which_parts__ [--trace] [--force]'."\n\n".
- 'Rerun the Diffusion parser on specific commits and repositories. '.
- 'Mostly useful for debugging changes to Diffusion.'."\n\n".
- 'e.g. enqueue reparse owners in the TEST repo for all commits:'."\n".
- 'repository reparse --all TEST --owners'."\n\n".
- 'e.g. do same but exclude before yesterday (local time):'."\n".
- 'repository reparse --all TEST --owners --min-date yesterday'."\n".
- 'repository reparse --all TEST --owners --min-date "today -1 day".'.
- "\n\n".
- 'e.g. do same but exclude before 03/31/2013 (local time):'."\n".
- 'repository reparse --all TEST --owners --min-date "03/31/2013"'))
+ ->setSynopsis(
+ pht(
+ '**reparse** __what__ __which_parts__ [--trace] [--force]'."\n\n".
+ 'Rerun the Diffusion parser on specific commits and repositories. '.
+ 'Mostly useful for debugging changes to Diffusion.'."\n\n".
+ 'e.g. enqueue reparse owners in the TEST repo for all commits:'."\n".
+ 'repository reparse --all TEST --owners'."\n\n".
+ 'e.g. do same but exclude before yesterday (local time):'."\n".
+ 'repository reparse --all TEST --owners --min-date yesterday'."\n".
+ 'repository reparse --all TEST --owners --min-date "today -1 day".'.
+ "\n\n".
+ 'e.g. do same but exclude before 03/31/2013 (local time):'."\n".
+ 'repository reparse --all TEST --owners --min-date "03/31/2013"'))
->setArguments(
array(
array(
'name' => 'revision',
'wildcard' => true,
),
array(
'name' => 'all',
'param' => 'callsign or phid',
'help' => pht(
'Reparse all commits in the specified repository. This mode '.
'queues parsers into the task queue; you must run taskmasters '.
- 'to actually do the parses. Use with __--force-local__ to run '.
- 'the tasks locally instead of with taskmasters.'),
+ 'to actually do the parses. Use with __%s__ to run '.
+ 'the tasks locally instead of with taskmasters.',
+ '--force-local'),
),
array(
'name' => 'min-date',
'param' => 'date',
'help' => pht(
- 'Must be used with __--all__, this will exclude commits which '.
- 'are earlier than __date__.'."\n".
+ "Must be used with __%s__, this will exclude commits which ".
+ "are earlier than __date__.\n".
"Valid examples:\n".
" 'today', 'today 2pm', '-1 hour', '-2 hours', '-24 hours',\n".
" 'yesterday', 'today -1 day', 'yesterday 2pm', '2pm -1 day',\n".
" 'last Monday', 'last Monday 14:00', 'last Monday 2pm',\n".
" '31 March 2013', '31 Mar', '03/31', '03/31/2013',\n".
- 'See __http://www.php.net/manual/en/datetime.formats.php__ for '.
- 'more.'),
+ "See __%s__ for more.",
+ '--all',
+ 'http://www.php.net/manual/en/datetime.formats.php'),
),
array(
'name' => 'message',
'help' => pht('Reparse commit messages.'),
),
array(
'name' => 'change',
'help' => pht('Reparse changes.'),
),
array(
'name' => 'herald',
'help' => pht(
'Reevaluate Herald rules (may send huge amounts of email!)'),
),
array(
'name' => 'owners',
'help' => pht(
'Reevaluate related commits for owners packages (may delete '.
'existing relationship entries between your package and some '.
'old commits!)'),
),
array(
'name' => 'force',
'short' => 'f',
'help' => pht('Act noninteractively, without prompting.'),
),
array(
'name' => 'force-local',
'help' => pht(
- 'Only used with __--all__, use this to run the tasks locally '.
- 'instead of deferring them to taskmaster daemons.'),
+ 'Only used with __%s__, use this to run the tasks locally '.
+ 'instead of deferring them to taskmaster daemons.',
+ '--all'),
),
array(
'name' => 'force-autoclose',
'help' => pht(
- 'Only used with __--message__, use this to make sure any '.
- 'pertinent diffs are closed regardless of configuration.'),
+ 'Only used with __%s, use this to make sure any '.
+ 'pertinent diffs are closed regardless of configuration.',
+ '--message__'),
),
));
}
public function execute(PhutilArgumentParser $args) {
$console = PhutilConsole::getConsole();
$all_from_repo = $args->getArg('all');
$reparse_message = $args->getArg('message');
$reparse_change = $args->getArg('change');
$reparse_herald = $args->getArg('herald');
$reparse_owners = $args->getArg('owners');
$reparse_what = $args->getArg('revision');
$force = $args->getArg('force');
$force_local = $args->getArg('force-local');
$min_date = $args->getArg('min-date');
if (!$all_from_repo && !$reparse_what) {
throw new PhutilArgumentUsageException(
pht('Specify a commit or repository to reparse.'));
}
if ($all_from_repo && $reparse_what) {
$commits = implode(', ', $reparse_what);
throw new PhutilArgumentUsageException(
pht(
"Specify a commit or repository to reparse, not both:\n".
"All from repo: %s\n".
"Commit(s) to reparse: %s",
$all_from_repo,
$commits));
}
if (!$reparse_message && !$reparse_change && !$reparse_herald &&
!$reparse_owners) {
throw new PhutilArgumentUsageException(
- pht('Specify what information to reparse with --message, --change, '.
- '--herald, and/or --owners'));
+ pht(
+ 'Specify what information to reparse with %s, %s, %s, and/or %s.',
+ '--message',
+ '--change',
+ '--herald',
+ '--owners'));
}
$min_timestamp = false;
if ($min_date) {
$min_timestamp = strtotime($min_date);
if (!$all_from_repo) {
throw new PhutilArgumentUsageException(
pht(
"You must use --all if you specify --min-date\n".
"e.g.\n".
" repository reparse --all TEST --owners --min-date yesterday"));
}
// previous to PHP 5.1.0 you would compare with -1, instead of false
if (false === $min_timestamp) {
throw new PhutilArgumentUsageException(
pht(
"Supplied --min-date is not valid. See help for valid examples.\n".
"Supplied value: '%s'\n",
$min_date));
}
}
if ($reparse_owners && !$force) {
- $console->writeOut("%s\n", pht(
- 'You are about to recreate the relationship entries between the '.
- 'commits and the packages they touch. This might delete some existing '.
- 'relationship entries for some old commits.'));
+ $console->writeOut(
+ "%s\n",
+ pht(
+ 'You are about to recreate the relationship entries between the '.
+ 'commits and the packages they touch. This might delete some '.
+ 'existing relationship entries for some old commits.'));
- if (!phutil_console_confirm('Are you ready to continue?')) {
+ if (!phutil_console_confirm(pht('Are you ready to continue?'))) {
throw new PhutilArgumentUsageException(pht('Cancelled.'));
}
}
$commits = array();
if ($all_from_repo) {
$repository = id(new PhabricatorRepository())->loadOneWhere(
'callsign = %s OR phid = %s',
$all_from_repo,
$all_from_repo);
if (!$repository) {
throw new PhutilArgumentUsageException(
pht('Unknown repository %s!', $all_from_repo));
}
$constraint = '';
if ($min_timestamp) {
$console->writeOut("%s\n", pht(
'Excluding entries before UNIX timestamp: %s',
$min_timestamp));
$table = new PhabricatorRepositoryCommit();
$conn_r = $table->establishConnection('r');
$constraint = qsprintf(
$conn_r,
'AND epoch >= %d',
$min_timestamp);
}
$commits = id(new PhabricatorRepositoryCommit())->loadAllWhere(
'repositoryID = %d %Q',
$repository->getID(),
$constraint);
$callsign = $repository->getCallsign();
if (!$commits) {
throw new PhutilArgumentUsageException(pht(
"No commits have been discovered in %s repository!\n",
$callsign));
}
} else {
$commits = array();
foreach ($reparse_what as $identifier) {
$matches = null;
if (!preg_match('/r([A-Z]+)([a-z0-9]+)/', $identifier, $matches)) {
throw new PhutilArgumentUsageException(pht(
"Can't parse commit identifier: %s",
$identifier));
}
$callsign = $matches[1];
$commit_identifier = $matches[2];
$repository = id(new PhabricatorRepository())->loadOneWhere(
'callsign = %s',
$callsign);
if (!$repository) {
throw new PhutilArgumentUsageException(pht(
"No repository with callsign '%s'!",
$callsign));
}
$commit = id(new PhabricatorRepositoryCommit())->loadOneWhere(
'repositoryID = %d AND commitIdentifier = %s',
$repository->getID(),
$commit_identifier);
if (!$commit) {
throw new PhutilArgumentUsageException(pht(
"No matching commit '%s' in repository '%s'. ".
"(For git and mercurial repositories, you must specify the entire ".
"commit hash.)",
$commit_identifier,
$callsign));
}
$commits[] = $commit;
}
}
if ($all_from_repo && !$force_local) {
$console->writeOut("%s\n", pht(
'**NOTE**: This script will queue tasks to reparse the data. Once the '.
'tasks have been queued, you need to run Taskmaster daemons to '.
'execute them.'."\n\n".
"QUEUEING TASKS (%d Commits):",
number_format(count($commits))));
}
$progress = new PhutilConsoleProgressBar();
$progress->setTotal(count($commits));
$tasks = array();
foreach ($commits as $commit) {
$classes = array();
switch ($repository->getVersionControlSystem()) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
if ($reparse_message) {
$classes[] = 'PhabricatorRepositoryGitCommitMessageParserWorker';
}
if ($reparse_change) {
$classes[] = 'PhabricatorRepositoryGitCommitChangeParserWorker';
}
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
if ($reparse_message) {
$classes[] =
'PhabricatorRepositoryMercurialCommitMessageParserWorker';
}
if ($reparse_change) {
$classes[] = 'PhabricatorRepositoryMercurialCommitChangeParserWorker';
}
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
if ($reparse_message) {
$classes[] = 'PhabricatorRepositorySvnCommitMessageParserWorker';
}
if ($reparse_change) {
$classes[] = 'PhabricatorRepositorySvnCommitChangeParserWorker';
}
break;
}
if ($reparse_herald) {
$classes[] = 'PhabricatorRepositoryCommitHeraldWorker';
}
if ($reparse_owners) {
$classes[] = 'PhabricatorRepositoryCommitOwnersWorker';
}
$spec = array(
'commitID' => $commit->getID(),
'only' => true,
'forceAutoclose' => $args->getArg('force-autoclose'),
);
if ($all_from_repo && !$force_local) {
foreach ($classes as $class) {
PhabricatorWorker::scheduleTask(
$class,
$spec,
array(
'priority' => PhabricatorWorker::PRIORITY_IMPORT,
));
}
} else {
foreach ($classes as $class) {
$worker = newv($class, array($spec));
$worker->executeTask();
}
}
$progress->update(1);
}
$progress->done();
return 0;
}
}
diff --git a/src/applications/repository/management/PhabricatorRepositoryManagementUpdateWorkflow.php b/src/applications/repository/management/PhabricatorRepositoryManagementUpdateWorkflow.php
index afe8b9e5d..99b4a3adc 100644
--- a/src/applications/repository/management/PhabricatorRepositoryManagementUpdateWorkflow.php
+++ b/src/applications/repository/management/PhabricatorRepositoryManagementUpdateWorkflow.php
@@ -1,200 +1,200 @@
<?php
final class PhabricatorRepositoryManagementUpdateWorkflow
extends PhabricatorRepositoryManagementWorkflow {
private $verbose;
public function setVerbose($verbose) {
$this->verbose = $verbose;
return $this;
}
public function getVerbose() {
return $this->verbose;
}
protected function didConstruct() {
$this
->setName('update')
->setExamples('**update** [options] __repository__')
->setSynopsis(
pht(
'Update __repository__, named by callsign. '.
'This performs the __pull__, __discover__, __ref__ and __mirror__ '.
'operations and is primarily an internal workflow.'))
->setArguments(
array(
array(
'name' => 'verbose',
- 'help' => 'Show additional debugging information.',
+ 'help' => pht('Show additional debugging information.'),
),
array(
'name' => 'no-discovery',
- 'help' => 'Do not perform discovery.',
+ 'help' => pht('Do not perform discovery.'),
),
array(
'name' => 'repos',
'wildcard' => true,
),
));
}
public function execute(PhutilArgumentParser $args) {
$this->setVerbose($args->getArg('verbose'));
$console = PhutilConsole::getConsole();
$repos = $this->loadRepositories($args, 'repos');
if (count($repos) !== 1) {
throw new PhutilArgumentUsageException(
pht('Specify exactly one repository to update, by callsign.'));
}
$repository = head($repos);
try {
$lock_name = 'repository.update:'.$repository->getID();
$lock = PhabricatorGlobalLock::newLock($lock_name);
try {
$lock->lock();
} catch (PhutilLockException $ex) {
throw new PhutilProxyException(
pht(
'Another process is currently holding the update lock for '.
'repository "%s". Repositories may only be updated by one '.
'process at a time. This can happen if you are running multiple '.
'copies of the daemons. This can also happen if you manually '.
'update a repository while the daemons are also updating it '.
'(in this case, just try again in a few moments).',
$repository->getMonogram()),
$ex);
}
try {
$no_discovery = $args->getArg('no-discovery');
id(new PhabricatorRepositoryPullEngine())
->setRepository($repository)
->setVerbose($this->getVerbose())
->pullRepository();
if ($no_discovery) {
$lock->unlock();
return;
}
// TODO: It would be nice to discover only if we pulled something, but
// this isn't totally trivial. It's slightly more complicated with
// hosted repositories, too.
$repository->writeStatusMessage(
PhabricatorRepositoryStatusMessage::TYPE_NEEDS_UPDATE,
null);
$this->discoverRepository($repository);
$this->checkIfRepositoryIsFullyImported($repository);
$this->updateRepositoryRefs($repository);
$this->mirrorRepository($repository);
$repository->writeStatusMessage(
PhabricatorRepositoryStatusMessage::TYPE_FETCH,
PhabricatorRepositoryStatusMessage::CODE_OKAY);
} catch (Exception $ex) {
$lock->unlock();
throw $ex;
}
} catch (Exception $ex) {
$repository->writeStatusMessage(
PhabricatorRepositoryStatusMessage::TYPE_FETCH,
PhabricatorRepositoryStatusMessage::CODE_ERROR,
array(
'message' => pht(
'Error updating working copy: %s', $ex->getMessage()),
));
throw $ex;
}
$lock->unlock();
$console->writeOut(
pht(
'Updated repository **%s**.',
$repository->getMonogram())."\n");
return 0;
}
private function discoverRepository(PhabricatorRepository $repository) {
$refs = id(new PhabricatorRepositoryDiscoveryEngine())
->setRepository($repository)
->setVerbose($this->getVerbose())
->discoverCommits();
return (bool)count($refs);
}
private function mirrorRepository(PhabricatorRepository $repository) {
try {
id(new PhabricatorRepositoryMirrorEngine())
->setRepository($repository)
->pushToMirrors();
} catch (Exception $ex) {
// TODO: We should report these into the UI properly, but for now just
// complain. These errors are much less severe than pull errors.
$proxy = new PhutilProxyException(
pht(
'Error while pushing "%s" repository to mirrors.',
$repository->getMonogram()),
$ex);
phlog($proxy);
}
}
private function updateRepositoryRefs(PhabricatorRepository $repository) {
id(new PhabricatorRepositoryRefEngine())
->setRepository($repository)
->updateRefs();
}
private function checkIfRepositoryIsFullyImported(
PhabricatorRepository $repository) {
// Check if the repository has the "Importing" flag set. We want to clear
// the flag if we can.
$importing = $repository->getDetail('importing');
if (!$importing) {
// This repository isn't marked as "Importing", so we're done.
return;
}
// Look for any commit which hasn't imported.
$unparsed_commit = queryfx_one(
$repository->establishConnection('r'),
'SELECT * FROM %T WHERE repositoryID = %d AND (importStatus & %d) != %d
LIMIT 1',
id(new PhabricatorRepositoryCommit())->getTableName(),
$repository->getID(),
PhabricatorRepositoryCommit::IMPORTED_ALL,
PhabricatorRepositoryCommit::IMPORTED_ALL);
if ($unparsed_commit) {
// We found a commit which still needs to import, so we can't clear the
// flag.
return;
}
// Clear the "importing" flag.
$repository->openTransaction();
$repository->beginReadLocking();
$repository = $repository->reload();
$repository->setDetail('importing', false);
$repository->save();
$repository->endReadLocking();
$repository->saveTransaction();
}
}
diff --git a/src/applications/repository/management/PhabricatorRepositoryManagementWorkflow.php b/src/applications/repository/management/PhabricatorRepositoryManagementWorkflow.php
index 504772dcb..26ab7c315 100644
--- a/src/applications/repository/management/PhabricatorRepositoryManagementWorkflow.php
+++ b/src/applications/repository/management/PhabricatorRepositoryManagementWorkflow.php
@@ -1,62 +1,62 @@
<?php
abstract class PhabricatorRepositoryManagementWorkflow
extends PhabricatorManagementWorkflow {
protected function loadRepositories(PhutilArgumentParser $args, $param) {
$callsigns = $args->getArg($param);
if (!$callsigns) {
return null;
}
$repos = id(new PhabricatorRepositoryQuery())
->setViewer($this->getViewer())
->withCallsigns($callsigns)
->execute();
$repos = mpull($repos, null, 'getCallsign');
foreach ($callsigns as $callsign) {
if (empty($repos[$callsign])) {
throw new PhutilArgumentUsageException(
- "No repository with callsign '{$callsign}' exists!");
+ pht("No repository with callsign '%s' exists!", $callsign));
}
}
return $repos;
}
protected function loadCommits(PhutilArgumentParser $args, $param) {
$names = $args->getArg($param);
if (!$names) {
return null;
}
return $this->loadNamedCommits($names);
}
protected function loadNamedCommit($name) {
$map = $this->loadNamedCommits(array($name));
return $map[$name];
}
protected function loadNamedCommits(array $names) {
$query = id(new DiffusionCommitQuery())
->setViewer($this->getViewer())
->withIdentifiers($names);
$query->execute();
$map = $query->getIdentifierMap();
foreach ($names as $name) {
if (empty($map[$name])) {
throw new PhutilArgumentUsageException(
pht('Commit "%s" does not exist or is ambiguous.', $name));
}
}
return $map;
}
}
diff --git a/src/applications/repository/query/PhabricatorRepositoryQuery.php b/src/applications/repository/query/PhabricatorRepositoryQuery.php
index eea20780b..9a056b202 100644
--- a/src/applications/repository/query/PhabricatorRepositoryQuery.php
+++ b/src/applications/repository/query/PhabricatorRepositoryQuery.php
@@ -1,552 +1,551 @@
<?php
final class PhabricatorRepositoryQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $ids;
private $phids;
private $callsigns;
private $types;
private $uuids;
private $nameContains;
private $remoteURIs;
private $datasourceQuery;
private $numericIdentifiers;
private $callsignIdentifiers;
private $phidIdentifiers;
private $identifierMap;
const STATUS_OPEN = 'status-open';
const STATUS_CLOSED = 'status-closed';
const STATUS_ALL = 'status-all';
private $status = self::STATUS_ALL;
const HOSTED_PHABRICATOR = 'hosted-phab';
const HOSTED_REMOTE = 'hosted-remote';
const HOSTED_ALL = 'hosted-all';
private $hosted = self::HOSTED_ALL;
private $needMostRecentCommits;
private $needCommitCounts;
private $needProjectPHIDs;
public function withIDs(array $ids) {
$this->ids = $ids;
return $this;
}
public function withPHIDs(array $phids) {
$this->phids = $phids;
return $this;
}
public function withCallsigns(array $callsigns) {
$this->callsigns = $callsigns;
return $this;
}
public function withIdentifiers(array $identifiers) {
$ids = array(); $callsigns = array(); $phids = array();
foreach ($identifiers as $identifier) {
if (ctype_digit($identifier)) {
$ids[$identifier] = $identifier;
} else {
$repository_type = PhabricatorRepositoryRepositoryPHIDType::TYPECONST;
if (phid_get_type($identifier) === $repository_type) {
$phids[$identifier] = $identifier;
} else {
$callsigns[$identifier] = $identifier;
}
}
}
$this->numericIdentifiers = $ids;
$this->callsignIdentifiers = $callsigns;
$this->phidIdentifiers = $phids;
return $this;
}
public function withStatus($status) {
$this->status = $status;
return $this;
}
public function withHosted($hosted) {
$this->hosted = $hosted;
return $this;
}
public function withTypes(array $types) {
$this->types = $types;
return $this;
}
public function withUUIDs(array $uuids) {
$this->uuids = $uuids;
return $this;
}
public function withNameContains($contains) {
$this->nameContains = $contains;
return $this;
}
public function withRemoteURIs(array $uris) {
$this->remoteURIs = $uris;
return $this;
}
public function withDatasourceQuery($query) {
$this->datasourceQuery = $query;
return $this;
}
public function needCommitCounts($need_counts) {
$this->needCommitCounts = $need_counts;
return $this;
}
public function needMostRecentCommits($need_commits) {
$this->needMostRecentCommits = $need_commits;
return $this;
}
public function needProjectPHIDs($need_phids) {
$this->needProjectPHIDs = $need_phids;
return $this;
}
public function getBuiltinOrders() {
return array(
'committed' => array(
'vector' => array('committed', 'id'),
'name' => pht('Most Recent Commit'),
),
'name' => array(
'vector' => array('name', 'id'),
'name' => pht('Name'),
),
'callsign' => array(
'vector' => array('callsign'),
'name' => pht('Callsign'),
),
'size' => array(
'vector' => array('size', 'id'),
'name' => pht('Size'),
),
) + parent::getBuiltinOrders();
}
public function getIdentifierMap() {
if ($this->identifierMap === null) {
- throw new Exception(
- 'You must execute() the query before accessing the identifier map.');
+ throw new PhutilInvalidStateException('execute');
}
return $this->identifierMap;
}
protected function willExecute() {
$this->identifierMap = array();
}
protected function loadPage() {
$table = new PhabricatorRepository();
$conn_r = $table->establishConnection('r');
$data = queryfx_all(
$conn_r,
'%Q FROM %T r %Q %Q %Q %Q %Q %Q',
$this->buildSelectClause($conn_r),
$table->getTableName(),
$this->buildJoinClause($conn_r),
$this->buildWhereClause($conn_r),
$this->buildGroupClause($conn_r),
$this->buildHavingClause($conn_r),
$this->buildOrderClause($conn_r),
$this->buildLimitClause($conn_r));
$repositories = $table->loadAllFromArray($data);
if ($this->needCommitCounts) {
$sizes = ipull($data, 'size', 'id');
foreach ($repositories as $id => $repository) {
$repository->attachCommitCount(nonempty($sizes[$id], 0));
}
}
if ($this->needMostRecentCommits) {
$commit_ids = ipull($data, 'lastCommitID', 'id');
$commit_ids = array_filter($commit_ids);
if ($commit_ids) {
$commits = id(new DiffusionCommitQuery())
->setViewer($this->getViewer())
->withIDs($commit_ids)
->execute();
} else {
$commits = array();
}
foreach ($repositories as $id => $repository) {
$commit = null;
if (idx($commit_ids, $id)) {
$commit = idx($commits, $commit_ids[$id]);
}
$repository->attachMostRecentCommit($commit);
}
}
return $repositories;
}
protected function willFilterPage(array $repositories) {
assert_instances_of($repositories, 'PhabricatorRepository');
// TODO: Denormalize repository status into the PhabricatorRepository
// table so we can do this filtering in the database.
foreach ($repositories as $key => $repo) {
$status = $this->status;
switch ($status) {
case self::STATUS_OPEN:
if (!$repo->isTracked()) {
unset($repositories[$key]);
}
break;
case self::STATUS_CLOSED:
if ($repo->isTracked()) {
unset($repositories[$key]);
}
break;
case self::STATUS_ALL:
break;
default:
throw new Exception("Unknown status '{$status}'!");
}
// TODO: This should also be denormalized.
$hosted = $this->hosted;
switch ($hosted) {
case self::HOSTED_PHABRICATOR:
if (!$repo->isHosted()) {
unset($repositories[$key]);
}
break;
case self::HOSTED_REMOTE:
if ($repo->isHosted()) {
unset($repositories[$key]);
}
break;
case self::HOSTED_ALL:
break;
default:
- throw new Exception("Uknown hosted failed '${hosted}'!");
+ throw new Exception(pht("Unknown hosted failed '%s'!", $hosted));
}
}
// TODO: Denormalize this, too.
if ($this->remoteURIs) {
$try_uris = $this->getNormalizedPaths();
$try_uris = array_fuse($try_uris);
foreach ($repositories as $key => $repository) {
if (!isset($try_uris[$repository->getNormalizedPath()])) {
unset($repositories[$key]);
}
}
}
// Build the identifierMap
if ($this->numericIdentifiers) {
foreach ($this->numericIdentifiers as $id) {
if (isset($repositories[$id])) {
$this->identifierMap[$id] = $repositories[$id];
}
}
}
if ($this->callsignIdentifiers) {
$repository_callsigns = mpull($repositories, null, 'getCallsign');
foreach ($this->callsignIdentifiers as $callsign) {
if (isset($repository_callsigns[$callsign])) {
$this->identifierMap[$callsign] = $repository_callsigns[$callsign];
}
}
}
if ($this->phidIdentifiers) {
$repository_phids = mpull($repositories, null, 'getPHID');
foreach ($this->phidIdentifiers as $phid) {
if (isset($repository_phids[$phid])) {
$this->identifierMap[$phid] = $repository_phids[$phid];
}
}
}
return $repositories;
}
protected function didFilterPage(array $repositories) {
if ($this->needProjectPHIDs) {
$type_project = PhabricatorProjectObjectHasProjectEdgeType::EDGECONST;
$edge_query = id(new PhabricatorEdgeQuery())
->withSourcePHIDs(mpull($repositories, 'getPHID'))
->withEdgeTypes(array($type_project));
$edge_query->execute();
foreach ($repositories as $repository) {
$project_phids = $edge_query->getDestinationPHIDs(
array(
$repository->getPHID(),
));
$repository->attachProjectPHIDs($project_phids);
}
}
return $repositories;
}
protected function getPrimaryTableAlias() {
return 'r';
}
public function getOrderableColumns() {
return parent::getOrderableColumns() + array(
'committed' => array(
'table' => 's',
'column' => 'epoch',
'type' => 'int',
'null' => 'tail',
),
'callsign' => array(
'table' => 'r',
'column' => 'callsign',
'type' => 'string',
'unique' => true,
'reverse' => true,
),
'name' => array(
'table' => 'r',
'column' => 'name',
'type' => 'string',
'reverse' => true,
),
'size' => array(
'table' => 's',
'column' => 'size',
'type' => 'int',
'null' => 'tail',
),
);
}
protected function willExecuteCursorQuery(
PhabricatorCursorPagedPolicyAwareQuery $query) {
$vector = $this->getOrderVector();
if ($vector->containsKey('committed')) {
$query->needMostRecentCommits(true);
}
if ($vector->containsKey('size')) {
$query->needCommitCounts(true);
}
}
protected function getPagingValueMap($cursor, array $keys) {
$repository = $this->loadCursorObject($cursor);
$map = array(
'id' => $repository->getID(),
'callsign' => $repository->getCallsign(),
'name' => $repository->getName(),
);
foreach ($keys as $key) {
switch ($key) {
case 'committed':
$commit = $repository->getMostRecentCommit();
if ($commit) {
$map[$key] = $commit->getEpoch();
} else {
$map[$key] = null;
}
break;
case 'size':
$count = $repository->getCommitCount();
if ($count) {
$map[$key] = $count;
} else {
$map[$key] = null;
}
break;
}
}
return $map;
}
protected function buildSelectClause(AphrontDatabaseConnection $conn) {
$parts = $this->buildSelectClauseParts($conn);
if ($this->shouldJoinSummaryTable()) {
$parts[] = 's.*';
}
return $this->formatSelectClause($parts);
}
protected function buildJoinClause(AphrontDatabaseConnection $conn_r) {
$joins = $this->buildJoinClauseParts($conn_r);
if ($this->shouldJoinSummaryTable()) {
$joins[] = qsprintf(
$conn_r,
'LEFT JOIN %T s ON r.id = s.repositoryID',
PhabricatorRepository::TABLE_SUMMARY);
}
return $this->formatJoinClause($joins);
}
private function shouldJoinSummaryTable() {
if ($this->needCommitCounts) {
return true;
}
if ($this->needMostRecentCommits) {
return true;
}
$vector = $this->getOrderVector();
if ($vector->containsKey('committed')) {
return true;
}
if ($vector->containsKey('size')) {
return true;
}
return false;
}
protected function buildWhereClauseParts(AphrontDatabaseConnection $conn_r) {
$where = parent::buildWhereClauseParts($conn_r);
if ($this->ids) {
$where[] = qsprintf(
$conn_r,
'r.id IN (%Ld)',
$this->ids);
}
if ($this->phids) {
$where[] = qsprintf(
$conn_r,
'r.phid IN (%Ls)',
$this->phids);
}
if ($this->callsigns) {
$where[] = qsprintf(
$conn_r,
'r.callsign IN (%Ls)',
$this->callsigns);
}
if ($this->numericIdentifiers ||
$this->callsignIdentifiers ||
$this->phidIdentifiers) {
$identifier_clause = array();
if ($this->numericIdentifiers) {
$identifier_clause[] = qsprintf(
$conn_r,
'r.id IN (%Ld)',
$this->numericIdentifiers);
}
if ($this->callsignIdentifiers) {
$identifier_clause[] = qsprintf(
$conn_r,
'r.callsign IN (%Ls)',
$this->callsignIdentifiers);
}
if ($this->phidIdentifiers) {
$identifier_clause[] = qsprintf(
$conn_r,
'r.phid IN (%Ls)',
$this->phidIdentifiers);
}
$where = array('('.implode(' OR ', $identifier_clause).')');
}
if ($this->types) {
$where[] = qsprintf(
$conn_r,
'r.versionControlSystem IN (%Ls)',
$this->types);
}
if ($this->uuids) {
$where[] = qsprintf(
$conn_r,
'r.uuid IN (%Ls)',
$this->uuids);
}
if (strlen($this->nameContains)) {
$where[] = qsprintf(
$conn_r,
'name LIKE %~',
$this->nameContains);
}
if (strlen($this->datasourceQuery)) {
// This handles having "rP" match callsigns starting with "P...".
$query = trim($this->datasourceQuery);
if (preg_match('/^r/', $query)) {
$callsign = substr($query, 1);
} else {
$callsign = $query;
}
$where[] = qsprintf(
$conn_r,
'r.name LIKE %> OR r.callsign LIKE %>',
$query,
$callsign);
}
return $where;
}
public function getQueryApplicationClass() {
return 'PhabricatorDiffusionApplication';
}
private function getNormalizedPaths() {
$normalized_uris = array();
// Since we don't know which type of repository this URI is in the general
// case, just generate all the normalizations. We could refine this in some
// cases: if the query specifies VCS types, or the URI is a git-style URI
// or an `svn+ssh` URI, we could deduce how to normalize it. However, this
// would be more complicated and it's not clear if it matters in practice.
foreach ($this->remoteURIs as $uri) {
$normalized_uris[] = new PhabricatorRepositoryURINormalizer(
PhabricatorRepositoryURINormalizer::TYPE_GIT,
$uri);
$normalized_uris[] = new PhabricatorRepositoryURINormalizer(
PhabricatorRepositoryURINormalizer::TYPE_SVN,
$uri);
$normalized_uris[] = new PhabricatorRepositoryURINormalizer(
PhabricatorRepositoryURINormalizer::TYPE_MERCURIAL,
$uri);
}
return array_unique(mpull($normalized_uris, 'getNormalizedPath'));
}
}
diff --git a/src/applications/repository/search/PhabricatorRepositoryCommitSearchIndexer.php b/src/applications/repository/search/PhabricatorRepositoryCommitSearchIndexer.php
index 1c17e39af..9565bf763 100644
--- a/src/applications/repository/search/PhabricatorRepositoryCommitSearchIndexer.php
+++ b/src/applications/repository/search/PhabricatorRepositoryCommitSearchIndexer.php
@@ -1,63 +1,63 @@
<?php
final class PhabricatorRepositoryCommitSearchIndexer
extends PhabricatorSearchDocumentIndexer {
public function getIndexableObject() {
return new PhabricatorRepositoryCommit();
}
protected function buildAbstractDocumentByPHID($phid) {
$commit = $this->loadDocumentByPHID($phid);
$commit_data = id(new PhabricatorRepositoryCommitData())->loadOneWhere(
'commitID = %d',
$commit->getID());
$date_created = $commit->getEpoch();
$commit_message = $commit_data->getCommitMessage();
$author_phid = $commit_data->getCommitDetail('authorPHID');
$repository = id(new PhabricatorRepositoryQuery())
->setViewer($this->getViewer())
->withIDs(array($commit->getRepositoryID()))
->executeOne();
if (!$repository) {
- throw new Exception('No such repository!');
+ throw new Exception(pht('No such repository!'));
}
$title = 'r'.$repository->getCallsign().$commit->getCommitIdentifier().
' '.$commit_data->getSummary();
$doc = new PhabricatorSearchAbstractDocument();
$doc->setPHID($commit->getPHID());
$doc->setDocumentType(PhabricatorRepositoryCommitPHIDType::TYPECONST);
$doc->setDocumentCreated($date_created);
$doc->setDocumentModified($date_created);
$doc->setDocumentTitle($title);
$doc->addField(
PhabricatorSearchField::FIELD_BODY,
$commit_message);
if ($author_phid) {
$doc->addRelationship(
PhabricatorSearchRelationship::RELATIONSHIP_AUTHOR,
$author_phid,
PhabricatorPeopleUserPHIDType::TYPECONST,
$date_created);
}
$doc->addRelationship(
PhabricatorSearchRelationship::RELATIONSHIP_REPOSITORY,
$repository->getPHID(),
PhabricatorRepositoryRepositoryPHIDType::TYPECONST,
$date_created);
$this->indexTransactions(
$doc,
new PhabricatorAuditTransactionQuery(),
array($commit->getPHID()));
return $doc;
}
}
diff --git a/src/applications/repository/storage/PhabricatorRepository.php b/src/applications/repository/storage/PhabricatorRepository.php
index 70387edec..547fdb484 100644
--- a/src/applications/repository/storage/PhabricatorRepository.php
+++ b/src/applications/repository/storage/PhabricatorRepository.php
@@ -1,1891 +1,1898 @@
<?php
/**
* @task uri Repository URI Management
* @task autoclose Autoclose
*/
final class PhabricatorRepository extends PhabricatorRepositoryDAO
implements
PhabricatorApplicationTransactionInterface,
PhabricatorPolicyInterface,
PhabricatorFlaggableInterface,
PhabricatorMarkupInterface,
PhabricatorDestructibleInterface,
PhabricatorProjectInterface {
/**
* Shortest hash we'll recognize in raw "a829f32" form.
*/
const MINIMUM_UNQUALIFIED_HASH = 7;
/**
* Shortest hash we'll recognize in qualified "rXab7ef2f8" form.
*/
const MINIMUM_QUALIFIED_HASH = 5;
const TABLE_PATH = 'repository_path';
const TABLE_PATHCHANGE = 'repository_pathchange';
const TABLE_FILESYSTEM = 'repository_filesystem';
const TABLE_SUMMARY = 'repository_summary';
const TABLE_BADCOMMIT = 'repository_badcommit';
const TABLE_LINTMESSAGE = 'repository_lintmessage';
const TABLE_PARENTS = 'repository_parents';
const TABLE_COVERAGE = 'repository_coverage';
const SERVE_OFF = 'off';
const SERVE_READONLY = 'readonly';
const SERVE_READWRITE = 'readwrite';
const BECAUSE_REPOSITORY_IMPORTING = 'auto/importing';
const BECAUSE_AUTOCLOSE_DISABLED = 'auto/disabled';
const BECAUSE_NOT_ON_AUTOCLOSE_BRANCH = 'auto/nobranch';
const BECAUSE_BRANCH_UNTRACKED = 'auto/notrack';
const BECAUSE_BRANCH_NOT_AUTOCLOSE = 'auto/noclose';
const BECAUSE_AUTOCLOSE_FORCED = 'auto/forced';
protected $name;
protected $callsign;
protected $uuid;
protected $viewPolicy;
protected $editPolicy;
protected $pushPolicy;
protected $versionControlSystem;
protected $details = array();
protected $credentialPHID;
protected $almanacServicePHID;
private $commitCount = self::ATTACHABLE;
private $mostRecentCommit = self::ATTACHABLE;
private $projectPHIDs = self::ATTACHABLE;
public static function initializeNewRepository(PhabricatorUser $actor) {
$app = id(new PhabricatorApplicationQuery())
->setViewer($actor)
->withClasses(array('PhabricatorDiffusionApplication'))
->executeOne();
$view_policy = $app->getPolicy(DiffusionDefaultViewCapability::CAPABILITY);
$edit_policy = $app->getPolicy(DiffusionDefaultEditCapability::CAPABILITY);
$push_policy = $app->getPolicy(DiffusionDefaultPushCapability::CAPABILITY);
$repository = id(new PhabricatorRepository())
->setViewPolicy($view_policy)
->setEditPolicy($edit_policy)
->setPushPolicy($push_policy);
// Put the repository in "Importing" mode until we finish
// parsing it.
$repository->setDetail('importing', true);
return $repository;
}
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'details' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'name' => 'sort255',
'callsign' => 'sort32',
'versionControlSystem' => 'text32',
'uuid' => 'text64?',
'pushPolicy' => 'policy',
'credentialPHID' => 'phid?',
'almanacServicePHID' => 'phid?',
),
self::CONFIG_KEY_SCHEMA => array(
'key_phid' => null,
'phid' => array(
'columns' => array('phid'),
'unique' => true,
),
'callsign' => array(
'columns' => array('callsign'),
'unique' => true,
),
'key_name' => array(
'columns' => array('name(128)'),
),
'key_vcs' => array(
'columns' => array('versionControlSystem'),
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhabricatorRepositoryRepositoryPHIDType::TYPECONST);
}
public function toDictionary() {
return array(
'id' => $this->getID(),
'name' => $this->getName(),
'phid' => $this->getPHID(),
'callsign' => $this->getCallsign(),
'monogram' => $this->getMonogram(),
'vcs' => $this->getVersionControlSystem(),
'uri' => PhabricatorEnv::getProductionURI($this->getURI()),
'remoteURI' => (string)$this->getRemoteURI(),
'description' => $this->getDetail('description'),
'isActive' => $this->isTracked(),
'isHosted' => $this->isHosted(),
'isImporting' => $this->isImporting(),
);
}
public function getMonogram() {
return 'r'.$this->getCallsign();
}
public function getDetail($key, $default = null) {
return idx($this->details, $key, $default);
}
public function getHumanReadableDetail($key, $default = null) {
$value = $this->getDetail($key, $default);
switch ($key) {
case 'branch-filter':
case 'close-commits-filter':
$value = array_keys($value);
$value = implode(', ', $value);
break;
}
return $value;
}
public function setDetail($key, $value) {
$this->details[$key] = $value;
return $this;
}
public function attachCommitCount($count) {
$this->commitCount = $count;
return $this;
}
public function getCommitCount() {
return $this->assertAttached($this->commitCount);
}
public function attachMostRecentCommit(
PhabricatorRepositoryCommit $commit = null) {
$this->mostRecentCommit = $commit;
return $this;
}
public function getMostRecentCommit() {
return $this->assertAttached($this->mostRecentCommit);
}
public function getDiffusionBrowseURIForPath(
PhabricatorUser $user,
$path,
$line = null,
$branch = null) {
$drequest = DiffusionRequest::newFromDictionary(
array(
'user' => $user,
'repository' => $this,
'path' => $path,
'branch' => $branch,
));
return $drequest->generateURI(
array(
'action' => 'browse',
'line' => $line,
));
}
public function getLocalPath() {
return $this->getDetail('local-path');
}
public function getSubversionBaseURI($commit = null) {
$subpath = $this->getDetail('svn-subpath');
if (!strlen($subpath)) {
$subpath = null;
}
return $this->getSubversionPathURI($subpath, $commit);
}
public function getSubversionPathURI($path = null, $commit = null) {
$vcs = $this->getVersionControlSystem();
if ($vcs != PhabricatorRepositoryType::REPOSITORY_TYPE_SVN) {
- throw new Exception('Not a subversion repository!');
+ throw new Exception(pht('Not a subversion repository!'));
}
if ($this->isHosted()) {
$uri = 'file://'.$this->getLocalPath();
} else {
$uri = $this->getDetail('remote-uri');
}
$uri = rtrim($uri, '/');
if (strlen($path)) {
$path = rawurlencode($path);
$path = str_replace('%2F', '/', $path);
$uri = $uri.'/'.ltrim($path, '/');
}
if ($path !== null || $commit !== null) {
$uri .= '@';
}
if ($commit !== null) {
$uri .= $commit;
}
return $uri;
}
public function attachProjectPHIDs(array $project_phids) {
$this->projectPHIDs = $project_phids;
return $this;
}
public function getProjectPHIDs() {
return $this->assertAttached($this->projectPHIDs);
}
/**
* Get the name of the directory this repository should clone or checkout
* into. For example, if the repository name is "Example Repository", a
* reasonable name might be "example-repository". This is used to help users
* get reasonable results when cloning repositories, since they generally do
* not want to clone into directories called "X/" or "Example Repository/".
*
* @return string
*/
public function getCloneName() {
$name = $this->getDetail('clone-name');
// Make some reasonable effort to produce reasonable default directory
// names from repository names.
if (!strlen($name)) {
$name = $this->getName();
$name = phutil_utf8_strtolower($name);
$name = preg_replace('@[/ -:]+@', '-', $name);
$name = trim($name, '-');
if (!strlen($name)) {
$name = $this->getCallsign();
}
}
return $name;
}
/* -( Remote Command Execution )------------------------------------------- */
public function execRemoteCommand($pattern /* , $arg, ... */) {
$args = func_get_args();
return $this->newRemoteCommandFuture($args)->resolve();
}
public function execxRemoteCommand($pattern /* , $arg, ... */) {
$args = func_get_args();
return $this->newRemoteCommandFuture($args)->resolvex();
}
public function getRemoteCommandFuture($pattern /* , $arg, ... */) {
$args = func_get_args();
return $this->newRemoteCommandFuture($args);
}
public function passthruRemoteCommand($pattern /* , $arg, ... */) {
$args = func_get_args();
return $this->newRemoteCommandPassthru($args)->execute();
}
private function newRemoteCommandFuture(array $argv) {
$argv = $this->formatRemoteCommand($argv);
$future = newv('ExecFuture', $argv);
$future->setEnv($this->getRemoteCommandEnvironment());
return $future;
}
private function newRemoteCommandPassthru(array $argv) {
$argv = $this->formatRemoteCommand($argv);
$passthru = newv('PhutilExecPassthru', $argv);
$passthru->setEnv($this->getRemoteCommandEnvironment());
return $passthru;
}
/* -( Local Command Execution )-------------------------------------------- */
public function execLocalCommand($pattern /* , $arg, ... */) {
$args = func_get_args();
return $this->newLocalCommandFuture($args)->resolve();
}
public function execxLocalCommand($pattern /* , $arg, ... */) {
$args = func_get_args();
return $this->newLocalCommandFuture($args)->resolvex();
}
public function getLocalCommandFuture($pattern /* , $arg, ... */) {
$args = func_get_args();
return $this->newLocalCommandFuture($args);
}
public function passthruLocalCommand($pattern /* , $arg, ... */) {
$args = func_get_args();
return $this->newLocalCommandPassthru($args)->execute();
}
private function newLocalCommandFuture(array $argv) {
$this->assertLocalExists();
$argv = $this->formatLocalCommand($argv);
$future = newv('ExecFuture', $argv);
$future->setEnv($this->getLocalCommandEnvironment());
if ($this->usesLocalWorkingCopy()) {
$future->setCWD($this->getLocalPath());
}
return $future;
}
private function newLocalCommandPassthru(array $argv) {
$this->assertLocalExists();
$argv = $this->formatLocalCommand($argv);
$future = newv('PhutilExecPassthru', $argv);
$future->setEnv($this->getLocalCommandEnvironment());
if ($this->usesLocalWorkingCopy()) {
$future->setCWD($this->getLocalPath());
}
return $future;
}
/* -( Command Infrastructure )--------------------------------------------- */
private function getSSHWrapper() {
$root = dirname(phutil_get_library_root('phabricator'));
return $root.'/bin/ssh-connect';
}
private function getCommonCommandEnvironment() {
$env = array(
// NOTE: Force the language to "en_US.UTF-8", which overrides locale
// settings. This makes stuff print in English instead of, e.g., French,
// so we can parse the output of some commands, error messages, etc.
'LANG' => 'en_US.UTF-8',
// Propagate PHABRICATOR_ENV explicitly. For discussion, see T4155.
'PHABRICATOR_ENV' => PhabricatorEnv::getSelectedEnvironmentName(),
);
switch ($this->getVersionControlSystem()) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
// NOTE: See T2965. Some time after Git 1.7.5.4, Git started fataling if
// it can not read $HOME. For many users, $HOME points at /root (this
// seems to be a default result of Apache setup). Instead, explicitly
// point $HOME at a readable, empty directory so that Git looks for the
// config file it's after, fails to locate it, and moves on. This is
// really silly, but seems like the least damaging approach to
// mitigating the issue.
$root = dirname(phutil_get_library_root('phabricator'));
$env['HOME'] = $root.'/support/empty/';
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
// NOTE: This overrides certain configuration, extensions, and settings
// which make Mercurial commands do random unusual things.
$env['HGPLAIN'] = 1;
break;
default:
- throw new Exception('Unrecognized version control system.');
+ throw new Exception(pht('Unrecognized version control system.'));
}
return $env;
}
private function getLocalCommandEnvironment() {
return $this->getCommonCommandEnvironment();
}
private function getRemoteCommandEnvironment() {
$env = $this->getCommonCommandEnvironment();
if ($this->shouldUseSSH()) {
// NOTE: This is read by `bin/ssh-connect`, and tells it which credentials
// to use.
$env['PHABRICATOR_CREDENTIAL'] = $this->getCredentialPHID();
switch ($this->getVersionControlSystem()) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
// Force SVN to use `bin/ssh-connect`.
$env['SVN_SSH'] = $this->getSSHWrapper();
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
// Force Git to use `bin/ssh-connect`.
$env['GIT_SSH'] = $this->getSSHWrapper();
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
// We force Mercurial through `bin/ssh-connect` too, but it uses a
// command-line flag instead of an environmental variable.
break;
default:
- throw new Exception('Unrecognized version control system.');
+ throw new Exception(pht('Unrecognized version control system.'));
}
}
return $env;
}
private function formatRemoteCommand(array $args) {
$pattern = $args[0];
$args = array_slice($args, 1);
switch ($this->getVersionControlSystem()) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
if ($this->shouldUseHTTP() || $this->shouldUseSVNProtocol()) {
$flags = array();
$flag_args = array();
$flags[] = '--non-interactive';
$flags[] = '--no-auth-cache';
if ($this->shouldUseHTTP()) {
$flags[] = '--trust-server-cert';
}
$credential_phid = $this->getCredentialPHID();
if ($credential_phid) {
$key = PassphrasePasswordKey::loadFromPHID(
$credential_phid,
PhabricatorUser::getOmnipotentUser());
$flags[] = '--username %P';
$flags[] = '--password %P';
$flag_args[] = $key->getUsernameEnvelope();
$flag_args[] = $key->getPasswordEnvelope();
}
$flags = implode(' ', $flags);
$pattern = "svn {$flags} {$pattern}";
$args = array_mergev(array($flag_args, $args));
} else {
$pattern = "svn --non-interactive {$pattern}";
}
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
$pattern = "git {$pattern}";
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
if ($this->shouldUseSSH()) {
$pattern = "hg --config ui.ssh=%s {$pattern}";
array_unshift(
$args,
$this->getSSHWrapper());
} else {
$pattern = "hg {$pattern}";
}
break;
default:
- throw new Exception('Unrecognized version control system.');
+ throw new Exception(pht('Unrecognized version control system.'));
}
array_unshift($args, $pattern);
return $args;
}
private function formatLocalCommand(array $args) {
$pattern = $args[0];
$args = array_slice($args, 1);
switch ($this->getVersionControlSystem()) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
$pattern = "svn --non-interactive {$pattern}";
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
$pattern = "git {$pattern}";
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
$pattern = "hg {$pattern}";
break;
default:
- throw new Exception('Unrecognized version control system.');
+ throw new Exception(pht('Unrecognized version control system.'));
}
array_unshift($args, $pattern);
return $args;
}
/**
* Sanitize output of an `hg` command invoked with the `--debug` flag to make
* it usable.
*
* @param string Output from `hg --debug ...`
* @return string Usable output.
*/
public static function filterMercurialDebugOutput($stdout) {
// When hg commands are run with `--debug` and some config file isn't
// trusted, Mercurial prints out a warning to stdout, twice, after Feb 2011.
//
// http://selenic.com/pipermail/mercurial-devel/2011-February/028541.html
//
// After Jan 2015, it may also fail to write to a revision branch cache.
$ignore = array(
'ignoring untrusted configuration option',
"couldn't write revision branch cache:",
);
foreach ($ignore as $key => $pattern) {
$ignore[$key] = preg_quote($pattern, '/');
}
$ignore = '('.implode('|', $ignore).')';
$lines = preg_split('/(?<=\n)/', $stdout);
$regex = '/'.$ignore.'.*\n$/';
foreach ($lines as $key => $line) {
$lines[$key] = preg_replace($regex, '', $line);
}
return implode('', $lines);
}
public function getURI() {
return '/diffusion/'.$this->getCallsign().'/';
}
public function getNormalizedPath() {
$uri = (string)$this->getCloneURIObject();
switch ($this->getVersionControlSystem()) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
$normalized_uri = new PhabricatorRepositoryURINormalizer(
PhabricatorRepositoryURINormalizer::TYPE_GIT,
$uri);
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
$normalized_uri = new PhabricatorRepositoryURINormalizer(
PhabricatorRepositoryURINormalizer::TYPE_SVN,
$uri);
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
$normalized_uri = new PhabricatorRepositoryURINormalizer(
PhabricatorRepositoryURINormalizer::TYPE_MERCURIAL,
$uri);
break;
default:
- throw new Exception('Unrecognized version control system.');
+ throw new Exception(pht('Unrecognized version control system.'));
}
return $normalized_uri->getNormalizedPath();
}
public function isTracked() {
return $this->getDetail('tracking-enabled', false);
}
public function getDefaultBranch() {
$default = $this->getDetail('default-branch');
if (strlen($default)) {
return $default;
}
$default_branches = array(
PhabricatorRepositoryType::REPOSITORY_TYPE_GIT => 'master',
PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL => 'default',
);
return idx($default_branches, $this->getVersionControlSystem());
}
public function getDefaultArcanistBranch() {
return coalesce($this->getDefaultBranch(), 'svn');
}
private function isBranchInFilter($branch, $filter_key) {
$vcs = $this->getVersionControlSystem();
$is_git = ($vcs == PhabricatorRepositoryType::REPOSITORY_TYPE_GIT);
$use_filter = ($is_git);
if (!$use_filter) {
// If this VCS doesn't use filters, pass everything through.
return true;
}
$filter = $this->getDetail($filter_key, array());
// If there's no filter set, let everything through.
if (!$filter) {
return true;
}
// If this branch isn't literally named `regexp(...)`, and it's in the
// filter list, let it through.
if (isset($filter[$branch])) {
if (self::extractBranchRegexp($branch) === null) {
return true;
}
}
// If the branch matches a regexp, let it through.
foreach ($filter as $pattern => $ignored) {
$regexp = self::extractBranchRegexp($pattern);
if ($regexp !== null) {
if (preg_match($regexp, $branch)) {
return true;
}
}
}
// Nothing matched, so filter this branch out.
return false;
}
public static function extractBranchRegexp($pattern) {
$matches = null;
if (preg_match('/^regexp\\((.*)\\)\z/', $pattern, $matches)) {
return $matches[1];
}
return null;
}
public function shouldTrackBranch($branch) {
return $this->isBranchInFilter($branch, 'branch-filter');
}
public function formatCommitName($commit_identifier) {
$vcs = $this->getVersionControlSystem();
$type_git = PhabricatorRepositoryType::REPOSITORY_TYPE_GIT;
$type_hg = PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL;
$is_git = ($vcs == $type_git);
$is_hg = ($vcs == $type_hg);
if ($is_git || $is_hg) {
$short_identifier = substr($commit_identifier, 0, 12);
} else {
$short_identifier = $commit_identifier;
}
return 'r'.$this->getCallsign().$short_identifier;
}
public function isImporting() {
return (bool)$this->getDetail('importing', false);
}
/**
* Should this repository publish feed, notifications, audits, and email?
*
* We do not publish information about repositories during initial import,
* or if the repository has been set not to publish.
*/
public function shouldPublish() {
if ($this->isImporting()) {
return false;
}
if ($this->getDetail('disable-herald')) {
return false;
}
return true;
}
/* -( Autoclose )---------------------------------------------------------- */
/**
* Determine if autoclose is active for a branch.
*
* For more details about why, use @{method:shouldSkipAutocloseBranch}.
*
* @param string Branch name to check.
* @return bool True if autoclose is active for the branch.
* @task autoclose
*/
public function shouldAutocloseBranch($branch) {
return ($this->shouldSkipAutocloseBranch($branch) === null);
}
/**
* Determine if autoclose is active for a commit.
*
* For more details about why, use @{method:shouldSkipAutocloseCommit}.
*
* @param PhabricatorRepositoryCommit Commit to check.
* @return bool True if autoclose is active for the commit.
* @task autoclose
*/
public function shouldAutocloseCommit(PhabricatorRepositoryCommit $commit) {
return ($this->shouldSkipAutocloseCommit($commit) === null);
}
/**
* Determine why autoclose should be skipped for a branch.
*
* This method gives a detailed reason why autoclose will be skipped. To
* perform a simple test, use @{method:shouldAutocloseBranch}.
*
* @param string Branch name to check.
* @return const|null Constant identifying reason to skip this branch, or null
* if autoclose is active.
* @task autoclose
*/
public function shouldSkipAutocloseBranch($branch) {
$all_reason = $this->shouldSkipAllAutoclose();
if ($all_reason) {
return $all_reason;
}
if (!$this->shouldTrackBranch($branch)) {
return self::BECAUSE_BRANCH_UNTRACKED;
}
if (!$this->isBranchInFilter($branch, 'close-commits-filter')) {
return self::BECAUSE_BRANCH_NOT_AUTOCLOSE;
}
return null;
}
/**
* Determine why autoclose should be skipped for a commit.
*
* This method gives a detailed reason why autoclose will be skipped. To
* perform a simple test, use @{method:shouldAutocloseCommit}.
*
* @param PhabricatorRepositoryCommit Commit to check.
* @return const|null Constant identifying reason to skip this commit, or null
* if autoclose is active.
* @task autoclose
*/
public function shouldSkipAutocloseCommit(
PhabricatorRepositoryCommit $commit) {
$all_reason = $this->shouldSkipAllAutoclose();
if ($all_reason) {
return $all_reason;
}
switch ($this->getVersionControlSystem()) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
return null;
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
break;
default:
- throw new Exception('Unrecognized version control system.');
+ throw new Exception(pht('Unrecognized version control system.'));
}
$closeable_flag = PhabricatorRepositoryCommit::IMPORTED_CLOSEABLE;
if (!$commit->isPartiallyImported($closeable_flag)) {
return self::BECAUSE_NOT_ON_AUTOCLOSE_BRANCH;
}
return null;
}
/**
* Determine why all autoclose operations should be skipped for this
* repository.
*
* @return const|null Constant identifying reason to skip all autoclose
* operations, or null if autoclose operations are not blocked at the
* repository level.
* @task autoclose
*/
private function shouldSkipAllAutoclose() {
if ($this->isImporting()) {
return self::BECAUSE_REPOSITORY_IMPORTING;
}
if ($this->getDetail('disable-autoclose', false)) {
return self::BECAUSE_AUTOCLOSE_DISABLED;
}
return null;
}
/* -( Repository URI Management )------------------------------------------ */
/**
* Get the remote URI for this repository.
*
* @return string
* @task uri
*/
public function getRemoteURI() {
return (string)$this->getRemoteURIObject();
}
/**
* Get the remote URI for this repository, including credentials if they're
* used by this repository.
*
* @return PhutilOpaqueEnvelope URI, possibly including credentials.
* @task uri
*/
public function getRemoteURIEnvelope() {
$uri = $this->getRemoteURIObject();
$remote_protocol = $this->getRemoteProtocol();
if ($remote_protocol == 'http' || $remote_protocol == 'https') {
// For SVN, we use `--username` and `--password` flags separately, so
// don't add any credentials here.
if (!$this->isSVN()) {
$credential_phid = $this->getCredentialPHID();
if ($credential_phid) {
$key = PassphrasePasswordKey::loadFromPHID(
$credential_phid,
PhabricatorUser::getOmnipotentUser());
$uri->setUser($key->getUsernameEnvelope()->openEnvelope());
$uri->setPass($key->getPasswordEnvelope()->openEnvelope());
}
}
}
return new PhutilOpaqueEnvelope((string)$uri);
}
/**
* Get the clone (or checkout) URI for this repository, without authentication
* information.
*
* @return string Repository URI.
* @task uri
*/
public function getPublicCloneURI() {
$uri = $this->getCloneURIObject();
// Make sure we don't leak anything if this repo is using HTTP Basic Auth
// with the credentials in the URI or something zany like that.
// If repository is not accessed over SSH we remove both username and
// password.
if (!$this->isHosted()) {
if (!$this->shouldUseSSH()) {
$uri->setUser(null);
// This might be a Git URI or a normal URI. If it's Git, there's no
// password support.
if ($uri instanceof PhutilURI) {
$uri->setPass(null);
}
}
}
return (string)$uri;
}
/**
* Get the protocol for the repository's remote.
*
* @return string Protocol, like "ssh" or "git".
* @task uri
*/
public function getRemoteProtocol() {
$uri = $this->getRemoteURIObject();
if ($uri instanceof PhutilGitURI) {
return 'ssh';
} else {
return $uri->getProtocol();
}
}
/**
* Get a parsed object representation of the repository's remote URI. This
* may be a normal URI (returned as a @{class@libphutil:PhutilURI}) or a git
* URI (returned as a @{class@libphutil:PhutilGitURI}).
*
* @return wild A @{class@libphutil:PhutilURI} or
* @{class@libphutil:PhutilGitURI}.
* @task uri
*/
public function getRemoteURIObject() {
$raw_uri = $this->getDetail('remote-uri');
if (!$raw_uri) {
return new PhutilURI('');
}
if (!strncmp($raw_uri, '/', 1)) {
return new PhutilURI('file://'.$raw_uri);
}
$uri = new PhutilURI($raw_uri);
if ($uri->getProtocol()) {
return $uri;
}
$uri = new PhutilGitURI($raw_uri);
if ($uri->getDomain()) {
return $uri;
}
- throw new Exception("Remote URI '{$raw_uri}' could not be parsed!");
+ throw new Exception(pht("Remote URI '%s' could not be parsed!", $raw_uri));
}
/**
* Get the "best" clone/checkout URI for this repository, on any protocol.
*/
public function getCloneURIObject() {
if (!$this->isHosted()) {
if ($this->isSVN()) {
// Make sure we pick up the "Import Only" path for Subversion, so
// the user clones the repository starting at the correct path, not
// from the root.
$base_uri = $this->getSubversionBaseURI();
$base_uri = new PhutilURI($base_uri);
$path = $base_uri->getPath();
if (!$path) {
$path = '/';
}
// If the trailing "@" is not required to escape the URI, strip it for
// readability.
if (!preg_match('/@.*@/', $path)) {
$path = rtrim($path, '@');
}
$base_uri->setPath($path);
return $base_uri;
} else {
return $this->getRemoteURIObject();
}
}
// Choose the best URI: pick a read/write URI over a URI which is not
// read/write, and SSH over HTTP.
$serve_ssh = $this->getServeOverSSH();
$serve_http = $this->getServeOverHTTP();
if ($serve_ssh === self::SERVE_READWRITE) {
return $this->getSSHCloneURIObject();
} else if ($serve_http === self::SERVE_READWRITE) {
return $this->getHTTPCloneURIObject();
} else if ($serve_ssh !== self::SERVE_OFF) {
return $this->getSSHCloneURIObject();
} else if ($serve_http !== self::SERVE_OFF) {
return $this->getHTTPCloneURIObject();
} else {
return null;
}
}
/**
* Get the repository's SSH clone/checkout URI, if one exists.
*/
public function getSSHCloneURIObject() {
if (!$this->isHosted()) {
if ($this->shouldUseSSH()) {
return $this->getRemoteURIObject();
} else {
return null;
}
}
$serve_ssh = $this->getServeOverSSH();
if ($serve_ssh === self::SERVE_OFF) {
return null;
}
$uri = new PhutilURI(PhabricatorEnv::getProductionURI($this->getURI()));
if ($this->isSVN()) {
$uri->setProtocol('svn+ssh');
} else {
$uri->setProtocol('ssh');
}
if ($this->isGit()) {
$uri->setPath($uri->getPath().$this->getCloneName().'.git');
} else if ($this->isHg()) {
$uri->setPath($uri->getPath().$this->getCloneName().'/');
}
$ssh_user = PhabricatorEnv::getEnvConfig('diffusion.ssh-user');
if ($ssh_user) {
$uri->setUser($ssh_user);
}
$ssh_host = PhabricatorEnv::getEnvConfig('diffusion.ssh-host');
if (strlen($ssh_host)) {
$uri->setDomain($ssh_host);
}
$uri->setPort(PhabricatorEnv::getEnvConfig('diffusion.ssh-port'));
return $uri;
}
/**
* Get the repository's HTTP clone/checkout URI, if one exists.
*/
public function getHTTPCloneURIObject() {
if (!$this->isHosted()) {
if ($this->shouldUseHTTP()) {
return $this->getRemoteURIObject();
} else {
return null;
}
}
$serve_http = $this->getServeOverHTTP();
if ($serve_http === self::SERVE_OFF) {
return null;
}
$uri = PhabricatorEnv::getProductionURI($this->getURI());
$uri = new PhutilURI($uri);
if ($this->isGit()) {
$uri->setPath($uri->getPath().$this->getCloneName().'.git');
} else if ($this->isHg()) {
$uri->setPath($uri->getPath().$this->getCloneName().'/');
}
return $uri;
}
/**
* Determine if we should connect to the remote using SSH flags and
* credentials.
*
* @return bool True to use the SSH protocol.
* @task uri
*/
private function shouldUseSSH() {
if ($this->isHosted()) {
return false;
}
$protocol = $this->getRemoteProtocol();
if ($this->isSSHProtocol($protocol)) {
return true;
}
return false;
}
/**
* Determine if we should connect to the remote using HTTP flags and
* credentials.
*
* @return bool True to use the HTTP protocol.
* @task uri
*/
private function shouldUseHTTP() {
if ($this->isHosted()) {
return false;
}
$protocol = $this->getRemoteProtocol();
return ($protocol == 'http' || $protocol == 'https');
}
/**
* Determine if we should connect to the remote using SVN flags and
* credentials.
*
* @return bool True to use the SVN protocol.
* @task uri
*/
private function shouldUseSVNProtocol() {
if ($this->isHosted()) {
return false;
}
$protocol = $this->getRemoteProtocol();
return ($protocol == 'svn');
}
/**
* Determine if a protocol is SSH or SSH-like.
*
* @param string A protocol string, like "http" or "ssh".
* @return bool True if the protocol is SSH-like.
* @task uri
*/
private function isSSHProtocol($protocol) {
return ($protocol == 'ssh' || $protocol == 'svn+ssh');
}
public function delete() {
$this->openTransaction();
$paths = id(new PhabricatorOwnersPath())
->loadAllWhere('repositoryPHID = %s', $this->getPHID());
foreach ($paths as $path) {
$path->delete();
}
$projects = id(new PhabricatorRepositoryArcanistProject())
->loadAllWhere('repositoryID = %d', $this->getID());
foreach ($projects as $project) {
$project->delete();
}
queryfx(
$this->establishConnection('w'),
'DELETE FROM %T WHERE repositoryPHID = %s',
id(new PhabricatorRepositorySymbol())->getTableName(),
$this->getPHID());
$commits = id(new PhabricatorRepositoryCommit())
->loadAllWhere('repositoryID = %d', $this->getID());
foreach ($commits as $commit) {
// note PhabricatorRepositoryAuditRequests and
// PhabricatorRepositoryCommitData are deleted here too.
$commit->delete();
}
$mirrors = id(new PhabricatorRepositoryMirror())
->loadAllWhere('repositoryPHID = %s', $this->getPHID());
foreach ($mirrors as $mirror) {
$mirror->delete();
}
$ref_cursors = id(new PhabricatorRepositoryRefCursor())
->loadAllWhere('repositoryPHID = %s', $this->getPHID());
foreach ($ref_cursors as $cursor) {
$cursor->delete();
}
$conn_w = $this->establishConnection('w');
queryfx(
$conn_w,
'DELETE FROM %T WHERE repositoryID = %d',
self::TABLE_FILESYSTEM,
$this->getID());
queryfx(
$conn_w,
'DELETE FROM %T WHERE repositoryID = %d',
self::TABLE_PATHCHANGE,
$this->getID());
queryfx(
$conn_w,
'DELETE FROM %T WHERE repositoryID = %d',
self::TABLE_SUMMARY,
$this->getID());
$result = parent::delete();
$this->saveTransaction();
return $result;
}
public function isGit() {
$vcs = $this->getVersionControlSystem();
return ($vcs == PhabricatorRepositoryType::REPOSITORY_TYPE_GIT);
}
public function isSVN() {
$vcs = $this->getVersionControlSystem();
return ($vcs == PhabricatorRepositoryType::REPOSITORY_TYPE_SVN);
}
public function isHg() {
$vcs = $this->getVersionControlSystem();
return ($vcs == PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL);
}
public function isHosted() {
return (bool)$this->getDetail('hosting-enabled', false);
}
public function setHosted($enabled) {
return $this->setDetail('hosting-enabled', $enabled);
}
public function getServeOverHTTP() {
if ($this->isSVN()) {
return self::SERVE_OFF;
}
$serve = $this->getDetail('serve-over-http', self::SERVE_OFF);
return $this->normalizeServeConfigSetting($serve);
}
public function setServeOverHTTP($mode) {
return $this->setDetail('serve-over-http', $mode);
}
public function getServeOverSSH() {
$serve = $this->getDetail('serve-over-ssh', self::SERVE_OFF);
return $this->normalizeServeConfigSetting($serve);
}
public function setServeOverSSH($mode) {
return $this->setDetail('serve-over-ssh', $mode);
}
public static function getProtocolAvailabilityName($constant) {
switch ($constant) {
case self::SERVE_OFF:
return pht('Off');
case self::SERVE_READONLY:
return pht('Read Only');
case self::SERVE_READWRITE:
return pht('Read/Write');
default:
return pht('Unknown');
}
}
private function normalizeServeConfigSetting($value) {
switch ($value) {
case self::SERVE_OFF:
case self::SERVE_READONLY:
return $value;
case self::SERVE_READWRITE:
if ($this->isHosted()) {
return self::SERVE_READWRITE;
} else {
return self::SERVE_READONLY;
}
default:
return self::SERVE_OFF;
}
}
/**
* Raise more useful errors when there are basic filesystem problems.
*/
private function assertLocalExists() {
if (!$this->usesLocalWorkingCopy()) {
return;
}
$local = $this->getLocalPath();
Filesystem::assertExists($local);
Filesystem::assertIsDirectory($local);
Filesystem::assertReadable($local);
}
/**
* Determine if the working copy is bare or not. In Git, this corresponds
* to `--bare`. In Mercurial, `--noupdate`.
*/
public function isWorkingCopyBare() {
switch ($this->getVersionControlSystem()) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
return false;
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
$local = $this->getLocalPath();
if (Filesystem::pathExists($local.'/.git')) {
return false;
} else {
return true;
}
}
}
public function usesLocalWorkingCopy() {
switch ($this->getVersionControlSystem()) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
return $this->isHosted();
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
return true;
}
}
public function getHookDirectories() {
$directories = array();
if (!$this->isHosted()) {
return $directories;
}
$root = $this->getLocalPath();
switch ($this->getVersionControlSystem()) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
if ($this->isWorkingCopyBare()) {
$directories[] = $root.'/hooks/pre-receive-phabricator.d/';
} else {
$directories[] = $root.'/.git/hooks/pre-receive-phabricator.d/';
}
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
$directories[] = $root.'/hooks/pre-commit-phabricator.d/';
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
// NOTE: We don't support custom Mercurial hooks for now because they're
// messy and we can't easily just drop a `hooks.d/` directory next to
// the hooks.
break;
}
return $directories;
}
public function canDestroyWorkingCopy() {
if ($this->isHosted()) {
// Never destroy hosted working copies.
return false;
}
$default_path = PhabricatorEnv::getEnvConfig(
'repository.default-local-path');
return Filesystem::isDescendant($this->getLocalPath(), $default_path);
}
public function canUsePathTree() {
return !$this->isSVN();
}
public function canMirror() {
if ($this->isGit() || $this->isHg()) {
return true;
}
return false;
}
public function canAllowDangerousChanges() {
if (!$this->isHosted()) {
return false;
}
if ($this->isGit() || $this->isHg()) {
return true;
}
return false;
}
public function shouldAllowDangerousChanges() {
return (bool)$this->getDetail('allow-dangerous-changes');
}
public function writeStatusMessage(
$status_type,
$status_code,
array $parameters = array()) {
$table = new PhabricatorRepositoryStatusMessage();
$conn_w = $table->establishConnection('w');
$table_name = $table->getTableName();
if ($status_code === null) {
queryfx(
$conn_w,
'DELETE FROM %T WHERE repositoryID = %d AND statusType = %s',
$table_name,
$this->getID(),
$status_type);
} else {
queryfx(
$conn_w,
'INSERT INTO %T
(repositoryID, statusType, statusCode, parameters, epoch)
VALUES (%d, %s, %s, %s, %d)
ON DUPLICATE KEY UPDATE
statusCode = VALUES(statusCode),
parameters = VALUES(parameters),
epoch = VALUES(epoch)',
$table_name,
$this->getID(),
$status_type,
$status_code,
json_encode($parameters),
time());
}
return $this;
}
public static function getRemoteURIProtocol($raw_uri) {
$uri = new PhutilURI($raw_uri);
if ($uri->getProtocol()) {
return strtolower($uri->getProtocol());
}
$git_uri = new PhutilGitURI($raw_uri);
if (strlen($git_uri->getDomain()) && strlen($git_uri->getPath())) {
return 'ssh';
}
return null;
}
public static function assertValidRemoteURI($uri) {
if (trim($uri) != $uri) {
throw new Exception(
- pht(
- 'The remote URI has leading or trailing whitespace.'));
+ pht('The remote URI has leading or trailing whitespace.'));
}
$protocol = self::getRemoteURIProtocol($uri);
// Catch confusion between Git/SCP-style URIs and normal URIs. See T3619
// for discussion. This is usually a user adding "ssh://" to an implicit
// SSH Git URI.
if ($protocol == 'ssh') {
if (preg_match('(^[^:@]+://[^/:]+:[^\d])', $uri)) {
throw new Exception(
pht(
"The remote URI is not formatted correctly. Remote URIs ".
"with an explicit protocol should be in the form ".
- "'proto://domain/path', not 'proto://domain:/path'. ".
- "The ':/path' syntax is only valid in SCP-style URIs."));
+ "'%s', not '%s'. The '%s' syntax is only valid in SCP-style URIs.",
+ 'proto://domain/path',
+ 'proto://domain:/path',
+ ':/path'));
}
}
switch ($protocol) {
case 'ssh':
case 'http':
case 'https':
case 'git':
case 'svn':
case 'svn+ssh':
break;
default:
// NOTE: We're explicitly rejecting 'file://' because it can be
// used to clone from the working copy of another repository on disk
// that you don't normally have permission to access.
throw new Exception(
pht(
"The URI protocol is unrecognized. It should begin ".
- "'ssh://', 'http://', 'https://', 'git://', 'svn://', ".
- "'svn+ssh://', or be in the form 'git@domain.com:path'."));
+ "'%s', '%s', '%s', '%s', '%s', '%s', or be in the form '%s'.",
+ 'ssh://',
+ 'http://',
+ 'https://',
+ 'git://',
+ 'svn://',
+ 'svn+ssh://',
+ 'git@domain.com:path'));
}
return true;
}
/**
* Load the pull frequency for this repository, based on the time since the
* last activity.
*
* We pull rarely used repositories less frequently. This finds the most
* recent commit which is older than the current time (which prevents us from
* spinning on repositories with a silly commit post-dated to some time in
* 2037). We adjust the pull frequency based on when the most recent commit
* occurred.
*
* @param int The minimum update interval to use, in seconds.
* @return int Repository update interval, in seconds.
*/
public function loadUpdateInterval($minimum = 15) {
// If a repository is still importing, always pull it as frequently as
// possible. This prevents us from hanging for a long time at 99.9% when
// importing an inactive repository.
if ($this->isImporting()) {
return $minimum;
}
$window_start = (PhabricatorTime::getNow() + $minimum);
$table = id(new PhabricatorRepositoryCommit());
$last_commit = queryfx_one(
$table->establishConnection('r'),
'SELECT epoch FROM %T
WHERE repositoryID = %d AND epoch <= %d
ORDER BY epoch DESC LIMIT 1',
$table->getTableName(),
$this->getID(),
$window_start);
if ($last_commit) {
$time_since_commit = ($window_start - $last_commit['epoch']);
$last_few_days = phutil_units('3 days in seconds');
if ($time_since_commit <= $last_few_days) {
// For repositories with activity in the recent past, we wait one
// extra second for every 10 minutes since the last commit. This
// shorter backoff is intended to handle weekends and other short
// breaks from development.
$smart_wait = ($time_since_commit / 600);
} else {
// For repositories without recent activity, we wait one extra second
// for every 4 minutes since the last commit. This longer backoff
// handles rarely used repositories, up to the maximum.
$smart_wait = ($time_since_commit / 240);
}
// We'll never wait more than 6 hours to pull a repository.
$longest_wait = phutil_units('6 hours in seconds');
$smart_wait = min($smart_wait, $longest_wait);
$smart_wait = max($minimum, $smart_wait);
} else {
$smart_wait = $minimum;
}
return $smart_wait;
}
/**
* Retrieve the sevice URI for the device hosting this repository.
*
* See @{method:newConduitClient} for a general discussion of interacting
* with repository services. This method provides lower-level resolution of
* services, returning raw URIs.
*
* @param PhabricatorUser Viewing user.
* @param bool `true` to throw if a remote URI would be returned.
* @param list<string> List of allowable protocols.
* @return string|null URI, or `null` for local repositories.
*/
public function getAlmanacServiceURI(
PhabricatorUser $viewer,
$never_proxy,
array $protocols) {
$service_phid = $this->getAlmanacServicePHID();
if (!$service_phid) {
// No service, so this is a local repository.
return null;
}
$service = id(new AlmanacServiceQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withPHIDs(array($service_phid))
->needBindings(true)
->executeOne();
if (!$service) {
throw new Exception(
pht(
- 'The Alamnac service for this repository is invalid or could not '.
+ 'The Almanac service for this repository is invalid or could not '.
'be loaded.'));
}
$service_type = $service->getServiceType();
if (!($service_type instanceof AlmanacClusterRepositoryServiceType)) {
throw new Exception(
pht(
- 'The Alamnac service for this repository does not have the correct '.
+ 'The Almanac service for this repository does not have the correct '.
'service type.'));
}
$bindings = $service->getBindings();
if (!$bindings) {
throw new Exception(
pht(
- 'The Alamanc service for this repository is not bound to any '.
+ 'The Almanac service for this repository is not bound to any '.
'interfaces.'));
}
$local_device = AlmanacKeys::getDeviceID();
if ($never_proxy && !$local_device) {
throw new Exception(
pht(
'Unable to handle proxied service request. This device is not '.
'registered, so it can not identify local services. Register '.
'this device before sending requests here.'));
}
$protocol_map = array_fuse($protocols);
$uris = array();
foreach ($bindings as $binding) {
$iface = $binding->getInterface();
// If we're never proxying this and it's locally satisfiable, return
// `null` to tell the caller to handle it locally. If we're allowed to
// proxy, we skip this check and may proxy the request to ourselves.
// (That proxied request will end up here with proxying forbidden,
// return `null`, and then the request will actually run.)
if ($local_device && $never_proxy) {
if ($iface->getDevice()->getName() == $local_device) {
return null;
}
}
$protocol = $binding->getAlmanacPropertyValue('protocol');
if ($protocol === null) {
$protocol = 'https';
}
if (empty($protocol_map[$protocol])) {
continue;
}
$uris[] = $protocol.'://'.$iface->renderDisplayAddress().'/';
}
if (!$uris) {
throw new Exception(
pht(
'The Almanac service for this repository is not bound to any '.
'interfaces which support the required protocols (%s).',
implode(', ', $protocols)));
}
if ($never_proxy) {
throw new Exception(
pht(
'Refusing to proxy a repository request from a cluster host. '.
'Cluster hosts must correctly route their intracluster requests.'));
}
shuffle($uris);
return head($uris);
}
/**
* Build a new Conduit client in order to make a service call to this
* repository.
*
* If the repository is hosted locally, this method may return `null`. The
* caller should use `ConduitCall` or other local logic to complete the
* request.
*
* By default, we will return a @{class:ConduitClient} for any repository with
* a service, even if that service is on the current device.
*
* We do this because this configuration does not make very much sense in a
* production context, but is very common in a test/development context
* (where the developer's machine is both the web host and the repository
* service). By proxying in development, we get more consistent behavior
* between development and production, and don't have a major untested
* codepath.
*
* The `$never_proxy` parameter can be used to prevent this local proxying.
* If the flag is passed:
*
* - The method will return `null` (implying a local service call)
* if the repository service is hosted on the current device.
* - The method will throw if it would need to return a client.
*
* This is used to prevent loops in Conduit: the first request will proxy,
* even in development, but the second request will be identified as a
* cluster request and forced not to proxy.
*
* For lower-level service resolution, see @{method:getAlmanacServiceURI}.
*
* @param PhabricatorUser Viewing user.
* @param bool `true` to throw if a client would be returned.
* @return ConduitClient|null Client, or `null` for local repositories.
*/
public function newConduitClient(
PhabricatorUser $viewer,
$never_proxy = false) {
$uri = $this->getAlmanacServiceURI(
$viewer,
$never_proxy,
array(
'http',
'https',
));
if ($uri === null) {
return null;
}
$domain = id(new PhutilURI(PhabricatorEnv::getURI('/')))->getDomain();
$client = id(new ConduitClient($uri))
->setHost($domain);
if ($viewer->isOmnipotent()) {
// If the caller is the omnipotent user (normally, a daemon), we will
// sign the request with this host's asymmetric keypair.
$public_path = AlmanacKeys::getKeyPath('device.pub');
try {
$public_key = Filesystem::readFile($public_path);
} catch (Exception $ex) {
throw new PhutilAggregateException(
pht(
'Unable to read device public key while attempting to make '.
'authenticated method call within the Phabricator cluster. '.
- 'Use `bin/almanac register` to register keys for this device. '.
- 'Exception: %s',
+ 'Use `%s` to register keys for this device. Exception: %s',
+ 'bin/almanac register',
$ex->getMessage()),
array($ex));
}
$private_path = AlmanacKeys::getKeyPath('device.key');
try {
$private_key = Filesystem::readFile($private_path);
$private_key = new PhutilOpaqueEnvelope($private_key);
} catch (Exception $ex) {
throw new PhutilAggregateException(
pht(
'Unable to read device private key while attempting to make '.
'authenticated method call within the Phabricator cluster. '.
- 'Use `bin/almanac register` to register keys for this device. '.
- 'Exception: %s',
+ 'Use `%s` to register keys for this device. Exception: %s',
+ 'bin/almanac register',
$ex->getMessage()),
array($ex));
}
$client->setSigningKeys($public_key, $private_key);
} else {
// If the caller is a normal user, we generate or retrieve a cluster
// API token.
$token = PhabricatorConduitToken::loadClusterTokenForUser($viewer);
if ($token) {
$client->setConduitToken($token->getToken());
}
}
return $client;
}
/* -( Symbols )-------------------------------------------------------------*/
public function getSymbolSources() {
return $this->getDetail('symbol-sources', array());
}
public function getSymbolLanguages() {
return $this->getDetail('symbol-languages', array());
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new PhabricatorRepositoryEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new PhabricatorRepositoryTransaction();
}
public function willRenderTimeline(
PhabricatorApplicationTransactionView $timeline,
AphrontRequest $request) {
return $timeline;
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
DiffusionPushCapability::CAPABILITY,
);
}
public function getPolicy($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
return $this->getViewPolicy();
case PhabricatorPolicyCapability::CAN_EDIT:
return $this->getEditPolicy();
case DiffusionPushCapability::CAPABILITY:
return $this->getPushPolicy();
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $user) {
return false;
}
public function describeAutomaticCapability($capability) {
return null;
}
/* -( PhabricatorMarkupInterface )----------------------------------------- */
public function getMarkupFieldKey($field) {
$hash = PhabricatorHash::digestForIndex($this->getMarkupText($field));
return "repo:{$hash}";
}
public function newMarkupEngine($field) {
return PhabricatorMarkupEngine::newMarkupEngine(array());
}
public function getMarkupText($field) {
return $this->getDetail('description');
}
public function didMarkupText(
$field,
$output,
PhutilMarkupEngine $engine) {
require_celerity_resource('phabricator-remarkup-css');
return phutil_tag(
'div',
array(
'class' => 'phabricator-remarkup',
),
$output);
}
public function shouldUseMarkupCache($field) {
return true;
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$this->openTransaction();
$this->delete();
$this->saveTransaction();
}
}
diff --git a/src/applications/repository/storage/PhabricatorRepositoryVCSPassword.php b/src/applications/repository/storage/PhabricatorRepositoryVCSPassword.php
index f9cbb69a0..2f5c20cf7 100644
--- a/src/applications/repository/storage/PhabricatorRepositoryVCSPassword.php
+++ b/src/applications/repository/storage/PhabricatorRepositoryVCSPassword.php
@@ -1,60 +1,60 @@
<?php
final class PhabricatorRepositoryVCSPassword extends PhabricatorRepositoryDAO {
protected $id;
protected $userPHID;
protected $passwordHash;
protected function getConfiguration() {
return array(
self::CONFIG_COLUMN_SCHEMA => array(
'passwordHash' => 'text128',
),
self::CONFIG_KEY_SCHEMA => array(
'key_phid' => array(
'columns' => array('userPHID'),
'unique' => true,
),
),
) + parent::getConfiguration();
}
public function setPassword(
PhutilOpaqueEnvelope $password,
PhabricatorUser $user) {
$hash_envelope = $this->hashPassword($password, $user);
return $this->setPasswordHash($hash_envelope->openEnvelope());
}
public function comparePassword(
PhutilOpaqueEnvelope $password,
PhabricatorUser $user) {
return PhabricatorPasswordHasher::comparePassword(
$this->getPasswordHashInput($password, $user),
new PhutilOpaqueEnvelope($this->getPasswordHash()));
}
private function getPasswordHashInput(
PhutilOpaqueEnvelope $password,
PhabricatorUser $user) {
if ($user->getPHID() != $this->getUserPHID()) {
- throw new Exception('User does not match password user PHID!');
+ throw new Exception(pht('User does not match password user PHID!'));
}
$raw_input = PhabricatorHash::digestPassword($password, $user->getPHID());
return new PhutilOpaqueEnvelope($raw_input);
}
private function hashPassword(
PhutilOpaqueEnvelope $password,
PhabricatorUser $user) {
$input_envelope = $this->getPasswordHashInput($password, $user);
$best_hasher = PhabricatorPasswordHasher::getBestHasher();
return $best_hasher->getPasswordHashForStorage($input_envelope);
}
}
diff --git a/src/applications/repository/storage/__tests__/PhabricatorRepositoryTestCase.php b/src/applications/repository/storage/__tests__/PhabricatorRepositoryTestCase.php
index 8e308ed60..7e71efe75 100644
--- a/src/applications/repository/storage/__tests__/PhabricatorRepositoryTestCase.php
+++ b/src/applications/repository/storage/__tests__/PhabricatorRepositoryTestCase.php
@@ -1,155 +1,155 @@
<?php
final class PhabricatorRepositoryTestCase
extends PhabricatorTestCase {
public function testRepositoryURIProtocols() {
$tests = array(
'/path/to/repo' => 'file',
'file:///path/to/repo' => 'file',
'ssh://user@domain.com/path' => 'ssh',
'git@example.com:path' => 'ssh',
'git://git@example.com/path' => 'git',
'svn+ssh://example.com/path' => 'svn+ssh',
'https://example.com/repo/' => 'https',
'http://example.com/' => 'http',
'https://user@example.com/' => 'https',
);
foreach ($tests as $uri => $expect) {
$repository = new PhabricatorRepository();
$repository->setDetail('remote-uri', $uri);
$this->assertEqual(
$expect,
$repository->getRemoteProtocol(),
- "Protocol for '{$uri}'.");
+ pht("Protocol for '%s'.", $uri));
}
}
public function testBranchFilter() {
$git = PhabricatorRepositoryType::REPOSITORY_TYPE_GIT;
$repo = new PhabricatorRepository();
$repo->setVersionControlSystem($git);
$this->assertTrue(
$repo->shouldTrackBranch('imaginary'),
- 'Track all branches by default.');
+ pht('Track all branches by default.'));
$repo->setDetail(
'branch-filter',
array(
'master' => true,
));
$this->assertTrue(
$repo->shouldTrackBranch('master'),
- 'Track listed branches.');
+ pht('Track listed branches.'));
$this->assertFalse(
$repo->shouldTrackBranch('imaginary'),
- 'Do not track unlisted branches.');
+ pht('Do not track unlisted branches.'));
}
public function testSubversionPathInfo() {
$svn = PhabricatorRepositoryType::REPOSITORY_TYPE_SVN;
$repo = new PhabricatorRepository();
$repo->setVersionControlSystem($svn);
$repo->setDetail('remote-uri', 'http://svn.example.com/repo');
$this->assertEqual(
'http://svn.example.com/repo',
$repo->getSubversionPathURI());
$repo->setDetail('remote-uri', 'http://svn.example.com/repo/');
$this->assertEqual(
'http://svn.example.com/repo',
$repo->getSubversionPathURI());
$repo->setDetail('hosting-enabled', true);
$repo->setDetail('local-path', '/var/repo/SVN');
$this->assertEqual(
'file:///var/repo/SVN',
$repo->getSubversionPathURI());
$repo->setDetail('local-path', '/var/repo/SVN/');
$this->assertEqual(
'file:///var/repo/SVN',
$repo->getSubversionPathURI());
$this->assertEqual(
'file:///var/repo/SVN/a@',
$repo->getSubversionPathURI('a'));
$this->assertEqual(
'file:///var/repo/SVN/a@1',
$repo->getSubversionPathURI('a', 1));
$this->assertEqual(
'file:///var/repo/SVN/%3F@22',
$repo->getSubversionPathURI('?', 22));
$repo->setDetail('svn-subpath', 'quack/trunk/');
$this->assertEqual(
'file:///var/repo/SVN/quack/trunk/@',
$repo->getSubversionBaseURI());
$this->assertEqual(
'file:///var/repo/SVN/quack/trunk/@HEAD',
$repo->getSubversionBaseURI('HEAD'));
}
public function testFilterMercurialDebugOutput() {
$map = array(
'' => '',
"quack\n" => "quack\n",
"ignoring untrusted configuration option x.y = z\nquack\n" =>
"quack\n",
"ignoring untrusted configuration option x.y = z\n".
"ignoring untrusted configuration option x.y = z\n".
"quack\n" =>
"quack\n",
"ignoring untrusted configuration option x.y = z\n".
"ignoring untrusted configuration option x.y = z\n".
"ignoring untrusted configuration option x.y = z\n".
"quack\n" =>
"quack\n",
"quack\n".
"ignoring untrusted configuration option x.y = z\n".
"ignoring untrusted configuration option x.y = z\n".
"ignoring untrusted configuration option x.y = z\n" =>
"quack\n",
"ignoring untrusted configuration option x.y = z\n".
"ignoring untrusted configuration option x.y = z\n".
"duck\n".
"ignoring untrusted configuration option x.y = z\n".
"ignoring untrusted configuration option x.y = z\n".
"bread\n".
"ignoring untrusted configuration option x.y = z\n".
"quack\n" =>
"duck\nbread\nquack\n",
"ignoring untrusted configuration option x.y = z\n".
"duckignoring untrusted configuration option x.y = z\n".
"quack" =>
'duckquack',
);
foreach ($map as $input => $expect) {
$actual = PhabricatorRepository::filterMercurialDebugOutput($input);
$this->assertEqual($expect, $actual, $input);
}
}
}
diff --git a/src/applications/repository/worker/PhabricatorRepositoryCommitOwnersWorker.php b/src/applications/repository/worker/PhabricatorRepositoryCommitOwnersWorker.php
index 986627388..5756c4e88 100644
--- a/src/applications/repository/worker/PhabricatorRepositoryCommitOwnersWorker.php
+++ b/src/applications/repository/worker/PhabricatorRepositoryCommitOwnersWorker.php
@@ -1,139 +1,139 @@
<?php
final class PhabricatorRepositoryCommitOwnersWorker
extends PhabricatorRepositoryCommitParserWorker {
protected function parseCommit(
PhabricatorRepository $repository,
PhabricatorRepositoryCommit $commit) {
$this->triggerOwnerAudits($repository, $commit);
$commit->writeImportStatusFlag(
PhabricatorRepositoryCommit::IMPORTED_OWNERS);
if ($this->shouldQueueFollowupTasks()) {
$this->queueTask(
'PhabricatorRepositoryCommitHeraldWorker',
array(
'commitID' => $commit->getID(),
));
}
}
private function triggerOwnerAudits(
PhabricatorRepository $repository,
PhabricatorRepositoryCommit $commit) {
if (!$repository->shouldPublish()) {
return;
}
$affected_paths = PhabricatorOwnerPathQuery::loadAffectedPaths(
$repository,
$commit,
PhabricatorUser::getOmnipotentUser());
$affected_packages = PhabricatorOwnersPackage::loadAffectedPackages(
$repository,
$affected_paths);
if ($affected_packages) {
$requests = id(new PhabricatorRepositoryAuditRequest())
->loadAllWhere(
'commitPHID = %s',
$commit->getPHID());
$requests = mpull($requests, null, 'getAuditorPHID');
foreach ($affected_packages as $package) {
$request = idx($requests, $package->getPHID());
if ($request) {
// Don't update request if it exists already.
continue;
}
if ($package->getAuditingEnabled()) {
$reasons = $this->checkAuditReasons($commit, $package);
if ($reasons) {
$audit_status =
PhabricatorAuditStatusConstants::AUDIT_REQUIRED;
} else {
$audit_status =
PhabricatorAuditStatusConstants::AUDIT_NOT_REQUIRED;
}
} else {
$reasons = array();
$audit_status = PhabricatorAuditStatusConstants::NONE;
}
$relationship = new PhabricatorRepositoryAuditRequest();
$relationship->setAuditorPHID($package->getPHID());
$relationship->setCommitPHID($commit->getPHID());
$relationship->setAuditReasons($reasons);
$relationship->setAuditStatus($audit_status);
$relationship->save();
$requests[$package->getPHID()] = $relationship;
}
$commit->updateAuditStatus($requests);
$commit->save();
}
}
private function checkAuditReasons(
PhabricatorRepositoryCommit $commit,
PhabricatorOwnersPackage $package) {
$data = id(new PhabricatorRepositoryCommitData())->loadOneWhere(
'commitID = %d',
$commit->getID());
$reasons = array();
if ($data->getCommitDetail('vsDiff')) {
- $reasons[] = 'Changed After Revision Was Accepted';
+ $reasons[] = pht('Changed After Revision Was Accepted');
}
$commit_author_phid = $data->getCommitDetail('authorPHID');
if (!$commit_author_phid) {
- $reasons[] = 'Commit Author Not Recognized';
+ $reasons[] = pht('Commit Author Not Recognized');
}
$revision_id = $data->getCommitDetail('differential.revisionID');
$revision_author_phid = null;
$commit_reviewedby_phid = null;
if ($revision_id) {
$revision = id(new DifferentialRevisionQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withIDs(array($revision_id))
->executeOne();
if ($revision) {
$revision_author_phid = $revision->getAuthorPHID();
$commit_reviewedby_phid = $data->getCommitDetail('reviewerPHID');
if ($revision_author_phid !== $commit_author_phid) {
- $reasons[] = 'Author Not Matching with Revision';
+ $reasons[] = pht('Author Not Matching with Revision');
}
} else {
- $reasons[] = 'Revision Not Found';
+ $reasons[] = pht('Revision Not Found');
}
} else {
- $reasons[] = 'No Revision Specified';
+ $reasons[] = pht('No Revision Specified');
}
$owners_phids = PhabricatorOwnersOwner::loadAffiliatedUserPHIDs(
array($package->getID()));
if (!($commit_author_phid && in_array($commit_author_phid, $owners_phids) ||
$commit_reviewedby_phid && in_array($commit_reviewedby_phid,
$owners_phids))) {
- $reasons[] = 'Owners Not Involved';
+ $reasons[] = pht('Owners Not Involved');
}
return $reasons;
}
}
diff --git a/src/applications/repository/worker/__tests__/PhabricatorChangeParserTestCase.php b/src/applications/repository/worker/__tests__/PhabricatorChangeParserTestCase.php
index 7efc3a4a1..4925c9dbe 100644
--- a/src/applications/repository/worker/__tests__/PhabricatorChangeParserTestCase.php
+++ b/src/applications/repository/worker/__tests__/PhabricatorChangeParserTestCase.php
@@ -1,1237 +1,1237 @@
<?php
final class PhabricatorChangeParserTestCase
extends PhabricatorWorkingCopyTestCase {
public function testGitParser() {
$repository = $this->buildDiscoveredRepository('CHA');
$viewer = PhabricatorUser::getOmnipotentUser();
$commits = id(new DiffusionCommitQuery())
->setViewer($viewer)
->withRepositoryIDs(array($repository->getID()))
->execute();
$this->expectChanges(
$repository,
$commits,
array(
// 8ebb73c add +x
'8ebb73c3f127625ad090472f4f3bfc72804def54' => array(
array(
'/',
null,
null,
DifferentialChangeType::TYPE_CHILD,
DifferentialChangeType::FILE_DIRECTORY,
0,
1389892449,
),
array(
'/file_moved',
null,
null,
DifferentialChangeType::TYPE_CHANGE,
DifferentialChangeType::FILE_NORMAL,
1,
1389892449,
),
),
// ee9c790 add symlink
'ee9c7909e012da7d75e8e1293c7803a6e73ac26a' => array(
array(
'/',
null,
null,
DifferentialChangeType::TYPE_CHILD,
DifferentialChangeType::FILE_DIRECTORY,
0,
1389892436,
),
array(
'/file_link',
null,
null,
DifferentialChangeType::TYPE_ADD,
DifferentialChangeType::FILE_SYMLINK,
1,
1389892436,
),
),
// 7260ca4 add directory file
'7260ca4b6cec35e755bb5365c4ccdd3f1977772e' => array(
array(
'/',
null,
null,
DifferentialChangeType::TYPE_CHILD,
DifferentialChangeType::FILE_DIRECTORY,
0,
1389892408,
),
array(
'/dir',
null,
null,
DifferentialChangeType::TYPE_ADD,
DifferentialChangeType::FILE_DIRECTORY,
1,
1389892408,
),
array(
'/dir/subfile',
null,
null,
DifferentialChangeType::TYPE_ADD,
DifferentialChangeType::FILE_NORMAL,
1,
1389892408,
),
),
// 1fe783c move a file
'1fe783cf207c1e5f3e01650d2d9cb80b8a707f0e' => array(
array(
'/',
null,
null,
DifferentialChangeType::TYPE_CHILD,
DifferentialChangeType::FILE_DIRECTORY,
0,
1389892388,
),
array(
'/file',
null,
null,
DifferentialChangeType::TYPE_MOVE_AWAY,
DifferentialChangeType::FILE_NORMAL,
1,
1389892388,
),
array(
'/file_moved',
'/file',
'1fe783cf207c1e5f3e01650d2d9cb80b8a707f0e',
DifferentialChangeType::TYPE_MOVE_HERE,
DifferentialChangeType::FILE_NORMAL,
1,
1389892388,
),
),
// 376af8c copy a file
'376af8cd8f5b96ec55b7d9a86ccc85b8df8fb833' => array(
array(
'/',
null,
null,
DifferentialChangeType::TYPE_CHILD,
DifferentialChangeType::FILE_DIRECTORY,
0,
1389892377,
),
array(
'/file',
null,
null,
DifferentialChangeType::TYPE_COPY_AWAY,
DifferentialChangeType::FILE_NORMAL,
0,
1389892377,
),
array(
'/file_copy',
'/file',
'376af8cd8f5b96ec55b7d9a86ccc85b8df8fb833',
DifferentialChangeType::TYPE_COPY_HERE,
DifferentialChangeType::FILE_NORMAL,
1,
1389892377,
),
),
// ece6ea6 changed a file
'ece6ea6c6836e8b11a103e21707b8f30e6840c94' => array(
array(
'/',
null,
null,
DifferentialChangeType::TYPE_CHILD,
DifferentialChangeType::FILE_DIRECTORY,
0,
1389892352,
),
array(
'/file',
null,
null,
DifferentialChangeType::TYPE_CHANGE,
DifferentialChangeType::FILE_NORMAL,
1,
1389892352,
),
),
// 513103f added a file
'513103f65b8413dd2f1a1b5c1d4852a4a598540f' => array(
array(
'/',
null,
null,
DifferentialChangeType::TYPE_CHILD,
DifferentialChangeType::FILE_DIRECTORY,
// This is the initial commit and technically created this
// directory; arguably the parser should figure this out and
// mark this as a direct change.
0,
1389892330,
),
array(
'/file',
null,
null,
DifferentialChangeType::TYPE_ADD,
DifferentialChangeType::FILE_NORMAL,
1,
1389892330,
),
),
));
}
public function testMercurialParser() {
$this->requireBinaryForTest('hg');
$repository = $this->buildDiscoveredRepository('CHB');
$viewer = PhabricatorUser::getOmnipotentUser();
$commits = id(new DiffusionCommitQuery())
->setViewer($viewer)
->withRepositoryIDs(array($repository->getID()))
->execute();
$this->expectChanges(
$repository,
$commits,
array(
'970357a2dc4264060e65d68e42240bb4e5984085' => array(
array(
'/',
null,
null,
DifferentialChangeType::TYPE_CHILD,
DifferentialChangeType::FILE_DIRECTORY,
0,
1390249395,
),
array(
'/file_moved',
null,
null,
DifferentialChangeType::TYPE_CHANGE,
DifferentialChangeType::FILE_NORMAL,
1,
1390249395,
),
),
'fbb49af9788e5dbffbc05a060b680df1fd457be3' => array(
array(
'/',
null,
null,
DifferentialChangeType::TYPE_CHILD,
DifferentialChangeType::FILE_DIRECTORY,
0,
1390249380,
),
array(
'/file_link',
null,
null,
DifferentialChangeType::TYPE_ADD,
// TODO: This is not correct, and should be FILE_SYMLINK. See
// note in the parser about this. This is a known bug.
DifferentialChangeType::FILE_NORMAL,
1,
1390249380,
),
),
'0e8d3465944c7ed7a7c139da7edc652cf80dba69' => array(
array(
'/',
null,
null,
DifferentialChangeType::TYPE_CHILD,
DifferentialChangeType::FILE_DIRECTORY,
0,
1390249342,
),
array(
'/dir',
null,
null,
DifferentialChangeType::TYPE_ADD,
DifferentialChangeType::FILE_DIRECTORY,
1,
1390249342,
),
array(
'/dir/subfile',
null,
null,
DifferentialChangeType::TYPE_ADD,
DifferentialChangeType::FILE_NORMAL,
1,
1390249342,
),
),
'22c75131ff15c8a44d7a729c4542b7f4c8ed27f4' => array(
array(
'/',
null,
null,
DifferentialChangeType::TYPE_CHILD,
DifferentialChangeType::FILE_DIRECTORY,
0,
1390249320,
),
array(
'/file',
null,
null,
DifferentialChangeType::TYPE_MOVE_AWAY,
DifferentialChangeType::FILE_NORMAL,
1,
1390249320,
),
array(
'/file_moved',
'/file',
'22c75131ff15c8a44d7a729c4542b7f4c8ed27f4',
DifferentialChangeType::TYPE_MOVE_HERE,
DifferentialChangeType::FILE_NORMAL,
1,
1390249320,
),
),
'd9d252df30cb7251ad3ea121eff30c7d2e36dd67' => array(
array(
'/',
null,
null,
DifferentialChangeType::TYPE_CHILD,
DifferentialChangeType::FILE_DIRECTORY,
0,
1390249308,
),
array(
'/file',
null,
null,
DifferentialChangeType::TYPE_COPY_AWAY,
DifferentialChangeType::FILE_NORMAL,
0,
1390249308,
),
array(
'/file_copy',
'/file',
'd9d252df30cb7251ad3ea121eff30c7d2e36dd67',
DifferentialChangeType::TYPE_COPY_HERE,
DifferentialChangeType::FILE_NORMAL,
1,
1390249308,
),
),
'1fc0445d5e3d0f33e9dcbb68bbe419a847460d25' => array(
array(
'/',
null,
null,
DifferentialChangeType::TYPE_CHILD,
DifferentialChangeType::FILE_DIRECTORY,
0,
1390249294,
),
array(
'/file',
null,
null,
DifferentialChangeType::TYPE_CHANGE,
DifferentialChangeType::FILE_NORMAL,
1,
1390249294,
),
),
'61518e196efb7f80700333cc0d00634c2578871a' => array(
array(
'/',
null,
null,
DifferentialChangeType::TYPE_ADD,
DifferentialChangeType::FILE_DIRECTORY,
1,
1390249286,
),
array(
'/file',
null,
null,
DifferentialChangeType::TYPE_ADD,
DifferentialChangeType::FILE_NORMAL,
1,
1390249286,
),
),
));
}
public function testSubversionParser() {
$repository = $this->buildDiscoveredRepository('CHC');
$viewer = PhabricatorUser::getOmnipotentUser();
$commits = id(new DiffusionCommitQuery())
->setViewer($viewer)
->withRepositoryIDs(array($repository->getID()))
->execute();
$this->expectChanges(
$repository,
$commits,
array(
'15' => array(
array(
'/',
null,
null,
DifferentialChangeType::TYPE_CHILD,
DifferentialChangeType::FILE_DIRECTORY,
0,
15,
),
array(
'/file_copy',
null,
null,
DifferentialChangeType::TYPE_MULTICOPY,
DifferentialChangeType::FILE_NORMAL,
1,
15,
),
array(
'/file_copy_x',
'/file_copy',
'12',
DifferentialChangeType::TYPE_MOVE_HERE,
DifferentialChangeType::FILE_NORMAL,
1,
15,
),
array(
'/file_copy_y',
'/file_copy',
'12',
DifferentialChangeType::TYPE_MOVE_HERE,
DifferentialChangeType::FILE_NORMAL,
1,
15,
),
array(
'/file_copy_z',
'/file_copy',
'12',
DifferentialChangeType::TYPE_MOVE_HERE,
DifferentialChangeType::FILE_NORMAL,
1,
15,
),
),
// Add a file from a different revision
'14' => array(
array(
'/',
null,
null,
DifferentialChangeType::TYPE_CHILD,
DifferentialChangeType::FILE_DIRECTORY,
0,
14,
),
array(
'/file',
null,
null,
DifferentialChangeType::TYPE_COPY_AWAY,
DifferentialChangeType::FILE_NORMAL,
0,
14,
),
array(
'/file_1',
'/file',
'1',
DifferentialChangeType::TYPE_COPY_HERE,
DifferentialChangeType::FILE_NORMAL,
1,
14,
),
),
// Property change on "/"
'13' => array(
array(
'/',
null,
null,
DifferentialChangeType::TYPE_CHANGE,
DifferentialChangeType::FILE_DIRECTORY,
1,
13,
),
),
// Copy a directory, removing and adding files to the copy
'12' => array(
array(
'/',
null,
null,
DifferentialChangeType::TYPE_CHILD,
DifferentialChangeType::FILE_DIRECTORY,
0,
12,
),
array(
'/dir',
null,
null,
// TODO: This might reasonbly be considered a bug in the parser; it
// should probably be COPY_AWAY.
DifferentialChangeType::TYPE_CHILD,
DifferentialChangeType::FILE_DIRECTORY,
0,
12,
),
array(
'/dir/a',
null,
null,
DifferentialChangeType::TYPE_COPY_AWAY,
DifferentialChangeType::FILE_NORMAL,
0,
12,
),
array(
'/dir/b',
null,
null,
DifferentialChangeType::TYPE_COPY_AWAY,
DifferentialChangeType::FILE_NORMAL,
0,
12,
),
array(
'/dir/subdir',
null,
null,
DifferentialChangeType::TYPE_COPY_AWAY,
DifferentialChangeType::FILE_DIRECTORY,
0,
12,
),
array(
'/dir/subdir/a',
null,
null,
DifferentialChangeType::TYPE_COPY_AWAY,
DifferentialChangeType::FILE_NORMAL,
0,
12,
),
array(
'/dir/subdir/b',
null,
null,
DifferentialChangeType::TYPE_COPY_AWAY,
DifferentialChangeType::FILE_NORMAL,
0,
12,
),
array(
'/dir_copy',
'/dir',
'11',
DifferentialChangeType::TYPE_COPY_HERE,
DifferentialChangeType::FILE_DIRECTORY,
1,
12,
),
array(
'/dir_copy/a',
'/dir/a',
'11',
DifferentialChangeType::TYPE_COPY_HERE,
DifferentialChangeType::FILE_NORMAL,
1,
12,
),
array(
'/dir_copy/b',
'/dir/b',
'11',
DifferentialChangeType::TYPE_COPY_HERE,
DifferentialChangeType::FILE_NORMAL,
1,
12,
),
array(
'/dir_copy/subdir',
'/dir/subdir',
'11',
DifferentialChangeType::TYPE_COPY_HERE,
DifferentialChangeType::FILE_DIRECTORY,
1,
12,
),
array(
'/dir_copy/subdir/a',
'/dir/subdir/a',
'11',
DifferentialChangeType::TYPE_COPY_HERE,
DifferentialChangeType::FILE_NORMAL,
1,
12,
),
array(
'/dir_copy/subdir/b',
'/dir/subdir/b',
'11',
DifferentialChangeType::TYPE_DELETE,
DifferentialChangeType::FILE_NORMAL,
1,
12,
),
array(
'/dir_copy/subdir/c',
null,
null,
DifferentialChangeType::TYPE_ADD,
DifferentialChangeType::FILE_NORMAL,
1,
12,
),
),
// Add a directory with a subdirectory and files, sets up next commit
'11' => array(
array(
'/',
null,
null,
DifferentialChangeType::TYPE_CHILD,
DifferentialChangeType::FILE_DIRECTORY,
0,
11,
),
array(
'/dir',
null,
null,
DifferentialChangeType::TYPE_ADD,
DifferentialChangeType::FILE_DIRECTORY,
1,
11,
),
array(
'/dir/a',
null,
null,
DifferentialChangeType::TYPE_ADD,
DifferentialChangeType::FILE_NORMAL,
1,
11,
),
array(
'/dir/b',
null,
null,
DifferentialChangeType::TYPE_ADD,
DifferentialChangeType::FILE_NORMAL,
1,
11,
),
array(
'/dir/subdir',
null,
null,
DifferentialChangeType::TYPE_ADD,
DifferentialChangeType::FILE_DIRECTORY,
1,
11,
),
array(
'/dir/subdir/a',
null,
null,
DifferentialChangeType::TYPE_ADD,
DifferentialChangeType::FILE_NORMAL,
1,
11,
),
array(
'/dir/subdir/b',
null,
null,
DifferentialChangeType::TYPE_ADD,
DifferentialChangeType::FILE_NORMAL,
1,
11,
),
),
// Remove directory
'10' => array(
array(
'/',
null,
null,
DifferentialChangeType::TYPE_CHILD,
DifferentialChangeType::FILE_DIRECTORY,
0,
10,
),
array(
'/dir',
null,
null,
DifferentialChangeType::TYPE_DELETE,
DifferentialChangeType::FILE_DIRECTORY,
1,
10,
),
array(
'/dir/subfile',
null,
null,
DifferentialChangeType::TYPE_DELETE,
DifferentialChangeType::FILE_NORMAL,
1,
10,
),
),
// Replace directory with file
'9' => array(
array(
'/',
null,
null,
DifferentialChangeType::TYPE_CHILD,
DifferentialChangeType::FILE_DIRECTORY,
0,
9,
),
array(
'/file_moved',
null,
null,
DifferentialChangeType::TYPE_CHANGE,
DifferentialChangeType::FILE_DIRECTORY,
1,
9,
),
),
// Replace file with file
'8' => array(
array(
'/',
null,
null,
DifferentialChangeType::TYPE_CHILD,
DifferentialChangeType::FILE_DIRECTORY,
0,
8,
),
array(
'/file_moved',
null,
null,
DifferentialChangeType::TYPE_CHANGE,
DifferentialChangeType::FILE_NORMAL,
1,
8,
),
),
'7' => array(
array(
'/',
null,
null,
DifferentialChangeType::TYPE_CHILD,
DifferentialChangeType::FILE_DIRECTORY,
0,
7,
),
array(
'/file_moved',
null,
null,
DifferentialChangeType::TYPE_CHANGE,
DifferentialChangeType::FILE_NORMAL,
1,
7,
),
),
'6' => array(
array(
'/',
null,
null,
DifferentialChangeType::TYPE_CHILD,
DifferentialChangeType::FILE_DIRECTORY,
0,
6,
),
array(
'/file_link',
null,
null,
DifferentialChangeType::TYPE_ADD,
// TODO: This is not correct, and should be FILE_SYMLINK.
DifferentialChangeType::FILE_NORMAL,
1,
6,
),
),
'5' => array(
array(
'/',
null,
null,
DifferentialChangeType::TYPE_CHILD,
DifferentialChangeType::FILE_DIRECTORY,
0,
5,
),
array(
'/dir',
null,
null,
DifferentialChangeType::TYPE_ADD,
DifferentialChangeType::FILE_DIRECTORY,
1,
5,
),
array(
'/dir/subfile',
null,
null,
DifferentialChangeType::TYPE_ADD,
DifferentialChangeType::FILE_NORMAL,
1,
5,
),
),
'4' => array(
array(
'/',
null,
null,
DifferentialChangeType::TYPE_CHILD,
DifferentialChangeType::FILE_DIRECTORY,
0,
4,
),
array(
'/file',
null,
null,
DifferentialChangeType::TYPE_MOVE_AWAY,
DifferentialChangeType::FILE_NORMAL,
1,
4,
),
array(
'/file_moved',
'/file',
'2',
DifferentialChangeType::TYPE_MOVE_HERE,
DifferentialChangeType::FILE_NORMAL,
1,
4,
),
),
'3' => array(
array(
'/',
null,
null,
DifferentialChangeType::TYPE_CHILD,
DifferentialChangeType::FILE_DIRECTORY,
0,
3,
),
array(
'/file',
null,
null,
DifferentialChangeType::TYPE_COPY_AWAY,
DifferentialChangeType::FILE_NORMAL,
0,
3,
),
array(
'/file_copy',
'/file',
'2',
DifferentialChangeType::TYPE_COPY_HERE,
DifferentialChangeType::FILE_NORMAL,
1,
3,
),
),
'2' => array(
array(
'/',
null,
null,
DifferentialChangeType::TYPE_CHILD,
DifferentialChangeType::FILE_DIRECTORY,
0,
2,
),
array(
'/file',
null,
null,
DifferentialChangeType::TYPE_CHANGE,
DifferentialChangeType::FILE_NORMAL,
1,
2,
),
),
'1' => array(
array(
'/',
null,
null,
// The Git and Svn parsers don't recognize the first commit as
// creating "/", while the Mercurial parser does. All the parsers
// should probably behave like the Mercurial parser.
DifferentialChangeType::TYPE_CHILD,
DifferentialChangeType::FILE_DIRECTORY,
0,
1,
),
array(
'/file',
null,
null,
DifferentialChangeType::TYPE_ADD,
DifferentialChangeType::FILE_NORMAL,
1,
1,
),
),
));
}
public function testSubversionPartialParser() {
$repository = $this->buildBareRepository('CHD');
$repository->setDetail('svn-subpath', 'trunk/');
id(new PhabricatorRepositoryPullEngine())
->setRepository($repository)
->pullRepository();
id(new PhabricatorRepositoryDiscoveryEngine())
->setRepository($repository)
->discoverCommits();
$viewer = PhabricatorUser::getOmnipotentUser();
$commits = id(new DiffusionCommitQuery())
->setViewer($viewer)
->withRepositoryIDs(array($repository->getID()))
->execute();
$this->expectChanges(
$repository,
$commits,
array(
// Copy of a file outside of the subpath from an earlier revision
// into the subpath.
4 => array(
array(
'/',
null,
null,
DifferentialChangeType::TYPE_CHILD,
DifferentialChangeType::FILE_DIRECTORY,
0,
4,
),
array(
'/goat',
null,
null,
DifferentialChangeType::TYPE_COPY_AWAY,
DifferentialChangeType::FILE_NORMAL,
0,
4,
),
array(
'/trunk',
null,
null,
DifferentialChangeType::TYPE_CHILD,
DifferentialChangeType::FILE_DIRECTORY,
0,
4,
),
array(
'/trunk/goat',
'/goat',
'1',
DifferentialChangeType::TYPE_COPY_HERE,
DifferentialChangeType::FILE_NORMAL,
1,
4,
),
),
3 => array(
array(
'/',
null,
null,
DifferentialChangeType::TYPE_CHILD,
DifferentialChangeType::FILE_DIRECTORY,
0,
3,
),
array(
'/trunk',
null,
null,
DifferentialChangeType::TYPE_ADD,
DifferentialChangeType::FILE_DIRECTORY,
1,
3,
),
array(
'/trunk/apple',
null,
null,
DifferentialChangeType::TYPE_ADD,
DifferentialChangeType::FILE_NORMAL,
1,
3,
),
array(
'/trunk/banana',
null,
null,
DifferentialChangeType::TYPE_ADD,
DifferentialChangeType::FILE_NORMAL,
1,
3,
),
),
));
}
public function testSubversionValidRootParser() {
// First, automatically configure the root correctly.
$repository = $this->buildBareRepository('CHD');
id(new PhabricatorRepositoryPullEngine())
->setRepository($repository)
->pullRepository();
$caught = null;
try {
id(new PhabricatorRepositoryDiscoveryEngine())
->setRepository($repository)
->discoverCommits();
} catch (Exception $ex) {
$caught = $ex;
}
$this->assertFalse(
($caught instanceof Exception),
pht('Natural SVN root should work properly.'));
// This time, artificially break the root. We expect this to fail.
$repository = $this->buildBareRepository('CHD');
$repository->setDetail(
'remote-uri',
$repository->getDetail('remote-uri').'trunk/');
id(new PhabricatorRepositoryPullEngine())
->setRepository($repository)
->pullRepository();
$caught = null;
try {
id(new PhabricatorRepositoryDiscoveryEngine())
->setRepository($repository)
->discoverCommits();
} catch (Exception $ex) {
$caught = $ex;
}
$this->assertTrue(
($caught instanceof Exception),
pht('Artificial SVN root should fail.'));
}
public function testSubversionForeignStubsParser() {
$repository = $this->buildBareRepository('CHE');
$repository->setDetail('svn-subpath', 'branch/');
id(new PhabricatorRepositoryPullEngine())
->setRepository($repository)
->pullRepository();
id(new PhabricatorRepositoryDiscoveryEngine())
->setRepository($repository)
->discoverCommits();
$viewer = PhabricatorUser::getOmnipotentUser();
$commits = id(new DiffusionCommitQuery())
->setViewer($viewer)
->withRepositoryIDs(array($repository->getID()))
->execute();
foreach ($commits as $commit) {
$this->parseCommit($repository, $commit);
}
// As a side effect, we expect parsing these commits to have created
// foreign stubs of other commits.
$commits = id(new DiffusionCommitQuery())
->setViewer($viewer)
->withRepositoryIDs(array($repository->getID()))
->execute();
$commits = mpull($commits, null, 'getCommitIdentifier');
$this->assertTrue(
isset($commits['2']),
- 'Expect rCHE2 to exist as a foreign stub.');
+ pht('Expect %s to exist as a foreign stub.', 'rCHE2'));
// The foreign stub should be marked imported.
$commit = $commits['2'];
$this->assertEqual(
PhabricatorRepositoryCommit::IMPORTED_ALL,
(int)$commit->getImportStatus());
}
private function parseCommit(
PhabricatorRepository $repository,
PhabricatorRepositoryCommit $commit) {
switch ($repository->getVersionControlSystem()) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
$parser = 'PhabricatorRepositoryGitCommitChangeParserWorker';
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
$parser = 'PhabricatorRepositoryMercurialCommitChangeParserWorker';
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
$parser = 'PhabricatorRepositorySvnCommitChangeParserWorker';
break;
default:
throw new Exception(pht('No support yet.'));
}
$parser_object = newv($parser, array(array()));
return $parser_object->parseChangesForUnitTest($repository, $commit);
}
private function expectChanges(
PhabricatorRepository $repository,
array $commits,
array $expect) {
foreach ($commits as $commit) {
$commit_identifier = $commit->getCommitIdentifier();
$expect_changes = idx($expect, $commit_identifier);
if ($expect_changes === null) {
$this->assertEqual(
$commit_identifier,
null,
pht(
'No test entry for commit "%s" in repository "%s"!',
$commit_identifier,
$repository->getCallsign()));
}
$changes = $this->parseCommit($repository, $commit);
$path_map = id(new DiffusionPathQuery())
->withPathIDs(mpull($changes, 'getPathID'))
->execute();
$path_map = ipull($path_map, 'path');
$target_commits = array_filter(mpull($changes, 'getTargetCommitID'));
if ($target_commits) {
$commits = id(new DiffusionCommitQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withIDs($target_commits)
->execute();
$target_commits = mpull($commits, 'getCommitIdentifier', 'getID');
}
$dicts = array();
foreach ($changes as $key => $change) {
$target_path = idx($path_map, $change->getTargetPathID());
$target_commit = idx($target_commits, $change->getTargetCommitID());
$dicts[$key] = array(
$path_map[(int)$change->getPathID()],
$target_path,
$target_commit ? (string)$target_commit : null,
(int)$change->getChangeType(),
(int)$change->getFileType(),
(int)$change->getIsDirect(),
(int)$change->getCommitSequence(),
);
}
$dicts = ipull($dicts, null, 0);
$expect_changes = ipull($expect_changes, null, 0);
ksort($dicts);
ksort($expect_changes);
$this->assertEqual(
$expect_changes,
$dicts,
pht('Commit %s', $commit_identifier));
}
}
}
diff --git a/src/applications/repository/worker/commitchangeparser/PhabricatorRepositoryCommitChangeParserWorker.php b/src/applications/repository/worker/commitchangeparser/PhabricatorRepositoryCommitChangeParserWorker.php
index ffcb78435..92ba0d8c5 100644
--- a/src/applications/repository/worker/commitchangeparser/PhabricatorRepositoryCommitChangeParserWorker.php
+++ b/src/applications/repository/worker/commitchangeparser/PhabricatorRepositoryCommitChangeParserWorker.php
@@ -1,159 +1,159 @@
<?php
abstract class PhabricatorRepositoryCommitChangeParserWorker
extends PhabricatorRepositoryCommitParserWorker {
public function getRequiredLeaseTime() {
// It can take a very long time to parse commits; some commits in the
// Facebook repository affect many millions of paths. Acquire 24h leases.
return phutil_units('24 hours in seconds');
}
abstract protected function parseCommitChanges(
PhabricatorRepository $repository,
PhabricatorRepositoryCommit $commit);
protected function parseCommit(
PhabricatorRepository $repository,
PhabricatorRepositoryCommit $commit) {
$identifier = $commit->getCommitIdentifier();
$callsign = $repository->getCallsign();
$full_name = 'r'.$callsign.$identifier;
- $this->log("Parsing %s...\n", $full_name);
+ $this->log("%s\n", pht('Parsing %s...', $full_name));
if ($this->isBadCommit($full_name)) {
- $this->log('This commit is marked bad!');
+ $this->log(pht('This commit is marked bad!'));
return;
}
$results = $this->parseCommitChanges($repository, $commit);
if ($results) {
$this->writeCommitChanges($repository, $commit, $results);
}
$this->finishParse();
}
public function parseChangesForUnitTest(
PhabricatorRepository $repository,
PhabricatorRepositoryCommit $commit) {
return $this->parseCommitChanges($repository, $commit);
}
public static function lookupOrCreatePaths(array $paths) {
$repository = new PhabricatorRepository();
$conn_w = $repository->establishConnection('w');
$result_map = self::lookupPaths($paths);
$missing_paths = array_fill_keys($paths, true);
$missing_paths = array_diff_key($missing_paths, $result_map);
$missing_paths = array_keys($missing_paths);
if ($missing_paths) {
foreach (array_chunk($missing_paths, 128) as $path_chunk) {
$sql = array();
foreach ($path_chunk as $path) {
$sql[] = qsprintf($conn_w, '(%s, %s)', $path, md5($path));
}
queryfx(
$conn_w,
'INSERT IGNORE INTO %T (path, pathHash) VALUES %Q',
PhabricatorRepository::TABLE_PATH,
implode(', ', $sql));
}
$result_map += self::lookupPaths($missing_paths);
}
return $result_map;
}
private static function lookupPaths(array $paths) {
$repository = new PhabricatorRepository();
$conn_w = $repository->establishConnection('w');
$result_map = array();
foreach (array_chunk($paths, 128) as $path_chunk) {
$chunk_map = queryfx_all(
$conn_w,
'SELECT path, id FROM %T WHERE pathHash IN (%Ls)',
PhabricatorRepository::TABLE_PATH,
array_map('md5', $path_chunk));
foreach ($chunk_map as $row) {
$result_map[$row['path']] = $row['id'];
}
}
return $result_map;
}
protected function finishParse() {
$commit = $this->commit;
$commit->writeImportStatusFlag(
PhabricatorRepositoryCommit::IMPORTED_CHANGE);
id(new PhabricatorSearchIndexer())
->queueDocumentForIndexing($commit->getPHID());
PhabricatorOwnersPackagePathValidator::updateOwnersPackagePaths(
$commit,
PhabricatorUser::getOmnipotentUser());
if ($this->shouldQueueFollowupTasks()) {
$this->queueTask(
'PhabricatorRepositoryCommitOwnersWorker',
array(
'commitID' => $commit->getID(),
));
}
}
private function writeCommitChanges(
PhabricatorRepository $repository,
PhabricatorRepositoryCommit $commit,
array $changes) {
$repository_id = (int)$repository->getID();
$commit_id = (int)$commit->getID();
// NOTE: This SQL is being built manually instead of with qsprintf()
// because some SVN changes affect an enormous number of paths (millions)
// and this showed up as significantly slow on a profile at some point.
$changes_sql = array();
foreach ($changes as $change) {
$values = array(
$repository_id,
(int)$change->getPathID(),
$commit_id,
nonempty((int)$change->getTargetPathID(), 'null'),
nonempty((int)$change->getTargetCommitID(), 'null'),
(int)$change->getChangeType(),
(int)$change->getFileType(),
(int)$change->getIsDirect(),
(int)$change->getCommitSequence(),
);
$changes_sql[] = '('.implode(', ', $values).')';
}
$conn_w = $repository->establishConnection('w');
queryfx(
$conn_w,
'DELETE FROM %T WHERE commitID = %d',
PhabricatorRepository::TABLE_PATHCHANGE,
$commit_id);
foreach (PhabricatorLiskDAO::chunkSQL($changes_sql) as $chunk) {
queryfx(
$conn_w,
'INSERT INTO %T
(repositoryID, pathID, commitID, targetPathID, targetCommitID,
changeType, fileType, isDirect, commitSequence)
VALUES %Q',
PhabricatorRepository::TABLE_PATHCHANGE,
$chunk);
}
}
}
diff --git a/src/applications/repository/worker/commitchangeparser/PhabricatorRepositoryGitCommitChangeParserWorker.php b/src/applications/repository/worker/commitchangeparser/PhabricatorRepositoryGitCommitChangeParserWorker.php
index fb191631b..4ab6f03f3 100644
--- a/src/applications/repository/worker/commitchangeparser/PhabricatorRepositoryGitCommitChangeParserWorker.php
+++ b/src/applications/repository/worker/commitchangeparser/PhabricatorRepositoryGitCommitChangeParserWorker.php
@@ -1,248 +1,248 @@
<?php
final class PhabricatorRepositoryGitCommitChangeParserWorker
extends PhabricatorRepositoryCommitChangeParserWorker {
protected function parseCommitChanges(
PhabricatorRepository $repository,
PhabricatorRepositoryCommit $commit) {
// Check if the commit has parents. We're testing to see whether it is the
// first commit in history (in which case we must use "git log") or some
// other commit (in which case we can use "git diff"). We'd rather use
// "git diff" because it has the right behavior for merge commits, but
// it requires the commit to have a parent that we can diff against. The
// first commit doesn't, so "commit^" is not a valid ref.
list($parents) = $repository->execxLocalCommand(
'log -n1 --format=%s %s',
'%P',
$commit->getCommitIdentifier());
$use_log = !strlen(trim($parents));
if ($use_log) {
// This is the first commit so we need to use "log". We know it's not a
// merge commit because it couldn't be merging anything, so this is safe.
// NOTE: "--pretty=format: " is to disable diff output, we only want the
// part we get from "--raw".
list($raw) = $repository->execxLocalCommand(
'log -n1 -M -C -B --find-copies-harder --raw -t '.
'--pretty=format: --abbrev=40 %s',
$commit->getCommitIdentifier());
} else {
// Otherwise, we can use "diff", which will give us output for merges.
// We diff against the first parent, as this is generally the expectation
// and results in sensible behavior.
list($raw) = $repository->execxLocalCommand(
'diff -n1 -M -C -B --find-copies-harder --raw -t '.
'--abbrev=40 %s^1 %s',
$commit->getCommitIdentifier(),
$commit->getCommitIdentifier());
}
$changes = array();
$move_away = array();
$copy_away = array();
$lines = explode("\n", $raw);
foreach ($lines as $line) {
if (!strlen(trim($line))) {
continue;
}
list($old_mode, $new_mode,
$old_hash, $new_hash,
$more_stuff) = preg_split('/ +/', $line, 5);
// We may only have two pieces here.
list($action, $src_path, $dst_path) = array_merge(
explode("\t", $more_stuff),
array(null));
// Normalize the paths for consistency with the SVN workflow.
$src_path = '/'.$src_path;
if ($dst_path) {
$dst_path = '/'.$dst_path;
}
$old_mode = intval($old_mode, 8);
$new_mode = intval($new_mode, 8);
switch ($new_mode & 0160000) {
case 0160000:
$file_type = DifferentialChangeType::FILE_SUBMODULE;
break;
case 0120000:
$file_type = DifferentialChangeType::FILE_SYMLINK;
break;
case 0040000:
$file_type = DifferentialChangeType::FILE_DIRECTORY;
break;
default:
$file_type = DifferentialChangeType::FILE_NORMAL;
break;
}
// TODO: We can detect binary changes as git does, through a combination
// of running 'git check-attr' for stuff like 'binary', 'merge' or 'diff',
// and by falling back to inspecting the first 8,000 characters of the
// buffer for null bytes (this is seriously git's algorithm, see
// buffer_is_binary() in xdiff-interface.c).
$change_type = null;
$change_path = $src_path;
$change_target = null;
$is_direct = true;
switch ($action[0]) {
case 'A':
$change_type = DifferentialChangeType::TYPE_ADD;
break;
case 'D':
$change_type = DifferentialChangeType::TYPE_DELETE;
break;
case 'C':
$change_type = DifferentialChangeType::TYPE_COPY_HERE;
$change_path = $dst_path;
$change_target = $src_path;
$copy_away[$change_target][] = $change_path;
break;
case 'R':
$change_type = DifferentialChangeType::TYPE_MOVE_HERE;
$change_path = $dst_path;
$change_target = $src_path;
$move_away[$change_target][] = $change_path;
break;
case 'T':
// Type of the file changed, fall through and treat it as a
// modification. Not 100% sure this is the right thing to do but it
// seems reasonable.
case 'M':
if ($file_type == DifferentialChangeType::FILE_DIRECTORY) {
$change_type = DifferentialChangeType::TYPE_CHILD;
$is_direct = false;
} else {
$change_type = DifferentialChangeType::TYPE_CHANGE;
}
break;
// NOTE: "U" (unmerged) and "X" (unknown) statuses are also possible
// in theory but shouldn't appear here.
default:
- throw new Exception("Failed to parse line '{$line}'.");
+ throw new Exception(pht("Failed to parse line '%s'.", $line));
}
$changes[$change_path] = array(
'repositoryID' => $repository->getID(),
'commitID' => $commit->getID(),
'path' => $change_path,
'changeType' => $change_type,
'fileType' => $file_type,
'isDirect' => $is_direct,
'commitSequence' => $commit->getEpoch(),
'targetPath' => $change_target,
'targetCommitID' => $change_target ? $commit->getID() : null,
);
}
// Add a change to '/' since git doesn't mention it.
$changes['/'] = array(
'repositoryID' => $repository->getID(),
'commitID' => $commit->getID(),
'path' => '/',
'changeType' => DifferentialChangeType::TYPE_CHILD,
'fileType' => DifferentialChangeType::FILE_DIRECTORY,
'isDirect' => false,
'commitSequence' => $commit->getEpoch(),
'targetPath' => null,
'targetCommitID' => null,
);
foreach ($copy_away as $change_path => $destinations) {
if (isset($move_away[$change_path])) {
$change_type = DifferentialChangeType::TYPE_MULTICOPY;
$is_direct = true;
unset($move_away[$change_path]);
} else {
$change_type = DifferentialChangeType::TYPE_COPY_AWAY;
// This change is direct if we picked up a modification above (i.e.,
// the original copy source was also edited). Otherwise the original
// wasn't touched, so leave it as an indirect change.
$is_direct = isset($changes[$change_path]);
}
$reference = $changes[reset($destinations)];
$changes[$change_path] = array(
'repositoryID' => $repository->getID(),
'commitID' => $commit->getID(),
'path' => $change_path,
'changeType' => $change_type,
'fileType' => $reference['fileType'],
'isDirect' => $is_direct,
'commitSequence' => $commit->getEpoch(),
'targetPath' => null,
'targetCommitID' => null,
);
}
foreach ($move_away as $change_path => $destinations) {
$reference = $changes[reset($destinations)];
$changes[$change_path] = array(
'repositoryID' => $repository->getID(),
'commitID' => $commit->getID(),
'path' => $change_path,
'changeType' => DifferentialChangeType::TYPE_MOVE_AWAY,
'fileType' => $reference['fileType'],
'isDirect' => true,
'commitSequence' => $commit->getEpoch(),
'targetPath' => null,
'targetCommitID' => null,
);
}
$paths = array();
foreach ($changes as $change) {
$paths[$change['path']] = true;
if ($change['targetPath']) {
$paths[$change['targetPath']] = true;
}
}
$path_map = $this->lookupOrCreatePaths(array_keys($paths));
foreach ($changes as $key => $change) {
$changes[$key]['pathID'] = $path_map[$change['path']];
if ($change['targetPath']) {
$changes[$key]['targetPathID'] = $path_map[$change['targetPath']];
} else {
$changes[$key]['targetPathID'] = null;
}
}
$results = array();
foreach ($changes as $change) {
$result = id(new PhabricatorRepositoryParsedChange())
->setPathID($change['pathID'])
->setTargetPathID($change['targetPathID'])
->setTargetCommitID($change['targetCommitID'])
->setChangeType($change['changeType'])
->setFileType($change['fileType'])
->setIsDirect($change['isDirect'])
->setCommitSequence($change['commitSequence']);
$results[] = $result;
}
return $results;
}
}
diff --git a/src/applications/repository/worker/commitchangeparser/PhabricatorRepositorySvnCommitChangeParserWorker.php b/src/applications/repository/worker/commitchangeparser/PhabricatorRepositorySvnCommitChangeParserWorker.php
index e65905b98..6725ddb1d 100644
--- a/src/applications/repository/worker/commitchangeparser/PhabricatorRepositorySvnCommitChangeParserWorker.php
+++ b/src/applications/repository/worker/commitchangeparser/PhabricatorRepositorySvnCommitChangeParserWorker.php
@@ -1,786 +1,808 @@
<?php
final class PhabricatorRepositorySvnCommitChangeParserWorker
extends PhabricatorRepositoryCommitChangeParserWorker {
protected function parseCommitChanges(
PhabricatorRepository $repository,
PhabricatorRepositoryCommit $commit) {
// PREAMBLE: This class is absurdly complicated because it is very difficult
// to get the information we need out of SVN. The actual data we need is:
//
// 1. Recursively, what were the affected paths?
// 2. For each affected path, is it a file or a directory?
// 3. How was each path affected (e.g. add, delete, move, copy)?
//
// We spend nearly all of our effort figuring out (1) and (2) because
// "svn log" is not recursive and does not give us file/directory
// information (that is, it will report a directory move as a single move,
// even if many thousands of paths are affected).
//
// Instead, we have to "svn ls -R" the location of each path in its previous
// life to figure out whether it is a file or a directory and exactly which
// recursive paths were affected if it was moved or copied. This is very
// complicated and has many special cases.
$uri = $repository->getSubversionPathURI();
$svn_commit = $commit->getCommitIdentifier();
// Pull the top-level path changes out of "svn log". This is pretty
// straightforward; just parse the XML log.
$log = $this->getSVNLogXMLObject($repository, $uri, $svn_commit);
$entry = $log->logentry[0];
if (!$entry->paths) {
// TODO: Explicitly mark this commit as broken elsewhere? This isn't
// supposed to happen but we have some cases like rE27 and rG935 in the
// Facebook repositories where things got all clowned up.
return array();
}
$raw_paths = array();
foreach ($entry->paths->path as $path) {
$name = trim((string)$path);
$raw_paths[$name] = array(
'rawPath' => $name,
'rawTargetPath' => (string)$path['copyfrom-path'],
'rawChangeType' => (string)$path['action'],
'rawTargetCommit' => (string)$path['copyfrom-rev'],
);
}
$copied_or_moved_map = array();
$deleted_paths = array();
$add_paths = array();
foreach ($raw_paths as $path => $raw_info) {
if ($raw_info['rawTargetPath']) {
$copied_or_moved_map[$raw_info['rawTargetPath']][] = $raw_info;
}
switch ($raw_info['rawChangeType']) {
case 'D':
$deleted_paths[$path] = $raw_info;
break;
case 'A':
case 'R':
$add_paths[$path] = $raw_info;
break;
}
}
// If a path was deleted, we need to look in the repository history to
// figure out where the former valid location for it is so we can figure out
// if it was a directory or not, among other things.
$lookup_here = array();
foreach ($raw_paths as $path => $raw_info) {
if ($raw_info['rawChangeType'] != 'D') {
continue;
}
// If a change copies a directory and then deletes something from it,
// we need to look at the old location for information about the path, not
// the new location. This workflow is pretty ridiculous -- so much so that
// Trac gets it wrong. See Facebook rO6 for an example, if you happen to
// work at Facebook.
$parents = $this->expandAllParentPaths($path, $include_self = true);
foreach ($parents as $parent) {
if (isset($add_paths[$parent])) {
$relative_path = substr($path, strlen($parent));
$lookup_here[$path] = array(
'rawPath' => $add_paths[$parent]['rawTargetPath'].$relative_path,
'rawCommit' => $add_paths[$parent]['rawTargetCommit'],
);
continue 2;
}
}
// Otherwise we can just look at the previous revision.
$lookup_here[$path] = array(
'rawPath' => $path,
'rawCommit' => $svn_commit - 1,
);
}
$lookup = array();
foreach ($raw_paths as $path => $raw_info) {
if ($raw_info['rawChangeType'] == 'D') {
$lookup[$path] = $lookup_here[$path];
} else {
// For everything that wasn't deleted, we can just look it up directly.
$lookup[$path] = array(
'rawPath' => $path,
'rawCommit' => $svn_commit,
);
}
}
$effects = array();
$path_file_types = $this->lookupPathFileTypes($repository, $lookup);
foreach ($raw_paths as $path => $raw_info) {
if ($raw_info['rawChangeType'] == 'D' &&
$path_file_types[$path] == DifferentialChangeType::FILE_DIRECTORY) {
// Bad. Child paths aren't enumerated in "svn log" so we need
// to go fishing.
$list = $this->lookupRecursiveFileList(
$repository,
$lookup[$path]);
foreach ($list as $deleted_path => $path_file_type) {
$deleted_path = rtrim($path.'/'.$deleted_path, '/');
if (!empty($raw_paths[$deleted_path])) {
// We somehow learned about this deletion explicitly?
// TODO: Unclear how this is possible.
continue;
}
$effect_type = DifferentialChangeType::TYPE_DELETE;
$effect_target_path = null;
if (isset($copied_or_moved_map[$deleted_path])) {
$effect_target_path = $path;
if (count($copied_or_moved_map[$deleted_path]) > 1) {
$effect_type = DifferentialChangeType::TYPE_MULTICOPY;
} else {
$effect_type = DifferentialChangeType::TYPE_MOVE_AWAY;
}
}
$effects[$deleted_path] = array(
'rawPath' => $deleted_path,
'rawTargetPath' => $effect_target_path,
'rawTargetCommit' => null,
'rawDirect' => true,
'changeType' => $effect_type,
'fileType' => $path_file_type,
);
$deleted_paths[$deleted_path] = $effects[$deleted_path];
}
}
}
$resolved_types = array();
$supplemental = array();
foreach ($raw_paths as $path => $raw_info) {
if (isset($resolved_types[$path])) {
$type = $resolved_types[$path];
} else {
switch ($raw_info['rawChangeType']) {
case 'D':
if (isset($copied_or_moved_map[$path])) {
if (count($copied_or_moved_map[$path]) > 1) {
$type = DifferentialChangeType::TYPE_MULTICOPY;
} else {
$type = DifferentialChangeType::TYPE_MOVE_AWAY;
}
} else {
$type = DifferentialChangeType::TYPE_DELETE;
}
break;
case 'A':
$copy_from = $raw_info['rawTargetPath'];
$copy_rev = $raw_info['rawTargetCommit'];
if (!strlen($copy_from)) {
$type = DifferentialChangeType::TYPE_ADD;
} else {
if (isset($deleted_paths[$copy_from])) {
$type = DifferentialChangeType::TYPE_MOVE_HERE;
$other_type = DifferentialChangeType::TYPE_MOVE_AWAY;
} else {
$type = DifferentialChangeType::TYPE_COPY_HERE;
$other_type = DifferentialChangeType::TYPE_COPY_AWAY;
}
$source_file_type = $this->lookupPathFileType(
$repository,
$copy_from,
array(
'rawPath' => $copy_from,
'rawCommit' => $copy_rev,
));
if ($source_file_type == DifferentialChangeType::FILE_DELETED) {
throw new Exception(
- 'Something is wrong; source of a copy must exist.');
+ pht('Something is wrong; source of a copy must exist.'));
}
if ($source_file_type != DifferentialChangeType::FILE_DIRECTORY) {
if (isset($raw_paths[$copy_from]) ||
isset($effects[$copy_from])) {
break;
}
$effects[$copy_from] = array(
'rawPath' => $copy_from,
'rawTargetPath' => null,
'rawTargetCommit' => null,
'rawDirect' => false,
'changeType' => $other_type,
'fileType' => $source_file_type,
);
} else {
// ULTRADISASTER. We've added a directory which was copied
// or moved from somewhere else. This is the most complex and
// ridiculous case.
$list = $this->lookupRecursiveFileList(
$repository,
array(
'rawPath' => $copy_from,
'rawCommit' => $copy_rev,
));
foreach ($list as $from_path => $from_file_type) {
$full_from = rtrim($copy_from.'/'.$from_path, '/');
$full_to = rtrim($path.'/'.$from_path, '/');
if (empty($raw_paths[$full_to])) {
$effects[$full_to] = array(
'rawPath' => $full_to,
'rawTargetPath' => $full_from,
'rawTargetCommit' => $copy_rev,
'rawDirect' => true,
'changeType' => $type,
'fileType' => $from_file_type,
);
} else {
// This means we picked the file up explicitly elsewhere.
// If the file as modified, SVN will drop the copy
// information. We need to restore it.
$supplemental[$full_to]['rawTargetPath'] = $full_from;
$supplemental[$full_to]['rawTargetCommit'] = $copy_rev;
if ($raw_paths[$full_to]['rawChangeType'] == 'M') {
$resolved_types[$full_to] = $type;
}
}
if (empty($raw_paths[$full_from]) &&
empty($effects[$full_from])) {
if ($other_type == DifferentialChangeType::TYPE_COPY_AWAY) {
// Add an indirect effect for the copied file, if we
// don't already have an entry for it (e.g., a separate
// change).
$effects[$full_from] = array(
'rawPath' => $full_from,
'rawTargetPath' => null,
'rawTargetCommit' => null,
'rawDirect' => false,
'changeType' => $other_type,
'fileType' => $from_file_type,
);
}
}
}
}
}
break;
// This is "replaced", caused by "svn rm"-ing a file, putting another
// in its place, and then "svn add"-ing it. We do not distinguish
// between this and "M".
case 'R':
case 'M':
if (isset($copied_or_moved_map[$path])) {
$type = DifferentialChangeType::TYPE_COPY_AWAY;
} else {
$type = DifferentialChangeType::TYPE_CHANGE;
}
break;
}
}
$resolved_types[$path] = $type;
}
foreach ($raw_paths as $path => $raw_info) {
$raw_paths[$path]['changeType'] = $resolved_types[$path];
if (isset($supplemental[$path])) {
foreach ($supplemental[$path] as $key => $value) {
$raw_paths[$path][$key] = $value;
}
}
}
foreach ($raw_paths as $path => $raw_info) {
$effects[$path] = array(
'rawPath' => $path,
'rawTargetPath' => $raw_info['rawTargetPath'],
'rawTargetCommit' => $raw_info['rawTargetCommit'],
'rawDirect' => true,
'changeType' => $raw_info['changeType'],
'fileType' => $path_file_types[$path],
);
}
$parents = array();
foreach ($effects as $path => $effect) {
foreach ($this->expandAllParentPaths($path) as $parent_path) {
$parents[$parent_path] = true;
}
}
$parents = array_keys($parents);
foreach ($parents as $parent) {
if (isset($effects[$parent])) {
continue;
}
$effects[$parent] = array(
'rawPath' => $parent,
'rawTargetPath' => null,
'rawTargetCommit' => null,
'rawDirect' => false,
'changeType' => DifferentialChangeType::TYPE_CHILD,
'fileType' => DifferentialChangeType::FILE_DIRECTORY,
);
}
$lookup_paths = array();
foreach ($effects as $effect) {
$lookup_paths[$effect['rawPath']] = true;
if ($effect['rawTargetPath']) {
$lookup_paths[$effect['rawTargetPath']] = true;
}
}
$lookup_paths = array_keys($lookup_paths);
$lookup_commits = array();
foreach ($effects as $effect) {
if ($effect['rawTargetCommit']) {
$lookup_commits[$effect['rawTargetCommit']] = true;
}
}
$lookup_commits = array_keys($lookup_commits);
$path_map = $this->lookupOrCreatePaths($lookup_paths);
$commit_map = $this->lookupSvnCommits($repository, $lookup_commits);
$this->writeBrowse($repository, $commit, $effects, $path_map);
return $this->buildChanges(
$repository,
$commit,
$effects,
$path_map,
$commit_map);
}
private function buildChanges(
PhabricatorRepository $repository,
PhabricatorRepositoryCommit $commit,
array $effects,
array $path_map,
array $commit_map) {
$results = array();
foreach ($effects as $effect) {
$path_id = $path_map[$effect['rawPath']];
$target_path_id = null;
if ($effect['rawTargetPath']) {
$target_path_id = $path_map[$effect['rawTargetPath']];
}
$target_commit_id = null;
if ($effect['rawTargetCommit']) {
$target_commit_id = $commit_map[$effect['rawTargetCommit']];
}
$result = id(new PhabricatorRepositoryParsedChange())
->setPathID($path_id)
->setTargetPathID($target_path_id)
->setTargetCommitID($target_commit_id)
->setChangeType($effect['changeType'])
->setFileType($effect['fileType'])
->setIsDirect($effect['rawDirect'])
->setCommitSequence($commit->getCommitIdentifier());
$results[] = $result;
}
return $results;
}
private function writeBrowse(
PhabricatorRepository $repository,
PhabricatorRepositoryCommit $commit,
array $effects,
array $path_map) {
$conn_w = $repository->establishConnection('w');
$sql = array();
foreach ($effects as $effect) {
$type = $effect['changeType'];
if (!$effect['rawDirect']) {
if ($type == DifferentialChangeType::TYPE_COPY_AWAY) {
// Don't write COPY_AWAY to the filesystem table if it isn't a direct
// event.
continue;
}
if ($type == DifferentialChangeType::TYPE_CHILD) {
// Don't write CHILD to the filesystem table. Although doing these
// writes has the nice property of letting you see when a directory's
// contents were last changed, it explodes the table tremendously
// and makes Diffusion far slower.
continue;
}
}
if ($effect['rawPath'] == '/') {
// Don't write any events on '/' to the filesystem table; in
// particular, it doesn't have a meaningful parentID.
continue;
}
$existed = !DifferentialChangeType::isDeleteChangeType($type);
$sql[] = qsprintf(
$conn_w,
'(%d, %d, %d, %d, %d, %d)',
$repository->getID(),
$path_map[$this->getParentPath($effect['rawPath'])],
$commit->getCommitIdentifier(),
$path_map[$effect['rawPath']],
$existed
? 1
: 0,
$effect['fileType']);
}
queryfx(
$conn_w,
'DELETE FROM %T WHERE repositoryID = %d AND svnCommit = %d',
PhabricatorRepository::TABLE_FILESYSTEM,
$repository->getID(),
$commit->getCommitIdentifier());
foreach (array_chunk($sql, 512) as $sql_chunk) {
queryfx(
$conn_w,
'INSERT INTO %T
(repositoryID, parentID, svnCommit, pathID, existed, fileType)
VALUES %Q',
PhabricatorRepository::TABLE_FILESYSTEM,
implode(', ', $sql_chunk));
}
}
private function lookupSvnCommits(
PhabricatorRepository $repository,
array $commits) {
if (!$commits) {
return array();
}
$commit_table = new PhabricatorRepositoryCommit();
$commit_data = queryfx_all(
$commit_table->establishConnection('w'),
'SELECT id, commitIdentifier FROM %T
WHERE repositoryID = %d AND commitIdentifier in (%Ls)',
$commit_table->getTableName(),
$repository->getID(),
$commits);
$commit_map = ipull($commit_data, 'id', 'commitIdentifier');
$need = array();
foreach ($commits as $commit) {
if (empty($commit_map[$commit])) {
$need[] = $commit;
}
}
// If we are parsing a Subversion repository and have been configured to
// import only some subdirectory of it, we may find commits which reference
// other foreign commits outside of the directory (for instance, because of
// a move or copy). Rather than trying to execute full parses on them, just
// create stub commits and identify the stubs as foreign commits.
if ($need) {
$subpath = $repository->getDetail('svn-subpath');
if (!$subpath) {
- $commits = implode(', ', $need);
throw new Exception(
- "Missing commits ({$need}) in a SVN repository which is not ".
- "configured for subdirectory-only parsing!");
+ pht(
+ 'Missing commits (%s) in a SVN repository which is not '.
+ 'configured for subdirectory-only parsing!',
+ implode(', ', $need)));
}
foreach ($need as $foreign_commit) {
$commit = new PhabricatorRepositoryCommit();
$commit->setRepositoryID($repository->getID());
$commit->setCommitIdentifier($foreign_commit);
$commit->setEpoch(0);
// Mark this commit as imported so it doesn't prevent the repository
// from transitioning into the "Imported" state.
$commit->setImportStatus(PhabricatorRepositoryCommit::IMPORTED_ALL);
$commit->save();
$data = new PhabricatorRepositoryCommitData();
$data->setCommitID($commit->getID());
$data->setAuthorName('');
$data->setCommitMessage('');
$data->setCommitDetails(
array(
'foreign-svn-stub' => true,
// Denormalize this to make it easier to debug cases where someone
// did half a parse and then changed the subdirectory or something
// like that.
'svn-subpath' => $subpath,
));
$data->save();
$commit_map[$foreign_commit] = $commit->getID();
}
}
return $commit_map;
}
private function lookupPathFileType(
PhabricatorRepository $repository,
$path,
array $path_info) {
$result = $this->lookupPathFileTypes(
$repository,
array(
$path => $path_info,
));
return $result[$path];
}
private function lookupPathFileTypes(
PhabricatorRepository $repository,
array $paths) {
$result_map = array();
$repository_uri = $repository->getSubversionPathURI();
if (isset($paths['/'])) {
$result_map['/'] = DifferentialChangeType::FILE_DIRECTORY;
unset($paths['/']);
}
$parents = array();
$path_mapping = array();
foreach ($paths as $path => $lookup) {
$parent = dirname($lookup['rawPath']);
$parent = $repository->getSubversionPathURI(
$parent,
$lookup['rawCommit']);
$parent = escapeshellarg($parent);
$parents[$parent] = true;
$path_mapping[$parent][] = dirname($path);
}
// Reverse this list so we can pop $path_mapping, as that's more efficient
// than shifting it. We need to associate these maps positionally because
// a change can copy the same source path from multiple revisions via
// "svn cp path@1 a; svn cp path@2 b;" and the XML output gives us no way
// to distinguish which revision we're looking at except based on its
// position in the document.
$all_paths = array_reverse(array_keys($parents));
foreach (array_chunk($all_paths, 64) as $path_chunk) {
list($raw_xml) = $repository->execxRemoteCommand(
'--xml ls %C',
implode(' ', $path_chunk));
$xml = new SimpleXMLElement($raw_xml);
foreach ($xml->list as $list) {
$list_path = (string)$list['path'];
// SVN is a big mess. See Facebook rG8 (a revision which adds files
// with spaces in their names) for an example.
$list_path = rawurldecode($list_path);
if ($list_path == $repository_uri) {
$base = '/';
} else {
$base = substr($list_path, strlen($repository_uri));
}
$mapping = array_pop($path_mapping);
foreach ($list->entry as $entry) {
$val = $this->getFileTypeFromSVNKind($entry['kind']);
foreach ($mapping as $base_path) {
// rtrim() causes us to handle top-level directories correctly.
$key = rtrim($base_path, '/').'/'.$entry->name;
$result_map[$key] = $val;
}
}
}
}
foreach ($paths as $path => $lookup) {
if (empty($result_map[$path])) {
$result_map[$path] = DifferentialChangeType::FILE_DELETED;
}
}
return $result_map;
}
private function getFileTypeFromSVNKind($kind) {
$kind = (string)$kind;
switch ($kind) {
case 'dir': return DifferentialChangeType::FILE_DIRECTORY;
case 'file': return DifferentialChangeType::FILE_NORMAL;
default:
- throw new Exception("Unknown SVN file kind '{$kind}'.");
+ throw new Exception(pht("Unknown SVN file kind '%s'.", $kind));
}
}
private function lookupRecursiveFileList(
PhabricatorRepository $repository,
array $info) {
$path = $info['rawPath'];
$rev = $info['rawCommit'];
$path_uri = $repository->getSubversionPathURI($path, $rev);
$hashkey = md5($path_uri);
// This method is quite horrible. The underlying challenge is that some
// commits in the Facebook repository are enormous, taking multiple hours
// to 'ls -R' out of the repository and producing XML files >1GB in size.
// If we try to SimpleXML them, the object exhausts available memory on a
// 64G machine. Instead, cache the XML output and then parse it line by line
// to limit space requirements.
$cache_loc = sys_get_temp_dir().'/diffusion.'.$hashkey.'.svnls';
if (!Filesystem::pathExists($cache_loc)) {
$tmp = new TempFile();
$repository->execxRemoteCommand(
'--xml ls -R %s > %s',
$path_uri,
$tmp);
execx(
'mv %s %s',
$tmp,
$cache_loc);
}
$map = $this->parseRecursiveListFileData($cache_loc);
Filesystem::remove($cache_loc);
return $map;
}
private function parseRecursiveListFileData($file_path) {
$map = array();
$mode = 'xml';
$done = false;
$entry = null;
foreach (new LinesOfALargeFile($file_path) as $lno => $line) {
switch ($mode) {
case 'entry':
if ($line == '</entry>') {
$entry = implode('', $entry);
$pattern = '@^\s+kind="(file|dir)">'.
'<name>(.*?)</name>'.
'(<size>(.*?)</size>)?@';
$matches = null;
if (!preg_match($pattern, $entry, $matches)) {
- throw new Exception('Unable to parse entry!');
+ throw new Exception(pht('Unable to parse entry!'));
}
$map[html_entity_decode($matches[2])] =
$this->getFileTypeFromSVNKind($matches[1]);
$mode = 'entry-or-end';
} else {
$entry[] = $line;
}
break;
case 'entry-or-end':
if ($line == '</list>') {
$done = true;
break 2;
} else if ($line == '<entry') {
$mode = 'entry';
$entry = array();
} else {
- throw new Exception("Expected </list> or <entry, got {$line}.");
+ throw new Exception(
+ pht(
+ 'Expected %s or %s, got %s.',
+ '</list>',
+ '<entry',
+ $line));
}
break;
case 'xml':
$expect = '/<?xml version="1.0".*?>/';
if (!preg_match($expect, $line)) {
- throw new Exception("Expected '{$expect}', got {$line}.");
+ throw new Exception(
+ pht(
+ "Expected '%s', got %s.",
+ $expect,
+ $line));
}
$mode = 'list';
break;
case 'list':
$expect = '<lists>';
if ($line !== $expect) {
- throw new Exception("Expected '{$expect}', got {$line}.");
+ throw new Exception(
+ pht(
+ "Expected '%s', got %s.",
+ $expect,
+ $line));
}
$mode = 'list1';
break;
case 'list1':
$expect = '<list';
if ($line !== $expect) {
- throw new Exception("Expected '{$expect}', got {$line}.");
+ throw new Exception(
+ pht(
+ "Expected '%s', got %s.",
+ $expect,
+ $line));
}
$mode = 'list2';
break;
case 'list2':
if (!preg_match('/^\s+path="/', $line)) {
- throw new Exception("Expected ' path=...', got {$line}.");
+ throw new Exception(
+ pht(
+ "Expected '%s', got %s.",
+ ' path=...',
+ $line));
}
$mode = 'entry-or-end';
break;
}
}
if (!$done) {
- throw new Exception('Unexpected end of file.');
+ throw new Exception(pht('Unexpected end of file.'));
}
return $map;
}
// TODO: Replace with DiffusionPathIDQuery::getParentPath().
private function getParentPath($path) {
$path = rtrim($path, '/');
$path = dirname($path);
if (!$path) {
$path = '/';
}
return $path;
}
// TODO: Replace with DiffusionPathIDQuery::expandPathToRoot().
private function expandAllParentPaths($path, $include_self = false) {
$parents = array();
if ($include_self) {
$parents[] = '/'.rtrim($path, '/');
}
$parts = explode('/', trim($path, '/'));
while (count($parts) >= 1) {
array_pop($parts);
$parents[] = '/'.implode('/', $parts);
}
return $parents;
}
private function getSVNLogXMLObject(
PhabricatorRepository $repository,
$uri,
$revision) {
list($xml) = $repository->execxRemoteCommand(
'log --xml --verbose --limit 1 %s@%d',
$uri,
$revision);
// Subversion may send us back commit messages which won't parse because
// they have non UTF-8 garbage in them. Slam them into valid UTF-8.
$xml = phutil_utf8ize($xml);
return new SimpleXMLElement($xml);
}
}
diff --git a/src/applications/repository/worker/commitmessageparser/PhabricatorRepositoryCommitMessageParserWorker.php b/src/applications/repository/worker/commitmessageparser/PhabricatorRepositoryCommitMessageParserWorker.php
index 89fa5f34f..c93810cb0 100644
--- a/src/applications/repository/worker/commitmessageparser/PhabricatorRepositoryCommitMessageParserWorker.php
+++ b/src/applications/repository/worker/commitmessageparser/PhabricatorRepositoryCommitMessageParserWorker.php
@@ -1,544 +1,545 @@
<?php
abstract class PhabricatorRepositoryCommitMessageParserWorker
extends PhabricatorRepositoryCommitParserWorker {
abstract protected function parseCommitWithRef(
PhabricatorRepository $repository,
PhabricatorRepositoryCommit $commit,
DiffusionCommitRef $ref);
final protected function parseCommit(
PhabricatorRepository $repository,
PhabricatorRepositoryCommit $commit) {
$viewer = PhabricatorUser::getOmnipotentUser();
$refs_raw = DiffusionQuery::callConduitWithDiffusionRequest(
$viewer,
DiffusionRequest::newFromDictionary(
array(
'repository' => $repository,
'user' => $viewer,
)),
'diffusion.querycommits',
array(
'phids' => array($commit->getPHID()),
'bypassCache' => true,
'needMessages' => true,
));
if (empty($refs_raw['data'])) {
throw new Exception(
pht(
'Unable to retrieve details for commit "%s"!',
$commit->getPHID()));
}
$ref = DiffusionCommitRef::newFromConduitResult(head($refs_raw['data']));
$this->parseCommitWithRef($repository, $commit, $ref);
}
final protected function updateCommitData(DiffusionCommitRef $ref) {
$commit = $this->commit;
$author = $ref->getAuthor();
$message = $ref->getMessage();
$committer = $ref->getCommitter();
$hashes = $ref->getHashes();
$data = id(new PhabricatorRepositoryCommitData())->loadOneWhere(
'commitID = %d',
$commit->getID());
if (!$data) {
$data = new PhabricatorRepositoryCommitData();
}
$data->setCommitID($commit->getID());
$data->setAuthorName(id(new PhutilUTF8StringTruncator())
->setMaximumBytes(255)
->truncateString((string)$author));
$data->setCommitDetail('authorName', $ref->getAuthorName());
$data->setCommitDetail('authorEmail', $ref->getAuthorEmail());
$data->setCommitDetail(
'authorPHID',
$this->resolveUserPHID($commit, $author));
$data->setCommitMessage($message);
if (strlen($committer)) {
$data->setCommitDetail('committer', $committer);
$data->setCommitDetail('committerName', $ref->getCommitterName());
$data->setCommitDetail('committerEmail', $ref->getCommitterEmail());
$data->setCommitDetail(
'committerPHID',
$this->resolveUserPHID($commit, $committer));
}
$repository = $this->repository;
$author_phid = $data->getCommitDetail('authorPHID');
$committer_phid = $data->getCommitDetail('committerPHID');
$user = new PhabricatorUser();
if ($author_phid) {
$user = $user->loadOneWhere(
'phid = %s',
$author_phid);
}
$differential_app = 'PhabricatorDifferentialApplication';
$revision_id = null;
$low_level_query = null;
if (PhabricatorApplication::isClassInstalled($differential_app)) {
$low_level_query = id(new DiffusionLowLevelCommitFieldsQuery())
->setRepository($repository)
->withCommitRef($ref);
$field_values = $low_level_query->execute();
$revision_id = idx($field_values, 'revisionID');
if (!empty($field_values['reviewedByPHIDs'])) {
$data->setCommitDetail(
'reviewerPHID',
reset($field_values['reviewedByPHIDs']));
}
$data->setCommitDetail('differential.revisionID', $revision_id);
}
if ($author_phid != $commit->getAuthorPHID()) {
$commit->setAuthorPHID($author_phid);
}
$commit->setSummary($data->getSummary());
$commit->save();
// Figure out if we're going to try to "autoclose" related objects (e.g.,
// close linked tasks and related revisions) and, if not, record why we
// aren't. Autoclose can be disabled for various reasons at the repository
// or commit levels.
$force_autoclose = idx($this->getTaskData(), 'forceAutoclose', false);
if ($force_autoclose) {
$autoclose_reason = PhabricatorRepository::BECAUSE_AUTOCLOSE_FORCED;
} else {
$autoclose_reason = $repository->shouldSkipAutocloseCommit($commit);
}
$data->setCommitDetail('autocloseReason', $autoclose_reason);
$should_autoclose = $force_autoclose ||
$repository->shouldAutocloseCommit($commit);
// When updating related objects, we'll act under an omnipotent user to
// ensure we can see them, but take actions as either the committer or
// author (if we recognize their accounts) or the Diffusion application
// (if we do not).
$actor = PhabricatorUser::getOmnipotentUser();
$acting_as_phid = nonempty(
$committer_phid,
$author_phid,
id(new PhabricatorDiffusionApplication())->getPHID());
$conn_w = id(new DifferentialRevision())->establishConnection('w');
// NOTE: The `differential_commit` table has a unique ID on `commitPHID`,
// preventing more than one revision from being associated with a commit.
// Generally this is good and desirable, but with the advent of hash
// tracking we may end up in a situation where we match several different
// revisions. We just kind of ignore this and pick one, we might want to
// revisit this and do something differently. (If we match several revisions
// someone probably did something very silly, though.)
$revision = null;
if ($revision_id) {
$revision_query = id(new DifferentialRevisionQuery())
->withIDs(array($revision_id))
->setViewer($actor)
->needReviewerStatus(true)
->needActiveDiffs(true);
$revision = $revision_query->executeOne();
if ($revision) {
if (!$data->getCommitDetail('precommitRevisionStatus')) {
$data->setCommitDetail(
'precommitRevisionStatus',
$revision->getStatus());
}
$commit_drev = DiffusionCommitHasRevisionEdgeType::EDGECONST;
id(new PhabricatorEdgeEditor())
->addEdge($commit->getPHID(), $commit_drev, $revision->getPHID())
->save();
queryfx(
$conn_w,
'INSERT IGNORE INTO %T (revisionID, commitPHID) VALUES (%d, %s)',
DifferentialRevision::TABLE_COMMIT,
$revision->getID(),
$commit->getPHID());
$status_closed = ArcanistDifferentialRevisionStatus::CLOSED;
$should_close = ($revision->getStatus() != $status_closed) &&
$should_autoclose;
if ($should_close) {
$commit_close_xaction = id(new DifferentialTransaction())
->setTransactionType(DifferentialTransaction::TYPE_ACTION)
->setNewValue(DifferentialAction::ACTION_CLOSE)
->setMetadataValue('isCommitClose', true);
$commit_close_xaction->setMetadataValue(
'commitPHID',
$commit->getPHID());
$commit_close_xaction->setMetadataValue(
'committerPHID',
$committer_phid);
$commit_close_xaction->setMetadataValue(
'committerName',
$data->getCommitDetail('committer'));
$commit_close_xaction->setMetadataValue(
'authorPHID',
$author_phid);
$commit_close_xaction->setMetadataValue(
'authorName',
$data->getAuthorName());
if ($low_level_query) {
$commit_close_xaction->setMetadataValue(
'revisionMatchData',
$low_level_query->getRevisionMatchData());
$data->setCommitDetail(
'revisionMatchData',
$low_level_query->getRevisionMatchData());
}
$diff = $this->generateFinalDiff($revision, $acting_as_phid);
$vs_diff = $this->loadChangedByCommit($revision, $diff);
$changed_uri = null;
if ($vs_diff) {
$data->setCommitDetail('vsDiff', $vs_diff->getID());
$changed_uri = PhabricatorEnv::getProductionURI(
'/D'.$revision->getID().
'?vs='.$vs_diff->getID().
'&id='.$diff->getID().
'#toc');
}
$xactions = array();
$xactions[] = id(new DifferentialTransaction())
->setTransactionType(DifferentialTransaction::TYPE_UPDATE)
->setIgnoreOnNoEffect(true)
->setNewValue($diff->getPHID())
->setMetadataValue('isCommitUpdate', true);
$xactions[] = $commit_close_xaction;
$content_source = PhabricatorContentSource::newForSource(
PhabricatorContentSource::SOURCE_DAEMON,
array());
$editor = id(new DifferentialTransactionEditor())
->setActor($actor)
->setActingAsPHID($acting_as_phid)
->setContinueOnMissingFields(true)
->setContentSource($content_source)
->setChangedPriorToCommitURI($changed_uri)
->setIsCloseByCommit(true);
try {
$editor->applyTransactions($revision, $xactions);
} catch (PhabricatorApplicationTransactionNoEffectException $ex) {
// NOTE: We've marked transactions other than the CLOSE transaction
// as ignored when they don't have an effect, so this means that we
// lost a race to close the revision. That's perfectly fine, we can
// just continue normally.
}
}
}
}
if ($should_autoclose) {
$this->closeTasks(
$actor,
$acting_as_phid,
$repository,
$commit,
$message);
}
$data->save();
$commit->writeImportStatusFlag(
PhabricatorRepositoryCommit::IMPORTED_MESSAGE);
}
private function generateFinalDiff(
DifferentialRevision $revision,
$actor_phid) {
$viewer = PhabricatorUser::getOmnipotentUser();
$drequest = DiffusionRequest::newFromDictionary(array(
'user' => $viewer,
'repository' => $this->repository,
));
$raw_diff = DiffusionQuery::callConduitWithDiffusionRequest(
$viewer,
$drequest,
'diffusion.rawdiffquery',
array(
'commit' => $this->commit->getCommitIdentifier(),
));
// TODO: Support adds, deletes and moves under SVN.
if (strlen($raw_diff)) {
$changes = id(new ArcanistDiffParser())->parseDiff($raw_diff);
} else {
// This is an empty diff, maybe made with `git commit --allow-empty`.
// NOTE: These diffs have the same tree hash as their ancestors, so
// they may attach to revisions in an unexpected way. Just let this
// happen for now, although it might make sense to special case it
// eventually.
$changes = array();
}
$diff = DifferentialDiff::newFromRawChanges($viewer, $changes)
->setRepositoryPHID($this->repository->getPHID())
->setAuthorPHID($actor_phid)
->setCreationMethod('commit')
->setSourceControlSystem($this->repository->getVersionControlSystem())
->setLintStatus(DifferentialLintStatus::LINT_AUTO_SKIP)
->setUnitStatus(DifferentialUnitStatus::UNIT_AUTO_SKIP)
->setDateCreated($this->commit->getEpoch())
->setDescription(
- 'Commit r'.
- $this->repository->getCallsign().
- $this->commit->getCommitIdentifier());
+ pht(
+ 'Commit %s',
+ 'r'.$this->repository->getCallsign().
+ $this->commit->getCommitIdentifier()));
$parents = DiffusionQuery::callConduitWithDiffusionRequest(
$viewer,
$drequest,
'diffusion.commitparentsquery',
array(
'commit' => $this->commit->getCommitIdentifier(),
));
if ($parents) {
$diff->setSourceControlBaseRevision(head($parents));
}
// TODO: Attach binary files.
return $diff->save();
}
private function loadChangedByCommit(
DifferentialRevision $revision,
DifferentialDiff $diff) {
$repository = $this->repository;
$vs_diff = id(new DifferentialDiffQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withRevisionIDs(array($revision->getID()))
->needChangesets(true)
->setLimit(1)
->executeOne();
if (!$vs_diff) {
return null;
}
if ($vs_diff->getCreationMethod() == 'commit') {
return null;
}
$vs_changesets = array();
foreach ($vs_diff->getChangesets() as $changeset) {
$path = $changeset->getAbsoluteRepositoryPath($repository, $vs_diff);
$path = ltrim($path, '/');
$vs_changesets[$path] = $changeset;
}
$changesets = array();
foreach ($diff->getChangesets() as $changeset) {
$path = $changeset->getAbsoluteRepositoryPath($repository, $diff);
$path = ltrim($path, '/');
$changesets[$path] = $changeset;
}
if (array_fill_keys(array_keys($changesets), true) !=
array_fill_keys(array_keys($vs_changesets), true)) {
return $vs_diff;
}
$file_phids = array();
foreach ($vs_changesets as $changeset) {
$metadata = $changeset->getMetadata();
$file_phid = idx($metadata, 'new:binary-phid');
if ($file_phid) {
$file_phids[$file_phid] = $file_phid;
}
}
$files = array();
if ($file_phids) {
$files = id(new PhabricatorFileQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withPHIDs($file_phids)
->execute();
$files = mpull($files, null, 'getPHID');
}
foreach ($changesets as $path => $changeset) {
$vs_changeset = $vs_changesets[$path];
$file_phid = idx($vs_changeset->getMetadata(), 'new:binary-phid');
if ($file_phid) {
if (!isset($files[$file_phid])) {
return $vs_diff;
}
$drequest = DiffusionRequest::newFromDictionary(array(
'user' => PhabricatorUser::getOmnipotentUser(),
'repository' => $this->repository,
'commit' => $this->commit->getCommitIdentifier(),
'path' => $path,
));
$corpus = DiffusionFileContentQuery::newFromDiffusionRequest($drequest)
->setViewer(PhabricatorUser::getOmnipotentUser())
->loadFileContent()
->getCorpus();
if ($files[$file_phid]->loadFileData() != $corpus) {
return $vs_diff;
}
} else {
$context = implode("\n", $changeset->makeChangesWithContext());
$vs_context = implode("\n", $vs_changeset->makeChangesWithContext());
// We couldn't just compare $context and $vs_context because following
// diffs will be considered different:
//
// -(empty line)
// -echo 'test';
// (empty line)
//
// (empty line)
// -echo "test";
// -(empty line)
$hunk = id(new DifferentialModernHunk())->setChanges($context);
$vs_hunk = id(new DifferentialModernHunk())->setChanges($vs_context);
if ($hunk->makeOldFile() != $vs_hunk->makeOldFile() ||
$hunk->makeNewFile() != $vs_hunk->makeNewFile()) {
return $vs_diff;
}
}
}
return null;
}
private function resolveUserPHID(
PhabricatorRepositoryCommit $commit,
$user_name) {
return id(new DiffusionResolveUserQuery())
->withCommit($commit)
->withName($user_name)
->execute();
}
private function closeTasks(
PhabricatorUser $actor,
$acting_as,
PhabricatorRepository $repository,
PhabricatorRepositoryCommit $commit,
$message) {
$maniphest = 'PhabricatorManiphestApplication';
if (!PhabricatorApplication::isClassInstalled($maniphest)) {
return;
}
$prefixes = ManiphestTaskStatus::getStatusPrefixMap();
$suffixes = ManiphestTaskStatus::getStatusSuffixMap();
$matches = id(new ManiphestCustomFieldStatusParser())
->parseCorpus($message);
$task_statuses = array();
foreach ($matches as $match) {
$prefix = phutil_utf8_strtolower($match['prefix']);
$suffix = phutil_utf8_strtolower($match['suffix']);
$status = idx($suffixes, $suffix);
if (!$status) {
$status = idx($prefixes, $prefix);
}
foreach ($match['monograms'] as $task_monogram) {
$task_id = (int)trim($task_monogram, 'tT');
$task_statuses[$task_id] = $status;
}
}
if (!$task_statuses) {
return;
}
$tasks = id(new ManiphestTaskQuery())
->setViewer($actor)
->withIDs(array_keys($task_statuses))
->needProjectPHIDs(true)
->execute();
foreach ($tasks as $task_id => $task) {
$xactions = array();
$edge_type = ManiphestTaskHasCommitEdgeType::EDGECONST;
$edge_xaction = id(new ManiphestTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
->setMetadataValue('edge:type', $edge_type)
->setNewValue(
array(
'+' => array(
$commit->getPHID() => $commit->getPHID(),
),
));
$status = $task_statuses[$task_id];
if ($status) {
if ($task->getStatus() != $status) {
$xactions[] = id(new ManiphestTransaction())
->setTransactionType(ManiphestTransaction::TYPE_STATUS)
->setMetadataValue('commitPHID', $commit->getPHID())
->setNewValue($status);
$edge_xaction->setMetadataValue('commitPHID', $commit->getPHID());
}
}
$xactions[] = $edge_xaction;
$content_source = PhabricatorContentSource::newForSource(
PhabricatorContentSource::SOURCE_DAEMON,
array());
$editor = id(new ManiphestTransactionEditor())
->setActor($actor)
->setActingAsPHID($acting_as)
->setContinueOnNoEffect(true)
->setContinueOnMissingFields(true)
->setUnmentionablePHIDMap(
array($commit->getPHID() => $commit->getPHID()))
->setContentSource($content_source);
$editor->applyTransactions($task, $xactions);
}
}
}
diff --git a/src/applications/search/controller/PhabricatorSearchAttachController.php b/src/applications/search/controller/PhabricatorSearchAttachController.php
index b2e0a92b9..2d00ac8bd 100644
--- a/src/applications/search/controller/PhabricatorSearchAttachController.php
+++ b/src/applications/search/controller/PhabricatorSearchAttachController.php
@@ -1,324 +1,325 @@
<?php
final class PhabricatorSearchAttachController
extends PhabricatorSearchBaseController {
public function handleRequest(AphrontRequest $request) {
$user = $request->getUser();
$phid = $request->getURIData('phid');
$attach_type = $request->getURIData('type');
$action = $request->getURIData('action', self::ACTION_ATTACH);
$handle = id(new PhabricatorHandleQuery())
->setViewer($user)
->withPHIDs(array($phid))
->executeOne();
$object_type = $handle->getType();
$object = id(new PhabricatorObjectQuery())
->setViewer($user)
->withPHIDs(array($phid))
->executeOne();
if (!$object) {
return new Aphront404Response();
}
$edge_type = null;
switch ($action) {
case self::ACTION_EDGE:
case self::ACTION_DEPENDENCIES:
case self::ACTION_BLOCKS:
case self::ACTION_ATTACH:
$edge_type = $this->getEdgeType($object_type, $attach_type);
break;
}
if ($request->isFormPost()) {
$phids = explode(';', $request->getStr('phids'));
$phids = array_filter($phids);
$phids = array_values($phids);
if ($edge_type) {
if (!$object instanceof PhabricatorApplicationTransactionInterface) {
throw new Exception(
pht(
'Expected object ("%s") to implement interface "%s".',
get_class($object),
'PhabricatorApplicationTransactionInterface'));
}
$old_phids = PhabricatorEdgeQuery::loadDestinationPHIDs(
$phid,
$edge_type);
$add_phids = $phids;
$rem_phids = array_diff($old_phids, $add_phids);
$txn_editor = $object->getApplicationTransactionEditor()
->setActor($user)
->setContentSourceFromRequest($request)
->setContinueOnMissingFields(true)
->setContinueOnNoEffect(true);
$txn_template = $object->getApplicationTransactionTemplate()
->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
->setMetadataValue('edge:type', $edge_type)
->setNewValue(array(
'+' => array_fuse($add_phids),
'-' => array_fuse($rem_phids),
));
try {
$txn_editor->applyTransactions(
$object->getApplicationTransactionObject(),
array($txn_template));
} catch (PhabricatorEdgeCycleException $ex) {
$this->raiseGraphCycleException($ex);
}
return id(new AphrontReloadResponse())->setURI($handle->getURI());
} else {
return $this->performMerge($object, $handle, $phids);
}
} else {
if ($edge_type) {
$phids = PhabricatorEdgeQuery::loadDestinationPHIDs(
$phid,
$edge_type);
} else {
// This is a merge.
$phids = array();
}
}
$strings = $this->getStrings($attach_type, $action);
$handles = $this->loadViewerHandles($phids);
$obj_dialog = new PhabricatorObjectSelectorDialog();
$obj_dialog
->setUser($user)
->setHandles($handles)
->setFilters($this->getFilters($strings, $attach_type))
->setSelectedFilter($strings['selected'])
->setExcluded($phid)
->setCancelURI($handle->getURI())
->setSearchURI('/search/select/'.$attach_type.'/'.$action.'/')
->setTitle($strings['title'])
->setHeader($strings['header'])
->setButtonText($strings['button'])
->setInstructions($strings['instructions']);
$dialog = $obj_dialog->buildDialog();
return id(new AphrontDialogResponse())->setDialog($dialog);
}
private function performMerge(
ManiphestTask $task,
PhabricatorObjectHandle $handle,
array $phids) {
$user = $this->getRequest()->getUser();
$response = id(new AphrontReloadResponse())->setURI($handle->getURI());
$phids = array_fill_keys($phids, true);
unset($phids[$task->getPHID()]); // Prevent merging a task into itself.
if (!$phids) {
return $response;
}
$targets = id(new ManiphestTaskQuery())
->setViewer($user)
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->withPHIDs(array_keys($phids))
->needSubscriberPHIDs(true)
->needProjectPHIDs(true)
->execute();
if (empty($targets)) {
return $response;
}
$editor = id(new ManiphestTransactionEditor())
->setActor($user)
->setContentSourceFromRequest($this->getRequest())
->setContinueOnNoEffect(true)
->setContinueOnMissingFields(true);
$cc_vector = array();
// since we loaded this via a generic object query, go ahead and get the
// attach the subscriber and project phids now
$task->attachSubscriberPHIDs(
PhabricatorSubscribersQuery::loadSubscribersForPHID($task->getPHID()));
$task->attachProjectPHIDs(
PhabricatorEdgeQuery::loadDestinationPHIDs($task->getPHID(),
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST));
$cc_vector[] = $task->getSubscriberPHIDs();
foreach ($targets as $target) {
$cc_vector[] = $target->getSubscriberPHIDs();
$cc_vector[] = array(
$target->getAuthorPHID(),
$target->getOwnerPHID(),
);
$merged_into_txn = id(new ManiphestTransaction())
->setTransactionType(ManiphestTransaction::TYPE_MERGED_INTO)
->setNewValue($task->getPHID());
$editor->applyTransactions(
$target,
array($merged_into_txn));
}
$all_ccs = array_mergev($cc_vector);
$all_ccs = array_filter($all_ccs);
$all_ccs = array_unique($all_ccs);
$add_ccs = id(new ManiphestTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS)
->setNewValue(array('=' => $all_ccs));
$merged_from_txn = id(new ManiphestTransaction())
->setTransactionType(ManiphestTransaction::TYPE_MERGED_FROM)
->setNewValue(mpull($targets, 'getPHID'));
$editor->applyTransactions(
$task,
array($add_ccs, $merged_from_txn));
return $response;
}
private function getStrings($attach_type, $action) {
switch ($attach_type) {
case DifferentialRevisionPHIDType::TYPECONST:
- $noun = 'Revisions';
- $selected = 'created';
+ $noun = pht('Revisions');
+ $selected = pht('created');
break;
case ManiphestTaskPHIDType::TYPECONST:
- $noun = 'Tasks';
- $selected = 'assigned';
+ $noun = pht('Tasks');
+ $selected = pht('assigned');
break;
case PhabricatorRepositoryCommitPHIDType::TYPECONST:
- $noun = 'Commits';
- $selected = 'created';
+ $noun = pht('Commits');
+ $selected = pht('created');
break;
case PholioMockPHIDType::TYPECONST:
- $noun = 'Mocks';
- $selected = 'created';
+ $noun = pht('Mocks');
+ $selected = pht('created');
break;
}
switch ($action) {
case self::ACTION_EDGE:
case self::ACTION_ATTACH:
- $dialog_title = "Manage Attached {$noun}";
- $header_text = "Currently Attached {$noun}";
- $button_text = "Save {$noun}";
+ $dialog_title = pht('Manage Attached %s', $noun);
+ $header_text = pht('Currently Attached %s', $noun);
+ $button_text = pht('Save %s', $noun);
$instructions = null;
break;
case self::ACTION_MERGE:
- $dialog_title = 'Merge Duplicate Tasks';
- $header_text = 'Tasks To Merge';
- $button_text = "Merge {$noun}";
- $instructions =
+ $dialog_title = pht('Merge Duplicate Tasks');
+ $header_text = pht('Tasks To Merge');
+ $button_text = pht('Merge %s', $noun);
+ $instructions = pht(
'These tasks will be merged into the current task and then closed. '.
- 'The current task will grow stronger.';
+ 'The current task will grow stronger.');
break;
case self::ACTION_DEPENDENCIES:
- $dialog_title = 'Edit Dependencies';
- $header_text = 'Current Dependencies';
- $button_text = 'Save Dependencies';
+ $dialog_title = pht('Edit Dependencies');
+ $header_text = pht('Current Dependencies');
+ $button_text = pht('Save Dependencies');
$instructions = null;
break;
case self::ACTION_BLOCKS:
$dialog_title = pht('Edit Blocking Tasks');
$header_text = pht('Current Blocking Tasks');
$button_text = pht('Save Blocking Tasks');
$instructions = null;
break;
}
return array(
'target_plural_noun' => $noun,
'selected' => $selected,
'title' => $dialog_title,
'header' => $header_text,
'button' => $button_text,
'instructions' => $instructions,
);
}
private function getFilters(array $strings, $attach_type) {
if ($attach_type == PholioMockPHIDType::TYPECONST) {
$filters = array(
- 'created' => 'Created By Me',
- 'all' => 'All '.$strings['target_plural_noun'],
+ 'created' => pht('Created By Me'),
+ 'all' => pht('All %s', $strings['target_plural_noun']),
);
} else {
$filters = array(
- 'assigned' => 'Assigned to Me',
- 'created' => 'Created By Me',
- 'open' => 'All Open '.$strings['target_plural_noun'],
- 'all' => 'All '.$strings['target_plural_noun'],
+ 'assigned' => pht('Assigned to Me'),
+ 'created' => pht('Created By Me'),
+ 'open' => pht('All Open %s', $strings['target_plural_noun']),
+ 'all' => pht('All %s', $strings['target_plural_noun']),
);
}
return $filters;
}
private function getEdgeType($src_type, $dst_type) {
$t_cmit = PhabricatorRepositoryCommitPHIDType::TYPECONST;
$t_task = ManiphestTaskPHIDType::TYPECONST;
$t_drev = DifferentialRevisionPHIDType::TYPECONST;
$t_mock = PholioMockPHIDType::TYPECONST;
$map = array(
$t_cmit => array(
$t_task => DiffusionCommitHasTaskEdgeType::EDGECONST,
),
$t_task => array(
$t_cmit => ManiphestTaskHasCommitEdgeType::EDGECONST,
$t_task => ManiphestTaskDependsOnTaskEdgeType::EDGECONST,
$t_drev => ManiphestTaskHasRevisionEdgeType::EDGECONST,
$t_mock => ManiphestTaskHasMockEdgeType::EDGECONST,
),
$t_drev => array(
$t_drev => DifferentialRevisionDependsOnRevisionEdgeType::EDGECONST,
$t_task => DifferentialRevisionHasTaskEdgeType::EDGECONST,
),
$t_mock => array(
$t_task => PholioMockHasTaskEdgeType::EDGECONST,
),
);
if (empty($map[$src_type][$dst_type])) {
return null;
}
return $map[$src_type][$dst_type];
}
private function raiseGraphCycleException(PhabricatorEdgeCycleException $ex) {
$cycle = $ex->getCycle();
$handles = $this->loadViewerHandles($cycle);
$names = array();
foreach ($cycle as $cycle_phid) {
$names[] = $handles[$cycle_phid]->getFullName();
}
- $names = implode(" \xE2\x86\x92 ", $names);
throw new Exception(
- "You can not create that dependency, because it would create a ".
- "circular dependency: {$names}.");
+ pht(
+ 'You can not create that dependency, because it would create a '.
+ 'circular dependency: %s.',
+ implode(" \xE2\x86\x92 ", $names)));
}
}
diff --git a/src/applications/search/engine/PhabricatorApplicationSearchEngine.php b/src/applications/search/engine/PhabricatorApplicationSearchEngine.php
index cd0a9655f..e89e82afd 100644
--- a/src/applications/search/engine/PhabricatorApplicationSearchEngine.php
+++ b/src/applications/search/engine/PhabricatorApplicationSearchEngine.php
@@ -1,1031 +1,1031 @@
<?php
/**
* Represents an abstract search engine for an application. It supports
* creating and storing saved queries.
*
* @task construct Constructing Engines
* @task app Applications
* @task builtin Builtin Queries
* @task uri Query URIs
* @task dates Date Filters
* @task order Result Ordering
* @task read Reading Utilities
* @task exec Paging and Executing Queries
* @task render Rendering Results
*/
abstract class PhabricatorApplicationSearchEngine {
private $application;
private $viewer;
private $errors = array();
private $customFields = false;
private $request;
private $context;
const CONTEXT_LIST = 'list';
const CONTEXT_PANEL = 'panel';
public function setViewer(PhabricatorUser $viewer) {
$this->viewer = $viewer;
return $this;
}
protected function requireViewer() {
if (!$this->viewer) {
throw new PhutilInvalidStateException('setViewer');
}
return $this->viewer;
}
public function setContext($context) {
$this->context = $context;
return $this;
}
public function isPanelContext() {
return ($this->context == self::CONTEXT_PANEL);
}
public function canUseInPanelContext() {
return true;
}
public function saveQuery(PhabricatorSavedQuery $query) {
$query->setEngineClassName(get_class($this));
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
try {
$query->save();
} catch (AphrontDuplicateKeyQueryException $ex) {
// Ignore, this is just a repeated search.
}
unset($unguarded);
}
/**
* Create a saved query object from the request.
*
* @param AphrontRequest The search request.
* @return PhabricatorSavedQuery
*/
abstract public function buildSavedQueryFromRequest(
AphrontRequest $request);
/**
* Executes the saved query.
*
* @param PhabricatorSavedQuery The saved query to operate on.
* @return The result of the query.
*/
abstract public function buildQueryFromSavedQuery(
PhabricatorSavedQuery $saved);
/**
* Builds the search form using the request.
*
* @param AphrontFormView Form to populate.
* @param PhabricatorSavedQuery The query from which to build the form.
* @return void
*/
abstract public function buildSearchForm(
AphrontFormView $form,
PhabricatorSavedQuery $query);
public function getErrors() {
return $this->errors;
}
public function addError($error) {
$this->errors[] = $error;
return $this;
}
/**
* Return an application URI corresponding to the results page of a query.
* Normally, this is something like `/application/query/QUERYKEY/`.
*
* @param string The query key to build a URI for.
* @return string URI where the query can be executed.
* @task uri
*/
public function getQueryResultsPageURI($query_key) {
return $this->getURI('query/'.$query_key.'/');
}
/**
* Return an application URI for query management. This is used when, e.g.,
* a query deletion operation is cancelled.
*
* @return string URI where queries can be managed.
* @task uri
*/
public function getQueryManagementURI() {
return $this->getURI('query/edit/');
}
/**
* Return the URI to a path within the application. Used to construct default
* URIs for management and results.
*
* @return string URI to path.
* @task uri
*/
abstract protected function getURI($path);
/**
* Return a human readable description of the type of objects this query
* searches for.
*
* For example, "Tasks" or "Commits".
*
* @return string Human-readable description of what this engine is used to
* find.
*/
abstract public function getResultTypeDescription();
public function newSavedQuery() {
return id(new PhabricatorSavedQuery())
->setEngineClassName(get_class($this));
}
public function addNavigationItems(PHUIListView $menu) {
$viewer = $this->requireViewer();
$menu->newLabel(pht('Queries'));
$named_queries = $this->loadEnabledNamedQueries();
foreach ($named_queries as $query) {
$key = $query->getQueryKey();
$uri = $this->getQueryResultsPageURI($key);
$menu->newLink($query->getQueryName(), $uri, 'query/'.$key);
}
if ($viewer->isLoggedIn()) {
$manage_uri = $this->getQueryManagementURI();
$menu->newLink(pht('Edit Queries...'), $manage_uri, 'query/edit');
}
$menu->newLabel(pht('Search'));
$advanced_uri = $this->getQueryResultsPageURI('advanced');
$menu->newLink(pht('Advanced Search'), $advanced_uri, 'query/advanced');
return $this;
}
public function loadAllNamedQueries() {
$viewer = $this->requireViewer();
$named_queries = id(new PhabricatorNamedQueryQuery())
->setViewer($viewer)
->withUserPHIDs(array($viewer->getPHID()))
->withEngineClassNames(array(get_class($this)))
->execute();
$named_queries = mpull($named_queries, null, 'getQueryKey');
$builtin = $this->getBuiltinQueries($viewer);
$builtin = mpull($builtin, null, 'getQueryKey');
foreach ($named_queries as $key => $named_query) {
if ($named_query->getIsBuiltin()) {
if (isset($builtin[$key])) {
$named_queries[$key]->setQueryName($builtin[$key]->getQueryName());
unset($builtin[$key]);
} else {
unset($named_queries[$key]);
}
}
unset($builtin[$key]);
}
$named_queries = msort($named_queries, 'getSortKey');
return $named_queries + $builtin;
}
public function loadEnabledNamedQueries() {
$named_queries = $this->loadAllNamedQueries();
foreach ($named_queries as $key => $named_query) {
if ($named_query->getIsBuiltin() && $named_query->getIsDisabled()) {
unset($named_queries[$key]);
}
}
return $named_queries;
}
protected function setQueryProjects(
PhabricatorCursorPagedPolicyAwareQuery $query,
PhabricatorSavedQuery $saved) {
$datasource = id(new PhabricatorProjectLogicalDatasource())
->setViewer($this->requireViewer());
$projects = $saved->getParameter('projects', array());
$constraints = $datasource->evaluateTokens($projects);
if ($constraints) {
$query->withEdgeLogicConstraints(
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST,
$constraints);
}
}
/* -( Applications )------------------------------------------------------- */
protected function getApplicationURI($path = '') {
return $this->getApplication()->getApplicationURI($path);
}
protected function getApplication() {
if (!$this->application) {
$class = $this->getApplicationClassName();
$this->application = id(new PhabricatorApplicationQuery())
->setViewer($this->requireViewer())
->withClasses(array($class))
->withInstalled(true)
->executeOne();
if (!$this->application) {
throw new Exception(
pht(
'Application "%s" is not installed!',
$class));
}
}
return $this->application;
}
abstract public function getApplicationClassName();
/* -( Constructing Engines )----------------------------------------------- */
/**
* Load all available application search engines.
*
* @return list<PhabricatorApplicationSearchEngine> All available engines.
* @task construct
*/
public static function getAllEngines() {
$engines = id(new PhutilSymbolLoader())
->setAncestorClass(__CLASS__)
->loadObjects();
return $engines;
}
/**
* Get an engine by class name, if it exists.
*
* @return PhabricatorApplicationSearchEngine|null Engine, or null if it does
* not exist.
* @task construct
*/
public static function getEngineByClassName($class_name) {
return idx(self::getAllEngines(), $class_name);
}
/* -( Builtin Queries )---------------------------------------------------- */
/**
* @task builtin
*/
public function getBuiltinQueries() {
$names = $this->getBuiltinQueryNames();
$queries = array();
$sequence = 0;
foreach ($names as $key => $name) {
$queries[$key] = id(new PhabricatorNamedQuery())
->setUserPHID($this->requireViewer()->getPHID())
->setEngineClassName(get_class($this))
->setQueryName($name)
->setQueryKey($key)
->setSequence((1 << 24) + $sequence++)
->setIsBuiltin(true);
}
return $queries;
}
/**
* @task builtin
*/
public function getBuiltinQuery($query_key) {
if (!$this->isBuiltinQuery($query_key)) {
- throw new Exception("'{$query_key}' is not a builtin!");
+ throw new Exception(pht("'%s' is not a builtin!", $query_key));
}
return idx($this->getBuiltinQueries(), $query_key);
}
/**
* @task builtin
*/
protected function getBuiltinQueryNames() {
return array();
}
/**
* @task builtin
*/
public function isBuiltinQuery($query_key) {
$builtins = $this->getBuiltinQueries();
return isset($builtins[$query_key]);
}
/**
* @task builtin
*/
public function buildSavedQueryFromBuiltin($query_key) {
- throw new Exception("Builtin '{$query_key}' is not supported!");
+ throw new Exception(pht("Builtin '%s' is not supported!", $query_key));
}
/* -( Reading Utilities )--------------------------------------------------- */
/**
* Read a list of user PHIDs from a request in a flexible way. This method
* supports either of these forms:
*
* users[]=alincoln&users[]=htaft
* users=alincoln,htaft
*
* Additionally, users can be specified either by PHID or by name.
*
* The main goal of this flexibility is to allow external programs to generate
* links to pages (like "alincoln's open revisions") without needing to make
* API calls.
*
* @param AphrontRequest Request to read user PHIDs from.
* @param string Key to read in the request.
* @param list<const> Other permitted PHID types.
* @return list<phid> List of user PHIDs and selector functions.
* @task read
*/
protected function readUsersFromRequest(
AphrontRequest $request,
$key,
array $allow_types = array()) {
$list = $this->readListFromRequest($request, $key);
$phids = array();
$names = array();
$allow_types = array_fuse($allow_types);
$user_type = PhabricatorPeopleUserPHIDType::TYPECONST;
foreach ($list as $item) {
$type = phid_get_type($item);
if ($type == $user_type) {
$phids[] = $item;
} else if (isset($allow_types[$type])) {
$phids[] = $item;
} else {
if (PhabricatorTypeaheadDatasource::isFunctionToken($item)) {
// If this is a function, pass it through unchanged; we'll evaluate
// it later.
$phids[] = $item;
} else {
$names[] = $item;
}
}
}
if ($names) {
$users = id(new PhabricatorPeopleQuery())
->setViewer($this->requireViewer())
->withUsernames($names)
->execute();
foreach ($users as $user) {
$phids[] = $user->getPHID();
}
$phids = array_unique($phids);
}
return $phids;
}
/**
* Read a list of project PHIDs from a request in a flexible way.
*
* @param AphrontRequest Request to read user PHIDs from.
* @param string Key to read in the request.
* @return list<phid> List of projet PHIDs and selector functions.
* @task read
*/
protected function readProjectsFromRequest(AphrontRequest $request, $key) {
$list = $this->readListFromRequest($request, $key);
$phids = array();
$slugs = array();
$project_type = PhabricatorProjectProjectPHIDType::TYPECONST;
foreach ($list as $item) {
$type = phid_get_type($item);
if ($type == $project_type) {
$phids[] = $item;
} else {
if (PhabricatorTypeaheadDatasource::isFunctionToken($item)) {
// If this is a function, pass it through unchanged; we'll evaluate
// it later.
$phids[] = $item;
} else {
$slugs[] = $item;
}
}
}
if ($slugs) {
$projects = id(new PhabricatorProjectQuery())
->setViewer($this->requireViewer())
->withSlugs($slugs)
->execute();
foreach ($projects as $project) {
$phids[] = $project->getPHID();
}
$phids = array_unique($phids);
}
return $phids;
}
/**
* Read a list of subscribers from a request in a flexible way.
*
* @param AphrontRequest Request to read PHIDs from.
* @param string Key to read in the request.
* @return list<phid> List of object PHIDs.
* @task read
*/
protected function readSubscribersFromRequest(
AphrontRequest $request,
$key) {
return $this->readUsersFromRequest(
$request,
$key,
array(
PhabricatorProjectProjectPHIDType::TYPECONST,
PhabricatorMailingListListPHIDType::TYPECONST,
));
}
/**
* Read a list of generic PHIDs from a request in a flexible way. Like
* @{method:readUsersFromRequest}, this method supports either array or
* comma-delimited forms. Objects can be specified either by PHID or by
* object name.
*
* @param AphrontRequest Request to read PHIDs from.
* @param string Key to read in the request.
* @param list<const> Optional, list of permitted PHID types.
* @return list<phid> List of object PHIDs.
*
* @task read
*/
protected function readPHIDsFromRequest(
AphrontRequest $request,
$key,
array $allow_types = array()) {
$list = $this->readListFromRequest($request, $key);
$objects = id(new PhabricatorObjectQuery())
->setViewer($this->requireViewer())
->withNames($list)
->execute();
$list = mpull($objects, 'getPHID');
if (!$list) {
return array();
}
// If only certain PHID types are allowed, filter out all the others.
if ($allow_types) {
$allow_types = array_fuse($allow_types);
foreach ($list as $key => $phid) {
if (empty($allow_types[phid_get_type($phid)])) {
unset($list[$key]);
}
}
}
return $list;
}
/**
* Read a list of items from the request, in either array format or string
* format:
*
* list[]=item1&list[]=item2
* list=item1,item2
*
* This provides flexibility when constructing URIs, especially from external
* sources.
*
* @param AphrontRequest Request to read strings from.
* @param string Key to read in the request.
* @return list<string> List of values.
*/
protected function readListFromRequest(
AphrontRequest $request,
$key) {
$list = $request->getArr($key, null);
if ($list === null) {
$list = $request->getStrList($key);
}
if (!$list) {
return array();
}
return $list;
}
protected function readDateFromRequest(
AphrontRequest $request,
$key) {
$value = AphrontFormDateControlValue::newFromRequest($request, $key);
if ($value->isEmpty()) {
return null;
}
return $value->getDictionary();
}
protected function readBoolFromRequest(
AphrontRequest $request,
$key) {
if (!strlen($request->getStr($key))) {
return null;
}
return $request->getBool($key);
}
protected function getBoolFromQuery(PhabricatorSavedQuery $query, $key) {
$value = $query->getParameter($key);
if ($value === null) {
return $value;
}
return $value ? 'true' : 'false';
}
/* -( Dates )-------------------------------------------------------------- */
/**
* @task dates
*/
protected function parseDateTime($date_time) {
if (!strlen($date_time)) {
return null;
}
return PhabricatorTime::parseLocalTime($date_time, $this->requireViewer());
}
/**
* @task dates
*/
protected function buildDateRange(
AphrontFormView $form,
PhabricatorSavedQuery $saved_query,
$start_key,
$start_name,
$end_key,
$end_name) {
$start_str = $saved_query->getParameter($start_key);
$start = null;
if (strlen($start_str)) {
$start = $this->parseDateTime($start_str);
if (!$start) {
$this->addError(
pht(
'"%s" date can not be parsed.',
$start_name));
}
}
$end_str = $saved_query->getParameter($end_key);
$end = null;
if (strlen($end_str)) {
$end = $this->parseDateTime($end_str);
if (!$end) {
$this->addError(
pht(
'"%s" date can not be parsed.',
$end_name));
}
}
if ($start && $end && ($start >= $end)) {
$this->addError(
pht(
'"%s" must be a date before "%s".',
$start_name,
$end_name));
}
$form
->appendChild(
id(new PHUIFormFreeformDateControl())
->setName($start_key)
->setLabel($start_name)
->setValue($start_str))
->appendChild(
id(new AphrontFormTextControl())
->setName($end_key)
->setLabel($end_name)
->setValue($end_str));
}
/* -( Result Ordering )---------------------------------------------------- */
/**
* Save order selection to a @{class:PhabricatorSavedQuery}.
*/
protected function saveQueryOrder(
PhabricatorSavedQuery $saved,
AphrontRequest $request) {
$saved->setParameter('order', $request->getStr('order'));
return $this;
}
/**
* Set query ordering from a saved value.
*/
protected function setQueryOrder(
PhabricatorCursorPagedPolicyAwareQuery $query,
PhabricatorSavedQuery $saved) {
$order = $saved->getParameter('order');
$builtin = $query->getBuiltinOrders();
if (strlen($order) && isset($builtin[$order])) {
$query->setOrder($order);
} else {
// If the order is invalid or not available, we choose the first
// builtin order. This isn't always the default order for the query,
// but is the first value in the "Order" dropdown, and makes the query
// behavior more consistent with the UI. In queries where the two
// orders differ, this order is the preferred order for humans.
$query->setOrder(head_key($builtin));
}
return $this;
}
protected function appendOrderFieldsToForm(
AphrontFormView $form,
PhabricatorSavedQuery $saved,
PhabricatorCursorPagedPolicyAwareQuery $query) {
$orders = $query->getBuiltinOrders();
$orders = ipull($orders, 'name');
$form->appendControl(
id(new AphrontFormSelectControl())
->setLabel(pht('Order'))
->setName('order')
->setOptions($orders)
->setValue($saved->getParameter('order')));
}
/* -( Paging and Executing Queries )--------------------------------------- */
public function getPageSize(PhabricatorSavedQuery $saved) {
return $saved->getParameter('limit', 100);
}
public function shouldUseOffsetPaging() {
return false;
}
public function newPagerForSavedQuery(PhabricatorSavedQuery $saved) {
if ($this->shouldUseOffsetPaging()) {
$pager = new AphrontPagerView();
} else {
$pager = new AphrontCursorPagerView();
}
$page_size = $this->getPageSize($saved);
if (is_finite($page_size)) {
$pager->setPageSize($page_size);
} else {
// Consider an INF pagesize to mean a large finite pagesize.
// TODO: It would be nice to handle this more gracefully, but math
// with INF seems to vary across PHP versions, systems, and runtimes.
$pager->setPageSize(0xFFFF);
}
return $pager;
}
public function executeQuery(
PhabricatorPolicyAwareQuery $query,
AphrontView $pager) {
$query->setViewer($this->requireViewer());
if ($this->shouldUseOffsetPaging()) {
$objects = $query->executeWithOffsetPager($pager);
} else {
$objects = $query->executeWithCursorPager($pager);
}
return $objects;
}
/* -( Rendering )---------------------------------------------------------- */
public function setRequest(AphrontRequest $request) {
$this->request = $request;
return $this;
}
public function getRequest() {
return $this->request;
}
public function renderResults(
array $objects,
PhabricatorSavedQuery $query) {
$phids = $this->getRequiredHandlePHIDsForResultList($objects, $query);
if ($phids) {
$handles = id(new PhabricatorHandleQuery())
->setViewer($this->requireViewer())
->witHPHIDs($phids)
->execute();
} else {
$handles = array();
}
return $this->renderResultList($objects, $query, $handles);
}
protected function getRequiredHandlePHIDsForResultList(
array $objects,
PhabricatorSavedQuery $query) {
return array();
}
protected function renderResultList(
array $objects,
PhabricatorSavedQuery $query,
array $handles) {
throw new Exception(pht('Not supported here yet!'));
}
/* -( Application Search )------------------------------------------------- */
/**
* Retrieve an object to use to define custom fields for this search.
*
* To integrate with custom fields, subclasses should override this method
* and return an instance of the application object which implements
* @{interface:PhabricatorCustomFieldInterface}.
*
* @return PhabricatorCustomFieldInterface|null Object with custom fields.
* @task appsearch
*/
public function getCustomFieldObject() {
return null;
}
/**
* Get the custom fields for this search.
*
* @return PhabricatorCustomFieldList|null Custom fields, if this search
* supports custom fields.
* @task appsearch
*/
public function getCustomFieldList() {
if ($this->customFields === false) {
$object = $this->getCustomFieldObject();
if ($object) {
$fields = PhabricatorCustomField::getObjectFields(
$object,
PhabricatorCustomField::ROLE_APPLICATIONSEARCH);
$fields->setViewer($this->requireViewer());
} else {
$fields = null;
}
$this->customFields = $fields;
}
return $this->customFields;
}
/**
* Moves data from the request into a saved query.
*
* @param AphrontRequest Request to read.
* @param PhabricatorSavedQuery Query to write to.
* @return void
* @task appsearch
*/
protected function readCustomFieldsFromRequest(
AphrontRequest $request,
PhabricatorSavedQuery $saved) {
$list = $this->getCustomFieldList();
if (!$list) {
return;
}
foreach ($list->getFields() as $field) {
$key = $this->getKeyForCustomField($field);
$value = $field->readApplicationSearchValueFromRequest(
$this,
$request);
$saved->setParameter($key, $value);
}
}
/**
* Applies data from a saved query to an executable query.
*
* @param PhabricatorCursorPagedPolicyAwareQuery Query to constrain.
* @param PhabricatorSavedQuery Saved query to read.
* @return void
*/
protected function applyCustomFieldsToQuery(
PhabricatorCursorPagedPolicyAwareQuery $query,
PhabricatorSavedQuery $saved) {
$list = $this->getCustomFieldList();
if (!$list) {
return;
}
foreach ($list->getFields() as $field) {
$key = $this->getKeyForCustomField($field);
$value = $field->applyApplicationSearchConstraintToQuery(
$this,
$query,
$saved->getParameter($key));
}
}
protected function applyOrderByToQuery(
PhabricatorCursorPagedPolicyAwareQuery $query,
array $standard_values,
$order) {
if (substr($order, 0, 7) === 'custom:') {
$list = $this->getCustomFieldList();
if (!$list) {
$query->setOrderBy(head($standard_values));
return;
}
foreach ($list->getFields() as $field) {
$key = $this->getKeyForCustomField($field);
if ($key === $order) {
$index = $field->buildOrderIndex();
if ($index === null) {
$query->setOrderBy(head($standard_values));
return;
}
$query->withApplicationSearchOrder(
$field,
$index,
false);
break;
}
}
} else {
$order = idx($standard_values, $order);
if ($order) {
$query->setOrderBy($order);
} else {
$query->setOrderBy(head($standard_values));
}
}
}
protected function getCustomFieldOrderOptions() {
$list = $this->getCustomFieldList();
if (!$list) {
return;
}
$custom_order = array();
foreach ($list->getFields() as $field) {
if ($field->shouldAppearInApplicationSearch()) {
if ($field->buildOrderIndex() !== null) {
$key = $this->getKeyForCustomField($field);
$custom_order[$key] = $field->getFieldName();
}
}
}
return $custom_order;
}
/**
* Get a unique key identifying a field.
*
* @param PhabricatorCustomField Field to identify.
* @return string Unique identifier, suitable for use as an input name.
*/
public function getKeyForCustomField(PhabricatorCustomField $field) {
return 'custom:'.$field->getFieldIndex();
}
/**
* Add inputs to an application search form so the user can query on custom
* fields.
*
* @param AphrontFormView Form to update.
* @param PhabricatorSavedQuery Values to prefill.
* @return void
*/
protected function appendCustomFieldsToForm(
AphrontFormView $form,
PhabricatorSavedQuery $saved) {
$list = $this->getCustomFieldList();
if (!$list) {
return;
}
$phids = array();
foreach ($list->getFields() as $field) {
$key = $this->getKeyForCustomField($field);
$value = $saved->getParameter($key);
$phids[$key] = $field->getRequiredHandlePHIDsForApplicationSearch($value);
}
$all_phids = array_mergev($phids);
$handles = array();
if ($all_phids) {
$handles = id(new PhabricatorHandleQuery())
->setViewer($this->requireViewer())
->withPHIDs($all_phids)
->execute();
}
foreach ($list->getFields() as $field) {
$key = $this->getKeyForCustomField($field);
$value = $saved->getParameter($key);
$field->appendToApplicationSearchForm(
$this,
$form,
$value,
array_select_keys($handles, $phids[$key]));
}
}
}
diff --git a/src/applications/search/engine/PhabricatorJumpNavHandler.php b/src/applications/search/engine/PhabricatorJumpNavHandler.php
index 53e92b6e5..7a340a7e8 100644
--- a/src/applications/search/engine/PhabricatorJumpNavHandler.php
+++ b/src/applications/search/engine/PhabricatorJumpNavHandler.php
@@ -1,122 +1,122 @@
<?php
final class PhabricatorJumpNavHandler {
public static function getJumpResponse(PhabricatorUser $viewer, $jump) {
$jump = trim($jump);
$patterns = array(
'/^a$/i' => 'uri:/audit/',
'/^f$/i' => 'uri:/feed/',
'/^d$/i' => 'uri:/differential/',
'/^r$/i' => 'uri:/diffusion/',
'/^t$/i' => 'uri:/maniphest/',
'/^p$/i' => 'uri:/project/',
'/^u$/i' => 'uri:/people/',
'/^p\s+(.+)$/i' => 'project',
'/^u\s+(\S+)$/i' => 'user',
'/^task:\s*(.+)/i' => 'create-task',
'/^(?:s)\s+(\S+)/i' => 'find-symbol',
'/^r\s+(.+)$/i' => 'find-repository',
);
foreach ($patterns as $pattern => $effect) {
$matches = null;
if (preg_match($pattern, $jump, $matches)) {
if (!strncmp($effect, 'uri:', 4)) {
return id(new AphrontRedirectResponse())
->setURI(substr($effect, 4));
} else {
switch ($effect) {
case 'user':
return id(new AphrontRedirectResponse())
->setURI('/p/'.$matches[1].'/');
case 'project':
$project = self::findCloselyNamedProject($matches[1]);
if ($project) {
return id(new AphrontRedirectResponse())
->setURI('/project/view/'.$project->getID().'/');
} else {
$jump = $matches[1];
}
break;
case 'find-symbol':
$context = '';
$symbol = $matches[1];
$parts = array();
if (preg_match('/(.*)(?:\\.|::|->)(.*)/', $symbol, $parts)) {
$context = '&context='.phutil_escape_uri($parts[1]);
$symbol = $parts[2];
}
return id(new AphrontRedirectResponse())
->setURI("/diffusion/symbol/$symbol/?jump=true$context");
case 'find-repository':
$name = $matches[1];
$repositories = id(new PhabricatorRepositoryQuery())
->setViewer($viewer)
->withNameContains($name)
->execute();
if (count($repositories) == 1) {
// Just one match, jump to repository.
$uri = '/diffusion/'.head($repositories)->getCallsign().'/';
} else {
// More than one match, jump to search.
$uri = urisprintf('/diffusion/?order=name&name=%s', $name);
}
return id(new AphrontRedirectResponse())->setURI($uri);
case 'create-task':
return id(new AphrontRedirectResponse())
->setURI('/maniphest/task/create/?title='
.phutil_escape_uri($matches[1]));
default:
- throw new Exception("Unknown jump effect '{$effect}'!");
+ throw new Exception(pht("Unknown jump effect '%s'!", $effect));
}
}
}
}
// If none of the patterns matched, look for an object by name.
$objects = id(new PhabricatorObjectQuery())
->setViewer($viewer)
->withNames(array($jump))
->execute();
if (count($objects) == 1) {
$handle = id(new PhabricatorHandleQuery())
->setViewer($viewer)
->withPHIDs(mpull($objects, 'getPHID'))
->executeOne();
return id(new AphrontRedirectResponse())->setURI($handle->getURI());
}
return null;
}
private static function findCloselyNamedProject($name) {
$project = id(new PhabricatorProject())->loadOneWhere(
'name = %s',
$name);
if ($project) {
return $project;
} else { // no exact match, try a fuzzy match
$projects = id(new PhabricatorProject())->loadAllWhere(
'name LIKE %~',
$name);
if ($projects) {
$min_name_length = PHP_INT_MAX;
$best_project = null;
foreach ($projects as $project) {
$name_length = strlen($project->getName());
if ($name_length <= $min_name_length) {
$min_name_length = $name_length;
$best_project = $project;
}
}
return $best_project;
} else {
return null;
}
}
}
}
diff --git a/src/applications/search/index/PhabricatorSearchDocumentIndexer.php b/src/applications/search/index/PhabricatorSearchDocumentIndexer.php
index 18d07765a..b3e90148c 100644
--- a/src/applications/search/index/PhabricatorSearchDocumentIndexer.php
+++ b/src/applications/search/index/PhabricatorSearchDocumentIndexer.php
@@ -1,195 +1,199 @@
<?php
abstract class PhabricatorSearchDocumentIndexer extends Phobject {
private $context;
protected function setContext($context) {
$this->context = $context;
return $this;
}
protected function getContext() {
return $this->context;
}
abstract public function getIndexableObject();
abstract protected function buildAbstractDocumentByPHID($phid);
protected function getViewer() {
return PhabricatorUser::getOmnipotentUser();
}
public function shouldIndexDocumentByPHID($phid) {
$object = $this->getIndexableObject();
return (phid_get_type($phid) == phid_get_type($object->generatePHID()));
}
public function getIndexIterator() {
$object = $this->getIndexableObject();
return new LiskMigrationIterator($object);
}
protected function loadDocumentByPHID($phid) {
$object = id(new PhabricatorObjectQuery())
->setViewer($this->getViewer())
->withPHIDs(array($phid))
->executeOne();
if (!$object) {
- throw new Exception("Unable to load object by phid '{$phid}'!");
+ throw new Exception(pht("Unable to load object by PHID '%s'!", $phid));
}
return $object;
}
public function indexDocumentByPHID($phid, $context) {
try {
$this->setContext($context);
$document = $this->buildAbstractDocumentByPHID($phid);
if ($document === null) {
// This indexer doesn't build a document index, so we're done.
return $this;
}
$object = $this->loadDocumentByPHID($phid);
// Automatically rebuild CustomField indexes if the object uses custom
// fields.
if ($object instanceof PhabricatorCustomFieldInterface) {
$this->indexCustomFields($document, $object);
}
// Automatically rebuild subscriber indexes if the object is subscribable.
if ($object instanceof PhabricatorSubscribableInterface) {
$this->indexSubscribers($document);
}
// Automatically build project relationships
if ($object instanceof PhabricatorProjectInterface) {
$this->indexProjects($document, $object);
}
$engine = PhabricatorSearchEngine::loadEngine();
try {
$engine->reindexAbstractDocument($document);
} catch (Exception $ex) {
- $phid = $document->getPHID();
- $class = get_class($engine);
-
- phlog("Unable to index document {$phid} with engine {$class}.");
+ phlog(
+ pht(
+ 'Unable to index document %s with engine %s.',
+ $document->getPHID(),
+ get_class($engine)));
phlog($ex);
}
$this->dispatchDidUpdateIndexEvent($phid, $document);
} catch (Exception $ex) {
- $class = get_class($this);
- phlog("Unable to build document {$phid} with indexer {$class}.");
+ phlog(
+ pht(
+ 'Unable to build document %s with indexer %s.',
+ $phid,
+ get_class($this)));
phlog($ex);
}
return $this;
}
protected function newDocument($phid) {
return id(new PhabricatorSearchAbstractDocument())
->setPHID($phid)
->setDocumentType(phid_get_type($phid));
}
protected function indexSubscribers(
PhabricatorSearchAbstractDocument $doc) {
$subscribers = PhabricatorSubscribersQuery::loadSubscribersForPHID(
$doc->getPHID());
$handles = id(new PhabricatorHandleQuery())
->setViewer($this->getViewer())
->withPHIDs($subscribers)
->execute();
foreach ($handles as $phid => $handle) {
$doc->addRelationship(
PhabricatorSearchRelationship::RELATIONSHIP_SUBSCRIBER,
$phid,
$handle->getType(),
$doc->getDocumentModified()); // Bogus timestamp.
}
}
protected function indexProjects(
PhabricatorSearchAbstractDocument $doc,
PhabricatorProjectInterface $object) {
$project_phids = PhabricatorEdgeQuery::loadDestinationPHIDs(
$object->getPHID(),
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST);
if ($project_phids) {
foreach ($project_phids as $project_phid) {
$doc->addRelationship(
PhabricatorSearchRelationship::RELATIONSHIP_PROJECT,
$project_phid,
PhabricatorProjectProjectPHIDType::TYPECONST,
$doc->getDocumentModified()); // Bogus timestamp.
}
}
}
protected function indexTransactions(
PhabricatorSearchAbstractDocument $doc,
PhabricatorApplicationTransactionQuery $query,
array $phids) {
$xactions = id(clone $query)
->setViewer($this->getViewer())
->withObjectPHIDs($phids)
->execute();
foreach ($xactions as $xaction) {
if (!$xaction->hasComment()) {
continue;
}
$comment = $xaction->getComment();
$doc->addField(
PhabricatorSearchField::FIELD_COMMENT,
$comment->getContent());
}
}
protected function indexCustomFields(
PhabricatorSearchAbstractDocument $document,
PhabricatorCustomFieldInterface $object) {
// Rebuild the ApplicationSearch indexes. These are internal and not part of
// the fulltext search, but putting them in this workflow allows users to
// use the same tools to rebuild the indexes, which is easy to understand.
$field_list = PhabricatorCustomField::getObjectFields(
$object,
PhabricatorCustomField::ROLE_DEFAULT);
$field_list->setViewer($this->getViewer());
$field_list->readFieldsFromStorage($object);
// Rebuild ApplicationSearch indexes.
$field_list->rebuildIndexes($object);
// Rebuild global search indexes.
$field_list->updateAbstractDocument($document);
}
private function dispatchDidUpdateIndexEvent(
$phid,
PhabricatorSearchAbstractDocument $document) {
$event = new PhabricatorEvent(
PhabricatorEventType::TYPE_SEARCH_DIDUPDATEINDEX,
array(
'phid' => $phid,
'object' => $this->loadDocumentByPHID($phid),
'document' => $document,
));
$event->setUser($this->getViewer());
PhutilEventEngine::dispatchEvent($event);
}
}
diff --git a/src/applications/search/management/PhabricatorSearchManagementIndexWorkflow.php b/src/applications/search/management/PhabricatorSearchManagementIndexWorkflow.php
index fea5817de..f7c679782 100644
--- a/src/applications/search/management/PhabricatorSearchManagementIndexWorkflow.php
+++ b/src/applications/search/management/PhabricatorSearchManagementIndexWorkflow.php
@@ -1,139 +1,147 @@
<?php
final class PhabricatorSearchManagementIndexWorkflow
extends PhabricatorSearchManagementWorkflow {
protected function didConstruct() {
$this
->setName('index')
- ->setSynopsis('Build or rebuild search indexes.')
+ ->setSynopsis(pht('Build or rebuild search indexes.'))
->setExamples(
"**index** D123\n".
"**index** --type DREV\n".
"**index** --all")
->setArguments(
array(
array(
'name' => 'all',
- 'help' => 'Reindex all documents.',
+ 'help' => pht('Reindex all documents.'),
),
array(
'name' => 'type',
'param' => 'TYPE',
- 'help' => 'PHID type to reindex, like "TASK" or "DREV".',
+ 'help' => pht('PHID type to reindex, like "TASK" or "DREV".'),
),
array(
'name' => 'background',
- 'help' => 'Instead of indexing in this process, queue tasks for '.
- 'the daemons. This can improve performance, but makes '.
- 'it more difficult to debug search indexing.',
+ 'help' => pht(
+ 'Instead of indexing in this process, queue tasks for '.
+ 'the daemons. This can improve performance, but makes '.
+ 'it more difficult to debug search indexing.'),
),
array(
'name' => 'objects',
'wildcard' => true,
),
));
}
public function execute(PhutilArgumentParser $args) {
$console = PhutilConsole::getConsole();
$is_all = $args->getArg('all');
$is_type = $args->getArg('type');
$obj_names = $args->getArg('objects');
if ($obj_names && ($is_all || $is_type)) {
throw new PhutilArgumentUsageException(
- "You can not name objects to index alongside the '--all' or '--type' ".
- "flags.");
+ pht(
+ "You can not name objects to index alongside the '%s' or '%s' flags.",
+ '--all',
+ '--type'));
} else if (!$obj_names && !($is_all || $is_type)) {
throw new PhutilArgumentUsageException(
- "Provide one of '--all', '--type' or a list of object names.");
+ pht(
+ "Provide one of '%s', '%s' or a list of object names.",
+ '--all',
+ '--type'));
}
if ($obj_names) {
$phids = $this->loadPHIDsByNames($obj_names);
} else {
$phids = $this->loadPHIDsByTypes($is_type);
}
if (!$phids) {
- throw new PhutilArgumentUsageException('Nothing to index!');
+ throw new PhutilArgumentUsageException(pht('Nothing to index!'));
}
if ($args->getArg('background')) {
$is_background = true;
} else {
PhabricatorWorker::setRunAllTasksInProcess(true);
$is_background = false;
}
if (!$is_background) {
$console->writeOut(
"%s\n",
pht(
- 'Run this workflow with "--background" to queue tasks for the '.
- 'daemon workers.'));
+ 'Run this workflow with "%s" to queue tasks for the daemon workers.',
+ '--background'));
}
$groups = phid_group_by_type($phids);
foreach ($groups as $group_type => $group) {
$console->writeOut(
"%s\n",
pht('Indexing %d object(s) of type %s.', count($group), $group_type));
}
$bar = id(new PhutilConsoleProgressBar())
->setTotal(count($phids));
$indexer = new PhabricatorSearchIndexer();
foreach ($phids as $phid) {
$indexer->queueDocumentForIndexing($phid);
$bar->update(1);
}
$bar->done();
}
private function loadPHIDsByNames(array $names) {
$query = id(new PhabricatorObjectQuery())
->setViewer($this->getViewer())
->withNames($names);
$query->execute();
$objects = $query->getNamedResults();
foreach ($names as $name) {
if (empty($objects[$name])) {
throw new PhutilArgumentUsageException(
- "'{$name}' is not the name of a known object.");
+ pht(
+ "'%s' is not the name of a known object.",
+ $name));
}
}
return mpull($objects, 'getPHID');
}
private function loadPHIDsByTypes($type) {
$indexers = id(new PhutilSymbolLoader())
->setAncestorClass('PhabricatorSearchDocumentIndexer')
->loadObjects();
$phids = array();
foreach ($indexers as $indexer) {
$indexer_phid = $indexer->getIndexableObject()->generatePHID();
$indexer_type = phid_get_type($indexer_phid);
if ($type && strcasecmp($indexer_type, $type)) {
continue;
}
$iterator = $indexer->getIndexIterator();
foreach ($iterator as $object) {
$phids[] = $object->getPHID();
}
}
return $phids;
}
}
diff --git a/src/applications/search/management/PhabricatorSearchManagementInitWorkflow.php b/src/applications/search/management/PhabricatorSearchManagementInitWorkflow.php
index e4a8ddb69..56a5b3335 100644
--- a/src/applications/search/management/PhabricatorSearchManagementInitWorkflow.php
+++ b/src/applications/search/management/PhabricatorSearchManagementInitWorkflow.php
@@ -1,50 +1,51 @@
<?php
final class PhabricatorSearchManagementInitWorkflow
extends PhabricatorSearchManagementWorkflow {
protected function didConstruct() {
$this
->setName('init')
- ->setSynopsis('Initialize or repair an index.')
+ ->setSynopsis(pht('Initialize or repair an index.'))
->setExamples('**init**');
}
public function execute(PhutilArgumentParser $args) {
$console = PhutilConsole::getConsole();
$engine = PhabricatorSearchEngine::loadEngine();
$work_done = false;
if (!$engine->indexExists()) {
$console->writeOut(
'%s',
pht('Index does not exist, creating...'));
$engine->initIndex();
$console->writeOut(
"%s\n",
pht('done.'));
$work_done = true;
} else if (!$engine->indexIsSane()) {
$console->writeOut(
'%s',
- pht('Index exists but is incorrect, fixing...'));
+ pht('Index exists but is incorrect, fixing...'));
$engine->initIndex();
$console->writeOut(
"%s\n",
pht('done.'));
$work_done = true;
}
if ($work_done) {
$console->writeOut(
"%s\n",
- pht('Index maintenance complete. Run `./bin/search index` to '.
- 'reindex documents'));
+ pht(
+ 'Index maintenance complete. Run `%s` to reindex documents',
+ './bin/search index'));
} else {
$console->writeOut(
"%s\n",
pht('Nothing to do.'));
}
}
}
diff --git a/src/applications/search/query/PhabricatorSearchApplicationSearchEngine.php b/src/applications/search/query/PhabricatorSearchApplicationSearchEngine.php
index 1ca6b109d..092f758f3 100644
--- a/src/applications/search/query/PhabricatorSearchApplicationSearchEngine.php
+++ b/src/applications/search/query/PhabricatorSearchApplicationSearchEngine.php
@@ -1,281 +1,281 @@
<?php
final class PhabricatorSearchApplicationSearchEngine
extends PhabricatorApplicationSearchEngine {
public function getResultTypeDescription() {
return pht('Fulltext Results');
}
public function getApplicationClassName() {
return 'PhabricatorSearchApplication';
}
public function buildSavedQueryFromRequest(AphrontRequest $request) {
$saved = new PhabricatorSavedQuery();
$saved->setParameter('query', $request->getStr('query'));
$saved->setParameter(
'statuses',
$this->readListFromRequest($request, 'statuses'));
$saved->setParameter(
'types',
$this->readListFromRequest($request, 'types'));
$saved->setParameter(
'authorPHIDs',
$this->readUsersFromRequest($request, 'authorPHIDs'));
$saved->setParameter(
'ownerPHIDs',
$this->readUsersFromRequest($request, 'ownerPHIDs'));
$saved->setParameter(
'subscriberPHIDs',
$this->readSubscribersFromRequest($request, 'subscriberPHIDs'));
$saved->setParameter(
'projectPHIDs',
$this->readPHIDsFromRequest($request, 'projectPHIDs'));
return $saved;
}
public function buildQueryFromSavedQuery(PhabricatorSavedQuery $saved) {
$query = new PhabricatorSearchDocumentQuery();
// Convert the saved query into a resolved form (without typeahead
// functions) which the fulltext search engines can execute.
$config = clone $saved;
$viewer = $this->requireViewer();
$datasource = id(new PhabricatorPeopleOwnerDatasource())
->setViewer($viewer);
$owner_phids = $this->readOwnerPHIDs($config);
$owner_phids = $datasource->evaluateTokens($owner_phids);
foreach ($owner_phids as $key => $phid) {
if ($phid == PhabricatorPeopleNoOwnerDatasource::FUNCTION_TOKEN) {
$config->setParameter('withUnowned', true);
unset($owner_phids[$key]);
}
if ($phid == PhabricatorPeopleAnyOwnerDatasource::FUNCTION_TOKEN) {
$config->setParameter('withAnyOwner', true);
unset($owner_phids[$key]);
}
}
$config->setParameter('ownerPHIDs', $owner_phids);
$datasource = id(new PhabricatorPeopleUserFunctionDatasource())
->setViewer($viewer);
$author_phids = $config->getParameter('authorPHIDs', array());
$author_phids = $datasource->evaluateTokens($author_phids);
$config->setParameter('authorPHIDs', $author_phids);
$datasource = id(new PhabricatorMetaMTAMailableFunctionDatasource())
->setViewer($viewer);
$subscriber_phids = $config->getParameter('subscriberPHIDs', array());
$subscriber_phids = $datasource->evaluateTokens($subscriber_phids);
$config->setParameter('subscriberPHIDs', $subscriber_phids);
$query->withSavedQuery($config);
return $query;
}
public function buildSearchForm(
AphrontFormView $form,
PhabricatorSavedQuery $saved) {
$options = array();
$author_value = null;
$owner_value = null;
$subscribers_value = null;
$project_value = null;
$author_phids = $saved->getParameter('authorPHIDs', array());
$owner_phids = $this->readOwnerPHIDs($saved);
$subscriber_phids = $saved->getParameter('subscriberPHIDs', array());
$project_phids = $saved->getParameter('projectPHIDs', array());
$status_values = $saved->getParameter('statuses', array());
$status_values = array_fuse($status_values);
$statuses = array(
PhabricatorSearchRelationship::RELATIONSHIP_OPEN => pht('Open'),
PhabricatorSearchRelationship::RELATIONSHIP_CLOSED => pht('Closed'),
);
$status_control = id(new AphrontFormCheckboxControl())
->setLabel(pht('Document Status'));
foreach ($statuses as $status => $name) {
$status_control->addCheckbox(
'statuses[]',
$status,
$name,
isset($status_values[$status]));
}
$type_values = $saved->getParameter('types', array());
$type_values = array_fuse($type_values);
$types_control = id(new AphrontFormTokenizerControl())
->setLabel(pht('Document Types'))
->setName('types')
->setDatasource(new PhabricatorSearchDocumentTypeDatasource())
->setValue($type_values);
$form
->appendChild(
phutil_tag(
'input',
array(
'type' => 'hidden',
'name' => 'jump',
'value' => 'no',
)))
->appendChild(
id(new AphrontFormTextControl())
- ->setLabel('Query')
+ ->setLabel(pht('Query'))
->setName('query')
->setValue($saved->getParameter('query')))
->appendChild($status_control)
->appendControl($types_control)
->appendControl(
id(new AphrontFormTokenizerControl())
->setName('authorPHIDs')
- ->setLabel('Authors')
+ ->setLabel(pht('Authors'))
->setDatasource(new PhabricatorPeopleUserFunctionDatasource())
->setValue($author_phids))
->appendControl(
id(new AphrontFormTokenizerControl())
->setName('ownerPHIDs')
- ->setLabel('Owners')
+ ->setLabel(pht('Owners'))
->setDatasource(new PhabricatorPeopleOwnerDatasource())
->setValue($owner_phids))
->appendControl(
id(new AphrontFormTokenizerControl())
->setName('subscriberPHIDs')
- ->setLabel('Subscribers')
+ ->setLabel(pht('Subscribers'))
->setDatasource(new PhabricatorMetaMTAMailableFunctionDatasource())
->setValue($subscriber_phids))
->appendControl(
id(new AphrontFormTokenizerControl())
->setName('projectPHIDs')
- ->setLabel('In Any Project')
+ ->setLabel(pht('In Any Project'))
->setDatasource(new PhabricatorProjectDatasource())
->setValue($project_phids));
}
protected function getURI($path) {
return '/search/'.$path;
}
protected function getBuiltinQueryNames() {
return array(
'all' => pht('All Documents'),
'open' => pht('Open Documents'),
'open-tasks' => pht('Open Tasks'),
);
}
public function buildSavedQueryFromBuiltin($query_key) {
$query = $this->newSavedQuery();
$query->setQueryKey($query_key);
switch ($query_key) {
case 'all':
return $query;
case 'open':
return $query->setParameter('statuses', array('open'));
case 'open-tasks':
return $query
->setParameter('statuses', array('open'))
->setParameter('types', array(ManiphestTaskPHIDType::TYPECONST));
}
return parent::buildSavedQueryFromBuiltin($query_key);
}
public static function getIndexableDocumentTypes(
PhabricatorUser $viewer = null) {
// TODO: This is inelegant and not very efficient, but gets us reasonable
// results. It would be nice to do this more elegantly.
$indexers = id(new PhutilSymbolLoader())
->setAncestorClass('PhabricatorSearchDocumentIndexer')
->loadObjects();
if ($viewer) {
$types = PhabricatorPHIDType::getAllInstalledTypes($viewer);
} else {
$types = PhabricatorPHIDType::getAllTypes();
}
$results = array();
foreach ($types as $type) {
$typeconst = $type->getTypeConstant();
foreach ($indexers as $indexer) {
$fake_phid = 'PHID-'.$typeconst.'-fake';
if ($indexer->shouldIndexDocumentByPHID($fake_phid)) {
$results[$typeconst] = $type->getTypeName();
}
}
}
asort($results);
return $results;
}
public function shouldUseOffsetPaging() {
return true;
}
protected function renderResultList(
array $results,
PhabricatorSavedQuery $query,
array $handles) {
$viewer = $this->requireViewer();
if ($results) {
$objects = id(new PhabricatorObjectQuery())
->setViewer($viewer)
->withPHIDs(mpull($results, 'getPHID'))
->execute();
$list = new PHUIObjectItemListView();
foreach ($results as $phid => $handle) {
$view = id(new PhabricatorSearchResultView())
->setHandle($handle)
->setQuery($query)
->setObject(idx($objects, $phid))
->render();
$list->addItem($view);
}
$results = $list;
} else {
$results = id(new PHUIInfoView())
->appendChild(pht('No results returned for that query.'))
->setSeverity(PHUIInfoView::SEVERITY_NODATA);
}
return $results;
}
private function readOwnerPHIDs(PhabricatorSavedQuery $saved) {
$owner_phids = $saved->getParameter('ownerPHIDs', array());
// This was an old checkbox from before typeahead functions.
if ($saved->getParameter('withUnowned')) {
$owner_phids[] = PhabricatorPeopleNoOwnerDatasource::FUNCTION_TOKEN;
}
return $owner_phids;
}
}
diff --git a/src/applications/search/query/PhabricatorSearchDocumentQuery.php b/src/applications/search/query/PhabricatorSearchDocumentQuery.php
index 1770f4b04..4928ee6a6 100644
--- a/src/applications/search/query/PhabricatorSearchDocumentQuery.php
+++ b/src/applications/search/query/PhabricatorSearchDocumentQuery.php
@@ -1,98 +1,97 @@
<?php
final class PhabricatorSearchDocumentQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $savedQuery;
private $objectCapabilities;
public function withSavedQuery(PhabricatorSavedQuery $query) {
$this->savedQuery = $query;
return $this;
}
public function requireObjectCapabilities(array $capabilities) {
$this->objectCapabilities = $capabilities;
return $this;
}
protected function getRequiredObjectCapabilities() {
if ($this->objectCapabilities) {
return $this->objectCapabilities;
}
return $this->getRequiredCapabilities();
}
protected function loadPage() {
$phids = $this->loadDocumentPHIDsWithoutPolicyChecks();
$handles = id(new PhabricatorHandleQuery())
->setViewer($this->getViewer())
->requireObjectCapabilities($this->getRequiredObjectCapabilities())
->withPHIDs($phids)
->execute();
// Retain engine order.
$handles = array_select_keys($handles, $phids);
return $handles;
}
protected function willFilterPage(array $handles) {
// NOTE: This is used by the object selector dialog to exclude the object
// you're looking at, so that, e.g., a task can't be set as a dependency
// of itself in the UI.
// TODO: Remove this after object selection moves to ApplicationSearch.
$exclude = array();
if ($this->savedQuery) {
$exclude_phids = $this->savedQuery->getParameter('excludePHIDs', array());
$exclude = array_fuse($exclude_phids);
}
foreach ($handles as $key => $handle) {
if (!$handle->isComplete()) {
unset($handles[$key]);
continue;
}
if ($handle->getPolicyFiltered()) {
unset($handles[$key]);
continue;
}
if (isset($exclude[$handle->getPHID()])) {
unset($handles[$key]);
continue;
}
}
return $handles;
}
public function loadDocumentPHIDsWithoutPolicyChecks() {
$query = id(clone($this->savedQuery))
->setParameter('offset', $this->getOffset())
->setParameter('limit', $this->getRawResultLimit());
$engine = PhabricatorSearchEngine::loadEngine();
return $engine->executeSearch($query);
}
public function getQueryApplicationClass() {
return 'PhabricatorSearchApplication';
}
protected function getResultCursor($result) {
throw new Exception(
pht(
- 'This query does not support cursor paging; it must be offset '.
- 'paged.'));
+ 'This query does not support cursor paging; it must be offset paged.'));
}
protected function nextPage(array $page) {
$this->setOffset($this->getOffset() + count($page));
return $this;
}
}
diff --git a/src/applications/settings/panel/PhabricatorConduitCertificateSettingsPanel.php b/src/applications/settings/panel/PhabricatorConduitCertificateSettingsPanel.php
index 5ffc536fa..d7210a3c8 100644
--- a/src/applications/settings/panel/PhabricatorConduitCertificateSettingsPanel.php
+++ b/src/applications/settings/panel/PhabricatorConduitCertificateSettingsPanel.php
@@ -1,127 +1,130 @@
<?php
final class PhabricatorConduitCertificateSettingsPanel
extends PhabricatorSettingsPanel {
public function isEditableByAdministrators() {
return true;
}
public function getPanelKey() {
return 'conduit';
}
public function getPanelName() {
return pht('Conduit Certificate');
}
public function getPanelGroup() {
return pht('Authentication');
}
public function processRequest(AphrontRequest $request) {
$user = $this->getUser();
$viewer = $request->getUser();
id(new PhabricatorAuthSessionEngine())->requireHighSecuritySession(
$viewer,
$request,
'/settings/');
if ($request->isFormPost()) {
if (!$request->isDialogFormPost()) {
$dialog = new AphrontDialogView();
$dialog->setUser($viewer);
$dialog->setTitle(pht('Really regenerate session?'));
$dialog->setSubmitURI($this->getPanelURI());
$dialog->addSubmitButton(pht('Regenerate'));
$dialog->addCancelbutton($this->getPanelURI());
$dialog->appendChild(phutil_tag('p', array(), pht(
'Really destroy the old certificate? Any established '.
'sessions will be terminated.')));
return id(new AphrontDialogResponse())
->setDialog($dialog);
}
$sessions = id(new PhabricatorAuthSessionQuery())
->setViewer($user)
->withIdentityPHIDs(array($user->getPHID()))
->withSessionTypes(array(PhabricatorAuthSession::TYPE_CONDUIT))
->execute();
foreach ($sessions as $session) {
$session->delete();
}
// This implicitly regenerates the certificate.
$user->setConduitCertificate(null);
$user->save();
return id(new AphrontRedirectResponse())
->setURI($this->getPanelURI('?regenerated=true'));
}
if ($request->getStr('regenerated')) {
$notice = new PHUIInfoView();
$notice->setSeverity(PHUIInfoView::SEVERITY_NOTICE);
$notice->setTitle(pht('Certificate Regenerated'));
$notice->appendChild(phutil_tag(
'p',
array(),
- pht('Your old certificate has been destroyed and you have been issued '.
+ pht(
+ 'Your old certificate has been destroyed and you have been issued '.
'a new certificate. Sessions established under the old certificate '.
'are no longer valid.')));
$notice = $notice->render();
} else {
$notice = null;
}
Javelin::initBehavior('select-on-click');
$cert_form = new AphrontFormView();
$cert_form
->setUser($viewer)
->appendChild(phutil_tag(
'p',
array('class' => 'aphront-form-instructions'),
- pht('This certificate allows you to authenticate over Conduit, '.
+ pht(
+ 'This certificate allows you to authenticate over Conduit, '.
'the Phabricator API. Normally, you just run %s to install it.',
phutil_tag('tt', array(), 'arc install-certificate'))))
->appendChild(
id(new AphrontFormTextAreaControl())
->setLabel(pht('Certificate'))
->setHeight(AphrontFormTextAreaControl::HEIGHT_SHORT)
->setReadonly(true)
->setSigil('select-on-click')
->setValue($user->getConduitCertificate()));
$cert_form = id(new PHUIObjectBoxView())
->setHeaderText(pht('Arcanist Certificate'))
->setForm($cert_form);
- $regen_instruction = pht('You can regenerate this certificate, which '.
+ $regen_instruction = pht(
+ 'You can regenerate this certificate, which '.
'will invalidate the old certificate and create a new one.');
$regen_form = new AphrontFormView();
$regen_form
->setUser($viewer)
->setAction($this->getPanelURI())
->setWorkflow(true)
->appendChild(phutil_tag(
'p',
array('class' => 'aphront-form-instructions'),
$regen_instruction))
->appendChild(
id(new AphrontFormSubmitControl())
->setValue(pht('Regenerate Certificate')));
$regen_form = id(new PHUIObjectBoxView())
->setHeaderText(pht('Regenerate Certificate'))
->setForm($regen_form);
return array(
$notice,
$cert_form,
$regen_form,
);
}
}
diff --git a/src/applications/settings/panel/PhabricatorConpherencePreferencesSettingsPanel.php b/src/applications/settings/panel/PhabricatorConpherencePreferencesSettingsPanel.php
index c9383f417..69f84f16a 100644
--- a/src/applications/settings/panel/PhabricatorConpherencePreferencesSettingsPanel.php
+++ b/src/applications/settings/panel/PhabricatorConpherencePreferencesSettingsPanel.php
@@ -1,69 +1,70 @@
<?php
final class PhabricatorConpherencePreferencesSettingsPanel
extends PhabricatorSettingsPanel {
public function isEnabled() {
return PhabricatorApplication::isClassInstalled(
'PhabricatorConpherenceApplication');
}
public function getPanelKey() {
return 'conpherence';
}
public function getPanelName() {
return pht('Conpherence Preferences');
}
public function getPanelGroup() {
return pht('Application Settings');
}
public function processRequest(AphrontRequest $request) {
$user = $request->getUser();
$preferences = $user->loadPreferences();
$pref = PhabricatorUserPreferences::PREFERENCE_CONPH_NOTIFICATIONS;
if ($request->isFormPost()) {
$notifications = $request->getInt($pref);
$preferences->setPreference($pref, $notifications);
$preferences->save();
return id(new AphrontRedirectResponse())
->setURI($this->getPanelURI('?saved=true'));
}
$form = id(new AphrontFormView())
->setUser($user)
->appendChild(
id(new AphrontFormSelectControl())
->setLabel(pht('Conpherence Notifications'))
->setName($pref)
->setValue($preferences->getPreference($pref))
->setOptions(
array(
ConpherenceSettings::EMAIL_ALWAYS
=> pht('Email Always'),
ConpherenceSettings::NOTIFICATIONS_ONLY
=> pht('Notifications Only'),
))
->setCaption(
- pht('Should Conpherence send emails for updates or '.
- 'notifications only? This global setting can be overridden '.
- 'on a per-thread basis within Conpherence.')))
+ pht(
+ 'Should Conpherence send emails for updates or '.
+ 'notifications only? This global setting can be overridden '.
+ 'on a per-thread basis within Conpherence.')))
->appendChild(
id(new AphrontFormSubmitControl())
->setValue(pht('Save Preferences')));
$form_box = id(new PHUIObjectBoxView())
->setHeaderText(pht('Conpherence Preferences'))
->setForm($form)
->setFormSaved($request->getBool('saved'));
return array(
$form_box,
);
}
}
diff --git a/src/applications/settings/panel/PhabricatorDeveloperPreferencesSettingsPanel.php b/src/applications/settings/panel/PhabricatorDeveloperPreferencesSettingsPanel.php
index 4d59d1941..1d81cc67b 100644
--- a/src/applications/settings/panel/PhabricatorDeveloperPreferencesSettingsPanel.php
+++ b/src/applications/settings/panel/PhabricatorDeveloperPreferencesSettingsPanel.php
@@ -1,97 +1,99 @@
<?php
final class PhabricatorDeveloperPreferencesSettingsPanel
extends PhabricatorSettingsPanel {
public function getPanelKey() {
return 'developer';
}
public function getPanelName() {
return pht('Developer Settings');
}
public function getPanelGroup() {
return pht('Developer');
}
public function processRequest(AphrontRequest $request) {
$user = $request->getUser();
$preferences = $user->loadPreferences();
$pref_dark_console = PhabricatorUserPreferences::PREFERENCE_DARK_CONSOLE;
$dark_console_value = $preferences->getPreference($pref_dark_console);
if ($request->isFormPost()) {
$new_dark_console = $request->getBool($pref_dark_console);
$preferences->setPreference($pref_dark_console, $new_dark_console);
// If the user turned Dark Console on, enable it (as though they had hit
// "`").
if ($new_dark_console && !$dark_console_value) {
$user->setConsoleVisible(true);
$user->save();
}
$preferences->save();
return id(new AphrontRedirectResponse())
->setURI($this->getPanelURI('?saved=true'));
}
$is_console_enabled = PhabricatorEnv::getEnvConfig('darkconsole.enabled');
$preamble = pht(
- '**DarkConsole** is a developer console which can help build and '.
- 'debug Phabricator applications. It includes tools for understanding '.
- 'errors, performance, service calls, and other low-level aspects of '.
- 'Phabricator\'s inner workings.');
+ "**DarkConsole** is a developer console which can help build and ".
+ "debug Phabricator applications. It includes tools for understanding ".
+ "errors, performance, service calls, and other low-level aspects of ".
+ "Phabricator's inner workings.");
if ($is_console_enabled) {
$instructions = pht(
"%s\n\n".
'You can enable it for your account below. Enabling DarkConsole will '.
'slightly decrease performance, but give you access to debugging '.
'tools. You may want to disable it again later if you only need it '.
'temporarily.'.
"\n\n".
- 'NOTE: After enabling DarkConsole, **press the ##`## key on your '.
+ 'NOTE: After enabling DarkConsole, **press the ##%s## key on your '.
'keyboard** to show or hide it.',
- $preamble);
+ $preamble,
+ '`');
} else {
$instructions = pht(
"%s\n\n".
'Before you can turn on DarkConsole, it needs to be enabled in '.
- 'the configuration for this install (`darkconsole.enabled`).',
- $preamble);
+ 'the configuration for this install (`%s`).',
+ $preamble,
+ 'darkconsole.enabled');
}
$form = id(new AphrontFormView())
->setUser($user)
->appendRemarkupInstructions($instructions)
->appendChild(
id(new AphrontFormSelectControl())
->setLabel(pht('Dark Console'))
->setName($pref_dark_console)
->setValue($dark_console_value)
->setOptions(
array(
0 => pht('Disable DarkConsole'),
1 => pht('Enable DarkConsole'),
))
->setDisabled(!$is_console_enabled))
->appendChild(
id(new AphrontFormSubmitControl())
->setValue(pht('Save Preferences')));
$form_box = id(new PHUIObjectBoxView())
->setHeaderText(pht('Developer Settings'))
->setFormSaved($request->getBool('saved'))
->setForm($form);
return array(
$form_box,
);
}
}
diff --git a/src/applications/settings/panel/PhabricatorDiffPreferencesSettingsPanel.php b/src/applications/settings/panel/PhabricatorDiffPreferencesSettingsPanel.php
index cc6a4e91e..72fca5e87 100644
--- a/src/applications/settings/panel/PhabricatorDiffPreferencesSettingsPanel.php
+++ b/src/applications/settings/panel/PhabricatorDiffPreferencesSettingsPanel.php
@@ -1,105 +1,106 @@
<?php
final class PhabricatorDiffPreferencesSettingsPanel
extends PhabricatorSettingsPanel {
public function getPanelKey() {
return 'diff';
}
public function getPanelName() {
return pht('Diff Preferences');
}
public function getPanelGroup() {
return pht('Application Settings');
}
public function processRequest(AphrontRequest $request) {
$user = $request->getUser();
$preferences = $user->loadPreferences();
$pref_unified = PhabricatorUserPreferences::PREFERENCE_DIFF_UNIFIED;
$pref_ghosts = PhabricatorUserPreferences::PREFERENCE_DIFF_GHOSTS;
$pref_filetree = PhabricatorUserPreferences::PREFERENCE_DIFF_FILETREE;
if ($request->isFormPost()) {
$filetree = $request->getInt($pref_filetree);
if ($filetree && !$preferences->getPreference($pref_filetree)) {
$preferences->setPreference(
PhabricatorUserPreferences::PREFERENCE_NAV_COLLAPSED,
false);
}
$preferences->setPreference($pref_filetree, $filetree);
$unified = $request->getStr($pref_unified);
$preferences->setPreference($pref_unified, $unified);
$ghosts = $request->getStr($pref_ghosts);
$preferences->setPreference($pref_ghosts, $ghosts);
$preferences->save();
return id(new AphrontRedirectResponse())
->setURI($this->getPanelURI('?saved=true'));
}
$form = id(new AphrontFormView())
->setUser($user)
->appendRemarkupInstructions(
pht(
'Phabricator normally shows diffs in a side-by-side layout on '.
'large screens, and automatically switches to a unified '.
'view on small screens (like mobile phones). If you prefer '.
'unified diffs even on large screens, you can select them as '.
'the default layout.'))
->appendChild(
id(new AphrontFormSelectControl())
->setLabel(pht('Show Unified Diffs'))
->setName($pref_unified)
->setValue($preferences->getPreference($pref_unified))
->setOptions(
array(
'default' => pht('On Small Screens'),
'unified' => pht('Always'),
)))
->appendChild(
id(new AphrontFormSelectControl())
->setLabel(pht('Show Older Inlines'))
->setName($pref_ghosts)
->setValue($preferences->getPreference($pref_ghosts))
->setOptions(
array(
'default' => pht('Enabled'),
'disabled' => pht('Disabled'),
)))
->appendChild(
id(new AphrontFormSelectControl())
->setLabel(pht('Show Filetree'))
->setName($pref_filetree)
->setValue($preferences->getPreference($pref_filetree))
->setOptions(
array(
0 => pht('Disable Filetree'),
1 => pht('Enable Filetree'),
))
->setCaption(
- pht('When looking at a revision or commit, enable a sidebar '.
- 'showing affected files. You can press %s to show or hide '.
- 'the sidebar.',
- phutil_tag('tt', array(), 'f'))))
+ pht(
+ 'When looking at a revision or commit, enable a sidebar '.
+ 'showing affected files. You can press %s to show or hide '.
+ 'the sidebar.',
+ phutil_tag('tt', array(), 'f'))))
->appendChild(
id(new AphrontFormSubmitControl())
->setValue(pht('Save Preferences')));
$form_box = id(new PHUIObjectBoxView())
->setHeaderText(pht('Diff Preferences'))
->setFormSaved($request->getBool('saved'))
->setForm($form);
return array(
$form_box,
);
}
}
diff --git a/src/applications/settings/panel/PhabricatorDisplayPreferencesSettingsPanel.php b/src/applications/settings/panel/PhabricatorDisplayPreferencesSettingsPanel.php
index 71f533dcd..c2dcc36e6 100644
--- a/src/applications/settings/panel/PhabricatorDisplayPreferencesSettingsPanel.php
+++ b/src/applications/settings/panel/PhabricatorDisplayPreferencesSettingsPanel.php
@@ -1,174 +1,178 @@
<?php
final class PhabricatorDisplayPreferencesSettingsPanel
extends PhabricatorSettingsPanel {
public function getPanelKey() {
return 'display';
}
public function getPanelName() {
return pht('Display Preferences');
}
public function getPanelGroup() {
return pht('Application Settings');
}
public function processRequest(AphrontRequest $request) {
$user = $request->getUser();
$preferences = $user->loadPreferences();
$pref_monospaced = PhabricatorUserPreferences::PREFERENCE_MONOSPACED;
$pref_editor = PhabricatorUserPreferences::PREFERENCE_EDITOR;
$pref_multiedit = PhabricatorUserPreferences::PREFERENCE_MULTIEDIT;
$pref_titles = PhabricatorUserPreferences::PREFERENCE_TITLES;
$pref_monospaced_textareas =
PhabricatorUserPreferences::PREFERENCE_MONOSPACED_TEXTAREAS;
$errors = array();
$e_editor = null;
if ($request->isFormPost()) {
$monospaced = $request->getStr($pref_monospaced);
$monospaced = PhabricatorUserPreferences::filterMonospacedCSSRule(
$monospaced);
$preferences->setPreference($pref_titles, $request->getStr($pref_titles));
$preferences->setPreference($pref_editor, $request->getStr($pref_editor));
$preferences->setPreference(
$pref_multiedit,
$request->getStr($pref_multiedit));
$preferences->setPreference($pref_monospaced, $monospaced);
$preferences->setPreference(
$pref_monospaced_textareas,
$request->getStr($pref_monospaced_textareas));
$editor_pattern = $preferences->getPreference($pref_editor);
if (strlen($editor_pattern)) {
$ok = PhabricatorHelpEditorProtocolController::hasAllowedProtocol(
$editor_pattern);
if (!$ok) {
$allowed_key = 'uri.allowed-editor-protocols';
$allowed_protocols = PhabricatorEnv::getEnvConfig($allowed_key);
$proto_names = array();
foreach (array_keys($allowed_protocols) as $protocol) {
$proto_names[] = $protocol.'://';
}
$errors[] = pht(
'Editor link has an invalid or missing protocol. You must '.
'use a whitelisted editor protocol from this list: %s. To '.
'add protocols, update %s.',
implode(', ', $proto_names),
phutil_tag('tt', array(), $allowed_key));
$e_editor = pht('Invalid');
}
}
if (!$errors) {
$preferences->save();
return id(new AphrontRedirectResponse())
->setURI($this->getPanelURI('?saved=true'));
}
}
$example_string = <<<EXAMPLE
// This is what your monospaced font currently looks like.
function helloWorld() {
alert("Hello world!");
}
EXAMPLE;
$editor_doc_link = phutil_tag(
'a',
array(
'href' => PhabricatorEnv::getDoclink(
'User Guide: Configuring an External Editor'),
),
pht('User Guide: Configuring an External Editor'));
$pref_monospaced_textareas_value = $preferences
->getPreference($pref_monospaced_textareas);
if (!$pref_monospaced_textareas_value) {
$pref_monospaced_textareas_value = 'disabled';
}
- $editor_instructions = pht('Link to edit files in external editor. '.
+ $editor_instructions = pht(
+ 'Link to edit files in external editor. '.
'%%f is replaced by filename, %%l by line number, %%r by repository '.
'callsign, %%%% by literal %%. For documentation, see: %s',
$editor_doc_link);
- $font_instructions = pht('Overrides default fonts in tools like '.
- 'Differential. Input should be valid CSS "font" declaration, such as '.
+ $font_instructions = pht(
+ 'Overrides default fonts in tools like Differential. '.
+ 'Input should be valid CSS "font" declaration, such as '.
'"13px Consolas"');
$form = id(new AphrontFormView())
->setUser($user)
->appendChild(
id(new AphrontFormSelectControl())
->setLabel(pht('Page Titles'))
->setName($pref_titles)
->setValue($preferences->getPreference($pref_titles))
->setOptions(
array(
'glyph' =>
- pht("In page titles, show Tool names as unicode glyphs: ".
+ pht(
+ 'In page titles, show Tool names as unicode glyphs: %s',
"\xE2\x9A\x99"),
'text' =>
- pht('In page titles, show Tool names as plain text: '.
+ pht(
+ 'In page titles, show Tool names as plain text: '.
'[Differential]'),
)))
->appendChild(
id(new AphrontFormTextControl())
->setLabel(pht('Editor Link'))
->setName($pref_editor)
->setCaption($editor_instructions)
->setError($e_editor)
->setValue($preferences->getPreference($pref_editor)))
->appendChild(
id(new AphrontFormSelectControl())
->setLabel(pht('Edit Multiple Files'))
->setName($pref_multiedit)
->setOptions(array(
'' => pht('Supported (paths separated by spaces)'),
'disable' => pht('Not Supported'),
))
->setValue($preferences->getPreference($pref_multiedit)))
->appendChild(
id(new AphrontFormTextControl())
->setLabel(pht('Monospaced Font'))
->setName($pref_monospaced)
->setCaption($font_instructions)
->setValue($preferences->getPreference($pref_monospaced)))
->appendChild(
id(new AphrontFormMarkupControl())
->setValue(phutil_tag(
'pre',
array('class' => 'PhabricatorMonospaced'),
$example_string)))
->appendChild(
id(new AphrontFormRadioButtonControl())
->setLabel(pht('Monospaced Textareas'))
->setName($pref_monospaced_textareas)
->setValue($pref_monospaced_textareas_value)
->addButton('enabled', pht('Enabled'),
pht('Show all textareas using the monospaced font defined above.'))
->addButton('disabled', pht('Disabled'), null));
$form->appendChild(
id(new AphrontFormSubmitControl())
->setValue(pht('Save Preferences')));
$form_box = id(new PHUIObjectBoxView())
->setHeaderText(pht('Display Preferences'))
->setFormErrors($errors)
->setFormSaved($request->getStr('saved') === 'true')
->setForm($form);
return array(
$form_box,
);
}
}
diff --git a/src/applications/settings/panel/PhabricatorEmailAddressesSettingsPanel.php b/src/applications/settings/panel/PhabricatorEmailAddressesSettingsPanel.php
index 7ad59d4d3..21ccd3830 100644
--- a/src/applications/settings/panel/PhabricatorEmailAddressesSettingsPanel.php
+++ b/src/applications/settings/panel/PhabricatorEmailAddressesSettingsPanel.php
@@ -1,392 +1,392 @@
<?php
final class PhabricatorEmailAddressesSettingsPanel
extends PhabricatorSettingsPanel {
public function getPanelKey() {
return 'email';
}
public function getPanelName() {
return pht('Email Addresses');
}
public function getPanelGroup() {
return pht('Email');
}
public function processRequest(AphrontRequest $request) {
$user = $request->getUser();
$editable = PhabricatorEnv::getEnvConfig('account.editable');
$uri = $request->getRequestURI();
$uri->setQueryParams(array());
if ($editable) {
$new = $request->getStr('new');
if ($new) {
return $this->returnNewAddressResponse($request, $uri, $new);
}
$delete = $request->getInt('delete');
if ($delete) {
return $this->returnDeleteAddressResponse($request, $uri, $delete);
}
}
$verify = $request->getInt('verify');
if ($verify) {
return $this->returnVerifyAddressResponse($request, $uri, $verify);
}
$primary = $request->getInt('primary');
if ($primary) {
return $this->returnPrimaryAddressResponse($request, $uri, $primary);
}
$emails = id(new PhabricatorUserEmail())->loadAllWhere(
'userPHID = %s ORDER BY address',
$user->getPHID());
$rowc = array();
$rows = array();
foreach ($emails as $email) {
$button_verify = javelin_tag(
'a',
array(
'class' => 'button small grey',
'href' => $uri->alter('verify', $email->getID()),
'sigil' => 'workflow',
),
pht('Verify'));
$button_make_primary = javelin_tag(
'a',
array(
'class' => 'button small grey',
'href' => $uri->alter('primary', $email->getID()),
'sigil' => 'workflow',
),
pht('Make Primary'));
$button_remove = javelin_tag(
'a',
array(
'class' => 'button small grey',
'href' => $uri->alter('delete', $email->getID()),
'sigil' => 'workflow',
),
pht('Remove'));
$button_primary = phutil_tag(
'a',
array(
'class' => 'button small disabled',
),
pht('Primary'));
if (!$email->getIsVerified()) {
$action = $button_verify;
} else if ($email->getIsPrimary()) {
$action = $button_primary;
} else {
$action = $button_make_primary;
}
if ($email->getIsPrimary()) {
$remove = $button_primary;
$rowc[] = 'highlighted';
} else {
$remove = $button_remove;
$rowc[] = null;
}
$rows[] = array(
$email->getAddress(),
$action,
$remove,
);
}
$table = new AphrontTableView($rows);
$table->setHeaders(
array(
pht('Email'),
pht('Status'),
pht('Remove'),
));
$table->setColumnClasses(
array(
'wide',
'action',
'action',
));
$table->setRowClasses($rowc);
$table->setColumnVisibility(
array(
true,
true,
$editable,
));
$view = new PHUIObjectBoxView();
$header = new PHUIHeaderView();
$header->setHeader(pht('Email Addresses'));
if ($editable) {
$icon = id(new PHUIIconView())
->setIconFont('fa-plus');
$button = new PHUIButtonView();
$button->setText(pht('Add New Address'));
$button->setTag('a');
$button->setHref($uri->alter('new', 'true'));
$button->setIcon($icon);
$button->addSigil('workflow');
$header->addActionLink($button);
}
$view->setHeader($header);
$view->appendChild($table);
return $view;
}
private function returnNewAddressResponse(
AphrontRequest $request,
PhutilURI $uri,
$new) {
$user = $request->getUser();
$e_email = true;
$email = null;
$errors = array();
if ($request->isDialogFormPost()) {
$email = trim($request->getStr('email'));
if ($new == 'verify') {
// The user clicked "Done" from the "an email has been sent" dialog.
return id(new AphrontReloadResponse())->setURI($uri);
}
PhabricatorSystemActionEngine::willTakeAction(
array($user->getPHID()),
new PhabricatorSettingsAddEmailAction(),
1);
if (!strlen($email)) {
$e_email = pht('Required');
$errors[] = pht('Email is required.');
} else if (!PhabricatorUserEmail::isValidAddress($email)) {
$e_email = pht('Invalid');
$errors[] = PhabricatorUserEmail::describeValidAddresses();
} else if (!PhabricatorUserEmail::isAllowedAddress($email)) {
$e_email = pht('Disallowed');
$errors[] = PhabricatorUserEmail::describeAllowedAddresses();
}
if ($e_email === true) {
$application_email = id(new PhabricatorMetaMTAApplicationEmailQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withAddresses(array($email))
->executeOne();
if ($application_email) {
$e_email = pht('In Use');
$errors[] = $application_email->getInUseMessage();
}
}
if (!$errors) {
$object = id(new PhabricatorUserEmail())
->setAddress($email)
->setIsVerified(0);
try {
id(new PhabricatorUserEditor())
->setActor($user)
->addEmail($user, $object);
$object->sendVerificationEmail($user);
$dialog = id(new AphrontDialogView())
->setUser($user)
->addHiddenInput('new', 'verify')
->setTitle(pht('Verification Email Sent'))
->appendChild(phutil_tag('p', array(), pht(
'A verification email has been sent. Click the link in the '.
'email to verify your address.')))
->setSubmitURI($uri)
->addSubmitButton(pht('Done'));
return id(new AphrontDialogResponse())->setDialog($dialog);
} catch (AphrontDuplicateKeyQueryException $ex) {
$e_email = pht('Duplicate');
$errors[] = pht('Another user already has this email.');
}
}
}
if ($errors) {
$errors = id(new PHUIInfoView())
->setErrors($errors);
}
$form = id(new PHUIFormLayoutView())
->appendChild(
id(new AphrontFormTextControl())
->setLabel(pht('Email'))
->setName('email')
->setValue($email)
->setCaption(PhabricatorUserEmail::describeAllowedAddresses())
->setError($e_email));
$dialog = id(new AphrontDialogView())
->setUser($user)
->addHiddenInput('new', 'true')
->setTitle(pht('New Address'))
->appendChild($errors)
->appendChild($form)
->addSubmitButton(pht('Save'))
->addCancelButton($uri);
return id(new AphrontDialogResponse())->setDialog($dialog);
}
private function returnDeleteAddressResponse(
AphrontRequest $request,
PhutilURI $uri,
$email_id) {
$user = $request->getUser();
// NOTE: You can only delete your own email addresses, and you can not
// delete your primary address.
$email = id(new PhabricatorUserEmail())->loadOneWhere(
'id = %d AND userPHID = %s AND isPrimary = 0',
$email_id,
$user->getPHID());
if (!$email) {
return new Aphront404Response();
}
if ($request->isFormPost()) {
id(new PhabricatorUserEditor())
->setActor($user)
->removeEmail($user, $email);
return id(new AphrontRedirectResponse())->setURI($uri);
}
$address = $email->getAddress();
$dialog = id(new AphrontDialogView())
->setUser($user)
->addHiddenInput('delete', $email_id)
->setTitle(pht("Really delete address '%s'?", $address))
->appendParagraph(
pht(
'Are you sure you want to delete this address? You will no '.
- 'longer be able to use it to login.'))
+ 'longer be able to use it to login.'))
->appendParagraph(
pht(
'Note: Removing an email address from your account will invalidate '.
'any outstanding password reset links.'))
->addSubmitButton(pht('Delete'))
->addCancelButton($uri);
return id(new AphrontDialogResponse())->setDialog($dialog);
}
private function returnVerifyAddressResponse(
AphrontRequest $request,
PhutilURI $uri,
$email_id) {
$user = $request->getUser();
// NOTE: You can only send more email for your unverified addresses.
$email = id(new PhabricatorUserEmail())->loadOneWhere(
'id = %d AND userPHID = %s AND isVerified = 0',
$email_id,
$user->getPHID());
if (!$email) {
return new Aphront404Response();
}
if ($request->isFormPost()) {
$email->sendVerificationEmail($user);
return id(new AphrontRedirectResponse())->setURI($uri);
}
$address = $email->getAddress();
$dialog = id(new AphrontDialogView())
->setUser($user)
->addHiddenInput('verify', $email_id)
->setTitle(pht('Send Another Verification Email?'))
->appendChild(phutil_tag('p', array(), pht(
'Send another copy of the verification email to %s?',
$address)))
->addSubmitButton(pht('Send Email'))
->addCancelButton($uri);
return id(new AphrontDialogResponse())->setDialog($dialog);
}
private function returnPrimaryAddressResponse(
AphrontRequest $request,
PhutilURI $uri,
$email_id) {
$user = $request->getUser();
$token = id(new PhabricatorAuthSessionEngine())->requireHighSecuritySession(
$user,
$request,
$this->getPanelURI());
// NOTE: You can only make your own verified addresses primary.
$email = id(new PhabricatorUserEmail())->loadOneWhere(
'id = %d AND userPHID = %s AND isVerified = 1 AND isPrimary = 0',
$email_id,
$user->getPHID());
if (!$email) {
return new Aphront404Response();
}
if ($request->isFormPost()) {
id(new PhabricatorUserEditor())
->setActor($user)
->changePrimaryEmail($user, $email);
return id(new AphrontRedirectResponse())->setURI($uri);
}
$address = $email->getAddress();
$dialog = id(new AphrontDialogView())
->setUser($user)
->addHiddenInput('primary', $email_id)
->setTitle(pht('Change primary email address?'))
->appendParagraph(
pht(
'If you change your primary address, Phabricator will send all '.
'email to %s.',
$address))
->appendParagraph(
pht(
'Note: Changing your primary email address will invalidate any '.
'outstanding password reset links.'))
->addSubmitButton(pht('Change Primary Address'))
->addCancelButton($uri);
return id(new AphrontDialogResponse())->setDialog($dialog);
}
}
diff --git a/src/applications/settings/panel/PhabricatorEmailFormatSettingsPanel.php b/src/applications/settings/panel/PhabricatorEmailFormatSettingsPanel.php
index a2b040aab..f7dd7909e 100644
--- a/src/applications/settings/panel/PhabricatorEmailFormatSettingsPanel.php
+++ b/src/applications/settings/panel/PhabricatorEmailFormatSettingsPanel.php
@@ -1,227 +1,227 @@
<?php
final class PhabricatorEmailFormatSettingsPanel
extends PhabricatorSettingsPanel {
public function getPanelKey() {
return 'emailformat';
}
public function getPanelName() {
return pht('Email Format');
}
public function getPanelGroup() {
return pht('Email');
}
public function processRequest(AphrontRequest $request) {
$user = $request->getUser();
$preferences = $user->loadPreferences();
$pref_re_prefix = PhabricatorUserPreferences::PREFERENCE_RE_PREFIX;
$pref_vary = PhabricatorUserPreferences::PREFERENCE_VARY_SUBJECT;
$prefs_html_email = PhabricatorUserPreferences::PREFERENCE_HTML_EMAILS;
$errors = array();
if ($request->isFormPost()) {
if (PhabricatorMetaMTAMail::shouldMultiplexAllMail()) {
if ($request->getStr($pref_re_prefix) == 'default') {
$preferences->unsetPreference($pref_re_prefix);
} else {
$preferences->setPreference(
$pref_re_prefix,
$request->getBool($pref_re_prefix));
}
if ($request->getStr($pref_vary) == 'default') {
$preferences->unsetPreference($pref_vary);
} else {
$preferences->setPreference(
$pref_vary,
$request->getBool($pref_vary));
}
if ($request->getStr($prefs_html_email) == 'default') {
$preferences->unsetPreference($prefs_html_email);
} else {
$preferences->setPreference(
$prefs_html_email,
$request->getBool($prefs_html_email));
}
}
$preferences->save();
return id(new AphrontRedirectResponse())
->setURI($this->getPanelURI('?saved=true'));
}
$re_prefix_default = PhabricatorEnv::getEnvConfig('metamta.re-prefix')
? pht('Enabled')
: pht('Disabled');
$vary_default = PhabricatorEnv::getEnvConfig('metamta.vary-subjects')
? pht('Vary')
: pht('Do Not Vary');
- $html_emails_default = 'Plain Text';
+ $html_emails_default = pht('Plain Text');
$re_prefix_value = $preferences->getPreference($pref_re_prefix);
if ($re_prefix_value === null) {
$re_prefix_value = 'default';
} else {
$re_prefix_value = $re_prefix_value
? 'true'
: 'false';
}
$vary_value = $preferences->getPreference($pref_vary);
if ($vary_value === null) {
$vary_value = 'default';
} else {
$vary_value = $vary_value
? 'true'
: 'false';
}
$html_emails_value = $preferences->getPreference($prefs_html_email);
if ($html_emails_value === null) {
$html_emails_value = 'default';
} else {
$html_emails_value = $html_emails_value
? 'true'
: 'false';
}
$form = new AphrontFormView();
$form
->setUser($user);
if (PhabricatorMetaMTAMail::shouldMultiplexAllMail()) {
$html_email_control = id(new AphrontFormSelectControl())
->setName($prefs_html_email)
->setOptions(
array(
'default' => pht('Default (%s)', $html_emails_default),
'true' => pht('Send HTML Email'),
'false' => pht('Send Plain Text Email'),
))
->setValue($html_emails_value);
$re_control = id(new AphrontFormSelectControl())
->setName($pref_re_prefix)
->setOptions(
array(
'default' => pht('Use Server Default (%s)', $re_prefix_default),
'true' => pht('Enable "Re:" prefix'),
'false' => pht('Disable "Re:" prefix'),
))
->setValue($re_prefix_value);
$vary_control = id(new AphrontFormSelectControl())
->setName($pref_vary)
->setOptions(
array(
'default' => pht('Use Server Default (%s)', $vary_default),
'true' => pht('Vary Subjects'),
'false' => pht('Do Not Vary Subjects'),
))
->setValue($vary_value);
} else {
$html_email_control = id(new AphrontFormStaticControl())
- ->setValue('Server Default ('.$html_emails_default.')');
+ ->setValue(pht('Server Default (%s)', $html_emails_default));
$re_control = id(new AphrontFormStaticControl())
- ->setValue('Server Default ('.$re_prefix_default.')');
+ ->setValue(pht('Server Default (%s)', $re_prefix_default));
$vary_control = id(new AphrontFormStaticControl())
- ->setValue('Server Default ('.$vary_default.')');
+ ->setValue(pht('Server Default (%s)', $vary_default));
}
$form
->appendRemarkupInstructions(
pht(
'These settings fine-tune some technical aspects of how email is '.
'formatted. You may be able to adjust them to make mail more '.
'useful or improve threading.'));
if (!PhabricatorMetaMTAMail::shouldMultiplexAllMail()) {
$form->appendRemarkupInstructions(
pht(
'NOTE: This install of Phabricator is configured to send a '.
'single mail message to all recipients, so all settings are '.
'locked at the server default value.'));
}
$form
->appendRemarkupInstructions(
pht(
"You can use the **HTML Email** setting to control whether ".
"Phabricator send you HTML email (which has more color and ".
"formatting) or plain text email (which is more compatible).\n".
"\n".
"WARNING: This feature is new and experimental! If you enable ".
"it, mail may not render properly and replying to mail may not ".
"work as well."))
->appendChild(
$html_email_control
->setLabel(pht('HTML Email')))
->appendRemarkupInstructions('')
->appendRemarkupInstructions(
pht(
'The **Add "Re:" Prefix** setting adds "Re:" in front of all '.
'messages, even if they are not replies. If you use **Mail.app** on '.
'Mac OS X, this may improve mail threading.'.
"\n\n".
"| Setting | Example Mail Subject\n".
"|------------------------|----------------\n".
"| Enable \"Re:\" Prefix | ".
"`Re: [Differential] [Accepted] D123: Example Revision`\n".
"| Disable \"Re:\" Prefix | ".
"`[Differential] [Accepted] D123: Example Revision`"))
->appendChild(
$re_control
->setLabel(pht('Add "Re:" Prefix')))
->appendRemarkupInstructions('')
->appendRemarkupInstructions(
pht(
'With **Vary Subjects** enabled, most mail subject lines will '.
'include a brief description of their content, like **[Closed]** '.
'for a notification about someone closing a task.'.
"\n\n".
"| Setting | Example Mail Subject\n".
"|----------------------|----------------\n".
"| Vary Subjects | ".
"`[Maniphest] [Closed] T123: Example Task`\n".
"| Do Not Vary Subjects | ".
"`[Maniphest] T123: Example Task`\n".
"\n".
'This can make mail more useful, but some clients have difficulty '.
'threading these messages. Disabling this option may improve '.
'threading, at the cost of less useful subject lines.'))
->appendChild(
$vary_control
->setLabel(pht('Vary Subjects')));
$form
->appendChild(
id(new AphrontFormSubmitControl())
->setValue(pht('Save Preferences')));
$form_box = id(new PHUIObjectBoxView())
->setHeaderText(pht('Email Format'))
->setFormSaved($request->getStr('saved'))
->setFormErrors($errors)
->setForm($form);
return id(new AphrontNullView())
->appendChild(
array(
$form_box,
));
}
}
diff --git a/src/applications/settings/panel/PhabricatorSSHKeysSettingsPanel.php b/src/applications/settings/panel/PhabricatorSSHKeysSettingsPanel.php
index 2749e7147..288552fd4 100644
--- a/src/applications/settings/panel/PhabricatorSSHKeysSettingsPanel.php
+++ b/src/applications/settings/panel/PhabricatorSSHKeysSettingsPanel.php
@@ -1,79 +1,79 @@
<?php
final class PhabricatorSSHKeysSettingsPanel extends PhabricatorSettingsPanel {
public function isEditableByAdministrators() {
return true;
}
public function getPanelKey() {
return 'ssh';
}
public function getPanelName() {
return pht('SSH Public Keys');
}
public function getPanelGroup() {
return pht('Authentication');
}
public function isEnabled() {
return true;
}
public function processRequest(AphrontRequest $request) {
$user = $this->getUser();
$viewer = $request->getUser();
$keys = id(new PhabricatorAuthSSHKeyQuery())
->setViewer($viewer)
->withObjectPHIDs(array($user->getPHID()))
->execute();
$table = id(new PhabricatorAuthSSHKeyTableView())
->setUser($viewer)
->setKeys($keys)
->setCanEdit(true)
- ->setNoDataString("You haven't added any SSH Public Keys.");
+ ->setNoDataString(pht("You haven't added any SSH Public Keys."));
$panel = new PHUIObjectBoxView();
$header = new PHUIHeaderView();
$upload_icon = id(new PHUIIconView())
->setIconFont('fa-upload');
$upload_button = id(new PHUIButtonView())
->setText(pht('Upload Public Key'))
->setHref('/auth/sshkey/upload/?objectPHID='.$user->getPHID())
->setWorkflow(true)
->setTag('a')
->setIcon($upload_icon);
try {
PhabricatorSSHKeyGenerator::assertCanGenerateKeypair();
$can_generate = true;
} catch (Exception $ex) {
$can_generate = false;
}
$generate_icon = id(new PHUIIconView())
->setIconFont('fa-lock');
$generate_button = id(new PHUIButtonView())
->setText(pht('Generate Keypair'))
->setHref('/auth/sshkey/generate/?objectPHID='.$user->getPHID())
->setTag('a')
->setWorkflow(true)
->setDisabled(!$can_generate)
->setIcon($generate_icon);
$header->setHeader(pht('SSH Public Keys'));
$header->addActionLink($generate_button);
$header->addActionLink($upload_button);
$panel->setHeader($header);
$panel->appendChild($table);
return $panel;
}
}
diff --git a/src/applications/settings/panel/PhabricatorSearchPreferencesSettingsPanel.php b/src/applications/settings/panel/PhabricatorSearchPreferencesSettingsPanel.php
index 496aed67e..f05a5dd9f 100644
--- a/src/applications/settings/panel/PhabricatorSearchPreferencesSettingsPanel.php
+++ b/src/applications/settings/panel/PhabricatorSearchPreferencesSettingsPanel.php
@@ -1,62 +1,62 @@
<?php
final class PhabricatorSearchPreferencesSettingsPanel
extends PhabricatorSettingsPanel {
public function getPanelKey() {
return 'search';
}
public function getPanelName() {
return pht('Search Preferences');
}
public function getPanelGroup() {
return pht('Application Settings');
}
public function processRequest(AphrontRequest $request) {
$user = $request->getUser();
$preferences = $user->loadPreferences();
$pref_jump = PhabricatorUserPreferences::PREFERENCE_SEARCHBAR_JUMP;
$pref_shortcut = PhabricatorUserPreferences::PREFERENCE_SEARCH_SHORTCUT;
if ($request->isFormPost()) {
$preferences->setPreference($pref_jump,
$request->getBool($pref_jump));
$preferences->setPreference($pref_shortcut,
$request->getBool($pref_shortcut));
$preferences->save();
return id(new AphrontRedirectResponse())
->setURI($this->getPanelURI('?saved=true'));
}
$form = id(new AphrontFormView())
->setUser($user)
->appendChild(
id(new AphrontFormCheckboxControl())
->addCheckbox($pref_jump,
1,
pht('Enable jump nav functionality in all search boxes.'),
$preferences->getPreference($pref_jump, 1))
->addCheckbox($pref_shortcut,
1,
- pht("Press '/' to focus the search input."),
+ pht("Press '%s' to focus the search input.", '/'),
$preferences->getPreference($pref_shortcut, 1)))
->appendChild(
id(new AphrontFormSubmitControl())
->setValue(pht('Save')));
$form_box = id(new PHUIObjectBoxView())
->setHeaderText(pht('Search Preferences'))
->setFormSaved($request->getStr('saved') === 'true')
->setForm($form);
return array(
$form_box,
);
}
}
diff --git a/src/applications/settings/storage/PhabricatorUserPreferences.php b/src/applications/settings/storage/PhabricatorUserPreferences.php
index 7732df556..d54cf6cb4 100644
--- a/src/applications/settings/storage/PhabricatorUserPreferences.php
+++ b/src/applications/settings/storage/PhabricatorUserPreferences.php
@@ -1,112 +1,112 @@
<?php
final class PhabricatorUserPreferences extends PhabricatorUserDAO {
const PREFERENCE_MONOSPACED = 'monospaced';
const PREFERENCE_DARK_CONSOLE = 'dark_console';
const PREFERENCE_EDITOR = 'editor';
const PREFERENCE_MULTIEDIT = 'multiedit';
const PREFERENCE_TITLES = 'titles';
const PREFERENCE_MONOSPACED_TEXTAREAS = 'monospaced-textareas';
const PREFERENCE_TIME_FORMAT = 'time-format';
const PREFERENCE_WEEK_START_DAY = 'week-start-day';
const PREFERENCE_RE_PREFIX = 're-prefix';
const PREFERENCE_NO_SELF_MAIL = 'self-mail';
const PREFERENCE_NO_MAIL = 'no-mail';
const PREFERENCE_MAILTAGS = 'mailtags';
const PREFERENCE_VARY_SUBJECT = 'vary-subject';
const PREFERENCE_HTML_EMAILS = 'html-emails';
const PREFERENCE_SEARCHBAR_JUMP = 'searchbar-jump';
const PREFERENCE_SEARCH_SHORTCUT = 'search-shortcut';
- const PREFERENCE_SEARCH_SCOPE = 'search-scope';
+ const PREFERENCE_SEARCH_SCOPE = 'search-scope';
const PREFERENCE_DIFFUSION_BLAME = 'diffusion-blame';
const PREFERENCE_DIFFUSION_COLOR = 'diffusion-color';
const PREFERENCE_NAV_COLLAPSED = 'nav-collapsed';
const PREFERENCE_NAV_WIDTH = 'nav-width';
const PREFERENCE_APP_TILES = 'app-tiles';
const PREFERENCE_APP_PINNED = 'app-pinned';
const PREFERENCE_DIFF_UNIFIED = 'diff-unified';
const PREFERENCE_DIFF_FILETREE = 'diff-filetree';
- const PREFERENCE_DIFF_GHOSTS = 'diff-ghosts';
+ const PREFERENCE_DIFF_GHOSTS = 'diff-ghosts';
const PREFERENCE_CONPH_NOTIFICATIONS = 'conph-notifications';
- const PREFERENCE_CONPHERENCE_COLUMN = 'conpherence-column';
+ const PREFERENCE_CONPHERENCE_COLUMN = 'conpherence-column';
// These are in an unusual order for historic reasons.
const MAILTAG_PREFERENCE_NOTIFY = 0;
const MAILTAG_PREFERENCE_EMAIL = 1;
const MAILTAG_PREFERENCE_IGNORE = 2;
protected $userPHID;
protected $preferences = array();
protected function getConfiguration() {
return array(
self::CONFIG_SERIALIZATION => array(
'preferences' => self::SERIALIZATION_JSON,
),
self::CONFIG_TIMESTAMPS => false,
self::CONFIG_KEY_SCHEMA => array(
'userPHID' => array(
'columns' => array('userPHID'),
'unique' => true,
),
),
) + parent::getConfiguration();
}
public function getPreference($key, $default = null) {
return idx($this->preferences, $key, $default);
}
public function setPreference($key, $value) {
$this->preferences[$key] = $value;
return $this;
}
public function unsetPreference($key) {
unset($this->preferences[$key]);
return $this;
}
public function getPinnedApplications(array $apps, PhabricatorUser $viewer) {
$pref_pinned = self::PREFERENCE_APP_PINNED;
$pinned = $this->getPreference($pref_pinned);
if ($pinned) {
return $pinned;
}
$pref_tiles = self::PREFERENCE_APP_TILES;
$tiles = $this->getPreference($pref_tiles, array());
$full_tile = 'full';
$large = array();
foreach ($apps as $app) {
$show = $app->isPinnedByDefault($viewer);
// TODO: This is legacy stuff, clean it up eventually. This approximately
// retains the old "tiles" preference.
if (isset($tiles[get_class($app)])) {
$show = ($tiles[get_class($app)] == $full_tile);
}
if ($show) {
$large[] = get_class($app);
}
}
return $large;
}
public static function filterMonospacedCSSRule($monospaced) {
// Prevent the user from doing dangerous things.
return preg_replace('/[^a-z0-9 ,".]+/i', '', $monospaced);
}
}
diff --git a/src/applications/slowvote/conduit/SlowvoteInfoConduitAPIMethod.php b/src/applications/slowvote/conduit/SlowvoteInfoConduitAPIMethod.php
index 50478abc0..cecd799ad 100644
--- a/src/applications/slowvote/conduit/SlowvoteInfoConduitAPIMethod.php
+++ b/src/applications/slowvote/conduit/SlowvoteInfoConduitAPIMethod.php
@@ -1,47 +1,47 @@
<?php
final class SlowvoteInfoConduitAPIMethod extends SlowvoteConduitAPIMethod {
public function getAPIMethodName() {
return 'slowvote.info';
}
public function getMethodDescription() {
- return 'Retrieve an array of information about a poll.';
+ return pht('Retrieve an array of information about a poll.');
}
protected function defineParamTypes() {
return array(
'poll_id' => 'required id',
);
}
protected function defineReturnType() {
return 'nonempty dict';
}
protected function defineErrorTypes() {
return array(
- 'ERR_BAD_POLL' => 'No such poll exists',
+ 'ERR_BAD_POLL' => pht('No such poll exists.'),
);
}
protected function execute(ConduitAPIRequest $request) {
$poll_id = $request->getValue('poll_id');
$poll = id(new PhabricatorSlowvotePoll())->load($poll_id);
if (!$poll) {
throw new ConduitException('ERR_BAD_POLL');
}
$result = array(
'id' => $poll->getID(),
'phid' => $poll->getPHID(),
'authorPHID' => $poll->getAuthorPHID(),
'question' => $poll->getQuestion(),
'uri' => PhabricatorEnv::getProductionURI('/V'.$poll->getID()),
);
return $result;
}
}
diff --git a/src/applications/subscriptions/command/PhabricatorSubscriptionsSubscribeEmailCommand.php b/src/applications/subscriptions/command/PhabricatorSubscriptionsSubscribeEmailCommand.php
index a20da47c4..0c361ad2c 100644
--- a/src/applications/subscriptions/command/PhabricatorSubscriptionsSubscribeEmailCommand.php
+++ b/src/applications/subscriptions/command/PhabricatorSubscriptionsSubscribeEmailCommand.php
@@ -1,74 +1,75 @@
<?php
final class PhabricatorSubscriptionsSubscribeEmailCommand
extends MetaMTAEmailTransactionCommand {
public function getCommand() {
return 'subscribe';
}
public function getCommandSyntax() {
return '**!subscribe** //username #project ...//';
}
public function getCommandSummary() {
return pht('Add users or projects as subscribers.');
}
public function getCommandDescription() {
return pht(
- 'Add one or more subscribers to the object. You can add users '.
- 'by providing their usernames, or add projects by adding their '.
- 'hashtags. For example, use `!subscribe alincoln #ios` to add the '.
- 'user `alincoln` and the project with hashtag `#ios` as subscribers.'.
+ 'Add one or more subscribers to the object. You can add users by '.
+ 'providing their usernames, or add projects by adding their hashtags. '.
+ 'For example, use `%s` to add the user `alincoln` and the project with '.
+ 'hashtag `#ios` as subscribers.'.
"\n\n".
'Subscribers which are invalid or unrecognized will be ignored. This '.
'command has no effect if you do not specify any subscribers.'.
"\n\n".
'Users who are CC\'d on the email itself are also automatically '.
'subscribed if Phabricator knows which accounts are linked to their '.
- 'email addresses.');
+ 'email addresses.',
+ '!subscribe alincoln #ios');
}
public function getCommandAliases() {
return array(
'cc',
);
}
public function isCommandSupportedForObject(
PhabricatorApplicationTransactionInterface $object) {
return ($object instanceof PhabricatorSubscribableInterface);
}
public function buildTransactions(
PhabricatorUser $viewer,
PhabricatorApplicationTransactionInterface $object,
PhabricatorMetaMTAReceivedMail $mail,
$command,
array $argv) {
$subscriber_phids = id(new PhabricatorObjectListQuery())
->setViewer($viewer)
->setAllowedTypes(
array(
PhabricatorPeopleUserPHIDType::TYPECONST,
PhabricatorProjectProjectPHIDType::TYPECONST,
))
->setObjectList(implode(' ', $argv))
->setAllowPartialResults(true)
->execute();
$xactions = array();
$xactions[] = $object->getApplicationTransactionTemplate()
->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS)
->setNewValue(
array(
'+' => array_fuse($subscriber_phids),
));
return $xactions;
}
}
diff --git a/src/applications/system/controller/PhabricatorSystemSelectEncodingController.php b/src/applications/system/controller/PhabricatorSystemSelectEncodingController.php
index 4392f53ad..efbe67882 100644
--- a/src/applications/system/controller/PhabricatorSystemSelectEncodingController.php
+++ b/src/applications/system/controller/PhabricatorSystemSelectEncodingController.php
@@ -1,55 +1,57 @@
<?php
final class PhabricatorSystemSelectEncodingController
extends PhabricatorController {
public function shouldRequireLogin() {
return false;
}
public function processRequest() {
$request = $this->getRequest();
if (!function_exists('mb_list_encodings')) {
return $this->newDialog()
->setTitle(pht('No Encoding Support'))
->appendParagraph(
pht(
- 'This system does not have the "mbstring" extension installed, '.
- 'so character encodings are not supported. Install "mbstring" to '.
- 'enable support.'))
+ 'This system does not have the "%s" extension installed, '.
+ 'so character encodings are not supported. Install "%s" to '.
+ 'enable support.',
+ 'mbstring',
+ 'mbstring'))
->addCancelButton('/');
}
if ($request->isFormPost()) {
$result = array('encoding' => $request->getStr('encoding'));
return id(new AphrontAjaxResponse())->setContent($result);
}
$encodings = mb_list_encodings();
$encodings = array_fuse($encodings);
asort($encodings);
unset($encodings['pass']);
unset($encodings['auto']);
$encodings = array(
'' => pht('(Use Default)'),
) + $encodings;
$form = id(new AphrontFormView())
->setUser($this->getRequest()->getUser())
->appendRemarkupInstructions(pht('Choose a text encoding to use.'))
->appendChild(
id(new AphrontFormSelectControl())
->setLabel(pht('Encoding'))
->setName('encoding')
->setValue($request->getStr('encoding'))
->setOptions($encodings));
return $this->newDialog()
->setTitle(pht('Select Character Encoding'))
->appendChild($form->buildLayoutView())
->addSubmitButton(pht('Choose Encoding'))
->addCancelButton('/');
}
}
diff --git a/src/applications/tokens/phid/PhabricatorTokenTokenPHIDType.php b/src/applications/tokens/phid/PhabricatorTokenTokenPHIDType.php
index c458c52fb..1791cc88c 100644
--- a/src/applications/tokens/phid/PhabricatorTokenTokenPHIDType.php
+++ b/src/applications/tokens/phid/PhabricatorTokenTokenPHIDType.php
@@ -1,37 +1,37 @@
<?php
final class PhabricatorTokenTokenPHIDType extends PhabricatorPHIDType {
const TYPECONST = 'TOKN';
public function getTypeName() {
return pht('Token');
}
public function newObject() {
return new PhabricatorToken();
}
protected function buildQueryForObjects(
PhabricatorObjectQuery $query,
array $phids) {
return id(new PhabricatorTokenQuery())
->withPHIDs($phids);
}
public function loadHandles(
PhabricatorHandleQuery $query,
array $handles,
array $objects) {
foreach ($handles as $phid => $handle) {
$token = $objects[$phid];
$name = $token->getName();
- $handle->setName("{$name} Token");
+ $handle->setName(pht('%s Token', $name));
}
}
}
diff --git a/src/applications/transactions/editor/PhabricatorApplicationTransactionCommentEditor.php b/src/applications/transactions/editor/PhabricatorApplicationTransactionCommentEditor.php
index 23b77fbd1..94a1cdaa3 100644
--- a/src/applications/transactions/editor/PhabricatorApplicationTransactionCommentEditor.php
+++ b/src/applications/transactions/editor/PhabricatorApplicationTransactionCommentEditor.php
@@ -1,158 +1,160 @@
<?php
final class PhabricatorApplicationTransactionCommentEditor
extends PhabricatorEditor {
private $contentSource;
private $actingAsPHID;
public function setActingAsPHID($acting_as_phid) {
$this->actingAsPHID = $acting_as_phid;
return $this;
}
public function getActingAsPHID() {
if ($this->actingAsPHID) {
return $this->actingAsPHID;
}
return $this->getActor()->getPHID();
}
public function setContentSource(PhabricatorContentSource $content_source) {
$this->contentSource = $content_source;
return $this;
}
public function getContentSource() {
return $this->contentSource;
}
/**
* Edit a transaction's comment. This method effects the required create,
* update or delete to set the transaction's comment to the provided comment.
*/
public function applyEdit(
PhabricatorApplicationTransaction $xaction,
PhabricatorApplicationTransactionComment $comment) {
$this->validateEdit($xaction, $comment);
$actor = $this->requireActor();
$comment->setContentSource($this->getContentSource());
$comment->setAuthorPHID($this->getActingAsPHID());
// TODO: This needs to be more sophisticated once we have meta-policies.
$comment->setViewPolicy(PhabricatorPolicies::POLICY_PUBLIC);
$comment->setEditPolicy($this->getActingAsPHID());
$file_phids = PhabricatorMarkupEngine::extractFilePHIDsFromEmbeddedFiles(
$actor,
array(
$comment->getContent(),
));
$xaction->openTransaction();
$xaction->beginReadLocking();
if ($xaction->getID()) {
$xaction->reload();
}
$new_version = $xaction->getCommentVersion() + 1;
$comment->setCommentVersion($new_version);
$comment->setTransactionPHID($xaction->getPHID());
$comment->save();
$xaction->setCommentVersion($new_version);
$xaction->setCommentPHID($comment->getPHID());
$xaction->setViewPolicy($comment->getViewPolicy());
$xaction->setEditPolicy($comment->getEditPolicy());
$xaction->save();
$xaction->attachComment($comment);
// For comment edits, we need to make sure there are no automagical
// transactions like adding mentions or projects.
if ($new_version > 1) {
$object = id(new PhabricatorObjectQuery())
->withPHIDs(array($xaction->getObjectPHID()))
->setViewer($this->getActor())
->executeOne();
if ($object &&
$object instanceof PhabricatorApplicationTransactionInterface) {
$editor = $object->getApplicationTransactionEditor();
$editor->setActor($this->getActor());
$support_xactions = $editor->getExpandedSupportTransactions(
$object,
$xaction);
if ($support_xactions) {
$editor
->setContentSource($this->getContentSource())
->setContinueOnNoEffect(true)
->applyTransactions($object, $support_xactions);
}
}
}
$xaction->endReadLocking();
$xaction->saveTransaction();
// Add links to any files newly referenced by the edit.
if ($file_phids) {
$editor = new PhabricatorEdgeEditor();
foreach ($file_phids as $file_phid) {
$editor->addEdge(
$xaction->getObjectPHID(),
PhabricatorObjectHasFileEdgeType::EDGECONST ,
$file_phid);
}
$editor->save();
}
return $this;
}
/**
* Validate that the edit is permissible, and the actor has permission to
* perform it.
*/
private function validateEdit(
PhabricatorApplicationTransaction $xaction,
PhabricatorApplicationTransactionComment $comment) {
if (!$xaction->getPHID()) {
throw new Exception(
- 'Transaction must have a PHID before calling applyEdit()!');
+ pht(
+ 'Transaction must have a PHID before calling %s!',
+ 'applyEdit()'));
}
$type_comment = PhabricatorTransactions::TYPE_COMMENT;
if ($xaction->getTransactionType() == $type_comment) {
if ($comment->getPHID()) {
throw new Exception(
- 'Transaction comment must not yet have a PHID!');
+ pht('Transaction comment must not yet have a PHID!'));
}
}
if (!$this->getContentSource()) {
throw new PhutilInvalidStateException('applyEdit');
}
$actor = $this->requireActor();
PhabricatorPolicyFilter::requireCapability(
$actor,
$xaction,
PhabricatorPolicyCapability::CAN_VIEW);
if ($comment->getIsRemoved() && $actor->getIsAdmin()) {
// NOTE: Administrators can remove comments by any user, and don't need
// to pass the edit check.
} else {
PhabricatorPolicyFilter::requireCapability(
$actor,
$xaction,
PhabricatorPolicyCapability::CAN_EDIT);
}
}
}
diff --git a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php
index 57a3e4bde..7c8a1cff4 100644
--- a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php
+++ b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php
@@ -1,2711 +1,2743 @@
<?php
/**
* @task mail Sending Mail
* @task feed Publishing Feed Stories
* @task search Search Index
* @task files Integration with Files
*/
abstract class PhabricatorApplicationTransactionEditor
extends PhabricatorEditor {
private $contentSource;
private $object;
private $xactions;
private $isNewObject;
private $mentionedPHIDs;
private $continueOnNoEffect;
private $continueOnMissingFields;
private $parentMessageID;
private $heraldAdapter;
private $heraldTranscript;
private $subscribers;
private $unmentionablePHIDMap = array();
private $applicationEmail;
private $isPreview;
private $isHeraldEditor;
private $isInverseEdgeEditor;
private $actingAsPHID;
private $disableEmail;
/**
* Get the class name for the application this editor is a part of.
*
* Uninstalling the application will disable the editor.
*
* @return string Editor's application class name.
*/
abstract public function getEditorApplicationClass();
/**
* Get a description of the objects this editor edits, like "Differential
* Revisions".
*
* @return string Human readable description of edited objects.
*/
abstract public function getEditorObjectsDescription();
public function setActingAsPHID($acting_as_phid) {
$this->actingAsPHID = $acting_as_phid;
return $this;
}
public function getActingAsPHID() {
if ($this->actingAsPHID) {
return $this->actingAsPHID;
}
return $this->getActor()->getPHID();
}
/**
* When the editor tries to apply transactions that have no effect, should
* it raise an exception (default) or drop them and continue?
*
* Generally, you will set this flag for edits coming from "Edit" interfaces,
* and leave it cleared for edits coming from "Comment" interfaces, so the
* user will get a useful error if they try to submit a comment that does
* nothing (e.g., empty comment with a status change that has already been
* performed by another user).
*
* @param bool True to drop transactions without effect and continue.
* @return this
*/
public function setContinueOnNoEffect($continue) {
$this->continueOnNoEffect = $continue;
return $this;
}
public function getContinueOnNoEffect() {
return $this->continueOnNoEffect;
}
/**
* When the editor tries to apply transactions which don't populate all of
* an object's required fields, should it raise an exception (default) or
* drop them and continue?
*
* For example, if a user adds a new required custom field (like "Severity")
* to a task, all existing tasks won't have it populated. When users
* manually edit existing tasks, it's usually desirable to have them provide
* a severity. However, other operations (like batch editing just the
* owner of a task) will fail by default.
*
* By setting this flag for edit operations which apply to specific fields
* (like the priority, batch, and merge editors in Maniphest), these
* operations can continue to function even if an object is outdated.
*
* @param bool True to continue when transactions don't completely satisfy
* all required fields.
* @return this
*/
public function setContinueOnMissingFields($continue_on_missing_fields) {
$this->continueOnMissingFields = $continue_on_missing_fields;
return $this;
}
public function getContinueOnMissingFields() {
return $this->continueOnMissingFields;
}
/**
* Not strictly necessary, but reply handlers ideally set this value to
* make email threading work better.
*/
public function setParentMessageID($parent_message_id) {
$this->parentMessageID = $parent_message_id;
return $this;
}
public function getParentMessageID() {
return $this->parentMessageID;
}
public function getIsNewObject() {
return $this->isNewObject;
}
protected function getMentionedPHIDs() {
return $this->mentionedPHIDs;
}
public function setIsPreview($is_preview) {
$this->isPreview = $is_preview;
return $this;
}
public function getIsPreview() {
return $this->isPreview;
}
public function setIsInverseEdgeEditor($is_inverse_edge_editor) {
$this->isInverseEdgeEditor = $is_inverse_edge_editor;
return $this;
}
public function getIsInverseEdgeEditor() {
return $this->isInverseEdgeEditor;
}
public function setIsHeraldEditor($is_herald_editor) {
$this->isHeraldEditor = $is_herald_editor;
return $this;
}
public function getIsHeraldEditor() {
return $this->isHeraldEditor;
}
/**
* Prevent this editor from generating email when applying transactions.
*
* @param bool True to disable email.
* @return this
*/
public function setDisableEmail($disable_email) {
$this->disableEmail = $disable_email;
return $this;
}
public function getDisableEmail() {
return $this->disableEmail;
}
public function setUnmentionablePHIDMap(array $map) {
$this->unmentionablePHIDMap = $map;
return $this;
}
public function getUnmentionablePHIDMap() {
return $this->unmentionablePHIDMap;
}
protected function shouldEnableMentions(
PhabricatorLiskDAO $object,
array $xactions) {
return true;
}
public function setApplicationEmail(
PhabricatorMetaMTAApplicationEmail $email) {
$this->applicationEmail = $email;
return $this;
}
public function getApplicationEmail() {
return $this->applicationEmail;
}
public function getTransactionTypes() {
$types = array();
if ($this->object instanceof PhabricatorSubscribableInterface) {
$types[] = PhabricatorTransactions::TYPE_SUBSCRIBERS;
}
if ($this->object instanceof PhabricatorCustomFieldInterface) {
$types[] = PhabricatorTransactions::TYPE_CUSTOMFIELD;
}
if ($this->object instanceof HarbormasterBuildableInterface) {
$types[] = PhabricatorTransactions::TYPE_BUILDABLE;
}
if ($this->object instanceof PhabricatorTokenReceiverInterface) {
$types[] = PhabricatorTransactions::TYPE_TOKEN;
}
if ($this->object instanceof PhabricatorProjectInterface ||
$this->object instanceof PhabricatorMentionableInterface) {
$types[] = PhabricatorTransactions::TYPE_EDGE;
}
return $types;
}
private function adjustTransactionValues(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
if ($xaction->shouldGenerateOldValue()) {
$old = $this->getTransactionOldValue($object, $xaction);
$xaction->setOldValue($old);
}
$new = $this->getTransactionNewValue($object, $xaction);
$xaction->setNewValue($new);
}
private function getTransactionOldValue(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorTransactions::TYPE_SUBSCRIBERS:
return array_values($this->subscribers);
case PhabricatorTransactions::TYPE_VIEW_POLICY:
return $object->getViewPolicy();
case PhabricatorTransactions::TYPE_EDIT_POLICY:
return $object->getEditPolicy();
case PhabricatorTransactions::TYPE_JOIN_POLICY:
return $object->getJoinPolicy();
case PhabricatorTransactions::TYPE_EDGE:
$edge_type = $xaction->getMetadataValue('edge:type');
if (!$edge_type) {
- throw new Exception("Edge transaction has no 'edge:type'!");
+ throw new Exception(
+ pht(
+ "Edge transaction has no '%s'!",
+ 'edge:type'));
}
$old_edges = array();
if ($object->getPHID()) {
$edge_src = $object->getPHID();
$old_edges = id(new PhabricatorEdgeQuery())
->withSourcePHIDs(array($edge_src))
->withEdgeTypes(array($edge_type))
->needEdgeData(true)
->execute();
$old_edges = $old_edges[$edge_src][$edge_type];
}
return $old_edges;
case PhabricatorTransactions::TYPE_CUSTOMFIELD:
// NOTE: Custom fields have their old value pre-populated when they are
// built by PhabricatorCustomFieldList.
return $xaction->getOldValue();
case PhabricatorTransactions::TYPE_COMMENT:
return null;
default:
return $this->getCustomTransactionOldValue($object, $xaction);
}
}
private function getTransactionNewValue(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorTransactions::TYPE_SUBSCRIBERS:
return $this->getPHIDTransactionNewValue($xaction);
case PhabricatorTransactions::TYPE_VIEW_POLICY:
case PhabricatorTransactions::TYPE_EDIT_POLICY:
case PhabricatorTransactions::TYPE_JOIN_POLICY:
case PhabricatorTransactions::TYPE_BUILDABLE:
case PhabricatorTransactions::TYPE_TOKEN:
case PhabricatorTransactions::TYPE_INLINESTATE:
return $xaction->getNewValue();
case PhabricatorTransactions::TYPE_EDGE:
return $this->getEdgeTransactionNewValue($xaction);
case PhabricatorTransactions::TYPE_CUSTOMFIELD:
$field = $this->getCustomFieldForTransaction($object, $xaction);
return $field->getNewValueFromApplicationTransactions($xaction);
case PhabricatorTransactions::TYPE_COMMENT:
return null;
default:
return $this->getCustomTransactionNewValue($object, $xaction);
}
}
protected function getCustomTransactionOldValue(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
- throw new Exception('Capability not supported!');
+ throw new Exception(pht('Capability not supported!'));
}
protected function getCustomTransactionNewValue(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
- throw new Exception('Capability not supported!');
+ throw new Exception(pht('Capability not supported!'));
}
protected function transactionHasEffect(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorTransactions::TYPE_COMMENT:
return $xaction->hasComment();
case PhabricatorTransactions::TYPE_CUSTOMFIELD:
$field = $this->getCustomFieldForTransaction($object, $xaction);
return $field->getApplicationTransactionHasEffect($xaction);
case PhabricatorTransactions::TYPE_EDGE:
// A straight value comparison here doesn't always get the right
// result, because newly added edges aren't fully populated. Instead,
// compare the changes in a more granular way.
$old = $xaction->getOldValue();
$new = $xaction->getNewValue();
$old_dst = array_keys($old);
$new_dst = array_keys($new);
// NOTE: For now, we don't consider edge reordering to be a change.
// We have very few order-dependent edges and effectively no order
// oriented UI. This might change in the future.
sort($old_dst);
sort($new_dst);
if ($old_dst !== $new_dst) {
// We've added or removed edges, so this transaction definitely
// has an effect.
return true;
}
// We haven't added or removed edges, but we might have changed
// edge data.
foreach ($old as $key => $old_value) {
$new_value = $new[$key];
if ($old_value['data'] !== $new_value['data']) {
return true;
}
}
return false;
}
return ($xaction->getOldValue() !== $xaction->getNewValue());
}
protected function shouldApplyInitialEffects(
PhabricatorLiskDAO $object,
array $xactions) {
return false;
}
protected function applyInitialEffects(
PhabricatorLiskDAO $object,
array $xactions) {
throw new PhutilMethodNotImplementedException();
}
private function applyInternalEffects(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorTransactions::TYPE_CUSTOMFIELD:
$field = $this->getCustomFieldForTransaction($object, $xaction);
return $field->applyApplicationTransactionInternalEffects($xaction);
case PhabricatorTransactions::TYPE_BUILDABLE:
case PhabricatorTransactions::TYPE_TOKEN:
case PhabricatorTransactions::TYPE_VIEW_POLICY:
case PhabricatorTransactions::TYPE_EDIT_POLICY:
case PhabricatorTransactions::TYPE_JOIN_POLICY:
case PhabricatorTransactions::TYPE_SUBSCRIBERS:
case PhabricatorTransactions::TYPE_INLINESTATE:
case PhabricatorTransactions::TYPE_EDGE:
case PhabricatorTransactions::TYPE_COMMENT:
return $this->applyBuiltinInternalTransaction($object, $xaction);
}
return $this->applyCustomInternalTransaction($object, $xaction);
}
private function applyExternalEffects(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorTransactions::TYPE_SUBSCRIBERS:
$subeditor = id(new PhabricatorSubscriptionsEditor())
->setObject($object)
->setActor($this->requireActor());
$old_map = array_fuse($xaction->getOldValue());
$new_map = array_fuse($xaction->getNewValue());
$subeditor->unsubscribe(
array_keys(
array_diff_key($old_map, $new_map)));
$subeditor->subscribeExplicit(
array_keys(
array_diff_key($new_map, $old_map)));
$subeditor->save();
// for the rest of these edits, subscribers should include those just
// added as well as those just removed.
$subscribers = array_unique(array_merge(
$this->subscribers,
$xaction->getOldValue(),
$xaction->getNewValue()));
$this->subscribers = $subscribers;
return $this->applyBuiltinExternalTransaction($object, $xaction);
case PhabricatorTransactions::TYPE_CUSTOMFIELD:
$field = $this->getCustomFieldForTransaction($object, $xaction);
return $field->applyApplicationTransactionExternalEffects($xaction);
case PhabricatorTransactions::TYPE_EDGE:
case PhabricatorTransactions::TYPE_BUILDABLE:
case PhabricatorTransactions::TYPE_TOKEN:
case PhabricatorTransactions::TYPE_VIEW_POLICY:
case PhabricatorTransactions::TYPE_EDIT_POLICY:
case PhabricatorTransactions::TYPE_JOIN_POLICY:
case PhabricatorTransactions::TYPE_INLINESTATE:
case PhabricatorTransactions::TYPE_COMMENT:
return $this->applyBuiltinExternalTransaction($object, $xaction);
}
return $this->applyCustomExternalTransaction($object, $xaction);
}
protected function applyCustomInternalTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
$type = $xaction->getTransactionType();
throw new Exception(
- "Transaction type '{$type}' is missing an internal apply ".
- "implementation!");
+ pht(
+ "Transaction type '%s' is missing an internal apply implementation!",
+ $type));
}
protected function applyCustomExternalTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
$type = $xaction->getTransactionType();
throw new Exception(
- "Transaction type '{$type}' is missing an external apply ".
- "implementation!");
+ pht(
+ "Transaction type '%s' is missing an external apply implementation!",
+ $type));
}
/**
* @{class:PhabricatorTransactions} provides many built-in transactions
* which should not require much - if any - code in specific applications.
*
* This method is a hook for the exceedingly-rare cases where you may need
* to do **additional** work for built-in transactions. Developers should
* extend this method, making sure to return the parent implementation
* regardless of handling any transactions.
*
* See also @{method:applyBuiltinExternalTransaction}.
*/
protected function applyBuiltinInternalTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorTransactions::TYPE_VIEW_POLICY:
$object->setViewPolicy($xaction->getNewValue());
break;
case PhabricatorTransactions::TYPE_EDIT_POLICY:
$object->setEditPolicy($xaction->getNewValue());
break;
case PhabricatorTransactions::TYPE_JOIN_POLICY:
$object->setJoinPolicy($xaction->getNewValue());
break;
}
}
/**
* See @{method::applyBuiltinInternalTransaction}.
*/
protected function applyBuiltinExternalTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorTransactions::TYPE_EDGE:
if ($this->getIsInverseEdgeEditor()) {
// If we're writing an inverse edge transaction, don't actually
// do anything. The initiating editor on the other side of the
// transaction will take care of the edge writes.
break;
}
$old = $xaction->getOldValue();
$new = $xaction->getNewValue();
$src = $object->getPHID();
$const = $xaction->getMetadataValue('edge:type');
$type = PhabricatorEdgeType::getByConstant($const);
if ($type->shouldWriteInverseTransactions()) {
$this->applyInverseEdgeTransactions(
$object,
$xaction,
$type->getInverseEdgeConstant());
}
foreach ($new as $dst_phid => $edge) {
$new[$dst_phid]['src'] = $src;
}
$editor = new PhabricatorEdgeEditor();
foreach ($old as $dst_phid => $edge) {
if (!empty($new[$dst_phid])) {
if ($old[$dst_phid]['data'] === $new[$dst_phid]['data']) {
continue;
}
}
$editor->removeEdge($src, $const, $dst_phid);
}
foreach ($new as $dst_phid => $edge) {
if (!empty($old[$dst_phid])) {
if ($old[$dst_phid]['data'] === $new[$dst_phid]['data']) {
continue;
}
}
$data = array(
'data' => $edge['data'],
);
$editor->addEdge($src, $const, $dst_phid, $data);
}
$editor->save();
break;
}
}
/**
* Fill in a transaction's common values, like author and content source.
*/
protected function populateTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
$actor = $this->getActor();
// TODO: This needs to be more sophisticated once we have meta-policies.
$xaction->setViewPolicy(PhabricatorPolicies::POLICY_PUBLIC);
if ($actor->isOmnipotent()) {
$xaction->setEditPolicy(PhabricatorPolicies::POLICY_NOONE);
} else {
$xaction->setEditPolicy($this->getActingAsPHID());
}
$xaction->setAuthorPHID($this->getActingAsPHID());
$xaction->setContentSource($this->getContentSource());
$xaction->attachViewer($actor);
$xaction->attachObject($object);
if ($object->getPHID()) {
$xaction->setObjectPHID($object->getPHID());
}
return $xaction;
}
protected function didApplyInternalEffects(
PhabricatorLiskDAO $object,
array $xactions) {
return $xactions;
}
protected function applyFinalEffects(
PhabricatorLiskDAO $object,
array $xactions) {
return $xactions;
}
public function setContentSource(PhabricatorContentSource $content_source) {
$this->contentSource = $content_source;
return $this;
}
public function setContentSourceFromRequest(AphrontRequest $request) {
return $this->setContentSource(
PhabricatorContentSource::newFromRequest($request));
}
public function setContentSourceFromConduitRequest(
ConduitAPIRequest $request) {
$content_source = PhabricatorContentSource::newForSource(
PhabricatorContentSource::SOURCE_CONDUIT,
array());
return $this->setContentSource($content_source);
}
public function getContentSource() {
return $this->contentSource;
}
final public function applyTransactions(
PhabricatorLiskDAO $object,
array $xactions) {
$this->object = $object;
$this->xactions = $xactions;
$this->isNewObject = ($object->getPHID() === null);
$this->validateEditParameters($object, $xactions);
$actor = $this->requireActor();
// NOTE: Some transaction expansion requires that the edited object be
// attached.
foreach ($xactions as $xaction) {
$xaction->attachObject($object);
$xaction->attachViewer($actor);
}
$xactions = $this->expandTransactions($object, $xactions);
$xactions = $this->expandSupportTransactions($object, $xactions);
$xactions = $this->combineTransactions($xactions);
foreach ($xactions as $xaction) {
$xaction = $this->populateTransaction($object, $xaction);
}
$is_preview = $this->getIsPreview();
$read_locking = false;
$transaction_open = false;
if (!$is_preview) {
$errors = array();
$type_map = mgroup($xactions, 'getTransactionType');
foreach ($this->getTransactionTypes() as $type) {
$type_xactions = idx($type_map, $type, array());
$errors[] = $this->validateTransaction($object, $type, $type_xactions);
}
$errors[] = $this->validateAllTransactions($object, $xactions);
$errors = array_mergev($errors);
$continue_on_missing = $this->getContinueOnMissingFields();
foreach ($errors as $key => $error) {
if ($continue_on_missing && $error->getIsMissingFieldError()) {
unset($errors[$key]);
}
}
if ($errors) {
throw new PhabricatorApplicationTransactionValidationException($errors);
}
$file_phids = $this->extractFilePHIDs($object, $xactions);
if ($object->getID()) {
foreach ($xactions as $xaction) {
// If any of the transactions require a read lock, hold one and
// reload the object. We need to do this fairly early so that the
// call to `adjustTransactionValues()` (which populates old values)
// is based on the synchronized state of the object, which may differ
// from the state when it was originally loaded.
if ($this->shouldReadLock($object, $xaction)) {
$object->openTransaction();
$object->beginReadLocking();
$transaction_open = true;
$read_locking = true;
$object->reload();
break;
}
}
}
if ($this->shouldApplyInitialEffects($object, $xactions)) {
if (!$transaction_open) {
$object->openTransaction();
$transaction_open = true;
}
}
}
if ($this->shouldApplyInitialEffects($object, $xactions)) {
$this->applyInitialEffects($object, $xactions);
}
foreach ($xactions as $xaction) {
$this->adjustTransactionValues($object, $xaction);
}
$xactions = $this->filterTransactions($object, $xactions);
if (!$xactions) {
if ($read_locking) {
$object->endReadLocking();
$read_locking = false;
}
if ($transaction_open) {
$object->killTransaction();
$transaction_open = false;
}
return array();
}
// Now that we've merged, filtered, and combined transactions, check for
// required capabilities.
foreach ($xactions as $xaction) {
$this->requireCapabilities($object, $xaction);
}
$xactions = $this->sortTransactions($xactions);
if ($is_preview) {
$this->loadHandles($xactions);
return $xactions;
}
$comment_editor = id(new PhabricatorApplicationTransactionCommentEditor())
->setActor($actor)
->setActingAsPHID($this->getActingAsPHID())
->setContentSource($this->getContentSource());
if (!$transaction_open) {
$object->openTransaction();
}
foreach ($xactions as $xaction) {
$this->applyInternalEffects($object, $xaction);
}
$xactions = $this->didApplyInternalEffects($object, $xactions);
$object->save();
foreach ($xactions as $xaction) {
$xaction->setObjectPHID($object->getPHID());
if ($xaction->getComment()) {
$xaction->setPHID($xaction->generatePHID());
$comment_editor->applyEdit($xaction, $xaction->getComment());
} else {
$xaction->save();
}
}
if ($file_phids) {
$this->attachFiles($object, $file_phids);
}
foreach ($xactions as $xaction) {
$this->applyExternalEffects($object, $xaction);
}
$xactions = $this->applyFinalEffects($object, $xactions);
if ($read_locking) {
$object->endReadLocking();
$read_locking = false;
}
$object->saveTransaction();
// Now that we've completely applied the core transaction set, try to apply
// Herald rules. Herald rules are allowed to either take direct actions on
// the database (like writing flags), or take indirect actions (like saving
// some targets for CC when we generate mail a little later), or return
// transactions which we'll apply normally using another Editor.
// First, check if *this* is a sub-editor which is itself applying Herald
// rules: if it is, stop working and return so we don't descend into
// madness.
// Otherwise, we're not a Herald editor, so process Herald rules (possibly
// using a Herald editor to apply resulting transactions) and then send out
// mail, notifications, and feed updates about everything.
if ($this->getIsHeraldEditor()) {
// We are the Herald editor, so stop work here and return the updated
// transactions.
return $xactions;
} else if ($this->getIsInverseEdgeEditor()) {
// If we're applying inverse edge transactions, don't trigger Herald.
// From a product perspective, the current set of inverse edges (most
// often, mentions) aren't things users would expect to trigger Herald.
// From a technical perspective, objects loaded by the inverse editor may
// not have enough data to execute rules. At least for now, just stop
// Herald from executing when applying inverse edges.
} else if ($this->shouldApplyHeraldRules($object, $xactions)) {
// We are not the Herald editor, so try to apply Herald rules.
$herald_xactions = $this->applyHeraldRules($object, $xactions);
if ($herald_xactions) {
$xscript_id = $this->getHeraldTranscript()->getID();
foreach ($herald_xactions as $herald_xaction) {
$herald_xaction->setMetadataValue('herald:transcriptID', $xscript_id);
}
// NOTE: We're acting as the omnipotent user because rules deal with
// their own policy issues. We use a synthetic author PHID (the
// Herald application) as the author of record, so that transactions
// will render in a reasonable way ("Herald assigned this task ...").
$herald_actor = PhabricatorUser::getOmnipotentUser();
$herald_phid = id(new PhabricatorHeraldApplication())->getPHID();
// TODO: It would be nice to give transactions a more specific source
// which points at the rule which generated them. You can figure this
// out from transcripts, but it would be cleaner if you didn't have to.
$herald_source = PhabricatorContentSource::newForSource(
PhabricatorContentSource::SOURCE_HERALD,
array());
$herald_editor = newv(get_class($this), array())
->setContinueOnNoEffect(true)
->setContinueOnMissingFields(true)
->setParentMessageID($this->getParentMessageID())
->setIsHeraldEditor(true)
->setActor($herald_actor)
->setActingAsPHID($herald_phid)
->setContentSource($herald_source);
$herald_xactions = $herald_editor->applyTransactions(
$object,
$herald_xactions);
// Merge the new transactions into the transaction list: we want to
// send email and publish feed stories about them, too.
$xactions = array_merge($xactions, $herald_xactions);
}
}
// Before sending mail or publishing feed stories, reload the object
// subscribers to pick up changes caused by Herald (or by other side effects
// in various transaction phases).
$this->loadSubscribers($object);
// Hook for other edges that may need (re-)loading
$this->loadEdges($object, $xactions);
$this->loadHandles($xactions);
$mail = null;
if (!$this->getDisableEmail()) {
if ($this->shouldSendMail($object, $xactions)) {
$mail = $this->sendMail($object, $xactions);
}
}
if ($this->supportsSearch()) {
id(new PhabricatorSearchIndexer())
->queueDocumentForIndexing(
$object->getPHID(),
$this->getSearchContextParameter($object, $xactions));
}
if ($this->shouldPublishFeedStory($object, $xactions)) {
$mailed = array();
if ($mail) {
$mailed = $mail->buildRecipientList();
}
$this->publishFeedStory(
$object,
$xactions,
$mailed);
}
$this->didApplyTransactions($xactions);
if ($object instanceof PhabricatorCustomFieldInterface) {
// Maybe this makes more sense to move into the search index itself? For
// now I'm putting it here since I think we might end up with things that
// need it to be up to date once the next page loads, but if we don't go
// there we we could move it into search once search moves to the daemons.
// It now happens in the search indexer as well, but the search indexer is
// always daemonized, so the logic above still potentially holds. We could
// possibly get rid of this. The major motivation for putting it in the
// indexer was to enable reindexing to work.
$fields = PhabricatorCustomField::getObjectFields(
$object,
PhabricatorCustomField::ROLE_APPLICATIONSEARCH);
$fields->readFieldsFromStorage($object);
$fields->rebuildIndexes($object);
}
return $xactions;
}
protected function didApplyTransactions(array $xactions) {
// Hook for subclasses.
return;
}
/**
* Determine if the editor should hold a read lock on the object while
* applying a transaction.
*
* If the editor does not hold a lock, two editors may read an object at the
* same time, then apply their changes without any synchronization. For most
* transactions, this does not matter much. However, it is important for some
* transactions. For example, if an object has a transaction count on it, both
* editors may read the object with `count = 23`, then independently update it
* and save the object with `count = 24` twice. This will produce the wrong
* state: the object really has 25 transactions, but the count is only 24.
*
* Generally, transactions fall into one of four buckets:
*
* - Append operations: Actions like adding a comment to an object purely
* add information to its state, and do not depend on the current object
* state in any way. These transactions never need to hold locks.
* - Overwrite operations: Actions like changing the title or description
* of an object replace the current value with a new value, so the end
* state is consistent without a lock. We currently do not lock these
* transactions, although we may in the future.
* - Edge operations: Edge and subscription operations have internal
* synchronization which limits the damage race conditions can cause.
* We do not currently lock these transactions, although we may in the
* future.
* - Update operations: Actions like incrementing a count on an object.
* These operations generally should use locks, unless it is not
* important that the state remain consistent in the presence of races.
*
* @param PhabricatorLiskDAO Object being updated.
* @param PhabricatorApplicationTransaction Transaction being applied.
* @return bool True to synchronize the edit with a lock.
*/
protected function shouldReadLock(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
return false;
}
private function loadHandles(array $xactions) {
$phids = array();
foreach ($xactions as $key => $xaction) {
$phids[$key] = $xaction->getRequiredHandlePHIDs();
}
$handles = array();
$merged = array_mergev($phids);
if ($merged) {
$handles = id(new PhabricatorHandleQuery())
->setViewer($this->requireActor())
->withPHIDs($merged)
->execute();
}
foreach ($xactions as $key => $xaction) {
$xaction->setHandles(array_select_keys($handles, $phids[$key]));
}
}
private function loadSubscribers(PhabricatorLiskDAO $object) {
if ($object->getPHID() &&
($object instanceof PhabricatorSubscribableInterface)) {
$subs = PhabricatorSubscribersQuery::loadSubscribersForPHID(
$object->getPHID());
$this->subscribers = array_fuse($subs);
} else {
$this->subscribers = array();
}
}
protected function loadEdges(
PhabricatorLiskDAO $object,
array $xactions) {
return;
}
private function validateEditParameters(
PhabricatorLiskDAO $object,
array $xactions) {
if (!$this->getContentSource()) {
throw new PhutilInvalidStateException('setContentSource');
}
// Do a bunch of sanity checks that the incoming transactions are fresh.
// They should be unsaved and have only "transactionType" and "newValue"
// set.
$types = array_fill_keys($this->getTransactionTypes(), true);
assert_instances_of($xactions, 'PhabricatorApplicationTransaction');
foreach ($xactions as $xaction) {
if ($xaction->getPHID() || $xaction->getID()) {
throw new PhabricatorApplicationTransactionStructureException(
$xaction,
- pht(
- 'You can not apply transactions which already have IDs/PHIDs!'));
+ pht('You can not apply transactions which already have IDs/PHIDs!'));
}
if ($xaction->getObjectPHID()) {
throw new PhabricatorApplicationTransactionStructureException(
$xaction,
pht(
- 'You can not apply transactions which already have objectPHIDs!'));
+ 'You can not apply transactions which already have %s!',
+ 'objectPHIDs'));
}
if ($xaction->getAuthorPHID()) {
throw new PhabricatorApplicationTransactionStructureException(
$xaction,
pht(
- 'You can not apply transactions which already have authorPHIDs!'));
+ 'You can not apply transactions which already have %s!',
+ 'authorPHIDs'));
}
if ($xaction->getCommentPHID()) {
throw new PhabricatorApplicationTransactionStructureException(
$xaction,
pht(
- 'You can not apply transactions which already have '.
- 'commentPHIDs!'));
+ 'You can not apply transactions which already have %s!',
+ 'commentPHIDs'));
}
if ($xaction->getCommentVersion() !== 0) {
throw new PhabricatorApplicationTransactionStructureException(
$xaction,
pht(
'You can not apply transactions which already have '.
'commentVersions!'));
}
$expect_value = !$xaction->shouldGenerateOldValue();
$has_value = $xaction->hasOldValue();
if ($expect_value && !$has_value) {
throw new PhabricatorApplicationTransactionStructureException(
$xaction,
pht(
- 'This transaction is supposed to have an oldValue set, but '.
- 'it does not!'));
+ 'This transaction is supposed to have an %s set, but it does not!',
+ 'oldValue'));
}
if ($has_value && !$expect_value) {
throw new PhabricatorApplicationTransactionStructureException(
$xaction,
pht(
- 'This transaction should generate its oldValue automatically, '.
- 'but has already had one set!'));
+ 'This transaction should generate its %s automatically, '.
+ 'but has already had one set!',
+ 'oldValue'));
}
$type = $xaction->getTransactionType();
if (empty($types[$type])) {
throw new PhabricatorApplicationTransactionStructureException(
$xaction,
pht(
'Transaction has type "%s", but that transaction type is not '.
'supported by this editor (%s).',
$type,
get_class($this)));
}
}
}
protected function requireCapabilities(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
if ($this->getIsNewObject()) {
return;
}
$actor = $this->requireActor();
switch ($xaction->getTransactionType()) {
case PhabricatorTransactions::TYPE_COMMENT:
PhabricatorPolicyFilter::requireCapability(
$actor,
$object,
PhabricatorPolicyCapability::CAN_VIEW);
break;
case PhabricatorTransactions::TYPE_VIEW_POLICY:
PhabricatorPolicyFilter::requireCapability(
$actor,
$object,
PhabricatorPolicyCapability::CAN_EDIT);
break;
case PhabricatorTransactions::TYPE_EDIT_POLICY:
PhabricatorPolicyFilter::requireCapability(
$actor,
$object,
PhabricatorPolicyCapability::CAN_EDIT);
break;
case PhabricatorTransactions::TYPE_JOIN_POLICY:
PhabricatorPolicyFilter::requireCapability(
$actor,
$object,
PhabricatorPolicyCapability::CAN_EDIT);
break;
}
}
private function buildSubscribeTransaction(
PhabricatorLiskDAO $object,
array $xactions,
array $blocks) {
if (!($object instanceof PhabricatorSubscribableInterface)) {
return null;
}
if ($this->shouldEnableMentions($object, $xactions)) {
$texts = array_mergev($blocks);
$phids = PhabricatorMarkupEngine::extractPHIDsFromMentions(
$this->getActor(),
$texts);
} else {
$phids = array();
}
$this->mentionedPHIDs = $phids;
if ($object->getPHID()) {
// Don't try to subscribe already-subscribed mentions: we want to generate
// a dialog about an action having no effect if the user explicitly adds
// existing CCs, but not if they merely mention existing subscribers.
$phids = array_diff($phids, $this->subscribers);
}
if ($phids) {
$users = id(new PhabricatorPeopleQuery())
->setViewer($this->getActor())
->withPHIDs($phids)
->execute();
$users = mpull($users, null, 'getPHID');
foreach ($phids as $key => $phid) {
// Do not subscribe mentioned users
// who do not have VIEW Permissions
if ($object instanceof PhabricatorPolicyInterface
&& !PhabricatorPolicyFilter::hasCapability(
$users[$phid],
$object,
PhabricatorPolicyCapability::CAN_VIEW)
) {
unset($phids[$key]);
} else {
if ($object->isAutomaticallySubscribed($phid)) {
unset($phids[$key]);
}
}
}
$phids = array_values($phids);
}
// No else here to properly return null should we unset all subscriber
if (!$phids) {
return null;
}
$xaction = newv(get_class(head($xactions)), array());
$xaction->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS);
$xaction->setNewValue(array('+' => $phids));
return $xaction;
}
protected function getRemarkupBlocksFromTransaction(
PhabricatorApplicationTransaction $transaction) {
return $transaction->getRemarkupBlocks();
}
protected function mergeTransactions(
PhabricatorApplicationTransaction $u,
PhabricatorApplicationTransaction $v) {
$type = $u->getTransactionType();
switch ($type) {
case PhabricatorTransactions::TYPE_SUBSCRIBERS:
return $this->mergePHIDOrEdgeTransactions($u, $v);
case PhabricatorTransactions::TYPE_EDGE:
$u_type = $u->getMetadataValue('edge:type');
$v_type = $v->getMetadataValue('edge:type');
if ($u_type == $v_type) {
return $this->mergePHIDOrEdgeTransactions($u, $v);
}
return null;
}
// By default, do not merge the transactions.
return null;
}
/**
* Optionally expand transactions which imply other effects. For example,
* resigning from a revision in Differential implies removing yourself as
* a reviewer.
*/
private function expandTransactions(
PhabricatorLiskDAO $object,
array $xactions) {
$results = array();
foreach ($xactions as $xaction) {
foreach ($this->expandTransaction($object, $xaction) as $expanded) {
$results[] = $expanded;
}
}
return $results;
}
protected function expandTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
return array($xaction);
}
public function getExpandedSupportTransactions(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
$xactions = array($xaction);
$xactions = $this->expandSupportTransactions(
$object,
$xactions);
if (count($xactions) == 1) {
return array();
}
foreach ($xactions as $index => $cxaction) {
if ($cxaction === $xaction) {
unset($xactions[$index]);
break;
}
}
return $xactions;
}
private function expandSupportTransactions(
PhabricatorLiskDAO $object,
array $xactions) {
$this->loadSubscribers($object);
$xactions = $this->applyImplicitCC($object, $xactions);
$blocks = array();
foreach ($xactions as $key => $xaction) {
$blocks[$key] = $this->getRemarkupBlocksFromTransaction($xaction);
}
$subscribe_xaction = $this->buildSubscribeTransaction(
$object,
$xactions,
$blocks);
if ($subscribe_xaction) {
$xactions[] = $subscribe_xaction;
}
// TODO: For now, this is just a placeholder.
$engine = PhabricatorMarkupEngine::getEngine('extract');
$engine->setConfig('viewer', $this->requireActor());
$block_xactions = $this->expandRemarkupBlockTransactions(
$object,
$xactions,
$blocks,
$engine);
foreach ($block_xactions as $xaction) {
$xactions[] = $xaction;
}
return $xactions;
}
private function expandRemarkupBlockTransactions(
PhabricatorLiskDAO $object,
array $xactions,
$blocks,
PhutilMarkupEngine $engine) {
$block_xactions = $this->expandCustomRemarkupBlockTransactions(
$object,
$xactions,
$blocks,
$engine);
$mentioned_phids = array();
if ($this->shouldEnableMentions($object, $xactions)) {
foreach ($blocks as $key => $xaction_blocks) {
foreach ($xaction_blocks as $block) {
$engine->markupText($block);
$mentioned_phids += $engine->getTextMetadata(
PhabricatorObjectRemarkupRule::KEY_MENTIONED_OBJECTS,
array());
}
}
}
if (!$mentioned_phids) {
return $block_xactions;
}
$mentioned_objects = id(new PhabricatorObjectQuery())
->setViewer($this->getActor())
->withPHIDs($mentioned_phids)
->execute();
$mentionable_phids = array();
if ($this->shouldEnableMentions($object, $xactions)) {
foreach ($mentioned_objects as $mentioned_object) {
if ($mentioned_object instanceof PhabricatorMentionableInterface) {
$mentioned_phid = $mentioned_object->getPHID();
if (idx($this->getUnmentionablePHIDMap(), $mentioned_phid)) {
continue;
}
// don't let objects mention themselves
if ($object->getPHID() && $mentioned_phid == $object->getPHID()) {
continue;
}
$mentionable_phids[$mentioned_phid] = $mentioned_phid;
}
}
}
if ($mentionable_phids) {
$edge_type = PhabricatorObjectMentionsObjectEdgeType::EDGECONST;
$block_xactions[] = newv(get_class(head($xactions)), array())
->setIgnoreOnNoEffect(true)
->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
->setMetadataValue('edge:type', $edge_type)
->setNewValue(array('+' => $mentionable_phids));
}
return $block_xactions;
}
protected function expandCustomRemarkupBlockTransactions(
PhabricatorLiskDAO $object,
array $xactions,
$blocks,
PhutilMarkupEngine $engine) {
return array();
}
/**
* Attempt to combine similar transactions into a smaller number of total
* transactions. For example, two transactions which edit the title of an
* object can be merged into a single edit.
*/
private function combineTransactions(array $xactions) {
$stray_comments = array();
$result = array();
$types = array();
foreach ($xactions as $key => $xaction) {
$type = $xaction->getTransactionType();
if (isset($types[$type])) {
foreach ($types[$type] as $other_key) {
$merged = $this->mergeTransactions($result[$other_key], $xaction);
if ($merged) {
$result[$other_key] = $merged;
if ($xaction->getComment() &&
($xaction->getComment() !== $merged->getComment())) {
$stray_comments[] = $xaction->getComment();
}
if ($result[$other_key]->getComment() &&
($result[$other_key]->getComment() !== $merged->getComment())) {
$stray_comments[] = $result[$other_key]->getComment();
}
// Move on to the next transaction.
continue 2;
}
}
}
$result[$key] = $xaction;
$types[$type][] = $key;
}
// If we merged any comments away, restore them.
foreach ($stray_comments as $comment) {
$xaction = newv(get_class(head($result)), array());
$xaction->setTransactionType(PhabricatorTransactions::TYPE_COMMENT);
$xaction->setComment($comment);
$result[] = $xaction;
}
return array_values($result);
}
protected function mergePHIDOrEdgeTransactions(
PhabricatorApplicationTransaction $u,
PhabricatorApplicationTransaction $v) {
$result = $u->getNewValue();
foreach ($v->getNewValue() as $key => $value) {
if ($u->getTransactionType() == PhabricatorTransactions::TYPE_EDGE) {
if (empty($result[$key])) {
$result[$key] = $value;
} else {
// We're merging two lists of edge adds, sets, or removes. Merge
// them by merging individual PHIDs within them.
$merged = $result[$key];
foreach ($value as $dst => $v_spec) {
if (empty($merged[$dst])) {
$merged[$dst] = $v_spec;
} else {
// Two transactions are trying to perform the same operation on
// the same edge. Normalize the edge data and then merge it. This
// allows transactions to specify how data merges execute in a
// precise way.
$u_spec = $merged[$dst];
if (!is_array($u_spec)) {
$u_spec = array('dst' => $u_spec);
}
if (!is_array($v_spec)) {
$v_spec = array('dst' => $v_spec);
}
$ux_data = idx($u_spec, 'data', array());
$vx_data = idx($v_spec, 'data', array());
$merged_data = $this->mergeEdgeData(
$u->getMetadataValue('edge:type'),
$ux_data,
$vx_data);
$u_spec['data'] = $merged_data;
$merged[$dst] = $u_spec;
}
}
$result[$key] = $merged;
}
} else {
$result[$key] = array_merge($value, idx($result, $key, array()));
}
}
$u->setNewValue($result);
// When combining an "ignore" transaction with a normal transaction, make
// sure we don't propagate the "ignore" flag.
if (!$v->getIgnoreOnNoEffect()) {
$u->setIgnoreOnNoEffect(false);
}
return $u;
}
protected function mergeEdgeData($type, array $u, array $v) {
return $v + $u;
}
protected function getPHIDTransactionNewValue(
PhabricatorApplicationTransaction $xaction,
$old = null) {
if ($old !== null) {
$old = array_fuse($old);
} else {
$old = array_fuse($xaction->getOldValue());
}
$new = $xaction->getNewValue();
$new_add = idx($new, '+', array());
unset($new['+']);
$new_rem = idx($new, '-', array());
unset($new['-']);
$new_set = idx($new, '=', null);
if ($new_set !== null) {
$new_set = array_fuse($new_set);
}
unset($new['=']);
if ($new) {
throw new Exception(
- "Invalid 'new' value for PHID transaction. Value should contain only ".
- "keys '+' (add PHIDs), '-' (remove PHIDs) and '=' (set PHIDS).");
+ pht(
+ "Invalid '%s' value for PHID transaction. Value should contain only ".
+ "keys '%s' (add PHIDs), '%' (remove PHIDs) and '%s' (set PHIDS).",
+ 'new',
+ '+',
+ '-',
+ '='));
}
$result = array();
foreach ($old as $phid) {
if ($new_set !== null && empty($new_set[$phid])) {
continue;
}
$result[$phid] = $phid;
}
if ($new_set !== null) {
foreach ($new_set as $phid) {
$result[$phid] = $phid;
}
}
foreach ($new_add as $phid) {
$result[$phid] = $phid;
}
foreach ($new_rem as $phid) {
unset($result[$phid]);
}
return array_values($result);
}
protected function getEdgeTransactionNewValue(
PhabricatorApplicationTransaction $xaction) {
$new = $xaction->getNewValue();
$new_add = idx($new, '+', array());
unset($new['+']);
$new_rem = idx($new, '-', array());
unset($new['-']);
$new_set = idx($new, '=', null);
unset($new['=']);
if ($new) {
throw new Exception(
- "Invalid 'new' value for Edge transaction. Value should contain only ".
- "keys '+' (add edges), '-' (remove edges) and '=' (set edges).");
+ pht(
+ "Invalid '%s' value for Edge transaction. Value should contain only ".
+ "keys '%s' (add edges), '%s' (remove edges) and '%s' (set edges).",
+ 'new',
+ '+',
+ '-',
+ '='));
}
$old = $xaction->getOldValue();
$lists = array($new_set, $new_add, $new_rem);
foreach ($lists as $list) {
$this->checkEdgeList($list);
}
$result = array();
foreach ($old as $dst_phid => $edge) {
if ($new_set !== null && empty($new_set[$dst_phid])) {
continue;
}
$result[$dst_phid] = $this->normalizeEdgeTransactionValue(
$xaction,
$edge,
$dst_phid);
}
if ($new_set !== null) {
foreach ($new_set as $dst_phid => $edge) {
$result[$dst_phid] = $this->normalizeEdgeTransactionValue(
$xaction,
$edge,
$dst_phid);
}
}
foreach ($new_add as $dst_phid => $edge) {
$result[$dst_phid] = $this->normalizeEdgeTransactionValue(
$xaction,
$edge,
$dst_phid);
}
foreach ($new_rem as $dst_phid => $edge) {
unset($result[$dst_phid]);
}
return $result;
}
private function checkEdgeList($list) {
if (!$list) {
return;
}
foreach ($list as $key => $item) {
if (phid_get_type($key) === PhabricatorPHIDConstants::PHID_TYPE_UNKNOWN) {
throw new Exception(
- "Edge transactions must have destination PHIDs as in edge ".
- "lists (found key '{$key}').");
+ pht(
+ "Edge transactions must have destination PHIDs as in edge ".
+ "lists (found key '%s').",
+ $key));
}
if (!is_array($item) && $item !== $key) {
throw new Exception(
- "Edge transactions must have PHIDs or edge specs as values ".
- "(found value '{$item}').");
+ pht(
+ "Edge transactions must have PHIDs or edge specs as values ".
+ "(found value '%s').",
+ $item));
}
}
}
private function normalizeEdgeTransactionValue(
PhabricatorApplicationTransaction $xaction,
$edge,
$dst_phid) {
if (!is_array($edge)) {
if ($edge != $dst_phid) {
throw new Exception(
pht(
'Transaction edge data must either be the edge PHID or an edge '.
'specification dictionary.'));
}
$edge = array();
} else {
foreach ($edge as $key => $value) {
switch ($key) {
case 'src':
case 'dst':
case 'type':
case 'data':
case 'dateCreated':
case 'dateModified':
case 'seq':
case 'dataID':
break;
default:
throw new Exception(
pht(
- 'Transaction edge specification contains unexpected key '.
- '"%s".',
+ 'Transaction edge specification contains unexpected key "%s".',
$key));
}
}
}
$edge['dst'] = $dst_phid;
$edge_type = $xaction->getMetadataValue('edge:type');
if (empty($edge['type'])) {
$edge['type'] = $edge_type;
} else {
if ($edge['type'] != $edge_type) {
$this_type = $edge['type'];
throw new Exception(
- "Edge transaction includes edge of type '{$this_type}', but ".
- "transaction is of type '{$edge_type}'. Each edge transaction must ".
- "alter edges of only one type.");
+ pht(
+ "Edge transaction includes edge of type '%s', but ".
+ "transaction is of type '%s'. Each edge transaction ".
+ "must alter edges of only one type.",
+ $this_type,
+ $edge_type));
}
}
if (!isset($edge['data'])) {
$edge['data'] = array();
}
return $edge;
}
protected function sortTransactions(array $xactions) {
$head = array();
$tail = array();
// Move bare comments to the end, so the actions precede them.
foreach ($xactions as $xaction) {
$type = $xaction->getTransactionType();
if ($type == PhabricatorTransactions::TYPE_COMMENT) {
$tail[] = $xaction;
} else {
$head[] = $xaction;
}
}
return array_values(array_merge($head, $tail));
}
protected function filterTransactions(
PhabricatorLiskDAO $object,
array $xactions) {
$type_comment = PhabricatorTransactions::TYPE_COMMENT;
$no_effect = array();
$has_comment = false;
$any_effect = false;
foreach ($xactions as $key => $xaction) {
if ($this->transactionHasEffect($object, $xaction)) {
if ($xaction->getTransactionType() != $type_comment) {
$any_effect = true;
}
} else if ($xaction->getIgnoreOnNoEffect()) {
unset($xactions[$key]);
} else {
$no_effect[$key] = $xaction;
}
if ($xaction->hasComment()) {
$has_comment = true;
}
}
if (!$no_effect) {
return $xactions;
}
if (!$this->getContinueOnNoEffect() && !$this->getIsPreview()) {
throw new PhabricatorApplicationTransactionNoEffectException(
$no_effect,
$any_effect,
$has_comment);
}
if (!$any_effect && !$has_comment) {
// If we only have empty comment transactions, just drop them all.
return array();
}
foreach ($no_effect as $key => $xaction) {
if ($xaction->getComment()) {
$xaction->setTransactionType($type_comment);
$xaction->setOldValue(null);
$xaction->setNewValue(null);
} else {
unset($xactions[$key]);
}
}
return $xactions;
}
/**
* Hook for validating transactions. This callback will be invoked for each
* available transaction type, even if an edit does not apply any transactions
* of that type. This allows you to raise exceptions when required fields are
* missing, by detecting that the object has no field value and there is no
* transaction which sets one.
*
* @param PhabricatorLiskDAO Object being edited.
* @param string Transaction type to validate.
* @param list<PhabricatorApplicationTransaction> Transactions of given type,
* which may be empty if the edit does not apply any transactions of the
* given type.
* @return list<PhabricatorApplicationTransactionValidationError> List of
* validation errors.
*/
protected function validateTransaction(
PhabricatorLiskDAO $object,
$type,
array $xactions) {
$errors = array();
switch ($type) {
case PhabricatorTransactions::TYPE_VIEW_POLICY:
$errors[] = $this->validatePolicyTransaction(
$object,
$xactions,
$type,
PhabricatorPolicyCapability::CAN_VIEW);
break;
case PhabricatorTransactions::TYPE_EDIT_POLICY:
$errors[] = $this->validatePolicyTransaction(
$object,
$xactions,
$type,
PhabricatorPolicyCapability::CAN_EDIT);
break;
case PhabricatorTransactions::TYPE_CUSTOMFIELD:
$groups = array();
foreach ($xactions as $xaction) {
$groups[$xaction->getMetadataValue('customfield:key')][] = $xaction;
}
$field_list = PhabricatorCustomField::getObjectFields(
$object,
PhabricatorCustomField::ROLE_EDIT);
$field_list->setViewer($this->getActor());
$role_xactions = PhabricatorCustomField::ROLE_APPLICATIONTRANSACTIONS;
foreach ($field_list->getFields() as $field) {
if (!$field->shouldEnableForRole($role_xactions)) {
continue;
}
$errors[] = $field->validateApplicationTransactions(
$this,
$type,
idx($groups, $field->getFieldKey(), array()));
}
break;
}
return array_mergev($errors);
}
private function validatePolicyTransaction(
PhabricatorLiskDAO $object,
array $xactions,
$transaction_type,
$capability) {
$actor = $this->requireActor();
$errors = array();
// Note $this->xactions is necessary; $xactions is $this->xactions of
// $transaction_type
$policy_object = $this->adjustObjectForPolicyChecks(
$object,
$this->xactions);
// Make sure the user isn't editing away their ability to $capability this
// object.
foreach ($xactions as $xaction) {
try {
PhabricatorPolicyFilter::requireCapabilityWithForcedPolicy(
$actor,
$policy_object,
$capability,
$xaction->getNewValue());
} catch (PhabricatorPolicyException $ex) {
$errors[] = new PhabricatorApplicationTransactionValidationError(
$transaction_type,
pht('Invalid'),
pht(
'You can not select this %s policy, because you would no longer '.
'be able to %s the object.',
$capability,
$capability),
$xaction);
}
}
if ($this->getIsNewObject()) {
if (!$xactions) {
$has_capability = PhabricatorPolicyFilter::hasCapability(
$actor,
$policy_object,
$capability);
if (!$has_capability) {
$errors[] = new PhabricatorApplicationTransactionValidationError(
$transaction_type,
pht('Invalid'),
- pht('The selected %s policy excludes you. Choose a %s policy '.
- 'which allows you to %s the object.',
- $capability,
- $capability,
- $capability));
+ pht(
+ 'The selected %s policy excludes you. Choose a %s policy '.
+ 'which allows you to %s the object.',
+ $capability,
+ $capability,
+ $capability));
}
}
}
return $errors;
}
protected function adjustObjectForPolicyChecks(
PhabricatorLiskDAO $object,
array $xactions) {
return clone $object;
}
protected function validateAllTransactions(
PhabricatorLiskDAO $object,
array $xactions) {
return array();
}
/**
* Check for a missing text field.
*
* A text field is missing if the object has no value and there are no
* transactions which set a value, or if the transactions remove the value.
* This method is intended to make implementing @{method:validateTransaction}
* more convenient:
*
* $missing = $this->validateIsEmptyTextField(
* $object->getName(),
* $xactions);
*
* This will return `true` if the net effect of the object and transactions
* is an empty field.
*
* @param wild Current field value.
* @param list<PhabricatorApplicationTransaction> Transactions editing the
* field.
* @return bool True if the field will be an empty text field after edits.
*/
protected function validateIsEmptyTextField($field_value, array $xactions) {
if (strlen($field_value) && empty($xactions)) {
return false;
}
if ($xactions && strlen(last($xactions)->getNewValue())) {
return false;
}
return true;
}
/* -( Implicit CCs )------------------------------------------------------- */
/**
* When a user interacts with an object, we might want to add them to CC.
*/
final public function applyImplicitCC(
PhabricatorLiskDAO $object,
array $xactions) {
if (!($object instanceof PhabricatorSubscribableInterface)) {
// If the object isn't subscribable, we can't CC them.
return $xactions;
}
$actor_phid = $this->getActingAsPHID();
$type_user = PhabricatorPeopleUserPHIDType::TYPECONST;
if (phid_get_type($actor_phid) != $type_user) {
// Transactions by application actors like Herald, Harbormaster and
// Diffusion should not CC the applications.
return $xactions;
}
if ($object->isAutomaticallySubscribed($actor_phid)) {
// If they're auto-subscribed, don't CC them.
return $xactions;
}
$should_cc = false;
foreach ($xactions as $xaction) {
if ($this->shouldImplyCC($object, $xaction)) {
$should_cc = true;
break;
}
}
if (!$should_cc) {
// Only some types of actions imply a CC (like adding a comment).
return $xactions;
}
if ($object->getPHID()) {
if (isset($this->subscribers[$actor_phid])) {
// If the user is already subscribed, don't implicitly CC them.
return $xactions;
}
$unsub = PhabricatorEdgeQuery::loadDestinationPHIDs(
$object->getPHID(),
PhabricatorObjectHasUnsubscriberEdgeType::EDGECONST);
$unsub = array_fuse($unsub);
if (isset($unsub[$actor_phid])) {
// If the user has previously unsubscribed from this object explicitly,
// don't implicitly CC them.
return $xactions;
}
}
$xaction = newv(get_class(head($xactions)), array());
$xaction->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS);
$xaction->setNewValue(array('+' => array($actor_phid)));
array_unshift($xactions, $xaction);
return $xactions;
}
protected function shouldImplyCC(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
return $xaction->isCommentTransaction();
}
/* -( Sending Mail )------------------------------------------------------- */
/**
* @task mail
*/
protected function shouldSendMail(
PhabricatorLiskDAO $object,
array $xactions) {
return false;
}
/**
* @task mail
*/
protected function sendMail(
PhabricatorLiskDAO $object,
array $xactions) {
// Check if any of the transactions are visible. If we don't have any
// visible transactions, don't send the mail.
$any_visible = false;
foreach ($xactions as $xaction) {
if (!$xaction->shouldHideForMail($xactions)) {
$any_visible = true;
break;
}
}
if (!$any_visible) {
return;
}
$email_force = array();
$email_to = $this->getMailTo($object);
$email_cc = $this->getMailCC($object);
$adapter = $this->getHeraldAdapter();
if ($adapter) {
$email_cc = array_merge($email_cc, $adapter->getEmailPHIDs());
$email_force = $adapter->getForcedEmailPHIDs();
}
$phids = array_merge($email_to, $email_cc);
$handles = id(new PhabricatorHandleQuery())
->setViewer($this->requireActor())
->withPHIDs($phids)
->execute();
$template = $this->buildMailTemplate($object);
$body = $this->buildMailBody($object, $xactions);
$mail_tags = $this->getMailTags($object, $xactions);
$action = $this->getMailAction($object, $xactions);
$reply_handler = $this->buildReplyHandler($object);
$body->addEmailPreferenceSection();
$template
->setFrom($this->getActingAsPHID())
->setSubjectPrefix($this->getMailSubjectPrefix())
->setVarySubjectPrefix('['.$action.']')
->setThreadID($this->getMailThreadID($object), $this->getIsNewObject())
->setRelatedPHID($object->getPHID())
->setExcludeMailRecipientPHIDs($this->getExcludeMailRecipientPHIDs())
->setForceHeraldMailRecipientPHIDs($email_force)
->setMailTags($mail_tags)
->setIsBulk(true)
->setBody($body->render())
->setHTMLBody($body->renderHTML());
foreach ($body->getAttachments() as $attachment) {
$template->addAttachment($attachment);
}
$herald_xscript = $this->getHeraldTranscript();
if ($herald_xscript) {
$herald_header = $herald_xscript->getXHeraldRulesHeader();
$herald_header = HeraldTranscript::saveXHeraldRulesHeader(
$object->getPHID(),
$herald_header);
} else {
$herald_header = HeraldTranscript::loadXHeraldRulesHeader(
$object->getPHID());
}
if ($herald_header) {
$template->addHeader('X-Herald-Rules', $herald_header);
}
if ($object instanceof PhabricatorProjectInterface) {
$this->addMailProjectMetadata($object, $template);
}
if ($this->getParentMessageID()) {
$template->setParentMessageID($this->getParentMessageID());
}
$mails = $reply_handler->multiplexMail(
$template,
array_select_keys($handles, $email_to),
array_select_keys($handles, $email_cc));
foreach ($mails as $mail) {
$mail->saveAndSend();
}
$template->addTos($email_to);
$template->addCCs($email_cc);
return $template;
}
private function addMailProjectMetadata(
PhabricatorLiskDAO $object,
PhabricatorMetaMTAMail $template) {
$project_phids = PhabricatorEdgeQuery::loadDestinationPHIDs(
$object->getPHID(),
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST);
if (!$project_phids) {
return;
}
// TODO: This viewer isn't quite right. It would be slightly better to use
// the mail recipient, but that's not very easy given the way rendering
// works today.
$handles = id(new PhabricatorHandleQuery())
->setViewer($this->requireActor())
->withPHIDs($project_phids)
->execute();
$project_tags = array();
foreach ($handles as $handle) {
if (!$handle->isComplete()) {
continue;
}
$project_tags[] = '<'.$handle->getObjectName().'>';
}
if (!$project_tags) {
return;
}
$project_tags = implode(', ', $project_tags);
$template->addHeader('X-Phabricator-Projects', $project_tags);
}
protected function getMailThreadID(PhabricatorLiskDAO $object) {
return $object->getPHID();
}
/**
* @task mail
*/
protected function getStrongestAction(
PhabricatorLiskDAO $object,
array $xactions) {
return last(msort($xactions, 'getActionStrength'));
}
/**
* @task mail
*/
protected function buildReplyHandler(PhabricatorLiskDAO $object) {
- throw new Exception('Capability not supported.');
+ throw new Exception(pht('Capability not supported.'));
}
/**
* @task mail
*/
protected function getMailSubjectPrefix() {
- throw new Exception('Capability not supported.');
+ throw new Exception(pht('Capability not supported.'));
}
/**
* @task mail
*/
protected function getMailTags(
PhabricatorLiskDAO $object,
array $xactions) {
$tags = array();
foreach ($xactions as $xaction) {
$tags[] = $xaction->getMailTags();
}
return array_mergev($tags);
}
/**
* @task mail
*/
public function getMailTagsMap() {
// TODO: We should move shared mail tags, like "comment", here.
return array();
}
/**
* @task mail
*/
protected function getMailAction(
PhabricatorLiskDAO $object,
array $xactions) {
return $this->getStrongestAction($object, $xactions)->getActionName();
}
/**
* @task mail
*/
protected function buildMailTemplate(PhabricatorLiskDAO $object) {
- throw new Exception('Capability not supported.');
+ throw new Exception(pht('Capability not supported.'));
}
/**
* @task mail
*/
protected function getMailTo(PhabricatorLiskDAO $object) {
- throw new Exception('Capability not supported.');
+ throw new Exception(pht('Capability not supported.'));
}
/**
* @task mail
*/
protected function getMailCC(PhabricatorLiskDAO $object) {
$phids = array();
$has_support = false;
if ($object instanceof PhabricatorSubscribableInterface) {
$phids[] = $this->subscribers;
$has_support = true;
}
if ($object instanceof PhabricatorProjectInterface) {
$project_phids = PhabricatorEdgeQuery::loadDestinationPHIDs(
$object->getPHID(),
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST);
if ($project_phids) {
$watcher_type = PhabricatorObjectHasWatcherEdgeType::EDGECONST;
$query = id(new PhabricatorEdgeQuery())
->withSourcePHIDs($project_phids)
->withEdgeTypes(array($watcher_type));
$query->execute();
$watcher_phids = $query->getDestinationPHIDs();
if ($watcher_phids) {
// We need to do a visibility check for all the watchers, as
// watching a project is not a guarantee that you can see objects
// associated with it.
$users = id(new PhabricatorPeopleQuery())
->setViewer($this->requireActor())
->withPHIDs($watcher_phids)
->execute();
$watchers = array();
foreach ($users as $user) {
$can_see = PhabricatorPolicyFilter::hasCapability(
$user,
$object,
PhabricatorPolicyCapability::CAN_VIEW);
if ($can_see) {
$watchers[] = $user->getPHID();
}
}
$phids[] = $watchers;
}
}
$has_support = true;
}
if (!$has_support) {
- throw new Exception('Capability not supported.');
+ throw new Exception(pht('Capability not supported.'));
}
return array_mergev($phids);
}
/**
* @task mail
*/
protected function buildMailBody(
PhabricatorLiskDAO $object,
array $xactions) {
$body = new PhabricatorMetaMTAMailBody();
$body->setViewer($this->requireActor());
$this->addHeadersAndCommentsToMailBody($body, $xactions);
$this->addCustomFieldsToMailBody($body, $object, $xactions);
return $body;
}
/**
* @task mail
*/
protected function addHeadersAndCommentsToMailBody(
PhabricatorMetaMTAMailBody $body,
array $xactions) {
$headers = array();
$comments = array();
foreach ($xactions as $xaction) {
if ($xaction->shouldHideForMail($xactions)) {
continue;
}
$header = $xaction->getTitleForMail();
if ($header !== null) {
$headers[] = $header;
}
$comment = $xaction->getBodyForMail();
if ($comment !== null) {
$comments[] = $comment;
}
}
$body->addRawSection(implode("\n", $headers));
foreach ($comments as $comment) {
$body->addRemarkupSection($comment);
}
}
/**
* @task mail
*/
protected function addCustomFieldsToMailBody(
PhabricatorMetaMTAMailBody $body,
PhabricatorLiskDAO $object,
array $xactions) {
if ($object instanceof PhabricatorCustomFieldInterface) {
$field_list = PhabricatorCustomField::getObjectFields(
$object,
PhabricatorCustomField::ROLE_TRANSACTIONMAIL);
$field_list->setViewer($this->getActor());
$field_list->readFieldsFromStorage($object);
foreach ($field_list->getFields() as $field) {
$field->updateTransactionMailBody(
$body,
$this,
$xactions);
}
}
}
/* -( Publishing Feed Stories )-------------------------------------------- */
/**
* @task feed
*/
protected function shouldPublishFeedStory(
PhabricatorLiskDAO $object,
array $xactions) {
return false;
}
/**
* @task feed
*/
protected function getFeedStoryType() {
return 'PhabricatorApplicationTransactionFeedStory';
}
/**
* @task feed
*/
protected function getFeedRelatedPHIDs(
PhabricatorLiskDAO $object,
array $xactions) {
$phids = array(
$object->getPHID(),
$this->getActingAsPHID(),
);
if ($object instanceof PhabricatorProjectInterface) {
$project_phids = PhabricatorEdgeQuery::loadDestinationPHIDs(
$object->getPHID(),
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST);
foreach ($project_phids as $project_phid) {
$phids[] = $project_phid;
}
}
return $phids;
}
/**
* @task feed
*/
protected function getFeedNotifyPHIDs(
PhabricatorLiskDAO $object,
array $xactions) {
return array_unique(array_merge(
$this->getMailTo($object),
$this->getMailCC($object)));
}
/**
* @task feed
*/
protected function getFeedStoryData(
PhabricatorLiskDAO $object,
array $xactions) {
$xactions = msort($xactions, 'getActionStrength');
$xactions = array_reverse($xactions);
return array(
'objectPHID' => $object->getPHID(),
'transactionPHIDs' => mpull($xactions, 'getPHID'),
);
}
/**
* @task feed
*/
protected function publishFeedStory(
PhabricatorLiskDAO $object,
array $xactions,
array $mailed_phids) {
$xactions = mfilter($xactions, 'shouldHideForFeed', true);
if (!$xactions) {
return;
}
$related_phids = $this->getFeedRelatedPHIDs($object, $xactions);
$subscribed_phids = $this->getFeedNotifyPHIDs($object, $xactions);
$story_type = $this->getFeedStoryType();
$story_data = $this->getFeedStoryData($object, $xactions);
id(new PhabricatorFeedStoryPublisher())
->setStoryType($story_type)
->setStoryData($story_data)
->setStoryTime(time())
->setStoryAuthorPHID($this->getActingAsPHID())
->setRelatedPHIDs($related_phids)
->setPrimaryObjectPHID($object->getPHID())
->setSubscribedPHIDs($subscribed_phids)
->setMailRecipientPHIDs($mailed_phids)
->setMailTags($this->getMailTags($object, $xactions))
->publish();
}
/* -( Search Index )------------------------------------------------------- */
/**
* @task search
*/
protected function supportsSearch() {
return false;
}
/**
* @task search
*/
protected function getSearchContextParameter(
PhabricatorLiskDAO $object,
array $xactions) {
return null;
}
/* -( Herald Integration )-------------------------------------------------- */
protected function shouldApplyHeraldRules(
PhabricatorLiskDAO $object,
array $xactions) {
return false;
}
protected function buildHeraldAdapter(
PhabricatorLiskDAO $object,
array $xactions) {
- throw new Exception('No herald adapter specified.');
+ throw new Exception(pht('No herald adapter specified.'));
}
private function setHeraldAdapter(HeraldAdapter $adapter) {
$this->heraldAdapter = $adapter;
return $this;
}
protected function getHeraldAdapter() {
return $this->heraldAdapter;
}
private function setHeraldTranscript(HeraldTranscript $transcript) {
$this->heraldTranscript = $transcript;
return $this;
}
protected function getHeraldTranscript() {
return $this->heraldTranscript;
}
private function applyHeraldRules(
PhabricatorLiskDAO $object,
array $xactions) {
$adapter = $this->buildHeraldAdapter($object, $xactions);
$adapter->setContentSource($this->getContentSource());
$adapter->setIsNewObject($this->getIsNewObject());
if ($this->getApplicationEmail()) {
$adapter->setApplicationEmail($this->getApplicationEmail());
}
$xscript = HeraldEngine::loadAndApplyRules($adapter);
$this->setHeraldAdapter($adapter);
$this->setHeraldTranscript($xscript);
return array_merge(
$this->didApplyHeraldRules($object, $adapter, $xscript),
$adapter->getQueuedTransactions());
}
protected function didApplyHeraldRules(
PhabricatorLiskDAO $object,
HeraldAdapter $adapter,
HeraldTranscript $transcript) {
return array();
}
/* -( Custom Fields )------------------------------------------------------ */
/**
* @task customfield
*/
private function getCustomFieldForTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
$field_key = $xaction->getMetadataValue('customfield:key');
if (!$field_key) {
throw new Exception(
- "Custom field transaction has no 'customfield:key'!");
+ pht(
+ "Custom field transaction has no '%s'!",
+ 'customfield:key'));
}
$field = PhabricatorCustomField::getObjectField(
$object,
PhabricatorCustomField::ROLE_APPLICATIONTRANSACTIONS,
$field_key);
if (!$field) {
throw new Exception(
- "Custom field transaction has invalid 'customfield:key'; field ".
- "'{$field_key}' is disabled or does not exist.");
+ pht(
+ "Custom field transaction has invalid '%s'; field '%s' ".
+ "is disabled or does not exist.",
+ 'customfield:key',
+ $field_key));
}
if (!$field->shouldAppearInApplicationTransactions()) {
throw new Exception(
- "Custom field transaction '{$field_key}' does not implement ".
- "integration for ApplicationTransactions.");
+ pht(
+ "Custom field transaction '%s' does not implement ".
+ "integration for %s.",
+ $field_key,
+ 'ApplicationTransactions'));
}
$field->setViewer($this->getActor());
return $field;
}
/* -( Files )-------------------------------------------------------------- */
/**
* Extract the PHIDs of any files which these transactions attach.
*
* @task files
*/
private function extractFilePHIDs(
PhabricatorLiskDAO $object,
array $xactions) {
$blocks = array();
foreach ($xactions as $xaction) {
$blocks[] = $this->getRemarkupBlocksFromTransaction($xaction);
}
$blocks = array_mergev($blocks);
$phids = array();
if ($blocks) {
$phids[] = PhabricatorMarkupEngine::extractFilePHIDsFromEmbeddedFiles(
$this->getActor(),
$blocks);
}
foreach ($xactions as $xaction) {
$phids[] = $this->extractFilePHIDsFromCustomTransaction(
$object,
$xaction);
}
$phids = array_unique(array_filter(array_mergev($phids)));
if (!$phids) {
return array();
}
// Only let a user attach files they can actually see, since this would
// otherwise let you access any file by attaching it to an object you have
// view permission on.
$files = id(new PhabricatorFileQuery())
->setViewer($this->getActor())
->withPHIDs($phids)
->execute();
return mpull($files, 'getPHID');
}
/**
* @task files
*/
protected function extractFilePHIDsFromCustomTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
return array();
}
/**
* @task files
*/
private function attachFiles(
PhabricatorLiskDAO $object,
array $file_phids) {
if (!$file_phids) {
return;
}
$editor = new PhabricatorEdgeEditor();
$src = $object->getPHID();
$type = PhabricatorObjectHasFileEdgeType::EDGECONST;
foreach ($file_phids as $dst) {
$editor->addEdge($src, $type, $dst);
}
$editor->save();
}
private function applyInverseEdgeTransactions(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction,
$inverse_type) {
$old = $xaction->getOldValue();
$new = $xaction->getNewValue();
$add = array_keys(array_diff_key($new, $old));
$rem = array_keys(array_diff_key($old, $new));
$add = array_fuse($add);
$rem = array_fuse($rem);
$all = $add + $rem;
$nodes = id(new PhabricatorObjectQuery())
->setViewer($this->requireActor())
->withPHIDs($all)
->execute();
foreach ($nodes as $node) {
if (!($node instanceof PhabricatorApplicationTransactionInterface)) {
continue;
}
$editor = $node->getApplicationTransactionEditor();
$template = $node->getApplicationTransactionTemplate();
$target = $node->getApplicationTransactionObject();
if (isset($add[$node->getPHID()])) {
$edge_edit_type = '+';
} else {
$edge_edit_type = '-';
}
$template
->setTransactionType($xaction->getTransactionType())
->setMetadataValue('edge:type', $inverse_type)
->setNewValue(
array(
$edge_edit_type => array($object->getPHID() => $object->getPHID()),
));
$editor
->setContinueOnNoEffect(true)
->setContinueOnMissingFields(true)
->setParentMessageID($this->getParentMessageID())
->setIsInverseEdgeEditor(true)
->setActor($this->requireActor())
->setActingAsPHID($this->getActingAsPHID())
->setContentSource($this->getContentSource());
$editor->applyTransactions($target, array($template));
}
}
}
diff --git a/src/applications/transactions/exception/PhabricatorApplicationTransactionValidationException.php b/src/applications/transactions/exception/PhabricatorApplicationTransactionValidationException.php
index edc8d9798..2ed1f54fe 100644
--- a/src/applications/transactions/exception/PhabricatorApplicationTransactionValidationException.php
+++ b/src/applications/transactions/exception/PhabricatorApplicationTransactionValidationException.php
@@ -1,43 +1,43 @@
<?php
final class PhabricatorApplicationTransactionValidationException
extends Exception {
private $errors;
public function __construct(array $errors) {
assert_instances_of(
$errors,
'PhabricatorApplicationTransactionValidationError');
$this->errors = $errors;
$message = array();
- $message[] = 'Validation errors:';
+ $message[] = pht('Validation errors:');
foreach ($this->errors as $error) {
$message[] = ' - '.$error->getMessage();
}
parent::__construct(implode("\n", $message));
}
public function getErrors() {
return $this->errors;
}
public function getErrorMessages() {
return mpull($this->errors, 'getMessage');
}
public function getShortMessage($type) {
foreach ($this->errors as $error) {
if ($error->getType() === $type) {
if ($error->getShortMessage() !== null) {
return $error->getShortMessage();
}
}
}
return null;
}
}
diff --git a/src/applications/transactions/storage/PhabricatorApplicationTransaction.php b/src/applications/transactions/storage/PhabricatorApplicationTransaction.php
index 355f15c2a..a6c171511 100644
--- a/src/applications/transactions/storage/PhabricatorApplicationTransaction.php
+++ b/src/applications/transactions/storage/PhabricatorApplicationTransaction.php
@@ -1,1247 +1,1246 @@
<?php
abstract class PhabricatorApplicationTransaction
extends PhabricatorLiskDAO
implements
PhabricatorPolicyInterface,
PhabricatorDestructibleInterface {
const TARGET_TEXT = 'text';
const TARGET_HTML = 'html';
protected $phid;
protected $objectPHID;
protected $authorPHID;
protected $viewPolicy;
protected $editPolicy;
protected $commentPHID;
protected $commentVersion = 0;
protected $transactionType;
protected $oldValue;
protected $newValue;
protected $metadata = array();
protected $contentSource;
private $comment;
private $commentNotLoaded;
private $handles;
private $renderingTarget = self::TARGET_HTML;
private $transactionGroup = array();
private $viewer = self::ATTACHABLE;
private $object = self::ATTACHABLE;
private $oldValueHasBeenSet = false;
private $ignoreOnNoEffect;
/**
* Flag this transaction as a pure side-effect which should be ignored when
* applying transactions if it has no effect, even if transaction application
* would normally fail. This both provides users with better error messages
* and allows transactions to perform optional side effects.
*/
public function setIgnoreOnNoEffect($ignore) {
$this->ignoreOnNoEffect = $ignore;
return $this;
}
public function getIgnoreOnNoEffect() {
return $this->ignoreOnNoEffect;
}
public function shouldGenerateOldValue() {
switch ($this->getTransactionType()) {
case PhabricatorTransactions::TYPE_BUILDABLE:
case PhabricatorTransactions::TYPE_TOKEN:
case PhabricatorTransactions::TYPE_CUSTOMFIELD:
case PhabricatorTransactions::TYPE_INLINESTATE:
return false;
}
return true;
}
abstract public function getApplicationTransactionType();
private function getApplicationObjectTypeName() {
$types = PhabricatorPHIDType::getAllTypes();
$type = idx($types, $this->getApplicationTransactionType());
if ($type) {
return $type->getTypeName();
}
return pht('Object');
}
public function getApplicationTransactionCommentObject() {
throw new PhutilMethodNotImplementedException();
}
public function getApplicationTransactionViewObject() {
return new PhabricatorApplicationTransactionView();
}
public function getMetadataValue($key, $default = null) {
return idx($this->metadata, $key, $default);
}
public function setMetadataValue($key, $value) {
$this->metadata[$key] = $value;
return $this;
}
public function generatePHID() {
$type = PhabricatorApplicationTransactionTransactionPHIDType::TYPECONST;
$subtype = $this->getApplicationTransactionType();
return PhabricatorPHID::generateNewPHID($type, $subtype);
}
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'oldValue' => self::SERIALIZATION_JSON,
'newValue' => self::SERIALIZATION_JSON,
'metadata' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'commentPHID' => 'phid?',
'commentVersion' => 'uint32',
'contentSource' => 'text',
'transactionType' => 'text32',
),
self::CONFIG_KEY_SCHEMA => array(
'key_object' => array(
'columns' => array('objectPHID'),
),
),
) + parent::getConfiguration();
}
public function setContentSource(PhabricatorContentSource $content_source) {
$this->contentSource = $content_source->serialize();
return $this;
}
public function getContentSource() {
return PhabricatorContentSource::newFromSerialized($this->contentSource);
}
public function hasComment() {
return $this->getComment() && strlen($this->getComment()->getContent());
}
public function getComment() {
if ($this->commentNotLoaded) {
- throw new Exception('Comment for this transaction was not loaded.');
+ throw new Exception(pht('Comment for this transaction was not loaded.'));
}
return $this->comment;
}
public function attachComment(
PhabricatorApplicationTransactionComment $comment) {
$this->comment = $comment;
$this->commentNotLoaded = false;
return $this;
}
public function setCommentNotLoaded($not_loaded) {
$this->commentNotLoaded = $not_loaded;
return $this;
}
public function attachObject($object) {
$this->object = $object;
return $this;
}
public function getObject() {
return $this->assertAttached($this->object);
}
public function getRemarkupBlocks() {
$blocks = array();
switch ($this->getTransactionType()) {
case PhabricatorTransactions::TYPE_CUSTOMFIELD:
$field = $this->getTransactionCustomField();
if ($field) {
$custom_blocks = $field->getApplicationTransactionRemarkupBlocks(
$this);
foreach ($custom_blocks as $custom_block) {
$blocks[] = $custom_block;
}
}
break;
}
if ($this->getComment()) {
$blocks[] = $this->getComment()->getContent();
}
return $blocks;
}
public function setOldValue($value) {
$this->oldValueHasBeenSet = true;
$this->writeField('oldValue', $value);
return $this;
}
public function hasOldValue() {
return $this->oldValueHasBeenSet;
}
/* -( Rendering )---------------------------------------------------------- */
public function setRenderingTarget($rendering_target) {
$this->renderingTarget = $rendering_target;
return $this;
}
public function getRenderingTarget() {
return $this->renderingTarget;
}
public function attachViewer(PhabricatorUser $viewer) {
$this->viewer = $viewer;
return $this;
}
public function getViewer() {
return $this->assertAttached($this->viewer);
}
public function getRequiredHandlePHIDs() {
$phids = array();
$old = $this->getOldValue();
$new = $this->getNewValue();
$phids[] = array($this->getAuthorPHID());
switch ($this->getTransactionType()) {
case PhabricatorTransactions::TYPE_CUSTOMFIELD:
$field = $this->getTransactionCustomField();
if ($field) {
$phids[] = $field->getApplicationTransactionRequiredHandlePHIDs(
$this);
}
break;
case PhabricatorTransactions::TYPE_SUBSCRIBERS:
$phids[] = $old;
$phids[] = $new;
break;
case PhabricatorTransactions::TYPE_EDGE:
$phids[] = ipull($old, 'dst');
$phids[] = ipull($new, 'dst');
break;
case PhabricatorTransactions::TYPE_EDIT_POLICY:
case PhabricatorTransactions::TYPE_VIEW_POLICY:
case PhabricatorTransactions::TYPE_JOIN_POLICY:
if (!PhabricatorPolicyQuery::isGlobalPolicy($old)) {
$phids[] = array($old);
}
if (!PhabricatorPolicyQuery::isGlobalPolicy($new)) {
$phids[] = array($new);
}
break;
case PhabricatorTransactions::TYPE_TOKEN:
break;
case PhabricatorTransactions::TYPE_BUILDABLE:
$phid = $this->getMetadataValue('harbormaster:buildablePHID');
if ($phid) {
$phids[] = array($phid);
}
break;
}
if ($this->getComment()) {
$phids[] = array($this->getComment()->getAuthorPHID());
}
return array_mergev($phids);
}
public function setHandles(array $handles) {
$this->handles = $handles;
return $this;
}
public function getHandle($phid) {
if (empty($this->handles[$phid])) {
throw new Exception(
pht(
'Transaction ("%s", of type "%s") requires a handle ("%s") that it '.
'did not load.',
$this->getPHID(),
$this->getTransactionType(),
$phid));
}
return $this->handles[$phid];
}
public function getHandleIfExists($phid) {
return idx($this->handles, $phid);
}
public function getHandles() {
if ($this->handles === null) {
throw new Exception(
- 'Transaction requires handles and it did not load them.'
- );
+ pht('Transaction requires handles and it did not load them.'));
}
return $this->handles;
}
public function renderHandleLink($phid) {
if ($this->renderingTarget == self::TARGET_HTML) {
return $this->getHandle($phid)->renderLink();
} else {
return $this->getHandle($phid)->getLinkName();
}
}
public function renderHandleList(array $phids) {
$links = array();
foreach ($phids as $phid) {
$links[] = $this->renderHandleLink($phid);
}
if ($this->renderingTarget == self::TARGET_HTML) {
return phutil_implode_html(', ', $links);
} else {
return implode(', ', $links);
}
}
private function renderSubscriberList(array $phids, $change_type) {
if ($this->getRenderingTarget() == self::TARGET_TEXT) {
return $this->renderHandleList($phids);
} else {
$handles = array_select_keys($this->getHandles(), $phids);
return id(new SubscriptionListStringBuilder())
->setHandles($handles)
->setObjectPHID($this->getPHID())
->buildTransactionString($change_type);
}
}
protected function renderPolicyName($phid, $state = 'old') {
$policy = PhabricatorPolicy::newFromPolicyAndHandle(
$phid,
$this->getHandleIfExists($phid));
if ($this->renderingTarget == self::TARGET_HTML) {
switch ($policy->getType()) {
case PhabricatorPolicyType::TYPE_CUSTOM:
$policy->setHref('/transactions/'.$state.'/'.$this->getPHID().'/');
$policy->setWorkflow(true);
break;
default:
break;
}
$output = $policy->renderDescription();
} else {
$output = hsprintf('%s', $policy->getFullName());
}
return $output;
}
public function getIcon() {
switch ($this->getTransactionType()) {
case PhabricatorTransactions::TYPE_COMMENT:
$comment = $this->getComment();
if ($comment && $comment->getIsRemoved()) {
return 'fa-eraser';
}
return 'fa-comment';
case PhabricatorTransactions::TYPE_SUBSCRIBERS:
return 'fa-envelope';
case PhabricatorTransactions::TYPE_VIEW_POLICY:
case PhabricatorTransactions::TYPE_EDIT_POLICY:
case PhabricatorTransactions::TYPE_JOIN_POLICY:
return 'fa-lock';
case PhabricatorTransactions::TYPE_EDGE:
return 'fa-link';
case PhabricatorTransactions::TYPE_BUILDABLE:
return 'fa-wrench';
case PhabricatorTransactions::TYPE_TOKEN:
return 'fa-trophy';
}
return 'fa-pencil';
}
public function getToken() {
switch ($this->getTransactionType()) {
case PhabricatorTransactions::TYPE_TOKEN:
$old = $this->getOldValue();
$new = $this->getNewValue();
if ($new) {
$icon = substr($new, 10);
} else {
$icon = substr($old, 10);
}
return array($icon, !$this->getNewValue());
}
return array(null, null);
}
public function getColor() {
switch ($this->getTransactionType()) {
case PhabricatorTransactions::TYPE_COMMENT;
$comment = $this->getComment();
if ($comment && $comment->getIsRemoved()) {
return 'black';
}
break;
case PhabricatorTransactions::TYPE_BUILDABLE:
switch ($this->getNewValue()) {
case HarbormasterBuildable::STATUS_PASSED:
return 'green';
case HarbormasterBuildable::STATUS_FAILED:
return 'red';
}
break;
}
return null;
}
protected function getTransactionCustomField() {
switch ($this->getTransactionType()) {
case PhabricatorTransactions::TYPE_CUSTOMFIELD:
$key = $this->getMetadataValue('customfield:key');
if (!$key) {
return null;
}
$field = PhabricatorCustomField::getObjectField(
$this->getObject(),
PhabricatorCustomField::ROLE_APPLICATIONTRANSACTIONS,
$key);
if (!$field) {
return null;
}
$field->setViewer($this->getViewer());
return $field;
}
return null;
}
public function shouldHide() {
switch ($this->getTransactionType()) {
case PhabricatorTransactions::TYPE_VIEW_POLICY:
case PhabricatorTransactions::TYPE_EDIT_POLICY:
case PhabricatorTransactions::TYPE_JOIN_POLICY:
if ($this->getOldValue() === null) {
return true;
} else {
return false;
}
break;
case PhabricatorTransactions::TYPE_CUSTOMFIELD:
$field = $this->getTransactionCustomField();
if ($field) {
return $field->shouldHideInApplicationTransactions($this);
}
case PhabricatorTransactions::TYPE_EDGE:
$edge_type = $this->getMetadataValue('edge:type');
switch ($edge_type) {
case PhabricatorObjectMentionsObjectEdgeType::EDGECONST:
return true;
break;
case PhabricatorObjectMentionedByObjectEdgeType::EDGECONST:
$new = ipull($this->getNewValue(), 'dst');
$old = ipull($this->getOldValue(), 'dst');
$add = array_diff($new, $old);
$add_value = reset($add);
$add_handle = $this->getHandle($add_value);
if ($add_handle->getPolicyFiltered()) {
return true;
}
return false;
break;
default:
break;
}
break;
}
return false;
}
public function shouldHideForMail(array $xactions) {
switch ($this->getTransactionType()) {
case PhabricatorTransactions::TYPE_TOKEN:
return true;
case PhabricatorTransactions::TYPE_BUILDABLE:
switch ($this->getNewValue()) {
case HarbormasterBuildable::STATUS_FAILED:
// For now, only ever send mail when builds fail. We might let
// you customize this later, but in most cases this is probably
// completely uninteresting.
return false;
}
return true;
case PhabricatorTransactions::TYPE_EDGE:
$edge_type = $this->getMetadataValue('edge:type');
switch ($edge_type) {
case PhabricatorObjectMentionsObjectEdgeType::EDGECONST:
case PhabricatorObjectMentionedByObjectEdgeType::EDGECONST:
return true;
break;
default:
break;
}
break;
}
// If a transaction publishes an inline comment:
//
// - Don't show it if there are other kinds of transactions. The
// rationale here is that application mail will make the presence
// of inline comments obvious enough by including them prominently
// in the body. We could change this in the future if the obviousness
// needs to be increased.
// - If there are only inline transactions, only show the first
// transaction. The rationale is that seeing multiple "added an inline
// comment" transactions is not useful.
if ($this->isInlineCommentTransaction()) {
foreach ($xactions as $xaction) {
if (!$xaction->isInlineCommentTransaction()) {
return true;
}
}
return ($this !== head($xactions));
}
return $this->shouldHide();
}
public function shouldHideForFeed() {
switch ($this->getTransactionType()) {
case PhabricatorTransactions::TYPE_TOKEN:
return true;
case PhabricatorTransactions::TYPE_BUILDABLE:
switch ($this->getNewValue()) {
case HarbormasterBuildable::STATUS_FAILED:
// For now, don't notify on build passes either. These are pretty
// high volume and annoying, with very little present value. We
// might want to turn them back on in the specific case of
// build successes on the current document?
return false;
}
return true;
case PhabricatorTransactions::TYPE_EDGE:
$edge_type = $this->getMetadataValue('edge:type');
switch ($edge_type) {
case PhabricatorObjectMentionsObjectEdgeType::EDGECONST:
case PhabricatorObjectMentionedByObjectEdgeType::EDGECONST:
return true;
break;
default:
break;
}
break;
case PhabricatorTransactions::TYPE_INLINESTATE:
return true;
}
return $this->shouldHide();
}
public function getTitleForMail() {
return id(clone $this)->setRenderingTarget('text')->getTitle();
}
public function getBodyForMail() {
if ($this->isInlineCommentTransaction()) {
// We don't return inline comment content as mail body content, because
// applications need to contextualize it (by adding line numbers, for
// example) in order for it to make sense.
return null;
}
$comment = $this->getComment();
if ($comment && strlen($comment->getContent())) {
return $comment->getContent();
}
return null;
}
public function getNoEffectDescription() {
switch ($this->getTransactionType()) {
case PhabricatorTransactions::TYPE_COMMENT:
return pht('You can not post an empty comment.');
case PhabricatorTransactions::TYPE_VIEW_POLICY:
return pht(
'This %s already has that view policy.',
$this->getApplicationObjectTypeName());
case PhabricatorTransactions::TYPE_EDIT_POLICY:
return pht(
'This %s already has that edit policy.',
$this->getApplicationObjectTypeName());
case PhabricatorTransactions::TYPE_JOIN_POLICY:
return pht(
'This %s already has that join policy.',
$this->getApplicationObjectTypeName());
case PhabricatorTransactions::TYPE_SUBSCRIBERS:
return pht(
'All users are already subscribed to this %s.',
$this->getApplicationObjectTypeName());
case PhabricatorTransactions::TYPE_EDGE:
return pht('Edges already exist; transaction has no effect.');
}
return pht('Transaction has no effect.');
}
public function getTitle() {
$author_phid = $this->getAuthorPHID();
$old = $this->getOldValue();
$new = $this->getNewValue();
switch ($this->getTransactionType()) {
case PhabricatorTransactions::TYPE_COMMENT:
return pht(
'%s added a comment.',
$this->renderHandleLink($author_phid));
case PhabricatorTransactions::TYPE_VIEW_POLICY:
return pht(
'%s changed the visibility of this %s from "%s" to "%s".',
$this->renderHandleLink($author_phid),
$this->getApplicationObjectTypeName(),
$this->renderPolicyName($old, 'old'),
$this->renderPolicyName($new, 'new'));
case PhabricatorTransactions::TYPE_EDIT_POLICY:
return pht(
'%s changed the edit policy of this %s from "%s" to "%s".',
$this->renderHandleLink($author_phid),
$this->getApplicationObjectTypeName(),
$this->renderPolicyName($old, 'old'),
$this->renderPolicyName($new, 'new'));
case PhabricatorTransactions::TYPE_JOIN_POLICY:
return pht(
'%s changed the join policy of this %s from "%s" to "%s".',
$this->renderHandleLink($author_phid),
$this->getApplicationObjectTypeName(),
$this->renderPolicyName($old, 'old'),
$this->renderPolicyName($new, 'new'));
case PhabricatorTransactions::TYPE_SUBSCRIBERS:
$add = array_diff($new, $old);
$rem = array_diff($old, $new);
if ($add && $rem) {
return pht(
'%s edited subscriber(s), added %d: %s; removed %d: %s.',
$this->renderHandleLink($author_phid),
count($add),
$this->renderSubscriberList($add, 'add'),
count($rem),
$this->renderSubscriberList($rem, 'rem'));
} else if ($add) {
return pht(
'%s added %d subscriber(s): %s.',
$this->renderHandleLink($author_phid),
count($add),
$this->renderSubscriberList($add, 'add'));
} else if ($rem) {
return pht(
'%s removed %d subscriber(s): %s.',
$this->renderHandleLink($author_phid),
count($rem),
$this->renderSubscriberList($rem, 'rem'));
} else {
// This is used when rendering previews, before the user actually
// selects any CCs.
return pht(
'%s updated subscribers...',
$this->renderHandleLink($author_phid));
}
break;
case PhabricatorTransactions::TYPE_EDGE:
$new = ipull($new, 'dst');
$old = ipull($old, 'dst');
$add = array_diff($new, $old);
$rem = array_diff($old, $new);
$type = $this->getMetadata('edge:type');
$type = head($type);
$type_obj = PhabricatorEdgeType::getByConstant($type);
if ($add && $rem) {
return $type_obj->getTransactionEditString(
$this->renderHandleLink($author_phid),
new PhutilNumber(count($add) + count($rem)),
new PhutilNumber(count($add)),
$this->renderHandleList($add),
new PhutilNumber(count($rem)),
$this->renderHandleList($rem));
} else if ($add) {
return $type_obj->getTransactionAddString(
$this->renderHandleLink($author_phid),
new PhutilNumber(count($add)),
$this->renderHandleList($add));
} else if ($rem) {
return $type_obj->getTransactionRemoveString(
$this->renderHandleLink($author_phid),
new PhutilNumber(count($rem)),
$this->renderHandleList($rem));
} else {
return $type_obj->getTransactionPreviewString(
$this->renderHandleLink($author_phid));
}
case PhabricatorTransactions::TYPE_CUSTOMFIELD:
$field = $this->getTransactionCustomField();
if ($field) {
return $field->getApplicationTransactionTitle($this);
} else {
return pht(
'%s edited a custom field.',
$this->renderHandleLink($author_phid));
}
case PhabricatorTransactions::TYPE_TOKEN:
if ($old && $new) {
return pht(
'%s updated a token.',
$this->renderHandleLink($author_phid));
} else if ($old) {
return pht(
'%s rescinded a token.',
$this->renderHandleLink($author_phid));
} else {
return pht(
'%s awarded a token.',
$this->renderHandleLink($author_phid));
}
case PhabricatorTransactions::TYPE_BUILDABLE:
switch ($this->getNewValue()) {
case HarbormasterBuildable::STATUS_BUILDING:
return pht(
'%s started building %s.',
$this->renderHandleLink($author_phid),
$this->renderHandleLink(
$this->getMetadataValue('harbormaster:buildablePHID')));
case HarbormasterBuildable::STATUS_PASSED:
return pht(
'%s completed building %s.',
$this->renderHandleLink($author_phid),
$this->renderHandleLink(
$this->getMetadataValue('harbormaster:buildablePHID')));
case HarbormasterBuildable::STATUS_FAILED:
return pht(
'%s failed to build %s!',
$this->renderHandleLink($author_phid),
$this->renderHandleLink(
$this->getMetadataValue('harbormaster:buildablePHID')));
default:
return null;
}
case PhabricatorTransactions::TYPE_INLINESTATE:
$done = 0;
$undone = 0;
foreach ($new as $phid => $state) {
if ($state == PhabricatorInlineCommentInterface::STATE_DONE) {
$done++;
} else {
$undone++;
}
}
if ($done && $undone) {
return pht(
'%s marked %s inline comment(s) as done and %s inline comment(s) '.
'as not done.',
$this->renderHandleLink($author_phid),
new PhutilNumber($done),
new PhutilNumber($undone));
} else if ($done) {
return pht(
'%s marked %s inline comment(s) as done.',
$this->renderHandleLink($author_phid),
new PhutilNumber($done));
} else {
return pht(
'%s marked %s inline comment(s) as not done.',
$this->renderHandleLink($author_phid),
new PhutilNumber($undone));
}
break;
default:
return pht(
'%s edited this %s.',
$this->renderHandleLink($author_phid),
$this->getApplicationObjectTypeName());
}
}
public function getTitleForFeed() {
$author_phid = $this->getAuthorPHID();
$object_phid = $this->getObjectPHID();
$old = $this->getOldValue();
$new = $this->getNewValue();
switch ($this->getTransactionType()) {
case PhabricatorTransactions::TYPE_COMMENT:
return pht(
'%s added a comment to %s.',
$this->renderHandleLink($author_phid),
$this->renderHandleLink($object_phid));
case PhabricatorTransactions::TYPE_VIEW_POLICY:
return pht(
'%s changed the visibility for %s.',
$this->renderHandleLink($author_phid),
$this->renderHandleLink($object_phid));
case PhabricatorTransactions::TYPE_EDIT_POLICY:
return pht(
'%s changed the edit policy for %s.',
$this->renderHandleLink($author_phid),
$this->renderHandleLink($object_phid));
case PhabricatorTransactions::TYPE_JOIN_POLICY:
return pht(
'%s changed the join policy for %s.',
$this->renderHandleLink($author_phid),
$this->renderHandleLink($object_phid));
case PhabricatorTransactions::TYPE_SUBSCRIBERS:
return pht(
'%s updated subscribers of %s.',
$this->renderHandleLink($author_phid),
$this->renderHandleLink($object_phid));
case PhabricatorTransactions::TYPE_EDGE:
$new = ipull($new, 'dst');
$old = ipull($old, 'dst');
$add = array_diff($new, $old);
$rem = array_diff($old, $new);
$type = $this->getMetadata('edge:type');
$type = head($type);
$type_obj = PhabricatorEdgeType::getByConstant($type);
if ($add && $rem) {
return $type_obj->getFeedEditString(
$this->renderHandleLink($author_phid),
$this->renderHandleLink($object_phid),
new PhutilNumber(count($add) + count($rem)),
new PhutilNumber(count($add)),
$this->renderHandleList($add),
new PhutilNumber(count($rem)),
$this->renderHandleList($rem));
} else if ($add) {
return $type_obj->getFeedAddString(
$this->renderHandleLink($author_phid),
$this->renderHandleLink($object_phid),
new PhutilNumber(count($add)),
$this->renderHandleList($add));
} else if ($rem) {
return $type_obj->getFeedRemoveString(
$this->renderHandleLink($author_phid),
$this->renderHandleLink($object_phid),
new PhutilNumber(count($rem)),
$this->renderHandleList($rem));
} else {
return pht(
'%s edited edge metadata for %s.',
$this->renderHandleLink($author_phid),
$this->renderHandleLink($object_phid));
}
case PhabricatorTransactions::TYPE_CUSTOMFIELD:
$field = $this->getTransactionCustomField();
if ($field) {
return $field->getApplicationTransactionTitleForFeed($this);
} else {
return pht(
'%s edited a custom field on %s.',
$this->renderHandleLink($author_phid),
$this->renderHandleLink($object_phid));
}
case PhabricatorTransactions::TYPE_BUILDABLE:
switch ($this->getNewValue()) {
case HarbormasterBuildable::STATUS_BUILDING:
return pht(
'%s started building %s for %s.',
$this->renderHandleLink($author_phid),
$this->renderHandleLink(
$this->getMetadataValue('harbormaster:buildablePHID')),
$this->renderHandleLink($object_phid));
case HarbormasterBuildable::STATUS_PASSED:
return pht(
'%s completed building %s for %s.',
$this->renderHandleLink($author_phid),
$this->renderHandleLink(
$this->getMetadataValue('harbormaster:buildablePHID')),
$this->renderHandleLink($object_phid));
case HarbormasterBuildable::STATUS_FAILED:
return pht(
'%s failed to build %s for %s.',
$this->renderHandleLink($author_phid),
$this->renderHandleLink(
$this->getMetadataValue('harbormaster:buildablePHID')),
$this->renderHandleLink($object_phid));
default:
return null;
}
}
return $this->getTitle();
}
public function getMarkupFieldsForFeed(PhabricatorFeedStory $story) {
$fields = array();
switch ($this->getTransactionType()) {
case PhabricatorTransactions::TYPE_COMMENT:
$text = $this->getComment()->getContent();
if (strlen($text)) {
$fields[] = 'comment/'.$this->getID();
}
break;
}
return $fields;
}
public function getMarkupTextForFeed(PhabricatorFeedStory $story, $field) {
switch ($this->getTransactionType()) {
case PhabricatorTransactions::TYPE_COMMENT:
$text = $this->getComment()->getContent();
return PhabricatorMarkupEngine::summarize($text);
}
return null;
}
public function getBodyForFeed(PhabricatorFeedStory $story) {
$old = $this->getOldValue();
$new = $this->getNewValue();
$body = null;
switch ($this->getTransactionType()) {
case PhabricatorTransactions::TYPE_COMMENT:
$text = $this->getComment()->getContent();
if (strlen($text)) {
$body = $story->getMarkupFieldOutput('comment/'.$this->getID());
}
break;
}
return $body;
}
public function getActionStrength() {
if ($this->isInlineCommentTransaction()) {
return 0.25;
}
switch ($this->getTransactionType()) {
case PhabricatorTransactions::TYPE_COMMENT:
return 0.5;
case PhabricatorTransactions::TYPE_SUBSCRIBERS:
$old = $this->getOldValue();
$new = $this->getNewValue();
$add = array_diff($old, $new);
$rem = array_diff($new, $old);
// If this action is the actor subscribing or unsubscribing themselves,
// it is less interesting. In particular, if someone makes a comment and
// also implicitly subscribes themselves, we should treat the
// transaction group as "comment", not "subscribe". In this specific
// case (one affected user, and that affected user it the actor),
// decrease the action strength.
if ((count($add) + count($rem)) != 1) {
// Not exactly one CC change.
break;
}
$affected_phid = head(array_merge($add, $rem));
if ($affected_phid != $this->getAuthorPHID()) {
// Affected user is someone else.
break;
}
// Make this weaker than TYPE_COMMENT.
return 0.25;
}
return 1.0;
}
public function isCommentTransaction() {
if ($this->hasComment()) {
return true;
}
switch ($this->getTransactionType()) {
case PhabricatorTransactions::TYPE_COMMENT:
return true;
}
return false;
}
public function isInlineCommentTransaction() {
return false;
}
public function getActionName() {
switch ($this->getTransactionType()) {
case PhabricatorTransactions::TYPE_COMMENT:
return pht('Commented On');
case PhabricatorTransactions::TYPE_VIEW_POLICY:
case PhabricatorTransactions::TYPE_EDIT_POLICY:
case PhabricatorTransactions::TYPE_JOIN_POLICY:
return pht('Changed Policy');
case PhabricatorTransactions::TYPE_SUBSCRIBERS:
return pht('Changed Subscribers');
case PhabricatorTransactions::TYPE_BUILDABLE:
switch ($this->getNewValue()) {
case HarbormasterBuildable::STATUS_PASSED:
return pht('Build Passed');
case HarbormasterBuildable::STATUS_FAILED:
return pht('Build Failed');
default:
return pht('Build Status');
}
default:
return pht('Updated');
}
}
public function getMailTags() {
return array();
}
public function hasChangeDetails() {
switch ($this->getTransactionType()) {
case PhabricatorTransactions::TYPE_CUSTOMFIELD:
$field = $this->getTransactionCustomField();
if ($field) {
return $field->getApplicationTransactionHasChangeDetails($this);
}
break;
}
return false;
}
public function renderChangeDetails(PhabricatorUser $viewer) {
switch ($this->getTransactionType()) {
case PhabricatorTransactions::TYPE_CUSTOMFIELD:
$field = $this->getTransactionCustomField();
if ($field) {
return $field->getApplicationTransactionChangeDetails($this, $viewer);
}
break;
}
return $this->renderTextCorpusChangeDetails(
$viewer,
$this->getOldValue(),
$this->getNewValue());
}
public function renderTextCorpusChangeDetails(
PhabricatorUser $viewer,
$old,
$new) {
require_celerity_resource('differential-changeset-view-css');
$view = id(new PhabricatorApplicationTransactionTextDiffDetailView())
->setUser($viewer)
->setOldText($old)
->setNewText($new);
return $view->render();
}
public function attachTransactionGroup(array $group) {
assert_instances_of($group, __CLASS__);
$this->transactionGroup = $group;
return $this;
}
public function getTransactionGroup() {
return $this->transactionGroup;
}
/**
* Should this transaction be visually grouped with an existing transaction
* group?
*
* @param list<PhabricatorApplicationTransaction> List of transactions.
* @return bool True to display in a group with the other transactions.
*/
public function shouldDisplayGroupWith(array $group) {
$this_source = null;
if ($this->getContentSource()) {
$this_source = $this->getContentSource()->getSource();
}
foreach ($group as $xaction) {
// Don't group transactions by different authors.
if ($xaction->getAuthorPHID() != $this->getAuthorPHID()) {
return false;
}
// Don't group transactions for different objects.
if ($xaction->getObjectPHID() != $this->getObjectPHID()) {
return false;
}
// Don't group anything into a group which already has a comment.
if ($xaction->isCommentTransaction()) {
return false;
}
// Don't group transactions from different content sources.
$other_source = null;
if ($xaction->getContentSource()) {
$other_source = $xaction->getContentSource()->getSource();
}
if ($other_source != $this_source) {
return false;
}
// Don't group transactions which happened more than 2 minutes apart.
$apart = abs($xaction->getDateCreated() - $this->getDateCreated());
if ($apart > (60 * 2)) {
return false;
}
}
return true;
}
public function renderExtraInformationLink() {
$herald_xscript_id = $this->getMetadataValue('herald:transcriptID');
if ($herald_xscript_id) {
return phutil_tag(
'a',
array(
'href' => '/herald/transcript/'.$herald_xscript_id.'/',
),
pht('View Herald Transcript'));
}
return null;
}
public function renderAsTextForDoorkeeper(
DoorkeeperFeedStoryPublisher $publisher,
PhabricatorFeedStory $story,
array $xactions) {
$text = array();
$body = array();
foreach ($xactions as $xaction) {
$xaction_body = $xaction->getBodyForMail();
if ($xaction_body !== null) {
$body[] = $xaction_body;
}
if ($xaction->shouldHideForMail($xactions)) {
continue;
}
$old_target = $xaction->getRenderingTarget();
$new_target = self::TARGET_TEXT;
$xaction->setRenderingTarget($new_target);
if ($publisher->getRenderWithImpliedContext()) {
$text[] = $xaction->getTitle();
} else {
$text[] = $xaction->getTitleForFeed();
}
$xaction->setRenderingTarget($old_target);
}
$text = implode("\n", $text);
$body = implode("\n\n", $body);
return rtrim($text."\n\n".$body);
}
/* -( PhabricatorPolicyInterface Implementation )-------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
return $this->getViewPolicy();
case PhabricatorPolicyCapability::CAN_EDIT:
return $this->getEditPolicy();
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return ($viewer->getPHID() == $this->getAuthorPHID());
}
public function describeAutomaticCapability($capability) {
return pht(
'Transactions are visible to users that can see the object which was '.
'acted upon. Some transactions - in particular, comments - are '.
'editable by the transaction author.');
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$this->openTransaction();
$comment_template = null;
try {
$comment_template = $this->getApplicationTransactionCommentObject();
} catch (Exception $ex) {
// Continue; no comments for these transactions.
}
if ($comment_template) {
$comments = $comment_template->loadAllWhere(
'transactionPHID = %s',
$this->getPHID());
foreach ($comments as $comment) {
$engine->destroyObject($comment);
}
}
$this->delete();
$this->saveTransaction();
}
}
diff --git a/src/applications/transactions/view/PhabricatorApplicationTransactionView.php b/src/applications/transactions/view/PhabricatorApplicationTransactionView.php
index 2a8575e3c..021abfb91 100644
--- a/src/applications/transactions/view/PhabricatorApplicationTransactionView.php
+++ b/src/applications/transactions/view/PhabricatorApplicationTransactionView.php
@@ -1,524 +1,523 @@
<?php
/**
* @concrete-extensible
*/
class PhabricatorApplicationTransactionView extends AphrontView {
private $transactions;
private $engine;
private $showEditActions = true;
private $isPreview;
private $objectPHID;
private $shouldTerminate = false;
private $quoteTargetID;
private $quoteRef;
private $pager;
private $renderAsFeed;
private $renderData = array();
private $hideCommentOptions = false;
public function setRenderAsFeed($feed) {
$this->renderAsFeed = $feed;
return $this;
}
public function setQuoteRef($quote_ref) {
$this->quoteRef = $quote_ref;
return $this;
}
public function getQuoteRef() {
return $this->quoteRef;
}
public function setQuoteTargetID($quote_target_id) {
$this->quoteTargetID = $quote_target_id;
return $this;
}
public function getQuoteTargetID() {
return $this->quoteTargetID;
}
public function setObjectPHID($object_phid) {
$this->objectPHID = $object_phid;
return $this;
}
public function getObjectPHID() {
return $this->objectPHID;
}
public function setIsPreview($is_preview) {
$this->isPreview = $is_preview;
return $this;
}
public function setShowEditActions($show_edit_actions) {
$this->showEditActions = $show_edit_actions;
return $this;
}
public function getShowEditActions() {
return $this->showEditActions;
}
public function setMarkupEngine(PhabricatorMarkupEngine $engine) {
$this->engine = $engine;
return $this;
}
public function setTransactions(array $transactions) {
assert_instances_of($transactions, 'PhabricatorApplicationTransaction');
$this->transactions = $transactions;
return $this;
}
public function getTransactions() {
return $this->transactions;
}
public function setShouldTerminate($term) {
$this->shouldTerminate = $term;
return $this;
}
public function setPager(AphrontCursorPagerView $pager) {
$this->pager = $pager;
return $this;
}
public function getPager() {
return $this->pager;
}
/**
* This is additional data that may be necessary to render the next set
* of transactions. Objects that implement
* PhabricatorApplicationTransactionInterface use this data in
* willRenderTimeline.
*/
public function setRenderData(array $data) {
$this->renderData = $data;
return $this;
}
public function getRenderData() {
return $this->renderData;
}
public function setHideCommentOptions($hide_comment_options) {
$this->hideCommentOptions = $hide_comment_options;
return $this;
}
public function getHideCommentOptions() {
return $this->hideCommentOptions;
}
public function buildEvents($with_hiding = false) {
$user = $this->getUser();
$xactions = $this->transactions;
$xactions = $this->filterHiddenTransactions($xactions);
$xactions = $this->groupRelatedTransactions($xactions);
$groups = $this->groupDisplayTransactions($xactions);
// If the viewer has interacted with this object, we hide things from
// before their most recent interaction by default. This tends to make
// very long threads much more manageable, because you don't have to
// scroll through a lot of history and can focus on just new stuff.
$show_group = null;
if ($with_hiding) {
// Find the most recent comment by the viewer.
$group_keys = array_keys($groups);
$group_keys = array_reverse($group_keys);
// If we would only hide a small number of transactions, don't hide
// anything. Just don't examine the last few keys. Also, we always
// want to show the most recent pieces of activity, so don't examine
// the first few keys either.
$group_keys = array_slice($group_keys, 2, -2);
$type_comment = PhabricatorTransactions::TYPE_COMMENT;
foreach ($group_keys as $group_key) {
$group = $groups[$group_key];
foreach ($group as $xaction) {
if ($xaction->getAuthorPHID() == $user->getPHID() &&
$xaction->getTransactionType() == $type_comment) {
// This is the most recent group where the user commented.
$show_group = $group_key;
break 2;
}
}
}
}
$events = array();
$hide_by_default = ($show_group !== null);
$set_next_page_id = false;
foreach ($groups as $group_key => $group) {
if ($hide_by_default && ($show_group === $group_key)) {
$hide_by_default = false;
$set_next_page_id = true;
}
$group_event = null;
foreach ($group as $xaction) {
$event = $this->renderEvent($xaction, $group);
$event->setHideByDefault($hide_by_default);
if (!$group_event) {
$group_event = $event;
} else {
$group_event->addEventToGroup($event);
}
if ($set_next_page_id) {
$set_next_page_id = false;
$pager = $this->getPager();
if ($pager) {
$pager->setNextPageID($xaction->getID());
}
}
}
$events[] = $group_event;
}
return $events;
}
public function render() {
if (!$this->getObjectPHID()) {
- throw new Exception('Call setObjectPHID() before render()!');
+ throw new PhutilInvalidStateException('setObjectPHID');
}
$view = $this->buildPHUITimelineView();
if ($this->getShowEditActions()) {
Javelin::initBehavior('phabricator-transaction-list');
}
return $view->render();
}
public function buildPHUITimelineView($with_hiding = true) {
if (!$this->getObjectPHID()) {
- throw new Exception(
- 'Call setObjectPHID() before buildPHUITimelineView()!');
+ throw new PhutilInvalidStateException('setObjectPHID');
}
$view = new PHUITimelineView();
$view->setShouldTerminate($this->shouldTerminate);
$view->setQuoteTargetID($this->getQuoteTargetID());
$view->setQuoteRef($this->getQuoteRef());
$events = $this->buildEvents($with_hiding);
foreach ($events as $event) {
$view->addEvent($event);
}
if ($this->getPager()) {
$view->setPager($this->getPager());
}
if ($this->getRenderData()) {
$view->setRenderData($this->getRenderData());
}
return $view;
}
protected function getOrBuildEngine() {
if (!$this->engine) {
$field = PhabricatorApplicationTransactionComment::MARKUP_FIELD_COMMENT;
$engine = id(new PhabricatorMarkupEngine())
->setViewer($this->getUser());
foreach ($this->transactions as $xaction) {
if (!$xaction->hasComment()) {
continue;
}
$engine->addObject($xaction->getComment(), $field);
}
$engine->process();
$this->engine = $engine;
}
return $this->engine;
}
private function buildChangeDetailsLink(
PhabricatorApplicationTransaction $xaction) {
return javelin_tag(
'a',
array(
'href' => '/transactions/detail/'.$xaction->getPHID().'/',
'sigil' => 'workflow',
),
pht('(Show Details)'));
}
private function buildExtraInformationLink(
PhabricatorApplicationTransaction $xaction) {
$link = $xaction->renderExtraInformationLink();
if (!$link) {
return null;
}
return phutil_tag(
'span',
array(
'class' => 'phui-timeline-extra-information',
),
array(" \xC2\xB7 ", $link));
}
protected function shouldGroupTransactions(
PhabricatorApplicationTransaction $u,
PhabricatorApplicationTransaction $v) {
return false;
}
protected function renderTransactionContent(
PhabricatorApplicationTransaction $xaction) {
$field = PhabricatorApplicationTransactionComment::MARKUP_FIELD_COMMENT;
$engine = $this->getOrBuildEngine();
$comment = $xaction->getComment();
if ($comment) {
if ($comment->getIsRemoved()) {
return javelin_tag(
'span',
array(
'class' => 'comment-deleted',
'sigil' => 'transaction-comment',
'meta' => array('phid' => $comment->getTransactionPHID()),
),
pht(
'This comment was removed by %s.',
$xaction->getHandle($comment->getAuthorPHID())->renderLink()));
} else if ($comment->getIsDeleted()) {
return javelin_tag(
'span',
array(
'class' => 'comment-deleted',
'sigil' => 'transaction-comment',
'meta' => array('phid' => $comment->getTransactionPHID()),
),
pht('This comment has been deleted.'));
} else if ($xaction->hasComment()) {
return javelin_tag(
'span',
array(
'class' => 'transaction-comment',
'sigil' => 'transaction-comment',
'meta' => array('phid' => $comment->getTransactionPHID()),
),
$engine->getOutput($comment, $field));
} else {
// This is an empty, non-deleted comment. Usually this happens when
// rendering previews.
return null;
}
}
return null;
}
private function filterHiddenTransactions(array $xactions) {
foreach ($xactions as $key => $xaction) {
if ($xaction->shouldHide()) {
unset($xactions[$key]);
}
}
return $xactions;
}
private function groupRelatedTransactions(array $xactions) {
$last = null;
$last_key = null;
$groups = array();
foreach ($xactions as $key => $xaction) {
if ($last && $this->shouldGroupTransactions($last, $xaction)) {
$groups[$last_key][] = $xaction;
unset($xactions[$key]);
} else {
$last = $xaction;
$last_key = $key;
}
}
foreach ($xactions as $key => $xaction) {
$xaction->attachTransactionGroup(idx($groups, $key, array()));
}
return $xactions;
}
private function groupDisplayTransactions(array $xactions) {
$groups = array();
$group = array();
foreach ($xactions as $xaction) {
if ($xaction->shouldDisplayGroupWith($group)) {
$group[] = $xaction;
} else {
if ($group) {
$groups[] = $group;
}
$group = array($xaction);
}
}
if ($group) {
$groups[] = $group;
}
foreach ($groups as $key => $group) {
$group = msort($group, 'getActionStrength');
$group = array_reverse($group);
$groups[$key] = $group;
}
return $groups;
}
private function renderEvent(
PhabricatorApplicationTransaction $xaction,
array $group) {
$viewer = $this->getUser();
$event = id(new PHUITimelineEventView())
->setUser($viewer)
->setTransactionPHID($xaction->getPHID())
->setUserHandle($xaction->getHandle($xaction->getAuthorPHID()))
->setIcon($xaction->getIcon())
->setColor($xaction->getColor())
->setHideCommentOptions($this->getHideCommentOptions());
list($token, $token_removed) = $xaction->getToken();
if ($token) {
$event->setToken($token, $token_removed);
}
if (!$this->shouldSuppressTitle($xaction, $group)) {
if ($this->renderAsFeed) {
$title = $xaction->getTitleForFeed();
} else {
$title = $xaction->getTitle();
}
if ($xaction->hasChangeDetails()) {
if (!$this->isPreview) {
$details = $this->buildChangeDetailsLink($xaction);
$title = array(
$title,
' ',
$details,
);
}
}
if (!$this->isPreview) {
$more = $this->buildExtraInformationLink($xaction);
if ($more) {
$title = array($title, ' ', $more);
}
}
$event->setTitle($title);
}
if ($this->isPreview) {
$event->setIsPreview(true);
} else {
$event
->setDateCreated($xaction->getDateCreated())
->setContentSource($xaction->getContentSource())
->setAnchor($xaction->getID());
}
$transaction_type = $xaction->getTransactionType();
$comment_type = PhabricatorTransactions::TYPE_COMMENT;
$is_normal_comment = ($transaction_type == $comment_type);
if ($this->getShowEditActions() &&
!$this->isPreview &&
$is_normal_comment) {
$has_deleted_comment =
$xaction->getComment() &&
$xaction->getComment()->getIsDeleted();
$has_removed_comment =
$xaction->getComment() &&
$xaction->getComment()->getIsRemoved();
if ($xaction->getCommentVersion() > 1 && !$has_removed_comment) {
$event->setIsEdited(true);
}
if (!$has_removed_comment) {
$event->setIsNormalComment(true);
}
// If we have a place for quoted text to go and this is a quotable
// comment, pass the quote target ID to the event view.
if ($this->getQuoteTargetID()) {
if ($xaction->hasComment()) {
if (!$has_removed_comment && !$has_deleted_comment) {
$event->setQuoteTargetID($this->getQuoteTargetID());
$event->setQuoteRef($this->getQuoteRef());
}
}
}
$can_edit = PhabricatorPolicyCapability::CAN_EDIT;
if ($xaction->hasComment() || $has_deleted_comment) {
$has_edit_capability = PhabricatorPolicyFilter::hasCapability(
$viewer,
$xaction,
$can_edit);
if ($has_edit_capability && !$has_removed_comment) {
$event->setIsEditable(true);
}
if ($has_edit_capability || $viewer->getIsAdmin()) {
if (!$has_removed_comment) {
$event->setIsRemovable(true);
}
}
}
}
$comment = $this->renderTransactionContent($xaction);
if ($comment) {
$event->appendChild($comment);
}
return $event;
}
private function shouldSuppressTitle(
PhabricatorApplicationTransaction $xaction,
array $group) {
// This is a little hard-coded, but we don't have any other reasonable
// cases for now. Suppress "commented on" if there are other actions in
// the display group.
if (count($group) > 1) {
$type_comment = PhabricatorTransactions::TYPE_COMMENT;
if ($xaction->getTransactionType() == $type_comment) {
return true;
}
}
return false;
}
}
diff --git a/src/applications/typeahead/controller/PhabricatorTypeaheadFunctionHelpController.php b/src/applications/typeahead/controller/PhabricatorTypeaheadFunctionHelpController.php
index dd29069ee..851551d77 100644
--- a/src/applications/typeahead/controller/PhabricatorTypeaheadFunctionHelpController.php
+++ b/src/applications/typeahead/controller/PhabricatorTypeaheadFunctionHelpController.php
@@ -1,154 +1,150 @@
<?php
final class PhabricatorTypeaheadFunctionHelpController
extends PhabricatorTypeaheadDatasourceController {
public function shouldAllowPublic() {
return true;
}
public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
$class = $request->getURIData('class');
$sources = id(new PhutilSymbolLoader())
->setAncestorClass('PhabricatorTypeaheadDatasource')
->loadObjects();
if (!isset($sources[$class])) {
return new Aphront404Response();
}
$source = $sources[$class];
$application_class = $source->getDatasourceApplicationClass();
if ($application_class) {
$result = id(new PhabricatorApplicationQuery())
->setViewer($this->getViewer())
->withClasses(array($application_class))
->execute();
if (!$result) {
return new Aphront404Response();
}
}
$source->setViewer($viewer);
$title = pht('Typeahead Function Help');
$functions = $source->getAllDatasourceFunctions();
ksort($functions);
$content = array();
$content[] = '= '.pht('Overview');
$content[] = pht(
'Typeahead functions are an advanced feature which allow you to build '.
'more powerful queries. This document explains functions available '.
'for the selected control.'.
"\n\n".
'For general help with search, see the [[ %s | Search User Guide ]] in '.
'the documentation.'.
"\n\n".
'Note that different controls support //different// functions '.
'(depending on what the control is doing), so these specific functions '.
'may not work everywhere. You can always check the help for a control '.
'to review which functions are available for that control.',
PhabricatorEnv::getDoclink('Search User Guide'));
$table = array();
$table_header = array(
pht('Function'),
pht('Token Name'),
pht('Summary'),
);
$table[] = '| '.implode(' | ', $table_header).' |';
$table[] = '|---|---|---|';
foreach ($functions as $function => $spec) {
$spec = $spec + array(
'summary' => null,
'arguments' => null,
);
if (idx($spec, 'arguments')) {
$signature = '**'.$function.'(**//'.$spec['arguments'].'//**)**';
} else {
$signature = '**'.$function.'()**';
}
$name = idx($spec, 'name', '');
$summary = idx($spec, 'summary', '');
$table[] = '| '.$signature.' | '.$name.' | '.$summary.' |';
}
$table = implode("\n", $table);
$content[] = '= '.pht('Function Quick Reference');
$content[] = pht(
'This table briefly describes available functions for this control. '.
'For details on a particular function, see the corresponding section '.
'below.');
$content[] = $table;
$content[] = '= '.pht('Using Typeahead Functions');
$content[] = pht(
- 'In addition to typing user and project names to build queries, you can '.
- 'also type the names of special functions which give you more options '.
- 'and the ability to express more complex queries.'.
- "\n\n".
- 'Functions have an internal name (like `viewer()`) and a '.
- 'human-readable name, like `Current Viewer`. In general, you can type '.
- 'either one to select the function. You can also click the '.
- '{nav icon=search} button on any typeahead control to browse available '.
- 'functions and find this documentation.'.
- "\n\n".
- 'This documentation uses the internal names to make it clear where '.
- 'tokens begin and end. Specifically, you will find queries written '.
- 'out like this in the documentation: '.
- "\n\n".
- '> viewer(), alincoln'.
- "\n\n".
- 'When this query is actually shown in the control, it will look more '.
- 'like this:'.
- "\n\n".
+ "In addition to typing user and project names to build queries, you can ".
+ "also type the names of special functions which give you more options ".
+ "and the ability to express more complex queries.\n\n".
+ "Functions have an internal name (like `%s`) and a human-readable name, ".
+ "like `Current Viewer`. In general, you can type either one to select ".
+ "the function. You can also click the {nav icon=search} button on any ".
+ "typeahead control to browse available functions and find this ".
+ "documentation.\n\n".
+ "This documentation uses the internal names to make it clear where ".
+ "tokens begin and end. Specifically, you will find queries written ".
+ "out like this in the documentation:\n\n%s\n\n".
+ "When this query is actually shown in the control, it will look more ".
+ "like this:\n\n%s",
+ 'viewer()',
+ '> viewer(), alincoln',
'> {nav Current Viewer} {nav alincoln (Abraham Lincoln)}');
$middot = "\xC2\xB7";
foreach ($functions as $function => $spec) {
$arguments = idx($spec, 'arguments', '');
$name = idx($spec, 'name');
$content[] = '= '.$function.'('.$arguments.') '.$middot.' '.$name;
$content[] = $spec['description'];
}
$content = implode("\n\n", $content);
$content_box = PhabricatorMarkupEngine::renderOneObject(
id(new PhabricatorMarkupOneOff())->setContent($content),
'default',
$viewer);
$header = id(new PHUIHeaderView())
->setHeader($title);
$document = id(new PHUIDocumentView())
->setHeader($header)
->setFontKit(PHUIDocumentView::FONT_SOURCE_SANS)
->appendChild($content_box);
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb(pht('Function Help'));
return $this->buildApplicationPage(
array(
$crumbs,
$document,
),
array(
'title' => $title,
));
}
}
diff --git a/src/applications/uiexample/examples/JavelinReactorUIExample.php b/src/applications/uiexample/examples/JavelinReactorUIExample.php
index 9104c84c2..9e4754507 100644
--- a/src/applications/uiexample/examples/JavelinReactorUIExample.php
+++ b/src/applications/uiexample/examples/JavelinReactorUIExample.php
@@ -1,91 +1,95 @@
<?php
final class JavelinReactorUIExample extends PhabricatorUIExample {
public function getName() {
- return 'Javelin Reactor';
+ return pht('Javelin Reactor');
}
public function getDescription() {
- return 'Lots of code';
+ return pht('Lots of code');
}
public function renderExample() {
$rows = array();
$examples = array(
array(
- 'Reactive button only generates a stream of events',
+ pht('Reactive button only generates a stream of events'),
'ReactorButtonExample',
'phabricator-uiexample-reactor-button',
array(),
),
array(
- 'Reactive checkbox generates a boolean dynamic value',
+ pht('Reactive checkbox generates a boolean dynamic value'),
'ReactorCheckboxExample',
'phabricator-uiexample-reactor-checkbox',
array('checked' => true),
),
array(
- 'Reactive focus detector generates a boolean dynamic value',
+ pht('Reactive focus detector generates a boolean dynamic value'),
'ReactorFocusExample',
'phabricator-uiexample-reactor-focus',
array(),
),
array(
- 'Reactive input box, with normal and calmed output',
+ pht('Reactive input box, with normal and calmed output'),
'ReactorInputExample',
'phabricator-uiexample-reactor-input',
array('init' => 'Initial value'),
),
array(
- 'Reactive mouseover detector generates a boolean dynamic value',
+ pht('Reactive mouseover detector generates a boolean dynamic value'),
'ReactorMouseoverExample',
'phabricator-uiexample-reactor-mouseover',
array(),
),
array(
- 'Reactive radio buttons generate a string dynamic value',
+ pht('Reactive radio buttons generate a string dynamic value'),
'ReactorRadioExample',
'phabricator-uiexample-reactor-radio',
array(),
),
array(
- 'Reactive select box generates a string dynamic value',
+ pht('Reactive select box generates a string dynamic value'),
'ReactorSelectExample',
'phabricator-uiexample-reactor-select',
array(),
),
array(
- 'sendclass makes the class of an element a string dynamic value',
+ pht(
+ '%s makes the class of an element a string dynamic value',
+ 'sendclass'),
'ReactorSendClassExample',
'phabricator-uiexample-reactor-sendclass',
array(),
),
array(
- 'sendproperties makes some properties of an object into dynamic values',
+ pht(
+ '%s makes some properties of an object into dynamic values',
+ 'sendproperties'),
'ReactorSendPropertiesExample',
'phabricator-uiexample-reactor-sendproperties',
array(),
),
);
foreach ($examples as $example) {
list($desc, $name, $resource, $params) = $example;
$template = new AphrontJavelinView();
$template
->setName($name)
->setParameters($params)
->setCelerityResource($resource);
$rows[] = array($desc, $template->render());
}
$table = new AphrontTableView($rows);
$panel = new PHUIObjectBoxView();
$panel->setHeaderText(pht('Example'));
$panel->appendChild($table);
return $panel;
}
}
diff --git a/src/applications/uiexample/examples/JavelinUIExample.php b/src/applications/uiexample/examples/JavelinUIExample.php
index 5976066bd..c85a2d4cd 100644
--- a/src/applications/uiexample/examples/JavelinUIExample.php
+++ b/src/applications/uiexample/examples/JavelinUIExample.php
@@ -1,66 +1,66 @@
<?php
final class JavelinUIExample extends PhabricatorUIExample {
public function getName() {
- return 'Javelin UI';
+ return pht('Javelin UI');
}
public function getDescription() {
- return 'Here are some Javelin UI elements that you could use.';
+ return pht('Here are some Javelin UI elements that you could use.');
}
public function renderExample() {
$request = $this->getRequest();
$user = $request->getUser();
// toggle-class
$container_id = celerity_generate_unique_node_id();
$button_red_id = celerity_generate_unique_node_id();
$button_blue_id = celerity_generate_unique_node_id();
$button_red = javelin_tag(
'a',
array(
'class' => 'button',
'sigil' => 'jx-toggle-class',
'href' => '#',
'id' => $button_red_id,
'meta' => array(
'map' => array(
$container_id => 'jxui-red-border',
$button_red_id => 'jxui-active',
),
),
),
- 'Toggle Red Border');
+ pht('Toggle Red Border'));
$button_blue = javelin_tag(
'a',
array(
'class' => 'button jxui-active',
'sigil' => 'jx-toggle-class',
'href' => '#',
'id' => $button_blue_id,
'meta' => array(
'state' => true,
'map' => array(
$container_id => 'jxui-blue-background',
$button_blue_id => 'jxui-active',
),
),
),
- 'Toggle Blue Background');
+ pht('Toggle Blue Background'));
$div = phutil_tag(
'div',
array(
'id' => $container_id,
'class' => 'jxui-example-container jxui-blue-background',
),
array($button_red, $button_blue));
return array($div);
}
}
diff --git a/src/applications/uiexample/examples/JavelinViewUIExample.php b/src/applications/uiexample/examples/JavelinViewUIExample.php
index 73389475a..f7df1749b 100644
--- a/src/applications/uiexample/examples/JavelinViewUIExample.php
+++ b/src/applications/uiexample/examples/JavelinViewUIExample.php
@@ -1,45 +1,45 @@
<?php
final class JavelinViewUIExample extends PhabricatorUIExample {
public function getName() {
- return 'Javelin Views';
+ return pht('Javelin Views');
}
public function getDescription() {
- return 'Mix and match client and server views.';
+ return pht('Mix and match client and server views.');
}
public function renderExample() {
$request = $this->getRequest();
$init = $request->getStr('init');
$parent_server_template = new JavelinViewExampleServerView();
$parent_client_template = new AphrontJavelinView();
$parent_client_template
->setName('JavelinViewExample')
->setCelerityResource('phabricator-uiexample-javelin-view');
$child_server_template = new JavelinViewExampleServerView();
$child_client_template = new AphrontJavelinView();
$child_client_template
->setName('JavelinViewExample')
->setCelerityResource('phabricator-uiexample-javelin-view');
$parent_server_template->appendChild($parent_client_template);
$parent_client_template->appendChild($child_server_template);
$child_server_template->appendChild($child_client_template);
- $child_client_template->appendChild('Hey, it worked.');
+ $child_client_template->appendChild(pht('Hey, it worked.'));
$panel = new PHUIObjectBoxView();
$panel->setHeaderText(pht('Example'));
$panel->appendChild(
phutil_tag_div('ml', $parent_server_template));
return $panel;
}
}
diff --git a/src/applications/uiexample/examples/PHUIActionHeaderExample.php b/src/applications/uiexample/examples/PHUIActionHeaderExample.php
index 5962871f6..57a9435a9 100644
--- a/src/applications/uiexample/examples/PHUIActionHeaderExample.php
+++ b/src/applications/uiexample/examples/PHUIActionHeaderExample.php
@@ -1,270 +1,270 @@
<?php
final class PHUIActionHeaderExample extends PhabricatorUIExample {
public function getName() {
- return 'Action Headers';
+ return pht('Action Headers');
}
public function getDescription() {
- return 'Various header layouts with and without icons';
+ return pht('Various header layouts with and without icons');
}
public function renderExample() {
/* Colors */
$title1 = id(new PHUIHeaderView())
->setHeader(pht('Header Plain'));
$header1 = id(new PHUIActionHeaderView())
- ->setHeaderTitle('Colorless');
+ ->setHeaderTitle(pht('Colorless'));
$header2 = id(new PHUIActionHeaderView())
- ->setHeaderTitle('Light Grey')
+ ->setHeaderTitle(pht('Light Grey'))
->setHeaderColor(PHUIActionHeaderView::HEADER_GREY);
$header3 = id(new PHUIActionHeaderView())
- ->setHeaderTitle('Light Blue')
+ ->setHeaderTitle(pht('Light Blue'))
->setHeaderColor(PHUIActionHeaderView::HEADER_LIGHTBLUE);
$header4 = id(new PHUIActionHeaderView())
- ->setHeaderTitle('Light Green')
+ ->setHeaderTitle(pht('Light Green'))
->setHeaderColor(PHUIActionHeaderView::HEADER_LIGHTGREEN);
$header5 = id(new PHUIActionHeaderView())
- ->setHeaderTitle('Light Red')
+ ->setHeaderTitle(pht('Light Red'))
->setHeaderColor(PHUIActionHeaderView::HEADER_LIGHTRED);
$header6 = id(new PHUIActionHeaderView())
- ->setHeaderTitle('Light Violet')
+ ->setHeaderTitle(pht('Light Violet'))
->setHeaderColor(PHUIActionHeaderView::HEADER_LIGHTVIOLET);
$layout1 = id(new AphrontMultiColumnView())
->addColumn($header1)
->addColumn($header2)
->addColumn($header3)
->addColumn($header4)
->addColumn($header5)
->addColumn($header6)
->setFluidLayout(true)
->setGutter(AphrontMultiColumnView::GUTTER_SMALL);
$wrap1 = id(new PHUIBoxView())
->appendChild($layout1)
->addMargin(PHUI::MARGIN_LARGE);
-/* Policy Icons */
+ // Policy Icons
$title2 = id(new PHUIHeaderView())
->setHeader(pht('With Icons'));
$header1 = id(new PHUIActionHeaderView())
->setHeaderTitle('Quack')
->setHeaderIcon(
id(new PHUIIconView())
->setIconFont('fa-coffee'));
$header2 = id(new PHUIActionHeaderView())
->setHeaderTitle('Moo')
->setHeaderColor(PHUIActionHeaderView::HEADER_GREY)
->setHeaderIcon(
id(new PHUIIconView())
->setIconFont('fa-magic'));
$header3 = id(new PHUIActionHeaderView())
->setHeaderTitle('Woof')
->setHeaderColor(PHUIActionHeaderView::HEADER_LIGHTBLUE)
->setHeaderIcon(
id(new PHUIIconView())
->setIconFont('fa-fighter-jet'));
$header4 = id(new PHUIActionHeaderView())
->setHeaderTitle('Buzz')
->setHeaderColor(PHUIActionHeaderView::HEADER_LIGHTGREEN)
->setHeaderIcon(
id(new PHUIIconView())
->setIconFont('fa-child'));
$header5 = id(new PHUIActionHeaderView())
->setHeaderTitle('Fizz')
->setHeaderColor(PHUIActionHeaderView::HEADER_LIGHTRED)
->setHeaderIcon(
id(new PHUIIconView())
->setIconFont('fa-car'));
$header6 = id(new PHUIActionHeaderView())
->setHeaderTitle('Blarp')
->setHeaderColor(PHUIActionHeaderView::HEADER_LIGHTVIOLET)
->setHeaderIcon(
id(new PHUIIconView())
->setIconFont('fa-truck'));
$layout2 = id(new AphrontMultiColumnView())
->addColumn($header1)
->addColumn($header2)
->addColumn($header3)
->addColumn($header4)
->addColumn($header5)
->addColumn($header6)
->setFluidLayout(true)
->setGutter(AphrontMultiColumnView::GUTTER_SMALL);
$wrap2 = id(new PHUIBoxView())
->appendChild($layout2)
->addMargin(PHUI::MARGIN_LARGE);
-/* Action Icons */
+ // Action Icons
$title3 = id(new PHUIHeaderView())
->setHeader(pht('With Action Icons'));
$action1 = new PHUIIconView();
$action1->setIconFont('fa-cog');
$action1->setHref('#');
$action2 = new PHUIIconView();
$action2->setIconFont('fa-heart');
$action2->setHref('#');
$action3 = new PHUIIconView();
$action3->setIconFont('fa-tag');
$action3->setHref('#');
$action4 = new PHUIIconView();
$action4->setIconFont('fa-plus');
$action4->setHref('#');
$action5 = new PHUIIconView();
$action5->setIconFont('fa-search');
$action5->setHref('#');
$action6 = new PHUIIconView();
$action6->setIconFont('fa-arrows');
$action6->setHref('#');
$header1 = id(new PHUIActionHeaderView())
- ->setHeaderTitle('Company')
+ ->setHeaderTitle(pht('Company'))
->setHeaderHref('http://example.com/')
->addAction($action1);
$header2 = id(new PHUIActionHeaderView())
- ->setHeaderTitle('Public')
+ ->setHeaderTitle(pht('Public'))
->setHeaderHref('http://example.com/')
->setHeaderColor(PHUIActionHeaderView::HEADER_GREY)
->addAction($action1);
$header3 = id(new PHUIActionHeaderView())
- ->setHeaderTitle('Restricted')
+ ->setHeaderTitle(pht('Restricted'))
->setHeaderHref('http://example.com/')
->setHeaderColor(PHUIActionHeaderView::HEADER_LIGHTBLUE)
->addAction($action2);
$header4 = id(new PHUIActionHeaderView())
->setHeaderTitle('Company')
->setHeaderHref('http://example.com/')
->setHeaderColor(PHUIActionHeaderView::HEADER_LIGHTGREEN)
->addAction($action3);
$header5 = id(new PHUIActionHeaderView())
->setHeaderTitle('Public')
->setHeaderHref('http://example.com/')
->setHeaderColor(PHUIActionHeaderView::HEADER_LIGHTRED)
->addAction($action4)
->addAction($action5);
$header6 = id(new PHUIActionHeaderView())
->setHeaderTitle('Restricted')
->setHeaderHref('http://example.com/')
->setHeaderColor(PHUIActionHeaderView::HEADER_LIGHTVIOLET)
->addAction($action6);
$layout3 = id(new AphrontMultiColumnView())
->addColumn($header1)
->addColumn($header2)
->addColumn($header3)
->addColumn($header4)
->addColumn($header5)
->addColumn($header6)
->setFluidLayout(true)
->setGutter(AphrontMultiColumnView::GUTTER_SMALL);
$wrap3 = id(new PHUIBoxView())
->appendChild($layout3)
->addMargin(PHUI::MARGIN_LARGE);
-/* Action Icons */
+ // Action Icons
$title4 = id(new PHUIHeaderView())
->setHeader(pht('With Tags'));
$tag1 = id(new PHUITagView())
->setType(PHUITagView::TYPE_STATE)
->setBackgroundColor(PHUITagView::COLOR_RED)
- ->setName('Open');
+ ->setName(pht('Open'));
$tag2 = id(new PHUITagView())
->setType(PHUITagView::TYPE_STATE)
->setBackgroundColor(PHUITagView::COLOR_BLUE)
- ->setName('Closed');
+ ->setName(pht('Closed'));
$action1 = new PHUIIconView();
$action1->setIconFont('fa-flag');
$action1->setHref('#');
$header1 = id(new PHUIActionHeaderView())
- ->setHeaderTitle('Company')
+ ->setHeaderTitle(pht('Company'))
->setTag($tag2);
$header2 = id(new PHUIActionHeaderView())
- ->setHeaderTitle('Public')
+ ->setHeaderTitle(pht('Public'))
->setHeaderColor(PHUIActionHeaderView::HEADER_GREY)
->addAction($action1)
->setTag($tag1);
$header3 = id(new PHUIActionHeaderView())
- ->setHeaderTitle('Restricted')
+ ->setHeaderTitle(pht('Restricted'))
->setHeaderColor(PHUIActionHeaderView::HEADER_LIGHTBLUE)
->setTag($tag2);
$header4 = id(new PHUIActionHeaderView())
- ->setHeaderTitle('Company')
+ ->setHeaderTitle(pht('Company'))
->setHeaderColor(PHUIActionHeaderView::HEADER_LIGHTGREEN)
->setTag($tag1);
$header5 = id(new PHUIActionHeaderView())
- ->setHeaderTitle('Public')
+ ->setHeaderTitle(pht('Public'))
->setHeaderColor(PHUIActionHeaderView::HEADER_LIGHTRED)
->setTag($tag2);
$header6 = id(new PHUIActionHeaderView())
- ->setHeaderTitle('Restricted')
+ ->setHeaderTitle(pht('Restricted'))
->setHeaderColor(PHUIActionHeaderView::HEADER_LIGHTVIOLET)
->setTag($tag1);
$layout4 = id(new AphrontMultiColumnView())
->addColumn($header1)
->addColumn($header2)
->addColumn($header3)
->addColumn($header4)
->addColumn($header5)
->addColumn($header6)
->setFluidLayout(true)
->setGutter(AphrontMultiColumnView::GUTTER_SMALL);
$wrap4 = id(new PHUIBoxView())
->appendChild($layout4)
->addMargin(PHUI::MARGIN_LARGE);
return phutil_tag(
'div',
array(),
array(
$title1,
$wrap1,
$title2,
$wrap2,
$title3,
$wrap3,
$title4,
$wrap4,
));
}
}
diff --git a/src/applications/uiexample/examples/PHUIActionPanelExample.php b/src/applications/uiexample/examples/PHUIActionPanelExample.php
index 229cfec4b..cbffbf612 100644
--- a/src/applications/uiexample/examples/PHUIActionPanelExample.php
+++ b/src/applications/uiexample/examples/PHUIActionPanelExample.php
@@ -1,99 +1,99 @@
<?php
final class PHUIActionPanelExample extends PhabricatorUIExample {
public function getName() {
- return 'Action Panel';
+ return pht('Action Panel');
}
public function getDescription() {
- return 'A panel with strong tendencies for inciting ACTION!';
+ return pht('A panel with strong tendencies for inciting ACTION!');
}
public function renderExample() {
$view = id(new AphrontMultiColumnView())
->setFluidLayout(true)
->setBorder(true);
/* Action Panels */
$panel1 = id(new PHUIActionPanelView())
->setFontIcon('fa-book')
->setHeader(pht('Read Documentation'))
->setHref('#')
->setSubHeader(pht('Reading is a common way to learn about things.'))
->setStatus(pht('Carrots help you see better.'))
->setState(PHUIActionPanelView::STATE_NONE);
$view->addColumn($panel1);
$panel2 = id(new PHUIActionPanelView())
->setFontIcon('fa-server')
->setHeader(pht('Launch Instance'))
->setHref('#')
- ->setSubHeader(pht('Maybe this is what you\'re likely here for.'))
+ ->setSubHeader(pht("Maybe this is what you're likely here for."))
->setStatus(pht('You have no instances.'))
->setState(PHUIActionPanelView::STATE_ERROR);
$view->addColumn($panel2);
$panel3 = id(new PHUIActionPanelView())
->setFontIcon('fa-group')
->setHeader(pht('Code with Friends'))
->setHref('#')
->setSubHeader(pht('Writing code is much more fun with friends!'))
->setStatus(pht('You need more friends.'))
->setState(PHUIActionPanelView::STATE_WARN);
$view->addColumn($panel3);
$panel4 = id(new PHUIActionPanelView())
->setFontIcon('fa-cloud-download')
->setHeader(pht('Download Data'))
->setHref('#')
->setSubHeader(pht('Need a backup of all your kitten memes?'))
->setStatus(pht('Building Download'))
->setState(PHUIActionPanelView::STATE_PROGRESS);
$view->addColumn($panel4);
$view2 = id(new AphrontMultiColumnView())
->setFluidLayout(true)
->setBorder(true);
/* Action Panels */
$panel1 = id(new PHUIActionPanelView())
->setFontIcon('fa-credit-card')
->setHeader(pht('Account Balance'))
->setHref('#')
->setSubHeader(pht('You were last billed $2,245.12 on Dec 12, 2014.'))
->setStatus(pht('Account in good standing.'))
->setState(PHUIActionPanelView::STATE_SUCCESS);
$view2->addColumn($panel1);
$panel2 = id(new PHUIActionPanelView())
->setBigText('148')
->setHeader(pht('Instance Users'))
->setHref('#')
->setSubHeader(
pht('You currently have 140 active and 8 inactive accounts'));
$view2->addColumn($panel2);
$panel3 = id(new PHUIActionPanelView())
->setBigText('March 12')
->setHeader(pht('Next Maintenance Window'))
->setHref('#')
->setSubHeader(
pht('At 6:00 am PST, Phacility will conduct weekly maintenence.'))
->setStatus(pht('Very Important!'))
->setState(PHUIActionPanelView::STATE_ERROR);
$view2->addColumn($panel3);
$panel4 = id(new PHUIActionPanelView())
->setBigText('1,113,377')
->setHeader(pht('Lines of Code'))
->setHref('#')
->setSubHeader(pht('Your team has reviewed lots of code!'));
$view2->addColumn($panel4);
$view = phutil_tag_div('mlb', $view);
return phutil_tag_div('ml', array($view, $view2));
}
}
diff --git a/src/applications/uiexample/examples/PHUIBoxExample.php b/src/applications/uiexample/examples/PHUIBoxExample.php
index a8999f5cf..1f372c4cd 100644
--- a/src/applications/uiexample/examples/PHUIBoxExample.php
+++ b/src/applications/uiexample/examples/PHUIBoxExample.php
@@ -1,121 +1,121 @@
<?php
final class PHUIBoxExample extends PhabricatorUIExample {
public function getName() {
- return 'Box';
+ return pht('Box');
}
public function getDescription() {
- return 'It\'s a fancy or non-fancy box. Put stuff in it.';
+ return pht("It's a fancy or non-fancy box. Put stuff in it.");
}
public function renderExample() {
$content1 = 'Asmund and Signy';
$content2 = 'The Cottager and his Cat';
- $content3 = 'Geirlug The King\'s Daughter';
+ $content3 = "Geirlug The King's Daughter";
$layout1 =
array(
id(new PHUIBoxView())
->appendChild($content1),
id(new PHUIBoxView())
->appendChild($content2),
id(new PHUIBoxView())
->appendChild($content3),
);
$layout2 =
array(
id(new PHUIBoxView())
->appendChild($content1)
->addMargin(PHUI::MARGIN_SMALL_LEFT),
id(new PHUIBoxView())
->appendChild($content2)
->addMargin(PHUI::MARGIN_MEDIUM_LEFT)
->addMargin(PHUI::MARGIN_MEDIUM_TOP),
id(new PHUIBoxView())
->appendChild($content3)
->addMargin(PHUI::MARGIN_LARGE_LEFT)
->addMargin(PHUI::MARGIN_LARGE_TOP),
);
$layout3 =
array(
id(new PHUIBoxView())
->appendChild($content1)
->setBorder(true)
->addPadding(PHUI::PADDING_SMALL)
->addMargin(PHUI::MARGIN_LARGE_BOTTOM),
id(new PHUIBoxView())
->appendChild($content2)
->setBorder(true)
->addPadding(PHUI::PADDING_MEDIUM)
->addMargin(PHUI::MARGIN_LARGE_BOTTOM),
id(new PHUIBoxView())
->appendChild($content3)
->setBorder(true)
->addPadding(PHUI::PADDING_LARGE)
->addMargin(PHUI::MARGIN_LARGE_BOTTOM),
);
$image = id(new PHUIIconView())
->setIconFont('fa-heart');
$button = id(new PHUIButtonView())
->setTag('a')
->setColor(PHUIButtonView::SIMPLE)
->setIcon($image)
- ->setText('Such Wow')
+ ->setText(pht('Such Wow'))
->addClass(PHUI::MARGIN_SMALL_RIGHT);
$header = id(new PHUIHeaderView())
- ->setHeader('Fancy Box')
+ ->setHeader(pht('Fancy Box'))
->addActionLink($button);
$obj4 = id(new PHUIObjectBoxView())
->setHeader($header)
->appendChild(id(new PHUIBoxView())
->addPadding(PHUI::PADDING_MEDIUM)
- ->appendChild('Such Fancy, Nice Box, Many Corners.'));
+ ->appendChild(pht('Such Fancy, Nice Box, Many Corners.')));
$head1 = id(new PHUIHeaderView())
->setHeader(pht('Plain Box'));
$head2 = id(new PHUIHeaderView())
->setHeader(pht('Plain Box with space'));
$head3 = id(new PHUIHeaderView())
->setHeader(pht('Border Box with space'));
$head4 = id(new PHUIHeaderView())
->setHeader(pht('PHUIObjectBoxView'));
$wrap1 = id(new PHUIBoxView())
->appendChild($layout1)
->addMargin(PHUI::MARGIN_LARGE);
$wrap2 = id(new PHUIBoxView())
->appendChild($layout2)
->addMargin(PHUI::MARGIN_LARGE);
$wrap3 = id(new PHUIBoxView())
->appendChild($layout3)
->addMargin(PHUI::MARGIN_LARGE);
return phutil_tag(
'div',
array(),
array(
$head1,
$wrap1,
$head2,
$wrap2,
$head3,
$wrap3,
$head4,
$obj4,
));
}
}
diff --git a/src/applications/uiexample/examples/PHUIButtonBarExample.php b/src/applications/uiexample/examples/PHUIButtonBarExample.php
index 293cd2336..8e00a9943 100644
--- a/src/applications/uiexample/examples/PHUIButtonBarExample.php
+++ b/src/applications/uiexample/examples/PHUIButtonBarExample.php
@@ -1,84 +1,84 @@
<?php
final class PHUIButtonBarExample extends PhabricatorUIExample {
public function getName() {
return pht('Button Bar');
}
public function getDescription() {
return pht('A minimal UI for Buttons');
}
public function renderExample() {
$request = $this->getRequest();
$user = $request->getUser();
// Icon Buttons
$icons = array(
'Go Back' => 'fa-chevron-left bluegrey',
'Choose Date' => 'fa-calendar bluegrey',
'Edit View' => 'fa-pencil bluegrey',
'Go Forward' => 'fa-chevron-right bluegrey',
);
$button_bar1 = new PHUIButtonBarView();
foreach ($icons as $text => $icon) {
$image = id(new PHUIIconView())
->setIconFont($icon);
$button = id(new PHUIButtonView())
->setTag('a')
->setColor(PHUIButtonView::GREY)
->setTitle($text)
->setIcon($image);
$button_bar1->addButton($button);
}
$button_bar2 = new PHUIButtonBarView();
foreach ($icons as $text => $icon) {
$image = id(new PHUIIconView())
->setIconFont($icon);
$button = id(new PHUIButtonView())
->setTag('a')
->setColor(PHUIButtonView::SIMPLE)
->setTitle($text)
->setText($text);
$button_bar2->addButton($button);
}
$button_bar3 = new PHUIButtonBarView();
foreach ($icons as $text => $icon) {
$image = id(new PHUIIconView())
->setIconFont($icon);
$button = id(new PHUIButtonView())
->setTag('a')
->setColor(PHUIButtonView::SIMPLE)
->setTitle($text)
->setTooltip($text)
->setIcon($image);
$button_bar3->addButton($button);
}
$layout1 = id(new PHUIBoxView())
->appendChild($button_bar1)
->addClass('ml');
$layout2 = id(new PHUIBoxView())
->appendChild($button_bar2)
->addClass('mlr mll mlb');
$layout3 = id(new PHUIBoxView())
->appendChild($button_bar3)
->addClass('mlr mll mlb');
$wrap1 = id(new PHUIObjectBoxView())
- ->setHeaderText('Button Bar Example')
+ ->setHeaderText(pht('Button Bar Example'))
->appendChild($layout1)
->appendChild($layout2)
->appendChild($layout3);
return array($wrap1);
}
}
diff --git a/src/applications/uiexample/examples/PHUIButtonExample.php b/src/applications/uiexample/examples/PHUIButtonExample.php
index 218e97e95..216a4a440 100644
--- a/src/applications/uiexample/examples/PHUIButtonExample.php
+++ b/src/applications/uiexample/examples/PHUIButtonExample.php
@@ -1,222 +1,233 @@
<?php
final class PHUIButtonExample extends PhabricatorUIExample {
public function getName() {
- return 'Buttons';
+ return pht('Buttons');
}
public function getDescription() {
- return hsprintf('Use <tt>&lt;button&gt;</tt> to render buttons.');
+ return pht(
+ 'Use %s to render buttons.',
+ phutil_tag('tt', array(), '&lt;button&gt;'));
}
public function renderExample() {
$request = $this->getRequest();
$user = $request->getUser();
$colors = array('', 'green', 'grey', 'disabled');
$sizes = array('', 'small');
$tags = array('a', 'button');
// phutil_tag
$column = array();
foreach ($tags as $tag) {
foreach ($colors as $color) {
foreach ($sizes as $key => $size) {
$class = implode(' ', array($color, $size));
if ($tag == 'a') {
$class .= ' button';
}
$column[$key][] = phutil_tag(
$tag,
array(
'class' => $class,
),
phutil_utf8_ucwords($size.' '.$color.' '.$tag));
$column[$key][] = hsprintf('<br /><br />');
}
}
}
$column3 = array();
foreach ($colors as $color) {
$caret = phutil_tag('span', array('class' => 'caret'), '');
$column3[] = phutil_tag(
'a',
array(
'class' => $color.' button dropdown',
),
array(
phutil_utf8_ucwords($color.' Dropdown'),
$caret,
));
$column3[] = hsprintf('<br /><br />');
}
$layout1 = id(new AphrontMultiColumnView())
->addColumn($column[0])
->addColumn($column[1])
->addColumn($column3)
->setFluidLayout(true)
->setGutter(AphrontMultiColumnView::GUTTER_MEDIUM);
// PHUIButtonView
$colors = array(null,
PHUIButtonView::GREEN,
PHUIButtonView::GREY,
PHUIButtonView::DISABLED,
);
$sizes = array(null, PHUIButtonView::SMALL);
$column = array();
foreach ($colors as $color) {
foreach ($sizes as $key => $size) {
$column[$key][] = id(new PHUIButtonView())
->setColor($color)
->setSize($size)
->setTag('a')
- ->setText('Clicky');
+ ->setText(pht('Clicky'));
$column[$key][] = hsprintf('<br /><br />');
}
}
foreach ($colors as $color) {
$column[2][] = id(new PHUIButtonView())
->setColor($color)
->setTag('button')
- ->setText('Button')
+ ->setText(pht('Button'))
->setDropdown(true);
$column[2][] = hsprintf('<br /><br />');
}
$layout2 = id(new AphrontMultiColumnView())
->addColumn($column[0])
->addColumn($column[1])
->addColumn($column[2])
->setFluidLayout(true)
->setGutter(AphrontMultiColumnView::GUTTER_MEDIUM);
// Icon Buttons
$column = array();
$icons = array(
'Comment' => 'fa-comment',
'Give Token' => 'fa-trophy',
'Reverse Time' => 'fa-clock-o',
'Implode Earth' => 'fa-exclamation-triangle red',
);
foreach ($icons as $text => $icon) {
$image = id(new PHUIIconView())
- ->setIconFont($icon);
+ ->setIconFont($icon);
$column[] = id(new PHUIButtonView())
->setTag('a')
->setColor(PHUIButtonView::GREY)
->setIcon($image)
->setText($text)
->addClass(PHUI::MARGIN_SMALL_RIGHT);
}
$layout3 = id(new AphrontMultiColumnView())
->addColumn($column)
->setFluidLayout(true)
->setGutter(AphrontMultiColumnView::GUTTER_MEDIUM);
$icons = array(
'Subscribe' => 'fa-check-circle bluegrey',
'Edit' => 'fa-pencil bluegrey',
);
$colors = array(
PHUIButtonView::SIMPLE,
PHUIButtonView::SIMPLE_YELLOW,
PHUIButtonView::SIMPLE_GREY,
PHUIButtonView::SIMPLE_BLUE,
);
$column = array();
foreach ($colors as $color) {
foreach ($icons as $text => $icon) {
$image = id(new PHUIIconView())
->setIconFont($icon);
$column[] = id(new PHUIButtonView())
->setTag('a')
->setColor($color)
->setIcon($image)
->setText($text)
->addClass(PHUI::MARGIN_SMALL_RIGHT);
}
}
$layout4 = id(new AphrontMultiColumnView())
->addColumn($column)
->setFluidLayout(true)
->setGutter(AphrontMultiColumnView::GUTTER_MEDIUM);
// Baby Got Back Buttons
$column = array();
$icons = array('Asana', 'Github', 'Facebook', 'Google', 'LDAP');
foreach ($icons as $icon) {
$image = id(new PHUIIconView())
- ->setSpriteSheet(PHUIIconView::SPRITE_LOGIN)
- ->setSpriteIcon($icon);
+ ->setSpriteSheet(PHUIIconView::SPRITE_LOGIN)
+ ->setSpriteIcon($icon);
$column[] = id(new PHUIButtonView())
->setTag('a')
->setSize(PHUIButtonView::BIG)
->setColor(PHUIButtonView::GREY)
->setIcon($image)
- ->setText('Login or Register')
+ ->setText(pht('Login or Register'))
->setSubtext($icon)
->addClass(PHUI::MARGIN_MEDIUM_RIGHT);
}
$layout5 = id(new AphrontMultiColumnView())
->addColumn($column)
->setFluidLayout(true)
->setGutter(AphrontMultiColumnView::GUTTER_MEDIUM);
// Set it and forget it
$head1 = id(new PHUIHeaderView())
->setHeader('phutil_tag');
$head2 = id(new PHUIHeaderView())
->setHeader('PHUIButtonView');
$head3 = id(new PHUIHeaderView())
- ->setHeader('Icon Buttons');
+ ->setHeader(pht('Icon Buttons'));
$head4 = id(new PHUIHeaderView())
- ->setHeader('Simple Buttons');
+ ->setHeader(pht('Simple Buttons'));
$head5 = id(new PHUIHeaderView())
- ->setHeader('Big Icon Buttons');
+ ->setHeader(pht('Big Icon Buttons'));
$wrap1 = id(new PHUIBoxView())
->appendChild($layout1)
->addMargin(PHUI::MARGIN_LARGE);
$wrap2 = id(new PHUIBoxView())
->appendChild($layout2)
->addMargin(PHUI::MARGIN_LARGE);
$wrap3 = id(new PHUIBoxView())
->appendChild($layout3)
->addMargin(PHUI::MARGIN_LARGE);
$wrap4 = id(new PHUIBoxView())
->appendChild($layout4)
->addMargin(PHUI::MARGIN_LARGE);
$wrap5 = id(new PHUIBoxView())
->appendChild($layout5)
->addMargin(PHUI::MARGIN_LARGE);
- return array($head1, $wrap1, $head2, $wrap2, $head3, $wrap3,
- $head4, $wrap4, $head5, $wrap5,
+ return array(
+ $head1,
+ $wrap1,
+ $head2,
+ $wrap2,
+ $head3,
+ $wrap3,
+ $head4,
+ $wrap4,
+ $head5,
+ $wrap5,
);
}
}
diff --git a/src/applications/uiexample/examples/PHUIColorPalletteExample.php b/src/applications/uiexample/examples/PHUIColorPalletteExample.php
index 08459d60b..0e3193d24 100644
--- a/src/applications/uiexample/examples/PHUIColorPalletteExample.php
+++ b/src/applications/uiexample/examples/PHUIColorPalletteExample.php
@@ -1,125 +1,125 @@
<?php
final class PHUIColorPalletteExample extends PhabricatorUIExample {
public function getName() {
- return 'Colors';
+ return pht('Colors');
}
public function getDescription() {
- return 'A Standard Palette of Colors for use.';
+ return pht('A Standard Palette of Colors for use.');
}
public function renderExample() {
$colors = array(
'c0392b' => 'Base Red {$red}',
'f4dddb' => '83% Red {$lightred}',
'e67e22' => 'Base Orange {$orange}',
'f7e2d4' => '83% Orange {$lightorange}',
'f1c40f' => 'Base Yellow {$yellow}',
'fdf5d4' => '83% Yellow {$lightyellow}',
'139543' => 'Base Green {$green}',
'd7eddf' => '83% Green {$lightgreen}',
'2980b9' => 'Base Blue {$blue}',
'daeaf3' => '83% Blue {$lightblue}',
'3498db' => 'Sky Base {$sky}',
'ddeef9' => '83% Sky {$lightsky}',
'6e5cb6' => 'Base Indigo {$indigo}',
'eae6f7' => '83% Indigo {$lightindigo}',
'da49be' => 'Base Pink {$pink}',
'fbeaf8' => '83% Pink {$lightpink}',
'8e44ad' => 'Base Violet {$violet}',
'ecdff1' => '83% Violet {$lightviolet}',
);
$greys = array(
'C7CCD9' => 'Light Grey Border {$lightgreyborder}',
'A1A6B0' => 'Grey Border {$greyborder}',
'676A70' => 'Dark Grey Border {$darkgreyborder}',
'92969D' => 'Light Grey Text {$lightgreytext}',
'74777D' => 'Grey Text {$greytext}',
'4B4D51' => 'Dark Grey Text {$darkgreytext}',
'F7F7F7' => 'Light Grey Background {$lightgreybackground}',
'EBECEE' => 'Grey Background {$greybackground}',
'DFE0E2' => 'Dark Grey Background {$darkgreybackground}',
);
$blues = array(
'DDE8EF' => 'Thin Blue Border {$thinblueborder}',
'BFCFDA' => 'Light Blue Border {$lightblueborder}',
'95A6C5' => 'Blue Border {$blueborder}',
'626E82' => 'Dark Blue Border {$darkblueborder}',
'F8F9FC' => 'Light Blue Background {$lightbluebackground}',
'DAE7FF' => 'Blue Background {$bluebackground}',
'8C98B8' => 'Light Blue Text {$lightbluetext}',
'6B748C' => 'Blue Text {$bluetext}',
'464C5C' => 'Dark Blue Text {$darkbluetext}',
);
$d_column = array();
foreach ($greys as $color => $name) {
$d_column[] = phutil_tag(
'div',
array(
'style' => 'background-color: #'.$color.';',
'class' => 'pl',
),
$name.' #'.$color);
}
$b_column = array();
foreach ($blues as $color => $name) {
$b_column[] = phutil_tag(
'div',
array(
'style' => 'background-color: #'.$color.';',
'class' => 'pl',
),
$name.' #'.$color);
}
$c_column = array();
$url = array();
foreach ($colors as $color => $name) {
$url[] = $color;
$c_column[] = phutil_tag(
'div',
array(
'style' => 'background-color: #'.$color.';',
'class' => 'pl',
),
$name.' #'.$color);
}
$color_url = phutil_tag(
'a',
array(
'href' => 'http://color.hailpixel.com/#'.implode(',', $url),
'class' => 'button grey mlb',
),
- 'Color Palette');
+ pht('Color Palette'));
$wrap1 = id(new PHUIObjectBoxView())
->setHeaderText(pht('Greys'))
->appendChild($d_column);
$wrap2 = id(new PHUIObjectBoxView())
->setHeaderText(pht('Blues'))
->appendChild($b_column);
$wrap3 = id(new PHUIObjectBoxView())
->setHeaderText(pht('Colors'))
->appendChild($c_column);
return phutil_tag(
'div',
array(),
array(
$wrap1,
$wrap2,
$wrap3,
));
}
}
diff --git a/src/applications/uiexample/examples/PHUIDocumentExample.php b/src/applications/uiexample/examples/PHUIDocumentExample.php
index f0a77e354..6fc656073 100644
--- a/src/applications/uiexample/examples/PHUIDocumentExample.php
+++ b/src/applications/uiexample/examples/PHUIDocumentExample.php
@@ -1,200 +1,200 @@
<?php
final class PHUIDocumentExample extends PhabricatorUIExample {
public function getName() {
return pht('Document View');
}
public function getDescription() {
return pht('Useful for areas of large content navigation');
}
public function renderExample() {
$request = $this->getRequest();
$user = $request->getUser();
$action = id(new PHUIListItemView())
- ->setName('Actions')
+ ->setName(pht('Actions'))
->setType(PHUIListItemView::TYPE_LABEL);
$action1 = id(new PHUIListItemView())
- ->setName('Edit Document')
+ ->setName(pht('Edit Document'))
->setHref('#')
->setIcon('fa-edit')
->setType(PHUIListItemView::TYPE_LINK);
$action2 = id(new PHUIListItemView())
- ->setName('Move Document')
+ ->setName(pht('Move Document'))
->setHref('#')
->setIcon('fa-arrows')
->setType(PHUIListItemView::TYPE_LINK);
$action3 = id(new PHUIListItemView())
- ->setName('Delete Document')
+ ->setName(pht('Delete Document'))
->setHref('#')
->setIcon('fa-times')
->setType(PHUIListItemView::TYPE_LINK);
$action4 = id(new PHUIListItemView())
- ->setName('View History')
+ ->setName(pht('View History'))
->setHref('#')
->setIcon('fa-list')
->setType(PHUIListItemView::TYPE_LINK);
$action5 = id(new PHUIListItemView())
- ->setName('Subscribe')
+ ->setName(pht('Subscribe'))
->setHref('#')
->setIcon('fa-plus-circle')
->setType(PHUIListItemView::TYPE_LINK);
$divider = id(new PHUIListItemView())
->setType(PHUIListItemView::TYPE_DIVIDER);
$header = id(new PHUIHeaderView())
- ->setHeader('Installation');
+ ->setHeader(pht('Installation'));
$label1 = id(new PHUIListItemView())
- ->setName('Getting Started')
+ ->setName(pht('Getting Started'))
->setType(PHUIListItemView::TYPE_LABEL);
$label2 = id(new PHUIListItemView())
- ->setName('Documentation')
+ ->setName(pht('Documentation'))
->setType(PHUIListItemView::TYPE_LABEL);
$item1 = id(new PHUIListItemView())
- ->setName('Installation')
+ ->setName(pht('Installation'))
->setHref('#')
->setType(PHUIListItemView::TYPE_LINK);
$item2 = id(new PHUIListItemView())
- ->setName('Webserver Config')
+ ->setName(pht('Webserver Config'))
->setHref('#')
->setType(PHUIListItemView::TYPE_LINK);
$item3 = id(new PHUIListItemView())
- ->setName('Adding Users')
+ ->setName(pht('Adding Users'))
->setHref('#')
->setType(PHUIListItemView::TYPE_LINK);
$item4 = id(new PHUIListItemView())
- ->setName('Debugging')
+ ->setName(pht('Debugging'))
->setHref('#')
->setType(PHUIListItemView::TYPE_LINK);
$sidenav = id(new PHUIListView())
->setType(PHUIListView::SIDENAV_LIST)
->addMenuItem($action)
->addMenuItem($action1)
->addMenuItem($action2)
->addMenuItem($action3)
->addMenuItem($action4)
->addMenuItem($action5)
->addMenuItem($divider)
->addMenuItem($label1)
->addMenuItem($item1)
->addMenuItem($item2)
->addMenuItem($item3)
->addMenuItem($item4)
->addMenuItem($label2)
->addMenuItem($item2)
->addMenuItem($item3)
->addMenuItem($item4)
->addMenuItem($item1);
$home = id(new PHUIListItemView())
->setIcon('fa-home')
->setHref('#')
->setType(PHUIListItemView::TYPE_ICON);
$item1 = id(new PHUIListItemView())
- ->setName('Installation')
+ ->setName(pht('Installation'))
->setHref('#')
->setSelected(true)
->setType(PHUIListItemView::TYPE_LINK);
$item2 = id(new PHUIListItemView())
- ->setName('Webserver Config')
+ ->setName(pht('Webserver Config'))
->setHref('#')
->setType(PHUIListItemView::TYPE_LINK);
$item3 = id(new PHUIListItemView())
- ->setName('Adding Users')
+ ->setName(pht('Adding Users'))
->setHref('#')
->setType(PHUIListItemView::TYPE_LINK);
$item4 = id(new PHUIListItemView())
- ->setName('Debugging')
+ ->setName(pht('Debugging'))
->setHref('#')
->setType(PHUIListItemView::TYPE_LINK);
$topnav = id(new PHUIListView())
->setType(PHUIListView::NAVBAR_LIST)
->addMenuItem($home)
->addMenuItem($item1)
->addMenuItem($item2)
->addMenuItem($item3)
->addMenuItem($item4);
$document = hsprintf(
'<p class="pl">Lorem ipsum dolor sit amet, consectetur adipisicing, '.
'sed do eiusmod tempor incididunt ut labore et dolore magna '.
'aliqua. Ut enim ad minim veniam, quis nostrud exercitation '.
'ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis '.
'aute irure dolor in reprehenderit in voluptate velit esse cillum '.
'dolore eu fugiat nulla pariatur. Excepteur sint occaecat '.
'cupidatat non proident, sunt in culpa qui officia deserunt '.
'mollit anim id est laborum.</p>'.
'<p class="plr pll plb">Lorem ipsum dolor sit amet, consectetur, '.
'sed do eiusmod tempor incididunt ut labore et dolore magna '.
'aliqua. Ut enim ad minim veniam, quis nostrud exercitation '.
'ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis '.
'aute irure dolor in reprehenderit in voluptate velit esse cillum '.
'dolore eu fugiat nulla pariatur. Excepteur sint occaecat '.
'cupidatat non proident, sunt in culpa qui officia deserunt '.
'mollit anim id est laborum.</p>'.
'<p class="plr pll plb">Lorem ipsum dolor sit amet, consectetur, '.
'sed do eiusmod tempor incididunt ut labore et dolore magna '.
'aliqua. Ut enim ad minim veniam, quis nostrud exercitation '.
'ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis '.
'aute irure dolor in reprehenderit in voluptate velit esse cillum '.
'dolore eu fugiat nulla pariatur. Excepteur sint occaecat '.
'cupidatat non proident, sunt in culpa qui officia deserunt '.
'mollit anim id est laborum.</p>'.
'<p class="plr pll plb">Lorem ipsum dolor sit amet, consectetur, '.
'sed do eiusmod tempor incididunt ut labore et dolore magna '.
'aliqua. Ut enim ad minim veniam, quis nostrud exercitation '.
'ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis '.
'aute irure dolor in reprehenderit in voluptate velit esse cillum '.
'dolore eu fugiat nulla pariatur. Excepteur sint occaecat '.
'cupidatat non proident, sunt in culpa qui officia deserunt '.
'mollit anim id est laborum.</p>'.
'<p class="plr pll plb">Lorem ipsum dolor sit amet, consectetur, '.
'sed do eiusmod tempor incididunt ut labore et dolore magna '.
'aliqua. Ut enim ad minim veniam, quis nostrud exercitation '.
'ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis '.
'aute irure dolor in reprehenderit in voluptate velit esse cillum '.
'dolore eu fugiat nulla pariatur. Excepteur sint occaecat '.
'cupidatat non proident, sunt in culpa qui officia deserunt '.
'mollit anim id est laborum.</p>'.
'<p class="plr pll plb">Lorem ipsum dolor sit amet, consectetur, '.
'sed do eiusmod tempor incididunt ut labore et dolore magna '.
'aliqua. Ut enim ad minim veniam, quis nostrud exercitation '.
'ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis '.
'aute irure dolor in reprehenderit in voluptate velit esse cillum '.
'dolore eu fugiat nulla pariatur. Excepteur sint occaecat '.
'cupidatat non proident, sunt in culpa qui officia deserunt '.
'mollit anim id est laborum.</p>');
$content = new PHUIDocumentView();
- $content->setBook('Book or Project Name', 'Article');
+ $content->setBook(pht('Book or Project Name'), pht('Article'));
$content->setHeader($header);
$content->setFluid(true);
$content->setTopNav($topnav);
$content->setSidenav($sidenav);
$content->appendChild($document);
$content->setFontKit(PHUIDocumentView::FONT_SOURCE_SANS);
return $content;
}
}
diff --git a/src/applications/uiexample/examples/PHUIFeedStoryExample.php b/src/applications/uiexample/examples/PHUIFeedStoryExample.php
index 2a896ec09..6731f1ce1 100644
--- a/src/applications/uiexample/examples/PHUIFeedStoryExample.php
+++ b/src/applications/uiexample/examples/PHUIFeedStoryExample.php
@@ -1,217 +1,223 @@
<?php
final class PHUIFeedStoryExample extends PhabricatorUIExample {
public function getName() {
- return 'Feed Story';
+ return pht('Feed Story');
}
public function getDescription() {
- return 'An outlandish exaggeration of intricate tales from '.
- 'around the realm';
+ return pht(
+ 'An outlandish exaggeration of intricate tales from around the realm');
}
public function renderExample() {
$request = $this->getRequest();
$user = $request->getUser();
/* Basic Story */
$text = hsprintf(
'<strong><a>harding (Tom Harding)</a></strong> closed <a>'.
'D12: New spacer classes for blog views</a>.');
$story1 = id(new PHUIFeedStoryView())
->setTitle($text)
->setImage(celerity_get_resource_uri('/rsrc/image/people/harding.png'))
->setImageHref('http://en.wikipedia.org/wiki/Warren_G._Harding')
->setEpoch(1)
->setAppIcon('fa-star')
->setUser($user);
/* Text Story, useful in Blogs, Ponders, Status */
$tokens = array(
'like-1',
'like-2',
'heart-1',
'heart-2',
);
$tokenview = array();
foreach ($tokens as $token) {
$tokenview[] =
id(new PHUIIconView())
->setSpriteSheet(PHUIIconView::SPRITE_TOKENS)
->setSpriteIcon($token);
}
- $text = hsprintf('<strong><a>lincoln (Honest Abe)</a></strong> wrote a '.
+ $text = hsprintf(
+ '<strong><a>lincoln (Honest Abe)</a></strong> wrote a '.
'new blog post.');
$story2 = id(new PHUIFeedStoryView())
->setTitle($text)
->setImage(celerity_get_resource_uri('/rsrc/image/people/lincoln.png'))
->setImageHref('http://en.wikipedia.org/wiki/Abraham_Lincoln')
->setEpoch(strtotime('November 19, 1863'))
->setAppIcon('fa-star')
->setUser($user)
->setTokenBar($tokenview)
- ->setPontification('Four score and seven years ago our fathers brought '.
+ ->setPontification(
+ 'Four score and seven years ago our fathers brought '.
'forth on this continent, a new nation, conceived in Liberty, and '.
'dedicated to the proposition that all men are created equal. '.
'Now we are engaged in a great civil war, testing whether that '.
'nation, or any nation so conceived and so dedicated, can long '.
'endure. We are met on a great battle-field of that war. We have '.
'come to dedicate a portion of that field, as a final resting '.
'place for those who here gave their lives that that nation might '.
'live. It is altogether fitting and proper that we should do this.',
'Gettysburg Address');
/* Action Story, let's give people tokens! */
- $text = hsprintf('<strong><a>harding (Tom Harding)</a></strong> awarded '.
+ $text = hsprintf(
+ '<strong><a>harding (Tom Harding)</a></strong> awarded '.
'<a>M10: Workboards</a> a token.');
$action1 = id(new PHUIIconView())
->setIconFont('fa-trophy bluegrey')
->setHref('#');
$token =
id(new PHUIIconView())
->setSpriteSheet(PHUIIconView::SPRITE_TOKENS)
->setSpriteIcon('like-1');
$story3 = id(new PHUIFeedStoryView())
->setTitle($text)
->setImage(celerity_get_resource_uri('/rsrc/image/people/harding.png'))
->setImageHref('http://en.wikipedia.org/wiki/Warren_G._Harding')
->appendChild($token)
->setEpoch(1)
->addAction($action1)
->setAppIcon('fa-trophy')
->setUser($user);
/* Image Story, used in Pholio, Macro */
- $text = hsprintf('<strong><a>wgharding (Warren Harding)</a></strong> '.
+ $text = hsprintf(
+ '<strong><a>wgharding (Warren Harding)</a></strong> '.
'asked a new question.');
$action1 = id(new PHUIIconView())
->setIconFont('fa-chevron-up bluegrey')
->setHref('#');
$action2 = id(new PHUIIconView())
->setIconFont('fa-chevron-down bluegrey')
->setHref('#');
$story4 = id(new PHUIFeedStoryView())
->setTitle($text)
->setImage(celerity_get_resource_uri('/rsrc/image/people/harding.png'))
->setImageHref('http://en.wikipedia.org/wiki/Warren_G._Harding')
->setEpoch(1)
->setAppIcon('fa-cogs')
- ->setPontification('Why does inline-block add space under my spans and '.
- 'anchors?')
+ ->setPontification(
+ 'Why does inline-block add space under my spans and anchors?')
->addAction($action1)
->addAction($action2)
->setUser($user);
/* Text Story, useful in Blogs, Ponders, Status */
- $text = hsprintf('<strong><a>lincoln (Honest Abe)</a></strong> updated '.
+ $text = hsprintf(
+ '<strong><a>lincoln (Honest Abe)</a></strong> updated '.
'his status.');
$story5 = id(new PHUIFeedStoryView())
->setTitle($text)
->setImage(celerity_get_resource_uri('/rsrc/image/people/lincoln.png'))
->setImageHref('http://en.wikipedia.org/wiki/Abraham_Lincoln')
->setEpoch(strtotime('November 19, 1863'))
->setAppIcon('fa-rocket')
->setUser($user)
- ->setPontification('If we ever create a lightweight status app '.
- 'this story would be how that would be displayed.');
+ ->setPontification(
+ 'If we ever create a lightweight status app '.
+ 'this story would be how that would be displayed.');
/* Basic "One Line" Story */
$text = hsprintf(
'<strong><a>harding (Tom Harding)</a></strong> updated <a>'.
'D12: New spacer classes for blog views</a>.');
$story6 = id(new PHUIFeedStoryView())
->setTitle($text)
->setImage(celerity_get_resource_uri('/rsrc/image/people/harding.png'))
->setImageHref('http://en.wikipedia.org/wiki/Warren_G._Harding')
->setEpoch(1)
->setAppIcon('fa-wifi')
->setUser($user);
$head1 = id(new PHUIHeaderView())
->setHeader(pht('Basic Story'));
$head2 = id(new PHUIHeaderView())
->setHeader(pht('Title / Text Story'));
$head3 = id(new PHUIHeaderView())
->setHeader(pht('Token Story'));
$head4 = id(new PHUIHeaderView())
->setHeader(pht('Action Story'));
$head5 = id(new PHUIHeaderView())
->setHeader(pht('Status Story'));
$head6 = id(new PHUIHeaderView())
->setHeader(pht('One Line Story'));
$wrap1 =
array(
id(new PHUIBoxView())
->appendChild($story1)
->addMargin(PHUI::MARGIN_MEDIUM)
->addPadding(PHUI::PADDING_SMALL),
);
$wrap2 =
array(
id(new PHUIBoxView())
->appendChild($story2)
->addMargin(PHUI::MARGIN_MEDIUM)
->addPadding(PHUI::PADDING_SMALL),
);
$wrap3 =
array(
id(new PHUIBoxView())
->appendChild($story3)
->addMargin(PHUI::MARGIN_MEDIUM)
->addPadding(PHUI::PADDING_SMALL),
);
$wrap4 =
array(
id(new PHUIBoxView())
->appendChild($story4)
->addMargin(PHUI::MARGIN_MEDIUM)
->addPadding(PHUI::PADDING_SMALL),
);
$wrap5 =
array(
id(new PHUIBoxView())
->appendChild($story5)
->addMargin(PHUI::MARGIN_MEDIUM)
->addPadding(PHUI::PADDING_SMALL),
);
$wrap6 =
array(
id(new PHUIBoxView())
->appendChild($story6)
->addMargin(PHUI::MARGIN_MEDIUM)
->addPadding(PHUI::PADDING_SMALL),
);
return phutil_tag(
'div',
array(),
array(
$head1,
$wrap1,
$head2,
$wrap2,
$head3,
$wrap3,
$head4,
$wrap4,
$head5,
$wrap5,
$head6,
$wrap6,
));
}
}
diff --git a/src/applications/uiexample/examples/PHUIIconExample.php b/src/applications/uiexample/examples/PHUIIconExample.php
index 32259c1ce..2644672a1 100644
--- a/src/applications/uiexample/examples/PHUIIconExample.php
+++ b/src/applications/uiexample/examples/PHUIIconExample.php
@@ -1,178 +1,178 @@
<?php
final class PHUIIconExample extends PhabricatorUIExample {
public function getName() {
- return 'Icons and Images';
+ return pht('Icons and Images');
}
public function getDescription() {
- return 'Easily render icons or images with links and sprites.';
+ return pht('Easily render icons or images with links and sprites.');
}
private function listTransforms() {
return array(
'ph-rotate-90',
'ph-rotate-180',
'ph-rotate-270',
'ph-flip-horizontal',
'ph-flip-vertical',
'ph-spin',
);
}
public function renderExample() {
$colors = PHUIIconView::getFontIconColors();
$colors = array_merge(array(null), $colors);
$fas = PHUIIconView::getFontIcons();
$trans = $this->listTransforms();
$cicons = array();
foreach ($colors as $color) {
$cicons[] = id(new PHUIIconView())
->addClass('phui-example-icon-transform')
->setIconFont('fa-tag '.$color)
->setText(pht('fa-tag %s', $color));
}
$ficons = array();
sort($fas);
foreach ($fas as $fa) {
$ficons[] = id(new PHUIIconView())
->addClass('phui-example-icon-name')
->setIconFont($fa)
->setText($fa);
}
$person1 = new PHUIIconView();
$person1->setHeadSize(PHUIIconView::HEAD_MEDIUM);
$person1->setHref('http://en.wikipedia.org/wiki/George_Washington');
$person1->setImage(
celerity_get_resource_uri('/rsrc/image/people/washington.png'));
$person2 = new PHUIIconView();
$person2->setHeadSize(PHUIIconView::HEAD_MEDIUM);
$person2->setHref('http://en.wikipedia.org/wiki/Warren_G._Harding');
$person2->setImage(
celerity_get_resource_uri('/rsrc/image/people/harding.png'));
$person3 = new PHUIIconView();
$person3->setHeadSize(PHUIIconView::HEAD_MEDIUM);
$person3->setHref('http://en.wikipedia.org/wiki/William_Howard_Taft');
$person3->setImage(
celerity_get_resource_uri('/rsrc/image/people/taft.png'));
$person4 = new PHUIIconView();
$person4->setHeadSize(PHUIIconView::HEAD_SMALL);
$person4->setHref('http://en.wikipedia.org/wiki/George_Washington');
$person4->setImage(
celerity_get_resource_uri('/rsrc/image/people/washington.png'));
$person5 = new PHUIIconView();
$person5->setHeadSize(PHUIIconView::HEAD_SMALL);
$person5->setHref('http://en.wikipedia.org/wiki/Warren_G._Harding');
$person5->setImage(
celerity_get_resource_uri('/rsrc/image/people/harding.png'));
$person6 = new PHUIIconView();
$person6->setHeadSize(PHUIIconView::HEAD_SMALL);
$person6->setHref('http://en.wikipedia.org/wiki/William_Howard_Taft');
$person6->setImage(
celerity_get_resource_uri('/rsrc/image/people/taft.png'));
$tokens = array(
'like-1',
'like-2',
'heart-1',
'heart-2',
);
$tokenview = array();
foreach ($tokens as $token) {
$tokenview[] =
id(new PHUIIconView())
->setSpriteSheet(PHUIIconView::SPRITE_TOKENS)
->setSpriteIcon($token);
}
$logins = array(
'Asana',
'Dropbox',
'Google',
'Github',
);
$loginview = array();
foreach ($logins as $login) {
$loginview[] =
id(new PHUIIconView())
->setSpriteSheet(PHUIIconView::SPRITE_LOGIN)
->setSpriteIcon($login)
->addClass(PHUI::MARGIN_SMALL_RIGHT);
}
$layout_cicons = id(new PHUIBoxView())
->appendChild($cicons)
->addMargin(PHUI::MARGIN_LARGE);
$layout_fa = id(new PHUIBoxView())
->appendChild($ficons)
->addMargin(PHUI::MARGIN_LARGE);
$layout2 = id(new PHUIBoxView())
->appendChild(array($person1, $person2, $person3))
->addMargin(PHUI::MARGIN_MEDIUM);
$layout2a = id(new PHUIBoxView())
->appendChild(array($person4, $person5, $person6))
->addMargin(PHUI::MARGIN_MEDIUM);
$layout3 = id(new PHUIBoxView())
->appendChild($tokenview)
->addMargin(PHUI::MARGIN_MEDIUM);
$layout5 = id(new PHUIBoxView())
->appendChild($loginview)
->addMargin(PHUI::MARGIN_MEDIUM);
$fa_link = phutil_tag(
'a',
array(
'href' => 'http://fontawesome.io',
),
'http://fontawesome.io');
$fa_text = pht('Font Awesome by Dave Gandy - %s', $fa_link);
$fontawesome = id(new PHUIObjectBoxView())
->setHeaderText($fa_text)
->appendChild($layout_fa);
$transforms = id(new PHUIObjectBoxView())
->setHeaderText(pht('Colors and Transforms'))
->appendChild($layout_cicons);
$wrap2 = id(new PHUIObjectBoxView())
->setHeaderText(pht('People!'))
->appendChild(array($layout2, $layout2a));
$wrap3 = id(new PHUIObjectBoxView())
->setHeaderText(pht('Tokens'))
->appendChild($layout3);
$wrap5 = id(new PHUIObjectBoxView())
->setHeaderText(pht('Authentication'))
->appendChild($layout5);
return phutil_tag(
'div',
array(
'class' => 'phui-icon-example',
),
array(
$fontawesome,
$transforms,
$wrap2,
$wrap3,
$wrap5,
));
}
}
diff --git a/src/applications/uiexample/examples/PHUIImageMaskExample.php b/src/applications/uiexample/examples/PHUIImageMaskExample.php
index c43da6302..823823038 100644
--- a/src/applications/uiexample/examples/PHUIImageMaskExample.php
+++ b/src/applications/uiexample/examples/PHUIImageMaskExample.php
@@ -1,88 +1,87 @@
<?php
final class PHUIImageMaskExample extends PhabricatorUIExample {
public function getName() {
- return 'Image Masks';
+ return pht('Image Masks');
}
public function getDescription() {
- return 'Display images with crops.';
+ return pht('Display images with crops.');
}
public function renderExample() {
-
$image = celerity_get_resource_uri('/rsrc/image/examples/hero.png');
$display_height = 100;
$display_width = 200;
$mask1 = id(new PHUIImageMaskView())
->addClass('ml')
->setImage($image)
->setDisplayHeight($display_height)
->setDisplayWidth($display_width)
->centerViewOnPoint(265, 185, 30, 140);
$mask2 = id(new PHUIImageMaskView())
->addClass('ml')
->setImage($image)
->setDisplayHeight($display_height)
->setDisplayWidth($display_width)
->centerViewOnPoint(18, 18, 40, 80);
$mask3 = id(new PHUIImageMaskView())
->addClass('ml')
->setImage($image)
->setDisplayHeight($display_height)
->setDisplayWidth($display_width)
->centerViewOnPoint(265, 185, 30, 140)
->withMask(true);
$mask4 = id(new PHUIImageMaskView())
->addClass('ml')
->setImage($image)
->setDisplayHeight($display_height)
->setDisplayWidth($display_width)
->centerViewOnPoint(18, 18, 40, 80)
->withMask(true);
$mask5 = id(new PHUIImageMaskView())
->addClass('ml')
->setImage($image)
->setDisplayHeight($display_height)
->setDisplayWidth($display_width)
->centerViewOnPoint(254, 272, 60, 240)
->withMask(true);
$box1 = id(new PHUIObjectBoxView())
->setHeaderText(pht('Center is in the middle'))
->appendChild($mask1);
$box2 = id(new PHUIObjectBoxView())
->setHeaderText(pht('Center is on an edge'))
->appendChild($mask2);
$box3 = id(new PHUIObjectBoxView())
->setHeaderText(pht('Center Masked'))
->appendChild($mask3);
$box4 = id(new PHUIObjectBoxView())
->setHeaderText(pht('Edge Masked'))
->appendChild($mask4);
$box5 = id(new PHUIObjectBoxView())
->setHeaderText(pht('Wide Masked'))
->appendChild($mask5);
return phutil_tag(
'div',
array(),
array(
$box1,
$box2,
$box3,
$box4,
$box5,
));
}
}
diff --git a/src/applications/uiexample/examples/PHUIInfoExample.php b/src/applications/uiexample/examples/PHUIInfoExample.php
index 012494f0e..29a4e661a 100644
--- a/src/applications/uiexample/examples/PHUIInfoExample.php
+++ b/src/applications/uiexample/examples/PHUIInfoExample.php
@@ -1,82 +1,83 @@
<?php
final class PHUIInfoExample extends PhabricatorUIExample {
public function getName() {
- return 'Info View';
+ return pht('Info View');
}
public function getDescription() {
- return hsprintf(
- 'Use <tt>PHUIInfoView</tt> to render errors, warnings and notices.');
+ return pht(
+ 'Use %s to render errors, warnings and notices.',
+ phutil_tag('tt', array(), 'PHUIInfoView'));
}
public function renderExample() {
$request = $this->getRequest();
$user = $request->getUser();
$sevs = array(
- PHUIInfoView::SEVERITY_ERROR => 'Error',
- PHUIInfoView::SEVERITY_WARNING => 'Warning',
- PHUIInfoView::SEVERITY_NODATA => 'No Data',
- PHUIInfoView::SEVERITY_NOTICE => 'Notice',
- PHUIInfoView::SEVERITY_SUCCESS => 'Success',
+ PHUIInfoView::SEVERITY_ERROR => pht('Error'),
+ PHUIInfoView::SEVERITY_WARNING => pht('Warning'),
+ PHUIInfoView::SEVERITY_NODATA => pht('No Data'),
+ PHUIInfoView::SEVERITY_NOTICE => pht('Notice'),
+ PHUIInfoView::SEVERITY_SUCCESS => pht('Success'),
);
$button = id(new PHUIButtonView())
->setTag('a')
- ->setText('Resolve Issue')
+ ->setText(pht('Resolve Issue'))
->setHref('#');
$views = array();
// Only Title
foreach ($sevs as $sev => $title) {
$view = new PHUIInfoView();
$view->setSeverity($sev);
$view->setTitle($title);
$views[] = $view;
}
$views[] = phutil_tag('br', array(), null);
// Only Body
foreach ($sevs as $sev => $title) {
$view = new PHUIInfoView();
$view->setSeverity($sev);
- $view->appendChild('Several issues were encountered.');
+ $view->appendChild(pht('Several issues were encountered.'));
$view->addButton($button);
$views[] = $view;
}
$views[] = phutil_tag('br', array(), null);
// Only Errors
foreach ($sevs as $sev => $title) {
$view = new PHUIInfoView();
$view->setSeverity($sev);
$view->setErrors(
array(
- 'Overcooked.',
- 'Too much salt.',
- 'Full of sand.',
+ pht('Overcooked.'),
+ pht('Too much salt.'),
+ pht('Full of sand.'),
));
$views[] = $view;
}
$views[] = phutil_tag('br', array(), null);
// All
foreach ($sevs as $sev => $title) {
$view = new PHUIInfoView();
$view->setSeverity($sev);
$view->setTitle($title);
- $view->appendChild('Several issues were encountered.');
+ $view->appendChild(pht('Several issues were encountered.'));
$view->setErrors(
array(
- 'Overcooked.',
- 'Too much salt.',
- 'Full of sand.',
+ pht('Overcooked.'),
+ pht('Too much salt.'),
+ pht('Full of sand.'),
));
$views[] = $view;
}
return $views;
}
}
diff --git a/src/applications/uiexample/examples/PHUIInfoPanelExample.php b/src/applications/uiexample/examples/PHUIInfoPanelExample.php
index 7f75656b3..74ed18f37 100644
--- a/src/applications/uiexample/examples/PHUIInfoPanelExample.php
+++ b/src/applications/uiexample/examples/PHUIInfoPanelExample.php
@@ -1,139 +1,138 @@
<?php
final class PHUIInfoPanelExample extends PhabricatorUIExample {
public function getName() {
- return 'Info Panel';
+ return pht('Info Panel');
}
public function getDescription() {
- return 'A medium sized box with bits of gooey information.';
+ return pht('A medium sized box with bits of gooey information.');
}
public function renderExample() {
-
$header1 = id(new PHUIHeaderView())
->setHeader(pht('Conpherence'));
$header2 = id(new PHUIHeaderView())
->setHeader(pht('Diffusion'));
$header3 = id(new PHUIHeaderView())
->setHeader(pht('Backend Ops Projects'));
$header4 = id(new PHUIHeaderView())
->setHeader(pht('Revamp Liberty'))
->setSubHeader(pht('For great justice'))
->setImage(
celerity_get_resource_uri('/rsrc/image/people/washington.png'));
$header5 = id(new PHUIHeaderView())
->setHeader(pht('Phacility Redesign'))
->setSubHeader(pht('Move them pixels'))
->setImage(
celerity_get_resource_uri('/rsrc/image/people/harding.png'));
$header6 = id(new PHUIHeaderView())
->setHeader(pht('Python Phlux'))
->setSubHeader(pht('No. Sleep. Till Brooklyn.'))
->setImage(
celerity_get_resource_uri('/rsrc/image/people/taft.png'));
$column1 = id(new PHUIInfoPanelView())
->setHeader($header1)
->setColumns(3)
- ->addInfoBlock(3, 'Needs Triage')
- ->addInfoBlock(5, 'Unbreak Now')
- ->addInfoBlock(0, 'High')
- ->addInfoBlock(0, 'Normal')
- ->addInfoBlock(12, 'Low')
- ->addInfoBlock(123, 'Wishlist');
+ ->addInfoBlock(3, pht('Needs Triage'))
+ ->addInfoBlock(5, pht('Unbreak Now'))
+ ->addInfoBlock(0, pht('High'))
+ ->addInfoBlock(0, pht('Normal'))
+ ->addInfoBlock(12, pht('Low'))
+ ->addInfoBlock(123, pht('Wishlist'));
$column2 = id(new PHUIInfoPanelView())
->setHeader($header2)
->setColumns(3)
- ->addInfoBlock(3, 'Needs Triage')
- ->addInfoBlock(5, 'Unbreak Now')
- ->addInfoBlock(0, 'High')
- ->addInfoBlock(0, 'Normal')
- ->addInfoBlock(12, 'Low')
- ->addInfoBlock(123, 'Wishlist');
+ ->addInfoBlock(3, pht('Needs Triage'))
+ ->addInfoBlock(5, pht('Unbreak Now'))
+ ->addInfoBlock(0, pht('High'))
+ ->addInfoBlock(0, pht('Normal'))
+ ->addInfoBlock(12, pht('Low'))
+ ->addInfoBlock(123, pht('Wishlist'));
$column3 = id(new PHUIInfoPanelView())
->setHeader($header3)
->setColumns(3)
- ->addInfoBlock(3, 'Needs Triage')
- ->addInfoBlock(5, 'Unbreak Now')
- ->addInfoBlock(0, 'High')
- ->addInfoBlock(0, 'Normal')
- ->addInfoBlock(12, 'Low')
- ->addInfoBlock(123, 'Wishlist');
+ ->addInfoBlock(3, pht('Needs Triage'))
+ ->addInfoBlock(5, pht('Unbreak Now'))
+ ->addInfoBlock(0, pht('High'))
+ ->addInfoBlock(0, pht('Normal'))
+ ->addInfoBlock(12, pht('Low'))
+ ->addInfoBlock(123, pht('Wishlist'));
$column4 = id(new PHUIInfoPanelView())
->setHeader($header4)
->setColumns(3)
->setProgress(90)
- ->addInfoBlock(3, 'Needs Triage')
- ->addInfoBlock(5, 'Unbreak Now')
- ->addInfoBlock(0, 'High')
- ->addInfoBlock(0, 'Normal')
- ->addInfoBlock(0, 'Wishlist');
+ ->addInfoBlock(3, pht('Needs Triage'))
+ ->addInfoBlock(5, pht('Unbreak Now'))
+ ->addInfoBlock(0, pht('High'))
+ ->addInfoBlock(0, pht('Normal'))
+ ->addInfoBlock(0, pht('Wishlist'));
$column5 = id(new PHUIInfoPanelView())
->setHeader($header5)
->setColumns(2)
->setProgress(25)
- ->addInfoBlock(3, 'Needs Triage')
- ->addInfoBlock(5, 'Unbreak Now')
- ->addInfoBlock(0, 'High')
- ->addInfoBlock(0, 'Normal');
+ ->addInfoBlock(3, pht('Needs Triage'))
+ ->addInfoBlock(5, pht('Unbreak Now'))
+ ->addInfoBlock(0, pht('High'))
+ ->addInfoBlock(0, pht('Normal'));
$column6 = id(new PHUIInfoPanelView())
->setHeader($header6)
->setColumns(2)
->setProgress(50)
- ->addInfoBlock(3, 'Needs Triage')
- ->addInfoBlock(5, 'Unbreak Now')
- ->addInfoBlock(0, 'High')
- ->addInfoBlock(0, 'Normal');
+ ->addInfoBlock(3, pht('Needs Triage'))
+ ->addInfoBlock(5, pht('Unbreak Now'))
+ ->addInfoBlock(0, pht('High'))
+ ->addInfoBlock(0, pht('Normal'));
$layout1 = id(new AphrontMultiColumnView())
->addColumn($column1)
->addColumn($column2)
->addColumn($column3)
->setFluidLayout(true);
$layout2 = id(new AphrontMultiColumnView())
->addColumn($column4)
->addColumn($column5)
->addColumn($column6)
->setFluidLayout(true);
$head1 = id(new PHUIHeaderView())
->setHeader(pht('Flagged'));
$head2 = id(new PHUIHeaderView())
->setHeader(pht('Sprints'));
$wrap1 = id(new PHUIBoxView())
->appendChild($layout1)
->addMargin(PHUI::MARGIN_LARGE_BOTTOM);
$wrap2 = id(new PHUIBoxView())
->appendChild($layout2)
->addMargin(PHUI::MARGIN_LARGE_BOTTOM);
return phutil_tag(
'div',
array(),
array(
$head1,
$wrap1,
$head2,
$wrap2,
));
}
}
diff --git a/src/applications/uiexample/examples/PHUIListExample.php b/src/applications/uiexample/examples/PHUIListExample.php
index bda1e1a2b..eb4882b1d 100644
--- a/src/applications/uiexample/examples/PHUIListExample.php
+++ b/src/applications/uiexample/examples/PHUIListExample.php
@@ -1,302 +1,300 @@
<?php
final class PHUIListExample extends PhabricatorUIExample {
public function getName() {
- return 'Lists';
+ return pht('Lists');
}
public function getDescription() {
- return 'Create a fanciful list of objects and prismatic donuts.';
+ return pht('Create a fanciful list of objects and prismatic donuts.');
}
public function renderExample() {
-
-
/* Action Menu */
$action1 = id(new PHUIListItemView())
- ->setName('Edit Document')
+ ->setName(pht('Edit Document'))
->setHref('#')
->setIcon('fa-pencil')
->setType(PHUIListItemView::TYPE_LINK);
$action2 = id(new PHUIListItemView())
- ->setName('Move Document')
+ ->setName(pht('Move Document'))
->setHref('#')
->setIcon('fa-arrows')
->setType(PHUIListItemView::TYPE_LINK);
$action3 = id(new PHUIListItemView())
- ->setName('Delete Document')
+ ->setName(pht('Delete Document'))
->setHref('#')
->setIcon('fa-times')
->setType(PHUIListItemView::TYPE_LINK);
$action4 = id(new PHUIListItemView())
- ->setName('View History')
+ ->setName(pht('View History'))
->setHref('#')
->setIcon('fa-list')
->setType(PHUIListItemView::TYPE_LINK);
$action5 = id(new PHUIListItemView())
- ->setName('Subscribe')
+ ->setName(pht('Subscribe'))
->setHref('#')
->setIcon('fa-plus-circle')
->setType(PHUIListItemView::TYPE_LINK);
$actionmenu = id(new PHUIListView())
->setType(PHUIListView::SIDENAV_LIST)
->addMenuItem($action1)
->addMenuItem($action2)
->addMenuItem($action3)
->addMenuItem($action4)
->addMenuItem($action5);
/* Side Navigation */
$label1 = id(new PHUIListItemView())
- ->setName('Getting Started')
+ ->setName(pht('Getting Started'))
->setType(PHUIListItemView::TYPE_LABEL);
$label2 = id(new PHUIListItemView())
- ->setName('Documentation')
+ ->setName(pht('Documentation'))
->setType(PHUIListItemView::TYPE_LABEL);
$item1 = id(new PHUIListItemView())
- ->setName('Installation')
+ ->setName(pht('Installation'))
->setHref('#')
->setType(PHUIListItemView::TYPE_LINK);
$item2 = id(new PHUIListItemView())
- ->setName('Webserver Config')
+ ->setName(pht('Webserver Config'))
->setHref('#')
->setType(PHUIListItemView::TYPE_LINK);
$item3 = id(new PHUIListItemView())
- ->setName('Adding Users')
+ ->setName(pht('Adding Users'))
->setHref('#')
->setType(PHUIListItemView::TYPE_LINK);
$item4 = id(new PHUIListItemView())
- ->setName('Debugging')
+ ->setName(pht('Debugging'))
->setHref('#')
->setType(PHUIListItemView::TYPE_LINK);
$divider = id(new PHUIListItemView())
->setType(PHUIListItemView::TYPE_DIVIDER);
$sidenav = id(new PHUIListView())
->setType(PHUIListView::SIDENAV_LIST)
->addMenuItem($label1)
->addMenuItem($item3)
->addMenuItem($item2)
->addMenuItem($item1)
->addMenuItem($item4)
->addMenuItem($divider)
->addMenuItem($label2)
->addMenuItem($item3)
->addMenuItem($item2)
->addMenuItem($item1)
->addMenuItem($item4);
/* Unstyled */
$item1 = id(new PHUIListItemView())
- ->setName('Rain');
+ ->setName(pht('Rain'));
$item2 = id(new PHUIListItemView())
- ->setName('Spain');
+ ->setName(pht('Spain'));
$item3 = id(new PHUIListItemView())
- ->setName('Mainly');
+ ->setName(pht('Mainly'));
$item4 = id(new PHUIListItemView())
- ->setName('Plains');
+ ->setName(pht('Plains'));
$unstyled = id(new PHUIListView())
->addMenuItem($item1)
->addMenuItem($item2)
->addMenuItem($item3)
->addMenuItem($item4);
/* Top Navigation */
$home = id(new PHUIListItemView())
->setIcon('fa-home')
->setHref('#')
->setType(PHUIListItemView::TYPE_ICON);
$item1 = id(new PHUIListItemView())
- ->setName('Installation')
+ ->setName(pht('Installation'))
->setHref('#')
->setSelected(true)
->setType(PHUIListItemView::TYPE_LINK);
$item2 = id(new PHUIListItemView())
- ->setName('Webserver Config')
+ ->setName(pht('Webserver Config'))
->setHref('#')
->setType(PHUIListItemView::TYPE_LINK);
$item3 = id(new PHUIListItemView())
- ->setName('Adding Users')
+ ->setName(pht('Adding Users'))
->setHref('#')
->setType(PHUIListItemView::TYPE_LINK);
$item4 = id(new PHUIListItemView())
- ->setName('Debugging')
+ ->setName(pht('Debugging'))
->setHref('#')
->setType(PHUIListItemView::TYPE_LINK);
$item1 = id(new PHUIListItemView())
- ->setName('Installation')
+ ->setName(pht('Installation'))
->setHref('#')
->setSelected(true)
->setType(PHUIListItemView::TYPE_LINK);
$item2 = id(new PHUIListItemView())
- ->setName('Webserver Config')
+ ->setName(pht('Webserver Config'))
->setHref('#')
->setType(PHUIListItemView::TYPE_LINK);
$details1 = id(new PHUIListItemView())
- ->setName('Details')
+ ->setName(pht('Details'))
->setHref('#')
->setSelected(true)
->setType(PHUIListItemView::TYPE_LINK);
$details2 = id(new PHUIListItemView())
- ->setName('Lint (OK)')
+ ->setName(pht('Lint (OK)'))
->setHref('#')
->setType(PHUIListItemView::TYPE_LINK);
$details3 = id(new PHUIListItemView())
- ->setName('Unit (5/5)')
+ ->setName(pht('Unit (5/5)'))
->setHref('#')
->setType(PHUIListItemView::TYPE_LINK);
$details4 = id(new PHUIListItemView())
- ->setName('Lint (Warn)')
+ ->setName(pht('Lint (Warn)'))
->setHref('#')
->setStatusColor(PHUIListItemView::STATUS_WARN)
->setType(PHUIListItemView::TYPE_LINK);
$details5 = id(new PHUIListItemView())
- ->setName('Unit (3/5)')
+ ->setName(pht('Unit (3/5)'))
->setHref('#')
->setStatusColor(PHUIListItemView::STATUS_FAIL)
->setType(PHUIListItemView::TYPE_LINK);
$topnav = id(new PHUIListView())
->setType(PHUIListView::NAVBAR_LIST)
->addMenuItem($home)
->addMenuItem($item1)
->addMenuItem($item2)
->addMenuItem($item3)
->addMenuItem($item4);
$statustabs = id(new PHUIListView())
->setType(PHUIListView::NAVBAR_LIST)
->addMenuItem($details1)
->addMenuItem($details2)
->addMenuItem($details3)
->addMenuItem($details4)
->addMenuItem($details5);
$layout1 =
array(
id(new PHUIBoxView())
->appendChild($unstyled)
->addMargin(PHUI::MARGIN_MEDIUM)
->addPadding(PHUI::PADDING_SMALL)
->setBorder(true),
);
$layout2 =
array(
id(new PHUIBoxView())
->appendChild($sidenav)
->addMargin(PHUI::MARGIN_MEDIUM)
->setBorder(true),
);
$layout3 =
array(
id(new PHUIBoxView())
->appendChild($topnav)
->addMargin(PHUI::MARGIN_MEDIUM)
->setBorder(true),
);
$layout4 =
array(
id(new PHUIBoxView())
->appendChild($actionmenu)
->addMargin(PHUI::MARGIN_MEDIUM)
->setBorder(true),
);
$layout5 =
array(
id(new PHUIBoxView())
->appendChild($statustabs)
->addMargin(PHUI::MARGIN_MEDIUM)
->setBorder(true),
);
$head1 = id(new PHUIHeaderView())
->setHeader(pht('Unstyled'));
$head2 = id(new PHUIHeaderView())
->setHeader(pht('Side Navigation'));
$head3 = id(new PHUIHeaderView())
->setHeader(pht('Top Navigation'));
$head4 = id(new PHUIHeaderView())
->setHeader(pht('Action Menu'));
$head5 = id(new PHUIHeaderView())
->setHeader(pht('Status Tabs'));
$wrap1 = id(new PHUIBoxView())
->appendChild($layout1)
->addMargin(PHUI::MARGIN_LARGE);
$wrap2 = id(new PHUIBoxView())
->appendChild($layout2)
->addMargin(PHUI::MARGIN_LARGE);
$wrap3 = id(new PHUIBoxView())
->appendChild($layout3)
->addMargin(PHUI::MARGIN_LARGE);
$wrap4 = id(new PHUIBoxView())
->appendChild($layout4)
->addMargin(PHUI::MARGIN_LARGE);
$wrap5 = id(new PHUIBoxView())
->appendChild($layout5)
->addMargin(PHUI::MARGIN_LARGE);
return phutil_tag(
'div',
array(
'class' => 'phui-list-example',
),
array(
$head1,
$wrap1,
$head2,
$wrap2,
$head3,
$wrap3,
$head5,
$wrap5,
$head4,
$wrap4,
));
}
}
diff --git a/src/applications/uiexample/examples/PHUIObjectItemListExample.php b/src/applications/uiexample/examples/PHUIObjectItemListExample.php
index 370276eaa..10f9474f2 100644
--- a/src/applications/uiexample/examples/PHUIObjectItemListExample.php
+++ b/src/applications/uiexample/examples/PHUIObjectItemListExample.php
@@ -1,415 +1,417 @@
<?php
final class PHUIObjectItemListExample extends PhabricatorUIExample {
public function getName() {
- return 'Object Item List';
+ return pht('Object Item List');
}
public function getDescription() {
- return hsprintf(
- 'Use <tt>PHUIObjectItemListView</tt> to render lists of objects.');
+ return pht(
+ 'Use %s to render lists of objects.',
+ hsprintf('<tt>PHUIObjectItemListView</tt>'));
}
public function renderExample() {
$request = $this->getRequest();
$user = $request->getUser();
$handle = id(new PhabricatorHandleQuery())
->setViewer($user)
->withPHIDs(array($user->getPHID()))
->executeOne();
$out = array();
$head = id(new PHUIHeaderView())
->setHeader(pht('Basic List'));
$list = new PHUIObjectItemListView();
$list->addItem(
id(new PHUIObjectItemView())
->setObjectName('FRUIT1')
->setHeader(pht('Apple'))
->setHref('#'));
$list->addItem(
id(new PHUIObjectItemView())
->setObjectName('FRUIT2')
->setHeader(pht('Banana'))
->setHref('#'));
$list->addItem(
id(new PHUIObjectItemView())
->setObjectName('FRUIT3')
->setHeader(pht('Cherry'))
->setHref('#'));
$out[] = array($head, $list);
$head = id(new PHUIHeaderView())
->setHeader(pht('Empty List'));
$list = new PHUIObjectItemListView();
$list->setNoDataString(pht('This list is empty.'));
$out[] = array($head, $list);
$head = id(new PHUIHeaderView())
->setHeader(pht('Stacked List'));
$list = new PHUIObjectItemListView();
$list->setStackable(true);
$list->addItem(
id(new PHUIObjectItemView())
->setHeader(pht('Monday'))
->setHref('#'));
$list->addItem(
id(new PHUIObjectItemView())
->setHeader(pht('Tuesday'))
->setHref('#'));
$list->addItem(
id(new PHUIObjectItemView())
->setHeader(pht('Wednesday'))
->setHref('#'));
$list->addItem(
id(new PHUIObjectItemView())
->setHeader(pht('Thursday'))
->setHref('#'));
$out[] = array($head, $list);
$head = id(new PHUIHeaderView())
->setHeader(pht('Plain List'));
$list = new PHUIObjectItemListView();
$list->setPlain(true);
$list->addItem(
id(new PHUIObjectItemView())
->setHeader(pht('Monday'))
->setSubHead('I love cats')
->setHref('#'));
$list->addItem(
id(new PHUIObjectItemView())
->setHeader(pht('Tuesday'))
->setSubHead('Cat, cats, cats')
->setHref('#'));
$list->addItem(
id(new PHUIObjectItemView())
->setHeader(pht('Wednesday'))
->setSubHead('Meow, meow, meow')
->setHref('#'));
$list->addItem(
id(new PHUIObjectItemView())
->setHeader(pht('Thursday'))
->setSubHead('Every single day')
->setHref('#'));
$out[] = array($head, $list);
$head = id(new PHUIHeaderView())
->setHeader(pht('Card List'));
$list = new PHUIObjectItemListView();
$list->addItem(
id(new PHUIObjectItemView())
->setHeader(pht('Business Card'))
->setBarColor('red'));
$list->addItem(
id(new PHUIObjectItemView())
->setHeader(pht('Playing Card'))
->setBarColor('orange')
->addIcon('fa-comment', pht('Royal Flush!')));
$owner = phutil_tag('a', array('href' => '#'), pht('jackofclubs'));
$list->addItem(
id(new PHUIObjectItemView())
->setHeader(pht('House of Cards'))
->setBarColor('yellow')
->setDisabled(true)
->addByline(pht('Owner: %s', $owner)));
$author = phutil_tag('a', array('href' => '#'), pht('agoat'));
$list->addItem(
id(new PHUIObjectItemView())
->setHeader(pht('Cardigan'))
->setBarColor('green')
->addIcon('fa-star', pht('Warm!'))
->addByline(pht('Author: %s', $author)));
$list->addItem(
id(new PHUIObjectItemView())
->setHeader(pht('Cardamom'))
->addFootIcon('fa-shield white', 'Spice')
->setBarColor('blue'));
$list->addItem(
id(new PHUIObjectItemView())
- ->setHeader(pht(
- 'The human cardiovascular system includes the heart, lungs, and '.
- 'some other parts; most of these parts are pretty squishy'))
+ ->setHeader(
+ pht(
+ 'The human cardiovascular system includes the heart, lungs, and '.
+ 'some other parts; most of these parts are pretty squishy'))
->addFootIcon('fa-search white', pht('Respiration!'))
->addHandleIcon($handle, pht('You have a cardiovascular system!'))
->setBarColor('indigo'));
$out[] = array($head, $list);
$head = id(new PHUIHeaderView())
->setHeader(pht('Grippable List'));
$list = new PHUIObjectItemListView();
$list->addItem(
id(new PHUIObjectItemView())
->setHeader(pht('Grab ahold!'))
->setHref('#')
->setGrippable(true)
->setBarColor('red'));
$list->addItem(
id(new PHUIObjectItemView())
->setHeader(pht('Hold on tight!'))
->setHref('#')
->setGrippable(true)
->setBarColor('yellow'));
$list->addItem(
id(new PHUIObjectItemView())
->setHeader(pht("Don't let go!"))
->setHref('#')
->setGrippable(true)
->setBarColor('green')
->addAction(
id(new PHUIListItemView())
->setHref('#')
->setIcon('fa-times')));
$out[] = array($head, $list);
$head = id(new PHUIHeaderView())
->setHeader(pht('List With Actions'));
$list = new PHUIObjectItemListView();
$list->addItem(
id(new PHUIObjectItemView())
->setHeader(pht('You Have The Power'))
->setHref('#')
->setBarColor('blue')
->addAction(
id(new PHUIListItemView())
->setHref('#')
->setName(pht('Moo'))
->setIcon('fa-pencil')));
$list->addItem(
id(new PHUIObjectItemView())
->setHeader(pht('Believe In Yourself'))
->setHref('#')
->setBarColor('violet')
->addAction(
id(new PHUIListItemView())
->setHref('#')
->setName(pht('Quack'))
->setIcon('fa-pencil'))
->addAction(
id(new PHUIListItemView())
->setHref('#')
->setName(pht('Oink'))
->setIcon('fa-times')));
$out[] = array($head, $list);
$head = id(new PHUIHeaderView())
->setHeader(pht('Extras'));
$list = new PHUIObjectItemListView();
$list->addItem(
id(new PHUIObjectItemView())
->setHeader(pht('Ace of Hearts'))
->setSubHead(
pht('This is a powerful card in the game "Hearts".'))
->setHref('#')
->addAttribute(pht('Suit: Hearts'))
->addAttribute(pht('Rank: Ace'))
->addIcon('fa-heart', pht('Ace'))
->addIcon('fa-heart red', pht('Hearts'))
->addFootIcon('fa-heart white', pht('Ace'))
->addFootIcon('fa-heart white', pht('Heart'))
->addHandleIcon($handle, pht('You hold all the cards.'))
->addHandleIcon($handle, pht('You make all the rules.')));
$list->addItem(
id(new PHUIObjectItemView())
->setHeader(pht('Just A Handle'))
->setHref('#')
->addHandleIcon($handle, pht('Handle Here')));
$list->addItem(
id(new PHUIObjectItemView())
->setHeader(pht('Poor Use of Space'))
->setHref('#')
->addAttribute('North West')
->addHandleIcon($handle, pht('South East')));
$list->addItem(
id(new PHUIObjectItemView())
->setHeader(pht('Crowded Eastern Edge'))
->setHref('#')
->addIcon('fa-circle red', pht('Stuff'))
->addIcon('fa-circle yellow', pht('Stuff'))
->addIcon('fa-circle green', pht('Stuff'))
->addHandleIcon($handle, pht('More Stuff')));
$out[] = array($head, $list);
$head = id(new PHUIHeaderView())
->setHeader(pht('Effects'));
$list = new PHUIObjectItemListView();
$list->addItem(
id(new PHUIObjectItemView())
->setObjectName('X1')
->setHeader(pht('Normal'))
->setHref('#'));
$list->addItem(
id(new PHUIObjectItemView())
->setObjectName('X2')
->setHeader(pht('Highlighted'))
->setEffect('highlighted')
->setHref('#'));
$list->addItem(
id(new PHUIObjectItemView())
->setObjectName('X3')
->setHeader(pht('Selected'))
->setEffect('selected')
->setHref('#'));
$list->addItem(
id(new PHUIObjectItemView())
->setObjectName('X4')
->setHeader(pht('Disabled'))
->setDisabled(true)
->setHref('#'));
$out[] = array($head, $list);
$head = id(new PHUIHeaderView())
->setHeader(pht('Colors'));
$list = new PHUIObjectItemListView();
$bar_colors = array(
null => pht('None'),
'red' => pht('Red'),
'orange' => pht('Orange'),
'yellow' => pht('Yellow'),
'green' => pht('Green'),
'sky' => pht('Sky'),
'blue' => pht('Blue'),
'indigo' => pht('Indigo'),
'violet' => pht('Violet'),
'grey' => pht('Grey'),
'black' => pht('Black'),
);
foreach ($bar_colors as $bar_color => $bar_label) {
$list->addItem(
id(new PHUIObjectItemView())
->setHeader($bar_label)
->setBarColor($bar_color));
}
$out[] = array($head, $list);
$head = id(new PHUIHeaderView())
->setHeader(pht('Images'));
$list = new PHUIObjectItemListView();
$default_profile = PhabricatorFile::loadBuiltin($user, 'profile.png');
$default_project = PhabricatorFile::loadBuiltin($user, 'project.png');
$list->addItem(
id(new PHUIObjectItemView())
->setImageURI($default_profile->getViewURI())
->setHeader(pht('Default User Profile Image'))
->setBarColor('violet')
->addAction(
id(new PHUIListItemView())
->setHref('#')
->setIcon('fa-plus-square')));
$list->addItem(
id(new PHUIObjectItemView())
->setImageURI($default_project->getViewURI())
->setHeader(pht('Default Project Profile Image'))
->setGrippable(true)
->addAttribute(pht('This is the default project profile image.')));
$out[] = array($head, $list);
$head = id(new PHUIHeaderView())
->setHeader(pht('States'));
$list = id(new PHUIObjectItemListView())
->setStates(true);
$list->addItem(
id(new PHUIObjectItemView())
->setObjectName('X1200')
->setHeader(pht('Action Passed'))
->addAttribute(pht('That went swimmingly, go you'))
->setHref('#')
->setState(PHUIObjectItemView::STATE_SUCCESS));
$list->addItem(
id(new PHUIObjectItemView())
->setObjectName('X1201')
->setHeader(pht('Action Failed'))
->addAttribute(pht('Whoopsies, might want to fix that'))
->setHref('#')
->setState(PHUIObjectItemView::STATE_FAIL));
$list->addItem(
id(new PHUIObjectItemView())
->setObjectName('X1202')
->setHeader(pht('Action Warning'))
->addAttribute(pht('We need to talk about things'))
->setHref('#')
->setState(PHUIObjectItemView::STATE_WARN));
$list->addItem(
id(new PHUIObjectItemView())
->setObjectName('X1203')
->setHeader(pht('Action Noted'))
->addAttribute(pht('The weather seems nice today'))
->setHref('#')
->setState(PHUIObjectItemView::STATE_NOTE));
$list->addItem(
id(new PHUIObjectItemView())
->setObjectName('X1203')
->setHeader(pht('Action In Progress'))
->addAttribute(pht('Outlook fuzzy, try again later'))
->setHref('#')
->setState(PHUIObjectItemView::STATE_BUILD));
$box = id(new PHUIObjectBoxView())
- ->setHeaderText('Test Things')
+ ->setHeaderText(pht('Test Things'))
->appendChild($list);
$out[] = array($head, $box);
return $out;
}
}
diff --git a/src/applications/uiexample/examples/PHUIPropertyListExample.php b/src/applications/uiexample/examples/PHUIPropertyListExample.php
index 6438f31e6..76a39774b 100644
--- a/src/applications/uiexample/examples/PHUIPropertyListExample.php
+++ b/src/applications/uiexample/examples/PHUIPropertyListExample.php
@@ -1,133 +1,138 @@
<?php
final class PHUIPropertyListExample extends PhabricatorUIExample {
public function getName() {
- return 'Property List';
+ return pht('Property List');
}
public function getDescription() {
- return hsprintf(
- 'Use <tt>PHUIPropertyListView</tt> to render object properties.');
+ return pht(
+ 'Use %s to render object properties.',
+ phutil_tag('tt', array(), 'PHUIPropertyListView'));
}
public function renderExample() {
$request = $this->getRequest();
$user = $request->getUser();
$details1 = id(new PHUIListItemView())
- ->setName('Details')
+ ->setName(pht('Details'))
->setSelected(true);
$details2 = id(new PHUIListItemView())
- ->setName('Rainbow Info')
+ ->setName(pht('Rainbow Info'))
->setStatusColor(PHUIListItemView::STATUS_WARN);
$details3 = id(new PHUIListItemView())
- ->setName('Pasta Haiku')
+ ->setName(pht('Pasta Haiku'))
->setStatusColor(PHUIListItemView::STATUS_FAIL);
$statustabs = id(new PHUIListView())
->setType(PHUIListView::NAVBAR_LIST)
->addMenuItem($details1)
->addMenuItem($details2)
->addMenuItem($details3);
$view = new PHUIPropertyListView();
$view->addProperty(
pht('Color'),
pht('Yellow'));
$view->addProperty(
pht('Size'),
pht('Mouse'));
$view->addProperty(
pht('Element'),
pht('Electric'));
$view->addTextContent(
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. '.
'Quisque rhoncus tempus massa, sit amet faucibus lectus bibendum '.
'viverra. Nunc tempus tempor quam id iaculis. Maecenas lectus '.
'velit, aliquam et consequat quis, tincidunt id dolor.');
$view2 = new PHUIPropertyListView();
- $view2->addSectionHeader('Colors of the Rainbow');
+ $view2->addSectionHeader(pht('Colors of the Rainbow'));
- $view2->addProperty('R', 'Red');
- $view2->addProperty('O', 'Orange');
- $view2->addProperty('Y', 'Yellow');
- $view2->addProperty('G', 'Green');
- $view2->addProperty('B', 'Blue');
- $view2->addProperty('I', 'Indigo');
- $view2->addProperty('V', 'Violet');
+ $view2->addProperty('R', pht('Red'));
+ $view2->addProperty('O', pht('Orange'));
+ $view2->addProperty('Y', pht('Yellow'));
+ $view2->addProperty('G', pht('Green'));
+ $view2->addProperty('B', pht('Blue'));
+ $view2->addProperty('I', pht('Indigo'));
+ $view2->addProperty('V', pht('Violet'));
$view3 = new PHUIPropertyListView();
- $view3->addSectionHeader('Haiku About Pasta');
+ $view3->addSectionHeader(pht('Haiku About Pasta'));
$view3->addTextContent(
hsprintf(
- 'this is a pasta<br />'.
- 'haiku. it is very bad.<br />'.
- 'what did you expect?'));
+ '%s<br />%s<br />%s',
+ pht('this is a pasta'),
+ pht('haiku. it is very bad.'),
+ pht('what did you expect?')));
$object_box1 = id(new PHUIObjectBoxView())
- ->setHeaderText('PHUIPropertyListView Stackered')
+ ->setHeaderText(pht('%s Stackered', 'PHUIPropertyListView'))
->addPropertyList($view, $details1)
->addPropertyList($view2, $details2)
->addPropertyList($view3, $details3);
$edge_cases_view = new PHUIPropertyListView();
$edge_cases_view->addProperty(
pht('Description'),
- pht('These layouts test UI edge cases in the element. This block '.
- 'tests wrapping and overflow behavior.'));
+ pht(
+ 'These layouts test UI edge cases in the element. This block '.
+ 'tests wrapping and overflow behavior.'));
$edge_cases_view->addProperty(
pht('A Very Very Very Very Very Very Very Very Very Long Property Label'),
- pht('This property label and property value are quite long. They '.
- 'demonstrate the wrapping behavior of the element, or lack thereof '.
- 'if something terrible has happened.'));
+ pht(
+ 'This property label and property value are quite long. They '.
+ 'demonstrate the wrapping behavior of the element, or lack thereof '.
+ 'if something terrible has happened.'));
$edge_cases_view->addProperty(
pht('AVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongUnbrokenPropertyLabel'),
- pht('Thispropertylabelandpropertyvaluearequitelongandhave'.
- 'nospacestheydemonstratetheoverflowbehavioroftheelement'.
- 'orlackthereof.'));
+ pht(
+ 'Thispropertylabelandpropertyvaluearequitelongandhave'.
+ 'nospacestheydemonstratetheoverflowbehavioroftheelement'.
+ 'orlackthereof.'));
$edge_cases_view->addProperty(
pht('Description'),
pht('The next section is an empty text block.'));
$edge_cases_view->addTextContent('');
$edge_cases_view->addProperty(
pht('Description'),
pht('This section should have multiple properties with the same name.'));
$edge_cases_view->addProperty(
pht('Joe'),
pht('Smith'));
$edge_cases_view->addProperty(
pht('Joe'),
pht('Smith'));
$edge_cases_view->addProperty(
pht('Joe'),
pht('Smith'));
$object_box2 = id(new PHUIObjectBoxView())
- ->setHeaderText('Some Bad Examples')
+ ->setHeaderText(pht('Some Bad Examples'))
->addPropertyList($edge_cases_view);
return array(
$object_box1,
$object_box2,
);
}
}
diff --git a/src/applications/uiexample/examples/PHUITagExample.php b/src/applications/uiexample/examples/PHUITagExample.php
index 8b9648007..155fe4e9d 100644
--- a/src/applications/uiexample/examples/PHUITagExample.php
+++ b/src/applications/uiexample/examples/PHUITagExample.php
@@ -1,205 +1,207 @@
<?php
final class PHUITagExample extends PhabricatorUIExample {
public function getName() {
- return 'Tags';
+ return pht('Tags');
}
public function getDescription() {
- return hsprintf('Use <tt>PHUITagView</tt> to render various tags.');
+ return pht(
+ 'Use %s to render various tags.',
+ phutil_tag('tt', array(), 'PHUITagView'));
}
public function renderExample() {
$intro = array();
$intro[] = 'Hey, ';
$intro[] = id(new PHUITagView())
->setType(PHUITagView::TYPE_PERSON)
->setName('@alincoln')
->setHref('#');
$intro[] = ' how is stuff?';
$intro[] = hsprintf('<br /><br />');
$intro[] = 'Did you hear that ';
$intro[] = id(new PHUITagView())
->setType(PHUITagView::TYPE_PERSON)
->setName('@gwashington')
->setDotColor(PHUITagView::COLOR_RED)
->setHref('#');
$intro[] = ' is away, ';
$intro[] = id(new PHUITagView())
->setType(PHUITagView::TYPE_PERSON)
->setName('@tjefferson')
->setDotColor(PHUITagView::COLOR_ORANGE)
->setHref('#');
$intro[] = ' has some errands, and ';
$intro[] = id(new PHUITagView())
->setType(PHUITagView::TYPE_PERSON)
->setName('@rreagan')
->setDotColor(PHUITagView::COLOR_GREY)
->setHref('#');
$intro[] = ' is gone?';
$intro[] = hsprintf('<br /><br />');
$intro[] = 'Take a look at ';
$intro[] = id(new PHUITagView())
->setType(PHUITagView::TYPE_OBJECT)
->setName('D123')
->setHref('#');
$intro[] = ' when you get a chance.';
$intro[] = hsprintf('<br /><br />');
$intro[] = 'Hmm? ';
$intro[] = id(new PHUITagView())
->setType(PHUITagView::TYPE_OBJECT)
->setName('D123')
->setClosed(true)
->setHref('#');
$intro[] = ' is ';
$intro[] = id(new PHUITagView())
->setType(PHUITagView::TYPE_STATE)
->setBackgroundColor(PHUITagView::COLOR_BLACK)
->setName('Abandoned');
$intro[] = '.';
$intro[] = hsprintf('<br /><br />');
$intro[] = 'I hope someone is going to ';
$intro[] = id(new PHUITagView())
->setType(PHUITagView::TYPE_OBJECT)
->setName('T123: Water The Dog')
->setHref('#');
$intro[] = ' -- that task is ';
$intro[] = id(new PHUITagView())
->setType(PHUITagView::TYPE_STATE)
->setBackgroundColor(PHUITagView::COLOR_RED)
->setName('High Priority');
$intro[] = '!';
$intro = id(new PHUIBoxView())
->appendChild($intro)
->addPadding(PHUI::PADDING_LARGE);
$header1 = id(new PHUIHeaderView())
->setHeader('Colors');
$colors = PHUITagView::getColors();
$tags = array();
foreach ($colors as $color) {
$tags[] = id(new PHUITagView())
->setType(PHUITagView::TYPE_STATE)
->setBackgroundColor($color)
->setName(ucwords($color));
$tags[] = hsprintf('<br /><br />');
}
$content1 = id(new PHUIBoxView())
->appendChild($tags)
->addPadding(PHUI::PADDING_LARGE);
$tags = array();
$tags[] = id(new PHUITagView())
->setType(PHUITagView::TYPE_STATE)
->setBackgroundColor(PHUITagView::COLOR_GREEN)
->setDotColor(PHUITagView::COLOR_RED)
- ->setName('Christmas');
+ ->setName(pht('Christmas'));
$tags[] = hsprintf('<br /><br />');
$tags[] = id(new PHUITagView())
->setType(PHUITagView::TYPE_OBJECT)
->setBackgroundColor(PHUITagView::COLOR_ORANGE)
->setDotColor(PHUITagView::COLOR_BLACK)
- ->setName('Halloween');
+ ->setName(pht('Halloween'));
$tags[] = hsprintf('<br /><br />');
$tags[] = id(new PHUITagView())
->setType(PHUITagView::TYPE_STATE)
->setBackgroundColor(PHUITagView::COLOR_INDIGO)
->setDotColor(PHUITagView::COLOR_YELLOW)
- ->setName('Easter');
+ ->setName(pht('Easter'));
$content2 = id(new PHUIBoxView())
->appendChild($tags)
->addPadding(PHUI::PADDING_LARGE);
$icons = array();
$icons[] = id(new PHUITagView())
->setType(PHUITagView::TYPE_STATE)
->setBackgroundColor(PHUITagView::COLOR_GREEN)
->setIcon('fa-check white')
- ->setName('Passed');
+ ->setName(pht('Passed'));
$icons[] = hsprintf('<br /><br />');
$icons[] = id(new PHUITagView())
->setType(PHUITagView::TYPE_STATE)
->setBackgroundColor(PHUITagView::COLOR_RED)
->setIcon('fa-times white')
- ->setName('Failed');
+ ->setName(pht('Failed'));
$icons[] = hsprintf('<br /><br />');
$icons[] = id(new PHUITagView())
->setType(PHUITagView::TYPE_STATE)
->setBackgroundColor(PHUITagView::COLOR_BLUE)
->setIcon('fa-refresh white')
- ->setName('Running');
+ ->setName(pht('Running'));
$icons[] = hsprintf('<br /><br />');
$icons[] = id(new PHUITagView())
->setType(PHUITagView::TYPE_STATE)
->setBackgroundColor(PHUITagView::COLOR_GREY)
->setIcon('fa-pause white')
- ->setName('Paused');
+ ->setName(pht('Paused'));
$icons[] = hsprintf('<br /><br />');
$icons[] = id(new PHUITagView())
->setType(PHUITagView::TYPE_STATE)
->setBackgroundColor(PHUITagView::COLOR_BLACK)
->setIcon('fa-stop white')
- ->setName('Stopped');
+ ->setName(pht('Stopped'));
$content3 = id(new PHUIBoxView())
->appendChild($icons)
->addPadding(PHUI::PADDING_LARGE);
$shades = PHUITagView::getShades();
$tags = array();
foreach ($shades as $shade) {
$tags[] = id(new PHUITagView())
->setType(PHUITagView::TYPE_OBJECT)
->setShade($shade)
->setIcon('fa-tags')
->setName(ucwords($shade))
->setHref('#');
$tags[] = hsprintf('&nbsp;');
$tags[] = id(new PHUITagView())
->setType(PHUITagView::TYPE_OBJECT)
->setShade($shade)
->setSlimShady(true)
->setIcon('fa-tags')
->setName(ucwords($shade))
->setHref('#');
$tags[] = hsprintf('<br /><br />');
}
$content4 = id(new PHUIBoxView())
->appendChild($tags)
->addPadding(PHUI::PADDING_LARGE);
$box = id(new PHUIObjectBoxView())
- ->setHeaderText('Inline')
+ ->setHeaderText(pht('Inline'))
->appendChild($intro);
$box1 = id(new PHUIObjectBoxView())
- ->setHeaderText('Colors')
+ ->setHeaderText(pht('Colors'))
->appendChild($content1);
$box2 = id(new PHUIObjectBoxView())
- ->setHeaderText('Holidays')
+ ->setHeaderText(pht('Holidays'))
->appendChild($content2);
$box3 = id(new PHUIObjectBoxView())
- ->setHeaderText('Icons')
+ ->setHeaderText(pht('Icons'))
->appendChild($content3);
$box4 = id(new PHUIObjectBoxView())
- ->setHeaderText('Shades')
+ ->setHeaderText(pht('Shades'))
->appendChild($content4);
return array($box, $box1, $box2, $box3, $box4);
}
}
diff --git a/src/applications/uiexample/examples/PHUITextExample.php b/src/applications/uiexample/examples/PHUITextExample.php
index d61c7b9ce..6ab69b22b 100644
--- a/src/applications/uiexample/examples/PHUITextExample.php
+++ b/src/applications/uiexample/examples/PHUITextExample.php
@@ -1,107 +1,107 @@
<?php
final class PHUITextExample extends PhabricatorUIExample {
public function getName() {
- return 'Text';
+ return pht('Text');
}
public function getDescription() {
- return 'Simple styles for displaying text.';
+ return pht('Simple styles for displaying text.');
}
public function renderExample() {
- $color1 = 'This is RED. ';
- $color2 = 'This is ORANGE. ';
- $color3 = 'This is YELLOW. ';
- $color4 = 'This is GREEN. ';
- $color5 = 'This is BLUE. ';
- $color6 = 'This is INDIGO. ';
- $color7 = 'This is VIOLET. ';
- $color8 = 'This is WHITE. ';
- $color9 = 'This is BLACK. ';
+ $color1 = pht('This is RED.');
+ $color2 = pht('This is ORANGE.');
+ $color3 = pht('This is YELLOW.');
+ $color4 = pht('This is GREEN.');
+ $color5 = pht('This is BLUE.');
+ $color6 = pht('This is INDIGO.');
+ $color7 = pht('This is VIOLET.');
+ $color8 = pht('This is WHITE.');
+ $color9 = pht('This is BLACK.');
- $text1 = 'This is BOLD. ';
- $text2 = 'This is Uppercase. ';
- $text3 = 'This is Stricken.';
+ $text1 = pht('This is BOLD.');
+ $text2 = pht('This is Uppercase.');
+ $text3 = pht('This is Stricken.');
$content =
array(
id(new PHUITextView())
->setText($color1)
->addClass(PHUI::TEXT_RED),
id(new PHUITextView())
->setText($color2)
->addClass(PHUI::TEXT_ORANGE),
id(new PHUITextView())
->setText($color3)
->addClass(PHUI::TEXT_YELLOW),
id(new PHUITextView())
->setText($color4)
->addClass(PHUI::TEXT_GREEN),
id(new PHUITextView())
->setText($color5)
->addClass(PHUI::TEXT_BLUE),
id(new PHUITextView())
->setText($color6)
->addClass(PHUI::TEXT_INDIGO),
id(new PHUITextView())
->setText($color7)
->addClass(PHUI::TEXT_VIOLET),
id(new PHUITextView())
->setText($color8)
->addClass(PHUI::TEXT_WHITE),
id(new PHUITextView())
->setText($color9)
->addClass(PHUI::TEXT_BLACK),
);
$content2 =
array(
id(new PHUITextView())
->setText($text1)
->addClass(PHUI::TEXT_BOLD),
id(new PHUITextView())
->setText($text2)
->addClass(PHUI::TEXT_UPPERCASE),
id(new PHUITextView())
->setText($text3)
->addClass(PHUI::TEXT_STRIKE),
);
$layout1 = id(new PHUIBoxView())
->appendChild($content)
- ->setBorder(true)
+ ->setBorder(true)
->addPadding(PHUI::PADDING_MEDIUM);
$head1 = id(new PHUIHeaderView())
->setHeader(pht('Basic Colors'));
$wrap1 = id(new PHUIBoxView())
->appendChild($layout1)
->addMargin(PHUI::MARGIN_LARGE);
$layout2 = id(new PHUIBoxView())
->appendChild($content2)
->setBorder(true)
->addPadding(PHUI::PADDING_MEDIUM);
$head2 = id(new PHUIHeaderView())
->setHeader(pht('Basic Transforms'));
$wrap2 = id(new PHUIBoxView())
->appendChild($layout2)
->addMargin(PHUI::MARGIN_LARGE);
return phutil_tag(
'div',
array(),
array(
$head1,
$wrap1,
$head2,
$wrap2,
));
}
}
diff --git a/src/applications/uiexample/examples/PHUITimelineExample.php b/src/applications/uiexample/examples/PHUITimelineExample.php
index 0ed81b01a..33b4fe975 100644
--- a/src/applications/uiexample/examples/PHUITimelineExample.php
+++ b/src/applications/uiexample/examples/PHUITimelineExample.php
@@ -1,201 +1,202 @@
<?php
final class PHUITimelineExample extends PhabricatorUIExample {
public function getName() {
- return 'Timeline View';
+ return pht('Timeline View');
}
public function getDescription() {
- return hsprintf(
- 'Use <tt>PHUITimelineView</tt> to comments and transactions.');
+ return pht(
+ 'Use %s to comments and transactions.',
+ hsprintf('<tt>PHUITimelineView</tt>'));
}
public function renderExample() {
$request = $this->getRequest();
$user = $request->getUser();
$handle = id(new PhabricatorHandleQuery())
->setViewer($user)
->withPHIDs(array($user->getPHID()))
->executeOne();
$events = array();
$events[] = id(new PHUITimelineEventView())
->setUserHandle($handle)
- ->setTitle('A major event.')
- ->appendChild('This is a major timeline event.');
+ ->setTitle(pht('A major event.'))
+ ->appendChild(pht('This is a major timeline event.'));
$events[] = id(new PHUITimelineEventView())
->setUserHandle($handle)
->setIcon('fa-heart')
- ->setTitle('A minor event.');
+ ->setTitle(pht('A minor event.'));
$events[] = id(new PHUITimelineEventView())
->setUserHandle($handle)
->setIcon('fa-comment')
- ->appendChild('A major event with no title.');
+ ->appendChild(pht('A major event with no title.'));
$events[] = id(new PHUITimelineEventView())
->setUserHandle($handle)
->setIcon('fa-star')
- ->setTitle('Another minor event.');
+ ->setTitle(pht('Another minor event.'));
$events[] = id(new PHUITimelineEventView())
->setIcon('fa-trophy')
->setToken('medal-1')
->setUserHandle($handle);
$events[] = id(new PHUITimelineEventView())
->setIcon('fa-quote-left')
->setToken('medal-1', true)
->setUserHandle($handle);
$events[] = id(new PHUITimelineEventView())
->setUserHandle($handle)
- ->setTitle('Major Red Event')
+ ->setTitle(pht('Major Red Event'))
->setIcon('fa-heart-o')
- ->appendChild('This event is red!')
+ ->appendChild(pht('This event is red!'))
->setColor(PhabricatorTransactions::COLOR_RED);
$events[] = id(new PHUITimelineEventView())
->setUserHandle($handle)
->setIcon('fa-female')
- ->setTitle('Minor Red Event')
+ ->setTitle(pht('Minor Red Event'))
->setColor(PhabricatorTransactions::COLOR_RED);
$events[] = id(new PHUITimelineEventView())
->setIcon('fa-refresh')
->setUserHandle($handle)
- ->setTitle('Minor Not-Red Event')
+ ->setTitle(pht('Minor Not-Red Event'))
->setColor(PhabricatorTransactions::COLOR_GREEN);
$events[] = id(new PHUITimelineEventView())
->setUserHandle($handle)
->setIcon('fa-calendar-o')
- ->setTitle('Minor Red Event')
+ ->setTitle(pht('Minor Red Event'))
->setColor(PhabricatorTransactions::COLOR_RED);
$events[] = id(new PHUITimelineEventView())
->setUserHandle($handle)
->setIcon('fa-check')
- ->setTitle('Historically Important Action')
+ ->setTitle(pht('Historically Important Action'))
->setColor(PhabricatorTransactions::COLOR_BLACK)
->setReallyMajorEvent(true);
$events[] = id(new PHUITimelineEventView())
->setUserHandle($handle)
->setIcon('fa-circle-o')
- ->setTitle('Major Green Disagreement Action')
- ->appendChild('This event is green!')
+ ->setTitle(pht('Major Green Disagreement Action'))
+ ->appendChild(pht('This event is green!'))
->setColor(PhabricatorTransactions::COLOR_GREEN);
$events[] = id(new PHUITimelineEventView())
->setUserHandle($handle)
->setIcon('fa-tag')
->setTitle(str_repeat('Long Text Title ', 64))
->appendChild(str_repeat('Long Text Body ', 64))
->setColor(PhabricatorTransactions::COLOR_ORANGE);
$events[] = id(new PHUITimelineEventView())
->setUserHandle($handle)
->setTitle(str_repeat('LongTextEventNoSpaces', 1024))
->appendChild(str_repeat('LongTextNoSpaces', 1024))
->setColor(PhabricatorTransactions::COLOR_RED);
$colors = array(
PhabricatorTransactions::COLOR_RED,
PhabricatorTransactions::COLOR_ORANGE,
PhabricatorTransactions::COLOR_YELLOW,
PhabricatorTransactions::COLOR_GREEN,
PhabricatorTransactions::COLOR_SKY,
PhabricatorTransactions::COLOR_BLUE,
PhabricatorTransactions::COLOR_INDIGO,
PhabricatorTransactions::COLOR_VIOLET,
PhabricatorTransactions::COLOR_GREY,
PhabricatorTransactions::COLOR_BLACK,
);
$events[] = id(new PHUITimelineEventView())
->setUserHandle($handle)
- ->setTitle('Colorless')
+ ->setTitle(pht('Colorless'))
->setIcon('fa-lock');
foreach ($colors as $color) {
$events[] = id(new PHUITimelineEventView())
->setUserHandle($handle)
- ->setTitle("Color '{$color}'")
+ ->setTitle(pht("Color '%s'", $color))
->setIcon('fa-paw')
->setColor($color);
}
$vhandle = $handle->renderLink();
$group_event = id(new PHUITimelineEventView())
->setUserHandle($handle)
->setTitle(pht('%s went to the store.', $vhandle));
$group_event->addEventToGroup(
id(new PHUITimelineEventView())
->setUserHandle($handle)
->setTitle(pht('%s bought an apple.', $vhandle))
->setColor('green')
->setIcon('fa-apple'));
$group_event->addEventToGroup(
id(new PHUITimelineEventView())
->setUserHandle($handle)
->setTitle(pht('%s bought a banana.', $vhandle))
->setColor('yellow')
->setIcon('fa-check'));
$group_event->addEventToGroup(
id(new PHUITimelineEventView())
->setUserHandle($handle)
->setTitle(pht('%s bought a cherry.', $vhandle))
->setColor('red')
->setIcon('fa-check'));
$group_event->addEventToGroup(
id(new PHUITimelineEventView())
->setUserHandle($handle)
->setTitle(pht('%s paid for his goods.', $vhandle)));
$group_event->addEventToGroup(
id(new PHUITimelineEventView())
->setUserHandle($handle)
->setTitle(pht('%s returned home.', $vhandle))
->setIcon('fa-home')
->setColor('blue'));
$group_event->addEventToGroup(
id(new PHUITimelineEventView())
->setUserHandle($handle)
->setTitle(pht('%s related on his adventures.', $vhandle))
->appendChild(
pht(
'Today, I went to the store. I bought an apple. I bought a '.
'banana. I bought a cherry. I paid for my goods, then I returned '.
'home.')));
$events[] = $group_event;
$anchor = 0;
foreach ($events as $group) {
foreach ($group->getEventGroup() as $event) {
$event->setUser($user);
$event->setDateCreated(time() + ($anchor * 60 * 8));
$event->setAnchor(++$anchor);
}
}
$timeline = id(new PHUITimelineView());
foreach ($events as $event) {
$timeline->addEvent($event);
}
return $timeline;
}
}
diff --git a/src/applications/uiexample/examples/PHUITypeaheadExample.php b/src/applications/uiexample/examples/PHUITypeaheadExample.php
index 810a6cc15..79f0ed5ea 100644
--- a/src/applications/uiexample/examples/PHUITypeaheadExample.php
+++ b/src/applications/uiexample/examples/PHUITypeaheadExample.php
@@ -1,58 +1,58 @@
<?php
final class PHUITypeaheadExample extends PhabricatorUIExample {
public function getName() {
- return 'Typeaheads';
+ return pht('Typeaheads');
}
public function getDescription() {
return pht('Typeaheads, tokenizers and tokens.');
}
public function renderExample() {
$token_list = array();
$token_list[] = id(new PhabricatorTypeaheadTokenView())
->setValue(pht('Normal Object'))
->setIcon('fa-user');
$token_list[] = id(new PhabricatorTypeaheadTokenView())
->setValue(pht('Disabled Object'))
->setTokenType(PhabricatorTypeaheadTokenView::TYPE_DISABLED)
->setIcon('fa-user');
$token_list[] = id(new PhabricatorTypeaheadTokenView())
->setValue(pht('Object with Color'))
->setIcon('fa-tag')
->setColor('green');
$token_list[] = id(new PhabricatorTypeaheadTokenView())
->setValue(pht('Function Token'))
->setTokenType(PhabricatorTypeaheadTokenView::TYPE_FUNCTION)
->setIcon('fa-users');
$token_list[] = id(new PhabricatorTypeaheadTokenView())
->setValue(pht('Invalid Token'))
->setTokenType(PhabricatorTypeaheadTokenView::TYPE_INVALID)
->setIcon('fa-exclamation-circle');
$token_list = phutil_tag(
'div',
array(
'class' => 'grouped',
'style' => 'padding: 8px',
),
$token_list);
$output = array();
$output[] = id(new PHUIObjectBoxView())
- ->setHeaderText('Tokens')
+ ->setHeaderText(pht('Tokens'))
->appendChild($token_list);
return $output;
}
}
diff --git a/src/applications/uiexample/examples/PhabricatorAphrontBarUIExample.php b/src/applications/uiexample/examples/PhabricatorAphrontBarUIExample.php
index 6520aab0c..d28d145ec 100644
--- a/src/applications/uiexample/examples/PhabricatorAphrontBarUIExample.php
+++ b/src/applications/uiexample/examples/PhabricatorAphrontBarUIExample.php
@@ -1,72 +1,72 @@
<?php
final class PhabricatorAphrontBarUIExample extends PhabricatorUIExample {
public function getName() {
- return 'Bars';
+ return pht('Bars');
}
public function getDescription() {
- return 'Like fractions, but more horizontal.';
+ return pht('Like fractions, but more horizontal.');
}
public function renderExample() {
$out = array();
$out[] = $this->renderTestThings('AphrontProgressBarView', 13, 10);
$out[] = $this->renderTestThings('AphrontGlyphBarView', 13, 10);
$out[] = $this->renderWeirdOrderGlyphBars();
$out[] = $this->renderAsciiStarBar();
return $out;
}
private function wrap($title, $thing) {
$thing = phutil_tag_div('ml grouped', $thing);
return id(new PHUIObjectBoxView())
->setHeaderText($title)
->appendChild($thing);
}
private function renderTestThings($class, $max, $incr) {
$bars = array();
for ($ii = 0; $ii <= $max; $ii++) {
$bars[] = newv($class, array())
->setValue($ii * $incr)
->setMax($max * $incr)
->setCaption("{$ii} outta {$max} ain't bad!");
}
return $this->wrap("Test {$class}", $bars);
}
private function renderWeirdOrderGlyphBars() {
$views = array();
$indices = array(1, 3, 7, 4, 2, 8, 9, 5, 10, 6);
$max = count($indices);
foreach ($indices as $index) {
$views[] = id(new AphrontGlyphBarView())
->setValue($index)
->setMax($max)
->setNumGlyphs(5)
->setCaption("Lol score is {$index}/{$max}")
->setGlyph(hsprintf('%s', 'LOL!'))
->setBackgroundGlyph(hsprintf('%s', '____'));
$views[] = hsprintf('<div style="clear:both;"></div>');
}
return $this->wrap(
'Glyph bars in weird order',
$views);
}
private function renderAsciiStarBar() {
$bar = id(new AphrontGlyphBarView())
->setValue(50)
->setMax(100)
->setCaption('Glyphs!')
->setNumGlyphs(10)
->setGlyph(hsprintf('%s', '*'));
return $this->wrap(
'Ascii star glyph bar', $bar);
}
}
diff --git a/src/applications/uiexample/examples/PhabricatorBarePageUIExample.php b/src/applications/uiexample/examples/PhabricatorBarePageUIExample.php
index d9fe54ddb..3f377edad 100644
--- a/src/applications/uiexample/examples/PhabricatorBarePageUIExample.php
+++ b/src/applications/uiexample/examples/PhabricatorBarePageUIExample.php
@@ -1,25 +1,25 @@
<?php
final class PhabricatorBarePageUIExample extends PhabricatorUIExample {
public function getName() {
- return 'Bare Page';
+ return pht('Bare Page');
}
public function getDescription() {
- return 'This is a bare page.';
+ return pht('This is a bare page.');
}
public function renderExample() {
$view = new PhabricatorBarePageView();
$view->appendChild(
phutil_tag(
'h1',
array(),
$this->getDescription()));
$response = new AphrontWebpageResponse();
$response->setContent($view->render());
return $response;
}
}
diff --git a/src/applications/uiexample/examples/PhabricatorBusyUIExample.php b/src/applications/uiexample/examples/PhabricatorBusyUIExample.php
index bfafe28c8..ab23e5cf7 100644
--- a/src/applications/uiexample/examples/PhabricatorBusyUIExample.php
+++ b/src/applications/uiexample/examples/PhabricatorBusyUIExample.php
@@ -1,17 +1,17 @@
<?php
final class PhabricatorBusyUIExample extends PhabricatorUIExample {
public function getName() {
- return 'Busy';
+ return pht('Busy');
}
public function getDescription() {
- return 'Busy.';
+ return pht('Busy.');
}
public function renderExample() {
Javelin::initBehavior('phabricator-busy-example');
return null;
}
}
diff --git a/src/applications/uiexample/examples/PhabricatorGestureUIExample.php b/src/applications/uiexample/examples/PhabricatorGestureUIExample.php
index 404727715..1adb7dee1 100644
--- a/src/applications/uiexample/examples/PhabricatorGestureUIExample.php
+++ b/src/applications/uiexample/examples/PhabricatorGestureUIExample.php
@@ -1,35 +1,35 @@
<?php
final class PhabricatorGestureUIExample extends PhabricatorUIExample {
public function getName() {
- return 'Gestures';
+ return pht('Gestures');
}
public function getDescription() {
- return hsprintf(
- 'Use <tt>touchable</tt> to listen for gesture events. Note that you '.
+ return pht(
+ 'Use %s to listen for gesture events. Note that you '.
'must be in device mode for this to work (you can narrow your browser '.
- 'window if you are on a desktop).');
+ 'window if you are on a desktop).',
+ phutil_tag('tt', array(), 'touchable'));
}
public function renderExample() {
-
$id = celerity_generate_unique_node_id();
Javelin::initBehavior(
'phabricator-gesture-example',
array(
'rootID' => $id,
));
return javelin_tag(
'div',
array(
'sigil' => 'touchable',
'id' => $id,
'style' => 'width: 320px; height: 240px; margin: auto;',
),
'');
}
}
diff --git a/src/applications/uiexample/examples/PhabricatorHovercardUIExample.php b/src/applications/uiexample/examples/PhabricatorHovercardUIExample.php
index 022307239..2f841cdda 100644
--- a/src/applications/uiexample/examples/PhabricatorHovercardUIExample.php
+++ b/src/applications/uiexample/examples/PhabricatorHovercardUIExample.php
@@ -1,78 +1,79 @@
<?php
final class PhabricatorHovercardUIExample extends PhabricatorUIExample {
public function getName() {
- return 'Hovercard';
+ return pht('Hovercard');
}
public function getDescription() {
- return hsprintf('Use <tt>PhabricatorHovercardView</tt> to render '.
- 'hovercards. Aren\'t I genius?');
+ return pht(
+ "Use %s to render hovercards. Aren't I genius?",
+ phutil_tag('tt', array(), 'PhabricatorHovercardView'));
}
public function renderExample() {
$request = $this->getRequest();
$user = $request->getUser();
$elements = array();
$diff_handle = $this->createBasicDummyHandle(
'D123',
DifferentialRevisionPHIDType::TYPECONST,
- 'Introduce cooler Differential Revisions');
+ pht('Introduce cooler Differential Revisions'));
- $panel = $this->createPanel('Differential Hovercard');
+ $panel = $this->createPanel(pht('Differential Hovercard'));
$panel->appendChild(id(new PhabricatorHovercardView())
->setObjectHandle($diff_handle)
->addField(pht('Author'), $user->getUsername())
->addField(pht('Updated'), phabricator_datetime(time(), $user))
->addAction(pht('Subscribe'), '/dev/random')
->setUser($user));
$elements[] = $panel;
$task_handle = $this->createBasicDummyHandle(
'T123',
ManiphestTaskPHIDType::TYPECONST,
- 'Improve Mobile Experience for Phabricator');
+ pht('Improve Mobile Experience for Phabricator'));
$tag = id(new PHUITagView())
->setType(PHUITagView::TYPE_STATE)
- ->setName('Closed, Resolved');
- $panel = $this->createPanel('Maniphest Hovercard');
+ ->setName(pht('Closed, Resolved'));
+ $panel = $this->createPanel(pht('Maniphest Hovercard'));
$panel->appendChild(id(new PhabricatorHovercardView())
->setObjectHandle($task_handle)
->setUser($user)
->addField(pht('Assigned to'), $user->getUsername())
->addField(pht('Dependent Tasks'), 'T123, T124, T125')
->addAction(pht('Subscribe'), '/dev/random')
->addAction(pht('Create Subtask'), '/dev/urandom')
->addTag($tag));
$elements[] = $panel;
$user_handle = $this->createBasicDummyHandle(
'gwashington',
PhabricatorPeopleUserPHIDType::TYPECONST,
'George Washington');
$user_handle->setImageURI(
celerity_get_resource_uri('/rsrc/image/people/washington.png'));
- $panel = $this->createPanel('Whatevery Hovercard');
+ $panel = $this->createPanel(pht('Whatevery Hovercard'));
$panel->appendChild(id(new PhabricatorHovercardView())
->setObjectHandle($user_handle)
- ->addField(pht('Status'), 'Available')
+ ->addField(pht('Status'), pht('Available'))
->addField(pht('Member since'), '30. February 1750')
->addAction(pht('Send a Message'), '/dev/null')
->setUser($user));
$elements[] = $panel;
return phutil_implode_html('', $elements);
}
private function createPanel($header) {
$panel = new PHUIBoxView();
$panel->addClass('grouped');
$panel->addClass('ml');
return $panel;
}
}
diff --git a/src/applications/uiexample/examples/PhabricatorListFilterUIExample.php b/src/applications/uiexample/examples/PhabricatorListFilterUIExample.php
index f346baf99..6c934a3e4 100644
--- a/src/applications/uiexample/examples/PhabricatorListFilterUIExample.php
+++ b/src/applications/uiexample/examples/PhabricatorListFilterUIExample.php
@@ -1,34 +1,35 @@
<?php
final class PhabricatorListFilterUIExample extends PhabricatorUIExample {
public function getName() {
- return 'ListFilter';
+ return pht('ListFilter');
}
public function getDescription() {
- return hsprintf(
- 'Use <tt>AphrontListFilterView</tt> to layout controls for filtering '.
- 'and manipulating lists of objects.');
+ return pht(
+ 'Use %s to layout controls for filtering '.
+ 'and manipulating lists of objects.',
+ phutil_tag('tt', array(), 'AphrontListFilterView'));
}
public function renderExample() {
$filter = new AphrontListFilterView();
$form = new AphrontFormView();
$form->setUser($this->getRequest()->getUser());
$form
->appendChild(
id(new AphrontFormTextControl())
- ->setLabel('Query'))
+ ->setLabel(pht('Query')))
->appendChild(
id(new AphrontFormSubmitControl())
- ->setValue('Search'));
+ ->setValue(pht('Search')));
$filter->appendChild($form);
return $filter;
}
}
diff --git a/src/applications/uiexample/examples/PhabricatorMultiColumnUIExample.php b/src/applications/uiexample/examples/PhabricatorMultiColumnUIExample.php
index 53c847d31..c34eb9028 100644
--- a/src/applications/uiexample/examples/PhabricatorMultiColumnUIExample.php
+++ b/src/applications/uiexample/examples/PhabricatorMultiColumnUIExample.php
@@ -1,225 +1,226 @@
<?php
final class PhabricatorMultiColumnUIExample extends PhabricatorUIExample {
public function getName() {
- return 'Multiple Column Layouts';
+ return pht('Multiple Column Layouts');
}
public function getDescription() {
- return 'A container good for 1-7 equally spaced columns. '.
- 'Fixed and Fluid layouts.';
+ return pht(
+ 'A container good for 1-7 equally spaced columns. '.
+ 'Fixed and Fluid layouts.');
}
public function renderExample() {
$request = $this->getRequest();
$user = $request->getUser();
$column1 = phutil_tag(
'div',
array(
'class' => 'pm',
'style' => 'border: 1px solid green;',
),
'Bruce Campbell');
$column2 = phutil_tag(
'div',
array(
'class' => 'pm',
'style' => 'border: 1px solid blue;',
),
'Army of Darkness');
$head1 = id(new PHUIHeaderView())
->setHeader(pht('2 Column Fixed'));
$layout1 = id(new AphrontMultiColumnView())
->addColumn($column1)
->addColumn($column2)
->setGutter(AphrontMultiColumnView::GUTTER_MEDIUM);
$head2 = id(new PHUIHeaderView())
->setHeader(pht('2 Column Fluid'));
$layout2 = id(new AphrontMultiColumnView())
->addColumn($column1)
->addColumn($column2)
->setFluidLayout(true)
->setGutter(AphrontMultiColumnView::GUTTER_MEDIUM);
$head3 = id(new PHUIHeaderView())
->setHeader(pht('4 Column Fixed'));
$layout3 = id(new AphrontMultiColumnView())
->addColumn($column1)
->addColumn($column2)
->addColumn($column1)
->addColumn($column2)
->setGutter(AphrontMultiColumnView::GUTTER_SMALL);
$head4 = id(new PHUIHeaderView())
->setHeader(pht('4 Column Fluid'));
$layout4 = id(new AphrontMultiColumnView())
->addColumn($column1)
->addColumn($column2)
->addColumn($column1)
->addColumn($column2)
->setFluidLayout(true)
->setGutter(AphrontMultiColumnView::GUTTER_SMALL);
$sunday = hsprintf('<strong>Sunday</strong><br /><br />Watch Football'.
'<br />Code<br />Eat<br />Sleep');
$monday = hsprintf('<strong>Monday</strong><br /><br />Code'.
'<br />Eat<br />Sleep');
$tuesday = hsprintf('<strong>Tuesday</strong><br />'.
'<br />Code<br />Eat<br />Sleep');
$wednesday = hsprintf('<strong>Wednesday</strong><br /><br />Code'.
'<br />Eat<br />Sleep');
$thursday = hsprintf('<strong>Thursday</strong><br />'.
'<br />Code<br />Eat<br />Sleep');
$friday = hsprintf('<strong>Friday</strong><br /><br />Code'.
'<br />Eat<br />Sleep');
$saturday = hsprintf('<strong>Saturday</strong><br /><br />StarCraft II'.
'<br />All<br />Damn<br />Day');
$head5 = id(new PHUIHeaderView())
->setHeader(pht('7 Column Fluid'));
$layout5 = id(new AphrontMultiColumnView())
->addColumn($sunday)
->addColumn($monday)
->addColumn($tuesday)
->addColumn($wednesday)
->addColumn($thursday)
->addColumn($friday)
->addColumn($saturday)
->setFluidLayout(true)
->setBorder(true);
$shipping = id(new PHUIFormLayoutView())
->setUser($user)
->setFullWidth(true)
->appendChild(
id(new AphrontFormTextControl())
- ->setLabel('Name')
- ->setDisableAutocomplete(true)
+ ->setLabel(pht('Name'))
+ ->setDisableAu4tocomplete(true)
->setSigil('name-input'))
->appendChild(
id(new AphrontFormTextControl())
- ->setLabel('Address')
+ ->setLabel(pht('Address'))
->setDisableAutocomplete(true)
->setSigil('address-input'))
->appendChild(
id(new AphrontFormTextControl())
- ->setLabel('City/State')
+ ->setLabel(pht('City/State'))
->setDisableAutocomplete(true)
->setSigil('city-input'))
->appendChild(
id(new AphrontFormTextControl())
- ->setLabel('Country')
+ ->setLabel(pht('Country'))
->setDisableAutocomplete(true)
->setSigil('country-input'))
->appendChild(
id(new AphrontFormTextControl())
- ->setLabel('Postal Code')
+ ->setLabel(pht('Postal Code'))
->setDisableAutocomplete(true)
->setSigil('postal-input'));
$cc = id(new PHUIFormLayoutView())
->setUser($user)
->setFullWidth(true)
->appendChild(
id(new AphrontFormTextControl())
- ->setLabel('Card Number')
+ ->setLabel(pht('Card Number'))
->setDisableAutocomplete(true)
->setSigil('number-input')
->setError(''))
->appendChild(
id(new AphrontFormTextControl())
- ->setLabel('CVC')
+ ->setLabel(pht('CVC'))
->setDisableAutocomplete(true)
->setSigil('cvc-input')
->setError(''))
->appendChild(
id(new PhortuneMonthYearExpiryControl())
- ->setLabel('Expiration')
+ ->setLabel(pht('Expiration'))
->setUser($user)
->setError(''));
$shipping_title = pht('Shipping Address');
$billing_title = pht('Billing Address');
$cc_title = pht('Payment Information');
$head6 = id(new PHUIHeaderView())
- ->setHeader(pht('Let\'s Go Shopping'));
+ ->setHeader(pht("Let's Go Shopping"));
$layout6 = id(new AphrontMultiColumnView())
->addColumn(hsprintf('<h1>%s</h1>%s', $shipping_title, $shipping))
->addColumn(hsprintf('<h1>%s</h1>%s', $billing_title, $shipping))
->addColumn(hsprintf('<h1>%s</h1>%s', $cc_title, $cc))
->setFluidLayout(true)
->setBorder(true);
$wrap1 = phutil_tag(
'div',
array(
'class' => 'ml',
),
$layout1);
$wrap2 = phutil_tag(
'div',
array(
'class' => 'ml',
),
$layout2);
$wrap3 = phutil_tag(
'div',
array(
'class' => 'ml',
),
$layout3);
$wrap4 = phutil_tag(
'div',
array(
'class' => 'ml',
),
$layout4);
$wrap5 = phutil_tag(
'div',
array(
'class' => 'ml',
),
$layout5);
$wrap6 = phutil_tag(
'div',
array(
'class' => 'ml',
),
$layout6);
return phutil_tag(
'div',
array(),
array(
$head1,
$wrap1,
$head2,
$wrap2,
$head3,
$wrap3,
$head4,
$wrap4,
$head5,
$wrap5,
$head6,
$wrap6,
));
}
}
diff --git a/src/applications/uiexample/examples/PhabricatorNotificationUIExample.php b/src/applications/uiexample/examples/PhabricatorNotificationUIExample.php
index c9d0d85e5..1b825693f 100644
--- a/src/applications/uiexample/examples/PhabricatorNotificationUIExample.php
+++ b/src/applications/uiexample/examples/PhabricatorNotificationUIExample.php
@@ -1,30 +1,31 @@
<?php
final class PhabricatorNotificationUIExample extends PhabricatorUIExample {
public function getName() {
- return 'Notifications';
+ return pht('Notifications');
}
public function getDescription() {
- return hsprintf('Use <tt>JX.Notification</tt> to create notifications.');
+ return pht(
+ 'Use %s to create notifications.',
+ phutil_tag('tt', array(), 'JX.Notification'));
}
public function renderExample() {
-
require_celerity_resource('phabricator-notification-css');
Javelin::initBehavior('phabricator-notification-example');
$content = javelin_tag(
'a',
array(
'sigil' => 'notification-example',
'class' => 'button green',
),
- 'Show Notification');
+ pht('Show Notification'));
$content = hsprintf('<div style="padding: 1em 3em;">%s</div>', $content);
return $content;
}
}
diff --git a/src/applications/uiexample/examples/PhabricatorPagerUIExample.php b/src/applications/uiexample/examples/PhabricatorPagerUIExample.php
index cb5212d04..e2e938a30 100644
--- a/src/applications/uiexample/examples/PhabricatorPagerUIExample.php
+++ b/src/applications/uiexample/examples/PhabricatorPagerUIExample.php
@@ -1,80 +1,81 @@
<?php
final class PhabricatorPagerUIExample extends PhabricatorUIExample {
public function getName() {
- return 'Pager';
+ return pht('Pager');
}
public function getDescription() {
- return hsprintf(
- 'Use <tt>AphrontPagerView</tt> to create a control which allows '.
- 'users to paginate through large amounts of content.');
+ return pht(
+ 'Use %s to create a control which allows '.
+ 'users to paginate through large amounts of content.',
+ phutil_tag('tt', array(), 'AphrontPagerView'));
}
public function renderExample() {
-
$request = $this->getRequest();
$offset = (int)$request->getInt('offset');
$page_size = 20;
$item_count = 173;
$rows = array();
for ($ii = $offset; $ii < min($item_count, $offset + $page_size); $ii++) {
$rows[] = array(
'Item #'.($ii + 1),
);
}
$table = new AphrontTableView($rows);
$table->setHeaders(
array(
'Item',
));
$panel = new PHUIObjectBoxView();
$panel->setHeaderText(pht('Example'));
$panel->appendChild($table);
$panel->appendChild(hsprintf(
- '<p class="phabricator-ui-example-note">'.
- 'Use <tt>AphrontPagerView</tt> to render a pager element.'.
- '</p>'));
+ '<p class="phabricator-ui-example-note">%s</p>',
+ pht(
+ 'Use %s to render a pager element.',
+ phutil_tag('tt', array(), 'AphrontPagerView'))));
$pager = new AphrontPagerView();
$pager->setPageSize($page_size);
$pager->setOffset($offset);
$pager->setCount($item_count);
$pager->setURI($request->getRequestURI(), 'offset');
$panel->appendChild($pager);
$panel->appendChild(hsprintf(
- '<p class="phabricator-ui-example-note">'.
- 'You can show more or fewer pages of surrounding context.'.
- '</p>'));
+ '<p class="phabricator-ui-example-note">%s</p>',
+ pht('You can show more or fewer pages of surrounding context.')));
$many_pages_pager = new AphrontPagerView();
$many_pages_pager->setPageSize($page_size);
$many_pages_pager->setOffset($offset);
$many_pages_pager->setCount($item_count);
$many_pages_pager->setURI($request->getRequestURI(), 'offset');
$many_pages_pager->setSurroundingPages(7);
$panel->appendChild($many_pages_pager);
$panel->appendChild(hsprintf(
- '<p class="phabricator-ui-example-note">'.
+ '<p class="phabricator-ui-example-note">%s</p>',
+ pht(
'When it is prohibitively expensive or complex to attain a complete '.
'count of the items, you can select one extra item and set '.
- '<tt>hasMorePages(true)</tt> if it exists, creating an inexact pager.'.
- '</p>'));
+ '%s if it exists, creating an inexact pager.',
+ phutil_tag('tt', array(), 'hasMorePages(true)'))));
$inexact_pager = new AphrontPagerView();
$inexact_pager->setPageSize($page_size);
$inexact_pager->setOffset($offset);
$inexact_pager->setHasMorePages($offset < ($item_count - $page_size));
$inexact_pager->setURI($request->getRequestURI(), 'offset');
$panel->appendChild($inexact_pager);
return $panel;
}
}
diff --git a/src/applications/uiexample/examples/PhabricatorSetupIssueUIExample.php b/src/applications/uiexample/examples/PhabricatorSetupIssueUIExample.php
index a166a3dc8..e5370d30c 100644
--- a/src/applications/uiexample/examples/PhabricatorSetupIssueUIExample.php
+++ b/src/applications/uiexample/examples/PhabricatorSetupIssueUIExample.php
@@ -1,38 +1,38 @@
<?php
final class PhabricatorSetupIssueUIExample extends PhabricatorUIExample {
public function getName() {
- return 'Setup Issue';
+ return pht('Setup Issue');
}
public function getDescription() {
- return 'Setup errors and warnings.';
+ return pht('Setup errors and warnings.');
}
public function renderExample() {
$request = $this->getRequest();
$user = $request->getUser();
$issue = id(new PhabricatorSetupIssue())
->setShortName(pht('Short Name'))
->setName(pht('Name'))
->setSummary(pht('Summary'))
->setMessage(pht('Message'))
->setIssueKey('example.key')
->addCommand('$ # Add Command')
->addCommand(hsprintf('<tt>$</tt> %s', '$ ls -1 > /dev/null'))
->addPHPConfig('php.config.example')
->addPhabricatorConfig('test.value')
->addPHPExtension('libexample');
// NOTE: Since setup issues may be rendered before we can build the page
// chrome, they don't explicitly include resources.
require_celerity_resource('setup-issue-css');
$view = id(new PhabricatorSetupIssueView())
->setIssue($issue);
return $view;
}
}
diff --git a/src/applications/uiexample/examples/PhabricatorSortTableUIExample.php b/src/applications/uiexample/examples/PhabricatorSortTableUIExample.php
index 7ed2cdce2..791171081 100644
--- a/src/applications/uiexample/examples/PhabricatorSortTableUIExample.php
+++ b/src/applications/uiexample/examples/PhabricatorSortTableUIExample.php
@@ -1,96 +1,96 @@
<?php
final class PhabricatorSortTableUIExample extends PhabricatorUIExample {
public function getName() {
- return 'Sortable Tables';
+ return pht('Sortable Tables');
}
public function getDescription() {
- return 'Using sortable tables.';
+ return pht('Using sortable tables.');
}
public function renderExample() {
$rows = array(
array(
'make' => 'Honda',
'model' => 'Civic',
'year' => 2004,
'price' => 3199,
'color' => 'Blue',
),
array(
'make' => 'Ford',
'model' => 'Focus',
'year' => 2001,
'price' => 2549,
'color' => 'Red',
),
array(
'make' => 'Toyota',
'model' => 'Camry',
'year' => 2009,
'price' => 4299,
'color' => 'Black',
),
array(
'make' => 'NASA',
'model' => 'Shuttle',
'year' => 1998,
'price' => 1000000000,
'color' => 'White',
),
);
$request = $this->getRequest();
$orders = array(
'make',
'model',
'year',
'price',
);
$sort = $request->getStr('sort');
list($sort, $reverse) = AphrontTableView::parseSort($sort);
if (!in_array($sort, $orders)) {
$sort = 'make';
}
$rows = isort($rows, $sort);
if ($reverse) {
$rows = array_reverse($rows);
}
$table = new AphrontTableView($rows);
$table->setHeaders(
array(
- 'Make',
- 'Model',
- 'Year',
- 'Price',
- 'Color',
+ pht('Make'),
+ pht('Model'),
+ pht('Year'),
+ pht('Price'),
+ pht('Color'),
));
$table->setColumnClasses(
array(
'',
'wide',
'n',
'n',
'',
));
$table->makeSortable(
$request->getRequestURI(),
'sort',
$sort,
$reverse,
$orders);
$panel = new PHUIObjectBoxView();
- $panel->setHeaderText('Sortable Table of Vehicles');
+ $panel->setHeaderText(pht('Sortable Table of Vehicles'));
$panel->appendChild($table);
return $panel;
}
}
diff --git a/src/applications/uiexample/examples/PhabricatorStatusUIExample.php b/src/applications/uiexample/examples/PhabricatorStatusUIExample.php
index 16961885b..6583bc584 100644
--- a/src/applications/uiexample/examples/PhabricatorStatusUIExample.php
+++ b/src/applications/uiexample/examples/PhabricatorStatusUIExample.php
@@ -1,92 +1,92 @@
<?php
final class PhabricatorStatusUIExample extends PhabricatorUIExample {
public function getName() {
- return 'Status List';
+ return pht('Status List');
}
public function getDescription() {
- return hsprintf(
- 'Use <tt>PHUIStatusListView</tt> to show relationships with objects.');
+ return pht(
+ 'Use %s to show relationships with objects.',
+ phutil_tag('tt', array(), 'PHUIStatusListView'));
}
public function renderExample() {
-
$out = array();
$view = new PHUIStatusListView();
$view->addItem(
id(new PHUIStatusItemView())
->setIcon(PHUIStatusItemView::ICON_ACCEPT, 'green', pht('Yum'))
->setTarget(pht('Apple'))
->setNote(pht('You can eat them.')));
$view->addItem(
id(new PHUIStatusItemView())
->setIcon(PHUIStatusItemView::ICON_ADD, 'blue', pht('Has Peel'))
->setTarget(pht('Banana'))
->setNote(pht('Comes in bunches.'))
->setHighlighted(true));
$view->addItem(
id(new PHUIStatusItemView())
->setIcon(PHUIStatusItemView::ICON_WARNING, 'dark', pht('Caution'))
->setTarget(pht('Pomegranite'))
->setNote(pht('Lots of seeds. Watch out.')));
$view->addItem(
id(new PHUIStatusItemView())
->setIcon(PHUIStatusItemView::ICON_REJECT, 'red', pht('Bleh!'))
->setTarget(pht('Zucchini'))
->setNote(pht('Slimy and gross. Yuck!')));
$out[] = id(new PHUIHeaderView())
->setHeader(pht('Fruit and Vegetable Status'));
$out[] = id(new PHUIBoxView())
->addMargin(PHUI::MARGIN_LARGE)
->addPadding(PHUI::PADDING_LARGE)
->setBorder(true)
->appendChild($view);
$view = new PHUIStatusListView();
$manifest = array(
PHUIStatusItemView::ICON_ACCEPT => 'PHUIStatusItemView::ICON_ACCEPT',
PHUIStatusItemView::ICON_REJECT => 'PHUIStatusItemView::ICON_REJECT',
PHUIStatusItemView::ICON_LEFT => 'PHUIStatusItemView::ICON_LEFT',
PHUIStatusItemView::ICON_RIGHT => 'PHUIStatusItemView::ICON_RIGHT',
PHUIStatusItemView::ICON_UP => 'PHUIStatusItemView::ICON_UP',
PHUIStatusItemView::ICON_DOWN => 'PHUIStatusItemView::ICON_DOWN',
PHUIStatusItemView::ICON_QUESTION => 'PHUIStatusItemView::ICON_QUESTION',
PHUIStatusItemView::ICON_WARNING => 'PHUIStatusItemView::ICON_WARNING',
PHUIStatusItemView::ICON_INFO => 'PHUIStatusItemView::ICON_INFO',
PHUIStatusItemView::ICON_ADD => 'PHUIStatusItemView::ICON_ADD',
PHUIStatusItemView::ICON_MINUS => 'PHUIStatusItemView::ICON_MINUS',
PHUIStatusItemView::ICON_OPEN => 'PHUIStatusItemView::ICON_OPEN',
PHUIStatusItemView::ICON_CLOCK => 'PHUIStatusItemView::ICON_CLOCK',
);
foreach ($manifest as $icon => $label) {
$view->addItem(
id(new PHUIStatusItemView())
->setIcon($icon, 'indigo')
->setTarget($label));
}
$out[] = id(new PHUIHeaderView())
->setHeader(pht('All Icons'));
$out[] = id(new PHUIBoxView())
->addMargin(PHUI::MARGIN_LARGE)
->addPadding(PHUI::PADDING_LARGE)
->setBorder(true)
->appendChild($view);
return $out;
}
}
diff --git a/src/applications/uiexample/examples/PhabricatorTooltipUIExample.php b/src/applications/uiexample/examples/PhabricatorTooltipUIExample.php
index d93aa3a09..373161612 100644
--- a/src/applications/uiexample/examples/PhabricatorTooltipUIExample.php
+++ b/src/applications/uiexample/examples/PhabricatorTooltipUIExample.php
@@ -1,100 +1,102 @@
<?php
final class PhabricatorTooltipUIExample extends PhabricatorUIExample {
public function getName() {
- return 'Tooltips';
+ return pht('Tooltips');
}
public function getDescription() {
- return hsprintf('Use <tt>JX.Tooltip</tt> to create tooltips.');
+ return pht(
+ 'Use %s to create tooltips.',
+ phutil_tag('tt', array(), 'JX.Tooltip'));
}
public function renderExample() {
Javelin::initBehavior('phabricator-tooltips');
require_celerity_resource('aphront-tooltip-css');
$style = 'width: 200px; '.
'text-align: center; '.
'margin: 20px; '.
'background: #dfdfdf; '.
'padding: 20px 10px; '.
'border: 1px solid black; ';
$lorem = <<<EOTEXT
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
EOTEXT;
$overflow = str_repeat('M', 1024);
$metas = array(
'hi' => array(
'tip' => 'Hi',
),
'lorem (north)' => array(
'tip' => $lorem,
),
'lorem (east)' => array(
'tip' => $lorem,
'align' => 'E',
),
'lorem (south)' => array(
'tip' => $lorem,
'align' => 'S',
),
'lorem (west)' => array(
'tip' => $lorem,
'align' => 'W',
),
'lorem (large, north)' => array(
'tip' => $lorem,
'size' => 300,
),
'lorem (large, east)' => array(
'tip' => $lorem,
'size' => 300,
'align' => 'E',
),
'lorem (large, west)' => array(
'tip' => $lorem,
'size' => 300,
'align' => 'W',
),
'lorem (large, south)' => array(
'tip' => $lorem,
'size' => 300,
'align' => 'S',
),
'overflow (north)' => array(
'tip' => $overflow,
),
'overflow (east)' => array(
'tip' => $overflow,
'align' => 'E',
),
'overflow (south)' => array(
'tip' => $overflow,
'align' => 'S',
),
'overflow (west)' => array(
'tip' => $overflow,
'align' => 'W',
),
);
$content = array();
foreach ($metas as $key => $meta) {
$content[] = javelin_tag(
'div',
array(
'sigil' => 'has-tooltip',
'meta' => $meta,
'style' => $style,
),
$key);
}
return $content;
}
}
diff --git a/src/applications/uiexample/examples/PhabricatorTwoColumnUIExample.php b/src/applications/uiexample/examples/PhabricatorTwoColumnUIExample.php
index a77b06870..fd9b422a2 100644
--- a/src/applications/uiexample/examples/PhabricatorTwoColumnUIExample.php
+++ b/src/applications/uiexample/examples/PhabricatorTwoColumnUIExample.php
@@ -1,36 +1,36 @@
<?php
final class PhabricatorTwoColumnUIExample extends PhabricatorUIExample {
public function getName() {
- return 'Two Column Layout';
+ return pht('Two Column Layout');
}
public function getDescription() {
- return 'Two Column mobile friendly layout';
+ return pht('Two Column mobile friendly layout');
}
public function renderExample() {
$main = phutil_tag(
'div',
array(
'style' => 'border: 1px solid blue; padding: 20px;',
),
'Mary, mary quite contrary.');
$side = phutil_tag(
'div',
array(
'style' => 'border: 1px solid red; padding: 20px;',
),
'How does your garden grow?');
$content = id(new AphrontTwoColumnView())
->setMainColumn($main)
->setSideColumn($side);
return $content;
}
}
diff --git a/src/applications/uiexample/examples/PhabricatorUIExample.php b/src/applications/uiexample/examples/PhabricatorUIExample.php
index 0afe5447a..2f2ffe0b5 100644
--- a/src/applications/uiexample/examples/PhabricatorUIExample.php
+++ b/src/applications/uiexample/examples/PhabricatorUIExample.php
@@ -1,45 +1,46 @@
<?php
abstract class PhabricatorUIExample {
private $request;
public function setRequest($request) {
$this->request = $request;
return $this;
}
public function getRequest() {
return $this->request;
}
abstract public function getName();
abstract public function getDescription();
abstract public function renderExample();
protected function createBasicDummyHandle($name, $type, $fullname = null,
$uri = null) {
$id = mt_rand(15, 9999);
$handle = new PhabricatorObjectHandle();
$handle->setName($name);
$handle->setType($type);
$handle->setPHID(PhabricatorPHID::generateNewPHID($type));
if ($fullname) {
$handle->setFullName($fullname);
} else {
- $handle->setFullName(sprintf('%s%d: %s',
- substr($type, 0, 1),
- $id,
- $name));
+ $handle->setFullName(
+ sprintf('%s%d: %s',
+ substr($type, 0, 1),
+ $id,
+ $name));
}
if ($uri) {
$handle->setURI($uri);
}
return $handle;
}
}
diff --git a/src/applications/xhprof/view/PhabricatorXHProfProfileTopLevelView.php b/src/applications/xhprof/view/PhabricatorXHProfProfileTopLevelView.php
index e332ca4d2..172e4d344 100644
--- a/src/applications/xhprof/view/PhabricatorXHProfProfileTopLevelView.php
+++ b/src/applications/xhprof/view/PhabricatorXHProfProfileTopLevelView.php
@@ -1,141 +1,143 @@
<?php
/**
* @phutil-external-symbol function xhprof_compute_flat_info
*/
final class PhabricatorXHProfProfileTopLevelView
extends PhabricatorXHProfProfileView {
private $profileData;
private $limit;
private $file;
public function setProfileData(array $data) {
$this->profileData = $data;
return $this;
}
public function setLimit($limit) {
$this->limit = $limit;
return $this;
}
public function setFile(PhabricatorFile $file) {
$this->file = $file;
return $this;
}
public function render() {
DarkConsoleXHProfPluginAPI::includeXHProfLib();
$GLOBALS['display_calls'] = true;
$totals = array();
$flat = xhprof_compute_flat_info($this->profileData, $totals);
unset($GLOBALS['display_calls']);
$aggregated = array();
foreach ($flat as $call => $counters) {
$parts = explode('@', $call, 2);
$agg_call = reset($parts);
if (empty($aggregated[$agg_call])) {
$aggregated[$agg_call] = $counters;
} else {
foreach ($aggregated[$agg_call] as $key => $val) {
if ($key != 'wt') {
$aggregated[$agg_call][$key] += $counters[$key];
}
}
}
}
$flat = $aggregated;
$flat = isort($flat, 'wt');
$flat = array_reverse($flat);
$rows = array();
$rows[] = array(
pht('Total'),
number_format($totals['ct']),
number_format($totals['wt']).' us',
'100.0%',
number_format($totals['wt']).' us',
'100.0%',
);
if ($this->limit) {
$flat = array_slice($flat, 0, $this->limit);
}
foreach ($flat as $call => $counters) {
$rows[] = array(
$this->renderSymbolLink($call),
number_format($counters['ct']),
number_format($counters['wt']).' us',
sprintf('%.1f%%', 100 * $counters['wt'] / $totals['wt']),
number_format($counters['excl_wt']).' us',
sprintf('%.1f%%', 100 * $counters['excl_wt'] / $totals['wt']),
);
}
Javelin::initBehavior('phabricator-tooltips');
$table = new AphrontTableView($rows);
$table->setHeaders(
array(
pht('Symbol'),
pht('Count'),
javelin_tag(
'span',
array(
'sigil' => 'has-tooltip',
'meta' => array(
- 'tip' => pht('Total wall time spent in this function and all of '.
- 'its children (children are other functions it called '.
- 'while executing).'),
+ 'tip' => pht(
+ 'Total wall time spent in this function and all of '.
+ 'its children (children are other functions it called '.
+ 'while executing).'),
'size' => 200,
),
),
- 'Wall Time (Inclusive)'),
+ pht('Wall Time (Inclusive)')),
'%',
javelin_tag(
'span',
array(
'sigil' => 'has-tooltip',
'meta' => array(
- 'tip' => pht('Wall time spent in this function, excluding time '.
- 'spent in children (children are other functions it '.
- 'called while executing).'),
+ 'tip' => pht(
+ 'Wall time spent in this function, excluding time '.
+ 'spent in children (children are other functions it '.
+ 'called while executing).'),
'size' => 200,
),
),
- 'Wall Time (Exclusive)'),
+ pht('Wall Time (Exclusive)')),
'%',
));
$table->setColumnClasses(
array(
'wide pri',
'n',
'n',
'n',
'n',
'n',
));
$panel = new PHUIObjectBoxView();
$header = id(new PHUIHeaderView())
->setHeader(pht('XHProf Profile'));
if ($this->file) {
$button = id(new PHUIButtonView())
->setHref($this->file->getBestURI())
- ->setText(pht('Download .xhprof Profile'))
+ ->setText(pht('Download %s Profile', '.xhprof'))
->setTag('a');
$header->addActionLink($button);
}
$panel->setHeader($header);
$panel->appendChild($table);
return $panel->render();
}
}
diff --git a/src/infrastructure/customfield/exception/PhabricatorCustomFieldDataNotAvailableException.php b/src/infrastructure/customfield/exception/PhabricatorCustomFieldDataNotAvailableException.php
index 26141dc50..c1bc74d36 100644
--- a/src/infrastructure/customfield/exception/PhabricatorCustomFieldDataNotAvailableException.php
+++ b/src/infrastructure/customfield/exception/PhabricatorCustomFieldDataNotAvailableException.php
@@ -1,16 +1,15 @@
<?php
-final class PhabricatorCustomFieldDataNotAvailableException
- extends Exception {
+final class PhabricatorCustomFieldDataNotAvailableException extends Exception {
public function __construct(PhabricatorCustomField $field) {
- $key = $field->getFieldKey();
- $name = $field->getFieldName();
- $class = get_class($field);
-
parent::__construct(
- "Custom field '{$name}' (with key '{$key}', of class '{$class}') is ".
- "attempting to access data which is not available in this context.");
+ pht(
+ "Custom field '%s' (with key '%s', of class '%s') is attempting ".
+ "to access data which is not available in this context.",
+ $field->getFieldName(),
+ $field->getFieldKey(),
+ get_class($field)));
}
}
diff --git a/src/infrastructure/customfield/exception/PhabricatorCustomFieldNotProxyException.php b/src/infrastructure/customfield/exception/PhabricatorCustomFieldNotProxyException.php
index 585f31c41..6fd2f93be 100644
--- a/src/infrastructure/customfield/exception/PhabricatorCustomFieldNotProxyException.php
+++ b/src/infrastructure/customfield/exception/PhabricatorCustomFieldNotProxyException.php
@@ -1,17 +1,18 @@
<?php
-final class PhabricatorCustomFieldNotProxyException
- extends Exception {
+final class PhabricatorCustomFieldNotProxyException extends Exception {
public function __construct(PhabricatorCustomField $field) {
- $key = $field->getFieldKey();
- $name = $field->getFieldName();
- $class = get_class($field);
-
parent::__construct(
- "Custom field '{$name}' (with key '{$key}', of class '{$class}') can ".
- "not have a proxy set with setProxy(), because it returned false from ".
- "canSetProxy().");
+ pht(
+ "Custom field '%s' (with key '%s', of class '%s') can not have a ".
+ "proxy set with %s, because it returned %s from %s.",
+ $field->getFieldName(),
+ $field->getFieldKey(),
+ get_class($field),
+ 'setProxy()',
+ 'false',
+ 'canSetProxy()'));
}
}
diff --git a/src/infrastructure/customfield/field/PhabricatorCustomFieldList.php b/src/infrastructure/customfield/field/PhabricatorCustomFieldList.php
index 15b83928e..160a61839 100644
--- a/src/infrastructure/customfield/field/PhabricatorCustomFieldList.php
+++ b/src/infrastructure/customfield/field/PhabricatorCustomFieldList.php
@@ -1,345 +1,349 @@
<?php
/**
* Convenience class to perform operations on an entire field list, like reading
* all values from storage.
*
* $field_list = new PhabricatorCustomFieldList($fields);
*
*/
final class PhabricatorCustomFieldList extends Phobject {
private $fields;
private $viewer;
public function __construct(array $fields) {
assert_instances_of($fields, 'PhabricatorCustomField');
$this->fields = $fields;
}
public function getFields() {
return $this->fields;
}
public function setViewer(PhabricatorUser $viewer) {
$this->viewer = $viewer;
foreach ($this->getFields() as $field) {
$field->setViewer($viewer);
}
return $this;
}
/**
* Read stored values for all fields which support storage.
*
* @param PhabricatorCustomFieldInterface Object to read field values for.
* @return void
*/
public function readFieldsFromStorage(
PhabricatorCustomFieldInterface $object) {
foreach ($this->fields as $field) {
$field->setObject($object);
$field->readValueFromObject($object);
}
$keys = array();
foreach ($this->fields as $field) {
if ($field->shouldEnableForRole(PhabricatorCustomField::ROLE_STORAGE)) {
$keys[$field->getFieldIndex()] = $field;
}
}
if (!$keys) {
return $this;
}
// NOTE: We assume all fields share the same storage. This isn't guaranteed
// to be true, but always is for now.
$table = head($keys)->newStorageObject();
$objects = array();
if ($object->getPHID()) {
$objects = $table->loadAllWhere(
'objectPHID = %s AND fieldIndex IN (%Ls)',
$object->getPHID(),
array_keys($keys));
$objects = mpull($objects, null, 'getFieldIndex');
}
foreach ($keys as $key => $field) {
$storage = idx($objects, $key);
if ($storage) {
$field->setValueFromStorage($storage->getFieldValue());
} else if ($object->getPHID()) {
// NOTE: We set this only if the object exists. Otherwise, we allow the
// field to retain any default value it may have.
$field->setValueFromStorage(null);
}
}
return $this;
}
public function appendFieldsToForm(AphrontFormView $form) {
$enabled = array();
foreach ($this->fields as $field) {
if ($field->shouldEnableForRole(PhabricatorCustomField::ROLE_EDIT)) {
$enabled[] = $field;
}
}
$phids = array();
foreach ($enabled as $field_key => $field) {
$phids[$field_key] = $field->getRequiredHandlePHIDsForEdit();
}
$all_phids = array_mergev($phids);
if ($all_phids) {
$handles = id(new PhabricatorHandleQuery())
->setViewer($this->viewer)
->withPHIDs($all_phids)
->execute();
} else {
$handles = array();
}
foreach ($enabled as $field_key => $field) {
$field_handles = array_select_keys($handles, $phids[$field_key]);
$instructions = $field->getInstructionsForEdit();
if (strlen($instructions)) {
$form->appendRemarkupInstructions($instructions);
}
$form->appendChild($field->renderEditControl($field_handles));
}
}
public function appendFieldsToPropertyList(
PhabricatorCustomFieldInterface $object,
PhabricatorUser $viewer,
PHUIPropertyListView $view) {
$this->readFieldsFromStorage($object);
$fields = $this->fields;
foreach ($fields as $field) {
$field->setViewer($viewer);
}
// Move all the blocks to the end, regardless of their configuration order,
// because it always looks silly to render a block in the middle of a list
// of properties.
$head = array();
$tail = array();
foreach ($fields as $key => $field) {
$style = $field->getStyleForPropertyView();
switch ($style) {
case 'property':
case 'header':
$head[$key] = $field;
break;
case 'block':
$tail[$key] = $field;
break;
default:
throw new Exception(
- "Unknown field property view style '{$style}'; valid styles are ".
- "'block' and 'property'.");
+ pht(
+ "Unknown field property view style '%s'; valid styles are ".
+ "'%s' and '%s'.",
+ $style,
+ 'block',
+ 'property'));
}
}
$fields = $head + $tail;
$add_header = null;
$phids = array();
foreach ($fields as $key => $field) {
$phids[$key] = $field->getRequiredHandlePHIDsForPropertyView();
}
$all_phids = array_mergev($phids);
if ($all_phids) {
$handles = id(new PhabricatorHandleQuery())
->setViewer($viewer)
->withPHIDs($all_phids)
->execute();
} else {
$handles = array();
}
foreach ($fields as $key => $field) {
$field_handles = array_select_keys($handles, $phids[$key]);
$label = $field->renderPropertyViewLabel();
$value = $field->renderPropertyViewValue($field_handles);
if ($value !== null) {
switch ($field->getStyleForPropertyView()) {
case 'header':
// We want to hide headers if the fields the're assciated with
// don't actually produce any visible properties. For example, in a
// list like this:
//
// Header A
// Prop A: Value A
// Header B
// Prop B: Value B
//
// ...if the "Prop A" field returns `null` when rendering its
// property value and we rendered naively, we'd get this:
//
// Header A
// Header B
// Prop B: Value B
//
// This is silly. Instead, we hide "Header A".
$add_header = $value;
break;
case 'property':
if ($add_header !== null) {
// Add the most recently seen header.
$view->addSectionHeader($add_header);
$add_header = null;
}
$view->addProperty($label, $value);
break;
case 'block':
$icon = $field->getIconForPropertyView();
$view->invokeWillRenderEvent();
if ($label !== null) {
$view->addSectionHeader($label, $icon);
}
$view->addTextContent($value);
break;
}
}
}
}
public function buildFieldTransactionsFromRequest(
PhabricatorApplicationTransaction $template,
AphrontRequest $request) {
$xactions = array();
$role = PhabricatorCustomField::ROLE_APPLICATIONTRANSACTIONS;
foreach ($this->fields as $field) {
if (!$field->shouldEnableForRole($role)) {
continue;
}
$transaction_type = $field->getApplicationTransactionType();
$xaction = id(clone $template)
->setTransactionType($transaction_type);
if ($transaction_type == PhabricatorTransactions::TYPE_CUSTOMFIELD) {
// For TYPE_CUSTOMFIELD transactions only, we provide the old value
// as an input.
$old_value = $field->getOldValueForApplicationTransactions();
$xaction->setOldValue($old_value);
}
$field->readValueFromRequest($request);
$xaction
->setNewValue($field->getNewValueForApplicationTransactions());
if ($transaction_type == PhabricatorTransactions::TYPE_CUSTOMFIELD) {
// For TYPE_CUSTOMFIELD transactions, add the field key in metadata.
$xaction->setMetadataValue('customfield:key', $field->getFieldKey());
}
$metadata = $field->getApplicationTransactionMetadata();
foreach ($metadata as $key => $value) {
$xaction->setMetadataValue($key, $value);
}
$xactions[] = $xaction;
}
return $xactions;
}
/**
* Publish field indexes into index tables, so ApplicationSearch can search
* them.
*
* @return void
*/
public function rebuildIndexes(PhabricatorCustomFieldInterface $object) {
$indexes = array();
$index_keys = array();
$phid = $object->getPHID();
$role = PhabricatorCustomField::ROLE_APPLICATIONSEARCH;
foreach ($this->fields as $field) {
if (!$field->shouldEnableForRole($role)) {
continue;
}
$index_keys[$field->getFieldIndex()] = true;
foreach ($field->buildFieldIndexes() as $index) {
$index->setObjectPHID($phid);
$indexes[$index->getTableName()][] = $index;
}
}
if (!$indexes) {
return;
}
$any_index = head(head($indexes));
$conn_w = $any_index->establishConnection('w');
foreach ($indexes as $table => $index_list) {
$sql = array();
foreach ($index_list as $index) {
$sql[] = $index->formatForInsert($conn_w);
}
$indexes[$table] = $sql;
}
$any_index->openTransaction();
foreach ($indexes as $table => $sql_list) {
queryfx(
$conn_w,
'DELETE FROM %T WHERE objectPHID = %s AND indexKey IN (%Ls)',
$table,
$phid,
array_keys($index_keys));
if (!$sql_list) {
continue;
}
foreach (PhabricatorLiskDAO::chunkSQL($sql_list) as $chunk) {
queryfx(
$conn_w,
'INSERT INTO %T (objectPHID, indexKey, indexValue) VALUES %Q',
$table,
$chunk);
}
}
$any_index->saveTransaction();
}
public function updateAbstractDocument(
PhabricatorSearchAbstractDocument $document) {
$role = PhabricatorCustomField::ROLE_GLOBALSEARCH;
foreach ($this->getFields() as $field) {
if (!$field->shouldEnableForRole($role)) {
continue;
}
$field->updateAbstractDocument($document);
}
}
}
diff --git a/src/infrastructure/customfield/storage/PhabricatorCustomFieldIndexStorage.php b/src/infrastructure/customfield/storage/PhabricatorCustomFieldIndexStorage.php
index 18024ec17..d16be819f 100644
--- a/src/infrastructure/customfield/storage/PhabricatorCustomFieldIndexStorage.php
+++ b/src/infrastructure/customfield/storage/PhabricatorCustomFieldIndexStorage.php
@@ -1,19 +1,18 @@
<?php
-abstract class PhabricatorCustomFieldIndexStorage
- extends PhabricatorLiskDAO {
+abstract class PhabricatorCustomFieldIndexStorage extends PhabricatorLiskDAO {
protected $objectPHID;
protected $indexKey;
protected $indexValue;
protected function getConfiguration() {
return array(
self::CONFIG_TIMESTAMPS => false,
) + parent::getConfiguration();
}
abstract public function formatForInsert(AphrontDatabaseConnection $conn);
abstract public function getIndexValueType();
}
diff --git a/src/infrastructure/daemon/bot/PhabricatorBot.php b/src/infrastructure/daemon/bot/PhabricatorBot.php
index ef949cb9f..b79767777 100644
--- a/src/infrastructure/daemon/bot/PhabricatorBot.php
+++ b/src/infrastructure/daemon/bot/PhabricatorBot.php
@@ -1,164 +1,168 @@
<?php
/**
* Simple IRC bot which runs as a Phabricator daemon. Although this bot is
* somewhat useful, it is also intended to serve as a demo of how to write
* "system agents" which communicate with Phabricator over Conduit, so you can
* script system interactions and integrate with other systems.
*
* NOTE: This is super janky and experimental right now.
*/
final class PhabricatorBot extends PhabricatorDaemon {
private $handlers;
private $conduit;
private $config;
private $pollFrequency;
protected function run() {
$argv = $this->getArgv();
if (count($argv) !== 1) {
throw new Exception(
pht(
'Usage: %s %s',
__CLASS__,
'<json_config_file>'));
}
$json_raw = Filesystem::readFile($argv[0]);
try {
$config = phutil_json_decode($json_raw);
} catch (PhutilJSONParserException $ex) {
throw new PhutilProxyException(
pht("File '%s' is not valid JSON!", $argv[0]),
$ex);
}
$nick = idx($config, 'nick', 'phabot');
$handlers = idx($config, 'handlers', array());
$protocol_adapter_class = idx(
$config,
'protocol-adapter',
'PhabricatorIRCProtocolAdapter');
$this->pollFrequency = idx($config, 'poll-frequency', 1);
$this->config = $config;
foreach ($handlers as $handler) {
$obj = newv($handler, array($this));
$this->handlers[] = $obj;
}
$ca_bundle = idx($config, 'https.cabundle');
if ($ca_bundle) {
HTTPSFuture::setGlobalCABundleFromPath($ca_bundle);
}
$conduit_uri = idx($config, 'conduit.uri');
if ($conduit_uri) {
$conduit_token = idx($config, 'conduit.token');
// Normalize the path component of the URI so users can enter the
// domain without the "/api/" part.
$conduit_uri = new PhutilURI($conduit_uri);
$conduit_host = (string)$conduit_uri->setPath('/');
$conduit_uri = (string)$conduit_uri->setPath('/api/');
$conduit = new ConduitClient($conduit_uri);
if ($conduit_token) {
$conduit->setConduitToken($conduit_token);
} else {
$conduit_user = idx($config, 'conduit.user');
$conduit_cert = idx($config, 'conduit.cert');
$response = $conduit->callMethodSynchronous(
'conduit.connect',
array(
'client' => __CLASS__,
'clientVersion' => '1.0',
'clientDescription' => php_uname('n').':'.$nick,
'host' => $conduit_host,
'user' => $conduit_user,
'certificate' => $conduit_cert,
));
}
$this->conduit = $conduit;
}
// Instantiate Protocol Adapter, for now follow same technique as
// handler instantiation
$this->protocolAdapter = newv($protocol_adapter_class, array());
$this->protocolAdapter
->setConfig($this->config)
->connect();
$this->runLoop();
$this->protocolAdapter->disconnect();
}
public function getConfig($key, $default = null) {
return idx($this->config, $key, $default);
}
private function runLoop() {
do {
$this->stillWorking();
$messages = $this->protocolAdapter->getNextMessages($this->pollFrequency);
if (count($messages) > 0) {
foreach ($messages as $message) {
$this->routeMessage($message);
}
}
foreach ($this->handlers as $handler) {
$handler->runBackgroundTasks();
}
} while (!$this->shouldExit());
}
public function writeMessage(PhabricatorBotMessage $message) {
return $this->protocolAdapter->writeMessage($message);
}
private function routeMessage(PhabricatorBotMessage $message) {
$ignore = $this->getConfig('ignore');
if ($ignore) {
$sender = $message->getSender();
if ($sender && in_array($sender->getName(), $ignore)) {
return;
}
}
if ($message->getCommand() == 'LOG') {
$this->log('[LOG] '.$message->getBody());
}
foreach ($this->handlers as $handler) {
try {
$handler->receiveMessage($message);
} catch (Exception $ex) {
phlog($ex);
}
}
}
public function getAdapter() {
return $this->protocolAdapter;
}
public function getConduit() {
if (empty($this->conduit)) {
throw new Exception(
- "This bot is not configured with a Conduit uplink. Set 'conduit.uri', ".
- "'conduit.user' and 'conduit.cert' in the configuration to connect.");
+ pht(
+ "This bot is not configured with a Conduit uplink. Set '%s', ".
+ "'%s' and '%s' in the configuration to connect.",
+ 'conduit.uri',
+ 'conduit.user',
+ 'conduit.cert'));
}
return $this->conduit;
}
}
diff --git a/src/infrastructure/daemon/bot/adapter/PhabricatorIRCProtocolAdapter.php b/src/infrastructure/daemon/bot/adapter/PhabricatorIRCProtocolAdapter.php
index eab9c4b8f..eee497487 100644
--- a/src/infrastructure/daemon/bot/adapter/PhabricatorIRCProtocolAdapter.php
+++ b/src/infrastructure/daemon/bot/adapter/PhabricatorIRCProtocolAdapter.php
@@ -1,280 +1,282 @@
<?php
final class PhabricatorIRCProtocolAdapter extends PhabricatorProtocolAdapter {
private $socket;
private $writeBuffer;
private $readBuffer;
private $nickIncrement = 0;
public function getServiceType() {
return 'IRC';
}
public function getServiceName() {
return $this->getConfig('network', $this->getConfig('server'));
}
// Hash map of command translations
public static $commandTranslations = array(
'PRIVMSG' => 'MESSAGE',
);
public function connect() {
$nick = $this->getConfig('nick', 'phabot');
$server = $this->getConfig('server');
$port = $this->getConfig('port', 6667);
$pass = $this->getConfig('pass');
$ssl = $this->getConfig('ssl', false);
$user = $this->getConfig('user', $nick);
if (!preg_match('/^[A-Za-z0-9_`[{}^|\]\\-]+$/', $nick)) {
throw new Exception(
- "Nickname '{$nick}' is invalid!");
+ pht(
+ "Nickname '%s' is invalid!",
+ $nick));
}
$errno = null;
$error = null;
if (!$ssl) {
$socket = fsockopen($server, $port, $errno, $error);
} else {
$socket = fsockopen('ssl://'.$server, $port, $errno, $error);
}
if (!$socket) {
- throw new Exception("Failed to connect, #{$errno}: {$error}");
+ throw new Exception(pht('Failed to connect, #%d: %s', $errno, $error));
}
$ok = stream_set_blocking($socket, false);
if (!$ok) {
- throw new Exception('Failed to set stream nonblocking.');
+ throw new Exception(pht('Failed to set stream nonblocking.'));
}
$this->socket = $socket;
if ($pass) {
$this->write("PASS {$pass}");
}
$this->write("NICK {$nick}");
$this->write("USER {$user} 0 * :{$user}");
}
public function getNextMessages($poll_frequency) {
$messages = array();
$read = array($this->socket);
if (strlen($this->writeBuffer)) {
$write = array($this->socket);
} else {
$write = array();
}
$except = array();
$ok = @stream_select($read, $write, $except, $timeout_sec = 1);
if ($ok === false) {
// We may have been interrupted by a signal, like a SIGINT. Try
// selecting again. If the second select works, conclude that the failure
// was most likely because we were signaled.
$ok = @stream_select($read, $write, $except, $timeout_sec = 0);
if ($ok === false) {
- throw new Exception('stream_select() failed!');
+ throw new Exception(pht('%s failed!', 'stream_select()'));
}
}
if ($read) {
// Test for connection termination; in PHP, fread() off a nonblocking,
// closed socket is empty string.
if (feof($this->socket)) {
// This indicates the connection was terminated on the other side,
// just exit via exception and let the overseer restart us after a
// delay so we can reconnect.
- throw new Exception('Remote host closed connection.');
+ throw new Exception(pht('Remote host closed connection.'));
}
do {
$data = fread($this->socket, 4096);
if ($data === false) {
- throw new Exception('fread() failed!');
+ throw new Exception(pht('%s failed!', 'fread()'));
} else {
$messages[] = id(new PhabricatorBotMessage())
->setCommand('LOG')
->setBody('>>> '.$data);
$this->readBuffer .= $data;
}
} while (strlen($data));
}
if ($write) {
do {
$len = fwrite($this->socket, $this->writeBuffer);
if ($len === false) {
- throw new Exception('fwrite() failed!');
+ throw new Exception(pht('%s failed!', 'fwrite()'));
} else if ($len === 0) {
break;
} else {
$messages[] = id(new PhabricatorBotMessage())
->setCommand('LOG')
->setBody('>>> '.substr($this->writeBuffer, 0, $len));
$this->writeBuffer = substr($this->writeBuffer, $len);
}
} while (strlen($this->writeBuffer));
}
while (($m = $this->processReadBuffer()) !== false) {
if ($m !== null) {
$messages[] = $m;
}
}
return $messages;
}
private function write($message) {
$this->writeBuffer .= $message."\r\n";
return $this;
}
public function writeMessage(PhabricatorBotMessage $message) {
switch ($message->getCommand()) {
case 'MESSAGE':
case 'PASTE':
$name = $message->getTarget()->getName();
$body = $message->getBody();
$this->write("PRIVMSG {$name} :{$body}");
return true;
default:
return false;
}
}
private function processReadBuffer() {
$until = strpos($this->readBuffer, "\r\n");
if ($until === false) {
return false;
}
$message = substr($this->readBuffer, 0, $until);
$this->readBuffer = substr($this->readBuffer, $until + 2);
$pattern =
'/^'.
'(?::(?P<sender>(\S+?))(?:!\S*)? )?'. // This may not be present.
'(?P<command>[A-Z0-9]+) '.
'(?P<data>.*)'.
'$/';
$matches = null;
if (!preg_match($pattern, $message, $matches)) {
throw new Exception("Unexpected message from server: {$message}");
}
if ($this->handleIRCProtocol($matches)) {
return null;
}
$command = $this->getBotCommand($matches['command']);
list($target, $body) = $this->parseMessageData($command, $matches['data']);
if (!strlen($matches['sender'])) {
$sender = null;
} else {
$sender = id(new PhabricatorBotUser())
->setName($matches['sender']);
}
$bot_message = id(new PhabricatorBotMessage())
->setSender($sender)
->setCommand($command)
->setTarget($target)
->setBody($body);
return $bot_message;
}
private function handleIRCProtocol(array $matches) {
$data = $matches['data'];
switch ($matches['command']) {
case '433': // Nickname already in use
// If we receive this error, try appending "-1", "-2", etc. to the nick
$this->nickIncrement++;
$nick = $this->getConfig('nick', 'phabot').'-'.$this->nickIncrement;
$this->write("NICK {$nick}");
return true;
case '422': // Error - no MOTD
case '376': // End of MOTD
$nickpass = $this->getConfig('nickpass');
if ($nickpass) {
$this->write("PRIVMSG nickserv :IDENTIFY {$nickpass}");
}
$join = $this->getConfig('join');
if (!$join) {
- throw new Exception('Not configured to join any channels!');
+ throw new Exception(pht('Not configured to join any channels!'));
}
foreach ($join as $channel) {
$this->write("JOIN {$channel}");
}
return true;
case 'PING':
$this->write("PONG {$data}");
return true;
}
return false;
}
private function getBotCommand($irc_command) {
if (isset(self::$commandTranslations[$irc_command])) {
return self::$commandTranslations[$irc_command];
}
// We have no translation for this command, use as-is
return $irc_command;
}
private function parseMessageData($command, $data) {
switch ($command) {
case 'MESSAGE':
$matches = null;
if (preg_match('/^(\S+)\s+:?(.*)$/', $data, $matches)) {
$target_name = $matches[1];
if (strncmp($target_name, '#', 1) === 0) {
$target = id(new PhabricatorBotChannel())
->setName($target_name);
} else {
$target = id(new PhabricatorBotUser())
->setName($target_name);
}
return array(
$target,
rtrim($matches[2], "\r\n"),
);
}
break;
}
// By default we assume there is no target, only a body
return array(
null,
$data,
);
}
public function disconnect() {
// NOTE: FreeNode doesn't show quit messages if you've recently joined a
// channel, presumably to prevent some kind of abuse. If you're testing
// this, you may need to stay connected to the network for a few minutes
// before it works. If you disconnect too quickly, the server will replace
// your message with a "Client Quit" message.
$quit = $this->getConfig('quit', pht('Shutting down.'));
$this->write("QUIT :{$quit}");
// Flush the write buffer.
while (strlen($this->writeBuffer)) {
$this->getNextMessages(0);
}
@fclose($this->socket);
$this->socket = null;
}
}
diff --git a/src/infrastructure/daemon/bot/adapter/PhabricatorStreamingProtocolAdapter.php b/src/infrastructure/daemon/bot/adapter/PhabricatorStreamingProtocolAdapter.php
index 6863880c5..52707b1a0 100644
--- a/src/infrastructure/daemon/bot/adapter/PhabricatorStreamingProtocolAdapter.php
+++ b/src/infrastructure/daemon/bot/adapter/PhabricatorStreamingProtocolAdapter.php
@@ -1,170 +1,170 @@
<?php
abstract class PhabricatorStreamingProtocolAdapter
extends PhabricatorProtocolAdapter {
protected $readHandles;
protected $multiHandle;
protected $authtoken;
private $readBuffers;
private $server;
private $active;
private $inRooms = array();
public function getServiceName() {
$uri = new PhutilURI($this->server);
return $uri->getDomain();
}
public function connect() {
$this->server = $this->getConfig('server');
$this->authtoken = $this->getConfig('authtoken');
$rooms = $this->getConfig('join');
// First, join the room
if (!$rooms) {
- throw new Exception('Not configured to join any rooms!');
+ throw new Exception(pht('Not configured to join any rooms!'));
}
$this->readBuffers = array();
// Set up our long poll in a curl multi request so we can
// continue running while it executes in the background
$this->multiHandle = curl_multi_init();
$this->readHandles = array();
foreach ($rooms as $room_id) {
$this->joinRoom($room_id);
// Set up the curl stream for reading
$url = $this->buildStreamingUrl($room_id);
$ch = $this->readHandles[$url] = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
curl_setopt(
$ch,
CURLOPT_USERPWD,
$this->authtoken.':x');
curl_setopt(
$ch,
CURLOPT_HTTPHEADER,
array('Content-type: application/json'));
curl_setopt(
$ch,
CURLOPT_WRITEFUNCTION,
array($this, 'read'));
curl_setopt($ch, CURLOPT_BUFFERSIZE, 128);
curl_setopt($ch, CURLOPT_TIMEOUT, 0);
curl_multi_add_handle($this->multiHandle, $ch);
// Initialize read buffer
$this->readBuffers[$url] = '';
}
$this->active = null;
$this->blockingMultiExec();
}
protected function joinRoom($room_id) {
// Optional hook, by default, do nothing
}
// This is our callback for the background curl multi-request.
// Puts the data read in on the readBuffer for processing.
private function read($ch, $data) {
$info = curl_getinfo($ch);
$length = strlen($data);
$this->readBuffers[$info['url']] .= $data;
return $length;
}
private function blockingMultiExec() {
do {
$status = curl_multi_exec($this->multiHandle, $this->active);
} while ($status == CURLM_CALL_MULTI_PERFORM);
// Check for errors
if ($status != CURLM_OK) {
throw new Exception(
- 'Phabricator Bot had a problem reading from stream.');
+ pht('Phabricator Bot had a problem reading from stream.'));
}
}
public function getNextMessages($poll_frequency) {
$messages = array();
if (!$this->active) {
- throw new Exception('Phabricator Bot stopped reading from stream.');
+ throw new Exception(pht('Phabricator Bot stopped reading from stream.'));
}
// Prod our http request
curl_multi_select($this->multiHandle, $poll_frequency);
$this->blockingMultiExec();
// Process anything waiting on the read buffer
while ($m = $this->processReadBuffer()) {
$messages[] = $m;
}
return $messages;
}
private function processReadBuffer() {
foreach ($this->readBuffers as $url => &$buffer) {
$until = strpos($buffer, "}\r");
if ($until == false) {
continue;
}
$message = substr($buffer, 0, $until + 1);
$buffer = substr($buffer, $until + 2);
$m_obj = phutil_json_decode($message);
if ($message = $this->processMessage($m_obj)) {
return $message;
}
}
// If we're here, there's nothing to process
return false;
}
protected function performPost($endpoint, $data = null) {
$uri = new PhutilURI($this->server);
$uri->setPath($endpoint);
$payload = json_encode($data);
list($output) = id(new HTTPSFuture($uri))
->setMethod('POST')
->addHeader('Content-Type', 'application/json')
->addHeader('Authorization', $this->getAuthorizationHeader())
->setData($payload)
->resolvex();
$output = trim($output);
if (strlen($output)) {
return phutil_json_decode($output);
}
return true;
}
protected function getAuthorizationHeader() {
return 'Basic '.$this->getEncodedAuthToken();
}
protected function getEncodedAuthToken() {
return base64_encode($this->authtoken.':x');
}
abstract protected function buildStreamingUrl($channel);
abstract protected function processMessage(array $raw_object);
}
diff --git a/src/infrastructure/daemon/bot/handler/PhabricatorBotDebugLogHandler.php b/src/infrastructure/daemon/bot/handler/PhabricatorBotDebugLogHandler.php
index 7c2d0b57c..eb4f9b24a 100644
--- a/src/infrastructure/daemon/bot/handler/PhabricatorBotDebugLogHandler.php
+++ b/src/infrastructure/daemon/bot/handler/PhabricatorBotDebugLogHandler.php
@@ -1,17 +1,17 @@
<?php
/**
- * Logs messages to stdout
+ * Logs messages to stdout.
*/
final class PhabricatorBotDebugLogHandler extends PhabricatorBotHandler {
public function receiveMessage(PhabricatorBotMessage $message) {
switch ($message->getCommand()) {
case 'LOG':
echo addcslashes(
$message->getBody(),
"\0..\37\177..\377");
echo "\n";
break;
}
}
}
diff --git a/src/infrastructure/daemon/bot/handler/PhabricatorBotHandler.php b/src/infrastructure/daemon/bot/handler/PhabricatorBotHandler.php
index b649d3361..2b41cb63f 100644
--- a/src/infrastructure/daemon/bot/handler/PhabricatorBotHandler.php
+++ b/src/infrastructure/daemon/bot/handler/PhabricatorBotHandler.php
@@ -1,72 +1,72 @@
<?php
/**
* Responds to IRC messages. You plug a bunch of these into a
* @{class:PhabricatorBot} to give it special behavior.
*/
abstract class PhabricatorBotHandler {
private $bot;
final public function __construct(PhabricatorBot $irc_bot) {
$this->bot = $irc_bot;
}
final protected function writeMessage(PhabricatorBotMessage $message) {
$this->bot->writeMessage($message);
return $this;
}
final protected function getConduit() {
return $this->bot->getConduit();
}
final protected function getConfig($key, $default = null) {
return $this->bot->getConfig($key, $default);
}
final protected function getURI($path) {
$base_uri = new PhutilURI($this->bot->getConfig('conduit.uri'));
$base_uri->setPath($path);
return (string)$base_uri;
}
final protected function getServiceName() {
return $this->bot->getAdapter()->getServiceName();
}
final protected function getServiceType() {
return $this->bot->getAdapter()->getServiceType();
}
abstract public function receiveMessage(PhabricatorBotMessage $message);
public function runBackgroundTasks() {
return;
}
public function replyTo(PhabricatorBotMessage $original_message, $body) {
if ($original_message->getCommand() != 'MESSAGE') {
throw new Exception(
- 'Handler is trying to reply to something which is not a message!');
+ pht('Handler is trying to reply to something which is not a message!'));
}
$reply = id(new PhabricatorBotMessage())
->setCommand('MESSAGE');
if ($original_message->getTarget()->isPublic()) {
// This is a public target, like a chatroom. Send the response to the
// chatroom.
$reply->setTarget($original_message->getTarget());
} else {
// This is a private target, like a private message. Send the response
// back to the sender (presumably, we are the target).
$reply->setTarget($original_message->getSender());
}
$reply->setBody($body);
return $this->writeMessage($reply);
}
}
diff --git a/src/infrastructure/daemon/bot/handler/PhabricatorBotSymbolHandler.php b/src/infrastructure/daemon/bot/handler/PhabricatorBotSymbolHandler.php
index 48734af94..2e7ff01be 100644
--- a/src/infrastructure/daemon/bot/handler/PhabricatorBotSymbolHandler.php
+++ b/src/infrastructure/daemon/bot/handler/PhabricatorBotSymbolHandler.php
@@ -1,47 +1,50 @@
<?php
/**
* Watches for "where is <symbol>?"
*/
final class PhabricatorBotSymbolHandler extends PhabricatorBotHandler {
public function receiveMessage(PhabricatorBotMessage $message) {
switch ($message->getCommand()) {
case 'MESSAGE':
$text = $message->getBody();
$matches = null;
if (!preg_match('/where(?: in the world)? is (\S+?)\?/i',
$text, $matches)) {
break;
}
$symbol = $matches[1];
$results = $this->getConduit()->callMethodSynchronous(
'diffusion.findsymbols',
array(
'name' => $symbol,
));
$default_uri = $this->getURI('/diffusion/symbol/'.$symbol.'/');
if (count($results) > 1) {
- $response = "Multiple symbols named '{$symbol}': {$default_uri}";
+ $response = pht(
+ "Multiple symbols named '%s': %s",
+ $symbol,
+ $default_uri);
} else if (count($results) == 1) {
$result = head($results);
$response =
$result['type'].' '.
$result['name'].' '.
'('.$result['language'].'): '.
nonempty($result['uri'], $default_uri);
} else {
- $response = "No symbol '{$symbol}' found anywhere.";
+ $response = pht("No symbol '%s' found anywhere.", $symbol);
}
$this->replyTo($message, $response);
break;
}
}
}
diff --git a/src/infrastructure/daemon/bot/handler/PhabricatorBotWhatsNewHandler.php b/src/infrastructure/daemon/bot/handler/PhabricatorBotWhatsNewHandler.php
index cc1cda2f7..a7d7ad905 100644
--- a/src/infrastructure/daemon/bot/handler/PhabricatorBotWhatsNewHandler.php
+++ b/src/infrastructure/daemon/bot/handler/PhabricatorBotWhatsNewHandler.php
@@ -1,43 +1,43 @@
<?php
/**
- * Responds to "Whats new?" with some recent feed content
+ * Responds to "Whats new?" with some recent feed content.
*/
final class PhabricatorBotWhatsNewHandler extends PhabricatorBotHandler {
private $floodblock = 0;
public function receiveMessage(PhabricatorBotMessage $message) {
switch ($message->getCommand()) {
case 'MESSAGE':
$message_body = $message->getBody();
$now = time();
$prompt = '~what( i|\')?s new\?~i';
if (preg_match($prompt, $message_body)) {
if ($now < $this->floodblock) {
return;
}
$this->floodblock = $now + 60;
$this->reportNew($message);
}
break;
}
}
public function reportNew(PhabricatorBotMessage $message) {
$latest = $this->getConduit()->callMethodSynchronous(
'feed.query',
array(
'limit' => 5,
'view' => 'text',
));
foreach ($latest as $feed_item) {
if (isset($feed_item['text'])) {
$this->replyTo($message, html_entity_decode($feed_item['text']));
}
}
}
}
diff --git a/src/infrastructure/daemon/workers/PhabricatorTaskmasterDaemon.php b/src/infrastructure/daemon/workers/PhabricatorTaskmasterDaemon.php
index f85f230c9..46027814d 100644
--- a/src/infrastructure/daemon/workers/PhabricatorTaskmasterDaemon.php
+++ b/src/infrastructure/daemon/workers/PhabricatorTaskmasterDaemon.php
@@ -1,53 +1,53 @@
<?php
final class PhabricatorTaskmasterDaemon extends PhabricatorDaemon {
protected function run() {
do {
$tasks = id(new PhabricatorWorkerLeaseQuery())
->setLimit(1)
->execute();
if ($tasks) {
$this->willBeginWork();
foreach ($tasks as $task) {
$id = $task->getID();
$class = $task->getTaskClass();
- $this->log("Working on task {$id} ({$class})...");
+ $this->log(pht('Working on task %d (%s)...', $id, $class));
$task = $task->executeTask();
$ex = $task->getExecutionException();
if ($ex) {
if ($ex instanceof PhabricatorWorkerPermanentFailureException) {
throw new PhutilProxyException(
pht('Permanent failure while executing Task ID %d.', $id),
$ex);
} else if ($ex instanceof PhabricatorWorkerYieldException) {
$this->log(pht('Task %s yielded.', $id));
} else {
$this->log("Task {$id} failed!");
throw new PhutilProxyException(
pht('Error while executing Task ID %d.', $id),
$ex);
}
} else {
- $this->log("Task {$id} complete! Moved to archive.");
+ $this->log(pht('Task %s complete! Moved to archive.', $id));
}
}
$sleep = 0;
} else {
// When there's no work, sleep for one second. The pool will
// autoscale down if we're continuously idle for an extended period
// of time.
$this->willBeginIdle();
$sleep = 1;
}
$this->sleep($sleep);
} while (!$this->shouldExit());
}
}
diff --git a/src/infrastructure/daemon/workers/__tests__/PhabricatorTestWorker.php b/src/infrastructure/daemon/workers/__tests__/PhabricatorTestWorker.php
index 1b4788a51..86e83acb8 100644
--- a/src/infrastructure/daemon/workers/__tests__/PhabricatorTestWorker.php
+++ b/src/infrastructure/daemon/workers/__tests__/PhabricatorTestWorker.php
@@ -1,38 +1,38 @@
<?php
final class PhabricatorTestWorker extends PhabricatorWorker {
public function getRequiredLeaseTime() {
return idx(
$this->getTaskData(),
'getRequiredLeaseTime',
parent::getRequiredLeaseTime());
}
public function getMaximumRetryCount() {
return idx(
$this->getTaskData(),
'getMaximumRetryCount',
parent::getMaximumRetryCount());
}
public function getWaitBeforeRetry(PhabricatorWorkerTask $task) {
return idx(
$this->getTaskData(),
'getWaitBeforeRetry',
parent::getWaitBeforeRetry($task));
}
protected function doWork() {
switch (idx($this->getTaskData(), 'doWork')) {
case 'fail-temporary':
- throw new Exception('Temporary failure!');
+ throw new Exception(pht('Temporary failure!'));
case 'fail-permanent':
throw new PhabricatorWorkerPermanentFailureException(
- 'Permanent failure!');
+ pht('Permanent failure!'));
default:
return;
}
}
}
diff --git a/src/infrastructure/daemon/workers/__tests__/PhabricatorWorkerTestCase.php b/src/infrastructure/daemon/workers/__tests__/PhabricatorWorkerTestCase.php
index 739d135f8..83d2585e1 100644
--- a/src/infrastructure/daemon/workers/__tests__/PhabricatorWorkerTestCase.php
+++ b/src/infrastructure/daemon/workers/__tests__/PhabricatorWorkerTestCase.php
@@ -1,212 +1,215 @@
<?php
final class PhabricatorWorkerTestCase extends PhabricatorTestCase {
protected function getPhabricatorTestCaseConfiguration() {
return array(
self::PHABRICATOR_TESTCONFIG_BUILD_STORAGE_FIXTURES => true,
);
}
public function testLeaseTask() {
$task = $this->scheduleTask();
- $this->expectNextLease($task, 'Leasing should work.');
+ $this->expectNextLease($task, pht('Leasing should work.'));
}
public function testMultipleLease() {
$task = $this->scheduleTask();
$this->expectNextLease($task);
$this->expectNextLease(
null,
- 'We should not be able to lease a task multiple times.');
+ pht('We should not be able to lease a task multiple times.'));
}
public function testOldestFirst() {
$task1 = $this->scheduleTask();
$task2 = $this->scheduleTask();
$this->expectNextLease(
$task1,
- 'Older tasks should lease first, all else being equal.');
+ pht('Older tasks should lease first, all else being equal.'));
$this->expectNextLease($task2);
}
public function testNewBeforeLeased() {
$task1 = $this->scheduleTask();
$task2 = $this->scheduleTask();
$task1->setLeaseOwner('test');
$task1->setLeaseExpires(time() - 100000);
$task1->forceSaveWithoutLease();
$this->expectNextLease(
$task2,
- 'Tasks not previously leased should lease before previously '.
- 'leased tasks.');
+ pht(
+ 'Tasks not previously leased should lease before previously '.
+ 'leased tasks.'));
$this->expectNextLease($task1);
}
public function testExecuteTask() {
$task = $this->scheduleAndExecuteTask();
$this->assertEqual(true, $task->isArchived());
$this->assertEqual(
PhabricatorWorkerArchiveTask::RESULT_SUCCESS,
$task->getResult());
}
public function testPermanentTaskFailure() {
$task = $this->scheduleAndExecuteTask(
array(
'doWork' => 'fail-permanent',
));
$this->assertEqual(true, $task->isArchived());
$this->assertEqual(
PhabricatorWorkerArchiveTask::RESULT_FAILURE,
$task->getResult());
}
public function testTemporaryTaskFailure() {
$task = $this->scheduleAndExecuteTask(
array(
'doWork' => 'fail-temporary',
));
$this->assertFalse($task->isArchived());
$this->assertTrue($task->getExecutionException() instanceof Exception);
}
public function testTooManyTaskFailures() {
// Expect temporary failures, then a permanent failure.
$task = $this->scheduleAndExecuteTask(
array(
'doWork' => 'fail-temporary',
'getMaximumRetryCount' => 3,
'getWaitBeforeRetry' => -60,
));
// Temporary...
$this->assertFalse($task->isArchived());
$this->assertTrue($task->getExecutionException() instanceof Exception);
$this->assertEqual(1, $task->getFailureCount());
// Temporary...
$task = $this->expectNextLease($task);
$task = $task->executeTask();
$this->assertFalse($task->isArchived());
$this->assertTrue($task->getExecutionException() instanceof Exception);
$this->assertEqual(2, $task->getFailureCount());
// Temporary...
$task = $this->expectNextLease($task);
$task = $task->executeTask();
$this->assertFalse($task->isArchived());
$this->assertTrue($task->getExecutionException() instanceof Exception);
$this->assertEqual(3, $task->getFailureCount());
// Temporary...
$task = $this->expectNextLease($task);
$task = $task->executeTask();
$this->assertFalse($task->isArchived());
$this->assertTrue($task->getExecutionException() instanceof Exception);
$this->assertEqual(4, $task->getFailureCount());
// Permanent.
$task = $this->expectNextLease($task);
$task = $task->executeTask();
$this->assertTrue($task->isArchived());
$this->assertEqual(
PhabricatorWorkerArchiveTask::RESULT_FAILURE,
$task->getResult());
}
public function testWaitBeforeRetry() {
$task = $this->scheduleTask(
array(
'doWork' => 'fail-temporary',
'getWaitBeforeRetry' => 1000000,
));
$this->expectNextLease($task)->executeTask();
$this->expectNextLease(null);
}
public function testRequiredLeaseTime() {
$task = $this->scheduleAndExecuteTask(
array(
'getRequiredLeaseTime' => 1000000,
));
$this->assertTrue(($task->getLeaseExpires() - time()) > 1000);
}
public function testLeasedIsOldestFirst() {
$task1 = $this->scheduleTask();
$task2 = $this->scheduleTask();
$task1->setLeaseOwner('test');
$task1->setLeaseExpires(time() - 100000);
$task1->forceSaveWithoutLease();
$task2->setLeaseOwner('test');
$task2->setLeaseExpires(time() - 200000);
$task2->forceSaveWithoutLease();
$this->expectNextLease(
$task2,
- 'Tasks which expired earlier should lease first, all else being equal.');
+ pht(
+ 'Tasks which expired earlier should lease first, '.
+ 'all else being equal.'));
$this->expectNextLease($task1);
}
public function testLeasedIsLowestPriority() {
$task1 = $this->scheduleTask(array(), 2);
$task2 = $this->scheduleTask(array(), 2);
$task3 = $this->scheduleTask(array(), 1);
$this->expectNextLease(
$task3,
- 'Tasks with a lower priority should be scheduled first.');
+ pht('Tasks with a lower priority should be scheduled first.'));
$this->expectNextLease(
$task1,
- 'Tasks with the same priority should be FIFO.');
+ pht('Tasks with the same priority should be FIFO.'));
$this->expectNextLease($task2);
}
private function expectNextLease($task, $message = null) {
$leased = id(new PhabricatorWorkerLeaseQuery())
->setLimit(1)
->execute();
if ($task === null) {
$this->assertEqual(0, count($leased), $message);
return null;
} else {
$this->assertEqual(1, count($leased), $message);
$this->assertEqual(
(int)head($leased)->getID(),
(int)$task->getID(),
$message);
return head($leased);
}
}
private function scheduleAndExecuteTask(
array $data = array(),
$priority = null) {
$task = $this->scheduleTask($data, $priority);
$task = $this->expectNextLease($task);
$task = $task->executeTask();
return $task;
}
private function scheduleTask(array $data = array(), $priority = null) {
return PhabricatorWorker::scheduleTask(
'PhabricatorTestWorker',
$data,
array('priority' => $priority));
}
}
diff --git a/src/infrastructure/daemon/workers/clock/PhabricatorMetronomicTriggerClock.php b/src/infrastructure/daemon/workers/clock/PhabricatorMetronomicTriggerClock.php
index 99a708086..d70421138 100644
--- a/src/infrastructure/daemon/workers/clock/PhabricatorMetronomicTriggerClock.php
+++ b/src/infrastructure/daemon/workers/clock/PhabricatorMetronomicTriggerClock.php
@@ -1,33 +1,32 @@
<?php
/**
* Triggers an event repeatedly, delaying a fixed number of seconds between
* triggers.
*
* For example, this clock can trigger an event every 30 seconds.
*/
-final class PhabricatorMetronomicTriggerClock
- extends PhabricatorTriggerClock {
+final class PhabricatorMetronomicTriggerClock extends PhabricatorTriggerClock {
public function validateProperties(array $properties) {
PhutilTypeSpec::checkMap(
$properties,
array(
'period' => 'int',
));
}
public function getNextEventEpoch($last_epoch, $is_reschedule) {
$period = $this->getProperty('period');
if ($last_epoch) {
$next = $last_epoch + $period;
$next = max($next, $last_epoch + 1);
} else {
$next = PhabricatorTime::getNow() + $period;
}
return $next;
}
}
diff --git a/src/infrastructure/daemon/workers/clock/PhabricatorNeverTriggerClock.php b/src/infrastructure/daemon/workers/clock/PhabricatorNeverTriggerClock.php
index 3f809c961..cbf5be286 100644
--- a/src/infrastructure/daemon/workers/clock/PhabricatorNeverTriggerClock.php
+++ b/src/infrastructure/daemon/workers/clock/PhabricatorNeverTriggerClock.php
@@ -1,21 +1,20 @@
<?php
/**
* Never triggers an event.
*
* This clock can be used for testing, or to cancel events.
*/
-final class PhabricatorNeverTriggerClock
- extends PhabricatorTriggerClock {
+final class PhabricatorNeverTriggerClock extends PhabricatorTriggerClock {
public function validateProperties(array $properties) {
PhutilTypeSpec::checkMap(
$properties,
array());
}
public function getNextEventEpoch($last_epoch, $is_reschedule) {
return null;
}
}
diff --git a/src/infrastructure/daemon/workers/management/PhabricatorWorkerTriggerManagementWorkflow.php b/src/infrastructure/daemon/workers/management/PhabricatorWorkerTriggerManagementWorkflow.php
index 6251a8af3..05eefdd91 100644
--- a/src/infrastructure/daemon/workers/management/PhabricatorWorkerTriggerManagementWorkflow.php
+++ b/src/infrastructure/daemon/workers/management/PhabricatorWorkerTriggerManagementWorkflow.php
@@ -1,45 +1,45 @@
<?php
abstract class PhabricatorWorkerTriggerManagementWorkflow
extends PhabricatorManagementWorkflow {
protected function getTriggerSelectionArguments() {
return array(
array(
'name' => 'id',
'param' => 'id',
'repeat' => true,
'help' => pht('Select one or more triggers by ID.'),
),
);
}
protected function loadTriggers(PhutilArgumentParser $args) {
$ids = $args->getArg('id');
if (!$ids) {
throw new PhutilArgumentUsageException(
- pht('Use --id to select triggers by ID.'));
+ pht('Use %s to select triggers by ID.', '--id'));
}
$triggers = id(new PhabricatorWorkerTriggerQuery())
->setViewer($this->getViewer())
->withIDs($ids)
->needEvents(true)
->execute();
$triggers = mpull($triggers, null, 'getID');
foreach ($ids as $id) {
if (empty($triggers[$id])) {
throw new PhutilArgumentUsageException(
pht('No trigger exists with id "%s"!', $id));
}
}
return $triggers;
}
protected function describeTrigger(PhabricatorWorkerTrigger $trigger) {
return pht('Trigger %d', $trigger->getID());
}
}
diff --git a/src/infrastructure/daemon/workers/query/PhabricatorWorkerLeaseQuery.php b/src/infrastructure/daemon/workers/query/PhabricatorWorkerLeaseQuery.php
index 07c346049..bb70f03d8 100644
--- a/src/infrastructure/daemon/workers/query/PhabricatorWorkerLeaseQuery.php
+++ b/src/infrastructure/daemon/workers/query/PhabricatorWorkerLeaseQuery.php
@@ -1,335 +1,337 @@
<?php
/**
* Select and lease tasks from the worker task queue.
*/
final class PhabricatorWorkerLeaseQuery extends PhabricatorQuery {
const PHASE_LEASED = 'leased';
const PHASE_UNLEASED = 'unleased';
const PHASE_EXPIRED = 'expired';
private $ids;
private $objectPHIDs;
private $limit;
private $skipLease;
private $leased = false;
public static function getDefaultWaitBeforeRetry() {
return phutil_units('5 minutes in seconds');
}
public static function getDefaultLeaseDuration() {
return phutil_units('2 hours in seconds');
}
/**
* Set this flag to select tasks from the top of the queue without leasing
* them.
*
* This can be used to show which tasks are coming up next without altering
* the queue's behavior.
*
* @param bool True to skip the lease acquisition step.
*/
public function setSkipLease($skip) {
$this->skipLease = $skip;
return $this;
}
public function withIDs(array $ids) {
$this->ids = $ids;
return $this;
}
public function withObjectPHIDs(array $phids) {
$this->objectPHIDs = $phids;
return $this;
}
/**
* Select only leased tasks, only unleased tasks, or both types of task.
*
* By default, queries select only unleased tasks (equivalent to passing
* `false` to this method). You can pass `true` to select only leased tasks,
* or `null` to ignore the lease status of tasks.
*
* If your result set potentially includes leased tasks, you must disable
* leasing using @{method:setSkipLease}. These options are intended for use
* when displaying task status information.
*
* @param mixed `true` to select only leased tasks, `false` to select only
* unleased tasks (default), or `null` to select both.
* @return this
*/
public function withLeasedTasks($leased) {
$this->leased = $leased;
return $this;
}
public function setLimit($limit) {
$this->limit = $limit;
return $this;
}
public function execute() {
if (!$this->limit) {
throw new Exception(
- pht('You must setLimit() when leasing tasks.'));
+ pht('You must %s when leasing tasks.', 'setLimit()'));
}
if ($this->leased !== false) {
if (!$this->skipLease) {
throw new Exception(
pht(
- 'If you potentially select leased tasks using withLeasedTasks(), '.
- 'you MUST disable lease acquisition by calling setSkipLease().'));
+ 'If you potentially select leased tasks using %s, '.
+ 'you MUST disable lease acquisition by calling %s.',
+ 'withLeasedTasks()',
+ 'setSkipLease()'));
}
}
$task_table = new PhabricatorWorkerActiveTask();
$taskdata_table = new PhabricatorWorkerTaskData();
$lease_ownership_name = $this->getLeaseOwnershipName();
$conn_w = $task_table->establishConnection('w');
// Try to satisfy the request from new, unleased tasks first. If we don't
// find enough tasks, try tasks with expired leases (i.e., tasks which have
// previously failed).
// If we're selecting leased tasks, look for them first.
$phases = array();
if ($this->leased !== false) {
$phases[] = self::PHASE_LEASED;
}
if ($this->leased !== true) {
$phases[] = self::PHASE_UNLEASED;
$phases[] = self::PHASE_EXPIRED;
}
$limit = $this->limit;
$leased = 0;
$task_ids = array();
foreach ($phases as $phase) {
// NOTE: If we issue `UPDATE ... WHERE ... ORDER BY id ASC`, the query
// goes very, very slowly. The `ORDER BY` triggers this, although we get
// the same apparent results without it. Without the ORDER BY, binary
// read slaves complain that the query isn't repeatable. To avoid both
// problems, do a SELECT and then an UPDATE.
$rows = queryfx_all(
$conn_w,
'SELECT id, leaseOwner FROM %T %Q %Q %Q',
$task_table->getTableName(),
$this->buildCustomWhereClause($conn_w, $phase),
$this->buildOrderClause($conn_w, $phase),
$this->buildLimitClause($conn_w, $limit - $leased));
// NOTE: Sometimes, we'll race with another worker and they'll grab
// this task before we do. We could reduce how often this happens by
// selecting more tasks than we need, then shuffling them and trying
// to lock only the number we're actually after. However, the amount
// of time workers spend here should be very small relative to their
// total runtime, so keep it simple for the moment.
if ($rows) {
if ($this->skipLease) {
$leased += count($rows);
$task_ids += array_fuse(ipull($rows, 'id'));
} else {
queryfx(
$conn_w,
'UPDATE %T task
SET leaseOwner = %s, leaseExpires = UNIX_TIMESTAMP() + %d
%Q',
$task_table->getTableName(),
$lease_ownership_name,
self::getDefaultLeaseDuration(),
$this->buildUpdateWhereClause($conn_w, $phase, $rows));
$leased += $conn_w->getAffectedRows();
}
if ($leased == $limit) {
break;
}
}
}
if (!$leased) {
return array();
}
if ($this->skipLease) {
$selection_condition = qsprintf(
$conn_w,
'task.id IN (%Ld)',
$task_ids);
} else {
$selection_condition = qsprintf(
$conn_w,
'task.leaseOwner = %s AND leaseExpires > UNIX_TIMESTAMP()',
$lease_ownership_name);
}
$data = queryfx_all(
$conn_w,
'SELECT task.*, taskdata.data _taskData, UNIX_TIMESTAMP() _serverTime
FROM %T task LEFT JOIN %T taskdata
ON taskdata.id = task.dataID
WHERE %Q %Q %Q',
$task_table->getTableName(),
$taskdata_table->getTableName(),
$selection_condition,
$this->buildOrderClause($conn_w, $phase),
$this->buildLimitClause($conn_w, $limit));
$tasks = $task_table->loadAllFromArray($data);
$tasks = mpull($tasks, null, 'getID');
foreach ($data as $row) {
$tasks[$row['id']]->setServerTime($row['_serverTime']);
if ($row['_taskData']) {
$task_data = json_decode($row['_taskData'], true);
} else {
$task_data = null;
}
$tasks[$row['id']]->setData($task_data);
}
if ($this->skipLease) {
// Reorder rows into the original phase order if this is a status query.
$tasks = array_select_keys($tasks, $task_ids);
}
return $tasks;
}
protected function buildCustomWhereClause(
AphrontDatabaseConnection $conn_w,
$phase) {
$where = array();
switch ($phase) {
case self::PHASE_LEASED:
$where[] = 'leaseOwner IS NOT NULL';
$where[] = 'leaseExpires >= UNIX_TIMESTAMP()';
break;
case self::PHASE_UNLEASED:
$where[] = 'leaseOwner IS NULL';
break;
case self::PHASE_EXPIRED:
$where[] = 'leaseExpires < UNIX_TIMESTAMP()';
break;
default:
- throw new Exception("Unknown phase '{$phase}'!");
+ throw new Exception(pht("Unknown phase '%s'!", $phase));
}
if ($this->ids !== null) {
$where[] = qsprintf($conn_w, 'id IN (%Ld)', $this->ids);
}
if ($this->objectPHIDs !== null) {
$where[] = qsprintf($conn_w, 'objectPHID IN (%Ls)', $this->objectPHIDs);
}
return $this->formatWhereClause($where);
}
private function buildUpdateWhereClause(
AphrontDatabaseConnection $conn_w,
$phase,
array $rows) {
$where = array();
// NOTE: This is basically working around the MySQL behavior that
// `IN (NULL)` doesn't match NULL.
switch ($phase) {
case self::PHASE_LEASED:
throw new Exception(
pht(
'Trying to lease tasks selected in the leased phase! This is '.
- 'intended to be imposssible.'));
+ 'intended to be impossible.'));
case self::PHASE_UNLEASED:
$where[] = qsprintf($conn_w, 'leaseOwner IS NULL');
$where[] = qsprintf($conn_w, 'id IN (%Ld)', ipull($rows, 'id'));
break;
case self::PHASE_EXPIRED:
$in = array();
foreach ($rows as $row) {
$in[] = qsprintf(
$conn_w,
'(id = %d AND leaseOwner = %s)',
$row['id'],
$row['leaseOwner']);
}
$where[] = qsprintf($conn_w, '(%Q)', implode(' OR ', $in));
break;
default:
- throw new Exception("Unknown phase '{$phase}'!");
+ throw new Exception(pht('Unknown phase "%s"!', $phase));
}
return $this->formatWhereClause($where);
}
private function buildOrderClause(AphrontDatabaseConnection $conn_w, $phase) {
switch ($phase) {
case self::PHASE_LEASED:
// Ideally we'd probably order these by lease acquisition time, but
// we don't have that handy and this is a good approximation.
return qsprintf($conn_w, 'ORDER BY priority ASC, id ASC');
case self::PHASE_UNLEASED:
// When selecting new tasks, we want to consume them in order of
// increasing priority (and then FIFO).
return qsprintf($conn_w, 'ORDER BY priority ASC, id ASC');
case self::PHASE_EXPIRED:
// When selecting failed tasks, we want to consume them in roughly
// FIFO order of their failures, which is not necessarily their original
// queue order.
// Particularly, this is important for tasks which use soft failures to
// indicate that they are waiting on other tasks to complete: we need to
// push them to the end of the queue after they fail, at least on
// average, so we don't deadlock retrying the same blocked task over
// and over again.
return qsprintf($conn_w, 'ORDER BY leaseExpires ASC');
default:
throw new Exception(pht('Unknown phase "%s"!', $phase));
}
}
private function buildLimitClause(AphrontDatabaseConnection $conn_w, $limit) {
return qsprintf($conn_w, 'LIMIT %d', $limit);
}
private function getLeaseOwnershipName() {
static $sequence = 0;
// TODO: If the host name is very long, this can overflow the 64-character
// column, so we pick just the first part of the host name. It might be
// useful to just use a random hash as the identifier instead and put the
// pid / time / host (which are somewhat useful diagnostically) elsewhere.
// Likely, we could store a daemon ID instead and use that to identify
// when and where code executed. See T6742.
$host = php_uname('n');
$host = id(new PhutilUTF8StringTruncator())
->setMaximumBytes(32)
->setTerminator('...')
->truncateString($host);
$parts = array(
getmypid(),
time(),
$host,
++$sequence,
);
return implode(':', $parts);
}
}
diff --git a/src/infrastructure/daemon/workers/storage/PhabricatorWorkerActiveTask.php b/src/infrastructure/daemon/workers/storage/PhabricatorWorkerActiveTask.php
index 36ada93a1..0705a2778 100644
--- a/src/infrastructure/daemon/workers/storage/PhabricatorWorkerActiveTask.php
+++ b/src/infrastructure/daemon/workers/storage/PhabricatorWorkerActiveTask.php
@@ -1,218 +1,222 @@
<?php
final class PhabricatorWorkerActiveTask extends PhabricatorWorkerTask {
protected $failureTime;
private $serverTime;
private $localTime;
protected function getConfiguration() {
$parent = parent::getConfiguration();
$config = array(
self::CONFIG_IDS => self::IDS_COUNTER,
self::CONFIG_TIMESTAMPS => false,
self::CONFIG_KEY_SCHEMA => array(
'dataID' => array(
'columns' => array('dataID'),
'unique' => true,
),
'taskClass' => array(
'columns' => array('taskClass'),
),
'leaseExpires' => array(
'columns' => array('leaseExpires'),
),
'leaseOwner' => array(
'columns' => array('leaseOwner(16)'),
),
'key_failuretime' => array(
'columns' => array('failureTime'),
),
'leaseOwner_2' => array(
'columns' => array('leaseOwner', 'priority', 'id'),
),
) + $parent[self::CONFIG_KEY_SCHEMA],
);
$config[self::CONFIG_COLUMN_SCHEMA] = array(
// T6203/NULLABILITY
// This isn't nullable in the archive table, so at a minimum these
// should be the same.
'dataID' => 'uint32?',
) + $parent[self::CONFIG_COLUMN_SCHEMA];
return $config + $parent;
}
public function setServerTime($server_time) {
$this->serverTime = $server_time;
$this->localTime = time();
return $this;
}
public function setLeaseDuration($lease_duration) {
$this->checkLease();
$server_lease_expires = $this->serverTime + $lease_duration;
$this->setLeaseExpires($server_lease_expires);
// NOTE: This is primarily to allow unit tests to set negative lease
// durations so they don't have to wait around for leases to expire. We
// check that the lease is valid above.
return $this->forceSaveWithoutLease();
}
public function save() {
$this->checkLease();
return $this->forceSaveWithoutLease();
}
public function forceSaveWithoutLease() {
$is_new = !$this->getID();
if ($is_new) {
$this->failureCount = 0;
}
if ($is_new && ($this->getData() !== null)) {
$data = new PhabricatorWorkerTaskData();
$data->setData($this->getData());
$data->save();
$this->setDataID($data->getID());
}
return parent::save();
}
protected function checkLease() {
if ($this->leaseOwner) {
$current_server_time = $this->serverTime + (time() - $this->localTime);
if ($current_server_time >= $this->leaseExpires) {
- $id = $this->getID();
- $class = $this->getTaskClass();
throw new Exception(
- "Trying to update Task {$id} ({$class}) after lease expiration!");
+ pht(
+ 'Trying to update Task %d (%s) after lease expiration!',
+ $this->getID(),
+ $this->getTaskClass()));
}
}
}
public function delete() {
throw new Exception(
- 'Active tasks can not be deleted directly. '.
- 'Use archiveTask() to move tasks to the archive.');
+ pht(
+ 'Active tasks can not be deleted directly. '.
+ 'Use %s to move tasks to the archive.',
+ 'archiveTask()'));
}
public function archiveTask($result, $duration) {
if ($this->getID() === null) {
throw new Exception(
- "Attempting to archive a task which hasn't been save()d!");
+ pht("Attempting to archive a task which hasn't been saved!"));
}
$this->checkLease();
$archive = id(new PhabricatorWorkerArchiveTask())
->setID($this->getID())
->setTaskClass($this->getTaskClass())
->setLeaseOwner($this->getLeaseOwner())
->setLeaseExpires($this->getLeaseExpires())
->setFailureCount($this->getFailureCount())
->setDataID($this->getDataID())
->setPriority($this->getPriority())
->setObjectPHID($this->getObjectPHID())
->setResult($result)
->setDuration($duration);
// NOTE: This deletes the active task (this object)!
$archive->save();
return $archive;
}
public function executeTask() {
// We do this outside of the try .. catch because we don't have permission
// to release the lease otherwise.
$this->checkLease();
$did_succeed = false;
$worker = null;
try {
$worker = $this->getWorkerInstance();
$maximum_failures = $worker->getMaximumRetryCount();
if ($maximum_failures !== null) {
if ($this->getFailureCount() > $maximum_failures) {
- $id = $this->getID();
throw new PhabricatorWorkerPermanentFailureException(
- "Task {$id} has exceeded the maximum number of failures ".
- "({$maximum_failures}).");
+ pht(
+ 'Task % has exceeded the maximum number of failures (%d).',
+ $this->getID(),
+ $maximum_failures));
}
}
$lease = $worker->getRequiredLeaseTime();
if ($lease !== null) {
$this->setLeaseDuration($lease);
}
$t_start = microtime(true);
$worker->executeTask();
$t_end = microtime(true);
$duration = (int)(1000000 * ($t_end - $t_start));
$result = $this->archiveTask(
PhabricatorWorkerArchiveTask::RESULT_SUCCESS,
$duration);
$did_succeed = true;
} catch (PhabricatorWorkerPermanentFailureException $ex) {
$result = $this->archiveTask(
PhabricatorWorkerArchiveTask::RESULT_FAILURE,
0);
$result->setExecutionException($ex);
} catch (PhabricatorWorkerYieldException $ex) {
$this->setExecutionException($ex);
$retry = $ex->getDuration();
$retry = max($retry, 5);
// NOTE: As a side effect, this saves the object.
$this->setLeaseDuration($retry);
$result = $this;
} catch (Exception $ex) {
$this->setExecutionException($ex);
$this->setFailureCount($this->getFailureCount() + 1);
$this->setFailureTime(time());
$retry = null;
if ($worker) {
$retry = $worker->getWaitBeforeRetry($this);
}
$retry = coalesce(
$retry,
PhabricatorWorkerLeaseQuery::getDefaultWaitBeforeRetry());
// NOTE: As a side effect, this saves the object.
$this->setLeaseDuration($retry);
$result = $this;
}
// NOTE: If this throws, we don't want it to cause the task to fail again,
// so execute it out here and just let the exception escape.
if ($did_succeed) {
foreach ($worker->getQueuedTasks() as $task) {
list($class, $data) = $task;
PhabricatorWorker::scheduleTask(
$class,
$data,
array(
'priority' => $this->getPriority(),
));
}
}
return $result;
}
}
diff --git a/src/infrastructure/daemon/workers/storage/PhabricatorWorkerArchiveTask.php b/src/infrastructure/daemon/workers/storage/PhabricatorWorkerArchiveTask.php
index 5186a005c..9797373eb 100644
--- a/src/infrastructure/daemon/workers/storage/PhabricatorWorkerArchiveTask.php
+++ b/src/infrastructure/daemon/workers/storage/PhabricatorWorkerArchiveTask.php
@@ -1,97 +1,97 @@
<?php
final class PhabricatorWorkerArchiveTask extends PhabricatorWorkerTask {
const RESULT_SUCCESS = 0;
const RESULT_FAILURE = 1;
const RESULT_CANCELLED = 2;
protected $duration;
protected $result;
protected function getConfiguration() {
$parent = parent::getConfiguration();
$config = array(
// We manage the IDs in this table; they are allocated in the ActiveTask
// table and moved here without alteration.
self::CONFIG_IDS => self::IDS_MANUAL,
) + $parent;
$config[self::CONFIG_COLUMN_SCHEMA] = array(
'result' => 'uint32',
'duration' => 'uint64',
) + $config[self::CONFIG_COLUMN_SCHEMA];
$config[self::CONFIG_KEY_SCHEMA] = array(
'dateCreated' => array(
'columns' => array('dateCreated'),
),
'leaseOwner' => array(
'columns' => array('leaseOwner', 'priority', 'id'),
),
) + $parent[self::CONFIG_KEY_SCHEMA];
return $config;
}
public function save() {
if ($this->getID() === null) {
- throw new Exception('Trying to archive a task with no ID.');
+ throw new Exception(pht('Trying to archive a task with no ID.'));
}
$other = new PhabricatorWorkerActiveTask();
$conn_w = $this->establishConnection('w');
$this->openTransaction();
queryfx(
$conn_w,
'DELETE FROM %T WHERE id = %d',
$other->getTableName(),
$this->getID());
$result = parent::insert();
$this->saveTransaction();
return $result;
}
public function delete() {
$this->openTransaction();
if ($this->getDataID()) {
$conn_w = $this->establishConnection('w');
$data_table = new PhabricatorWorkerTaskData();
queryfx(
$conn_w,
'DELETE FROM %T WHERE id = %d',
$data_table->getTableName(),
$this->getDataID());
}
$result = parent::delete();
$this->saveTransaction();
return $result;
}
public function unarchiveTask() {
$this->openTransaction();
$active = id(new PhabricatorWorkerActiveTask())
->setID($this->getID())
->setTaskClass($this->getTaskClass())
->setLeaseOwner(null)
->setLeaseExpires(0)
->setFailureCount(0)
->setDataID($this->getDataID())
->setPriority($this->getPriority())
->setObjectPHID($this->getObjectPHID())
->insert();
$this->setDataID(null);
$this->delete();
$this->saveTransaction();
return $active;
}
}
diff --git a/src/infrastructure/daemon/workers/storage/PhabricatorWorkerTask.php b/src/infrastructure/daemon/workers/storage/PhabricatorWorkerTask.php
index 77543befb..3d8c6887c 100644
--- a/src/infrastructure/daemon/workers/storage/PhabricatorWorkerTask.php
+++ b/src/infrastructure/daemon/workers/storage/PhabricatorWorkerTask.php
@@ -1,76 +1,81 @@
<?php
abstract class PhabricatorWorkerTask extends PhabricatorWorkerDAO {
// NOTE: If you provide additional fields here, make sure they are handled
// correctly in the archiving process.
protected $taskClass;
protected $leaseOwner;
protected $leaseExpires;
protected $failureCount;
protected $dataID;
protected $priority;
protected $objectPHID;
private $data;
private $executionException;
protected function getConfiguration() {
return array(
self::CONFIG_COLUMN_SCHEMA => array(
'taskClass' => 'text64',
'leaseOwner' => 'text64?',
'leaseExpires' => 'epoch?',
'failureCount' => 'uint32',
'failureTime' => 'epoch?',
'priority' => 'uint32',
'objectPHID' => 'phid?',
),
self::CONFIG_KEY_SCHEMA => array(
'key_object' => array(
'columns' => array('objectPHID'),
),
),
) + parent::getConfiguration();
}
final public function setExecutionException(Exception $execution_exception) {
$this->executionException = $execution_exception;
return $this;
}
final public function getExecutionException() {
return $this->executionException;
}
final public function setData($data) {
$this->data = $data;
return $this;
}
final public function getData() {
return $this->data;
}
final public function isArchived() {
return ($this instanceof PhabricatorWorkerArchiveTask);
}
final public function getWorkerInstance() {
$id = $this->getID();
$class = $this->getTaskClass();
if (!class_exists($class)) {
throw new PhabricatorWorkerPermanentFailureException(
- "Task class '{$class}' does not exist!");
+ pht(
+ "Task class '%s' does not exist!",
+ $class));
}
if (!is_subclass_of($class, 'PhabricatorWorker')) {
throw new PhabricatorWorkerPermanentFailureException(
- "Task class '{$class}' does not extend PhabricatorWorker.");
+ pht(
+ "Task class '%s' does not extend %s.",
+ $class,
+ 'PhabricatorWorker'));
}
return newv($class, array($this->getData()));
}
}
diff --git a/src/infrastructure/diff/view/PHUIDiffInlineCommentUndoView.php b/src/infrastructure/diff/view/PHUIDiffInlineCommentUndoView.php
index 4d398ca0c..c4bdd65bf 100644
--- a/src/infrastructure/diff/view/PHUIDiffInlineCommentUndoView.php
+++ b/src/infrastructure/diff/view/PHUIDiffInlineCommentUndoView.php
@@ -1,29 +1,29 @@
<?php
/**
* Render the "Undo" action to recover discarded inline comments.
*
* This extends @{class:PHUIDiffInlineCommentView} so it can use the same
* scaffolding code as other kinds of inline comments.
*/
final class PHUIDiffInlineCommentUndoView
extends PHUIDiffInlineCommentView {
public function render() {
$link = javelin_tag(
'a',
array(
'href' => '#',
'sigil' => 'differential-inline-comment-undo',
),
pht('Undo'));
return phutil_tag(
'div',
array(
'class' => 'differential-inline-undo',
),
- array('Changes discarded. ', $link));
+ array(pht('Changes discarded. '), $link));
}
}
diff --git a/src/infrastructure/edges/constants/PhabricatorEdgeConfig.php b/src/infrastructure/edges/constants/PhabricatorEdgeConfig.php
index b7662353b..4e256dd5c 100644
--- a/src/infrastructure/edges/constants/PhabricatorEdgeConfig.php
+++ b/src/infrastructure/edges/constants/PhabricatorEdgeConfig.php
@@ -1,33 +1,35 @@
<?php
final class PhabricatorEdgeConfig extends PhabricatorEdgeConstants {
const TABLE_NAME_EDGE = 'edge';
const TABLE_NAME_EDGEDATA = 'edgedata';
public static function establishConnection($phid_type, $conn_type) {
$map = PhabricatorPHIDType::getAllTypes();
if (isset($map[$phid_type])) {
$type = $map[$phid_type];
$object = $type->newObject();
if ($object) {
return $object->establishConnection($conn_type);
}
}
static $class_map = array(
PhabricatorPHIDConstants::PHID_TYPE_TOBJ => 'HarbormasterObject',
PhabricatorPHIDConstants::PHID_TYPE_XOBJ => 'DoorkeeperExternalObject',
);
$class = idx($class_map, $phid_type);
if (!$class) {
throw new Exception(
- "Edges are not available for objects of type '{$phid_type}'!");
+ pht(
+ "Edges are not available for objects of type '%s'!",
+ $phid_type));
}
return newv($class, array())->establishConnection($conn_type);
}
}
diff --git a/src/infrastructure/edges/exception/PhabricatorEdgeCycleException.php b/src/infrastructure/edges/exception/PhabricatorEdgeCycleException.php
index 7556405b0..87ece77e8 100644
--- a/src/infrastructure/edges/exception/PhabricatorEdgeCycleException.php
+++ b/src/infrastructure/edges/exception/PhabricatorEdgeCycleException.php
@@ -1,26 +1,29 @@
<?php
final class PhabricatorEdgeCycleException extends Exception {
private $cycleEdgeType;
private $cycle;
public function __construct($cycle_edge_type, array $cycle) {
$this->cycleEdgeType = $cycle_edge_type;
$this->cycle = $cycle;
$cycle_list = implode(', ', $cycle);
parent::__construct(
- "Graph cycle detected (type={$cycle_edge_type}, cycle={$cycle_list}).");
+ pht(
+ 'Graph cycle detected (type=%s, cycle=%s).',
+ $cycle_edge_type,
+ $cycle_list));
}
public function getCycle() {
return $this->cycle;
}
public function getCycleEdgeType() {
return $this->cycleEdgeType;
}
}
diff --git a/src/infrastructure/edges/query/PhabricatorEdgeQuery.php b/src/infrastructure/edges/query/PhabricatorEdgeQuery.php
index 1323d42e3..2dfceb7fb 100644
--- a/src/infrastructure/edges/query/PhabricatorEdgeQuery.php
+++ b/src/infrastructure/edges/query/PhabricatorEdgeQuery.php
@@ -1,332 +1,333 @@
<?php
/**
* Load object edges created by @{class:PhabricatorEdgeEditor}.
*
* name=Querying Edges
* $src = $earth_phid;
* $type = PhabricatorEdgeConfig::TYPE_BODY_HAS_SATELLITE;
*
* // Load the earth's satellites.
* $satellite_edges = id(new PhabricatorEdgeQuery())
* ->withSourcePHIDs(array($src))
* ->withEdgeTypes(array($type))
* ->execute();
*
* For more information on edges, see @{article:Using Edges}.
*
* @task config Configuring the Query
* @task exec Executing the Query
* @task internal Internal
*/
final class PhabricatorEdgeQuery extends PhabricatorQuery {
private $sourcePHIDs;
private $destPHIDs;
private $edgeTypes;
private $resultSet;
const ORDER_OLDEST_FIRST = 'order:oldest';
const ORDER_NEWEST_FIRST = 'order:newest';
private $order = self::ORDER_NEWEST_FIRST;
private $needEdgeData;
/* -( Configuring the Query )---------------------------------------------- */
/**
* Find edges originating at one or more source PHIDs. You MUST provide this
* to execute an edge query.
*
* @param list List of source PHIDs.
* @return this
*
* @task config
*/
public function withSourcePHIDs(array $source_phids) {
$this->sourcePHIDs = $source_phids;
return $this;
}
/**
* Find edges terminating at one or more destination PHIDs.
*
* @param list List of destination PHIDs.
* @return this
*
*/
public function withDestinationPHIDs(array $dest_phids) {
$this->destPHIDs = $dest_phids;
return $this;
}
/**
* Find edges of specific types.
*
* @param list List of PhabricatorEdgeConfig type constants.
* @return this
*
* @task config
*/
public function withEdgeTypes(array $types) {
$this->edgeTypes = $types;
return $this;
}
/**
* Configure the order edge results are returned in.
*
* @param const Order constant.
* @return this
*
* @task config
*/
public function setOrder($order) {
$this->order = $order;
return $this;
}
/**
* When loading edges, also load edge data.
*
* @param bool True to load edge data.
* @return this
*
* @task config
*/
public function needEdgeData($need) {
$this->needEdgeData = $need;
return $this;
}
/* -( Executing the Query )------------------------------------------------ */
/**
* Convenience method for loading destination PHIDs with one source and one
* edge type. Equivalent to building a full query, but simplifies a common
* use case.
*
* @param phid Source PHID.
* @param const Edge type.
* @return list<phid> List of destination PHIDs.
*/
public static function loadDestinationPHIDs($src_phid, $edge_type) {
$edges = id(new PhabricatorEdgeQuery())
->withSourcePHIDs(array($src_phid))
->withEdgeTypes(array($edge_type))
->execute();
return array_keys($edges[$src_phid][$edge_type]);
}
/**
* Convenience method for loading a single edge's metadata for
* a given source, destination, and edge type. Returns null
* if the edge does not exist or does not have metadata. Builds
* and immediately executes a full query.
*
* @param phid Source PHID.
* @param const Edge type.
* @param phid Destination PHID.
* @return wild Edge annotation (or null).
*/
public static function loadSingleEdgeData($src_phid, $edge_type, $dest_phid) {
$edges = id(new PhabricatorEdgeQuery())
->withSourcePHIDs(array($src_phid))
->withEdgeTypes(array($edge_type))
->withDestinationPHIDs(array($dest_phid))
->needEdgeData(true)
->execute();
if (isset($edges[$src_phid][$edge_type][$dest_phid]['data'])) {
return $edges[$src_phid][$edge_type][$dest_phid]['data'];
}
return null;
}
/**
* Load specified edges.
*
* @task exec
*/
public function execute() {
if (!$this->sourcePHIDs) {
throw new Exception(
- 'You must use withSourcePHIDs() to query edges.');
+ pht(
+ 'You must use %s to query edges.',
+ 'withSourcePHIDs()'));
}
$sources = phid_group_by_type($this->sourcePHIDs);
$result = array();
// When a query specifies types, make sure we return data for all queried
// types.
if ($this->edgeTypes) {
foreach ($this->sourcePHIDs as $phid) {
foreach ($this->edgeTypes as $type) {
$result[$phid][$type] = array();
}
}
}
foreach ($sources as $type => $phids) {
$conn_r = PhabricatorEdgeConfig::establishConnection($type, 'r');
$where = $this->buildWhereClause($conn_r);
$order = $this->buildOrderClause($conn_r);
$edges = queryfx_all(
$conn_r,
'SELECT edge.* FROM %T edge %Q %Q',
PhabricatorEdgeConfig::TABLE_NAME_EDGE,
$where,
$order);
if ($this->needEdgeData) {
$data_ids = array_filter(ipull($edges, 'dataID'));
$data_map = array();
if ($data_ids) {
$data_rows = queryfx_all(
$conn_r,
'SELECT edgedata.* FROM %T edgedata WHERE id IN (%Ld)',
PhabricatorEdgeConfig::TABLE_NAME_EDGEDATA,
$data_ids);
foreach ($data_rows as $row) {
$data_map[$row['id']] = idx(
phutil_json_decode($row['data']),
'data');
}
}
foreach ($edges as $key => $edge) {
$edges[$key]['data'] = idx($data_map, $edge['dataID'], array());
}
}
foreach ($edges as $edge) {
$result[$edge['src']][$edge['type']][$edge['dst']] = $edge;
}
}
$this->resultSet = $result;
return $result;
}
/**
* Convenience function for selecting edge destination PHIDs after calling
* execute().
*
* Returns a flat list of PHIDs matching the provided source PHID and type
* filters. By default, the filters are empty so all PHIDs will be returned.
* For example, if you're doing a batch query from several sources, you might
* write code like this:
*
* $query = new PhabricatorEdgeQuery();
* $query->setViewer($viewer);
* $query->withSourcePHIDs(mpull($objects, 'getPHID'));
* $query->withEdgeTypes(array($some_type));
* $query->execute();
*
* // Gets all of the destinations.
* $all_phids = $query->getDestinationPHIDs();
* $handles = id(new PhabricatorHandleQuery())
* ->setViewer($viewer)
* ->withPHIDs($all_phids)
* ->execute();
*
* foreach ($objects as $object) {
* // Get all of the destinations for the given object.
* $dst_phids = $query->getDestinationPHIDs(array($object->getPHID()));
* $object->attachHandles(array_select_keys($handles, $dst_phids));
* }
*
* @param list? List of PHIDs to select, or empty to select all.
* @param list? List of edge types to select, or empty to select all.
* @return list<phid> List of matching destination PHIDs.
*/
public function getDestinationPHIDs(
array $src_phids = array(),
array $types = array()) {
if ($this->resultSet === null) {
- throw new Exception(
- 'You must execute() a query before you you can getDestinationPHIDs().');
+ throw new PhutilInvalidStateException('execute');
}
$result_phids = array();
$set = $this->resultSet;
if ($src_phids) {
$set = array_select_keys($set, $src_phids);
}
foreach ($set as $src => $edges_by_type) {
if ($types) {
$edges_by_type = array_select_keys($edges_by_type, $types);
}
foreach ($edges_by_type as $edges) {
foreach ($edges as $edge_phid => $edge) {
$result_phids[$edge_phid] = true;
}
}
}
return array_keys($result_phids);
}
/* -( Internals )---------------------------------------------------------- */
/**
* @task internal
*/
protected function buildWhereClause(AphrontDatabaseConnection $conn_r) {
$where = array();
if ($this->sourcePHIDs) {
$where[] = qsprintf(
$conn_r,
'edge.src IN (%Ls)',
$this->sourcePHIDs);
}
if ($this->edgeTypes) {
$where[] = qsprintf(
$conn_r,
'edge.type IN (%Ls)',
$this->edgeTypes);
}
if ($this->destPHIDs) {
// potentially complain if $this->edgeType was not set
$where[] = qsprintf(
$conn_r,
'edge.dst IN (%Ls)',
$this->destPHIDs);
}
return $this->formatWhereClause($where);
}
/**
* @task internal
*/
private function buildOrderClause($conn_r) {
if ($this->order == self::ORDER_NEWEST_FIRST) {
return 'ORDER BY edge.dateCreated DESC, edge.seq DESC';
} else {
return 'ORDER BY edge.dateCreated ASC, edge.seq ASC';
}
}
}
diff --git a/src/infrastructure/edges/type/PhabricatorEdgeType.php b/src/infrastructure/edges/type/PhabricatorEdgeType.php
index c485efd98..d62c5a967 100644
--- a/src/infrastructure/edges/type/PhabricatorEdgeType.php
+++ b/src/infrastructure/edges/type/PhabricatorEdgeType.php
@@ -1,232 +1,236 @@
<?php
/**
* Defines an edge type.
*
* Edges are typed, directed connections between two objects. They are used to
* represent most simple relationships, like when a user is subscribed to an
* object or an object is a member of a project.
*
* @task load Loading Types
*/
abstract class PhabricatorEdgeType extends Phobject {
final public function getEdgeConstant() {
$class = new ReflectionClass($this);
$const = $class->getConstant('EDGECONST');
if ($const === false) {
throw new Exception(
pht(
- 'EdgeType class "%s" must define an EDGECONST property.',
- get_class($this)));
+ '%s class "%s" must define an %s property.',
+ __CLASS__,
+ get_class($this),
+ 'EDGECONST'));
}
if (!is_int($const) || ($const <= 0)) {
throw new Exception(
pht(
- 'EdgeType class "%s" has an invalid EDGECONST property. Edge '.
- 'constants must be positive integers.',
- get_class($this)));
+ '%s class "%s" has an invalid %s property. '.
+ 'Edge constants must be positive integers.',
+ __CLASS__,
+ get_class($this),
+ 'EDGECONST'));
}
return $const;
}
public function getInverseEdgeConstant() {
return null;
}
public function shouldPreventCycles() {
return false;
}
public function shouldWriteInverseTransactions() {
return false;
}
public function getTransactionPreviewString($actor) {
return pht(
'%s edited edge metadata.',
$actor);
}
public function getTransactionAddString(
$actor,
$add_count,
$add_edges) {
return pht(
'%s added %s edge(s): %s.',
$actor,
$add_count,
$add_edges);
}
public function getTransactionRemoveString(
$actor,
$rem_count,
$rem_edges) {
return pht(
'%s removed %s edge(s): %s.',
$actor,
$rem_count,
$rem_edges);
}
public function getTransactionEditString(
$actor,
$total_count,
$add_count,
$add_edges,
$rem_count,
$rem_edges) {
return pht(
'%s edited %s edge(s), added %s: %s; removed %s: %s.',
$actor,
$total_count,
$add_count,
$add_edges,
$rem_count,
$rem_edges);
}
public function getFeedAddString(
$actor,
$object,
$add_count,
$add_edges) {
return pht(
'%s added %s edge(s) to %s: %s.',
$actor,
$add_count,
$object,
$add_edges);
}
public function getFeedRemoveString(
$actor,
$object,
$rem_count,
$rem_edges) {
return pht(
'%s removed %s edge(s) from %s: %s.',
$actor,
$rem_count,
$object,
$rem_edges);
}
public function getFeedEditString(
$actor,
$object,
$total_count,
$add_count,
$add_edges,
$rem_count,
$rem_edges) {
return pht(
'%s edited %s edge(s) for %s, added %s: %s; removed %s: %s.',
$actor,
$total_count,
$object,
$add_count,
$add_edges,
$rem_count,
$rem_edges);
}
/* -( Loading Types )------------------------------------------------------ */
/**
* @task load
*/
public static function getAllTypes() {
static $type_map;
if ($type_map === null) {
$types = id(new PhutilSymbolLoader())
->setAncestorClass(__CLASS__)
->loadObjects();
$map = array();
foreach ($types as $class => $type) {
$const = $type->getEdgeConstant();
if (isset($map[$const])) {
throw new Exception(
pht(
'Two edge types ("%s", "%s") share the same edge constant '.
'(%d). Each edge type must have a unique constant.',
$class,
get_class($map[$const]),
$const));
}
$map[$const] = $type;
}
// Check that all the inverse edge definitions actually make sense. If
// edge type A says B is its inverse, B must exist and say that A is its
// inverse.
foreach ($map as $const => $type) {
$inverse = $type->getInverseEdgeConstant();
if ($inverse === null) {
continue;
}
if (empty($map[$inverse])) {
throw new Exception(
pht(
'Edge type "%s" ("%d") defines an inverse type ("%d") which '.
'does not exist.',
get_class($type),
$const,
$inverse));
}
$inverse_inverse = $map[$inverse]->getInverseEdgeConstant();
if ($inverse_inverse !== $const) {
throw new Exception(
pht(
'Edge type "%s" ("%d") defines an inverse type ("%d"), but that '.
'inverse type defines a different type ("%d") as its '.
'inverse.',
get_class($type),
$const,
$inverse,
$inverse_inverse));
}
}
$type_map = $map;
}
return $type_map;
}
/**
* @task load
*/
public static function getByConstant($const) {
$type = idx(self::getAllTypes(), $const);
if (!$type) {
throw new Exception(
pht('Unknown edge constant "%s"!', $const));
}
return $type;
}
}
diff --git a/src/infrastructure/edges/util/PhabricatorEdgeGraph.php b/src/infrastructure/edges/util/PhabricatorEdgeGraph.php
index 57b422328..457b9ec70 100644
--- a/src/infrastructure/edges/util/PhabricatorEdgeGraph.php
+++ b/src/infrastructure/edges/util/PhabricatorEdgeGraph.php
@@ -1,34 +1,34 @@
<?php
final class PhabricatorEdgeGraph extends AbstractDirectedGraph {
private $edgeType;
public function setEdgeType($edge_type) {
$this->edgeType = $edge_type;
return $this;
}
protected function loadEdges(array $nodes) {
if (!$this->edgeType) {
- throw new Exception('Set edge type before loading graph!');
+ throw new Exception(pht('Set edge type before loading graph!'));
}
$edges = id(new PhabricatorEdgeQuery())
->withSourcePHIDs($nodes)
->withEdgeTypes(array($this->edgeType))
->execute();
$results = array_fill_keys($nodes, array());
foreach ($edges as $src => $types) {
foreach ($types as $type => $dsts) {
foreach ($dsts as $dst => $edge) {
$results[$src][] = $dst;
}
}
}
return $results;
}
}
diff --git a/src/infrastructure/env/PhabricatorConfigFileSource.php b/src/infrastructure/env/PhabricatorConfigFileSource.php
index fa02040c3..6855a9f0b 100644
--- a/src/infrastructure/env/PhabricatorConfigFileSource.php
+++ b/src/infrastructure/env/PhabricatorConfigFileSource.php
@@ -1,23 +1,22 @@
<?php
/**
* Configuration source which reads from a configuration file on disk (a
- * PHP file in the conf/ directory). This source
+ * PHP file in the `conf/` directory).
*/
-final class PhabricatorConfigFileSource
- extends PhabricatorConfigProxySource {
+final class PhabricatorConfigFileSource extends PhabricatorConfigProxySource {
/**
* @phutil-external-symbol function phabricator_read_config_file
*/
public function __construct($config) {
$root = dirname(phutil_get_library_root('phabricator'));
require_once $root.'/conf/__init_conf__.php';
$dictionary = phabricator_read_config_file($config);
$dictionary['phabricator.env'] = $config;
$this->setSource(new PhabricatorConfigDictionarySource($dictionary));
}
}
diff --git a/src/infrastructure/env/PhabricatorConfigLocalSource.php b/src/infrastructure/env/PhabricatorConfigLocalSource.php
index 8e3c52406..3a2295eb5 100644
--- a/src/infrastructure/env/PhabricatorConfigLocalSource.php
+++ b/src/infrastructure/env/PhabricatorConfigLocalSource.php
@@ -1,51 +1,50 @@
<?php
-final class PhabricatorConfigLocalSource
- extends PhabricatorConfigProxySource {
+final class PhabricatorConfigLocalSource extends PhabricatorConfigProxySource {
public function __construct() {
$config = $this->loadConfig();
$this->setSource(new PhabricatorConfigDictionarySource($config));
}
public function setKeys(array $keys) {
$result = parent::setKeys($keys);
$this->saveConfig();
return $result;
}
public function deleteKeys(array $keys) {
$result = parent::deleteKeys($keys);
$this->saveConfig();
return parent::deleteKeys($keys);
}
private function loadConfig() {
$path = $this->getConfigPath();
if (@file_exists($path)) {
$data = @file_get_contents($path);
if ($data) {
$data = json_decode($data, true);
if (is_array($data)) {
return $data;
}
}
}
return array();
}
private function saveConfig() {
$config = $this->getSource()->getAllKeys();
$json = new PhutilJSON();
$data = $json->encodeFormatted($config);
Filesystem::writeFile($this->getConfigPath(), $data);
}
private function getConfigPath() {
$root = dirname(phutil_get_library_root('phabricator'));
$path = $root.'/conf/local/local.json';
return $path;
}
}
diff --git a/src/infrastructure/env/PhabricatorConfigProxySource.php b/src/infrastructure/env/PhabricatorConfigProxySource.php
index 818b55452..23e4d0a22 100644
--- a/src/infrastructure/env/PhabricatorConfigProxySource.php
+++ b/src/infrastructure/env/PhabricatorConfigProxySource.php
@@ -1,54 +1,54 @@
<?php
/**
* Configuration source which proxies some other configuration source.
*/
abstract class PhabricatorConfigProxySource
extends PhabricatorConfigSource {
private $source;
final protected function getSource() {
if (!$this->source) {
- throw new Exception('No configuration source set!');
+ throw new Exception(pht('No configuration source set!'));
}
return $this->source;
}
final protected function setSource(PhabricatorConfigSource $source) {
$this->source = $source;
return $this;
}
public function getAllKeys() {
return $this->getSource()->getAllKeys();
}
public function getKeys(array $keys) {
return $this->getSource()->getKeys($keys);
}
public function canWrite() {
return $this->getSource()->canWrite();
}
public function setKeys(array $keys) {
$this->getSource()->setKeys($keys);
return $this;
}
public function deleteKeys(array $keys) {
$this->getSource()->deleteKeys($keys);
return $this;
}
public function setName($name) {
$this->getSource()->setName($name);
return $this;
}
public function getName() {
return $this->getSource()->getName();
}
}
diff --git a/src/infrastructure/env/PhabricatorConfigSource.php b/src/infrastructure/env/PhabricatorConfigSource.php
index dd52a18c7..1fbdf2f20 100644
--- a/src/infrastructure/env/PhabricatorConfigSource.php
+++ b/src/infrastructure/env/PhabricatorConfigSource.php
@@ -1,31 +1,33 @@
<?php
abstract class PhabricatorConfigSource {
private $name;
public function setName($name) {
$this->name = $name;
return $this;
}
public function getName() {
return $this->name;
}
abstract public function getKeys(array $keys);
abstract public function getAllKeys();
public function canWrite() {
return false;
}
public function setKeys(array $keys) {
- throw new Exception('This configuration source does not support writes.');
+ throw new Exception(
+ pht('This configuration source does not support writes.'));
}
public function deleteKeys(array $keys) {
- throw new Exception('This configuration source does not support writes.');
+ throw new Exception(
+ pht('This configuration source does not support writes.'));
}
}
diff --git a/src/infrastructure/env/PhabricatorConfigStackSource.php b/src/infrastructure/env/PhabricatorConfigStackSource.php
index a118cf01b..a8c5113f1 100644
--- a/src/infrastructure/env/PhabricatorConfigStackSource.php
+++ b/src/infrastructure/env/PhabricatorConfigStackSource.php
@@ -1,80 +1,80 @@
<?php
/**
* Configuration source which reads from a stack of other configuration
* sources.
*
* This source is writable if any source in the stack is writable. Writes happen
* to the first writable source only.
*/
final class PhabricatorConfigStackSource
extends PhabricatorConfigSource {
private $stack = array();
public function pushSource(PhabricatorConfigSource $source) {
array_unshift($this->stack, $source);
return $this;
}
public function popSource() {
if (empty($this->stack)) {
- throw new Exception('Popping an empty config stack!');
+ throw new Exception(pht('Popping an empty %s!', __CLASS__));
}
return array_shift($this->stack);
}
public function getStack() {
return $this->stack;
}
public function getKeys(array $keys) {
$result = array();
foreach ($this->stack as $source) {
$result = $result + $source->getKeys($keys);
}
return $result;
}
public function getAllKeys() {
$result = array();
foreach ($this->stack as $source) {
$result = $result + $source->getAllKeys();
}
return $result;
}
public function canWrite() {
foreach ($this->stack as $source) {
if ($source->canWrite()) {
return true;
}
}
return false;
}
public function setKeys(array $keys) {
foreach ($this->stack as $source) {
if ($source->canWrite()) {
$source->setKeys($keys);
return;
}
}
// We can't write; this will throw an appropriate exception.
parent::setKeys($keys);
}
public function deleteKeys(array $keys) {
foreach ($this->stack as $source) {
if ($source->canWrite()) {
$source->deleteKeys($keys);
return;
}
}
// We can't write; this will throw an appropriate exception.
parent::deleteKeys($keys);
}
}
diff --git a/src/infrastructure/env/PhabricatorEnv.php b/src/infrastructure/env/PhabricatorEnv.php
index 39f911e3c..4d1f8ad4a 100644
--- a/src/infrastructure/env/PhabricatorEnv.php
+++ b/src/infrastructure/env/PhabricatorEnv.php
@@ -1,876 +1,882 @@
<?php
/**
* Manages the execution environment configuration, exposing APIs to read
* configuration settings and other similar values that are derived directly
* from configuration settings.
*
*
* = Reading Configuration =
*
* The primary role of this class is to provide an API for reading
* Phabricator configuration, @{method:getEnvConfig}:
*
* $value = PhabricatorEnv::getEnvConfig('some.key', $default);
*
* The class also handles some URI construction based on configuration, via
* the methods @{method:getURI}, @{method:getProductionURI},
* @{method:getCDNURI}, and @{method:getDoclink}.
*
* For configuration which allows you to choose a class to be responsible for
* some functionality (e.g., which mail adapter to use to deliver email),
* @{method:newObjectFromConfig} provides a simple interface that validates
* the configured value.
*
*
* = Unit Test Support =
*
* In unit tests, you can use @{method:beginScopedEnv} to create a temporary,
* mutable environment. The method returns a scope guard object which restores
* the environment when it is destroyed. For example:
*
* public function testExample() {
* $env = PhabricatorEnv::beginScopedEnv();
* $env->overrideEnv('some.key', 'new-value-for-this-test');
*
* // Some test which depends on the value of 'some.key'.
*
* }
*
* Your changes will persist until the `$env` object leaves scope or is
* destroyed.
*
* You should //not// use this in normal code.
*
*
* @task read Reading Configuration
* @task uri URI Validation
* @task test Unit Test Support
* @task internal Internals
*/
final class PhabricatorEnv {
private static $sourceStack;
private static $repairSource;
private static $overrideSource;
private static $requestBaseURI;
private static $cache;
private static $localeCode;
/**
* @phutil-external-symbol class PhabricatorStartup
*/
public static function initializeWebEnvironment() {
self::initializeCommonEnvironment();
}
public static function initializeScriptEnvironment() {
self::initializeCommonEnvironment();
// NOTE: This is dangerous in general, but we know we're in a script context
// and are not vulnerable to CSRF.
AphrontWriteGuard::allowDangerousUnguardedWrites(true);
// There are several places where we log information (about errors, events,
// service calls, etc.) for analysis via DarkConsole or similar. These are
// useful for web requests, but grow unboundedly in long-running scripts and
// daemons. Discard data as it arrives in these cases.
PhutilServiceProfiler::getInstance()->enableDiscardMode();
DarkConsoleErrorLogPluginAPI::enableDiscardMode();
DarkConsoleEventPluginAPI::enableDiscardMode();
}
private static function initializeCommonEnvironment() {
PhutilErrorHandler::initialize();
self::buildConfigurationSourceStack();
// Force a valid timezone. If both PHP and Phabricator configuration are
// invalid, use UTC.
$tz = self::getEnvConfig('phabricator.timezone');
if ($tz) {
@date_default_timezone_set($tz);
}
$ok = @date_default_timezone_set(date_default_timezone_get());
if (!$ok) {
date_default_timezone_set('UTC');
}
// Prepend '/support/bin' and append any paths to $PATH if we need to.
$env_path = getenv('PATH');
$phabricator_path = dirname(phutil_get_library_root('phabricator'));
$support_path = $phabricator_path.'/support/bin';
$env_path = $support_path.PATH_SEPARATOR.$env_path;
$append_dirs = self::getEnvConfig('environment.append-paths');
if (!empty($append_dirs)) {
$append_path = implode(PATH_SEPARATOR, $append_dirs);
$env_path = $env_path.PATH_SEPARATOR.$append_path;
}
putenv('PATH='.$env_path);
// Write this back into $_ENV, too, so ExecFuture picks it up when creating
// subprocess environments.
$_ENV['PATH'] = $env_path;
// If an instance identifier is defined, write it into the environment so
// it's available to subprocesses.
$instance = self::getEnvConfig('cluster.instance');
if (strlen($instance)) {
putenv('PHABRICATOR_INSTANCE='.$instance);
$_ENV['PHABRICATOR_INSTANCE'] = $instance;
}
PhabricatorEventEngine::initialize();
// TODO: Add a "locale.default" config option once we have some reasonable
// defaults which aren't silly nonsense.
self::setLocaleCode('en_US');
}
public static function setLocaleCode($locale_code) {
if ($locale_code == self::$localeCode) {
return;
}
try {
$locale = PhutilLocale::loadLocale($locale_code);
$translations = PhutilTranslation::getTranslationMapForLocale(
$locale_code);
$override = self::getEnvConfig('translation.override');
if (!is_array($override)) {
$override = array();
}
PhutilTranslator::getInstance()
->setLocale($locale)
->setTranslations($override + $translations);
self::$localeCode = $locale_code;
} catch (Exception $ex) {
// Just ignore this; the user likely has an out-of-date locale code.
}
}
private static function buildConfigurationSourceStack() {
self::dropConfigCache();
$stack = new PhabricatorConfigStackSource();
self::$sourceStack = $stack;
$default_source = id(new PhabricatorConfigDefaultSource())
->setName(pht('Global Default'));
$stack->pushSource($default_source);
$env = self::getSelectedEnvironmentName();
if ($env) {
$stack->pushSource(
id(new PhabricatorConfigFileSource($env))
->setName(pht("File '%s'", $env)));
}
$stack->pushSource(
id(new PhabricatorConfigLocalSource())
->setName(pht('Local Config')));
// If the install overrides the database adapter, we might need to load
// the database adapter class before we can push on the database config.
// This config is locked and can't be edited from the web UI anyway.
foreach (self::getEnvConfig('load-libraries') as $library) {
phutil_load_library($library);
}
// If custom libraries specify config options, they won't get default
// values as the Default source has already been loaded, so we get it to
// pull in all options from non-phabricator libraries now they are loaded.
$default_source->loadExternalOptions();
// If this install has site config sources, load them now.
$site_sources = id(new PhutilSymbolLoader())
->setAncestorClass('PhabricatorConfigSiteSource')
->loadObjects();
$site_sources = msort($site_sources, 'getPriority');
foreach ($site_sources as $site_source) {
$stack->pushSource($site_source);
}
try {
$stack->pushSource(
id(new PhabricatorConfigDatabaseSource('default'))
->setName(pht('Database')));
} catch (AphrontQueryException $exception) {
// If the database is not available, just skip this configuration
// source. This happens during `bin/storage upgrade`, `bin/conf` before
// schema setup, etc.
}
}
public static function repairConfig($key, $value) {
if (!self::$repairSource) {
self::$repairSource = id(new PhabricatorConfigDictionarySource(array()))
->setName(pht('Repaired Config'));
self::$sourceStack->pushSource(self::$repairSource);
}
self::$repairSource->setKeys(array($key => $value));
self::dropConfigCache();
}
public static function overrideConfig($key, $value) {
if (!self::$overrideSource) {
self::$overrideSource = id(new PhabricatorConfigDictionarySource(array()))
->setName(pht('Overridden Config'));
self::$sourceStack->pushSource(self::$overrideSource);
}
self::$overrideSource->setKeys(array($key => $value));
self::dropConfigCache();
}
public static function getUnrepairedEnvConfig($key, $default = null) {
foreach (self::$sourceStack->getStack() as $source) {
if ($source === self::$repairSource) {
continue;
}
$result = $source->getKeys(array($key));
if ($result) {
return $result[$key];
}
}
return $default;
}
public static function getSelectedEnvironmentName() {
$env_var = 'PHABRICATOR_ENV';
$env = idx($_SERVER, $env_var);
if (!$env) {
$env = getenv($env_var);
}
if (!$env) {
$env = idx($_ENV, $env_var);
}
if (!$env) {
$root = dirname(phutil_get_library_root('phabricator'));
$path = $root.'/conf/local/ENVIRONMENT';
if (Filesystem::pathExists($path)) {
$env = trim(Filesystem::readFile($path));
}
}
return $env;
}
public static function calculateEnvironmentHash() {
$keys = self::getKeysForConsistencyCheck();
$values = array();
foreach ($keys as $key) {
$values[$key] = self::getEnvConfigIfExists($key);
}
return PhabricatorHash::digest(json_encode($values));
}
/**
* Returns a summary of non-default configuration settings to allow the
* "daemons and web have different config" setup check to list divergent
* keys.
*/
public static function calculateEnvironmentInfo() {
$keys = self::getKeysForConsistencyCheck();
$info = array();
$defaults = id(new PhabricatorConfigDefaultSource())->getAllKeys();
foreach ($keys as $key) {
$current = self::getEnvConfigIfExists($key);
$default = idx($defaults, $key, null);
if ($current !== $default) {
$info[$key] = PhabricatorHash::digestForIndex(json_encode($current));
}
}
$keys_hash = array_keys($defaults);
sort($keys_hash);
$keys_hash = implode("\0", $keys_hash);
$keys_hash = PhabricatorHash::digestForIndex($keys_hash);
return array(
'version' => 1,
'keys' => $keys_hash,
'values' => $info,
);
}
/**
* Compare two environment info summaries to generate a human-readable
* list of discrepancies.
*/
public static function compareEnvironmentInfo(array $u, array $v) {
$issues = array();
$uversion = idx($u, 'version');
$vversion = idx($v, 'version');
if ($uversion != $vversion) {
$issues[] = pht(
'The two configurations were generated by different versions '.
'of Phabricator.');
// These may not be comparable, so stop here.
return $issues;
}
if ($u['keys'] !== $v['keys']) {
$issues[] = pht(
'The two configurations have different keys. This usually means '.
'that they are running different versions of Phabricator.');
}
$uval = idx($u, 'values', array());
$vval = idx($v, 'values', array());
$all_keys = array_keys($uval + $vval);
foreach ($all_keys as $key) {
$uv = idx($uval, $key);
$vv = idx($vval, $key);
if ($uv !== $vv) {
if ($uv && $vv) {
$issues[] = pht(
'The configuration key "%s" is set in both configurations, but '.
'set to different values.',
$key);
} else {
$issues[] = pht(
'The configuration key "%s" is set in only one configuration.',
$key);
}
}
}
return $issues;
}
private static function getKeysForConsistencyCheck() {
$keys = array_keys(self::getAllConfigKeys());
sort($keys);
$skip_keys = self::getEnvConfig('phd.variant-config');
return array_diff($keys, $skip_keys);
}
/* -( Reading Configuration )---------------------------------------------- */
/**
* Get the current configuration setting for a given key.
*
* If the key is not found, then throw an Exception.
*
* @task read
*/
public static function getEnvConfig($key) {
if (isset(self::$cache[$key])) {
return self::$cache[$key];
}
if (array_key_exists($key, self::$cache)) {
return self::$cache[$key];
}
$result = self::$sourceStack->getKeys(array($key));
if (array_key_exists($key, $result)) {
self::$cache[$key] = $result[$key];
return $result[$key];
} else {
- throw new Exception("No config value specified for key '{$key}'.");
+ throw new Exception(
+ pht(
+ "No config value specified for key '%s'.",
+ $key));
}
}
/**
* Get the current configuration setting for a given key. If the key
* does not exist, return a default value instead of throwing. This is
* primarily useful for migrations involving keys which are slated for
* removal.
*
* @task read
*/
public static function getEnvConfigIfExists($key, $default = null) {
try {
return self::getEnvConfig($key);
} catch (Exception $ex) {
return $default;
}
}
/**
* Get the fully-qualified URI for a path.
*
* @task read
*/
public static function getURI($path) {
return rtrim(self::getAnyBaseURI(), '/').$path;
}
/**
* Get the fully-qualified production URI for a path.
*
* @task read
*/
public static function getProductionURI($path) {
// If we're passed a URI which already has a domain, simply return it
// unmodified. In particular, files may have URIs which point to a CDN
// domain.
$uri = new PhutilURI($path);
if ($uri->getDomain()) {
return $path;
}
$production_domain = self::getEnvConfig('phabricator.production-uri');
if (!$production_domain) {
$production_domain = self::getAnyBaseURI();
}
return rtrim($production_domain, '/').$path;
}
public static function getAllowedURIs($path) {
$uri = new PhutilURI($path);
if ($uri->getDomain()) {
return $path;
}
$allowed_uris = self::getEnvConfig('phabricator.allowed-uris');
$return = array();
foreach ($allowed_uris as $allowed_uri) {
$return[] = rtrim($allowed_uri, '/').$path;
}
return $return;
}
/**
* Get the fully-qualified production URI for a static resource path.
*
* @task read
*/
public static function getCDNURI($path) {
$alt = self::getEnvConfig('security.alternate-file-domain');
if (!$alt) {
$alt = self::getAnyBaseURI();
}
$uri = new PhutilURI($alt);
$uri->setPath($path);
return (string)$uri;
}
/**
* Get the fully-qualified production URI for a documentation resource.
*
* @task read
*/
public static function getDoclink($resource, $type = 'article') {
$uri = new PhutilURI('https://secure.phabricator.com/diviner/find/');
$uri->setQueryParam('name', $resource);
$uri->setQueryParam('type', $type);
$uri->setQueryParam('jump', true);
return (string)$uri;
}
/**
* Build a concrete object from a configuration key.
*
* @task read
*/
public static function newObjectFromConfig($key, $args = array()) {
$class = self::getEnvConfig($key);
return newv($class, $args);
}
public static function getAnyBaseURI() {
$base_uri = self::getEnvConfig('phabricator.base-uri');
if (!$base_uri) {
$base_uri = self::getRequestBaseURI();
}
if (!$base_uri) {
throw new Exception(
- "Define 'phabricator.base-uri' in your configuration to continue.");
+ pht(
+ "Define '%s' in your configuration to continue.",
+ 'phabricator.base-uri'));
}
return $base_uri;
}
public static function getRequestBaseURI() {
return self::$requestBaseURI;
}
public static function setRequestBaseURI($uri) {
self::$requestBaseURI = $uri;
}
/* -( Unit Test Support )-------------------------------------------------- */
/**
* @task test
*/
public static function beginScopedEnv() {
return new PhabricatorScopedEnv(self::pushTestEnvironment());
}
/**
* @task test
*/
private static function pushTestEnvironment() {
self::dropConfigCache();
$source = new PhabricatorConfigDictionarySource(array());
self::$sourceStack->pushSource($source);
return spl_object_hash($source);
}
/**
* @task test
*/
public static function popTestEnvironment($key) {
self::dropConfigCache();
$source = self::$sourceStack->popSource();
$stack_key = spl_object_hash($source);
if ($stack_key !== $key) {
self::$sourceStack->pushSource($source);
throw new Exception(
- 'Scoped environments were destroyed in a diffent order than they '.
- 'were initialized.');
+ pht(
+ 'Scoped environments were destroyed in a different order than they '.
+ 'were initialized.'));
}
}
/* -( URI Validation )----------------------------------------------------- */
/**
* Detect if a URI satisfies either @{method:isValidLocalURIForLink} or
* @{method:isValidRemoteURIForLink}, i.e. is a page on this server or the
* URI of some other resource which has a valid protocol. This rejects
* garbage URIs and URIs with protocols which do not appear in the
* `uri.allowed-protocols` configuration, notably 'javascript:' URIs.
*
* NOTE: This method is generally intended to reject URIs which it may be
* unsafe to put in an "href" link attribute.
*
* @param string URI to test.
* @return bool True if the URI identifies a web resource.
* @task uri
*/
public static function isValidURIForLink($uri) {
return self::isValidLocalURIForLink($uri) ||
self::isValidRemoteURIForLink($uri);
}
/**
* Detect if a URI identifies some page on this server.
*
* NOTE: This method is generally intended to reject URIs which it may be
* unsafe to issue a "Location:" redirect to.
*
* @param string URI to test.
* @return bool True if the URI identifies a local page.
* @task uri
*/
public static function isValidLocalURIForLink($uri) {
$uri = (string)$uri;
if (!strlen($uri)) {
return false;
}
if (preg_match('/\s/', $uri)) {
// PHP hasn't been vulnerable to header injection attacks for a bunch of
// years, but we can safely reject these anyway since they're never valid.
return false;
}
// Chrome (at a minimum) interprets backslashes in Location headers and the
// URL bar as forward slashes. This is probably intended to reduce user
// error caused by confusion over which key is "forward slash" vs "back
// slash".
//
// However, it means a URI like "/\evil.com" is interpreted like
// "//evil.com", which is a protocol relative remote URI.
//
// Since we currently never generate URIs with backslashes in them, reject
// these unconditionally rather than trying to figure out how browsers will
// interpret them.
if (preg_match('/\\\\/', $uri)) {
return false;
}
// Valid URIs must begin with '/', followed by the end of the string or some
// other non-'/' character. This rejects protocol-relative URIs like
// "//evil.com/evil_stuff/".
return (bool)preg_match('@^/([^/]|$)@', $uri);
}
/**
* Detect if a URI identifies some valid linkable remote resource.
*
* @param string URI to test.
* @return bool True if a URI idenfies a remote resource with an allowed
* protocol.
* @task uri
*/
public static function isValidRemoteURIForLink($uri) {
try {
self::requireValidRemoteURIForLink($uri);
return true;
} catch (Exception $ex) {
return false;
}
}
/**
* Detect if a URI identifies a valid linkable remote resource, throwing a
* detailed message if it does not.
*
* A valid linkable remote resource can be safely linked or redirected to.
* This is primarily a protocol whitelist check.
*
* @param string URI to test.
* @return void
* @task uri
*/
public static function requireValidRemoteURIForLink($uri) {
$uri = new PhutilURI($uri);
$proto = $uri->getProtocol();
if (!strlen($proto)) {
throw new Exception(
pht(
'URI "%s" is not a valid linkable resource. A valid linkable '.
'resource URI must specify a protocol.',
$uri));
}
$protocols = self::getEnvConfig('uri.allowed-protocols');
if (!isset($protocols[$proto])) {
throw new Exception(
pht(
'URI "%s" is not a valid linkable resource. A valid linkable '.
'resource URI must use one of these protocols: %s.',
$uri,
implode(', ', array_keys($protocols))));
}
$domain = $uri->getDomain();
if (!strlen($domain)) {
throw new Exception(
pht(
'URI "%s" is not a valid linkable resource. A valid linkable '.
'resource URI must specify a domain.',
$uri));
}
}
/**
* Detect if a URI identifies a valid fetchable remote resource.
*
* @param string URI to test.
* @param list<string> Allowed protocols.
* @return bool True if the URI is a valid fetchable remote resource.
* @task uri
*/
public static function isValidRemoteURIForFetch($uri, array $protocols) {
try {
self::requireValidRemoteURIForFetch($uri, $protocols);
return true;
} catch (Exception $ex) {
return false;
}
}
/**
* Detect if a URI identifies a valid fetchable remote resource, throwing
* a detailed message if it does not.
*
* A valid fetchable remote resource can be safely fetched using a request
* originating on this server. This is a primarily an address check against
* the outbound address blacklist.
*
* @param string URI to test.
* @param list<string> Allowed protocols.
* @return pair<string, string> Pre-resolved URI and domain.
* @task uri
*/
public static function requireValidRemoteURIForFetch(
$uri,
array $protocols) {
$uri = new PhutilURI($uri);
$proto = $uri->getProtocol();
if (!strlen($proto)) {
throw new Exception(
pht(
'URI "%s" is not a valid fetchable resource. A valid fetchable '.
'resource URI must specify a protocol.',
$uri));
}
$protocols = array_fuse($protocols);
if (!isset($protocols[$proto])) {
throw new Exception(
pht(
'URI "%s" is not a valid fetchable resource. A valid fetchable '.
'resource URI must use one of these protocols: %s.',
$uri,
implode(', ', array_keys($protocols))));
}
$domain = $uri->getDomain();
if (!strlen($domain)) {
throw new Exception(
pht(
'URI "%s" is not a valid fetchable resource. A valid fetchable '.
'resource URI must specify a domain.',
$uri));
}
$addresses = gethostbynamel($domain);
if (!$addresses) {
throw new Exception(
pht(
'URI "%s" is not a valid fetchable resource. The domain "%s" could '.
'not be resolved.',
$uri,
$domain));
}
foreach ($addresses as $address) {
if (self::isBlacklistedOutboundAddress($address)) {
throw new Exception(
pht(
'URI "%s" is not a valid fetchable resource. The domain "%s" '.
'resolves to the address "%s", which is blacklisted for '.
'outbound requests.',
$uri,
$domain,
$address));
}
}
$resolved_uri = clone $uri;
$resolved_uri->setDomain(head($addresses));
return array($resolved_uri, $domain);
}
/**
* Determine if an IP address is in the outbound address blacklist.
*
* @param string IP address.
* @return bool True if the address is blacklisted.
*/
public static function isBlacklistedOutboundAddress($address) {
$blacklist = self::getEnvConfig('security.outbound-blacklist');
return PhutilCIDRList::newList($blacklist)->containsAddress($address);
}
public static function isClusterRemoteAddress() {
$address = idx($_SERVER, 'REMOTE_ADDR');
if (!$address) {
throw new Exception(
pht(
'Unable to test remote address against cluster whitelist: '.
'REMOTE_ADDR is not defined.'));
}
return self::isClusterAddress($address);
}
public static function isClusterAddress($address) {
$cluster_addresses = self::getEnvConfig('cluster.addresses');
if (!$cluster_addresses) {
throw new Exception(
pht(
'Phabricator is not configured to serve cluster requests. '.
'Set `cluster.addresses` in the configuration to whitelist '.
'cluster hosts before sending requests that use a cluster '.
'authentication mechanism.'));
}
return PhutilCIDRList::newList($cluster_addresses)
->containsAddress($address);
}
/* -( Internals )---------------------------------------------------------- */
/**
* @task internal
*/
public static function envConfigExists($key) {
return array_key_exists($key, self::$sourceStack->getKeys(array($key)));
}
/**
* @task internal
*/
public static function getAllConfigKeys() {
return self::$sourceStack->getAllKeys();
}
public static function getConfigSourceStack() {
return self::$sourceStack;
}
/**
* @task internal
*/
public static function overrideTestEnvConfig($stack_key, $key, $value) {
$tmp = array();
// If we don't have the right key, we'll throw when popping the last
// source off the stack.
do {
$source = self::$sourceStack->popSource();
array_unshift($tmp, $source);
if (spl_object_hash($source) == $stack_key) {
$source->setKeys(array($key => $value));
break;
}
} while (true);
foreach ($tmp as $source) {
self::$sourceStack->pushSource($source);
}
self::dropConfigCache();
}
private static function dropConfigCache() {
self::$cache = array();
}
}
diff --git a/src/infrastructure/env/__tests__/PhabricatorEnvTestCase.php b/src/infrastructure/env/__tests__/PhabricatorEnvTestCase.php
index e363cf2fb..d47258822 100644
--- a/src/infrastructure/env/__tests__/PhabricatorEnvTestCase.php
+++ b/src/infrastructure/env/__tests__/PhabricatorEnvTestCase.php
@@ -1,220 +1,221 @@
<?php
final class PhabricatorEnvTestCase extends PhabricatorTestCase {
public function testLocalURIForLink() {
$map = array(
'/' => true,
'/D123' => true,
'/path/to/something/' => true,
"/path/to/\nHeader: x" => false,
'http://evil.com/' => false,
'//evil.com/evil/' => false,
'javascript:lol' => false,
'' => false,
null => false,
'/\\evil.com' => false,
);
foreach ($map as $uri => $expect) {
$this->assertEqual(
$expect,
PhabricatorEnv::isValidLocalURIForLink($uri),
- "Valid local resource: {$uri}");
+ pht('Valid local resource: %s', $uri));
}
}
public function testRemoteURIForLink() {
$map = array(
'http://example.com/' => true,
'derp://example.com/' => false,
'javascript:alert(1)' => false,
'http://127.0.0.1/' => true,
'http://169.254.169.254/latest/meta-data/hostname' => true,
);
foreach ($map as $uri => $expect) {
$this->assertEqual(
$expect,
PhabricatorEnv::isValidRemoteURIForLink($uri),
- "Valid linkable remote URI: {$uri}");
+ pht('Valid linkable remote URI: %s', $uri));
}
}
public function testRemoteURIForFetch() {
$map = array(
'http://example.com/' => true,
// No domain or protocol.
'' => false,
// No domain.
'http://' => false,
// No protocol.
'evil.com' => false,
// No protocol.
'//evil.com' => false,
// Bad protocol.
'javascript://evil.com/' => false,
'file:///etc/shadow' => false,
// Unresolvable hostname.
'http://u1VcxwUp368SIFzl7rkWWg23KM5JPB2kTHHngxjXCQc.zzz/' => false,
// Domains explicitly in blacklisted IP space.
'http://127.0.0.1/' => false,
'http://169.254.169.254/latest/meta-data/hostname' => false,
// Domain resolves into blacklisted IP space.
'http://localhost/' => false,
);
$protocols = array('http', 'https');
foreach ($map as $uri => $expect) {
$this->assertEqual(
$expect,
PhabricatorEnv::isValidRemoteURIForFetch($uri, $protocols),
- "Valid fetchable remote URI: {$uri}");
+ pht('Valid fetchable remote URI: %s', $uri));
}
}
public function testDictionarySource() {
$source = new PhabricatorConfigDictionarySource(array('x' => 1));
$this->assertEqual(
array(
'x' => 1,
),
$source->getKeys(array('x', 'z')));
$source->setKeys(array('z' => 2));
$this->assertEqual(
array(
'x' => 1,
'z' => 2,
),
$source->getKeys(array('x', 'z')));
$source->setKeys(array('x' => 3));
$this->assertEqual(
array(
'x' => 3,
'z' => 2,
),
$source->getKeys(array('x', 'z')));
$source->deleteKeys(array('x'));
$this->assertEqual(
array(
'z' => 2,
),
$source->getKeys(array('x', 'z')));
}
public function testStackSource() {
$s1 = new PhabricatorConfigDictionarySource(array('x' => 1));
$s2 = new PhabricatorConfigDictionarySource(array('x' => 2));
$stack = new PhabricatorConfigStackSource();
$this->assertEqual(array(), $stack->getKeys(array('x')));
$stack->pushSource($s1);
$this->assertEqual(array('x' => 1), $stack->getKeys(array('x')));
$stack->pushSource($s2);
$this->assertEqual(array('x' => 2), $stack->getKeys(array('x')));
$stack->setKeys(array('x' => 3));
$this->assertEqual(array('x' => 3), $stack->getKeys(array('x')));
$stack->popSource();
$this->assertEqual(array('x' => 1), $stack->getKeys(array('x')));
$stack->popSource();
$this->assertEqual(array(), $stack->getKeys(array('x')));
$caught = null;
try {
$stack->popSource();
} catch (Exception $ex) {
$caught = $ex;
}
$this->assertTrue($caught instanceof Exception);
}
public function testOverrides() {
$outer = PhabricatorEnv::beginScopedEnv();
$outer->overrideEnvConfig('test.value', 1);
$this->assertEqual(1, PhabricatorEnv::getEnvConfig('test.value'));
$inner = PhabricatorEnv::beginScopedEnv();
$inner->overrideEnvConfig('test.value', 2);
$this->assertEqual(2, PhabricatorEnv::getEnvConfig('test.value'));
if (phutil_is_hiphop_runtime()) {
$inner->__destruct();
}
unset($inner);
$this->assertEqual(1, PhabricatorEnv::getEnvConfig('test.value'));
if (phutil_is_hiphop_runtime()) {
$outer->__destruct();
}
unset($outer);
}
public function testOverrideOrder() {
$outer = PhabricatorEnv::beginScopedEnv();
$inner = PhabricatorEnv::beginScopedEnv();
$caught = null;
try {
$outer->__destruct();
} catch (Exception $ex) {
$caught = $ex;
}
$this->assertTrue(
$caught instanceof Exception,
- 'Destroying a scoped environment which is not on the top of the stack '.
- 'should throw.');
+ pht(
+ 'Destroying a scoped environment which is not on the top of the '.
+ 'stack should throw.'));
if (phutil_is_hiphop_runtime()) {
$inner->__destruct();
}
unset($inner);
if (phutil_is_hiphop_runtime()) {
$outer->__destruct();
}
unset($outer);
}
public function testGetEnvExceptions() {
$caught = null;
try {
PhabricatorEnv::getEnvConfig('not.a.real.config.option');
} catch (Exception $ex) {
$caught = $ex;
}
$this->assertTrue($caught instanceof Exception);
$caught = null;
try {
PhabricatorEnv::getEnvConfig('test.value');
} catch (Exception $ex) {
$caught = $ex;
}
$this->assertFalse($caught instanceof Exception);
}
}
diff --git a/src/infrastructure/events/PhabricatorEventEngine.php b/src/infrastructure/events/PhabricatorEventEngine.php
index 67807317c..0cbeb478a 100644
--- a/src/infrastructure/events/PhabricatorEventEngine.php
+++ b/src/infrastructure/events/PhabricatorEventEngine.php
@@ -1,49 +1,49 @@
<?php
final class PhabricatorEventEngine {
public static function initialize() {
// NOTE: If any of this fails, we just log it and move on. It's important
// to try to make it through here because users may have difficulty fixing
// fix the errors if we don't: for example, if we fatal here a user may not
// be able to run `bin/config` in order to remove an invalid listener.
// Load automatic listeners.
$listeners = id(new PhutilSymbolLoader())
->setAncestorClass('PhabricatorAutoEventListener')
->loadObjects();
// Load configured listeners.
$config_listeners = PhabricatorEnv::getEnvConfig('events.listeners');
foreach ($config_listeners as $listener_class) {
try {
$listeners[] = newv($listener_class, array());
} catch (Exception $ex) {
phlog($ex);
}
}
- // Add builtin listeners.
+ // Add built-in listeners.
$listeners[] = new DarkConsoleEventPluginAPI();
// Add application listeners.
$applications = PhabricatorApplication::getAllInstalledApplications();
foreach ($applications as $application) {
$app_listeners = $application->getEventListeners();
foreach ($app_listeners as $listener) {
$listener->setApplication($application);
$listeners[] = $listener;
}
}
// Now, register all of the listeners.
foreach ($listeners as $listener) {
try {
$listener->register();
} catch (Exception $ex) {
phlog($ex);
}
}
}
}
diff --git a/src/infrastructure/events/PhabricatorEventListener.php b/src/infrastructure/events/PhabricatorEventListener.php
index abaad375e..be0750016 100644
--- a/src/infrastructure/events/PhabricatorEventListener.php
+++ b/src/infrastructure/events/PhabricatorEventListener.php
@@ -1,51 +1,51 @@
<?php
abstract class PhabricatorEventListener extends PhutilEventListener {
private $application;
public function setApplication(PhabricatorApplication $application) {
$this->application = $application;
return $this;
}
public function getApplication() {
return $this->application;
}
public function hasApplicationCapability(
PhabricatorUser $viewer,
$capability) {
return PhabricatorPolicyFilter::hasCapability(
$viewer,
$this->getApplication(),
$capability);
}
public function canUseApplication(PhabricatorUser $viewer) {
return $this->hasApplicationCapability(
$viewer,
PhabricatorPolicyCapability::CAN_VIEW);
}
protected function addActionMenuItems(PhutilEvent $event, $items) {
if ($event->getType() !== PhabricatorEventType::TYPE_UI_DIDRENDERACTIONS) {
- throw new Exception('Not an action menu event!');
+ throw new Exception(pht('Not an action menu event!'));
}
if (!$items) {
return;
}
if (!is_array($items)) {
$items = array($items);
}
$event_actions = $event->getValue('actions');
foreach ($items as $item) {
$event_actions[] = $item;
}
$event->setValue('actions', $event_actions);
}
}
diff --git a/src/infrastructure/events/PhabricatorExampleEventListener.php b/src/infrastructure/events/PhabricatorExampleEventListener.php
index fb85678da..3e3353f37 100644
--- a/src/infrastructure/events/PhabricatorExampleEventListener.php
+++ b/src/infrastructure/events/PhabricatorExampleEventListener.php
@@ -1,31 +1,31 @@
<?php
/**
* Example event listener. For details about installing Phabricator event hooks,
* refer to @{article:Events User Guide: Installing Event Listeners}.
*/
final class PhabricatorExampleEventListener extends PhabricatorEventListener {
public function register() {
// When your listener is installed, its register() method will be called.
// You should listen() to any events you are interested in here.
$this->listen(PhabricatorEventType::TYPE_TEST_DIDRUNTEST);
}
public function handleEvent(PhutilEvent $event) {
// When an event you have called listen() for in your register() method
// occurs, this method will be invoked. You should respond to the event.
// In this case, we just echo a message out so the event test script will
// do something visible.
$console = PhutilConsole::getConsole();
$console->writeOut(
"%s\n",
pht(
- '% got test event at %d',
+ '%s got test event at %d',
__CLASS__,
$event->getValue('time')));
}
}
diff --git a/src/infrastructure/internationalization/translation/PhabricatorBritishEnglishTranslation.php b/src/infrastructure/internationalization/translation/PhabricatorBritishEnglishTranslation.php
index 3361b963b..530b3aac1 100644
--- a/src/infrastructure/internationalization/translation/PhabricatorBritishEnglishTranslation.php
+++ b/src/infrastructure/internationalization/translation/PhabricatorBritishEnglishTranslation.php
@@ -1,31 +1,31 @@
<?php
final class PhabricatorBritishEnglishTranslation
extends PhutilTranslation {
public function getLocaleCode() {
return 'en_GB';
}
protected function getTranslations() {
return array(
- '%s set this project\'s color to %s.' =>
- '%s set this project\'s colour to %s.',
+ "%s set this project's color to %s." =>
+ "%s set this project's colour to %s.",
'Basic Colors' =>
'Basic Colours',
'Choose Icon and Color...' =>
'Choose Icon and Colour...',
'Choose Background Color' =>
'Choose Background Colour',
'Color' => 'Colour',
'Colors' => 'Colours',
'Colors and Transforms' => 'Colours and Transforms',
'Configure the Phabricator UI, including colors.' =>
'Configure the Phabricator UI, including colours.',
'Flag Color' => 'Flag Colour',
'Sets the color of the main header.' =>
'Sets the colour of the main header.',
);
}
}
diff --git a/src/infrastructure/lint/linter/PhabricatorJavelinLinter.php b/src/infrastructure/lint/linter/PhabricatorJavelinLinter.php
index 2cca2030a..d235dc889 100644
--- a/src/infrastructure/lint/linter/PhabricatorJavelinLinter.php
+++ b/src/infrastructure/lint/linter/PhabricatorJavelinLinter.php
@@ -1,274 +1,290 @@
<?php
final class PhabricatorJavelinLinter extends ArcanistLinter {
private $symbols = array();
private $symbolsBinary;
private $haveWarnedAboutBinary;
const LINT_PRIVATE_ACCESS = 1;
const LINT_MISSING_DEPENDENCY = 2;
const LINT_UNNECESSARY_DEPENDENCY = 3;
const LINT_UNKNOWN_DEPENDENCY = 4;
const LINT_MISSING_BINARY = 5;
public function getInfoName() {
- return 'Javelin Linter';
+ return pht('Javelin Linter');
}
public function getInfoDescription() {
return pht(
'This linter is intended for use with the Javelin JS library and '.
'extensions. Use `javelinsymbols` to run Javelin rules on Javascript '.
'source files.');
}
private function getBinaryPath() {
if ($this->symbolsBinary === null) {
list($err, $stdout) = exec_manual('which javelinsymbols');
$this->symbolsBinary = ($err ? false : rtrim($stdout));
}
return $this->symbolsBinary;
}
public function willLintPaths(array $paths) {
if (!$this->getBinaryPath()) {
return;
}
$root = dirname(phutil_get_library_root('phabricator'));
require_once $root.'/scripts/__init_script__.php';
$futures = array();
foreach ($paths as $path) {
if ($this->shouldIgnorePath($path)) {
continue;
}
$future = $this->newSymbolsFuture($path);
$futures[$path] = $future;
}
foreach (id(new FutureIterator($futures))->limit(8) as $path => $future) {
$this->symbols[$path] = $future->resolvex();
}
}
public function getLinterName() {
return 'JAVELIN';
}
public function getLinterConfigurationName() {
return 'javelin';
}
public function getLintSeverityMap() {
return array(
self::LINT_MISSING_BINARY => ArcanistLintSeverity::SEVERITY_WARNING,
);
}
public function getLintNameMap() {
return array(
- self::LINT_PRIVATE_ACCESS => 'Private Method/Member Access',
- self::LINT_MISSING_DEPENDENCY => 'Missing Javelin Dependency',
- self::LINT_UNNECESSARY_DEPENDENCY => 'Unnecessary Javelin Dependency',
- self::LINT_UNKNOWN_DEPENDENCY => 'Unknown Javelin Dependency',
- self::LINT_MISSING_BINARY => '`javelinsymbols` Not In Path',
+ self::LINT_PRIVATE_ACCESS =>
+ pht('Private Method/Member Access'),
+ self::LINT_MISSING_DEPENDENCY =>
+ pht('Missing Javelin Dependency'),
+ self::LINT_UNNECESSARY_DEPENDENCY =>
+ pht('Unnecessary Javelin Dependency'),
+ self::LINT_UNKNOWN_DEPENDENCY =>
+ pht('Unknown Javelin Dependency'),
+ self::LINT_MISSING_BINARY =>
+ pht('`%s` Not In Path', 'javelinsymbols'),
);
}
public function getCacheGranularity() {
return ArcanistLinter::GRANULARITY_REPOSITORY;
}
public function getCacheVersion() {
$version = '0';
$binary_path = $this->getBinaryPath();
if ($binary_path) {
$version .= '-'.md5_file($binary_path);
}
return $version;
}
private function shouldIgnorePath($path) {
return preg_match('@/__tests__/|externals/javelin/docs/@', $path);
}
public function lintPath($path) {
if ($this->shouldIgnorePath($path)) {
return;
}
if (!$this->symbolsBinary) {
if (!$this->haveWarnedAboutBinary) {
$this->haveWarnedAboutBinary = true;
// TODO: Write build documentation for the Javelin binaries and point
// the user at it.
$this->raiseLintAtLine(
1,
0,
self::LINT_MISSING_BINARY,
- "The 'javelinsymbols' binary in the Javelin project is not ".
- "available in \$PATH, so the Javelin linter can't run. This ".
- "isn't a big concern, but means some Javelin problems can't be ".
- "automatically detected.");
+ pht(
+ "The '%s' binary in the Javelin project is not available in %s, ".
+ "so the Javelin linter can't run. This isn't a big concern, ".
+ "but means some Javelin problems can't be automatically detected.",
+ 'javelinsymbols',
+ '$PATH'));
}
return;
}
list($uses, $installs) = $this->getUsedAndInstalledSymbolsForPath($path);
foreach ($uses as $symbol => $line) {
$parts = explode('.', $symbol);
foreach ($parts as $part) {
if ($part[0] == '_' && $part[1] != '_') {
$base = implode('.', array_slice($parts, 0, 2));
if (!array_key_exists($base, $installs)) {
$this->raiseLintAtLine(
$line,
0,
self::LINT_PRIVATE_ACCESS,
- "This file accesses private symbol '{$symbol}' across file ".
- "boundaries. You may only access private members and methods ".
- "from the file where they are defined.");
+ pht(
+ "This file accesses private symbol '%s' across file ".
+ "boundaries. You may only access private members and methods ".
+ "from the file where they are defined.",
+ $symbol));
}
break;
}
}
}
$external_classes = array();
foreach ($uses as $symbol => $line) {
$parts = explode('.', $symbol);
$class = implode('.', array_slice($parts, 0, 2));
if (!array_key_exists($class, $external_classes) &&
!array_key_exists($class, $installs)) {
$external_classes[$class] = $line;
}
}
$celerity = CelerityResourceMap::getNamedInstance('phabricator');
$path = preg_replace(
'@^externals/javelinjs/src/@',
'webroot/rsrc/js/javelin/',
$path);
$need = $external_classes;
$resource_name = substr($path, strlen('webroot/'));
$requires = $celerity->getRequiredSymbolsForName($resource_name);
if (!$requires) {
$requires = array();
}
foreach ($requires as $key => $requires_symbol) {
$requires_name = $celerity->getResourceNameForSymbol($requires_symbol);
if ($requires_name === null) {
$this->raiseLintAtLine(
0,
0,
self::LINT_UNKNOWN_DEPENDENCY,
- "This file @requires component '{$requires_symbol}', but it does ".
- "not exist. You may need to rebuild the Celerity map.");
+ pht(
+ "This file %s component '%s', but it does not exist. ".
+ "You may need to rebuild the Celerity map.",
+ '@requires',
+ $requires_symbol));
unset($requires[$key]);
continue;
}
if (preg_match('/\\.css$/', $requires_name)) {
// If JS requires CSS, just assume everything is fine.
unset($requires[$key]);
} else {
$symbol_path = 'webroot/'.$requires_name;
list($ignored, $req_install) = $this->getUsedAndInstalledSymbolsForPath(
$symbol_path);
if (array_intersect_key($req_install, $external_classes)) {
$need = array_diff_key($need, $req_install);
unset($requires[$key]);
}
}
}
foreach ($need as $class => $line) {
$this->raiseLintAtLine(
$line,
0,
self::LINT_MISSING_DEPENDENCY,
- "This file uses '{$class}' but does not @requires the component ".
- "which installs it. You may need to rebuild the Celerity map.");
+ pht(
+ "This file uses '%s' but does not @requires the component ".
+ "which installs it. You may need to rebuild the Celerity map.",
+ $class));
}
foreach ($requires as $component) {
$this->raiseLintAtLine(
0,
0,
self::LINT_UNNECESSARY_DEPENDENCY,
- "This file @requires component '{$component}' but does not use ".
- "anything it provides.");
+ pht(
+ "This file %s component '%s' but does not use anything it provides.",
+ '@requires',
+ $component));
}
}
private function loadSymbols($path) {
if (empty($this->symbols[$path])) {
$this->symbols[$path] = $this->newSymbolsFuture($path)->resolvex();
}
return $this->symbols[$path];
}
private function newSymbolsFuture($path) {
$future = new ExecFuture('javelinsymbols # %s', $path);
$future->write($this->getData($path));
return $future;
}
private function getUsedAndInstalledSymbolsForPath($path) {
list($symbols) = $this->loadSymbols($path);
$symbols = trim($symbols);
$uses = array();
$installs = array();
if (empty($symbols)) {
// This file has no symbols.
return array($uses, $installs);
}
$symbols = explode("\n", trim($symbols));
foreach ($symbols as $line) {
$matches = null;
if (!preg_match('/^([?+\*])([^:]*):(\d+)$/', $line, $matches)) {
throw new Exception(
- 'Received malformed output from `javelinsymbols`.');
+ pht('Received malformed output from `%s`.', 'javelinsymbols'));
}
$type = $matches[1];
$symbol = $matches[2];
$line = $matches[3];
switch ($type) {
case '?':
$uses[$symbol] = $line;
break;
case '+':
$installs['JX.'.$symbol] = $line;
break;
}
}
$contents = $this->getData($path);
$matches = null;
$count = preg_match_all(
'/@javelin-installs\W+(\S+)/',
$contents,
$matches,
PREG_PATTERN_ORDER);
if ($count) {
foreach ($matches[1] as $symbol) {
$installs[$symbol] = 0;
}
}
return array($uses, $installs);
}
}
diff --git a/src/infrastructure/markup/PhabricatorMarkupEngine.php b/src/infrastructure/markup/PhabricatorMarkupEngine.php
index c29dddd82..7083a6973 100644
--- a/src/infrastructure/markup/PhabricatorMarkupEngine.php
+++ b/src/infrastructure/markup/PhabricatorMarkupEngine.php
@@ -1,634 +1,638 @@
<?php
/**
* Manages markup engine selection, configuration, application, caching and
* pipelining.
*
* @{class:PhabricatorMarkupEngine} can be used to render objects which
* implement @{interface:PhabricatorMarkupInterface} in a batched, cache-aware
* way. For example, if you have a list of comments written in remarkup (and
* the objects implement the correct interface) you can render them by first
* building an engine and adding the fields with @{method:addObject}.
*
* $field = 'field:body'; // Field you want to render. Each object exposes
* // one or more fields of markup.
*
* $engine = new PhabricatorMarkupEngine();
* foreach ($comments as $comment) {
* $engine->addObject($comment, $field);
* }
*
* Now, call @{method:process} to perform the actual cache/rendering
* step. This is a heavyweight call which does batched data access and
* transforms the markup into output.
*
* $engine->process();
*
* Finally, do something with the results:
*
* $results = array();
* foreach ($comments as $comment) {
* $results[] = $engine->getOutput($comment, $field);
* }
*
* If you have a single object to render, you can use the convenience method
* @{method:renderOneObject}.
*
* @task markup Markup Pipeline
* @task engine Engine Construction
*/
final class PhabricatorMarkupEngine {
private $objects = array();
private $viewer;
private $contextObject;
private $version = 15;
/* -( Markup Pipeline )---------------------------------------------------- */
/**
* Convenience method for pushing a single object through the markup
* pipeline.
*
* @param PhabricatorMarkupInterface The object to render.
* @param string The field to render.
* @param PhabricatorUser User viewing the markup.
* @param object A context object for policy checks
* @return string Marked up output.
* @task markup
*/
public static function renderOneObject(
PhabricatorMarkupInterface $object,
$field,
PhabricatorUser $viewer,
$context_object = null) {
return id(new PhabricatorMarkupEngine())
->setViewer($viewer)
->setContextObject($context_object)
->addObject($object, $field)
->process()
->getOutput($object, $field);
}
/**
* Queue an object for markup generation when @{method:process} is
* called. You can retrieve the output later with @{method:getOutput}.
*
* @param PhabricatorMarkupInterface The object to render.
* @param string The field to render.
* @return this
* @task markup
*/
public function addObject(PhabricatorMarkupInterface $object, $field) {
$key = $this->getMarkupFieldKey($object, $field);
$this->objects[$key] = array(
'object' => $object,
'field' => $field,
);
return $this;
}
/**
* Process objects queued with @{method:addObject}. You can then retrieve
* the output with @{method:getOutput}.
*
* @return this
* @task markup
*/
public function process() {
$keys = array();
foreach ($this->objects as $key => $info) {
if (!isset($info['markup'])) {
$keys[] = $key;
}
}
if (!$keys) {
return;
}
$objects = array_select_keys($this->objects, $keys);
// Build all the markup engines. We need an engine for each field whether
// we have a cache or not, since we still need to postprocess the cache.
$engines = array();
foreach ($objects as $key => $info) {
$engines[$key] = $info['object']->newMarkupEngine($info['field']);
$engines[$key]->setConfig('viewer', $this->viewer);
$engines[$key]->setConfig('contextObject', $this->contextObject);
}
// Load or build the preprocessor caches.
$blocks = $this->loadPreprocessorCaches($engines, $objects);
$blocks = mpull($blocks, 'getCacheData');
$this->engineCaches = $blocks;
// Finalize the output.
foreach ($objects as $key => $info) {
$engine = $engines[$key];
$field = $info['field'];
$object = $info['object'];
$output = $engine->postprocessText($blocks[$key]);
$output = $object->didMarkupText($field, $output, $engine);
$this->objects[$key]['output'] = $output;
}
return $this;
}
/**
* Get the output of markup processing for a field queued with
* @{method:addObject}. Before you can call this method, you must call
* @{method:process}.
*
* @param PhabricatorMarkupInterface The object to retrieve.
* @param string The field to retrieve.
* @return string Processed output.
* @task markup
*/
public function getOutput(PhabricatorMarkupInterface $object, $field) {
$key = $this->getMarkupFieldKey($object, $field);
$this->requireKeyProcessed($key);
return $this->objects[$key]['output'];
}
/**
* Retrieve engine metadata for a given field.
*
* @param PhabricatorMarkupInterface The object to retrieve.
* @param string The field to retrieve.
* @param string The engine metadata field to retrieve.
* @param wild Optional default value.
* @task markup
*/
public function getEngineMetadata(
PhabricatorMarkupInterface $object,
$field,
$metadata_key,
$default = null) {
$key = $this->getMarkupFieldKey($object, $field);
$this->requireKeyProcessed($key);
return idx($this->engineCaches[$key]['metadata'], $metadata_key, $default);
}
/**
* @task markup
*/
private function requireKeyProcessed($key) {
if (empty($this->objects[$key])) {
throw new Exception(
- "Call addObject() before using results (key = '{$key}').");
+ pht(
+ "Call %s before using results (key = '%s').",
+ 'addObject()',
+ $key));
}
if (!isset($this->objects[$key]['output'])) {
throw new Exception(
- 'Call process() before using results.');
+ pht(
+ 'Call %s before using results.',
+ 'process()'));
}
}
/**
* @task markup
*/
private function getMarkupFieldKey(
PhabricatorMarkupInterface $object,
$field) {
static $custom;
if ($custom === null) {
$custom = array_merge(
self::loadCustomInlineRules(),
self::loadCustomBlockRules());
$custom = mpull($custom, 'getRuleVersion', null);
ksort($custom);
$custom = PhabricatorHash::digestForIndex(serialize($custom));
}
return $object->getMarkupFieldKey($field).'@'.$this->version.'@'.$custom;
}
/**
* @task markup
*/
private function loadPreprocessorCaches(array $engines, array $objects) {
$blocks = array();
$use_cache = array();
foreach ($objects as $key => $info) {
if ($info['object']->shouldUseMarkupCache($info['field'])) {
$use_cache[$key] = true;
}
}
if ($use_cache) {
try {
$blocks = id(new PhabricatorMarkupCache())->loadAllWhere(
'cacheKey IN (%Ls)',
array_keys($use_cache));
$blocks = mpull($blocks, null, 'getCacheKey');
} catch (Exception $ex) {
phlog($ex);
}
}
foreach ($objects as $key => $info) {
// False check in case MySQL doesn't support unicode characters
// in the string (T1191), resulting in unserialize returning false.
if (isset($blocks[$key]) && $blocks[$key]->getCacheData() !== false) {
// If we already have a preprocessing cache, we don't need to rebuild
// it.
continue;
}
$text = $info['object']->getMarkupText($info['field']);
$data = $engines[$key]->preprocessText($text);
// NOTE: This is just debugging information to help sort out cache issues.
// If one machine is misconfigured and poisoning caches you can use this
// field to hunt it down.
$metadata = array(
'host' => php_uname('n'),
);
$blocks[$key] = id(new PhabricatorMarkupCache())
->setCacheKey($key)
->setCacheData($data)
->setMetadata($metadata);
if (isset($use_cache[$key])) {
// This is just filling a cache and always safe, even on a read pathway.
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$blocks[$key]->replace();
unset($unguarded);
}
}
return $blocks;
}
/**
* Set the viewing user. Used to implement object permissions.
*
* @param PhabricatorUser The viewing user.
* @return this
* @task markup
*/
public function setViewer(PhabricatorUser $viewer) {
$this->viewer = $viewer;
return $this;
}
/**
* Set the context object. Used to implement object permissions.
*
* @param The object in which context this remarkup is used.
* @return this
* @task markup
*/
public function setContextObject($object) {
$this->contextObject = $object;
return $this;
}
/* -( Engine Construction )------------------------------------------------ */
/**
* @task engine
*/
public static function newManiphestMarkupEngine() {
return self::newMarkupEngine(array(
));
}
/**
* @task engine
*/
public static function newPhrictionMarkupEngine() {
return self::newMarkupEngine(array(
'header.generate-toc' => true,
));
}
/**
* @task engine
*/
public static function newPhameMarkupEngine() {
return self::newMarkupEngine(array(
'macros' => false,
'uri.full' => true,
));
}
/**
* @task engine
*/
public static function newFeedMarkupEngine() {
return self::newMarkupEngine(
array(
'macros' => false,
'youtube' => false,
));
}
/**
* @task engine
*/
public static function newCalendarMarkupEngine() {
return self::newMarkupEngine(array(
));
}
/**
* @task engine
*/
public static function newDifferentialMarkupEngine(array $options = array()) {
return self::newMarkupEngine(array(
'differential.diff' => idx($options, 'differential.diff'),
));
}
/**
* @task engine
*/
public static function newDiffusionMarkupEngine(array $options = array()) {
return self::newMarkupEngine(array(
'header.generate-toc' => true,
));
}
/**
* @task engine
*/
public static function getEngine($ruleset = 'default') {
static $engines = array();
if (isset($engines[$ruleset])) {
return $engines[$ruleset];
}
$engine = null;
switch ($ruleset) {
case 'default':
$engine = self::newMarkupEngine(array());
break;
case 'nolinebreaks':
$engine = self::newMarkupEngine(array());
$engine->setConfig('preserve-linebreaks', false);
break;
case 'diffusion-readme':
$engine = self::newMarkupEngine(array());
$engine->setConfig('preserve-linebreaks', false);
$engine->setConfig('header.generate-toc', true);
break;
case 'diviner':
$engine = self::newMarkupEngine(array());
$engine->setConfig('preserve-linebreaks', false);
// $engine->setConfig('diviner.renderer', new DivinerDefaultRenderer());
$engine->setConfig('header.generate-toc', true);
break;
case 'extract':
// Engine used for reference/edge extraction. Turn off anything which
// is slow and doesn't change reference extraction.
$engine = self::newMarkupEngine(array());
$engine->setConfig('pygments.enabled', false);
break;
default:
- throw new Exception("Unknown engine ruleset: {$ruleset}!");
+ throw new Exception(pht('Unknown engine ruleset: %s!', $ruleset));
}
$engines[$ruleset] = $engine;
return $engine;
}
/**
* @task engine
*/
private static function getMarkupEngineDefaultConfiguration() {
return array(
'pygments' => PhabricatorEnv::getEnvConfig('pygments.enabled'),
'youtube' => PhabricatorEnv::getEnvConfig(
'remarkup.enable-embedded-youtube'),
'differential.diff' => null,
'header.generate-toc' => false,
'macros' => true,
'uri.allowed-protocols' => PhabricatorEnv::getEnvConfig(
'uri.allowed-protocols'),
'uri.full' => false,
'syntax-highlighter.engine' => PhabricatorEnv::getEnvConfig(
'syntax-highlighter.engine'),
'preserve-linebreaks' => true,
);
}
/**
* @task engine
*/
public static function newMarkupEngine(array $options) {
-
$options += self::getMarkupEngineDefaultConfiguration();
$engine = new PhutilRemarkupEngine();
$engine->setConfig('preserve-linebreaks', $options['preserve-linebreaks']);
$engine->setConfig('pygments.enabled', $options['pygments']);
$engine->setConfig(
'uri.allowed-protocols',
$options['uri.allowed-protocols']);
$engine->setConfig('differential.diff', $options['differential.diff']);
$engine->setConfig('header.generate-toc', $options['header.generate-toc']);
$engine->setConfig(
'syntax-highlighter.engine',
$options['syntax-highlighter.engine']);
$engine->setConfig('uri.full', $options['uri.full']);
$rules = array();
$rules[] = new PhutilRemarkupEscapeRemarkupRule();
$rules[] = new PhutilRemarkupMonospaceRule();
$rules[] = new PhutilRemarkupDocumentLinkRule();
$rules[] = new PhabricatorNavigationRemarkupRule();
if ($options['youtube']) {
$rules[] = new PhabricatorYoutubeRemarkupRule();
}
$applications = PhabricatorApplication::getAllInstalledApplications();
foreach ($applications as $application) {
foreach ($application->getRemarkupRules() as $rule) {
$rules[] = $rule;
}
}
$rules[] = new PhutilRemarkupHyperlinkRule();
if ($options['macros']) {
$rules[] = new PhabricatorImageMacroRemarkupRule();
$rules[] = new PhabricatorMemeRemarkupRule();
}
$rules[] = new PhutilRemarkupBoldRule();
$rules[] = new PhutilRemarkupItalicRule();
$rules[] = new PhutilRemarkupDelRule();
$rules[] = new PhutilRemarkupUnderlineRule();
foreach (self::loadCustomInlineRules() as $rule) {
$rules[] = $rule;
}
$blocks = array();
$blocks[] = new PhutilRemarkupQuotesBlockRule();
$blocks[] = new PhutilRemarkupReplyBlockRule();
$blocks[] = new PhutilRemarkupLiteralBlockRule();
$blocks[] = new PhutilRemarkupHeaderBlockRule();
$blocks[] = new PhutilRemarkupHorizontalRuleBlockRule();
$blocks[] = new PhutilRemarkupListBlockRule();
$blocks[] = new PhutilRemarkupCodeBlockRule();
$blocks[] = new PhutilRemarkupNoteBlockRule();
$blocks[] = new PhutilRemarkupTableBlockRule();
$blocks[] = new PhutilRemarkupSimpleTableBlockRule();
$blocks[] = new PhutilRemarkupInterpreterBlockRule();
$blocks[] = new PhutilRemarkupDefaultBlockRule();
foreach (self::loadCustomBlockRules() as $rule) {
$blocks[] = $rule;
}
foreach ($blocks as $block) {
$block->setMarkupRules($rules);
}
$engine->setBlockRules($blocks);
return $engine;
}
public static function extractPHIDsFromMentions(
PhabricatorUser $viewer,
array $content_blocks) {
$mentions = array();
$engine = self::newDifferentialMarkupEngine();
$engine->setConfig('viewer', $viewer);
foreach ($content_blocks as $content_block) {
$engine->markupText($content_block);
$phids = $engine->getTextMetadata(
PhabricatorMentionRemarkupRule::KEY_MENTIONED,
array());
$mentions += $phids;
}
return $mentions;
}
public static function extractFilePHIDsFromEmbeddedFiles(
PhabricatorUser $viewer,
array $content_blocks) {
$files = array();
$engine = self::newDifferentialMarkupEngine();
$engine->setConfig('viewer', $viewer);
foreach ($content_blocks as $content_block) {
$engine->markupText($content_block);
$phids = $engine->getTextMetadata(
PhabricatorEmbedFileRemarkupRule::KEY_EMBED_FILE_PHIDS,
array());
foreach ($phids as $phid) {
$files[$phid] = $phid;
}
}
return array_values($files);
}
/**
* Produce a corpus summary, in a way that shortens the underlying text
* without truncating it somewhere awkward.
*
* TODO: We could do a better job of this.
*
* @param string Remarkup corpus to summarize.
* @return string Summarized corpus.
*/
public static function summarize($corpus) {
// Major goals here are:
// - Don't split in the middle of a character (utf-8).
// - Don't split in the middle of, e.g., **bold** text, since
// we end up with hanging '**' in the summary.
// - Try not to pick an image macro, header, embedded file, etc.
// - Hopefully don't return too much text. We don't explicitly limit
// this right now.
$blocks = preg_split("/\n *\n\s*/", $corpus);
$best = null;
foreach ($blocks as $block) {
// This is a test for normal spaces in the block, i.e. a heuristic to
// distinguish standard paragraphs from things like image macros. It may
// not work well for non-latin text. We prefer to summarize with a
// paragraph of normal words over an image macro, if possible.
$has_space = preg_match('/\w\s\w/', $block);
// This is a test to find embedded images and headers. We prefer to
// summarize with a normal paragraph over a header or an embedded object,
// if possible.
$has_embed = preg_match('/^[{=]/', $block);
if ($has_space && !$has_embed) {
// This seems like a good summary, so return it.
return $block;
}
if (!$best) {
// This is the first block we found; if everything is garbage just
// use the first block.
$best = $block;
}
}
return $best;
}
private static function loadCustomInlineRules() {
return id(new PhutilSymbolLoader())
->setAncestorClass('PhabricatorRemarkupCustomInlineRule')
->loadObjects();
}
private static function loadCustomBlockRules() {
return id(new PhutilSymbolLoader())
->setAncestorClass('PhabricatorRemarkupCustomBlockRule')
->loadObjects();
}
}
diff --git a/src/infrastructure/markup/interpreter/PhabricatorRemarkupCowsayBlockInterpreter.php b/src/infrastructure/markup/interpreter/PhabricatorRemarkupCowsayBlockInterpreter.php
index 8f3cd3552..d1c4de115 100644
--- a/src/infrastructure/markup/interpreter/PhabricatorRemarkupCowsayBlockInterpreter.php
+++ b/src/infrastructure/markup/interpreter/PhabricatorRemarkupCowsayBlockInterpreter.php
@@ -1,58 +1,60 @@
<?php
final class PhabricatorRemarkupCowsayBlockInterpreter
extends PhutilRemarkupBlockInterpreter {
public function getInterpreterName() {
return 'cowsay';
}
public function markupContent($content, array $argv) {
if (!Filesystem::binaryExists('cowsay')) {
return $this->markupError(
- pht('Unable to locate the `cowsay` binary. Install cowsay.'));
+ pht(
+ 'Unable to locate the `%s` binary. Install cowsay.',
+ 'cowsay'));
}
$bin = idx($argv, 'think') ? 'cowthink' : 'cowsay';
$eyes = idx($argv, 'eyes', 'oo');
$tongue = idx($argv, 'tongue', ' ');
$cow = idx($argv, 'cow', 'default');
// NOTE: Strip this aggressively to prevent nonsense like
// `cow=/etc/passwd`. We could build a whiltelist with `cowsay -l`.
$cow = preg_replace('/[^a-z.-]+/', '', $cow);
$future = new ExecFuture(
'%s -e %s -T %s -f %s ',
$bin,
$eyes,
$tongue,
$cow);
$future->setTimeout(15);
$future->write($content);
list($err, $stdout, $stderr) = $future->resolve();
if ($err) {
return $this->markupError(
pht(
'Execution of `%s` failed: %s',
'cowsay',
$stderr));
}
if ($this->getEngine()->isTextMode()) {
return $stdout;
}
return phutil_tag(
'div',
array(
'class' => 'PhabricatorMonospaced remarkup-cowsay',
),
$stdout);
}
}
diff --git a/src/infrastructure/query/order/PhabricatorQueryOrderItem.php b/src/infrastructure/query/order/PhabricatorQueryOrderItem.php
index a37cb92e0..474b495e4 100644
--- a/src/infrastructure/query/order/PhabricatorQueryOrderItem.php
+++ b/src/infrastructure/query/order/PhabricatorQueryOrderItem.php
@@ -1,62 +1,61 @@
<?php
/**
* Structural class representing one item in an order vector.
*
* See @{class:PhabricatorQueryOrderVector} for discussion of order vectors.
* This represents one item in an order vector, like "id". When combined with
* the other items in the vector, a complete ordering (like "name, id") is
* described.
*
* Construct an item using @{method:newFromScalar}:
*
* $item = PhabricatorQueryOrderItem::newFromScalar('id');
*
* This class is primarily internal to the query infrastructure, and most
* application code should not need to interact with it directly.
*/
-final class PhabricatorQueryOrderItem
- extends Phobject {
+final class PhabricatorQueryOrderItem extends Phobject {
private $orderKey;
private $isReversed;
private function __construct() {
// <private>
}
public static function newFromScalar($scalar) {
// If the string is something like "-id", strip the "-" off and mark it
// as reversed.
$is_reversed = false;
if (!strncmp($scalar, '-', 1)) {
$is_reversed = true;
$scalar = substr($scalar, 1);
}
$item = new PhabricatorQueryOrderItem();
$item->orderKey = $scalar;
$item->isReversed = $is_reversed;
return $item;
}
public function getIsReversed() {
return $this->isReversed;
}
public function getOrderKey() {
return $this->orderKey;
}
public function getAsScalar() {
if ($this->getIsReversed()) {
$prefix = '-';
} else {
$prefix = '';
}
return $prefix.$this->getOrderKey();
}
}
diff --git a/src/infrastructure/query/policy/PhabricatorPolicyAwareQuery.php b/src/infrastructure/query/policy/PhabricatorPolicyAwareQuery.php
index e1b2028a1..1d8f10b91 100644
--- a/src/infrastructure/query/policy/PhabricatorPolicyAwareQuery.php
+++ b/src/infrastructure/query/policy/PhabricatorPolicyAwareQuery.php
@@ -1,683 +1,683 @@
<?php
/**
* A @{class:PhabricatorQuery} which filters results according to visibility
* policies for the querying user. Broadly, this class allows you to implement
* a query that returns only objects the user is allowed to see.
*
* $results = id(new ExampleQuery())
* ->setViewer($user)
* ->withConstraint($example)
* ->execute();
*
* Normally, you should extend @{class:PhabricatorCursorPagedPolicyAwareQuery},
* not this class. @{class:PhabricatorCursorPagedPolicyAwareQuery} provides a
* more practical interface for building usable queries against most object
* types.
*
* NOTE: Although this class extends @{class:PhabricatorOffsetPagedQuery},
* offset paging with policy filtering is not efficient. All results must be
* loaded into the application and filtered here: skipping `N` rows via offset
* is an `O(N)` operation with a large constant. Prefer cursor-based paging
* with @{class:PhabricatorCursorPagedPolicyAwareQuery}, which can filter far
* more efficiently in MySQL.
*
* @task config Query Configuration
* @task exec Executing Queries
* @task policyimpl Policy Query Implementation
*/
abstract class PhabricatorPolicyAwareQuery extends PhabricatorOffsetPagedQuery {
private $viewer;
private $parentQuery;
private $rawResultLimit;
private $capabilities;
private $workspace = array();
private $inFlightPHIDs = array();
private $policyFilteredPHIDs = array();
private $canUseApplication;
/**
* Should we continue or throw an exception when a query result is filtered
* by policy rules?
*
* Values are `true` (raise exceptions), `false` (do not raise exceptions)
* and `null` (inherit from parent query, with no exceptions by default).
*/
private $raisePolicyExceptions;
/* -( Query Configuration )------------------------------------------------ */
/**
* Set the viewer who is executing the query. Results will be filtered
* according to the viewer's capabilities. You must set a viewer to execute
* a policy query.
*
* @param PhabricatorUser The viewing user.
* @return this
* @task config
*/
final public function setViewer(PhabricatorUser $viewer) {
$this->viewer = $viewer;
return $this;
}
/**
* Get the query's viewer.
*
* @return PhabricatorUser The viewing user.
* @task config
*/
final public function getViewer() {
return $this->viewer;
}
/**
* Set the parent query of this query. This is useful for nested queries so
* that configuration like whether or not to raise policy exceptions is
* seamlessly passed along to child queries.
*
* @return this
* @task config
*/
final public function setParentQuery(PhabricatorPolicyAwareQuery $query) {
$this->parentQuery = $query;
return $this;
}
/**
* Get the parent query. See @{method:setParentQuery} for discussion.
*
* @return PhabricatorPolicyAwareQuery The parent query.
* @task config
*/
final public function getParentQuery() {
return $this->parentQuery;
}
/**
* Hook to configure whether this query should raise policy exceptions.
*
* @return this
* @task config
*/
final public function setRaisePolicyExceptions($bool) {
$this->raisePolicyExceptions = $bool;
return $this;
}
/**
* @return bool
* @task config
*/
final public function shouldRaisePolicyExceptions() {
return (bool)$this->raisePolicyExceptions;
}
/**
* @task config
*/
final public function requireCapabilities(array $capabilities) {
$this->capabilities = $capabilities;
return $this;
}
/* -( Query Execution )---------------------------------------------------- */
/**
* Execute the query, expecting a single result. This method simplifies
* loading objects for detail pages or edit views.
*
* // Load one result by ID.
* $obj = id(new ExampleQuery())
* ->setViewer($user)
* ->withIDs(array($id))
* ->executeOne();
* if (!$obj) {
* return new Aphront404Response();
* }
*
* If zero results match the query, this method returns `null`.
* If one result matches the query, this method returns that result.
*
* If two or more results match the query, this method throws an exception.
* You should use this method only when the query constraints guarantee at
* most one match (e.g., selecting a specific ID or PHID).
*
* If one result matches the query but it is caught by the policy filter (for
* example, the user is trying to view or edit an object which exists but
* which they do not have permission to see) a policy exception is thrown.
*
* @return mixed Single result, or null.
* @task exec
*/
final public function executeOne() {
$this->setRaisePolicyExceptions(true);
try {
$results = $this->execute();
} catch (Exception $ex) {
$this->setRaisePolicyExceptions(false);
throw $ex;
}
if (count($results) > 1) {
- throw new Exception('Expected a single result!');
+ throw new Exception(pht('Expected a single result!'));
}
if (!$results) {
return null;
}
return head($results);
}
/**
* Execute the query, loading all visible results.
*
* @return list<PhabricatorPolicyInterface> Result objects.
* @task exec
*/
final public function execute() {
if (!$this->viewer) {
- throw new Exception('Call setViewer() before execute()!');
+ throw new PhutilInvalidStateException('setViewer');
}
$parent_query = $this->getParentQuery();
if ($parent_query && ($this->raisePolicyExceptions === null)) {
$this->setRaisePolicyExceptions(
$parent_query->shouldRaisePolicyExceptions());
}
$results = array();
$filter = $this->getPolicyFilter();
$offset = (int)$this->getOffset();
$limit = (int)$this->getLimit();
$count = 0;
if ($limit) {
$need = $offset + $limit;
} else {
$need = 0;
}
$this->willExecute();
do {
if ($need) {
$this->rawResultLimit = min($need - $count, 1024);
} else {
$this->rawResultLimit = 0;
}
if ($this->canViewerUseQueryApplication()) {
try {
$page = $this->loadPage();
} catch (PhabricatorEmptyQueryException $ex) {
$page = array();
}
} else {
$page = array();
}
if ($page) {
$maybe_visible = $this->willFilterPage($page);
} else {
$maybe_visible = array();
}
if ($this->shouldDisablePolicyFiltering()) {
$visible = $maybe_visible;
} else {
$visible = $filter->apply($maybe_visible);
$policy_filtered = array();
foreach ($maybe_visible as $key => $object) {
if (empty($visible[$key])) {
$phid = $object->getPHID();
if ($phid) {
$policy_filtered[$phid] = $phid;
}
}
}
$this->addPolicyFilteredPHIDs($policy_filtered);
}
if ($visible) {
$this->putObjectsInWorkspace($this->getWorkspaceMapForPage($visible));
$visible = $this->didFilterPage($visible);
}
$removed = array();
foreach ($maybe_visible as $key => $object) {
if (empty($visible[$key])) {
$removed[$key] = $object;
}
}
$this->didFilterResults($removed);
foreach ($visible as $key => $result) {
++$count;
// If we have an offset, we just ignore that many results and start
// storing them only once we've hit the offset. This reduces memory
// requirements for large offsets, compared to storing them all and
// slicing them away later.
if ($count > $offset) {
$results[$key] = $result;
}
if ($need && ($count >= $need)) {
// If we have all the rows we need, break out of the paging query.
break 2;
}
}
if (!$this->rawResultLimit) {
// If we don't have a load count, we loaded all the results. We do
// not need to load another page.
break;
}
if (count($page) < $this->rawResultLimit) {
// If we have a load count but the unfiltered results contained fewer
// objects, we know this was the last page of objects; we do not need
// to load another page because we can deduce it would be empty.
break;
}
$this->nextPage($page);
} while (true);
$results = $this->didLoadResults($results);
return $results;
}
private function getPolicyFilter() {
$filter = new PhabricatorPolicyFilter();
$filter->setViewer($this->viewer);
$capabilities = $this->getRequiredCapabilities();
$filter->requireCapabilities($capabilities);
$filter->raisePolicyExceptions($this->shouldRaisePolicyExceptions());
return $filter;
}
protected function getRequiredCapabilities() {
if ($this->capabilities) {
return $this->capabilities;
}
return array(
PhabricatorPolicyCapability::CAN_VIEW,
);
}
protected function applyPolicyFilter(array $objects, array $capabilities) {
if ($this->shouldDisablePolicyFiltering()) {
return $objects;
}
$filter = $this->getPolicyFilter();
$filter->requireCapabilities($capabilities);
return $filter->apply($objects);
}
protected function didRejectResult(PhabricatorPolicyInterface $object) {
$this->getPolicyFilter()->rejectObject(
$object,
$object->getPolicy(PhabricatorPolicyCapability::CAN_VIEW),
PhabricatorPolicyCapability::CAN_VIEW);
}
public function addPolicyFilteredPHIDs(array $phids) {
$this->policyFilteredPHIDs += $phids;
if ($this->getParentQuery()) {
$this->getParentQuery()->addPolicyFilteredPHIDs($phids);
}
return $this;
}
/**
* Return a map of all object PHIDs which were loaded in the query but
* filtered out by policy constraints. This allows a caller to distinguish
* between objects which do not exist (or, at least, were filtered at the
* content level) and objects which exist but aren't visible.
*
* @return map<phid, phid> Map of object PHIDs which were filtered
* by policies.
* @task exec
*/
public function getPolicyFilteredPHIDs() {
return $this->policyFilteredPHIDs;
}
/* -( Query Workspace )---------------------------------------------------- */
/**
* Put a map of objects into the query workspace. Many queries perform
* subqueries, which can eventually end up loading the same objects more than
* once (often to perform policy checks).
*
* For example, loading a user may load the user's profile image, which might
* load the user object again in order to verify that the viewer has
* permission to see the file.
*
* The "query workspace" allows queries to load objects from elsewhere in a
* query block instead of refetching them.
*
* When using the query workspace, it's important to obey two rules:
*
* **Never put objects into the workspace which the viewer may not be able
* to see**. You need to apply all policy filtering //before// putting
* objects in the workspace. Otherwise, subqueries may read the objects and
* use them to permit access to content the user shouldn't be able to view.
*
* **Fully enrich objects pulled from the workspace.** After pulling objects
* from the workspace, you still need to load and attach any additional
* content the query requests. Otherwise, a query might return objects without
* requested content.
*
* Generally, you do not need to update the workspace yourself: it is
* automatically populated as a side effect of objects surviving policy
* filtering.
*
* @param map<phid, PhabricatorPolicyInterface> Objects to add to the query
* workspace.
* @return this
* @task workspace
*/
public function putObjectsInWorkspace(array $objects) {
assert_instances_of($objects, 'PhabricatorPolicyInterface');
$viewer_phid = $this->getViewer()->getPHID();
// The workspace is scoped per viewer to prevent accidental contamination.
if (empty($this->workspace[$viewer_phid])) {
$this->workspace[$viewer_phid] = array();
}
$this->workspace[$viewer_phid] += $objects;
return $this;
}
/**
* Retrieve objects from the query workspace. For more discussion about the
* workspace mechanism, see @{method:putObjectsInWorkspace}. This method
* searches both the current query's workspace and the workspaces of parent
* queries.
*
* @param list<phid> List of PHIDs to retrieve.
* @return this
* @task workspace
*/
public function getObjectsFromWorkspace(array $phids) {
$viewer_phid = $this->getViewer()->getPHID();
$results = array();
foreach ($phids as $key => $phid) {
if (isset($this->workspace[$viewer_phid][$phid])) {
$results[$phid] = $this->workspace[$viewer_phid][$phid];
unset($phids[$key]);
}
}
if ($phids && $this->getParentQuery()) {
$results += $this->getParentQuery()->getObjectsFromWorkspace($phids);
}
return $results;
}
/**
* Convert a result page to a `<phid, PhabricatorPolicyInterface>` map.
*
* @param list<PhabricatorPolicyInterface> Objects.
* @return map<phid, PhabricatorPolicyInterface> Map of objects which can
* be put into the workspace.
* @task workspace
*/
protected function getWorkspaceMapForPage(array $results) {
$map = array();
foreach ($results as $result) {
$phid = $result->getPHID();
if ($phid !== null) {
$map[$phid] = $result;
}
}
return $map;
}
/**
* Mark PHIDs as in flight.
*
* PHIDs which are "in flight" are actively being queried for. Using this
* list can prevent infinite query loops by aborting queries which cycle.
*
* @param list<phid> List of PHIDs which are now in flight.
* @return this
*/
public function putPHIDsInFlight(array $phids) {
foreach ($phids as $phid) {
$this->inFlightPHIDs[$phid] = $phid;
}
return $this;
}
/**
* Get PHIDs which are currently in flight.
*
* PHIDs which are "in flight" are actively being queried for.
*
* @return map<phid, phid> PHIDs currently in flight.
*/
public function getPHIDsInFlight() {
$results = $this->inFlightPHIDs;
if ($this->getParentQuery()) {
$results += $this->getParentQuery()->getPHIDsInFlight();
}
return $results;
}
/* -( Policy Query Implementation )---------------------------------------- */
/**
* Get the number of results @{method:loadPage} should load. If the value is
* 0, @{method:loadPage} should load all available results.
*
* @return int The number of results to load, or 0 for all results.
* @task policyimpl
*/
final protected function getRawResultLimit() {
return $this->rawResultLimit;
}
/**
* Hook invoked before query execution. Generally, implementations should
* reset any internal cursors.
*
* @return void
* @task policyimpl
*/
protected function willExecute() {
return;
}
/**
* Load a raw page of results. Generally, implementations should load objects
* from the database. They should attempt to return the number of results
* hinted by @{method:getRawResultLimit}.
*
* @return list<PhabricatorPolicyInterface> List of filterable policy objects.
* @task policyimpl
*/
abstract protected function loadPage();
/**
* Update internal state so that the next call to @{method:loadPage} will
* return new results. Generally, you should adjust a cursor position based
* on the provided result page.
*
* @param list<PhabricatorPolicyInterface> The current page of results.
* @return void
* @task policyimpl
*/
abstract protected function nextPage(array $page);
/**
* Hook for applying a page filter prior to the privacy filter. This allows
* you to drop some items from the result set without creating problems with
* pagination or cursor updates. You can also load and attach data which is
* required to perform policy filtering.
*
* Generally, you should load non-policy data and perform non-policy filtering
* later, in @{method:didFilterPage}. Strictly fewer objects will make it that
* far (so the program will load less data) and subqueries from that context
* can use the query workspace to further reduce query load.
*
* This method will only be called if data is available. Implementations
* do not need to handle the case of no results specially.
*
* @param list<wild> Results from `loadPage()`.
* @return list<PhabricatorPolicyInterface> Objects for policy filtering.
* @task policyimpl
*/
protected function willFilterPage(array $page) {
return $page;
}
/**
* Hook for performing additional non-policy loading or filtering after an
* object has satisfied all policy checks. Generally, this means loading and
* attaching related data.
*
* Subqueries executed during this phase can use the query workspace, which
* may improve performance or make circular policies resolvable. Data which
* is not necessary for policy filtering should generally be loaded here.
*
* This callback can still filter objects (for example, if attachable data
* is discovered to not exist), but should not do so for policy reasons.
*
* This method will only be called if data is available. Implementations do
* not need to handle the case of no results specially.
*
* @param list<wild> Results from @{method:willFilterPage()}.
* @return list<PhabricatorPolicyInterface> Objects after additional
* non-policy processing.
*/
protected function didFilterPage(array $page) {
return $page;
}
/**
* Hook for removing filtered results from alternate result sets. This
* hook will be called with any objects which were returned by the query but
* filtered for policy reasons. The query should remove them from any cached
* or partial result sets.
*
* @param list<wild> List of objects that should not be returned by alternate
* result mechanisms.
* @return void
* @task policyimpl
*/
protected function didFilterResults(array $results) {
return;
}
/**
* Hook for applying final adjustments before results are returned. This is
* used by @{class:PhabricatorCursorPagedPolicyAwareQuery} to reverse results
* that are queried during reverse paging.
*
* @param list<PhabricatorPolicyInterface> Query results.
* @return list<PhabricatorPolicyInterface> Final results.
* @task policyimpl
*/
protected function didLoadResults(array $results) {
return $results;
}
/**
* Allows a subclass to disable policy filtering. This method is dangerous.
* It should be used only if the query loads data which has already been
* filtered (for example, because it wraps some other query which uses
* normal policy filtering).
*
* @return bool True to disable all policy filtering.
* @task policyimpl
*/
protected function shouldDisablePolicyFiltering() {
return false;
}
/**
* If this query belongs to an application, return the application class name
* here. This will prevent the query from returning results if the viewer can
* not access the application.
*
* If this query does not belong to an application, return `null`.
*
* @return string|null Application class name.
*/
abstract public function getQueryApplicationClass();
/**
* Determine if the viewer has permission to use this query's application.
* For queries which aren't part of an application, this method always returns
* true.
*
* @return bool True if the viewer has application-level permission to
* execute the query.
*/
public function canViewerUseQueryApplication() {
if ($this->canUseApplication === null) {
$class = $this->getQueryApplicationClass();
if (!$class) {
$this->canUseApplication = true;
} else {
$result = id(new PhabricatorApplicationQuery())
->setViewer($this->getViewer())
->withClasses(array($class))
->execute();
$this->canUseApplication = (bool)$result;
}
}
return $this->canUseApplication;
}
}
diff --git a/src/infrastructure/sms/management/PhabricatorSMSManagementListOutboundWorkflow.php b/src/infrastructure/sms/management/PhabricatorSMSManagementListOutboundWorkflow.php
index 1597ba928..576620166 100644
--- a/src/infrastructure/sms/management/PhabricatorSMSManagementListOutboundWorkflow.php
+++ b/src/infrastructure/sms/management/PhabricatorSMSManagementListOutboundWorkflow.php
@@ -1,55 +1,54 @@
<?php
final class PhabricatorSMSManagementListOutboundWorkflow
extends PhabricatorSMSManagementWorkflow {
protected function didConstruct() {
$this
->setName('list-outbound')
- ->setSynopsis('List outbound sms messages sent by Phabricator.')
- ->setExamples(
- '**list-outbound**')
+ ->setSynopsis(pht('List outbound SMS messages sent by Phabricator.'))
+ ->setExamples('**list-outbound**')
->setArguments(
array(
array(
'name' => 'limit',
'param' => 'N',
'default' => 100,
- 'help' =>
- 'Show a specific number of sms messages (default 100).',
+ 'help' => pht(
+ 'Show a specific number of SMS messages (default 100).'),
),
));
}
public function execute(PhutilArgumentParser $args) {
$console = PhutilConsole::getConsole();
$viewer = $this->getViewer();
$sms_messages = id(new PhabricatorSMS())->loadAllWhere(
'1 = 1 ORDER BY id DESC LIMIT %d',
$args->getArg('limit'));
if (!$sms_messages) {
- $console->writeErr("%s\n", pht('No sent sms.'));
+ $console->writeErr("%s\n", pht('No sent SMS.'));
return 0;
}
$table = id(new PhutilConsoleTable())
->setShowHeader(false)
- ->addColumn('id', array('title' => 'ID'))
- ->addColumn('status', array('title' => 'Status'))
- ->addColumn('recv', array('title' => 'Recipient'));
+ ->addColumn('id', array('title' => pht('ID')))
+ ->addColumn('status', array('title' => pht('Status')))
+ ->addColumn('recv', array('title' => pht('Recipient')));
foreach (array_reverse($sms_messages) as $sms) {
$table->addRow(array(
'id' => $sms->getID(),
'status' => $sms->getSendStatus(),
'recv' => $sms->getToNumber(),
));
}
$table->draw();
return 0;
}
}
diff --git a/src/infrastructure/sms/management/PhabricatorSMSManagementSendTestWorkflow.php b/src/infrastructure/sms/management/PhabricatorSMSManagementSendTestWorkflow.php
index c49f9443a..7bb457309 100644
--- a/src/infrastructure/sms/management/PhabricatorSMSManagementSendTestWorkflow.php
+++ b/src/infrastructure/sms/management/PhabricatorSMSManagementSendTestWorkflow.php
@@ -1,48 +1,47 @@
<?php
final class PhabricatorSMSManagementSendTestWorkflow
extends PhabricatorSMSManagementWorkflow {
protected function didConstruct() {
$this
->setName('send-test')
->setSynopsis(
pht(
- 'Simulate sending an sms. This may be useful to test your sms '.
- 'configuration, or while developing new sms adapters.'))
- ->setExamples(
- "**send-test** --to 12345678 --body 'pizza time yet?'")
+ 'Simulate sending an SMS. This may be useful to test your SMS '.
+ 'configuration, or while developing new SMS adapters.'))
+ ->setExamples("**send-test** --to 12345678 --body 'pizza time yet?'")
->setArguments(
array(
array(
'name' => 'to',
'param' => 'number',
- 'help' => 'Send sms "To:" the specified number.',
+ 'help' => pht('Send SMS "To:" the specified number.'),
'repeat' => true,
),
array(
'name' => 'body',
'param' => 'text',
- 'help' => 'Send sms with the specified body.',
+ 'help' => pht('Send SMS with the specified body.'),
),
));
}
public function execute(PhutilArgumentParser $args) {
$console = PhutilConsole::getConsole();
$viewer = $this->getViewer();
$tos = $args->getArg('to');
$body = $args->getArg('body');
PhabricatorWorker::setRunAllTasksInProcess(true);
PhabricatorSMSImplementationAdapter::sendSMS($tos, $body);
$console->writeErr(
"%s\n\n phabricator/ $ ./bin/sms list-outbound \n\n",
pht(
'Send completed! You can view the list of SMS messages sent by '.
'running this command:'));
}
}
diff --git a/src/infrastructure/sms/management/PhabricatorSMSManagementShowOutboundWorkflow.php b/src/infrastructure/sms/management/PhabricatorSMSManagementShowOutboundWorkflow.php
index d9bd7e65a..19a306b6d 100644
--- a/src/infrastructure/sms/management/PhabricatorSMSManagementShowOutboundWorkflow.php
+++ b/src/infrastructure/sms/management/PhabricatorSMSManagementShowOutboundWorkflow.php
@@ -1,69 +1,72 @@
<?php
final class PhabricatorSMSManagementShowOutboundWorkflow
extends PhabricatorSMSManagementWorkflow {
protected function didConstruct() {
$this
->setName('show-outbound')
- ->setSynopsis('Show diagnostic details about outbound sms.')
+ ->setSynopsis(pht('Show diagnostic details about outbound SMS.'))
->setExamples(
'**show-outbound** --id 1 --id 2')
->setArguments(
array(
array(
'name' => 'id',
'param' => 'id',
- 'help' => 'Show details about outbound sms with given ID.',
+ 'help' => pht('Show details about outbound SMS with given ID.'),
'repeat' => true,
),
));
}
public function execute(PhutilArgumentParser $args) {
$console = PhutilConsole::getConsole();
$ids = $args->getArg('id');
if (!$ids) {
throw new PhutilArgumentUsageException(
- "Use the '--id' flag to specify one or more sms messages to show.");
+ pht(
+ "Use the '%s' flag to specify one or more SMS messages to show.",
+ '--id'));
}
$messages = id(new PhabricatorSMS())->loadAllWhere(
'id IN (%Ld)',
$ids);
if ($ids) {
$ids = array_fuse($ids);
$missing = array_diff_key($ids, $messages);
if ($missing) {
throw new PhutilArgumentUsageException(
- 'Some specified sms messages do not exist: '.
- implode(', ', array_keys($missing)));
+ pht(
+ 'Some specified SMS messages do not exist: %s',
+ implode(', ', array_keys($missing))));
}
}
$last_key = last_key($messages);
foreach ($messages as $message_key => $message) {
$info = array();
$info[] = pht('PROPERTIES');
$info[] = pht('ID: %d', $message->getID());
$info[] = pht('Status: %s', $message->getSendStatus());
$info[] = pht('To: %s', $message->getToNumber());
$info[] = pht('From: %s', $message->getFromNumber());
$info[] = null;
$info[] = pht('BODY');
$info[] = $message->getBody();
$info[] = null;
$console->writeOut('%s', implode("\n", $info));
if ($message_key != $last_key) {
$console->writeOut("\n%s\n\n", str_repeat('-', 80));
}
}
}
}
diff --git a/src/infrastructure/sms/worker/PhabricatorSMSDemultiplexWorker.php b/src/infrastructure/sms/worker/PhabricatorSMSDemultiplexWorker.php
index daa5244a1..60baa0dbd 100644
--- a/src/infrastructure/sms/worker/PhabricatorSMSDemultiplexWorker.php
+++ b/src/infrastructure/sms/worker/PhabricatorSMSDemultiplexWorker.php
@@ -1,31 +1,30 @@
<?php
-final class PhabricatorSMSDemultiplexWorker
- extends PhabricatorSMSWorker {
+final class PhabricatorSMSDemultiplexWorker extends PhabricatorSMSWorker {
protected function doWork() {
$viewer = PhabricatorUser::getOmnipotentUser();
$task_data = $this->getTaskData();
$to_numbers = idx($task_data, 'toNumbers');
if (!$to_numbers) {
// If we don't have any to numbers, don't send any sms.
return;
}
foreach ($to_numbers as $number) {
// NOTE: we will set the fromNumber and the proper provider data
// in the `PhabricatorSMSSendWorker`.
$sms = PhabricatorSMS::initializeNewSMS($task_data['body']);
$sms->setToNumber($number);
$sms->save();
$this->queueTask(
'PhabricatorSMSSendWorker',
array(
'smsID' => $sms->getID(),
));
}
}
}
diff --git a/src/infrastructure/sms/worker/PhabricatorSMSSendWorker.php b/src/infrastructure/sms/worker/PhabricatorSMSSendWorker.php
index 0ee9aebb7..d7fefe25d 100644
--- a/src/infrastructure/sms/worker/PhabricatorSMSSendWorker.php
+++ b/src/infrastructure/sms/worker/PhabricatorSMSSendWorker.php
@@ -1,85 +1,85 @@
<?php
-final class PhabricatorSMSSendWorker
- extends PhabricatorSMSWorker {
+final class PhabricatorSMSSendWorker extends PhabricatorSMSWorker {
public function getMaximumRetryCount() {
return PhabricatorSMS::MAXIMUM_SEND_TRIES;
}
public function getWaitBeforeRetry(PhabricatorWorkerTask $task) {
return phutil_units('1 minute in seconds');
}
protected function doWork() {
$viewer = PhabricatorUser::getOmnipotentUser();
$task_data = $this->getTaskData();
$sms = id(new PhabricatorSMS())
->loadOneWhere('id = %d', $task_data['smsID']);
if (!$sms) {
throw new PhabricatorWorkerPermanentFailureException(
pht('SMS object was not found.'));
}
// this has the potential to be updated asynchronously
if ($sms->getSendStatus() == PhabricatorSMS::STATUS_SENT) {
return;
}
$adapter = PhabricatorEnv::getEnvConfig('sms.default-adapter');
$adapter = newv($adapter, array());
if ($sms->hasBeenSentAtLeastOnce()) {
$up_to_date_status = $adapter->pollSMSSentStatus($sms);
if ($up_to_date_status) {
$sms->setSendStatus($up_to_date_status);
if ($up_to_date_status == PhabricatorSMS::STATUS_SENT) {
$sms->save();
return;
}
}
// TODO - re-jigger this so we can try if appropos (e.g. rate limiting)
return;
}
$from_number = PhabricatorEnv::getEnvConfig('sms.default-sender');
// always set the from number if we get this far in case of configuration
// changes.
$sms->setFromNumber($from_number);
$adapter->setTo($sms->getToNumber());
$adapter->setFrom($sms->getFromNumber());
$adapter->setBody($sms->getBody());
// give the provider name the same treatment as phone number
$sms->setProviderShortName($adapter->getProviderShortName());
if (PhabricatorEnv::getEnvConfig('phabricator.silent')) {
$sms->setSendStatus(PhabricatorSMS::STATUS_FAILED_PERMANENTLY);
$sms->save();
throw new PhabricatorWorkerPermanentFailureException(
pht(
- 'Phabricator is running in silent mode. See `phabricator.silent` '.
- 'in the configuration to change this setting.'));
+ 'Phabricator is running in silent mode. See `%s` '.
+ 'in the configuration to change this setting.',
+ 'phabricator.silent'));
}
try {
$result = $adapter->send();
list($sms_id, $sent_status) = $adapter->getSMSDataFromResult($result);
} catch (PhabricatorWorkerPermanentFailureException $e) {
$sms->setSendStatus(PhabricatorSMS::STATUS_FAILED_PERMANENTLY);
$sms->save();
throw $e;
} catch (Exception $e) {
$sms->setSendStatus(PhabricatorSMS::STATUS_FAILED_PERMANENTLY);
$sms->save();
throw new PhabricatorWorkerPermanentFailureException(
$e->getMessage());
}
$sms->setProviderSMSID($sms_id);
$sms->setSendStatus($sent_status);
$sms->save();
}
}
diff --git a/src/infrastructure/ssh/PhabricatorSSHPassthruCommand.php b/src/infrastructure/ssh/PhabricatorSSHPassthruCommand.php
index f27d44660..9cd961488 100644
--- a/src/infrastructure/ssh/PhabricatorSSHPassthruCommand.php
+++ b/src/infrastructure/ssh/PhabricatorSSHPassthruCommand.php
@@ -1,224 +1,233 @@
<?php
/**
* Proxy an IO channel to an underlying command, with optional callbacks. This
* is a mostly a more general version of @{class:PhutilExecPassthru}. This
* class is used to proxy Git, SVN and Mercurial traffic to the commands which
* can actually serve it.
*
* Largely, this just reads an IO channel (like stdin from SSH) and writes
* the results into a command channel (like a command's stdin). Then it reads
* the command channel (like the command's stdout) and writes it into the IO
* channel (like stdout from SSH):
*
* IO Channel Command Channel
* stdin -> stdin
* stdout <- stdout
* stderr <- stderr
*
* You can provide **read and write callbacks** which are invoked as data
* is passed through this class. They allow you to inspect and modify traffic.
*
* IO Channel Passthru Command Channel
* stdout -> willWrite -> stdin
* stdin <- willRead <- stdout
* stderr <- (identity) <- stderr
*
* Primarily, this means:
*
* - the **IO Channel** can be a @{class:PhutilProtocolChannel} if the
* **write callback** can convert protocol messages into strings; and
* - the **write callback** can inspect and reject requests over the channel,
* e.g. to enforce policies.
*
* In practice, this is used when serving repositories to check each command
* issued over SSH and determine if it is a read command or a write command.
* Writes can then be checked for appropriate permissions.
*/
final class PhabricatorSSHPassthruCommand extends Phobject {
private $commandChannel;
private $ioChannel;
private $errorChannel;
private $execFuture;
private $willWriteCallback;
private $willReadCallback;
private $pauseIOReads;
public function setCommandChannelFromExecFuture(ExecFuture $exec_future) {
$exec_channel = new PhutilExecChannel($exec_future);
$exec_channel->setStderrHandler(array($this, 'writeErrorIOCallback'));
$this->execFuture = $exec_future;
$this->commandChannel = $exec_channel;
return $this;
}
public function setIOChannel(PhutilChannel $io_channel) {
$this->ioChannel = $io_channel;
return $this;
}
public function setErrorChannel(PhutilChannel $error_channel) {
$this->errorChannel = $error_channel;
return $this;
}
public function setWillReadCallback($will_read_callback) {
$this->willReadCallback = $will_read_callback;
return $this;
}
public function setWillWriteCallback($will_write_callback) {
$this->willWriteCallback = $will_write_callback;
return $this;
}
public function writeErrorIOCallback(PhutilChannel $channel, $data) {
$this->errorChannel->write($data);
}
public function setPauseIOReads($pause) {
$this->pauseIOReads = $pause;
return $this;
}
public function execute() {
$command_channel = $this->commandChannel;
$io_channel = $this->ioChannel;
$error_channel = $this->errorChannel;
if (!$command_channel) {
- throw new Exception('Set a command channel before calling execute()!');
+ throw new Exception(
+ pht(
+ 'Set a command channel before calling %s!',
+ __FUNCTION__.'()'));
}
if (!$io_channel) {
- throw new Exception('Set an IO channel before calling execute()!');
+ throw new Exception(
+ pht(
+ 'Set an IO channel before calling %s!',
+ __FUNCTION__.'()'));
}
if (!$error_channel) {
- throw new Exception('Set an error channel before calling execute()!');
+ throw new Exception(
+ pht(
+ 'Set an error channel before calling %s!',
+ __FUNCTION__.'()'));
}
$channels = array($command_channel, $io_channel, $error_channel);
// We want to limit the amount of data we'll hold in memory for this
// process. See T4241 for a discussion of this issue in general.
$buffer_size = (1024 * 1024); // 1MB
$io_channel->setReadBufferSize($buffer_size);
$command_channel->setReadBufferSize($buffer_size);
// TODO: This just makes us throw away stderr after the first 1MB, but we
// don't currently have the support infrastructure to buffer it correctly.
// It's difficult to imagine this causing problems in practice, though.
$this->execFuture->getStderrSizeLimit($buffer_size);
while (true) {
PhutilChannel::waitForAny($channels);
$io_channel->update();
$command_channel->update();
$error_channel->update();
// If any channel is blocked on the other end, wait for it to flush before
// we continue reading. For example, if a user is running `git clone` on
// a 1GB repository, the underlying `git-upload-pack` may
// be able to produce data much more quickly than we can send it over
// the network. If we don't throttle the reads, we may only send a few
// MB over the I/O channel in the time it takes to read the entire 1GB off
// the command channel. That leaves us with 1GB of data in memory.
while ($command_channel->isOpen() &&
$io_channel->isOpenForWriting() &&
($command_channel->getWriteBufferSize() >= $buffer_size ||
$io_channel->getWriteBufferSize() >= $buffer_size ||
$error_channel->getWriteBufferSize() >= $buffer_size)) {
PhutilChannel::waitForActivity(array(), $channels);
$io_channel->update();
$command_channel->update();
$error_channel->update();
}
// If the subprocess has exited and we've read everything from it,
// we're all done.
$done = !$command_channel->isOpenForReading() &&
$command_channel->isReadBufferEmpty();
if (!$this->pauseIOReads) {
$in_message = $io_channel->read();
if ($in_message !== null) {
$this->writeIORead($in_message);
}
}
$out_message = $command_channel->read();
if (strlen($out_message)) {
$out_message = $this->willReadData($out_message);
if ($out_message !== null) {
$io_channel->write($out_message);
}
}
// If we have nothing left on stdin, close stdin on the subprocess.
if (!$io_channel->isOpenForReading()) {
$command_channel->closeWriteChannel();
}
if ($done) {
break;
}
// If the client has disconnected, kill the subprocess and bail.
if (!$io_channel->isOpenForWriting()) {
$this->execFuture
->setStdoutSizeLimit(0)
->setStderrSizeLimit(0)
->setReadBufferSize(null)
->resolveKill();
break;
}
}
list($err) = $this->execFuture
->setStdoutSizeLimit(0)
->setStderrSizeLimit(0)
->setReadBufferSize(null)
->resolve();
return $err;
}
public function writeIORead($in_message) {
$in_message = $this->willWriteData($in_message);
if (strlen($in_message)) {
$this->commandChannel->write($in_message);
}
}
public function willWriteData($message) {
if ($this->willWriteCallback) {
return call_user_func($this->willWriteCallback, $this, $message);
} else {
if (strlen($message)) {
return $message;
} else {
return null;
}
}
}
public function willReadData($message) {
if ($this->willReadCallback) {
return call_user_func($this->willReadCallback, $this, $message);
} else {
if (strlen($message)) {
return $message;
} else {
return null;
}
}
}
}
diff --git a/src/infrastructure/storage/__tests__/AphrontIsolatedDatabaseConnectionTestCase.php b/src/infrastructure/storage/__tests__/AphrontIsolatedDatabaseConnectionTestCase.php
index ed3c8560d..a0a64481b 100644
--- a/src/infrastructure/storage/__tests__/AphrontIsolatedDatabaseConnectionTestCase.php
+++ b/src/infrastructure/storage/__tests__/AphrontIsolatedDatabaseConnectionTestCase.php
@@ -1,144 +1,144 @@
<?php
final class AphrontIsolatedDatabaseConnectionTestCase
extends PhabricatorTestCase {
protected function getPhabricatorTestCaseConfiguration() {
return array(
// We disable this here because this test is unique (it is testing that
// isolation actually occurs) and must establish a live connection to the
// database to verify that.
self::PHABRICATOR_TESTCONFIG_ISOLATE_LISK => false,
);
}
public function testIsolation() {
// This will fail if the connection isn't isolated.
queryfx(
$this->newIsolatedConnection(),
'INSERT INVALID SYNTAX');
$this->assertTrue(true);
}
public function testInsertGeneratesID() {
$conn = $this->newIsolatedConnection();
queryfx($conn, 'INSERT');
$id1 = $conn->getInsertID();
queryfx($conn, 'INSERT');
$id2 = $conn->getInsertID();
- $this->assertTrue((bool)$id1, 'ID1 exists.');
- $this->assertTrue((bool)$id2, 'ID2 exists.');
+ $this->assertTrue((bool)$id1, pht('ID1 exists.'));
+ $this->assertTrue((bool)$id2, pht('ID2 exists.'));
$this->assertTrue(
$id1 != $id2,
- "IDs '{$id1}' and '{$id2}' are distinct.");
+ pht("IDs '%s' and '%s' are distinct.", $id1, $id2));
}
public function testDeletePermitted() {
$conn = $this->newIsolatedConnection();
queryfx($conn, 'DELETE');
$this->assertTrue(true);
}
public function testTransactionStack() {
$conn = $this->newIsolatedConnection();
$conn->openTransaction();
queryfx($conn, 'INSERT');
$conn->saveTransaction();
$this->assertEqual(
array(
'START TRANSACTION',
'INSERT',
'COMMIT',
),
$conn->getQueryTranscript());
$conn = $this->newIsolatedConnection();
$conn->openTransaction();
queryfx($conn, 'INSERT 1');
$conn->openTransaction();
queryfx($conn, 'INSERT 2');
$conn->killTransaction();
$conn->openTransaction();
queryfx($conn, 'INSERT 3');
$conn->openTransaction();
queryfx($conn, 'INSERT 4');
$conn->saveTransaction();
$conn->saveTransaction();
$conn->openTransaction();
queryfx($conn, 'INSERT 5');
$conn->killTransaction();
queryfx($conn, 'INSERT 6');
$conn->saveTransaction();
$this->assertEqual(
array(
'START TRANSACTION',
'INSERT 1',
'SAVEPOINT Aphront_Savepoint_1',
'INSERT 2',
'ROLLBACK TO SAVEPOINT Aphront_Savepoint_1',
'SAVEPOINT Aphront_Savepoint_1',
'INSERT 3',
'SAVEPOINT Aphront_Savepoint_2',
'INSERT 4',
'SAVEPOINT Aphront_Savepoint_1',
'INSERT 5',
'ROLLBACK TO SAVEPOINT Aphront_Savepoint_1',
'INSERT 6',
'COMMIT',
),
$conn->getQueryTranscript());
}
public function testTransactionRollback() {
$check = array();
$phid = new HarbormasterScratchTable();
$phid->openTransaction();
for ($ii = 0; $ii < 3; $ii++) {
$key = $this->generateTestData();
$obj = new HarbormasterScratchTable();
$obj->setData($key);
$obj->save();
$check[] = $key;
}
$phid->killTransaction();
foreach ($check as $key) {
$this->assertNoSuchRow($key);
}
}
private function newIsolatedConnection() {
$config = array();
return new AphrontIsolatedDatabaseConnection($config);
}
private function generateTestData() {
return Filesystem::readRandomCharacters(20);
}
private function assertNoSuchRow($data) {
try {
$row = id(new HarbormasterScratchTable())->loadOneWhere(
'data = %s',
$data);
$this->assertEqual(
null,
$row,
- 'Expect fake row to exist only in isolation.');
+ pht('Expect fake row to exist only in isolation.'));
} catch (AphrontConnectionQueryException $ex) {
// If we can't connect to the database, conclude that the isolated
// connection actually is isolated. Philosophically, this perhaps allows
// us to claim this test does not depend on the database?
}
}
}
diff --git a/src/infrastructure/storage/__tests__/QueryFormattingTestCase.php b/src/infrastructure/storage/__tests__/QueryFormattingTestCase.php
index 1e2955bbb..6bd0eeeda 100644
--- a/src/infrastructure/storage/__tests__/QueryFormattingTestCase.php
+++ b/src/infrastructure/storage/__tests__/QueryFormattingTestCase.php
@@ -1,56 +1,56 @@
<?php
final class QueryFormattingTestCase extends PhabricatorTestCase {
public function testQueryFormatting() {
$conn_r = id(new PhabricatorUser())->establishConnection('r');
$this->assertEqual(
'NULL',
qsprintf($conn_r, '%nd', null));
$this->assertEqual(
'0',
qsprintf($conn_r, '%nd', 0));
$this->assertEqual(
'0',
qsprintf($conn_r, '%d', 0));
$raised = null;
try {
qsprintf($conn_r, '%d', 'derp');
} catch (Exception $ex) {
$raised = $ex;
}
$this->assertTrue(
(bool)$raised,
- 'qsprintf should raise exception for invalid %d conversion.');
+ pht('%s should raise exception for invalid %%d conversion.', 'qsprintf'));
$this->assertEqual(
"'<S>'",
qsprintf($conn_r, '%s', null));
$this->assertEqual(
'NULL',
qsprintf($conn_r, '%ns', null));
$this->assertEqual(
"'<S>', '<S>'",
qsprintf($conn_r, '%Ls', array('x', 'y')));
$this->assertEqual(
"'<B>'",
qsprintf($conn_r, '%B', null));
$this->assertEqual(
'NULL',
qsprintf($conn_r, '%nB', null));
$this->assertEqual(
"'<B>', '<B>'",
qsprintf($conn_r, '%LB', array('x', 'y')));
}
}
diff --git a/src/infrastructure/storage/lisk/LiskDAO.php b/src/infrastructure/storage/lisk/LiskDAO.php
index d21001c0a..9d8172e02 100644
--- a/src/infrastructure/storage/lisk/LiskDAO.php
+++ b/src/infrastructure/storage/lisk/LiskDAO.php
@@ -1,1930 +1,1946 @@
<?php
/**
* Simple object-authoritative data access object that makes it easy to build
* stuff that you need to save to a database. Basically, it means that the
* amount of boilerplate code (and, particularly, boilerplate SQL) you need
* to write is greatly reduced.
*
* Lisk makes it fairly easy to build something quickly and end up with
* reasonably high-quality code when you're done (e.g., getters and setters,
* objects, transactions, reasonably structured OO code). It's also very thin:
* you can break past it and use MySQL and other lower-level tools when you
* need to in those couple of cases where it doesn't handle your workflow
* gracefully.
*
* However, Lisk won't scale past one database and lacks many of the features
* of modern DAOs like Hibernate: for instance, it does not support joins or
* polymorphic storage.
*
* This means that Lisk is well-suited for tools like Differential, but often a
* poor choice elsewhere. And it is strictly unsuitable for many projects.
*
* Lisk's model is object-authoritative: the PHP class definition is the
* master authority for what the object looks like.
*
* =Building New Objects=
*
* To create new Lisk objects, extend @{class:LiskDAO} and implement
* @{method:establishLiveConnection}. It should return an
* @{class:AphrontDatabaseConnection}; this will tell Lisk where to save your
* objects.
*
* class Dog extends LiskDAO {
*
* protected $name;
* protected $breed;
*
* public function establishLiveConnection() {
* return $some_connection_object;
* }
* }
*
* Now, you should create your table:
*
* lang=sql
* CREATE TABLE dog (
* id int unsigned not null auto_increment primary key,
* name varchar(32) not null,
* breed varchar(32) not null,
* dateCreated int unsigned not null,
* dateModified int unsigned not null
* );
*
* For each property in your class, add a column with the same name to the table
* (see @{method:getConfiguration} for information about changing this mapping).
* Additionally, you should create the three columns `id`, `dateCreated` and
* `dateModified`. Lisk will automatically manage these, using them to implement
* autoincrement IDs and timestamps. If you do not want to use these features,
* see @{method:getConfiguration} for information on disabling them. At a bare
* minimum, you must normally have an `id` column which is a primary or unique
* key with a numeric type, although you can change its name by overriding
* @{method:getIDKey} or disable it entirely by overriding @{method:getIDKey} to
* return null. Note that many methods rely on a single-part primary key and
* will no longer work (they will throw) if you disable it.
*
* As you add more properties to your class in the future, remember to add them
* to the database table as well.
*
* Lisk will now automatically handle these operations: getting and setting
* properties, saving objects, loading individual objects, loading groups
* of objects, updating objects, managing IDs, updating timestamps whenever
* an object is created or modified, and some additional specialized
* operations.
*
* = Creating, Retrieving, Updating, and Deleting =
*
* To create and persist a Lisk object, use @{method:save}:
*
* $dog = id(new Dog())
* ->setName('Sawyer')
* ->setBreed('Pug')
* ->save();
*
* Note that **Lisk automatically builds getters and setters for all of your
* object's protected properties** via @{method:__call}. If you want to add
* custom behavior to your getters or setters, you can do so by overriding the
* @{method:readField} and @{method:writeField} methods.
*
* Calling @{method:save} will persist the object to the database. After calling
* @{method:save}, you can call @{method:getID} to retrieve the object's ID.
*
* To load objects by ID, use the @{method:load} method:
*
* $dog = id(new Dog())->load($id);
*
* This will load the Dog record with ID $id into $dog, or `null` if no such
* record exists (@{method:load} is an instance method rather than a static
* method because PHP does not support late static binding, at least until PHP
* 5.3).
*
* To update an object, change its properties and save it:
*
* $dog->setBreed('Lab')->save();
*
* To delete an object, call @{method:delete}:
*
* $dog->delete();
*
* That's Lisk CRUD in a nutshell.
*
* = Queries =
*
* Often, you want to load a bunch of objects, or execute a more specialized
* query. Use @{method:loadAllWhere} or @{method:loadOneWhere} to do this:
*
* $pugs = $dog->loadAllWhere('breed = %s', 'Pug');
* $sawyer = $dog->loadOneWhere('name = %s', 'Sawyer');
*
* These methods work like @{function@libphutil:queryfx}, but only take half of
* a query (the part after the WHERE keyword). Lisk will handle the connection,
* columns, and object construction; you are responsible for the rest of it.
* @{method:loadAllWhere} returns a list of objects, while
* @{method:loadOneWhere} returns a single object (or `null`).
*
* There's also a @{method:loadRelatives} method which helps to prevent the 1+N
* queries problem.
*
* = Managing Transactions =
*
* Lisk uses a transaction stack, so code does not generally need to be aware
* of the transactional state of objects to implement correct transaction
* semantics:
*
* $obj->openTransaction();
* $obj->save();
* $other->save();
* // ...
* $other->openTransaction();
* $other->save();
* $another->save();
* if ($some_condition) {
* $other->saveTransaction();
* } else {
* $other->killTransaction();
* }
* // ...
* $obj->saveTransaction();
*
* Assuming ##$obj##, ##$other## and ##$another## live on the same database,
* this code will work correctly by establishing savepoints.
*
* Selects whose data are used later in the transaction should be included in
* @{method:beginReadLocking} or @{method:beginWriteLocking} block.
*
* @task conn Managing Connections
* @task config Configuring Lisk
* @task load Loading Objects
* @task info Examining Objects
* @task save Writing Objects
* @task hook Hooks and Callbacks
* @task util Utilities
* @task xaction Managing Transactions
* @task isolate Isolation for Unit Testing
*/
abstract class LiskDAO {
const CONFIG_IDS = 'id-mechanism';
const CONFIG_TIMESTAMPS = 'timestamps';
const CONFIG_AUX_PHID = 'auxiliary-phid';
const CONFIG_SERIALIZATION = 'col-serialization';
const CONFIG_BINARY = 'binary';
const CONFIG_COLUMN_SCHEMA = 'col-schema';
const CONFIG_KEY_SCHEMA = 'key-schema';
const CONFIG_NO_TABLE = 'no-table';
const CONFIG_NO_MUTATE = 'no-mutate';
const SERIALIZATION_NONE = 'id';
const SERIALIZATION_JSON = 'json';
const SERIALIZATION_PHP = 'php';
const IDS_AUTOINCREMENT = 'ids-auto';
const IDS_COUNTER = 'ids-counter';
const IDS_MANUAL = 'ids-manual';
const COUNTER_TABLE_NAME = 'lisk_counter';
private static $processIsolationLevel = 0;
private static $transactionIsolationLevel = 0;
private $ephemeral = false;
private $forcedConnection;
private static $connections = array();
private $inSet = null;
protected $id;
protected $phid;
protected $dateCreated;
protected $dateModified;
/**
* Build an empty object.
*
* @return obj Empty object.
*/
public function __construct() {
$id_key = $this->getIDKey();
if ($id_key) {
$this->$id_key = null;
}
}
/* -( Managing Connections )----------------------------------------------- */
/**
* Establish a live connection to a database service. This method should
* return a new connection. Lisk handles connection caching and management;
* do not perform caching deeper in the stack.
*
* @param string Mode, either 'r' (reading) or 'w' (reading and writing).
* @return AphrontDatabaseConnection New database connection.
* @task conn
*/
abstract protected function establishLiveConnection($mode);
/**
* Return a namespace for this object's connections in the connection cache.
* Generally, the database name is appropriate. Two connections are considered
* equivalent if they have the same connection namespace and mode.
*
* @return string Connection namespace for cache
* @task conn
*/
abstract protected function getConnectionNamespace();
/**
* Get an existing, cached connection for this object.
*
* @param mode Connection mode.
* @return AprontDatabaseConnection|null Connection, if it exists in cache.
* @task conn
*/
protected function getEstablishedConnection($mode) {
$key = $this->getConnectionNamespace().':'.$mode;
if (isset(self::$connections[$key])) {
return self::$connections[$key];
}
return null;
}
/**
* Store a connection in the connection cache.
*
* @param mode Connection mode.
* @param AphrontDatabaseConnection Connection to cache.
* @return this
* @task conn
*/
protected function setEstablishedConnection(
$mode,
AphrontDatabaseConnection $connection,
$force_unique = false) {
$key = $this->getConnectionNamespace().':'.$mode;
if ($force_unique) {
$key .= ':unique';
while (isset(self::$connections[$key])) {
$key .= '!';
}
}
self::$connections[$key] = $connection;
return $this;
}
/**
* Force an object to use a specific connection.
*
* This overrides all connection management and forces the object to use
* a specific connection when interacting with the database.
*
* @param AphrontDatabaseConnection Connection to force this object to use.
* @task conn
*/
public function setForcedConnection(AphrontDatabaseConnection $connection) {
$this->forcedConnection = $connection;
return $this;
}
/* -( Configuring Lisk )--------------------------------------------------- */
/**
* Change Lisk behaviors, like ID configuration and timestamps. If you want
* to change these behaviors, you should override this method in your child
* class and change the options you're interested in. For example:
*
* protected function getConfiguration() {
* return array(
* Lisk_DataAccessObject::CONFIG_EXAMPLE => true,
* ) + parent::getConfiguration();
* }
*
* The available options are:
*
* CONFIG_IDS
* Lisk objects need to have a unique identifying ID. The three mechanisms
* available for generating this ID are IDS_AUTOINCREMENT (default, assumes
* the ID column is an autoincrement primary key), IDS_MANUAL (you are taking
* full responsibility for ID management), or IDS_COUNTER (see below).
*
* InnoDB does not persist the value of `auto_increment` across restarts,
* and instead initializes it to `MAX(id) + 1` during startup. This means it
* may reissue the same autoincrement ID more than once, if the row is deleted
* and then the database is restarted. To avoid this, you can set an object to
* use a counter table with IDS_COUNTER. This will generally behave like
* IDS_AUTOINCREMENT, except that the counter value will persist across
* restarts and inserts will be slightly slower. If a database stores any
* DAOs which use this mechanism, you must create a table there with this
* schema:
*
* CREATE TABLE lisk_counter (
* counterName VARCHAR(64) COLLATE utf8_bin PRIMARY KEY,
* counterValue BIGINT UNSIGNED NOT NULL
* ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
*
* CONFIG_TIMESTAMPS
* Lisk can automatically handle keeping track of a `dateCreated' and
* `dateModified' column, which it will update when it creates or modifies
* an object. If you don't want to do this, you may disable this option.
* By default, this option is ON.
*
* CONFIG_AUX_PHID
* This option can be enabled by being set to some truthy value. The meaning
* of this value is defined by your PHID generation mechanism. If this option
* is enabled, a `phid' property will be populated with a unique PHID when an
* object is created (or if it is saved and does not currently have one). You
* need to override generatePHID() and hook it into your PHID generation
* mechanism for this to work. By default, this option is OFF.
*
* CONFIG_SERIALIZATION
* You can optionally provide a column serialization map that will be applied
* to values when they are written to the database. For example:
*
* self::CONFIG_SERIALIZATION => array(
* 'complex' => self::SERIALIZATION_JSON,
* )
*
* This will cause Lisk to JSON-serialize the 'complex' field before it is
* written, and unserialize it when it is read.
*
* CONFIG_BINARY
* You can optionally provide a map of columns to a flag indicating that
* they store binary data. These columns will not raise an error when
* handling binary writes.
*
* CONFIG_COLUMN_SCHEMA
* Provide a map of columns to schema column types.
*
* CONFIG_KEY_SCHEMA
* Provide a map of key names to key specifications.
*
* CONFIG_NO_TABLE
* Allows you to specify that this object does not actually have a table in
* the database.
*
* CONFIG_NO_MUTATE
* Provide a map of columns which should not be included in UPDATE statements.
* If you have some columns which are always written to explicitly and should
* never be overwritten by a save(), you can specify them here. This is an
* advanced, specialized feature and there are usually better approaches for
* most locking/contention problems.
*
* @return dictionary Map of configuration options to values.
*
* @task config
*/
protected function getConfiguration() {
return array(
self::CONFIG_IDS => self::IDS_AUTOINCREMENT,
self::CONFIG_TIMESTAMPS => true,
);
}
/**
* Determine the setting of a configuration option for this class of objects.
*
* @param const Option name, one of the CONFIG_* constants.
* @return mixed Option value, if configured (null if unavailable).
*
* @task config
*/
public function getConfigOption($option_name) {
static $options = null;
if (!isset($options)) {
$options = $this->getConfiguration();
}
return idx($options, $option_name);
}
/* -( Loading Objects )---------------------------------------------------- */
/**
* Load an object by ID. You need to invoke this as an instance method, not
* a class method, because PHP doesn't have late static binding (until
* PHP 5.3.0). For example:
*
* $dog = id(new Dog())->load($dog_id);
*
* @param int Numeric ID identifying the object to load.
* @return obj|null Identified object, or null if it does not exist.
*
* @task load
*/
public function load($id) {
if (is_object($id)) {
$id = (string)$id;
}
if (!$id || (!is_int($id) && !ctype_digit($id))) {
return null;
}
return $this->loadOneWhere(
'%C = %d',
$this->getIDKeyForUse(),
$id);
}
/**
* Loads all of the objects, unconditionally.
*
* @return dict Dictionary of all persisted objects of this type, keyed
* on object ID.
*
* @task load
*/
public function loadAll() {
return $this->loadAllWhere('1 = 1');
}
/**
* Load all objects which match a WHERE clause. You provide everything after
* the 'WHERE'; Lisk handles everything up to it. For example:
*
* $old_dogs = id(new Dog())->loadAllWhere('age > %d', 7);
*
* The pattern and arguments are as per queryfx().
*
* @param string queryfx()-style SQL WHERE clause.
* @param ... Zero or more conversions.
* @return dict Dictionary of matching objects, keyed on ID.
*
* @task load
*/
public function loadAllWhere($pattern /* , $arg, $arg, $arg ... */) {
$args = func_get_args();
$data = call_user_func_array(
array($this, 'loadRawDataWhere'),
$args);
return $this->loadAllFromArray($data);
}
/**
* Load a single object identified by a 'WHERE' clause. You provide
* everything after the 'WHERE', and Lisk builds the first half of the
* query. See loadAllWhere(). This method is similar, but returns a single
* result instead of a list.
*
* @param string queryfx()-style SQL WHERE clause.
* @param ... Zero or more conversions.
* @return obj|null Matching object, or null if no object matches.
*
* @task load
*/
public function loadOneWhere($pattern /* , $arg, $arg, $arg ... */) {
$args = func_get_args();
$data = call_user_func_array(
array($this, 'loadRawDataWhere'),
$args);
if (count($data) > 1) {
throw new AphrontCountQueryException(
- 'More than 1 result from loadOneWhere()!');
+ pht(
+ 'More than one result from %s!',
+ __FUNCTION__.'()'));
}
$data = reset($data);
if (!$data) {
return null;
}
return $this->loadFromArray($data);
}
protected function loadRawDataWhere($pattern /* , $args... */) {
$connection = $this->establishConnection('r');
$lock_clause = '';
if ($connection->isReadLocking()) {
$lock_clause = 'FOR UPDATE';
} else if ($connection->isWriteLocking()) {
$lock_clause = 'LOCK IN SHARE MODE';
}
$args = func_get_args();
$args = array_slice($args, 1);
$pattern = 'SELECT * FROM %T WHERE '.$pattern.' %Q';
array_unshift($args, $this->getTableName());
array_push($args, $lock_clause);
array_unshift($args, $pattern);
return call_user_func_array(
array($connection, 'queryData'),
$args);
}
/**
* Reload an object from the database, discarding any changes to persistent
* properties. This is primarily useful after entering a transaction but
* before applying changes to an object.
*
* @return this
*
* @task load
*/
public function reload() {
if (!$this->getID()) {
- throw new Exception("Unable to reload object that hasn't been loaded!");
+ throw new Exception(
+ pht("Unable to reload object that hasn't been loaded!"));
}
$result = $this->loadOneWhere(
'%C = %d',
$this->getIDKeyForUse(),
$this->getID());
if (!$result) {
throw new AphrontObjectMissingQueryException();
}
return $this;
}
/**
* Initialize this object's properties from a dictionary. Generally, you
* load single objects with loadOneWhere(), but sometimes it may be more
* convenient to pull data from elsewhere directly (e.g., a complicated
* join via @{method:queryData}) and then load from an array representation.
*
* @param dict Dictionary of properties, which should be equivalent to
* selecting a row from the table or calling
* @{method:getProperties}.
* @return this
*
* @task load
*/
public function loadFromArray(array $row) {
static $valid_properties = array();
$map = array();
foreach ($row as $k => $v) {
// We permit (but ignore) extra properties in the array because a
// common approach to building the array is to issue a raw SELECT query
// which may include extra explicit columns or joins.
// This pathway is very hot on some pages, so we're inlining a cache
// and doing some microoptimization to avoid a strtolower() call for each
// assignment. The common path (assigning a valid property which we've
// already seen) always incurs only one empty(). The second most common
// path (assigning an invalid property which we've already seen) costs
// an empty() plus an isset().
if (empty($valid_properties[$k])) {
if (isset($valid_properties[$k])) {
// The value is set but empty, which means it's false, so we've
// already determined it's not valid. We don't need to check again.
continue;
}
$valid_properties[$k] = $this->hasProperty($k);
if (!$valid_properties[$k]) {
continue;
}
}
$map[$k] = $v;
}
$this->willReadData($map);
foreach ($map as $prop => $value) {
$this->$prop = $value;
}
$this->didReadData();
return $this;
}
/**
* Initialize a list of objects from a list of dictionaries. Usually you
* load lists of objects with @{method:loadAllWhere}, but sometimes that
* isn't flexible enough. One case is if you need to do joins to select the
* right objects:
*
* function loadAllWithOwner($owner) {
* $data = $this->queryData(
* 'SELECT d.*
* FROM owner o
* JOIN owner_has_dog od ON o.id = od.ownerID
* JOIN dog d ON od.dogID = d.id
* WHERE o.id = %d',
* $owner);
* return $this->loadAllFromArray($data);
* }
*
* This is a lot messier than @{method:loadAllWhere}, but more flexible.
*
* @param list List of property dictionaries.
* @return dict List of constructed objects, keyed on ID.
*
* @task load
*/
public function loadAllFromArray(array $rows) {
$result = array();
$id_key = $this->getIDKey();
foreach ($rows as $row) {
$obj = clone $this;
if ($id_key && isset($row[$id_key])) {
$result[$row[$id_key]] = $obj->loadFromArray($row);
} else {
$result[] = $obj->loadFromArray($row);
}
if ($this->inSet) {
$this->inSet->addToSet($obj);
}
}
return $result;
}
/**
* This method helps to prevent the 1+N queries problem. It happens when you
* execute a query for each row in a result set. Like in this code:
*
* COUNTEREXAMPLE, name=Easy to write but expensive to execute
* $diffs = id(new DifferentialDiff())->loadAllWhere(
* 'revisionID = %d',
* $revision->getID());
* foreach ($diffs as $diff) {
* $changesets = id(new DifferentialChangeset())->loadAllWhere(
* 'diffID = %d',
* $diff->getID());
* // Do something with $changesets.
* }
*
* One can solve this problem by reading all the dependent objects at once and
* assigning them later:
*
* COUNTEREXAMPLE, name=Cheaper to execute but harder to write and maintain
* $diffs = id(new DifferentialDiff())->loadAllWhere(
* 'revisionID = %d',
* $revision->getID());
* $all_changesets = id(new DifferentialChangeset())->loadAllWhere(
* 'diffID IN (%Ld)',
* mpull($diffs, 'getID'));
* $all_changesets = mgroup($all_changesets, 'getDiffID');
* foreach ($diffs as $diff) {
* $changesets = idx($all_changesets, $diff->getID(), array());
* // Do something with $changesets.
* }
*
* The method @{method:loadRelatives} abstracts this approach which allows
* writing a code which is simple and efficient at the same time:
*
* name=Easy to write and cheap to execute
* $diffs = $revision->loadRelatives(new DifferentialDiff(), 'revisionID');
* foreach ($diffs as $diff) {
* $changesets = $diff->loadRelatives(
* new DifferentialChangeset(),
* 'diffID');
* // Do something with $changesets.
* }
*
* This will load dependent objects for all diffs in the first call of
* @{method:loadRelatives} and use this result for all following calls.
*
* The method supports working with set of sets, like in this code:
*
* $diffs = $revision->loadRelatives(new DifferentialDiff(), 'revisionID');
* foreach ($diffs as $diff) {
* $changesets = $diff->loadRelatives(
* new DifferentialChangeset(),
* 'diffID');
* foreach ($changesets as $changeset) {
* $hunks = $changeset->loadRelatives(
* new DifferentialHunk(),
* 'changesetID');
* // Do something with hunks.
* }
* }
*
* This code will execute just three queries - one to load all diffs, one to
* load all their related changesets and one to load all their related hunks.
* You can try to write an equivalent code without using this method as
* a homework.
*
* The method also supports retrieving referenced objects, for example authors
* of all diffs (using shortcut @{method:loadOneRelative}):
*
* foreach ($diffs as $diff) {
* $author = $diff->loadOneRelative(
* new PhabricatorUser(),
* 'phid',
* 'getAuthorPHID');
* // Do something with author.
* }
*
* It is also possible to specify additional conditions for the `WHERE`
* clause. Similarly to @{method:loadAllWhere}, you can specify everything
* after `WHERE` (except `LIMIT`). Contrary to @{method:loadAllWhere}, it is
* allowed to pass only a constant string (`%` doesn't have a special
* meaning). This is intentional to avoid mistakes with using data from one
* row in retrieving other rows. Example of a correct usage:
*
* $status = $author->loadOneRelative(
* new PhabricatorCalendarEvent(),
* 'userPHID',
* 'getPHID',
* '(UNIX_TIMESTAMP() BETWEEN dateFrom AND dateTo)');
*
* @param LiskDAO Type of objects to load.
* @param string Name of the column in target table.
* @param string Method name in this table.
* @param string Additional constraints on returned rows. It supports no
* placeholders and requires putting the WHERE part into
* parentheses. It's not possible to use LIMIT.
* @return list Objects of type $object.
*
* @task load
*/
public function loadRelatives(
LiskDAO $object,
$foreign_column,
$key_method = 'getID',
$where = '') {
if (!$this->inSet) {
id(new LiskDAOSet())->addToSet($this);
}
$relatives = $this->inSet->loadRelatives(
$object,
$foreign_column,
$key_method,
$where);
return idx($relatives, $this->$key_method(), array());
}
/**
* Load referenced row. See @{method:loadRelatives} for details.
*
* @param LiskDAO Type of objects to load.
* @param string Name of the column in target table.
* @param string Method name in this table.
* @param string Additional constraints on returned rows. It supports no
* placeholders and requires putting the WHERE part into
* parentheses. It's not possible to use LIMIT.
* @return LiskDAO Object of type $object or null if there's no such object.
*
* @task load
*/
final public function loadOneRelative(
LiskDAO $object,
$foreign_column,
$key_method = 'getID',
$where = '') {
$relatives = $this->loadRelatives(
$object,
$foreign_column,
$key_method,
$where);
if (!$relatives) {
return null;
}
if (count($relatives) > 1) {
throw new AphrontCountQueryException(
- 'More than 1 result from loadOneRelative()!');
+ pht(
+ 'More than one result from %s!',
+ __FUNCTION__.'()'));
}
return reset($relatives);
}
final public function putInSet(LiskDAOSet $set) {
$this->inSet = $set;
return $this;
}
final protected function getInSet() {
return $this->inSet;
}
/* -( Examining Objects )-------------------------------------------------- */
/**
* Set unique ID identifying this object. You normally don't need to call this
* method unless with `IDS_MANUAL`.
*
* @param mixed Unique ID.
* @return this
* @task save
*/
public function setID($id) {
static $id_key = null;
if ($id_key === null) {
$id_key = $this->getIDKeyForUse();
}
$this->$id_key = $id;
return $this;
}
/**
* Retrieve the unique ID identifying this object. This value will be null if
* the object hasn't been persisted and you didn't set it manually.
*
* @return mixed Unique ID.
*
* @task info
*/
public function getID() {
static $id_key = null;
if ($id_key === null) {
$id_key = $this->getIDKeyForUse();
}
return $this->$id_key;
}
public function getPHID() {
return $this->phid;
}
/**
* Test if a property exists.
*
* @param string Property name.
* @return bool True if the property exists.
* @task info
*/
public function hasProperty($property) {
return (bool)$this->checkProperty($property);
}
/**
* Retrieve a list of all object properties. This list only includes
* properties that are declared as protected, and it is expected that
* all properties returned by this function should be persisted to the
* database.
* Properties that should not be persisted must be declared as private.
*
* @return dict Dictionary of normalized (lowercase) to canonical (original
* case) property names.
*
* @task info
*/
protected function getAllLiskProperties() {
static $properties = null;
if (!isset($properties)) {
$class = new ReflectionClass(get_class($this));
$properties = array();
foreach ($class->getProperties(ReflectionProperty::IS_PROTECTED) as $p) {
$properties[strtolower($p->getName())] = $p->getName();
}
$id_key = $this->getIDKey();
if ($id_key != 'id') {
unset($properties['id']);
}
if (!$this->getConfigOption(self::CONFIG_TIMESTAMPS)) {
unset($properties['datecreated']);
unset($properties['datemodified']);
}
if ($id_key != 'phid' && !$this->getConfigOption(self::CONFIG_AUX_PHID)) {
unset($properties['phid']);
}
}
return $properties;
}
/**
* Check if a property exists on this object.
*
* @return string|null Canonical property name, or null if the property
* does not exist.
*
* @task info
*/
protected function checkProperty($property) {
static $properties = null;
if ($properties === null) {
$properties = $this->getAllLiskProperties();
}
$property = strtolower($property);
if (empty($properties[$property])) {
return null;
}
return $properties[$property];
}
/**
* Get or build the database connection for this object.
*
* @param string 'r' for read, 'w' for read/write.
* @param bool True to force a new connection. The connection will not
* be retrieved from or saved into the connection cache.
* @return LiskDatabaseConnection Lisk connection object.
*
* @task info
*/
public function establishConnection($mode, $force_new = false) {
if ($mode != 'r' && $mode != 'w') {
- throw new Exception("Unknown mode '{$mode}', should be 'r' or 'w'.");
+ throw new Exception(
+ pht(
+ "Unknown mode '%s', should be 'r' or 'w'.",
+ $mode));
}
if ($this->forcedConnection) {
return $this->forcedConnection;
}
if (self::shouldIsolateAllLiskEffectsToCurrentProcess()) {
$mode = 'isolate-'.$mode;
$connection = $this->getEstablishedConnection($mode);
if (!$connection) {
$connection = $this->establishIsolatedConnection($mode);
$this->setEstablishedConnection($mode, $connection);
}
return $connection;
}
if (self::shouldIsolateAllLiskEffectsToTransactions()) {
// If we're doing fixture transaction isolation, force the mode to 'w'
// so we always get the same connection for reads and writes, and thus
// can see the writes inside the transaction.
$mode = 'w';
}
// TODO: There is currently no protection on 'r' queries against writing.
$connection = null;
if (!$force_new) {
if ($mode == 'r') {
// If we're requesting a read connection but already have a write
// connection, reuse the write connection so that reads can take place
// inside transactions.
$connection = $this->getEstablishedConnection('w');
}
if (!$connection) {
$connection = $this->getEstablishedConnection($mode);
}
}
if (!$connection) {
$connection = $this->establishLiveConnection($mode);
if (self::shouldIsolateAllLiskEffectsToTransactions()) {
$connection->openTransaction();
}
$this->setEstablishedConnection(
$mode,
$connection,
$force_unique = $force_new);
}
return $connection;
}
/**
* Convert this object into a property dictionary. This dictionary can be
* restored into an object by using @{method:loadFromArray} (unless you're
* using legacy features with CONFIG_CONVERT_CAMELCASE, but in that case you
* should just go ahead and die in a fire).
*
* @return dict Dictionary of object properties.
*
* @task info
*/
protected function getAllLiskPropertyValues() {
$map = array();
foreach ($this->getAllLiskProperties() as $p) {
// We may receive a warning here for properties we've implicitly added
// through configuration; squelch it.
$map[$p] = @$this->$p;
}
return $map;
}
/* -( Writing Objects )---------------------------------------------------- */
/**
* Make an object read-only.
*
* Making an object ephemeral indicates that you will be changing state in
* such a way that you would never ever want it to be written back to the
* storage.
*/
public function makeEphemeral() {
$this->ephemeral = true;
return $this;
}
private function isEphemeralCheck() {
if ($this->ephemeral) {
throw new LiskEphemeralObjectException();
}
}
/**
* Persist this object to the database. In most cases, this is the only
* method you need to call to do writes. If the object has not yet been
* inserted this will do an insert; if it has, it will do an update.
*
* @return this
*
* @task save
*/
public function save() {
if ($this->shouldInsertWhenSaved()) {
return $this->insert();
} else {
return $this->update();
}
}
/**
* Save this object, forcing the query to use REPLACE regardless of object
* state.
*
* @return this
*
* @task save
*/
public function replace() {
$this->isEphemeralCheck();
return $this->insertRecordIntoDatabase('REPLACE');
}
/**
* Save this object, forcing the query to use INSERT regardless of object
* state.
*
* @return this
*
* @task save
*/
public function insert() {
$this->isEphemeralCheck();
return $this->insertRecordIntoDatabase('INSERT');
}
/**
* Save this object, forcing the query to use UPDATE regardless of object
* state.
*
* @return this
*
* @task save
*/
public function update() {
$this->isEphemeralCheck();
$this->willSaveObject();
$data = $this->getAllLiskPropertyValues();
// Remove colums flagged as nonmutable from the update statement.
$no_mutate = $this->getConfigOption(self::CONFIG_NO_MUTATE);
if ($no_mutate) {
foreach ($no_mutate as $column) {
unset($data[$column]);
}
}
$this->willWriteData($data);
$map = array();
foreach ($data as $k => $v) {
$map[$k] = $v;
}
$conn = $this->establishConnection('w');
$binary = $this->getBinaryColumns();
foreach ($map as $key => $value) {
if (!empty($binary[$key])) {
$map[$key] = qsprintf($conn, '%C = %nB', $key, $value);
} else {
$map[$key] = qsprintf($conn, '%C = %ns', $key, $value);
}
}
$map = implode(', ', $map);
$id = $this->getID();
$conn->query(
'UPDATE %T SET %Q WHERE %C = '.(is_int($id) ? '%d' : '%s'),
$this->getTableName(),
$map,
$this->getIDKeyForUse(),
$id);
// We can't detect a missing object because updating an object without
// changing any values doesn't affect rows. We could jiggle timestamps
// to catch this for objects which track them if we wanted.
$this->didWriteData();
return $this;
}
/**
* Delete this object, permanently.
*
* @return this
*
* @task save
*/
public function delete() {
$this->isEphemeralCheck();
$this->willDelete();
$conn = $this->establishConnection('w');
$conn->query(
'DELETE FROM %T WHERE %C = %d',
$this->getTableName(),
$this->getIDKeyForUse(),
$this->getID());
$this->didDelete();
return $this;
}
/**
* Internal implementation of INSERT and REPLACE.
*
* @param const Either "INSERT" or "REPLACE", to force the desired mode.
*
* @task save
*/
protected function insertRecordIntoDatabase($mode) {
$this->willSaveObject();
$data = $this->getAllLiskPropertyValues();
$conn = $this->establishConnection('w');
$id_mechanism = $this->getConfigOption(self::CONFIG_IDS);
switch ($id_mechanism) {
case self::IDS_AUTOINCREMENT:
// If we are using autoincrement IDs, let MySQL assign the value for the
// ID column, if it is empty. If the caller has explicitly provided a
// value, use it.
$id_key = $this->getIDKeyForUse();
if (empty($data[$id_key])) {
unset($data[$id_key]);
}
break;
case self::IDS_COUNTER:
// If we are using counter IDs, assign a new ID if we don't already have
// one.
$id_key = $this->getIDKeyForUse();
if (empty($data[$id_key])) {
$counter_name = $this->getTableName();
$id = self::loadNextCounterValue($conn, $counter_name);
$this->setID($id);
$data[$id_key] = $id;
}
break;
case self::IDS_MANUAL:
break;
default:
- throw new Exception('Unknown CONFIG_IDs mechanism!');
+ throw new Exception(pht('Unknown %s mechanism!', 'CONFIG_IDs'));
}
$this->willWriteData($data);
$columns = array_keys($data);
$binary = $this->getBinaryColumns();
foreach ($data as $key => $value) {
try {
if (!empty($binary[$key])) {
$data[$key] = qsprintf($conn, '%nB', $value);
} else {
$data[$key] = qsprintf($conn, '%ns', $value);
}
} catch (AphrontParameterQueryException $parameter_exception) {
throw new PhutilProxyException(
pht(
"Unable to insert or update object of class %s, field '%s' ".
- "has a nonscalar value.",
+ "has a non-scalar value.",
get_class($this),
$key),
$parameter_exception);
}
}
$data = implode(', ', $data);
$conn->query(
'%Q INTO %T (%LC) VALUES (%Q)',
$mode,
$this->getTableName(),
$columns,
$data);
// Only use the insert id if this table is using auto-increment ids
if ($id_mechanism === self::IDS_AUTOINCREMENT) {
$this->setID($conn->getInsertID());
}
$this->didWriteData();
return $this;
}
/**
* Method used to determine whether to insert or update when saving.
*
* @return bool true if the record should be inserted
*/
protected function shouldInsertWhenSaved() {
$key_type = $this->getConfigOption(self::CONFIG_IDS);
if ($key_type == self::IDS_MANUAL) {
throw new Exception(
- 'You are using manual IDs. You must override the '.
- 'shouldInsertWhenSaved() method to properly detect '.
- 'when to insert a new record.');
+ pht(
+ 'You are using manual IDs. You must override the %s method '.
+ 'to properly detect when to insert a new record.',
+ __FUNCTION__.'()'));
} else {
return !$this->getID();
}
}
/* -( Hooks and Callbacks )------------------------------------------------ */
/**
* Retrieve the database table name. By default, this is the class name.
*
* @return string Table name for object storage.
*
* @task hook
*/
public function getTableName() {
return get_class($this);
}
/**
* Retrieve the primary key column, "id" by default. If you can not
* reasonably name your ID column "id", override this method.
*
* @return string Name of the ID column.
*
* @task hook
*/
public function getIDKey() {
return 'id';
}
protected function getIDKeyForUse() {
$id_key = $this->getIDKey();
if (!$id_key) {
throw new Exception(
- 'This DAO does not have a single-part primary key. The method you '.
- 'called requires a single-part primary key.');
+ pht(
+ 'This DAO does not have a single-part primary key. The method you '.
+ 'called requires a single-part primary key.'));
}
return $id_key;
}
/**
* Generate a new PHID, used by CONFIG_AUX_PHID.
*
* @return phid Unique, newly allocated PHID.
*
* @task hook
*/
public function generatePHID() {
throw new Exception(
- 'To use CONFIG_AUX_PHID, you need to overload '.
- 'generatePHID() to perform PHID generation.');
+ pht(
+ 'To use %s, you need to overload %s to perform PHID generation.',
+ 'CONFIG_AUX_PHID',
+ 'generatePHID()'));
}
/**
* Hook to apply serialization or validation to data before it is written to
* the database. See also @{method:willReadData}.
*
* @task hook
*/
protected function willWriteData(array &$data) {
$this->applyLiskDataSerialization($data, false);
}
/**
* Hook to perform actions after data has been written to the database.
*
* @task hook
*/
protected function didWriteData() {}
/**
* Hook to make internal object state changes prior to INSERT, REPLACE or
* UPDATE.
*
* @task hook
*/
protected function willSaveObject() {
$use_timestamps = $this->getConfigOption(self::CONFIG_TIMESTAMPS);
if ($use_timestamps) {
if (!$this->getDateCreated()) {
$this->setDateCreated(time());
}
$this->setDateModified(time());
}
if ($this->getConfigOption(self::CONFIG_AUX_PHID) && !$this->getPHID()) {
$this->setPHID($this->generatePHID());
}
}
/**
* Hook to apply serialization or validation to data as it is read from the
* database. See also @{method:willWriteData}.
*
* @task hook
*/
protected function willReadData(array &$data) {
$this->applyLiskDataSerialization($data, $deserialize = true);
}
/**
* Hook to perform an action on data after it is read from the database.
*
* @task hook
*/
protected function didReadData() {}
/**
* Hook to perform an action before the deletion of an object.
*
* @task hook
*/
protected function willDelete() {}
/**
* Hook to perform an action after the deletion of an object.
*
* @task hook
*/
protected function didDelete() {}
/**
* Reads the value from a field. Override this method for custom behavior
* of @{method:getField} instead of overriding getField directly.
*
* @param string Canonical field name
* @return mixed Value of the field
*
* @task hook
*/
protected function readField($field) {
if (isset($this->$field)) {
return $this->$field;
}
return null;
}
/**
* Writes a value to a field. Override this method for custom behavior of
* setField($value) instead of overriding setField directly.
*
* @param string Canonical field name
* @param mixed Value to write
*
* @task hook
*/
protected function writeField($field, $value) {
$this->$field = $value;
}
/* -( Manging Transactions )----------------------------------------------- */
/**
* Increase transaction stack depth.
*
* @return this
*/
public function openTransaction() {
$this->establishConnection('w')->openTransaction();
return $this;
}
/**
* Decrease transaction stack depth, saving work.
*
* @return this
*/
public function saveTransaction() {
$this->establishConnection('w')->saveTransaction();
return $this;
}
/**
* Decrease transaction stack depth, discarding work.
*
* @return this
*/
public function killTransaction() {
$this->establishConnection('w')->killTransaction();
return $this;
}
/**
* Begins read-locking selected rows with SELECT ... FOR UPDATE, so that
* other connections can not read them (this is an enormous oversimplification
* of FOR UPDATE semantics; consult the MySQL documentation for details). To
* end read locking, call @{method:endReadLocking}. For example:
*
* $beach->openTransaction();
* $beach->beginReadLocking();
*
* $beach->reload();
* $beach->setGrainsOfSand($beach->getGrainsOfSand() + 1);
* $beach->save();
*
* $beach->endReadLocking();
* $beach->saveTransaction();
*
* @return this
* @task xaction
*/
public function beginReadLocking() {
$this->establishConnection('w')->beginReadLocking();
return $this;
}
/**
* Ends read-locking that began at an earlier @{method:beginReadLocking} call.
*
* @return this
* @task xaction
*/
public function endReadLocking() {
$this->establishConnection('w')->endReadLocking();
return $this;
}
/**
* Begins write-locking selected rows with SELECT ... LOCK IN SHARE MODE, so
* that other connections can not update or delete them (this is an
* oversimplification of LOCK IN SHARE MODE semantics; consult the
* MySQL documentation for details). To end write locking, call
* @{method:endWriteLocking}.
*
* @return this
* @task xaction
*/
public function beginWriteLocking() {
$this->establishConnection('w')->beginWriteLocking();
return $this;
}
/**
* Ends write-locking that began at an earlier @{method:beginWriteLocking}
* call.
*
* @return this
* @task xaction
*/
public function endWriteLocking() {
$this->establishConnection('w')->endWriteLocking();
return $this;
}
/* -( Isolation )---------------------------------------------------------- */
/**
* @task isolate
*/
public static function beginIsolateAllLiskEffectsToCurrentProcess() {
self::$processIsolationLevel++;
}
/**
* @task isolate
*/
public static function endIsolateAllLiskEffectsToCurrentProcess() {
self::$processIsolationLevel--;
if (self::$processIsolationLevel < 0) {
throw new Exception(
- 'Lisk process isolation level was reduced below 0.');
+ pht('Lisk process isolation level was reduced below 0.'));
}
}
/**
* @task isolate
*/
public static function shouldIsolateAllLiskEffectsToCurrentProcess() {
return (bool)self::$processIsolationLevel;
}
/**
* @task isolate
*/
private function establishIsolatedConnection($mode) {
$config = array();
return new AphrontIsolatedDatabaseConnection($config);
}
/**
* @task isolate
*/
public static function beginIsolateAllLiskEffectsToTransactions() {
if (self::$transactionIsolationLevel === 0) {
self::closeAllConnections();
}
self::$transactionIsolationLevel++;
}
/**
* @task isolate
*/
public static function endIsolateAllLiskEffectsToTransactions() {
self::$transactionIsolationLevel--;
if (self::$transactionIsolationLevel < 0) {
throw new Exception(
- 'Lisk transaction isolation level was reduced below 0.');
+ pht('Lisk transaction isolation level was reduced below 0.'));
} else if (self::$transactionIsolationLevel == 0) {
foreach (self::$connections as $key => $conn) {
if ($conn) {
$conn->killTransaction();
}
}
self::closeAllConnections();
}
}
/**
* @task isolate
*/
public static function shouldIsolateAllLiskEffectsToTransactions() {
return (bool)self::$transactionIsolationLevel;
}
public static function closeAllConnections() {
self::$connections = array();
}
/* -( Utilities )---------------------------------------------------------- */
/**
* Applies configured serialization to a dictionary of values.
*
* @task util
*/
protected function applyLiskDataSerialization(array &$data, $deserialize) {
$serialization = $this->getConfigOption(self::CONFIG_SERIALIZATION);
if ($serialization) {
foreach (array_intersect_key($serialization, $data) as $col => $format) {
switch ($format) {
case self::SERIALIZATION_NONE:
break;
case self::SERIALIZATION_PHP:
if ($deserialize) {
$data[$col] = unserialize($data[$col]);
} else {
$data[$col] = serialize($data[$col]);
}
break;
case self::SERIALIZATION_JSON:
if ($deserialize) {
$data[$col] = json_decode($data[$col], true);
} else {
$data[$col] = json_encode($data[$col]);
}
break;
default:
- throw new Exception("Unknown serialization format '{$format}'.");
+ throw new Exception(
+ pht("Unknown serialization format '%s'.", $format));
}
}
}
}
/**
* Black magic. Builds implied get*() and set*() for all properties.
*
* @param string Method name.
* @param list Argument vector.
* @return mixed get*() methods return the property value. set*() methods
* return $this.
* @task util
*/
public function __call($method, $args) {
// NOTE: PHP has a bug that static variables defined in __call() are shared
// across all children classes. Call a different method to work around this
// bug.
return $this->call($method, $args);
}
/**
* @task util
*/
final protected function call($method, $args) {
// NOTE: This method is very performance-sensitive (many thousands of calls
// per page on some pages), and thus has some silliness in the name of
// optimizations.
static $dispatch_map = array();
if ($method[0] === 'g') {
if (isset($dispatch_map[$method])) {
$property = $dispatch_map[$method];
} else {
if (substr($method, 0, 3) !== 'get') {
- throw new Exception("Unable to resolve method '{$method}'!");
+ throw new Exception(pht("Unable to resolve method '%s'!", $method));
}
$property = substr($method, 3);
if (!($property = $this->checkProperty($property))) {
- throw new Exception("Bad getter call: {$method}");
+ throw new Exception(pht('Bad getter call: %s', $method));
}
$dispatch_map[$method] = $property;
}
return $this->readField($property);
}
if ($method[0] === 's') {
if (isset($dispatch_map[$method])) {
$property = $dispatch_map[$method];
} else {
if (substr($method, 0, 3) !== 'set') {
- throw new Exception("Unable to resolve method '{$method}'!");
+ throw new Exception(pht("Unable to resolve method '%s'!", $method));
}
$property = substr($method, 3);
$property = $this->checkProperty($property);
if (!$property) {
- throw new Exception("Bad setter call: {$method}");
+ throw new Exception(pht('Bad setter call: %s', $method));
}
$dispatch_map[$method] = $property;
}
$this->writeField($property, $args[0]);
return $this;
}
- throw new Exception("Unable to resolve method '{$method}'.");
+ throw new Exception(pht("Unable to resolve method '%s'.", $method));
}
/**
* Warns against writing to undeclared property.
*
* @task util
*/
public function __set($name, $value) {
- phlog('Wrote to undeclared property '.get_class($this).'::$'.$name.'.');
+ phlog(
+ pht(
+ 'Wrote to undeclared property %s.',
+ get_class($this).'::$'.$name));
$this->$name = $value;
}
/**
* Increments a named counter and returns the next value.
*
* @param AphrontDatabaseConnection Database where the counter resides.
* @param string Counter name to create or increment.
* @return int Next counter value.
*
* @task util
*/
public static function loadNextCounterValue(
AphrontDatabaseConnection $conn_w,
$counter_name) {
// NOTE: If an insert does not touch an autoincrement row or call
// LAST_INSERT_ID(), MySQL normally does not change the value of
// LAST_INSERT_ID(). This can cause a counter's value to leak to a
// new counter if the second counter is created after the first one is
// updated. To avoid this, we insert LAST_INSERT_ID(1), to ensure the
// LAST_INSERT_ID() is always updated and always set correctly after the
// query completes.
queryfx(
$conn_w,
'INSERT INTO %T (counterName, counterValue) VALUES
(%s, LAST_INSERT_ID(1))
ON DUPLICATE KEY UPDATE
counterValue = LAST_INSERT_ID(counterValue + 1)',
self::COUNTER_TABLE_NAME,
$counter_name);
return $conn_w->getInsertID();
}
/**
* Returns the current value of a named counter.
*
* @param AphrontDatabaseConnection Database where the counter resides.
* @param string Counter name to read.
* @return int|null Current value, or `null` if the counter does not exist.
*
* @task util
*/
public static function loadCurrentCounterValue(
AphrontDatabaseConnection $conn_r,
$counter_name) {
$row = queryfx_one(
$conn_r,
'SELECT counterValue FROM %T WHERE counterName = %s',
self::COUNTER_TABLE_NAME,
$counter_name);
if (!$row) {
return null;
}
return (int)$row['counterValue'];
}
/**
* Overwrite a named counter, forcing it to a specific value.
*
* If the counter does not exist, it is created.
*
* @param AphrontDatabaseConnection Database where the counter resides.
* @param string Counter name to create or overwrite.
* @return void
*
* @task util
*/
public static function overwriteCounterValue(
AphrontDatabaseConnection $conn_w,
$counter_name,
$counter_value) {
queryfx(
$conn_w,
'INSERT INTO %T (counterName, counterValue) VALUES (%s, %d)
ON DUPLICATE KEY UPDATE counterValue = VALUES(counterValue)',
self::COUNTER_TABLE_NAME,
$counter_name,
$counter_value);
}
private function getBinaryColumns() {
return $this->getConfigOption(self::CONFIG_BINARY);
}
public function getSchemaColumns() {
$custom_map = $this->getConfigOption(self::CONFIG_COLUMN_SCHEMA);
if (!$custom_map) {
$custom_map = array();
}
$serialization = $this->getConfigOption(self::CONFIG_SERIALIZATION);
if (!$serialization) {
$serialization = array();
}
$serialization_map = array(
self::SERIALIZATION_JSON => 'text',
self::SERIALIZATION_PHP => 'bytes',
);
$binary_map = $this->getBinaryColumns();
$id_mechanism = $this->getConfigOption(self::CONFIG_IDS);
if ($id_mechanism == self::IDS_AUTOINCREMENT) {
$id_type = 'auto';
} else {
$id_type = 'id';
}
$builtin = array(
'id' => $id_type,
'phid' => 'phid',
'viewPolicy' => 'policy',
'editPolicy' => 'policy',
'epoch' => 'epoch',
'dateCreated' => 'epoch',
'dateModified' => 'epoch',
);
$map = array();
foreach ($this->getAllLiskProperties() as $property) {
// First, use types specified explicitly in the table configuration.
if (array_key_exists($property, $custom_map)) {
$map[$property] = $custom_map[$property];
continue;
}
// If we don't have an explicit type, try a builtin type for the
// column.
$type = idx($builtin, $property);
if ($type) {
$map[$property] = $type;
continue;
}
// If the column has serialization, we can infer the column type.
if (isset($serialization[$property])) {
$type = idx($serialization_map, $serialization[$property]);
if ($type) {
$map[$property] = $type;
continue;
}
}
if (isset($binary_map[$property])) {
$map[$property] = 'bytes';
continue;
}
// If the column is named `somethingPHID`, infer it is a PHID.
if (preg_match('/[a-z]PHID$/', $property)) {
$map[$property] = 'phid';
continue;
}
// If the column is named `somethingID`, infer it is an ID.
if (preg_match('/[a-z]ID$/', $property)) {
$map[$property] = 'id';
continue;
}
// We don't know the type of this column.
$map[$property] = PhabricatorConfigSchemaSpec::DATATYPE_UNKNOWN;
}
return $map;
}
public function getSchemaKeys() {
$custom_map = $this->getConfigOption(self::CONFIG_KEY_SCHEMA);
if (!$custom_map) {
$custom_map = array();
}
$default_map = array();
foreach ($this->getAllLiskProperties() as $property) {
switch ($property) {
case 'id':
$default_map['PRIMARY'] = array(
'columns' => array('id'),
'unique' => true,
);
break;
case 'phid':
$default_map['key_phid'] = array(
'columns' => array('phid'),
'unique' => true,
);
break;
}
}
return $custom_map + $default_map;
}
}
diff --git a/src/infrastructure/storage/lisk/LiskDAOSet.php b/src/infrastructure/storage/lisk/LiskDAOSet.php
index 399aef368..f81f54f9c 100644
--- a/src/infrastructure/storage/lisk/LiskDAOSet.php
+++ b/src/infrastructure/storage/lisk/LiskDAOSet.php
@@ -1,89 +1,92 @@
<?php
/**
* You usually don't need to use this class directly as it is controlled by
* @{class:LiskDAO}. You can create it if you want to work with objects of same
* type from different sources as with one set. Let's say you want to get
* e-mails of all users involved in a revision:
*
* $users = new LiskDAOSet();
* $users->addToSet($author);
* foreach ($reviewers as $reviewer) {
* $users->addToSet($reviewer);
* }
* foreach ($ccs as $cc) {
* $users->addToSet($cc);
* }
* // Preload e-mails of all involved users and return e-mails of author.
* $author_emails = $author->loadRelatives(
* new PhabricatorUserEmail(),
* 'userPHID',
* 'getPHID');
*/
final class LiskDAOSet {
private $daos = array();
private $relatives = array();
private $subsets = array();
public function addToSet(LiskDAO $dao) {
if ($this->relatives) {
- throw new Exception("Don't call addToSet() after loading data!");
+ throw new Exception(
+ pht(
+ "Don't call %s after loading data!",
+ __FUNCTION__.'()'));
}
$this->daos[] = $dao;
$dao->putInSet($this);
return $this;
}
/**
* The main purpose of this method is to break cyclic dependency.
* It removes all objects from this set and all subsets created by it.
*/
public function clearSet() {
$this->daos = array();
$this->relatives = array();
foreach ($this->subsets as $set) {
$set->clearSet();
}
$this->subsets = array();
return $this;
}
/**
* See @{method:LiskDAO::loadRelatives}.
*/
public function loadRelatives(
LiskDAO $object,
$foreign_column,
$key_method = 'getID',
$where = '') {
$relatives = &$this->relatives[
get_class($object)."-{$foreign_column}-{$key_method}-{$where}"];
if ($relatives === null) {
$ids = array();
foreach ($this->daos as $dao) {
$id = $dao->$key_method();
if ($id !== null) {
$ids[$id] = $id;
}
}
if (!$ids) {
$relatives = array();
} else {
$set = new LiskDAOSet();
$this->subsets[] = $set;
$relatives = $object->putInSet($set)->loadAllWhere(
'%C IN (%Ls) %Q',
$foreign_column,
$ids,
($where != '' ? 'AND '.$where : ''));
$relatives = mgroup($relatives, 'get'.$foreign_column);
}
}
return $relatives;
}
}
diff --git a/src/infrastructure/storage/lisk/PhabricatorDataNotAttachedException.php b/src/infrastructure/storage/lisk/PhabricatorDataNotAttachedException.php
index 7e32428b5..6f25c4d55 100644
--- a/src/infrastructure/storage/lisk/PhabricatorDataNotAttachedException.php
+++ b/src/infrastructure/storage/lisk/PhabricatorDataNotAttachedException.php
@@ -1,35 +1,35 @@
<?php
final class PhabricatorDataNotAttachedException extends Exception {
public function __construct($object) {
$stack = debug_backtrace();
// Shift off `PhabricatorDataNotAttachedException::__construct()`.
array_shift($stack);
// Shift off `PhabricatorLiskDAO::assertAttached()`.
array_shift($stack);
$frame = head($stack);
$via = null;
if (is_array($frame)) {
$method = idx($frame, 'function');
if (preg_match('/^get[A-Z]/', $method)) {
- $via = " (via {$method}())";
+ $via = ' '.pht('(via %s)', "{$method}()");
}
}
- $class = get_class($object);
-
- $message =
- "Attempting to access attached data on {$class}{$via}, but the data is ".
- "not actually attached. Before accessing attachable data on an object, ".
- "you must load and attach it.\n\n".
- "Data is normally attached by calling the corresponding needX() ".
- "method on the Query class when the object is loaded. You can also ".
- "call the corresponding attachX() method explicitly.";
-
- parent::__construct($message);
+ parent::__construct(
+ pht(
+ "Attempting to access attached data on %s, but the data is not ".
+ "actually attached. Before accessing attachable data on an object, ".
+ "you must load and attach it.\n\n".
+ "Data is normally attached by calling the corresponding %s method on ".
+ "the Query class when the object is loaded. You can also call the ".
+ "corresponding %s method explicitly.",
+ get_class($object).$via,
+ 'needX()',
+ 'attachX()'));
}
}
diff --git a/src/infrastructure/storage/lisk/PhabricatorLiskDAO.php b/src/infrastructure/storage/lisk/PhabricatorLiskDAO.php
index f233fe25e..534c38b7d 100644
--- a/src/infrastructure/storage/lisk/PhabricatorLiskDAO.php
+++ b/src/infrastructure/storage/lisk/PhabricatorLiskDAO.php
@@ -1,244 +1,244 @@
<?php
/**
* @task config Configuring Storage
*/
abstract class PhabricatorLiskDAO extends LiskDAO {
private static $namespaceStack = array();
const ATTACHABLE = '<attachable>';
const CONFIG_APPLICATION_SERIALIZERS = 'phabricator/serializers';
/* -( Configuring Storage )------------------------------------------------ */
/**
* @task config
*/
public static function pushStorageNamespace($namespace) {
self::$namespaceStack[] = $namespace;
}
/**
* @task config
*/
public static function popStorageNamespace() {
array_pop(self::$namespaceStack);
}
/**
* @task config
*/
public static function getDefaultStorageNamespace() {
return PhabricatorEnv::getEnvConfig('storage.default-namespace');
}
/**
* @task config
*/
public static function getStorageNamespace() {
$namespace = end(self::$namespaceStack);
if (!strlen($namespace)) {
$namespace = self::getDefaultStorageNamespace();
}
if (!strlen($namespace)) {
- throw new Exception('No storage namespace configured!');
+ throw new Exception(pht('No storage namespace configured!'));
}
return $namespace;
}
/**
* @task config
*/
protected function establishLiveConnection($mode) {
$namespace = self::getStorageNamespace();
$conf = PhabricatorEnv::newObjectFromConfig(
'mysql.configuration-provider',
array($this, $mode, $namespace));
return PhabricatorEnv::newObjectFromConfig(
'mysql.implementation',
array(
array(
'user' => $conf->getUser(),
'pass' => $conf->getPassword(),
'host' => $conf->getHost(),
'port' => $conf->getPort(),
'database' => $conf->getDatabase(),
'retries' => 3,
),
));
}
/**
* @task config
*/
public function getTableName() {
$str = 'phabricator';
$len = strlen($str);
$class = strtolower(get_class($this));
if (!strncmp($class, $str, $len)) {
$class = substr($class, $len);
}
$app = $this->getApplicationName();
if (!strncmp($class, $app, strlen($app))) {
$class = substr($class, strlen($app));
}
if (strlen($class)) {
return $app.'_'.$class;
} else {
return $app;
}
}
/**
* @task config
*/
abstract public function getApplicationName();
protected function getConnectionNamespace() {
return self::getStorageNamespace().'_'.$this->getApplicationName();
}
/**
* Break a list of escaped SQL statement fragments (e.g., VALUES lists for
* INSERT, previously built with @{function:qsprintf}) into chunks which will
* fit under the MySQL 'max_allowed_packet' limit.
*
* Chunks are glued together with `$glue`, by default ", ".
*
* If a statement is too large to fit within the limit, it is broken into
* its own chunk (but might fail when the query executes).
*/
public static function chunkSQL(
array $fragments,
$glue = ', ',
$limit = null) {
if ($limit === null) {
// NOTE: Hard-code this at 1MB for now, minus a 10% safety buffer.
// Eventually we could query MySQL or let the user configure it.
$limit = (int)((1024 * 1024) * 0.90);
}
$result = array();
$chunk = array();
$len = 0;
$glue_len = strlen($glue);
foreach ($fragments as $fragment) {
$this_len = strlen($fragment);
if ($chunk) {
// Chunks after the first also imply glue.
$this_len += $glue_len;
}
if ($len + $this_len <= $limit) {
$len += $this_len;
$chunk[] = $fragment;
} else {
if ($chunk) {
$result[] = $chunk;
}
$len = strlen($fragment);
$chunk = array($fragment);
}
}
if ($chunk) {
$result[] = $chunk;
}
foreach ($result as $key => $fragment_list) {
$result[$key] = implode($glue, $fragment_list);
}
return $result;
}
protected function assertAttached($property) {
if ($property === self::ATTACHABLE) {
throw new PhabricatorDataNotAttachedException($this);
}
return $property;
}
protected function assertAttachedKey($value, $key) {
$this->assertAttached($value);
if (!array_key_exists($key, $value)) {
throw new PhabricatorDataNotAttachedException($this);
}
return $value[$key];
}
protected function detectEncodingForStorage($string) {
return phutil_is_utf8($string) ? 'utf8' : null;
}
protected function getUTF8StringFromStorage($string, $encoding) {
if ($encoding == 'utf8') {
return $string;
}
if (function_exists('mb_detect_encoding')) {
if (strlen($encoding)) {
$try_encodings = array(
$encoding,
);
} else {
// TODO: This is pretty much a guess, and probably needs to be
// configurable in the long run.
$try_encodings = array(
'JIS',
'EUC-JP',
'SJIS',
'ISO-8859-1',
);
}
$guess = mb_detect_encoding($string, $try_encodings);
if ($guess) {
return mb_convert_encoding($string, 'UTF-8', $guess);
}
}
return phutil_utf8ize($string);
}
protected function willReadData(array &$data) {
parent::willReadData($data);
static $custom;
if ($custom === null) {
$custom = $this->getConfigOption(self::CONFIG_APPLICATION_SERIALIZERS);
}
if ($custom) {
foreach ($custom as $key => $serializer) {
$data[$key] = $serializer->willReadValue($data[$key]);
}
}
}
protected function willWriteData(array &$data) {
static $custom;
if ($custom === null) {
$custom = $this->getConfigOption(self::CONFIG_APPLICATION_SERIALIZERS);
}
if ($custom) {
foreach ($custom as $key => $serializer) {
$data[$key] = $serializer->willWriteValue($data[$key]);
}
}
parent::willWriteData($data);
}
}
diff --git a/src/infrastructure/storage/lisk/__tests__/LiskFixtureTestCase.php b/src/infrastructure/storage/lisk/__tests__/LiskFixtureTestCase.php
index c4bd744ab..065c23e2f 100644
--- a/src/infrastructure/storage/lisk/__tests__/LiskFixtureTestCase.php
+++ b/src/infrastructure/storage/lisk/__tests__/LiskFixtureTestCase.php
@@ -1,166 +1,166 @@
<?php
final class LiskFixtureTestCase extends PhabricatorTestCase {
protected function getPhabricatorTestCaseConfiguration() {
return array(
self::PHABRICATOR_TESTCONFIG_BUILD_STORAGE_FIXTURES => true,
);
}
public function testTransactionalIsolation1of2() {
// NOTE: These tests are verifying that data is destroyed between tests.
// If the user from either test persists, the other test will fail.
$this->assertEqual(
0,
count(id(new HarbormasterScratchTable())->loadAll()));
id(new HarbormasterScratchTable())
->setData('alincoln')
->save();
}
public function testTransactionalIsolation2of2() {
$this->assertEqual(
0,
count(id(new HarbormasterScratchTable())->loadAll()));
id(new HarbormasterScratchTable())
->setData('ugrant')
->save();
}
public function testFixturesBasicallyWork() {
$this->assertEqual(
0,
count(id(new HarbormasterScratchTable())->loadAll()));
id(new HarbormasterScratchTable())
->setData('gwashington')
->save();
$this->assertEqual(
1,
count(id(new HarbormasterScratchTable())->loadAll()));
}
public function testReadableTransactions() {
// TODO: When we have semi-durable fixtures, use those instead. This is
// extremely hacky.
LiskDAO::endIsolateAllLiskEffectsToTransactions();
try {
$data = Filesystem::readRandomCharacters(32);
$obj = new HarbormasterScratchTable();
$obj->openTransaction();
$obj->setData($data);
$obj->save();
$loaded = id(new HarbormasterScratchTable())->loadOneWhere(
'data = %s',
$data);
$obj->killTransaction();
$this->assertTrue(
($loaded !== null),
- 'Reads inside transactions should have transaction visibility.');
+ pht('Reads inside transactions should have transaction visibility.'));
LiskDAO::beginIsolateAllLiskEffectsToTransactions();
} catch (Exception $ex) {
LiskDAO::beginIsolateAllLiskEffectsToTransactions();
throw $ex;
}
}
public function testGarbageLoadCalls() {
$obj = new HarbormasterObject();
$obj->save();
$id = $obj->getID();
$load = new HarbormasterObject();
$this->assertEqual(null, $load->load(0));
$this->assertEqual(null, $load->load(-1));
$this->assertEqual(null, $load->load(9999));
$this->assertEqual(null, $load->load(''));
$this->assertEqual(null, $load->load('cow'));
$this->assertEqual(null, $load->load($id.'cow'));
$this->assertTrue((bool)$load->load((int)$id));
$this->assertTrue((bool)$load->load((string)$id));
}
public function testCounters() {
$obj = new HarbormasterObject();
$conn_w = $obj->establishConnection('w');
// Test that the counter bascially behaves as expected.
$this->assertEqual(1, LiskDAO::loadNextCounterValue($conn_w, 'a'));
$this->assertEqual(2, LiskDAO::loadNextCounterValue($conn_w, 'a'));
$this->assertEqual(3, LiskDAO::loadNextCounterValue($conn_w, 'a'));
// This first insert is primarily a test that the previous LAST_INSERT_ID()
// value does not bleed into the creation of a new counter.
$this->assertEqual(1, LiskDAO::loadNextCounterValue($conn_w, 'b'));
$this->assertEqual(2, LiskDAO::loadNextCounterValue($conn_w, 'b'));
// Test alternate access/overwrite methods.
$this->assertEqual(3, LiskDAO::loadCurrentCounterValue($conn_w, 'a'));
LiskDAO::overwriteCounterValue($conn_w, 'a', 42);
$this->assertEqual(42, LiskDAO::loadCurrentCounterValue($conn_w, 'a'));
$this->assertEqual(43, LiskDAO::loadNextCounterValue($conn_w, 'a'));
// These inserts alternate database connections. Since unit tests are
// transactional by default, we need to break out of them or we'll deadlock
// since the transactions don't normally close until we exit the test.
LiskDAO::endIsolateAllLiskEffectsToTransactions();
try {
$conn_1 = $obj->establishConnection('w', $force_new = true);
$conn_2 = $obj->establishConnection('w', $force_new = true);
$this->assertEqual(1, LiskDAO::loadNextCounterValue($conn_1, 'z'));
$this->assertEqual(2, LiskDAO::loadNextCounterValue($conn_2, 'z'));
$this->assertEqual(3, LiskDAO::loadNextCounterValue($conn_1, 'z'));
$this->assertEqual(4, LiskDAO::loadNextCounterValue($conn_2, 'z'));
$this->assertEqual(5, LiskDAO::loadNextCounterValue($conn_1, 'z'));
LiskDAO::beginIsolateAllLiskEffectsToTransactions();
} catch (Exception $ex) {
LiskDAO::beginIsolateAllLiskEffectsToTransactions();
throw $ex;
}
}
public function testNonmutableColumns() {
$object = id(new HarbormasterScratchTable())
->setData('val1')
->setNonmutableData('val1')
->save();
$object->reload();
$this->assertEqual('val1', $object->getData());
$this->assertEqual('val1', $object->getNonmutableData());
$object
->setData('val2')
->setNonmutableData('val2')
->save();
$object->reload();
$this->assertEqual('val2', $object->getData());
// NOTE: This is the important test: the nonmutable column should not have
// been affected by the update.
$this->assertEqual('val1', $object->getNonmutableData());
}
}
diff --git a/src/infrastructure/storage/lisk/__tests__/LiskIsolationTestCase.php b/src/infrastructure/storage/lisk/__tests__/LiskIsolationTestCase.php
index 848dfe7b6..8ff506850 100644
--- a/src/infrastructure/storage/lisk/__tests__/LiskIsolationTestCase.php
+++ b/src/infrastructure/storage/lisk/__tests__/LiskIsolationTestCase.php
@@ -1,112 +1,114 @@
<?php
final class LiskIsolationTestCase extends PhabricatorTestCase {
public function testIsolatedWrites() {
$dao = new LiskIsolationTestDAO();
- $this->assertEqual(null, $dao->getID(), 'Expect no ID.');
- $this->assertEqual(null, $dao->getPHID(), 'Expect no PHID.');
+ $this->assertEqual(null, $dao->getID(), pht('Expect no ID.'));
+ $this->assertEqual(null, $dao->getPHID(), pht('Expect no PHID.'));
$dao->save(); // Effects insert
$id = $dao->getID();
$phid = $dao->getPHID();
- $this->assertTrue((bool)$id, 'Expect ID generated.');
- $this->assertTrue((bool)$phid, 'Expect PHID generated.');
+ $this->assertTrue((bool)$id, pht('Expect ID generated.'));
+ $this->assertTrue((bool)$phid, pht('Expect PHID generated.'));
$dao->save(); // Effects update
- $this->assertEqual($id, $dao->getID(), 'Expect ID unchanged.');
- $this->assertEqual($phid, $dao->getPHID(), 'Expect PHID unchanged.');
+ $this->assertEqual($id, $dao->getID(), pht('Expect ID unchanged.'));
+ $this->assertEqual($phid, $dao->getPHID(), pht('Expect PHID unchanged.'));
}
public function testEphemeral() {
$dao = new LiskIsolationTestDAO();
$dao->save();
$dao->makeEphemeral();
$this->tryTestCases(
array(
$dao,
),
array(
false,
),
array($this, 'saveDAO'));
}
public function saveDAO($dao) {
$dao->save();
}
public function testIsolationContainment() {
$dao = new LiskIsolationTestDAO();
try {
$method = new ReflectionMethod($dao, 'establishLiveConnection');
$method->setAccessible(true);
$method->invoke($dao, 'r');
$this->assertFailure(
- 'LiskIsolationTestDAO did not throw an exception when instructed to '.
- 'explicitly connect to an external database.');
+ pht(
+ '%s did not throw an exception when instructed to '.
+ 'explicitly connect to an external database.',
+ 'LiskIsolationTestDAO'));
} catch (LiskIsolationTestDAOException $ex) {
// Expected, pass.
}
$this->assertTrue(true);
}
public function testMagicMethods() {
$dao = new LiskIsolationTestDAO();
$this->assertEqual(
null,
$dao->getName(),
- 'getName() on empty object');
+ pht('%s on empty object', 'getName()'));
$this->assertEqual(
$dao,
$dao->setName('x'),
- 'setName() returns $this');
+ pht('%s returns %s', 'setName()', '$this'));
$this->assertEqual(
'y',
$dao->setName('y')->getName(),
- 'setName() has an effect');
+ pht('%s has an effect', 'setName()'));
$ex = null;
try {
$dao->gxxName();
} catch (Exception $thrown) {
$ex = $thrown;
}
$this->assertTrue(
(bool)$ex,
- 'Typoing "get" should throw.');
+ pht('Typoing "%s" should throw.', 'get'));
$ex = null;
try {
$dao->sxxName('z');
} catch (Exception $thrown) {
$ex = $thrown;
}
$this->assertTrue(
(bool)$ex,
- 'Typoing "set" should throw.');
+ pht('Typoing "%s" should throw.', 'set'));
$ex = null;
try {
$dao->madeUpMethod();
} catch (Exception $thrown) {
$ex = $thrown;
}
$this->assertTrue(
(bool)$ex,
- 'Made up method should throw.');
+ pht('Made up method should throw.'));
}
}
diff --git a/src/infrastructure/storage/lisk/__tests__/LiskIsolationTestDAO.php b/src/infrastructure/storage/lisk/__tests__/LiskIsolationTestDAO.php
index 7d5a5d602..d9070753f 100644
--- a/src/infrastructure/storage/lisk/__tests__/LiskIsolationTestDAO.php
+++ b/src/infrastructure/storage/lisk/__tests__/LiskIsolationTestDAO.php
@@ -1,32 +1,33 @@
<?php
final class LiskIsolationTestDAO extends LiskDAO {
protected $name;
protected $phid;
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID('TISO');
}
protected function establishLiveConnection($mode) {
throw new LiskIsolationTestDAOException(
- 'Isolation failure! DAO is attempting to connect to an external '.
- 'resource!');
+ pht(
+ 'Isolation failure! DAO is attempting to connect to an external '.
+ 'resource!'));
}
protected function getConnectionNamespace() {
return 'test';
}
public function getTableName() {
return 'test';
}
}
diff --git a/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementAdjustWorkflow.php b/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementAdjustWorkflow.php
index 8800bfb0e..e7437a30b 100644
--- a/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementAdjustWorkflow.php
+++ b/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementAdjustWorkflow.php
@@ -1,64 +1,66 @@
<?php
final class PhabricatorStorageManagementAdjustWorkflow
extends PhabricatorStorageManagementWorkflow {
protected function didConstruct() {
$this
->setName('adjust')
->setExamples('**adjust** [__options__]')
->setSynopsis(
pht(
'Make schemata adjustments to correct issues with characters sets, '.
'collations, and keys.'))
->setArguments(
array(
array(
'name' => 'unsafe',
'help' => pht(
'Permit adjustments which truncate data. This option may '.
'destroy some data, but the lost data is usually not '.
'important (most commonly, the ends of very long object '.
'titles).'),
),
));
}
public function execute(PhutilArgumentParser $args) {
$force = $args->getArg('force');
$unsafe = $args->getArg('unsafe');
$dry_run = $args->getArg('dryrun');
$this->requireAllPatchesApplied();
return $this->adjustSchemata($force, $unsafe, $dry_run);
}
private function requireAllPatchesApplied() {
$api = $this->getAPI();
$applied = $api->getAppliedPatches();
if ($applied === null) {
throw new PhutilArgumentUsageException(
pht(
'You have not initialized the database yet. You must initialize '.
- 'the database before you can adjust schemata. Run `storage upgrade` '.
- 'to initialize the database.'));
+ 'the database before you can adjust schemata. Run `%s` '.
+ 'to initialize the database.',
+ 'storage upgrade'));
}
$applied = array_fuse($applied);
$patches = $this->getPatches();
$patches = mpull($patches, null, 'getFullKey');
$missing = array_diff_key($patches, $applied);
if ($missing) {
throw new PhutilArgumentUsageException(
pht(
'You have not applied all available storage patches yet. You must '.
'apply all available patches before you can adjust schemata. '.
- 'Run `storage status` to show patch status, and `storage upgrade` '.
- 'to apply missing patches.'));
+ 'Run `%s` to show patch status, and `%s` to apply missing patches.',
+ 'storage status',
+ 'storage upgrade'));
}
}
}
diff --git a/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementDatabasesWorkflow.php b/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementDatabasesWorkflow.php
index 085ba1c87..5509a32d7 100644
--- a/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementDatabasesWorkflow.php
+++ b/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementDatabasesWorkflow.php
@@ -1,23 +1,23 @@
<?php
final class PhabricatorStorageManagementDatabasesWorkflow
extends PhabricatorStorageManagementWorkflow {
protected function didConstruct() {
$this
->setName('databases')
->setExamples('**databases** [__options__]')
- ->setSynopsis('List Phabricator databases.');
+ ->setSynopsis(pht('List Phabricator databases.'));
}
public function execute(PhutilArgumentParser $args) {
$api = $this->getAPI();
$patches = $this->getPatches();
$databases = $api->getDatabaseList($patches, $only_living = true);
echo implode("\n", $databases)."\n";
return 0;
}
}
diff --git a/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementDestroyWorkflow.php b/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementDestroyWorkflow.php
index 4666494e5..6c1c21635 100644
--- a/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementDestroyWorkflow.php
+++ b/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementDestroyWorkflow.php
@@ -1,81 +1,84 @@
<?php
final class PhabricatorStorageManagementDestroyWorkflow
extends PhabricatorStorageManagementWorkflow {
protected function didConstruct() {
$this
->setName('destroy')
->setExamples('**destroy** [__options__]')
- ->setSynopsis('Permanently destroy all storage and data.')
+ ->setSynopsis(pht('Permanently destroy all storage and data.'))
->setArguments(
array(
array(
'name' => 'unittest-fixtures',
- 'help' => 'Restrict **destroy** operations to databases created '.
- 'by PhabricatorTestCase test fixtures.',
+ 'help' => pht(
+ 'Restrict **destroy** operations to databases created '.
+ 'by %s test fixtures.',
+ 'PhabricatorTestCase'),
),
));
}
public function execute(PhutilArgumentParser $args) {
$is_dry = $args->getArg('dryrun');
$is_force = $args->getArg('force');
if (!$is_dry && !$is_force) {
echo phutil_console_wrap(
- 'Are you completely sure you really want to permanently destroy all '.
- 'storage for Phabricator data? This operation can not be undone and '.
- 'your data will not be recoverable if you proceed.');
+ pht(
+ 'Are you completely sure you really want to permanently destroy all '.
+ 'storage for Phabricator data? This operation can not be undone and '.
+ 'your data will not be recoverable if you proceed.'));
- if (!phutil_console_confirm('Permanently destroy all data?')) {
- echo "Cancelled.\n";
+ if (!phutil_console_confirm(pht('Permanently destroy all data?'))) {
+ echo pht('Cancelled.')."\n";
exit(1);
}
- if (!phutil_console_confirm('Really destroy all data forever?')) {
- echo "Cancelled.\n";
+ if (!phutil_console_confirm(pht('Really destroy all data forever?'))) {
+ echo pht('Cancelled.')."\n";
exit(1);
}
}
$api = $this->getAPI();
$patches = $this->getPatches();
if ($args->getArg('unittest-fixtures')) {
$conn = $api->getConn(null);
$databases = queryfx_all(
$conn,
'SELECT DISTINCT(TABLE_SCHEMA) AS db '.
'FROM INFORMATION_SCHEMA.TABLES '.
'WHERE TABLE_SCHEMA LIKE %>',
PhabricatorTestCase::NAMESPACE_PREFIX);
$databases = ipull($databases, 'db');
} else {
$databases = $api->getDatabaseList($patches);
$databases[] = $api->getDatabaseName('meta_data');
// These are legacy databases that were dropped long ago. See T2237.
$databases[] = $api->getDatabaseName('phid');
$databases[] = $api->getDatabaseName('directory');
}
foreach ($databases as $database) {
if ($is_dry) {
- echo "DRYRUN: Would drop database '{$database}'.\n";
+ echo pht("DRYRUN: Would drop database '%s'.", $database)."\n";
} else {
- echo "Dropping database '{$database}'...\n";
+ echo pht("Dropping database '%s'...", $database)."\n";
queryfx(
$api->getConn(null),
'DROP DATABASE IF EXISTS %T',
$database);
}
}
if (!$is_dry) {
- echo "Storage was destroyed.\n";
+ echo pht('Storage was destroyed.')."\n";
}
return 0;
}
}
diff --git a/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementDumpWorkflow.php b/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementDumpWorkflow.php
index 2c975bdec..e9a09bccd 100644
--- a/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementDumpWorkflow.php
+++ b/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementDumpWorkflow.php
@@ -1,56 +1,57 @@
<?php
final class PhabricatorStorageManagementDumpWorkflow
extends PhabricatorStorageManagementWorkflow {
protected function didConstruct() {
$this
->setName('dump')
->setExamples('**dump** [__options__]')
- ->setSynopsis('Dump all data in storage to stdout.');
+ ->setSynopsis(pht('Dump all data in storage to stdout.'));
}
public function execute(PhutilArgumentParser $args) {
$console = PhutilConsole::getConsole();
$api = $this->getAPI();
$patches = $this->getPatches();
$applied = $api->getAppliedPatches();
if ($applied === null) {
$namespace = $api->getNamespace();
$console->writeErr(
pht(
'**Storage Not Initialized**: There is no database storage '.
'initialized in this storage namespace ("%s"). Use '.
- '**storage upgrade** to initialize storage.',
- $namespace));
+ '**%s** to initialize storage.',
+ $namespace,
+ 'storage upgrade'));
return 1;
}
$databases = $api->getDatabaseList($patches, $only_living = true);
list($host, $port) = $this->getBareHostAndPort($api->getHost());
$flag_password = '';
$password = $api->getPassword();
if ($password) {
if (strlen($password->openEnvelope())) {
$flag_password = csprintf('-p%P', $password);
}
}
$flag_port = $port
? csprintf('--port %d', $port)
: '';
return phutil_passthru(
'mysqldump --hex-blob --single-transaction --default-character-set=utf8 '.
'-u %s %C -h %s %C --databases %Ls',
$api->getUser(),
$flag_password,
$host,
$flag_port,
$databases);
}
}
diff --git a/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementProbeWorkflow.php b/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementProbeWorkflow.php
index 00492f68a..a4ef562c3 100644
--- a/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementProbeWorkflow.php
+++ b/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementProbeWorkflow.php
@@ -1,78 +1,78 @@
<?php
final class PhabricatorStorageManagementProbeWorkflow
extends PhabricatorStorageManagementWorkflow {
protected function didConstruct() {
$this
->setName('probe')
->setExamples('**probe**')
- ->setSynopsis('Show approximate table sizes.');
+ ->setSynopsis(pht('Show approximate table sizes.'));
}
public function execute(PhutilArgumentParser $args) {
$console = PhutilConsole::getConsole();
$console->writeErr(
"%s\n",
pht('Analyzing table sizes (this may take a moment)...'));
$api = $this->getAPI();
$patches = $this->getPatches();
$databases = $api->getDatabaseList($patches, $only_living = true);
$conn_r = $api->getConn(null);
$data = array();
foreach ($databases as $database) {
queryfx($conn_r, 'USE %C', $database);
$tables = queryfx_all(
$conn_r,
'SHOW TABLE STATUS');
$tables = ipull($tables, null, 'Name');
$data[$database] = $tables;
}
$totals = array_fill_keys(array_keys($data), 0);
$overall = 0;
foreach ($data as $db => $tables) {
foreach ($tables as $table => $info) {
$table_size = $info['Data_length'] + $info['Index_length'];
$data[$db][$table]['_totalSize'] = $table_size;
$totals[$db] += $table_size;
$overall += $table_size;
}
}
$console->writeOut("%s\n", pht('APPROXIMATE TABLE SIZES'));
asort($totals);
foreach ($totals as $db => $size) {
$database_size = $this->formatSize($totals[$db], $overall);
$console->writeOut(
"**%s**\n",
sprintf('%-32.32s %18s', $db, $database_size));
$data[$db] = isort($data[$db], '_totalSize');
foreach ($data[$db] as $table => $info) {
$table_size = $this->formatSize($info['_totalSize'], $overall);
$console->writeOut(
"%s\n",
sprintf(' %-28.28s %18s', $table, $table_size));
}
}
$overall_size = $this->formatSize($overall, $overall);
$console->writeOut(
"**%s**\n",
sprintf('%-32.32s %18s', pht('TOTAL'), $overall_size));
return 0;
}
private function formatSize($n, $o) {
return sprintf(
'%8.8s MB %5.5s%%',
number_format($n / (1024 * 1024), 1),
sprintf('%3.1f', 100 * ($n / $o)));
}
}
diff --git a/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementQuickstartWorkflow.php b/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementQuickstartWorkflow.php
index b20787c05..561e01ced 100644
--- a/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementQuickstartWorkflow.php
+++ b/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementQuickstartWorkflow.php
@@ -1,162 +1,165 @@
<?php
final class PhabricatorStorageManagementQuickstartWorkflow
extends PhabricatorStorageManagementWorkflow {
protected function didConstruct() {
$this
->setName('quickstart')
->setExamples('**quickstart** [__options__]')
->setSynopsis(
pht(
'Generate a new quickstart database dump. This command is mostly '.
'useful when developing Phabricator.'))
->setArguments(
array(
array(
'name' => 'output',
'param' => 'file',
'help' => pht('Specify output file to write.'),
),
));
}
public function execute(PhutilArgumentParser $args) {
$output = $args->getArg('output');
if (!$output) {
throw new PhutilArgumentUsageException(
pht(
- 'Specify a file to write with `--output`.'));
+ 'Specify a file to write with `%s`.',
+ '--output'));
}
$namespace = 'phabricator_quickstart_'.Filesystem::readRandomCharacters(8);
$bin = dirname(phutil_get_library_root('phabricator')).'/bin/storage';
if (!$this->getAPI()->isCharacterSetAvailable('utf8mb4')) {
throw new PhutilArgumentUsageException(
pht(
'You can only generate a new quickstart file if MySQL supports '.
'the utf8mb4 character set (available in MySQL 5.5 and newer). The '.
'configured server does not support utf8mb4.'));
}
$err = phutil_passthru(
'%s upgrade --force --no-quickstart --namespace %s',
$bin,
$namespace);
if ($err) {
return $err;
}
$err = phutil_passthru(
'%s adjust --force --namespace %s',
$bin,
$namespace);
if ($err) {
return $err;
}
$tmp = new TempFile();
$err = phutil_passthru(
'%s dump --namespace %s > %s',
$bin,
$namespace,
$tmp);
if ($err) {
return $err;
}
$err = phutil_passthru(
'%s destroy --force --namespace %s',
$bin,
$namespace);
if ($err) {
return $err;
}
$dump = Filesystem::readFile($tmp);
$dump = str_replace(
$namespace,
'{$NAMESPACE}',
$dump);
// NOTE: This is a hack. We can not use `binary` for these columns, because
// they are a part of a fulltext index. This regex is avoiding matching a
// possible NOT NULL at the end of the line.
$old = $dump;
$dump = preg_replace(
'/`corpus` longtext CHARACTER SET .*? COLLATE [^\s,]+/mi',
'`corpus` longtext CHARACTER SET {$CHARSET_FULLTEXT} '.
'COLLATE {$COLLATE_FULLTEXT}',
$dump);
if ($dump == $old) {
// If we didn't make any changes, yell about it. We'll produce an invalid
// dump otherwise.
throw new PhutilArgumentUsageException(
- pht('Failed to apply hack to adjust FULLTEXT search column!'));
+ pht(
+ 'Failed to apply hack to adjust %s search column!',
+ 'FULLTEXT'));
}
$dump = str_replace(
'utf8mb4_bin',
'{$COLLATE_TEXT}',
$dump);
$dump = str_replace(
'utf8mb4_unicode_ci',
'{$COLLATE_SORT}',
$dump);
$dump = str_replace(
'utf8mb4',
'{$CHARSET}',
$dump);
$old = $dump;
$dump = preg_replace(
'/CHARACTER SET {\$CHARSET} COLLATE {\$COLLATE_SORT}/mi',
'CHARACTER SET {$CHARSET_SORT} COLLATE {$COLLATE_SORT}',
$dump);
if ($dump == $old) {
throw new PhutilArgumentUsageException(
pht('Failed to adjust SORT columns!'));
}
// Strip out a bunch of unnecessary commands which make the dump harder
// to handle and slower to import.
// Remove character set adjustments and key disables.
$dump = preg_replace(
'(^/\*.*\*/;$)m',
'',
$dump);
// Remove comments.
$dump = preg_replace('/^--.*$/m', '', $dump);
// Remove table drops, locks, and unlocks. These are never relevant when
// performing q quickstart.
$dump = preg_replace(
'/^(DROP TABLE|LOCK TABLES|UNLOCK TABLES).*$/m',
'',
$dump);
// Collapse adjacent newlines.
$dump = preg_replace('/\n\s*\n/', "\n", $dump);
$dump = str_replace(';', ";\n", $dump);
$dump = trim($dump)."\n";
Filesystem::writeFile($output, $dump);
$console = PhutilConsole::getConsole();
$console->writeOut(
"**<bg:green> %s </bg>** %s\n",
pht('SUCCESS'),
pht('Wrote fresh quickstart SQL.'));
return 0;
}
}
diff --git a/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementShellWorkflow.php b/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementShellWorkflow.php
index ffbb01868..58a7c072a 100644
--- a/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementShellWorkflow.php
+++ b/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementShellWorkflow.php
@@ -1,38 +1,38 @@
<?php
final class PhabricatorStorageManagementShellWorkflow
extends PhabricatorStorageManagementWorkflow {
protected function didConstruct() {
$this
->setName('shell')
->setExamples('**shell** [__options__]')
- ->setSynopsis('Launch an interactive shell.');
+ ->setSynopsis(pht('Launch an interactive shell.'));
}
public function execute(PhutilArgumentParser $args) {
$api = $this->getAPI();
list($host, $port) = $this->getBareHostAndPort($api->getHost());
$flag_port = $port
? csprintf('--port %d', $port)
: '';
$flag_password = '';
$password = $api->getPassword();
if ($password) {
if (strlen($password->openEnvelope())) {
$flag_password = csprintf('--password=%P', $password);
}
}
return phutil_passthru(
'mysql --default-character-set=utf8 '.
'-u %s %C -h %s %C',
$api->getUser(),
$flag_password,
$host,
$flag_port);
}
}
diff --git a/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementStatusWorkflow.php b/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementStatusWorkflow.php
index 8ab341996..ebff080bd 100644
--- a/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementStatusWorkflow.php
+++ b/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementStatusWorkflow.php
@@ -1,49 +1,50 @@
<?php
final class PhabricatorStorageManagementStatusWorkflow
extends PhabricatorStorageManagementWorkflow {
protected function didConstruct() {
$this
->setName('status')
->setExamples('**status** [__options__]')
- ->setSynopsis('Show patch application status.');
+ ->setSynopsis(pht('Show patch application status.'));
}
public function execute(PhutilArgumentParser $args) {
$api = $this->getAPI();
$patches = $this->getPatches();
$applied = $api->getAppliedPatches();
if ($applied === null) {
echo phutil_console_format(
- "**Database Not Initialized**: Run **storage upgrade** to ".
- "initialize.\n");
+ "**%s**: %s\n",
+ pht('Database Not Initialized'),
+ pht('Run **%s** to initialize.', 'storage upgrade'));
return 1;
}
$table = id(new PhutilConsoleTable())
->setShowHeader(false)
- ->addColumn('id', array('title' => 'ID'))
- ->addColumn('status', array('title' => 'Status'))
- ->addColumn('type', array('title' => 'Type'))
- ->addColumn('name', array('title' => 'Name'));
+ ->addColumn('id', array('title' => pht('ID')))
+ ->addColumn('status', array('title' => pht('Status')))
+ ->addColumn('type', array('title' => pht('Type')))
+ ->addColumn('name', array('title' => pht('Name')));
foreach ($patches as $patch) {
$table->addRow(array(
'id' => $patch->getFullKey(),
'status' => in_array($patch->getFullKey(), $applied)
- ? 'Applied'
- : 'Not Applied',
+ ? pht('Applied')
+ : pht('Not Applied'),
'type' => $patch->getType(),
'name' => $patch->getName(),
));
}
$table->draw();
return 0;
}
}
diff --git a/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementUpgradeWorkflow.php b/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementUpgradeWorkflow.php
index 3ce089131..f6cf6ad47 100644
--- a/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementUpgradeWorkflow.php
+++ b/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementUpgradeWorkflow.php
@@ -1,210 +1,225 @@
<?php
final class PhabricatorStorageManagementUpgradeWorkflow
extends PhabricatorStorageManagementWorkflow {
protected function didConstruct() {
$this
->setName('upgrade')
->setExamples('**upgrade** [__options__]')
- ->setSynopsis('Upgrade database schemata.')
+ ->setSynopsis(pht('Upgrade database schemata.'))
->setArguments(
array(
array(
'name' => 'apply',
'param' => 'patch',
'help' => pht(
'Apply __patch__ explicitly. This is an advanced feature for '.
'development and debugging; you should not normally use this '.
'flag. This skips adjustment.'),
),
array(
'name' => 'no-quickstart',
'help' => pht(
'Build storage patch-by-patch from scatch, even if it could '.
'be loaded from the quickstart template.'),
),
array(
'name' => 'init-only',
'help' => pht(
'Initialize storage only; do not apply patches or adjustments.'),
),
array(
'name' => 'no-adjust',
'help' => pht(
'Do not apply storage adjustments after storage upgrades.'),
),
));
}
public function execute(PhutilArgumentParser $args) {
$is_dry = $args->getArg('dryrun');
$is_force = $args->getArg('force');
$api = $this->getAPI();
$patches = $this->getPatches();
if (!$is_dry && !$is_force) {
echo phutil_console_wrap(
- 'Before running storage upgrades, you should take down the '.
- 'Phabricator web interface and stop any running Phabricator '.
- 'daemons (you can disable this warning with --force).');
-
- if (!phutil_console_confirm('Are you ready to continue?')) {
- echo "Cancelled.\n";
+ pht(
+ 'Before running storage upgrades, you should take down the '.
+ 'Phabricator web interface and stop any running Phabricator '.
+ 'daemons (you can disable this warning with %s).',
+ '--force'));
+
+ if (!phutil_console_confirm(pht('Are you ready to continue?'))) {
+ echo pht('Cancelled.')."\n";
return 1;
}
}
$apply_only = $args->getArg('apply');
if ($apply_only) {
if (empty($patches[$apply_only])) {
throw new PhutilArgumentUsageException(
- "--apply argument '{$apply_only}' is not a valid patch. Use ".
- "'storage status' to show patch status.");
+ pht(
+ "%s argument '%s' is not a valid patch. ".
+ "Use '%s' to show patch status.",
+ '--apply',
+ $apply_only,
+ 'storage status'));
}
}
$no_quickstart = $args->getArg('no-quickstart');
$init_only = $args->getArg('init-only');
$no_adjust = $args->getArg('no-adjust');
$applied = $api->getAppliedPatches();
if ($applied === null) {
if ($is_dry) {
- echo "DRYRUN: Patch metadata storage doesn't exist yet, it would ".
- "be created.\n";
+ echo pht(
+ "DRYRUN: Patch metadata storage doesn't exist yet, ".
+ "it would be created.\n");
return 0;
}
if ($apply_only) {
throw new PhutilArgumentUsageException(
- 'Storage has not been initialized yet, you must initialize storage '.
- 'before selectively applying patches.');
+ pht(
+ 'Storage has not been initialized yet, you must initialize '.
+ 'storage before selectively applying patches.'));
return 1;
}
$legacy = $api->getLegacyPatches($patches);
if ($legacy || $no_quickstart || $init_only) {
// If we have legacy patches, we can't quickstart.
$api->createDatabase('meta_data');
$api->createTable(
'meta_data',
'patch_status',
array(
'patch VARCHAR(255) NOT NULL PRIMARY KEY COLLATE utf8_general_ci',
'applied INT UNSIGNED NOT NULL',
));
foreach ($legacy as $patch) {
$api->markPatchApplied($patch);
}
} else {
- echo "Loading quickstart template...\n";
+ echo pht('Loading quickstart template...')."\n";
$root = dirname(phutil_get_library_root('phabricator'));
$sql = $root.'/resources/sql/quickstart.sql';
$api->applyPatchSQL($sql);
}
}
if ($init_only) {
- echo "Storage initialized.\n";
+ echo pht('Storage initialized.')."\n";
return 0;
}
$applied = $api->getAppliedPatches();
$applied = array_fuse($applied);
$skip_mark = false;
if ($apply_only) {
if (isset($applied[$apply_only])) {
unset($applied[$apply_only]);
$skip_mark = true;
if (!$is_force && !$is_dry) {
echo phutil_console_wrap(
- "Patch '{$apply_only}' has already been applied. Are you sure ".
- "you want to apply it again? This may put your storage in a state ".
- "that the upgrade scripts can not automatically manage.");
- if (!phutil_console_confirm('Apply patch again?')) {
- echo "Cancelled.\n";
+ pht(
+ "Patch '%s' has already been applied. Are you sure you want ".
+ "to apply it again? This may put your storage in a state ".
+ "that the upgrade scripts can not automatically manage.",
+ $apply_only));
+ if (!phutil_console_confirm(pht('Apply patch again?'))) {
+ echo pht('Cancelled.')."\n";
return 1;
}
}
}
}
while (true) {
$applied_something = false;
foreach ($patches as $key => $patch) {
if (isset($applied[$key])) {
unset($patches[$key]);
continue;
}
if ($apply_only && $apply_only != $key) {
unset($patches[$key]);
continue;
}
$can_apply = true;
foreach ($patch->getAfter() as $after) {
if (empty($applied[$after])) {
if ($apply_only) {
- echo "Unable to apply patch '{$apply_only}' because it depends ".
- "on patch '{$after}', which has not been applied.\n";
+ echo pht(
+ "Unable to apply patch '%s' because it depends ".
+ "on patch '%s', which has not been applied.\n",
+ $apply_only,
+ $after);
return 1;
}
$can_apply = false;
break;
}
}
if (!$can_apply) {
continue;
}
$applied_something = true;
if ($is_dry) {
- echo "DRYRUN: Would apply patch '{$key}'.\n";
+ echo pht("DRYRUN: Would apply patch '%s'.", $key)."\n";
} else {
- echo "Applying patch '{$key}'...\n";
+ echo pht("Applying patch '%s'...", $key)."\n";
$api->applyPatch($patch);
if (!$skip_mark) {
$api->markPatchApplied($key);
}
}
unset($patches[$key]);
$applied[$key] = true;
}
if (!$applied_something) {
if (count($patches)) {
throw new Exception(
'Some patches could not be applied: '.
implode(', ', array_keys($patches)));
} else if (!$is_dry && !$apply_only) {
- echo "Storage is up to date. Use 'storage status' for details.\n";
+ echo pht(
+ "Storage is up to date. Use '%s' for details.\n",
+ 'storage status');
}
break;
}
}
$console = PhutilConsole::getConsole();
if ($no_adjust || $init_only || $apply_only) {
$console->writeOut(
"%s\n",
pht('Declining to apply storage adjustments.'));
return 0;
} else {
return $this->adjustSchemata($is_force, $unsafe = false, $is_dry);
}
}
}
diff --git a/src/infrastructure/storage/patch/PhabricatorSQLPatchList.php b/src/infrastructure/storage/patch/PhabricatorSQLPatchList.php
index 390d591a0..2ab9d528c 100644
--- a/src/infrastructure/storage/patch/PhabricatorSQLPatchList.php
+++ b/src/infrastructure/storage/patch/PhabricatorSQLPatchList.php
@@ -1,181 +1,217 @@
<?php
abstract class PhabricatorSQLPatchList {
public abstract function getNamespace();
public abstract function getPatches();
/**
* Examine a directory for `.php` and `.sql` files and build patch
* specifications for them.
*/
protected function buildPatchesFromDirectory($directory) {
$patch_list = Filesystem::listDirectory(
$directory,
$include_hidden = false);
sort($patch_list);
$patches = array();
foreach ($patch_list as $patch) {
$matches = null;
if (!preg_match('/\.(sql|php)$/', $patch, $matches)) {
throw new Exception(
pht(
'Unknown patch "%s" in "%s", expected ".php" or ".sql" suffix.',
$patch,
$directory));
}
$patches[$patch] = array(
'type' => $matches[1],
'name' => rtrim($directory, '/').'/'.$patch,
);
}
return $patches;
}
final public static function buildAllPatches() {
$patch_lists = id(new PhutilSymbolLoader())
->setAncestorClass(__CLASS__)
->setConcreteOnly(true)
->selectAndLoadSymbols();
$specs = array();
$seen_namespaces = array();
foreach ($patch_lists as $patch_class) {
$patch_class = $patch_class['name'];
$patch_list = newv($patch_class, array());
$namespace = $patch_list->getNamespace();
if (isset($seen_namespaces[$namespace])) {
$prior = $seen_namespaces[$namespace];
throw new Exception(
- "PatchList '{$patch_class}' has the same namespace, '{$namespace}', ".
- "as another patch list class, '{$prior}'. Each patch list MUST have ".
- "a unique namespace.");
+ pht(
+ "%s '%s' has the same namespace, '%s', as another patch list ".
+ "class, '%s'. Each patch list MUST have a unique namespace.",
+ __CLASS__,
+ $patch_class,
+ $namespace,
+ $prior));
}
$last_key = null;
foreach ($patch_list->getPatches() as $key => $patch) {
if (!is_array($patch)) {
throw new Exception(
- "PatchList '{$patch_class}' has a patch '{$key}' which is not ".
- "an array.");
+ pht(
+ "%s '%s' has a patch '%s' which is not an array.",
+ __CLASS__,
+ $patch_class,
+ $key));
}
$valid = array(
'type' => true,
'name' => true,
'after' => true,
'legacy' => true,
'dead' => true,
);
foreach ($patch as $pkey => $pval) {
if (empty($valid[$pkey])) {
throw new Exception(
- "PatchList '{$patch_class}' has a patch, '{$key}', with an ".
- "unknown property, '{$pkey}'. Patches must have only valid ".
- "keys: ".implode(', ', array_keys($valid)).'.');
+ pht(
+ "%s '%s' has a patch, '%s', with an unknown property, '%s'.".
+ "Patches must have only valid keys: %s.",
+ __CLASS__,
+ $patch_class,
+ $key,
+ $pkey,
+ implode(', ', array_keys($valid))));
}
}
if (is_numeric($key)) {
throw new Exception(
- "PatchList '{$patch_class}' has a patch with a numeric key, ".
- "'{$key}'. Patches must use string keys.");
+ pht(
+ "%s '%s' has a patch with a numeric key, '%s'. ".
+ "Patches must use string keys.",
+ __CLASS__,
+ $patch_class,
+ $key));
}
if (strpos($key, ':') !== false) {
throw new Exception(
- "PatchList '{$patch_class}' has a patch with a colon in the ".
- "key name, '{$key}'. Patch keys may not contain colons.");
+ pht(
+ "%s '%s' has a patch with a colon in the key name, '%s'. ".
+ "Patch keys may not contain colons.",
+ __CLASS__,
+ $patch_class,
+ $key));
}
$full_key = "{$namespace}:{$key}";
if (isset($specs[$full_key])) {
throw new Exception(
- "PatchList '{$patch_class}' has a patch '{$key}' which ".
- "duplicates an existing patch key.");
+ pht(
+ "%s '%s' has a patch '%s' which duplicates an ".
+ "existing patch key.",
+ __CLASS__,
+ $patch_class,
+ $key));
}
$patch['key'] = $key;
$patch['fullKey'] = $full_key;
$patch['dead'] = (bool)idx($patch, 'dead', false);
if (isset($patch['legacy'])) {
if ($namespace != 'phabricator') {
throw new Exception(
- "Only patches in the 'phabricator' namespace may contain ".
- "'legacy' keys.");
+ pht(
+ "Only patches in the '%s' namespace may contain '%s' keys.",
+ 'phabricator',
+ 'legacy'));
}
} else {
$patch['legacy'] = false;
}
if (!array_key_exists('after', $patch)) {
if ($last_key === null) {
throw new Exception(
- "Patch '{$full_key}' is missing key 'after', and is the first ".
- "patch in the patch list '{$patch_class}', so its application ".
- "order can not be determined implicitly. The first patch in a ".
- "patch list must list the patch or patches it depends on ".
- "explicitly.");
+ pht(
+ "Patch '%s' is missing key 'after', and is the first patch ".
+ "in the patch list '%s', so its application order can not be ".
+ "determined implicitly. The first patch in a patch list must ".
+ "list the patch or patches it depends on explicitly.",
+ $full_key,
+ $patch_class));
} else {
$patch['after'] = array($last_key);
}
}
$last_key = $full_key;
foreach ($patch['after'] as $after_key => $after) {
if (strpos($after, ':') === false) {
$patch['after'][$after_key] = $namespace.':'.$after;
}
}
$type = idx($patch, 'type');
if (!$type) {
throw new Exception(
- "Patch '{$namespace}:{$key}' is missing key 'type'. Every patch ".
- "must have a type.");
+ pht(
+ "Patch '%s' is missing key '%s'. Every patch must have a type.",
+ "{$namespace}:{$key}",
+ 'type'));
}
switch ($type) {
case 'db':
case 'sql':
case 'php':
break;
default:
throw new Exception(
- "Patch '{$namespace}:{$key}' has unknown patch type '{$type}'.");
+ pht(
+ "Patch '%s' has unknown patch type '%s'.",
+ "{$namespace}:{$key}",
+ $type));
}
$specs[$full_key] = $patch;
}
}
foreach ($specs as $key => $patch) {
foreach ($patch['after'] as $after) {
if (empty($specs[$after])) {
throw new Exception(
- "Patch '{$key}' references nonexistent dependency, '{$after}'. ".
- "Patches may only depend on patches which actually exist.");
+ pht(
+ "Patch '%s' references nonexistent dependency, '%s'. ".
+ "Patches may only depend on patches which actually exist.",
+ $key,
+ $after));
}
}
}
$patches = array();
foreach ($specs as $full_key => $spec) {
$patches[$full_key] = new PhabricatorStoragePatch($spec);
}
// TODO: Detect cycles?
return $patches;
}
}
diff --git a/src/infrastructure/testing/PhabricatorTestCase.php b/src/infrastructure/testing/PhabricatorTestCase.php
index 2545413de..ad88ca360 100644
--- a/src/infrastructure/testing/PhabricatorTestCase.php
+++ b/src/infrastructure/testing/PhabricatorTestCase.php
@@ -1,224 +1,227 @@
<?php
abstract class PhabricatorTestCase extends PhutilTestCase {
const NAMESPACE_PREFIX = 'phabricator_unittest_';
/**
* If true, put Lisk in process-isolated mode for the duration of the tests so
* that it will establish only isolated, side-effect-free database
* connections. Defaults to true.
*
* NOTE: You should disable this only in rare circumstances. Unit tests should
* not rely on external resources like databases, and should not produce
* side effects.
*/
const PHABRICATOR_TESTCONFIG_ISOLATE_LISK = 'isolate-lisk';
/**
* If true, build storage fixtures before running tests, and connect to them
* during test execution. This will impose a performance penalty on test
* execution (currently, it takes roughly one second to build the fixture)
* but allows you to perform tests which require data to be read from storage
* after writes. The fixture is shared across all test cases in this process.
* Defaults to false.
*
* NOTE: All connections to fixture storage open transactions when established
* and roll them back when tests complete. Each test must independently
* write data it relies on; data will not persist across tests.
*
* NOTE: Enabling this implies disabling process isolation.
*/
const PHABRICATOR_TESTCONFIG_BUILD_STORAGE_FIXTURES = 'storage-fixtures';
private $configuration;
private $env;
private static $storageFixtureReferences = 0;
private static $storageFixture;
private static $storageFixtureObjectSeed = 0;
private static $testsAreRunning = 0;
protected function getPhabricatorTestCaseConfiguration() {
return array();
}
private function getComputedConfiguration() {
$config = $this->getPhabricatorTestCaseConfiguration() + array(
self::PHABRICATOR_TESTCONFIG_ISOLATE_LISK => true,
self::PHABRICATOR_TESTCONFIG_BUILD_STORAGE_FIXTURES => false,
);
if ($config[self::PHABRICATOR_TESTCONFIG_BUILD_STORAGE_FIXTURES]) {
// Fixtures don't make sense with process isolation.
$config[self::PHABRICATOR_TESTCONFIG_ISOLATE_LISK] = false;
}
return $config;
}
public function willRunTestCases(array $test_cases) {
$root = dirname(phutil_get_library_root('phabricator'));
require_once $root.'/scripts/__init_script__.php';
$config = $this->getComputedConfiguration();
if ($config[self::PHABRICATOR_TESTCONFIG_BUILD_STORAGE_FIXTURES]) {
++self::$storageFixtureReferences;
if (!self::$storageFixture) {
self::$storageFixture = $this->newStorageFixture();
}
}
++self::$testsAreRunning;
}
public function didRunTestCases(array $test_cases) {
if (self::$storageFixture) {
self::$storageFixtureReferences--;
if (!self::$storageFixtureReferences) {
self::$storageFixture = null;
}
}
--self::$testsAreRunning;
}
protected function willRunTests() {
$config = $this->getComputedConfiguration();
if ($config[self::PHABRICATOR_TESTCONFIG_ISOLATE_LISK]) {
LiskDAO::beginIsolateAllLiskEffectsToCurrentProcess();
}
$this->env = PhabricatorEnv::beginScopedEnv();
// NOTE: While running unit tests, we act as though all applications are
// installed, regardless of the install's configuration. Tests which need
// to uninstall applications are responsible for adjusting state themselves
// (such tests are exceedingly rare).
$this->env->overrideEnvConfig(
'phabricator.uninstalled-applications',
array());
$this->env->overrideEnvConfig(
'phabricator.show-prototypes',
true);
// Reset application settings to defaults, particularly policies.
$this->env->overrideEnvConfig(
'phabricator.application-settings',
array());
// We can't stub this service right now, and it's not generally useful
// to publish notifications about test execution.
$this->env->overrideEnvConfig(
'notification.enabled',
false);
$this->env->overrideEnvConfig(
'phabricator.base-uri',
'http://phabricator.example.com');
// Tests do their own stubbing/voiding for events.
$this->env->overrideEnvConfig('phabricator.silent', false);
}
protected function didRunTests() {
$config = $this->getComputedConfiguration();
if ($config[self::PHABRICATOR_TESTCONFIG_ISOLATE_LISK]) {
LiskDAO::endIsolateAllLiskEffectsToCurrentProcess();
}
try {
if (phutil_is_hiphop_runtime()) {
$this->env->__destruct();
}
unset($this->env);
} catch (Exception $ex) {
throw new Exception(
- 'Some test called PhabricatorEnv::beginScopedEnv(), but is still '.
- 'holding a reference to the scoped environment!');
+ pht(
+ 'Some test called %s, but is still holding '.
+ 'a reference to the scoped environment!',
+ 'PhabricatorEnv::beginScopedEnv()'));
}
}
protected function willRunOneTest($test) {
$config = $this->getComputedConfiguration();
if ($config[self::PHABRICATOR_TESTCONFIG_BUILD_STORAGE_FIXTURES]) {
LiskDAO::beginIsolateAllLiskEffectsToTransactions();
}
}
protected function didRunOneTest($test) {
$config = $this->getComputedConfiguration();
if ($config[self::PHABRICATOR_TESTCONFIG_BUILD_STORAGE_FIXTURES]) {
LiskDAO::endIsolateAllLiskEffectsToTransactions();
}
}
protected function newStorageFixture() {
$bytes = Filesystem::readRandomCharacters(24);
$name = self::NAMESPACE_PREFIX.$bytes;
return new PhabricatorStorageFixtureScopeGuard($name);
}
/**
* Returns an integer seed to use when building unique identifiers (e.g.,
* non-colliding usernames). The seed is unstable and its value will change
* between test runs, so your tests must not rely on it.
*
* @return int A unique integer.
*/
protected function getNextObjectSeed() {
self::$storageFixtureObjectSeed += mt_rand(1, 100);
return self::$storageFixtureObjectSeed;
}
protected function generateNewTestUser() {
$seed = $this->getNextObjectSeed();
$user = id(new PhabricatorUser())
->setRealName("Test User {$seed}}")
->setUserName("test{$seed}")
->setIsApproved(1);
$email = id(new PhabricatorUserEmail())
->setAddress("testuser{$seed}@example.com")
->setIsVerified(1);
$editor = new PhabricatorUserEditor();
$editor->setActor($user);
$editor->createNewUser($user, $email);
return $user;
}
/**
* Throws unless tests are currently executing. This method can be used to
* guard code which is specific to unit tests and should not normally be
* reachable.
*
* If tests aren't currently being executed, throws an exception.
*/
public static function assertExecutingUnitTests() {
if (!self::$testsAreRunning) {
throw new Exception(
- 'Executing test code outside of test execution! This code path can '.
- 'only be run during unit tests.');
+ pht(
+ 'Executing test code outside of test execution! This code path can '.
+ 'only be run during unit tests.'));
}
}
protected function requireBinaryForTest($binary) {
if (!Filesystem::binaryExists($binary)) {
$this->assertSkipped(
pht('No binary "%s" found on this system, skipping test.', $binary));
}
}
}
diff --git a/src/infrastructure/time/PhabricatorTime.php b/src/infrastructure/time/PhabricatorTime.php
index 03495d841..e31222cf7 100644
--- a/src/infrastructure/time/PhabricatorTime.php
+++ b/src/infrastructure/time/PhabricatorTime.php
@@ -1,75 +1,75 @@
<?php
final class PhabricatorTime {
private static $stack = array();
private static $originalZone;
public static function pushTime($epoch, $timezone) {
if (empty(self::$stack)) {
self::$originalZone = date_default_timezone_get();
}
$ok = date_default_timezone_set($timezone);
if (!$ok) {
- throw new Exception("Invalid timezone '{$timezone}'!");
+ throw new Exception(pht("Invalid timezone '%s'!", $timezone));
}
self::$stack[] = array(
'epoch' => $epoch,
'timezone' => $timezone,
);
return new PhabricatorTimeGuard(last_key(self::$stack));
}
public static function popTime($key) {
if ($key !== last_key(self::$stack)) {
throw new Exception(
pht(
'%s with bad key.',
__METHOD__));
}
array_pop(self::$stack);
if (empty(self::$stack)) {
date_default_timezone_set(self::$originalZone);
} else {
$frame = end(self::$stack);
date_default_timezone_set($frame['timezone']);
}
}
public static function getNow() {
if (self::$stack) {
$frame = end(self::$stack);
return $frame['epoch'];
}
return time();
}
public static function parseLocalTime($time, PhabricatorUser $user) {
$old_zone = date_default_timezone_get();
date_default_timezone_set($user->getTimezoneIdentifier());
$timestamp = (int)strtotime($time, self::getNow());
if ($timestamp <= 0) {
$timestamp = null;
}
date_default_timezone_set($old_zone);
return $timestamp;
}
public static function getTodayMidnightDateTime($viewer) {
$timezone = new DateTimeZone($viewer->getTimezoneIdentifier());
$today = new DateTime('@'.time());
$today->setTimeZone($timezone);
$year = $today->format('Y');
$month = $today->format('m');
$day = $today->format('d');
$today = new DateTime("{$year}-{$month}-{$day}", $timezone);
return $today;
}
}
diff --git a/src/infrastructure/util/PhabricatorHash.php b/src/infrastructure/util/PhabricatorHash.php
index df1fbfa08..00d7cca63 100644
--- a/src/infrastructure/util/PhabricatorHash.php
+++ b/src/infrastructure/util/PhabricatorHash.php
@@ -1,124 +1,127 @@
<?php
final class PhabricatorHash extends Phobject {
const INDEX_DIGEST_LENGTH = 12;
/**
* Digest a string for general use, including use which relates to security.
*
* @param string Input string.
* @return string 32-byte hexidecimal SHA1+HMAC hash.
*/
public static function digest($string, $key = null) {
if ($key === null) {
$key = PhabricatorEnv::getEnvConfig('security.hmac-key');
}
if (!$key) {
throw new Exception(
- "Set a 'security.hmac-key' in your Phabricator configuration!");
+ pht(
+ "Set a '%s' in your Phabricator configuration!",
+ 'security.hmac-key'));
}
return hash_hmac('sha1', $string, $key);
}
/**
* Digest a string into a password hash. This is similar to @{method:digest},
* but requires a salt and iterates the hash to increase cost.
*/
public static function digestPassword(PhutilOpaqueEnvelope $envelope, $salt) {
$result = $envelope->openEnvelope();
if (!$result) {
- throw new Exception('Trying to digest empty password!');
+ throw new Exception(pht('Trying to digest empty password!'));
}
for ($ii = 0; $ii < 1000; $ii++) {
$result = self::digest($result, $salt);
}
return $result;
}
/**
* Digest a string for use in, e.g., a MySQL index. This produces a short
* (12-byte), case-sensitive alphanumeric string with 72 bits of entropy,
* which is generally safe in most contexts (notably, URLs).
*
* This method emphasizes compactness, and should not be used for security
* related hashing (for general purpose hashing, see @{method:digest}).
*
* @param string Input string.
* @return string 12-byte, case-sensitive alphanumeric hash of the string
* which
*/
public static function digestForIndex($string) {
$hash = sha1($string, $raw_output = true);
static $map;
if ($map === null) {
$map = '0123456789'.
'abcdefghij'.
'klmnopqrst'.
'uvwxyzABCD'.
'EFGHIJKLMN'.
'OPQRSTUVWX'.
'YZ._';
}
$result = '';
for ($ii = 0; $ii < self::INDEX_DIGEST_LENGTH; $ii++) {
$result .= $map[(ord($hash[$ii]) & 0x3F)];
}
return $result;
}
/**
* Shorten a string to a maximum byte length in a collision-resistant way
* while retaining some degree of human-readability.
*
* This function converts an input string into a prefix plus a hash. For
* example, a very long string beginning with "crabapplepie..." might be
* digested to something like "crabapp-N1wM1Nz3U84k".
*
* This allows the maximum length of identifiers to be fixed while
* maintaining a high degree of collision resistance and a moderate degree
* of human readability.
*
* @param string The string to shorten.
* @param int Maximum length of the result.
* @return string String shortened in a collision-resistant way.
*/
public static function digestToLength($string, $length) {
// We need at least two more characters than the hash length to fit in a
// a 1-character prefix and a separator.
$min_length = self::INDEX_DIGEST_LENGTH + 2;
if ($length < $min_length) {
throw new Exception(
pht(
- 'Length parameter in digestToLength() must be at least %s, '.
+ 'Length parameter in %s must be at least %s, '.
'but %s was provided.',
+ 'digestToLength()',
new PhutilNumber($min_length),
new PhutilNumber($length)));
}
// We could conceivably return the string unmodified if it's shorter than
// the specified length. Instead, always hash it. This makes the output of
// the method more recognizable and consistent (no surprising new behavior
// once you hit a string longer than `$length`) and prevents an attacker
// who can control the inputs from intentionally using the hashed form
// of a string to cause a collision.
$hash = self::digestForIndex($string);
$prefix = substr($string, 0, ($length - ($min_length - 1)));
return $prefix.'-'.$hash;
}
}
diff --git a/src/infrastructure/util/__tests__/PhabricatorHashTestCase.php b/src/infrastructure/util/__tests__/PhabricatorHashTestCase.php
index 8b33d22f5..c6839dae0 100644
--- a/src/infrastructure/util/__tests__/PhabricatorHashTestCase.php
+++ b/src/infrastructure/util/__tests__/PhabricatorHashTestCase.php
@@ -1,41 +1,41 @@
<?php
final class PhabricatorHashTestCase extends PhabricatorTestCase {
public function testHashForIndex() {
$map = array(
'dog' => 'Aliif7Qjd5ct',
'cat' => 'toudDsue3Uv8',
'rat' => 'RswaKgTjqOuj',
'bat' => 'rAkJKyX4YdYm',
);
foreach ($map as $input => $expect) {
$this->assertEqual(
$expect,
PhabricatorHash::digestForIndex($input),
- "Input: {$input}");
+ pht('Input: %s', $input));
}
// Test that the encoding produces 6 bits of entropy per byte.
$entropy = array(
'dog', 'cat', 'rat', 'bat', 'dig', 'fig', 'cot',
'cut', 'fog', 'rig', 'rug', 'dug', 'mat', 'pat',
'eat', 'tar', 'pot',
);
$seen = array();
foreach ($entropy as $input) {
$chars = preg_split('//', PhabricatorHash::digestForIndex($input));
foreach ($chars as $char) {
$seen[$char] = true;
}
}
$this->assertEqual(
(1 << 6),
count($seen),
- "Distinct characters in hash of: {$input}");
+ pht('Distinct characters in hash of: %s', $input));
}
}
diff --git a/src/infrastructure/util/__tests__/PhabricatorSlugTestCase.php b/src/infrastructure/util/__tests__/PhabricatorSlugTestCase.php
index 3c820a085..0f96a735e 100644
--- a/src/infrastructure/util/__tests__/PhabricatorSlugTestCase.php
+++ b/src/infrastructure/util/__tests__/PhabricatorSlugTestCase.php
@@ -1,77 +1,77 @@
<?php
final class PhabricatorSlugTestCase extends PhabricatorTestCase {
public function testSlugNormalization() {
$slugs = array(
'' => '/',
'/' => '/',
'//' => '/',
'&&&' => '_/',
'/derp/' => 'derp/',
'derp' => 'derp/',
'derp//derp' => 'derp/derp/',
'DERP//DERP' => 'derp/derp/',
'a B c' => 'a_b_c/',
'-1~2.3abcd' => '-1~2.3abcd/',
"T\x00O\x00D\x00O" => 't_o_d_o/',
'x#%&+=\\?<> y' => 'x_y/',
"\xE2\x98\x83" => "\xE2\x98\x83/",
'..' => 'dotdot/',
'../' => 'dotdot/',
'/../' => 'dotdot/',
'a/b' => 'a/b/',
'a//b' => 'a/b/',
'a/../b/' => 'a/dotdot/b/',
'/../a' => 'dotdot/a/',
'../a' => 'dotdot/a/',
'a/..' => 'a/dotdot/',
'a/../' => 'a/dotdot/',
'a?' => 'a/',
'??' => '_/',
'a/?' => 'a/_/',
'??/a/??' => '_/a/_/',
'a/??/c' => 'a/_/c/',
'a/?b/c' => 'a/b/c/',
'a/b?/c' => 'a/b/c/',
);
foreach ($slugs as $slug => $normal) {
$this->assertEqual(
$normal,
PhabricatorSlug::normalize($slug),
- "Normalization of '{$slug}'");
+ pht("Normalization of '%s'", $slug));
}
}
public function testSlugAncestry() {
$slugs = array(
'/' => array(),
'pokemon/' => array('/'),
'pokemon/squirtle/' => array('/', 'pokemon/'),
);
foreach ($slugs as $slug => $ancestry) {
$this->assertEqual(
$ancestry,
PhabricatorSlug::getAncestry($slug),
- "Ancestry of '{$slug}'");
+ pht("Ancestry of '%s'", $slug));
}
}
public function testSlugDepth() {
$slugs = array(
'/' => 0,
'a/' => 1,
'a/b/' => 2,
'a////b/' => 2,
);
foreach ($slugs as $slug => $depth) {
$this->assertEqual(
$depth,
PhabricatorSlug::getDepth($slug),
- "Depth of '{$slug}'");
+ pht("Depth of '%s'", $slug));
}
}
}
diff --git a/src/view/__tests__/PhabricatorLocalTimeTestCase.php b/src/view/__tests__/PhabricatorLocalTimeTestCase.php
index 609f725ec..f9fd31ad9 100644
--- a/src/view/__tests__/PhabricatorLocalTimeTestCase.php
+++ b/src/view/__tests__/PhabricatorLocalTimeTestCase.php
@@ -1,36 +1,36 @@
<?php
final class PhabricatorLocalTimeTestCase extends PhabricatorTestCase {
public function testLocalTimeFormatting() {
$user = new PhabricatorUser();
$user->setTimezoneIdentifier('America/Los_Angeles');
$utc = new PhabricatorUser();
$utc->setTimezoneIdentifier('UTC');
$this->assertEqual(
'Jan 1 2000, 12:00 AM',
phabricator_datetime(946684800, $utc),
- 'Datetime formatting');
+ pht('Datetime formatting'));
$this->assertEqual(
'Jan 1 2000',
phabricator_date(946684800, $utc),
- 'Date formatting');
+ pht('Date formatting'));
$this->assertEqual(
'12:00 AM',
phabricator_time(946684800, $utc),
- 'Time formatting');
+ pht('Time formatting'));
$this->assertEqual(
'Dec 31 1999, 4:00 PM',
phabricator_datetime(946684800, $user),
- 'Localization');
+ pht('Localization'));
$this->assertEqual(
'',
phabricator_datetime(0, $user),
- 'Missing epoch should fail gracefully');
+ pht('Missing epoch should fail gracefully'));
}
}
diff --git a/src/view/form/PHUIFormPageView.php b/src/view/form/PHUIFormPageView.php
index 7b45edb25..cb62d3312 100644
--- a/src/view/form/PHUIFormPageView.php
+++ b/src/view/form/PHUIFormPageView.php
@@ -1,224 +1,224 @@
<?php
/**
* @concrete-extensible
*/
class PHUIFormPageView extends AphrontView {
private $key;
private $form;
private $controls = array();
private $content = array();
private $values = array();
private $isValid;
private $validateFormPageCallback;
private $adjustFormPageCallback;
private $pageErrors = array();
private $pageName;
public function setPageName($page_name) {
$this->pageName = $page_name;
return $this;
}
public function getPageName() {
return $this->pageName;
}
public function addPageError($page_error) {
$this->pageErrors[] = $page_error;
return $this;
}
public function getPageErrors() {
return $this->pageErrors;
}
public function setAdjustFormPageCallback($adjust_form_page_callback) {
$this->adjustFormPageCallback = $adjust_form_page_callback;
return $this;
}
public function setValidateFormPageCallback($validate_form_page_callback) {
$this->validateFormPageCallback = $validate_form_page_callback;
return $this;
}
public function addInstructions($text, $before = null) {
$tag = phutil_tag(
'div',
array(
'class' => 'aphront-form-instructions',
),
$text);
$append = true;
if ($before !== null) {
for ($ii = 0; $ii < count($this->content); $ii++) {
if ($this->content[$ii] instanceof AphrontFormControl) {
if ($this->content[$ii]->getName() == $before) {
array_splice($this->content, $ii, 0, array($tag));
$append = false;
break;
}
}
}
}
if ($append) {
$this->content[] = $tag;
}
return $this;
}
public function addRemarkupInstructions($remarkup, $before = null) {
return $this->addInstructions(
PhabricatorMarkupEngine::renderOneObject(
id(new PhabricatorMarkupOneOff())->setContent($remarkup),
'default',
$this->getUser()), $before);
}
public function addControl(AphrontFormControl $control) {
$name = $control->getName();
if (!strlen($name)) {
- throw new Exception('Form control has no name!');
+ throw new Exception(pht('Form control has no name!'));
}
if (isset($this->controls[$name])) {
throw new Exception(
- "Form page contains duplicate control with name '{$name}'!");
+ pht("Form page contains duplicate control with name '%s'!", $name));
}
$this->controls[$name] = $control;
$this->content[] = $control;
$control->setFormPage($this);
return $this;
}
public function getControls() {
return $this->controls;
}
public function getControl($name) {
if (empty($this->controls[$name])) {
- throw new Exception("No page control '{$name}'!");
+ throw new Exception(pht("No page control '%s'!", $name));
}
return $this->controls[$name];
}
protected function canAppendChild() {
return false;
}
public function setPagedFormView(PHUIPagedFormView $view, $key) {
if ($this->key) {
- throw new Exception('This page is already part of a form!');
+ throw new Exception(pht('This page is already part of a form!'));
}
$this->form = $view;
$this->key = $key;
return $this;
}
public function adjustFormPage() {
if ($this->adjustFormPageCallback) {
call_user_func($this->adjustFormPageCallback, $this);
}
return $this;
}
protected function validateFormPage() {
if ($this->validateFormPageCallback) {
return call_user_func($this->validateFormPageCallback, $this);
}
return true;
}
public function getKey() {
return $this->key;
}
public function render() {
return $this->content;
}
public function getForm() {
return $this->form;
}
public function getRequestKey($key) {
return $this->getForm()->getRequestKey('p:'.$this->key.':'.$key);
}
public function validateObjectType($object) {
return true;
}
public function validateResponseType($response) {
return true;
}
protected function validateControls() {
$result = true;
foreach ($this->getControls() as $name => $control) {
if (!$control->isValid()) {
$result = false;
break;
}
}
return $result;
}
public function isValid() {
if ($this->isValid === null) {
$this->isValid = $this->validateControls() && $this->validateFormPage();
}
return $this->isValid;
}
public function readFromRequest(AphrontRequest $request) {
foreach ($this->getControls() as $name => $control) {
$control->readValueFromRequest($request);
}
return $this;
}
public function readFromObject($object) {
foreach ($this->getControls() as $name => $control) {
if (is_array($object)) {
$control->readValueFromDictionary($object);
}
}
return $this;
}
public function writeToResponse($response) {
return $this;
}
public function readSerializedValues(AphrontRequest $request) {
foreach ($this->getControls() as $name => $control) {
$key = $this->getRequestKey($name);
$control->readSerializedValue($request->getStr($key));
}
return $this;
}
public function getSerializedValues() {
$dict = array();
foreach ($this->getControls() as $name => $control) {
$key = $this->getRequestKey($name);
$dict[$key] = $control->getSerializedValue();
}
return $dict;
}
}
diff --git a/src/view/form/PHUIPagedFormView.php b/src/view/form/PHUIPagedFormView.php
index ff72f67ca..b5f6ad706 100644
--- a/src/view/form/PHUIPagedFormView.php
+++ b/src/view/form/PHUIPagedFormView.php
@@ -1,278 +1,278 @@
<?php
/**
* @task page Managing Pages
*/
final class PHUIPagedFormView extends AphrontView {
private $name = 'pages';
private $pages = array();
private $selectedPage;
private $choosePage;
private $nextPage;
private $prevPage;
private $complete;
private $cancelURI;
protected function canAppendChild() {
return false;
}
/* -( Managing Pages )----------------------------------------------------- */
/**
* @task page
*/
public function addPage($key, PHUIFormPageView $page) {
if (isset($this->pages[$key])) {
- throw new Exception("Duplicate page with key '{$key}'!");
+ throw new Exception(pht("Duplicate page with key '%s'!", $key));
}
$this->pages[$key] = $page;
$page->setPagedFormView($this, $key);
$this->selectedPage = null;
$this->complete = null;
return $this;
}
/**
* @task page
*/
public function getPage($key) {
if (!$this->pageExists($key)) {
- throw new Exception("No page '{$key}' exists!");
+ throw new Exception(pht("No page '%s' exists!", $key));
}
return $this->pages[$key];
}
/**
* @task page
*/
public function pageExists($key) {
return isset($this->pages[$key]);
}
/**
* @task page
*/
protected function getPageIndex($key) {
$page = $this->getPage($key);
$index = 0;
foreach ($this->pages as $target_page) {
if ($page === $target_page) {
break;
}
$index++;
}
return $index;
}
/**
* @task page
*/
protected function getPageByIndex($index) {
foreach ($this->pages as $page) {
if (!$index) {
return $page;
}
$index--;
}
- throw new Exception("Requesting out-of-bounds page '{$index}'.");
+ throw new Exception(pht("Requesting out-of-bounds page '%s'.", $index));
}
protected function getLastIndex() {
return count($this->pages) - 1;
}
protected function isFirstPage(PHUIFormPageView $page) {
return ($this->getPageIndex($page->getKey()) === 0);
}
protected function isLastPage(PHUIFormPageView $page) {
return ($this->getPageIndex($page->getKey()) === (count($this->pages) - 1));
}
public function getSelectedPage() {
return $this->selectedPage;
}
public function readFromObject($object) {
return $this->processForm($is_request = false, $object);
}
public function writeToResponse($response) {
foreach ($this->pages as $page) {
$page->validateResponseType($response);
$response = $page->writeToResponse($page);
}
return $response;
}
public function readFromRequest(AphrontRequest $request) {
$this->choosePage = $request->getStr($this->getRequestKey('page'));
$this->nextPage = $request->getStr('__submit__');
$this->prevPage = $request->getStr('__back__');
return $this->processForm($is_request = true, $request);
}
public function setName($name) {
$this->name = $name;
return $this;
}
public function getValue($page, $key, $default = null) {
return $this->getPage($page)->getValue($key, $default);
}
public function setValue($page, $key, $value) {
$this->getPage($page)->setValue($key, $value);
return $this;
}
private function processForm($is_request, $source) {
if ($this->pageExists($this->choosePage)) {
$selected = $this->getPage($this->choosePage);
} else {
$selected = $this->getPageByIndex(0);
}
$is_attempt_complete = false;
if ($this->prevPage) {
$prev_index = $this->getPageIndex($selected->getKey()) - 1;
$index = max(0, $prev_index);
$selected = $this->getPageByIndex($index);
} else if ($this->nextPage) {
$next_index = $this->getPageIndex($selected->getKey()) + 1;
if ($next_index > $this->getLastIndex()) {
$is_attempt_complete = true;
}
$index = min($this->getLastIndex(), $next_index);
$selected = $this->getPageByIndex($index);
}
$validation_error = false;
$found_selected = false;
foreach ($this->pages as $key => $page) {
if ($is_request) {
if ($key === $this->choosePage) {
$page->readFromRequest($source);
} else {
$page->readSerializedValues($source);
}
} else {
$page->readFromObject($source);
}
if (!$found_selected) {
$page->adjustFormPage();
}
if ($page === $selected) {
$found_selected = true;
}
if (!$found_selected || $is_attempt_complete) {
if (!$page->isValid()) {
$selected = $page;
$validation_error = true;
break;
}
}
}
if ($is_attempt_complete && !$validation_error) {
$this->complete = true;
} else {
$this->selectedPage = $selected;
}
return $this;
}
public function isComplete() {
return $this->complete;
}
public function getRequestKey($key) {
return $this->name.':'.$key;
}
public function setCancelURI($cancel_uri) {
$this->cancelURI = $cancel_uri;
return $this;
}
public function getCancelURI() {
return $this->cancelURI;
}
public function render() {
$form = id(new AphrontFormView())
->setUser($this->getUser());
$selected_page = $this->getSelectedPage();
if (!$selected_page) {
- throw new Exception('No selected page!');
+ throw new Exception(pht('No selected page!'));
}
$form->addHiddenInput(
$this->getRequestKey('page'),
$selected_page->getKey());
$errors = array();
foreach ($this->pages as $page) {
if ($page == $selected_page) {
$errors = $page->getPageErrors();
continue;
}
foreach ($page->getSerializedValues() as $key => $value) {
$form->addHiddenInput($key, $value);
}
}
$submit = id(new PHUIFormMultiSubmitControl());
if (!$this->isFirstPage($selected_page)) {
$submit->addBackButton();
} else if ($this->getCancelURI()) {
$submit->addCancelButton($this->getCancelURI());
}
if ($this->isLastPage($selected_page)) {
$submit->addSubmitButton(pht('Save'));
} else {
- $submit->addSubmitButton(pht("Continue \xC2\xBB"));
+ $submit->addSubmitButton(pht('Continue')." \xC2\xBB");
}
$form->appendChild($selected_page);
$form->appendChild($submit);
$box = id(new PHUIObjectBoxView())
->setFormErrors($errors)
->setForm($form);
if ($selected_page->getPageName()) {
$header = id(new PHUIHeaderView())
->setHeader($selected_page->getPageName());
$box->setHeader($header);
}
return $box;
}
}
diff --git a/src/view/form/control/AphrontFormCheckboxControl.php b/src/view/form/control/AphrontFormCheckboxControl.php
index 0d89e0a6a..69be93ab9 100644
--- a/src/view/form/control/AphrontFormCheckboxControl.php
+++ b/src/view/form/control/AphrontFormCheckboxControl.php
@@ -1,61 +1,61 @@
<?php
final class AphrontFormCheckboxControl extends AphrontFormControl {
private $boxes = array();
public function addCheckbox(
$name,
$value,
$label,
$checked = false,
$id = null) {
$this->boxes[] = array(
'name' => $name,
'value' => $value,
'label' => $label,
'checked' => $checked,
- 'id' => $id,
+ 'id' => $id,
);
return $this;
}
protected function getCustomControlClass() {
return 'aphront-form-control-checkbox';
}
protected function renderInput() {
$rows = array();
foreach ($this->boxes as $box) {
$id = idx($box, 'id');
if ($id === null) {
$id = celerity_generate_unique_node_id();
}
$checkbox = phutil_tag(
'input',
array(
'id' => $id,
'type' => 'checkbox',
'name' => $box['name'],
'value' => $box['value'],
'checked' => $box['checked'] ? 'checked' : null,
'disabled' => $this->getDisabled() ? 'disabled' : null,
));
$label = phutil_tag(
'label',
array(
'for' => $id,
),
$box['label']);
$rows[] = phutil_tag('tr', array(), array(
phutil_tag('td', array(), $checkbox),
phutil_tag('th', array(), $label),
));
}
return phutil_tag(
'table',
array('class' => 'aphront-form-control-checkbox-layout'),
$rows);
}
}
diff --git a/src/view/form/control/AphrontFormControl.php b/src/view/form/control/AphrontFormControl.php
index 4118d9ad4..16e36be66 100644
--- a/src/view/form/control/AphrontFormControl.php
+++ b/src/view/form/control/AphrontFormControl.php
@@ -1,260 +1,260 @@
<?php
abstract class AphrontFormControl extends AphrontView {
private $label;
private $caption;
private $error;
private $name;
private $value;
private $disabled;
private $id;
private $controlID;
private $controlStyle;
private $formPage;
private $required;
private $hidden;
private $classes;
public function setHidden($hidden) {
$this->hidden = $hidden;
return $this;
}
public function setID($id) {
$this->id = $id;
return $this;
}
public function getID() {
return $this->id;
}
public function setControlID($control_id) {
$this->controlID = $control_id;
return $this;
}
public function getControlID() {
return $this->controlID;
}
public function setControlStyle($control_style) {
$this->controlStyle = $control_style;
return $this;
}
public function getControlStyle() {
return $this->controlStyle;
}
public function setLabel($label) {
$this->label = $label;
return $this;
}
public function getLabel() {
return $this->label;
}
public function setCaption($caption) {
$this->caption = $caption;
return $this;
}
public function getCaption() {
return $this->caption;
}
public function setError($error) {
$this->error = $error;
return $this;
}
public function getError() {
return $this->error;
}
public function setName($name) {
$this->name = $name;
return $this;
}
public function getName() {
return $this->name;
}
public function setValue($value) {
$this->value = $value;
return $this;
}
public function getValue() {
return $this->value;
}
public function isValid() {
if ($this->error && $this->error !== true) {
return false;
}
if ($this->isRequired() && $this->isEmpty()) {
return false;
}
return true;
}
public function isRequired() {
return $this->required;
}
public function isEmpty() {
return !strlen($this->getValue());
}
public function getSerializedValue() {
return $this->getValue();
}
public function readSerializedValue($value) {
$this->setValue($value);
return $this;
}
public function readValueFromRequest(AphrontRequest $request) {
$this->setValue($request->getStr($this->getName()));
return $this;
}
public function readValueFromDictionary(array $dictionary) {
$this->setValue(idx($dictionary, $this->getName()));
return $this;
}
public function setFormPage(PHUIFormPageView $page) {
if ($this->formPage) {
- throw new Exception('This control is already a member of a page!');
+ throw new Exception(pht('This control is already a member of a page!'));
}
$this->formPage = $page;
return $this;
}
public function getFormPage() {
if ($this->formPage === null) {
- throw new Exception('This control does not have a page!');
+ throw new Exception(pht('This control does not have a page!'));
}
return $this->formPage;
}
public function setDisabled($disabled) {
$this->disabled = $disabled;
return $this;
}
public function getDisabled() {
return $this->disabled;
}
abstract protected function renderInput();
abstract protected function getCustomControlClass();
protected function shouldRender() {
return true;
}
public function addClass($class) {
$this->classes[] = $class;
return $this;
}
final public function render() {
if (!$this->shouldRender()) {
return null;
}
$custom_class = $this->getCustomControlClass();
// If we don't have an ID yet, assign an automatic one so we can associate
// the label with the control. This allows assistive technologies to read
// form labels.
if (!$this->getID()) {
$this->setID(celerity_generate_unique_node_id());
}
$input = phutil_tag(
'div',
array('class' => 'aphront-form-input'),
$this->renderInput());
$error = null;
if (strlen($this->getError())) {
$error = $this->getError();
if ($error === true) {
$error = phutil_tag(
'span',
array('class' => 'aphront-form-error aphront-form-required'),
pht('Required'));
} else {
$error = phutil_tag(
'span',
array('class' => 'aphront-form-error'),
$error);
}
}
if (strlen($this->getLabel())) {
$label = phutil_tag(
'label',
array(
'class' => 'aphront-form-label',
'for' => $this->getID(),
),
array(
$this->getLabel(),
$error,
));
} else {
$label = null;
$custom_class .= ' aphront-form-control-nolabel';
}
if (strlen($this->getCaption())) {
$caption = phutil_tag(
'div',
array('class' => 'aphront-form-caption'),
$this->getCaption());
} else {
$caption = null;
}
$classes = array();
$classes[] = 'aphront-form-control';
$classes[] = 'grouped';
$classes[] = $custom_class;
if ($this->classes) {
foreach ($this->classes as $class) {
$classes[] = $class;
}
}
$style = $this->controlStyle;
if ($this->hidden) {
$style = 'display: none; '.$style;
}
return phutil_tag(
'div',
array(
'class' => implode(' ', $classes),
'id' => $this->controlID,
'style' => $style,
),
array(
$label,
$error,
$input,
$caption,
));
}
}
diff --git a/src/view/form/control/AphrontFormDateControl.php b/src/view/form/control/AphrontFormDateControl.php
index 2615c1c5e..d829a2909 100644
--- a/src/view/form/control/AphrontFormDateControl.php
+++ b/src/view/form/control/AphrontFormDateControl.php
@@ -1,405 +1,405 @@
<?php
final class AphrontFormDateControl extends AphrontFormControl {
private $initialTime;
private $zone;
private $valueDay;
private $valueMonth;
private $valueYear;
private $valueTime;
private $allowNull;
private $continueOnInvalidDate = false;
private $isTimeDisabled;
private $isDisabled;
private $endDateID;
public function setAllowNull($allow_null) {
$this->allowNull = $allow_null;
return $this;
}
public function setIsTimeDisabled($is_disabled) {
$this->isTimeDisabled = $is_disabled;
return $this;
}
public function setEndDateID($value) {
$this->endDateID = $value;
return $this;
}
const TIME_START_OF_DAY = 'start-of-day';
const TIME_END_OF_DAY = 'end-of-day';
const TIME_START_OF_BUSINESS = 'start-of-business';
const TIME_END_OF_BUSINESS = 'end-of-business';
public function setInitialTime($time) {
$this->initialTime = $time;
return $this;
}
public function readValueFromRequest(AphrontRequest $request) {
$day = $request->getInt($this->getDayInputName());
$month = $request->getInt($this->getMonthInputName());
$year = $request->getInt($this->getYearInputName());
$time = $request->getStr($this->getTimeInputName());
$enabled = $request->getBool($this->getCheckboxInputName());
if ($this->allowNull && !$enabled) {
$this->setError(null);
$this->setValue(null);
return;
}
$err = $this->getError();
if ($day || $month || $year || $time) {
$this->valueDay = $day;
$this->valueMonth = $month;
$this->valueYear = $year;
$this->valueTime = $time;
// Assume invalid.
$err = 'Invalid';
$zone = $this->getTimezone();
try {
$date = new DateTime("{$year}-{$month}-{$day} {$time}", $zone);
$value = $date->format('U');
} catch (Exception $ex) {
$value = null;
}
if ($value) {
$this->setValue($value);
$err = null;
} else {
$this->setValue(null);
}
} else {
$value = $this->getInitialValue();
if ($value) {
$this->setValue($value);
} else {
$this->setValue(null);
}
}
$this->setError($err);
return $this->getValue();
}
protected function getCustomControlClass() {
return 'aphront-form-control-date';
}
public function setValue($epoch) {
if ($epoch instanceof AphrontFormDateControlValue) {
$this->continueOnInvalidDate = true;
$this->valueYear = $epoch->getValueYear();
$this->valueMonth = $epoch->getValueMonth();
$this->valueDay = $epoch->getValueDay();
$this->valueTime = $epoch->getValueTime();
$this->allowNull = $epoch->getOptional();
$this->isDisabled = $epoch->isDisabled();
return parent::setValue($epoch->getEpoch());
}
$result = parent::setValue($epoch);
if ($epoch === null) {
return $result;
}
$readable = $this->formatTime($epoch, 'Y!m!d!g:i A');
$readable = explode('!', $readable, 4);
$this->valueYear = $readable[0];
$this->valueMonth = $readable[1];
$this->valueDay = $readable[2];
$this->valueTime = $readable[3];
return $result;
}
private function getMinYear() {
$cur_year = $this->formatTime(
time(),
'Y');
$val_year = $this->getYearInputValue();
return min($cur_year, $val_year) - 3;
}
private function getMaxYear() {
$cur_year = $this->formatTime(
time(),
'Y');
$val_year = $this->getYearInputValue();
return max($cur_year, $val_year) + 3;
}
private function getDayInputValue() {
return $this->valueDay;
}
private function getMonthInputValue() {
return $this->valueMonth;
}
private function getYearInputValue() {
return $this->valueYear;
}
private function getTimeInputValue() {
return $this->valueTime;
}
private function formatTime($epoch, $fmt) {
return phabricator_format_local_time(
$epoch,
$this->user,
$fmt);
}
private function getDayInputName() {
return $this->getName().'_d';
}
private function getMonthInputName() {
return $this->getName().'_m';
}
private function getYearInputName() {
return $this->getName().'_y';
}
private function getTimeInputName() {
return $this->getName().'_t';
}
private function getCheckboxInputName() {
return $this->getName().'_e';
}
protected function renderInput() {
$disabled = null;
if ($this->getValue() === null && !$this->continueOnInvalidDate) {
$this->setValue($this->getInitialValue());
if ($this->allowNull) {
$disabled = 'disabled';
}
}
if ($this->isDisabled) {
$disabled = 'disabled';
}
$min_year = $this->getMinYear();
$max_year = $this->getMaxYear();
$days = range(1, 31);
$days = array_fuse($days);
$months = array(
1 => pht('Jan'),
2 => pht('Feb'),
3 => pht('Mar'),
4 => pht('Apr'),
5 => pht('May'),
6 => pht('Jun'),
7 => pht('Jul'),
8 => pht('Aug'),
9 => pht('Sep'),
10 => pht('Oct'),
11 => pht('Nov'),
12 => pht('Dec'),
);
$checkbox = null;
if ($this->allowNull) {
$checkbox = javelin_tag(
'input',
array(
'type' => 'checkbox',
'name' => $this->getCheckboxInputName(),
'sigil' => 'calendar-enable',
'class' => 'aphront-form-date-enabled-input',
'value' => 1,
'checked' => ($disabled === null ? 'checked' : null),
));
}
$years = range($this->getMinYear(), $this->getMaxYear());
$years = array_fuse($years);
$days_sel = AphrontFormSelectControl::renderSelectTag(
$this->getDayInputValue(),
$days,
array(
'name' => $this->getDayInputName(),
'sigil' => 'day-input',
));
$months_sel = AphrontFormSelectControl::renderSelectTag(
$this->getMonthInputValue(),
$months,
array(
'name' => $this->getMonthInputName(),
'sigil' => 'month-input',
));
$years_sel = AphrontFormSelectControl::renderSelectTag(
$this->getYearInputValue(),
$years,
array(
'name' => $this->getYearInputName(),
'sigil' => 'year-input',
));
$cicon = id(new PHUIIconView())
->setIconFont('fa-calendar');
$cal_icon = javelin_tag(
'a',
array(
'href' => '#',
'class' => 'calendar-button',
'sigil' => 'calendar-button',
),
$cicon);
$values = $this->getTimeTypeaheadValues();
$time_id = celerity_generate_unique_node_id();
Javelin::initBehavior('time-typeahead', array(
'startTimeID' => $time_id,
'endTimeID' => $this->endDateID,
'timeValues' => $values,
));
$time_sel = javelin_tag(
'input',
array(
'autocomplete' => 'off',
'name' => $this->getTimeInputName(),
'sigil' => 'time-input',
'value' => $this->getTimeInputValue(),
'type' => 'text',
'class' => 'aphront-form-date-time-input',
),
'');
$time_div = javelin_tag(
'div',
array(
'id' => $time_id,
'class' => 'aphront-form-date-time-input-container',
),
$time_sel);
Javelin::initBehavior('fancy-datepicker', array());
$classes = array();
$classes[] = 'aphront-form-date-container';
if ($disabled) {
$classes[] = 'datepicker-disabled';
}
if ($this->isTimeDisabled) {
$classes[] = 'no-time';
}
return javelin_tag(
'div',
array(
'class' => implode(' ', $classes),
'sigil' => 'phabricator-date-control',
'meta' => array(
'disabled' => (bool)$disabled,
),
'id' => $this->getID(),
),
array(
$checkbox,
$days_sel,
$months_sel,
$years_sel,
$cal_icon,
$time_div,
));
}
private function getTimezone() {
if ($this->zone) {
return $this->zone;
}
$user = $this->getUser();
if (!$this->getUser()) {
- throw new Exception('Call setUser() before getTimezone()!');
+ throw new PhutilInvalidStateException('setUser');
}
$user_zone = $user->getTimezoneIdentifier();
$this->zone = new DateTimeZone($user_zone);
return $this->zone;
}
private function getInitialValue() {
$zone = $this->getTimezone();
// TODO: We could eventually allow these to be customized per install or
// per user or both, but let's wait and see.
switch ($this->initialTime) {
case self::TIME_START_OF_DAY:
default:
$time = '12:00 AM';
break;
case self::TIME_START_OF_BUSINESS:
$time = '9:00 AM';
break;
case self::TIME_END_OF_BUSINESS:
$time = '5:00 PM';
break;
case self::TIME_END_OF_DAY:
$time = '11:59 PM';
break;
}
$today = $this->formatTime(time(), 'Y-m-d');
try {
$date = new DateTime("{$today} {$time}", $zone);
$value = $date->format('U');
} catch (Exception $ex) {
$value = null;
}
return $value;
}
private function getTimeTypeaheadValues() {
$times = array();
$am_pm_list = array('AM', 'PM');
foreach ($am_pm_list as $am_pm) {
for ($hour = 0; $hour < 12; $hour++) {
$actual_hour = ($hour == 0) ? 12 : $hour;
$times[] = $actual_hour.':00 '.$am_pm;
$times[] = $actual_hour.':30 '.$am_pm;
}
}
foreach ($times as $key => $time) {
$times[$key] = array($key, $time);
}
return $times;
}
}
diff --git a/src/view/form/control/AphrontFormPasswordControl.php b/src/view/form/control/AphrontFormPasswordControl.php
index 9e37d7aa2..081628c33 100644
--- a/src/view/form/control/AphrontFormPasswordControl.php
+++ b/src/view/form/control/AphrontFormPasswordControl.php
@@ -1,29 +1,29 @@
<?php
final class AphrontFormPasswordControl extends AphrontFormControl {
private $disableAutocomplete;
public function setDisableAutocomplete($disable_autocomplete) {
$this->disableAutocomplete = $disable_autocomplete;
return $this;
}
protected function getCustomControlClass() {
return 'aphront-form-control-password';
}
protected function renderInput() {
return phutil_tag(
'input',
array(
- 'type' => 'password',
- 'name' => $this->getName(),
- 'value' => $this->getValue(),
- 'disabled' => $this->getDisabled() ? 'disabled' : null,
+ 'type' => 'password',
+ 'name' => $this->getName(),
+ 'value' => $this->getValue(),
+ 'disabled' => $this->getDisabled() ? 'disabled' : null,
'autocomplete' => ($this->disableAutocomplete ? 'off' : null),
- 'id' => $this->getID(),
+ 'id' => $this->getID(),
));
}
}
diff --git a/src/view/form/control/PHUIFormMultiSubmitControl.php b/src/view/form/control/PHUIFormMultiSubmitControl.php
index af234919d..4c7454bd6 100644
--- a/src/view/form/control/PHUIFormMultiSubmitControl.php
+++ b/src/view/form/control/PHUIFormMultiSubmitControl.php
@@ -1,61 +1,61 @@
<?php
final class PHUIFormMultiSubmitControl extends AphrontFormControl {
private $buttons = array();
public function addBackButton($label = null) {
if ($label === null) {
- $label = pht("\xC2\xAB Back");
+ $label = "\xC2\xAB ".pht('Back');
}
return $this->addButton('__back__', $label, 'grey');
}
public function addSubmitButton($label) {
return $this->addButton('__submit__', $label);
}
public function addCancelButton($uri, $label = null) {
if ($label === null) {
$label = pht('Cancel');
}
$this->buttons[] = phutil_tag(
'a',
array(
'class' => 'grey button',
'href' => $uri,
),
$label);
return $this;
}
public function addButtonView(PHUIButtonView $button) {
$this->buttons[] = $button;
return $this;
}
public function addButton($name, $label, $class = null) {
$this->buttons[] = javelin_tag(
'input',
array(
'type' => 'submit',
'name' => $name,
'value' => $label,
'class' => $class,
'sigil' => 'alternate-submit-button',
'disabled' => $this->getDisabled() ? 'disabled' : null,
));
return $this;
}
protected function getCustomControlClass() {
return 'phui-form-control-multi-submit';
}
protected function renderInput() {
return array_reverse($this->buttons);
}
}
diff --git a/src/view/layout/AphrontMoreView.php b/src/view/layout/AphrontMoreView.php
index 0bd1b82d8..9fe9389ed 100644
--- a/src/view/layout/AphrontMoreView.php
+++ b/src/view/layout/AphrontMoreView.php
@@ -1,57 +1,57 @@
<?php
final class AphrontMoreView extends AphrontView {
private $some;
private $more;
private $expandtext;
public function setSome($some) {
$this->some = $some;
return $this;
}
public function setMore($more) {
$this->more = $more;
return $this;
}
public function setExpandText($text) {
$this->expandtext = $text;
return $this;
}
public function render() {
$content = array();
$content[] = $this->some;
if ($this->more && $this->more != $this->some) {
- $text = "(Show More\xE2\x80\xA6)";
+ $text = "(".pht('Show More')."\xE2\x80\xA6)";
if ($this->expandtext !== null) {
$text = $this->expandtext;
}
Javelin::initBehavior('aphront-more');
$content[] = ' ';
$content[] = javelin_tag(
'a',
array(
'sigil' => 'aphront-more-view-show-more',
'mustcapture' => true,
'href' => '#',
'meta' => array(
'more' => $this->more,
),
),
$text);
}
return javelin_tag(
'div',
array(
'sigil' => 'aphront-more-view',
),
$content);
}
}
diff --git a/src/view/layout/AphrontMultiColumnView.php b/src/view/layout/AphrontMultiColumnView.php
index 6705b8912..dfd5f8176 100644
--- a/src/view/layout/AphrontMultiColumnView.php
+++ b/src/view/layout/AphrontMultiColumnView.php
@@ -1,156 +1,156 @@
<?php
final class AphrontMultiColumnView extends AphrontView {
const GUTTER_SMALL = 'msr';
const GUTTER_MEDIUM = 'mmr';
const GUTTER_LARGE = 'mlr';
private $id;
private $columns = array();
private $fluidLayout = false;
private $fluidishLayout = false;
private $gutter;
private $border;
public function setID($id) {
$this->id = $id;
return $this;
}
public function getID() {
return $this->id;
}
public function addColumn(
$column,
$class = null,
$sigil = null,
$metadata = null) {
$this->columns[] = array(
'column' => $column,
'class' => $class,
'sigil' => $sigil,
'metadata' => $metadata,
);
return $this;
}
public function setFluidlayout($layout) {
$this->fluidLayout = $layout;
return $this;
}
public function setFluidishLayout($layout) {
$this->fluidLayout = true;
$this->fluidishLayout = $layout;
return $this;
}
public function setGutter($gutter) {
$this->gutter = $gutter;
return $this;
}
public function setBorder($border) {
$this->border = $border;
return $this;
}
public function render() {
require_celerity_resource('aphront-multi-column-view-css');
$classes = array();
$classes[] = 'aphront-multi-column-inner';
$classes[] = 'grouped';
if ($this->fluidishLayout || $this->fluidLayout) {
// we only support seven columns for now for fluid views; see T4054
if (count($this->columns) > 7) {
- throw new Exception('No more than 7 columns per view.');
+ throw new Exception(pht('No more than 7 columns per view.'));
}
}
$classes[] = 'aphront-multi-column-'.count($this->columns).'-up';
$columns = array();
$i = 0;
foreach ($this->columns as $column_data) {
$column_class = array('aphront-multi-column-column');
if ($this->gutter) {
$column_class[] = $this->gutter;
}
$outer_class = array('aphront-multi-column-column-outer');
if (++$i === count($this->columns)) {
$column_class[] = 'aphront-multi-column-column-last';
$outer_class[] = 'aphront-multi-colum-column-outer-last';
}
$column = $column_data['column'];
if ($column_data['class']) {
$outer_class[] = $column_data['class'];
}
$column_sigil = idx($column_data, 'sigil');
$column_metadata = idx($column_data, 'metadata');
$column_inner = javelin_tag(
'div',
array(
'class' => implode(' ', $column_class),
'sigil' => $column_sigil,
'meta' => $column_metadata,
),
$column);
$columns[] = phutil_tag(
'div',
array(
'class' => implode(' ', $outer_class),
),
$column_inner);
}
$view = phutil_tag(
'div',
array(
'class' => implode(' ', $classes),
),
array(
$columns,
));
$classes = array();
$classes[] = 'aphront-multi-column-outer';
if ($this->fluidLayout) {
$classes[] = 'aphront-multi-column-fluid';
if ($this->fluidishLayout) {
$classes[] = 'aphront-multi-column-fluidish';
}
} else {
$classes[] = 'aphront-multi-column-fixed';
}
$board = phutil_tag(
'div',
array(
'class' => implode(' ', $classes),
),
$view);
if ($this->border) {
$board = id(new PHUIBoxView())
->setBorder(true)
->appendChild($board)
->addPadding(PHUI::PADDING_MEDIUM_TOP)
->addPadding(PHUI::PADDING_MEDIUM_BOTTOM);
}
return javelin_tag(
'div',
array(
'class' => 'aphront-multi-column-view',
'id' => $this->getID(),
// TODO: It would be nice to convert this to an AphrontTagView and
// use addSigil() from Workboards instead of hard-coding this.
'sigil' => 'aphront-multi-column-view',
),
$board);
}
}
diff --git a/src/view/layout/PhabricatorActionView.php b/src/view/layout/PhabricatorActionView.php
index 65dd5f146..43f84495c 100644
--- a/src/view/layout/PhabricatorActionView.php
+++ b/src/view/layout/PhabricatorActionView.php
@@ -1,204 +1,206 @@
<?php
final class PhabricatorActionView extends AphrontView {
private $name;
private $icon;
private $href;
private $disabled;
private $label;
private $workflow;
private $renderAsForm;
private $download;
private $objectURI;
private $sigils = array();
private $metadata;
private $selected;
public function setSelected($selected) {
$this->selected = $selected;
return $this;
}
public function getSelected() {
return $this->selected;
}
public function setMetadata($metadata) {
$this->metadata = $metadata;
return $this;
}
public function getMetadata() {
return $this->metadata;
}
public function setObjectURI($object_uri) {
$this->objectURI = $object_uri;
return $this;
}
public function getObjectURI() {
return $this->objectURI;
}
public function setDownload($download) {
$this->download = $download;
return $this;
}
public function getDownload() {
return $this->download;
}
public function setHref($href) {
$this->href = $href;
return $this;
}
public function addSigil($sigil) {
$this->sigils[] = $sigil;
return $this;
}
/**
* If the user is not logged in and the action is relatively complicated,
* give them a generic login link that will re-direct to the page they're
* viewing.
*/
public function getHref() {
if (($this->workflow || $this->renderAsForm) && !$this->download) {
if (!$this->user || !$this->user->isLoggedIn()) {
return id(new PhutilURI('/auth/start/'))
->setQueryParam('next', (string)$this->getObjectURI());
}
}
return $this->href;
}
public function setIcon($icon) {
$this->icon = $icon;
return $this;
}
public function setName($name) {
$this->name = $name;
return $this;
}
public function setLabel($label) {
$this->label = $label;
return $this;
}
public function setDisabled($disabled) {
$this->disabled = $disabled;
return $this;
}
public function setWorkflow($workflow) {
$this->workflow = $workflow;
return $this;
}
public function setRenderAsForm($form) {
$this->renderAsForm = $form;
return $this;
}
public function render() {
$icon = null;
if ($this->icon) {
$color = '';
if ($this->disabled) {
$color = ' grey';
}
$icon = id(new PHUIIconView())
->addClass('phabricator-action-view-icon')
->setIconFont($this->icon.$color);
}
if ($this->href) {
$sigils = array();
if ($this->workflow) {
$sigils[] = 'workflow';
}
if ($this->download) {
$sigils[] = 'download';
}
if ($this->sigils) {
$sigils = array_merge($sigils, $this->sigils);
}
$sigils = $sigils ? implode(' ', $sigils) : null;
if ($this->renderAsForm) {
if (!$this->user) {
throw new Exception(
- 'Call setUser() when rendering an action as a form.');
+ pht(
+ 'Call %s when rendering an action as a form.',
+ 'setUser()'));
}
$item = javelin_tag(
'button',
array(
'class' => 'phabricator-action-view-item',
),
array($icon, $this->name));
$item = phabricator_form(
$this->user,
array(
'action' => $this->getHref(),
'method' => 'POST',
'sigil' => $sigils,
- 'meta' => $this->metadata,
+ 'meta' => $this->metadata,
),
$item);
} else {
$item = javelin_tag(
'a',
array(
'href' => $this->getHref(),
'class' => 'phabricator-action-view-item',
'sigil' => $sigils,
'meta' => $this->metadata,
),
array($icon, $this->name));
}
} else {
$item = phutil_tag(
'span',
array(
'class' => 'phabricator-action-view-item',
),
array($icon, $this->name));
}
$classes = array();
$classes[] = 'phabricator-action-view';
if ($this->disabled) {
$classes[] = 'phabricator-action-view-disabled';
}
if ($this->label) {
$classes[] = 'phabricator-action-view-label';
}
if ($this->selected) {
$classes[] = 'phabricator-action-view-selected';
}
return phutil_tag(
'li',
array(
'class' => implode(' ', $classes),
),
$item);
}
}
diff --git a/src/view/page/PhabricatorStandardPageView.php b/src/view/page/PhabricatorStandardPageView.php
index 7c5dcaf1c..8033ef3c7 100644
--- a/src/view/page/PhabricatorStandardPageView.php
+++ b/src/view/page/PhabricatorStandardPageView.php
@@ -1,722 +1,723 @@
<?php
/**
* This is a standard Phabricator page with menus, Javelin, DarkConsole, and
* basic styles.
*/
final class PhabricatorStandardPageView extends PhabricatorBarePageView {
private $baseURI;
private $applicationName;
private $glyph;
private $menuContent;
private $showChrome = true;
private $disableConsole;
private $pageObjects = array();
private $applicationMenu;
private $showFooter = true;
private $showDurableColumn = true;
public function setShowFooter($show_footer) {
$this->showFooter = $show_footer;
return $this;
}
public function getShowFooter() {
return $this->showFooter;
}
public function setApplicationMenu(PHUIListView $application_menu) {
$this->applicationMenu = $application_menu;
return $this;
}
public function getApplicationMenu() {
return $this->applicationMenu;
}
public function setApplicationName($application_name) {
$this->applicationName = $application_name;
return $this;
}
public function setDisableConsole($disable) {
$this->disableConsole = $disable;
return $this;
}
public function getApplicationName() {
return $this->applicationName;
}
public function setBaseURI($base_uri) {
$this->baseURI = $base_uri;
return $this;
}
public function getBaseURI() {
return $this->baseURI;
}
public function setShowChrome($show_chrome) {
$this->showChrome = $show_chrome;
return $this;
}
public function getShowChrome() {
return $this->showChrome;
}
public function appendPageObjects(array $objs) {
foreach ($objs as $obj) {
$this->pageObjects[] = $obj;
}
}
public function setShowDurableColumn($show) {
$this->showDurableColumn = $show;
return $this;
}
public function getShowDurableColumn() {
$request = $this->getRequest();
if (!$request) {
return false;
}
$viewer = $request->getUser();
if (!$viewer->isLoggedIn()) {
return false;
}
$conpherence_installed = PhabricatorApplication::isClassInstalledForViewer(
'PhabricatorConpherenceApplication',
$viewer);
if (!$conpherence_installed) {
return false;
}
if ($this->isQuicksandBlacklistURI()) {
return false;
}
return true;
}
private function isQuicksandBlacklistURI() {
$request = $this->getRequest();
if (!$request) {
return false;
}
$patterns = $this->getQuicksandURIPatternBlacklist();
$path = $request->getRequestURI()->getPath();
foreach ($patterns as $pattern) {
if (preg_match('(^'.$pattern.'$)', $path)) {
return true;
}
}
return false;
}
public function getDurableColumnVisible() {
$column_key = PhabricatorUserPreferences::PREFERENCE_CONPHERENCE_COLUMN;
return (bool)$this->getUserPreference($column_key, 0);
}
public function getTitle() {
$glyph_key = PhabricatorUserPreferences::PREFERENCE_TITLES;
if ($this->getUserPreference($glyph_key) == 'text') {
$use_glyph = false;
} else {
$use_glyph = true;
}
$title = parent::getTitle();
$prefix = null;
if ($use_glyph) {
$prefix = $this->getGlyph();
} else {
$application_name = $this->getApplicationName();
if (strlen($application_name)) {
$prefix = '['.$application_name.']';
}
}
if (strlen($prefix)) {
$title = $prefix.' '.$title;
}
return $title;
}
protected function willRenderPage() {
parent::willRenderPage();
if (!$this->getRequest()) {
throw new Exception(
pht(
- 'You must set the Request to render a %s.',
+ 'You must set the %s to render a %s.',
+ 'Request',
__CLASS__));
}
$console = $this->getConsole();
require_celerity_resource('phabricator-core-css');
require_celerity_resource('phabricator-zindex-css');
require_celerity_resource('phui-button-css');
require_celerity_resource('phui-spacing-css');
require_celerity_resource('phui-form-css');
require_celerity_resource('sprite-gradient-css');
require_celerity_resource('phabricator-standard-page-view');
require_celerity_resource('conpherence-durable-column-view');
Javelin::initBehavior('workflow', array());
$request = $this->getRequest();
$user = null;
if ($request) {
$user = $request->getUser();
}
if ($user) {
$default_img_uri =
celerity_get_resource_uri(
'rsrc/image/icon/fatcow/document_black.png');
$download_form = phabricator_form(
$user,
array(
'action' => '#',
'method' => 'POST',
'class' => 'lightbox-download-form',
'sigil' => 'download',
),
phutil_tag(
'button',
array(),
pht('Download')));
Javelin::initBehavior(
'lightbox-attachments',
array(
'defaultImageUri' => $default_img_uri,
'downloadForm' => $download_form,
));
}
Javelin::initBehavior('aphront-form-disable-on-submit');
Javelin::initBehavior('toggle-class', array());
Javelin::initBehavior('history-install');
Javelin::initBehavior('phabricator-gesture');
$current_token = null;
if ($user) {
$current_token = $user->getCSRFToken();
}
Javelin::initBehavior(
'refresh-csrf',
array(
'tokenName' => AphrontRequest::getCSRFTokenName(),
'header' => AphrontRequest::getCSRFHeaderName(),
'current' => $current_token,
));
Javelin::initBehavior('device');
Javelin::initBehavior(
'high-security-warning',
$this->getHighSecurityWarningConfig());
if ($console) {
require_celerity_resource('aphront-dark-console-css');
$headers = array();
if (DarkConsoleXHProfPluginAPI::isProfilerStarted()) {
$headers[DarkConsoleXHProfPluginAPI::getProfilerHeader()] = 'page';
}
if (DarkConsoleServicesPlugin::isQueryAnalyzerRequested()) {
$headers[DarkConsoleServicesPlugin::getQueryAnalyzerHeader()] = true;
}
Javelin::initBehavior(
'dark-console',
$this->getConsoleConfig());
// Change this to initBehavior when there is some behavior to initialize
require_celerity_resource('javelin-behavior-error-log');
}
if ($user) {
$viewer = $user;
} else {
$viewer = new PhabricatorUser();
}
$menu = id(new PhabricatorMainMenuView())
->setUser($viewer);
if ($this->getController()) {
$menu->setController($this->getController());
}
if ($this->getApplicationMenu()) {
$menu->setApplicationMenu($this->getApplicationMenu());
}
$this->menuContent = $menu->render();
}
protected function getHead() {
$monospaced = null;
$request = $this->getRequest();
if ($request) {
$user = $request->getUser();
if ($user) {
$monospaced = $user->loadPreferences()->getPreference(
PhabricatorUserPreferences::PREFERENCE_MONOSPACED);
}
}
$response = CelerityAPI::getStaticResourceResponse();
$font_css = null;
if (!empty($monospaced)) {
// We can't print this normally because escaping quotation marks will
// break the CSS. Instead, filter it strictly and then mark it as safe.
$monospaced = new PhutilSafeHTML(
PhabricatorUserPreferences::filterMonospacedCSSRule(
$monospaced));
$font_css = hsprintf(
'<style type="text/css">'.
'.PhabricatorMonospaced, '.
'.phabricator-remarkup .remarkup-code-block '.
'.remarkup-code { font: %s !important; } '.
'</style>',
$monospaced);
}
return hsprintf(
'%s%s%s',
parent::getHead(),
$font_css,
$response->renderSingleResource('javelin-magical-init', 'phabricator'));
}
public function setGlyph($glyph) {
$this->glyph = $glyph;
return $this;
}
public function getGlyph() {
return $this->glyph;
}
protected function willSendResponse($response) {
$request = $this->getRequest();
$response = parent::willSendResponse($response);
$console = $request->getApplicationConfiguration()->getConsole();
if ($console) {
$response = PhutilSafeHTML::applyFunction(
'str_replace',
hsprintf('<darkconsole />'),
$console->render($request),
$response);
}
return $response;
}
protected function getBody() {
$user = null;
$request = $this->getRequest();
if ($request) {
$user = $request->getUser();
}
$header_chrome = null;
if ($this->getShowChrome()) {
$header_chrome = $this->menuContent;
}
$classes = array();
$classes[] = 'main-page-frame';
$developer_warning = null;
if (PhabricatorEnv::getEnvConfig('phabricator.developer-mode') &&
DarkConsoleErrorLogPluginAPI::getErrors()) {
$developer_warning = phutil_tag_div(
'aphront-developer-error-callout',
pht(
'This page raised PHP errors. Find them in DarkConsole '.
'or the error log.'));
}
// Render the "you have unresolved setup issues..." warning.
$setup_warning = null;
if ($user && $user->getIsAdmin()) {
$open = PhabricatorSetupCheck::getOpenSetupIssueKeys();
if ($open) {
$classes[] = 'page-has-warning';
$setup_warning = phutil_tag_div(
'setup-warning-callout',
phutil_tag(
'a',
array(
'href' => '/config/issue/',
'title' => implode(', ', $open),
),
pht('You have %d unresolved setup issue(s)...', count($open))));
}
}
$main_page = phutil_tag(
'div',
array(
'id' => 'phabricator-standard-page',
'class' => 'phabricator-standard-page',
),
array(
$developer_warning,
$header_chrome,
$setup_warning,
phutil_tag(
'div',
array(
'id' => 'phabricator-standard-page-body',
'class' => 'phabricator-standard-page-body',
),
$this->renderPageBodyContent()),
));
$durable_column = null;
if ($this->getShowDurableColumn()) {
$is_visible = $this->getDurableColumnVisible();
$durable_column = id(new ConpherenceDurableColumnView())
->setSelectedConpherence(null)
->setUser($user)
->setQuicksandConfig($this->buildQuicksandConfig())
->setVisible($is_visible)
->setInitialLoad(true);
}
Javelin::initBehavior('quicksand-blacklist', array(
'patterns' => $this->getQuicksandURIPatternBlacklist(),
));
return phutil_tag(
'div',
array(
'class' => implode(' ', $classes),
),
array(
$main_page,
$durable_column,
));
}
private function renderPageBodyContent() {
$console = $this->getConsole();
return array(
($console ? hsprintf('<darkconsole />') : null),
parent::getBody(),
$this->renderFooter(),
);
}
protected function getTail() {
$request = $this->getRequest();
$user = $request->getUser();
$tail = array(
parent::getTail(),
);
$response = CelerityAPI::getStaticResourceResponse();
if (PhabricatorEnv::getEnvConfig('notification.enabled')) {
if ($user && $user->isLoggedIn()) {
$client_uri = PhabricatorEnv::getEnvConfig('notification.client-uri');
$client_uri = new PhutilURI($client_uri);
if ($client_uri->getDomain() == 'localhost') {
$this_host = $this->getRequest()->getHost();
$this_host = new PhutilURI('http://'.$this_host.'/');
$client_uri->setDomain($this_host->getDomain());
}
if ($request->isHTTPS()) {
$client_uri->setProtocol('wss');
} else {
$client_uri->setProtocol('ws');
}
Javelin::initBehavior(
'aphlict-listen',
array(
'websocketURI' => (string)$client_uri,
) + $this->buildAphlictListenConfigData());
}
}
$tail[] = $response->renderHTMLFooter();
return $tail;
}
protected function getBodyClasses() {
$classes = array();
if (!$this->getShowChrome()) {
$classes[] = 'phabricator-chromeless-page';
}
$agent = AphrontRequest::getHTTPHeader('User-Agent');
// Try to guess the device resolution based on UA strings to avoid a flash
// of incorrectly-styled content.
$device_guess = 'device-desktop';
if (preg_match('@iPhone|iPod|(Android.*Chrome/[.0-9]* Mobile)@', $agent)) {
$device_guess = 'device-phone device';
} else if (preg_match('@iPad|(Android.*Chrome/)@', $agent)) {
$device_guess = 'device-tablet device';
}
$classes[] = $device_guess;
if (preg_match('@Windows@', $agent)) {
$classes[] = 'platform-windows';
} else if (preg_match('@Macintosh@', $agent)) {
$classes[] = 'platform-mac';
} else if (preg_match('@X11@', $agent)) {
$classes[] = 'platform-linux';
}
if ($this->getRequest()->getStr('__print__')) {
$classes[] = 'printable';
}
if ($this->getRequest()->getStr('__aural__')) {
$classes[] = 'audible';
}
return implode(' ', $classes);
}
private function getConsole() {
if ($this->disableConsole) {
return null;
}
return $this->getRequest()->getApplicationConfiguration()->getConsole();
}
private function getConsoleConfig() {
$user = $this->getRequest()->getUser();
$headers = array();
if (DarkConsoleXHProfPluginAPI::isProfilerStarted()) {
$headers[DarkConsoleXHProfPluginAPI::getProfilerHeader()] = 'page';
}
if (DarkConsoleServicesPlugin::isQueryAnalyzerRequested()) {
$headers[DarkConsoleServicesPlugin::getQueryAnalyzerHeader()] = true;
}
return array(
// NOTE: We use a generic label here to prevent input reflection
// and mitigate compression attacks like BREACH. See discussion in
// T3684.
'uri' => pht('Main Request'),
'selected' => $user ? $user->getConsoleTab() : null,
'visible' => $user ? (int)$user->getConsoleVisible() : true,
'headers' => $headers,
);
}
private function getHighSecurityWarningConfig() {
$user = $this->getRequest()->getUser();
$show = false;
if ($user->hasSession()) {
$hisec = ($user->getSession()->getHighSecurityUntil() - time());
if ($hisec > 0) {
$show = true;
}
}
return array(
'show' => $show,
'uri' => '/auth/session/downgrade/',
'message' => pht(
'Your session is in high security mode. When you '.
'finish using it, click here to leave.'),
);
}
private function renderFooter() {
if (!$this->getShowChrome()) {
return null;
}
if (!$this->getShowFooter()) {
return null;
}
$items = PhabricatorEnv::getEnvConfig('ui.footer-items');
if (!$items) {
return null;
}
$foot = array();
foreach ($items as $item) {
$name = idx($item, 'name', pht('Unnamed Footer Item'));
$href = idx($item, 'href');
if (!PhabricatorEnv::isValidURIForLink($href)) {
$href = null;
}
if ($href !== null) {
$tag = 'a';
} else {
$tag = 'span';
}
$foot[] = phutil_tag(
$tag,
array(
'href' => $href,
),
$name);
}
$foot = phutil_implode_html(" \xC2\xB7 ", $foot);
return phutil_tag(
'div',
array(
'class' => 'phabricator-standard-page-footer grouped',
),
$foot);
}
public function renderForQuicksand(array $extra_config) {
parent::willRenderPage();
$response = $this->renderPageBodyContent();
$response = $this->willSendResponse($response);
return array(
'content' => hsprintf('%s', $response),
) + $this->buildQuicksandConfig()
+ $extra_config;
}
private function buildQuicksandConfig() {
$viewer = $this->getRequest()->getUser();
$controller = $this->getController();
$dropdown_query = id(new AphlictDropdownDataQuery())
->setViewer($viewer);
$dropdown_query->execute();
$rendered_dropdowns = array();
$applications = array(
'PhabricatorHelpApplication',
);
foreach ($applications as $application_class) {
if (!PhabricatorApplication::isClassInstalledForViewer(
$application_class,
$viewer)) {
continue;
}
$application = PhabricatorApplication::getByClass($application_class);
$rendered_dropdowns[$application_class] =
$application->buildMainMenuExtraNodes(
$viewer,
$controller);
}
$hisec_warning_config = $this->getHighSecurityWarningConfig();
$console_config = null;
$console = $this->getConsole();
if ($console) {
$console_config = $this->getConsoleConfig();
}
$upload_enabled = false;
if ($controller) {
$upload_enabled = $controller->isGlobalDragAndDropUploadEnabled();
}
$application_class = null;
$application_search_icon = null;
$controller = $this->getController();
if ($controller) {
$application = $controller->getCurrentApplication();
if ($application) {
$application_class = get_class($application);
if ($application->getApplicationSearchDocumentTypes()) {
$application_search_icon = $application->getFontIcon();
}
}
}
return array(
'title' => $this->getTitle(),
'aphlictDropdownData' => array(
$dropdown_query->getNotificationData(),
$dropdown_query->getConpherenceData(),
),
'globalDragAndDrop' => $upload_enabled,
'aphlictDropdowns' => $rendered_dropdowns,
'hisecWarningConfig' => $hisec_warning_config,
'consoleConfig' => $console_config,
'applicationClass' => $application_class,
'applicationSearchIcon' => $application_search_icon,
) + $this->buildAphlictListenConfigData();
}
private function buildAphlictListenConfigData() {
$user = $this->getRequest()->getUser();
$subscriptions = $this->pageObjects;
$subscriptions[] = $user->getPHID();
return array(
'pageObjects' => array_fill_keys($this->pageObjects, true),
'subscriptions' => $subscriptions,
);
}
private function getQuicksandURIPatternBlacklist() {
$applications = PhabricatorApplication::getAllApplications();
$blacklist = array();
foreach ($applications as $application) {
$blacklist[] = $application->getQuicksandURIPatternBlacklist();
}
return array_mergev($blacklist);
}
private function getUserPreference($key, $default = null) {
$request = $this->getRequest();
if (!$request) {
return $default;
}
$user = $request->getUser();
if (!$user) {
return $default;
}
return $user->loadPreferences()->getPreference($key, $default);
}
}
diff --git a/src/view/phui/PHUIHeaderView.php b/src/view/phui/PHUIHeaderView.php
index 14159e500..c87f1c486 100644
--- a/src/view/phui/PHUIHeaderView.php
+++ b/src/view/phui/PHUIHeaderView.php
@@ -1,297 +1,297 @@
<?php
final class PHUIHeaderView extends AphrontTagView {
const PROPERTY_STATUS = 1;
private $objectName;
private $header;
private $tags = array();
private $image;
private $imageURL = null;
private $subheader;
private $headerColor;
private $noBackground;
private $bleedHeader;
private $properties = array();
private $actionLinks = array();
private $buttonBar = null;
private $policyObject;
private $epoch;
public function setHeader($header) {
$this->header = $header;
return $this;
}
public function setObjectName($object_name) {
$this->objectName = $object_name;
return $this;
}
public function setNoBackground($nada) {
$this->noBackground = $nada;
return $this;
}
public function addTag(PHUITagView $tag) {
$this->tags[] = $tag;
return $this;
}
public function setImage($uri) {
$this->image = $uri;
return $this;
}
public function setImageURL($url) {
$this->imageURL = $url;
return $this;
}
public function setSubheader($subheader) {
$this->subheader = $subheader;
return $this;
}
public function setBleedHeader($bleed) {
$this->bleedHeader = $bleed;
return $this;
}
public function setHeaderColor($color) {
$this->headerColor = $color;
return $this;
}
public function setPolicyObject(PhabricatorPolicyInterface $object) {
$this->policyObject = $object;
return $this;
}
public function addProperty($property, $value) {
$this->properties[$property] = $value;
return $this;
}
public function addActionLink(PHUIButtonView $button) {
$this->actionLinks[] = $button;
return $this;
}
public function setButtonBar(PHUIButtonBarView $bb) {
$this->buttonBar = $bb;
return $this;
}
public function setStatus($icon, $color, $name) {
$header_class = 'phui-header-status';
if ($color) {
$icon = $icon.' '.$color;
$header_class = $header_class.'-'.$color;
}
$img = id(new PHUIIconView())
->setIconFont($icon);
$tag = phutil_tag(
'span',
array(
'class' => "{$header_class} plr",
),
array(
$img,
$name,
));
return $this->addProperty(self::PROPERTY_STATUS, $tag);
}
public function setEpoch($epoch) {
$age = time() - $epoch;
$age = floor($age / (60 * 60 * 24));
if ($age < 1) {
$when = pht('Today');
} else if ($age == 1) {
$when = pht('Yesterday');
} else {
$when = pht('%d Days Ago', $age);
}
$this->setStatus('fa-clock-o bluegrey', null, pht('Updated %s', $when));
return $this;
}
protected function getTagName() {
return 'div';
}
protected function getTagAttributes() {
require_celerity_resource('phui-header-view-css');
$classes = array();
$classes[] = 'phui-header-shell';
if ($this->noBackground) {
$classes[] = 'phui-header-no-backgound';
}
if ($this->bleedHeader) {
$classes[] = 'phui-bleed-header';
}
if ($this->headerColor) {
$classes[] = 'sprite-gradient';
$classes[] = 'gradient-'.$this->headerColor.'-header';
}
if ($this->properties || $this->policyObject || $this->subheader) {
$classes[] = 'phui-header-tall';
}
if ($this->image) {
$classes[] = 'phui-header-has-image';
}
return array(
'class' => $classes,
);
}
protected function getTagContent() {
$image = null;
if ($this->image) {
$image = phutil_tag(
($this->imageURL ? 'a' : 'span'),
array(
'href' => $this->imageURL,
'class' => 'phui-header-image',
'style' => 'background-image: url('.$this->image.')',
),
' ');
}
$header = array();
if ($this->actionLinks) {
$actions = array();
foreach ($this->actionLinks as $button) {
$button->setColor(PHUIButtonView::SIMPLE);
$button->addClass(PHUI::MARGIN_SMALL_LEFT);
$button->addClass('phui-header-action-link');
$actions[] = $button;
}
$header[] = phutil_tag(
'div',
array(
'class' => 'phui-header-action-links',
),
$actions);
}
if ($this->buttonBar) {
$header[] = phutil_tag(
'div',
array(
'class' => 'phui-header-action-links',
),
$this->buttonBar);
}
$header[] = $this->header;
if ($this->objectName) {
array_unshift(
$header,
phutil_tag(
'a',
array(
'href' => '/'.$this->objectName,
),
$this->objectName),
' ');
}
if ($this->tags) {
$header[] = ' ';
$header[] = phutil_tag(
'span',
array(
'class' => 'phui-header-tags',
),
array_interleave(' ', $this->tags));
}
if ($this->subheader) {
$header[] = phutil_tag(
'div',
array(
'class' => 'phui-header-subheader',
),
$this->subheader);
}
if ($this->properties || $this->policyObject) {
$property_list = array();
foreach ($this->properties as $type => $property) {
switch ($type) {
case self::PROPERTY_STATUS:
$property_list[] = $property;
break;
default:
- throw new Exception('Incorrect Property Passed');
+ throw new Exception(pht('Incorrect Property Passed'));
break;
}
}
if ($this->policyObject) {
$property_list[] = $this->renderPolicyProperty($this->policyObject);
}
$header[] = phutil_tag(
'div',
array(
'class' => 'phui-header-subheader',
),
$property_list);
}
return array(
$image,
phutil_tag(
'h1',
array(
'class' => 'phui-header-view grouped',
),
$header),
);
}
private function renderPolicyProperty(PhabricatorPolicyInterface $object) {
$policies = PhabricatorPolicyQuery::loadPolicies(
$this->getUser(),
$object);
$view_capability = PhabricatorPolicyCapability::CAN_VIEW;
$policy = idx($policies, $view_capability);
if (!$policy) {
return null;
}
$phid = $object->getPHID();
$icon = id(new PHUIIconView())
->setIconFont($policy->getIcon().' bluegrey');
$link = javelin_tag(
'a',
array(
'class' => 'policy-link',
'href' => '/policy/explain/'.$phid.'/'.$view_capability.'/',
'sigil' => 'workflow',
),
$policy->getShortName());
return array($icon, $link);
}
}
diff --git a/src/view/phui/PHUIObjectItemView.php b/src/view/phui/PHUIObjectItemView.php
index cc2753e26..812fa993c 100644
--- a/src/view/phui/PHUIObjectItemView.php
+++ b/src/view/phui/PHUIObjectItemView.php
@@ -1,691 +1,689 @@
<?php
final class PHUIObjectItemView extends AphrontTagView {
private $objectName;
private $header;
private $subhead;
private $href;
private $attributes = array();
private $icons = array();
private $barColor;
private $object;
private $effect;
private $footIcons = array();
private $handleIcons = array();
private $bylines = array();
private $grippable;
private $actions = array();
private $headIcons = array();
private $disabled;
private $imageURI;
private $state;
private $fontIcon;
private $imageIcon;
private $titleText;
const AGE_FRESH = 'fresh';
const AGE_STALE = 'stale';
const AGE_OLD = 'old';
const STATE_SUCCESS = 'green';
const STATE_FAIL = 'red';
const STATE_WARN = 'yellow';
const STATE_NOTE = 'blue';
const STATE_BUILD = 'sky';
public function setDisabled($disabled) {
$this->disabled = $disabled;
return $this;
}
public function getDisabled() {
return $this->disabled;
}
public function addHeadIcon($icon) {
$this->headIcons[] = $icon;
return $this;
}
public function setObjectName($name) {
$this->objectName = $name;
return $this;
}
public function setGrippable($grippable) {
$this->grippable = $grippable;
return $this;
}
public function getGrippable() {
return $this->grippable;
}
public function setEffect($effect) {
$this->effect = $effect;
return $this;
}
public function getEffect() {
return $this->effect;
}
public function setObject($object) {
$this->object = $object;
return $this;
}
public function getObject() {
return $this->object;
}
public function setHref($href) {
$this->href = $href;
return $this;
}
public function getHref() {
return $this->href;
}
public function setHeader($header) {
$this->header = $header;
return $this;
}
public function setSubHead($subhead) {
$this->subhead = $subhead;
return $this;
}
public function setTitleText($title_text) {
$this->titleText = $title_text;
return $this;
}
public function getTitleText() {
return $this->titleText;
}
public function getHeader() {
return $this->header;
}
public function addByline($byline) {
$this->bylines[] = $byline;
return $this;
}
public function setImageURI($image_uri) {
$this->imageURI = $image_uri;
return $this;
}
public function getImageURI() {
return $this->imageURI;
}
public function setImageIcon($image_icon) {
$this->imageIcon = $image_icon;
return $this;
}
public function getImageIcon() {
return $this->imageIcon;
}
public function setState($state) {
$this->state = $state;
switch ($state) {
case self::STATE_SUCCESS:
$fi = 'fa-check-circle green';
break;
case self::STATE_FAIL:
$fi = 'fa-times-circle red';
break;
case self::STATE_WARN:
$fi = 'fa-exclamation-circle yellow';
break;
case self::STATE_NOTE:
$fi = 'fa-info-circle blue';
break;
case self::STATE_BUILD:
$fi = 'fa-refresh ph-spin sky';
break;
}
$this->setFontIcon($fi);
return $this;
}
public function setFontIcon($icon) {
$this->fontIcon = id(new PHUIIconView())
->setIconFont($icon);
return $this;
}
public function setEpoch($epoch, $age = self::AGE_FRESH) {
$date = phabricator_datetime($epoch, $this->getUser());
$days = floor((time() - $epoch) / 60 / 60 / 24);
switch ($age) {
case self::AGE_FRESH:
$this->addIcon('none', $date);
break;
case self::AGE_STALE:
$attr = array(
'tip' => pht('Stale (%s day(s))', new PhutilNumber($days)),
'class' => 'icon-age-stale',
);
$this->addIcon('fa-clock-o yellow', $date, $attr);
break;
case self::AGE_OLD:
$attr = array(
'tip' => pht('Old (%s day(s))', new PhutilNumber($days)),
'class' => 'icon-age-old',
);
$this->addIcon('fa-clock-o red', $date, $attr);
break;
default:
- throw new Exception("Unknown age '{$age}'!");
+ throw new Exception(pht("Unknown age '%s'!", $age));
}
return $this;
}
public function addAction(PHUIListItemView $action) {
if (count($this->actions) >= 3) {
- throw new Exception('Limit 3 actions per item.');
+ throw new Exception(pht('Limit 3 actions per item.'));
}
$this->actions[] = $action;
return $this;
}
public function addIcon($icon, $label = null, $attributes = array()) {
$this->icons[] = array(
'icon' => $icon,
'label' => $label,
'attributes' => $attributes,
);
return $this;
}
public function addFootIcon($icon, $label = null) {
$this->footIcons[] = array(
'icon' => $icon,
'label' => $label,
);
return $this;
}
public function addHandleIcon(
PhabricatorObjectHandle $handle,
$label = null) {
$this->handleIcons[] = array(
'icon' => $handle,
'label' => $label,
);
return $this;
}
public function setBarColor($bar_color) {
$this->barColor = $bar_color;
return $this;
}
public function getBarColor() {
return $this->barColor;
}
public function addAttribute($attribute) {
if (!empty($attribute)) {
$this->attributes[] = $attribute;
}
return $this;
}
protected function getTagName() {
return 'li';
}
protected function getTagAttributes() {
$item_classes = array();
$item_classes[] = 'phui-object-item';
if ($this->icons) {
$item_classes[] = 'phui-object-item-with-icons';
}
if ($this->attributes) {
$item_classes[] = 'phui-object-item-with-attrs';
}
if ($this->handleIcons) {
$item_classes[] = 'phui-object-item-with-handle-icons';
}
if ($this->barColor) {
$item_classes[] = 'phui-object-item-bar-color-'.$this->barColor;
}
if ($this->footIcons) {
$item_classes[] = 'phui-object-item-with-foot-icons';
}
if ($this->actions) {
$n = count($this->actions);
$item_classes[] = 'phui-object-item-with-actions';
$item_classes[] = 'phui-object-item-with-'.$n.'-actions';
}
if ($this->disabled) {
$item_classes[] = 'phui-object-item-disabled';
}
if ($this->state) {
$item_classes[] = 'phui-object-item-state-'.$this->state;
}
switch ($this->effect) {
case 'highlighted':
$item_classes[] = 'phui-object-item-highlighted';
break;
case 'selected':
$item_classes[] = 'phui-object-item-selected';
break;
case null:
break;
default:
throw new Exception(pht('Invalid effect!'));
}
if ($this->getGrippable()) {
$item_classes[] = 'phui-object-item-grippable';
}
if ($this->getImageURI()) {
$item_classes[] = 'phui-object-item-with-image';
}
if ($this->getImageIcon()) {
$item_classes[] = 'phui-object-item-with-image-icon';
}
if ($this->fontIcon) {
$item_classes[] = 'phui-object-item-with-ficon';
}
return array(
'class' => $item_classes,
);
}
protected function getTagContent() {
$content_classes = array();
$content_classes[] = 'phui-object-item-content';
$header_name = null;
if ($this->objectName) {
$header_name = array(
phutil_tag(
'span',
array(
'class' => 'phui-object-item-objname',
),
$this->objectName),
' ',
);
}
$title_text = null;
if ($this->titleText) {
$title_text = $this->titleText;
} else if ($this->href) {
$title_text = $this->header;
}
$header_link = phutil_tag(
$this->href ? 'a' : 'div',
array(
'href' => $this->href,
'class' => 'phui-object-item-link',
'title' => $title_text,
),
$this->header);
$header = javelin_tag(
'div',
array(
'class' => 'phui-object-item-name',
'sigil' => 'slippery',
),
array(
$this->headIcons,
$header_name,
$header_link,
));
$icons = array();
if ($this->icons) {
$icon_list = array();
foreach ($this->icons as $spec) {
$icon = $spec['icon'];
$icon = id(new PHUIIconView())
->setIconFont($icon)
->addClass('phui-object-item-icon-image');
if (isset($spec['attributes']['tip'])) {
$sigil = 'has-tooltip';
$meta = array(
'tip' => $spec['attributes']['tip'],
'align' => 'W',
);
$icon->addSigil($sigil);
$icon->setMetadata($meta);
}
$label = phutil_tag(
'span',
array(
'class' => 'phui-object-item-icon-label',
),
$spec['label']);
if (isset($spec['attributes']['href'])) {
$icon_href = phutil_tag(
'a',
array('href' => $spec['attributes']['href']),
array($icon, $label));
} else {
$icon_href = array($icon, $label);
}
$classes = array();
$classes[] = 'phui-object-item-icon';
if (isset($spec['attributes']['class'])) {
$classes[] = $spec['attributes']['class'];
}
$icon_list[] = javelin_tag(
'li',
array(
'class' => implode(' ', $classes),
),
$icon_href);
}
$icons[] = phutil_tag(
'ul',
array(
'class' => 'phui-object-item-icons',
),
$icon_list);
}
if ($this->handleIcons) {
$handle_bar = array();
foreach ($this->handleIcons as $icon) {
$handle_bar[] = $this->renderHandleIcon($icon['icon'], $icon['label']);
}
$icons[] = phutil_tag(
'div',
array(
'class' => 'phui-object-item-handle-icons',
),
$handle_bar);
}
$bylines = array();
if ($this->bylines) {
foreach ($this->bylines as $byline) {
$bylines[] = phutil_tag(
'div',
array(
'class' => 'phui-object-item-byline',
),
$byline);
}
$bylines = phutil_tag(
'div',
array(
'class' => 'phui-object-item-bylines',
),
$bylines);
}
$subhead = null;
if ($this->subhead) {
$subhead = phutil_tag(
'div',
array(
'class' => 'phui-object-item-subhead',
),
$this->subhead);
}
if ($icons) {
$icons = phutil_tag(
'div',
array(
'class' => 'phui-object-icon-pane',
),
$icons);
}
$attrs = null;
if ($this->attributes) {
$attrs = array();
$spacer = phutil_tag(
'span',
array(
'class' => 'phui-object-item-attribute-spacer',
),
"\xC2\xB7");
$first = true;
foreach ($this->attributes as $attribute) {
$attrs[] = phutil_tag(
'li',
array(
'class' => 'phui-object-item-attribute',
),
array(
($first ? null : $spacer),
$attribute,
));
$first = false;
}
$attrs = phutil_tag(
'ul',
array(
'class' => 'phui-object-item-attributes',
),
$attrs);
}
$foot = null;
if ($this->footIcons) {
$foot_bar = array();
foreach ($this->footIcons as $icon) {
$foot_bar[] = $this->renderFootIcon($icon['icon'], $icon['label']);
}
$foot = phutil_tag(
'div',
array(
'class' => 'phui-object-item-foot-icons',
),
$foot_bar);
}
$grippable = null;
if ($this->getGrippable()) {
$grippable = phutil_tag(
'div',
array(
'class' => 'phui-object-item-grip',
),
'');
}
$content = phutil_tag(
'div',
array(
'class' => implode(' ', $content_classes),
),
array(
$subhead,
$attrs,
$this->renderChildren(),
$foot,
));
$image = null;
if ($this->getImageURI()) {
$image = phutil_tag(
'div',
array(
'class' => 'phui-object-item-image',
'style' => 'background-image: url('.$this->getImageURI().')',
),
'');
} else if ($this->getImageIcon()) {
$image = phutil_tag(
'div',
array(
'class' => 'phui-object-item-image-icon',
),
$this->getImageIcon());
}
$ficon = null;
if ($this->fontIcon) {
$image = phutil_tag(
'div',
array(
'class' => 'phui-object-item-ficon',
),
$this->fontIcon);
}
if ($image && $this->href) {
$image = phutil_tag(
'a',
array(
'href' => $this->href,
),
$image);
}
/* Build a fake table */
$column1 = phutil_tag(
'div',
array(
'class' => 'phui-object-item-col1',
),
array(
$header,
$content,
));
$column2 = null;
if ($icons || $bylines) {
$column2 = phutil_tag(
'div',
array(
'class' => 'phui-object-item-col2',
),
array(
$icons,
$bylines,
));
}
$table = phutil_tag(
'div',
array(
'class' => 'phui-object-item-table',
),
phutil_tag_div('phui-object-item-table-row', array($column1, $column2)));
$box = phutil_tag(
'div',
array(
'class' => 'phui-object-item-content-box',
),
array(
$grippable,
$table,
));
$actions = array();
if ($this->actions) {
Javelin::initBehavior('phabricator-tooltips');
foreach (array_reverse($this->actions) as $action) {
$action->setRenderNameAsTooltip(true);
$actions[] = $action;
}
$actions = phutil_tag(
'ul',
array(
'class' => 'phui-object-item-actions',
),
$actions);
}
return phutil_tag(
'div',
array(
'class' => 'phui-object-item-frame',
),
array(
$actions,
$image,
$box,
));
}
private function renderFootIcon($icon, $label) {
$icon = id(new PHUIIconView())
->setIconFont($icon);
$label = phutil_tag(
'span',
array(
),
$label);
return phutil_tag(
'span',
array(
'class' => 'phui-object-item-foot-icon',
),
array($icon, $label));
}
private function renderHandleIcon(PhabricatorObjectHandle $handle, $label) {
Javelin::initBehavior('phabricator-tooltips');
$options = array(
'class' => 'phui-object-item-handle-icon',
'style' => 'background-image: url('.$handle->getImageURI().')',
);
if (strlen($label)) {
$options['sigil'] = 'has-tooltip';
$options['meta'] = array('tip' => $label);
}
return javelin_tag(
'span',
$options,
'');
}
-
-
}
diff --git a/src/view/widget/hovercard/PhabricatorHovercardView.php b/src/view/widget/hovercard/PhabricatorHovercardView.php
index 58131a6a6..e342fecce 100644
--- a/src/view/widget/hovercard/PhabricatorHovercardView.php
+++ b/src/view/widget/hovercard/PhabricatorHovercardView.php
@@ -1,171 +1,171 @@
<?php
/**
* The default one-for-all hovercard. We may derive from this one to create
- * more specialized ones
+ * more specialized ones.
*/
final class PhabricatorHovercardView extends AphrontView {
/**
* @var PhabricatorObjectHandle
*/
private $handle;
private $title = array();
private $detail;
private $tags = array();
private $fields = array();
private $actions = array();
private $color = 'lightblue';
public function setObjectHandle(PhabricatorObjectHandle $handle) {
$this->handle = $handle;
return $this;
}
public function setTitle($title) {
$this->title = $title;
return $this;
}
public function setDetail($detail) {
$this->detail = $detail;
return $this;
}
public function addField($label, $value) {
$this->fields[] = array(
'label' => $label,
'value' => $value,
);
return $this;
}
public function addAction($label, $uri, $workflow = false) {
$this->actions[] = array(
'label' => $label,
'uri' => $uri,
'workflow' => $workflow,
);
return $this;
}
public function addTag(PHUITagView $tag) {
$this->tags[] = $tag;
return $this;
}
public function setColor($color) {
$this->color = $color;
return $this;
}
public function render() {
if (!$this->handle) {
throw new PhutilInvalidStateException('setObjectHandle');
}
$handle = $this->handle;
require_celerity_resource('phabricator-hovercard-view-css');
$title = pht('%s: %s',
$handle->getTypeName(),
$this->title ? $this->title : $handle->getName());
$header = new PHUIActionHeaderView();
$header->setHeaderColor($this->color);
$header->setHeaderTitle($title);
if ($this->tags) {
foreach ($this->tags as $tag) {
$header->setTag($tag);
}
}
$body = array();
if ($this->detail) {
$body_title = $this->detail;
} else {
// Fallback for object handles
$body_title = $handle->getFullName();
}
$body[] = phutil_tag_div('phabricator-hovercard-body-header', $body_title);
foreach ($this->fields as $field) {
$item = array(
phutil_tag('strong', array(), $field['label']),
' ',
phutil_tag('span', array(), $field['value']),
);
$body[] = phutil_tag_div('phabricator-hovercard-body-item', $item);
}
if ($handle->getImageURI()) {
// Probably a user, we don't need to assume something else
// "Prepend" the image by appending $body
$body = phutil_tag(
'div',
array(
'class' => 'phabricator-hovercard-body-image',
),
phutil_tag(
'div',
array(
'class' => 'profile-header-picture-frame',
'style' => 'background-image: url('.$handle->getImageURI().');',
),
''))
->appendHTML(
phutil_tag(
'div',
array(
'class' => 'phabricator-hovercard-body-details',
),
$body));
}
$buttons = array();
foreach ($this->actions as $action) {
$options = array(
'class' => 'button grey',
'href' => $action['uri'],
);
if ($action['workflow']) {
$options['sigil'] = 'workflow';
$buttons[] = javelin_tag(
'a',
$options,
$action['label']);
} else {
$buttons[] = phutil_tag(
'a',
$options,
$action['label']);
}
}
$tail = null;
if ($buttons) {
$tail = phutil_tag_div('phabricator-hovercard-tail', $buttons);
}
// Assemble container
// TODO: Add color support
$hovercard = phutil_tag_div(
'phabricator-hovercard-container',
array(
phutil_tag_div('phabricator-hovercard-head', $header),
phutil_tag_div('phabricator-hovercard-body grouped', $body),
$tail,
));
// Wrap for thick border
// and later the tip at the bottom
return phutil_tag_div('phabricator-hovercard-wrapper', $hovercard);
}
}
diff --git a/support/aphlict/server/aphlict_launcher.php b/support/aphlict/server/aphlict_launcher.php
index 4af50a778..8a550265b 100755
--- a/support/aphlict/server/aphlict_launcher.php
+++ b/support/aphlict/server/aphlict_launcher.php
@@ -1,23 +1,23 @@
#!/usr/bin/env php
<?php
$root = dirname(dirname(dirname(dirname(__FILE__))));
require_once $root.'/scripts/__init_script__.php';
PhabricatorAphlictManagementWorkflow::requireExtensions();
$args = new PhutilArgumentParser($argv);
-$args->setTagline('manage Aphlict notification server');
+$args->setTagline(pht('manage Aphlict notification server'));
$args->setSynopsis(<<<EOSYNOPSIS
**aphlict** __command__ [__options__]
Manage the Aphlict server.
EOSYNOPSIS
);
$args->parseStandardArguments();
$workflows = id(new PhutilSymbolLoader())
->setAncestorClass('PhabricatorAphlictManagementWorkflow')
->loadObjects();
$workflows[] = new PhutilHelpArgumentWorkflow();
$args->parseWorkflows($workflows);
diff --git a/webroot/index.php b/webroot/index.php
index 7c417194a..6bdcf5147 100644
--- a/webroot/index.php
+++ b/webroot/index.php
@@ -1,37 +1,36 @@
<?php
$phabricator_root = dirname(dirname(__FILE__));
require_once $phabricator_root.'/support/PhabricatorStartup.php';
// If the preamble script exists, load it.
$preamble_path = $phabricator_root.'/support/preamble.php';
if (file_exists($preamble_path)) {
require_once $preamble_path;
}
PhabricatorStartup::didStartup();
try {
PhabricatorStartup::loadCoreLibraries();
$sink = new AphrontPHPHTTPSink();
try {
AphrontApplicationConfiguration::runHTTPRequest($sink);
} catch (Exception $ex) {
try {
$response = new AphrontUnhandledExceptionResponse();
$response->setException($ex);
PhabricatorStartup::endOutputCapture();
$sink->writeResponse($response);
} catch (Exception $response_exception) {
// If we hit a rendering exception, ignore it and throw the original
// exception. It is generally more interesting and more likely to be
// the root cause.
throw $ex;
}
}
-
} catch (Exception $ex) {
PhabricatorStartup::didEncounterFatalException('Core Exception', $ex, false);
}

Event Timeline