diff --git a/src/__celerity_resource_map__.php b/src/__celerity_resource_map__.php index 83ccdcb708..d534591623 100644 --- a/src/__celerity_resource_map__.php +++ b/src/__celerity_resource_map__.php @@ -1652,6 +1652,20 @@ celerity_register_resource_map(array( ), 'disk' => '/rsrc/js/application/diffusion/behavior-pull-lastmodified.js', ), + 'javelin-behavior-doorkeeper-tag' => + array( + 'uri' => '/res/59480572/rsrc/js/application/doorkeeper/behavior-doorkeeper-tag.js', + 'type' => 'js', + 'requires' => + array( + 0 => 'javelin-behavior', + 1 => 'javelin-dom', + 2 => 'javelin-json', + 3 => 'javelin-workflow', + 4 => 'javelin-magical-init', + ), + 'disk' => '/rsrc/js/application/doorkeeper/behavior-doorkeeper-tag.js', + ), 'javelin-behavior-error-log' => array( 'uri' => '/res/acefdea7/rsrc/js/core/behavior-error-log.js', diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index b3ac5f457c..b6ffc5b844 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -537,7 +537,10 @@ phutil_register_library_map(array( 'DoorkeeperBridgeAsana' => 'applications/doorkeeper/bridge/DoorkeeperBridgeAsana.php', 'DoorkeeperDAO' => 'applications/doorkeeper/storage/DoorkeeperDAO.php', 'DoorkeeperExternalObject' => 'applications/doorkeeper/storage/DoorkeeperExternalObject.php', + 'DoorkeeperImportEngine' => 'applications/doorkeeper/engine/DoorkeeperImportEngine.php', 'DoorkeeperObjectRef' => 'applications/doorkeeper/engine/DoorkeeperObjectRef.php', + 'DoorkeeperRemarkupRuleAsana' => 'applications/doorkeeper/remarkup/DoorkeeperRemarkupRuleAsana.php', + 'DoorkeeperTagsController' => 'applications/doorkeeper/controller/DoorkeeperTagsController.php', 'DrydockAllocatorWorker' => 'applications/drydock/worker/DrydockAllocatorWorker.php', 'DrydockApacheWebrootInterface' => 'applications/drydock/interface/webroot/DrydockApacheWebrootInterface.php', 'DrydockBlueprint' => 'applications/drydock/blueprint/DrydockBlueprint.php', @@ -745,6 +748,7 @@ phutil_register_library_map(array( 'PhabricatorApplicationDifferential' => 'applications/differential/application/PhabricatorApplicationDifferential.php', 'PhabricatorApplicationDiffusion' => 'applications/diffusion/application/PhabricatorApplicationDiffusion.php', 'PhabricatorApplicationDiviner' => 'applications/diviner/application/PhabricatorApplicationDiviner.php', + 'PhabricatorApplicationDoorkeeper' => 'applications/doorkeeper/application/PhabricatorApplicationDoorkeeper.php', 'PhabricatorApplicationDrydock' => 'applications/drydock/application/PhabricatorApplicationDrydock.php', 'PhabricatorApplicationFact' => 'applications/fact/application/PhabricatorApplicationFact.php', 'PhabricatorApplicationFeed' => 'applications/feed/application/PhabricatorApplicationFeed.php', @@ -2417,7 +2421,10 @@ phutil_register_library_map(array( 0 => 'DoorkeeperDAO', 1 => 'PhabricatorPolicyInterface', ), + 'DoorkeeperImportEngine' => 'Phobject', 'DoorkeeperObjectRef' => 'Phobject', + 'DoorkeeperRemarkupRuleAsana' => 'PhutilRemarkupRule', + 'DoorkeeperTagsController' => 'PhabricatorController', 'DrydockAllocatorWorker' => 'PhabricatorWorker', 'DrydockApacheWebrootInterface' => 'DrydockWebrootInterface', 'DrydockCommandInterface' => 'DrydockInterface', @@ -2608,6 +2615,7 @@ phutil_register_library_map(array( 'PhabricatorApplicationDifferential' => 'PhabricatorApplication', 'PhabricatorApplicationDiffusion' => 'PhabricatorApplication', 'PhabricatorApplicationDiviner' => 'PhabricatorApplication', + 'PhabricatorApplicationDoorkeeper' => 'PhabricatorApplication', 'PhabricatorApplicationDrydock' => 'PhabricatorApplication', 'PhabricatorApplicationFact' => 'PhabricatorApplication', 'PhabricatorApplicationFeed' => 'PhabricatorApplication', diff --git a/src/applications/diffusion/remarkup/DiffusionRemarkupRule.php b/src/applications/diffusion/remarkup/DiffusionRemarkupRule.php index eea5ad44b5..e3b2fdb1a7 100644 --- a/src/applications/diffusion/remarkup/DiffusionRemarkupRule.php +++ b/src/applications/diffusion/remarkup/DiffusionRemarkupRule.php @@ -15,12 +15,16 @@ final class DiffusionRemarkupRule $min_unqualified = PhabricatorRepository::MINIMUM_UNQUALIFIED_HASH; $min_qualified = PhabricatorRepository::MINIMUM_QUALIFIED_HASH; + // NOTE: The "(? array( + 'tags/' => 'DoorkeeperTagsController', + ), + ); + } + +} diff --git a/src/applications/doorkeeper/bridge/DoorkeeperBridgeAsana.php b/src/applications/doorkeeper/bridge/DoorkeeperBridgeAsana.php index 36722ccd91..e8be40cbd6 100644 --- a/src/applications/doorkeeper/bridge/DoorkeeperBridgeAsana.php +++ b/src/applications/doorkeeper/bridge/DoorkeeperBridgeAsana.php @@ -25,6 +25,10 @@ final class DoorkeeperBridgeAsana extends DoorkeeperBridge { ->withAccountDomains(array($provider->getProviderDomain())) ->execute(); + if (!$accounts) { + return; + } + // TODO: If the user has several linked Asana accounts, we just pick the // first one arbitrarily. We might want to try using all of them or do // something with more finesse. There's no UI way to link multiple accounts @@ -47,10 +51,16 @@ final class DoorkeeperBridgeAsana extends DoorkeeperBridge { $results = array(); foreach (Futures($futures) as $key => $future) { - $results[$key] = $future->resolve(); + try { + $results[$key] = $future->resolve(); + } catch (Exception $ex) { + // TODO: For now, ignore this stuff. + } } foreach ($refs as $ref) { + $ref->setAttribute('name', pht('Asana Task %s', $ref->getObjectID())); + $result = idx($results, $ref->getObjectKey()); if (!$result) { continue; @@ -58,7 +68,8 @@ final class DoorkeeperBridgeAsana extends DoorkeeperBridge { $ref->setIsVisible(true); $ref->setAttribute('asana.data', $result); - $ref->setAttribute('name', $result['name']); + $ref->setAttribute('fullname', pht('Asana: %s', $result['name'])); + $ref->setAttribute('title', $result['name']); $ref->setAttribute('description', $result['notes']); $obj = $ref->getExternalObject(); diff --git a/src/applications/doorkeeper/controller/DoorkeeperTagsController.php b/src/applications/doorkeeper/controller/DoorkeeperTagsController.php new file mode 100644 index 0000000000..d46c1e535d --- /dev/null +++ b/src/applications/doorkeeper/controller/DoorkeeperTagsController.php @@ -0,0 +1,69 @@ +getRequest(); + $viewer = $request->getUser(); + + $tags = $request->getStr('tags'); + $tags = json_decode($tags, true); + if (!is_array($tags)) { + $tags = array(); + } + + $refs = array(); + $id_map = array(); + foreach ($tags as $tag_spec) { + $tag = $tag_spec['ref']; + $ref = id(new DoorkeeperObjectRef()) + ->setApplicationType($tag[0]) + ->setApplicationDomain($tag[1]) + ->setObjectType($tag[2]) + ->setObjectID($tag[3]); + + $key = $ref->getObjectKey(); + $id_map[$key] = $tag_spec['id']; + $refs[$key] = $ref; + } + + $refs = id(new DoorkeeperImportEngine()) + ->setViewer($viewer) + ->setRefs($refs) + ->execute(); + + $results = array(); + foreach ($refs as $key => $ref) { + if (!$ref->getIsVisible()) { + continue; + } + + $uri = $ref->getExternalObject()->getObjectURI(); + if (!$uri) { + continue; + } + + $id = $id_map[$key]; + + $tag = id(new PhabricatorTagView()) + ->setID($id) + ->setName($ref->getFullName()) + ->setHref($uri) + ->setType(PhabricatorTagView::TYPE_OBJECT) + ->setExternal(true) + ->render(); + + $results[] = array( + 'id' => $id, + 'markup' => $tag, + ); + } + + return id(new AphrontAjaxResponse())->setContent( + array( + 'tags' => $results, + )); + } + + +} diff --git a/src/applications/doorkeeper/engine/DoorkeeperImportEngine.php b/src/applications/doorkeeper/engine/DoorkeeperImportEngine.php new file mode 100644 index 0000000000..8c069342e2 --- /dev/null +++ b/src/applications/doorkeeper/engine/DoorkeeperImportEngine.php @@ -0,0 +1,74 @@ +viewer = $viewer; + return $this; + } + + public function getViewer() { + return $this->viewer; + } + + public function setRefs(array $refs) { + assert_instances_of($refs, 'DoorkeeperObjectRef'); + $this->refs = $refs; + return $this; + } + + public function getRefs() { + return $this->refs; + } + + public function execute() { + $refs = $this->getRefs(); + $viewer = $this->getViewer(); + + $keys = mpull($refs, 'getObjectKey'); + if ($keys) { + $xobjs = id(new DoorkeeperExternalObject())->loadAllWhere( + 'objectKey IN (%Ls)', + $keys); + $xobjs = mpull($xobjs, null, 'getObjectKey'); + foreach ($refs as $ref) { + $xobj = idx($xobjs, $ref->getObjectKey()); + if (!$xobj) { + $xobj = $ref->newExternalObject() + ->setImporterPHID($viewer->getPHID()); + } + $ref->attachExternalObject($xobj); + } + } + + $bridges = id(new PhutilSymbolLoader()) + ->setAncestorClass('DoorkeeperBridge') + ->loadObjects(); + + foreach ($bridges as $key => $bridge) { + if (!$bridge->isEnabled()) { + unset($bridges[$key]); + } + $bridge->setViewer($viewer); + } + + foreach ($bridges as $bridge) { + $bridge_refs = array(); + foreach ($refs as $key => $ref) { + if ($bridge->canPullRef($ref)) { + $bridge_refs[$key] = $ref; + unset($refs[$key]); + } + } + if ($bridge_refs) { + $bridge->pullRefs($bridge_refs); + } + } + + return $this->getRefs(); + } + +} diff --git a/src/applications/doorkeeper/engine/DoorkeeperObjectRef.php b/src/applications/doorkeeper/engine/DoorkeeperObjectRef.php index f88ea97b33..71f11d6807 100644 --- a/src/applications/doorkeeper/engine/DoorkeeperObjectRef.php +++ b/src/applications/doorkeeper/engine/DoorkeeperObjectRef.php @@ -44,7 +44,7 @@ final class DoorkeeperObjectRef extends Phobject { } public function getAttribute($key, $default = null) { - return idx($this->attribute, $key, $default); + return idx($this->attributes, $key, $default); } public function setAttribute($key, $value) { @@ -91,6 +91,13 @@ final class DoorkeeperObjectRef extends Phobject { return $this->applicationType; } + public function getFullName() { + return coalesce( + $this->getAttribute('fullname'), + $this->getAttribute('name'), + pht('External Object')); + } + public function getObjectKey() { if (!$this->objectKey) { $this->objectKey = PhabricatorHash::digestForIndex( diff --git a/src/applications/doorkeeper/remarkup/DoorkeeperRemarkupRuleAsana.php b/src/applications/doorkeeper/remarkup/DoorkeeperRemarkupRuleAsana.php new file mode 100644 index 0000000000..a5cf1d37c0 --- /dev/null +++ b/src/applications/doorkeeper/remarkup/DoorkeeperRemarkupRuleAsana.php @@ -0,0 +1,70 @@ +getEngine(); + $token = $engine->storeText('AsanaDoorkeeper'); + + $tags = $engine->getTextMetadata($key, array()); + + $tags[] = array( + 'token' => $token, + 'href' => $matches[0], + 'tag' => array( + 'ref' => array('asana', 'asana.com', 'asana:task', $matches[2]), + 'extra' => array( + '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']; + + $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()); + } + +} diff --git a/src/applications/phid/handle/PhabricatorObjectHandleData.php b/src/applications/phid/handle/PhabricatorObjectHandleData.php index 81ccf2fba1..4e9720ae25 100644 --- a/src/applications/phid/handle/PhabricatorObjectHandleData.php +++ b/src/applications/phid/handle/PhabricatorObjectHandleData.php @@ -213,6 +213,8 @@ final class PhabricatorObjectHandleData { return mpull($xusrs, null, 'getPHID'); } + + return array(); } public function loadHandles() { diff --git a/src/infrastructure/markup/PhabricatorMarkupEngine.php b/src/infrastructure/markup/PhabricatorMarkupEngine.php index f929c111fd..474de96972 100644 --- a/src/infrastructure/markup/PhabricatorMarkupEngine.php +++ b/src/infrastructure/markup/PhabricatorMarkupEngine.php @@ -449,12 +449,6 @@ final class PhabricatorMarkupEngine { $rules[] = new PhabricatorRemarkupRuleYoutube(); } - $rules[] = new PhutilRemarkupRuleHyperlink(); - $rules[] = new PhrictionRemarkupRule(); - - $rules[] = new PhabricatorRemarkupRuleEmbedFile(); - $rules[] = new PhabricatorCountdownRemarkupRule(); - $applications = PhabricatorApplication::getAllInstalledApplications(); foreach ($applications as $application) { foreach ($application->getRemarkupRules() as $rule) { @@ -462,6 +456,12 @@ final class PhabricatorMarkupEngine { } } + $rules[] = new PhutilRemarkupRuleHyperlink(); + $rules[] = new PhrictionRemarkupRule(); + + $rules[] = new PhabricatorRemarkupRuleEmbedFile(); + $rules[] = new PhabricatorCountdownRemarkupRule(); + if ($options['macros']) { $rules[] = new PhabricatorRemarkupRuleImageMacro(); $rules[] = new PhabricatorRemarkupRuleMeme(); diff --git a/src/view/layout/PhabricatorTagView.php b/src/view/layout/PhabricatorTagView.php index 364f7671a2..1607fb9ad8 100644 --- a/src/view/layout/PhabricatorTagView.php +++ b/src/view/layout/PhabricatorTagView.php @@ -28,6 +28,17 @@ final class PhabricatorTagView extends AphrontView { private $dotColor; private $barColor; private $closed; + private $external; + private $id; + + public function setID($id) { + $this->id = $id; + return $this; + } + + public function getID() { + return $this->id; + } public function setType($type) { $this->type = $type; @@ -135,20 +146,24 @@ final class PhabricatorTagView extends AphrontView { return javelin_tag( 'a', array( + 'id' => $this->id, 'href' => $this->href, 'class' => implode(' ', $classes), 'sigil' => 'hovercard', 'meta' => array( 'hoverPHID' => $this->phid, ), + 'target' => $this->external ? '_blank' : null, ), array($bar, $content)); } else { return phutil_tag( $this->href ? 'a' : 'span', array( + 'id' => $this->id, 'href' => $this->href, 'class' => implode(' ', $classes), + 'target' => $this->external ? '_blank' : null, ), array($bar, $content)); } @@ -180,4 +195,13 @@ final class PhabricatorTagView extends AphrontView { ); } + public function setExternal($external) { + $this->external = $external; + return $this; + } + + public function getExternal() { + return $this->external; + } + } diff --git a/webroot/rsrc/js/application/doorkeeper/behavior-doorkeeper-tag.js b/webroot/rsrc/js/application/doorkeeper/behavior-doorkeeper-tag.js new file mode 100644 index 0000000000..17a3aa7aa5 --- /dev/null +++ b/webroot/rsrc/js/application/doorkeeper/behavior-doorkeeper-tag.js @@ -0,0 +1,65 @@ +/** + * @provides javelin-behavior-doorkeeper-tag + * @requires javelin-behavior + * javelin-dom + * javelin-json + * javelin-workflow + * javelin-magical-init + */ + +JX.behavior('doorkeeper-tag', function(config, statics) { + statics.tags = (statics.tags || []).concat(config.tags); + statics.cache = statics.cache || {}; + + // NOTE: We keep a cache in the browser of external objects that we've already + // looked up. This is mostly to keep previews from being flickery messes. + + var load = function() { + var tags = statics.tags; + statics.tags = []; + + if (!tags.length) { + return; + } + + var have = []; + var need = []; + var keys = {}; + + var draw = function(tags) { + for (var ii = 0; ii < tags.length; ii++) { + try { + JX.DOM.replace(JX.$(tags[ii].id), JX.$H(tags[ii].markup)); + } catch (ignored) { + // The tag may have been wiped out of the body by the time the + // response returns, for whatever reason. That's fine, just don't + // bother drawing it. + } + statics.cache[keys[tags[ii].id]] = tags[ii].markup; + } + }; + + for (var ii = 0; ii < tags.length; ii++) { + var tag_key = tags[ii].ref.join('@'); + if (tag_key in statics.cache) { + have.push({id: tags[ii].id, markup: statics.cache[tag_key]}); + } else { + need.push(tags[ii]); + keys[tags[ii].id] = tag_key; + } + } + + if (have.length) { + draw(have); + } + + if (need.length) { + new JX.Workflow('/doorkeeper/tags/', {tags: JX.JSON.stringify(need)}) + .setHandler(function(r) { draw(r.tags); }) + .start(); + } + }; + + JX.onload(load); +}); +