MessagesController::enrichDownloadUrl()   A
last analyzed

Complexity

Conditions 3
Paths 3

Size

Total Lines 18
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

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