From 185c88568722f66c1b764eb3201bd88d2eb88777 Mon Sep 17 00:00:00 2001 From: Christoph Haas Date: Mon, 20 May 2013 21:09:51 +0000 Subject: [PATCH] vcf importer pre alpha. Import working but contact pictures are not saved. --- build.xml | 276 +++++++++ changelog.txt | 0 config.php | 12 + js/ABOUT.js | 58 ++ js/data/ResponseHandler.js | 93 +++ js/dialogs/ImportContentPanel.js | 69 +++ js/dialogs/ImportPanel.js | 545 ++++++++++++++++ js/dialogs/fields.txt | 826 +++++++++++++++++++++++++ js/plugin.contactimporter.js | 242 ++++++++ manifest.xml | 39 ++ php/download.php | 44 ++ php/module.contact.php | 594 ++++++++++++++++++ php/plugin.contactimporter.php | 81 +++ php/upload.php | 72 +++ php/vcf/Herr test of master jun .vcf | 410 ++++++++++++ php/vcf/class.vCard.php | 690 +++++++++++++++++++++ php/vcf/test.php | 216 +++++++ resources/css/contactimporter-main.css | 6 + resources/images/import_icon.png | Bin 0 -> 926 bytes resources/images/import_icon.xcf | Bin 0 -> 8430 bytes 20 files changed, 4273 insertions(+) create mode 100644 build.xml create mode 100644 changelog.txt create mode 100644 config.php create mode 100644 js/ABOUT.js create mode 100644 js/data/ResponseHandler.js create mode 100644 js/dialogs/ImportContentPanel.js create mode 100644 js/dialogs/ImportPanel.js create mode 100644 js/dialogs/fields.txt create mode 100644 js/plugin.contactimporter.js create mode 100644 manifest.xml create mode 100644 php/download.php create mode 100644 php/module.contact.php create mode 100644 php/plugin.contactimporter.php create mode 100644 php/upload.php create mode 100644 php/vcf/Herr test of master jun .vcf create mode 100644 php/vcf/class.vCard.php create mode 100644 php/vcf/test.php create mode 100644 resources/css/contactimporter-main.css create mode 100644 resources/images/import_icon.png create mode 100644 resources/images/import_icon.xcf diff --git a/build.xml b/build.xml new file mode 100644 index 0000000..44d59b5 --- /dev/null +++ b/build.xml @@ -0,0 +1,276 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + var Ext = {}; + var Zarafa = {}; + var container = {}; + var _ = function(key, domain) {}; + var dgettext = function(domain, msgid) {}; + var dngettext = function(domain, msgid, msgid_plural, count) {}; + var dnpgettext = function(domain, msgctxt, msgid, msgid_plural, count) {}; + var dpgettext = function(domain, msgctxt, msgid) {}; + var ngettext = function(msgid, msgid_plural, count) {}; + var npgettext = function(msgctxt, msgid, msgid_plural, count) {}; + var pgettext = function(msgctxt, msgid) {}; + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Processing manifest.xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/changelog.txt b/changelog.txt new file mode 100644 index 0000000..e69de29 diff --git a/config.php b/config.php new file mode 100644 index 0000000..1804424 --- /dev/null +++ b/config.php @@ -0,0 +1,12 @@ + diff --git a/js/ABOUT.js b/js/ABOUT.js new file mode 100644 index 0000000..2a3786f --- /dev/null +++ b/js/ABOUT.js @@ -0,0 +1,58 @@ +/** + * ABOUT.js zarafa contact to vcf im/exporter + * + * Author: Christoph Haas + * Copyright (C) 2012-2013 Christoph Haas + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ + +Ext.namespace('Zarafa.plugins.contactimporter'); + +/** + * @class Zarafa.plugins.contactimporter.ABOUT + * @extends String + * + * The copyright string holding the copyright notice for the Zarafa contactimporter Plugin. + */ +Zarafa.plugins.contactimporter.ABOUT = "" + + "

Copyright (C) 2012-2013 Christoph Haas <christoph.h@sprinternet.at>

" + + + "

This program is free software; you can redistribute it and/or " + + "modify it under the terms of the GNU Lesser General Public " + + "License as published by the Free Software Foundation; either " + + "version 2.1 of the License, or (at your option) any later version.

" + + + "

This program is distributed in the hope that it will be useful, " + + "but WITHOUT ANY WARRANTY; without even the implied warranty of " + + "MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU " + + "Lesser General Public License for more details.

" + + + "

You should have received a copy of the GNU Lesser General Public " + + "License along with this program; if not, write to the Free Software " + + "Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA

" + + + "
" + + + "

The contactimporter plugin contains the following third-party components:

" + + + "

vCard-parser

" + + + "

Copyright (C) 2012 Nuovo

" + + + "

Licensed under the MIT License.

" + + + "

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.

" \ No newline at end of file diff --git a/js/data/ResponseHandler.js b/js/data/ResponseHandler.js new file mode 100644 index 0000000..1e67d8c --- /dev/null +++ b/js/data/ResponseHandler.js @@ -0,0 +1,93 @@ +/** + * ResponseHandler.js zarafa contact im/exporter + * + * Author: Christoph Haas + * Copyright (C) 2012-2013 Christoph Haas + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ + +/** + * ResponseHandler + * + * This class handles all responses from the php backend + */ +Ext.namespace('Zarafa.plugins.contactimporter.data'); + +/** + * @class Zarafa.plugins.contactimporter.data.ResponseHandler + * @extends Zarafa.plugins.contactimporter.data.AbstractResponseHandler + * + * Calendar specific response handler. + */ +Zarafa.plugins.contactimporter.data.ResponseHandler = Ext.extend(Zarafa.core.data.AbstractResponseHandler, { + /** + * @cfg {Function} successCallback The function which + * will be called after success request. + */ + successCallback : null, + + /** + * Call the successCallback callback function. + * @param {Object} response Object contained the response data. + */ + doExport : function(response) { + this.successCallback(response); + }, + + /** + * Call the successCallback callback function. + * @param {Object} response Object contained the response data. + */ + doList : function(response) { + this.successCallback(response); + }, + + /** + * Call the successCallback callback function. + * @param {Object} response Object contained the response data. + */ + doImport : function(response) { + this.successCallback(response); + }, + + /** + * Call the successCallback callback function. + * @param {Object} response Object contained the response data. + */ + doAttachmentpath : function(response) { + this.successCallback(response); + }, + + /** + * Call the successCallback callback function. + * @param {Object} response Object contained the response data. + */ + doAddattachment : function(response) { + this.successCallback(response); + }, + + /** + * In case exception happened on server, server will return + * exception response with the code of exception. + * @param {Object} response Object contained the response data. + */ + doError: function(response) { + alert("error response code: " + response.error.info.code); + } +}); + +Ext.reg('contactimporter.contactresponsehandler', Zarafa.plugins.contactimporter.data.ResponseHandler); \ No newline at end of file diff --git a/js/dialogs/ImportContentPanel.js b/js/dialogs/ImportContentPanel.js new file mode 100644 index 0000000..4934671 --- /dev/null +++ b/js/dialogs/ImportContentPanel.js @@ -0,0 +1,69 @@ +/** + * ImportContentPanel.js zarafa contact to vcf im/exporter + * + * Author: Christoph Haas + * Copyright (C) 2012-2013 Christoph Haas + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ + +/** + * ImportContentPanel + * + * Container for the importpanel. + */ +Ext.namespace("Zarafa.plugins.contactimporter.dialogs"); + +/** + * @class Zarafa.plugins.contactimporter.dialogs.ImportContentPanel + * @extends Zarafa.core.ui.ContentPanel + * + * The content panel which shows the hierarchy tree of Owncloud account files. + * @xtype contactimportercontentpanel + */ +Zarafa.plugins.contactimporter.dialogs.ImportContentPanel = Ext.extend(Zarafa.core.ui.ContentPanel, { + + /** + * @constructor + * @param config Configuration structure + */ + constructor : function(config) { + config = config || {}; + var title = _('Import Contacts'); + if(container.getSettingsModel().get("zarafa/v1/plugins/contactimporter/enable_export")){ + title = _('Import/Export Contacts'); + } + Ext.applyIf(config, { + layout : 'fit', + title : title, + closeOnSave : true, + width : 620, + height : 465, + //Add panel + items : [ + { + xtype : 'contactimporter.importcontactpanel', + filename : config.filename + } + ] + }); + + Zarafa.plugins.contactimporter.dialogs.ImportContentPanel.superclass.constructor.call(this, config); + } + +}); + +Ext.reg('contactimporter.contentpanel' ,Zarafa.plugins.contactimporter.dialogs.ImportContentPanel); \ No newline at end of file diff --git a/js/dialogs/ImportPanel.js b/js/dialogs/ImportPanel.js new file mode 100644 index 0000000..bbea271 --- /dev/null +++ b/js/dialogs/ImportPanel.js @@ -0,0 +1,545 @@ +/** + * ImportPanel.js zarafa contact to vcf im/exporter + * + * Author: Christoph Haas + * Copyright (C) 2012-2013 Christoph Haas + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ + +/** + * ImportPanel + * + * The main Panel of the contactimporter plugin. + */ +Ext.namespace("Zarafa.plugins.contactimporter.dialogs"); + +/** + * @class Zarafa.plugins.contactimporter.dialogs.ImportPanel + * @extends Ext.form.FormPanel + */ +Zarafa.plugins.contactimporter.dialogs.ImportPanel = Ext.extend(Ext.Panel, { + + /* path to vcf file on server... */ + vcffile: null, + + /* export contacts buffer */ + exportResponse: new Array(), + + /* how many requests are still running? */ + runningRequests: null, + + /* The store for the selection grid */ + store: null, + + /** + * The internal 'iframe' which is hidden from the user, which is used for downloading + * attachments. See {@link #doOpen}. + * @property + * @type Ext.Element + */ + downloadFrame : undefined, + + /** + * @constructor + * @param {object} config + */ + constructor : function (config) { + config = config || {}; + var self = this; + + if(typeof config.filename !== "undefined") { + this.vcffile = config.filename; + } + + // create the data store + // we only display the firstname, lastname, homephone and primary email address in our grid + this.store = new Ext.data.ArrayStore({ + fields: [ + {name: 'display_name'}, + {name: 'given_name'}, + {name: 'surname'}, + {name: 'company_name'}, + {name: 'record'} + ] + }); + + Ext.apply(config, { + xtype : 'contactimporter.importpanel', + ref : "importcontactpanel", + layout : { + type : 'form', + align : 'stretch' + }, + anchor : '100%', + bodyStyle : 'background-color: inherit;', + defaults : { + border : true, + bodyStyle : 'background-color: inherit; padding: 3px 0px 3px 0px; border-style: none none solid none;' + }, + items : [ + this.createSelectBox(), + this.initForm(), + this.createGrid() + ], + buttons: [ + this.createSubmitAllButton(), + this.createSubmitButton(), + this.createCancelButton() + ], + listeners: { + afterrender: function (cmp) { + this.loadMask = new Ext.LoadMask(this.getEl(), {msg:'Loading...'}); + + if(this.vcffile != null) { // if we have got the filename from an attachment + this.parseContacts(this.vcffile); + } + }, + close: function (cmp) { + Ext.getCmp("importcontactsbutton").enable(); + }, + hide: function (cmp) { + Ext.getCmp("importcontactsbutton").enable(); + }, + destroy: function (cmp) { + Ext.getCmp("importcontactsbutton").enable(); + }, + scope: this + } + }); + + Zarafa.plugins.contactimporter.dialogs.ImportPanel.superclass.constructor.call(this, config); + }, + + /** + * Init embedded form, this is the form that is + * posted and contains the attachments + * @private + */ + initForm : function () { + return { + xtype: 'form', + ref: 'addContactFormPanel', + layout : 'column', + fileUpload: true, + autoWidth: true, + autoHeight: true, + border: false, + bodyStyle: 'padding: 5px;', + defaults: { + anchor: '95%', + border: false, + bodyStyle: 'padding: 5px;' + }, + items: [this.createUploadField()] + }; + }, + + /** + * Reloads the data of the grid + * @private + */ + reloadGridStore: function(contactdata) { + var parsedData = []; + + if(contactdata) { + parsedData = new Array(contactdata.contacts.length); + var i = 0; + for(i = 0; i < contactdata.contacts.length; i++) { + + parsedData[i] = new Array( + contactdata.contacts[i]["display_name"], + contactdata.contacts[i]["given_name"], + contactdata.contacts[i]["surname"], + contactdata.contacts[i]["company_name"], + contactdata.contacts[i] + ); + } + } else { + return null; + } + + this.store.loadData(parsedData, false); + }, + + /** + * Init embedded form, this is the form that is + * posted and contains the attachments + * @private + */ + createGrid : function() { + return { + xtype: 'grid', + ref: 'contactGrid', + columnWidth: 1.0, + store: this.store, + width: '100%', + height: 300, + title: 'Select contacts to import', + frame: false, + viewConfig:{ + forceFit:true + }, + colModel: new Ext.grid.ColumnModel({ + defaults: { + width: 300, + sortable: true + }, + columns: [ + {id: 'Displayname', header: 'Displayname', width: 350, sortable: true, dataIndex: 'display_name'}, + {header: 'Firstname', width: 200, sortable: true, dataIndex: 'given_name'}, + {header: 'Lastname', width: 200, sortable: true, dataIndex: 'surname'}, + {header: 'Company', sortable: true, dataIndex: 'company_name'} + ] + }), + sm: new Ext.grid.RowSelectionModel({multiSelect:true}) + } + }, + + createSelectBox: function() { + var defaultFolder = container.getHierarchyStore().getDefaultFolder('contact'); // @type: Zarafa.hierarchy.data.MAPIFolderRecord + var subFolders = defaultFolder.getChildren(); + var myStore = []; + + /* add all local contact folders */ + var i = 0; + myStore.push(new Array(defaultFolder.getDefaultFolderKey(), defaultFolder.getDisplayName())); + for(i = 0; i < subFolders.length; i++) { + /* Store all subfolders */ + myStore.push(new Array(subFolders[i].getDisplayName(), subFolders[i].getDisplayName(), false)); // 3rd field = isPublicfolder + } + + /* add all shared contact folders */ + var pubStore = container.getHierarchyStore().getPublicStore(); + + if(typeof pubStore !== "undefined") { + try { + var pubFolder = pubStore.getDefaultFolder("publicfolders"); + var pubSubFolders = pubFolder.getChildren(); + for(i = 0; i < pubSubFolders.length; i++) { + if(pubSubFolders[i].isContainerClass("IPF.Contact")){ + myStore.push(new Array(pubSubFolders[i].getDisplayName(), pubSubFolders[i].getDisplayName() + " [Shared]", true)); // 3rd field = isPublicfolder + } + } + } catch (e) { + console.log("Error opening the shared folder..."); + console.log(e); + } + } + + return { + xtype: "selectbox", + ref: 'addressbookSelector', + editable: false, + name: "choosen_addressbook", + value: container.getSettingsModel().get("zarafa/v1/plugins/contactimporter/default_addressbook"), + width: 100, + fieldLabel: "Select an addressbook", + store: myStore, + mode: 'local', + labelSeperator: ":", + border: false, + anchor: "100%", + scope: this, + allowBlank: false + } + }, + + createUploadField: function() { + return { + xtype: "fileuploadfield", + ref: 'contactfileuploadfield', + columnWidth: 1.0, + id: 'form-file', + name: 'vcfdata', + emptyText: 'Select an .vcf addressbook', + border: false, + anchor: "100%", + scope: this, + allowBlank: false, + listeners: { + 'fileselected': this.onFileSelected, + scope: this + } + } + }, + + createSubmitButton: function() { + return { + xtype: "button", + ref: "../submitButton", + disabled: true, + width: 100, + border: false, + text: _("Import"), + anchor: "100%", + handler: this.importCheckedContacts, + scope: this, + allowBlank: false + } + }, + + createSubmitAllButton: function() { + return { + xtype: "button", + ref: "../submitAllButton", + disabled: true, + width: 100, + border: false, + text: _("Import All"), + anchor: "100%", + handler: this.importAllContacts, + scope: this, + allowBlank: false + } + }, + + createCancelButton: function() { + return { + xtype: "button", + width: 100, + border: false, + text: _("Cancel"), + anchor: "100%", + handler: this.close, + scope: this, + allowBlank: false + } + }, + + /** + * This is called when a file has been seleceted in the file dialog + * in the {@link Ext.ux.form.FileUploadField} and the dialog is closed + * @param {Ext.ux.form.FileUploadField} uploadField being added a file to + */ + onFileSelected : function(uploadField) { + var form = this.addContactFormPanel.getForm(); + + if (form.isValid()) { + form.submit({ + waitMsg: 'Uploading and parsing contacts...', + url: 'plugins/contactimporter/php/upload.php', + failure: function(file, action) { + this.submitButton.disable(); + this.submitAllButton.disable(); + Zarafa.common.dialogs.MessageBox.show({ + title : _('Error'), + msg : _(action.result.error), + icon : Zarafa.common.dialogs.MessageBox.ERROR, + buttons : Zarafa.common.dialogs.MessageBox.OK + }); + }, + success: function(file, action){ + uploadField.reset(); + this.vcffile = action.result.vcf_file; + + this.parseContacts(this.vcffile); + }, + scope : this + }); + } + }, + + parseContacts: function (vcfPath) { + this.loadMask.show(); + + // call export function here! + var responseHandler = new Zarafa.plugins.contactimporter.data.ResponseHandler({ + successCallback: this.handleParsingResult.createDelegate(this) + }); + + container.getRequest().singleRequest( + 'contactmodule', + 'import', + { + vcf_filepath: vcfPath + }, + responseHandler + ); + }, + + handleParsingResult: function(response) { + this.loadMask.hide(); + + if(response["status"] == true) { + this.submitButton.enable(); + this.submitAllButton.enable(); + + this.reloadGridStore(response.parsed); + } else { + this.submitButton.disable(); + this.submitAllButton.disable(); + Zarafa.common.dialogs.MessageBox.show({ + title : _('Parser Error'), + msg : _(response["message"]), + icon : Zarafa.common.dialogs.MessageBox.ERROR, + buttons : Zarafa.common.dialogs.MessageBox.OK + }); + } + }, + + close: function () { + this.addContactFormPanel.getForm().reset(); + this.dialog.close() + }, + + importCheckedContacts: function () { + var newRecords = this.contactGrid.selModel.getSelections(); + this.importContacts(newRecords); + }, + + importAllContacts: function () { + //receive Records from grid rows + this.contactGrid.selModel.selectAll(); // select all entries + var newRecords = this.contactGrid.selModel.getSelections(); + this.importContacts(newRecords); + }, + + /** + * This function stores all given events to the appointmentstore + * @param events + */ + importContacts: function (contacts) { + //receive existing contact store + var folderValue = this.addressbookSelector.getValue(); + + if(folderValue == undefined) { // no addressbook choosen + Zarafa.common.dialogs.MessageBox.show({ + title : _('Error'), + msg : _('You have to choose an addressbook!'), + icon : Zarafa.common.dialogs.MessageBox.ERROR, + buttons : Zarafa.common.dialogs.MessageBox.OK + }); + } else { + var addressbookexist = true; + if(this.contactGrid.selModel.getCount() < 1) { + Zarafa.common.dialogs.MessageBox.show({ + title : _('Error'), + msg : _('You have to choose at least one contact to import!'), + icon : Zarafa.common.dialogs.MessageBox.ERROR, + buttons : Zarafa.common.dialogs.MessageBox.OK + }); + } else { + var contactStore = new Zarafa.contact.ContactStore(); + var contactFolder = container.getHierarchyStore().getDefaultFolder('contact'); + var pubStore = container.getHierarchyStore().getPublicStore(); + var pubFolder = pubStore.getDefaultFolder("publicfolders"); + var pubSubFolders = pubFolder.getChildren(); + + if(folderValue != "contact") { + var subFolders = contactFolder.getChildren(); + var i = 0; + for(i = 0; i < pubSubFolders.length; i++) { + if(pubSubFolders[i].isContainerClass("IPF.Contact")){ + subFolders.push(pubSubFolders[i]); + } + } + for(i=0;i
" + + +icon_index + + +512 + + +im + + +"" + + +isHTML + + +false + + +isdn_number + + +"" + + +last_modification_time + + +null + + +mailing_address + + +"" + + +manager_name + + +"" + + +message_class + + +"IPM.Contact" + + +message_flags + + +1 + + +message_size + + +"" + + +middle_name + + +"testm" + + +nickname + + +"nickn" + + +normalized_subject + + +"" + + +object_type + + +5 + + +office_location + + +"olocation" + + +other_address + + +"" + + +other_address_city + + +"" + + +other_address_country + + +"" + + +other_address_postal_code + + +"" + + +other_address_state + + +"" + + +other_address_street + + +"" + + +other_telephone_number + + +"" + + +pager_telephone_number + + +"" + + +primary_fax_number + + +"" + + +primary_telephone_number + + +"" + + +private + + +false + + +profession + + +"profess" + + +radio_telephone_number + + +"" + + +read_receipt_requested + + +false + + +sender_address_type + + +"" + + +sender_email_address + + +"" + + +sender_entryid + + +"" + + +sender_name + + +"" + + +sender_search_key + + +"" + + +sensitivity + + +0 + + +sent_representing_address_type + + +"" + + +sent_representing_email_address + + +"" + + +sent_representing_entryid + + +"" + + +sent_representing_name + + +"" + + +sent_representing_search_key + + +"" + + +spouse_name + + +"" + + +subject + + +"testf testm testl" + + +surname + + +"testl" + + +telex_telephone_number + + +"" + + +timezone + + +"" + + +timezonedst + + +"" + + +title + + +"jobt" + + +ttytdd_telephone_number + + +"" + + +unk + + +"" + + +webpage + + +"" + + +wedding_anniversary + + +null \ No newline at end of file diff --git a/js/plugin.contactimporter.js b/js/plugin.contactimporter.js new file mode 100644 index 0000000..3370a08 --- /dev/null +++ b/js/plugin.contactimporter.js @@ -0,0 +1,242 @@ +/** + * plugin.contactimporter.js zarafa calender to ics im/exporter + * + * Author: Christoph Haas + * Copyright (C) 2012-2013 Christoph Haas + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ + +Ext.namespace("Zarafa.plugins.contactimporter"); // Assign the right namespace + +Zarafa.plugins.contactimporter.ImportPlugin = Ext.extend(Zarafa.core.Plugin, { // create new import plugin + + /** + * @constructor + * @param {Object} config Configuration object + * + */ + constructor: function (config) { + config = config || {}; + + Ext.applyIf(config, { + name : 'contactimporter', + displayName : _('Contactimporter Plugin'), + about : Zarafa.plugins.contactimporter.ABOUT + }); + + Zarafa.plugins.contactimporter.ImportPlugin.superclass.constructor.call(this, config); + }, + + /** + * initialises insertion point for plugin + * @protected + */ + initPlugin : function() { + Zarafa.plugins.contactimporter.ImportPlugin.superclass.initPlugin.apply(this, arguments); + + /* our panel */ + Zarafa.core.data.SharedComponentType.addProperty('plugins.contactimporter.dialogs.importcontacts'); + + /* directly import received vcfs */ + this.registerInsertionPoint('common.contextmenu.attachment.actions', this.createAttachmentImportButton); + /* add import button to south navigation */ + this.registerInsertionPoint("navigation.south", this.createImportButton, this); + }, + + /** + * Creates the button + * + * @return {Object} Configuration object for a {@link Ext.Button button} + * + */ + createImportButton: function () { + var button = { + xtype : 'button', + id : "importcontactsbutton", + text : _('Import Contacts'), + iconCls : 'icon_contactimporter_button', + navigationContext : container.getContextByName('contact'), + handler : this.onImportButtonClick, + scope : this + }; + + if(container.getSettingsModel().get("zarafa/v1/plugins/contactimporter/enable_export")) { + button.text = _('Import/Export Contacts'); + } + + return button; + }, + + /** + * Insert import button in all attachment suggestions + + * @return {Object} Configuration object for a {@link Ext.Button button} + */ + createAttachmentImportButton : function(include, btn) { + return { + text : _('Import Contacts'), + handler : this.getAttachmentFileName.createDelegate(this, [btn, this.gotAttachmentFileName]), + scope : this, + iconCls : 'icon_contactimporter_button', + beforeShow : function(item, record) { + var extension = record.data.name.split('.').pop().toLowerCase(); + + if(record.data.filetype == "text/vcard" || extension == "vcf" || extension == "vcard") { + item.setDisabled(false); + } else { + item.setDisabled(true); + } + } + }; + }, + + /** + * Callback for getAttachmentFileName + */ + gotAttachmentFileName: function(response) { + if(response.status == true) { + Zarafa.core.data.UIFactory.openLayerComponent(Zarafa.core.data.SharedComponentType['plugins.contactimporter.dialogs.importcontacts'], undefined, { + manager : Ext.WindowMgr, + filename : response.tmpname + }); + } else { + Zarafa.common.dialogs.MessageBox.show({ + title : _('Error'), + msg : _(response["message"]), + icon : Zarafa.common.dialogs.MessageBox.ERROR, + buttons : Zarafa.common.dialogs.MessageBox.OK + }); + } + }, + + /** + * Clickhandler for the button + */ + getAttachmentFileName: function (btn, callback) { + Zarafa.common.dialogs.MessageBox.show({ + title: 'Please wait', + msg: 'Loading attachment...', + progressText: 'Initializing...', + width:300, + progress:true, + closable:false + }); + + // progress bar... ;) + var f = function(v){ + return function(){ + if(v == 100){ + Zarafa.common.dialogs.MessageBox.hide(); + }else{ + Zarafa.common.dialogs.MessageBox.updateProgress(v/100, Math.round(v)+'% loaded'); + } + }; + }; + + for(var i = 1; i < 101; i++){ + setTimeout(f(i), 20*i); + } + + /* store the attachment to a temporary folder and prepare it for uploading */ + var attachmentRecord = btn.records; + var attachmentStore = attachmentRecord.store; + + var store = attachmentStore.getParentRecord().get('store_entryid'); + var entryid = attachmentStore.getAttachmentParentRecordEntryId(); + var attachNum = new Array(1); + if (attachmentRecord.get('attach_num') != -1) + attachNum[0] = attachmentRecord.get('attach_num'); + else + attachNum[0] = attachmentRecord.get('tmpname'); + var dialog_attachments = attachmentStore.getId(); + var filename = attachmentRecord.data.name; + + var responseHandler = new Zarafa.plugins.contactimporter.data.ResponseHandler({ + successCallback: callback + }); + + // request attachment preperation + container.getRequest().singleRequest( + 'calendarmodule', + 'attachmentpath', + { + entryid : entryid, + store: store, + attachNum: attachNum, + dialog_attachments: dialog_attachments, + filename: filename + }, + responseHandler + ); + }, + + /** + * Clickhandler for the button + */ + onImportButtonClick: function () { + Ext.getCmp("importcontactsbutton").disable(); + Zarafa.core.data.UIFactory.openLayerComponent(Zarafa.core.data.SharedComponentType['plugins.contactimporter.dialogs.importcontacts'], undefined, { + manager : Ext.WindowMgr + }); + }, + + /** + * Bid for the type of shared component + * and the given record. + * This will bid on calendar.dialogs.importcontacts + * @param {Zarafa.core.data.SharedComponentType} type Type of component a context can bid for. + * @param {Ext.data.Record} record Optionally passed record. + * @return {Number} The bid for the shared component + */ + bidSharedComponent : function(type, record) { + var bid = -1; + switch(type) { + case Zarafa.core.data.SharedComponentType['plugins.contactimporter.dialogs.importcontacts']: + bid = 2; + break; + } + return bid; + }, + + /** + * Will return the reference to the shared component. + * Based on the type of component requested a component is returned. + * @param {Zarafa.core.data.SharedComponentType} type Type of component a context can bid for. + * @param {Ext.data.Record} record Optionally passed record. + * @return {Ext.Component} Component + */ + getSharedComponent : function(type, record) { + var component; + switch(type) { + case Zarafa.core.data.SharedComponentType['plugins.contactimporter.dialogs.importcontacts']: + component = Zarafa.plugins.contactimporter.dialogs.ImportContentPanel; + break; + } + + return component; + } +}); + + +/*############################################################################################################################ + * STARTUP + *############################################################################################################################*/ +Zarafa.onReady(function() { + if(container.getSettingsModel().get("zarafa/v1/plugins/contactimporter/enable") === true) { + container.registerPlugin(new Zarafa.plugins.contactimporter.ImportPlugin); + } +}); diff --git a/manifest.xml b/manifest.xml new file mode 100644 index 0000000..bcd1b92 --- /dev/null +++ b/manifest.xml @@ -0,0 +1,39 @@ + + + + + @_@PLUGIN_VERSION@_@ + contactimporter + VCF Contact Importer/Exporter + Christoph Haas + http://www.sprinternet.at + Import or Export a VCF file to/from the zarafa addressbook + + + config.php + + + + + + php/plugin.contactimporter.php + php/module.contact.php + + + js/contactimporter.js + js/contactimporter-debug.js + + js/plugin.contactimporter.js + js/data/ResponseHandler.js + js/dialogs/ImportContentPanel.js + js/dialogs/ImportPanel.js + + + resources/css/contactimporter-min.css + resources/css/contactimporter.css + resources/css/contactimporter-main.css + + + + + diff --git a/php/download.php b/php/download.php new file mode 100644 index 0000000..b99cd56 --- /dev/null +++ b/php/download.php @@ -0,0 +1,44 @@ + + * Copyright (C) 2012-2013 Christoph Haas + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +$basedir = $_GET["basedir"]; +$secid = $_GET["secid"]; +$fileid = $_GET["fileid"]; +$realname = $_GET["realname"]; + +$secfile = $basedir . "/secid." . $secid; +$vcffile = $basedir . "/" . $fileid . "." . $secid; + +// if the secid file exists -> download! +if(file_exists($secfile)) { + @header("Last-Modified: " . @gmdate("D, d M Y H:i:s",time()) . " GMT"); + @header("Content-type: text/vcard"); + header("Content-Length: " . filesize($vcffile)); + header("Content-Disposition: attachment; filename=" . $realname . ".vcf"); + + //write vcf + readfile($vcffile); + unlink($secfile); + unlink($vcffile); +} + +?> \ No newline at end of file diff --git a/php/module.contact.php b/php/module.contact.php new file mode 100644 index 0000000..00e5939 --- /dev/null +++ b/php/module.contact.php @@ -0,0 +1,594 @@ + + * Copyright (C) 2012-2013 Christoph Haas + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ + +include_once('vcf/class.vCard.php'); + +class ContactModule extends Module { + + private $DEBUG = true; // enable error_log debugging + + /** + * @constructor + * @param $id + * @param $data + */ + public function __construct($id, $data) { + parent::Module($id, $data); + } + + /** + * Executes all the actions in the $data variable. + * Exception part is used for authentication errors also + * @return boolean true on success or false on failure. + */ + public function execute() { + $result = false; + + if(!$this->DEBUG) { + /* disable error printing - otherwise json communication might break... */ + ini_set('display_errors', '0'); + } + + foreach($this->data as $actionType => $actionData) { + if(isset($actionType)) { + try { + if($this->DEBUG) { + error_log("exec: " . $actionType); + } + switch($actionType) { + case "export": + $result = $this->exportCalendar($actionType, $actionData); + break; + case "import": + $result = $this->importContacts($actionType, $actionData); + break; + case "addattachment": + $result = $this->addAttachment($actionType, $actionData); + break; + case "attachmentpath": + $result = $this->getAttachmentPath($actionType, $actionData); + break; + default: + $this->handleUnknownActionType($actionType); + } + + } catch (MAPIException $e) { + if($this->DEBUG) { + error_log("mapi exception: " . $e->getMessage()); + } + } catch (Exception $e) { + if($this->DEBUG) { + error_log("exception: " . $e->getMessage()); + } + } + } + } + + return $result; + } + + /** + * Generates a random string with variable length. + * @param $length the lenght of the generated string + * @return string a random string + */ + private function randomstring($length = 6) { + // $chars - all allowed charakters + $chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"; + + srand((double)microtime()*1000000); + $i = 0; + $pass = ""; + while ($i < $length) { + $num = rand() % strlen($chars); + $tmp = substr($chars, $num, 1); + $pass = $pass . $tmp; + $i++; + } + return $pass; + } + + /** + * Generates the secid file (used to verify the download path) + * @param $secid the secid, a random security token + */ + private function createSecIDFile($secid) { + $lockFile = TMP_PATH . "/secid." . $secid; + $fh = fopen($lockFile, 'w') or die("can't open secid file"); + $stringData = date(DATE_RFC822); + fwrite($fh, $stringData); + fclose($fh); + } + + /** + * Generates the secid file (used to verify the download path) + * @param $time a timestamp + * @param $incl_time true if date should include time + * @ return date object + */ + private function getIcalDate($time, $incl_time = true) { + return $incl_time ? date('Ymd\THis', $time) : date('Ymd', $time); + } + + /** + * Add an attachment to the give contact + * @param $actionType + * @param $actionData + */ + private function addAttachment($actionType, $actionData) { + // Get Attachment data from state + $attachment_state = new AttachmentState(); + $attachment_state->open(); + + $filename = "ContactPicture.jpg"; + $tmppath = $actionData["tmpfile"]; + $filesize = filesize($tmppath); + $f = getimagesize($tmppath); + $filetype = $f["mime"]; + + // Move the uploaded file into the attachment state + $attachid = $attachment_state->addProvidedAttachmentFile($actionData["storeid"], $filename, $tmppath, array( + "name" => $filename, + "size" => $filesize, + "type" => $filetype, + "sourcetype" => 'default' + )); + + $attachment_state->close(); + + $response['status'] = true; + $response['storeid'] = $actionData["storeid"]; + $response['tmpname'] = $attachid; + $response['name'] = $filename; + $response['size'] = $filesize; + $response['type'] = $filetype; + + $this->addActionData($actionType, $response); + $GLOBALS["bus"]->addData($this->getResponseData()); + } + + /** + * The main export function, creates the ics file for download + * @param $actionType + * @param $actionData + */ + private function exportContacts($actionType, $actionData) { + $secid = $this->randomstring(); + $this->createSecIDFile($secid); + $tmpname = stripslashes($actionData["calendar"] . ".ics." . $this->randomstring(8)); + $filename = TMP_PATH . "/" . $tmpname . "." . $secid; + + if(!is_writable(TMP_PATH . "/")) { + error_log("could not write to export tmp directory!"); + } + + $tz = date("e"); // use php timezone (maybe set up in php.ini, date.timezone) + + if($this->DEBUG) { + error_log("PHP Timezone: " . $tz); + } + + $config = array( + "language" => substr($GLOBALS["settings"]->get("zarafa/v1/main/language"),0,2), + "directory" => TMP_PATH . "/", + "filename" => $tmpname . "." . $secid, + "unique_id" => "zarafa-export-plugin", + "TZID" => $tz + ); + + $v = new vcalendar($config); + $v->setProperty("method", "PUBLISH"); // required of some calendar software + $v->setProperty("x-wr-calname", $actionData["calendar"]); // required of some calendar software + $v->setProperty("X-WR-CALDESC", "Exported Zarafa Calendar"); // required of some calendar software + $v->setProperty("X-WR-TIMEZONE", $tz); + + $xprops = array("X-LIC-LOCATION" => $tz); // required of some calendar software + iCalUtilityFunctions::createTimezone($v, $tz, $xprops); // create timezone object in calendar + + + foreach($actionData["data"] as $event) { + $event["props"]["description"] = $this->loadEventDescription($event); + $event["props"]["attendees"] = $this->loadAttendees($event); + + $vevent = & $v->newComponent("vevent"); // create a new event object + $this->addEvent($vevent, $event["props"]); + } + + $v->saveCalendar(); + + $response['status'] = true; + $response['fileid'] = $tmpname; // number of entries that will be exported + $response['basedir'] = TMP_PATH; + $response['secid'] = $secid; + $response['realname'] = $actionData["calendar"]; + $this->addActionData($actionType, $response); + $GLOBALS["bus"]->addData($this->getResponseData()); + + if($this->DEBUG) { + error_log("export done, bus data written!"); + } + } + + /** + * The main import function, parses the uploaded vcf file + * @param $actionType + * @param $actionData + */ + private function importContacts($actionType, $actionData) { + if($this->DEBUG) { + error_log("PHP Timezone: " . $tz); + } + + if(is_readable ($actionData["vcf_filepath"])) { + $vcard = new vCard($actionData["vcf_filepath"], false, array('Collapse' => false)); // Parse it! + error_log(print_r($vcard, true)); + if(count($vcard) == 0) { + $response['status'] = false; + $response['message']= "No contacts in vcf file"; + } else { + $vCard = $vcard; + if (count($vCard) == 1) { + $vCard = array($vcard); + } + + $response['status'] = true; + $response['parsed_file']= $actionData["vcf_filepath"]; + $response['parsed'] = array ( + 'contacts' => $this->parseContactsToArray($vCard) + ); + } + } else { + $response['status'] = false; + $response['message']= "File could not be read by server"; + } + + $this->addActionData($actionType, $response); + $GLOBALS["bus"]->addData($this->getResponseData()); + + if($this->DEBUG) { + error_log("parsing done, bus data written!"); + } + } + + /** + * Create a array with contacts + * + * @param contacts vcard or csv contacts + * @param csv optional, true if contacts are csv contacts + * @return array parsed contacts + * @private + */ + private function parseContactsToArray($contacts, $csv = false) { + $carr = array(); + if(!$csv) { + foreach ($contacts as $Index => $vCard) { + $properties = array(); + $properties["display_name"] = $vCard -> FN[0]; + $properties["fileas"] = $vCard -> FN[0]; + + foreach ($vCard -> N as $Name) { + $properties["given_name"] = $Name['FirstName']; + $properties["middle_name"] = $Name['AdditionalNames']; + $properties["surname"] = $Name['LastName']; + $properties["display_name_prefix"] = $Name['Prefixes']; + } + if ($vCard -> TEL) { + foreach ($vCard -> TEL as $Tel) { + if(!is_scalar($Tel)) { + if(in_array("home", $Tel['Type'])) { + $properties["home_telephone_number"] = $Tel['Value']; + } else if(in_array("cell", $Tel['Type'])) { + $properties["cellular_telephone_number"] = $Tel['Value']; + } else if(in_array("work", $Tel['Type'])) { + $properties["business_telephone_number"] = $Tel['Value']; + } else if(in_array("fax", $Tel['Type'])) { + $properties["business_fax_number"] = $Tel['Value']; + } else if(in_array("pager", $Tel['Type'])) { + $properties["pager_telephone_number"] = $Tel['Value']; + } else if(in_array("isdn", $Tel['Type'])) { + $properties["isdn_number"] = $Tel['Value']; + } else if(in_array("car", $Tel['Type'])) { + $properties["car_telephone_number"] = $Tel['Value']; + } else if(in_array("modem", $Tel['Type'])) { + $properties["ttytdd_telephone_number"] = $Tel['Value']; + } + } + } + } + if ($vCard -> EMAIL) { + $e=0; + foreach ($vCard -> EMAIL as $Email) { + $fileas = $Email['Value']; + if(isset($properties["fileas"]) && !empty($properties["fileas"])) { + $fileas = $properties["fileas"]; + } + + if(!is_scalar($Email)) { + switch($e) { + case 0: + $properties["email_address_1"] = $Email['Value']; + $properties["email_address_display_name_1"] = $fileas . " (" . $Email['Value'] . ")"; + break; + case 1: + $properties["email_address_2"] = $Email['Value']; + $properties["email_address_display_name_2"] = $fileas . " (" . $Email['Value'] . ")"; + break; + case 2: + $properties["email_address_3"] = $Email['Value']; + $properties["email_address_display_name_3"] = $fileas . " (" . $Email['Value'] . ")"; + break; + default: break; + } + $e++; + } + } + } + if ($vCard -> ORG) { + foreach ($vCard -> ORG as $Organization) { + $properties["company_name"] = $Organization['Name']; + if(empty($properties["display_name"])) { + $properties["display_name"] = $Organization['Name']; // if we have no displayname - use the company name as displayname + $properties["fileas"] = $Organization['Name']; + } + } + } + if ($vCard -> TITLE) { + $properties["title"] = $vCard -> TITLE[0]; + } + if ($vCard -> URL) { + $properties["webpage"] = $vCard -> URL[0]; + } + if ($vCard -> IMPP) { + foreach ($vCard -> IMPP as $IMPP) { + if (!is_scalar($IMPP)) { + $properties["im"] = $IMPP['Value']; + } + } + } + if ($vCard -> ADR) { + foreach ($vCard -> ADR as $Address) { + if(in_array("work", $Address['Type'])) { + $properties["business_address_street"] = $Address['StreetAddress']; + $properties["business_address_city"] = $Address['Locality']; + $properties["business_address_state"] = $Address['Region']; + $properties["business_address_postal_code"] = $Address['PostalCode']; + $properties["business_address_country"] = $Address['Country']; + $properties["business_address"] = $this->buildAddressString($Address['StreetAddress'], $Address['PostalCode'], $Address['Locality'], $Address['Region'], $Address['Country']); + } else if(in_array("home", $Address['Type'])) { + $properties["home_address_street"] = $Address['StreetAddress']; + $properties["home_address_city"] = $Address['Locality']; + $properties["home_address_state"] = $Address['Region']; + $properties["home_address_postal_code"] = $Address['PostalCode']; + $properties["home_address_country"] = $Address['Country']; + $properties["home_address"] = $this->buildAddressString($Address['StreetAddress'], $Address['PostalCode'], $Address['Locality'], $Address['Region'], $Address['Country']); + } else if(in_array("postal", $Address['Type'])||in_array("parcel", $Address['Type'])||in_array("intl", $Address['Type'])||in_array("dom", $Address['Type'])) { + $properties["other_address_street"] = $Address['StreetAddress']; + $properties["other_address_city"] = $Address['Locality']; + $properties["other_address_state"] = $Address['Region']; + $properties["other_address_postal_code"] = $Address['PostalCode']; + $properties["other_address_country"] = $Address['Country']; + $properties["other_address"] = $this->buildAddressString($Address['StreetAddress'], $Address['PostalCode'], $Address['Locality'], $Address['Region'], $Address['Country']); + } + } + } + if ($vCard -> BDAY) { + $properties["birthday"] = strtotime($vCard -> BDAY[0]); + } + if ($vCard -> NOTE) { + $properties["body"] = $vCard -> NOTE[0]; + } + if ($vCard -> PHOTO) { + if(!is_writable(TMP_PATH . "/")) { + error_log("could not write to export tmp directory!: " . $E); + } else { + $tmppath = TMP_PATH . "/" . $this->randomstring(15); + try { + if($vCard -> SaveFile('photo', 0, $tmppath)) { + $properties["x_photo_path"] = $tmppath; + } else { + if($this->DEBUG) { + error_log("remote imagefetching not implemented"); + } + } + } catch (Exception $E) { + error_log("Image exception: " . $E); + } + } + } + array_push($carr, $properties); + } + } else { + error_log("csv parsing not implemented"); + } + + return $carr; + } + + /** + * Generate the whole addressstring + * + * @param street + * @param zip + * @param city + * @param state + * @param country + * @return string the concatinated address string + * @private + */ + private function buildAddressString($street, $zip, $city, $state, $country) { + $out = ""; + + if (isset($country) && $street != "") $out = $country; + + $zcs = ""; + if (isset($zip) && $zip != "") $zcs = $zip; + if (isset($city) && $city != "") $zcs .= (($zcs)?" ":"") . $city; + if (isset($state) && $state != "") $zcs .= (($zcs)?" ":"") . $state; + if ($zcs) $out = $zcs . "\r\n" . $out; + + if (isset($street) && $street != "") $out = $street . (($out)?"\r\n". $out: "") ; + + return $out; + } + + /** + * Store the file to a temporary directory, prepare it for oc upload + * @param $actionType + * @param $actionData + * @private + */ + private function getAttachmentPath($actionType, $actionData) { + // Get store id + $storeid = false; + if(isset($actionData["store"])) { + $storeid = $actionData["store"]; + } + + // Get message entryid + $entryid = false; + if(isset($actionData["entryid"])) { + $entryid = $actionData["entryid"]; + } + + // Check which type isset + $openType = "attachment"; + + // Get number of attachment which should be opened. + $attachNum = false; + if(isset($actionData["attachNum"])) { + $attachNum = $actionData["attachNum"]; + } + + // Check if storeid and entryid isset + if($storeid && $entryid) { + // Open the store + $store = $GLOBALS["mapisession"]->openMessageStore(hex2bin($storeid)); + + if($store) { + // Open the message + $message = mapi_msgstore_openentry($store, hex2bin($entryid)); + + if($message) { + $attachment = false; + + // Check if attachNum isset + if($attachNum) { + // Loop through the attachNums, message in message in message ... + for($i = 0; $i < (count($attachNum) - 1); $i++) + { + // Open the attachment + $tempattach = mapi_message_openattach($message, (int) $attachNum[$i]); + if($tempattach) { + // Open the object in the attachment + $message = mapi_attach_openobj($tempattach); + } + } + + // Open the attachment + $attachment = mapi_message_openattach($message, (int) $attachNum[(count($attachNum) - 1)]); + } + + // Check if the attachment is opened + if($attachment) { + + // Get the props of the attachment + $props = mapi_attach_getprops($attachment, array(PR_ATTACH_LONG_FILENAME, PR_ATTACH_MIME_TAG, PR_DISPLAY_NAME, PR_ATTACH_METHOD)); + // Content Type + $contentType = "application/octet-stream"; + // Filename + $filename = "ERROR"; + + // Set filename + if(isset($props[PR_ATTACH_LONG_FILENAME])) { + $filename = $props[PR_ATTACH_LONG_FILENAME]; + } else if(isset($props[PR_ATTACH_FILENAME])) { + $filename = $props[PR_ATTACH_FILENAME]; + } else if(isset($props[PR_DISPLAY_NAME])) { + $filename = $props[PR_DISPLAY_NAME]; + } + + // Set content type + if(isset($props[PR_ATTACH_MIME_TAG])) { + $contentType = $props[PR_ATTACH_MIME_TAG]; + } else { + // Parse the extension of the filename to get the content type + if(strrpos($filename, ".") !== false) { + $extension = strtolower(substr($filename, strrpos($filename, "."))); + $contentType = "application/octet-stream"; + if (is_readable("mimetypes.dat")){ + $fh = fopen("mimetypes.dat","r"); + $ext_found = false; + while (!feof($fh) && !$ext_found){ + $line = fgets($fh); + preg_match("/(\.[a-z0-9]+)[ \t]+([^ \t\n\r]*)/i", $line, $result); + if ($extension == $result[1]){ + $ext_found = true; + $contentType = $result[2]; + } + } + fclose($fh); + } + } + } + + + $tmpname = tempnam(TMP_PATH, stripslashes($filename)); + + // Open a stream to get the attachment data + $stream = mapi_openpropertytostream($attachment, PR_ATTACH_DATA_BIN); + $stat = mapi_stream_stat($stream); + // File length = $stat["cb"] + + $fhandle = fopen($tmpname,'w'); + $buffer = null; + for($i = 0; $i < $stat["cb"]; $i += BLOCK_SIZE) { + // Write stream + $buffer = mapi_stream_read($stream, BLOCK_SIZE); + fwrite($fhandle,$buffer,strlen($buffer)); + } + fclose($fhandle); + + $response = array(); + $response['tmpname'] = $tmpname; + $response['filename'] = $filename; + $response['status'] = true; + $this->addActionData($actionType, $response); + $GLOBALS["bus"]->addData($this->getResponseData()); + } + } + } else { + $response['status'] = false; + $response['message'] = "Store could not be opened!"; + $this->addActionData($actionType, $response); + $GLOBALS["bus"]->addData($this->getResponseData()); + } + } else { + $response['status'] = false; + $response['message'] = "Wrong call, store and entryid have to be set!"; + $this->addActionData($actionType, $response); + $GLOBALS["bus"]->addData($this->getResponseData()); + } + } +}; + +?> diff --git a/php/plugin.contactimporter.php b/php/plugin.contactimporter.php new file mode 100644 index 0000000..623a0cc --- /dev/null +++ b/php/plugin.contactimporter.php @@ -0,0 +1,81 @@ + + * Copyright (C) 2012-2013 Christoph Haas + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ + +/** + * contactimporter Plugin + * + * With this plugin you can import a vcf file to your zarafa addressbook + * + */ +class Plugincontactimporter extends Plugin { + /** + * Constructor + */ + function Plugincontactimporter() {} + + /** + * Function initializes the Plugin and registers all hooks + * + * @return void + */ + function init() { + $this->registerHook('server.core.settings.init.before'); + } + + /** + * Function is executed when a hook is triggered by the PluginManager + * + * @param string $eventID the id of the triggered hook + * @param mixed $data object(s) related to the hook + * @return void + */ + function execute($eventID, &$data) { + switch($eventID) { + case 'server.core.settings.init.before' : + $this->injectPluginSettings($data); + break; + } + } + + /** + * Called when the core Settings class is initialized and ready to accept sysadmin default + * settings. + * @param Array $data Reference to the data of the triggered hook + */ + function injectPluginSettings(&$data) { + $data['settingsObj']->addSysAdminDefaults(Array( + 'zarafa' => Array( + 'v1' => Array( + 'plugins' => Array( + 'contactimporter' => Array( + 'enable' => PLUGIN_CONTACTIMPORTER_USER_DEFAULT_ENABLE, + 'enable_export' => PLUGIN_CONTACTIMPORTER_USER_DEFAULT_ENABLE_EXPORT, + 'default_addressbook' => PLUGIN_CONTACTIMPORTER_DEFAULT + ) + ) + ) + ) + )); + } +} +?> diff --git a/php/upload.php b/php/upload.php new file mode 100644 index 0000000..aaaf720 --- /dev/null +++ b/php/upload.php @@ -0,0 +1,72 @@ + + * Copyright (C) 2012-2013 Christoph Haas + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ + +require_once("../config.php"); + +/* disable error printing - otherwise json communication might break... */ +ini_set('display_errors', '0'); + + /** + * respond/echo JSON + * @param $arr + */ +function respondJSON($arr) { + echo json_encode($arr); +} + +/** + * Generates a random string with variable length. + * @param $length the lenght of the generated string + * @return string a random string + */ +function randomstring($length = 6) { + // $chars - all allowed charakters + $chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"; + + srand((double)microtime()*1000000); + $i = 0; + $pass = ""; + while ($i < $length) { + $num = rand() % strlen($chars); + $tmp = substr($chars, $num, 1); + $pass = $pass . $tmp; + $i++; + } + return $pass; +} + +$destpath = PLUGIN_CONTACTIMPORTER_TMP_UPLOAD; +$destpath .= $_FILES['vcfdata']['name'] . randomstring(); + +if(is_readable ($_FILES['vcfdata']['tmp_name'])) { + $result = move_uploaded_file($_FILES['vcfdata']['tmp_name'],$destpath); + + if($result) { + respondJSON(array ('success'=>true, 'vcf_file'=>$destpath)); + } else { + respondJSON(array ('success'=>false,'error'=>"File could not be moved to TMP path! Check plugin config and folder permissions!")); + } +} else { + respondJSON(array ('success'=>false,'error'=>"File could not be read by server, upload error!")); +} +?> \ No newline at end of file diff --git a/php/vcf/Herr test of master jun .vcf b/php/vcf/Herr test of master jun .vcf new file mode 100644 index 0000000..7b31eb7 --- /dev/null +++ b/php/vcf/Herr test of master jun .vcf @@ -0,0 +1,410 @@ +BEGIN:VCARD +VERSION:2.1 +N;LANGUAGE=de:master;test;of;Herr;jun. +FN:Herr test of master jun. +ORG:testcomp;Abteilung X +TITLE:chef +TEL;WORK;VOICE:123456 +TEL;HOME;VOICE:234567 +TEL;CELL;VOICE:456789 +ADR;WORK:;;test 1;entenh;tirol;789;Albanien +LABEL;WORK;ENCODING=QUOTED-PRINTABLE:test 1=0D=0A= +789 entenh tirol=0D=0A= +Albanien +ADR;HOME;PREF:;;priv 2;sepplh;vorarlberg;213;Anguilla +LABEL;HOME;PREF;ENCODING=QUOTED-PRINTABLE:priv 2=0D=0A= +213 sepplh vorarlberg=0D=0A= +Anguilla +ADR;POSTAL:;;other 4;saadsf;addsag;5649;Brasilien +LABEL;POSTAL;ENCODING=QUOTED-PRINTABLE:other 4=0D=0A= +saadsf - addsag=0D=0A= +Brasilien=0D=0A= +5649 +X-MS-OL-DEFAULT-POSTAL-ADDRESS:1 +URL;WORK:www.google.at +EMAIL;PREF;INTERNET:email1@at.e +EMAIL;INTERNET:email2@at.e +EMAIL;INTERNET:email3@at.e +PHOTO;TYPE=JPEG;ENCODING=BASE64: + /9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAMCAgICAgMCAgIDAwMDBAYEBAQEBAgGBgUGCQgK + CgkICQkKDA8MCgsOCwkJDRENDg8QEBEQCgwSExIQEw8QEBD/2wBDAQMDAwQDBAgEBAgQCwkL + EBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBD/wAAR + CAE6AToDASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAA + AgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkK + FhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWG + h4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl + 5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREA + AgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYk + NOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOE + hYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk + 5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwD5+m0rBwBg5qhNp8yngj8K799K3jOBz2xV + ebQXc5VP0r4eNdLc/G5Ybm1Rwf2aVRhlPFKIWxgiu0bw+2eV5+lVrjQnTlV/KtFWTOaWHlA5 + B4yjHAqPOOtdFNosgPKVRudMZAcjH4Vqqi2OZp31Mrfk4FOVsMM1K8BVsY/CnR2rEZOarmQJ + DlQhQ2epweKejBRjNPCAIBxSbCQcHPNDYcutkTW7ljsJrTgwQD6Gs22jKt05rYs7dnAIH4VE + pFKnIfGApyo61ZRnByD+VTR2DZyVxmriWJYYIrmlNdTojSdhdPZtwLN7DntXfaDMgRZHbqAO + tcNFbtGQMVv6TdtEQrNgVz1LNaHTh3KnO7PUdFlimY8jriunhhBQYPavMNI1lopwsbcZrvLL + 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 + XX3YBDZA71mlVz90flVRbQToxZ//2Q== + +X-MS-OL-DESIGN;CHARSET=utf-8: +REV:20130520T141721Z +END:VCARD diff --git a/php/vcf/class.vCard.php b/php/vcf/class.vCard.php new file mode 100644 index 0000000..3b30e7e --- /dev/null +++ b/php/vcf/class.vCard.php @@ -0,0 +1,690 @@ + false + ); + + /** + * @var array Internal data container. Contains vCard objects for multiple vCards and just the data for single vCards. + */ + private $Data = array(); + + /** + * @static Parts of structured elements according to the spec. + */ + private static $Spec_StructuredElements = array( + 'n' => array('LastName', 'FirstName', 'AdditionalNames', 'Prefixes', 'Suffixes'), + 'adr' => array('POBox', 'ExtendedAddress', 'StreetAddress', 'Locality', 'Region', 'PostalCode', 'Country'), + 'geo' => array('Latitude', 'Longitude'), + 'org' => array('Name', 'Unit1', 'Unit2') + ); + private static $Spec_MultipleValueElements = array('nickname', 'categories'); + + private static $Spec_ElementTypes = array( + 'email' => array('internet', 'x400', 'pref'), + 'adr' => array('dom', 'intl', 'postal', 'parcel', 'home', 'work', 'pref'), + 'label' => array('dom', 'intl', 'postal', 'parcel', 'home', 'work', 'pref'), + 'tel' => array('home', 'msg', 'work', 'pref', 'voice', 'fax', 'cell', 'video', 'pager', 'bbs', 'modem', 'car', 'isdn', 'pcs'), + 'impp' => array('personal', 'business', 'home', 'work', 'mobile', 'pref') + ); + + private static $Spec_FileElements = array('photo', 'logo', 'sound'); + + /** + * vCard constructor + * + * @param string Path to file, optional. + * @param string Raw data, optional. + * @param array Additional options, optional. Currently supported options: + * bool Collapse: If true, elements that can have multiple values but have only a single value are returned as that value instead of an array + * If false, an array is returned even if it has only one value. + * + * One of these parameters must be provided, otherwise an exception is thrown. + */ + public function __construct($Path = false, $RawData = false, array $Options = null) + { + // Checking preconditions for the parser. + // If path is given, the file should be accessible. + // If raw data is given, it is taken as it is. + // In both cases the real content is put in $this -> RawData + if ($Path) + { + if (!is_readable($Path)) + { + throw new Exception('vCard: Path not accessible ('.$Path.')'); + } + + $this -> Path = $Path; + $this -> RawData = file_get_contents($this -> Path); + } + elseif ($RawData) + { + $this -> RawData = $RawData; + } + else + { + //throw new Exception('vCard: No content provided'); + // Not necessary anymore as possibility to create vCards is added + } + + if (!$this -> Path && !$this -> RawData) + { + return true; + } + + if ($Options) + { + $this -> Options = array_merge($this -> Options, $Options); + } + + // Counting the begin/end separators. If there aren't any or the count doesn't match, there is a problem with the file. + // If there is only one, this is a single vCard, if more, multiple vCards are combined. + $Matches = array(); + $vCardBeginCount = preg_match_all('{^BEGIN\:VCARD}miS', $this -> RawData, $Matches); + $vCardEndCount = preg_match_all('{^END\:VCARD}miS', $this -> RawData, $Matches); + + if (($vCardBeginCount != $vCardEndCount) || !$vCardBeginCount) + { + $this -> Mode = vCard::MODE_ERROR; + throw new Exception('vCard: invalid vCard'); + } + + $this -> Mode = $vCardBeginCount == 1 ? vCard::MODE_SINGLE : vCard::MODE_MULTIPLE; + + // Removing/changing inappropriate newlines, i.e., all CRs or multiple newlines are changed to a single newline + $this -> RawData = str_replace("\r", "\n", $this -> RawData); + $this -> RawData = preg_replace('{(\n+)}', "\n", $this -> RawData); + + // In multiple card mode the raw text is split at card beginning markers and each + // fragment is parsed in a separate vCard object. + if ($this -> Mode == self::MODE_MULTIPLE) + { + $this -> RawData = explode('BEGIN:VCARD', $this -> RawData); + $this -> RawData = array_filter($this -> RawData); + + foreach ($this -> RawData as $SinglevCardRawData) + { + // Prepending "BEGIN:VCARD" to the raw string because we exploded on that one. + // If there won't be the BEGIN marker in the new object, it will fail. + $SinglevCardRawData = 'BEGIN:VCARD'."\n".$SinglevCardRawData; + + $ClassName = get_class($this); + $this -> Data[] = new $ClassName(false, $SinglevCardRawData); + } + } + else + { + // Protect the BASE64 final = sign (detected by the line beginning with whitespace), otherwise the next replace will get rid of it + $this -> RawData = preg_replace('{(\n\s.+)=(\n)}', '$1-base64=-$2', $this -> RawData); + + // Joining multiple lines that are split with a hard wrap and indicated by an equals sign at the end of line + // (quoted-printable-encoded values in v2.1 vCards) + $this -> RawData = str_replace("=\n", '', $this -> RawData); + + // Joining multiple lines that are split with a soft wrap (space or tab on the beginning of the next line + $this -> RawData = str_replace(array("\n ", "\n\t"), '-wrap-', $this -> RawData); + + // Restoring the BASE64 final equals sign (see a few lines above) + $this -> RawData = str_replace("-base64=-\n", "=\n", $this -> RawData); + + $Lines = explode("\n", $this -> RawData); + + foreach ($Lines as $Line) + { + // Lines without colons are skipped because, most likely, they contain no data. + if (strpos($Line, ':') === false) + { + continue; + } + + // Each line is split into two parts. The key contains the element name and additional parameters, if present, + // value is just the value + list($Key, $Value) = explode(':', $Line, 2); + + // Key is transformed to lowercase because, even though the element and parameter names are written in uppercase, + // it is quite possible that they will be in lower- or mixed case. + // The key is trimmed to allow for non-significant WSP characters as allowed by v2.1 + $Key = strtolower(trim(self::Unescape($Key))); + + // These two lines can be skipped as they aren't necessary at all. + if ($Key == 'begin' || $Key == 'end') + { + continue; + } + + if ((strpos($Key, 'agent') === 0) && (stripos($Value, 'begin:vcard') !== false)) + { + $ClassName = get_class($this); + $Value = new $ClassName(false, str_replace('-wrap-', "\n", $Value)); + if (!isset($this -> Data[$Key])) + { + $this -> Data[$Key] = array(); + } + $this -> Data[$Key][] = $Value; + continue; + } + else + { + $Value = str_replace('-wrap-', '', $Value); + } + + $Value = trim(self::Unescape($Value)); + $Type = array(); + + // Here additional parameters are parsed + $KeyParts = explode(';', $Key); + $Key = $KeyParts[0]; + $Encoding = false; + + if (strpos($Key, 'item') === 0) + { + $TmpKey = explode('.', $Key, 2); + $Key = $TmpKey[1]; + $ItemIndex = (int)str_ireplace('item', '', $TmpKey[0]); + } + + if (count($KeyParts) > 1) + { + $Parameters = self::ParseParameters($Key, array_slice($KeyParts, 1)); + + foreach ($Parameters as $ParamKey => $ParamValue) + { + switch ($ParamKey) + { + case 'encoding': + $Encoding = $ParamValue; + if (in_array($ParamValue, array('b', 'base64'))) + { + //$Value = base64_decode($Value); + } + elseif ($ParamValue == 'quoted-printable') // v2.1 + { + $Value = quoted_printable_decode($Value); + } + break; + case 'charset': // v2.1 + if ($ParamValue != 'utf-8' && $ParamValue != 'utf8') + { + $Value = mb_convert_encoding($Value, 'UTF-8', $ParamValue); + } + break; + case 'type': + $Type = $ParamValue; + break; + } + } + } + + // Checking files for colon-separated additional parameters (Apple's Address Book does this), for example, "X-ABCROP-RECTANGLE" for photos + if (in_array($Key, self::$Spec_FileElements) && isset($Parameters['encoding']) && in_array($Parameters['encoding'], array('b', 'base64'))) + { + // If colon is present in the value, it must contain Address Book parameters + // (colon is an invalid character for base64 so it shouldn't appear in valid files) + if (strpos($Value, ':') !== false) + { + $Value = explode(':', $Value); + $Value = array_pop($Value); + } + } + + // Values are parsed according to their type + if (isset(self::$Spec_StructuredElements[$Key])) + { + $Value = self::ParseStructuredValue($Value, $Key); + if ($Type) + { + $Value['Type'] = $Type; + } + } + else + { + if (in_array($Key, self::$Spec_MultipleValueElements)) + { + $Value = self::ParseMultipleTextValue($Value, $Key); + } + + if ($Type) + { + $Value = array( + 'Value' => $Value, + 'Type' => $Type + ); + } + } + + if (is_array($Value) && $Encoding) + { + $Value['Encoding'] = $Encoding; + } + + if (!isset($this -> Data[$Key])) + { + $this -> Data[$Key] = array(); + } + + $this -> Data[$Key][] = $Value; + } + } + } + + /** + * Magic method to get the various vCard values as object members, e.g. + * a call to $vCard -> N gets the "N" value + * + * @param string Key + * + * @return mixed Value + */ + public function __get($Key) + { + $Key = strtolower($Key); + if (isset($this -> Data[$Key])) + { + if ($Key == 'agent') + { + return $this -> Data[$Key]; + } + elseif (in_array($Key, self::$Spec_FileElements)) + { + $Value = $this -> Data[$Key]; + foreach ($Value as $K => $V) + { + if (stripos($V['Value'], 'uri:') === 0) + { + $Value[$K]['Value'] = substr($V, 4); + $Value[$K]['Encoding'] = 'uri'; + } + } + return $Value; + } + + if ($this -> Options['Collapse'] && is_array($this -> Data[$Key]) && (count($this -> Data[$Key]) == 1)) + { + return $this -> Data[$Key][0]; + } + return $this -> Data[$Key]; + } + elseif ($Key == 'Mode') + { + return $this -> Mode; + } + return array(); + } + + /** + * Saves an embedded file + * + * @param string Key + * @param int Index of the file, defaults to 0 + * @param string Target path where the file should be saved, including the filename + * + * @return bool Operation status + */ + public function SaveFile($Key, $Index = 0, $TargetPath = '') + { + if (!isset($this -> Data[$Key])) + { + return false; + } + if (!isset($this -> Data[$Key][$Index])) + { + return false; + } + + // Returing false if it is an image URL + if (stripos($this -> Data[$Key][$Index]['Value'], 'uri:') === 0) + { + return false; + } + + if (is_writable($TargetPath) || (!file_exists($TargetPath) && is_writable(dirname($TargetPath)))) + { + $RawContent = $this -> Data[$Key][$Index]['Value']; + if (isset($this -> Data[$Key][$Index]['Encoding']) && $this -> Data[$Key][$Index]['Encoding'] == 'b') + { + $RawContent = base64_decode($RawContent); + } + $Status = file_put_contents($TargetPath, $RawContent); + return (bool)$Status; + } + else + { + throw new Exception('vCard: Cannot save file ('.$Key.'), target path not writable ('.$TargetPath.')'); + } + return false; + } + + /** + * Magic method for adding data to the vCard + * + * @param string Key + * @param string Method call arguments. First element is value. + * + * @return vCard Current object for method chaining + */ + public function __call($Key, $Arguments) + { + $Key = strtolower($Key); + + if (!isset($this -> Data[$Key])) + { + $this -> Data[$Key] = array(); + } + + $Value = isset($Arguments[0]) ? $Arguments[0] : false; + + if (count($Arguments) > 1) + { + $Types = array_values(array_slice($Arguments, 1)); + + if (isset(self::$Spec_StructuredElements[$Key]) && + in_array($Arguments[1], self::$Spec_StructuredElements[$Key]) + ) + { + $LastElementIndex = 0; + + if (count($this -> Data[$Key])) + { + $LastElementIndex = count($this -> Data[$Key]) - 1; + } + + if (isset($this -> Data[$Key][$LastElementIndex])) + { + if (empty($this -> Data[$Key][$LastElementIndex][$Types[0]])) + { + $this -> Data[$Key][$LastElementIndex][$Types[0]] = $Value; + } + else + { + $LastElementIndex++; + } + } + + if (!isset($this -> Data[$Key][$LastElementIndex])) + { + $this -> Data[$Key][$LastElementIndex] = array( + $Types[0] => $Value + ); + } + } + elseif (isset(self::$Spec_ElementTypes[$Key])) + { + $this -> Data[$Key][] = array( + 'Value' => $Value, + 'Type' => $Types + ); + } + } + elseif ($Value) + { + $this -> Data[$Key][] = $Value; + } + + return $this; + } + + /** + * Magic method for getting vCard content out + * + * @return string Raw vCard content + */ + public function __toString() + { + $Text = 'BEGIN:VCARD'.self::endl; + $Text .= 'VERSION:3.0'.self::endl; + + foreach ($this -> Data as $Key => $Values) + { + $KeyUC = strtoupper($Key); + $Key = strtolower($Key); + + if (in_array($KeyUC, array('PHOTO', 'VERSION'))) + { + continue; + } + + foreach ($Values as $Index => $Value) + { + $Text .= $KeyUC; + if (is_array($Value) && isset($Value['Type'])) + { + $Text .= ';TYPE='.self::PrepareTypeStrForOutput($Value['Type']); + } + $Text .= ':'; + + if (isset(self::$Spec_StructuredElements[$Key])) + { + $PartArray = array(); + foreach (self::$Spec_StructuredElements[$Key] as $Part) + { + $PartArray[] = isset($Value[$Part]) ? $Value[$Part] : ''; + } + $Text .= implode(';', $PartArray); + } + elseif (is_array($Value) && isset(self::$Spec_ElementTypes[$Key])) + { + $Text .= $Value['Value']; + } + else + { + $Text .= $Value; + } + + $Text .= self::endl; + } + } + + $Text .= 'END:VCARD'.self::endl; + return $Text; + } + + // !Helper methods + + private static function PrepareTypeStrForOutput($Type) + { + return implode(',', array_map('strtoupper', $Type)); + } + + /** + * Removes the escaping slashes from the text. + * + * @access private + * + * @param string Text to prepare. + * + * @return string Resulting text. + */ + private static function Unescape($Text) + { + return str_replace(array('\:', '\;', '\,', "\n"), array(':', ';', ',', ''), $Text); + } + + /** + * Separates the various parts of a structured value according to the spec. + * + * @access private + * + * @param string Raw text string + * @param string Key (e.g., N, ADR, ORG, etc.) + * + * @return array Parts in an associative array. + */ + private static function ParseStructuredValue($Text, $Key) + { + $Text = array_map('trim', explode(';', $Text)); + + $Result = array(); + $Ctr = 0; + + foreach (self::$Spec_StructuredElements[$Key] as $Index => $StructurePart) + { + $Result[$StructurePart] = isset($Text[$Index]) ? $Text[$Index] : null; + } + return $Result; + } + + /** + * @access private + */ + private static function ParseMultipleTextValue($Text) + { + return explode(',', $Text); + } + + /** + * @access private + */ + private static function ParseParameters($Key, array $RawParams = null) + { + if (!$RawParams) + { + return array(); + } + + // Parameters are split into (key, value) pairs + $Parameters = array(); + foreach ($RawParams as $Item) + { + $Parameters[] = explode('=', strtolower($Item)); + } + + $Type = array(); + $Result = array(); + + // And each parameter is checked whether anything can/should be done because of it + foreach ($Parameters as $Index => $Parameter) + { + // Skipping empty elements + if (!$Parameter) + { + continue; + } + + // Handling type parameters without the explicit TYPE parameter name (2.1 valid) + if (count($Parameter) == 1) + { + // Checks if the type value is allowed for the specific element + // The second part of the "if" statement means that email elements can have non-standard types (see the spec) + if ( + (isset(self::$Spec_ElementTypes[$Key]) && in_array($Parameter[0], self::$Spec_ElementTypes[$Key])) || + ($Key == 'email' && is_scalar($Parameter[0])) + ) + { + $Type[] = $Parameter[0]; + } + } + elseif (count($Parameter) > 2) + { + $TempTypeParams = self::ParseParameters($Key, explode(',', $RawParams[$Index])); + if ($TempTypeParams['type']) + { + $Type = array_merge($Type, $TempTypeParams['type']); + } + } + else + { + switch ($Parameter[0]) + { + case 'encoding': + if (in_array($Parameter[1], array('quoted-printable', 'b', 'base64'))) + { + $Result['encoding'] = $Parameter[1] == 'base64' ? 'b' : $Parameter[1]; + } + break; + case 'charset': + $Result['charset'] = $Parameter[1]; + break; + case 'type': + $Type = array_merge($Type, explode(',', $Parameter[1])); + break; + case 'value': + if (strtolower($Parameter[1]) == 'url') + { + $Result['encoding'] = 'uri'; + } + break; + } + } + } + + $Result['type'] = $Type; + + return $Result; + } + + // !Interface methods + + // Countable interface + public function count() + { + switch ($this -> Mode) + { + case self::MODE_ERROR: + return 0; + break; + case self::MODE_SINGLE: + return 1; + break; + case self::MODE_MULTIPLE: + return count($this -> Data); + break; + } + return 0; + } + + // Iterator interface + public function rewind() + { + reset($this -> Data); + } + + public function current() + { + return current($this -> Data); + } + + public function next() + { + return next($this -> Data); + } + + public function valid() + { + return ($this -> current() !== false); + } + + public function key() + { + return key($this -> Data); + } +} +?> \ No newline at end of file diff --git a/php/vcf/test.php b/php/vcf/test.php new file mode 100644 index 0000000..ecc8737 --- /dev/null +++ b/php/vcf/test.php @@ -0,0 +1,216 @@ + + + + + + + + +'.$vCard -> FN[0].''; + + if ($vCard -> PHOTO) + { + foreach ($vCard -> PHOTO as $Photo) + { + if ($Photo['Encoding'] == 'b') + { + echo '
'; + } + else + { + echo '
'; + } + + /* + // It can also be saved to a file + try + { + $vCard -> SaveFile('photo', 0, 'test_image.jpg'); + // The parameters are: + // - name of the file we want to save (photo, logo or sound) + // - index of the file in case of multiple files (defaults to 0) + // - target path to save to, including the filenam + } + catch (Exception $E) + { + // Target path not writable + } + */ + } + } + + foreach ($vCard -> N as $Name) + { + echo '

Name: '.$Name['FirstName'].' '.$Name['LastName'].'

'; + } + + foreach ($vCard -> ORG as $Organization) + { + echo '

Organization: '.$Organization['Name']. + ($Organization['Unit1'] || $Organization['Unit2'] ? + ' ('.implode(', ', array($Organization['Unit1'], $Organization['Unit2'])).')' : + '' + ).'

'; + } + + if ($vCard -> TEL) + { + echo '

Phone

'; + foreach ($vCard -> TEL as $Tel) + { + if (is_scalar($Tel)) + { + echo $Tel.'
'; + } + else + { + echo $Tel['Value'].' ('.implode(', ', $Tel['Type']).')
'; + } + } + echo '

'; + } + + if ($vCard -> EMAIL) + { + echo '

Email

'; + foreach ($vCard -> EMAIL as $Email) + { + if (is_scalar($Email)) + { + echo $Email; + } + else + { + echo $Email['Value'].' ('.implode(', ', $Email['Type']).')
'; + } + } + echo '

'; + } + + if ($vCard -> URL) + { + echo '

URL

'; + foreach ($vCard -> URL as $URL) + { + if (is_scalar($URL)) + { + echo $URL.'
'; + } + else + { + echo $URL['Value'].'
'; + } + } + echo '

'; + } + + if ($vCard -> IMPP) + { + echo '

Instant messaging

'; + foreach ($vCard -> IMPP as $IMPP) + { + if (is_scalar($IMPP)) + { + echo $IMPP.'
'; + } + else + { + echo $IMPP['Value'].'
'; + } + } + echo '

'; + } + + if ($vCard -> ADR) + { + foreach ($vCard -> ADR as $Address) + { + echo '

Address ('.implode(', ', $Address['Type']).')

'; + echo 'Street address: '.($Address['StreetAddress'] ? $Address['StreetAddress'] : '-').'
'. + 'PO Box: '.($Address['POBox'] ? $Address['POBox'] : '-').'
'. + 'Extended address: '.($Address['ExtendedAddress'] ? $Address['ExtendedAddress'] : '-').'
'. + 'Locality: '.($Address['Locality'] ? $Address['Locality'] : '-').'
'. + 'Region: '.($Address['Region'] ? $Address['Region'] : '-').'
'. + 'ZIP/Post code: '.($Address['PostalCode'] ? $Address['PostalCode'] : '-').'
'. + 'Country: '.($Address['Country'] ? $Address['Country'] : '-').''; + } + echo '

'; + } + + if ($vCard -> AGENT) + { + echo '

Agents

'; + foreach ($vCard -> AGENT as $Agent) + { + if (is_scalar($Agent)) + { + echo '
'.$Agent.'
'; + } + elseif (is_a($Agent, 'vCard')) + { + echo '
'; + OutputvCard($Agent); + echo '
'; + } + } + } + } + + $vCard = new vCard( + 'test.vcf', // Path to vCard file + false, // Raw vCard text, can be used instead of a file + array( // Option array + // This lets you get single values for elements that could contain multiple values but have only one value. + // This defaults to false so every value that could have multiple values is returned as array. + 'Collapse' => false + ) + ); + + if (count($vCard) == 0) + { + throw new Exception('vCard test: empty vCard!'); + } + // if the file contains a single vCard, it is accessible directly. + elseif (count($vCard) == 1) + { + OutputvCard($vCard); + } + // if the file contains multiple vCards, they are accessible as elements of an array + else + { + foreach ($vCard as $Index => $vCardPart) + { + OutputvCard($vCardPart); + } + } +?> + + \ No newline at end of file diff --git a/resources/css/contactimporter-main.css b/resources/css/contactimporter-main.css new file mode 100644 index 0000000..b92327f --- /dev/null +++ b/resources/css/contactimporter-main.css @@ -0,0 +1,6 @@ +.icon_contactimporter_button { + background: url(../images/import_icon.png) no-repeat !important; + background-repeat: no-repeat; + background-position: center; + background-size: 18px!important; +} \ No newline at end of file diff --git a/resources/images/import_icon.png b/resources/images/import_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..1588aedad9a626da151f7b9bbe02a33151a393dd GIT binary patch literal 926 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