visit: function vmh_visit(chan) { if (log.enabled) { let msg = chan.URI.spec + "\nRequest:\n"; let visitor = { visitHeader: function(header, value) { msg += header + ": " + value + "\n"; } }; chan.visitRequestHeaders(visitor); msg += "\nResponse:\n"; chan.visitResponseHeaders(visitor); log(LOG_DEBUG, msg); } try { this.type = chan.getResponseHeader("content-type"); var ch = this.type.match(/charset=['"]?([\w\d_-]+)/i); if (ch && ch[1].length) { log(LOG_DEBUG, "visitHeader: found override to " + ch[1]); this._charset = this.overrideCharset = identity(ch[1]); } } catch (ex) {} try { this.encoding = identity(chan.getResponseHeader("content-encoding")); } catch (ex) {} try { this.acceptRanges = !/none/i.test(chan.getResponseHeader("accept-ranges")); if (!this.acceptRanges) { this.acceptRanges = !~this.acceptRanges.toLowerCase().indexOf('none'); } } catch (ex) {} let contentLength; try { contentLength = parseInt(chan.getResponseHeader("content-length"), 10); } catch (ex) {} if (contentLength < 0 || isNaN(contentLength)) { try { contentLength = parseInt(chan.getResponseHeader("content-range").split("/").pop(), 10); } catch (ex) {} } if (contentLength > 0 && !isNaN(contentLength)) { this.contentLength = contentLength; } try { let digest = chan.getResponseHeader("digest").replace(/,/g, ";"); digest = ";" + digest; for (let t in DTA.SUPPORTED_HASHES_ALIASES) { try { let v = Services.mimeheader.getParameter(digest, t, this._charset, true, {}); if (!v) { continue; } v = atob(v); v = new DTA.Hash(v, t); if (!this.hash || this.hash.q < v.q) { this.hash = v; } } catch (ex) { // no-op } } } catch (ex) {} try { let poweredby = chan.getResponseHeader("x-powered-by"); if (!!poweredby) { this.relaxSize = true; } } catch (ex) {} try { delete this.mirrors; let links = chan.getResponseHeader("Link").split(/,\s*/g); for (let link of links) { try { let linkURI = Services.mimeheader.getParameter(link, null, null, true, {}) .replace(/[<>]/g, ''); const rel = Services.mimeheader.getParameter(link, "rel", null, true, {}); if (rel === "describedby") { const type = Services.mimeheader.getParameter(link, "type", null, true, {}); if (type === "application/metalink4+xml") { this.metaDescribedBy = Services.io.newURI(linkURI, null, null); } } else if (rel === "duplicate") { linkURI = Services.io.newURI(linkURI, null, null); let pri, pref, depth; try { pri = Services.mimeheader.getParameter(link, "pri", null, true, {}); pri = parseInt(pri, 10); try { pref = Services.mimeheader.getParameter(link, "pref", null, true, {}); pri = 1; } catch (ex) {} try{ depth = Services.mimeheader.getParameter(link, "depth", null, true, {}); } catch (ex) {} try { const geo = Services.mimeheader.getParameter(link, "geo", null, true, {}) .slice(0,2).toLowerCase(); if (~LOCALE.indexOf(geo)) { pri = Math.max(pri / 4, 1); } } catch (ex) {} } catch (ex) {} if (!this.mirrors) { this.mirrors = []; } this.mirrors.push(new DTA.URL(linkURI, pri)); } } catch (ex) { log(LOG_ERROR, "VM: failed to process a link", ex); } } if (this.mirrors) { normalizeMetaPrefs(this.mirrors); } } catch (ex) { log(LOG_DEBUG, "VM: failed to process links", ex); } for (let header in this.cmpKeys) { try { let value = chan.getResponseHeader(header); this[header] = value; } catch (ex) {} } if ("etag" in this) { let etag = this.etag; this.etag = etag .replace(/^(?:[Ww]\/)?"(.+)"$/, '$1') .replace(/^[a-f\d]+-([a-f\d]+)-([a-f\d]+)$/, '$1-$2') .replace(/^([a-f\d]+):[a-f\d]{1,6}$/, '$1'); log(LOG_DEBUG, "Etag: " + this.etag + " - " + etag); } if ("last-modified" in this) { try { this.time = getTimestamp(this["last-modified"]); } catch (ex) {} } try { this._checkFileName(chan.getResponseHeader("content-disposition")); } catch (ex) {} if (!("fileName" in this) && ("type" in this)) { this._checkFileName(this.type); } },
parse(aReferrer) { if (aReferrer && 'spec' in aReferrer) { aReferrer = aReferrer.spec; } let doc = this._doc; let root = doc.documentElement; let downloads = []; let files = this.getNodes(doc, '/ml:metalink/ml:file'); if (!files.length) { throw new Exception("No valid file nodes"); } for (let file of files) { let fileName = file.getAttribute('name'); if (!fileName) { throw new Exception("LocalFile name not provided!"); } let referrer = null; if (file.hasAttributeNS(NS_DTA, 'referrer')) { referrer = file.getAttributeNS(NS_DTA, 'referrer'); } else { referrer = aReferrer; } let num = null; if (file.hasAttributeNS(NS_DTA, 'num')) { try { num = parseInt(file.getAttributeNS(NS_DTA, 'num'), 10); } catch (ex) { /* no-op */ } } if (!num) { num = DTA.currentSeries(); } let startDate = new Date(); if (file.hasAttributeNS(NS_DTA, 'startDate')) { try { startDate = new Date(parseInt(file.getAttributeNS(NS_DTA, 'startDate'), 10)); } catch (ex) { /* no-op */ } } let urls = []; let urlNodes = this.getNodes(file, 'ml:url'); for (var url of urlNodes) { let preference = 1; let charset = doc.characterSet; if (url.hasAttributeNS(NS_DTA, 'charset')) { charset = url.getAttributeNS(NS_DTA, 'charset'); } let uri = null; try { uri = this.checkURL(url.textContent.trim()); if (!uri) { throw new Exception("Invalid url"); } uri = Services.io.newURI(uri, charset, null); } catch (ex) { log(LOG_ERROR, "Failed to parse URL" + url.textContent, ex); continue; } if (url.hasAttribute('priority')) { let a = parseInt(url.getAttribute('priority'), 10); if (a > 0) { preference = a; } } if (url.hasAttribute('location')) { let a = url.getAttribute('location').slice(0,2).toLowerCase(); if (~LOCALE.indexOf(a)) { preference = Math.max(preference / 4, 1); } } urls.push(new DTA.URL(uri, preference)); } if (!urls.length) { continue; } normalizeMetaPrefs(urls); let size = this.getSingle(file, 'size'); size = parseInt(size, 10); if (!isFinite(size)) { size = 0; } let hash = null; for (let h of this.getNodes(file, 'ml:hash')) { try { h = new DTA.Hash(h.textContent.trim(), h.getAttribute('type')); if (!hash || hash.q < h.q) { hash = h; } } catch (ex) { log(LOG_ERROR, "Failed to parse hash: " + h.textContent.trim() + "/" + h.getAttribute('type'), ex); } } if (hash) { Cu.reportError(hash); hash = new DTA.HashCollection(hash); let pieces = this.getNodes(file, 'ml:pieces'); if (pieces.length) { pieces = pieces[0]; let type = pieces.getAttribute('type').trim(); try { hash.parLength = parseInt(pieces.getAttribute('length'), 10); if (!isFinite(hash.parLength) || hash.parLength < 1) { throw new Exception("Invalid pieces length"); } for (let piece of this.getNodes(pieces, 'ml:hash')) { try { hash.add(new DTA.Hash(piece.textContent.trim(), type)); } catch (ex) { log(LOG_ERROR, "Failed to parse piece", ex); throw ex; } } if (size && hash.parLength * hash.partials.length < size) { throw new Exception("too few partials"); } else if(size && (hash.partials.length - 1) * hash.parLength > size) { throw new Exception("too many partials"); } log(LOG_DEBUG, "loaded " + hash.partials.length + " partials"); } catch (ex) { log(LOG_ERROR, "Failed to parse pieces", ex); hash = new DTA.HashCollection(hash.full); } } } let desc = this.getSingle(file, 'description'); if (!desc) { desc = this.getSingle(root, 'description'); } downloads.push({ 'url': new UrlManager(urls), 'fileName': fileName, 'referrer': referrer ? referrer : null, 'numIstance': num, 'title': '', 'description': desc, 'startDate': startDate, 'hashCollection': hash, 'license': this.getLinkRes(file, "license"), 'publisher': this.getLinkRes(file, "publisher"), 'identity': this.getSingle(file, "identity"), 'copyright': this.getSingle(file, "copyright"), 'size': size, 'version': this.getSingle(file, "version"), 'logo': this.checkURL(this.getSingle(file, "logo", ['data'])), 'lang': this.getSingle(file, "language"), 'sys': this.getSingle(file, "os"), 'mirrors': urls.length, 'selected': true, 'fromMetalink': true }); } if (!downloads.length) { throw new Exception("No valid files to process"); } let info = { 'identity': this.getSingle(root, "identity"), 'description': this.getSingle(root, "description"), 'logo': this.checkURL(this.getSingle(root, "logo", ['data'])), 'license': this.getLinkRes(root, "license"), 'publisher': this.getLinkRes(root, "publisher"), 'start': false }; return new Metalink(downloads, info, "Metalinker Version 4.0 (RFC5854/IETF)"); }