Test Failed
Push — master ( 63c9aa...89bf96 )
by
unknown
21:07 queued 14s
created

parse_smime()   D

Complexity

Conditions 23
Paths 30

Size

Total Lines 108
Code Lines 67

Duplication

Lines 0
Ratio 0 %

Importance

Changes 6
Bugs 0 Features 0
Metric Value
cc 23
eloc 67
c 6
b 0
f 0
nc 30
nop 2
dl 0
loc 108
rs 4.1666

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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
/**
479
 * Function will be used to decode smime messages and convert it to normal messages.
480
 *
481
 * @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...
482
 * @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...
483
 */
484
function parse_smime($store, $message) {
485
	$props = mapi_getprops($message, [PR_MESSAGE_CLASS, PR_MESSAGE_FLAGS,
486
	PR_SENT_REPRESENTING_NAME, PR_SENT_REPRESENTING_ENTRYID, PR_SENT_REPRESENTING_SEARCH_KEY,
487
	PR_SENT_REPRESENTING_EMAIL_ADDRESS, PR_SENT_REPRESENTING_SMTP_ADDRESS, PR_SENT_REPRESENTING_ADDRTYPE, PR_CLIENT_SUBMIT_TIME, ]);
488
	$read = $props[PR_MESSAGE_FLAGS] & MSGFLAG_READ;
489
490
	if (isset($props[PR_MESSAGE_CLASS]) && stripos($props[PR_MESSAGE_CLASS], 'IPM.Note.SMIME.MultipartSigned') !== false) {
491
		// this is a signed message. decode it.
492
		$atable = mapi_message_getattachmenttable($message);
493
494
		$rows = mapi_table_queryallrows($atable, [PR_ATTACH_MIME_TAG, PR_ATTACH_NUM]);
495
		$attnum = false;
496
497
		foreach ($rows as $row) {
498
			if (isset($row[PR_ATTACH_MIME_TAG]) && $row[PR_ATTACH_MIME_TAG] == 'multipart/signed') {
499
				$attnum = $row[PR_ATTACH_NUM];
500
			}
501
		}
502
503
		if ($attnum !== false) {
504
			$att = mapi_message_openattach($message, $attnum);
505
			$data = mapi_openproperty($att, PR_ATTACH_DATA_BIN);
506
507
			// Allowing to hook in before the signed attachment is removed
508
			$GLOBALS['PluginManager']->triggerHook('server.util.parse_smime.signed', [
509
				'store' => $store,
510
				'props' => $props,
511
				'message' => &$message,
512
				'data' => &$data,
513
			]);
514
515
			// also copy recipients because they are lost after mapi_inetmapi_imtomapi
516
			$origRcptTable = mapi_message_getrecipienttable($message);
517
			if (!isset($GLOBALS["properties"])) {
518
				$GLOBALS["properties"] = new Properties();
519
			}
520
			$origRecipients = mapi_table_queryallrows($origRcptTable, $GLOBALS["properties"]->getRecipientProperties());
521
522
			mapi_inetmapi_imtomapi($GLOBALS['mapisession']->getSession(), $store, $GLOBALS['mapisession']->getAddressbook(), $message, $data, ["parse_smime_signed" => 1]);
523
524
			$decapRcptTable = mapi_message_getrecipienttable($message);
525
			$decapRecipients = mapi_table_queryallrows($decapRcptTable, $GLOBALS["properties"]->getRecipientProperties());
526
			if (empty($decapRecipients) && !empty($origRecipients)) {
527
				mapi_message_modifyrecipients($message, MODRECIP_ADD, $origRecipients);
528
			}
529
530
			mapi_setprops($message, [
531
				PR_MESSAGE_CLASS => $props[PR_MESSAGE_CLASS],
532
				PR_SENT_REPRESENTING_NAME => $props[PR_SENT_REPRESENTING_NAME],
533
				PR_SENT_REPRESENTING_ENTRYID => $props[PR_SENT_REPRESENTING_ENTRYID],
534
				PR_SENT_REPRESENTING_SEARCH_KEY => $props[PR_SENT_REPRESENTING_SEARCH_KEY],
535
				PR_SENT_REPRESENTING_EMAIL_ADDRESS => $props[PR_SENT_REPRESENTING_EMAIL_ADDRESS] ?? '',
536
				PR_SENT_REPRESENTING_SMTP_ADDRESS => $props[PR_SENT_REPRESENTING_SMTP_ADDRESS] ?? '',
537
				PR_SENT_REPRESENTING_ADDRTYPE => $props[PR_SENT_REPRESENTING_ADDRTYPE] ?? 'SMTP',
538
				PR_CLIENT_SUBMIT_TIME => $props[PR_CLIENT_SUBMIT_TIME] ?? time(),
539
			]);
540
		}
541
	}
542
	elseif (isset($props[PR_MESSAGE_CLASS]) && stripos($props[PR_MESSAGE_CLASS], 'IPM.Note.SMIME') !== false) {
543
		// this is a encrypted message. decode it.
544
		$attachTable = mapi_message_getattachmenttable($message);
545
546
		$rows = mapi_table_queryallrows($attachTable, [PR_ATTACH_MIME_TAG, PR_ATTACH_NUM, PR_ATTACH_LONG_FILENAME]);
547
		$attnum = false;
548
		foreach ($rows as $row) {
549
			if (isset($row[PR_ATTACH_MIME_TAG]) && in_array($row[PR_ATTACH_MIME_TAG], ['application/x-pkcs7-mime', 'application/pkcs7-mime'])) {
550
				$attnum = $row[PR_ATTACH_NUM];
551
			}
552
		}
553
554
		if ($attnum !== false) {
555
			$att = mapi_message_openattach($message, $attnum);
556
			$data = mapi_openproperty($att, PR_ATTACH_DATA_BIN);
557
558
			// also copy recipients because they are lost after decrypting
559
			$origRcptTable = mapi_message_getrecipienttable($message);
560
			if (!isset($GLOBALS["properties"])) {
561
				$GLOBALS["properties"] = new Properties();
562
			}
563
			$origRecipients = mapi_table_queryallrows($origRcptTable, $GLOBALS["properties"]->getRecipientProperties());
564
565
			// Allowing to hook in before the encrypted attachment is removed
566
			$GLOBALS['PluginManager']->triggerHook('server.util.parse_smime.encrypted', [
567
				'store' => $store,
568
				'props' => $props,
569
				'message' => &$message,
570
				'data' => &$data,
571
			]);
572
573
			// after decrypting $message is a IPM.Note message,
574
			// deleting an attachment removes an actual attachment of the message
575
			$mprops = mapi_getprops($message, [PR_MESSAGE_CLASS]);
576
			if (isSmimePluginEnabled() && isset($mprops[PR_MESSAGE_CLASS]) &&
577
				stripos($mprops[PR_MESSAGE_CLASS], 'IPM.Note.SMIME') !== false) {
578
				mapi_message_deleteattach($message, $attnum);
579
			}
580
581
			$decapRcptTable = mapi_message_getrecipienttable($message);
582
			$decapRecipients = mapi_table_queryallrows($decapRcptTable, $GLOBALS["properties"]->getRecipientProperties());
583
			if (empty($decapRecipients) && !empty($origRecipients)) {
584
				mapi_message_modifyrecipients($message, MODRECIP_ADD, $origRecipients);
585
			}
586
		}
587
	}
588
	// mark the message as read if the main message has read flag
589
	if ($read) {
590
		$mprops = mapi_getprops($message, [PR_MESSAGE_FLAGS]);
591
		mapi_setprops($message, [PR_MESSAGE_FLAGS => $mprops[PR_MESSAGE_FLAGS] | MSGFLAG_READ]);
592
	}
593
}
594
595
/**
596
 * Helper function which used to check smime plugin is enabled.
597
 *
598
 * @return bool true if smime plugin is enabled else false
599
 */
600
function isSmimePluginEnabled() {
601
	return $GLOBALS['settings']->get("zarafa/v1/plugins/smime/enable", false);
602
}
603
604
/**
605
 * Helper to stream a MAPI property.
606
 *
607
 * @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...
608
 * @param mixed      $proptag
609
 *
610
 * @return string $datastring the streamed data
611
 */
612
function streamProperty($mapiobj, $proptag) {
613
	$stream = mapi_openproperty($mapiobj, $proptag, IID_IStream, 0, 0);
614
	$stat = mapi_stream_stat($stream);
615
	mapi_stream_seek($stream, 0, STREAM_SEEK_SET);
616
617
	$datastring = '';
618
	for ($i = 0; $i < $stat['cb']; $i += BLOCK_SIZE) {
619
		$datastring .= mapi_stream_read($stream, BLOCK_SIZE);
620
	}
621
622
	return $datastring;
623
}
624
625
/**
626
 * Function will decode JSON string into objects.
627
 *
628
 * @param string $jsonString JSON data that should be decoded
629
 * @param bool $toAssoc flag to indicate that associative arrays should be
630
 * returned as objects or arrays, true means it will return associative array as arrays and
631
 * false will return associative arrays as objects
632
 *
633
 * @return object decoded data
634
 */
635
function json_decode_data($jsonString, $toAssoc = false) {
636
	$data = json_decode($jsonString, $toAssoc);
637
	$errorString = '';
638
639
	switch (json_last_error()) {
640
		case JSON_ERROR_DEPTH:
641
			$errorString = _("The maximum stack depth has been exceeded");
642
			break;
643
644
		case JSON_ERROR_CTRL_CHAR:
645
			$errorString = _("Control character error, possibly incorrectly encoded");
646
			break;
647
648
		case JSON_ERROR_STATE_MISMATCH:
649
			$errorString = _("Invalid or malformed JSON");
650
			break;
651
652
		case JSON_ERROR_SYNTAX:
653
			$errorString = _("Syntax error");
654
			break;
655
656
		case JSON_ERROR_UTF8:
657
			$errorString = _("Malformed UTF-8 characters, possibly incorrectly encoded");
658
			break;
659
	}
660
661
	if (!empty($errorString)) {
662
		throw new JsonException(sprintf(_("Some problem encountered when encoding/decoding JSON data: - %s"), $errorString), json_last_error(), null);
663
	}
664
665
	return $data;
666
}
667
668
/**
669
 * Tries to open the IPM subtree. If opening fails, it will try to fix it by
670
 * trying to find the correct entryid of the IPM subtree in the hierarchy.
671
 *
672
 * @param resource $store the store to retrieve IPM subtree from
673
 *
674
 * @return mixed false if the subtree is broken beyond quick repair,
675
 * the IPM subtree resource otherwise
676
 */
677
function getSubTree($store) {
678
	$storeProps = mapi_getprops($store, [PR_IPM_SUBTREE_ENTRYID]);
679
680
	try {
681
		$ipmsubtree = mapi_msgstore_openentry($store, $storeProps[PR_IPM_SUBTREE_ENTRYID]);
682
	}
683
	catch (MAPIException $e) {
684
		if ($e->getCode() == MAPI_E_NOT_FOUND || $e->getCode() == MAPI_E_INVALID_ENTRYID) {
685
			$username = $GLOBALS["mapisession"]->getUserName();
686
			error_log(sprintf('Unable to open IPM_SUBTREE for %s, trying to correct PR_IPM_SUBTREE_ENTRYID', $username));
687
			$ipmsubtree = fix_ipmsubtree($store);
0 ignored issues
show
Bug introduced by
$store of type resource is incompatible with the type object expected by parameter $store of fix_ipmsubtree(). ( Ignorable by Annotation )

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

687
			$ipmsubtree = fix_ipmsubtree(/** @scrutinizer ignore-type */ $store);
Loading history...
688
		}
689
	}
690
691
	return $ipmsubtree;
692
}
693
694
/**
695
 * Fetches the full hierarchy and returns an array with a cache of the stat
696
 * of the folders in the hierarchy. Passing the folderType is required for cases where
697
 * the user has permission on the inbox folder, but no folder visible
698
 * rights on the rest of the store.
699
 *
700
 * @param string $username the user who's store to retrieve hierarchy counters from.
701
 * If no username is given, the currently logged in user's store will be used.
702
 * @param string $folderType if inbox use the inbox as root folder
703
 *
704
 * @return array folderStatCache a cache of the hierarchy folders
705
 */
706
function updateHierarchyCounters($username = '', $folderType = '') {
707
	// Open the correct store
708
	if ($username) {
709
		$userEntryid = $GLOBALS["mapisession"]->getStoreEntryIdOfUser($username);
710
		$store = $GLOBALS["mapisession"]->openMessageStore($userEntryid);
711
	}
712
	else {
713
		$store = $GLOBALS["mapisession"]->getDefaultMessageStore();
714
	}
715
716
	$props = [PR_DISPLAY_NAME, PR_LOCAL_COMMIT_TIME_MAX, PR_CONTENT_COUNT, PR_CONTENT_UNREAD, PR_ENTRYID, PR_STORE_ENTRYID];
717
718
	if ($folderType === 'inbox') {
719
		try {
720
			$rootFolder = mapi_msgstore_getreceivefolder($store);
721
		}
722
		catch (MAPIException $e) {
723
			$username = $GLOBALS["mapisession"]->getUserName();
724
			error_log(sprintf("Unable to open Inbox for %s. MAPI Error '%s'", $username, get_mapi_error_name($e->getCode())));
725
726
			return [];
727
		}
728
	}
729
	else {
730
		$rootFolder = getSubTree($store);
731
	}
732
733
	$hierarchy = mapi_folder_gethierarchytable($rootFolder, CONVENIENT_DEPTH | MAPI_DEFERRED_ERRORS);
734
	$rows = mapi_table_queryallrows($hierarchy, $props);
735
736
	// Append the Inbox folder itself.
737
	if ($folderType === 'inbox') {
738
		array_push($rows, mapi_getprops($rootFolder, $props));
739
	}
740
741
	$folderStatCache = [];
742
	foreach ($rows as $folder) {
743
		$folderStatCache[$folder[PR_DISPLAY_NAME]] = [
744
			'commit_time' => isset($folder[PR_LOCAL_COMMIT_TIME_MAX]) ? $folder[PR_LOCAL_COMMIT_TIME_MAX] : "0000000000",
745
			'entryid' => bin2hex($folder[PR_ENTRYID]),
746
			'store_entryid' => bin2hex($folder[PR_STORE_ENTRYID]),
747
			'content_count' => isset($folder[PR_CONTENT_COUNT]) ? $folder[PR_CONTENT_COUNT] : -1,
748
			'content_unread' => isset($folder[PR_CONTENT_UNREAD]) ? $folder[PR_CONTENT_UNREAD] : -1,
749
		];
750
	}
751
752
	return $folderStatCache;
753
}
754
755
/**
756
 * Fix the PR_IPM_SUBTREE_ENTRYID in the Store properties when it is broken,
757
 * by looking up the IPM_SUBTREE in the Hierarchytable and fetching the entryid
758
 * and if found, setting the PR_IPM_SUBTREE_ENTRYID to that found entryid.
759
 *
760
 * @param object $store the users MAPI Store
761
 *
762
 * @return mixed false if unable to correct otherwise return the subtree
763
 */
764
function fix_ipmsubtree($store) {
765
	$root = mapi_msgstore_openentry($store, null);
766
	$username = $GLOBALS["mapisession"]->getUserName();
767
	$hierarchytable = mapi_folder_gethierarchytable($root);
768
	mapi_table_restrict($hierarchytable, [RES_CONTENT,
769
		[
770
			FUZZYLEVEL => FL_PREFIX,
771
			ULPROPTAG => PR_DISPLAY_NAME,
772
			VALUE => [PR_DISPLAY_NAME => "IPM_SUBTREE"],
773
		],
774
	]);
775
776
	$folders = mapi_table_queryallrows($hierarchytable, [PR_ENTRYID]);
777
	if (empty($folders)) {
778
		error_log(sprintf("No IPM_SUBTREE found for %s, store is broken", $username));
779
780
		return false;
781
	}
782
783
	try {
784
		$entryid = $folders[0][PR_ENTRYID];
785
		$ipmsubtree = mapi_msgstore_openentry($store, $entryid);
786
	}
787
	catch (MAPIException $e) {
788
		error_log(sprintf(
789
			'Unable to open IPM_SUBTREE for %s, IPM_SUBTREE folder can not be opened. MAPI error: %s',
790
			$username,
791
			get_mapi_error_name($e->getCode())
792
		));
793
794
		return false;
795
	}
796
797
	mapi_setprops($store, [PR_IPM_SUBTREE_ENTRYID => $entryid]);
798
	error_log(sprintf('Fixed PR_IPM_SUBTREE_ENTRYID for %s', $username));
799
800
	return $ipmsubtree;
801
}
802
803
/**
804
 * Helper function which provide protocol used by current request.
805
 *
806
 * @return string it can be either https or http
807
 */
808
function getRequestProtocol() {
809
	if (!empty($_SERVER['HTTP_X_FORWARDED_PROTO'])) {
810
		return $_SERVER['HTTP_X_FORWARDED_PROTO'];
811
	}
812
813
	return !empty($_SERVER['HTTPS']) ? "https" : "http";
814
}
815
816
/**
817
 * Helper function which defines that webapp has to use secure cookies
818
 * or not. by default webapp always use secure cookies whether or not
819
 * 'SECURE_COOKIES' defined. webapp only use insecure cookies
820
 * where a user has explicitly set 'SECURE_COOKIES' to false.
821
 *
822
 * @return bool return false only when a user has explicitly set
823
 *              'SECURE_COOKIES' to false else returns true
824
 */
825
function useSecureCookies() {
826
	return !defined('SECURE_COOKIES') || SECURE_COOKIES !== false;
827
}
828
829
/**
830
 * Check if the eml stream is corrupted or not.
831
 *
832
 * @param string $attachment content fetched from PR_ATTACH_DATA_BIN property of an attachment
833
 *
834
 * @return true if eml is broken, false otherwise
835
 */
836
function isBrokenEml($attachment) {
837
	// Get header part to process further
838
	$splittedContent = preg_split("/\r?\n\r?\n/", $attachment);
839
840
	// Fetch raw header
841
	if (preg_match_all('/([^\n^:]+:)/', $splittedContent[0], $matches)) {
842
		$rawHeaders = $matches[1];
843
	}
844
845
	// Compare if necessary headers are present or not
846
	if (isset($rawHeaders) && in_array('From:', $rawHeaders) && in_array('Date:', $rawHeaders)) {
847
		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...
848
	}
849
850
	return true;
851
}
852
853
/**
854
 * Function returns the IP address of the client.
855
 *
856
 * @return string the IP address of the client
857
 */
858
function getClientIPAddress() {
859
	// Here, there is a scenario where the server is behind a proxy, when that
860
	// happens, 'REMOTE_ADDR' will not return the real IP, there is another variable
861
	// 'HTTP_X_FORWARDED_FOR' which is set by a proxy server. But the risk in using that
862
	// is that it can be easily forged. 'REMOTE_ADDR' is the only reliable thing
863
	// as it is nearly impossible to be altered.
864
	return $_SERVER['REMOTE_ADDR'];
865
}
866
867
/**
868
 * Helper function which return the webapp version.
869
 *
870
 * @return string webapp version
871
 */
872
function getWebappVersion() {
873
	return trim(file_get_contents('version'));
874
}
875
876
/**
877
 * function which remove double quotes or PREF from vcf stream
878
 * if it has.
879
 *
880
 * @param string $attachmentStream The attachment stream
881
 */
882
function processVCFStream(&$attachmentStream) {
883
	/*
884
	 * https://github.com/libical/libical/issues/488
885
	 * https://github.com/libical/libical/issues/490
886
	 *
887
	 * Because of above issues we need to remove
888
	 * double quotes or PREF from vcf stream if
889
	 * it exists in vcf stream.
890
	 */
891
	if (preg_match('/"/', $attachmentStream) > 0) {
892
		$attachmentStream = str_replace('"', '', $attachmentStream);
893
	}
894
895
	if (preg_match('/EMAIL;PREF=/', $attachmentStream) > 0) {
896
		$rows = explode("\n", $attachmentStream);
897
		foreach ($rows as $key => $row) {
898
			if (preg_match("/EMAIL;PREF=/", $row)) {
899
				unset($rows[$key]);
900
			}
901
		}
902
903
		$attachmentStream = join("\n", $rows);
904
	}
905
}
906
907
/**
908
 * Formats time string for DateTime object, e.g.
909
 * last Sunday of March 2022 02:00.
910
 *
911
 * @param mixed $relDayofWeek
912
 * @param mixed $dayOfWeek
913
 * @param mixed $month
914
 * @param mixed $year
915
 * @param mixed $hour
916
 * @param mixed $minute
917
 */
918
function formatDateTimeString($relDayofWeek, $dayOfWeek, $month, $year, $hour, $minute) {
919
	return sprintf("%s %s of %s %04d %02d:%02d", $relDayofWeek, $dayOfWeek, $month, $year, $hour, $minute);
920
}
921
922
/**
923
 * Converts offset minutes to PHP TimeZone offset (+0200/-0530).
924
 *
925
 * Note: it is necessary to invert the bias sign in order to receive
926
 * the correct offset (-60 => +0100).
927
 *
928
 * @param int $minutes
929
 *
930
 * @return string PHP TimeZone offset
931
 */
932
function convertOffset($minutes) {
933
	$m = abs($minutes);
934
935
	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

935
	return sprintf("%s%02d%02d", $minutes > 0 ? '-' : '+', intdiv(/** @scrutinizer ignore-type */ $m, 60), $m % 60);
Loading history...
936
}
937
938
/**
939
 * Returns the index of effective rule (TZRULE_FLAG_EFFECTIVE_TZREG).
940
 *
941
 * @param array $tzrules
942
 *
943
 * @return null|int
944
 */
945
function getEffectiveTzreg($tzrules) {
946
	foreach ($tzrules as $idx => $tzDefRule) {
947
		if ($tzDefRule['tzruleflags'] & TZRULE_FLAG_EFFECTIVE_TZREG) {
948
			return $idx;
949
		}
950
	}
951
952
	return null;
953
}
954
955
/**
956
 * Returns the timestamp of std or dst start.
957
 *
958
 * @param array  $tzrule
959
 * @param int    $year
960
 * @param string $fOffset
961
 *
962
 * @return int
963
 */
964
function getRuleStart($tzrule, $year, $fOffset) {
965
	$daysOfWeek = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
966
	$monthNames = [1 => "January", "February", "March", "April", "May", "June",
967
		"July", "August", "September", "October", "November", "December", ];
968
	$relDaysOfWeek = [
969
		1 => 'first',
970
		2 => 'second',
971
		3 => 'third',
972
		4 => 'fourth',
973
		5 => 'last',
974
	];
975
976
	$f = formatDateTimeString(
977
		$relDaysOfWeek[$tzrule['day']],
978
		$daysOfWeek[$tzrule['dayofweek']],
979
		$monthNames[$tzrule['month']],
980
		$year,
981
		$tzrule['hour'],
982
		$tzrule['minute'],
983
	);
984
	$dt = new DateTime($f, new DateTimeZone($fOffset));
985
986
	return $dt->getTimestamp();
987
}
988
989
/**
990
 * Returns TRUE if DST is in effect.
991
 *
992
 * 1. Check if the timezone defines std and dst times
993
 * 2. Get the std and dst start in UTC
994
 * 3. Check if the appointment is in dst:
995
 *    - dst start > std start and not (std start < app time < dst start)
996
 *    - dst start < std start and std start > app time > dst start
997
 *
998
 * @param array $tzrules
999
 * @param int   $startdate
1000
 *
1001
 * @return bool
1002
 */
1003
function isDst($tzrules, $startdate) {
1004
	if (array_sum($tzrules['stStandardDate']) == 0 || array_sum($tzrules['stDaylightDate']) == 0) {
1005
		return false;
1006
	}
1007
	$appStartDate = getdate($startdate);
1008
	$fOffset = convertOffset($tzrules['bias']);
1009
1010
	$tzStdStart = getRuleStart($tzrules['stStandardDate'], $appStartDate['year'], $fOffset);
1011
	$tzDstStart = getRuleStart($tzrules['stDaylightDate'], $appStartDate['year'], $fOffset);
1012
1013
	return
1014
		(($tzDstStart > $tzStdStart) && !($startdate > $tzStdStart && $startdate < $tzDstStart)) ||
1015
		(($tzDstStart < $tzStdStart) && ($startdate < $tzStdStart && $startdate > $tzDstStart));
1016
}
1017