Skip to content

Commit b22e52e

Browse files
author
epriestley
committed
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
1 parent e723b7e commit b22e52e

13 files changed

+389
-10
lines changed

src/__celerity_resource_map__.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1652,6 +1652,20 @@
16521652
),
16531653
'disk' => '/rsrc/js/application/diffusion/behavior-pull-lastmodified.js',
16541654
),
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+
),
16551669
'javelin-behavior-error-log' =>
16561670
array(
16571671
'uri' => '/res/acefdea7/rsrc/js/core/behavior-error-log.js',

src/__phutil_library_map__.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -537,7 +537,10 @@
537537
'DoorkeeperBridgeAsana' => 'applications/doorkeeper/bridge/DoorkeeperBridgeAsana.php',
538538
'DoorkeeperDAO' => 'applications/doorkeeper/storage/DoorkeeperDAO.php',
539539
'DoorkeeperExternalObject' => 'applications/doorkeeper/storage/DoorkeeperExternalObject.php',
540+
'DoorkeeperImportEngine' => 'applications/doorkeeper/engine/DoorkeeperImportEngine.php',
540541
'DoorkeeperObjectRef' => 'applications/doorkeeper/engine/DoorkeeperObjectRef.php',
542+
'DoorkeeperRemarkupRuleAsana' => 'applications/doorkeeper/remarkup/DoorkeeperRemarkupRuleAsana.php',
543+
'DoorkeeperTagsController' => 'applications/doorkeeper/controller/DoorkeeperTagsController.php',
541544
'DrydockAllocatorWorker' => 'applications/drydock/worker/DrydockAllocatorWorker.php',
542545
'DrydockApacheWebrootInterface' => 'applications/drydock/interface/webroot/DrydockApacheWebrootInterface.php',
543546
'DrydockBlueprint' => 'applications/drydock/blueprint/DrydockBlueprint.php',
@@ -745,6 +748,7 @@
745748
'PhabricatorApplicationDifferential' => 'applications/differential/application/PhabricatorApplicationDifferential.php',
746749
'PhabricatorApplicationDiffusion' => 'applications/diffusion/application/PhabricatorApplicationDiffusion.php',
747750
'PhabricatorApplicationDiviner' => 'applications/diviner/application/PhabricatorApplicationDiviner.php',
751+
'PhabricatorApplicationDoorkeeper' => 'applications/doorkeeper/application/PhabricatorApplicationDoorkeeper.php',
748752
'PhabricatorApplicationDrydock' => 'applications/drydock/application/PhabricatorApplicationDrydock.php',
749753
'PhabricatorApplicationFact' => 'applications/fact/application/PhabricatorApplicationFact.php',
750754
'PhabricatorApplicationFeed' => 'applications/feed/application/PhabricatorApplicationFeed.php',
@@ -2417,7 +2421,10 @@
24172421
0 => 'DoorkeeperDAO',
24182422
1 => 'PhabricatorPolicyInterface',
24192423
),
2424+
'DoorkeeperImportEngine' => 'Phobject',
24202425
'DoorkeeperObjectRef' => 'Phobject',
2426+
'DoorkeeperRemarkupRuleAsana' => 'PhutilRemarkupRule',
2427+
'DoorkeeperTagsController' => 'PhabricatorController',
24212428
'DrydockAllocatorWorker' => 'PhabricatorWorker',
24222429
'DrydockApacheWebrootInterface' => 'DrydockWebrootInterface',
24232430
'DrydockCommandInterface' => 'DrydockInterface',
@@ -2608,6 +2615,7 @@
26082615
'PhabricatorApplicationDifferential' => 'PhabricatorApplication',
26092616
'PhabricatorApplicationDiffusion' => 'PhabricatorApplication',
26102617
'PhabricatorApplicationDiviner' => 'PhabricatorApplication',
2618+
'PhabricatorApplicationDoorkeeper' => 'PhabricatorApplication',
26112619
'PhabricatorApplicationDrydock' => 'PhabricatorApplication',
26122620
'PhabricatorApplicationFact' => 'PhabricatorApplication',
26132621
'PhabricatorApplicationFeed' => 'PhabricatorApplication',

src/applications/diffusion/remarkup/DiffusionRemarkupRule.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,16 @@ protected function getObjectIDPattern() {
1515
$min_unqualified = PhabricatorRepository::MINIMUM_UNQUALIFIED_HASH;
1616
$min_qualified = PhabricatorRepository::MINIMUM_QUALIFIED_HASH;
1717

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+
1822
return
1923
'r[A-Z]+[1-9]\d*'.
2024
'|'.
2125
'r[A-Z]+[a-f0-9]{'.$min_qualified.',40}'.
2226
'|'.
23-
'[a-f0-9]{'.$min_unqualified.',40}';
27+
'(?<!/)[a-f0-9]{'.$min_unqualified.',40}';
2428
}
2529

2630
protected function loadObjects(array $ids) {
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
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+
}

src/applications/doorkeeper/bridge/DoorkeeperBridgeAsana.php

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ public function pullRefs(array $refs) {
2525
->withAccountDomains(array($provider->getProviderDomain()))
2626
->execute();
2727

28+
if (!$accounts) {
29+
return;
30+
}
31+
2832
// TODO: If the user has several linked Asana accounts, we just pick the
2933
// first one arbitrarily. We might want to try using all of them or do
3034
// something with more finesse. There's no UI way to link multiple accounts
@@ -47,18 +51,25 @@ public function pullRefs(array $refs) {
4751

4852
$results = array();
4953
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+
}
5159
}
5260

5361
foreach ($refs as $ref) {
62+
$ref->setAttribute('name', pht('Asana Task %s', $ref->getObjectID()));
63+
5464
$result = idx($results, $ref->getObjectKey());
5565
if (!$result) {
5666
continue;
5767
}
5868

5969
$ref->setIsVisible(true);
6070
$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']);
6273
$ref->setAttribute('description', $result['notes']);
6374

6475
$obj = $ref->getExternalObject();
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
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+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
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+
}

src/applications/doorkeeper/engine/DoorkeeperObjectRef.php

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ public function getIsVisible() {
4444
}
4545

4646
public function getAttribute($key, $default = null) {
47-
return idx($this->attribute, $key, $default);
47+
return idx($this->attributes, $key, $default);
4848
}
4949

5050
public function setAttribute($key, $value) {
@@ -91,6 +91,13 @@ public function getApplicationType() {
9191
return $this->applicationType;
9292
}
9393

94+
public function getFullName() {
95+
return coalesce(
96+
$this->getAttribute('fullname'),
97+
$this->getAttribute('name'),
98+
pht('External Object'));
99+
}
100+
94101
public function getObjectKey() {
95102
if (!$this->objectKey) {
96103
$this->objectKey = PhabricatorHash::digestForIndex(
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
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+
}

src/applications/phid/handle/PhabricatorObjectHandleData.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,8 @@ private function loadObjectsOfType($type, array $phids) {
213213
return mpull($xusrs, null, 'getPHID');
214214

215215
}
216+
217+
return array();
216218
}
217219

218220
public function loadHandles() {

0 commit comments

Comments
 (0)