1<?php
2/**
3 * EGroupware - eSync - ActiveSync protocol based on Z-Push: backend for EGroupware
4 *
5 * @link http://www.egroupware.org
6 * @package esync
7 * @author Ralf Becker <rb@egroupware.org>
8 * @author EGroupware GmbH <info@egroupware.org>
9 * @author Philip Herbert <philip@knauber.de>
10 * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
11 */
12
13use EGroupware\Api;
14
15/**
16 * Z-Push backend for EGroupware
17 *
18 * Uses EGroupware application specific plugins, eg. mail_zpush class
19 *
20 * @todo store states in DB
21 * @todo change AlterPingChanges method to GetFolderState in plugins, interfaces and egw backend directly returning state
22 */
23class activesync_backend extends BackendDiff implements ISearchProvider
24{
25	var $egw_sessionID;
26
27	var $hierarchyimporter;
28	var $contentsimporter;
29	var $exporter;
30
31	var $authenticated = false;
32
33	/**
34	 * Integer waitOnFailureDefault how long (in seconds) to wait on connection failure
35	 *
36	 * @var int
37	 */
38	static $waitOnFailureDefault = 30;
39
40	/**
41	 * Integer waitOnFailureLimit how long (in seconds) to wait on connection failure until a 500 is raised
42	 *
43	 * @var int
44	 */
45	static $waitOnFailureLimit = 7200;
46
47	/**
48	 * Constructor
49	 *
50	 * We create/verify EGroupware session here, as we need our plugins before Logon get called
51	 */
52	function __construct()
53	{
54		parent::__construct();
55
56		// AS preferences needs to instanciate this class too, but has no running AS request
57		// regular AS still runs as "login", when it instanciates our backend
58		if ($GLOBALS['egw_info']['flags']['currentapp'] == 'login')
59		{
60			// need to call ProcessHeaders() here, to be able to use ::GetAuth(User|Password), unfortunately zpush calls it too so it runs twice
61			Request::ProcessHeaders();
62			$username = Request::GetAuthUser();
63			$password = Request::GetAuthPassword();
64
65			// check credentials and create session
66			$GLOBALS['egw_info']['flags']['currentapp'] = 'activesync';
67			$this->authenticated = (($this->egw_sessionID = Api\Session::get_sessionid(true)) &&
68				$GLOBALS['egw']->session->verify($this->egw_sessionID) &&
69				base64_decode(Api\Cache::getSession('phpgwapi', 'password')) === $password ||	// check if session contains password
70				($this->egw_sessionID = $GLOBALS['egw']->session->create($username,$password,'text',true)));	// true = no real session
71
72			// closing session right away to not block parallel requests,
73			// this is a must for Ping requests, others might give better performance
74			if ($this->authenticated) // && Request::GetCommand() == 'Ping')
75			{
76				$GLOBALS['egw']->session->commit_session();
77			}
78
79			// enable logging for all devices of a user or a specific device
80			$dev_id = Request::GetDeviceID();
81			//error_log(__METHOD__."() username=$username, dev_id=$dev_id, prefs[activesync]=".array2string($GLOBALS['egw_info']['user']['preferences']['activesync']));
82			if ($GLOBALS['egw_info']['user']['preferences']['activesync']['logging'] == 'user' ||
83				$GLOBALS['egw_info']['user']['preferences']['activesync']['logging'] == $dev_id.'.log')
84			{
85				ZLog::SpecialLogUser();
86			}
87
88			// check if we support loose provisioning for that device
89			$response = $this->run_on_all_plugins('LooseProvisioning', array(), $dev_id);
90			$loose_provisioning = $response ? (boolean)$response['response'] : false;
91			define('LOOSE_PROVISIONING', $loose_provisioning);
92
93			ZLog::Write(LOGLEVEL_DEBUG, __METHOD__."() username=$username, loose_provisioning=".array2string($loose_provisioning).", autheticated=".array2string($this->authenticated));
94		}
95	}
96
97	/**
98	 * Log into EGroupware
99	 *
100	 * @param string $username
101	 * @param string $domain
102	 * @param string $password
103	 * @return boolean TRUE if the logon succeeded, FALSE if not
104     * @throws FatalException   e.g. some required libraries are unavailable
105	 */
106	public function Logon($username, $domain, $password)
107	{
108		if (!$this->authenticated)
109		{
110			ZLog::Write(LOGLEVEL_DEBUG, __METHOD__."() z-push authentication failed: ".$GLOBALS['egw']->session->cd_reason);
111			return false;
112		}
113		if (!isset($GLOBALS['egw_info']['user']['apps']['activesync']))
114		{
115			ZLog::Write(LOGLEVEL_DEBUG, __METHOD__."() z-push authentication failed: NO run rights for E-Push application!");
116			return false;
117		}
118   		ZLog::Write(LOGLEVEL_DEBUG, __METHOD__."('$username','$domain',...) logon SUCCESS");
119
120   		// call plugins in case they are interested in being call on each command
121   		$this->run_on_all_plugins(__FUNCTION__, array(), $username, $domain, $password);
122
123		return true;
124	}
125
126	/**
127	 * Called before closing connection
128	 */
129	public function Logoff()
130	{
131		$this->_loggedin = FALSE;
132
133		ZLog::Write(LOGLEVEL_DEBUG, "LOGOFF");
134	}
135
136	/**
137	 *  This function is analogous to GetMessageList.
138	 */
139	function GetFolderList()
140	{
141		//error_log(__METHOD__."()");
142		$folderlist = $this->run_on_all_plugins(__FUNCTION__);
143		//error_log(__METHOD__."() run_On_all_plugins() returned ".array2string($folderlist));
144		$applist = array('addressbook','calendar','mail');
145		foreach($applist as $app)
146		{
147			if (!isset($GLOBALS['egw_info']['user']['apps'][$app]))
148			{
149				$folderlist[] = $folder = array(
150					'id'	=>	$this->createID($app,
151						$app == 'mail' ? 0 :	// fmail uses id=0 for INBOX, other apps account_id of user
152							$GLOBALS['egw_info']['user']['account_id']),
153					'mod'	=>	'not-enabled',
154					'parent'=>	'0',
155				);
156				ZLog::Write(LOGLEVEL_DEBUG, __METHOD__."() adding for disabled $app ".array2string($folder));
157			}
158		}
159		//ZLog::Write(LOGLEVEL_DEBUG, __METHOD__."() returning ".array2string($folderlist));
160
161		return $folderlist;
162	}
163
164	/**
165	 * Get Information about a folder
166	 *
167	 * @param string $id
168	 * @return SyncFolder|boolean false on error
169	 */
170	function GetFolder($id)
171	{
172		if (!($ret = $this->run_on_plugin_by_id(__FUNCTION__, $id)))
173		{
174			$type = $folder = $app = null;
175			$this->splitID($id, $type, $folder, $app);
176
177			if (!isset($GLOBALS['egw_info']['user']['apps'][$app]))
178			{
179				$ret = new SyncFolder();
180				$ret->serverid = $id;
181				$ret->parentid = '0';
182				$ret->displayname = 'not-enabled';
183				$account_id = $GLOBALS['egw_info']['user']['account_id'];
184				switch($app)
185				{
186					case 'addressbook':
187						$ret->type = $folder == $account_id ? SYNC_FOLDER_TYPE_CONTACT : SYNC_FOLDER_TYPE_USER_CONTACT;
188						break;
189					case 'calendar':
190						$ret->type = $folder == $account_id ? SYNC_FOLDER_TYPE_APPOINTMENT : SYNC_FOLDER_TYPE_USER_APPOINTMENT;
191						break;
192					case 'infolog':
193						$ret->type = $folder == $account_id ? SYNC_FOLDER_TYPE_TASK : SYNC_FOLDER_TYPE_USER_TASK;
194						break;
195					default:
196						$ret->type = $folder == 0 ? SYNC_FOLDER_TYPE_INBOX : SYNC_FOLDER_TYPE_USER_MAIL;
197						break;
198				}
199				ZLog::Write(LOGLEVEL_DEBUG, __METHOD__."($id) return ".array2string($ret)." for disabled app!");
200			}
201		}
202		//ZLog::Write(LOGLEVEL_DEBUG, __METHOD__."('$id') returning ".array2string($ret));
203		return $ret;
204	}
205
206	/**
207	 * Return folder stats. This means you must return an associative array with the
208	 * following properties:
209	 *
210	 * "id" => The server ID that will be used to identify the folder. It must be unique, and not too long
211	 *		 How long exactly is not known, but try keeping it under 20 chars or so. It must be a string.
212	 * "parent" => The server ID of the parent of the folder. Same restrictions as 'id' apply.
213	 * "mod" => This is the modification signature. It is any arbitrary string which is constant as long as
214	 *		  the folder has not changed. In practice this means that 'mod' can be equal to the folder name
215	 *		  as this is the only thing that ever changes in folders. (the type is normally constant)
216	 *
217	 * @return array with values for keys 'id', 'mod' and 'parent'
218	 */
219	function StatFolder($id)
220	{
221		if (!($ret = $this->run_on_plugin_by_id(__FUNCTION__, $id)))
222		{
223			$type = $folder = $app = null;
224			$this->splitID($id, $type, $folder, $app);
225
226			if (!isset($GLOBALS['egw_info']['user']['apps'][$app]))
227			{
228				$ret = array(
229					'id' => $id,
230					'mod'	=>	'not-enabled',
231					'parent'=>	'0',
232				);
233				ZLog::Write(LOGLEVEL_DEBUG, __METHOD__."($id) return ".array2string($ret)." for disabled app!");
234			}
235		}
236		//ZLog::Write(LOGLEVEL_DEBUG, __METHOD__."('$id') returning ".array2string($ret));
237		return $ret;
238	}
239
240
241	/**
242	 * Creates or modifies a folder
243	 *
244	 * Attention: eGroupware currently does not support creating folders. The first device seen during testing
245	 * is now iOS 5 (beta). At least returning false causes the sync not to break.
246	 * As we currently do not support this function currently nothing is forwarded to the plugin.
247	 *
248	 * @param $id of the parent folder
249	 * @param $oldid => if empty -> new folder created, else folder is to be renamed
250	 * @param $displayname => new folder name (to be created, or to be renamed to)
251	 * @param type => folder type, ignored in IMAP
252	 *
253	 * @return stat | boolean false on error
254	 *
255	 */
256	function ChangeFolder($id, $oldid, $displayname, $type)
257	{
258		ZLog::Write(LOGLEVEL_DEBUG, __METHOD__."(ParentId=$id, oldid=$oldid, displaname=$displayname, type=$type)");
259		$ret = $this->run_on_plugin_by_id(__FUNCTION__, $id,  $oldid, $displayname, $type);
260		if (!$ret) ZLog::Write(LOGLEVEL_ERROR, __METHOD__." WARNING : something failed changing folders, now informing the device that this has failed");
261		return $ret;
262	}
263
264
265	/**
266	 * Should return a list (array) of messages, each entry being an associative array
267	 * with the same entries as StatMessage(). This function should return stable information; ie
268	 * if nothing has changed, the items in the array must be exactly the same. The order of
269	 * the items within the array is not important though.
270	 *
271	 * The cutoffdate is a date in the past, representing the date since which items should be shown.
272	 * This cutoffdate is determined by the user's setting of getting 'Last 3 days' of e-mail, etc. If
273	 * you ignore the cutoffdate, the user will not be able to select their own cutoffdate, but all
274	 * will work OK apart from that.
275	 *
276	 * @param string $id folder id
277	 * @param int $cutoffdate =null
278	 * @return array
279  	 */
280	function GetMessageList($id, $cutoffdate=NULL)
281	{
282		$type = $folder = $app = null;
283		$this->splitID($id, $type, $folder, $app);
284		//ZLog::Write(LOGLEVEL_DEBUG, __METHOD__."($id, $cutoffdate) type=$type, folder=$folder, app=$app");
285		if (!($ret = $this->run_on_plugin_by_id(__FUNCTION__, $id, $cutoffdate)))
286		{
287			if (!isset($GLOBALS['egw_info']['user']['apps'][$app]))
288			{
289				ZLog::Write(LOGLEVEL_ERROR, __METHOD__."($id, $cutoffdate) return array() for disabled app!");
290				$ret = array();
291			}
292		}
293		// allow other apps to insert meeting requests
294		/*if ($app == 'mail' && $folder == 0)
295		{
296			$before = count($ret);
297			$not_uids = array();
298			foreach($ret as $message)
299			{
300				if (isset($message->meetingrequest) && is_a($message->meetingrequest, 'SyncMeetingRequest'))
301				{
302					$not_uids[] = self::globalObjId2uid($message->meetingrequest->globalobjid);
303				}
304			}
305			$ret2 = $this->run_on_all_plugins('GetMeetingRequests', $ret, $not_uids, $cutoffdate);
306			if (is_array($ret2) && !empty($ret2))
307			{
308				ZLog::Write(LOGLEVEL_DEBUG, __METHOD__."($id, $cutoffdate) call to GetMeetingRequests added ".(count($ret2)-$before)." messages");
309				ZLog::Write(LOGLEVEL_DEBUG, array2string($ret2));
310				$ret = $ret2; // should be already merged by run_on_all_plugins
311			}
312		}*/
313		ZLog::Write(LOGLEVEL_DEBUG, __METHOD__.'->retrieved '.count($ret)." Messages for type=$type, folder=$folder, app=$app ($id, $cutoffdate)");//.array2string($ret));
314		return $ret;
315	}
316
317	/**
318	 * convert UID to GlobalObjID for meeting requests
319	 *
320	 * @param string $uid iCal UID
321	 * @return binary GlobalObjId
322	 */
323	public static function uid2globalObjId($uid)
324	{
325		$objid = base64_encode(
326			/* Bytes 1-16: */	"\x04\x00\x00\x00\x82\x00\xE0\x00\x74\xC5\xB7\x10\x1A\x82\xE0\x08".
327			/* Bytes 17-20: */	"\x00\x00\x00\x00".
328			/* Bytes 21-36: */	"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00".
329			/* Bytes 37-­40: */	pack('V',13+bytes($uid)).	// binary length + 13 for next line and terminating \x00
330			/* Bytes 41-­52: */	'vCal-Uid'."\x01\x00\x00\x00".
331			$uid."\x00");
332		ZLog::Write(LOGLEVEL_DEBUG, __METHOD__."('$uid') returning '$objid'");
333		return $objid;
334	}
335
336	/**
337	 * Extract UID from GlobalObjId
338	 *
339	 * @param string $objid
340	 * @return string
341	 */
342	public static function globalObjId2uid($objid)
343	{
344		$uid = cut_bytes(base64_decode($objid), 52, -1);	// -1 to cut off terminating \0x00
345		ZLog::Write(LOGLEVEL_DEBUG, __METHOD__."('$objid') returning '$uid'");
346		return $uid;
347	}
348
349	/**
350	 * Get specified item from specified folder.
351	 *
352	 * @param string $folderid
353	 * @param string $id
354     * @param ContentParameters $contentparameters  parameters of the requested message (truncation, mimesupport etc)
355	 * @return $messageobject|boolean false on error
356	 */
357	function GetMessage($folderid, $id, $contentparameters)
358	{
359		ZLog::Write(LOGLEVEL_DEBUG, __METHOD__."($folderid, $id)");
360		/*if ($id < 0)
361		{
362			$type = $folder = $app = null;
363			$this->splitID($folderid, $type, $folder, $app);
364			if ($app == 'mail' && $folder == 0)
365			{
366
367				return $this->run_on_all_plugins('GetMeetingRequest', 'return-first', $id, $contentparameters);
368			}
369		}*/
370		return $this->run_on_plugin_by_id(__FUNCTION__, $folderid, $id, $contentparameters);
371	}
372
373	/**
374	 * GetAttachmentData - may be MailSpecific
375	 * Should return attachment data for the specified attachment. The passed attachment identifier is
376	 * the exact string that is returned in the 'AttName' property of an SyncAttachment. So, you should
377	 * encode any information you need to find the attachment in that 'attname' property.
378	 *
379	 * @param string $attname - should contain (folder)id
380	 * @return true, prints the content of the attachment
381	 */
382	function GetAttachmentData($attname)
383	{
384		list($id) = explode(":", $attname); // split name as it is constructed that way FOLDERID:UID:PARTID
385		return $this->run_on_plugin_by_id(__FUNCTION__, $id, $attname);
386	}
387
388	/**
389	 * ItemOperationsGetAttachmentData - may be MailSpecific
390	 * Should return attachment data for the specified attachment. The passed attachment identifier is
391	 * the exact string that is returned in the 'AttName' property of an SyncAttachment. So, you should
392	 * encode any information you need to find the attachment in that 'attname' property.
393	 *
394	 * @param string $attname - should contain (folder)id
395	 * @return SyncAirSyncBaseFileAttachment-object
396	 */
397	function ItemOperationsGetAttachmentData($attname)
398	{
399		list($id) = explode(":", $attname); // split name as it is constructed that way FOLDERID:UID:PARTID
400		return $this->run_on_plugin_by_id(__FUNCTION__, $id, $attname);
401	}
402
403	function ItemOperationsFetchMailbox($entryid, $bodypreference, $mimesupport = 0) {
404		ZLog::Write(LOGLEVEL_DEBUG, __METHOD__.__LINE__.'Entry:'.$entryid.', BodyPref:'.array2string( $bodypreference).', MimeSupport:'.array2string($mimesupport));
405		list($folderid, $uid) = explode(":", $entryid); // split name as it is constructed that way FOLDERID:UID:PARTID
406		$type = $folder = $app = null;
407		$this->splitID($folderid, $type, $folder, $app);
408		//ZLog::Write(LOGLEVEL_DEBUG, __METHOD__.__LINE__."$folderid, $type, $folder, $app");
409		if ($app == 'mail')
410		{									// GetMessage($folderid, $id, $truncsize, $bodypreference=false, $optionbodypreference=false, $mimesupport = 0)
411			return $this->run_on_plugin_by_id('GetMessage', $folderid, $uid, $truncsize=($bodypreference[1]['TruncationSize']?$bodypreference[1]['TruncationSize']:500), $bodypreference, false, $mimesupport);
412		}
413		return false;
414	}
415
416	/**
417	 * StatMessage should return message stats, analogous to the folder stats (StatFolder). Entries are:
418	 * 'id'     => Server unique identifier for the message. Again, try to keep this short (under 20 chars)
419	 * 'flags'  => simply '0' for unread, '1' for read
420	 * 'mod'    => modification signature. As soon as this signature changes, the item is assumed to be completely
421	 *             changed, and will be sent to the PDA as a whole. Normally you can use something like the modification
422	 *             time for this field, which will change as soon as the contents have changed.
423	 *
424	 * @param string $folderid
425	 * @param int integer id of message
426	 * @return array
427	 */
428	function StatMessage($folderid, $id)
429	{
430		if ($id < 0)
431		{
432			$type = $folder = $app = null;
433			$this->splitID($folderid, $type, $folder, $app);
434			if (($app == 'mail') && $folder == 0)
435			{
436				return $this->run_on_all_plugins('StatMeetingRequest',array(),$id);
437			}
438		}
439		return $this->run_on_plugin_by_id(__FUNCTION__, $folderid, $id);
440	}
441
442	/**
443	 * Indicates if the backend has a ChangesSink.
444	 * A sink is an active notification mechanism which does not need polling.
445	 * The EGroupware backend simulates a sink by polling status information of the folder
446	 *
447	 * @access public
448	 * @return boolean
449	 */
450	public function HasChangesSink()
451	{
452		$this->sinkfolders = array();
453		$this->sinkstates = array();
454		return true;
455	}
456
457	/**
458	 * The folder should be considered by the sink.
459	 * Folders which were not initialized should not result in a notification
460	 * of IBacken->ChangesSink().
461	 *
462	 * @param string        $folderid
463	 *
464	 * @access public
465	 * @return boolean      false if found can not be found
466	 */
467	public function ChangesSinkInitialize($folderid)
468	{
469		//ZLog::Write(LOGLEVEL_DEBUG, __METHOD__."($folderid)");
470
471		$this->sinkfolders[] = $folderid;
472
473		return true;
474	}
475
476	/**
477	 * The actual ChangesSink.
478	 * For max. the $timeout value this method should block and if no changes
479	 * are available return an empty array.
480	 * If changes are available a list of folderids is expected.
481	 *
482	 * @param int           $timeout        max. amount of seconds to block
483	 *
484	 * @access public
485	 * @return array
486	 */
487	public function ChangesSink($timeout = 30)
488	{
489		ZLog::Write(LOGLEVEL_DEBUG, __METHOD__."($timeout)");
490		$notifications = array();
491		$stopat = time() + $timeout - 1;
492
493		while($stopat > time() && empty($notifications))
494		{
495			foreach ($this->sinkfolders as $folderid)
496			{
497				$newstate = null;
498				$this->AlterPingChanges($folderid, $newstate);
499
500				if (!isset($this->sinkstates[$folderid]))
501					$this->sinkstates[$folderid] = $newstate;
502
503				if ($this->sinkstates[$folderid] != $newstate)
504				{
505					ZLog::Write(LOGLEVEL_DEBUG, "ChangeSink() found change for folderid=$folderid from ".array2string($this->sinkstates[$folderid])." to ".array2string($newstate));
506					$notifications[] = $folderid;
507					$this->sinkstates[$folderid] = $newstate;
508				}
509			}
510
511			if (empty($notifications))
512			{
513				$sleep_time = min($timeout, 30);
514				ZLog::Write(LOGLEVEL_DEBUG, __METHOD__."($timeout) no changes, going to sleep($sleep_time)");
515				sleep($sleep_time);
516			}
517		}
518		ZLog::Write(LOGLEVEL_DEBUG, __METHOD__."($timeout) returning ".array2string($notifications));
519
520		return $notifications;
521	}
522
523	/**
524	 * Return a changes array
525	 *
526	 * if changes occurr default diff engine computes the actual changes
527	 *
528	 * We can NOT use run_on_plugin_by_id, because $syncstate is passed by reference!
529	 *
530	 * @param string $folderid
531	 * @param string &$syncstate on call old syncstate, on return new syncstate
532	 * @return array|boolean false if $folderid not found, array() if no changes or array(array("type" => "fakeChange"))
533	 */
534	function AlterPingChanges($folderid, &$syncstate)
535	{
536		$this->setup_plugins();
537
538		$type = $folder = null;
539		$this->splitID($folderid, $type, $folder);
540
541		if (is_numeric($type))
542		{
543			$type = 'mail';
544		}
545
546		$ret = array();		// so unsupported or not enabled/installed backends return "no change"
547		if (isset($this->plugins[$type]) && method_exists($this->plugins[$type], __FUNCTION__))
548		{
549			$this->device_wait_on_failure(__FUNCTION__);
550			try {
551				$ret = call_user_func_array(array($this->plugins[$type], __FUNCTION__), array($folderid, &$syncstate));
552			}
553			catch(Exception $e) {
554				// log error and block device
555				$this->device_wait_on_failure(__FUNCTION__, $type, $e);
556			}
557		}
558		//ZLog::Write(LOGLEVEL_DEBUG, __METHOD__."('$folderid','".array2string($syncstate)."') type=$type, folder=$folder returning ".array2string($ret));
559		return $ret;
560	}
561
562	/**
563	 * Indicates if the Backend supports folder statistics.
564	 *
565	 * @access public
566	 * @return boolean
567	 */
568	public function HasFolderStats()
569	{
570		return true;
571	}
572
573	/**
574	 * Returns a status indication of the folder.
575	 * If there are changes in the folder, the returned value must change.
576	 * The returned values are compared with '===' to determine if a folder needs synchronization or not.
577	 *
578	 * @param string $store         the store where the folder resides
579	 * @param string $folderid      the folder id
580	 *
581	 * @access public
582	 * @return string
583	 */
584	public function GetFolderStat($store, $folderid)
585	{
586		$syncstate = null;
587		$this->AlterPingChanges($folderid, $syncstate);
588		ZLog::Write(LOGLEVEL_DEBUG, __METHOD__."($store, '$folderid') returning ".array2string($syncstate));
589		return $syncstate;
590	}
591
592    /**
593     * Called when a message has been changed on the mobile. The new message must be saved to disk.
594     * The return value must be whatever would be returned from StatMessage() after the message has been saved.
595     * This way, the 'flags' and the 'mod' properties of the StatMessage() item may change via ChangeMessage().
596     * This method will never be called on E-mail items as it's not 'possible' to change e-mail items. It's only
597     * possible to set them as 'read' or 'unread'.
598     *
599     * @param string              $folderid            id of the folder
600     * @param string              $id                  id of the message
601     * @param SyncXXX             $message             the SyncObject containing a message
602     * @param ContentParameters   $contentParameters
603     *
604     * @access public
605     * @return array                        same return value as StatMessage()
606     * @throws StatusException              could throw specific SYNC_STATUS_* exceptions
607     */
608    public function ChangeMessage($folderid, $id, $message, $contentParameters)
609	{
610		return $this->run_on_plugin_by_id(__FUNCTION__, $folderid, $id, $message, $contentParameters);
611	}
612
613    /**
614     * Called when the user moves an item on the PDA from one folder to another. Whatever is needed
615     * to move the message on disk has to be done here. After this call, StatMessage() and GetMessageList()
616     * should show the items to have a new parent. This means that it will disappear from GetMessageList()
617     * of the sourcefolder and the destination folder will show the new message
618     *
619     * @param string              $folderid            id of the source folder
620     * @param string              $id                  id of the message
621     * @param string              $newfolderid         id of the destination folder
622     * @param ContentParameters   $contentParameters
623     *
624     * @access public
625     * @return boolean                      status of the operation
626     * @throws StatusException              could throw specific SYNC_MOVEITEMSSTATUS_* exceptions
627     */
628    public function MoveMessage($folderid, $id, $newfolderid, $contentParameters)
629	{
630		return $this->run_on_plugin_by_id(__FUNCTION__, $folderid, $id, $newfolderid, $contentParameters);
631	}
632
633    /**
634     * Called when the user has requested to delete (really delete) a message. Usually
635     * this means just unlinking the file its in or somesuch. After this call has succeeded, a call to
636     * GetMessageList() should no longer list the message. If it does, the message will be re-sent to the mobile
637     * as it will be seen as a 'new' item. This means that if this method is not implemented, it's possible to
638     * delete messages on the PDA, but as soon as a sync is done, the item will be resynched to the mobile
639     *
640     * @param string              $folderid             id of the folder
641     * @param string              $id                   id of the message
642     * @param ContentParameters   $contentParameters
643     *
644     * @access public
645     * @return boolean                      status of the operation
646     * @throws StatusException              could throw specific SYNC_STATUS_* exceptions
647     */
648    public function DeleteMessage($folderid, $id, $contentParameters)
649	{
650		ZLog::Write(LOGLEVEL_DEBUG, __METHOD__."('$folderid','".array2string($id)."') with Params ".array2string($contentParameters));
651		if ($id < 0)
652		{
653			$type = $folder = $app = null;
654			$this->splitID($folderid, $type, $folder, $app);
655			if ($app == 'mail' && $folder == 0)
656			{
657				return $this->run_on_all_plugins('DeleteMeetingRequest', array(), $id, $contentParameters);
658			}
659		}
660		return $this->run_on_plugin_by_id(__FUNCTION__, $folderid, $id, $contentParameters);
661	}
662
663    /**
664     * Changes the 'read' flag of a message on disk. The $flags
665     * parameter can only be '1' (read) or '0' (unread). After a call to
666     * SetReadFlag(), GetMessageList() should return the message with the
667     * new 'flags' but should not modify the 'mod' parameter. If you do
668     * change 'mod', simply setting the message to 'read' on the mobile will trigger
669     * a full resync of the item from the server.
670     *
671     * @param string              $folderid            id of the folder
672     * @param string              $id                  id of the message
673     * @param int                 $flags               read flag of the message
674     * @param ContentParameters   $contentParameters
675     *
676     * @access public
677     * @return boolean                      status of the operation
678     * @throws StatusException              could throw specific SYNC_STATUS_* exceptions
679     */
680    public function SetReadFlag($folderid, $id, $flags, $contentParameters)
681	{
682		return $this->run_on_plugin_by_id(__FUNCTION__, $folderid, $id, $flags, $contentParameters);
683	}
684
685	function ChangeMessageFlag($folderid, $id, $flag)
686	{
687		return $this->run_on_plugin_by_id(__FUNCTION__, $folderid, $id, $flag);
688	}
689
690	/**
691	 * Applies settings to and gets informations from the device
692	 *
693	 * @param SyncObject    $settings (SyncOOF or SyncUserInformation possible)
694	 *
695	 * @access public
696	 * @return SyncObject   $settings
697	 */
698	public function Settings($settings)
699	{
700		parent::Settings($settings);
701
702		if ($settings instanceof SyncUserInformation)
703		{
704			$settings->emailaddresses[] = $GLOBALS['egw_info']['user']['account_email'];
705			$settings->Status = SYNC_SETTINGSSTATUS_SUCCESS;
706		}
707		if ($settings instanceof SyncOOF)
708		{
709			// OOF (out of office) not yet supported via AS, but required parameter
710			$settings->oofstate = 0;
711
712			// seems bodytype is not allowed in outgoing SyncOOF message
713			// iOS 8.3 shows Oof Response as "Loading ..." instead of "Off"
714			unset($settings->bodytype);
715
716			// iOS console shows: received results for an unknown oof settings request
717			// setting following parameters do not help
718			//$settings->starttime = $settings->endtime = time();
719			//$settings->oofmessage = array();
720
721		}
722
723		// call all plugins with settings
724		$this->run_on_all_plugins(__FUNCTION__, array(), $settings);
725
726		return $settings;
727	}
728
729	/**
730	 * Get provisioning data for a given device and user
731	 *
732	 * @param string $devid
733	 * @param string $user
734	 * @return array
735	 */
736	function getProvision($devid, $user)
737	{
738		$ret = $this->run_on_all_plugins(__FUNCTION__, array(), $devid, $user);
739		//error_log(__METHOD__."('$devid', '$user') returning ".array2string($ret));
740		return $ret;
741	}
742
743	/**
744	 * Checks if the sent policykey matches the latest policykey on the server
745	 *
746	 * Plugins either return array() to NOT change standard response or array('response' => value)
747	 *
748	 * @param string $policykey
749	 * @param string $devid
750	 *
751	 * @return int status flag SYNC_PROVISION_STATUS_SUCCESS, SYNC_PROVISION_STATUS_POLKEYMISM (triggers provisioning)
752	 */
753	function CheckPolicy($policykey, $devid)
754	{
755		$response_in = array('response' => parent::CheckPolicy($policykey, $devid));
756
757		// allow plugins to overwrite standard responses
758		$response = $this->run_on_all_plugins(__FUNCTION__, $response_in, $policykey, $devid);
759
760		//error_log(__METHOD__."('$policykey', '$devid') returning ".array2string($response['response']));
761		return $response['response'];
762	}
763
764	/**
765	 * Return a device wipe status
766	 *
767	 * Z-Push will send remote wipe request to client, if returned status is SYNC_PROVISION_RWSTATUS_PENDING or _WIPED
768	 *
769	 * Plugins either return array() to NOT change standard response or array('response' => value)
770	 *
771	 * @param string $user
772	 * @param string $pass
773	 * @param string $devid
774	 * @return int SYNC_PROVISION_RWSTATUS_NA, SYNC_PROVISION_RWSTATUS_OK, SYNC_PROVISION_RWSTATUS_PENDING, SYNC_PROVISION_RWSTATUS_WIPED
775	 */
776	function getDeviceRWStatus($user, $pass, $devid)
777	{
778		$response_in = array('response' => false);
779		// allow plugins to overwrite standard responses
780		$response = $this->run_on_all_plugins(__FUNCTION__, $response_in, $user, $pass, $devid);
781
782		//error_log(__METHOD__."('$user', '$pass', '$devid') returning ".array2string($response['response']));
783		return $response['response'];
784	}
785
786	/**
787	 * Set a new rw status for the device
788	 *
789	 * Z-Push call this with SYNC_PROVISION_RWSTATUS_WIPED, after sending remote wipe command
790	 *
791	 * Plugins either return array() to NOT change standard response or array('response' => value)
792	 *
793	 * @param string $user
794	 * @param string $pass
795	 * @param string $devid
796	 * @param int $status SYNC_PROVISION_RWSTATUS_OK, SYNC_PROVISION_RWSTATUS_PENDING, SYNC_PROVISION_RWSTATUS_WIPED
797	 *
798	 * @return boolean seems not to be used in Z-Push
799	 */
800	function setDeviceRWStatus($user, $pass, $devid, $status)
801	{
802		$response_in = array('response' => false);
803		// allow plugins to overwrite standard responses
804		$response = $this->run_on_all_plugins(__FUNCTION__, $response_in, $user, $pass, $devid, $status);
805
806		//error_log(__METHOD__."('$user', '$pass', '$devid', '$status') returning ".array2string($response['response']));
807		return $response['response'];
808	}
809
810	/**
811	 * Sends a message which is passed as rfc822. You basically can do two things
812	 * 1) Send the message to an SMTP server as-is
813	 * 2) Parse the message yourself, and send it some other way
814	 * It is up to you whether you want to put the message in the sent items folder. If you
815	 * want it in 'sent items', then the next sync on the 'sent items' folder should return
816	 * the new message as any other new message in a folder.
817	 *
818	 * @param string $rfc822 mail
819	 * @param array $smartdata =array() values for keys:
820	 * 	'task': 'forward', 'new', 'reply'
821	 *  'itemid': id of message if it's an reply or forward
822	 *  'folderid': folder
823	 *  'replacemime': false = send as is, false = decode and recode for whatever reason ???
824	 *	'saveinsentitems': 1 or absent?
825	 * @param boolean|double $protocolversion =false
826	 * @return boolean true on success, false on error
827	 *
828	 * @see eg. BackendIMAP::SendMail()
829	 * @todo implement either here or in fmail backend
830	 * 	(maybe sending here and storing to sent folder in plugin, as sending is supposed to always work in EGroupware)
831	 */
832	function SendMail($rfc822, $smartdata=array(), $protocolversion = false)
833	{
834		$ret = $this->run_on_all_plugins(__FUNCTION__, 'return-first', $rfc822, $smartdata, $protocolversion);
835		ZLog::Write(LOGLEVEL_DEBUG, __METHOD__."('$rfc822', ".array2string($smartdata).", $protocolversion) returning ".array2string($ret));
836		return $ret;
837	}
838
839	/**
840	 * Searches the GAL.
841	 *
842	 * @param string                        $searchquery        string to be searched for
843	 * @param string                        $searchrange        specified searchrange
844	 * @param SyncResolveRecipientsPicture  $searchpicture      limitations for picture
845	 *
846	 * @access public
847	 * @return array        search results
848	 * @throws StatusException
849	 */
850	public function GetGALSearchResults($searchquery, $searchrange, $searchpicture)
851	{
852		ZLog::Write(LOGLEVEL_DEBUG, __METHOD__.__LINE__.':'.array2string(array('query'=>$searchquery, 'range'=>$searchrange)));
853		return $this->getSearchResults(array('query'=>$searchquery, 'range'=>$searchrange),'GAL');
854	}
855
856	/**
857	 * Searches for the emails on the server
858	 *
859	 * @param ContentParameter $cpo
860	 *
861	 * @return array
862	 */
863	public function GetMailboxSearchResults($cpo)
864	{
865		ZLog::Write(LOGLEVEL_DEBUG, __METHOD__.__LINE__.':'.array2string($cpo));
866		return $this->getSearchResults($cpo,'MAILBOX');
867	}
868
869    /**
870     * Returns a ISearchProvider implementation used for searches
871     *
872     * @access public
873     * @return object       Implementation of ISearchProvider
874     */
875    public function GetSearchProvider()
876	{
877		return $this;
878	}
879
880	/**
881	 * Indicates if a search type is supported by this SearchProvider
882	 * Currently only the type SEARCH_GAL (Global Address List) is implemented
883	 *
884	 * @param string        $searchtype
885	 *
886	 * @access public
887	 * @return boolean
888	 */
889	public function SupportsType($searchtype)
890	{
891		ZLog::Write(LOGLEVEL_DEBUG, __METHOD__.__LINE__.'='.array2string($searchtype));
892		return ($searchtype == ISearchProvider::SEARCH_MAILBOX) || ($searchtype == ISearchProvider::SEARCH_GAL);
893	}
894
895	/**
896	 * Terminates a search for a given PID
897	 *
898	 * @param int $pid
899	 *
900	 * @return boolean
901	 */
902	public function TerminateSearch($pid)
903	{
904		ZLog::Write(LOGLEVEL_DEBUG, __METHOD__.__LINE__.' PID:'.array2string($pid));
905		return true;
906	}
907
908	/**
909	 * Disconnects from the current search provider
910	 *
911	 * @access public
912	 * @return boolean
913	 */
914	public function Disconnect()
915	{
916		return true;
917	}
918
919	/**
920	 * Returns array of items which contain searched for information
921	 *
922	 * @param string $searchquery
923	 * @param string $searchname
924	 *
925	 * @return array
926	 */
927	function getSearchResults($searchquery,$searchname)
928	{
929		ZLog::Write(LOGLEVEL_DEBUG, "EGW:getSearchResults : query: ". print_r($searchquery,true) . " : searchname : ". $searchname);
930		switch (strtoupper($searchname)) {
931			case 'GAL':
932				$rows = $this->run_on_all_plugins('getSearchResultsGAL',array(),$searchquery);
933				break;
934			case 'MAILBOX':
935				$rows = $this->run_on_all_plugins('getSearchResultsMailbox',array(),$searchquery);
936				break;
937		  	case 'DOCUMENTLIBRARY':
938				$rows = $this->run_on_all_plugins('getSearchDocumentLibrary',array(),$searchquery);
939		  		break;
940		  	default:
941		  		ZLog::Write(LOGLEVEL_DEBUG, __METHOD__." unknown searchname ". $searchname);
942		  		return NULL;
943		}
944		if (is_array($rows))
945		{
946			$result =  &$rows;
947		}
948		//error_log(__METHOD__."('$searchquery', '$searchname') returning ".count($result['rows']).' rows = '.array2string($result));
949		return $result;
950	}
951
952/*
95304/28/11 21:55:28 [8923] POST cmd: MeetingResponse
95404/28/11 21:55:28 [8923] I  <MeetingResponse:MeetingResponse>
95504/28/11 21:55:28 [8923] I   <MeetingResponse:Request>
95604/28/11 21:55:28 [8923] I    <MeetingResponse:UserResponse>
95704/28/11 21:55:28 [8923] I     1
95804/28/11 21:55:28 [8923] I    </MeetingResponse:UserResponse>
95904/28/11 21:55:28 [8923] I    <MeetingResponse:FolderId>
96004/28/11 21:55:28 [8923] I     101000000000
96104/28/11 21:55:28 [8923] I    </MeetingResponse:FolderId>
96204/28/11 21:55:28 [8923] I    <MeetingResponse:RequestId>
96304/28/11 21:55:28 [8923] I     99723
96404/28/11 21:55:28 [8923] I    </MeetingResponse:RequestId>
96504/28/11 21:55:28 [8923] I   </MeetingResponse:Request>
96604/28/11 21:55:28 [8923] I  </MeetingResponse:MeetingResponse>
96704/28/11 21:55:28 [8923] activesync_backend::MeetingResponse('99723', '101000000000', '1', ) returning FALSE
96804/28/11 21:55:28 [8923] O  <MeetingResponse:MeetingResponse>
96904/28/11 21:55:28 [8923] O   <MeetingResponse:Result>
97004/28/11 21:55:28 [8923] O    <MeetingResponse:RequestId>
97104/28/11 21:55:28 [8923] O    99723
97204/28/11 21:55:28 [8923] O    </MeetingResponse:RequestId>
97304/28/11 21:55:28 [8923] O    <MeetingResponse:Status>
97404/28/11 21:55:28 [8923] O    2
97504/28/11 21:55:28 [8923] O    </MeetingResponse:Status>
97604/28/11 21:55:28 [8923] O   </MeetingResponse:Result>
97704/28/11 21:55:28 [8923] O  </MeetingResponse:MeetingResponse>
978*/
979	/**
980	 *
981	 * @see BackendDiff::MeetingResponse()
982	 * @param int $requestid uid of mail with meeting request
983	 * @param string $folderid folder of meeting request mail
984	 * @param int $response 1=accepted, 2=tentative, 3=decline
985	 * @return boolean calendar-id on success, false on error
986	 */
987	function MeetingResponse($requestid, $folderid, $response)
988	{
989		$calendarid = $this->run_on_plugin_by_id(__FUNCTION__, $folderid, $requestid, $response);
990
991		ZLog::Write(LOGLEVEL_DEBUG, __METHOD__."('$requestid', '$folderid', '$response') returning ".array2string($calendarid));
992		return $calendarid;
993	}
994
995	/**
996	 * Type ID for addressbook
997	 *
998	 * To work around a bug in Android / HTC the ID must not have leading "0" (as they get removed)
999	 *
1000	 * @var int
1001	 */
1002	const TYPE_ADDRESSBOOK = 0x1000;
1003	const TYPE_CALENDAR = 0x1001;
1004	const TYPE_INFOLOG = 0x1002;
1005	const TYPE_MAIL = 0x1010;
1006
1007	/**
1008	 * Create a max. 32 hex letter ID, current 20 chars are used
1009	 *
1010	 * Currently only $folder supports negative numbers correctly on 64bit PHP systems
1011	 *
1012	 * @param int|string $type appname or integer mail account id
1013	 * @param int $folder integer folder ID
1014	 * @return string
1015	 * @throws Api\Exception\WrongParameter
1016	 */
1017	public function createID($type,$folder)
1018	{
1019		// get a nummeric $type
1020		switch((string)($t=$type))	// we have to cast to string, as (0 == 'addressbook')===TRUE!
1021		{
1022			case 'addressbook':
1023				$type = self::TYPE_ADDRESSBOOK;
1024				break;
1025			case 'calendar':
1026				$type = self::TYPE_CALENDAR;
1027				break;
1028			case 'infolog':
1029				$type = self::TYPE_INFOLOG;
1030				break;
1031			case 'mail':
1032				$type = self::TYPE_MAIL;
1033				break;
1034			default:
1035				if (!is_numeric($type))
1036				{
1037					throw new Api\Exception\WrongParameter("type='$type' is NOT nummeric!");
1038				}
1039				$type += self::TYPE_MAIL;
1040				break;
1041		}
1042
1043		if (!is_numeric($folder))
1044		{
1045			throw new Api\Exception\WrongParameter("folder='$folder' is NOT nummeric!");
1046		}
1047
1048		$folder_hex = sprintf('%08X',$folder);
1049		// truncate negative number on a 64bit system to 8 hex digits = 32bit
1050		if (strlen($folder_hex) > 8) $folder_hex = substr($folder_hex,-8);
1051		$str = sprintf('%04X',$type).$folder_hex;
1052
1053		//ZLog::Write(LOGLEVEL_DEBUG, __METHOD__."('$t','$folder') type=$type --> '$str'");
1054		return $str;
1055	}
1056
1057	/**
1058	 * Split an ID string into $app and $folder
1059	 *
1060	 * Currently only $folder supports negative numbers correctly on 64bit PHP systems
1061	 *
1062	 * @param string $str
1063	 * @param string|int &$type on return appname or integer mail account ID
1064	 * @param int &$folder on return integer folder ID
1065	 * @param string &$app=null application of ID
1066	 * @throws Api\Exception\WrongParameter
1067	 */
1068	public function splitID($str,&$type,&$folder,&$app=null)
1069	{
1070		$type = hexdec(substr($str,0,4));
1071		$folder = hexdec(substr($str,4,8));
1072		// convert 32bit negative numbers on a 64bit system to a 64bit negative number
1073		if ($folder > 0x7fffffff) $folder -= 0x100000000;
1074
1075		switch($type)
1076		{
1077			case self::TYPE_ADDRESSBOOK:
1078				$app = $type = 'addressbook';
1079				break;
1080			case self::TYPE_CALENDAR:
1081				$app = $type = 'calendar';
1082				break;
1083			case self::TYPE_INFOLOG:
1084				$app = $type = 'infolog';
1085				break;
1086			default:
1087				if ($type < self::TYPE_MAIL)
1088				{
1089					throw new Api\Exception\WrongParameter("Unknown type='$type'!");
1090				}
1091				$app = 'mail';
1092				$type -= self::TYPE_MAIL;
1093				break;
1094		}
1095		// ZLog::Write(LOGLEVEL_DEBUG, __METHOD__."('$str','$type','$folder')");
1096	}
1097
1098	/**
1099	 * Convert note to requested bodypreference format and truncate if requested
1100	 *
1101	 * @param string $note containing the plaintext message
1102	 * @param array $bodypreference
1103	 * @param SyncBaseBody $airsyncbasebody the airsyncbasebody object to send to the client
1104	 *
1105	 * @return string plain textbody for message or false
1106	 */
1107	public function note2messagenote($note, $bodypreference, SyncBaseBody $airsyncbasebody)
1108	{
1109		//error_log (__METHOD__."('$note', ".array2string($bodypreference).", ...)");
1110		if ($bodypreference == false)
1111		{
1112			return $note;
1113		}
1114		else
1115		{
1116			if (isset($bodypreference[2]))
1117			{
1118				ZLog::Write(LOGLEVEL_DEBUG, "HTML Body");
1119				$airsyncbasebody->type = 2;
1120				$html = '<html>'.
1121						'<head>'.
1122						'<meta name="Generator" content="eGroupware/Z-Push">'.
1123						'<meta http-equiv="Content-Type" content="text/html; charset=utf-8">'.
1124						'</head>'.
1125						'<body>'.
1126						// using <p> instead of <br/>, as W10mobile seems to have problems with it
1127						str_replace(array("\r\n", "\n", "\r"), "<p>", $note).
1128						'</body>'.
1129						'</html>';
1130				if (isset($bodypreference[2]["TruncationSize"]) && strlen($html) > $bodypreference[2]["TruncationSize"])
1131				{
1132					$html = utf8_truncate($html,$bodypreference[2]["TruncationSize"]);
1133					$airsyncbasebody->truncated = 1;
1134				}
1135				$airsyncbasebody->estimateddatasize = strlen($html);
1136				$airsyncbasebody->data = StringStreamWrapper::Open($html);
1137			}
1138			else
1139			{
1140				ZLog::Write(LOGLEVEL_DEBUG, "Plaintext Body");
1141				$airsyncbasebody->type = 1;
1142				$plainnote = str_replace("\n","\r\n",str_replace("\r","",$note));
1143				if(isset($bodypreference[1]["TruncationSize"]) && strlen($plainnote) > $bodypreference[1]["TruncationSize"])
1144				{
1145					$plainnote = utf8_truncate($plainnote, $bodypreference[1]["TruncationSize"]);
1146					$airsyncbasebody->truncated = 1;
1147				}
1148				$airsyncbasebody->estimateddatasize = strlen($plainnote);
1149				$airsyncbasebody->data = StringStreamWrapper::Open((string)$plainnote !== '' ? $plainnote : ' ');
1150			}
1151			if ($airsyncbasebody->type != 3 && !isset($airsyncbasebody->data))
1152			{
1153				$airsyncbasebody->data = StringStreamWrapper::Open(" ");
1154			}
1155		}
1156	}
1157
1158	/**
1159	 * Convert received messagenote to egroupware plaintext note
1160	 *
1161	 * @param string $body the plain body received
1162	 * @param string $rtf the rtf body data
1163	 * @param SyncBaseBody $airsyncbasebody =null object received from client, or null if none received
1164	 *
1165	 * @return string plaintext for eGroupware
1166	 */
1167	public function messagenote2note($body, $rtf, SyncBaseBody $airsyncbasebody=null)
1168	{
1169		if (isset($airsyncbasebody) && is_resource($airsyncbasebody->data))
1170		{
1171			switch($airsyncbasebody->type)
1172			{
1173				case '3':
1174					$rtf = stream_get_contents($airsyncbasebody->data);
1175					//error_log("Airsyncbase RTF Body");
1176					break;
1177
1178				case '2':
1179					$body = Api\Mail\Html::convertHTMLToText(stream_get_contents($airsyncbasebody->data));
1180					break;
1181
1182				case '1':
1183					$body = stream_get_contents($airsyncbasebody->data);
1184					//error_log("Airsyncbase Plain Body");
1185					break;
1186			}
1187		}
1188		// Nokia MfE 2.9.158 sends contact notes with RTF and Body element.
1189		// The RTF is empty, the body contains the note therefore we need to unpack the rtf
1190		// to see if it is realy empty and in case not, take the appointment body.
1191		/*if (isset($message->rtf))
1192		{
1193			error_log("RTF Body");
1194			$rtf_body = new rtf ();
1195			$rtf_body->loadrtf(base64_decode($rtf));
1196			$rtf_body->output("ascii");
1197			$rtf_body->parse();
1198			if (isset($body) && isset($rtf_body->out) && $rtf_body->out == "" && $body != "")
1199			{
1200				unset($rtf);
1201			}
1202			if($rtf_body->out <> "") $body=$rtf_body->out;
1203		}*/
1204		ZLog::Write(LOGLEVEL_DEBUG, __METHOD__."(body=".array2string($body).", rtf=".array2string($rtf).", airsyncbasebody=".array2string($airsyncbasebody).") returning ".array2string($body));
1205		return $body;
1206	}
1207
1208	/**
1209	 * Run and return settings from all plugins
1210	 *
1211	 * @param array|string $hook_data
1212	 * @return array with settings from all plugins
1213	 */
1214	public function egw_settings($hook_data)
1215	{
1216		return $this->run_on_all_plugins('egw_settings',array(),$hook_data);
1217	}
1218
1219	/**
1220	 * Run and return verify_settings from all plugins
1221	 *
1222	 * @param array|string $hook_data
1223	 * @return array with error-messages from all plugins
1224	 */
1225	public function verify_settings($hook_data)
1226	{
1227		return $this->run_on_all_plugins('verify_settings', array(), $hook_data);
1228	}
1229
1230    /**
1231     * Returns the waste basket
1232     *
1233     * The waste basked is used when deleting items; if this function returns a valid folder ID,
1234     * then all deletes are handled as moves and are sent to the backend as a move.
1235     * If it returns FALSE, then deletes are handled as real deletes
1236     *
1237     * @access public
1238     * @return string
1239     */
1240    public function GetWasteBasket()
1241	{
1242		//return $this->run_on_all_plugins(__FUNCTION__, 'return-first');
1243		return false;
1244	}
1245
1246	/**
1247     * Deletes a folder
1248     *
1249     * @param string        $id
1250     * @param string        $parentid         is normally false
1251     *
1252     * @access public
1253     * @return boolean                      status - false if e.g. does not exist
1254     * @throws StatusException              could throw specific SYNC_FSSTATUS_* exceptions
1255     */
1256    public function DeleteFolder($id, $parentid=false)
1257	{
1258		$ret = $this->run_on_plugin_by_id(__FUNCTION__, $id,  $parentid);
1259		if (!$ret) ZLog::Write(LOGLEVEL_ERROR, __METHOD__." WARNING : something failed deleting a folder ($id), now informing the device that this has failed");
1260		return $ret;
1261	}
1262
1263	/**
1264	 * Plugins to use, filled by setup_plugins
1265	 *
1266	 * @var array
1267	 */
1268	private $plugins;
1269
1270	/**
1271	 * Run a certain method on the plugin for the given id
1272	 *
1273	 * @param string $method
1274	 * @param string $id will be first parameter to method
1275	 * @param optional further parameters
1276	 * @return mixed
1277	 */
1278	public function run_on_plugin_by_id($method,$id)
1279	{
1280		$this->setup_plugins();
1281
1282		// check if device is still blocked
1283		$this->device_wait_on_failure($method);
1284
1285		$type = $folder = null;
1286		$this->splitID($id, $type, $folder);
1287
1288		if (is_numeric($type))
1289		{
1290			$type = 'mail';
1291		}
1292		$params = func_get_args();
1293		array_shift($params);	// remove $method
1294
1295		$ret = false;
1296		if (isset($this->plugins[$type]) && method_exists($this->plugins[$type], $method))
1297		{
1298			try {
1299				//error_log($method.' called with Params:'.array2string($params));
1300				$ret = call_user_func_array(array($this->plugins[$type], $method),$params);
1301			}
1302			catch(Exception $e) {
1303				// log error and block device
1304				$this->device_wait_on_failure($method, $type, $e);
1305			}
1306		}
1307		//error_log(__METHOD__."('$method','$id') type=$type, folder=$folder returning ".array2string($ret));
1308		return $ret;
1309	}
1310
1311	/**
1312	 * Run a certain method on all plugins
1313	 *
1314	 * @param string $method
1315	 * @param mixed $agregate=array() if array given array_merge is used, otherwise +=
1316	 * 	or 'return-first' to return result from first matching plugin returning not null, false or '' result
1317	 * @param optional parameters
1318	 * @return mixed agregated result
1319	 */
1320	public function run_on_all_plugins($method,$agregate=array())
1321	{
1322		$this->setup_plugins();
1323
1324		// check if device is still blocked
1325		$this->device_wait_on_failure($method);
1326
1327		$params = func_get_args();
1328		array_shift($params); array_shift($params);	// remove $method+$agregate
1329
1330		foreach($this->plugins as $app => $plugin)
1331		{
1332			if (method_exists($plugin, $method))
1333			{
1334				try {
1335					//ZLog::Write(LOGLEVEL_DEBUG, __METHOD__."() calling ".get_class($plugin).'::'.$method);
1336					$result = call_user_func_array(array($plugin, $method),$params);
1337					//ZLog::Write(LOGLEVEL_DEBUG, __METHOD__."() calling ".get_class($plugin).'::'.$method.' returning '.array2string($result));
1338				}
1339				catch(Exception $e) {
1340					// log error and block device
1341					$this->device_wait_on_failure($method, $app, $e);
1342				}
1343
1344				if (is_array($agregate))
1345				{
1346					$agregate = array_merge($agregate,$result);
1347					//error_log(__METHOD__."('$method', , ".array2string($params).") result plugin::$method=".array2string($result).' --> agregate='.array2string($agregate));
1348				}
1349				elseif ($agregate === 'return-first')
1350				{
1351					if ($result)
1352					{
1353						$agregate = $result;
1354						break;
1355					}
1356				}
1357				else
1358				{
1359					//error_log(__METHOD__."('$method') agg:".array2string($agregate).' res:'.array2string($result));
1360					$agregate += (is_bool($agregate)? (bool) $result:$result);
1361				}
1362			}
1363		}
1364		if ($agregate === 'return-first') $agregate = false;
1365		//error_log(__METHOD__."('$method') returning ".array2string($agregate));
1366		return $agregate;
1367	}
1368
1369	/**
1370	 * Instanciate all plugins the user has application rights for
1371	 */
1372	private function setup_plugins()
1373	{
1374		if (isset($this->plugins)) return;
1375
1376		$this->plugins = array();
1377		if (isset($GLOBALS['egw_info']['user']['apps'])) $apps = array_keys($GLOBALS['egw_info']['user']['apps']);
1378		if (!isset($apps))	// happens during setup
1379		{
1380			$apps = array('addressbook', 'calendar', 'mail', 'infolog'/*, 'filemanager'*/);
1381		}
1382		// allow apps without user run-rights to hook into eSync
1383		if (($hook_data = Api\Hooks::process('esync_extra_apps', array(), true)))	// true = no perms. check
1384		{
1385			foreach($hook_data as $app => $extra_apps)
1386			{
1387				if ($extra_apps) $apps = array_unique(array_merge($apps, (array)$extra_apps));
1388			}
1389		}
1390		foreach($apps as $app)
1391		{
1392			if (strpos($app,'_')!==false) continue;
1393			$class = $app.'_zpush';
1394			if (class_exists($class))
1395			{
1396				$this->plugins[$app] = new $class($this);
1397			}
1398		}
1399		//error_log(__METHOD__."() hook_data=".array2string($hook_data).' returning '.array2string(array_keys($this->plugins)));
1400	}
1401
1402	/**
1403	 * Name of log for blocked devices within instances files dir or null to not log blocking
1404	 */
1405	const BLOCKING_LOG = 'esync-blocking.log';
1406
1407	/**
1408	 * Check or set device failure mode: in failure mode we only return 503 Service unavailble
1409	 *
1410	 * Behavior on exceptions eg. connection failures (flags stored by user and device-ID):
1411	 * a) if connections fails initialy:
1412	 *    we log time under "lastattempt" and initial blocking-time of self::$waitOnFailureDefault=30 as "howlong" and
1413	 *    send a "Retry-After: 30" header and a HTTP-Status of "503 Service Unavailable"
1414	 * b) if clients attempts connection before lastattempt+howlong:
1415	 *    send a "Retry-After: <remaining-time>" header and a HTTP-Status of "503 Service Unavailable"
1416	 * c) if connection fails again within twice the blocking time:
1417	 *    we double the blocking time up to maximum of $this->waitOnFailureLimit=7200=2h and
1418	 *    send a "Retry-After: 2*<blocking-time>" header and a HTTP-Status of "503 Service Unavailable"
1419	 *
1420	 * @link https://social.msdn.microsoft.com/Forums/en-US/3658aca8-36fd-4058-9d43-10f48c3f7d3b/what-does-commandfrequency-mean-with-respect-to-eas-throttling?forum=os_exchangeprotocols
1421	 * @param string $method z-push backend method called
1422	 * @param string $app application of pluging causing the failure
1423	 * @param Exception $set =null
1424	 */
1425	private function device_wait_on_failure($method, $app=null, $set=null)
1426	{
1427		if (($dev_id = Request::GetDeviceID()) === false)
1428		{
1429			return;	// no real request, or request not yet initialised
1430		}
1431		$waitOnFailure = Api\Cache::getInstance(__CLASS__, 'waitOnFailure-'.$GLOBALS['egw_info']['user']['account_lid'], function()
1432		{
1433			return array();
1434		});
1435		$deviceWaitOnFailure =& $waitOnFailure[$dev_id];
1436
1437		// check if device is blocked
1438		if (!isset($set))
1439		{
1440			// case b: client attempts new connection, before blocking-time is over --> return 503 Service Unavailable immediatly
1441			if ($deviceWaitOnFailure && $deviceWaitOnFailure['lastattempt']+$deviceWaitOnFailure['howlong'] > time())
1442			{
1443				$keepwaiting = $deviceWaitOnFailure['lastattempt']+$deviceWaitOnFailure['howlong'] - time();
1444				ZLog::Write(LOGLEVEL_ERROR, "$method() still blocking for an other $keepwaiting seconds for Instance=".$GLOBALS['egw_info']['user']['domain'].', User='.$GLOBALS['egw_info']['user']['account_lid'].', Device:'.Request::GetDeviceID());
1445				if (self::BLOCKING_LOG) error_log(date('Y-m-d H:i:s ')."$method() still blocking for an other $keepwaiting seconds for Instance=".$GLOBALS['egw_info']['user']['domain'].', User='.$GLOBALS['egw_info']['user']['account_lid'].', Device:'.Request::GetDeviceID()."\n", 3, $GLOBALS['egw_info']['server']['files_dir'].'/'.self::BLOCKING_LOG);
1446				// let z-push know we want to terminate
1447				header("Retry-After: ".$keepwaiting);
1448				throw new HTTPReturnCodeException('Service Unavailable', 503);
1449			}
1450			return;	// everything ok, device is not blocked
1451		}
1452		// block device because Exception happend in $method plugin for $app
1453
1454		// case a) initial failure: set lastattempt and howlong=self::$waitOnFailureDefault=30
1455		if (!$deviceWaitOnFailure || time() > $deviceWaitOnFailure['lastattempt']+2*$deviceWaitOnFailure['howlong'])
1456		{
1457			$deviceWaitOnFailure = array(
1458				'lastattempt' => time(),
1459				'howlong'     => self::$waitOnFailureDefault,
1460				'app'         => $app,
1461			);
1462		}
1463		// case c) connection failed again: double waiting time up to max. of self::$waitOnFailureLimit=2h
1464		else
1465		{
1466			$deviceWaitOnFailure = array(
1467				'lastattempt' => time(),
1468				'howlong'     => 2*$deviceWaitOnFailure['howlong'],
1469				'app'         => $app,
1470			);
1471			if ($deviceWaitOnFailure['howlong'] > self::$waitOnFailureLimit)
1472			{
1473				$deviceWaitOnFailure['howlong'] = self::$waitOnFailureLimit;
1474			}
1475		}
1476		Api\Cache::setInstance(__CLASS__, 'waitOnFailure-'.$GLOBALS['egw_info']['user']['account_lid'], $waitOnFailure);
1477
1478		ZLog::Write(LOGLEVEL_ERROR, "$method() Error happend in $app ".$set->getMessage()." blocking for $deviceWaitOnFailure[howlong] seconds for Instance=".$GLOBALS['egw_info']['user']['domain'].', User='.$GLOBALS['egw_info']['user']['account_lid'].', Device:'.Request::GetDeviceID());
1479		if (self::BLOCKING_LOG) error_log(date('Y-m-d H:i:s ')."$method() Error happend in $app: ".$set->getMessage()." blocking for $deviceWaitOnFailure[howlong] seconds for Instance=".$GLOBALS['egw_info']['user']['domain'].', User='.$GLOBALS['egw_info']['user']['account_lid'].', Device:'.Request::GetDeviceID()."\n", 3, $GLOBALS['egw_info']['server']['files_dir'].'/'.self::BLOCKING_LOG);
1480		// let z-push know we want to terminate
1481		header("Retry-After: ".$deviceWaitOnFailure['howlong']);
1482		throw new HTTPReturnCodeException('Service Unavailable', 503, $set);
1483	}
1484
1485    /**
1486     * Indicates which AS version is supported by the backend.
1487     * By default AS version 2.5 (ASV_25) is returned (Z-Push 1 standard).
1488     * Subclasses can overwrite this method to set another AS version
1489     *
1490     * @access public
1491     * @return string       AS version constant
1492     */
1493    public function GetSupportedASVersion()
1494	{
1495        return ZPush::ASV_14;
1496    }
1497
1498	/**
1499	 * Returns a IStateMachine implementation used to save states
1500	 * The default StateMachine should be used here, so, false is fine
1501	 *
1502	 * @access public
1503	 * @return boolean/object
1504	 */
1505	public function GetStateMachine()
1506	{
1507		return new activesync_statemachine($this);
1508	}
1509}
1510