Issues (203)

class.taskrecurrence.php (1 issue)

1
<?php
2
3
/*
4
 * SPDX-License-Identifier: AGPL-3.0-only
5
 * SPDX-FileCopyrightText: Copyright 2005-2016 Zarafa Deutschland GmbH
6
 * SPDX-FileCopyrightText: Copyright 2020-2022 grommunio GmbH
7
 */
8
9
class TaskRecurrence extends BaseRecurrence {
10
	/**
11
	 * Timezone info which is always false for task.
12
	 *
13
	 * @var false
14
	 */
15
	public $tz = false;
16
17
	private $action;
18
19
	public function __construct($store, $message) {
20
		$this->store = $store;
21
		$this->message = $message;
22
23
		$properties = [];
24
		$properties["entryid"] = PR_ENTRYID;
25
		$properties["parent_entryid"] = PR_PARENT_ENTRYID;
26
		$properties["icon_index"] = PR_ICON_INDEX;
27
		$properties["message_class"] = PR_MESSAGE_CLASS;
28
		$properties["message_flags"] = PR_MESSAGE_FLAGS;
29
		$properties["subject"] = PR_SUBJECT;
30
		$properties["importance"] = PR_IMPORTANCE;
31
		$properties["sensitivity"] = PR_SENSITIVITY;
32
		$properties["last_modification_time"] = PR_LAST_MODIFICATION_TIME;
33
		$properties["status"] = "PT_LONG:PSETID_Task:" . PidLidTaskStatus;
34
		$properties["percent_complete"] = "PT_DOUBLE:PSETID_Task:" . PidLidPercentComplete;
35
		$properties["startdate"] = "PT_SYSTIME:PSETID_Task:" . PidLidTaskStartDate;
36
		$properties["duedate"] = "PT_SYSTIME:PSETID_Task:" . PidLidTaskDueDate;
37
		$properties["reset_reminder"] = "PT_BOOLEAN:PSETID_Task:0x8107";
38
		$properties["dead_occurrence"] = "PT_BOOLEAN:PSETID_Task:0x8109";
39
		$properties["datecompleted"] = "PT_SYSTIME:PSETID_Task:" . PidLidTaskDateCompleted;
40
		$properties["recurring_data"] = "PT_BINARY:PSETID_Task:0x8116";
41
		$properties["actualwork"] = "PT_LONG:PSETID_Task:0x8110";
42
		$properties["totalwork"] = "PT_LONG:PSETID_Task:0x8111";
43
		$properties["complete"] = "PT_BOOLEAN:PSETID_Task:" . PidLidTaskComplete;
44
		$properties["task_f_creator"] = "PT_BOOLEAN:PSETID_Task:0x811e";
45
		$properties["owner"] = "PT_STRING8:PSETID_Task:0x811f";
46
		$properties["recurring"] = "PT_BOOLEAN:PSETID_Task:0x8126";
47
48
		$properties["reminder_minutes"] = "PT_LONG:PSETID_Common:" . PidLidReminderDelta;
49
		$properties["reminder_time"] = "PT_SYSTIME:PSETID_Common:" . PidLidReminderTime;
50
		$properties["reminder"] = "PT_BOOLEAN:PSETID_Common:" . PidLidReminderSet;
51
52
		$properties["private"] = "PT_BOOLEAN:PSETID_Common:" . PidLidPrivate;
53
		$properties["contacts"] = "PT_MV_STRING8:PSETID_Common:0x853a";
54
		$properties["contacts_string"] = "PT_STRING8:PSETID_Common:0x8586";
55
		$properties["categories"] = "PT_MV_STRING8:PS_PUBLIC_STRINGS:Keywords";
56
57
		$properties["commonstart"] = "PT_SYSTIME:PSETID_Common:0x8516";
58
		$properties["commonend"] = "PT_SYSTIME:PSETID_Common:0x8517";
59
		$properties["commonassign"] = "PT_LONG:PSETID_Common:0x8518";
60
		$properties["flagdueby"] = "PT_SYSTIME:PSETID_Common:" . PidLidReminderSignalTime;
61
		$properties["side_effects"] = "PT_LONG:PSETID_Common:0x8510";
62
63
		$this->proptags = getPropIdsFromStrings($store, $properties);
64
65
		parent::__construct($store, $message);
66
	}
67
68
	/**
69
	 * Function which saves recurrence and also regenerates task if necessary.
70
	 *
71
	 * @param mixed $recur new recurrence properties
72
	 *
73
	 * @return array|bool of properties of regenerated task else false
74
	 */
75
	public function setRecurrence(&$recur) {
76
		$this->recur = $recur;
77
		$this->action = &$recur;
78
79
		if (!isset($this->recur["changed_occurrences"])) {
80
			$this->recur["changed_occurrences"] = [];
81
		}
82
83
		if (!isset($this->recur["deleted_occurrences"])) {
84
			$this->recur["deleted_occurrences"] = [];
85
		}
86
87
		if (!isset($this->recur['startocc'])) {
88
			$this->recur['startocc'] = 0;
89
		}
90
		if (!isset($this->recur['endocc'])) {
91
			$this->recur['endocc'] = 0;
92
		}
93
94
		// Save recurrence because we need proper startrecurrdate and endrecurrdate
95
		$this->saveRecurrence();
96
97
		// Update $this->recur with proper startrecurrdate and endrecurrdate updated after saving recurrence
98
		$msgProps = mapi_getprops($this->message, [$this->proptags['recurring_data']]);
99
		$recurring_data = $this->parseRecurrence($msgProps[$this->proptags['recurring_data']]);
100
		foreach ($recurring_data as $key => $value) {
101
			$this->recur[$key] = $value;
102
		}
103
104
		$this->setFirstOccurrence();
105
106
		// Let's see if next occurrence has to be generated
107
		return $this->moveToNextOccurrence();
108
	}
109
110
	/**
111
	 * Sets task object to first occurrence if startdate/duedate of task object is different from first occurrence.
112
	 */
113
	public function setFirstOccurrence() {
114
		// Check if it is already the first occurrence
115
		if ($this->action['start'] == $this->recur["start"]) {
116
			return;
117
		}
118
		$items = $this->getNextOccurrence();
119
120
		$props = [];
121
		$props[$this->proptags['startdate']] = $items[$this->proptags['startdate']];
122
		$props[$this->proptags['commonstart']] = $items[$this->proptags['startdate']];
123
124
		$props[$this->proptags['duedate']] = $items[$this->proptags['duedate']];
125
		$props[$this->proptags['commonend']] = $items[$this->proptags['duedate']];
126
127
		mapi_setprops($this->message, $props);
128
	}
129
130
	/**
131
	 * Function which creates new task as current occurrence and moves the
132
	 * existing task to next occurrence.
133
	 *
134
	 * @return array|bool properties of newly created task if moving to next occurrence succeeds
135
	 *                    false if that was last occurrence
136
	 */
137
	public function moveToNextOccurrence() {
138
		$result = false;
139
		/*
140
		 * Every recurring task should have a 'duedate'. If a recurring task is created with no start/end date
141
		 * then we create first two occurrence separately and for first occurrence recurrence has ended.
142
		 */
143
		if ((empty($this->action['startdate']) && empty($this->action['duedate'])) ||
144
			($this->action['complete'] == 1) || (isset($this->action['deleteOccurrence']) && $this->action['deleteOccurrence'])) {
145
			$nextOccurrence = $this->getNextOccurrence();
146
			$result = mapi_getprops($this->message, [PR_ENTRYID, PR_PARENT_ENTRYID, PR_STORE_ENTRYID]);
147
148
			$props = [];
149
			if (!empty($nextOccurrence)) {
150
				if (!isset($this->action['deleteOccurrence'])) {
151
					// Create current occurrence as separate task
152
					$result = $this->regenerateTask($this->action['complete']);
153
				}
154
155
				// Set reminder for next occurrence
156
				$this->setReminder($nextOccurrence);
157
158
				// Update properties for next occurrence
159
				$this->action['duedate'] = $props[$this->proptags['duedate']] = $nextOccurrence[$this->proptags['duedate']];
160
				$this->action['commonend'] = $props[$this->proptags['commonend']] = $nextOccurrence[$this->proptags['duedate']];
161
162
				$this->action['startdate'] = $props[$this->proptags['startdate']] = $nextOccurrence[$this->proptags['startdate']];
163
				$this->action['commonstart'] = $props[$this->proptags['commonstart']] = $nextOccurrence[$this->proptags['startdate']];
164
165
				// If current task as been mark as 'Complete' then next occurrence should be incomplete.
166
				if (isset($this->action['complete']) && $this->action['complete'] == 1) {
167
					$this->action['status'] = $props[$this->proptags["status"]] = olTaskNotStarted;
168
					$this->action['complete'] = $props[$this->proptags["complete"]] = false;
169
					$this->action['percent_complete'] = $props[$this->proptags["percent_complete"]] = 0;
170
				}
171
172
				$props[$this->proptags["dead_occurrence"]] = false;
173
			}
174
			else {
175
				if (isset($this->action['deleteOccurrence']) && $this->action['deleteOccurrence']) {
176
					return false;
177
				}
178
179
				// Didn't get next occurrence, probably this is the last one, so recurrence ends here
180
				$props[$this->proptags["dead_occurrence"]] = true;
181
				$props[$this->proptags["datecompleted"]] = $this->action['datecompleted'];
182
				$props[$this->proptags["task_f_creator"]] = true;
183
184
				// OL props
185
				$props[$this->proptags["side_effects"]] = 1296;
186
				$props[$this->proptags["icon_index"]] = 1280;
187
			}
188
189
			mapi_setprops($this->message, $props);
190
		}
191
192
		return $result;
193
	}
194
195
	/**
196
	 * Function which return properties of next occurrence.
197
	 *
198
	 * @return null|array|false|T startdate/enddate of next occurrence
199
	 */
200
	public function getNextOccurrence() {
201
		if ($this->recur) {
202
			// @TODO: fix start of range
203
			$start = $this->messageprops[$this->proptags["duedate"]] ?? $this->action['start'];
204
			$dayend = ($this->recur['term'] == 0x23) ? 0x7FFFFFFF : $this->dayStartOf($this->recur["end"]);
205
206
			// Fix recur object
207
			$this->recur['startocc'] = 0;
208
			$this->recur['endocc'] = 0;
209
210
			// Retrieve next occurrence
211
			$items = $this->getItems($start, $dayend, 1);
212
213
			return !empty($items) ? $items[0] : false;
214
		}
215
	}
216
217
	/**
218
	 * Function which clones current occurrence and sets appropriate properties.
219
	 * The original recurring item is moved to next occurrence.
220
	 *
221
	 * @param bool $markComplete true if existing occurrence has to be marked complete
222
	 */
223
	public function regenerateTask($markComplete) {
224
		// Get all properties
225
		$taskItemProps = mapi_getprops($this->message);
226
227
		if (isset($this->action["subject"])) {
228
			$taskItemProps[$this->proptags["subject"]] = $this->action["subject"];
229
		}
230
		if (isset($this->action["importance"])) {
231
			$taskItemProps[$this->proptags["importance"]] = $this->action["importance"];
232
		}
233
		if (isset($this->action["startdate"])) {
234
			$taskItemProps[$this->proptags["startdate"]] = $this->action["startdate"];
235
			$taskItemProps[$this->proptags["commonstart"]] = $this->action["startdate"];
236
		}
237
		if (isset($this->action["duedate"])) {
238
			$taskItemProps[$this->proptags["duedate"]] = $this->action["duedate"];
239
			$taskItemProps[$this->proptags["commonend"]] = $this->action["duedate"];
240
		}
241
242
		$folder = mapi_msgstore_openentry($this->store, $taskItemProps[PR_PARENT_ENTRYID]);
243
		$newMessage = mapi_folder_createmessage($folder);
244
245
		$taskItemProps[$this->proptags["status"]] = $markComplete ? olTaskComplete : olTaskNotStarted;
246
		$taskItemProps[$this->proptags["complete"]] = $markComplete;
247
		$taskItemProps[$this->proptags["percent_complete"]] = $markComplete ? 1 : 0;
248
249
		// This occurrence has been marked as 'Complete' so disable reminder
250
		if ($markComplete) {
251
			$taskItemProps[$this->proptags["reset_reminder"]] = false;
252
			$taskItemProps[$this->proptags["reminder"]] = false;
253
			$taskItemProps[$this->proptags["datecompleted"]] = $this->action["datecompleted"];
254
255
			unset($this->action[$this->proptags['datecompleted']]);
256
		}
257
258
		// Recurrence ends for this item
259
		$taskItemProps[$this->proptags["dead_occurrence"]] = true;
260
		$taskItemProps[$this->proptags["task_f_creator"]] = true;
261
262
		// OL props
263
		$taskItemProps[$this->proptags["side_effects"]] = 1296;
264
		$taskItemProps[$this->proptags["icon_index"]] = 1280;
265
266
		// Copy recipients
267
		$recipienttable = mapi_message_getrecipienttable($this->message);
268
		$recipients = mapi_table_queryallrows($recipienttable, [PR_ENTRYID, PR_DISPLAY_NAME, PR_EMAIL_ADDRESS, PR_RECIPIENT_ENTRYID, PR_RECIPIENT_TYPE, PR_SEND_INTERNET_ENCODING, PR_SEND_RICH_INFO, PR_RECIPIENT_DISPLAY_NAME, PR_ADDRTYPE, PR_DISPLAY_TYPE, PR_RECIPIENT_TRACKSTATUS, PR_RECIPIENT_TRACKSTATUS_TIME, PR_RECIPIENT_FLAGS, PR_ROWID]);
269
270
		$copy_to_recipientTable = mapi_message_getrecipienttable($newMessage);
271
		$copy_to_recipientRows = mapi_table_queryallrows($copy_to_recipientTable, [PR_ROWID]);
272
		foreach ($copy_to_recipientRows as $recipient) {
273
			mapi_message_modifyrecipients($newMessage, MODRECIP_REMOVE, [$recipient]);
274
		}
275
		mapi_message_modifyrecipients($newMessage, MODRECIP_ADD, $recipients);
276
277
		// Copy attachments
278
		$attachmentTable = mapi_message_getattachmenttable($this->message);
279
		if ($attachmentTable) {
0 ignored issues
show
$attachmentTable is of type resource, thus it always evaluated to true.
Loading history...
280
			$attachments = mapi_table_queryallrows($attachmentTable, [PR_ATTACH_NUM, PR_ATTACH_SIZE, PR_ATTACH_LONG_FILENAME, PR_ATTACHMENT_HIDDEN, PR_DISPLAY_NAME, PR_ATTACH_METHOD]);
281
282
			foreach ($attachments as $attach_props) {
283
				$attach_old = mapi_message_openattach($this->message, (int) $attach_props[PR_ATTACH_NUM]);
284
				$attach_newResourceMsg = mapi_message_createattach($newMessage);
285
286
				mapi_copyto($attach_old, [], [], $attach_newResourceMsg, 0);
287
				mapi_savechanges($attach_newResourceMsg);
288
			}
289
		}
290
291
		mapi_setprops($newMessage, $taskItemProps);
292
		mapi_savechanges($newMessage);
293
294
		// Update body of original message
295
		$msgbody = mapi_openproperty($this->message, PR_BODY);
296
		$msgbody = trim($msgbody, "\0");
297
		$separator = "------------\r\n";
298
299
		if (!empty($msgbody) && strrpos($msgbody, $separator) === false) {
300
			$msgbody = $separator . $msgbody;
301
			$stream = mapi_openproperty($this->message, PR_BODY, IID_IStream, STGM_TRANSACTED, 0);
302
			mapi_stream_setsize($stream, strlen($msgbody));
303
			mapi_stream_write($stream, $msgbody);
304
			mapi_stream_commit($stream);
305
		}
306
307
		// We need these properties to notify client
308
		return mapi_getprops($newMessage, [PR_ENTRYID, PR_PARENT_ENTRYID, PR_STORE_ENTRYID]);
309
	}
310
311
	/**
312
	 * processOccurrenceItem, adds an item to a list of occurrences, but only if the
313
	 * resulting occurrence starts or ends in the interval <$start, $end>.
314
	 *
315
	 * @param array $items        reference to the array to be added to
316
	 * @param int   $start        start of timeframe in GMT TIME
317
	 * @param int   $end          end of timeframe in GMT TIME
318
	 * @param int   $basedate     (hour/sec/min assumed to be 00:00:00) in LOCAL TIME OF THE OCCURRENCE
319
	 * @param int   $startocc     start of occurrence since beginning of day in minutes
320
	 * @param int   $endocc       end of occurrence since beginning of day in minutes
321
	 * @param int   $tz           the timezone info for this occurrence ( applied to $basedate / $startocc / $endocc )
322
	 * @param bool  $reminderonly If TRUE, only add the item if the reminder is set
323
	 */
324
	public function processOccurrenceItem(&$items, $start, $end, $basedate, $startocc, $endocc, $tz, $reminderonly) {
325
		if ($basedate > $start) {
326
			$newItem = [];
327
			$newItem[$this->proptags['startdate']] = $basedate;
328
329
			// If startdate and enddate are set on task, then slide enddate according to duration
330
			if (isset($this->messageprops[$this->proptags["startdate"]], $this->messageprops[$this->proptags["duedate"]])) {
331
				$newItem[$this->proptags['duedate']] = $newItem[$this->proptags['startdate']] + ($this->messageprops[$this->proptags["duedate"]] - $this->messageprops[$this->proptags["startdate"]]);
332
			}
333
			else {
334
				$newItem[$this->proptags['duedate']] = $newItem[$this->proptags['startdate']];
335
			}
336
337
			$items[] = $newItem;
338
		}
339
	}
340
341
	/**
342
	 * Function which marks existing occurrence to 'Complete'.
343
	 *
344
	 * @param array $recur array action from client
345
	 *
346
	 * @return array|bool of properties of regenerated task else false
347
	 */
348
	public function markOccurrenceComplete(&$recur) {
349
		// Fix timezone object
350
		$this->tz = false;
351
		$this->action = &$recur;
352
		$dead_occurrence = $this->messageprops[$this->proptags['dead_occurrence']] ?? false;
353
354
		if (!$dead_occurrence) {
355
			return $this->moveToNextOccurrence();
356
		}
357
358
		return false;
359
	}
360
361
	/**
362
	 * Function which sets reminder on recurring task after existing occurrence has been deleted or marked complete.
363
	 *
364
	 * @param mixed $nextOccurrence properties of next occurrence
365
	 */
366
	public function setReminder($nextOccurrence): void {
367
		$props = [];
368
		if (!empty($nextOccurrence)) {
369
			// Check if reminder is reset. Default is 'false'
370
			$reset_reminder = $this->messageprops[$this->proptags['reset_reminder']] ?? false;
371
			$reminder = $this->messageprops[$this->proptags['reminder']];
372
373
			// Either reminder was already set OR reminder was set but was dismissed bty user
374
			if ($reminder || $reset_reminder) {
375
				// Reminder can be set at any time either before or after the duedate, so get duration between the reminder time and duedate
376
				$reminder_time = $this->messageprops[$this->proptags['reminder_time']] ?? 0;
377
				$reminder_difference = $this->messageprops[$this->proptags['duedate']] ?? 0;
378
				$reminder_difference = $reminder_difference - $reminder_time;
379
380
				// Apply duration to next calculated duedate
381
				$next_reminder_time = $nextOccurrence[$this->proptags['duedate']] - $reminder_difference;
382
383
				$props[$this->proptags['reminder_time']] = $next_reminder_time;
384
				$props[$this->proptags['flagdueby']] = $next_reminder_time;
385
				$this->action['reminder'] = $props[$this->proptags['reminder']] = true;
386
			}
387
		}
388
		else {
389
			// Didn't get next occurrence, probably this is the last occurrence
390
			$props[$this->proptags['reminder']] = false;
391
			$props[$this->proptags['reset_reminder']] = false;
392
		}
393
394
		if (!empty($props)) {
395
			mapi_setprops($this->message, $props);
396
		}
397
	}
398
399
	/**
400
	 * Function which recurring task to next occurrence.
401
	 * It simply doesn't regenerate task.
402
	 *
403
	 * @param array $action
404
	 *
405
	 * @return array|bool
406
	 */
407
	public function deleteOccurrence($action) {
408
		$this->tz = false;
409
		$this->action = $action;
410
		$result = $this->moveToNextOccurrence();
411
412
		mapi_savechanges($this->message);
413
414
		return $result;
415
	}
416
}
417