// Released under the MIT/X11 license
// http://www.opensource.org/licenses/mit-license.php
"use strict";
var preferences = require("preferences-service");
var prompts = require("prompts");
var clipboard = require("clipboard");
var tabs = require("tabs");
var logger = require("logger");
var passUtils = require("passwords");
var Request = require("request").Request;
var selfMod = require("self");
var urlMod = require("url");
var xhrMod = require("xhr");
var panelMod = require("panel");
var myStorage = require("simple-storage");
var JSONURLDefault = "https://fedorahosted.org/released"+
"/bugzilla-triage-scripts/Config_data.json";
var BTSPrefNS = "bugzilla-triage.setting.";
var BTSPassRealm = "BTSXMLRPCPass";
var copiedAttributes = [ "queryButton", "upstreamButton", "parseAbrtBacktraces",
"submitsLogging", "XorgLogAnalysis", "objectStyle", "signature",
"suspiciousComponents", "verboseInlineHistory" ];
var config = exports.config = {};
var debugOption = null;
function Message(cmd, data) {
debug("Message: cmd = " + cmd + ", data = " + data);
this.cmd = cmd;
this.data = data;
}
function debug(str) {
if (debugOption) {
console.log(str);
}
}
/**
* parse XML object out of string working around various bugs in Gecko
* implementation see https://developer.mozilla.org/en/E4X for more information
*
* @param inStr
* String with unparsed XML string
* @return XML object
*/
function parseXMLfromString (inStuff) {
// if (typeof inStuff !== 'string') In future we should recognize
// this.response
// and get just .text property out of it. TODO
var respStr = inStuff.replace(/^<\?xml\s+version\s*=\s*(["'])[^\1]+\1[^?]*\?>/, ""); // bug
// 336551
return new XML(respStr);
}
/**
* In case URL contains alias, not the real bug number, get the real bug no from
* the XML representation. Sets correct value to this.bugNo.
*
* This is a slow variant for bugs other than actual window
*/
function getRealBugNoSlow(bugNo, location, callback) {
debug("We have to deal with bug aliased as " + this.bugNo);
// https://bugzilla.redhat.com/show_bug.cgi?ctype=xml&id=serialWacom
Request({
url: location.href+"&ctype=xml",
onComplete: function(response) {
if (response.status === 200) {
var xmlRepr = parseXMLfromString(response.text);
// TODO this probably wrong, both XPath and .text attribute
var bugID = parseInt(xmlRepr.bug.bug_id.text, 10);
if (isNaN(bugID)) {
throw new Error("Cannot get bug no. even from XML representation!");
}
debug("The real bug no. is " + bugID);
callback(bugID)
}
}
}).get();
}
function getPassword(login, domain, callback) {
var passPrompt = "Enter your Bugzilla password " +
"for accessing JSONRPC services";
var switchPrompt = "Do you want to switch off XML-RPC " +
"for domain ";
var prefName = BTSPrefNS+"withoutPassowrd", prefValue = [];
var retObject = {
password: null, // password string or null if no password provided
withoutPass: [] // whether user doesn't want to use password at all
};
if (preferences.has(prefName)) {
prefValue = JSON.parse(preferences.get(prefName, null));
debug("getPassword: prefValue = " + prefValue);
if ((prefValue === true) || (prefValue === false)) {
console.log("Clearing previous scheme of " + prefName +
" preference " + prefValue + ".");
preferences.set(prefName, JSON.stringify([]));
prefValue = [];
}
}
if (prefValue.indexOf(domain) == -1) {
passUtils.search({
username: login,
url: domain,
realm: BTSPassRealm,
onComplete: function onComplete([credential]) {
if (credential) {
// We found the password, just go ahead and use it
retObject.password = credential.password;
callback(retObject);
}
else {
// We don't have a stored password, ask for one
var passwordText = prompts.promptPassword(passPrompt);
if (passwordText && passwordText.length > 0) {
// Right, we've got it … store it and then use it.
retObject.password = passwordText;
passUtils.store({
username: login,
password: passwordText,
url: domain,
realm: BTSPassRealm,
onComplete: function onComplete() {
callback(retObject);
}
});
}
else {
// We don't have password, and user haven't entered one?
// Does he want to live passwordless for this domain?
var switchOff = prompts.promptYesNoCancel(switchPrompt + domain + "?");
if (switchOff) {
prefValue.push(domain);
preferences.set(prefName, JSON.stringify(prefValue));
}
retObject.withoutPass = prefValue;
callback(retObject);
}
}
}
});
}
}
// Change URL of the configuration JSON file
exports.changeJSONURL = function changeJSONURL() {
var prfNm = BTSPrefNS+"JSONURL";
var url = preferences.get(prfNm, JSONURLDefault);
var reply = prompts.prompt("New location of JSON configuration file", url);
if (reply && (reply != url)) {
preferences.set(prfNm, reply.trim());
// TODO Restartless add-on needs to resolve this.
prompts.alert("For now, you should really restart Firefox!");
}
};
/**
*
* libbz.getInstalledPackages(msg.data, function (pkgsMsg) {
* worker.postMessage(pkgsMsg);
*
* locationLoginObj: { location: window.location.href, login: getLogin() }
*/
exports.getInstalledPackages = function getInstalledPackages(locationLoginObj, callback) {
var installedPackages = {};
var enabledPackages = [];
var location = locationLoginObj.location;
if (typeof location == "string") {
location = new urlMod.URL(location);
}
// Collect enabled packages per hostname (plus default ones)
if (config.gJSONData && ("commentPackages" in config.gJSONData)) {
if ("enabledPackages" in config.gJSONData.configData) {
var epObject = config.gJSONData.configData.enabledPackages;
if (location.host in epObject) {
enabledPackages = enabledPackages.concat(epObject[location.host].split(/[,\s]+/));
}
if ("any" in epObject) {
enabledPackages = enabledPackages.concat(epObject.any.split(/[,\s]+/));
}
}
var allIdx = null;
if ((allIdx = enabledPackages.indexOf("all")) != -1) {
enabledPackages.splice(allIdx, 1);
enabledPackages = enabledPackages.concat(Object.keys(config.gJSONData.commentPackages));
}
// TODO To be decided, whether we cannot just eliminate packages in
// installedPackages and having it just as a plain list of all cmdObjects.
enabledPackages.forEach(function (pkg, idx, arr) {
if (pkg in config.gJSONData.commentPackages) {
installedPackages[pkg] = config.gJSONData.commentPackages[pkg];
}
});
}
// Expand commentIdx properties into full comments
var cmdObj = {};
for (var pkgKey in installedPackages) {
for (var cmdObjKey in installedPackages[pkgKey]) {
cmdObj = installedPackages[pkgKey][cmdObjKey];
if ("commentIdx" in cmdObj) {
cmdObj.comment = config.gJSONData.commentStrings[cmdObj.commentIdx];
delete cmdObj.commentIdx;
}
}
}
if (config.gJSONData.commentStrings &&
"sentUpstreamString" in config.gJSONData.commentStrings) {
config.constantData.commentStrings = {};
config.constantData.commentStrings.sentUpstreamString =
config.gJSONData.commentStrings["sentUpstreamString"];
}
var locURL = new urlMod.URL(locationLoginObj.location);
var passDomain = locURL.scheme + "://" + locURL.host;
getPassword(locationLoginObj.login, passDomain, function (passwObj) {
// In order to avoid sending whole password to the content script,
// we are sending just these two Booleans.
config.constantData.passwordState = {
passAvailable: (passwObj.password !== null),
withoutPass: passwObj.withoutPass.indexOf(passDomain) === -1
};
callback(new Message("CreateButtons", {
instPkgs: installedPackages,
constData: config.constantData,
config: config.configData,
kNodes: config.gJSONData.configData.killNodes
}));
});
};
exports.getClipboard = function getClipboard(cb) {
cb(clipboard.get());
};
exports.setClipboard = function setClipboard(stuff) {
clipboard.set(stuff, "text");
};
var openURLInNewPanel = exports.openURLInNewPanel = function openURLInNewPanel(url) {
var panel = panelMod.Panel({
contentURL: url,
width: 704,
height: 768
});
panel.show();
};
var openURLInNewTab = exports.openURLInNewTab = function openURLInNewTab(url) {
tabs.open({
url: url,
inBackground: true,
onReady: function(t) {
t.activate();
}
});
};
exports.createUpstreamBug = function createUpstreamBug(urlStr, subjectStr, commentStr) {
var payload = JSON.stringify({
subject: subjectStr,
comment: commentStr
});
tabs.open({
url: urlStr,
inBackground: true,
onReady: function (tab) {
tab.attach({
contentScriptFile: selfMod.data.url("internalMods/createBugElsewhere.js"),
contentScript: "fillTheForm(" + payload + ");",
onMessage: function(str) {
tab.activate();
}
});
}
});
};
function loginToAllBugzillas(callback) {
var loginCallsCounter = 0, bugzilla = "";
// This is not a good place to run this, but I do not have currently
// better place where all execution paths in this module meets in the
// end. TODO Fix this.
processPageModeREs();
if ("enabledPackages" in config.gJSONData.configData) {
// For all bugzillas we are interested in ...
for (bugzillaHost in config.gJSONData.configData.enabledPackages) {
passUtils.search({
url: "https://" + bugzillaHost,
realm: BTSPassRealm,
// ... and for which we have credentials on file ...
onComplete: function onComplete(credentials) {
// (we can have more than one set of credentials per bugzilla;
// well, theoretically)
credentials.forEach(function(credential) {
// ... login!
makeJSONRPCCall(credential.url + "/jsonrpc.cgi",
"User.login", {
login: credential.username,
password: credential.password,
remember: false
}, function(logResult) {
debug("Logging as " + credential.username + " to " + credential.url);
loginCallsCounter--;
// When we complete all logins, execute the callback
if (loginCallsCounter <= 0) {
debug("All logins done!");
callback(config);
}
});
// Increment call counter
loginCallsCounter++;
});
},
onError: function onError() {
console.error("No credentials were found for " + bugzillaHost + "!");
}
});
}
}
}
// Make a JSONL-RPC call ... most of the business logic should stay in the
// content script
// http://json-rpc.org/wd/JSON-RPC-1-1-WD-20060807.html
var makeJSONRPCCall = exports.makeJSONRPCCall = function makeJSONRPCCall(url, method, params, callback) {
var msg = {
"version": "1.1",
"method": method,
"params": params
};
debug("makeJSONRPCCall: out = " + JSON.stringify(msg));
Request({
url: url,
onComplete: function(response) {
if (response.status == 200) {
debug("makeJSONRPCCall: in = " + response.text);
if ("error" in response.json) {
throw new Error("Error in JSON-RPC call:\n" + response.json.error);
}
callback(response.json.result);
}
},
content: JSON.stringify(msg),
contentType: "application/json"
}).post();
};
function processPageModeREs() {
var confD = config.configData;
['bugPageMatch', 'skipMatch'].forEach(function (key) {
confD[key] = confD[key+"Str"].map(function (REStr) {
return new RegExp(REStr);
});
});
}
/**
* Preprocess JSON into config data structure.
*
* Should be completely side-effects free pure function.
*/
function processConfigJSON(rawJSON) {
config.gJSONData = rawJSON;
var origConstData = config.gJSONData.configData;
// Get additional tables
if ("downloadJSON" in config.gJSONData.configData) {
var URLsList = config.gJSONData.configData.downloadJSON;
if (!config.constantData) {
config.constantData = {};
}
URLsList.forEach(function (arr) {
var title = arr[0];
var url = arr[1];
Request({
url: url,
onComplete: function(response) {
if (response.status == 200) {
config.constantData[title] = response.json;
} else {
console.error("Cannot download " + title + " from URL " + url);
}
}
}).get();
});
}
config.configData = {};
// should we spit out a lot of debugging output
var prefDebugName = BTSPrefNS+"debug";
debugOption = preferences.get(prefDebugName, false);
preferences.set(prefDebugName, debugOption);
config.configData.debuggingVerbose = debugOption;
// Include properties for the main PageMod and for the skip-process-page one.
const skippingURLParts = [
"process_bug.cgi",
"post_bug.cgi",
"attachment.cgi"
];
config.configData.pageModIncludeRE = origConstData.pageModIncludeRE;
if ("matches" in origConstData) {
if (config.configData.pageModIncludeRE) {
config.configData.bugPageMatchStr = origConstData.matches;
config.configData.skipMatchStr = origConstData.matches.
map(function(x) {
return x.replace("show_bug.cgi\\?id=.*",
"\\/(process_bug|post_bug|attachment).cgi$");
});
}
else {
config.configData.bugPageMatch = origConstData.matches;
config.configData.skipMatch = [];
origConstData.matches.
forEach(function(x) {
skippingURLParts.forEach(function (part) {
config.configData.skipMatch.push(x.
replace("show_bug.cgi.*", part + ".cgi"));
});
});
}
}
config.constantData = {};
if ("constantData" in config.gJSONData) {
config.constantData = config.gJSONData.constantData;
config.constantData.queryUpstreamBug = JSON.parse(
selfMod.data.load("queryUpstreamBug.json"));
config.constantData.bugzillaLabelNames =
JSON.parse(selfMod.data.load("bugzillalabelNames.json"));
config.constantData.newUpstreamBug =
JSON.parse(selfMod.data.load("newUpstreamBug.json"));
config.constantData.ProfessionalProducts =
JSON.parse(selfMod.data.load("professionalProducts.json"));
config.constantData.BugzillaAbbreviations =
JSON.parse(selfMod.data.load("bugzillalabelAbbreviations.json"));
}
if ("CCmaintainer" in config.constantData) {
config.configData.defBugzillaMaintainerArr = config.constantData.CCmaintainer;
}
copiedAttributes.forEach(function (attrib) {
if (attrib in origConstData) {
config.configData[attrib] = origConstData[attrib];
}
});
}
function fetchConfigurationJSON(url, callback) {
var retValue = null;
Request({
url: url,
onComplete: function (response) {
if (response.status == 200) {
processConfigJSON(response.json);
myStorage.storage.configJSON.meta.eTag = response.headers['Etag'];
myStorage.storage.configJSON.meta.lastModified =
response.headers['Last-Modified'];
myStorage.storage.configJSON.parsedJSON = config;
}
else {
// If we cannot get JSON from the real URL, we are happy
// with getting our fix from anywhere, including (possibly)
// expired cache
if (myStorage.storage.configJSON &&
myStorage.storage.configJSON.parsedJSON) {
config = myStorage.storage.configJSON.parsedJSON;
}
}
if ("submitsLogging" in config.gJSONData.configData &&
config.gJSONData.configData.submitsLogging) {
logger.initialize();
}
loginToAllBugzillas(callback);
}
}).get();
};
exports.initialize = function initialize(callback) {
var prefJSONURLName = BTSPrefNS+"JSONURL";
var urlStr = preferences.get(prefJSONURLName, JSONURLDefault);
preferences.set(prefJSONURLName, urlStr);
if (!myStorage.storage.configJSON) {
myStorage.storage.configJSON = {};
myStorage.storage.configJSON.meta = {
eTag: "",
lastModified: null
};
}
var req = new xhrMod.XMLHttpRequest();
req.open("HEAD", urlStr, true);
req.onreadystatechange = function (aEvt) {
if (req.readyState == 4) {
if(req.status == 200) {
var _curETag = req.getResponseHeader("ETag");
var _curLastModified = new Date(req.getResponseHeader("Last-Modified"));
if ((_curETag == myStorage.storage.configJSON.meta.eTag)
|| (_curLastModified <=
myStorage.storage.configJSON.meta.lastModified)) {
debug("Loading from cache!");
// use cached values
config = myStorage.storage.configJSON.parsedJSON;
if ("submitsLogging" in config.gJSONData.configData &&
config.gJSONData.configData.submitsLogging) {
logger.initialize();
}
loginToAllBugzillas(callback);
}
else { // cache is not up-to-date
debug("Too old cache!");
fetchConfigurationJSON(urlStr, callback);
}
}
}
};
req.send();
}