Completed
Pull Request — master (#1267)
by Christoph
05:31
created

MessagesController::downloadAttachment()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 10
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2
Metric Value
dl 0
loc 10
ccs 0
cts 7
cp 0
rs 9.4285
cc 1
eloc 7
nc 1
nop 4
crap 2
1
<?php
2
/**
3
 * ownCloud - Mail app
4
 *
5
 * @author Thomas Müller
6
 * @copyright 2013-2014 Thomas Müller [email protected]
7
 *
8
 * You should have received a copy of the GNU Lesser General Public
9
 * License along with this library.  If not, see <http://www.gnu.org/licenses/>.
10
 *
11
 */
12
13
namespace OCA\Mail\Controller;
14
15
use OCA\Mail\Http\AttachmentDownloadResponse;
16
use OCA\Mail\Http\HtmlResponse;
17
use OCA\Mail\Service\AccountService;
18
use OCA\Mail\Service\ContactsIntegration;
19
use OCA\Mail\Service\IAccount;
20
use OCA\Mail\Service\IMailBox;
21
use OCA\Mail\Service\Logger;
22
use OCA\Mail\Service\UnifiedAccount;
23
use OCP\AppFramework\Controller;
24
use OCP\AppFramework\Db\DoesNotExistException;
25
use OCP\AppFramework\Http;
26
use OCP\AppFramework\Http\ContentSecurityPolicy;
27
use OCP\AppFramework\Http\JSONResponse;
28
use OCP\AppFramework\Http\TemplateResponse;
29
use OCP\IL10N;
30
use OCP\IRequest;
31
use OCP\Util;
32
33
class MessagesController extends Controller {
34
35
	/** @var AccountService */
36
	private $accountService;
37
38
	/**
39
	 * @var string
40
	 */
41
	private $currentUserId;
42
43
	/**
44
	 * @var ContactsIntegration
45
	 */
46
	private $contactsIntegration;
47
48
	/**
49
	 * @var \OCA\Mail\Service\Logger
50
	 */
51
	private $logger;
52
53
	/**
54
	 * @var \OCP\Files\Folder
55
	 */
56
	private $userFolder;
57
58
	/**
59
	 * @var IL10N
60
	 */
61
	private $l10n;
62
63
	/**
64
	 * @var IAccount[]
65
	 */
66
	private $accounts = [];
67
68
	/**
69
	 * @param string $appName
70
	 * @param IRequest $request
71
	 * @param AccountService $accountService
72
	 * @param $UserId
73
	 * @param $userFolder
74
	 * @param ContactsIntegration $contactsIntegration
75
	 * @param Logger $logger
76
	 * @param IL10N $l10n
77
	 */
78
	public function __construct($appName,
79
								IRequest $request,
80
								AccountService $accountService,
81
								$UserId,
82
								$userFolder,
83
								ContactsIntegration $contactsIntegration,
84
								Logger $logger,
85
								IL10N $l10n) {
86
		parent::__construct($appName, $request);
87
		$this->accountService = $accountService;
88
		$this->currentUserId = $UserId;
89
		$this->userFolder = $userFolder;
90
		$this->contactsIntegration = $contactsIntegration;
91
		$this->logger = $logger;
92
		$this->l10n = $l10n;
93
	}
94
95
	/**
96
	 * @NoAdminRequired
97
	 * @NoCSRFRequired
98
	 *
99
	 * @param int $accountId
100
	 * @param string $folderId
101
	 * @param int $from
102
	 * @param int $to
103
	 * @param string $filter
104
	 * @param array $ids
105
	 * @return JSONResponse
106
	 */
107
	public function index($accountId, $folderId, $from=0, $to=20, $filter=null, $ids=null) {
108
		if (!is_null($ids)) {
109
			$ids = explode(',', $ids);
110
111
			return $this->loadMultiple($accountId, $folderId, $ids);
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->loadMultip...ntId, $folderId, $ids); (array) is incompatible with the return type documented by OCA\Mail\Controller\MessagesController::index of type OCP\AppFramework\Http\JSONResponse.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
112
		}
113
		$mailBox = $this->getFolder($accountId, $folderId);
114
115
		$this->logger->debug("loading messages $from to $to of folder <$folderId>");
116
117
		$json = $mailBox->getMessages($from, $to-$from+1, $filter);
118
119
		$ci = $this->contactsIntegration;
120
		$json = array_map(function($j) use ($ci, $mailBox) {
121
			if ($mailBox->getSpecialRole() === 'trash') {
122
				$j['delete'] = (string)$this->l10n->t('Delete permanently');
123
			}
124
125
			if ($mailBox->getSpecialRole() === 'sent') {
126
				$j['fromEmail'] = $j['toEmail'];
127
				$j['from'] = $j['to'];
128
				if((count($j['toList']) > 1) || (count($j['ccList']) > 0)) {
129
					$j['from'] .= ' ' . $this->l10n->t('& others');
130
				}
131
			}
132
133
			$j['senderImage'] = $ci->getPhoto($j['fromEmail']);
134
			return $j;
135
		}, $json);
136
137
		return new JSONResponse($json);
138
	}
139
140
	private function loadMessage($accountId, $folderId, $id) {
141
		$account = $this->getAccount($accountId);
142
		$mailBox = $account->getMailbox(base64_decode($folderId));
143
		$m = $mailBox->getMessage($id);
144
145
		$json = $this->enhanceMessage($accountId, $folderId, $id, $m, $account, $mailBox);
146
147
		// Unified inbox hack
148
		$messageId = $id;
149
		if ($accountId === UnifiedAccount::ID) {
150
			// Add accountId, folderId for unified inbox replies
151
			list($accountId, $messageId) = json_decode(base64_decode($id));
152
			$account = $this->getAccount($accountId);
153
			$inbox = $account->getInbox();
154
			$folderId = base64_encode($inbox->getFolderId());
155
		}
156
		$json['messageId'] = $messageId;
157
		$json['accountId'] = $accountId;
158
		$json['folderId'] = $folderId;
159
		// End unified inbox hack
160
161
		return $json;
162
	}
163
164
	/**
165
	 * @NoAdminRequired
166
	 * @NoCSRFRequired
167
	 *
168
	 * @param int $accountId
169
	 * @param string $folderId
170
	 * @param mixed $id
171
	 * @return JSONResponse
172
	 */
173
	public function show($accountId, $folderId, $id) {
174
		try {
175
			$json = $this->loadMessage($accountId, $folderId, $id);
176
		} catch (DoesNotExistException $ex) {
0 ignored issues
show
Bug introduced by
The class OCP\AppFramework\Db\DoesNotExistException does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
177
			return new JSONResponse([], 404);
178
		}
179
		return new JSONResponse($json);
180
	}
181
182
	/**
183
	 * @NoAdminRequired
184
	 * @NoCSRFRequired
185
	 *
186
	 * @param int $accountId
187
	 * @param string $folderId
188
	 * @param string $messageId
189
	 * @return \OCA\Mail\Http\HtmlResponse
190
	 */
191
	public function getHtmlBody($accountId, $folderId, $messageId) {
192
		try {
193
			$mailBox = $this->getFolder($accountId, $folderId);
194
195
			$m = $mailBox->getMessage($messageId, true);
0 ignored issues
show
Unused Code introduced by
The call to IMailBox::getMessage() has too many arguments starting with true.

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.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
196
			$html = $m->getHtmlBody($accountId, $folderId, $messageId, function($cid) use ($m){
197
				$match = array_filter($m->attachments, function($a) use($cid){
198
					return $a['cid'] === $cid;
199
				});
200
				$match = array_shift($match);
201
				if (is_null($match)) {
202
					return null;
203
				}
204
				return $match['id'];
205
			});
206
207
			$htmlResponse = new HtmlResponse($html);
208
209
			// Harden the default security policy
210
			// FIXME: Remove once ownCloud 8.1 is a requirement for the mail app
211 View Code Duplication
			if(class_exists('\OCP\AppFramework\Http\ContentSecurityPolicy')) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
212
				$policy = new ContentSecurityPolicy();
213
				$policy->allowEvalScript(false);
214
				$policy->disallowScriptDomain('\'self\'');
215
				$policy->disallowConnectDomain('\'self\'');
216
				$policy->disallowFontDomain('\'self\'');
217
				$policy->disallowMediaDomain('\'self\'');
218
				$htmlResponse->setContentSecurityPolicy($policy);
219
			}
220
221
			// Enable caching
222
			$htmlResponse->cacheFor(60 * 60);
223
			$htmlResponse->addHeader('Pragma', 'cache');
224
225
			return $htmlResponse;
226
		} catch(\Exception $ex) {
227
			return new TemplateResponse($this->appName, 'error', ['message' => $ex->getMessage()], 'none');
228
		}
229
	}
230
231
	/**
232
	 * @NoAdminRequired
233
	 * @NoCSRFRequired
234
	 *
235
	 * @param int $accountId
236
	 * @param string $folderId
237
	 * @param string $messageId
238
	 * @param string $attachmentId
239
	 * @return AttachmentDownloadResponse
240
	 */
241
	public function downloadAttachment($accountId, $folderId, $messageId, $attachmentId) {
242
		$mailBox = $this->getFolder($accountId, $folderId);
243
244
		$attachment = $mailBox->getAttachment($messageId, $attachmentId);
245
246
		return new AttachmentDownloadResponse(
247
			$attachment->getContents(),
248
			$attachment->getName(),
249
			$attachment->getType());
250
	}
251
252
	/**
253
	 * @NoAdminRequired
254
	 * @NoCSRFRequired
255
	 *
256
	 * @param int $accountId
257
	 * @param string $folderId
258
	 * @param string $messageId
259
	 * @param string $attachmentId
260
	 * @param string $targetPath
261
	 * @return JSONResponse
262
	 */
263
	public function saveAttachment($accountId, $folderId, $messageId, $attachmentId, $targetPath) {
264
		$mailBox = $this->getFolder($accountId, $folderId);
265
266
		$attachmentIds = [];
0 ignored issues
show
Unused Code introduced by
$attachmentIds is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
267
		if($attachmentId === 0) {
0 ignored issues
show
Unused Code Bug introduced by
The strict comparison === seems to always evaluate to false as the types of $attachmentId (string) and 0 (integer) can never be identical. Maybe you want to use a loose comparison == instead?
Loading history...
268
			// Save all attachments
269
			$m = $mailBox->getMessage($messageId);
270
			$attachmentIds = array_map(function($a){
271
				return $a['id'];
272
			}, $m->attachments);
273
		} else {
274
			$attachmentIds = [$attachmentId];
275
		}
276
277
		foreach($attachmentIds as $attachmentId) {
278
			$attachment = $mailBox->getAttachment($messageId, $attachmentId);
279
280
			$fileName = $attachment->getName();
281
			$fileParts = pathinfo($fileName);
282
			$fileName = $fileParts['filename'];
283
			$fileExtension = $fileParts['extension'];
284
			$fullPath = "$targetPath/$fileName.$fileExtension";
285
			$counter = 2;
286
			while($this->userFolder->nodeExists($fullPath)) {
287
				$fullPath = "$targetPath/$fileName ($counter).$fileExtension";
288
				$counter++;
289
			}
290
291
			$newFile = $this->userFolder->newFile($fullPath);
292
			$newFile->putContent($attachment->getContents());
293
		}
294
295
		return new JSONResponse();
296
	}
297
298
	/**
299
	 * @NoAdminRequired
300
	 *
301
	 * @param int $accountId
302
	 * @param string $folderId
303
	 * @param string $messageId
304
	 * @param array $flags
305
	 * @return JSONResponse
306
	 */
307
	public function setFlags($accountId, $folderId, $messageId, $flags) {
308
		$mailBox = $this->getFolder($accountId, $folderId);
309
310
		foreach($flags as $flag => $value) {
311
			$value = filter_var($value, FILTER_VALIDATE_BOOLEAN);
312
			if ($flag === 'unseen') {
313
				$flag = 'seen';
314
				$value = !$value;
315
			}
316
			$mailBox->setMessageFlag($messageId, '\\'.$flag, $value);
317
		}
318
319
		return new JSONResponse();
320
	}
321
322
	/**
323
	 * @NoAdminRequired
324
	 *
325
	 * @param int $accountId
326
	 * @param string $folderId
327
	 * @param string $id
328
	 * @return JSONResponse
329
	 */
330
	public function destroy($accountId, $folderId, $id) {
331
		$this->logger->debug("deleting message <$id> of folder <$folderId>, account <$accountId>");
332
		try {
333
			$account = $this->getAccount($accountId);
334
			$account->deleteMessage(base64_decode($folderId), $id);
335
			return new JSONResponse();
336
337
		} catch (DoesNotExistException $e) {
0 ignored issues
show
Bug introduced by
The class OCP\AppFramework\Db\DoesNotExistException does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
338
			$this->logger->error("could not delete message <$id> of folder <$folderId>, "
339
				. "account <$accountId> because it does not exist");
340
			return new JSONResponse([], Http::STATUS_NOT_FOUND);
341
		}
342
	}
343
344
	/**
345
	 * @param int $accountId
346
	 * @return \OCA\Mail\Service\IAccount
347
	 */
348
	private function getAccount($accountId) {
349
		if (!array_key_exists($accountId, $this->accounts)) {
350
			$this->accounts[$accountId] = $this->accountService->find($this->currentUserId, $accountId);
351
		}
352
		return $this->accounts[$accountId];
353
	}
354
355
	/**
356
	 * @param int $accountId
357
	 * @param string $folderId
358
	 * @return IMailBox
359
	 */
360
	private function getFolder($accountId, $folderId) {
361
		$account = $this->getAccount($accountId);
362
		return $account->getMailbox(base64_decode($folderId));
363
	}
364
365
	/**
366
	 * @param string $messageId
367
	 * @param $accountId
368
	 * @param $folderId
369
	 * @return callable
370
	 */
371
	private function enrichDownloadUrl($accountId, $folderId, $messageId, $attachment) {
372
		$downloadUrl = \OCP\Util::linkToRoute('mail.messages.downloadAttachment', [
373
			'accountId' => $accountId,
374
			'folderId' => $folderId,
375
			'messageId' => $messageId,
376
			'attachmentId' => $attachment['id'],
377
		]);
378
		$downloadUrl = \OC::$server->getURLGenerator()->getAbsoluteURL($downloadUrl);
379
		$attachment['downloadUrl'] = $downloadUrl;
380
		$attachment['mimeUrl'] = $this->mimeTypeIcon($attachment['mime']);
381
382
		if ($this->attachmentIsImage($attachment)) {
383
			$attachment['isImage'] = true;
384
		}
385
		return $attachment;
386
	}
387
388
	/**
389
	 * @param $attachment
390
	 *
391
	 * Determines if the content of this attachment is an image
392
	 */
393
	private function attachmentIsImage($attachment) {
394
		return in_array($attachment['mime'], array('image/jpeg',
395
			'image/png',
396
			'image/gif'));
397
	}
398
399
	/**
400
	 * @param string $accountId
401
	 * @param string $folderId
402
	 * @param string $messageId
403
	 * @return string
404
	 */
405
	private function buildHtmlBodyUrl($accountId, $folderId, $messageId) {
406
		$htmlBodyUrl = \OC::$server->getURLGenerator()->linkToRoute('mail.messages.getHtmlBody', [
407
			'accountId' => $accountId,
408
			'folderId' => $folderId,
409
			'messageId' => $messageId,
410
		]);
411
		return \OC::$server->getURLGenerator()->getAbsoluteURL($htmlBodyUrl);
412
	}
413
414
	private function loadMultiple($accountId, $folderId, $ids) {
415
		$messages = array_map(function($id) use ($accountId, $folderId){
416
			try {
417
				return $this->loadMessage($accountId, $folderId, $id);
418
			} catch (DoesNotExistException $ex) {
0 ignored issues
show
Bug introduced by
The class OCP\AppFramework\Db\DoesNotExistException does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
419
				return null;
420
			}
421
		}, $ids);
422
423
		return $messages;
424
	}
425
426
	/**
427
	 * @param $accountId
428
	 * @param $folderId
429
	 * @param $id
430
	 * @param $m
431
	 * @param IAccount $account
432
	 * @param $mailBox
433
	 * @return mixed
434
	 */
435
	private function enhanceMessage($accountId, $folderId, $id, $m, IAccount $account, $mailBox) {
436
		$json = $m->getFullMessage($account->getEmail(), $mailBox->getSpecialRole());
437
		$json['senderImage'] = $this->contactsIntegration->getPhoto($m->getFromEmail());
438
		if (isset($json['hasHtmlBody'])) {
439
			$json['htmlBodyUrl'] = $this->buildHtmlBodyUrl($accountId, $folderId, $id);
440
		}
441
442
		if (isset($json['attachment'])) {
443
			$json['attachment'] = $this->enrichDownloadUrl($accountId, $folderId, $id, $json['attachment']);
444
		}
445
		if (isset($json['attachments'])) {
446
			$json['attachments'] = array_map(function ($a) use ($accountId, $folderId, $id) {
447
				return $this->enrichDownloadUrl($accountId, $folderId, $id, $a);
448
			}, $json['attachments']);
449
450
			// show images first
451
			usort($json['attachments'], function($a, $b) {
452
				if (isset($a['isImage']) && !isset($b['isImage'])) {
453
					return -1;
454
				} elseif (!isset($a['isImage']) && isset($b['isImage'])) {
455
					return 1;
456
				} else {
457
					Util::naturalSortCompare($a['fileName'], $b['fileName']);
458
				}
459
			});
460
			return $json;
461
		}
462
		return $json;
463
	}
464
465
	/**
466
	 * Get path to the icon of a file type
467
	 *
468
	 * @todo Inject IMimeTypeDetector once core 8.2+ is supported
469
	 *
470
	 * @param string $mimeType the MIME type
471
	 */
472
	private function mimeTypeIcon($mimeType) {
473
		$ocVersion = \OC::$server->getConfig()->getSystemValue('version', '0.0.0');
474
		if (version_compare($ocVersion, '8.2.0', '<')) {
475
			// Version-hack for 8.1 and lower
476
			return \OC_Helper::mimetypeIcon($mimeType);
477
		}
478
		/* @var IMimeTypeDetector */
479
		$mimeTypeDetector = \OC::$server->getMimeTypeDetector();
480
		return $mimeTypeDetector->mimeTypeIcon($mimeType);
481
	}
482
483
}
484