Issues (752)

plugins/kendox/php/class.kendox-module.php (14 issues)

1
<?php
2
3
use Kendox\Client;
4
5
require_once __DIR__ . '/vendor/autoload.php';
6
require_once __DIR__ . "/kendox-client/class.kendox-client.php";
7
require_once __DIR__ . "/class.attachment-info.php";
8
require_once __DIR__ . "/class.uploadfile.php";
9
10
class KendoxModule extends Module {
11
	// Certificate prod environment
12
	public $pfxFile = __DIR__ . '/../resources/KendoxCertificate.pfx';
13
	public $pfxPw = 'KxTest';
14
15
	// Certificate test environment
16
	public $pfxFileTest = __DIR__ . '/../resources/KendoxCertificate.pfx';
17
	public $pfxPwTest = 'KxTest';
18
19
	public $dialogUrl;
20
	public $mapiMessage;
21
22
	public $kendoxClient;
23
24
	/**
25
	 * @constructor
26
	 *
27
	 * @param mixed $id
28
	 * @param mixed $data
29
	 */
30
	public function __construct($id, $data) {
31
		parent::__construct($id, $data);
32
		$this->store = $GLOBALS['mapisession']->getDefaultMessageStore();
0 ignored issues
show
Bug Best Practice introduced by
The property store does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
33
	}
34
35
	/**
36
	 * Executes all the actions in the $data variable.
37
	 * Exception part is used for authentication errors also.
38
	 *
39
	 * @return bool true on success or false on failure
40
	 */
41
	#[Override]
42
	public function execute() {
43
		foreach ($this->data as $actionType => $actionData) {
44
			if (isset($actionType)) {
45
				try {
46
					switch ($actionType) {
47
						case "attachmentinfo":
48
							$response = $this->getAttachmentInfo($actionData["storeId"], $actionData["mailEntryId"]);
49
							$this->addActionData($actionType, $response);
50
							$GLOBALS['bus']->addData($this->getResponseData());
51
							break;
52
53
						case "upload":
54
							$response = $this->upload($actionData["storeId"], $actionData["mailEntryId"], $actionData["uploadType"], $actionData["selectedAttachments"], $actionData["environment"], $actionData["apiUrl"], $actionData["userEMail"]);
55
							$this->addActionData($actionType, $response);
56
							$GLOBALS['bus']->addData($this->getResponseData());
57
							break;
58
					}
59
				}
60
				catch (Exception $e) {
61
					$response = [];
62
					$response["Successful"] = false;
63
					$response["errorMessage"] = $e->getMessage();
64
					$this->addActionData($actionType, $response);
65
					$GLOBALS['bus']->addData($this->getResponseData());
66
				}
67
			}
68
		}
69
	}
70
71
	/**
72
	 * Get attachment information (meta data) of mail.
73
	 *
74
	 * @param mixed $storeId
75
	 * @param mixed $mailEntryId
76
	 */
77
	private function getAttachmentInfo($storeId, $mailEntryId) {
78
		$this->loadMapiMessage($storeId, $mailEntryId);
79
		$items = [];
80
		$attachmentTable = mapi_message_getattachmenttable($this->mapiMessage);
81
		$messageAttachments = mapi_table_queryallrows($attachmentTable, [PR_ATTACH_NUM, PR_ATTACH_SIZE, PR_ATTACH_LONG_FILENAME]);
82
		foreach ($messageAttachments as $att) {
83
			$item = new AttachmentInfo();
84
			$item->id = $att[PR_ATTACH_NUM];
85
			$item->name = $att[PR_ATTACH_LONG_FILENAME];
86
			$item->size = $att[PR_ATTACH_SIZE];
87
			$items[] = $item;
88
		}
89
		$response = [];
90
		$response["Successful"] = true;
91
		$response["attachments"] = $items;
92
93
		return $response;
94
	}
95
96
	/**
97
	 * Upload of e-mail to InfoShare.
98
	 *
99
	 * @param string $mailEntryId         Mail Entry ID
100
	 * @param mixed  $storeId
101
	 * @param mixed  $uploadType
102
	 * @param mixed  $selectedAttachments
103
	 * @param mixed  $environment
104
	 * @param mixed  $apiUrl
105
	 * @param mixed  $userEMail
106
	 *
107
	 * @return object
108
	 */
109
	private function upload($storeId, $mailEntryId, $uploadType, $selectedAttachments, $environment, $apiUrl, $userEMail) {
110
		$emlFile = null;
111
		$this->loadMapiMessage($storeId, $mailEntryId);
112
		// Write temporary message file (.EML)
113
		// Send to Kendox InfoShare
114
		$uploadFiles = [];
115
		if ($uploadType == "fullEmail") {
116
			$emlFile = $this->createTempEmlFileFromMapiMessage($mailEntryId);
117
			if (!file_exists($emlFile)) {
118
				throw new Exception("EML file " . $emlFile . " not available.");
119
			}
120
			$file = new UploadFile();
121
			$file->tempFile = $emlFile;
122
			$file->fileType = "email";
123
			$file->fileName = "email.eml";
124
			$file->fileLength = filesize($emlFile);
0 ignored issues
show
The property fileLength does not seem to exist on UploadFile.
Loading history...
125
			$file->kendoxFileId = null;
126
			$uploadFiles[] = $file;
127
		}
128
		if ($uploadType == "attachmentsOnly") {
129
			$uploadFiles = $this->getUploadFilesFromSelectedAttachments($selectedAttachments);
130
		}
131
132
		try {
133
			$this->sendFiles($environment, $uploadFiles, $apiUrl, $userEMail);
134
		}
135
		catch (Exception $ex) {
136
			throw $ex;
137
		}
138
		finally {
139
			if ($emlFile != null) {
0 ignored issues
show
It seems like you are loosely comparing $emlFile of type null|string against null; this is ambiguous if the string can be empty. Consider using a strict comparison !== instead.
Loading history...
140
				@unlink($emlFile);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for unlink(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

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

140
				/** @scrutinizer ignore-unhandled */ @unlink($emlFile);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
141
			}
142
			foreach ($uploadFiles as $uploadFile) {
143
				@unlink($uploadFile->tempFile);
144
			}
145
		}
146
147
		try {
148
			// Return response
149
			$response = [];
150
			$response["Successful"] = true;
151
			$response["apiUrl"] = $apiUrl;
152
			$response["userEMail"] = $userEMail;
153
			$response["messageId"] = $mailEntryId;
154
			$response["kendoxConnectionId"] = $this->kendoxClient->ConnectionId;
155
			$response["kendoxFiles"] = $uploadFiles;
156
		}
157
		catch (Exception $ex) {
158
			if ($emlFile != null) {
0 ignored issues
show
It seems like you are loosely comparing $emlFile of type null|string against null; this is ambiguous if the string can be empty. Consider using a strict comparison !== instead.
Loading history...
159
				@unlink($emlFile);
160
			}
161
			if ($uploadFiles != null) {
162
				foreach ($uploadFiles as $uploadFile) {
163
					@unlink($uploadFile->tempFile);
164
				}
165
			}
166
			$this->logErrorAndThrow("Error on building response message", $ex);
167
		}
168
169
		return $response;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $response returns the type array which is incompatible with the documented return type object.
Loading history...
170
	}
171
172
	private function loadMapiMessage($storeId, $mailEntryId) {
173
		try {
174
			// Read message store
175
			$store = $GLOBALS['mapisession']->openMessageStore(hex2bin((string) $storeId));
176
		}
177
		catch (Exception $ex) {
178
			$this->logErrorAndThrow("Error on open MAPI store", $ex);
179
		}
180
181
		try {
182
			$this->mapiMessage = mapi_msgstore_openentry($store, hex2bin((string) $mailEntryId));
183
		}
184
		catch (Exception $ex) {
185
			$this->logErrorAndThrow("Error on open MAPI message", $ex);
186
		}
187
	}
188
189
	private function createTempEmlFileFromMapiMessage($mailEntryId) {
190
		// Read message properties
191
		try {
192
			$messageProps = mapi_getprops($this->mapiMessage, [PR_SUBJECT, PR_MESSAGE_CLASS]);
193
		}
194
		catch (Exception $ex) {
195
			$this->logErrorAndThrow("Error on getting MAPI message properties", $ex);
196
		}
197
198
		// Get EML-Stream
199
		try {
200
			$fileName = $this->sanitizeValue($mailEntryId, '', ID_REGEX) . '.eml';
0 ignored issues
show
The assignment to $fileName is dead and can be removed.
Loading history...
201
			$stream = $this->getEmlStream($messageProps);
202
			$stat = mapi_stream_stat($stream);
203
		}
204
		catch (Exception $ex) {
205
			$this->logErrorAndThrow("Error on reading EML stream from MAPI Message", $ex);
206
		}
207
208
		// Create temporary file
209
		try {
210
			// Set the file length
211
			$fileLength = $stat['cb'];
212
			$tempFile = $this->createTempFilename();
213
			// Read stream for whole message
214
			for ($i = 0; $i < $fileLength; $i += BLOCK_SIZE) {
215
				$appendData = mapi_stream_read($stream, BLOCK_SIZE);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $stream does not seem to be defined for all execution paths leading up to this point.
Loading history...
216
				file_put_contents($tempFile, $appendData, FILE_APPEND);
217
			}
218
219
			return $tempFile;
220
		}
221
		catch (Exception $ex) {
222
			$this->logErrorAndThrow("Error on writing temporary EML file", $ex);
223
		}
224
	}
225
226
	private function createTempFilename() {
227
		$tempPath = TMP_PATH;
228
229
		return $tempPath . $this->getGUID() . ".tmp";
230
	}
231
232
	public function getGUID() {
233
		if (function_exists('com_create_guid')) {
234
			return com_create_guid();
235
		}
236
		mt_srand((float) microtime() * 10000); // optional for php 4.2.0 and up.
0 ignored issues
show
(double)microtime() * 10000 of type double is incompatible with the type integer expected by parameter $seed of mt_srand(). ( Ignorable by Annotation )

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

236
		mt_srand(/** @scrutinizer ignore-type */ (float) microtime() * 10000); // optional for php 4.2.0 and up.
Loading history...
237
		$charid = strtoupper(md5(uniqid(random_int(0, mt_getrandmax()), true)));
238
		$hyphen = chr(45); // "-"
239
240
		return chr(123) . // "{"
241
			substr($charid, 0, 8) . $hyphen .
242
			substr($charid, 8, 4) . $hyphen .
243
			substr($charid, 12, 4) . $hyphen .
244
			substr($charid, 16, 4) . $hyphen .
245
			substr($charid, 20, 12) .
246
			chr(125); // "}"
247
	}
248
249
	private function getUploadFilesFromSelectedAttachments($selectedAttachments) {
250
		try {
251
			$uploadFiles = [];
252
			$attachmentTable = mapi_message_getattachmenttable($this->mapiMessage);
253
			$messageAttachments = mapi_table_queryallrows($attachmentTable, [PR_ATTACH_NUM, PR_ATTACH_SIZE, PR_ATTACH_LONG_FILENAME]);
254
			foreach ($selectedAttachments as $att) {
255
				$tmpFile = $this->createTempFilename();
256
257
				try {
258
					$this->saveAttachmentToTempFile($tmpFile, $att["attachmentNumber"]);
259
					$file = new UploadFile();
260
					$file->tempFile = $tmpFile;
261
					$file->fileType = "attachment";
262
					$file->fileName = $messageAttachments[$att["attachmentNumber"]][PR_ATTACH_LONG_FILENAME];
263
					$file->fileLength = filesize($tmpFile);
0 ignored issues
show
The property fileLength does not seem to exist on UploadFile.
Loading history...
264
					$file->kendoxFileId = null;
265
					$uploadFiles[] = $file;
266
				}
267
				catch (Exception $exFile) {
268
					@unlink($tmpFile);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for unlink(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

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

268
					/** @scrutinizer ignore-unhandled */ @unlink($tmpFile);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
269
270
					throw $exFile;
271
				}
272
			}
273
			if (count($uploadFiles) == 0) {
274
				throw new Exception("No attachments selected.");
275
			}
276
277
			return $uploadFiles;
278
		}
279
		catch (Exception $ex) {
280
			$this->logErrorAndThrow("Error on creating upload files from attachment.", $ex);
281
		}
282
	}
283
284
	/**
285
	 * Sending file to Kendox InfoShare.
286
	 *
287
	 * @param UploadFile[] $uploadFiles Files to upload
288
	 * @param mixed        $environment
289
	 * @param mixed        $apiUrl
290
	 * @param mixed        $userEMail
291
	 */
292
	private function sendFiles($environment, $uploadFiles, $apiUrl, $userEMail) {
293
		try {
294
			$pfx = $this->pfxFile;
295
			$pfxPw = $this->pfxPw;
296
			if ($environment != "prod") {
297
				$pfx = $this->pfxFileTest;
298
				$pfxPw = $this->pfxPwTest;
299
			}
300
			if (!file_exists($pfx)) {
301
				throw new Exception(_("Kendox certificate is not available."));
302
			}
303
			$this->kendoxClient = new Client($apiUrl);
304
			$uid = $this->kendoxClient->loginWithToken($pfx, $pfxPw, "svc_grommunio");
0 ignored issues
show
The assignment to $uid is dead and can be removed.
Loading history...
305
			$query = [
306
				[
307
					"ColumnName" => "email",
308
					"RelationalOperator" => "Equals",
309
					"Value" => $userEMail],
310
			];
311
			$result = $this->kendoxClient->userTableQuery("grommunio", $query, false);
312
			if (count($result) == 0) {
313
				throw new Exception("User with e-mail address " . $userEMail . " not found in Kendox user table grommunio.");
314
			}
315
			$uid = $result[0][1];
316
			$this->kendoxClient->logout();
317
			// Login and upload
318
			$this->kendoxClient->loginWithToken($pfx, $pfxPw, $uid);
319
			foreach ($uploadFiles as $uploadFile) {
320
				try {
321
					$uploadFile->kendoxFileId = $this->kendoxClient->uploadFile($uploadFile->tempFile);
322
				}
323
				catch (Exception $exUpload) {
324
					$this->logErrorAndThrow("Upload of file " . $uploadFile->fileName . " failed.", $exUpload);
325
				}
326
			}
327
		}
328
		catch (Exception $ex) {
329
			$this->logErrorAndThrow("Sending files failed", $ex);
330
		}
331
	}
332
333
	/**
334
	 * Logging an error and throw the exception.
335
	 *
336
	 * @param string    $displayMessage
337
	 * @param Exception $ex
338
	 */
339
	public function logErrorAndThrow($displayMessage, $ex): never {
340
		$errMsg = $displayMessage . ": " . $ex->getMessage();
341
		error_log($errMsg);
342
		error_log($ex->getTraceAsString());
343
344
		throw new Exception($errMsg);
345
	}
346
347
	/**
348
	 * Function will obtain stream from the message, For email messages it will open email as
349
	 * inet object and get the stream content as eml format.
350
	 *
351
	 * @param array $messageProps properties of this particular message
352
	 *
353
	 * @return Stream $stream the eml stream obtained from message
0 ignored issues
show
The type Stream 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...
354
	 */
355
	public function getEmlStream($messageProps) {
356
		$addrBook = $GLOBALS['mapisession']->getAddressbook();
357
358
		return mapi_inetmapi_imtoinet($GLOBALS['mapisession']->getSession(), $addrBook, $this->mapiMessage, []);
359
	}
360
361
	/**
362
	 * Function to get binary content of an attachment
363
	 * PR_ATTACH_DATA_BIN.
364
	 *
365
	 * @param string $tempFile         Path and file of temporary attachment file
366
	 * @param int    $attachmentNumber Number of attachment
367
	 *
368
	 * @return string
369
	 */
370
	public function saveAttachmentToTempFile($tempFile, $attachmentNumber) {
371
		$attach = mapi_message_openattach($this->mapiMessage, $attachmentNumber);
372
		file_put_contents($tempFile, mapi_attach_openbin($attach, PR_ATTACH_DATA_BIN));
373
	}
374
375
	/**
376
	 * Function to sanitize user input values to prevent XSS attacks.
377
	 *
378
	 * @param mixed  $value   value that should be sanitized
379
	 * @param mixed  $default default value to return when value is not safe
380
	 * @param string $regex   regex to validate values based on type of value passed
381
	 */
382
	public function sanitizeValue($value, $default = '', $regex = false) {
383
		$result = addslashes((string) $value);
384
		if ($regex) {
385
			$match = preg_match_all($regex, $result);
386
			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...
387
				$result = $default;
388
			}
389
		}
390
391
		return $result;
392
	}
393
}
394