diff --git a/src/components/passmanager.js b/src/components/passmanager.js
index a13a8d6..3a25b78 100644
--- a/src/components/passmanager.js
+++ b/src/components/passmanager.js
@@ -25,6 +25,8 @@ Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Promise.jsm");
Cu.import("chrome://passmanager/content/subprocess/subprocess.jsm");
+// all these values are handled, but false values
+// are not mapped automatically
const PropertyMap = {
username: true,
password: false,
@@ -35,6 +37,7 @@ const PropertyMap = {
passwordField: true
};
+// vars without "=" are copied from existing environment
const EnvironmentVars = [
"HOME", "USER", "DISPLAY", "PATH",
"GPG_AGENT_INFO",
@@ -54,14 +57,16 @@ function PassManager() {}
PassManager.prototype = {
classID: Components.ID("{1dadf2b7-f243-41b4-a2f2-e53207f29167}"),
QueryInterface: XPCOMUtils.generateQI([Ci.nsILoginManagerStorage]),
- _uiBusy: false,
+
_environment: null,
- _pass: null,
- _realm: null,
+ _propMap: null,
+ _passCmd: "",
+ _realm: "",
_fuzzy: false,
+
_cache: {
defaultLifetime: 300,
- _entries: new Array(),
+ _entries: {},
get: function (key) {
if (this._entries[key]) {
return this._entries[key].value;
@@ -71,13 +76,13 @@ PassManager.prototype = {
add: function (key, value, lifetime) {
lifetime = (lifetime ? lifetime : this.defaultLifetime) * 1000;
if (lifetime > 0) {
- var entry = {
+ let entry = {
value: value,
notify: function (timer) {
this.cache.del(key);
}
};
- var timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+ let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
entry.timer = timer;
entry.cache = this;
this._entries[key] = entry;
@@ -97,15 +102,14 @@ PassManager.prototype = {
}
},
-
- stub: function(arguments) {
+ _stub: function(arguments) {
throw Error('Not yet implemented: ' + arguments.callee.name + '()');
},
- pass: function (args, stdin) {
- var result = null;
+ _pass: function (args, stdin) {
+ let result = null;
pi = {
- command: this._pass,
+ command: this._passCmd,
arguments: args,
charset: "UTF-8",
environment: this._environment,
@@ -113,55 +117,65 @@ PassManager.prototype = {
stdin: stdin,
mergeStderr: false
}
- var p = subprocess.call(pi);
- this._uiBusy = true;
+ let p = subprocess.call(pi);
p.wait();
- this._uiBusy = false;
return result;
},
- sanitizeHostname: function (hostname) {
- return hostname.replace(/^.*:\/\/([^:\/]+)(?:[:\/].*)?$/, "$1");
+ // strip protocol, port and path from URL
+ _sanitizeHostname: function (url) {
+ return url.replace(/^.*:\/\/([^:\/]+)(?:[:\/].*)?$/, "$1");
},
- getHostnamePath: function (hostname) {
+ // strip path from URL
+ _sanitizeURL: function (url) {
+ return url.replace(/^(.*:\/\/[^\/]+)(?:\/.*)?/, "$1");
+ },
+
+ _getHostnamePath: function (hostname) {
if (hostname) {
return this._realm + "/" +
- this.sanitizeHostname(hostname);
+ this._sanitizeHostname(hostname);
}
return this._realm;
},
- loginToStr: function (login) {
- var s = new Array();
+ _loginToStr: function (login) {
+ let s = [];
s.push(login.password);
- for (let prop in PropertyMap) {
- if (PropertyMap[prop] && login[prop]) {
- s.push(PropertyMap[prop][0] + ": " + login[prop]);
+ for (let prop in this._propMap) {
+ if (this._propMap[prop] && login[prop]) {
+ s.push(this._propMap[prop][0] + ": " + login[prop]);
}
}
return s.join("\n");
},
- strToLogin: function (s) {
- var login = new LoginInfo();
- var lines = s.split("\n");
+ _strToLogin: function (s) {
+ let lines = s.split("\n");
+ let re = /^([a-zA-Z]+):\s*(.*)$/;
+
+ // init login with safe values
+ let login = new LoginInfo();
login.username = "";
login.hostname = "";
login.password = lines.shift();
- var re = /^([a-zA-Z]+):\s*(.*)$/;
- var props = new Array();
- for(let i = 0 ; i < lines.length; i++) {
- let match = re.exec(lines[i]);
+
+ // parse input to object
+ let props = {};
+ for each (let line in lines) {
+ let match = re.exec(line);
if(match) {
props[match[1].toLowerCase()] = match[2].trim();
}
}
- for (let prop in PropertyMap) {
- if (PropertyMap[prop]) {
- for (let i = 0; i < PropertyMap[prop].length; i ++) {
- if (props[PropertyMap[prop][i]]) {
- login[prop] = props[PropertyMap[prop][i]];
+
+ // map object to login
+ for (let prop in this._propMap) {
+ if (this._propMap[prop]) {
+ for each (let name in this._propMap[prop]) {
+ if (props[name]) {
+ login[prop] = props[name];
break;
}
}
@@ -170,45 +184,47 @@ PassManager.prototype = {
return login;
},
- saveLogin: function (loginPath, login) {
- this.pass(["insert", "-m", "-f", loginPath],
- this.loginToStr(login));
+ _saveLogin: function (loginPath, login) {
+ this._pass(["insert", "-m", "-f", loginPath],
+ this._loginToStr(login));
+
+ // update cache with logins clone
this._cache.add(loginPath, login.clone());
},
- loadLogin: function (loginPath, autocomplete) {
- var login = this._cache.get(loginPath);
+ _loadLogin: function (loginPath) {
+ let login = this._cache.get(loginPath);
if (!login) {
- var result = this.pass(["show", loginPath]);
+ let result = this._pass(["show", loginPath]);
if (result.exitCode == 0) {
- login = this.strToLogin(result.stdout);
+ login = this._strToLogin(result.stdout);
if (login) {
this._cache.add(loginPath, login);
}
}
}
if (login) {
- if (autocomplete) {
- return autocomplete(login.clone());
- }
+ // always return a clone!
+ // we need the original login cached!
return login.clone();
}
return null;
},
- getLoginPaths: function (hostname, filter, load, autocomplete) {
- var path = this.getHostnamePath(hostname);
- result = this.pass(["ls", path]);
+ // return all paths to logins matching hostname,
+ // all logins if hostname is undefined
+ _getLoginPaths: function (hostname) {
+ let path = this._getHostnamePath(hostname);
+ result = this._pass(["ls", path]);
if (result.exitCode != 0) {
- return new Array();
+ return [];
}
- var lines = result.stdout.split("\n");
- var re = /^(.*[|`]+)-- (.*)$/;
- var logins = new Array();
- var tree = new Array();
- var lastIndent = 0;
- var lastNode = null;
- var lastSaved = false;
+ let lines = result.stdout.split("\n");
+ let re = /^(.*[|`]+)-- (.*)$/;
+ let paths = [];
+ let tree = [];
+ let lastIndent = 0;
+ let lastNode = null;
for(let i = 0 ; i < lines.length; i++) {
let match = re.exec(lines[i]);
if(match) {
@@ -216,212 +232,229 @@ PassManager.prototype = {
if (lastNode) {
if (lastIndent < indent) {
tree.push(lastNode);
- if (lastSaved) {
- logins.pop();
- }
+ paths.pop();
} else if (lastIndent > indent) {
tree.pop();
}
}
lastIndent = indent;
lastNode = match[2];
- lastSaved = true;
- let loginPath = path + "/" +
- tree.concat([match[2]]).join("/");
- let login = null;
- if (filter || load) {
- login = this.loadLogin(loginPath, autocomplete);
- lastSaved = login && (!filter || filter(login));
- }
- if (lastSaved) {
- logins.push(load ? login : loginPath);
- }
+ paths.push(path + "/" +
+ tree.concat([lastNode]).join("/"));
}
}
- return logins;
+ return paths;
},
- filterLogins: function (load, hostname, formSubmitURL, httpRealm) {
- var filter = null;
- var autocomplete = null;
- if (hostname instanceof Ci.nsILoginInfo) {
- var oldLogin = hostname;
- formSubmitURL = oldLogin.formSubmitURL;
- httpRealm = oldLogin.httpRealm;
- hostname = oldLogin.hostname;
- filter = function (login) {
- return this._fuzzy ?
- oldLogin.matches(login) :
- oldLogin.equals(login);
- };
- } else if (formSubmitURL != null || httpRealm != null) {
- filter = function (login) {
- return login.hostname == hostname &&
- (formSubmitURL == null ||
- (formSubmitURL == "" ?
- login.formSubmitURL || this._fuzzy :
- login.formSubmitURL == formSubmitURL)) &&
- (httpRealm == null ||
- (httpRealm == "" ?
- login.httpRealmi || this._fuzzy :
- login.httpRealm == httpRealm))
- };
+ // for fuzzy option
+ _autocomplete: function (login, md) {
+ if (login.hostname) {
+ login.hostname = this._sanitizeURL(login.hostname);
+ } else {
+ // set to unknown for "Saved Passwords..."
+ login.hostname = md.hostname ? md.hostname : "unknown";
}
- if (this._fuzzy) {
- autocomplete = function (login) {
- var cleanURL = function (url) {
- return url.replace(/^(.*:\/\/[^\/]+)(?:\/.*)?/, "$1");
- }
- if (login.hostname) {
- login.hostname = cleanURL(login.hostname);
- } else {
- login.hostname = hostname ? hostname : "unknown";
+ if (login.formSubmitURL) {
+ login.formSubmitURL = this._sanitizeURL(login.formSubmitURL);
+ } else if (!login.formSubmitURL && !login.httpRealm) {
+ if (!md.formSubmitURL && !md.httpRealm) {
+ // no info if protocol or HTML login requested,
+ // choose HTML since we may return an empty string
+ // as wildcard here
+ login.formSubmitURL = "";
+ } else {
+ login.formSubmitURL = md.formSubmitURL;
+ login.httpRealm = md.httpRealm;
+ }
+ }
+ if (!login.usernameField) {
+ login.usernameField = md.usernameField;
+ }
+ if (!login.passwordField) {
+ login.passwordField = md.passwordField;
+ }
+ return login;
+ },
+
+ _filterLogins: function (matchData) {
+ // copy all handled fields from matchData
+ let md = {};
+ for (let prop in this._propMap) {
+ if (prop in matchData) {
+ md[prop] = matchData[prop];
+ }
+ }
+
+ let paths = [];
+ let logins = [];
+
+ for each (let path in this._getLoginPaths(md.hostname)) {
+ let login = this._loadLogin(path);
+ if (login) {
+ if (this._fuzzy) {
+ this._autocomplete(login, md);
}
- if (!login.formSubmitURL && !login.httpRealm) {
- if (!formSubmitURL && httpRealm == null) {
- login.formSubmitURL = login.hostname;
- } else {
- login.formSubmitURL = formSubmitURL;
- login.httpRealm = httpRealm;
+ let matches = true;
+ for (let prop in md) {
+ if (login[prop] != md[prop]) {
+ matches = false;
+ break;
}
- } else if (login.formSubmitURL) {
- login.formSubmitURL = cleanURL(login.formSubmitURL);
}
- return login;
- };
+ if (matches) {
+ paths.push(path);
+ logins.push(login);
+ }
+ }
}
- return this.getLoginPaths(hostname, filter, load, autocomplete);
+ return [logins, paths];
},
- init: function () {
+ // legacy function called by initialize
+ init: function init() {
+ // setup environment
e = Cc["@mozilla.org/process/environment;1"].
getService(Ci.nsIEnvironment)
- this._environment = new Array();
- for (let i = 0; i < EnvironmentVars.length; i++) {
- if (EnvironmentVars[i].indexOf("=") > 0) {
- this._environment.push(EnvironmentVars[i]);
- } else if (e.exists(EnvironmentVars[i])) {
- this._environment.push(EnvironmentVars[i] + "=" +
- e.get(EnvironmentVars[i]));
+ this._environment = [];
+ for each (let env in EnvironmentVars) {
+ if (env.indexOf("=") > 0) {
+ this._environment.push(env);
+ } else if (e.exists(env)) {
+ this._environment.push(env + "=" + e.get(env));
}
}
- var prefObserver = {
+ // load preferences
+ let prefObserver = {
register: function () {
- var prefServ = Cc["@mozilla.org/preferences-service;1"].
+ let prefServ = Cc["@mozilla.org/preferences-service;1"].
getService(Ci.nsIPrefService);
this.branch = prefServ.getBranch("extensions.passmanager.");
this.branch.addObserver("", this, false);
+
+ // initial loading for preferences
this.observe(this.branch);
},
+
observe: function (subject, topic, data) {
- this.pm._pass = subject.getCharPref("pass");
+ this.pm._passCmd = subject.getCharPref("pass");
this.pm._fuzzy = subject.getBoolPref("fuzzy");
this.pm._cache.defaultLifetime = subject.getIntPref("cache");
- var realm = new Array(subject.getCharPref("realm"));
+
+ // construct realm
+ let realm = [subject.getCharPref("realm")];
if (subject.getBoolPref("realm.append_product")) {
realm.push(Services.appinfo.name.toLowerCase());
}
this.pm._realm = realm.join("/");
+
+ // load property map
+ this.pm._propMap = {};
for (prop in PropertyMap) {
if (PropertyMap[prop]) {
- PropertyMap[prop] =
+ this.pm._propMap[prop] =
subject.getCharPref("map." + prop.toLowerCase()).
toLowerCase().split(",");
+ } else {
+ this.pm._propMap[prop] = false;
}
}
- subject.addObserver("", this, false);
}
};
+
prefObserver.pm = this;
prefObserver.register();
},
- initialize: function () {
+ initialize: function initialize() {
this.init();
return Promise.resolve();
},
- terminate: function () {
+ terminate: function terminate() {
return Promise.resolve();
},
addLogin: function addLogin(login) {
- var logins = this.filterLogins(false, login.hostname);
- var path = this.getHostnamePath(login.hostname);
- var re = /\/passmanager([0-9]+)$/;
- var max = 0;
- for (let i = 0; i < logins.length; i ++) {
- let matches = re.exec(logins[i]);
+ let paths = this._getLoginPaths(login.hostname);
+ let re = /\/passmanager([0-9]+)$/;
+ let max = 0;
+ for each (let path in paths) {
+ let matches = re.exec(path);
if (matches && matches[1] > max) {
max = Number(matches[1]);
}
}
- this.saveLogin(path + "/passmanager" + (max + 1), login);
+ let path = this._getHostnamePath(login.hostname);
+ this._saveLogin(path + "/passmanager" + (max + 1), login);
},
removeLogin: function removeLogin(login) {
- var logins = this.filterLogins(false, login);
- for (let i = 0; i < logins.length; i++) {
- let loginPath = logins[i];
- this.pass(["rm", "-f", loginPath]);
- this._cache.del(loginPath);
+ let [logins, paths] = this._filterLogins(login);
+ for each (let path in paths) {
+ this._pass(["rm", "-f", path]);
+ this._cache.del(path);
}
},
modifyLogin: function modifyLogin(oldLogin, newLogin) {
- var logins = this.filterLogins(false, oldLogin);
+ // try to find original login
+ let [logins, paths] = this._filterLogins(oldLogin);
if (logins.length == 0) {
return;
}
+
if (newLogin instanceof Ci.nsIPropertyBag) {
- let propEnum = newLogin.enumerator;
- for (let i = 0; i < logins.length; i++) {
- let login = this.loadLogin(logins[i]);
+ // we know what we are supposed to change,
+ // so we can load the original login, without
+ // autocompletion and only update what's requested
+ for each (let path in paths) {
let changed = false;
+ let login = this._loadLogin(path);
+ let propEnum = newLogin.enumerator;
while (propEnum.hasMoreElements()) {
let prop = propEnum.getNext().QueryInterface(Ci.nsIProperty);
- if (prop.name in PropertyMap &&
+ if (prop.name in this._propMap &&
login[prop.name] != prop.value) {
login[prop.name] = prop.value;
changed = true;
}
}
if (changed) {
- this.saveLogin(logins[i], login);
+ this._saveLogin(path, login);
}
}
} else {
+ // newLogin is nsLoginInfo, copy all properties if changed.
+ // this case does not seem to happen...
let changed = false;
- for (let prop in PropertyMap) {
+ for (let prop in this._propMap) {
if (newLogin[prop] && oldLogin[prop] != newLogin[prop]) {
oldLogin[prop] = newLogin[prop];
changed = true;
}
}
if (changed) {
- for (let i = 0; i < logins.length; i++) {
- this.saveLogin(logins[i], oldLogin);
+ for each (let path in paths) {
+ this._saveLogin(path, oldLogin);
}
}
}
},
getAllLogins: function getAllLogins(count) {
- var ret = this.filterLogins(true);
- if (count) {
- count.value = ret.length;
- }
- return ret;
+ let [logins, paths] = this._filterLogins({});
+ count.value = logins.length;
+ return logins;
},
removeAllLogins: function removeAllLogins() {
- this.pass(["rm", "-r", "-f", this._realm]);
+ this._pass(["rm", "-r", "-f", this._realm]);
this._cache.clear();
},
getAllDisabledHosts: function getAllDisabledHosts(count) {
- this.stub(arguments);
+ this._stub(arguments);
},
getLoginSavingEnabled: function getLoginSavingEnabled(hostname) {
@@ -429,28 +462,53 @@ PassManager.prototype = {
},
setLoginSavingEnabled: function setLoginSavingEnabled(hostname, enabled) {
- this.stub(arguments);
+ this._stub(arguments);
},
- searchLogins: function searchLogins() {
- this.stub(arguments);
+ searchLogins: function searchLogins(count, matchData) {
+ // extract from nsPropertyBag
+ let md = {};
+ let propEnum = matchData.enumerator;
+ while (propEnum.hasMoreElements()) {
+ let prop = propEnum.getNext().QueryInterface(Ci.nsIProperty);
+ md[prop.name] = prop.value;
+ }
+
+ let [logins, paths] = this._filterLogins(md);
+ count.value = logins.length;
+ return logins;
},
findLogins: function findLogins(count, hostname, formSubmitURL, httpRealm) {
- var ret = this.filterLogins(true, hostname, formSubmitURL, httpRealm);
- if (count) {
- count.value = ret.length;
+ let login= {
+ hostname: hostname,
+ formSubmitURL: formSubmitURL,
+ httpRealm: httpRealm
+ };
+ let md = {};
+ for each (let prop in ["hostname", "formSubmitURL", "httpRealm"]) {
+ // empty string means wildcard, null means null
+ if (login[prop] != "") {
+ md[prop] = login[prop];
+ }
}
- return ret;
+
+ let [logins, paths] = this._filterLogins(md);
+ count.value = logins.length;
+ return logins;
},
+ // called to check if its worth calling findLogins,
+ // which may prompt for master password or pinentry in our case
countLogins: function countLogins(hostname, formSubmitURL, httpRealm) {
- var logins = this.filterLogins(false, hostname, formSubmitURL, httpRealm);
- return logins.length;
+ // only way to check if we have logins without
+ // decrypting is by hostname
+ let paths = this._getLoginPaths(hostname);
+ return paths.length;
},
get uiBusy() {
- return this._uiBusy;
+ return false;
},
get isLoggedIn() {
diff --git a/src/install.rdf b/src/install.rdf
index c5929f1..65305f5 100644
--- a/src/install.rdf
+++ b/src/install.rdf
@@ -7,7 +7,7 @@
passmanager@gekmihesg.de
pass-manager
Replace integrated password manager by zx2c4's pass
- 0.3.1
+ 0.4
Markus Weippert
2