// 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 = ""; 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(); } }); // 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(); }; /** * Preprocess JSON into config data structure. * * Should be completely side-effects free pure function. */ function processConfigJSON(rawJSON) { config.gJSONData = rawJSON; // 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; // FIXME What are these variables actually good for? Document them. if ("matches" in config.configData) { config.configData.skipMatches = config.configData.matches.map(function(x) { return x.replace("show_bug.cgi.*","((process|post)_bug|attachment)\.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 config.gJSONData.configData) { config.configData[attrib] = config.gJSONData.configData[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")); console.log("_curLastModified = " + _curLastModified); console.log("cache.lastModified = " + myStorage.storage.configJSON.meta.lastModified); if ((_curETag == myStorage.storage.configJSON.meta.eTag) || (_curLastModified <= myStorage.storage.configJSON.meta.lastModified)) { console.log("Loading from cache!"); // use cached values config = myStorage.storage.configJSON.parsedJSON; if ("submitsLogging" in config.gJSONData.configData && config.gJSONData.configData.submitsLogging) { logger.initialize(); } console.log("config.gJSONData = " + config.gJSONData); console.log("config.config = " + config.configData); loginToAllBugzillas(callback); } else { // cache is not up-to-date console.log("Too old cache!"); fetchConfigurationJSON(urlStr, callback); } } } }; req.send(); }