/* -*- Mode: javascript; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ (function() { 'use strict'; /** * @name Card * @constructor * @param {object} futureCardData * @param {string} [partial] */ function Card(futureCardData, partial) { // Data is immediately available if (typeof futureCardData.then !== 'function') { this.init(futureCardData, partial); if (this.pid && !this.id) { // Prepare for the creation of a new card; // Get UID from the server. var newCardData = Card.$$resource.newguid(this.pid); this.$unwrap(newCardData); this.isNew = true; } } else { // The promise will be unwrapped first this.$unwrap(futureCardData); } } Card.$TEL_TYPES = ['work', 'home', 'cell', 'fax', 'pager']; Card.$EMAIL_TYPES = ['work', 'home', 'pref']; Card.$URL_TYPES = ['work', 'home', 'pref']; Card.$ADDRESS_TYPES = ['work', 'home']; /** * @memberof Card * @desc The factory we'll use to register with Angular. * @returns the Card constructor */ Card.$factory = ['$q', '$timeout', 'sgSettings', 'sgCard_STATUS', 'encodeUriFilter', 'linkyFilter', 'Resource', 'Preferences', function($q, $timeout, Settings, Card_STATUS, encodeUriFilter, linkyFilter, Resource, Preferences) { angular.extend(Card, { STATUS: Card_STATUS, encodeUri: encodeUriFilter, linky: linkyFilter, $$resource: new Resource(Settings.activeUser('folderURL') + 'Contacts', Settings.activeUser()), $q: $q, $timeout: $timeout, $Preferences: Preferences }); // Initialize categories from user's defaults if (Preferences.defaults.SOGoContactsCategories) { Card.$categories = Preferences.defaults.SOGoContactsCategories; } if (Preferences.defaults.SOGoAlternateAvatar) Card.$alternateAvatar = Preferences.defaults.SOGoAlternateAvatar; return Card; // return constructor }]; /** * @module SOGo.ContactsUI * @desc Factory registration of Card in Angular module. */ try { angular.module('SOGo.ContactsUI'); } catch(e) { angular.module('SOGo.ContactsUI', ['SOGo.Common', 'SOGo.PreferencesUI']); } angular.module('SOGo.ContactsUI') .constant('sgCard_STATUS', { NOT_LOADED: 0, DELAYED_LOADING: 1, LOADING: 2, LOADED: 3, DELAYED_MS: 300 }) .factory('Card', Card.$factory); /** * @memberof Card * @desc Fetch a card from a specific addressbook. * @param {string} addressbookId - the addressbook ID * @param {string} cardId - the card ID * @see {@link AddressBook.$getCard} */ Card.$find = function(addressbookId, cardId) { var futureCardData = this.$$resource.fetch([addressbookId, cardId].join('/'), 'view'); if (cardId) return new Card(futureCardData); // a single card return Card.$unwrapCollection(futureCardData); // a collection of cards }; /** * @function filterCategories * @memberof Card.prototype * @desc Search for categories matching some criterias * @param {string} search - the search string to match * @returns a collection of strings */ Card.filterCategories = function(query) { var re = new RegExp(query, 'i'); return _.map(_.filter(Card.$categories, function(category) { return category.search(re) != -1; }), function(category) { return { value: category }; }); }; /** * @memberof Card * @desc Unwrap to a collection of Card instances. * @param {object} futureCardData */ Card.$unwrapCollection = function(futureCardData) { var collection = {}; collection.$futureCardData = futureCardData; futureCardData.then(function(cards) { Card.$timeout(function() { angular.forEach(cards, function(data, index) { collection[data.id] = new Card(data); }); }); }); return collection; }; /** * @function init * @memberof Card.prototype * @desc Extend instance with required attributes and new data. * @param {object} data - attributes of card */ Card.prototype.init = function(data, partial) { var _this = this; if (angular.isUndefined(this.refs)) this.refs = []; if (angular.isUndefined(this.categories)) this.categories = []; this.c_screenname = null; angular.extend(this, data); if (!this.pid) this.pid = this.container; if (!this.$$fullname) this.$$fullname = this.$fullname(); if (!this.$$email) this.$$email = this.$preferredEmail(partial); if (!this.$$image) this.$$image = this.image; if (!this.$$image) this.$$image = Card.$Preferences.avatar(this.$$email, 40, {no_404: true}); if (this.hasphoto) this.photoURL = Card.$$resource.path(this.pid, this.id, 'photo'); if (this.isgroup) this.c_component = 'vlist'; this.$avatarIcon = this.$isList()? 'group' : 'person'; if (data.orgs && data.orgs.length) this.orgs = _.map(data.orgs, function(org) { return { 'value': org }; }); if (data.notes && data.notes.length) this.notes = _.map(data.notes, function(note) { return { 'value': note }; }); else if (!this.notes || !this.notes.length) this.notes = [ { value: '' } ]; // Lowercase the type of specific fields angular.forEach(['addresses', 'phones', 'urls'], function(key) { angular.forEach(_this[key], function(o) { if (o.type) o.type = o.type.toLowerCase(); }); }); // Instanciate Card objects for list members angular.forEach(this.refs, function(o, i) { if (o.email) o.emails = [{value: o.email}]; o.id = o.reference; _this.refs[i] = new Card(o); }); // Instanciate date object of birthday if (this.birthday && angular.isString(this.birthday)) { var dlp = Card.$Preferences.$mdDateLocaleProvider; this.birthday = this.birthday.parseDate(dlp, '%Y-%m-%d'); this.$birthday = dlp.formatDate(this.birthday); } this.$loaded = angular.isDefined(this.c_name)? Card.STATUS.LOADED : Card.STATUS.NOT_LOADED; // An empty attribute to trick md-autocomplete when adding attendees from the appointment editor this.empty = ' '; }; /** * @function $id * @memberof Card.prototype * @desc Return the card ID. * @returns the card ID */ Card.prototype.$id = function() { return this.$futureCardData.then(function(data) { return data.id; }); }; /** * @function $path * @memberof Card.prototype * @desc Return the relative URL of the card. * @returns the relative URL, properly encoded */ Card.prototype.$path = function() { return [this.pid, this.id]; }; /** * @function $isLoading * @memberof Card.prototype * @returns true if the Card definition is still being retrieved from server after a specific delay * @see sgCard_STATUS */ Card.prototype.$isLoading = function() { return this.$loaded == Card.STATUS.LOADING; }; /** * @function $reload * @memberof Card.prototype * @desc Fetch all available attributes of the contact. * @returns a promise of the HTTP operation */ Card.prototype.$reload = function() { var _this = this, futureCardData; if (this.$futureCardData) return this; futureCardData = Card.$$resource.fetch(this.$path(), 'view'); return this.$unwrap(futureCardData); }; /** * @function $members * @memberof Card.prototype * @desc Fetch members of the LDAP group. * @returns a promise that resolves with the members */ Card.prototype.$members = function() { var _this = this; if (this.members) return Card.$q.when(this.members); if (this.$isGroup({expandable: true})) { return Card.$$resource.fetch(this.$path(), 'members').then(function(data) { _this.members = _.map(data.members, function(member) { return new Card(member); }); return _this.members; }); } return Card.$q.reject("Card " + this.id + " is not an LDAP group"); }; /** * @function $save * @memberof Card.prototype * @desc Save the card to the server. */ Card.prototype.$save = function(options) { var _this = this, action = 'saveAsContact', data; if (this.c_component == 'vlist') { action = 'saveAsList'; _.forEach(this.refs, function(ref) { ref.reference = ref.id; }); } data = this.$omit(); if (options && options.ignoreDuplicate) { angular.extend(data, options); } return Card.$$resource.save([ Card.encodeUri(this.pid), Card.encodeUri(this.id) || '_new_' ].join('/'), data, { action: action }) .then(function(data) { // Format birthdate if (_this.birthday) _this.$birthday = Card.$Preferences.$mdDateLocaleProvider.formatDate(_this.birthday); // Make a copy of the data for an eventual reset _this.$shadowData = _this.$omit(true); return data; }); }; Card.prototype.$delete = function(attribute, index) { if (attribute) { if (index > -1 && this[attribute].length > index) { this[attribute].splice(index, 1); } else delete this[attribute]; } else { // No arguments -- delete card return Card.$$resource.remove(this.$path()); } }; /** * @function export * @memberof Card.prototype * @desc Download the current card * @returns a promise of the HTTP operation */ Card.prototype.export = function() { var data, options; data = { uids: [ this.id ] }; options = { type: 'application/octet-stream', filename: this.$$fullname + '.ldif' }; return Card.$$resource.download(this.pid, 'export', data, options); }; Card.prototype.$fullname = function(options) { var fn = Card.linky(this.c_cn) || '', html = options && options.html, email, names; if (fn.length === 0) { names = []; if (this.c_givenname && this.c_givenname.length > 0) names.push(Card.linky(this.c_givenname)); if (this.nickname && this.nickname.length > 0) names.push((html?'':'') + Card.linky(this.nickname) + (html?'':'')); if (this.c_sn && this.c_sn.length > 0) names.push(Card.linky(this.c_sn)); if (names.length > 0) fn = names.join(' '); else if (this.org && this.org.length > 0) { fn = Card.linky(this.org); } else if (this.emails && this.emails.length > 0) { email = _.find(this.emails, function(i) { return i.value !== ''; }); if (email) fn = Card.linky(email.value); } } if (this.contactinfo) fn += ' (' + Card.linky(this.contactinfo.split("\n").join("; ")) + ')'; return fn; }; Card.prototype.$description = function() { var description = []; if (this.title) description.push(this.title); if (this.role) description.push(this.role); if (this.org) description.push(this.org); if (this.orgs) description = _.concat(description, _.map(this.orgs, 'value')); if (this.description) description.push(this.description); return description.join(', '); }; /** * @function $preferredEmail * @memberof Card.prototype * @desc Get the preferred email address. * @param {string} [partial] - a partial string that the email must match * @returns the first email address of type "pref" or the first address if none found */ Card.prototype.$preferredEmail = function(partial) { var email, re; if (partial) { re = new RegExp(partial, 'i'); email = _.find(this.emails, function(o) { return re.test(o.value); }); } if (email) { email = email.value; } else { email = _.find(this.emails, function(o) { return o.type == 'pref'; }); if (email) { email = email.value; } else if (this.emails && this.emails.length) { email = this.emails[0].value; } else if (this.c_mail && this.c_mail.length) { email = this.c_mail[0]; } else { email = ''; } } return email; }; /** * @function $shortFormat * @memberof Card.prototype * @param {string} [partial] - a partial string that the email must match * @returns the fullname along with a matching email address in parentheses */ Card.prototype.$shortFormat = function(partial) { var fullname = [this.$$fullname], email = this.$preferredEmail(partial); if (email && email != this.$$fullname) fullname.push(' <' + email + '>'); return fullname.join(' '); }; Card.prototype.$isCard = function() { return this.c_component == 'vcard'; }; Card.prototype.$isList = function(options) { // isGroup attribute means it's a group of a LDAP source (not automatically expanded on the client-side) var condition = (!options || !options.expandable || options.expandable && !this.isgroup); return this.c_component == 'vlist' && condition; }; Card.prototype.$isGroup = function(options) { var condition = (!options || !options.expandable || options.expandable && Card.$Preferences.defaults.SOGoLDAPGroupExpansionEnabled); return this.isgroup && condition; }; Card.prototype.$addOrg = function(org) { if (angular.isUndefined(this.orgs)) { this.orgs = [org]; } else if (org != this.org && !_.includes(this.orgs, org)) { this.orgs.push(org); } return this.orgs.length - 1; }; // Card.prototype.$addCategory = function(category) { // if (category) { // if (angular.isUndefined(this.categories)) { // this.categories = [{value: category}]; // } // else { // for (var i = 0; i < this.categories.length; i++) { // if (this.categories[i].value == category) { // break; // } // } // if (i == this.categories.length) // this.categories.push({value: category}); // } // } // }; Card.prototype.$addEmail = function(type) { if (angular.isUndefined(this.emails)) { this.emails = [{type: type, value: ''}]; } else if (_.isUndefined(_.find(this.emails, function(i) { return i.value === ''; }))) { this.emails.push({type: type, value: ''}); } return this.emails.length - 1; }; Card.prototype.$addScreenName = function(screenName) { this.c_screenname = screenName; }; Card.prototype.$addPhone = function(type) { if (angular.isUndefined(this.phones)) { this.phones = [{type: type, value: ''}]; } else if (_.isUndefined(_.find(this.phones, function(i) { return i.value === ''; }))) { this.phones.push({type: type, value: ''}); } return this.phones.length - 1; }; Card.prototype.$addUrl = function(type, url) { if (angular.isUndefined(this.urls)) { this.urls = [{type: type, value: url}]; } else if (_.isUndefined(_.find(this.urls, function(i) { return i.value == url; }))) { this.urls.push({type: type, value: url}); } return this.urls.length - 1; }; Card.prototype.$addAddress = function(type, postoffice, street, street2, locality, region, country, postalcode) { if (angular.isUndefined(this.addresses)) { this.addresses = [{type: type, postoffice: postoffice, street: street, street2: street2, locality: locality, region: region, country: country, postalcode: postalcode}]; } else if (!_.find(this.addresses, function(i) { return i.street == street && i.street2 == street2 && i.locality == locality && i.country == country && i.postalcode == postalcode; })) { this.addresses.push({type: type, postoffice: postoffice, street: street, street2: street2, locality: locality, region: region, country: country, postalcode: postalcode}); } return this.addresses.length - 1; }; Card.prototype.$addMember = function(email) { var card = new Card({email: email, emails: [{value: email}]}), i; if (angular.isUndefined(this.refs)) { this.refs = [card]; } else if (email.length === 0) { this.refs.push(card); } else { for (i = 0; i < this.refs.length; i++) { if (this.refs[i].email == email) { break; } } if (i == this.refs.length) this.refs.push(card); } return this.refs.length - 1; }; /** * @function $certificate * @memberof Account.prototype * @desc View the S/MIME certificate details associated to the account. * @returns a promise of the HTTP operation */ Card.prototype.$certificate = function() { var _this = this; if (this.hasCertificate) { if (this.$$certificate) return Card.$q.when(this.$$certificate); else { return Card.$$resource.fetch(this.$path(), 'certificate').then(function(data) { _this.$$certificate = data; return data; }); } } else { return Card.$q.reject(); } }; /** * @function $removeCertificate * @memberof Account.prototype * @desc Remove any S/MIME certificate associated with the account. * @returns a promise of the HTTP operation */ Card.prototype.$removeCertificate = function() { var _this = this; return Card.$$resource.fetch(this.$path(), 'removeCertificate').then(function() { _this.hasCertificate = false; }); }; /** * @function explode * @memberof Card.prototype * @desc Create a new Card associated to each email address of this card. * @return an array of Card instances */ Card.prototype.explode = function() { var _this = this, cards = [], data; if (this.emails) { if (this.emails.length > 1) { data = this.$omit(); _.forEach(this.emails, function(email) { var card = new Card(angular.extend({}, data, {emails: [email]})); cards.push(card); }); return cards; } else return [this]; } return []; }; /** * @function $reset * @memberof Card.prototype * @desc Reset the original state the card's data. */ Card.prototype.$reset = function() { var _this = this; angular.forEach(this, function(value, key) { if (key != 'constructor' && key[0] != '$') { delete _this[key]; } }); this.init(this.$shadowData); this.$shadowData = this.$omit(true); }; /** * @function $updateMember * @memberof Card.prototype * @desc Update an existing list member from a Card instance. * A list member has the following attribtues: * - email * - reference * - fn * @param {number} index * @param {string} email * @param {Card} card */ // Card.prototype.$updateMember = function(index, email, card) { // var ref = { // email: email, // emails: [{value: email}], // reference: card.c_name, // c_cn: card.$fullname() // }; // this.refs[index] = new Card(ref); // }; /** * @function $unwrap * @memberof Card.prototype * @desc Unwrap a promise and make a copy of the resolved data. * @param {object} futureCardData - a promise of the Card's data */ Card.prototype.$unwrap = function(futureCardData) { var _this = this; // Card is not loaded yet this.$loaded = Card.STATUS.DELAYED_LOADING; Card.$timeout(function() { if (_this.$loaded != Card.STATUS.LOADED) _this.$loaded = Card.STATUS.LOADING; }, Card.STATUS.DELAYED_MS); // Expose the promise this.$futureCardData = futureCardData.then(function(data) { _this.init(data); // Mark card as loaded _this.$loaded = Card.STATUS.LOADED; // Make a copy of the data for an eventual reset _this.$shadowData = _this.$omit(true); return _this; }); return this.$futureCardData; }; /** * @function $omit * @memberof Card.prototype * @desc Return a sanitized object used to send to the server. * @param {boolean} [deep] - make a deep copy if true * @return an object literal copy of the Card instance */ Card.prototype.$omit = function(deep) { var card = {}; angular.forEach(this, function(value, key) { if (key == 'refs') { card.refs = _.map(value, function(o) { return o.$omit(deep); }); } else if (key != 'constructor' && key[0] != '$') { if (deep) card[key] = angular.copy(value); else card[key] = value; } }); // We convert back our birthday object if (!deep) { if (card.birthday) card.birthday = card.birthday.format(Card.$Preferences.$mdDateLocaleProvider, '%Y-%m-%d'); else card.birthday = ''; } // We flatten the organizations to an array of strings if (this.orgs) card.orgs = _.map(this.orgs, 'value'); // We flatten the notes to an array of strings if (this.notes) card.notes = _.map(this.notes, 'value'); return card; }; Card.prototype.toString = function() { var desc = this.id + ' ' + this.$$fullname; if (this.$$email) desc += ' <' + this.$$email + '>'; return '[' + desc + ']'; }; })();