# 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 |
22 | var AdmZip = require('adm-zip'),
23 | fs = require('fs'),
24 | path = require('path'),
25 | util = require('util'),
26 | xml = require('xml2js');
27 |
28 | var 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 | */
39 | function 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 | }
50 | util.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 | */
62 | function 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 | */
87 | var 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 | */
95 | function 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 | */
152 | function 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 |
185 | exports.install = install;