Skip to content

Latest commit

 

History

History
796 lines (642 loc) · 20.3 KB

File metadata and controls

796 lines (642 loc) · 20.3 KB
name firefox-extension
description Comprehensive guide for developing WebExtensions (browser extensions) for Mozilla Firefox, including Manifest V2/V3 configuration, all WebExtension APIs, security practices, web-ext CLI, and AMO submission.
metadata
author version tags
mte90
1.0.0
firefox
webextension
browser-extension
mozilla
amo
manifest-v3

Firefox WebExtension Development

Complete reference for building, testing, and publishing browser extensions for Mozilla Firefox.

Overview

Firefox extensions use the WebExtensions API with the browser.* namespace (Promise-based natively). Firefox supports both Manifest V2 and V3, with MV2 NOT deprecated (unlike Chrome).

Key Characteristics:

  • Global namespace: browser (Promise-based)
  • Both MV2 and MV3 supported
  • Firefox-specific APIs: sidebarAction, userScripts, contextualIdentities, protocol_handlers
  • Submission via AMO (addons.mozilla.org)

Manifest Structure

Manifest V3 (Recommended, Firefox 109+)

{
  "manifest_version": 3,
  "name": "Extension Name",
  "version": "1.0.0",
  "description": "Brief description",

  "browser_specific_settings": {
    "gecko": {
      "id": "extension@example.com",
      "strict_min_version": "109.0"
    },
    "gecko_android": {
      "strict_min_version": "109.0"
    }
  },

  "background": {
    "service_worker": "background.js",
    "type": "module"
  },

  "action": {
    "default_popup": "popup.html",
    "default_icon": {
      "16": "icons/icon-16.png",
      "48": "icons/icon-48.png",
      "96": "icons/icon-96.png"
    },
    "default_title": "Extension Title"
  },

  "content_scripts": [
    {
      "matches": ["https://*/*"],
      "js": ["content.js"],
      "css": ["styles.css"],
      "run_at": "document_idle"
    }
  ],

  "permissions": ["storage", "activeTab"],
  "host_permissions": ["https://api.example.com/*"],

  "content_security_policy": {
    "extension_pages": "script-src 'self'; object-src 'self';"
  }
}

Manifest V2 (Still Supported)

{
  "manifest_version": 2,
  "name": "Extension Name",
  "version": "1.0.0",

  "browser_specific_settings": {
    "gecko": {
      "id": "extension@example.com",
      "strict_min_version": "78.0"
    }
  },

  "background": {
    "scripts": ["background.js"],
    "persistent": false
  },

  "browser_action": {
    "default_popup": "popup.html",
    "default_icon": "icons/icon-48.png"
  },

  "permissions": ["storage", "activeTab", "https://*/*"]
}

MV2 vs MV3 Key Differences

Feature MV2 MV3
Toolbar button browser_action action
Background background.scripts / page background.service_worker
Host permissions In permissions Separate host_permissions
Default CSP script-src 'self'; object-src 'self'; script-src 'self'; upgrade-insecure-requests;
Request blocking webRequest.onBeforeRequest declarativeNetRequest

All Manifest Keys Reference

Metadata:

  • name (required) - Extension name
  • version (required) - Version string (e.g., "1.0.0")
  • description - Short description
  • author - Author name
  • homepage_url - Extension homepage
  • icons - Extension icons object

Firefox-Specific:

  • browser_specific_settings.gecko.id - Required for AMO
  • browser_specific_settings.gecko.strict_min_version - Minimum Firefox version
  • browser_specific_settings.gecko.strict_max_version - Maximum Firefox version
  • browser_specific_settings.gecko_android - Android-specific settings

Background & Scripts:

  • background - Service worker (MV3) or scripts/page (MV2)
  • content_scripts - Scripts injected into pages
  • userScripts - User script registration (Firefox-only)
  • declarative_net_request - Rule-based request modification

UI Components:

  • action (MV3) / browser_action (MV2) - Toolbar button
  • page_action - Address bar button
  • sidebar_action - Sidebar panel (Firefox-only)
  • options_ui - Options page configuration
  • devtools_page - DevTools extension page

Permissions:

  • permissions - API permissions
  • host_permissions (MV3) - Host access
  • optional_permissions - Optional API permissions
  • optional_host_permissions - Optional host access

Other:

  • commands - Keyboard shortcuts
  • omnibox - Address bar integration
  • web_accessible_resources - Resources accessible from pages
  • protocol_handlers - Custom protocol handlers
  • chrome_settings_overrides - Override homepage/search
  • chrome_url_overrides - Override new tab/bookmarks

WebExtension APIs

Complete API Namespace List (51 APIs)

API Permission Description
action - Toolbar button (MV3)
alarms alarms Schedule code execution
bookmarks bookmarks Bookmark management
browserAction - Toolbar button (MV2)
browserSettings - Browser settings
browsingData browsingData Clear browsing data
clipboard clipboardWrite Clipboard access
commands - Keyboard shortcuts
contentScripts - Register content scripts
contextualIdentities contextualIdentities Container tabs (Firefox-only)
cookies cookies Cookie management
declarativeNetRequest declarativeNetRequest Rule-based request blocking
devtools devtools DevTools integration
dns dns DNS resolution
downloads downloads Download management
events - Common event types
extension - Extension utilities
find find Find text in pages
history history Browser history
i18n - Internationalization
identity identity OAuth2 authentication
idle idle Idle state detection
management management Installed add-ons info
menus menus Context menu items
notifications notifications System notifications
omnibox - Address bar suggestions
pageAction - Address bar button (MV2)
permissions - Runtime permissions
pkcs11 pkcs11 PKCS#11 modules
privacy privacy Privacy settings
proxy proxy Request proxying
runtime - Extension runtime
scripting scripting Inject scripts/CSS (MV3)
search search Search engines
sessions sessions Closed tabs/windows
sidebarAction - Sidebar (Firefox-only)
storage storage Local/managed storage
tabGroups tabGroups Tab groups
tabs tabs Tab management
theme theme Theme API
topSites topSites Frequently visited
userScripts userScripts User scripts (Firefox-only)
webNavigation webNavigation Navigation events
webRequest webRequest Request interception
windows - Window management

Core API Usage Patterns

Tabs API:

// Query tabs
const tabs = await browser.tabs.query({ active: true, currentWindow: true });

// Create tab
const tab = await browser.tabs.create({ url: 'https://example.com' });

// Update tab
await browser.tabs.update(tabId, { active: true });

// Send message to tab
await browser.tabs.sendMessage(tabId, { action: 'update' });

// Execute script (MV3)
await browser.scripting.executeScript({
  target: { tabId },
  files: ['content.js']
});

Storage API:

// Save data
await browser.storage.local.set({ key: 'value', settings: config });

// Get data
const { key, settings } = await browser.storage.local.get(['key', 'settings']);

// Remove data
await browser.storage.local.remove('key');

// Clear all
await browser.storage.local.clear();

// Listen for changes
browser.storage.onChanged.addListener((changes, area) => {
  if (changes.key) {
    console.log('Old:', changes.key.oldValue, 'New:', changes.key.newValue);
  }
});

Runtime Messaging:

// Content script → Background
const response = await browser.runtime.sendMessage({ action: 'getData' });

// Background listener
browser.runtime.onMessage.addListener((message, sender, sendResponse) => {
  if (message.action === 'getData') {
    return Promise.resolve({ data: 'response' });
  }
});

// Background → Content script
browser.tabs.sendMessage(tabId, { action: 'update' });

// External messaging (from web pages)
browser.runtime.onMessageExternal.addListener((message, sender) => {
  if (sender.id === 'allowed-extension-id') {
    // Handle message
  }
});

Context Menus:

// Create context menu
browser.menus.create({
  id: 'my-menu',
  title: 'My Menu Item',
  contexts: ['selection']
});

// Handle click
browser.menus.onClicked.addListener((info, tab) => {
  if (info.menuItemId === 'my-menu') {
    console.log('Selected:', info.selectionText);
  }
});

Alarms:

// Create alarm
browser.alarms.create('my-alarm', { delayInMinutes: 1, periodInMinutes: 5 });

// Handle alarm
browser.alarms.onAlarm.addListener((alarm) => {
  if (alarm.name === 'my-alarm') {
    // Do work
  }
});

Notifications:

// Show notification
browser.notifications.create({
  type: 'basic',
  iconUrl: 'icon.png',
  title: 'Title',
  message: 'Message'
});

Firefox-Specific APIs

Sidebar Action:

// Toggle sidebar
browser.sidebarAction.open();
browser.sidebarAction.close();

// Set panel
await browser.sidebarAction.setPanel({ panel: 'sidebar.html' });

Container Tabs (Contextual Identities):

// List containers
const containers = await browser.contextualIdentities.query({});

// Create container
const container = await browser.contextualIdentities.create({
  name: 'Work',
  color: 'blue',
  icon: 'briefcase'
});

// Create tab in container
await browser.tabs.create({
  url: 'https://work.example.com',
  cookieStoreId: container.cookieStoreId
});

User Scripts:

// Register user script
await browser.userScripts.register([{
  js: [{ file: 'script.js' }],
  matches: ['*://example.com/*'],
  runAt: 'document_start'
}]);

Security & Content Security Policy

Default CSP

MV3: script-src 'self'; upgrade-insecure-requests; MV2: script-src 'self'; object-src 'self';

CSP Restrictions

Forbidden (causes AMO rejection):

  • Remote script sources
  • 'unsafe-inline'
  • 'unsafe-eval' (except 'wasm-unsafe-eval' for WebAssembly)
  • Data URLs for scripts
  • eval(), new Function(), string-based code execution

Allowed:

  • 'self' - Scripts from extension package
  • 'wasm-unsafe-eval' - WebAssembly support
  • http://localhost:<port> - Development only (remove before submission)

Custom CSP Example

{
  "content_security_policy": {
    "extension_pages": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self';",
    "sandbox": "sandbox allow-scripts allow-forms allow-popups; script-src 'self';"
  }
}

Security Best Practices

  1. Package all dependencies locally - No CDN scripts
  2. Request minimal permissions - Use activeTab instead of <all_urls> when possible
  3. Use optional_permissions - Request permissions at runtime for non-critical features
  4. Validate user input - Sanitize HTML before innerHTML, validate URLs
  5. Use HTTPS - All external requests should use HTTPS
  6. No obfuscated code - Must be reviewable for AMO

web-ext CLI Reference

Installation

npm install -g web-ext

Commands

Run extension (development):

web-ext run                                    # Default Firefox
web-ext run --firefox-path /path/to/firefox    # Specific Firefox
web-ext run --target firefox-android           # Android
web-ext run --profile-create-new               # Clean profile
web-ext run --start-url https://example.com    # Start URL
web-ext run --verbose                          # Verbose output
web-ext run --no-reload                        # Disable auto-reload

Lint extension:

web-ext lint                   # Validate manifest and code
web-ext lint --warnings-as-errors
web-ext lint --self-hosted     # Skip AMO-specific checks

Build extension:

web-ext build                  # Create .zip in web-ext-artifacts/
web-ext build --source-dir ./src
web-ext build --artifacts-dir ./dist

Sign extension:

web-ext sign --api-key $KEY --api-secret $SECRET
web-ext sign --channel listed      # Public on AMO
web-ext sign --channel unlisted    # Direct download

Other commands:

web-ext docs       # Open documentation
web-ext dump-config  # Show configuration

Configuration File (.web-ext-config.js)

module.exports = {
  sourceDir: './src',
  artifactsDir: './dist',

  run: {
    firefox: '/Applications/Firefox.app/Contents/MacOS/firefox',
    startUrl: 'https://example.com',
    pref: ['extensions.webextensions.debug=true']
  },

  build: {
    overwriteDest: true
  },

  sign: {
    apiKey: process.env.AMO_API_KEY,
    apiSecret: process.env.AMO_API_SECRET,
    channel: 'listed'
  }
};

Environment Variables

WEB_EXT_API_KEY=your_key
WEB_EXT_API_SECRET=your_secret
WEB_EXT_SOURCE_DIR=./src
WEB_EXT_ARTIFACTS_DIR=./dist
WEB_EXT_FIREFOX=/path/to/firefox

AMO Submission Process

Pre-Submission Checklist

  • Extension ID in browser_specific_settings.gecko.id
  • web-ext lint passes with no errors
  • All permissions are necessary and documented
  • Privacy policy included (if collecting data)
  • No obfuscated code
  • Source code available (if using build tools)
  • Icons: 48x48 and 96x96 minimum
  • Screenshots: minimum 464x200px
  • Description is clear and accurate

Submission Steps

  1. Build extension:

    web-ext build
  2. Create developer account:

  3. Submit:

    • Go to Developer Hub → "Submit a New Add-on"
    • Upload .zip from web-ext build
    • Choose distribution: Listed (public) or Unlisted (direct)
  4. Fill listing:

    • Name, description, categories
    • Screenshots, icons
    • Privacy policy URL (if collecting data)
    • Support email/URL
  5. Review process:

    • Automated validation: 5-15 minutes
    • Human review: 1-7 days for listed extensions
    • Respond to reviewer comments promptly

Common Rejection Reasons

Reason Solution
Remote code execution Package all scripts locally
Unnecessary permissions Remove unused permissions
Obfuscated code Provide source code
Missing privacy policy Add policy if collecting data
Unclear functionality Improve description
CSP violations Fix CSP to not allow remote scripts
Hidden functionality Disclose all features

Data Collection Requirements

If extension collects/transmits user data, privacy policy must disclose:

  • What data is collected
  • How it's transmitted
  • Purpose of collection
  • User consent mechanism
  • Data retention policy

Testing & Debugging

Development Workflow

# Start development
web-ext run --verbose

# Auto-reloads on file changes
# Access via about:debugging

Debugging Tools

Access debugging:

  1. Open about:debugging#/runtime/this-firefox
  2. Click "Inspect" next to extension

Debug components:

  • Background: Console in debugging page
  • Popup: Right-click popup → "Inspect"
  • Content scripts: Regular page DevTools Console
  • Options page: Right-click options → "Inspect"

Debugging Commands

// Check manifest
browser.runtime.getManifest();

// Check permissions
browser.permissions.contains({ permissions: ['tabs'] });

// Get extension URL
browser.runtime.getURL('/path/to/resource');

// Check last error
if (browser.runtime.lastError) {
  console.error(browser.runtime.lastError);
}

// Reload extension programmatically
browser.runtime.reload();

Testing Strategies

Unit Testing (Jest):

import { mockBrowser } from 'webextension-pockito';

describe('Extension', () => {
  beforeEach(() => mockBrowser.reset());

  test('storage', async () => {
    await browser.storage.local.set({ key: 'value' });
    const result = await browser.storage.local.get('key');
    expect(result.key).toBe('value');
  });
});

Integration Testing (Selenium):

const { Builder } = require('selenium-webdriver');
const firefox = require('selenium-webdriver/firefox');

const driver = await new Builder()
  .forBrowser('firefox')
  .setFirefoxOptions(new firefox.Options()
    .setPreference('extensions.autoDisableScopes', 0))
  .build();

Best Practices

Background Service Worker (MV3)

// Keep service worker alive briefly
let keepAlive;

browser.runtime.onMessage.addListener((msg) => {
  clearTimeout(keepAlive);
  keepAlive = setTimeout(() => {}, 25000);
  return handleMessage(msg);
});

Error Handling

async function safeAsync(fn) {
  try {
    return await fn();
  } catch (error) {
    console.error('Error:', error);
    return { error: error.message };
  }
}

Cross-Browser Compatibility

import browser from 'webextension-polyfill';

// Feature detection
if (browser.sidebarAction) {
  // Firefox-specific
  browser.sidebarAction.open();
}

// Fallback pattern
const action = browser.action || browser.browserAction;
action.setPopup({ popup: 'popup.html' });

Performance

  • Use browser.tabs.query() with specific filters
  • Debounce frequent operations
  • Cache storage API results
  • Use alarms API instead of setInterval in background

Troubleshooting

Common Issues

Error Cause Solution
browser is not defined Script not in extension context Check manifest script paths
Permission denied Missing permission Add to manifest permissions
CSP violation Remote script Move script to extension package
Service worker terminated Long operation Use alarms API, avoid blocking
webRequest not blocking MV3 limitation Use declarativeNetRequest
ID mismatch Different IDs Set consistent ID in manifest

Debug Commands

# Validate manifest
web-ext lint --verbose

# Run with debug prefs
web-ext run --pref extensions.webextensions.debug=true

# Test in clean profile
web-ext run --profile-create-new

# Check specific Firefox
web-ext run --firefox /path/to/firefox-beta

Cross-Browser with webextension-polyfill

Installation

npm install webextension-polyfill

Usage

// ES modules
import browser from 'webextension-polyfill';

// CommonJS
const browser = require('webextension-polyfill');

// Now browser.* works in Chrome with promises
const tabs = await browser.tabs.query({ active: true });

Manifest Setup

{
  "background": {
    "scripts": ["browser-polyfill.js", "background.js"]
  },
  "content_scripts": [{
    "matches": ["<all_urls>"],
    "js": ["browser-polyfill.js", "content.js"]
  }]
}

TypeScript Support

npm install @types/webextension-polyfill
import browser from 'webextension-polyfill';

async function getActiveTab(): Promise<browser.Tabs.Tab> {
  const [tab] = await browser.tabs.query({ active: true });
  return tab;
}

File Structure Template

my-extension/
├── manifest.json
├── background.js
├── content.js
├── popup.html
├── popup.js
├── options.html
├── options.js
├── browser-polyfill.js
├── styles/
│   ├── popup.css
│   └── content.css
├── icons/
│   ├── icon-16.png
│   ├── icon-32.png
│   ├── icon-48.png
│   └── icon-96.png
├── _locales/
│   ├── en/
│   │   └── messages.json
│   └── it/
│       └── messages.json
├── .web-ext-config.js
├── package.json
└── README.md

References