extension.js 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224
  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. /** @fileoverview Utilities for working with Firefox extensions. */
  18. 'use strict';
  19. const fs = require('fs'),
  20. path = require('path'),
  21. xml = require('xml2js');
  22. const io = require('../io');
  23. const zip = require('../io/zip');
  24. /**
  25. * Thrown when there an add-on is malformed.
  26. */
  27. class AddonFormatError extends Error {
  28. /** @param {string} msg The error message. */
  29. constructor(msg) {
  30. super(msg);
  31. /** @override */
  32. this.name = this.constructor.name;
  33. }
  34. }
  35. /**
  36. * Installs an extension to the given directory.
  37. * @param {string} extension Path to the extension to install, as either a xpi
  38. * file or a directory.
  39. * @param {string} dir Path to the directory to install the extension in.
  40. * @return {!Promise<string>} A promise for the add-on ID once
  41. * installed.
  42. */
  43. function install(extension, dir) {
  44. return getDetails(extension).then(function(details) {
  45. var dst = path.join(dir, details.id);
  46. if (extension.slice(-4) === '.xpi') {
  47. if (!details.unpack) {
  48. return io.copy(extension, dst + '.xpi').then(() => details.id);
  49. } else {
  50. return zip.unzip(extension, dst).then(() => details.id);
  51. }
  52. } else {
  53. return io.copyDir(extension, dst).then(() => details.id);
  54. }
  55. });
  56. }
  57. /**
  58. * Describes a Firefox add-on.
  59. * @typedef {{id: string, name: string, version: string, unpack: boolean}}
  60. */
  61. var AddonDetails;
  62. /** @typedef {{$: !Object<string, string>}} */
  63. var RdfRoot;
  64. /**
  65. * Extracts the details needed to install an add-on.
  66. * @param {string} addonPath Path to the extension directory.
  67. * @return {!Promise<!AddonDetails>} A promise for the add-on details.
  68. */
  69. function getDetails(addonPath) {
  70. return io.stat(addonPath).then((stats) => {
  71. if (stats.isDirectory()) {
  72. return parseDirectory(addonPath);
  73. } else if (addonPath.slice(-4) === '.xpi') {
  74. return parseXpiFile(addonPath);
  75. } else {
  76. throw Error('Add-on path is not an xpi or a directory: ' + addonPath);
  77. }
  78. });
  79. /**
  80. * Parse an install.rdf for a Firefox add-on.
  81. * @param {string} rdf The contents of install.rdf for the add-on.
  82. * @return {!Promise<!AddonDetails>} A promise for the add-on details.
  83. */
  84. function parseInstallRdf(rdf) {
  85. return parseXml(rdf).then(function(doc) {
  86. var em = getNamespaceId(doc, 'http://www.mozilla.org/2004/em-rdf#');
  87. var rdf = getNamespaceId(
  88. doc, 'http://www.w3.org/1999/02/22-rdf-syntax-ns#');
  89. var description = doc[rdf + 'RDF'][rdf + 'Description'][0];
  90. var details = {
  91. id: getNodeText(description, em + 'id'),
  92. name: getNodeText(description, em + 'name'),
  93. version: getNodeText(description, em + 'version'),
  94. unpack: getNodeText(description, em + 'unpack') || false
  95. };
  96. if (typeof details.unpack === 'string') {
  97. details.unpack = details.unpack.toLowerCase() === 'true';
  98. }
  99. if (!details.id) {
  100. throw new AddonFormatError('Could not find add-on ID for ' + addonPath);
  101. }
  102. return details;
  103. });
  104. function parseXml(text) {
  105. return new Promise((resolve, reject) => {
  106. xml.parseString(text, (err, data) => {
  107. if (err) {
  108. reject(err);
  109. } else {
  110. resolve(data);
  111. }
  112. });
  113. });
  114. }
  115. function getNodeText(node, name) {
  116. return node[name] && node[name][0] || '';
  117. }
  118. function getNamespaceId(doc, url) {
  119. var keys = Object.keys(doc);
  120. if (keys.length !== 1) {
  121. throw new AddonFormatError('Malformed manifest for add-on ' + addonPath);
  122. }
  123. var namespaces = /** @type {!RdfRoot} */(doc[keys[0]]).$;
  124. var id = '';
  125. Object.keys(namespaces).some(function(ns) {
  126. if (namespaces[ns] !== url) {
  127. return false;
  128. }
  129. if (ns.indexOf(':') != -1) {
  130. id = ns.split(':')[1] + ':';
  131. }
  132. return true;
  133. });
  134. return id;
  135. }
  136. }
  137. /**
  138. * Parse a manifest for a Firefox WebExtension.
  139. * @param {{
  140. * name: string,
  141. * version: string,
  142. * applications: {gecko:{id:string}}
  143. * }} json JSON representation of the manifest.
  144. * @return {!AddonDetails} The add-on details.
  145. */
  146. function parseManifestJson({name, version, applications}) {
  147. if (!(applications && applications.gecko && applications.gecko.id)) {
  148. throw new AddonFormatError('Could not find add-on ID for ' + addonPath);
  149. }
  150. return {id: applications.gecko.id, name, version, unpack: false};
  151. }
  152. function parseXpiFile(filePath) {
  153. return zip.load(filePath).then(archive => {
  154. if (archive.has('install.rdf')) {
  155. return archive.getFile('install.rdf')
  156. .then(buf => parseInstallRdf(buf.toString('utf8')));
  157. }
  158. if (archive.has('manifest.json')) {
  159. return archive.getFile('manifest.json')
  160. .then(buf => JSON.parse(buf.toString('utf8')))
  161. .then(parseManifestJson);
  162. }
  163. throw new AddonFormatError(
  164. `Couldn't find install.rdf or manifest.json in ${filePath}`);
  165. });
  166. }
  167. function parseDirectory(dirPath) {
  168. const rdfPath = path.join(dirPath, 'install.rdf');
  169. const jsonPath = path.join(dirPath, 'manifest.json');
  170. return io.exists(rdfPath)
  171. .then(rdfExists => {
  172. if (rdfExists) {
  173. return io.read(rdfPath)
  174. .then(buf => parseInstallRdf(buf.toString('utf8')));
  175. }
  176. return io.exists(jsonPath)
  177. .then(jsonExists => {
  178. if (jsonExists) {
  179. return io.read(jsonPath)
  180. .then(buf => JSON.parse(buf.toString('utf8')))
  181. .then(parseManifestJson);
  182. }
  183. throw new AddonFormatError(
  184. `Couldn't find install.rdf or manifest.json in ${dirPath}`);
  185. });
  186. })
  187. }
  188. }
  189. // PUBLIC API
  190. exports.install = install;