firefox/extension.js

1// Copyright 2014 Selenium committers
2// Copyright 2014 Software Freedom Conservancy
3//
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 at
7//
8// http://www.apache.org/licenses/LICENSE-2.0
9//
10// Unless required by applicable law or agreed to in writing, software
11// 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 and
14// limitations under the License.
15
16/** @fileoverview Utilities for working with Firefox extensions. */
17
18'use strict';
19
20var AdmZip = require('adm-zip'),
21 fs = require('fs'),
22 path = require('path'),
23 util = require('util'),
24 xml = require('xml2js');
25
26var promise = require('..').promise,
27 checkedCall = promise.checkedNodeCall,
28 io = require('../io');
29
30
31/**
32 * Thrown when there an add-on is malformed.
33 * @param {string} msg The error message.
34 * @constructor
35 * @extends {Error}
36 */
37function AddonFormatError(msg) {
38 Error.call(this);
39
40 Error.captureStackTrace(this, AddonFormatError);
41
42 /** @override */
43 this.name = AddonFormatError.name;
44
45 /** @override */
46 this.message = msg;
47}
48util.inherits(AddonFormatError, Error);
49
50
51
52/**
53 * Installs an extension to the given directory.
54 * @param {string} extension Path to the extension to install, as either a xpi
55 * 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 once
58 * installed.
59 */
60function install(extension, dir) {
61 return getDetails(extension).then(function(details) {
62 function returnId() { return details.id; }
63
64 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}
80
81
82/**
83 * Describes a Firefox add-on.
84 * @typedef {{id: string, name: string, version: string, unpack: boolean}}
85 */
86var AddonDetails;
87
88
89/**
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#');
99
100 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') || false
106 };
107
108 if (typeof details.unpack === 'string') {
109 details.unpack = details.unpack.toLowerCase() === 'true';
110 }
111
112 if (!details.id) {
113 throw new AddonFormatError('Could not find add-on ID for ' + addonPath);
114 }
115
116 return details;
117 });
118
119 function getNodeText(node, name) {
120 return node[name] && node[name][0] || '';
121 }
122
123 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 }
128
129 var namespaces = doc[keys[0]].$;
130 var id = '';
131 Object.keys(namespaces).some(function(ns) {
132 if (namespaces[ns] !== url) {
133 return false;
134 }
135
136 if (ns.indexOf(':') != -1) {
137 id = ns.split(':')[1] + ':';
138 }
139 return true;
140 });
141 return id;
142 }
143}
144
145
146/**
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;
153
154 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 }
174
175 return manifest.then(function(content) {
176 return checkedCall(xml.parseString, content);
177 });
178}
179
180
181// PUBLIC API
182
183
184exports.install = install;