aboutsummaryrefslogblamecommitdiffstats
path: root/lib/libbugzilla.js
blob: 18cd2b5ffe075a3e30b785384a949eedd10ba5e7 (plain) (tree)
1
2
3
4
5
6
7
8
9

                                                     
 




                                                 
                               

                                         

                              
                            
                                
                                          

                                                         
                                            

                                           
                                                                                
                                                                                         
                                                                          
 
 
                                 
                       
 
                             
                                                      

                   

 





                     
   

                                                                               
  

                                           


                                       

                                                                   
                                                
                                                                                             
           
                          


   

                                                                                
  
                                                           
   
                                                      
                                                             









                                                                              
         
                                              



                       

 
                                               

                                                    


                                                             
                   
                                                                         
                                                                         

    




















                                                             
         
              
























                                                                                   
           

         

       
 
 
                                            
                                                  
                                  
                                                   






                                                                             


   
  

                                                            
  
                                                                          
   
                                                                                          

















                                                                                          

     

                                                          

                                                                                              
     
 

















                                                                              
     














                                                                       

                                                                  
      
 






                                                   

  
                                                  
                      

  
                                                     
                               

  
                                                                                     

                              

               

               

  
                                                                               






                          
  
 




                                                                                        


                       








                                                                                  

     

  










                                                        
                                        
                                           





                                                                       
                                                         
                                                 
                                                                           


                                       
                                                            
                                                      

                                                                        

                                                             
                                                    
                         






                                                                                     


                                                                    
                                            
                                   

                 

                                     



                                                                               
         



         
 


                                                                          
                                                                                                         
 




                       
 
                                                         
 





                                                        
                                                                             






                                       

  





                                                        
                  
                             
                                                  























                                                                           
 



                                                      
                                                   
 












                                                                               
                                                      


           

                                                                


                                                    
                                                  



                                                        





















                                                                                  

                                                        

     

                 

 







                                                            
                                                
                                                   
                      
           
             

                                     

                                                  

            
                                                                                  








                                                            
 



                                                                
 

                                           
 
// 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 processPageModeREs() {
  var confD = config.configData;
  ['bugPageMatch', 'skipMatch'].forEach(function (key) {
    confD[key] = confD[key+"Str"].map(function (REStr) {
      return new RegExp(REStr);
    });
  });
}


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 (var 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)
          debug("loginToAllBugzillas: credentials found:\n" +
            credentials.toSource());
          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();
};

/**
 * Preprocess JSON into config data structure.
 *
 * Should be completely side-effects free pure function.
 */
function processConfigJSON(rawJSON) {
  var config = {};
  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.bugPageMatchStr = origConstData.matches;
      config.configData.skipMatchStr = [];
      origConstData.matches.
        forEach(function(x) {
          skippingURLParts.forEach(function (part) {
            config.configData.skipMatchStr.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];
    }
  });

  return(config);
}

function ConfigurationLoadError(msg) {
  this.name = "ConfigurationLoadError";
  this.message = msg || "Cannot load configuration!";
}
ConfigurationLoadError.prototype = new Error();
ConfigurationLoadError.constructor = ConfigurationLoadError;


function fetchConfigurationJSON(url, callback) {
  debug("Fetching configuration JSON from " + url);
  var retValue = null;
  Request({
    url: url,
    onComplete: function (response) {
      if (response.status == 200) {
        config = processConfigJSON(response.json);
        debug("Loaded configuration: " + config);
      }
      else {
        throw new ConfigurationLoadError("Cannot load configuration from " + url);
      }
      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);

  debug("Starting initialize!");
  fetchConfigurationJSON(urlStr, callback);
}