Completed
Pull Request — master (#192)
by Morris
01:37
created

MailQueueHandler   B

Complexity

Total Complexity 42

Size/Duplication

Total Lines 411
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 2

Test Coverage

Coverage 70.24%

Importance

Changes 0
Metric Value
wmc 42
lcom 1
cbo 2
dl 0
loc 411
ccs 118
cts 168
cp 0.7024
rs 8.295
c 0
b 0
f 0

9 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 23 1
C sendEmails() 0 52 10
D getAffectedUsers() 0 40 9
B getItemsForUser() 0 32 3
A getLanguage() 0 7 2
A getSenderData() 0 14 4
C sendEmailToUser() 0 79 7
A parseEvent() 0 18 4
A deleteSentItems() 0 11 2

How to fix   Complexity   

Complex Class

Complex classes like MailQueueHandler often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use MailQueueHandler, and based on these observations, apply Extract Interface, too.

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] === 'no') {
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 (\UnexpectedValueException $e) {
189
				// continue;
190
			}
191
		}
192
		$this->activityManager->setRequirePNG(false);
193
194
		// Delete all entries we dealt with
195
		$this->deleteSentItems($deleteItemsForUsers, $sendTime);
196
197
		return count($affectedUsers);
198
	}
199
200
	/**
201
	 * Get the users we want to send an email to
202
	 *
203
	 * @param int|null $limit
204
	 * @param int $latestSend
205
	 * @param bool $forceSending
206
	 * @param int|null $restrictEmails
207
	 * @return array
208
	 */
209 6
	protected function getAffectedUsers($limit, $latestSend, $forceSending, $restrictEmails) {
210 6
		$query = $this->connection->getQueryBuilder();
211 6
		$query->select('amq_affecteduser')
212 6
			->selectAlias($query->createFunction('MIN(' . $query->getColumnName('amq_latest_send') . ')'), 'amq_trigger_time')
213 6
			->from('activity_mq')
214 6
			->groupBy('amq_affecteduser')
215 6
			->orderBy('amq_trigger_time', 'ASC');
216
217 6
		if ($limit > 0) {
218 5
			$query->setMaxResults($limit);
219
		}
220
221 6
		if ($forceSending) {
222
			$query->where($query->expr()->lt('amq_timestamp', $query->createNamedParameter($latestSend)));
223
		} else {
224 6
			$query->where($query->expr()->lt('amq_latest_send', $query->createNamedParameter($latestSend)));
225
		}
226
227 6
		if ($restrictEmails !== null) {
228
			if ($restrictEmails === UserSettings::EMAIL_SEND_HOURLY) {
229
				$query->where($query->expr()->lte('amq_timestamp', $query->createFunction($query->getColumnName('amq_latest_send') . ' + ' . 3600)));
230
			} else if ($restrictEmails === UserSettings::EMAIL_SEND_DAILY) {
231
				$query->where($query->expr()->eq('amq_timestamp', $query->createFunction($query->getColumnName('amq_latest_send') . ' + ' . 3600 * 24)));
232
			} else if ($restrictEmails === UserSettings::EMAIL_SEND_WEEKLY) {
233
				$query->where($query->expr()->eq('amq_timestamp', $query->createFunction($query->getColumnName('amq_latest_send') . ' + ' . 3600 * 24 * 7)));
234
			} else if ($restrictEmails === UserSettings::EMAIL_SEND_ASAP) {
235
				$query->where($query->expr()->eq('amq_timestamp', 'amq_latest_send'));
236
			}
237
		}
238
239 6
		$result = $query->execute();
240
241 6
		$affectedUsers = array();
242 6
		while ($row = $result->fetch()) {
243 6
			$affectedUsers[] = $row['amq_affecteduser'];
244
		}
245 6
		$result->closeCursor();
246
247 6
		return $affectedUsers;
248
	}
249
250
	/**
251
	 * Get all items for the user we want to send an email to
252
	 *
253
	 * @param string $affectedUser
254
	 * @param int $maxTime
255
	 * @param int $maxNumItems
256
	 * @return array [data of the first max. 200 entries, total number of entries]
257
	 */
258 7
	protected function getItemsForUser($affectedUser, $maxTime, $maxNumItems = self::ENTRY_LIMIT) {
259 7
		$query = $this->connection->prepare(
260
			'SELECT * '
261
			. ' FROM `*PREFIX*activity_mq` '
262
			. ' WHERE `amq_timestamp` <= ? '
263
			. ' AND `amq_affecteduser` = ? '
264 7
			. ' ORDER BY `amq_timestamp` ASC',
265
			$maxNumItems
266
		);
267 7
		$query->execute([(int) $maxTime, $affectedUser]);
268
269 7
		$activities = array();
270 7
		while ($row = $query->fetch()) {
271 7
			$activities[] = $row;
272
		}
273
274 7
		if (isset($activities[$maxNumItems - 1])) {
275
			// Reached the limit, run a query to get the actual count.
276 1
			$query = $this->connection->prepare(
277
				'SELECT COUNT(*) AS `actual_count`'
278
				. ' FROM `*PREFIX*activity_mq` '
279
				. ' WHERE `amq_timestamp` <= ? '
280 1
				. ' AND `amq_affecteduser` = ?'
281
			);
282 1
			$query->execute([(int) $maxTime, $affectedUser]);
283
284 1
			$row = $query->fetch();
285 1
			return [$activities, $row['actual_count'] - $maxNumItems];
286
		} else {
287 7
			return [$activities, 0];
288
		}
289
	}
290
291
	/**
292
	 * Get a language object for a specific language
293
	 *
294
	 * @param string $lang Language identifier
295
	 * @return \OCP\IL10N Language object of $lang
296
	 */
297 1
	protected function getLanguage($lang) {
298 1
		if (!isset($this->languages[$lang])) {
299 1
			$this->languages[$lang] = $this->lFactory->get('activity', $lang);
300
		}
301
302 1
		return $this->languages[$lang];
303
	}
304
305
	/**
306
	 * Get the sender data
307
	 * @param string $setting Either `email` or `name`
308
	 * @return string
309
	 */
310 1
	protected function getSenderData($setting) {
311 1
		if (empty($this->senderAddress)) {
312 1
			$this->senderAddress = Util::getDefaultEmailAddress('no-reply');
313
		}
314 1
		if (empty($this->senderName)) {
315 1
			$defaults = new Defaults();
316 1
			$this->senderName = $defaults->getName();
317
		}
318
319 1
		if ($setting === 'email') {
320 1
			return $this->senderAddress;
321
		}
322 1
		return $this->senderName;
323
	}
324
325
	/**
326
	 * Send a notification to one user
327
	 *
328
	 * @param string $userName Username of the recipient
329
	 * @param string $email Email address of the recipient
330
	 * @param string $lang Selected language of the recipient
331
	 * @param string $timezone Selected timezone of the recipient
332
	 * @param int $maxTime
333
	 * @return bool True if the entries should be removed, false otherwise
334
	 * @throws \UnexpectedValueException
335
	 */
336 1
	protected function sendEmailToUser($userName, $email, $lang, $timezone, $maxTime) {
337 1
		$user = $this->userManager->get($userName);
338 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...
339 1
			return true;
340
		}
341
342 1
		list($mailData, $skippedCount) = $this->getItemsForUser($userName, $maxTime);
343
344 1
		$l = $this->getLanguage($lang);
345 1
		$this->dataHelper->setUser($userName);
346 1
		$this->dataHelper->setL10n($l);
347 1
		$this->activityManager->setCurrentUserId($userName);
348
349 1
		$template = $this->mailer->createEMailTemplate();
350 1
		$template->addHeader();
351 1
		$template->addHeading($l->t('Hello %s',[$user->getDisplayName()]), $l->t('Hello %s,',[$user->getDisplayName()]));
352 1
		$template->addBodyText($l->t('There was some activity at %s', [$this->urlGenerator->getAbsoluteURL('/')]));
353
354 1
		$activityEvents = [];
355 1
		foreach ($mailData as $activity) {
356 1
			$event = $this->activityManager->generateEvent();
357
			try {
358 1
				$event->setApp($activity['amq_appid'])
359 1
					->setType($activity['amq_type'])
360 1
					->setTimestamp((int) $activity['amq_timestamp'])
361 1
					->setSubject($activity['amq_subject'], json_decode($activity['amq_subjectparams'], true));
362
			} catch (\InvalidArgumentException $e) {
363
				continue;
364
			}
365
366 1
			$relativeDateTime = $this->dateFormatter->formatDateTimeRelativeDay(
367 1
				$activity['amq_timestamp'],
368 1
				'long', 'short',
369 1
				new \DateTimeZone($timezone), $l
370
			);
371
372
			try {
373 1
				$event = $this->parseEvent($lang, $event);
374
			} catch (\InvalidArgumentException $e) {
375
				continue;
376
			}
377
378 1
			$activityEvents[] = [
379 1
				'event' => $event,
380 1
				'relativeDateTime' => $relativeDateTime
381
			];
382
383 1
			$template->addBodyListItem($event->getParsedSubject(), $relativeDateTime, $event->getIcon());
384
		}
385
386 1
		if ($skippedCount) {
387
			$template->addBodyListItem($l->n('and %n more ', 'and %n more ', $skippedCount));
388
		}
389
390 1
		$template->setMetaData('activity.Notification', [
391 1
			'displayname' => $user->getDisplayName(),
392 1
			'url' => $this->urlGenerator->getAbsoluteURL('/'),
393 1
			'activityEvents' => $activityEvents,
394 1
			'skippedCount' => $skippedCount,
395
		]);
396
397 1
		$template->addFooter();
398
399 1
		$message = $this->mailer->createMessage();
400 1
		$message->setTo([$email => $user->getDisplayName()]);
401 1
		$message->setSubject((string) $l->t('Activity notification'));
402 1
		$message->setHtmlBody($template->renderHtml());
403 1
		$message->setPlainBody($template->renderText());
404 1
		$message->setFrom([$this->getSenderData('email') => $this->getSenderData('name')]);
405
406
		try {
407 1
			$this->mailer->send($message);
408
		} catch (\Exception $e) {
409
			return false;
410
		}
411
412 1
		$this->activityManager->setCurrentUserId(null);
413 1
		return true;
414
	}
415
416
	/**
417
	 * @param string $lang
418
	 * @param IEvent $event
419
	 * @return IEvent
420
	 * @throws \InvalidArgumentException when the event could not be parsed
421
	 */
422 1
	protected function parseEvent($lang, IEvent $event) {
423 1
		foreach ($this->activityManager->getProviders() as $provider) {
424
			try {
425
				$this->activityManager->setFormattingObject($event->getObjectType(), $event->getObjectId());
426
				$event = $provider->parse($lang, $event);
427
				$this->activityManager->setFormattingObject('', 0);
428
			} catch (\InvalidArgumentException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
429
			}
430
		}
431
432 1
		if (!$event->getParsedSubject()) {
433 1
			$this->activityManager->setFormattingObject($event->getObjectType(), $event->getObjectId());
434 1
			$event = $this->legacyParser->parse($lang, $event);
435 1
			$this->activityManager->setFormattingObject('', 0);
436
		}
437
438 1
		return $event;
439
	}
440
441
	/**
442
	 * Delete all entries we dealt with
443
	 *
444
	 * @param array $affectedUsers
445
	 * @param int $maxTime
446
	 */
447 5
	protected function deleteSentItems(array $affectedUsers, $maxTime) {
448 5
		if (empty($affectedUsers)) {
449
			return;
450
		}
451
452 5
		$query = $this->connection->getQueryBuilder();
453 5
		$query->delete('activity_mq')
454 5
			->where($query->expr()->lte('amq_timestamp', $query->createNamedParameter($maxTime, IQueryBuilder::PARAM_INT)))
455 5
			->andWhere($query->expr()->in('amq_affecteduser', $query->createNamedParameter($affectedUsers, IQueryBuilder::PARAM_STR_ARRAY), IQueryBuilder::PARAM_STR));
456 5
		$query->execute();
457 5
	}
458
}
459