diff --git a/resources/sql/autopatches/20190127.project.01.subtype.sql b/resources/sql/autopatches/20190127.project.01.subtype.sql new file mode 100644 index 0000000000..107f9202d4 --- /dev/null +++ b/resources/sql/autopatches/20190127.project.01.subtype.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_project.project + ADD subtype VARCHAR(64) COLLATE {$COLLATE_TEXT} NOT NULL; diff --git a/resources/sql/autopatches/20190127.project.02.default.sql b/resources/sql/autopatches/20190127.project.02.default.sql new file mode 100644 index 0000000000..1a74506cf7 --- /dev/null +++ b/resources/sql/autopatches/20190127.project.02.default.sql @@ -0,0 +1,2 @@ +UPDATE {$NAMESPACE}_project.project + SET subtype = 'default' WHERE subtype = ''; diff --git a/resources/sql/autopatches/20190129.project.01.spaces.php b/resources/sql/autopatches/20190129.project.01.spaces.php new file mode 100644 index 0000000000..845b4ff25d --- /dev/null +++ b/resources/sql/autopatches/20190129.project.01.spaces.php @@ -0,0 +1,18 @@ +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']); +} diff --git a/scripts/mail/mail_handler.php b/scripts/mail/mail_handler.php index 1c3c71f305..bf6f315f3a 100755 --- a/scripts/mail/mail_handler.php +++ b/scripts/mail/mail_handler.php @@ -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); diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index c5723894d5..324fc3c5de 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -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', diff --git a/src/aphront/configuration/AphrontApplicationConfiguration.php b/src/aphront/configuration/AphrontApplicationConfiguration.php index 60b12557c9..8d36bbc880 100644 --- a/src/aphront/configuration/AphrontApplicationConfiguration.php +++ b/src/aphront/configuration/AphrontApplicationConfiguration.php @@ -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(); + } + } + } + } diff --git a/src/aphront/configuration/AphrontDefaultApplicationConfiguration.php b/src/aphront/configuration/AphrontDefaultApplicationConfiguration.php deleted file mode 100644 index fb67919576..0000000000 --- a/src/aphront/configuration/AphrontDefaultApplicationConfiguration.php +++ /dev/null @@ -1,121 +0,0 @@ -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, - ), - ); - } - -} diff --git a/src/aphront/handler/PhabricatorHighSecurityRequestExceptionHandler.php b/src/aphront/handler/PhabricatorHighSecurityRequestExceptionHandler.php index 2a737ecf5c..7f4eddad45 100644 --- a/src/aphront/handler/PhabricatorHighSecurityRequestExceptionHandler.php +++ b/src/aphront/handler/PhabricatorHighSecurityRequestExceptionHandler.php @@ -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); } diff --git a/src/applications/auth/application/PhabricatorAuthApplication.php b/src/applications/auth/application/PhabricatorAuthApplication.php index a9ab3be181..df86595b46 100644 --- a/src/applications/auth/application/PhabricatorAuthApplication.php +++ b/src/applications/auth/application/PhabricatorAuthApplication.php @@ -95,6 +95,8 @@ final class PhabricatorAuthApplication extends PhabricatorApplication { 'PhabricatorAuthFactorProviderEditController', '(?P[1-9]\d*)/' => 'PhabricatorAuthFactorProviderViewController', + 'message/(?P[1-9]\d*)/' => + 'PhabricatorAuthFactorProviderMessageController', ), 'message/' => array( diff --git a/src/applications/auth/controller/mfa/PhabricatorAuthFactorProviderMessageController.php b/src/applications/auth/controller/mfa/PhabricatorAuthFactorProviderMessageController.php new file mode 100644 index 0000000000..563ee39931 --- /dev/null +++ b/src/applications/auth/controller/mfa/PhabricatorAuthFactorProviderMessageController.php @@ -0,0 +1,84 @@ +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')); + } + +} diff --git a/src/applications/auth/controller/mfa/PhabricatorAuthFactorProviderViewController.php b/src/applications/auth/controller/mfa/PhabricatorAuthFactorProviderViewController.php index 3047c8714d..1dac49bcf9 100644 --- a/src/applications/auth/controller/mfa/PhabricatorAuthFactorProviderViewController.php +++ b/src/applications/auth/controller/mfa/PhabricatorAuthFactorProviderViewController.php @@ -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; } diff --git a/src/applications/auth/factor/PhabricatorAuthFactor.php b/src/applications/auth/factor/PhabricatorAuthFactor.php index 345ace3df9..ec49f7f748 100644 --- a/src/applications/auth/factor/PhabricatorAuthFactor.php +++ b/src/applications/auth/factor/PhabricatorAuthFactor.php @@ -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); + } + } diff --git a/src/applications/auth/factor/PhabricatorDuoAuthFactor.php b/src/applications/auth/factor/PhabricatorDuoAuthFactor.php index f5e2455c79..187e011953 100644 --- a/src/applications/auth/factor/PhabricatorDuoAuthFactor.php +++ b/src/applications/auth/factor/PhabricatorDuoAuthFactor.php @@ -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, diff --git a/src/applications/auth/future/PhabricatorDuoFuture.php b/src/applications/auth/future/PhabricatorDuoFuture.php index fd95906da1..81a5a2a2b8 100644 --- a/src/applications/auth/future/PhabricatorDuoFuture.php +++ b/src/applications/auth/future/PhabricatorDuoFuture.php @@ -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, diff --git a/src/applications/auth/storage/PhabricatorAuthFactorProvider.php b/src/applications/auth/storage/PhabricatorAuthFactorProvider.php index 79acd4f23e..2213535dff 100644 --- a/src/applications/auth/storage/PhabricatorAuthFactorProvider.php +++ b/src/applications/auth/storage/PhabricatorAuthFactorProvider.php @@ -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; diff --git a/src/applications/auth/xaction/PhabricatorAuthFactorProviderEnrollMessageTransaction.php b/src/applications/auth/xaction/PhabricatorAuthFactorProviderEnrollMessageTransaction.php new file mode 100644 index 0000000000..d6d26143c1 --- /dev/null +++ b/src/applications/auth/xaction/PhabricatorAuthFactorProviderEnrollMessageTransaction.php @@ -0,0 +1,39 @@ +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()); + } + +} diff --git a/src/applications/base/controller/__tests__/PhabricatorAccessControlTestCase.php b/src/applications/base/controller/__tests__/PhabricatorAccessControlTestCase.php index 98fa948722..7d763d6e64 100644 --- a/src/applications/base/controller/__tests__/PhabricatorAccessControlTestCase.php +++ b/src/applications/base/controller/__tests__/PhabricatorAccessControlTestCase.php @@ -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'; diff --git a/src/applications/config/check/PhabricatorExtensionsSetupCheck.php b/src/applications/config/check/PhabricatorExtensionsSetupCheck.php index 973c80629b..51105de1c5 100644 --- a/src/applications/config/check/PhabricatorExtensionsSetupCheck.php +++ b/src/applications/config/check/PhabricatorExtensionsSetupCheck.php @@ -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 diff --git a/src/applications/config/check/PhabricatorExtraConfigSetupCheck.php b/src/applications/config/check/PhabricatorExtraConfigSetupCheck.php index d5f44c87c8..1c8a593a78 100644 --- a/src/applications/config/check/PhabricatorExtraConfigSetupCheck.php +++ b/src/applications/config/check/PhabricatorExtraConfigSetupCheck.php @@ -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; diff --git a/src/applications/config/controller/PhabricatorConfigVersionController.php b/src/applications/config/controller/PhabricatorConfigVersionController.php index 8a87dec5cc..a9571a1f85 100644 --- a/src/applications/config/controller/PhabricatorConfigVersionController.php +++ b/src/applications/config/controller/PhabricatorConfigVersionController.php @@ -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()) { diff --git a/src/applications/config/option/PhabricatorExtendingPhabricatorConfigOptions.php b/src/applications/config/option/PhabricatorExtendingPhabricatorConfigOptions.php index 28a0f619dd..0a3ea32e44 100644 --- a/src/applications/config/option/PhabricatorExtendingPhabricatorConfigOptions.php +++ b/src/applications/config/option/PhabricatorExtendingPhabricatorConfigOptions.php @@ -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.')), ); } diff --git a/src/applications/differential/editor/DifferentialRevisionEditEngine.php b/src/applications/differential/editor/DifferentialRevisionEditEngine.php index 07b693044f..9c399036d5 100644 --- a/src/applications/differential/editor/DifferentialRevisionEditEngine.php +++ b/src/applications/differential/editor/DifferentialRevisionEditEngine.php @@ -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'); diff --git a/src/applications/differential/engine/DifferentialDiffExtractionEngine.php b/src/applications/differential/engine/DifferentialDiffExtractionEngine.php index d7d7c767e1..861d2ad220 100644 --- a/src/applications/differential/engine/DifferentialDiffExtractionEngine.php +++ b/src/applications/differential/engine/DifferentialDiffExtractionEngine.php @@ -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) { diff --git a/src/applications/diffusion/controller/DiffusionServeController.php b/src/applications/diffusion/controller/DiffusionServeController.php index 5a5c446a29..cb4ad0ba95 100644 --- a/src/applications/diffusion/controller/DiffusionServeController.php +++ b/src/applications/diffusion/controller/DiffusionServeController.php @@ -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. diff --git a/src/applications/diffusion/protocol/DiffusionRepositoryClusterEngine.php b/src/applications/diffusion/protocol/DiffusionRepositoryClusterEngine.php index c72021f0c1..717e730ab1 100644 --- a/src/applications/diffusion/protocol/DiffusionRepositoryClusterEngine.php +++ b/src/applications/diffusion/protocol/DiffusionRepositoryClusterEngine.php @@ -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, diff --git a/src/applications/diffusion/ssh/DiffusionSSHWorkflow.php b/src/applications/diffusion/ssh/DiffusionSSHWorkflow.php index 02cacbfe94..b2d1d25f44 100644 --- a/src/applications/diffusion/ssh/DiffusionSSHWorkflow.php +++ b/src/applications/diffusion/ssh/DiffusionSSHWorkflow.php @@ -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.', diff --git a/src/applications/project/config/PhabricatorProjectConfigOptions.php b/src/applications/project/config/PhabricatorProjectConfigOptions.php index c61faa64fb..2d87bf159f 100644 --- a/src/applications/project/config/PhabricatorProjectConfigOptions.php +++ b/src/applications/project/config/PhabricatorProjectConfigOptions.php @@ -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(<<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')), + ); } diff --git a/src/applications/project/config/PhabricatorProjectSubtypesConfigType.php b/src/applications/project/config/PhabricatorProjectSubtypesConfigType.php new file mode 100644 index 0000000000..7603ad7683 --- /dev/null +++ b/src/applications/project/config/PhabricatorProjectSubtypesConfigType.php @@ -0,0 +1,14 @@ +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); diff --git a/src/applications/project/editor/PhabricatorProjectTransactionEditor.php b/src/applications/project/editor/PhabricatorProjectTransactionEditor.php index ee2c087085..b714f66830 100644 --- a/src/applications/project/editor/PhabricatorProjectTransactionEditor.php +++ b/src/applications/project/editor/PhabricatorProjectTransactionEditor.php @@ -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); } diff --git a/src/applications/project/query/PhabricatorProjectQuery.php b/src/applications/project/query/PhabricatorProjectQuery.php index 6f81bc5e68..f6087f7d2a 100644 --- a/src/applications/project/query/PhabricatorProjectQuery.php +++ b/src/applications/project/query/PhabricatorProjectQuery.php @@ -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; } diff --git a/src/applications/project/query/PhabricatorProjectSearchEngine.php b/src/applications/project/query/PhabricatorProjectSearchEngine.php index 02e795395a..88a35676cc 100644 --- a/src/applications/project/query/PhabricatorProjectSearchEngine.php +++ b/src/applications/project/query/PhabricatorProjectSearchEngine.php @@ -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; } diff --git a/src/applications/project/storage/PhabricatorProject.php b/src/applications/project/storage/PhabricatorProject.php index 4dae13f3c6..5182a941bf 100644 --- a/src/applications/project/storage/PhabricatorProject.php +++ b/src/applications/project/storage/PhabricatorProject.php @@ -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); + } + } diff --git a/src/applications/project/typeahead/PhabricatorProjectSubtypeDatasource.php b/src/applications/project/typeahead/PhabricatorProjectSubtypeDatasource.php new file mode 100644 index 0000000000..68de11e630 --- /dev/null +++ b/src/applications/project/typeahead/PhabricatorProjectSubtypeDatasource.php @@ -0,0 +1,45 @@ +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; + } + +} diff --git a/src/applications/project/view/PhabricatorProjectListView.php b/src/applications/project/view/PhabricatorProjectListView.php index 645440d831..d8fb011c2e 100644 --- a/src/applications/project/view/PhabricatorProjectListView.php +++ b/src/applications/project/view/PhabricatorProjectListView.php @@ -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); } diff --git a/src/applications/repository/daemon/PhabricatorRepositoryPullLocalDaemon.php b/src/applications/repository/daemon/PhabricatorRepositoryPullLocalDaemon.php index 332d67f7af..125ee833f8 100644 --- a/src/applications/repository/daemon/PhabricatorRepositoryPullLocalDaemon.php +++ b/src/applications/repository/daemon/PhabricatorRepositoryPullLocalDaemon.php @@ -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(); + } + } diff --git a/src/applications/repository/management/PhabricatorRepositoryManagementWorkflow.php b/src/applications/repository/management/PhabricatorRepositoryManagementWorkflow.php index 654a974e6c..6d48d7c5d1 100644 --- a/src/applications/repository/management/PhabricatorRepositoryManagementWorkflow.php +++ b/src/applications/repository/management/PhabricatorRepositoryManagementWorkflow.php @@ -7,7 +7,7 @@ abstract class PhabricatorRepositoryManagementWorkflow $identifiers = $args->getArg($param); if (!$identifiers) { - return null; + return array(); } $query = id(new PhabricatorRepositoryQuery()) diff --git a/src/applications/repository/storage/PhabricatorRepository.php b/src/applications/repository/storage/PhabricatorRepository.php index ed90f47f13..e66fb78afa 100644 --- a/src/applications/repository/storage/PhabricatorRepository.php +++ b/src/applications/repository/storage/PhabricatorRepository.php @@ -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(); diff --git a/src/applications/settings/panel/PhabricatorMultiFactorSettingsPanel.php b/src/applications/settings/panel/PhabricatorMultiFactorSettingsPanel.php index 4da09dd324..6809b51334 100644 --- a/src/applications/settings/panel/PhabricatorMultiFactorSettingsPanel.php +++ b/src/applications/settings/panel/PhabricatorMultiFactorSettingsPanel.php @@ -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)); } diff --git a/src/applications/transactions/conduit/TransactionSearchConduitAPIMethod.php b/src/applications/transactions/conduit/TransactionSearchConduitAPIMethod.php index 0394432ff3..0edc0b3f5a 100644 --- a/src/applications/transactions/conduit/TransactionSearchConduitAPIMethod.php +++ b/src/applications/transactions/conduit/TransactionSearchConduitAPIMethod.php @@ -68,6 +68,7 @@ final class TransactionSearchConduitAPIMethod $object); $xaction_query + ->needHandles(false) ->withObjectPHIDs(array($object->getPHID())) ->setViewer($viewer); diff --git a/src/applications/transactions/editengine/PhabricatorEditEngine.php b/src/applications/transactions/editengine/PhabricatorEditEngine.php index b3106d27b2..feb783e724 100644 --- a/src/applications/transactions/editengine/PhabricatorEditEngine.php +++ b/src/applications/transactions/editengine/PhabricatorEditEngine.php @@ -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( diff --git a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php index c6458b0631..91825eb73d 100644 --- a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php +++ b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php @@ -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; + } + + } diff --git a/src/applications/transactions/engineextension/PhabricatorEditorExtension.php b/src/applications/transactions/engineextension/PhabricatorEditorExtension.php new file mode 100644 index 0000000000..6ffac522c2 --- /dev/null +++ b/src/applications/transactions/engineextension/PhabricatorEditorExtension.php @@ -0,0 +1,83 @@ +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); + } + + +} diff --git a/src/applications/transactions/engineextension/PhabricatorEditorExtensionModule.php b/src/applications/transactions/engineextension/PhabricatorEditorExtensionModule.php new file mode 100644 index 0000000000..e34a0bb3a7 --- /dev/null +++ b/src/applications/transactions/engineextension/PhabricatorEditorExtensionModule.php @@ -0,0 +1,40 @@ +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', + )); + } + +} diff --git a/src/applications/transactions/storage/PhabricatorApplicationTransaction.php b/src/applications/transactions/storage/PhabricatorApplicationTransaction.php index efbbdb4a09..6d047fc823 100644 --- a/src/applications/transactions/storage/PhabricatorApplicationTransaction.php +++ b/src/applications/transactions/storage/PhabricatorApplicationTransaction.php @@ -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: diff --git a/src/applications/transactions/storage/PhabricatorModularTransactionType.php b/src/applications/transactions/storage/PhabricatorModularTransactionType.php index abe7a31025..2d0cb8e7c1 100644 --- a/src/applications/transactions/storage/PhabricatorModularTransactionType.php +++ b/src/applications/transactions/storage/PhabricatorModularTransactionType.php @@ -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; + } + } diff --git a/src/applications/transactions/view/PhabricatorApplicationTransactionCommentView.php b/src/applications/transactions/view/PhabricatorApplicationTransactionCommentView.php index d830309119..f6a27d4bcd 100644 --- a/src/applications/transactions/view/PhabricatorApplicationTransactionCommentView.php +++ b/src/applications/transactions/view/PhabricatorApplicationTransactionCommentView.php @@ -1,9 +1,7 @@ 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');