Completed
Pull Request — master (#1536)
by
unknown
09:24
created

MessagesController::loadMessage()   B

Complexity

Conditions 3
Paths 4

Size

Total Lines 26
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 26
ccs 0
cts 19
cp 0
rs 8.8571
cc 3
eloc 18
nc 4
nop 3
crap 12
1
<?php
2
/**
3
 * @author Alexander Weidinger <[email protected]>
4
 * @author Christoph Wurst <[email protected]>
5
 * @author Christoph Wurst <[email protected]>
6
 * @author Jakob Sack <[email protected]>
7
 * @author Jan-Christoph Borchardt <[email protected]>
8
 * @author Lukas Reschke <[email protected]>
9
 * @author Scrutinizer Auto-Fixer <[email protected]>
10
 * @author Thomas Imbreckx <[email protected]>
11
 * @author Thomas Müller <[email protected]>
12
 *
13
 * ownCloud - Mail
14
 *
15
 * This code is free software: you can redistribute it and/or modify
16
 * it under the terms of the GNU Affero General Public License, version 3,
17
 * as published by the Free Software Foundation.
18
 *
19
 * This program is distributed in the hope that it will be useful,
20
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
21
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22
 * GNU Affero General Public License for more details.
23
 *
24
 * You should have received a copy of the GNU Affero General Public License, version 3,
25
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
26
 *
27
 */
28
29
namespace OCA\Mail\Controller;
30
31
use OCA\Mail\Http\AttachmentDownloadResponse;
32
use OCA\Mail\Http\HtmlResponse;
33
use OCA\Mail\Service\AccountService;
34
use OCA\Mail\Service\ContactsIntegration;
35
use OCA\Mail\Service\IAccount;
36
use OCA\Mail\Service\IMailBox;
37
use OCA\Mail\Service\Logger;
38
use OCA\Mail\Service\UnifiedAccount;
39
use OCP\AppFramework\Controller;
40
use OCP\AppFramework\Db\DoesNotExistException;
41
use OCP\AppFramework\Http;
42
use OCP\AppFramework\Http\ContentSecurityPolicy;
43
use OCP\AppFramework\Http\JSONResponse;
44
use OCP\AppFramework\Http\TemplateResponse;
45
use OCP\IL10N;
46
use OCP\IRequest;
47
use OCP\Util;
48
49
class MessagesController extends Controller {
50
51
	/** @var AccountService */
52
	private $accountService;
53
54
	/**
55
	 * @var string
56
	 */
57
	private $currentUserId;
58
59
	/**
60
	 * @var ContactsIntegration
61
	 */
62
	private $contactsIntegration;
63
64
	/**
65
	 * @var \OCA\Mail\Service\Logger
66
	 */
67
	private $logger;
68
69
	/**
70
	 * @var \OCP\Files\Folder
71
	 */
72
	private $userFolder;
73
74
	/**
75
	 * @var IL10N
76
	 */
77
	private $l10n;
78
79
	/**
80
	 * @var IAccount[]
81
	 */
82
	private $accounts = [];
83
84
	/**
85
	 * @param string $appName
86
	 * @param IRequest $request
87
	 * @param AccountService $accountService
88
	 * @param string $UserId
89
	 * @param $userFolder
90
	 * @param ContactsIntegration $contactsIntegration
91
	 * @param Logger $logger
92
	 * @param IL10N $l10n
93
	 */
94 12
	public function __construct($appName,
95
								IRequest $request,
96
								AccountService $accountService,
97
								$UserId,
98
								$userFolder,
99
								ContactsIntegration $contactsIntegration,
100
								Logger $logger,
101
								IL10N $l10n) {
102 12
		parent::__construct($appName, $request);
103 12
		$this->accountService = $accountService;
104 12
		$this->currentUserId = $UserId;
105 12
		$this->userFolder = $userFolder;
106 12
		$this->contactsIntegration = $contactsIntegration;
107 12
		$this->logger = $logger;
108 12
		$this->l10n = $l10n;
109 12
	}
110
111
	/**
112
	 * @NoAdminRequired
113
	 * @NoCSRFRequired
114
	 *
115
	 * @param int $accountId
116
	 * @param string $folderId
117
	 * @param int $from
118
	 * @param int $to
119
	 * @param string $filter
120
	 * @param array $ids
121
	 * @return JSONResponse
122
	 */
123
	public function index($accountId, $folderId, $from=0, $to=20, $filter=null, $ids=null) {
124
		if (!is_null($ids)) {
125
			$ids = explode(',', $ids);
126
127
			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...
128
		}
129
		$mailBox = $this->getFolder($accountId, $folderId);
130
131
		$this->logger->debug("loading messages $from to $to of folder <$folderId>");
132
133
		$json = $mailBox->getMessages($from, $to-$from+1, $filter);
134
135
		$ci = $this->contactsIntegration;
136
		$json = array_map(function($j) use ($ci, $mailBox) {
137
			if ($mailBox->getSpecialRole() === 'trash') {
138
				$j['delete'] = (string)$this->l10n->t('Delete permanently');
139
			}
140
141
			if ($mailBox->getSpecialRole() === 'sent') {
142
				$j['fromEmail'] = $j['toEmail'];
143
				$j['from'] = $j['to'];
144
				if((count($j['toList']) > 1) || (count($j['ccList']) > 0)) {
145
					$j['from'] .= ' ' . $this->l10n->t('& others');
146
				}
147
			}
148
149
			$j['senderImage'] = $ci->getPhoto($j['fromEmail']);
150
			return $j;
151
		}, $json);
152
153
		return new JSONResponse($json);
154
	}
155
156
	private function loadMessage($accountId, $folderId, $id) {
157
		$account = $this->getAccount($accountId);
158
		$mailBox = $account->getMailbox(base64_decode($folderId));
159
		if($mailBox->getSpecialRole() === 'drafts'){
160
			$m = $mailBox->getMessage($id,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...
161
		}else{
162
			$m = $mailBox->getMessage($id);
163
		}
164
		$json = $this->enhanceMessage($accountId, $folderId, $id, $m, $account, $mailBox);
165
166
		// Unified inbox hack
167
		$messageId = $id;
168
		if ($accountId === UnifiedAccount::ID) {
169
			// Add accountId, folderId for unified inbox replies
170
			list($accountId, $messageId) = json_decode(base64_decode($id));
171
			$account = $this->getAccount($accountId);
172
			$inbox = $account->getInbox();
173
			$folderId = base64_encode($inbox->getFolderId());
174
		}
175
		$json['messageId'] = $messageId;
176
		$json['accountId'] = $accountId;
177
		$json['folderId'] = $folderId;
178
		// End unified inbox hack
179
180
		return $json;
181
	}
182
183
	/**
184
	 * @NoAdminRequired
185
	 * @NoCSRFRequired
186
	 *
187
	 * @param int $accountId
188
	 * @param string $folderId
189
	 * @param mixed $id
190
	 * @return JSONResponse
191
	 */
192
	public function show($accountId, $folderId, $id) {
193
		try {
194
			$json = $this->loadMessage($accountId, $folderId, $id);
195
		} 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...
196
			return new JSONResponse([], 404);
197
		}
198
		return new JSONResponse($json);
199
	}
200
201
	/**
202
	 * @NoAdminRequired
203
	 * @NoCSRFRequired
204
	 *
205
	 * @param int $accountId
206
	 * @param string $folderId
207
	 * @param string $messageId
208
	 * @return \OCA\Mail\Http\HtmlResponse
209
	 */
210 1
	public function getHtmlBody($accountId, $folderId, $messageId) {
211
		try {
212 1
			$mailBox = $this->getFolder($accountId, $folderId);
213
214 1
			$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...
215
			$html = $m->getHtmlBody($accountId, $folderId, $messageId, function($cid) use ($m){
216
				$match = array_filter($m->attachments, function($a) use($cid){
217
					return $a['cid'] === $cid;
218
				});
219
				$match = array_shift($match);
220
				if (is_null($match)) {
221
					return null;
222
				}
223
				return $match['id'];
224 1
			});
225
226 1
			$htmlResponse = new HtmlResponse($html);
227
228
			// Harden the default security policy
229
			// FIXME: Remove once ownCloud 8.1 is a requirement for the mail app
230 1 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...
231 1
				$policy = new ContentSecurityPolicy();
232 1
				$policy->allowEvalScript(false);
233 1
				$policy->disallowScriptDomain('\'self\'');
234 1
				$policy->disallowConnectDomain('\'self\'');
235 1
				$policy->disallowFontDomain('\'self\'');
236 1
				$policy->disallowMediaDomain('\'self\'');
237 1
				$htmlResponse->setContentSecurityPolicy($policy);
238 1
			}
239
240
			// Enable caching
241 1
			$htmlResponse->cacheFor(60 * 60);
242 1
			$htmlResponse->addHeader('Pragma', 'cache');
243
244 1
			return $htmlResponse;
245
		} catch(\Exception $ex) {
246
			return new TemplateResponse($this->appName, 'error', ['message' => $ex->getMessage()], 'none');
247
		}
248
	}
249
250
	/**
251
	 * @NoAdminRequired
252
	 * @NoCSRFRequired
253
	 *
254
	 * @param int $accountId
255
	 * @param string $folderId
256
	 * @param string $messageId
257
	 * @param string $attachmentId
258
	 * @return AttachmentDownloadResponse
259
	 */
260 1
	public function downloadAttachment($accountId, $folderId, $messageId, $attachmentId) {
261 1
		$mailBox = $this->getFolder($accountId, $folderId);
262
263 1
		$attachment = $mailBox->getAttachment($messageId, $attachmentId);
264
265 1
		return new AttachmentDownloadResponse(
266 1
			$attachment->getContents(),
267 1
			$attachment->getName(),
268 1
			$attachment->getType());
269
	}
270
271
	/**
272
	 * @NoAdminRequired
273
	 * @NoCSRFRequired
274
	 *
275
	 * @param int $accountId
276
	 * @param string $folderId
277
	 * @param string $messageId
278
	 * @param int $attachmentId
279
	 * @param string $targetPath
280
	 * @return JSONResponse
281
	 */
282 2
	public function saveAttachment($accountId, $folderId, $messageId, $attachmentId, $targetPath) {
283 2
		$mailBox = $this->getFolder($accountId, $folderId);
284
285 2
		$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...
286 2
		if($attachmentId === 0) {
287
			// Save all attachments
288 1
			$m = $mailBox->getMessage($messageId);
289
			$attachmentIds = array_map(function($a){
290 1
				return $a['id'];
291 1
			}, $m->attachments);
292 1
		} else {
293 1
			$attachmentIds = [$attachmentId];
294
		}
295
296 2
		foreach($attachmentIds as $attachmentId) {
297 2
			$attachment = $mailBox->getAttachment($messageId, $attachmentId);
298
299 2
			$fileName = $attachment->getName();
300 2
			$fileParts = pathinfo($fileName);
301 2
			$fileName = $fileParts['filename'];
302 2
			$fileExtension = $fileParts['extension'];
303 2
			$fullPath = "$targetPath/$fileName.$fileExtension";
304 2
			$counter = 2;
305 2
			while($this->userFolder->nodeExists($fullPath)) {
306
				$fullPath = "$targetPath/$fileName ($counter).$fileExtension";
307
				$counter++;
308
			}
309
310 2
			$newFile = $this->userFolder->newFile($fullPath);
311 2
			$newFile->putContent($attachment->getContents());
312 2
		}
313
314 2
		return new JSONResponse();
315
	}
316
317
	/**
318
	 * @NoAdminRequired
319
	 *
320
	 * @param int $accountId
321
	 * @param string $folderId
322
	 * @param string $messageId
323
	 * @param array $flags
324
	 * @return JSONResponse
325
	 */
326 2
	public function setFlags($accountId, $folderId, $messageId, $flags) {
327 2
		$mailBox = $this->getFolder($accountId, $folderId);
328
329 2
		foreach($flags as $flag => $value) {
330 2
			$value = filter_var($value, FILTER_VALIDATE_BOOLEAN);
331 2
			if ($flag === 'unseen') {
332 1
				$flag = 'seen';
333 1
				$value = !$value;
334 1
			}
335 2
			$mailBox->setMessageFlag($messageId, '\\'.$flag, $value);
336 2
		}
337
338 2
		return new JSONResponse();
339
	}
340
341
	/**
342
	 * @NoAdminRequired
343
	 *
344
	 * @param int $accountId
345
	 * @param string $folderId
346
	 * @param string $id
347
	 * @return JSONResponse
348
	 */
349 3
	public function destroy($accountId, $folderId, $id) {
350 3
		$this->logger->debug("deleting message <$id> of folder <$folderId>, account <$accountId>");
351
		try {
352 3
			$account = $this->getAccount($accountId);
353 2
			$account->deleteMessage(base64_decode($folderId), $id);
354 1
			return new JSONResponse();
355
356 2
		} 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...
357 2
			$this->logger->error("could not delete message <$id> of folder <$folderId>, "
358 2
				. "account <$accountId> because it does not exist");
359 2
			return new JSONResponse([], Http::STATUS_NOT_FOUND);
360
		}
361
	}
362
363
	/**
364
	 * @param int $accountId
365
	 * @return \OCA\Mail\Service\IAccount
366
	 */
367 9
	private function getAccount($accountId) {
368 9
		if (!array_key_exists($accountId, $this->accounts)) {
369 9
			$this->accounts[$accountId] = $this->accountService->find($this->currentUserId, $accountId);
370 8
		}
371 8
		return $this->accounts[$accountId];
372
	}
373
374
	/**
375
	 * @param int $accountId
376
	 * @param string $folderId
377
	 * @return IMailBox
378
	 */
379 6
	private function getFolder($accountId, $folderId) {
380 6
		$account = $this->getAccount($accountId);
381 6
		return $account->getMailbox(base64_decode($folderId));
382
	}
383
384
	/**
385
	 * @param string $messageId
386
	 * @param $accountId
387
	 * @param $folderId
388
	 * @return callable
389
	 */
390
	private function enrichDownloadUrl($accountId, $folderId, $messageId, $attachment) {
391
		$downloadUrl = \OCP\Util::linkToRoute('mail.messages.downloadAttachment', [
392
			'accountId' => $accountId,
393
			'folderId' => $folderId,
394
			'messageId' => $messageId,
395
			'attachmentId' => $attachment['id'],
396
		]);
397
		$downloadUrl = \OC::$server->getURLGenerator()->getAbsoluteURL($downloadUrl);
398
		$attachment['downloadUrl'] = $downloadUrl;
399
		$attachment['mimeUrl'] = $this->mimeTypeIcon($attachment['mime']);
400
401
		if ($this->attachmentIsImage($attachment)) {
402
			$attachment['isImage'] = true;
403
		} else if ($this->attachmentIsCalendarEvent($attachment)) {
404
			$attachment['isCalendarEvent'] = true;
405
		}
406
		return $attachment;
407
	}
408
409
	/**
410
	 * @param $attachment
411
	 *
412
	 * Determines if the content of this attachment is an image
413
	 *
414
	 * @return boolean
415
	 */
416
	private function attachmentIsImage($attachment) {
417
		return in_array(
418
			$attachment['mime'], [
419
			'image/jpeg',
420
			'image/png',
421
			'image/gif'
422
		]);
423
	}
424
425
	/**
426
	 * @param type $attachment
427
	 * @return boolean
428
	 */
429
	private function attachmentIsCalendarEvent($attachment) {
430
		return $attachment['mime'] === 'text/calendar';
431
	}
432
433
	/**
434
	 * @param string $accountId
435
	 * @param string $folderId
436
	 * @param string $messageId
437
	 * @return string
438
	 */
439
	private function buildHtmlBodyUrl($accountId, $folderId, $messageId) {
440
		$htmlBodyUrl = \OC::$server->getURLGenerator()->linkToRoute('mail.messages.getHtmlBody', [
441
			'accountId' => $accountId,
442
			'folderId' => $folderId,
443
			'messageId' => $messageId,
444
		]);
445
		return \OC::$server->getURLGenerator()->getAbsoluteURL($htmlBodyUrl);
446
	}
447
448
	/**
449
	 * @param integer $accountId
450
	 * @param string $folderId
451
	 */
452
	private function loadMultiple($accountId, $folderId, $ids) {
453
		$messages = array_map(function($id) use ($accountId, $folderId){
454
			try {
455
				return $this->loadMessage($accountId, $folderId, $id);
456
			} 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...
457
				return null;
458
			}
459
		}, $ids);
460
461
		return $messages;
462
	}
463
464
	/**
465
	 * @param $accountId
466
	 * @param $folderId
467
	 * @param $id
468
	 * @param $m
469
	 * @param IAccount $account
470
	 * @param IMailBox $mailBox
471
	 * @return mixed
472
	 */
473
	private function enhanceMessage($accountId, $folderId, $id, $m, IAccount $account, $mailBox) {
474
		$json = $m->getFullMessage($account->getEmail(), $mailBox->getSpecialRole());
475
		$json['senderImage'] = $this->contactsIntegration->getPhoto($m->getFromEmail());
476
		if (isset($json['hasHtmlBody'])) {
477
			$json['htmlBodyUrl'] = $this->buildHtmlBodyUrl($accountId, $folderId, $id);
478
		}
479
480
		if (isset($json['attachments'])) {
481
			$json['attachments'] = array_map(function ($a) use ($accountId, $folderId, $id) {
482
				return $this->enrichDownloadUrl($accountId, $folderId, $id, $a);
483
			}, $json['attachments']);
484
485
			// show images first
486
			usort($json['attachments'], function($a, $b) {
487
				if (isset($a['isImage']) && !isset($b['isImage'])) {
488
					return -1;
489
				} elseif (!isset($a['isImage']) && isset($b['isImage'])) {
490
					return 1;
491
				} else {
492
					Util::naturalSortCompare($a['fileName'], $b['fileName']);
493
				}
494
			});
495
			return $json;
496
		}
497
		return $json;
498
	}
499
500
	/**
501
	 * Get path to the icon of a file type
502
	 *
503
	 * @todo Inject IMimeTypeDetector once core 8.2+ is supported
504
	 *
505
	 * @param string $mimeType the MIME type
506
	 */
507
	private function mimeTypeIcon($mimeType) {
508
		$ocVersion = \OC::$server->getConfig()->getSystemValue('version', '0.0.0');
509
		if (version_compare($ocVersion, '8.2.0', '<')) {
510
			// Version-hack for 8.1 and lower
511
			return \OC_Helper::mimetypeIcon($mimeType);
512
		}
513
		/* @var IMimeTypeDetector */
514
		$mimeTypeDetector = \OC::$server->getMimeTypeDetector();
515
		return $mimeTypeDetector->mimeTypeIcon($mimeType);
516
	}
517
518
}
519