Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
489 changes: 104 additions & 385 deletions lib/constructors/wp-request.js

Large diffs are not rendered by default.

4 changes: 1 addition & 3 deletions lib/endpoint-factories.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,7 @@ function generateEndpointFactories( routesByNamespace ) {
// "handler" object is now fully prepared; create the factory method that
// will instantiate and return a handler instance
handlers[ resource ] = function( options ) {
options = options || {};
options = extend( options, this._options );
return new EndpointRequest( options );
return new EndpointRequest( extend( {}, this._options, options ) );
};

// Expose the constructor as a property on the factory function, so that
Expand Down
323 changes: 323 additions & 0 deletions lib/http-transport.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,323 @@
'use strict';
/**
* @module http-transport
*/

/*jshint -W079 */// Suppress warning about redefiniton of `Promise`
var Promise = require( 'es6-promise' ).Promise;

var _reduce = require( 'lodash.reduce' );
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' );

/**
* 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.
*
* @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 );
}

/**
* 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)
*
* @param result {Object} The response object from the HTTP request
* @param endpoint {String} The base URL of the requested API endpoint
* @returns {Object} The body of the HTTP request, conditionally augmented with
* pagination metadata
*/
function paginateResponse( result, endpoint, httpTransport ) {
if ( ! result.headers || ! result.headers[ 'x-wp-totalpages' ] ) {
// No headers: return as-is
return result;
}

var totalPages = result.headers[ 'x-wp-totalpages' ];

if ( ! totalPages || totalPages === '0' ) {
// No paging: return as-is
return result;
}

// Decode the link header object
var links = result.headers.link ? parseLinkHeader( result.headers.link ) : {};

// Store pagination data from response headers on the response collection
result.body._paging = {
total: result.headers[ 'x-wp-total' ],
totalPages: totalPages,
links: links
};

// Create a WPRequest instance pre-bound to the "next" page, if available
if ( links.next ) {
result.body._paging.next = new WPRequest({
transport: httpTransport,
endpoint: mergeUrl( endpoint, links.next )
});
}

// Create a WPRequest instance pre-bound to the "prev" page, if available
if ( links.prev ) {
result.body._paging.prev = new WPRequest({
transport: httpTransport,
endpoint: mergeUrl( endpoint, links.prev )
});
}

return result;
}

// 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.
*
* @param {Object} request A superagent request object
* @param {Function} callback A callback function (optional)
* @param {Function} transform A function to transform the result data
* @return {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 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.
*
* @method returnBody
* @private
* @param {WPRequest} wpquery The WPRequest representing the returned HTTP response
* @param result {Object} The results from the HTTP request
* @return {Object} The "body" property of the result, conditionally augmented with
* pagination information if the result is a partial collection.
*/
function returnBody( wpquery, result ) {
var endpoint = wpquery._options.endpoint;
var httpTransport = wpquery.transport;
return paginateResponse( result, endpoint, httpTransport ).body;
}

/**
* Extract and return the headers property from a superagent response object
*
* @param {Object} result The results from the HTTP request
* @return {Object} The "headers" property of the result
*/
function returnHeaders( result ) {
return result.headers;
}

// HTTP Methods: Private HTTP-verb versions
// ========================================

/**
* @method _httpGet
* @async
* @private
* @param {WPRequest} wpquery A WPRequest query object
* @param {Function} [callback] A callback to invoke with the results of the GET request
* @return {Promise} A promise to the results of the HTTP request
*/
function _httpGet( wpquery, callback ) {
checkMethodSupport( 'get', wpquery );
var url = wpquery.toString();

var request = _auth( agent.get( url ), wpquery._options );

return invokeAndPromisify( request, callback, returnBody.bind( null, wpquery ) );
}

/**
* Invoke an HTTP "POST" request against the provided endpoint
* @method _httpPost
* @async
* @private
* @param {WPRequest} wpquery 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
* @return {Promise} A promise to the results of the HTTP request
*/
function _httpPost( wpquery, data, callback ) {
checkMethodSupport( 'post', wpquery );
var url = wpquery.toString();
data = data || {};
var request = _auth( agent.post( url ), wpquery._options, true );

if ( wpquery._attachment ) {
// Data must be form-encoded alongside image attachment
request = _reduce( data, function( req, value, key ) {
return req.field( key, value );
}, request.attach( 'file', wpquery._attachment, wpquery._attachmentName ) );
} else {
request = request.send( data );
}

return invokeAndPromisify( request, callback, returnBody.bind( null, wpquery ) );
}

/**
* @method _httpPut
* @async
* @private
* @param {WPRequest} wpquery 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
* @return {Promise} A promise to the results of the HTTP request
*/
function _httpPut( wpquery, data, callback ) {
checkMethodSupport( 'put', wpquery );
var url = wpquery.toString();
data = data || {};

var request = _auth( agent.put( url ), wpquery._options, true ).send( data );

return invokeAndPromisify( request, callback, returnBody.bind( null, wpquery ) );
}

/**
* @method _httpDelete
* @async
* @private
* @param {WPRequest} wpquery 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
* @return {Promise} A promise to the results of the HTTP request
*/
function _httpDelete( wpquery, data, callback ) {
if ( ! callback && typeof data === 'function' ) {
callback = data;
data = null;
}
checkMethodSupport( 'delete', wpquery );
var url = wpquery.toString();
var request = _auth( agent.del( url ), wpquery._options, true ).send( data );

return invokeAndPromisify( request, callback, returnBody.bind( null, wpquery ) );
}

/**
* @method _httpHead
* @async
* @private
* @param {WPRequest} wpquery A WPRequest query object
* @param {Function} [callback] A callback to invoke with the results of the HEAD request
* @return {Promise} A promise to the header results of the HTTP request
*/
function _httpHead( wpquery, callback ) {
checkMethodSupport( 'head', wpquery );
var url = wpquery.toString();
var request = _auth( agent.head( url ), wpquery._options );

return invokeAndPromisify( request, callback, returnHeaders );
}

module.exports = {
delete: _httpDelete,
get: _httpGet,
head: _httpHead,
post: _httpPost,
put: _httpPut
};
19 changes: 19 additions & 0 deletions lib/util/check-method-support.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
'use strict';

/**
* Verify that a specific HTTP method is supported by the provided WPRequest
*
* @param {String} method An HTTP method to check ('get', 'post', etc)
* @param {WPRequest} request A WPRequest object with a _supportedMethods array
* @return true iff the method is within request._supportedMethods
*/
module.exports = function( method, request ) {
if ( request._supportedMethods.indexOf( method.toLowerCase() ) === -1 ) {
throw new Error(
'Unsupported method; supported methods are: ' +
request._supportedMethods.join( ', ' )
);
}

return true;
};
Loading