1: <?php
2: /**
3: * The Horde_SyncMl_Device:: class provides functionality that is potentially
4: * (client) device dependant.
5: *
6: * If a sync client needs any kind of special conversion of the data sent to it
7: * or received from it, this is done here. There are two sources of information
8: * to identify an device: The first (and better) one is the DevInf device info
9: * sent by the device during a request. If DevInf is not supported or sent by
10: * the client, the Source/LocURI of the device request might be sufficent to
11: * identify it.
12: *
13: * Copyright 2005-2012 Horde LLC (http://www.horde.org/)
14: *
15: * See the enclosed file COPYING for license information (LGPL). If you
16: * did not receive this file, see http://www.horde.org/licenses/lgpl21.
17: *
18: * @author Karsten Fourmont <karsten@horde.org>
19: * @package SyncMl
20: */
21: class Horde_SyncMl_Device
22: {
23: /**
24: * The original preferred content type of the client, if provided through
25: * DevInf.
26: *
27: * @var string
28: */
29: public $requestedContentType;
30:
31: /**
32: * Attempts to return a concrete Horde_SyncMl_Device instance based on $driver.
33: *
34: * @param string $driver The type of concrete Horde_SyncMl_Device subclass to
35: * return.
36: *
37: * @return Horde_SyncMl_Device The newly created concrete Horde_SyncMl_Device
38: * instance, or false on error.
39: */
40: public function factory($driver)
41: {
42: $driver = basename($driver);
43:
44: if (empty($driver) || $driver == 'none' || $driver == 'default') {
45: $GLOBALS['backend']->logMessage(
46: 'Using default device class', 'DEBUG');
47: return new Horde_SyncMl_Device();
48: }
49:
50: $class = 'Horde_SyncMl_Device_' . $driver;
51: if (!class_exists($class)) {
52: return false;
53: }
54:
55: $device = new $class();
56: $GLOBALS['backend']->logMessage('Created device class ' . $class, 'DEBUG');
57: return $device;
58: }
59:
60: /**
61: * Returns the guessed content type for a database URI.
62: *
63: * When a client sends data during a sync but does not provide information
64: * about the MIME content type with this individual item, this function
65: * returns the content type the item is supposed to be in.
66: *
67: * @param string $database A database URI.
68: *
69: * @return string A MIME type that might match the database URI.
70: */
71: public function getPreferredContentType($database)
72: {
73: $database = $GLOBALS['backend']->normalize($database);
74:
75: /* Use some wild guessings. */
76: if (strpos($database, 'contact') !== false ||
77: strpos($database, 'card') !== false) {
78: return 'text/x-vcard';
79: } elseif (strpos($database, 'note') !== false ||
80: strpos($database, 'memo') !== false) {
81: return 'text/plain';
82: } elseif (strpos($database, 'task') !== false ||
83: strpos($database, 'cal') !== false ||
84: strpos($database, 'event') !== false) {
85: return 'text/calendar';
86: }
87: }
88:
89: /**
90: * Returns the preferrred MIME content type of the client for the given
91: * sync data type (contacts/tasks/notes/calendar).
92: *
93: * The result is passed as an option to the backend export functions.
94: * This is not the content type ultimately passed to the client but rather
95: * the content type presented to the backend export functions.
96: *
97: * After the data is retrieved from the backend, convertServer2Client()
98: * can do some post-processing and set the correct content type acceptable
99: * for the client if necessary.
100: *
101: * The default implementation tries to extract the content type from the
102: * device info. If this does not work, some defaults are used.
103: *
104: * If the client does not provice proper DevInf data, this public function may
105: * have to be overwritten to return the correct values.
106: *
107: * @param string $serverSyncURI The URI for the server database: contacts,
108: * notes, calendar or tasks.
109: * @param string $sourceSyncURI The URI for the client database. This is
110: * needed as the DevInf is grouped by
111: * sourceSyncURIs.
112: */
113: public function getPreferredContentTypeClient($serverSyncURI, $sourceSyncURI)
114: {
115: $di = $GLOBALS['backend']->state->deviceInfo;
116: $ds = $di->getDataStore($sourceSyncURI);
117: if (!empty($ds)) {
118: $r = $ds->getPreferredRXContentType();
119: if (!empty($r)) {
120: $this->requestedContentType = $r;
121: return $r;
122: }
123: }
124:
125: $database = $GLOBALS['backend']->normalize($serverSyncURI);
126:
127: /* No information in DevInf, use some wild guessings. */
128: if (strpos($database, 'contact') !== false ||
129: strpos($database, 'card') !== false) {
130: return 'text/x-vcard';
131: } elseif (strpos($database, 'note') !== false ||
132: strpos($database, 'memo') !== false) {
133: // SyncML conformance suite expects this rather than text/x-vnote
134: return 'text/plain';
135: } elseif (strpos($database, 'task') !== false ||
136: strpos($database, 'cal') !== false ||
137: strpos($database, 'event') !== false) {
138: return 'text/calendar';
139: }
140: }
141:
142: /**
143: * Converts the content received from the client for the backend.
144: *
145: * Currently strips UID (primary key) information as client and server
146: * might use different ones.
147: *
148: * Charset conversions might be added here too.
149: *
150: * @todo remove UID stripping or move it anywhere else.
151: *
152: * @param string $content The content to convert.
153: * @param string $contentType The content type of the content.
154: *
155: * @return array Two-element array with the converted content and the
156: * (possibly changed) new content type.
157: */
158: public function convertClient2Server($content, $contentType)
159: {
160: $GLOBALS['backend']->logFile(
161: Horde_SyncMl_Backend::LOGFILE_DATA,
162: "\nInput received from client ($contentType):\n$content\n");
163:
164: // Always remove client UID. UID will be seperately passed in XML.
165: $content = preg_replace('/(\r\n|\r|\n)UID:.*?(\r\n|\r|\n)/',
166: '\1', $content, 1);
167:
168: return array($content, $contentType);
169: }
170:
171: /**
172: * Converts the content from the backend to a format suitable for the
173: * client device.
174: *
175: * Strips the UID (primary key) information as client and server might use
176: * different ones.
177: *
178: * Charset conversions might be added here too.
179: *
180: * @param string $content The content to convert
181: * @param string $contentType The content type of content as returned
182: * from the backend
183: * @param string $database The server database URI.
184: *
185: * @return array Three-element array with the converted content, the
186: * (possibly changed) new content type, and encoding type
187: * (like b64 as used by Funambol).
188: */
189: public function convertServer2Client($content, $contentType, $database)
190: {
191: if (is_array($contentType)) {
192: $contentType = $contentType['ContentType'];
193: }
194:
195: $GLOBALS['backend']->logFile(
196: Horde_SyncMl_Backend::LOGFILE_DATA,
197: "\nOutput received from backend ($contentType):\n" . $content
198: . "\n");
199:
200: /* Always remove server UID. UID will be seperately passed in XML. */
201: $content = preg_replace('/(\r\n|\r|\n)UID:.*?(\r\n|\r|\n)/',
202: '\1', $content, 1);
203:
204: if ($this->useLocalTime()) {
205: $content = preg_replace_callback(
206: '/\d{8}T\d{6}Z/',
207: array($this, '_convertUTC2LocalTime'),
208: $content);
209: }
210:
211: return array($content, $contentType, null);
212: }
213:
214: /**
215: * Returns whether the device handles tasks and events in a single
216: * "calendar" sync.
217: *
218: * This requires special actions on our side as we store this in different
219: * backend databases.
220: *
221: * @return boolean True if tasks and events are processed in a single
222: * request.
223: */
224: public function handleTasksInCalendar()
225: {
226: return false;
227: }
228:
229: /**
230: * Returns whether to send individual status response for each Add, Delete
231: * and Replace.
232: *
233: * @return boolean False if individual status responses should be send.
234: */
235: public function omitIndividualSyncStatus()
236: {
237: return false;
238: }
239:
240: /**
241: * Returns whether the payload data should be enclosed in a [CDATA[
242: * section when sending via XML.
243: *
244: * The synchronized data may contain XML special characters like &,
245: * < or >. Clients might choke when sending these embedded in XML.
246: * The data should be enclosed in [CDATA[ in these cases. This applies
247: * only to XML, not to WBXML devices.
248: *
249: * @return boolean True if the data should be enclosed in [CDATA[.
250: */
251: public function useCdataTag()
252: {
253: return true;
254: }
255:
256: /**
257: * Returns whether the device accepts datetimes only in local time format
258: * (DTSTART:20061222T130000) instead of the more robust UTC time
259: * (DTSTART:20061222T110000Z).
260: *
261: * @return boolean True if the client doesn't accept UTC datetimes.
262: */
263: public function useLocalTime()
264: {
265: return false;
266: }
267:
268: /**
269: * Converts an UTC timestamp like "20061222T110000Z" into a local
270: * timestamp like "20061222T130000" using the server timezone.
271: *
272: * @param array $utc Array with a datetime string in UTC.
273: *
274: * @return string The datetime string converted to the local timezone.
275: */
276: protected function _convertUTC2LocalTime($utc)
277: {
278: $date = new Horde_Date($utc[0]);
279: $date->setTimezone(date_default_timezone_get());
280: return $date->format("Ymd\THis");
281: }
282: }
283: