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 var zip = new AdmZip(buff);
73 // TODO: find an async library for inflating a zip archive.
74 new AdmZip(buff).extractAllTo(dst, true);
75 }).then(returnId);
76 }
77 } else {
78 return io.copyDir(extension, dst).then(returnId);
79 }
80 });
81}
82
83
84/**
85 * Describes a Firefox add-on.
86 * @typedef {{id: string, name: string, version: string, unpack: boolean}}
87 */
88var AddonDetails;
89
90
91/**
92 * Extracts the details needed to install an add-on.
93 * @param {string} addonPath Path to the extension directory.
94 * @return {!promise.Promise.<!AddonDetails>} A promise for the add-on details.
95 */
96function getDetails(addonPath) {
97 return readManifest(addonPath).then(function(doc) {
98 var em = getNamespaceId(doc, 'http://www.mozilla.org/2004/em-rdf#');
99 var rdf = getNamespaceId(
100 doc, 'http://www.w3.org/1999/02/22-rdf-syntax-ns#');
101
102 var description = doc[rdf + 'RDF'][rdf + 'Description'][0];
103 var details = {
104 id: getNodeText(description, em + 'id'),
105 name: getNodeText(description, em + 'name'),
106 version: getNodeText(description, em + 'version'),
107 unpack: getNodeText(description, em + 'unpack') || false
108 };
109
110 if (typeof details.unpack === 'string') {
111 details.unpack = details.unpack.toLowerCase() === 'true';
112 }
113
114 if (!details.id) {
115 throw new AddonFormatError('Could not find add-on ID for ' + addonPath);
116 }
117
118 return details;
119 });
120
121 function getNodeText(node, name) {
122 return node[name] && node[name][0] || '';
123 }
124
125 function getNamespaceId(doc, url) {
126 var keys = Object.keys(doc);
127 if (keys.length !== 1) {
128 throw new AddonFormatError('Malformed manifest for add-on ' + addonPath);
129 }
130
131 var namespaces = doc[keys[0]].$;
132 var id = '';
133 Object.keys(namespaces).some(function(ns) {
134 if (namespaces[ns] !== url) {
135 return false;
136 }
137
138 if (ns.indexOf(':') != -1) {
139 id = ns.split(':')[1] + ':';
140 }
141 return true;
142 });
143 return id;
144 }
145}
146
147
148/**
149 * Reads the manifest for a Firefox add-on.
150 * @param {string} addonPath Path to a Firefox add-on as a xpi or an extension.
151 * @return {!promise.Promise.<!Object>} A promise for the parsed manifest.
152 */
153function readManifest(addonPath) {
154 var manifest;
155
156 if (addonPath.slice(-4) === '.xpi') {
157 manifest = checkedCall(fs.readFile, addonPath).then(function(buff) {
158 var zip = new AdmZip(buff);
159 if (!zip.getEntry('install.rdf')) {
160 throw new AddonFormatError(
161 'Could not find install.rdf in ' + addonPath);
162 }
163 var done = promise.defer();
164 zip.readAsTextAsync('install.rdf', done.fulfill);
165 return done.promise;
166 });
167 } else {
168 manifest = checkedCall(fs.stat, addonPath).then(function(stats) {
169 if (!stats.isDirectory()) {
170 throw Error(
171 'Add-on path is niether a xpi nor a directory: ' + addonPath);
172 }
173 return checkedCall(fs.readFile, path.join(addonPath, 'install.rdf'));
174 });
175 }
176
177 return manifest.then(function(content) {
178 return checkedCall(xml.parseString, content);
179 });
180}
181
182
183// PUBLIC API
184
185
186exports.install = install;