1/*
2  Copyright (C) 2005-2019 Inverse inc.
3
4  This file is part of SOGo.
5
6  SOGo is free software; you can redistribute it and/or modify it under
7  the terms of the GNU Lesser General Public License as published by the
8  Free Software Foundation; either version 2, or (at your option) any
9  later version.
10
11  SOGo is distributed in the hope that it will be useful, but WITHOUT ANY
12  WARRANTY; without even the implied warranty of MERCHANTABILITY or
13  FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public
14  License for more details.
15
16  You should have received a copy of the GNU Lesser General Public
17  License along with OGo; see the file COPYING.  If not, write to the
18  Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA
19  02111-1307, USA.
20*/
21
22#import <Foundation/NSURL.h>
23#import <Foundation/NSValue.h>
24
25#import <NGObjWeb/NSException+HTTP.h>
26#import <NGObjWeb/WOResponse.h>
27#import <NGCards/NSArray+NGCards.h>
28#import <NGExtensions/NSString+Ext.h>
29#import <NGExtensions/NSString+misc.h>
30
31#import <SOGo/NSArray+Utilities.h>
32#import <SOGo/NSCalendarDate+SOGo.h>
33#import <SOGo/NSDictionary+Utilities.h>
34#import <SOGo/SOGoSource.h>
35#import <SOGo/SOGoUser.h>
36#import <SOGo/SOGoUserDefaults.h>
37#import <SOGo/SOGoUserManager.h>
38
39#import <Contacts/NGVCard+SOGo.h>
40#import <Contacts/SOGoContactObject.h>
41
42#import "UIxContactView.h"
43
44@implementation UIxContactView
45
46- (id) init
47{
48  if ((self = [super init]))
49    {
50      photosURL = nil;
51      card = nil;
52      phones = nil;
53      homeAdr = nil;
54      workAdr = nil;
55    }
56
57  return self;
58}
59
60- (void) dealloc
61{
62  [card release];
63  [photosURL release];
64  [super dealloc];
65}
66
67- (NSArray *) _languageContactsCategories
68{
69  NSArray *categoryLabels;
70
71  categoryLabels = [[self labelForKey: @"contacts_category_labels"] componentsSeparatedByString: @","];
72  if (!categoryLabels)
73    categoryLabels = [NSArray array];
74
75  return [categoryLabels trimmedComponents];
76}
77
78- (NSArray *) _fetchAndCombineCategoriesList
79{
80  NSString *ownerLogin;
81  SOGoUserDefaults *ud;
82  NSArray *cats, *newCats, *contactCategories;
83
84  ownerLogin = [[self clientObject] ownerInContext: context];
85  ud = [[SOGoUser userWithLogin: ownerLogin] userDefaults];
86  cats = [ud contactsCategories];
87  if (!cats)
88    cats = [self _languageContactsCategories];
89
90  contactCategories = [card categories];
91  if (contactCategories)
92    {
93      newCats = [cats mergedArrayWithArray: [contactCategories trimmedComponents]];
94      if ([newCats count] != [cats count])
95        {
96          cats = [newCats sortedArrayUsingSelector:
97                            @selector (localizedCaseInsensitiveCompare:)];
98          [ud setContactsCategories: cats];
99          [ud synchronize];
100        }
101    }
102
103  return cats;
104}
105
106- (NSArray *) categories
107{
108  NSMutableArray *categories;
109  NSArray *values;
110  NSString *category;
111  NSUInteger count, max;
112
113  values = [card categories];
114  max = [values count];
115  if (max > 0)
116    {
117      categories = [NSMutableArray arrayWithCapacity: max];
118      for (count = 0; count < max; count++)
119        {
120          category = [values objectAtIndex: count];
121          if ([category length] > 0)
122            [categories addObject: [NSDictionary dictionaryWithObject: category forKey: @"value"]];
123        }
124    }
125  else
126    categories = nil;
127
128  return categories;
129}
130
131- (NSArray *) urls
132{
133  NSMutableArray *urls;
134  NSMutableDictionary *attrs;
135  NSArray *values;
136  NSString *type, *value;
137  NSURL *url;
138  CardElement *element;
139  NSUInteger count, max;
140
141  values = [card childrenWithTag: @"url"];
142  max = [values count];
143  if (max > 0)
144    {
145      urls = [NSMutableArray arrayWithCapacity: max];
146      for (count = 0; count < max; count++)
147        {
148          attrs = [NSMutableDictionary dictionary];
149          element = [values objectAtIndex: count];
150          type = [element value: 0 ofAttribute: @"type"];
151          if ([type length])
152            [attrs setObject: type forKey: @"type"];
153          value = [element flattenedValuesForKey: @""];
154          url = [NSURL URLWithString: value];
155          if (![url scheme] && [value length] > 0)
156            url = [NSURL URLWithString: [NSString stringWithFormat: @"http://%@", value]];
157          if (url)
158            [attrs setObject: [url absoluteString] forKey: @"value"];
159
160          [urls addObject: attrs];
161        }
162    }
163  else
164    urls = nil;
165
166  return urls;
167}
168
169- (NSArray *) deliveryAddresses
170{
171  NSString *type, *postoffice, *street, *street2, *locality, *region, *postalcode, *country;
172  NSMutableDictionary *address;
173  NSMutableArray *addresses;
174  NSArray *elements;
175  CardElement *adr;
176
177  NSUInteger count, max;
178
179  elements = [card childrenWithTag: @"adr"];
180  max = [elements count];
181
182  if (max > 0)
183    {
184      addresses = [NSMutableArray arrayWithCapacity: max];
185      for (count = 0; count < max; count++)
186        {
187          adr = [elements objectAtIndex: count];
188          type = [adr value: 0 ofAttribute: @"type"];
189          postoffice = [adr flattenedValueAtIndex: 0 forKey: @""];
190          street2    = [adr flattenedValueAtIndex: 1 forKey: @""];
191          street     = [adr flattenedValueAtIndex: 2 forKey: @""];
192          locality   = [adr flattenedValueAtIndex: 3 forKey: @""];
193          region     = [adr flattenedValueAtIndex: 4 forKey: @""];
194          postalcode = [adr flattenedValueAtIndex: 5 forKey: @""];
195          country    = [adr flattenedValueAtIndex: 6 forKey: @""];
196
197          address = [NSMutableDictionary dictionaryWithObject: type  forKey: @"type"];
198          if (postoffice) [address setObject: postoffice forKey: @"postoffice"];
199          if (street2)    [address setObject: street2 forKey: @"street2"];
200          if (street)     [address setObject: street forKey: @"street"];
201          if (locality)   [address setObject: locality forKey: @"locality"];
202          if (region)     [address setObject: region forKey: @"region"];
203          if (postalcode) [address setObject: postalcode forKey: @"postalcode"];
204          if (country)    [address setObject: country forKey: @"country"];
205          if ([[address allKeys] count] > 1) [addresses addObject: address];
206        }
207    }
208  else
209    addresses = nil;
210
211  return addresses;
212}
213
214/* action */
215
216- (id <WOActionResults>) defaultAction
217{
218  card = [[self clientObject] vCard];
219  if (card)
220    {
221      [card retain];
222      phones = nil;
223      homeAdr = nil;
224      workAdr = nil;
225    }
226  else
227    return [NSException exceptionWithHTTPStatus: 404 /* Not Found */
228                        reason: @"could not locate contact"];
229
230  return self;
231}
232
233/**
234 * @api {get} /so/:username/Contacts/:addressbookId/:cardId/view Get card
235 * @apiVersion 1.0.0
236 * @apiName GetData
237 * @apiGroup Contacts
238 * @apiExample {curl} Example usage:
239 *     curl -i http://localhost/SOGo/so/sogo1/Contacts/personal/1BC8-52F53F80-1-38C52040.vcf/view
240 *
241 * @apiSuccess (Success 200) {String} id                   Card ID
242 * @apiSuccess (Success 200) {String} pid                  Address book ID (card's container)
243 * @apiSuccess (Success 200) {String} c_component          Either vcard or vlist
244 * @apiSuccess (Success 200) {String} [c_givenname]        Firstname
245 * @apiSuccess (Success 200) {String} [nickname]           Nickname
246 * @apiSuccess (Success 200) {String} [c_sn]               Lastname
247 * @apiSuccess (Success 200) {String} [c_fn]               Fullname
248 * @apiSuccess (Success 200) {String} [title]              Title
249 * @apiSuccess (Success 200) {String} [role]               Role
250 * @apiSuccess (Success 200) {String} [c_screenname]       Screen Name (X-AIM for now)
251 * @apiSuccess (Success 200) {String} [tz]                 Timezone
252 * @apiSuccess (Success 200) {String} [org]                Main organization
253 * @apiSuccess (Success 200) {String[]} [orgs]             Additional organizations
254 * @apiSuccess (Success 200) {String[]} [notes]            Notes
255 * @apiSuccess (Success 200) {String[]} allCategories      All available categories
256 * @apiSuccess (Success 200) {Object[]} [categories]       Categories assigned to the card
257 * @apiSuccess (Success 200) {String} categories.value     Category name
258 * @apiSuccess (Success 200) {Number} hasCertificate       1 if contact has a mail certificate
259 * @apiSuccess (Success 200) {Object[]} [addresses]        Postal addresses
260 * @apiSuccess (Success 200) {String} addresses.type       Type (e.g., home or work)
261 * @apiSuccess (Success 200) {String} addresses.postoffice Post office box
262 * @apiSuccess (Success 200) {String} addresses.street     Street address
263 * @apiSuccess (Success 200) {String} addresses.street2    Extended address (e.g., apartment or suite number)
264 * @apiSuccess (Success 200) {String} addresses.locality   Locality (e.g., city)
265 * @apiSuccess (Success 200) {String} addresses.region     Region (e.g., state or province)
266 * @apiSuccess (Success 200) {String} addresses.postalcode Postal code
267 * @apiSuccess (Success 200) {String} addresses.country    Country name
268 * @apiSuccess (Success 200) {Object[]} [emails]           Email addresses
269 * @apiSuccess (Success 200) {String} emails.type          Type (e.g., home or work)
270 * @apiSuccess (Success 200) {String} emails.value         Email address
271 * @apiSuccess (Success 200) {Object[]} [phones]           Phone numbers
272 * @apiSuccess (Success 200) {String} phones.type          Type (e.g., mobile or work)
273 * @apiSuccess (Success 200) {String} phones.value         Phone number
274 * @apiSuccess (Success 200) {Object[]} [urls]             URLs
275 * @apiSuccess (Success 200) {String} urls.type            Type (e.g., personal or work)
276 * @apiSuccess (Success 200) {String} urls.value           URL
277 * @apiSuccess (Success 200) {Object[]} customFields       Custom fields from Thunderbird
278 */
279- (id <WOActionResults>) dataAction
280{
281  NSMutableDictionary *customFields, *data;
282  SOGoObject <SOGoContactObject> *contact;
283  id <WOActionResults> result;
284  NSArray *values;
285  id o;
286
287  contact = [self clientObject];
288  card = [contact vCard];
289  if (card)
290    {
291      [card retain];
292      phones = nil;
293      homeAdr = nil;
294      workAdr = nil;
295    }
296  else
297    return [NSException exceptionWithHTTPStatus: 404 /* Not Found */
298                                         reason: @"could not locate contact"];
299
300  data = [NSMutableDictionary dictionaryWithObjectsAndKeys:
301                                [[contact container] nameInContainer], @"pid",
302                              [contact nameInContainer], @"id",
303                              [[card tag] lowercaseString], @"c_component",
304                              nil];
305  o = [card fn];
306  if (o) [data setObject: o forKey: @"c_cn"];
307  o = [card n];
308  if (o)
309    {
310      NSString *lastName = [o flattenedValueAtIndex: 0 forKey: @""];
311      NSString *firstName = [o flattenedValueAtIndex: 1 forKey: @""];
312      if ([lastName length] > 0)
313        [data setObject: lastName forKey: @"c_sn"];
314      if ([firstName length] > 0)
315        [data setObject: firstName forKey: @"c_givenname"];
316    }
317
318  o = [[card uniqueChildWithTag: @"x-aim"] flattenedValuesForKey: @""];
319  if ([o length]) [data setObject: o forKey: @"c_screenname"];
320
321  o = [card nickname];
322  if (o) [data setObject: o forKey: @"nickname"];
323  o = [card titles];
324  if ([o count])
325    [data setObject: [o componentsJoinedByString: @" / "] forKey: @"title"];
326  o = [card role];
327  if ([o length] > 0)
328    [data setObject: o forKey: @"role"];
329  values = [card organizations];
330  if ([values count])
331    {
332      [data setObject: [values objectAtIndex: 0] forKey: @"org"];
333      if ([values count] > 1)
334        [data setObject: [values subarrayWithRange: NSMakeRange(1, [values count] - 1)] forKey: @"orgs"];
335    }
336
337  o = [card certificate];
338  if ([o length])
339    [data setObject: [NSNumber numberWithBool: YES] forKey: @"hasCertificate"];
340
341  o = [card birthday];
342  if (o)
343    [data setObject: [o descriptionWithCalendarFormat: @"%Y-%m-%d"]
344               forKey: @"birthday"];
345
346  o = [card tz];
347  if (o) [data setObject: o forKey: @"tz"];
348
349  o = [card childrenWithTag: @"email"];
350  if ([o count]) [data setObject: o forKey: @"emails"];
351  o = [card childrenWithTag: @"tel"];
352  if ([o count]) [data setObject: o forKey: @"phones"];
353  o = [self categories];
354  if ([o count]) [data setObject: o forKey: @"categories"];
355  o = [self deliveryAddresses];
356  if ([o count] > 0) [data setObject: o forKey: @"addresses"];
357  o = [self urls];
358  if ([o count]) [data setObject: o forKey: @"urls"];
359
360  o = [card notes];
361  if (o) [data setObject: o forKey: @"notes"];
362  o = [self _fetchAndCombineCategoriesList];
363  if (o) [data setObject: o forKey: @"allCategories"];
364  if ([contact hasPhoto])
365    [data setObject: [self photoURL] forKey: @"photoURL"];
366
367  // Custom fields from Thunderbird
368  customFields = [NSMutableDictionary dictionary];
369  if ((o = [[card uniqueChildWithTag: @"custom1"] flattenedValuesForKey: @""]) && [o length])
370    [customFields setObject: o  forKey: @"1"];
371
372  if ((o = [[card uniqueChildWithTag: @"custom2"] flattenedValuesForKey: @""]) && [o length])
373    [customFields setObject: o  forKey: @"2"];
374
375  if ((o = [[card uniqueChildWithTag: @"custom3"] flattenedValuesForKey: @""]) && [o length])
376    [customFields setObject: o  forKey: @"3"];
377
378  if ((o = [[card uniqueChildWithTag: @"custom4"] flattenedValuesForKey: @""]) && [o length])
379    [customFields setObject: o  forKey: @"4"];
380
381  if ([customFields count])
382    [data setObject: customFields  forKey: @"customFields"];
383
384  result = [self responseWithStatus: 200
385                          andString: [data jsonRepresentation]];
386
387  return result;
388}
389
390- (id <WOActionResults>) membersAction
391{
392  NSArray *allUsers;
393  NSDictionary *dict;
394  NSEnumerator *emails;
395  NSMutableArray *allUsersData, *allUserEmails;
396  NSMutableDictionary *userData;
397  NSString *email;
398  SOGoObject <SOGoContactObject> *contact;
399  SOGoObject <SOGoSource> *source;
400  SOGoUser *user;
401  id <WOActionResults> result;
402  unsigned int i, max;
403
404  result = nil;
405  contact = [self clientObject];
406  source = [[contact container] source];
407  dict = [source lookupContactEntryWithUIDorEmail: [contact nameInContainer]
408                                         inDomain: nil];
409
410  if ([[dict objectForKey: @"isGroup"] boolValue])
411    {
412      if ([source conformsToProtocol:@protocol(SOGoMembershipSource)])
413        {
414          allUsers = [(id<SOGoMembershipSource>)(source) membersForGroupWithUID: [dict objectForKey: @"c_uid"]];
415          max = [allUsers count];
416          allUsersData = [NSMutableArray arrayWithCapacity: max];
417          for (i = 0; i < max; i++)
418            {
419              user = [allUsers objectAtIndex: i];
420              allUserEmails = [NSMutableArray array];
421              emails = [[user allEmails] objectEnumerator];
422              while ((email = [emails nextObject])) {
423                [allUserEmails addObject: [NSDictionary dictionaryWithObjectsAndKeys:
424                                                          email, @"value", @"work", @"type", nil]];
425              }
426              userData = [NSDictionary dictionaryWithObjectsAndKeys:
427                                       [user loginInDomain], @"c_uid",
428                                       [user cn], @"c_cn",
429                                       allUserEmails, @"emails", nil];
430              [allUsersData addObject: userData];
431            }
432          dict = [NSDictionary dictionaryWithObject: allUsersData forKey: @"members"];
433          result = [self responseWithStatus: 200
434                                  andString: [dict jsonRepresentation]];
435        }
436      else
437        {
438          result = [self responseWithStatus: 403
439                                  andString: @"Group is not expandable"];
440        }
441    }
442  else
443    {
444      result = [self responseWithStatus: 405
445                              andString: @"Contact is not a group"];
446    }
447
448  return result;
449}
450
451
452- (BOOL) hasPhoto
453{
454  return [[self clientObject] hasPhoto];
455}
456
457- (NSString *) photoURL
458{
459  NSURL *soURL;
460
461  soURL = [[self clientObject] soURL];
462
463  return [NSString stringWithFormat: @"%@/photo", [soURL absoluteString]];
464}
465
466@end /* UIxContactView */
467