Passed
Push — master ( d44c66...31a96e )
by
unknown
16:00 queued 12s
created

getRuleStart()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 23
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 1 Features 0
Metric Value
cc 1
eloc 18
c 1
b 1
f 0
nc 1
nop 3
dl 0
loc 23
rs 9.6666
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') || 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, ]);
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
				$recipienttable = mapi_message_getrecipienttable($message);
517
				if (!isset($GLOBALS["properties"])) {
518
					$GLOBALS["properties"] = new Properties();
519
				}
520
				$messageRecipients = mapi_table_queryallrows($recipienttable, $GLOBALS["properties"]->getRecipientProperties());
521
522
				mapi_inetmapi_imtomapi($GLOBALS['mapisession']->getSession(), $store, $GLOBALS['mapisession']->getAddressbook(), $message, $data, ["parse_smime_signed" => 1]);
523
524
				if (!empty($messageRecipients)) {
525
					mapi_message_modifyrecipients($message, MODRECIP_ADD, $messageRecipients);
526
				}
527
528
				mapi_setprops($message, [
529
					PR_MESSAGE_CLASS => $props[PR_MESSAGE_CLASS],
530
					PR_SENT_REPRESENTING_NAME => $props[PR_SENT_REPRESENTING_NAME],
531
					PR_SENT_REPRESENTING_ENTRYID => $props[PR_SENT_REPRESENTING_ENTRYID],
532
					PR_SENT_REPRESENTING_SEARCH_KEY => $props[PR_SENT_REPRESENTING_SEARCH_KEY],
533
					PR_SENT_REPRESENTING_EMAIL_ADDRESS => $props[PR_SENT_REPRESENTING_EMAIL_ADDRESS] ?? '',
534
					PR_SENT_REPRESENTING_SMTP_ADDRESS => $props[PR_SENT_REPRESENTING_SMTP_ADDRESS] ?? '',
535
					PR_SENT_REPRESENTING_ADDRTYPE => $props[PR_SENT_REPRESENTING_ADDRTYPE] ?? 'SMTP',
536
				]);
537
			}
538
		}
539
		elseif (isset($props[PR_MESSAGE_CLASS]) && stripos($props[PR_MESSAGE_CLASS], 'IPM.Note.SMIME') !== false) {
540
			// this is a encrypted message. decode it.
541
			$attachTable = mapi_message_getattachmenttable($message);
542
543
			$rows = mapi_table_queryallrows($attachTable, [PR_ATTACH_MIME_TAG, PR_ATTACH_NUM, PR_ATTACH_LONG_FILENAME]);
544
			$attnum = false;
545
			foreach ($rows as $row) {
546
				if (isset($row[PR_ATTACH_MIME_TAG]) && in_array($row[PR_ATTACH_MIME_TAG], ['application/x-pkcs7-mime', 'application/pkcs7-mime'])) {
547
					$attnum = $row[PR_ATTACH_NUM];
548
				}
549
			}
550
551
			if ($attnum !== false) {
552
				$att = mapi_message_openattach($message, $attnum);
553
				$data = mapi_openproperty($att, PR_ATTACH_DATA_BIN);
554
555
				// Allowing to hook in before the encrypted attachment is removed
556
				$GLOBALS['PluginManager']->triggerHook('server.util.parse_smime.encrypted', [
557
					'store' => $store,
558
					'props' => $props,
559
					'message' => &$message,
560
					'data' => &$data,
561
				]);
562
563
				if (isSmimePluginEnabled()) {
564
					mapi_message_deleteattach($message, $attnum);
565
				}
566
			}
567
		}
568
		// mark the message as read if the main message has read flag
569
		if ($read) {
570
			$mprops = mapi_getprops($message, [PR_MESSAGE_FLAGS]);
571
			mapi_setprops($message, [PR_MESSAGE_FLAGS => $mprops[PR_MESSAGE_FLAGS] | MSGFLAG_READ]);
572
		}
573
	}
574
575
	/**
576
	 * Helper function which used to check smime plugin is enabled.
577
	 *
578
	 * @return bool true if smime plugin is enabled else false
579
	 */
580
	function isSmimePluginEnabled() {
581
		return $GLOBALS['settings']->get("zarafa/v1/plugins/smime/enable", false);
582
	}
583
584
	/**
585
	 * Helper to stream a MAPI property.
586
	 *
587
	 * @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...
588
	 * @param mixed      $proptag
589
	 *
590
	 * @return string $datastring the streamed data
591
	 */
592
	function streamProperty($mapiobj, $proptag) {
593
		$stream = mapi_openproperty($mapiobj, $proptag, IID_IStream, 0, 0);
594
		$stat = mapi_stream_stat($stream);
595
		mapi_stream_seek($stream, 0, STREAM_SEEK_SET);
596
597
		$datastring = '';
598
		for ($i = 0; $i < $stat['cb']; $i += BLOCK_SIZE) {
599
			$datastring .= mapi_stream_read($stream, BLOCK_SIZE);
600
		}
601
602
		return $datastring;
603
	}
604
605
	/**
606
	 * Function will decode JSON string into objects.
607
	 *
608
	 * @param {String} $jsonString JSON data that should be decoded
0 ignored issues
show
Documentation Bug introduced by
The doc comment {String} at position 0 could not be parsed: Unknown type name '{' at position 0 in {String}.
Loading history...
609
	 * @param {Boolean} $toAssoc flag to indicate that associative arrays should be
610
	 * returned as objects or arrays, true means it will return associative array as arrays and
611
	 * false will return associative arrays as objects
612
	 *
613
	 * @return {Object} decoded data
0 ignored issues
show
Documentation Bug introduced by
The doc comment {Object} at position 0 could not be parsed: Unknown type name '{' at position 0 in {Object}.
Loading history...
614
	 */
615
	function json_decode_data($jsonString, $toAssoc = false) {
616
		$data = json_decode($jsonString, $toAssoc);
617
		$errorString = '';
618
619
		switch (json_last_error()) {
620
			case JSON_ERROR_DEPTH:
621
				$errorString = _("The maximum stack depth has been exceeded");
622
				break;
623
624
			case JSON_ERROR_CTRL_CHAR:
625
				$errorString = _("Control character error, possibly incorrectly encoded");
626
				break;
627
628
			case JSON_ERROR_STATE_MISMATCH:
629
				$errorString = _("Invalid or malformed JSON");
630
				break;
631
632
			case JSON_ERROR_SYNTAX:
633
				$errorString = _("Syntax error");
634
				break;
635
636
			case JSON_ERROR_UTF8:
637
				$errorString = _("Malformed UTF-8 characters, possibly incorrectly encoded");
638
				break;
639
		}
640
641
		if (!empty($errorString)) {
642
			throw new JsonException(sprintf(_("JSON Error: - %s"), $errorString), json_last_error(), null, _("Some problem encountered when encoding/decoding JSON data."));
0 ignored issues
show
Unused Code introduced by
The call to JsonException::__construct() has too many arguments starting with _('Some problem encounte...g/decoding JSON data.'). ( Ignorable by Annotation )

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

642
			throw /** @scrutinizer ignore-call */ new JsonException(sprintf(_("JSON Error: - %s"), $errorString), json_last_error(), null, _("Some problem encountered when encoding/decoding JSON data."));

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

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

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