lib/http-transport.js

  1. /**
  2. * @module http-transport
  3. */
  4. 'use strict';
  5. /*jshint -W079 */// Suppress warning about redefiniton of `Promise`
  6. var Promise = require( 'es6-promise' ).Promise;
  7. var agent = require( 'superagent' );
  8. var parseLinkHeader = require( 'li' ).parse;
  9. var url = require( 'url' );
  10. var WPRequest = require( './constructors/wp-request' );
  11. var checkMethodSupport = require( './util/check-method-support' );
  12. var extend = require( 'node.extend' );
  13. var objectReduce = require( './util/object-reduce' );
  14. var isEmptyObject = require( './util/is-empty-object' );
  15. /**
  16. * Set any provided headers on the outgoing request object. Runs after _auth.
  17. *
  18. * @method _setHeaders
  19. * @private
  20. * @param {Object} request A superagent request object
  21. * @param {Object} options A WPRequest _options object
  22. * @param {Object} A superagent request object, with any available headers set
  23. */
  24. function _setHeaders( request, options ) {
  25. // If there's no headers, do nothing
  26. if ( ! options.headers ) {
  27. return request;
  28. }
  29. return objectReduce( options.headers, function( request, value, key ) {
  30. return request.set( key, value );
  31. }, request );
  32. }
  33. /**
  34. * Conditionally set basic authentication on a server request object.
  35. *
  36. * @method _auth
  37. * @private
  38. * @param {Object} request A superagent request object
  39. * @param {Object} options A WPRequest _options object
  40. * @param {Boolean} forceAuthentication whether to force authentication on the request
  41. * @param {Object} A superagent request object, conditionally configured to use basic auth
  42. */
  43. function _auth( request, options, forceAuthentication ) {
  44. // If we're not supposed to authenticate, don't even start
  45. if ( ! forceAuthentication && ! options.auth && ! options.nonce ) {
  46. return request;
  47. }
  48. // Enable nonce in options for Cookie authentication http://wp-api.org/guides/authentication.html
  49. if ( options.nonce ) {
  50. request.set( 'X-WP-Nonce', options.nonce );
  51. return request;
  52. }
  53. // Retrieve the username & password from the request options if they weren't provided
  54. var username = username || options.username;
  55. var password = password || options.password;
  56. // If no username or no password, can't authenticate
  57. if ( ! username || ! password ) {
  58. return request;
  59. }
  60. // Can authenticate: set basic auth parameters on the request
  61. return request.auth( username, password );
  62. }
  63. // Pagination-Related Helpers
  64. // ==========================
  65. /**
  66. * Combine the API endpoint root URI and link URI into a valid request URL.
  67. * Endpoints are generally a full path to the JSON API's root endpoint, such
  68. * as `website.com/wp-json`: the link headers, however, are returned as root-
  69. * relative paths. Concatenating these would generate a URL such as
  70. * `website.com/wp-json/wp-json/posts?page=2`: we must intelligently merge the
  71. * URI strings in order to generate a valid new request URL.
  72. *
  73. * @private
  74. * @param endpoint {String} The endpoint URL for the REST API root
  75. * @param linkPath {String} A root-relative link path to an API request
  76. * @returns {String} The full URL path to the provided link
  77. */
  78. function mergeUrl( endpoint, linkPath ) {
  79. var request = url.parse( endpoint );
  80. linkPath = url.parse( linkPath, true );
  81. // Overwrite relevant request URL object properties with the link's values:
  82. // Setting these three values from the link will ensure proper URL generation
  83. request.query = linkPath.query;
  84. request.search = linkPath.search;
  85. request.pathname = linkPath.pathname;
  86. // Reassemble and return the merged URL
  87. return url.format( request );
  88. }
  89. /**
  90. * Extract the body property from the superagent response, or else try to parse
  91. * the response text to get a JSON object.
  92. *
  93. * @private
  94. * @param {Object} response The response object from the HTTP request
  95. * @param {String} response.text The response content as text
  96. * @param {Object} response.body The response content as a JS object
  97. * @returns {Object} The response content as a JS object
  98. */
  99. function extractResponseBody( response ) {
  100. var responseBody = response.body;
  101. if ( isEmptyObject( responseBody ) && response.type === 'text/html' ) {
  102. // Response may have come back as HTML due to caching plugin; try to parse
  103. // the response text into JSON
  104. try {
  105. responseBody = JSON.parse( response.text );
  106. } catch ( e ) {
  107. // Swallow errors, it's OK to fall back to returning the body
  108. }
  109. }
  110. return responseBody;
  111. }
  112. /**
  113. * If the response is not paged, return the body as-is. If pagination
  114. * information is present in the response headers, parse those headers into
  115. * a custom `_paging` property on the response body. `_paging` contains links
  116. * to the previous and next pages in the collection, as well as metadata
  117. * about the size and number of pages in the collection.
  118. *
  119. * The structure of the `_paging` property is as follows:
  120. *
  121. * - `total` {Integer} The total number of records in the collection
  122. * - `totalPages` {Integer} The number of pages available
  123. * - `links` {Object} The parsed "links" headers, separated into individual URI strings
  124. * - `next` {WPRequest} A WPRequest object bound to the "next" page (if page exists)
  125. * - `prev` {WPRequest} A WPRequest object bound to the "previous" page (if page exists)
  126. *
  127. * @private
  128. * @param {Object} result The response object from the HTTP request
  129. * @param {Object} options The options hash from the original request
  130. * @param {String} options.endpoint The base URL of the requested API endpoint
  131. * @param {Object} httpTransport The HTTP transport object used by the original request
  132. * @returns {Object} The pagination metadata object for this HTTP request, or else null
  133. */
  134. function createPaginationObject( result, options, httpTransport ) {
  135. var _paging = null;
  136. if ( ! result.headers || ! result.headers[ 'x-wp-totalpages' ] ) {
  137. // No headers: return as-is
  138. return _paging;
  139. }
  140. var totalPages = result.headers[ 'x-wp-totalpages' ];
  141. if ( ! totalPages || totalPages === '0' ) {
  142. // No paging: return as-is
  143. return _paging;
  144. }
  145. // Decode the link header object
  146. var links = result.headers.link ? parseLinkHeader( result.headers.link ) : {};
  147. // Store pagination data from response headers on the response collection
  148. _paging = {
  149. total: result.headers[ 'x-wp-total' ],
  150. totalPages: totalPages,
  151. links: links
  152. };
  153. // Re-use any options from the original request, updating only the endpoint
  154. // (this ensures that request properties like authentication are preserved)
  155. var endpoint = options.endpoint;
  156. // Create a WPRequest instance pre-bound to the "next" page, if available
  157. if ( links.next ) {
  158. _paging.next = new WPRequest( extend( {}, options, {
  159. transport: httpTransport,
  160. endpoint: mergeUrl( endpoint, links.next )
  161. }));
  162. }
  163. // Create a WPRequest instance pre-bound to the "prev" page, if available
  164. if ( links.prev ) {
  165. _paging.prev = new WPRequest( extend( {}, options, {
  166. transport: httpTransport,
  167. endpoint: mergeUrl( endpoint, links.prev )
  168. }));
  169. }
  170. return _paging;
  171. }
  172. // HTTP-Related Helpers
  173. // ====================
  174. /**
  175. * Submit the provided superagent request object, invoke a callback (if it was
  176. * provided), and return a promise to the response from the HTTP request.
  177. *
  178. * @private
  179. * @param {Object} request A superagent request object
  180. * @param {Function} callback A callback function (optional)
  181. * @param {Function} transform A function to transform the result data
  182. * @returns {Promise} A promise to the superagent request
  183. */
  184. function invokeAndPromisify( request, callback, transform ) {
  185. return new Promise(function( resolve, reject ) {
  186. // Fire off the result
  187. request.end(function( err, result ) {
  188. // Return the results as a promise
  189. if ( err || result.error ) {
  190. reject( err || result.error );
  191. } else {
  192. resolve( result );
  193. }
  194. });
  195. }).then( transform ).then(function( result ) {
  196. // If a node-style callback was provided, call it, but also return the
  197. // result value for use via the returned Promise
  198. if ( callback && typeof callback === 'function' ) {
  199. callback( null, result );
  200. }
  201. return result;
  202. }, function( err ) {
  203. // If the API provided an error object, it will be available within the
  204. // superagent response object as response.body (containing the response
  205. // JSON). If that object exists, it will have a .code property if it is
  206. // truly an API error (non-API errors will not have a .code).
  207. if ( err.response && err.response.body && err.response.body.code ) {
  208. // Forward API error response JSON on to the calling method: omit
  209. // all transport-specific (superagent-specific) properties
  210. err = err.response.body;
  211. }
  212. // If a callback was provided, ensure it is called with the error; otherwise
  213. // re-throw the error so that it can be handled by a Promise .catch or .then
  214. if ( callback && typeof callback === 'function' ) {
  215. callback( err );
  216. } else {
  217. throw err;
  218. }
  219. });
  220. }
  221. /**
  222. * Return the body of the request, augmented with pagination information if the
  223. * result is a paged collection.
  224. *
  225. * @private
  226. * @param {WPRequest} wpreq The WPRequest representing the returned HTTP response
  227. * @param {Object} result The results from the HTTP request
  228. * @returns {Object} The "body" property of the result, conditionally augmented with
  229. * pagination information if the result is a partial collection.
  230. */
  231. function returnBody( wpreq, result ) {
  232. var body = extractResponseBody( result );
  233. var _paging = createPaginationObject( result, wpreq._options, wpreq.transport );
  234. if ( _paging ) {
  235. body._paging = _paging;
  236. }
  237. return body;
  238. }
  239. /**
  240. * Extract and return the headers property from a superagent response object
  241. *
  242. * @private
  243. * @param {Object} result The results from the HTTP request
  244. * @returns {Object} The "headers" property of the result
  245. */
  246. function returnHeaders( result ) {
  247. return result.headers;
  248. }
  249. // HTTP Methods: Private HTTP-verb versions
  250. // ========================================
  251. /**
  252. * @method get
  253. * @async
  254. * @param {WPRequest} wpreq A WPRequest query object
  255. * @param {Function} [callback] A callback to invoke with the results of the GET request
  256. * @returns {Promise} A promise to the results of the HTTP request
  257. */
  258. function _httpGet( wpreq, callback ) {
  259. checkMethodSupport( 'get', wpreq );
  260. var url = wpreq.toString();
  261. var request = _auth( agent.get( url ), wpreq._options );
  262. request = _setHeaders( request, wpreq._options );
  263. return invokeAndPromisify( request, callback, returnBody.bind( null, wpreq ) );
  264. }
  265. /**
  266. * Invoke an HTTP "POST" request against the provided endpoint
  267. * @method post
  268. * @async
  269. * @param {WPRequest} wpreq A WPRequest query object
  270. * @param {Object} data The data for the POST request
  271. * @param {Function} [callback] A callback to invoke with the results of the POST request
  272. * @returns {Promise} A promise to the results of the HTTP request
  273. */
  274. function _httpPost( wpreq, data, callback ) {
  275. checkMethodSupport( 'post', wpreq );
  276. var url = wpreq.toString();
  277. data = data || {};
  278. var request = _auth( agent.post( url ), wpreq._options, true );
  279. request = _setHeaders( request, wpreq._options );
  280. if ( wpreq._attachment ) {
  281. // Data must be form-encoded alongside image attachment
  282. request = objectReduce( data, function( req, value, key ) {
  283. return req.field( key, value );
  284. }, request.attach( 'file', wpreq._attachment, wpreq._attachmentName ) );
  285. } else {
  286. request = request.send( data );
  287. }
  288. return invokeAndPromisify( request, callback, returnBody.bind( null, wpreq ) );
  289. }
  290. /**
  291. * @method put
  292. * @async
  293. * @param {WPRequest} wpreq A WPRequest query object
  294. * @param {Object} data The data for the PUT request
  295. * @param {Function} [callback] A callback to invoke with the results of the PUT request
  296. * @returns {Promise} A promise to the results of the HTTP request
  297. */
  298. function _httpPut( wpreq, data, callback ) {
  299. checkMethodSupport( 'put', wpreq );
  300. var url = wpreq.toString();
  301. data = data || {};
  302. var request = _auth( agent.put( url ), wpreq._options, true ).send( data );
  303. request = _setHeaders( request, wpreq._options );
  304. return invokeAndPromisify( request, callback, returnBody.bind( null, wpreq ) );
  305. }
  306. /**
  307. * @method delete
  308. * @async
  309. * @param {WPRequest} wpreq A WPRequest query object
  310. * @param {Object} [data] Data to send along with the DELETE request
  311. * @param {Function} [callback] A callback to invoke with the results of the DELETE request
  312. * @returns {Promise} A promise to the results of the HTTP request
  313. */
  314. function _httpDelete( wpreq, data, callback ) {
  315. if ( ! callback && typeof data === 'function' ) {
  316. callback = data;
  317. data = null;
  318. }
  319. checkMethodSupport( 'delete', wpreq );
  320. var url = wpreq.toString();
  321. var request = _auth( agent.del( url ), wpreq._options, true ).send( data );
  322. request = _setHeaders( request, wpreq._options );
  323. return invokeAndPromisify( request, callback, returnBody.bind( null, wpreq ) );
  324. }
  325. /**
  326. * @method head
  327. * @async
  328. * @param {WPRequest} wpreq A WPRequest query object
  329. * @param {Function} [callback] A callback to invoke with the results of the HEAD request
  330. * @returns {Promise} A promise to the header results of the HTTP request
  331. */
  332. function _httpHead( wpreq, callback ) {
  333. checkMethodSupport( 'head', wpreq );
  334. var url = wpreq.toString();
  335. var request = _auth( agent.head( url ), wpreq._options );
  336. request = _setHeaders( request, wpreq._options );
  337. return invokeAndPromisify( request, callback, returnHeaders );
  338. }
  339. module.exports = {
  340. delete: _httpDelete,
  341. get: _httpGet,
  342. head: _httpHead,
  343. post: _httpPost,
  344. put: _httpPut
  345. };