Firefox Extension Development: Content Script and HTTP Request

firefox logo with extension logo

I recently got into Firefox extension development with the WebExtension API. In this tutorial, I will create an example extension that uses a single content script and an XMLHTTPRequest to add duckduckgo result rankings into Google search results. If the result in question appears in duckduckgo results for the same query, an orange box with the position will be placed next to the result. Not insanely useful, but a realistic example to get your feet wet with Firefox extension development.

Screenshot of extension in use, showing Google search results.
Example of extension working. We can see the top result on Google is also the top result on duckduckgo.

Step 1: Firefox Extension Manifest.json

Every extension needs a manifest.json file. This file tells Firefox the information it needs to know about the extension such as version, name and description. It is also where we will be able to specify scripts to load.

{
  "manifest_version": 2,
  "name": "DDG Position",
  "version": "1.0",

  "description": "Places a marker with the position a result in Google appears on duckduckgo for the same search term.",

  "content_scripts": [
    {
      "matches": ["*://*.google.com/search?*"],
      "js": ["ddgposition.js"]
    }
  ],

  "permissions": [
    "*://duckduckgo.com/html/*"
  ]
}

The above is the complete manifest.json code we will need for the extension. The manifest version specifies the manifest version to use and must always be 2 in Firefox for now. Name, version, and description are somewhat self-explanatory.

Content_scripts is where things get more interesting. WebExtensions have two types of scripts they can use: content scripts and background scripts. Content scripts can access page content but have limited access to WebExtension APIs. Background scripts have full access to WebExtension APIs but can’t change page content. The two can communicate with each other through a messaging system. In the case of our extension, we only need a single content script. Content scripts can make HTTP Requests and modify the page, and those are the only two things we need to do. The matches array specifies which pages Firefox should load the script on. The Google URL pattern allows it to match Google search result pages.

You will also need the permissions array. By placing this duckduckgo URL pattern in the array, the extension can make a request to a URL matching that pattern without being affected by cross site scripting security restrictions. If you find later in the tutorial that the web request isn’t working, make sure you have the permissions array set up.

Step 2: ddgposition.js Test

Next we need to create the ddgposition.js file. To start, put this simple border changing line suggested in Mozilla’s documentation. That way, you know the script is working.

document.body.style.border = "5px solid red";

Next, load the extension by navigating to about:debugging and click Load Temporary Add-on… and select the manifest.json file.

Screenshot showing loading of temporary add-on in firefox

Now visit a Google search result page. If it has a red border around it, everything is working! Move on to the next step. If not, check if any errors were identified in about:debugging that caused the add-on to not load properly. If not, check the on page console on the search results and finally the browser console (found under Web Developer in the menu). When no errors appear, simply double check everything is typed in correctly. Sometimes there is an error but it doesn’t show anywhere.

Step 3: ddgposition.js

The full code for the finished ddgposition.js is below.

// get the GET variables in the URL
const queryString = window.location.search;

const urlParams = new URLSearchParams(queryString);

// check if it has a q GET paramater, which represents the search term.
if (urlParams.has('q')) {
  const searchQuery = urlParams.get('q');

  // setup the HTTP request to get ddg results
  let xhr = new XMLHttpRequest();

  xhr.onload = function () {
    // this function will be called when the results come in, this line gets a list of links
    let results = Array.from(this.responseXML.getElementsByClassName('result')).map(result => result.getElementsByClassName('result__a')[0].href);
    // remove links that are ads
    results = results.filter((result) => !result.startsWith('https://duckduckgo.com/y.js'));
    // pass the array of links to the applyResults function
    applyResults(results);
  };

  xhr.onerror = function () {
    console.log('An error occurred');
  }

  // open and send the request.
  xhr.open('GET', 'https://duckduckgo.com/html/?q=' + searchQuery, true);
  xhr.responseType = 'document';
  xhr.send();

} else {
  console.log('Couldn\'t get query');
}

function applyResults(results) {
  // this is for debugging
  console.log(results);

  // get a list of 'a' elements that are google search results
  let googleResults = Array.from(document.getElementsByClassName('r')).map(result => result.getElementsByTagName('a')[0]);

  for (const gResult of googleResults) {
    // establish result URL. Some results are links to google.com/url, while others are direct.
    let url = gResult.href;
    if (url.startsWith(window.location.origin + '/url')) {
      const _urlParams = new URLSearchParams(url);
      if (_urlParams.has('url')) {
        url = _urlParams.get('url');
      } else {
        // this url seems to be malformed, just move on
        continue;
      }
    }

    // Check if URL appears in DDG results, if so, get position and create the div for the little orange box.
    const ddgPosition = results.findIndex((element) => element === url);
    if (ddgPosition >= 0)
      gResult.insertAdjacentHTML('beforeend', '<div style="background-color: #de5833; position: absolute; top:0; right:0;"><p style="font-size: 15px; color: white; margin: 0; padding: 2px 9px 2px 9px;">'+(ddgPosition+1)+'</p></div>');

  }
}

The code gets the search query, creates a web request to https://duckduckgo.com/html for the same search term, parses the results, and finally compares them with the google results and inserts the little orange boxes where necessary. The HTML version of DuckDuckGo is used because otherwise the results would not be sent and a message to turn on Javascript is sent instead.

After inserting the above code, you should be able to see the orange boxes in your Google search results. For more information on Firefox extension development, refer to Mozilla’s documentation. If you are having problems or have questions, feel free to leave a comment below!

One thought on “Firefox Extension Development: Content Script and HTTP Request”

Leave a Reply

Your email address will not be published. Required fields are marked *