h54s
Version:
HTML5 Data Adapter for SAS
1,628 lines (1,459 loc) • 203 kB
JavaScript
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.h54s = f()}})(function(){var define,module,exports;return (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})()({1:[function(require,module,exports){
/*
* h54s error constructor
* @constructor
*
*@param {string} type - Error type
*@param {string} message - Error message
*@param {string} status - Error status returned from SAS
*
*/
function h54sError(type, message, status) {
if(Error.captureStackTrace) {
Error.captureStackTrace(this);
}
this.message = message;
this.type = type;
this.status = status;
}
h54sError.prototype = Object.create(Error.prototype, {
constructor: {
configurable: false,
enumerable: false,
writable: false,
value: h54sError
},
name: {
configurable: false,
enumerable: false,
writable: false,
value: 'h54sError'
}
});
module.exports = h54sError;
},{}],2:[function(require,module,exports){
const h54sError = require('../error.js');
/**
* h54s SAS Files object constructor
* @constructor
*
*@param {file} file - File added when object is created
*@param {string} macroName - macro name
*
*/
function Files(file, macroName) {
this._files = {};
Files.prototype.add.call(this, file, macroName);
}
/**
* Add file to files object
* @param {file} file - Instance of JavaScript File object
* @param {string} macroName - Sas macro name
*
*/
Files.prototype.add = function(file, macroName) {
if(file && macroName) {
if(!(file instanceof File || file instanceof Blob)) {
throw new h54sError('argumentError', 'First argument must be instance of File object');
}
if(typeof macroName !== 'string') {
throw new h54sError('argumentError', 'Second argument must be string');
}
if(!isNaN(macroName[macroName.length - 1])) {
throw new h54sError('argumentError', 'Macro name cannot have number at the end');
}
} else {
throw new h54sError('argumentError', 'Missing arguments');
}
this._files[macroName] = [
'FILE',
file
];
};
module.exports = Files;
},{"../error.js":1}],3:[function(require,module,exports){
const h54sError = require('./error.js');
const sasVersionMap = {
v9: {
url: '/SASStoredProcess/do',
loginUrl: '/SASLogon/login',
logoutUrl: '/SASStoredProcess/do?_action=logoff',
RESTAuthLoginUrl: '/SASLogon/v1/tickets'
},
viya: {
url: '/SASJobExecution/',
loginUrl: '/SASLogon/login.do',
logoutUrl: '/SASLogon/logout.do?',
RESTAuthLoginUrl: ''
}
}
/**
*
* @constructor
* @param {Object} config - Configuration object for the H54S SAS Adapter
* @param {String} config.sasVersion - Version of SAS, either 'v9' or 'viya'
* @param {Boolean} config.debug - Whether debug mode is enabled, sets _debug=131
* @param {String} config.metadataRoot - Base path of all project services to be prepended to _program path
* @param {String} config.url - URI of the job executor - SPWA or JES
* @param {String} config.loginUrl - URI of the SASLogon web login path - overridden by form action
* @param {String} config.logoutUrl - URI of the logout action
* @param {String} config.RESTauth - Boolean to toggle use of REST authentication in SAS v9
* @param {String} config.RESTauthLoginUrl - Address of SASLogon tickets endpoint for REST auth
* @param {Boolean} config.retryAfterLogin - Whether to resume requests which were parked with login redirect after a successful re-login
* @param {Number} config.maxXhrRetries - If a program call fails, attempt to call it again N times until it succeeds
* @param {Number} config.ajaxTimeout - Number of milliseconds to wait for a response before closing the request
* @param {Boolean} config.useMultipartFormData - Whether to use multipart for POST - for legacy backend support
* @param {String} config.csrf - CSRF token for JES
* @
*
*/
const h54s = module.exports = function(config) {
// Default config values, overridden by anything in the config object
this.sasVersion = (config && config.sasVersion) || 'v9' //use v9 as default=
this.debug = (config && config.debug) || false;
this.metadataRoot = (config && config.metadataRoot) || '';
this.url = sasVersionMap[this.sasVersion].url;
this.loginUrl = sasVersionMap[this.sasVersion].loginUrl;
this.logoutUrl = sasVersionMap[this.sasVersion].logoutUrl;
this.RESTauth = false;
this.RESTauthLoginUrl = sasVersionMap[this.sasVersion].RESTAuthLoginUrl;
this.retryAfterLogin = true;
this.maxXhrRetries = 5;
this.ajaxTimeout = (config && config.ajaxTimeout) || 300000;
this.useMultipartFormData = (config && config.useMultipartFormData) || true;
this.csrf = ''
this.isViya = this.sasVersion === 'viya';
// Initialising callback stacks for when authentication is paused
this.remoteConfigUpdateCallbacks = [];
this._pendingCalls = [];
this._customPendingCalls = [];
this._disableCalls = false
this._ajax = require('./methods/ajax.js')();
_setConfig.call(this, config);
// If this instance was deployed with a standalone config external to the build use that
if(config && config.isRemoteConfig) {
const self = this;
this._disableCalls = true;
// 'h54sConfig.json' is for the testing with karma
//replaced by gulp in dev build (defined in gulpfile under proxies)
this._ajax.get('h54sConfig.json').success(function(res) {
const remoteConfig = JSON.parse(res.responseText)
// Save local config before updating it with remote config
const localConfig = Object.assign({}, config)
const oldMetadataRoot = localConfig.metadataRoot;
for(let key in remoteConfig) {
if(remoteConfig.hasOwnProperty(key) && key !== 'isRemoteConfig') {
config[key] = remoteConfig[key];
}
}
_setConfig.call(self, config);
// Execute callbacks when overrides from remote config are applied
for(let i = 0, n = self.remoteConfigUpdateCallbacks.length; i < n; i++) {
const fn = self.remoteConfigUpdateCallbacks[i];
fn();
}
// Execute sas calls disabled while waiting for the config
self._disableCalls = false;
while(self._pendingCalls.length > 0) {
const pendingCall = self._pendingCalls.shift();
const sasProgram = pendingCall.options.sasProgram;
const callbackPending = pendingCall.options.callback;
const params = pendingCall.params;
//update debug because it may change in the meantime
params._debug = self.debug ? 131 : 0;
// Update program path with metadataRoot if it's not set
if(self.metadataRoot && params._program.indexOf(self.metadataRoot) === -1) {
params._program = self.metadataRoot.replace(/\/?$/, '/') + params._program.replace(oldMetadataRoot, '').replace(/^\//, '');
}
// Update debug because it may change in the meantime
params._debug = self.debug ? 131 : 0;
self.call(sasProgram, null, callbackPending, params);
}
// Execute custom calls that we made while waitinf for the config
while(self._customPendingCalls.length > 0) {
const pendingCall = self._customPendingCalls.shift()
const callMethod = pendingCall.callMethod
const _url = pendingCall._url
const options = pendingCall.options;
///update program with metadataRoot if it's not set
if(self.metadataRoot && options.params && options.params._program.indexOf(self.metadataRoot) === -1) {
options.params._program = self.metadataRoot.replace(/\/?$/, '/') + options.params._program.replace(oldMetadataRoot, '').replace(/^\//, '');
}
//update debug because it also may have changed from remoteConfig
if (options.params) {
options.params._debug = self.debug ? 131 : 0;
}
self.managedRequest(callMethod, _url, options);
}
}).error(function (err) {
throw new h54sError('ajaxError', 'Remote config file cannot be loaded. Http status code: ' + err.status);
});
}
// private function to set h54s instance properties
function _setConfig(config) {
if(!config) {
this._ajax.setTimeout(this.ajaxTimeout);
return;
} else if(typeof config !== 'object') {
throw new h54sError('argumentError', 'First parameter should be config object');
}
//merge config object from parameter with this
for(let key in config) {
if(config.hasOwnProperty(key)) {
if((key === 'url' || key === 'loginUrl') && config[key].charAt(0) !== '/') {
config[key] = '/' + config[key];
}
this[key] = config[key];
}
}
//if server is remote use the full server url
//NOTE: This requires CORS and is here for legacy support
if(config.hostUrl) {
if(config.hostUrl.charAt(config.hostUrl.length - 1) === '/') {
config.hostUrl = config.hostUrl.slice(0, -1);
}
this.hostUrl = config.hostUrl;
if (!this.url.includes(this.hostUrl)) {
this.url = config.hostUrl + this.url;
}
if (!this.loginUrl.includes(this.hostUrl)) {
this.loginUrl = config.hostUrl + this.loginUrl;
}
if (!this.RESTauthLoginUrl.includes(this.hostUrl)) {
this.RESTauthLoginUrl = config.hostUrl + this.RESTauthLoginUrl;
}
}
this._ajax.setTimeout(this.ajaxTimeout);
}
};
// replaced by gulp with real version at build time
h54s.version = '2.2.5';
h54s.prototype = require('./methods');
h54s.Tables = require('./tables');
h54s.Files = require('./files');
h54s.SasData = require('./sasData.js');
h54s.fromSasDateTime = require('./methods/utils.js').fromSasDateTime;
h54s.toSasDateTime = require('./tables/utils.js').toSasDateTime;
//self invoked function module
require('./ie_polyfills.js');
},{"./error.js":1,"./files":2,"./ie_polyfills.js":4,"./methods":7,"./methods/ajax.js":6,"./methods/utils.js":8,"./sasData.js":9,"./tables":10,"./tables/utils.js":11}],4:[function(require,module,exports){
module.exports = function() {
if (!Object.create) {
Object.create = function(proto, props) {
if (typeof props !== "undefined") {
throw "The multiple-argument version of Object.create is not provided by this browser and cannot be shimmed.";
}
function ctor() { }
ctor.prototype = proto;
return new ctor();
};
}
// From https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/keys
if (!Object.keys) {
Object.keys = (function () {
'use strict';
var hasOwnProperty = Object.prototype.hasOwnProperty,
hasDontEnumBug = !({toString: null}).propertyIsEnumerable('toString'),
dontEnums = [
'toString',
'toLocaleString',
'valueOf',
'hasOwnProperty',
'isPrototypeOf',
'propertyIsEnumerable',
'constructor'
],
dontEnumsLength = dontEnums.length;
return function (obj) {
if (typeof obj !== 'object' && (typeof obj !== 'function' || obj === null)) {
throw new TypeError('Object.keys called on non-object');
}
var result = [], prop, i;
for (prop in obj) {
if (hasOwnProperty.call(obj, prop)) {
result.push(prop);
}
}
if (hasDontEnumBug) {
for (i = 0; i < dontEnumsLength; i++) {
if (hasOwnProperty.call(obj, dontEnums[i])) {
result.push(dontEnums[i]);
}
}
}
return result;
};
}());
}
// From https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/lastIndexOf
if (!Array.prototype.lastIndexOf) {
Array.prototype.lastIndexOf = function(searchElement /*, fromIndex*/) {
'use strict';
if (this === void 0 || this === null) {
throw new TypeError();
}
var n, k,
t = Object(this),
len = t.length >>> 0;
if (len === 0) {
return -1;
}
n = len - 1;
if (arguments.length > 1) {
n = Number(arguments[1]);
if (n != n) {
n = 0;
}
else if (n !== 0 && n != (1 / 0) && n != -(1 / 0)) {
n = (n > 0 || -1) * Math.floor(Math.abs(n));
}
}
for (k = n >= 0 ? Math.min(n, len - 1) : len - Math.abs(n); k >= 0; k--) {
if (k in t && t[k] === searchElement) {
return k;
}
}
return -1;
};
}
}();
if (window.NodeList && !NodeList.prototype.forEach) {
NodeList.prototype.forEach = Array.prototype.forEach;
}
},{}],5:[function(require,module,exports){
const logs = {
applicationLogs: [],
debugData: [],
sasErrors: [],
failedRequests: []
};
const limits = {
applicationLogs: 100,
debugData: 20,
failedRequests: 20,
sasErrors: 100
};
module.exports.get = {
getSasErrors: function() {
return logs.sasErrors;
},
getApplicationLogs: function() {
return logs.applicationLogs;
},
getDebugData: function() {
return logs.debugData;
},
getFailedRequests: function() {
return logs.failedRequests;
},
getAllLogs: function () {
return {
sasErrors: logs.sasErrors,
applicationLogs: logs.applicationLogs,
debugData: logs.debugData,
failedRequests: logs.failedRequests
}
}
};
module.exports.clear = {
clearApplicationLogs: function() {
logs.applicationLogs.splice(0, logs.applicationLogs.length);
},
clearDebugData: function() {
logs.debugData.splice(0, logs.debugData.length);
},
clearSasErrors: function() {
logs.sasErrors.splice(0, logs.sasErrors.length);
},
clearFailedRequests: function() {
logs.failedRequests.splice(0, logs.failedRequests.length);
},
clearAllLogs: function() {
this.clearApplicationLogs();
this.clearDebugData();
this.clearSasErrors();
this.clearFailedRequests();
}
};
/**
* Adds application logs to an array of logs
*
* @param {String} message - Message to add to applicationLogs
* @param {String} sasProgram - Header - which request did message come from
*
*/
module.exports.addApplicationLog = function(message, sasProgram) {
if(message === 'blank') {
return;
}
const log = {
message: message,
time: new Date(),
sasProgram: sasProgram
};
logs.applicationLogs.push(log);
if(logs.applicationLogs.length > limits.applicationLogs) {
logs.applicationLogs.shift();
}
};
/**
* Adds debug data to an array of logs
*
* @param {String} htmlData - Full html log from executor
* @param {String} debugText - Debug text that came after data output
* @param {String} sasProgram - Which program request did message come from
* @param {String} params - Web app params that were received
*
*/
module.exports.addDebugData = function(htmlData, debugText, sasProgram, params) {
logs.debugData.push({
debugHtml: htmlData,
debugText: debugText,
sasProgram: sasProgram,
params: params,
time: new Date()
});
if(logs.debugData.length > limits.debugData) {
logs.debugData.shift();
}
};
/**
* Adds failed requests to an array of failed request logs
*
* @param {String} responseText - Full html output from executor
* @param {String} debugText - Debug text that came after data output
* @param {String} sasProgram - Which program request did message come from
*
*/
module.exports.addFailedRequest = function(responseText, debugText, sasProgram) {
logs.failedRequests.push({
responseHtml: responseText,
responseText: debugText,
sasProgram: sasProgram,
time: new Date()
});
//max 20 failed requests
if(logs.failedRequests.length > limits.failedRequests) {
logs.failedRequests.shift();
}
};
/**
* Adds SAS errors to an array of logs
*
* @param {Array} errors - Array of errors to concat to main log
*
*/
module.exports.addSasErrors = function(errors) {
logs.sasErrors = logs.sasErrors.concat(errors);
while(logs.sasErrors.length > limits.sasErrors) {
logs.sasErrors.shift();
}
};
},{}],6:[function(require,module,exports){
module.exports = function () {
let timeout = 30000;
let timeoutHandle;
const xhr = function (type, url, data, multipartFormData, headers = {}) {
const methods = {
success: function () {
},
error: function () {
}
};
const XHR = XMLHttpRequest;
const request = new XHR('MSXML2.XMLHTTP.3.0');
request.open(type, url, true);
//multipart/form-data is set automatically so no need for else block
// Content-Type header has to be explicitly set up
if (!multipartFormData) {
if (headers['Content-Type']) {
request.setRequestHeader('Content-Type', headers['Content-Type'])
} else {
request.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
}
}
Object.keys(headers).forEach(key => {
if (key !== 'Content-Type') {
request.setRequestHeader(key, headers[key])
}
})
request.onreadystatechange = function () {
if (request.readyState === 4) {
clearTimeout(timeoutHandle);
if (request.status >= 200 && request.status < 300) {
methods.success.call(methods, request);
} else {
methods.error.call(methods, request);
}
}
};
if (timeout > 0) {
timeoutHandle = setTimeout(function () {
request.abort();
}, timeout);
}
request.send(data);
return {
success: function (callback) {
methods.success = callback;
return this;
},
error: function (callback) {
methods.error = callback;
return this;
}
};
};
const serialize = function (obj) {
const str = [];
for (let p in obj) {
if (obj.hasOwnProperty(p)) {
if (obj[p] instanceof Array) {
for (let i = 0, n = obj[p].length; i < n; i++) {
str.push(encodeURIComponent(p) + "=" + encodeURIComponent(obj[p][i]));
}
} else {
str.push(encodeURIComponent(p) + "=" + encodeURIComponent(obj[p]));
}
}
}
return str.join("&");
};
const createMultipartFormDataPayload = function (obj) {
let data = new FormData();
for (let p in obj) {
if (obj.hasOwnProperty(p)) {
if (obj[p] instanceof Array && p !== 'file') {
for (let i = 0, n = obj[p].length; i < n; i++) {
data.append(p, obj[p][i]);
}
} else if (p === 'file') {
data.append(p, obj[p][0], obj[p][1]);
} else {
data.append(p, obj[p]);
}
}
}
return data;
};
return {
get: function (url, data, multipartFormData, headers) {
let dataStr;
if (typeof data === 'object') {
dataStr = serialize(data);
}
const urlWithParams = dataStr ? (url + '?' + dataStr) : url;
return xhr('GET', urlWithParams, null, multipartFormData, headers);
},
post: function(url, data, multipartFormData, headers) {
let payload = data;
if(typeof data === 'object') {
if(multipartFormData) {
payload = createMultipartFormDataPayload(data);
} else {
payload = serialize(data);
}
}
return xhr('POST', url, payload, multipartFormData, headers);
},
put: function(url, data, multipartFormData, headers) {
let payload = data;
if(typeof data === 'object') {
if(multipartFormData) {
payload = createMultipartFormDataPayload(data);
}
}
return xhr('PUT', url, payload, multipartFormData, headers);
},
delete: function(url, payload, multipartFormData, headers) {
return xhr('DELETE', url, payload, null, headers);
},
patch: function(url, data, multipartFormData, headers) {
let payload = data;
if(typeof data === 'object') {
if(multipartFormData) {
payload = createMultipartFormDataPayload(data);
}
}
return xhr('PATCH', url, payload, multipartFormData, headers);
},
setTimeout: function (t) {
timeout = t;
},
serialize
};
};
},{}],7:[function(require,module,exports){
const h54sError = require('../error.js');
const logs = require('../logs.js');
const Tables = require('../tables');
const SasData = require('../sasData.js');
const Files = require('../files');
/**
* Call Sas program
*
* @param {string} sasProgram - Path of the sas program
* @param {Object} dataObj - Instance of Tables object with data added
* @param {function} callback - Callback function called when ajax call is finished
* @param {Object} params - object containing additional program parameters
*
*/
module.exports.call = function (sasProgram, dataObj, callback, params) {
const self = this;
let retryCount = 0;
const dbg = this.debug
const csrf = this.csrf;
if (!callback || typeof callback !== 'function') {
throw new h54sError('argumentError', 'You must provide a callback');
}
if (!sasProgram) {
throw new h54sError('argumentError', 'You must provide Sas program file path');
}
if (typeof sasProgram !== 'string') {
throw new h54sError('argumentError', 'First parameter should be string');
}
if (this.useMultipartFormData === false && !(dataObj instanceof Tables)) {
throw new h54sError('argumentError', 'Cannot send files using application/x-www-form-urlencoded. Please use Tables or default value for useMultipartFormData');
}
if (!params) {
params = {
_program: this._utils.getFullProgramPath(this.metadataRoot, sasProgram),
_debug: this.debug ? 131 : 0,
_service: 'default',
_csrf: csrf
};
} else {
params = Object.assign({}, params, {_csrf: csrf})
}
if (dataObj) {
let key, dataProvider;
if (dataObj instanceof Tables) {
dataProvider = dataObj._tables;
} else if (dataObj instanceof Files || dataObj instanceof SasData) {
dataProvider = dataObj._files;
} else {
console.log(new h54sError('argumentError', 'Wrong type of tables object'))
}
for (key in dataProvider) {
if (dataProvider.hasOwnProperty(key)) {
params[key] = dataProvider[key];
}
}
}
if (this._disableCalls) {
this._pendingCalls.push({
params,
options: {
sasProgram,
dataObj,
callback
}
});
return;
}
this._ajax.post(this.url, params, this.useMultipartFormData).success(async function (res) {
if (self._utils.needToLogin.call(self, res)) {
//remember the call for latter use
self._pendingCalls.push({
params,
options: {
sasProgram,
dataObj,
callback
}
});
//there's no need to continue if previous call returned login error
if (self._disableCalls) {
return;
} else {
self._disableCalls = true;
}
callback(new h54sError('notLoggedinError', 'You are not logged in'));
} else {
let resObj, unescapedResObj, err;
let done = false;
if (!dbg) {
try {
resObj = self._utils.parseRes(res.responseText, sasProgram, params);
logs.addApplicationLog(resObj.logmessage, sasProgram);
if (dataObj instanceof Tables) {
unescapedResObj = self._utils.unescapeValues(resObj);
} else {
unescapedResObj = resObj;
}
if (resObj.status !== 'success') {
err = new h54sError('programError', resObj.errormessage, resObj.status);
}
done = true;
} catch (e) {
if (e instanceof SyntaxError) {
if (retryCount < self.maxXhrRetries) {
done = false;
self._ajax.post(self.url, params, self.useMultipartFormData).success(this.success).error(this.error);
retryCount++;
logs.addApplicationLog("Retrying #" + retryCount, sasProgram);
} else {
self._utils.parseErrorResponse(res.responseText, sasProgram);
self._utils.addFailedResponse(res.responseText, sasProgram);
err = new h54sError('parseError', 'Unable to parse response json');
done = true;
}
} else if (e instanceof h54sError) {
self._utils.parseErrorResponse(res.responseText, sasProgram);
self._utils.addFailedResponse(res.responseText, sasProgram);
err = e;
done = true;
} else {
self._utils.parseErrorResponse(res.responseText, sasProgram);
self._utils.addFailedResponse(res.responseText, sasProgram);
err = new h54sError('unknownError', e.message);
err.stack = e.stack;
done = true;
}
} finally {
if (done) {
callback(err, unescapedResObj);
}
}
} else {
try {
resObj = await self._utils.parseDebugRes(res.responseText, sasProgram, params, self.hostUrl, self.isViya);
logs.addApplicationLog(resObj.logmessage, sasProgram);
if (dataObj instanceof Tables) {
unescapedResObj = self._utils.unescapeValues(resObj);
} else {
unescapedResObj = resObj;
}
if (resObj.status !== 'success') {
err = new h54sError('programError', resObj.errormessage, resObj.status);
}
done = true;
} catch (e) {
if (e instanceof SyntaxError) {
err = new h54sError('parseError', e.message);
done = true;
} else if (e instanceof h54sError) {
if (e.type === 'parseError' && retryCount < 1) {
done = false;
self._ajax.post(self.url, params, self.useMultipartFormData).success(this.success).error(this.error);
retryCount++;
logs.addApplicationLog("Retrying #" + retryCount, sasProgram);
} else {
if (e instanceof h54sError) {
err = e;
} else {
err = new h54sError('parseError', 'Unable to parse response json');
}
done = true;
}
} else {
err = new h54sError('unknownError', e.message);
err.stack = e.stack;
done = true;
}
} finally {
if (done) {
callback(err, unescapedResObj);
}
}
}
}
}).error(function (res) {
let _csrf
if (res.status === 449 || (res.status === 403 && (res.responseText.includes('_csrf') || res.getResponseHeader('X-Forbidden-Reason') === 'CSRF') && (_csrf = res.getResponseHeader(res.getResponseHeader('X-CSRF-HEADER'))))) {
params['_csrf'] = _csrf;
self.csrf = _csrf
if (retryCount < self.maxXhrRetries) {
self._ajax.post(self.url, params, true).success(this.success).error(this.error);
retryCount++;
logs.addApplicationLog("Retrying #" + retryCount, sasProgram);
} else {
self._utils.parseErrorResponse(res.responseText, sasProgram);
self._utils.addFailedResponse(res.responseText, sasProgram);
callback(new h54sError('parseError', 'Unable to parse response json'));
}
} else {
logs.addApplicationLog('Request failed with status: ' + res.status, sasProgram);
// if request has error text else callback
callback(new h54sError('httpError', res.statusText));
}
});
};
/**
* Login method
*
* @param {string} user - Login username
* @param {string} pass - Login password
* @param {function} callback - Callback function called when ajax call is finished
*
* OR
*
* @param {function} callback - Callback function called when ajax call is finished
*
*/
module.exports.login = function (user, pass, callback) {
if (!user || !pass) {
throw new h54sError('argumentError', 'Credentials not set');
}
if (typeof user !== 'string' || typeof pass !== 'string') {
throw new h54sError('argumentError', 'User and pass parameters must be strings');
}
//NOTE: callback optional?
if (!callback || typeof callback !== 'function') {
throw new h54sError('argumentError', 'You must provide callback');
}
if (!this.RESTauth) {
handleSasLogon.call(this, user, pass, callback);
} else {
handleRestLogon.call(this, user, pass, callback);
}
};
/**
* ManagedRequest method
*
* @param {string} callMethod - get, post,
* @param {string} _url - URL to make request to
* @param {object} options - callback function as callback paramter in options object is required
*
*/
module.exports.managedRequest = function (callMethod = 'get', _url, options = {
callback: () => console.log('Missing callback function')
}) {
const self = this;
const csrf = this.csrf;
let retryCount = 0;
const {useMultipartFormData, sasProgram, dataObj, params, callback, headers} = options
if (sasProgram) {
return self.call(sasProgram, dataObj, callback, params)
}
let url = _url
if (!_url.startsWith('http')) {
url = self.hostUrl + _url
}
const _headers = Object.assign({}, headers, {
'X-CSRF-TOKEN': csrf
})
const _options = Object.assign({}, options, {
headers: _headers
})
if (this._disableCalls) {
this._customPendingCalls.push({
callMethod,
_url,
options: _options
});
return;
}
self._ajax[callMethod](url, params, useMultipartFormData, _headers).success(function (res) {
if (self._utils.needToLogin.call(self, res)) {
//remember the call for latter use
self._customPendingCalls.push({
callMethod,
_url,
options: _options
});
//there's no need to continue if previous call returned login error
if (self._disableCalls) {
return;
} else {
self._disableCalls = true;
}
callback(new h54sError('notLoggedinError', 'You are not logged in'));
} else {
let resObj, err;
let done = false;
try {
const arr = res.getAllResponseHeaders().split('\r\n');
const resHeaders = arr.reduce(function (acc, current, i) {
let parts = current.split(': ');
acc[parts[0]] = parts[1];
return acc;
}, {});
let body = res.responseText
try {
body = JSON.parse(body)
} catch (e) {
console.log('response is not JSON string')
} finally {
resObj = Object.assign({}, {
headers: resHeaders,
status: res.status,
statusText: res.statusText,
body
})
done = true;
}
} catch (e) {
err = new h54sError('unknownError', e.message);
err.stack = e.stack;
done = true;
} finally {
if (done) {
callback(err, resObj)
}
}
}
}).error(function (res) {
let _csrf
if (res.status == 449 || (res.status == 403 && (res.responseText.includes('_csrf') || res.getResponseHeader('X-Forbidden-Reason') === 'CSRF') && (_csrf = res.getResponseHeader(res.getResponseHeader('X-CSRF-HEADER'))))) {
self.csrf = _csrf
const _headers = Object.assign({}, headers, {[res.getResponseHeader('X-CSRF-HEADER')]: _csrf})
if (retryCount < self.maxXhrRetries) {
self._ajax[callMethod](url, params, useMultipartFormData, _headers).success(this.success).error(this.error);
retryCount++;
} else {
callback(new h54sError('parseError', 'Unable to parse response json'));
}
} else {
logs.addApplicationLog('Managed request failed with status: ' + res.status, _url);
// if request has error text else callback
callback(new h54sError('httpError', res.responseText, res.status));
}
});
}
/**
* Log on to SAS if we are asked to
* @param {String} user - Username of user
* @param {String} pass - Password of user
* @param {function} callback - what to do after
*/
function handleSasLogon(user, pass, callback) {
const self = this;
const loginParams = {
_service: 'default',
//for SAS 9.4,
username: user,
password: pass
};
for (let key in this._aditionalLoginParams) {
loginParams[key] = this._aditionalLoginParams[key];
}
this._loginAttempts = 0;
this._ajax.post(this.loginUrl, loginParams)
.success(handleSasLogonSuccess)
.error(handleSasLogonError);
function handleSasLogonError(res) {
if (res.status == 449) {
handleSasLogonSuccess(res);
return;
}
logs.addApplicationLog('Login failed with status code: ' + res.status);
callback(res.status);
}
function handleSasLogonSuccess(res) {
if (++self._loginAttempts === 3) {
return callback(-2);
}
if (self._utils.needToLogin.call(self, res)) {
//we are getting form again after redirect
//and need to login again using the new url
//_loginChanged is set in needToLogin function
//but if login url is not different, we are checking if there are aditional parameters
if (self._loginChanged || (self._isNewLoginPage && !self._aditionalLoginParams)) {
delete self._loginChanged;
const inputs = res.responseText.match(/<input.*"hidden"[^>]*>/g);
if (inputs) {
inputs.forEach(function (inputStr) {
const valueMatch = inputStr.match(/name="([^"]*)"\svalue="([^"]*)/);
loginParams[valueMatch[1]] = valueMatch[2];
});
}
self._ajax.post(self.loginUrl, loginParams).success(function () {
//we need this get request because of the sas 9.4 security checks
self._ajax.get(self.url).success(handleSasLogonSuccess).error(handleSasLogonError);
}).error(handleSasLogonError);
}
else {
//getting form again, but it wasn't a redirect
logs.addApplicationLog('Wrong username or password');
callback(-1);
}
}
else {
self._disableCalls = false;
callback(res.status);
while (self._pendingCalls.length > 0) {
const pendingCall = self._pendingCalls.shift();
const method = pendingCall.method || self.call.bind(self);
const sasProgram = pendingCall.options.sasProgram;
const callbackPending = pendingCall.options.callback;
const params = pendingCall.params;
//update debug because it may change in the meantime
params._debug = self.debug ? 131 : 0;
if (self.retryAfterLogin) {
method(sasProgram, null, callbackPending, params);
}
}
}
}
}
/**
* REST logon for 9.4 v1 ticket based auth
* @param {String} user -
* @param {String} pass
* @param {function} callback
*/
function handleRestLogon(user, pass, callback) {
const self = this;
const loginParams = {
username: user,
password: pass
};
this._ajax.post(this.RESTauthLoginUrl, loginParams).success(function (res) {
const location = res.getResponseHeader('Location');
self._ajax.post(location, {
service: self.url
}).success(function (res) {
if (self.url.indexOf('?') === -1) {
self.url += '?ticket=' + res.responseText;
} else {
if (self.url.indexOf('ticket') !== -1) {
self.url = self.url.replace(/ticket=[^&]+/, 'ticket=' + res.responseText);
} else {
self.url += '&ticket=' + res.responseText;
}
}
callback(res.status);
}).error(function (res) {
logs.addApplicationLog('Login failed with status code: ' + res.status);
callback(res.status);
});
}).error(function (res) {
if (res.responseText === 'error.authentication.credentials.bad') {
callback(-1);
} else {
logs.addApplicationLog('Login failed with status code: ' + res.status);
callback(res.status);
}
});
}
/**
* Logout method
*
* @param {function} callback - Callback function called when logout is done
*
*/
module.exports.logout = function (callback) {
const baseUrl = this.hostUrl || '';
const url = baseUrl + this.logoutUrl;
this._ajax.get(url).success(function (res) {
this._disableCalls = true
callback();
}).error(function (res) {
logs.addApplicationLog('Logout failed with status code: ' + res.status);
callback(res.status);
});
};
/*
* Enter debug mode
*
*/
module.exports.setDebugMode = function () {
this.debug = true;
};
/*
* Exit debug mode and clear logs
*
*/
module.exports.unsetDebugMode = function () {
this.debug = false;
};
for (let key in logs.get) {
if (logs.get.hasOwnProperty(key)) {
module.exports[key] = logs.get[key];
}
}
for (let key in logs.clear) {
if (logs.clear.hasOwnProperty(key)) {
module.exports[key] = logs.clear[key];
}
}
/*
* Add callback functions executed when properties are updated with remote config
*
*@callback - callback pushed to array
*
*/
module.exports.onRemoteConfigUpdate = function (callback) {
this.remoteConfigUpdateCallbacks.push(callback);
};
module.exports._utils = require('./utils.js');
/**
* Login call which returns a promise
* @param {String} user - Username
* @param {String} pass - Password
*/
module.exports.promiseLogin = function (user, pass) {
return new Promise((resolve, reject) => {
if (!user || !pass) {
reject(new h54sError('argumentError', 'Credentials not set'))
}
if (typeof user !== 'string' || typeof pass !== 'string') {
reject(new h54sError('argumentError', 'User and pass parameters must be strings'))
}
if (!this.RESTauth) {
customHandleSasLogon.call(this, user, pass, resolve);
} else {
customHandleRestLogon.call(this, user, pass, resolve);
}
})
}
/**
*
* @param {String} user - Username
* @param {String} pass - Password
* @param {function} callback - function to call when successful
*/
function customHandleSasLogon(user, pass, callback) {
const self = this;
let loginParams = {
_service: 'default',
//for SAS 9.4,
username: user,
password: pass
};
for (let key in this._aditionalLoginParams) {
loginParams[key] = this._aditionalLoginParams[key];
}
this._loginAttempts = 0;
loginParams = this._ajax.serialize(loginParams)
this._ajax.post(this.loginUrl, loginParams)
.success(handleSasLogonSuccess)
.error(handleSasLogonError);
function handleSasLogonError(res) {
if (res.status == 449) {
handleSasLogonSuccess(res);
// resolve(res.status);
} else {
logs.addApplicationLog('Login failed with status code: ' + res.status);
callback(res.status);
}
}
function handleSasLogonSuccess(res) {
if (++self._loginAttempts === 3) {
callback(-2);
}
if (self._utils.needToLogin.call(self, res)) {
//we are getting form again after redirect
//and need to login again using the new url
//_loginChanged is set in needToLogin function
//but if login url is not different, we are checking if there are aditional parameters
if (self._loginChanged || (self._isNewLoginPage && !self._aditionalLoginParams)) {
delete self._loginChanged;
const inputs = res.responseText.match(/<input.*"hidden"[^>]*>/g);
if (inputs) {
inputs.forEach(function (inputStr) {
const valueMatch = inputStr.match(/name="([^"]*)"\svalue="([^"]*)/);
loginParams[valueMatch[1]] = valueMatch[2];
});
}
self._ajax.post(self.loginUrl, loginParams).success(function () {
handleSasLogonSuccess()
}).error(handleSasLogonError);
}
else {
//getting form again, but it wasn't a redirect
logs.addApplicationLog('Wrong username or password');
callback(-1);
}
}
else {
self._disableCalls = false;
callback(res.status);
while (self._customPendingCalls.length > 0) {
const pendingCall = self._customPendingCalls.shift()
const method = pendingCall.method || self.managedRequest.bind(self);
const callMethod = pendingCall.callMethod
const _url = pendingCall._url
const options = pendingCall.options;
//update debug because it may change in the meantime
if (options.params) {
options.params._debug = self.debug ? 131 : 0;
}
if (self.retryAfterLogin) {
method(callMethod, _url, options);
}
}
while (self._pendingCalls.length > 0) {
const pendingCall = self._pendingCalls.shift();
const method = pendingCall.method || self.call.bind(self);
const sasProgram = pendingCall.options.sasProgram;
const callbackPending = pendingCall.options.callback;
const params = pendingCall.params;
//update debug because it may change in the meantime
params._debug = self.debug ? 131 : 0;
if (self.retryAfterLogin) {
method(sasProgram, null, callbackPending, params);
}
}
}
};
}
/**
* To be used with future managed metadata calls
* @param {String} user - Username
* @param {String} pass - Password
* @param {function} callback - what to call after
* @param {String} callbackUrl - where to navigate after getting ticket
*/
function customHandleRestLogon(user, pass, callback, callbackUrl) {
const self = this;
const loginParams = {
username: user,
password: pass
};
this._ajax.post(this.RESTauthLoginUrl, loginParams).success(function (res) {
const location = res.getResponseHeader('Location');
self._ajax.post(location, {
service: callbackUrl
}).success(function (res) {
if (callbackUrl.indexOf('?') === -1) {
callbackUrl += '?ticket=' + res.responseText;
} else {
if (callbackUrl.indexOf('ticket') !== -1) {
callbackUrl = callbackUrl.replace(/ticket=[^&]+/, 'ticket=' + res.responseText);
} else {
callbackUrl += '&ticket=' + res.responseText;
}
}
callback(res.status);
}).error(function (res) {
logs.addApplicationLog('Login failed with status code: ' + res.status);
callback(res.status);
});
}).error(function (res) {
if (res.responseText === 'error.authentication.credentials.bad') {
callback(-1);
} else {
logs.addApplicationLog('Login failed with status code: ' + res.status);
callback(res.status);
}
});
}
// Utilility functions for handling files and folders on VIYA
/**
* Returns the details of a folder from folder service
* @param {String} folderName - Full path of folder to be found
* @param {Object} options - Options object for managedRequest
*/
module.exports.getFolderDetails = function (folderName, options) {
// First call to get folder's id
let url = "/folders/folders/@item?path=" + folderName
return this.managedRequest('get', url, options);
}
/**
* Returns the details of a file from files service
* @param {String} fileUri - Full path of file to be found
* @param {Object} options - Options object for managedRequest: cacheBust forces browser to fetch new file
*/
module.exports.getFileDetails = function (fileUri, options) {
const cacheBust = options.cacheBust
if (cacheBust) {
fileUri += '?cacheBust=' + new Date().getTime()
}
return this.managedRequest('get', fileUri, options);
}
/**
* Returns the contents of a file from files service
* @param {String} fileUri - Full path of file to be downloaded
* @param {Object} options - Options object for managedRequest: cacheBust forces browser to fetch new file
*/
module.exports.getFileContent = function (fileUri, options) {
const cacheBust = options.cacheBust
let uri = fileUri + '/content'
if (cacheBust) {
uri += '?cacheBust=' + new Date().getTime()
}
return this.managedRequest('get', uri, options);
}
// Util functions for working with files and folders
/**
* Returns details about folder it self and it's members with details
* @param {String} folderName - Full path of folder to be found
* @param {Object} options - Options object for managedRequest
*/
module.exports.getFolderContents = async function (folderName, options) {
const self = this
const {callback} = options
// Second call to get folder's memebers
const _callback = (err, data) => {
// handle error of the first call
if(err) {
callback(err, data)
return
}
let id = data.body.id
let membersUrl = '/folders/folders/' + id + '/members' + '/?limit=10000000';
return self.managedRequest('get', membersUrl, {callback})
}
// First call to get folder's id
let url = "/folders/folders/@item?path=" + folderName
const optionsObj = Object.assign({}, options, {
callback: _callback
})
this.managedRequest('get', url, optionsObj)
}
/**
* Creates a folder
* @param {String} parentUri - The uri of the folder where the new child is being created
* @param {String} folderName - Full path of folder to be found
* @param {Object} options - Options object for managedRequest
*/
module.exports.createNewFolder = function (parentUri, folderName, options) {
const headers = {
'Accept': 'application/json, text/javascript, */*; q=0.01',
'Content-Type': 'application/json',
}
const url = '/folders/folders?parentFolderUri=' + parentUri;
const data = {
'name': folderName,
'type': "folder"
}
const optionsObj = Object.assign({}, options, {
params: JSON.stringify(data),
headers,
useMultipartFormData: false
})
return this.managedRequest('post', url, optionsObj);
}
/**
* Deletes a folder
* @param {String} folderId - Full URI of folder to be deleted
* @param {Object} options - Options object for managedRequest
*/
module.exports.deleteFolderById = function (folderId, options) {
const url = '/folders/folders/' + folderId;
return this.managedRequest('delete', url, options)
}
/**
* Creates a new file
* @param {String} fileName - Name of the file being created
* @param {String} fileBlob - Content of the file
* @param {String} parentFOlderUri - URI of the parent folder where the file is to be created
* @param {Object} options - Options object for managedRequest
*/
module.exports.createNewFile = function (fileName, fileBlob, parentFolderUri, options) {
let url = "/files/files#multipartUpload";
let dataObj = {
file: [fileBlob, fileName],
parentFolderUri
}
const optionsObj = Object.assign({}, options, {
params: dataObj,
useMultipartFormData: true,
})
return this.managedRequest('post', url, optionsObj);
}
/**
* Generic delete function that deletes by URI
* @param {String} itemUri - Name of the item being deleted
* @param {Object} options - Options object for managedRequest
*/
module.exports.deleteItem = function (itemUri, options) {
return this.managedRequest('delete', itemUri, options)
}
/**
* Updates contents of a file
* @param {String} fileName - Name of the file being updated
* @param {Object | Blob} dataObj - New content of the file (Object must contain file key)
* Object example {
* file: [<blob>, <fileName>]
* }
* @param {String} lastModified - the last-modified header string that matches that of file being overwritten
* @param {Object} options - Options object for managedRequest
*/
module.exports.updateFile = function (itemUri, dataObj, lastModified, options) {
const url = itemUri + '/content'
console.log('URL', url)
let headers = {
'Content-Type': 'application/vnd.sas.file',
'If-Unmodified-Since': lastModified
}
const isBlob = dataObj instanceof Blob
const useMultipartFormData = !isBlob // set useMultipartFormData to true if dataObj is not Blob
const optionsObj = Object.assign({}, options, {
params: dataObj,
headers,
useMultipartFormData
})
return this.managedRequest('put', url, optionsObj);
}
/**
Updates file Metadata
* @param {String} fileName - Name of the file being updated
* @param {String} lastModified - the last-modified header string that matches that of file being updated
* @param {Object | Blob} dataObj - objects containing the fields that are being changed
* @param {Object} options - Options object for managedRequest
*/
module.exports.updateFileMetadata = function (itemUri, dataObj, lastModified, options) {
let headers = {
'Content-Type':'application/vnd.sas.file+json',
'If-Unmodified-Since': lastModified
}
const isBlob = dataObj instanceof Blob
const useMultipartFormData = !isBlob // set useMultipartFormData to true if dataObj is not Blob
const optionsObj = Object.assign({}, options, {
params: dataObj,
headers,
useMultipartFormData
})
return this.managedRequest('patch', itemUri, optionsObj);
}
/**
* Updates folder info
* @param {String} folderUri - uri of the folder that is being changed
* @param {String} lastModified - the last-modified header string that matches that of the folder being updated
* @param {Object | Blob} dataObj - object thats is either the whole folder or partial data
* @param {Object} options - Options object for managedRequest
*/
module.exports.updateFolderMetadata = function (folderUri, dataObj, lastModified, options) {
/**
@constant {Boolean} partialData - indicates wether dataObj containts all the data that needs to be send to the server
or partial data which contatins only the fields that need to be updated, in which case a call needs to be made to the server for
the rest of the data before the update can be done
*/
const {partialData} = options;
const headers = {
'Content-Type': "application/vnd.sas.content.folder+json",
'If-Unmodified-Since': lastModified,
}
if (partialData) {
const _callback = (err, res) => {
if (res) {
const folder = Object.assign({}, res.body, dataObj);
let forBlob = JSON.stringify(folder);
let data = new Blob([forBlob], {type: "octet/stream"});
const optionsObj = Object.assign({}, options, {
params: data,
headers,
useMultipartFormData : false,
})
return this.managedRequest('put', folderUri, optionsObj);
}
return options.callback(err);
}
const getOptionsObj = Object.assign({}, options, {
headers: {'Content-Type': "application/vnd.sas.content.folder+json"},
callback: _callback
})
return this.managedRequest('get', folderUri, getOptionsObj);
}
else {
if ( !(dataObj instanceof Blob)) {
let forBlob = JSON.stringify(dataObj);
dataObj = new Blob([forBlob], {type: "octet/stream"});
}
const optionsObj = Object.assign({}, options, {
params: dataObj,
headers,
useMultipartFormData : false,
})
return this.managedRequest('put', folderUri, optionsObj);
}
}
},{"../error.js":1,"../files":2,"../logs.js":5,"../sasData.js":9,"../tables":10,"./utils.js":8}],8:[function(require,module,exports){
const logs = require('../logs.js');
const h54sError = require('../error.js');
const programNotFoundPatt = /<title>(Stored Process Error|SASStoredProcess)<\/title>[\s\S]*<h2>(Stored process not found:.*|.*not a valid stored process path.)<\/h2>/;
const badJobDefinition = "<h2>Parameter Error <br/>Unable to get job definition.</h2>";
const responseReplace = fu