Test Failed
Push — master ( 647c72...cd42b5 )
by
unknown
10:25
created

server/includes/download_attachment.php (2 issues)

1
<?php
2
// required to handle php errors
3
require_once(__DIR__ . '/exceptions/class.ZarafaErrorException.php');
4
require_once(__DIR__ . '/download_base.php');
5
6
/**
7
 * DownloadAttachment
8
 *
9
 * A class to manage downloading of attachments from message, additionally
10
 * this class can be used to download inline images from message as well.
11
 *
12
 * Main reason to create this class is to not pollute the global namespace.
13
 */
14
class DownloadAttachment extends DownloadBase
15
{
16
	/**
17
	 * Content disposition type for the attachment that will be sent with header with the attachment data
18
	 * Possible values are 'inline' and 'attachment'. When content-type is application/octet-stream and
19
	 * content disposition type is 'attachment' then browser will show dialog to save attachment as instead of
20
	 * directly displaying content inline.
21
	 */
22
	private $contentDispositionType;
23
24
	/**
25
	 * Attachment number of the attachment that should be downloaded. For normal attachments this will contain
26
	 * a single element array with numeric value as sequential attachment number, for attachments that are not saved
27
	 * in AttachmentTable of MAPIMessage yet (recently uploaded attachments) this will give single element array
28
	 * having value as a string in form of 'filename randomstring'. When accessing embedded messages this array can contain
29
	 * multiple elements indicating attachment numbers at each level, So value [0, 1] will indicate we want to download
30
	 * second attachment of first embedded message.
31
	 */
32
	private $attachNum;
33
34
	/**
35
	 * Attachment Content Id is used to download inline images of the MAPIMessage, When requesting inline images only
36
	 * content id is passed but if we are accessing inline image from embedded message then besides content id,
37
	 * attachment number is also passed to access embedded message.
38
	 */
39
	private $attachCid;
40
41
	/**
42
	 * A string that will be initialized with grommunio Web-specific and common-for-all file name for ZIP file.
43
	 */
44
	private $zipFileName;
45
46
	/**
47
	 * A random string that will be generated with every MAPIMessage instance to uniquely identify attachments that
48
	 * belongs to this MAPIMessage, this is mainly used to get recently uploaded attachments for MAPIMessage.
49
	 */
50
	private $dialogAttachments;
51
52
	/**
53
	 * A boolean value, set to false by default, to define if the message, of which the attachments are required to be wrapped in ZIP,
54
	 * is a sub message of other webapp item or not.
55
	 */
56
	private $isSubMessage;
57
58
	/**
59
	 * Entryid of the MAPIFolder to which the given attachment needs to be imported as webapp item.
60
	 */
61
	private $destinationFolderId;
62
63
	/**
64
	 * Resource of the MAPIFolder to which the given attachment needs to be imported as webapp item.
65
	 */
66
	private $destinationFolder;
67
68
	/**
69
	 * A boolean value, set to false by default, to define if the attachment needs to be imported into folder as webapp item.
70
	 */
71
	private $import;
72
73
	/**
74
	 * A boolean value, set to false by default, to define if the embedded attachment needs to be imported into folder.
75
	 */
76
	private $isEmbedded;
77
78
	/**
79
	 * Resource of the shared MAPIStore into which attachments needs to be imported.
80
	 */
81
	private $otherStore;
82
83
	/**
84
	 * Constructor
85
	 */
86
	public function __construct()
87
	{
88
		$this->contentDispositionType = 'attachment';
89
		$this->attachNum = array();
90
		$this->attachCid = false;
91
		$this->zipFileName = _('Attachments').'%s.zip';
92
		$this->messageSubject = '';
93
		$this->isSubMessage = false;
94
		$this->destinationFolderId = false;
95
		$this->destinationFolder = false;
96
		$this->import = false;
97
		$this->isEmbedded = false;
98
		$this->otherStore = false;
99
100
		parent:: __construct();
101
	}
102
103
	/**
104
	 * Function will initialize data for this class object. it will also sanitize data
105
	 * for possible XSS attack because data is received in $_GET
106
	 */
107
	public function init($data)
108
	{
109
		if(isset($data['store'])) {
110
			$this->store = sanitizeValue($data['store'], '', ID_REGEX);
111
		}
112
113
		if(isset($data['entryid'])) {
114
			$this->entryId = sanitizeValue($data['entryid'], '', ID_REGEX);
115
		}
116
117
		if(isset($data['contentDispositionType'])) {
118
			$this->contentDispositionType = sanitizeValue($data['contentDispositionType'], 'attachment', STRING_REGEX);
119
		}
120
121
		if(!empty($data['attachNum'])) {
122
			/**
123
			 * if you are opening an already saved attachment then $data["attachNum"]
124
			 * will contain array of numeric index for that attachment (like 0 or 1 or 2)
125
			 *
126
			 * if you are opening a recently uploaded attachment then $data["attachNum"]
127
			 * will be a one element array and it will contain a string in "filename.randomstring" format
128
			 * like README.txtu6K6AH
129
			 */
130
			foreach($data['attachNum'] as $attachNum) {
131
				$num = sanitizeValue($attachNum, false, NUMERIC_REGEX);
132
133
				if($num === false) {
134
					// string is passed in attachNum so get it
135
					$num = sanitizeValue($attachNum, '', FILENAME_REGEX);
136
137
					if(!empty($num)) {
138
						array_push($this->attachNum, $num);
139
					}
140
				} else {
141
					array_push($this->attachNum, (int) $num);
142
				}
143
			}
144
		}
145
146
		if(isset($data['attachCid'])) {
147
			$this->attachCid = rawurldecode($data['attachCid']);
148
		}
149
150
		if(isset($data['AllAsZip'])) {
151
			$this->allAsZip = sanitizeValue($data['AllAsZip'], '', STRING_REGEX);
152
		}
153
154
		if(isset($data['subject'])) {
155
			// Remove characters that we cannot use in a filename
156
			$data['subject'] = preg_replace('/[^a-z0-9 ()]/mi', '_', $data['subject']);
157
			$this->messageSubject = sanitizeValue($data['subject'], '', FILENAME_REGEX);
158
		}
159
160
		if($this->allAsZip && isset($data['isSubMessage'])){
161
			$this->isSubMessage = sanitizeValue($data['isSubMessage'], '', STRING_REGEX);
162
		}
163
164
		if(isset($data['dialog_attachments'])) {
165
			$this->dialogAttachments = sanitizeValue($data['dialog_attachments'], '', STRING_REGEX);
166
		}
167
168
		if($this->store && $this->entryId) {
169
			$this->store = $GLOBALS['mapisession']->openMessageStore(hex2bin($this->store));
170
			$this->message = mapi_msgstore_openentry($this->store, hex2bin($this->entryId));
171
172
			// Decode smime signed messages on this message
173
			parse_smime($this->store, $this->message);
174
		}
175
176
		if(isset($data['destination_folder'])) {
177
			$this->destinationFolderId = sanitizeValue($data['destination_folder'], '', ID_REGEX);
178
179
			if ($this->destinationFolder === false){
180
				try {
181
					$this->destinationFolder = mapi_msgstore_openentry($this->store, hex2bin($this->destinationFolderId));
182
				} catch(Exception $e) {
183
					// Try to find the folder from shared stores in case if it is not found in current user's store
184
					$this->otherStore = $GLOBALS['operations']->getOtherStoreFromEntryid($this->destinationFolderId);
185
					if ($this->otherStore !== false) {
186
						$this->destinationFolder = mapi_msgstore_openentry($this->otherStore, hex2bin($this->destinationFolderId));
187
					} else {
188
						$this->destinationFolder = mapi_msgstore_openentry($GLOBALS["mapisession"]->getPublicMessageStore(), hex2bin($this->destinationFolderId));
189
						if (!$this->destinationFolder) {
190
							throw new ZarafaException(_("Destination folder not found."));
191
						}
192
					}
193
				}
194
			}
195
		}
196
197
		if(isset($data['import'])) {
198
			$this->import = sanitizeValue($data['import'], '', STRING_REGEX);
199
		}
200
201
		if(isset($data['is_embedded'])) {
202
			$this->isEmbedded = sanitizeValue($data['is_embedded'], '', STRING_REGEX);
203
		}
204
	}
205
206
	/**
207
	 * Returns inline image attachment based on specified attachCid, To get inline image attachment
208
	 * we need to compare passed attachCid with PR_ATTACH_CONTENT_ID, PR_ATTACH_CONTENT_LOCATION or
209
	 * PR_ATTACH_FILENAME and if that matches then we can get that attachment.
210
	 * @param MAPIAttach $attachment (optional) embedded message attachment from where we need to get the inline image
211
	 * @return MAPIAttach attachment that is requested and will be sent to client
212
	 */
213
	public function getAttachmentByAttachCid($attachment = false)
214
	{
215
		// If the inline image was in a submessage, we have to open that first
216
		if($attachment !== false) {
217
			$this->message = mapi_attach_openobj($attachment);
218
		}
219
220
		/**
221
		 * restriction to find inline image attachment with matching cid passed
222
		 */
223
		$restriction =	Array(RES_OR,
224
							Array(
225
								Array(RES_CONTENT,
226
									Array(
227
										FUZZYLEVEL => FL_FULLSTRING | FL_IGNORECASE,
228
										ULPROPTAG => PR_ATTACH_CONTENT_ID,
229
										VALUE => array(PR_ATTACH_CONTENT_ID => $this->attachCid)
230
									)
231
								),
232
								Array(RES_CONTENT,
233
									Array(
234
										FUZZYLEVEL => FL_FULLSTRING | FL_IGNORECASE,
235
										ULPROPTAG => PR_ATTACH_CONTENT_LOCATION,
236
										VALUE => array(PR_ATTACH_CONTENT_LOCATION => $this->attachCid)
237
									)
238
								),
239
								Array(RES_CONTENT,
240
									Array(
241
										FUZZYLEVEL => FL_FULLSTRING | FL_IGNORECASE,
242
										ULPROPTAG => PR_ATTACH_FILENAME,
243
										VALUE => array(PR_ATTACH_FILENAME => $this->attachCid)
244
									)
245
								)
246
							)
247
						);
248
249
		// Get the attachment table
250
		$attachTable = mapi_message_getattachmenttable($this->message);
251
		mapi_table_restrict($attachTable, $restriction, TBL_BATCH);
252
		$attachments = mapi_table_queryallrows($attachTable, Array(PR_ATTACH_NUM));
253
254
		if(count($attachments) > 0) {
255
			// there should be only one attachment
256
			$attachment = mapi_message_openattach($this->message, $attachments[0][PR_ATTACH_NUM]);
257
		}
258
259
		return $attachment;
260
	}
261
262
	/**
263
	 * Returns attachment based on specified attachNum, additionally it will also get embedded message
264
	 * if we want to get the inline image attachment.
265
	 * @return MAPIAttach embedded message attachment or attachment that is requested
266
	 */
267
	public function getAttachmentByAttachNum()
268
	{
269
		$attachment = false;
270
271
		$len = count($this->attachNum);
272
273
		// Loop through the attachNums, message in message in message ...
274
		for($index = 0; $index < $len - 1; $index++) {
275
			// Open the attachment
276
			$tempattach = mapi_message_openattach($this->message, $this->attachNum[$index]);
277
			if($tempattach) {
278
				// Open the object in the attachment
279
				$this->message = mapi_attach_openobj($tempattach);
280
			}
281
		}
282
283
		// open the attachment
284
		$attachment = mapi_message_openattach($this->message, $this->attachNum[$len - 1]);
285
286
		return $attachment;
287
	}
288
289
	/**
290
	 * Function will open passed attachment and generate response for that attachment to send it to client.
291
	 * This should only be used to download attachment that is already saved in MAPIMessage.
292
	 * @param MAPIAttach $attachment attachment which will be dumped to client side
293
	 * @param Boolean $inline inline attachment or not
294
	 * @return Response response to sent to client including attachment data
295
	 */
296
	public function downloadSavedAttachment($attachment, $inline = False)
297
	{
298
		// Check if the attachment is opened
299
		if($attachment) {
300
			// Get the props of the attachment
301
			$props = mapi_attach_getprops($attachment, array(PR_ATTACH_FILENAME, PR_ATTACH_LONG_FILENAME, PR_ATTACH_MIME_TAG, PR_DISPLAY_NAME, PR_ATTACH_METHOD, PR_ATTACH_CONTENT_ID));
302
			// Content Type
303
			$contentType = 'application/octet-stream';
304
			// Filename
305
			$filename = 'ERROR';
306
307
			// Set filename
308
			if ($inline) {
309
				/*
310
				 * Inline attachments are set to "inline.txt" by Gromox, see inetmapi/VMIMEToMAPI.cpp and search for inline.txt.
311
				 * Gromox would have to extract the alt/title tag from the img tag when converting it to MAPI. Since it
312
				 * does not handle this, set the filename to CONTENT_ID plus mime tag.
313
				 */
314
				$tags = explode('/', $props[PR_ATTACH_MIME_TAG]);
315
				// IE 11 is weird, when a user renames the file it's not saved as in image, when
316
				// the filename is "test.jpeg", but it works when it's "test.jpg".
317
				$filename = $props[PR_ATTACH_CONTENT_ID] . '.' . str_replace('jpeg', 'jpg', $tags[1]);
318
			} else if(isset($props[PR_ATTACH_LONG_FILENAME])) {
319
				$filename = $props[PR_ATTACH_LONG_FILENAME];
320
			} else if(isset($props[PR_ATTACH_FILENAME])) {
321
				$filename = $props[PR_ATTACH_FILENAME];
322
			} else if(isset($props[PR_DISPLAY_NAME])) {
323
				$filename = $props[PR_DISPLAY_NAME];
324
			}
325
326
			// Set content type if available, otherwise it will be default to application/octet-stream
327
			if(isset($props[PR_ATTACH_MIME_TAG])) {
328
				$contentType = $props[PR_ATTACH_MIME_TAG];
329
			}
330
331
			$contentIsSentAsUTF8 = false;
332
			// For ODF files we must change the content type because otherwise
333
			// IE<11 cannot properly read it in the xmlhttprequest object
334
			// NOTE: We only need to check for IE<=10, so no need to check for TRIDENT (IE11)
335
			preg_match('/MSIE (.*?);/', $_SERVER['HTTP_USER_AGENT'], $matches);
336
			if (count($matches)>1){
337
				if ( strpos($contentType, 'application/vnd.oasis.opendocument.') !== false ){
338
					$contentType = 'text/plain; charset=UTF-8';
339
					$contentIsSentAsUTF8 = true;
340
				}
341
			}
342
343
			// Set the headers
344
			header('Pragma: public');
345
			header('Expires: 0'); // set expiration time
346
			header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
347
			header('Content-Disposition: ' . $this->contentDispositionType . '; filename="' . addslashes(browserDependingHTTPHeaderEncode($filename)) . '"');
348
			header('Content-Type: ' . $contentType);
349
			header('Content-Transfer-Encoding: binary');
350
351
			// Open a stream to get the attachment data
352
			$stream = mapi_openproperty($attachment, PR_ATTACH_DATA_BIN, IID_IStream, 0, 0);
353
			$stat = mapi_stream_stat($stream);
354
			// File length
355
			header('Content-Length: ' . $stat['cb']);
356
357
			// Read the attachment content from the stream
358
			$body = '';
359
			for($i = 0; $i < $stat['cb']; $i += BLOCK_SIZE) {
360
				$body .= mapi_stream_read($stream, BLOCK_SIZE);
361
			}
362
363
			// Convert the content to UTF-8 if we want to send it like that
364
			if ( $contentIsSentAsUTF8 ){
365
				$body = mb_convert_encoding($body, 'UTF-8');
366
			}
367
			echo $body;
368
		}
369
	}
370
371
	/**
372
	 * Helper function to configure header information which is required to send response as a ZIP archive
373
	 * containing all the attachments.
374
	 * @param String $randomZipName A random zip archive name.
375
	 */
376
	public function sendZipResponse($randomZipName)
377
	{
378
		$subject = isset($this->messageSubject) ? ' '.$this->messageSubject : '';
379
380
		// Set the headers
381
		header('Pragma: public');
382
		header('Expires: 0'); // set expiration time
383
		header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
384
		header('Content-Disposition: ' . $this->contentDispositionType . '; filename="' . addslashes(browserDependingHTTPHeaderEncode(sprintf($this->zipFileName, $subject))) . '"');
385
		header('Content-Transfer-Encoding: binary');
386
		header('Content-Type:  application/zip');
387
		header('Content-Length: ' . filesize($randomZipName));
388
389
		// Send the actual response as ZIP file
390
		readfile($randomZipName);
391
392
		// Remove the zip file to avoid unnecessary disk-space consumption
393
		unlink($randomZipName);
394
	}
395
396
	/**
397
	 * Function will open all attachments of message and prepare a ZIP file response for that attachment to send it to client.
398
	 * This should only be used to download attachment that is already saved in MAPIMessage.
399
	 * @param AttachmentState $attachment_state Object of AttachmentState class.
400
	 * @param ZipArchive $zip ZipArchive object.
401
	 */
402
	public function addAttachmentsToZipArchive($attachment_state, $zip)
403
	{
404
		// Get all the attachments from message
405
		$attachmentTable = mapi_message_getattachmenttable($this->message);
406
		$attachments = mapi_table_queryallrows($attachmentTable, array(PR_ATTACH_NUM, PR_ATTACH_METHOD));
407
408
		foreach($attachments as $attachmentRow) {
409
			if($attachmentRow[PR_ATTACH_METHOD] !== ATTACH_EMBEDDED_MSG) {
410
				$attachment = mapi_message_openattach($this->message, $attachmentRow[PR_ATTACH_NUM]);
411
412
				// Prevent inclusion of inline attachments and contact photos into ZIP
413
				if(!$attachment_state->isInlineAttachment($attachment) && !$attachment_state->isContactPhoto($attachment)){
414
					$props = mapi_attach_getprops($attachment, array(PR_ATTACH_LONG_FILENAME));
415
416
					// Open a stream to get the attachment data
417
					$stream = mapi_openproperty($attachment, PR_ATTACH_DATA_BIN, IID_IStream, 0, 0);
418
					$stat = mapi_stream_stat($stream);
419
420
					// Get the stream
421
					$datastring = '';
422
					for($i = 0; $i < $stat['cb']; $i += BLOCK_SIZE) {
423
						$datastring .=  mapi_stream_read($stream, BLOCK_SIZE);
424
					}
425
426
					// Add file into zip by stream
427
					$fileDownloadName = $this->handleDuplicateFileNames($props[PR_ATTACH_LONG_FILENAME]);
428
					$zip->addFromString($fileDownloadName, $datastring);
429
				}
430
			}
431
		}
432
433
		// Go for adding unsaved attachments in ZIP, if any.
434
		// This situation arise while user upload attachments in draft.
435
		$attachmentFiles = $attachment_state->getAttachmentFiles($this->dialogAttachments);
436
		if($attachmentFiles){
437
			$this->addUnsavedAttachmentsToZipArchive($attachment_state, $zip);
438
		}
439
	}
440
441
	/**
442
	 * Function will send attachment data to client side.
443
	 * This should only be used to download attachment that is recently uploaded and not saved in MAPIMessage.
444
	 * @return Response response to sent to client including attachment data
445
	 */
446
	public function downloadUnsavedAttachment()
447
	{
448
		// return recently uploaded file
449
		$attachment_state = new AttachmentState();
450
		$attachment_state->open();
451
452
		// there will be only one value in attachNum so directly access 0th element of it
453
		$tmpname = $attachment_state->getAttachmentPath($this->attachNum[0]);
454
		$fileinfo = $attachment_state->getAttachmentFile($this->dialogAttachments, $this->attachNum[0]);
455
456
		// Check if the file still exists
457
		if (is_file($tmpname)) {
458
			// Set the headers
459
			header('Pragma: public');
460
			header('Expires: 0'); // set expiration time
461
			header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
462
			header('Content-Disposition: ' . $this->contentDispositionType . '; filename="' . addslashes(browserDependingHTTPHeaderEncode($fileinfo['name'])) . '"');
463
			header('Content-Transfer-Encoding: binary');
464
			header('Content-Type: application/octet-stream');
465
			header('Content-Length: ' . filesize($tmpname));
466
467
			// Open the uploaded file and print it
468
			$file = fopen($tmpname, 'r');
469
			fpassthru($file);
470
			fclose($file);
471
		} else if($fileinfo['sourcetype'] === 'icsfile') {
472
			// When "Send to" option used with calendar item. which create the new mail with
473
			// ics file as an attachment and now user try to download the ics attachment before saving
474
			// mail at that time this code is used to download the ics file successfully.
475
			$messageStore = $GLOBALS['mapisession']->openMessageStore(hex2bin($fileinfo['store_entryid']));
476
			$message = mapi_msgstore_openentry($messageStore , hex2bin($fileinfo['entryid']));
477
478
			// Get address book for current session
479
			$addrBook = $GLOBALS['mapisession']->getAddressbook();
480
481
			// get message properties.
482
			$messageProps = mapi_getprops($message, array(PR_SUBJECT));
483
484
			// Read the appointment as RFC2445-formatted ics stream.
485
			$appointmentStream = mapi_mapitoical($GLOBALS['mapisession']->getSession(), $addrBook, $message, array());
486
487
			$filename = (!empty($messageProps[PR_SUBJECT])) ? $messageProps[PR_SUBJECT] : _('Untitled');
488
			$filename .= '.ics';
489
			// Set the headers
490
			header('Pragma: public');
491
			header('Expires: 0'); // set expiration time
492
			header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
493
			header('Content-Transfer-Encoding: binary');
494
495
			// Set Content Disposition header
496
			header('Content-Disposition: ' . $this->contentDispositionType . '; filename="' . addslashes(browserDependingHTTPHeaderEncode($filename)) . '"');
497
			// Set content type header
498
			header('Content-Type: application/octet-stream');
499
500
			// Set the file length
501
			header('Content-Length: ' . strlen($appointmentStream));
502
503
			$split = str_split($appointmentStream, BLOCK_SIZE);
504
			foreach ($split as $s) echo $s;
505
		}
506
		$attachment_state->close();
507
	}
508
509
	/**
510
	 * Function will send all the attachments to client side wrapped in a ZIP file.
511
	 * This should only be used to download all the attachments that are recently uploaded and not saved in MAPIMessage.
512
	 * @param AttachmentState $attachment_state Object of AttachmentState class.
513
	 * @param ZipArchive $zip ZipArchive object.
514
	 */
515
	public function addUnsavedAttachmentsToZipArchive($attachment_state, $zip)
516
	{
517
		//Get recently uploaded attachment files
518
		$attachmentFiles = $attachment_state->getAttachmentFiles($this->dialogAttachments);
519
520
		foreach ($attachmentFiles as $fileName => $fileInfo) {
521
			$filePath = $attachment_state->getAttachmentPath($fileName);
522
			// Avoid including contact photo and embedded messages in ZIP
523
			if ($fileInfo['sourcetype'] !== 'embedded' && $fileInfo['sourcetype'] !== 'contactphoto') {
524
				$fileDownloadName = $this->handleDuplicateFileNames( $fileInfo['name']);
525
				$zip->addFile($filePath, $fileDownloadName);
526
			}
527
		}
528
	}
529
530
	/**
531
	 * Function will get the attachement and import it to the given MAPIFolder as webapp item.
532
	 */
533
	public function importAttachment()
534
	{
535
		$addrBook = $GLOBALS['mapisession']->getAddressbook();
536
537
		$newMessage = mapi_folder_createmessage($this->destinationFolder);
538
		$attachment = $this->getAttachmentByAttachNum();
539
		$attachmentProps = mapi_attach_getprops($attachment, array(PR_ATTACH_LONG_FILENAME));
540
		$attachmentStream = streamProperty($attachment, PR_ATTACH_DATA_BIN);
541
542
		switch(pathinfo($attachmentProps[PR_ATTACH_LONG_FILENAME], PATHINFO_EXTENSION))
543
		{
544
			case 'eml':
545
				if (isBrokenEml($attachmentStream)) {
546
					throw new ZarafaException(_("Eml is corrupted"));
547
				} else {
548
					try {
549
						// Convert an RFC822-formatted e-mail to a MAPI Message
550
						$ok = mapi_inetmapi_imtomapi($GLOBALS['mapisession']->getSession(), $this->store, $addrBook, $newMessage, $attachmentStream, array());
551
					} catch(Exception $e) {
552
						throw new ZarafaException(_("The eml Attachment is not imported successfully"));
553
					}
554
				}
555
				break;
556
557
			case 'vcf':
558
				try {
559
					// Convert an RFC6350-formatted vCard to a MAPI Contact
560
					$ok = mapi_vcftomapi($GLOBALS['mapisession']->getSession(), $this->store, $newMessage, $attachmentStream);
561
				} catch(Exception $e) {
562
					throw new ZarafaException(_("The vcf attachment is not imported successfully"));
563
				}
564
				break;
565
			case 'vcs':
566
			case 'ics':
567
				try {
568
					// Convert vCalendar 1.0 or iCalendar to a MAPI Appointment
569
					$ok = mapi_icaltomapi($GLOBALS['mapisession']->getSession(), $this->store, $addrBook, $newMessage, $attachmentStream, false);
570
				} catch(Exception $e) {
571
					$destinationFolderProps = mapi_getprops($this->destinationFolder, array(PR_DISPLAY_NAME, PR_MDB_PROVIDER));
572
					$fullyQualifiedFolderName = $destinationFolderProps[PR_DISPLAY_NAME];
573
					if ($destinationFolderProps[PR_MDB_PROVIDER] === ZARAFA_STORE_PUBLIC_GUID) {
574
						$publicStore = $GLOBALS["mapisession"]->getPublicMessageStore();
575
						$publicStoreName = mapi_getprops($publicStore, array(PR_DISPLAY_NAME));
576
						$fullyQualifiedFolderName .= " - " . $publicStoreName[PR_DISPLAY_NAME];
577
					} else if ($destinationFolderProps[PR_MDB_PROVIDER] === ZARAFA_STORE_DELEGATE_GUID) {
578
						$sharedStoreOwnerName = mapi_getprops($this->otherStore, array(PR_MAILBOX_OWNER_NAME));
579
						$fullyQualifiedFolderName .= " - " . $sharedStoreOwnerName[PR_MAILBOX_OWNER_NAME];
580
					}
581
582
					$message = sprintf(_("Unable to import '%s' to '%s'. "), $attachmentProps[PR_ATTACH_LONG_FILENAME], $fullyQualifiedFolderName);
583
					if ($e->getCode() === MAPI_E_TABLE_EMPTY) {
584
						$message .= _("There is no appointment found in this file.");
585
					} else if ($e->getCode() === MAPI_E_CORRUPT_DATA) {
586
						$message .= _("The file is corrupt.");
587
					} else if ($e->getCode() === MAPI_E_INVALID_PARAMETER) {
588
						$message .= _("The file is invalid.");
589
					} else {
590
						$message = sprintf(_("Unable to import '%s'. "), $attachmentProps[PR_ATTACH_LONG_FILENAME]) . _("Please contact your system administrator if the problem persists.");
591
					}
592
593
					$e = new ZarafaException($message);
594
					$e->setTitle(_("Import error"));
595
					throw $e;
596
				}
597
				break;
598
		}
599
600
		if($ok === true) {
601
			mapi_savechanges($newMessage);
602
603
			// Check that record is not appointment record. we have to only convert the
604
			// Meeting request record to appointment record.
605
			$newMessageProps = mapi_getprops($newMessage, array(PR_MESSAGE_CLASS));
606
			if (isset($newMessageProps[PR_MESSAGE_CLASS]) && $newMessageProps[PR_MESSAGE_CLASS] !== 'IPM.Appointment') {
607
				// Convert the Meeting request record to proper appointment record so we can
608
				// properly show the appointment in calendar.
609
				$req = new Meetingrequest($this->store, $newMessage, $GLOBALS['mapisession']->getSession(), ENABLE_DIRECT_BOOKING);
610
				$req->doAccept(true, false, false, false,false,false, false, false,false, true);
611
			}
612
			$storeProps = mapi_getprops($this->store, array(PR_ENTRYID));
613
			$destinationFolderProps = mapi_getprops($this->destinationFolder, array(PR_PARENT_ENTRYID, PR_CONTENT_UNREAD));
614
615
			$return = Array(
616
				// 'success' property is needed for Extjs Ext.form.Action.Submit#success handler
617
				'success' => true,
618
				'zarafa' => Array(
619
					sanitizeGetValue('module', '', STRING_REGEX) => Array(
620
						sanitizeGetValue('moduleid', '', STRING_REGEX) => Array(
621
							'update' => Array(
622
								'success'=> true
623
							)
624
						)
625
					)
626
				)
627
			);
628
629
			// send hierarchy notification only in case of 'eml'
630
			if (pathinfo($attachmentProps[PR_ATTACH_LONG_FILENAME], PATHINFO_EXTENSION) === 'eml') {
631
				$hierarchynotifier = Array(
632
					'hierarchynotifier1' => Array(
633
						'folders' => Array(
634
							'item' => Array(
635
								0 => Array(
636
									'entryid' => $this->destinationFolderId,
637
									'parent_entryid' => bin2hex($destinationFolderProps[PR_PARENT_ENTRYID]),
638
									'store_entryid' => bin2hex($storeProps[PR_ENTRYID]),
639
									'props' => Array(
640
										'content_unread' => $destinationFolderProps[PR_CONTENT_UNREAD] + 1
641
									)
642
								)
643
							)
644
						)
645
					)
646
				);
647
648
				$return['zarafa']['hierarchynotifier'] = $hierarchynotifier;
649
			}
650
651
			echo json_encode($return);
652
		} else {
653
			throw new ZarafaException(_("Attachment is not imported successfully"));
654
		}
655
	}
656
657
	/**
658
	 * Function will get the embedded attachment and import it to the given MAPIFolder as webapp item.
659
	 */
660
	public function importEmbeddedAttachment()
661
	{
662
		// get message props of sub message
663
		$copyFromMessage = $GLOBALS['operations']->openMessage($this->store, hex2bin($this->entryId), $this->attachNum, true);
664
665
		if(empty($copyFromMessage)) {
666
			throw new ZarafaException(_("Embedded attachment not found."));
667
		}
668
669
		$newMessage = mapi_folder_createmessage($this->destinationFolder);
670
671
		// Copy the entire message
672
		mapi_copyto($copyFromMessage, array(), array(), $newMessage);
673
		mapi_savechanges($newMessage);
674
675
		$storeProps = mapi_getprops($this->store, array(PR_ENTRYID));
676
		$destinationFolderProps = mapi_getprops($this->destinationFolder, array(PR_PARENT_ENTRYID, PR_CONTENT_UNREAD));
677
		$return = Array(
678
			// 'success' property is needed for Extjs Ext.form.Action.Submit#success handler
679
			'success' => true,
680
			'zarafa' => Array(
681
				sanitizeGetValue('module', '', STRING_REGEX) => Array(
682
					sanitizeGetValue('moduleid', '', STRING_REGEX) => Array(
683
						'update' => Array(
684
							'success'=> true
685
						)
686
					)
687
				),
688
				'hierarchynotifier' => Array(
689
					'hierarchynotifier1' => Array(
690
						'folders' => Array(
691
							'item' => Array(
692
								0 => Array(
693
									'entryid' => $this->destinationFolderId,
694
									'parent_entryid' => bin2hex($destinationFolderProps[PR_PARENT_ENTRYID]),
695
									'store_entryid' => bin2hex($storeProps[PR_ENTRYID]),
696
									'props' => Array(
697
										'content_unread' => $destinationFolderProps[PR_CONTENT_UNREAD] + 1
698
									)
699
								)
700
							)
701
						)
702
					)
703
				)
704
			)
705
		);
706
707
		echo json_encode($return);
708
	}
709
710
	/**
711
	 * Check if the attached eml is corrupted or not
712
	 * @param String $attachment Content fetched from PR_ATTACH_DATA_BIN property of an attachment.
713
	 * @return True if eml is broken, false otherwise.
714
	 */
715
	public function isBroken($attachment)
716
	{
717
		// Get header part to process further
718
		$splittedContent = preg_split("/\r?\n\r?\n/", $attachment);
719
720
		// Fetch raw header
721
		if (preg_match_all('/([^:]+): ?.*\n/', $splittedContent[0], $matches)) {
722
			$rawHeaders = $matches[1];
723
		}
724
725
		// Compare if necessary headers are present or not
726
		if (isset($rawHeaders) && in_array('From', $rawHeaders) && in_array('Date', $rawHeaders)) {
727
			return false;
728
		}
729
730
		return true;
731
	}
732
733
	/**
734
	 * Generic function to check passed data and decide which type of attachment is requested.
735
	 */
736
	public function download()
737
	{
738
		$attachment = false;
739
740
		// Check if all attachments are requested to be downloaded as ZIP
741
		if ($this->allAsZip) {
742
			$attachment_state = new AttachmentState();
743
			$attachment_state->open();
744
745
			// Generate random ZIP file name at default temporary path of PHP
746
			$randomZipName = tempnam(sys_get_temp_dir(), 'zip');
747
748
			// Create an open zip archive.
749
			$zip = new ZipArchive();
750
			$result = $zip->open($randomZipName, ZipArchive::CREATE);
751
752
			if ($result === TRUE) {
753
				// Check if attachments are of saved message.
754
				// Only saved message has the entryid configured.
755
				if($this->entryId) {
756
					// Check if the requested attachment(s) are of an embedded message
757
					if($this->isSubMessage){
758
						// Loop through the attachNums, message in message in message ...
759
						for($index = 0, $len = count($this->attachNum); $index < $len - 1; $index++) {
760
							// Open the attachment
761
							$tempattach = mapi_message_openattach($this->message, $this->attachNum[$index]);
762
							if($tempattach) {
763
								// Open the object in the attachment
764
								$this->message = mapi_attach_openobj($tempattach);
765
							}
766
						}
767
					}
768
					$this->addAttachmentsToZipArchive($attachment_state, $zip);
769
				} else {
770
					$this->addUnsavedAttachmentsToZipArchive($attachment_state, $zip);
771
				}
772
			} else {
773
				// Throw exception if ZIP is not created successfully
774
				throw new ZarafaException(_("ZIP is not created successfully"));
775
			}
776
777
			$zip->close();
778
779
			$this->sendZipResponse($randomZipName);
780
			$attachment_state->close();
781
		// check if inline image is requested
782
		} else if($this->attachCid) {
783
			// check if the inline image is in a embedded message
784
			if(count($this->attachNum) > 0) {
785
				// get the embedded message attachment
786
				$attachment = $this->getAttachmentByAttachNum();
787
			}
788
789
			// now get the actual attachment object that should be sent back to client
790
			$attachment = $this->getAttachmentByAttachCid($attachment);
791
792
			// no need to return anything here function will echo all the output
793
			$this->downloadSavedAttachment($attachment, true);
794
795
		} else if(count($this->attachNum) > 0) {
796
			// check if the attachment needs to be imported
797
			if ($this->import) {
798
				if ($this->isEmbedded) {
799
					$this->importEmbeddedAttachment();
800
				} else {
801
					$this->importAttachment();
802
				}
803
				return;
804
			}
805
806
			// check if temporary unsaved attachment is requested
807
			if(is_string($this->attachNum[0])) {
808
				$this->downloadUnsavedAttachment();
809
			} else {
810
				// normal saved attachment is requested, so get it
811
				$attachment = $this->getAttachmentByAttachNum();
812
813
				if($attachment === false) {
0 ignored issues
show
The condition $attachment === false is always false.
Loading history...
814
					// something terrible happened and we can't continue
815
					return;
816
				}
817
818
				// no need to return anything here function will echo all the output
819
				$this->downloadSavedAttachment($attachment);
820
			}
821
		} else {
822
			throw new ZarafaException(_("Attachments can not be downloaded"));
823
		}
824
	}
825
826
	/**
827
	 * Function will encode all the necessary information about the exception
828
	 * into JSON format and send the response back to client.
829
	 *
830
	 * @param object $exception Exception object.
831
	 */
832
	function handleSaveMessageException($exception)
833
	{
834
		$return = array();
835
836
		// MAPI_E_NOT_FOUND exception contains generalize exception message.
837
		// Set proper exception message as display message should be user understandable.
838
		if($exception->getCode() == MAPI_E_NOT_FOUND) {
839
			$exception->setDisplayMessage(_('Could not find attachment.'));
840
		}
841
842
		// Set the headers
843
		header('Expires: 0'); // set expiration time
844
		header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
845
846
		// Set Content Disposition header
847
		header('Content-Disposition: inline');
848
		// Set content type header
849
		header('Content-Type: text/plain');
850
851
		//prepare exception response according to exception class
852
		if($exception instanceof MAPIException) {
853
			$return = array(
854
				'success' => false,
855
				'zarafa' => array(
856
					'error' => array(
857
						'type' => ERROR_MAPI,
858
						'info' => array(
859
							'hresult' => $exception->getCode(),
860
							'hresult_name' => get_mapi_error_name($exception->getCode()),
861
							'file' => $exception->getFileLine(),
862
							'display_message' => $exception->getDisplayMessage()
863
						)
864
					)
865
				)
866
			);
867
		} else if($exception instanceof ZarafaException) {
868
			$return = array(
869
				'success' => false,
870
				'zarafa' => array(
871
					'error' => array(
872
						'type' => ERROR_ZARAFA,
873
						'info' => array(
874
							'file' => $exception->getFileLine(),
875
							'display_message' => $exception->getDisplayMessage(),
876
							'original_message' => $exception->getMessage()
877
						)
878
					)
879
				)
880
			);
881
		} else if($exception instanceof BaseException) {
882
			$return = array(
883
				'success' => false,
884
				'zarafa' => array(
885
					'error' => array(
886
						'type' => ERROR_GENERAL,
887
						'info' => array(
888
							'file' => $exception->getFileLine(),
889
							'display_message' => $exception->getDisplayMessage(),
890
							'original_message' => $exception->getMessage()
891
						)
892
					)
893
				)
894
			);
895
		} else {
896
			$return = array(
897
				'success' => false,
898
				'zarafa' => array(
899
					'error' => array(
900
						'type' => ERROR_GENERAL,
901
						'info' => array(
902
							'display_message' => _('Operation failed'),
903
							'original_message' => $exception->getMessage()
904
						)
905
					)
906
				)
907
			);
908
		}
909
		echo json_encode($return);
910
	}
911
}
912
913
// create instance of class to download attachment
914
$attachInstance = new DownloadAttachment();
915
916
try{
917
	// initialize variables
918
	$attachInstance->init($_GET);
919
920
	// download attachment
921
	$attachInstance->download();
922
} catch (Exception $e) {
923
	$attachInstance->handleSaveMessageException($e);
924
}
925
?>
0 ignored issues
show
It is not recommended to use PHP's closing tag ?> in files other than templates.

Using a closing tag in PHP files that only contain PHP code is not recommended as you might accidentally add whitespace after the closing tag which would then be output by PHP. This can cause severe problems, for example headers cannot be sent anymore.

A simple precaution is to leave off the closing tag as it is not required, and it also has no negative effects whatsoever.

Loading history...
926