diff --git a/src/applications/base/PhabricatorApplication.php b/src/applications/base/PhabricatorApplication.php
index 03f14a241b..1f605476bc 100644
--- a/src/applications/base/PhabricatorApplication.php
+++ b/src/applications/base/PhabricatorApplication.php
@@ -1,602 +1,613 @@
 <?php
 
 /**
  * @task  info  Application Information
  * @task  ui    UI Integration
  * @task  uri   URI Routing
  * @task  mail  Email integration
  * @task  fact  Fact Integration
  * @task  meta  Application Management
  */
 abstract class PhabricatorApplication
   extends Phobject
   implements PhabricatorPolicyInterface {
 
   const MAX_STATUS_ITEMS      = 100;
 
   const GROUP_CORE            = 'core';
   const GROUP_UTILITIES       = 'util';
   const GROUP_ADMIN           = 'admin';
   const GROUP_DEVELOPER       = 'developer';
 
   final public static function getApplicationGroups() {
     return array(
       self::GROUP_CORE          => pht('Core Applications'),
       self::GROUP_UTILITIES     => pht('Utilities'),
       self::GROUP_ADMIN         => pht('Administration'),
       self::GROUP_DEVELOPER     => pht('Developer Tools'),
     );
   }
 
 
 /* -(  Application Information  )-------------------------------------------- */
 
   abstract public function getName();
 
   public function getShortDescription() {
     return pht('%s Application', $this->getName());
   }
 
   final public function isInstalled() {
     if (!$this->canUninstall()) {
       return true;
     }
 
     $prototypes = PhabricatorEnv::getEnvConfig('phabricator.show-prototypes');
     if (!$prototypes && $this->isPrototype()) {
       return false;
     }
 
     $uninstalled = PhabricatorEnv::getEnvConfig(
       'phabricator.uninstalled-applications');
 
     return empty($uninstalled[get_class($this)]);
   }
 
 
   public function isPrototype() {
     return false;
   }
 
 
   /**
    * Return `true` if this application should never appear in application lists
    * in the UI. Primarily intended for unit test applications or other
    * pseudo-applications.
    *
    * Few applications should be unlisted. For most applications, use
    * @{method:isLaunchable} to hide them from main launch views instead.
    *
    * @return bool True to remove application from UI lists.
    */
   public function isUnlisted() {
     return false;
   }
 
 
   /**
    * Return `true` if this application is a normal application with a base
    * URI and a web interface.
    *
    * Launchable applications can be pinned to the home page, and show up in the
    * "Launcher" view of the Applications application. Making an application
    * unlauncahble prevents pinning and hides it from this view.
    *
    * Usually, an application should be marked unlaunchable if:
    *
    *   - it is available on every page anyway (like search); or
    *   - it does not have a web interface (like subscriptions); or
    *   - it is still pre-release and being intentionally buried.
    *
    * To hide applications more completely, use @{method:isUnlisted}.
    *
    * @return bool True if the application is launchable.
    */
   public function isLaunchable() {
     return true;
   }
 
 
   /**
    * Return `true` if this application should be pinned by default.
    *
    * Users who have not yet set preferences see a default list of applications.
    *
    * @param PhabricatorUser User viewing the pinned application list.
    * @return bool True if this application should be pinned by default.
    */
   public function isPinnedByDefault(PhabricatorUser $viewer) {
     return false;
   }
 
 
   /**
    * Returns true if an application is first-party (developed by Phacility)
    * and false otherwise.
    *
    * @return bool True if this application is developed by Phacility.
    */
   final public function isFirstParty() {
     $where = id(new ReflectionClass($this))->getFileName();
     $root = phutil_get_library_root('phabricator');
 
     if (!Filesystem::isDescendant($where, $root)) {
       return false;
     }
 
     if (Filesystem::isDescendant($where, $root.'/extensions')) {
       return false;
     }
 
     return true;
   }
 
   public function canUninstall() {
     return true;
   }
 
   final public function getPHID() {
     return 'PHID-APPS-'.get_class($this);
   }
 
   public function getTypeaheadURI() {
     return $this->isLaunchable() ? $this->getBaseURI() : null;
   }
 
   public function getBaseURI() {
     return null;
   }
 
   final public function getApplicationURI($path = '') {
     return $this->getBaseURI().ltrim($path, '/');
   }
 
   public function getIconURI() {
     return null;
   }
 
   public function getFontIcon() {
     return 'fa-puzzle-piece';
   }
 
   public function getApplicationOrder() {
     return PHP_INT_MAX;
   }
 
   public function getApplicationGroup() {
     return self::GROUP_CORE;
   }
 
   public function getTitleGlyph() {
     return null;
   }
 
   final public function getHelpMenuItems(PhabricatorUser $viewer) {
     $items = array();
 
     $articles = $this->getHelpDocumentationArticles($viewer);
     if ($articles) {
       $items[] = id(new PHUIListItemView())
         ->setType(PHUIListItemView::TYPE_LABEL)
         ->setName(pht('%s Documentation', $this->getName()));
       foreach ($articles as $article) {
         $item = id(new PHUIListItemView())
           ->setName($article['name'])
           ->setIcon('fa-book')
           ->setHref($article['href']);
 
         $items[] = $item;
       }
     }
 
     $command_specs = $this->getMailCommandObjects();
     if ($command_specs) {
       $items[] = id(new PHUIListItemView())
         ->setType(PHUIListItemView::TYPE_LABEL)
         ->setName(pht('Email Help'));
       foreach ($command_specs as $key => $spec) {
         $object = $spec['object'];
 
         $class = get_class($this);
         $href = '/applications/mailcommands/'.$class.'/'.$key.'/';
 
         $item = id(new PHUIListItemView())
           ->setName($spec['name'])
           ->setIcon('fa-envelope-o')
           ->setHref($href);
         $items[] = $item;
       }
     }
 
     return $items;
   }
 
   public function getHelpDocumentationArticles(PhabricatorUser $viewer) {
     return array();
   }
 
   public function getOverview() {
     return null;
   }
 
   public function getEventListeners() {
     return array();
   }
 
   public function getRemarkupRules() {
     return array();
   }
 
   public function getQuicksandURIPatternBlacklist() {
     return array();
   }
 
   public function getMailCommandObjects() {
     return array();
   }
 
 
 /* -(  URI Routing  )-------------------------------------------------------- */
 
 
   public function getRoutes() {
     return array();
   }
 
 
 /* -(  Email Integration  )-------------------------------------------------- */
 
 
   public function supportsEmailIntegration() {
     return false;
   }
 
   final protected function getInboundEmailSupportLink() {
     return PhabricatorEnv::getDocLink('Configuring Inbound Email');
   }
 
   public function getAppEmailBlurb() {
     throw new PhutilMethodNotImplementedException();
   }
 
 
 /* -(  Fact Integration  )--------------------------------------------------- */
 
 
   public function getFactObjectsForAnalysis() {
     return array();
   }
 
 
 /* -(  UI Integration  )----------------------------------------------------- */
 
 
   /**
    * Render status elements (like "3 Waiting Reviews") for application list
    * views. These provide a way to alert users to new or pending action items
    * in applications.
    *
    * @param PhabricatorUser Viewing user.
    * @return list<PhabricatorApplicationStatusView> Application status elements.
    * @task ui
    */
   public function loadStatus(PhabricatorUser $user) {
     return array();
   }
 
   /**
    * @return string
    * @task ui
    */
   final public static function formatStatusCount(
     $count,
     $limit_string = '%s',
     $base_string = '%d') {
     if ($count == self::MAX_STATUS_ITEMS) {
       $count_str = pht($limit_string, ($count - 1).'+');
     } else {
       $count_str = pht($base_string, $count);
     }
     return $count_str;
   }
 
 
   /**
    * You can provide an optional piece of flavor text for the application. This
    * is currently rendered in application launch views if the application has no
    * status elements.
    *
    * @return string|null Flavor text.
    * @task ui
    */
   public function getFlavorText() {
     return null;
   }
 
 
   /**
    * Build items for the main menu.
    *
    * @param  PhabricatorUser    The viewing user.
    * @param  AphrontController  The current controller. May be null for special
    *                            pages like 404, exception handlers, etc.
    * @return list<PHUIListItemView> List of menu items.
    * @task ui
    */
   public function buildMainMenuItems(
     PhabricatorUser $user,
     PhabricatorController $controller = null) {
     return array();
   }
 
 
   /**
    * Build extra items for the main menu. Generally, this is used to render
    * static dropdowns.
    *
    * @param  PhabricatorUser    The viewing user.
    * @param  AphrontController  The current controller. May be null for special
    *                            pages like 404, exception handlers, etc.
    * @return view               List of menu items.
    * @task ui
    */
   public function buildMainMenuExtraNodes(
     PhabricatorUser $viewer,
     PhabricatorController $controller = null) {
     return array();
   }
 
 
   /**
    * Build items for the "quick create" menu.
    *
    * @param   PhabricatorUser         The viewing user.
    * @return  list<PHUIListItemView>  List of menu items.
    */
   public function getQuickCreateItems(PhabricatorUser $viewer) {
     return array();
   }
 
 
 /* -(  Application Management  )--------------------------------------------- */
 
 
   final public static function getByClass($class_name) {
     $selected = null;
     $applications = self::getAllApplications();
 
     foreach ($applications as $application) {
       if (get_class($application) == $class_name) {
         $selected = $application;
         break;
       }
     }
 
     if (!$selected) {
       throw new Exception(pht("No application '%s'!", $class_name));
     }
 
     return $selected;
   }
 
   final public static function getAllApplications() {
     static $applications;
 
     if ($applications === null) {
       $apps = id(new PhutilSymbolLoader())
         ->setAncestorClass(__CLASS__)
         ->loadObjects();
 
       // Reorder the applications into "application order". Notably, this
       // ensures their event handlers register in application order.
       $apps = msort($apps, 'getApplicationOrder');
       $apps = mgroup($apps, 'getApplicationGroup');
 
       $group_order = array_keys(self::getApplicationGroups());
       $apps = array_select_keys($apps, $group_order) + $apps;
 
       $apps = array_mergev($apps);
 
       $applications = $apps;
     }
 
     return $applications;
   }
 
   final public static function getAllInstalledApplications() {
     $all_applications = self::getAllApplications();
     $apps = array();
     foreach ($all_applications as $app) {
       if (!$app->isInstalled()) {
         continue;
       }
 
       $apps[] = $app;
     }
 
     return $apps;
   }
 
 
   /**
    * Determine if an application is installed, by application class name.
    *
    * To check if an application is installed //and// available to a particular
    * viewer, user @{method:isClassInstalledForViewer}.
    *
    * @param string  Application class name.
    * @return bool   True if the class is installed.
    * @task meta
    */
   final public static function isClassInstalled($class) {
     return self::getByClass($class)->isInstalled();
   }
 
 
   /**
    * Determine if an application is installed and available to a viewer, by
    * application class name.
    *
    * To check if an application is installed at all, use
    * @{method:isClassInstalled}.
    *
    * @param string Application class name.
    * @param PhabricatorUser Viewing user.
    * @return bool True if the class is installed for the viewer.
    * @task meta
    */
   final public static function isClassInstalledForViewer(
     $class,
     PhabricatorUser $viewer) {
 
-    if (!self::isClassInstalled($class)) {
-      return false;
+    $cache = PhabricatorCaches::getRequestCache();
+    $viewer_phid = $viewer->getPHID();
+    $key = 'app.'.$class.'.installed.'.$viewer_phid;
+
+    $result = $cache->getKey($key);
+    if ($result === null) {
+      if (!self::isClassInstalled($class)) {
+        $result = false;
+      } else {
+        $result = PhabricatorPolicyFilter::hasCapability(
+          $viewer,
+          self::getByClass($class),
+          PhabricatorPolicyCapability::CAN_VIEW);
+      }
+
+      $cache->setKey($key, $result);
     }
 
-    return PhabricatorPolicyFilter::hasCapability(
-      $viewer,
-      self::getByClass($class),
-      PhabricatorPolicyCapability::CAN_VIEW);
+    return $result;
   }
 
 
 /* -(  PhabricatorPolicyInterface  )----------------------------------------- */
 
 
   public function getCapabilities() {
     return array_merge(
       array(
         PhabricatorPolicyCapability::CAN_VIEW,
         PhabricatorPolicyCapability::CAN_EDIT,
       ),
       array_keys($this->getCustomCapabilities()));
   }
 
   public function getPolicy($capability) {
     $default = $this->getCustomPolicySetting($capability);
     if ($default) {
       return $default;
     }
 
     switch ($capability) {
       case PhabricatorPolicyCapability::CAN_VIEW:
         return PhabricatorPolicies::getMostOpenPolicy();
       case PhabricatorPolicyCapability::CAN_EDIT:
         return PhabricatorPolicies::POLICY_ADMIN;
       default:
         $spec = $this->getCustomCapabilitySpecification($capability);
         return idx($spec, 'default', PhabricatorPolicies::POLICY_USER);
     }
   }
 
   public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
     return false;
   }
 
   public function describeAutomaticCapability($capability) {
     return null;
   }
 
 
 /* -(  Policies  )----------------------------------------------------------- */
 
   protected function getCustomCapabilities() {
     return array();
   }
 
   final private function getCustomPolicySetting($capability) {
     if (!$this->isCapabilityEditable($capability)) {
       return null;
     }
 
     $policy_locked = PhabricatorEnv::getEnvConfig('policy.locked');
     if (isset($policy_locked[$capability])) {
       return $policy_locked[$capability];
     }
 
     $config = PhabricatorEnv::getEnvConfig('phabricator.application-settings');
 
     $app = idx($config, $this->getPHID());
     if (!$app) {
       return null;
     }
 
     $policy = idx($app, 'policy');
     if (!$policy) {
       return null;
     }
 
     return idx($policy, $capability);
   }
 
 
   final private function getCustomCapabilitySpecification($capability) {
     $custom = $this->getCustomCapabilities();
     if (!isset($custom[$capability])) {
       throw new Exception(pht("Unknown capability '%s'!", $capability));
     }
     return $custom[$capability];
   }
 
   final public function getCapabilityLabel($capability) {
     switch ($capability) {
       case PhabricatorPolicyCapability::CAN_VIEW:
         return pht('Can Use Application');
       case PhabricatorPolicyCapability::CAN_EDIT:
         return pht('Can Configure Application');
     }
 
     $capobj = PhabricatorPolicyCapability::getCapabilityByKey($capability);
     if ($capobj) {
       return $capobj->getCapabilityName();
     }
 
     return null;
   }
 
   final public function isCapabilityEditable($capability) {
     switch ($capability) {
       case PhabricatorPolicyCapability::CAN_VIEW:
         return $this->canUninstall();
       case PhabricatorPolicyCapability::CAN_EDIT:
         return false;
       default:
         $spec = $this->getCustomCapabilitySpecification($capability);
         return idx($spec, 'edit', true);
     }
   }
 
   final public function getCapabilityCaption($capability) {
     switch ($capability) {
       case PhabricatorPolicyCapability::CAN_VIEW:
         if (!$this->canUninstall()) {
           return pht(
             'This application is required for Phabricator to operate, so all '.
             'users must have access to it.');
         } else {
           return null;
         }
       case PhabricatorPolicyCapability::CAN_EDIT:
         return null;
       default:
         $spec = $this->getCustomCapabilitySpecification($capability);
         return idx($spec, 'caption');
     }
   }
 
   final public function getCapabilityTemplatePHIDType($capability) {
     switch ($capability) {
       case PhabricatorPolicyCapability::CAN_VIEW:
       case PhabricatorPolicyCapability::CAN_EDIT:
         return null;
     }
 
     $spec = $this->getCustomCapabilitySpecification($capability);
     return idx($spec, 'template');
   }
 
   public function getApplicationSearchDocumentTypes() {
     return array();
   }
 
 }
diff --git a/src/infrastructure/query/policy/PhabricatorPolicyAwareQuery.php b/src/infrastructure/query/policy/PhabricatorPolicyAwareQuery.php
index e7b9b35628..30b7b8dd0e 100644
--- a/src/infrastructure/query/policy/PhabricatorPolicyAwareQuery.php
+++ b/src/infrastructure/query/policy/PhabricatorPolicyAwareQuery.php
@@ -1,699 +1,690 @@
 <?php
 
 /**
  * A @{class:PhabricatorQuery} which filters results according to visibility
  * policies for the querying user. Broadly, this class allows you to implement
  * a query that returns only objects the user is allowed to see.
  *
  *   $results = id(new ExampleQuery())
  *     ->setViewer($user)
  *     ->withConstraint($example)
  *     ->execute();
  *
  * Normally, you should extend @{class:PhabricatorCursorPagedPolicyAwareQuery},
  * not this class. @{class:PhabricatorCursorPagedPolicyAwareQuery} provides a
  * more practical interface for building usable queries against most object
  * types.
  *
  * NOTE: Although this class extends @{class:PhabricatorOffsetPagedQuery},
  * offset paging with policy filtering is not efficient. All results must be
  * loaded into the application and filtered here: skipping `N` rows via offset
  * is an `O(N)` operation with a large constant. Prefer cursor-based paging
  * with @{class:PhabricatorCursorPagedPolicyAwareQuery}, which can filter far
  * more efficiently in MySQL.
  *
  * @task config     Query Configuration
  * @task exec       Executing Queries
  * @task policyimpl Policy Query Implementation
  */
 abstract class PhabricatorPolicyAwareQuery extends PhabricatorOffsetPagedQuery {
 
   private $viewer;
   private $parentQuery;
   private $rawResultLimit;
   private $capabilities;
   private $workspace = array();
   private $inFlightPHIDs = array();
   private $policyFilteredPHIDs = array();
-  private $canUseApplication;
 
   /**
    * Should we continue or throw an exception when a query result is filtered
    * by policy rules?
    *
    * Values are `true` (raise exceptions), `false` (do not raise exceptions)
    * and `null` (inherit from parent query, with no exceptions by default).
    */
   private $raisePolicyExceptions;
 
 
 /* -(  Query Configuration  )------------------------------------------------ */
 
 
   /**
    * Set the viewer who is executing the query. Results will be filtered
    * according to the viewer's capabilities. You must set a viewer to execute
    * a policy query.
    *
    * @param PhabricatorUser The viewing user.
    * @return this
    * @task config
    */
   final public function setViewer(PhabricatorUser $viewer) {
     $this->viewer = $viewer;
     return $this;
   }
 
 
   /**
    * Get the query's viewer.
    *
    * @return PhabricatorUser The viewing user.
    * @task config
    */
   final public function getViewer() {
     return $this->viewer;
   }
 
 
   /**
    * Set the parent query of this query. This is useful for nested queries so
    * that configuration like whether or not to raise policy exceptions is
    * seamlessly passed along to child queries.
    *
    * @return this
    * @task config
    */
   final public function setParentQuery(PhabricatorPolicyAwareQuery $query) {
     $this->parentQuery = $query;
     return $this;
   }
 
 
   /**
    * Get the parent query. See @{method:setParentQuery} for discussion.
    *
    * @return PhabricatorPolicyAwareQuery The parent query.
    * @task config
    */
   final public function getParentQuery() {
     return $this->parentQuery;
   }
 
 
   /**
    * Hook to configure whether this query should raise policy exceptions.
    *
    * @return this
    * @task config
    */
   final public function setRaisePolicyExceptions($bool) {
     $this->raisePolicyExceptions = $bool;
     return $this;
   }
 
 
   /**
    * @return bool
    * @task config
    */
   final public function shouldRaisePolicyExceptions() {
     return (bool)$this->raisePolicyExceptions;
   }
 
 
   /**
    * @task config
    */
   final public function requireCapabilities(array $capabilities) {
     $this->capabilities = $capabilities;
     return $this;
   }
 
 
 /* -(  Query Execution  )---------------------------------------------------- */
 
 
   /**
    * Execute the query, expecting a single result. This method simplifies
    * loading objects for detail pages or edit views.
    *
    *   // Load one result by ID.
    *   $obj = id(new ExampleQuery())
    *     ->setViewer($user)
    *     ->withIDs(array($id))
    *     ->executeOne();
    *   if (!$obj) {
    *     return new Aphront404Response();
    *   }
    *
    * If zero results match the query, this method returns `null`.
    * If one result matches the query, this method returns that result.
    *
    * If two or more results match the query, this method throws an exception.
    * You should use this method only when the query constraints guarantee at
    * most one match (e.g., selecting a specific ID or PHID).
    *
    * If one result matches the query but it is caught by the policy filter (for
    * example, the user is trying to view or edit an object which exists but
    * which they do not have permission to see) a policy exception is thrown.
    *
    * @return mixed Single result, or null.
    * @task exec
    */
   final public function executeOne() {
 
     $this->setRaisePolicyExceptions(true);
     try {
       $results = $this->execute();
     } catch (Exception $ex) {
       $this->setRaisePolicyExceptions(false);
       throw $ex;
     }
 
     if (count($results) > 1) {
       throw new Exception(pht('Expected a single result!'));
     }
 
     if (!$results) {
       return null;
     }
 
     return head($results);
   }
 
 
   /**
    * Execute the query, loading all visible results.
    *
    * @return list<PhabricatorPolicyInterface> Result objects.
    * @task exec
    */
   final public function execute() {
     if (!$this->viewer) {
       throw new PhutilInvalidStateException('setViewer');
     }
 
     $parent_query = $this->getParentQuery();
     if ($parent_query && ($this->raisePolicyExceptions === null)) {
       $this->setRaisePolicyExceptions(
         $parent_query->shouldRaisePolicyExceptions());
     }
 
     $results = array();
 
     $filter = $this->getPolicyFilter();
 
     $offset = (int)$this->getOffset();
     $limit  = (int)$this->getLimit();
     $count  = 0;
 
     if ($limit) {
       $need = $offset + $limit;
     } else {
       $need = 0;
     }
 
     $this->willExecute();
 
     do {
       if ($need) {
         $this->rawResultLimit = min($need - $count, 1024);
       } else {
         $this->rawResultLimit = 0;
       }
 
       if ($this->canViewerUseQueryApplication()) {
         try {
           $page = $this->loadPage();
         } catch (PhabricatorEmptyQueryException $ex) {
           $page = array();
         }
       } else {
         $page = array();
       }
 
       if ($page) {
         $maybe_visible = $this->willFilterPage($page);
       } else {
         $maybe_visible = array();
       }
 
       if ($this->shouldDisablePolicyFiltering()) {
         $visible = $maybe_visible;
       } else {
         $visible = $filter->apply($maybe_visible);
 
         $policy_filtered = array();
         foreach ($maybe_visible as $key => $object) {
           if (empty($visible[$key])) {
             $phid = $object->getPHID();
             if ($phid) {
               $policy_filtered[$phid] = $phid;
             }
           }
         }
         $this->addPolicyFilteredPHIDs($policy_filtered);
       }
 
       if ($visible) {
         $this->putObjectsInWorkspace($this->getWorkspaceMapForPage($visible));
         $visible = $this->didFilterPage($visible);
       }
 
       $removed = array();
       foreach ($maybe_visible as $key => $object) {
         if (empty($visible[$key])) {
           $removed[$key] = $object;
         }
       }
 
       $this->didFilterResults($removed);
 
       foreach ($visible as $key => $result) {
         ++$count;
 
         // If we have an offset, we just ignore that many results and start
         // storing them only once we've hit the offset. This reduces memory
         // requirements for large offsets, compared to storing them all and
         // slicing them away later.
         if ($count > $offset) {
           $results[$key] = $result;
         }
 
         if ($need && ($count >= $need)) {
           // If we have all the rows we need, break out of the paging query.
           break 2;
         }
       }
 
       if (!$this->rawResultLimit) {
         // If we don't have a load count, we loaded all the results. We do
         // not need to load another page.
         break;
       }
 
       if (count($page) < $this->rawResultLimit) {
         // If we have a load count but the unfiltered results contained fewer
         // objects, we know this was the last page of objects; we do not need
         // to load another page because we can deduce it would be empty.
         break;
       }
 
       $this->nextPage($page);
     } while (true);
 
     $results = $this->didLoadResults($results);
 
     return $results;
   }
 
   private function getPolicyFilter() {
     $filter = new PhabricatorPolicyFilter();
     $filter->setViewer($this->viewer);
     $capabilities = $this->getRequiredCapabilities();
     $filter->requireCapabilities($capabilities);
     $filter->raisePolicyExceptions($this->shouldRaisePolicyExceptions());
 
     return $filter;
   }
 
   protected function getRequiredCapabilities() {
     if ($this->capabilities) {
       return $this->capabilities;
     }
 
     return array(
       PhabricatorPolicyCapability::CAN_VIEW,
     );
   }
 
   protected function applyPolicyFilter(array $objects, array $capabilities) {
     if ($this->shouldDisablePolicyFiltering()) {
       return $objects;
     }
     $filter = $this->getPolicyFilter();
     $filter->requireCapabilities($capabilities);
     return $filter->apply($objects);
   }
 
   protected function didRejectResult(PhabricatorPolicyInterface $object) {
     // Some objects (like commits) may be rejected because related objects
     // (like repositories) can not be loaded. In some cases, we may need these
     // related objects to determine the object policy, so it's expected that
     // we may occasionally be unable to determine the policy.
 
     try {
       $policy = $object->getPolicy(PhabricatorPolicyCapability::CAN_VIEW);
     } catch (Exception $ex) {
       $policy = null;
     }
 
     // Mark this object as filtered so handles can render "Restricted" instead
     // of "Unknown".
     $phid = $object->getPHID();
     $this->addPolicyFilteredPHIDs(array($phid => $phid));
 
     $this->getPolicyFilter()->rejectObject(
       $object,
       $policy,
       PhabricatorPolicyCapability::CAN_VIEW);
   }
 
   public function addPolicyFilteredPHIDs(array $phids) {
     $this->policyFilteredPHIDs += $phids;
     if ($this->getParentQuery()) {
       $this->getParentQuery()->addPolicyFilteredPHIDs($phids);
     }
     return $this;
   }
 
   /**
    * Return a map of all object PHIDs which were loaded in the query but
    * filtered out by policy constraints. This allows a caller to distinguish
    * between objects which do not exist (or, at least, were filtered at the
    * content level) and objects which exist but aren't visible.
    *
    * @return map<phid, phid> Map of object PHIDs which were filtered
    *   by policies.
    * @task exec
    */
   public function getPolicyFilteredPHIDs() {
     return $this->policyFilteredPHIDs;
   }
 
 
 /* -(  Query Workspace  )---------------------------------------------------- */
 
 
   /**
    * Put a map of objects into the query workspace. Many queries perform
    * subqueries, which can eventually end up loading the same objects more than
    * once (often to perform policy checks).
    *
    * For example, loading a user may load the user's profile image, which might
    * load the user object again in order to verify that the viewer has
    * permission to see the file.
    *
    * The "query workspace" allows queries to load objects from elsewhere in a
    * query block instead of refetching them.
    *
    * When using the query workspace, it's important to obey two rules:
    *
    * **Never put objects into the workspace which the viewer may not be able
    * to see**. You need to apply all policy filtering //before// putting
    * objects in the workspace. Otherwise, subqueries may read the objects and
    * use them to permit access to content the user shouldn't be able to view.
    *
    * **Fully enrich objects pulled from the workspace.** After pulling objects
    * from the workspace, you still need to load and attach any additional
    * content the query requests. Otherwise, a query might return objects without
    * requested content.
    *
    * Generally, you do not need to update the workspace yourself: it is
    * automatically populated as a side effect of objects surviving policy
    * filtering.
    *
    * @param map<phid, PhabricatorPolicyInterface> Objects to add to the query
    *   workspace.
    * @return this
    * @task workspace
    */
   public function putObjectsInWorkspace(array $objects) {
     assert_instances_of($objects, 'PhabricatorPolicyInterface');
 
     $viewer_phid = $this->getViewer()->getPHID();
 
     // The workspace is scoped per viewer to prevent accidental contamination.
     if (empty($this->workspace[$viewer_phid])) {
       $this->workspace[$viewer_phid] = array();
     }
 
     $this->workspace[$viewer_phid] += $objects;
 
     return $this;
   }
 
 
   /**
    * Retrieve objects from the query workspace. For more discussion about the
    * workspace mechanism, see @{method:putObjectsInWorkspace}. This method
    * searches both the current query's workspace and the workspaces of parent
    * queries.
    *
    * @param list<phid> List of PHIDs to retrieve.
    * @return this
    * @task workspace
    */
   public function getObjectsFromWorkspace(array $phids) {
     $viewer_phid = $this->getViewer()->getPHID();
 
     $results = array();
     foreach ($phids as $key => $phid) {
       if (isset($this->workspace[$viewer_phid][$phid])) {
         $results[$phid] = $this->workspace[$viewer_phid][$phid];
         unset($phids[$key]);
       }
     }
 
     if ($phids && $this->getParentQuery()) {
       $results += $this->getParentQuery()->getObjectsFromWorkspace($phids);
     }
 
     return $results;
   }
 
 
   /**
    * Convert a result page to a `<phid, PhabricatorPolicyInterface>` map.
    *
    * @param list<PhabricatorPolicyInterface> Objects.
    * @return map<phid, PhabricatorPolicyInterface> Map of objects which can
    *   be put into the workspace.
    * @task workspace
    */
   protected function getWorkspaceMapForPage(array $results) {
     $map = array();
     foreach ($results as $result) {
       $phid = $result->getPHID();
       if ($phid !== null) {
         $map[$phid] = $result;
       }
     }
 
     return $map;
   }
 
 
   /**
    * Mark PHIDs as in flight.
    *
    * PHIDs which are "in flight" are actively being queried for. Using this
    * list can prevent infinite query loops by aborting queries which cycle.
    *
    * @param list<phid> List of PHIDs which are now in flight.
    * @return this
    */
   public function putPHIDsInFlight(array $phids) {
     foreach ($phids as $phid) {
       $this->inFlightPHIDs[$phid] = $phid;
     }
     return $this;
   }
 
 
   /**
    * Get PHIDs which are currently in flight.
    *
    * PHIDs which are "in flight" are actively being queried for.
    *
    * @return map<phid, phid> PHIDs currently in flight.
    */
   public function getPHIDsInFlight() {
     $results = $this->inFlightPHIDs;
     if ($this->getParentQuery()) {
       $results += $this->getParentQuery()->getPHIDsInFlight();
     }
     return $results;
   }
 
 
 /* -(  Policy Query Implementation  )---------------------------------------- */
 
 
   /**
    * Get the number of results @{method:loadPage} should load. If the value is
    * 0, @{method:loadPage} should load all available results.
    *
    * @return int The number of results to load, or 0 for all results.
    * @task policyimpl
    */
   final protected function getRawResultLimit() {
     return $this->rawResultLimit;
   }
 
 
   /**
    * Hook invoked before query execution. Generally, implementations should
    * reset any internal cursors.
    *
    * @return void
    * @task policyimpl
    */
   protected function willExecute() {
     return;
   }
 
 
   /**
    * Load a raw page of results. Generally, implementations should load objects
    * from the database. They should attempt to return the number of results
    * hinted by @{method:getRawResultLimit}.
    *
    * @return list<PhabricatorPolicyInterface> List of filterable policy objects.
    * @task policyimpl
    */
   abstract protected function loadPage();
 
 
   /**
    * Update internal state so that the next call to @{method:loadPage} will
    * return new results. Generally, you should adjust a cursor position based
    * on the provided result page.
    *
    * @param list<PhabricatorPolicyInterface> The current page of results.
    * @return void
    * @task policyimpl
    */
   abstract protected function nextPage(array $page);
 
 
   /**
    * Hook for applying a page filter prior to the privacy filter. This allows
    * you to drop some items from the result set without creating problems with
    * pagination or cursor updates. You can also load and attach data which is
    * required to perform policy filtering.
    *
    * Generally, you should load non-policy data and perform non-policy filtering
    * later, in @{method:didFilterPage}. Strictly fewer objects will make it that
    * far (so the program will load less data) and subqueries from that context
    * can use the query workspace to further reduce query load.
    *
    * This method will only be called if data is available. Implementations
    * do not need to handle the case of no results specially.
    *
    * @param   list<wild>  Results from `loadPage()`.
    * @return  list<PhabricatorPolicyInterface> Objects for policy filtering.
    * @task policyimpl
    */
   protected function willFilterPage(array $page) {
     return $page;
   }
 
   /**
    * Hook for performing additional non-policy loading or filtering after an
    * object has satisfied all policy checks. Generally, this means loading and
    * attaching related data.
    *
    * Subqueries executed during this phase can use the query workspace, which
    * may improve performance or make circular policies resolvable. Data which
    * is not necessary for policy filtering should generally be loaded here.
    *
    * This callback can still filter objects (for example, if attachable data
    * is discovered to not exist), but should not do so for policy reasons.
    *
    * This method will only be called if data is available. Implementations do
    * not need to handle the case of no results specially.
    *
    * @param list<wild> Results from @{method:willFilterPage()}.
    * @return list<PhabricatorPolicyInterface> Objects after additional
    *   non-policy processing.
    */
   protected function didFilterPage(array $page) {
     return $page;
   }
 
 
   /**
    * Hook for removing filtered results from alternate result sets. This
    * hook will be called with any objects which were returned by the query but
    * filtered for policy reasons. The query should remove them from any cached
    * or partial result sets.
    *
    * @param list<wild>  List of objects that should not be returned by alternate
    *                    result mechanisms.
    * @return void
    * @task policyimpl
    */
   protected function didFilterResults(array $results) {
     return;
   }
 
 
   /**
    * Hook for applying final adjustments before results are returned. This is
    * used by @{class:PhabricatorCursorPagedPolicyAwareQuery} to reverse results
    * that are queried during reverse paging.
    *
    * @param   list<PhabricatorPolicyInterface> Query results.
    * @return  list<PhabricatorPolicyInterface> Final results.
    * @task policyimpl
    */
   protected function didLoadResults(array $results) {
     return $results;
   }
 
 
   /**
    * Allows a subclass to disable policy filtering. This method is dangerous.
    * It should be used only if the query loads data which has already been
    * filtered (for example, because it wraps some other query which uses
    * normal policy filtering).
    *
    * @return bool True to disable all policy filtering.
    * @task policyimpl
    */
   protected function shouldDisablePolicyFiltering() {
     return false;
   }
 
 
   /**
    * If this query belongs to an application, return the application class name
    * here. This will prevent the query from returning results if the viewer can
    * not access the application.
    *
    * If this query does not belong to an application, return `null`.
    *
    * @return string|null Application class name.
    */
   abstract public function getQueryApplicationClass();
 
 
   /**
    * Determine if the viewer has permission to use this query's application.
    * For queries which aren't part of an application, this method always returns
    * true.
    *
    * @return bool True if the viewer has application-level permission to
    *   execute the query.
    */
   public function canViewerUseQueryApplication() {
-    if ($this->canUseApplication === null) {
-      $class = $this->getQueryApplicationClass();
-      if (!$class) {
-        $this->canUseApplication = true;
-      } else {
-        $result = id(new PhabricatorApplicationQuery())
-          ->setViewer($this->getViewer())
-          ->withClasses(array($class))
-          ->execute();
-
-        $this->canUseApplication = (bool)$result;
-      }
+    $class = $this->getQueryApplicationClass();
+    if (!$class) {
+      return true;
     }
 
-    return $this->canUseApplication;
+    $viewer = $this->getViewer();
+    return PhabricatorApplication::isClassInstalledForViewer($class, $viewer);
   }
 
 }