1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-11-10 08:52:39 +01:00

Implement Asana and JIRA external links via HyperlinkEngineExtension, not separate Remarkup rules

Summary:
Depends on D20527. Ref T13291. Now that we have more flexible support for URI rewriting, use it for Doorkeeper URIs.

These are used when you set up Asana or JIRA and include the URI to an Asana task or a JIRA issue in a comment.

Test Plan:
  - Linked up to Asana and JIRA.
  - Put Asana and JIRA URIs in comments.
  - Saw the UI update to pull task titles from Asana / JIRA using my OAuth credentials.

Reviewers: amckinley

Reviewed By: amckinley

Maniphest Tasks: T13291

Differential Revision: https://secure.phabricator.com/D20528
This commit is contained in:
epriestley 2019-05-20 10:33:55 -07:00
parent 33688c8a41
commit f420159380
10 changed files with 260 additions and 199 deletions

View file

@ -1072,7 +1072,6 @@ phutil_register_library_map(array(
'DivinerSymbolRemarkupRule' => 'applications/diviner/markup/DivinerSymbolRemarkupRule.php',
'DivinerWorkflow' => 'applications/diviner/workflow/DivinerWorkflow.php',
'DoorkeeperAsanaFeedWorker' => 'applications/doorkeeper/worker/DoorkeeperAsanaFeedWorker.php',
'DoorkeeperAsanaRemarkupRule' => 'applications/doorkeeper/remarkup/DoorkeeperAsanaRemarkupRule.php',
'DoorkeeperBridge' => 'applications/doorkeeper/bridge/DoorkeeperBridge.php',
'DoorkeeperBridgeAsana' => 'applications/doorkeeper/bridge/DoorkeeperBridgeAsana.php',
'DoorkeeperBridgeGitHub' => 'applications/doorkeeper/bridge/DoorkeeperBridgeGitHub.php',
@ -1088,15 +1087,16 @@ phutil_register_library_map(array(
'DoorkeeperExternalObjectQuery' => 'applications/doorkeeper/query/DoorkeeperExternalObjectQuery.php',
'DoorkeeperFeedStoryPublisher' => 'applications/doorkeeper/engine/DoorkeeperFeedStoryPublisher.php',
'DoorkeeperFeedWorker' => 'applications/doorkeeper/worker/DoorkeeperFeedWorker.php',
'DoorkeeperHyperlinkEngineExtension' => 'applications/doorkeeper/engineextension/DoorkeeperHyperlinkEngineExtension.php',
'DoorkeeperImportEngine' => 'applications/doorkeeper/engine/DoorkeeperImportEngine.php',
'DoorkeeperJIRAFeedWorker' => 'applications/doorkeeper/worker/DoorkeeperJIRAFeedWorker.php',
'DoorkeeperJIRARemarkupRule' => 'applications/doorkeeper/remarkup/DoorkeeperJIRARemarkupRule.php',
'DoorkeeperMissingLinkException' => 'applications/doorkeeper/exception/DoorkeeperMissingLinkException.php',
'DoorkeeperObjectRef' => 'applications/doorkeeper/engine/DoorkeeperObjectRef.php',
'DoorkeeperRemarkupRule' => 'applications/doorkeeper/remarkup/DoorkeeperRemarkupRule.php',
'DoorkeeperRemarkupURIInterface' => 'applications/doorkeeper/interface/DoorkeeperRemarkupURIInterface.php',
'DoorkeeperSchemaSpec' => 'applications/doorkeeper/storage/DoorkeeperSchemaSpec.php',
'DoorkeeperTagView' => 'applications/doorkeeper/view/DoorkeeperTagView.php',
'DoorkeeperTagsController' => 'applications/doorkeeper/controller/DoorkeeperTagsController.php',
'DoorkeeperURIRef' => 'applications/doorkeeper/engine/DoorkeeperURIRef.php',
'DrydockAcquiredBrokenResourceException' => 'applications/drydock/exception/DrydockAcquiredBrokenResourceException.php',
'DrydockAlmanacServiceHostBlueprintImplementation' => 'applications/drydock/blueprint/DrydockAlmanacServiceHostBlueprintImplementation.php',
'DrydockApacheWebrootInterface' => 'applications/drydock/interface/webroot/DrydockApacheWebrootInterface.php',
@ -6761,7 +6761,6 @@ phutil_register_library_map(array(
'DivinerSymbolRemarkupRule' => 'PhutilRemarkupRule',
'DivinerWorkflow' => 'PhabricatorManagementWorkflow',
'DoorkeeperAsanaFeedWorker' => 'DoorkeeperFeedWorker',
'DoorkeeperAsanaRemarkupRule' => 'DoorkeeperRemarkupRule',
'DoorkeeperBridge' => 'Phobject',
'DoorkeeperBridgeAsana' => 'DoorkeeperBridge',
'DoorkeeperBridgeGitHub' => 'DoorkeeperBridge',
@ -6779,15 +6778,15 @@ phutil_register_library_map(array(
'DoorkeeperExternalObjectQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'DoorkeeperFeedStoryPublisher' => 'Phobject',
'DoorkeeperFeedWorker' => 'FeedPushWorker',
'DoorkeeperHyperlinkEngineExtension' => 'PhutilRemarkupHyperlinkEngineExtension',
'DoorkeeperImportEngine' => 'Phobject',
'DoorkeeperJIRAFeedWorker' => 'DoorkeeperFeedWorker',
'DoorkeeperJIRARemarkupRule' => 'DoorkeeperRemarkupRule',
'DoorkeeperMissingLinkException' => 'Exception',
'DoorkeeperObjectRef' => 'Phobject',
'DoorkeeperRemarkupRule' => 'PhutilRemarkupRule',
'DoorkeeperSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'DoorkeeperTagView' => 'AphrontView',
'DoorkeeperTagsController' => 'PhabricatorController',
'DoorkeeperURIRef' => 'Phobject',
'DrydockAcquiredBrokenResourceException' => 'Exception',
'DrydockAlmanacServiceHostBlueprintImplementation' => 'DrydockBlueprintImplementation',
'DrydockApacheWebrootInterface' => 'DrydockWebrootInterface',
@ -8090,7 +8089,10 @@ phutil_register_library_map(array(
'PhabricatorApplicationsController' => 'PhabricatorController',
'PhabricatorApplicationsListController' => 'PhabricatorApplicationsController',
'PhabricatorApplyEditField' => 'PhabricatorEditField',
'PhabricatorAsanaAuthProvider' => 'PhabricatorOAuth2AuthProvider',
'PhabricatorAsanaAuthProvider' => array(
'PhabricatorOAuth2AuthProvider',
'DoorkeeperRemarkupURIInterface',
),
'PhabricatorAsanaConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorAsanaSubtaskHasObjectEdgeType' => 'PhabricatorEdgeType',
'PhabricatorAsanaTaskHasObjectEdgeType' => 'PhabricatorEdgeType',
@ -9569,7 +9571,10 @@ phutil_register_library_map(array(
'PhabricatorIteratedMD5PasswordHasher' => 'PhabricatorPasswordHasher',
'PhabricatorIteratedMD5PasswordHasherTestCase' => 'PhabricatorTestCase',
'PhabricatorIteratorFileUploadSource' => 'PhabricatorFileUploadSource',
'PhabricatorJIRAAuthProvider' => 'PhabricatorOAuth1AuthProvider',
'PhabricatorJIRAAuthProvider' => array(
'PhabricatorOAuth1AuthProvider',
'DoorkeeperRemarkupURIInterface',
),
'PhabricatorJSONConfigType' => 'PhabricatorTextConfigType',
'PhabricatorJSONDocumentEngine' => 'PhabricatorTextDocumentEngine',
'PhabricatorJSONExportFormat' => 'PhabricatorExportFormat',

View file

@ -1,6 +1,8 @@
<?php
final class PhabricatorAsanaAuthProvider extends PhabricatorOAuth2AuthProvider {
final class PhabricatorAsanaAuthProvider
extends PhabricatorOAuth2AuthProvider
implements DoorkeeperRemarkupURIInterface {
public function getProviderName() {
return pht('Asana');
@ -46,4 +48,26 @@ final class PhabricatorAsanaAuthProvider extends PhabricatorOAuth2AuthProvider {
return null;
}
/* -( DoorkeeperRemarkupURIInterface )------------------------------------- */
public function getDoorkeeperURIRef(PhutilURI $uri) {
$uri_string = phutil_string_cast($uri);
$pattern = '(https://app\\.asana\\.com/0/(\\d+)/(\\d+))';
$matches = null;
if (!preg_match($pattern, $uri_string, $matches)) {
return null;
}
$context_id = $matches[1];
$task_id = $matches[2];
return id(new DoorkeeperURIRef())
->setURI($uri)
->setApplicationType(DoorkeeperBridgeAsana::APPTYPE_ASANA)
->setApplicationDomain(DoorkeeperBridgeAsana::APPDOMAIN_ASANA)
->setObjectType(DoorkeeperBridgeAsana::OBJTYPE_TASK)
->setObjectID($task_id);
}
}

View file

@ -1,10 +1,8 @@
<?php
final class PhabricatorJIRAAuthProvider extends PhabricatorOAuth1AuthProvider {
public function getJIRABaseURI() {
return $this->getProviderConfig()->getProperty(self::PROPERTY_JIRA_URI);
}
final class PhabricatorJIRAAuthProvider
extends PhabricatorOAuth1AuthProvider
implements DoorkeeperRemarkupURIInterface {
public function getProviderName() {
return pht('JIRA');
@ -332,4 +330,33 @@ final class PhabricatorJIRAAuthProvider extends PhabricatorOAuth1AuthProvider {
return $config->getProperty(self::PROPERTY_REPORT_COMMENT, true);
}
/* -( DoorkeeperRemarkupURIInterface )------------------------------------- */
public function getDoorkeeperURIRef(PhutilURI $uri) {
$uri_string = phutil_string_cast($uri);
$pattern = '((https?://\S+?)/browse/([A-Z]+-[1-9]\d*))';
$matches = null;
if (!preg_match($pattern, $uri_string, $matches)) {
return null;
}
$domain = $matches[1];
$issue = $matches[2];
$config = $this->getProviderConfig();
$base_uri = $config->getProperty(self::PROPERTY_JIRA_URI);
if ($domain !== rtrim($base_uri, '/')) {
return null;
}
return id(new DoorkeeperURIRef())
->setURI($uri)
->setApplicationType(DoorkeeperBridgeJIRA::APPTYPE_JIRA)
->setApplicationDomain($this->getProviderDomain())
->setObjectType(DoorkeeperBridgeJIRA::OBJTYPE_ISSUE)
->setObjectID($issue);
}
}

View file

@ -22,13 +22,6 @@ final class PhabricatorDoorkeeperApplication extends PhabricatorApplication {
return pht('Connect to Other Software');
}
public function getRemarkupRules() {
return array(
new DoorkeeperAsanaRemarkupRule(),
new DoorkeeperJIRARemarkupRule(),
);
}
public function getRoutes() {
return array(
'/doorkeeper/' => array(

View file

@ -0,0 +1,91 @@
<?php
final class DoorkeeperURIRef extends Phobject {
private $uri;
private $applicationType;
private $applicationDomain;
private $objectType;
private $objectID;
private $text;
private $displayMode = self::DISPLAY_FULL;
const DISPLAY_FULL = 'full';
const DISPLAY_SHORT = 'short';
public function setURI(PhutilURI $uri) {
$this->uri = $uri;
return $this;
}
public function getURI() {
return $this->uri;
}
public function setApplicationType($application_type) {
$this->applicationType = $application_type;
return $this;
}
public function getApplicationType() {
return $this->applicationType;
}
public function setApplicationDomain($application_domain) {
$this->applicationDomain = $application_domain;
return $this;
}
public function getApplicationDomain() {
return $this->applicationDomain;
}
public function setObjectType($object_type) {
$this->objectType = $object_type;
return $this;
}
public function getObjectType() {
return $this->objectType;
}
public function setObjectID($object_id) {
$this->objectID = $object_id;
return $this;
}
public function getObjectID() {
return $this->objectID;
}
public function setText($text) {
$this->text = $text;
return $this;
}
public function getText() {
return $this->text;
}
public function setDisplayMode($display_mode) {
$options = array(
self::DISPLAY_FULL => true,
self::DISPLAY_SHORT => true,
);
if (!isset($options[$display_mode])) {
throw new Exception(
pht(
'DoorkeeperURIRef display mode "%s" is unknown.',
$display_mode));
}
$this->displayMode = $display_mode;
return $this;
}
public function getDisplayMode() {
return $this->displayMode;
}
}

View file

@ -0,0 +1,92 @@
<?php
final class DoorkeeperHyperlinkEngineExtension
extends PhutilRemarkupHyperlinkEngineExtension {
const LINKENGINEKEY = 'doorkeeper';
public function processHyperlinks(array $hyperlinks) {
$engine = $this->getEngine();
$viewer = $engine->getConfig('viewer');
if (!$viewer) {
return;
}
$configs = id(new PhabricatorAuthProviderConfigQuery())
->setViewer($viewer)
->withIsEnabled(true)
->execute();
$providers = array();
foreach ($configs as $key => $config) {
$provider = $config->getProvider();
if (($provider instanceof DoorkeeperRemarkupURIInterface)) {
$providers[] = $provider;
}
}
if (!$providers) {
return;
}
$refs = array();
foreach ($hyperlinks as $hyperlink) {
$uri = $hyperlink->getURI();
$uri = new PhutilURI($uri);
foreach ($providers as $provider) {
$ref = $provider->getDoorkeeperURIRef($uri);
if (($ref !== null) && !($ref instanceof DoorkeeperURIRef)) {
throw new Exception(
pht(
'Expected "getDoorkeeperURIRef()" to return "null" or an '.
'object of type "DoorkeeperURIRef", but got %s from provider '.
'"%s".',
phutil_describe_type($ref),
get_class($provider)));
}
if ($ref === null) {
continue;
}
$tag_id = celerity_generate_unique_node_id();
$href = phutil_string_cast($ref->getURI());
$refs[] = array(
'id' => $tag_id,
'href' => $href,
'ref' => array(
$ref->getApplicationType(),
$ref->getApplicationDomain(),
$ref->getObjectType(),
$ref->getObjectID(),
),
'view' => $ref->getDisplayMode(),
);
$text = $ref->getText();
if ($text === null) {
$text = $href;
}
$view = id(new PHUITagView())
->setID($tag_id)
->setName($text)
->setHref($href)
->setType(PHUITagView::TYPE_OBJECT)
->setExternal(true);
$hyperlink->setResult($view);
break;
}
}
if ($refs) {
Javelin::initBehavior('doorkeeper-tag', array('tags' => $refs));
}
}
}

View file

@ -0,0 +1,7 @@
<?php
interface DoorkeeperRemarkupURIInterface {
public function getDoorkeeperURIRef(PhutilURI $uri);
}

View file

@ -1,31 +0,0 @@
<?php
final class DoorkeeperAsanaRemarkupRule
extends DoorkeeperRemarkupRule {
public function apply($text) {
return preg_replace_callback(
'@https://app\\.asana\\.com/0/(\\d+)/(\\d+)@',
array($this, 'markupAsanaLink'),
$text);
}
public function markupAsanaLink($matches) {
return $this->addDoorkeeperTag(
array(
'href' => $matches[0],
'tag' => array(
'ref' => array(
DoorkeeperBridgeAsana::APPTYPE_ASANA,
DoorkeeperBridgeAsana::APPDOMAIN_ASANA,
DoorkeeperBridgeAsana::OBJTYPE_TASK,
$matches[2],
),
'extra' => array(
'asana.context' => $matches[1],
),
),
));
}
}

View file

@ -1,44 +0,0 @@
<?php
final class DoorkeeperJIRARemarkupRule
extends DoorkeeperRemarkupRule {
public function apply($text) {
return preg_replace_callback(
'@(https?://\S+?)/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 = PhabricatorJIRAAuthProvider::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,
),
),
));
}
}

View file

@ -1,103 +0,0 @@
<?php
abstract class DoorkeeperRemarkupRule extends PhutilRemarkupRule {
const KEY_TAGS = 'doorkeeper.tags';
const VIEW_FULL = 'full';
const VIEW_SHORT = 'short';
public function getPriority() {
return 350.0;
}
protected function addDoorkeeperTag(array $spec) {
PhutilTypeSpec::checkMap(
$spec,
array(
'href' => 'string',
'tag' => 'map<string, wild>',
'name' => 'optional string',
'view' => 'optional string',
));
$spec = $spec + array(
'view' => self::VIEW_FULL,
);
$views = array(
self::VIEW_FULL,
self::VIEW_SHORT,
);
$views = array_fuse($views);
if (!isset($views[$spec['view']])) {
throw new Exception(
pht(
'Unsupported Doorkeeper tag view mode "%s". Supported modes are: %s.',
$spec['view'],
implode(', ', $views)));
}
$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) {
$href = $spec['href'];
$name = idx($spec, 'name', $href);
$this->assertFlatText($href);
$this->assertFlatText($name);
if ($this->getEngine()->isTextMode()) {
$view = "{$name} <{$href}>";
} else {
$tag_id = celerity_generate_unique_node_id();
$refs[] = array(
'id' => $tag_id,
'view' => $spec['view'],
) + $spec['tag'];
$view = id(new PHUITagView())
->setID($tag_id)
->setName($name)
->setHref($href)
->setType(PHUITagView::TYPE_OBJECT)
->setExternal(true);
}
$engine->overwriteStoredText($spec['token'], $view);
}
if ($refs) {
Javelin::initBehavior('doorkeeper-tag', array('tags' => $refs));
}
$engine->setTextMetadata($key, array());
}
}