firefox/extension.js

1// Licensed to the Software Freedom Conservancy (SFC) under one
2// or more contributor license agreements. See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership. The SFC licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License. You may obtain a copy of the License at
8//
9// http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied. See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18/** @fileoverview Utilities for working with Firefox extensions. */
19
20'use strict';
21
22var AdmZip = require('adm-zip'),
23 fs = require('fs'),
24 path = require('path'),
25 util = require('util'),
26 xml = require('xml2js');
27
28var promise = require('..').promise,
29 checkedCall = promise.checkedNodeCall,
30 io = require('../io');
31
32
33/**
34 * Thrown when there an add-on is malformed.
35 * @param {string} msg The error message.
36 * @constructor
37 * @extends {Error}
38 */
39function AddonFormatError(msg) {
40 Error.call(this);
41
42 Error.captureStackTrace(this, AddonFormatError);
43
44 /** @override */
45 this.name = AddonFormatError.name;
46
47 /** @override */
48 this.message = msg;
49}
50util.inherits(AddonFormatError, Error);
51
52
53
54/**
55 * Installs an extension to the given directory.
56 * @param {string} extension Path to the extension to install, as either a xpi
57 * file or a directory.
58 * @param {string} dir Path to the directory to install the extension in.
59 * @return {!promise.Promise.<string>} A promise for the add-on ID once
60 * installed.
61 */
62function install(extension, dir) {
63 return getDetails(extension).then(function(details) {
64 function returnId() { return details.id; }
65
66 var dst = path.join(dir, details.id);
67 if (extension.slice(-4) === '.xpi') {
68 if (!details.unpack) {
69 return io.copy(extension, dst + '.xpi').then(returnId);
70 } else {
71 return checkedCall(fs.readFile, extension).then(function(buff) {
72 // TODO: find an async library for inflating a zip archive.
73 new AdmZip(buff).extractAllTo(dst, true);
74 }).then(returnId);
75 }
76 } else {
77 return io.copyDir(extension, dst).then(returnId);
78 }
79 });
80}
81
82
83/**
84 * Describes a Firefox add-on.
85 * @typedef {{id: string, name: string, version: string, unpack: boolean}}
86 */
87var AddonDetails;
88
89
90/**
91 * Extracts the details needed to install an add-on.
92 * @param {string} addonPath Path to the extension directory.
93 * @return {!promise.Promise.<!AddonDetails>} A promise for the add-on details.
94 */
95function getDetails(addonPath) {
96 return readManifest(addonPath).then(function(doc) {
97 var em = getNamespaceId(doc, 'http://www.mozilla.org/2004/em-rdf#');
98 var rdf = getNamespaceId(
99 doc, 'http://www.w3.org/1999/02/22-rdf-syntax-ns#');
100
101 var description = doc[rdf + 'RDF'][rdf + 'Description'][0];
102 var details = {
103 id: getNodeText(description, em + 'id'),
104 name: getNodeText(description, em + 'name'),
105 version: getNodeText(description, em + 'version'),
106 unpack: getNodeText(description, em + 'unpack') || false
107 };
108
109 if (typeof details.unpack === 'string') {
110 details.unpack = details.unpack.toLowerCase() === 'true';
111 }
112
113 if (!details.id) {
114 throw new AddonFormatError('Could not find add-on ID for ' + addonPath);
115 }
116
117 return details;
118 });
119
120 function getNodeText(node, name) {
121 return node[name] && node[name][0] || '';
122 }
123
124 function getNamespaceId(doc, url) {
125 var keys = Object.keys(doc);
126 if (keys.length !== 1) {
127 throw new AddonFormatError('Malformed manifest for add-on ' + addonPath);
128 }
129
130 var namespaces = doc[keys[0]].$;
131 var id = '';
132 Object.keys(namespaces).some(function(ns) {
133 if (namespaces[ns] !== url) {
134 return false;
135 }
136
137 if (ns.indexOf(':') != -1) {
138 id = ns.split(':')[1] + ':';
139 }
140 return true;
141 });
142 return id;
143 }
144}
145
146
147/**
148 * Reads the manifest for a Firefox add-on.
149 * @param {string} addonPath Path to a Firefox add-on as a xpi or an extension.
150 * @return {!promise.Promise.<!Object>} A promise for the parsed manifest.
151 */
152function readManifest(addonPath) {
153 var manifest;
154
155 if (addonPath.slice(-4) === '.xpi') {
156 manifest = checkedCall(fs.readFile, addonPath).then(function(buff) {
157 var zip = new AdmZip(buff);
158 if (!zip.getEntry('install.rdf')) {
159 throw new AddonFormatError(
160 'Could not find install.rdf in ' + addonPath);
161 }
162 var done = promise.defer();
163 zip.readAsTextAsync('install.rdf', done.fulfill);
164 return done.promise;
165 });
166 } else {
167 manifest = checkedCall(fs.stat, addonPath).then(function(stats) {
168 if (!stats.isDirectory()) {
169 throw Error(
170 'Add-on path is niether a xpi nor a directory: ' + addonPath);
171 }
172 return checkedCall(fs.readFile, path.join(addonPath, 'install.rdf'));
173 });
174 }
175
176 return manifest.then(function(content) {
177 return checkedCall(xml.parseString, content);
178 });
179}
180
181
182// PUBLIC API
183
184
185exports.install = install;