Completed
Push — master ( 471742...334697 )
by Ralf
15:03
created

ZLog::Write()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 1
Code Lines 0

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 0
nc 1
nop 2
dl 0
loc 1
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * EGroupware: eSync: Calendar plugin
4
 *
5
 * @link http://www.egroupware.org
6
 * @package calendar
7
 * @subpackage esync
8
 * @author Ralf Becker <[email protected]>
9
 * @author Klaus Leithoff
10
 * @author Philip Herbert <[email protected]>
11
 * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
12
 * @version $Id$
13
 */
14
15
use EGroupware\Api;
16
use EGroupware\Api\Acl;
17
18
/**
19
 * Required for TZID <--> AS timezone blob test, if script is called directly via URL
20
 */
21
if (isset($_SERVER['SCRIPT_FILENAME']) && realpath($_SERVER['SCRIPT_FILENAME']) == __FILE__)
22
{
23
	interface activesync_plugin_write {}
24
	interface activesync_plugin_meeting_requests {}
25
	class ZLog { static function Write($level, $msg) { unset($level, $msg); }}
26
}
27
28
/**
29
 * Calendar eSync plugin
30
 *
31
 * Plugin to make EGroupware calendar data available
32
 *
33
 * Handling of (virtual) exceptions of recurring events:
34
 * ----------------------------------------------------
35
 * Virtual exceptions are exceptions caused by recurcences with just different participant status
36
 * compared to regular series (master). Real exceptions usually have different dates and/or other data.
37
 * EGroupware calendar data model does NOT store virtual exceptions as exceptions,
38
 * as participant status is stored per recurrence date and not just per event!
39
 *
40
 * --> ActiveSync protocol does NOT support different participants or status for exceptions!
41
 *
42
 * Handling of alarms:
43
 * ------------------
44
 * We report only alarms of the current user (which should ring on the device)
45
 * and save alarms set on the device only for the current user, if not yet there (preserving all other alarms).
46
 * How to deal with multiple alarms allowed in EGroupware: report earliest one to the device
47
 * (and hope it resyncs before next one is due, thought we do NOT report that as change currently!).
48
 */
49
class calendar_zpush implements activesync_plugin_write, activesync_plugin_meeting_requests
50
{
51
	/**
52
	 * var activesync_backend
53
	 */
54
	private $backend;
55
56
	/**
57
	 * Instance of calendar_bo
58
	 *
59
	 * @var calendar_boupdate
60
	 */
61
	private $calendar;
62
63
	/**
64
	 * Instance of Api\Contacts
65
	 *
66
	 * @var Api\Contacts
67
	 */
68
	private $addressbook;
69
70
	/**
71
	 * Constructor
72
	 *
73
	 * @param activesync_backend $backend
74
	 */
75
	public function __construct(activesync_backend $backend)
0 ignored issues
show
Bug introduced by
The type activesync_backend was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
76
	{
77
		$this->backend = $backend;
78
	}
79
80
	/**
81
	 *  This function is analogous to GetMessageList.
82
	 */
83
	public function GetFolderList()
84
	{
85
		if (!isset($this->calendar)) $this->calendar = new calendar_boupdate();
86
87
		$cals_pref = $GLOBALS['egw_info']['user']['preferences']['activesync']['calendar-cals'];
88
		$cals = $cals_pref ? explode(',',$cals_pref) : array('P');	// implicit default of 'P'
89
		$folderlist = array();
90
91
		foreach ($this->calendar->list_cals() as $entry)
92
		{
93
			$account_id = $entry['grantor'];
94
			if (in_array('A',$cals) || in_array($account_id,$cals) ||
95
				$account_id == $GLOBALS['egw_info']['user']['account_id'] ||	// always incl. own calendar!
96
				$account_id == $GLOBALS['egw_info']['user']['account_primary_group'] && in_array('G',$cals))
97
			{
98
				$folderlist[] = $f = array(
0 ignored issues
show
Unused Code introduced by
The assignment to $f is dead and can be removed.
Loading history...
99
					'id'	=>	$this->backend->createID('calendar',$account_id),
100
					'mod'	=>	$GLOBALS['egw']->accounts->id2name($account_id,'account_fullname'),
101
					'parent'=>	'0',
102
				);
103
			}
104
		}
105
		//error_log(__METHOD__."() returning ".array2string($folderlist));
106
		ZLog::Write(LOGLEVEL_DEBUG, __METHOD__."() returning ".array2string($folderlist));
107
		return $folderlist;
108
	}
109
110
	/**
111
	 * Get Information about a folder
112
	 *
113
	 * @param string $id
114
	 * @return SyncFolder|boolean false on error
115
	 */
116
	public function GetFolder($id)
117
	{
118
		$type = $owner = null;
119
		$this->backend->splitID($id, $type, $owner);
120
121
		$folderObj = new SyncFolder();
122
		$folderObj->serverid = $id;
123
		$folderObj->parentid = '0';
124
		$folderObj->displayname = $GLOBALS['egw']->accounts->id2name($owner,'account_fullname');
125
		if ($owner == $GLOBALS['egw_info']['user']['account_id'])
126
		{
127
			$folderObj->type = SYNC_FOLDER_TYPE_APPOINTMENT;
128
		}
129
		else
130
		{
131
			$folderObj->type = SYNC_FOLDER_TYPE_USER_APPOINTMENT;
132
		}
133
		//error_log(__METHOD__."('$id') folderObj=".array2string($folderObj));
134
		//ZLog::Write(LOGLEVEL_DEBUG, __METHOD__."('$id') folderObj=".array2string($folderObj));
135
		return $folderObj;
136
	}
137
138
	/**
139
	 * Return folder stats. This means you must return an associative array with the
140
	 * following properties:
141
	 *
142
	 * "id" => The server ID that will be used to identify the folder. It must be unique, and not too long
143
	 *		 How long exactly is not known, but try keeping it under 20 chars or so. It must be a string.
144
	 * "parent" => The server ID of the parent of the folder. Same restrictions as 'id' apply.
145
	 * "mod" => This is the modification signature. It is any arbitrary string which is constant as long as
146
	 *		  the folder has not changed. In practice this means that 'mod' can be equal to the folder name
147
	 *		  as this is the only thing that ever changes in folders. (the type is normally constant)
148
	 *
149
	 * @return array with values for keys 'id', 'mod' and 'parent'
150
	 */
151
	public function StatFolder($id)
152
	{
153
		$type = $owner = null;
154
		$this->backend->splitID($id, $type, $owner);
155
156
		$stat = array(
157
			'id'	 => $id,
158
			'mod'	=> $GLOBALS['egw']->accounts->id2name($owner,'account_fullname'),
159
			'parent' => '0',
160
		);
161
		//error_log(__METHOD__."('$id') folderObj=".array2string($stat));
162
		//ZLog::Write(LOGLEVEL_DEBUG, __METHOD__."('$id') folderObj=".array2string($stat));
163
		return $stat;
164
	}
165
166
	/**
167
	 * Should return a list (array) of messages, each entry being an associative array
168
	 * with the same entries as StatMessage(). This function should return stable information; ie
169
	 * if nothing has changed, the items in the array must be exactly the same. The order of
170
	 * the items within the array is not important though.
171
	 *
172
	 * The cutoffdate is a date in the past, representing the date since which items should be shown.
173
	 * This cutoffdate is determined by the user's setting of getting 'Last 3 days' of e-mail, etc. If
174
	 * you ignore the cutoffdate, the user will not be able to select their own cutoffdate, but all
175
	 * will work OK apart from that.
176
	 *
177
	 * @param string $id folder id
178
	 * @param int $cutoffdate =null
179
	 * @param array $not_uids =null uids NOT to return for meeting requests
180
	 * @return array
181
  	 */
182
	function GetMessageList($id, $cutoffdate=NULL, array $not_uids=null)
183
	{
184
		if (!isset($this->calendar)) $this->calendar = new calendar_boupdate();
185
186
		ZLog::Write(LOGLEVEL_DEBUG, __METHOD__."('$id',$cutoffdate)");
187
		$type = $user = null;
188
		$this->backend->splitID($id,$type,$user);
189
190
		if (!$cutoffdate) $cutoffdate = $this->bo->now - 100*24*3600;	// default three month back -30 breaks all sync recurrences
0 ignored issues
show
Bug Best Practice introduced by
The property bo does not exist on calendar_zpush. Did you maybe forget to declare it?
Loading history...
Bug Best Practice introduced by
The expression $cutoffdate of type integer|null is loosely compared to false; this is ambiguous if the integer can be 0. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
191
192
		$filter = array(
193
			'users' => $user,
194
			'start' => $cutoffdate,	// default one month back -30 breaks all sync recurrences
195
			'enum_recuring' => false,
196
			'daywise' => false,
197
			'date_format' => 'server',
198
			// default = not rejected, current user return NO meeting requests (status=unknown), as they are returned via email!
199
			// with filter="default" iOS 12.3 and z-push 2.5 shows events double: 1. email/meeting-request and 2. calendar entry itself
200
			// ToDo: use filter="default", if user does NOT have email or email-notfications in calendar
201
			'filter' => $user == $GLOBALS['egw_info']['user']['account_id'] ? (is_array($not_uids) ? 'unknown' : 'not-unknown') : 'default',
202
			//'filter' => $user == $GLOBALS['egw_info']['user']['account_id'] ? (is_array($not_uids) ? 'unknown' : 'default') : 'default',
203
			// @todo return only etag relevant information (seems not to work ...)
204
			//'cols'		=> array('egw_cal.cal_id', 'cal_start',	'recur_type', 'cal_modified', 'cal_uid', 'cal_etag'),
205
			'query' => array('cal_recurrence' => 0),	// do NOT return recurrence exceptions
206
		);
207
208
		$messagelist = array();
209
		// reading events in chunks of 100, to keep memory down for huge calendars
210
		$num_rows = 100;
211
		for($start=0; ($events = $this->calendar->search($filter+array(
212
			'offset'   => $start,
213
			'num_rows' => $num_rows,
214
		))); $start += $num_rows)
215
		{
216
			foreach ($events as $event)
217
			{
218
				if ($not_uids && in_array($event['uid'], $not_uids)) continue;
219
				$messagelist[] = $this->StatMessage($id, $event);
220
221
				// add virtual exceptions for recuring events too
222
				// (we need to read event, as get_recurrence_exceptions need all infos!)
223
	/*			if ($event['recur_type'] != calendar_rrule::NONE)// && ($event = $this->calendar->read($event['id'],0,true,'server')))
224
				{
225
226
					foreach($this->calendar->so->get_recurrence_exceptions($event,
227
						Api\DateTime::$server_timezone->getName(), $cutoffdate, 0, 'all') as $recur_date)
228
					{
229
						$messagelist[] = $this->StatMessage($id, $event['id'].':'.$recur_date);
230
					}
231
				}*/
232
			}
233
			if (count($events) < $num_rows) break;
234
		}
235
		//error_log(__METHOD__."($id, $cutoffdate, ".array2string($not_uids).") type=$type, user=$user returning ".count($messagelist)." messages ".function_backtrace());
236
		return $messagelist;
237
	}
238
239
	/**
240
	 * List all meeting requests / invitations of user NOT having a UID in $not_uids (already received by email)
241
	 *
242
	 * @param array $not_uids
243
	 * @param int $cutoffdate =null
244
	 * @return array
245
	 */
246
	function GetMeetingRequests(array $not_uids, $cutoffdate=NULL)
247
	{
248
		unset($not_uids, $cutoffdate);
249
		return array();
250
		/* temporary disabling meeting requests from calendar
251
		$folderid = $this->backend->createID('calendar', $GLOBALS['egw_info']['user']['account_id']);	// users personal calendar
252
253
		$ret = $this->GetMessageList($folderid, $cutoffdate, $not_uids);
254
		// return all id's negative to not conflict with uids from fmail
255
		foreach($ret as &$message)
256
		{
257
			$message['id'] = -$message['id'];
258
		}
259
260
		ZLog::Write(LOGLEVEL_DEBUG, __METHOD__.'('.array2string($not_uids).", $cutoffdate) returning ".array2string($ret));
261
		return $ret;*/
262
	}
263
264
	/**
265
	 * Stat a meeting request
266
	 *
267
	 * @param int $id negative! id
268
	 * @return array
269
	 */
270
	function StatMeetingRequest($id)
271
	{
272
		$folderid = $this->backend->createID('calendar', $GLOBALS['egw_info']['user']['account_id']);	// users personal calendar
273
274
		$ret = $this->StatMessage($folderid, abs($id));
275
		$ret['id'] = $id;
276
277
		ZLog::Write(LOGLEVEL_DEBUG, __METHOD__."($id) returning ".array2string($ret));
278
		return $ret;
279
	}
280
281
	/**
282
	 * Return a meeting request as AS SyncMail object
283
	 *
284
	 * @param int $id negative! cal_id
285
	 * @param int $truncsize
286
	 * @param int $bodypreference
287
	 * @param $optionbodypreference
288
	 * @param bool $mimesupport
289
	 * @return SyncMail
290
	 */
291
	function GetMeetingRequest($id, $truncsize, $bodypreference=false, $optionbodypreference=false, $mimesupport = 0)
292
	{
293
		unset($truncsize, $optionbodypreference, $mimesupport);	// not used, but required by function signature
294
295
		if (!isset($this->calendar)) $this->calendar = new calendar_boupdate();
296
297
		if (!($event = $this->calendar->read(abs($id), 0, false, 'server')))
298
		{
299
			$message = false;
300
		}
301
		else
302
		{
303
			$message = new SyncMail();
304
			$message->read = false;
305
			$message->subject = $event['title'];
306
			$message->importance = 1;	// 0=Low, 1=Normal, 2=High
307
			$message->datereceived = $event['created'];
308
			$message->to = $message->displayto = $GLOBALS['egw_info']['user']['account_email'];
309
			$message->from = $GLOBALS['egw']->accounts->id2name($event['owner'],'account_fullname').
310
				' <'.$GLOBALS['egw']->accounts->id2name($event['owner'],'account_email').'>';
311
			$message->internetcpid = 65001;
312
			$message->contentclass="urn:content-classes:message";
313
314
			$message->meetingrequest = self::meetingRequest($event);
315
			$message->messageclass = "IPM.Schedule.Meeting.Request";
316
317
			// add description as message body
318
			if ($bodypreference == false)
0 ignored issues
show
Bug Best Practice introduced by
It seems like you are loosely comparing $bodypreference of type false|integer against false; this is ambiguous if the integer can be zero. Consider using a strict comparison === instead.
Loading history...
319
			{
320
				$message->body = $event['description'];
321
				$message->bodysize = strlen($message->body);
322
				$message->bodytruncated = 0;
323
			}
324
			else
325
			{
326
				$message->airsyncbasebody = new SyncAirSyncBaseBody();
0 ignored issues
show
Bug introduced by
The type SyncAirSyncBaseBody was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
Bug introduced by
The property airsyncbasebody does not seem to exist on SyncMail.
Loading history...
327
				$message->airsyncbasenativebodytype=1;
0 ignored issues
show
Bug introduced by
The property airsyncbasenativebodytype does not exist on SyncMail. Did you mean nativebodytype?
Loading history...
328
				$this->backend->note2messagenote($event['description'], $bodypreference, $message->airsyncbasebody);
329
			}
330
		}
331
		ZLog::Write(LOGLEVEL_DEBUG, __METHOD__."($id) returning ".array2string($message));
332
		return $message;
333
	}
334
335
	/**
336
	 * Generate SyncMeetingRequest object from an event array
337
	 *
338
	 * Used by (calendar|mail)_zpush
339
	 *
340
	 * @param array|string $event event array or string with iCal
341
	 * @return SyncMeetingRequest or null ical not parsable
342
	 */
343
	public static function meetingRequest($event)
344
	{
345
		if (!is_array($event))
346
		{
347
			$ical = new calendar_ical();
348
			if (!($events = $ical->icaltoegw($event, '', 'utf-8')) || count($events) != 1)
349
			{
350
				ZLog::Write(LOGLEVEL_DEBUG, __METHOD__."('$event') error parsing iCal!");
351
				return null;
352
			}
353
			$event = array_shift($events);
354
			ZLog::Write(LOGLEVEL_DEBUG, __METHOD__."(...) parsed as ".array2string($event));
355
		}
356
		$message = new SyncMeetingRequest();
357
		// set timezone
358
		try {
359
			$as_tz = self::tz2as($event['tzid']);
360
			$message->timezone = base64_encode(self::_getSyncBlobFromTZ($as_tz));
361
		}
362
		catch(Exception $e) {
363
			unset($e);
364
			// ignore exception, simply set no timezone, as it is optional
365
		}
366
		// copying timestamps (they are already read in servertime, so non tz conversation)
367
		foreach(array(
368
			'start' => 'starttime',
369
			'end'   => 'endtime',
370
			'created' => 'dtstamp',
371
		) as $key => $attr)
372
		{
373
			if (!empty($event[$key])) $message->$attr = $event[$key];
374
		}
375
		if (($message->alldayevent = (int)calendar_bo::isWholeDay($event)))
376
		{
377
			++$message->endtime;	// EGw all-day-events are 1 sec shorter!
378
		}
379
		// copying strings
380
		foreach(array(
381
			'title' => 'subject',
382
			'location' => 'location',
383
		) as $key => $attr)
384
		{
385
			if (!empty($event[$key])) $message->$attr = $event[$key];
386
		}
387
		$message->organizer = $event['organizer'];
388
389
		$message->sensitivity = !isset($event['public']) || $event['public'] ? 0 : 2;	// 0=normal, 1=personal, 2=private, 3=confidential
390
391
		// busystatus=(0=free|1=tentative|2=busy|3=out-of-office), EGw has non_blocking=0|1
392
		$message->busystatus = $event['non_blocking'] ? 0 : 2;
393
394
		// ToDo: recurring events: InstanceType, RecurrenceId, Recurrences; ...
395
		$message->instancetype = 0;	// 0=Single, 1=Master recurring, 2=Single recuring, 3=Exception
396
397
		$message->responserequested = 1;	//0=No, 1=Yes
398
		$message->disallownewtimeproposal = 1;	//1=forbidden, 0=allowed
399
		//$message->messagemeetingtype;	// email2
400
401
		// ToDo: alarme: Reminder
402
403
		// convert UID to GlobalObjID
404
		$message->globalobjid = activesync_backend::uid2globalObjId($event['uid']);
405
406
		return $message;
407
	}
408
409
	/**
410
	 * Process response to meeting request
411
	 *
412
	 * @see BackendDiff::MeetingResponse()
413
	 * @param string $folderid folder of meeting request mail
414
	 * @param int|string $requestid cal_id, or string with iCal from fmail plugin
415
	 * @param int $response 1=accepted, 2=tentative, 3=decline
416
	 * @return int|boolean id of calendar item, false on error
417
	 */
418
	function MeetingResponse($folderid, $requestid, $response)
419
	{
420
		if (!isset($this->calendar)) $this->calendar = new calendar_boupdate();
421
422
		static $as2status = array(	// different from self::$status2as!
423
			1 => 'A',
424
			2 => 'T',
425
			3 => 'D',
426
		);
427
		$status_in = isset($as2status[$response]) ? $as2status[$response] : 'U';
428
		$uid = $GLOBALS['egw_info']['user']['account_id'];
429
430
		if (!is_numeric($requestid))	// iCal from fmail
431
		{
432
			$ical = new calendar_ical();
433
			if (!($events = $ical->icaltoegw($requestid, '', 'utf-8')) || count($events) != 1)
434
			{
435
				ZLog::Write(LOGLEVEL_DEBUG, __METHOD__."($folderid, '$requestid') error parsing iCal!");
436
				return null;
437
			}
438
			$parsed_event = array_shift($events);
439
			ZLog::Write(LOGLEVEL_DEBUG, __METHOD__."(...) parsed as ".array2string($parsed_event));
440
441
			// check if event already exist (invitation of or already imported by other user)
442
			if (!($event = $this->calendar->read($parsed_event['uid'], 0, false, 'server')))
443
			{
444
				$event = $parsed_event;	// create new event from external invitation
445
			}
446
			elseif(!isset($event['participants'][$uid]))
447
			{
448
				ZLog::Write(LOGLEVEL_DEBUG, __METHOD__.'('.array2string($requestid).", $folderid, $response) current user ($uid) is NO participant of event ".array2string($event));
449
				// maybe we should silently add him, as he might not have the rights to add him himself with calendar->update ...
450
			}
451
			elseif($event['deleted'])
452
			{
453
				ZLog::Write(LOGLEVEL_DEBUG, __METHOD__.'('.array2string($requestid).", $folderid, $response) event ($uid) deleted on server --> return false");
454
				return false;
455
			}
456
		}
457
		elseif (!($event = $this->calendar->read(abs($requestid), 0, false, 'server')))
458
		{
459
			ZLog::Write(LOGLEVEL_DEBUG, __METHOD__."('$requestid', '$folderid', $response) returning FALSE");
460
			return false;
461
		}
462
		// keep role and quantity as AS has no idea about it
463
		$quantity = $role = null;
464
		calendar_so::split_status($event['participants'][$uid], $quantity, $role);
465
		$status = calendar_so::combine_status($status_in, $quantity, $role);
466
467
		if ($event['id'] && isset($event['participants'][$uid]))
468
		{
469
			$ret = $this->calendar->set_status($event, $uid, $status) ? $event['id'] : false;
470
			$msg = $ret ? "status '$status' set for event #$ret" : "could NOT set status '$status' for event #$event[id]";
471
			ZLog::Write(LOGLEVEL_DEBUG, __METHOD__.'('.array2string($requestid).", '$folderid', $response) $msg, returning ".array2string($ret));
472
		}
473
		else
474
		{
475
			$event['participants'][$uid] = $status;
476
			$ret = $this->calendar->update($event, true);	// true = ignore conflicts, as there seems no conflict handling in AS
477
			$msg = $ret ? "new event #$ret created" : "could NOT create event";
478
			ZLog::Write(LOGLEVEL_DEBUG, __LINE__.': '.__METHOD__.'('.array2string($requestid).", '$folderid', $response) $msg, returning ".array2string($ret));
479
		}
480
		return $ret;
481
	}
482
483
	/**
484
	 * Conversation to AS status
485
	 *
486
	 * @var array
487
	 */
488
	static $status2as = array(
489
		'U' => 0,	// unknown
490
		'T' => 2,	// tentative
491
		'A' => 3,	// accepted
492
		'R' => 4,	// decline
493
		// 5 = not responded
494
	);
495
	/**
496
	 * Conversation to AS "roles", not really the same thing
497
	 *
498
	 * @var array
499
	 */
500
	static $role2as = array(
501
		'REQ-PARTICIPANT' => 1,	// required
502
		'CHAIR' => 1,			// required
503
		'OPT-PARTICIPANT' => 2,	// optional
504
		'NON-PARTICIPANT' => 2,
505
		// 3 = ressource
506
	);
507
	/**
508
	 * Conversation to AS recurrence types
509
	 *
510
	 * @var array
511
	 */
512
	static $recur_type2as = array(
513
		calendar_rrule::DAILY => 0,
514
		calendar_rrule::WEEKLY => 1,
515
		calendar_rrule::MONTHLY_MDAY => 2,	// monthly
516
		calendar_rrule::MONTHLY_WDAY => 3,	// monthly on nth day
517
		calendar_rrule::YEARLY => 5,
518
		// 6 = yearly on nth day (same as 5 on non-leapyears or before March on leapyears)
519
	);
520
521
	/**
522
	 * Changes or adds a message on the server
523
	 *
524
	 * Timestamps from z-push are in servertime and need to get converted to user-time, as bocalendar_update::save()
525
	 * expects user-time!
526
	 *
527
	 * @param string $folderid
528
	 * @param int $_id for change | empty for create new
529
	 * @param SyncAppointment $message object to SyncObject to create
530
	 * @param ContentParameters   $contentParameters
531
	 *
532
	 * @return array $stat whatever would be returned from StatMessage
533
	 *
534
	 * This function is called when a message has been changed on the PDA. You should parse the new
535
	 * message here and save the changes to disk. The return value must be whatever would be returned
536
	 * from StatMessage() after the message has been saved. This means that both the 'flags' and the 'mod'
537
	 * properties of the StatMessage() item may change via ChangeMessage().
538
	 * Note that this function will never be called on E-mail items as you can't change e-mail items, you
539
	 * can only set them as 'read'.
540
	 */
541
	public function ChangeMessage($folderid, $_id, $message, $contentParameters)
542
	{
543
		unset($contentParameters);	// unused, but required by function signature
544
545
		if (!isset($this->calendar)) $this->calendar = new calendar_boupdate();
546
547
		$old_event = array();
548
		$type = $account = null;
549
		$this->backend->splitID($folderid, $type, $account);
550
551
		ZLog::Write(LOGLEVEL_DEBUG, __METHOD__."('$folderid', $_id, ".array2string($message).") type='$type', account=$account");
552
553
		list($id,$recur_date) = explode(':', $_id);
554
555
		if ($type != 'calendar' || $id && !($old_event = $this->calendar->read($id, $recur_date, false, 'server')))
0 ignored issues
show
introduced by
The condition $type != 'calendar' is always true.
Loading history...
556
		{
557
			ZLog::Write(LOGLEVEL_DEBUG, __METHOD__."('$folderid',$id,...) Folder wrong or event does not existing");
558
			return false;
559
		}
560
		if ($recur_date)	// virtual exception
561
		{
562
			// @todo check if virtual exception needs to be saved as real exception, or only stati need to be changed
563
			ZLog::Write(LOGLEVEL_DEBUG, __METHOD__."('$folderid',$id:$recur_date,".array2string($message).") handling of virtual exception not yet implemented!");
564
			//error_log(__METHOD__."('$folderid',$id:$recur_date,".array2string($message).") handling of virtual exception not yet implemented!");
565
		}
566
		if (!$this->calendar->check_perms($id ? Acl::EDIT : Acl::ADD, $old_event ? $old_event : 0,$account))
567
		{
568
			// @todo: write in users calendar and make account only a participant
569
			ZLog::Write(LOGLEVEL_DEBUG, __METHOD__."('$folderid',$id,...) no rights to add/edit event!");
570
			//error_log(__METHOD__."('$folderid',$id,".array2string($message).") no rights to add/edit event!");
571
			return false;
572
		}
573
		if (!$id) $old_event['owner'] = $account;	// we do NOT allow to change the owner of existing events
574
575
		$event = $this->message2event($message, $account, $old_event);
576
577
		// store event, ignore conflicts and skip notifications, as AS clients do their own notifications
578
		$skip_notification = false;
579
		if (isset($GLOBALS['egw_info']['user']['preferences']['activesync']['mail-allowSendingInvitations']) &&
580
			$GLOBALS['egw_info']['user']['preferences']['activesync']['mail-allowSendingInvitations']=='send')
581
		{
582
			$skip_notification = true; // to avoid double notification from client AND Server
583
		}
584
		$messages = null;
585
		if (!($id = $this->calendar->update($event, true, true, false, true, $messages, $skip_notification)))
586
		{
587
			ZLog::Write(LOGLEVEL_DEBUG, __METHOD__."('$folderid',$id,...) error saving event=".array2string($event)."!");
588
			return false;
589
		}
590
		// store non-delete exceptions
591
		if ($message->exceptions)
592
		{
593
			foreach($message->exceptions as $exception)
594
			{
595
				if (!$exception->deleted)
596
				{
597
					$ex_event = $event;
598
					unset($ex_event['id']);
599
					unset($ex_event['etag']);
600
					foreach(array_keys($ex_event) as $name)
601
					{
602
						if (substr($name,0,6) == 'recur_') unset($ex_event[$name]);
603
					}
604
					$ex_event['recur_type'] = calendar_rrule::NONE;
605
606
					if ($event['id'] && ($ex_events = $this->calendar->search(array(
607
						'user' => $account,
608
						'enum_recuring' => false,
609
						'daywise' => false,
610
						'filter' => 'owner',  // return all possible entries
611
						'query' => array(
612
							'cal_uid' => $event['uid'],
613
							'cal_recurrence' => $exception->exceptionstarttime,	// in servertime
614
						),
615
					))))
616
					{
617
						$ex_event = array_shift($ex_events);
618
						$participants = $ex_event['participants'];
619
					}
620
					else
621
					{
622
						$participants = $event['participants'];
623
					}
624
					$save_event = $this->message2event($exception, $account, $ex_event);
625
					$save_event['participants'] = $participants;	// not contained in $exception
626
					$save_event['reference'] = $event['id'];
627
					$save_event['recurrence'] = Api\DateTime::server2user($exception->exceptionstarttime);
628
					$ex_ok = $this->calendar->save($save_event);
629
					ZLog::Write(LOGLEVEL_DEBUG, __METHOD__."('$folderid',$id,...) saving exception=".array2string($save_event).' returned '.array2string($ex_ok));
630
					//error_log(__METHOD__."('$folderid',$id,...) exception=".array2string($exception).") saving exception=".array2string($save_event).' returned '.array2string($ex_ok));
631
				}
632
			}
633
		}
634
		ZLog::Write(LOGLEVEL_DEBUG, __METHOD__."('$folderid',$id,...) SUCESS saving event=".array2string($event).", id=$id");
635
		//error_log(__METHOD__."('$folderid',$id,".array2string($message).") SUCESS saving event=".array2string($event).", id=$id");
636
		return $this->StatMessage($folderid, $id);
637
	}
638
639
	/**
640
	 * Parse AS message into EGw event array
641
	 *
642
	 * @param SyncAppointment $message
643
	 * @param int $account
644
	 * @param array $event =array()
645
	 * @return array
646
	 */
647
	private function message2event(SyncAppointment $message, $account, $event=array())
648
	{
649
		// timestamps (created & modified are updated automatically)
650
		foreach(array(
651
			'start' => 'starttime',
652
			'end' => 'endtime',
653
		) as $key => $attr)
654
		{
655
			if (isset($message->$attr)) $event[$key] = Api\DateTime::server2user($message->$attr);
656
		}
657
		// copying strings
658
		foreach(array(
659
			'title' => 'subject',
660
			'uid'   => 'uid',
661
			'location' => 'location',
662
		) as $key => $attr)
663
		{
664
			if (isset($message->$attr)) $event[$key] = $message->$attr;
665
		}
666
667
		// only change description, if one given, as iOS5 skips description in ChangeMessage after MeetingResponse
668
		// --> we ignore empty / not set description, so description get no longer lost, but you cant empty it via eSync
669
		if (($description = $this->backend->messagenote2note($message->body, $message->rtf, $message->asbody)))
670
		{
671
			$event['description'] = $description;
672
		}
673
		$event['public'] = (int)($message->sensitivity < 1);	// 0=normal, 1=personal, 2=private, 3=confidential
674
675
		// busystatus=(0=free|1=tentative|2=busy|3=out-of-office), EGw has non_blocking=0|1
676
		if (isset($message->busystatus))
677
		{
678
			$event['non_blocking'] = $message->busystatus ? 0 : 1;
679
		}
680
681
		if (($event['whole_day'] = $message->alldayevent))
682
		{
683
			if ($event['end'] == $event['start']) $event['end'] += 24*3600;	// some clients send equal start&end for 1day
684
			$event['end']--;	// otherwise our whole-day event code in save makes it one more day!
685
		}
686
687
		$participants = array();
688
		foreach((array)$message->attendees as $attendee)
689
		{
690
			if ($attendee->type == 3) continue;	// we can not identify resources and re-add them anyway later
691
692
			$matches = null;
693
			if (preg_match('/^noreply-(.*)[email protected]$/',$attendee->email,$matches))
694
			{
695
				$uid = $matches[1];
696
			}
697
			elseif (!($uid = $GLOBALS['egw']->accounts->name2id($attendee->email,'account_email')))
698
			{
699
				$search = array(
700
					'email' => $attendee->email,
701
					'email_home' => $attendee->email,
702
					//'n_fn' => $attendee->name,	// not sure if we want matches without email
703
				);
704
				// search addressbook for participant
705
				if (!isset($this->addressbook)) $this->addressbook = new Api\Contacts();
706
				if ((list($data) = $this->addressbook->search($search,
707
					array('id','egw_addressbook.account_id as account_id','n_fn'),
708
					'egw_addressbook.account_id IS NOT NULL DESC, n_fn IS NOT NULL DESC',
709
					'','',false,'OR')))
710
				{
711
					$uid = $data['account_id'] ? (int)$data['account_id'] : 'c'.$data['id'];
712
				}
713
				elseif($attendee->name === $attendee->email || empty($attendee->name))	// dont store empty or email as name
714
				{
715
					$uid = 'e'.$attendee->email;
716
				}
717
				else	// store just the email
718
				{
719
					$uid = 'e'.$attendee->name.' <'.$attendee->email.'>';
720
				}
721
			}
722
			// as status and (attendee-)type are optional, keep old status, quantity and role, if not specified
723
			if ($event['id'] && isset($event['participants'][$uid]))
724
			{
725
				$status = $event['participants'][$uid];
726
				$quantity = $role = null;
727
				calendar_so::split_status($status, $quantity, $role);
728
				//ZLog::Write(LOGLEVEL_DEBUG, "old status for $uid is status=$status, quantity=$quantitiy, role=$role");
729
			}
730
			// check if just email is an existing attendee (iOS returns email as name too!), keep it to keep status/role if not set
731
			elseif ($event['id'] && (isset($event['participants'][$u='e'.$attendee->email]) ||
732
				(isset($event['participants'][$u='e'.$attendee->name.' <'.$attendee->email.'>']))))
733
			{
734
				$status = $event['participants'][$u];
735
				calendar_so::split_status($status, $quantity, $role);
736
				//ZLog::Write(LOGLEVEL_DEBUG, "old status for $uid as $u is status=$status, quantity=$quantitiy, role=$role");
737
			}
738
			else	// set some defaults
739
			{
740
				$status = 'U';
741
				$quantitiy = 1;
742
				$role = 'REQ-PARTICIPANT';
743
				//ZLog::Write(LOGLEVEL_DEBUG, "default status for $uid is status=$status, quantity=$quantitiy, role=$role");
744
			}
745
			if ($role == 'CHAIR') $chair_set = true;	// by role from existing participant
746
747
			if (isset($attendee->attendeestatus) && ($s = array_search($attendee->attendeestatus,self::$status2as)))
748
			{
749
				$status = $s;
750
			}
751
			if ($attendee->email == $message->organizeremail)
752
			{
753
				$role = 'CHAIR';
754
				$chair_set = true;
755
			}
756
			elseif (isset($attendee->attendeetype) &&
757
				!($role == 'CHAIR' && !is_numeric($uid)) &&	// do not override our external ORGANIZER
758
				($r = array_search($attendee->attendeetype,self::$role2as)) &&
759
				(int)self::$role2as[$role] != $attendee->attendeetype)	// if old role gives same type, use old role, as we have a lot more roles then AS
760
			{
761
				$role = $r;
762
			}
763
			//ZLog::Write(LOGLEVEL_DEBUG, "-> status for $uid is status=$status ($s), quantity=$quantitiy, role=$role ($r)");
764
			$participants[$uid] = calendar_so::combine_status($status,$quantitiy,$role);
765
		}
766
		// if organizer is not already participant, add him as chair
767
		if (($uid = $GLOBALS['egw']->accounts->name2id($message->organizeremail,'account_email')) && !isset($participants[$uid]))
768
		{
769
			$participants[$uid] = calendar_so::combine_status($uid == $GLOBALS['egw_info']['user']['account_id'] ?
770
				'A' : 'U',1,'CHAIR');
771
			$chair_set = true;
772
		}
773
		// preserve all resource types not account, contact or email (eg. resources) for existing events
774
		// $account is also preserved, as AS does not add him as participant!
775
		foreach((array)$event['participant_types'] as $type => $parts)
776
		{
777
			if (in_array($type,array('c','e'))) continue;	// they are correctly representable in AS
778
779
			foreach($parts as $id => $status)
780
			{
781
				// accounts are represented correctly, but the event owner which is no participant in AS
782
				if ($type == 'u' && $id != $account) continue;
783
784
				$uid = calendar_so::combine_user($type, $id);
785
				if (!isset($participants[$uid]))
786
				{
787
					$participants[$uid] = $status;
788
				}
789
			}
790
		}
791
		// add calendar owner as participant, as otherwise event will NOT be in his calendar, in which it was posted
792
		if (!$event['id'] || !$participants || !isset($participants[$account]))
793
		{
794
			$participants[$account] = calendar_so::combine_status($account == $GLOBALS['egw_info']['user']['account_id'] ?
795
				'A' : 'U',1,!$chair_set ? 'CHAIR' : 'REQ-PARTICIPANT');
796
		}
797
		$event['participants'] = $participants;
798
799
		if (isset($message->categories))
800
		{
801
			$event['category'] = implode(',', array_filter($this->calendar->find_or_add_categories($message->categories, $event),'strlen'));
802
		}
803
804
		// check if event is recurring and import recur information (incl. timezone)
805
		if ($message->recurrence)
806
		{
807
			if ($message->timezone && !$event['id'])	// dont care for timezone, if no new and recurring event
808
			{
809
				$event['tzid'] = self::as2tz(self::_getTZFromSyncBlob(base64_decode($message->timezone)));
810
			}
811
			$event['recur_type'] = $message->recurrence->type == 6 ? calendar_rrule::YEARLY :
812
				array_search($message->recurrence->type, self::$recur_type2as);
813
			$event['recur_interval'] = $message->recurrence->interval;
814
815
			switch ($event['recur_type'])
816
			{
817
				case calendar_rrule::MONTHLY_WDAY:
818
					// $message->recurrence->weekofmonth is not explicitly stored in egw, just taken from start date
819
					// fall throught
820
				case calendar_rrule::WEEKLY:
821
					$event['recur_data'] = $message->recurrence->dayofweek;	// 1=Su, 2=Mo, 4=Tu, .., 64=Sa
822
					break;
823
				case calendar_rrule::MONTHLY_MDAY:
824
					// $message->recurrence->dayofmonth is not explicitly stored in egw, just taken from start date
825
					break;
826
				case calendar_rrule::YEARLY:
827
					// $message->recurrence->(dayofmonth|monthofyear) is not explicitly stored in egw, just taken from start date
828
					break;
829
			}
830
			if ($message->recurrence->until)
831
			{
832
				$event['recur_enddate'] = Api\DateTime::server2user($message->recurrence->until);
833
			}
834
			$event['recur_exception'] = array();
835
			if ($message->exceptions)
836
			{
837
				foreach($message->exceptions as $exception)
838
				{
839
					$event['recur_exception'][] = Api\DateTime::server2user($exception->exceptionstarttime);
840
				}
841
				$event['recur_exception'] = array_unique($event['recur_exception']);
842
			}
843
			if ($message->recurrence->occurrences > 0)
844
			{
845
				// calculate enddate from occurences count, as we only support enddate
846
				$count = $message->recurrence->occurrences;
847
				foreach(calendar_rrule::event2rrule($event, true) as $rtime)	// true = timestamps are user time here, because of save!
848
				{
849
					if (--$count <= 0) break;
850
				}
851
				$event['recur_enddate'] = $rtime->format('ts');
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $rtime seems to be defined by a foreach iteration on line 847. Are you sure the iterator is never empty, otherwise this variable is not defined?
Loading history...
852
			}
853
		}
854
		// only import alarms in own calendar
855
		if ($message->reminder && $account == $GLOBALS['egw_info']['user']['account_id'])
856
		{
857
			foreach((array)$event['alarm'] as $alarm)
858
			{
859
				if (($alarm['all'] || $alarm['owner'] == $account) && $alarm['offset'] == 60*$message->reminder)
860
				{
861
					$alarm = true;	// alarm already exists --> do nothing
862
					break;
863
				}
864
			}
865
			if ($alarm !== true)	// new alarm
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $alarm seems to be defined by a foreach iteration on line 857. Are you sure the iterator is never empty, otherwise this variable is not defined?
Loading history...
866
			{
867
				// delete all earlier alarms of that user
868
				// user get's per AS only the earliest alarm, as AS only supports one alarm
869
				// --> if a later alarm is returned, user probably modifed an existing alarm
870
				foreach((array)$event['alarm'] as $key => $alarm)
871
				{
872
					if ($alarm['owner'] == $account && $alarm['offset'] > 60*$message->reminder)
873
					{
874
						unset($event['alarm'][$key]);
875
					}
876
				}
877
				$event['alarm'][] = $alarm = array(
0 ignored issues
show
Unused Code introduced by
The assignment to $alarm is dead and can be removed.
Loading history...
878
					'owner' => $account,
879
					'offset' => 60*$message->reminder,
880
				);
881
			}
882
		}
883
		return $event;
884
	}
885
886
	/**
887
	 *  Creates or modifies a folder
888
	 *
889
	 * @param string $id of the parent folder
890
	 * @param string $oldid => if empty -> new folder created, else folder is to be renamed
891
	 * @param string $displayname => new folder name (to be created, or to be renamed to)
892
	 * @param string $type folder type, ignored in IMAP
893
	 *
894
	 * @return array|boolean stat array or false on error
895
	 */
896
	public function ChangeFolder($id, $oldid, $displayname, $type)
897
	{
898
		ZLog::Write(LOGLEVEL_DEBUG, __METHOD__."('$id', '$oldid', '$displayname', $type) NOT supported!");
899
		return false;
900
	}
901
902
	/**
903
	 * Deletes (really delete) a Folder
904
	 *
905
	 * @param string $parentid of the folder to delete
906
	 * @param string $id of the folder to delete
907
	 *
908
	 * @return
909
	 * @TODO check what is to be returned
910
	 */
911
	public function DeleteFolder($parentid, $id)
912
	{
913
		ZLog::Write(LOGLEVEL_DEBUG, __METHOD__."('$parentid', '$id') NOT supported!");
914
		return false;
915
	}
916
917
	/**
918
	 * Moves a message from one folder to another
919
	 *
920
	 * @param $folderid of the current folder
0 ignored issues
show
Bug introduced by
The type of was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
921
	 * @param $id of the message
922
	 * @param $newfolderid
923
	 * @param ContentParameters   $contentParameters
924
	 *
925
	 * @return $newid as a string | boolean false on error
0 ignored issues
show
Documentation Bug introduced by
The doc comment $newid at position 0 could not be parsed: Unknown type name '$newid' at position 0 in $newid.
Loading history...
926
	 *
927
	 * After this call, StatMessage() and GetMessageList() should show the items
928
	 * to have a new parent. This means that it will disappear from GetMessageList() will not return the item
929
	 * at all on the source folder, and the destination folder will show the new message
930
	 */
931
	public function MoveMessage($folderid, $id, $newfolderid, $contentParameters)
932
	{
933
		ZLog::Write(LOGLEVEL_DEBUG, __METHOD__."('$folderid', $id, '$newfolderid',".array2string($contentParameters).") NOT supported!");
934
		return false;
935
	}
936
937
	/**
938
	 * Delete (really delete) a message in a folder
939
	 *
940
	 * @param $folderid
941
	 * @param $id
942
	 * @param ContentParameters   $contentParameters
943
	 *
944
	 * @return boolean true on success, false on error, diffbackend does NOT use the returnvalue
945
	 *
946
	 * @DESC After this call has succeeded, a call to
947
	 * GetMessageList() should no longer list the message. If it does, the message will be re-sent to the PDA
948
	 * as it will be seen as a 'new' item. This means that if you don't implement this function, you will
949
	 * be able to delete messages on the PDA, but as soon as you sync, you'll get the item back
950
	 */
951
	public function DeleteMessage($folderid, $id, $contentParameters)
952
	{
953
		unset($contentParameters);	// not used, but required by function signature
954
955
		if (!isset($this->caledar)) $this->calendar = new calendar_boupdate();
0 ignored issues
show
Bug introduced by
The property caledar does not exist on calendar_zpush. Did you mean calendar?
Loading history...
956
957
		$ret = $this->calendar->delete($id);
958
		ZLog::Write(LOGLEVEL_DEBUG, __METHOD__."('$folderid', $id) delete($id) returned ".array2string($ret));
959
		return $ret;
960
	}
961
962
	/**
963
	 * This should change the 'read' flag of a message on disk. The $flags
964
	 * parameter can only be '1' (read) or '0' (unread). After a call to
965
	 * SetReadFlag(), GetMessageList() should return the message with the
966
	 * new 'flags' but should not modify the 'mod' parameter. If you do
967
	 * change 'mod', simply setting the message to 'read' on the PDA will trigger
968
	 * a full resync of the item from the server
969
	 *
970
	 * @param string              $folderid            id of the folder
971
	 * @param string              $id                  id of the message
972
	 * @param int                 $flags               read flag of the message
973
	 * @param ContentParameters   $contentParameters
974
	 *
975
	 * @access public
976
	 * @return boolean                      status of the operation
977
	 * @throws StatusException              could throw specific SYNC_STATUS_* exceptions
978
	 */
979
	function SetReadFlag($folderid, $id, $flags, $contentParameters)
980
	{
981
		unset($contentParameters);	// not used, but required by function signature
982
983
		ZLog::Write(LOGLEVEL_DEBUG, __METHOD__."('$folderid', $id, ".array2string($flags)." NOT supported!");
984
		return false;
985
	}
986
987
	/**
988
	 * modify olflags (outlook style) flag of a message
989
	 *
990
	 * @param $folderid
991
	 * @param $id
992
	 * @param $flags
993
	 *
994
	 * @DESC The $flags parameter must contains the poommailflag Object
995
	 */
996
	function ChangeMessageFlag($folderid, $id, $flags)
997
	{
998
		ZLog::Write(LOGLEVEL_DEBUG, __METHOD__."('$folderid', $id, ".array2string($flags)." NOT supported!");
999
		return false;
1000
	}
1001
1002
	/**
1003
	 * Get specified item from specified folder.
1004
	 *
1005
	 * Timezone wise we supply zpush with timestamps in servertime (!), which it "converts" in streamer::formatDate($ts)
1006
	 * via gmstrftime("%Y%m%dT%H%M%SZ", $ts) to UTC times.
1007
	 * Timezones are only used to get correct recurring events!
1008
	 *
1009
	 * @param string $folderid
1010
	 * @param string|array $id cal_id or event array (used internally)
1011
	 * @param ContentParameters   $contentparameters
1012
	 * @param string $class ='SyncAppointment' or 'SyncAppointmentException'
1013
	 * @return SyncAppointment|boolean false on error
1014
	 */
1015
	public function GetMessage($folderid, $id, $contentparameters, $class='SyncAppointment')
1016
	{
1017
		if (!isset($this->calendar)) $this->calendar = new calendar_boupdate();
1018
		//error_log(__METHOD__.__LINE__.array2string($contentparameters).function_backtrace());
1019
		$truncsize = Utils::GetTruncSize($contentparameters->GetTruncation());
1020
		$mimesupport = $contentparameters->GetMimeSupport();
1021
		$bodypreference = $contentparameters->GetBodyPreference(); /* fmbiete's contribution r1528, ZP-320 */
1022
1023
		ZLog::Write(LOGLEVEL_DEBUG, __METHOD__."('$folderid', ".array2string($id).", truncsize=$truncsize, bodyprefence=".array2string($bodypreference).", mimesupport=$mimesupport)");
1024
		$type = $account = null;
1025
		$this->backend->splitID($folderid, $type, $account);
1026
		if (is_array($id))
1027
		{
1028
			$event = $id;
1029
			$id = $event['id'];
1030
		}
1031
		else
1032
		{
1033
			list($id,$recur_date) = explode(':',$id);
1034
			if ($type != 'calendar' || !($event = $this->calendar->read($id,$recur_date,false,'server',$account)))
0 ignored issues
show
introduced by
The condition $type != 'calendar' is always true.
Loading history...
1035
			{
1036
				error_log(__METHOD__."('$folderid', $id, ...) read($id,null,false,'server',$account) returned false");
1037
				return false;
1038
			}
1039
		}
1040
		ZLog::Write(LOGLEVEL_DEBUG, __METHOD__."($folderid,$id,...) start=$event[start]=".date('Y-m-d H:i:s',$event['start']).", recurrence=$event[recurrence]=".date('Y-m-d H:i:s',$event['recurrence']));
1041
		foreach((array)$event['recur_exception'] as $ex)
1042
		{
1043
			ZLog::Write(LOGLEVEL_DEBUG, "exception=$ex=".date('Y-m-d H:i:s',$ex));
1044
		}
1045
		$message = new $class();
1046
1047
		// set timezone
1048
		try {
1049
			$as_tz = self::tz2as($event['tzid']);
1050
			$message->timezone = base64_encode(self::_getSyncBlobFromTZ($as_tz));
1051
		}
1052
		catch(Exception $e) {
1053
			unset($e);
1054
			// z-push (2.3 at least) requires a timezone for recurring events
1055
			if ($event['recur_type']) $message->timezone = self::UTC_BLOB;
1056
		}
1057
1058
		// copying timestamps (they are already read in servertime, so non tz conversation)
1059
		foreach(array(
1060
			'start' => 'starttime',
1061
			'end'   => 'endtime',
1062
			'created' => 'dtstamp',
1063
			'modified' => 'dtstamp',
1064
		) as $key => $attr)
1065
		{
1066
			if (!empty($event[$key])) $message->$attr = $event[$key];
1067
		}
1068
		if (($message->alldayevent = (int)calendar_bo::isWholeDay($event)))
1069
		{
1070
			++$message->endtime;	// EGw all-day-events are 1 sec shorter!
1071
		}
1072
		// copying strings
1073
		foreach(array(
1074
			'title' => 'subject',
1075
			'uid'   => 'uid',
1076
			'location' => 'location',
1077
		) as $key => $attr)
1078
		{
1079
			if (!empty($event[$key])) $message->$attr = $event[$key];
1080
		}
1081
1082
		// appoint description
1083
		if ($bodypreference == false)
1084
		{
1085
			$message->body = $event['description'];
1086
			$message->bodysize = strlen($message->body);
1087
			$message->bodytruncated = 0;
1088
		}
1089
		else
1090
		{
1091
			if (strlen($event['description']) > 0)
1092
			{
1093
				ZLog::Write(LOGLEVEL_DEBUG, "airsyncbasebody!");
1094
				$message->asbody = new SyncBaseBody();
1095
				$message->nativebodytype=1;
1096
				$this->backend->note2messagenote($event['description'], $bodypreference, $message->asbody);
1097
			}
1098
		}
1099
		$message->organizername  = $GLOBALS['egw']->accounts->id2name($event['owner'],'account_fullname');
1100
		// at least iOS calendar crashes, if organizer has no email address (true = generate an email, if user has none)
1101
		$message->organizeremail = $GLOBALS['egw']->accounts->id2name($event['owner'], 'account_email', true);
1102
1103
		$message->sensitivity = $event['public'] ? 0 : 2;	// 0=normal, 1=personal, 2=private, 3=confidential
1104
1105
		// busystatus=(0=free|1=tentative|2=busy|3=out-of-office), EGw has non_blocking=0|1
1106
		$message->busystatus = $event['non_blocking'] ? 0 : 2;
1107
1108
		$message->attendees = array();
1109
		foreach($event['participants'] as $uid => $status)
1110
		{
1111
			// we send all participants (incl. organizer), as this is what Exchange also does
1112
			$quantity = $role = null;
1113
			calendar_so::split_status($status, $quantity, $role);
1114
1115
			$attendee = new SyncAttendee();
1116
			$attendee->attendeestatus = (int)self::$status2as[$status];
1117
			$attendee->attendeetype = (int)self::$role2as[$role];
1118
			if (is_numeric($uid))
1119
			{
1120
				$attendee->name = $GLOBALS['egw']->accounts->id2name($uid,'account_fullname');
1121
				$attendee->email = $GLOBALS['egw']->accounts->id2name($uid, 'account_email', true);
1122
			}
1123
			else
1124
			{
1125
				list($info) = $i = $this->calendar->resources[$uid[0]]['info'] ?
0 ignored issues
show
Unused Code introduced by
The assignment to $i is dead and can be removed.
Loading history...
1126
					ExecMethod($this->calendar->resources[$uid[0]]['info'],substr($uid,1)) : array(false);
0 ignored issues
show
Deprecated Code introduced by
The function ExecMethod() has been deprecated: use autoloadable class-names, instanciate and call method or use static methods ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

1126
					/** @scrutinizer ignore-deprecated */ ExecMethod($this->calendar->resources[$uid[0]]['info'],substr($uid,1)) : array(false);

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
1127
1128
				if (!$info) continue;
1129
1130
				if (!$info['email'] && $info['responsible'])
1131
				{
1132
					$info['email'] = $GLOBALS['egw']->accounts->id2name($info['responsible'], 'account_email', true);
1133
				}
1134
				$attendee->name = empty($info['cn']) ? $info['name'] : $info['cn'];
1135
				$attendee->email = $info['email'];
1136
1137
				// external organizer: make him AS organizer, to get correct notifications
1138
				if ($role == 'CHAIR' && $uid[0] == 'e' && !empty($attendee->email))
1139
				{
1140
					$message->organizername  = $attendee->name;
1141
					$message->organizeremail = $attendee->email;
1142
					ZLog::Write(LOGLEVEL_DEBUG, __METHOD__."($folderid, $id, ...) external organizer detected (role=$role, uid=$uid), set as AS organizer: $message->organizername <$message->organizeremail>");
1143
				}
1144
				if ($uid[0] == 'r') $attendee->type = 3;	// 3 = resource
0 ignored issues
show
Bug introduced by
The property type does not seem to exist on SyncAttendee.
Loading history...
1145
			}
1146
			// email must NOT be empty, but MAY be an arbitrary text
1147
			if (empty($attendee->email)) $attendee->email = 'noreply-'.$uid.'[email protected]';
1148
1149
			$message->attendees[] = $attendee;
1150
		}
1151
		$message->categories = array();
1152
		foreach($event['category'] ? explode(',',$event['category']) : array() as $cat_id)
1153
		{
1154
			$message->categories[] = Api\Categories::id2name($cat_id);
1155
		}
1156
1157
		// recurring information, only if not a single recurrence eg. virtual exception (!$recur_date)
1158
		if ($event['recur_type'] != calendar_rrule::NONE && !$recur_date)
1159
		{
1160
			$message->recurrence = $recurrence = new SyncRecurrence();
1161
			$rrule = calendar_rrule::event2rrule($event,false);	// false = timestamps in $event are servertime
1162
			$recurrence->type = (int)self::$recur_type2as[$rrule->type];
1163
			$recurrence->interval = $rrule->interval;
1164
			switch ($rrule->type)
1165
			{
1166
				case calendar_rrule::MONTHLY_WDAY:
1167
					$recurrence->weekofmonth = $rrule->monthly_byday_num >= 1 ?
1168
						$rrule->monthly_byday_num : 5;	// 1..5=last week of month, not -1
1169
					// fall throught
1170
				case calendar_rrule::WEEKLY:
1171
					$recurrence->dayofweek = $rrule->weekdays;	// 1=Su, 2=Mo, 4=Tu, .., 64=Sa
1172
					break;
1173
				case calendar_rrule::MONTHLY_MDAY:
1174
					$recurrence->dayofmonth = $rrule->monthly_bymonthday >= 1 ?	// 1..31
1175
						$rrule->monthly_bymonthday : 31;	// not -1 for last day of month!
1176
					break;
1177
				case calendar_rrule::YEARLY:
1178
					$recurrence->dayofmonth = (int)$rrule->time->format('d');	// 1..31
1179
					$recurrence->monthofyear = (int)$rrule->time->format('m');	// 1..12
1180
					break;
1181
			}
1182
			if ($rrule->enddate)	// enddate is only a date, but AS needs a time incl. correct starttime!
1183
			{
1184
				$enddate = clone $rrule->time;
1185
				$enddate->setDate($rrule->enddate->format('Y'), $rrule->enddate->format('m'),
1186
					$rrule->enddate->format('d'));
1187
				$recurrence->until = $enddate->format('server');
1188
			}
1189
1190
			if ($rrule->exceptions)
0 ignored issues
show
Bug Best Practice introduced by
The expression $rrule->exceptions of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
1191
			{
1192
				// search real / non-virtual exceptions
1193
				if (!empty($event['uid']))
1194
				{
1195
					$ex_events =& $this->calendar->search(array(
1196
						'query' => array('cal_uid' => $event['uid']),
1197
						'filter' => 'owner',  // return all possible entries
1198
						'daywise' => false,
1199
						'date_format' => 'server',
1200
					));
1201
				}
1202
				else
1203
				{
1204
					ZLog::Write(LOGLEVEL_DEBUG, __METHOD__.__LINE__." Exceptions found but no UID given for Event:".$event['id'].' Exceptions:'.array2string($event['recur_exception']));
1205
				}
1206
				if (count($ex_events)>=1) ZLog::Write(LOGLEVEL_DEBUG, __METHOD__.__LINE__." found ".count($ex_events)." exeptions for event with UID/ID:".$event['uid'].'/'.$event['id']);
1207
1208
				$message->exceptions = array();
1209
				foreach($ex_events as $ex_event)
1210
				{
1211
					if ($ex_event['id'] == $event['id']) continue;	// ignore series master
1212
					$exception = $this->GetMessage($folderid, $ex_event, $contentparameters, 'SyncAppointmentException');
1213
					$exception->exceptionstarttime = $exception_time = $ex_event['recurrence'];
1214
					foreach(array('attendees','recurrence','uid','timezone','organizername','organizeremail') as $not_supported)
1215
					{
1216
						$exception->$not_supported = null;	// not allowed in exceptions :-(
1217
					}
1218
					$exception->deleted = 0;
1219
					if (($key = array_search($exception_time,$event['recur_exception'])) !== false)
1220
					{
1221
						unset($event['recur_exception'][$key]);
1222
					}
1223
					ZLog::Write(LOGLEVEL_DEBUG, __METHOD__."() added exception ".date('Y-m-d H:i:s',$exception_time).' '.array2string($exception));
1224
					$message->exceptions[] = $exception;
1225
				}
1226
				// add rest of exceptions as deleted
1227
				foreach($event['recur_exception'] as $exception_time)
1228
				{
1229
					if (!empty($exception_time))
1230
					{
1231
						if (empty($event['uid'])) ZLog::Write(LOGLEVEL_DEBUG, __METHOD__.__LINE__." BEWARE no UID given for this event:".$event['id'].' but exception is set for '.$exception_time);
1232
						$exception = new SyncAppointmentException();	// exceptions seems to be full SyncAppointments, with only starttime required
1233
						$exception->deleted = 1;
1234
						$exception->exceptionstarttime = $exception_time;
1235
						ZLog::Write(LOGLEVEL_DEBUG, __METHOD__."() added deleted exception ".date('Y-m-d H:i:s',$exception_time).' '.array2string($exception));
1236
						$message->exceptions[] = $exception;
1237
					}
1238
				}
1239
			}
1240
			/* disabled virtual exceptions for now, as AS does NOT support changed participants or status
1241
			// add virtual exceptions here too (get_recurrence_exceptions should be able to return real-exceptions too!)
1242
			foreach($this->calendar->so->get_recurrence_exceptions($event,
1243
				Api\DateTime::$server_timezone->getName(), $cutoffdate, 0, 'all') as $exception_time)
1244
			{
1245
				// exceptions seems to be full SyncAppointments, with only exceptionstarttime required
1246
				$exception = $this->GetMessage($folderid, $event['id'].':'.$exception_time, $contentparameters);
1247
				$exception->deleted = 0;
1248
				$exception->exceptionstarttime = $exception_time;
1249
				ZLog::Write(LOGLEVEL_DEBUG, __METHOD__."() added virtual exception ".date('Y-m-d H:i:s',$exception_time).' '.array2string($exception));
1250
				$message->exceptions[] = $exception;
1251
			}*/
1252
			//ZLog::Write(LOGLEVEL_DEBUG, __METHOD__."($id) message->exceptions=".array2string($message->exceptions));
1253
		}
1254
		// only return alarms if in own calendar
1255
		if ($account == $GLOBALS['egw_info']['user']['account_id'] && $event['alarm'])
1256
		{
1257
			foreach($event['alarm'] as $alarm)
1258
			{
1259
				if ($alarm['all'] || $alarm['owner'] == $account)
1260
				{
1261
					$message->reminder = $alarm['offset']/60;	// is in minutes, not seconds as in EGw
1262
					break;	// AS supports only one alarm! (we use the next/earliest one)
1263
				}
1264
			}
1265
		}
1266
		//$message->meetingstatus;
1267
1268
		return $message;
1269
	}
1270
1271
	/**
1272
	 * StatMessage should return message stats, analogous to the folder stats (StatFolder). Entries are:
1273
	 * 'id'	 => Server unique identifier for the message. Again, try to keep this short (under 20 chars)
1274
	 * 'flags'	 => simply '0' for unread, '1' for read
1275
	 * 'mod'	=> modification signature. As soon as this signature changes, the item is assumed to be completely
1276
	 *			 changed, and will be sent to the PDA as a whole. Normally you can use something like the modification
1277
	 *			 time for this field, which will change as soon as the contents have changed.
1278
	 *
1279
	 * @param string $folderid
1280
	 * @param int|array $id event id or array or cal_id:recur_date for virtual exception
1281
	 * @return array
1282
	 */
1283
	public function StatMessage($folderid, $id)
1284
	{
1285
		unset($folderid);	// not used ($id is unique), but required by function signature
1286
1287
		if (!isset($this->calendar)) $this->calendar = new calendar_boupdate();
1288
1289
		$nul = null;
1290
		if (!($etag = $this->calendar->get_etag($id, $nul, true, true)))	// last true: $only_master=true
0 ignored issues
show
Unused Code introduced by
The call to calendar_bo::get_etag() has too many arguments starting with true. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

1290
		if (!($etag = $this->calendar->/** @scrutinizer ignore-call */ get_etag($id, $nul, true, true)))	// last true: $only_master=true

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
1291
		{
1292
			$stat = false;
1293
			// error_log why access is denied (should never happen for everything returned by calendar_bo::search)
1294
			$backup = $this->calendar->debug;
1295
			//$this->calendar->debug = 2;
1296
			list($id) = explode(':',$id);
1297
			$this->calendar->check_perms(calendar_bo::ACL_FREEBUSY, $id, 0, 'server');
1298
			$this->calendar->debug = $backup;
1299
		}
1300
		else
1301
		{
1302
			$stat = array(
1303
				'mod' => $etag,
1304
				'id' => is_array($id) ? $id['id'] : $id,
1305
				'flags' => 1,
1306
			);
1307
		}
1308
		//ZLog::Write(LOGLEVEL_DEBUG, __METHOD__."('$folderid',".array2string(is_array($id) ? $id['id'] : $id).") returning ".array2string($stat));
1309
1310
		return $stat;
1311
	}
1312
1313
	/**
1314
	 * Return a changes array
1315
	 *
1316
	 * if changes occurr default diff engine computes the actual changes
1317
	 *
1318
	 * @param string $folderid
1319
	 * @param string &$syncstate on return new syncstate
1320
	 */
1321
	function AlterPingChanges($folderid, &$syncstate)
1322
	{
1323
		$type = $owner = null;
1324
		$this->backend->splitID($folderid, $type, $owner);
1325
1326
		if ($type != 'calendar') return false;
0 ignored issues
show
introduced by
The condition $type != 'calendar' is always true.
Loading history...
1327
1328
		if (!isset($this->calendar)) $this->calendar = new calendar_boupdate();
1329
		//$ctag = $this->calendar->get_ctag($owner,'owner',true);	// true only consider recurrence master
1330
		$syncstate = $this->calendar->get_ctag($owner,false,true); // we only want to fetch the owners events, where he is a participant too
1331
		// workaround for syncstate = 0 when calendar is empty causes synctate to not return 0 but array resulting in foldersync loop
1332
		if ($syncstate == 0) $syncstate = 1;
1333
1334
		ZLog::Write(LOGLEVEL_DEBUG, __METHOD__."('$folderid', ...) type='$type', owner=$owner --> syncstate='$syncstate'");
1335
	}
1336
1337
	/**
1338
	 * AS Timezone blob for UTC
1339
	 */
1340
	const UTC_BLOB = 'AAAAAAAoAEcATQBUACkAIABHAHIAZQBlAG4AdwBpAGMAaAAgAE0AZQBhAG4AIABUAGkAbQBlADoAIABEAHUAYgBsAGkAAAoAAAAFAAIAAAAAAAAAAAAAAAAoAEcATQBUACkAIABHAHIAZQBlAG4AdwBpAGMAaAAgAE0AZQBhAG4AIABUAGkAbQBlADoAIABEAHUAYgBsAGkAAAMAAAAFAAEAAAAAAAAAxP///w==';
1341
1342
	/**
1343
	 * Return AS timezone data from given timezone and time
1344
	 *
1345
	 * AS spezifies the timezone by the date it changes to dst and back and the offsets.
1346
	 * Unfortunately this data is not available from PHP's DateTime(Zone) class.
1347
	 * Just given the exact time of the next transition, which is available via DateTimeZone::getTransistions(),
1348
	 * will fail for recurring events longer then a year, as the transition date/time changes!
1349
	 *
1350
	 * We use now the RRule given in the iCal timezone defintion available via calendar_timezones::tz2id($tz,'component').
1351
	 *
1352
	 * Not every timezone uses DST, in which case only bias matters and dstbias=0
1353
	 * (probably all other values should be 0, as MapiMapping::_getGMTTZ() in backend/ics.php does it).
1354
	 *
1355
	 * For southern hermisphere DST in southern winter (eg. January), Active Sync implementation of iPhone
1356
	 * uses a negative dstbias (eg. -60) and an accordingly moved start- and end-time.
1357
	 * For Pacific/Auckland TZ iPhone AS implementation uses -720=-12h instead of 720=+12h.
1358
	 * Both are corrected now in our Active Sync timezone generation, as we can not find
1359
	 * matching timezones for incomming timezone data. iPhone seems not to care on receiving about the above.
1360
	 *
1361
	 * @param string|DateTimeZone $tz timezone, timezone name (eg. "Europe/Berlin") or ical with VTIMEZONE
1362
	 * @return array with values for keys:
1363
	 * - "bias": timezone offset from UTC in minutes for NO DST
1364
	 * - "dstendmonth", "dstendday", "dstendweek", "dstendhour", "dstendminute", "dstendsecond", "dstendmillis"
1365
	 * - "stdbias": seems not to be used
1366
	 * - "dststartmonth", "dststartday", "dststartweek", "dststarthour", "dststartminute", "dststartsecond", "dststartmillis"
1367
	 * - "dstbias": offset in minutes for no DST --> DST, usually 60 or 0 for no DST
1368
	 *
1369
	 * @link http://download.microsoft.com/download/5/D/D/5DD33FDF-91F5-496D-9884-0A0B0EE698BB/%5BMS-ASDTYPE%5D.pdf
1370
	 * @throws Api\Exception\AssertionFailed if no vtimezone data found for given timezone
1371
	 */
1372
	static public function tz2as($tz)
1373
	{
1374
/*
1375
BEGIN:VTIMEZONE
1376
TZID:Europe/Berlin
1377
X-LIC-LOCATION:Europe/Berlin
1378
BEGIN:DAYLIGHT
1379
TZOFFSETFROM:+0100
1380
--> bias: -60 min
1381
TZOFFSETTO:+0200
1382
--> dstbias: +1000 - +0200 = +0100 = -60 min
1383
TZNAME:CEST
1384
DTSTART:19700329T020000
1385
RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=3
1386
--> dststart: month: 3, day: SU(0???), week: -1|5, hour: 2, minute, second, millis: 0
1387
END:DAYLIGHT
1388
BEGIN:STANDARD
1389
TZOFFSETFROM:+0200
1390
TZOFFSETTO:+0100
1391
TZNAME:CET
1392
DTSTART:19701025T030000
1393
RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10
1394
--> dstend: month: 10, day: SU(0???), week: -1|5, hour: 3, minute, second, millis: 0
1395
END:STANDARD
1396
END:VTIMEZONE
1397
*/
1398
		$data = array(
1399
			'bias' => 0,
1400
			'stdbias' => 0,
1401
			'dstbias' => 0,
1402
			'dststartyear' => 0, 'dststartmonth' => 0, 'dststartday' => 0, 'dststartweek' => 0,
1403
			'dststarthour' => 0, 'dststartminute' => 0, 'dststartsecond' => 0, 'dststartmillis' => 0,
1404
			'dstendyear' => 0, 'dstendmonth' => 0, 'dstendday' => 0, 'dstendweek' => 0,
1405
			'dstendhour' => 0, 'dstendminute' => 0, 'dstendsecond' => 0, 'dstendmillis' => 0,
1406
		);
1407
1408
		if ($tz === 'UTC') return $data;
1409
1410
		$name = $component = is_a($tz,'DateTimeZone') ? $tz->getName() : $tz;
1411
		if (strpos($component, 'VTIMEZONE') === false) $component = calendar_timezones::tz2id($name,'component');
1412
		// parse ical timezone defintion
1413
		$ical = self::ical2array($component);
1414
		$standard = $ical['VTIMEZONE']['STANDARD'];
1415
		$daylight = $ical['VTIMEZONE']['DAYLIGHT'];
1416
1417
		if (!isset($standard))
1418
		{
1419
			$matches = null;
1420
			if (preg_match('/^etc\/gmt([+-])([0-9]+)$/i',$name,$matches))
1421
			{
1422
				$standard = array(
1423
					'TZOFFSETTO'   => sprintf('%s%02d00',$matches[1],$matches[2]),
1424
					'TZOFFSETFROM' => sprintf('%s%02d00',$matches[1],$matches[2]),
1425
				);
1426
				unset($daylight);
1427
			}
1428
			else
1429
			{
1430
				throw new Api\Exception\AssertionFailed("NO standard component for '$name' in '$component'!");
1431
			}
1432
		}
1433
		// get bias and dstbias from standard component, which is present in all tz's
1434
		// (dstbias is relative to bias and almost always 60 or 0)
1435
		$data['bias'] = -(60 * substr($standard['TZOFFSETTO'],0,-2) + substr($standard['TZOFFSETTO'],-2));
1436
		$data['dstbias'] = -(60 * substr($standard['TZOFFSETFROM'],0,-2) + substr($standard['TZOFFSETFROM'],-2) + $data['bias']);
1437
1438
		// check if we have an additional DAYLIGHT component and both have a RRULE component --> tz uses daylight saving
1439
		if (isset($standard['RRULE']) && isset($daylight) && isset($daylight['RRULE']))
1440
		{
1441
			foreach(array('dststart' => $daylight,'dstend' => $standard) as $prefix => $comp)
1442
			{
1443
				// fix RRULE order
1444
				$comp['RRULE'] = preg_replace('/FREQ=YEARLY;BYMONTH=(\d+);BYDAY=(.*)/',
1445
					'FREQ=YEARLY;BYDAY=$2;BYMONTH=$1', $comp['RRULE']);
1446
1447
				if (preg_match('/FREQ=YEARLY;BYDAY=(.*);BYMONTH=(\d+)/',$comp['RRULE'],$matches))
1448
				{
1449
					$data[$prefix.'month'] = (int)$matches[2];
1450
					$data[$prefix.'week'] = (int)$matches[1];
1451
					// -1 for last week might be 5 for as as in recuring events definition
1452
					// seems for start 1SU is always returned with week=5, like -1SU
1453
					if ($data[$prefix.'week'] < 0 || $prefix == 'dststart' && $matches[1] == '1SU')
1454
					{
1455
						$data[$prefix.'week'] = 5;
1456
					}
1457
					// if both start and end use 1SU use week=5 and decrement month
1458
					if ($prefix == 'dststart') $start_byday = $matches[1];
1459
					if ($prefix == 'dstend' && $matches[1] == '1SU' && $start_byday == '1SU')
1460
					{
1461
						$data[$prefix.'week'] = 5;
1462
						if ($prefix == 'dstend') $data[$prefix.'month'] -= 1;
1463
					}
1464
					static $day2int = array('SU'=>0,'MO'=>1,'TU'=>2,'WE'=>3,'TH'=>4,'FR'=>5,'SA'=>6);
1465
					$data[$prefix.'day'] = (int)$day2int[substr($matches[1],-2)];
1466
				}
1467
				if (preg_match('/^\d{8}T(\d{6})$/',$comp['DTSTART'],$matches))
1468
				{
1469
					$data[$prefix.'hour'] = (int)substr($matches[1],0,2)+($prefix=='dststart'?-1:1)*$data['dstbias']/60;
1470
					$data[$prefix.'minute'] = (int)substr($matches[1],2,2)+($prefix=='dststart'?-1:1)*$data['dstbias']%60;
1471
					$data[$prefix.'second'] = (int)substr($matches[1],4,2);
1472
				}
1473
			}
1474
			// for southern hermisphere, were DST is in January, we have to swap start- and end-hour/-minute
1475
			if ($data['dststartmonth'] > $data['dstendmonth'])
1476
			{
1477
				$start = $data['dststarthour'];   $data['dststarthour'] = $data['dstendhour'];     $data['dstendhour'] = $start;
1478
				$end = $data['dststartminute']; $data['dststartminute'] = $data['dstendminute']; $data['dstendminute'] = $end;
1479
			}
1480
		}
1481
		//error_log(__METHOD__."('$name') returning ".array2string($data));
1482
		return $data;
1483
	}
1484
1485
	/**
1486
	 * Simple iCal parser:
1487
	 *
1488
	 * BEGIN:VTIMEZONE
1489
	 * TZID:Europe/Berlin
1490
	 * X-LIC-LOCATION:Europe/Berlin
1491
	 * BEGIN:DAYLIGHT
1492
	 * TZOFFSETFROM:+0100
1493
	 * TZOFFSETTO:+0200
1494
	 * TZNAME:CEST
1495
	 * DTSTART:19700329T020000
1496
	 * RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=3
1497
	 * END:DAYLIGHT
1498
	 * BEGIN:STANDARD
1499
	 * TZOFFSETFROM:+0200
1500
	 * TZOFFSETTO:+0100
1501
	 * TZNAME:CET
1502
	 * DTSTART:19701025T030000
1503
	 * RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10
1504
	 * END:STANDARD
1505
	 * END:VTIMEZONE
1506
	 *
1507
	 * Array
1508
	 * (
1509
	 *	 [VTIMEZONE] => Array
1510
	 *		 (
1511
	 *			 [TZID] => Europe/Berlin
1512
	 *			 [X-LIC-LOCATION] => Europe/Berlin
1513
	 *			 [DAYLIGHT] => Array
1514
	 *				 (
1515
	 *					 [TZOFFSETFROM] => +0100
1516
	 *					 [TZOFFSETTO] => +0200
1517
	 *					 [TZNAME] => CEST
1518
	 *					 [DTSTART] => 19700329T020000
1519
	 *					 [RRULE] => FREQ=YEARLY;BYDAY=-1SU;BYMONTH=3
1520
	 *				 )
1521
	 *			 [STANDARD] => Array
1522
	 *				 (
1523
	 *					 [TZOFFSETFROM] => +0200
1524
	 *					 [TZOFFSETTO] => +0100
1525
	 *					 [TZNAME] => CET
1526
	 *					 [DTSTART] => 19701025T030000
1527
	 *					 [RRULE] => FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10
1528
	 *				 )
1529
	 *		 )
1530
	 * )
1531
	 *
1532
	 * @param string|array $ical lines of ical file
1533
	 * @return array with parsed ical components
1534
	 */
1535
	static public function ical2array(&$ical,$section=null)
1536
	{
1537
		$arr = array();
1538
		if (!is_array($ical)) $ical = preg_split("/[\r\n]+/m", $ical);
1539
		while (($line = array_shift($ical)))
1540
		{
1541
			list($name,$value) = explode(':',$line,2);
1542
			if ($name == 'BEGIN')
1543
			{
1544
				$arr[$value] = self::ical2array($ical,$value);
1545
			}
1546
			elseif($name == 'END')
1547
			{
1548
				if ($section && $section==$value) return $arr;
1549
				break;
1550
			}
1551
			else
1552
			{
1553
				$arr[$name] = $value;
1554
			}
1555
		}
1556
		return $arr;
1557
	}
1558
1559
	/**
1560
	 * Get timezone from AS timezone data
1561
	 *
1562
	 * Here we can only loop through all available timezones (starting with the users timezone) and
1563
	 * try to find a timezone matching the change data and offsets specified in $data.
1564
	 * This conversation is not unique, as multiple timezones can match the given data or none!
1565
	 *
1566
	 * @param array $data
1567
	 * @return string timezone name, eg. "Europe/Berlin" or 'UTC' if NO matching timezone found
1568
	 */
1569
	public static function as2tz(array $data)
1570
	{
1571
		static $cache=null;	// some caching withing the request
1572
1573
		unset($data['name']);	// not used, but can stall the match
1574
1575
		$key = serialize($data);
1576
1577
		for($n = 0; !isset($cache[$key]); ++$n)
1578
		{
1579
			if (!$n)	// check users timezone first
1580
			{
1581
				$tz = Api\DateTime::$user_timezone->getName();
1582
			}
1583
			elseif (!($tz = calendar_timezones::id2tz($n)))	// no further timezones to check
1584
			{
1585
				$cache[$key] = 'UTC';
1586
				error_log(__METHOD__.'('.array2string($data).') NO matching timezone found --> using UTC now!');
1587
				break;
1588
			}
1589
			try {
1590
				if (self::tz2as($tz) == $data)
1591
				{
1592
					$cache[$key] = $tz;
1593
					break;
1594
				}
1595
			}
1596
			catch(Exception $e) {
1597
				unset($e);
1598
				// simpy ignore that, as it only means $tz can NOT be converted, because it has no VTIMEZONE component
1599
			}
1600
		}
1601
		return $cache[$key];
1602
	}
1603
1604
	/**
1605
	 * Unpack timezone info from Sync
1606
	 *
1607
	 * copied from backend/ics.php
1608
	 */
1609
	static public function _getTZFromSyncBlob($data)
1610
	{
1611
		$tz = unpack(	"lbias/a64name/vdstendyear/vdstendmonth/vdstendday/vdstendweek/vdstendhour/vdstendminute/vdstendsecond/vdstendmillis/" .
1612
						"lstdbias/a64name/vdststartyear/vdststartmonth/vdststartday/vdststartweek/vdststarthour/vdststartminute/vdststartsecond/vdststartmillis/" .
1613
						"ldstbias", $data);
1614
1615
		return $tz;
1616
	}
1617
1618
	/**
1619
	 * Pack timezone info for Sync
1620
	 *
1621
	 * copied from backend/ics.php
1622
	 */
1623
	static public function _getSyncBlobFromTZ($tz)
1624
	{
1625
		$packed = pack("la64vvvvvvvv" . "la64vvvvvvvv" . "l",
1626
				$tz["bias"], "", 0, $tz["dstendmonth"], $tz["dstendday"], $tz["dstendweek"], $tz["dstendhour"], $tz["dstendminute"], $tz["dstendsecond"], $tz["dstendmillis"],
1627
				$tz["stdbias"], "", 0, $tz["dststartmonth"], $tz["dststartday"], $tz["dststartweek"], $tz["dststarthour"], $tz["dststartminute"], $tz["dststartsecond"], $tz["dststartmillis"],
1628
				$tz["dstbias"]);
1629
1630
		return $packed;
1631
	}
1632
1633
	/**
1634
	 * Populates $settings for the preferences
1635
	 *
1636
	 * @param array|string $hook_data
1637
	 * @return array
1638
	 */
1639
	function egw_settings($hook_data)
1640
	{
1641
		$cals = array();
1642
		if (!$hook_data['setup'] && in_array($hook_data['type'], array('user', 'group')))
1643
		{
1644
			foreach (calendar_bo::list_calendars($hook_data['account_id']) as $entry)
1645
			{
1646
				$account_id = $entry['grantor'];
1647
				$cals[$account_id] = $entry['name'];
1648
			}
1649
			if ($hook_data['account_id'] > 0) unset($cals[$hook_data['account_id']]);	// skip current user
1650
		}
1651
		$cals['G'] = lang('Primary group');
1652
		$cals['A'] = lang('All');
1653
		// allow to force "none", to not show the prefs to the users
1654
		if ($GLOBALS['type'] == 'forced')
1655
		{
1656
			$cals['N'] = lang('None');
1657
		}
1658
		$settings['calendar-cals'] = array(
0 ignored issues
show
Comprehensibility Best Practice introduced by
$settings was never initialized. Although not strictly required by PHP, it is generally a good practice to add $settings = array(); before regardless.
Loading history...
1659
			'type'   => 'multiselect',
1660
			'label'  => 'Additional calendars to sync',
1661
			'help'   => 'Not all devices support additonal calendars. Your personal calendar is always synchronised.',
1662
			'name'   => 'calendar-cals',
1663
			'values' => $cals,
1664
			'xmlrpc' => True,
1665
			'admin'  => False,
1666
		);
1667
1668
		return $settings;
1669
	}
1670
}
1671
1672
    /**
1673
 * Testcode for active sync timezone stuff
1674
 *
1675
 * You need to comment implements activesync_plugin_write
1676
 */
1677
if (isset($_SERVER['SCRIPT_FILENAME']) && realpath($_SERVER['SCRIPT_FILENAME']) == __FILE__)	// some tests
1678
{
1679
	$GLOBALS['egw_info'] = array(
1680
		'flags' => array(
1681
			'currentapp' => 'login'
1682
		)
1683
	);
1684
	require_once('../../header.inc.php');
1685
	ini_set('display_errors',1);
1686
	error_reporting(E_ALL & ~E_NOTICE);
1687
1688
	echo "<html><head><title>Conversation of ActiveSync Timezone Blobs to TZID's</title></head>\n<body>\n";
1689
	echo "<h3>Conversation of ActiveSync Timezone Blobs to TZID's</h3>\n";
1690
	echo "<table border='1'>\n<tbody>\n";
1691
	echo "<tr><th>TZID</th><th>bias</th><th>dstbias</th></th><th>dststart</th><th>dstend</th><th>matched TZID</th></tr>\n";
1692
	echo "<script>
1693
	function toggle_display(pre)
1694
	{
1695
		pre.style.display = pre.style.display && pre.style.display == 'none' ? 'block' : 'none';
1696
	}
1697
	</script>\n";
1698
1699
	// TZID => AS timezone blobs reported by various devices
1700
	foreach(array(
1701
		// Exchange 2016 Europe/Berlin
1702
		'Europe/Berlin'    => 'xP///1cALgAgAEUAdQByAG8AcABlACAAUwB0AGEAbgBkAGEAcgBkACAAVABpAG0AZQAAAAAAAAAAAAAAAAAAAAAAAAAAAAoAAAAFAAMAAAAAAAAAAAAAACgAVQBUAEMAKwAwADEAOgAwADAAKQAgAEEAbQBzAHQAZQByAGQAYQBtACwAIABCAGUAcgBsAGkAbgAsACAAQgAAAAMAAAAFAAIAAAAAAAAAxP///w==',
1703
		//'Europe/Berlin'    => 'xP///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAoAAAAFAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAFAAMAAAAAAAAAxP///w==',
1704
		'Europe/Helsinki'  => 'iP///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAoAAAAFAAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAFAAQAAAAAAAAAxP///w==',
1705
		'Asia/Tokyo'       => '5P3//wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAxP///w==',
1706
		'Atlantic/Azores'  => 'PAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAoAAAAFAAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAFAAIAAAAAAAAAxP///w==',
1707
		'America/Los_Angeles' => '4AEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAsAAAABAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAACAAMAAAAAAAAAxP///w==',
1708
		'America/New_York' => 'LAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAsAAAABAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAACAAMAAAAAAAAAxP///w==',
1709
		'Pacific/Auckland' => 'MP3//wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAABAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAkAAAAFAAMAAAAAAAAAxP///w==',
1710
		'Australia/Sydney' => 'qP3//wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAFAAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAoAAAAFAAIAAAAAAAAAxP///w==',
1711
		'Etc/GMT+3'        => 'TP///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==',
1712
		'UTC'              => calendar_zpush::UTC_BLOB,
1713
	) as $tz => $sync_blob)
1714
	{
1715
		// get as timezone data for a given timezone
1716
		$ical = calendar_timezones::tz2id($tz,'component');
1717
		//echo "<pre>".print_r($ical,true)."</pre>\n";
1718
		$ical_tz = $ical;
1719
		$ical_arr = calendar_zpush::ical2array($ical_tz);
1720
		//echo "<pre>".print_r($ical_arr,true)."</pre>\n";
1721
		$as_tz = calendar_zpush::tz2as($tz);
1722
		//echo "$tz=<pre>".print_r($as_tz,true)."</pre>\n";
1723
1724
		$as_tz_org = calendar_zpush::_getTZFromSyncBlob(base64_decode($sync_blob));
1725
		echo "sync_blob=<pre>".print_r($as_tz_org,true)."</pre>\n";
1726
1727
		// find matching timezone from as data
1728
		// this returns the FIRST match, which is in case of Pacific/Auckland eg. Antarctica/McMurdo ;-)
1729
		$matched = calendar_zpush::as2tz($as_tz);
1730
		//echo array2string($matched);
1731
1732
		echo "<tr><td><b onclick='toggle_display(this.nextSibling);' style='cursor:pointer;'>$tz</b><pre style='margin:0; font-size: 90%; display:none;'>$ical</pre></td><td>$as_tz_org[bias]<br/>$as_tz[bias]</td><td>$as_tz_org[dstbias]<br/>$as_tz[dstbias]</td>\n";
1733
		foreach(array('dststart','dstend') as $prefix)
1734
		{
1735
			echo "<td>\n";
1736
			foreach(array($as_tz_org,$as_tz) as $n => $arr)
1737
			{
1738
				$parts = array();
1739
				foreach(array('year','month','day','week','hour','minute','second') as $postfix)
1740
				{
1741
					$failed = $n && $as_tz_org[$prefix.$postfix] !== $as_tz[$prefix.$postfix];
1742
					$parts[] = ($failed?'<font color="red">':'').
1743
						"<span title='$postfix'>".$arr[$prefix.$postfix].'</span>'.
1744
						($failed?'</font>':'');
1745
				}
1746
				echo implode(' ', $parts).(!$n?'<br/>':'');
1747
			}
1748
			echo "</td>\n";
1749
		}
1750
		echo "<td>&nbsp;<br/>".($matched=='UTC'?'<font color="red">':'').$matched.($matched=='UTC'?'</font>':'')."</td></tr>\n";
1751
	}
1752
	echo "</tbody></table>\n";
1753
	echo "</body></html>\n";
1754
}
1755