# firefox/extension.js

    1// Copyright 2014 Selenium committers2// Copyright 2014 Software Freedom Conservancy3//4// Licensed under the Apache License, Version 2.0 (the "License");5// you may not use this file except in compliance with the License.6//     You may obtain a copy of the License at7//8// http://www.apache.org/licenses/LICENSE-2.09//10// Unless required by applicable law or agreed to in writing, software11// distributed under the License is distributed on an "AS IS" BASIS,12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.13// See the License for the specific language governing permissions and14// limitations under the License.1516/** @fileoverview Utilities for working with Firefox extensions. */1718'use strict';1920var AdmZip = require('adm-zip'),21    fs = require('fs'),22    path = require('path'),23    util = require('util'),24    xml = require('xml2js');2526var promise = require('..').promise,27    checkedCall = promise.checkedNodeCall,28    io = require('../io');293031/**32 * Thrown when there an add-on is malformed.33 * @param {string} msg The error message.34 * @constructor35 * @extends {Error}36 */37function AddonFormatError(msg) {38  Error.call(this);3940  Error.captureStackTrace(this, AddonFormatError);4142  /** @override */43  this.name = AddonFormatError.name;4445  /** @override */46  this.message = msg;47}48util.inherits(AddonFormatError, Error);49505152/**53 * Installs an extension to the given directory.54 * @param {string} extension Path to the extension to install, as either a xpi55 *     file or a directory.56 * @param {string} dir Path to the directory to install the extension in.57 * @return {!promise.Promise.<string>} A promise for the add-on ID once58 *     installed.59 */60function install(extension, dir) {61  return getDetails(extension).then(function(details) {62    function returnId() { return details.id; }6364    var dst = path.join(dir, details.id);65    if (extension.slice(-4) === '.xpi') {66      if (!details.unpack) {67        return io.copy(extension, dst + '.xpi').then(returnId);68      } else {69        return checkedCall(fs.readFile, extension).then(function(buff) {70          var zip = new AdmZip(buff);71          // TODO: find an async library for inflating a zip archive.72          new AdmZip(buff).extractAllTo(dst, true);73        }).then(returnId);74      }75    } else {76      return io.copyDir(extension, dst).then(returnId);77    }78  });79}808182/**83 * Describes a Firefox add-on.84 * @typedef {{id: string, name: string, version: string, unpack: boolean}}85 */86var AddonDetails;878889/**90 * Extracts the details needed to install an add-on.91 * @param {string} addonPath Path to the extension directory.92 * @return {!promise.Promise.<!AddonDetails>} A promise for the add-on details.93 */94function getDetails(addonPath) {95  return readManifest(addonPath).then(function(doc) {96    var em = getNamespaceId(doc, 'http://www.mozilla.org/2004/em-rdf#');97    var rdf = getNamespaceId(98        doc, 'http://www.w3.org/1999/02/22-rdf-syntax-ns#');99100    var description = doc[rdf + 'RDF'][rdf + 'Description'][0];101    var details = {102      id: getNodeText(description, em + 'id'),103      name: getNodeText(description, em + 'name'),104      version: getNodeText(description, em + 'version'),105      unpack: getNodeText(description, em + 'unpack') || false106    };107108    if (typeof details.unpack === 'string') {109      details.unpack = details.unpack.toLowerCase() === 'true';110    }111112    if (!details.id) {113      throw new AddonFormatError('Could not find add-on ID for ' + addonPath);114    }115116    return details;117  });118119  function getNodeText(node, name) {120    return node[name] && node[name][0] || '';121  }122123  function getNamespaceId(doc, url) {124    var keys = Object.keys(doc);125    if (keys.length !== 1) {126      throw new AddonFormatError('Malformed manifest for add-on ' + addonPath);127    }128129    var namespaces = doc[keys[0]].$;130    var id = '';131    Object.keys(namespaces).some(function(ns) {132      if (namespaces[ns] !== url) {133        return false;134      }135136      if (ns.indexOf(':') != -1) {137        id = ns.split(':')[1] + ':';138      }139      return true;140    });141    return id;142  }143}144145146/**147 * Reads the manifest for a Firefox add-on.148 * @param {string} addonPath Path to a Firefox add-on as a xpi or an extension.149 * @return {!promise.Promise.<!Object>} A promise for the parsed manifest.150 */151function readManifest(addonPath) {152  var manifest;153154  if (addonPath.slice(-4) === '.xpi') {155    manifest = checkedCall(fs.readFile, addonPath).then(function(buff) {156      var zip = new AdmZip(buff);157      if (!zip.getEntry('install.rdf')) {158        throw new AddonFormatError(159            'Could not find install.rdf in ' + addonPath);160      }161      var done = promise.defer();162      zip.readAsTextAsync('install.rdf', done.fulfill);163      return done.promise;164    });165  } else {166    manifest = checkedCall(fs.stat, addonPath).then(function(stats) {167      if (!stats.isDirectory()) {168        throw Error(169            'Add-on path is niether a xpi nor a directory: ' + addonPath);170      }171      return checkedCall(fs.readFile, path.join(addonPath, 'install.rdf'));172    });173  }174175  return manifest.then(function(content) {176    return checkedCall(xml.parseString, content);177  });178}179180181// PUBLIC API182183184exports.install = install;