mirror of
https://we.phorge.it/source/phorge.git
synced 2025-01-08 05:41:01 +01:00
(stable) Promote 2019 Week 5
This commit is contained in:
commit
c65ad751ab
47 changed files with 1082 additions and 220 deletions
|
@ -0,0 +1,2 @@
|
|||
ALTER TABLE {$NAMESPACE}_project.project
|
||||
ADD subtype VARCHAR(64) COLLATE {$COLLATE_TEXT} NOT NULL;
|
|
@ -0,0 +1,2 @@
|
|||
UPDATE {$NAMESPACE}_project.project
|
||||
SET subtype = 'default' WHERE subtype = '';
|
18
resources/sql/autopatches/20190129.project.01.spaces.php
Normal file
18
resources/sql/autopatches/20190129.project.01.spaces.php
Normal 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']);
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -95,6 +95,8 @@ final class PhabricatorAuthApplication extends PhabricatorApplication {
|
|||
'PhabricatorAuthFactorProviderEditController',
|
||||
'(?P<id>[1-9]\d*)/' =>
|
||||
'PhabricatorAuthFactorProviderViewController',
|
||||
'message/(?P<id>[1-9]\d*)/' =>
|
||||
'PhabricatorAuthFactorProviderMessageController',
|
||||
),
|
||||
|
||||
'message/' => array(
|
||||
|
|
|
@ -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'));
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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()) {
|
||||
|
|
|
@ -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.')),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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');
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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.',
|
||||
|
|
|
@ -83,6 +83,34 @@ EOTEXT
|
|||
|
||||
$custom_field_type = 'custom:PhabricatorCustomFieldConfigOptionType';
|
||||
|
||||
|
||||
$subtype_type = 'projects.subtypes';
|
||||
$subtype_default_key = PhabricatorEditEngineSubtype::SUBTYPE_DEFAULT;
|
||||
$subtype_example = array(
|
||||
array(
|
||||
'key' => $subtype_default_key,
|
||||
'name' => pht('Project'),
|
||||
),
|
||||
array(
|
||||
'key' => 'team',
|
||||
'name' => pht('Team'),
|
||||
),
|
||||
);
|
||||
$subtype_example = id(new PhutilJSON())->encodeAsList($subtype_example);
|
||||
|
||||
$subtype_default = array(
|
||||
array(
|
||||
'key' => $subtype_default_key,
|
||||
'name' => pht('Project'),
|
||||
),
|
||||
);
|
||||
|
||||
$subtype_description = $this->deformat(pht(<<<EOTEXT
|
||||
Allows you to define project subtypes. For a more detailed description of
|
||||
subtype configuration, see @{config:maniphest.subtypes}.
|
||||
EOTEXT
|
||||
));
|
||||
|
||||
return array(
|
||||
$this->newOption('projects.custom-field-definitions', 'wild', array())
|
||||
->setSummary(pht('Custom Projects fields.'))
|
||||
|
@ -102,6 +130,11 @@ EOTEXT
|
|||
$this->newOption('projects.colors', $colors_type, $default_colors)
|
||||
->setSummary(pht('Adjust project colors.'))
|
||||
->setDescription($colors_description),
|
||||
$this->newOption('projects.subtypes', $subtype_type, $subtype_default)
|
||||
->setSummary(pht('Define project subtypes.'))
|
||||
->setDescription($subtype_description)
|
||||
->addExample($subtype_example, pht('Simple Subtypes')),
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
<?php
|
||||
|
||||
final class PhabricatorProjectSubtypesConfigType
|
||||
extends PhabricatorJSONConfigType {
|
||||
|
||||
const TYPEKEY = 'projects.subtypes';
|
||||
|
||||
public function validateStoredValue(
|
||||
PhabricatorConfigOption $option,
|
||||
$value) {
|
||||
PhabricatorEditEngineSubtype::validateConfiguration($value);
|
||||
}
|
||||
|
||||
}
|
|
@ -51,6 +51,12 @@ final class PhabricatorProjectProfileController
|
|||
$watch_action = $this->renderWatchAction($project);
|
||||
$header->addActionLink($watch_action);
|
||||
|
||||
$subtype = $project->newSubtypeObject();
|
||||
if ($subtype && $subtype->hasTagView()) {
|
||||
$subtype_tag = $subtype->newTagView();
|
||||
$header->addTag($subtype_tag);
|
||||
}
|
||||
|
||||
$milestone_list = $this->buildMilestoneList($project);
|
||||
$subproject_list = $this->buildSubprojectList($project);
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@ abstract class PhabricatorRepositoryManagementWorkflow
|
|||
$identifiers = $args->getArg($param);
|
||||
|
||||
if (!$identifiers) {
|
||||
return null;
|
||||
return array();
|
||||
}
|
||||
|
||||
$query = id(new PhabricatorRepositoryQuery())
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
|
|
@ -68,6 +68,7 @@ final class TransactionSearchConduitAPIMethod
|
|||
$object);
|
||||
|
||||
$xaction_query
|
||||
->needHandles(false)
|
||||
->withObjectPHIDs(array($object->getPHID()))
|
||||
->setViewer($viewer);
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -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',
|
||||
));
|
||||
}
|
||||
|
||||
}
|
|
@ -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:
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @concrete-extensible
|
||||
*/
|
||||
class PhabricatorApplicationTransactionCommentView extends AphrontView {
|
||||
final class PhabricatorApplicationTransactionCommentView
|
||||
extends AphrontView {
|
||||
|
||||
private $submitButtonName;
|
||||
private $action;
|
||||
|
@ -24,6 +22,7 @@ class PhabricatorApplicationTransactionCommentView extends AphrontView {
|
|||
private $infoView;
|
||||
private $editEngineLock;
|
||||
private $noBorder;
|
||||
private $requiresMFA;
|
||||
|
||||
private $currentVersion;
|
||||
private $versionedDraft;
|
||||
|
@ -160,6 +159,15 @@ class PhabricatorApplicationTransactionCommentView extends AphrontView {
|
|||
return $this->editEngineLock;
|
||||
}
|
||||
|
||||
public function setRequiresMFA($requires_mfa) {
|
||||
$this->requiresMFA = $requires_mfa;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getRequiresMFA() {
|
||||
return $this->requiresMFA;
|
||||
}
|
||||
|
||||
public function setTransactionTimeline(
|
||||
PhabricatorApplicationTransactionView $timeline) {
|
||||
|
||||
|
@ -187,8 +195,8 @@ class PhabricatorApplicationTransactionCommentView extends AphrontView {
|
|||
));
|
||||
}
|
||||
|
||||
$user = $this->getUser();
|
||||
if (!$user->isLoggedIn()) {
|
||||
$viewer = $this->getViewer();
|
||||
if (!$viewer->isLoggedIn()) {
|
||||
$uri = id(new PhutilURI('/login/'))
|
||||
->setQueryParam('next', (string)$this->getRequestURI());
|
||||
return id(new PHUIObjectBoxView())
|
||||
|
@ -203,6 +211,25 @@ class PhabricatorApplicationTransactionCommentView extends AphrontView {
|
|||
pht('Log In to Comment')));
|
||||
}
|
||||
|
||||
if ($this->getRequiresMFA()) {
|
||||
if (!$viewer->getIsEnrolledInMultiFactor()) {
|
||||
$viewer->updateMultiFactorEnrollment();
|
||||
if (!$viewer->getIsEnrolledInMultiFactor()) {
|
||||
$messages = array();
|
||||
$messages[] = pht(
|
||||
'You must provide multi-factor credentials to comment or make '.
|
||||
'changes, but you do not have multi-factor authentication '.
|
||||
'configured on your account.');
|
||||
$messages[] = pht(
|
||||
'To continue, configure multi-factor authentication in Settings.');
|
||||
|
||||
return id(new PHUIInfoView())
|
||||
->setSeverity(PHUIInfoView::SEVERITY_MFA)
|
||||
->setErrors($messages);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$data = array();
|
||||
|
||||
$comment = $this->renderCommentPanel();
|
||||
|
@ -226,7 +253,7 @@ class PhabricatorApplicationTransactionCommentView extends AphrontView {
|
|||
}
|
||||
|
||||
require_celerity_resource('phui-comment-form-css');
|
||||
$image_uri = $user->getProfileImageURI();
|
||||
$image_uri = $viewer->getProfileImageURI();
|
||||
$image = phutil_tag(
|
||||
'div',
|
||||
array(
|
||||
|
@ -388,6 +415,17 @@ class PhabricatorApplicationTransactionCommentView extends AphrontView {
|
|||
$form->appendChild($info_view);
|
||||
}
|
||||
|
||||
if ($this->getRequiresMFA()) {
|
||||
$message = pht(
|
||||
'You will be required to provide multi-factor credentials to '.
|
||||
'comment or make changes.');
|
||||
|
||||
$form->appendChild(
|
||||
id(new PHUIInfoView())
|
||||
->setSeverity(PHUIInfoView::SEVERITY_MFA)
|
||||
->setErrors(array($message)));
|
||||
}
|
||||
|
||||
$form->appendChild($invisi_bar);
|
||||
$form->addClass('phui-comment-has-actions');
|
||||
|
||||
|
|
Loading…
Reference in a new issue