mirror of
https://we.phorge.it/source/phorge.git
synced 2025-01-03 11:21:01 +01:00
When users confirm Duo MFA in the mobile app, live-update the UI
Summary: Ref T13249. Poll for Duo updates in the background so we can automatically update the UI when the user clicks the mobile phone app button. Test Plan: Hit a Duo gate, clicked "Approve" in the mobile app, saw the UI update immediately. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13249 Differential Revision: https://secure.phabricator.com/D20169
This commit is contained in:
parent
454a762562
commit
2ca316d652
11 changed files with 284 additions and 30 deletions
|
@ -9,7 +9,7 @@ return array(
|
||||||
'names' => array(
|
'names' => array(
|
||||||
'conpherence.pkg.css' => '3c8a0668',
|
'conpherence.pkg.css' => '3c8a0668',
|
||||||
'conpherence.pkg.js' => '020aebcf',
|
'conpherence.pkg.js' => '020aebcf',
|
||||||
'core.pkg.css' => '7a73ffc5',
|
'core.pkg.css' => 'e0f5d66f',
|
||||||
'core.pkg.js' => '5c737607',
|
'core.pkg.js' => '5c737607',
|
||||||
'differential.pkg.css' => 'b8df73d4',
|
'differential.pkg.css' => 'b8df73d4',
|
||||||
'differential.pkg.js' => '67c9ea4c',
|
'differential.pkg.js' => '67c9ea4c',
|
||||||
|
@ -151,7 +151,7 @@ return array(
|
||||||
'rsrc/css/phui/phui-document.css' => '52b748a5',
|
'rsrc/css/phui/phui-document.css' => '52b748a5',
|
||||||
'rsrc/css/phui/phui-feed-story.css' => 'a0c05029',
|
'rsrc/css/phui/phui-feed-story.css' => 'a0c05029',
|
||||||
'rsrc/css/phui/phui-fontkit.css' => '9b714a5e',
|
'rsrc/css/phui/phui-fontkit.css' => '9b714a5e',
|
||||||
'rsrc/css/phui/phui-form-view.css' => '0807e7ac',
|
'rsrc/css/phui/phui-form-view.css' => '01b796c0',
|
||||||
'rsrc/css/phui/phui-form.css' => '159e2d9c',
|
'rsrc/css/phui/phui-form.css' => '159e2d9c',
|
||||||
'rsrc/css/phui/phui-head-thing.css' => 'd7f293df',
|
'rsrc/css/phui/phui-head-thing.css' => 'd7f293df',
|
||||||
'rsrc/css/phui/phui-header-view.css' => '93cea4ec',
|
'rsrc/css/phui/phui-header-view.css' => '93cea4ec',
|
||||||
|
@ -502,6 +502,7 @@ return array(
|
||||||
'rsrc/js/phui/behavior-phui-selectable-list.js' => 'b26a41e4',
|
'rsrc/js/phui/behavior-phui-selectable-list.js' => 'b26a41e4',
|
||||||
'rsrc/js/phui/behavior-phui-submenu.js' => 'b5e9bff9',
|
'rsrc/js/phui/behavior-phui-submenu.js' => 'b5e9bff9',
|
||||||
'rsrc/js/phui/behavior-phui-tab-group.js' => '242aa08b',
|
'rsrc/js/phui/behavior-phui-tab-group.js' => '242aa08b',
|
||||||
|
'rsrc/js/phui/behavior-phui-timer-control.js' => 'f84bcbf4',
|
||||||
'rsrc/js/phuix/PHUIXActionListView.js' => 'c68f183f',
|
'rsrc/js/phuix/PHUIXActionListView.js' => 'c68f183f',
|
||||||
'rsrc/js/phuix/PHUIXActionView.js' => 'aaa08f3b',
|
'rsrc/js/phuix/PHUIXActionView.js' => 'aaa08f3b',
|
||||||
'rsrc/js/phuix/PHUIXAutocomplete.js' => '58cc4ab8',
|
'rsrc/js/phuix/PHUIXAutocomplete.js' => '58cc4ab8',
|
||||||
|
@ -650,6 +651,7 @@ return array(
|
||||||
'javelin-behavior-phui-selectable-list' => 'b26a41e4',
|
'javelin-behavior-phui-selectable-list' => 'b26a41e4',
|
||||||
'javelin-behavior-phui-submenu' => 'b5e9bff9',
|
'javelin-behavior-phui-submenu' => 'b5e9bff9',
|
||||||
'javelin-behavior-phui-tab-group' => '242aa08b',
|
'javelin-behavior-phui-tab-group' => '242aa08b',
|
||||||
|
'javelin-behavior-phui-timer-control' => 'f84bcbf4',
|
||||||
'javelin-behavior-phuix-example' => 'c2c500a7',
|
'javelin-behavior-phuix-example' => 'c2c500a7',
|
||||||
'javelin-behavior-policy-control' => '0eaa33a9',
|
'javelin-behavior-policy-control' => '0eaa33a9',
|
||||||
'javelin-behavior-policy-rule-editor' => '9347f172',
|
'javelin-behavior-policy-rule-editor' => '9347f172',
|
||||||
|
@ -817,7 +819,7 @@ return array(
|
||||||
'phui-font-icon-base-css' => 'd7994e06',
|
'phui-font-icon-base-css' => 'd7994e06',
|
||||||
'phui-fontkit-css' => '9b714a5e',
|
'phui-fontkit-css' => '9b714a5e',
|
||||||
'phui-form-css' => '159e2d9c',
|
'phui-form-css' => '159e2d9c',
|
||||||
'phui-form-view-css' => '0807e7ac',
|
'phui-form-view-css' => '01b796c0',
|
||||||
'phui-head-thing-view-css' => 'd7f293df',
|
'phui-head-thing-view-css' => 'd7f293df',
|
||||||
'phui-header-view-css' => '93cea4ec',
|
'phui-header-view-css' => '93cea4ec',
|
||||||
'phui-hovercard' => '074f0783',
|
'phui-hovercard' => '074f0783',
|
||||||
|
@ -2111,6 +2113,11 @@ return array(
|
||||||
'javelin-stratcom',
|
'javelin-stratcom',
|
||||||
'javelin-dom',
|
'javelin-dom',
|
||||||
),
|
),
|
||||||
|
'f84bcbf4' => array(
|
||||||
|
'javelin-behavior',
|
||||||
|
'javelin-stratcom',
|
||||||
|
'javelin-dom',
|
||||||
|
),
|
||||||
'f8c4e135' => array(
|
'f8c4e135' => array(
|
||||||
'javelin-install',
|
'javelin-install',
|
||||||
'javelin-dom',
|
'javelin-dom',
|
||||||
|
|
|
@ -2195,6 +2195,8 @@ phutil_register_library_map(array(
|
||||||
'PhabricatorAuthChallengeGarbageCollector' => 'applications/auth/garbagecollector/PhabricatorAuthChallengeGarbageCollector.php',
|
'PhabricatorAuthChallengeGarbageCollector' => 'applications/auth/garbagecollector/PhabricatorAuthChallengeGarbageCollector.php',
|
||||||
'PhabricatorAuthChallengePHIDType' => 'applications/auth/phid/PhabricatorAuthChallengePHIDType.php',
|
'PhabricatorAuthChallengePHIDType' => 'applications/auth/phid/PhabricatorAuthChallengePHIDType.php',
|
||||||
'PhabricatorAuthChallengeQuery' => 'applications/auth/query/PhabricatorAuthChallengeQuery.php',
|
'PhabricatorAuthChallengeQuery' => 'applications/auth/query/PhabricatorAuthChallengeQuery.php',
|
||||||
|
'PhabricatorAuthChallengeStatusController' => 'applications/auth/controller/mfa/PhabricatorAuthChallengeStatusController.php',
|
||||||
|
'PhabricatorAuthChallengeUpdate' => 'applications/auth/view/PhabricatorAuthChallengeUpdate.php',
|
||||||
'PhabricatorAuthChangePasswordAction' => 'applications/auth/action/PhabricatorAuthChangePasswordAction.php',
|
'PhabricatorAuthChangePasswordAction' => 'applications/auth/action/PhabricatorAuthChangePasswordAction.php',
|
||||||
'PhabricatorAuthConduitAPIMethod' => 'applications/auth/conduit/PhabricatorAuthConduitAPIMethod.php',
|
'PhabricatorAuthConduitAPIMethod' => 'applications/auth/conduit/PhabricatorAuthConduitAPIMethod.php',
|
||||||
'PhabricatorAuthConduitTokenRevoker' => 'applications/auth/revoker/PhabricatorAuthConduitTokenRevoker.php',
|
'PhabricatorAuthConduitTokenRevoker' => 'applications/auth/revoker/PhabricatorAuthConduitTokenRevoker.php',
|
||||||
|
@ -7925,6 +7927,8 @@ phutil_register_library_map(array(
|
||||||
'PhabricatorAuthChallengeGarbageCollector' => 'PhabricatorGarbageCollector',
|
'PhabricatorAuthChallengeGarbageCollector' => 'PhabricatorGarbageCollector',
|
||||||
'PhabricatorAuthChallengePHIDType' => 'PhabricatorPHIDType',
|
'PhabricatorAuthChallengePHIDType' => 'PhabricatorPHIDType',
|
||||||
'PhabricatorAuthChallengeQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
|
'PhabricatorAuthChallengeQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
|
||||||
|
'PhabricatorAuthChallengeStatusController' => 'PhabricatorAuthController',
|
||||||
|
'PhabricatorAuthChallengeUpdate' => 'Phobject',
|
||||||
'PhabricatorAuthChangePasswordAction' => 'PhabricatorSystemAction',
|
'PhabricatorAuthChangePasswordAction' => 'PhabricatorSystemAction',
|
||||||
'PhabricatorAuthConduitAPIMethod' => 'ConduitAPIMethod',
|
'PhabricatorAuthConduitAPIMethod' => 'ConduitAPIMethod',
|
||||||
'PhabricatorAuthConduitTokenRevoker' => 'PhabricatorAuthRevoker',
|
'PhabricatorAuthConduitTokenRevoker' => 'PhabricatorAuthRevoker',
|
||||||
|
|
|
@ -97,6 +97,8 @@ final class PhabricatorAuthApplication extends PhabricatorApplication {
|
||||||
'PhabricatorAuthFactorProviderViewController',
|
'PhabricatorAuthFactorProviderViewController',
|
||||||
'message/(?P<id>[1-9]\d*)/' =>
|
'message/(?P<id>[1-9]\d*)/' =>
|
||||||
'PhabricatorAuthFactorProviderMessageController',
|
'PhabricatorAuthFactorProviderMessageController',
|
||||||
|
'challenge/status/(?P<id>[1-9]\d*)/' =>
|
||||||
|
'PhabricatorAuthChallengeStatusController',
|
||||||
),
|
),
|
||||||
|
|
||||||
'message/' => array(
|
'message/' => array(
|
||||||
|
|
|
@ -0,0 +1,40 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
final class PhabricatorAuthChallengeStatusController
|
||||||
|
extends PhabricatorAuthController {
|
||||||
|
|
||||||
|
public function handleRequest(AphrontRequest $request) {
|
||||||
|
$viewer = $this->getViewer();
|
||||||
|
$id = $request->getURIData('id');
|
||||||
|
$now = PhabricatorTime::getNow();
|
||||||
|
|
||||||
|
$result = new PhabricatorAuthChallengeUpdate();
|
||||||
|
|
||||||
|
$challenge = id(new PhabricatorAuthChallengeQuery())
|
||||||
|
->setViewer($viewer)
|
||||||
|
->withIDs(array($id))
|
||||||
|
->withUserPHIDs(array($viewer->getPHID()))
|
||||||
|
->withChallengeTTLBetween($now, null)
|
||||||
|
->executeOne();
|
||||||
|
if ($challenge) {
|
||||||
|
$config = id(new PhabricatorAuthFactorConfigQuery())
|
||||||
|
->setViewer($viewer)
|
||||||
|
->withPHIDs(array($challenge->getFactorPHID()))
|
||||||
|
->executeOne();
|
||||||
|
if ($config) {
|
||||||
|
$provider = $config->getFactorProvider();
|
||||||
|
$factor = $provider->getFactor();
|
||||||
|
|
||||||
|
$result = $factor->newChallengeStatusView(
|
||||||
|
$config,
|
||||||
|
$provider,
|
||||||
|
$viewer,
|
||||||
|
$challenge);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return id(new AphrontAjaxResponse())
|
||||||
|
->setContent($result->newContent());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -80,6 +80,14 @@ abstract class PhabricatorAuthFactor extends Phobject {
|
||||||
return array();
|
return array();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function newChallengeStatusView(
|
||||||
|
PhabricatorAuthFactorConfig $config,
|
||||||
|
PhabricatorAuthFactorProvider $provider,
|
||||||
|
PhabricatorUser $viewer,
|
||||||
|
PhabricatorAuthChallenge $challenge) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Is this a factor which depends on the user's contact number?
|
* Is this a factor which depends on the user's contact number?
|
||||||
*
|
*
|
||||||
|
@ -210,8 +218,6 @@ abstract class PhabricatorAuthFactor extends Phobject {
|
||||||
get_class($this)));
|
get_class($this)));
|
||||||
}
|
}
|
||||||
|
|
||||||
$result->setIssuedChallenges($challenges);
|
|
||||||
|
|
||||||
return $result;
|
return $result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -242,8 +248,6 @@ abstract class PhabricatorAuthFactor extends Phobject {
|
||||||
get_class($this)));
|
get_class($this)));
|
||||||
}
|
}
|
||||||
|
|
||||||
$result->setIssuedChallenges($challenges);
|
|
||||||
|
|
||||||
return $result;
|
return $result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -339,9 +343,18 @@ abstract class PhabricatorAuthFactor extends Phobject {
|
||||||
->setIcon('fa-commenting', 'green');
|
->setIcon('fa-commenting', 'green');
|
||||||
}
|
}
|
||||||
|
|
||||||
return id(new PHUIFormTimerControl())
|
$control = id(new PHUIFormTimerControl())
|
||||||
->setIcon($icon)
|
->setIcon($icon)
|
||||||
->appendChild($error);
|
->appendChild($error);
|
||||||
|
|
||||||
|
$status_challenge = $result->getStatusChallenge();
|
||||||
|
if ($status_challenge) {
|
||||||
|
$id = $status_challenge->getID();
|
||||||
|
$uri = "/auth/mfa/challenge/status/{$id}/";
|
||||||
|
$control->setUpdateURI($uri);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $control;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,7 @@ final class PhabricatorAuthFactorResult
|
||||||
private $value;
|
private $value;
|
||||||
private $issuedChallenges = array();
|
private $issuedChallenges = array();
|
||||||
private $icon;
|
private $icon;
|
||||||
|
private $statusChallenge;
|
||||||
|
|
||||||
public function setAnsweredChallenge(PhabricatorAuthChallenge $challenge) {
|
public function setAnsweredChallenge(PhabricatorAuthChallenge $challenge) {
|
||||||
if (!$challenge->getIsAnsweredChallenge()) {
|
if (!$challenge->getIsAnsweredChallenge()) {
|
||||||
|
@ -34,6 +35,15 @@ final class PhabricatorAuthFactorResult
|
||||||
return $this->answeredChallenge;
|
return $this->answeredChallenge;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function setStatusChallenge(PhabricatorAuthChallenge $challenge) {
|
||||||
|
$this->statusChallenge = $challenge;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getStatusChallenge() {
|
||||||
|
return $this->statusChallenge;
|
||||||
|
}
|
||||||
|
|
||||||
public function getIsValid() {
|
public function getIsValid() {
|
||||||
return (bool)$this->getAnsweredChallenge();
|
return (bool)$this->getAnsweredChallenge();
|
||||||
}
|
}
|
||||||
|
@ -83,16 +93,6 @@ final class PhabricatorAuthFactorResult
|
||||||
return $this->value;
|
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function setIcon(PHUIIconView $icon) {
|
public function setIcon(PHUIIconView $icon) {
|
||||||
$this->icon = $icon;
|
$this->icon = $icon;
|
||||||
return $this;
|
return $this;
|
||||||
|
|
|
@ -585,7 +585,7 @@ final class PhabricatorDuoAuthFactor
|
||||||
$result = $this->newDuoFuture($provider)
|
$result = $this->newDuoFuture($provider)
|
||||||
->setHTTPMethod('GET')
|
->setHTTPMethod('GET')
|
||||||
->setMethod('auth_status', $parameters)
|
->setMethod('auth_status', $parameters)
|
||||||
->setTimeout(5)
|
->setTimeout(3)
|
||||||
->resolve();
|
->resolve();
|
||||||
|
|
||||||
$state = $result['response']['result'];
|
$state = $result['response']['result'];
|
||||||
|
@ -661,15 +661,6 @@ final class PhabricatorDuoAuthFactor
|
||||||
PhabricatorAuthFactorResult $result) {
|
PhabricatorAuthFactorResult $result) {
|
||||||
|
|
||||||
$control = $this->newAutomaticControl($result);
|
$control = $this->newAutomaticControl($result);
|
||||||
if (!$control) {
|
|
||||||
$result = $this->newResult()
|
|
||||||
->setIsContinue(true)
|
|
||||||
->setErrorMessage(
|
|
||||||
pht(
|
|
||||||
'A challenge has been sent to your phone. Open the Duo '.
|
|
||||||
'application and confirm the challenge, then continue.'));
|
|
||||||
$control = $this->newAutomaticControl($result);
|
|
||||||
}
|
|
||||||
|
|
||||||
$control
|
$control
|
||||||
->setLabel(pht('Duo'))
|
->setLabel(pht('Duo'))
|
||||||
|
@ -689,7 +680,27 @@ final class PhabricatorDuoAuthFactor
|
||||||
PhabricatorUser $viewer,
|
PhabricatorUser $viewer,
|
||||||
AphrontRequest $request,
|
AphrontRequest $request,
|
||||||
array $challenges) {
|
array $challenges) {
|
||||||
return $this->newResult();
|
|
||||||
|
$result = $this->newResult()
|
||||||
|
->setIsContinue(true)
|
||||||
|
->setErrorMessage(
|
||||||
|
pht(
|
||||||
|
'A challenge has been sent to your phone. Open the Duo '.
|
||||||
|
'application and confirm the challenge, then continue.'));
|
||||||
|
|
||||||
|
$challenge = $this->getChallengeForCurrentContext(
|
||||||
|
$config,
|
||||||
|
$viewer,
|
||||||
|
$challenges);
|
||||||
|
if ($challenge) {
|
||||||
|
$result
|
||||||
|
->setStatusChallenge($challenge)
|
||||||
|
->setIcon(
|
||||||
|
id(new PHUIIconView())
|
||||||
|
->setIcon('fa-refresh', 'green ph-spin'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function newDuoFuture(PhabricatorAuthFactorProvider $provider) {
|
private function newDuoFuture(PhabricatorAuthFactorProvider $provider) {
|
||||||
|
@ -790,4 +801,54 @@ final class PhabricatorDuoAuthFactor
|
||||||
$hostname));
|
$hostname));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function newChallengeStatusView(
|
||||||
|
PhabricatorAuthFactorConfig $config,
|
||||||
|
PhabricatorAuthFactorProvider $provider,
|
||||||
|
PhabricatorUser $viewer,
|
||||||
|
PhabricatorAuthChallenge $challenge) {
|
||||||
|
|
||||||
|
$duo_xaction = $challenge->getChallengeKey();
|
||||||
|
|
||||||
|
$parameters = array(
|
||||||
|
'txid' => $duo_xaction,
|
||||||
|
);
|
||||||
|
|
||||||
|
$default_result = id(new PhabricatorAuthChallengeUpdate())
|
||||||
|
->setRetry(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$result = $this->newDuoFuture($provider)
|
||||||
|
->setHTTPMethod('GET')
|
||||||
|
->setMethod('auth_status', $parameters)
|
||||||
|
->setTimeout(5)
|
||||||
|
->resolve();
|
||||||
|
|
||||||
|
$state = $result['response']['result'];
|
||||||
|
} catch (HTTPFutureCURLResponseStatus $exception) {
|
||||||
|
// If we failed or timed out, retry. Usually, this is a timeout.
|
||||||
|
return id(new PhabricatorAuthChallengeUpdate())
|
||||||
|
->setRetry(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// For now, don't update the view for anything but an "Allow". Updates
|
||||||
|
// here are just about providing more visual feedback for user convenience.
|
||||||
|
if ($state !== 'allow') {
|
||||||
|
return id(new PhabricatorAuthChallengeUpdate())
|
||||||
|
->setRetry(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
$icon = id(new PHUIIconView())
|
||||||
|
->setIcon('fa-check-circle-o', 'green');
|
||||||
|
|
||||||
|
$view = id(new PHUIFormTimerControl())
|
||||||
|
->setIcon($icon)
|
||||||
|
->appendChild(pht('You responded to this challenge correctly.'))
|
||||||
|
->newTimerView();
|
||||||
|
|
||||||
|
return id(new PhabricatorAuthChallengeUpdate())
|
||||||
|
->setState('allow')
|
||||||
|
->setRetry(false)
|
||||||
|
->setMarkup($view);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,44 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
final class PhabricatorAuthChallengeUpdate
|
||||||
|
extends Phobject {
|
||||||
|
|
||||||
|
private $retry = false;
|
||||||
|
private $state;
|
||||||
|
private $markup;
|
||||||
|
|
||||||
|
public function setRetry($retry) {
|
||||||
|
$this->retry = $retry;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getRetry() {
|
||||||
|
return $this->retry;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setState($state) {
|
||||||
|
$this->state = $state;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getState() {
|
||||||
|
return $this->state;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setMarkup($markup) {
|
||||||
|
$this->markup = $markup;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getMarkup() {
|
||||||
|
return $this->markup;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function newContent() {
|
||||||
|
return array(
|
||||||
|
'retry' => $this->getRetry(),
|
||||||
|
'state' => $this->getState(),
|
||||||
|
'markup' => $this->getMarkup(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,6 +3,7 @@
|
||||||
final class PHUIFormTimerControl extends AphrontFormControl {
|
final class PHUIFormTimerControl extends AphrontFormControl {
|
||||||
|
|
||||||
private $icon;
|
private $icon;
|
||||||
|
private $updateURI;
|
||||||
|
|
||||||
public function setIcon(PHUIIconView $icon) {
|
public function setIcon(PHUIIconView $icon) {
|
||||||
$this->icon = $icon;
|
$this->icon = $icon;
|
||||||
|
@ -13,11 +14,24 @@ final class PHUIFormTimerControl extends AphrontFormControl {
|
||||||
return $this->icon;
|
return $this->icon;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function setUpdateURI($update_uri) {
|
||||||
|
$this->updateURI = $update_uri;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getUpdateURI() {
|
||||||
|
return $this->updateURI;
|
||||||
|
}
|
||||||
|
|
||||||
protected function getCustomControlClass() {
|
protected function getCustomControlClass() {
|
||||||
return 'phui-form-timer';
|
return 'phui-form-timer';
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function renderInput() {
|
protected function renderInput() {
|
||||||
|
return $this->newTimerView();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function newTimerView() {
|
||||||
$icon_cell = phutil_tag(
|
$icon_cell = phutil_tag(
|
||||||
'td',
|
'td',
|
||||||
array(
|
array(
|
||||||
|
@ -34,7 +48,21 @@ final class PHUIFormTimerControl extends AphrontFormControl {
|
||||||
|
|
||||||
$row = phutil_tag('tr', array(), array($icon_cell, $content_cell));
|
$row = phutil_tag('tr', array(), array($icon_cell, $content_cell));
|
||||||
|
|
||||||
return phutil_tag('table', array(), $row);
|
$node_id = null;
|
||||||
|
|
||||||
|
$update_uri = $this->getUpdateURI();
|
||||||
|
if ($update_uri) {
|
||||||
|
$node_id = celerity_generate_unique_node_id();
|
||||||
|
|
||||||
|
Javelin::initBehavior(
|
||||||
|
'phui-timer-control',
|
||||||
|
array(
|
||||||
|
'nodeID' => $node_id,
|
||||||
|
'uri' => $update_uri,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
return phutil_tag('table', array('id' => $node_id), $row);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -578,3 +578,17 @@ properly, and submit values. */
|
||||||
.mfa-form-enroll-button {
|
.mfa-form-enroll-button {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.phui-form-timer-updated {
|
||||||
|
animation: phui-form-timer-fade-in 2s linear;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@keyframes phui-form-timer-fade-in {
|
||||||
|
0% {
|
||||||
|
background-color: {$lightyellow};
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
41
webroot/rsrc/js/phui/behavior-phui-timer-control.js
Normal file
41
webroot/rsrc/js/phui/behavior-phui-timer-control.js
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
/**
|
||||||
|
* @provides javelin-behavior-phui-timer-control
|
||||||
|
* @requires javelin-behavior
|
||||||
|
* javelin-stratcom
|
||||||
|
* javelin-dom
|
||||||
|
*/
|
||||||
|
|
||||||
|
JX.behavior('phui-timer-control', function(config) {
|
||||||
|
var node = JX.$(config.nodeID);
|
||||||
|
var uri = config.uri;
|
||||||
|
var state = null;
|
||||||
|
|
||||||
|
function onupdate(result) {
|
||||||
|
var markup = result.markup;
|
||||||
|
if (markup) {
|
||||||
|
var new_node = JX.$H(markup).getFragment().firstChild;
|
||||||
|
JX.DOM.replace(node, new_node);
|
||||||
|
node = new_node;
|
||||||
|
|
||||||
|
// If the overall state has changed from the previous display state,
|
||||||
|
// animate the control to draw the user's attention to the state change.
|
||||||
|
if (result.state !== state) {
|
||||||
|
state = result.state;
|
||||||
|
JX.DOM.alterClass(node, 'phui-form-timer-updated', true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var retry = result.retry;
|
||||||
|
if (retry) {
|
||||||
|
setTimeout(update, 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function update() {
|
||||||
|
new JX.Request(uri, onupdate)
|
||||||
|
.setTimeout(10000)
|
||||||
|
.send();
|
||||||
|
}
|
||||||
|
|
||||||
|
update();
|
||||||
|
});
|
Loading…
Reference in a new issue