Test Failed
Push — master ( 19e7e7...e85ccb )
by
unknown
07:48
created

useSecureCookies()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 2
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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

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