Completed
Pull Request — master (#1352)
by Christoph
14:47
created

MessagesController::enhanceMessage()   C

Complexity

Conditions 8
Paths 8

Size

Total Lines 29
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 72

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 29
ccs 0
cts 21
cp 0
rs 5.3846
cc 8
eloc 20
nc 8
nop 6
crap 72
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
			$image = $ci->getPhoto($j['fromEmail']);
150
			if (substr($image, 0, 4) === "http") {
151
				$j['senderImage'] = $image;
152
			} else {
153
				// ignore contacts >= 1.0 binary images
154
				// TODO: fix
155
				$j['senderImage'] = null;
156
			}
157
			return $j;
158
		}, $json);
159
160
		return new JSONResponse($json);
161
	}
162
163
	private function loadMessage($accountId, $folderId, $id) {
164
		$account = $this->getAccount($accountId);
165
		$mailBox = $account->getMailbox(base64_decode($folderId));
166
		$m = $mailBox->getMessage($id);
167
168
		$json = $this->enhanceMessage($accountId, $folderId, $id, $m, $account, $mailBox);
169
170
		// Unified inbox hack
171
		$messageId = $id;
172
		if ($accountId === UnifiedAccount::ID) {
173
			// Add accountId, folderId for unified inbox replies
174
			list($accountId, $messageId) = json_decode(base64_decode($id));
175
			$account = $this->getAccount($accountId);
176
			$inbox = $account->getInbox();
177
			$folderId = base64_encode($inbox->getFolderId());
178
		}
179
		$json['messageId'] = $messageId;
180
		$json['accountId'] = $accountId;
181
		$json['folderId'] = $folderId;
182
		// End unified inbox hack
183
184
		return $json;
185
	}
186
187
	/**
188
	 * @NoAdminRequired
189
	 * @NoCSRFRequired
190
	 *
191
	 * @param int $accountId
192
	 * @param string $folderId
193
	 * @param mixed $id
194
	 * @return JSONResponse
195
	 */
196
	public function show($accountId, $folderId, $id) {
197
		try {
198
			$json = $this->loadMessage($accountId, $folderId, $id);
199
		} 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...
200
			return new JSONResponse([], 404);
201
		}
202
		return new JSONResponse($json);
203
	}
204
205
	/**
206
	 * @NoAdminRequired
207
	 * @NoCSRFRequired
208
	 *
209
	 * @param int $accountId
210
	 * @param string $folderId
211
	 * @param string $messageId
212
	 * @return \OCA\Mail\Http\HtmlResponse
213
	 */
214 1
	public function getHtmlBody($accountId, $folderId, $messageId) {
215
		try {
216 1
			$mailBox = $this->getFolder($accountId, $folderId);
217
218 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...
219
			$html = $m->getHtmlBody($accountId, $folderId, $messageId, function($cid) use ($m){
220
				$match = array_filter($m->attachments, function($a) use($cid){
221
					return $a['cid'] === $cid;
222
				});
223
				$match = array_shift($match);
224
				if (is_null($match)) {
225
					return null;
226
				}
227
				return $match['id'];
228 1
			});
229
230 1
			$htmlResponse = new HtmlResponse($html);
231
232
			// Harden the default security policy
233
			// FIXME: Remove once ownCloud 8.1 is a requirement for the mail app
234 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...
235 1
				$policy = new ContentSecurityPolicy();
236 1
				$policy->allowEvalScript(false);
237 1
				$policy->disallowScriptDomain('\'self\'');
238 1
				$policy->disallowConnectDomain('\'self\'');
239 1
				$policy->disallowFontDomain('\'self\'');
240 1
				$policy->disallowMediaDomain('\'self\'');
241 1
				$htmlResponse->setContentSecurityPolicy($policy);
242 1
			}
243
244
			// Enable caching
245 1
			$htmlResponse->cacheFor(60 * 60);
246 1
			$htmlResponse->addHeader('Pragma', 'cache');
247
248 1
			return $htmlResponse;
249
		} catch(\Exception $ex) {
250
			return new TemplateResponse($this->appName, 'error', ['message' => $ex->getMessage()], 'none');
251
		}
252
	}
253
254
	/**
255
	 * @NoAdminRequired
256
	 * @NoCSRFRequired
257
	 *
258
	 * @param int $accountId
259
	 * @param string $folderId
260
	 * @param string $messageId
261
	 * @param string $attachmentId
262
	 * @return AttachmentDownloadResponse
263
	 */
264 1
	public function downloadAttachment($accountId, $folderId, $messageId, $attachmentId) {
265 1
		$mailBox = $this->getFolder($accountId, $folderId);
266
267 1
		$attachment = $mailBox->getAttachment($messageId, $attachmentId);
268
269 1
		return new AttachmentDownloadResponse(
270 1
			$attachment->getContents(),
271 1
			$attachment->getName(),
272 1
			$attachment->getType());
273
	}
274
275
	/**
276
	 * @NoAdminRequired
277
	 * @NoCSRFRequired
278
	 *
279
	 * @param int $accountId
280
	 * @param string $folderId
281
	 * @param string $messageId
282
	 * @param string $attachmentId
283
	 * @param string $targetPath
284
	 * @return JSONResponse
285
	 */
286 2
	public function saveAttachment($accountId, $folderId, $messageId, $attachmentId, $targetPath) {
287 2
		$mailBox = $this->getFolder($accountId, $folderId);
288
289 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...
290 2
		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...
291
			// Save all attachments
292 1
			$m = $mailBox->getMessage($messageId);
293
			$attachmentIds = array_map(function($a){
294 1
				return $a['id'];
295 1
			}, $m->attachments);
296 1
		} else {
297 1
			$attachmentIds = [$attachmentId];
298
		}
299
300 2
		foreach($attachmentIds as $attachmentId) {
301 2
			$attachment = $mailBox->getAttachment($messageId, $attachmentId);
302
303 2
			$fileName = $attachment->getName();
304 2
			$fileParts = pathinfo($fileName);
305 2
			$fileName = $fileParts['filename'];
306 2
			$fileExtension = $fileParts['extension'];
307 2
			$fullPath = "$targetPath/$fileName.$fileExtension";
308 2
			$counter = 2;
309 2
			while($this->userFolder->nodeExists($fullPath)) {
310
				$fullPath = "$targetPath/$fileName ($counter).$fileExtension";
311
				$counter++;
312
			}
313
314 2
			$newFile = $this->userFolder->newFile($fullPath);
315 2
			$newFile->putContent($attachment->getContents());
316 2
		}
317
318 2
		return new JSONResponse();
319
	}
320
321
	/**
322
	 * @NoAdminRequired
323
	 *
324
	 * @param int $accountId
325
	 * @param string $folderId
326
	 * @param string $messageId
327
	 * @param array $flags
328
	 * @return JSONResponse
329
	 */
330 2
	public function setFlags($accountId, $folderId, $messageId, $flags) {
331 2
		$mailBox = $this->getFolder($accountId, $folderId);
332
333 2
		foreach($flags as $flag => $value) {
334 2
			$value = filter_var($value, FILTER_VALIDATE_BOOLEAN);
335 2
			if ($flag === 'unseen') {
336 1
				$flag = 'seen';
337 1
				$value = !$value;
338 1
			}
339 2
			$mailBox->setMessageFlag($messageId, '\\'.$flag, $value);
340 2
		}
341
342 2
		return new JSONResponse();
343
	}
344
345
	/**
346
	 * @NoAdminRequired
347
	 *
348
	 * @param int $accountId
349
	 * @param string $folderId
350
	 * @param string $id
351
	 * @return JSONResponse
352
	 */
353 3
	public function destroy($accountId, $folderId, $id) {
354 3
		$this->logger->debug("deleting message <$id> of folder <$folderId>, account <$accountId>");
355
		try {
356 3
			$account = $this->getAccount($accountId);
357 2
			$account->deleteMessage(base64_decode($folderId), $id);
358 1
			return new JSONResponse();
359
360 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...
361 2
			$this->logger->error("could not delete message <$id> of folder <$folderId>, "
362 2
				. "account <$accountId> because it does not exist");
363 2
			return new JSONResponse([], Http::STATUS_NOT_FOUND);
364
		}
365
	}
366
367
	/**
368
	 * @param int $accountId
369
	 * @return \OCA\Mail\Service\IAccount
370
	 */
371 9
	private function getAccount($accountId) {
372 9
		if (!array_key_exists($accountId, $this->accounts)) {
373 9
			$this->accounts[$accountId] = $this->accountService->find($this->currentUserId, $accountId);
374 8
		}
375 8
		return $this->accounts[$accountId];
376
	}
377
378
	/**
379
	 * @param int $accountId
380
	 * @param string $folderId
381
	 * @return IMailBox
382
	 */
383 6
	private function getFolder($accountId, $folderId) {
384 6
		$account = $this->getAccount($accountId);
385 6
		return $account->getMailbox(base64_decode($folderId));
386
	}
387
388
	/**
389
	 * @param string $messageId
390
	 * @param $accountId
391
	 * @param $folderId
392
	 * @return callable
393
	 */
394
	private function enrichDownloadUrl($accountId, $folderId, $messageId, $attachment) {
395
		$downloadUrl = \OCP\Util::linkToRoute('mail.messages.downloadAttachment', [
396
			'accountId' => $accountId,
397
			'folderId' => $folderId,
398
			'messageId' => $messageId,
399
			'attachmentId' => $attachment['id'],
400
		]);
401
		$downloadUrl = \OC::$server->getURLGenerator()->getAbsoluteURL($downloadUrl);
402
		$attachment['downloadUrl'] = $downloadUrl;
403
		$attachment['mimeUrl'] = $this->mimeTypeIcon($attachment['mime']);
404
405
		if ($this->attachmentIsImage($attachment)) {
406
			$attachment['isImage'] = true;
407
		}
408
		return $attachment;
409
	}
410
411
	/**
412
	 * @param $attachment
413
	 *
414
	 * Determines if the content of this attachment is an image
415
	 */
416
	private function attachmentIsImage($attachment) {
417
		return in_array($attachment['mime'], array('image/jpeg',
418
			'image/png',
419
			'image/gif'));
420
	}
421
422
	/**
423
	 * @param string $accountId
424
	 * @param string $folderId
425
	 * @param string $messageId
426
	 * @return string
427
	 */
428
	private function buildHtmlBodyUrl($accountId, $folderId, $messageId) {
429
		$htmlBodyUrl = \OC::$server->getURLGenerator()->linkToRoute('mail.messages.getHtmlBody', [
430
			'accountId' => $accountId,
431
			'folderId' => $folderId,
432
			'messageId' => $messageId,
433
		]);
434
		return \OC::$server->getURLGenerator()->getAbsoluteURL($htmlBodyUrl);
435
	}
436
437
	/**
438
	 * @param integer $accountId
439
	 * @param string $folderId
440
	 */
441
	private function loadMultiple($accountId, $folderId, $ids) {
442
		$messages = array_map(function($id) use ($accountId, $folderId){
443
			try {
444
				return $this->loadMessage($accountId, $folderId, $id);
445
			} 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...
446
				return null;
447
			}
448
		}, $ids);
449
450
		return $messages;
451
	}
452
453
	/**
454
	 * @param $accountId
455
	 * @param $folderId
456
	 * @param $id
457
	 * @param $m
458
	 * @param IAccount $account
459
	 * @param IMailBox $mailBox
460
	 * @return mixed
461
	 */
462
	private function enhanceMessage($accountId, $folderId, $id, $m, IAccount $account, $mailBox) {
463
		$json = $m->getFullMessage($account->getEmail(), $mailBox->getSpecialRole());
464
		$json['senderImage'] = $this->contactsIntegration->getPhoto($m->getFromEmail());
465
		if (isset($json['hasHtmlBody'])) {
466
			$json['htmlBodyUrl'] = $this->buildHtmlBodyUrl($accountId, $folderId, $id);
467
		}
468
469
		if (isset($json['attachment'])) {
470
			$json['attachment'] = $this->enrichDownloadUrl($accountId, $folderId, $id, $json['attachment']);
471
		}
472
		if (isset($json['attachments'])) {
473
			$json['attachments'] = array_map(function ($a) use ($accountId, $folderId, $id) {
474
				return $this->enrichDownloadUrl($accountId, $folderId, $id, $a);
475
			}, $json['attachments']);
476
477
			// show images first
478
			usort($json['attachments'], function($a, $b) {
479
				if (isset($a['isImage']) && !isset($b['isImage'])) {
480
					return -1;
481
				} elseif (!isset($a['isImage']) && isset($b['isImage'])) {
482
					return 1;
483
				} else {
484
					Util::naturalSortCompare($a['fileName'], $b['fileName']);
485
				}
486
			});
487
			return $json;
488
		}
489
		return $json;
490
	}
491
492
	/**
493
	 * Get path to the icon of a file type
494
	 *
495
	 * @todo Inject IMimeTypeDetector once core 8.2+ is supported
496
	 *
497
	 * @param string $mimeType the MIME type
498
	 */
499
	private function mimeTypeIcon($mimeType) {
500
		$ocVersion = \OC::$server->getConfig()->getSystemValue('version', '0.0.0');
501
		if (version_compare($ocVersion, '8.2.0', '<')) {
502
			// Version-hack for 8.1 and lower
503
			return \OC_Helper::mimetypeIcon($mimeType);
504
		}
505
		/* @var IMimeTypeDetector */
506
		$mimeTypeDetector = \OC::$server->getMimeTypeDetector();
507
		return $mimeTypeDetector->mimeTypeIcon($mimeType);
508
	}
509
510
}
511