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/ */
7
* Metalinker3 namespace
9
const NS_METALINKER3 = 'http://www.metalinker.org/';
11
* Metalinker 4 namespace
13
const NS_METALINK_RFC5854 = 'urn:ietf:params:xml:ns:metalink';
15
const DTA = require("api");
16
const Preferences = require("preferences");
17
const {LOCALE} = require("version");
18
const {UrlManager} = require("./urlmanager");
19
const {NS_DTA, NS_HTML, normalizeMetaPrefs} = require("utils");
21
const XPathResult = Ci.nsIDOMXPathResult;
24
* Parsed Metalink representation
25
* (Do not construct yourself unless you know what you're doing)
27
function Metalink(downloads, info, parser) {
28
this.downloads = downloads;
32
Metalink.prototype = {
38
* Dict of general information
42
* Parser identifaction
47
function Base(doc, NS) {
52
lookupNamespaceURI: function Base_lookupNamespaceURI(prefix) {
61
getNodes: function (elem, query) {
63
let iterator = this._doc.evaluate(
67
XPathResult.ORDERED_NODE_ITERATOR_TYPE,
70
for (let n = iterator.iterateNext(); n; n = iterator.iterateNext()) {
75
getNode: function Base_getNode(elem, query) {
76
let r = this.getNodes(elem, query);
82
getSingle: function BasegetSingle(elem, query) {
83
let rv = this.getNode(elem, 'ml:' + query);
84
return rv ? rv.textContent.trim() : '';
86
getLinkRes: function BasegetLinkRes(elem, query) {
87
let rv = this.getNode(elem, 'ml:' + query);
89
let n = this.getSingle(rv, 'name'), l = this.checkURL(this.getSingle(rv, 'url'));
96
checkURL: function Base_checkURL(url, allowed) {
101
url = Services.io.newURI(url, this._doc.characterSet, null);
102
if (url.scheme === 'file') {
103
throw new Exception("file protocol invalid!");
105
// check for some popular bad links :p
106
if (!~['http', 'https', 'ftp'].indexOf(url.scheme) || !~url.host.indexOf('.')) {
107
if (!(allowed instanceof Array)) {
108
throw new Exception("bad link!");
110
if (!~allowed.indexOf(url.scheme)) {
111
throw new Exception("not allowed!");
117
log(LOG_ERROR, "checkURL: failed to parse " + url, ex);
126
* @param doc document to parse
129
function Metalinker3(doc) {
130
let root = doc.documentElement;
131
if (root.nodeName !== 'metalink' || root.getAttribute('version') !== '3.0') {
132
throw new Exception('mlinvalid');
134
Base.call(this, doc, NS_METALINKER3);
136
Metalinker3.prototype = {
137
__proto__: Base.prototype,
138
parse: function ML3_parse(aReferrer) {
139
if (aReferrer && 'spec' in aReferrer) {
140
aReferrer = aReferrer.spec;
144
let root = doc.documentElement;
147
let files = this.getNodes(doc, '//ml:files/ml:file');
150
throw new Exception("No valid file nodes");
153
for (let file of files) {
154
let fileName = file.getAttribute('name');
156
throw new Exception("LocalFile name not provided!");
159
if (file.hasAttributeNS(NS_DTA, 'referrer')) {
160
referrer = file.getAttributeNS(NS_DTA, 'referrer');
163
referrer = aReferrer;
166
if (file.hasAttributeNS(NS_DTA, 'num')) {
168
num = parseInt(file.getAttributeNS(NS_DTA, 'num'), 10);
175
num = DTA.currentSeries();
177
let startDate = new Date();
178
if (file.hasAttributeNS(NS_DTA, 'startDate')) {
180
startDate = new Date(parseInt(file.getAttributeNS(NS_DTA, 'startDate'), 10));
188
let urlNodes = this.getNodes(file, 'ml:resources/ml:url');
189
for (var url of urlNodes) {
191
let charset = doc.characterSet;
192
if (url.hasAttributeNS(NS_DTA, 'charset')) {
193
charset = url.getAttributeNS(NS_DTA, 'charset');
198
if (url.hasAttribute('type') && !url.getAttribute('type').match(/^(?:https?|ftp)$/i)) {
199
throw new Exception("Invalid url type");
201
uri = this.checkURL(url.textContent.trim());
203
throw new Exception("Invalid url");
205
else if(!url.hasAttribute('type') && uri.substr(-8) === ".torrent") {
206
throw new Exception("Torrent downloads not supported");
208
uri = Services.io.newURI(uri, charset, null);
211
log(LOG_ERROR, "Failed to parse URL" + url.textContent, ex);
215
if (url.hasAttribute('preference')) {
216
let a = parseInt(url.getAttribute('preference'), 10);
217
if (isFinite(a) && a > 0 && a < 101) {
221
if (url.hasAttribute('location')) {
222
let a = url.getAttribute('location').slice(0,2).toLowerCase();
223
if (~LOCALE.indexOf(a)) {
224
preference = 100 + preference;
227
urls.push(new DTA.URL(uri, preference));
232
let size = this.getSingle(file, 'size');
233
size = parseInt(size, 10);
234
if (!isFinite(size)) {
239
for (let h of this.getNodes(file, 'ml:verification/ml:hash')) {
241
h = new DTA.Hash(h.textContent.trim(), h.getAttribute('type'));
242
if (!hash || hash.q < h.q) {
247
log(LOG_ERROR, "Failed to parse hash: " + h.textContent.trim() + "/" + h.getAttribute('type'), ex);
251
hash = new DTA.HashCollection(hash);
252
let pieces = this.getNodes(file, 'ml:verification/ml:pieces');
255
let type = pieces.getAttribute('type').trim();
257
hash.parLength = parseInt(pieces.getAttribute('length'), 10);
258
if (!isFinite(hash.parLength) || hash.parLength < 1) {
259
throw new Exception("Invalid pieces length");
262
let maxPiece = Math.ceil(size / hash.parLength);
263
for (let piece of this.getNodes(pieces, 'ml:hash')) {
265
let num = parseInt(piece.getAttribute('piece'), 10);
266
if (!maxPiece || (num >= 0 && num <= maxPiece)) {
267
collection[num] = new DTA.Hash(piece.textContent.trim(), type);
270
throw new Exception("out of bound piece");
274
log(LOG_ERROR, "Failed to parse piece", ex);
278
let totalPieces = maxPiece || collection.length;
279
for (let i = 0; i < totalPieces; i++) {
281
hash.add(collection[i]);
284
throw new Exception("missing piece");
287
log(LOG_DEBUG, "loaded " + hash.partials.length + " partials");
290
log(LOG_ERROR, "Failed to parse pieces", ex);
291
hash = new DTA.HashCollection(hash.full);
295
let desc = this.getSingle(file, 'description');
297
desc = this.getSingle(root, 'description');
301
'url': new UrlManager(urls),
302
'fileName': fileName,
303
'referrer': referrer ? referrer : null,
307
'startDate': startDate,
308
'hashCollection': hash,
309
'license': this.getLinkRes(file, "license"),
310
'publisher': this.getLinkRes(file, "publisher"),
311
'identity': this.getSingle(file, 'identity'),
312
'copyright': this.getSingle(file, 'copyright'),
314
'version': this.getSingle(file, 'version'),
315
'logo': this.checkURL(this.getSingle(file, 'logo', ['data'])),
316
'lang': this.getSingle(file, 'language'),
317
'sys': this.getSingle(file, 'os'),
318
'mirrors': urls.length,
324
if (!downloads.length) {
325
throw new Exception("No valid files to process");
328
'identity': this.getSingle(root, 'identity'),
329
'description': this.getSingle(root, 'description'),
330
'logo': this.checkURL(this.getSingle(root, 'logo', ['data'])),
331
'license': this.getLinkRes(root, "license"),
332
'publisher': this.getLinkRes(root, "publisher"),
335
return new Metalink(downloads, info, "Metalinker Version 3.0");
340
* Metalink RFC5854 (IETF) Parser
341
* @param doc document to parse
344
function MetalinkerRFC5854(doc) {
345
let root = doc.documentElement;
346
if (root.nodeName !== 'metalink' || root.namespaceURI !== NS_METALINK_RFC5854 ) {
348
log(LOG_DEBUG, root.nodeName + "\nns:" + root.namespaceURI);
350
throw new Exception('mlinvalid');
352
Base.call(this, doc, NS_METALINK_RFC5854);
354
MetalinkerRFC5854.prototype = {
355
__proto__: Base.prototype,
356
parse: function ML4_parse(aReferrer) {
357
if (aReferrer && 'spec' in aReferrer) {
358
aReferrer = aReferrer.spec;
362
let root = doc.documentElement;
365
let files = this.getNodes(doc, '/ml:metalink/ml:file');
367
throw new Exception("No valid file nodes");
369
for (let file of files) {
370
let fileName = file.getAttribute('name');
372
throw new Exception("LocalFile name not provided!");
375
if (file.hasAttributeNS(NS_DTA, 'referrer')) {
376
referrer = file.getAttributeNS(NS_DTA, 'referrer');
379
referrer = aReferrer;
382
if (file.hasAttributeNS(NS_DTA, 'num')) {
384
num = parseInt(file.getAttributeNS(NS_DTA, 'num'), 10);
391
num = DTA.currentSeries();
393
let startDate = new Date();
394
if (file.hasAttributeNS(NS_DTA, 'startDate')) {
396
startDate = new Date(parseInt(file.getAttributeNS(NS_DTA, 'startDate'), 10));
404
let urlNodes = this.getNodes(file, 'ml:url');
405
for (var url of urlNodes) {
407
let charset = doc.characterSet;
408
if (url.hasAttributeNS(NS_DTA, 'charset')) {
409
charset = url.getAttributeNS(NS_DTA, 'charset');
414
uri = this.checkURL(url.textContent.trim());
416
throw new Exception("Invalid url");
418
uri = Services.io.newURI(uri, charset, null);
421
log(LOG_ERROR, "Failed to parse URL" + url.textContent, ex);
425
if (url.hasAttribute('priority')) {
426
let a = parseInt(url.getAttribute('priority'), 10);
431
if (url.hasAttribute('location')) {
432
let a = url.getAttribute('location').slice(0,2).toLowerCase();
433
if (~LOCALE.indexOf(a)) {
434
preference = Math.max(preference / 4, 1);
437
urls.push(new DTA.URL(uri, preference));
442
normalizeMetaPrefs(urls);
444
let size = this.getSingle(file, 'size');
445
size = parseInt(size, 10);
446
if (!isFinite(size)) {
451
for (let h of this.getNodes(file, 'ml:hash')) {
453
h = new DTA.Hash(h.textContent.trim(), h.getAttribute('type'));
454
if (!hash || hash.q < h.q) {
459
log(LOG_ERROR, "Failed to parse hash: " + h.textContent.trim() + "/" + h.getAttribute('type'), ex);
463
hash = new DTA.HashCollection(hash);
464
let pieces = this.getNodes(file, 'ml:pieces');
467
let type = pieces.getAttribute('type').trim();
469
hash.parLength = parseInt(pieces.getAttribute('length'), 10);
470
if (!isFinite(hash.parLength) || hash.parLength < 1) {
471
throw new Exception("Invalid pieces length");
473
for (let piece of this.getNodes(pieces, 'ml:hash')) {
475
hash.add(new DTA.Hash(piece.textContent.trim(), type));
478
log(LOG_ERROR, "Failed to parse piece", ex);
482
if (size && hash.parLength * hash.partials.length < size) {
483
throw new Exception("too few partials");
485
else if(size && (hash.partials.length - 1) * hash.parLength > size) {
486
throw new Exception("too many partials");
488
log(LOG_DEBUG, "loaded " + hash.partials.length + " partials");
491
log(LOG_ERROR, "Failed to parse pieces", ex);
492
hash = new DTA.HashCollection(hash.full);
497
let desc = this.getSingle(file, 'description');
499
desc = this.getSingle(root, 'description');
502
'url': new UrlManager(urls),
503
'fileName': fileName,
504
'referrer': referrer ? referrer : null,
508
'startDate': startDate,
509
'hashCollection': hash,
510
'license': this.getLinkRes(file, "license"),
511
'publisher': this.getLinkRes(file, "publisher"),
512
'identity': this.getSingle(file, "identity"),
513
'copyright': this.getSingle(file, "copyright"),
515
'version': this.getSingle(file, "version"),
516
'logo': this.checkURL(this.getSingle(file, "logo", ['data'])),
517
'lang': this.getSingle(file, "language"),
518
'sys': this.getSingle(file, "os"),
519
'mirrors': urls.length,
525
if (!downloads.length) {
526
throw new Exception("No valid files to process");
530
'identity': this.getSingle(root, "identity"),
531
'description': this.getSingle(root, "description"),
532
'logo': this.checkURL(this.getSingle(root, "logo", ['data'])),
533
'license': this.getLinkRes(root, "license"),
534
'publisher': this.getLinkRes(root, "publisher"),
537
return new Metalink(downloads, info, "Metalinker Version 4.0 (RFC5854/IETF)");
541
const __parsers__ = [
548
* @param aURI (nsIURI) Metalink URI
549
* @param aReferrer (String) Optional. Referrer
550
* @param aCallback (Function) Receiving callback function of form f(result, exception || null)
551
* @return async (Metalink) Parsed metalink data
553
function parse(aURI, aReferrer, aCallback) {
554
let xhrLoad, xhrError;
555
let xhr = new Instances.XHR();
556
xhr.open("GET", aURI.spec);
557
log(LOG_DEBUG, "parsing metalink at " + aURI.spec);
558
xhr.overrideMimeType("application/xml");
559
xhr.addEventListener("loadend", function xhrLoadend() {
560
xhr.removeEventListener("loadend", xhrLoadend, false);
562
let doc = xhr.responseXML;
563
if (doc.documentElement.nodeName === 'parsererror') {
564
throw new Exception("Failed to parse XML");
566
for (let Parser of __parsers__) {
569
parser = new Parser(doc);
572
log(LOG_DEBUG, Parser.name + " failed", ex);
575
aCallback(parser.parse(aReferrer));
578
throw new Exception("no suitable parser found!");
581
log(LOG_ERROR, "Failed to parse metalink", ex);
588
Object.defineProperties(exports, {
589
"parse": {value: parse, enumerable: true},
590
"Metalink": {value: Metalink, enumerable: true},
591
"NS_DTA": {value: NS_DTA, enumerable: true},
592
"NS_HTML": {value: NS_HTML, enumerable: true},
593
"NS_METALINKER3": {value: NS_METALINKER3, enumerable: true},
594
"NS_METALINK_RFC5854": {value: NS_METALINK_RFC5854, enumerable: true}
596
Object.freeze(exports);