Completed
Push — patch_1-1-7 ( d70ee2...4638d6 )
by Emanuele
14:49 queued 12s
created

validateNotifierToken()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 22
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
cc 3
eloc 10
nc 3
nop 4
dl 0
loc 22
rs 9.9332
c 1
b 0
f 1
1
<?php
2
3
/**
4
 * Functions to support the sending of notifications (new posts, replys, topics)
5
 *
6
 * @name      ElkArte Forum
7
 * @copyright ElkArte Forum contributors
8
 * @license   BSD http://opensource.org/licenses/BSD-3-Clause
9
 *
10
 * @version 1.1
11
 *
12
 */
13
14
/**
15
 * Sends a notification to members who have elected to receive emails
16
 * when things happen to a topic, such as replies are posted.
17
 * The function automatically finds the subject and its board, and
18
 * checks permissions for each member who is "signed up" for notifications.
19
 * It will not send 'reply' notifications more than once in a row.
20
 *
21
 * @param int[]|int $topics - represents the topics the action is happening to.
22
 * @param string $type - can be any of reply, sticky, lock, unlock, remove,
23
 *                       move, merge, and split.  An appropriate message will be sent for each.
24
 * @param int[]|int $exclude = array() - members in the exclude array will not be
25
 *                                   processed for the topic with the same key.
26
 * @param int[]|int $members_only = array() - are the only ones that will be sent the notification if they have it on.
27
 * @param mixed[] $pbe = array() - array containing user_info if this is being run as a result of an email posting
28
 * @uses Post language file
29
 * @throws Elk_Exception
30
 */
31
function sendNotifications($topics, $type, $exclude = array(), $members_only = array(), $pbe = array())
32
{
33
	global $txt, $scripturl, $language, $user_info, $webmaster_email, $mbname, $modSettings;
34
35
	$db = database();
36
37
	// Coming in from emailpost or emailtopic, if so pbe values will be set to the credentials of the emailer
38
	$user_id = (!empty($pbe['user_info']['id']) && !empty($modSettings['maillist_enabled'])) ? $pbe['user_info']['id'] : $user_info['id'];
39
	$user_language = (!empty($pbe['user_info']['language']) && !empty($modSettings['maillist_enabled'])) ? $pbe['user_info']['language'] : $user_info['language'];
40
41
	// Can't do it if there's no topics.
42
	if (empty($topics))
43
		return;
44
45
	// It must be an array - it must!
46
	if (!is_array($topics))
47
		$topics = array($topics);
48
49
	// I hope we are not sending one of those silly moderation notices
50
	$maillist = !empty($modSettings['maillist_enabled']) && !empty($modSettings['pbe_post_enabled']);
51
	if ($type !== 'reply' && !empty($maillist) && !empty($modSettings['pbe_no_mod_notices']))
52
		return;
53
54
	// Load in our dependencies
55
	require_once(SUBSDIR . '/Emailpost.subs.php');
56
	require_once(SUBSDIR . '/Mail.subs.php');
57
58
	// Get the subject, body and basic poster details, number of attachments if any
59
	$result = $db->query('', '
60
		SELECT mf.subject, ml.body, ml.id_member, t.id_last_msg, t.id_topic, t.id_board, mem.signature,
61
			COALESCE(mem.real_name, ml.poster_name) AS poster_name, COUNT(a.id_attach) as num_attach
62
		FROM {db_prefix}topics AS t
63
			INNER JOIN {db_prefix}messages AS mf ON (mf.id_msg = t.id_first_msg)
64
			INNER JOIN {db_prefix}messages AS ml ON (ml.id_msg = t.id_last_msg)
65
			LEFT JOIN {db_prefix}members AS mem ON (mem.id_member = ml.id_member)
66
			LEFT JOIN {db_prefix}attachments AS a ON(a.attachment_type = {int:attachment_type} AND a.id_msg = t.id_last_msg)
67
		WHERE t.id_topic IN ({array_int:topic_list})
68
		GROUP BY t.id_topic, mf.subject, ml.body, ml.id_member, mem.signature, mem.real_name, ml.poster_name',
69
		array(
70
			'topic_list' => $topics,
71
			'attachment_type' => 0,
72
		)
73
	);
74
	$topicData = array();
75
	$boards_index = array();
76
	while ($row = $db->fetch_assoc($result))
77
	{
78
		// Convert to markdown e.g. text ;) and clean it up
79
		pbe_prepare_text($row['body'], $row['subject'], $row['signature']);
80
81
		// all the boards for these topics, used to find all the members to be notified
82
		$boards_index[] = $row['id_board'];
83
84
		// And the information we are going to tell them about
85
		$topicData[$row['id_topic']] = array(
86
			'subject' => $row['subject'],
87
			'body' => $row['body'],
88
			'last_id' => $row['id_last_msg'],
89
			'topic' => $row['id_topic'],
90
			'board' => $row['id_board'],
91
			'name' => $type === 'reply' ? $row['poster_name'] : $user_info['name'],
92
			'exclude' => '',
93
			'signature' => $row['signature'],
94
			'attachments' => $row['num_attach'],
95
		);
96
	}
97
	$db->free_result($result);
98
99
	// Work out any exclusions...
100
	foreach ($topics as $key => $id)
101
		if (isset($topicData[$id]) && !empty($exclude[$key]))
102
			$topicData[$id]['exclude'] = (int) $exclude[$key];
103
104
	// Nada?
105
	if (empty($topicData))
106
		trigger_error('sendNotifications(): topics not found', E_USER_NOTICE);
107
108
	$topics = array_keys($topicData);
109
110
	// Just in case they've gone walkies.
111
	if (empty($topics))
112
		return;
113
114
	// Insert all of these items into the digest log for those who want notifications later.
115
	$digest_insert = array();
116
	foreach ($topicData as $id => $data)
117
		$digest_insert[] = array($data['topic'], $data['last_id'], $type, (int) $data['exclude']);
118
119
	$db->insert('',
120
		'{db_prefix}log_digest',
121
		array(
122
			'id_topic' => 'int', 'id_msg' => 'int', 'note_type' => 'string', 'exclude' => 'int',
123
		),
124
		$digest_insert,
125
		array()
126
	);
127
128
	// Are we doing anything here?
129
	$sent = 0;
130
131
	// Using the posting email function in either group or list mode
132
	if ($maillist)
133
	{
134
		// Find the members with *board* notifications on.
135
		$members = $db->query('', '
136
			SELECT
137
				mem.id_member, mem.email_address, mem.notify_regularity, mem.notify_types, mem.notify_send_body, mem.lngfile, mem.warning,
138
				ln.sent, mem.id_group, mem.additional_groups, b.member_groups, mem.id_post_group, b.name, b.id_profile,
139
				ln.id_board, mem.password_salt
140
			FROM {db_prefix}log_notify AS ln
141
				INNER JOIN {db_prefix}boards AS b ON (b.id_board = ln.id_board)
142
				INNER JOIN {db_prefix}members AS mem ON (mem.id_member = ln.id_member)
143
			WHERE ln.id_board IN ({array_int:board_list})
144
				AND mem.notify_types != {int:notify_types}
145
				AND mem.notify_regularity < {int:notify_regularity}
146
				AND mem.is_activated = {int:is_activated}
147
				AND ln.id_member != {int:current_member}' .
148
				(empty($members_only) ? '' : ' AND ln.id_member IN ({array_int:members_only})') . '
149
			ORDER BY mem.lngfile',
150
			array(
151
				'current_member' => $user_id,
152
				'board_list' => $boards_index,
153
				'notify_types' => $type === 'reply' ? 4 : 3,
154
				'notify_regularity' => 2,
155
				'is_activated' => 1,
156
				'members_only' => is_array($members_only) ? $members_only : array($members_only),
157
			)
158
		);
159
		$boards = array();
160
		while ($row = $db->fetch_assoc($members))
161
		{
162
			// If they are not the poster do they want to know?
163
			// @todo maybe if they posted via email?
164
			if ($type !== 'reply' && $row['notify_types'] == 2)
165
				continue;
166
167
			// for this member/board, loop through the topics and see if we should send it
168
			foreach ($topicData as $id => $data)
169
			{
170
				// Don't send it if its not from the right board
171
				if ($data['board'] !== $row['id_board'])
172
					continue;
173
				else
174
					$data['board_name'] = $row['name'];
175
176
				// Don't do the excluded...
177
				if ($data['exclude'] === $row['id_member'])
178
					continue;
179
180
				$email_perm = true;
181
				if (validateNotificationAccess($row, $maillist, $email_perm) === false)
182
					continue;
183
184
				$needed_language = empty($row['lngfile']) || empty($modSettings['userLanguage']) ? $language : $row['lngfile'];
185
				if (empty($current_language) || $current_language != $needed_language)
186
					$current_language = loadLanguage('Post', $needed_language, false);
187
188
				$message_type = 'notification_' . $type;
189
				$replacements = array(
190
					'TOPICSUBJECT' => $data['subject'],
191
					'POSTERNAME' => un_htmlspecialchars($data['name']),
192
					'TOPICLINKNEW' => $scripturl . '?topic=' . $id . '.new;topicseen#new',
193
					'TOPICLINK' => $scripturl . '?topic=' . $id . '.msg' . $data['last_id'] . '#msg' . $data['last_id'],
194
					'UNSUBSCRIBELINK' => replaceBasicActionUrl('{script_url}?action=notify;sa=unsubscribe;token=' .
195
						getNotifierToken($row['id_member'], $row['email_address'], $row['password_salt'], 'board', $data['board'])),
196
					'SIGNATURE' => $data['signature'],
197
					'BOARDNAME' => $data['board_name'],
198
					'SUBSCRIPTION' => $txt['board'],
199
				);
200
201
				if ($type === 'remove')
202
					unset($replacements['TOPICLINK'], $replacements['UNSUBSCRIBELINK']);
203
204
				// Do they want the body of the message sent too?
205
				if (!empty($row['notify_send_body']) && $type === 'reply')
206
				{
207
					$message_type .= '_body';
208
					$replacements['MESSAGE'] = $data['body'];
209
210
					// Any attachments? if so lets make a big deal about them!
211
					if ($data['attachments'] != 0)
212
						$replacements['MESSAGE'] .= "\n\n" . sprintf($txt['message_attachments'], $data['attachments'], $replacements['TOPICLINK']);
213
				}
214
215
				if (!empty($row['notify_regularity']) && $type === 'reply')
216
					$message_type .= '_once';
217
218
				// Give them a way to add in their own replacements
219
				call_integration_hook('integrate_notification_replacements', array(&$replacements, $row, $type, $current_language));
220
221
				// Send only if once is off or it's on and it hasn't been sent.
222
				if ($type !== 'reply' || empty($row['notify_regularity']) || empty($row['sent']))
223
				{
224
					$emaildata = loadEmailTemplate((($maillist && $email_perm && $type === 'reply' && !empty($row['notify_send_body'])) ? 'pbe_' : '') . $message_type, $replacements, $needed_language);
225
226
					// If using the maillist functions, we adjust who this is coming from
227
					if ($maillist && $email_perm && $type === 'reply' && !empty($row['notify_send_body']))
228
					{
229
						// In group mode like google group or yahoo group, the mail is from the poster
230
						// Otherwise in maillist mode, it is from the site
231
						$emailfrom = !empty($modSettings['maillist_group_mode']) ? un_htmlspecialchars($data['name']) : (!empty($modSettings['maillist_sitename']) ? un_htmlspecialchars($modSettings['maillist_sitename']) : $mbname);
232
233
						// The email address of the sender, irrespective of the envelope name above
234
						$from_wrapper = !empty($modSettings['maillist_mail_from']) ? $modSettings['maillist_mail_from'] : (empty($modSettings['maillist_sitename_address']) ? $webmaster_email : $modSettings['maillist_sitename_address']);
235
						sendmail($row['email_address'], $emaildata['subject'], $emaildata['body'], $emailfrom, 'm' . $data['last_id'], false, 3, null, false, $from_wrapper, $id);
236
					}
237
					else
238
						sendmail($row['email_address'], $emaildata['subject'], $emaildata['body'], null, 'm' . $data['last_id']);
239
240
					$sent++;
241
242
					// Make a note that this member was sent this topic
243
					$boards[$row['id_member']][$id] = 1;
244
				}
245
			}
246
		}
247
		$db->free_result($members);
248
	}
249
250
	// Find the members with notification on for this topic.
251
	$members = $db->query('', '
252
		SELECT
253
			mem.id_member, mem.email_address, mem.notify_regularity, mem.notify_types, mem.warning,
254
			mem.notify_send_body, mem.lngfile, mem.id_group, mem.additional_groups,mem.id_post_group,
255
			t.id_member_started, b.member_groups, b.name, b.id_profile, b.id_board,
256
			ln.id_topic, ln.sent, mem.password_salt
257
		FROM {db_prefix}log_notify AS ln
258
			INNER JOIN {db_prefix}members AS mem ON (mem.id_member = ln.id_member)
259
			INNER JOIN {db_prefix}topics AS t ON (t.id_topic = ln.id_topic)
260
			INNER JOIN {db_prefix}boards AS b ON (b.id_board = t.id_board)
261
		WHERE ln.id_topic IN ({array_int:topic_list})
262
			AND mem.notify_types < {int:notify_types}
263
			AND mem.notify_regularity < {int:notify_regularity}
264
			AND mem.is_activated = {int:is_activated}
265
			AND ln.id_member != {int:current_member}' .
266
			(empty($members_only) ? '' : ' AND ln.id_member IN ({array_int:members_only})') . '
267
		ORDER BY mem.lngfile',
268
		array(
269
			'current_member' => $user_id,
270
			'topic_list' => $topics,
271
			'notify_types' => $type == 'reply' ? 4 : 3,
272
			'notify_regularity' => 2,
273
			'is_activated' => 1,
274
			'members_only' => is_array($members_only) ? $members_only : array($members_only),
275
		)
276
	);
277
	while ($row = $db->fetch_assoc($members))
278
	{
279
		// Don't do the excluded...
280
		if ($topicData[$row['id_topic']]['exclude'] == $row['id_member'])
281
			continue;
282
283
		// Don't do the ones that were sent via board notification, you only get one notice
284
		if (isset($boards[$row['id_member']][$row['id_topic']]))
285
			continue;
286
287
		// Easier to check this here... if they aren't the topic poster do they really want to know?
288
		// @todo perhaps just if they posted by email?
289
		if ($type != 'reply' && $row['notify_types'] == 2 && $row['id_member'] != $row['id_member_started'])
290
			continue;
291
292
		$email_perm = true;
293
		if (validateNotificationAccess($row, $maillist, $email_perm) === false)
294
			continue;
295
296
		$needed_language = empty($row['lngfile']) || empty($modSettings['userLanguage']) ? $language : $row['lngfile'];
297
		if (empty($current_language) || $current_language != $needed_language)
298
			$current_language = loadLanguage('Post', $needed_language, false);
299
300
		$message_type = 'notification_' . $type;
301
		$replacements = array(
302
			'TOPICSUBJECT' => $topicData[$row['id_topic']]['subject'],
303
			'POSTERNAME' => un_htmlspecialchars($topicData[$row['id_topic']]['name']),
304
			'TOPICLINKNEW' => $scripturl . '?topic=' . $row['id_topic'] . '.new;topicseen#new',
305
			'TOPICLINK' => $scripturl . '?topic=' . $row['id_topic'] . '.msg' . $data['last_id'] . '#msg' . $data['last_id'],
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $data does not seem to be defined for all execution paths leading up to this point.
Loading history...
306
			'UNSUBSCRIBELINK' => replaceBasicActionUrl('{script_url}?action=notify;sa=unsubscribe;token=' .
307
				getNotifierToken($row['id_member'], $row['email_address'], $row['password_salt'], 'topic', $row['id_topic'])),
308
			'SIGNATURE' => $topicData[$row['id_topic']]['signature'],
309
			'BOARDNAME' => $row['name'],
310
			'SUBSCRIPTION' => $txt['topic'],
311
		);
312
313
		if ($type == 'remove')
314
			unset($replacements['TOPICLINK'], $replacements['UNSUBSCRIBELINK']);
315
316
		// Do they want the body of the message sent too?
317
		if (!empty($row['notify_send_body']) && $type == 'reply')
318
		{
319
			$message_type .= '_body';
320
			$replacements['MESSAGE'] = $topicData[$row['id_topic']]['body'];
321
		}
322
		if (!empty($row['notify_regularity']) && $type == 'reply')
323
			$message_type .= '_once';
324
325
		// Send only if once is off or it's on and it hasn't been sent.
326
		if ($type != 'reply' || empty($row['notify_regularity']) || empty($row['sent']))
327
		{
328
			$emaildata = loadEmailTemplate((($maillist && $email_perm && $type === 'reply' && !empty($row['notify_send_body'])) ? 'pbe_' : '') . $message_type, $replacements, $needed_language);
329
330
			// Using the maillist functions? Then adjust the from wrapper
331
			if ($maillist && $email_perm && $type === 'reply' && !empty($row['notify_send_body']))
332
			{
333
				// Set the from name base on group or maillist mode
334
				$emailfrom = !empty($modSettings['maillist_group_mode']) ? un_htmlspecialchars($topicData[$row['id_topic']]['name']) : un_htmlspecialchars($modSettings['maillist_sitename']);
335
				$from_wrapper = !empty($modSettings['maillist_mail_from']) ? $modSettings['maillist_mail_from'] : (empty($modSettings['maillist_sitename_address']) ? $webmaster_email : $modSettings['maillist_sitename_address']);
336
				sendmail($row['email_address'], $emaildata['subject'], $emaildata['body'], $emailfrom, 'm' . $data['last_id'], false, 3, null, false, $from_wrapper, $row['id_topic']);
337
			}
338
			else
339
				sendmail($row['email_address'], $emaildata['subject'], $emaildata['body'], null, 'm' . $topicData[$row['id_topic']]['last_id']);
340
341
			$sent++;
342
		}
343
	}
344
	$db->free_result($members);
345
346
	if (isset($current_language) && $current_language != $user_language)
347
		loadLanguage('Post');
348
349
	// Sent!
350
	if ($type == 'reply' && !empty($sent))
351
		$db->query('', '
352
			UPDATE {db_prefix}log_notify
353
			SET sent = {int:is_sent}
354
			WHERE id_topic IN ({array_int:topic_list})
355
				AND id_member != {int:current_member}',
356
			array(
357
				'current_member' => $user_id,
358
				'topic_list' => $topics,
359
				'is_sent' => 1,
360
			)
361
		);
362
363
	// For approvals we need to unsend the exclusions (This *is* the quickest way!)
364
	if (!empty($sent) && !empty($exclude))
365
	{
366
		foreach ($topicData as $id => $data)
367
		{
368
			if ($data['exclude'])
369
			{
370
				$db->query('', '
371
					UPDATE {db_prefix}log_notify
372
					SET sent = {int:not_sent}
373
					WHERE id_topic = {int:id_topic}
374
						AND id_member = {int:id_member}',
375
					array(
376
						'not_sent' => 0,
377
						'id_topic' => $id,
378
						'id_member' => $data['exclude'],
379
					)
380
				);
381
			}
382
		}
383
	}
384
}
385
386
/**
387
 * Notifies members who have requested notification for new topics posted on a board of said posts.
388
 *
389
 * receives data on the topics to send out notifications to by the passed in array.
390
 * only sends notifications to those who can *currently* see the topic (it doesn't matter if they could when they requested notification.)
391
 * loads the Post language file multiple times for each language if the userLanguage setting is set.
392
 *
393
 * @param mixed[] $topicData
394
 * @throws Elk_Exception
395
 */
396
function sendBoardNotifications(&$topicData)
397
{
398
	global $scripturl, $language, $user_info, $modSettings, $webmaster_email;
399
400
	$db = database();
401
402
	require_once(SUBSDIR . '/Mail.subs.php');
403
	require_once(SUBSDIR . '/Emailpost.subs.php');
404
405
	// Do we have one or lots of topics?
406
	if (isset($topicData['body']))
407
		$topicData = array($topicData);
408
409
	// Using the post to email functions?
410
	$maillist = !empty($modSettings['maillist_enabled']) && !empty($modSettings['pbe_post_enabled']);
411
412
	// Find out what boards we have... and clear out any rubbish!
413
	$boards = array();
414
	foreach ($topicData as $key => $topic)
415
	{
416
		if (!empty($topic['board']))
417
			$boards[$topic['board']][] = $key;
418
		else
419
		{
420
			unset($topic[$key]);
421
			continue;
422
		}
423
424
		// Convert to markdown markup e.g. styled plain text, while doing the censoring
425
		pbe_prepare_text($topicData[$key]['body'], $topicData[$key]['subject'], $topicData[$key]['signature']);
426
	}
427
428
	// Just the board numbers.
429
	$board_index = array_unique(array_keys($boards));
430
	if (empty($board_index))
431
		return;
432
433
	// Load the actual board names
434
	require_once(SUBSDIR . '/Boards.subs.php');
435
	$board_names = fetchBoardsInfo(array('boards' => $board_index, 'override_permissions' => true));
436
437
	// Yea, we need to add this to the digest queue.
438
	$digest_insert = array();
439
	foreach ($topicData as $id => $data)
440
		$digest_insert[] = array($data['topic'], $data['msg'], 'topic', $user_info['id']);
441
	$db->insert('',
442
		'{db_prefix}log_digest',
443
		array(
444
			'id_topic' => 'int', 'id_msg' => 'int', 'note_type' => 'string', 'exclude' => 'int',
445
		),
446
		$digest_insert,
447
		array()
448
	);
449
450
	// Find the members with notification on for these boards.
451
	$members = $db->query('', '
452
		SELECT
453
			mem.id_member, mem.email_address, mem.notify_regularity, mem.notify_send_body, mem.lngfile, mem.warning,
454
			ln.sent, ln.id_board, mem.id_group, mem.additional_groups, b.member_groups, b.id_profile,
455
			mem.id_post_group, mem.password_salt
456
		FROM {db_prefix}log_notify AS ln
457
			INNER JOIN {db_prefix}boards AS b ON (b.id_board = ln.id_board)
458
			INNER JOIN {db_prefix}members AS mem ON (mem.id_member = ln.id_member)
459
		WHERE ln.id_board IN ({array_int:board_list})
460
			AND mem.id_member != {int:current_member}
461
			AND mem.is_activated = {int:is_activated}
462
			AND mem.notify_types != {int:notify_types}
463
			AND mem.notify_regularity < {int:notify_regularity}
464
		ORDER BY mem.lngfile',
465
		array(
466
			'current_member' => $user_info['id'],
467
			'board_list' => $board_index,
468
			'is_activated' => 1,
469
			'notify_types' => 4,
470
			'notify_regularity' => 2,
471
		)
472
	);
473
	// While we have members with board notifications
474
	while ($rowmember = $db->fetch_assoc($members))
475
	{
476
		$email_perm = true;
477
		if (validateNotificationAccess($rowmember, $maillist, $email_perm) === false)
478
			continue;
479
480
		$langloaded = loadLanguage('index', empty($rowmember['lngfile']) || empty($modSettings['userLanguage']) ? $language : $rowmember['lngfile'], false);
481
482
		// Now loop through all the notifications to send for this board.
483
		if (empty($boards[$rowmember['id_board']]))
484
			continue;
485
486
		$sentOnceAlready = 0;
487
488
		// For each message we need to send (from this board to this member)
489
		foreach ($boards[$rowmember['id_board']] as $key)
490
		{
491
			// Don't notify the guy who started the topic!
492
			// @todo In this case actually send them a "it's approved hooray" email :P
493
			if ($topicData[$key]['poster'] == $rowmember['id_member'])
494
				continue;
495
496
			// Setup the string for adding the body to the message, if a user wants it.
497
			$send_body = $maillist || (empty($modSettings['disallow_sendBody']) && !empty($rowmember['notify_send_body']));
498
499
			$replacements = array(
500
				'TOPICSUBJECT' => $topicData[$key]['subject'],
501
				'POSTERNAME' => un_htmlspecialchars($topicData[$key]['name']),
502
				'TOPICLINK' => $scripturl . '?topic=' . $topicData[$key]['topic'] . '.new#new',
503
				'TOPICLINKNEW' => $scripturl . '?topic=' . $topicData[$key]['topic'] . '.new#new',
504
				'MESSAGE' => $send_body ? $topicData[$key]['body'] : '',
505
				'UNSUBSCRIBELINK' => replaceBasicActionUrl('{script_url}?action=notify;sa=unsubscribe;token=' .
506
					getNotifierToken($rowmember['id_member'], $rowmember['email_address'], $rowmember['password_salt'], 'board', $topicData[$key]['board'])),
507
				'SIGNATURE' => !empty($topicData[$key]['signature']) ? $topicData[$key]['signature'] : '',
508
				'BOARDNAME' => $board_names[$topicData[$key]['board']]['name'],
509
			);
510
511
			// Figure out which email to send
512
			$emailtype = '';
513
514
			// Send only if once is off or it's on and it hasn't been sent.
515
			if (!empty($rowmember['notify_regularity']) && !$sentOnceAlready && empty($rowmember['sent']))
516
				$emailtype = 'notify_boards_once';
517
			elseif (empty($rowmember['notify_regularity']))
518
				$emailtype = 'notify_boards';
519
520
			if (!empty($emailtype))
521
			{
522
				$emailtype .= $send_body ? '_body' : '';
523
				$emaildata = loadEmailTemplate((($maillist && $email_perm && $send_body) ? 'pbe_' : '') . $emailtype, $replacements, $langloaded);
524
				$emailname = (!empty($topicData[$key]['name'])) ? un_htmlspecialchars($topicData[$key]['name']) : null;
525
526
				// Maillist style?
527
				if ($maillist && $email_perm && $send_body)
528
				{
529
					// Add in the from wrapper and trigger sendmail to add in a security key
530
					$from_wrapper = !empty($modSettings['maillist_mail_from']) ? $modSettings['maillist_mail_from'] : (empty($modSettings['maillist_sitename_address']) ? $webmaster_email : $modSettings['maillist_sitename_address']);
531
					sendmail($rowmember['email_address'], $emaildata['subject'], $emaildata['body'], $emailname, 't' . $topicData[$key]['topic'], false, 3, null, false, $from_wrapper, $topicData[$key]['topic']);
532
				}
533
				else
534
					sendmail($rowmember['email_address'], $emaildata['subject'], $emaildata['body'], null, null, false, 3);
535
			}
536
537
			$sentOnceAlready = 1;
538
		}
539
	}
540
	$db->free_result($members);
541
542
	loadLanguage('index', $user_info['language']);
543
544
	// Sent!
545
	$db->query('', '
546
		UPDATE {db_prefix}log_notify
547
		SET sent = {int:is_sent}
548
		WHERE id_board IN ({array_int:board_list})
549
			AND id_member != {int:current_member}',
550
		array(
551
			'current_member' => $user_info['id'],
552
			'board_list' => $board_index,
553
			'is_sent' => 1,
554
		)
555
	);
556
}
557
558
/**
559
 * A special function for handling the hell which is sending approval notifications.
560
 *
561
 * @param mixed[] $topicData
562
 * @throws Elk_Exception
563
 */
564
function sendApprovalNotifications(&$topicData)
565
{
566
	global $scripturl, $language, $user_info, $modSettings;
567
568
	$db = database();
569
570
	// Clean up the data...
571
	if (!is_array($topicData) || empty($topicData))
0 ignored issues
show
introduced by
The condition is_array($topicData) is always true.
Loading history...
572
		return;
573
574
	// Email ahoy
575
	require_once(SUBSDIR . '/Mail.subs.php');
576
	require_once(SUBSDIR . '/Emailpost.subs.php');
577
578
	$topics = array();
579
	$digest_insert = array();
580
	foreach ($topicData as $topic => $msgs)
581
	{
582
		foreach ($msgs as $msgKey => $msg)
583
		{
584
			// Convert it to markdown for sending, censor is done as well
585
			pbe_prepare_text($topicData[$topic][$msgKey]['body'], $topicData[$topic][$msgKey]['subject']);
586
587
			$topics[] = $msg['id'];
588
			$digest_insert[] = array($msg['topic'], $msg['id'], 'reply', $user_info['id']);
589
		}
590
	}
591
592
	// These need to go into the digest too...
593
	$db->insert('',
594
		'{db_prefix}log_digest',
595
		array(
596
			'id_topic' => 'int', 'id_msg' => 'int', 'note_type' => 'string', 'exclude' => 'int',
597
		),
598
		$digest_insert,
599
		array()
600
	);
601
602
	// Find everyone who needs to know about this.
603
	$members = $db->query('', '
604
		SELECT
605
			mem.id_member, mem.email_address, mem.notify_regularity, mem.notify_types, mem.notify_send_body, mem.lngfile,
606
			ln.sent, mem.id_group, mem.additional_groups, b.member_groups, mem.id_post_group, t.id_member_started,
607
			ln.id_topic, mem.password_salt
608
		FROM {db_prefix}log_notify AS ln
609
			INNER JOIN {db_prefix}members AS mem ON (mem.id_member = ln.id_member)
610
			INNER JOIN {db_prefix}topics AS t ON (t.id_topic = ln.id_topic)
611
			INNER JOIN {db_prefix}boards AS b ON (b.id_board = t.id_board)
612
		WHERE ln.id_topic IN ({array_int:topic_list})
613
			AND mem.is_activated = {int:is_activated}
614
			AND mem.notify_types < {int:notify_types}
615
			AND mem.notify_regularity < {int:notify_regularity}
616
		GROUP BY mem.id_member, ln.id_topic, mem.email_address, mem.notify_regularity, mem.notify_types, mem.notify_send_body, mem.lngfile, ln.sent, mem.id_group, mem.additional_groups, b.member_groups, mem.id_post_group, t.id_member_started
617
		ORDER BY mem.lngfile',
618
		array(
619
			'topic_list' => $topics,
620
			'is_activated' => 1,
621
			'notify_types' => 4,
622
			'notify_regularity' => 2,
623
		)
624
	);
625
	$sent = 0;
626
	$current_language = $user_info['language'];
627
	while ($row = $db->fetch_assoc($members))
628
	{
629
		if ($row['id_group'] != 1)
630
		{
631
			$allowed = explode(',', $row['member_groups']);
632
			$row['additional_groups'] = explode(',', $row['additional_groups']);
633
			$row['additional_groups'][] = $row['id_group'];
634
			$row['additional_groups'][] = $row['id_post_group'];
635
636
			if (count(array_intersect($allowed, $row['additional_groups'])) == 0)
637
				continue;
638
		}
639
640
		$needed_language = empty($row['lngfile']) || empty($modSettings['userLanguage']) ? $language : $row['lngfile'];
641
		if (empty($current_language) || $current_language != $needed_language)
642
			$current_language = loadLanguage('Post', $needed_language, false);
643
644
		$sent_this_time = false;
645
		$replacements = array(
646
			'TOPICLINK' => $scripturl . '?topic=' . $row['id_topic'] . '.new;topicseen#new',
647
			'UNSUBSCRIBELINK' => replaceBasicActionUrl('{script_url}?action=notify;sa=unsubscribe;token=' .
648
				getNotifierToken($row['id_member'], $row['email_address'], $row['password_salt'], 'topic', $row['id_topic'])),
649
		);
650
651
		// Now loop through all the messages to send.
652
		foreach ($topicData[$row['id_topic']] as $msg)
653
		{
654
			$replacements += array(
655
				'TOPICSUBJECT' => $msg['subject'],
656
				'POSTERNAME' => un_htmlspecialchars($msg['name']),
657
			);
658
659
			$message_type = 'notification_reply';
660
661
			// Do they want the body of the message sent too?
662
			if (!empty($row['notify_send_body']) && empty($modSettings['disallow_sendBody']))
663
			{
664
				$message_type .= '_body';
665
				$replacements['MESSAGE'] = $msg['body'];
666
			}
667
668
			if (!empty($row['notify_regularity']))
669
				$message_type .= '_once';
670
671
			// Send only if once is off or it's on and it hasn't been sent.
672
			if (empty($row['notify_regularity']) || (empty($row['sent']) && !$sent_this_time))
673
			{
674
				$emaildata = loadEmailTemplate($message_type, $replacements, $needed_language);
675
				sendmail($row['email_address'], $emaildata['subject'], $emaildata['body'], null, 'm' . $msg['last_id']);
676
				$sent++;
677
			}
678
679
			$sent_this_time = true;
680
		}
681
	}
682
	$db->free_result($members);
683
684
	if (isset($current_language) && $current_language != $user_info['language'])
685
		loadLanguage('Post');
686
687
	// Sent!
688
	if (!empty($sent))
689
		$db->query('', '
690
			UPDATE {db_prefix}log_notify
691
			SET sent = {int:is_sent}
692
			WHERE id_topic IN ({array_int:topic_list})
693
				AND id_member != {int:current_member}',
694
			array(
695
				'current_member' => $user_info['id'],
696
				'topic_list' => $topics,
697
				'is_sent' => 1,
698
			)
699
		);
700
}
701
702
/**
703
 * This simple function gets a list of all administrators and sends them an email
704
 * to let them know a new member has joined.
705
 * Called by registerMember() function in subs/Members.subs.php.
706
 * Email is sent to all groups that have the moderate_forum permission.
707
 * The language set by each member is being used (if available).
708
 *
709
 * @param string $type types supported are 'approval', 'activation', and 'standard'.
710
 * @param int $memberID
711
 * @param string|null $member_name = null
712
 * @uses the Login language file.
713
 * @throws Elk_Exception
714
 */
715
function sendAdminNotifications($type, $memberID, $member_name = null)
716
{
717
	global $modSettings, $language, $scripturl, $user_info;
718
719
	$db = database();
720
721
	// If the setting isn't enabled then just exit.
722
	if (empty($modSettings['notify_new_registration']))
723
		return;
724
725
	// Needed to notify admins, or anyone
726
	require_once(SUBSDIR . '/Mail.subs.php');
727
728
	if ($member_name === null)
729
	{
730
		require_once(SUBSDIR . '/Members.subs.php');
731
732
		// Get the new user's name....
733
		$member_info = getBasicMemberData($memberID);
734
		$member_name = $member_info['real_name'];
735
	}
736
737
	$groups = array();
738
739
	// All membergroups who can approve members.
740
	$request = $db->query('', '
741
		SELECT id_group
742
		FROM {db_prefix}permissions
743
		WHERE permission = {string:moderate_forum}
744
			AND add_deny = {int:add_deny}
745
			AND id_group != {int:id_group}',
746
		array(
747
			'add_deny' => 1,
748
			'id_group' => 0,
749
			'moderate_forum' => 'moderate_forum',
750
		)
751
	);
752
	while ($row = $db->fetch_assoc($request))
753
		$groups[] = $row['id_group'];
754
	$db->free_result($request);
755
756
	// Add administrators too...
757
	$groups[] = 1;
758
	$groups = array_unique($groups);
759
760
	// Get a list of all members who have ability to approve accounts - these are the people who we inform.
761
	$request = $db->query('', '
762
		SELECT id_member, lngfile, email_address
763
		FROM {db_prefix}members
764
		WHERE (id_group IN ({array_int:group_list}) OR FIND_IN_SET({raw:group_array_implode}, additional_groups) != 0)
765
			AND notify_types != {int:notify_types}
766
		ORDER BY lngfile',
767
		array(
768
			'group_list' => $groups,
769
			'notify_types' => 4,
770
			'group_array_implode' => implode(', additional_groups) != 0 OR FIND_IN_SET(', $groups),
771
		)
772
	);
773
774
	$current_language = $user_info['language'];
775
	while ($row = $db->fetch_assoc($request))
776
	{
777
		$replacements = array(
778
			'USERNAME' => $member_name,
779
			'PROFILELINK' => $scripturl . '?action=profile;u=' . $memberID
780
		);
781
		$emailtype = 'admin_notify';
782
783
		// If they need to be approved add more info...
784
		if ($type == 'approval')
785
		{
786
			$replacements['APPROVALLINK'] = $scripturl . '?action=admin;area=viewmembers;sa=browse;type=approve';
787
			$emailtype .= '_approval';
788
		}
789
790
		$emaildata = loadEmailTemplate($emailtype, $replacements, empty($row['lngfile']) || empty($modSettings['userLanguage']) ? $language : $row['lngfile']);
791
792
		// And do the actual sending...
793
		sendmail($row['email_address'], $emaildata['subject'], $emaildata['body'], null, null, false, 0);
794
	}
795
	$db->free_result($request);
796
797
	if (isset($current_language) && $current_language != $user_info['language'])
798
		loadLanguage('Login');
799
}
800
801
/**
802
 * Checks if a user has the correct access to get notifications
803
 * - validates they have proper group access to a board
804
 * - if using the maillist, checks if they should get a reply-able message
805
 *     - not muted
806
 *     - has postby_email permission on the board
807
 *
808
 * Returns false if they do not have the proper group access to a board
809
 * Sets email_perm to false if they should not get a reply-able message
810
 *
811
 * @param mixed[] $row
812
 * @param boolean $maillist
813
 * @param boolean $email_perm
814
 * @throws Elk_Exception
815
 */
816
function validateNotificationAccess($row, $maillist, &$email_perm = true)
817
{
818
	global $modSettings;
819
820
	static $board_profile = array();
821
822
	$allowed = explode(',', $row['member_groups']);
823
	$row['additional_groups'] = !empty($row['additional_groups']) ? explode(',', $row['additional_groups']) : array();
824
	$row['additional_groups'][] = $row['id_group'];
825
	$row['additional_groups'][] = $row['id_post_group'];
826
827
	// No need to check for you ;)
828
	if ($row['id_group'] == 1 || in_array('1', $row['additional_groups']))
829
		return $email_perm;
830
831
	// They do have access to this board?
832
	if (count(array_intersect($allowed, $row['additional_groups'])) === 0)
833
		return false;
834
835
	// If using maillist, see if they should get a reply-able message
836
	if ($maillist)
837
	{
838
		// Perhaps they don't require a security key in the message
839
		if (!empty($modSettings['postmod_active']) && !empty($modSettings['warning_mute']) && $modSettings['warning_mute'] <= $row['warning'])
840
			$email_perm = false;
841
		else
842
		{
843
			if (!isset($board_profile[$row['id_board']]))
844
			{
845
				require_once(SUBSDIR . '/Members.subs.php');
846
				$board_profile[$row['id_board']] = groupsAllowedTo('postby_email', $row['id_board']);
847
			}
848
849
			// In a group that has email posting permissions on this board
850
			if (count(array_intersect($board_profile[$row['id_board']]['allowed'], $row['additional_groups'])) === 0)
851
				$email_perm = false;
852
853
			// And not specifically denied?
854
			if ($email_perm && !empty($modSettings['permission_enable_deny'])
855
				&& count(array_intersect($row['additional_groups'], $board_profile[$row['id_board']]['denied'])) !== 0)
856
				$email_perm = false;
857
		}
858
	}
859
860
	return $email_perm;
861
}
862
863
/**
864
 * Queries the database for notification preferences of a set of members.
865
 *
866
 * @param string[]|string $notification_types
867
 * @param int[]|int $members
868
 */
869
function getUsersNotificationsPreferences($notification_types, $members)
870
{
871
	$db = database();
872
873
	$notification_types = (array) $notification_types;
874
	$query_members = (array) $members;
875
	$query_members[] = 0;
876
877
	$request = $db->query('', '
878
		SELECT id_member, notification_level, mention_type
879
		FROM {db_prefix}notifications_pref
880
		WHERE id_member IN ({array_int:members_to})
881
			AND mention_type IN ({array_string:mention_types})',
882
		array(
883
			'members_to' => $query_members,
884
			'mention_types' => $notification_types,
885
		)
886
	);
887
	$results = array();
888
889
	while ($row = $db->fetch_assoc($request))
890
	{
891
		if (!isset($results[$row['id_member']]))
892
			$results[$row['id_member']] = array();
893
894
		$results[$row['id_member']][$row['mention_type']] = (int) $row['notification_level'];
895
	}
896
897
	$db->free_result($request);
898
899
	$defaults = array();
900
	foreach ($notification_types as $val)
901
	{
902
		$defaults[$val] = 0;
903
	}
904
	if (isset($results[0]))
905
	{
906
		$defaults = array_merge($defaults, $results[0]);
907
	}
908
909
	$preferences = array();
910
	foreach ((array) $members as $member)
911
	{
912
		$preferences[$member] = $defaults;
913
		if (isset($results[$member]))
914
		$preferences[$member] = array_merge($preferences[$member], $results[$member]);
915
	}
916
917
	return $preferences;
918
}
919
920
/**
921
 * Saves into the database the notification preferences of a certain member.
922
 *
923
 * @param int $member The member id
924
 * @param int[] $notification_data The array of notifications ('type' => 'level')
925
 */
926
function saveUserNotificationsPreferences($member, $notification_data)
927
{
928
	$db = database();
929
930
	// First drop the existing settings
931
	$db->query('', '
932
		DELETE FROM {db_prefix}notifications_pref
933
		WHERE id_member = {int:member}
934
			AND mention_type IN ({array_string:mention_types})',
935
		array(
936
			'member' => $member,
937
			'mention_types' => array_keys($notification_data),
938
		)
939
	);
940
941
	$inserts = array();
942
	foreach ($notification_data as $type => $level)
943
	{
944
		$inserts[] = array(
945
			$member,
946
			$type,
947
			$level,
948
		);
949
	}
950
951
	if (empty($inserts))
952
		return;
953
954
	$db->insert('',
955
		'{db_prefix}notifications_pref',
956
		array(
957
			'id_member' => 'int',
958
			'mention_type' => 'string-12',
959
			'notification_level' => 'int',
960
		),
961
		$inserts,
962
		array('id_member', 'mention_type')
963
	);
964
}
965
966
/**
967
 * From the list of all possible notification methods available, only those
968
 * enabled are returned.
969
 *
970
 * @param string[] $possible_methods The array of notifications ('type' => 'level')
971
 * @param string $type The type of notification (mentionmem, likemsg, etc.)
972
 */
973
function filterNotificationMethods($possible_methods, $type)
974
{
975
	$unserialized = getConfiguredNotificationMethods($type);
976
977
	if (empty($unserialized))
978
		return array();
979
980
	$allowed = array();
981
	foreach ($possible_methods as $key => $val)
982
	{
983
		if (isset($unserialized[$val]))
984
			$allowed[$key] = $val;
985
	}
986
987
	return $allowed;
988
}
989
990
/**
991
 * Returns all the enabled methods of notification for a specific
992
 * type of notification.
993
 *
994
 * @param string $type The type of notification (mentionmem, likemsg, etc.)
995
 */
996
function getConfiguredNotificationMethods($type)
997
{
998
	global $modSettings;
999
	static $unserialized = null;
1000
1001
	if ($unserialized === null)
1002
		$unserialized = unserialize($modSettings['notification_methods']);
1003
1004
	if (isset($unserialized[$type]))
1005
	{
1006
		return $unserialized[$type];
1007
	}
1008
	else
1009
	{
1010
		return array();
1011
	}
1012
}
1013
1014
/**
1015
 * Creates a hash code using the notification details and our secret key
1016
 *
1017
 * - If no salt (secret key) has been set, creates a random one and saves it
1018
 * in modSettings for future use
1019
 *
1020
 * @param string $memID member id
1021
 * @param string $memEmail member email address
1022
 * @param string $memSalt member salt
1023
 * @param string $area area to unsubscribe
1024
 * @param string $extra area specific data such as topic id or liked msg
1025
 * @return string the token for the unsubscribe link
1026
 */
1027
function getNotifierToken($memID, $memEmail, $memSalt, $area, $extra)
1028
{
1029
	global $modSettings;
1030
1031
	// Generate a 22 digit random code suitable for Blowfish crypt.
1032
	$tokenizer = new \Token_Hash();
1033
	$blowfish_salt = '$2a$10$' . $tokenizer->generate_hash(22);
1034
	$now = time();
1035
1036
	// Ideally we would just have a larger -per user- password_salt
1037
	if (empty($modSettings['unsubscribe_site_salt']))
1038
	{
1039
		// extra 10 digits of salt
1040
		$modSettings['unsubscribe_site_salt'] = $tokenizer->generate_hash();
1041
		updateSettings(array('unsubscribe_site_salt' => $modSettings['unsubscribe_site_salt']));
1042
	}
1043
1044
	// Add member salt + site salt to the otherwise deterministic data
1045
	$hash = crypt($area . $extra . $now . $memEmail . $memSalt . $modSettings['unsubscribe_site_salt'], $blowfish_salt);
1046
1047
	return urlencode(implode('_',
1048
		array(
1049
			$memID,
1050
			substr($hash, 7),
1051
			$area,
1052
			$extra,
1053
			$now
1054
		)
1055
	));
1056
}
1057
1058
/**
1059
 * Creates a hash code using the notification details and our secret key
1060
 *
1061
 * - If no site salt (secret key) has been set, simply fails
1062
 *
1063
 * @param string $memEmail member email address
1064
 * @param string $memSalt member salt
1065
 * @param string $area area to unsubscribe
1066
 * @param string $hash the sent hash
1067
 * @return bool
1068
 */
1069
function validateNotifierToken($memEmail, $memSalt, $area, $hash)
1070
{
1071
	global $modSettings;
1072
1073
	if (empty($modSettings['unsubscribe_site_salt']))
1074
	{
1075
		return false;
1076
	}
1077
1078
	$expected = '$2a$10$' . $hash;
1079
	$check = crypt($area . $memEmail . $memSalt . $modSettings['unsubscribe_site_salt'], $expected);
1080
1081
	// Basic safe compare as hash_equals is PHP 5.6+
1082
	if (function_exists('hash_equals'))
1083
	{
1084
		return hash_equals($expected, $check);
1085
	}
1086
1087
	$ret = strlen($expected) ^ strlen($check);
1088
	$ret |= array_sum(unpack("C*", $expected ^ $check));
0 ignored issues
show
Bug introduced by
It seems like unpack('C*', $expected ^ $check) can also be of type false; however, parameter $array of array_sum() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1088
	$ret |= array_sum(/** @scrutinizer ignore-type */ unpack("C*", $expected ^ $check));
Loading history...
1089
1090
	return !$ret;
1091
}
1092