/* * Copyright (c) 2017 Digital Bazaar, Inc. All rights reserved. */ 'use strict'; const {parseLinkHeader, buildHeaders} = require('../util'); const {LINK_HEADER_CONTEXT} = require('../constants'); const JsonLdError = require('../JsonLdError'); const RequestQueue = require('../RequestQueue'); const {prependBase} = require('../url'); const REGEX_LINK_HEADER = /(^|(\r\n))link:/i; /** * Creates a built-in XMLHttpRequest document loader. * * @param options the options to use: * secure: require all URLs to use HTTPS. * headers: an object (map) of headers which will be passed as request * headers for the requested document. Accept is not allowed. * [xhr]: the XMLHttpRequest API to use. * * @return the XMLHttpRequest document loader. */ module.exports = ({ secure, headers = {}, xhr } = {headers: {}}) => { headers = buildHeaders(headers); const queue = new RequestQueue(); return queue.wrapLoader(loader); async function loader(url) { if(url.indexOf('http:') !== 0 && url.indexOf('https:') !== 0) { throw new JsonLdError( 'URL could not be dereferenced; only "http" and "https" URLs are ' + 'supported.', 'jsonld.InvalidUrl', {code: 'loading document failed', url}); } if(secure && url.indexOf('https') !== 0) { throw new JsonLdError( 'URL could not be dereferenced; secure mode is enabled and ' + 'the URL\'s scheme is not "https".', 'jsonld.InvalidUrl', {code: 'loading document failed', url}); } let req; try { req = await _get(xhr, url, headers); } catch(e) { throw new JsonLdError( 'URL could not be dereferenced, an error occurred.', 'jsonld.LoadDocumentError', {code: 'loading document failed', url, cause: e}); } if(req.status >= 400) { throw new JsonLdError( 'URL could not be dereferenced: ' + req.statusText, 'jsonld.LoadDocumentError', { code: 'loading document failed', url, httpStatusCode: req.status }); } let doc = {contextUrl: null, documentUrl: url, document: req.response}; let alternate = null; // handle Link Header (avoid unsafe header warning by existence testing) const contentType = req.getResponseHeader('Content-Type'); let linkHeader; if(REGEX_LINK_HEADER.test(req.getAllResponseHeaders())) { linkHeader = req.getResponseHeader('Link'); } if(linkHeader && contentType !== 'application/ld+json') { // only 1 related link header permitted const linkHeaders = parseLinkHeader(linkHeader); const linkedContext = linkHeaders[LINK_HEADER_CONTEXT]; if(Array.isArray(linkedContext)) { throw new JsonLdError( 'URL could not be dereferenced, it has more than one ' + 'associated HTTP Link Header.', 'jsonld.InvalidUrl', {code: 'multiple context link headers', url}); } if(linkedContext) { doc.contextUrl = linkedContext.target; } // "alternate" link header is a redirect alternate = linkHeaders.alternate; if(alternate && alternate.type == 'application/ld+json' && !(contentType || '').match(/^application\/(\w*\+)?json$/)) { doc = await loader(prependBase(url, alternate.target)); } } return doc; } }; function _get(xhr, url, headers) { xhr = xhr || XMLHttpRequest; const req = new xhr(); return new Promise((resolve, reject) => { req.onload = () => resolve(req); req.onerror = err => reject(err); req.open('GET', url, true); for(const k in headers) { req.setRequestHeader(k, headers[k]); } req.send(); }); }