359 lines
12 KiB
JavaScript
359 lines
12 KiB
JavaScript
'use strict';
|
|
|
|
var debug = require('debug')('pinterest:Resource');
|
|
var http = require('http');
|
|
var https = require('https');
|
|
var path = require('path');
|
|
var xml2js = require('xml2js');
|
|
var qs = require('qs');
|
|
var iconv = require('iconv-lite');
|
|
|
|
var utils = require('./utils');
|
|
var Error = require('./Error');
|
|
|
|
var hasOwn = {}.hasOwnProperty;
|
|
|
|
var RESPONSE_CONTENT_TYPE = ['text/xml', 'text/xml;charset=utf-8', 'application/xml'];
|
|
// Provide extension mechanism for pinterest Resource Sub-Classes
|
|
pinterestResource.extend = utils.protoExtend;
|
|
|
|
// Expose method-creator & prepared (basic) methods
|
|
pinterestResource.methods = require('./PinterestHttpClient');
|
|
|
|
/**
|
|
* Encapsulates request logic for a pinterest Resource
|
|
*/
|
|
function pinterestResource(pinterest, urlData) {
|
|
this._pinterest = pinterest;
|
|
this._urlData = urlData || {};
|
|
|
|
this.basePath = utils.makeURLInterpolator(pinterest.getApiField('basePath'));
|
|
this.path = utils.makeURLInterpolator(this.path);
|
|
|
|
if (this.includeBasic) {
|
|
this.includeBasic.forEach(function (methodName) {
|
|
this[methodName] = pinterestResource.methods[methodName];
|
|
}, this);
|
|
}
|
|
|
|
this.initialize.apply(this, arguments);
|
|
}
|
|
|
|
pinterestResource.prototype = {
|
|
|
|
path: '',
|
|
requestBody: {},
|
|
requestParamsJSON: {},
|
|
initialize: function () {
|
|
},
|
|
|
|
// Function to override the default data processor. This allows full control
|
|
// over how a pinterestResource's request data will get converted into an HTTP
|
|
// body. This is useful for non-standard HTTP requests. The function should
|
|
// take method name, data, and headers as arguments.
|
|
requestDataProcessor: null,
|
|
|
|
// String that overrides the base API endpoint. If `overrideHost` is not null
|
|
// then all requests for a particular resource will be sent to a base API
|
|
// endpoint as defined by `overrideHost`.
|
|
overrideHost: null,
|
|
|
|
createFullPath: function (commandPath, urlData) {
|
|
return path.join(
|
|
this.basePath(urlData),
|
|
this.path(urlData),
|
|
typeof commandPath === 'function' ?
|
|
commandPath(urlData) : commandPath
|
|
).replace(/\\/g, '/'); // ugly workaround for Windows
|
|
},
|
|
|
|
createUrlData: function () {
|
|
var urlData = {};
|
|
// Merge in baseData
|
|
for (var i in this._urlData) {
|
|
if (hasOwn.call(this._urlData, i)) {
|
|
urlData[i] = this._urlData[i];
|
|
}
|
|
}
|
|
return urlData;
|
|
},
|
|
|
|
wrapTimeout: function (promise, callback) {
|
|
if (callback) {
|
|
// Ensure callback is called outside of promise stack.
|
|
return promise.then(function (res) {
|
|
setTimeout(function () {
|
|
callback(null, res);
|
|
}, 0);
|
|
}, function (err) {
|
|
setTimeout(function () {
|
|
callback(err, null);
|
|
}, 0);
|
|
});
|
|
}
|
|
|
|
return promise;
|
|
},
|
|
|
|
_timeoutHandler: function (timeout, req, callback) {
|
|
var self = this;
|
|
return function () {
|
|
var timeoutErr = new Error('ETIMEDOUT');
|
|
timeoutErr.code = 'ETIMEDOUT';
|
|
|
|
req._isAborted = true;
|
|
req.abort();
|
|
|
|
callback.call(self, new Error.pinterestConnectionError({
|
|
message: 'Request aborted due to timeout being reached (' + timeout + 'ms)'
|
|
}), null);
|
|
};
|
|
},
|
|
|
|
_responseHandler: function (requestParamsJSONCopy, req, userOptions, callback) {
|
|
var self = this;
|
|
|
|
function processResponseType(res, responseString, callback) {
|
|
//debug('res %o ', res);
|
|
//debug('res.headers %o ', res.headers);
|
|
if (RESPONSE_CONTENT_TYPE.indexOf(res.headers['content-type'].toLowerCase()) > -1) {
|
|
debug('It is XML Response');
|
|
var parser = new xml2js.Parser({
|
|
explicitArray: false,
|
|
ignoreAttrs: true
|
|
});
|
|
|
|
parser.parseString(responseString, function (err, response) {
|
|
//debug('response after parsing JSON %o ', response);
|
|
return callback(null, response);
|
|
});
|
|
} else {
|
|
debug('It is NON-XML Response');
|
|
try {
|
|
var response = JSON.parse(responseString);
|
|
return callback(null, response);
|
|
} catch (exception) {
|
|
debug('exception ', exception);
|
|
return callback(callback, responseString);
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
return function (res) {
|
|
debug('----------- Received Response -------------');
|
|
var dbgResponseBuffer = [];
|
|
var headers = res.headers;
|
|
var statusCode = res.statusCode;
|
|
// debug('res ', res);
|
|
try {
|
|
statusCode = parseInt(statusCode, 10);
|
|
} catch (Exception) {
|
|
debug('Failed to parse statusCode as statusCode not provided in the response. ', statusCode);
|
|
}
|
|
var charset = '';
|
|
var content_type = '';
|
|
var responseString = '';
|
|
if (headers['content-type']) {
|
|
content_type = headers['content-type'].toLowerCase();
|
|
}
|
|
|
|
if (content_type && content_type.indexOf('charset') > -1 && content_type.split(';')[0] && content_type.split(';')[1]) {
|
|
if (content_type.split(';')[1] && content_type.split(';')[1].trim().match(/^((\b[^\s=]+)=(([^=]|\\=)+))*$/)[3]) {
|
|
charset = content_type.split(';')[1].trim().match(/^((\b[^\s=]+)=(([^=]|\\=)+))*$/)[3];
|
|
}
|
|
content_type = content_type.split(';')[0].toLowerCase();
|
|
}
|
|
|
|
var ResponseHeaders = headers;
|
|
res.on('data', function (chunk) {
|
|
dbgResponseBuffer.push(chunk);
|
|
});
|
|
res.on('end', function () {
|
|
var bufferString = Buffer.concat(dbgResponseBuffer);
|
|
/**
|
|
* You can use Either userRaw or userCharset.
|
|
* Both of them will not work together.
|
|
*/
|
|
if (userOptions.userRaw === true) {
|
|
var response = {
|
|
data: bufferString,
|
|
Headers: ResponseHeaders
|
|
};
|
|
return callback.call(self, null, response);
|
|
}
|
|
|
|
if (userOptions.userCharset && userOptions.userCharset.length > 0) {
|
|
debug('Inside user Charset option');
|
|
charset = userOptions.userCharset;
|
|
try {
|
|
var win1251String = iconv.decode(bufferString, 'win1251');
|
|
responseString = iconv.encode(win1251String, charset);
|
|
} catch (Exception) {
|
|
debug('Exception iconv ', Exception);
|
|
return callback.call(self, new Error.pinterestAPIError({
|
|
message: 'Failed to parse response received from the pinterest API',
|
|
StatusCode: statusCode || 'unknown'
|
|
}), null);
|
|
}
|
|
} else {
|
|
responseString = bufferString.toString('utf8');
|
|
}
|
|
|
|
debug('responseString ', responseString);
|
|
debug('content_type ', content_type);
|
|
debug('statusCode ', statusCode);
|
|
debug('x-ratelimit-remaining ', ResponseHeaders['x-ratelimit-remaining']);
|
|
|
|
try {
|
|
if (!responseString && statusCode < 400) {
|
|
Object.defineProperty({}, 'lastResponse', {
|
|
enumerable: false,
|
|
writable: false,
|
|
value: res
|
|
});
|
|
return callback.call(self, null, {});
|
|
}
|
|
processResponseType(res, responseString, function (error, response) {
|
|
if (response.errors) {
|
|
debug('It is ErrorResponse');
|
|
var errorResponse = response.errors;
|
|
errorResponse.Headers = ResponseHeaders;
|
|
errorResponse.StatusCode = statusCode || 'unknown';
|
|
return callback.call(self, errorResponse, null);
|
|
} else if (response.error) {
|
|
debug('It is ErrorResponse have a single error');
|
|
var errorDetail = response;
|
|
errorDetail.Headers = ResponseHeaders;
|
|
errorDetail.StatusCode = statusCode || 'unknown';
|
|
return callback.call(self, errorDetail, null);
|
|
} else if (error) {
|
|
return callback.call(self, new Error.pinterestAPIError({
|
|
message: 'Failed to parse response received from the pinterest API',
|
|
StatusCode: statusCode || 'unknown'
|
|
}), null);
|
|
}
|
|
debug('-----------------------------------------------------------------');
|
|
// debug('final response %o ', response);
|
|
// Expose res object
|
|
Object.defineProperty(response, 'lastResponse', {
|
|
enumerable: false,
|
|
writable: false,
|
|
value: res
|
|
});
|
|
return callback.call(self, null, response);
|
|
});
|
|
} catch (exception) {
|
|
return callback.call(self, new Error.pinterestAPIError({
|
|
message: 'Invalid response received from the pinterest API',
|
|
StatusCode: statusCode || 'unknown'
|
|
}), null);
|
|
}
|
|
});
|
|
};
|
|
},
|
|
|
|
_errorHandler: function (req, callback) {
|
|
var self = this;
|
|
return function (error) {
|
|
if (req._isAborted) {
|
|
// already handled
|
|
return;
|
|
}
|
|
callback.call(self, new Error.pinterestConnectionError({
|
|
message: 'An error occurred with our connection to pinterest',
|
|
error: error
|
|
}), null);
|
|
};
|
|
},
|
|
|
|
_request: function (method, path, data, auth, options, callback) {
|
|
var requestParamsJSONCopy = JSON.parse(JSON.stringify(data));
|
|
var self = this;
|
|
|
|
self.body = '';
|
|
var userRaw = '';
|
|
var userCharset = '';
|
|
/**custom option passed by user, a better way to do this would be nice */
|
|
if (data.__RAW__) {
|
|
userRaw = data.__RAW__;
|
|
delete data.__RAW__;
|
|
}
|
|
if (data.__CHARSET__) {
|
|
userCharset = data.__CHARSET__;
|
|
delete data.__CHARSET__;
|
|
}
|
|
|
|
var authorization = 'Bearer';
|
|
if (options.authorization === 'Basic') {
|
|
authorization = options.authorization;
|
|
var keySecret = self._pinterest.getApiField('key') + ':' + self._pinterest.getApiField('secret');
|
|
var buffer = new Buffer(keySecret);
|
|
var base64String = buffer.toString('base64');
|
|
authorization = authorization.concat(' ', base64String);
|
|
} else {
|
|
authorization = authorization.concat(' ', self._pinterest.getApiField('userToken'));
|
|
}
|
|
|
|
var headers = {
|
|
// 'Content-Type': 'application/json',
|
|
'Accept': 'application/json',
|
|
'cache-control': 'no-cache',
|
|
'Content-Language': 'en-US',
|
|
'Authorization': authorization
|
|
};
|
|
|
|
if (options.contentType && options.contentType === 'application/json') {
|
|
self.body = JSON.stringify(data);
|
|
headers['Content-Length'] = self.body.length;
|
|
} else if (['POST', 'PUT', 'PATCH'].indexOf(method) > -1) {
|
|
headers['Content-Type'] = 'application/json';
|
|
self.body = JSON.stringify(data);
|
|
headers['Content-Length'] = self.body.length;
|
|
} else if (['GET'].indexOf(method) > -1) {
|
|
path = path.concat('?', qs.stringify(data));
|
|
} else if (['DELETE'].indexOf(method) > -1) {
|
|
self.body = JSON.stringify(data);
|
|
}
|
|
|
|
makeRequest();
|
|
|
|
function makeRequest() {
|
|
var timeout = self._pinterest.getApiField('timeout');
|
|
var isInsecureConnection = self._pinterest.getApiField('protocol') === 'http';
|
|
|
|
var host = self.overrideHost || self._pinterest.getApiField('host');
|
|
var params = {
|
|
hostname: host,
|
|
port: self._pinterest.getApiField('port'),
|
|
path: path,
|
|
method: method,
|
|
headers: headers
|
|
};
|
|
|
|
debug('path ', path);
|
|
debug('params %o ', params);
|
|
debug('self.body %o ', self.body);
|
|
var req = (isInsecureConnection ? http : https).request(params);
|
|
|
|
req.setTimeout(timeout, self._timeoutHandler(timeout, req, callback));
|
|
var userOptions = {
|
|
userCharset: userCharset,
|
|
userRaw: userRaw
|
|
};
|
|
req.on('response', self._responseHandler(requestParamsJSONCopy, req, userOptions, callback));
|
|
req.on('error', self._errorHandler(req, callback));
|
|
|
|
req.on('socket', function (socket) {
|
|
socket.on((isInsecureConnection ? 'connect' : 'secureConnect'), function () {
|
|
// Send payload; we're safe:
|
|
req.write(self.body);
|
|
req.end();
|
|
});
|
|
});
|
|
}
|
|
}
|
|
};
|
|
|
|
module.exports = pinterestResource;
|