h54s
Version:
HTML5 Data Adapter for SAS
308 lines (275 loc) • 9.73 kB
JavaScript
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 = function(res) {
return res
};
/**
* Parse response from server
*
* @param {object} responseText - response html from the server
* @param {string} sasProgram - sas program path
* @param {object} params - params sent to sas program with addTable
*
*/
module.exports.parseRes = function(responseText, sasProgram, params) {
const matches = responseText.match(programNotFoundPatt);
if(matches) {
throw new h54sError('programNotFound', 'You have not been granted permission to perform this action, or the STP is missing.');
}
//remove new lines in json response
//replace \\(d) with \(d) - SAS json parser is escaping it
return JSON.parse(responseReplace(responseText));
};
/**
* Parse response from server in debug mode
*
* @param {object} responseText - response html from the server
* @param {string} sasProgram - sas program path
* @param {object} params - params sent to sas program with addTable
* @param {string} hostUrl - same as in h54s constructor
* @param {bool} isViya - same as in h54s constructor
*
*/
module.exports.parseDebugRes = function (responseText, sasProgram, params, hostUrl, isViya) {
const self = this
let matches = responseText.match(programNotFoundPatt);
if (matches) {
throw new h54sError('programNotFound', 'Sas program completed with errors');
}
if (isViya) {
const matchesWrongJob = responseText.match(badJobDefinition);
if (matchesWrongJob) {
throw new h54sError('programNotFound', 'Sas program completed with errors. Unable to get job definition.');
}
}
//find json
let patt = isViya ? /^(.?<iframe.*src=")([^"]+)(.*iframe>)/m : /^(.?--h54s-data-start--)([\S\s]*?)(--h54s-data-end--)/m;
matches = responseText.match(patt);
const page = responseText.replace(patt, '');
const htmlBodyPatt = /<body.*>([\s\S]*)<\/body>/;
const bodyMatches = page.match(htmlBodyPatt);
//remove html tags
let debugText = bodyMatches[1].replace(/<[^>]*>/g, '');
debugText = this.decodeHTMLEntities(debugText);
logs.addDebugData(bodyMatches[1], debugText, sasProgram, params);
if (isViya && this.parseErrorResponse(responseText, sasProgram)) {
throw new h54sError('sasError', 'Sas program completed with errors');
}
if (!matches) {
throw new h54sError('parseError', 'Unable to parse response json');
}
const promise = new Promise(function (resolve, reject) {
let jsonObj
if (isViya) {
const xhr = new XMLHttpRequest();
const baseUrl = hostUrl || "";
xhr.open("GET", baseUrl + matches[2]);
xhr.onload = function () {
if (this.status >= 200 && this.status < 300) {
resolve(JSON.parse(xhr.responseText.replace(/(\r\n|\r|\n)/g, '')));
} else {
reject(new h54sError('fetchError', xhr.statusText, this.status))
}
};
xhr.onerror = function () {
reject(new h54sError('fetchError', xhr.statusText))
};
xhr.send();
} else {
try {
jsonObj = JSON.parse(responseReplace(matches[2]));
} catch (e) {
reject(new h54sError('parseError', 'Unable to parse response json'))
}
if (jsonObj && jsonObj.h54sAbort) {
resolve(jsonObj);
} else if (self.parseErrorResponse(responseText, sasProgram)) {
reject(new h54sError('sasError', 'Sas program completed with errors'))
} else {
resolve(jsonObj);
}
}
});
return promise;
};
/**
* Add failed response to logs - used only if debug=false
*
* @param {string} responseText - response html from the server
* @param {string} sasProgram - sas program path
*
*/
module.exports.addFailedResponse = function(responseText, sasProgram) {
const patt = /<script([\s\S]*)\/form>/;
const patt2 = /display\s?:\s?none;?\s?/;
//remove script with form for toggling the logs and "display:none" from style
responseText = responseText.replace(patt, '').replace(patt2, '');
let debugText = responseText.replace(/<[^>]*>/g, '');
debugText = this.decodeHTMLEntities(debugText);
logs.addFailedRequest(responseText, debugText, sasProgram);
};
/**
* Unescape all string values in returned object
*
* @param {object} obj
*
*/
module.exports.unescapeValues = function(obj) {
for (let key in obj) {
if (typeof obj[key] === 'string') {
obj[key] = decodeURIComponent(obj[key]);
} else if(typeof obj === 'object') {
this.unescapeValues(obj[key]);
}
}
return obj;
};
/**
* Parse error response from server and save errors in memory
*
* @param {string} res - server response
* @param {string} sasProgram - sas program which returned the response
*
*/
module.exports.parseErrorResponse = function(res, sasProgram) {
//capture 'ERROR: [text].' or 'ERROR xx [text].'
const patt = /^ERROR(:\s|\s\d\d)(.*\.|.*\n.*\.)/gm;
let errors = res.replace(/(<([^>]+)>)/ig, '').match(patt);
if(!errors) {
return;
}
let errMessage;
for(let i = 0, n = errors.length; i < n; i++) {
errMessage = errors[i].replace(/<[^>]*>/g, '').replace(/(\n|\s{2,})/g, ' ');
errMessage = this.decodeHTMLEntities(errMessage);
errors[i] = {
sasProgram: sasProgram,
message: errMessage,
time: new Date()
};
}
logs.addSasErrors(errors);
return true;
};
/**
* Decode HTML entities - old utility function
*
* @param {string} res - server response
*
*/
module.exports.decodeHTMLEntities = function (html) {
const tempElement = document.createElement('span');
let str = html.replace(/&(#(?:x[0-9a-f]+|\d+)|[a-z]+);/gi,
function (str) {
tempElement.innerHTML = str;
str = tempElement.textContent || tempElement.innerText;
return str;
}
);
return str;
};
/**
* Convert sas time to javascript date
*
* @param {number} sasDate - sas Tate object
*
*/
module.exports.fromSasDateTime = function (sasDate) {
const basedate = new Date("January 1, 1960 00:00:00");
const currdate = sasDate;
// offsets for UTC and timezones and BST
const baseOffset = basedate.getTimezoneOffset(); // in minutes
// convert sas datetime to a current valid javascript date
const basedateMs = basedate.getTime(); // in ms
const currdateMs = currdate * 1000; // to ms
const sasDatetime = currdateMs + basedateMs;
const jsDate = new Date();
jsDate.setTime(sasDatetime); // first time to get offset BST daylight savings etc
const currOffset = jsDate.getTimezoneOffset(); // adjust for offset in minutes
const offsetVar = (baseOffset - currOffset) * 60 * 1000; // difference in milliseconds
const offsetTime = sasDatetime - offsetVar; // finding BST and daylight savings
jsDate.setTime(offsetTime); // update with offset
return jsDate;
};
/**
* Checks whether response object is a login redirect
* @param {Object} responseObj xhr response to be checked for logon redirect
*/
module.exports.needToLogin = function(responseObj) {
const isSASLogon = responseObj.responseURL && responseObj.responseURL.includes('SASLogon')
if (isSASLogon === false) {
return false
}
const patt = /<form.+action="(.*Logon[^"]*).*>/;
const matches = patt.exec(responseObj.responseText);
let newLoginUrl;
if(!matches) {
//there's no form, we are in. hooray!
return false;
} else {
const actionUrl = matches[1].replace(/\?.*/, '');
if(actionUrl.charAt(0) === '/') {
newLoginUrl = this.hostUrl ? this.hostUrl + actionUrl : actionUrl;
if(newLoginUrl !== this.loginUrl) {
this._loginChanged = true;
this.loginUrl = newLoginUrl;
}
} else {
//relative path
const lastIndOfSlash = responseObj.responseURL.lastIndexOf('/') + 1;
//remove everything after the last slash, and everything until the first
const relativeLoginUrl = responseObj.responseURL.substr(0, lastIndOfSlash).replace(/.*\/{2}[^\/]*/, '') + actionUrl;
newLoginUrl = this.hostUrl ? this.hostUrl + relativeLoginUrl : relativeLoginUrl;
if(newLoginUrl !== this.loginUrl) {
this._loginChanged = true;
this.loginUrl = newLoginUrl;
}
}
//save parameters from hidden form fields
const parser = new DOMParser();
const doc = parser.parseFromString(responseObj.responseText,"text/html");
const res = doc.querySelectorAll("input[type='hidden']");
const hiddenFormParams = {};
if(res) {
//it's new login page if we have these additional parameters
this._isNewLoginPage = true;
res.forEach(function(node) {
hiddenFormParams[node.name] = node.value;
});
this._aditionalLoginParams = hiddenFormParams;
}
return true;
}
};
/**
* Get full program path from metadata root and relative path
*
* @param {string} metadataRoot - Metadata root (path where all programs for the project are located)
* @param {string} sasProgramPath - Sas program path
*
*/
module.exports.getFullProgramPath = function(metadataRoot, sasProgramPath) {
return metadataRoot ? metadataRoot.replace(/\/?$/, '/') + sasProgramPath.replace(/^\//, '') : sasProgramPath;
};
// Returns object where table rows are groupped by key
module.exports.getObjOfTable = function (table, key, value = null) {
const obj = {}
table.forEach(row => {
if (!obj[row[key]]) {
obj[row[key]] = []
obj[row[key]].push(value ? row[value] : row)
} else {
obj[row[key]].push(value ? row[value] : row)
}
})
return obj
}
// Returns self uri out of links array
module.exports.getSelfUri = function (links) {
return links
.filter(e => e.rel === 'self')
.map(e => e.uri)
.shift();
}