From eb90760b39e9134925243bd1442c9ad2b7425b37 Mon Sep 17 00:00:00 2001 From: Francis Lachapelle Date: Tue, 21 Nov 2017 15:56:16 -0500 Subject: [PATCH] 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). --- Documentation/SOGoInstallationGuide.asciidoc | 16 ++-- NEWS | 2 + SoObjects/Contacts/SOGoContactFolder.h | 2 +- SoObjects/Contacts/SOGoContactFolders.m | 2 +- SoObjects/Contacts/SOGoContactGCSFolder.h | 2 +- SoObjects/Contacts/SOGoContactGCSFolder.m | 67 +++++++++----- SoObjects/Contacts/SOGoContactSourceFolder.m | 11 ++- SoObjects/SOGo/LDAPSource.h | 2 - SoObjects/SOGo/LDAPSource.m | 78 ++++++++++------ SoObjects/SOGo/SOGoSource.h | 2 + SoObjects/SOGo/SQLSource.h | 1 + SoObjects/SOGo/SQLSource.m | 76 +++++++++++----- UI/Contacts/English.lproj/Localizable.strings | 24 +++++ UI/Contacts/UIxContactFoldersView.m | 3 +- UI/Contacts/UIxContactsListActions.h | 2 + UI/Contacts/UIxContactsListActions.m | 91 +++++++++++-------- .../ContactsUI/UIxContactFoldersView.wox | 14 ++- .../js/Common/sgSearch.directive.js | 13 ++- .../js/Contacts/AddressBook.service.js | 4 +- 19 files changed, 275 insertions(+), 137 deletions(-) diff --git a/Documentation/SOGoInstallationGuide.asciidoc b/Documentation/SOGoInstallationGuide.asciidoc index 6c3dd6db1..093961414 100644 --- a/Documentation/SOGoInstallationGuide.asciidoc +++ b/Documentation/SOGoInstallationGuide.asciidoc @@ -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 table. -|MailFieldNames +|MailFieldNames (optional) |An array of fields that returns the user's email addresses (defaults to `mail` when unset). Note that SOGo will always automatically strip the protocol value from the attribute if the attribute name is `proxyAddresses`. -|SearchFieldNames -|An array of fields to to match against the search string when filtering -users (defaults to `sn`, `displayName`, and `telephoneNumber` when -unset). +|SearchFieldNames (optional) +|An array of fields to match against the search string when filtering +users (defaults to `sn`, `displayName`, `cn`, `mail`, and `telephoneNumber` +when unset). |IMAPHostFieldName (optional) |The field that returns either an URI to the IMAP server as described @@ -1612,7 +1612,7 @@ SQL source: [cols="3,47a,50"] |======================================================================= -.20+^|D |SOGoUserSources +.21+^|D |SOGoUserSources |Parameter used to set the SQL and/or LDAP sources used for authentication and global address books. Multiple sources can be 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. 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) |The field that returns the IMAP hostname for the user. diff --git a/NEWS b/NEWS index 2c37970c7..b0f16cfc4 100644 --- a/NEWS +++ b/NEWS @@ -5,6 +5,7 @@ New features - [core] can now invite attendees to exceptions only (#2561) - [core] add support for module constraints 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] register SOGo as a handler for the mailto scheme (#1223) - [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] now also give modify permission when selecting all calendar rights - [web] allow edition of IMAP flags associated to mail labels + - [web] search scope of address book is now respected Bug fixes - [core] yearly repeating events are not shown in web calendar (#4237) diff --git a/SoObjects/Contacts/SOGoContactFolder.h b/SoObjects/Contacts/SOGoContactFolder.h index 47b098548..7d52b232d 100644 --- a/SoObjects/Contacts/SOGoContactFolder.h +++ b/SoObjects/Contacts/SOGoContactFolder.h @@ -43,7 +43,7 @@ @protocol SOGoContactFolder - (NSArray *) lookupContactsWithFilter: (NSString *) filter - onCriteria: (NSString *) criteria + onCriteria: (NSArray *) criteria sortBy: (NSString *) sortKey ordering: (NSComparisonResult) sortOrdering inDomain: (NSString *) domain; diff --git a/SoObjects/Contacts/SOGoContactFolders.m b/SoObjects/Contacts/SOGoContactFolders.m index 5a0d81333..3e7236e17 100644 --- a/SoObjects/Contacts/SOGoContactFolders.m +++ b/SoObjects/Contacts/SOGoContactFolders.m @@ -459,7 +459,7 @@ Class SOGoContactSourceFolderK; folder = [sortedFolders objectAtIndex: i]; //NSLog(@" Address book: %@ (%@)", [folder displayName], [folder class]); contacts = [folder lookupContactsWithFilter: theFilter - onCriteria: @"name_or_address" + onCriteria: nil sortBy: @"c_cn" ordering: NSOrderedAscending inDomain: domain]; diff --git a/SoObjects/Contacts/SOGoContactGCSFolder.h b/SoObjects/Contacts/SOGoContactGCSFolder.h index 4447aa186..01789a6dd 100644 --- a/SoObjects/Contacts/SOGoContactGCSFolder.h +++ b/SoObjects/Contacts/SOGoContactGCSFolder.h @@ -37,7 +37,7 @@ } - (void) fixupContactRecord: (NSMutableDictionary *) contactRecord; - (EOQualifier *) qualifierForFilter: (NSString *) filter - onCriteria: (NSString *) criteria; + onCriteria: (NSArray *) criteria; - (NSDictionary *) lookupContactWithName: (NSString *) aName; - (NSArray *) lookupContactsWithQualifier: (EOQualifier *) qualifier; - (NSArray *) lookupContactsFields: (NSArray *) fields diff --git a/SoObjects/Contacts/SOGoContactGCSFolder.m b/SoObjects/Contacts/SOGoContactGCSFolder.m index 7ce3feecd..927b55ad1 100644 --- a/SoObjects/Contacts/SOGoContactGCSFolder.m +++ b/SoObjects/Contacts/SOGoContactGCSFolder.m @@ -79,6 +79,20 @@ static NSArray *folderListingFields = nil; [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 { CardGroup *cardEntry; @@ -183,39 +197,42 @@ static NSArray *folderListingFields = nil; } - (EOQualifier *) qualifierForFilter: (NSString *) filter - onCriteria: (NSString *) criteria + onCriteria: (NSArray *) criteria { - NSString *qs; + NSEnumerator *criteriaList; + NSMutableArray *filters; + NSString *filterFormat, *currentCriteria, *qs; EOQualifier *qualifier; + qualifier = nil; if ([filter length] > 0) { filter = [filter asSafeSQLString]; - if ([criteria isEqualToString: @"name_or_address"]) - qs = [NSString stringWithFormat: - @"(c_sn isCaseInsensitiveLike: '%%%@%%') OR " - @"(c_givenname isCaseInsensitiveLike: '%%%@%%') OR " - @"(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]; + filters = [NSMutableArray array]; + filterFormat = [NSString stringWithFormat: @"(%%@ isCaseInsensitiveLike: '%%%%%@%%%%')", filter]; + if (criteria) + criteriaList = [criteria objectEnumerator]; else - qs = @"(1 == 0)"; + criteriaList = [[self searchFields] objectEnumerator]; - if (qs) - qualifier = [EOQualifier qualifierWithQualifierFormat: qs]; - else - qualifier = nil; + while (( currentCriteria = [criteriaList nextObject] )) + { + if ([currentCriteria isEqualToString: @"name"]) + { + [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; } @@ -357,7 +374,7 @@ static NSArray *folderListingFields = nil; * The domain is therefore ignored. */ - (NSArray *) lookupContactsWithFilter: (NSString *) filter - onCriteria: (NSString *) criteria + onCriteria: (NSArray *) criteria sortBy: (NSString *) sortKey ordering: (NSComparisonResult) sortOrdering inDomain: (NSString *) domain diff --git a/SoObjects/Contacts/SOGoContactSourceFolder.m b/SoObjects/Contacts/SOGoContactSourceFolder.m index a74515fae..ce1195a18 100644 --- a/SoObjects/Contacts/SOGoContactSourceFolder.m +++ b/SoObjects/Contacts/SOGoContactSourceFolder.m @@ -115,6 +115,11 @@ return isPersonalSource; } +- (NSArray *) searchFields +{ + return [source searchFields]; +} + - (BOOL) listRequiresDot { return [source listRequiresDot]; @@ -391,7 +396,7 @@ } - (NSArray *) lookupContactsWithFilter: (NSString *) filter - onCriteria: (NSString *) criteria + onCriteria: (NSArray *) criteria sortBy: (NSString *) sortKey ordering: (NSComparisonResult) sortOrdering inDomain: (NSString *) domain @@ -401,10 +406,10 @@ result = nil; - if (([filter length] > 0 && [criteria isEqualToString: @"name_or_address"]) - || ![source listRequiresDot]) + if ([filter length] > 0 || ![source listRequiresDot]) { records = [source fetchContactsMatching: filter + withCriteria: criteria inDomain: domain]; [childRecords setObjects: records forKeys: [records objectsForKey: @"c_name" diff --git a/SoObjects/SOGo/LDAPSource.h b/SoObjects/SOGo/LDAPSource.h index 60762950e..df02dd461 100644 --- a/SoObjects/SOGo/LDAPSource.h +++ b/SoObjects/SOGo/LDAPSource.h @@ -74,8 +74,6 @@ NSDictionary *modulesConstraints; - NSMutableArray *searchAttributes; - BOOL passwordPolicy; BOOL updateSambaNTLMPasswords; diff --git a/SoObjects/SOGo/LDAPSource.m b/SoObjects/SOGo/LDAPSource.m index d5da0555c..a55565d21 100644 --- a/SoObjects/SOGo/LDAPSource.m +++ b/SoObjects/SOGo/LDAPSource.m @@ -92,7 +92,9 @@ static Class NSStringK; mailFields = [NSArray arrayWithObject: @"mail"]; [mailFields retain]; 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]; groupObjectClasses = [NSArray arrayWithObjects: @"group", @"groupofnames", @"groupofuniquenames", @"posixgroup", nil]; [groupObjectClasses retain]; @@ -105,7 +107,6 @@ static Class NSStringK; _userPasswordAlgorithm = nil; listRequiresDot = YES; - searchAttributes = nil; passwordPolicy = NO; updateSambaNTLMPasswords = NO; @@ -149,7 +150,6 @@ static Class NSStringK; [sourceID release]; [modulesConstraints release]; [_scope release]; - [searchAttributes release]; [domain release]; [kindField release]; [multipleBookingsField release]; @@ -373,6 +373,11 @@ groupObjectClasses: (NSArray *) newGroupObjectClasses return listRequiresDot; } +- (NSArray *) searchFields +{ + return searchFields; +} + - (void) setContactMapping: (NSDictionary *) newMapping andObjectClasses: (NSArray *) newObjectClasses { @@ -761,41 +766,57 @@ groupObjectClasses: (NSArray *) newGroupObjectClasses * @return a EOQualifier matching the filter */ - (EOQualifier *) _qualifierForFilter: (NSString *) filter + onCriteria: (NSArray *) criteria { + NSEnumerator *criteriaList; NSMutableArray *fields; - NSString *fieldFormat, *searchFormat, *escapedFilter; + NSString *fieldFormat, *currentCriteria, *searchFormat, *escapedFilter; EOQualifier *qualifier; NSMutableString *qs; escapedFilter = SafeLDAPCriteria(filter); - if ([escapedFilter length] > 0) + qs = [NSMutableString string]; + + if (([escapedFilter length] == 0 && !listRequiresDot) || [escapedFilter isEqualToString: @"."]) { - qs = [NSMutableString string]; - if ([escapedFilter isEqualToString: @"."]) - [qs appendFormat: @"(%@='*')", CNField]; + [qs appendFormat: @"(%@='*')", CNField]; + } + else + { + fieldFormat = [NSString stringWithFormat: @"(%%@='*%@*')", escapedFilter]; + if (criteria) + criteriaList = [criteria objectEnumerator]; else + criteriaList = [[self searchFields] objectEnumerator]; + + fields = [NSMutableArray array]; + while (( currentCriteria = [criteriaList nextObject] )) { - fieldFormat = [NSString stringWithFormat: @"(%%@='*%@*')", escapedFilter]; - fields = [NSMutableArray arrayWithArray: searchFields]; - [fields addObjectsFromArray: mailFields]; - [fields addObject: CNField]; - searchFormat = [[[fields uniqueObjects] stringsWithFormat: fieldFormat] - componentsJoinedByString: @" OR "]; - [qs appendString: searchFormat]; + if ([currentCriteria isEqualToString: @"name"]) + { + [fields addObject: @"sn"]; + [fields addObject: @"displayname"]; + [fields addObject: @"cn"]; + } + 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]) - [qs appendFormat: @" AND %@", _filter]; + searchFormat = [[[fields uniqueObjects] stringsWithFormat: fieldFormat] componentsJoinedByString: @" OR "]; + [qs appendString: searchFormat]; + } - qualifier = [EOQualifier qualifierWithQualifierFormat: qs]; - } - else if (!listRequiresDot) - { - qs = [NSMutableString stringWithFormat: @"(%@='*')", CNField]; - if ([_filter length]) - [qs appendFormat: @" AND %@", _filter]; - qualifier = [EOQualifier qualifierWithQualifierFormat: qs]; - } + if (_filter && [_filter length]) + [qs appendFormat: @" AND %@", _filter]; + + if ([qs length]) + qualifier = [EOQualifier qualifierWithQualifierFormat: qs]; else qualifier = nil; @@ -832,6 +853,7 @@ groupObjectClasses: (NSArray *) newGroupObjectClasses return [EOQualifier qualifierWithQualifierFormat: qs]; } +/* - (NSArray *) _constraintsFields { NSMutableArray *fields; @@ -845,6 +867,7 @@ groupObjectClasses: (NSArray *) newGroupObjectClasses return fields; } +*/ /* This is required for SQL sources when DomainFieldName is enabled. * For LDAP, simply discard the domain and call the original method */ @@ -1202,6 +1225,7 @@ groupObjectClasses: (NSArray *) newGroupObjectClasses } - (NSArray *) fetchContactsMatching: (NSString *) match + withCriteria: (NSArray *) criteria inDomain: (NSString *) domain { NGLdapConnection *ldapConnection; @@ -1216,7 +1240,7 @@ groupObjectClasses: (NSArray *) newGroupObjectClasses if ([match length] > 0 || !listRequiresDot) { ldapConnection = [self _ldapConnection]; - qualifier = [self _qualifierForFilter: match]; + qualifier = [self _qualifierForFilter: match onCriteria: criteria]; attributes = [NSArray arrayWithObject: @"*"]; if ([_scope caseInsensitiveCompare: @"BASE"] == NSOrderedSame) diff --git a/SoObjects/SOGo/SOGoSource.h b/SoObjects/SOGo/SOGoSource.h index 572efb569..d3c53bcad 100644 --- a/SoObjects/SOGo/SOGoSource.h +++ b/SoObjects/SOGo/SOGoSource.h @@ -39,6 +39,7 @@ inDomain: (NSString *) domain; - (NSString *) domain; +- (NSArray *) searchFields; /* requires a "." to obtain the full list of contacts */ - (void) setListRequiresDot: (BOOL) aBool; @@ -63,6 +64,7 @@ - (NSArray *) allEntryIDs; - (NSArray *) allEntryIDsVisibleFromDomain: (NSString *) domain; - (NSArray *) fetchContactsMatching: (NSString *) filter + withCriteria: (NSArray *) criteria inDomain: (NSString *) domain; - (void) setSourceID: (NSString *) newSourceID; diff --git a/SoObjects/SOGo/SQLSource.h b/SoObjects/SOGo/SQLSource.h index b3451df59..e84b4c05b 100644 --- a/SoObjects/SOGo/SQLSource.h +++ b/SoObjects/SOGo/SQLSource.h @@ -40,6 +40,7 @@ NSString *_authenticationFilter; NSArray *_loginFields; NSArray *_mailFields; + NSArray *_searchFields; NSString *_imapLoginField; NSString *_imapHostField; NSString *_sieveHostField; diff --git a/SoObjects/SOGo/SQLSource.m b/SoObjects/SOGo/SQLSource.m index e023c26d6..cff7cc43a 100644 --- a/SoObjects/SOGo/SQLSource.m +++ b/SoObjects/SOGo/SQLSource.m @@ -1,9 +1,8 @@ /* SQLSource.h - this file is part of SOGo * - * Copyright (C) 2009-2012 Inverse inc. + * Copyright (C) 2009-2017 Inverse inc. * - * Authors: Ludovic Marcotte - * Francis Lachapelle + * This file is part of SOGo. * * 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 @@ -35,6 +34,7 @@ #import +#import "NSArray+Utilities.h" #import "NSString+Utilities.h" #import "NSString+Crypto.h" @@ -90,6 +90,9 @@ _authenticationFilter = nil; _loginFields = nil; _mailFields = nil; + // "mail" expands to all entries of MailFieldNames + _searchFields = [NSArray arrayWithObjects: @"c_cn", @"mail", nil]; + [_searchFields retain]; _userPasswordAlgorithm = nil; _viewURL = nil; _kindField = nil; @@ -109,6 +112,7 @@ [_authenticationFilter release]; [_loginFields release]; [_mailFields release]; + [_searchFields release]; [_userPasswordAlgorithm release]; [_viewURL release]; [_kindField release]; @@ -140,6 +144,8 @@ ASSIGN(_multipleBookingsField, [udSource objectForKey: @"MultipleBookingsFieldName"]); ASSIGN(_domainField, [udSource objectForKey: @"DomainFieldName"]); ASSIGN(_modulesConstraints, [udSource objectForKey: @"ModulesConstraints"]); + if ([udSource objectForKey: @"SearchFieldNames"]) + ASSIGN(_searchFields, [udSource objectForKey: @"SearchFieldNames"]); if ([udSource objectForKey: @"prependPasswordScheme"]) _prependPasswordScheme = [[udSource objectForKey: @"prependPasswordScheme"] boolValue]; else @@ -157,7 +163,7 @@ #warning this domain code has no effect yet if ([sourceDomain length]) - ASSIGN (_domain, sourceDomain); + ASSIGN(_domain, sourceDomain); if (!_viewURL) { @@ -173,6 +179,11 @@ return _domain; } +- (NSArray *) searchFields +{ + return _searchFields; +} + - (BOOL) _isPassword: (NSString *) plainPassword equalTo: (NSString *) encryptedPassword { @@ -369,6 +380,7 @@ return didChange; } +/* - (NSString *) _whereClauseFromArray: (NSArray *) theArray value: (NSString *) theValue exact: (BOOL) theBOOL @@ -388,6 +400,7 @@ return s; } +*/ - (void) _fillConstraintsForModule: (NSString *) module intoRecord: (NSMutableDictionary *) record @@ -562,7 +575,7 @@ [emails addObjectsFromArray: [s componentsSeparatedByString: @" "]]; } - [response setObject: emails forKey: @"c_emails"]; + [response setObject: [emails uniqueObjects] forKey: @"c_emails"]; if (_imapHostField) { value = [response objectForKey: _imapHostField]; @@ -792,14 +805,16 @@ } - (NSArray *) fetchContactsMatching: (NSString *) filter + withCriteria: (NSArray *) criteria inDomain: (NSString *) domain { EOAdaptorChannel *channel; - NSMutableArray *results; + NSEnumerator *criteriaList; + NSMutableArray *fields, *results; GCSChannelManager *cm; NSException *ex; NSMutableString *sql; - NSString *lowerFilter; + NSString *lowerFilter, *filterFormat, *currentCriteria, *qs; results = [NSMutableArray array]; @@ -809,22 +824,40 @@ channel = [cm acquireOpenChannelForURL: _viewURL]; if (channel) { - lowerFilter = [filter lowercaseString]; - lowerFilter = [lowerFilter stringByReplacingString: @"'" withString: @"''"]; - - sql = [NSMutableString stringWithFormat: (@"SELECT *" - @" FROM %@" - @" WHERE" - @" (LOWER(c_cn) LIKE '%%%@%%'" - @" OR LOWER(mail) LIKE '%%%@%%'"), - [_viewURL gcsTableName], - lowerFilter, lowerFilter]; - - if (_mailFields && [_mailFields count] > 0) + fields = [NSMutableArray array]; + if ([filter length]) { - [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: @")"]; if (_domainField) @@ -832,8 +865,7 @@ if ([domain length]) { EOQualifier *domainQualifier; - domainQualifier = - [self _visibleDomainsQualifierFromDomain: domain]; + domainQualifier = [self _visibleDomainsQualifierFromDomain: domain]; if (domainQualifier) { [sql appendFormat: @" AND ("]; diff --git a/UI/Contacts/English.lproj/Localizable.strings b/UI/Contacts/English.lproj/Localizable.strings index 5ba619472..500aea21c 100644 --- a/UI/Contacts/English.lproj/Localizable.strings +++ b/UI/Contacts/English.lproj/Localizable.strings @@ -45,6 +45,30 @@ "Carbon Copy (Cc)" = "Carbon Copy (Cc)"; "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 */ "No contact" = "No contact"; diff --git a/UI/Contacts/UIxContactFoldersView.m b/UI/Contacts/UIxContactFoldersView.m index 1fcb8e879..d100faee3 100644 --- a/UI/Contacts/UIxContactFoldersView.m +++ b/UI/Contacts/UIxContactFoldersView.m @@ -199,7 +199,7 @@ Class SOGoContactSourceFolderK, SOGoGCSFolderK; folder = [sortedFolders objectAtIndex: i]; //NSLog(@" Address book: %@ (%@)", [folder displayName], [folder class]); contacts = [folder lookupContactsWithFilter: searchText - onCriteria: @"name_or_address" + onCriteria: nil sortBy: @"c_cn" ordering: NSOrderedAscending inDomain: domain]; @@ -343,6 +343,7 @@ Class SOGoContactSourceFolderK, SOGoGCSFolderK; && [currentFolder listRequiresDot]], @"listRequiresDot", acls, @"acls", urls, @"urls", + [currentFolder searchFields], @"searchFields", nil]; [foldersAttrs addObject: folderAttrs]; } diff --git a/UI/Contacts/UIxContactsListActions.h b/UI/Contacts/UIxContactsListActions.h index 8813b873d..42279902e 100644 --- a/UI/Contacts/UIxContactsListActions.h +++ b/UI/Contacts/UIxContactsListActions.h @@ -31,6 +31,8 @@ @interface UIxContactsListActions : SOGoDirectAction { + NSDictionary *requestData; + NSDictionary *currentContact; NSArray *contactInfos; diff --git a/UI/Contacts/UIxContactsListActions.m b/UI/Contacts/UIxContactsListActions.m index 6358eb3bd..1a1939d28 100644 --- a/UI/Contacts/UIxContactsListActions.m +++ b/UI/Contacts/UIxContactsListActions.m @@ -54,6 +54,7 @@ { if ((self = [super init])) { + requestData = nil; contactInfos = nil; sortedIDs = nil; } @@ -63,6 +64,7 @@ - (void) dealloc { + [requestData release]; [contactInfos release]; [sortedIDs release]; [super dealloc]; @@ -70,6 +72,20 @@ /* accessors */ +- (NSDictionary *) requestData +{ + WORequest *rq; + + if (!requestData) + { + rq = [context request]; + requestData = [[rq contentAsString] objectFromJSONString]; + [requestData retain]; + } + + return requestData; +} + - (NSString *) defaultSortKey { return @"c_cn"; @@ -78,7 +94,6 @@ - (NSString *) sortKey { NSString *s; - WORequest *rq; static NSArray *sortKeys = nil; if (!sortKeys) @@ -88,8 +103,7 @@ [sortKeys retain]; } - rq = [context request]; - s = [rq formValueForKey: @"sort"]; + s = [[self requestData] objectForKey: @"sort"]; if (![s length] || ![sortKeys containsObject: s]) s = [self defaultSortKey]; @@ -102,8 +116,8 @@ NSString *ascending, *sort; SOGoUserSettings *us; - sort = [[context request] formValueForKey: @"sort"]; - ascending = [[context request] formValueForKey: @"asc"]; + sort = [[self requestData] objectForKey: @"sort"]; + ascending = [[self requestData] objectForKey: @"asc"]; if ([sort length]) { @@ -125,14 +139,13 @@ - (NSArray *) contactInfos { id folder; - NSString *ascending, *searchText, *valueText; - NSArray *results, *fields; + NSString *ascending, *valueText; + NSArray *results, *searchFields, *fields; NSMutableArray *filteredContacts, *headers; - NSDictionary *contact; + NSDictionary *data, *contact; BOOL excludeLists; NSComparisonResult ordering; NSUInteger max, count; - WORequest *rq; unsigned int i; [self saveSortValue]; @@ -140,23 +153,26 @@ if (!contactInfos) { folder = [self clientObject]; - rq = [context request]; + data = [self requestData]; - ascending = [rq formValueForKey: @"asc"]; - ordering = ((![ascending length] || [ascending boolValue]) + ascending = [data objectForKey: @"asc"]; + ordering = ((!ascending || [ascending boolValue]) ? NSOrderedAscending : NSOrderedDescending); - searchText = [rq formValueForKey: @"search"]; - if ([searchText length] > 0) - valueText = [rq formValueForKey: @"value"]; + searchFields = [data objectForKey: @"search"]; + if ([searchFields isKindOfClass: [NSArray class]] && [searchFields count] > 0) + valueText = [data objectForKey: @"value"]; else - valueText = nil; + { + searchFields = nil; + valueText = nil; + } - excludeLists = [[rq formValueForKey: @"excludeLists"] boolValue]; + excludeLists = [[data objectForKey: @"excludeLists"] boolValue]; [contactInfos release]; results = [folder lookupContactsWithFilter: valueText - onCriteria: searchText + onCriteria: searchFields sortBy: [self sortKey] ordering: ordering inDomain: [[context activeUser] domain]]; @@ -204,16 +220,15 @@ - (NSArray *) sortedIDs { id folder; - NSString *ascending, *searchText, *valueText; - NSArray *fields, *records; - NSDictionary *record; + NSString *ascending, *valueText; + NSArray *searchFields, *fields, *records; + NSDictionary *data, *record; NSEnumerator *recordsList; NSMutableArray *ids; BOOL excludeLists; EOKeyValueQualifier *kvQualifier; EOSortOrdering *ordering; EOQualifier *qualifier; - WORequest *rq; SEL compare; folder = [self clientObject]; @@ -221,12 +236,12 @@ if (!sortedIDs && [folder isKindOfClass: [SOGoContactGCSFolder class]]) { fields = [NSArray arrayWithObjects: @"c_name", nil]; - rq = [context request]; + data = [self requestData]; qualifier = nil; // ORDER BY clause - ascending = [rq formValueForKey: @"asc"]; - if (![ascending length] || [ascending boolValue]) + ascending = [data valueForKey: @"asc"]; + if (!ascending || [ascending boolValue]) compare = EOCompareAscending; else compare = EOCompareDescending; @@ -234,14 +249,14 @@ selector: compare]; // WHERE clause - searchText = [rq formValueForKey: @"search"]; - if ([searchText length] > 0) + searchFields = (NSArray *)[data objectForKey: @"search"]; + if ([searchFields count] > 0) { - valueText = [rq formValueForKey: @"value"]; + valueText = [data objectForKey: @"value"]; qualifier = [(SOGoContactGCSFolder *) folder qualifierForFilter: valueText - onCriteria: searchText]; + onCriteria: searchFields]; } - excludeLists = [[rq formValueForKey: @"excludeLists"] boolValue]; + excludeLists = [[data objectForKey: @"excludeLists"] boolValue]; if (excludeLists) { kvQualifier = [[EOKeyValueQualifier alloc] @@ -337,7 +352,7 @@ * @apiExample {curl} Example usage: * 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 {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. @@ -383,7 +398,7 @@ [self cardDavURL], @"cardDavURL", [self publicCardDavURL], @"publicCardDavURL", nil]; - partial = [[context request] formValueForKey: @"partial"]; + partial = [[self requestData] objectForKey: @"partial"]; if ([partial intValue] && [folder isKindOfClass: [SOGoContactGCSFolder class]]) { @@ -427,11 +442,9 @@ { NSArray *ids, *headers; NSDictionary *data; - WORequest *request; WOResponse *response; - request = [context request]; - data = [[request contentAsString] objectFromJSONString]; + data = [self requestData]; if (![[data objectForKey: @"ids"] isKindOfClass: [NSArray class]] || [[data objectForKey: @"ids"] count] == 0) { @@ -463,11 +476,9 @@ NSMutableDictionary *uniqueContacts; unsigned int i; NSSortDescriptor *commonNameDescriptor; - WORequest *rq; - rq = [context request]; - excludeLists = [[rq formValueForKey: @"excludeLists"] boolValue]; - searchText = [rq formValueForKey: @"search"]; + excludeLists = [[[self requestData] objectForKey: @"excludeLists"] boolValue]; + searchText = [[self requestData] objectForKey: @"search"]; if ([searchText length] > 0) { NS_DURING @@ -482,7 +493,7 @@ domain = [[context activeUser] domain]; uniqueContacts = [NSMutableDictionary dictionary]; contacts = [folder lookupContactsWithFilter: searchText - onCriteria: @"name_or_address" + onCriteria: nil sortBy: @"c_cn" ordering: NSOrderedAscending inDomain: domain]; diff --git a/UI/Templates/ContactsUI/UIxContactFoldersView.wox b/UI/Templates/ContactsUI/UIxContactFoldersView.wox index 85f101147..074260f45 100644 --- a/UI/Templates/ContactsUI/UIxContactFoldersView.wox +++ b/UI/Templates/ContactsUI/UIxContactFoldersView.wox @@ -317,7 +317,8 @@ layout="row" ng-show="addressbook.mode.search" 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"> @@ -330,10 +331,13 @@ - - - - + + + + {{::field | loc}} + diff --git a/UI/WebServerResources/js/Common/sgSearch.directive.js b/UI/WebServerResources/js/Common/sgSearch.directive.js index d791f5615..3325475db 100644 --- a/UI/WebServerResources/js/Common/sgSearch.directive.js +++ b/UI/WebServerResources/js/Common/sgSearch.directive.js @@ -20,7 +20,7 @@ - + Subject sender @@ -67,6 +67,9 @@ // Associate the sg-allow-dot parameter (boolean) to the controller 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 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 vm.onChange = function() { var form = $scope[vm.formName], diff --git a/UI/WebServerResources/js/Contacts/AddressBook.service.js b/UI/WebServerResources/js/Contacts/AddressBook.service.js index 0697aa8e1..ea8abbd4e 100644 --- a/UI/WebServerResources/js/Contacts/AddressBook.service.js +++ b/UI/WebServerResources/js/Contacts/AddressBook.service.js @@ -43,7 +43,7 @@ $Card: Card, $$Acl: Acl, $Preferences: Preferences, - $query: {search: 'name_or_address', value: '', sort: 'c_cn', asc: 1}, + $query: {value: '', sort: 'c_cn', asc: 1}, activeUser: Settings.activeUser(), selectedFolder: null, $refreshTimeout: null @@ -497,7 +497,7 @@ query.value = search; return _this.$id().then(function(addressbookId) { - var futureData = AddressBook.$$resource.fetch(addressbookId, 'view', query); + var futureData = AddressBook.$$resource.post(addressbookId, 'view', query); if (dry) { return futureData.then(function(response) {