1: <?php
2: /**
3: * Base class for ActiveSync backends. Provides the communication between
4: * the ActiveSync classes and the actual backend data that is being sync'd.
5: *
6: * Also responsible for providing objects to the command objects that can
7: * generate the delta between the PIM and server.
8: *
9: * Based, in part, on code by the Z-Push project. Original copyright notices
10: * appear below.
11: *
12: * Copyright 2010-2012 Horde LLC (http://www.horde.org/)
13: *
14: * @author Michael J. Rubinsky <mrubinsk@horde.org>
15: * @package ActiveSync
16: */
17: /**
18: * File : diffbackend.php
19: * Project : Z-Push
20: * Descr : We do a standard differential
21: * change detection by sorting both
22: * lists of items by their unique id,
23: * and then traversing both arrays
24: * of items at once. Changes can be
25: * detected by comparing items at
26: * the same position in both arrays.
27: *
28: * Created : 01.10.2007
29: *
30: * Zarafa Deutschland GmbH, www.zarafaserver.de
31: * This file is distributed under GPL-2.0.
32: * Consult COPYING file for details
33: */
34: abstract class Horde_ActiveSync_Driver_Base
35: {
36: /**
37: * The username to sync with the backend as
38: *
39: * @var string
40: */
41: protected $_user;
42:
43: /**
44: * Authenticating user
45: *
46: * @var string
47: */
48: protected $_authUser;
49:
50: /**
51: * User password
52: *
53: * @var string
54: */
55: protected $_authPass;
56:
57: /**
58: * Logger instance
59: *
60: * @var Horde_Log_Logger
61: */
62: protected $_logger;
63:
64: /**
65: * Parameters
66: *
67: * @var array
68: */
69: protected $_params;
70:
71: /**
72: * Secuirity Policies. These settings can be overridden by the backend
73: * provider by passing in a 'policies' key in the const'r params array. This
74: * way the server can provide user-specific policies.
75: *
76: * <pre>
77: * Currently supported settings are:
78: * pin - Device must have a pin lock enabled.
79: * computerunlock - Device can be unlocked by a computer.
80: * AEFrequencyValue - Time (in minutes) of inactivity before device locks
81: * DeviceWipeThreshold - Number of failed unlock attempts before the
82: * device should wipe on devices that support this.
83: * CodewordFrequency - Number of failed unlock attempts before needing
84: * to verify that a person who can read and write is
85: * using the PIM.
86: * MinimumPasswordLength
87: * PasswordComplexity - 0 - alphanumeric, 1 - numeric, 2 - anything
88: * </pre>
89: */
90: protected $_policies = array(
91: 'pin' => true,
92: 'extended_policies' => true,
93: 'inactivity' => 5,
94: 'wipethreshold' => 10,
95: 'codewordfrequency' => 0,
96: 'minimumlength' => 5,
97: 'complexity' => 2,
98: );
99:
100: /**
101: * The state object for this request. Needs to be injected into this class.
102: * Different Sync objects may require more then one type of stateObject.
103: * For instance, Horde can sync contacts and caledar data with a history
104: * based state engine, but cannot due the same for email.
105: *
106: * @var Horde_ActiveSync_State_Base
107: */
108: protected $_stateObject;
109:
110: /**
111: * Const'r
112: *
113: * @param array $params Any configuration parameters or injected objects
114: * the concrete driver may need.
115: * <pre>
116: * (optional) logger Horde_Log_Logger instance
117: * (required) state_basic A Horde_ActiveSync_State_Base object that is
118: * capable of handling all collections except
119: * email.
120: * (optional) state_email A Horde_ActiveSync_State_Base object that is
121: * capable of handling email collections.
122: * </pre>
123: *
124: * @return Horde_ActiveSync_Driver
125: */
126: public function __construct($params = array())
127: {
128: $this->_params = $params;
129: if (empty($params['state_basic']) ||
130: !($params['state_basic'] instanceof Horde_ActiveSync_State_Base)) {
131:
132: throw new InvalidArgumentException('Missing required state object');
133: }
134:
135: /* Create a stub if we don't have a useable logger. */
136: if (isset($params['logger'])
137: && is_callable(array($params['logger'], 'log'))) {
138: $this->_logger = $params['logger'];
139: unset($params['logger']);
140: } else {
141: $this->_logger = new Horde_Support_Stub;
142: }
143:
144: $this->_stateObject = $params['state_basic'];
145: $this->_stateObject->setLogger($this->_logger);
146: $this->_stateObject->setBackend($this);
147:
148: /* Override any security policies */
149: if (!empty($params['policies'])) {
150: $this->_policies = array_merge($this->_policies, $params['policies']);
151: }
152: }
153:
154: public function __destruct()
155: {
156: unset($this->_stateObject);
157: }
158:
159: /**
160: * Setter for the logger instance
161: *
162: * @param Horde_Log_Logger $logger The logger
163: *
164: * @void
165: */
166: public function setLogger(Horde_Log_Logger $logger)
167: {
168: $this->_logger = $logger;
169: }
170:
171: /**
172: * Obtain the ping heartbeat settings
173: *
174: * @return array
175: */
176: public function getHeartbeatConfig()
177: {
178: return $this->_params['ping'];
179: }
180:
181: /**
182: * Get folder stat
183: * "id" => The server ID that will be used to identify the folder.
184: * It must be unique, and not too long. How long exactly is not
185: * known, but try keeping it under 20 chars or so.
186: * It must be a string.
187: * "parent" => The server ID of the parent of the folder. Same restrictions
188: * as 'id' apply.
189: * "mod" => This is the modification signature. It is any arbitrary string
190: * which is constant as long as the folder has not changed. In
191: * practice this means that 'mod' can be equal to the folder name
192: * as this is the only thing that ever changes in folders.
193: */
194: abstract public function statFolder($id);
195:
196: /**
197: * Get a folder from the backend
198: *
199: * To be implemented by concrete backend driver.
200: */
201: abstract public function getFolder($id);
202:
203: /**
204: * Get the list of folders from the backend.
205: */
206: abstract public function getFolderList();
207:
208: /**
209: * Get a full list of messages on the server
210: *
211: * @param string $folderId The folder id
212: * @param timestamp $cutOffDate The timestamp of the earliest date for
213: * calendar or mail entries
214: *
215: * @return array A list of messages
216: */
217: abstract public function getMessageList($folderId, $cutOffDate);
218:
219: /**
220: * Get a list of server changes that occured during the specified time
221: * period.
222: *
223: * @param string $folderId The server id of the collection to check.
224: * @param timestamp $from_ts The starting timestamp
225: * @param timestamp $to_ts The ending timestamp
226: *
227: * @return array A list of messge uids that have chnaged in the specified
228: * time period.
229: */
230: abstract public function getServerChanges($folderId, $from_ts, $to_ts, $cutoffdate);
231:
232: /**
233: * Get a message stat.
234: *
235: * @param string $folderId The folder id
236: * @param string $id The message id (??)
237: *
238: * @return hash with 'id', 'mod', and 'flags' members
239: */
240: abstract public function statMessage($folderId, $id);
241:
242: /**
243: * Obtain an ActiveSync message from the backend.
244: *
245: * @param string $folderid The server's folder id this message is from
246: * @param string $id The server's message id
247: * @param integer $truncsize A TRUNCATION_* constant
248: * @param integer $mimesupport Mime support for this message
249: *
250: * @return Horde_ActiveSync_Message_Base The message data
251: */
252: abstract public function getMessage($folderid, $id, $truncsize, $mimesupport = 0);
253:
254: /**
255: * Delete a message
256: *
257: * @param string $folderId Folder id
258: * @param string $id Message id
259: *
260: * @return boolean
261: */
262: abstract public function deleteMessage($folderid, $id);
263:
264: /**
265: * Add/Edit a message
266: *
267: * @param string $folderid The server id for the folder the message belongs
268: * to.
269: * @param string $id The server's uid for the message if this is a
270: * change to an existing message.
271: * @param Horde_ActiveSync_Message_Base $message The activesync message
272: * @param stdClass $device The device information
273: */
274: abstract public function changeMessage($folderid, $id, $message, $device);
275:
276: /**
277: * Any code needed to authenticate to backend as the actual user.
278: *
279: * @param string $username The username to authenticate as
280: * @param string $password The password
281: * @param string $domain The user domain
282: *
283: * @return boolean
284: */
285: public function logon($username, $password, $domain = null)
286: {
287: $this->_authUser = $username;
288: $this->_authPass = $password;
289:
290: return true;
291: }
292:
293: /**
294: * Get the username for this request.
295: *
296: * @return string The current username
297: */
298: public function getUser()
299: {
300: return $this->_authUser;
301: }
302:
303: /**
304: * Any code to run on log off
305: *
306: * @return boolean
307: */
308: public function logOff()
309: {
310: return true;
311: }
312:
313: /**
314: * Setup sync parameters. The user provided here is the user the backend
315: * will sync with. This allows you to authenticate as one user, and sync as
316: * another, if the backend supports this.
317: *
318: * @param string $user The username to sync as on the backend.
319: *
320: * @return boolean
321: */
322: public function setup($user)
323: {
324: $this->_user = $user;
325:
326: return true;
327: }
328:
329: /**
330: * Return the helper for importing hierarchy changes from the PIM.
331: *
332: * @return Horde_ActiveSync_DiffState_ImportHierarchy
333: */
334: public function getHierarchyImporter()
335: {
336: $importer = new Horde_ActiveSync_DiffState_ImportHierarchy($this);
337: $importer->setLogger($this->_logger);
338:
339: return $importer;
340: }
341:
342: /**
343: * Return the helper for importing message changes from the PIM.
344: *
345: * @param string $folderid
346: *
347: * @return Horde_ActiveSync_DiffState_ImportContents
348: */
349: public function getContentsImporter($folderId)
350: {
351: $importer = new Horde_ActiveSync_DiffState_ImportContents($this, $folderId);
352: $importer->setLogger($this->_logger);
353:
354: return $importer;
355: }
356:
357: /**
358: * @TODO: This will replace the above two methods. (They are still called
359: * from the (unused/unsupported MoveItems and CreateFolder Requests).
360: *
361: * @return Horde_ActiveSync_Connector_Importer
362: */
363: public function getImporter()
364: {
365: $importer = new Horde_ActiveSync_Connector_Importer($this);
366: return $importer;
367: }
368:
369: /**
370: * Return helper for performing the actual sync operation.
371: *
372: * @param string $folderId
373: *
374: * @return Horde_ActiveSync_Sync
375: */
376: public function getSyncObject()
377: {
378: $exporter = new Horde_ActiveSync_Sync($this);
379: $exporter->setLogger($this->_logger);
380:
381: return $exporter;
382: }
383:
384: /**
385: * Will (eventually) return an appropriate state object based on the class
386: * being sync'd.
387: *
388: * @param array $collection
389: */
390: public function &getStateObject($collection = array())
391: {
392: $this->_stateObject->init($collection);
393: $this->_stateObject->setLogger($this->_logger);
394: return $this->_stateObject;
395: }
396:
397: /**
398: * Get the full folder hierarchy from the backend.
399: *
400: * @return array
401: */
402: public function getHierarchy()
403: {
404: $folders = array();
405:
406: $fl = $this->getFolderList();
407: foreach ($fl as $f) {
408: $folders[] = $this->getFolder($f['id']);
409: }
410:
411: return $folders;
412: }
413:
414: /**
415: * Obtain a message from the backend.
416: *
417: * @param string $folderid
418: * @param string $id
419: * @param ?? $mimesupport (Not sure what this was supposed to do)
420: *
421: * @return Horde_ActiveSync_Message_Base The message data
422: */
423: public function fetch($folderid, $id, $mimesupport = 0)
424: {
425: // Forces entire message (up to 1Mb)
426: return $this->getMessage($folderid, $id, 1024 * 1024, $mimesupport);
427: }
428:
429: /**
430: *
431: * @param $attname
432: * @return unknown_type
433: */
434: public function getAttachmentData($attname)
435: {
436: return false;
437: }
438:
439: /**
440: * Sends the email represented by the rfc822 string received by the PIM.
441: *
442: * @param string $rfc822 The rfc822 mime message
443: * @param boolean $forward Is this a message forward?
444: * @param boolean $reply Is this a reply?
445: * @param boolean $parent Parent message in thread.
446: *
447: * @return boolean
448: */
449: abstract function sendMail($rfc822, $forward = false, $reply = false, $parent = false);
450:
451: /**
452: * @return unknown_type
453: */
454: public function getWasteBasket()
455: {
456: return false;
457: }
458:
459: /**
460: * Delete a folder on the server.
461: *
462: * @param string $parent The parent folder.
463: * @param string $id The folder to delete.
464: *
465: * @return boolean
466: * @throws Horde_ActiveSync_Exception
467: */
468: public function deleteFolder($parent, $id)
469: {
470: throw new Horde_ActiveSync_Exception('DeleteFolder not yet implemented');
471: }
472:
473: /**
474: *
475: * @param $folderid
476: * @param $id
477: * @param $flags
478: * @return unknown_type
479: */
480: public function setReadFlag($folderid, $id, $flags)
481: {
482: return false;
483: }
484:
485: /**
486: * Change the name and/or type of a folder.
487: *
488: * @param string $parent
489: * @param string $id
490: * @param string $displayname
491: * @param string $type
492: *
493: * @return boolean
494: */
495: public function changeFolder($parent, $id, $displayname, $type)
496: {
497: throw new Horde_ActiveSync_Exception('changeFolder not yet implemented.');
498: }
499:
500: /**
501: * @todo
502: *
503: * @param $folderid
504: * @param $id
505: * @param $newfolderid
506: * @return unknown_type
507: */
508: public function moveMessage($folderid, $id, $newfolderid)
509: {
510: throw new Horde_ActiveSync_Exception('moveMessage not yet implemented.');
511: }
512:
513: /**
514: * @todo
515: *
516: * @param $requestid
517: * @param $folderid
518: * @param $error
519: * @param $calendarid
520: * @return unknown_type
521: */
522: public function meetingResponse($requestid, $folderid, $error, &$calendarid)
523: {
524: throw new Horde_ActiveSync_Exception('meetingResponse not yet implemented.');
525: }
526:
527: /**
528: * Returns array of items which contain contact information
529: *
530: * @param string $query
531: * @param string $range
532: *
533: * @return array
534: */
535: public function getSearchResults($query, $range)
536: {
537: throw new Horde_ActiveSync_Exception('getSearchResults not implemented.');
538: }
539:
540: /**
541: * Specifies if this driver has an alternate way of checking for changes
542: * when PING is used.
543: *
544: * @return boolean
545: */
546: public function alterPing()
547: {
548: return false;
549: }
550:
551: /**
552: * If this driver can check for changes in an alternate way for PING then
553: * for SYNC, this method is used to do so. Also, alterPing() should return
554: * true in this case.
555: *
556: * @param string $folderid The folder id
557: * @param array $syncstate The syncstate
558: *
559: * @deprecated - This will probably be removed.
560: * @return array An array of changes, the same as retunred from getChanges
561: */
562: public function alterPingChanges($folderid, &$syncstate)
563: {
564: return array();
565: }
566:
567: /**
568: * Build a <wap-provisioningdoc> for the given security settings provided
569: * by the backend.
570: *
571: * 4131 (Enforce password on device) 0: enabled 1: disabled
572: * AEFrequencyType 0: no inactivity time 1: inactivity time is set
573: * AEFrequencyValue inactivity time in minutes
574: * DeviceWipeThreshold after how many wrong password to device should get wiped
575: * CodewordFrequency validate every x wrong passwords, that a person is using the device which is able to read and write. should be half of DeviceWipeThreshold
576: * MinimumPasswordLength minimum password length
577: * PasswordComplexity 0: Require alphanumeric 1: Require only numeric, 2: anything goes
578: *
579: * @param string The type of policy to return.
580: *
581: * @return string
582: */
583: public function getCurrentPolicy($policyType = 'MS-WAP-Provisioning-XML')
584: {
585: $xml = '<wap-provisioningdoc><characteristic type="SecurityPolicy">'
586: . '<parm name="4131" value="' . ($this->_policies['pin'] ? 0 : 1) . '"/>'
587: . '</characteristic>';
588:
589: if ($this->_policies['pin'] && $this->_policies['extended_policies']) {
590: $xml .= '<characteristic type="Registry">'
591: . '<characteristic type="HKLM\Comm\Security\Policy\LASSD\AE\{50C13377-C66D-400C-889E-C316FC4AB374}">'
592: . '<parm name="AEFrequencyType" value="' . (!empty($this->_policies['inactivity']) ? 1 : 0) . '"/>'
593: . (!empty($this->_policies['inactivity']) ? '<parm name="AEFrequencyValue" value="' . $this->_policies['inactivity'] . '"/>' : '')
594: . '</characteristic>';
595:
596: if (!empty($this->_policies['wipethreshold'])) {
597: $xml .= '<characteristic type="HKLM\Comm\Security\Policy\LASSD"><parm name="DeviceWipeThreshold" value="' . $this->_policies['wipethreshold'] . '"/></characteristic>';
598: }
599: if (!empty($this->_policies['codewordfrequency'])) {
600: $xml .= '<characteristic type="HKLM\Comm\Security\Policy\LASSD"><parm name="CodewordFrequency" value="' . $this->_policies['codewordfrequency'] . '"/></characteristic>';
601: }
602: if (!empty($this->_policies['minimumlength'])) {
603: $xml .= '<characteristic type="HKLM\Comm\Security\Policy\LASSD\LAP\lap_pw"><parm name="MinimumPasswordLength" value="' . $this->_policies['minimumlength'] . '"/></characteristic>';
604: }
605: if ($this->_policies['complexity'] !== false) {
606: $xml .= '<characteristic type="HKLM\Comm\Security\Policy\LASSD\LAP\lap_pw"><parm name="PasswordComplexity" value="' . $this->_policies['complexity'] . '"/></characteristic>';
607: }
608: $xml .= '</characteristic>';
609: }
610:
611: $xml .= '</wap-provisioningdoc>';
612:
613: return $xml;
614: }
615:
616: /**
617: * Truncate an UTF-8 encoded sting correctly
618: *
619: * If it's not possible to truncate properly, an empty string is returned
620: *
621: * @param string $string The string to truncate
622: * @param string $length The length of the returned string
623: *
624: * @return string The truncated string
625: */
626: static public function truncate($string, $length)
627: {
628: if (strlen($string) <= $length) {
629: return $string;
630: }
631: while($length >= 0) {
632: if ((ord($string[$length]) < 0x80) || (ord($string[$length]) >= 0xC0)) {
633: return substr($string, 0, $length);
634: }
635: $length--;
636: }
637:
638: return "";
639: }
640:
641: }
642: