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) |
|
|
|
|
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( |
|
|
|
|
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 |
|
|
|
|
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) |
|
|
|
|
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(); |
|
|
|
|
327
|
|
|
$message->airsyncbasenativebodytype=1; |
|
|
|
|
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'))) |
|
|
|
|
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'); |
|
|
|
|
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 |
|
|
|
|
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( |
|
|
|
|
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 |
|
|
|
|
921
|
|
|
* @param $id of the message |
922
|
|
|
* @param $newfolderid |
923
|
|
|
* @param ContentParameters $contentParameters |
924
|
|
|
* |
925
|
|
|
* @return $newid as a string | boolean false on error |
|
|
|
|
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(); |
|
|
|
|
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))) |
|
|
|
|
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'] ? |
|
|
|
|
1126
|
|
|
ExecMethod($this->calendar->resources[$uid[0]]['info'],substr($uid,1)) : array(false); |
|
|
|
|
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 |
|
|
|
|
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) |
|
|
|
|
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 |
|
|
|
|
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; |
|
|
|
|
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( |
|
|
|
|
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> <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
|
|
|
|
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:For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths