1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2025-01-23 21:18:19 +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 = $parser->getHeaders();
$headers['subject'] = iconv_mime_decode($headers['subject'], 0, 'UTF-8'); $headers['subject'] = phutil_decode_mime_header($headers['subject']);
$headers['from'] = iconv_mime_decode($headers['from'], 0, 'UTF-8'); $headers['from'] = phutil_decode_mime_header($headers['from']);
if ($args->getArg('process-duplicates')) { if ($args->getArg('process-duplicates')) {
$headers['message-id'] = Filesystem::readRandomCharacters(64); $headers['message-id'] = Filesystem::readRandomCharacters(64);

View file

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

View file

@ -5,55 +5,81 @@
* @task response Response Handling * @task response Response Handling
* @task exception Exception Handling * @task exception Exception Handling
*/ */
abstract class AphrontApplicationConfiguration extends Phobject { final class AphrontApplicationConfiguration
extends Phobject {
private $request; private $request;
private $host; private $host;
private $path; private $path;
private $console; private $console;
abstract public function buildRequest(); public function buildRequest() {
abstract public function build404Controller(); $parser = new PhutilQueryStringParser();
abstract public function buildRedirectController($uri, $external);
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; $this->request = $request;
return $this; return $this;
} }
final public function getRequest() { public function getRequest() {
return $this->request; return $this->request;
} }
final public function getConsole() { public function getConsole() {
return $this->console; return $this->console;
} }
final public function setConsole($console) { public function setConsole($console) {
$this->console = $console; $this->console = $console;
return $this; return $this;
} }
final public function setHost($host) { public function setHost($host) {
$this->host = $host; $this->host = $host;
return $this; return $this;
} }
final public function getHost() { public function getHost() {
return $this->host; return $this->host;
} }
final public function setPath($path) { public function setPath($path) {
$this->path = $path; $this->path = $path;
return $this; return $this;
} }
final public function getPath() { public function getPath() {
return $this->path; return $this->path;
} }
public function willBuildRequest() {}
/** /**
* @phutil-external-symbol class PhabricatorStartup * @phutil-external-symbol class PhabricatorStartup
@ -83,6 +109,8 @@ abstract class AphrontApplicationConfiguration extends Phobject {
PhabricatorStartup::beginStartupPhase('env.init'); PhabricatorStartup::beginStartupPhase('env.init');
self::readHTTPPOSTData();
try { try {
PhabricatorEnv::initializeWebEnvironment(); PhabricatorEnv::initializeWebEnvironment();
$database_exception = null; $database_exception = null;
@ -142,16 +170,10 @@ abstract class AphrontApplicationConfiguration extends Phobject {
$host = AphrontRequest::getHTTPHeader('Host'); $host = AphrontRequest::getHTTPHeader('Host');
$path = $_REQUEST['__path__']; $path = $_REQUEST['__path__'];
switch ($host) { $application = new self();
default:
$config_key = 'aphront.default-application-configuration-class';
$application = PhabricatorEnv::newObjectFromConfig($config_key);
break;
}
$application->setHost($host); $application->setHost($host);
$application->setPath($path); $application->setPath($path);
$application->willBuildRequest();
$request = $application->buildRequest(); $request = $application->buildRequest();
// Now that we have a request, convert the write guard into one which // Now that we have a request, convert the write guard into one which
@ -313,7 +335,7 @@ abstract class AphrontApplicationConfiguration extends Phobject {
* parameters. * parameters.
* @task routing * @task routing
*/ */
final private function buildController() { private function buildController() {
$request = $this->getRequest(); $request = $this->getRequest();
// If we're configured to operate in cluster mode, reject requests which // If we're configured to operate in cluster mode, reject requests which
@ -708,4 +730,88 @@ abstract class AphrontApplicationConfiguration extends Phobject {
->setContent($result); ->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(); $form_layout = $form->buildLayoutView();
if ($is_upgrade) { if ($is_upgrade) {
$messages = array( $message = pht(
pht( 'You are taking an action which requires you to enter '.
'You are taking an action which requires you to enter '. 'high security.');
'high security.'),
);
$info_view = id(new PHUIInfoView()) $info_view = id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_MFA) ->setSeverity(PHUIInfoView::SEVERITY_MFA)
->setErrors($messages); ->setErrors(array($message));
$dialog $dialog
->appendChild($info_view) ->appendChild($info_view)
@ -100,12 +98,18 @@ final class PhabricatorHighSecurityRequestExceptionHandler
'period of time. When you are finished taking sensitive '. 'period of time. When you are finished taking sensitive '.
'actions, you should leave high security.')); 'actions, you should leave high security.'));
} else { } 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 $dialog
->appendChild($info_view)
->setErrors( ->setErrors(
array( array(
pht(
'You are taking an action which requires you to provide '.
'multi-factor credentials.'),
)) ))
->appendChild($form_layout); ->appendChild($form_layout);
} }

View file

@ -95,6 +95,8 @@ final class PhabricatorAuthApplication extends PhabricatorApplication {
'PhabricatorAuthFactorProviderEditController', 'PhabricatorAuthFactorProviderEditController',
'(?P<id>[1-9]\d*)/' => '(?P<id>[1-9]\d*)/' =>
'PhabricatorAuthFactorProviderViewController', 'PhabricatorAuthFactorProviderViewController',
'message/(?P<id>[1-9]\d*)/' =>
'PhabricatorAuthFactorProviderMessageController',
), ),
'message/' => array( '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'), pht('Factor Type'),
$provider->getFactor()->getFactorName()); $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; return $view;
} }
@ -103,6 +113,14 @@ final class PhabricatorAuthFactorProviderViewController
->setDisabled(!$can_edit) ->setDisabled(!$can_edit)
->setWorkflow(!$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; return $curtain;
} }

View file

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

View file

@ -157,6 +157,10 @@ final class PhabricatorDuoAuthFactor
PhabricatorUser $user) { PhabricatorUser $user) {
$token = $this->loadMFASyncToken($provider, $request, $form, $user); $token = $this->loadMFASyncToken($provider, $request, $form, $user);
if ($this->isAuthResult($token)) {
$form->appendChild($this->newAutomaticControl($token));
return;
}
$enroll = $token->getTemporaryTokenProperty('duo.enroll'); $enroll = $token->getTemporaryTokenProperty('duo.enroll');
$duo_id = $token->getTemporaryTokenProperty('duo.user-id'); $duo_id = $token->getTemporaryTokenProperty('duo.user-id');
@ -350,6 +354,7 @@ final class PhabricatorDuoAuthFactor
$external_uri = null; $external_uri = null;
$result_code = $result['response']['result']; $result_code = $result['response']['result'];
$status_message = $result['response']['status_msg'];
switch ($result_code) { switch ($result_code) {
case 'auth': case 'auth':
case 'allow': case 'allow':
@ -376,7 +381,13 @@ final class PhabricatorDuoAuthFactor
return $this->newResult() return $this->newResult()
->setIsError(true) ->setIsError(true)
->setErrorMessage( ->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 // Duo's "/enroll" API isn't repeatable for the same username. If we're
@ -476,7 +487,10 @@ final class PhabricatorDuoAuthFactor
->setIsError(true) ->setIsError(true)
->setErrorMessage( ->setErrorMessage(
pht( 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, $next_step,
$status_message)); $status_message));
} }
@ -504,10 +518,7 @@ final class PhabricatorDuoAuthFactor
$push_info = array( $push_info = array(
pht('Domain') => $this->getInstallDisplayName(), pht('Domain') => $this->getInstallDisplayName(),
); );
foreach ($push_info as $k => $v) { $push_info = phutil_build_http_querystring($push_info);
$push_info[$k] = rawurlencode($k).'='.rawurlencode($v);
}
$push_info = implode('&', $push_info);
$parameters = array( $parameters = array(
'username' => $duo_user, 'username' => $duo_user,

View file

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

View file

@ -57,6 +57,14 @@ final class PhabricatorAuthFactorProvider
return $this; 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) { public function attachFactor(PhabricatorAuthFactor $factor) {
$this->factor = $factor; $this->factor = $factor;
return $this; 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')); $root = dirname(phutil_get_library_root('phabricator'));
require_once $root.'/support/startup/PhabricatorStartup.php'; require_once $root.'/support/startup/PhabricatorStartup.php';
$application_configuration = new AphrontDefaultApplicationConfiguration(); $application_configuration = new AphrontApplicationConfiguration();
$host = 'meow.example.com'; $host = 'meow.example.com';

View file

@ -11,14 +11,13 @@ final class PhabricatorExtensionsSetupCheck extends PhabricatorSetupCheck {
} }
protected function executeChecks() { protected function executeChecks() {
// TODO: Make 'mbstring' and 'iconv' soft requirements. // TODO: Make 'mbstring' a soft requirement.
$required = array( $required = array(
'hash', 'hash',
'json', 'json',
'openssl', 'openssl',
'mbstring', 'mbstring',
'iconv',
'ctype', 'ctype',
// There is a tiny chance we might not need this, but a significant // 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.pholio.subject-prefix' => $prefix_reason,
'metamta.phriction.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; return $ancient_config;

View file

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

View file

@ -36,14 +36,6 @@ final class PhabricatorExtendingPhabricatorConfigOptions
'occur. Specify a list of classes which extend '. 'occur. Specify a list of classes which extend '.
'PhabricatorEventListener here.')) 'PhabricatorEventListener here.'))
->addExample('MyEventListener', pht('Valid Setting')), ->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.')) ->setConduitTypeDescription(pht('List of tasks.'))
->setValue(array()); ->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 = DifferentialRevisionActionTransaction::loadAllActions();
$actions = msortv($actions, 'getRevisionActionOrderVector'); $actions = msortv($actions, 'getRevisionActionOrderVector');

View file

@ -177,14 +177,21 @@ final class DifferentialDiffExtractionEngine extends Phobject {
'repository' => $repository, 'repository' => $repository,
)); ));
$response = DiffusionQuery::callConduitWithDiffusionRequest( try {
$viewer, $response = DiffusionQuery::callConduitWithDiffusionRequest(
$drequest, $viewer,
'diffusion.filecontentquery', $drequest,
array( 'diffusion.filecontentquery',
'commit' => $identifier, array(
'path' => $path, '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']; $new_file_phid = $response['filePHID'];
if (!$new_file_phid) { if (!$new_file_phid) {

View file

@ -528,7 +528,7 @@ final class DiffusionServeController extends DiffusionController {
unset($query_data[$key]); 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 // We're about to wipe out PATH with the rest of the environment, so
// resolve the binary first. // resolve the binary first.

View file

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

View file

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

View file

@ -83,6 +83,34 @@ EOTEXT
$custom_field_type = 'custom:PhabricatorCustomFieldConfigOptionType'; $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( return array(
$this->newOption('projects.custom-field-definitions', 'wild', array()) $this->newOption('projects.custom-field-definitions', 'wild', array())
->setSummary(pht('Custom Projects fields.')) ->setSummary(pht('Custom Projects fields.'))
@ -102,6 +130,11 @@ EOTEXT
$this->newOption('projects.colors', $colors_type, $default_colors) $this->newOption('projects.colors', $colors_type, $default_colors)
->setSummary(pht('Adjust project colors.')) ->setSummary(pht('Adjust project colors.'))
->setDescription($colors_description), ->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); $watch_action = $this->renderWatchAction($project);
$header->addActionLink($watch_action); $header->addActionLink($watch_action);
$subtype = $project->newSubtypeObject();
if ($subtype && $subtype->hasTagView()) {
$subtype_tag = $subtype->newTagView();
$header->addTag($subtype_tag);
}
$milestone_list = $this->buildMilestoneList($project); $milestone_list = $this->buildMilestoneList($project);
$subproject_list = $this->buildSubprojectList($project); $subproject_list = $this->buildSubprojectList($project);

View file

@ -249,6 +249,17 @@ final class PhabricatorProjectTransactionEditor
->rematerialize($new_parent); ->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); return parent::applyFinalEffects($object, $xactions);
} }

View file

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

View file

@ -19,6 +19,9 @@ final class PhabricatorProjectSearchEngine
} }
protected function buildCustomSearchFields() { protected function buildCustomSearchFields() {
$subtype_map = id(new PhabricatorProject())->newEditEngineSubtypeMap();
$hide_subtypes = ($subtype_map->getCount() == 1);
return array( return array(
id(new PhabricatorSearchTextField()) id(new PhabricatorSearchTextField())
->setLabel(pht('Name')) ->setLabel(pht('Name'))
@ -62,6 +65,14 @@ final class PhabricatorProjectSearchEngine
pht( pht(
'Pass true to find only milestones, or false to omit '. 'Pass true to find only milestones, or false to omit '.
'milestones.')), '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()) id(new PhabricatorSearchCheckboxesField())
->setLabel(pht('Icons')) ->setLabel(pht('Icons'))
->setKey('icons') ->setKey('icons')
@ -134,6 +145,10 @@ final class PhabricatorProjectSearchEngine
$query->withAncestorProjectPHIDs($map['ancestorPHIDs']); $query->withAncestorProjectPHIDs($map['ancestorPHIDs']);
} }
if ($map['subtypes']) {
$query->withSubtypes($map['subtypes']);
}
return $query; return $query;
} }

View file

@ -12,7 +12,8 @@ final class PhabricatorProject extends PhabricatorProjectDAO
PhabricatorFerretInterface, PhabricatorFerretInterface,
PhabricatorConduitResultInterface, PhabricatorConduitResultInterface,
PhabricatorColumnProxyInterface, PhabricatorColumnProxyInterface,
PhabricatorSpacesInterface { PhabricatorSpacesInterface,
PhabricatorEditEngineSubtypeInterface {
protected $name; protected $name;
protected $status = PhabricatorProjectStatus::STATUS_ACTIVE; protected $status = PhabricatorProjectStatus::STATUS_ACTIVE;
@ -40,6 +41,7 @@ final class PhabricatorProject extends PhabricatorProjectDAO
protected $properties = array(); protected $properties = array();
protected $spacePHID; protected $spacePHID;
protected $subtype;
private $memberPHIDs = self::ATTACHABLE; private $memberPHIDs = self::ATTACHABLE;
private $watcherPHIDs = self::ATTACHABLE; private $watcherPHIDs = self::ATTACHABLE;
@ -102,6 +104,7 @@ final class PhabricatorProject extends PhabricatorProjectDAO
->setHasWorkboard(0) ->setHasWorkboard(0)
->setHasMilestones(0) ->setHasMilestones(0)
->setHasSubprojects(0) ->setHasSubprojects(0)
->setSubtype(PhabricatorEditEngineSubtype::SUBTYPE_DEFAULT)
->attachParentProject(null); ->attachParentProject(null);
} }
@ -237,6 +240,7 @@ final class PhabricatorProject extends PhabricatorProjectDAO
'projectPath' => 'hashpath64', 'projectPath' => 'hashpath64',
'projectDepth' => 'uint32', 'projectDepth' => 'uint32',
'projectPathKey' => 'bytes4', 'projectPathKey' => 'bytes4',
'subtype' => 'text64',
), ),
self::CONFIG_KEY_SCHEMA => array( self::CONFIG_KEY_SCHEMA => array(
'key_icon' => array( 'key_icon' => array(
@ -765,6 +769,10 @@ final class PhabricatorProject extends PhabricatorProjectDAO
->setKey('slug') ->setKey('slug')
->setType('string') ->setType('string')
->setDescription(pht('Primary slug/hashtag.')), ->setDescription(pht('Primary slug/hashtag.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('subtype')
->setType('string')
->setDescription(pht('Subtype of the project.')),
id(new PhabricatorConduitSearchFieldSpecification()) id(new PhabricatorConduitSearchFieldSpecification())
->setKey('milestone') ->setKey('milestone')
->setType('int?') ->setType('int?')
@ -814,6 +822,7 @@ final class PhabricatorProject extends PhabricatorProjectDAO
return array( return array(
'name' => $this->getName(), 'name' => $this->getName(),
'slug' => $this->getPrimarySlug(), 'slug' => $this->getPrimarySlug(),
'subtype' => $this->getSubtype(),
'milestone' => $milestone, 'milestone' => $milestone,
'depth' => (int)$this->getProjectDepth(), 'depth' => (int)$this->getProjectDepth(),
'parent' => $parent_ref, '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); $list->addItem($item);
} }

View file

@ -73,6 +73,9 @@ final class PhabricatorRepositoryPullLocalDaemon
$futures = array(); $futures = array();
$queue = array(); $queue = array();
$sync_wait = phutil_units('2 minutes in seconds');
$last_sync = array();
while (!$this->shouldExit()) { while (!$this->shouldExit()) {
PhabricatorCaches::destroyRequestCache(); PhabricatorCaches::destroyRequestCache();
$device = AlmanacKeys::getLiveDevice(); $device = AlmanacKeys::getLiveDevice();
@ -96,6 +99,37 @@ final class PhabricatorRepositoryPullLocalDaemon
$retry_after[$message->getRepositoryID()] = time(); $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 // 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 // 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. // causes us to sleep for the minimum amount of time.
@ -521,4 +555,41 @@ final class PhabricatorRepositoryPullLocalDaemon
return false; 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); $identifiers = $args->getArg($param);
if (!$identifiers) { if (!$identifiers) {
return null; return array();
} }
$query = id(new PhabricatorRepositoryQuery()) $query = id(new PhabricatorRepositoryQuery())

View file

@ -1506,9 +1506,18 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO
return $this->setDetail('hosting-enabled', $enabled); return $this->setDetail('hosting-enabled', $enabled);
} }
public function canServeProtocol($protocol, $write) { public function canServeProtocol(
if (!$this->isTracked()) { $protocol,
return false; $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(); $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 // sometimes requires us to push a challenge to them as a side effect (for
// example, with SMS). // example, with SMS).
if (!$request->isFormPost() || !$request->getBool('mfa.start')) { 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() return $this->newDialog()
->addHiddenInput('providerPHID', $selected_provider->getPHID()) ->addHiddenInput('providerPHID', $selected_provider->getPHID())
->addHiddenInput('mfa.start', 1) ->addHiddenInput('mfa.start', 1)
->setTitle(pht('Add Authentication Factor')) ->setTitle(pht('Add Authentication Factor'))
->appendChild(new PHUIRemarkupView($viewer, $description)) ->appendChild(new PHUIRemarkupView($viewer, $enroll))
->addCancelButton($cancel_uri) ->addCancelButton($cancel_uri)
->addSubmitButton($selected_provider->getEnrollButtonText($viewer)); ->addSubmitButton($selected_provider->getEnrollButtonText($viewer));
} }

View file

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

View file

@ -1279,14 +1279,41 @@ abstract class PhabricatorEditEngine
$fields = $this->willBuildEditForm($object, $fields); $fields = $this->willBuildEditForm($object, $fields);
$request_path = $request->getRequestURI()
->setQueryParams(array());
$form = id(new AphrontFormView()) $form = id(new AphrontFormView())
->setUser($viewer) ->setUser($viewer)
->setAction($request_path)
->addHiddenInput('editEngine', 'true'); ->addHiddenInput('editEngine', 'true');
foreach ($this->contextParameters as $param) { foreach ($this->contextParameters as $param) {
$form->addHiddenInput($param, $request->getStr($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) { foreach ($fields as $field) {
if (!$field->getIsFormField()) { if (!$field->getIsFormField()) {
continue; continue;
@ -1565,11 +1592,19 @@ abstract class PhabricatorEditEngine
$comment_uri = $this->getEditURI($object, 'comment/'); $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()) $view = id(new PhabricatorApplicationTransactionCommentView())
->setUser($viewer) ->setUser($viewer)
->setObjectPHID($object_phid) ->setObjectPHID($object_phid)
->setHeaderText($header_text) ->setHeaderText($header_text)
->setAction($comment_uri) ->setAction($comment_uri)
->setRequiresMFA($requires_mfa)
->setSubmitButtonName($button_text); ->setSubmitButtonName($button_text);
$draft = PhabricatorVersionedDraft::loadDraft( $draft = PhabricatorVersionedDraft::loadDraft(

View file

@ -88,6 +88,7 @@ abstract class PhabricatorApplicationTransactionEditor
private $hasRequiredMFA = false; private $hasRequiredMFA = false;
private $request; private $request;
private $cancelURI; private $cancelURI;
private $extensions;
const STORAGE_ENCODING_BINARY = 'binary'; const STORAGE_ENCODING_BINARY = 'binary';
@ -1013,6 +1014,7 @@ abstract class PhabricatorApplicationTransactionEditor
} }
$errors[] = $this->validateAllTransactions($object, $xactions); $errors[] = $this->validateAllTransactions($object, $xactions);
$errors[] = $this->validateTransactionsWithExtensions($object, $xactions);
$errors = array_mergev($errors); $errors = array_mergev($errors);
$continue_on_missing = $this->getContinueOnMissingFields(); $continue_on_missing = $this->getContinueOnMissingFields();
@ -2667,9 +2669,15 @@ abstract class PhabricatorApplicationTransactionEditor
$transaction_type) { $transaction_type) {
$errors = array(); $errors = array();
$factors = id(new PhabricatorAuthFactorConfig())->loadAllWhere( $factors = id(new PhabricatorAuthFactorConfigQuery())
'userPHID = %s', ->setViewer($this->getActor())
$this->getActingAsPHID()); ->withUserPHIDs(array($this->getActingAsPHID()))
->withFactorProviderStatuses(
array(
PhabricatorAuthFactorProviderStatus::STATUS_ACTIVE,
PhabricatorAuthFactorProviderStatus::STATUS_DEPRECATED,
))
->execute();
foreach ($xactions as $xaction) { foreach ($xactions as $xaction) {
if (!$factors) { if (!$factors) {
@ -3286,7 +3294,7 @@ abstract class PhabricatorApplicationTransactionEditor
// move the other transactions down so they provide context above the // move the other transactions down so they provide context above the
// actual comment. // actual comment.
$comment = $xaction->getBodyForMail(); $comment = $this->getBodyForTextMail($xaction);
if ($comment !== null) { if ($comment !== null) {
$is_comment = true; $is_comment = true;
$comments[] = array( $comments[] = array(
@ -3299,12 +3307,12 @@ abstract class PhabricatorApplicationTransactionEditor
} }
if (!$is_comment || !$seen_comment) { if (!$is_comment || !$seen_comment) {
$header = $xaction->getTitleForTextMail(); $header = $this->getTitleForTextMail($xaction);
if ($header !== null) { if ($header !== null) {
$headers[] = $header; $headers[] = $header;
} }
$header_html = $xaction->getTitleForHTMLMail(); $header_html = $this->getTitleForHTMLMail($xaction);
if ($header_html !== null) { if ($header_html !== null) {
$headers_html[] = $header_html; $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 // If this is not the first comment in the mail, add the header showing
// who wrote the comment immediately above the comment. // who wrote the comment immediately above the comment.
if (!$is_initial) { if (!$is_initial) {
$header = $xaction->getTitleForTextMail(); $header = $this->getTitleForTextMail($xaction);
if ($header !== null) { if ($header !== null) {
$body->addRawPlaintextSection($header); $body->addRawPlaintextSection($header);
} }
$header_html = $xaction->getTitleForHTMLMail(); $header_html = $this->getTitleForHTMLMail($xaction);
if ($header_html !== null) { if ($header_html !== null) {
$body->addRawHTMLSection($header_html); $body->addRawHTMLSection($header_html);
} }
@ -4848,6 +4856,13 @@ abstract class PhabricatorApplicationTransactionEditor
} }
private function requireMFA(PhabricatorLiskDAO $object, array $xactions) { 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); $editor_class = get_class($this);
$object_phid = $object->getPHID(); $object_phid = $object->getPHID();
@ -4862,8 +4877,6 @@ abstract class PhabricatorApplicationTransactionEditor
$editor_class); $editor_class);
} }
$actor = $this->getActor();
$request = $this->getRequest(); $request = $this->getRequest();
if ($request === null) { if ($request === null) {
$source_type = $this->getContentSource()->getSourceTypeConstant(); $source_type = $this->getContentSource()->getSourceTypeConstant();
@ -4975,4 +4988,112 @@ abstract class PhabricatorApplicationTransactionEditor
return $xactions; 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()) { switch ($this->getTransactionType()) {
case PhabricatorTransactions::TYPE_TOKEN: case PhabricatorTransactions::TYPE_TOKEN:
case PhabricatorTransactions::TYPE_MFA:
return true; return true;
case PhabricatorTransactions::TYPE_EDGE: case PhabricatorTransactions::TYPE_EDGE:
$edge_type = $this->getMetadataValue('edge:type'); $edge_type = $this->getMetadataValue('edge:type');
switch ($edge_type) { switch ($edge_type) {
case PhabricatorObjectMentionsObjectEdgeType::EDGECONST: case PhabricatorObjectMentionsObjectEdgeType::EDGECONST:

View file

@ -431,4 +431,68 @@ abstract class PhabricatorModularTransactionType
return false; 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 <?php
/** final class PhabricatorApplicationTransactionCommentView
* @concrete-extensible extends AphrontView {
*/
class PhabricatorApplicationTransactionCommentView extends AphrontView {
private $submitButtonName; private $submitButtonName;
private $action; private $action;
@ -24,6 +22,7 @@ class PhabricatorApplicationTransactionCommentView extends AphrontView {
private $infoView; private $infoView;
private $editEngineLock; private $editEngineLock;
private $noBorder; private $noBorder;
private $requiresMFA;
private $currentVersion; private $currentVersion;
private $versionedDraft; private $versionedDraft;
@ -160,6 +159,15 @@ class PhabricatorApplicationTransactionCommentView extends AphrontView {
return $this->editEngineLock; return $this->editEngineLock;
} }
public function setRequiresMFA($requires_mfa) {
$this->requiresMFA = $requires_mfa;
return $this;
}
public function getRequiresMFA() {
return $this->requiresMFA;
}
public function setTransactionTimeline( public function setTransactionTimeline(
PhabricatorApplicationTransactionView $timeline) { PhabricatorApplicationTransactionView $timeline) {
@ -187,8 +195,8 @@ class PhabricatorApplicationTransactionCommentView extends AphrontView {
)); ));
} }
$user = $this->getUser(); $viewer = $this->getViewer();
if (!$user->isLoggedIn()) { if (!$viewer->isLoggedIn()) {
$uri = id(new PhutilURI('/login/')) $uri = id(new PhutilURI('/login/'))
->setQueryParam('next', (string)$this->getRequestURI()); ->setQueryParam('next', (string)$this->getRequestURI());
return id(new PHUIObjectBoxView()) return id(new PHUIObjectBoxView())
@ -203,6 +211,25 @@ class PhabricatorApplicationTransactionCommentView extends AphrontView {
pht('Log In to Comment'))); 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(); $data = array();
$comment = $this->renderCommentPanel(); $comment = $this->renderCommentPanel();
@ -226,7 +253,7 @@ class PhabricatorApplicationTransactionCommentView extends AphrontView {
} }
require_celerity_resource('phui-comment-form-css'); require_celerity_resource('phui-comment-form-css');
$image_uri = $user->getProfileImageURI(); $image_uri = $viewer->getProfileImageURI();
$image = phutil_tag( $image = phutil_tag(
'div', 'div',
array( array(
@ -388,6 +415,17 @@ class PhabricatorApplicationTransactionCommentView extends AphrontView {
$form->appendChild($info_view); $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->appendChild($invisi_bar);
$form->addClass('phui-comment-has-actions'); $form->addClass('phui-comment-has-actions');