From 07a04c6aed73c8efe9e17264e305be2d8d9d5156 Mon Sep 17 00:00:00 2001 From: Ping Chen Date: Tue, 8 Jun 2021 13:24:20 +0300 Subject: [PATCH] Bug 1708349 - Implement nsIAbDirectoryQuery in JS. r=darktrojan Depends on D116805. Differential Revision: https://phabricator.services.mozilla.com/D116966 --HG-- extra : amend_source : a501bf9b43320ed6cdd1873b1bc82ce83164ec87 --- ldap/modules/LDAPMessage.jsm | 15 +- .../addrbook/modules/LDAPDirectoryQuery.jsm | 193 ++++++++++++++++++ .../addrbook/modules/LDAPListenerBase.jsm | 117 +++++++++++ .../addrbook/modules/LDAPModuleLoader.jsm | 5 + .../modules/LDAPReplicationService.jsm | 84 ++------ mailnews/addrbook/modules/moz.build | 2 + 6 files changed, 339 insertions(+), 77 deletions(-) create mode 100644 mailnews/addrbook/modules/LDAPDirectoryQuery.jsm create mode 100644 mailnews/addrbook/modules/LDAPListenerBase.jsm diff --git a/ldap/modules/LDAPMessage.jsm b/ldap/modules/LDAPMessage.jsm index fa9233ac08..bd5b4bc4f1 100644 --- a/ldap/modules/LDAPMessage.jsm +++ b/ldap/modules/LDAPMessage.jsm @@ -164,12 +164,15 @@ class SearchRequest extends LDAPMessage { this._convertFilterToBlock(filter), // attributes new asn1js.Sequence({ - value: attributes.split(",").map( - attr => - new asn1js.OctetString({ - valueHex: new TextEncoder().encode(attr), - }) - ), + value: attributes + .split(",") + .filter(Boolean) + .map( + attr => + new asn1js.OctetString({ + valueHex: new TextEncoder().encode(attr), + }) + ), }), ], }); diff --git a/mailnews/addrbook/modules/LDAPDirectoryQuery.jsm b/mailnews/addrbook/modules/LDAPDirectoryQuery.jsm new file mode 100644 index 0000000000..cad3359a3d --- /dev/null +++ b/mailnews/addrbook/modules/LDAPDirectoryQuery.jsm @@ -0,0 +1,193 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const EXPORTED_SYMBOLS = ["LDAPDirectoryQuery"]; + +const { LDAPListenerBase } = ChromeUtils.import( + "resource:///modules/LDAPListenerBase.jsm" +); + +/** + * Convert a nsIAbBooleanExpression to a filter string. + * @param {nsIAbLDAPAttributeMap} attrMap - A mapping between address book + * properties and ldap attributes. + * @param {nsIAbBooleanExpression} exp - The expression to convert. + * @returns {string} + */ +function boolExpressionToFilter(attrMap, exp) { + let filter = "("; + filter += + { + [Ci.nsIAbBooleanOperationTypes.AND]: "&", + [Ci.nsIAbBooleanOperationTypes.OR]: "|", + [Ci.nsIAbBooleanOperationTypes.NOT]: "!", + }[exp.operation] || ""; + + if (exp.expressions) { + for (let childExp of exp.expressions) { + if (childExp instanceof Ci.nsIAbBooleanExpression) { + filter += boolExpressionToFilter(attrMap, childExp); + } else if (childExp instanceof Ci.nsIAbBooleanConditionString) { + filter += boolConditionToFilter(attrMap, childExp); + } + } + } + + filter += ")"; + return filter; +} + +/** + * Convert a nsIAbBooleanConditionString to a filter string. + * @param {nsIAbLDAPAttributeMap} attrMap - A mapping between addressbook + * properties and ldap attributes. + * @param {nsIAbBooleanConditionString} exp - The expression to convert. + * @returns {string} + */ +function boolConditionToFilter(attrMap, exp) { + let attr = attrMap.getFirstAttribute(exp.name); + switch (exp.condition) { + case Ci.nsIAbBooleanConditionTypes.DoesNotExist: + return `(!(${attr}=*))`; + case Ci.nsIAbBooleanConditionTypes.Exists: + return `(${attr}=*)`; + case Ci.nsIAbBooleanConditionTypes.Contains: + return `(${attr}=*${exp.value}*)`; + case Ci.nsIAbBooleanConditionTypes.DoesNotContain: + return `(!(${attr}=*${exp.value}*))`; + case Ci.nsIAbBooleanConditionTypes.Is: + return `(${attr}=${exp.value})`; + case Ci.nsIAbBooleanConditionTypes.IsNot: + return `(!(${attr}=${exp.value}))`; + case Ci.nsIAbBooleanConditionTypes.BeginsWith: + return `(${attr}=${exp.value}*)`; + case Ci.nsIAbBooleanConditionTypes.EndsWith: + return `(${attr}=*${exp.value})`; + case Ci.nsIAbBooleanConditionTypes.LessThan: + return `(${attr}<=${exp.value})`; + case Ci.nsIAbBooleanConditionTypes.GreaterThan: + return `(${attr}>=${exp.value})`; + case Ci.nsIAbBooleanConditionTypes.SoundsLike: + return `(${attr}~=${exp.value})`; + default: + return ""; + } +} + +/** + * @implements {nsIAbDirectoryQuery} + */ +class LDAPDirectoryQuery extends LDAPListenerBase { + QueryInterface = ChromeUtils.generateQI(["nsIAbDirectoryQuery"]); + + i = 0; + + doQuery(directory, args, listener, limit, timeout) { + this._directory = directory.QueryInterface(Ci.nsIAbLDAPDirectory); + this._listener = listener; + this._attrMap = args.typeSpecificArg; + this._filter = + args.filter || boolExpressionToFilter(this._attrMap, args.expression); + this._limit = limit; + this._timeout = timeout; + + this._connection = Cc[ + "@mozilla.org/network/ldap-connection;1" + ].createInstance(Ci.nsILDAPConnection); + this._operation = Cc[ + "@mozilla.org/network/ldap-operation;1" + ].createInstance(Ci.nsILDAPOperation); + + this._connection.init( + directory.lDAPURL, + directory.authDn, + this, + null, + directory.protocolVersion + ); + return this.i++; + } + + stopQuery(contextId) { + this._operation?.abandonExt(); + } + + /** + * @see nsILDAPMessageListener + */ + onLDAPMessage(msg) { + switch (msg.type) { + case Ci.nsILDAPMessage.RES_BIND: + this._onLDAPBind(msg); + break; + case Ci.nsILDAPMessage.RES_SEARCH_ENTRY: + this._onLDAPSearchEntry(msg); + break; + case Ci.nsILDAPMessage.RES_SEARCH_RESULT: + this._onLDAPSearchResult(msg); + break; + default: + break; + } + } + + /** + * @see nsILDAPMessageListener + */ + onLDAPError(status, secInfo, location) { + this._onSearchFinished(status, secInfo, location); + } + + /** + * @see LDAPListenerBase + */ + _actionOnBindSuccess() { + let ldapUrl = this._directory.lDAPURL; + this._operation.searchExt( + ldapUrl.dn, + ldapUrl.scope, + this._filter, + ldapUrl.attributes, + this._timeout, + this._limit + ); + } + + /** + * @see LDAPListenerBase + */ + _actionOnBindFailure() { + this._onSearchFinished(Cr.NS_ERROR_FAILURE); + } + + /** + * Handler of nsILDAPMessage.RES_SEARCH_ENTRY message. + * @param {nsILDAPMessage} msg - The received LDAP message. + */ + _onLDAPSearchEntry(msg) { + let newCard = Cc["@mozilla.org/addressbook/cardproperty;1"].createInstance( + Ci.nsIAbCard + ); + this._attrMap.setCardPropertiesFromLDAPMessage(msg, newCard); + this._listener.onSearchFoundCard(newCard); + } + + /** + * Handler of nsILDAPMessage.RES_SEARCH_RESULT message. + * @param {nsILDAPMessage} msg - The received LDAP message. + */ + _onLDAPSearchResult(msg) { + this._onSearchFinished( + msg.errorCode == Ci.nsILDAPErrors.SUCCESS ? Cr.NS_OK : Cr.NS_ERROR_FAILURE + ); + } + + _onSearchFinished(status, secInfo, localtion) { + this._listener.onSearchFinished(status, secInfo, localtion); + } +} + +LDAPDirectoryQuery.prototype.classID = Components.ID( + "{5ad5d311-1a50-43db-a03c-63d45f443903}" +); diff --git a/mailnews/addrbook/modules/LDAPListenerBase.jsm b/mailnews/addrbook/modules/LDAPListenerBase.jsm new file mode 100644 index 0000000000..097790dc41 --- /dev/null +++ b/mailnews/addrbook/modules/LDAPListenerBase.jsm @@ -0,0 +1,117 @@ +/* -*- Mode: JavaScript; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const EXPORTED_SYMBOLS = ["LDAPListenerBase"]; + +ChromeUtils.defineModuleGetter( + this, + "Services", + "resource://gre/modules/Services.jsm" +); + +class LDAPListenerBase { + /** + * @see nsILDAPMessageListener + */ + onLDAPInit() { + let outPassword = {}; + if (this._directory.authDn && this._directory.saslMechanism != "GSSAPI") { + // If authDn is set, we're expected to use it to get a password. + let bundle = Services.strings.createBundle( + "chrome://mozldap/locale/ldap.properties" + ); + + let authPrompt = Services.ww.getNewAuthPrompter( + Services.wm.getMostRecentWindow(null) + ); + authPrompt.promptPassword( + bundle.GetStringFromName("authPromptTitle"), + bundle.formatStringFromName("authPromptText", [ + this._directory.lDAPURL.host, + ]), + this._directory.lDAPURL.spec, + Ci.nsIAuthPrompt.SAVE_PASSWORD_PERMANENTLY, + outPassword + ); + } + this._operation.init(this._connection, this, null); + + if (this._directory.saslMechanism != "GSSAPI") { + this._operation.simpleBind(outPassword.value); + return; + } + + // Handle GSSAPI now. + this._operation.saslBind( + `ldap@${this._directory.lDAPURL.host}`, + "GSSAPI", + "sasl-gssapi" + ); + } + + /** + * Handler of nsILDAPMessage.RES_BIND message. + * @param {nsILDAPMessage} msg - The received LDAP message. + */ + _onLDAPBind(msg) { + let errCode = msg.errorCode; + if ( + errCode == Ci.nsILDAPErrors.INAPPROPRIATE_AUTH || + errCode == Ci.nsILDAPErrors.INVALID_CREDENTIALS + ) { + // Login failed, remove any existing login(s). + let ldapUrl = this._directory.lDAPURL; + let logins = Services.logins.findLogins( + ldapUrl.prePath, + "", + ldapUrl.spec + ); + for (let login of logins) { + Services.logins.removeLogin(login); + } + // Trigger the auth prompt. + this.onLDAPInit(); + return; + } + if (errCode != Ci.nsILDAPErrors.SUCCESS) { + this._actionOnBindFailure(); + return; + } + this._actionOnBindSuccess(); + } + + /** + * @see nsILDAPMessageListener + * @abstract + */ + onLDAPMessage() { + throw new Components.Exception( + `${this.constructor.name} does not implement onLDAPMessage.`, + Cr.NS_ERROR_NOT_IMPLEMENTED + ); + } + + /** + * Callback when BindResponse succeeded. + * @abstract + */ + _actionOnBindSuccess() { + throw new Components.Exception( + `${this.constructor.name} does not implement _actionOnBindSuccess.`, + Cr.NS_ERROR_NOT_IMPLEMENTED + ); + } + + /** + * Callback when BindResponse failed. + * @abstract + */ + _actionOnBindFailure() { + throw new Components.Exception( + `${this.constructor.name} does not implement _actionOnBindFailure.`, + Cr.NS_ERROR_NOT_IMPLEMENTED + ); + } +} diff --git a/mailnews/addrbook/modules/LDAPModuleLoader.jsm b/mailnews/addrbook/modules/LDAPModuleLoader.jsm index 4811078b9f..8fba0c5fd6 100644 --- a/mailnews/addrbook/modules/LDAPModuleLoader.jsm +++ b/mailnews/addrbook/modules/LDAPModuleLoader.jsm @@ -38,6 +38,11 @@ var ldapJSModules = [ "{8683e821-f1b0-476d-ac15-07771c79bb11}", "@mozilla.org/addressbook/directory;1?type=moz-abldapdirectory", ], + [ + "LDAPDirectoryQuery", + "{5ad5d311-1a50-43db-a03c-63d45f443903}", + "@mozilla.org/addressbook/ldap-directory-query;1", + ], [ "LDAPReplicationService", "{dbe204e8-ae09-11eb-b4c8-a7e4b3e6e82e}", diff --git a/mailnews/addrbook/modules/LDAPReplicationService.jsm b/mailnews/addrbook/modules/LDAPReplicationService.jsm index 12cc0b7e81..b6a8a25b5e 100644 --- a/mailnews/addrbook/modules/LDAPReplicationService.jsm +++ b/mailnews/addrbook/modules/LDAPReplicationService.jsm @@ -5,21 +5,21 @@ const EXPORTED_SYMBOLS = ["LDAPReplicationService"]; -var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); +const { LDAPListenerBase } = ChromeUtils.import( + "resource:///modules/LDAPListenerBase.jsm" +); /** * A service to replicate a LDAP directory to a local SQLite db. * @implements {nsIAbLDAPReplicationService} * @implements {nsILDAPMessageListener} */ -class LDAPReplicationService { +class LDAPReplicationService extends LDAPListenerBase { QueryInterface = ChromeUtils.generateQI([ "nsIAbLDAPReplicationService", "nsILDAPMessageListener", ]); - _requestNum = 0; - /** * @see nsIAbLDAPReplicationService */ @@ -59,46 +59,6 @@ class LDAPReplicationService { this._done(success); } - /** - * @see nsILDAPMessageListener - */ - onLDAPInit() { - let outPassword = {}; - if (this._directory.authDn && this._directory.saslMechanism != "GSSAPI") { - // If authDn is set, we're expected to use it to get a password. - let bundle = Services.strings.createBundle( - "chrome://mozldap/locale/ldap.properties" - ); - - let authPrompt = Services.ww.getNewAuthPrompter( - Services.wm.getMostRecentWindow(null) - ); - authPrompt.promptPassword( - bundle.GetStringFromName("authPromptTitle"), - bundle.formatStringFromName("authPromptText", [ - this._directory.lDAPURL.host, - ]), - this._directory.lDAPURL.spec, - Ci.nsIAuthPrompt.SAVE_PASSWORD_PERMANENTLY, - outPassword - ); - } - this._operation.init(this._connection, this, null); - this._operation.requestNum = ++this._requestNum; - - if (this._directory.saslMechanism != "GSSAPI") { - this._operation.simpleBind(outPassword.value); - return; - } - - // Handle GSSAPI now. - this._operation.saslBind( - `ldap@${this._directory.lDAPURL.host}`, - "GSSAPI", - "sasl-gssapi" - ); - } - /** * @see nsILDAPMessageListener */ @@ -126,37 +86,12 @@ class LDAPReplicationService { } /** - * Handler of nsILDAPMessage.RES_BIND message. - * @param {nsILDAPMessage} msg - The received LDAP message. + * @see LDAPListenerBase */ - _onLDAPBind(msg) { - let errCode = msg.errorCode; - if ( - errCode == Ci.nsILDAPErrors.INAPPROPRIATE_AUTH || - errCode == Ci.nsILDAPErrors.INVALID_CREDENTIALS - ) { - // Login failed, remove any existing login(s). - let ldapUrl = this._directory.lDAPURL; - let logins = Services.logins.findLogins( - ldapUrl.prePath, - "", - ldapUrl.spec - ); - for (let login of logins) { - Services.logins.removeLogin(login); - } - // Trigger the auth prompt. - this.onLDAPInit(); - return; - } - if (errCode != Ci.nsILDAPErrors.SUCCESS) { - this.done(false); - return; - } + _actionOnBindSuccess() { this._openABForReplicationDir(); let ldapUrl = this._directory.lDAPURL; this._operation.init(this._connection, this, null); - this._operation.requestNum = ++this._requestNum; this._listener.onStateChange( null, null, @@ -173,6 +108,13 @@ class LDAPReplicationService { ); } + /** + * @see LDAPListenerBase + */ + _actionOnBindFailure() { + this._done(false); + } + /** * Handler of nsILDAPMessage.RES_SEARCH_ENTRY message. * @param {nsILDAPMessage} msg - The received LDAP message. diff --git a/mailnews/addrbook/modules/moz.build b/mailnews/addrbook/modules/moz.build index b6cbc0013e..d35edf5dbc 100644 --- a/mailnews/addrbook/modules/moz.build +++ b/mailnews/addrbook/modules/moz.build @@ -11,6 +11,8 @@ EXTRA_JS_MODULES += [ "CardDAVDirectory.jsm", "CardDAVUtils.jsm", "LDAPDirectory.jsm", + "LDAPDirectoryQuery.jsm", + "LDAPListenerBase.jsm", "LDAPModuleLoader.jsm", "LDAPReplicationService.jsm", "QueryStringToExpression.jsm",