From 2118320a5d2c926294122abf8d4462e4bdacefe3 Mon Sep 17 00:00:00 2001 From: Christoph Haas Date: Mon, 13 Jun 2016 10:18:47 +0200 Subject: [PATCH 01/16] starting changes for v2 --- .idea/vcs.xml | 6 ++ build.xml | 201 +++++++++++------------------------ config.php | 2 +- js/dialogs/ImportPanel.js | 14 +-- js/plugin.contactimporter.js | 34 +++--- manifest.xml | 3 +- php/vcf/class.vCard.php | 119 ++++++++++++++------- 7 files changed, 178 insertions(+), 201 deletions(-) create mode 100644 .idea/vcs.xml diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/build.xml b/build.xml index 687da16..c47e16b 100644 --- a/build.xml +++ b/build.xml @@ -1,18 +1,15 @@ - - - - - - - + + + + + - @@ -21,30 +18,8 @@ - - - - - - - - - - - - - - - - - - - - - - @@ -54,8 +29,7 @@ - - + @@ -68,16 +42,6 @@ - - - - - - - - - - @@ -89,19 +53,11 @@ - - - - - - - - - + @@ -110,8 +66,8 @@ - - + + @@ -155,16 +111,7 @@ var npgettext = function(msgctxt, msgid, msgid_plural, count) {}; var pgettext = function(msgctxt, msgid) {}; - - + @@ -172,105 +119,81 @@ - + - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + - + - - - - Processing manifest.xml - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + - + + - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/config.php b/config.php index f8c526e..29ed52f 100644 --- a/config.php +++ b/config.php @@ -2,7 +2,7 @@ /** Disable the import plugin for all clients */ define('PLUGIN_CONTACTIMPORTER_USER_DEFAULT_ENABLE', false); /** Disable the export feature for all clients */ - define('PLUGIN_CONTACTIMPORTER_USER_DEFAULT_ENABLE_EXPORT', false); // currently not available + define('PLUGIN_CONTACTIMPORTER_USER_DEFAULT_ENABLE_EXPORT', false); /** The default addressbook to import to (default: contact)*/ define('PLUGIN_CONTACTIMPORTER_DEFAULT', "contact"); diff --git a/js/dialogs/ImportPanel.js b/js/dialogs/ImportPanel.js index c58fbe5..0840159 100644 --- a/js/dialogs/ImportPanel.js +++ b/js/dialogs/ImportPanel.js @@ -47,7 +47,7 @@ Zarafa.plugins.contactimporter.dialogs.ImportPanel = Ext.extend(Ext.Panel, { config = config || {}; var self = this; - if(typeof config.filename !== "undefined") { + if(!Ext.isEmpty(config.filename)) { this.vcffile = config.filename; } @@ -146,13 +146,13 @@ Zarafa.plugins.contactimporter.dialogs.ImportPanel = Ext.extend(Ext.Panel, { var i = 0; for(i = 0; i < contactdata.contacts.length; i++) { - parsedData[i] = new Array( + parsedData[i] = [ 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; @@ -202,10 +202,10 @@ Zarafa.plugins.contactimporter.dialogs.ImportPanel = Ext.extend(Ext.Panel, { /* add all local contact folders */ var i = 0; - myStore.push(new Array(defaultFolder.getDefaultFolderKey(), defaultFolder.getDisplayName())); + myStore.push([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 + myStore.push([subFolders[i].getDisplayName(), subFolders[i].getDisplayName(), false]); // 3rd field = isPublicfolder } /* add all shared contact folders */ @@ -217,7 +217,7 @@ Zarafa.plugins.contactimporter.dialogs.ImportPanel = Ext.extend(Ext.Panel, { 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 + myStore.push([pubSubFolders[i].getDisplayName(), pubSubFolders[i].getDisplayName() + " [Shared]", true]); // 3rd field = isPublicfolder } } } catch (e) { @@ -455,7 +455,7 @@ Zarafa.plugins.contactimporter.dialogs.ImportPanel = Ext.extend(Ext.Panel, { if(addressbookexist) { this.loadMask.show(); - var uids = new Array(); + var uids = []; var store_entryid = ""; //receive Records from grid rows diff --git a/js/plugin.contactimporter.js b/js/plugin.contactimporter.js index aba36a7..c36a696 100644 --- a/js/plugin.contactimporter.js +++ b/js/plugin.contactimporter.js @@ -47,6 +47,7 @@ Zarafa.plugins.contactimporter.ImportPlugin = Ext.extend(Zarafa.core.Plugin, { /* 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); }, @@ -60,7 +61,6 @@ Zarafa.plugins.contactimporter.ImportPlugin = Ext.extend(Zarafa.core.Plugin, { createImportButton: function () { var button = { xtype : 'button', - id : "importcontactsbutton", text : _('Import Contacts'), iconCls : 'icon_contactimporter_button', navigationContext : container.getContextByName('contact'), @@ -90,9 +90,9 @@ Zarafa.plugins.contactimporter.ImportPlugin = Ext.extend(Zarafa.core.Plugin, { var extension = record.data.name.split('.').pop().toLowerCase(); if(record.data.filetype == "text/vcard" || extension == "vcf" || extension == "vcard") { - item.setDisabled(false); + item.setVisible(false); } else { - item.setDisabled(true); + item.setVisible(true); } } }; @@ -103,10 +103,7 @@ Zarafa.plugins.contactimporter.ImportPlugin = Ext.extend(Zarafa.core.Plugin, { */ 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 - }); + this.openImportDialog(response.tmpname); } else { Zarafa.common.dialogs.MessageBox.show({ title : _('Error'), @@ -182,12 +179,23 @@ Zarafa.plugins.contactimporter.ImportPlugin = Ext.extend(Zarafa.core.Plugin, { * 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 - }); + this.openImportDialog(); }, - + + /** + * Open the import dialog. + * + * @param {String} filename + */ + openImportDialog: function(filename) { + var componentType = Zarafa.core.data.SharedComponentType['plugins.contactimporter.dialogs.importcontacts']; + var config = { + filename: filename + }; + + Zarafa.core.data.UIFactory.openLayerComponent(componentType, undefined, config); + }, + /** * Bid for the type of shared component * and the given record. @@ -200,7 +208,7 @@ Zarafa.plugins.contactimporter.ImportPlugin = Ext.extend(Zarafa.core.Plugin, { var bid = -1; switch(type) { case Zarafa.core.data.SharedComponentType['plugins.contactimporter.dialogs.importcontacts']: - bid = 2; + bid = 1; break; } return bid; diff --git a/manifest.xml b/manifest.xml index bcd1b92..acafc6f 100644 --- a/manifest.xml +++ b/manifest.xml @@ -2,7 +2,7 @@ - @_@PLUGIN_VERSION@_@ + 2.0.0 contactimporter VCF Contact Importer/Exporter Christoph Haas @@ -24,6 +24,7 @@ js/contactimporter-debug.js js/plugin.contactimporter.js + js/ABOUT.js js/data/ResponseHandler.js js/dialogs/ImportContentPanel.js js/dialogs/ImportPanel.js diff --git a/php/vcf/class.vCard.php b/php/vcf/class.vCard.php index 3b30e7e..bed6891 100644 --- a/php/vcf/class.vCard.php +++ b/php/vcf/class.vCard.php @@ -5,9 +5,8 @@ * @link https://github.com/nuovo/vCard-parser * @author Martins Pilsetnieks, Roberts Bruveris * @see RFC 2426, RFC 2425 - * @version 0.4.8 -*/ - + * @version 0.4.9 + */ class vCard implements Countable, Iterator { const MODE_ERROR = 'error'; @@ -42,15 +41,15 @@ class vCard implements Countable, Iterator * @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') + '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'), + 'email' => array('internet', 'x400', 'pref', 'home', 'work'), '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'), @@ -121,21 +120,26 @@ class vCard implements Countable, Iterator $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); + + // MCA: removed, this break crlf vcard specification, all line dilimiter are CRLF + //$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); + //Cannot use "explode", because we need to ignore, for example, 'AGENT:BEGIN:VCARD' + $this -> RawData = preg_split('{^BEGIN\:VCARD}miS', $this -> RawData); $this -> RawData = array_filter($this -> RawData); foreach ($this -> RawData as $SinglevCardRawData) { + // mca: remove \n and \r at start + //$SinglevCardRawData=ltrim($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; + $SinglevCardRawData = 'BEGIN:VCARD'.$SinglevCardRawData; $ClassName = get_class($this); $this -> Data[] = new $ClassName(false, $SinglevCardRawData); @@ -143,20 +147,20 @@ class vCard implements Countable, Iterator } 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); + $this -> RawData = str_replace("=\r\n", '', $this -> RawData); + + // 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('{(\r\n\s.+)=(\r\n)}', '$1-base64=-$2', $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); + $this -> RawData = str_replace(array("\r\n ", "\r\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); + $this -> RawData = str_replace("-base64=-\r\n", "=\r\n", $this -> RawData); - $Lines = explode("\n", $this -> RawData); + $Lines = explode("\r\n", $this -> RawData); foreach ($Lines as $Line) { @@ -212,6 +216,7 @@ class vCard implements Countable, Iterator $ItemIndex = (int)str_ireplace('item', '', $TmpKey[0]); } + if (count($KeyParts) > 1) { $Parameters = self::ParseParameters($Key, array_slice($KeyParts, 1)); @@ -262,7 +267,7 @@ class vCard implements Countable, Iterator $Value = self::ParseStructuredValue($Value, $Key); if ($Type) { - $Value['Type'] = $Type; + $Value['type'] = $Type; } } else @@ -275,15 +280,15 @@ class vCard implements Countable, Iterator if ($Type) { $Value = array( - 'Value' => $Value, - 'Type' => $Type + 'value' => $Value, + 'type' => $Type ); } } if (is_array($Value) && $Encoding) { - $Value['Encoding'] = $Encoding; + $Value['encoding'] = $Encoding; } if (!isset($this -> Data[$Key])) @@ -296,6 +301,22 @@ class vCard implements Countable, Iterator } } + /** + * method to get key list of the current vcard + * + * @return array list of key + */ + public function getKeyList() + { + $keylist=array(); + if (isset($this -> Data)) + { + foreach($this -> Data as $key => $val) + $keylist[]=$key; + } + return $keylist; + } + /** * Magic method to get the various vCard values as object members, e.g. * a call to $vCard -> N gets the "N" value @@ -318,10 +339,10 @@ class vCard implements Countable, Iterator $Value = $this -> Data[$Key]; foreach ($Value as $K => $V) { - if (stripos($V['Value'], 'uri:') === 0) + if (isset($V['Value']) && stripos($V['Value'], 'uri:') === 0) { - $Value[$K]['Value'] = substr($V, 4); - $Value[$K]['Encoding'] = 'uri'; + $Value[$K]['value'] = substr($V, 4); + $Value[$K]['encoding'] = 'uri'; } } return $Value; @@ -340,6 +361,20 @@ class vCard implements Countable, Iterator return array(); } + /** + * Magic method to check isset for the various vCard values as object members, e.g. + * a call to isset( $vCard -> fn ) checks existence of a value. + * + * @param string Key + * + * @return bool isset + */ + public function __isset($Key) { + $Key = strtolower($Key); + $val = $this->$Key; + return isset($val); + } + /** * Saves an embedded file * @@ -361,15 +396,15 @@ class vCard implements Countable, Iterator } // Returing false if it is an image URL - if (stripos($this -> Data[$Key][$Index]['Value'], 'uri:') === 0) + 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 = $this -> Data[$Key][$Index]['value']; + if (isset($this -> Data[$Key][$Index]['encoding']) && $this -> Data[$Key][$Index]['encoding'] == 'b') { $RawContent = base64_decode($RawContent); } @@ -404,10 +439,10 @@ class vCard implements Countable, Iterator if (count($Arguments) > 1) { - $Types = array_values(array_slice($Arguments, 1)); + $Types = array_map('strtolower', array_values(array_slice($Arguments, 1))); if (isset(self::$Spec_StructuredElements[$Key]) && - in_array($Arguments[1], self::$Spec_StructuredElements[$Key]) + in_array(strtolower($Arguments[1]), self::$Spec_StructuredElements[$Key]) ) { $LastElementIndex = 0; @@ -439,8 +474,8 @@ class vCard implements Countable, Iterator elseif (isset(self::$Spec_ElementTypes[$Key])) { $this -> Data[$Key][] = array( - 'Value' => $Value, - 'Type' => $Types + 'value' => $Value, + 'type' => $Types ); } } @@ -475,9 +510,9 @@ class vCard implements Countable, Iterator foreach ($Values as $Index => $Value) { $Text .= $KeyUC; - if (is_array($Value) && isset($Value['Type'])) + if (is_array($Value) && isset($Value['type'])) { - $Text .= ';TYPE='.self::PrepareTypeStrForOutput($Value['Type']); + $Text .= ';TYPE='.self::PrepareTypeStrForOutput($Value['type']); } $Text .= ':'; @@ -492,7 +527,7 @@ class vCard implements Countable, Iterator } elseif (is_array($Value) && isset(self::$Spec_ElementTypes[$Key])) { - $Text .= $Value['Value']; + $Text .= $Value['value']; } else { @@ -574,7 +609,8 @@ class vCard implements Countable, Iterator $Parameters = array(); foreach ($RawParams as $Item) { - $Parameters[] = explode('=', strtolower($Item)); + // try to correct issue https://github.com/nuovo/vCard-parser/issues/20 + $Parameters[] = explode('=', strtolower($Item),2); } $Type = array(); @@ -604,10 +640,13 @@ class vCard implements Countable, Iterator } elseif (count($Parameter) > 2) { - $TempTypeParams = self::ParseParameters($Key, explode(',', $RawParams[$Index])); - if ($TempTypeParams['type']) + if(count(explode(',', $RawParams[$Index], -1)) > 0) { - $Type = array_merge($Type, $TempTypeParams['type']); + $TempTypeParams = self::ParseParameters($Key, explode(',', $RawParams[$Index])); + if ($TempTypeParams['type']) + { + $Type = array_merge($Type, $TempTypeParams['type']); + } } } else From 7fd75b3efdf3b7abbe06ac0b24568cb8af9326fc Mon Sep 17 00:00:00 2001 From: Christoph Haas Date: Mon, 13 Jun 2016 13:41:52 +0200 Subject: [PATCH 02/16] using new vcard parser --- .idea/contactimporter.iml | 18 + build.xml | 1 + js/dialogs/ImportPanel.js | 10 - js/plugin.contactimporter.js | 13 +- manifest.xml | 4 +- php/.gitignore | 5 + php/composer.json | 5 + php/module.contact.php | 291 +++++++------- php/vcf/class.vCard.php | 729 ----------------------------------- 9 files changed, 191 insertions(+), 885 deletions(-) create mode 100644 .idea/contactimporter.iml create mode 100644 php/.gitignore create mode 100644 php/composer.json delete mode 100644 php/vcf/class.vCard.php diff --git a/.idea/contactimporter.iml b/.idea/contactimporter.iml new file mode 100644 index 0000000..0c76797 --- /dev/null +++ b/.idea/contactimporter.iml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/build.xml b/build.xml index c47e16b..dba3f1c 100644 --- a/build.xml +++ b/build.xml @@ -135,6 +135,7 @@ + diff --git a/js/dialogs/ImportPanel.js b/js/dialogs/ImportPanel.js index 0840159..44db926 100644 --- a/js/dialogs/ImportPanel.js +++ b/js/dialogs/ImportPanel.js @@ -94,15 +94,6 @@ Zarafa.plugins.contactimporter.dialogs.ImportPanel = Ext.extend(Ext.Panel, { 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 } }); @@ -486,7 +477,6 @@ Zarafa.plugins.contactimporter.dialogs.ImportPanel = Ext.extend(Ext.Panel, { }, importContactsDone : function (response) { - console.log(response); this.loadMask.hide(); this.dialog.close(); if(response.status == true) { diff --git a/js/plugin.contactimporter.js b/js/plugin.contactimporter.js index c36a696..f4588d5 100644 --- a/js/plugin.contactimporter.js +++ b/js/plugin.contactimporter.js @@ -83,16 +83,16 @@ Zarafa.plugins.contactimporter.ImportPlugin = Ext.extend(Zarafa.core.Plugin, { createAttachmentImportButton : function(include, btn) { return { text : _('Import Contacts'), - handler : this.getAttachmentFileName.createDelegate(this, [btn, this.gotAttachmentFileName]), + handler : this.getAttachmentFileName.createDelegate(this, [btn]), 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.setVisible(false); - } else { + if(record.data.filetype == "text/vcard" || record.data.filetype == "text/x-vcard" || extension == "vcf" || extension == "vcard") { item.setVisible(true); + } else { + item.setVisible(false); } } }; @@ -117,7 +117,7 @@ Zarafa.plugins.contactimporter.ImportPlugin = Ext.extend(Zarafa.core.Plugin, { /** * Clickhandler for the button */ - getAttachmentFileName: function (btn, callback) { + getAttachmentFileName: function (btn) { Zarafa.common.dialogs.MessageBox.show({ title: 'Please wait', msg: 'Loading attachment...', @@ -157,7 +157,8 @@ Zarafa.plugins.contactimporter.ImportPlugin = Ext.extend(Zarafa.core.Plugin, { var filename = attachmentRecord.data.name; var responseHandler = new Zarafa.plugins.contactimporter.data.ResponseHandler({ - successCallback: callback + successCallback: this.gotAttachmentFileName.createDelegate(this), + scope: this }); // request attachment preperation diff --git a/manifest.xml b/manifest.xml index acafc6f..e01248d 100644 --- a/manifest.xml +++ b/manifest.xml @@ -20,7 +20,7 @@ php/module.contact.php - js/contactimporter.js + js/contactimporter-debug.js js/contactimporter-debug.js js/plugin.contactimporter.js @@ -30,7 +30,7 @@ js/dialogs/ImportPanel.js - resources/css/contactimporter-min.css + resources/css/contactimporter.css resources/css/contactimporter.css resources/css/contactimporter-main.css diff --git a/php/.gitignore b/php/.gitignore new file mode 100644 index 0000000..3d60449 --- /dev/null +++ b/php/.gitignore @@ -0,0 +1,5 @@ +composer.phar +.composer.lock +composer.lock +vendor +vendor/* diff --git a/php/composer.json b/php/composer.json new file mode 100644 index 0000000..303bfe5 --- /dev/null +++ b/php/composer.json @@ -0,0 +1,5 @@ +{ + "require": { + "jeroendesloovere/vcard": "1.2.*" + } +} diff --git a/php/module.contact.php b/php/module.contact.php index d7eefd3..fd7e5d0 100644 --- a/php/module.contact.php +++ b/php/module.contact.php @@ -21,9 +21,11 @@ * */ -include_once('vcf/class.vCard.php'); -require_once('mapi/mapitags.php' ); - +include_once('vendor/autoload.php'); + +use JeroenDesloovere\VCard\VCard; +use JeroenDesloovere\VCard\VCardParser; + class ContactModule extends Module { private $DEBUG = false; // enable error_log debugging @@ -142,8 +144,9 @@ class ContactModule extends Module { $error_msg = ""; // parse the vcf file a last time... + $parser = null; try { - $vcard = new vCard($vcffile, false, array('Collapse' => false)); // Parse it! + $parser = VCardParser::parseFromFile($vcffile); } catch (Exception $e) { $error = true; $error_msg = $e->getMessage(); @@ -151,13 +154,8 @@ class ContactModule extends Module { $contacts = array(); - if(!$error && count($vcard) > 0) { - $vCard = $vcard; - if (count($vCard) == 1) { - $vCard = array($vcard); - } - - $contacts = $this->parseContactsToArray($vCard); + if(!$error && iterator_count($parser) > 0) { + $contacts = $this->parseContactsToArray($parser); $store = $GLOBALS["mapisession"]->openMessageStore(hex2bin($storeid)); $folder = mapi_msgstore_openentry($store, hex2bin($folderid)); @@ -172,7 +170,7 @@ class ContactModule extends Module { $count = 0; // iterate through all contacts and import them :) - foreach($contacts as $contact) { + foreach($contacts as $contact) { if (isset($contact["display_name"]) && ($importall || in_array($contact["internal_fields"]["contact_uid"], $uids))) { // parse the arraykeys // TODO: this is very slow... @@ -194,7 +192,7 @@ class ContactModule extends Module { $contactPicture = file_get_contents($contact["internal_fields"]["x_photo_path"]); $attach = mapi_message_createattach($message); - // Set properties of the attachment + // Set properties of the attachment $propValuesIMG = array( PR_ATTACH_SIZE => strlen($contactPicture), PR_ATTACH_LONG_FILENAME => 'ContactPicture.jpg', @@ -224,7 +222,7 @@ class ContactModule extends Module { mapi_savechanges($message); if($this->DEBUG) { error_log("New contact added: \"" . $propValuesMAPI[$properties["display_name"]] . "\".\n"); - } + } $count++; } } @@ -450,8 +448,10 @@ class ContactModule extends Module { $error_msg = ""; if(is_readable ($actionData["vcf_filepath"])) { + $parser = null; + try { - $vcard = new vCard($actionData["vcf_filepath"], false, array('Collapse' => false)); // Parse it! + $parser = VCardParser::parseFromFile($actionData["vcf_filepath"]); } catch (Exception $e) { $error = true; $error_msg = $e->getMessage(); @@ -460,19 +460,14 @@ class ContactModule extends Module { $response['status'] = false; $response['message']= $error_msg; } else { - if(count($vcard) == 0) { + if(iterator_count($parser) == 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) + 'contacts' => $this->parseContactsToArray($parser) ); } } @@ -503,141 +498,156 @@ class ContactModule extends Module { if(!$csv) { foreach ($contacts as $Index => $vCard) { $properties = array(); - $properties["display_name"] = $vCard -> FN[0]; - $properties["fileas"] = $vCard -> FN[0]; + if (isset($vCard->fullname)) { + $properties["display_name"] = $vCard->fullname; + $properties["fileas"] = $vCard->fullname; + } elseif(!isset($vCard->organization)) { + error_log("Skipping entry! No fullname/organization given."); + continue; + } //uid - used for front/backend communication $properties["internal_fields"] = array(); $properties["internal_fields"]["contact_uid"] = base64_encode($Index . $properties["fileas"]); - - 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']; - } + + $properties["given_name"] = $vCard->firstname; + $properties["middle_name"] = $vCard->additional; + $properties["surname"] = $vCard->lastname; + $properties["display_name_prefix"] = $vCard->prefix; + + if (isset($vCard->phone) && count($vCard->phone) > 0) { + foreach ($vCard->phone as $type => $number) { + $number = $number[0]; // we only can store one number + if($this->startswith(strtolower($type), "home") || strtolower($type) === "default") { + $properties["home_telephone_number"] = $number; + } else if($this->startswith(strtolower($type), "cell")) { + $properties["cellular_telephone_number"] = $number; + } else if($this->startswith(strtolower($type), "work")) { + $properties["business_telephone_number"] = $number; + } else if($this->startswith(strtolower($type), "fax")) { + $properties["business_fax_number"] = $number; + } else if($this->startswith(strtolower($type), "pager")) { + $properties["pager_telephone_number"] = $number; + } else if($this->startswith(strtolower($type), "isdn")) { + $properties["isdn_number"] = $number; + } else if($this->startswith(strtolower($type), "car")) { + $properties["car_telephone_number"] = $number; + } else if($this->startswith(strtolower($type), "modem")) { + $properties["ttytdd_telephone_number"] = $number; } } } - if ($vCard -> EMAIL) { - $e=0; - foreach ($vCard -> EMAIL as $Email) { - $fileas = $Email['Value']; + if (isset($vCard->email) && count($vCard->email) > 0) { + $emailcount = 0; + foreach ($vCard->email as $type => $email) { + $email = $email[0]; // we only can store one mail address + $fileas = $email; if(isset($properties["fileas"]) && !empty($properties["fileas"])) { - $fileas = $properties["fileas"]; + $fileas = $properties["fileas"]; // set to real name } - - 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; + + // we only have storage for 3 mail addresses! + switch($emailcount) { + case 0: + $properties["email_address_1"] = $email; + $properties["email_address_display_name_1"] = $fileas . " (" . $email . ")"; + break; + case 1: + $properties["email_address_2"] = $email; + $properties["email_address_display_name_2"] = $fileas . " (" . $email . ")"; + break; + case 2: + $properties["email_address_3"] = $email; + $properties["email_address_display_name_3"] = $fileas . " (" . $email . ")"; + break; + default: break; + } + $emailcount++; + } + } + if (isset($vCard->organization)) { + $properties["company_name"] = $vCard->organization; + if(empty($properties["display_name"])) { + $properties["display_name"] = $vCard->organization; // if we have no displayname - use the company name as displayname + $properties["fileas"] = $vCard->organization; + } + } + if (isset($vCard->title)) { + $properties["title"] = $vCard->title; + } + if (isset($vCard->url) && count($vCard->url) > 0) { + foreach ($vCard->url as $type => $url) { + $url = $url[0]; // only 1 webaddress per type + $properties["webpage"] = $url; + break; // we can only store on url + } + } + if (isset($vCard->address) && count($vCard->address) > 0) { + + foreach ($vCard->address as $type => $address) { + $address = $address[0]; // we only can store one address per type + if($this->startswith(strtolower($type), "work")) { + $properties["business_address_street"] = $address->street; + if(!empty($address->extended)) { + $properties["business_address_street"] .= "\n" . $address->extended; } - $e++; + $properties["business_address_city"] = $address->city; + $properties["business_address_state"] = $address->region; + $properties["business_address_postal_code"] = $address->zip; + $properties["business_address_country"] = $address->country; + $properties["business_address"] = $this->buildAddressString($properties["business_address_street"], $address->zip, $address->city, $address->region, $address->country); + } else if($this->startswith(strtolower($type), "home")) { + $properties["home_address_street"] = $address->street; + if(!empty($address->extended)) { + $properties["home_address_street"] .= "\n" . $address->extended; + } + $properties["home_address_city"] = $address->city; + $properties["home_address_state"] = $address->region; + $properties["home_address_postal_code"] = $address->zip; + $properties["home_address_country"] = $address->country; + $properties["home_address"] = $this->buildAddressString($properties["home_address_street"], $address->zip, $address->city, $address->region, $address->country); + } else { + $properties["other_address_street"] = $address->street; + if(!empty($address->extended)) { + $properties["other_address_street"] .= "\n" . $address->extended; + } + $properties["other_address_city"] = $address->city; + $properties["other_address_state"] = $address->region; + $properties["other_address_postal_code"] = $address->zip; + $properties["other_address_country"] = $address->country; + $properties["other_address"] = $this->buildAddressString($properties["other_address_street"], $address->zip, $address->city, $address->region, $address->country); } } } - 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 (isset($vCard->birthday)) { + $properties["birthday"] = $vCard->birthday->getTimestamp(); } - if ($vCard -> TITLE) { - $title = $vCard -> TITLE[0]; - $properties["title"] = is_array($title) ? $title["Value"] : $title; + if (isset($vCard->note)) { + $properties["notes"] = $vCard->note; } - if ($vCard -> URL) { - $url = $vCard -> URL[0]; // only 1 webaddress - $properties["webpage"] = is_array($url) ? $url["Value"] : $url; - } - 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["notes"] = $vCard -> NOTE[0]; - } - if ($vCard -> PHOTO) { + if (isset($vCard->rawPhoto) || isset($vCard->photo)) { if(!is_writable(TMP_PATH . "/")) { - error_log("could not write to export tmp directory!: " . $E); + error_log("Can not write to export tmp directory!"); } else { $tmppath = TMP_PATH . "/" . $this->randomstring(15); - try { - if($vCard -> SaveFile('photo', 0, $tmppath)) { - $properties["internal_fields"]["x_photo_path"] = $tmppath; - } else { - if($this->DEBUG) { - error_log("remote imagefetching not implemented"); - } + if(isset($vCard->rawPhoto)) { + if(file_put_contents($tmppath, $vCard->rawPhoto)) { + $properties["internal_fields"]["x_photo_path"] = $tmppath; + } + } elseif(isset($vCard->photo)) { + if($this->startswith(strtolower($vCard->photo), "http://") || $this->startswith(strtolower($vCard->photo), "https://")) { // check if it starts with http + $ctx = stream_context_create(array('http'=> + array( + 'timeout' => 3, //3 Seconds timout + ) + )); + + if(file_put_contents($tmppath, file_get_contents($vCard->photo, false, $ctx))) { + $properties["internal_fields"]["x_photo_path"] = $tmppath; + } + } else { + error_log("Invalid photo url: " . $vCard->photo); } - } catch (Exception $E) { - error_log("Image exception: " . $E); } } } @@ -815,6 +825,11 @@ class ContactModule extends Module { $GLOBALS["bus"]->addData($this->getResponseData()); } } + + private function startswith($haystack, $needle) { + $haystack = str_replace("type=", "", $haystack); // remove type from string + return substr($haystack, 0, strlen($needle)) === $needle; + } }; ?> diff --git a/php/vcf/class.vCard.php b/php/vcf/class.vCard.php deleted file mode 100644 index bed6891..0000000 --- a/php/vcf/class.vCard.php +++ /dev/null @@ -1,729 +0,0 @@ - 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', 'home', 'work'), - '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 - - // MCA: removed, this break crlf vcard specification, all line dilimiter are CRLF - //$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) - { - //Cannot use "explode", because we need to ignore, for example, 'AGENT:BEGIN:VCARD' - $this -> RawData = preg_split('{^BEGIN\:VCARD}miS', $this -> RawData); - $this -> RawData = array_filter($this -> RawData); - - foreach ($this -> RawData as $SinglevCardRawData) - { - // mca: remove \n and \r at start - //$SinglevCardRawData=ltrim($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'.$SinglevCardRawData; - - $ClassName = get_class($this); - $this -> Data[] = new $ClassName(false, $SinglevCardRawData); - } - } - else - { - // 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("=\r\n", '', $this -> RawData); - - // 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('{(\r\n\s.+)=(\r\n)}', '$1-base64=-$2', $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("\r\n ", "\r\n\t"), '-wrap-', $this -> RawData); - - // Restoring the BASE64 final equals sign (see a few lines above) - $this -> RawData = str_replace("-base64=-\r\n", "=\r\n", $this -> RawData); - - $Lines = explode("\r\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; - } - } - } - - /** - * method to get key list of the current vcard - * - * @return array list of key - */ - public function getKeyList() - { - $keylist=array(); - if (isset($this -> Data)) - { - foreach($this -> Data as $key => $val) - $keylist[]=$key; - } - return $keylist; - } - - /** - * 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 (isset($V['Value']) && 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(); - } - - /** - * Magic method to check isset for the various vCard values as object members, e.g. - * a call to isset( $vCard -> fn ) checks existence of a value. - * - * @param string Key - * - * @return bool isset - */ - public function __isset($Key) { - $Key = strtolower($Key); - $val = $this->$Key; - return isset($val); - } - - /** - * 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_map('strtolower', array_values(array_slice($Arguments, 1))); - - if (isset(self::$Spec_StructuredElements[$Key]) && - in_array(strtolower($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) - { - // try to correct issue https://github.com/nuovo/vCard-parser/issues/20 - $Parameters[] = explode('=', strtolower($Item),2); - } - - $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) - { - if(count(explode(',', $RawParams[$Index], -1)) > 0) - { - $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 From 838353bf9dd94fc8340b010f7819b9042ea6b4af Mon Sep 17 00:00:00 2001 From: Christoph Haas Date: Mon, 13 Jun 2016 13:51:42 +0200 Subject: [PATCH 03/16] fix address book not updated bug --- php/module.contact.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/php/module.contact.php b/php/module.contact.php index fd7e5d0..8b33fef 100644 --- a/php/module.contact.php +++ b/php/module.contact.php @@ -551,6 +551,8 @@ class ContactModule extends Module { case 0: $properties["email_address_1"] = $email; $properties["email_address_display_name_1"] = $fileas . " (" . $email . ")"; + $properties["address_book_mv"] = [0]; // this is needed for adding the contact to the email address book, 0 = email 1 + $properties["address_book_long"] = 1; // this specifies the number of elements in address_book_mv break; case 1: $properties["email_address_2"] = $email; From 8540bd910490ca82fd41712829d157bad592457c Mon Sep 17 00:00:00 2001 From: Christoph Haas Date: Mon, 13 Jun 2016 14:13:59 +0200 Subject: [PATCH 04/16] fix unsaved changes bug --- php/module.contact.php | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/php/module.contact.php b/php/module.contact.php index 8b33fef..0948fd2 100644 --- a/php/module.contact.php +++ b/php/module.contact.php @@ -309,6 +309,7 @@ class ContactModule extends Module { $properties = array(); $properties["subject"] = PR_SUBJECT; + $properties["hide_attachments"] = "PT_BOOLEAN:PSETID_Common:0x851"; $properties["icon_index"] = PR_ICON_INDEX; $properties["message_class"] = PR_MESSAGE_CLASS; $properties["display_name"] = PR_DISPLAY_NAME; @@ -505,6 +506,8 @@ class ContactModule extends Module { error_log("Skipping entry! No fullname/organization given."); continue; } + + $properties["hide_attachments"] = true; //uid - used for front/backend communication $properties["internal_fields"] = array(); @@ -551,16 +554,19 @@ class ContactModule extends Module { case 0: $properties["email_address_1"] = $email; $properties["email_address_display_name_1"] = $fileas . " (" . $email . ")"; + $properties["email_address_display_name_email_1"] = $email; $properties["address_book_mv"] = [0]; // this is needed for adding the contact to the email address book, 0 = email 1 $properties["address_book_long"] = 1; // this specifies the number of elements in address_book_mv break; case 1: $properties["email_address_2"] = $email; $properties["email_address_display_name_2"] = $fileas . " (" . $email . ")"; + $properties["email_address_display_name_email_2"] = $email; break; case 2: $properties["email_address_3"] = $email; $properties["email_address_display_name_3"] = $fileas . " (" . $email . ")"; + $properties["email_address_display_name_email_3"] = $email; break; default: break; } @@ -682,9 +688,9 @@ class ContactModule extends Module { 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 ($zcs) $out = $zcs . "\n" . $out; - if (isset($street) && $street != "") $out = $street . (($out)?"\r\n". $out: "") ; + if (isset($street) && $street != "") $out = $street . (($out)?"\n\n". $out: "") ; return $out; } From dae8a7e610abb3069c39b0fd6063d6c769e19d51 Mon Sep 17 00:00:00 2001 From: Christoph Haas Date: Mon, 13 Jun 2016 15:51:19 +0200 Subject: [PATCH 05/16] starting on export feature --- js/data/ResponseHandler.js | 8 +++++ js/plugin.contactimporter.js | 63 ++++++++++++++++++++++++++++++++++ php/download.php | 47 +++++++++++++++++++++++++ php/module.contact.php | 52 +++++++++++++++++++++++++++- php/plugin.contactimporter.php | 9 ++++- 5 files changed, 177 insertions(+), 2 deletions(-) create mode 100644 php/download.php diff --git a/js/data/ResponseHandler.js b/js/data/ResponseHandler.js index 6b38745..dea5600 100644 --- a/js/data/ResponseHandler.js +++ b/js/data/ResponseHandler.js @@ -55,6 +55,14 @@ Zarafa.plugins.contactimporter.data.ResponseHandler = Ext.extend(Zarafa.core.dat doImport : function(response) { this.successCallback(response); }, + + /** + * Call the successCallback callback function. + * @param {Object} response Object contained the response data. + */ + doExport : function(response) { + this.successCallback(response); + }, /** * Call the successCallback callback function. diff --git a/js/plugin.contactimporter.js b/js/plugin.contactimporter.js index f4588d5..35cc34a 100644 --- a/js/plugin.contactimporter.js +++ b/js/plugin.contactimporter.js @@ -50,6 +50,9 @@ Zarafa.plugins.contactimporter.ImportPlugin = Ext.extend(Zarafa.core.Plugin, { /* add import button to south navigation */ this.registerInsertionPoint("navigation.south", this.createImportButton, this); + + /* export a contact via rightclick */ + this.registerInsertionPoint('context.contact.contextmenu.actions', this.createItemExportInsertionPoint, this); }, /** @@ -74,6 +77,66 @@ Zarafa.plugins.contactimporter.ImportPlugin = Ext.extend(Zarafa.core.Plugin, { return button; }, + + /** + * This method hooks to the contact context menu and allows users to export users to vcf. + * + * @param include + * @param btn + * @returns {Object} + */ + createItemExportInsertionPoint: function (include, btn) { + return { + text : dgettext('plugin_files', 'Export VCF'), + handler: this.exportToVCF.createDelegate(this, [btn]), + scope : this, + iconCls: 'icon_contactimporter_export' + }; + }, + + exportToVCF: function(btn) { + if(btn.records.length == 0) { + return; // skip if no records where given! + } + + var recordIds = []; + + for(var i=0;iimportContacts($actionType, $actionData); break; + case "export": + $result = $this->exportContacts($actionType, $actionData); + break; case "importattachment": $result = $this->getAttachmentPath($actionType, $actionData); break; @@ -240,7 +243,54 @@ class ContactModule extends Module { $this->addActionData($actionType, $response); $GLOBALS["bus"]->addData($this->getResponseData()); } - + + private function exportContacts($actionType, $actionData) + { + // Get store id + $storeid = false; + if (isset($actionData["storeid"])) { + $storeid = $actionData["storeid"]; + } + + // Get records + $records = array(); + if (isset($actionData["records"])) { + $records = $actionData["records"]; + } + + $response = array(); + $error = false; + $error_msg = ""; + + // write csv + $token = $this->randomstring(16); + $file = PLUGIN_CONTACTIMPORTER_TMP_UPLOAD . "vcf_" . $token . ".vcf"; + file_put_contents($file, ""); + + $store = $GLOBALS["mapisession"]->openMessageStore(hex2bin($storeid)); + if ($store) { + for ($index = 0, $count = count($records); $index < $count; $index++) { + $message = mapi_msgstore_openentry($store, hex2bin($records[$index])); + + // get message properties. + $messageProps = mapi_getprops($message, array(PR_DISPLAY_NAME)); + file_put_contents($file, file_get_contents($file) . $messageProps[PR_DISPLAY_NAME]); + + // TODO: implement vcf + } + } else { + return false; + } + + $response['status'] = true; + $response['download_token'] = $token; + $response['filename'] = "test.csv"; + + $this->addActionData($actionType, $response); + $GLOBALS["bus"]->addData($this->getResponseData()); + } + + private function replaceStringPropertyTags($store, $properties) { $newProperties = array(); diff --git a/php/plugin.contactimporter.php b/php/plugin.contactimporter.php index 623a0cc..031e0fc 100644 --- a/php/plugin.contactimporter.php +++ b/php/plugin.contactimporter.php @@ -20,7 +20,8 @@ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA * */ - +require_once __DIR__ . "/download.php"; + /** * contactimporter Plugin * @@ -40,6 +41,7 @@ class Plugincontactimporter extends Plugin { */ function init() { $this->registerHook('server.core.settings.init.before'); + $this->registerHook('server.index.load.custom'); } /** @@ -54,6 +56,11 @@ class Plugincontactimporter extends Plugin { case 'server.core.settings.init.before' : $this->injectPluginSettings($data); break; + case 'server.index.load.custom': + if ($data['name'] == 'download_vcf') { + DownloadHandler::doDownload(); + } + break; } } From eb0cb9a5ad93266da9bc1c206c1b4fbbf847a9ca Mon Sep 17 00:00:00 2001 From: Christoph Haas Date: Mon, 13 Jun 2016 19:59:17 +0200 Subject: [PATCH 06/16] exporting vcards works now --- php/module.contact.php | 171 ++++++++++++++++++++++++- resources/css/contactimporter-main.css | 8 +- resources/images/download.png | Bin 0 -> 224 bytes resources/images/download.xcf | Bin 0 -> 93773 bytes 4 files changed, 173 insertions(+), 6 deletions(-) create mode 100755 resources/images/download.png create mode 100755 resources/images/download.xcf diff --git a/php/module.contact.php b/php/module.contact.php index 132c72b..82eefa6 100644 --- a/php/module.contact.php +++ b/php/module.contact.php @@ -28,7 +28,7 @@ use JeroenDesloovere\VCard\VCardParser; class ContactModule extends Module { - private $DEBUG = false; // enable error_log debugging + private $DEBUG = true; // enable error_log debugging /** * @constructor @@ -244,6 +244,14 @@ class ContactModule extends Module { $GLOBALS["bus"]->addData($this->getResponseData()); } + private function getProp($props, $propname) { + $p = $this->getProperties(); + if(isset($props["props"][$propname])){ + return $props["props"][$propname]; + } + return ""; + } + private function exportContacts($actionType, $actionData) { // Get store id @@ -270,13 +278,129 @@ class ContactModule extends Module { $store = $GLOBALS["mapisession"]->openMessageStore(hex2bin($storeid)); if ($store) { for ($index = 0, $count = count($records); $index < $count; $index++) { + // define vcard + $vcard = new VCard(); + $message = mapi_msgstore_openentry($store, hex2bin($records[$index])); // get message properties. - $messageProps = mapi_getprops($message, array(PR_DISPLAY_NAME)); - file_put_contents($file, file_get_contents($file) . $messageProps[PR_DISPLAY_NAME]); + $properties = $GLOBALS['properties']->getContactProperties(); + $plaintext = true; + $messageProps = $GLOBALS['operations']->getMessageProps($store, $message, $properties, $plaintext); - // TODO: implement vcf + // define variables + $firstname = $this->getProp($messageProps, "given_name"); + $lastname = $this->getProp($messageProps, "surname"); + $additional = $this->getProp($messageProps, "middle_name"); + $prefix = $this->getProp($messageProps, "display_name_prefix"); + $suffix = ''; + + // add personal data + $vcard->addName($lastname, $firstname, $additional, $prefix, $suffix); + + $company = $this->getProp($messageProps, "company_name"); + if(!empty($company)) { + $vcard->addCompany($company); + } + + $jobtitle = $this->getProp($messageProps, "title"); + if(!empty($jobtitle)) { + $vcard->addJobtitle($jobtitle); + } + + // MAIL + $mail = $this->getProp($messageProps, "email_address_1"); + if(!empty($mail)) { + $vcard->addEmail($mail); + } + $mail = $this->getProp($messageProps, "email_address_2"); + if(!empty($mail)) { + $vcard->addEmail($mail); + } + $mail = $this->getProp($messageProps, "email_address_3"); + if(!empty($mail)) { + $vcard->addEmail($mail); + } + + // PHONE + $wphone = $this->getProp($messageProps, "business_telephone_number"); + if(!empty($wphone)) { + $vcard->addPhoneNumber($wphone, 'WORK'); + } + $wphone = $this->getProp($messageProps, "home_telephone_number"); + if(!empty($wphone)) { + $vcard->addPhoneNumber($wphone, 'HOME'); + } + $wphone = $this->getProp($messageProps, "cellular_telephone_number"); + if(!empty($wphone)) { + $vcard->addPhoneNumber($wphone, 'CELL'); + } + $wphone = $this->getProp($messageProps, "business_fax_number"); + if(!empty($wphone)) { + $vcard->addPhoneNumber($wphone, 'FAX'); + } + $wphone = $this->getProp($messageProps, "pager_telephone_number"); + if(!empty($wphone)) { + $vcard->addPhoneNumber($wphone, 'PAGER'); + } + $wphone = $this->getProp($messageProps, "car_telephone_number"); + if(!empty($wphone)) { + $vcard->addPhoneNumber($wphone, 'CAR'); + } + + // ADDRESS + $addr = $this->getProp($messageProps, "business_address"); + if(!empty($addr)) { + $vcard->addAddress(null, null, $this->getProp($messageProps, "business_address_street"), $this->getProp($messageProps, "business_address_city"), $this->getProp($messageProps, "business_address_state"), $this->getProp($messageProps, "business_address_postal_code"), $this->getProp($messageProps, "business_address_country"), "WORK"); + } + $addr = $this->getProp($messageProps, "home_address"); + if(!empty($addr)) { + $vcard->addAddress(null, null, $this->getProp($messageProps, "home_address_street"), $this->getProp($messageProps, "home_address_city"), $this->getProp($messageProps, "home_address_state"), $this->getProp($messageProps, "home_address_postal_code"), $this->getProp($messageProps, "home_address_country"), "HOME"); + } + $addr = $this->getProp($messageProps, "other_address"); + if(!empty($addr)) { + $vcard->addAddress(null, null, $this->getProp($messageProps, "other_address_street"), $this->getProp($messageProps, "other_address_city"), $this->getProp($messageProps, "other_address_state"), $this->getProp($messageProps, "other_address_postal_code"), $this->getProp($messageProps, "other_address_country"), "OTHER"); + } + + // MISC + $url = $this->getProp($messageProps, "webpage"); + if(!empty($url)) { + $vcard->addURL($url); + } + + $bday = $this->getProp($messageProps, "birthday"); + if(!empty($bday)) { + $vcard->addBirthday(date("Y-m-d", $bday)); + } + + $notes = $this->getProp($messageProps, "body"); + if(!empty($notes)) { + $vcard->addNote($notes); + } + + $haspicture = $this->getProp($messageProps, "has_picture"); + if(!empty($haspicture) && $haspicture === true) { + $attachnum = -1; + if(isset($messageProps["attachments"]) && isset($messageProps["attachments"]["item"])) { + foreach($messageProps["attachments"]["item"] as $attachment) { + if($attachment["props"]["attachment_contactphoto"] == true) { + $attachnum = $attachment["props"]["attach_num"]; + break; + } + } + } + + if($attachnum >= 0) { + $attachment = $this->getAttachmentByAttachNum($message, $attachnum); // get first attachment only + $phototoken = $this->randomstring(16); + $tmpphoto = PLUGIN_CONTACTIMPORTER_TMP_UPLOAD . "photo_" . $phototoken . ".jpg"; + $this->storeSavedAttachment($tmpphoto, $attachment); + $vcard->addPhoto($tmpphoto, true); + unlink($tmpphoto); + } + } + // write combined vcf + file_put_contents($file, file_get_contents($file) . $vcard->getOutput()); } } else { return false; @@ -284,12 +408,49 @@ class ContactModule extends Module { $response['status'] = true; $response['download_token'] = $token; - $response['filename'] = "test.csv"; + $response['filename'] = count($records)."contacts.vcf"; $this->addActionData($actionType, $response); $GLOBALS["bus"]->addData($this->getResponseData()); } + /** + * Returns attachment based on specified attachNum, additionally it will also get embedded message + * if we want to get the inline image attachment. + * @param $message + * @param array $attachNum + * @return MAPIAttach embedded message attachment or attachment that is requested + */ + private function getAttachmentByAttachNum($message, $attachNum) + { + // open the attachment + $attachment = mapi_message_openattach($message, $attachNum); + + return $attachment; + } + + /** + * Function will open passed attachment and generate response for that attachment to send it to client. + * This should only be used to download attachment that is already saved in MAPIMessage. + * @param MAPIAttach $attachment attachment which will be dumped to client side + * @return Response response to sent to client including attachment data + */ + private function storeSavedAttachment($temppath, $attachment) { + // Check if the attachment is opened + if($attachment) { + // Open a stream to get the attachment data + $stream = mapi_openproperty($attachment, PR_ATTACH_DATA_BIN, IID_IStream, 0, 0); + $stat = mapi_stream_stat($stream); + + // Read the attachment content from the stream + $body = ''; + for($i = 0; $i < $stat['cb']; $i += BLOCK_SIZE) { + $body .= mapi_stream_read($stream, BLOCK_SIZE); + } + + file_put_contents($temppath, $body); + } + } private function replaceStringPropertyTags($store, $properties) { $newProperties = array(); diff --git a/resources/css/contactimporter-main.css b/resources/css/contactimporter-main.css index b92327f..97f1e5f 100644 --- a/resources/css/contactimporter-main.css +++ b/resources/css/contactimporter-main.css @@ -3,4 +3,10 @@ background-repeat: no-repeat; background-position: center; background-size: 18px!important; -} \ No newline at end of file +} + +.icon_contactimporter_export { + background: url(../images/download.png) no-repeat; + background-repeat: no-repeat; + background-position: center; +} diff --git a/resources/images/download.png b/resources/images/download.png new file mode 100755 index 0000000000000000000000000000000000000000..38a828115f79b5f70942577589b71ab86af6f3a5 GIT binary patch literal 224 zcmeAS@N?(olHy`uVBq!ia0vp^d?3uh1|;P@bT0xawj^(N7l!{JxM1({$v_d#0*}aI z1_o|n5N2eUHAey{$X?><>&pIsjaN{SdHo4NN1#xyr;B5V#`(RM4)QiA@U%V*k8D?r zY1-bmgHPw7(3xO=l@6AREB>$b-5<@`JEu76oT1ywd^MXl^%^RBSLtn^a3U+nYxdSK zhBJ#bS|0E+^gQZeIPm1DsNO}!Zw0-68(O1RNPGHUdi-|DztiXK)7hu%8tz?h+vo(e On8DN4&t;ucLK6VmSxpZB literal 0 HcmV?d00001 diff --git a/resources/images/download.xcf b/resources/images/download.xcf new file mode 100755 index 0000000000000000000000000000000000000000..9a5b62c6c719e6dbcbe61eccdbd24838d52c18c6 GIT binary patch literal 93773 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 literal 0 HcmV?d00001 From 21af50aa6c02032828993abf976adb1996922152 Mon Sep 17 00:00:00 2001 From: Christoph Haas Date: Mon, 13 Jun 2016 20:22:24 +0200 Subject: [PATCH 07/16] fixed multi email import --- php/module.contact.php | 72 +++++++++++++++++++++++++++--------------- 1 file changed, 46 insertions(+), 26 deletions(-) diff --git a/php/module.contact.php b/php/module.contact.php index 82eefa6..228991c 100644 --- a/php/module.contact.php +++ b/php/module.contact.php @@ -753,35 +753,55 @@ class ContactModule extends Module { } if (isset($vCard->email) && count($vCard->email) > 0) { $emailcount = 0; + $properties["address_book_long"] = 0; foreach ($vCard->email as $type => $email) { - $email = $email[0]; // we only can store one mail address - $fileas = $email; - if(isset($properties["fileas"]) && !empty($properties["fileas"])) { - $fileas = $properties["fileas"]; // set to real name - } + foreach ($email as $mail) { + $fileas = $mail; + if (isset($properties["fileas"]) && !empty($properties["fileas"])) { + $fileas = $properties["fileas"]; // set to real name + } - // we only have storage for 3 mail addresses! - switch($emailcount) { - case 0: - $properties["email_address_1"] = $email; - $properties["email_address_display_name_1"] = $fileas . " (" . $email . ")"; - $properties["email_address_display_name_email_1"] = $email; - $properties["address_book_mv"] = [0]; // this is needed for adding the contact to the email address book, 0 = email 1 - $properties["address_book_long"] = 1; // this specifies the number of elements in address_book_mv - break; - case 1: - $properties["email_address_2"] = $email; - $properties["email_address_display_name_2"] = $fileas . " (" . $email . ")"; - $properties["email_address_display_name_email_2"] = $email; - break; - case 2: - $properties["email_address_3"] = $email; - $properties["email_address_display_name_3"] = $fileas . " (" . $email . ")"; - $properties["email_address_display_name_email_3"] = $email; - break; - default: break; + // we only have storage for 3 mail addresses! + /** + * type of email address address_book_mv address_book_long + * email1 0 1 (0x00000001) + * email2 1 2 (0x00000002) + * email3 2 4 (0x00000004) + * fax2(business fax) 3 8 (0x00000008) + * fax3(home fax) 4 16 (0x00000010) + * fax1(primary fax) 5 32 (0x00000020) + * + * address_book_mv is a multivalued property so all the values are passed in array + * address_book_long stores sum of the flags + * these both properties should be in sync always + */ + switch ($emailcount) { + case 0: + $properties["email_address_1"] = $mail; + $properties["email_address_display_name_1"] = $fileas . " (" . $mail . ")"; + $properties["email_address_display_name_email_1"] = $mail; + $properties["address_book_mv"][] = 0; // this is needed for adding the contact to the email address book, 0 = email 1 + $properties["address_book_long"] += 1; // this specifies the number of elements in address_book_mv + break; + case 1: + $properties["email_address_2"] = $mail; + $properties["email_address_display_name_2"] = $fileas . " (" . $mail . ")"; + $properties["email_address_display_name_email_2"] = $mail; + $properties["address_book_mv"][] = 1; // this is needed for adding the contact to the email address book, 1 = email 2 + $properties["address_book_long"] += 2; // this specifies the number of elements in address_book_mv + break; + case 2: + $properties["email_address_3"] = $mail; + $properties["email_address_display_name_3"] = $fileas . " (" . $mail . ")"; + $properties["email_address_display_name_email_3"] = $mail; + $properties["address_book_mv"][] = 2; // this is needed for adding the contact to the email address book, 2 = email 3 + $properties["address_book_long"] += 4; // this specifies the number of elements in address_book_mv + break; + default: + break; + } + $emailcount++; } - $emailcount++; } } if (isset($vCard->organization)) { From 4ce04ab6f9f07a0a2a33c9460c7f670aa3be32bd Mon Sep 17 00:00:00 2001 From: Christoph Haas Date: Mon, 13 Jun 2016 22:59:05 +0200 Subject: [PATCH 08/16] code beautifications --- config.php | 18 +- js/ABOUT.js | 42 +-- js/data/ResponseHandler.js | 36 +- js/dialogs/ImportContentPanel.js | 29 +- js/dialogs/ImportPanel.js | 388 +++++++++---------- js/plugin.contactimporter.js | 181 +++++---- manifest.xml | 2 +- php/download.php | 27 +- php/module.contact.php | 499 ++++++++++++++----------- php/plugin.contactimporter.php | 26 +- php/upload.php | 34 +- resources/css/contactimporter-main.css | 14 +- 12 files changed, 682 insertions(+), 614 deletions(-) diff --git a/config.php b/config.php index 29ed52f..f64d22f 100644 --- a/config.php +++ b/config.php @@ -1,12 +1,10 @@ diff --git a/js/ABOUT.js b/js/ABOUT.js index 2a3786f..0c2f1ec 100644 --- a/js/ABOUT.js +++ b/js/ABOUT.js @@ -19,7 +19,7 @@ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA * */ - + Ext.namespace('Zarafa.plugins.contactimporter'); /** @@ -29,30 +29,30 @@ Ext.namespace('Zarafa.plugins.contactimporter'); * 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>

" ++ "

Copyright (C) 2012-2016 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 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.

" ++ "

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

" ++ "

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

" ++ "

The contactimporter plugin contains the following third-party components:

" - + "

Copyright (C) 2012 Nuovo

" ++ "

vCard-parser

" - + "

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 ++ "

Copyright (C) 2016 Jeroen Desloovere

" + ++ "

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 index dea5600..fdb5228 100644 --- a/js/data/ResponseHandler.js +++ b/js/data/ResponseHandler.js @@ -2,7 +2,7 @@ * ResponseHandler.js zarafa contact im/exporter * * Author: Christoph Haas - * Copyright (C) 2012-2013 Christoph Haas + * Copyright (C) 2012-2016 Christoph Haas * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -19,7 +19,7 @@ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA * */ - + /** * ResponseHandler * @@ -38,21 +38,13 @@ Zarafa.plugins.contactimporter.data.ResponseHandler = Ext.extend(Zarafa.core.dat * @cfg {Function} successCallback The function which * will be called after success request. */ - successCallback : null, - + successCallback: null, + /** * Call the successCallback callback function. * @param {Object} response Object contained the response data. */ - doLoad : function(response) { - this.successCallback(response); - }, - - /** - * Call the successCallback callback function. - * @param {Object} response Object contained the response data. - */ - doImport : function(response) { + doLoad: function (response) { this.successCallback(response); }, @@ -60,24 +52,32 @@ Zarafa.plugins.contactimporter.data.ResponseHandler = Ext.extend(Zarafa.core.dat * Call the successCallback callback function. * @param {Object} response Object contained the response data. */ - doExport : function(response) { + doImport: function (response) { this.successCallback(response); }, - + /** * Call the successCallback callback function. * @param {Object} response Object contained the response data. */ - doImportattachment : function(response) { + doExport: function (response) { this.successCallback(response); }, - + + /** + * Call the successCallback callback function. + * @param {Object} response Object contained the response data. + */ + doImportattachment: 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) { + doError: function (response) { alert("error response code: " + response.error.info.code); } }); diff --git a/js/dialogs/ImportContentPanel.js b/js/dialogs/ImportContentPanel.js index 4934671..4a58ab7 100644 --- a/js/dialogs/ImportContentPanel.js +++ b/js/dialogs/ImportContentPanel.js @@ -2,7 +2,7 @@ * ImportContentPanel.js zarafa contact to vcf im/exporter * * Author: Christoph Haas - * Copyright (C) 2012-2013 Christoph Haas + * Copyright (C) 2012-2016 Christoph Haas * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -19,13 +19,13 @@ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA * */ - + /** * ImportContentPanel * * Container for the importpanel. */ -Ext.namespace("Zarafa.plugins.contactimporter.dialogs"); +Ext.namespace("Zarafa.plugins.contactimporter.dialogs"); /** * @class Zarafa.plugins.contactimporter.dialogs.ImportContentPanel @@ -40,23 +40,20 @@ Zarafa.plugins.contactimporter.dialogs.ImportContentPanel = Ext.extend(Zarafa.co * @constructor * @param config Configuration structure */ - constructor : function(config) { + 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, + layout : 'fit', + title : title, + closeOnSave: true, + width : 620, + height : 465, //Add panel - items : [ + items : [ { - xtype : 'contactimporter.importcontactpanel', - filename : config.filename + xtype : 'contactimporter.importcontactpanel', + filename: config.filename } ] }); @@ -66,4 +63,4 @@ Zarafa.plugins.contactimporter.dialogs.ImportContentPanel = Ext.extend(Zarafa.co }); -Ext.reg('contactimporter.contentpanel' ,Zarafa.plugins.contactimporter.dialogs.ImportContentPanel); \ No newline at end of file +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 index 44db926..8f65415 100644 --- a/js/dialogs/ImportPanel.js +++ b/js/dialogs/ImportPanel.js @@ -2,7 +2,7 @@ * ImportPanel.js zarafa contact to vcf im/exporter * * Author: Christoph Haas - * Copyright (C) 2012-2013 Christoph Haas + * Copyright (C) 2012-2016 Christoph Haas * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -13,7 +13,7 @@ * 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 @@ -25,7 +25,7 @@ * * The main Panel of the contactimporter plugin. */ -Ext.namespace("Zarafa.plugins.contactimporter.dialogs"); +Ext.namespace("Zarafa.plugins.contactimporter.dialogs"); /** * @class Zarafa.plugins.contactimporter.dialogs.ImportPanel @@ -35,22 +35,22 @@ Zarafa.plugins.contactimporter.dialogs.ImportPanel = Ext.extend(Ext.Panel, { /* path to vcf file on server... */ vcffile: null, - + /* The store for the selection grid */ - store: null, + store : null, /** * @constructor * @param {object} config */ - constructor : function (config) { + constructor: function (config) { config = config || {}; var self = this; - - if(!Ext.isEmpty(config.filename)) { + + if (!Ext.isEmpty(config.filename)) { 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({ @@ -62,42 +62,42 @@ Zarafa.plugins.contactimporter.dialogs.ImportPanel = Ext.extend(Ext.Panel, { {name: 'record'} ] }); - + Ext.apply(config, { - xtype : 'contactimporter.importpanel', - ref : "importcontactpanel", - layout : { - type : 'form', - align : 'stretch' + 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;' + 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 : [ + items : [ this.createSelectBox(), this.initForm(), this.createGrid() ], - buttons: [ + 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.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); } }, - scope: this + scope : this } }); - + Zarafa.plugins.contactimporter.dialogs.ImportPanel.superclass.constructor.call(this, config); }, @@ -106,22 +106,22 @@ Zarafa.plugins.contactimporter.dialogs.ImportPanel = Ext.extend(Ext.Panel, { * posted and contains the attachments * @private */ - initForm : function () { + initForm: function () { return { - xtype: 'form', - ref: 'addContactFormPanel', - layout : 'column', + xtype : 'form', + ref : 'addContactFormPanel', + layout : 'column', fileUpload: true, - autoWidth: true, + autoWidth : true, autoHeight: true, - border: false, - bodyStyle: 'padding: 5px;', - defaults: { - anchor: '95%', - border: false, + border : false, + bodyStyle : 'padding: 5px;', + defaults : { + anchor : '95%', + border : false, bodyStyle: 'padding: 5px;' }, - items: [this.createUploadField()] + items : [this.createUploadField()] }; }, @@ -129,14 +129,14 @@ Zarafa.plugins.contactimporter.dialogs.ImportPanel = Ext.extend(Ext.Panel, { * Reloads the data of the grid * @private */ - reloadGridStore: function(contactdata) { + reloadGridStore: function (contactdata) { var parsedData = []; - - if(contactdata) { + + if (contactdata) { parsedData = new Array(contactdata.contacts.length); var i = 0; - for(i = 0; i < contactdata.contacts.length; i++) { - + for (i = 0; i < contactdata.contacts.length; i++) { + parsedData[i] = [ contactdata.contacts[i]["display_name"], contactdata.contacts[i]["given_name"], @@ -151,63 +151,63 @@ Zarafa.plugins.contactimporter.dialogs.ImportPanel = Ext.extend(Ext.Panel, { this.store.loadData(parsedData, false); }, - + /** * Init embedded form, this is the form that is * posted and contains the attachments * @private */ - createGrid : function() { + createGrid: function () { return { - xtype: 'grid', - ref: 'contactGrid', + xtype : 'grid', + ref : 'contactGrid', columnWidth: 1.0, - store: this.store, - width: '100%', - height: 300, - title: 'Select contacts to import', - frame: false, - viewConfig:{ - forceFit:true + store : this.store, + width : '100%', + height : 300, + title : 'Select contacts to import', + frame : false, + viewConfig : { + forceFit: true }, - colModel: new Ext.grid.ColumnModel({ + colModel : new Ext.grid.ColumnModel({ defaults: { - width: 300, + width : 300, sortable: true }, - columns: [ + 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}) + sm : new Ext.grid.RowSelectionModel({multiSelect: true}) } }, - - createSelectBox: function() { + + 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([defaultFolder.getDefaultFolderKey(), defaultFolder.getDisplayName()]); - for(i = 0; i < subFolders.length; i++) { + for (i = 0; i < subFolders.length; i++) { /* Store all subfolders */ myStore.push([subFolders[i].getDisplayName(), subFolders[i].getDisplayName(), false]); // 3rd field = isPublicfolder } - + /* add all shared contact folders */ var pubStore = container.getHierarchyStore().getPublicStore(); - - if(typeof pubStore !== "undefined") { + + 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")){ + for (i = 0; i < pubSubFolders.length; i++) { + if (pubSubFolders[i].isContainerClass("IPF.Contact")) { myStore.push([pubSubFolders[i].getDisplayName(), pubSubFolders[i].getDisplayName() + " [Shared]", true]); // 3rd field = isPublicfolder } } @@ -216,128 +216,128 @@ Zarafa.plugins.contactimporter.dialogs.ImportPanel = Ext.extend(Ext.Panel, { 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', + 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 + border : false, + anchor : "100%", + scope : this, + allowBlank : false } }, - - createUploadField: function() { + + createUploadField: function () { return { - xtype: "fileuploadfield", - ref: 'contactfileuploadfield', + 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: { + id : 'form-file', + name : 'vcfdata', + emptyText : 'Select an .vcf addressbook', + border : false, + anchor : "100%", + scope : this, + allowBlank : false, + listeners : { 'fileselected': this.onFileSelected, - scope: this + scope : this } } }, - - createSubmitButton: function() { + + createSubmitButton: function () { return { - xtype: "button", - ref: "../submitButton", - disabled: true, - width: 100, - border: false, - text: _("Import"), - anchor: "100%", - handler: this.importCheckedContacts, - scope: this, + xtype : "button", + ref : "../submitButton", + disabled : true, + width : 100, + border : false, + text : _("Import"), + anchor : "100%", + handler : this.importCheckedContacts, + scope : this, allowBlank: false } }, - - createSubmitAllButton: function() { + + createSubmitAllButton: function () { return { - xtype: "button", - ref: "../submitAllButton", - disabled: true, - width: 100, - border: false, - text: _("Import All"), - anchor: "100%", - handler: this.importAllContacts, - scope: this, + xtype : "button", + ref : "../submitAllButton", + disabled : true, + width : 100, + border : false, + text : _("Import All"), + anchor : "100%", + handler : this.importAllContacts, + scope : this, allowBlank: false } }, - - createCancelButton: function() { + + createCancelButton: function () { return { - xtype: "button", - width: 100, - border: false, - text: _("Cancel"), - anchor: "100%", - handler: this.close, - scope: this, + 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) { + 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) { + 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 + title : _('Error'), + msg : _(action.result.error), + icon : Zarafa.common.dialogs.MessageBox.ERROR, + buttons: Zarafa.common.dialogs.MessageBox.OK }); }, - success: function(file, action){ + success: function (file, action) { uploadField.reset(); this.vcffile = action.result.vcf_file; - + this.parseContacts(this.vcffile); }, - scope : this + 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', 'load', @@ -347,23 +347,23 @@ Zarafa.plugins.contactimporter.dialogs.ImportPanel = Ext.extend(Ext.Panel, { responseHandler ); }, - - handleParsingResult: function(response) { + + handleParsingResult: function (response) { this.loadMask.hide(); - - if(response["status"] == true) { + + 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 + title : _('Parser Error'), + msg : _(response["message"]), + icon : Zarafa.common.dialogs.MessageBox.ERROR, + buttons: Zarafa.common.dialogs.MessageBox.OK }); } }, @@ -376,117 +376,117 @@ Zarafa.plugins.contactimporter.dialogs.ImportPanel = Ext.extend(Ext.Panel, { 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 + }, + + /** + * 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 + 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 + 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) { + 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 + 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 contactFolder = container.getHierarchyStore().getDefaultFolder('contact'); var pubStore = container.getHierarchyStore().getPublicStore(); var pubFolder = pubStore.getDefaultFolder("publicfolders"); var pubSubFolders = pubFolder.getChildren(); - - if(folderValue != "contact") { + + if (folderValue != "contact") { var subFolders = contactFolder.getChildren(); var i = 0; - for(i = 0; i < pubSubFolders.length; i++) { - if(pubSubFolders[i].isContainerClass("IPF.Contact")){ + for (i = 0; i < pubSubFolders.length; i++) { + if (pubSubFolders[i].isContainerClass("IPF.Contact")) { subFolders.push(pubSubFolders[i]); } } - for(i=0;i - * Copyright (C) 2012-2013 Christoph Haas + * Copyright (C) 2012-2016 Christoph Haas * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -19,34 +19,34 @@ * 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 + * @param {Object} config Configuration object + * + */ constructor: function (config) { config = config || {}; - + Zarafa.plugins.contactimporter.ImportPlugin.superclass.constructor.call(this, config); }, - + /** * initialises insertion point for plugin * @protected */ - initPlugin : function() { + 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); + this.registerInsertionPoint('common.contextmenu.attachment.actions', this.createAttachmentImportButton, this); /* add import button to south navigation */ this.registerInsertionPoint("navigation.south", this.createImportButton, this); @@ -54,28 +54,24 @@ Zarafa.plugins.contactimporter.ImportPlugin = Ext.extend(Zarafa.core.Plugin, { /* export a contact via rightclick */ this.registerInsertionPoint('context.contact.contextmenu.actions', this.createItemExportInsertionPoint, this); }, - - /** - * Creates the button - * - * @return {Object} Configuration object for a {@link Ext.Button button} - * - */ + + /** + * Creates the button + * + * @return {Object} Configuration object for a {@link Ext.Button button} + * + */ createImportButton: function () { var button = { - xtype : 'button', - text : _('Import Contacts'), - iconCls : 'icon_contactimporter_button', - navigationContext : container.getContextByName('contact'), - handler : this.onImportButtonClick, - scope : this + xtype : 'button', + 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; + + return button; }, /** @@ -94,20 +90,20 @@ Zarafa.plugins.contactimporter.ImportPlugin = Ext.extend(Zarafa.core.Plugin, { }; }, - exportToVCF: function(btn) { - if(btn.records.length == 0) { + exportToVCF: function (btn) { + if (btn.records.length == 0) { return; // skip if no records where given! } var recordIds = []; - for(var i=0;i js/contactimporter-debug.js js/contactimporter-debug.js - + js/plugin.contactimporter.js js/ABOUT.js js/data/ResponseHandler.js diff --git a/php/download.php b/php/download.php index a415291..dd984b5 100644 --- a/php/download.php +++ b/php/download.php @@ -1,9 +1,30 @@ + * Copyright (C) 2012-2016 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 + * + */ class DownloadHandler { - public static function doDownload() { + public static function doDownload() + { if (isset($_GET["token"])) { $token = $_GET["token"]; } else { @@ -23,7 +44,7 @@ class DownloadHandler $file = PLUGIN_CONTACTIMPORTER_TMP_UPLOAD . "vcf_" . $token . ".vcf"; - if(!file_exists($file)) { // invalid token + if (!file_exists($file)) { // invalid token return false; } diff --git a/php/module.contact.php b/php/module.contact.php index 228991c..6894b99 100644 --- a/php/module.contact.php +++ b/php/module.contact.php @@ -1,9 +1,9 @@ - - * Copyright (C) 2012-2013 Christoph Haas + * Copyright (C) 2012-2016 Christoph Haas * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -20,23 +20,25 @@ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA * */ - + include_once('vendor/autoload.php'); use JeroenDesloovere\VCard\VCard; use JeroenDesloovere\VCard\VCardParser; -class ContactModule extends Module { +class ContactModule extends Module +{ - private $DEBUG = true; // enable error_log debugging + private $DEBUG = true; // enable error_log debugging /** * @constructor * @param $id * @param $data */ - public function __construct($id, $data) { - parent::Module($id, $data); + public function __construct($id, $data) + { + parent::Module($id, $data); } /** @@ -44,22 +46,23 @@ class ContactModule extends Module { * Exception part is used for authentication errors also * @return boolean true on success or false on failure. */ - public function execute() { + public function execute() + { $result = false; - - if(!$this->DEBUG) { + + 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)) { + + foreach ($this->data as $actionType => $actionData) { + if (isset($actionType)) { try { - if($this->DEBUG) { + if ($this->DEBUG) { error_log("exec: " . $actionType); } - switch($actionType) { - case "load": + switch ($actionType) { + case "load": $result = $this->loadContacts($actionType, $actionData); break; case "import": @@ -76,30 +79,31 @@ class ContactModule extends Module { } } catch (MAPIException $e) { - if($this->DEBUG) { + if ($this->DEBUG) { error_log("mapi exception: " . $e->getMessage()); } } catch (Exception $e) { - if($this->DEBUG) { + 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) { + private function randomstring($length = 6) + { // $chars - all allowed charakters $chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"; - srand((double)microtime()*1000000); + srand((double)microtime() * 1000000); $i = 0; $pass = ""; while ($i < $length) { @@ -110,42 +114,43 @@ class ContactModule extends Module { } return $pass; } - + /** * Add an attachment to the give contact * @param $actionType * @param $actionData */ - private function importContacts($actionType, $actionData) { - + private function importContacts($actionType, $actionData) + { + // Get uploaded vcf path $vcffile = false; - if(isset($actionData["vcf_filepath"])) { + if (isset($actionData["vcf_filepath"])) { $vcffile = $actionData["vcf_filepath"]; } - + // Get store id $storeid = false; - if(isset($actionData["storeid"])) { + if (isset($actionData["storeid"])) { $storeid = $actionData["storeid"]; } - + // Get folder entryid $folderid = false; - if(isset($actionData["folderid"])) { + if (isset($actionData["folderid"])) { $folderid = $actionData["folderid"]; } - + // Get uids $uids = array(); - if(isset($actionData["uids"])) { + if (isset($actionData["uids"])) { $uids = $actionData["uids"]; } - + $response = array(); $error = false; $error_msg = ""; - + // parse the vcf file a last time... $parser = null; try { @@ -154,47 +159,47 @@ class ContactModule extends Module { $error = true; $error_msg = $e->getMessage(); } - + $contacts = array(); - - if(!$error && iterator_count($parser) > 0) { + + if (!$error && iterator_count($parser) > 0) { $contacts = $this->parseContactsToArray($parser); $store = $GLOBALS["mapisession"]->openMessageStore(hex2bin($storeid)); $folder = mapi_msgstore_openentry($store, hex2bin($folderid)); - + $importall = false; - if(count($uids) == count($contacts)) { + if (count($uids) == count($contacts)) { $importall = true; } - + $propValuesMAPI = array(); $properties = $this->getProperties(); $properties = $this->replaceStringPropertyTags($store, $properties); $count = 0; - + // iterate through all contacts and import them :) - foreach($contacts as $contact) { + foreach ($contacts as $contact) { if (isset($contact["display_name"]) && ($importall || in_array($contact["internal_fields"]["contact_uid"], $uids))) { // parse the arraykeys // TODO: this is very slow... - foreach($contact as $key => $value) { - if($key !== "internal_fields") { + foreach ($contact as $key => $value) { + if ($key !== "internal_fields") { $propValuesMAPI[$properties[$key]] = $value; } } - + $propValuesMAPI[$properties["message_class"]] = "IPM.Contact"; $propValuesMAPI[$properties["icon_index"]] = "512"; $message = mapi_folder_createmessage($folder); - - - if(isset($contact["internal_fields"]["x_photo_path"])) { + + + if (isset($contact["internal_fields"]["x_photo_path"])) { $propValuesMAPI[$properties["picture"]] = 1; // contact has an image // import the photo $contactPicture = file_get_contents($contact["internal_fields"]["x_photo_path"]); $attach = mapi_message_createattach($message); - + // Set properties of the attachment $propValuesIMG = array( PR_ATTACH_SIZE => strlen($contactPicture), @@ -203,27 +208,27 @@ class ContactModule extends Module { PR_DISPLAY_NAME => 'ContactPicture.jpg', PR_ATTACH_METHOD => ATTACH_BY_VALUE, PR_ATTACH_MIME_TAG => 'image/jpeg', - PR_ATTACHMENT_CONTACTPHOTO => true, + PR_ATTACHMENT_CONTACTPHOTO => true, PR_ATTACH_DATA_BIN => $contactPicture, PR_ATTACHMENT_FLAGS => 1, PR_ATTACH_EXTENSION_A => '.jpg', PR_ATTACH_NUM => 1 ); - + mapi_setprops($attach, $propValuesIMG); mapi_savechanges($attach); - if($this->DEBUG) { + if ($this->DEBUG) { error_log("Contactpicture imported!"); } - + if (mapi_last_hresult() > 0) { error_log("Error saving attach to contact: " . get_mapi_error_name()); } } - + mapi_setprops($message, $propValuesMAPI); mapi_savechanges($message); - if($this->DEBUG) { + if ($this->DEBUG) { error_log("New contact added: \"" . $propValuesMAPI[$properties["display_name"]] . "\".\n"); } $count++; @@ -233,20 +238,21 @@ class ContactModule extends Module { $response['status'] = true; $response['count'] = $count; $response['message'] = ""; - + } else { $response['status'] = false; $response['count'] = 0; $response['message'] = $error ? $error_msg : "VCF file empty!"; } - + $this->addActionData($actionType, $response); $GLOBALS["bus"]->addData($this->getResponseData()); } - private function getProp($props, $propname) { + private function getProp($props, $propname) + { $p = $this->getProperties(); - if(isset($props["props"][$propname])){ + if (isset($props["props"][$propname])) { return $props["props"][$propname]; } return ""; @@ -299,98 +305,98 @@ class ContactModule extends Module { $vcard->addName($lastname, $firstname, $additional, $prefix, $suffix); $company = $this->getProp($messageProps, "company_name"); - if(!empty($company)) { + if (!empty($company)) { $vcard->addCompany($company); } $jobtitle = $this->getProp($messageProps, "title"); - if(!empty($jobtitle)) { + if (!empty($jobtitle)) { $vcard->addJobtitle($jobtitle); } // MAIL $mail = $this->getProp($messageProps, "email_address_1"); - if(!empty($mail)) { + if (!empty($mail)) { $vcard->addEmail($mail); } $mail = $this->getProp($messageProps, "email_address_2"); - if(!empty($mail)) { + if (!empty($mail)) { $vcard->addEmail($mail); } $mail = $this->getProp($messageProps, "email_address_3"); - if(!empty($mail)) { + if (!empty($mail)) { $vcard->addEmail($mail); } // PHONE $wphone = $this->getProp($messageProps, "business_telephone_number"); - if(!empty($wphone)) { + if (!empty($wphone)) { $vcard->addPhoneNumber($wphone, 'WORK'); } $wphone = $this->getProp($messageProps, "home_telephone_number"); - if(!empty($wphone)) { + if (!empty($wphone)) { $vcard->addPhoneNumber($wphone, 'HOME'); } $wphone = $this->getProp($messageProps, "cellular_telephone_number"); - if(!empty($wphone)) { + if (!empty($wphone)) { $vcard->addPhoneNumber($wphone, 'CELL'); } $wphone = $this->getProp($messageProps, "business_fax_number"); - if(!empty($wphone)) { + if (!empty($wphone)) { $vcard->addPhoneNumber($wphone, 'FAX'); } $wphone = $this->getProp($messageProps, "pager_telephone_number"); - if(!empty($wphone)) { + if (!empty($wphone)) { $vcard->addPhoneNumber($wphone, 'PAGER'); } $wphone = $this->getProp($messageProps, "car_telephone_number"); - if(!empty($wphone)) { + if (!empty($wphone)) { $vcard->addPhoneNumber($wphone, 'CAR'); } // ADDRESS $addr = $this->getProp($messageProps, "business_address"); - if(!empty($addr)) { + if (!empty($addr)) { $vcard->addAddress(null, null, $this->getProp($messageProps, "business_address_street"), $this->getProp($messageProps, "business_address_city"), $this->getProp($messageProps, "business_address_state"), $this->getProp($messageProps, "business_address_postal_code"), $this->getProp($messageProps, "business_address_country"), "WORK"); } $addr = $this->getProp($messageProps, "home_address"); - if(!empty($addr)) { + if (!empty($addr)) { $vcard->addAddress(null, null, $this->getProp($messageProps, "home_address_street"), $this->getProp($messageProps, "home_address_city"), $this->getProp($messageProps, "home_address_state"), $this->getProp($messageProps, "home_address_postal_code"), $this->getProp($messageProps, "home_address_country"), "HOME"); } $addr = $this->getProp($messageProps, "other_address"); - if(!empty($addr)) { + if (!empty($addr)) { $vcard->addAddress(null, null, $this->getProp($messageProps, "other_address_street"), $this->getProp($messageProps, "other_address_city"), $this->getProp($messageProps, "other_address_state"), $this->getProp($messageProps, "other_address_postal_code"), $this->getProp($messageProps, "other_address_country"), "OTHER"); } // MISC $url = $this->getProp($messageProps, "webpage"); - if(!empty($url)) { + if (!empty($url)) { $vcard->addURL($url); } $bday = $this->getProp($messageProps, "birthday"); - if(!empty($bday)) { + if (!empty($bday)) { $vcard->addBirthday(date("Y-m-d", $bday)); } $notes = $this->getProp($messageProps, "body"); - if(!empty($notes)) { + if (!empty($notes)) { $vcard->addNote($notes); } $haspicture = $this->getProp($messageProps, "has_picture"); - if(!empty($haspicture) && $haspicture === true) { + if (!empty($haspicture) && $haspicture === true) { $attachnum = -1; - if(isset($messageProps["attachments"]) && isset($messageProps["attachments"]["item"])) { - foreach($messageProps["attachments"]["item"] as $attachment) { - if($attachment["props"]["attachment_contactphoto"] == true) { + if (isset($messageProps["attachments"]) && isset($messageProps["attachments"]["item"])) { + foreach ($messageProps["attachments"]["item"] as $attachment) { + if ($attachment["props"]["attachment_contactphoto"] == true) { $attachnum = $attachment["props"]["attach_num"]; break; } } } - if($attachnum >= 0) { + if ($attachnum >= 0) { $attachment = $this->getAttachmentByAttachNum($message, $attachnum); // get first attachment only $phototoken = $this->randomstring(16); $tmpphoto = PLUGIN_CONTACTIMPORTER_TMP_UPLOAD . "photo_" . $phototoken . ".jpg"; @@ -408,7 +414,7 @@ class ContactModule extends Module { $response['status'] = true; $response['download_token'] = $token; - $response['filename'] = count($records)."contacts.vcf"; + $response['filename'] = count($records) . "contacts.vcf"; $this->addActionData($actionType, $response); $GLOBALS["bus"]->addData($this->getResponseData()); @@ -435,16 +441,17 @@ class ContactModule extends Module { * @param MAPIAttach $attachment attachment which will be dumped to client side * @return Response response to sent to client including attachment data */ - private function storeSavedAttachment($temppath, $attachment) { + private function storeSavedAttachment($temppath, $attachment) + { // Check if the attachment is opened - if($attachment) { + if ($attachment) { // Open a stream to get the attachment data $stream = mapi_openproperty($attachment, PR_ATTACH_DATA_BIN, IID_IStream, 0, 0); $stat = mapi_stream_stat($stream); // Read the attachment content from the stream $body = ''; - for($i = 0; $i < $stat['cb']; $i += BLOCK_SIZE) { + for ($i = 0; $i < $stat['cb']; $i += BLOCK_SIZE) { $body .= mapi_stream_read($stream, BLOCK_SIZE); } @@ -452,25 +459,26 @@ class ContactModule extends Module { } } - private function replaceStringPropertyTags($store, $properties) { + private function replaceStringPropertyTags($store, $properties) + { $newProperties = array(); - $ids = array("name"=>array(), "id"=>array(), "guid"=>array(), "type"=>array()); // this array stores all the information needed to retrieve a named property + $ids = array("name" => array(), "id" => array(), "guid" => array(), "type" => array()); // this array stores all the information needed to retrieve a named property $num = 0; // caching $guids = array(); - foreach($properties as $name => $val) { - if(is_string($val)) { + foreach ($properties as $name => $val) { + if (is_string($val)) { $split = explode(":", $val); - if(count($split) != 3) { // invalid string, ignore - trigger_error(sprintf("Invalid property: %s \"%s\"",$name,$val), E_USER_NOTICE); + if (count($split) != 3) { // invalid string, ignore + trigger_error(sprintf("Invalid property: %s \"%s\"", $name, $val), E_USER_NOTICE); continue; } - if(substr($split[2], 0, 2) == "0x") { + if (substr($split[2], 0, 2) == "0x") { $id = hexdec(substr($split[2], 2)); } else { $id = $split[2]; @@ -504,19 +512,20 @@ class ContactModule extends Module { // get the ids $named = mapi_getidsfromnames($store, $ids["id"], $ids["guid"]); - foreach($named as $num => $prop) { + foreach ($named as $num => $prop) { $newProperties[$ids["name"][$num]] = mapi_prop_tag(constant($ids["type"][$num]), mapi_prop_id($prop)); } return $newProperties; } - + /** * A simple Property map initialization * * @return [array] the propertyarray */ - private function getProperties() { + private function getProperties() + { $properties = array(); $properties["subject"] = PR_SUBJECT; @@ -596,8 +605,8 @@ class ContactModule extends Module { $properties["radio_telephone_number"] = PR_RADIO_TELEPHONE_NUMBER; $properties["telex_telephone_number"] = PR_TELEX_NUMBER; $properties["ttytdd_telephone_number"] = PR_TTYTDD_PHONE_NUMBER; - $properties["business_telephone_number"] =PR_BUSINESS_TELEPHONE_NUMBER; - + $properties["business_telephone_number"] = PR_BUSINESS_TELEPHONE_NUMBER; + // Additional fax properties $properties["fax_1_address_type"] = "PT_STRING8:PSETID_Address:0x80B2"; $properties["fax_1_email_address"] = "PT_STRING8:PSETID_Address:0x80B3"; @@ -643,10 +652,10 @@ class ContactModule extends Module { $properties["anniversary_eventid"] = "PT_BINARY:PSETID_Address:0x804E"; $properties["notes"] = PR_BODY; - + // hasimage $properties["picture"] = "PT_BOOLEAN:{00062004-0000-0000-C000-000000000046}:0x8015"; - + return $properties; } @@ -655,11 +664,12 @@ class ContactModule extends Module { * @param $actionType * @param $actionData */ - private function loadContacts($actionType, $actionData) { + private function loadContacts($actionType, $actionData) + { $error = false; $error_msg = ""; - - if(is_readable ($actionData["vcf_filepath"])) { + + if (is_readable($actionData["vcf_filepath"])) { $parser = null; try { @@ -668,58 +678,59 @@ class ContactModule extends Module { $error = true; $error_msg = $e->getMessage(); } - if($error) { - $response['status'] = false; - $response['message']= $error_msg; + if ($error) { + $response['status'] = false; + $response['message'] = $error_msg; } else { - if(iterator_count($parser) == 0) { - $response['status'] = false; - $response['message']= "No contacts in vcf file"; + if (iterator_count($parser) == 0) { + $response['status'] = false; + $response['message'] = "No contacts in vcf file"; } else { - $response['status'] = true; - $response['parsed_file']= $actionData["vcf_filepath"]; - $response['parsed'] = array ( - 'contacts' => $this->parseContactsToArray($parser) + $response['status'] = true; + $response['parsed_file'] = $actionData["vcf_filepath"]; + $response['parsed'] = array( + 'contacts' => $this->parseContactsToArray($parser) ); } } } else { - $response['status'] = false; - $response['message']= "File could not be read by server"; + $response['status'] = false; + $response['message'] = "File could not be read by server"; } - + $this->addActionData($actionType, $response); $GLOBALS["bus"]->addData($this->getResponseData()); - - if($this->DEBUG) { + + 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) { + private function parseContactsToArray($contacts, $csv = false) + { $carr = array(); - - if(!$csv) { + + if (!$csv) { foreach ($contacts as $Index => $vCard) { $properties = array(); if (isset($vCard->fullname)) { $properties["display_name"] = $vCard->fullname; $properties["fileas"] = $vCard->fullname; - } elseif(!isset($vCard->organization)) { + } elseif (!isset($vCard->organization)) { error_log("Skipping entry! No fullname/organization given."); continue; } $properties["hide_attachments"] = true; - + //uid - used for front/backend communication $properties["internal_fields"] = array(); $properties["internal_fields"]["contact_uid"] = base64_encode($Index . $properties["fileas"]); @@ -732,22 +743,36 @@ class ContactModule extends Module { if (isset($vCard->phone) && count($vCard->phone) > 0) { foreach ($vCard->phone as $type => $number) { $number = $number[0]; // we only can store one number - if($this->startswith(strtolower($type), "home") || strtolower($type) === "default") { + if ($this->startswith(strtolower($type), "home") || strtolower($type) === "default") { $properties["home_telephone_number"] = $number; - } else if($this->startswith(strtolower($type), "cell")) { - $properties["cellular_telephone_number"] = $number; - } else if($this->startswith(strtolower($type), "work")) { - $properties["business_telephone_number"] = $number; - } else if($this->startswith(strtolower($type), "fax")) { - $properties["business_fax_number"] = $number; - } else if($this->startswith(strtolower($type), "pager")) { - $properties["pager_telephone_number"] = $number; - } else if($this->startswith(strtolower($type), "isdn")) { - $properties["isdn_number"] = $number; - } else if($this->startswith(strtolower($type), "car")) { - $properties["car_telephone_number"] = $number; - } else if($this->startswith(strtolower($type), "modem")) { - $properties["ttytdd_telephone_number"] = $number; + } else { + if ($this->startswith(strtolower($type), "cell")) { + $properties["cellular_telephone_number"] = $number; + } else { + if ($this->startswith(strtolower($type), "work")) { + $properties["business_telephone_number"] = $number; + } else { + if ($this->startswith(strtolower($type), "fax")) { + $properties["business_fax_number"] = $number; + } else { + if ($this->startswith(strtolower($type), "pager")) { + $properties["pager_telephone_number"] = $number; + } else { + if ($this->startswith(strtolower($type), "isdn")) { + $properties["isdn_number"] = $number; + } else { + if ($this->startswith(strtolower($type), "car")) { + $properties["car_telephone_number"] = $number; + } else { + if ($this->startswith(strtolower($type), "modem")) { + $properties["ttytdd_telephone_number"] = $number; + } + } + } + } + } + } + } } } } @@ -763,17 +788,17 @@ class ContactModule extends Module { // we only have storage for 3 mail addresses! /** - * type of email address address_book_mv address_book_long - * email1 0 1 (0x00000001) - * email2 1 2 (0x00000002) - * email3 2 4 (0x00000004) - * fax2(business fax) 3 8 (0x00000008) - * fax3(home fax) 4 16 (0x00000010) - * fax1(primary fax) 5 32 (0x00000020) + * type of email address address_book_mv address_book_long + * email1 0 1 (0x00000001) + * email2 1 2 (0x00000002) + * email3 2 4 (0x00000004) + * fax2(business fax) 3 8 (0x00000008) + * fax3(home fax) 4 16 (0x00000010) + * fax1(primary fax) 5 32 (0x00000020) * - * address_book_mv is a multivalued property so all the values are passed in array - * address_book_long stores sum of the flags - * these both properties should be in sync always + * address_book_mv is a multivalued property so all the values are passed in array + * address_book_long stores sum of the flags + * these both properties should be in sync always */ switch ($emailcount) { case 0: @@ -806,7 +831,7 @@ class ContactModule extends Module { } if (isset($vCard->organization)) { $properties["company_name"] = $vCard->organization; - if(empty($properties["display_name"])) { + if (empty($properties["display_name"])) { $properties["display_name"] = $vCard->organization; // if we have no displayname - use the company name as displayname $properties["fileas"] = $vCard->organization; } @@ -825,9 +850,9 @@ class ContactModule extends Module { foreach ($vCard->address as $type => $address) { $address = $address[0]; // we only can store one address per type - if($this->startswith(strtolower($type), "work")) { + if ($this->startswith(strtolower($type), "work")) { $properties["business_address_street"] = $address->street; - if(!empty($address->extended)) { + if (!empty($address->extended)) { $properties["business_address_street"] .= "\n" . $address->extended; } $properties["business_address_city"] = $address->city; @@ -835,26 +860,28 @@ class ContactModule extends Module { $properties["business_address_postal_code"] = $address->zip; $properties["business_address_country"] = $address->country; $properties["business_address"] = $this->buildAddressString($properties["business_address_street"], $address->zip, $address->city, $address->region, $address->country); - } else if($this->startswith(strtolower($type), "home")) { - $properties["home_address_street"] = $address->street; - if(!empty($address->extended)) { - $properties["home_address_street"] .= "\n" . $address->extended; - } - $properties["home_address_city"] = $address->city; - $properties["home_address_state"] = $address->region; - $properties["home_address_postal_code"] = $address->zip; - $properties["home_address_country"] = $address->country; - $properties["home_address"] = $this->buildAddressString($properties["home_address_street"], $address->zip, $address->city, $address->region, $address->country); } else { - $properties["other_address_street"] = $address->street; - if(!empty($address->extended)) { - $properties["other_address_street"] .= "\n" . $address->extended; + if ($this->startswith(strtolower($type), "home")) { + $properties["home_address_street"] = $address->street; + if (!empty($address->extended)) { + $properties["home_address_street"] .= "\n" . $address->extended; + } + $properties["home_address_city"] = $address->city; + $properties["home_address_state"] = $address->region; + $properties["home_address_postal_code"] = $address->zip; + $properties["home_address_country"] = $address->country; + $properties["home_address"] = $this->buildAddressString($properties["home_address_street"], $address->zip, $address->city, $address->region, $address->country); + } else { + $properties["other_address_street"] = $address->street; + if (!empty($address->extended)) { + $properties["other_address_street"] .= "\n" . $address->extended; + } + $properties["other_address_city"] = $address->city; + $properties["other_address_state"] = $address->region; + $properties["other_address_postal_code"] = $address->zip; + $properties["other_address_country"] = $address->country; + $properties["other_address"] = $this->buildAddressString($properties["other_address_street"], $address->zip, $address->city, $address->region, $address->country); } - $properties["other_address_city"] = $address->city; - $properties["other_address_state"] = $address->region; - $properties["other_address_postal_code"] = $address->zip; - $properties["other_address_country"] = $address->country; - $properties["other_address"] = $this->buildAddressString($properties["other_address_street"], $address->zip, $address->city, $address->region, $address->country); } } } @@ -865,23 +892,23 @@ class ContactModule extends Module { $properties["notes"] = $vCard->note; } if (isset($vCard->rawPhoto) || isset($vCard->photo)) { - if(!is_writable(TMP_PATH . "/")) { + if (!is_writable(TMP_PATH . "/")) { error_log("Can not write to export tmp directory!"); } else { $tmppath = TMP_PATH . "/" . $this->randomstring(15); - if(isset($vCard->rawPhoto)) { - if(file_put_contents($tmppath, $vCard->rawPhoto)) { + if (isset($vCard->rawPhoto)) { + if (file_put_contents($tmppath, $vCard->rawPhoto)) { $properties["internal_fields"]["x_photo_path"] = $tmppath; } - } elseif(isset($vCard->photo)) { - if($this->startswith(strtolower($vCard->photo), "http://") || $this->startswith(strtolower($vCard->photo), "https://")) { // check if it starts with http - $ctx = stream_context_create(array('http'=> + } elseif (isset($vCard->photo)) { + if ($this->startswith(strtolower($vCard->photo), "http://") || $this->startswith(strtolower($vCard->photo), "https://")) { // check if it starts with http + $ctx = stream_context_create(array('http' => array( 'timeout' => 3, //3 Seconds timout ) )); - if(file_put_contents($tmppath, file_get_contents($vCard->photo, false, $ctx))) { + if (file_put_contents($tmppath, file_get_contents($vCard->photo, false, $ctx))) { $properties["internal_fields"]["x_photo_path"] = $tmppath; } } else { @@ -895,10 +922,10 @@ class ContactModule extends Module { } else { error_log("csv parsing not implemented"); } - + return $carr; } - + /** * Generate the whole addressstring * @@ -910,38 +937,52 @@ class ContactModule extends Module { * @return string the concatinated address string * @private */ - private function buildAddressString($street, $zip, $city, $state, $country) { + private function buildAddressString($street, $zip, $city, $state, $country) + { $out = ""; - if (isset($country) && $street != "") $out = $country; + 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 . "\n" . $out; + if (isset($zip) && $zip != "") { + $zcs = $zip; + } + if (isset($city) && $city != "") { + $zcs .= (($zcs) ? " " : "") . $city; + } + if (isset($state) && $state != "") { + $zcs .= (($zcs) ? " " : "") . $state; + } + if ($zcs) { + $out = $zcs . "\n" . $out; + } - if (isset($street) && $street != "") $out = $street . (($out)?"\n\n". $out: "") ; + if (isset($street) && $street != "") { + $out = $street . (($out) ? "\n\n" . $out : ""); + } return $out; } - + /** * Store the file to a temporary directory * @param $actionType * @param $actionData * @private */ - private function getAttachmentPath($actionType, $actionData) { + private function getAttachmentPath($actionType, $actionData) + { // Get store id $storeid = false; - if(isset($actionData["store"])) { + if (isset($actionData["store"])) { $storeid = $actionData["store"]; } // Get message entryid $entryid = false; - if(isset($actionData["entryid"])) { + if (isset($actionData["entryid"])) { $entryid = $actionData["entryid"]; } @@ -950,42 +991,41 @@ class ContactModule extends Module { // Get number of attachment which should be opened. $attachNum = false; - if(isset($actionData["attachNum"])) { + if (isset($actionData["attachNum"])) { $attachNum = $actionData["attachNum"]; } // Check if storeid and entryid isset - if($storeid && $entryid) { + if ($storeid && $entryid) { // Open the store $store = $GLOBALS["mapisession"]->openMessageStore(hex2bin($storeid)); - - if($store) { + + if ($store) { // Open the message $message = mapi_msgstore_openentry($store, hex2bin($entryid)); - - if($message) { + + if ($message) { $attachment = false; // Check if attachNum isset - if($attachNum) { + if ($attachNum) { // Loop through the attachNums, message in message in message ... - for($i = 0; $i < (count($attachNum) - 1); $i++) - { + for ($i = 0; $i < (count($attachNum) - 1); $i++) { // Open the attachment - $tempattach = mapi_message_openattach($message, (int) $attachNum[$i]); - if($tempattach) { + $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)]); + $attachment = mapi_message_openattach($message, (int)$attachNum[(count($attachNum) - 1)]); } // Check if the attachment is opened - if($attachment) { - + 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 @@ -994,29 +1034,33 @@ class ContactModule extends Module { $filename = "ERROR"; // Set filename - if(isset($props[PR_ATTACH_LONG_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]; - } - + } 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])) { + 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) { + if (strrpos($filename, ".") !== false) { $extension = strtolower(substr($filename, strrpos($filename, "."))); $contentType = "application/octet-stream"; - if (is_readable("mimetypes.dat")){ - $fh = fopen("mimetypes.dat","r"); + if (is_readable("mimetypes.dat")) { + $fh = fopen("mimetypes.dat", "r"); $ext_found = false; - while (!feof($fh) && !$ext_found){ + while (!feof($fh) && !$ext_found) { $line = fgets($fh); preg_match("/(\.[a-z0-9]+)[ \t]+([^ \t\n\r]*)/i", $line, $result); - if ($extension == $result[1]){ + if ($extension == $result[1]) { $ext_found = true; $contentType = $result[2]; } @@ -1025,24 +1069,24 @@ class ContactModule extends Module { } } } - - + + $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'); + + $fhandle = fopen($tmpname, 'w'); $buffer = null; - for($i = 0; $i < $stat["cb"]; $i += BLOCK_SIZE) { + for ($i = 0; $i < $stat["cb"]; $i += BLOCK_SIZE) { // Write stream $buffer = mapi_stream_read($stream, BLOCK_SIZE); - fwrite($fhandle,$buffer,strlen($buffer)); + fwrite($fhandle, $buffer, strlen($buffer)); } fclose($fhandle); - + $response = array(); $response['tmpname'] = $tmpname; $response['filename'] = $filename; @@ -1065,10 +1109,13 @@ class ContactModule extends Module { } } - private function startswith($haystack, $needle) { + private function startswith($haystack, $needle) + { $haystack = str_replace("type=", "", $haystack); // remove type from string return substr($haystack, 0, strlen($needle)) === $needle; } -}; +} + +; ?> diff --git a/php/plugin.contactimporter.php b/php/plugin.contactimporter.php index 031e0fc..5484993 100644 --- a/php/plugin.contactimporter.php +++ b/php/plugin.contactimporter.php @@ -3,7 +3,7 @@ * plugin.contactimporter.php, zarafa contact to vcf im/exporter * * Author: Christoph Haas - * Copyright (C) 2012-2013 Christoph Haas + * Copyright (C) 2012-2016 Christoph Haas * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -28,18 +28,22 @@ require_once __DIR__ . "/download.php"; * With this plugin you can import a vcf file to your zarafa addressbook * */ -class Plugincontactimporter extends Plugin { +class Plugincontactimporter extends Plugin +{ /** * Constructor */ - function Plugincontactimporter() {} + function Plugincontactimporter() + { + } /** * Function initializes the Plugin and registers all hooks * * @return void */ - function init() { + function init() + { $this->registerHook('server.core.settings.init.before'); $this->registerHook('server.index.load.custom'); } @@ -51,8 +55,9 @@ class Plugincontactimporter extends Plugin { * @param mixed $data object(s) related to the hook * @return void */ - function execute($eventID, &$data) { - switch($eventID) { + function execute($eventID, &$data) + { + switch ($eventID) { case 'server.core.settings.init.before' : $this->injectPluginSettings($data); break; @@ -69,15 +74,15 @@ class Plugincontactimporter extends Plugin { * settings. * @param Array $data Reference to the data of the triggered hook */ - function injectPluginSettings(&$data) { + 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 + 'enable' => PLUGIN_CONTACTIMPORTER_USER_DEFAULT_ENABLE, + 'default_addressbook' => PLUGIN_CONTACTIMPORTER_DEFAULT ) ) ) @@ -85,4 +90,5 @@ class Plugincontactimporter extends Plugin { )); } } + ?> diff --git a/php/upload.php b/php/upload.php index aaaf720..73b97c8 100644 --- a/php/upload.php +++ b/php/upload.php @@ -3,7 +3,7 @@ * upload.php, zarafa contact to vcf im/exporter * * Author: Christoph Haas - * Copyright (C) 2012-2013 Christoph Haas + * Copyright (C) 2012-2016 Christoph Haas * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -20,17 +20,18 @@ * 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) { +/** + * respond/echo JSON + * @param $arr + */ +function respondJSON($arr) +{ echo json_encode($arr); } @@ -39,11 +40,12 @@ function respondJSON($arr) { * @param $length the lenght of the generated string * @return string a random string */ -function randomstring($length = 6) { +function randomstring($length = 6) +{ // $chars - all allowed charakters $chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"; - srand((double)microtime()*1000000); + srand((double)microtime() * 1000000); $i = 0; $pass = ""; while ($i < $length) { @@ -58,15 +60,15 @@ function randomstring($length = 6) { $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)); +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!")); + 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!")); + respondJSON(array('success' => false, 'error' => "File could not be read by server, upload error!")); } ?> \ No newline at end of file diff --git a/resources/css/contactimporter-main.css b/resources/css/contactimporter-main.css index 97f1e5f..cf35f6c 100644 --- a/resources/css/contactimporter-main.css +++ b/resources/css/contactimporter-main.css @@ -1,12 +1,12 @@ .icon_contactimporter_button { - background: url(../images/import_icon.png) no-repeat !important; - background-repeat: no-repeat; - background-position: center; - background-size: 18px!important; + background: url(../images/import_icon.png) no-repeat !important; + background-repeat: no-repeat; + background-position: center; + background-size: 18px !important; } .icon_contactimporter_export { - background: url(../images/download.png) no-repeat; - background-repeat: no-repeat; - background-position: center; + background: url(../images/download.png) no-repeat; + background-repeat: no-repeat; + background-position: center; } From 35e1cf5b8e5ff28c9cd0ed04c5909cd3b30416bc Mon Sep 17 00:00:00 2001 From: Christoph Haas Date: Tue, 14 Jun 2016 00:21:04 +0200 Subject: [PATCH 09/16] download whole contact folders --- js/plugin.contactimporter.js | 55 +++++++++---- js/ui/ContextMenu.js | 108 +++++++++++++++++++++++++ manifest.xml | 1 + php/module.contact.php | 28 ++++++- resources/css/contactimporter-main.css | 12 +++ 5 files changed, 186 insertions(+), 18 deletions(-) create mode 100644 js/ui/ContextMenu.js diff --git a/js/plugin.contactimporter.js b/js/plugin.contactimporter.js index 8d9843d..904b900 100644 --- a/js/plugin.contactimporter.js +++ b/js/plugin.contactimporter.js @@ -63,12 +63,18 @@ Zarafa.plugins.contactimporter.ImportPlugin = Ext.extend(Zarafa.core.Plugin, { */ createImportButton: function () { var button = { - xtype : 'button', - text : _('Import Contacts'), - iconCls : 'icon_contactimporter_button', + xtype: 'panel', + cls: 'zarafa-ciplg-container', + layout: 'fit', navigationContext: container.getContextByName('contact'), - handler : this.onImportButtonClick, - scope : this + items: [{ + xtype : 'button', + text : _('Import Contacts'), + iconCls : 'icon_contactimporter_button', + cls: 'zarafa-ciplg-button', + handler : this.onImportButtonClick, + scope : this + }] }; return button; @@ -119,19 +125,28 @@ Zarafa.plugins.contactimporter.ImportPlugin = Ext.extend(Zarafa.core.Plugin, { }, downloadVCF: function (response) { - var downloadFrame = Ext.getBody().createChild({ - tag: 'iframe', - cls: 'x-hidden' - }); + if(response.status == false) { + Zarafa.common.dialogs.MessageBox.show({ + title : dgettext('plugin_files', 'Warning'), + msg : dgettext('plugin_files', response.message), + icon : Zarafa.common.dialogs.MessageBox.WARNING, + buttons: Zarafa.common.dialogs.MessageBox.OK + }); + } else { + var downloadFrame = Ext.getBody().createChild({ + tag: 'iframe', + cls: 'x-hidden' + }); - var url = document.URL; - var link = url.substring(0, url.lastIndexOf('/') + 1); + var url = document.URL; + var link = url.substring(0, url.lastIndexOf('/') + 1); - link += "index.php?sessionid=" + container.getUser().getSessionId() + "&load=custom&name=download_vcf"; - link = Ext.urlAppend(link, "token=" + encodeURIComponent(response.download_token)); - link = Ext.urlAppend(link, "filename=" + encodeURIComponent(response.filename)); + link += "index.php?sessionid=" + container.getUser().getSessionId() + "&load=custom&name=download_vcf"; + link = Ext.urlAppend(link, "token=" + encodeURIComponent(response.download_token)); + link = Ext.urlAppend(link, "filename=" + encodeURIComponent(response.filename)); - downloadFrame.dom.contentWindow.location = link; + downloadFrame.dom.contentWindow.location = link; + } }, /** @@ -271,6 +286,13 @@ Zarafa.plugins.contactimporter.ImportPlugin = Ext.extend(Zarafa.core.Plugin, { case Zarafa.core.data.SharedComponentType['plugins.contactimporter.dialogs.importcontacts']: bid = 1; break; + case Zarafa.core.data.SharedComponentType['common.contextmenu']: + if (record instanceof Zarafa.core.data.MAPIRecord) { + if (record.get('object_type') == Zarafa.core.mapi.ObjectType.MAPI_FOLDER) { + bid = 2; + } + } + break; } return bid; }, @@ -288,6 +310,9 @@ Zarafa.plugins.contactimporter.ImportPlugin = Ext.extend(Zarafa.core.Plugin, { case Zarafa.core.data.SharedComponentType['plugins.contactimporter.dialogs.importcontacts']: component = Zarafa.plugins.contactimporter.dialogs.ImportContentPanel; break; + case Zarafa.core.data.SharedComponentType['common.contextmenu']: + component = Zarafa.plugins.contactimporter.ui.ContextMenu; + break; } return component; diff --git a/js/ui/ContextMenu.js b/js/ui/ContextMenu.js new file mode 100644 index 0000000..a8e104c --- /dev/null +++ b/js/ui/ContextMenu.js @@ -0,0 +1,108 @@ +Ext.namespace('Zarafa.plugins.contactimporter.ui'); + +/** + * @class Zarafa.plugins.contactimporter.ui.ContextMenu + * @extends Zarafa.hierarchy.ui.ContextMenu + * @xtype contactimporter.hierarchycontextmenu + */ +Zarafa.plugins.contactimporter.ui.ContextMenu = Ext.extend(Zarafa.hierarchy.ui.ContextMenu, { + + /** + * @constructor + * @param {Object} config Configuration object + */ + constructor : function(config) + { + config = config || {}; + + if (config.contextNode) { + config.contextTree = config.contextNode.getOwnerTree(); + } + + Zarafa.plugins.contactimporter.ui.ContextMenu.superclass.constructor.call(this, config); + + // add item to menu + var additionalItems = this.createAdditionalContextMenuItems(config); + for(var i=0; i js/data/ResponseHandler.js js/dialogs/ImportContentPanel.js js/dialogs/ImportPanel.js + js/ui/ContextMenu.js resources/css/contactimporter.css diff --git a/php/module.contact.php b/php/module.contact.php index 6894b99..4cf09f9 100644 --- a/php/module.contact.php +++ b/php/module.contact.php @@ -272,6 +272,12 @@ class ContactModule extends Module $records = $actionData["records"]; } + // Get folders + $folder = false; + if (isset($actionData["folder"])) { + $folder = $actionData["folder"]; + } + $response = array(); $error = false; $error_msg = ""; @@ -283,6 +289,17 @@ class ContactModule extends Module $store = $GLOBALS["mapisession"]->openMessageStore(hex2bin($storeid)); if ($store) { + // load folder first + if($folder !== false) { + $mapifolder = mapi_msgstore_openentry($store, hex2bin($folder)); + + $table = mapi_folder_getcontentstable($mapifolder); + $list = mapi_table_queryallrows($table, array(PR_ENTRYID)); + + foreach ($list as $item) { + $records[] = bin2hex($item[PR_ENTRYID]); + } + } for ($index = 0, $count = count($records); $index < $count; $index++) { // define vcard $vcard = new VCard(); @@ -412,9 +429,14 @@ class ContactModule extends Module return false; } - $response['status'] = true; - $response['download_token'] = $token; - $response['filename'] = count($records) . "contacts.vcf"; + if(count($records) > 0) { + $response['status'] = true; + $response['download_token'] = $token; + $response['filename'] = count($records) . "contacts.vcf"; + } else { + $response['status'] = false; + $response['message'] = "No contacts found. Export skipped!"; + } $this->addActionData($actionType, $response); $GLOBALS["bus"]->addData($this->getResponseData()); diff --git a/resources/css/contactimporter-main.css b/resources/css/contactimporter-main.css index cf35f6c..397d613 100644 --- a/resources/css/contactimporter-main.css +++ b/resources/css/contactimporter-main.css @@ -10,3 +10,15 @@ background-repeat: no-repeat; background-position: center; } + +.zarafa-ciplg-container { + width: 100%; + height: 30px; +} + +.zarafa-ciplg-button.x-btn-small { + width: 80%; + height: 25px; + margin-left: auto; + margin-right: auto; +} \ No newline at end of file From 02569735073d81976d1d56a7d8afbd4d7ceb3bdf Mon Sep 17 00:00:00 2001 From: Christoph Haas Date: Tue, 14 Jun 2016 00:23:34 +0200 Subject: [PATCH 10/16] only show download button on contact folders --- js/plugin.contactimporter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/plugin.contactimporter.js b/js/plugin.contactimporter.js index 904b900..e6338c8 100644 --- a/js/plugin.contactimporter.js +++ b/js/plugin.contactimporter.js @@ -288,7 +288,7 @@ Zarafa.plugins.contactimporter.ImportPlugin = Ext.extend(Zarafa.core.Plugin, { break; case Zarafa.core.data.SharedComponentType['common.contextmenu']: if (record instanceof Zarafa.core.data.MAPIRecord) { - if (record.get('object_type') == Zarafa.core.mapi.ObjectType.MAPI_FOLDER) { + if (record.get('object_type') == Zarafa.core.mapi.ObjectType.MAPI_FOLDER && record.get('container_class') == "IPF.Contact") { bid = 2; } } From 1d4605b23ad27f379509596d9bec82603aebc250 Mon Sep 17 00:00:00 2001 From: Christoph Haas Date: Tue, 14 Jun 2016 00:57:00 +0200 Subject: [PATCH 11/16] some ui improvements --- js/dialogs/ImportContentPanel.js | 3 ++- js/dialogs/ImportPanel.js | 10 ++++++++- js/plugin.contactimporter.js | 3 ++- js/ui/ContextMenu.js | 30 +++++++++++++++++++++++-- resources/css/contactimporter-main.css | 10 +++++++-- resources/images/upload.png | Bin 0 -> 213 bytes resources/images/upload.xcf | Bin 0 -> 930 bytes 7 files changed, 49 insertions(+), 7 deletions(-) create mode 100644 resources/images/upload.png create mode 100755 resources/images/upload.xcf diff --git a/js/dialogs/ImportContentPanel.js b/js/dialogs/ImportContentPanel.js index 4a58ab7..973be71 100644 --- a/js/dialogs/ImportContentPanel.js +++ b/js/dialogs/ImportContentPanel.js @@ -53,7 +53,8 @@ Zarafa.plugins.contactimporter.dialogs.ImportContentPanel = Ext.extend(Zarafa.co items : [ { xtype : 'contactimporter.importcontactpanel', - filename: config.filename + filename: config.filename, + folder: config.folder } ] }); diff --git a/js/dialogs/ImportPanel.js b/js/dialogs/ImportPanel.js index 8f65415..486d5d0 100644 --- a/js/dialogs/ImportPanel.js +++ b/js/dialogs/ImportPanel.js @@ -39,6 +39,9 @@ Zarafa.plugins.contactimporter.dialogs.ImportPanel = Ext.extend(Ext.Panel, { /* The store for the selection grid */ store : null, + /* selected folder */ + folder : null, + /** * @constructor * @param {object} config @@ -51,6 +54,10 @@ Zarafa.plugins.contactimporter.dialogs.ImportPanel = Ext.extend(Ext.Panel, { this.vcffile = config.filename; } + if (!Ext.isEmpty(config.folder)) { + this.folder = config.folder; + } + // create the data store // we only display the firstname, lastname, homephone and primary email address in our grid this.store = new Ext.data.ArrayStore({ @@ -224,7 +231,7 @@ Zarafa.plugins.contactimporter.dialogs.ImportPanel = Ext.extend(Ext.Panel, { name : "choosen_addressbook", value : container.getSettingsModel().get("zarafa/v1/plugins/contactimporter/default_addressbook"), width : 100, - fieldLabel : "Select an addressbook", + fieldLabel : "Select folder", store : myStore, mode : 'local', labelSeperator: ":", @@ -245,6 +252,7 @@ Zarafa.plugins.contactimporter.dialogs.ImportPanel = Ext.extend(Ext.Panel, { emptyText : 'Select an .vcf addressbook', border : false, anchor : "100%", + height : "30", scope : this, allowBlank : false, listeners : { diff --git a/js/plugin.contactimporter.js b/js/plugin.contactimporter.js index e6338c8..52e88e4 100644 --- a/js/plugin.contactimporter.js +++ b/js/plugin.contactimporter.js @@ -266,7 +266,8 @@ Zarafa.plugins.contactimporter.ImportPlugin = Ext.extend(Zarafa.core.Plugin, { openImportDialog: function (filename) { var componentType = Zarafa.core.data.SharedComponentType['plugins.contactimporter.dialogs.importcontacts']; var config = { - filename: filename + filename: filename, + modal: true }; Zarafa.core.data.UIFactory.openLayerComponent(componentType, undefined, config); diff --git a/js/ui/ContextMenu.js b/js/ui/ContextMenu.js index a8e104c..8d5037a 100644 --- a/js/ui/ContextMenu.js +++ b/js/ui/ContextMenu.js @@ -43,7 +43,19 @@ Zarafa.plugins.contactimporter.ui.ContextMenu = Ext.extend(Zarafa.hierarchy.ui.C return [{ xtype: 'menuseparator' }, { - text : _('Export VCF'), + text : _('Import vCard'), + iconCls : 'icon_contactimporter_import', + handler : this.onContextItemImport, + beforeShow : function(item, record) { + var access = record.get('access') & Zarafa.core.mapi.Access.ACCESS_MODIFY; + if (!access || (record.isIPMSubTree() && !record.getMAPIStore().isDefaultStore())) { + item.setDisabled(true); + } else { + item.setDisabled(false); + } + } + }, { + text : _('Export vCard'), iconCls : 'icon_contactimporter_export', handler : this.onContextItemExport, beforeShow : function(item, record) { @@ -58,7 +70,7 @@ Zarafa.plugins.contactimporter.ui.ContextMenu = Ext.extend(Zarafa.hierarchy.ui.C }, /** - * Fires on selecting 'Open' menu option from {@link Zarafa.hierarchy.ui.ContextMenu ContextMenu} + * Fires on selecting 'Open' menu option from {@link Zarafa.plugins.contactimporter.ui.ContextMenu ContextMenu} * @private */ onContextItemExport: function () { @@ -79,6 +91,20 @@ Zarafa.plugins.contactimporter.ui.ContextMenu = Ext.extend(Zarafa.hierarchy.ui.C ); }, + /** + * Fires on selecting 'Open' menu option from {@link Zarafa.plugins.contactimporter.ui.ContextMenu ContextMenu} + * @private + */ + onContextItemImport: function () { + var componentType = Zarafa.core.data.SharedComponentType['plugins.contactimporter.dialogs.importcontacts']; + var config = { + modal: true, + folder: this.records.get("entryid") + }; + + Zarafa.core.data.UIFactory.openLayerComponent(componentType, undefined, config); + }, + downloadVCF: function (response) { if(response.status == false) { Zarafa.common.dialogs.MessageBox.show({ diff --git a/resources/css/contactimporter-main.css b/resources/css/contactimporter-main.css index 397d613..258f31c 100644 --- a/resources/css/contactimporter-main.css +++ b/resources/css/contactimporter-main.css @@ -11,14 +11,20 @@ background-position: center; } +.icon_contactimporter_import { + background: url(../images/upload.png) no-repeat; + background-repeat: no-repeat; + background-position: center; +} + .zarafa-ciplg-container { width: 100%; height: 30px; } -.zarafa-ciplg-button.x-btn-small { +.zarafa-ciplg-button .x-btn-small { width: 80%; height: 25px; margin-left: auto; margin-right: auto; -} \ No newline at end of file +} diff --git a/resources/images/upload.png b/resources/images/upload.png new file mode 100644 index 0000000000000000000000000000000000000000..9e3158a4f4363faf5cf39ad03980820bc814009d GIT binary patch literal 213 zcmeAS@N?(olHy`uVBq!ia0vp^d?3uh1|;P@bT0xawj^(N7l!{JxM1({$v_d#0*}aI z1_o|n5N2eUHAey{$X?><>&pIsjaN)jHlV|5IZ!Cc)5S4Fi9Qd7{WOD*hM zLXX~YP<+N0a`aB8o`m@YxdzFt5C5ncAI?7iQ*K>mpY_V4&*BdQZDa6s^>bP0l+XkK DM?FXf literal 0 HcmV?d00001 diff --git a/resources/images/upload.xcf b/resources/images/upload.xcf new file mode 100755 index 0000000000000000000000000000000000000000..e89500d6498ef0f592c4e42e5c72a00ef6cb91dc GIT binary patch literal 930 zcmd6k%~HZJ5XaL}q0|p_P%nDO5e{|m6?g`ddnrxKOlg_6&cH1*zJL$lo5~P(A;B`@ z-JSVof8CF?yOnCOq^nd=q4JD?U0EUDfIJV5At;=F1fMA2TToziZ#~xCLH#56YYErJ zYLUn+mX(@Ah(O*Wqqgg}gEjBn5qP(B z+~MG*N{U*P)gozBSzwiq2Ur(`Yd4_KGPd+BgCn?ptU-%Fj?Y_s(c;S%4}kGrXFUMd z-~Qz`?@ukb&xcx{AIE%w&kt-!JRXrR&KM^>vgOQYTTaf90;CR77s*2k*@hF&qJ65v y`s7E!f4|Pauo{Nd#%LT?8{cd-4y#Ro1jB06MS@|q3C%_mZK8duV_?{W0`d(*c)UOW literal 0 HcmV?d00001 From 5bd9a496df31b54690e3513e8d3fa6371ad45716 Mon Sep 17 00:00:00 2001 From: Christoph Haas Date: Tue, 14 Jun 2016 01:09:52 +0200 Subject: [PATCH 12/16] more ui improvements --- js/plugin.contactimporter.js | 39 ++----------------------- resources/css/contactimporter-main.css | 10 +++---- resources/images/import_icon.png | Bin 926 -> 407 bytes 3 files changed, 7 insertions(+), 42 deletions(-) diff --git a/js/plugin.contactimporter.js b/js/plugin.contactimporter.js index 52e88e4..1557d2c 100644 --- a/js/plugin.contactimporter.js +++ b/js/plugin.contactimporter.js @@ -48,38 +48,10 @@ Zarafa.plugins.contactimporter.ImportPlugin = Ext.extend(Zarafa.core.Plugin, { /* directly import received vcfs */ this.registerInsertionPoint('common.contextmenu.attachment.actions', this.createAttachmentImportButton, this); - /* add import button to south navigation */ - this.registerInsertionPoint("navigation.south", this.createImportButton, this); - /* export a contact via rightclick */ this.registerInsertionPoint('context.contact.contextmenu.actions', this.createItemExportInsertionPoint, this); }, - /** - * Creates the button - * - * @return {Object} Configuration object for a {@link Ext.Button button} - * - */ - createImportButton: function () { - var button = { - xtype: 'panel', - cls: 'zarafa-ciplg-container', - layout: 'fit', - navigationContext: container.getContextByName('contact'), - items: [{ - xtype : 'button', - text : _('Import Contacts'), - iconCls : 'icon_contactimporter_button', - cls: 'zarafa-ciplg-button', - handler : this.onImportButtonClick, - scope : this - }] - }; - - return button; - }, - /** * This method hooks to the contact context menu and allows users to export users to vcf. * @@ -89,7 +61,7 @@ Zarafa.plugins.contactimporter.ImportPlugin = Ext.extend(Zarafa.core.Plugin, { */ createItemExportInsertionPoint: function (include, btn) { return { - text : dgettext('plugin_files', 'Export VCF'), + text : dgettext('plugin_files', 'Export vCard'), handler: this.exportToVCF.createDelegate(this, [btn]), scope : this, iconCls: 'icon_contactimporter_export' @@ -156,7 +128,7 @@ Zarafa.plugins.contactimporter.ImportPlugin = Ext.extend(Zarafa.core.Plugin, { */ createAttachmentImportButton: function (include, btn) { return { - text : _('Import Contacts'), + text : _('Import to Contacts'), handler : this.getAttachmentFileName.createDelegate(this, [btn]), scope : this, iconCls : 'icon_contactimporter_button', @@ -251,13 +223,6 @@ Zarafa.plugins.contactimporter.ImportPlugin = Ext.extend(Zarafa.core.Plugin, { ); }, - /** - * Clickhandler for the button - */ - onImportButtonClick: function () { - this.openImportDialog(); - }, - /** * Open the import dialog. * diff --git a/resources/css/contactimporter-main.css b/resources/css/contactimporter-main.css index 258f31c..cee0e8a 100644 --- a/resources/css/contactimporter-main.css +++ b/resources/css/contactimporter-main.css @@ -2,7 +2,6 @@ background: url(../images/import_icon.png) no-repeat !important; background-repeat: no-repeat; background-position: center; - background-size: 18px !important; } .icon_contactimporter_export { @@ -19,12 +18,13 @@ .zarafa-ciplg-container { width: 100%; - height: 30px; + height: 50px; } .zarafa-ciplg-button .x-btn-small { width: 80%; - height: 25px; - margin-left: auto; - margin-right: auto; + height: 30px; + margin-left: 10%; + margin-right: 10%; + margin-top: 10px; } diff --git a/resources/images/import_icon.png b/resources/images/import_icon.png index 1588aedad9a626da151f7b9bbe02a33151a393dd..fb8d7bbed78ccd1c067a28bc765f5bd977e347b4 100644 GIT binary patch literal 407 zcmeAS@N?(olHy`uVBq!ia0vp^LLkh+1|-AI^@Rf|wj^(N7l!{JxM1({$v_d#0*}aI z1_o|n5N2eUHAey{$X?><>&pIsjaQseTt#}W4g&+Dil>WXh{fr(Qx@hm2Z*%okJj47 z)9Tc_gyp6}0LLljFP_svl(n@gn2v;`>T>csY|<8pxNx<$A#b(jnG2jz5sAOA+M3tz z`#pco9c6}Tr=JQh__W()^}0rW&8N#V6!II+TkK_Ce_5@QUqac@?t{(C;upKlmF-sb z4Q^6!xV**h{T6JTWh=KAY**LEr7?v-$>SGq6% zybP0l+XkKltQ5T literal 926 zcmV;P17ZA$P)CwiHge1?a-Ieel3`vU?<6eCSH?D@TGC#7GKu_vL^M0G5(=RZ zvSiYxH{k5-Z2I`}@)FHvlR{P{;5Nl#@t@br4SwP z(s36tt6W_VjOipunD<_-Rtx=ppF%2?3fk@V$^-DK!zxElI*z?R?7A*fP?{2gwIgad zHf02FSCnnDDMPO|+LXbAA)%0HC=?15q7$`RjY4$d^wTMY=)}gx28HOv$?*w==*0Hx zZ3@wetruINml^tJSxNtmAV&y=kW~nws1;?;;5lWH`v@5`n#dUk5i(+4kuwS+IKP5Si zvj(69AO|4B^Gl$u?Z{ogFQk;e7zrVS5Rwr70Gr+EAA=c Date: Tue, 14 Jun 2016 01:18:00 +0200 Subject: [PATCH 13/16] TODO: rework folder selection --- js/dialogs/ImportPanel.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/js/dialogs/ImportPanel.js b/js/dialogs/ImportPanel.js index 486d5d0..caf6a0e 100644 --- a/js/dialogs/ImportPanel.js +++ b/js/dialogs/ImportPanel.js @@ -132,6 +132,16 @@ Zarafa.plugins.contactimporter.dialogs.ImportPanel = Ext.extend(Ext.Panel, { }; }, + getAllContactFolders: function() { + var allFolders = []; + + /** + * TODO: + * container.getHierarchyStore() -> 2 stores: inbux und public + * inbox -> substores -> alle folder! + */ + }, + /** * Reloads the data of the grid * @private From e0eb5cf1f03fa3751087a739daeab02d6456be56 Mon Sep 17 00:00:00 2001 From: Christoph Haas Date: Thu, 16 Jun 2016 21:37:55 +0200 Subject: [PATCH 14/16] reworked folder selection hide dropdown if import via context menu --- config.php | 4 +- js/dialogs/ImportPanel.js | 118 ++++++++++++++++++++++++++------------ 2 files changed, 84 insertions(+), 38 deletions(-) diff --git a/config.php b/config.php index f64d22f..ac536fc 100644 --- a/config.php +++ b/config.php @@ -2,8 +2,8 @@ /** Disable the import plugin for all clients */ define('PLUGIN_CONTACTIMPORTER_USER_DEFAULT_ENABLE', false); -/** The default addressbook to import to (default: contact)*/ -define('PLUGIN_CONTACTIMPORTER_DEFAULT', "contact"); +/** The default addressbook to import to (default: Kontakte or Contacts - depending on your language)*/ +define('PLUGIN_CONTACTIMPORTER_DEFAULT', "Kontakte"); /** Tempory path for uploaded files... */ define('PLUGIN_CONTACTIMPORTER_TMP_UPLOAD', "/var/lib/zarafa-webapp/tmp/"); diff --git a/js/dialogs/ImportPanel.js b/js/dialogs/ImportPanel.js index caf6a0e..c8083a2 100644 --- a/js/dialogs/ImportPanel.js +++ b/js/dialogs/ImportPanel.js @@ -132,14 +132,87 @@ Zarafa.plugins.contactimporter.dialogs.ImportPanel = Ext.extend(Ext.Panel, { }; }, - getAllContactFolders: function() { + getAllContactFolders: function(asDropdownStore) { + asDropdownStore = Ext.isEmpty(asDropdownStore) ? false : asDropdownStore; + var allFolders = []; - /** - * TODO: - * container.getHierarchyStore() -> 2 stores: inbux und public - * inbox -> substores -> alle folder! - */ + var defaultContactFolder = container.getHierarchyStore().getDefaultFolder('contact'); + + var inbox = container.getHierarchyStore().getDefaultStore(); + var pub = container.getHierarchyStore().getPublicStore(); + + if(!Ext.isEmpty(inbox.subStores) && inbox.subStores.folders.totalLength > 0) { + for(var i=0; i < inbox.subStores.folders.totalLength; i++) { + var folder = inbox.subStores.folders.getAt(i); + if(folder.get("container_class") == "IPF.Contact") { + if(asDropdownStore) { + allFolders.push([ + folder.get("entryid"), + folder.get("display_name") + ]); + } else { + allFolders.push({ + display_name : folder.get("display_name"), + entryid : folder.get("entryid"), + store_entryid: folder.get("store_entryid"), + is_public : false + }); + } + } + } + } + + if(!Ext.isEmpty(pub.subStores) && pub.subStores.folders.totalLength > 0) { + for(var j=0; j < pub.subStores.folders.totalLength; j++) { + var folder = pub.subStores.folders.getAt(j); + if(folder.get("container_class") == "IPF.Contact") { + if(asDropdownStore) { + allFolders.push([ + folder.get("entryid"), + folder.get("display_name") + " (Public)" + ]); + } else { + allFolders.push({ + display_name : folder.get("display_name"), + entryid : folder.get("entryid"), + store_entryid: folder.get("store_entryid"), + is_public : true + }); + } + } + } + } + + if(asDropdownStore) { + return allFolders.sort(this.dynamicSort(1)); + } else { + return allFolders; + } + }, + + dynamicSort: function(property) { + var sortOrder = 1; + if(property[0] === "-") { + sortOrder = -1; + property = property.substr(1); + } + return function (a,b) { + var result = (a[property].toLowerCase() < b[property].toLowerCase()) ? -1 : (a[property].toLowerCase() > b[property].toLowerCase()) ? 1 : 0; + return result * sortOrder; + } + }, + + getContactFolderByName: function(name) { + var folders = this.getAllContactFolders(false); + + for(var i=0; i