# 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 |
20 | var AdmZip = require('adm-zip'),
21 | fs = require('fs'),
22 | path = require('path'),
23 | util = require('util'),
24 | xml = require('xml2js');
25 |
26 | var 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 | */
37 | function 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 | }
48 | util.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 | */
60 | function 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 | */
86 | var 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 | */
94 | function 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 | */
151 | function 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 |
184 | exports.install = install;