Issues (203)

mapi.util.php (2 issues)

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-2024 grommunio GmbH
7
 */
8
9
define('NOERROR', 0);
10
11
// Load all mapi defs
12
mapi_load_mapidefs(1);
13
14
/**
15
 * Function to make a MAPIGUID from a php string.
16
 * The C++ definition for the GUID is:
17
 *  typedef struct _GUID
18
 *  {
19
 *   unsigned long        Data1;
20
 *   unsigned short       Data2;
21
 *   unsigned short       Data3;
22
 *   unsigned char        Data4[8];
23
 *  } GUID;.
24
 *
25
 * A GUID is normally represented in the following form:
26
 *  {00062008-0000-0000-C000-000000000046}
27
 *
28
 * @param string $guid
29
 */
30
function makeGuid($guid): string {
31
	return pack("vvvv", hexdec(substr($guid, 5, 4)), hexdec(substr($guid, 1, 4)), hexdec(substr($guid, 10, 4)), hexdec(substr($guid, 15, 4))) . hex2bin(substr($guid, 20, 4)) . hex2bin(substr($guid, 25, 12));
32
}
33
34
/**
35
 * Function to get a human readable string from a MAPI error code.
36
 *
37
 * @param mixed $errcode the MAPI error code, if not given, we use mapi_last_hresult
38
 *
39
 * @return string The defined name for the MAPI error code
40
 */
41
function get_mapi_error_name($errcode = null) {
42
	if ($errcode === null) {
43
		$errcode = mapi_last_hresult();
44
	}
45
46
	if (strcasecmp(substr($errcode, 0, 2), '0x') === 0) {
47
		$errcode = hexdec($errcode);
48
	}
49
50
	if ($errcode !== 0) {
51
		// Retrieve constants categories, MAPI error names are defined in gromox.
52
		foreach (get_defined_constants(true)['Core'] as $key => $value) {
53
			/*
54
			 * If PHP encounters a number beyond the bounds of the integer type,
55
			 * it will be interpreted as a float instead, so when comparing these error codes
56
			 * we have to manually typecast value to integer, so float will be converted in integer,
57
			 * but still its out of bound for integer limit so it will be auto adjusted to minus value
58
			 */
59
			if ($errcode == (int) $value) {
60
				// Check that we have an actual MAPI error or warning definition
61
				$prefix = substr($key, 0, 7);
62
				if ($prefix == "MAPI_E_" || $prefix == "MAPI_W_") {
63
					return $key;
64
				}
65
				$prefix = substr($key, 0, 2);
66
				if ($prefix == "ec") {
67
					return $key;
68
				}
69
			}
70
		}
71
	}
72
	else {
73
		return "NOERROR";
74
	}
75
76
	// error code not found, return hex value (this is a fix for 64-bit systems, we can't use the dechex() function for this)
77
	$result = unpack("H*", pack("N", $errcode));
78
79
	return "0x" . $result[1];
80
}
81
82
/**
83
 * Parses properties from an array of strings. Each "string" may be either an ULONG, which is a direct property ID,
84
 * or a string with format "PT_TYPE:{GUID}:StringId" or "PT_TYPE:{GUID}:0xXXXX" for named
85
 * properties.
86
 *
87
 * @param mixed $store
88
 * @param mixed $mapping
89
 *
90
 * @return array
91
 */
92
function getPropIdsFromStrings($store, $mapping) {
93
	$props = [];
94
95
	$ids = ["name" => [], "id" => [], "guid" => [], "type" => []]; // this array stores all the information needed to retrieve a named property
96
	$num = 0;
97
98
	// caching
99
	$guids = [];
100
101
	foreach ($mapping as $name => $val) {
102
		if (is_string($val)) {
103
			$split = explode(":", $val);
104
105
			if (count($split) != 3) { // invalid string, ignore
106
				trigger_error(sprintf("Invalid property: %s \"%s\"", $name, $val), E_USER_NOTICE);
107
108
				continue;
109
			}
110
111
			if (str_starts_with($split[2], "0x")) {
112
				$id = hexdec(substr($split[2], 2));
113
			}
114
			elseif (preg_match('/^[1-9][0-9]{0,12}$/', $split[2])) {
115
				$id = (int) $split[2];
116
			}
117
			else {
118
				$id = $split[2];
119
			}
120
121
			// have we used this guid before?
122
			if (!defined($split[1])) {
123
				if (!array_key_exists($split[1], $guids)) {
124
					$guids[$split[1]] = makeguid($split[1]);
125
				}
126
				$guid = $guids[$split[1]];
127
			}
128
			else {
129
				$guid = constant($split[1]);
130
			}
131
132
			// temp store info about named prop, so we have to call mapi_getidsfromnames just one time
133
			$ids["name"][$num] = $name;
134
			$ids["id"][$num] = $id;
135
			$ids["guid"][$num] = $guid;
136
			$ids["type"][$num] = $split[0];
137
			++$num;
138
		}
139
		else {
140
			// not a named property
141
			$props[$name] = $val;
142
		}
143
	}
144
145
	if (empty($ids["id"])) {
146
		return $props;
147
	}
148
149
	// get the ids
150
	$named = mapi_getidsfromnames($store, $ids["id"], $ids["guid"]);
151
	foreach ($named as $num => $prop) {
152
		$props[$ids["name"][$num]] = mapi_prop_tag(constant($ids["type"][$num]), mapi_prop_id($prop));
153
	}
154
155
	return $props;
156
}
157
158
/**
159
 * Check whether a call to mapi_getprops returned errors for some properties.
160
 * mapi_getprops function tries to get values of properties requested but somehow if
161
 * if a property value can not be fetched then it changes type of property tag as PT_ERROR
162
 * and returns error for that particular property, probable errors
163
 * that can be returned as value can be MAPI_E_NOT_FOUND, MAPI_E_NOT_ENOUGH_MEMORY.
164
 *
165
 * @param int   $property  Property to check for error
166
 * @param array $propArray An array of properties
167
 *
168
 * @return bool|mixed Gives back false when there is no error, if there is, gives the error
169
 */
170
function propIsError($property, $propArray) {
171
	if (array_key_exists(mapi_prop_tag(PT_ERROR, mapi_prop_id($property)), $propArray)) {
172
		return $propArray[mapi_prop_tag(PT_ERROR, mapi_prop_id($property))];
173
	}
174
175
	return false;
176
}
177
178
/**
179
 * Note: Static function, more like a utility function.
180
 *
181
 * Gets all the items (including recurring items) in the specified calendar in the given timeframe. Items are
182
 * included as a whole if they overlap the interval <$start, $end> (non-inclusive). This means that if the interval
183
 * 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
184
 * [7:00 - 9:00> is included as a whole, and is NOT capped to [8:00 - 9:00>.
185
 *
186
 * @param resource $store          The store in which the calendar resides
187
 * @param resource $calendar       The calendar to get the items from
188
 * @param int      $viewstart      Timestamp of beginning of view window
189
 * @param int      $viewend        Timestamp of end of view window
190
 * @param array    $propsrequested Array of properties to return
191
 *
192
 * @psalm-return list<mixed>
193
 */
194
function getCalendarItems($store, $calendar, $viewstart, $viewend, $propsrequested): array {
195
	$result = [];
196
	$properties = getPropIdsFromStrings($store, [
197
		"duedate" => "PT_SYSTIME:PSETID_Appointment:" . PidLidAppointmentEndWhole,
198
		"startdate" => "PT_SYSTIME:PSETID_Appointment:" . PidLidAppointmentStartWhole,
199
		"enddate_recurring" => "PT_SYSTIME:PSETID_Appointment:" . PidLidClipEnd,
200
		"recurring" => "PT_BOOLEAN:PSETID_Appointment:" . PidLidRecurring,
201
		"recurring_data" => "PT_BINARY:PSETID_Appointment:" . PidLidAppointmentRecur,
202
		"timezone_data" => "PT_BINARY:PSETID_Appointment:" . PidLidTimeZoneStruct,
203
		"label" => "PT_LONG:PSETID_Appointment:0x8214",
204
	]);
205
206
	// Create a restriction that will discard rows of appointments that are definitely not in our
207
	// requested time frame
208
209
	$table = mapi_folder_getcontentstable($calendar);
0 ignored issues
show
$calendar of type resource is incompatible with the type resource expected by parameter $fld of mapi_folder_getcontentstable(). ( Ignorable by Annotation )

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

209
	$table = mapi_folder_getcontentstable(/** @scrutinizer ignore-type */ $calendar);
Loading history...
210
211
	$restriction =
212
		// OR
213
		[
214
			RES_OR,
215
			[
216
				[RES_AND,	// Normal items: itemEnd must be after viewStart, itemStart must be before viewEnd
217
					[
218
						[
219
							RES_PROPERTY,
220
							[
221
								RELOP => RELOP_GT,
222
								ULPROPTAG => $properties["duedate"],
223
								VALUE => $viewstart,
224
							],
225
						],
226
						[
227
							RES_PROPERTY,
228
							[
229
								RELOP => RELOP_LT,
230
								ULPROPTAG => $properties["startdate"],
231
								VALUE => $viewend,
232
							],
233
						],
234
					],
235
				],
236
				// OR
237
				[
238
					RES_PROPERTY,
239
					[
240
						RELOP => RELOP_EQ,
241
						ULPROPTAG => $properties["recurring"],
242
						VALUE => true,
243
					],
244
				],
245
			],	// EXISTS OR
246
		];		// global OR
247
248
	// Get requested properties, plus whatever we need
249
	$proplist = [PR_ENTRYID, $properties["recurring"], $properties["recurring_data"], $properties["timezone_data"]];
250
	$proplist = array_merge($proplist, $propsrequested);
251
252
	$rows = mapi_table_queryallrows($table, $proplist, $restriction);
253
254
	// $rows now contains all the items that MAY be in the window; a recurring item needs expansion before including in the output.
255
256
	foreach ($rows as $row) {
257
		$items = [];
258
259
		if (isset($row[$properties["recurring"]]) && $row[$properties["recurring"]]) {
260
			// Recurring item
261
			$rec = new Recurrence($store, $row);
262
263
			// GetItems guarantees that the item overlaps the interval <$viewstart, $viewend>
264
			$occurrences = $rec->getItems($viewstart, $viewend);
265
			foreach ($occurrences as $occurrence) {
266
				// The occurrence takes all properties from the main row, but overrides some properties (like start and end obviously)
267
				$item = $occurrence + $row;
268
				array_push($items, $item);
269
			}
270
		}
271
		else {
272
			// Normal item, it matched the search criteria and therefore overlaps the interval <$viewstart, $viewend>
273
			array_push($items, $row);
274
		}
275
276
		$result = array_merge($result, $items);
277
	}
278
279
	// All items are guaranteed to overlap the interval <$viewstart, $viewend>. Note that we may be returning a few extra
280
	// properties that the caller did not request (recurring, etc). This shouldn't be a problem though.
281
	return $result;
282
}
283
284
/**
285
 * Compares two entryIds. It is possible to have two different entryIds that should match as they
286
 * represent the same object (in multiserver environments).
287
 *
288
 * @param mixed $entryId1 EntryID
289
 * @param mixed $entryId2 EntryID
290
 *
291
 * @return bool Result of the comparison
292
 */
293
function compareEntryIds($entryId1, $entryId2) {
294
	if (!is_string($entryId1) || !is_string($entryId2)) {
295
		return false;
296
	}
297
298
	if ($entryId1 === $entryId2) {
299
		// if normal comparison succeeds then we can directly say that entryids are same
300
		return true;
301
	}
302
303
	return false;
304
}
305
306
/**
307
 * Creates a goid from an ical uuid.
308
 *
309
 * @param string $uid
310
 *
311
 * @return string binary string representation of goid
312
 */
313
function getGoidFromUid($uid) {
314
	return hex2bin("040000008200E00074C5B7101A82E0080000000000000000000000000000000000000000" .
315
				bin2hex(pack("V", 12 + strlen($uid)) . "vCal-Uid" . pack("V", 1) . $uid));
316
}
317
318
/**
319
 * Returns zero terminated goid. It is required for backwards compatibility.
320
 *
321
 * @param mixed $uid
322
 *
323
 * @return string an OL compatible GlobalObjectID
324
 */
325
function getGoidFromUidZero($uid) {
326
	if (strlen((string) $uid) <= 64) {
327
		return hex2bin("040000008200E00074C5B7101A82E0080000000000000000000000000000000000000000" .
328
			bin2hex(pack("V", 13 + strlen((string) $uid)) . "vCal-Uid" . pack("V", 1) . $uid) . "00");
329
	}
330
331
	return hex2bin((string) $uid);
332
}
333
334
/**
335
 * Creates an ical uuid from a goid.
336
 *
337
 * @param string $goid
338
 *
339
 * @return null|string ical uuid
340
 */
341
function getUidFromGoid($goid) {
342
	// check if "vCal-Uid" is somewhere in outlookid case-insensitive
343
	$uid = stristr($goid, "vCal-Uid");
344
	if ($uid !== false) {
345
		// get the length of the ical id - go back 4 position from where "vCal-Uid" was found
346
		$begin = unpack("V", substr($goid, strlen($uid) * (-1) - 4, 4));
347
348
		// remove "vCal-Uid" and packed "1" and use the ical id length
349
		return trim(substr($uid, 12, $begin[1] - 12));
350
	}
351
352
	return null;
353
}
354
355
/**
356
 * Converts a MAPI property tag into a human readable value.
357
 *
358
 * This depends on the definition of the property tag in core
359
 *
360
 * @example prop2Str(0x0037001e) => 'PR_SUBJECT'
361
 *
362
 * @param mixed $property
363
 *
364
 * @return string the symbolic name of the property tag
365
 */
366
function prop2Str($property) {
367
	if (is_integer($property)) {
368
		// Retrieve constants categories, zcore provides them in 'Core'
369
		foreach (get_defined_constants(true)['Core'] as $key => $value) {
370
			if ($property == $value && str_starts_with($key, 'PR_')) {
371
				return $key;
372
			}
373
		}
374
375
		return sprintf("0x%08X", $property);
376
	}
377
378
	return $property;
379
}
380
381
/**
382
 * Converts all constants of restriction into a human readable strings.
383
 *
384
 * @param mixed $restriction
385
 */
386
function simplifyRestriction($restriction) {
387
	if (!is_array($restriction)) {
388
		return $restriction;
389
	}
390
391
	switch ($restriction[0]) {
392
		case RES_AND:
393
			$restriction[0] = "RES_AND";
394
			if (isset($restriction[1][0]) && is_array($restriction[1][0])) {
395
				foreach ($restriction[1] as &$res) {
396
					$res = simplifyRestriction($res);
397
				}
398
				unset($res);
399
			}
400
			elseif (isset($restriction[1]) && $restriction[1]) {
401
				$restriction[1] = simplifyRestriction($restriction[1]);
402
			}
403
			break;
404
405
		case RES_OR:
406
			$restriction[0] = "RES_OR";
407
			if (isset($restriction[1][0]) && is_array($restriction[1][0])) {
408
				foreach ($restriction[1] as &$res) {
409
					$res = simplifyRestriction($res);
410
				}
411
				unset($res);
412
			}
413
			elseif (isset($restriction[1]) && $restriction[1]) {
414
				$restriction[1] = simplifyRestriction($restriction[1]);
415
			}
416
			break;
417
418
		case RES_NOT:
419
			$restriction[0] = "RES_NOT";
420
			$restriction[1][0] = simplifyRestriction($restriction[1][0]);
421
			break;
422
423
		case RES_COMMENT:
424
			$restriction[0] = "RES_COMMENT";
425
			$res = simplifyRestriction($restriction[1][RESTRICTION]);
426
			$props = $restriction[1][PROPS];
427
428
			foreach ($props as &$prop) {
429
				$propTag = $prop[ULPROPTAG];
430
				$propValue = $prop[VALUE];
431
432
				unset($prop);
433
434
				$prop["ULPROPTAG"] = is_string($propTag) ? $propTag : prop2Str($propTag);
435
				$prop["VALUE"] = is_array($propValue) ? $propValue[$propTag] : $propValue;
436
			}
437
			unset($prop, $restriction[1]);
438
439
			$restriction[1]["RESTRICTION"] = $res;
440
			$restriction[1]["PROPS"] = $props;
441
			break;
442
443
		case RES_PROPERTY:
444
			$restriction[0] = "RES_PROPERTY";
445
			$propTag = $restriction[1][ULPROPTAG];
446
			$propValue = $restriction[1][VALUE];
447
			$relOp = $restriction[1][RELOP];
448
449
			unset($restriction[1]);
450
451
			// relop flags
452
			$relOpFlags = "";
453
			if ($relOp == RELOP_LT) {
454
				$relOpFlags = "RELOP_LT";
455
			}
456
			elseif ($relOp == RELOP_LE) {
457
				$relOpFlags = "RELOP_LE";
458
			}
459
			elseif ($relOp == RELOP_GT) {
460
				$relOpFlags = "RELOP_GT";
461
			}
462
			elseif ($relOp == RELOP_GE) {
463
				$relOpFlags = "RELOP_GE";
464
			}
465
			elseif ($relOp == RELOP_EQ) {
466
				$relOpFlags = "RELOP_EQ";
467
			}
468
			elseif ($relOp == RELOP_NE) {
469
				$relOpFlags = "RELOP_NE";
470
			}
471
			elseif ($relOp == RELOP_RE) {
472
				$relOpFlags = "RELOP_RE";
473
			}
474
475
			$restriction[1]["RELOP"] = $relOpFlags;
476
			$restriction[1]["ULPROPTAG"] = is_string($propTag) ? $propTag : prop2Str($propTag);
477
			$restriction[1]["VALUE"] = is_array($propValue) ? $propValue[$propTag] : $propValue;
478
			break;
479
480
		case RES_CONTENT:
481
			$restriction[0] = "RES_CONTENT";
482
			$propTag = $restriction[1][ULPROPTAG];
483
			$propValue = $restriction[1][VALUE];
484
			$fuzzyLevel = $restriction[1][FUZZYLEVEL];
485
486
			unset($restriction[1]);
487
488
			// fuzzy level flags
489
			$levels = [];
490
491
			if (($fuzzyLevel & FL_SUBSTRING) == FL_SUBSTRING) {
492
				$levels[] = "FL_SUBSTRING";
493
			}
494
			elseif (($fuzzyLevel & FL_PREFIX) == FL_PREFIX) {
495
				$levels[] = "FL_PREFIX";
496
			}
497
			else {
498
				$levels[] = "FL_FULLSTRING";
499
			}
500
501
			if (($fuzzyLevel & FL_IGNORECASE) == FL_IGNORECASE) {
502
				$levels[] = "FL_IGNORECASE";
503
			}
504
505
			if (($fuzzyLevel & FL_IGNORENONSPACE) == FL_IGNORENONSPACE) {
506
				$levels[] = "FL_IGNORENONSPACE";
507
			}
508
509
			if (($fuzzyLevel & FL_LOOSE) == FL_LOOSE) {
510
				$levels[] = "FL_LOOSE";
511
			}
512
513
			$fuzzyLevelFlags = implode(" | ", $levels);
514
515
			$restriction[1]["FUZZYLEVEL"] = $fuzzyLevelFlags;
516
			$restriction[1]["ULPROPTAG"] = is_string($propTag) ? $propTag : prop2Str($propTag);
517
			$restriction[1]["VALUE"] = is_array($propValue) ? $propValue[$propTag] : $propValue;
518
			break;
519
520
		case RES_COMPAREPROPS:
521
			$propTag1 = $restriction[1][ULPROPTAG1];
522
			$propTag2 = $restriction[1][ULPROPTAG2];
523
524
			unset($restriction[1]);
525
526
			$restriction[1]["ULPROPTAG1"] = is_string($propTag1) ? $proptag1 : prop2Str($proptag1);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $proptag1 does not exist. Did you maybe mean $propTag1?
Loading history...
527
			$restriction[1]["ULPROPTAG2"] = is_string($propTag2) ? $propTag2 : prop2Str($propTag2);
528
			break;
529
530
		case RES_BITMASK:
531
			$restriction[0] = "RES_BITMASK";
532
			$propTag = $restriction[1][ULPROPTAG];
533
			$maskType = $restriction[1][ULTYPE];
534
			$maskValue = $restriction[1][ULMASK];
535
536
			unset($restriction[1]);
537
538
			// relop flags
539
			$maskTypeFlags = "";
540
			if ($maskType == BMR_EQZ) {
541
				$maskTypeFlags = "BMR_EQZ";
542
			}
543
			elseif ($maskType == BMR_NEZ) {
544
				$maskTypeFlags = "BMR_NEZ";
545
			}
546
547
			$restriction[1]["ULPROPTAG"] = is_string($propTag) ? $propTag : prop2Str($propTag);
548
			$restriction[1]["ULTYPE"] = $maskTypeFlags;
549
			$restriction[1]["ULMASK"] = $maskValue;
550
			break;
551
552
		case RES_SIZE:
553
			$restriction[0] = "RES_SIZE";
554
			$propTag = $restriction[1][ULPROPTAG];
555
			$propValue = $restriction[1][CB];
556
			$relOp = $restriction[1][RELOP];
557
558
			unset($restriction[1]);
559
560
			// relop flags
561
			$relOpFlags = "";
562
			if ($relOp == RELOP_LT) {
563
				$relOpFlags = "RELOP_LT";
564
			}
565
			elseif ($relOp == RELOP_LE) {
566
				$relOpFlags = "RELOP_LE";
567
			}
568
			elseif ($relOp == RELOP_GT) {
569
				$relOpFlags = "RELOP_GT";
570
			}
571
			elseif ($relOp == RELOP_GE) {
572
				$relOpFlags = "RELOP_GE";
573
			}
574
			elseif ($relOp == RELOP_EQ) {
575
				$relOpFlags = "RELOP_EQ";
576
			}
577
			elseif ($relOp == RELOP_NE) {
578
				$relOpFlags = "RELOP_NE";
579
			}
580
			elseif ($relOp == RELOP_RE) {
581
				$relOpFlags = "RELOP_RE";
582
			}
583
584
			$restriction[1]["ULPROPTAG"] = is_string($propTag) ? $propTag : prop2Str($propTag);
585
			$restriction[1]["RELOP"] = $relOpFlags;
586
			$restriction[1]["CB"] = $propValue;
587
			break;
588
589
		case RES_EXIST:
590
			$propTag = $restriction[1][ULPROPTAG];
591
592
			unset($restriction[1]);
593
594
			$restriction[1]["ULPROPTAG"] = is_string($propTag) ? $propTag : prop2Str($propTag);
595
			break;
596
597
		case RES_SUBRESTRICTION:
598
			$propTag = $restriction[1][ULPROPTAG];
599
			$res = simplifyRestriction($restriction[1][RESTRICTION]);
600
601
			unset($restriction[1]);
602
603
			$restriction[1]["ULPROPTAG"] = is_string($propTag) ? $propTag : prop2Str($propTag);
604
			$restriction[1]["RESTRICTION"] = $res;
605
			break;
606
	}
607
608
	return $restriction;
609
}
610