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/app.js (7 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
Modernizr.load({
2
	test: Modernizr.input.placeholder,
3
	nope: [
4
			OC.filePath('contacts', 'css', 'placeholder_polyfill.min.css'),
5
			OC.filePath('contacts', 'js', 'placeholder_polyfill.jquery.min.combo.js')
6
		]
7
});
8
9
(function($) {
10
	$.QueryString = (function(a) {
11
		if (a === '') {return {};}
12
		var b = {};
13
		for (var i = 0; i < a.length; ++i)
14
		{
15
			var p=a[i].split('=');
16
			if (p.length !== 2) {
17
				continue;
18
			}
19
			b[p[0]] = decodeURIComponent(p[1].replace(/\+/g, ' '));
20
		}
21
		return b;
22
	})(window.location.search.substr(1).split('&'));
23
})(jQuery);
24
25
var utils = {};
26
27
/**
28
 * utils.isArray
29
 *
30
 * Best guess if object is an array.
31
 */
32
utils.isArray = function(obj) {
33
     // do an instanceof check first
34
     if (obj instanceof Array) {
35
         return true;
36
     }
37
     // then check for obvious falses
38
     if (typeof obj !== 'object') {
39
         return false;
40
     }
41
     if (utils.type(obj) === 'array') {
42
         return true;
43
     }
44
     return false;
45
};
46
47
utils.isInt = function(s) {
48
  return typeof s === 'number' && (s.toString().search(/^-?[0-9]+$/) === 0);
49
};
50
51
utils.isUInt = function(s) {
52
  return typeof s === 'number' && (s.toString().search(/^[0-9]+$/) === 0);
53
};
54
55
/**
56
 * utils.type
57
 *
58
 * Attempt to ascertain actual object type.
59
 */
60
utils.type = function(obj) {
61
    if (obj === null || typeof obj === 'undefined') {
62
        return String (obj);
63
    }
64
    return Object.prototype.toString.call(obj)
65
        .replace(/\[object ([a-zA-Z]+)\]/, '$1').toLowerCase();
66
};
67
68
utils.moveCursorToEnd = function(el) {
69
	if (typeof el.selectionStart === 'number') {
70
		el.selectionStart = el.selectionEnd = el.value.length;
71
	} else if (typeof el.createTextRange !== 'undefined') {
72
		el.focus();
73
		var range = el.createTextRange();
74
		range.collapse(false);
75
		range.select();
76
	}
77
};
78
79
Array.prototype.clone = function() {
80
  return this.slice(0);
81
};
82
83
Array.prototype.clean = function(deleteValue) {
84
	var arr = this.clone();
85
	for (var i = 0; i < arr.length; i++) {
86
		if (arr[i] === deleteValue) {
87
			arr.splice(i, 1);
88
			i--;
89
		}
90
	}
91
	return arr;
92
};
93
94
// Keep it DRY ;)
95
var wrongKey = function(event) {
96
	return ((event.type === 'keydown' || event.type === 'keypress') 
97
		&& (event.keyCode !== 32 && event.keyCode !== 13));
98
};
99
100
/**
101
 * Simply notifier
102
 * Arguments:
103
 * @param string message - The text message to show.
104
 * @param int timeout - The timeout in seconds before the notification disappears. Default 10.
105
 * @param function timeouthandler - A function to run on timeout.
106
 * @param function clickhandler - A function to run on click. If a timeouthandler is given it will be cancelled on click.
107
 * @param object data - An object that will be passed as argument to the timeouthandler and clickhandler functions.
108
 * @param bool cancel - If set cancel all ongoing timer events and hide the notification.
109
 */
110
OC.notify = function(params) {
111
	var self = this;
112
	if(!self.notifier) {
113
		self.notifier = $('#notification');
114
		self.notifier.on('click', function() { $(this).fadeOut();});
115
	}
116
	if(params.cancel) {
117
		self.notifier.off('click');
118
		for(var id in self.notifier.data()) {
119
			if($.isNumeric(id)) {
120
				clearTimeout(parseInt(id));
121
			}
122
		}
123
		self.notifier.text('').fadeOut().removeData();
124
	}
125
	if(params.message) {
126
		self.notifier.text(params.message).fadeIn().css('display', 'inline');
127
	}
128
129
	var timer = setTimeout(function() {
130
		self.notifier.fadeOut();
131
		if(params.timeouthandler && $.isFunction(params.timeouthandler)) {
132
			params.timeouthandler(self.notifier.data(dataid));
133
			self.notifier.off('click');
134
			self.notifier.removeData(dataid);
135
		}
136
	}, params.timeout && $.isNumeric(params.timeout) ? parseInt(params.timeout)*1000 : 10000);
137
	var dataid = timer.toString();
138
	if(params.data) {
139
		self.notifier.data(dataid, params.data);
140
	}
141
	if(params.clickhandler && $.isFunction(params.clickhandler)) {
142
		self.notifier.on('click', function() {
143
			clearTimeout(timer);
144
			self.notifier.off('click');
145
			params.clickhandler(self.notifier.data(dataid));
146
			self.notifier.removeData(dataid);
147
		});
148
	}
149
};
150
151
(function(window, $, OC) {
152
	'use strict';
153
154
	OC.Contacts = OC.Contacts || {
155
		init:function() {
156
			if(oc_debug === true) {
157
				$.error = console.error;
158
			}
159
			var self = this;
160
161
			this.lastSelectedContacts = [];
162
			this.scrollTimeoutMiliSecs = 100;
163
			this.isScrolling = false;
164
			this.cacheElements();
165
			this.storage = new OC.Contacts.Storage();
166
			this.addressBooks = new OC.Contacts.AddressBookList(
167
				this.storage,
168
				$('#app-settings-content'),
169
				$('#addressBookTemplate')
170
			);
171
			this.otherBackendConfig = new OC.Contacts.OtherBackendConfig(
172
				this.storage,
173
				this.addressBooks,
174
				$('#addressBookConfigTemplate')
175
			);
176
			this.contacts = new OC.Contacts.ContactList(
177
				this.storage,
178
				this.addressBooks,
179
				this.$contactList,
180
				this.$contactListItemTemplate,
181
				this.$contactDragItemTemplate,
182
				this.$contactFullTemplate,
183
				this.detailTemplates
184
			);
185
			this.groups = new OC.Contacts.GroupList(
186
				this.storage,
187
				this.$groupList,
188
				this.$groupListItemTemplate
189
			);
190
			self.groups.loadGroups(function() {
191
				self.loading(self.$navigation, false);
192
			});
193
			// Hide the list while populating it.
194
			this.$contactList.hide();
195
			$.when(this.addressBooks.loadAddressBooks()).then(function(addressBooks) {
196
				var deferreds = $(addressBooks).map(function(/*i, elem*/) {
197
					return self.contacts.loadContacts(this.getBackend(), this.getId(), this.isActive());
198
				});
199
				// This little beauty is from http://stackoverflow.com/a/6162959/373007 ;)
200
				$.when.apply(null, deferreds.get()).then(function() {
201
					self.contacts.setSortOrder(contacts_sortby);
202
					self.$contactList.show();
203
					$(document).trigger('status.contacts.loaded', {
204
						numcontacts: self.contacts.length
205
					});
206
					self.loading(self.$rightContent, false);
207
					// TODO: Move this to event handler
208
					self.groups.selectGroup({id:contacts_lastgroup});
209
					var id = $.QueryString.id; // Keep for backwards compatible links.
210
					if(!id) {
211
						id = window.location.hash.substr(1);
212
					}
213
					console.log('Groups loaded, id from url:', id);
214
					if(id) {
215
						self.openContact(id);
216
					}
217
					if(!contacts_properties_indexed) {
218
						// Wait a couple of mins then check if contacts are indexed.
219
						setTimeout(function() {
220
								$.when($.post(OC.generateUrl('apps/contacts/indexproperties/{user}/', {user: OC.currentUser})))
221
									.then(function(response) {
222
										if(!response.isIndexed) {
223
											OC.notify({message:t('contacts', 'Indexing contacts'), timeout:20});
224
										}
225
									});
226
						}, 10000);
227
					} else {
228
						console.log('contacts are indexed.');
229
					}
230
				}).fail(function(response) {
231
					console.warn(response);
232
					self.$rightContent.removeClass('loading');
233
					var message = t('contacts', 'Unrecoverable error loading address books: {msg}', {msg:response.message});
234
					OC.dialogs.alert(message, t('contacts', 'Error.'));
235
				});
236
			}).fail(function(response) {
237
				console.log(response.message);
238
				$(document).trigger('status.contacts.error', response);
239
			});
240
			$(OC.Tags).on('change', this.groups.categoriesChanged);
241
			this.bindEvents();
242
			this.$toggleAll.show();
243
			this.hideActions();
244
			$('.hidden-on-load').removeClass('hidden-on-load');
245
		},
246
		loading:function(obj, state) {
247
			$(obj).toggleClass('loading', state);
248
		},
249
		/**
250
		* Show/hide elements in the header
251
		* @param act An array of actions to show based on class name e.g ['add', 'delete']
252
		*/
253
		hideActions:function() {
254
			this.showActions(false);
255
		},
256
		showActions:function(act) {
257
			console.log('showActions', act);
258
			//console.trace();
259
			this.$headeractions.children().hide();
260
			if(act && act.length > 0) {
261
				this.$contactList.addClass('multiselect');
262
				this.$contactListHeader.find('.actions').css('display', '');
263
				this.$contactListHeader.find('.action').css('display', 'none');
264
				this.$contactListHeader.find('.name').attr('colspan', '5');
265
				this.$contactListHeader.find('.info').css('display', 'none');
266
				this.$contactListHeader.find('.'+act.join(',.')).css('display', '');
267
			} else {
268
				this.$contactListHeader.find('.actions').css('display', 'none');
269
				this.$contactListHeader.find('.action').css('display', '');
270
				this.$contactListHeader.find('.name').attr('colspan', '1');
271
				this.$contactListHeader.find('.info').css('display', '');
272
				this.$contactList.removeClass('multiselect');
273
			}
274
		},
275
		showAction:function(act, show) {
276
			this.$contactListHeader.find('.' + act).toggle(show);
277
		},
278
		cacheElements: function() {
279
			var self = this;
280
			this.detailTemplates = {};
281
			// Load templates for contact details.
282
			// The weird double loading is because jquery apparently doesn't
283
			// create a searchable object from a script element.
284
			$.each($($('#contactDetailsTemplate').html()), function(idx, node) {
285
				var $node = $(node);
286
				if($node.is('div')) {
287
					var $tmpl = $(node.innerHTML);
288
					self.detailTemplates[$tmpl.data('element')] = $node;
289
				}
290
			});
291
			this.$groupListItemTemplate = $('#groupListItemTemplate');
292
			this.$contactListItemTemplate = $('#contactListItemTemplate');
293
			this.$contactDragItemTemplate = $('#contactDragItemTemplate');
294
			this.$contactFullTemplate = $('#contactFullTemplate');
295
			this.$contactDetailsTemplate = $('#contactDetailsTemplate');
296
			this.$rightContent = $('#app-content');
297
			this.$navigation = $('#app-navigation');
298
			//this.$header = $('#contactsheader');
299
			this.$groupList = $('#grouplist');
300
			this.$contactList = $('#contactlist');
301
			this.$contactListHeader = $('#contactsHeader');
302
			this.$sortOrder = this.$contactListHeader.find('.action.sort');
303
			this.$sortOrder.val(contacts_sortby||'fn');
304
			this.$headeractions = this.$groupList.find('.contact-actions');
305
			this.$toggleAll = this.$contactListHeader.find('.toggle');
306
			this.$groups = this.$contactListHeader.find('.groups');
307
			this.$ninjahelp = $('#ninjahelp');
308
			this.$firstRun = $('#firstrun');
309
			this.$settings = $('#app-settings');
310
		},
311
		// Build the select to add/remove from groups.
312
		buildGroupSelect: function() {
313
			// If a contact is open we know which categories it's in
314
			if(this.currentid) {
315
				var contact = this.contacts.findById(this.currentid);
316
				if(contact === null) {
317
					return false;
318
				}
319
				this.$groups.find('optgroup,option:not([value="-1"])').remove();
320
				var addopts = '', rmopts = '';
321
				$.each(this.groups.categories, function(i, category) {
322
					if(contact.inGroup(category.name)) {
323
						rmopts += '<option value="' + category.id + '">' + category.name + '</option>';
324
					} else {
325
						addopts += '<option value="' + category.id + '">' + category.name + '</option>';
326
					}
327
				});
328
				if(addopts.length) {
329
					$(addopts).appendTo(this.$groups)
330
					.wrapAll('<optgroup data-action="add" label="' + t('contacts', 'Add to...') + '"/>');
331
				}
332
				if(rmopts.length) {
333
					$(rmopts).appendTo(this.$groups)
334
					.wrapAll('<optgroup data-action="remove" label="' + t('contacts', 'Remove from...') + '"/>');
335
				}
336
			} else if(this.contacts.getSelectedContacts().length > 0) { // Otherwise add all categories to both add and remove
337
				this.$groups.find('optgroup,option:not([value="-1"])').remove();
338
				var addopts = '', rmopts = '';
339
				$.each(this.groups.categories, function(i, category) {
340
					rmopts += '<option value="' + category.id + '">' + category.name + '</option>';
341
					addopts += '<option value="' + category.id + '">' + category.name + '</option>';
342
				});
343
				$(addopts).appendTo(this.$groups)
344
					.wrapAll('<optgroup data-action="add" label="' + t('contacts', 'Add to...') + '"/>');
345
				$(rmopts).appendTo(this.$groups)
346
					.wrapAll('<optgroup data-action="remove" label="' + t('contacts', 'Remove from...') + '"/>');
347
			} else {
348
				// 3rd option: No contact open, none checked, just show "Add group..."
349
				this.$groups.find('optgroup,option:not([value="-1"])').remove();
350
			}
351
			$('<option value="add">' + t('contacts', 'Add group...') + '</option>').appendTo(this.$groups);
352
			this.$groups.val(-1);
353
		},
354
		bindEvents: function() {
355
			var self = this;
356
357
			// Should fix Opera check for delayed delete.
358
			$(window).unload(function (){
359
				$(window).trigger('beforeunload');
360
			});
361
362
			this.hashChange = function() {
363
				console.log('hashchange', window.location.hash);
364
				var id = String(window.location.hash.substr(1));
365
				if(id && id !== self.currentid && self.contacts.findById(id) !== null) {
366
					self.openContact(id);
367
				} else if(!id && self.currentid) {
368
					self.closeContact(self.currentid);
369
				}
370
			};
371
372
			// This apparently get's called on some weird occasions.
373
			//$(window).bind('popstate', this.hashChange);
374
			$(window).bind('hashchange', this.hashChange);
375
376
			// App specific events
377
			$(document).bind('status.contact.deleted', function(e, data) {
378
				var id = String(data.id);
379
				if(id === self.currentid) {
380
					delete self.currentid;
381
				}
382
				console.log('contact', data.id, 'deleted');
383
				// update counts on group lists
384
				self.groups.removeFromAll(data.id, true, true);
385
			});
386
387
			$(document).bind('status.contact.added', function(e, data) {
388
				self.currentid = String(data.id);
389
				self.buildGroupSelect();
390
				self.hideActions();
391
			});
392
393
			// Keep error messaging at one place to be able to replace it.
394
			$(document).bind('status.contacts.error', function(e, data) {
395
				var message = data.message;
396
				console.warn(message);
397
				//console.trace();
398
				OC.notify({message:message});
399
			});
400
401
			$(document).bind('status.contact.enabled', function(e, enabled) {
402
				console.log('status.contact.enabled', enabled);
403
				/*if(enabled) {
404
					self.showActions(['back', 'download', 'delete', 'groups']);
405
				} else {
406
					self.showActions(['back']);
407
				}*/
408
			});
409
410
			$(document).bind('status.contacts.count', function(e, response) {
411
				console.log('Num contacts:', response.count);
412
				if(response.count > 0) {
413
					self.$contactList.show();
414
					self.$firstRun.hide();
415
				}
416
			});
417
418
			$(document).bind('status.contacts.loaded status.contacts.deleted', function(e, response) {
419
				console.log('status.contacts.loaded', response);
420
				if(response.error) {
421
					$(document).trigger('status.contacts.error', response);
422
					console.log('Error loading contacts!');
423
				} else {
424
					if(response.numcontacts === 0) {
425
						self.$contactList.hide();
426
						self.$firstRun.show();
427
					} else {
428
						self.$contactList.show();
429
						self.$firstRun.hide();
430
					$.each(self.addressBooks.addressBooks, function(idx, addressBook) {
431
						console.log('addressBook', addressBook);
432
						if(!addressBook.isActive()) {
433
							self.contacts.showFromAddressbook(addressBook.getId(), false);
434
						}
435
					});
436
					}
437
				}
438
			});
439
440
			$(document).bind('status.contact.currentlistitem', function(e, result) {
441
				//console.log('status.contact.currentlistitem', result, self.$rightContent.height());
442
				if(self.dontScroll !== true) {
443
					if(result.pos > self.$rightContent.height()) {
444
						self.$rightContent.scrollTop(result.pos - self.$rightContent.height() + result.height);
445
					}
446
					else if(result.pos < self.$rightContent.offset().top) {
447
						self.$rightContent.scrollTop(result.pos);
448
					}
449
				} else {
450
					setTimeout(function() {
451
						self.dontScroll = false;
452
					}, 100);
453
				}
454
				self.currentlistid = result.id;
455
			});
456
457
			$(document).bind('status.nomorecontacts', function(e, result) {
458
				console.log('status.nomorecontacts', result);
459
				self.$contactList.hide();
460
				self.$firstRun.show();
461
			});
462
463
			$(document).bind('status.visiblecontacts', function(e, result) {
464
				console.log('status.visiblecontacts', result);
465
				// TODO: To be decided.
466
			});
467
468
			$(document).bind('request.openurl', function(e, data) {
469
				switch(data.type) {
470
					case 'url':
471
						var regexp = /(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!-\/]))?/;
472
						//if(new RegExp("[a-zA-Z0-9]+://([a-zA-Z0-9_]+:[a-zA-Z0-9_]+@)?([a-zA-Z0-9.-]+\\.[A-Za-z]{2,4})(:[0-9]+)?(/.*)?").test(data.url)) {
473
						if(regexp.test(data.url)) {
474
							var newWindow = window.open(data.url,'_blank');
475
							newWindow.focus();
476
						} else {
477
							$(document).trigger('status.contacts.error', {
478
								error: true,
479
								message: t('contacts', 'Invalid URL: "{url}"', {url:data.url})
480
							});
481
						}
482
						break;
483
					case 'email':
484
						var regexp = /^([a-zA-Z0-9_\.\-])+\@(([a-zA-Z0-9\-])+\.)+([a-zA-Z0-9]{2,4})+$/;
485
						if(regexp.test(data.url)) {
486
							console.log('success');
487
							var url = 'mailto:' + data.url;
488
							try {
489
								var mailer = window.open(url, 'Mailer');
490
							} catch(e) {
491
								console.log('There was an error opening a mail composer.', e);
492
							}
493
							setTimeout(function(){
494
								try {
495
									if(mailer.location.href === url || mailer.location.href.substr(0, 6) === 'about:') {
496
										mailer.close();
497
									}
498
								} catch(e) {
499
									console.log('There was an error opening a mail composer.', e);
500
								}
501
								}, 1000);
502
						} else {
503
							$(document).trigger('status.contacts.error', {
504
								error: true,
505
								message: t('contacts', 'Invalid email: "{url}"', {url:data.url})
506
							});
507
						}
508
						break;
509
					case 'adr':
510
						var address = data.url.filter(function(n) {
511
							return n;
512
						});
513
						var newWindow = window.open('http://open.mapquest.com/?q='+address, '_blank');
514
						newWindow.focus();
515
						break;
516
				}
517
			});
518
519
			// A contact id was in the request
520
			$(document).bind('request.loadcontact', function(e, result) {
521
				console.log('request.loadcontact', result);
522
				if(self.numcontacts) {
523
					self.openContact(result.id);
524
				} else {
525
					// Contacts are not loaded yet, try again.
526
					console.log('waiting for contacts to load');
527
					setTimeout(function() {
528
						$(document).trigger('request.loadcontact', {
529
							id: result.id
530
						});
531
					}, 1000);
532
				}
533
			});
534
535
			$(document).bind('request.contact.move', function(e, data) {
536
				console.log('contact', data, 'request.contact.move');
537
				self.addressBooks.moveContact(data.contact, data.from, data.target);
538
			});
539
540
			$(document).bind('request.contact.setasfavorite', function(e, data) {
541
				console.log('contact', data.id, 'request.contact.setasfavorite');
542
				self.groups.setAsFavorite(data.id, data.state);
543
			});
544
545
			$(document).bind('request.contact.addtogroup', function(e, data) {
546
				self.groups.addTo(data.id, data.groupid, function(response) {
547
					console.log('contact', data.id, 'request.contact.addtogroup', response);
548
				});
549
			});
550
551
			$(document).bind('request.contact.removefromgroup', function(e, data) {
552
				console.log('contact', data.id, 'request.contact.removefromgroup');
553
				self.groups.removeFrom(data.id, data.groupid);
554
			});
555
556
			$(document).bind('request.contact.export', function(e, data) {
557
				console.log('request.contact.export', data);
558
				document.location.href = OC.generateUrl('apps/contacts/addressbook/{backend}/{addressBookId}/contact/{contactId}/export', data);
559
			});
560
561
			$(document).bind('request.contact.close', function(e, data) {
562
				var id = String(data.id);
563
				console.log('contact', data.id, 'request.contact.close');
564
				self.closeContact(id);
565
			});
566
567
			$(document).bind('request.contact.open', function(e, data) {
568
				var id = String(data.id);
569
				console.log('contact', data.id, 'request.contact.open');
570
				self.openContact(id);
571
			});
572
573
			$(document).bind('request.contact.delete', function(e, data) {
574
				var id = String(data.contactId);
575
				console.log('contact', data, 'request.contact.delete');
576
				self.closeContact(id);
577
				self.contacts.delayedDelete(data);
578
				self.$contactList.removeClass('dim');
579
				self.hideActions();
580
			});
581
582
			$(document).bind('request.contact.merge', function(e, data) {
583
				console.log('contact','request.contact.merge', data);
584
				var merger = self.contacts.findById(data.merger);
585
				var mergees = [];
586
				if(!merger) {
587
					$(document).trigger('status.contacts.error', {
588
						message: t('contacts', 'Merge failed. Cannot find contact: {id}', {id:data.merger})
589
					});
590
					return;
591
				}
592
				$.each(data.mergees, function(idx, id) {
593
					var contact = self.contacts.findById(id);
594
					if(!contact) {
595
						console.warn('cannot find', id, 'by id');
596
					}
597
					mergees.push(contact);
598
				});
599
				if(!merger.merge(mergees)) {
600
					$(document).trigger('status.contacts.error', {
601
						message: t('contacts', 'Merge failed.')
602
					});
603
					return;
604
				}
605
				merger.saveAll(function(response) {
606
					if(response.error) {
607
						$(document).trigger('status.contacts.error', {
608
							message: t('contacts', 'Merge failed. Error saving contact.')
609
						});
610
						return;
611
					} else {
612
						if(data.deleteOther) {
613
							self.contacts.delayedDelete(mergees);
614
						}
615
						console.log('merger', merger);
616
						self.openContact(merger.getId());
617
					}
618
				});
619
			});
620
621
			$(document).bind('request.select.contactphoto.fromlocal', function(e, contact) {
622
				console.log('request.select.contactphoto.fromlocal', contact);
623
				$('#contactphoto_fileupload').trigger('click', contact);
624
			});
625
626
			$(document).bind('request.select.contactphoto.fromcloud', function(e, metadata) {
627
				console.log('request.select.contactphoto.fromcloud', metadata);
628
				OC.dialogs.filepicker(t('contacts', 'Select photo'), function(path) {
629
					self.cloudPhotoSelected(metadata, path);
630
				}, false, 'image', true);
631
			});
632
633
			$(document).bind('request.edit.contactphoto', function(e, metadata) {
634
				console.log('request.edit.contactphoto', metadata);
635
				self.editCurrentPhoto(metadata);
636
			});
637
638
			$(document).bind('request.groups.reload', function(e, result) {
639
				console.log('request.groups.reload', result);
640
				self.groups.loadGroups(function() {
641
					self.groups.triggerLastGroup();
642
				});
643
			});
644
645
			$(document).bind('status.group.groupremoved', function(e, result) {
646
				console.log('status.group.groupremoved', result);
647
				if(parseInt(result.groupid) === parseInt(self.currentgroup)) {
648
					self.contacts.showContacts([]);
649
					self.currentgroup = 'all';
650
				}
651
				$.each(result.contacts, function(idx, contactid) {
652
					var contact = self.contacts.findById(contactid);
653
654
					// Test if valid because there could be stale ids in the tag index.
655
					if(contact) {
656
						contact.removeFromGroup(result.groupname);
657
					}
658
				});
659
			});
660
661
			$(document).bind('status.group.grouprenamed', function(e, result) {
662
				console.log('status.group.grouprenamed', result);
663
				$.each(result.contacts, function(idx, contactid) {
664
					var contact = self.contacts.findById(contactid);
665
					if(!contact) {
666
						console.warn('Couldn\'t find contact', contactid);
667
						return true; // continue
668
					}
669
					contact.renameGroup(result.from, result.to);
670
				});
671
			});
672
673
			$(document).bind('status.group.contactremoved', function(e, result) {
674
				console.log('status.group.contactremoved', result, self.currentgroup, result.groupid);
675
				var contact = self.contacts.findById(result.contactid);
676
				if(contact) {
677
					if(contact.inGroup(result.groupname)) {
678
						contact.removeFromGroup(result.groupname);
679
					}
680
					if(parseInt(self.currentgroup) === parseInt(result.groupid)) {
681
						console.log('Hiding', contact.getId());
682
						contact.hide();
683
					}
684
				}
685
			});
686
687
			$(document).bind('status.group.contactadded', function(e, result) {
688
				console.log('status.group.contactadded', result);
689
				var contact = self.contacts.findById(result.contactid);
690
				if(contact) {
691
					if(!contact.inGroup(result.groupname)) {
692
						contact.addToGroup(result.groupname);
693
					}
694
					if(parseInt(self.currentgroup) === parseInt(result.groupid)) {
695
						console.log('Showing', contact.getId());
696
						contact.show();
697
					}
698
					if(self.currentgroup === 'uncategorized') {
699
						console.log('Hiding', contact.getId());
700
						contact.hide();
701
					}
702
				}
703
			});
704
705
			// Group sorted, save the sort order
706
			$(document).bind('status.groups.sorted', function(e, result) {
707
				console.log('status.groups.sorted', result);
708
				$.when(self.storage.setPreference('groupsort', result.sortorder)).then(function(response) {
709
					if(response.error) {
710
						$(document).trigger('status.contacts.error', {
711
							message: response ? response.message : t('contacts', 'Network or server error. Please inform administrator.')
712
						});
713
					}
714
				})
715
				.fail(function(response) {
716
					console.log(response.message);
717
					$(document).trigger('status.contacts.error', response);
718
					done = true;
719
				});
720
			});
721
			// Group selected, only show contacts from that group
722
			$(document).bind('status.group.selected', function(e, result) {
723
				console.log('status.group.selected', result);
724
				self.currentgroup = result.id;
725
				// Close any open contact.
726
				if(self.currentid) {
727
					var id = self.currentid;
728
					self.closeContact(id);
729
					self.jumpToContact(id);
730
				}
731
				self.$toggleAll.show();
732
				self.hideActions();
733
				if(result.type === 'category' ||  result.type === 'fav') {
734
					self.contacts.showContacts(result.contacts);
735
				} else if(result.type === 'shared') {
736
					self.contacts.showFromAddressbook(self.currentgroup, true, true);
737
				} else if(result.type === 'uncategorized') {
738
					self.contacts.showUncategorized();
739
				} else {
740
					self.contacts.showContacts(self.currentgroup);
741
				}
742
				$.when(self.storage.setPreference('lastgroup', self.currentgroup)).then(function(response) {
743
					if(response.error) {
744
						$(document).trigger('status.contacts.error', response);
745
					}
746
				})
747
				.fail(function(response) {
748
					console.log(response.message);
749
					$(document).trigger('status.contacts.error', response);
750
					done = true;
751
				});
752
				self.$rightContent.scrollTop(0);
753
			});
754
			// mark items whose title was hid under the top edge as read
755
			/*this.$rightContent.scroll(function() {
756
				// prevent too many scroll requests;
757
				if(!self.isScrolling) {
758
					self.isScrolling = true;
759
					var num = self.$contactList.find('tr').length;
760
					//console.log('num', num);
761
					var offset = self.$contactList.find('tr:eq(' + (num-20) + ')').offset().top;
762
					if(offset < self.$rightContent.height()) {
763
						console.log('load more');
764
						self.contacts.loadContacts(num, function() {
765
							self.isScrolling = false;
766
						});
767
					} else {
768
						setTimeout(function() {
769
							self.isScrolling = false;
770
						}, self.scrollTimeoutMiliSecs);
771
					}
772
					//console.log('scroll, unseen:', offset, self.$rightContent.height());
773
				}
774
			});*/
775
			$('#contactphoto_fileupload').on('click', function(event, contact) {
776
				console.log('contact', contact);
777
				var metaData = contact.metaData();
778
				var url = OC.generateUrl(
779
					'apps/contacts/addressbook/{backend}/{addressBookId}/contact/{contactId}/photo',
780
					{backend: metaData.backend, addressBookId: metaData.addressBookId, contactId: metaData.contactId}
781
				);
782
				$(this).fileupload('option', 'url', url);
783
			}).fileupload({
784
				singleFileUploads: true,
785
				multipart: false,
786
				dataType: 'json',
787
				type: 'PUT',
788
				dropZone: null, pasteZone: null,
789
				acceptFileTypes: /^image\//,
790
				add: function(e, data) {
791
					var file = data.files[0];
792
					if (file.type.substr(0, 6) !== 'image/') {
793
						$(document).trigger('status.contacts.error', {
794
							error: true,
795
							message: t('contacts', 'Only images can be used as contact photos')
796
						});
797
						return;
798
					}
799
					if (file.size > parseInt($(this).siblings('[name="MAX_FILE_SIZE"]').val())) {
800
						$(document).trigger('status.contacts.error', {
801
							error: true,
802
							message: t(
803
								'contacts',
804
								'The size of "{filename}" exceeds the maximum allowed {size}',
805
								{filename: file.name, size: $(this).siblings('[name="max_human_file_size"]').val()}
806
							)
807
						});
808
						return;
809
					}
810
					data.submit();
811
				},
812
				start: function(e, data) {
813
					console.log('fileupload.start',data);
814
				},
815
				done: function (e, data) {
816
					console.log('Upload done:', data);
817
					self.editPhoto(
818
						data.result.metadata,
819
						data.result.tmp
820
					);
821
				},
822
				fail: function(e, data) {
823
					console.log('fail', data);
824
					var response = self.storage.formatResponse(data.jqXHR);
825
					$(document).trigger('status.contacts.error', response);
826
				}
827
			});
828
829
			this.$rightContent.bind('drop dragover', function (e) {
830
				e.preventDefault();
831
			});
832
833
			this.$ninjahelp.find('.close').on('click keydown',function(event) {
834
				if(wrongKey(event)) {
835
					return;
836
				}
837
				self.$ninjahelp.hide();
838
			});
839
840
			this.$toggleAll.on('change', function(event) {
841
				event.stopPropagation();
842
				event.preventDefault();
843
				var isChecked = $(this).is(':checked');
844
				self.setAllChecked(isChecked);
845
				if(self.$groups.find('option').length === 1) {
846
					self.buildGroupSelect();
847
				}
848
				if(isChecked) {
849
					self.showActions(['toggle', 'add', 'download', 'groups', 'delete', 'favorite', 'merge']);
850
				} else {
851
					self.hideActions();
852
				}
853
			});
854
855
			this.$contactList.on('change', 'input:checkbox', function(/*event*/) {
856
				var selected = self.contacts.getSelectedContacts();
857
				var id = String($(this).val());
858
				// Save list of last selected contact to be able to select range
859
				$(this).is(':checked') && self.lastSelectedContacts.indexOf(id) === -1
860
					? self.lastSelectedContacts.push(id)
861
					: self.lastSelectedContacts.splice(self.lastSelectedContacts.indexOf(id), 1);
862
863
				if(selected.length > 0 && self.$groups.find('option').length === 1) {
864
					self.buildGroupSelect();
865
				}
866
				if(selected.length === 0) {
867
					self.hideActions();
868
				} else if(selected.length === 1) {
869
					self.showActions(['toggle', 'add', 'download', 'groups', 'delete', 'favorite']);
870
				} else {
871
					self.showActions(['toggle', 'add', 'download', 'groups', 'delete', 'favorite', 'merge']);
872
				}
873
			});
874
875
			this.$contactList.on('click', 'label:not([for=select_all])', function(/*event*/) {
876
				var $input = $(this).prev('input');
877
				$input.prop('checked', !$input.prop('checked'));
878
				$input.trigger('change');
879
				return false; // Prevent opening contact
880
			});
881
882
			// Add title to names that would elliptisized (is that a word?)
883
			this.$contactList.on('mouseenter', '.nametext', function() {
884
				var $this = $(this);
885
886
				if($this.width() > $this.parent().width() && !$this.attr('title')) {
887
					$this.attr('title', $this.text());
888
				}
889
			});
890
891
			this.$sortOrder.on('change', function() {
892
				$(this).blur().addClass('loading');
893
				contacts_sortby = $(this).val();
894
				self.contacts.setSortOrder();
895
				$(this).removeClass('loading');
896
				self.storage.setPreference('sortby', contacts_sortby);
897
			});
898
899
			// Add to/remove from group multiple contacts.
900
			this.$groups.on('change', function() {
901
				var $opt = $(this).find('option:selected');
902
				var action = $opt.parent().data('action');
903
				var groupName, groupId, buildnow = false;
904
905
				var contacts = self.contacts.getSelectedContacts();
906
				var ids = $.map(contacts, function(c) {return c.getId();});
907
908
				self.setAllChecked(false);
909
				self.$toggleAll.prop('checked', false);
910
				if(!self.currentid) {
911
					self.hideActions();
912
				}
913
914
				if($opt.val() === 'add') { // Add new group
915
					action = 'add';
916
					console.log('add group...');
917
					self.$groups.val(-1);
918
					self.addGroup(function(response) {
919
						if(!response.error) {
920
							groupId = response.id;
921
							groupName = response.name;
922
							self.groups.addTo(ids, groupId, function(result) {
923
								if(!result.error) {
924
									$.each(ids, function(idx, id) {
925
										// Delay each contact to not trigger too many ajax calls
926
										// at a time.
927
										setTimeout(function() {
928
											var contact = self.contacts.findById(id);
929
											if(contact === null) {
930
												return true;
931
											}
932
											contact.addToGroup(groupName);
933
											// I don't think this is used...
934
											if(buildnow) {
935
												self.buildGroupSelect();
936
											}
937
										}, 1000);
938
									});
939
								} else {
940
									$(document).trigger('status.contacts.error', result);
941
								}
942
							});
943
						} else {
944
							$(document).trigger('status.contacts.error', response);
945
						}
946
					});
947
					return;
948
				}
949
950
				groupName = $opt.text(), groupId = $opt.val();
951
952
				if(action === 'add') {
953
					self.groups.addTo(ids, $opt.val(), function(result) {
954
						console.log('after add', result);
955
						if(!result.error) {
956
							$.each(result.ids, function(idx, id) {
957
								// Delay each contact to not trigger too many ajax calls
958
								// at a time.
959
								setTimeout(function() {
960
									console.log('adding', id, 'to', groupName);
961
									var contact = self.contacts.findById(id);
962
									if(contact === null) {
963
										return true;
964
									}
965
									contact.addToGroup(groupName);
966
									// I don't think this is used...
967
									if(buildnow) {
968
										self.buildGroupSelect();
969
									}
970
								}, 1000);
971
							});
972
						} else {
973
							var msg = result.message ? result.message : t('contacts', 'Error adding to group.');
974
							$(document).trigger('status.contacts.error', {message:msg});
975
						}
976
					});
977
					if(!buildnow) {
978
						self.$groups.val(-1).hide().find('optgroup,option:not([value="-1"])').remove();
979
					}
980
				} else if(action === 'remove') {
981
					self.groups.removeFrom(ids, $opt.val(), false, function(result) {
982
						console.log('after remove', result);
983
						if(!result.error) {
984
							var groupname = $opt.text(), groupid = $opt.val();
0 ignored issues
show
groupid does not seem to be used.
Loading history...
985
							$.each(result.ids, function(idx, id) {
986
								var contact = self.contacts.findById(id);
987
								if(contact === null) {
988
									return true;
989
								}
990
								contact.removeFromGroup(groupname);
991
								if(buildnow) {
992
									self.buildGroupSelect();
993
								}
994
							});
995
						} else {
996
							var msg = result.message ? result.message : t('contacts', 'Error removing from group.');
997
							$(document).trigger('status.contacts.error', {message:msg});
998
						}
999
					});
1000
					if(!buildnow) {
1001
						self.$groups.val(-1).hide().find('optgroup,option:not([value="-1"])').remove();
1002
					}
1003
				} // else something's wrong ;)
1004
				self.setAllChecked(false);
1005
			});
1006
1007
			this.$contactList.on('mouseenter', 'tr.contact', function(event) {
0 ignored issues
show
event does not seem to be used.
Loading history...
1008
				if ($(this).data('obj').hasPermission(OC.PERMISSION_DELETE)) {
1009
					var $td = $(this).find('td').filter(':visible').last();
1010
					$('<a />').addClass('icon-delete svg delete action').appendTo($td);
1011
				}
1012
			});
1013
1014
			this.$contactList.on('mouseleave', 'tr.contact', function(event) {
0 ignored issues
show
event does not seem to be used.
Loading history...
1015
				$(this).find('a.delete').remove();
1016
			});
1017
1018
			// Prevent Firefox from selecting the table-cell
1019
			this.$contactList.mousedown(function (event) {
1020
				if (event.ctrlKey || event.metaKey || event.shiftKey) {
1021
					event.preventDefault();
1022
				}
1023
			});
1024
1025
			$(window).on('click', function(event) {
1026
				if(!$(event.target).is('a[href^="mailto"]')) {
1027
					return;
1028
				}
1029
				console.log('mailto clicked', $(event.target));
1030
1031
				$(document).trigger('request.openurl', {
1032
					type: 'email',
1033
					url: $(event.target).attr('href').substr(7)
1034
				});
1035
1036
				event.stopPropagation();
1037
				event.preventDefault();
1038
			});
1039
1040
			// Contact list. Either open a contact or perform an action (mailto etc.)
1041
			this.$contactList.on('click', 'tr.contact', function(event) {
1042
				if($(event.target).is('input') || $(event.target).is('a[href^="mailto"]')) {
1043
					return;
1044
				}
1045
				// Select a single contact or a range of contacts.
1046
				if(event.ctrlKey || event.metaKey || event.shiftKey) {
1047
					event.stopPropagation();
1048
					event.preventDefault();
1049
					self.dontScroll = true;
1050
					var $input = $(this).find('input:checkbox');
1051
					var index = self.$contactList.find('tr.contact:visible').index($(this));
0 ignored issues
show
index does not seem to be used.
Loading history...
1052
					if(event.shiftKey && self.lastSelectedContacts.length > 0) {
1053
						self.contacts.selectRange(
1054
							$(this).data('id'),
1055
							self.lastSelectedContacts[self.lastSelectedContacts.length-1]
1056
						);
1057
					} else {
1058
						self.contacts.setSelected($(this).data('id'), !$input.prop('checked'));
1059
					}
1060
					return;
1061
				}
1062
				if($(event.target).is('a.mailto')) {
1063
					$(document).trigger('request.openurl', {
1064
						type: 'email',
1065
						url: $.trim($(this).find('.email').text())
1066
					});
1067
					return;
1068
				}
1069
				if($(event.target).is('a.delete')) {
1070
					$(document).trigger('request.contact.delete', {
1071
						contactId: $(this).data('id')
1072
					});
1073
					return;
1074
				}
1075
				self.openContact(String($(this).data('id')));
1076
			});
1077
1078
			this.$settings.find('#app-settings-header').on('click keydown',function(event) {
1079
				if(wrongKey(event)) {
1080
					return;
1081
				}
1082
				var bodyListener = function(e) {
1083
					if(self.$settings.find($(e.target)).length === 0) {
1084
						self.$settings.switchClass('open', '');
1085
					}
1086
				};
1087
				if(self.$settings.hasClass('open')) {
1088
					self.$settings.switchClass('open', '');
1089
					$('body').unbind('click', bodyListener);
1090
				} else {
1091
					self.$settings.switchClass('', 'open');
1092
					$('body').bind('click', bodyListener);
1093
				}
1094
			});
1095
1096
			var addContact = function() {
1097
				if(self.contacts.addressBooks.count() > 0){
1098
                    			console.log('add');
1099
                    			if(self.currentid) {
1100
                        			if(self.currentid === 'new') {
1101
                            				return;
1102
                        			} else {
1103
                            				var contact = self.contacts.findById(self.currentid);
1104
                            				if(contact) {
1105
                                				contact.close(true);
1106
                            				}
1107
                        			}
1108
                    			}
1109
				    	self.currentid = 'new';
1110
				    	// Properties that the contact doesn't know
1111
				    	console.log('addContact, groupid', self.currentgroup);
1112
				    	var groupprops = {
1113
				        	favorite: false,
1114
				        	groups: self.groups.categories,
1115
				        	currentgroup: {id:self.currentgroup, name:self.groups.nameById(self.currentgroup)}
1116
				    	};
1117
				    	self.$firstRun.hide();
1118
				    	self.$contactList.show();
1119
				    	self.tmpcontact = self.contacts.addContact(groupprops);
1120
				    	self.tmpcontact.prependTo(self.$contactList.find('tbody')).show().find('.fullname').focus();
1121
				    	self.$rightContent.scrollTop(0);
1122
				    	self.hideActions();
1123
                		}else{
1124
                    			OC.dialogs.alert(t('contacts','Please create an addressbook first'),t('contacts','Contacts'));
1125
                		}
1126
			};
1127
1128
			this.$firstRun.on('click keydown', '.import', function(event) {
1129
				event.preventDefault();
1130
				event.stopPropagation();
1131
				self.$settings.find('.settings').click();
1132
			});
1133
1134
			this.$firstRun.on('click keydown', '.add-contact', function(event) {
1135
				if(wrongKey(event)) {
1136
					return;
1137
				}
1138
				addContact();
1139
			});
1140
1141
			this.$groupList.on('click keydown', '.add-contact', function(event) {
1142
				if(wrongKey(event)) {
1143
					return;
1144
				}
1145
				addContact();
1146
			});
1147
1148
			this.$contactListHeader.on('click keydown', '.delete', function(event) {
1149
				if(wrongKey(event)) {
1150
					return;
1151
				}
1152
				console.log('delete');
1153
				if(self.currentid) {
1154
					console.assert(typeof self.currentid === 'string', 'self.currentid is not a string');
1155
					contactInfo = self.contacts[self.currentid].metaData();
1156
					self.contacts.delayedDelete(contactInfo);
1157
				} else {
1158
					self.contacts.delayedDelete(self.contacts.getSelectedContacts());
1159
				}
1160
				self.hideActions();
1161
			});
1162
1163
			this.$contactListHeader.on('click keydown', '.download', function(event) {
1164
				if(wrongKey(event)) {
1165
					return;
1166
				}
1167
1168
				var doDownload = function(contacts) {
1169
					// Only get backend, addressbookid and contactid
1170
					contacts = $.map(contacts, function(c) {return c.metaData();});
1171
					var targets = {};
1172
					// Try to shorten request URI
1173
					$.each(contacts, function(idx, contact) {
1174
						if(!targets[contact.backend]) {
1175
							targets[contact.backend] = {};
1176
						}
1177
						if(!targets[contact.backend][contact.addressBookId]) {
1178
							targets[contact.backend][contact.addressBookId] = [];
1179
						}
1180
						targets[contact.backend][contact.addressBookId].push(contact.contactId);
1181
					});
1182
					targets = JSON.stringify(targets);
1183
					var url = OC.generateUrl('apps/contacts/exportSelected?t={t}', {t:targets});
1184
					//console.log('export url', url);
1185
					document.location.href = url;
1186
				};
1187
				var contacts = self.contacts.getSelectedContacts();
1188
				console.log('download', contacts.length);
1189
1190
				// The 300 is just based on my little testing with Apache2
1191
				// Other web servers may fail before.
1192
				if(contacts.length > 300) {
1193
					OC.notify({
1194
						message:t('contacts', 'You have selected over 300 contacts.\nThis will most likely fail! Click here to try anyway.'),
1195
						timeout:5,
1196
						clickhandler:function() {
1197
							doDownload(contacts);
1198
						}
1199
					});
1200
				} else {
1201
					doDownload(contacts);
1202
				}
1203
			});
1204
1205
			this.$contactListHeader.on('click keydown', '.merge', function(event) {
1206
				if(wrongKey(event)) {
1207
					return;
1208
				}
1209
				console.log('merge');
1210
				self.mergeSelectedContacts();
1211
			});
1212
1213
			this.$contactListHeader.on('click keydown', '.favorite', function(event) {
1214
				if(wrongKey(event)) {
1215
					return;
1216
				}
1217
1218
				var contacts = self.contacts.getSelectedContacts();
1219
1220
				self.setAllChecked(false);
1221
				self.$toggleAll.prop('checked', false);
1222
				if(!self.currentid) {
1223
					self.hideActions();
1224
				}
1225
1226
				$.each(contacts, function(idx, contact) {
1227
					if(!self.groups.isFavorite(contact.getId())) {
1228
						self.groups.setAsFavorite(contact.getId(), true, function(result) {
1229
							if(result.status !== 'success') {
1230
								$(document).trigger('status.contacts.error', {message:
1231
									t('contacts',
1232
										'Error setting {name} as favorite.',
1233
										{name:contact.getDisplayName()})
1234
								});
1235
							}
1236
						});
1237
					}
1238
				});
1239
1240
				self.hideActions();
1241
			});
1242
1243
			this.$contactList.on('mouseenter', 'td.email', function(event) {
0 ignored issues
show
event does not seem to be used.
Loading history...
1244
				if($.trim($(this).text()).length > 3) {
1245
					$(this).find('.mailto').css('display', 'inline-block'); //.fadeIn(100);
1246
				}
1247
			});
1248
			this.$contactList.on('mouseleave', 'td.email', function(event) {
0 ignored issues
show
event does not seem to be used.
Loading history...
1249
				$(this).find('.mailto').fadeOut(100);
1250
			});
1251
1252
			$('body').on('touchmove', function(event) {
1253
				event.preventDefault();
1254
			});
1255
1256
			$(document).on('keyup', function(event) {
1257
				if(!$(event.target).is('body') || event.isPropagationStopped()) {
1258
					return;
1259
				}
1260
				var keyCode = Math.max(event.keyCode, event.which);
1261
				// TODO: This should go in separate method
1262
				console.log(event, keyCode + ' ' + event.target.nodeName);
1263
				/**
1264
				* To add:
1265
				* Shift-a: add addressbook
1266
				* u (85): hide/show leftcontent
1267
				* f (70): add field
1268
				*/
1269
				switch(keyCode) {
1270
					case 13: // Enter?
1271
						console.log('Enter?');
1272
						if(!self.currentid && self.currentlistid) {
1273
							self.openContact(self.currentlistid);
1274
						}
1275
						break;
1276
					case 27: // Esc
1277
						if(self.$ninjahelp.is(':visible')) {
1278
							self.$ninjahelp.hide();
1279
						} else if(self.currentid) {
1280
							self.closeContact(self.currentid);
1281
						}
1282
						break;
1283
					case 46: // Delete
1284
						if(event.shiftKey) {
1285
							self.contacts.delayedDelete(self.currentid);
1286
						}
1287
						break;
1288
					case 40: // down
1289
					case 74: // j
1290
						console.log('next');
1291
						if(!self.currentid && self.currentlistid) {
1292
							self.contacts.contacts[self.currentlistid].next();
1293
						}
1294
						break;
1295
					case 65: // a
1296
						if(event.shiftKey) {
1297
							console.log('add group?');
1298
							break;
1299
						}
1300
						addContact();
1301
						break;
1302
					case 38: // up
1303
					case 75: // k
1304
						console.log('previous');
1305
						if(!self.currentid && self.currentlistid) {
1306
							self.contacts.contacts[self.currentlistid].prev();
1307
						}
1308
						break;
1309
					case 34: // PageDown
1310
					case 78: // n
1311
						console.log('page down');
1312
						break;
1313
					case 79: // o
1314
						console.log('open contact?');
1315
						break;
1316
					case 33: // PageUp
1317
					case 80: // p
1318
						// prev addressbook
1319
						//OC.contacts.contacts.previousAddressbook();
1320
						break;
1321
					case 82: // r
1322
						console.log('refresh - what?');
1323
						break;
1324
					case 63: // ? German.
1325
						if(event.shiftKey) {
1326
							self.$ninjahelp.toggle('fast');
1327
						}
1328
						break;
1329
					case 171: // ? Danish
1330
					case 191: // ? Standard qwerty
1331
						self.$ninjahelp.toggle('fast').position({my: 'center', at: 'center', of: '#content'});
1332
						break;
1333
				}
1334
1335
			});
1336
1337
			// find all with a title attribute and tipsy them
1338
			$('.tooltipped.downwards:not(.onfocus)').tipsy({gravity: 'n'});
1339
			$('.tooltipped.upwards:not(.onfocus)').tipsy({gravity: 's'});
1340
			$('.tooltipped.rightwards:not(.onfocus)').tipsy({gravity: 'w'});
1341
			$('.tooltipped.leftwards:not(.onfocus)').tipsy({gravity: 'e'});
1342
			$('.tooltipped.downwards.onfocus').tipsy({trigger: 'focus', gravity: 'n'});
1343
			$('.tooltipped.rightwards.onfocus').tipsy({trigger: 'focus', gravity: 'w'});
1344
		},
1345
		mergeSelectedContacts: function() {
1346
			var contacts = this.contacts.getSelectedContacts();
1347
			this.$rightContent.append('<div id="merge_contacts_dialog"></div>');
1348
			if(!this.$mergeContactsTmpl) {
1349
				this.$mergeContactsTmpl = $('#mergeContactsTemplate');
1350
			}
1351
			var $dlg = this.$mergeContactsTmpl.octemplate();
1352
			var $liTmpl = $dlg.find('li').detach();
1353
			var $mergeList = $dlg.find('.mergelist');
1354
			$.each(contacts, function(idx, contact) {
1355
				var $li = $liTmpl
1356
					.octemplate({idx: idx, id: contact.getId(), displayname: contact.getDisplayName()});
1357
				if(!contact.data.thumbnail) {
1358
					$li.addClass('thumbnail');
1359
				} else {
1360
					$li.css('background-image', 'url(data:image/png;base64,' + contact.data.thumbnail + ')');
1361
				}
1362
				if(idx === 0) {
1363
					$li.find('input:radio').prop('checked', true);
1364
				}
1365
				$mergeList.append($li);
1366
			});
1367
			$('#merge_contacts_dialog').html($dlg).ocdialog({
1368
				modal: true,
1369
				closeOnEscape: true,
1370
				title:  t('contacts', 'Merge contacts'),
1371
				height: 'auto', width: 'auto',
1372
				buttons: [
1373
					{
1374
						text: t('contacts', 'Merge contacts'),
1375
						click:function() {
1376
							// Do the merging, use $(this) to get dialog
1377
							var contactid = $(this).find('input:radio:checked').val();
1378
							var others = [];
1379
							var deleteOther = $(this).find('#delete_other').prop('checked');
1380
							console.log('Selected contact', contactid, 'Delete others', deleteOther);
1381
							$.each($(this).find('input:radio:not(:checked)'), function(idx, item) {
1382
								others.push($(item).val());
1383
							});
1384
							console.log('others', others);
1385
							$(document).trigger('request.contact.merge', {
1386
								merger: contactid,
1387
								mergees: others,
1388
								deleteOther: deleteOther
1389
							});
1390
1391
							$(this).ocdialog('close');
1392
						},
1393
						defaultButton: true
1394
					},
1395
					{
1396
						text: t('contacts', 'Cancel'),
1397
						click:function() {
1398
							$(this).ocdialog('close');
1399
							return false;
1400
						}
1401
					}
1402
				],
1403
				close: function(/*event, ui*/) {
1404
					$(this).ocdialog('destroy').remove();
1405
					$('#merge_contacts_dialog').remove();
1406
				},
1407
				open: function(/*event, ui*/) {
1408
					$dlg.find('input').focus();
1409
				}
1410
			});
1411
		},
1412
		addGroup: function(cb) {
1413
			var self = this;
1414
			this.$rightContent.append('<div id="add_group_dialog"></div>');
1415
			if(!this.$addGroupTmpl) {
1416
				this.$addGroupTmpl = $('#addGroupTemplate');
1417
			}
1418
			this.$contactList.addClass('dim');
1419
			var $dlg = this.$addGroupTmpl.octemplate();
1420
			$('#add_group_dialog').html($dlg).ocdialog({
1421
				modal: true,
1422
				closeOnEscape: true,
1423
				title:  t('contacts', 'Add group'),
1424
				height: 'auto', width: 'auto',
1425
				buttons: [
1426
					{
1427
						text: t('contacts', 'OK'),
1428
						click:function() {
1429
							var name = $(this).find('input').val();
1430
							if(name.trim() === '') {
1431
								return false;
1432
							}
1433
							self.groups.addGroup(
1434
								{name:$dlg.find('input:text').val()},
1435
								function(response) {
1436
									if(typeof cb === 'function') {
1437
										cb(response);
1438
									} else {
1439
										if(response.error) {
1440
											$(document).trigger('status.contacts.error', response);
1441
										}
1442
									}
1443
								});
1444
							$(this).ocdialog('close');
1445
						},
1446
						defaultButton: true
1447
					},
1448
					{
1449
						text: t('contacts', 'Cancel'),
1450
						click:function() {
1451
							$(this).ocdialog('close');
1452
							return false;
1453
						}
1454
					}
1455
				],
1456
				close: function(/*event, ui*/) {
1457
					$(this).ocdialog('destroy').remove();
1458
					$('#add_group_dialog').remove();
1459
					self.$contactList.removeClass('dim');
1460
				},
1461
				open: function(/*event, ui*/) {
1462
					$dlg.find('input').focus();
1463
				}
1464
			});
1465
		},
1466
		setAllChecked: function(checked) {
1467
			var selector = checked ? 'input:checkbox:visible:not(checked)' : 'input:checkbox:visible:checked';
1468
			$.each(this.$contactList.find(selector), function() {
1469
				$(this).prop('checked', checked);
1470
			});
1471
			this.lastSelectedContacts = [];
1472
		},
1473
		jumpToContact: function(id) {
1474
			this.$rightContent.scrollTop(this.contacts.contactPos(id));
1475
		},
1476
		closeContact: function(id) {
1477
			$(window).unbind('hashchange', this.hashChange);
1478
			if(this.currentid === 'new') {
1479
				this.tmpcontact.slideUp().remove();
1480
				this.$contactList.show();
1481
			} else {
1482
				var contact = this.contacts.findById(id);
1483
				if(contact) {
1484
					// Only show the list element if contact is in current group
1485
					var showListElement = contact.inGroup(this.groups.nameById(this.currentgroup))
1486
						|| ['all', 'fav', 'uncategorized'].indexOf(this.currentgroup) !== -1
1487
						|| (this.currentgroup === 'uncategorized' && contact.groups().length === 0);
1488
					contact.close(showListElement);
1489
				}
1490
			}
1491
			delete this.currentid;
1492
			this.hideActions();
1493
			this.$groups.find('optgroup,option:not([value="-1"])').remove();
1494
			if(this.contacts.length === 0) {
1495
				$(document).trigger('status.nomorecontacts');
1496
			}
1497
			window.location.hash = '';
1498
			$(window).bind('hashchange', this.hashChange);
1499
		},
1500
		openContact: function(id) {
1501
			var self = this, contact;
1502
			if(typeof id === 'undefined' || id === 'undefined') {
1503
				console.warn('id is undefined!');
1504
				console.trace();
1505
			}
1506
			console.log('Contacts.openContact', id, typeof id);
1507
			if(this.currentid && this.currentid !== id) {
1508
				this.closeContact(this.currentid);
1509
			}
1510
			contact = this.contacts.findById(id);
1511
			if (!contact) {
1512
				console.warn('Contact', id, 'not found. Possibly deleted');
1513
				return;
1514
			}
1515
			this.currentid = id;
1516
			this.hideActions();
1517
			// If opened from search we can't be sure the contact is in currentgroup
1518
			if(!contact.inGroup(this.groups.nameById(this.currentgroup))
1519
				&& ['all', 'fav', 'uncategorized'].indexOf(this.currentgroup) === -1
1520
			) {
1521
				this.groups.selectGroup({id:'all'});
1522
			}
1523
			$(window).unbind('hashchange', this.hashChange);
1524
			console.assert(typeof this.currentid === 'string', 'Current ID not string:' + this.currentid);
1525
			// Properties that the contact doesn't know
1526
			var groupprops = {
1527
				favorite: this.groups.isFavorite(this.currentid),
1528
				groups: this.groups.categories,
1529
				currentgroup: {id:this.currentgroup, name:this.groups.nameById(this.currentgroup)}
1530
			};
1531
			if(!contact) {
1532
				console.warn('Error opening', this.currentid);
1533
				$(document).trigger('status.contacts.error', {
1534
					message: t('contacts', 'Could not find contact: {id}', {id:this.currentid})
1535
				});
1536
				this.currentid = null;
1537
				return;
1538
			}
1539
			var $contactelem = contact.renderContact(groupprops);
1540
			var $listElement = contact.getListItemElement();
1541
			console.log('selected element', $listElement);
1542
			window.location.hash = this.currentid;
1543
			$contactelem.insertAfter($listElement).show().find('.fullname').focus();
1544
			// Remove once IE8 is finally obsoleted in oC.
1545
			if (!OC.Util.hasSVGSupport()) {
1546
				OC.Util.replaceSVG($contactelem);
1547
			}
1548
			self.jumpToContact(self.currentid);
1549
			$listElement.hide();
1550
			setTimeout(function() {
1551
				$(window).bind('hashchange', self.hashChange);
1552
			}, 500);
1553
		},
1554
		update: function() {
1555
			console.log('update');
1556
		},
1557
		cloudPhotoSelected:function(metadata, path) {
1558
			var self = this;
1559
			console.log('cloudPhotoSelected', metadata);
1560
			var url = OC.generateUrl(
1561
				'apps/contacts/addressbook/{backend}/{addressBookId}/contact/{contactId}/photo/cacheFS',
1562
				{backend: metadata.backend, addressBookId: metadata.addressBookId, contactId: metadata.contactId}
1563
			);
1564
			var jqXHR = $.getJSON(url, {path: path}, function(response) {
1565
				console.log('response', response);
1566
				response = self.storage.formatResponse(jqXHR);
1567
				if(!response.error) {
1568
					self.editPhoto(metadata, response.data.tmp);
1569
				} else {
1570
					$(document).trigger('status.contacts.error', response);
1571
				}
1572
			}).fail(function(response) {
1573
				response = self.storage.formatResponse(jqXHR);
1574
				console.warn('response', response);
1575
				$(document).trigger('status.contacts.error', response);
1576
			});
1577
		},
1578
		editCurrentPhoto:function(metadata) {
1579
			var self = this;
1580
			var url = OC.generateUrl(
1581
				'apps/contacts/addressbook/{backend}/{addressBookId}/contact/{contactId}/photo/cacheCurrent',
1582
				{backend: metadata.backend, addressBookId: metadata.addressBookId, contactId: metadata.contactId}
1583
			);
1584
			console.log('url', url);
1585
			var jqXHR = $.getJSON(url, function(response) {
1586
				response = self.storage.formatResponse(jqXHR);
1587
				if(!response.error) {
1588
					self.editPhoto(metadata, response.data.tmp);
1589
				} else {
1590
					$(document).trigger('status.contacts.error', response);
1591
				}
1592
			}).fail(function(response) {
1593
				response = self.storage.formatResponse(jqXHR);
1594
				console.warn('response', response);
1595
				$(document).trigger('status.contacts.error', response);
1596
			});
1597
		},
1598
		editPhoto:function(metadata, tmpkey) {
1599
			console.log('editPhoto', metadata, tmpkey);
1600
			$('.tipsy').remove();
1601
			// Simple event handler, called from onChange and onSelect
1602
			// event handlers, as per the Jcrop invocation below
1603
			var showCoords = function(c) {
1604
				$('#x').val(c.x);
1605
				$('#y').val(c.y);
1606
				$('#w').val(c.w);
1607
				$('#h').val(c.h);
1608
			};
1609
1610
			var clearCoords = function() {
1611
				$('#coords input').val('');
1612
			};
1613
1614
			var self = this;
1615
			if(!this.$cropBoxTmpl) {
1616
				this.$cropBoxTmpl = $('#cropBoxTemplate');
1617
			}
1618
			var $container = $('<div />').appendTo($('#content'));
1619
			var $dlg = this.$cropBoxTmpl.octemplate().prependTo($container);
1620
1621
			$.when(this.storage.getTempContactPhoto(
1622
				metadata.backend,
1623
				metadata.addressBookId,
1624
				metadata.contactId,
1625
				tmpkey
1626
			))
1627
			.then(function(image) {
1628
				var x = 5, y = 5, w = Math.min(image.width, image.height), h = w;
1629
				//$dlg.css({'min-width': w, 'min-height': h});
1630
				console.log(x,y,w,h);
1631
				$(image).attr('id', 'cropbox').prependTo($dlg).show()
1632
				.Jcrop({
1633
					onChange:	showCoords,
1634
					onSelect:	showCoords,
1635
					onRelease:	clearCoords,
1636
					//maxSize:	[w, h],
1637
					bgColor:	'black',
1638
					bgOpacity:	.4,
0 ignored issues
show
Coding Style Comprehensibility introduced by
A leading decimal point can be confused with a dot: '.4'.
Loading history...
1639
					boxWidth:	400,
1640
					boxHeight:	400,
1641
					setSelect:	[ x, y, w-10, h-10 ],
1642
					aspectRatio: 1
1643
				});
1644
				$container.ocdialog({
1645
					modal: true,
1646
					closeOnEscape: true,
1647
					title:  t('contacts', 'Edit profile picture'),
1648
					height: image.height+100, width: image.width+20,
1649
					buttons: [
1650
						{
1651
							text: t('contacts', 'Crop photo'),
1652
							click:function() {
1653
								self.savePhoto($(this), metadata, tmpkey, function() {
1654
									$container.ocdialog('close');
1655
								});
1656
							},
1657
							defaultButton: true
1658
						}
1659
					],
1660
					close: function(/*event, ui*/) {
1661
						$(this).ocdialog('destroy').remove();
1662
						$container.remove();
1663
					},
1664
					open: function(/*event, ui*/) {
1665
						showCoords({x:x,y:y,w:w-10,h:h-10});
1666
					}
1667
				});
1668
			})
1669
			.fail(function() {
1670
				console.warn('Error getting temporary photo');
1671
			});
1672
		},
1673
		savePhoto:function($dlg, metaData, key, cb) {
1674
			var coords = {};
1675
			$.each($dlg.find('#coords').serializeArray(), function(idx, coord) {
1676
				coords[coord.name] = coord.value;
1677
			});
1678
1679
			$.when(this.storage.cropContactPhoto(
1680
				metaData.backend, metaData.addressBookId, metaData.contactId, key, coords
1681
			))
1682
			.then(function(response) {
1683
				$(document).trigger('status.contact.photoupdated', {
1684
					id: response.data.id,
1685
					thumbnail: response.data.thumbnail
1686
				});
1687
			})
1688
			.fail(function(response) {
1689
				console.log('response', response);
1690
				if(!response || !response.message) {
1691
					$(document).trigger('status.contacts.error', {
1692
						message:t('contacts', 'Network or server error. Please inform administrator.')
1693
					});
1694
				} else {
1695
					$(document).trigger('status.contacts.error', response);
1696
				}
1697
			})
1698
			.always(function() {
1699
				cb();
1700
			});
1701
		}
1702
	};
1703
})(window, jQuery, OC);
1704
1705
$(document).ready(function() {
1706
1707
	$.getScript(OC.generateUrl('apps/contacts/ajax/config.js'))
1708
	.done(function() {
1709
		OC.Contacts.init();
1710
	})
1711
	.fail(function(jqxhr, settings, exception) {
1712
		console.log('Failed loading settings.', jqxhr, settings, exception);
1713
	});
1714
1715
});
1716