Passed
Push — master ( ae800f...172061 )
by
unknown
02:27
created

TaskRecurrence::setRecurrence()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 23
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
cc 2
eloc 13
c 3
b 0
f 0
nc 2
nop 1
dl 0
loc 23
rs 9.8333
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(mixed $store, mixed $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["date_completed"] = "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(mixed &$recur): array|bool {
76
		$this->recur = $recur;
77
		$this->action = &$recur;
78
79
		$this->recur["changed_occurrences"] ??= [];
80
		$this->recur["deleted_occurrences"] ??= [];
81
		$this->recur['startocc'] ??= 0;
82
		$this->recur['endocc'] ??= 0;
83
84
		// Save recurrence because we need proper startrecurrdate and endrecurrdate
85
		$this->saveRecurrence();
86
87
		// Update $this->recur with proper startrecurrdate and endrecurrdate updated after saving recurrence
88
		$msgProps = mapi_getprops($this->message, [$this->proptags['recurring_data']]);
89
		$recurring_data = $this->parseRecurrence($msgProps[$this->proptags['recurring_data']]);
90
		foreach ($recurring_data as $key => $value) {
91
			$this->recur[$key] = $value;
92
		}
93
94
		$this->setFirstOccurrence();
95
96
		// Let's see if next occurrence has to be generated
97
		return $this->moveToNextOccurrence();
98
	}
99
100
	/**
101
	 * Sets task object to first occurrence if startdate/duedate of task object is different from first occurrence.
102
	 */
103
	public function setFirstOccurrence(): void {
104
		// Check if it is already the first occurrence
105
		if ($this->action['start'] == $this->recur["start"]) {
106
			return;
107
		}
108
		$items = $this->getNextOccurrence();
109
110
		$props = [];
111
		$props[$this->proptags['startdate']] = $items[$this->proptags['startdate']];
112
		$props[$this->proptags['commonstart']] = $items[$this->proptags['startdate']];
113
114
		$props[$this->proptags['duedate']] = $items[$this->proptags['duedate']];
115
		$props[$this->proptags['commonend']] = $items[$this->proptags['duedate']];
116
117
		mapi_setprops($this->message, $props);
118
	}
119
120
	/**
121
	 * Function which creates new task as current occurrence and moves the
122
	 * existing task to next occurrence.
123
	 *
124
	 * @return array|bool properties of newly created task if moving to next occurrence succeeds
125
	 *                    false if that was last occurrence
126
	 */
127
	public function moveToNextOccurrence(): array|bool {
128
		$result = false;
129
		/*
130
		 * Every recurring task should have a 'duedate'. If a recurring task is created with no start/end date
131
		 * then we create first two occurrence separately and for first occurrence recurrence has ended.
132
		 */
133
		if ((empty($this->action['startdate']) && empty($this->action['duedate'])) ||
134
			($this->action['complete'] == 1) || (isset($this->action['deleteOccurrence']) && $this->action['deleteOccurrence'])) {
135
			$nextOccurrence = $this->getNextOccurrence();
136
			$result = mapi_getprops($this->message, [PR_ENTRYID, PR_PARENT_ENTRYID, PR_STORE_ENTRYID]);
137
138
			$props = [];
139
			if (!empty($nextOccurrence)) {
140
				if (!isset($this->action['deleteOccurrence'])) {
141
					// Create current occurrence as separate task
142
					$result = $this->regenerateTask($this->action['complete']);
143
				}
144
145
				// Set reminder for next occurrence
146
				$this->setReminder($nextOccurrence);
147
148
				// Update properties for next occurrence
149
				$this->action['duedate'] = $props[$this->proptags['duedate']] = $nextOccurrence[$this->proptags['duedate']];
150
				$this->action['commonend'] = $props[$this->proptags['commonend']] = $nextOccurrence[$this->proptags['duedate']];
151
152
				$this->action['startdate'] = $props[$this->proptags['startdate']] = $nextOccurrence[$this->proptags['startdate']];
153
				$this->action['commonstart'] = $props[$this->proptags['commonstart']] = $nextOccurrence[$this->proptags['startdate']];
154
155
				// If current task as been mark as 'Complete' then next occurrence should be incomplete.
156
				if (isset($this->action['complete']) && $this->action['complete'] == 1) {
157
					$this->action['status'] = $props[$this->proptags["status"]] = olTaskNotStarted;
158
					$this->action['complete'] = $props[$this->proptags["complete"]] = false;
159
					$this->action['percent_complete'] = $props[$this->proptags["percent_complete"]] = 0;
160
				}
161
162
				$props[$this->proptags["dead_occurrence"]] = false;
163
			}
164
			else {
165
				if (isset($this->action['deleteOccurrence']) && $this->action['deleteOccurrence']) {
166
					return false;
167
				}
168
169
				// Didn't get next occurrence, probably this is the last one, so recurrence ends here
170
				$props[$this->proptags["dead_occurrence"]] = true;
171
				$props[$this->proptags["date_completed"]] = $this->action['date_completed'];
172
				$props[$this->proptags["task_f_creator"]] = true;
173
174
				// OL props
175
				$props[$this->proptags["side_effects"]] = 1296;
176
				$props[$this->proptags["icon_index"]] = 1280;
177
			}
178
179
			mapi_setprops($this->message, $props);
180
		}
181
182
		return $result;
183
	}
184
185
	/**
186
	 * Function which return properties of next occurrence.
187
	 *
188
	 * @return null|array|false|T startdate/enddate of next occurrence
189
	 */
190
	public function getNextOccurrence(): mixed {
191
		if ($this->recur) {
192
			// @TODO: fix start of range
193
			$start = $this->messageprops[$this->proptags["duedate"]] ?? $this->action['start'];
194
			$dayend = ($this->recur['term'] == 0x23) ? 0x7FFFFFFF : $this->dayStartOf($this->recur["end"]);
195
196
			// Fix recur object
197
			$this->recur['startocc'] = 0;
198
			$this->recur['endocc'] = 0;
199
200
			// Retrieve next occurrence
201
			$items = $this->getItems($start, $dayend, 1);
202
203
			return !empty($items) ? $items[0] : false;
204
		}
205
	}
206
207
	/**
208
	 * Function which clones current occurrence and sets appropriate properties.
209
	 * The original recurring item is moved to next occurrence.
210
	 *
211
	 * @param bool $markComplete true if existing occurrence has to be marked complete
212
	 */
213
	public function regenerateTask(bool $markComplete): array {
214
		// Get all properties
215
		$taskItemProps = mapi_getprops($this->message);
216
217
		if (isset($this->action["subject"])) {
218
			$taskItemProps[$this->proptags["subject"]] = $this->action["subject"];
219
		}
220
		if (isset($this->action["importance"])) {
221
			$taskItemProps[$this->proptags["importance"]] = $this->action["importance"];
222
		}
223
		if (isset($this->action["startdate"])) {
224
			$taskItemProps[$this->proptags["startdate"]] = $this->action["startdate"];
225
			$taskItemProps[$this->proptags["commonstart"]] = $this->action["startdate"];
226
		}
227
		if (isset($this->action["duedate"])) {
228
			$taskItemProps[$this->proptags["duedate"]] = $this->action["duedate"];
229
			$taskItemProps[$this->proptags["commonend"]] = $this->action["duedate"];
230
		}
231
232
		$folder = mapi_msgstore_openentry($this->store, $taskItemProps[PR_PARENT_ENTRYID]);
0 ignored issues
show
Bug introduced by
$this->store of type resource is incompatible with the type resource expected by parameter $store of mapi_msgstore_openentry(). ( Ignorable by Annotation )

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

232
		$folder = mapi_msgstore_openentry(/** @scrutinizer ignore-type */ $this->store, $taskItemProps[PR_PARENT_ENTRYID]);
Loading history...
233
		$newMessage = mapi_folder_createmessage($folder);
234
235
		$taskItemProps[$this->proptags["status"]] = $markComplete ? olTaskComplete : olTaskNotStarted;
236
		$taskItemProps[$this->proptags["complete"]] = $markComplete;
237
		$taskItemProps[$this->proptags["percent_complete"]] = $markComplete ? 1 : 0;
238
239
		// This occurrence has been marked as 'Complete' so disable reminder
240
		if ($markComplete) {
241
			$taskItemProps[$this->proptags["reset_reminder"]] = false;
242
			$taskItemProps[$this->proptags["reminder"]] = false;
243
			$taskItemProps[$this->proptags["date_completed"]] = $this->action["date_completed"];
244
245
			unset($this->action[$this->proptags['date_completed']]);
246
		}
247
248
		// Recurrence ends for this item
249
		$taskItemProps[$this->proptags["dead_occurrence"]] = true;
250
		$taskItemProps[$this->proptags["task_f_creator"]] = true;
251
252
		// OL props
253
		$taskItemProps[$this->proptags["side_effects"]] = 1296;
254
		$taskItemProps[$this->proptags["icon_index"]] = 1280;
255
256
		// Copy recipients
257
		$recipienttable = mapi_message_getrecipienttable($this->message);
258
		$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]);
259
260
		$copy_to_recipientTable = mapi_message_getrecipienttable($newMessage);
261
		$copy_to_recipientRows = mapi_table_queryallrows($copy_to_recipientTable, [PR_ROWID]);
262
		foreach ($copy_to_recipientRows as $recipient) {
263
			mapi_message_modifyrecipients($newMessage, MODRECIP_REMOVE, [$recipient]);
264
		}
265
		mapi_message_modifyrecipients($newMessage, MODRECIP_ADD, $recipients);
266
267
		// Copy attachments
268
		$attachmentTable = mapi_message_getattachmenttable($this->message);
269
		if ($attachmentTable) {
0 ignored issues
show
introduced by
$attachmentTable is of type resource, thus it always evaluated to true.
Loading history...
270
			$attachments = mapi_table_queryallrows($attachmentTable, [PR_ATTACH_NUM, PR_ATTACH_SIZE, PR_ATTACH_LONG_FILENAME, PR_ATTACHMENT_HIDDEN, PR_DISPLAY_NAME, PR_ATTACH_METHOD]);
271
272
			foreach ($attachments as $attach_props) {
273
				$attach_old = mapi_message_openattach($this->message, (int) $attach_props[PR_ATTACH_NUM]);
274
				$attach_newResourceMsg = mapi_message_createattach($newMessage);
275
276
				mapi_copyto($attach_old, [], [], $attach_newResourceMsg, 0);
277
				mapi_savechanges($attach_newResourceMsg);
278
			}
279
		}
280
281
		mapi_setprops($newMessage, $taskItemProps);
282
		mapi_savechanges($newMessage);
283
284
		// Update body of original message
285
		$msgbody = mapi_openproperty($this->message, PR_BODY);
286
		$msgbody = trim($msgbody, "\0");
0 ignored issues
show
Bug introduced by
$msgbody of type resource is incompatible with the type string expected by parameter $string of trim(). ( Ignorable by Annotation )

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

286
		$msgbody = trim(/** @scrutinizer ignore-type */ $msgbody, "\0");
Loading history...
287
		$separator = "------------\r\n";
288
289
		if (!empty($msgbody) && strrpos($msgbody, $separator) === false) {
290
			$msgbody = $separator . $msgbody;
291
			$stream = mapi_openproperty($this->message, PR_BODY, IID_IStream, STGM_TRANSACTED, 0);
292
			mapi_stream_setsize($stream, strlen($msgbody));
293
			mapi_stream_write($stream, $msgbody);
294
			mapi_stream_commit($stream);
295
		}
296
297
		// We need these properties to notify client
298
		return mapi_getprops($newMessage, [PR_ENTRYID, PR_PARENT_ENTRYID, PR_STORE_ENTRYID]);
299
	}
300
301
	/**
302
	 * processOccurrenceItem, adds an item to a list of occurrences, but only if the
303
	 * resulting occurrence starts or ends in the interval <$start, $end>.
304
	 *
305
	 * @param array $items        reference to the array to be added to
306
	 * @param int   $start        start of timeframe in GMT TIME
307
	 * @param int   $end          end of timeframe in GMT TIME
308
	 * @param int   $basedate     (hour/sec/min assumed to be 00:00:00) in LOCAL TIME OF THE OCCURRENCE
309
	 * @param int   $startocc     start of occurrence since beginning of day in minutes
310
	 * @param int   $endocc       end of occurrence since beginning of day in minutes
311
	 * @param int   $tz           the timezone info for this occurrence ( applied to $basedate / $startocc / $endocc )
312
	 * @param bool  $reminderonly If TRUE, only add the item if the reminder is set
313
	 */
314
	public function processOccurrenceItem(array &$items, mixed $start, int $end, mixed $basedate, mixed $startocc, mixed $endocc, mixed $tz, mixed $reminderonly): ?false {
315
		if ($basedate > $start) {
316
			$newItem = [];
317
			$newItem[$this->proptags['startdate']] = $basedate;
318
319
			// If startdate and enddate are set on task, then slide enddate according to duration
320
			if (isset($this->messageprops[$this->proptags["startdate"]], $this->messageprops[$this->proptags["duedate"]])) {
321
				$newItem[$this->proptags['duedate']] = $newItem[$this->proptags['startdate']] + ($this->messageprops[$this->proptags["duedate"]] - $this->messageprops[$this->proptags["startdate"]]);
322
			}
323
			else {
324
				$newItem[$this->proptags['duedate']] = $newItem[$this->proptags['startdate']];
325
			}
326
327
			$items[] = $newItem;
328
		}
329
330
		return null;
331
	}
332
333
	/**
334
	 * Function which marks existing occurrence to 'Complete'.
335
	 *
336
	 * @param array $recur array action from client
337
	 *
338
	 * @return array|bool of properties of regenerated task else false
339
	 */
340
	public function markOccurrenceComplete(array &$recur): array|bool {
341
		// Fix timezone object
342
		$this->tz = false;
343
		$this->action = &$recur;
344
		$dead_occurrence = $this->messageprops[$this->proptags['dead_occurrence']] ?? false;
345
346
		if (!$dead_occurrence) {
347
			return $this->moveToNextOccurrence();
348
		}
349
350
		return false;
351
	}
352
353
	/**
354
	 * Function which sets reminder on recurring task after existing occurrence has been deleted or marked complete.
355
	 *
356
	 * @param mixed $nextOccurrence properties of next occurrence
357
	 */
358
	public function setReminder(mixed $nextOccurrence): void {
359
		$props = [];
360
		if (!empty($nextOccurrence)) {
361
			// Check if reminder is reset. Default is 'false'
362
			$reset_reminder = $this->messageprops[$this->proptags['reset_reminder']] ?? false;
363
			$reminder = $this->messageprops[$this->proptags['reminder']];
364
365
			// Either reminder was already set OR reminder was set but was dismissed bty user
366
			if ($reminder || $reset_reminder) {
367
				// Reminder can be set at any time either before or after the duedate, so get duration between the reminder time and duedate
368
				$reminder_time = $this->messageprops[$this->proptags['reminder_time']] ?? 0;
369
				$reminder_difference = $this->messageprops[$this->proptags['duedate']] ?? 0;
370
				$reminder_difference = $reminder_difference - $reminder_time;
371
372
				// Apply duration to next calculated duedate
373
				$next_reminder_time = $nextOccurrence[$this->proptags['duedate']] - $reminder_difference;
374
375
				$props[$this->proptags['reminder_time']] = $next_reminder_time;
376
				$props[$this->proptags['flagdueby']] = $next_reminder_time;
377
				$this->action['reminder'] = $props[$this->proptags['reminder']] = true;
378
			}
379
		}
380
		else {
381
			// Didn't get next occurrence, probably this is the last occurrence
382
			$props[$this->proptags['reminder']] = false;
383
			$props[$this->proptags['reset_reminder']] = false;
384
		}
385
386
		if (!empty($props)) {
387
			mapi_setprops($this->message, $props);
388
		}
389
	}
390
391
	/**
392
	 * Function which recurring task to next occurrence.
393
	 * It simply doesn't regenerate task.
394
	 */
395
	public function deleteOccurrence(array $action): array|bool {
396
		$this->tz = false;
397
		$this->action = $action;
398
		$result = $this->moveToNextOccurrence();
399
400
		mapi_savechanges($this->message);
401
402
		return $result;
403
	}
404
}
405