TaskRecurrence   B
last analyzed

Complexity

Total Complexity 45

Size/Duplication

Total Lines 396
Duplicated Lines 0 %

Importance

Changes 8
Bugs 1 Features 3
Metric Value
eloc 191
c 8
b 1
f 3
dl 0
loc 396
rs 8.8
wmc 45

10 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 47 1
C moveToNextOccurrence() 0 56 12
A setFirstOccurrence() 0 15 2
A setRecurrence() 0 23 2
F regenerateTask() 0 86 13
A deleteOccurrence() 0 8 1
A processOccurrenceItem() 0 17 3
A markOccurrenceComplete() 0 11 2
A getNextOccurrence() 0 17 4
A setReminder() 0 30 5

How to fix   Complexity   

Complex Class

Complex classes like TaskRecurrence often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use TaskRecurrence, and based on these observations, apply Extract Interface, too.

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 array|false|null 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
		return null;
207
	}
208
209
	/**
210
	 * Function which clones current occurrence and sets appropriate properties.
211
	 * The original recurring item is moved to next occurrence.
212
	 *
213
	 * @param bool $markComplete true if existing occurrence has to be marked complete
214
	 */
215
	public function regenerateTask(bool $markComplete): array {
216
		// Get all properties
217
		$taskItemProps = mapi_getprops($this->message);
218
219
		if (isset($this->action["subject"])) {
220
			$taskItemProps[$this->proptags["subject"]] = $this->action["subject"];
221
		}
222
		if (isset($this->action["importance"])) {
223
			$taskItemProps[$this->proptags["importance"]] = $this->action["importance"];
224
		}
225
		if (isset($this->action["startdate"])) {
226
			$taskItemProps[$this->proptags["startdate"]] = $this->action["startdate"];
227
			$taskItemProps[$this->proptags["commonstart"]] = $this->action["startdate"];
228
		}
229
		if (isset($this->action["duedate"])) {
230
			$taskItemProps[$this->proptags["duedate"]] = $this->action["duedate"];
231
			$taskItemProps[$this->proptags["commonend"]] = $this->action["duedate"];
232
		}
233
234
		$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

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

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