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; |