@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
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add remarkup support for Asana URIs

Summary:
Ref T2852. Primarily, this expands API access to Asana. As a user-visible effect, it links Asana tasks in Remarkup.

When a user enters an Asana URI, we register an onload behavior to make an Ajax call for the lookup. This respects privacy imposed by the API without creating a significant performance impact.

Test Plan: {F47183}

Reviewers: btrahan

Reviewed By: btrahan

CC: chad, aran

Maniphest Tasks: T2852

Differential Revision: https://secure.phabricator.com/D6274

+389 -10
+14
src/__celerity_resource_map__.php
··· 1652 1652 ), 1653 1653 'disk' => '/rsrc/js/application/diffusion/behavior-pull-lastmodified.js', 1654 1654 ), 1655 + 'javelin-behavior-doorkeeper-tag' => 1656 + array( 1657 + 'uri' => '/res/59480572/rsrc/js/application/doorkeeper/behavior-doorkeeper-tag.js', 1658 + 'type' => 'js', 1659 + 'requires' => 1660 + array( 1661 + 0 => 'javelin-behavior', 1662 + 1 => 'javelin-dom', 1663 + 2 => 'javelin-json', 1664 + 3 => 'javelin-workflow', 1665 + 4 => 'javelin-magical-init', 1666 + ), 1667 + 'disk' => '/rsrc/js/application/doorkeeper/behavior-doorkeeper-tag.js', 1668 + ), 1655 1669 'javelin-behavior-error-log' => 1656 1670 array( 1657 1671 'uri' => '/res/acefdea7/rsrc/js/core/behavior-error-log.js',
+8
src/__phutil_library_map__.php
··· 537 537 'DoorkeeperBridgeAsana' => 'applications/doorkeeper/bridge/DoorkeeperBridgeAsana.php', 538 538 'DoorkeeperDAO' => 'applications/doorkeeper/storage/DoorkeeperDAO.php', 539 539 'DoorkeeperExternalObject' => 'applications/doorkeeper/storage/DoorkeeperExternalObject.php', 540 + 'DoorkeeperImportEngine' => 'applications/doorkeeper/engine/DoorkeeperImportEngine.php', 540 541 'DoorkeeperObjectRef' => 'applications/doorkeeper/engine/DoorkeeperObjectRef.php', 542 + 'DoorkeeperRemarkupRuleAsana' => 'applications/doorkeeper/remarkup/DoorkeeperRemarkupRuleAsana.php', 543 + 'DoorkeeperTagsController' => 'applications/doorkeeper/controller/DoorkeeperTagsController.php', 541 544 'DrydockAllocatorWorker' => 'applications/drydock/worker/DrydockAllocatorWorker.php', 542 545 'DrydockApacheWebrootInterface' => 'applications/drydock/interface/webroot/DrydockApacheWebrootInterface.php', 543 546 'DrydockBlueprint' => 'applications/drydock/blueprint/DrydockBlueprint.php', ··· 745 748 'PhabricatorApplicationDifferential' => 'applications/differential/application/PhabricatorApplicationDifferential.php', 746 749 'PhabricatorApplicationDiffusion' => 'applications/diffusion/application/PhabricatorApplicationDiffusion.php', 747 750 'PhabricatorApplicationDiviner' => 'applications/diviner/application/PhabricatorApplicationDiviner.php', 751 + 'PhabricatorApplicationDoorkeeper' => 'applications/doorkeeper/application/PhabricatorApplicationDoorkeeper.php', 748 752 'PhabricatorApplicationDrydock' => 'applications/drydock/application/PhabricatorApplicationDrydock.php', 749 753 'PhabricatorApplicationFact' => 'applications/fact/application/PhabricatorApplicationFact.php', 750 754 'PhabricatorApplicationFeed' => 'applications/feed/application/PhabricatorApplicationFeed.php', ··· 2417 2421 0 => 'DoorkeeperDAO', 2418 2422 1 => 'PhabricatorPolicyInterface', 2419 2423 ), 2424 + 'DoorkeeperImportEngine' => 'Phobject', 2420 2425 'DoorkeeperObjectRef' => 'Phobject', 2426 + 'DoorkeeperRemarkupRuleAsana' => 'PhutilRemarkupRule', 2427 + 'DoorkeeperTagsController' => 'PhabricatorController', 2421 2428 'DrydockAllocatorWorker' => 'PhabricatorWorker', 2422 2429 'DrydockApacheWebrootInterface' => 'DrydockWebrootInterface', 2423 2430 'DrydockCommandInterface' => 'DrydockInterface', ··· 2608 2615 'PhabricatorApplicationDifferential' => 'PhabricatorApplication', 2609 2616 'PhabricatorApplicationDiffusion' => 'PhabricatorApplication', 2610 2617 'PhabricatorApplicationDiviner' => 'PhabricatorApplication', 2618 + 'PhabricatorApplicationDoorkeeper' => 'PhabricatorApplication', 2611 2619 'PhabricatorApplicationDrydock' => 'PhabricatorApplication', 2612 2620 'PhabricatorApplicationFact' => 'PhabricatorApplication', 2613 2621 'PhabricatorApplicationFeed' => 'PhabricatorApplication',
+5 -1
src/applications/diffusion/remarkup/DiffusionRemarkupRule.php
··· 15 15 $min_unqualified = PhabricatorRepository::MINIMUM_UNQUALIFIED_HASH; 16 16 $min_qualified = PhabricatorRepository::MINIMUM_QUALIFIED_HASH; 17 17 18 + // NOTE: The "(?<!/)" negative lookbehind prevents this rule from matching 19 + // hashes or hash-like substrings in most URLs. For example, this will not 20 + // match: http://www.example.com/article/28903218328/ 21 + 18 22 return 19 23 'r[A-Z]+[1-9]\d*'. 20 24 '|'. 21 25 'r[A-Z]+[a-f0-9]{'.$min_qualified.',40}'. 22 26 '|'. 23 - '[a-f0-9]{'.$min_unqualified.',40}'; 27 + '(?<!/)[a-f0-9]{'.$min_unqualified.',40}'; 24 28 } 25 29 26 30 protected function loadObjects(array $ids) {
+31
src/applications/doorkeeper/application/PhabricatorApplicationDoorkeeper.php
··· 1 + <?php 2 + 3 + final class PhabricatorApplicationDoorkeeper extends PhabricatorApplication { 4 + 5 + public function canUninstall() { 6 + return false; 7 + } 8 + 9 + public function getBaseURI() { 10 + return '/doorkeeper/'; 11 + } 12 + 13 + public function shouldAppearOnLaunchView() { 14 + return false; 15 + } 16 + 17 + public function getRemarkupRules() { 18 + return array( 19 + new DoorkeeperRemarkupRuleAsana(), 20 + ); 21 + } 22 + 23 + public function getRoutes() { 24 + return array( 25 + '/doorkeeper/' => array( 26 + 'tags/' => 'DoorkeeperTagsController', 27 + ), 28 + ); 29 + } 30 + 31 + }
+13 -2
src/applications/doorkeeper/bridge/DoorkeeperBridgeAsana.php
··· 25 25 ->withAccountDomains(array($provider->getProviderDomain())) 26 26 ->execute(); 27 27 28 + if (!$accounts) { 29 + return; 30 + } 31 + 28 32 // TODO: If the user has several linked Asana accounts, we just pick the 29 33 // first one arbitrarily. We might want to try using all of them or do 30 34 // something with more finesse. There's no UI way to link multiple accounts ··· 47 51 48 52 $results = array(); 49 53 foreach (Futures($futures) as $key => $future) { 50 - $results[$key] = $future->resolve(); 54 + try { 55 + $results[$key] = $future->resolve(); 56 + } catch (Exception $ex) { 57 + // TODO: For now, ignore this stuff. 58 + } 51 59 } 52 60 53 61 foreach ($refs as $ref) { 62 + $ref->setAttribute('name', pht('Asana Task %s', $ref->getObjectID())); 63 + 54 64 $result = idx($results, $ref->getObjectKey()); 55 65 if (!$result) { 56 66 continue; ··· 58 68 59 69 $ref->setIsVisible(true); 60 70 $ref->setAttribute('asana.data', $result); 61 - $ref->setAttribute('name', $result['name']); 71 + $ref->setAttribute('fullname', pht('Asana: %s', $result['name'])); 72 + $ref->setAttribute('title', $result['name']); 62 73 $ref->setAttribute('description', $result['notes']); 63 74 64 75 $obj = $ref->getExternalObject();
+69
src/applications/doorkeeper/controller/DoorkeeperTagsController.php
··· 1 + <?php 2 + 3 + final class DoorkeeperTagsController extends PhabricatorController { 4 + 5 + public function processRequest() { 6 + $request = $this->getRequest(); 7 + $viewer = $request->getUser(); 8 + 9 + $tags = $request->getStr('tags'); 10 + $tags = json_decode($tags, true); 11 + if (!is_array($tags)) { 12 + $tags = array(); 13 + } 14 + 15 + $refs = array(); 16 + $id_map = array(); 17 + foreach ($tags as $tag_spec) { 18 + $tag = $tag_spec['ref']; 19 + $ref = id(new DoorkeeperObjectRef()) 20 + ->setApplicationType($tag[0]) 21 + ->setApplicationDomain($tag[1]) 22 + ->setObjectType($tag[2]) 23 + ->setObjectID($tag[3]); 24 + 25 + $key = $ref->getObjectKey(); 26 + $id_map[$key] = $tag_spec['id']; 27 + $refs[$key] = $ref; 28 + } 29 + 30 + $refs = id(new DoorkeeperImportEngine()) 31 + ->setViewer($viewer) 32 + ->setRefs($refs) 33 + ->execute(); 34 + 35 + $results = array(); 36 + foreach ($refs as $key => $ref) { 37 + if (!$ref->getIsVisible()) { 38 + continue; 39 + } 40 + 41 + $uri = $ref->getExternalObject()->getObjectURI(); 42 + if (!$uri) { 43 + continue; 44 + } 45 + 46 + $id = $id_map[$key]; 47 + 48 + $tag = id(new PhabricatorTagView()) 49 + ->setID($id) 50 + ->setName($ref->getFullName()) 51 + ->setHref($uri) 52 + ->setType(PhabricatorTagView::TYPE_OBJECT) 53 + ->setExternal(true) 54 + ->render(); 55 + 56 + $results[] = array( 57 + 'id' => $id, 58 + 'markup' => $tag, 59 + ); 60 + } 61 + 62 + return id(new AphrontAjaxResponse())->setContent( 63 + array( 64 + 'tags' => $results, 65 + )); 66 + } 67 + 68 + 69 + }
+74
src/applications/doorkeeper/engine/DoorkeeperImportEngine.php
··· 1 + <?php 2 + 3 + final class DoorkeeperImportEngine extends Phobject { 4 + 5 + private $viewer; 6 + private $refs; 7 + 8 + public function setViewer(PhabricatorUser $viewer) { 9 + $this->viewer = $viewer; 10 + return $this; 11 + } 12 + 13 + public function getViewer() { 14 + return $this->viewer; 15 + } 16 + 17 + public function setRefs(array $refs) { 18 + assert_instances_of($refs, 'DoorkeeperObjectRef'); 19 + $this->refs = $refs; 20 + return $this; 21 + } 22 + 23 + public function getRefs() { 24 + return $this->refs; 25 + } 26 + 27 + public function execute() { 28 + $refs = $this->getRefs(); 29 + $viewer = $this->getViewer(); 30 + 31 + $keys = mpull($refs, 'getObjectKey'); 32 + if ($keys) { 33 + $xobjs = id(new DoorkeeperExternalObject())->loadAllWhere( 34 + 'objectKey IN (%Ls)', 35 + $keys); 36 + $xobjs = mpull($xobjs, null, 'getObjectKey'); 37 + foreach ($refs as $ref) { 38 + $xobj = idx($xobjs, $ref->getObjectKey()); 39 + if (!$xobj) { 40 + $xobj = $ref->newExternalObject() 41 + ->setImporterPHID($viewer->getPHID()); 42 + } 43 + $ref->attachExternalObject($xobj); 44 + } 45 + } 46 + 47 + $bridges = id(new PhutilSymbolLoader()) 48 + ->setAncestorClass('DoorkeeperBridge') 49 + ->loadObjects(); 50 + 51 + foreach ($bridges as $key => $bridge) { 52 + if (!$bridge->isEnabled()) { 53 + unset($bridges[$key]); 54 + } 55 + $bridge->setViewer($viewer); 56 + } 57 + 58 + foreach ($bridges as $bridge) { 59 + $bridge_refs = array(); 60 + foreach ($refs as $key => $ref) { 61 + if ($bridge->canPullRef($ref)) { 62 + $bridge_refs[$key] = $ref; 63 + unset($refs[$key]); 64 + } 65 + } 66 + if ($bridge_refs) { 67 + $bridge->pullRefs($bridge_refs); 68 + } 69 + } 70 + 71 + return $this->getRefs(); 72 + } 73 + 74 + }
+8 -1
src/applications/doorkeeper/engine/DoorkeeperObjectRef.php
··· 44 44 } 45 45 46 46 public function getAttribute($key, $default = null) { 47 - return idx($this->attribute, $key, $default); 47 + return idx($this->attributes, $key, $default); 48 48 } 49 49 50 50 public function setAttribute($key, $value) { ··· 89 89 90 90 public function getApplicationType() { 91 91 return $this->applicationType; 92 + } 93 + 94 + public function getFullName() { 95 + return coalesce( 96 + $this->getAttribute('fullname'), 97 + $this->getAttribute('name'), 98 + pht('External Object')); 92 99 } 93 100 94 101 public function getObjectKey() {
+70
src/applications/doorkeeper/remarkup/DoorkeeperRemarkupRuleAsana.php
··· 1 + <?php 2 + 3 + final class DoorkeeperRemarkupRuleAsana 4 + extends PhutilRemarkupRule { 5 + 6 + const KEY_TAGS = 'doorkeeper.tags'; 7 + 8 + public function apply($text) { 9 + return preg_replace_callback( 10 + '@https://app\\.asana\\.com/0/(\\d+)/(\\d+)@', 11 + array($this, 'markupAsanaLink'), 12 + $text); 13 + } 14 + 15 + public function markupAsanaLink($matches) { 16 + $key = self::KEY_TAGS; 17 + $engine = $this->getEngine(); 18 + $token = $engine->storeText('AsanaDoorkeeper'); 19 + 20 + $tags = $engine->getTextMetadata($key, array()); 21 + 22 + $tags[] = array( 23 + 'token' => $token, 24 + 'href' => $matches[0], 25 + 'tag' => array( 26 + 'ref' => array('asana', 'asana.com', 'asana:task', $matches[2]), 27 + 'extra' => array( 28 + 'asana.context' => $matches[1], 29 + ), 30 + ), 31 + ); 32 + 33 + $engine->setTextMetadata($key, $tags); 34 + 35 + return $token; 36 + } 37 + 38 + public function didMarkupText() { 39 + $key = self::KEY_TAGS; 40 + $engine = $this->getEngine(); 41 + $tags = $engine->getTextMetadata($key, array()); 42 + 43 + if (!$tags) { 44 + return; 45 + } 46 + 47 + $refs = array(); 48 + foreach ($tags as $spec) { 49 + $tag_id = celerity_generate_unique_node_id(); 50 + 51 + $refs[] = array( 52 + 'id' => $tag_id, 53 + ) + $spec['tag']; 54 + 55 + $view = id(new PhabricatorTagView()) 56 + ->setID($tag_id) 57 + ->setName($spec['href']) 58 + ->setHref($spec['href']) 59 + ->setType(PhabricatorTagView::TYPE_OBJECT) 60 + ->setExternal(true); 61 + 62 + $engine->overwriteStoredText($spec['token'], $view); 63 + } 64 + 65 + Javelin::initBehavior('doorkeeper-tag', array('tags' => $refs)); 66 + 67 + $engine->setTextMetadata($key, array()); 68 + } 69 + 70 + }
+2
src/applications/phid/handle/PhabricatorObjectHandleData.php
··· 213 213 return mpull($xusrs, null, 'getPHID'); 214 214 215 215 } 216 + 217 + return array(); 216 218 } 217 219 218 220 public function loadHandles() {
+6 -6
src/infrastructure/markup/PhabricatorMarkupEngine.php
··· 449 449 $rules[] = new PhabricatorRemarkupRuleYoutube(); 450 450 } 451 451 452 - $rules[] = new PhutilRemarkupRuleHyperlink(); 453 - $rules[] = new PhrictionRemarkupRule(); 454 - 455 - $rules[] = new PhabricatorRemarkupRuleEmbedFile(); 456 - $rules[] = new PhabricatorCountdownRemarkupRule(); 457 - 458 452 $applications = PhabricatorApplication::getAllInstalledApplications(); 459 453 foreach ($applications as $application) { 460 454 foreach ($application->getRemarkupRules() as $rule) { 461 455 $rules[] = $rule; 462 456 } 463 457 } 458 + 459 + $rules[] = new PhutilRemarkupRuleHyperlink(); 460 + $rules[] = new PhrictionRemarkupRule(); 461 + 462 + $rules[] = new PhabricatorRemarkupRuleEmbedFile(); 463 + $rules[] = new PhabricatorCountdownRemarkupRule(); 464 464 465 465 if ($options['macros']) { 466 466 $rules[] = new PhabricatorRemarkupRuleImageMacro();
+24
src/view/layout/PhabricatorTagView.php
··· 28 28 private $dotColor; 29 29 private $barColor; 30 30 private $closed; 31 + private $external; 32 + private $id; 33 + 34 + public function setID($id) { 35 + $this->id = $id; 36 + return $this; 37 + } 38 + 39 + public function getID() { 40 + return $this->id; 41 + } 31 42 32 43 public function setType($type) { 33 44 $this->type = $type; ··· 135 146 return javelin_tag( 136 147 'a', 137 148 array( 149 + 'id' => $this->id, 138 150 'href' => $this->href, 139 151 'class' => implode(' ', $classes), 140 152 'sigil' => 'hovercard', 141 153 'meta' => array( 142 154 'hoverPHID' => $this->phid, 143 155 ), 156 + 'target' => $this->external ? '_blank' : null, 144 157 ), 145 158 array($bar, $content)); 146 159 } else { 147 160 return phutil_tag( 148 161 $this->href ? 'a' : 'span', 149 162 array( 163 + 'id' => $this->id, 150 164 'href' => $this->href, 151 165 'class' => implode(' ', $classes), 166 + 'target' => $this->external ? '_blank' : null, 152 167 ), 153 168 array($bar, $content)); 154 169 } ··· 178 193 self::COLOR_OBJECT, 179 194 self::COLOR_PERSON, 180 195 ); 196 + } 197 + 198 + public function setExternal($external) { 199 + $this->external = $external; 200 + return $this; 201 + } 202 + 203 + public function getExternal() { 204 + return $this->external; 181 205 } 182 206 183 207 }
+65
webroot/rsrc/js/application/doorkeeper/behavior-doorkeeper-tag.js
··· 1 + /** 2 + * @provides javelin-behavior-doorkeeper-tag 3 + * @requires javelin-behavior 4 + * javelin-dom 5 + * javelin-json 6 + * javelin-workflow 7 + * javelin-magical-init 8 + */ 9 + 10 + JX.behavior('doorkeeper-tag', function(config, statics) { 11 + statics.tags = (statics.tags || []).concat(config.tags); 12 + statics.cache = statics.cache || {}; 13 + 14 + // NOTE: We keep a cache in the browser of external objects that we've already 15 + // looked up. This is mostly to keep previews from being flickery messes. 16 + 17 + var load = function() { 18 + var tags = statics.tags; 19 + statics.tags = []; 20 + 21 + if (!tags.length) { 22 + return; 23 + } 24 + 25 + var have = []; 26 + var need = []; 27 + var keys = {}; 28 + 29 + var draw = function(tags) { 30 + for (var ii = 0; ii < tags.length; ii++) { 31 + try { 32 + JX.DOM.replace(JX.$(tags[ii].id), JX.$H(tags[ii].markup)); 33 + } catch (ignored) { 34 + // The tag may have been wiped out of the body by the time the 35 + // response returns, for whatever reason. That's fine, just don't 36 + // bother drawing it. 37 + } 38 + statics.cache[keys[tags[ii].id]] = tags[ii].markup; 39 + } 40 + }; 41 + 42 + for (var ii = 0; ii < tags.length; ii++) { 43 + var tag_key = tags[ii].ref.join('@'); 44 + if (tag_key in statics.cache) { 45 + have.push({id: tags[ii].id, markup: statics.cache[tag_key]}); 46 + } else { 47 + need.push(tags[ii]); 48 + keys[tags[ii].id] = tag_key; 49 + } 50 + } 51 + 52 + if (have.length) { 53 + draw(have); 54 + } 55 + 56 + if (need.length) { 57 + new JX.Workflow('/doorkeeper/tags/', {tags: JX.JSON.stringify(need)}) 58 + .setHandler(function(r) { draw(r.tags); }) 59 + .start(); 60 + } 61 + }; 62 + 63 + JX.onload(load); 64 + }); 65 +