1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2025-01-08 05:41:01 +01:00

(stable) Promote 2019 Week 5

This commit is contained in:
epriestley 2019-02-02 10:49:00 -08:00
commit c65ad751ab
47 changed files with 1082 additions and 220 deletions

View file

@ -0,0 +1,2 @@
ALTER TABLE {$NAMESPACE}_project.project
ADD subtype VARCHAR(64) COLLATE {$COLLATE_TEXT} NOT NULL;

View file

@ -0,0 +1,2 @@
UPDATE {$NAMESPACE}_project.project
SET subtype = 'default' WHERE subtype = '';

View file

@ -0,0 +1,18 @@
<?php
// See PHI1046. The "spacePHID" column for milestones may have fallen out of
// sync; correct all existing values.
$table = new PhabricatorProject();
$conn = $table->establishConnection('w');
$table_name = $table->getTableName();
foreach (new LiskRawMigrationIterator($conn, $table_name) as $project_row) {
queryfx(
$conn,
'UPDATE %R SET spacePHID = %ns
WHERE parentProjectPHID = %s AND milestoneNumber IS NOT NULL',
$table,
$project_row['spacePHID'],
$project_row['phid']);
}

View file

@ -55,8 +55,8 @@ foreach (array('text', 'html') as $part) {
}
$headers = $parser->getHeaders();
$headers['subject'] = iconv_mime_decode($headers['subject'], 0, 'UTF-8');
$headers['from'] = iconv_mime_decode($headers['from'], 0, 'UTF-8');
$headers['subject'] = phutil_decode_mime_header($headers['subject']);
$headers['from'] = phutil_decode_mime_header($headers['from']);
if ($args->getArg('process-duplicates')) {
$headers['message-id'] = Filesystem::readRandomCharacters(64);

View file

@ -183,7 +183,6 @@ phutil_register_library_map(array(
'AphrontCalendarEventView' => 'applications/calendar/view/AphrontCalendarEventView.php',
'AphrontController' => 'aphront/AphrontController.php',
'AphrontCursorPagerView' => 'view/control/AphrontCursorPagerView.php',
'AphrontDefaultApplicationConfiguration' => 'aphront/configuration/AphrontDefaultApplicationConfiguration.php',
'AphrontDialogResponse' => 'aphront/response/AphrontDialogResponse.php',
'AphrontDialogView' => 'view/AphrontDialogView.php',
'AphrontEpochHTTPParameterType' => 'aphront/httpparametertype/AphrontEpochHTTPParameterType.php',
@ -2235,8 +2234,10 @@ phutil_register_library_map(array(
'PhabricatorAuthFactorProviderEditController' => 'applications/auth/controller/mfa/PhabricatorAuthFactorProviderEditController.php',
'PhabricatorAuthFactorProviderEditEngine' => 'applications/auth/editor/PhabricatorAuthFactorProviderEditEngine.php',
'PhabricatorAuthFactorProviderEditor' => 'applications/auth/editor/PhabricatorAuthFactorProviderEditor.php',
'PhabricatorAuthFactorProviderEnrollMessageTransaction' => 'applications/auth/xaction/PhabricatorAuthFactorProviderEnrollMessageTransaction.php',
'PhabricatorAuthFactorProviderListController' => 'applications/auth/controller/mfa/PhabricatorAuthFactorProviderListController.php',
'PhabricatorAuthFactorProviderMFAEngine' => 'applications/auth/engine/PhabricatorAuthFactorProviderMFAEngine.php',
'PhabricatorAuthFactorProviderMessageController' => 'applications/auth/controller/mfa/PhabricatorAuthFactorProviderMessageController.php',
'PhabricatorAuthFactorProviderNameTransaction' => 'applications/auth/xaction/PhabricatorAuthFactorProviderNameTransaction.php',
'PhabricatorAuthFactorProviderQuery' => 'applications/auth/query/PhabricatorAuthFactorProviderQuery.php',
'PhabricatorAuthFactorProviderStatus' => 'applications/auth/constants/PhabricatorAuthFactorProviderStatus.php',
@ -3061,6 +3062,8 @@ phutil_register_library_map(array(
'PhabricatorEditPage' => 'applications/transactions/editengine/PhabricatorEditPage.php',
'PhabricatorEditType' => 'applications/transactions/edittype/PhabricatorEditType.php',
'PhabricatorEditor' => 'infrastructure/PhabricatorEditor.php',
'PhabricatorEditorExtension' => 'applications/transactions/engineextension/PhabricatorEditorExtension.php',
'PhabricatorEditorExtensionModule' => 'applications/transactions/engineextension/PhabricatorEditorExtensionModule.php',
'PhabricatorEditorMailEngineExtension' => 'applications/transactions/engineextension/PhabricatorEditorMailEngineExtension.php',
'PhabricatorEditorMultipleSetting' => 'applications/settings/setting/PhabricatorEditorMultipleSetting.php',
'PhabricatorEditorSetting' => 'applications/settings/setting/PhabricatorEditorSetting.php',
@ -4114,6 +4117,8 @@ phutil_register_library_map(array(
'PhabricatorProjectSubprojectWarningController' => 'applications/project/controller/PhabricatorProjectSubprojectWarningController.php',
'PhabricatorProjectSubprojectsController' => 'applications/project/controller/PhabricatorProjectSubprojectsController.php',
'PhabricatorProjectSubprojectsProfileMenuItem' => 'applications/project/menuitem/PhabricatorProjectSubprojectsProfileMenuItem.php',
'PhabricatorProjectSubtypeDatasource' => 'applications/project/typeahead/PhabricatorProjectSubtypeDatasource.php',
'PhabricatorProjectSubtypesConfigType' => 'applications/project/config/PhabricatorProjectSubtypesConfigType.php',
'PhabricatorProjectTestDataGenerator' => 'applications/project/lipsum/PhabricatorProjectTestDataGenerator.php',
'PhabricatorProjectTransaction' => 'applications/project/storage/PhabricatorProjectTransaction.php',
'PhabricatorProjectTransactionEditor' => 'applications/project/editor/PhabricatorProjectTransactionEditor.php',
@ -5625,7 +5630,6 @@ phutil_register_library_map(array(
'AphrontCalendarEventView' => 'AphrontView',
'AphrontController' => 'Phobject',
'AphrontCursorPagerView' => 'AphrontView',
'AphrontDefaultApplicationConfiguration' => 'AphrontApplicationConfiguration',
'AphrontDialogResponse' => 'AphrontResponse',
'AphrontDialogView' => array(
'AphrontView',
@ -7971,8 +7975,10 @@ phutil_register_library_map(array(
'PhabricatorAuthFactorProviderEditController' => 'PhabricatorAuthFactorProviderController',
'PhabricatorAuthFactorProviderEditEngine' => 'PhabricatorEditEngine',
'PhabricatorAuthFactorProviderEditor' => 'PhabricatorApplicationTransactionEditor',
'PhabricatorAuthFactorProviderEnrollMessageTransaction' => 'PhabricatorAuthFactorProviderTransactionType',
'PhabricatorAuthFactorProviderListController' => 'PhabricatorAuthProviderController',
'PhabricatorAuthFactorProviderMFAEngine' => 'PhabricatorEditEngineMFAEngine',
'PhabricatorAuthFactorProviderMessageController' => 'PhabricatorAuthFactorProviderController',
'PhabricatorAuthFactorProviderNameTransaction' => 'PhabricatorAuthFactorProviderTransactionType',
'PhabricatorAuthFactorProviderQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorAuthFactorProviderStatus' => 'Phobject',
@ -8925,6 +8931,8 @@ phutil_register_library_map(array(
'PhabricatorEditPage' => 'Phobject',
'PhabricatorEditType' => 'Phobject',
'PhabricatorEditor' => 'Phobject',
'PhabricatorEditorExtension' => 'Phobject',
'PhabricatorEditorExtensionModule' => 'PhabricatorConfigModule',
'PhabricatorEditorMailEngineExtension' => 'PhabricatorMailEngineExtension',
'PhabricatorEditorMultipleSetting' => 'PhabricatorSelectSetting',
'PhabricatorEditorSetting' => 'PhabricatorStringSetting',
@ -10029,6 +10037,7 @@ phutil_register_library_map(array(
'PhabricatorConduitResultInterface',
'PhabricatorColumnProxyInterface',
'PhabricatorSpacesInterface',
'PhabricatorEditEngineSubtypeInterface',
),
'PhabricatorProjectAddHeraldAction' => 'PhabricatorProjectHeraldAction',
'PhabricatorProjectApplication' => 'PhabricatorApplication',
@ -10156,6 +10165,8 @@ phutil_register_library_map(array(
'PhabricatorProjectSubprojectWarningController' => 'PhabricatorProjectController',
'PhabricatorProjectSubprojectsController' => 'PhabricatorProjectController',
'PhabricatorProjectSubprojectsProfileMenuItem' => 'PhabricatorProfileMenuItem',
'PhabricatorProjectSubtypeDatasource' => 'PhabricatorTypeaheadDatasource',
'PhabricatorProjectSubtypesConfigType' => 'PhabricatorJSONConfigType',
'PhabricatorProjectTestDataGenerator' => 'PhabricatorTestDataGenerator',
'PhabricatorProjectTransaction' => 'PhabricatorModularTransaction',
'PhabricatorProjectTransactionEditor' => 'PhabricatorApplicationTransactionEditor',

View file

@ -5,55 +5,81 @@
* @task response Response Handling
* @task exception Exception Handling
*/
abstract class AphrontApplicationConfiguration extends Phobject {
final class AphrontApplicationConfiguration
extends Phobject {
private $request;
private $host;
private $path;
private $console;
abstract public function buildRequest();
abstract public function build404Controller();
abstract public function buildRedirectController($uri, $external);
public function buildRequest() {
$parser = new PhutilQueryStringParser();
final public function setRequest(AphrontRequest $request) {
$data = array();
$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 build404Controller() {
return array(new Phabricator404Controller(), array());
}
public function buildRedirectController($uri, $external) {
return array(
new PhabricatorRedirectController(),
array(
'uri' => $uri,
'external' => $external,
),
);
}
public function setRequest(AphrontRequest $request) {
$this->request = $request;
return $this;
}
final public function getRequest() {
public function getRequest() {
return $this->request;
}
final public function getConsole() {
public function getConsole() {
return $this->console;
}
final public function setConsole($console) {
public function setConsole($console) {
$this->console = $console;
return $this;
}
final public function setHost($host) {
public function setHost($host) {
$this->host = $host;
return $this;
}
final public function getHost() {
public function getHost() {
return $this->host;
}
final public function setPath($path) {
public function setPath($path) {
$this->path = $path;
return $this;
}
final public function getPath() {
public function getPath() {
return $this->path;
}
public function willBuildRequest() {}
/**
* @phutil-external-symbol class PhabricatorStartup
@ -83,6 +109,8 @@ abstract class AphrontApplicationConfiguration extends Phobject {
PhabricatorStartup::beginStartupPhase('env.init');
self::readHTTPPOSTData();
try {
PhabricatorEnv::initializeWebEnvironment();
$database_exception = null;
@ -142,16 +170,10 @@ abstract class AphrontApplicationConfiguration extends Phobject {
$host = AphrontRequest::getHTTPHeader('Host');
$path = $_REQUEST['__path__'];
switch ($host) {
default:
$config_key = 'aphront.default-application-configuration-class';
$application = PhabricatorEnv::newObjectFromConfig($config_key);
break;
}
$application = new self();
$application->setHost($host);
$application->setPath($path);
$application->willBuildRequest();
$request = $application->buildRequest();
// Now that we have a request, convert the write guard into one which
@ -313,7 +335,7 @@ abstract class AphrontApplicationConfiguration extends Phobject {
* parameters.
* @task routing
*/
final private function buildController() {
private function buildController() {
$request = $this->getRequest();
// If we're configured to operate in cluster mode, reject requests which
@ -708,4 +730,88 @@ abstract class AphrontApplicationConfiguration extends Phobject {
->setContent($result);
}
private static function readHTTPPOSTData() {
$request_method = idx($_SERVER, 'REQUEST_METHOD');
if ($request_method === 'PUT') {
// For PUT requests, do nothing: in particular, do NOT read input. This
// allows us to stream input later and process very large PUT requests,
// like those coming from Git LFS.
return;
}
// For POST requests, we're going to read the raw input ourselves here
// if we can. Among other things, this corrects variable names with
// the "." character in them, which PHP normally converts into "_".
// There are two major considerations here: whether the
// `enable_post_data_reading` option is set, and whether the content
// type is "multipart/form-data" or not.
// If `enable_post_data_reading` is off, we're free to read the entire
// raw request body and parse it -- and we must, because $_POST and
// $_FILES are not built for us. If `enable_post_data_reading` is on,
// which is the default, we may not be able to read the body (the
// documentation says we can't, but empirically we can at least some
// of the time).
// If the content type is "multipart/form-data", we need to build both
// $_POST and $_FILES, which is involved. The body itself is also more
// difficult to parse than other requests.
$raw_input = PhabricatorStartup::getRawInput();
$parser = new PhutilQueryStringParser();
if (strlen($raw_input)) {
$content_type = idx($_SERVER, 'CONTENT_TYPE');
$is_multipart = preg_match('@^multipart/form-data@i', $content_type);
if ($is_multipart && !ini_get('enable_post_data_reading')) {
$multipart_parser = id(new AphrontMultipartParser())
->setContentType($content_type);
$multipart_parser->beginParse();
$multipart_parser->continueParse($raw_input);
$parts = $multipart_parser->endParse();
// We're building and then parsing a query string so that requests
// with arrays (like "x[]=apple&x[]=banana") work correctly. This also
// means we can't use "phutil_build_http_querystring()", since it
// can't build a query string with duplicate names.
$query_string = array();
foreach ($parts as $part) {
if (!$part->isVariable()) {
continue;
}
$name = $part->getName();
$value = $part->getVariableValue();
$query_string[] = rawurlencode($name).'='.rawurlencode($value);
}
$query_string = implode('&', $query_string);
$post = $parser->parseQueryString($query_string);
$files = array();
foreach ($parts as $part) {
if ($part->isVariable()) {
continue;
}
$files[$part->getName()] = $part->getPHPFileDictionary();
}
$_FILES = $files;
} else {
$post = $parser->parseQueryString($raw_input);
}
$_POST = $post;
PhabricatorStartup::rebuildRequest();
} else if ($_POST) {
$post = filter_input_array(INPUT_POST, FILTER_UNSAFE_RAW);
if (is_array($post)) {
$_POST = $post;
PhabricatorStartup::rebuildRequest();
}
}
}
}

View file

@ -1,121 +0,0 @@
<?php
/**
* NOTE: Do not extend this!
*
* @concrete-extensible
*/
class AphrontDefaultApplicationConfiguration
extends AphrontApplicationConfiguration {
/**
* @phutil-external-symbol class PhabricatorStartup
*/
public function buildRequest() {
$parser = new PhutilQueryStringParser();
$data = array();
$request_method = idx($_SERVER, 'REQUEST_METHOD');
if ($request_method === 'PUT') {
// For PUT requests, do nothing: in particular, do NOT read input. This
// allows us to stream input later and process very large PUT requests,
// like those coming from Git LFS.
} else {
// For POST requests, we're going to read the raw input ourselves here
// if we can. Among other things, this corrects variable names with
// the "." character in them, which PHP normally converts into "_".
// There are two major considerations here: whether the
// `enable_post_data_reading` option is set, and whether the content
// type is "multipart/form-data" or not.
// If `enable_post_data_reading` is off, we're free to read the entire
// raw request body and parse it -- and we must, because $_POST and
// $_FILES are not built for us. If `enable_post_data_reading` is on,
// which is the default, we may not be able to read the body (the
// documentation says we can't, but empirically we can at least some
// of the time).
// If the content type is "multipart/form-data", we need to build both
// $_POST and $_FILES, which is involved. The body itself is also more
// difficult to parse than other requests.
$raw_input = PhabricatorStartup::getRawInput();
if (strlen($raw_input)) {
$content_type = idx($_SERVER, 'CONTENT_TYPE');
$is_multipart = preg_match('@^multipart/form-data@i', $content_type);
if ($is_multipart && !ini_get('enable_post_data_reading')) {
$multipart_parser = id(new AphrontMultipartParser())
->setContentType($content_type);
$multipart_parser->beginParse();
$multipart_parser->continueParse($raw_input);
$parts = $multipart_parser->endParse();
$query_string = array();
foreach ($parts as $part) {
if (!$part->isVariable()) {
continue;
}
$name = $part->getName();
$value = $part->getVariableValue();
$query_string[] = urlencode($name).'='.urlencode($value);
}
$query_string = implode('&', $query_string);
$post = $parser->parseQueryString($query_string);
$files = array();
foreach ($parts as $part) {
if ($part->isVariable()) {
continue;
}
$files[$part->getName()] = $part->getPHPFileDictionary();
}
$_FILES = $files;
} else {
$post = $parser->parseQueryString($raw_input);
}
$_POST = $post;
PhabricatorStartup::rebuildRequest();
$data += $post;
} else if ($_POST) {
$post = filter_input_array(INPUT_POST, FILTER_UNSAFE_RAW);
if (is_array($post)) {
$_POST = $post;
PhabricatorStartup::rebuildRequest();
}
$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 build404Controller() {
return array(new Phabricator404Controller(), array());
}
public function buildRedirectController($uri, $external) {
return array(
new PhabricatorRedirectController(),
array(
'uri' => $uri,
'external' => $external,
),
);
}
}

View file

@ -78,15 +78,13 @@ final class PhabricatorHighSecurityRequestExceptionHandler
$form_layout = $form->buildLayoutView();
if ($is_upgrade) {
$messages = array(
pht(
'You are taking an action which requires you to enter '.
'high security.'),
);
$message = pht(
'You are taking an action which requires you to enter '.
'high security.');
$info_view = id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_MFA)
->setErrors($messages);
->setErrors(array($message));
$dialog
->appendChild($info_view)
@ -100,12 +98,18 @@ final class PhabricatorHighSecurityRequestExceptionHandler
'period of time. When you are finished taking sensitive '.
'actions, you should leave high security.'));
} else {
$message = pht(
'You are taking an action which requires you to provide '.
'multi-factor credentials.');
$info_view = id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_MFA)
->setErrors(array($message));
$dialog
->appendChild($info_view)
->setErrors(
array(
pht(
'You are taking an action which requires you to provide '.
'multi-factor credentials.'),
))
->appendChild($form_layout);
}

View file

@ -95,6 +95,8 @@ final class PhabricatorAuthApplication extends PhabricatorApplication {
'PhabricatorAuthFactorProviderEditController',
'(?P<id>[1-9]\d*)/' =>
'PhabricatorAuthFactorProviderViewController',
'message/(?P<id>[1-9]\d*)/' =>
'PhabricatorAuthFactorProviderMessageController',
),
'message/' => array(

View file

@ -0,0 +1,84 @@
<?php
final class PhabricatorAuthFactorProviderMessageController
extends PhabricatorAuthFactorProviderController {
public function handleRequest(AphrontRequest $request) {
$this->requireApplicationCapability(
AuthManageProvidersCapability::CAPABILITY);
$viewer = $request->getViewer();
$id = $request->getURIData('id');
$provider = id(new PhabricatorAuthFactorProviderQuery())
->setViewer($viewer)
->withIDs(array($id))
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
if (!$provider) {
return new Aphront404Response();
}
$cancel_uri = $provider->getURI();
$enroll_key =
PhabricatorAuthFactorProviderEnrollMessageTransaction::TRANSACTIONTYPE;
$message = $provider->getEnrollMessage();
if ($request->isFormOrHisecPost()) {
$message = $request->getStr('message');
$xactions = array();
$xactions[] = id(new PhabricatorAuthFactorProviderTransaction())
->setTransactionType($enroll_key)
->setNewValue($message);
$editor = id(new PhabricatorAuthFactorProviderEditor())
->setActor($viewer)
->setContentSourceFromRequest($request)
->setContinueOnNoEffect(true)
->setContinueOnMissingFields(true)
->setCancelURI($cancel_uri);
$editor->applyTransactions($provider, $xactions);
return id(new AphrontRedirectResponse())->setURI($cancel_uri);
}
$default_message = $provider->getEnrollDescription($viewer);
$default_message = new PHUIRemarkupView($viewer, $default_message);
$form = id(new AphrontFormView())
->setViewer($viewer)
->appendRemarkupInstructions(
pht(
'When users add a factor for this provider, they are given this '.
'enrollment guidance by default:'))
->appendControl(
id(new AphrontFormMarkupControl())
->setLabel(pht('Default Message'))
->setValue($default_message))
->appendRemarkupInstructions(
pht(
'You may optionally customize the enrollment message users are '.
'presented with by providing a replacement message below:'))
->appendControl(
id(new PhabricatorRemarkupControl())
->setLabel(pht('Custom Message'))
->setName('message')
->setValue($message));
return $this->newDialog()
->setTitle(pht('Change Enroll Message'))
->setWidth(AphrontDialogView::WIDTH_FORM)
->appendForm($form)
->addCancelButton($cancel_uri)
->addSubmitButton(pht('Save'));
}
}

View file

@ -81,6 +81,16 @@ final class PhabricatorAuthFactorProviderViewController
pht('Factor Type'),
$provider->getFactor()->getFactorName());
$custom_enroll = $provider->getEnrollMessage();
if (strlen($custom_enroll)) {
$view->addSectionHeader(
pht('Custom Enroll Message'),
PHUIPropertyListView::ICON_SUMMARY);
$view->addTextContent(
new PHUIRemarkupView($viewer, $custom_enroll));
}
return $view;
}
@ -103,6 +113,14 @@ final class PhabricatorAuthFactorProviderViewController
->setDisabled(!$can_edit)
->setWorkflow(!$can_edit));
$curtain->addAction(
id(new PhabricatorActionView())
->setName(pht('Customize Enroll Message'))
->setIcon('fa-commenting-o')
->setHref($this->getApplicationURI("mfa/message/{$id}/"))
->setDisabled(!$can_edit)
->setWorkflow(true));
return $curtain;
}

View file

@ -147,7 +147,7 @@ abstract class PhabricatorAuthFactor extends Phobject {
$viewer,
$challenges);
if ($new_challenges instanceof PhabricatorAuthFactorResult) {
if ($this->isAuthResult($new_challenges)) {
unset($unguarded);
return $new_challenges;
}
@ -200,7 +200,7 @@ abstract class PhabricatorAuthFactor extends Phobject {
return $result;
}
if (!($result instanceof PhabricatorAuthFactorResult)) {
if (!$this->isAuthResult($result)) {
throw new Exception(
pht(
'Expected "newResultFromIssuedChallenges()" to return null or '.
@ -232,7 +232,7 @@ abstract class PhabricatorAuthFactor extends Phobject {
$request,
$challenges);
if (!($result instanceof PhabricatorAuthFactorResult)) {
if (!$this->isAuthResult($result)) {
throw new Exception(
pht(
'Expected "newResultFromChallengeResponse()" to return an object '.
@ -408,6 +408,10 @@ abstract class PhabricatorAuthFactor extends Phobject {
$provider,
$user);
if ($this->isAuthResult($properties)) {
return $properties;
}
foreach ($properties as $key => $value) {
$sync_token->setTemporaryTokenProperty($key, $value);
}
@ -555,4 +559,8 @@ abstract class PhabricatorAuthFactor extends Phobject {
->execute();
}
final protected function isAuthResult($object) {
return ($object instanceof PhabricatorAuthFactorResult);
}
}

View file

@ -157,6 +157,10 @@ final class PhabricatorDuoAuthFactor
PhabricatorUser $user) {
$token = $this->loadMFASyncToken($provider, $request, $form, $user);
if ($this->isAuthResult($token)) {
$form->appendChild($this->newAutomaticControl($token));
return;
}
$enroll = $token->getTemporaryTokenProperty('duo.enroll');
$duo_id = $token->getTemporaryTokenProperty('duo.user-id');
@ -350,6 +354,7 @@ final class PhabricatorDuoAuthFactor
$external_uri = null;
$result_code = $result['response']['result'];
$status_message = $result['response']['status_msg'];
switch ($result_code) {
case 'auth':
case 'allow':
@ -376,7 +381,13 @@ final class PhabricatorDuoAuthFactor
return $this->newResult()
->setIsError(true)
->setErrorMessage(
pht('Your account is not permitted to access this system.'));
pht(
'Your Duo account ("%s") is not permitted to access this '.
'system. Contact your Duo administrator for help. '.
'The Duo preauth API responded with status message ("%s"): %s',
$duo_user,
$result_code,
$status_message));
}
// Duo's "/enroll" API isn't repeatable for the same username. If we're
@ -476,7 +487,10 @@ final class PhabricatorDuoAuthFactor
->setIsError(true)
->setErrorMessage(
pht(
'Duo has denied you access. Duo status message ("%s"): %s',
'Your Duo account ("%s") is not permitted to access this '.
'system. Contact your Duo administrator for help. The Duo '.
'preauth API responded with status message ("%s"): %s',
$duo_user,
$next_step,
$status_message));
}
@ -504,10 +518,7 @@ final class PhabricatorDuoAuthFactor
$push_info = array(
pht('Domain') => $this->getInstallDisplayName(),
);
foreach ($push_info as $k => $v) {
$push_info[$k] = rawurlencode($k).'='.rawurlencode($v);
}
$push_info = implode('&', $push_info);
$push_info = phutil_build_http_querystring($push_info);
$parameters = array(
'username' => $duo_user,

View file

@ -91,11 +91,7 @@ final class PhabricatorDuoFuture
$http_method = $this->getHTTPMethod();
ksort($data);
$data_parts = array();
foreach ($data as $key => $value) {
$data_parts[] = rawurlencode($key).'='.rawurlencode($value);
}
$data_parts = implode('&', $data_parts);
$data_parts = phutil_build_http_querystring($data);
$corpus = array(
$date,

View file

@ -57,6 +57,14 @@ final class PhabricatorAuthFactorProvider
return $this;
}
public function getEnrollMessage() {
return $this->getAuthFactorProviderProperty('enroll-message');
}
public function setEnrollMessage($message) {
return $this->setAuthFactorProviderProperty('enroll-message', $message);
}
public function attachFactor(PhabricatorAuthFactor $factor) {
$this->factor = $factor;
return $this;

View file

@ -0,0 +1,39 @@
<?php
final class PhabricatorAuthFactorProviderEnrollMessageTransaction
extends PhabricatorAuthFactorProviderTransactionType {
const TRANSACTIONTYPE = 'enroll-message';
public function generateOldValue($object) {
return $object->getEnrollMessage();
}
public function applyInternalEffects($object, $value) {
$object->setEnrollMessage($value);
}
public function getTitle() {
return pht(
'%s updated the enroll message.',
$this->renderAuthor());
}
public function hasChangeDetailView() {
return true;
}
public function getMailDiffSectionHeader() {
return pht('CHANGES TO ENROLL MESSAGE');
}
public function newChangeDetailView() {
$viewer = $this->getViewer();
return id(new PhabricatorApplicationTransactionTextDiffDetailView())
->setViewer($viewer)
->setOldText($this->getOldValue())
->setNewText($this->getNewValue());
}
}

View file

@ -12,7 +12,7 @@ final class PhabricatorAccessControlTestCase extends PhabricatorTestCase {
$root = dirname(phutil_get_library_root('phabricator'));
require_once $root.'/support/startup/PhabricatorStartup.php';
$application_configuration = new AphrontDefaultApplicationConfiguration();
$application_configuration = new AphrontApplicationConfiguration();
$host = 'meow.example.com';

View file

@ -11,14 +11,13 @@ final class PhabricatorExtensionsSetupCheck extends PhabricatorSetupCheck {
}
protected function executeChecks() {
// TODO: Make 'mbstring' and 'iconv' soft requirements.
// TODO: Make 'mbstring' a soft requirement.
$required = array(
'hash',
'json',
'openssl',
'mbstring',
'iconv',
'ctype',
// There is a tiny chance we might not need this, but a significant

View file

@ -416,6 +416,10 @@ final class PhabricatorExtraConfigSetupCheck extends PhabricatorSetupCheck {
'metamta.pholio.subject-prefix' => $prefix_reason,
'metamta.phriction.subject-prefix' => $prefix_reason,
'aphront.default-application-configuration-class' => pht(
'This ancient extension point has been replaced with other '.
'mechanisms, including "AphrontSite".'),
);
return $ancient_config;

View file

@ -63,6 +63,8 @@ final class PhabricatorConfigVersionController
$version_from_file);
}
$version_property_list->addProperty('php', phpversion());
$binaries = PhutilBinaryAnalyzer::getAllBinaries();
foreach ($binaries as $binary) {
if (!$binary->isBinaryAvailable()) {

View file

@ -36,14 +36,6 @@ final class PhabricatorExtendingPhabricatorConfigOptions
'occur. Specify a list of classes which extend '.
'PhabricatorEventListener here.'))
->addExample('MyEventListener', pht('Valid Setting')),
$this->newOption(
'aphront.default-application-configuration-class',
'class',
'AphrontDefaultApplicationConfiguration')
->setLocked(true)
->setBaseClass('AphrontApplicationConfiguration')
// TODO: This could probably use some better documentation.
->setDescription(pht('Application configuration class.')),
);
}

View file

@ -235,6 +235,32 @@ final class DifferentialRevisionEditEngine
->setConduitTypeDescription(pht('List of tasks.'))
->setValue(array());
$fields[] = id(new PhabricatorHandlesEditField())
->setKey('parents')
->setUseEdgeTransactions(true)
->setIsFormField(false)
->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
->setMetadataValue(
'edge:type',
DifferentialRevisionDependsOnRevisionEdgeType::EDGECONST)
->setDescription(pht('Parent revisions of this revision.'))
->setConduitDescription(pht('Change associated parent revisions.'))
->setConduitTypeDescription(pht('List of revisions.'))
->setValue(array());
$fields[] = id(new PhabricatorHandlesEditField())
->setKey('children')
->setUseEdgeTransactions(true)
->setIsFormField(false)
->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
->setMetadataValue(
'edge:type',
DifferentialRevisionDependedOnByRevisionEdgeType::EDGECONST)
->setDescription(pht('Child revisions of this revision.'))
->setConduitDescription(pht('Change associated child revisions.'))
->setConduitTypeDescription(pht('List of revisions.'))
->setValue(array());
$actions = DifferentialRevisionActionTransaction::loadAllActions();
$actions = msortv($actions, 'getRevisionActionOrderVector');

View file

@ -177,14 +177,21 @@ final class DifferentialDiffExtractionEngine extends Phobject {
'repository' => $repository,
));
$response = DiffusionQuery::callConduitWithDiffusionRequest(
$viewer,
$drequest,
'diffusion.filecontentquery',
array(
'commit' => $identifier,
'path' => $path,
));
try {
$response = DiffusionQuery::callConduitWithDiffusionRequest(
$viewer,
$drequest,
'diffusion.filecontentquery',
array(
'commit' => $identifier,
'path' => $path,
));
} catch (Exception $ex) {
// TODO: See PHI1044. This call may fail if the diff deleted the
// file. If the call fails, just detect a change for now. This should
// generally be made cleaner in the future.
return true;
}
$new_file_phid = $response['filePHID'];
if (!$new_file_phid) {

View file

@ -528,7 +528,7 @@ final class DiffusionServeController extends DiffusionController {
unset($query_data[$key]);
}
}
$query_string = http_build_query($query_data, '', '&');
$query_string = phutil_build_http_querystring($query_data);
// We're about to wipe out PATH with the rest of the environment, so
// resolve the binary first.

View file

@ -188,7 +188,7 @@ final class DiffusionRepositoryClusterEngine extends Phobject {
if ($this_version) {
$this_version = (int)$this_version->getRepositoryVersion();
} else {
$this_version = -1;
$this_version = null;
}
if ($versions) {
@ -197,7 +197,7 @@ final class DiffusionRepositoryClusterEngine extends Phobject {
// leader, we want to fetch from a leader and then update our version.
$max_version = (int)max(mpull($versions, 'getRepositoryVersion'));
if ($max_version > $this_version) {
if (($this_version === null) || ($max_version > $this_version)) {
if ($repository->isHosted()) {
$fetchable = array();
foreach ($versions as $version) {
@ -206,6 +206,7 @@ final class DiffusionRepositoryClusterEngine extends Phobject {
}
}
$this->synchronizeWorkingCopyFromDevices(
$fetchable,
$this_version,
@ -445,10 +446,10 @@ final class DiffusionRepositoryClusterEngine extends Phobject {
if ($this_version) {
$this_version = (int)$this_version->getRepositoryVersion();
} else {
$this_version = -1;
$this_version = null;
}
if ($new_version > $this_version) {
if (($this_version === null) || ($new_version > $this_version)) {
PhabricatorRepositoryWorkingCopyVersion::updateVersion(
$repository_phid,
$device_phid,

View file

@ -222,8 +222,10 @@ abstract class DiffusionSSHWorkflow extends PhabricatorSSHWorkflow {
pht('No repository "%s" exists!', $identifier));
}
$is_cluster = $this->getIsClusterRequest();
$protocol = PhabricatorRepositoryURI::BUILTIN_PROTOCOL_SSH;
if (!$repository->canServeProtocol($protocol, false)) {
if (!$repository->canServeProtocol($protocol, false, $is_cluster)) {
throw new Exception(
pht(
'This repository ("%s") is not available over SSH.',

View file

@ -83,6 +83,34 @@ EOTEXT
$custom_field_type = 'custom:PhabricatorCustomFieldConfigOptionType';
$subtype_type = 'projects.subtypes';
$subtype_default_key = PhabricatorEditEngineSubtype::SUBTYPE_DEFAULT;
$subtype_example = array(
array(
'key' => $subtype_default_key,
'name' => pht('Project'),
),
array(
'key' => 'team',
'name' => pht('Team'),
),
);
$subtype_example = id(new PhutilJSON())->encodeAsList($subtype_example);
$subtype_default = array(
array(
'key' => $subtype_default_key,
'name' => pht('Project'),
),
);
$subtype_description = $this->deformat(pht(<<<EOTEXT
Allows you to define project subtypes. For a more detailed description of
subtype configuration, see @{config:maniphest.subtypes}.
EOTEXT
));
return array(
$this->newOption('projects.custom-field-definitions', 'wild', array())
->setSummary(pht('Custom Projects fields.'))
@ -102,6 +130,11 @@ EOTEXT
$this->newOption('projects.colors', $colors_type, $default_colors)
->setSummary(pht('Adjust project colors.'))
->setDescription($colors_description),
$this->newOption('projects.subtypes', $subtype_type, $subtype_default)
->setSummary(pht('Define project subtypes.'))
->setDescription($subtype_description)
->addExample($subtype_example, pht('Simple Subtypes')),
);
}

View file

@ -0,0 +1,14 @@
<?php
final class PhabricatorProjectSubtypesConfigType
extends PhabricatorJSONConfigType {
const TYPEKEY = 'projects.subtypes';
public function validateStoredValue(
PhabricatorConfigOption $option,
$value) {
PhabricatorEditEngineSubtype::validateConfiguration($value);
}
}

View file

@ -51,6 +51,12 @@ final class PhabricatorProjectProfileController
$watch_action = $this->renderWatchAction($project);
$header->addActionLink($watch_action);
$subtype = $project->newSubtypeObject();
if ($subtype && $subtype->hasTagView()) {
$subtype_tag = $subtype->newTagView();
$header->addTag($subtype_tag);
}
$milestone_list = $this->buildMilestoneList($project);
$subproject_list = $this->buildSubprojectList($project);

View file

@ -249,6 +249,17 @@ final class PhabricatorProjectTransactionEditor
->rematerialize($new_parent);
}
// See PHI1046. Milestones are always in the Space of their parent project.
// Synchronize the database values to match the application values.
$conn = $object->establishConnection('w');
queryfx(
$conn,
'UPDATE %R SET spacePHID = %ns
WHERE parentProjectPHID = %s AND milestoneNumber IS NOT NULL',
$object,
$object->getSpacePHID(),
$object->getPHID());
return parent::applyFinalEffects($object, $xactions);
}

View file

@ -24,6 +24,7 @@ final class PhabricatorProjectQuery
private $maxDepth;
private $minMilestoneNumber;
private $maxMilestoneNumber;
private $subtypes;
private $status = 'status-any';
const STATUS_ANY = 'status-any';
@ -131,6 +132,11 @@ final class PhabricatorProjectQuery
return $this;
}
public function withSubtypes(array $subtypes) {
$this->subtypes = $subtypes;
return $this;
}
public function needMembers($need_members) {
$this->needMembers = $need_members;
return $this;
@ -618,6 +624,13 @@ final class PhabricatorProjectQuery
$this->maxMilestoneNumber);
}
if ($this->subtypes !== null) {
$where[] = qsprintf(
$conn,
'subtype IN (%Ls)',
$this->subtypes);
}
return $where;
}

View file

@ -19,6 +19,9 @@ final class PhabricatorProjectSearchEngine
}
protected function buildCustomSearchFields() {
$subtype_map = id(new PhabricatorProject())->newEditEngineSubtypeMap();
$hide_subtypes = ($subtype_map->getCount() == 1);
return array(
id(new PhabricatorSearchTextField())
->setLabel(pht('Name'))
@ -62,6 +65,14 @@ final class PhabricatorProjectSearchEngine
pht(
'Pass true to find only milestones, or false to omit '.
'milestones.')),
id(new PhabricatorSearchDatasourceField())
->setLabel(pht('Subtypes'))
->setKey('subtypes')
->setAliases(array('subtype'))
->setDescription(
pht('Search for projects with given subtypes.'))
->setDatasource(new PhabricatorProjectSubtypeDatasource())
->setIsHidden($hide_subtypes),
id(new PhabricatorSearchCheckboxesField())
->setLabel(pht('Icons'))
->setKey('icons')
@ -134,6 +145,10 @@ final class PhabricatorProjectSearchEngine
$query->withAncestorProjectPHIDs($map['ancestorPHIDs']);
}
if ($map['subtypes']) {
$query->withSubtypes($map['subtypes']);
}
return $query;
}

View file

@ -12,7 +12,8 @@ final class PhabricatorProject extends PhabricatorProjectDAO
PhabricatorFerretInterface,
PhabricatorConduitResultInterface,
PhabricatorColumnProxyInterface,
PhabricatorSpacesInterface {
PhabricatorSpacesInterface,
PhabricatorEditEngineSubtypeInterface {
protected $name;
protected $status = PhabricatorProjectStatus::STATUS_ACTIVE;
@ -40,6 +41,7 @@ final class PhabricatorProject extends PhabricatorProjectDAO
protected $properties = array();
protected $spacePHID;
protected $subtype;
private $memberPHIDs = self::ATTACHABLE;
private $watcherPHIDs = self::ATTACHABLE;
@ -102,6 +104,7 @@ final class PhabricatorProject extends PhabricatorProjectDAO
->setHasWorkboard(0)
->setHasMilestones(0)
->setHasSubprojects(0)
->setSubtype(PhabricatorEditEngineSubtype::SUBTYPE_DEFAULT)
->attachParentProject(null);
}
@ -237,6 +240,7 @@ final class PhabricatorProject extends PhabricatorProjectDAO
'projectPath' => 'hashpath64',
'projectDepth' => 'uint32',
'projectPathKey' => 'bytes4',
'subtype' => 'text64',
),
self::CONFIG_KEY_SCHEMA => array(
'key_icon' => array(
@ -765,6 +769,10 @@ final class PhabricatorProject extends PhabricatorProjectDAO
->setKey('slug')
->setType('string')
->setDescription(pht('Primary slug/hashtag.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('subtype')
->setType('string')
->setDescription(pht('Subtype of the project.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('milestone')
->setType('int?')
@ -814,6 +822,7 @@ final class PhabricatorProject extends PhabricatorProjectDAO
return array(
'name' => $this->getName(),
'slug' => $this->getPrimarySlug(),
'subtype' => $this->getSubtype(),
'milestone' => $milestone,
'depth' => (int)$this->getProjectDepth(),
'parent' => $parent_ref,
@ -873,4 +882,26 @@ final class PhabricatorProject extends PhabricatorProjectDAO
}
/* -( PhabricatorEditEngineSubtypeInterface )------------------------------ */
public function getEditEngineSubtype() {
return $this->getSubtype();
}
public function setEditEngineSubtype($value) {
return $this->setSubtype($value);
}
public function newEditEngineSubtypeMap() {
$config = PhabricatorEnv::getEnvConfig('projects.subtypes');
return PhabricatorEditEngineSubtype::newSubtypeMap($config);
}
public function newSubtypeObject() {
$subtype_key = $this->getEditEngineSubtype();
$subtype_map = $this->newEditEngineSubtypeMap();
return $subtype_map->getSubtype($subtype_key);
}
}

View file

@ -0,0 +1,45 @@
<?php
final class PhabricatorProjectSubtypeDatasource
extends PhabricatorTypeaheadDatasource {
public function getBrowseTitle() {
return pht('Browse Subtypes');
}
public function getPlaceholderText() {
return pht('Type a project subtype name...');
}
public function getDatasourceApplicationClass() {
return 'PhabricatorProjectApplication';
}
public function loadResults() {
$results = $this->buildResults();
return $this->filterResultsAgainstTokens($results);
}
protected function renderSpecialTokens(array $values) {
return $this->renderTokensFromResults($this->buildResults(), $values);
}
private function buildResults() {
$results = array();
$subtype_map = id(new PhabricatorProject())->newEditEngineSubtypeMap();
foreach ($subtype_map->getSubtypes() as $key => $subtype) {
$result = id(new PhabricatorTypeaheadResult())
->setIcon($subtype->getIcon())
->setColor($subtype->getColor())
->setPHID($key)
->setName($subtype->getName());
$results[$key] = $result;
}
return $results;
}
}

View file

@ -87,6 +87,13 @@ final class PhabricatorProjectListView extends AphrontView {
}
}
$subtype = $project->newSubtypeObject();
if ($subtype && $subtype->hasTagView()) {
$subtype_tag = $subtype->newTagView()
->setSlimShady(true);
$item->addAttribute($subtype_tag);
}
$list->addItem($item);
}

View file

@ -73,6 +73,9 @@ final class PhabricatorRepositoryPullLocalDaemon
$futures = array();
$queue = array();
$sync_wait = phutil_units('2 minutes in seconds');
$last_sync = array();
while (!$this->shouldExit()) {
PhabricatorCaches::destroyRequestCache();
$device = AlmanacKeys::getLiveDevice();
@ -96,6 +99,37 @@ final class PhabricatorRepositoryPullLocalDaemon
$retry_after[$message->getRepositoryID()] = time();
}
if ($device) {
$unsynchronized = $this->loadUnsynchronizedRepositories($device);
$now = PhabricatorTime::getNow();
foreach ($unsynchronized as $repository) {
$id = $repository->getID();
$this->log(
pht(
'Cluster repository ("%s") is out of sync on this node ("%s").',
$repository->getDisplayName(),
$device->getName()));
// Don't let out-of-sync conditions trigger updates too frequently,
// since we don't want to get trapped in a death spiral if sync is
// failing.
$sync_at = idx($last_sync, $id, 0);
$wait_duration = ($now - $sync_at);
if ($wait_duration < $sync_wait) {
$this->log(
pht(
'Skipping forced out-of-sync update because the last update '.
'was too recent (%s seconds ago).',
$wait_duration));
continue;
}
$last_sync[$id] = $now;
$retry_after[$id] = $now;
}
}
// 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.
@ -521,4 +555,41 @@ final class PhabricatorRepositoryPullLocalDaemon
return false;
}
private function loadUnsynchronizedRepositories(AlmanacDevice $device) {
$viewer = $this->getViewer();
$table = new PhabricatorRepositoryWorkingCopyVersion();
$conn = $table->establishConnection('r');
$our_versions = queryfx_all(
$conn,
'SELECT repositoryPHID, repositoryVersion FROM %R WHERE devicePHID = %s',
$table,
$device->getPHID());
$our_versions = ipull($our_versions, 'repositoryVersion', 'repositoryPHID');
$max_versions = queryfx_all(
$conn,
'SELECT repositoryPHID, MAX(repositoryVersion) maxVersion FROM %R
GROUP BY repositoryPHID',
$table);
$max_versions = ipull($max_versions, 'maxVersion', 'repositoryPHID');
$unsynchronized_phids = array();
foreach ($max_versions as $repository_phid => $max_version) {
$our_version = idx($our_versions, $repository_phid);
if (($our_version === null) || ($our_version < $max_version)) {
$unsynchronized_phids[] = $repository_phid;
}
}
if (!$unsynchronized_phids) {
return array();
}
return id(new PhabricatorRepositoryQuery())
->setViewer($viewer)
->withPHIDs($unsynchronized_phids)
->execute();
}
}

View file

@ -7,7 +7,7 @@ abstract class PhabricatorRepositoryManagementWorkflow
$identifiers = $args->getArg($param);
if (!$identifiers) {
return null;
return array();
}
$query = id(new PhabricatorRepositoryQuery())

View file

@ -1506,9 +1506,18 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO
return $this->setDetail('hosting-enabled', $enabled);
}
public function canServeProtocol($protocol, $write) {
if (!$this->isTracked()) {
return false;
public function canServeProtocol(
$protocol,
$write,
$is_intracluster = false) {
// See T13192. If a repository is inactive, don't serve it to users. We
// still synchronize it within the cluster and serve it to other repository
// nodes.
if (!$is_intracluster) {
if (!$this->isTracked()) {
return false;
}
}
$clone_uris = $this->getCloneURIs();

View file

@ -256,13 +256,16 @@ final class PhabricatorMultiFactorSettingsPanel
// sometimes requires us to push a challenge to them as a side effect (for
// example, with SMS).
if (!$request->isFormPost() || !$request->getBool('mfa.start')) {
$description = $selected_provider->getEnrollDescription($viewer);
$enroll = $selected_provider->getEnrollMessage();
if (!strlen($enroll)) {
$enroll = $selected_provider->getEnrollDescription($viewer);
}
return $this->newDialog()
->addHiddenInput('providerPHID', $selected_provider->getPHID())
->addHiddenInput('mfa.start', 1)
->setTitle(pht('Add Authentication Factor'))
->appendChild(new PHUIRemarkupView($viewer, $description))
->appendChild(new PHUIRemarkupView($viewer, $enroll))
->addCancelButton($cancel_uri)
->addSubmitButton($selected_provider->getEnrollButtonText($viewer));
}

View file

@ -68,6 +68,7 @@ final class TransactionSearchConduitAPIMethod
$object);
$xaction_query
->needHandles(false)
->withObjectPHIDs(array($object->getPHID()))
->setViewer($viewer);

View file

@ -1279,14 +1279,41 @@ abstract class PhabricatorEditEngine
$fields = $this->willBuildEditForm($object, $fields);
$request_path = $request->getRequestURI()
->setQueryParams(array());
$form = id(new AphrontFormView())
->setUser($viewer)
->setAction($request_path)
->addHiddenInput('editEngine', 'true');
foreach ($this->contextParameters as $param) {
$form->addHiddenInput($param, $request->getStr($param));
}
$requires_mfa = false;
if ($object instanceof PhabricatorEditEngineMFAInterface) {
$mfa_engine = PhabricatorEditEngineMFAEngine::newEngineForObject($object)
->setViewer($viewer);
$requires_mfa = $mfa_engine->shouldRequireMFA();
}
if ($requires_mfa) {
$message = pht(
'You will be required to provide multi-factor credentials to make '.
'changes.');
$form->appendChild(
id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_MFA)
->setErrors(array($message)));
// TODO: This should also set workflow on the form, so the user doesn't
// lose any form data if they "Cancel". However, Maniphest currently
// overrides "newEditResponse()" if the request is Ajax and returns a
// bag of view data. This can reasonably be cleaned up when workboards
// get their next iteration.
}
foreach ($fields as $field) {
if (!$field->getIsFormField()) {
continue;
@ -1565,11 +1592,19 @@ abstract class PhabricatorEditEngine
$comment_uri = $this->getEditURI($object, 'comment/');
$requires_mfa = false;
if ($object instanceof PhabricatorEditEngineMFAInterface) {
$mfa_engine = PhabricatorEditEngineMFAEngine::newEngineForObject($object)
->setViewer($viewer);
$requires_mfa = $mfa_engine->shouldRequireMFA();
}
$view = id(new PhabricatorApplicationTransactionCommentView())
->setUser($viewer)
->setObjectPHID($object_phid)
->setHeaderText($header_text)
->setAction($comment_uri)
->setRequiresMFA($requires_mfa)
->setSubmitButtonName($button_text);
$draft = PhabricatorVersionedDraft::loadDraft(

View file

@ -88,6 +88,7 @@ abstract class PhabricatorApplicationTransactionEditor
private $hasRequiredMFA = false;
private $request;
private $cancelURI;
private $extensions;
const STORAGE_ENCODING_BINARY = 'binary';
@ -1013,6 +1014,7 @@ abstract class PhabricatorApplicationTransactionEditor
}
$errors[] = $this->validateAllTransactions($object, $xactions);
$errors[] = $this->validateTransactionsWithExtensions($object, $xactions);
$errors = array_mergev($errors);
$continue_on_missing = $this->getContinueOnMissingFields();
@ -2667,9 +2669,15 @@ abstract class PhabricatorApplicationTransactionEditor
$transaction_type) {
$errors = array();
$factors = id(new PhabricatorAuthFactorConfig())->loadAllWhere(
'userPHID = %s',
$this->getActingAsPHID());
$factors = id(new PhabricatorAuthFactorConfigQuery())
->setViewer($this->getActor())
->withUserPHIDs(array($this->getActingAsPHID()))
->withFactorProviderStatuses(
array(
PhabricatorAuthFactorProviderStatus::STATUS_ACTIVE,
PhabricatorAuthFactorProviderStatus::STATUS_DEPRECATED,
))
->execute();
foreach ($xactions as $xaction) {
if (!$factors) {
@ -3286,7 +3294,7 @@ abstract class PhabricatorApplicationTransactionEditor
// move the other transactions down so they provide context above the
// actual comment.
$comment = $xaction->getBodyForMail();
$comment = $this->getBodyForTextMail($xaction);
if ($comment !== null) {
$is_comment = true;
$comments[] = array(
@ -3299,12 +3307,12 @@ abstract class PhabricatorApplicationTransactionEditor
}
if (!$is_comment || !$seen_comment) {
$header = $xaction->getTitleForTextMail();
$header = $this->getTitleForTextMail($xaction);
if ($header !== null) {
$headers[] = $header;
}
$header_html = $xaction->getTitleForHTMLMail();
$header_html = $this->getTitleForHTMLMail($xaction);
if ($header_html !== null) {
$headers_html[] = $header_html;
}
@ -3384,12 +3392,12 @@ abstract class PhabricatorApplicationTransactionEditor
// If this is not the first comment in the mail, add the header showing
// who wrote the comment immediately above the comment.
if (!$is_initial) {
$header = $xaction->getTitleForTextMail();
$header = $this->getTitleForTextMail($xaction);
if ($header !== null) {
$body->addRawPlaintextSection($header);
}
$header_html = $xaction->getTitleForHTMLMail();
$header_html = $this->getTitleForHTMLMail($xaction);
if ($header_html !== null) {
$body->addRawHTMLSection($header_html);
}
@ -4848,6 +4856,13 @@ abstract class PhabricatorApplicationTransactionEditor
}
private function requireMFA(PhabricatorLiskDAO $object, array $xactions) {
$actor = $this->getActor();
// Let omnipotent editors skip MFA. This is mostly aimed at scripts.
if ($actor->isOmnipotent()) {
return;
}
$editor_class = get_class($this);
$object_phid = $object->getPHID();
@ -4862,8 +4877,6 @@ abstract class PhabricatorApplicationTransactionEditor
$editor_class);
}
$actor = $this->getActor();
$request = $this->getRequest();
if ($request === null) {
$source_type = $this->getContentSource()->getSourceTypeConstant();
@ -4975,4 +4988,112 @@ abstract class PhabricatorApplicationTransactionEditor
return $xactions;
}
private function getTitleForTextMail(
PhabricatorApplicationTransaction $xaction) {
$type = $xaction->getTransactionType();
$xtype = $this->getModularTransactionType($type);
if ($xtype) {
$xtype = clone $xtype;
$xtype->setStorage($xaction);
$comment = $xtype->getTitleForTextMail();
if ($comment !== false) {
return $comment;
}
}
return $xaction->getTitleForTextMail();
}
private function getTitleForHTMLMail(
PhabricatorApplicationTransaction $xaction) {
$type = $xaction->getTransactionType();
$xtype = $this->getModularTransactionType($type);
if ($xtype) {
$xtype = clone $xtype;
$xtype->setStorage($xaction);
$comment = $xtype->getTitleForHTMLMail();
if ($comment !== false) {
return $comment;
}
}
return $xaction->getTitleForHTMLMail();
}
private function getBodyForTextMail(
PhabricatorApplicationTransaction $xaction) {
$type = $xaction->getTransactionType();
$xtype = $this->getModularTransactionType($type);
if ($xtype) {
$xtype = clone $xtype;
$xtype->setStorage($xaction);
$comment = $xtype->getBodyForTextMail();
if ($comment !== false) {
return $comment;
}
}
return $xaction->getBodyForMail();
}
/* -( Extensions )--------------------------------------------------------- */
private function validateTransactionsWithExtensions(
PhabricatorLiskDAO $object,
array $xactions) {
$errors = array();
$extensions = $this->getEditorExtensions();
foreach ($extensions as $extension) {
$extension_errors = $extension
->setObject($object)
->validateTransactions($object, $xactions);
assert_instances_of(
$extension_errors,
'PhabricatorApplicationTransactionValidationError');
$errors[] = $extension_errors;
}
return array_mergev($errors);
}
private function getEditorExtensions() {
if ($this->extensions === null) {
$this->extensions = $this->newEditorExtensions();
}
return $this->extensions;
}
private function newEditorExtensions() {
$extensions = PhabricatorEditorExtension::getAllExtensions();
$actor = $this->getActor();
$object = $this->object;
foreach ($extensions as $key => $extension) {
$extension = id(clone $extension)
->setViewer($actor)
->setEditor($this)
->setObject($object);
if (!$extension->supportsObject($this, $object)) {
unset($extensions[$key]);
continue;
}
$extensions[$key] = $extension;
}
return $extensions;
}
}

View file

@ -0,0 +1,83 @@
<?php
abstract class PhabricatorEditorExtension
extends Phobject {
private $viewer;
private $editor;
private $object;
final public function getExtensionKey() {
return $this->getPhobjectClassConstant('EXTENSIONKEY');
}
final public function setEditor(
PhabricatorApplicationTransactionEditor $editor) {
$this->editor = $editor;
return $this;
}
final public function getEditor() {
return $this->editor;
}
final public function setViewer(PhabricatorUser $viewer) {
$this->viewer = $viewer;
return $this;
}
final public function getViewer() {
return $this->viewer;
}
final public function setObject(
PhabricatorApplicationTransactionInterface $object) {
$this->object = $object;
return $this;
}
final public static function getAllExtensions() {
return id(new PhutilClassMapQuery())
->setAncestorClass(__CLASS__)
->setUniqueMethod('getExtensionKey')
->execute();
}
abstract public function getExtensionName();
public function supportsObject(
PhabricatorApplicationTransactionEditor $editor,
PhabricatorApplicationTransactionInterface $object) {
return true;
}
public function validateTransactions($object, array $xactions) {
return array();
}
final protected function newTransactionError(
PhabricatorApplicationTransaction $xaction,
$title,
$message) {
return new PhabricatorApplicationTransactionValidationError(
$xaction->getTransactionType(),
$title,
$message,
$xaction);
}
final protected function newRequiredTransasctionError(
PhabricatorApplicationTransaction $xaction,
$message) {
return $this->newError($xaction, pht('Required'), $message)
->setIsMissingFieldError(true);
}
final protected function newInvalidTransactionError(
PhabricatorApplicationTransaction $xaction,
$message) {
return $this->newTransactionError($xaction, pht('Invalid'), $message);
}
}

View file

@ -0,0 +1,40 @@
<?php
final class PhabricatorEditorExtensionModule
extends PhabricatorConfigModule {
public function getModuleKey() {
return 'editor';
}
public function getModuleName() {
return pht('Engine: Editor');
}
public function renderModuleStatus(AphrontRequest $request) {
$viewer = $request->getViewer();
$extensions = PhabricatorEditorExtension::getAllExtensions();
$rows = array();
foreach ($extensions as $extension) {
$rows[] = array(
get_class($extension),
$extension->getExtensionName(),
);
}
return id(new AphrontTableView($rows))
->setHeaders(
array(
pht('Class'),
pht('Name'),
))
->setColumnClasses(
array(
null,
'wide pri',
));
}
}

View file

@ -740,8 +740,9 @@ abstract class PhabricatorApplicationTransaction
switch ($this->getTransactionType()) {
case PhabricatorTransactions::TYPE_TOKEN:
case PhabricatorTransactions::TYPE_MFA:
return true;
case PhabricatorTransactions::TYPE_EDGE:
case PhabricatorTransactions::TYPE_EDGE:
$edge_type = $this->getMetadataValue('edge:type');
switch ($edge_type) {
case PhabricatorObjectMentionsObjectEdgeType::EDGECONST:

View file

@ -431,4 +431,68 @@ abstract class PhabricatorModularTransactionType
return false;
}
// NOTE: See T12921. These APIs are somewhat aspirational. For now, all of
// these use "TARGET_TEXT" (even the HTML methods!) and the body methods
// actually return Remarkup, not text or HTML.
final public function getTitleForTextMail() {
return $this->getTitleForMailWithRenderingTarget(
PhabricatorApplicationTransaction::TARGET_TEXT);
}
final public function getTitleForHTMLMail() {
return $this->getTitleForMailWithRenderingTarget(
PhabricatorApplicationTransaction::TARGET_TEXT);
}
final public function getBodyForTextMail() {
return $this->getBodyForMailWithRenderingTarget(
PhabricatorApplicationTransaction::TARGET_TEXT);
}
final public function getBodyForHTMLMail() {
return $this->getBodyForMailWithRenderingTarget(
PhabricatorApplicationTransaction::TARGET_TEXT);
}
private function getTitleForMailWithRenderingTarget($target) {
$storage = $this->getStorage();
$old_target = $storage->getRenderingTarget();
try {
$storage->setRenderingTarget($target);
$result = $this->getTitleForMail();
} catch (Exception $ex) {
$storage->setRenderingTarget($old_target);
throw $ex;
}
$storage->setRenderingTarget($old_target);
return $result;
}
private function getBodyForMailWithRenderingTarget($target) {
$storage = $this->getStorage();
$old_target = $storage->getRenderingTarget();
try {
$storage->setRenderingTarget($target);
$result = $this->getBodyForMail();
} catch (Exception $ex) {
$storage->setRenderingTarget($old_target);
throw $ex;
}
$storage->setRenderingTarget($old_target);
return $result;
}
protected function getTitleForMail() {
return false;
}
protected function getBodyForMail() {
return false;
}
}

View file

@ -1,9 +1,7 @@
<?php
/**
* @concrete-extensible
*/
class PhabricatorApplicationTransactionCommentView extends AphrontView {
final class PhabricatorApplicationTransactionCommentView
extends AphrontView {
private $submitButtonName;
private $action;
@ -24,6 +22,7 @@ class PhabricatorApplicationTransactionCommentView extends AphrontView {
private $infoView;
private $editEngineLock;
private $noBorder;
private $requiresMFA;
private $currentVersion;
private $versionedDraft;
@ -160,6 +159,15 @@ class PhabricatorApplicationTransactionCommentView extends AphrontView {
return $this->editEngineLock;
}
public function setRequiresMFA($requires_mfa) {
$this->requiresMFA = $requires_mfa;
return $this;
}
public function getRequiresMFA() {
return $this->requiresMFA;
}
public function setTransactionTimeline(
PhabricatorApplicationTransactionView $timeline) {
@ -187,8 +195,8 @@ class PhabricatorApplicationTransactionCommentView extends AphrontView {
));
}
$user = $this->getUser();
if (!$user->isLoggedIn()) {
$viewer = $this->getViewer();
if (!$viewer->isLoggedIn()) {
$uri = id(new PhutilURI('/login/'))
->setQueryParam('next', (string)$this->getRequestURI());
return id(new PHUIObjectBoxView())
@ -203,6 +211,25 @@ class PhabricatorApplicationTransactionCommentView extends AphrontView {
pht('Log In to Comment')));
}
if ($this->getRequiresMFA()) {
if (!$viewer->getIsEnrolledInMultiFactor()) {
$viewer->updateMultiFactorEnrollment();
if (!$viewer->getIsEnrolledInMultiFactor()) {
$messages = array();
$messages[] = pht(
'You must provide multi-factor credentials to comment or make '.
'changes, but you do not have multi-factor authentication '.
'configured on your account.');
$messages[] = pht(
'To continue, configure multi-factor authentication in Settings.');
return id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_MFA)
->setErrors($messages);
}
}
}
$data = array();
$comment = $this->renderCommentPanel();
@ -226,7 +253,7 @@ class PhabricatorApplicationTransactionCommentView extends AphrontView {
}
require_celerity_resource('phui-comment-form-css');
$image_uri = $user->getProfileImageURI();
$image_uri = $viewer->getProfileImageURI();
$image = phutil_tag(
'div',
array(
@ -388,6 +415,17 @@ class PhabricatorApplicationTransactionCommentView extends AphrontView {
$form->appendChild($info_view);
}
if ($this->getRequiresMFA()) {
$message = pht(
'You will be required to provide multi-factor credentials to '.
'comment or make changes.');
$form->appendChild(
id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_MFA)
->setErrors(array($message)));
}
$form->appendChild($invisi_bar);
$form->addClass('phui-comment-has-actions');