1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2025-01-09 14:21:02 +01:00

(stable) Promote 2018 Week 51

This commit is contained in:
epriestley 2018-12-28 12:44:23 -08:00
commit a3acd3450d
191 changed files with 3107 additions and 1924 deletions

View file

@ -9,8 +9,8 @@ return array(
'names' => array(
'conpherence.pkg.css' => 'e68cf1fa',
'conpherence.pkg.js' => '15191c65',
'core.pkg.css' => '9d1148a4',
'core.pkg.js' => '4bde473b',
'core.pkg.css' => '47535fd5',
'core.pkg.js' => 'bd89cb1d',
'differential.pkg.css' => '06dc617c',
'differential.pkg.js' => 'ef0b989b',
'diffusion.pkg.css' => 'a2d17c7d',
@ -151,7 +151,7 @@ return array(
'rsrc/css/phui/phui-document.css' => 'c4ac41f9',
'rsrc/css/phui/phui-feed-story.css' => '44a9c8e9',
'rsrc/css/phui/phui-fontkit.css' => '1320ed01',
'rsrc/css/phui/phui-form-view.css' => '2f43fae7',
'rsrc/css/phui/phui-form-view.css' => 'b04e08d9',
'rsrc/css/phui/phui-form.css' => '7aaa04e3',
'rsrc/css/phui/phui-head-thing.css' => 'fd311e5f',
'rsrc/css/phui/phui-header-view.css' => '1ba8b707',
@ -425,7 +425,7 @@ return array(
'rsrc/js/application/transactions/behavior-comment-actions.js' => '59e27e74',
'rsrc/js/application/transactions/behavior-reorder-configs.js' => 'd7a74243',
'rsrc/js/application/transactions/behavior-reorder-fields.js' => 'b59e1e96',
'rsrc/js/application/transactions/behavior-show-older-transactions.js' => '8f29b364',
'rsrc/js/application/transactions/behavior-show-older-transactions.js' => '0e1eca96',
'rsrc/js/application/transactions/behavior-transaction-comment-form.js' => 'b23b49e6',
'rsrc/js/application/transactions/behavior-transaction-list.js' => '1f6794f6',
'rsrc/js/application/typeahead/behavior-typeahead-browse.js' => '635de1ec',
@ -639,7 +639,7 @@ return array(
'javelin-behavior-phabricator-remarkup-assist' => 'acd29eee',
'javelin-behavior-phabricator-reveal-content' => '60821bc7',
'javelin-behavior-phabricator-search-typeahead' => 'c3e917d9',
'javelin-behavior-phabricator-show-older-transactions' => '8f29b364',
'javelin-behavior-phabricator-show-older-transactions' => '0e1eca96',
'javelin-behavior-phabricator-tooltips' => 'c420b0b9',
'javelin-behavior-phabricator-transaction-comment-form' => 'b23b49e6',
'javelin-behavior-phabricator-transaction-list' => '1f6794f6',
@ -819,7 +819,7 @@ return array(
'phui-font-icon-base-css' => '870a7360',
'phui-fontkit-css' => '1320ed01',
'phui-form-css' => '7aaa04e3',
'phui-form-view-css' => '2f43fae7',
'phui-form-view-css' => 'b04e08d9',
'phui-head-thing-view-css' => 'fd311e5f',
'phui-header-view-css' => '1ba8b707',
'phui-hovercard' => '1bd28176',
@ -950,6 +950,12 @@ return array(
'javelin-install',
'phuix-button-view',
),
'0e1eca96' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-dom',
'phabricator-busy',
),
'0f764c35' => array(
'javelin-install',
'javelin-util',
@ -1581,12 +1587,6 @@ return array(
'8e1baf68' => array(
'phui-button-css',
),
'8f29b364' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-dom',
'phabricator-busy',
),
'8ff5e24c' => array(
'javelin-behavior',
'javelin-stratcom',

View file

@ -0,0 +1,12 @@
CREATE TABLE {$NAMESPACE}_auth.auth_challenge (
id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
phid VARBINARY(64) NOT NULL,
userPHID VARBINARY(64) NOT NULL,
factorPHID VARBINARY(64) NOT NULL,
sessionPHID VARBINARY(64) NOT NULL,
challengeKey VARCHAR(255) NOT NULL COLLATE {$COLLATE_TEXT},
challengeTTL INT UNSIGNED NOT NULL,
properties LONGTEXT NOT NULL COLLATE {$COLLATE_TEXT},
dateCreated INT UNSIGNED NOT NULL,
dateModified INT UNSIGNED NOT NULL
) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT};

View file

@ -0,0 +1,2 @@
ALTER TABLE {$NAMESPACE}_auth.auth_challenge
ADD workflowKey VARCHAR(255) NOT NULL COLLATE {$COLLATE_TEXT};

View file

@ -0,0 +1,2 @@
ALTER TABLE {$NAMESPACE}_auth.auth_challenge
ADD responseDigest VARCHAR(255) COLLATE {$COLLATE_TEXT};

View file

@ -0,0 +1,2 @@
ALTER TABLE {$NAMESPACE}_auth.auth_challenge
ADD responseTTL INT UNSIGNED;

View file

@ -0,0 +1,2 @@
ALTER TABLE {$NAMESPACE}_auth.auth_challenge
ADD isCompleted BOOL NOT NULL;

View file

@ -0,0 +1,2 @@
ALTER TABLE {$NAMESPACE}_pholio.pholio_image
ADD authorPHID VARBINARY(64) NOT NULL;

View file

@ -0,0 +1,2 @@
ALTER TABLE {$NAMESPACE}_pholio.pholio_image
ADD mockPHID VARBINARY(64);

View file

@ -0,0 +1,35 @@
<?php
// Old images used a "mockID" instead of a "mockPHID" to reference mocks.
// Set the "mockPHID" column to the value that corresponds to the "mockID".
$image = new PholioImage();
$mock = new PholioMock();
$conn = $image->establishConnection('w');
$iterator = new LiskRawMigrationIterator($conn, $image->getTableName());
foreach ($iterator as $image_row) {
if ($image_row['mockPHID']) {
continue;
}
$mock_id = $image_row['mockID'];
$mock_row = queryfx_one(
$conn,
'SELECT phid FROM %R WHERE id = %d',
$mock,
$mock_id);
if (!$mock_row) {
continue;
}
queryfx(
$conn,
'UPDATE %R SET mockPHID = %s WHERE id = %d',
$image,
$mock_row['phid'],
$image_row['id']);
}

View file

@ -0,0 +1,2 @@
ALTER TABLE {$NAMESPACE}_pholio.pholio_image
DROP mockID;

View file

@ -0,0 +1,28 @@
<?php
$mock_table = new PholioMock();
$mock_conn = $mock_table->establishConnection('w');
$properties_table = new PhabricatorMetaMTAMailProperties();
$conn = $properties_table->establishConnection('w');
$iterator = new LiskRawMigrationIterator(
$mock_conn,
$mock_table->getTableName());
foreach ($iterator as $row) {
queryfx(
$conn,
'INSERT IGNORE INTO %T
(objectPHID, mailProperties, dateCreated, dateModified)
VALUES
(%s, %s, %d, %d)',
$properties_table->getTableName(),
$row['phid'],
phutil_json_encode(
array(
'mailKey' => $row['mailKey'],
)),
PhabricatorTime::getNow(),
PhabricatorTime::getNow());
}

View file

@ -0,0 +1,2 @@
ALTER TABLE {$NAMESPACE}_pholio.pholio_mock
DROP mailKey;

View file

@ -200,9 +200,28 @@ $user->openTransaction();
$editor->updateUser($user, $verify_email);
}
$editor->makeAdminUser($user, $set_admin);
$editor->makeSystemAgentUser($user, $set_system_agent);
$xactions = array();
$xactions[] = id(new PhabricatorUserTransaction())
->setTransactionType(
PhabricatorUserEmpowerTransaction::TRANSACTIONTYPE)
->setNewValue($set_admin);
$actor = PhabricatorUser::getOmnipotentUser();
$content_source = PhabricatorContentSource::newForSource(
PhabricatorConsoleContentSource::SOURCECONST);
$people_application_phid = id(new PhabricatorPeopleApplication())->getPHID();
$transaction_editor = id(new PhabricatorUserTransactionEditor())
->setActor($actor)
->setActingAsPHID($people_application_phid)
->setContentSource($content_source)
->setContinueOnMissingFields(true);
$transaction_editor->applyTransactions($user, $xactions);
$user->saveTransaction();
echo pht('Saved changes.')."\n";

View file

@ -647,6 +647,7 @@ phutil_register_library_map(array(
'DifferentialRevisionSummaryTransaction' => 'applications/differential/xaction/DifferentialRevisionSummaryTransaction.php',
'DifferentialRevisionTestPlanHeraldField' => 'applications/differential/herald/DifferentialRevisionTestPlanHeraldField.php',
'DifferentialRevisionTestPlanTransaction' => 'applications/differential/xaction/DifferentialRevisionTestPlanTransaction.php',
'DifferentialRevisionTimelineEngine' => 'applications/differential/engine/DifferentialRevisionTimelineEngine.php',
'DifferentialRevisionTitleHeraldField' => 'applications/differential/herald/DifferentialRevisionTitleHeraldField.php',
'DifferentialRevisionTitleTransaction' => 'applications/differential/xaction/DifferentialRevisionTitleTransaction.php',
'DifferentialRevisionTransactionType' => 'applications/differential/xaction/DifferentialRevisionTransactionType.php',
@ -769,6 +770,7 @@ phutil_register_library_map(array(
'DiffusionCommitSearchConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionCommitSearchConduitAPIMethod.php',
'DiffusionCommitStateTransaction' => 'applications/diffusion/xaction/DiffusionCommitStateTransaction.php',
'DiffusionCommitTagsController' => 'applications/diffusion/controller/DiffusionCommitTagsController.php',
'DiffusionCommitTimelineEngine' => 'applications/diffusion/engine/DiffusionCommitTimelineEngine.php',
'DiffusionCommitTransactionType' => 'applications/diffusion/xaction/DiffusionCommitTransactionType.php',
'DiffusionCommitVerifyTransaction' => 'applications/diffusion/xaction/DiffusionCommitVerifyTransaction.php',
'DiffusionCompareController' => 'applications/diffusion/controller/DiffusionCompareController.php',
@ -1630,7 +1632,6 @@ phutil_register_library_map(array(
'LegalpadTransaction' => 'applications/legalpad/storage/LegalpadTransaction.php',
'LegalpadTransactionComment' => 'applications/legalpad/storage/LegalpadTransactionComment.php',
'LegalpadTransactionQuery' => 'applications/legalpad/query/LegalpadTransactionQuery.php',
'LegalpadTransactionView' => 'applications/legalpad/view/LegalpadTransactionView.php',
'LiskChunkTestCase' => 'infrastructure/storage/lisk/__tests__/LiskChunkTestCase.php',
'LiskDAO' => 'infrastructure/storage/lisk/LiskDAO.php',
'LiskDAOTestCase' => 'infrastructure/storage/lisk/__tests__/LiskDAOTestCase.php',
@ -1730,6 +1731,7 @@ phutil_register_library_map(array(
'ManiphestTaskListController' => 'applications/maniphest/controller/ManiphestTaskListController.php',
'ManiphestTaskListHTTPParameterType' => 'applications/maniphest/httpparametertype/ManiphestTaskListHTTPParameterType.php',
'ManiphestTaskListView' => 'applications/maniphest/view/ManiphestTaskListView.php',
'ManiphestTaskMFAEngine' => 'applications/maniphest/engine/ManiphestTaskMFAEngine.php',
'ManiphestTaskMailReceiver' => 'applications/maniphest/mail/ManiphestTaskMailReceiver.php',
'ManiphestTaskMergeInRelationship' => 'applications/maniphest/relationship/ManiphestTaskMergeInRelationship.php',
'ManiphestTaskMergedFromTransaction' => 'applications/maniphest/xaction/ManiphestTaskMergedFromTransaction.php',
@ -1944,6 +1946,7 @@ phutil_register_library_map(array(
'PHUIFormInsetView' => 'view/form/PHUIFormInsetView.php',
'PHUIFormLayoutView' => 'view/form/PHUIFormLayoutView.php',
'PHUIFormNumberControl' => 'view/form/control/PHUIFormNumberControl.php',
'PHUIFormTimerControl' => 'view/form/control/PHUIFormTimerControl.php',
'PHUIHandleListView' => 'applications/phid/view/PHUIHandleListView.php',
'PHUIHandleTagListView' => 'applications/phid/view/PHUIHandleTagListView.php',
'PHUIHandleView' => 'applications/phid/view/PHUIHandleView.php',
@ -2187,6 +2190,10 @@ phutil_register_library_map(array(
'PhabricatorAuthApplication' => 'applications/auth/application/PhabricatorAuthApplication.php',
'PhabricatorAuthAuthFactorPHIDType' => 'applications/auth/phid/PhabricatorAuthAuthFactorPHIDType.php',
'PhabricatorAuthAuthProviderPHIDType' => 'applications/auth/phid/PhabricatorAuthAuthProviderPHIDType.php',
'PhabricatorAuthChallenge' => 'applications/auth/storage/PhabricatorAuthChallenge.php',
'PhabricatorAuthChallengeGarbageCollector' => 'applications/auth/garbagecollector/PhabricatorAuthChallengeGarbageCollector.php',
'PhabricatorAuthChallengePHIDType' => 'applications/auth/phid/PhabricatorAuthChallengePHIDType.php',
'PhabricatorAuthChallengeQuery' => 'applications/auth/query/PhabricatorAuthChallengeQuery.php',
'PhabricatorAuthChangePasswordAction' => 'applications/auth/action/PhabricatorAuthChangePasswordAction.php',
'PhabricatorAuthConduitAPIMethod' => 'applications/auth/conduit/PhabricatorAuthConduitAPIMethod.php',
'PhabricatorAuthConduitTokenRevoker' => 'applications/auth/revoker/PhabricatorAuthConduitTokenRevoker.php',
@ -2198,6 +2205,7 @@ phutil_register_library_map(array(
'PhabricatorAuthEditController' => 'applications/auth/controller/config/PhabricatorAuthEditController.php',
'PhabricatorAuthFactor' => 'applications/auth/factor/PhabricatorAuthFactor.php',
'PhabricatorAuthFactorConfig' => 'applications/auth/storage/PhabricatorAuthFactorConfig.php',
'PhabricatorAuthFactorResult' => 'applications/auth/factor/PhabricatorAuthFactorResult.php',
'PhabricatorAuthFactorTestCase' => 'applications/auth/factor/__tests__/PhabricatorAuthFactorTestCase.php',
'PhabricatorAuthFinishController' => 'applications/auth/controller/PhabricatorAuthFinishController.php',
'PhabricatorAuthHMACKey' => 'applications/auth/storage/PhabricatorAuthHMACKey.php',
@ -2225,6 +2233,7 @@ phutil_register_library_map(array(
'PhabricatorAuthLoginController' => 'applications/auth/controller/PhabricatorAuthLoginController.php',
'PhabricatorAuthLoginHandler' => 'applications/auth/handler/PhabricatorAuthLoginHandler.php',
'PhabricatorAuthLogoutConduitAPIMethod' => 'applications/auth/conduit/PhabricatorAuthLogoutConduitAPIMethod.php',
'PhabricatorAuthMFAEditEngineExtension' => 'applications/auth/engineextension/PhabricatorAuthMFAEditEngineExtension.php',
'PhabricatorAuthMainMenuBarExtension' => 'applications/auth/extension/PhabricatorAuthMainMenuBarExtension.php',
'PhabricatorAuthManagementCachePKCS8Workflow' => 'applications/auth/management/PhabricatorAuthManagementCachePKCS8Workflow.php',
'PhabricatorAuthManagementLDAPWorkflow' => 'applications/auth/management/PhabricatorAuthManagementLDAPWorkflow.php',
@ -2666,6 +2675,7 @@ phutil_register_library_map(array(
'PhabricatorConfigProxySource' => 'infrastructure/env/PhabricatorConfigProxySource.php',
'PhabricatorConfigPurgeCacheController' => 'applications/config/controller/PhabricatorConfigPurgeCacheController.php',
'PhabricatorConfigRegexOptionType' => 'applications/config/custom/PhabricatorConfigRegexOptionType.php',
'PhabricatorConfigRemarkupRule' => 'infrastructure/markup/rule/PhabricatorConfigRemarkupRule.php',
'PhabricatorConfigRequestExceptionHandlerModule' => 'applications/config/module/PhabricatorConfigRequestExceptionHandlerModule.php',
'PhabricatorConfigResponse' => 'applications/config/response/PhabricatorConfigResponse.php',
'PhabricatorConfigSchemaQuery' => 'applications/config/schema/PhabricatorConfigSchemaQuery.php',
@ -2969,6 +2979,8 @@ phutil_register_library_map(array(
'PhabricatorEditEngineListController' => 'applications/transactions/controller/PhabricatorEditEngineListController.php',
'PhabricatorEditEngineLock' => 'applications/transactions/editengine/PhabricatorEditEngineLock.php',
'PhabricatorEditEngineLockableInterface' => 'applications/transactions/editengine/PhabricatorEditEngineLockableInterface.php',
'PhabricatorEditEngineMFAEngine' => 'applications/transactions/editengine/PhabricatorEditEngineMFAEngine.php',
'PhabricatorEditEngineMFAInterface' => 'applications/transactions/editengine/PhabricatorEditEngineMFAInterface.php',
'PhabricatorEditEnginePointsCommentAction' => 'applications/transactions/commentaction/PhabricatorEditEnginePointsCommentAction.php',
'PhabricatorEditEngineProfileMenuItem' => 'applications/search/menuitem/PhabricatorEditEngineProfileMenuItem.php',
'PhabricatorEditEngineQuery' => 'applications/transactions/query/PhabricatorEditEngineQuery.php',
@ -4428,6 +4440,7 @@ phutil_register_library_map(array(
'PhabricatorStandardCustomFieldUsers' => 'infrastructure/customfield/standard/PhabricatorStandardCustomFieldUsers.php',
'PhabricatorStandardPageView' => 'view/page/PhabricatorStandardPageView.php',
'PhabricatorStandardSelectCustomFieldDatasource' => 'infrastructure/customfield/datasource/PhabricatorStandardSelectCustomFieldDatasource.php',
'PhabricatorStandardTimelineEngine' => 'applications/transactions/engine/PhabricatorStandardTimelineEngine.php',
'PhabricatorStaticEditField' => 'applications/transactions/editfield/PhabricatorStaticEditField.php',
'PhabricatorStatusController' => 'applications/system/controller/PhabricatorStatusController.php',
'PhabricatorStatusUIExample' => 'applications/uiexample/examples/PhabricatorStatusUIExample.php',
@ -4527,6 +4540,8 @@ phutil_register_library_map(array(
'PhabricatorTimeFormatSetting' => 'applications/settings/setting/PhabricatorTimeFormatSetting.php',
'PhabricatorTimeGuard' => 'infrastructure/time/PhabricatorTimeGuard.php',
'PhabricatorTimeTestCase' => 'infrastructure/time/__tests__/PhabricatorTimeTestCase.php',
'PhabricatorTimelineEngine' => 'applications/transactions/engine/PhabricatorTimelineEngine.php',
'PhabricatorTimelineInterface' => 'applications/transactions/interface/PhabricatorTimelineInterface.php',
'PhabricatorTimezoneIgnoreOffsetSetting' => 'applications/settings/setting/PhabricatorTimezoneIgnoreOffsetSetting.php',
'PhabricatorTimezoneSetting' => 'applications/settings/setting/PhabricatorTimezoneSetting.php',
'PhabricatorTimezoneSetupCheck' => 'applications/config/check/PhabricatorTimezoneSetupCheck.php',
@ -4617,6 +4632,7 @@ phutil_register_library_map(array(
'PhabricatorUserEditorTestCase' => 'applications/people/editor/__tests__/PhabricatorUserEditorTestCase.php',
'PhabricatorUserEmail' => 'applications/people/storage/PhabricatorUserEmail.php',
'PhabricatorUserEmailTestCase' => 'applications/people/storage/__tests__/PhabricatorUserEmailTestCase.php',
'PhabricatorUserEmpowerTransaction' => 'applications/people/xaction/PhabricatorUserEmpowerTransaction.php',
'PhabricatorUserFerretEngine' => 'applications/people/search/PhabricatorUserFerretEngine.php',
'PhabricatorUserFulltextEngine' => 'applications/people/search/PhabricatorUserFulltextEngine.php',
'PhabricatorUserIconField' => 'applications/people/customfield/PhabricatorUserIconField.php',
@ -4865,6 +4881,7 @@ phutil_register_library_map(array(
'PholioMockSearchEngine' => 'applications/pholio/query/PholioMockSearchEngine.php',
'PholioMockStatusTransaction' => 'applications/pholio/xaction/PholioMockStatusTransaction.php',
'PholioMockThumbGridView' => 'applications/pholio/view/PholioMockThumbGridView.php',
'PholioMockTimelineEngine' => 'applications/pholio/engine/PholioMockTimelineEngine.php',
'PholioMockTransactionType' => 'applications/pholio/xaction/PholioMockTransactionType.php',
'PholioMockViewController' => 'applications/pholio/controller/PholioMockViewController.php',
'PholioRemarkupRule' => 'applications/pholio/remarkup/PholioRemarkupRule.php',
@ -5985,6 +6002,7 @@ phutil_register_library_map(array(
'PhabricatorSubscribableInterface',
'PhabricatorCustomFieldInterface',
'PhabricatorApplicationTransactionInterface',
'PhabricatorTimelineInterface',
'PhabricatorMentionableInterface',
'PhabricatorDestructibleInterface',
'PhabricatorProjectInterface',
@ -6066,6 +6084,7 @@ phutil_register_library_map(array(
'DifferentialRevisionSummaryTransaction' => 'DifferentialRevisionTransactionType',
'DifferentialRevisionTestPlanHeraldField' => 'DifferentialRevisionHeraldField',
'DifferentialRevisionTestPlanTransaction' => 'DifferentialRevisionTransactionType',
'DifferentialRevisionTimelineEngine' => 'PhabricatorTimelineEngine',
'DifferentialRevisionTitleHeraldField' => 'DifferentialRevisionHeraldField',
'DifferentialRevisionTitleTransaction' => 'DifferentialRevisionTransactionType',
'DifferentialRevisionTransactionType' => 'PhabricatorModularTransactionType',
@ -6188,6 +6207,7 @@ phutil_register_library_map(array(
'DiffusionCommitSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod',
'DiffusionCommitStateTransaction' => 'DiffusionCommitTransactionType',
'DiffusionCommitTagsController' => 'DiffusionController',
'DiffusionCommitTimelineEngine' => 'PhabricatorTimelineEngine',
'DiffusionCommitTransactionType' => 'PhabricatorModularTransactionType',
'DiffusionCommitVerifyTransaction' => 'DiffusionCommitAuditTransaction',
'DiffusionCompareController' => 'DiffusionController',
@ -7195,7 +7215,6 @@ phutil_register_library_map(array(
'LegalpadTransaction' => 'PhabricatorModularTransaction',
'LegalpadTransactionComment' => 'PhabricatorApplicationTransactionComment',
'LegalpadTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'LegalpadTransactionView' => 'PhabricatorApplicationTransactionView',
'LiskChunkTestCase' => 'PhabricatorTestCase',
'LiskDAO' => array(
'Phobject',
@ -7283,6 +7302,7 @@ phutil_register_library_map(array(
'DoorkeeperBridgedObjectInterface',
'PhabricatorEditEngineSubtypeInterface',
'PhabricatorEditEngineLockableInterface',
'PhabricatorEditEngineMFAInterface',
),
'ManiphestTaskAssignHeraldAction' => 'HeraldAction',
'ManiphestTaskAssignOtherHeraldAction' => 'ManiphestTaskAssignHeraldAction',
@ -7321,6 +7341,7 @@ phutil_register_library_map(array(
'ManiphestTaskListController' => 'ManiphestController',
'ManiphestTaskListHTTPParameterType' => 'AphrontListHTTPParameterType',
'ManiphestTaskListView' => 'ManiphestView',
'ManiphestTaskMFAEngine' => 'PhabricatorEditEngineMFAEngine',
'ManiphestTaskMailReceiver' => 'PhabricatorObjectMailReceiver',
'ManiphestTaskMergeInRelationship' => 'ManiphestTaskRelationship',
'ManiphestTaskMergedFromTransaction' => 'ManiphestTaskTransactionType',
@ -7554,6 +7575,7 @@ phutil_register_library_map(array(
'PHUIFormInsetView' => 'AphrontView',
'PHUIFormLayoutView' => 'AphrontView',
'PHUIFormNumberControl' => 'AphrontFormControl',
'PHUIFormTimerControl' => 'AphrontFormControl',
'PHUIHandleListView' => 'AphrontTagView',
'PHUIHandleTagListView' => 'AphrontTagView',
'PHUIHandleView' => 'AphrontView',
@ -7822,6 +7844,13 @@ phutil_register_library_map(array(
'PhabricatorAuthApplication' => 'PhabricatorApplication',
'PhabricatorAuthAuthFactorPHIDType' => 'PhabricatorPHIDType',
'PhabricatorAuthAuthProviderPHIDType' => 'PhabricatorPHIDType',
'PhabricatorAuthChallenge' => array(
'PhabricatorAuthDAO',
'PhabricatorPolicyInterface',
),
'PhabricatorAuthChallengeGarbageCollector' => 'PhabricatorGarbageCollector',
'PhabricatorAuthChallengePHIDType' => 'PhabricatorPHIDType',
'PhabricatorAuthChallengeQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorAuthChangePasswordAction' => 'PhabricatorSystemAction',
'PhabricatorAuthConduitAPIMethod' => 'ConduitAPIMethod',
'PhabricatorAuthConduitTokenRevoker' => 'PhabricatorAuthRevoker',
@ -7833,6 +7862,7 @@ phutil_register_library_map(array(
'PhabricatorAuthEditController' => 'PhabricatorAuthProviderConfigController',
'PhabricatorAuthFactor' => 'Phobject',
'PhabricatorAuthFactorConfig' => 'PhabricatorAuthDAO',
'PhabricatorAuthFactorResult' => 'Phobject',
'PhabricatorAuthFactorTestCase' => 'PhabricatorTestCase',
'PhabricatorAuthFinishController' => 'PhabricatorAuthController',
'PhabricatorAuthHMACKey' => 'PhabricatorAuthDAO',
@ -7863,6 +7893,7 @@ phutil_register_library_map(array(
'PhabricatorAuthLoginController' => 'PhabricatorAuthController',
'PhabricatorAuthLoginHandler' => 'Phobject',
'PhabricatorAuthLogoutConduitAPIMethod' => 'PhabricatorAuthConduitAPIMethod',
'PhabricatorAuthMFAEditEngineExtension' => 'PhabricatorEditEngineExtension',
'PhabricatorAuthMainMenuBarExtension' => 'PhabricatorMainMenuBarExtension',
'PhabricatorAuthManagementCachePKCS8Workflow' => 'PhabricatorAuthManagementWorkflow',
'PhabricatorAuthManagementLDAPWorkflow' => 'PhabricatorAuthManagementWorkflow',
@ -8388,6 +8419,7 @@ phutil_register_library_map(array(
'PhabricatorConfigProxySource' => 'PhabricatorConfigSource',
'PhabricatorConfigPurgeCacheController' => 'PhabricatorConfigController',
'PhabricatorConfigRegexOptionType' => 'PhabricatorConfigJSONOptionType',
'PhabricatorConfigRemarkupRule' => 'PhutilRemarkupRule',
'PhabricatorConfigRequestExceptionHandlerModule' => 'PhabricatorConfigModule',
'PhabricatorConfigResponse' => 'AphrontStandaloneHTMLResponse',
'PhabricatorConfigSchemaQuery' => 'Phobject',
@ -8729,6 +8761,7 @@ phutil_register_library_map(array(
'PhabricatorEditEngineExtensionModule' => 'PhabricatorConfigModule',
'PhabricatorEditEngineListController' => 'PhabricatorEditEngineController',
'PhabricatorEditEngineLock' => 'Phobject',
'PhabricatorEditEngineMFAEngine' => 'Phobject',
'PhabricatorEditEnginePointsCommentAction' => 'PhabricatorEditEngineCommentAction',
'PhabricatorEditEngineProfileMenuItem' => 'PhabricatorProfileMenuItem',
'PhabricatorEditEngineQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
@ -10065,6 +10098,7 @@ phutil_register_library_map(array(
'HarbormasterBuildkiteBuildableInterface',
'PhabricatorCustomFieldInterface',
'PhabricatorApplicationTransactionInterface',
'PhabricatorTimelineInterface',
'PhabricatorFulltextInterface',
'PhabricatorFerretInterface',
'PhabricatorConduitResultInterface',
@ -10455,6 +10489,7 @@ phutil_register_library_map(array(
'AphrontResponseProducerInterface',
),
'PhabricatorStandardSelectCustomFieldDatasource' => 'PhabricatorTypeaheadDatasource',
'PhabricatorStandardTimelineEngine' => 'PhabricatorTimelineEngine',
'PhabricatorStaticEditField' => 'PhabricatorEditField',
'PhabricatorStatusController' => 'PhabricatorController',
'PhabricatorStatusUIExample' => 'PhabricatorUIExample',
@ -10553,6 +10588,7 @@ phutil_register_library_map(array(
'PhabricatorTimeFormatSetting' => 'PhabricatorSelectSetting',
'PhabricatorTimeGuard' => 'Phobject',
'PhabricatorTimeTestCase' => 'PhabricatorTestCase',
'PhabricatorTimelineEngine' => 'Phobject',
'PhabricatorTimezoneIgnoreOffsetSetting' => 'PhabricatorInternalSetting',
'PhabricatorTimezoneSetting' => 'PhabricatorOptionGroupSetting',
'PhabricatorTimezoneSetupCheck' => 'PhabricatorSetupCheck',
@ -10670,6 +10706,7 @@ phutil_register_library_map(array(
'PhabricatorUserEditorTestCase' => 'PhabricatorTestCase',
'PhabricatorUserEmail' => 'PhabricatorUserDAO',
'PhabricatorUserEmailTestCase' => 'PhabricatorTestCase',
'PhabricatorUserEmpowerTransaction' => 'PhabricatorUserTransactionType',
'PhabricatorUserFerretEngine' => 'PhabricatorFerretEngine',
'PhabricatorUserFulltextEngine' => 'PhabricatorFulltextEngine',
'PhabricatorUserIconField' => 'PhabricatorUserCustomField',
@ -10931,8 +10968,8 @@ phutil_register_library_map(array(
'PholioDefaultViewCapability' => 'PhabricatorPolicyCapability',
'PholioImage' => array(
'PholioDAO',
'PhabricatorMarkupInterface',
'PhabricatorPolicyInterface',
'PhabricatorExtendedPolicyInterface',
),
'PholioImageDescriptionTransaction' => 'PholioImageTransactionType',
'PholioImageFileTransaction' => 'PholioImageTransactionType',
@ -10947,12 +10984,12 @@ phutil_register_library_map(array(
'PholioInlineListController' => 'PholioController',
'PholioMock' => array(
'PholioDAO',
'PhabricatorMarkupInterface',
'PhabricatorPolicyInterface',
'PhabricatorSubscribableInterface',
'PhabricatorTokenReceiverInterface',
'PhabricatorFlaggableInterface',
'PhabricatorApplicationTransactionInterface',
'PhabricatorTimelineInterface',
'PhabricatorProjectInterface',
'PhabricatorDestructibleInterface',
'PhabricatorSpacesInterface',
@ -10987,6 +11024,7 @@ phutil_register_library_map(array(
'PholioMockSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PholioMockStatusTransaction' => 'PholioMockTransactionType',
'PholioMockThumbGridView' => 'AphrontView',
'PholioMockTimelineEngine' => 'PhabricatorTimelineEngine',
'PholioMockTransactionType' => 'PholioTransactionType',
'PholioMockViewController' => 'PholioController',
'PholioRemarkupRule' => 'PhabricatorObjectRemarkupRule',

View file

@ -29,40 +29,80 @@ final class PhabricatorHighSecurityRequestExceptionHandler
$throwable) {
$viewer = $this->getViewer($request);
$results = $throwable->getFactorValidationResults();
$form = id(new PhabricatorAuthSessionEngine())->renderHighSecurityForm(
$throwable->getFactors(),
$throwable->getFactorValidationResults(),
$results,
$viewer,
$request);
$is_wait = false;
foreach ($results as $result) {
if ($result->getIsWait()) {
$is_wait = true;
break;
}
}
$is_upgrade = $throwable->getIsSessionUpgrade();
if ($is_upgrade) {
$title = pht('Enter High Security');
} else {
$title = pht('Provide MFA Credentials');
}
if ($is_wait) {
$submit = pht('Wait Patiently');
} else if ($is_upgrade) {
$submit = pht('Enter High Security');
} else {
$submit = pht('Continue');
}
$dialog = id(new AphrontDialogView())
->setUser($viewer)
->setTitle(pht('Entering High Security'))
->setTitle($title)
->setShortTitle(pht('Security Checkpoint'))
->setWidth(AphrontDialogView::WIDTH_FORM)
->addHiddenInput(AphrontRequest::TYPE_HISEC, true)
->setErrors(
array(
pht(
'You are taking an action which requires you to enter '.
'high security.'),
))
->appendParagraph(
pht(
'High security mode helps protect your account from security '.
'threats, like session theft or someone messing with your stuff '.
'while you\'re grabbing a coffee. To enter high security mode, '.
'confirm your credentials.'))
->appendChild($form->buildLayoutView())
->appendParagraph(
pht(
'Your account will remain in high security mode for a short '.
'period of time. When you are finished taking sensitive '.
'actions, you should leave high security.'))
->setSubmitURI($request->getPath())
->addCancelButton($throwable->getCancelURI())
->addSubmitButton(pht('Enter High Security'));
->addSubmitButton($submit);
$form_layout = $form->buildLayoutView();
if ($is_upgrade) {
$dialog
->setErrors(
array(
pht(
'You are taking an action which requires you to enter '.
'high security.'),
))
->appendParagraph(
pht(
'High security mode helps protect your account from security '.
'threats, like session theft or someone messing with your stuff '.
'while you\'re grabbing a coffee. To enter high security mode, '.
'confirm your credentials.'))
->appendChild($form_layout)
->appendParagraph(
pht(
'Your account will remain in high security mode for a short '.
'period of time. When you are finished taking sensitive '.
'actions, you should leave high security.'));
} else {
$dialog
->setErrors(
array(
pht(
'You are taking an action which requires you to provide '.
'multi-factor credentials.'),
))
->appendChild($form_layout);
}
$request_parameters = $request->getPassthroughRequestParameters(
$respect_quicksand = true);

View file

@ -206,20 +206,10 @@ final class AlmanacBinding
return new AlmanacBindingEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new AlmanacBindingTransaction();
}
public function willRenderTimeline(
PhabricatorApplicationTransactionView $timeline,
AphrontRequest $request) {
return $timeline;
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */

View file

@ -204,21 +204,10 @@ final class AlmanacDevice
return new AlmanacDeviceEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new AlmanacDeviceTransaction();
}
public function willRenderTimeline(
PhabricatorApplicationTransactionView $timeline,
AphrontRequest $request) {
return $timeline;
}
/* -( PhabricatorSSHPublicKeyInterface )----------------------------------- */

View file

@ -168,20 +168,10 @@ final class AlmanacInterface
return new AlmanacInterfaceEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new AlmanacInterfaceTransaction();
}
public function willRenderTimeline(
PhabricatorApplicationTransactionView $timeline,
AphrontRequest $request) {
return $timeline;
}
/* -( PhabricatorConduitResultInterface )---------------------------------- */

View file

@ -191,20 +191,10 @@ final class AlmanacNamespace
return new AlmanacNamespaceEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new AlmanacNamespaceTransaction();
}
public function willRenderTimeline(
PhabricatorApplicationTransactionView $timeline,
AphrontRequest $request) {
return $timeline;
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */

View file

@ -61,21 +61,10 @@ final class AlmanacNetwork
return new AlmanacNetworkEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new AlmanacNetworkTransaction();
}
public function willRenderTimeline(
PhabricatorApplicationTransactionView $timeline,
AphrontRequest $request) {
return $timeline;
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */

View file

@ -226,21 +226,10 @@ final class AlmanacService
return new AlmanacServiceEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new AlmanacServiceTransaction();
}
public function willRenderTimeline(
PhabricatorApplicationTransactionView $timeline,
AphrontRequest $request) {
return $timeline;
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */

View file

@ -55,11 +55,22 @@ final class AlmanacBindingInterfaceTransaction
}
public function getTitle() {
return pht(
'%s changed the interface for this binding from %s to %s.',
$this->renderAuthor(),
$this->renderOldHandle(),
$this->renderNewHandle());
if ($this->getOldValue() === null) {
return pht(
'%s set the interface for this binding to %s.',
$this->renderAuthor(),
$this->renderNewHandle());
} else if ($this->getNewValue() == null) {
return pht(
'%s removed the interface for this binding.',
$this->renderAuthor());
} else {
return pht(
'%s changed the interface for this binding from %s to %s.',
$this->renderAuthor(),
$this->renderOldHandle(),
$this->renderNewHandle());
}
}
public function validateTransactions($object, array $xactions) {

View file

@ -32,10 +32,6 @@ final class PhabricatorAuditTransaction
return new PhabricatorAuditTransactionComment();
}
public function getApplicationTransactionViewObject() {
return new PhabricatorAuditTransactionView();
}
public function getRemarkupBlocks() {
$blocks = parent::getRemarkupBlocks();

View file

@ -45,45 +45,39 @@ abstract class PhabricatorAuthController extends PhabricatorController {
* event and do something else if they prefer.
*
* @param PhabricatorUser User to log the viewer in as.
* @param bool True to issue a full session immediately, bypassing MFA.
* @return AphrontResponse Response which continues the login process.
*/
protected function loginUser(PhabricatorUser $user) {
protected function loginUser(
PhabricatorUser $user,
$force_full_session = false) {
$response = $this->buildLoginValidateResponse($user);
$session_type = PhabricatorAuthSession::TYPE_WEB;
$event_type = PhabricatorEventType::TYPE_AUTH_WILLLOGINUSER;
$event_data = array(
'user' => $user,
'type' => $session_type,
'response' => $response,
'shouldLogin' => true,
);
$event = id(new PhabricatorEvent($event_type, $event_data))
->setUser($user);
PhutilEventEngine::dispatchEvent($event);
$should_login = $event->getValue('shouldLogin');
if ($should_login) {
$session_key = id(new PhabricatorAuthSessionEngine())
->establishSession($session_type, $user->getPHID(), $partial = true);
// NOTE: We allow disabled users to login and roadblock them later, so
// there's no check for users being disabled here.
$request = $this->getRequest();
$request->setCookie(
PhabricatorCookies::COOKIE_USERNAME,
$user->getUsername());
$request->setCookie(
PhabricatorCookies::COOKIE_SESSION,
$session_key);
$this->clearRegistrationCookies();
if ($force_full_session) {
$partial_session = false;
} else {
$partial_session = true;
}
return $event->getValue('response');
$session_key = id(new PhabricatorAuthSessionEngine())
->establishSession($session_type, $user->getPHID(), $partial_session);
// NOTE: We allow disabled users to login and roadblock them later, so
// there's no check for users being disabled here.
$request = $this->getRequest();
$request->setCookie(
PhabricatorCookies::COOKIE_USERNAME,
$user->getUsername());
$request->setCookie(
PhabricatorCookies::COOKIE_SESSION,
$session_key);
$this->clearRegistrationCookies();
return $response;
}
protected function clearRegistrationCookies() {

View file

@ -152,7 +152,12 @@ final class PhabricatorAuthOneTimeLoginController
PhabricatorCookies::setNextURICookie($request, $next, $force = true);
return $this->loginUser($target_user);
$force_full_session = false;
if ($link_type === PhabricatorAuthSessionEngine::ONETIME_RECOVER) {
$force_full_session = $token->getShouldForceFullSession();
}
return $this->loginUser($target_user, $force_full_session);
}
// NOTE: We need to CSRF here so attackers can't generate an email link,

View file

@ -416,7 +416,26 @@ final class PhabricatorAuthRegisterController
}
if ($is_setup) {
$editor->makeAdminUser($user, true);
$xactions = array();
$xactions[] = id(new PhabricatorUserTransaction())
->setTransactionType(
PhabricatorUserEmpowerTransaction::TRANSACTIONTYPE)
->setNewValue(true);
$actor = PhabricatorUser::getOmnipotentUser();
$content_source = PhabricatorContentSource::newFromRequest(
$request);
$people_application_phid = id(new PhabricatorPeopleApplication())
->getPHID();
$transaction_editor = id(new PhabricatorUserTransactionEditor())
->setActor($actor)
->setActingAsPHID($people_application_phid)
->setContentSource($content_source)
->setContinueOnMissingFields(true);
$transaction_editor->applyTransactions($user, $xactions);
}
$account->setUserPHID($user->getPHID());

View file

@ -46,6 +46,26 @@ final class PhabricatorAuthSessionEngine extends Phobject {
const ONETIME_USERNAME = 'rename';
private $workflowKey;
public function setWorkflowKey($workflow_key) {
$this->workflowKey = $workflow_key;
return $this;
}
public function getWorkflowKey() {
// TODO: A workflow key should become required in order to issue an MFA
// challenge, but allow things to keep working for now until we can update
// callsites.
if ($this->workflowKey === null) {
return 'legacy';
}
return $this->workflowKey;
}
/**
* Get the session kind (e.g., anonymous, user, external account) from a
* session token. Returns a `KIND_` constant.
@ -193,26 +213,7 @@ final class PhabricatorAuthSessionEngine extends Phobject {
$session = id(new PhabricatorAuthSession())->loadFromArray($session_dict);
$ttl = PhabricatorAuthSession::getSessionTypeTTL($session_type);
// If more than 20% of the time on this session has been used, refresh the
// TTL back up to the full duration. The idea here is that sessions are
// good forever if used regularly, but get GC'd when they fall out of use.
// NOTE: If we begin rotating session keys when extending sessions, the
// CSRF code needs to be updated so CSRF tokens survive session rotation.
if (time() + (0.80 * $ttl) > $session->getSessionExpires()) {
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$conn_w = $session_table->establishConnection('w');
queryfx(
$conn_w,
'UPDATE %T SET sessionExpires = UNIX_TIMESTAMP() + %d WHERE id = %d',
$session->getTableName(),
$ttl,
$session->getID());
unset($unguarded);
}
$this->extendSession($session);
// TODO: Remove this, see T13225.
if ($is_weak) {
@ -264,7 +265,9 @@ final class PhabricatorAuthSessionEngine extends Phobject {
$conn_w = $session_table->establishConnection('w');
// This has a side effect of validating the session type.
$session_ttl = PhabricatorAuthSession::getSessionTypeTTL($session_type);
$session_ttl = PhabricatorAuthSession::getSessionTypeTTL(
$session_type,
$partial);
$digest_key = PhabricatorAuthSession::newSessionDigest(
new PhutilOpaqueEnvelope($session_key));
@ -431,7 +434,7 @@ final class PhabricatorAuthSessionEngine extends Phobject {
$viewer,
$request,
$cancel_uri,
false,
$jump_into_hisec,
true);
}
@ -473,6 +476,10 @@ final class PhabricatorAuthSessionEngine extends Phobject {
return $this->issueHighSecurityToken($session, true);
}
foreach ($factors as $factor) {
$factor->setSessionEngine($this);
}
// Check for a rate limit without awarding points, so the user doesn't
// get partway through the workflow only to get blocked.
PhabricatorSystemActionEngine::willTakeAction(
@ -480,38 +487,129 @@ final class PhabricatorAuthSessionEngine extends Phobject {
new PhabricatorAuthTryFactorAction(),
0);
$now = PhabricatorTime::getNow();
// We need to do challenge validation first, since this happens whether you
// submitted responses or not. You can't get a "bad response" error before
// you actually submit a response, but you can get a "wait, we can't
// issue a challenge yet" response. Load all issued challenges which are
// currently valid.
$challenges = id(new PhabricatorAuthChallengeQuery())
->setViewer($viewer)
->withFactorPHIDs(mpull($factors, 'getPHID'))
->withUserPHIDs(array($viewer->getPHID()))
->withChallengeTTLBetween($now, null)
->execute();
PhabricatorAuthChallenge::newChallengeResponsesFromRequest(
$challenges,
$request);
$challenge_map = mgroup($challenges, 'getFactorPHID');
$validation_results = array();
$ok = true;
// Validate each factor against issued challenges. For example, this
// prevents you from receiving or responding to a TOTP challenge if another
// challenge was recently issued to a different session.
foreach ($factors as $factor) {
$factor_phid = $factor->getPHID();
$issued_challenges = idx($challenge_map, $factor_phid, array());
$impl = $factor->requireImplementation();
$new_challenges = $impl->getNewIssuedChallenges(
$factor,
$viewer,
$issued_challenges);
foreach ($new_challenges as $new_challenge) {
$issued_challenges[] = $new_challenge;
}
$challenge_map[$factor_phid] = $issued_challenges;
if (!$issued_challenges) {
continue;
}
$result = $impl->getResultFromIssuedChallenges(
$factor,
$viewer,
$issued_challenges);
if (!$result) {
continue;
}
$ok = false;
$validation_results[$factor_phid] = $result;
}
if ($request->isHTTPPost()) {
$request->validateCSRF();
if ($request->getExists(AphrontRequest::TYPE_HISEC)) {
// Limit factor verification rates to prevent brute force attacks.
PhabricatorSystemActionEngine::willTakeAction(
array($viewer->getPHID()),
new PhabricatorAuthTryFactorAction(),
1);
$ok = true;
$any_attempt = false;
foreach ($factors as $factor) {
$id = $factor->getID();
$impl = $factor->requireImplementation();
$validation_results[$id] = $impl->processValidateFactorForm(
$factor,
$viewer,
$request);
if (!$impl->isFactorValid($factor, $validation_results[$id])) {
$ok = false;
if ($impl->getRequestHasChallengeResponse($factor, $request)) {
$any_attempt = true;
break;
}
}
if ($ok) {
// Give the user a credit back for a successful factor verification.
if ($any_attempt) {
PhabricatorSystemActionEngine::willTakeAction(
array($viewer->getPHID()),
new PhabricatorAuthTryFactorAction(),
-1);
1);
}
foreach ($factors as $factor) {
$factor_phid = $factor->getPHID();
// If we already have a validation result from previously issued
// challenges, skip validating this factor.
if (isset($validation_results[$factor_phid])) {
continue;
}
$issued_challenges = idx($challenge_map, $factor_phid, array());
$impl = $factor->requireImplementation();
$validation_result = $impl->getResultFromChallengeResponse(
$factor,
$viewer,
$request,
$issued_challenges);
if (!$validation_result->getIsValid()) {
$ok = false;
}
$validation_results[$factor_phid] = $validation_result;
}
if ($ok) {
// We're letting you through, so mark all the challenges you
// responded to as completed. These challenges can never be used
// again, even by the same session and workflow: you can't use the
// same response to take two different actions, even if those actions
// are of the same type.
foreach ($validation_results as $validation_result) {
$challenge = $validation_result->getAnsweredChallenge()
->markChallengeAsCompleted();
}
// Give the user a credit back for a successful factor verification.
if ($any_attempt) {
PhabricatorSystemActionEngine::willTakeAction(
array($viewer->getPHID()),
new PhabricatorAuthTryFactorAction(),
-1);
}
if ($session->getIsPartial() && !$jump_into_hisec) {
// If we have a partial session and are not jumping directly into
@ -555,8 +653,21 @@ final class PhabricatorAuthSessionEngine extends Phobject {
return $token;
}
// If we don't have a validation result for some factors yet, fill them
// in with an empty result so form rendering doesn't have to care if the
// results exist or not. This happens when you first load the form and have
// not submitted any responses yet.
foreach ($factors as $factor) {
$factor_phid = $factor->getPHID();
if (isset($validation_results[$factor_phid])) {
continue;
}
$validation_results[$factor_phid] = new PhabricatorAuthFactorResult();
}
throw id(new PhabricatorAuthHighSecurityRequiredException())
->setCancelURI($cancel_uri)
->setIsSessionUpgrade($upgrade_session)
->setFactors($factors)
->setFactorValidationResults($validation_results);
}
@ -595,21 +706,38 @@ final class PhabricatorAuthSessionEngine extends Phobject {
array $validation_results,
PhabricatorUser $viewer,
AphrontRequest $request) {
assert_instances_of($validation_results, 'PhabricatorAuthFactorResult');
$form = id(new AphrontFormView())
->setUser($viewer)
->appendRemarkupInstructions('');
$answered = array();
foreach ($factors as $factor) {
$result = $validation_results[$factor->getPHID()];
$factor->requireImplementation()->renderValidateFactorForm(
$factor,
$form,
$viewer,
idx($validation_results, $factor->getID()));
$result);
$answered_challenge = $result->getAnsweredChallenge();
if ($answered_challenge) {
$answered[] = $answered_challenge;
}
}
$form->appendRemarkupInstructions('');
if ($answered) {
$http_params = PhabricatorAuthChallenge::newHTTPParametersFromChallenges(
$answered);
foreach ($http_params as $key => $value) {
$form->addHiddenInput($key, $value);
}
}
return $form;
}
@ -748,24 +876,28 @@ final class PhabricatorAuthSessionEngine extends Phobject {
* link is used.
* @param string Optional context string for the URI. This is purely cosmetic
* and used only to customize workflow and error messages.
* @param bool True to generate a URI which forces an immediate upgrade to
* a full session, bypassing MFA and other login checks.
* @return string Login URI.
* @task onetime
*/
public function getOneTimeLoginURI(
PhabricatorUser $user,
PhabricatorUserEmail $email = null,
$type = self::ONETIME_RESET) {
$type = self::ONETIME_RESET,
$force_full_session = false) {
$key = Filesystem::readRandomCharacters(32);
$key_hash = $this->getOneTimeLoginKeyHash($user, $email, $key);
$onetime_type = PhabricatorAuthOneTimeLoginTemporaryTokenType::TOKENTYPE;
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
id(new PhabricatorAuthTemporaryToken())
$token = id(new PhabricatorAuthTemporaryToken())
->setTokenResource($user->getPHID())
->setTokenType($onetime_type)
->setTokenExpires(time() + phutil_units('1 day in seconds'))
->setTokenCode($key_hash)
->setShouldForceFullSession($force_full_session)
->save();
unset($unguarded);
@ -937,4 +1069,40 @@ final class PhabricatorAuthSessionEngine extends Phobject {
}
}
private function extendSession(PhabricatorAuthSession $session) {
$is_partial = $session->getIsPartial();
// Don't extend partial sessions. You have a relatively short window to
// upgrade into a full session, and your session expires otherwise.
if ($is_partial) {
return;
}
$session_type = $session->getType();
$ttl = PhabricatorAuthSession::getSessionTypeTTL(
$session_type,
$session->getIsPartial());
// If more than 20% of the time on this session has been used, refresh the
// TTL back up to the full duration. The idea here is that sessions are
// good forever if used regularly, but get GC'd when they fall out of use.
$now = PhabricatorTime::getNow();
if ($now + (0.80 * $ttl) <= $session->getSessionExpires()) {
return;
}
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
queryfx(
$session->establishConnection('w'),
'UPDATE %R SET sessionExpires = UNIX_TIMESTAMP() + %d
WHERE id = %d',
$session,
$ttl,
$session->getID());
unset($unguarded);
}
}

View file

@ -0,0 +1,52 @@
<?php
final class PhabricatorAuthMFAEditEngineExtension
extends PhabricatorEditEngineExtension {
const EXTENSIONKEY = 'auth.mfa';
const FIELDKEY = 'mfa';
public function getExtensionPriority() {
return 12000;
}
public function isExtensionEnabled() {
return true;
}
public function getExtensionName() {
return pht('MFA');
}
public function supportsObject(
PhabricatorEditEngine $engine,
PhabricatorApplicationTransactionInterface $object) {
return true;
}
public function buildCustomEditFields(
PhabricatorEditEngine $engine,
PhabricatorApplicationTransactionInterface $object) {
$mfa_type = PhabricatorTransactions::TYPE_MFA;
$viewer = $engine->getViewer();
$mfa_field = id(new PhabricatorApplyEditField())
->setViewer($viewer)
->setKey(self::FIELDKEY)
->setLabel(pht('MFA'))
->setIsFormField(false)
->setCommentActionLabel(pht('Sign With MFA'))
->setCommentActionOrder(12000)
->setActionDescription(
pht('You will be prompted to provide MFA when you submit.'))
->setDescription(pht('Sign this transaction group with MFA.'))
->setTransactionType($mfa_type);
return array(
$mfa_field,
);
}
}

View file

@ -5,8 +5,10 @@ final class PhabricatorAuthHighSecurityRequiredException extends Exception {
private $cancelURI;
private $factors;
private $factorValidationResults;
private $isSessionUpgrade;
public function setFactorValidationResults(array $results) {
assert_instances_of($results, 'PhabricatorAuthFactorResult');
$this->factorValidationResults = $results;
return $this;
}
@ -34,4 +36,13 @@ final class PhabricatorAuthHighSecurityRequiredException extends Exception {
return $this->cancelURI;
}
public function setIsSessionUpgrade($is_upgrade) {
$this->isSessionUpgrade = $is_upgrade;
return $this;
}
public function getIsSessionUpgrade() {
return $this->isSessionUpgrade;
}
}

View file

@ -14,18 +14,7 @@ abstract class PhabricatorAuthFactor extends Phobject {
PhabricatorAuthFactorConfig $config,
AphrontFormView $form,
PhabricatorUser $viewer,
$validation_result);
abstract public function processValidateFactorForm(
PhabricatorAuthFactorConfig $config,
PhabricatorUser $viewer,
AphrontRequest $request);
public function isFactorValid(
PhabricatorAuthFactorConfig $config,
$validation_result) {
return (idx($validation_result, 'valid') === true);
}
PhabricatorAuthFactorResult $validation_result);
public function getParameterName(
PhabricatorAuthFactorConfig $config,
@ -46,4 +35,181 @@ abstract class PhabricatorAuthFactor extends Phobject {
->setFactorKey($this->getFactorKey());
}
protected function newResult() {
return new PhabricatorAuthFactorResult();
}
protected function newChallenge(
PhabricatorAuthFactorConfig $config,
PhabricatorUser $viewer) {
$engine = $config->getSessionEngine();
return PhabricatorAuthChallenge::initializeNewChallenge()
->setUserPHID($viewer->getPHID())
->setSessionPHID($viewer->getSession()->getPHID())
->setFactorPHID($config->getPHID())
->setWorkflowKey($engine->getWorkflowKey());
}
abstract public function getRequestHasChallengeResponse(
PhabricatorAuthFactorConfig $config,
AphrontRequest $response);
final public function getNewIssuedChallenges(
PhabricatorAuthFactorConfig $config,
PhabricatorUser $viewer,
array $challenges) {
assert_instances_of($challenges, 'PhabricatorAuthChallenge');
$now = PhabricatorTime::getNow();
$new_challenges = $this->newIssuedChallenges(
$config,
$viewer,
$challenges);
assert_instances_of($new_challenges, 'PhabricatorAuthChallenge');
foreach ($new_challenges as $new_challenge) {
$ttl = $new_challenge->getChallengeTTL();
if (!$ttl) {
throw new Exception(
pht('Newly issued MFA challenges must have a valid TTL!'));
}
if ($ttl < $now) {
throw new Exception(
pht(
'Newly issued MFA challenges must have a future TTL. This '.
'factor issued a bad TTL ("%s"). (Did you use a relative '.
'time instead of an epoch?)',
$ttl));
}
}
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
foreach ($new_challenges as $challenge) {
$challenge->save();
}
unset($unguarded);
return $new_challenges;
}
abstract protected function newIssuedChallenges(
PhabricatorAuthFactorConfig $config,
PhabricatorUser $viewer,
array $challenges);
final public function getResultFromIssuedChallenges(
PhabricatorAuthFactorConfig $config,
PhabricatorUser $viewer,
array $challenges) {
assert_instances_of($challenges, 'PhabricatorAuthChallenge');
$result = $this->newResultFromIssuedChallenges(
$config,
$viewer,
$challenges);
if ($result === null) {
return $result;
}
if (!($result instanceof PhabricatorAuthFactorResult)) {
throw new Exception(
pht(
'Expected "newResultFromIssuedChallenges()" to return null or '.
'an object of class "%s"; got something else (in "%s").',
'PhabricatorAuthFactorResult',
get_class($this)));
}
$result->setIssuedChallenges($challenges);
return $result;
}
abstract protected function newResultFromIssuedChallenges(
PhabricatorAuthFactorConfig $config,
PhabricatorUser $viewer,
array $challenges);
final public function getResultFromChallengeResponse(
PhabricatorAuthFactorConfig $config,
PhabricatorUser $viewer,
AphrontRequest $request,
array $challenges) {
assert_instances_of($challenges, 'PhabricatorAuthChallenge');
$result = $this->newResultFromChallengeResponse(
$config,
$viewer,
$request,
$challenges);
if (!($result instanceof PhabricatorAuthFactorResult)) {
throw new Exception(
pht(
'Expected "newResultFromChallengeResponse()" to return an object '.
'of class "%s"; got something else (in "%s").',
'PhabricatorAuthFactorResult',
get_class($this)));
}
$result->setIssuedChallenges($challenges);
return $result;
}
abstract protected function newResultFromChallengeResponse(
PhabricatorAuthFactorConfig $config,
PhabricatorUser $viewer,
AphrontRequest $request,
array $challenges);
final protected function newAutomaticControl(
PhabricatorAuthFactorResult $result) {
$is_answered = (bool)$result->getAnsweredChallenge();
if ($is_answered) {
return $this->newAnsweredControl($result);
}
$is_wait = $result->getIsWait();
if ($is_wait) {
return $this->newWaitControl($result);
}
return null;
}
private function newWaitControl(
PhabricatorAuthFactorResult $result) {
$error = $result->getErrorMessage();
$icon = id(new PHUIIconView())
->setIcon('fa-clock-o', 'red');
return id(new PHUIFormTimerControl())
->setIcon($icon)
->appendChild($error)
->setError(pht('Wait'));
}
private function newAnsweredControl(
PhabricatorAuthFactorResult $result) {
$icon = id(new PHUIIconView())
->setIcon('fa-check-circle-o', 'green');
return id(new PHUIFormTimerControl())
->setIcon($icon)
->appendChild(
pht('You responded to this challenge correctly.'));
}
}

View file

@ -0,0 +1,75 @@
<?php
final class PhabricatorAuthFactorResult
extends Phobject {
private $answeredChallenge;
private $isWait = false;
private $errorMessage;
private $value;
private $issuedChallenges = array();
public function setAnsweredChallenge(PhabricatorAuthChallenge $challenge) {
if (!$challenge->getIsAnsweredChallenge()) {
throw new PhutilInvalidStateException('markChallengeAsAnswered');
}
if ($challenge->getIsCompleted()) {
throw new Exception(
pht(
'A completed challenge was provided as an answered challenge. '.
'The underlying factor is implemented improperly, challenges '.
'may not be reused.'));
}
$this->answeredChallenge = $challenge;
return $this;
}
public function getAnsweredChallenge() {
return $this->answeredChallenge;
}
public function getIsValid() {
return (bool)$this->getAnsweredChallenge();
}
public function setIsWait($is_wait) {
$this->isWait = $is_wait;
return $this;
}
public function getIsWait() {
return $this->isWait;
}
public function setErrorMessage($error_message) {
$this->errorMessage = $error_message;
return $this;
}
public function getErrorMessage() {
return $this->errorMessage;
}
public function setValue($value) {
$this->value = $value;
return $this;
}
public function getValue() {
return $this->value;
}
public function setIssuedChallenges(array $issued_challenges) {
assert_instances_of($issued_challenges, 'PhabricatorAuthChallenge');
$this->issuedChallenges = $issued_challenges;
return $this;
}
public function getIssuedChallenges() {
return $this->issuedChallenges;
}
}

View file

@ -77,8 +77,8 @@ final class PhabricatorTOTPAuthFactor extends PhabricatorAuthFactor {
$e_code = true;
if ($request->getExists('totp')) {
$okay = self::verifyTOTPCode(
$user,
$okay = (bool)$this->getTimestepAtWhichResponseIsValid(
$this->getAllowedTimesteps($this->getCurrentTimestep()),
new PhutilOpaqueEnvelope($key),
$code);
@ -150,77 +150,229 @@ final class PhabricatorTOTPAuthFactor extends PhabricatorAuthFactor {
}
protected function newIssuedChallenges(
PhabricatorAuthFactorConfig $config,
PhabricatorUser $viewer,
array $challenges) {
$current_step = $this->getCurrentTimestep();
// If we already issued a valid challenge, don't issue a new one.
if ($challenges) {
return array();
}
// Otherwise, generate a new challenge for the current timestep and compute
// the TTL.
// When computing the TTL, note that we accept codes within a certain
// window of the challenge timestep to account for clock skew and users
// needing time to enter codes.
// We don't want this challenge to expire until after all valid responses
// to it are no longer valid responses to any other challenge we might
// issue in the future. If the challenge expires too quickly, we may issue
// a new challenge which can accept the same TOTP code response.
// This means that we need to keep this challenge alive for double the
// window size: if we're currently at timestep 3, the user might respond
// with the code for timestep 5. This is valid, since timestep 5 is within
// the window for timestep 3.
// But the code for timestep 5 can be used to respond at timesteps 3, 4, 5,
// 6, and 7. To prevent any valid response to this challenge from being
// used again, we need to keep this challenge active until timestep 8.
$window_size = $this->getTimestepWindowSize();
$step_duration = $this->getTimestepDuration();
$ttl_steps = ($window_size * 2) + 1;
$ttl_seconds = ($ttl_steps * $step_duration);
return array(
$this->newChallenge($config, $viewer)
->setChallengeKey($current_step)
->setChallengeTTL(PhabricatorTime::getNow() + $ttl_seconds),
);
}
public function renderValidateFactorForm(
PhabricatorAuthFactorConfig $config,
AphrontFormView $form,
PhabricatorUser $viewer,
$validation_result) {
PhabricatorAuthFactorResult $result) {
if (!$validation_result) {
$validation_result = array();
$control = $this->newAutomaticControl($result);
if (!$control) {
$value = $result->getValue();
$error = $result->getErrorMessage();
$name = $this->getChallengeResponseParameterName($config);
$control = id(new PHUIFormNumberControl())
->setName($name)
->setDisableAutocomplete(true)
->setValue($value)
->setError($error);
}
$form->appendChild(
id(new PHUIFormNumberControl())
->setName($this->getParameterName($config, 'totpcode'))
->setLabel(pht('App Code'))
->setDisableAutocomplete(true)
->setCaption(pht('Factor Name: %s', $config->getFactorName()))
->setValue(idx($validation_result, 'value'))
->setError(idx($validation_result, 'error', true)));
$control
->setLabel(pht('App Code'))
->setCaption(pht('Factor Name: %s', $config->getFactorName()));
$form->appendChild($control);
}
public function processValidateFactorForm(
public function getRequestHasChallengeResponse(
PhabricatorAuthFactorConfig $config,
PhabricatorUser $viewer,
AphrontRequest $request) {
$code = $request->getStr($this->getParameterName($config, 'totpcode'));
$key = new PhutilOpaqueEnvelope($config->getFactorSecret());
if (self::verifyTOTPCode($viewer, $key, $code)) {
return array(
'error' => null,
'value' => $code,
'valid' => true,
);
} else {
return array(
'error' => strlen($code) ? pht('Invalid') : pht('Required'),
'value' => $code,
'valid' => false,
);
}
$value = $this->getChallengeResponseFromRequest($config, $request);
return (bool)strlen($value);
}
protected function newResultFromIssuedChallenges(
PhabricatorAuthFactorConfig $config,
PhabricatorUser $viewer,
array $challenges) {
// If we've already issued a challenge at the current timestep or any
// nearby timestep, require that it was issued to the current session.
// This is defusing attacks where you (broadly) look at someone's phone
// and type the code in more quickly than they do.
$session_phid = $viewer->getSession()->getPHID();
$now = PhabricatorTime::getNow();
$engine = $config->getSessionEngine();
$workflow_key = $engine->getWorkflowKey();
$current_timestep = $this->getCurrentTimestep();
foreach ($challenges as $challenge) {
$challenge_timestep = (int)$challenge->getChallengeKey();
$wait_duration = ($challenge->getChallengeTTL() - $now) + 1;
if ($challenge->getSessionPHID() !== $session_phid) {
return $this->newResult()
->setIsWait(true)
->setErrorMessage(
pht(
'This factor recently issued a challenge to a different login '.
'session. Wait %s second(s) for the code to cycle, then try '.
'again.',
new PhutilNumber($wait_duration)));
}
if ($challenge->getWorkflowKey() !== $workflow_key) {
return $this->newResult()
->setIsWait(true)
->setErrorMessage(
pht(
'This factor recently issued a challenge for a different '.
'workflow. Wait %s second(s) for the code to cycle, then try '.
'again.',
new PhutilNumber($wait_duration)));
}
// If the current realtime timestep isn't a valid response to the current
// challenge but the challenge hasn't expired yet, we're locking out
// the factor to prevent challenge windows from overlapping. Let the user
// know that they should wait for a new challenge.
$challenge_timesteps = $this->getAllowedTimesteps($challenge_timestep);
if (!isset($challenge_timesteps[$current_timestep])) {
return $this->newResult()
->setIsWait(true)
->setErrorMessage(
pht(
'This factor recently issued a challenge which has expired. '.
'A new challenge can not be issued yet. Wait %s second(s) for '.
'the code to cycle, then try again.',
new PhutilNumber($wait_duration)));
}
if ($challenge->getIsReusedChallenge()) {
return $this->newResult()
->setIsWait(true)
->setErrorMessage(
pht(
'You recently provided a response to this factor. Responses '.
'may not be reused. Wait %s second(s) for the code to cycle, '.
'then try again.',
new PhutilNumber($wait_duration)));
}
}
return null;
}
protected function newResultFromChallengeResponse(
PhabricatorAuthFactorConfig $config,
PhabricatorUser $viewer,
AphrontRequest $request,
array $challenges) {
$code = $this->getChallengeResponseFromRequest(
$config,
$request);
$result = $this->newResult()
->setValue($code);
// We expect to reach TOTP validation with exactly one valid challenge.
if (count($challenges) !== 1) {
throw new Exception(
pht(
'Reached TOTP challenge validation with an unexpected number of '.
'unexpired challenges (%d), expected exactly one.',
phutil_count($challenges)));
}
$challenge = head($challenges);
// If the client has already provided a valid answer to this challenge and
// submitted a token proving they answered it, we're all set.
if ($challenge->getIsAnsweredChallenge()) {
return $result->setAnsweredChallenge($challenge);
}
$challenge_timestep = (int)$challenge->getChallengeKey();
$current_timestep = $this->getCurrentTimestep();
$challenge_timesteps = $this->getAllowedTimesteps($challenge_timestep);
$current_timesteps = $this->getAllowedTimesteps($current_timestep);
// We require responses be both valid for the challenge and valid for the
// current timestep. A longer challenge TTL doesn't let you use older
// codes for a longer period of time.
$valid_timestep = $this->getTimestepAtWhichResponseIsValid(
array_intersect_key($challenge_timesteps, $current_timesteps),
new PhutilOpaqueEnvelope($config->getFactorSecret()),
$code);
if ($valid_timestep) {
$ttl = PhabricatorTime::getNow() + 60;
$challenge
->setProperty('totp.timestep', $valid_timestep)
->markChallengeAsAnswered($ttl);
$result->setAnsweredChallenge($challenge);
} else {
if (strlen($code)) {
$error_message = pht('Invalid');
} else {
$error_message = pht('Required');
}
$result->setErrorMessage($error_message);
}
return $result;
}
public static function generateNewTOTPKey() {
return strtoupper(Filesystem::readRandomCharacters(32));
}
public static function verifyTOTPCode(
PhabricatorUser $user,
PhutilOpaqueEnvelope $key,
$code) {
$now = (int)(time() / 30);
// Allow the user to enter a code a few minutes away on either side, in
// case the server or client has some clock skew.
for ($offset = -2; $offset <= 2; $offset++) {
$real = self::getTOTPCode($key, $now + $offset);
if (phutil_hashes_are_identical($real, $code)) {
return true;
}
}
// TODO: After validating a code, this should mark it as used and prevent
// it from being reused.
return false;
}
public static function base32Decode($buf) {
$buf = strtoupper($buf);
@ -313,4 +465,58 @@ final class PhabricatorTOTPAuthFactor extends PhabricatorAuthFactor {
$rows);
}
private function getTimestepDuration() {
return 30;
}
private function getCurrentTimestep() {
$duration = $this->getTimestepDuration();
return (int)(PhabricatorTime::getNow() / $duration);
}
private function getAllowedTimesteps($at_timestep) {
$window = $this->getTimestepWindowSize();
$range = range($at_timestep - $window, $at_timestep + $window);
return array_fuse($range);
}
private function getTimestepWindowSize() {
// The user is allowed to provide a code from the recent past or the
// near future to account for minor clock skew between the client
// and server, and the time it takes to actually enter a code.
return 1;
}
private function getTimestepAtWhichResponseIsValid(
array $timesteps,
PhutilOpaqueEnvelope $key,
$code) {
foreach ($timesteps as $timestep) {
$expect_code = self::getTOTPCode($key, $timestep);
if (phutil_hashes_are_identical($code, $expect_code)) {
return $timestep;
}
}
return null;
}
private function getChallengeResponseParameterName(
PhabricatorAuthFactorConfig $config) {
return $this->getParameterName($config, 'totpcode');
}
private function getChallengeResponseFromRequest(
PhabricatorAuthFactorConfig $config,
AphrontRequest $request) {
$name = $this->getChallengeResponseParameterName($config);
$value = $request->getStr($name);
$value = (string)$value;
$value = trim($value);
return $value;
}
}

View file

@ -0,0 +1,28 @@
<?php
final class PhabricatorAuthChallengeGarbageCollector
extends PhabricatorGarbageCollector {
const COLLECTORCONST = 'auth.challenges';
public function getCollectorName() {
return pht('Authentication Challenges');
}
public function hasAutomaticPolicy() {
return true;
}
protected function collectGarbage() {
$challenge_table = new PhabricatorAuthChallenge();
$conn = $challenge_table->establishConnection('w');
queryfx(
$conn,
'DELETE FROM %R WHERE challengeTTL < UNIX_TIMESTAMP() LIMIT 100',
$challenge_table);
return ($conn->getAffectedRows() == 100);
}
}

View file

@ -13,7 +13,13 @@ final class PhabricatorAuthManagementRecoverWorkflow
'of Phabricator.'))
->setArguments(
array(
'username' => array(
array(
'name' => 'force-full-session',
'help' => pht(
'Recover directly into a full session without requiring MFA '.
'or other login checks.'),
),
array(
'name' => 'username',
'wildcard' => true,
),
@ -54,11 +60,14 @@ final class PhabricatorAuthManagementRecoverWorkflow
$username));
}
$force_full_session = $args->getArg('force-full-session');
$engine = new PhabricatorAuthSessionEngine();
$onetime_uri = $engine->getOneTimeLoginURI(
$user,
null,
PhabricatorAuthSessionEngine::ONETIME_RECOVER);
PhabricatorAuthSessionEngine::ONETIME_RECOVER,
$force_full_session);
$console = PhutilConsole::getConsole();
$console->writeOut(

View file

@ -0,0 +1,32 @@
<?php
final class PhabricatorAuthChallengePHIDType extends PhabricatorPHIDType {
const TYPECONST = 'CHAL';
public function getTypeName() {
return pht('Auth Challenge');
}
public function newObject() {
return new PhabricatorAuthChallenge();
}
public function getPHIDTypeApplicationClass() {
return 'PhabricatorAuthApplication';
}
protected function buildQueryForObjects(
PhabricatorObjectQuery $query,
array $phids) {
return new PhabricatorAuthChallengeQuery();
}
public function loadHandles(
PhabricatorHandleQuery $query,
array $handles,
array $objects) {
return;
}
}

View file

@ -0,0 +1,99 @@
<?php
final class PhabricatorAuthChallengeQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $ids;
private $phids;
private $userPHIDs;
private $factorPHIDs;
private $challengeTTLMin;
private $challengeTTLMax;
public function withIDs(array $ids) {
$this->ids = $ids;
return $this;
}
public function withPHIDs(array $phids) {
$this->phids = $phids;
return $this;
}
public function withUserPHIDs(array $user_phids) {
$this->userPHIDs = $user_phids;
return $this;
}
public function withFactorPHIDs(array $factor_phids) {
$this->factorPHIDs = $factor_phids;
return $this;
}
public function withChallengeTTLBetween($challenge_min, $challenge_max) {
$this->challengeTTLMin = $challenge_min;
$this->challengeTTLMax = $challenge_max;
return $this;
}
public function newResultObject() {
return new PhabricatorAuthChallenge();
}
protected function loadPage() {
return $this->loadStandardPage($this->newResultObject());
}
protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
$where = parent::buildWhereClauseParts($conn);
if ($this->ids !== null) {
$where[] = qsprintf(
$conn,
'id IN (%Ld)',
$this->ids);
}
if ($this->phids !== null) {
$where[] = qsprintf(
$conn,
'phid IN (%Ls)',
$this->phids);
}
if ($this->userPHIDs !== null) {
$where[] = qsprintf(
$conn,
'userPHID IN (%Ls)',
$this->userPHIDs);
}
if ($this->factorPHIDs !== null) {
$where[] = qsprintf(
$conn,
'factorPHID IN (%Ls)',
$this->factorPHIDs);
}
if ($this->challengeTTLMin !== null) {
$where[] = qsprintf(
$conn,
'challengeTTL >= %d',
$this->challengeTTLMin);
}
if ($this->challengeTTLMax !== null) {
$where[] = qsprintf(
$conn,
'challengeTTL <= %d',
$this->challengeTTLMax);
}
return $where;
}
public function getQueryApplicationClass() {
return 'PhabricatorAuthApplication';
}
}

View file

@ -0,0 +1,256 @@
<?php
final class PhabricatorAuthChallenge
extends PhabricatorAuthDAO
implements PhabricatorPolicyInterface {
protected $userPHID;
protected $factorPHID;
protected $sessionPHID;
protected $workflowKey;
protected $challengeKey;
protected $challengeTTL;
protected $responseDigest;
protected $responseTTL;
protected $isCompleted;
protected $properties = array();
private $responseToken;
const HTTPKEY = '__hisec.challenges__';
const TOKEN_DIGEST_KEY = 'auth.challenge.token';
public static function initializeNewChallenge() {
return id(new self())
->setIsCompleted(0);
}
public static function newHTTPParametersFromChallenges(array $challenges) {
assert_instances_of($challenges, __CLASS__);
$token_list = array();
foreach ($challenges as $challenge) {
$token = $challenge->getResponseToken();
if ($token) {
$token_list[] = sprintf(
'%s:%s',
$challenge->getPHID(),
$token->openEnvelope());
}
}
if (!$token_list) {
return array();
}
$token_list = implode(' ', $token_list);
return array(
self::HTTPKEY => $token_list,
);
}
public static function newChallengeResponsesFromRequest(
array $challenges,
AphrontRequest $request) {
assert_instances_of($challenges, __CLASS__);
$token_list = $request->getStr(self::HTTPKEY);
$token_list = explode(' ', $token_list);
$token_map = array();
foreach ($token_list as $token_element) {
$token_element = trim($token_element, ' ');
if (!strlen($token_element)) {
continue;
}
// NOTE: This error message is intentionally not printing the token to
// avoid disclosing it. As a result, it isn't terribly useful, but no
// normal user should ever end up here.
if (!preg_match('/^[^:]+:/', $token_element)) {
throw new Exception(
pht(
'This request included an improperly formatted MFA challenge '.
'token and can not be processed.'));
}
list($phid, $token) = explode(':', $token_element, 2);
if (isset($token_map[$phid])) {
throw new Exception(
pht(
'This request improperly specifies an MFA challenge token ("%s") '.
'multiple times and can not be processed.',
$phid));
}
$token_map[$phid] = new PhutilOpaqueEnvelope($token);
}
$challenges = mpull($challenges, null, 'getPHID');
$now = PhabricatorTime::getNow();
foreach ($challenges as $challenge_phid => $challenge) {
// If the response window has expired, don't attach the token.
if ($challenge->getResponseTTL() < $now) {
continue;
}
$token = idx($token_map, $challenge_phid);
if (!$token) {
continue;
}
$challenge->setResponseToken($token);
}
}
protected function getConfiguration() {
return array(
self::CONFIG_SERIALIZATION => array(
'properties' => self::SERIALIZATION_JSON,
),
self::CONFIG_AUX_PHID => true,
self::CONFIG_COLUMN_SCHEMA => array(
'challengeKey' => 'text255',
'challengeTTL' => 'epoch',
'workflowKey' => 'text255',
'responseDigest' => 'text255?',
'responseTTL' => 'epoch?',
'isCompleted' => 'bool',
),
self::CONFIG_KEY_SCHEMA => array(
'key_issued' => array(
'columns' => array('userPHID', 'challengeTTL'),
),
'key_collection' => array(
'columns' => array('challengeTTL'),
),
),
) + parent::getConfiguration();
}
public function getPHIDType() {
return PhabricatorAuthChallengePHIDType::TYPECONST;
}
public function getIsReusedChallenge() {
if ($this->getIsCompleted()) {
return true;
}
if (!$this->getIsAnsweredChallenge()) {
return false;
}
// If the challenge has been answered but the client has provided a token
// proving that they answered it, this is still a valid response.
if ($this->getResponseToken()) {
return false;
}
return true;
}
public function getIsAnsweredChallenge() {
return (bool)$this->getResponseDigest();
}
public function markChallengeAsAnswered($ttl) {
$token = Filesystem::readRandomCharacters(32);
$token = new PhutilOpaqueEnvelope($token);
return $this
->setResponseToken($token)
->setResponseTTL($ttl)
->save();
}
public function markChallengeAsCompleted() {
return $this
->setIsCompleted(true)
->save();
}
public function setResponseToken(PhutilOpaqueEnvelope $token) {
if (!$this->getUserPHID()) {
throw new PhutilInvalidStateException('setUserPHID');
}
if ($this->responseToken) {
throw new Exception(
pht(
'This challenge already has a response token; you can not '.
'set a new response token.'));
}
if (preg_match('/ /', $token->openEnvelope())) {
throw new Exception(
pht(
'The response token for this challenge is invalid: response '.
'tokens may not include spaces.'));
}
$digest = PhabricatorHash::digestWithNamedKey(
$token->openEnvelope(),
self::TOKEN_DIGEST_KEY);
if ($this->responseDigest !== null) {
if (!phutil_hashes_are_identical($digest, $this->responseDigest)) {
throw new Exception(
pht(
'Invalid response token for this challenge: token digest does '.
'not match stored digest.'));
}
} else {
$this->responseDigest = $digest;
}
$this->responseToken = $token;
return $this;
}
public function getResponseToken() {
return $this->responseToken;
}
public function setResponseDigest($value) {
throw new Exception(
pht(
'You can not set the response digest for a challenge directly. '.
'Instead, set a response token. A response digest will be computed '.
'automatically.'));
}
public function setProperty($key, $value) {
$this->properties[$key] = $value;
return $this;
}
public function getProperty($key, $default = null) {
return $this->properties[$key];
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
);
}
public function getPolicy($capability) {
return PhabricatorPolicies::POLICY_NOONE;
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return ($viewer->getPHID() === $this->getUserPHID());
}
}

View file

@ -8,6 +8,8 @@ final class PhabricatorAuthFactorConfig extends PhabricatorAuthDAO {
protected $factorSecret;
protected $properties = array();
private $sessionEngine;
protected function getConfiguration() {
return array(
self::CONFIG_SERIALIZATION => array(
@ -49,4 +51,17 @@ final class PhabricatorAuthFactorConfig extends PhabricatorAuthDAO {
return $impl;
}
public function setSessionEngine(PhabricatorAuthSessionEngine $engine) {
$this->sessionEngine = $engine;
return $this;
}
public function getSessionEngine() {
if (!$this->sessionEngine) {
throw new PhutilInvalidStateException('setSessionEngine');
}
return $this->sessionEngine;
}
}

View file

@ -217,20 +217,8 @@ final class PhabricatorAuthPassword
return new PhabricatorAuthPasswordEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new PhabricatorAuthPasswordTransaction();
}
public function willRenderTimeline(
PhabricatorApplicationTransactionView $timeline,
AphrontRequest $request) {
return $timeline;
}
}

View file

@ -91,21 +91,10 @@ final class PhabricatorAuthProviderConfig
return new PhabricatorAuthProviderConfigEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new PhabricatorAuthProviderConfigTransaction();
}
public function willRenderTimeline(
PhabricatorApplicationTransactionView $timeline,
AphrontRequest $request) {
return $timeline;
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */

View file

@ -159,18 +159,8 @@ final class PhabricatorAuthSSHKey
return new PhabricatorAuthSSHKeyEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new PhabricatorAuthSSHKeyTransaction();
}
public function willRenderTimeline(
PhabricatorApplicationTransactionView $timeline,
AphrontRequest $request) {
return $timeline;
}
}

View file

@ -72,10 +72,14 @@ final class PhabricatorAuthSession extends PhabricatorAuthDAO
return $this->assertAttached($this->identityObject);
}
public static function getSessionTypeTTL($session_type) {
public static function getSessionTypeTTL($session_type, $is_partial) {
switch ($session_type) {
case self::TYPE_WEB:
return phutil_units('30 days in seconds');
if ($is_partial) {
return phutil_units('30 minutes in seconds');
} else {
return phutil_units('30 days in seconds');
}
case self::TYPE_CONDUIT:
return phutil_units('24 hours in seconds');
default:

View file

@ -106,6 +106,16 @@ final class PhabricatorAuthTemporaryToken extends PhabricatorAuthDAO
return $this;
}
public function setShouldForceFullSession($force_full) {
return $this->setTemporaryTokenProperty('force-full-session', $force_full);
}
public function getShouldForceFullSession() {
return $this->getTemporaryTokenProperty('force-full-session', false);
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */

View file

@ -51,6 +51,7 @@ final class PhabricatorBadgesCommentController
if ($request->isAjax() && $is_preview) {
return id(new PhabricatorApplicationTransactionResponse())
->setObject($badge)
->setViewer($viewer)
->setTransactions($xactions)
->setIsPreview($is_preview);

View file

@ -125,21 +125,10 @@ final class PhabricatorBadgesBadge extends PhabricatorBadgesDAO
return new PhabricatorBadgesEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new PhabricatorBadgesTransaction();
}
public function willRenderTimeline(
PhabricatorApplicationTransactionView $timeline,
AphrontRequest $request) {
return $timeline;
}
/* -( PhabricatorSubscribableInterface )----------------------------------- */

View file

@ -649,18 +649,8 @@ abstract class PhabricatorApplication
return new PhabricatorApplicationEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new PhabricatorApplicationApplicationTransaction();
}
public function willRenderTimeline(
PhabricatorApplicationTransactionView $timeline,
AphrontRequest $request) {
return $timeline;
}
}

View file

@ -482,14 +482,14 @@ abstract class PhabricatorController extends AphrontController {
PhabricatorApplicationTransactionInterface $object,
PhabricatorApplicationTransactionQuery $query,
PhabricatorMarkupEngine $engine = null,
$render_data = array()) {
$view_data = array()) {
$viewer = $this->getRequest()->getUser();
$request = $this->getRequest();
$viewer = $this->getViewer();
$xaction = $object->getApplicationTransactionTemplate();
$view = $xaction->getApplicationTransactionViewObject();
$pager = id(new AphrontCursorPagerView())
->readFromRequest($this->getRequest())
->readFromRequest($request)
->setURI(new PhutilURI(
'/transactions/showolder/'.$object->getPHID().'/'));
@ -500,6 +500,13 @@ abstract class PhabricatorController extends AphrontController {
->executeWithCursorPager($pager);
$xactions = array_reverse($xactions);
$timeline_engine = PhabricatorTimelineEngine::newForObject($object)
->setViewer($viewer)
->setTransactions($xactions)
->setViewData($view_data);
$view = $timeline_engine->buildTimelineView();
if ($engine) {
foreach ($xactions as $xaction) {
if ($xaction->getComment()) {
@ -513,14 +520,9 @@ abstract class PhabricatorController extends AphrontController {
}
$timeline = $view
->setUser($viewer)
->setObjectPHID($object->getPHID())
->setTransactions($xactions)
->setPager($pager)
->setRenderData($render_data)
->setQuoteTargetID($this->getRequest()->getStr('quoteTargetID'))
->setQuoteRef($this->getRequest()->getStr('quoteRef'));
$object->willRenderTimeline($timeline, $this->getRequest());
return $timeline;
}

View file

@ -1311,20 +1311,10 @@ final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO
return new PhabricatorCalendarEventEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new PhabricatorCalendarEventTransaction();
}
public function willRenderTimeline(
PhabricatorApplicationTransactionView $timeline,
AphrontRequest $request) {
return $timeline;
}
/* -( PhabricatorSubscribableInterface )----------------------------------- */

View file

@ -166,21 +166,10 @@ final class PhabricatorCalendarExport extends PhabricatorCalendarDAO
return new PhabricatorCalendarExportEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new PhabricatorCalendarExportTransaction();
}
public function willRenderTimeline(
PhabricatorApplicationTransactionView $timeline,
AphrontRequest $request) {
return $timeline;
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */

View file

@ -140,20 +140,10 @@ final class PhabricatorCalendarImport
return new PhabricatorCalendarImportEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new PhabricatorCalendarImportTransaction();
}
public function willRenderTimeline(
PhabricatorApplicationTransactionView $timeline,
AphrontRequest $request) {
return $timeline;
}
public function newLogMessage($type, array $parameters) {
$parameters = array(
'type' => $type,

View file

@ -16,18 +16,14 @@ final class PhabricatorConfigHistoryController
$xaction = $object->getApplicationTransactionTemplate();
$view = $xaction->getApplicationTransactionViewObject();
$timeline = $view
->setUser($viewer)
$timeline = id(new PhabricatorApplicationTransactionView())
->setViewer($viewer)
->setTransactions($xactions)
->setRenderAsFeed(true)
->setObjectPHID(PhabricatorPHIDConstants::PHID_VOID);
$timeline->setShouldTerminate(true);
$object->willRenderTimeline($timeline, $this->getRequest());
$title = pht('Settings History');
$header = $this->buildHeaderView($title);

View file

@ -33,7 +33,7 @@ final class PhabricatorAuthenticationConfigOptions
pht(
'If true, email addresses must be verified (by clicking a link '.
'in an email) before a user can login. By default, verification '.
'is optional unless {{auth.email-domains}} is nonempty.')),
'is optional unless @{config:auth.email-domains} is nonempty.')),
$this->newOption('auth.require-approval', 'bool', true)
->setBoolOptions(
array(
@ -55,7 +55,7 @@ final class PhabricatorAuthenticationConfigOptions
"registration, you can disable the queue to reduce administrative ".
"overhead.\n\n".
"NOTE: Before you disable the queue, make sure ".
"{{auth.email-domains}} is configured correctly ".
"@{config:auth.email-domains} is configured correctly ".
"for your install!")),
$this->newOption('auth.email-domains', 'list<string>', array())
->setSummary(pht('Only allow registration from particular domains.'))
@ -66,7 +66,7 @@ final class PhabricatorAuthenticationConfigOptions
"here.\n\nUsers will only be allowed to register using email ".
"addresses at one of the domains, and will only be able to add ".
"new email addresses for these domains. If you configure this, ".
"it implies {{auth.require-email-verification}}.\n\n".
"it implies @{config:auth.require-email-verification}.\n\n".
"You should omit the `@` from domains. Note that the domain must ".
"match exactly. If you allow `yourcompany.com`, that permits ".
"`joe@yourcompany.com` but rejects `joe@mail.yourcompany.com`."))

View file

@ -209,12 +209,6 @@ final class PhabricatorConfigOption
return null;
}
// TODO: Some day, we should probably implement this as a real rule.
$description = preg_replace(
'/{{([^}]+)}}/',
'[[/config/edit/\\1/ | \\1]]',
$description);
return new PHUIRemarkupView($viewer, $description);
}

View file

@ -61,21 +61,10 @@ final class PhabricatorConfigEntry
return new PhabricatorConfigEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new PhabricatorConfigTransaction();
}
public function willRenderTimeline(
PhabricatorApplicationTransactionView $timeline,
AphrontRequest $request) {
return $timeline;
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */

View file

@ -311,19 +311,10 @@ final class ConpherenceThread extends ConpherenceDAO
return new ConpherenceEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new ConpherenceTransaction();
}
public function willRenderTimeline(
PhabricatorApplicationTransactionView $timeline,
AphrontRequest $request) {
return $timeline;
}
/* -( PhabricatorNgramInterface )------------------------------------------ */

View file

@ -95,20 +95,10 @@ final class PhabricatorCountdown extends PhabricatorCountdownDAO
return new PhabricatorCountdownEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new PhabricatorCountdownTransaction();
}
public function willRenderTimeline(
PhabricatorApplicationTransactionView $timeline,
AphrontRequest $request) {
return $timeline;
}
/* -( PhabricatorTokenReceiverInterface )---------------------------------- */

View file

@ -130,21 +130,10 @@ final class PhabricatorDashboard extends PhabricatorDashboardDAO
return new PhabricatorDashboardTransactionEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new PhabricatorDashboardTransaction();
}
public function willRenderTimeline(
PhabricatorApplicationTransactionView $timeline,
AphrontRequest $request) {
return $timeline;
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */

View file

@ -113,21 +113,10 @@ final class PhabricatorDashboardPanel
return new PhabricatorDashboardPanelTransactionEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new PhabricatorDashboardPanelTransaction();
}
public function willRenderTimeline(
PhabricatorApplicationTransactionView $timeline,
AphrontRequest $request) {
return $timeline;
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */

View file

@ -0,0 +1,78 @@
<?php
final class DifferentialRevisionTimelineEngine
extends PhabricatorTimelineEngine {
protected function newTimelineView() {
$viewer = $this->getViewer();
$xactions = $this->getTransactions();
$revision = $this->getObject();
$view_data = $this->getViewData();
if (!$view_data) {
$view_data = array();
}
$left = idx($view_data, 'left');
$right = idx($view_data, 'right');
$diffs = id(new DifferentialDiffQuery())
->setViewer($viewer)
->withIDs(array($left, $right))
->execute();
$diffs = mpull($diffs, null, 'getID');
$left_diff = $diffs[$left];
$right_diff = $diffs[$right];
$old_ids = idx($view_data, 'old');
$new_ids = idx($view_data, 'new');
$old_ids = array_filter(explode(',', $old_ids));
$new_ids = array_filter(explode(',', $new_ids));
$type_inline = DifferentialTransaction::TYPE_INLINE;
$changeset_ids = array_merge($old_ids, $new_ids);
$inlines = array();
foreach ($xactions as $xaction) {
if ($xaction->getTransactionType() == $type_inline) {
$inlines[] = $xaction->getComment();
$changeset_ids[] = $xaction->getComment()->getChangesetID();
}
}
if ($changeset_ids) {
$changesets = id(new DifferentialChangesetQuery())
->setViewer($viewer)
->withIDs($changeset_ids)
->execute();
$changesets = mpull($changesets, null, 'getID');
} else {
$changesets = array();
}
foreach ($inlines as $key => $inline) {
$inlines[$key] = DifferentialInlineComment::newFromModernComment(
$inline);
}
$query = id(new DifferentialInlineCommentQuery())
->needHidden(true)
->setViewer($viewer);
// NOTE: This is a bit sketchy: this method adjusts the inlines as a
// side effect, which means it will ultimately adjust the transaction
// comments and affect timeline rendering.
$query->adjustInlinesForChangesets(
$inlines,
array_select_keys($changesets, $old_ids),
array_select_keys($changesets, $new_ids),
$revision);
return id(new DifferentialTransactionView())
->setViewData($view_data)
->setChangesets($changesets)
->setRevision($revision)
->setLeftDiff($left_diff)
->setRightDiff($right_diff);
}
}

View file

@ -699,21 +699,10 @@ final class DifferentialDiff
return new DifferentialDiffEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new DifferentialDiffTransaction();
}
public function willRenderTimeline(
PhabricatorApplicationTransactionView $timeline,
AphrontRequest $request) {
return $timeline;
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */

View file

@ -11,6 +11,7 @@ final class DifferentialRevision extends DifferentialDAO
PhabricatorSubscribableInterface,
PhabricatorCustomFieldInterface,
PhabricatorApplicationTransactionInterface,
PhabricatorTimelineInterface,
PhabricatorMentionableInterface,
PhabricatorDestructibleInterface,
PhabricatorProjectInterface,
@ -990,81 +991,10 @@ final class DifferentialRevision extends DifferentialDAO
return new DifferentialTransactionEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new DifferentialTransaction();
}
public function willRenderTimeline(
PhabricatorApplicationTransactionView $timeline,
AphrontRequest $request) {
$viewer = $request->getViewer();
$render_data = $timeline->getRenderData();
$left = $request->getInt('left', idx($render_data, 'left'));
$right = $request->getInt('right', idx($render_data, 'right'));
$diffs = id(new DifferentialDiffQuery())
->setViewer($request->getUser())
->withIDs(array($left, $right))
->execute();
$diffs = mpull($diffs, null, 'getID');
$left_diff = $diffs[$left];
$right_diff = $diffs[$right];
$old_ids = $request->getStr('old', idx($render_data, 'old'));
$new_ids = $request->getStr('new', idx($render_data, 'new'));
$old_ids = array_filter(explode(',', $old_ids));
$new_ids = array_filter(explode(',', $new_ids));
$type_inline = DifferentialTransaction::TYPE_INLINE;
$changeset_ids = array_merge($old_ids, $new_ids);
$inlines = array();
foreach ($timeline->getTransactions() as $xaction) {
if ($xaction->getTransactionType() == $type_inline) {
$inlines[] = $xaction->getComment();
$changeset_ids[] = $xaction->getComment()->getChangesetID();
}
}
if ($changeset_ids) {
$changesets = id(new DifferentialChangesetQuery())
->setViewer($request->getUser())
->withIDs($changeset_ids)
->execute();
$changesets = mpull($changesets, null, 'getID');
} else {
$changesets = array();
}
foreach ($inlines as $key => $inline) {
$inlines[$key] = DifferentialInlineComment::newFromModernComment(
$inline);
}
$query = id(new DifferentialInlineCommentQuery())
->needHidden(true)
->setViewer($viewer);
// NOTE: This is a bit sketchy: this method adjusts the inlines as a
// side effect, which means it will ultimately adjust the transaction
// comments and affect timeline rendering.
$query->adjustInlinesForChangesets(
$inlines,
array_select_keys($changesets, $old_ids),
array_select_keys($changesets, $new_ids),
$this);
return $timeline
->setChangesets($changesets)
->setRevision($this)
->setLeftDiff($left_diff)
->setRightDiff($right_diff);
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
@ -1206,4 +1136,13 @@ final class DifferentialRevision extends DifferentialDAO
return new DifferentialRevisionDraftEngine();
}
/* -( PhabricatorTimelineInterface )--------------------------------------- */
public function newTimelineEngine() {
return new DifferentialRevisionTimelineEngine();
}
}

View file

@ -65,10 +65,6 @@ final class DifferentialTransaction
return new DifferentialTransactionComment();
}
public function getApplicationTransactionViewObject() {
return new DifferentialTransactionView();
}
public function shouldHide() {
$old = $this->getOldValue();
$new = $this->getNewValue();

View file

@ -740,8 +740,6 @@ final class DiffusionCommitController extends DiffusionController {
$commit,
new PhabricatorAuditTransactionQuery());
$commit->willRenderTimeline($timeline, $this->getRequest());
$timeline->setQuoteRef($commit->getMonogram());
return $timeline;

View file

@ -0,0 +1,30 @@
<?php
final class DiffusionCommitTimelineEngine
extends PhabricatorTimelineEngine {
protected function newTimelineView() {
$xactions = $this->getTransactions();
$path_ids = array();
foreach ($xactions as $xaction) {
if ($xaction->hasComment()) {
$path_id = $xaction->getComment()->getPathID();
if ($path_id) {
$path_ids[] = $path_id;
}
}
}
$path_map = array();
if ($path_ids) {
$path_map = id(new DiffusionPathQuery())
->withPathIDs($path_ids)
->execute();
$path_map = ipull($path_map, 'path', 'id');
}
return id(new PhabricatorAuditTransactionView())
->setPathMap($path_map);
}
}

View file

@ -143,21 +143,10 @@ final class DivinerLiveBook extends DivinerDAO
return new DivinerLiveBookEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new DivinerLiveBookTransaction();
}
public function willRenderTimeline(
PhabricatorApplicationTransactionView $timeline,
AphrontRequest $request) {
return $timeline;
}
/* -( PhabricatorFulltextInterface )--------------------------------------- */

View file

@ -295,21 +295,10 @@ final class DrydockBlueprint extends DrydockDAO
return new DrydockBlueprintEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new DrydockBlueprintTransaction();
}
public function willRenderTimeline(
PhabricatorApplicationTransactionView $timeline,
AphrontRequest $request) {
return $timeline;
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */

View file

@ -1544,21 +1544,10 @@ final class PhabricatorFile extends PhabricatorFileDAO
return new PhabricatorFileEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new PhabricatorFileTransaction();
}
public function willRenderTimeline(
PhabricatorApplicationTransactionView $timeline,
AphrontRequest $request) {
return $timeline;
}
/* -( PhabricatorPolicyInterface Implementation )-------------------------- */

View file

@ -110,19 +110,8 @@ final class FundBacker extends FundDAO
return new FundBackerEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new FundBackerTransaction();
}
public function willRenderTimeline(
PhabricatorApplicationTransactionView $timeline,
AphrontRequest $request) {
return $timeline;
}
}

View file

@ -160,21 +160,10 @@ final class FundInitiative extends FundDAO
return new FundInitiativeEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new FundInitiativeTransaction();
}
public function willRenderTimeline(
PhabricatorApplicationTransactionView $timeline,
AphrontRequest $request) {
return $timeline;
}
/* -( PhabricatorSubscribableInterface )----------------------------------- */

View file

@ -96,9 +96,7 @@ abstract class HarbormasterBuildableEngine
$publishable = $this->getPublishableObject();
$editor = $this->newEditor();
$editor->applyTransactions(
$publishable->getApplicationTransactionObject(),
$xactions);
$editor->applyTransactions($publishable, $xactions);
}
public function getAuthorIdentity() {

View file

@ -283,21 +283,10 @@ final class HarbormasterBuildable
return new HarbormasterBuildableTransactionEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new HarbormasterBuildableTransaction();
}
public function willRenderTimeline(
PhabricatorApplicationTransactionView $timeline,
AphrontRequest $request) {
return $timeline;
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */

View file

@ -393,21 +393,10 @@ final class HarbormasterBuild extends HarbormasterDAO
return new HarbormasterBuildTransactionEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new HarbormasterBuildTransaction();
}
public function willRenderTimeline(
PhabricatorApplicationTransactionView $timeline,
AphrontRequest $request) {
return $timeline;
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */

View file

@ -136,22 +136,10 @@ final class HarbormasterBuildPlan extends HarbormasterDAO
return new HarbormasterBuildPlanEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new HarbormasterBuildPlanTransaction();
}
public function willRenderTimeline(
PhabricatorApplicationTransactionView $timeline,
AphrontRequest $request) {
return $timeline;
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */

View file

@ -121,21 +121,10 @@ final class HarbormasterBuildStep extends HarbormasterDAO
return new HarbormasterBuildStepEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new HarbormasterBuildStepTransaction();
}
public function willRenderTimeline(
PhabricatorApplicationTransactionView $timeline,
AphrontRequest $request) {
return $timeline;
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */

View file

@ -50,6 +50,19 @@ final class HeraldWebhookViewController
->setLimit(20)
->execute();
$warnings = array();
if (PhabricatorEnv::getEnvConfig('phabricator.silent')) {
$message = pht(
'Phabricator is currently configured in silent mode, so it will not '.
'publish webhooks. To adjust this setting, see '.
'@{config:phabricator.silent} in Config.');
$warnings[] = id(new PHUIInfoView())
->setTitle(pht('Silent Mode'))
->setSeverity(PHUIInfoView::SEVERITY_WARNING)
->appendChild(new PHUIRemarkupView($viewer, $message));
}
$requests_table = id(new HeraldWebhookRequestListView())
->setViewer($viewer)
->setRequests($requests)

View file

@ -318,21 +318,10 @@ final class HeraldRule extends HeraldDAO
return new HeraldRuleEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new HeraldRuleTransaction();
}
public function willRenderTimeline(
PhabricatorApplicationTransactionView $timeline,
AphrontRequest $request) {
return $timeline;
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */

View file

@ -200,20 +200,10 @@ final class HeraldWebhook
return new HeraldWebhookEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new HeraldWebhookTransaction();
}
public function willRenderTimeline(
PhabricatorApplicationTransactionView $timeline,
AphrontRequest $request) {
return $timeline;
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */

View file

@ -26,6 +26,15 @@ final class HeraldWebhookRequest
const RESULT_OKAY = 'okay';
const RESULT_FAIL = 'fail';
const ERRORTYPE_HOOK = 'hook';
const ERRORTYPE_HTTP = 'http';
const ERRORTYPE_TIMEOUT = 'timeout';
const ERROR_SILENT = 'silent';
const ERROR_DISABLED = 'disabled';
const ERROR_URI = 'uri';
const ERROR_OBJECT = 'object';
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
@ -108,6 +117,28 @@ final class HeraldWebhookRequest
return $this->getProperty('errorCode');
}
public function getErrorTypeForDisplay() {
$map = array(
self::ERRORTYPE_HOOK => pht('Hook Error'),
self::ERRORTYPE_HTTP => pht('HTTP Error'),
self::ERRORTYPE_TIMEOUT => pht('Request Timeout'),
);
$type = $this->getErrorType();
return idx($map, $type, $type);
}
public function getErrorCodeForDisplay() {
$code = $this->getErrorCode();
if ($this->getErrorType() !== self::ERRORTYPE_HOOK) {
return $code;
}
$spec = $this->getHookErrorSpec($code);
return idx($spec, 'display', $code);
}
public function setTransactionPHIDs(array $phids) {
return $this->setProperty('transactionPHIDs', $phids);
}
@ -187,6 +218,28 @@ final class HeraldWebhookRequest
->setTooltip($tooltip);
}
private function getHookErrorSpec($code) {
$map = $this->getHookErrorMap();
return idx($map, $code, array());
}
private function getHookErrorMap() {
return array(
self::ERROR_SILENT => array(
'display' => pht('In Silent Mode'),
),
self::ERROR_DISABLED => array(
'display' => pht('Hook Disabled'),
),
self::ERROR_URI => array(
'display' => pht('Invalid URI'),
),
self::ERROR_OBJECT => array(
'display' => pht('Invalid Object'),
),
);
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */

View file

@ -55,8 +55,8 @@ final class HeraldWebhookRequestListView
$request->getID(),
$icon,
$handles[$request->getObjectPHID()]->renderLink(),
$request->getErrorType(),
$request->getErrorCode(),
$request->getErrorTypeForDisplay(),
$request->getErrorCodeForDisplay(),
$last_request,
);
}
@ -66,7 +66,7 @@ final class HeraldWebhookRequestListView
->setHeaders(
array(
pht('ID'),
'',
null,
pht('Object'),
pht('Type'),
pht('Code'),

View file

@ -35,14 +35,20 @@ final class HeraldWebhookWorker
// If we're in silent mode, permanently fail the webhook request and then
// return to complete this task.
if (PhabricatorEnv::getEnvConfig('phabricator.silent')) {
$this->failRequest($request, 'hook', 'silent');
$this->failRequest(
$request,
HeraldWebhookRequest::ERRORTYPE_HOOK,
HeraldWebhookRequest::ERROR_SILENT);
return;
}
$hook = $request->getWebhook();
if ($hook->isDisabled()) {
$this->failRequest($request, 'hook', 'disabled');
$this->failRequest(
$request,
HeraldWebhookRequest::ERRORTYPE_HOOK,
HeraldWebhookRequest::ERROR_DISABLED);
throw new PhabricatorWorkerPermanentFailureException(
pht(
'Associated hook ("%s") for webhook request ("%s") is disabled.',
@ -59,7 +65,10 @@ final class HeraldWebhookWorker
'https',
));
} catch (Exception $ex) {
$this->failRequest($request, 'hook', 'uri');
$this->failRequest(
$request,
HeraldWebhookRequest::ERRORTYPE_HOOK,
HeraldWebhookRequest::ERROR_URI);
throw new PhabricatorWorkerPermanentFailureException(
pht(
'Associated hook ("%s") for webhook request ("%s") has invalid '.
@ -76,7 +85,11 @@ final class HeraldWebhookWorker
->withPHIDs(array($object_phid))
->executeOne();
if (!$object) {
$this->failRequest($request, 'hook', 'object');
$this->failRequest(
$request,
HeraldWebhookRequest::ERRORTYPE_HOOK,
HeraldWebhookRequest::ERROR_OBJECT);
throw new PhabricatorWorkerPermanentFailureException(
pht(
'Unable to load object ("%s") for webhook request ("%s").',
@ -182,9 +195,9 @@ final class HeraldWebhookWorker
list($status) = $future->resolve();
if ($status->isTimeout()) {
$error_type = 'timeout';
$error_type = HeraldWebhookRequest::ERRORTYPE_TIMEOUT;
} else {
$error_type = 'http';
$error_type = HeraldWebhookRequest::ERRORTYPE_HTTP;
}
$error_code = $status->getStatusCode();

View file

@ -151,16 +151,6 @@ final class LegalpadDocumentSignController extends LegalpadController {
$errors = array();
$hisec_token = null;
if ($request->isFormOrHisecPost() && !$has_signed) {
// Require two-factor auth to sign legal documents.
if ($viewer->isLoggedIn()) {
$hisec_token = id(new PhabricatorAuthSessionEngine())
->requireHighSecurityToken(
$viewer,
$request,
$document->getURI());
}
list($form_data, $errors, $field_errors) = $this->readSignatureForm(
$document,
$request);
@ -187,6 +177,20 @@ final class LegalpadDocumentSignController extends LegalpadController {
$signature->setVerified($verified);
if (!$errors) {
// Require MFA to sign legal documents.
if ($viewer->isLoggedIn()) {
$workflow_key = sprintf(
'legalpad.sign(%s)',
$document->getPHID());
$hisec_token = id(new PhabricatorAuthSessionEngine())
->setWorkflowKey($workflow_key)
->requireHighSecurityToken(
$viewer,
$request,
$document->getURI());
}
$signature->save();
// If the viewer is logged in, signing for themselves, send them to

View file

@ -209,21 +209,10 @@ final class LegalpadDocument extends LegalpadDAO
return new LegalpadDocumentEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new LegalpadTransaction();
}
public function willRenderTimeline(
PhabricatorApplicationTransactionView $timeline,
AphrontRequest $request) {
return $timeline;
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */

View file

@ -14,10 +14,6 @@ final class LegalpadTransaction extends PhabricatorModularTransaction {
return new LegalpadTransactionComment();
}
public function getApplicationTransactionViewObject() {
return new LegalpadTransactionView();
}
public function getBaseTransactionClass() {
return 'LegalpadDocumentTransactionType';
}

View file

@ -1,4 +0,0 @@
<?php
final class LegalpadTransactionView
extends PhabricatorApplicationTransactionView {}

View file

@ -98,21 +98,10 @@ final class PhabricatorFileImageMacro extends PhabricatorFileDAO
return new PhabricatorMacroEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new PhabricatorMacroTransaction();
}
public function willRenderTimeline(
PhabricatorApplicationTransactionView $timeline,
AphrontRequest $request) {
return $timeline;
}
/* -( PhabricatorSubscribableInterface )----------------------------------- */

View file

@ -212,6 +212,8 @@ The keys you can provide in a specification are:
status.
- `locked` //Optional bool.// Lock tasks in this status, preventing users
from commenting.
- `mfa` //Optional bool.// Require all edits to this task to be signed with
multi-factor authentication.
Statuses will appear in the UI in the order specified. Note the status marked
`special` as `duplicate` is not settable directly and will not appear in UI

View file

@ -160,6 +160,10 @@ final class ManiphestTaskStatus extends ManiphestConstants {
return self::getStatusAttribute($status, 'locked', false);
}
public static function isMFAStatus($status) {
return self::getStatusAttribute($status, 'mfa', false);
}
public static function getStatusActionName($status) {
return self::getStatusAttribute($status, 'name.action');
}
@ -282,6 +286,7 @@ final class ManiphestTaskStatus extends ManiphestConstants {
'disabled' => 'optional bool',
'claim' => 'optional bool',
'locked' => 'optional bool',
'mfa' => 'optional bool',
));
}

View file

@ -0,0 +1,11 @@
<?php
final class ManiphestTaskMFAEngine
extends PhabricatorEditEngineMFAEngine {
public function shouldRequireMFA() {
$status = $this->getObject()->getStatus();
return ManiphestTaskStatus::isMFAStatus($status);
}
}

View file

@ -19,7 +19,8 @@ final class ManiphestTask extends ManiphestDAO
PhabricatorFerretInterface,
DoorkeeperBridgedObjectInterface,
PhabricatorEditEngineSubtypeInterface,
PhabricatorEditEngineLockableInterface {
PhabricatorEditEngineLockableInterface,
PhabricatorEditEngineMFAInterface {
const MARKUP_FIELD_DESCRIPTION = 'markup:desc';
@ -452,21 +453,10 @@ final class ManiphestTask extends ManiphestDAO
return new ManiphestTransactionEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new ManiphestTransaction();
}
public function willRenderTimeline(
PhabricatorApplicationTransactionView $timeline,
AphrontRequest $request) {
return $timeline;
}
/* -( PhabricatorSpacesInterface )----------------------------------------- */
@ -630,4 +620,12 @@ final class ManiphestTask extends ManiphestDAO
return new ManiphestTaskFerretEngine();
}
/* -( PhabricatorEditEngineMFAInterface )---------------------------------- */
public function newEditEngineMFAEngine() {
return new ManiphestTaskMFAEngine();
}
}

View file

@ -123,20 +123,10 @@ final class PhabricatorMetaMTAApplicationEmail
return new PhabricatorMetaMTAApplicationEmailEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new PhabricatorMetaMTAApplicationEmailTransaction();
}
public function willRenderTimeline(
PhabricatorApplicationTransactionView $timeline,
AphrontRequest $request) {
return $timeline;
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */

View file

@ -193,18 +193,8 @@ final class NuanceItem
return new NuanceItemEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new NuanceItemTransaction();
}
public function willRenderTimeline(
PhabricatorApplicationTransactionView $timeline,
AphrontRequest $request) {
return $timeline;
}
}

View file

@ -79,18 +79,8 @@ final class NuanceQueue
return new NuanceQueueEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new NuanceQueueTransaction();
}
public function willRenderTimeline(
PhabricatorApplicationTransactionView $timeline,
AphrontRequest $request) {
return $timeline;
}
}

View file

@ -99,21 +99,10 @@ final class NuanceSource extends NuanceDAO
return new NuanceSourceEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new NuanceSourceTransaction();
}
public function willRenderTimeline(
PhabricatorApplicationTransactionView $timeline,
AphrontRequest $request) {
return $timeline;
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */

View file

@ -91,20 +91,10 @@ final class PhabricatorOAuthServerClient
return new PhabricatorOAuthServerEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new PhabricatorOAuthServerTransaction();
}
public function willRenderTimeline(
PhabricatorApplicationTransactionView $timeline,
AphrontRequest $request) {
return $timeline;
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */

View file

@ -607,20 +607,10 @@ final class PhabricatorOwnersPackage
return new PhabricatorOwnersPackageTransactionEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new PhabricatorOwnersPackageTransaction();
}
public function willRenderTimeline(
PhabricatorApplicationTransactionView $timeline,
AphrontRequest $request) {
return $timeline;
}
/* -( PhabricatorCustomFieldInterface )------------------------------------ */

View file

@ -189,20 +189,10 @@ final class PhabricatorPackagesPackage
return new PhabricatorPackagesPackageEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new PhabricatorPackagesPackageTransaction();
}
public function willRenderTimeline(
PhabricatorApplicationTransactionView $timeline,
AphrontRequest $request) {
return $timeline;
}
/* -( PhabricatorNgramsInterface )----------------------------------------- */

View file

@ -165,20 +165,10 @@ final class PhabricatorPackagesPublisher
return new PhabricatorPackagesPublisherEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new PhabricatorPackagesPublisherTransaction();
}
public function willRenderTimeline(
PhabricatorApplicationTransactionView $timeline,
AphrontRequest $request) {
return $timeline;
}
/* -( PhabricatorNgramsInterface )----------------------------------------- */

View file

@ -156,20 +156,10 @@ final class PhabricatorPackagesVersion
return new PhabricatorPackagesVersionEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new PhabricatorPackagesVersionTransaction();
}
public function willRenderTimeline(
PhabricatorApplicationTransactionView $timeline,
AphrontRequest $request) {
return $timeline;
}
/* -( PhabricatorNgramsInterface )----------------------------------------- */

View file

@ -117,21 +117,10 @@ final class PassphraseCredential extends PassphraseDAO
return new PassphraseCredentialTransactionEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new PassphraseCredentialTransaction();
}
public function willRenderTimeline(
PhabricatorApplicationTransactionView $timeline,
AphrontRequest $request) {
return $timeline;
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */

View file

@ -219,21 +219,10 @@ final class PhabricatorPaste extends PhabricatorPasteDAO
return new PhabricatorPasteEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new PhabricatorPasteTransaction();
}
public function willRenderTimeline(
PhabricatorApplicationTransactionView $timeline,
AphrontRequest $request) {
return $timeline;
}
/* -( PhabricatorSpacesInterface )----------------------------------------- */

Some files were not shown because too many files have changed in this diff Show more