Test Failed
Push — master ( 647c72...cd42b5 )
by
unknown
10:25
created

Recurrence::getCalendarItems()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 5
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
<?php
2
3
	require_once(BASE_PATH . 'server/includes/mapi/class.baserecurrence.php');
4
5
	/**
6
	 * Recurrence
7
	 */
8
	class Recurrence extends BaseRecurrence
9
	{
10
		/*
11
		 * ABOUT TIMEZONES
12
		 *
13
		 * Timezones are rather complicated here so here are some rules to think about:
14
		 *
15
		 * - Timestamps in mapi-like properties (so in PT_SYSTIME properties) are always in GMT (including
16
		 *   the 'basedate' property in exceptions !!)
17
		 * - Timestamps for recurrence (so start/end of recurrence, and basedates for exceptions (everything
18
		 *   outside the 'basedate' property in the exception !!), and start/endtimes for exceptions) are
19
		 *   always in LOCAL time.
20
		 */
21
22
		// All properties for a recipient that are interesting
23
		var $recipprops = Array(
24
			PR_ENTRYID,
25
			PR_SEARCH_KEY,
26
			PR_DISPLAY_NAME,
27
			PR_EMAIL_ADDRESS,
28
			PR_RECIPIENT_ENTRYID,
29
			PR_RECIPIENT_TYPE,
30
			PR_SEND_INTERNET_ENCODING,
31
			PR_SEND_RICH_INFO,
32
			PR_RECIPIENT_DISPLAY_NAME,
33
			PR_ADDRTYPE,
34
			PR_DISPLAY_TYPE,
35
			PR_DISPLAY_TYPE_EX,
36
			PR_RECIPIENT_TRACKSTATUS,
37
			PR_RECIPIENT_TRACKSTATUS_TIME,
38
			PR_RECIPIENT_FLAGS,
39
			PR_ROWID
40
		);
41
42
		/**
43
		 * Constructor
44
		 * @param resource $store MAPI Message Store Object
45
		 * @param resource $message the MAPI (appointment) message
46
		 * @param array    $proptags an associative array of protags and their values.
47
		 */
48
		function __construct($store, $message, $proptags = [])
49
		{
50
51
			if ($proptags) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $proptags 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...
52
				$this->proptags = $proptags;
53
			} else {
54
				$properties = array();
55
				$properties["entryid"] = PR_ENTRYID;
56
				$properties["parent_entryid"] = PR_PARENT_ENTRYID;
57
				$properties["message_class"] = PR_MESSAGE_CLASS;
58
				$properties["icon_index"] = PR_ICON_INDEX;
59
				$properties["subject"] = PR_SUBJECT;
60
				$properties["display_to"] = PR_DISPLAY_TO;
61
				$properties["importance"] = PR_IMPORTANCE;
62
				$properties["sensitivity"] = PR_SENSITIVITY;
63
				$properties["startdate"] = "PT_SYSTIME:PSETID_Appointment:0x820d";
64
				$properties["duedate"] = "PT_SYSTIME:PSETID_Appointment:0x820e";
65
				$properties["recurring"] = "PT_BOOLEAN:PSETID_Appointment:0x8223";
66
				$properties["recurring_data"] = "PT_BINARY:PSETID_Appointment:0x8216";
67
				$properties["busystatus"] = "PT_LONG:PSETID_Appointment:0x8205";
68
				$properties["label"] = "PT_LONG:PSETID_Appointment:0x8214";
69
				$properties["alldayevent"] = "PT_BOOLEAN:PSETID_Appointment:0x8215";
70
				$properties["private"] = "PT_BOOLEAN:PSETID_Common:0x8506";
71
				$properties["meeting"] = "PT_LONG:PSETID_Appointment:0x8217";
72
				$properties["startdate_recurring"] = "PT_SYSTIME:PSETID_Appointment:0x8235";
73
				$properties["enddate_recurring"] = "PT_SYSTIME:PSETID_Appointment:0x8236";
74
				$properties["recurring_pattern"] = "PT_STRING8:PSETID_Appointment:0x8232";
75
				$properties["location"] = "PT_STRING8:PSETID_Appointment:0x8208";
76
				$properties["duration"] = "PT_LONG:PSETID_Appointment:0x8213";
77
				$properties["responsestatus"] = "PT_LONG:PSETID_Appointment:0x8218";
78
				$properties["reminder"] = "PT_BOOLEAN:PSETID_Common:0x8503";
79
				$properties["reminder_minutes"] = "PT_LONG:PSETID_Common:0x8501";
80
				$properties["recurrencetype"] = "PT_LONG:PSETID_Appointment:0x8231";
81
				$properties["contacts"] = "PT_MV_STRING8:PSETID_Common:0x853a";
82
				$properties["contacts_string"] = "PT_STRING8:PSETID_Common:0x8586";
83
				$properties["categories"] = "PT_MV_STRING8:PS_PUBLIC_STRINGS:Keywords";
84
				$properties["reminder_time"] = "PT_SYSTIME:PSETID_Common:0x8502";
85
				$properties["commonstart"] = "PT_SYSTIME:PSETID_Common:0x8516";
86
				$properties["commonend"] = "PT_SYSTIME:PSETID_Common:0x8517";
87
				$properties["basedate"] = "PT_SYSTIME:PSETID_Appointment:0x8228";
88
				$properties["timezone_data"] = "PT_BINARY:PSETID_Appointment:0x8233";
89
				$properties["timezone"] = "PT_STRING8:PSETID_Appointment:0x8234";
90
				$properties["flagdueby"] = "PT_SYSTIME:PSETID_Common:0x8560";
91
				$properties["side_effects"] = "PT_LONG:PSETID_Common:0x8510";
92
				$properties["hideattachments"] = "PT_BOOLEAN:PSETID_Common:0x8514";
93
94
				$this->proptags = getPropIdsFromStrings($store, $properties);
95
			}
96
97
			parent::__construct($store, $message);
98
		}
99
100
		/**
101
		 * Create an exception
102
		 * @param array $exception_props the exception properties (same properties as normal recurring items)
103
		 * @param date $base_date the base date of the exception (LOCAL time of non-exception occurrence)
104
		 * @param boolean $delete true - delete occurrence, false - create new exception or modify existing
105
		 * @param array $exception_recips true - delete occurrence, false - create new exception or modify existing
106
		 * @param mapi_message $copy_attach_from mapi message from which attachments should be copied
0 ignored issues
show
Bug introduced by
The type mapi_message 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...
107
		 */
108
		function createException($exception_props, $base_date, $delete = false, $exception_recips = array(), $copy_attach_from = false)
109
		{
110
			$baseday = $this->dayStartOf($base_date);
111
			$basetime = $baseday + $this->recur["startocc"] * 60;
112
113
			// Remove any pre-existing exception on this base date
114
            if($this->isException($baseday)) {
115
                $this->deleteException($baseday); // note that deleting an exception is different from creating a deleted exception (deleting an occurrence).
116
            }
117
118
			if(!$delete) {
119
                if(isset($exception_props[$this->proptags["startdate"]]) && !$this->isValidExceptionDate($base_date, $this->fromGMT($this->tz, $exception_props[$this->proptags["startdate"]]))) {
120
                    return false;
121
                }
122
				// Properties in the attachment are the properties of the base object, plus $exception_props plus the base date
123
				foreach (array("subject", "location", "label", "reminder", "reminder_minutes", "alldayevent", "busystatus") as $propname) {
124
					if(isset($this->messageprops[$this->proptags[$propname]]))
125
						$props[$this->proptags[$propname]] = $this->messageprops[$this->proptags[$propname]];
126
				}
127
128
				$props[PR_MESSAGE_CLASS] = "IPM.OLE.CLASS.{00061055-0000-0000-C000-000000000046}";
129
				unset($exception_props[PR_MESSAGE_CLASS]);
130
				unset($exception_props[PR_ICON_INDEX]);
131
				$props = $exception_props + $props;
132
133
				// Basedate in the exception attachment is the GMT time at which the original occurrence would have been
134
				$props[$this->proptags["basedate"]] = $this->toGMT($this->tz, $basetime);
135
136
				if (!isset($exception_props[$this->proptags["startdate"]])) {
137
					$props[$this->proptags["startdate"]] = $this->getOccurrenceStart($base_date);
138
				}
139
140
				if (!isset($exception_props[$this->proptags["duedate"]])) {
141
					$props[$this->proptags["duedate"]] = $this->getOccurrenceEnd($base_date);
142
				}
143
144
				// synchronize commonstart/commonend with startdate/duedate
145
				if(isset($props[$this->proptags["startdate"]])) {
146
					$props[$this->proptags["commonstart"]] = $props[$this->proptags["startdate"]];
147
				}
148
149
				if(isset($props[$this->proptags["duedate"]])) {
150
					$props[$this->proptags["commonend"]] = $props[$this->proptags["duedate"]];
151
				}
152
153
				// Save the data into an attachment
154
				$this->createExceptionAttachment($props, $exception_recips, $copy_attach_from);
0 ignored issues
show
Bug introduced by
It seems like $copy_attach_from can also be of type false; however, parameter $copy_attach_from of Recurrence::createExceptionAttachment() does only seem to accept mapi_message, maybe add an additional type check? ( Ignorable by Annotation )

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

154
				$this->createExceptionAttachment($props, $exception_recips, /** @scrutinizer ignore-type */ $copy_attach_from);
Loading history...
155
156
                $changed_item = array();
157
158
                $changed_item["basedate"] = $baseday;
159
                $changed_item["start"] = $this->fromGMT($this->tz, $props[$this->proptags["startdate"]]);
160
                $changed_item["end"] = $this->fromGMT($this->tz, $props[$this->proptags["duedate"]]);
161
162
                if(array_key_exists($this->proptags["subject"], $exception_props)) {
163
                    $changed_item["subject"] = $exception_props[$this->proptags["subject"]];
164
                }
165
166
                if(array_key_exists($this->proptags["location"], $exception_props)) {
167
                    $changed_item["location"] = $exception_props[$this->proptags["location"]];
168
                }
169
170
                if(array_key_exists($this->proptags["label"], $exception_props)) {
171
                    $changed_item["label"] = $exception_props[$this->proptags["label"]];
172
                }
173
174
                if(array_key_exists($this->proptags["reminder"], $exception_props)) {
175
                    $changed_item["reminder_set"] = $exception_props[$this->proptags["reminder"]];
176
				}
177
178
				if(array_key_exists($this->proptags["reminder_minutes"], $exception_props)) {
179
					$changed_item["remind_before"] = $exception_props[$this->proptags["reminder_minutes"]];
180
				}
181
182
                if(array_key_exists($this->proptags["alldayevent"], $exception_props)) {
183
                    $changed_item["alldayevent"] = $exception_props[$this->proptags["alldayevent"]];
184
                }
185
186
                if(array_key_exists($this->proptags["busystatus"], $exception_props)) {
187
                    $changed_item["busystatus"] = $exception_props[$this->proptags["busystatus"]];
188
                }
189
190
                // Add the changed occurrence to the list
191
                array_push($this->recur["changed_occurrences"], $changed_item);
192
			} else {
193
			    // Delete the occurrence by placing it in the deleted occurrences list
194
			    array_push($this->recur["deleted_occurrences"], $baseday);
195
			}
196
197
			// Turn on hideattachments, because the attachments in this item are the exceptions
198
			mapi_setprops($this->message, array ( $this->proptags["hideattachments"] => true ));
199
200
			// Save recurrence data to message
201
			$this->saveRecurrence();
202
203
			return true;
204
		}
205
206
		/**
207
		 * Modifies an existing exception, but only updates the given properties
208
		 * NOTE: You can't remove properties from an exception, only add new ones
209
		 */
210
		function modifyException($exception_props, $base_date, $exception_recips = array(), $copy_attach_from = false)
211
		{
212
		    if(isset($exception_props[$this->proptags["startdate"]]) && !$this->isValidExceptionDate($base_date, $this->fromGMT($this->tz, $exception_props[$this->proptags["startdate"]]))) {
213
		        return false;
214
            }
215
216
			$baseday = $this->dayStartOf($base_date);
217
			$extomodify = false;
218
219
			for($i = 0, $len = count($this->recur["changed_occurrences"]); $i < $len; $i++) {
220
		    	if($this->isSameDay($this->recur["changed_occurrences"][$i]["basedate"], $baseday))
221
		    		$extomodify = &$this->recur["changed_occurrences"][$i];
222
		    }
223
224
			if(!$extomodify)
0 ignored issues
show
introduced by
$extomodify is of type mixed, thus it always evaluated to false.
Loading history...
225
				return false;
226
227
			// remove basedate property as we want to preserve the old value
228
			// client will send basedate with time part as zero, so discard that value
229
			unset($exception_props[$this->proptags["basedate"]]);
230
231
			if(array_key_exists($this->proptags["startdate"], $exception_props)) {
232
				$extomodify["start"] = $this->fromGMT($this->tz, $exception_props[$this->proptags["startdate"]]);
233
			}
234
235
			if(array_key_exists($this->proptags["duedate"], $exception_props)) {
236
				$extomodify["end"] =   $this->fromGMT($this->tz, $exception_props[$this->proptags["duedate"]]);
237
			}
238
239
			if(array_key_exists($this->proptags["subject"], $exception_props)) {
240
				$extomodify["subject"] = $exception_props[$this->proptags["subject"]];
241
			}
242
243
			if(array_key_exists($this->proptags["location"], $exception_props)) {
244
				$extomodify["location"] = $exception_props[$this->proptags["location"]];
245
			}
246
247
			if(array_key_exists($this->proptags["label"], $exception_props)) {
248
				$extomodify["label"] = $exception_props[$this->proptags["label"]];
249
			}
250
251
			if(array_key_exists($this->proptags["reminder"], $exception_props)) {
252
				$extomodify["reminder_set"] = $exception_props[$this->proptags["reminder"]];
253
			}
254
255
			if(array_key_exists($this->proptags["reminder_minutes"], $exception_props)) {
256
				$extomodify["remind_before"] = $exception_props[$this->proptags["reminder_minutes"]];
257
			}
258
259
			if(array_key_exists($this->proptags["alldayevent"], $exception_props)) {
260
				$extomodify["alldayevent"] = $exception_props[$this->proptags["alldayevent"]];
261
			}
262
263
			if(array_key_exists($this->proptags["busystatus"], $exception_props)) {
264
				$extomodify["busystatus"] = $exception_props[$this->proptags["busystatus"]];
265
			}
266
267
			$exception_props[PR_MESSAGE_CLASS] = "IPM.OLE.CLASS.{00061055-0000-0000-C000-000000000046}";
268
269
			// synchronize commonstart/commonend with startdate/duedate
270
			if(isset($exception_props[$this->proptags["startdate"]])) {
271
				$exception_props[$this->proptags["commonstart"]] = $exception_props[$this->proptags["startdate"]];
272
			}
273
274
			if(isset($exception_props[$this->proptags["duedate"]])) {
275
				$exception_props[$this->proptags["commonend"]] = $exception_props[$this->proptags["duedate"]];
276
			}
277
278
			$attach = $this->getExceptionAttachment($baseday);
279
			if(!$attach) {
280
				if ($copy_attach_from) {
281
					$this->deleteExceptionAttachment($base_date);
282
					$this->createException($exception_props, $base_date, false, $exception_recips, $copy_attach_from);
283
				} else {
284
					$this->createExceptionAttachment($exception_props, $exception_recips, $copy_attach_from);
285
				}
286
			} else {
287
				$message = mapi_attach_openobj($attach, MAPI_MODIFY);
288
289
				// Set exception properties on embedded message and save
290
				mapi_setprops($message, $exception_props);
291
				$this->setExceptionRecipients($message, $exception_recips, false);
292
				mapi_savechanges($message);
293
294
				// If a new start or duedate is provided, we update the properties 'PR_EXCEPTION_STARTTIME' and 'PR_EXCEPTION_ENDTIME'
295
				// on the attachment which holds the embedded msg and save everything.
296
				$props = array();
297
				if (isset($exception_props[$this->proptags["startdate"]])) {
298
					$props[PR_EXCEPTION_STARTTIME] = $this->fromGMT($this->tz, $exception_props[$this->proptags["startdate"]]);
299
				}
300
				if (isset($exception_props[$this->proptags["duedate"]])) {
301
					$props[PR_EXCEPTION_ENDTIME] = $this->fromGMT($this->tz, $exception_props[$this->proptags["duedate"]]);
302
				}
303
				if (!empty($props)) {
304
					mapi_setprops($attach, $props);
305
				}
306
307
				mapi_savechanges($attach);
308
			}
309
310
			// Save recurrence data to message
311
			$this->saveRecurrence();
312
313
			return true;
314
		}
315
316
		// Checks to see if the following is true:
317
		// 1) The exception to be created doesn't create two exceptions starting on one day (however, they can END on the same day by modifying duration)
318
		// 2) The exception to be created doesn't 'jump' over another occurrence (which may be an exception itself!)
319
		//
320
		// Both $basedate and $start are in LOCAL time
321
		function isValidExceptionDate($basedate, $start)
322
		{
323
		    // The way we do this is to look at the days that we're 'moving' the item in the exception. Each
324
		    // of these days may only contain the item that we're modifying. Any other item violates the rules.
325
326
		    if($this->isException($basedate)) {
327
		        // If we're modifying an exception, we want to look at the days that we're 'moving' compared to where
328
		        // the exception used to be.
329
		        $oldexception = $this->getChangeException($basedate);
330
		        $prevday = $this->dayStartOf($oldexception["start"]);
331
		    } else {
332
		        // If its a new exception, we want to look at the original placement of this item.
333
		        $prevday = $basedate;
334
		    }
335
336
		    $startday = $this->dayStartOf($start);
337
338
		    // Get all the occurrences on the days between the basedate (may be reversed)
339
		    if($prevday < $startday)
340
		        $items = $this->getItems($this->toGMT($this->tz, $prevday), $this->toGMT($this->tz, $startday + 24 * 60 * 60));
0 ignored issues
show
Bug introduced by
It seems like $this->toGMT($this->tz, $prevday) can also be of type integer; however, parameter $start of BaseRecurrence::getItems() does only seem to accept date, maybe add an additional type check? ( Ignorable by Annotation )

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

340
		        $items = $this->getItems(/** @scrutinizer ignore-type */ $this->toGMT($this->tz, $prevday), $this->toGMT($this->tz, $startday + 24 * 60 * 60));
Loading history...
Bug introduced by
$this->toGMT($this->tz, $startday + 24 * 60 * 60) of type integer is incompatible with the type date expected by parameter $end of BaseRecurrence::getItems(). ( Ignorable by Annotation )

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

340
		        $items = $this->getItems($this->toGMT($this->tz, $prevday), /** @scrutinizer ignore-type */ $this->toGMT($this->tz, $startday + 24 * 60 * 60));
Loading history...
341
            else
342
		        $items = $this->getItems($this->toGMT($this->tz, $startday), $this->toGMT($this->tz, $prevday + 24 * 60 * 60));
343
344
            // There should now be exactly one item, namely the item that we are modifying. If there are any other items in the range,
345
            // then we abort the change, since one of the rules has been violated.
346
            return count($items) == 1;
347
		}
348
349
		/**
350
		 * Check to see if the exception proposed at a certain basedate is allowed concerning reminder times:
351
		 *
352
		 * Both must be true:
353
		 * - reminder time of this item is not before the starttime of the previous recurring item
354
		 * - reminder time of the next item is not before the starttime of this item
355
		 *
356
		 * @param date $basedate the base date of the exception (LOCAL time of non-exception occurrence)
357
		 * @param string $reminderminutes reminder minutes which is set of the item
358
		 * @param date $startdate the startdate of the selected item
359
		 * @returns boolean if the reminder minutes value valid (FALSE if either of the rules above are FALSE)
360
		 */
361
		function isValidReminderTime($basedate, $reminderminutes, $startdate)
362
		{
363
			// get all occurrence items before the seleceted items occurrence starttime
364
			$occitems = $this->getItems($this->messageprops[$this->proptags["startdate"]], $this->toGMT($this->tz, $basedate));
0 ignored issues
show
Bug introduced by
It seems like $this->toGMT($this->tz, $basedate) can also be of type integer; however, parameter $end of BaseRecurrence::getItems() does only seem to accept date, maybe add an additional type check? ( Ignorable by Annotation )

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

364
			$occitems = $this->getItems($this->messageprops[$this->proptags["startdate"]], /** @scrutinizer ignore-type */ $this->toGMT($this->tz, $basedate));
Loading history...
365
366
			if(!empty($occitems)) {
367
				// as occitems array is sorted in ascending order of startdate, to get the previous occurrence we take the last items in occitems .
368
				$previousitem_startdate = $occitems[count($occitems) - 1][$this->proptags["startdate"]];
369
370
				// if our reminder is set before or equal to the beginning of the previous occurrence, then that's not allowed
371
				if($startdate - ($reminderminutes*60) <= $previousitem_startdate)
372
					return false;
373
			}
374
375
			// Get the endtime of the current occurrence and find the next two occurrences (including the current occurrence)
376
			$currentOcc = $this->getItems($this->toGMT($this->tz, $basedate), 0x7ff00000, 2, true);
0 ignored issues
show
Bug introduced by
It seems like $this->toGMT($this->tz, $basedate) can also be of type integer; however, parameter $start of BaseRecurrence::getItems() does only seem to accept date, maybe add an additional type check? ( Ignorable by Annotation )

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

376
			$currentOcc = $this->getItems(/** @scrutinizer ignore-type */ $this->toGMT($this->tz, $basedate), 0x7ff00000, 2, true);
Loading history...
377
378
			// If there are another two occurrences, then the first is the current occurrence, and the one after that
379
			// is the next occurrence.
380
			if(count($currentOcc) > 1) {
381
				$next = $currentOcc[1];
382
				// Get reminder time of the next occurrence.
383
				$nextOccReminderTime = $next[$this->proptags["startdate"]] - ($next[$this->proptags["reminder_minutes"]] * 60);
384
				// If the reminder time of the next item is before the start of this item, then that's not allowed
385
				if($nextOccReminderTime <= $startdate)
386
					return false;
387
			}
388
389
			// All was ok
390
			return true;
391
		}
392
393
		function setRecurrence($tz, $recur)
394
		{
395
			// only reset timezone if specified
396
			if($tz)
397
				$this->tz = $tz;
398
399
			$this->recur = $recur;
400
401
			if(!isset($this->recur["changed_occurrences"]))
402
				$this->recur["changed_occurrences"] = Array();
403
404
			if(!isset($this->recur["deleted_occurrences"]))
405
				$this->recur["deleted_occurrences"] = Array();
406
407
			$this->deleteAttachments();
408
			$this->saveRecurrence();
409
410
			// if client has not set the recurring_pattern then we should generate it and save it
411
			$messageProps = mapi_getprops($this->message, Array($this->proptags["recurring_pattern"]));
412
			if(empty($messageProps[$this->proptags["recurring_pattern"]])) {
413
				$this->saveRecurrencePattern();
414
			}
415
		}
416
417
		// Returns the start or end time of the occurrence on the given base date.
418
		// This assumes that the basedate you supply is in LOCAL time
419
		function getOccurrenceStart($basedate)  {
420
			$daystart = $this->dayStartOf($basedate);
421
			return $this->toGMT($this->tz, $daystart + $this->recur["startocc"] * 60);
422
		}
423
424
		function getOccurrenceEnd($basedate)  {
425
			$daystart = $this->dayStartOf($basedate);
426
			return $this->toGMT($this->tz, $daystart + $this->recur["endocc"] * 60);
427
		}
428
429
430
		// Backwards compatible code
431
		function getOccurenceStart($basedate)  {
432
			return $this->getOccurrenceStart($basedate);
433
		}
434
		function getOccurenceEnd($basedate)  {
435
			return $this->getOccurrenceEnd($basedate);
436
		}
437
438
		/**
439
		* This function returns the next remindertime starting from $timestamp
440
		* When no next reminder exists, false is returned.
441
		*
442
		* Note: Before saving this new reminder time (when snoozing), you must check for
443
		*       yourself if this reminder time is earlier than your snooze time, else
444
		*       use your snooze time and not this reminder time.
445
		*/
446
		function getNextReminderTime($timestamp)
447
		{
448
			/**
449
		     * Get next item from now until forever, but max 1 item with reminder set
450
		     * Note 0x7ff00000 instead of 0x7fffffff because of possible overflow failures when converting to GMT....
451
			 * Here for getting next 10 occurrences assuming that next here we will be able to find
452
			 * nextreminder occurrence in 10 occurrences
453
			 */
454
			$items = $this->getItems($timestamp, 0x7ff00000, 10, true);
0 ignored issues
show
Bug introduced by
2146435072 of type integer is incompatible with the type date expected by parameter $end of BaseRecurrence::getItems(). ( Ignorable by Annotation )

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

454
			$items = $this->getItems($timestamp, /** @scrutinizer ignore-type */ 0x7ff00000, 10, true);
Loading history...
455
456
			// Initially setting nextreminder to false so when no next reminder exists, false is returned.
457
			$nextreminder = false;
458
			/**
459
			 * Loop through all reminder which we get in items variable
460
			 * and check whether the remindertime is greater than timestamp.
461
			 * On the first occurrence of greater nextreminder break the loop
462
			 * and return the value to calling function.
463
			 */
464
			for($i = 0, $len = count($items); $i < $len; $i++)
465
			{
466
				$item = $items[$i];
467
				$tempnextreminder = $item[$this->proptags["startdate"]] - ( $item[$this->proptags["reminder_minutes"]] * 60 );
468
469
				// If tempnextreminder is greater than timestamp then save it in nextreminder and break from the loop.
470
				if($tempnextreminder > $timestamp)
471
				{
472
					$nextreminder = $tempnextreminder;
473
					break;
474
				}
475
			}
476
			return $nextreminder;
477
		}
478
479
		/**
480
		 * Note: Static function, more like a utility function.
481
		 *
482
		 * Gets all the items (including recurring items) in the specified calendar in the given timeframe. Items are
483
		 * included as a whole if they overlap the interval <$start, $end> (non-inclusive). This means that if the interval
484
		 * is <08:00 - 14:00>, the item [6:00 - 8:00> is NOT included, nor is the item [14:00 - 16:00>. However, the item
485
		 * [7:00 - 9:00> is included as a whole, and is NOT capped to [8:00 - 9:00>.
486
		 *
487
		 * @param $store resource The store in which the calendar resides
488
		 * @param $calendar resource The calendar to get the items from
489
		 * @param $viewstart int Timestamp of beginning of view window
490
		 * @param $viewend int Timestamp of end of view window
491
		 * @param $propsrequested array Array of properties to return
492
		 * @param $rows array Array of rowdata as if they were returned directly from mapi_table_queryrows. Each recurring item is
493
		 *                    expanded so that it seems that there are only many single appointments in the table.
494
		 */
495
		static function getCalendarItems($store, $calendar, $viewstart, $viewend, $propsrequested)
496
		{
497
			return getCalendarItems($store, $calendar, $viewstart, $viewend, $propsrequested);
498
		}
499
500
501
		/*****************************************************************************************************************
502
		 * CODE BELOW THIS LINE IS FOR INTERNAL USE ONLY
503
		 *****************************************************************************************************************
504
		 */
505
506
		/**
507
		 * Generates and stores recurrence pattern string to recurring_pattern property.
508
		 */
509
		function saveRecurrencePattern()
510
		{
511
			// Start formatting the properties in such a way we can apply
512
			// them directly into the recurrence pattern.
513
			$type = $this->recur['type'];
514
			$everyn = $this->recur['everyn'];
515
			$start = $this->recur['start'];
516
			$end = $this->recur['end'];
517
			$term = $this->recur['term'];
518
			$numocc = isset($this->recur['numoccur']) ? $this->recur['numoccur'] : false;
519
			$startocc = $this->recur['startocc'];
520
			$endocc = $this->recur['endocc'];
521
			$pattern = '';
522
			$occSingleDayRank = false;
523
			$occTimeRange = ($startocc != 0 && $endocc != 0);
524
525
			switch ($type) {
526
				// Daily
527
				case 0x0A:
528
					if ($everyn == 1) {
529
						$type = dgettext("zarafa","workday");
530
						$occSingleDayRank = true;
531
					} else if ($everyn == (24 * 60)) {
532
						$type = dgettext("zarafa","day");
533
						$occSingleDayRank = true;
534
					} else {
535
						$everyn /= (24 * 60);
536
						$type = dgettext("zarafa","days");
537
						$occSingleDayRank = false;
538
					}
539
					break;
540
				// Weekly
541
				case 0x0B:
542
					if ($everyn == 1) {
543
						$type = dgettext("zarafa","week");
544
						$occSingleDayRank = true;
545
					} else {
546
						$type = dgettext("zarafa","weeks");
547
						$occSingleDayRank = false;
548
					}
549
					break;
550
				// Monthly
551
				case 0x0C:
552
					if ($everyn == 1) {
553
						$type = dgettext("zarafa","month");
554
						$occSingleDayRank = true;
555
					} else {
556
						$type = dgettext("zarafa","months");
557
						$occSingleDayRank = false;
558
					}
559
					break;
560
				// Yearly
561
				case 0x0D:
562
					if ($everyn <= 12) {
563
						$everyn = 1;
564
						$type = dgettext("zarafa","year");
565
						$occSingleDayRank = true;
566
					} else {
567
						$everyn = $everyn / 12;
568
						$type = dgettext("zarafa","years");
569
						$occSingleDayRank = false;
570
					}
571
					break;
572
			}
573
574
			// get timings of the first occurrence
575
			$firstoccstartdate = isset($startocc) ? $start + (((int) $startocc) * 60) : $start;
576
			$firstoccenddate = isset($endocc) ? $end + (((int) $endocc) * 60) : $end;
577
578
			$start = gmdate(dgettext("zarafa","d-m-Y"), $firstoccstartdate);
579
			$end = gmdate(dgettext("zarafa","d-m-Y"), $firstoccenddate);
580
			$startocc = gmdate(dgettext("zarafa","G:i"), $firstoccstartdate);
581
			$endocc = gmdate(dgettext("zarafa","G:i"), $firstoccenddate);
582
583
			// Based on the properties, we need to generate the recurrence pattern string.
584
			// This is obviously very easy since we can simply concatenate a bunch of strings,
585
			// however this messes up translations for languages which order their words
586
			// differently.
587
			// To improve translation quality we create a series of default strings, in which
588
			// we only have to fill in the correct variables. The base string is thus selected
589
			// based on the available properties.
590
			if ($term == 0x23) {
591
				// Never ends
592
				if ($occTimeRange) {
593
					if ($occSingleDayRank) {
594
						$pattern = sprintf(dgettext("zarafa","Occurs every %s effective %s from %s to %s."), $type, $start, $startocc, $endocc);
595
					} else {
596
						$pattern = sprintf(dgettext("zarafa","Occurs every %s %s effective %s from %s to %s."), $everyn, $type, $start, $startocc, $endocc);
597
					}
598
				} else {
599
					if ($occSingleDayRank) {
600
						$pattern = sprintf(dgettext("zarafa","Occurs every %s effective %s."), $type, $start);
601
					} else {
602
						$pattern = sprintf(dgettext("zarafa","Occurs every %s %s effective %s."), $everyn, $type, $start);
603
					}
604
				}
605
			} else if ($term == 0x22) {
606
				// After a number of times
607
				if ($occTimeRange) {
608
					if ($occSingleDayRank) {
609
						$pattern = sprintf(dngettext("zarafa","Occurs every %s effective %s for %s occurrence from %s to %s.",
610
													 "Occurs every %s effective %s for %s occurrences from %s to %s.", $numocc), $type, $start, $numocc, $startocc, $endocc);
0 ignored issues
show
Bug introduced by
It seems like $numocc can also be of type false; however, parameter $count of dngettext() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

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

610
													 "Occurs every %s effective %s for %s occurrences from %s to %s.", /** @scrutinizer ignore-type */ $numocc), $type, $start, $numocc, $startocc, $endocc);
Loading history...
Bug introduced by
It seems like $numocc can also be of type false; however, parameter $values of sprintf() does only seem to accept double|integer|string, maybe add an additional type check? ( Ignorable by Annotation )

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

610
													 "Occurs every %s effective %s for %s occurrences from %s to %s.", $numocc), $type, $start, /** @scrutinizer ignore-type */ $numocc, $startocc, $endocc);
Loading history...
611
					} else {
612
						$pattern = sprintf(dngettext("zarafa","Occurs every %s %s effective %s for %s occurrence from %s to %s.",
613
													 "Occurs every %s %s effective %s for %s occurrences %s to %s.", $numocc), $everyn, $type, $start, $numocc, $startocc, $endocc);
614
					}
615
				} else {
616
					if ($occSingleDayRank) {
617
						$pattern = sprintf(dngettext("zarafa","Occurs every %s effective %s for %s occurrence.",
618
													 "Occurs every %s effective %s for %s occurrences.", $numocc), $type, $start, $numocc);
619
					} else {
620
						$pattern = sprintf(dngettext("zarafa","Occurs every %s %s effective %s for %s occurrence.",
621
													 "Occurs every %s %s effective %s for %s occurrences.", $numocc), $everyn, $type, $start, $numocc);
622
					}
623
				}
624
			} else if ($term == 0x21) {
625
				// After the given enddate
626
				if ($occTimeRange) {
627
					if ($occSingleDayRank) {
628
						$pattern = sprintf(dgettext("zarafa","Occurs every %s effective %s until %s from %s to %s."), $type, $start, $end, $startocc, $endocc);
629
					} else {
630
						$pattern = sprintf(dgettext("zarafa","Occurs every %s %s effective %s until %s from %s to %s."), $everyn, $type, $start, $end, $startocc, $endocc);
631
					}
632
				} else {
633
					if ($occSingleDayRank) {
634
						$pattern = sprintf(dgettext("zarafa","Occurs every %s effective %s until %s."), $type, $start, $end);
635
					} else {
636
						$pattern = sprintf(dgettext("zarafa","Occurs every %s %s effective %s until %s."), $everyn, $type, $start, $end);
637
					}
638
				}
639
			}
640
641
			if(!empty($pattern)) {
642
				mapi_setprops($this->message, Array($this->proptags["recurring_pattern"] => $pattern ));
643
			}
644
		}
645
646
		/*
647
		 * Remove an exception by base_date. This is the base date in local daystart time
648
		 */
649
		function deleteException($base_date)
650
		{
651
		    // Remove all exceptions on $base_date from the deleted and changed occurrences lists
652
653
		    // Remove all items in $todelete from deleted_occurrences
654
		    $new = Array();
655
656
		    foreach($this->recur["deleted_occurrences"] as $entry) {
657
		    	if($entry != $base_date)
658
		    		$new[] = $entry;
659
		    }
660
		    $this->recur["deleted_occurrences"] = $new;
661
662
		    $new = Array();
663
664
		    foreach($this->recur["changed_occurrences"] as $entry) {
665
		    	if(!$this->isSameDay($entry["basedate"], $base_date))
666
		    		$new[] = $entry;
667
		    	else
668
		    		$this->deleteExceptionAttachment($this->toGMT($this->tz, $base_date + $this->recur["startocc"] * 60));
0 ignored issues
show
Bug introduced by
$this->toGMT($this->tz, ...recur['startocc'] * 60) of type integer is incompatible with the type date expected by parameter $base_date of Recurrence::deleteExceptionAttachment(). ( Ignorable by Annotation )

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

668
		    		$this->deleteExceptionAttachment(/** @scrutinizer ignore-type */ $this->toGMT($this->tz, $base_date + $this->recur["startocc"] * 60));
Loading history...
669
		    }
670
671
		    $this->recur["changed_occurrences"] = $new;
672
		}
673
674
		/**
675
		 * Function which saves the exception data in an attachment.
676
		 * @param array $exception_props the exception data (like any other MAPI appointment)
677
		 * @param array $exception_recips list of recipients
678
		 * @param mapi_message $copy_attach_from mapi message from which attachments should be copied
679
		 * @return array properties of the exception
680
		 */
681
		function createExceptionAttachment($exception_props, $exception_recips = array(), $copy_attach_from = false)
682
		{
683
		  	// Create new attachment.
684
		  	$attachment = mapi_message_createattach($this->message);
685
		  	$props = array();
686
		  	$props[PR_ATTACHMENT_FLAGS] = 2;
687
		  	$props[PR_ATTACHMENT_HIDDEN] = true;
688
		  	$props[PR_ATTACHMENT_LINKID] = 0;
689
		  	$props[PR_ATTACH_FLAGS] = 0;
690
		  	$props[PR_ATTACH_METHOD] = ATTACH_EMBEDDED_MSG;
691
		  	$props[PR_DISPLAY_NAME] = "Exception";
692
		  	$props[PR_EXCEPTION_STARTTIME] = $this->fromGMT($this->tz, $exception_props[$this->proptags["startdate"]]);
693
		  	$props[PR_EXCEPTION_ENDTIME] = $this->fromGMT($this->tz, $exception_props[$this->proptags["duedate"]]);
694
		  	mapi_setprops($attachment, $props);
695
696
			$imessage = mapi_attach_openobj($attachment, MAPI_CREATE | MAPI_MODIFY);
697
698
			if ($copy_attach_from) {
699
				$attachmentTable = mapi_message_getattachmenttable($copy_attach_from);
700
				if($attachmentTable) {
701
					$attachments = mapi_table_queryallrows($attachmentTable, array(PR_ATTACH_NUM, PR_ATTACH_SIZE, PR_ATTACH_LONG_FILENAME, PR_ATTACHMENT_HIDDEN, PR_DISPLAY_NAME, PR_ATTACH_METHOD));
702
703
					foreach($attachments as $attach_props){
704
						$attach_old = mapi_message_openattach($copy_attach_from, (int) $attach_props[PR_ATTACH_NUM]);
705
						$attach_newResourceMsg = mapi_message_createattach($imessage);
706
						mapi_copyto($attach_old, array(), array(), $attach_newResourceMsg, 0);
707
						mapi_savechanges($attach_newResourceMsg);
708
					}
709
				}
710
			}
711
712
			$props = $props + $exception_props;
713
714
			// FIXME: the following piece of code is written to fix the creation
715
			// of an exception. This is only a quickfix as it is not yet possible
716
			// to change an existing exception.
717
			// remove mv properties when needed
718
			foreach($props as $propTag=>$propVal){
719
				if ((mapi_prop_type($propTag) & MV_FLAG) == MV_FLAG && is_null($propVal)){
720
					unset($props[$propTag]);
721
				}
722
			}
723
724
			mapi_setprops($imessage, $props);
725
726
			$this->setExceptionRecipients($imessage, $exception_recips, true);
727
728
			mapi_savechanges($imessage);
729
			mapi_savechanges($attachment);
730
		}
731
732
		/**
733
		 * Function which deletes the attachment of an exception.
734
		 * @param date $base_date base date of the attachment. Should be in GMT. The attachment
735
		 *                        actually saves the real time of the original date, so we have
736
		 *						  to check whether it's on the same day.
737
		 */
738
		function deleteExceptionAttachment($base_date)
739
		{
740
			$attachments = mapi_message_getattachmenttable($this->message);
741
			// Retrieve only exceptions which are stored as embedded messages
742
			$attach_res = Array(RES_PROPERTY,
743
							Array(
744
								RELOP => RELOP_EQ,
745
								ULPROPTAG => PR_ATTACH_METHOD,
746
								VALUE => array(PR_ATTACH_METHOD => ATTACH_EMBEDDED_MSG)
747
									)
748
			);
749
			$attachRows = mapi_table_queryallrows($attachments, Array(PR_ATTACH_NUM), $attach_res);
750
751
			foreach($attachRows as $attachRow)
752
			{
753
				$tempattach = mapi_message_openattach($this->message, $attachRow[PR_ATTACH_NUM]);
754
				$exception = mapi_attach_openobj($tempattach);
755
756
			  	$data = mapi_message_getprops($exception, array($this->proptags["basedate"]));
757
758
			  	if($this->dayStartOf($this->fromGMT($this->tz, $data[$this->proptags["basedate"]])) == $this->dayStartOf($base_date)) {
0 ignored issues
show
Bug introduced by
$this->fromGMT($this->tz...>proptags['basedate']]) of type integer is incompatible with the type date expected by parameter $date of BaseRecurrence::dayStartOf(). ( Ignorable by Annotation )

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

758
			  	if($this->dayStartOf(/** @scrutinizer ignore-type */ $this->fromGMT($this->tz, $data[$this->proptags["basedate"]])) == $this->dayStartOf($base_date)) {
Loading history...
759
			  		mapi_message_deleteattach($this->message, $attachRow[PR_ATTACH_NUM]);
760
			  	}
761
			}
762
		}
763
764
		/**
765
		 * Function which deletes all attachments of a message.
766
		 */
767
		function deleteAttachments()
768
		{
769
			$attachments = mapi_message_getattachmenttable($this->message);
770
			$attachTable = mapi_table_queryallrows($attachments, Array(PR_ATTACH_NUM, PR_ATTACHMENT_HIDDEN));
771
772
			foreach($attachTable as $attachRow)
773
			{
774
				if(isset($attachRow[PR_ATTACHMENT_HIDDEN]) && $attachRow[PR_ATTACHMENT_HIDDEN]) {
775
					mapi_message_deleteattach($this->message, $attachRow[PR_ATTACH_NUM]);
776
				}
777
			}
778
		}
779
780
		/**
781
		 * Get an exception attachment based on its basedate
782
		 */
783
		function getExceptionAttachment($base_date)
784
		{
785
			// Retrieve only exceptions which are stored as embedded messages
786
			$attach_res = Array(RES_PROPERTY, Array(
787
								RELOP => RELOP_EQ,
788
								ULPROPTAG => PR_ATTACH_METHOD,
789
								VALUE => array(PR_ATTACH_METHOD => ATTACH_EMBEDDED_MSG)
790
							)
791
			);
792
			$attachments = mapi_message_getattachmenttable($this->message);
793
			$attachRows = mapi_table_queryallrows($attachments, Array(PR_ATTACH_NUM), $attach_res);
794
795
			if(is_array($attachRows)) {
796
				foreach($attachRows as $attachRow)
797
				{
798
					$tempattach = mapi_message_openattach($this->message, $attachRow[PR_ATTACH_NUM]);
799
					$exception = mapi_attach_openobj($tempattach);
800
801
					$data = mapi_message_getprops($exception, array($this->proptags["basedate"]));
802
803
					if(!isset($data[$this->proptags["basedate"]])) {
804
						// if no basedate found then it could be embedded message so ignore it
805
						// we need proper restriction to exclude embedded messages as well
806
						continue;
807
					}
808
809
					if($this->isSameDay($this->fromGMT($this->tz, $data[$this->proptags["basedate"]]), $base_date)) {
810
						return $tempattach;
811
					}
812
				}
813
			}
814
815
			return false;
816
		}
817
818
		/**
819
		 * processOccurrenceItem, adds an item to a list of occurrences, but only if the following criteria are met:
820
		 * - The resulting occurrence (or exception) starts or ends in the interval <$start, $end>
821
		 * - The occurrence isn't specified as a deleted occurrence
822
		 * @param array $items reference to the array to be added to
823
		 * @param date $start start of timeframe in GMT TIME
824
		 * @param date $end end of timeframe in GMT TIME
825
		 * @param date $basedate (hour/sec/min assumed to be 00:00:00) in LOCAL TIME OF THE OCCURRENCE
826
		 * @param int $startocc start of occurrence since beginning of day in minutes
827
		 * @param int $endocc end of occurrence since beginning of day in minutes
828
		 * @param int $tz the timezone info for this occurrence ( applied to $basedate / $startocc / $endocc )
829
		 * @param bool $reminderonly If TRUE, only add the item if the reminder is set
830
		 */
831
        function processOccurrenceItem(&$items, $start, $end, $basedate, $startocc, $endocc, $tz, $reminderonly)
832
        {
833
			$exception = $this->isException($basedate);
834
			if($exception){
0 ignored issues
show
Bug Best Practice introduced by
The expression $exception 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...
835
				return false;
836
			} else {
837
				$occstart = $basedate + $startocc * 60;
838
				$occend = $basedate + $endocc * 60;
839
840
				// Convert to GMT
841
				$occstart = $this->toGMT($tz, $occstart);
842
				$occend = $this->toGMT($tz, $occend);
843
844
				/**
845
				 * FIRST PART: Check range criterium. Exact matches (eg when $occstart == $end), do NOT match since you cannot
846
				 * see any part of the appointment. Partial overlaps DO match.
847
				 *
848
				 * SECOND PART: check if occurrence is not a zero duration occurrence which
849
				 * starts at 00:00 and ends on 00:00. if it is so, then process
850
				 * the occurrence and send it in response.
851
				 */
852
				if(($occstart  >= $end || $occend <=  $start) && !($occstart == $occend && $occstart == $start))
853
					return;
854
855
                // Properties for this occurrence are the same as the main object,
856
                // With these properties overridden
857
				$newitem = $this->messageprops;
858
				$newitem[$this->proptags["startdate"]] = $occstart;
859
				$newitem[$this->proptags["duedate"]] = $occend;
860
				$newitem[$this->proptags["commonstart"]] = $occstart;
861
				$newitem[$this->proptags["commonend"]] = $occend;
862
                $newitem["basedate"] = $basedate;
863
            }
864
865
            // If reminderonly is set, only add reminders
866
            if($reminderonly && (!isset($newitem[$this->proptags["reminder"]]) || $newitem[$this->proptags["reminder"]] == false))
867
                return;
868
869
            $items[] = $newitem;
870
        }
871
872
		/**
873
		 * processExceptionItem, adds an all exception item to a list of occurrences, without any constraint on timeframe
874
		 * @param array $items reference to the array to be added to
875
		 * @param date $start start of timeframe in GMT TIME
876
		 * @param date $end end of timeframe in GMT TIME
877
		 */
878
		function processExceptionItems(&$items, $start, $end)
879
		{
880
			$limit = 0;
881
			foreach($this->recur["changed_occurrences"] as $exception) {
882
883
				// Convert to GMT
884
				$occstart = $this->toGMT($this->tz, $exception["start"]);
885
				$occend = $this->toGMT($this->tz, $exception["end"]);
886
887
				// Check range criterium. Exact matches (eg when $occstart == $end), do NOT match since you cannot
888
				// see any part of the appointment. Partial overlaps DO match.
889
				if($occstart >= $end || $occend <= $start)
890
					continue;
891
892
				array_push($items, $this->getExceptionProperties($exception));
893
			    if($limit && (count($items) == $limit))
894
					break;
895
				}
896
		}
897
898
		/**
899
		 * Function which verifies if on the given date an exception, delete or change, occurs.
900
		 * @param date $date the date
901
		 * @return array the exception, true - if an occurrence is deleted on the given date, false - no exception occurs on the given date
902
		 */
903
		function isException($basedate)
904
		{
905
            if($this->isDeleteException($basedate))
906
                return true;
0 ignored issues
show
Bug Best Practice introduced by
The expression return true returns the type true which is incompatible with the documented return type array.
Loading history...
907
908
            if($this->getChangeException($basedate) != false)
909
                return true;
0 ignored issues
show
Bug Best Practice introduced by
The expression return true returns the type true which is incompatible with the documented return type array.
Loading history...
910
911
			return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the documented return type array.
Loading history...
912
		}
913
914
		/**
915
		 * Returns TRUE if there is a DELETE exception on the given base date
916
		 */
917
        function isDeleteException($basedate)
918
        {
919
		    // Check if the occurrence is deleted on the specified date
920
			foreach($this->recur["deleted_occurrences"] as $deleted)
921
			{
922
			    if($this->isSameDay($deleted, $basedate))
923
			        return true;
924
			}
925
926
			return false;
927
        }
928
929
        /**
930
         * Returns the exception if there is a CHANGE exception on the given base date, or FALSE otherwise
931
         */
932
        function getChangeException($basedate)
933
        {
934
            // Check if the occurrence is modified on the specified date
935
			foreach($this->recur["changed_occurrences"] as $changed)
936
			{
937
                if($this->isSameDay($changed["basedate"], $basedate))
938
                    return $changed;
939
			}
940
941
			return false;
942
        }
943
944
		/**
945
		 * Function to see if two dates are on the same day
946
		 * @param date $time1 date 1
947
		 * @param date $time2 date 2
948
		 * @return boolean Returns TRUE when both dates are on the same day
949
		 */
950
		function isSameDay($date1, $date2)
951
		{
952
		    $time1 = $this->gmtime($date1);
953
		    $time2 = $this->gmtime($date2);
954
955
		    return $time1["tm_mon"] == $time2["tm_mon"] && $time1["tm_year"] == $time2["tm_year"] && $time1["tm_mday"] == $time2["tm_mday"];
956
		}
957
958
		/**
959
		 * Function to get all properties of a single changed exception.
960
		 * @param date $date base date of exception
961
		 * @return array associative array of properties for the exception, compatible with
962
		 */
963
        function getExceptionProperties($exception)
964
        {
965
        	// Exception has same properties as main object, with some properties overridden:
966
            $item = $this->messageprops;
967
968
            // Special properties
969
            $item["exception"] = true;
970
            $item["basedate"] = $exception["basedate"]; // note that the basedate is always in local time !
971
972
            // MAPI-compatible properties (you can handle an exception as a normal calendar item like this)
973
            $item[$this->proptags["startdate"]] = $this->toGMT($this->tz, $exception["start"]);
974
            $item[$this->proptags["duedate"]] = $this->toGMT($this->tz, $exception["end"]);
975
            $item[$this->proptags["commonstart"]] = $item[$this->proptags["startdate"]];
976
            $item[$this->proptags["commonend"]] = $item[$this->proptags["duedate"]];
977
978
            if(isset($exception["subject"])) {
979
                $item[$this->proptags["subject"]] = $exception["subject"];
980
            }
981
982
            if(isset($exception["label"])) {
983
                $item[$this->proptags["label"]] = $exception["label"];
984
            }
985
986
            if(isset($exception["alldayevent"])) {
987
                $item[$this->proptags["alldayevent"]] = $exception["alldayevent"];
988
            }
989
990
            if(isset($exception["location"])) {
991
                $item[$this->proptags["location"]] = $exception["location"];
992
            }
993
994
            if(isset($exception["remind_before"])) {
995
                $item[$this->proptags["reminder_minutes"]] = $exception["remind_before"];
996
            }
997
998
            if(isset($exception["reminder_set"])) {
999
                $item[$this->proptags["reminder"]] = $exception["reminder_set"];
1000
            }
1001
1002
            if(isset($exception["busystatus"])) {
1003
                $item[$this->proptags["busystatus"]] = $exception["busystatus"];
1004
            }
1005
1006
            return $item;
1007
        }
1008
1009
		/**
1010
		 * Function which sets recipients for an exception.
1011
		 *
1012
		 * The $exception_recips can be provided in 2 ways:
1013
		 *  - A delta which indicates which recipients must be added, removed or deleted.
1014
		 *  - A complete array of the recipients which should be applied to the message.
1015
		 *
1016
		 * The first option is preferred as it will require less work to be executed.
1017
		 *
1018
		 * @param resource $message exception attachment of recurring item
1019
		 * @param array $exception_recips list of recipients
1020
		 * @param boolean $copy_orig_recips True to copy all recipients which are on the original
1021
		 * message to the attachment by default. False if only the $exception_recips changes should
1022
		 * be applied.
1023
		 */
1024
		function setExceptionRecipients($message, $exception_recips, $copy_orig_recips = true)
1025
		{
1026
			if (isset($exception_recips['add']) || isset($exception_recips['remove']) || isset($exception_recips['modify'])) {
1027
				$this->setDeltaExceptionRecipients($message, $exception_recips, $copy_orig_recips);
1028
			} else {
1029
				$this->setAllExceptionRecipients($message, $exception_recips);
1030
			}
1031
		}
1032
1033
		/**
1034
		 * Function which applies the provided delta for recipients changes to the exception.
1035
		 *
1036
		 * The $exception_recips should be an array containing the following keys:
1037
		 *  - "add": this contains an array of recipients which must be added
1038
		 *  - "remove": This contains an array of recipients which must be removed
1039
		 *  - "modify": This contains an array of recipients which must be modified
1040
		 *
1041
		 * @param resource $message exception attachment of recurring item
1042
		 * @param array $exception_recips list of recipients
1043
		 * @param boolean $copy_orig_recips True to copy all recipients which are on the original
1044
		 * message to the attachment by default. False if only the $exception_recips changes should
1045
		 * be applied.
1046
		 */
1047
		function setDeltaExceptionRecipients($exception, $exception_recips, $copy_orig_recips)
1048
		{
1049
			// Check if the recipients from the original message should be copied,
1050
			// if so, open the recipient table of the parent message and apply all
1051
			// rows on the target recipient.
1052
			if ($copy_orig_recips === true) {
1053
				$origTable = mapi_message_getrecipienttable($this->message);
1054
				$recipientRows = mapi_table_queryallrows($origTable, $this->recipprops);
1055
				mapi_message_modifyrecipients($exception, MODRECIP_ADD, $recipientRows);
1056
			}
1057
1058
			// Add organizer to meeting only if it is not organized.
1059
			$msgprops = mapi_getprops($exception, array(PR_SENT_REPRESENTING_ENTRYID, PR_SENT_REPRESENTING_EMAIL_ADDRESS, PR_SENT_REPRESENTING_NAME, PR_SENT_REPRESENTING_ADDRTYPE, PR_SENT_REPRESENTING_SEARCH_KEY, $this->proptags['responsestatus']));
1060
			if (isset($msgprops[$this->proptags['responsestatus']]) && $msgprops[$this->proptags['responsestatus']] != olResponseOrganized){
1061
				$this->addOrganizer($msgprops, $exception_recips['add']);
1062
			}
1063
1064
			// Remove all deleted recipients
1065
			if (isset($exception_recips['remove'])) {
1066
				foreach ($exception_recips['remove'] as &$recip) {
1067
					if (!isset($recip[PR_RECIPIENT_FLAGS]) || $recip[PR_RECIPIENT_FLAGS] != (recipReserved | recipExceptionalDeleted | recipSendable)) {
1068
						$recip[PR_RECIPIENT_FLAGS] = recipSendable | recipExceptionalDeleted;
1069
					} else {
1070
						$recip[PR_RECIPIENT_FLAGS] = recipReserved | recipExceptionalDeleted | recipSendable;
1071
					}
1072
					$recip[PR_RECIPIENT_TRACKSTATUS] = olResponseNone;		// No Response required
1073
				}
1074
				unset($recip);
1075
				mapi_message_modifyrecipients($exception, MODRECIP_MODIFY, $exception_recips['remove']);
1076
			}
1077
1078
			// Add all new recipients
1079
			if (isset($exception_recips['add'])) {
1080
				mapi_message_modifyrecipients($exception, MODRECIP_ADD, $exception_recips['add']);
1081
			}
1082
1083
			// Modify the existing recipients
1084
			if (isset($exception_recips['modify'])) {
1085
				mapi_message_modifyrecipients($exception, MODRECIP_MODIFY, $exception_recips['modify']);
1086
			}
1087
		}
1088
1089
		/**
1090
		 * Function which applies the provided recipients to the exception, also checks for deleted recipients.
1091
		 *
1092
		 * The $exception_recips should be an array containing all recipients which must be applied
1093
		 * to the exception. This will copy all recipients from the original message and then start filter
1094
		 * out all recipients which are not provided by the $exception_recips list.
1095
		 *
1096
		 * @param resource $message exception attachment of recurring item
1097
		 * @param array $exception_recips list of recipients
1098
		 */
1099
		function setAllExceptionRecipients($message, $exception_recips)
1100
		{
1101
			$deletedRecipients = array();
1102
			$useMessageRecipients = false;
1103
1104
			$recipientTable = mapi_message_getrecipienttable($message);
1105
			$recipientRows = mapi_table_queryallrows($recipientTable, $this->recipprops);
1106
1107
			if (empty($recipientRows)) {
1108
				$useMessageRecipients = true;
1109
				$recipientTable = mapi_message_getrecipienttable($this->message);
1110
				$recipientRows = mapi_table_queryallrows($recipientTable, $this->recipprops);
1111
			}
1112
1113
			// Add organizer to meeting only if it is not organized.
1114
			$msgprops = mapi_getprops($message, array(PR_SENT_REPRESENTING_ENTRYID, PR_SENT_REPRESENTING_EMAIL_ADDRESS, PR_SENT_REPRESENTING_NAME, PR_SENT_REPRESENTING_ADDRTYPE, PR_SENT_REPRESENTING_SEARCH_KEY, $this->proptags['responsestatus']));
1115
			if (isset($msgprops[$this->proptags['responsestatus']]) && $msgprops[$this->proptags['responsestatus']] != olResponseOrganized){
1116
				$this->addOrganizer($msgprops, $exception_recips);
1117
			}
1118
1119
			if (!empty($exception_recips)) {
1120
				foreach($recipientRows as $key => $recipient) {
1121
					$found = false;
1122
					foreach($exception_recips as $excep_recip) {
1123
						if ($recipient[PR_SEARCH_KEY] == $excep_recip[PR_SEARCH_KEY])
1124
							$found = true;
1125
					}
1126
1127
					if (!$found) {
1128
						$foundInDeletedRecipients = false;
1129
						// Look if the $recipient is in the list of deleted recipients
1130
						if (!empty($deletedRecipients)) {
1131
								foreach($deletedRecipients as $recip) {
1132
									if ($recip[PR_SEARCH_KEY] == $recipient[PR_SEARCH_KEY]){
1133
										$foundInDeletedRecipients = true;
1134
										break;
1135
									}
1136
								}
1137
						}
1138
1139
						// If recipient is not in list of deleted recipient, add him
1140
						if (!$foundInDeletedRecipients) {
1141
							if (!isset($recipient[PR_RECIPIENT_FLAGS]) || $recipient[PR_RECIPIENT_FLAGS] != (recipReserved | recipExceptionalDeleted | recipSendable)) {
1142
								$recipient[PR_RECIPIENT_FLAGS] = recipSendable | recipExceptionalDeleted;
1143
							} else {
1144
								$recipient[PR_RECIPIENT_FLAGS] = recipReserved | recipExceptionalDeleted | recipSendable;
1145
							}
1146
							$recipient[PR_RECIPIENT_TRACKSTATUS] = olRecipientTrackStatusNone;	// No Response required
1147
							$deletedRecipients[] = $recipient;
1148
						}
1149
					}
1150
1151
					// When $message contains a non-empty recipienttable, we must delete the recipients
1152
					// before re-adding them. However, when $message is doesn't contain any recipients,
1153
					// we are using the recipient table of the original message ($this->message)
1154
					// rather then $message. In that case, we don't need to remove the recipients
1155
					// from the $message, as the recipient table is already empty, and
1156
					// mapi_message_modifyrecipients() will throw an error.
1157
					if ($useMessageRecipients === false) {
1158
						mapi_message_modifyrecipients($message, MODRECIP_REMOVE, array($recipient));
1159
					}
1160
				}
1161
				$exception_recips = array_merge($exception_recips, $deletedRecipients);
1162
			} else {
1163
				$exception_recips = $recipientRows;
1164
			}
1165
1166
			if (!empty($exception_recips)) {
1167
				// Set the new list of recipients on the exception message, this also removes the existing recipients
1168
				mapi_message_modifyrecipients($message, MODRECIP_MODIFY, $exception_recips);
1169
			}
1170
		}
1171
1172
		/**
1173
		 * Function returns basedates of all changed occurrences
1174
		 *@return array array(
1175
		 *					0 => 123459321
1176
		 *				)
1177
		 */
1178
		function getAllExceptions()
1179
		{
1180
			$result = false;
1181
			if (!empty($this->recur["changed_occurrences"])) {
1182
				$result = array();
1183
				foreach($this->recur["changed_occurrences"] as $exception) {
1184
					$result[] = $exception["basedate"];
1185
				}
1186
				return $result;
1187
			}
1188
			return $result;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $result returns the type false which is incompatible with the documented return type array.
Loading history...
1189
		}
1190
1191
		/**
1192
		 *  Function which adds organizer to recipient list which is passed.
1193
		 *  This function also checks if it has organizer.
1194
		 *
1195
		 * @param array $messageProps message properties
1196
		 * @param array $recipients	recipients list of message.
1197
		 * @param boolean $isException true if we are processing recipient of exception
1198
		 */
1199
		function addOrganizer($messageProps, &$recipients, $isException = false)
1200
		{
1201
			$hasOrganizer = false;
1202
			// Check if meeting already has an organizer.
1203
			foreach ($recipients as $key => $recipient){
1204
				if (isset($recipient[PR_RECIPIENT_FLAGS]) && $recipient[PR_RECIPIENT_FLAGS] == (recipSendable | recipOrganizer)) {
1205
					$hasOrganizer = true;
1206
				} else if ($isException && !isset($recipient[PR_RECIPIENT_FLAGS])){
1207
					// Recipients for an occurrence
1208
					$recipients[$key][PR_RECIPIENT_FLAGS] = recipSendable | recipExceptionalResponse;
1209
				}
1210
			}
1211
1212
			if (!$hasOrganizer){
1213
				// Create organizer.
1214
				$organizer = array();
1215
				$organizer[PR_ENTRYID] = $messageProps[PR_SENT_REPRESENTING_ENTRYID];
1216
				$organizer[PR_DISPLAY_NAME] = $messageProps[PR_SENT_REPRESENTING_NAME];
1217
				$organizer[PR_EMAIL_ADDRESS] = $messageProps[PR_SENT_REPRESENTING_EMAIL_ADDRESS];
1218
				$organizer[PR_RECIPIENT_TYPE] = MAPI_TO;
1219
				$organizer[PR_RECIPIENT_DISPLAY_NAME] = $messageProps[PR_SENT_REPRESENTING_NAME];
1220
				$organizer[PR_ADDRTYPE] = empty($messageProps[PR_SENT_REPRESENTING_ADDRTYPE]) ? 'SMTP' : $messageProps[PR_SENT_REPRESENTING_ADDRTYPE];
1221
				$organizer[PR_RECIPIENT_TRACKSTATUS] = olRecipientTrackStatusNone;
1222
				$organizer[PR_RECIPIENT_FLAGS] = recipSendable | recipOrganizer;
1223
				$organizer[PR_SEARCH_KEY] = $messageProps[PR_SENT_REPRESENTING_SEARCH_KEY];
1224
1225
				// Add organizer to recipients list.
1226
				array_unshift($recipients, $organizer);
1227
			}
1228
		}
1229
	}
1230
1231
	/*
1232
1233
	From http://www.ohelp-one.com/new-6765483-3268.html:
1234
1235
	Recurrence Data Structure Offset Type Value
1236
1237
	0 ULONG (?) Constant : { 0x04, 0x30, 0x04, 0x30}
1238
1239
	4 UCHAR 0x0A + recurrence type: 0x0A for daily, 0x0B for weekly, 0x0C for
1240
	monthly, 0x0D for yearly
1241
1242
	5 UCHAR Constant: { 0x20}
1243
1244
	6 ULONG Seems to be a variant of the recurrence type: 1 for daily every n
1245
	days, 2 for daily every weekday and weekly, 3 for monthly or yearly. The
1246
	special exception is regenerating tasks that regenerate on a weekly basis: 0
1247
	is used in that case (I have no idea why).
1248
1249
	Here's the recurrence-type-specific data. Because the daily every N days
1250
	data are 4 bytes shorter than the data for the other types, the offsets for
1251
	the rest of the data will be 4 bytes off depending on the recurrence type.
1252
1253
	Daily every N days:
1254
1255
	10 ULONG ( N - 1) * ( 24 * 60). I'm not sure what this is used for, but it's consistent.
1256
1257
	14 ULONG N * 24 * 60: minutes between recurrences
1258
1259
	18 ULONG 0 for all events and non-regenerating recurring tasks. 1 for
1260
	regenerating tasks.
1261
1262
	Daily every weekday (this is essentially a subtype of weekly recurrence):
1263
1264
	10 ULONG 6 * 24 * 60: minutes between recurrences ( a week... sort of)
1265
1266
	14 ULONG 1: recur every week (corresponds to the second parameter for weekly
1267
	recurrence)
1268
1269
	18 ULONG 0 for all events and non-regenerating recurring tasks. 1 for
1270
	regenerating tasks.
1271
1272
	22 ULONG 0x3E: bitmask for recurring every weekday (corresponds to fourth
1273
	parameter for weekly recurrence)
1274
1275
	Weekly every N weeks for all events and non-regenerating tasks:
1276
1277
	10 ULONG 6 * 24 * 60: minutes between recurrences (a week... sort of)
1278
1279
	14 ULONG N: recurrence interval
1280
1281
	18 ULONG Constant: 0
1282
1283
	22 ULONG Bitmask for determining which days of the week the event recurs on
1284
	( 1 << dayOfWeek, where Sunday is 0).
1285
1286
	Weekly every N weeks for regenerating tasks: 10 ULONG Constant: 0
1287
1288
	14 ULONG N * 7 * 24 * 60: recurrence interval in minutes between occurrences
1289
1290
	18 ULONG Constant: 1
1291
1292
	Monthly every N months on day D:
1293
1294
	10 ULONG This is the most complicated value
1295
	in the entire mess. It's basically a very complicated way of stating the
1296
	recurrence interval. I tweaked fbs' basic algorithm. DateTime::MonthInDays
1297
	simply returns the number of days in a given month, e.g. 31 for July for 28
1298
	for February (the algorithm doesn't take into account leap years, but it
1299
	doesn't seem to matter). My DateTime object, like Microsoft's COleDateTime,
1300
	uses 1-based months (i.e. January is 1, not 0). With that in mind, this
1301
	works:
1302
1303
	long monthIndex = ( ( ( ( 12 % schedule-=GetInterval()) *
1304
1305
	( ( schedule-=GetStartDate().GetYear() - 1601) %
1306
1307
	schedule-=GetInterval())) % schedule-=GetInterval()) +
1308
1309
	( schedule-=GetStartDate().GetMonth() - 1)) % schedule-=GetInterval();
1310
1311
	for( int i = 0; i < monthIndex; i++)
1312
1313
	{
1314
1315
	value += DateTime::GetDaysInMonth( ( i % 12) + 1) * 24 * 60;
1316
1317
	}
1318
1319
	This should work for any recurrence interval, including those greater than
1320
	12.
1321
1322
	14 ULONG N: recurrence interval
1323
1324
	18 ULONG 0 for all events and non-regenerating recurring tasks. 1 for
1325
	regenerating tasks.
1326
1327
	22 ULONG D: day of month the event recurs on (if this value is greater than
1328
	the number of days in a given month [e.g. 31 for and recurs in June], then
1329
	the event will recur on the last day of the month)
1330
1331
	Monthly every N months on the Xth Y (e.g. "2nd Tuesday"):
1332
1333
	10 ULONG See above: same as for monthly every N months on day D
1334
1335
	14 ULONG N: recurrence interval
1336
1337
	18 ULONG 0 for all events and non-regenerating recurring tasks. 1 for
1338
	regenerating tasks.
1339
1340
	22 ULONG Y: bitmask for determining which day of the week the event recurs
1341
	on (see weekly every N weeks). Some useful values are 0x7F for any day, 0x3E
1342
	for a weekday, or 0x41 for a weekend day.
1343
1344
	26 ULONG X: 1 for first occurrence, 2 for second, etc. 5 for last
1345
	occurrence. E.g. for "2nd Tuesday", you should have values of 0x04 for the
1346
	prior value and 2 for this one.
1347
1348
	Yearly on day D of month M:
1349
1350
	10 ULONG M (sort of): This is another messy
1351
	value. It's the number of minute since the startning of the year to the
1352
	given month. For an explanation of GetDaysInMonth, see monthly every N
1353
	months. This will work:
1354
1355
	ULONG monthOfYearInMinutes = 0;
1356
1357
	for( int i = DateTime::cJanuary; i < schedule-=GetMonth(); i++)
1358
1359
	{
1360
1361
	monthOfYearInMinutes += DateTime::GetDaysInMonth( i) * 24 * 60;
1362
1363
	}
1364
1365
1366
1367
	14 ULONG 12: recurrence interval in months. Naturally, 12.
1368
1369
	18 ULONG 0 for all events and non-regenerating recurring tasks. 1 for
1370
	regenerating tasks.
1371
1372
	22 ULONG D: day of month the event recurs on. See monthly every N months on
1373
	day D.
1374
1375
	Yearly on the Xth Y of month M: 10 ULONG M (sort of): See yearly on day D of
1376
	month M.
1377
1378
	14 ULONG 12: recurrence interval in months. Naturally, 12.
1379
1380
	18 ULONG Constant: 0
1381
1382
	22 ULONG Y: see monthly every N months on the Xth Y.
1383
1384
	26 ULONG X: see monthly every N months on the Xth Y.
1385
1386
	After these recurrence-type-specific values, the offsets will change
1387
	depending on the type. For every type except daily every N days, the offsets
1388
	will grow by at least 4. For those types using the Xth Y, the offsets will
1389
	grow by an additional 4, for a total of 8. The offsets for the rest of these
1390
	values will be given for the most basic case, daily every N days, i.e.
1391
	without any growth. Adjust as necessary. Also, the presence of exceptions
1392
	will change the offsets following the exception data by a variable number of
1393
	bytes, so the offsets given in the table are accurate only for those
1394
	recurrence patterns without any exceptions.
1395
1396
1397
	22 UCHAR Type of pattern termination: 0x21 for terminating on a given date, 0x22 for terminating
1398
	after a given number of recurrences, or 0x23 for never terminating
1399
	(recurring infinitely)
1400
1401
	23 UCHARx3 Constant: { 0x20, 0x00, 0x00}
1402
1403
	26 ULONG Number of occurrences in pattern: 0 for infinite recurrence,
1404
	otherwise supply the value, even if it terminates on a given date, not after
1405
	a given number
1406
1407
	30 ULONG Constant: 0
1408
1409
	34 ULONG Number of exceptions to pattern (i.e. deleted or changed
1410
	occurrences)
1411
1412
	.... ULONGxN Base date of each exception, given in hundreds of nanoseconds
1413
	since 1601, so see below to turn them into a comprehensible format. The base
1414
	date of an exception is the date (and only the date-- not the time) the
1415
	exception would have occurred on in the pattern. They must occur in
1416
	ascending order.
1417
1418
	38 ULONG Number of changed exceptions (i.e. total number of exceptions -
1419
	number of deleted exceptions): if there are changed exceptions, again, more
1420
	data will be needed, but that will wait
1421
1422
	.... ULONGxN Start date (and only the date-- not the time) of each changed
1423
	exception, i.e. the exceptions which aren't deleted. These must also occur
1424
	in ascending order. If all of the exceptions are deleted, this data will be
1425
	absent. If present, they will be in the format above. Any dates that are in
1426
	the first list but not in the second are exceptions that have been deleted
1427
	(i.e. the difference between the two sets). Note that this is the start date
1428
	(including time), not the base date. Given that the values are unordered and
1429
	that they can't be matched up against the previous list in this iteration of
1430
	the recurrence data (they could in previous ones), it is very difficult to
1431
	tell which exceptions are deleted and which are changed. Fortunately, for
1432
	this new format, the base dates are given on the attachment representing the
1433
	changed exception (described below), so you can simply ignore this list of
1434
	changed exceptions. Just create a list of exceptions from the previous list
1435
	and assume they're all deleted unless you encounter an attachment with a
1436
	matching base date later on.
1437
1438
	42 ULONG Start date of pattern given in hundreds of nanoseconds since 1601;
1439
	see below for an explanation.
1440
1441
	46 ULONG End date of pattern: see start date of pattern
1442
1443
	50 ULONG Constant: { 0x06, 0x30, 0x00, 0x00}
1444
1445
	NOTE: I find the following 8-byte sequence of bytes to be very useful for
1446
	orienting myself when looking at the raw data. If you can find { 0x06, 0x30,
1447
	0x00, 0x00, 0x08, 0x30, 0x00, 0x00}, you can use these tables to work either
1448
	forwards or backwards to find the data you need. The sequence sort of
1449
	delineates certain critical exception-related data and delineates the
1450
	exceptions themselves from the rest of the data and is relatively easy to
1451
	find. If you're going to be meddling in here a lot, I suggest making a
1452
	friend of ol' 0x00003006.
1453
1454
	54 UCHAR This number is some kind of version indicator. Use 0x08 for Outlook
1455
	2003. I believe 0x06 is Outlook 2000 and possibly 98, while 0x07 is Outlook
1456
	XP. This number must be consistent with the features of the data structure
1457
	generated by the version of Outlook indicated thereby-- there are subtle
1458
	differences between the structures, and, if the version doesn't match the
1459
	data, Outlook will sometimes failto read the structure.
1460
1461
	55 UCHARx3 Constant: { 0x30, 0x00, 0x00}
1462
1463
	58 ULONG Start time of occurrence in minutes: e.g. 0 for midnight or 720 for
1464
	12 PM
1465
1466
	62 ULONG End time of occurrence in minutes: i.e. start time + duration, e.g.
1467
	900 for an event that starts at 12 PM and ends at 3PM
1468
1469
	Exception Data 66 USHORT Number of changed exceptions: essentially a check
1470
	on the prior occurrence of this value; should be equivalent.
1471
1472
	NOTE: The following structure will occur N many times (where N = number of
1473
	changed exceptions), and each structure can be of variable length.
1474
1475
	.... ULONG Start date of changed exception given in hundreds of nanoseconds
1476
	since 1601
1477
1478
	.... ULONG End date of changed exception given in hundreds of nanoseconds
1479
	since 1601
1480
1481
	.... ULONG This is a value I don't clearly understand. It seems to be some
1482
	kind of archival value that matches the start time most of the time, but
1483
	will lag behind when the start time is changed and then match up again under
1484
	certain conditions later. In any case, setting to the same value as the
1485
	start time seems to work just fine (more information on this value would be
1486
	appreciated).
1487
1488
	.... USHORT Bitmask of changes to the exception (see below). This will be 0
1489
	if the only changes to the exception were to its start or end time.
1490
1491
	.... ULONGxN Numeric values (e.g. label or minutes to remind before the
1492
	event) changed in the exception. These will occur in the order of their
1493
	corresponding bits (see below). If no numeric values were changed, then
1494
	these values will be absent.
1495
1496
	NOTE: The following three values constitute a single sub-structure that will
1497
	occur N many times, where N is the number of strings that are changed in the
1498
	exception. Since there are at most 2 string values that can be excepted
1499
	(i.e. subject [or description], and location), there can at most be two of
1500
	these, but there may be none.
1501
1502
	.... USHORT Length of changed string value with NULL character
1503
1504
	.... USHORT Length of changed string value without NULL character (i.e.
1505
	previous value - 1)
1506
1507
	.... CHARxN Changed string value (without NULL terminator)
1508
1509
	Unicode Data NOTE: If a string value was changed on an exception, those
1510
	changed string values will reappear here in Unicode format after 8 bytes of
1511
	NULL padding (possibly a Unicode terminator?). For each exception with a
1512
	changed string value, there will be an identifier, followed by the changed
1513
	strings in Unicode. The strings will occur in the order of their
1514
	corresponding bits (see below). E.g., if both subject and location were
1515
	changed in the exception, there would be the 3-ULONG identifier, then the
1516
	length of the subject, then the subject, then the length of the location,
1517
	then the location.
1518
1519
	70 ULONGx2 Constant: { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}. This
1520
	padding serves as a barrier between the older data structure and the
1521
	appended Unicode data. This is the same sequence as the Unicode terminator,
1522
	but I'm not sure whether that's its identity or not.
1523
1524
	.... ULONGx3 These are the three times used to identify the exception above:
1525
	start date, end date, and repeated start date. These should be the same as
1526
	they were above.
1527
1528
	.... USHORT Length of changed string value without NULL character. This is
1529
	given as count of WCHARs, so it should be identical to the value above.
1530
1531
	.... WCHARxN Changed string value in Unicode (without NULL terminator)
1532
1533
	Terminator ... ULONGxN Constant: { 0x00, 0x00, 0x00, 0x00}. 4 bytes of NULL
1534
	padding per changed exception. If there were no changed exceptions, all
1535
	you'll need is the final terminator below.
1536
1537
	.... ULONGx2 Constant: { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}.
1538
1539
	*/
1540
?>
0 ignored issues
show
Best Practice introduced by
It is not recommended to use PHP's closing tag ?> in files other than templates.

Using a closing tag in PHP files that only contain PHP code is not recommended as you might accidentally add whitespace after the closing tag which would then be output by PHP. This can cause severe problems, for example headers cannot be sent anymore.

A simple precaution is to leave off the closing tag as it is not required, and it also has no negative effects whatsoever.

Loading history...
1541