' which should be the default anyhow. * * Copyright 2005-2006 Karsten Fourmont * * See the enclosed file COPYING for license information (LGPL). If you did not * receive this file, see http://www.fsf.org/copyleft/lgpl.html. * * $Horde: framework/SyncML/SyncML/Device/Sync4j.php,v 1.16 2006/10/07 13:17:29 karsten Exp $ * * @author Karsten Fourmont * @package SyncML */ class SyncML_Device_sync4j extends SyncML_Device { function getPreferredContentTypeClient($serverSyncURI, $sourceSyncURI) { if (stripos($serverSyncURI, 'notes') !== false || stripos($serverSyncURI, 'memo') !== false) { // SynCML conformance suite expects this rather than text/x-vnote return 'text/x-vnote'; } return parent::getPreferredContentTypeClient($serverSyncURI, $sourceSyncURI); } /** * Convert the content. * * Currently strips uid (primary key) information as client and * server might use different ones. * * Charset conversions might be added here too. */ function convertClient2Server($content, $contentType) { list($content, $contentType) = parent::convertClient2Server(base64_decode($content), $contentType); switch ($contentType) { case 'text/x-s4j-sifn' : $content = SyncML_Device_sync4j::sif2vnote($content); $contentType = 'text/x-vnote'; break; case 'text/x-s4j-sifc' : $content = SyncML_Device_sync4j::sif2vcard($content); $contentType = 'text/x-vcard'; break; case 'text/x-s4j-sife' : $content = SyncML_Device_sync4j::sif2vevent($content); $contentType = 'text/x-vevent'; break; case 'text/x-s4j-sift' : $content = SyncML_Device_sync4j::sif2vtodo($content); $contentType = 'text/x-vtodo'; break; } // This code is required for recurring events. // Always remove client UID. UID will be seperately passed in XML. $content = preg_replace('/(\r\n|\r|\n)UID:.*?(\r\n|\r|\n)/', '\1', $content, 1); // Ensure valid newline termination. if (substr($content, -1) != "\n" && substr($content, -1) != "\r") { $content .= "\r\n"; } // The above code is required for recurring events. if (!empty($GLOBALS['SyncML_debugDir']) && is_dir($GLOBALS['SyncML_debugDir'])) { $fp = @fopen($GLOBALS['SyncML_debugDir'] . '/data.txt', 'a'); if ($fp) { @fwrite($fp, "\ninput converted for server: $contentType\n"); @fwrite($fp,$content . "\n"); @fclose($fp); } } return array($content, $contentType); } /** * Converts the content from the backend to a format suitable for the * client device. * * Strips the uid (primary key) information as client and server might use * different ones. * * @param string $content The content to convert * @param string $contentType The contentType of content as returned from * the backend * @return array array($newcontent, $newcontentType, $newEncodingType): * the converted content and the * (possibly changed) new ContentType and EncodingType * (like b64 as used by Funambol/Sync4J). */ function convertServer2Client($content, $contentType) { global $backend; list($content, $contentType, $encodingType) = parent::convertServer2Client($content, $contentType); switch ($contentType) { case 'text/calendar' : case 'text/x-icalendar' : case 'text/x-vcalendar' : case 'text/x-vevent' : $content = SyncML_Device_sync4j::vevent2sif($content); $content = base64_encode($content); $contentType = 'text/x-s4j-sife'; break; case 'text/x-vtodo' : $content = SyncML_Device_sync4j::vtodo2sif($content); $content = base64_encode($content); $contentType = 'text/x-s4j-sift'; break; case 'text/x-vcard' : $content = SyncML_Device_sync4j::vcard2sif($content); $content = base64_encode($content); $contentType = 'text/x-s4j-sifc'; break; case 'text/x-vnote': case 'text/plain': $content = SyncML_Device_sync4j::vnote2sif($content); $content = base64_encode($content); $contentType = 'text/x-s4j-sifn'; break; } if (!empty($GLOBALS['SyncML_debugDir']) && is_dir($GLOBALS['SyncML_debugDir'])) { $fp = @fopen($GLOBALS['SyncML_debugDir'] . '/data.txt', 'a'); if ($fp) { @fwrite($fp, serialize($contentType)); @fwrite($fp, "\nconverted for sync4j client: $contentType\n"); @fwrite($fp, base64_decode($content) . "\n"); @fclose($fp); } } return array($content, $contentType, 'b64'); } /** * Decodes a sif xml string to an associative array. * * Quick hack to convert from text/vcard and text/vcalendar to * Sync4J's proprietery sif datatypes and vice versa. For details * about the sif format see the appendix of the developer guide on * www.sync4j.org. * * @access private * * @param string $sif A sif string like v1>v2 * * @return array Assoc array in utf8 like array ('k1' => 'v1>', 'k2' => 'v2'); */ function sif2array($sif) { $r = array(); if (preg_match_all('/<([^>]*)>([^<]*)<\/\1>/si', $sif, $matches, PREG_SET_ORDER)) { foreach ($matches as $match) { $r[$match[1]] = html_entity_decode($match[2]); } } return $r; } /** * Encodes an assoc. array to sif xml * * Quick hack to convert from text/vcard and text/vcalendar to * Sync4J's proprietery sif datatypes and vice versa. For details * about the sif format see the appendix of the developer guide on * www.sync4j.org. * * @access private * * @param array $array An assoc array. * * @return string The resulting XML string. */ function array2sif($array, $pre='', $post='') { $search = array('<', '>'); $replace = array('<', '>'); $r = $pre; foreach ($array as $key => $value) { if (is_a($value,'PEAR_Error')) { continue; } if (is_array($value)) { $value = $value[0]; } $r .= '<' . $key . '>' . str_replace($search, $replace, $value) . ''; } return $r . $post; } // ---------------------------------------------------- // This code starts the Sync4j to Horde synchronization // ---------------------------------------------------- // Synchronize Notes *from* Outlook *to* Horde. // Works perfectly! function sif2vnote($sif) { $a = SyncML_Device_sync4j::sif2array($sif); $iCal = &new Horde_iCalendar(); $iCal->setAttribute('VERSION', '1.1'); $iCal->setAttribute('PRODID', '-//The Horde Project//Mnemo //EN'); $iCal->setAttribute('METHOD', 'PUBLISH'); $vnote = &Horde_iCalendar::newComponent('vnote', $iCal); $vnote->setAttribute('BODY', isset($a['Body']) ? $a['Body'] : ''); $vnote->setAttribute('CATEGORIES', isset($a['Categories']) ? $a['Categories'] : ''); return $vnote->exportvCalendar(); } // Synchronize Tasks *from* Outlook *to* Horde. // Task Completed Status does not transfer to Horde. function sif2vtodo($sif) { $a = SyncML_Device_sync4j::sif2array($sif); $iCal = &new Horde_iCalendar(); $iCal->setAttribute('VERSION', '1.0'); $iCal->setAttribute('PRODID', '-//The Horde Project//Mnemo //EN'); $iCal->setAttribute('METHOD', 'PUBLISH'); $vtodo = &Horde_iCalendar::newComponent('vtodo', $iCal); $vtodo->setAttribute('SUMMARY', $a['Subject']); $vtodo->setAttribute('DESCRIPTION', $a['Body']); $vtodo->setAttribute('CATEGORIES', isset($a['Categories']) ? $a['Categories'] : ''); $vtodo->setAttribute('COMPLETED', $a['Complete'] == 'True' ? 1 : 0); if ($a['Importance'] == 0) { $vtodo->setAttribute('PRIORITY', 5); } elseif ($a['Importance'] == 2) { $vtodo->setAttribute('PRIORITY', 1); } else { $vtodo->setAttribute('PRIORITY', 3); } if (!empty($a['DueDate']) && $a['DueDate'] != '45001231T230000Z') { $vtodo->setAttribute('DUE', $iCal->_parseDateTime($a['DueDate'])); } return $vtodo->exportvCalendar(); } // Synchronize Calendar *from* Outlook *to* Horde. // Free/Busy Status is not maintained. function sif2vevent($sif) { $a = SyncML_Device_sync4j::sif2array($sif); $iCal = &new Horde_iCalendar(); $iCal->setAttribute('VERSION', '1.0'); $iCal->setAttribute('PRODID', '-//The Horde Project//Mnemo //EN'); $iCal->setAttribute('METHOD', 'PUBLISH'); $vEvent = &Horde_iCalendar::newComponent('vevent', $iCal); if ($a['AllDayEvent'] == 'True') { $t = $iCal->_parseDateTime($a['Start']); $vEvent->setAttribute('DTSTART', array('year' => date('Y', $t), 'month' => date('m', $t), 'mday' => date('d',$t)), array('VALUE' => 'DATE')); $t = $iCal->_parseDateTime($a['End']); $vEvent->setAttribute('DTEND', array('year' => date('Y', $t), 'month' => date('m', $t), 'mday' => date('d',$t)), array('VALUE' => 'DATE')); } else { $vEvent->setAttribute('DTSTART', $iCal->_parseDateTime($a['Start'])); $vEvent->setAttribute('DTEND', $iCal->_parseDateTime($a['End'])); } $vEvent->setAttribute('DTSTAMP', time()); $vEvent->setAttribute('SUMMARY', $a['Subject']); $vEvent->setAttribute('DESCRIPTION', $a['Body']); $vEvent->setAttribute('LOCATION', $a['Location']); $vEvent->setAttribute('CATEGORIES', $a['Categories']); $vEvent->setAttribute('STATUS', $a['FreeStatus']); // This code is used for Recurring Events. // Requires maskToString Function below. if ($a['IsRecurring'] == 1) { $typearray = array('DAILY','WEEKLY','MONTHLY','MONTHLY','NOTUSED','YEARLY'); $recurtype = $a['RecurrenceType']; $rule = "FREQ=" . $typearray[$recurtype] . ";"; $rule .= "INTERVAL=" . $a['Interval']; if ($recurtype == 1 || $recurtype == 3 || $recurtype == 6) { $rule .= ";BYDAY="; if ($recurtype == 3 || $recurtype == 6) { $rule .= $a['Instance']; } $rule .= $this->maskToString($a['DayOfWeekMask']); } if ($a['NoEndDate'] == 0) { $rule .= ';UNTIL=' . $a['PatternEndDate']; } $vEvent->setAttribute('RRULE', $rule); } // End Recurring Event Code. return $vEvent->exportvCalendar(); } // This code is required for Recurring Appointments. // Converts a SIF DayOfWeekMask to a string of days, used above function maskToString($mask) { $days = array('SU','MO','TU','WE','TH','FR','SA'); $result = ""; for ($i=0; $mask > 0; $i++) { if ($mask % 2 != 0) { $result .= $days[$i] . ","; $mask--; } $mask /= 2; } $result = substr_replace($result, "", -1); return $result; } // End Recurring Event Code. // Synchronize Contacts *from* Outlook *to* Horde. // Duplicates updated Contacts instead of replace. // Does not synchronize the FreebusyURL. function sif2vcard($sif) { $a = SyncML_Device_sync4j::sif2array($sif); $iCal = &new Horde_iCalendar(); $iCal->setAttribute('VERSION', '2.1'); $iCal->setAttribute('PRODID', '-//The Horde Project//Mnemo //EN'); $iCal->setAttribute('METHOD', 'PUBLISH'); $vcard = &Horde_iCalendar::newComponent('vcard', $iCal); $vcard->setAttribute('FN', $a['FileAs']); $vcard->setAttribute('NICKNAME', $a['NickName']); $vcard->setAttribute('TEL', $a['HomeTelephoneNumber'], array('TYPE'=>'HOME')); $vcard->setAttribute('TEL', $a['BusinessTelephoneNumber'], array('TYPE'=>'WORK')); $vcard->setAttribute('TEL', $a['MobileTelephoneNumber'], array('TYPE'=>'CELL')); $vcard->setAttribute('TEL', $a['BusinessFaxNumber'], array('TYPE'=>'FAX')); $vcard->setAttribute('EMAIL', $a['Email1Address']); $vcard->setAttribute('TITLE', $a['JobTitle']); $vcard->setAttribute('ORG', $a['CompanyName']); $vcard->setAttribute('NOTE', $a['Body']); $vcard->setAttribute('URL', $a['WebPage']); $vcard->setAttribute('FREEBUSYURL', $a['Freebusy']); $v = array( VCARD_N_FAMILY => $a['LastName'], VCARD_N_GIVEN => $a['FirstName'], VCARD_N_ADDL => $a['MiddleName'], VCARD_N_PREFIX => $a['Title'], VCARD_N_SUFFIX => $a['Suffix'] ); $vcard->setAttribute('N', implode(';', $v), array(), false, $v); $v = array( VCARD_ADR_POB => $a['HomeAddressPostOfficeBox'], VCARD_ADR_EXTEND => '', VCARD_ADR_STREET => $a['HomeAddressStreet'], VCARD_ADR_LOCALITY => $a['HomeAddressCity'], VCARD_ADR_REGION => $a['HomeAddressState'], VCARD_ADR_POSTCODE => $a['HomeAddressPostalCode'], VCARD_ADR_COUNTRY => $a['HomeAddressCountry'], ); $vcard->setAttribute('ADR', implode(';', $v), array('TYPE' => 'HOME' ), true, $v); $v = array( VCARD_ADR_POB => $a['BusinessAddressPostOfficeBox'], VCARD_ADR_EXTEND => '', VCARD_ADR_STREET => $a['BusinessAddressStreet'], VCARD_ADR_LOCALITY => $a['BusinessAddressCity'], VCARD_ADR_REGION => $a['BusinessAddressState'], VCARD_ADR_POSTCODE => $a['BusinessAddressPostalCode'], VCARD_ADR_COUNTRY => $a['BusinessAddressCountry'], ); $vcard->setAttribute('ADR', implode(';', $v), array('TYPE' => 'WORK' ), true, $v); $vcard->setAttribute('CATEGORIES', isset($a['Categories']) ? $a['Categories'] : ''); return $vcard->exportvCalendar(); } // ---------------------------------------------------- // This code starts the Horde to Sync4j synchronization // ---------------------------------------------------- // Synchronize Notes *from* Horde *to* Outlook. // This is working perfectly. function vnote2sif($vnote) { if (strpos($vnote,'BEGIN:') === false) { // handle plain text: $a = array('Body' => $vnote); } else { $iCal = &new Horde_iCalendar(); if (!$iCal->parsevCalendar($vnote)) { die("There was an error importing the data."); } $components = $iCal->getComponents(); if (!is_Array($components) || count($components) == 0) { $a = array('Body' => 'Conversion error in Sync4j.php: no data found!'); } else { $a = array('Body' => $components[0]->getAttribute('BODY'), 'Categories' => $components[0]->getAttribute('CATEGORIES')); $sum = $components[0]->getAttribute('SUMMARY'); if (!is_a($sum,'PEAR_Error')) { $a['Subject'] = $sum; } } } return SyncML_Device_sync4j::array2sif( $a, '', '' . ''); } // Synchronize Tasks *from* Horde *to* Outlook. // This is working perfectly. function vtodo2sif($vcard) { $iCal = &new Horde_iCalendar(); if (!$iCal->parsevCalendar($vcard)) { return PEAR::raiseError('There was an error importing the data.'); } $components = $iCal->getComponents(); switch (count($components)) { case 0: return PEAR::raiseError('No data was found'); case 1: $content = $components[0]; break; default: return PEAR::raiseError('Multiple components found; only one is supported.'); } $hash['Complete'] = 'False'; // Outlook's default for no due date $hash['DueDate'] = '45001231T230000Z'; $attr = $content->getAllAttributes(); foreach ($attr as $item) { switch ($item['name']) { case 'DUE': $hash['DueDate'] = Horde_iCalendar::_exportDateTime($item['value']); break; case 'SUMMARY': $hash['Subject'] = $item['value']; break; case 'DESCRIPTION': $hash['Body'] = $item['value']; break; case 'PRIORITY': if ($item['value'] == 1) { $hash['Importance'] = 2; } elseif ($item['value'] == 5) { $hash['Importance'] = 0; } else { $hash['Importance'] = 1; } break; case 'COMPLETED': $hash['Complete'] = $item['value'] ? 'True' : 'False'; break; case 'CATEGORIES': $hash['Categories'] = $item['value']; break; } } return SyncML_Device_sync4j::array2sif( $hash, '', ''); } // Synchronize Calendar *from* Horde *to* Outlook. // This is not working at all. function vevent2sif($vcard) { $iCal = &new Horde_iCalendar(); if (!$iCal->parsevCalendar($vcard)) { die("There was an error importing the data."); } $components = $iCal->getComponents(); switch (count($components)) { case 0: die("No data was found."); case 1: $content = $components[0]; break; default: die("Multiple components found; only one is supported."); } // Is there a real need to provide the correct value? $duration = 0; $attr = $content->getAllAttributes(); // The following line is to allow recurrences. $hash['IsRecurring'] = 0; // The preceding line is to allow recurrences. foreach ($attr as $item) { switch ($item['name']) { case 'DTSTART': if (!empty($item['params']['VALUE']) && $item['params']['VALUE'] == "DATE") { $hash['AllDayEvent'] = "True"; $hash['Start'] = Horde_iCalendar::_exportDateTime($item['value']); $duration = 1440; } else { $hash['AllDayEvent'] = "False"; $hash['Start'] = Horde_iCalendar::_exportDateTime($item['value']); } break; case 'DTEND': if (!empty($item['params']['VALUE']) && $item['params']['VALUE'] == "DATE") { $hash['AllDayEvent'] = "True"; $hash['End'] = Horde_iCalendar::_exportDateTime($item['value']); $duration = 1440; } else { $hash['AllDayEvent'] = "False"; $hash['End'] = Horde_iCalendar::_exportDateTime($item['value']); } break; case 'SUMMARY': $hash['Subject'] = $item['value']; break; case 'DESCRIPTION': $hash['Body'] = $item['value']; break; case 'LOCATION': $hash['Location'] = $item['value']; break; case 'CATEGORIES': $hash['Categories'] = $item['value']; break; // The following code is to allow recurrences. case 'RRULE': $hash['IsRecurring'] = 1; $rule = $item['value']; $types = array('DAILY'=>0,'WEEKLY'=>1,'MONTHLY'=>2,'YEARLY'=>5); preg_match('/FREQ=(\w+)/', $rule, $match); $hash['RecurrenceType'] = $types[$match[1]]; preg_match('/INTERVAL=(\d+)/', $rule, $match); $hash['Interval'] = $match[1]; if (preg_match('/BYDAY=(.*?)(;|$)/', $rule, $match)) { if ($hash['RecurrenceType'] == 2) { $hash['RecurrenceType'] = 3; } if (preg_match('/(\d)(\w+)/', $match[1], $submatch)) { $hash['DayOfWeekMask'] = $this->stringToMask($submatch[2]); $hash['Instance'] = $submatch[1]; } else { $hash['DayOfWeekMask'] = $this->stringToMask($match[1]); } } if (($hash['Interval'] % 12 == 0) && ($hash['RecurrenceType'] == 3)) { $hash['RecurrenceType'] = 6; } preg_match('/(UNTIL=(.+)$)|(COUNT=(\d+)$)/', $rule, $match); if ($match[2] != '') { $hash['NoEndDate'] = 0; // Outlook only honors a PatternEndDate that coincides with the final // date of the recurring event. Unfortunately, this is very labor // intensive to code around, so we just hope for the best. $hash['PatternEndDate'] = $match[2]; } elseif ($match[4] != '') { $hash['NoEndDate'] = 0; $hash['Occurences'] = $match[4]; } else { $hash['NoEndDate'] = 1; } break; // The preceding code is to allow recurrences. } } return SyncML_Device_sync4j::array2sif( $hash, '' . $duration . '', '02'); } // Converts a string of vCard days to a SIF DayOfWeekMask, used above function stringToMask($string) { $values = array('SU' => 1,'MO' => 2,'TU' => 4,'WE' => 8,'TH' => 16,'FR' => 32,'SA' => 64); $array = explode(',',$string); $mask = 0; foreach ($array as $day) { $mask += $values[$day]; } return $mask; } // Synchronize Contacts *from* Horde *to* Outlook. // Does not transfer Business/Work Address. // Does not transfer Freebusy URL. function vcard2sif($vcard) { $iCal = &new Horde_iCalendar(); if (!$iCal->parsevCalendar($vcard)) { die("There was an error importing the data."); } $components = $iCal->getComponents(); switch (count($components)) { case 0: die("No data was found."); case 1: $content = $components[0]; break; default: die("Multiple components found; only one is supported."); } // from here on, the code is taken from // Turba_Driver::toHash, v 1.65 2005/03/12 // and modified for the Sync4J attribute names. $attr = $content->getAllAttributes(); foreach ($attr as $item) { switch ($item['name']) { case 'FN': $hash['FileAs'] = $item['value']; break; case 'N': $name = $item['values']; $hash['LastName'] = $name[VCARD_N_FAMILY]; $hash['FirstName'] = $name[VCARD_N_GIVEN]; $hash['MiddleName'] = $name[VCARD_N_ADDL]; $hash['Title'] = $name[VCARD_N_PREFIX]; $hash['Suffix'] = $name[VCARD_N_SUFFIX]; break; case 'NICKNAME': $hash['NickName'] = $item['value']; break; // For vCard 3.0. case 'ADR': if (isset($item['params']['TYPE'])) { if (!is_array($item['params']['TYPE'])) { $item['params']['TYPE'] = array($item['params']['TYPE']); } } else { $item['params']['TYPE'] = array(); if (isset($item['params']['WORK'])) { $item['params']['TYPE'][] = 'WORK'; } if (isset($item['params']['HOME'])) { $item['params']['TYPE'][] = 'HOME'; } } $address = $item['values']; foreach ($item['params']['TYPE'] as $adr) { switch (String::upper($adr)) { case 'HOME': $prefix = 'HomeAddress'; break; case 'WORK': $prefix = 'WorkAddress'; break; default: $prefix = 'HomeAddress'; } if ($prefix) { $hash[$prefix . 'Street'] = isset($address[VCARD_ADR_STREET]) ? $address[VCARD_ADR_STREET] : null; $hash[$prefix . 'City'] = isset($address[VCARD_ADR_LOCALITY]) ? $address[VCARD_ADR_LOCALITY] : null; $hash[$prefix . 'State'] = isset($address[VCARD_ADR_REGION]) ? $address[VCARD_ADR_REGION] : null; $hash[$prefix . 'PostalCode'] = isset($address[VCARD_ADR_POSTCODE]) ? $address[VCARD_ADR_POSTCODE] : null; $hash[$prefix . 'Country'] = isset($address[VCARD_ADR_COUNTRY]) ? $address[VCARD_ADR_COUNTRY] : null; $hash[$prefix . 'PostOfficeBox'] = isset($address[VCARD_ADR_POB]) ? $address[VCARD_ADR_POB] : null; } } break; case 'TEL': if (isset($item['params']['FAX'])) { $hash['BusinessFaxNumber'] = $item['value']; } elseif (isset($item['params']['TYPE'])) { if (!is_array($item['params']['TYPE'])) { $item['params']['TYPE'] = array($item['params']['TYPE']); } // For vCard 3.0. foreach ($item['params']['TYPE'] as $tel) { if (String::upper($tel) == 'WORK') { $hash['BusinessTelephoneNumber'] = $item['value']; } elseif (String::upper($tel) == 'HOME') { $hash['HomeTelephoneNumber'] = $item['value']; } elseif (String::upper($tel) == 'CELL') { $hash['MobileTelephoneNumber'] = $item['value']; } elseif (String::upper($tel) == 'FAX') { $hash['BusinessFaxNumber'] = $item['value']; } } } else { if (isset($item['params']['HOME'])) { $hash['HomeTelephoneNumber'] = $item['value']; } elseif (isset($item['params']['WORK'])) { $hash['BusinessTelephoneNumber'] = $item['value']; } elseif (isset($item['params']['CELL'])) { $hash['MobileTelephoneNumber'] = $item['value']; } else { $hash['HomeTelephoneNumber'] = $item['value']; } } break; case 'EMAIL': if (isset($item['params']['PREF']) || !isset($hash['email'])) { $hash['Email1Address'] = Horde_iCalendar_vcard::getBareEmail($item['value']); $hash['Email1AddressType'] = 'SMTP'; } break; case 'TITLE': $hash['JobTitle'] = $item['value']; break; case 'ORG': $hash['CompanyName'] = $item['value']; break; case 'NOTE': $hash['Body'] = $item['value']; break; case 'URL': $hash['WebPage'] = $item['value']; break; case 'CATEGORIES': $hash['Categories'] = $item['value']; break; } } return SyncML_Device_sync4j::array2sif( $hash, '', ''); } }