diff --git a/README.md b/README.md index 37940209..ce4e4b53 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ This library is an isomorphic client for the [WordPress REST API](http://develop - [Embedding Data](#embedding-data) - [Collection Pagination](#collection-pagination) - [Customizing HTTP Request Behavior](#customizing-http-request-behavior) + - [Specifying HTTP Headers](#specifying-http-headers) - [Authentication](#authentication) - [API Documentation](#api-documentation) - [Issues](#issues) @@ -809,6 +810,35 @@ site.transport({ ``` Note that these transport methods are the internal methods used by `create` and `.update`, so the names of these methods therefore map to the HTTP verbs "get", "post", "put", "head" and "delete"; name your transport methods accordingly or they will not be used. +### Specifying HTTP Headers + +If you need to send additional HTTP headers along with your request (for example to provide a specific `Authorization` header for use with alternative authentication schemes), you can use the `.setHeaders()` method to specify one or more headers to send with the dispatched request: + +#### Set headers for a single request + +```js +// Specify a single header to send with the outgoing request +wp.posts().setHeaders( 'Authorization', 'Bearer xxxxx.yyyyy.zzzzz' )... + +// Specify multiple headers to send with the outgoing request +wp.posts().setHeaders({ + Authorization: 'Bearer xxxxx.yyyyy.zzzzz', + 'Accept-Language': 'pt-BR' +})... +``` + +#### Set headers globally + +You can also set headers globally on the WPAPI instance itself, which will then be used for all subsequent requests created from that site instance: + +```js +// Specify a header to be used by all subsequent requests +wp.setHeaders( 'Authorization', 'Bearer xxxxx.yyyyy.zzzzz' ); + +// These will now be sent with an Authorization header +wp.users().me()... +wp.posts().id( unpublishedPostId )... +``` ## Authentication diff --git a/lib/constructors/wp-request.js b/lib/constructors/wp-request.js index b5cf8327..916a6d20 100644 --- a/lib/constructors/wp-request.js +++ b/lib/constructors/wp-request.js @@ -38,6 +38,7 @@ function WPRequest( options ) { // Whitelisted options keys 'auth', 'endpoint', + 'headers', 'username', 'password', 'nonce' @@ -652,6 +653,39 @@ WPRequest.prototype.file = function( file, name ) { // HTTP Methods: Public Interface // ============================== +/** + * Specify one or more headers to send with the dispatched HTTP request. + * + * @example Set a single header to be used on this request + * + * request.setHeaders( 'Authorization', 'Bearer trustme' )... + * + * @example Set multiple headers to be used by this request + * + * request.setHeaders({ + * Authorization: 'Bearer comeonwereoldfriendsright', + * 'Accept-Language': 'en-CA' + * })... + * + * @method setHeaders + * @chainable + * @param {String|Object} headers The name of the header to set, or an object of + * header names and their associated string values + * @param {String} [value] The value of the header being set + * @return {WPRequest} The WPRequest instance (for chaining) + */ +WPRequest.prototype.setHeaders = function( headers, value ) { + // We can use the same iterator function below to handle explicit key-value + // pairs if we convert them into to an object we can iterate over: + if ( typeof headers === 'string' ) { + headers = keyValToObj( headers, value ); + } + + this._options.headers = Object.assign( {}, this._options.headers || {}, headers ); + + return this; +}; + /** * Get (download the data for) the specified resource * diff --git a/lib/http-transport.js b/lib/http-transport.js index 1db062b4..834df037 100644 --- a/lib/http-transport.js +++ b/lib/http-transport.js @@ -16,7 +16,27 @@ var objectReduce = require( './util/object-reduce' ); var isEmptyObject = require( './util/is-empty-object' ); /** - * Conditionally set basic authentication on a server request 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 @@ -265,6 +285,7 @@ function _httpGet( wpreq, callback ) { 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 ) ); } @@ -284,6 +305,7 @@ function _httpPost( wpreq, data, callback ) { 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 @@ -312,6 +334,7 @@ function _httpPut( wpreq, data, callback ) { 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 ) ); } @@ -333,6 +356,7 @@ function _httpDelete( wpreq, data, callback ) { 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 ) ); } @@ -349,6 +373,7 @@ 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 ); } diff --git a/tests/integration/custom-http-headers.js b/tests/integration/custom-http-headers.js new file mode 100644 index 00000000..34509b58 --- /dev/null +++ b/tests/integration/custom-http-headers.js @@ -0,0 +1,65 @@ +'use strict'; +var chai = require( 'chai' ); +// Variable to use as our "success token" in promise assertions +var SUCCESS = 'success'; +// Chai-as-promised and the `expect( prom ).to.eventually.equal( SUCCESS ) is +// used to ensure that the assertions running within the promise chains are +// actually run. +chai.use( require( 'chai-as-promised' ) ); +var expect = chai.expect; + +var WPAPI = require( '../../' ); + +// Inspecting the titles of the returned posts arrays is an easy way to +// validate that the right page of results was returned +var getTitles = require( './helpers/get-rendered-prop' ).bind( null, 'title' ); +var base64credentials = new Buffer( 'apiuser:password' ).toString( 'base64' ); + +describe( 'integration: custom HTTP Headers', function() { + var wp; + + beforeEach(function() { + wp = new WPAPI({ + endpoint: 'http://wpapi.loc/wp-json' + }); + }); + + // Testing basic authentication is an acceptable proxy for whether a header + // value (Authentication:, in this case) is being set + it( 'can be provided using WPRequest#setHeaders()', function() { + var prom = wp.posts() + .setHeaders( 'Authorization', 'Basic ' + base64credentials ) + .status([ 'future', 'draft' ]) + .get() + .then(function( posts ) { + expect( getTitles( posts ) ).to.deep.equal([ + 'Scheduled', + 'Draft' + ]); + return SUCCESS; + }); + return expect( prom ).to.eventually.equal( SUCCESS ); + }); + + it( 'can be provided at the WPAPI instance level using WPAPI#setHeaders()', function() { + var authenticated = WPAPI + .site( 'http://wpapi.loc/wp-json' ) + .setHeaders( 'Authorization', 'Basic ' + base64credentials ); + var prom = authenticated.posts() + .status([ 'future', 'draft' ]) + .get() + .then(function( posts ) { + expect( getTitles( posts ) ).to.deep.equal([ + 'Scheduled', + 'Draft' + ]); + return authenticated.users().me(); + }) + .then(function( me ) { + expect( me.slug ).to.equal( 'apiuser' ); + return SUCCESS; + }); + return expect( prom ).to.eventually.equal( SUCCESS ); + }); + +}); diff --git a/tests/unit/lib/constructors/wp-request.js b/tests/unit/lib/constructors/wp-request.js index 272dcc75..58c34870 100644 --- a/tests/unit/lib/constructors/wp-request.js +++ b/tests/unit/lib/constructors/wp-request.js @@ -671,6 +671,85 @@ describe( 'WPRequest', function() { }); + describe( '.setHeaders()', function() { + + it( 'method exists', function() { + expect( request ).to.have.property( 'setHeaders' ); + expect( request.setHeaders ).to.be.a( 'function' ); + }); + + it( 'will have no effect if called without any arguments', function() { + request.setHeaders(); + expect( request._options.headers ).to.deep.equal({}); + }); + + it( 'will set a header key/value pair', function() { + request.setHeaders( 'Authorization', 'Bearer sometoken' ); + expect( request._options.headers ).to.deep.equal({ + Authorization: 'Bearer sometoken' + }); + }); + + it( 'will replace an existing header key/value pair', function() { + request + .setHeaders( 'Authorization', 'Bearer sometoken' ) + .setHeaders( 'Authorization', 'Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==' ); + expect( request._options.headers ).to.deep.equal({ + Authorization: 'Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==' + }); + }); + + it( 'will set multiple header key/value pairs with chained calls', function() { + request + .setHeaders( 'Accept-Language', 'en-US' ) + .setHeaders( 'Authorization', 'Bearer sometoken' ); + expect( request._options.headers ).to.deep.equal({ + 'Accept-Language': 'en-US', + Authorization: 'Bearer sometoken' + }); + }); + + it( 'will set multiple header key/value pairs when passed an object', function() { + request.setHeaders({ + 'Accept-Language': 'en-US', + Authorization: 'Bearer sometoken' + }); + expect( request._options.headers ).to.deep.equal({ + 'Accept-Language': 'en-US', + Authorization: 'Bearer sometoken' + }); + }); + + it( 'will replace multiple existing header key/value pairs when passed an object', function() { + request + .setHeaders({ + 'Accept-Language': 'en-US', + Authorization: 'Bearer sometoken' + }) + .setHeaders({ + 'Accept-Language': 'pt-BR', + Authorization: 'Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==' + }); + expect( request._options.headers ).to.deep.equal({ + 'Accept-Language': 'pt-BR', + Authorization: 'Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==' + }); + }); + + it( 'inherits headers from the constructor options object', function() { + request = new WPRequest({ + endpoint: '/', + headers: { + 'Accept-Language': 'pt-BR' + } + }); + expect( request._options.headers ).to.deep.equal({ + 'Accept-Language': 'pt-BR' + }); + }); + + }); + describe( '.toString()', function() { beforeEach(function() { diff --git a/tests/unit/wpapi.js b/tests/unit/wpapi.js index fb4df45a..a0fa0ac4 100644 --- a/tests/unit/wpapi.js +++ b/tests/unit/wpapi.js @@ -772,6 +772,61 @@ describe( 'WPAPI', function() { }); + describe( '.setHeaders()', function() { + + beforeEach(function() { + site = new WPAPI({ endpoint: 'http://my.site.com/wp-json' }); + }); + + it( 'is defined', function() { + expect( site ).to.have.property( 'setHeaders' ); + expect( site.setHeaders ).to.be.a( 'function' ); + }); + + it( 'initializes site-wide headers object if called with no arguments', function() { + expect( site._options ).not.to.have.property( 'headers' ); + site.setHeaders(); + expect( site._options ).to.have.property( 'headers' ); + expect( site._options.headers ).to.deep.equal({}); + }); + + it( 'sets site-wide headers when provided a name-value pair', function() { + site.setHeaders( 'Accept-Language', 'en-US' ); + expect( site._options ).to.have.property( 'headers' ); + expect( site._options.headers ).to.deep.equal({ + 'Accept-Language': 'en-US' + }); + }); + + it( 'sets site-wide headers when provided an object of header name-value pairs', function() { + site.setHeaders({ + 'Accept-Language': 'en-CA', + Authorization: 'Bearer sometoken' + }); + expect( site._options ).to.have.property( 'headers' ); + expect( site._options.headers ).to.deep.equal({ + 'Accept-Language': 'en-CA', + Authorization: 'Bearer sometoken' + }); + }); + + it( 'passes headers to all subsequently-instantiated handlers', function() { + site.setHeaders({ + 'Accept-Language': 'en-IL', + Authorization: 'Bearer chicagostylepizza' + }); + var req = site.root( '' ); + expect( req ).to.have.property( '_options' ); + expect( req._options ).to.be.an( 'object' ); + expect( req._options ).to.have.property( 'headers' ); + expect( req._options.headers ).to.deep.equal({ + 'Accept-Language': 'en-IL', + Authorization: 'Bearer chicagostylepizza' + }); + }); + + }); + describe( '.registerRoute()', function() { it( 'is a function', function() { diff --git a/wpapi.js b/wpapi.js index 9f9e298a..954e231b 100644 --- a/wpapi.js +++ b/wpapi.js @@ -258,6 +258,31 @@ WPAPI.prototype.root = function( relativePath ) { return request; }; +/** + * Set the default headers to use for all HTTP requests created from this WPAPI + * site instance. Accepts a header name and its associated value as two strings, + * or multiple headers as an object of name-value pairs. + * + * @example Set a single header to be used by all requests to this site + * + * site.setHeaders( 'Authorization', 'Bearer trustme' )... + * + * @example Set multiple headers to be used by all requests to this site + * + * site.setHeaders({ + * Authorization: 'Bearer comeonwereoldfriendsright', + * 'Accept-Language': 'en-CA' + * })... + * + * @method setHeaders + * @chainable + * @param {String|Object} headers The name of the header to set, or an object of + * header names and their associated string values + * @param {String} [value] The value of the header being set + * @return {WPAPI} The WPAPI site handler instance, for chaining + */ +WPAPI.prototype.setHeaders = WPRequest.prototype.setHeaders; + /** * Set the authentication to use for a WPAPI site handler instance. Accepts basic * HTTP authentication credentials (string username & password) or a Nonce (for