зеркало из https://github.com/mozilla/pluotsorbet.git
339 строки
11 KiB
JavaScript
339 строки
11 KiB
JavaScript
/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
|
|
/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
|
|
|
|
'use strict';
|
|
|
|
/**
|
|
* ContactToVcard provides the functionality necessary to export from
|
|
* MozContacts to vCard 3.0 (https://www.ietf.org/rfc/rfc2426.txt). The reason
|
|
* to choose the 3.0 standard instead of the 4.0 one is that some systems
|
|
* most notoriously Android 4.x don't seem to be able to import vCard 4.0.
|
|
*/
|
|
var contact2vcard = (function() {
|
|
/** Mapping between contact fields and equivalent vCard fields */
|
|
var VCARD_MAP = {
|
|
'fax' : 'FAX',
|
|
'faxoffice' : 'FAX,WORK',
|
|
'faxhome' : 'FAX,HOME',
|
|
'faxother' : 'FAX',
|
|
'home' : 'HOME',
|
|
'mobile' : 'CELL',
|
|
'pager' : 'PAGER',
|
|
'personal' : 'HOME',
|
|
'pref' : 'PREF',
|
|
'text' : 'TEXT',
|
|
'textphone' : 'TEXTPHONE',
|
|
'voice' : 'VOICE',
|
|
'work' : 'WORK'
|
|
};
|
|
|
|
var CRLF = '\r\n';
|
|
|
|
/** Field list to be skipped when converting to vCard */
|
|
var VCARD_SKIP_FIELD = ['fb_profile_photo'];
|
|
var VCARD_VERSION = '3.0';
|
|
var HEADER = 'BEGIN:VCARD' + CRLF + 'VERSION:' + VCARD_VERSION + CRLF;
|
|
var FOOTER = 'END:VCARD' + CRLF;
|
|
|
|
function blobToBase64(blob, cb) {
|
|
var reader = new FileReader();
|
|
|
|
reader.onload = function() {
|
|
var dataUrl = reader.result;
|
|
var base64 = dataUrl.split(',')[1];
|
|
cb(base64);
|
|
};
|
|
|
|
reader.readAsDataURL(blob);
|
|
}
|
|
|
|
function ISODateString(d) {
|
|
if (typeof d === 'string') {
|
|
d = new Date(d);
|
|
}
|
|
|
|
var str = d.toISOString();
|
|
|
|
// Remove the milliseconds field
|
|
return str.slice(0, str.indexOf('.')) + 'Z';
|
|
}
|
|
|
|
/**
|
|
* Given an array with contact fields (usually containing only one field),
|
|
* returns the equivalent vcard field
|
|
*
|
|
* @param {Array} sourceField source field from a MozContact
|
|
* @param {String} vcardField vCard field name
|
|
* @return {Array} Array of vCard string entries
|
|
*/
|
|
function fromContactField(sourceField, vcardField) {
|
|
if (!sourceField || !sourceField.length) {
|
|
return [];
|
|
}
|
|
|
|
// Goes to the entries in the given field (usually only one but potentially
|
|
// more) and transforms them into string-based, vCard ones.
|
|
return sourceField.map(function(field) {
|
|
var str = vcardField;
|
|
/**
|
|
* If the field doesn't have an equivalent in vcard standard.
|
|
* Incompatible fields are stored in `VCARD_SKIP_FIELD`.
|
|
*
|
|
* @type {boolean}
|
|
*/
|
|
var skipField = false;
|
|
var types = [];
|
|
|
|
// Checks existing types and converts them to vcard types if necessary
|
|
// and fill `types` array with the final types.
|
|
if (Array.isArray(field.type)) {
|
|
var fieldType = field.type.map(function(aType) {
|
|
var out = '';
|
|
if (aType) {
|
|
aType = aType.trim().toLowerCase();
|
|
if (VCARD_SKIP_FIELD.indexOf(aType) !== -1) {
|
|
skipField = true;
|
|
}
|
|
out = VCARD_MAP[aType] || aType;
|
|
}
|
|
return out;
|
|
});
|
|
|
|
types = types.concat(fieldType);
|
|
}
|
|
|
|
if (skipField) {
|
|
return;
|
|
}
|
|
|
|
if (field.pref && field.pref === true) {
|
|
types.push('PREF');
|
|
}
|
|
|
|
if (types.length) {
|
|
str += ';TYPE=' + types.join(',');
|
|
}
|
|
|
|
return str + ':' + (field.value || '');
|
|
});
|
|
}
|
|
|
|
function fromStringArray(sourceField, vcardField) {
|
|
if (!sourceField) {
|
|
return '';
|
|
}
|
|
|
|
return vcardField + ':' + sourceField.join(',');
|
|
}
|
|
|
|
function joinFields(fields) {
|
|
return fields.filter(function(f) { return !!f; }).join(CRLF);
|
|
}
|
|
|
|
function toBlob(vcard) {
|
|
return new Blob([vcard], {'type': 'text/vcard'});
|
|
}
|
|
|
|
/**
|
|
* Convenience function that converts an array of contacts into a text/vcard
|
|
* blob. The blob is passed to the callback once the conversion is done.
|
|
*
|
|
* @param {Array} contacts An array of mozContact objects.
|
|
* @param {Function} callback A function invoked with the generated blob.
|
|
*/
|
|
function ContactToVcardBlob(contacts, callback) {
|
|
if (typeof callback !== 'function') {
|
|
throw Error('callback() is undefined or not a function');
|
|
}
|
|
|
|
var str = '';
|
|
|
|
ContactToVcard(contacts, function append(vcards, nCards) {
|
|
str += vcards;
|
|
}, function success() {
|
|
callback(str ? toBlob(str) : null);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Converts an array of contacts to a string of vCards. The conversion is
|
|
* done in batches. For every batch the append callback is invoked with a
|
|
* string of vCards and the number of contacts in the batch. Once all
|
|
* contacts have been processed the success callback is invoked.
|
|
*
|
|
* @param {Array} contacts An array of mozContact objects.
|
|
* @param {Function} append A function taking two parameters, the first one
|
|
* will be passed a string of vCards and the second an integer
|
|
* representing the number of contacts in the string.
|
|
* @param {Function} success A function with no parameters that will be
|
|
* invoked once all the contacts have been processed.
|
|
* @param {Number} batchSize An optional parameter specifying the maximum
|
|
* number of characters that should be added to the output string
|
|
* before invoking the append callback. If this parameter is not
|
|
* provided a default value of 1MiB will be used instead.
|
|
*/
|
|
function ContactToVcard(contacts, append, success, batchSize, skipPhoto) {
|
|
var vCardsString = '';
|
|
var nextIndex = 0;
|
|
var cardsInBatch = 0;
|
|
|
|
batchSize = batchSize || (1024 * 1024);
|
|
|
|
if (typeof append !== 'function') {
|
|
throw Error('append() is undefined or not a function');
|
|
}
|
|
|
|
if (typeof success !== 'function') {
|
|
throw Error('append() is undefined or not a function');
|
|
}
|
|
|
|
/**
|
|
* Append the vCard obtained by converting the contact to the string of
|
|
* vCards and if necessary pass the string to the user-specified callback
|
|
* function. If we're not done processing all the contacts start processing
|
|
* the following one.
|
|
*
|
|
* @param {String} vcard The string obtained from the previously processed
|
|
* contact.
|
|
*/
|
|
function appendVCard(vcard) {
|
|
if (vcard.length > 0) {
|
|
vCardsString += HEADER + vcard + CRLF + FOOTER;
|
|
}
|
|
|
|
nextIndex++;
|
|
cardsInBatch++;
|
|
|
|
/* Invoke the user-provided callback if we've filled the current batch or
|
|
* if we don't have more contacts to process. */
|
|
if ((vCardsString.length > batchSize) ||
|
|
(nextIndex === contacts.length)) {
|
|
append(vCardsString, cardsInBatch);
|
|
cardsInBatch = 0;
|
|
vCardsString = '';
|
|
}
|
|
|
|
if (nextIndex < contacts.length) {
|
|
processContact(contacts[nextIndex]);
|
|
} else {
|
|
success();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Process a contact and invokes appendVCard with the resulting vCard
|
|
* string.
|
|
*
|
|
* @param {Object} contacts A mozContact object.
|
|
*/
|
|
function processContact(ct) {
|
|
if (navigator.mozContact && !(ct instanceof navigator.mozContact)) {
|
|
console.error('An instance of mozContact was expected');
|
|
setZeroTimeout(function() { appendVCard(''); });
|
|
return;
|
|
}
|
|
|
|
/*
|
|
* N TYPE
|
|
* The structured type value corresponds, in
|
|
* sequence, to the Family Name, Given Name, Additional Names, Honorific
|
|
* Prefixes, and Honorific Suffixes. The text components are separated
|
|
* by the SEMI-COLON character (ASCII decimal 59). Individual text
|
|
* components can include multiple text values (e.g., multiple
|
|
* Additional Names) separated by the COMMA character (ASCII decimal
|
|
* 44). This type is based on the semantics of the X.520 individual name
|
|
* attributes. The property MUST be present in the vCard object.
|
|
**/
|
|
var n = 'n:' + ([
|
|
ct.familyName,
|
|
ct.givenName,
|
|
ct.additionalName,
|
|
ct.honorificPrefix,
|
|
ct.honorificSuffix
|
|
].map(function(f) {
|
|
f = f || [''];
|
|
return f.join(',') + ';';
|
|
}).join(''));
|
|
|
|
// vCard standard does not accept contacts without 'n' or 'fn' fields.
|
|
if (n === 'n:;;;;;' || !ct.name) {
|
|
setZeroTimeout(function() { appendVCard(''); });
|
|
return;
|
|
}
|
|
|
|
var allFields = [
|
|
n,
|
|
fromStringArray(ct.name, 'FN'),
|
|
fromStringArray(ct.nickname, 'NICKNAME'),
|
|
fromStringArray(ct.category, 'CATEGORY'),
|
|
fromStringArray(ct.org, 'ORG'),
|
|
fromStringArray(ct.jobTitle, 'TITLE'),
|
|
fromStringArray(ct.note, 'NOTE'),
|
|
fromStringArray(ct.key, 'KEY')
|
|
];
|
|
|
|
if (ct.bday) {
|
|
allFields.push('BDAY:' + ISODateString(ct.bday));
|
|
}
|
|
if (ct.anniversary) {
|
|
allFields.push('ANNIVERSARY:' + ISODateString(ct.anniversary));
|
|
}
|
|
|
|
allFields.push('UID:' + ct.id.toString().substr(0,30));
|
|
|
|
allFields.push.apply(allFields, fromContactField(ct.email, 'EMAIL'));
|
|
allFields.push.apply(allFields, fromContactField(ct.url, 'URL'));
|
|
allFields.push.apply(allFields, fromContactField(ct.tel, 'TEL'));
|
|
|
|
var adrs = fromContactField(ct.adr, 'ADR');
|
|
allFields.push.apply(allFields, adrs.map(function(adrStr, i) {
|
|
var orig = ct.adr[i];
|
|
return adrStr + ([
|
|
'',
|
|
'',
|
|
orig.streetAddress || '', orig.locality || '', orig.region || '',
|
|
orig.postalCode || '', orig.countryName || ''].join(';'));
|
|
}));
|
|
|
|
/**
|
|
* PHOTO TYPE
|
|
* The encoding MUST be reset to "b" using the ENCODING
|
|
* parameter in order to specify inline, encoded binary data. If the
|
|
* value is referenced by a URI value, then the default encoding of 8bit
|
|
* is used and no explicit ENCODING parameter is needed.
|
|
|
|
* Type value: A single value. The default is binary value. It can also
|
|
* be reset to uri value. The uri value can be used to specify a value
|
|
* outside of this MIME entity.
|
|
|
|
* Type special notes: The type can include the type parameter "TYPE" to
|
|
* specify the graphic image format type. The TYPE parameter values MUST
|
|
* be one of the IANA registered image formats or a non-standard image
|
|
* format.
|
|
*/
|
|
if ((typeof skipPhoto == 'undefined' || skipPhoto === false) &&
|
|
ct.photo && ct.photo.length) {
|
|
var photoMeta = ['PHOTO', 'ENCODING=b'];
|
|
var blob = ct.photo[0];
|
|
|
|
blobToBase64(blob, function(b64) {
|
|
if (blob.type) {
|
|
photoMeta.push('TYPE=' + blob.type);
|
|
}
|
|
allFields.push(photoMeta.join(';') + ':' + b64);
|
|
appendVCard(joinFields(allFields));
|
|
});
|
|
} else {
|
|
setZeroTimeout(function() { appendVCard(joinFields(allFields)); });
|
|
}
|
|
}
|
|
|
|
processContact(contacts[0]);
|
|
}
|
|
|
|
return {
|
|
ContactToVcard: ContactToVcard,
|
|
ContactToVcardBlob: ContactToVcardBlob,
|
|
};
|
|
})();
|