Issues (493)

Security Analysis    not enabled

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  Cross-Site Scripting
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Header Injection
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

js/contacts.js (6 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
OC.Contacts = OC.Contacts || {};
2
3
4
(function(window, $, OC) {
5
	'use strict';
6
	/**
7
	* An item which binds the appropriate html and event handlers
8
	* @param parent the parent ContactList
9
	* @param id The integer contact id.
10
	* @param metadata An metadata object containing and 'owner' string variable, a 'backend' string variable and an integer 'permissions' variable.
11
	* @param data the data used to populate the contact
12
	* @param listtemplate the jquery object used to render the contact list item
13
	* @param fulltemplate the jquery object used to render the entire contact
14
	* @param detailtemplates A map of jquery objects used to render the contact parts e.g. EMAIL, TEL etc.
15
	*/
16
	var Contact = function(parent, id, metadata, data, listtemplate, dragtemplate, fulltemplate, detailtemplates) {
17
		//console.log('contact:', id, metadata, data); //parent, id, data, listtemplate, fulltemplate);
18
		this.parent = parent;
19
		this.storage = parent.storage;
20
		this.id = id;
21
		this.metadata = metadata;
22
		this.data = data;
23
		this.$dragTemplate = dragtemplate;
24
		this.$listTemplate = listtemplate;
25
		this.$fullTemplate = fulltemplate;
26
		this.detailTemplates = detailtemplates;
27
		this.displayNames = {};
28
		this.sortOrder = contacts_sortby || 'fn';
29
		this.undoQueue = [];
30
		this.multi_properties = ['EMAIL', 'TEL', 'IMPP', 'ADR', 'URL', 'CLOUD'];
31
	};
32
33
	Contact.prototype.metaData = function() {
34
		return {
35
			contactId: this.id,
36
			addressBookId: this.metadata.parent,
37
			backend: this.metadata.backend
38
		};
39
	};
40
41
	Contact.prototype.getDisplayName = function() {
42
		return this.displayNames[this.sortOrder];
43
	};
44
45
	Contact.prototype.setDisplayMethod = function(method) {
46
		if(this.sortOrder === method) {
47
			return;
48
		}
49
		this.sortOrder = method;
50
		// ~30% faster than jQuery.
51
		try {
52
			this.$listelem.get(0).firstElementChild.getElementsByClassName('nametext')[0].innerHTML = escapeHTML(this.displayNames[method]);
53
			this.setThumbnail();
54
		} catch(e) {
55
			var $elem = this.$listelem.find('.nametext').text(escapeHTML(this.displayNames[method]));
56
			$elem.text(escapeHTML(this.displayNames[method]));
57
		}
58
	};
59
60
	Contact.prototype.getId = function() {
61
		return this.id;
62
	};
63
64
	Contact.prototype.getOwner = function() {
65
		return this.metadata.owner;
66
	};
67
68
	Contact.prototype.setOwner = function(owner) {
69
		this.metadata.owner = owner;
70
	};
71
72
	Contact.prototype.getPermissions = function() {
73
		return this.metadata.permissions;
74
	};
75
76
	Contact.prototype.hasPermission = function(permission) {
77
		//console.log('hasPermission', this.getPermissions(), permission, this.getPermissions() & permission);
78
		return (this.getPermissions() & permission);
79
	};
80
81
	Contact.prototype.getParent = function() {
82
		return this.metadata.parent;
83
	};
84
85
	Contact.prototype.setParent = function(parent) {
86
		this.metadata.parent = parent;
87
	};
88
89
	Contact.prototype.getBackend = function() {
90
		return this.metadata.backend;
91
	};
92
93
	Contact.prototype.setBackend = function(backend) {
94
		this.metadata.backend = backend;
95
	};
96
97
	Contact.prototype.hasPhoto = function() {
98
		return (this.getId() !== 'new' && this.data && this.data.photo) || false;
99
	};
100
101
	Contact.prototype.isOpen = function() {
102
		return this.$fullelem !== null;
103
	};
104
105
	Contact.prototype.reload = function(data) {
106
		console.log('Contact.reload', data);
107
		this.id = data.metadata.id;
108
		this.metadata = data.metadata;
109
		this.data = data.data;
110
		/*if(this.$fullelem) {
111
			this.$fullelem.replaceWith(this.renderContact(this.groupprops));
112
		}*/
113
	};
114
115
	Contact.prototype.merge = function(mergees) {
116
		console.log('Contact.merge, mergees', mergees);
117
		if(!mergees instanceof Array && !mergees instanceof Contact) {
0 ignored issues
show
The usage of ! looks confusing here.

The following shows a case which JSHint considers confusing and its respective non-confusing counterpart:

! (str.indexOf(i) > -1) // Bad
str.indexOf(i) === -1 // Good
Loading history...
118
			throw new TypeError('BadArgument: Contact.merge() only takes Contacts');
119
		} else {
120
			if(mergees instanceof Contact) {
121
				mergees = [mergees];
122
			}
123
		}
124
125
		// For multi_properties
126
		var addIfNotExists = function(name, newproperty) {
127
			// If the property isn't set at all just add it and return.
128
			if(!self.data[name]) {
129
				self.data[name] = [newproperty];
130
				return;
131
			}
132
			var found = false;
133
			$.each(self.data[name], function(idx, property) {
134
				if(name === 'ADR') {
135
					// Do a simple string comparison
136
					if(property.value.join(';').toLowerCase() === newproperty.value.join(';').toLowerCase()) {
137
						found = true;
138
						return false; // break loop
139
					}
140
				} else {
141
					if(property.value.toLowerCase() === newproperty.value.toLowerCase()) {
142
						found = true;
143
						return false; // break loop
144
					}
145
				}
146
			});
147
			if(found) {
148
				return;
149
			}
150
			// Not found, so adding it.
151
			self.data[name].push(newproperty);
152
		};
153
154
		var self = this;
155
		$.each(mergees, function(idx, mergee) {
156
			console.log('Contact.merge, mergee', mergee);
157
			if(!mergee instanceof Contact) {
0 ignored issues
show
The usage of ! looks confusing here.

The following shows a case which JSHint considers confusing and its respective non-confusing counterpart:

! (str.indexOf(i) > -1) // Bad
str.indexOf(i) === -1 // Good
Loading history...
158
				throw new TypeError('BadArgument: Contact.merge() only takes Contacts');
159
			}
160
			if(mergee === self) {
161
				throw new Error('BadArgument: Why should I merge with myself?');
162
			}
163
			$.each(mergee.data, function(name, properties) {
164
				if(self.multi_properties.indexOf(name) === -1) {
165
					if(self.data[name] && self.data[name].length > 0) {
166
						// If the property exists don't touch it.
167
						return true; // continue
168
					} else {
169
						// Otherwise add it.
170
						self.data[name] = properties;
171
					}
172
				} else {
173
					$.each(properties, function(idx, property) {
174
						addIfNotExists(name, property);
175
					});
176
				}
177
			});
178
			console.log('Merged', self.data);
179
		});
180
		return true;
181
	};
182
183
	Contact.prototype.showActions = function(act) {
184
		// non-destructive merge.
185
		var $actions = $.merge($.merge([], this.$footer.children()), this.$header.children());
186
		$.each($actions, function(idx, action) {
187
			$(action).hide();
188
			$.each(act, function(i, a) {
189
				if($(action).hasClass(a)) {
190
					$(action).show();
191
					return false; // break
192
				}
193
			});
194
		});
195
	};
196
197
	Contact.prototype.setAsSaving = function(obj, state) {
198
		if(!obj) {
199
			return;
200
		}
201
		$(obj).prop('disabled', state);
202
		$(obj).toggleClass('loading', state);
203
		/*if(state) {
204
			$(obj).addClass('loading');
205
		} else {
206
			$(obj).removeClass('loading');
207
		}*/
208
	};
209
210
	Contact.prototype.handleURL = function(obj) {
211
		if(!obj) {
212
			return;
213
		}
214
		var $container = this.propertyContainerFor(obj);
215
		$(document).trigger('request.openurl', {
216
			type: $container.data('element'),
217
			url: this.valueFor(obj)
218
		});
219
	};
220
221
	/**
222
	 * Update group name internally. No saving as this is done by groups backend.
223
	 */
224
	Contact.prototype.renameGroup = function(from, to) {
225
		if(!this.data.CATEGORIES.length) {
226
			console.warn(this.getDisplayName(), 'had no groups!?!');
227
			return;
228
		}
229
		var groups = this.data.CATEGORIES[0].value;
230
		var self = this;
231
		$.each(groups, function(idx, group) {
232
			if(from.toLowerCase() === group.toLowerCase()) {
233
				console.log('Updating group name for', self.getDisplayName(), group, to);
234
				self.data.CATEGORIES[0].value[idx] = to;
235
				return false; // break
236
			}
237
		});
238
		$(document).trigger('status.contact.updated', {
239
			property: 'CATEGORIES',
240
			contact: this
241
		});
242
	};
243
244
	Contact.prototype.pushToUndo = function(params) {
245
		// Check if the same property has been changed before
246
		// and update it's checksum if so.
247
		if(typeof params.oldchecksum !== 'undefined') {
248
			$.each(this.undoQueue, function(idx, item) {
249
				if(item.checksum === params.oldchecksum) {
250
					item.checksum = params.newchecksum;
251
					if(params.action === 'delete') {
252
						item.action = 'delete';
253
					}
254
					return false; // Break loop
255
				}
256
			});
257
		}
258
		this.undoQueue.push({
259
			action:params.action, 
260
			name: params.name,
261
			checksum: params.newchecksum,
262
			newvalue: params.newvalue,
263
			oldvalue: params.oldvalue
264
		});
265
		//console.log('undoQueue', this.undoQueue);
266
	};
267
	
268
	Contact.prototype.addProperty = function($option, name) {
269
		console.log('Contact.addProperty', name);
270
		var $elem, $list;
271
		switch(name) {
272
			case 'NICKNAME':
273
			case 'TITLE':
274
			case 'ORG':
275
			case 'BDAY':
276
			case 'NOTE':
277
				$elem = this.$fullelem.find('[data-element="' + name.toLowerCase() + '"]');
278
				$elem.addClass('new').show();
279
				$list = this.$fullelem.find('ul.' + name.toLowerCase());
280
				$list.show();
281
				$elem.find('input:not(:checkbox),textarea').first().focus();
282
				$option.prop('disabled', true);
283
				break;
284
			case 'TEL':
285
			case 'URL':
286
			case 'CLOUD':
287
			case 'EMAIL':
288
				$elem = this.renderStandardProperty(name.toLowerCase());
289
				$list = this.$fullelem.find('ul.' + name.toLowerCase());
290
				$list.show();
291
				$list.append($elem);
292
				$elem.find('input.value').addClass('new');
293
				$elem.find('input:not(:checkbox)').first().focus();
294
				break;
295
			case 'ADR':
296
				$elem = this.renderAddressProperty();
297
				$list = this.$fullelem.find('ul.' + name.toLowerCase());
298
				$list.show();
299
				$list.append($elem);
300
				$elem.find('.display').trigger('click');
301
				$elem.find('input.value').addClass('new');
302
				$elem.find('input:not(:checkbox)').first().focus();
303
				break;
304
			case 'IMPP':
305
				$elem = this.renderIMProperty();
306
				$list = this.$fullelem.find('ul.' + name.toLowerCase());
307
				$list.show();
308
				$list.append($elem);
309
				$elem.find('input.value').addClass('new');
310
				$elem.find('input:not(:checkbox)').first().focus();
311
				break;
312
		}
313
314
		if($elem) {
315
			// If there's already a property of this type enable setting as preferred.
316
			if(this.multi_properties.indexOf(name) !== -1 && this.data[name] && this.data[name].length > 0) {
317
				var selector = 'li[data-element="' + name.toLowerCase() + '"]';
318
				$.each(this.$fullelem.find(selector), function(idx, elem) {
319
					$(elem).find('input.parameter[value="PREF"]').show();
320
				});
321
			} else if(this.multi_properties.indexOf(name) !== -1) {
322
				$elem.find('input.parameter[value="PREF"]').hide();
323
			}
324
			$elem.find('select.type[name="parameters[TYPE][]"], select.type[name="parameters[X-SERVICE-TYPE]"]')
325
				.combobox({
326
					singleclick: true,
327
					classes: ['propertytype', 'float', 'label']
328
				});
329
		}
330
	};
331
332
	Contact.prototype.deleteProperty = function(params) {
333
		var obj = params.obj;
334
		params = {};
335
		if(!this.enabled) {
336
			return;
337
		}
338
		var element = this.propertyTypeFor(obj);
339
		var $container = this.propertyContainerFor(obj);
340
		console.log('Contact.deleteProperty, element', element, $container);
341
		params.name = element;
342
		params.value = null;
343
344
		if(this.multi_properties.indexOf(element) !== -1) {
345
			params.checksum = this.checksumFor(obj);
346
			if(params.checksum === 'new' && $.trim(this.valueFor(obj)) === '') {
347
				// If there's only one property of this type enable setting as preferred.
348
				if((undefined !== this.data[element] && this.data[element].length) && (this.data[element].length === 1)) {
349
					var selector = 'li[data-element="' + element.toLowerCase() + '"]';
350
					this.$fullelem.find(selector).find('input.parameter[value="PREF"]').hide();
351
				}
352
				// Hide propertygroup if there are no properties in it
353
				if(!(undefined !== this.data[element] && this.data[element].length)) {
354
					$(obj).parent().parent().parent().hide();
355
				}
356
				else if(this.data[element].length === 0) {
357
					$(obj).parent().parent().parent().hide();
358
				}
359
				$container.remove();
360
				return;
361
			}
362
		}
363
		this.setAsSaving(obj, true);
364
		var self = this;
365
		$.when(this.storage.patchContact(this.metadata.backend, this.metadata.parent, this.id, params))
366
			.then(function(response) {
367
			if(!response.error) {
368
				if(self.multi_properties.indexOf(element) !== -1) {
369
					// First find out if an existing element by looking for checksum
370
					var checksum = self.checksumFor(obj);
371
					self.pushToUndo({
372
						action:'delete', 
373
						name: element,
374
						oldchecksum: self.checksumFor(obj),
375
						newvalue: self.valueFor(obj)
376
					});
377
					if(checksum) {
378
						for(var i in self.data[element]) {
379
							if(self.data[element][i].checksum === checksum) {
380
								// Found it
381
								self.data[element].splice(self.data[element].indexOf(self.data[element][i]), 1);
382
								break;
383
							}
384
						}
385
					}
386
					// If there's only one property of this type enable setting as preferred.
387
					if((undefined !== self.data[element] && self.data[element].length) && (self.data[element].length === 1)) {
388
						var selector = 'li[data-element="' + element.toLowerCase() + '"]';
389
						self.$fullelem.find(selector).find('input.parameter[value="PREF"]').hide();
390
					}
391
					// Hide propertygroup if there are no properties in it
392
					if(!(undefined !== self.data[element] && self.data[element].length)) {
393
						$(obj).parent().parent().parent().hide();
394
					}
395
					else if(self.data[element].length === 0) {
396
						$(obj).parent().parent().parent().hide();
397
					}
398
					$container.remove();
399
				} else {
400
					self.pushToUndo({
401
						action:'delete', 
402
						name: element,
403
						newvalue: $container.find('input.value').val()
404
					});
405
					self.setAsSaving(obj, false);
406
					if(element === 'PHOTO') {
407
						self.data.photo = false;
408
						self.data.thumbnail = null;
409
					} else {
410
						self.$fullelem.find('[data-element="' + element.toLowerCase() + '"]').hide();
411
						$container.find('input.value').val('');
412
						self.$addMenu.find('option[value="' + element.toUpperCase() + '"]').prop('disabled', false);
413
					}
414
				}
415
				$(document).trigger('status.contact.updated', {
416
					property: element,
417
					contact: self
418
				});
419
				return true;
420
			} else {
421
				$(document).trigger('status.contacts.error', response);
422
				self.setAsSaving(obj, false);
423
				return false;
424
			}
425
		})
426
		.fail(function(response) {
427
			console.log(response.message);
428
			$(document).trigger('status.contacts.error', response);
429
		});
430
	};
431
432
	/**
433
	 * @brief Save all properties. Used for merging contacts.
434
	 * If this is a new contact it will first be saved to the datastore and a
435
	 * new datastructure will be added to the object.
436
	 */
437
	Contact.prototype.saveAll = function(cb) {
438
		console.log('Contact.saveAll');
439
		var self = this;
440
		if(!this.id) {
441
			this.add({isnew:true}, function(response) {
442
				if(response.error) {
443
					console.warn('No response object');
444
					return false;
445
				}
446
				self.saveAll();
447
			});
448
			return;
449
		}
450
451
		this.setAsSaving(this.$fullelem, true);
452
453
		$.when(this.storage.saveAllProperties(this.metadata.backend, this.metadata.parent, this.id, {data:this.data}))
454
			.then(function(response) {
455
			if(!response.error) {
456
				self.data = response.data.data;
457
				self.metadata = response.data.metadata;
458
				if(typeof cb === 'function') {
459
					cb({error:false});
460
				}
461
			} else {
462
				$(document).trigger('status.contacts.error', {
463
					message: response.message
464
				});
465
				if(typeof cb === 'function') {
466
					cb({error:true, message:response.message});
467
				}
468
			}
469
			self.setAsSaving(self.$fullelem, false);
470
		});
471
	};
472
473
	/**
474
	 * @brief Act on change of a property.
475
	 * If this is a new contact it will first be saved to the datastore and a
476
	 * new datastructure will be added to the object.
477
	 * If the obj argument is not provided 'name' and 'value' MUST be provided
478
	 * and this is only allowed for single elements like N, FN, CATEGORIES.
479
	 * @param obj. The form form field that has changed.
480
	 * @param name. The optional name of the element.
481
	 * @param value. The optional value.
482
	 */
483
	Contact.prototype.saveProperty = function(params) {
484
		console.log('Contact.saveProperty', params);
485
		var self = this;
486
487
		if(!this.id) {
488
			this.add({isnew:true}, function(response) {
489
				if(!response || response.status === 'error') {
490
					console.warn('No response object');
491
					return false;
492
				}
493
				self.saveProperty(params);
494
				self.showActions(['close', 'add', 'export', 'delete']);
495
			});
496
			return;
497
		}
498
		var obj = null;
499
		var element = null;
500
		var args = [];
501
502
		if(params.obj) {
503
			obj = params.obj;
504
			args = this.argumentsFor(obj);
505
			//args['parameters'] = $.param(this.parametersFor(obj));
506
			element = this.propertyTypeFor(obj);
507
		} else {
508
			args = params;
509
			element = params.name;
510
		}
511
512
		if(!args) {
513
			console.log('No arguments. returning');
514
			return false;
515
		}
516
		console.log('args', args);
517
518
		this.setAsSaving(obj, true);
519
		$.when(this.storage.patchContact(this.metadata.backend, this.metadata.parent, this.id, args))
520
			.then(function(response) {
521
			if(!response.error) {
522
				if(!self.data[element]) {
523
					self.data[element] = [];
524
				}
525
				if(self.multi_properties.indexOf(element) !== -1) {
526
					// First find out if an existing element by looking for checksum
527
					var checksum = self.checksumFor(obj);
528
					var value = self.valueFor(obj);
529
					var parameters = self.parametersFor(obj);
530
					if(parameters.TYPE && parameters.TYPE.indexOf('PREF') !== -1) {
531
						parameters.PREF = 1;
532
						parameters.TYPE.splice(parameters.TYPE.indexOf('PREF'), 1);
533
					}
534
					if(checksum && checksum !== 'new') {
535
						self.pushToUndo({
536
							action:'save', 
537
							name: element,
538
							newchecksum: response.data.checksum,
539
							oldchecksum: checksum,
540
							newvalue: value,
541
							oldvalue: obj.defaultValue
542
						});
543
						$.each(self.data[element], function(i, el) {
544
							if(el.checksum === checksum) {
545
								self.data[element][i] = {
546
									name: element,
547
									value: value,
548
									parameters: parameters,
549
									checksum: response.data.checksum
550
								};
551
								return false;
552
							}
553
						});
554
					} else {
555
						$(obj).removeClass('new');
556
						self.pushToUndo({
557
							action:'add', 
558
							name: element,
559
							newchecksum: response.data.checksum,
560
							newvalue: value
561
						});
562
						self.data[element].push({
563
							name: element,
564
							value: value,
565
							parameters: parameters,
566
							checksum: response.data.checksum
567
						});
568
					}
569
					self.propertyContainerFor(obj).data('checksum', response.data.checksum);
570
				} else {
571
					// Save value and parameters internally
572
					var value = obj ? self.valueFor(obj) : params.value;
0 ignored issues
show
It seems like value was already defined.
Loading history...
573
					self.pushToUndo({
574
						action: ((obj && obj.defaultValue) || self.data[element].length) ? 'save' : 'add', // FIXME
575
						name: element,
576
						newvalue: value
577
					});
578
					switch(element) {
579
						case 'CATEGORIES':
580
							// We deal with this in addToGroup()
581
							break;
582
						case 'BDAY':
583
							// reverse order again.
584
							value = $.datepicker.formatDate('yy-mm-dd', $.datepicker.parseDate(datepickerFormatDate, value));
585
							self.data[element][0] = {
586
								name: element,
587
								value: value,
588
								parameters: self.parametersFor(obj),
589
								checksum: response.data.checksum
590
							};
591
							break;
592
						case 'FN':
593
							if(!self.data.FN || !self.data.FN.length) {
594
								self.data.FN = [{name:'FN', value:'', parameters:[]}];
595
							}
596
							self.data.FN[0].value = value;
597
							// Used for sorting list elements
598
							self.displayNames.fn = value;
599
							var nempty = true;
600
							if(!self.data.N) {
601
								// TODO: Maybe add a method for constructing new elements?
602
								self.data.N = [{name:'N',value:['', '', '', '', ''],parameters:[]}];
603
							}
604
							$.each(self.data.N[0].value, function(idx, val) {
605
								if(val) {
606
									nempty = false;
607
									return false;
608
								}
609
							});
610
							if(nempty) {
611
								self.data.N[0].value = ['', '', '', '', ''];
612
								var nvalue = value.split(' ');
613
								// Very basic western style parsing. I'm not gonna implement
614
								// https://github.com/android/platform_packages_providers_contactsprovider/blob/master/src/com/android/providers/contacts/NameSplitter.java ;)
615
								self.data.N[0].value[0] = nvalue.length > 2 && nvalue.slice(nvalue.length-1).toString() || nvalue[1] || '';
616
								self.data.N[0].value[1] = nvalue[0] || '';
617
								self.data.N[0].value[2] = nvalue.length > 2 && nvalue.slice(1, nvalue.length-1).join(' ') || '';
618
								setTimeout(function() {
619
									self.saveProperty({name:'N', value:self.data.N[0].value.join(';')});
620
								}, 500);
621
							}
622
							// If contacts doesn't have a photo load new avatar
623
							if(!self.hasPhoto() && self.sortOrder === 'fn') {
624
								self.loadAvatar();
625
							}
626
							break;
627
						case 'N':
628
							if(!utils.isArray(value)) {
629
								// Then it is auto-generated from FN.
630
								value = value.split(';');
631
632
								$.each(value, function(idx, val) {
633
									self.$fullelem.find('#n_' + idx).val(val).get(0).defaultValue = val;
634
								});
635
							}
636
637
							// Used for sorting list elements
638
							self.displayNames.fl = value.slice(0, 2).reverse().join(' ');
639
							self.displayNames.lf = value.slice(0, 2).join(', ').trim();
640
641
							var $fullname = self.$fullelem.find('.fullname');
642
							var update_fn = false;
643
							if(!self.data.FN) {
644
								self.data.FN = [{name:'FN', value:'', parameters:[]}];
645
							}
646
							/* If FN is empty fill it with the values from N.
647
							 * As N consists of several fields which each trigger a change/save
648
							 * also check if the contents of FN equals parts of N and fill
649
							 * out the rest.
650
							 */
651
							if(self.data.FN[0].value === '') {
652
								self.data.FN[0].value = value[1] + ' ' + value[0];
653
								$fullname.val(self.data.FN[0].value);
654
								update_fn = true;
655
							} else if($fullname.val() === value[1] + ' ') {
656
								self.data.FN[0].value = value[1] + ' ' + value[0];
657
								$fullname.val(self.data.FN[0].value);
658
								update_fn = true;
659
							} else if($fullname.val() === ' ' + value[0]) {
660
								self.data.FN[0].value = value[1] + ' ' + value[0];
661
								$fullname.val(self.data.FN[0].value);
662
								update_fn = true;
663
							}
664
							if(update_fn) {
665
								setTimeout(function() {
666
									self.saveProperty({name:'FN', value:self.data.FN[0].value});
667
								}, 1000);
668
							}
669
							if(!self.hasPhoto() && self.sortOrder !== 'fn') {
670
								self.loadAvatar();
671
							}
0 ignored issues
show
Expected a 'break' statement before 'case'.
Loading history...
672
						case 'NICKNAME':
673
							/* falls through */
674
						case 'ORG':
675
							// Auto-fill FN if empty
676
							if(!self.data.FN) {
677
								self.data.FN = [{name:'FN', value:value, parameters:[]}];
678
								self.$fullelem.find('.fullname').val(value).trigger('change');
679
							}
0 ignored issues
show
Expected a 'break' statement before 'case'.
Loading history...
680
						case 'TITLE':
681
							/* falls through */
682
						case 'NOTE':
683
							self.data[element][0] = {
684
								name: element,
685
								value: value,
686
								parameters: self.parametersFor(obj),
687
								checksum: response.data.checksum
688
							};
689
							break;
690
						default:
691
							break;
692
					}
693
				}
694
				self.setAsSaving(obj, false);
695
				$(document).trigger('status.contact.updated', {
696
					property: element,
697
					contact: self
698
				});
699
				return true;
700
			} else {
701
				$(document).trigger('status.contacts.error', response);
702
				self.setAsSaving(obj, false);
703
				return false;
704
			}
705
		});
706
	};
707
708
	/**
709
	 * Hide contact list element.
710
	 */
711
	Contact.prototype.hide = function() {
712
		this.getListItemElement().hide();
713
	};
714
715
	/**
716
	 * Show contact list element.
717
	 */
718
	Contact.prototype.show = function() {
719
		this.getListItemElement().show();
720
	};
721
722
	/**
723
	 * Remove any open contact from the DOM.
724
	 */
725
	Contact.prototype.close = function(showListElement) {
726
		$(document).unbind('status.contact.photoupdated');
727
		console.log('Contact.close', this);
728
		if(this.$fullelem) {
729
			this.$fullelem.hide().remove();
730
			if(showListElement) {
731
				this.getListItemElement().show();
732
			}
733
			this.$fullelem = null;
734
			return true;
735
		} else {
736
			return false;
737
		}
738
	};
739
740
	/**
741
	 * Remove any open contact from the DOM and detach it's list
742
	 * element from the DOM.
743
	 * @returns The contact object.
744
	 */
745
	Contact.prototype.detach = function() {
746
		if(this.$fullelem) {
747
			this.$fullelem.remove();
748
		}
749
		if(this.$listelem) {
750
			this.$listelem.detach();
751
			return this;
752
		}
753
	};
754
755
	/**
756
	 * Set a contacts list element as (un)checked
757
	 * @returns The contact object.
758
	 */
759
	Contact.prototype.setChecked = function(checked) {
760
		if(this.$listelem) {
761
			this.$listelem.find('input:checkbox').prop('checked', checked);
762
			return this;
763
		}
764
	};
765
766
	/**
767
	 * Set a contact to en/disabled depending on its permissions.
768
	 * @param boolean enabled
769
	 */
770
	Contact.prototype.setEnabled = function(enabled) {
771
		if(enabled) {
772
			this.$fullelem.find('#addproperty').show();
773
		} else {
774
			this.$fullelem.find('#addproperty,.action.delete,.action.edit').hide();
775
		}
776
		this.enabled = enabled;
777
		this.$fullelem.find('.value,.action,.parameter').each(function () {
778
			$(this).prop('disabled', !enabled);
779
		});
780
		$(document).trigger('status.contact.enabled', enabled);
781
	};
782
783
	/**
784
	 * Add a contact to data store.
785
	 * @params params. An object which can contain the optional properties:
786
	 *		aid: The id of the addressbook to add the contact to. Per default it will be added to the first.
787
	 *		fn: The formatted name of the contact.
788
	 * @param cb Optional callback function which
789
	 * @returns The callback gets an object as argument with a variable 'status' of either 'success'
790
	 * or 'error'. On success the 'data' property of that object contains the contact id as 'id', the
791
	 * addressbook id as 'aid' and the contact data structure as 'details'.
792
	 */
793
	Contact.prototype.add = function(params, cb) {
794
		var self = this;
795
		$.when(this.storage.addContact(this.metadata.backend, this.metadata.parent))
796
			.then(function(response) {
797
			if(!response.error) {
798
				self.id = String(response.metadata.id);
799
				self.metadata = response.metadata;
800
				self.data = response.data;
801
				console.log('Contact.add, groupprops', self.groupprops);
802
				if(self.groupprops && self.groupprops.groups.length > 0) {
803
					self._buildGroupSelect(self.groupprops.groups);
804
					self.$groupSelect.multiselect('enable');
805
				}
806
				// Add contact to current group
807
				if(self.groupprops
808
					&& ['all', 'fav', 'uncategorized'].indexOf(self.groupprops.currentgroup.id) === -1
809
				) {
810
					if(!self.data.CATEGORIES) {
811
						self.addToGroup(self.groupprops.currentgroup.name);
812
						$(document).trigger('request.contact.addtogroup', {
813
							id: self.id,
814
							groupid: self.groupprops.currentgroup.id
815
						});
816
						self.$groupSelect.find('option[value="' + self.groupprops.currentgroup.id + '"]')
817
							.attr('selected', 'selected');
818
						self.$groupSelect.multiselect('refresh');
819
					}
820
				}
821
				$(document).trigger('status.contact.added', {
822
					id: self.id,
823
					contact: self
824
				});
825
			} else {
826
				$(document).trigger('status.contacts.error', response);
827
				return false;
828
			}
829
			if(typeof cb === 'function') {
830
				cb(response);
831
			}
832
		});
833
	};
834
	/**
835
	 * Delete contact from data store and remove it from the DOM
836
	 * @param cb Optional callback function which
837
	 * @returns An object with a variable 'status' of either success
838
	 *	or 'error'
839
	 */
840
	Contact.prototype.destroy = function(cb) {
841
		var self = this;
842
		$.when(this.storage.deleteContact(
843
			this.metadata.backend,
844
			this.metadata.parent,
845
			this.id)
846
		).then(function(response) {
847
			if(!response.error) {
848
				if(self.$listelem) {
849
					self.$listelem.remove();
850
				}
851
				if(self.$fullelem) {
852
					self.$fullelem.remove();
853
				}
854
			}
855
			if(typeof cb === 'function') {
856
				if(response.error) {
857
					cb(response);
858
				} else {
859
					cb({id:self.id});
860
				}
861
			}
862
		}).fail(function(response) {
863
			if(typeof cb === 'function') {
864
				cb(response);
865
			}
866
		});
867
	};
868
869
	Contact.prototype.argumentsFor = function(obj) {
870
		console.log('Contact.argumentsFor', $(obj));
871
		var args = {};
872
		var ptype = this.propertyTypeFor(obj);
873
		args.name = ptype;
874
875
		if(this.multi_properties.indexOf(ptype) !== -1) {
876
			args.checksum = this.checksumFor(obj);
877
		}
878
879
		if($(obj).hasClass('propertycontainer')) {
880
			if($(obj).is('select[data-element="categories"]')) {
881
				args.value = [];
882
				$.each($(obj).find(':selected'), function(idx, e) {
883
					args.value.push($(e).text());
884
				});
885
			} else {
886
				args.value = $(obj).val();
887
			}
888
		} else {
889
			var $elements = this.propertyContainerFor(obj)
890
				.find('input.value,select.value,textarea.value');
891
			if($elements.length > 1) {
892
				args.value = [];
893
				$.each($elements, function(idx, e) {
894
					args.value[parseInt($(e).attr('name').substr(6,1))] = $(e).val();
895
					//args['value'].push($(e).val());
896
				});
897
			} else {
898
				var value = $elements.val();
899
				switch(args.name) {
900
					case 'BDAY':
901
						try {
902
							args.value = $.datepicker.formatDate('yy-mm-dd', $.datepicker.parseDate(datepickerFormatDate, value));
903
						} catch(e) {
904
							$(document).trigger(
905
								'status.contacts.error',
906
								{message:t('contacts', 'Error parsing date: {date}', {date:value})}
907
							);
908
							return false;
909
						}
910
						break;
911
					default:
912
						args.value = value;
913
						break;
914
				}
915
			}
916
		}
917
		args.parameters = this.parametersFor(obj);
918
		console.log('Contact.argumentsFor', args);
919
		return args;
920
	};
921
922
	Contact.prototype.queryStringFor = function(obj) {
923
		var q = 'id=' + this.id;
924
		var ptype = this.propertyTypeFor(obj);
925
		q += '&name=' + ptype;
926
927
		if(this.multi_properties.indexOf(ptype) !== -1) {
928
			q += '&checksum=' + this.checksumFor(obj);
929
		}
930
931
		if($(obj).hasClass('propertycontainer')) {
932
			if($(obj).is('select[data-element="categories"]')) {
933
				$.each($(obj).find(':selected'), function(idx, e) {
934
					q += '&value=' + encodeURIComponent($(e).text());
935
				});
936
			} else {
937
				q += '&value=' + encodeURIComponent($(obj).val());
938
			}
939
		} else {
940
			var $elements = this.propertyContainerFor(obj)
941
				.find('input.value,select.value,textarea.value,.parameter');
942
			if($elements.length > 1) {
943
				q += '&' + $elements.serialize();
944
			} else {
945
				q += '&value=' + encodeURIComponent($elements.val());
946
			}
947
		}
948
		return q;
949
	};
950
951
	Contact.prototype.propertyContainerFor = function(obj) {
952
		return $(obj).hasClass('propertycontainer')
953
			? $(obj)
954
			: $(obj).parents('.propertycontainer').first();
955
	};
956
957
	Contact.prototype.checksumFor = function(obj) {
958
		return this.propertyContainerFor(obj).data('checksum');
959
	};
960
961
	Contact.prototype.valueFor = function(obj) {
962
		var $container = this.propertyContainerFor(obj);
963
		console.assert($container.length > 0, 'Couldn\'t find container for ' + $(obj));
964
		return $container.is('input.value')
965
			? $container.val()
966
			: (function() {
967
				var $elem = $container.find('textarea.value,input.value:not(:checkbox)');
968
				console.assert($elem.length > 0, 'Couldn\'t find value for ' + $container.data('element'));
969
				if($elem.length === 1) {
970
					return $elem.val();
971
				} else if($elem.length > 1) {
972
					var retval = [];
973
					$.each($elem, function(idx, e) {
974
						retval[parseInt($(e).attr('name').substr(6,1))] = $(e).val();
975
					});
976
					return retval;
977
				}
978
			})();
979
	};
980
981
	Contact.prototype.parametersFor = function(obj, asText) {
982
		var parameters = {};
983
		$.each(this.propertyContainerFor(obj)
984
			.find('select.parameter,input:checkbox:checked.parameter'), function(i, elem) {
985
			var $elem = $(elem);
986
			var paramname = $elem.data('parameter');
987
			if(!parameters[paramname]) {
988
				parameters[paramname] = [];
989
			}
990
			if($elem.is(':checkbox')) {
991
				if(asText) {
992
					parameters[paramname].push($elem.attr('title'));
993
				} else {
994
					parameters[paramname].push($elem.attr('value'));
995
				}
996
			} else if($elem.is('select')) {
997
				$.each($elem.find(':selected'), function(idx, e) {
998
					if(asText) {
999
						parameters[paramname].push($(e).text());
1000
					} else {
1001
						parameters[paramname].push($(e).val());
1002
					}
1003
				});
1004
			}
1005
		});
1006
		return parameters;
1007
	};
1008
1009
	Contact.prototype.propertyTypeFor = function(obj) {
1010
		var ptype = this.propertyContainerFor(obj).data('element');
1011
		return ptype ? ptype.toUpperCase() : null;
1012
	};
1013
1014
	/**
1015
	 * Render an element item to be shown during drag.
1016
	 * @return A jquery object
1017
	 */
1018
	Contact.prototype.renderDragItem = function() {
1019
		if(typeof this.$dragelem === 'undefined') {
1020
			this.$dragelem = this.$dragTemplate.octemplate({
1021
				id: this.id,
1022
				name: this.getPreferredValue('FN', '')
1023
			});
1024
		}
1025
		this.setThumbnail(this.$dragelem);
1026
		return this.$dragelem;
1027
	};
1028
1029
	/**
1030
	 * Render the list item
1031
	 * @return A jquery object to be inserted in the DOM
1032
	 */
1033
	Contact.prototype.renderListItem = function(isnew) {
1034
		this.displayNames.fn = this.getPreferredValue('FN')
1035
			|| this.getPreferredValue('ORG', []).pop()
1036
			|| this.getPreferredValue('EMAIL')
1037
			|| this.getPreferredValue('TEL');
1038
1039
		this.displayNames.fl = this.getPreferredValue('N', [this.displayNames.fn])
1040
			.slice(0, 2).reverse().join(' ');
1041
1042
		this.displayNames.lf = this.getPreferredValue('N', [this.displayNames.fn])
1043
			.slice(0, 2).join(', ').trim();
1044
		// Fix misplaced comma if either first or last name is missing
1045
		if(this.displayNames.lf[0] === ',') {
1046
			this.displayNames.lf = this.displayNames.lf.substr(1);
1047
		}
1048
		if(this.displayNames.lf[this.displayNames.lf.length-1] === ',') {
1049
			this.displayNames.lf = this.displayNames.lf.substr(0, this.displayNames.lf.length-1);
1050
		}
1051
1052
		this.$listelem = this.$listTemplate.octemplate({
1053
			id: this.id,
1054
			parent: this.metadata.parent,
1055
			backend: this.metadata.backend,
1056
			name: this.getDisplayName(),
1057
			email: this.getPreferredValue('EMAIL', ''),
1058
			tel: this.getPreferredValue('TEL', ''),
1059
			adr: this.getPreferredValue('ADR', []).clean('').join(', '),
1060
			categories: this.getPreferredValue('CATEGORIES', [])
1061
				.clean('').join(' / ')
1062
		});
1063
		if (this.getOwner() !== OC.currentUser
1064
			&& !(this.metadata.permissions & OC.PERMISSION_UPDATE
1065
			|| this.metadata.permissions & OC.PERMISSION_DELETE)
1066
		) {
1067
			this.$listelem.find('input:checkbox').prop('disabled', true).css('opacity', '0');
1068
		} else {
1069
			var self = this;
1070
			this.$listelem.find('td.name')
1071
				.draggable({
1072
					cursor: 'move',
1073
					distance: 10,
1074
					revert: 'invalid',
1075
					helper: function(/*event, ui*/) {
1076
						return self.renderDragItem().appendTo('body');
1077
					},
1078
					opacity: 1,
1079
					scope: 'contacts'
1080
				});
1081
		}
1082
		if(isnew) {
1083
			this.setThumbnail();
1084
		}
1085
		this.$listelem.data('obj', this);
1086
		return this.$listelem;
1087
	};
1088
1089
	Contact.prototype._buildGroupSelect = function(availableGroups) {
1090
		var self = this;
1091
		this.$fullelem.find('.groupscontainer').show();
1092
		//this.$groupSelect.find('option').remove();
1093
		$.each(availableGroups, function(idx, group) {
1094
			var $option = $('<option value="' + group.id + '">' + escapeHTML(group.name) + '</option>');
1095
			if(self.inGroup(group.name)) {
1096
				$option.attr('selected', 'selected');
1097
			}
1098
			self.$groupSelect.append($option);
1099
		});
1100
		self.$groupSelect.multiselect({
1101
			header: false,
1102
			selectedList: 3,
1103
			noneSelectedText: t('contacts', 'Select groups'),
1104
			selectedText: t('contacts', '# groups'),
1105
			minWidth: 300
1106
		});
1107
		self.$groupSelect.bind('multiselectclick', function(event, ui) {
1108
			var action = ui.checked ? 'addtogroup' : 'removefromgroup';
1109
			console.assert(typeof self.id === 'string', 'ID is not a string');
1110
			$(document).trigger('request.contact.' + action, {
1111
				id: self.id,
1112
				groupid: parseInt(ui.value)
1113
			});
1114
			if(ui.checked) {
1115
				self.addToGroup(ui.text);
1116
			} else {
1117
				self.removeFromGroup(ui.text);
1118
			}
1119
		});
1120
		if(!self.id || !self.hasPermission(OC.PERMISSION_UPDATE)) {
1121
			self.$groupSelect.multiselect('disable');
1122
		}
1123
	};
1124
1125
	Contact.prototype._buildAddressBookSelect = function(availableAddressBooks) {
1126
		var self = this;
1127
		console.log('address books', availableAddressBooks.length, availableAddressBooks);
1128
		$.each(availableAddressBooks, function(idx, addressBook) {
1129
			//console.log('addressBook', idx, addressBook);
1130
			var $option = $('<option />')
1131
				.val(addressBook.getId())
1132
				.text(addressBook.getDisplayName() + '(' + addressBook.getBackend() + ')')
1133
				.data('backend', addressBook.getBackend())
1134
				.data('owner', addressBook.getOwner());
1135
			if(self.metadata.parent === addressBook.getId()
1136
				&& self.metadata.backend === addressBook.getBackend()) {
1137
				$option.attr('selected', 'selected');
1138
			}
1139
			self.$addressBookSelect.append($option);
1140
		});
1141
		self.$addressBookSelect.multiselect({
1142
			header: false,
1143
			multiple: false,
1144
			selectedList: 3,
1145
			noneSelectedText: self.$addressBookSelect.attr('title'),
1146
			minWidth: 300
1147
		});
1148
		self.$addressBookSelect.on('multiselectclick', function(event, ui) {
1149
			console.log('AddressBook select', ui);
1150
			self.$addressBookSelect.val(ui.value);
1151
			var opt = self.$addressBookSelect.find(':selected');
1152
			if(self.id) {
1153
				console.log('AddressBook', opt);
1154
				$(document).trigger('request.contact.move', {
1155
					contact: self,
1156
					from: {id:self.getParent(), backend:self.getBackend()},
1157
					target: {id:opt.val(), backend:opt.data('backend')}
1158
				});
1159
			} else {
1160
				self.setBackend(opt.data('backend'));
1161
				self.setParent(opt.val());
1162
				self.setOwner(opt.data('owner'));
1163
			}
1164
		});
1165
	};
1166
1167
	/**
1168
	 * Render the full contact
1169
	 * @return A jquery object to be inserted in the DOM
1170
	 */
1171
	Contact.prototype.renderContact = function(groupprops) {
1172
		var self = this;
1173
		this.groupprops = groupprops;
1174
		
1175
		var values;
1176
		if(this.data) {
1177
			var n = this.getPreferredValue('N', ['', '', '', '', '']),
1178
				bday = this.getPreferredValue('BDAY', '');
1179
			if(bday.length >= 10) {
1180
				try {
1181
					bday = $.datepicker.parseDate('yy-mm-dd', bday.substring(0, 10));
1182
					bday = $.datepicker.formatDate(datepickerFormatDate, bday);
1183
				} catch (e) {
1184
					var message = t('contacts', 'Error parsing birthday {bday}', {bday:bday});
1185
					console.warn('Error parsing birthday', bday, e);
1186
					bday = '';
1187
					$(document).trigger('status.contacts.error', {
1188
						status: 'error',
1189
						message: message
1190
					});
1191
				}
1192
			}
1193
			values = {
1194
				id: this.id,
1195
				favorite:groupprops.favorite ? 'icon-starred' : 'icon-star',
1196
				name: this.getPreferredValue('FN', ''),
1197
				n0: n[0]||'', n1: n[1]||'', n2: n[2]||'', n3: n[3]||'', n4: n[4]||'',
1198
				nickname: this.getPreferredValue('NICKNAME', ''),
1199
				title: this.getPreferredValue('TITLE', ''),
1200
				org: this.getPreferredValue('ORG', []).clean('').join(', '), // TODO Add parts if more than one.
1201
				bday: bday,
1202
				note: this.getPreferredValue('NOTE', '')
1203
			};
1204
		} else {
1205
			values = {id:'', favorite:'', name:'', nickname:'', title:'', org:'', bday:'', note:'', n0:'', n1:'', n2:'', n3:'', n4:''};
1206
		}
1207
		this.$fullelem = this.$fullTemplate.octemplate(values).data('contactobject', this);
1208
1209
		this.$header = this.$fullelem.find('header');
1210
		this.$footer = this.$fullelem.find('footer');
1211
		this.$groupSelect = this.$fullelem.find('#contactgroups');
1212
		this.$addressBookSelect = this.$fullelem.find('#contactaddressbooks');
1213
1214
		this.$fullelem.find('.tooltipped.rightwards.onfocus').tipsy({trigger: 'focus', gravity: 'w'});
1215
		this.$fullelem.on('submit', function() {
1216
			return false;
1217
		});
1218
		
1219
		if(this.getOwner() === OC.currentUser && groupprops.groups.length > 0 && this.getBackend() === 'local') {
1220
			this._buildGroupSelect(groupprops.groups);
1221
		} else {
1222
			this.$fullelem.find('.groupscontainer').hide();
1223
		}
1224
		
1225
		var writeableAddressBooks = this.parent.addressBooks.selectByPermission(OC.PERMISSION_CREATE);
1226
		if(writeableAddressBooks.length > 1 && this.hasPermission(OC.PERMISSION_DELETE)) {
1227
			this._buildAddressBookSelect(writeableAddressBooks);
1228
		} else {
1229
			this.$fullelem.find('.addressbookcontainer').hide();
1230
		}
1231
1232
		this.$addMenu = this.$fullelem.find('#addproperty');
1233
		this.$addMenu.on('change', function(/*event*/) {
1234
			var $opt = $(this).find('option:selected');
1235
			self.addProperty($opt, $(this).val());
1236
			$(this).val('');
1237
		});
1238
		var $fullname = this.$fullelem.find('.fullname');
1239
		this.$fullelem.find('.singleproperties').on('mouseenter', function() {
1240
			$fullname.next('.edit').css('opacity', '1');
1241
		}).on('mouseleave', function() {
1242
			$fullname.next('.edit').css('opacity', '0');
1243
		});
1244
		$fullname.next('.edit').on('click keydown', function(event) {
1245
			//console.log('edit name', event);
1246
			$('.tipsy').remove();
1247
			if(wrongKey(event)) {
1248
				return;
1249
			}
1250
			$(this).css('opacity', '0');
1251
			var $editor = $(this).next('.n.editor').first();
1252
			var bodyListener = function(e) {
1253
				if($editor.find($(e.target)).length === 0) {
1254
					$editor.toggle('blind');
1255
					$('body').unbind('click', bodyListener);
1256
				}
1257
			};
1258
			$editor.toggle('blind', function() {
1259
				$('body').bind('click', bodyListener);
1260
			});
1261
		});
1262
1263
		this.$fullelem.on('click keydown', '.delete', function(event) {
1264
			$('.tipsy').remove();
1265
			if(wrongKey(event)) {
1266
				return;
1267
			}
1268
			self.deleteProperty({obj:event.target});
1269
		});
1270
1271
		this.$fullelem.on('click keydown', '.globe,.mail,.favorite', function(event) {
1272
			$('.tipsy').remove();
1273
			if(wrongKey(event)) {
1274
				return;
1275
			}
1276
			self.handleURL(event.target);
1277
		});
1278
1279
		var buttonHandler =  function(event) {
1280
			$('.tipsy').remove();
1281
			if(wrongKey(event)) {
1282
				return;
1283
			}
1284
			if($(this).is('.close') || $(this).is('.cancel')) {
1285
				$(document).trigger('request.contact.close', {
1286
					id: self.id
1287
				});
1288
			} else if($(this).is('.export')) {
1289
				$(document).trigger('request.contact.export', self.metaData());
1290
			} else if($(this).is('.delete')) {
1291
				$(document).trigger('request.contact.delete', self.metaData());
1292
			}
1293
			return false;
1294
		};
1295
		this.$header.on('click keydown', 'button, a', buttonHandler);
1296
		this.$footer.on('click keydown', 'button, a', buttonHandler);
1297
		
1298
		this.$fullelem.on('keypress', '.value,.parameter', function(event) {
1299
			if(event.keyCode === 13 && $(this).is('input')) {
1300
				$(this).trigger('change');
1301
				// Prevent a second save on blur.
1302
				this.previousValue = this.defaultValue || '';
1303
				this.defaultValue = this.value;
1304
				return false;
1305
			} else if(event.keyCode === 27) {
1306
				$(document).trigger('request.contact.close', {
1307
					id: self.id
1308
				});
1309
			}
1310
		});
1311
1312
		this.$fullelem.on('change', '.value,.parameter', function(event) {
1313
			if($(this).hasClass('value') && this.value === this.defaultValue) {
1314
				return;
1315
			}
1316
			function isMultiByte(str) {
1317
				return /[\uD800-\uDFFF]/.test(str);
1318
			}
1319
1320
			if (isMultiByte(this.value) && self.getBackend()) {
1321
				$(document).trigger('status.contacts.error',
1322
				{error:true, message: t('contacts', 'The backend does not support multi-byte characters.')});
1323
				if(this.defaultValue) {
1324
					this.value = this.defaultValue;
1325
				}
1326
				return;
1327
			}
1328
			//console.log('change', this.defaultValue, this.value);
1329
			this.defaultValue = this.value;
1330
			self.saveProperty({obj:event.target});
1331
		});
1332
1333
		var $bdayinput = this.$fullelem.find('[data-element="bday"]').find('input');
1334
		$bdayinput.datepicker({
1335
			dateFormat : datepickerFormatDate,
1336
			changeMonth: true,
1337
			changeYear: true,
1338
			yearRange: '-100:+0',
1339
			minDate : new Date(1900,1,1),
1340
			maxDate : new Date()
1341
		});
1342
		$bdayinput.attr('placeholder', $.datepicker.formatDate(datepickerFormatDate, new Date()));
1343
1344
		this.$fullelem.find('.favorite').on('click', function () {
1345
			var state = $(this).hasClass('icon-starred');
1346
			if(!self.data) {
1347
				return;
1348
			}
1349
			if(state) {
1350
				$(this).switchClass('icon-starred', 'icon-star');
1351
			} else {
1352
				$(this).switchClass('icon-star', 'icon-starred');
1353
			}
1354
			$(document).trigger('request.contact.setasfavorite', {
1355
				id: self.id,
1356
				state: !state
1357
			});
1358
		}).tipsy();
1359
		this.loadAvatar();
1360
		if(!this.data) {
1361
			// A new contact
1362
			this.setEnabled(true);
1363
			this.showActions(['cancel']);
1364
			// Show some default properties
1365
			$.each(['email', 'tel'], function(idx, name) {
1366
				var $list = self.$fullelem.find('ul.' + name);
1367
				$list.removeClass('hidden');
1368
				var $property = self.renderStandardProperty(name);
1369
				$property.find('select[name="parameters[TYPE][]"]')
1370
					.combobox({
1371
					singleclick: true,
1372
					classes: ['propertytype', 'float', 'label']
1373
				});
1374
				$list.append($property);
1375
			});
1376
			var $list = self.$fullelem.find('ul.adr');
1377
			$list.removeClass('hidden');
1378
			var $property = self.renderAddressProperty(name);
1379
			$property.find('select[name="parameters[TYPE][]"]')
1380
				.combobox({
1381
				singleclick: true,
1382
				classes: ['propertytype', 'float', 'label']
1383
			});
1384
			$list.append($property);
1385
1386
			// Hide some of the values
1387
			$.each(['bday', 'nickname', 'title'], function(idx, name) {
1388
				self.$fullelem.find('[data-element="' + name + '"]').hide();
1389
			});
1390
1391
			return this.$fullelem;
1392
		}
1393
		// Loop thru all single occurrence values. If not set hide the
1394
		// element, if set disable the add menu entry.
1395
		$.each(values, function(name, value) {
1396
			if(typeof value === 'undefined') {
1397
				return true; //continue
1398
			}
1399
			value = value.toString();
1400
			if(self.multi_properties.indexOf(value.toUpperCase()) === -1) {
1401
				if(!value.length) {
1402
					self.$fullelem.find('[data-element="' + name + '"]').hide();
1403
				} else {
1404
					self.$addMenu.find('option[value="' + name.toUpperCase() + '"]').prop('disabled', true);
1405
				}
1406
			}
1407
		});
1408
		$.each(this.multi_properties, function(idx, name) {
1409
			if(self.data[name]) {
1410
				var $list = self.$fullelem.find('ul.' + name.toLowerCase());
1411
				$list.removeClass('hidden');
1412
				for(var p in self.data[name]) {
1413
					if(typeof self.data[name][p] === 'object') {
1414
						var property = self.data[name][p];
1415
						//console.log(name, p, property);
1416
						var $property = null;
1417
						switch(name) {
1418
							case 'TEL':
1419
							case 'URL':
1420
							case 'CLOUD':
1421
							case 'EMAIL':
1422
								$property = self.renderStandardProperty(name.toLowerCase(), property);
1423
								if(self.data[name].length === 1) {
1424
									$property.find('input:checkbox[value="PREF"]').hide();
1425
								}
1426
								break;
1427
							case 'ADR':
1428
								$property = self.renderAddressProperty(idx, property);
1429
								break;
1430
							case 'IMPP':
1431
								$property = self.renderIMProperty(property);
1432
								if(self.data[name].length === 1) {
1433
									$property.find('input:checkbox[value="PREF"]').hide();
1434
								}
1435
								break;
1436
						}
1437
						if(!$property) {
1438
							continue;
1439
						}
1440
						//console.log('$property', $property);
1441
						var meta = [];
1442
						if(property.label) {
1443
							if(!property.parameters.TYPE) {
1444
								property.parameters.TYPE = [];
1445
							}
1446
							property.parameters.TYPE.push(property.label);
1447
							meta.push(property.label);
1448
						}
1449
						var preferred = false;
1450
						for(var param in property.parameters) {
1451
							if(property.parameters.hasOwnProperty(param)) {
1452
								//console.log('param', param);
1453
								if(param.toUpperCase() === 'PREF') {
1454
									preferred = true;
1455
									continue;
1456
								}
1457
								else if(param.toUpperCase() === 'TYPE') {
1458
									for(var etype in property.parameters[param]) {
1459
										if(property.parameters[param].hasOwnProperty(etype)) {
1460
											var found = false;
1461
											var et = property.parameters[param][etype];
1462
											if(typeof et !== 'string') {
1463
												continue;
1464
											}
1465
											if(et.toUpperCase() === 'PREF') {
1466
												preferred = true;
1467
												continue;
1468
											}
1469
											$property.find('select.type option').each(function() {
0 ignored issues
show
It is generally not recommended to make functions within a loop.

While making functions in a loop will not lead to any runtime error, the code might not behave as you expect as the variables in the scope are not imported by value, but by reference. Let’s take a look at an example:

var funcs = [];
for (var i=0; i<10; i++) {
    funcs.push(function() {
        alert(i);
    });
}

funcs[0](); // alert(10);
funcs[1](); // alert(10);
/// ...
funcs[9](); // alert(10);

If you would instead like to bind the function inside the loop to the value of the variable during that specific iteration, you can create the function from another function:

var createFunc = function(i) {
    return function() {
        alert(i);
    };
};

var funcs = [];
for (var i=0; i<10; i++) {
    funcs.push(createFunc(i));
}

funcs[0](); // alert(0)
funcs[1](); // alert(1)
// ...
funcs[9](); // alert(9)
Loading history...
1470
												if($(this).val().toUpperCase() === et.toUpperCase()) {
1471
													$(this).attr('selected', 'selected');
1472
													meta.push($(this).text());
1473
													found = true;
1474
												}
1475
											});
1476
											if(!found) {
1477
												$property.find('select.type option:last-child').after('<option value="'+et+'" selected="selected">'+et+'</option>');
1478
											}
1479
										}
1480
									}
1481
								}
1482
								else if(param.toUpperCase() === 'X-SERVICE-TYPE') {
1483
									//console.log('setting', $property.find('select.impp'), 'to', property.parameters[param].toLowerCase());
1484
									$property.find('select.rtl').val(property.parameters[param].toLowerCase());
1485
								}
1486
							}
1487
						}
1488
						if(preferred) {
1489
							var $cb = $property.find('input[type="checkbox"]');
1490
							$cb.attr('checked', 'checked');
1491
							meta.push($cb.attr('title'));
1492
						}
1493
						var $meta = $property.find('.meta');
1494
						if($meta.length) {
1495
							$meta.html(meta.join('/'));
1496
						}
1497
						if(self.metadata.owner === OC.currentUser
1498
								|| self.metadata.permissions & OC.PERMISSION_UPDATE
1499
								|| self.metadata.permissions & OC.PERMISSION_DELETE) {
1500
							$property.find('select.type[name="parameters[TYPE][]"], select.type[name="parameters[X-SERVICE-TYPE]"]')
1501
								.combobox({
1502
									singleclick: true,
1503
									classes: ['propertytype', 'float', 'label']
1504
								});
1505
						}
1506
						$list.append($property);
1507
					}
1508
				}
1509
			}
1510
		});
1511
		var actions = ['close', 'export'];
1512
		if(this.hasPermission(OC.PERMISSION_DELETE)) {
1513
			actions.push('delete');
1514
		}
1515
		if(this.hasPermission(OC.PERMISSION_UPDATE)) {
1516
			actions.push('add');
1517
			this.setEnabled(true);
1518
		} else {
1519
			this.setEnabled(false);
1520
		}
1521
		this.showActions(actions);
1522
1523
		return this.$fullelem;
1524
	};
1525
1526
	Contact.prototype.isEditable = function() {
1527
		return ((this.metadata.owner === OC.currentUser)
1528
			|| (this.metadata.permissions & OC.PERMISSION_UPDATE
1529
				|| this.metadata.permissions & OC.PERMISSION_DELETE));
1530
	};
1531
1532
	/**
1533
	 * Render a simple property. Used for EMAIL and TEL.
1534
	 * @return A jquery object to be injected in the DOM
1535
	 */
1536
	Contact.prototype.renderStandardProperty = function(name, property) {
1537
		if(!this.detailTemplates[name]) {
1538
			console.error('No template for', name);
1539
			return;
1540
		}
1541
		var values = property
1542
			? { value: property.value, checksum: property.checksum }
1543
			: { value: '', checksum: 'new' };
1544
		return this.detailTemplates[name].octemplate(values);
1545
	};
1546
1547
	/**
1548
	 * Render an ADR (address) property.
1549
	 * @return A jquery object to be injected in the DOM
1550
	 */
1551
	Contact.prototype.renderAddressProperty = function(idx, property) {
1552
		if(!this.detailTemplates.adr) {
1553
			console.warn('No template for adr', this.detailTemplates);
1554
			return;
1555
		}
1556
		if(typeof idx === 'undefined') {
1557
			if(this.data && this.data.ADR && this.data.ADR.length > 0) {
1558
				idx = this.data.ADR.length - 1;
1559
			} else {
1560
				idx = 0;
1561
			}
1562
		}
1563
		var values = property ? {
1564
				value: property.value.clean('').join(', '),
1565
				checksum: property.checksum,
1566
				adr0: property.value[0] || '',
1567
				adr1: property.value[1] || '',
1568
				adr2: property.value[2] || '',
1569
				adr3: property.value[3] || '',
1570
				adr4: property.value[4] || '',
1571
				adr5: property.value[5] || '',
1572
				adr6: property.value[6] || '',
1573
				idx: idx
1574
			}
1575
			: {value:'', checksum:'new', adr0:'', adr1:'', adr2:'', adr3:'', adr4:'', adr5:'', adr6:'', idx: idx};
1576
		var $elem = this.detailTemplates.adr.octemplate(values);
1577
		var self = this;
1578
		$elem.find('.tooltipped.downwards:not(.onfocus)').tipsy({gravity: 'n'});
1579
		$elem.find('.tooltipped.rightwards.onfocus').tipsy({trigger: 'focus', gravity: 'w'});
1580
		$elem.find('.display').on('click', function() {
1581
			$(this).next('.listactions').hide();
1582
			var $editor = $(this).siblings('.adr.editor').first();
1583
			var $viewer = $(this);
1584
			var bodyListener = function(e) {
1585
				if($editor.find($(e.target)).length === 0) {
1586
					$editor.toggle('blind');
1587
					$viewer.slideDown(550, function() {
1588
						var input = $editor.find('input').first();
1589
						var params = self.parametersFor(input, true);
1590
						$(this).find('.meta').html(params.TYPE.join('/'));
1591
						$(this).find('.adr').text(self.valueFor($editor.find('input').first()).clean('').join(', '));
1592
						$(this).next('.listactions').css('display', 'inline-block');
1593
						$('body').unbind('click', bodyListener);
1594
					});
1595
				}
1596
			};
1597
			$viewer.slideUp(100);
1598
			$editor.toggle('blind', function() {
1599
				$('body').bind('click', bodyListener);
1600
			});
1601
		});
1602
		return $elem;
1603
	};
1604
1605
	/**
1606
	 * Render an IMPP (Instant Messaging) property.
1607
	 * @return A jquery object to be injected in the DOM
1608
	 */
1609
	Contact.prototype.renderIMProperty = function(property) {
1610
		if(!this.detailTemplates.impp) {
1611
			console.warn('No template for impp', this.detailTemplates);
1612
			return;
1613
		}
1614
		var values = property ? {
1615
			value: property.value,
1616
			checksum: property.checksum
1617
		} : {value: '', checksum: 'new'};
1618
		return this.detailTemplates.impp.octemplate(values);
1619
	};
1620
1621
	/**
1622
	 * Set a thumbnail for the contact if a PHOTO property exists
1623
	 */
1624
	Contact.prototype.setThumbnail = function($elem, refresh) {
1625
		if(!this.data.photo && !refresh) {
1626
			this.getListItemElement().find('.avatar').css('height', '32px');
1627
			var name = String(this.getDisplayName()).replace(' ', '').replace(',', '');
1628
			this.getListItemElement().find('.avatar').imageplaceholder(name || '#');
1629
			return;
1630
		}
1631
		if(!$elem) {
1632
			$elem = this.getListItemElement().find('td.name');
1633
		}
1634
		if(!$elem.hasClass('thumbnail') && !refresh) {
1635
			return;
1636
		}
1637
		if(this.data.photo) {
1638
			$elem.removeClass('thumbnail').find('.avatar').remove();
1639
			var contactId = this.id || 'new',
1640
				backend = this.metadata.backend,
1641
				addressBookId = this.metadata.parent;
1642
			var url = OC.generateUrl(
1643
				'apps/contacts/addressbook/{backend}/{addressBookId}/contact/{contactId}/photo?maxSize=32',
1644
				{backend: backend, addressBookId: addressBookId, contactId: contactId}
1645
			);
1646
1647
			$elem.css('background-image', 'url(' + url + ')');
1648
		} else {
1649
			$elem.addClass('thumbnail');
1650
			$elem.removeAttr('style');
1651
		}
1652
	};
1653
1654
	/**
1655
	 * Render the PHOTO property or a generated avatar.
1656
	 */
1657
	Contact.prototype.loadAvatar = function() {
1658
		var self = this;
1659
		var id = this.id || 'new',
1660
			backend = this.metadata.backend,
1661
			parent = this.metadata.parent;
1662
1663
		var $phototools = this.$fullelem.find('#phototools');
1664
		var $photowrapper = this.$fullelem.find('#photowrapper');
1665
1666
		var finishLoad = function(image) {
1667
			console.log('finishLoad', self.getDisplayName(), image.width, image.height);
1668
			$(image).addClass('contactphoto');
1669
			$photowrapper.removeClass('loading');
1670
			$photowrapper.css({width: image.width + 10, height: image.height + 10});
1671
			$(image).insertAfter($phototools).fadeIn();
1672
		};
1673
1674
		var addAvatar = function() {
1675
			console.log('adding avatar for', self.getDisplayName());
1676
			$photowrapper.find('.contactphoto').remove();
1677
			var name = String(self.getDisplayName()).replace(' ', '').replace(',', '');
1678
			console.log('height', $photowrapper.height());
1679
			$('<div />').appendTo($photowrapper)
1680
				.css({width: '170', height: '170'})
1681
				.addClass('contactphoto')
1682
				.imageplaceholder(name || '#');
1683
		};
1684
1685
		$photowrapper.addClass('loading');
1686
		if(!this.hasPhoto()) {
1687
			$photowrapper.removeClass('loading');
1688
			addAvatar();
1689
		} else {
1690
			$.when(this.storage.getContactPhoto(backend, parent, id))
1691
				.then(function(image) {
1692
					$photowrapper.find('.contactphoto').remove();
1693
					finishLoad(image);
1694
				})
1695
				.fail(function() {
1696
					console.log('Error getting photo.');
1697
					$photowrapper.find('.contactphoto').remove();
1698
					addAvatar();
1699
				});
1700
		}
1701
1702
		if(this.isEditable()) {
1703
			$photowrapper.on('mouseenter', function(event) {
1704
				if($(event.target).is('.favorite') || !self.data) {
1705
					return;
1706
				}
1707
				$phototools.slideDown(200);
1708
			}).on('mouseleave', function() {
1709
				$phototools.slideUp(200);
1710
			});
1711
			$phototools.hover( function () {
1712
				$(this).removeClass('transparent');
1713
			}, function () {
1714
				$(this).addClass('transparent');
1715
			});
1716
			$phototools.find('li a').tipsy();
1717
1718
			$phototools.find('.action').off('click');
1719
			$phototools.find('.edit').on('click', function() {
1720
				$(document).trigger('request.edit.contactphoto', self.metaData());
1721
			});
1722
			$phototools.find('.cloud').on('click', function() {
1723
				$(document).trigger('request.select.contactphoto.fromcloud', self.metaData());
1724
			});
1725
			$phototools.find('.upload').on('click', function() {
1726
				$(document).trigger('request.select.contactphoto.fromlocal', self);
1727
			});
1728
			if(this.hasPhoto()) {
1729
				$phototools.find('.delete').show();
1730
				$phototools.find('.edit').show();
1731
			} else {
1732
				$phototools.find('.delete').hide();
1733
				$phototools.find('.edit').hide();
1734
			}
1735
			$(document).bind('status.contact.photoupdated', function(e, data) {
1736
				console.log('status.contact.photoupdated', data);
1737
				if(!self.hasPhoto()) {
1738
					self.data.PHOTO = [];
1739
				}
1740
				if(data.thumbnail) {
1741
					self.data.thumbnail = data.thumbnail;
1742
					self.data.photo = true;
1743
				} else {
1744
					self.data.thumbnail = null;
1745
					self.data.photo = false;
1746
				}
1747
				self.loadAvatar(true);
1748
				self.setThumbnail(null, true);
1749
			});
1750
		}
1751
	};
1752
1753
	/**
1754
	 * Get the jquery element associated with this object
1755
	 */
1756
	Contact.prototype.getListItemElement = function() {
1757
		if(!this.$listelem) {
1758
			this.renderListItem();
1759
		}
1760
		return this.$listelem;
1761
	};
1762
1763
	/**
1764
	 * Get the preferred value for a property.
1765
	 * If a preferred value is not found the first one will be returned.
1766
	 * @param string name The name of the property like EMAIL, TEL or ADR.
1767
	 * @param def A default value to return if nothing is found.
1768
	 */
1769
	Contact.prototype.getPreferredValue = function(name, def) {
1770
		var pref = def, found = false;
1771
		if(this.data && this.data[name]) {
1772
			var props = this.data[name];
1773
			//console.log('props', props);
1774
			$.each(props, function( i, prop ) {
1775
				//console.log('prop:', i, prop);
1776
				if(i === 0) { // Choose first to start with
1777
					pref = prop.value;
1778
				}
1779
				for(var param in prop.parameters) {
1780
					if(param.toUpperCase() === 'PREF') {
1781
						pref = prop.value;
1782
						found = true; //
1783
						break;
1784
					}
1785
				}
1786
				if(found) {
1787
					return false; // break out of loop
1788
				}
1789
			});
1790
		}
1791
		if(name === 'N' && pref.join('').trim() === '') {
1792
			return def;
1793
		}
1794
		return pref;
1795
	};
1796
1797
	/**
1798
	 * Returns an array with the names of the groups the contact is in
1799
	 *
1800
	 * @return Array
1801
	 */
1802
	Contact.prototype.groups = function() {
1803
		return this.getPreferredValue('CATEGORIES', []).clean('');
1804
	};
1805
1806
1807
	/**
1808
	 * Returns true/false depending on the contact being in the
1809
	 * specified group.
1810
	 * @param String name The group name (not case-sensitive)
1811
	 * @return Boolean
1812
	 */
1813
	Contact.prototype.inGroup = function(name) {
1814
		console.log('inGroup', name);
1815
		var categories = this.getPreferredValue('CATEGORIES', []);
1816
		var found = false;
1817
1818
		$.each(categories, function(idx, category) {
1819
			if(name.toLowerCase() === $.trim(category).toLowerCase()) {
1820
				found = true;
1821
				return false;
1822
			}
1823
		});
1824
1825
		return found;
1826
	};
1827
1828
	/**
1829
	 * Add this contact to a group
1830
	 * @param String name The group name
1831
	 */
1832
	Contact.prototype.addToGroup = function(name) {
1833
		console.log('addToGroup', name);
1834
		if(!this.data.CATEGORIES) {
1835
			this.data.CATEGORIES = [{value:[name]}];
1836
		} else {
1837
			if(this.inGroup(name)) {
1838
				return;
1839
			}
1840
			this.data.CATEGORIES[0].value.push(name);
1841
			if(this.$listelem) {
1842
				this.$listelem.find('td.categories')
1843
					.text(this.getPreferredValue('CATEGORIES', []).clean('').join(' / '));
1844
			}
1845
		}
1846
	};
1847
1848
	/**
1849
	 * Remove this contact from a group
1850
	 * @param String name The group name
1851
	 */
1852
	Contact.prototype.removeFromGroup = function(name) {
1853
		name = name.trim();
1854
		if(!this.data.CATEGORIES) {
1855
			console.warn('removeFromGroup. No groups found');
1856
			return;
1857
		} else {
1858
			var found = false;
1859
			var categories = [];
1860
			$.each(this.data.CATEGORIES[0].value, function(idx, category) {
1861
				category = category.trim();
1862
				if(name.toLowerCase() === category.toLowerCase()) {
1863
					found = true;
1864
				} else {
1865
					categories.push(category);
1866
				}
1867
			});
1868
			if(!found) {
1869
				return;
1870
			}
1871
			this.data.CATEGORIES[0].value = categories;
1872
			if(this.$listelem) {
1873
				this.$listelem.find('td.categories')
1874
					.text(categories.join(' / '));
1875
			}
1876
		}
1877
	};
1878
1879
	Contact.prototype.setCurrent = function(on) {
1880
		if(on) {
1881
			this.$listelem.addClass('active');
1882
		} else {
1883
			this.$listelem.removeClass('active');
1884
		}
1885
		$(document).trigger('status.contact.currentlistitem', {
1886
			id: this.id,
1887
			pos: Math.round(this.$listelem.position().top),
1888
			height: Math.round(this.$listelem.height())
1889
		});
1890
	};
1891
1892
	Contact.prototype.setSelected = function(state) {
1893
		//console.log('Selecting', this.getId(), state);
1894
		var $elem = this.getListItemElement();
1895
		var $input = $elem.find('input:checkbox');
1896
		$input.prop('checked', state).trigger('change');
1897
	};
1898
1899
	Contact.prototype.next = function() {
1900
		// This used to work..?
1901
		//var $next = this.$listelem.next('tr:visible');
1902
		var $next = this.$listelem.nextAll('tr').filter(':visible').first();
1903
		if($next.length > 0) {
1904
			this.$listelem.removeClass('active');
1905
			$next.addClass('active');
1906
			$(document).trigger('status.contact.currentlistitem', {
1907
				id: String($next.data('id')),
1908
				pos: Math.round($next.position().top),
1909
				height: Math.round($next.height())
1910
			});
1911
		}
1912
	};
1913
1914
	Contact.prototype.prev = function() {
1915
		//var $prev = this.$listelem.prev('tr:visible');
1916
		var $prev = this.$listelem.prevAll('tr').filter(':visible').first();
1917
		if($prev.length > 0) {
1918
			this.$listelem.removeClass('active');
1919
			$prev.addClass('active');
1920
			$(document).trigger('status.contact.currentlistitem', {
1921
				id: String($prev.data('id')),
1922
				pos: Math.round($prev.position().top),
1923
				height: Math.round($prev.height())
1924
			});
1925
		}
1926
	};
1927
1928
	var ContactList = function(
1929
			storage,
1930
			addressBooks,
1931
			contactlist,
1932
			contactlistitemtemplate,
1933
			contactdragitemtemplate,
1934
			contactfulltemplate,
1935
			contactdetailtemplates
1936
		) {
1937
		//console.log('ContactList', contactlist, contactlistitemtemplate, contactfulltemplate, contactdetailtemplates);
1938
		var self = this;
1939
		this.length = 0;
1940
		this.contacts = {};
1941
		this.addressBooks = addressBooks;
1942
		this.deletionQueue = [];
1943
		this.storage = storage;
1944
		this.$contactList = contactlist;
1945
		this.$contactDragItemTemplate = contactdragitemtemplate;
1946
		this.$contactListItemTemplate = contactlistitemtemplate;
1947
		this.$contactFullTemplate = contactfulltemplate;
1948
		this.contactDetailTemplates = contactdetailtemplates;
1949
		this.$contactList.scrollTop(0);
1950
		//this.getAddressBooks();
1951
		$(document).bind('status.contact.added', function(e, data) {
1952
			self.length += 1;
1953
			self.contacts[String(data.id)] = data.contact;
1954
			//self.insertContact(data.contact.renderListItem(true));
1955
		});
1956
		$(document).bind('status.contact.moved', function(e, data) {
1957
			var contact = data.contact;
1958
			contact.close();
1959
			contact.reload(data.data);
1960
			self.contacts[contact.getId()] = contact;
1961
			$(document).trigger('request.contact.open', {
1962
				id: contact.getId()
1963
			});
1964
			console.log('status.contact.moved', data);
1965
		});
1966
		$(document).bind('request.contact.close', function(/*e, data*/) {
1967
			self.currentContact = null;
1968
		});
1969
		$(document).bind('status.contact.updated', function(e, data) {
1970
			if(['FN', 'EMAIL', 'TEL', 'ADR', 'CATEGORIES'].indexOf(data.property) !== -1) {
1971
				data.contact.getListItemElement().remove();
1972
				self.insertContact(data.contact.renderListItem(true));
1973
			} else if(data.property === 'PHOTO') {
1974
				$(document).trigger('status.contact.photoupdated', {
1975
					id: data.contact.getId()
1976
				});
1977
			}
1978
		});
1979
		$(document).bind('status.addressbook.removed', function(e, data) {
1980
			var addressBook = data.addressbook;
1981
			self.purgeFromAddressbook(addressBook);
1982
			$(document).trigger('request.groups.reload');
1983
			$(document).trigger('status.contacts.deleted', {
1984
				numcontacts: self.length
1985
			});
1986
		});
1987
		$(document).bind('status.addressbook.imported', function(e, data) {
1988
			console.log('status.addressbook.imported', data);
1989
			var addressBook = data.addressbook;
1990
			self.purgeFromAddressbook(addressBook);
1991
			$.when(self.loadContacts(addressBook.getBackend(), addressBook.getId(), true))
1992
			.then(function() {
1993
				self.setSortOrder();
1994
				$(document).trigger('request.groups.reload');
1995
			});
1996
		});
1997
		$(document).bind('status.addressbook.activated', function(e, data) {
1998
			console.log('status.addressbook.activated', data);
1999
			var addressBook = data.addressbook;
2000
			if(!data.state) {
2001
				self.purgeFromAddressbook(addressBook);
2002
				$(document).trigger('status.contacts.deleted', {
2003
					numcontacts: self.length
2004
				});
2005
			} else {
2006
				$.when(self.loadContacts(addressBook.getBackend(), addressBook.getId(), true))
2007
				.then(function() {
2008
					self.setSortOrder();
2009
					$(document).trigger('request.groups.reload');
2010
				});
2011
			}
2012
		});
2013
	};
2014
2015
	/**
2016
	 * Get the number of contacts in the list
2017
	 * @return integer
2018
	 */
2019
	ContactList.prototype.count = function() {
2020
		return Object.keys(this.contacts.contacts).length;
2021
	};
2022
2023
	/**
2024
	* Remove contacts from the internal list and the DOM
2025
	*
2026
	* @param AddressBook addressBook
2027
	*/
2028
	ContactList.prototype.purgeFromAddressbook = function(addressBook) {
2029
		var self = this;
2030
		$.each(this.contacts, function(idx, contact) {
2031
			if(contact.getBackend() === addressBook.getBackend()
2032
				&& contact.getParent() === addressBook.getId()) {
2033
				//console.log('Removing', contact);
2034
				delete self.contacts[contact.getId()];
2035
				//var c = self.contacts.splice(self.contacts.indexOf(contact.getId()), 1);
2036
				//console.log('Removed', c);
2037
				contact.detach();
2038
				contact = null;
2039
				self.length -= 1;
2040
			}
2041
		});
2042
		$(document).trigger('status.contacts.count', {
2043
			count: self.length
2044
		});
2045
	};
2046
2047
	/**
2048
	* Show/hide contacts belonging to an addressbook.
2049
	* @param int aid. Addressbook id.
2050
	* @param boolean show. Whether to show or hide.
2051
	* @param boolean hideothers. Used when showing shared addressbook as a group.
2052
	*/
2053
	ContactList.prototype.showFromAddressbook = function(aid, show, hideothers) {
2054
		console.log('ContactList.showFromAddressbook', aid, show);
2055
		aid = String(aid);
2056
		for(var contact in this.contacts) {
2057
			if(this.contacts[contact].getParent() === aid) {
2058
				this.contacts[contact].getListItemElement().toggle(show);
2059
			} else if(hideothers) {
2060
				this.contacts[contact].getListItemElement().hide();
2061
			}
2062
		}
2063
		this.setSortOrder();
2064
	};
2065
2066
	/**
2067
	* Show only uncategorized contacts.
2068
	* @param int aid. Addressbook id.
2069
	* @param boolean show. Whether to show or hide.
2070
	* @param boolean hideothers. Used when showing shared addressbook as a group.
2071
	*/
2072
	ContactList.prototype.showUncategorized = function() {
2073
		console.log('ContactList.showUncategorized');
2074
		for(var contact in this.contacts) {
2075
			if(this.contacts[contact].getPreferredValue('CATEGORIES', []).clean('').length === 0) {
2076
				this.contacts[contact].getListItemElement().show();
2077
				this.contacts[contact].setThumbnail();
2078
			} else {
2079
				this.contacts[contact].getListItemElement().hide();
2080
			}
2081
		}
2082
		this.setSortOrder();
2083
	};
2084
2085
	/**
2086
	* Show/hide contacts belonging to shared addressbooks.
2087
	* @param Boolean show. Whether to show or hide.
2088
	*/
2089
	ContactList.prototype.showSharedAddressbooks = function(show) {
2090
		console.log('ContactList.showSharedAddressbooks', show);
2091
		for(var contact in this.contacts) {
2092
			if(this.contacts[contact].metadata.owner !== OC.currentUser) {
2093
				if(show) {
2094
					this.contacts[contact].getListItemElement().show();
2095
				} else {
2096
					this.contacts[contact].getListItemElement().hide();
2097
				}
2098
			}
2099
		}
2100
		this.setSortOrder();
2101
	};
2102
2103
	/**
2104
	* Show contacts in list
2105
	* @param String[] contacts. A list of contact ids.
2106
	*/
2107
	ContactList.prototype.showContacts = function(contacts) {
2108
		console.log('showContacts', contacts);
2109
		var self = this;
2110
		if(contacts.length === 0) {
2111
			// ~5 times faster
2112
			$('tr:visible.contact').hide();
2113
			return;
2114
		}
2115
		if(contacts === 'all') {
2116
			// ~2 times faster
2117
			var $elems = $('tr.contact:not(:visible)');
2118
			$elems.show();
2119
			$.each($elems, function(idx, elem) {
2120
				try {
2121
					var id = $(elem).data('id');
2122
					self.contacts[id].setThumbnail();
2123
				} catch(e) {
2124
					console.warn('Failed getting id from', $elem, e);
2125
				}
2126
			});
2127
			this.setSortOrder();
2128
			return;
2129
		}
2130
		console.time('show');
2131
		$('tr.contact').filter(':visible').hide();
2132
		$.each(contacts, function(idx, id) {
2133
			var contact = self.findById(id);
2134
			if(contact === null) {
2135
				return true; // continue
2136
			}
2137
			contact.getListItemElement().show();
2138
			contact.setThumbnail();
2139
		});
2140
		console.timeEnd('show');
2141
2142
		// Amazingly this is slightly faster
2143
		//console.time('show');
2144
		for(var id in this.contacts) {
2145
			if(this.contacts.hasOwnProperty(id)) {
2146
				var contact = this.findById(id);
2147
				if(contact === null) {
2148
					continue;
2149
				}
2150
				if(contacts.indexOf(String(id)) === -1) {
2151
					contact.getListItemElement().hide();
2152
				} else {
2153
					contact.getListItemElement().show();
2154
					contact.setThumbnail();
2155
				}
2156
			}
2157
		}
2158
		//console.timeEnd('show');*/
2159
2160
		this.setSortOrder();
2161
	};
2162
2163
	ContactList.prototype.contactPos = function(id) {
2164
		var contact = this.findById(id);
2165
		if(!contact) {
2166
			return 0;
2167
		}
2168
		
2169
		var $elem = contact.getListItemElement();
2170
		var pos = Math.round($elem.offset().top - (this.$contactList.offset().top + this.$contactList.scrollTop()));
2171
		console.log('contactPos', pos);
2172
		return pos;
2173
	};
2174
2175
	ContactList.prototype.hideContact = function(id) {
2176
		var contact = this.findById(id);
2177
		if(contact === null) {
2178
			return false;
2179
		}
2180
		contact.hide();
2181
	};
2182
2183
	ContactList.prototype.closeContact = function(id) {
2184
		var contact = this.findById(id);
2185
		if(contact === null) {
2186
			return false;
2187
		}
2188
		contact.close();
2189
	};
2190
2191
	/**
2192
	* Returns a Contact object by searching for its id
2193
	* @param id the id of the node
2194
	* @return the Contact object or undefined if not found.
2195
	* FIXME: If continious loading is reintroduced this will have
2196
	* to load the requested contact if not in list.
2197
	*/
2198
	ContactList.prototype.findById = function(id) {
2199
		if(!id) {
2200
			console.warn('ContactList.findById: id missing');
2201
			return false;
2202
		}
2203
		id = String(id);
2204
		if(typeof this.contacts[id] === 'undefined') {
2205
			console.warn('Could not find contact with id', id);
2206
			//console.trace();
2207
			return null;
2208
		}
2209
		return this.contacts[String(id)];
2210
	};
2211
2212
	/**
2213
	* TODO: Instead of having a timeout the contacts should be moved to a "Trash" backend/address book
2214
	* https://github.com/owncloud/contacts/issues/107
2215
	* @param Object|Object[] data An object or array of objects containing contact identification
2216
	* {
2217
	* 	contactid: '1234',
2218
	* 	addressbookid: '4321',
2219
	* 	backend: 'local'
2220
	* }
2221
	*/
2222
	ContactList.prototype.delayedDelete = function(data) {
2223
		console.log('delayedDelete, data:', typeof data, data);
2224
		var self = this;
2225
		if(!utils.isArray(data)) {
2226
			this.currentContact = null;
2227
			//self.$contactList.show();
2228
			if(data instanceof Contact) {
2229
				this.deletionQueue.push(data);
2230
			} else {
2231
				var contact = this.findById(data.contactId);
2232
				if(contact instanceof Contact) {
2233
					this.deletionQueue.push(contact);
2234
				}
2235
			}
2236
		} else if(utils.isArray(data)) {
2237
			$.each(data, function(idx, contact) {
2238
				//console.log('delayedDelete, meta:', contact);
2239
				if(contact instanceof Contact) {
2240
					self.deletionQueue.push(contact);
2241
				}
2242
			});
2243
			//$.extend(this.deletionQueue, data);
2244
		} else {
2245
			throw { name: 'WrongParameterType', message: 'ContactList.delayedDelete only accept objects or arrays.'};
2246
		}
2247
		//console.log('delayedDelete, deletionQueue', this.deletionQueue);
2248
		$.each(this.deletionQueue, function(idx, contact) {
2249
			//console.log('delayedDelete', contact);
2250
			contact && contact.detach().setChecked(false);
2251
		});
2252
		//console.log('deletionQueue', this.deletionQueue);
2253
		if(!window.onbeforeunload) {
2254
			window.onbeforeunload = function(e) {
2255
				e = e || window.event;
2256
				var warn = t('contacts', 'Some contacts are marked for deletion, but not deleted yet. Please wait for them to be deleted.');
2257
				if (e) {
2258
					e.returnValue = String(warn);
2259
				}
2260
				return warn;
2261
			};
2262
		}
2263
		if(this.$contactList.find('tr:visible').length === 0) {
2264
			$(document).trigger('status.visiblecontacts');
2265
		}
2266
		OC.notify({
2267
			message:t('contacts','Click to undo deletion of {num} contacts', {num: self.deletionQueue.length}),
2268
			//timeout:5,
2269
			timeouthandler:function() {
2270
				//console.log('timeout');
2271
				self.deleteContacts();
2272
			},
2273
			clickhandler:function() {
2274
				//console.log('clickhandler');
2275
				//OC.notify({cancel:true});
2276
				OC.notify({cancel:true, message:t('contacts', 'Cancelled deletion of {num} contacts', {num: self.deletionQueue.length})});
2277
				$.each(self.deletionQueue, function(idx, contact) {
2278
					self.insertContact(contact.getListItemElement());
2279
				});
2280
				self.deletionQueue = [];
2281
				window.onbeforeunload = null;
2282
			}
2283
		});
2284
	};
2285
2286
	/**
2287
	* Delete contacts in the queue
2288
	* TODO: Batch delete contacts instead of sending multiple requests.
2289
	*/
2290
	ContactList.prototype.deleteContacts = function() {
2291
		var self = this,
2292
			contact,
2293
			contactMap = {};
2294
		console.log('ContactList.deleteContacts, deletionQueue', this.deletionQueue);
2295
2296
		if(this.deletionQueue.length === 1) {
2297
			contact = this.deletionQueue.shift();
2298
			// Let contact remove itself.
2299
			var id = contact.getId();
2300
			contact.destroy(function(response) {
2301
				console.log('deleteContact', response, self.length);
2302
				if(!response.error) {
2303
					delete self.contacts[id];
2304
					$(document).trigger('status.contact.deleted', {
2305
						id: id
2306
					});
2307
					self.length -= 1;
2308
					if(self.length === 0) {
2309
						$(document).trigger('status.nomorecontacts');
2310
					}
2311
				} else {
2312
					self.insertContact(contact.getListItemElement());
2313
					OC.notify({message:response.message});
2314
				}
2315
			});
2316
		} else {
2317
2318
			// Make a map of backends, address books and contacts for easier processing.
2319
			do {
2320
				contact = this.deletionQueue.shift();
2321
				if (!_.isUndefined(contact)) {
2322
					if(!contactMap[contact.getBackend()]) {
2323
						contactMap[contact.getBackend()] = {};
2324
					}
2325
					if(!contactMap[contact.getBackend()][contact.getParent()]) {
2326
						contactMap[contact.getBackend()][contact.getParent()] = [];
2327
					}
2328
					contactMap[contact.getBackend()][contact.getParent()].push(contact.getId());
2329
				}
2330
			} while(this.deletionQueue.length > 0);
2331
			console.log('map', contactMap);
2332
2333
			// Call each backend/addressBook to delete contacts.
2334
			$.each(contactMap, function(backend, addressBooks) {
2335
				console.log(backend, addressBooks);
2336
				$.each(addressBooks, function(addressBook, contacts) {
2337
					console.log(addressBook, contacts);
2338
					var ab = self.addressBooks.find({backend:backend, id:addressBook});
2339
					ab.deleteContacts(contacts, function(response) {
2340
						console.log('response', response);
2341
						if(!response.error) {
2342
							// We get a result set back, so process all of them.
2343
							$.each(response.data.result, function(idx, result) {
2344
								console.log('deleting', idx, result.id);
2345
								if(result.status === 'success') {
2346
									delete self.contacts[result.id];
2347
									$(document).trigger('status.contact.deleted', {
2348
										id: result.id
2349
									});
2350
									self.length -= 1;
2351
									if(self.length === 0) {
2352
										$(document).trigger('status.nomorecontacts');
2353
									}
2354
								} else {
2355
									// Error deleting, so re-insert element.
2356
									// TODO: Collect errors and display them when done.
2357
									self.insertContact(self.contacts[result.id].getListItemElement());
2358
								}
2359
							});
2360
						} else {
2361
							console.warn(response);
2362
						}
2363
					});
2364
				});
2365
			});
2366
		}
2367
2368
		window.onbeforeunload = null;
2369
		return;
2370
2371
	};
2372
2373
	/**
2374
	 * Insert a rendered contact list item into the list
2375
	 * @param contact jQuery object.
2376
	 */
2377
	ContactList.prototype.insertContact = function($contact) {
2378
		$contact.find('td.name').draggable({
2379
			distance: 10,
2380
			revert: 'invalid',
2381
			//containment: '#content',
2382
			helper: function (/*event, ui*/) {
2383
				return $(this).clone().appendTo('body').css('zIndex', 5).show();
2384
			},
2385
			opacity: 0.8,
2386
			scope: 'contacts'
2387
		});
2388
		var name = $contact.find('.nametext').text().toLowerCase();
2389
		var added = false;
2390
		this.$contactList.find('tr').each(function() {
2391
			if ($(this).find('.nametext').text().toLowerCase().localeCompare(name) > 0) {
2392
				$(this).before($contact);
2393
				added = true;
2394
				return false;
2395
			}
2396
		});
2397
		if(!added) {
2398
			this.$contactList.append($contact);
2399
		}
2400
		if($contact.data('obj').isOpen()) {
2401
			$contact.hide();
2402
		} else {
2403
			$contact.show();
2404
		}
2405
		return $contact;
2406
	};
2407
2408
	/**
2409
	* Add contact
2410
	* @param object props
2411
	*/
2412
	ContactList.prototype.addContact = function(props) {
2413
		// Get first address book
2414
		var addressBooks = this.addressBooks.selectByPermission(OC.PERMISSION_UPDATE);
2415
		var addressBook = addressBooks[0];
2416
		var metadata = {
2417
			parent: addressBook.getId(),
2418
			backend: addressBook.getBackend(),
2419
			permissions: addressBook.getPermissions(),
2420
			owner: addressBook.getOwner()
2421
		};
2422
		var contact = new Contact(
2423
			this,
2424
			null,
2425
			metadata,
2426
			null,
2427
			this.$contactListItemTemplate,
2428
			this.$contactDragItemTemplate,
2429
			this.$contactFullTemplate,
2430
			this.contactDetailTemplates
2431
		);
2432
		if(this.currentContact) {
2433
			this.contacts[this.currentContact].close();
2434
		}
2435
		return contact.renderContact(props);
2436
	};
2437
2438
	/**
2439
	 * Get contacts selected in list
2440
	 *
2441
	 * @returns array of contact objects.
2442
	 */
2443
	ContactList.prototype.getSelectedContacts = function() {
2444
		var contacts = [];
2445
2446
		var self = this;
2447
		$.each(this.$contactList.find('tbody > tr > td > input:checkbox:visible:checked'), function(idx, checkbox) {
2448
			var id = String($(checkbox).val());
2449
			var contact = self.contacts[id];
2450
			if(contact) {
2451
				contacts.push(contact);
2452
			}
2453
		});
2454
		return contacts;
2455
	};
2456
2457
	ContactList.prototype.setCurrent = function(id, deselect_other) {
2458
		console.log('ContactList.setCurrent', id);
2459
		if(!id) {
2460
			return;
2461
		}
2462
		var self = this;
2463
		if(deselect_other === true) {
2464
			$.each(this.contacts, function(contact) {
2465
				self.contacts[contact].setCurrent(false);
2466
			});
2467
		}
2468
		this.contacts[String(id)].setCurrent(true);
2469
	};
2470
2471
	/**
2472
	 * (De)-select a contact
2473
	 *
2474
	 * @param string id
2475
	 * @param bool state
2476
	 * @param bool reverseOthers
2477
	 */
2478
	ContactList.prototype.setSelected = function(id, state, reverseOthers) {
2479
		console.log('ContactList.setSelected', id);
2480
		if(!id) {
2481
			return;
2482
		}
2483
		var self = this;
2484
		if(reverseOthers === true) {
2485
			var $rows = this.$contactList.find('tr:visible.contact');
2486
			$.each($rows, function(idx, row) {
2487
				self.contacts[$(row).data('id')].setSelected(!state);
2488
			});
2489
		}
2490
		this.contacts[String(id)].setSelected(state);
2491
	};
2492
2493
	/**
2494
	 * Select a range of contacts by their id.
2495
	 *
2496
	 * @param string from
2497
	 * @param string to
2498
	 */
2499
	ContactList.prototype.selectRange = function(from, to) {
2500
		var self = this;
2501
		var $rows = this.$contactList.find('tr:visible.contact');
2502
		var index1 = $rows.index(this.contacts[String(from)].getListItemElement());
2503
		var index2 = $rows.index(this.contacts[String(to)].getListItemElement());
2504
		from = Math.min(index1, index2);
2505
		to = Math.max(index1, index2)+1;
2506
		$rows = $rows.slice(from, to);
2507
		$.each($rows, function(idx, row) {
2508
			self.contacts[$(row).data('id')].setSelected(true);
2509
		});
2510
	};
2511
2512
	ContactList.prototype.setSortOrder = function(order) {
2513
		order = order || contacts_sortby;
2514
		//console.time('set name');
2515
		var $rows = this.$contactList.find('tr:visible.contact');
2516
		var self = this;
2517
		$.each($rows, function(idx, row) {
2518
			self.contacts[$(row).data('id')].setDisplayMethod(order);
2519
		});
2520
		//console.timeEnd('set name');
2521
		if($rows.length > 1) {
2522
			//console.time('sort');
2523
			var rows = $rows.get();
2524
			if(rows[0].firstElementChild && rows[0].firstElementChild.textContent) {
2525
				rows.sort(function(a, b) {
2526
					// 10 (TEN!) times faster than using jQuery!
2527
					return a.firstElementChild.lastElementChild.textContent.trim().toUpperCase()
2528
						.localeCompare(b.firstElementChild.lastElementChild.textContent.trim().toUpperCase());
2529
				});
2530
			} else {
2531
				// IE8 doesn't support firstElementChild or textContent
2532
				rows.sort(function(a, b) {
2533
					return $(a).find('.nametext').text().toUpperCase()
2534
						.localeCompare($(b).find('td.name').text().toUpperCase());
2535
				});
2536
			}
2537
			this.$contactList.prepend(rows);
2538
			//console.timeEnd('sort');
2539
		}
2540
	};
2541
2542
	ContactList.prototype.insertContacts = function(contacts) {
2543
		var self = this, items = [];
2544
		$.each(contacts, function(c, contact) {
2545
			var id = String(contact.metadata.id);
2546
			self.contacts[id]
2547
				= new Contact(
2548
					self,
2549
					id,
2550
					contact.metadata,
2551
					contact.data,
2552
					self.$contactListItemTemplate,
2553
					self.$contactDragItemTemplate,
2554
					self.$contactFullTemplate,
2555
					self.contactDetailTemplates
2556
				);
2557
			self.length +=1;
2558
			var $item = self.contacts[id].renderListItem();
2559
			if(!$item) {
2560
				console.warn('Contact', contact, 'could not be rendered!');
2561
				return true; // continue
2562
			}
2563
			items.push($item.get(0));
2564
		});
2565
		if(items.length > 0) {
2566
			self.$contactList.append(items);
2567
		}
2568
		$(document).trigger('status.contacts.count', {
2569
			count: self.length
2570
		});
2571
	};
2572
2573
	/**
2574
	* Load contacts
2575
	* @param string backend Name of the backend ('local', 'ldap' etc.)
2576
	* @param string addressBookId
2577
	*/
2578
	ContactList.prototype.loadContacts = function(backend, addressBookId, isActive) {
2579
		if(!isActive) {
2580
			return;
2581
		}
2582
		var self = this;
2583
2584
		return $.when(self.storage.getContacts(backend, addressBookId, false))
2585
			.then(function(response) {
2586
			console.log('ContactList.loadContacts - fetching', response);
2587
			if(!response.error) {
2588
				if(response.data) {
2589
					self.insertContacts(response.data.contacts);
2590
				}
2591
			} else {
2592
				console.warn('ContactList.loadContacts - no data!!');
2593
			}
2594
		})
2595
		.fail(function(response) {
2596
			console.warn('Request Failed:', response.message);
2597
			$(document).trigger('status.contacts.error', response);
2598
		});
2599
2600
	};
2601
2602
	OC.Contacts.ContactList = ContactList;
2603
2604
})(window, jQuery, OC);
2605