Issues (752)

server/includes/util.php (12 issues)

1
<?php
2
3
/**
4
 * Utility functions.
5
 */
6
require_once BASE_PATH . 'server/includes/exceptions/class.JSONException.php';
7
8
/**
9
 * Function which reads the data stream. This data is send by the WebClient.
10
 *
11
 * @return string data
12
 */
13
function readData() {
14
	$data = "";
15
	$putData = fopen("php://input", "r");
16
17
	while ($block = fread($putData, 1024)) {
18
		$data .= $block;
19
	}
20
21
	fclose($putData);
22
23
	return $data;
24
}
25
26
/*
27
 * Add in config specified default domain to email if no domain is set in form.
28
 * If no default domain is set in config, the input string will be return without changes.
29
 *
30
 * @param string user the user to append domain to
0 ignored issues
show
The type user 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...
31
 * @return string email
32
 */
33
function appendDefaultDomain($user) {
34
	if (empty($user)) {
35
		return '';
36
	}
37
	if (!defined('DEFAULT_DOMAIN') || empty(DEFAULT_DOMAIN) || str_contains((string) $user, '@')) {
38
		return $user;
39
	}
40
41
	return $user . "@" . DEFAULT_DOMAIN;
42
}
43
44
/**
45
 * Function which is called every time the "session_start" method is called.
46
 * It unserializes the objects in the session. This function called by PHP.
47
 *
48
 * @param string @className the className of the object in the session
0 ignored issues
show
Documentation Bug introduced by
The doc comment @className at position 0 could not be parsed: Unknown type name '@className' at position 0 in @className.
Loading history...
49
 * @param mixed $className
50
 */
51
function sessionNotifierLoader($className) {
52
	$className = strtolower((string) $className); // for PHP5 set className to lower case to find the file (see ticket #839 for more information)
53
54
	switch ($className) {
55
		case "bus":
56
			require_once BASE_PATH . 'server/includes/core/class.bus.php';
57
			break;
58
59
		default:
60
			$path = BASE_PATH . 'server/includes/notifiers/class.' . $className . '.php';
61
			if (is_file($path)) {
62
				require_once $path;
63
			}
64
			else {
65
				$path = $GLOBALS['PluginManager']->getNotifierFilePath($className);
66
				if (is_file($path)) {
67
					require_once $path;
68
				}
69
			}
70
			break;
71
	}
72
	if (!class_exists($className)) {
73
		trigger_error("Can't load " . $className . " while unserializing the session.", E_USER_WARNING);
74
	}
75
}
76
77
/**
78
 * Function which checks if an array is an associative array.
79
 *
80
 * @param array $data array which should be verified
81
 *
82
 * @return bool true if the given array is an associative array, false if not
83
 */
84
function is_assoc_array($data) {
85
	return is_array($data) && !empty($data) && !preg_match('/^\d+$/', implode('', array_keys($data)));
86
}
87
88
/**
89
 * gets maximum upload size of attachment from php ini settings
90
 * important settings are upload_max_filesize and post_max_size
91
 * upload_max_filesize specifies maximum upload size for attachments
92
 * post_max_size must be larger then upload_max_filesize.
93
 * these values are overwritten in .htaccess file of WA.
94
 *
95
 * @param mixed $as_string
96
 *
97
 * @return string return max value either upload max filesize or post max size
98
 */
99
function getMaxUploadSize($as_string = false) {
100
	$upload_max_value = strtoupper(ini_get('upload_max_filesize'));
101
	$post_max_value = getMaxPostRequestSize();
102
103
	/*
104
	 * if POST_MAX_SIZE is lower then UPLOAD_MAX_FILESIZE, then we have to check based on that value
105
	 * as we will not be able to upload attachment larger then POST_MAX_SIZE (file size + header data)
106
	 * so set POST_MAX_SIZE value to higher then UPLOAD_MAX_FILESIZE
107
	 */
108
109
	// calculate upload_max_value value to bytes
110
	if (str_contains($upload_max_value, "K")) {
111
		$upload_max_value = ((int) $upload_max_value) * 1024;
112
	}
113
	elseif (str_contains($upload_max_value, "M")) {
114
		$upload_max_value = ((int) $upload_max_value) * 1024 * 1024;
115
	}
116
	elseif (str_contains($upload_max_value, "G")) {
117
		$upload_max_value = ((int) $upload_max_value) * 1024 * 1024 * 1024;
118
	}
119
120
	// check which one is larger
121
	$value = $upload_max_value;
122
	if ($upload_max_value > $post_max_value) {
123
		$value = $post_max_value;
124
	}
125
126
	if ($as_string) {
127
		// make user readable string
128
		if ($value > (1024 * 1024 * 1024)) {
129
			$value = round($value / (1024 * 1024 * 1024), 1) . " " . _("GB");
130
		}
131
		elseif ($value > (1024 * 1024)) {
132
			$value = round($value / (1024 * 1024), 1) . " " . _("MB");
133
		}
134
		elseif ($value > 1024) {
135
			$value = round($value / 1024, 1) . " " . _("KB");
136
		}
137
		else {
138
			$value = $value . " " . _("B");
139
		}
140
	}
141
142
	return $value;
143
}
144
145
/**
146
 * Gets maximum post request size of attachment from php ini settings.
147
 * post_max_size specifies maximum size of a post request,
148
 * we are uploading attachment using post method.
149
 *
150
 * @return string returns the post request size with proper unit(MB, GB, KB etc.).
151
 */
152
function getMaxPostRequestSize() {
153
	$post_max_value = strtoupper(ini_get('post_max_size'));
154
155
	// calculate post_max_value value to bytes
156
	if (str_contains($post_max_value, "K")) {
157
		$post_max_value = ((int) $post_max_value) * 1024;
158
	}
159
	elseif (str_contains($post_max_value, "M")) {
160
		$post_max_value = ((int) $post_max_value) * 1024 * 1024;
161
	}
162
	elseif (str_contains($post_max_value, "G")) {
163
		$post_max_value = ((int) $post_max_value) * 1024 * 1024 * 1024;
164
	}
165
166
	return $post_max_value;
167
}
168
169
/**
170
 * Get maximum number of files that can be uploaded in single request from php ini settings.
171
 * max_file_uploads specifies maximum number of files allowed in post request.
172
 *
173
 * @return number maximum number of files can uploaded in single request
174
 */
175
function getMaxFileUploads() {
176
	return (int) ini_get('max_file_uploads');
177
}
178
179
/**
180
 * cleanTemp.
181
 *
182
 * Cleans up the temp directory.
183
 *
184
 * @param string $directory   the path to the temp dir or sessions dir
185
 * @param int    $maxLifeTime the maximum allowed age of files in seconds
186
 * @param bool   $recursive   False to prevent the folder to be cleaned up recursively
187
 * @param bool   $removeSubs  False to prevent empty subfolders from being deleted
188
 *
189
 * @return bool True if the folder is empty
190
 */
191
function cleanTemp($directory = TMP_PATH, $maxLifeTime = STATE_FILE_MAX_LIFETIME, $recursive = true, $removeSubs = true) {
192
	if (!is_dir($directory)) {
193
		return;
194
	}
195
196
	// PHP doesn't do this by itself, so before running through
197
	// the folder, we should flush the statcache, so the 'atime'
198
	// is current.
199
	clearstatcache();
200
201
	$dir = opendir($directory);
202
	$is_empty = true;
203
204
	while ($file = readdir($dir)) {
205
		// Skip special folders
206
		if ($file === '.' || $file === '..') {
207
			continue;
208
		}
209
210
		$path = $directory . DIRECTORY_SEPARATOR . $file;
211
212
		if (is_dir($path)) {
213
			// If it is a directory, check if we need to
214
			// recursively clean this subfolder.
215
			if ($recursive) {
216
				// If cleanTemp indicates the subfolder is empty,
217
				// and $removeSubs is true, we must delete the subfolder
218
				// otherwise the currently folder is not empty.
219
				if (cleanTemp($path, $maxLifeTime, $recursive) && $removeSubs) {
220
					rmdir($path);
221
				}
222
				else {
223
					$is_empty = false;
224
				}
225
			}
226
			else {
227
				// We are not cleaning recursively, the current
228
				// folder is not empty.
229
				$is_empty = false;
230
			}
231
		}
232
		else {
233
			$fileinfo = stat($path);
234
235
			if ($fileinfo && $fileinfo["atime"] < time() - $maxLifeTime) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $fileinfo 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...
236
				unlink($path);
237
			}
238
			else {
239
				$is_empty = false;
240
			}
241
		}
242
	}
243
244
	return $is_empty;
245
}
246
247
function cleanSearchFolders() {
248
	$store = $GLOBALS["mapisession"]->getDefaultMessageStore();
249
250
	$storeProps = mapi_getprops($store, [PR_STORE_SUPPORT_MASK, PR_FINDER_ENTRYID]);
251
	if (($storeProps[PR_STORE_SUPPORT_MASK] & STORE_SEARCH_OK) !== STORE_SEARCH_OK) {
252
		return;
253
	}
254
255
	$finderfolder = mapi_msgstore_openentry($store, $storeProps[PR_FINDER_ENTRYID]);
256
257
	$hierarchytable = mapi_folder_gethierarchytable($finderfolder, MAPI_DEFERRED_ERRORS);
258
	mapi_table_restrict($hierarchytable, [RES_AND,
259
		[
260
			[RES_CONTENT,
261
				[
262
					FUZZYLEVEL => FL_PREFIX,
263
					ULPROPTAG => PR_DISPLAY_NAME,
264
					VALUE => [PR_DISPLAY_NAME => "grommunio Web Search Folder"],
265
				],
266
			],
267
			[RES_PROPERTY,
268
				[
269
					RELOP => RELOP_LT,
270
					ULPROPTAG => PR_LAST_MODIFICATION_TIME,
271
					VALUE => [PR_LAST_MODIFICATION_TIME => (time() - ini_get("session.gc_maxlifetime"))],
272
				],
273
			],
274
		],
275
	], TBL_BATCH);
276
277
	$folders = mapi_table_queryallrows($hierarchytable, [PR_ENTRYID]);
278
	foreach ($folders as $folder) {
279
		mapi_folder_deletefolder($finderfolder, $folder[PR_ENTRYID]);
280
	}
281
}
282
283
function dechex_32($dec) {
284
	// Because on 64bit systems PHP handles integers as 64bit,
285
	// we need to convert these 64bit integers to 32bit when we
286
	// want the hex value
287
	$result = unpack("H*", pack("N", $dec));
288
289
	return $result[1];
290
}
291
292
/**
293
 * This function will encode the input string for the header based on the browser that makes the
294
 * HTTP request. MSIE and Edge has an issue with unicode filenames. All browsers do not seem to follow
295
 * the RFC specification. Firefox requires an unencoded string in the HTTP header. MSIE and Edge will
296
 * break on this and requires encoding.
297
 *
298
 * @param string $input Unencoded string
299
 *
300
 * @return string Encoded string
301
 */
302
function browserDependingHTTPHeaderEncode($input) {
303
	$input = preg_replace("/\r|\n/", "", $input);
304
	if (!isEdge()) {
305
		return $input;
306
	}
307
308
	return rawurlencode((string) $input);
309
}
310
311
/**
312
 * Helps to detect if the request came from Edge or not.
313
 *
314
 * @return bool true if Edge is the requester, position of the word otherwise
315
 */
316
function isEdge() {
317
	return str_contains((string) $_SERVER['HTTP_USER_AGENT'], 'Edge');
318
}
319
320
/**
321
 * This function will return base name of the file from the full path of the file.
322
 * PHP's basename() does not properly support streams or filenames beginning with a non-US-ASCII character.
323
 * The default implementation in php for basename is locale aware. So it will truncate umlauts which can not be
324
 * parsed by the current set locale.
325
 * This problem only occurs with PHP < 5.2.
326
 *
327
 * @see http://bugs.php.net/bug.php?id=37738, https://bugs.php.net/bug.php?id=37268
328
 *
329
 * @param string $filepath full path of the file
330
 * @param string $suffix   suffix that will be trimmed from file name
331
 *
332
 * @return string base name of the file
333
 */
334
function mb_basename($filepath, $suffix = '') {
335
	// Remove right-most slashes when $uri points to directory.
336
	$filepath = rtrim($filepath, DIRECTORY_SEPARATOR . ' ');
337
338
	// Returns the trailing part of the $uri starting after one of the directory
339
	// separators.
340
	$filename = preg_match('@[^' . preg_quote(DIRECTORY_SEPARATOR, '@') . ']+$@', $filepath, $matches) ? $matches[0] : '';
341
342
	// Cuts off a suffix from the filename.
343
	if ($suffix) {
344
		$filename = preg_replace('@' . preg_quote($suffix, '@') . '$@', '', $filename);
345
	}
346
347
	return $filename;
348
}
349
350
/**
351
 * Function is used to get data from query string and store it in session
352
 * for use when webapp is completely loaded.
353
 */
354
function storeURLDataToSession() {
355
	$data = [];
356
357
	$urlData = urldecode((string) $_SERVER['QUERY_STRING']);
358
	if (!empty($_GET['action']) && $_GET['action'] === 'mailto') {
359
		$data['mailto'] = $_GET['to'];
360
361
		// There may be some data after to field, like cc, subject, body
362
		// So add them in the urlData string as well
363
		$pos = stripos($urlData, (string) $_GET['to']) + strlen((string) $_GET['to']);
364
		$subString = substr($urlData, $pos);
365
		$data['mailto'] .= $subString;
366
	}
367
368
	if (!empty($data)) {
369
		// finally store all data to session
370
		$_SESSION['url_action'] = $data;
371
	}
372
}
373
374
/**
375
 * Checks if the given url is allowed as redirect url.
376
 *
377
 * @param string $url The url that will be checked
378
 *
379
 * @return bool True is the url is allowed as redirect url,
380
 *              false otherwise
381
 */
382
function isContinueRedirectAllowed($url) {
383
	// First check the protocol
384
	$selfProtocol = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] ? 'https' : 'http';
385
	$parsed = parse_url($url);
386
	if ($parsed === false || !isset($parsed['scheme']) || strtolower($parsed['scheme']) !== $selfProtocol) {
387
		return false;
388
	}
389
390
	// The same domain as grommunio Web is always allowed
391
	if ($parsed['host'] === $_SERVER['HTTP_HOST']) {
392
		return true;
393
	}
394
395
	// Check if the domain is white listed
396
	$allowedDomains = explode(' ', (string) preg_replace('/\s+/', ' ', REDIRECT_ALLOWED_DOMAINS));
397
	if (count($allowedDomains) && !empty($allowedDomains[0])) {
398
		foreach ($allowedDomains as $domain) {
399
			$parsedDomain = parse_url($domain);
400
401
			// Handle invalid configuration options
402
			if (!isset($parsedDomain['scheme']) || !isset($parsedDomain['host'])) {
403
				error_log("Invalid 'REDIRECT_ALLOWED_DOMAINS' " . $domain);
404
405
				continue;
406
			}
407
408
			if ($parsedDomain['scheme'] . '://' . $parsedDomain['host'] === $parsed['scheme'] . '://' . $parsed['host']) {
409
				// This domain was allowed to redirect to by the administrator
410
				return true;
411
			}
412
		}
413
	}
414
415
	return false;
416
}
417
418
// Constants for regular expressions which are used in get method to verify the input string
419
define("ID_REGEX", "/^[a-z0-9_]+$/im");
420
define("STRING_REGEX", "/^[a-z0-9_\\s()@]+$/im");
421
define("USERNAME_REGEX", "/^[a-z0-9\\-\\.\\'_@]+$/im");
422
define("ALLOWED_EMAIL_CHARS_REGEX", "/^[-a-z0-9_\\.@!#\$%&'\\*\\+\\/\\=\\?\\^_`\\{\\|\\}~]+$/im");
423
define("NUMERIC_REGEX", "/^[0-9]+$/im");
424
// Don't allow "\/:*?"<>|" characters in filename.
425
define("FILENAME_REGEX", "/^[^\\/\\:\\*\\?\"\\<\\>\\|]+$/im");
426
427
/**
428
 * Function to sanitize user input values to prevent XSS attacks.
429
 *
430
 * @param mixed  $value   value that should be sanitized
431
 * @param mixed  $default default value to return when value is not safe
432
 * @param string $regex   regex to validate values based on type of value passed
433
 */
434
function sanitizeValue($value, $default = '', $regex = false) {
435
	$result = addslashes((string) $value);
436
	if ($regex) {
437
		$match = preg_match_all($regex, $result);
438
		if (!$match) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $match of type integer|null is loosely compared to false; this is ambiguous if the integer can be 0. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
439
			$result = $default;
440
		}
441
	}
442
443
	return $result;
444
}
445
446
/**
447
 * Function to sanitize user input values to prevent XSS attacks.
448
 *
449
 * @param string $key     key that should be used to get value from $_GET to sanitize value
450
 * @param mixed  $default default value to return when value is not safe
451
 * @param string $regex   regex to validate values based on type of value passed
452
 */
453
function sanitizeGetValue($key, $default = '', $regex = false) {
454
	// check if value really exists
455
	if (isset($_GET[$key])) {
456
		return sanitizeValue($_GET[$key], $default, $regex);
0 ignored issues
show
It seems like $regex can also be of type false; however, parameter $regex of sanitizeValue() does only seem to accept 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

456
		return sanitizeValue($_GET[$key], $default, /** @scrutinizer ignore-type */ $regex);
Loading history...
457
	}
458
459
	return $default;
460
}
461
462
/**
463
 * Function to sanitize user input values to prevent XSS attacks.
464
 *
465
 * @param string $key     key that should be used to get value from $_POST to sanitize value
466
 * @param mixed  $default default value to return when value is not safe
467
 * @param string $regex   regex to validate values based on type of value passed
468
 */
469
function sanitizePostValue($key, $default = '', $regex = false) {
470
	// check if value really exists
471
	if (isset($_POST[$key])) {
472
		return sanitizeValue($_POST[$key], $default, $regex);
0 ignored issues
show
It seems like $regex can also be of type false; however, parameter $regex of sanitizeValue() does only seem to accept 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

472
		return sanitizeValue($_POST[$key], $default, /** @scrutinizer ignore-type */ $regex);
Loading history...
473
	}
474
475
	return $default;
476
}
477
478
function parse_smime__join_xph(&$prop, $msg) {
479
	$a = mapi_getprops($msg, [PR_TRANSPORT_MESSAGE_HEADERS]);
480
	$a = $a === false ? "" : ($a[PR_TRANSPORT_MESSAGE_HEADERS] ?? "");
481
	$prop[PR_TRANSPORT_MESSAGE_HEADERS] =
482
		"# Outer headers:\n" . ($prop[PR_TRANSPORT_MESSAGE_HEADERS] ?? "") .
483
		"# Inner headers:\n" . $a;
484
}
485
486
/**
487
 * Function will be used to decode smime messages and convert it to normal messages.
488
 *
489
 * @param MAPIStore   $store   user's store
0 ignored issues
show
The type MAPIStore 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...
490
 * @param MAPIMessage $message smime message
0 ignored issues
show
The type MAPIMessage 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...
491
 */
492
function parse_smime($store, $message) {
493
	$props = mapi_getprops($message, [PR_MESSAGE_CLASS, PR_MESSAGE_FLAGS,
494
		PR_SENT_REPRESENTING_NAME, PR_SENT_REPRESENTING_ENTRYID, PR_SENT_REPRESENTING_SEARCH_KEY,
495
		PR_SENT_REPRESENTING_EMAIL_ADDRESS, PR_SENT_REPRESENTING_SMTP_ADDRESS,
496
		PR_SENT_REPRESENTING_ADDRTYPE, PR_CLIENT_SUBMIT_TIME, PR_TRANSPORT_MESSAGE_HEADERS]);
497
	$read = $props[PR_MESSAGE_FLAGS] & MSGFLAG_READ;
498
499
	if (isset($props[PR_MESSAGE_CLASS]) && stripos((string) $props[PR_MESSAGE_CLASS], 'IPM.Note.SMIME.MultipartSigned') !== false) {
500
		// this is a signed message. decode it.
501
		$atable = mapi_message_getattachmenttable($message);
502
503
		$rows = mapi_table_queryallrows($atable, [PR_ATTACH_MIME_TAG, PR_ATTACH_NUM]);
504
		$attnum = false;
505
506
		foreach ($rows as $row) {
507
			if (isset($row[PR_ATTACH_MIME_TAG]) && $row[PR_ATTACH_MIME_TAG] == 'multipart/signed') {
508
				$attnum = $row[PR_ATTACH_NUM];
509
			}
510
		}
511
512
		if ($attnum !== false) {
513
			$att = mapi_message_openattach($message, $attnum);
514
			$data = mapi_openproperty($att, PR_ATTACH_DATA_BIN);
515
516
			// Allowing to hook in before the signed attachment is removed
517
			$GLOBALS['PluginManager']->triggerHook('server.util.parse_smime.signed', [
518
				'store' => $store,
519
				'props' => $props,
520
				'message' => &$message,
521
				'data' => &$data,
522
			]);
523
524
			// also copy recipients because they are lost after mapi_inetmapi_imtomapi
525
			$origRcptTable = mapi_message_getrecipienttable($message);
526
			if (!isset($GLOBALS["properties"])) {
527
				$GLOBALS["properties"] = new Properties();
528
			}
529
			$origRecipients = mapi_table_queryallrows($origRcptTable, $GLOBALS["properties"]->getRecipientProperties());
530
531
			mapi_inetmapi_imtomapi($GLOBALS['mapisession']->getSession(), $store, $GLOBALS['mapisession']->getAddressbook(), $message, $data, ["parse_smime_signed" => 1]);
532
			parse_smime__join_xph($props, $message);
533
			$decapRcptTable = mapi_message_getrecipienttable($message);
534
			$decapRecipients = mapi_table_queryallrows($decapRcptTable, $GLOBALS["properties"]->getRecipientProperties());
535
			if (empty($decapRecipients) && !empty($origRecipients)) {
536
				mapi_message_modifyrecipients($message, MODRECIP_ADD, $origRecipients);
537
			}
538
539
			mapi_setprops($message, [
540
				PR_MESSAGE_CLASS => $props[PR_MESSAGE_CLASS],
541
				PR_SENT_REPRESENTING_NAME => $props[PR_SENT_REPRESENTING_NAME],
542
				PR_SENT_REPRESENTING_ENTRYID => $props[PR_SENT_REPRESENTING_ENTRYID],
543
				PR_SENT_REPRESENTING_SEARCH_KEY => $props[PR_SENT_REPRESENTING_SEARCH_KEY],
544
				PR_SENT_REPRESENTING_EMAIL_ADDRESS => $props[PR_SENT_REPRESENTING_EMAIL_ADDRESS] ?? '',
545
				PR_SENT_REPRESENTING_SMTP_ADDRESS => $props[PR_SENT_REPRESENTING_SMTP_ADDRESS] ?? '',
546
				PR_SENT_REPRESENTING_ADDRTYPE => $props[PR_SENT_REPRESENTING_ADDRTYPE] ?? 'SMTP',
547
				PR_CLIENT_SUBMIT_TIME => $props[PR_CLIENT_SUBMIT_TIME] ?? time(),
548
				PR_TRANSPORT_MESSAGE_HEADERS => ($props[PR_TRANSPORT_MESSAGE_HEADERS] ?? "") . $inner_headers,
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $inner_headers seems to be never defined.
Loading history...
549
			]);
550
		}
551
	}
552
	elseif (isset($props[PR_MESSAGE_CLASS]) && stripos((string) $props[PR_MESSAGE_CLASS], 'IPM.Note.SMIME') !== false) {
553
		// this is a encrypted message. decode it.
554
		$attachTable = mapi_message_getattachmenttable($message);
555
556
		$rows = mapi_table_queryallrows($attachTable, [PR_ATTACH_MIME_TAG, PR_ATTACH_NUM, PR_ATTACH_LONG_FILENAME]);
557
		$attnum = false;
558
		foreach ($rows as $row) {
559
			if (isset($row[PR_ATTACH_MIME_TAG]) && in_array($row[PR_ATTACH_MIME_TAG], ['application/x-pkcs7-mime', 'application/pkcs7-mime'])) {
560
				$attnum = $row[PR_ATTACH_NUM];
561
			}
562
		}
563
564
		if ($attnum !== false) {
565
			$att = mapi_message_openattach($message, $attnum);
566
			$data = mapi_openproperty($att, PR_ATTACH_DATA_BIN);
567
568
			// also copy recipients because they are lost after decrypting
569
			$origRcptTable = mapi_message_getrecipienttable($message);
570
			if (!isset($GLOBALS["properties"])) {
571
				$GLOBALS["properties"] = new Properties();
572
			}
573
			$origRecipients = mapi_table_queryallrows($origRcptTable, $GLOBALS["properties"]->getRecipientProperties());
574
575
			// Allowing to hook in before the encrypted attachment is removed
576
			$GLOBALS['PluginManager']->triggerHook('server.util.parse_smime.encrypted', [
577
				'store' => $store,
578
				'props' => $props,
579
				'message' => &$message,
580
				'data' => &$data,
581
			]);
582
583
			// after decrypting $message is a IPM.Note message,
584
			// deleting an attachment removes an actual attachment of the message
585
			$mprops = mapi_getprops($message, [PR_MESSAGE_CLASS]);
586
			if (isSmimePluginEnabled() && isset($mprops[PR_MESSAGE_CLASS]) &&
587
				stripos((string) $mprops[PR_MESSAGE_CLASS], 'IPM.Note.SMIME') !== false) {
588
				mapi_message_deleteattach($message, $attnum);
589
			}
590
591
			$decapRcptTable = mapi_message_getrecipienttable($message);
592
			$decapRecipients = mapi_table_queryallrows($decapRcptTable, $GLOBALS["properties"]->getRecipientProperties());
593
			if (empty($decapRecipients) && !empty($origRecipients)) {
594
				mapi_message_modifyrecipients($message, MODRECIP_ADD, $origRecipients);
595
			}
596
		}
597
	}
598
	// mark the message as read if the main message has read flag
599
	if ($read) {
600
		$mprops = mapi_getprops($message, [PR_MESSAGE_FLAGS]);
601
		mapi_setprops($message, [PR_MESSAGE_FLAGS => $mprops[PR_MESSAGE_FLAGS] | MSGFLAG_READ]);
602
	}
603
}
604
605
/**
606
 * Helper function which used to check smime plugin is enabled.
607
 *
608
 * @return bool true if smime plugin is enabled else false
609
 */
610
function isSmimePluginEnabled() {
611
	return $GLOBALS['settings']->get("zarafa/v1/plugins/smime/enable", false);
612
}
613
614
/**
615
 * Helper to stream a MAPI property.
616
 *
617
 * @param MAPIObject $mapiobj mapi message or store
0 ignored issues
show
The type MAPIObject 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...
618
 * @param mixed      $proptag
619
 *
620
 * @return string $datastring the streamed data
621
 */
622
function streamProperty($mapiobj, $proptag) {
623
	$stream = mapi_openproperty($mapiobj, $proptag, IID_IStream, 0, 0);
624
	$stat = mapi_stream_stat($stream);
625
	mapi_stream_seek($stream, 0, STREAM_SEEK_SET);
626
627
	$datastring = '';
628
	for ($i = 0; $i < $stat['cb']; $i += BLOCK_SIZE) {
629
		$datastring .= mapi_stream_read($stream, BLOCK_SIZE);
630
	}
631
632
	return $datastring;
633
}
634
635
/**
636
 * Function will decode JSON string into objects.
637
 *
638
 * @param string $jsonString JSON data that should be decoded
639
 * @param bool   $toAssoc    flag to indicate that associative arrays should be
640
 *                           returned as objects or arrays, true means it will return associative array as arrays and
641
 *                           false will return associative arrays as objects
642
 *
643
 * @return object decoded data
644
 */
645
function json_decode_data($jsonString, $toAssoc = false) {
646
	$data = json_decode($jsonString, $toAssoc);
647
	$errorString = '';
648
649
	switch (json_last_error()) {
650
		case JSON_ERROR_DEPTH:
651
			$errorString = _("The maximum stack depth has been exceeded");
652
			break;
653
654
		case JSON_ERROR_CTRL_CHAR:
655
			$errorString = _("Control character error, possibly incorrectly encoded");
656
			break;
657
658
		case JSON_ERROR_STATE_MISMATCH:
659
			$errorString = _("Invalid or malformed JSON");
660
			break;
661
662
		case JSON_ERROR_SYNTAX:
663
			$errorString = _("Syntax error");
664
			break;
665
666
		case JSON_ERROR_UTF8:
667
			$errorString = _("Malformed UTF-8 characters, possibly incorrectly encoded");
668
			break;
669
	}
670
671
	if (!empty($errorString)) {
672
		throw new JsonException(sprintf(_("Some problem encountered when encoding/decoding JSON data: - %s"), $errorString), json_last_error(), null);
673
	}
674
675
	return $data;
676
}
677
678
/**
679
 * Tries to open the IPM subtree. If opening fails, it will try to fix it by
680
 * trying to find the correct entryid of the IPM subtree in the hierarchy.
681
 *
682
 * @param resource $store the store to retrieve IPM subtree from
683
 *
684
 * @return mixed false if the subtree is broken beyond quick repair,
685
 *               the IPM subtree resource otherwise
686
 */
687
function getSubTree($store) {
688
	$storeProps = mapi_getprops($store, [PR_IPM_SUBTREE_ENTRYID]);
689
690
	try {
691
		$ipmsubtree = mapi_msgstore_openentry($store, $storeProps[PR_IPM_SUBTREE_ENTRYID]);
692
	}
693
	catch (MAPIException $e) {
694
		if ($e->getCode() == MAPI_E_NOT_FOUND || $e->getCode() == MAPI_E_INVALID_ENTRYID) {
695
			$username = $GLOBALS["mapisession"]->getUserName();
696
			error_log(sprintf('Unable to open IPM_SUBTREE for %s, trying to correct PR_IPM_SUBTREE_ENTRYID', $username));
697
		}
698
	}
699
700
	return $ipmsubtree;
701
}
702
703
/**
704
 * Fetches the full hierarchy and returns an array with a cache of the stat
705
 * of the folders in the hierarchy. Passing the folderType is required for cases where
706
 * the user has permission on the inbox folder, but no folder visible
707
 * rights on the rest of the store.
708
 *
709
 * @param string $username   the user who's store to retrieve hierarchy counters from.
710
 *                           If no username is given, the currently logged in user's store will be used.
711
 * @param string $folderType if inbox use the inbox as root folder
712
 *
713
 * @return array folderStatCache a cache of the hierarchy folders
714
 */
715
function updateHierarchyCounters($username = '', $folderType = '') {
716
	// Open the correct store
717
	if ($username) {
718
		$userEntryid = $GLOBALS["mapisession"]->getStoreEntryIdOfUser($username);
719
		$store = $GLOBALS["mapisession"]->openMessageStore($userEntryid);
720
	}
721
	else {
722
		$store = $GLOBALS["mapisession"]->getDefaultMessageStore();
723
	}
724
725
	$props = [PR_DISPLAY_NAME, PR_LOCAL_COMMIT_TIME_MAX, PR_CONTENT_COUNT, PR_CONTENT_UNREAD, PR_ENTRYID, PR_STORE_ENTRYID];
726
727
	if ($folderType === 'inbox') {
728
		try {
729
			$rootFolder = mapi_msgstore_getreceivefolder($store);
730
		}
731
		catch (MAPIException $e) {
732
			$username = $GLOBALS["mapisession"]->getUserName();
733
			error_log(sprintf("Unable to open Inbox for %s. MAPI Error '%s'", $username, get_mapi_error_name($e->getCode())));
734
735
			return [];
736
		}
737
	}
738
	else {
739
		$rootFolder = getSubTree($store);
740
	}
741
742
	$hierarchy = mapi_folder_gethierarchytable($rootFolder, CONVENIENT_DEPTH | MAPI_DEFERRED_ERRORS);
743
	$rows = mapi_table_queryallrows($hierarchy, $props);
744
745
	// Append the Inbox folder itself.
746
	if ($folderType === 'inbox') {
747
		array_push($rows, mapi_getprops($rootFolder, $props));
748
	}
749
750
	$folderStatCache = [];
751
	foreach ($rows as $folder) {
752
		$folderStatCache[$folder[PR_DISPLAY_NAME]] = [
753
			'commit_time' => $folder[PR_LOCAL_COMMIT_TIME_MAX] ?? "0000000000",
754
			'entryid' => bin2hex((string) $folder[PR_ENTRYID]),
755
			'store_entryid' => bin2hex((string) $folder[PR_STORE_ENTRYID]),
756
			'content_count' => $folder[PR_CONTENT_COUNT] ?? -1,
757
			'content_unread' => $folder[PR_CONTENT_UNREAD] ?? -1,
758
		];
759
	}
760
761
	return $folderStatCache;
762
}
763
764
/**
765
 * Helper function which provide protocol used by current request.
766
 *
767
 * @return string it can be either https or http
768
 */
769
function getRequestProtocol() {
770
	if (!empty($_SERVER['HTTP_X_FORWARDED_PROTO'])) {
771
		return $_SERVER['HTTP_X_FORWARDED_PROTO'];
772
	}
773
774
	return !empty($_SERVER['HTTPS']) ? "https" : "http";
775
}
776
777
/**
778
 * Helper function which defines that webapp has to use secure cookies
779
 * or not. by default webapp always use secure cookies whether or not
780
 * 'SECURE_COOKIES' defined. webapp only use insecure cookies
781
 * where a user has explicitly set 'SECURE_COOKIES' to false.
782
 *
783
 * @return bool return false only when a user has explicitly set
784
 *              'SECURE_COOKIES' to false else returns true
785
 */
786
function useSecureCookies() {
787
	return !defined('SECURE_COOKIES') || SECURE_COOKIES !== false;
788
}
789
790
/**
791
 * Check if the eml stream is corrupted or not.
792
 *
793
 * @param string $attachment content fetched from PR_ATTACH_DATA_BIN property of an attachment
794
 *
795
 * @return true if eml is broken, false otherwise
796
 */
797
function isBrokenEml($attachment) {
798
	// Get header part to process further
799
	$splittedContent = preg_split("/\r?\n\r?\n/", $attachment);
800
801
	// Fetch raw header
802
	if (preg_match_all('/([^\n^:]+:)/', $splittedContent[0], $matches)) {
803
		$rawHeaders = $matches[1];
804
	}
805
806
	// Compare if necessary headers are present or not
807
	if (isset($rawHeaders) && in_array('From:', $rawHeaders) && in_array('Date:', $rawHeaders)) {
808
		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 true.
Loading history...
809
	}
810
811
	return true;
812
}
813
814
/**
815
 * Function returns the IP address of the client.
816
 *
817
 * @return string the IP address of the client
818
 */
819
function getClientIPAddress() {
820
	// Here, there is a scenario where the server is behind a proxy, when that
821
	// happens, 'REMOTE_ADDR' will not return the real IP, there is another variable
822
	// 'HTTP_X_FORWARDED_FOR' which is set by a proxy server. But the risk in using that
823
	// is that it can be easily forged. 'REMOTE_ADDR' is the only reliable thing
824
	// as it is nearly impossible to be altered.
825
	return $_SERVER['REMOTE_ADDR'];
826
}
827
828
/**
829
 * Helper function which return the webapp version.
830
 *
831
 * @return string webapp version
832
 */
833
function getWebappVersion() {
834
	return trim(file_get_contents('version'));
835
}
836
837
/**
838
 * function which remove double quotes or PREF from vcf stream
839
 * if it has.
840
 *
841
 * @param string $attachmentStream The attachment stream
842
 */
843
function processVCFStream(&$attachmentStream) {
844
	/*
845
	 * https://github.com/libical/libical/issues/488
846
	 * https://github.com/libical/libical/issues/490
847
	 *
848
	 * Because of above issues we need to remove
849
	 * double quotes or PREF from vcf stream if
850
	 * it exists in vcf stream.
851
	 */
852
	if (preg_match('/"/', $attachmentStream) > 0) {
853
		$attachmentStream = str_replace('"', '', $attachmentStream);
854
	}
855
856
	if (preg_match('/EMAIL;PREF=/', $attachmentStream) > 0) {
857
		$rows = explode("\n", $attachmentStream);
858
		foreach ($rows as $key => $row) {
859
			if (preg_match("/EMAIL;PREF=/", $row)) {
860
				unset($rows[$key]);
861
			}
862
		}
863
864
		$attachmentStream = join("\n", $rows);
865
	}
866
}
867
868
/**
869
 * Formats time string for DateTime object, e.g.
870
 * last Sunday of March 2022 02:00.
871
 *
872
 * @param mixed $relDayofWeek
873
 * @param mixed $dayOfWeek
874
 * @param mixed $month
875
 * @param mixed $year
876
 * @param mixed $hour
877
 * @param mixed $minute
878
 */
879
function formatDateTimeString($relDayofWeek, $dayOfWeek, $month, $year, $hour, $minute) {
880
	return sprintf("%s %s of %s %04d %02d:%02d", $relDayofWeek, $dayOfWeek, $month, $year, $hour, $minute);
881
}
882
883
/**
884
 * Converts offset minutes to PHP TimeZone offset (+0200/-0530).
885
 *
886
 * Note: it is necessary to invert the bias sign in order to receive
887
 * the correct offset (-60 => +0100).
888
 *
889
 * @param int $minutes
890
 *
891
 * @return string PHP TimeZone offset
892
 */
893
function convertOffset($minutes) {
894
	$m = abs($minutes);
895
896
	return sprintf("%s%02d%02d", $minutes > 0 ? '-' : '+', intdiv($m, 60), $m % 60);
0 ignored issues
show
It seems like $m can also be of type double; however, parameter $num1 of intdiv() 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

896
	return sprintf("%s%02d%02d", $minutes > 0 ? '-' : '+', intdiv(/** @scrutinizer ignore-type */ $m, 60), $m % 60);
Loading history...
897
}
898
899
/**
900
 * Returns the index of effective rule (TZRULE_FLAG_EFFECTIVE_TZREG).
901
 *
902
 * @param array $tzrules
903
 *
904
 * @return null|int
905
 */
906
function getEffectiveTzreg($tzrules) {
907
	foreach ($tzrules as $idx => $tzDefRule) {
908
		if ($tzDefRule['tzruleflags'] & TZRULE_FLAG_EFFECTIVE_TZREG) {
909
			return $idx;
910
		}
911
	}
912
913
	return null;
914
}
915
916
/**
917
 * Returns the timestamp of std or dst start.
918
 *
919
 * @param array  $tzrule
920
 * @param int    $year
921
 * @param string $fOffset
922
 *
923
 * @return int
924
 */
925
function getRuleStart($tzrule, $year, $fOffset) {
926
	$daysOfWeek = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
927
	$monthNames = [1 => "January", "February", "March", "April", "May", "June",
928
		"July", "August", "September", "October", "November", "December", ];
929
	$relDaysOfWeek = [
930
		1 => 'first',
931
		2 => 'second',
932
		3 => 'third',
933
		4 => 'fourth',
934
		5 => 'last',
935
	];
936
937
	$f = formatDateTimeString(
938
		$relDaysOfWeek[$tzrule['day']],
939
		$daysOfWeek[$tzrule['dayofweek']],
940
		$monthNames[$tzrule['month']],
941
		$year,
942
		$tzrule['hour'],
943
		$tzrule['minute'],
944
	);
945
	$dt = new DateTime($f, new DateTimeZone($fOffset));
946
947
	return $dt->getTimestamp();
948
}
949
950
/**
951
 * Returns TRUE if DST is in effect.
952
 *
953
 * 1. Check if the timezone defines std and dst times
954
 * 2. Get the std and dst start in UTC
955
 * 3. Check if the appointment is in dst:
956
 *    - dst start > std start and not (std start < app time < dst start)
957
 *    - dst start < std start and std start > app time > dst start
958
 *
959
 * @param array $tzrules
960
 * @param int   $startdate
961
 *
962
 * @return bool
963
 */
964
function isDst($tzrules, $startdate) {
965
	if (array_sum($tzrules['stStandardDate']) == 0 || array_sum($tzrules['stDaylightDate']) == 0) {
966
		return false;
967
	}
968
	$appStartDate = getdate($startdate);
969
	$fOffset = convertOffset($tzrules['bias']);
970
971
	$tzStdStart = getRuleStart($tzrules['stStandardDate'], $appStartDate['year'], $fOffset);
972
	$tzDstStart = getRuleStart($tzrules['stDaylightDate'], $appStartDate['year'], $fOffset);
973
974
	return
975
		(($tzDstStart > $tzStdStart) && !($startdate > $tzStdStart && $startdate < $tzDstStart)) ||
976
		(($tzDstStart < $tzStdStart) && ($startdate < $tzStdStart && $startdate > $tzDstStart));
977
}
978
979
/**
980
 * Calculates the local startime for a timezone from a timestamp.
981
 *
982
 * @param int    $ts
983
 * @param string $tz
984
 *
985
 * @return int
986
 */
987
function getLocalStart($ts, $tz) {
988
	$calItemStart = new DateTime();
989
	$calItemStart->setTimestamp($ts);
990
	$clientDate = DateTime::createFromInterface($calItemStart);
991
	$clientDate->setTimezone(new DateTimeZone($tz));
992
	// It's only necessary to calculate new start and end times
993
	// if the appointment does not start at midnight
994
	if ((int) $clientDate->format("His") != 0) {
995
		$clientMidnight = DateTimeImmutable::createFromFormat(
996
			"Y-m-d H:i:s",
997
			$clientDate->format("Y-m-d ") . "00:00:00",
998
			$clientDate->getTimezone()
999
		);
1000
		$interval = $clientDate->getTimestamp() - $clientMidnight->getTimestamp();
1001
		// The code here is based on assumption that if the interval
1002
		// is greater than 12 hours then the appointment takes place
1003
		// on the day before or after. This should be fine for all the
1004
		// timezones which do not exceed 12 hour difference to UTC.
1005
		$ts = $interval > 0 ?
1006
			$ts - ($interval < 43200 ? $interval : $interval - 86400) :
1007
			$ts + ($interval > -43200 ? $interval : $interval - 86400);
1008
	}
1009
1010
	return $ts;
1011
}
1012