Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F2896027
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Advanced/Developer...
View Handle
View Hovercard
Size
101 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/src/applications/herald/controller/HeraldRuleController.php b/src/applications/herald/controller/HeraldRuleController.php
index 5e13522b6e..be8c50f5ba 100644
--- a/src/applications/herald/controller/HeraldRuleController.php
+++ b/src/applications/herald/controller/HeraldRuleController.php
@@ -1,740 +1,739 @@
<?php
final class HeraldRuleController extends HeraldController {
public function handleRequest(AphrontRequest $request) {
$viewer = $request->getViewer();
$id = $request->getURIData('id');
$content_type_map = HeraldAdapter::getEnabledAdapterMap($viewer);
$rule_type_map = HeraldRuleTypeConfig::getRuleTypeMap();
if ($id) {
$rule = id(new HeraldRuleQuery())
->setViewer($viewer)
->withIDs(array($id))
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
if (!$rule) {
return new Aphront404Response();
}
$cancel_uri = '/'.$rule->getMonogram();
} else {
$new_uri = $this->getApplicationURI('new/');
$rule = new HeraldRule();
$rule->setAuthorPHID($viewer->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])) {
return $this->newDialog()
->setTitle(pht('Invalid Rule Type'))
->appendParagraph(
pht(
'The selected rule type ("%s") is not recognized by Herald.',
$rule_type))
->addCancelButton($new_uri);
}
$rule->setRuleType($rule_type);
try {
$adapter = HeraldAdapter::getAdapterForContentType(
$rule->getContentType());
} catch (Exception $ex) {
return $this->newDialog()
->setTitle(pht('Invalid Content Type'))
->appendParagraph(
pht(
'The selected content type ("%s") is not recognized by '.
'Herald.',
$rule->getContentType()))
->addCancelButton($new_uri);
}
if (!$adapter->supportsRuleType($rule->getRuleType())) {
return $this->newDialog()
->setTitle(pht('Rule/Content Mismatch'))
->appendParagraph(
pht(
'The selected rule type ("%s") is not supported by the selected '.
'content type ("%s").',
$rule->getRuleType(),
$rule->getContentType()))
->addCancelButton($new_uri);
}
if ($rule->isObjectRule()) {
$rule->setTriggerObjectPHID($request->getStr('targetPHID'));
$object = id(new PhabricatorObjectQuery())
->setViewer($viewer)
->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.'));
+ 'view or edit it in this older version. Upgrade your software.'));
}
// 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 = '/'.$rule->getMonogram();
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($viewer)
->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 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 button-green',
'sigil' => 'create-action',
'mustcapture' => true,
),
pht('New Action')))
->setDescription(pht(
'Take these actions %s',
$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: %s', $rule->getName())
: pht('Create Herald Rule: %s', idx($content_type_map, $content_type));
$form_box = id(new PHUIObjectBoxView())
->setHeaderText($title)
->setBackground(PHUIObjectBoxView::WHITE_CONFIG)
->setFormErrors($errors)
->setForm($form);
$crumbs = $this
->buildApplicationCrumbs()
->addTextCrumb($title)
->setBorder(true);
$view = id(new PHUITwoColumnView())
->setFooter($form_box);
return $this->newPage()
->setTitle($title)
->setCrumbs($crumbs)
->appendChild(
array(
$view,
));
}
private function saveRule(HeraldAdapter $adapter, $rule, $request) {
$new_name = $request->getStr('name');
$match_all = ($request->getStr('must_match') == 'all');
$repetition_policy = $request->getStr('repetition_policy');
// If the user selected an invalid policy, or there's only one possible
// value so we didn't render a control, adjust the value to the first
// valid policy value.
$repetition_options = $this->getRepetitionOptionMap($adapter);
if (!isset($repetition_options[$repetition_policy])) {
$repetition_policy = head_key($repetition_options);
}
$e_name = true;
$errors = array();
if (!strlen($new_name)) {
$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(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 HeraldActionRecord();
$obj->setAction($action[0]);
$obj->setTarget($action[1]);
try {
$adapter->willSaveAction($rule, $obj);
} catch (HeraldInvalidActionException $ex) {
$errors[] = $ex->getMessage();
}
$actions[] = $obj;
}
if (!$errors) {
$new_state = id(new HeraldRuleSerializer())->serializeRuleComponents(
$match_all,
$conditions,
$actions,
$repetition_policy);
$xactions = array();
// Until this moves to EditEngine, manually add a "CREATE" transaction
// if we're creating a new rule. This improves rendering of the initial
// group of transactions.
$is_new = (bool)(!$rule->getID());
if ($is_new) {
$xactions[] = id(new HeraldRuleTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_CREATE);
}
$xactions[] = id(new HeraldRuleTransaction())
->setTransactionType(HeraldRuleEditTransaction::TRANSACTIONTYPE)
->setNewValue($new_state);
$xactions[] = id(new HeraldRuleTransaction())
->setTransactionType(HeraldRuleNameTransaction::TRANSACTIONTYPE)
->setNewValue($new_name);
try {
id(new HeraldRuleEditor())
->setActor($this->getViewer())
->setContinueOnNoEffect(true)
->setContentSourceFromRequest($request)
->applyTransactions($rule, $xactions);
return array(null, null);
} catch (Exception $ex) {
$errors[] = $ex->getMessage();
}
}
// mutate current rule, so it would be sent to the client in the right state
$rule->setMustMatchAll((int)$match_all);
$rule->setName($new_name);
$rule->setRepetitionPolicyStringConstant($repetition_policy);
$rule->attachConditions($conditions);
$rule->attachActions($actions);
return array($e_name, $errors);
}
private function setupEditorBehavior(
HeraldRule $rule,
array $handles,
HeraldAdapter $adapter) {
$all_rules = $this->loadRulesThisRuleMayDependUpon($rule);
$all_rules = msortv($all_rules, 'getEditorSortVector');
$all_rules = mpull($all_rules, 'getEditorDisplayName', 'getPHID');
$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) {
$field_name = $condition->getFieldName();
if (empty($field_map[$field_name])) {
$field_map[$field_name] = pht('<Unknown Field "%s">', $field_name);
}
}
$actions = $adapter->getActions($rule->getRuleType());
$action_map = array_select_keys($all_actions, $actions);
// Populate any actions 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->getActions() as $action) {
$action_name = $action->getAction();
if (empty($action_map[$action_name])) {
$action_map[$action_name] = pht('<Unknown Action "%s">', $action_name);
}
}
$config_info = array();
$config_info['fields'] = $this->getFieldGroups($adapter, $field_map);
$config_info['conditions'] = $all_conditions;
$config_info['actions'] = $this->getActionGroups($adapter, $action_map);
$config_info['valueMap'] = array();
foreach ($field_map as $field => $name) {
try {
$field_conditions = $adapter->getConditionsForField($field);
} catch (Exception $ex) {
$field_conditions = array(HeraldAdapter::CONDITION_UNCONDITIONALLY);
}
$config_info['conditionMap'][$field] = $field_conditions;
}
foreach ($field_map as $field => $fname) {
foreach ($config_info['conditionMap'][$field] as $condition) {
$value_key = $adapter->getValueTypeForFieldAndCondition(
$field,
$condition);
if ($value_key instanceof HeraldFieldValue) {
$value_key->setViewer($this->getViewer());
$spec = $value_key->getControlSpecificationDictionary();
$value_key = $value_key->getFieldValueKey();
$config_info['valueMap'][$value_key] = $spec;
}
$config_info['values'][$field][$condition] = $value_key;
}
}
$config_info['rule_type'] = $rule->getRuleType();
foreach ($action_map as $action => $name) {
try {
$value_key = $adapter->getValueTypeForAction(
$action,
$rule->getRuleType());
} catch (Exception $ex) {
$value_key = new HeraldEmptyFieldValue();
}
if ($value_key instanceof HeraldFieldValue) {
$value_key->setViewer($this->getViewer());
$spec = $value_key->getControlSpecificationDictionary();
$value_key = $value_key->getFieldValueKey();
$config_info['valueMap'][$value_key] = $spec;
}
$config_info['targets'][$action] = $value_key;
}
$default_group = head($config_info['fields']);
$default_field = head_key($default_group['options']);
$default_condition = head($config_info['conditionMap'][$default_field]);
$default_actions = head($config_info['actions']);
$default_action = head_key($default_actions['options']);
if ($rule->getConditions()) {
$serial_conditions = array();
foreach ($rule->getConditions() as $condition) {
$value = $adapter->getEditorValueForCondition(
$this->getViewer(),
$condition);
$serial_conditions[] = array(
$condition->getFieldName(),
$condition->getFieldCondition(),
$value,
);
}
} else {
$serial_conditions = array(
array($default_field, $default_condition, null),
);
}
if ($rule->getActions()) {
$serial_actions = array();
foreach ($rule->getActions() as $action) {
$value = $adapter->getEditorValueForAction(
$this->getViewer(),
$action);
$serial_actions[] = array(
$action->getAction(),
$value,
);
}
} else {
$serial_actions = array(
array($default_action, null),
);
}
Javelin::initBehavior(
'herald-rule-editor',
array(
'root' => 'herald-rule-edit-form',
'default' => array(
'field' => $default_field,
'condition' => $default_condition,
'action' => $default_action,
),
'conditions' => (object)$serial_conditions,
'actions' => (object)$serial_actions,
'template' => $this->buildTokenizerTemplates() + array(
'rules' => $all_rules,
),
'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 = $rule->getRepetitionPolicyStringConstant();
$repetition_map = $this->getRepetitionOptionMap($adapter);
if (count($repetition_map) < 2) {
return head($repetition_map);
} else {
return AphrontFormSelectControl::renderSelectTag(
$repetition_policy,
$repetition_map,
array(
'name' => 'repetition_policy',
));
}
}
private function getRepetitionOptionMap(HeraldAdapter $adapter) {
$repetition_options = $adapter->getRepetitionOptions();
$repetition_names = HeraldRule::getRepetitionPolicySelectOptionMap();
return array_select_keys($repetition_names, $repetition_options);
}
protected function buildTokenizerTemplates() {
$template = new AphrontTokenizerTemplateView();
$template = $template->render();
return array(
'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();
}
// A rule can not depend upon itself.
unset($all_rules[$rule->getID()]);
return $all_rules;
}
private function getFieldGroups(HeraldAdapter $adapter, array $field_map) {
$group_map = array();
foreach ($field_map as $field_key => $field_name) {
$group_key = $adapter->getFieldGroupKey($field_key);
$group_map[$group_key][$field_key] = array(
'name' => $field_name,
'available' => $adapter->isFieldAvailable($field_key),
);
}
return $this->getGroups(
$group_map,
HeraldFieldGroup::getAllFieldGroups());
}
private function getActionGroups(HeraldAdapter $adapter, array $action_map) {
$group_map = array();
foreach ($action_map as $action_key => $action_name) {
$group_key = $adapter->getActionGroupKey($action_key);
$group_map[$group_key][$action_key] = array(
'name' => $action_name,
'available' => $adapter->isActionAvailable($action_key),
);
}
return $this->getGroups(
$group_map,
HeraldActionGroup::getAllActionGroups());
}
private function getGroups(array $item_map, array $group_list) {
assert_instances_of($group_list, 'HeraldGroup');
$groups = array();
foreach ($item_map as $group_key => $options) {
asort($options);
$group_object = idx($group_list, $group_key);
if ($group_object) {
$group_label = $group_object->getGroupLabel();
$group_order = $group_object->getSortKey();
} else {
$group_label = nonempty($group_key, pht('Other'));
$group_order = 'Z';
}
$groups[] = array(
'label' => $group_label,
'options' => $options,
'order' => $group_order,
);
}
return array_values(isort($groups, 'order'));
}
}
diff --git a/src/applications/herald/controller/HeraldWebhookViewController.php b/src/applications/herald/controller/HeraldWebhookViewController.php
index 5f6be9816c..dac4831c23 100644
--- a/src/applications/herald/controller/HeraldWebhookViewController.php
+++ b/src/applications/herald/controller/HeraldWebhookViewController.php
@@ -1,238 +1,238 @@
<?php
final class HeraldWebhookViewController
extends HeraldWebhookController {
public function shouldAllowPublic() {
return true;
}
public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
$hook = id(new HeraldWebhookQuery())
->setViewer($viewer)
->withIDs(array($request->getURIData('id')))
->executeOne();
if (!$hook) {
return new Aphront404Response();
}
$header = $this->buildHeaderView($hook);
$warnings = null;
if ($hook->isInErrorBackoff($viewer)) {
$message = pht(
'Many requests to this webhook have failed recently (at least %s '.
'errors in the last %s seconds). New requests are temporarily paused.',
$hook->getErrorBackoffThreshold(),
$hook->getErrorBackoffWindow());
$warnings = id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_WARNING)
->setErrors(
array(
$message,
));
}
$curtain = $this->buildCurtain($hook);
$properties_view = $this->buildPropertiesView($hook);
$timeline = $this->buildTransactionTimeline(
$hook,
new HeraldWebhookTransactionQuery());
$timeline->setShouldTerminate(true);
$requests = id(new HeraldWebhookRequestQuery())
->setViewer($viewer)
->withWebhookPHIDs(array($hook->getPHID()))
->setLimit(20)
->execute();
$warnings = array();
if (PhabricatorEnv::getEnvConfig('phabricator.silent')) {
$message = pht(
- 'Phabricator is currently configured in silent mode, so it will not '.
+ 'This server is running in silent mode, so it will not '.
'publish webhooks. To adjust this setting, see '.
'@{config:phabricator.silent} in Config.');
$warnings[] = id(new PHUIInfoView())
->setTitle(pht('Silent Mode'))
->setSeverity(PHUIInfoView::SEVERITY_WARNING)
->appendChild(new PHUIRemarkupView($viewer, $message));
}
$requests_table = id(new HeraldWebhookRequestListView())
->setViewer($viewer)
->setRequests($requests)
->setHighlightID($request->getURIData('requestID'));
$requests_view = id(new PHUIObjectBoxView())
->setHeaderText(pht('Recent Requests'))
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->setTable($requests_table);
$rules_view = $this->newRulesView($hook);
$hook_view = id(new PHUITwoColumnView())
->setHeader($header)
->setMainColumn(
array(
$warnings,
$properties_view,
$rules_view,
$requests_view,
$timeline,
))
->setCurtain($curtain);
$crumbs = $this->buildApplicationCrumbs()
->addTextCrumb(pht('Webhook %d', $hook->getID()))
->setBorder(true);
return $this->newPage()
->setTitle(
array(
pht('Webhook %d', $hook->getID()),
$hook->getName(),
))
->setCrumbs($crumbs)
->setPageObjectPHIDs(
array(
$hook->getPHID(),
))
->appendChild($hook_view);
}
private function buildHeaderView(HeraldWebhook $hook) {
$viewer = $this->getViewer();
$title = $hook->getName();
$status_icon = $hook->getStatusIcon();
$status_color = $hook->getStatusColor();
$status_name = $hook->getStatusDisplayName();
$header = id(new PHUIHeaderView())
->setHeader($title)
->setViewer($viewer)
->setPolicyObject($hook)
->setStatus($status_icon, $status_color, $status_name)
->setHeaderIcon('fa-cloud-upload');
return $header;
}
private function buildCurtain(HeraldWebhook $hook) {
$viewer = $this->getViewer();
$curtain = $this->newCurtainView($hook);
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$hook,
PhabricatorPolicyCapability::CAN_EDIT);
$id = $hook->getID();
$edit_uri = $this->getApplicationURI("webhook/edit/{$id}/");
$test_uri = $this->getApplicationURI("webhook/test/{$id}/");
$key_view_uri = $this->getApplicationURI("webhook/key/view/{$id}/");
$key_cycle_uri = $this->getApplicationURI("webhook/key/cycle/{$id}/");
$curtain->addAction(
id(new PhabricatorActionView())
->setName(pht('Edit Webhook'))
->setIcon('fa-pencil')
->setDisabled(!$can_edit)
->setWorkflow(!$can_edit)
->setHref($edit_uri));
$curtain->addAction(
id(new PhabricatorActionView())
->setName(pht('New Test Request'))
->setIcon('fa-cloud-upload')
->setDisabled(!$can_edit)
->setWorkflow(true)
->setHref($test_uri));
$curtain->addAction(
id(new PhabricatorActionView())
->setName(pht('View HMAC Key'))
->setIcon('fa-key')
->setDisabled(!$can_edit)
->setWorkflow(true)
->setHref($key_view_uri));
$curtain->addAction(
id(new PhabricatorActionView())
->setName(pht('Regenerate HMAC Key'))
->setIcon('fa-refresh')
->setDisabled(!$can_edit)
->setWorkflow(true)
->setHref($key_cycle_uri));
return $curtain;
}
private function buildPropertiesView(HeraldWebhook $hook) {
$viewer = $this->getViewer();
$properties = id(new PHUIPropertyListView())
->setViewer($viewer);
$properties->addProperty(
pht('URI'),
$hook->getWebhookURI());
$properties->addProperty(
pht('Status'),
$hook->getStatusDisplayName());
return id(new PHUIObjectBoxView())
->setHeaderText(pht('Details'))
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->appendChild($properties);
}
private function newRulesView(HeraldWebhook $hook) {
$viewer = $this->getViewer();
$rules = id(new HeraldRuleQuery())
->setViewer($viewer)
->withDisabled(false)
->withAffectedObjectPHIDs(array($hook->getPHID()))
->needValidateAuthors(true)
->setLimit(10)
->execute();
$list = id(new HeraldRuleListView())
->setViewer($viewer)
->setRules($rules)
->newObjectList();
$list->setNoDataString(pht('No active Herald rules call this webhook.'));
$more_href = new PhutilURI(
'/herald/',
array('affectedPHID' => $hook->getPHID()));
$more_link = id(new PHUIButtonView())
->setTag('a')
->setIcon('fa-list-ul')
->setText(pht('View All Rules'))
->setHref($more_href);
$header = id(new PHUIHeaderView())
->setHeader(pht('Called By Herald Rules'))
->addActionLink($more_link);
return id(new PHUIObjectBoxView())
->setHeader($header)
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->appendChild($list);
}
}
diff --git a/src/applications/legalpad/controller/LegalpadDocumentSignController.php b/src/applications/legalpad/controller/LegalpadDocumentSignController.php
index fb15e2af8f..7798a2aa9d 100644
--- a/src/applications/legalpad/controller/LegalpadDocumentSignController.php
+++ b/src/applications/legalpad/controller/LegalpadDocumentSignController.php
@@ -1,714 +1,715 @@
<?php
final class LegalpadDocumentSignController extends LegalpadController {
private $isSessionGate;
public function shouldAllowPublic() {
return true;
}
public function shouldAllowLegallyNonCompliantUsers() {
return true;
}
public function setIsSessionGate($is_session_gate) {
$this->isSessionGate = $is_session_gate;
return $this;
}
public function getIsSessionGate() {
return $this->isSessionGate;
}
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();
$hisec_token = null;
if ($request->isFormOrHisecPost() && !$has_signed) {
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) {
// Require MFA to sign legal documents.
if ($viewer->isLoggedIn()) {
$workflow_key = sprintf(
'legalpad.sign(%s)',
$document->getPHID());
$hisec_token = id(new PhabricatorAuthSessionEngine())
->setWorkflowKey($workflow_key)
->requireHighSecurityToken(
$viewer,
$request,
$document->getURI());
}
$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);
// Use the last content update as the modified date. We don't want to
// show that a document like a TOS was "updated" by an incidental change
// to a field like the preamble or privacy settings which does not actually
// affect the content of the agreement.
$content_updated = $document_body->getDateCreated();
// NOTE: We're avoiding `setPolicyObject()` here so we don't pick up
// extra UI elements that are unnecessary and clutter the signature page.
// These details are available on the "Manage" page.
$header = id(new PHUIHeaderView())
->setHeader($title)
->setUser($viewer)
->setEpoch($content_updated);
// If we're showing the user this document because it's required to use
// Phabricator and they haven't signed it, don't show the "Manage" button,
// since it won't work.
$is_gate = $this->getIsSessionGate();
if (!$is_gate) {
$header->addActionLink(
id(new PHUIButtonView())
->setTag('a')
->setIcon('fa-pencil')
->setText(pht('Manage'))
->setHref($manage_uri)
->setDisabled(!$can_edit)
->setWorkflow(!$can_edit));
}
$preamble_box = null;
if (strlen($document->getPreamble())) {
$preamble_text = new PHUIRemarkupView($viewer, $document->getPreamble());
// NOTE: We're avoiding `setObject()` here so we don't pick up extra UI
// elements like "Subscribers". This information is available on the
// "Manage" page, but just clutters up the "Signature" page.
$preamble = id(new PHUIPropertyListView())
->setUser($viewer)
->addSectionHeader(pht('Preamble'))
->addTextContent($preamble_text);
$preamble_box = new PHUIPropertyGroupView();
$preamble_box->addPropertyList($preamble);
}
$content = id(new PHUIDocumentView())
->addClass('legalpad')
->setHeader($header)
->appendChild(
array(
$signed_status,
$preamble_box,
$document_markup,
));
$signature_box = null;
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()) {
default:
break;
case LegalpadDocument::SIGNATURE_TYPE_INDIVIDUAL:
case LegalpadDocument::SIGNATURE_TYPE_CORPORATION:
$box = id(new PHUIObjectBoxView())
->addClass('document-sign-box')
->setHeaderText(pht('Agree and Sign Document'))
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->setForm($signature_form);
if ($error_view) {
$box->setInfoView($error_view);
}
$signature_box = phutil_tag_div(
'phui-document-view-pro-box plt', $box);
break;
}
}
$crumbs = $this->buildApplicationCrumbs();
$crumbs->setBorder(true);
$crumbs->addTextCrumb($document->getMonogram());
$box = id(new PHUITwoColumnView())
->setFooter($signature_box);
return $this->newPage()
->setTitle($title)
->setCrumbs($crumbs)
->setPageObjectPHIDs(array($document->getPHID()))
->appendChild(array(
$content,
$box,
));
}
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();
}
}
}
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 = pht(
"%s:\n\n".
"This email address was used to sign a Legalpad document ".
- "in Phabricator:\n\n".
+ "in %s:\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,
+ PlatformSymbols::getPlatformServerName(),
$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.'));
}
}
diff --git a/src/applications/legalpad/editor/LegalpadDocumentEditor.php b/src/applications/legalpad/editor/LegalpadDocumentEditor.php
index 4ede196e39..90f50564de 100644
--- a/src/applications/legalpad/editor/LegalpadDocumentEditor.php
+++ b/src/applications/legalpad/editor/LegalpadDocumentEditor.php
@@ -1,183 +1,183 @@
<?php
final class LegalpadDocumentEditor
extends PhabricatorApplicationTransactionEditor {
public function getEditorApplicationClass() {
return 'PhabricatorLegalpadApplication';
}
public function getEditorObjectsDescription() {
return pht('Legalpad Documents');
}
public function getTransactionTypes() {
$types = parent::getTransactionTypes();
$types[] = PhabricatorTransactions::TYPE_COMMENT;
$types[] = PhabricatorTransactions::TYPE_VIEW_POLICY;
$types[] = PhabricatorTransactions::TYPE_EDIT_POLICY;
return $types;
}
public function getCreateObjectTitle($author, $object) {
return pht('%s created this document.', $author);
}
public function getCreateObjectTitleForFeed($author, $object) {
return pht('%s created %s.', $author, $object);
}
protected function applyFinalEffects(
PhabricatorLiskDAO $object,
array $xactions) {
$is_contribution = false;
foreach ($xactions as $xaction) {
switch ($xaction->getTransactionType()) {
case LegalpadDocumentTitleTransaction::TRANSACTIONTYPE:
case LegalpadDocumentTextTransaction::TRANSACTIONTYPE:
$is_contribution = true;
break;
}
}
if ($is_contribution) {
$text = $object->getDocumentBody()->getText();
$title = $object->getDocumentBody()->getTitle();
$object->setVersions($object->getVersions() + 1);
$body = new LegalpadDocumentBody();
$body->setCreatorPHID($this->getActingAsPHID());
$body->setText($text);
$body->setTitle($title);
$body->setVersion($object->getVersions());
$body->setDocumentPHID($object->getPHID());
$body->save();
$object->setDocumentBodyPHID($body->getPHID());
$type = PhabricatorContributedToObjectEdgeType::EDGECONST;
id(new PhabricatorEdgeEditor())
->addEdge($this->getActingAsPHID(), $type, $object->getPHID())
->save();
$type = PhabricatorObjectHasContributorEdgeType::EDGECONST;
$contributors = PhabricatorEdgeQuery::loadDestinationPHIDs(
$object->getPHID(),
$type);
$object->setRecentContributorPHIDs(array_slice($contributors, 0, 3));
$object->setContributorCount(count($contributors));
$object->save();
}
return $xactions;
}
protected function validateAllTransactions(PhabricatorLiskDAO $object,
array $xactions) {
$errors = array();
$is_required = (bool)$object->getRequireSignature();
$document_type = $object->getSignatureType();
$individual = LegalpadDocument::SIGNATURE_TYPE_INDIVIDUAL;
foreach ($xactions as $xaction) {
switch ($xaction->getTransactionType()) {
case LegalpadDocumentRequireSignatureTransaction::TRANSACTIONTYPE:
$is_required = (bool)$xaction->getNewValue();
break;
case LegalpadDocumentSignatureTypeTransaction::TRANSACTIONTYPE:
$document_type = $xaction->getNewValue();
break;
}
}
if ($is_required && ($document_type != $individual)) {
$errors[] = new PhabricatorApplicationTransactionValidationError(
LegalpadDocumentRequireSignatureTransaction::TRANSACTIONTYPE,
pht('Invalid'),
pht('Only documents with signature type "individual" may '.
- 'require signing to use Phabricator.'),
+ 'require signing to log in.'),
null);
}
return $errors;
}
/* -( Sending Mail )------------------------------------------------------- */
protected function shouldSendMail(
PhabricatorLiskDAO $object,
array $xactions) {
return true;
}
protected function buildReplyHandler(PhabricatorLiskDAO $object) {
return id(new LegalpadReplyHandler())
->setMailReceiver($object);
}
protected function buildMailTemplate(PhabricatorLiskDAO $object) {
$id = $object->getID();
$title = $object->getDocumentBody()->getTitle();
return id(new PhabricatorMetaMTAMail())
->setSubject("L{$id}: {$title}");
}
protected function getMailTo(PhabricatorLiskDAO $object) {
return array(
$object->getCreatorPHID(),
$this->requireActor()->getPHID(),
);
}
protected function shouldImplyCC(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case LegalpadDocumentTextTransaction::TRANSACTIONTYPE:
case LegalpadDocumentTitleTransaction::TRANSACTIONTYPE:
case LegalpadDocumentPreambleTransaction::TRANSACTIONTYPE:
case LegalpadDocumentRequireSignatureTransaction::TRANSACTIONTYPE:
return true;
}
return parent::shouldImplyCC($object, $xaction);
}
protected function buildMailBody(
PhabricatorLiskDAO $object,
array $xactions) {
$body = parent::buildMailBody($object, $xactions);
$body->addLinkSection(
pht('DOCUMENT DETAIL'),
PhabricatorEnv::getProductionURI('/legalpad/view/'.$object->getID().'/'));
return $body;
}
protected function getMailSubjectPrefix() {
return pht('[Legalpad]');
}
protected function shouldPublishFeedStory(
PhabricatorLiskDAO $object,
array $xactions) {
return false;
}
protected function supportsSearch() {
return false;
}
}
diff --git a/src/applications/legalpad/query/LegalpadDocumentSearchEngine.php b/src/applications/legalpad/query/LegalpadDocumentSearchEngine.php
index 8c4dd31ff1..591174be57 100644
--- a/src/applications/legalpad/query/LegalpadDocumentSearchEngine.php
+++ b/src/applications/legalpad/query/LegalpadDocumentSearchEngine.php
@@ -1,195 +1,194 @@
<?php
final class LegalpadDocumentSearchEngine
extends PhabricatorApplicationSearchEngine {
public function getResultTypeDescription() {
return pht('Legalpad Documents');
}
public function getApplicationClassName() {
return 'PhabricatorLegalpadApplication';
}
public function newQuery() {
return id(new LegalpadDocumentQuery())
->needViewerSignatures(true);
}
protected function buildCustomSearchFields() {
return array(
id(new PhabricatorUsersSearchField())
->setLabel(pht('Signed By'))
->setKey('signerPHIDs')
->setAliases(array('signer', 'signers', 'signerPHID'))
->setDescription(
pht('Search for documents signed by given users.')),
id(new PhabricatorUsersSearchField())
->setLabel(pht('Creators'))
->setKey('creatorPHIDs')
->setAliases(array('creator', 'creators', 'creatorPHID'))
->setDescription(
pht('Search for documents with given creators.')),
id(new PhabricatorUsersSearchField())
->setLabel(pht('Contributors'))
->setKey('contributorPHIDs')
->setAliases(array('contributor', 'contributors', 'contributorPHID'))
->setDescription(
pht('Search for documents with given contributors.')),
id(new PhabricatorSearchDateField())
->setLabel(pht('Created After'))
->setKey('createdStart'),
id(new PhabricatorSearchDateField())
->setLabel(pht('Created Before'))
->setKey('createdEnd'),
);
}
protected function buildQueryFromParameters(array $map) {
$query = $this->newQuery();
if ($map['signerPHIDs']) {
$query->withSignerPHIDs($map['signerPHIDs']);
}
if ($map['contributorPHIDs']) {
$query->withContributorPHIDs($map['contributorPHIDs']);
}
if ($map['creatorPHIDs']) {
$query->withCreatorPHIDs($map['creatorPHIDs']);
}
if ($map['createdStart']) {
$query->withDateCreatedAfter($map['createdStart']);
}
if ($map['createdEnd']) {
$query->withDateCreatedAfter($map['createdStart']);
}
return $query;
}
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);
$viewer = $this->requireViewer();
switch ($query_key) {
case 'signed':
return $query->setParameter('signerPHIDs', array($viewer->getPHID()));
case 'all':
return $query;
}
return parent::buildSavedQueryFromBuiltin($query_key);
}
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())->setIcon('fa-check-square-o', 'green'),
' ',
pht(
'Signed on %s',
phabricator_date($signature->getDateCreated(), $viewer)),
));
} else {
$item->addAttribute(
array(
id(new PHUIIconView())->setIcon('fa-square-o', 'grey'),
' ',
pht('Not Signed'),
));
}
}
$item->addIcon(
'fa-pencil grey',
pht('Version %d (%s)', $document->getVersions(), $last_updated));
$list->addItem($item);
}
$result = new PhabricatorApplicationSearchResultView();
$result->setObjectList($list);
$result->setNoDataString(pht('No documents found.'));
return $result;
}
protected function getNewUserBody() {
$create_button = id(new PHUIButtonView())
->setTag('a')
->setText(pht('Create a Document'))
->setHref('/legalpad/edit/')
->setColor(PHUIButtonView::GREEN);
$icon = $this->getApplication()->getIcon();
$app_name = $this->getApplication()->getName();
$view = id(new PHUIBigInfoView())
->setIcon($icon)
->setTitle(pht('Welcome to %s', $app_name))
->setDescription(
- pht('Create documents and track signatures. Can also be re-used in '.
- 'other areas of Phabricator, like CLAs.'))
+ pht('Create documents and track signatures.'))
->addAction($create_button);
return $view;
}
}
diff --git a/src/applications/maniphest/xaction/ManiphestTaskOwnerTransaction.php b/src/applications/maniphest/xaction/ManiphestTaskOwnerTransaction.php
index 0ebef78976..e1d2749acc 100644
--- a/src/applications/maniphest/xaction/ManiphestTaskOwnerTransaction.php
+++ b/src/applications/maniphest/xaction/ManiphestTaskOwnerTransaction.php
@@ -1,167 +1,168 @@
<?php
final class ManiphestTaskOwnerTransaction
extends ManiphestTaskTransactionType {
const TRANSACTIONTYPE = 'reassign';
public function generateOldValue($object) {
return nonempty($object->getOwnerPHID(), null);
}
public function applyInternalEffects($object, $value) {
// Update the "ownerOrdering" column to contain the full name of the
// owner, if the task is assigned.
$handle = null;
if ($value) {
$handle = id(new PhabricatorHandleQuery())
->setViewer($this->getActor())
->withPHIDs(array($value))
->executeOne();
}
if ($handle) {
$object->setOwnerOrdering($handle->getName());
} else {
$object->setOwnerOrdering(null);
}
$object->setOwnerPHID($value);
}
public function getActionStrength() {
return 120;
}
public function getActionName() {
$old = $this->getOldValue();
$new = $this->getNewValue();
if ($this->getAuthorPHID() == $new) {
return pht('Claimed');
} else if (!$new) {
return pht('Unassigned');
} else if (!$old) {
return pht('Assigned');
} else {
return pht('Reassigned');
}
}
public function getTitle() {
$old = $this->getOldValue();
$new = $this->getNewValue();
if ($this->getAuthorPHID() == $new) {
return pht(
'%s claimed this task.',
$this->renderAuthor());
} else if (!$new) {
return pht(
'%s removed %s as the assignee of this task.',
$this->renderAuthor(),
$this->renderHandle($old));
} else if (!$old) {
return pht(
'%s assigned this task to %s.',
$this->renderAuthor(),
$this->renderHandle($new));
} else {
return pht(
'%s reassigned this task from %s to %s.',
$this->renderAuthor(),
$this->renderHandle($old),
$this->renderHandle($new));
}
}
public function getTitleForFeed() {
$old = $this->getOldValue();
$new = $this->getNewValue();
if ($this->getAuthorPHID() == $new) {
return pht(
'%s claimed %s.',
$this->renderAuthor(),
$this->renderObject());
} else if (!$new) {
return pht(
'%s placed %s up for grabs.',
$this->renderAuthor(),
$this->renderObject());
} else if (!$old) {
return pht(
'%s assigned %s to %s.',
$this->renderAuthor(),
$this->renderObject(),
$this->renderHandle($new));
} else {
return pht(
'%s reassigned %s from %s to %s.',
$this->renderAuthor(),
$this->renderObject(),
$this->renderHandle($old),
$this->renderHandle($new));
}
}
public function validateTransactions($object, array $xactions) {
$errors = array();
foreach ($xactions as $xaction) {
$old = $xaction->getOldValue();
$new = $xaction->getNewValue();
- if (!strlen($new)) {
+
+ if (!phutil_nonempty_string($new)) {
continue;
}
if ($new === $old) {
continue;
}
$assignee_list = id(new PhabricatorPeopleQuery())
->setViewer($this->getActor())
->withPHIDs(array($new))
->execute();
if (!$assignee_list) {
$errors[] = $this->newInvalidError(
pht('User "%s" is not a valid user.', $new));
}
}
return $errors;
}
public function getIcon() {
return 'fa-user';
}
public function getColor() {
$old = $this->getOldValue();
$new = $this->getNewValue();
if ($this->getAuthorPHID() == $new) {
return 'green';
} else if (!$new) {
return 'black';
} else if (!$old) {
return 'green';
} else {
return 'green';
}
}
public function getTransactionTypeForConduit($xaction) {
return 'owner';
}
public function getFieldValuesForConduit($xaction, $data) {
return array(
'old' => $xaction->getOldValue(),
'new' => $xaction->getNewValue(),
);
}
}
diff --git a/src/applications/meta/controller/PhabricatorApplicationEmailCommandsController.php b/src/applications/meta/controller/PhabricatorApplicationEmailCommandsController.php
index 43ed2e957e..596ed5755b 100644
--- a/src/applications/meta/controller/PhabricatorApplicationEmailCommandsController.php
+++ b/src/applications/meta/controller/PhabricatorApplicationEmailCommandsController.php
@@ -1,148 +1,149 @@
<?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 '.
+ 'After configuring processing for inbound mail, you can '.
'interact with objects (like tasks and revisions) over email. For '.
- 'information on configuring Phabricator, see '.
+ 'information on configuring inbound mail, see '.
'**[[ %s | Configuring Inbound Email ]]**.'.
"\n\n".
- 'In most cases, you can reply to email you receive from Phabricator '.
+ 'In most cases, you can reply to email you receive from this server '.
'to leave comments. You can also use **mail commands** to take a '.
'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".
'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\nI'll take care of this.\n```\n\n\n".
- "When Phabricator receives your mail, it will process any commands ".
+ "When %s 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\nI just talked to @alincoln, ".
"and he showed me that he fixed this.\n```\n",
- PhabricatorEnv::getDoclink('Configuring Inbound Email'));
+ PhabricatorEnv::getDoclink('Configuring Inbound Email'),
+ PlatformSymbols::getPlatformServerName());
$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);
$crumbs->setBorder(true);
$content_box = new PHUIRemarkupView($viewer, $content);
$info_view = null;
if (!PhabricatorEnv::getEnvConfig('metamta.reply-handler-domain')) {
$error = pht(
- "Phabricator is not currently configured to accept inbound mail. ".
+ "This server 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)
->appendChild($info_view)
->appendChild($content_box);
return $this->newPage()
->setTitle($title)
->setCrumbs($crumbs)
->appendChild($document);
}
}
diff --git a/src/applications/metamta/applicationpanel/PhabricatorMetaMTAApplicationEmailPanel.php b/src/applications/metamta/applicationpanel/PhabricatorMetaMTAApplicationEmailPanel.php
index 2f9ddcf22b..5f6911db0f 100644
--- a/src/applications/metamta/applicationpanel/PhabricatorMetaMTAApplicationEmailPanel.php
+++ b/src/applications/metamta/applicationpanel/PhabricatorMetaMTAApplicationEmailPanel.php
@@ -1,422 +1,422 @@
<?php
final class PhabricatorMetaMTAApplicationEmailPanel
extends PhabricatorApplicationConfigurationPanel {
public function getPanelKey() {
return 'email';
}
public function shouldShowForApplication(
PhabricatorApplication $application) {
return $application->supportsEmailIntegration();
}
public function buildConfigurationPagePanel() {
$viewer = $this->getViewer();
$application = $this->getApplication();
$table = $this->buildEmailTable($is_edit = false, null);
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$application,
PhabricatorPolicyCapability::CAN_EDIT);
$header = id(new PHUIHeaderView())
->setHeader(pht('Application Emails'))
->addActionLink(
id(new PHUIButtonView())
->setTag('a')
->setText(pht('Edit Application Emails'))
->setIcon('fa-pencil')
->setHref($this->getPanelURI())
->setDisabled(!$can_edit)
->setWorkflow(!$can_edit));
$box = id(new PHUIObjectBoxView())
->setHeader($header)
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->setTable($table);
return $box;
}
public function handlePanelRequest(
AphrontRequest $request,
PhabricatorController $controller) {
$viewer = $request->getViewer();
$application = $this->getApplication();
$path = $request->getURIData('path');
if (strlen($path)) {
return new Aphront404Response();
}
$uri = new PhutilURI($request->getPath());
$new = $request->getStr('new');
$edit = $request->getInt('edit');
$delete = $request->getInt('delete');
if ($new) {
return $this->returnNewAddressResponse($request, $uri, $application);
}
if ($edit) {
return $this->returnEditAddressResponse($request, $uri, $edit);
}
if ($delete) {
return $this->returnDeleteAddressResponse($request, $uri, $delete);
}
$table = $this->buildEmailTable(
$is_edit = true,
$request->getInt('id'));
$form = id(new AphrontFormView())
->setUser($viewer);
$crumbs = $controller->buildPanelCrumbs($this);
$crumbs->addTextCrumb(pht('Edit Application Emails'));
$crumbs->setBorder(true);
$header = id(new PHUIHeaderView())
->setHeader(pht('Edit Application Emails: %s', $application->getName()))
->setSubheader($application->getAppEmailBlurb())
->setHeaderIcon('fa-pencil');
$icon = id(new PHUIIconView())
->setIcon('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);
$object_box = id(new PHUIObjectBoxView())
->setHeaderText(pht('Emails'))
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->setTable($table);
$title = $application->getName();
$view = id(new PHUITwoColumnView())
->setHeader($header)
->setFooter($object_box);
return $controller->buildPanelPage(
$this,
$title,
$crumbs,
$view);
}
private function returnNewAddressResponse(
AphrontRequest $request,
PhutilURI $uri,
PhabricatorApplication $application) {
$viewer = $request->getUser();
$email_object =
PhabricatorMetaMTAApplicationEmail::initializeNewAppEmail($viewer)
->setApplicationPHID($application->getPHID());
return $this->returnSaveAddressResponse(
$request,
$uri,
$email_object,
$is_new = true);
}
private function returnEditAddressResponse(
AphrontRequest $request,
PhutilURI $uri,
$email_object_id) {
$viewer = $request->getUser();
$email_object = id(new PhabricatorMetaMTAApplicationEmailQuery())
->setViewer($viewer)
->withIDs(array($email_object_id))
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
if (!$email_object) {
return new Aphront404Response();
}
return $this->returnSaveAddressResponse(
$request,
$uri,
$email_object,
$is_new = false);
}
private function returnSaveAddressResponse(
AphrontRequest $request,
PhutilURI $uri,
PhabricatorMetaMTAApplicationEmail $email_object,
$is_new) {
$viewer = $request->getUser();
$config_default =
PhabricatorMetaMTAApplicationEmail::CONFIG_DEFAULT_AUTHOR;
$e_email = true;
$v_email = $email_object->getAddress();
$e_space = null;
$v_space = $email_object->getSpacePHID();
$v_default = $email_object->getConfigValue($config_default);
$validation_exception = null;
if ($request->isDialogFormPost()) {
$e_email = null;
$v_email = trim($request->getStr('email'));
$v_space = $request->getStr('spacePHID');
$v_default = $request->getArr($config_default);
$v_default = nonempty(head($v_default), null);
$type_address =
PhabricatorMetaMTAApplicationEmailTransaction::TYPE_ADDRESS;
$type_space = PhabricatorTransactions::TYPE_SPACE;
$type_config =
PhabricatorMetaMTAApplicationEmailTransaction::TYPE_CONFIG;
$key_config = PhabricatorMetaMTAApplicationEmailTransaction::KEY_CONFIG;
$xactions = array();
$xactions[] = id(new PhabricatorMetaMTAApplicationEmailTransaction())
->setTransactionType($type_address)
->setNewValue($v_email);
$xactions[] = id(new PhabricatorMetaMTAApplicationEmailTransaction())
->setTransactionType($type_space)
->setNewValue($v_space);
$xactions[] = id(new PhabricatorMetaMTAApplicationEmailTransaction())
->setTransactionType($type_config)
->setMetadataValue($key_config, $config_default)
->setNewValue($v_default);
$editor = id(new PhabricatorMetaMTAApplicationEmailEditor())
->setActor($viewer)
->setContentSourceFromRequest($request)
->setContinueOnNoEffect(true);
try {
$editor->applyTransactions($email_object, $xactions);
return id(new AphrontRedirectResponse())->setURI(
$uri->alter('highlight', $email_object->getID()));
} catch (PhabricatorApplicationTransactionValidationException $ex) {
$validation_exception = $ex;
$e_email = $ex->getShortMessage($type_address);
$e_space = $ex->getShortMessage($type_space);
}
}
if ($v_default) {
$v_default = array($v_default);
} else {
$v_default = array();
}
$form = id(new AphrontFormView())
->setUser($viewer)
->appendChild(
id(new AphrontFormTextControl())
->setLabel(pht('Email'))
->setName('email')
->setValue($v_email)
->setError($e_email));
if (PhabricatorSpacesNamespaceQuery::getViewerSpacesExist($viewer)) {
$form->appendControl(
id(new AphrontFormSelectControl())
->setLabel(pht('Space'))
->setName('spacePHID')
->setValue($v_space)
->setError($e_space)
->setOptions(
PhabricatorSpacesNamespaceQuery::getSpaceOptionsForViewer(
$viewer,
$v_space)));
}
$form
->appendControl(
id(new AphrontFormTokenizerControl())
->setDatasource(new PhabricatorPeopleDatasource())
->setLabel(pht('Default Author'))
->setName($config_default)
->setLimit(1)
->setValue($v_default)
->setCaption(
pht(
'Used if the "From:" address does not map to a user account. '.
'Setting a default author will allow anyone on the public '.
- 'internet to create objects in Phabricator by sending email to '.
+ 'internet to create objects by sending email to '.
'this address.')));
if ($is_new) {
$title = pht('New Address');
} else {
$title = pht('Edit Address');
}
$dialog = id(new AphrontDialogView())
->setUser($viewer)
->setWidth(AphrontDialogView::WIDTH_FORM)
->setTitle($title)
->setValidationException($validation_exception)
->appendForm($form)
->addSubmitButton(pht('Save'))
->addCancelButton($uri);
if ($is_new) {
$dialog->addHiddenInput('new', 'true');
}
return id(new AphrontDialogResponse())->setDialog($dialog);
}
private function returnDeleteAddressResponse(
AphrontRequest $request,
PhutilURI $uri,
$email_object_id) {
$viewer = $this->getViewer();
$email_object = id(new PhabricatorMetaMTAApplicationEmailQuery())
->setViewer($viewer)
->withIDs(array($email_object_id))
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
if (!$email_object) {
return new Aphront404Response();
}
if ($request->isDialogFormPost()) {
$engine = new PhabricatorDestructionEngine();
$engine->destroyObject($email_object);
return id(new AphrontRedirectResponse())->setURI($uri);
}
$dialog = id(new AphrontDialogView())
->setUser($viewer)
->addHiddenInput('delete', $email_object_id)
->setTitle(pht('Delete Address'))
->appendParagraph(pht(
'Are you sure you want to delete this email address?'))
->addSubmitButton(pht('Delete'))
->addCancelButton($uri);
return id(new AphrontDialogResponse())->setDialog($dialog);
}
private function buildEmailTable($is_edit, $highlight) {
$viewer = $this->getViewer();
$application = $this->getApplication();
$uri = new PhutilURI($this->getPanelURI());
$emails = id(new PhabricatorMetaMTAApplicationEmailQuery())
->setViewer($viewer)
->withApplicationPHIDs(array($application->getPHID()))
->execute();
$rowc = array();
$rows = array();
foreach ($emails as $email) {
$button_edit = javelin_tag(
'a',
array(
'class' => 'button small button-grey',
'href' => $uri->alter('edit', $email->getID()),
'sigil' => 'workflow',
),
pht('Edit'));
$button_remove = javelin_tag(
'a',
array(
'class' => 'button small button-grey',
'href' => $uri->alter('delete', $email->getID()),
'sigil' => 'workflow',
),
pht('Delete'));
if ($highlight == $email->getID()) {
$rowc[] = 'highlighted';
} else {
$rowc[] = null;
}
$space_phid = PhabricatorSpacesNamespaceQuery::getObjectSpacePHID($email);
if ($space_phid) {
$email_space = $viewer->renderHandle($space_phid);
} else {
$email_space = null;
}
$default_author_phid = $email->getDefaultAuthorPHID();
if (!$default_author_phid) {
$default_author = phutil_tag('em', array(), pht('None'));
} else {
$default_author = $viewer->renderHandle($default_author_phid);
}
$rows[] = array(
$email_space,
$email->getAddress(),
$default_author,
$button_edit,
$button_remove,
);
}
$table = id(new AphrontTableView($rows))
->setNoDataString(pht('No application emails created yet.'));
$table->setHeaders(
array(
pht('Space'),
pht('Email'),
pht('Default'),
pht('Edit'),
pht('Delete'),
));
$table->setColumnClasses(
array(
'',
'',
'wide',
'action',
'action',
));
$table->setRowClasses($rowc);
$table->setColumnVisibility(
array(
PhabricatorSpacesNamespaceQuery::getViewerSpacesExist($viewer),
true,
true,
$is_edit,
$is_edit,
));
return $table;
}
}
diff --git a/src/applications/metamta/query/PhabricatorMetaMTAActor.php b/src/applications/metamta/query/PhabricatorMetaMTAActor.php
index cf2060a8f7..676f0933ec 100644
--- a/src/applications/metamta/query/PhabricatorMetaMTAActor.php
+++ b/src/applications/metamta/query/PhabricatorMetaMTAActor.php
@@ -1,185 +1,185 @@
<?php
final class PhabricatorMetaMTAActor extends Phobject {
const STATUS_DELIVERABLE = 'deliverable';
const STATUS_UNDELIVERABLE = 'undeliverable';
const REASON_NONE = 'none';
const REASON_UNLOADABLE = 'unloadable';
const REASON_UNMAILABLE = 'unmailable';
const REASON_NO_ADDRESS = 'noaddress';
const REASON_DISABLED = 'disabled';
const REASON_MAIL_DISABLED = 'maildisabled';
const REASON_EXTERNAL_TYPE = 'exernaltype';
const REASON_RESPONSE = 'response';
const REASON_SELF = 'self';
const REASON_MAILTAGS = 'mailtags';
const REASON_BOT = 'bot';
const REASON_FORCE = 'force';
const REASON_FORCE_HERALD = 'force-herald';
const REASON_ROUTE_AS_NOTIFICATION = 'route-as-notification';
const REASON_ROUTE_AS_MAIL = 'route-as-mail';
const REASON_UNVERIFIED = 'unverified';
const REASON_MUTED = 'muted';
private $phid;
private $emailAddress;
private $name;
private $status = self::STATUS_DELIVERABLE;
private $reasons = array();
private $isVerified = false;
public function setName($name) {
$this->name = $name;
return $this;
}
public function getName() {
return $this->name;
}
public function setEmailAddress($email_address) {
$this->emailAddress = $email_address;
return $this;
}
public function getEmailAddress() {
return $this->emailAddress;
}
public function setIsVerified($is_verified) {
$this->isVerified = $is_verified;
return $this;
}
public function getIsVerified() {
return $this->isVerified;
}
public function setPHID($phid) {
$this->phid = $phid;
return $this;
}
public function getPHID() {
return $this->phid;
}
public function setUndeliverable($reason) {
$this->reasons[] = $reason;
$this->status = self::STATUS_UNDELIVERABLE;
return $this;
}
public function setDeliverable($reason) {
$this->reasons[] = $reason;
$this->status = self::STATUS_DELIVERABLE;
return $this;
}
public function isDeliverable() {
return ($this->status === self::STATUS_DELIVERABLE);
}
public function getDeliverabilityReasons() {
return $this->reasons;
}
public static function isDeliveryReason($reason) {
switch ($reason) {
case self::REASON_NONE:
case self::REASON_FORCE:
case self::REASON_FORCE_HERALD:
case self::REASON_ROUTE_AS_MAIL:
return true;
default:
// All other reasons cause the message to not be delivered.
return false;
}
}
public static function getReasonName($reason) {
$names = array(
self::REASON_NONE => pht('None'),
self::REASON_DISABLED => pht('Disabled Recipient'),
self::REASON_BOT => pht('Bot Recipient'),
self::REASON_NO_ADDRESS => pht('No Address'),
self::REASON_EXTERNAL_TYPE => pht('External Recipient'),
self::REASON_UNMAILABLE => pht('Not Mailable'),
self::REASON_RESPONSE => pht('Similar Reply'),
self::REASON_SELF => pht('Self Mail'),
self::REASON_MAIL_DISABLED => pht('Mail Disabled'),
self::REASON_MAILTAGS => pht('Mail Tags'),
self::REASON_UNLOADABLE => pht('Bad Recipient'),
self::REASON_FORCE => pht('Forced Mail'),
self::REASON_FORCE_HERALD => pht('Forced by Herald'),
self::REASON_ROUTE_AS_NOTIFICATION => pht('Route as Notification'),
self::REASON_ROUTE_AS_MAIL => pht('Route as Mail'),
self::REASON_UNVERIFIED => pht('Address Not Verified'),
self::REASON_MUTED => pht('Muted'),
);
return idx($names, $reason, pht('Unknown ("%s")', $reason));
}
public static function getReasonDescription($reason) {
$descriptions = array(
self::REASON_NONE => pht(
'No special rules affected this mail.'),
self::REASON_DISABLED => pht(
'This user is disabled; disabled users do not receive mail.'),
self::REASON_BOT => pht(
'This user is a bot; bot accounts do not receive mail.'),
self::REASON_NO_ADDRESS => pht(
'Unable to load an email address for this PHID.'),
self::REASON_EXTERNAL_TYPE => pht(
'Only external accounts of type "email" are deliverable; this '.
'account has a different type.'),
self::REASON_UNMAILABLE => pht(
'This PHID type does not correspond to a mailable object.'),
self::REASON_RESPONSE => pht(
'This message is a response to another email message, and this '.
'recipient received the original email message, so we are not '.
'sending them this substantially similar message (for example, '.
'the sender used "Reply All" instead of "Reply" in response to '.
- 'mail from Phabricator).'),
+ 'mail from this server).'),
self::REASON_SELF => pht(
'This recipient is the user whose actions caused delivery of '.
'this message, but they have set preferences so they do not '.
'receive mail about their own actions (Settings > Email '.
'Preferences > Self Actions).'),
self::REASON_MAIL_DISABLED => pht(
'This recipient has disabled all email notifications '.
'(Settings > Email Preferences > Email Notifications).'),
self::REASON_MAILTAGS => pht(
'This mail has tags which control which users receive it, and '.
'this recipient has not elected to receive mail with any of '.
'the tags on this message (Settings > Email Preferences).'),
self::REASON_UNLOADABLE => pht(
'Unable to load user record for this PHID.'),
self::REASON_FORCE => pht(
'Delivery of this mail is forced and ignores deliver preferences. '.
'Mail which uses forced delivery is usually related to account '.
'management or authentication. For example, password reset email '.
'ignores mail preferences.'),
self::REASON_FORCE_HERALD => pht(
'This recipient was added by a "Send me an Email" rule in Herald, '.
'which overrides some delivery settings.'),
self::REASON_ROUTE_AS_NOTIFICATION => pht(
'This message was downgraded to a notification by outbound mail '.
'rules in Herald.'),
self::REASON_ROUTE_AS_MAIL => pht(
'This message was upgraded to email by outbound mail rules '.
'in Herald.'),
self::REASON_UNVERIFIED => pht(
'This recipient does not have a verified primary email address.'),
self::REASON_MUTED => pht(
'This recipient has muted notifications for this object.'),
);
return idx($descriptions, $reason, pht('Unknown Reason ("%s")', $reason));
}
}
diff --git a/src/applications/metamta/receiver/PhabricatorObjectMailReceiver.php b/src/applications/metamta/receiver/PhabricatorObjectMailReceiver.php
index 16950c1577..0342a94a60 100644
--- a/src/applications/metamta/receiver/PhabricatorObjectMailReceiver.php
+++ b/src/applications/metamta/receiver/PhabricatorObjectMailReceiver.php
@@ -1,190 +1,190 @@
<?php
abstract class PhabricatorObjectMailReceiver extends PhabricatorMailReceiver {
/**
* Return a regular expression fragment which matches the name of an
* object which can receive mail. For example, Differential uses:
*
* D[1-9]\d*
*
* ...to match `D123`, etc., identifying Differential Revisions.
*
* @return string Regular expression fragment.
*/
abstract protected function getObjectPattern();
/**
* Load the object receiving mail, based on an identifying pattern. Normally
* this pattern is some sort of object ID.
*
* @param string A string matched by @{method:getObjectPattern}
* fragment.
* @param PhabricatorUser The viewing user.
* @return void
*/
abstract protected function loadObject($pattern, PhabricatorUser $viewer);
final protected function processReceivedMail(
PhabricatorMetaMTAReceivedMail $mail,
PhutilEmailAddress $target) {
$parts = $this->matchObjectAddress($target);
if (!$parts) {
// We should only make it here if we matched already in "canAcceptMail()",
// so this is a surprise.
throw new Exception(
pht(
'Failed to parse object address ("%s") during processing.',
(string)$target));
}
$pattern = $parts['pattern'];
$sender = $this->getSender();
try {
$object = $this->loadObject($pattern, $sender);
} catch (PhabricatorPolicyException $policy_exception) {
throw new PhabricatorMetaMTAReceivedMailProcessingException(
MetaMTAReceivedMailStatus::STATUS_POLICY_PROBLEM,
pht(
'This mail is addressed to an object ("%s") you do not have '.
'permission to see: %s',
$pattern,
$policy_exception->getMessage()));
}
if (!$object) {
throw new PhabricatorMetaMTAReceivedMailProcessingException(
MetaMTAReceivedMailStatus::STATUS_NO_SUCH_OBJECT,
pht(
'This mail is addressed to an object ("%s"), but that object '.
'does not exist.',
$pattern));
}
$sender_identifier = $parts['sender'];
if ($sender_identifier === 'public') {
if (!PhabricatorEnv::getEnvConfig('metamta.public-replies')) {
throw new PhabricatorMetaMTAReceivedMailProcessingException(
MetaMTAReceivedMailStatus::STATUS_NO_PUBLIC_MAIL,
pht(
'This mail is addressed to the public email address of an object '.
- '("%s"), but public replies are not enabled on this Phabricator '.
- 'install. An administrator may have recently disabled this '.
- 'setting, or you may have replied to an old message. Try '.
- 'replying to a more recent message instead.',
+ '("%s"), but public replies are not enabled on this server. An '.
+ 'administrator may have recently disabled this setting, or you '.
+ 'may have replied to an old message. Try replying to a more '.
+ 'recent message instead.',
$pattern));
}
$check_phid = $object->getPHID();
} else {
if ($sender_identifier != $sender->getID()) {
throw new PhabricatorMetaMTAReceivedMailProcessingException(
MetaMTAReceivedMailStatus::STATUS_USER_MISMATCH,
pht(
'This mail is addressed to the private email address of an object '.
'("%s"), but you are not the user who is authorized to use the '.
'address you sent mail to. Each private address is unique to the '.
'user who received the original mail. Try replying to a message '.
'which was sent directly to you instead.',
$pattern));
}
$check_phid = $sender->getPHID();
}
$mail_key = PhabricatorMetaMTAMailProperties::loadMailKey($object);
$expect_hash = self::computeMailHash($mail_key, $check_phid);
if (!phutil_hashes_are_identical($expect_hash, $parts['hash'])) {
throw new PhabricatorMetaMTAReceivedMailProcessingException(
MetaMTAReceivedMailStatus::STATUS_HASH_MISMATCH,
pht(
'This mail is addressed to an object ("%s"), but the address is '.
'not correct (the security hash is wrong). Check that the address '.
'is correct.',
$pattern));
}
$mail->setRelatedPHID($object->getPHID());
$this->processReceivedObjectMail($mail, $object, $sender);
return $this;
}
protected function processReceivedObjectMail(
PhabricatorMetaMTAReceivedMail $mail,
PhabricatorLiskDAO $object,
PhabricatorUser $sender) {
$handler = $this->getTransactionReplyHandler();
if ($handler) {
return $handler
->setMailReceiver($object)
->setActor($sender)
->setExcludeMailRecipientPHIDs($mail->loadAllRecipientPHIDs())
->processEmail($mail);
}
throw new PhutilMethodNotImplementedException();
}
protected function getTransactionReplyHandler() {
return null;
}
public function loadMailReceiverObject($pattern, PhabricatorUser $viewer) {
return $this->loadObject($pattern, $viewer);
}
final public function canAcceptMail(
PhabricatorMetaMTAReceivedMail $mail,
PhutilEmailAddress $target) {
// If we don't have a valid sender user account, we can never accept
// mail to any object.
$sender = $this->getSender();
if (!$sender) {
return false;
}
return (bool)$this->matchObjectAddress($target);
}
private function matchObjectAddress(PhutilEmailAddress $address) {
$address = PhabricatorMailUtil::normalizeAddress($address);
$local = $address->getLocalPart();
$regexp = $this->getAddressRegexp();
$matches = null;
if (!preg_match($regexp, $local, $matches)) {
return false;
}
return $matches;
}
private function getAddressRegexp() {
$pattern = $this->getObjectPattern();
$regexp =
'(^'.
'(?P<pattern>'.$pattern.')'.
'\\+'.
'(?P<sender>\w+)'.
'\\+'.
'(?P<hash>[a-f0-9]{16})'.
'$)Ui';
return $regexp;
}
public static function computeMailHash($mail_key, $phid) {
$hash = PhabricatorHash::digestWithNamedKey(
$mail_key.$phid,
'mail.object-address-key');
return substr($hash, 0, 16);
}
}
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Jan 19 2025, 22:26 (6 w, 3 d ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1129383
Default Alt Text
(101 KB)
Attached To
Mode
rP Phorge
Attached
Detach File
Event Timeline
Log In to Comment