mirror of
https://we.phorge.it/source/phorge.git
synced 2024-12-23 14:00:56 +01:00
Add JIRA doorkeeper and remarkup support
Summary: Ref T3687. Adds a Doorkeeper bridge for JIRA issues, plus remarkup support. In particular: - The Asana and JIRA remarkup rules shared most of their implementation, so I refactored what I could into a base class. - Actual bridge implementation is straightforward and similar to Asana, although probably not similar enough to really justify refactoring. Test Plan: - When logged in as a JIRA-connected user, pasted a JIRA issue link and saw it enriched at rendering time. - Logged in and out with JIRA. - Tested an Asana link, too (seems I haven't broken anything). Reviewers: btrahan Reviewed By: btrahan CC: aran Maniphest Tasks: T3687 Differential Revision: https://secure.phabricator.com/D6878
This commit is contained in:
parent
e5b4ce5525
commit
825fb9c85a
7 changed files with 281 additions and 68 deletions
|
@ -545,6 +545,7 @@ phutil_register_library_map(array(
|
|||
'DivinerWorkflow' => 'applications/diviner/workflow/DivinerWorkflow.php',
|
||||
'DoorkeeperBridge' => 'applications/doorkeeper/bridge/DoorkeeperBridge.php',
|
||||
'DoorkeeperBridgeAsana' => 'applications/doorkeeper/bridge/DoorkeeperBridgeAsana.php',
|
||||
'DoorkeeperBridgeJIRA' => 'applications/doorkeeper/bridge/DoorkeeperBridgeJIRA.php',
|
||||
'DoorkeeperDAO' => 'applications/doorkeeper/storage/DoorkeeperDAO.php',
|
||||
'DoorkeeperExternalObject' => 'applications/doorkeeper/storage/DoorkeeperExternalObject.php',
|
||||
'DoorkeeperExternalObjectQuery' => 'applications/doorkeeper/query/DoorkeeperExternalObjectQuery.php',
|
||||
|
@ -552,7 +553,9 @@ phutil_register_library_map(array(
|
|||
'DoorkeeperFeedWorkerAsana' => 'applications/doorkeeper/worker/DoorkeeperFeedWorkerAsana.php',
|
||||
'DoorkeeperImportEngine' => 'applications/doorkeeper/engine/DoorkeeperImportEngine.php',
|
||||
'DoorkeeperObjectRef' => 'applications/doorkeeper/engine/DoorkeeperObjectRef.php',
|
||||
'DoorkeeperRemarkupRule' => 'applications/doorkeeper/remarkup/DoorkeeperRemarkupRule.php',
|
||||
'DoorkeeperRemarkupRuleAsana' => 'applications/doorkeeper/remarkup/DoorkeeperRemarkupRuleAsana.php',
|
||||
'DoorkeeperRemarkupRuleJIRA' => 'applications/doorkeeper/remarkup/DoorkeeperRemarkupRuleJIRA.php',
|
||||
'DoorkeeperTagsController' => 'applications/doorkeeper/controller/DoorkeeperTagsController.php',
|
||||
'DrydockAllocatorWorker' => 'applications/drydock/worker/DrydockAllocatorWorker.php',
|
||||
'DrydockApacheWebrootInterface' => 'applications/drydock/interface/webroot/DrydockApacheWebrootInterface.php',
|
||||
|
@ -2026,6 +2029,7 @@ phutil_register_library_map(array(
|
|||
'function' =>
|
||||
array(
|
||||
'_phabricator_date_format' => 'view/viewutils.php',
|
||||
'_phabricator_time_format' => 'view/viewutils.php',
|
||||
'celerity_generate_unique_node_id' => 'infrastructure/celerity/api.php',
|
||||
'celerity_get_resource_uri' => 'infrastructure/celerity/api.php',
|
||||
'celerity_register_resource_map' => 'infrastructure/celerity/map.php',
|
||||
|
@ -2575,6 +2579,7 @@ phutil_register_library_map(array(
|
|||
'DivinerWorkflow' => 'PhutilArgumentWorkflow',
|
||||
'DoorkeeperBridge' => 'Phobject',
|
||||
'DoorkeeperBridgeAsana' => 'DoorkeeperBridge',
|
||||
'DoorkeeperBridgeJIRA' => 'DoorkeeperBridge',
|
||||
'DoorkeeperDAO' => 'PhabricatorLiskDAO',
|
||||
'DoorkeeperExternalObject' =>
|
||||
array(
|
||||
|
@ -2585,7 +2590,9 @@ phutil_register_library_map(array(
|
|||
'DoorkeeperFeedWorkerAsana' => 'FeedPushWorker',
|
||||
'DoorkeeperImportEngine' => 'Phobject',
|
||||
'DoorkeeperObjectRef' => 'Phobject',
|
||||
'DoorkeeperRemarkupRuleAsana' => 'PhutilRemarkupRule',
|
||||
'DoorkeeperRemarkupRule' => 'PhutilRemarkupRule',
|
||||
'DoorkeeperRemarkupRuleAsana' => 'DoorkeeperRemarkupRule',
|
||||
'DoorkeeperRemarkupRuleJIRA' => 'DoorkeeperRemarkupRule',
|
||||
'DoorkeeperTagsController' => 'PhabricatorController',
|
||||
'DrydockAllocatorWorker' => 'PhabricatorWorker',
|
||||
'DrydockApacheWebrootInterface' => 'DrydockWebrootInterface',
|
||||
|
|
|
@ -3,6 +3,10 @@
|
|||
final class PhabricatorAuthProviderOAuth1JIRA
|
||||
extends PhabricatorAuthProviderOAuth1 {
|
||||
|
||||
public function getJIRABaseURI() {
|
||||
return $this->getProviderConfig()->getProperty(self::PROPERTY_JIRA_URI);
|
||||
}
|
||||
|
||||
public function getProviderName() {
|
||||
return pht('JIRA');
|
||||
}
|
||||
|
@ -245,4 +249,29 @@ final class PhabricatorAuthProviderOAuth1JIRA
|
|||
return true;
|
||||
}
|
||||
|
||||
public static function getJIRAProvider() {
|
||||
$providers = self::getAllEnabledProviders();
|
||||
|
||||
foreach ($providers as $provider) {
|
||||
if ($provider instanceof PhabricatorAuthProviderOAuth1JIRA) {
|
||||
return $provider;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function newJIRAFuture(
|
||||
PhabricatorExternalAccount $account,
|
||||
$path,
|
||||
$method,
|
||||
$params = array()) {
|
||||
|
||||
$adapter = clone $this->getAdapter();
|
||||
$adapter->setToken($account->getProperty('oauth1.token'));
|
||||
$adapter->setTokenSecret($account->getProperty('oauth1.token.secret'));
|
||||
|
||||
return $adapter->newJIRAFuture($path, $method, $params);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@ final class PhabricatorApplicationDoorkeeper extends PhabricatorApplication {
|
|||
public function getRemarkupRules() {
|
||||
return array(
|
||||
new DoorkeeperRemarkupRuleAsana(),
|
||||
new DoorkeeperRemarkupRuleJIRA(),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
119
src/applications/doorkeeper/bridge/DoorkeeperBridgeJIRA.php
Normal file
119
src/applications/doorkeeper/bridge/DoorkeeperBridgeJIRA.php
Normal file
|
@ -0,0 +1,119 @@
|
|||
<?php
|
||||
|
||||
final class DoorkeeperBridgeJIRA extends DoorkeeperBridge {
|
||||
|
||||
const APPTYPE_JIRA = 'jira';
|
||||
const OBJTYPE_ISSUE = 'jira:issue';
|
||||
|
||||
public function canPullRef(DoorkeeperObjectRef $ref) {
|
||||
if ($ref->getApplicationType() != self::APPTYPE_JIRA) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$types = array(
|
||||
self::OBJTYPE_ISSUE => true,
|
||||
);
|
||||
|
||||
return isset($types[$ref->getObjectType()]);
|
||||
}
|
||||
|
||||
public function pullRefs(array $refs) {
|
||||
|
||||
$id_map = mpull($refs, 'getObjectID', 'getObjectKey');
|
||||
$viewer = $this->getViewer();
|
||||
|
||||
$provider = PhabricatorAuthProviderOAuth1JIRA::getJIRAProvider();
|
||||
if (!$provider) {
|
||||
return;
|
||||
}
|
||||
|
||||
$accounts = id(new PhabricatorExternalAccountQuery())
|
||||
->setViewer($viewer)
|
||||
->withUserPHIDs(array($viewer->getPHID()))
|
||||
->withAccountTypes(array($provider->getProviderType()))
|
||||
->execute();
|
||||
|
||||
if (!$accounts) {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: When we support multiple JIRA instances, we need to disambiguate
|
||||
// issues (perhaps with additional configuration) or cast a wide net
|
||||
// (by querying all instances). For now, just query the one instance.
|
||||
$account = head($accounts);
|
||||
|
||||
$futures = array();
|
||||
foreach ($id_map as $key => $id) {
|
||||
$futures[$key] = $provider->newJIRAFuture(
|
||||
$account,
|
||||
'rest/api/2/issue/'.phutil_escape_uri($id),
|
||||
'GET');
|
||||
}
|
||||
|
||||
$results = array();
|
||||
$failed = array();
|
||||
foreach (Futures($futures) as $key => $future) {
|
||||
try {
|
||||
$results[$key] = $future->resolveJSON();
|
||||
} catch (Exception $ex) {
|
||||
if (($ex instanceof HTTPFutureResponseStatus) &&
|
||||
($ex->getStatusCode() == 404)) {
|
||||
// This indicates that the object has been deleted (or never existed,
|
||||
// or isn't visible to the current user) but it's a successful sync of
|
||||
// an object which isn't visible.
|
||||
} else {
|
||||
// This is something else, so consider it a synchronization failure.
|
||||
phlog($ex);
|
||||
$failed[$key] = $ex;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($refs as $ref) {
|
||||
$ref->setAttribute('name', pht('JIRA %s', $ref->getObjectID()));
|
||||
|
||||
$did_fail = idx($failed, $ref->getObjectKey());
|
||||
if ($did_fail) {
|
||||
$ref->setSyncFailed(true);
|
||||
continue;
|
||||
}
|
||||
|
||||
$result = idx($results, $ref->getObjectKey());
|
||||
if (!$result) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$fields = idx($result, 'fields', array());
|
||||
|
||||
$ref->setIsVisible(true);
|
||||
$ref->setAttribute(
|
||||
'fullname',
|
||||
pht('JIRA %s %s', $result['key'], idx($fields, 'summary')));
|
||||
|
||||
$ref->setAttribute('title', idx($fields, 'summary'));
|
||||
$ref->setAttribute('description', idx($result, 'description'));
|
||||
|
||||
$obj = $ref->getExternalObject();
|
||||
if ($obj->getID()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->fillObjectFromData($obj, $result);
|
||||
|
||||
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
|
||||
$obj->save();
|
||||
unset($unguarded);
|
||||
}
|
||||
}
|
||||
|
||||
public function fillObjectFromData(DoorkeeperExternalObject $obj, $result) {
|
||||
// Convert the "self" URI, which points at the REST endpoint, into a
|
||||
// browse URI.
|
||||
$self = idx($result, 'self');
|
||||
$uri = new PhutilURI($self);
|
||||
$uri->setPath('browse/'.$obj->getObjectID());
|
||||
|
||||
$obj->setObjectURI((string)$uri);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
<?php
|
||||
|
||||
abstract class DoorkeeperRemarkupRule
|
||||
extends PhutilRemarkupRule {
|
||||
|
||||
const KEY_TAGS = 'doorkeeper.tags';
|
||||
|
||||
public function getPriority() {
|
||||
return 350.0;
|
||||
}
|
||||
|
||||
protected function addDoorkeeperTag(array $spec) {
|
||||
$key = self::KEY_TAGS;
|
||||
$engine = $this->getEngine();
|
||||
$token = $engine->storeText(get_class($this));
|
||||
|
||||
$tags = $engine->getTextMetadata($key, array());
|
||||
|
||||
$tags[] = array(
|
||||
'token' => $token,
|
||||
) + $spec + array(
|
||||
'extra' => array(),
|
||||
);
|
||||
|
||||
$engine->setTextMetadata($key, $tags);
|
||||
return $token;
|
||||
}
|
||||
|
||||
public function didMarkupText() {
|
||||
$key = self::KEY_TAGS;
|
||||
$engine = $this->getEngine();
|
||||
$tags = $engine->getTextMetadata($key, array());
|
||||
|
||||
if (!$tags) {
|
||||
return;
|
||||
}
|
||||
|
||||
$refs = array();
|
||||
foreach ($tags as $spec) {
|
||||
$tag_id = celerity_generate_unique_node_id();
|
||||
|
||||
$refs[] = array(
|
||||
'id' => $tag_id,
|
||||
) + $spec['tag'];
|
||||
|
||||
if ($this->getEngine()->isTextMode()) {
|
||||
$view = $spec['href'];
|
||||
} else {
|
||||
$view = id(new PhabricatorTagView())
|
||||
->setID($tag_id)
|
||||
->setName($spec['href'])
|
||||
->setHref($spec['href'])
|
||||
->setType(PhabricatorTagView::TYPE_OBJECT)
|
||||
->setExternal(true);
|
||||
}
|
||||
|
||||
$engine->overwriteStoredText($spec['token'], $view);
|
||||
}
|
||||
|
||||
Javelin::initBehavior('doorkeeper-tag', array('tags' => $refs));
|
||||
|
||||
$engine->setTextMetadata($key, array());
|
||||
}
|
||||
|
||||
}
|
|
@ -1,13 +1,7 @@
|
|||
<?php
|
||||
|
||||
final class DoorkeeperRemarkupRuleAsana
|
||||
extends PhutilRemarkupRule {
|
||||
|
||||
const KEY_TAGS = 'doorkeeper.tags';
|
||||
|
||||
public function getPriority() {
|
||||
return 350.0;
|
||||
}
|
||||
extends DoorkeeperRemarkupRule {
|
||||
|
||||
public function apply($text) {
|
||||
return preg_replace_callback(
|
||||
|
@ -17,14 +11,8 @@ final class DoorkeeperRemarkupRuleAsana
|
|||
}
|
||||
|
||||
public function markupAsanaLink($matches) {
|
||||
$key = self::KEY_TAGS;
|
||||
$engine = $this->getEngine();
|
||||
$token = $engine->storeText('AsanaDoorkeeper');
|
||||
|
||||
$tags = $engine->getTextMetadata($key, array());
|
||||
|
||||
$tags[] = array(
|
||||
'token' => $token,
|
||||
return $this->addDoorkeeperTag(
|
||||
array(
|
||||
'href' => $matches[0],
|
||||
'tag' => array(
|
||||
'ref' => array(
|
||||
|
@ -37,47 +25,7 @@ final class DoorkeeperRemarkupRuleAsana
|
|||
'asana.context' => $matches[1],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
$engine->setTextMetadata($key, $tags);
|
||||
|
||||
return $token;
|
||||
}
|
||||
|
||||
public function didMarkupText() {
|
||||
$key = self::KEY_TAGS;
|
||||
$engine = $this->getEngine();
|
||||
$tags = $engine->getTextMetadata($key, array());
|
||||
|
||||
if (!$tags) {
|
||||
return;
|
||||
}
|
||||
|
||||
$refs = array();
|
||||
foreach ($tags as $spec) {
|
||||
$tag_id = celerity_generate_unique_node_id();
|
||||
|
||||
$refs[] = array(
|
||||
'id' => $tag_id,
|
||||
) + $spec['tag'];
|
||||
|
||||
if ($this->getEngine()->isTextMode()) {
|
||||
$view = $spec['href'];
|
||||
} else {
|
||||
$view = id(new PhabricatorTagView())
|
||||
->setID($tag_id)
|
||||
->setName($spec['href'])
|
||||
->setHref($spec['href'])
|
||||
->setType(PhabricatorTagView::TYPE_OBJECT)
|
||||
->setExternal(true);
|
||||
}
|
||||
|
||||
$engine->overwriteStoredText($spec['token'], $view);
|
||||
}
|
||||
|
||||
Javelin::initBehavior('doorkeeper-tag', array('tags' => $refs));
|
||||
|
||||
$engine->setTextMetadata($key, array());
|
||||
));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
<?php
|
||||
|
||||
final class DoorkeeperRemarkupRuleJIRA
|
||||
extends DoorkeeperRemarkupRule {
|
||||
|
||||
public function apply($text) {
|
||||
return preg_replace_callback(
|
||||
'@(https?://[^/]+)/browse/([A-Z]+-[1-9]\d*)@',
|
||||
array($this, 'markupJIRALink'),
|
||||
$text);
|
||||
}
|
||||
|
||||
public function markupJIRALink($matches) {
|
||||
|
||||
$match_domain = $matches[1];
|
||||
$match_issue = $matches[2];
|
||||
|
||||
// TODO: When we support multiple instances, deal with them here.
|
||||
$provider = PhabricatorAuthProviderOAuth1JIRA::getJIRAProvider();
|
||||
if (!$provider) {
|
||||
return $matches[0];
|
||||
}
|
||||
|
||||
$jira_base = $provider->getJIRABaseURI();
|
||||
if ($match_domain != rtrim($jira_base, '/')) {
|
||||
return $matches[0];
|
||||
}
|
||||
|
||||
return $this->addDoorkeeperTag(
|
||||
array(
|
||||
'href' => $matches[0],
|
||||
'tag' => array(
|
||||
'ref' => array(
|
||||
DoorkeeperBridgeJIRA::APPTYPE_JIRA,
|
||||
$provider->getProviderDomain(),
|
||||
DoorkeeperBridgeJIRA::OBJTYPE_ISSUE,
|
||||
$match_issue,
|
||||
),
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
}
|
Loading…
Reference in a new issue