Manifest.sys.mjs (8157B)
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/. */ 4 5 /* 6 * Manifest.sys.mjs 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 */ 15 16 import { ManifestObtainer } from "resource://gre/modules/ManifestObtainer.sys.mjs"; 17 18 import { ManifestIcons } from "resource://gre/modules/ManifestIcons.sys.mjs"; 19 20 const lazy = {}; 21 22 ChromeUtils.defineESModuleGetters(lazy, { 23 JSONFile: "resource://gre/modules/JSONFile.sys.mjs", 24 }); 25 26 /** 27 * Generates an hash for the given string. 28 * 29 * Note: The generated hash is returned in base64 form. Mind the fact base64 30 * is case-sensitive if you are going to reuse this code. 31 */ 32 function generateHash(aString, hashAlg) { 33 const cryptoHash = Cc["@mozilla.org/security/hash;1"].createInstance( 34 Ci.nsICryptoHash 35 ); 36 cryptoHash.init(hashAlg); 37 const stringStream = Cc[ 38 "@mozilla.org/io/string-input-stream;1" 39 ].createInstance(Ci.nsIStringInputStream); 40 stringStream.setByteStringData(aString); 41 cryptoHash.updateFromStream(stringStream, -1); 42 // base64 allows the '/' char, but we can't use it for filenames. 43 return cryptoHash.finish(true).replace(/\//g, "-"); 44 } 45 46 /** 47 * Trims the query parameters from a uri. 48 * 49 * @param {nsIURI} uri 50 * 51 * @returns {string} The url as a string, without any query or hash/ref bits. 52 */ 53 function stripQuery(uri) { 54 return uri.mutate().setQuery("").setRef("").finalize().spec; 55 } 56 57 // Folder in which we store the manifest files 58 const MANIFESTS_DIR = PathUtils.join(PathUtils.profileDir, "manifests"); 59 60 // We maintain a list of scopes for installed webmanifests so we can determine 61 // whether a given url is within the scope of a previously installed manifest 62 const MANIFESTS_FILE = "manifest-scopes.json"; 63 64 /** 65 * Manifest object 66 */ 67 68 class Manifest { 69 constructor(browser, manifestUrl) { 70 this._manifestUrl = manifestUrl; 71 // The key for this is the manifests URL that is required to be unique. 72 // However arbitrary urls are not safe file paths so lets hash it. 73 const filename = 74 generateHash(manifestUrl, Ci.nsICryptoHash.SHA256) + ".json"; 75 this._path = PathUtils.join(MANIFESTS_DIR, filename); 76 this.browser = browser; 77 } 78 79 /** 80 * See Bug 1871109 81 * This function is called at the beginning of initialize() to check if a given 82 * manifest has MD5 based filename, if so we remove it and migrate the content to 83 * a new file with SHA256 based name. 84 * This is done due to security concern, as MD5 is an outdated hashing algorithm and 85 * shouldn't be used anymore 86 */ 87 async removeMD5BasedFilename() { 88 const filenameMD5 = 89 generateHash(this._manifestUrl, Ci.nsICryptoHash.MD5) + ".json"; 90 const MD5Path = PathUtils.join(MANIFESTS_DIR, filenameMD5); 91 try { 92 await IOUtils.copy(MD5Path, this._path, { noOverwrite: true }); 93 } catch (error) { 94 // we are ignoring the failures returned from copy as it should not stop us from 95 // installing a new manifest 96 } 97 98 // Remove the old MD5 based file unconditionally to ensure it's no longer used 99 try { 100 await IOUtils.remove(MD5Path); 101 } catch { 102 // ignore the error in case MD5 based file does not exist 103 } 104 } 105 106 get browser() { 107 return this._browser; 108 } 109 110 set browser(aBrowser) { 111 this._browser = aBrowser; 112 } 113 114 async initialize() { 115 await this.removeMD5BasedFilename(); 116 this._store = new lazy.JSONFile({ path: this._path, saveDelayMs: 100 }); 117 await this._store.load(); 118 } 119 120 async prefetch(browser) { 121 const manifestData = await ManifestObtainer.browserObtainManifest(browser); 122 const icon = await ManifestIcons.browserFetchIcon( 123 browser, 124 manifestData, 125 192 126 ); 127 const data = { 128 installed: false, 129 manifest: manifestData, 130 cached_icon: icon, 131 }; 132 return data; 133 } 134 135 async install() { 136 const manifestData = await ManifestObtainer.browserObtainManifest( 137 this._browser 138 ); 139 this._store.data = { 140 installed: true, 141 manifest: manifestData, 142 }; 143 Manifests.manifestInstalled(this); 144 this._store.saveSoon(); 145 } 146 147 async icon(expectedSize) { 148 if ("cached_icon" in this._store.data) { 149 return this._store.data.cached_icon; 150 } 151 const icon = await ManifestIcons.browserFetchIcon( 152 this._browser, 153 this._store.data.manifest, 154 expectedSize 155 ); 156 // Cache the icon so future requests do not go over the network 157 this._store.data.cached_icon = icon; 158 this._store.saveSoon(); 159 return icon; 160 } 161 162 get scope() { 163 const scope = 164 this._store.data.manifest.scope || this._store.data.manifest.start_url; 165 return stripQuery(Services.io.newURI(scope)); 166 } 167 168 get name() { 169 return ( 170 this._store.data.manifest.short_name || 171 this._store.data.manifest.name || 172 this._store.data.manifest.short_url 173 ); 174 } 175 176 get url() { 177 return this._manifestUrl; 178 } 179 180 get installed() { 181 return (this._store.data && this._store.data.installed) || false; 182 } 183 184 get start_url() { 185 return this._store.data.manifest.start_url; 186 } 187 188 get path() { 189 return this._path; 190 } 191 } 192 193 /* 194 * Manifests maintains the list of installed manifests 195 */ 196 export var Manifests = { 197 async _initialize() { 198 if (this._readyPromise) { 199 return this._readyPromise; 200 } 201 202 // Prevent multiple initializations 203 this._readyPromise = (async () => { 204 // Make sure the manifests have the folder needed to save into 205 await IOUtils.makeDirectory(MANIFESTS_DIR, { ignoreExisting: true }); 206 207 // Ensure any existing scope data we have about manifests is loaded 208 this._path = PathUtils.join(PathUtils.profileDir, MANIFESTS_FILE); 209 this._store = new lazy.JSONFile({ path: this._path }); 210 await this._store.load(); 211 212 // If we don't have any existing data, initialize empty 213 if (!this._store.data.hasOwnProperty("scopes")) { 214 this._store.data.scopes = new Map(); 215 } 216 })(); 217 218 // Cache the Manifest objects creates as they are references to files 219 // and we do not want multiple file handles 220 this.manifestObjs = new Map(); 221 return this._readyPromise; 222 }, 223 224 // When a manifest is installed, we save its scope so we can determine if 225 // future visits fall within this manifests scope 226 manifestInstalled(manifest) { 227 this._store.data.scopes[manifest.scope] = manifest.url; 228 this._store.saveSoon(); 229 }, 230 231 // Given a url, find if it is within an installed manifests scope and if so 232 // return that manifests url 233 findManifestUrl(url) { 234 for (let scope in this._store.data.scopes) { 235 if (url.startsWith(scope)) { 236 return this._store.data.scopes[scope]; 237 } 238 } 239 return null; 240 }, 241 242 // Get the manifest given a url, or if not look for a manifest that is 243 // tied to the current page 244 async getManifest(browser, manifestUrl) { 245 // Ensure we have all started up 246 if (!this._readyPromise) { 247 await this._initialize(); 248 } 249 250 // If the client does not already know its manifestUrl, we take the 251 // url of the client and see if it matches the scope of any installed 252 // manifests 253 if (!manifestUrl) { 254 const url = stripQuery(browser.currentURI); 255 manifestUrl = this.findManifestUrl(url); 256 } 257 258 // No matches so no manifest 259 if (manifestUrl === null) { 260 return null; 261 } 262 263 // If we have already created this manifest return cached 264 if (this.manifestObjs.has(manifestUrl)) { 265 const manifest = this.manifestObjs.get(manifestUrl); 266 if (manifest.browser !== browser) { 267 manifest.browser = browser; 268 } 269 return manifest; 270 } 271 272 // Otherwise create a new manifest object 273 const manifest = new Manifest(browser, manifestUrl); 274 this.manifestObjs.set(manifestUrl, manifest); 275 await manifest.initialize(); 276 return manifest; 277 }, 278 };