/**
* @module http-transport
*/
'use strict';
/*jshint -W079 */// Suppress warning about redefiniton of `Promise`
var Promise = require( 'es6-promise' ).Promise;
var agent = require( 'superagent' );
var parseLinkHeader = require( 'li' ).parse;
var url = require( 'url' );
var WPRequest = require( './constructors/wp-request' );
var checkMethodSupport = require( './util/check-method-support' );
var extend = require( 'node.extend' );
var objectReduce = require( './util/object-reduce' );
var isEmptyObject = require( './util/is-empty-object' );
/**
* Set any provided headers on the outgoing request object. Runs after _auth.
*
* @method _setHeaders
* @private
* @param {Object} request A superagent request object
* @param {Object} options A WPRequest _options object
* @param {Object} A superagent request object, with any available headers set
*/
function _setHeaders( request, options ) {
// If there's no headers, do nothing
if ( ! options.headers ) {
return request;
}
return objectReduce( options.headers, function( request, value, key ) {
return request.set( key, value );
}, request );
}
/**
* Conditionally set basic authentication on a server request object.
*
* @method _auth
* @private
* @param {Object} request A superagent request object
* @param {Object} options A WPRequest _options object
* @param {Boolean} forceAuthentication whether to force authentication on the request
* @param {Object} A superagent request object, conditionally configured to use basic auth
*/
function _auth( request, options, forceAuthentication ) {
// If we're not supposed to authenticate, don't even start
if ( ! forceAuthentication && ! options.auth && ! options.nonce ) {
return request;
}
// Enable nonce in options for Cookie authentication http://wp-api.org/guides/authentication.html
if ( options.nonce ) {
request.set( 'X-WP-Nonce', options.nonce );
return request;
}
// Retrieve the username & password from the request options if they weren't provided
var username = username || options.username;
var password = password || options.password;
// If no username or no password, can't authenticate
if ( ! username || ! password ) {
return request;
}
// Can authenticate: set basic auth parameters on the request
return request.auth( username, password );
}
// Pagination-Related Helpers
// ==========================
/**
* Combine the API endpoint root URI and link URI into a valid request URL.
* Endpoints are generally a full path to the JSON API's root endpoint, such
* as `website.com/wp-json`: the link headers, however, are returned as root-
* relative paths. Concatenating these would generate a URL such as
* `website.com/wp-json/wp-json/posts?page=2`: we must intelligently merge the
* URI strings in order to generate a valid new request URL.
*
* @private
* @param endpoint {String} The endpoint URL for the REST API root
* @param linkPath {String} A root-relative link path to an API request
* @returns {String} The full URL path to the provided link
*/
function mergeUrl( endpoint, linkPath ) {
var request = url.parse( endpoint );
linkPath = url.parse( linkPath, true );
// Overwrite relevant request URL object properties with the link's values:
// Setting these three values from the link will ensure proper URL generation
request.query = linkPath.query;
request.search = linkPath.search;
request.pathname = linkPath.pathname;
// Reassemble and return the merged URL
return url.format( request );
}
/**
* Extract the body property from the superagent response, or else try to parse
* the response text to get a JSON object.
*
* @private
* @param {Object} response The response object from the HTTP request
* @param {String} response.text The response content as text
* @param {Object} response.body The response content as a JS object
* @returns {Object} The response content as a JS object
*/
function extractResponseBody( response ) {
var responseBody = response.body;
if ( isEmptyObject( responseBody ) && response.type === 'text/html' ) {
// Response may have come back as HTML due to caching plugin; try to parse
// the response text into JSON
try {
responseBody = JSON.parse( response.text );
} catch ( e ) {
// Swallow errors, it's OK to fall back to returning the body
}
}
return responseBody;
}
/**
* If the response is not paged, return the body as-is. If pagination
* information is present in the response headers, parse those headers into
* a custom `_paging` property on the response body. `_paging` contains links
* to the previous and next pages in the collection, as well as metadata
* about the size and number of pages in the collection.
*
* The structure of the `_paging` property is as follows:
*
* - `total` {Integer} The total number of records in the collection
* - `totalPages` {Integer} The number of pages available
* - `links` {Object} The parsed "links" headers, separated into individual URI strings
* - `next` {WPRequest} A WPRequest object bound to the "next" page (if page exists)
* - `prev` {WPRequest} A WPRequest object bound to the "previous" page (if page exists)
*
* @private
* @param {Object} result The response object from the HTTP request
* @param {Object} options The options hash from the original request
* @param {String} options.endpoint The base URL of the requested API endpoint
* @param {Object} httpTransport The HTTP transport object used by the original request
* @returns {Object} The pagination metadata object for this HTTP request, or else null
*/
function createPaginationObject( result, options, httpTransport ) {
var _paging = null;
if ( ! result.headers || ! result.headers[ 'x-wp-totalpages' ] ) {
// No headers: return as-is
return _paging;
}
var totalPages = result.headers[ 'x-wp-totalpages' ];
if ( ! totalPages || totalPages === '0' ) {
// No paging: return as-is
return _paging;
}
// Decode the link header object
var links = result.headers.link ? parseLinkHeader( result.headers.link ) : {};
// Store pagination data from response headers on the response collection
_paging = {
total: result.headers[ 'x-wp-total' ],
totalPages: totalPages,
links: links
};
// Re-use any options from the original request, updating only the endpoint
// (this ensures that request properties like authentication are preserved)
var endpoint = options.endpoint;
// Create a WPRequest instance pre-bound to the "next" page, if available
if ( links.next ) {
_paging.next = new WPRequest( extend( {}, options, {
transport: httpTransport,
endpoint: mergeUrl( endpoint, links.next )
}));
}
// Create a WPRequest instance pre-bound to the "prev" page, if available
if ( links.prev ) {
_paging.prev = new WPRequest( extend( {}, options, {
transport: httpTransport,
endpoint: mergeUrl( endpoint, links.prev )
}));
}
return _paging;
}
// HTTP-Related Helpers
// ====================
/**
* Submit the provided superagent request object, invoke a callback (if it was
* provided), and return a promise to the response from the HTTP request.
*
* @private
* @param {Object} request A superagent request object
* @param {Function} callback A callback function (optional)
* @param {Function} transform A function to transform the result data
* @returns {Promise} A promise to the superagent request
*/
function invokeAndPromisify( request, callback, transform ) {
return new Promise(function( resolve, reject ) {
// Fire off the result
request.end(function( err, result ) {
// Return the results as a promise
if ( err || result.error ) {
reject( err || result.error );
} else {
resolve( result );
}
});
}).then( transform ).then(function( result ) {
// If a node-style callback was provided, call it, but also return the
// result value for use via the returned Promise
if ( callback && typeof callback === 'function' ) {
callback( null, result );
}
return result;
}, function( err ) {
// If the API provided an error object, it will be available within the
// superagent response object as response.body (containing the response
// JSON). If that object exists, it will have a .code property if it is
// truly an API error (non-API errors will not have a .code).
if ( err.response && err.response.body && err.response.body.code ) {
// Forward API error response JSON on to the calling method: omit
// all transport-specific (superagent-specific) properties
err = err.response.body;
}
// If a callback was provided, ensure it is called with the error; otherwise
// re-throw the error so that it can be handled by a Promise .catch or .then
if ( callback && typeof callback === 'function' ) {
callback( err );
} else {
throw err;
}
});
}
/**
* Return the body of the request, augmented with pagination information if the
* result is a paged collection.
*
* @private
* @param {WPRequest} wpreq The WPRequest representing the returned HTTP response
* @param {Object} result The results from the HTTP request
* @returns {Object} The "body" property of the result, conditionally augmented with
* pagination information if the result is a partial collection.
*/
function returnBody( wpreq, result ) {
var body = extractResponseBody( result );
var _paging = createPaginationObject( result, wpreq._options, wpreq.transport );
if ( _paging ) {
body._paging = _paging;
}
return body;
}
/**
* Extract and return the headers property from a superagent response object
*
* @private
* @param {Object} result The results from the HTTP request
* @returns {Object} The "headers" property of the result
*/
function returnHeaders( result ) {
return result.headers;
}
// HTTP Methods: Private HTTP-verb versions
// ========================================
/**
* @method get
* @async
* @param {WPRequest} wpreq A WPRequest query object
* @param {Function} [callback] A callback to invoke with the results of the GET request
* @returns {Promise} A promise to the results of the HTTP request
*/
function _httpGet( wpreq, callback ) {
checkMethodSupport( 'get', wpreq );
var url = wpreq.toString();
var request = _auth( agent.get( url ), wpreq._options );
request = _setHeaders( request, wpreq._options );
return invokeAndPromisify( request, callback, returnBody.bind( null, wpreq ) );
}
/**
* Invoke an HTTP "POST" request against the provided endpoint
* @method post
* @async
* @param {WPRequest} wpreq A WPRequest query object
* @param {Object} data The data for the POST request
* @param {Function} [callback] A callback to invoke with the results of the POST request
* @returns {Promise} A promise to the results of the HTTP request
*/
function _httpPost( wpreq, data, callback ) {
checkMethodSupport( 'post', wpreq );
var url = wpreq.toString();
data = data || {};
var request = _auth( agent.post( url ), wpreq._options, true );
request = _setHeaders( request, wpreq._options );
if ( wpreq._attachment ) {
// Data must be form-encoded alongside image attachment
request = objectReduce( data, function( req, value, key ) {
return req.field( key, value );
}, request.attach( 'file', wpreq._attachment, wpreq._attachmentName ) );
} else {
request = request.send( data );
}
return invokeAndPromisify( request, callback, returnBody.bind( null, wpreq ) );
}
/**
* @method put
* @async
* @param {WPRequest} wpreq A WPRequest query object
* @param {Object} data The data for the PUT request
* @param {Function} [callback] A callback to invoke with the results of the PUT request
* @returns {Promise} A promise to the results of the HTTP request
*/
function _httpPut( wpreq, data, callback ) {
checkMethodSupport( 'put', wpreq );
var url = wpreq.toString();
data = data || {};
var request = _auth( agent.put( url ), wpreq._options, true ).send( data );
request = _setHeaders( request, wpreq._options );
return invokeAndPromisify( request, callback, returnBody.bind( null, wpreq ) );
}
/**
* @method delete
* @async
* @param {WPRequest} wpreq A WPRequest query object
* @param {Object} [data] Data to send along with the DELETE request
* @param {Function} [callback] A callback to invoke with the results of the DELETE request
* @returns {Promise} A promise to the results of the HTTP request
*/
function _httpDelete( wpreq, data, callback ) {
if ( ! callback && typeof data === 'function' ) {
callback = data;
data = null;
}
checkMethodSupport( 'delete', wpreq );
var url = wpreq.toString();
var request = _auth( agent.del( url ), wpreq._options, true ).send( data );
request = _setHeaders( request, wpreq._options );
return invokeAndPromisify( request, callback, returnBody.bind( null, wpreq ) );
}
/**
* @method head
* @async
* @param {WPRequest} wpreq A WPRequest query object
* @param {Function} [callback] A callback to invoke with the results of the HEAD request
* @returns {Promise} A promise to the header results of the HTTP request
*/
function _httpHead( wpreq, callback ) {
checkMethodSupport( 'head', wpreq );
var url = wpreq.toString();
var request = _auth( agent.head( url ), wpreq._options );
request = _setHeaders( request, wpreq._options );
return invokeAndPromisify( request, callback, returnHeaders );
}
module.exports = {
delete: _httpDelete,
get: _httpGet,
head: _httpHead,
post: _httpPost,
put: _httpPut
};