Completed
Pull Request — master (#1253)
by
unknown
07:51
created

MessagesController::enrichDownloadUrl()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 16
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

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