/* * * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. * */ /* global mozContact */ // Cordova contact definition: // http://cordova.apache.org/docs/en/2.5.0/cordova_contacts_contacts.md.html#Contact // FxOS contact definition: // https://developer.mozilla.org/en-US/docs/Web/API/mozContact var Contact = require('./Contact'); var ContactField = require('./ContactField'); var ContactName = require('./ContactName'); // XXX: a hack to check if id is "empty". Cordova inserts a // string "this string is supposed to be a unique identifier that will // never show up on a device" if id is empty function _hasId(id) { if (!id || id.indexOf(' ') >= 0) { return false; } return true; } // Extend mozContact prototype to provide update from Cordova function updateFromCordova(contact, fromContact) { function exportContactFieldArray(contactFieldArray, key) { if (!key) { key = 'value'; } var arr = []; for (var i=0; i < contactFieldArray.length; i++) { arr.push(contactFieldArray[i][key]); } return arr; } function exportAddress(addresses) { // TODO: check moz address format var arr = []; for (var i=0; i < addresses.length; i++) { var addr = {}; for (var key in addresses[i]) { if (key == 'formatted' || key == 'id') { continue; } else if (key == 'type') { addr[key] = [addresses[i][key]]; } else if (key == 'country') { addr.countryName = addresses[i][key]; } else { addr[key] = addresses[i][key]; } } arr.push(addr); } return arr; } function exportContactField(data) { var contactFields = []; for (var i=0; i < data.length; i++) { var item = data[i]; if (item.value) { var itemData = {value: item.value}; if (item.type) { itemData.type = [item.type]; } if (item.pref) { itemData.pref = item.pref; } contactFields.push(itemData); } } return contactFields; } // adding simple fields [contactField, eventualMozContactField] var nameFields = [['givenName'], ['familyName'], ['honorificPrefix'], ['honorificSuffix'], ['middleName', 'additionalName']]; var baseArrayFields = [['displayName', 'name'], ['nickname']]; var baseStringFields = []; var j = 0, field; while(field = nameFields[j++]) { // jshint ignore:line if (fromContact.name[field[0]]) { contact[field[1] || field[0]] = fromContact.name[field[0]].split(' '); } } j = 0; while(field = baseArrayFields[j++]) { // jshint ignore:line if (fromContact[field[0]]) { contact[field[1] || field[0]] = fromContact[field[0]].split(' '); } } j = 0; while(field = baseStringFields[j++]) { // jshint ignore:line if (fromContact[field[0]]) { contact[field[1] || field[0]] = fromContact[field[0]]; } } if (fromContact.birthday) { contact.bday = new Date(fromContact.birthday); } if (fromContact.emails) { var emails = exportContactField(fromContact.emails); contact.email = emails; } if (fromContact.categories) { contact.category = exportContactFieldArray(fromContact.categories); } if (fromContact.addresses) { contact.adr = exportAddress(fromContact.addresses); } if (fromContact.phoneNumbers) { contact.tel = exportContactField(fromContact.phoneNumbers); } if (fromContact.organizations) { // XXX: organizations are saved in 2 arrays - org and jobTitle // depending on the usecase it might generate issues // where wrong title will be added to an organization contact.org = exportContactFieldArray(fromContact.organizations, 'name'); contact.jobTitle = exportContactFieldArray(fromContact.organizations, 'title'); } if (fromContact.note) { contact.note = [fromContact.note]; } } // Extend Cordova Contact prototype to provide update from FFOS contact Contact.prototype.updateFromMozilla = function(moz) { function exportContactField(data) { var contactFields = []; for (var i=0; i < data.length; i++) { var item = data[i]; var itemData = new ContactField(item.type, item.value, item.pref); contactFields.push(itemData); } return contactFields; } function makeContactFieldFromArray(data) { var contactFields = []; for (var i=0; i < data.length; i++) { var itemData = new ContactField(null, data[i]); contactFields.push(itemData); } return contactFields; } function exportAddresses(addresses) { // TODO: check moz address format var arr = []; for (var i=0; i < addresses.length; i++) { var addr = {}; for (var key in addresses[i]) { if (key == 'countryName') { addr.country = addresses[i][key]; } else if (key == 'type') { addr[key] = addresses[i][key].join(' '); } else { addr[key] = addresses[i][key]; } } arr.push(addr); } return arr; } function createOrganizations(orgs, jobs) { orgs = (orgs) ? orgs : []; jobs = (jobs) ? jobs : []; var max_length = Math.max(orgs.length, jobs.length); var organizations = []; for (var i=0; i < max_length; i++) { organizations.push(new ContactOrganization( null, null, orgs[i] || null, null, jobs[i] || null)); } return organizations; } function createFormatted(name) { var fields = ['honorificPrefix', 'givenName', 'middleName', 'familyName', 'honorificSuffix']; var f = ''; for (var i = 0; i < fields.length; i++) { if (name[fields[i]]) { if (f) { f += ' '; } f += name[fields[i]]; } } return f; } if (moz.id) { this.id = moz.id; } var nameFields = [['givenName'], ['familyName'], ['honorificPrefix'], ['honorificSuffix'], ['additionalName', 'middleName']]; var baseArrayFields = [['name', 'displayName'], 'nickname', ['note']]; var baseStringFields = []; var name = new ContactName(); var j = 0, field; while(field = nameFields[j++]) { // jshint ignore:line if (moz[field[0]]) { name[field[1] || field[0]] = moz[field[0]].join(' '); } } this.name = name; j = 0; while(field = baseArrayFields[j++]) { // jshint ignore:line if (moz[field[0]]) { this[field[1] || field[0]] = moz[field[0]].join(' '); } } j = 0; while(field = baseStringFields[j++]) { // jshint ignore:line if (moz[field[0]]) { this[field[1] || field[0]] = moz[field[0]]; } } // emails if (moz.email) { this.emails = exportContactField(moz.email); } // categories if (moz.category) { this.categories = makeContactFieldFromArray(moz.category); } // addresses if (moz.adr) { this.addresses = exportAddresses(moz.adr); } // phoneNumbers if (moz.tel) { this.phoneNumbers = exportContactField(moz.tel); } // birthday if (moz.bday) { this.birthday = Date.parse(moz.bday); } // organizations if (moz.org || moz.jobTitle) { // XXX: organizations array is created from org and jobTitle this.organizations = createOrganizations(moz.org, moz.jobTitle); } // construct a read-only formatted value this.name.formatted = createFormatted(this.name); /* Find out how to translate these parameters // photo: Blob // url: Array with metadata (?) // impp: exportIM(contact.ims), TODO: find the moz impp definition // anniversary // sex // genderIdentity // key */ }; function createMozillaFromCordova(successCB, errorCB, contact) { var moz; // get contact if exists if (_hasId(contact.id)) { var search = navigator.mozContacts.find({ filterBy: ['id'], filterValue: contact.id, filterOp: 'equals'}); search.onsuccess = function() { moz = search.result[0]; updateFromCordova(moz, contact); successCB(moz); }; search.onerror = errorCB; return; } // create empty contact moz = new mozContact(); // if ('init' in moz) { // 1.2 and below compatibility // moz.init(); // } updateFromCordova(moz, contact); successCB(moz); } function createCordovaFromMozilla(moz) { var contact = new Contact(); contact.updateFromMozilla(moz); return contact; } // However API requires the ability to save multiple contacts, it is // used to save only one element array function saveContacts(successCB, errorCB, contacts) { // a closure which is holding the right moz contact function makeSaveSuccessCB(moz) { return function(result) { // create contact from FXOS contact (might be different than // the original one due to differences in API) var contact = createCordovaFromMozilla(moz); // call callback successCB(contact); }; } var i=0; var contact; /*jshint -W083 */ while(contact = contacts[i++]) { // jshint ignore:line createMozillaFromCordova(function(moz) { var request = navigator.mozContacts.save(moz); // success and/or fail will be called every time a contact is saved request.onsuccess = makeSaveSuccessCB(moz); request.onerror = errorCB; }, function() {}, contact); } /*jshint +W083 */ } // API provides a list of ids to be removed function remove(successCB, errorCB, ids) { var i; /*jshint -W083 */ for (i = 0; i < ids.length; i++){ // throw an error if no id provided if (!_hasId(ids[i])) { console.error('FFOS: Attempt to remove unsaved contact'); errorCB(0); return; } // check if provided id actually exists var search = navigator.mozContacts.find({ filterBy: ['id'], filterValue: ids[i], filterOp: 'equals'}); search.onsuccess = function() { if (search.result.length === 0) { console.error('FFOS: Attempt to remove a non existing contact'); errorCB(0); return; } var moz = search.result[0]; var request = navigator.mozContacts.remove(moz); request.onsuccess = successCB; request.onerror = errorCB; }; search.onerror = errorCB; } /*jshint +W083 */ } var mozContactSearchFields = [['name', 'displayName'], ['givenName'], ['familyName'], ['email'], ['tel'], ['jobTitle'], ['note'], ['tel', 'phoneNumbers'], ['email', 'emails']]; // Searching by nickname and additionalName is forbidden in 1.3 and below // Searching by name is forbidden in 1.2 and below // finds if a key is allowed and returns FFOS name if different function getMozSearchField(key) { if (mozContactSearchFields.indexOf([key]) >= 0) { return key; } for (var i=0; i < mozContactSearchFields.length; i++) { if (mozContactSearchFields[i].length > 1) { if (mozContactSearchFields[i][1] === key) { return mozContactSearchFields[i][0]; } } } return false; } function _getAll(successCB, errorCB, params) { // [contactField, eventualMozContactField] var getall = navigator.mozContacts.getAll({}); var contacts = []; getall.onsuccess = function() { if (getall.result) { contacts.push(createCordovaFromMozilla(getall.result)); getall.continue(); } else { successCB(contacts); } }; getall.onerror = errorCB; } function search(successCB, errorCB, params) { var options = params[1] || {}; if (!options.filter) { return _getAll(successCB, errorCB, params); } var filterBy = []; // filter and translate fields for (var i=0; i < params[0].length; i++) { var searchField = params[0][i]; var mozField = getMozSearchField(searchField); if (searchField === 'name') { // Cordova uses name for search by all name fields. filterBy.push('givenName'); filterBy.push('familyName'); continue; } if (searchField === 'displayName' && 'init' in new mozContact()) { // ``init`` in ``mozContact`` indicates FFOS version 1.2 or below // Searching by name (in moz) is then forbidden console.log('FFOS ContactProxy: Unable to search by displayName on FFOS 1.2'); continue; } if (mozField) { filterBy.push(mozField); } else { console.log('FXOS ContactProxy: inallowed field passed to search filtered out: ' + searchField); } } var mozOptions = {filterBy: filterBy, filterOp: 'startsWith'}; if (!options.multiple) { mozOptions.filterLimit = 1; } mozOptions.filterValue = options.filter; var request = navigator.mozContacts.find(mozOptions); request.onsuccess = function() { var contacts = []; var mozContacts = request.result; for (var i=0; i < mozContacts.length; i++) { contacts.push(createCordovaFromMozilla(mozContacts[i])); } successCB(contacts); }; request.onerror = errorCB; } module.exports = { save: saveContacts, remove: remove, search: search }; require("cordova/exec/proxy").add("Contacts", module.exports);