MailQueueHandler::getHTMLSubject()   A
last analyzed

Complexity

Conditions 4
Paths 5

Size

Total Lines 20

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 8.1239

Importance

Changes 0
Metric Value
dl 0
loc 20
ccs 4
cts 11
cp 0.3636
rs 9.6
c 0
b 0
f 0
cc 4
nc 5
nop 1
crap 8.1239
1
<?php
2
/**
3
 * @copyright Copyright (c) 2016, ownCloud, Inc.
4
 *
5
 * @author Joas Schilling <[email protected]>
6
 * @author Lukas Reschke <[email protected]>
7
 *
8
 * @license AGPL-3.0
9
 *
10
 * This code is free software: you can redistribute it and/or modify
11
 * it under the terms of the GNU Affero General Public License, version 3,
12
 * as published by the Free Software Foundation.
13
 *
14
 * This program is distributed in the hope that it will be useful,
15
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17
 * GNU Affero General Public License for more details.
18
 *
19
 * You should have received a copy of the GNU Affero General Public License, version 3,
20
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
21
 *
22
 */
23
24
namespace OCA\Activity;
25
26
use OCA\Activity\Extension\LegacyParser;
27
use OCP\Activity\IEvent;
28
use OCP\Activity\IManager;
29
use OCP\DB\QueryBuilder\IQueryBuilder;
30
use OCP\Defaults;
31
use OCP\IConfig;
32
use OCP\IDateTimeFormatter;
33
use OCP\IDBConnection;
34
use OCP\ILogger;
35
use OCP\IURLGenerator;
36
use OCP\IUser;
37
use OCP\IUserManager;
38
use OCP\L10N\IFactory;
39
use OCP\Mail\IMailer;
40
use OCP\Util;
41
42
/**
43
 * Class MailQueueHandler
44
 * Gets the users from the database and
45
 *
46
 * @package OCA\Activity
47
 */
48
class MailQueueHandler {
49
50
	const CLI_EMAIL_BATCH_SIZE = 500;
51
52
	const WEB_EMAIL_BATCH_SIZE = 25;
53
54
	/** Number of entries we want to list in the email */
55
	const ENTRY_LIMIT = 200;
56
57
	/** @var array */
58
	protected $languages;
59
60
	/** @var string */
61
	protected $senderAddress;
62
63
	/** @var string */
64
	protected $senderName;
65
66
	/** @var IDateTimeFormatter */
67
	protected $dateFormatter;
68
69
	/** @var DataHelper */
70
	protected $dataHelper;
71
72
	/** @var IDBConnection */
73
	protected $connection;
74
75
	/** @var IMailer */
76
	protected $mailer;
77
78
	/** @var IURLGenerator */
79
	protected $urlGenerator;
80
81
	/** @var IUserManager */
82
	protected $userManager;
83
84
	/** @var IFactory */
85
	protected $lFactory;
86
87
	/** @var IManager */
88
	protected $activityManager;
89
90
	/** @var LegacyParser */
91
	protected $legacyParser;
92
93
	/** @var IConfig */
94
	protected $config;
95
96
	/** @var ILogger */
97
	protected $logger;
98
99
	/**
100
	 * Constructor
101
	 *
102
	 * @param IDateTimeFormatter $dateFormatter
103
	 * @param IDBConnection $connection
104
	 * @param DataHelper $dataHelper
105
	 * @param IMailer $mailer
106
	 * @param IURLGenerator $urlGenerator
107
	 * @param IUserManager $userManager
108
	 * @param IFactory $lFactory
109
	 * @param IManager $activityManager
110
	 * @param LegacyParser $legacyParser
111
	 * @param IConfig $config
112
	 * @param ILogger $logger
113
	 */
114 10
	public function __construct(IDateTimeFormatter $dateFormatter,
115
								IDBConnection $connection,
116
								DataHelper $dataHelper,
117
								IMailer $mailer,
118
								IURLGenerator $urlGenerator,
119
								IUserManager $userManager,
120
								IFactory $lFactory,
121
								IManager $activityManager,
122
								LegacyParser $legacyParser,
123
								IConfig $config,
124
								ILogger $logger) {
125 10
		$this->dateFormatter = $dateFormatter;
126 10
		$this->connection = $connection;
127 10
		$this->dataHelper = $dataHelper;
128 10
		$this->mailer = $mailer;
129 10
		$this->urlGenerator = $urlGenerator;
130 10
		$this->userManager = $userManager;
131 10
		$this->lFactory = $lFactory;
132 10
		$this->activityManager = $activityManager;
133 10
		$this->legacyParser = $legacyParser;
134 10
		$this->config = $config;
135 10
		$this->logger = $logger;
136 10
	}
137
138
	/**
139
	 * Send an email to {$limit} users
140
	 *
141
	 * @param int $limit Number of users we want to send an email to
142
	 * @param int $sendTime The latest send time
143
	 * @param bool $forceSending Ignores latest send and just sends all emails
144
	 * @param null|int $restrictEmails null or one of UserSettings::EMAIL_SEND_*
145
	 * @return int Number of users we sent an email to
146
	 */
147
	public function sendEmails($limit, $sendTime, $forceSending = false, $restrictEmails = null) {
148
		// Get all users which should receive an email
149
		$affectedUsers = $this->getAffectedUsers($limit, $sendTime, $forceSending, $restrictEmails);
150
		if (empty($affectedUsers)) {
151
			// No users found to notify, mission abort
152
			return 0;
153
		}
154
155
		$userLanguages = $this->config->getUserValueForUsers('core', 'lang', $affectedUsers);
156
		$userTimezones = $this->config->getUserValueForUsers('core', 'timezone', $affectedUsers);
157
		$userEmails = $this->config->getUserValueForUsers('settings', 'email', $affectedUsers);
158
		$userEnabled = $this->config->getUserValueForUsers('core', 'enabled', $affectedUsers);
159
160
		// Send Email
161
		$default_lang = $this->config->getSystemValue('default_language', 'en');
162
		$defaultTimeZone = date_default_timezone_get();
163
164
		$deleteItemsForUsers = [];
165
		$this->activityManager->setRequirePNG(true);
166
		foreach ($affectedUsers as $user) {
167
			if (isset($userEnabled[$user]) && $userEnabled[$user] === 'false') {
168
				$deleteItemsForUsers[] = $user;
169
				continue;
170
			}
171
172
			if (empty($userEmails[$user])) {
173
				// The user did not setup an email address
174
				// So we will not send an email :(
175
				$this->logger->debug("Couldn't send notification email to user '{user}' (email address isn't set for that user)", ['user' => $user, 'app' => 'activity']);
176
				$deleteItemsForUsers[] = $user;
177
				continue;
178
			}
179
180
			$language = (!empty($userLanguages[$user])) ? $userLanguages[$user] : $default_lang;
181
			$timezone = (!empty($userTimezones[$user])) ? $userTimezones[$user] : $defaultTimeZone;
182
			try {
183
				if ($this->sendEmailToUser($user, $userEmails[$user], $language, $timezone, $sendTime)) {
184
					$deleteItemsForUsers[] = $user;
185
				} else {
186
					$this->logger->debug("Failed sending activity email to user '{user}'.", ['user' => $user, 'app' => 'activity']);
187
				}
188
			} catch (\Exception $e) {
189
				$this->logger->logException($e, [
190
					'message' => 'Failed sending activity email to user "{user}"',
191
					'user' => $user,
192
					'app' => 'activity',
193
				]);
194
				// continue;
195
			}
196
		}
197
		$this->activityManager->setRequirePNG(false);
198
199
		// Delete all entries we dealt with
200
		$this->deleteSentItems($deleteItemsForUsers, $sendTime);
201
202
		return count($affectedUsers);
203
	}
204
205
	/**
206
	 * Get the users we want to send an email to
207
	 *
208
	 * @param int|null $limit
209
	 * @param int $latestSend
210
	 * @param bool $forceSending
211
	 * @param int|null $restrictEmails
212
	 * @return array
213
	 */
214 6
	protected function getAffectedUsers($limit, $latestSend, $forceSending, $restrictEmails) {
215 6
		$query = $this->connection->getQueryBuilder();
216 6
		$query->select('amq_affecteduser')
217 6
			->selectAlias($query->createFunction('MIN(' . $query->getColumnName('amq_latest_send') . ')'), 'amq_trigger_time')
218 6
			->from('activity_mq')
219 6
			->groupBy('amq_affecteduser')
220 6
			->orderBy('amq_trigger_time', 'ASC');
221
222 6
		if ($limit > 0) {
223 5
			$query->setMaxResults($limit);
224
		}
225
226 6
		if ($forceSending) {
227
			$query->where($query->expr()->lt('amq_timestamp', $query->createNamedParameter($latestSend)));
228
		} else {
229 6
			$query->where($query->expr()->lt('amq_latest_send', $query->createNamedParameter($latestSend)));
230
		}
231
232 6
		if ($restrictEmails !== null) {
233
			if ($restrictEmails === UserSettings::EMAIL_SEND_HOURLY) {
234
				$query->where($query->expr()->eq('amq_timestamp', $query->func()->subtract('amq_latest_send', $query->expr()->literal(3600))));
235
			} else if ($restrictEmails === UserSettings::EMAIL_SEND_DAILY) {
236
				$query->where($query->expr()->eq('amq_timestamp', $query->func()->subtract('amq_latest_send', $query->expr()->literal(3600 * 24))));
237
			} else if ($restrictEmails === UserSettings::EMAIL_SEND_WEEKLY) {
238
				$query->where($query->expr()->eq('amq_timestamp', $query->func()->subtract('amq_latest_send', $query->expr()->literal(3600 * 24 * 7))));
239
			} else if ($restrictEmails === UserSettings::EMAIL_SEND_ASAP) {
240
				$query->where($query->expr()->eq('amq_timestamp', 'amq_latest_send'));
241
			}
242
		}
243
244 6
		$result = $query->execute();
245
246 6
		$affectedUsers = array();
247 6
		while ($row = $result->fetch()) {
248 6
			$affectedUsers[] = $row['amq_affecteduser'];
249
		}
250 6
		$result->closeCursor();
251
252 6
		return $affectedUsers;
253
	}
254
255
	/**
256
	 * Get all items for the user we want to send an email to
257
	 *
258
	 * @param string $affectedUser
259
	 * @param int $maxTime
260
	 * @param int $maxNumItems
261
	 * @return array [data of the first max. 200 entries, total number of entries]
262
	 */
263 7
	protected function getItemsForUser($affectedUser, $maxTime, $maxNumItems = self::ENTRY_LIMIT) {
264 7
		$query = $this->connection->getQueryBuilder();
265 7
		$query->select('*')
266 7
			->from('activity_mq')
267 7
			->where($query->expr()->lte('amq_timestamp', $query->createNamedParameter($maxTime)))
268 7
			->andWhere($query->expr()->eq('amq_affecteduser', $query->createNamedParameter($affectedUser)))
269 7
			->orderBy('amq_timestamp', 'ASC')
270 7
			->setMaxResults($maxNumItems);
271 7
		$result = $query->execute();
272
273 7
		$activities = [];
274 7
		while ($row = $result->fetch()) {
275 7
			$activities[] = $row;
276
		}
277 7
		$result->closeCursor();
278
279 7
		if (isset($activities[$maxNumItems - 1])) {
280
			// Reached the limit, run a query to get the actual count.
281 1
			$query = $this->connection->getQueryBuilder();
282 1
			$query->selectAlias($query->func()->count('*'), 'actual_count')
283 1
				->from('activity_mq')
284 1
				->where($query->expr()->lte('amq_timestamp', $query->createNamedParameter($maxTime)))
285 1
				->andWhere($query->expr()->eq('amq_affecteduser', $query->createNamedParameter($affectedUser)));
286 1
			$result = $query->execute();
287 1
			$row = $result->fetch();
288 1
			$result->closeCursor();
289
290 1
			return [$activities, $row['actual_count'] - $maxNumItems];
291
		}
292
293 7
		return [$activities, 0];
294
	}
295
296
	/**
297
	 * Get a language object for a specific language
298
	 *
299
	 * @param string $lang Language identifier
300
	 * @return \OCP\IL10N Language object of $lang
301
	 */
302 1
	protected function getLanguage($lang) {
303 1
		if (!isset($this->languages[$lang])) {
304 1
			$this->languages[$lang] = $this->lFactory->get('activity', $lang);
305
		}
306
307 1
		return $this->languages[$lang];
308
	}
309
310
	/**
311
	 * Get the sender data
312
	 * @param string $setting Either `email` or `name`
313
	 * @return string
314
	 */
315 1
	protected function getSenderData($setting) {
316 1
		if (empty($this->senderAddress)) {
317 1
			$this->senderAddress = Util::getDefaultEmailAddress('no-reply');
318
		}
319 1
		if (empty($this->senderName)) {
320 1
			$defaults = new Defaults();
321 1
			$this->senderName = $defaults->getName();
322
		}
323
324 1
		if ($setting === 'email') {
325 1
			return $this->senderAddress;
326
		}
327 1
		return $this->senderName;
328
	}
329
330
	/**
331
	 * Send a notification to one user
332
	 *
333
	 * @param string $userName Username of the recipient
334
	 * @param string $email Email address of the recipient
335
	 * @param string $lang Selected language of the recipient
336
	 * @param string $timezone Selected timezone of the recipient
337
	 * @param int $maxTime
338
	 * @return bool True if the entries should be removed, false otherwise
339
	 * @throws \UnexpectedValueException
340
	 */
341 1
	protected function sendEmailToUser($userName, $email, $lang, $timezone, $maxTime) {
342 1
		$user = $this->userManager->get($userName);
343 1
		if (!$user instanceof IUser) {
0 ignored issues
show
Bug introduced by
The class OCP\IUser does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
344 1
			return true;
345
		}
346
347 1
		list($mailData, $skippedCount) = $this->getItemsForUser($userName, $maxTime);
348
349 1
		$l = $this->getLanguage($lang);
350 1
		$this->dataHelper->setUser($userName);
351 1
		$this->dataHelper->setL10n($l);
352 1
		$this->activityManager->setCurrentUserId($userName);
353
354 1
		$activityEvents = [];
355 1
		foreach ($mailData as $activity) {
356 1
			$event = $this->activityManager->generateEvent();
357
			try {
358 1
				$event->setApp((string) $activity['amq_appid'])
359 1
					->setType((string) $activity['amq_type'])
360 1
					->setTimestamp((int) $activity['amq_timestamp'])
361 1
					->setSubject((string) $activity['amq_subject'], (array) json_decode($activity['amq_subjectparams'], true))
362 1
					->setObject((string) $activity['object_type'], (int) $activity['object_id']);
363
			} catch (\InvalidArgumentException $e) {
364
				continue;
365
			}
366
367 1
			$relativeDateTime = $this->dateFormatter->formatDateTimeRelativeDay(
368 1
				(int) $activity['amq_timestamp'],
369 1
				'long', 'short',
370 1
				new \DateTimeZone($timezone), $l
371
			);
372
373
			try {
374 1
				$event = $this->parseEvent($lang, $event);
375
			} catch (\InvalidArgumentException $e) {
376
				continue;
377
			}
378
379 1
			$activityEvents[] = [
380 1
				'event' => $event,
381 1
				'relativeDateTime' => $relativeDateTime
382
			];
383
		}
384
385 1
		$template = $this->mailer->createEMailTemplate('activity.Notification', [
386 1
			'displayname' => $user->getDisplayName(),
387 1
			'url' => $this->urlGenerator->getAbsoluteURL('/'),
388 1
			'activityEvents' => $activityEvents,
389 1
			'skippedCount' => $skippedCount,
390
		]);
391 1
		$template->setSubject($l->t('Activity notification for %s', $this->getSenderData('name')));
392 1
		$template->addHeader();
393 1
		$template->addHeading($l->t('Hello %s',[$user->getDisplayName()]), $l->t('Hello %s,',[$user->getDisplayName()]));
394
395 1
		$homeLink = '<a href="' . $this->urlGenerator->getAbsoluteURL('/') . '">' . htmlspecialchars($this->getSenderData('name')) . '</a>';
396 1
		$template->addBodyText(
397 1
			$l->t('There was some activity at %s', [$homeLink]),
398 1
			$l->t('There was some activity at %s', [$this->urlGenerator->getAbsoluteURL('/')])
399
		);
400
401 1
		foreach ($activityEvents as $activity) {
402
			/** @var IEvent $event */
403 1
			$event = $activity['event'];
404 1
			$relativeDateTime = $activity['relativeDateTime'];
405
406 1
			$template->addBodyListItem($this->getHTMLSubject($event), $relativeDateTime, $event->getIcon(), $event->getParsedSubject());
407
		}
408
409 1
		if ($skippedCount) {
410
			$template->addBodyListItem($l->n('and %n more ', 'and %n more ', $skippedCount));
411
		}
412
413 1
		$template->addFooter();
414
415 1
		$message = $this->mailer->createMessage();
416 1
		$message->setTo([$email => $user->getDisplayName()]);
417 1
		$message->useTemplate($template);
418 1
		$message->setFrom([$this->getSenderData('email') => $this->getSenderData('name')]);
419
420
		try {
421 1
			$this->mailer->send($message);
422
		} catch (\Exception $e) {
423
			return false;
424
		}
425
426 1
		$this->activityManager->setCurrentUserId(null);
427 1
		return true;
428
	}
429
430
	/**
431
	 * @param IEvent $event
432
	 * @return string
433
	 */
434 1
	protected function getHTMLSubject(IEvent $event): string {
435 1
		$placeholders = $replacements = [];
436 1
		foreach ($event->getRichSubjectParameters() as $placeholder => $parameter) {
437
			$placeholders[] = '{' . $placeholder . '}';
438
439
			if ($parameter['type'] === 'file') {
440
				$replacement = $parameter['path'];
441
			} else {
442
				$replacement = $parameter['name'];
443
			}
444
445
			if (isset($parameter['link'])) {
446
				$replacements[] = '<a href="' . $parameter['link'] . '">' . htmlspecialchars($replacement) . '</a>';
447
			} else {
448
				$replacements[] = '<strong>' . htmlspecialchars($replacement) . '</strong>';
449
			}
450
		}
451
452 1
		return str_replace($placeholders, $replacements, $event->getRichSubject());
453
	}
454
455
	/**
456
	 * @param string $lang
457
	 * @param IEvent $event
458
	 * @return IEvent
459
	 * @throws \InvalidArgumentException when the event could not be parsed
460
	 */
461 1
	protected function parseEvent($lang, IEvent $event) {
462 1
		foreach ($this->activityManager->getProviders() as $provider) {
463
			try {
464
				$this->activityManager->setFormattingObject($event->getObjectType(), $event->getObjectId());
465
				$event = $provider->parse($lang, $event);
466
				$this->activityManager->setFormattingObject('', 0);
467
			} catch (\InvalidArgumentException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
468
			}
469
		}
470
471 1
		if (!$event->getParsedSubject()) {
472 1
			$this->activityManager->setFormattingObject($event->getObjectType(), $event->getObjectId());
473 1
			$event = $this->legacyParser->parse($lang, $event);
474 1
			$this->activityManager->setFormattingObject('', 0);
475
		}
476
477 1
		return $event;
478
	}
479
480
	/**
481
	 * Delete all entries we dealt with
482
	 *
483
	 * @param array $affectedUsers
484
	 * @param int $maxTime
485
	 */
486 5
	protected function deleteSentItems(array $affectedUsers, $maxTime) {
487 5
		if (empty($affectedUsers)) {
488
			return;
489
		}
490
491 5
		$query = $this->connection->getQueryBuilder();
492 5
		$query->delete('activity_mq')
493 5
			->where($query->expr()->lte('amq_timestamp', $query->createNamedParameter($maxTime, IQueryBuilder::PARAM_INT)))
494 5
			->andWhere($query->expr()->in('amq_affecteduser', $query->createNamedParameter($affectedUsers, IQueryBuilder::PARAM_STR_ARRAY), IQueryBuilder::PARAM_STR));
495 5
		$query->execute();
496 5
	}
497
}
498