DXR is a code search and navigation tool aimed at making sense of large projects. It supports full-text and regex searches as well as structural queries.

Mercurial (192e0e33eb59)

VCS Links

Line Code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

"use strict";

/**
 * This module exports a provider class that is used for providers created by
 * extensions.
 */

var EXPORTED_SYMBOLS = ["UrlbarProviderExtension"];

const { XPCOMUtils } = ChromeUtils.import(
  "resource://gre/modules/XPCOMUtils.jsm"
);
XPCOMUtils.defineLazyModuleGetters(this, {
  PlacesSearchAutocompleteProvider:
    "resource://gre/modules/PlacesSearchAutocompleteProvider.jsm",
  Services: "resource://gre/modules/Services.jsm",
  SkippableTimer: "resource:///modules/UrlbarUtils.jsm",
  UrlbarProvider: "resource:///modules/UrlbarUtils.jsm",
  UrlbarProvidersManager: "resource:///modules/UrlbarProvidersManager.jsm",
  UrlbarResult: "resource:///modules/UrlbarResult.jsm",
  UrlbarUtils: "resource:///modules/UrlbarUtils.jsm",
});

// When we send events to extensions, we wait this amount of time in ms for them
// to respond before timing out.  Tests can override this by setting
// UrlbarProviderExtension.notificationTimeout.
const DEFAULT_NOTIFICATION_TIMEOUT = 200;

/**
 * The browser.urlbar extension API allows extensions to create their own urlbar
 * providers.  The results from extension providers are integrated into the
 * urlbar view just like the results from providers that are built into Firefox.
 *
 * This class is the interface between the provider-related parts of the
 * browser.urlbar extension API implementation and our internal urlbar
 * implementation.  The API implementation should use this class to manage
 * providers created by extensions.  All extension providers must be instances
 * of this class.
 *
 * When an extension requires a provider, the API implementation should call
 * getOrCreate() to get or create it.  When an extension adds an event listener
 * related to a provider, the API implementation should call setEventListener()
 * to register its own event listener with the provider.
 */
class UrlbarProviderExtension extends UrlbarProvider {
  /**
   * Returns the extension provider with the given name, creating it first if
   * it doesn't exist.
   *
   * @param {string} name
   *   The provider name.
   * @returns {UrlbarProviderExtension}
   *   The provider.
   */
  static getOrCreate(name) {
    let provider = UrlbarProvidersManager.getProvider(name);
    if (!provider) {
      provider = new UrlbarProviderExtension(name);
      UrlbarProvidersManager.registerProvider(provider);
    }
    return provider;
  }

  /**
   * Constructor.
   *
   * @param {string} name
   *   The provider's name.
   */
  constructor(name) {
    super();
    this._name = name;
    this._eventListeners = new Map();
    this.behavior = "inactive";
  }

  /**
   * The provider's name.
   */
  get name() {
    return this._name;
  }

  /**
   * The provider's type.  The type of extension providers is always
   * UrlbarUtils.PROVIDER_TYPE.EXTENSION.
   */
  get type() {
    return UrlbarUtils.PROVIDER_TYPE.EXTENSION;
  }

  /**
   * Whether the provider should be invoked for the given context.  If this
   * method returns false, the providers manager won't start a query with this
   * provider, to save on resources.
   *
   * @param {UrlbarQueryContext} context
   *   The query context object.
   * @returns {boolean}
   *   Whether this provider should be invoked for the search.
   */
  isActive(context) {
    return this.behavior != "inactive";
  }

  /**
   * Whether this provider wants to restrict results to just itself.  Other
   * providers won't be invoked, unless this provider doesn't support the
   * current query.
   *
   * @param {UrlbarQueryContext} context
   *   The query context object.
   * @returns {boolean}
   *   Whether this provider wants to restrict results.
   */
  isRestricting(context) {
    return this.behavior == "restricting";
  }

  /**
   * Sets the listener function for an event.  The extension API implementation
   * should call this from its EventManager.register() implementations.  Since
   * EventManager.register() is called at most only once for each extension
   * event (the first time the extension adds a listener for the event), each
   * provider instance needs at most only one listener per event, and that's why
   * this method is named setEventListener instead of addEventListener.
   *
   * The given listener function may return a promise that's resolved once the
   * extension responds to the event, or if the event requires no response from
   * the extension, it may return a non-promise value (possibly nothing).
   *
   * To remove the previously set listener, call this method again but pass null
   * as the listener function.
   *
   * The event name should be one of the following:
   *
   *   behaviorRequested
   *     This event is fired when the provider's behavior is needed from the
   *     extension.  The listener should return a behavior string.
   *   queryCanceled
   *     This event is fired when an ongoing query is canceled.  The listener
   *     shouldn't return anything.
   *   resultsRequested
   *     This event is fired when the provider's results are needed from the
   *     extension.  The listener should return an array of results.
   *
   * @param {string} eventName
   *   The name of the event to listen to.
   * @param {function} listener
   *   The function that will be called when the event is fired.
   */
  setEventListener(eventName, listener) {
    if (listener) {
      this._eventListeners.set(eventName, listener);
    } else {
      this._eventListeners.delete(eventName);
      if (!this._eventListeners.size) {
        UrlbarProvidersManager.unregisterProvider(this);
      }
    }
  }

  /**
   * This method is called by the providers manager before a query starts to
   * update each extension provider's behavior.  It fires the behaviorRequested
   * event.
   *
   * @param {UrlbarQueryContext} context
   *   The query context.
   */
  async updateBehavior(context) {
    let behavior = await this._notifyListener("behaviorRequested", context);
    if (behavior) {
      this.behavior = behavior;
    }
  }

  /**
   * This method is called by the providers manager when a query starts to fetch
   * each extension provider's results.  It fires the resultsRequested event.
   *
   * @param {UrlbarQueryContext} context
   *   The query context.
   * @param {function} addCallback
   *   The callback invoked by this method to add each result.
   */
  async startQuery(context, addCallback) {
    let extResults = await this._notifyListener("resultsRequested", context);
    if (extResults) {
      for (let extResult of extResults) {
        let result = await this._makeUrlbarResult(context, extResult).catch(
          Cu.reportError
        );
        if (result) {
          addCallback(this, result);
        }
      }
    }
  }

  /**
   * This method is called by the providers manager when an ongoing query is
   * canceled.  It fires the queryCanceled event.
   *
   * @param {UrlbarQueryContext} context
   *   The query context.
   */
  cancelQuery(context) {
    this._notifyListener("queryCanceled", context);
  }

  /**
   * This method is called when a result from the provider without a URL is
   * picked, but currently only for tip results.  The provider should handle the
   * pick.
   *
   * @param {UrlbarResult} result
   *   The result that was picked.
   */
  pickResult(result) {
    this._notifyListener("resultPicked", result.payload);
  }

  /**
   * This method is called when the user starts and ends an engagement with the
   * urlbar.
   *
   * @param {boolean} isPrivate
   *   True if the engagement is in a private context.
   * @param {string} state
   *   The state of the engagement, one of: start, engagement, abandonment,
   *   discard.
   */
  onEngagement(isPrivate, state) {
    this._notifyListener("engagement", isPrivate, state);
  }

  /**
   * Calls a listener function set by the extension API implementation, if any.
   *
   * @param {string} eventName
   *   The name of the listener to call (i.e., the name of the event to fire).
   * @param {*} args
   *   Arguments to the listener function.
   * @returns {*}
   *   The value returned by the listener function, if any.
   */
  async _notifyListener(eventName, ...args) {
    let listener = this._eventListeners.get(eventName);
    if (!listener) {
      return undefined;
    }
    let result;
    try {
      result = listener(...args);
    } catch (error) {
      Cu.reportError(error);
      return undefined;
    }
    if (result.catch) {
      // The result is a promise, so wait for it to be resolved.  Set up a timer
      // so that we're not stuck waiting forever.
      let timer = new SkippableTimer({
        name: "UrlbarProviderExtension notification timer",
        time: UrlbarProviderExtension.notificationTimeout,
        reportErrorOnTimeout: true,
      });
      result = await Promise.race([
        timer.promise,
        result.catch(Cu.reportError),
      ]);
      timer.cancel();
    }
    return result;
  }

  /**
   * Converts a plain-JS-object result created by the extension into a
   * UrlbarResult object.
   *
   * @param {UrlbarQueryContext} context
   *   The query context.
   * @param {object} extResult
   *   A plain JS object representing a result created by the extension.
   * @returns {UrlbarResult}
   *   The UrlbarResult object.
   */
  async _makeUrlbarResult(context, extResult) {
    // If the result is a search result, make sure its payload has a valid
    // `engine` property, which is the name of an engine, and which we use later
    // on to look up the nsISearchEngine.  We allow the extension to specify the
    // engine by its name, alias, or domain.  Prefer aliases over domains since
    // one domain can have many engines.
    if (extResult.type == "search") {
      let engine;
      if (extResult.payload.engine) {
        // Validate the engine name by looking it up.
        engine = Services.search.getEngineByName(extResult.payload.engine);
      } else if (extResult.payload.keyword) {
        // Look up the engine by its alias.
        engine = await PlacesSearchAutocompleteProvider.engineForAlias(
          extResult.payload.keyword
        );
      } else if (extResult.payload.url) {
        // Look up the engine by its domain.
        let host;
        try {
          host = new URL(extResult.payload.url).hostname;
        } catch (err) {}
        if (host) {
          engine = await PlacesSearchAutocompleteProvider.engineForDomainPrefix(
            host
          );
        }
      }
      if (!engine) {
        // No engine found.
        throw new Error("Invalid or missing engine specified by extension");
      }
      extResult.payload.engine = engine.name;
    }

    let result = new UrlbarResult(
      UrlbarProviderExtension.RESULT_TYPES[extResult.type],
      UrlbarProviderExtension.SOURCE_TYPES[extResult.source],
      ...UrlbarResult.payloadAndSimpleHighlights(
        context.tokens,
        extResult.payload || {}
      )
    );
    if (extResult.heuristic && this.behavior == "restricting") {
      // The muxer chooses the final heuristic result by taking the first one
      // that claims to be the heuristic.  We don't want extensions to clobber
      // UnifiedComplete's heuristic, so we allow this only if the provider is
      // restricting.
      result.heuristic = extResult.heuristic;
    }
    if (extResult.suggestedIndex !== undefined) {
      result.suggestedIndex = extResult.suggestedIndex;
    }
    return result;
  }
}

// Maps extension result type enums to internal result types.
UrlbarProviderExtension.RESULT_TYPES = {
  keyword: UrlbarUtils.RESULT_TYPE.KEYWORD,
  omnibox: UrlbarUtils.RESULT_TYPE.OMNIBOX,
  remote_tab: UrlbarUtils.RESULT_TYPE.REMOTE_TAB,
  search: UrlbarUtils.RESULT_TYPE.SEARCH,
  tab: UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
  tip: UrlbarUtils.RESULT_TYPE.TIP,
  url: UrlbarUtils.RESULT_TYPE.URL,
};

// Maps extension source type enums to internal source types.
UrlbarProviderExtension.SOURCE_TYPES = {
  bookmarks: UrlbarUtils.RESULT_SOURCE.BOOKMARKS,
  history: UrlbarUtils.RESULT_SOURCE.HISTORY,
  local: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
  network: UrlbarUtils.RESULT_SOURCE.OTHER_NETWORK,
  search: UrlbarUtils.RESULT_SOURCE.SEARCH,
  tabs: UrlbarUtils.RESULT_SOURCE.TABS,
};

UrlbarProviderExtension.notificationTimeout = DEFAULT_NOTIFICATION_TIMEOUT;