Bug 1643721: part 11) Privatize member of `RangeContextSerializer`. r=masayuki
[gecko.git] / dom / manifest / Manifest.jsm
blob39553e68b0236b55b28997c789d7bdcaacd5c8fb
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2  * License, v. 2.0. If a copy of the MPL was not distributed with this
3  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 /*
6  * Manifest.jsm is the top level api for managing installed web applications
7  * https://www.w3.org/TR/appmanifest/
8  *
9  * It is used to trigger the installation of a web application via .install()
10  * and to access the manifest data (including icons).
11  *
12  * TODO:
13  *  - Trigger appropriate app installed events
14  */
16 "use strict";
18 const { ManifestObtainer } = ChromeUtils.import(
19   "resource://gre/modules/ManifestObtainer.jsm"
21 const { ManifestIcons } = ChromeUtils.import(
22   "resource://gre/modules/ManifestIcons.jsm"
25 ChromeUtils.defineModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm");
26 ChromeUtils.defineModuleGetter(
27   this,
28   "JSONFile",
29   "resource://gre/modules/JSONFile.jsm"
32 /**
33  * Generates an hash for the given string.
34  *
35  * @note The generated hash is returned in base64 form.  Mind the fact base64
36  * is case-sensitive if you are going to reuse this code.
37  */
38 function generateHash(aString) {
39   const cryptoHash = Cc["@mozilla.org/security/hash;1"].createInstance(
40     Ci.nsICryptoHash
41   );
42   cryptoHash.init(Ci.nsICryptoHash.MD5);
43   const stringStream = Cc[
44     "@mozilla.org/io/string-input-stream;1"
45   ].createInstance(Ci.nsIStringInputStream);
46   stringStream.data = aString;
47   cryptoHash.updateFromStream(stringStream, -1);
48   // base64 allows the '/' char, but we can't use it for filenames.
49   return cryptoHash.finish(true).replace(/\//g, "-");
52 /**
53  * Trims the query parameters from a url
54  */
55 function stripQuery(url) {
56   return url.split("?")[0];
59 // Folder in which we store the manifest files
60 const MANIFESTS_DIR = OS.Path.join(OS.Constants.Path.profileDir, "manifests");
62 // We maintain a list of scopes for installed webmanifests so we can determine
63 // whether a given url is within the scope of a previously installed manifest
64 const MANIFESTS_FILE = "manifest-scopes.json";
66 /**
67  * Manifest object
68  */
70 class Manifest {
71   constructor(browser, manifestUrl) {
72     this._manifestUrl = manifestUrl;
73     // The key for this is the manifests URL that is required to be unique.
74     // However arbitrary urls are not safe file paths so lets hash it.
75     const fileName = generateHash(manifestUrl) + ".json";
76     this._path = OS.Path.join(MANIFESTS_DIR, fileName);
77     this.browser = browser;
78   }
80   get browser() {
81     return this._browser;
82   }
84   set browser(aBrowser) {
85     this._browser = aBrowser;
86   }
88   async initialize() {
89     this._store = new JSONFile({ path: this._path, saveDelayMs: 100 });
90     await this._store.load();
91   }
93   async prefetch(browser) {
94     const manifestData = await ManifestObtainer.browserObtainManifest(browser);
95     const icon = await ManifestIcons.browserFetchIcon(
96       browser,
97       manifestData,
98       192
99     );
100     const data = {
101       installed: false,
102       manifest: manifestData,
103       cached_icon: icon,
104     };
105     return data;
106   }
108   async install() {
109     const manifestData = await ManifestObtainer.browserObtainManifest(
110       this._browser
111     );
112     this._store.data = {
113       installed: true,
114       manifest: manifestData,
115     };
116     Manifests.manifestInstalled(this);
117     this._store.saveSoon();
118   }
120   async icon(expectedSize) {
121     if ("cached_icon" in this._store.data) {
122       return this._store.data.cached_icon;
123     }
124     const icon = await ManifestIcons.browserFetchIcon(
125       this._browser,
126       this._store.data.manifest,
127       expectedSize
128     );
129     // Cache the icon so future requests do not go over the network
130     this._store.data.cached_icon = icon;
131     this._store.saveSoon();
132     return icon;
133   }
135   get scope() {
136     const scope =
137       this._store.data.manifest.scope || this._store.data.manifest.start_url;
138     return stripQuery(scope);
139   }
141   get name() {
142     return (
143       this._store.data.manifest.short_name ||
144       this._store.data.manifest.name ||
145       this._store.data.manifest.short_url
146     );
147   }
149   get url() {
150     return this._manifestUrl;
151   }
153   get installed() {
154     return (this._store.data && this._store.data.installed) || false;
155   }
157   get start_url() {
158     return this._store.data.manifest.start_url;
159   }
161   get path() {
162     return this._path;
163   }
167  * Manifests maintains the list of installed manifests
168  */
169 var Manifests = {
170   async _initialize() {
171     if (this._readyPromise) {
172       return this._readyPromise;
173     }
175     // Prevent multiple initializations
176     this._readyPromise = (async () => {
177       // Make sure the manifests have the folder needed to save into
178       await OS.File.makeDir(MANIFESTS_DIR, { ignoreExisting: true });
180       // Ensure any existing scope data we have about manifests is loaded
181       this._path = OS.Path.join(OS.Constants.Path.profileDir, MANIFESTS_FILE);
182       this._store = new JSONFile({ path: this._path });
183       await this._store.load();
185       // If we don't have any existing data, initialize empty
186       if (!this._store.data.hasOwnProperty("scopes")) {
187         this._store.data.scopes = new Map();
188       }
189     })();
191     // Cache the Manifest objects creates as they are references to files
192     // and we do not want multiple file handles
193     this.manifestObjs = new Map();
194     return this._readyPromise;
195   },
197   // When a manifest is installed, we save its scope so we can determine if
198   // future visits fall within this manifests scope
199   manifestInstalled(manifest) {
200     this._store.data.scopes[manifest.scope] = manifest.url;
201     this._store.saveSoon();
202   },
204   // Given a url, find if it is within an installed manifests scope and if so
205   // return that manifests url
206   findManifestUrl(url) {
207     for (let scope in this._store.data.scopes) {
208       if (url.startsWith(scope)) {
209         return this._store.data.scopes[scope];
210       }
211     }
212     return null;
213   },
215   // Get the manifest given a url, or if not look for a manifest that is
216   // tied to the current page
217   async getManifest(browser, manifestUrl) {
218     // Ensure we have all started up
219     if (!this._readyPromise) {
220       await this._initialize();
221     }
223     // If the client does not already know its manifestUrl, we take the
224     // url of the client and see if it matches the scope of any installed
225     // manifests
226     if (!manifestUrl) {
227       const url = stripQuery(browser.currentURI.spec);
228       manifestUrl = this.findManifestUrl(url);
229     }
231     // No matches so no manifest
232     if (manifestUrl === null) {
233       return null;
234     }
236     // If we have already created this manifest return cached
237     if (this.manifestObjs.has(manifestUrl)) {
238       const manifest = this.manifestObjs.get(manifestUrl);
239       if (manifest.browser !== browser) {
240         manifest.browser = browser;
241       }
242       return manifest;
243     }
245     // Otherwise create a new manifest object
246     const manifest = new Manifest(browser, manifestUrl);
247     this.manifestObjs.set(manifestUrl, manifest);
248     await manifest.initialize();
249     return manifest;
250   },
253 var EXPORTED_SYMBOLS = ["Manifests"]; // jshint ignore:line