Use address books search fields in Contacts module

Searches can now be scoped to one or multiple fields. Those fields are
now dynamic and can be defined using SearchFieldNames in external
contacts sources (SQL and LDAP).
pull/218/merge
Francis Lachapelle 2017-11-21 15:56:16 -05:00
parent aed62cab26
commit eb90760b39
19 changed files with 275 additions and 137 deletions

View File

@ -948,15 +948,15 @@ The returned value *must be unique across the whole SOGo installation*
since it is used to identify the user in the `folder_info` database since it is used to identify the user in the `folder_info` database
table. table.
|MailFieldNames |MailFieldNames (optional)
|An array of fields that returns the user's email addresses (defaults to |An array of fields that returns the user's email addresses (defaults to
`mail` when unset). Note that SOGo will always automatically strip the `mail` when unset). Note that SOGo will always automatically strip the
protocol value from the attribute if the attribute name is `proxyAddresses`. protocol value from the attribute if the attribute name is `proxyAddresses`.
|SearchFieldNames |SearchFieldNames (optional)
|An array of fields to to match against the search string when filtering |An array of fields to match against the search string when filtering
users (defaults to `sn`, `displayName`, and `telephoneNumber` when users (defaults to `sn`, `displayName`, `cn`, `mail`, and `telephoneNumber`
unset). when unset).
|IMAPHostFieldName (optional) |IMAPHostFieldName (optional)
|The field that returns either an URI to the IMAP server as described |The field that returns either an URI to the IMAP server as described
@ -1612,7 +1612,7 @@ SQL source:
[cols="3,47a,50"] [cols="3,47a,50"]
|======================================================================= |=======================================================================
.20+^|D |SOGoUserSources .21+^|D |SOGoUserSources
|Parameter used to set the SQL and/or LDAP sources used for |Parameter used to set the SQL and/or LDAP sources used for
authentication and global address books. Multiple sources can be authentication and global address books. Multiple sources can be
specified as an array of dictionaries. A dictionary that defines a SQL specified as an array of dictionaries. A dictionary that defines a SQL
@ -1688,6 +1688,10 @@ additional email addresses (beside the `mail` column) for each user.
Values must be unique and not appear in more than one column. Values must be unique and not appear in more than one column.
Space-separated values allowed in all *additional* columns (besides in `mail`). Space-separated values allowed in all *additional* columns (besides in `mail`).
|SearchFieldNames (optional)
|An array of fields to match against the search string when filtering
users (defaults to `c_cn` and `mail` when unset).
|IMAPHostFieldName (optional) |IMAPHostFieldName (optional)
|The field that returns the IMAP hostname for the user. |The field that returns the IMAP hostname for the user.

2
NEWS
View File

@ -5,6 +5,7 @@ New features
- [core] can now invite attendees to exceptions only (#2561) - [core] can now invite attendees to exceptions only (#2561)
- [core] add support for module constraints in SQL sources - [core] add support for module constraints in SQL sources
- [core] add support for listRequiresDot in SQL sources - [core] add support for listRequiresDot in SQL sources
- [web] add support for SearchFieldNames in SQL sources
- [web] display freebusy information of owner in appointment editor - [web] display freebusy information of owner in appointment editor
- [web] register SOGo as a handler for the mailto scheme (#1223) - [web] register SOGo as a handler for the mailto scheme (#1223)
- [web] new events list view where events are grouped by day - [web] new events list view where events are grouped by day
@ -15,6 +16,7 @@ Enhancements
- [web] added Simplified Chinese (zh_CN) translation - thanks to Thomas Kuiper - [web] added Simplified Chinese (zh_CN) translation - thanks to Thomas Kuiper
- [web] now also give modify permission when selecting all calendar rights - [web] now also give modify permission when selecting all calendar rights
- [web] allow edition of IMAP flags associated to mail labels - [web] allow edition of IMAP flags associated to mail labels
- [web] search scope of address book is now respected
Bug fixes Bug fixes
- [core] yearly repeating events are not shown in web calendar (#4237) - [core] yearly repeating events are not shown in web calendar (#4237)

View File

@ -43,7 +43,7 @@
@protocol SOGoContactFolder <NSObject> @protocol SOGoContactFolder <NSObject>
- (NSArray *) lookupContactsWithFilter: (NSString *) filter - (NSArray *) lookupContactsWithFilter: (NSString *) filter
onCriteria: (NSString *) criteria onCriteria: (NSArray *) criteria
sortBy: (NSString *) sortKey sortBy: (NSString *) sortKey
ordering: (NSComparisonResult) sortOrdering ordering: (NSComparisonResult) sortOrdering
inDomain: (NSString *) domain; inDomain: (NSString *) domain;

View File

@ -459,7 +459,7 @@ Class SOGoContactSourceFolderK;
folder = [sortedFolders objectAtIndex: i]; folder = [sortedFolders objectAtIndex: i];
//NSLog(@" Address book: %@ (%@)", [folder displayName], [folder class]); //NSLog(@" Address book: %@ (%@)", [folder displayName], [folder class]);
contacts = [folder lookupContactsWithFilter: theFilter contacts = [folder lookupContactsWithFilter: theFilter
onCriteria: @"name_or_address" onCriteria: nil
sortBy: @"c_cn" sortBy: @"c_cn"
ordering: NSOrderedAscending ordering: NSOrderedAscending
inDomain: domain]; inDomain: domain];

View File

@ -37,7 +37,7 @@
} }
- (void) fixupContactRecord: (NSMutableDictionary *) contactRecord; - (void) fixupContactRecord: (NSMutableDictionary *) contactRecord;
- (EOQualifier *) qualifierForFilter: (NSString *) filter - (EOQualifier *) qualifierForFilter: (NSString *) filter
onCriteria: (NSString *) criteria; onCriteria: (NSArray *) criteria;
- (NSDictionary *) lookupContactWithName: (NSString *) aName; - (NSDictionary *) lookupContactWithName: (NSString *) aName;
- (NSArray *) lookupContactsWithQualifier: (EOQualifier *) qualifier; - (NSArray *) lookupContactsWithQualifier: (EOQualifier *) qualifier;
- (NSArray *) lookupContactsFields: (NSArray *) fields - (NSArray *) lookupContactsFields: (NSArray *) fields

View File

@ -79,6 +79,20 @@ static NSArray *folderListingFields = nil;
[super dealloc]; [super dealloc];
} }
- (NSArray *) searchFields
{
static NSArray *searchFields = nil;
if (!searchFields)
{
// "name" expands to c_sn, c_givenname and c_cn
searchFields = [NSArray arrayWithObjects: @"name", @"c_mail", @"c_categories", @"c_o", nil];
[searchFields retain];
}
return searchFields;
}
- (Class) objectClassForContent: (NSString *) content - (Class) objectClassForContent: (NSString *) content
{ {
CardGroup *cardEntry; CardGroup *cardEntry;
@ -183,39 +197,42 @@ static NSArray *folderListingFields = nil;
} }
- (EOQualifier *) qualifierForFilter: (NSString *) filter - (EOQualifier *) qualifierForFilter: (NSString *) filter
onCriteria: (NSString *) criteria onCriteria: (NSArray *) criteria
{ {
NSString *qs; NSEnumerator *criteriaList;
NSMutableArray *filters;
NSString *filterFormat, *currentCriteria, *qs;
EOQualifier *qualifier; EOQualifier *qualifier;
qualifier = nil;
if ([filter length] > 0) if ([filter length] > 0)
{ {
filter = [filter asSafeSQLString]; filter = [filter asSafeSQLString];
if ([criteria isEqualToString: @"name_or_address"]) filters = [NSMutableArray array];
qs = [NSString stringWithFormat: filterFormat = [NSString stringWithFormat: @"(%%@ isCaseInsensitiveLike: '%%%%%@%%%%')", filter];
@"(c_sn isCaseInsensitiveLike: '%%%@%%') OR " if (criteria)
@"(c_givenname isCaseInsensitiveLike: '%%%@%%') OR " criteriaList = [criteria objectEnumerator];
@"(c_cn isCaseInsensitiveLike: '%%%@%%') OR "
@"(c_mail isCaseInsensitiveLike: '%%%@%%')",
filter, filter, filter, filter];
else if ([criteria isEqualToString: @"category"])
qs = [NSString stringWithFormat:
@"(c_categories isCaseInsensitiveLike: '%%%@%%')",
filter];
else if ([criteria isEqualToString: @"organization"])
qs = [NSString stringWithFormat:
@"(c_o isCaseInsensitiveLike: '%%%@%%')",
filter];
else else
qs = @"(1 == 0)"; criteriaList = [[self searchFields] objectEnumerator];
if (qs) while (( currentCriteria = [criteriaList nextObject] ))
qualifier = [EOQualifier qualifierWithQualifierFormat: qs]; {
else if ([currentCriteria isEqualToString: @"name"])
qualifier = nil; {
[filters addObject: @"c_sn"];
[filters addObject: @"c_givenname"];
[filters addObject: @"c_cn"];
}
else if ([[self searchFields] containsObject: currentCriteria])
[filters addObject: currentCriteria];
}
if ([filters count])
{
qs = [[[filters uniqueObjects] stringsWithFormat: filterFormat] componentsJoinedByString: @" OR "];
qualifier = [EOQualifier qualifierWithQualifierFormat: qs];
}
} }
else
qualifier = nil;
return qualifier; return qualifier;
} }
@ -357,7 +374,7 @@ static NSArray *folderListingFields = nil;
* The domain is therefore ignored. * The domain is therefore ignored.
*/ */
- (NSArray *) lookupContactsWithFilter: (NSString *) filter - (NSArray *) lookupContactsWithFilter: (NSString *) filter
onCriteria: (NSString *) criteria onCriteria: (NSArray *) criteria
sortBy: (NSString *) sortKey sortBy: (NSString *) sortKey
ordering: (NSComparisonResult) sortOrdering ordering: (NSComparisonResult) sortOrdering
inDomain: (NSString *) domain inDomain: (NSString *) domain

View File

@ -115,6 +115,11 @@
return isPersonalSource; return isPersonalSource;
} }
- (NSArray *) searchFields
{
return [source searchFields];
}
- (BOOL) listRequiresDot - (BOOL) listRequiresDot
{ {
return [source listRequiresDot]; return [source listRequiresDot];
@ -391,7 +396,7 @@
} }
- (NSArray *) lookupContactsWithFilter: (NSString *) filter - (NSArray *) lookupContactsWithFilter: (NSString *) filter
onCriteria: (NSString *) criteria onCriteria: (NSArray *) criteria
sortBy: (NSString *) sortKey sortBy: (NSString *) sortKey
ordering: (NSComparisonResult) sortOrdering ordering: (NSComparisonResult) sortOrdering
inDomain: (NSString *) domain inDomain: (NSString *) domain
@ -401,10 +406,10 @@
result = nil; result = nil;
if (([filter length] > 0 && [criteria isEqualToString: @"name_or_address"]) if ([filter length] > 0 || ![source listRequiresDot])
|| ![source listRequiresDot])
{ {
records = [source fetchContactsMatching: filter records = [source fetchContactsMatching: filter
withCriteria: criteria
inDomain: domain]; inDomain: domain];
[childRecords setObjects: records [childRecords setObjects: records
forKeys: [records objectsForKey: @"c_name" forKeys: [records objectsForKey: @"c_name"

View File

@ -74,8 +74,6 @@
NSDictionary *modulesConstraints; NSDictionary *modulesConstraints;
NSMutableArray *searchAttributes;
BOOL passwordPolicy; BOOL passwordPolicy;
BOOL updateSambaNTLMPasswords; BOOL updateSambaNTLMPasswords;

View File

@ -92,7 +92,9 @@ static Class NSStringK;
mailFields = [NSArray arrayWithObject: @"mail"]; mailFields = [NSArray arrayWithObject: @"mail"];
[mailFields retain]; [mailFields retain];
contactMapping = nil; contactMapping = nil;
searchFields = [NSArray arrayWithObjects: @"sn", @"displayname", @"telephonenumber", nil]; // "mail" expands to all entries of MailFieldNames
// "name" expands to sn, displayname and cn
searchFields = [NSArray arrayWithObjects: @"name", @"mail", @"telephonenumber", nil];
[searchFields retain]; [searchFields retain];
groupObjectClasses = [NSArray arrayWithObjects: @"group", @"groupofnames", @"groupofuniquenames", @"posixgroup", nil]; groupObjectClasses = [NSArray arrayWithObjects: @"group", @"groupofnames", @"groupofuniquenames", @"posixgroup", nil];
[groupObjectClasses retain]; [groupObjectClasses retain];
@ -105,7 +107,6 @@ static Class NSStringK;
_userPasswordAlgorithm = nil; _userPasswordAlgorithm = nil;
listRequiresDot = YES; listRequiresDot = YES;
searchAttributes = nil;
passwordPolicy = NO; passwordPolicy = NO;
updateSambaNTLMPasswords = NO; updateSambaNTLMPasswords = NO;
@ -149,7 +150,6 @@ static Class NSStringK;
[sourceID release]; [sourceID release];
[modulesConstraints release]; [modulesConstraints release];
[_scope release]; [_scope release];
[searchAttributes release];
[domain release]; [domain release];
[kindField release]; [kindField release];
[multipleBookingsField release]; [multipleBookingsField release];
@ -373,6 +373,11 @@ groupObjectClasses: (NSArray *) newGroupObjectClasses
return listRequiresDot; return listRequiresDot;
} }
- (NSArray *) searchFields
{
return searchFields;
}
- (void) setContactMapping: (NSDictionary *) newMapping - (void) setContactMapping: (NSDictionary *) newMapping
andObjectClasses: (NSArray *) newObjectClasses andObjectClasses: (NSArray *) newObjectClasses
{ {
@ -761,41 +766,57 @@ groupObjectClasses: (NSArray *) newGroupObjectClasses
* @return a EOQualifier matching the filter * @return a EOQualifier matching the filter
*/ */
- (EOQualifier *) _qualifierForFilter: (NSString *) filter - (EOQualifier *) _qualifierForFilter: (NSString *) filter
onCriteria: (NSArray *) criteria
{ {
NSEnumerator *criteriaList;
NSMutableArray *fields; NSMutableArray *fields;
NSString *fieldFormat, *searchFormat, *escapedFilter; NSString *fieldFormat, *currentCriteria, *searchFormat, *escapedFilter;
EOQualifier *qualifier; EOQualifier *qualifier;
NSMutableString *qs; NSMutableString *qs;
escapedFilter = SafeLDAPCriteria(filter); escapedFilter = SafeLDAPCriteria(filter);
if ([escapedFilter length] > 0) qs = [NSMutableString string];
if (([escapedFilter length] == 0 && !listRequiresDot) || [escapedFilter isEqualToString: @"."])
{ {
qs = [NSMutableString string]; [qs appendFormat: @"(%@='*')", CNField];
if ([escapedFilter isEqualToString: @"."]) }
[qs appendFormat: @"(%@='*')", CNField]; else
{
fieldFormat = [NSString stringWithFormat: @"(%%@='*%@*')", escapedFilter];
if (criteria)
criteriaList = [criteria objectEnumerator];
else else
criteriaList = [[self searchFields] objectEnumerator];
fields = [NSMutableArray array];
while (( currentCriteria = [criteriaList nextObject] ))
{ {
fieldFormat = [NSString stringWithFormat: @"(%%@='*%@*')", escapedFilter]; if ([currentCriteria isEqualToString: @"name"])
fields = [NSMutableArray arrayWithArray: searchFields]; {
[fields addObjectsFromArray: mailFields]; [fields addObject: @"sn"];
[fields addObject: CNField]; [fields addObject: @"displayname"];
searchFormat = [[[fields uniqueObjects] stringsWithFormat: fieldFormat] [fields addObject: @"cn"];
componentsJoinedByString: @" OR "]; }
[qs appendString: searchFormat]; else if ([currentCriteria isEqualToString: @"mail"])
{
// Expand to all mail fields
[fields addObject: currentCriteria];
[fields addObjectsFromArray: mailFields];
}
else if ([[self searchFields] containsObject: currentCriteria])
[fields addObject: currentCriteria];
} }
if (_filter && [_filter length]) searchFormat = [[[fields uniqueObjects] stringsWithFormat: fieldFormat] componentsJoinedByString: @" OR "];
[qs appendFormat: @" AND %@", _filter]; [qs appendString: searchFormat];
}
qualifier = [EOQualifier qualifierWithQualifierFormat: qs]; if (_filter && [_filter length])
} [qs appendFormat: @" AND %@", _filter];
else if (!listRequiresDot)
{ if ([qs length])
qs = [NSMutableString stringWithFormat: @"(%@='*')", CNField]; qualifier = [EOQualifier qualifierWithQualifierFormat: qs];
if ([_filter length])
[qs appendFormat: @" AND %@", _filter];
qualifier = [EOQualifier qualifierWithQualifierFormat: qs];
}
else else
qualifier = nil; qualifier = nil;
@ -832,6 +853,7 @@ groupObjectClasses: (NSArray *) newGroupObjectClasses
return [EOQualifier qualifierWithQualifierFormat: qs]; return [EOQualifier qualifierWithQualifierFormat: qs];
} }
/*
- (NSArray *) _constraintsFields - (NSArray *) _constraintsFields
{ {
NSMutableArray *fields; NSMutableArray *fields;
@ -845,6 +867,7 @@ groupObjectClasses: (NSArray *) newGroupObjectClasses
return fields; return fields;
} }
*/
/* This is required for SQL sources when DomainFieldName is enabled. /* This is required for SQL sources when DomainFieldName is enabled.
* For LDAP, simply discard the domain and call the original method */ * For LDAP, simply discard the domain and call the original method */
@ -1202,6 +1225,7 @@ groupObjectClasses: (NSArray *) newGroupObjectClasses
} }
- (NSArray *) fetchContactsMatching: (NSString *) match - (NSArray *) fetchContactsMatching: (NSString *) match
withCriteria: (NSArray *) criteria
inDomain: (NSString *) domain inDomain: (NSString *) domain
{ {
NGLdapConnection *ldapConnection; NGLdapConnection *ldapConnection;
@ -1216,7 +1240,7 @@ groupObjectClasses: (NSArray *) newGroupObjectClasses
if ([match length] > 0 || !listRequiresDot) if ([match length] > 0 || !listRequiresDot)
{ {
ldapConnection = [self _ldapConnection]; ldapConnection = [self _ldapConnection];
qualifier = [self _qualifierForFilter: match]; qualifier = [self _qualifierForFilter: match onCriteria: criteria];
attributes = [NSArray arrayWithObject: @"*"]; attributes = [NSArray arrayWithObject: @"*"];
if ([_scope caseInsensitiveCompare: @"BASE"] == NSOrderedSame) if ([_scope caseInsensitiveCompare: @"BASE"] == NSOrderedSame)

View File

@ -39,6 +39,7 @@
inDomain: (NSString *) domain; inDomain: (NSString *) domain;
- (NSString *) domain; - (NSString *) domain;
- (NSArray *) searchFields;
/* requires a "." to obtain the full list of contacts */ /* requires a "." to obtain the full list of contacts */
- (void) setListRequiresDot: (BOOL) aBool; - (void) setListRequiresDot: (BOOL) aBool;
@ -63,6 +64,7 @@
- (NSArray *) allEntryIDs; - (NSArray *) allEntryIDs;
- (NSArray *) allEntryIDsVisibleFromDomain: (NSString *) domain; - (NSArray *) allEntryIDsVisibleFromDomain: (NSString *) domain;
- (NSArray *) fetchContactsMatching: (NSString *) filter - (NSArray *) fetchContactsMatching: (NSString *) filter
withCriteria: (NSArray *) criteria
inDomain: (NSString *) domain; inDomain: (NSString *) domain;
- (void) setSourceID: (NSString *) newSourceID; - (void) setSourceID: (NSString *) newSourceID;

View File

@ -40,6 +40,7 @@
NSString *_authenticationFilter; NSString *_authenticationFilter;
NSArray *_loginFields; NSArray *_loginFields;
NSArray *_mailFields; NSArray *_mailFields;
NSArray *_searchFields;
NSString *_imapLoginField; NSString *_imapLoginField;
NSString *_imapHostField; NSString *_imapHostField;
NSString *_sieveHostField; NSString *_sieveHostField;

View File

@ -1,9 +1,8 @@
/* SQLSource.h - this file is part of SOGo /* SQLSource.h - this file is part of SOGo
* *
* Copyright (C) 2009-2012 Inverse inc. * Copyright (C) 2009-2017 Inverse inc.
* *
* Authors: Ludovic Marcotte <lmarcotte@inverse.ca> * This file is part of SOGo.
* Francis Lachapelle <flachapelle@inverse.ca>
* *
* This file is free software; you can redistribute it and/or modify * This file is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -35,6 +34,7 @@
#import <SOGo/SOGoSystemDefaults.h> #import <SOGo/SOGoSystemDefaults.h>
#import "NSArray+Utilities.h"
#import "NSString+Utilities.h" #import "NSString+Utilities.h"
#import "NSString+Crypto.h" #import "NSString+Crypto.h"
@ -90,6 +90,9 @@
_authenticationFilter = nil; _authenticationFilter = nil;
_loginFields = nil; _loginFields = nil;
_mailFields = nil; _mailFields = nil;
// "mail" expands to all entries of MailFieldNames
_searchFields = [NSArray arrayWithObjects: @"c_cn", @"mail", nil];
[_searchFields retain];
_userPasswordAlgorithm = nil; _userPasswordAlgorithm = nil;
_viewURL = nil; _viewURL = nil;
_kindField = nil; _kindField = nil;
@ -109,6 +112,7 @@
[_authenticationFilter release]; [_authenticationFilter release];
[_loginFields release]; [_loginFields release];
[_mailFields release]; [_mailFields release];
[_searchFields release];
[_userPasswordAlgorithm release]; [_userPasswordAlgorithm release];
[_viewURL release]; [_viewURL release];
[_kindField release]; [_kindField release];
@ -140,6 +144,8 @@
ASSIGN(_multipleBookingsField, [udSource objectForKey: @"MultipleBookingsFieldName"]); ASSIGN(_multipleBookingsField, [udSource objectForKey: @"MultipleBookingsFieldName"]);
ASSIGN(_domainField, [udSource objectForKey: @"DomainFieldName"]); ASSIGN(_domainField, [udSource objectForKey: @"DomainFieldName"]);
ASSIGN(_modulesConstraints, [udSource objectForKey: @"ModulesConstraints"]); ASSIGN(_modulesConstraints, [udSource objectForKey: @"ModulesConstraints"]);
if ([udSource objectForKey: @"SearchFieldNames"])
ASSIGN(_searchFields, [udSource objectForKey: @"SearchFieldNames"]);
if ([udSource objectForKey: @"prependPasswordScheme"]) if ([udSource objectForKey: @"prependPasswordScheme"])
_prependPasswordScheme = [[udSource objectForKey: @"prependPasswordScheme"] boolValue]; _prependPasswordScheme = [[udSource objectForKey: @"prependPasswordScheme"] boolValue];
else else
@ -157,7 +163,7 @@
#warning this domain code has no effect yet #warning this domain code has no effect yet
if ([sourceDomain length]) if ([sourceDomain length])
ASSIGN (_domain, sourceDomain); ASSIGN(_domain, sourceDomain);
if (!_viewURL) if (!_viewURL)
{ {
@ -173,6 +179,11 @@
return _domain; return _domain;
} }
- (NSArray *) searchFields
{
return _searchFields;
}
- (BOOL) _isPassword: (NSString *) plainPassword - (BOOL) _isPassword: (NSString *) plainPassword
equalTo: (NSString *) encryptedPassword equalTo: (NSString *) encryptedPassword
{ {
@ -369,6 +380,7 @@
return didChange; return didChange;
} }
/*
- (NSString *) _whereClauseFromArray: (NSArray *) theArray - (NSString *) _whereClauseFromArray: (NSArray *) theArray
value: (NSString *) theValue value: (NSString *) theValue
exact: (BOOL) theBOOL exact: (BOOL) theBOOL
@ -388,6 +400,7 @@
return s; return s;
} }
*/
- (void) _fillConstraintsForModule: (NSString *) module - (void) _fillConstraintsForModule: (NSString *) module
intoRecord: (NSMutableDictionary *) record intoRecord: (NSMutableDictionary *) record
@ -562,7 +575,7 @@
[emails addObjectsFromArray: [s componentsSeparatedByString: @" "]]; [emails addObjectsFromArray: [s componentsSeparatedByString: @" "]];
} }
[response setObject: emails forKey: @"c_emails"]; [response setObject: [emails uniqueObjects] forKey: @"c_emails"];
if (_imapHostField) if (_imapHostField)
{ {
value = [response objectForKey: _imapHostField]; value = [response objectForKey: _imapHostField];
@ -792,14 +805,16 @@
} }
- (NSArray *) fetchContactsMatching: (NSString *) filter - (NSArray *) fetchContactsMatching: (NSString *) filter
withCriteria: (NSArray *) criteria
inDomain: (NSString *) domain inDomain: (NSString *) domain
{ {
EOAdaptorChannel *channel; EOAdaptorChannel *channel;
NSMutableArray *results; NSEnumerator *criteriaList;
NSMutableArray *fields, *results;
GCSChannelManager *cm; GCSChannelManager *cm;
NSException *ex; NSException *ex;
NSMutableString *sql; NSMutableString *sql;
NSString *lowerFilter; NSString *lowerFilter, *filterFormat, *currentCriteria, *qs;
results = [NSMutableArray array]; results = [NSMutableArray array];
@ -809,22 +824,40 @@
channel = [cm acquireOpenChannelForURL: _viewURL]; channel = [cm acquireOpenChannelForURL: _viewURL];
if (channel) if (channel)
{ {
lowerFilter = [filter lowercaseString]; fields = [NSMutableArray array];
lowerFilter = [lowerFilter stringByReplacingString: @"'" withString: @"''"]; if ([filter length])
sql = [NSMutableString stringWithFormat: (@"SELECT *"
@" FROM %@"
@" WHERE"
@" (LOWER(c_cn) LIKE '%%%@%%'"
@" OR LOWER(mail) LIKE '%%%@%%'"),
[_viewURL gcsTableName],
lowerFilter, lowerFilter];
if (_mailFields && [_mailFields count] > 0)
{ {
[sql appendString: [self _whereClauseFromArray: _mailFields value: lowerFilter exact: NO]]; lowerFilter = [filter lowercaseString];
lowerFilter = [lowerFilter asSafeSQLString];
filterFormat = [NSString stringWithFormat: @"LOWER(%%@) LIKE '%%%%%@%%%%'", lowerFilter];
if (criteria)
criteriaList = [criteria objectEnumerator];
else
criteriaList = [[self searchFields] objectEnumerator];
while (( currentCriteria = [criteriaList nextObject] ))
{
if ([currentCriteria isEqualToString: @"mail"])
{
// Expand to all mail fields
[fields addObject: currentCriteria];
if (_mailFields)
[fields addObjectsFromArray: _mailFields];
}
else if ([[self searchFields] containsObject: currentCriteria])
[fields addObject: currentCriteria];
}
} }
sql = [NSMutableString stringWithFormat: @"SELECT * FROM %@ WHERE (", [_viewURL gcsTableName]];
if ([fields count])
{
qs = [[[fields uniqueObjects] stringsWithFormat: filterFormat] componentsJoinedByString: @" OR "];
[sql appendString: qs];
}
else
[sql appendString: @"1 = 1"];
[sql appendString: @")"]; [sql appendString: @")"];
if (_domainField) if (_domainField)
@ -832,8 +865,7 @@
if ([domain length]) if ([domain length])
{ {
EOQualifier *domainQualifier; EOQualifier *domainQualifier;
domainQualifier = domainQualifier = [self _visibleDomainsQualifierFromDomain: domain];
[self _visibleDomainsQualifierFromDomain: domain];
if (domainQualifier) if (domainQualifier)
{ {
[sql appendFormat: @" AND ("]; [sql appendFormat: @" AND ("];

View File

@ -45,6 +45,30 @@
"Carbon Copy (Cc)" = "Carbon Copy (Cc)"; "Carbon Copy (Cc)" = "Carbon Copy (Cc)";
"Blind Carbon Copy (Bcc)" = "Blind Carbon Copy (Bcc)"; "Blind Carbon Copy (Bcc)" = "Blind Carbon Copy (Bcc)";
/* Search scope: name fields */
"name" = "Name";
/* Search scope: name fields */
"c_cn" = "Name";
/* Search scope: mail fields */
"mail" = "Mail";
/* Search scope: mail fields */
"c_mail" = "Mail";
/* Search scope: telephone field */
"telephonenumber" = "Telephone";
/* Search scope: categories field */
"c_categories" = "Categories";
/* Search scope: categories field */
"vcardcategories" = "Categories";
/* Search scope: organization field */
"c_o" = "Organization";
/* Subheader of empty addressbook */ /* Subheader of empty addressbook */
"No contact" = "No contact"; "No contact" = "No contact";

View File

@ -199,7 +199,7 @@ Class SOGoContactSourceFolderK, SOGoGCSFolderK;
folder = [sortedFolders objectAtIndex: i]; folder = [sortedFolders objectAtIndex: i];
//NSLog(@" Address book: %@ (%@)", [folder displayName], [folder class]); //NSLog(@" Address book: %@ (%@)", [folder displayName], [folder class]);
contacts = [folder lookupContactsWithFilter: searchText contacts = [folder lookupContactsWithFilter: searchText
onCriteria: @"name_or_address" onCriteria: nil
sortBy: @"c_cn" sortBy: @"c_cn"
ordering: NSOrderedAscending ordering: NSOrderedAscending
inDomain: domain]; inDomain: domain];
@ -343,6 +343,7 @@ Class SOGoContactSourceFolderK, SOGoGCSFolderK;
&& [currentFolder listRequiresDot]], @"listRequiresDot", && [currentFolder listRequiresDot]], @"listRequiresDot",
acls, @"acls", acls, @"acls",
urls, @"urls", urls, @"urls",
[currentFolder searchFields], @"searchFields",
nil]; nil];
[foldersAttrs addObject: folderAttrs]; [foldersAttrs addObject: folderAttrs];
} }

View File

@ -31,6 +31,8 @@
@interface UIxContactsListActions : SOGoDirectAction @interface UIxContactsListActions : SOGoDirectAction
{ {
NSDictionary *requestData;
NSDictionary *currentContact; NSDictionary *currentContact;
NSArray *contactInfos; NSArray *contactInfos;

View File

@ -54,6 +54,7 @@
{ {
if ((self = [super init])) if ((self = [super init]))
{ {
requestData = nil;
contactInfos = nil; contactInfos = nil;
sortedIDs = nil; sortedIDs = nil;
} }
@ -63,6 +64,7 @@
- (void) dealloc - (void) dealloc
{ {
[requestData release];
[contactInfos release]; [contactInfos release];
[sortedIDs release]; [sortedIDs release];
[super dealloc]; [super dealloc];
@ -70,6 +72,20 @@
/* accessors */ /* accessors */
- (NSDictionary *) requestData
{
WORequest *rq;
if (!requestData)
{
rq = [context request];
requestData = [[rq contentAsString] objectFromJSONString];
[requestData retain];
}
return requestData;
}
- (NSString *) defaultSortKey - (NSString *) defaultSortKey
{ {
return @"c_cn"; return @"c_cn";
@ -78,7 +94,6 @@
- (NSString *) sortKey - (NSString *) sortKey
{ {
NSString *s; NSString *s;
WORequest *rq;
static NSArray *sortKeys = nil; static NSArray *sortKeys = nil;
if (!sortKeys) if (!sortKeys)
@ -88,8 +103,7 @@
[sortKeys retain]; [sortKeys retain];
} }
rq = [context request]; s = [[self requestData] objectForKey: @"sort"];
s = [rq formValueForKey: @"sort"];
if (![s length] || ![sortKeys containsObject: s]) if (![s length] || ![sortKeys containsObject: s])
s = [self defaultSortKey]; s = [self defaultSortKey];
@ -102,8 +116,8 @@
NSString *ascending, *sort; NSString *ascending, *sort;
SOGoUserSettings *us; SOGoUserSettings *us;
sort = [[context request] formValueForKey: @"sort"]; sort = [[self requestData] objectForKey: @"sort"];
ascending = [[context request] formValueForKey: @"asc"]; ascending = [[self requestData] objectForKey: @"asc"];
if ([sort length]) if ([sort length])
{ {
@ -125,14 +139,13 @@
- (NSArray *) contactInfos - (NSArray *) contactInfos
{ {
id <SOGoContactFolder> folder; id <SOGoContactFolder> folder;
NSString *ascending, *searchText, *valueText; NSString *ascending, *valueText;
NSArray *results, *fields; NSArray *results, *searchFields, *fields;
NSMutableArray *filteredContacts, *headers; NSMutableArray *filteredContacts, *headers;
NSDictionary *contact; NSDictionary *data, *contact;
BOOL excludeLists; BOOL excludeLists;
NSComparisonResult ordering; NSComparisonResult ordering;
NSUInteger max, count; NSUInteger max, count;
WORequest *rq;
unsigned int i; unsigned int i;
[self saveSortValue]; [self saveSortValue];
@ -140,23 +153,26 @@
if (!contactInfos) if (!contactInfos)
{ {
folder = [self clientObject]; folder = [self clientObject];
rq = [context request]; data = [self requestData];
ascending = [rq formValueForKey: @"asc"]; ascending = [data objectForKey: @"asc"];
ordering = ((![ascending length] || [ascending boolValue]) ordering = ((!ascending || [ascending boolValue])
? NSOrderedAscending : NSOrderedDescending); ? NSOrderedAscending : NSOrderedDescending);
searchText = [rq formValueForKey: @"search"]; searchFields = [data objectForKey: @"search"];
if ([searchText length] > 0) if ([searchFields isKindOfClass: [NSArray class]] && [searchFields count] > 0)
valueText = [rq formValueForKey: @"value"]; valueText = [data objectForKey: @"value"];
else else
valueText = nil; {
searchFields = nil;
valueText = nil;
}
excludeLists = [[rq formValueForKey: @"excludeLists"] boolValue]; excludeLists = [[data objectForKey: @"excludeLists"] boolValue];
[contactInfos release]; [contactInfos release];
results = [folder lookupContactsWithFilter: valueText results = [folder lookupContactsWithFilter: valueText
onCriteria: searchText onCriteria: searchFields
sortBy: [self sortKey] sortBy: [self sortKey]
ordering: ordering ordering: ordering
inDomain: [[context activeUser] domain]]; inDomain: [[context activeUser] domain]];
@ -204,16 +220,15 @@
- (NSArray *) sortedIDs - (NSArray *) sortedIDs
{ {
id <SOGoContactFolder> folder; id <SOGoContactFolder> folder;
NSString *ascending, *searchText, *valueText; NSString *ascending, *valueText;
NSArray *fields, *records; NSArray *searchFields, *fields, *records;
NSDictionary *record; NSDictionary *data, *record;
NSEnumerator *recordsList; NSEnumerator *recordsList;
NSMutableArray *ids; NSMutableArray *ids;
BOOL excludeLists; BOOL excludeLists;
EOKeyValueQualifier *kvQualifier; EOKeyValueQualifier *kvQualifier;
EOSortOrdering *ordering; EOSortOrdering *ordering;
EOQualifier *qualifier; EOQualifier *qualifier;
WORequest *rq;
SEL compare; SEL compare;
folder = [self clientObject]; folder = [self clientObject];
@ -221,12 +236,12 @@
if (!sortedIDs && [folder isKindOfClass: [SOGoContactGCSFolder class]]) if (!sortedIDs && [folder isKindOfClass: [SOGoContactGCSFolder class]])
{ {
fields = [NSArray arrayWithObjects: @"c_name", nil]; fields = [NSArray arrayWithObjects: @"c_name", nil];
rq = [context request]; data = [self requestData];
qualifier = nil; qualifier = nil;
// ORDER BY clause // ORDER BY clause
ascending = [rq formValueForKey: @"asc"]; ascending = [data valueForKey: @"asc"];
if (![ascending length] || [ascending boolValue]) if (!ascending || [ascending boolValue])
compare = EOCompareAscending; compare = EOCompareAscending;
else else
compare = EOCompareDescending; compare = EOCompareDescending;
@ -234,14 +249,14 @@
selector: compare]; selector: compare];
// WHERE clause // WHERE clause
searchText = [rq formValueForKey: @"search"]; searchFields = (NSArray *)[data objectForKey: @"search"];
if ([searchText length] > 0) if ([searchFields count] > 0)
{ {
valueText = [rq formValueForKey: @"value"]; valueText = [data objectForKey: @"value"];
qualifier = [(SOGoContactGCSFolder *) folder qualifierForFilter: valueText qualifier = [(SOGoContactGCSFolder *) folder qualifierForFilter: valueText
onCriteria: searchText]; onCriteria: searchFields];
} }
excludeLists = [[rq formValueForKey: @"excludeLists"] boolValue]; excludeLists = [[data objectForKey: @"excludeLists"] boolValue];
if (excludeLists) if (excludeLists)
{ {
kvQualifier = [[EOKeyValueQualifier alloc] kvQualifier = [[EOKeyValueQualifier alloc]
@ -337,7 +352,7 @@
* @apiExample {curl} Example usage: * @apiExample {curl} Example usage:
* curl -i http://localhost/SOGo/so/sogo1/Contacts/personal/view?search=name_or_address\&value=Bob * curl -i http://localhost/SOGo/so/sogo1/Contacts/personal/view?search=name_or_address\&value=Bob
* *
* @apiParam {Boolean} [partial] Send all contacts IDs and headers of a the first 50 contacts. Defaults to false. * @apiParam {Boolean} [partial] Send all contacts IDs and headers of the first 50 contacts. Defaults to false.
* @apiParam {Boolean} [asc] Descending sort when false. Defaults to true (ascending). * @apiParam {Boolean} [asc] Descending sort when false. Defaults to true (ascending).
* @apiParam {String} [sort] Sort field. Either c_cn, c_mail, c_screenname, c_o, or c_telephonenumber. * @apiParam {String} [sort] Sort field. Either c_cn, c_mail, c_screenname, c_o, or c_telephonenumber.
* @apiParam {String} [search] Field criteria. Either name_or_address, category, or organization. * @apiParam {String} [search] Field criteria. Either name_or_address, category, or organization.
@ -383,7 +398,7 @@
[self cardDavURL], @"cardDavURL", [self cardDavURL], @"cardDavURL",
[self publicCardDavURL], @"publicCardDavURL", [self publicCardDavURL], @"publicCardDavURL",
nil]; nil];
partial = [[context request] formValueForKey: @"partial"]; partial = [[self requestData] objectForKey: @"partial"];
if ([partial intValue] && [folder isKindOfClass: [SOGoContactGCSFolder class]]) if ([partial intValue] && [folder isKindOfClass: [SOGoContactGCSFolder class]])
{ {
@ -427,11 +442,9 @@
{ {
NSArray *ids, *headers; NSArray *ids, *headers;
NSDictionary *data; NSDictionary *data;
WORequest *request;
WOResponse *response; WOResponse *response;
request = [context request]; data = [self requestData];
data = [[request contentAsString] objectFromJSONString];
if (![[data objectForKey: @"ids"] isKindOfClass: [NSArray class]] || if (![[data objectForKey: @"ids"] isKindOfClass: [NSArray class]] ||
[[data objectForKey: @"ids"] count] == 0) [[data objectForKey: @"ids"] count] == 0)
{ {
@ -463,11 +476,9 @@
NSMutableDictionary *uniqueContacts; NSMutableDictionary *uniqueContacts;
unsigned int i; unsigned int i;
NSSortDescriptor *commonNameDescriptor; NSSortDescriptor *commonNameDescriptor;
WORequest *rq;
rq = [context request]; excludeLists = [[[self requestData] objectForKey: @"excludeLists"] boolValue];
excludeLists = [[rq formValueForKey: @"excludeLists"] boolValue]; searchText = [[self requestData] objectForKey: @"search"];
searchText = [rq formValueForKey: @"search"];
if ([searchText length] > 0) if ([searchText length] > 0)
{ {
NS_DURING NS_DURING
@ -482,7 +493,7 @@
domain = [[context activeUser] domain]; domain = [[context activeUser] domain];
uniqueContacts = [NSMutableDictionary dictionary]; uniqueContacts = [NSMutableDictionary dictionary];
contacts = [folder lookupContactsWithFilter: searchText contacts = [folder lookupContactsWithFilter: searchText
onCriteria: @"name_or_address" onCriteria: nil
sortBy: @"c_cn" sortBy: @"c_cn"
ordering: NSOrderedAscending ordering: NSOrderedAscending
inDomain: domain]; inDomain: domain];

View File

@ -317,7 +317,8 @@
layout="row" layout="row"
ng-show="addressbook.mode.search" ng-show="addressbook.mode.search"
sg-search="addressbook.selectedFolder.$filter(searchText, { search: searchField })" sg-search="addressbook.selectedFolder.$filter(searchText, { search: searchField })"
sg-allow-dot="addressbook.selectedFolder.listRequiresDot"> sg-allow-dot="addressbook.selectedFolder.listRequiresDot"
sg-search-fields="addressbook.selectedFolder.searchFields">
<md-button class="md-icon-button" <md-button class="md-icon-button"
sg-search-cancel="addressbook.cancelSearch()" sg-search-cancel="addressbook.cancelSearch()"
label:aria-label="Back"> label:aria-label="Back">
@ -330,10 +331,13 @@
</div> </div>
</md-input-container> </md-input-container>
<md-input-container flex="25"> <md-input-container flex="25">
<md-select label:aria-label="Search scope"> <label><var:string label:value="Search scope"/></label>
<md-option value="name_or_address" selected="selected"><var:string label:value="Name or Email"/></md-option> <md-select multiple="multiple">
<md-option value="category"><var:string label:value="Category"/></md-option> <md-optgroup label:label="Search scope">
<md-option value="organization"><var:string label:value="Organization"/></md-option> <md-option
ng-value="field"
ng-repeat="field in ::addressbook.selectedFolder.searchFields">{{::field | loc}}</md-option>
</md-optgroup>
</md-select> </md-select>
</md-input-container> </md-input-container>
</form> </form>

View File

@ -20,7 +20,7 @@
<md-input-container> <md-input-container>
<input name="search" type="search"/> <input name="search" type="search"/>
</md-input-container> </md-input-container>
<md-select class="sg-toolbar-sort md-contrast-light"> <md-select multiple>
<md-option value="subject">Subject</md-option> <md-option value="subject">Subject</md-option>
<md-option value="sender">sender</md-option> <md-option value="sender">sender</md-option>
</md-select> </md-select>
@ -67,6 +67,9 @@
// Associate the sg-allow-dot parameter (boolean) to the controller // Associate the sg-allow-dot parameter (boolean) to the controller
controller.allowDot = $parse(iElement.attr('sg-allow-dot'))(scope); controller.allowDot = $parse(iElement.attr('sg-allow-dot'))(scope);
// Associate the sg-search-fields parameter (array) to the controller
controller.fields = $parse(iElement.attr('sg-search-fields'))(scope);
// Associate callback to controller // Associate callback to controller
controller.doSearch = $parse(iElement.attr('sg-search')); controller.doSearch = $parse(iElement.attr('sg-search'));
@ -114,6 +117,14 @@
} }
}; };
if ($element.attr('sg-search-fields')) {
var waitforFieldsOnce = $scope.$watch(vm.fields, function(value) {
// Select all fields by default
vm.searchField = _.clone(vm.fields);
waitforFieldsOnce();
});
}
// Method to call on data changes // Method to call on data changes
vm.onChange = function() { vm.onChange = function() {
var form = $scope[vm.formName], var form = $scope[vm.formName],

View File

@ -43,7 +43,7 @@
$Card: Card, $Card: Card,
$$Acl: Acl, $$Acl: Acl,
$Preferences: Preferences, $Preferences: Preferences,
$query: {search: 'name_or_address', value: '', sort: 'c_cn', asc: 1}, $query: {value: '', sort: 'c_cn', asc: 1},
activeUser: Settings.activeUser(), activeUser: Settings.activeUser(),
selectedFolder: null, selectedFolder: null,
$refreshTimeout: null $refreshTimeout: null
@ -497,7 +497,7 @@
query.value = search; query.value = search;
return _this.$id().then(function(addressbookId) { return _this.$id().then(function(addressbookId) {
var futureData = AddressBook.$$resource.fetch(addressbookId, 'view', query); var futureData = AddressBook.$$resource.post(addressbookId, 'view', query);
if (dry) { if (dry) {
return futureData.then(function(response) { return futureData.then(function(response) {