Issues (159)

Security Analysis    no request data  

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  Cross-Site Scripting
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Header Injection
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

lib/MailQueueHandler.php (1 issue)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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) {
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