@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator

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

+260 -199
+13 -8
src/__phutil_library_map__.php
··· 1072 1072 'DivinerSymbolRemarkupRule' => 'applications/diviner/markup/DivinerSymbolRemarkupRule.php', 1073 1073 'DivinerWorkflow' => 'applications/diviner/workflow/DivinerWorkflow.php', 1074 1074 'DoorkeeperAsanaFeedWorker' => 'applications/doorkeeper/worker/DoorkeeperAsanaFeedWorker.php', 1075 - 'DoorkeeperAsanaRemarkupRule' => 'applications/doorkeeper/remarkup/DoorkeeperAsanaRemarkupRule.php', 1076 1075 'DoorkeeperBridge' => 'applications/doorkeeper/bridge/DoorkeeperBridge.php', 1077 1076 'DoorkeeperBridgeAsana' => 'applications/doorkeeper/bridge/DoorkeeperBridgeAsana.php', 1078 1077 'DoorkeeperBridgeGitHub' => 'applications/doorkeeper/bridge/DoorkeeperBridgeGitHub.php', ··· 1088 1087 'DoorkeeperExternalObjectQuery' => 'applications/doorkeeper/query/DoorkeeperExternalObjectQuery.php', 1089 1088 'DoorkeeperFeedStoryPublisher' => 'applications/doorkeeper/engine/DoorkeeperFeedStoryPublisher.php', 1090 1089 'DoorkeeperFeedWorker' => 'applications/doorkeeper/worker/DoorkeeperFeedWorker.php', 1090 + 'DoorkeeperHyperlinkEngineExtension' => 'applications/doorkeeper/engineextension/DoorkeeperHyperlinkEngineExtension.php', 1091 1091 'DoorkeeperImportEngine' => 'applications/doorkeeper/engine/DoorkeeperImportEngine.php', 1092 1092 'DoorkeeperJIRAFeedWorker' => 'applications/doorkeeper/worker/DoorkeeperJIRAFeedWorker.php', 1093 - 'DoorkeeperJIRARemarkupRule' => 'applications/doorkeeper/remarkup/DoorkeeperJIRARemarkupRule.php', 1094 1093 'DoorkeeperMissingLinkException' => 'applications/doorkeeper/exception/DoorkeeperMissingLinkException.php', 1095 1094 'DoorkeeperObjectRef' => 'applications/doorkeeper/engine/DoorkeeperObjectRef.php', 1096 - 'DoorkeeperRemarkupRule' => 'applications/doorkeeper/remarkup/DoorkeeperRemarkupRule.php', 1095 + 'DoorkeeperRemarkupURIInterface' => 'applications/doorkeeper/interface/DoorkeeperRemarkupURIInterface.php', 1097 1096 'DoorkeeperSchemaSpec' => 'applications/doorkeeper/storage/DoorkeeperSchemaSpec.php', 1098 1097 'DoorkeeperTagView' => 'applications/doorkeeper/view/DoorkeeperTagView.php', 1099 1098 'DoorkeeperTagsController' => 'applications/doorkeeper/controller/DoorkeeperTagsController.php', 1099 + 'DoorkeeperURIRef' => 'applications/doorkeeper/engine/DoorkeeperURIRef.php', 1100 1100 'DrydockAcquiredBrokenResourceException' => 'applications/drydock/exception/DrydockAcquiredBrokenResourceException.php', 1101 1101 'DrydockAlmanacServiceHostBlueprintImplementation' => 'applications/drydock/blueprint/DrydockAlmanacServiceHostBlueprintImplementation.php', 1102 1102 'DrydockApacheWebrootInterface' => 'applications/drydock/interface/webroot/DrydockApacheWebrootInterface.php', ··· 6761 6761 'DivinerSymbolRemarkupRule' => 'PhutilRemarkupRule', 6762 6762 'DivinerWorkflow' => 'PhabricatorManagementWorkflow', 6763 6763 'DoorkeeperAsanaFeedWorker' => 'DoorkeeperFeedWorker', 6764 - 'DoorkeeperAsanaRemarkupRule' => 'DoorkeeperRemarkupRule', 6765 6764 'DoorkeeperBridge' => 'Phobject', 6766 6765 'DoorkeeperBridgeAsana' => 'DoorkeeperBridge', 6767 6766 'DoorkeeperBridgeGitHub' => 'DoorkeeperBridge', ··· 6779 6778 'DoorkeeperExternalObjectQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 6780 6779 'DoorkeeperFeedStoryPublisher' => 'Phobject', 6781 6780 'DoorkeeperFeedWorker' => 'FeedPushWorker', 6781 + 'DoorkeeperHyperlinkEngineExtension' => 'PhutilRemarkupHyperlinkEngineExtension', 6782 6782 'DoorkeeperImportEngine' => 'Phobject', 6783 6783 'DoorkeeperJIRAFeedWorker' => 'DoorkeeperFeedWorker', 6784 - 'DoorkeeperJIRARemarkupRule' => 'DoorkeeperRemarkupRule', 6785 6784 'DoorkeeperMissingLinkException' => 'Exception', 6786 6785 'DoorkeeperObjectRef' => 'Phobject', 6787 - 'DoorkeeperRemarkupRule' => 'PhutilRemarkupRule', 6788 6786 'DoorkeeperSchemaSpec' => 'PhabricatorConfigSchemaSpec', 6789 6787 'DoorkeeperTagView' => 'AphrontView', 6790 6788 'DoorkeeperTagsController' => 'PhabricatorController', 6789 + 'DoorkeeperURIRef' => 'Phobject', 6791 6790 'DrydockAcquiredBrokenResourceException' => 'Exception', 6792 6791 'DrydockAlmanacServiceHostBlueprintImplementation' => 'DrydockBlueprintImplementation', 6793 6792 'DrydockApacheWebrootInterface' => 'DrydockWebrootInterface', ··· 8090 8089 'PhabricatorApplicationsController' => 'PhabricatorController', 8091 8090 'PhabricatorApplicationsListController' => 'PhabricatorApplicationsController', 8092 8091 'PhabricatorApplyEditField' => 'PhabricatorEditField', 8093 - 'PhabricatorAsanaAuthProvider' => 'PhabricatorOAuth2AuthProvider', 8092 + 'PhabricatorAsanaAuthProvider' => array( 8093 + 'PhabricatorOAuth2AuthProvider', 8094 + 'DoorkeeperRemarkupURIInterface', 8095 + ), 8094 8096 'PhabricatorAsanaConfigOptions' => 'PhabricatorApplicationConfigOptions', 8095 8097 'PhabricatorAsanaSubtaskHasObjectEdgeType' => 'PhabricatorEdgeType', 8096 8098 'PhabricatorAsanaTaskHasObjectEdgeType' => 'PhabricatorEdgeType', ··· 9569 9571 'PhabricatorIteratedMD5PasswordHasher' => 'PhabricatorPasswordHasher', 9570 9572 'PhabricatorIteratedMD5PasswordHasherTestCase' => 'PhabricatorTestCase', 9571 9573 'PhabricatorIteratorFileUploadSource' => 'PhabricatorFileUploadSource', 9572 - 'PhabricatorJIRAAuthProvider' => 'PhabricatorOAuth1AuthProvider', 9574 + 'PhabricatorJIRAAuthProvider' => array( 9575 + 'PhabricatorOAuth1AuthProvider', 9576 + 'DoorkeeperRemarkupURIInterface', 9577 + ), 9573 9578 'PhabricatorJSONConfigType' => 'PhabricatorTextConfigType', 9574 9579 'PhabricatorJSONDocumentEngine' => 'PhabricatorTextDocumentEngine', 9575 9580 'PhabricatorJSONExportFormat' => 'PhabricatorExportFormat',
+25 -1
src/applications/auth/provider/PhabricatorAsanaAuthProvider.php
··· 1 1 <?php 2 2 3 - final class PhabricatorAsanaAuthProvider extends PhabricatorOAuth2AuthProvider { 3 + final class PhabricatorAsanaAuthProvider 4 + extends PhabricatorOAuth2AuthProvider 5 + implements DoorkeeperRemarkupURIInterface { 4 6 5 7 public function getProviderName() { 6 8 return pht('Asana'); ··· 44 46 } 45 47 46 48 return null; 49 + } 50 + 51 + /* -( DoorkeeperRemarkupURIInterface )------------------------------------- */ 52 + 53 + public function getDoorkeeperURIRef(PhutilURI $uri) { 54 + $uri_string = phutil_string_cast($uri); 55 + 56 + $pattern = '(https://app\\.asana\\.com/0/(\\d+)/(\\d+))'; 57 + $matches = null; 58 + if (!preg_match($pattern, $uri_string, $matches)) { 59 + return null; 60 + } 61 + 62 + $context_id = $matches[1]; 63 + $task_id = $matches[2]; 64 + 65 + return id(new DoorkeeperURIRef()) 66 + ->setURI($uri) 67 + ->setApplicationType(DoorkeeperBridgeAsana::APPTYPE_ASANA) 68 + ->setApplicationDomain(DoorkeeperBridgeAsana::APPDOMAIN_ASANA) 69 + ->setObjectType(DoorkeeperBridgeAsana::OBJTYPE_TASK) 70 + ->setObjectID($task_id); 47 71 } 48 72 49 73 }
+32 -5
src/applications/auth/provider/PhabricatorJIRAAuthProvider.php
··· 1 1 <?php 2 2 3 - final class PhabricatorJIRAAuthProvider extends PhabricatorOAuth1AuthProvider { 4 - 5 - public function getJIRABaseURI() { 6 - return $this->getProviderConfig()->getProperty(self::PROPERTY_JIRA_URI); 7 - } 3 + final class PhabricatorJIRAAuthProvider 4 + extends PhabricatorOAuth1AuthProvider 5 + implements DoorkeeperRemarkupURIInterface { 8 6 9 7 public function getProviderName() { 10 8 return pht('JIRA'); ··· 330 328 public function shouldCreateJIRAComment() { 331 329 $config = $this->getProviderConfig(); 332 330 return $config->getProperty(self::PROPERTY_REPORT_COMMENT, true); 331 + } 332 + 333 + /* -( DoorkeeperRemarkupURIInterface )------------------------------------- */ 334 + 335 + public function getDoorkeeperURIRef(PhutilURI $uri) { 336 + $uri_string = phutil_string_cast($uri); 337 + 338 + $pattern = '((https?://\S+?)/browse/([A-Z]+-[1-9]\d*))'; 339 + $matches = null; 340 + if (!preg_match($pattern, $uri_string, $matches)) { 341 + return null; 342 + } 343 + 344 + $domain = $matches[1]; 345 + $issue = $matches[2]; 346 + 347 + $config = $this->getProviderConfig(); 348 + $base_uri = $config->getProperty(self::PROPERTY_JIRA_URI); 349 + 350 + if ($domain !== rtrim($base_uri, '/')) { 351 + return null; 352 + } 353 + 354 + return id(new DoorkeeperURIRef()) 355 + ->setURI($uri) 356 + ->setApplicationType(DoorkeeperBridgeJIRA::APPTYPE_JIRA) 357 + ->setApplicationDomain($this->getProviderDomain()) 358 + ->setObjectType(DoorkeeperBridgeJIRA::OBJTYPE_ISSUE) 359 + ->setObjectID($issue); 333 360 } 334 361 335 362 }
-7
src/applications/doorkeeper/application/PhabricatorDoorkeeperApplication.php
··· 22 22 return pht('Connect to Other Software'); 23 23 } 24 24 25 - public function getRemarkupRules() { 26 - return array( 27 - new DoorkeeperAsanaRemarkupRule(), 28 - new DoorkeeperJIRARemarkupRule(), 29 - ); 30 - } 31 - 32 25 public function getRoutes() { 33 26 return array( 34 27 '/doorkeeper/' => array(
+91
src/applications/doorkeeper/engine/DoorkeeperURIRef.php
··· 1 + <?php 2 + 3 + final class DoorkeeperURIRef extends Phobject { 4 + 5 + private $uri; 6 + private $applicationType; 7 + private $applicationDomain; 8 + private $objectType; 9 + private $objectID; 10 + private $text; 11 + private $displayMode = self::DISPLAY_FULL; 12 + 13 + const DISPLAY_FULL = 'full'; 14 + const DISPLAY_SHORT = 'short'; 15 + 16 + public function setURI(PhutilURI $uri) { 17 + $this->uri = $uri; 18 + return $this; 19 + } 20 + 21 + public function getURI() { 22 + return $this->uri; 23 + } 24 + 25 + public function setApplicationType($application_type) { 26 + $this->applicationType = $application_type; 27 + return $this; 28 + } 29 + 30 + public function getApplicationType() { 31 + return $this->applicationType; 32 + } 33 + 34 + public function setApplicationDomain($application_domain) { 35 + $this->applicationDomain = $application_domain; 36 + return $this; 37 + } 38 + 39 + public function getApplicationDomain() { 40 + return $this->applicationDomain; 41 + } 42 + 43 + public function setObjectType($object_type) { 44 + $this->objectType = $object_type; 45 + return $this; 46 + } 47 + 48 + public function getObjectType() { 49 + return $this->objectType; 50 + } 51 + 52 + public function setObjectID($object_id) { 53 + $this->objectID = $object_id; 54 + return $this; 55 + } 56 + 57 + public function getObjectID() { 58 + return $this->objectID; 59 + } 60 + 61 + public function setText($text) { 62 + $this->text = $text; 63 + return $this; 64 + } 65 + 66 + public function getText() { 67 + return $this->text; 68 + } 69 + 70 + public function setDisplayMode($display_mode) { 71 + $options = array( 72 + self::DISPLAY_FULL => true, 73 + self::DISPLAY_SHORT => true, 74 + ); 75 + 76 + if (!isset($options[$display_mode])) { 77 + throw new Exception( 78 + pht( 79 + 'DoorkeeperURIRef display mode "%s" is unknown.', 80 + $display_mode)); 81 + } 82 + 83 + $this->displayMode = $display_mode; 84 + return $this; 85 + } 86 + 87 + public function getDisplayMode() { 88 + return $this->displayMode; 89 + } 90 + 91 + }
+92
src/applications/doorkeeper/engineextension/DoorkeeperHyperlinkEngineExtension.php
··· 1 + <?php 2 + 3 + final class DoorkeeperHyperlinkEngineExtension 4 + extends PhutilRemarkupHyperlinkEngineExtension { 5 + 6 + const LINKENGINEKEY = 'doorkeeper'; 7 + 8 + public function processHyperlinks(array $hyperlinks) { 9 + $engine = $this->getEngine(); 10 + $viewer = $engine->getConfig('viewer'); 11 + 12 + if (!$viewer) { 13 + return; 14 + } 15 + 16 + $configs = id(new PhabricatorAuthProviderConfigQuery()) 17 + ->setViewer($viewer) 18 + ->withIsEnabled(true) 19 + ->execute(); 20 + 21 + $providers = array(); 22 + foreach ($configs as $key => $config) { 23 + $provider = $config->getProvider(); 24 + if (($provider instanceof DoorkeeperRemarkupURIInterface)) { 25 + $providers[] = $provider; 26 + } 27 + } 28 + 29 + if (!$providers) { 30 + return; 31 + } 32 + 33 + $refs = array(); 34 + foreach ($hyperlinks as $hyperlink) { 35 + $uri = $hyperlink->getURI(); 36 + $uri = new PhutilURI($uri); 37 + 38 + foreach ($providers as $provider) { 39 + $ref = $provider->getDoorkeeperURIRef($uri); 40 + 41 + if (($ref !== null) && !($ref instanceof DoorkeeperURIRef)) { 42 + throw new Exception( 43 + pht( 44 + 'Expected "getDoorkeeperURIRef()" to return "null" or an '. 45 + 'object of type "DoorkeeperURIRef", but got %s from provider '. 46 + '"%s".', 47 + phutil_describe_type($ref), 48 + get_class($provider))); 49 + } 50 + 51 + if ($ref === null) { 52 + continue; 53 + } 54 + 55 + $tag_id = celerity_generate_unique_node_id(); 56 + $href = phutil_string_cast($ref->getURI()); 57 + 58 + $refs[] = array( 59 + 'id' => $tag_id, 60 + 'href' => $href, 61 + 'ref' => array( 62 + $ref->getApplicationType(), 63 + $ref->getApplicationDomain(), 64 + $ref->getObjectType(), 65 + $ref->getObjectID(), 66 + ), 67 + 'view' => $ref->getDisplayMode(), 68 + ); 69 + 70 + $text = $ref->getText(); 71 + if ($text === null) { 72 + $text = $href; 73 + } 74 + 75 + $view = id(new PHUITagView()) 76 + ->setID($tag_id) 77 + ->setName($text) 78 + ->setHref($href) 79 + ->setType(PHUITagView::TYPE_OBJECT) 80 + ->setExternal(true); 81 + 82 + $hyperlink->setResult($view); 83 + break; 84 + } 85 + } 86 + 87 + if ($refs) { 88 + Javelin::initBehavior('doorkeeper-tag', array('tags' => $refs)); 89 + } 90 + } 91 + 92 + }
+7
src/applications/doorkeeper/interface/DoorkeeperRemarkupURIInterface.php
··· 1 + <?php 2 + 3 + interface DoorkeeperRemarkupURIInterface { 4 + 5 + public function getDoorkeeperURIRef(PhutilURI $uri); 6 + 7 + }
-31
src/applications/doorkeeper/remarkup/DoorkeeperAsanaRemarkupRule.php
··· 1 - <?php 2 - 3 - final class DoorkeeperAsanaRemarkupRule 4 - extends DoorkeeperRemarkupRule { 5 - 6 - public function apply($text) { 7 - return preg_replace_callback( 8 - '@https://app\\.asana\\.com/0/(\\d+)/(\\d+)@', 9 - array($this, 'markupAsanaLink'), 10 - $text); 11 - } 12 - 13 - public function markupAsanaLink($matches) { 14 - return $this->addDoorkeeperTag( 15 - array( 16 - 'href' => $matches[0], 17 - 'tag' => array( 18 - 'ref' => array( 19 - DoorkeeperBridgeAsana::APPTYPE_ASANA, 20 - DoorkeeperBridgeAsana::APPDOMAIN_ASANA, 21 - DoorkeeperBridgeAsana::OBJTYPE_TASK, 22 - $matches[2], 23 - ), 24 - 'extra' => array( 25 - 'asana.context' => $matches[1], 26 - ), 27 - ), 28 - )); 29 - } 30 - 31 - }
-44
src/applications/doorkeeper/remarkup/DoorkeeperJIRARemarkupRule.php
··· 1 - <?php 2 - 3 - final class DoorkeeperJIRARemarkupRule 4 - extends DoorkeeperRemarkupRule { 5 - 6 - public function apply($text) { 7 - return preg_replace_callback( 8 - '@(https?://\S+?)/browse/([A-Z]+-[1-9]\d*)@', 9 - array($this, 'markupJIRALink'), 10 - $text); 11 - } 12 - 13 - public function markupJIRALink($matches) { 14 - $match_domain = $matches[1]; 15 - $match_issue = $matches[2]; 16 - 17 - // TODO: When we support multiple instances, deal with them here. 18 - $provider = PhabricatorJIRAAuthProvider::getJIRAProvider(); 19 - if (!$provider) { 20 - return $matches[0]; 21 - } 22 - 23 - 24 - $jira_base = $provider->getJIRABaseURI(); 25 - if ($match_domain != rtrim($jira_base, '/')) { 26 - return $matches[0]; 27 - } 28 - 29 - return $this->addDoorkeeperTag( 30 - array( 31 - 'href' => $matches[0], 32 - 'tag' => array( 33 - 'ref' => array( 34 - DoorkeeperBridgeJIRA::APPTYPE_JIRA, 35 - $provider->getProviderDomain(), 36 - DoorkeeperBridgeJIRA::OBJTYPE_ISSUE, 37 - $match_issue, 38 - ), 39 - ), 40 - )); 41 - } 42 - 43 - 44 - }
-103
src/applications/doorkeeper/remarkup/DoorkeeperRemarkupRule.php
··· 1 - <?php 2 - 3 - abstract class DoorkeeperRemarkupRule extends PhutilRemarkupRule { 4 - 5 - const KEY_TAGS = 'doorkeeper.tags'; 6 - 7 - const VIEW_FULL = 'full'; 8 - const VIEW_SHORT = 'short'; 9 - 10 - public function getPriority() { 11 - return 350.0; 12 - } 13 - 14 - protected function addDoorkeeperTag(array $spec) { 15 - PhutilTypeSpec::checkMap( 16 - $spec, 17 - array( 18 - 'href' => 'string', 19 - 'tag' => 'map<string, wild>', 20 - 21 - 'name' => 'optional string', 22 - 'view' => 'optional string', 23 - )); 24 - 25 - $spec = $spec + array( 26 - 'view' => self::VIEW_FULL, 27 - ); 28 - 29 - $views = array( 30 - self::VIEW_FULL, 31 - self::VIEW_SHORT, 32 - ); 33 - $views = array_fuse($views); 34 - if (!isset($views[$spec['view']])) { 35 - throw new Exception( 36 - pht( 37 - 'Unsupported Doorkeeper tag view mode "%s". Supported modes are: %s.', 38 - $spec['view'], 39 - implode(', ', $views))); 40 - } 41 - 42 - $key = self::KEY_TAGS; 43 - $engine = $this->getEngine(); 44 - $token = $engine->storeText(get_class($this)); 45 - 46 - $tags = $engine->getTextMetadata($key, array()); 47 - 48 - $tags[] = array( 49 - 'token' => $token, 50 - ) + $spec + array( 51 - 'extra' => array(), 52 - ); 53 - 54 - $engine->setTextMetadata($key, $tags); 55 - return $token; 56 - } 57 - 58 - public function didMarkupText() { 59 - $key = self::KEY_TAGS; 60 - $engine = $this->getEngine(); 61 - $tags = $engine->getTextMetadata($key, array()); 62 - 63 - if (!$tags) { 64 - return; 65 - } 66 - 67 - $refs = array(); 68 - foreach ($tags as $spec) { 69 - $href = $spec['href']; 70 - $name = idx($spec, 'name', $href); 71 - 72 - $this->assertFlatText($href); 73 - $this->assertFlatText($name); 74 - 75 - if ($this->getEngine()->isTextMode()) { 76 - $view = "{$name} <{$href}>"; 77 - } else { 78 - $tag_id = celerity_generate_unique_node_id(); 79 - 80 - $refs[] = array( 81 - 'id' => $tag_id, 82 - 'view' => $spec['view'], 83 - ) + $spec['tag']; 84 - 85 - $view = id(new PHUITagView()) 86 - ->setID($tag_id) 87 - ->setName($name) 88 - ->setHref($href) 89 - ->setType(PHUITagView::TYPE_OBJECT) 90 - ->setExternal(true); 91 - } 92 - 93 - $engine->overwriteStoredText($spec['token'], $view); 94 - } 95 - 96 - if ($refs) { 97 - Javelin::initBehavior('doorkeeper-tag', array('tags' => $refs)); 98 - } 99 - 100 - $engine->setTextMetadata($key, array()); 101 - } 102 - 103 - }