|
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 * validateManifest() warns of the following errors: |
|
7 * - No manifest specified in page |
|
8 * - Manifest is not utf-8 |
|
9 * - Manifest mimetype not text/cache-manifest |
|
10 * - Manifest does not begin with "CACHE MANIFEST" |
|
11 * - Page modified since appcache last changed |
|
12 * - Duplicate entries |
|
13 * - Conflicting entries e.g. in both CACHE and NETWORK sections or in cache |
|
14 * but blocked by FALLBACK namespace |
|
15 * - Detect referenced files that are not available |
|
16 * - Detect referenced files that have cache-control set to no-store |
|
17 * - Wildcards used in a section other than NETWORK |
|
18 * - Spaces in URI not replaced with %20 |
|
19 * - Completely invalid URIs |
|
20 * - Too many dot dot slash operators |
|
21 * - SETTINGS section is valid |
|
22 * - Invalid section name |
|
23 * - etc. |
|
24 */ |
|
25 |
|
26 "use strict"; |
|
27 |
|
28 const { classes: Cc, interfaces: Ci, utils: Cu } = Components; |
|
29 |
|
30 let { XPCOMUtils } = Cu.import("resource://gre/modules/XPCOMUtils.jsm", {}); |
|
31 let { Services } = Cu.import("resource://gre/modules/Services.jsm", {}); |
|
32 let { Promise: promise } = Cu.import("resource://gre/modules/Promise.jsm", {}); |
|
33 |
|
34 this.EXPORTED_SYMBOLS = ["AppCacheUtils"]; |
|
35 |
|
36 function AppCacheUtils(documentOrUri) { |
|
37 this._parseManifest = this._parseManifest.bind(this); |
|
38 |
|
39 if (documentOrUri) { |
|
40 if (typeof documentOrUri == "string") { |
|
41 this.uri = documentOrUri; |
|
42 } |
|
43 if (/HTMLDocument/.test(documentOrUri.toString())) { |
|
44 this.doc = documentOrUri; |
|
45 } |
|
46 } |
|
47 } |
|
48 |
|
49 AppCacheUtils.prototype = { |
|
50 get cachePath() { |
|
51 return ""; |
|
52 }, |
|
53 |
|
54 validateManifest: function ACU_validateManifest() { |
|
55 let deferred = promise.defer(); |
|
56 this.errors = []; |
|
57 // Check for missing manifest. |
|
58 this._getManifestURI().then(manifestURI => { |
|
59 this.manifestURI = manifestURI; |
|
60 |
|
61 if (!this.manifestURI) { |
|
62 this._addError(0, "noManifest"); |
|
63 deferred.resolve(this.errors); |
|
64 } |
|
65 |
|
66 this._getURIInfo(this.manifestURI).then(uriInfo => { |
|
67 this._parseManifest(uriInfo).then(() => { |
|
68 // Sort errors by line number. |
|
69 this.errors.sort(function(a, b) { |
|
70 return a.line - b.line; |
|
71 }); |
|
72 deferred.resolve(this.errors); |
|
73 }); |
|
74 }); |
|
75 }); |
|
76 |
|
77 return deferred.promise; |
|
78 }, |
|
79 |
|
80 _parseManifest: function ACU__parseManifest(uriInfo) { |
|
81 let deferred = promise.defer(); |
|
82 let manifestName = uriInfo.name; |
|
83 let manifestLastModified = new Date(uriInfo.responseHeaders["Last-Modified"]); |
|
84 |
|
85 if (uriInfo.charset.toLowerCase() != "utf-8") { |
|
86 this._addError(0, "notUTF8", uriInfo.charset); |
|
87 } |
|
88 |
|
89 if (uriInfo.mimeType != "text/cache-manifest") { |
|
90 this._addError(0, "badMimeType", uriInfo.mimeType); |
|
91 } |
|
92 |
|
93 let parser = new ManifestParser(uriInfo.text, this.manifestURI); |
|
94 let parsed = parser.parse(); |
|
95 |
|
96 if (parsed.errors.length > 0) { |
|
97 this.errors.push.apply(this.errors, parsed.errors); |
|
98 } |
|
99 |
|
100 // Check for duplicate entries. |
|
101 let dupes = {}; |
|
102 for (let parsedUri of parsed.uris) { |
|
103 dupes[parsedUri.uri] = dupes[parsedUri.uri] || []; |
|
104 dupes[parsedUri.uri].push({ |
|
105 line: parsedUri.line, |
|
106 section: parsedUri.section, |
|
107 original: parsedUri.original |
|
108 }); |
|
109 } |
|
110 for (let [uri, value] of Iterator(dupes)) { |
|
111 if (value.length > 1) { |
|
112 this._addError(0, "duplicateURI", uri, JSON.stringify(value)); |
|
113 } |
|
114 } |
|
115 |
|
116 // Loop through network entries making sure that fallback and cache don't |
|
117 // contain uris starting with the network uri. |
|
118 for (let neturi of parsed.uris) { |
|
119 if (neturi.section == "NETWORK") { |
|
120 for (let parsedUri of parsed.uris) { |
|
121 if (parsedUri.uri.startsWith(neturi.uri)) { |
|
122 this._addError(neturi.line, "networkBlocksURI", neturi.line, |
|
123 neturi.original, parsedUri.line, parsedUri.original, |
|
124 parsedUri.section); |
|
125 } |
|
126 } |
|
127 } |
|
128 } |
|
129 |
|
130 // Loop through fallback entries making sure that fallback and cache don't |
|
131 // contain uris starting with the network uri. |
|
132 for (let fb of parsed.fallbacks) { |
|
133 for (let parsedUri of parsed.uris) { |
|
134 if (parsedUri.uri.startsWith(fb.namespace)) { |
|
135 this._addError(fb.line, "fallbackBlocksURI", fb.line, |
|
136 fb.original, parsedUri.line, parsedUri.original, |
|
137 parsedUri.section); |
|
138 } |
|
139 } |
|
140 } |
|
141 |
|
142 // Check that all resources exist and that their cach-control headers are |
|
143 // not set to no-store. |
|
144 let current = -1; |
|
145 for (let i = 0, len = parsed.uris.length; i < len; i++) { |
|
146 let parsedUri = parsed.uris[i]; |
|
147 this._getURIInfo(parsedUri.uri).then(uriInfo => { |
|
148 current++; |
|
149 |
|
150 if (uriInfo.success) { |
|
151 // Check that the resource was not modified after the manifest was last |
|
152 // modified. If it was then the manifest file should be refreshed. |
|
153 let resourceLastModified = |
|
154 new Date(uriInfo.responseHeaders["Last-Modified"]); |
|
155 |
|
156 if (manifestLastModified < resourceLastModified) { |
|
157 this._addError(parsedUri.line, "fileChangedButNotManifest", |
|
158 uriInfo.name, manifestName, parsedUri.line); |
|
159 } |
|
160 |
|
161 // If cache-control: no-store the file will not be added to the |
|
162 // appCache. |
|
163 if (uriInfo.nocache) { |
|
164 this._addError(parsedUri.line, "cacheControlNoStore", |
|
165 parsedUri.original, parsedUri.line); |
|
166 } |
|
167 } else { |
|
168 this._addError(parsedUri.line, "notAvailable", |
|
169 parsedUri.original, parsedUri.line); |
|
170 } |
|
171 |
|
172 if (current == len - 1) { |
|
173 deferred.resolve(); |
|
174 } |
|
175 }); |
|
176 } |
|
177 |
|
178 return deferred.promise; |
|
179 }, |
|
180 |
|
181 _getURIInfo: function ACU__getURIInfo(uri) { |
|
182 let inputStream = Cc["@mozilla.org/scriptableinputstream;1"] |
|
183 .createInstance(Ci.nsIScriptableInputStream); |
|
184 let deferred = promise.defer(); |
|
185 let channelCharset = ""; |
|
186 let buffer = ""; |
|
187 let channel = Services.io.newChannel(uri, null, null); |
|
188 |
|
189 // Avoid the cache: |
|
190 channel.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE; |
|
191 channel.loadFlags |= Ci.nsIRequest.INHIBIT_CACHING; |
|
192 |
|
193 channel.asyncOpen({ |
|
194 onStartRequest: function (request, context) { |
|
195 // This empty method is needed in order for onDataAvailable to be |
|
196 // called. |
|
197 }, |
|
198 |
|
199 onDataAvailable: function (request, context, stream, offset, count) { |
|
200 request.QueryInterface(Ci.nsIHttpChannel); |
|
201 inputStream.init(stream); |
|
202 buffer = buffer.concat(inputStream.read(count)); |
|
203 }, |
|
204 |
|
205 onStopRequest: function onStartRequest(request, context, statusCode) { |
|
206 if (statusCode == 0) { |
|
207 request.QueryInterface(Ci.nsIHttpChannel); |
|
208 |
|
209 let result = { |
|
210 name: request.name, |
|
211 success: request.requestSucceeded, |
|
212 status: request.responseStatus + " - " + request.responseStatusText, |
|
213 charset: request.contentCharset || "utf-8", |
|
214 mimeType: request.contentType, |
|
215 contentLength: request.contentLength, |
|
216 nocache: request.isNoCacheResponse() || request.isNoStoreResponse(), |
|
217 prePath: request.URI.prePath + "/", |
|
218 text: buffer |
|
219 }; |
|
220 |
|
221 result.requestHeaders = {}; |
|
222 request.visitRequestHeaders(function(header, value) { |
|
223 result.requestHeaders[header] = value; |
|
224 }); |
|
225 |
|
226 result.responseHeaders = {}; |
|
227 request.visitResponseHeaders(function(header, value) { |
|
228 result.responseHeaders[header] = value; |
|
229 }); |
|
230 |
|
231 deferred.resolve(result); |
|
232 } else { |
|
233 deferred.resolve({ |
|
234 name: request.name, |
|
235 success: false |
|
236 }); |
|
237 } |
|
238 } |
|
239 }, null); |
|
240 return deferred.promise; |
|
241 }, |
|
242 |
|
243 listEntries: function ACU_show(searchTerm) { |
|
244 if (!Services.prefs.getBoolPref("browser.cache.disk.enable")) { |
|
245 throw new Error(l10n.GetStringFromName("cacheDisabled")); |
|
246 } |
|
247 |
|
248 let entries = []; |
|
249 |
|
250 Services.cache.visitEntries({ |
|
251 visitDevice: function(deviceID, deviceInfo) { |
|
252 return true; |
|
253 }, |
|
254 |
|
255 visitEntry: function(deviceID, entryInfo) { |
|
256 if (entryInfo.deviceID == "offline") { |
|
257 let entry = {}; |
|
258 let lowerKey = entryInfo.key.toLowerCase(); |
|
259 |
|
260 if (searchTerm && lowerKey.indexOf(searchTerm.toLowerCase()) == -1) { |
|
261 return true; |
|
262 } |
|
263 |
|
264 for (let [key, value] of Iterator(entryInfo)) { |
|
265 if (key == "QueryInterface") { |
|
266 continue; |
|
267 } |
|
268 if (key == "clientID") { |
|
269 entry.key = entryInfo.key; |
|
270 } |
|
271 if (key == "expirationTime" || key == "lastFetched" || key == "lastModified") { |
|
272 value = new Date(value * 1000); |
|
273 } |
|
274 entry[key] = value; |
|
275 } |
|
276 entries.push(entry); |
|
277 } |
|
278 return true; |
|
279 } |
|
280 }); |
|
281 |
|
282 if (entries.length == 0) { |
|
283 throw new Error(l10n.GetStringFromName("noResults")); |
|
284 } |
|
285 return entries; |
|
286 }, |
|
287 |
|
288 viewEntry: function ACU_viewEntry(key) { |
|
289 let uri; |
|
290 |
|
291 Services.cache.visitEntries({ |
|
292 visitDevice: function(deviceID, deviceInfo) { |
|
293 return true; |
|
294 }, |
|
295 |
|
296 visitEntry: function(deviceID, entryInfo) { |
|
297 if (entryInfo.deviceID == "offline" && entryInfo.key == key) { |
|
298 uri = "about:cache-entry?client=" + entryInfo.clientID + |
|
299 "&sb=1&key=" + entryInfo.key; |
|
300 return false; |
|
301 } |
|
302 return true; |
|
303 } |
|
304 }); |
|
305 |
|
306 if (uri) { |
|
307 let wm = Cc["@mozilla.org/appshell/window-mediator;1"] |
|
308 .getService(Ci.nsIWindowMediator); |
|
309 let win = wm.getMostRecentWindow("navigator:browser"); |
|
310 win.gBrowser.selectedTab = win.gBrowser.addTab(uri); |
|
311 } else { |
|
312 return l10n.GetStringFromName("entryNotFound"); |
|
313 } |
|
314 }, |
|
315 |
|
316 clearAll: function ACU_clearAll() { |
|
317 Services.cache.evictEntries(Ci.nsICache.STORE_OFFLINE); |
|
318 }, |
|
319 |
|
320 _getManifestURI: function ACU__getManifestURI() { |
|
321 let deferred = promise.defer(); |
|
322 |
|
323 let getURI = node => { |
|
324 let htmlNode = this.doc.querySelector("html[manifest]"); |
|
325 if (htmlNode) { |
|
326 let pageUri = this.doc.location ? this.doc.location.href : this.uri; |
|
327 let origin = pageUri.substr(0, pageUri.lastIndexOf("/") + 1); |
|
328 return origin + htmlNode.getAttribute("manifest"); |
|
329 } |
|
330 }; |
|
331 |
|
332 if (this.doc) { |
|
333 let uri = getURI(this.doc); |
|
334 return promise.resolve(uri); |
|
335 } else { |
|
336 this._getURIInfo(this.uri).then(uriInfo => { |
|
337 if (uriInfo.success) { |
|
338 let html = uriInfo.text; |
|
339 let parser = _DOMParser; |
|
340 this.doc = parser.parseFromString(html, "text/html"); |
|
341 let uri = getURI(this.doc); |
|
342 deferred.resolve(uri); |
|
343 } else { |
|
344 this.errors.push({ |
|
345 line: 0, |
|
346 msg: l10n.GetStringFromName("invalidURI") |
|
347 }); |
|
348 } |
|
349 }); |
|
350 } |
|
351 return deferred.promise; |
|
352 }, |
|
353 |
|
354 _addError: function ACU__addError(line, l10nString, ...params) { |
|
355 let msg; |
|
356 |
|
357 if (params) { |
|
358 msg = l10n.formatStringFromName(l10nString, params, params.length); |
|
359 } else { |
|
360 msg = l10n.GetStringFromName(l10nString); |
|
361 } |
|
362 |
|
363 this.errors.push({ |
|
364 line: line, |
|
365 msg: msg |
|
366 }); |
|
367 }, |
|
368 }; |
|
369 |
|
370 /** |
|
371 * We use our own custom parser because we need far more detailed information |
|
372 * than the system manifest parser provides. |
|
373 * |
|
374 * @param {String} manifestText |
|
375 * The text content of the manifest file. |
|
376 * @param {String} manifestURI |
|
377 * The URI of the manifest file. This is used in calculating the path of |
|
378 * relative URIs. |
|
379 */ |
|
380 function ManifestParser(manifestText, manifestURI) { |
|
381 this.manifestText = manifestText; |
|
382 this.origin = manifestURI.substr(0, manifestURI.lastIndexOf("/") + 1) |
|
383 .replace(" ", "%20"); |
|
384 } |
|
385 |
|
386 ManifestParser.prototype = { |
|
387 parse: function OCIMP_parse() { |
|
388 let lines = this.manifestText.split(/\r?\n/); |
|
389 let fallbacks = this.fallbacks = []; |
|
390 let settings = this.settings = []; |
|
391 let errors = this.errors = []; |
|
392 let uris = this.uris = []; |
|
393 |
|
394 this.currSection = "CACHE"; |
|
395 |
|
396 for (let i = 0; i < lines.length; i++) { |
|
397 let text = this.text = lines[i].replace(/^\s+|\s+$/g); |
|
398 this.currentLine = i + 1; |
|
399 |
|
400 if (i == 0 && text != "CACHE MANIFEST") { |
|
401 this._addError(1, "firstLineMustBeCacheManifest", 1); |
|
402 } |
|
403 |
|
404 // Ignore comments |
|
405 if (/^#/.test(text) || !text.length) { |
|
406 continue; |
|
407 } |
|
408 |
|
409 if (text == "CACHE MANIFEST") { |
|
410 if (this.currentLine != 1) { |
|
411 this._addError(this.currentLine, "cacheManifestOnlyFirstLine2", |
|
412 this.currentLine); |
|
413 } |
|
414 continue; |
|
415 } |
|
416 |
|
417 if (this._maybeUpdateSectionName()) { |
|
418 continue; |
|
419 } |
|
420 |
|
421 switch (this.currSection) { |
|
422 case "CACHE": |
|
423 case "NETWORK": |
|
424 this.parseLine(); |
|
425 break; |
|
426 case "FALLBACK": |
|
427 this.parseFallbackLine(); |
|
428 break; |
|
429 case "SETTINGS": |
|
430 this.parseSettingsLine(); |
|
431 break; |
|
432 } |
|
433 } |
|
434 |
|
435 return { |
|
436 uris: uris, |
|
437 fallbacks: fallbacks, |
|
438 settings: settings, |
|
439 errors: errors |
|
440 }; |
|
441 }, |
|
442 |
|
443 parseLine: function OCIMP_parseLine() { |
|
444 let text = this.text; |
|
445 |
|
446 if (text.indexOf("*") != -1) { |
|
447 if (this.currSection != "NETWORK" || text.length != 1) { |
|
448 this._addError(this.currentLine, "asteriskInWrongSection2", |
|
449 this.currSection, this.currentLine); |
|
450 return; |
|
451 } |
|
452 } |
|
453 |
|
454 if (/\s/.test(text)) { |
|
455 this._addError(this.currentLine, "escapeSpaces", this.currentLine); |
|
456 text = text.replace(/\s/g, "%20") |
|
457 } |
|
458 |
|
459 if (text[0] == "/") { |
|
460 if (text.substr(0, 4) == "/../") { |
|
461 this._addError(this.currentLine, "slashDotDotSlashBad", this.currentLine); |
|
462 } else { |
|
463 this.uris.push(this._wrapURI(this.origin + text.substring(1))); |
|
464 } |
|
465 } else if (text.substr(0, 2) == "./") { |
|
466 this.uris.push(this._wrapURI(this.origin + text.substring(2))); |
|
467 } else if (text.substr(0, 4) == "http") { |
|
468 this.uris.push(this._wrapURI(text)); |
|
469 } else { |
|
470 let origin = this.origin; |
|
471 let path = text; |
|
472 |
|
473 while (path.substr(0, 3) == "../" && /^https?:\/\/.*?\/.*?\//.test(origin)) { |
|
474 let trimIdx = origin.substr(0, origin.length - 1).lastIndexOf("/") + 1; |
|
475 origin = origin.substr(0, trimIdx); |
|
476 path = path.substr(3); |
|
477 } |
|
478 |
|
479 if (path.substr(0, 3) == "../") { |
|
480 this._addError(this.currentLine, "tooManyDotDotSlashes", this.currentLine); |
|
481 return; |
|
482 } |
|
483 |
|
484 if (/^https?:\/\//.test(path)) { |
|
485 this.uris.push(this._wrapURI(path)); |
|
486 return; |
|
487 } |
|
488 this.uris.push(this._wrapURI(origin + path)); |
|
489 } |
|
490 }, |
|
491 |
|
492 parseFallbackLine: function OCIMP_parseFallbackLine() { |
|
493 let split = this.text.split(/\s+/); |
|
494 let origURI = this.text; |
|
495 |
|
496 if (split.length != 2) { |
|
497 this._addError(this.currentLine, "fallbackUseSpaces", this.currentLine); |
|
498 return; |
|
499 } |
|
500 |
|
501 let [ namespace, fallback ] = split; |
|
502 |
|
503 if (namespace.indexOf("*") != -1) { |
|
504 this._addError(this.currentLine, "fallbackAsterisk2", this.currentLine); |
|
505 } |
|
506 |
|
507 if (/\s/.test(namespace)) { |
|
508 this._addError(this.currentLine, "escapeSpaces", this.currentLine); |
|
509 namespace = namespace.replace(/\s/g, "%20") |
|
510 } |
|
511 |
|
512 if (namespace.substr(0, 4) == "/../") { |
|
513 this._addError(this.currentLine, "slashDotDotSlashBad", this.currentLine); |
|
514 } |
|
515 |
|
516 if (namespace.substr(0, 2) == "./") { |
|
517 namespace = this.origin + namespace.substring(2); |
|
518 } |
|
519 |
|
520 if (namespace.substr(0, 4) != "http") { |
|
521 let origin = this.origin; |
|
522 let path = namespace; |
|
523 |
|
524 while (path.substr(0, 3) == "../" && /^https?:\/\/.*?\/.*?\//.test(origin)) { |
|
525 let trimIdx = origin.substr(0, origin.length - 1).lastIndexOf("/") + 1; |
|
526 origin = origin.substr(0, trimIdx); |
|
527 path = path.substr(3); |
|
528 } |
|
529 |
|
530 if (path.substr(0, 3) == "../") { |
|
531 this._addError(this.currentLine, "tooManyDotDotSlashes", this.currentLine); |
|
532 } |
|
533 |
|
534 if (/^https?:\/\//.test(path)) { |
|
535 namespace = path; |
|
536 } else { |
|
537 if (path[0] == "/") { |
|
538 path = path.substring(1); |
|
539 } |
|
540 namespace = origin + path; |
|
541 } |
|
542 } |
|
543 |
|
544 this.text = fallback; |
|
545 this.parseLine(); |
|
546 |
|
547 this.fallbacks.push({ |
|
548 line: this.currentLine, |
|
549 original: origURI, |
|
550 namespace: namespace, |
|
551 fallback: fallback |
|
552 }); |
|
553 }, |
|
554 |
|
555 parseSettingsLine: function OCIMP_parseSettingsLine() { |
|
556 let text = this.text; |
|
557 |
|
558 if (this.settings.length == 1 || !/prefer-online|fast/.test(text)) { |
|
559 this._addError(this.currentLine, "settingsBadValue", this.currentLine); |
|
560 return; |
|
561 } |
|
562 |
|
563 switch (text) { |
|
564 case "prefer-online": |
|
565 this.settings.push(this._wrapURI(text)); |
|
566 break; |
|
567 case "fast": |
|
568 this.settings.push(this._wrapURI(text)); |
|
569 break; |
|
570 } |
|
571 }, |
|
572 |
|
573 _wrapURI: function OCIMP__wrapURI(uri) { |
|
574 return { |
|
575 section: this.currSection, |
|
576 line: this.currentLine, |
|
577 uri: uri, |
|
578 original: this.text |
|
579 }; |
|
580 }, |
|
581 |
|
582 _addError: function OCIMP__addError(line, l10nString, ...params) { |
|
583 let msg; |
|
584 |
|
585 if (params) { |
|
586 msg = l10n.formatStringFromName(l10nString, params, params.length); |
|
587 } else { |
|
588 msg = l10n.GetStringFromName(l10nString); |
|
589 } |
|
590 |
|
591 this.errors.push({ |
|
592 line: line, |
|
593 msg: msg |
|
594 }); |
|
595 }, |
|
596 |
|
597 _maybeUpdateSectionName: function OCIMP__maybeUpdateSectionName() { |
|
598 let text = this.text; |
|
599 |
|
600 if (text == text.toUpperCase() && text.charAt(text.length - 1) == ":") { |
|
601 text = text.substr(0, text.length - 1); |
|
602 |
|
603 switch (text) { |
|
604 case "CACHE": |
|
605 case "NETWORK": |
|
606 case "FALLBACK": |
|
607 case "SETTINGS": |
|
608 this.currSection = text; |
|
609 return true; |
|
610 default: |
|
611 this._addError(this.currentLine, |
|
612 "invalidSectionName", text, this.currentLine); |
|
613 return false; |
|
614 } |
|
615 } |
|
616 }, |
|
617 }; |
|
618 |
|
619 XPCOMUtils.defineLazyGetter(this, "l10n", function() Services.strings |
|
620 .createBundle("chrome://browser/locale/devtools/appcacheutils.properties")); |
|
621 |
|
622 XPCOMUtils.defineLazyGetter(this, "appcacheservice", function() { |
|
623 return Cc["@mozilla.org/network/application-cache-service;1"] |
|
624 .getService(Ci.nsIApplicationCacheService); |
|
625 |
|
626 }); |
|
627 |
|
628 XPCOMUtils.defineLazyGetter(this, "_DOMParser", function() { |
|
629 return Cc["@mozilla.org/xmlextras/domparser;1"].createInstance(Ci.nsIDOMParser); |
|
630 }); |