|
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 file, |
|
3 * You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
4 |
|
5 "use strict"; |
|
6 |
|
7 let Cc = Components.classes; |
|
8 let Ci = Components.interfaces; |
|
9 let Cu = Components.utils; |
|
10 |
|
11 Cu.import("resource://gre/modules/Services.jsm"); |
|
12 Cu.import("resource://gre/modules/ObjectWrapper.jsm"); |
|
13 |
|
14 this.EXPORTED_SYMBOLS = ["SettingsDB", "SETTINGSDB_NAME", "SETTINGSSTORE_NAME"]; |
|
15 |
|
16 const DEBUG = false; |
|
17 function debug(s) { |
|
18 if (DEBUG) dump("-*- SettingsDB: " + s + "\n"); |
|
19 } |
|
20 |
|
21 this.SETTINGSDB_NAME = "settings"; |
|
22 this.SETTINGSDB_VERSION = 3; |
|
23 this.SETTINGSSTORE_NAME = "settings"; |
|
24 |
|
25 Cu.import("resource://gre/modules/IndexedDBHelper.jsm"); |
|
26 Cu.import("resource://gre/modules/FileUtils.jsm"); |
|
27 Cu.import("resource://gre/modules/NetUtil.jsm"); |
|
28 |
|
29 this.SettingsDB = function SettingsDB() {} |
|
30 |
|
31 SettingsDB.prototype = { |
|
32 |
|
33 __proto__: IndexedDBHelper.prototype, |
|
34 |
|
35 upgradeSchema: function upgradeSchema(aTransaction, aDb, aOldVersion, aNewVersion) { |
|
36 let objectStore; |
|
37 if (aOldVersion == 0) { |
|
38 objectStore = aDb.createObjectStore(SETTINGSSTORE_NAME, { keyPath: "settingName" }); |
|
39 if (DEBUG) debug("Created object stores"); |
|
40 } else if (aOldVersion == 1) { |
|
41 if (DEBUG) debug("Get object store for upgrade and remove old index"); |
|
42 objectStore = aTransaction.objectStore(SETTINGSSTORE_NAME); |
|
43 objectStore.deleteIndex("settingValue"); |
|
44 } else { |
|
45 if (DEBUG) debug("Get object store for upgrade"); |
|
46 objectStore = aTransaction.objectStore(SETTINGSSTORE_NAME); |
|
47 } |
|
48 |
|
49 // Loading resource://app/defaults/settings.json doesn't work because |
|
50 // settings.json is not in the omnijar. |
|
51 // So we look for the app dir instead and go from here... |
|
52 let settingsFile = FileUtils.getFile("DefRt", ["settings.json"], false); |
|
53 if (!settingsFile || (settingsFile && !settingsFile.exists())) { |
|
54 // On b2g desktop builds the settings.json file is moved in the |
|
55 // profile directory by the build system. |
|
56 settingsFile = FileUtils.getFile("ProfD", ["settings.json"], false); |
|
57 if (!settingsFile || (settingsFile && !settingsFile.exists())) { |
|
58 return; |
|
59 } |
|
60 } |
|
61 |
|
62 let chan = NetUtil.newChannel(settingsFile); |
|
63 let stream = chan.open(); |
|
64 // Obtain a converter to read from a UTF-8 encoded input stream. |
|
65 let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"] |
|
66 .createInstance(Ci.nsIScriptableUnicodeConverter); |
|
67 converter.charset = "UTF-8"; |
|
68 let rawstr = converter.ConvertToUnicode(NetUtil.readInputStreamToString( |
|
69 stream, |
|
70 stream.available()) || ""); |
|
71 let settings; |
|
72 try { |
|
73 settings = JSON.parse(rawstr); |
|
74 } catch(e) { |
|
75 if (DEBUG) debug("Error parsing " + settingsFile.path + " : " + e); |
|
76 return; |
|
77 } |
|
78 stream.close(); |
|
79 |
|
80 objectStore.openCursor().onsuccess = function(event) { |
|
81 let cursor = event.target.result; |
|
82 if (cursor) { |
|
83 let value = cursor.value; |
|
84 if (value.settingName in settings) { |
|
85 if (DEBUG) debug("Upgrade " +settings[value.settingName]); |
|
86 value.defaultValue = this.prepareValue(settings[value.settingName]); |
|
87 delete settings[value.settingName]; |
|
88 if ("settingValue" in value) { |
|
89 value.userValue = this.prepareValue(value.settingValue); |
|
90 delete value.settingValue; |
|
91 } |
|
92 cursor.update(value); |
|
93 } else if ("userValue" in value || "settingValue" in value) { |
|
94 value.defaultValue = undefined; |
|
95 if (aOldVersion == 1 && value.settingValue) { |
|
96 value.userValue = this.prepareValue(value.settingValue); |
|
97 delete value.settingValue; |
|
98 } |
|
99 cursor.update(value); |
|
100 } else { |
|
101 cursor.delete(); |
|
102 } |
|
103 cursor.continue(); |
|
104 } else { |
|
105 for (let name in settings) { |
|
106 let value = this.prepareValue(settings[name]); |
|
107 if (DEBUG) debug("Set new:" + name +", " + value); |
|
108 objectStore.add({ settingName: name, defaultValue: value, userValue: undefined }); |
|
109 } |
|
110 } |
|
111 }.bind(this); |
|
112 }, |
|
113 |
|
114 // If the value is a data: uri, convert it to a Blob. |
|
115 convertDataURIToBlob: function(aValue) { |
|
116 /* base64 to ArrayBuffer decoding, from |
|
117 https://developer.mozilla.org/en-US/docs/Web/JavaScript/Base64_encoding_and_decoding |
|
118 */ |
|
119 function b64ToUint6 (nChr) { |
|
120 return nChr > 64 && nChr < 91 ? |
|
121 nChr - 65 |
|
122 : nChr > 96 && nChr < 123 ? |
|
123 nChr - 71 |
|
124 : nChr > 47 && nChr < 58 ? |
|
125 nChr + 4 |
|
126 : nChr === 43 ? |
|
127 62 |
|
128 : nChr === 47 ? |
|
129 63 |
|
130 : |
|
131 0; |
|
132 } |
|
133 |
|
134 function base64DecToArr(sBase64, nBlocksSize) { |
|
135 let sB64Enc = sBase64.replace(/[^A-Za-z0-9\+\/]/g, ""), |
|
136 nInLen = sB64Enc.length, |
|
137 nOutLen = nBlocksSize ? Math.ceil((nInLen * 3 + 1 >> 2) / nBlocksSize) * nBlocksSize |
|
138 : nInLen * 3 + 1 >> 2, |
|
139 taBytes = new Uint8Array(nOutLen); |
|
140 |
|
141 for (let nMod3, nMod4, nUint24 = 0, nOutIdx = 0, nInIdx = 0; nInIdx < nInLen; nInIdx++) { |
|
142 nMod4 = nInIdx & 3; |
|
143 nUint24 |= b64ToUint6(sB64Enc.charCodeAt(nInIdx)) << 18 - 6 * nMod4; |
|
144 if (nMod4 === 3 || nInLen - nInIdx === 1) { |
|
145 for (nMod3 = 0; nMod3 < 3 && nOutIdx < nOutLen; nMod3++, nOutIdx++) { |
|
146 taBytes[nOutIdx] = nUint24 >>> (16 >>> nMod3 & 24) & 255; |
|
147 } |
|
148 nUint24 = 0; |
|
149 } |
|
150 } |
|
151 return taBytes; |
|
152 } |
|
153 |
|
154 // Check if we have a data: uri, and if it's base64 encoded. |
|
155 // data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEA... |
|
156 if (typeof aValue == "string" && aValue.startsWith("data:")) { |
|
157 try { |
|
158 let uri = Services.io.newURI(aValue, null, null); |
|
159 // XXX: that would be nice to reuse the c++ bits of the data: |
|
160 // protocol handler instead. |
|
161 let mimeType = "application/octet-stream"; |
|
162 let mimeDelim = aValue.indexOf(";"); |
|
163 if (mimeDelim !== -1) { |
|
164 mimeType = aValue.substring(5, mimeDelim); |
|
165 } |
|
166 let start = aValue.indexOf(",") + 1; |
|
167 let isBase64 = ((aValue.indexOf("base64") + 7) == start); |
|
168 let payload = aValue.substring(start); |
|
169 |
|
170 return new Blob([isBase64 ? base64DecToArr(payload) : payload], |
|
171 { type: mimeType }); |
|
172 } catch(e) { |
|
173 dump(e); |
|
174 } |
|
175 } |
|
176 return aValue |
|
177 }, |
|
178 |
|
179 // Makes sure any property that is a data: uri gets converted to a Blob. |
|
180 prepareValue: function(aObject) { |
|
181 let kind = ObjectWrapper.getObjectKind(aObject); |
|
182 if (kind == "array") { |
|
183 let res = []; |
|
184 aObject.forEach(function(aObj) { |
|
185 res.push(this.prepareValue(aObj)); |
|
186 }, this); |
|
187 return res; |
|
188 } else if (kind == "file" || kind == "blob" || kind == "date") { |
|
189 return aObject; |
|
190 } else if (kind == "primitive") { |
|
191 return this.convertDataURIToBlob(aObject); |
|
192 } |
|
193 |
|
194 // Fall-through, we now have a dictionary object. |
|
195 for (let prop in aObject) { |
|
196 aObject[prop] = this.prepareValue(aObject[prop]); |
|
197 } |
|
198 return aObject; |
|
199 }, |
|
200 |
|
201 init: function init() { |
|
202 this.initDBHelper(SETTINGSDB_NAME, SETTINGSDB_VERSION, |
|
203 [SETTINGSSTORE_NAME]); |
|
204 } |
|
205 } |