Issues (1686)

sources/ElkArte/Controller/Notify.php (10 issues)

1
<?php
2
3
/**
4
 * This file contains just the functions that turn on and off notifications
5
 * to topics or boards.
6
 *
7
 * @package   ElkArte Forum
8
 * @copyright ElkArte Forum contributors
9
 * @license   BSD http://opensource.org/licenses/BSD-3-Clause (see accompanying LICENSE.txt file)
10
 *
11
 * This file contains code covered by:
12
 * copyright: 2011 Simple Machines (http://www.simplemachines.org)
13
 *
14
 * @version 2.0 dev
15
 *
16
 */
17
18
namespace ElkArte\Controller;
19
20
use ElkArte\AbstractController;
21
use ElkArte\Action;
22
use ElkArte\Exceptions\Exception;
23
use ElkArte\Languages\Txt;
24
25
/**
26
 * Functions that turn on and off various member notifications
27
 */
28
class Notify extends AbstractController
29
{
30
	/**
31
	 * Pre Dispatch, called before other methods, used to load common needs.
32
	 */
33
	public function pre_dispatch()
34
	{
35
		// Our topic functions are here
36
		require_once(SUBSDIR . '/Topic.subs.php');
37
		require_once(SUBSDIR . '/Boards.subs.php');
38
	}
39
40
	/**
41
	 * Dispatch to the right action.
42
	 *
43
	 * @see AbstractController::action_index
44
	 */
45
	public function action_index()
46
	{
47
		// The number of choices is boggling, ok there are just 2
48
		$subActions = array(
49
			'notify' => array($this, 'action_notify'),
50
			'unsubscribe' => array($this, 'action_unsubscribe'),
51
		);
52
53
		// We like action, so lets get ready for some
54
		$action = new Action('notify');
55
56
		// Get the subAction, or just go to action_notify
57
		$subAction = $action->initialize($subActions, 'notify');
58
59
		// forward to our respective method.
60
		$action->dispatch($subAction);
61
	}
62
63
	/**
64
	 * Turn off/on notification for a particular topic.
65
	 *
66
	 * What it does:
67
	 *
68
	 * - Must be called with a topic specified in the URL.
69
	 * - The sub-action can be 'on', 'off', or nothing for what to do.
70
	 * - Requires the mark_any_notify permission.
71
	 * - Upon successful completion of action will direct user back to topic.
72
	 * - Accessed via ?action=notify.
73
	 *
74
	 * @uses Notify.template, main sub-template
75
	 */
76
	public function action_notify()
77
	{
78
		global $topic, $txt, $context;
79
80
		// Make sure they aren't a guest or something - guests can't really receive notifications!
81
		is_not_guest();
82
		isAllowedTo('mark_any_notify');
83
84
		// Api ajax call?
85
		if ($this->getApi() === 'xml')
86
		{
87
			$this->action_notify_api();
88
			return true;
89
		}
90
91
		// Make sure the topic has been specified.
92
		if (empty($topic))
93
		{
94
			throw new Exception('not_a_topic', false);
95
		}
96
97
		// What do we do?  Better ask if they didn't say..
98
		if (empty($this->_req->query->sa))
99
		{
100
			// Load the template, but only if it is needed.
101
			theme()->getTemplates()->load('Notify');
102
103
			// Find out if they have notification set for this topic already.
104
			$context['notification_set'] = hasTopicNotification($this->user->id, $topic);
0 ignored issues
show
Bug Best Practice introduced by
The property id does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
105
106
			// Set the template variables...
107
			$context['topic_href'] = getUrl('action', ['topic' => $topic . '.' . $this->_req->query->start]);
108
			$context['start'] = $this->_req->query->start;
109
			$context['page_title'] = $txt['notifications'];
110
			$context['sub_template'] = 'notification_settings';
111
112
			return true;
113
		}
114
115
		checkSession('get');
116
		$this->_toggle_topic_notification();
117
118
		// Send them back to the topic.
119
		redirectexit('topic=' . $topic . '.' . $this->_req->query->start);
120
121
		return true;
122
	}
123
124
	/**
125
	 * Toggle a topic notification on/off
126
	 */
127
	private function _toggle_topic_notification($memID = null)
128
	{
129
		global $topic;
130
131
		// Attempt to turn notifications on/off.
132
		setTopicNotification($memID ?? $this->user->id, $topic, $this->_req->query->sa === 'on');
0 ignored issues
show
Bug Best Practice introduced by
The property id does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
133
	}
134
135
	/**
136
	 * Turn off/on notifications for a particular topic
137
	 *
138
	 * - Intended for use in XML or JSON calls
139
	 */
140
	public function action_notify_api()
141
	{
142
		global $topic, $txt, $context;
143
144
		theme()->getTemplates()->load('Xml');
145
146
		theme()->getLayers()->removeAll();
147
		$context['sub_template'] = 'generic_xml_buttons';
148
149
		// Even with Ajax, guests still can't do this
150
		if ($this->user->is_guest)
0 ignored issues
show
Bug Best Practice introduced by
The property is_guest does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
151
		{
152
			Txt::load('Errors');
153
			$context['xml_data'] = array(
154
				'error' => 1,
155
				'text' => $txt['not_guests']
156
			);
157
158
			return;
159
		}
160
161
		// And members still need the right permissions
162
		if (!allowedTo('mark_any_notify') || empty($topic) || empty($this->_req->query->sa))
163
		{
164
			Txt::load('Errors');
165
			$context['xml_data'] = array(
166
				'error' => 1,
167
				'text' => $txt['cannot_mark_any_notify']
168
			);
169
170
			return;
171
		}
172
173
		// And sessions still matter, so you better have a valid one
174
		if (checkSession('get', '', false))
175
		{
176
			Txt::load('Errors');
177
			$context['xml_data'] = array(
178
				'error' => 1,
179
				'url' => getUrl('action', ['action' => 'notify', 'sa' => ($this->_req->query->sa === 'on' ? 'on' : 'off'), 'topic' => $topic . '.' . $this->_req->query->start, '{session_data}']),
180
			);
181
182
			return;
183
		}
184
185
		$this->_toggle_topic_notification();
186
187
		// Return the results so the UI can be updated properly
188
		$context['xml_data'] = array(
189
			'text' => $this->_req->query->sa === 'on' ? $txt['unnotify'] : $txt['notify'],
190
			'url' => getUrl('action', ['action' => 'notify', 'sa' => ($this->_req->query->sa === 'on' ? 'off' : 'on'), 'topic' => $topic . '.' . $this->_req->query->start, '{session_data}', 'api' => '1']),
191
			'confirm' => $this->_req->query->sa === 'on' ? $txt['notification_disable_topic'] : $txt['notification_enable_topic']
192
		);
193
	}
194
195
	/**
196
	 * Turn off/on notification for a particular board.
197
	 *
198
	 * What it does:
199
	 *
200
	 * - Must be called with a board specified in the URL.
201
	 * - Only uses the template if no sub action is used. (on/off)
202
	 * - Requires the mark_notify permission.
203
	 * - Redirects the user back to the board after it is done.
204
	 * - Accessed via ?action=notifyboard.
205
	 *
206
	 * @uses template_notify_board() sub-template in Notify.template
207
	 */
208
	public function action_notifyboard()
209
	{
210
		global $txt, $board, $context;
211
212
		// Api ajax call?
213
		if ($this->getApi() === 'xml')
214
		{
215
			$this->action_notifyboard_api();
216
			return true;
217
		}
218
219
		// Permissions are an important part of anything ;).
220
		is_not_guest();
221
		isAllowedTo('mark_notify');
222
223
		// You have to specify a board to turn notifications on!
224
		if (empty($board))
225
		{
226
			throw new Exception('no_board', false);
227
		}
228
229
		// No subaction: find out what to do.
230
		if (empty($this->_req->query->sa))
231
		{
232
			// We're gonna need the notify template...
233
			theme()->getTemplates()->load('Notify');
234
235
			// Find out if they have notification set for this board already.
236
			$context['notification_set'] = hasBoardNotification($this->user->id, $board);
0 ignored issues
show
Bug Best Practice introduced by
The property id does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
237
238
			// Set the template variables...
239
			$context['board_href'] = getUrl('action', ['board' => $board . '.' . $this->_req->query->start]);
240
			$context['start'] = $this->_req->query->start;
241
			$context['page_title'] = $txt['notifications'];
242
			$context['sub_template'] = 'notify_board';
243
244
			return;
245
		}
246
247
		checkSession('get');
248
249
		// Turn notification on/off for this board.
250
		$this->_toggle_board_notification();
251
252
		// Back to the board!
253
		redirectexit('board=' . $board . '.' . $this->_req->query->start);
254
	}
255
256
	/**
257
	 * Toggle a board notification on/off
258
	 */
259
	private function _toggle_board_notification($memID = null)
260
	{
261
		global $board;
262
263
		// Our board functions are here
264
		require_once(SUBSDIR . '/Boards.subs.php');
265
266
		// Turn notification on/off for this board.
267
		setBoardNotification($memID ?? $this->user->id, $board, $this->_req->query->sa === 'on');
0 ignored issues
show
Bug Best Practice introduced by
The property id does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
268
	}
269
270
	/**
271
	 * Turn off/on notification for a particular board.
272
	 *
273
	 * - Intended for use in XML or JSON calls
274
	 * - Performs the same actions as action_notifyboard but provides ajax responses
275
	 */
276
	public function action_notifyboard_api()
277
	{
278
		global $txt, $board, $context;
279
280
		theme()->getTemplates()->load('Xml');
281
282
		theme()->getLayers()->removeAll();
283
		$context['sub_template'] = 'generic_xml_buttons';
284
285
		// Permissions are an important part of anything ;).
286
		if ($this->user->is_guest)
0 ignored issues
show
Bug Best Practice introduced by
The property is_guest does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
287
		{
288
			Txt::load('Errors');
289
			$context['xml_data'] = array(
290
				'error' => 1,
291
				'text' => $txt['not_guests']
292
			);
293
294
			return;
295
		}
296
297
		// Have to have provided the right information
298
		if (!allowedTo('mark_notify') || empty($board) || empty($this->_req->query->sa))
299
		{
300
			Txt::load('Errors');
301
			$context['xml_data'] = array(
302
				'error' => 1,
303
				'text' => $txt['cannot_mark_notify'],
304
			);
305
306
			return;
307
		}
308
309
		// Sessions are always verified
310
		if (checkSession('get', '', false))
311
		{
312
			Txt::load('Errors');
313
			$context['xml_data'] = array(
314
				'error' => 1,
315
				'url' => getUrl('action', ['action' => 'notifyboard', 'sa' => ($this->_req->query->sa === 'on' ? 'on' : 'off'), 'board' => $board . '.' . $this->_req->query->start, '{session_data}']),
316
			);
317
318
			return;
319
		}
320
321
		$this->_toggle_board_notification();
322
323
		$context['xml_data'] = array(
324
			'text' => $this->_req->query->sa === 'on' ? $txt['unnotify'] : $txt['notify'],
325
			'url' => getUrl('action', ['action' => 'notifyboard', 'sa' => ($this->_req->query->sa === 'on' ? 'off' : 'on'), 'board' => $board . '.' . $this->_req->query->start, '{session_data}', 'api' => '1'] + (isset($_REQUEST['json']) ? ['json'] : [])),
326
			'confirm' => $this->_req->query->sa === 'on' ? $txt['notification_disable_board'] : $txt['notification_enable_board']
327
		);
328
	}
329
330
	/**
331
	 * Turn off/on unread replies subscription for a topic
332
	 *
333
	 * What it does:
334
	 *
335
	 * - Must be called with a topic specified in the URL.
336
	 * - The sub-action can be 'on', 'off', or nothing for what to do.
337
	 * - Requires the mark_any_notify permission.
338
	 * - Upon successful completion of action will direct user back to topic.
339
	 * - Accessed via ?action=unwatchtopic.
340
	 */
341
	public function action_unwatchtopic()
342
	{
343
		global $topic, $modSettings;
344
345
		is_not_guest();
346
347
		// Let's do something only if the function is enabled
348
		if ($this->user->is_guest === false && !empty($modSettings['enable_unwatch']))
0 ignored issues
show
Bug Best Practice introduced by
The property is_guest does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
349
		{
350
			checkSession('get');
351
352
			if ($this->getApi() === 'xml')
353
			{
354
				$this->action_unwatchtopic_api();
355
				return;
356
			}
357
358
			$this->_toggle_topic_watch();
359
		}
360
361
		// Back to the topic.
362
		redirectexit('topic=' . $topic . '.' . $this->_req->query->start);
363
	}
364
365
	/**
366
	 * Toggle a watch topic on/off
367
	 */
368
	private function _toggle_topic_watch()
369
	{
370
		global $topic;
371
372
		setTopicWatch($this->user->id, $topic, $this->_req->query->sa === 'on');
0 ignored issues
show
Bug Best Practice introduced by
The property id does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
373
	}
374
375
	/**
376
	 * Turn off/on unread replies subscription for a topic
377
	 *
378
	 * - Intended for use in XML or JSON calls
379
	 */
380
	public function action_unwatchtopic_api()
381
	{
382
		global $topic, $modSettings, $txt, $context;
383
384
		theme()->getTemplates()->load('Xml');
385
386
		theme()->getLayers()->removeAll();
387
		$context['sub_template'] = 'generic_xml_buttons';
388
389
		// Sorry guests just can't do this
390
		if ($this->user->is_guest)
0 ignored issues
show
Bug Best Practice introduced by
The property is_guest does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
391
		{
392
			Txt::load('Errors');
393
			$context['xml_data'] = array(
394
				'error' => 1,
395
				'text' => $txt['not_guests']
396
			);
397
398
			return;
399
		}
400
401
		// Let's do something only if the function is enabled
402
		if (empty($modSettings['enable_unwatch']))
403
		{
404
			Txt::load('Errors');
405
			$context['xml_data'] = array(
406
				'error' => 1,
407
				'text' => $txt['feature_disabled'],
408
			);
409
410
			return;
411
		}
412
413
		// Sessions need to be validated
414
		if (checkSession('get', '', false))
415
		{
416
			Txt::load('Errors');
417
			$context['xml_data'] = array(
418
				'error' => 1,
419
				'url' => getUrl('action', ['action' => 'unwatchtopic', 'sa' => ($this->_req->query->sa === 'on' ? 'on' : 'off'), 'topic' => $topic . '.' . $this->_req->query->start, '{session_data}'])
420
			);
421
422
			return;
423
		}
424
425
		$this->_toggle_topic_watch();
426
427
		$context['xml_data'] = array(
428
			'text' => $this->_req->query->sa === 'on' ? $txt['watch'] : $txt['unwatch'],
429
			'url' => getUrl('action', ['action' => 'unwatchtopic', 'sa' => ($this->_req->query->sa === 'on' ? 'off' : 'on'), 'topic' => $context['current_topic'] . '.' . $this->_req->query->start, '{session_data}', 'api' => '1'] + (isset($_REQUEST['json']) ? ['json'] : [])),
430
		);
431
432
		setTopicWatch($this->user->id, $topic, $this->_req->query->sa === 'on');
433
	}
434
435
	/**
436
	 * Accessed via the unsubscribe link provided in site emails. This will then
437
	 * unsubscribe the user from a board or a topic (depending on the link) without them
438
	 * having to login.
439
	 */
440
	public function action_unsubscribe()
441
	{
442
		// Looks like we need to unsubscribe someone
443
		if ($this->_validateUnsubscribeToken($member, $area, $extra))
444
		{
445
			$this->_unsubscribeToggle($member, $area, $extra);
446
			$this->_prepareTemplateMessage($area, $extra, $member['email_address']);
447
448
			return true;
449
		}
450
451
		// A default msg that we did something and maybe take a chill?
452
		$this->_prepareTemplateMessage('default', '', '');
453
454
		// Not the proper message it should not happen either
455
		spamProtection('remind');
456
457
		return true;
458
	}
459
460
	/**
461
	 * Does the actual area unsubscribe toggle
462
	 *
463
	 * @param array $member Member info from getBasicMemberData
464
	 * @param string $area area they want to be removed from
465
	 * @param string $extra parameters needed for some areas
466
	 */
467
	private function _unsubscribeToggle($member, $area, $extra)
468
	{
469
		global $user_info, $board, $topic;
470
471
		$baseAreas = ['topic', 'board', 'buddy', 'likemsg', 'mentionmem', 'quotedmem', 'rlikemsg'];
472
473
		// Not a base method, so an addon will need to process this
474
		if (!in_array($area, $baseAreas))
475
		{
476
			return $this->_unsubscribeModuleToggle($member, $area, $extra);
477
		}
478
479
		// Look away while we stuff the old ballet box, power to the people!
480
		$this->_req->query->sa = 'off';
481
482
		switch ($area)
483
		{
484
			case 'topic':
485
				$topic = $extra;
486
				$this->_toggle_topic_notification($member['id_member']);
487
				break;
488
			case 'board':
489
				$board = $extra;
490
				$this->_toggle_board_notification($member['id_member']);
491
				break;
492
			case 'buddy':
493
			case 'likemsg':
494
			case 'mentionmem':
495
			case 'quotedmem':
496
			case 'rlikemsg':
497
				$this->_setUserEmailNotificationOff($member['id_member'], $area);
498
				break;
499
		}
500
501
		return true;
502
	}
503
504
	/**
505
	 * Pass unsubscribe information to the appropriate "addon" mention class/method
506
	 *
507
	 * @param array $member Member info from getBasicMemberData
508
	 * @param string $area area they want to be removed from
509
	 * @param string $extra parameters needed for some
510
	 *
511
	 * @return bool if the $unsubscribe method was called
512
	 */
513
	private function _unsubscribeModuleToggle($member, $area, $extra)
514
	{
515
		$class_name = '\\ElkArte\\Mentions\\MentionType\\Event\\' . ucwords($area);
516
517
		if (method_exists($class_name, 'unsubscribe'))
518
		{
519
			return (new $class_name)->unsubscribe($member, $area, $extra);
520
		}
521
522
		return false;
523
	}
524
525
	/**
526
	 * Validates a supplied token and extracts the needed bits
527
	 *
528
	 * What it does:
529
	 *  - Checks token conforms to a known pattern
530
	 *  - Checks token is for a known notification type
531
	 *  - Checks the age of the token
532
	 *  - Finds the member claimed in the token
533
	 *  - Runs crypt on member data to validate it matches the supplied hash
534
	 *
535
	 * @param array $member Member info from getBasicMemberData
536
	 * @param string $area area they want to be removed from
537
	 * @param string $extra parameters needed for some areas
538
	 * @return bool
539
	 */
540
	private function _validateUnsubscribeToken(&$member, &$area, &$extra)
541
	{
542
		// Token was passed and matches our expected pattern
543
		$token = $this->_req->getQuery('token', 'trim', '');
544
		$token = urldecode($token);
545
		if (empty($token) || preg_match('~^(\d+_[a-zA-Z0-9./]{32,44}_[^)]*)~m', $token, $match) !== 1)
546
		{
547
			return false;
548
		}
549
550
		// Load all notification types in the system e.g.buddy, likemsg, etc
551
		require_once(SUBSDIR . '/ManageFeatures.subs.php');
552
		$potentialAreas = [];
553
		$notification_classes = getAvailableNotifications();
554
		foreach ($notification_classes as $class)
555
		{
556
			if ($class::canUse() === false)
557
			{
558
				continue;
559
			}
560
561
			$potentialAreas[] = strtolower($class::getType());
562
		}
563
564
		$potentialAreas = array_merge($potentialAreas, ['topic', 'board']);
565
566
		// Expand the token
567
		[$id_member, $hash, $area, $extra, $time] = explode('_', $match[1]);
568
569
		// The area is a known one
570
		if (!in_array($area, $potentialAreas, true))
571
		{
572
			return false;
573
		}
574
575
		// Not so old, 2 weeks is plenty
576
		if (time() - $time > (60 * 60 * 24 * 14))
577
		{
578
			return false;
579
		}
580
581
		// Find the claimed member
582
		require_once(SUBSDIR . '/Members.subs.php');
583
		$member = getBasicMemberData((int) $id_member, array('authentication' => true));
584
		if (empty($member))
585
		{
586
			return false;
587
		}
588
589
		// Validate the token belongs to this member
590
		require_once(SUBSDIR . '/Notification.subs.php');
591
		return validateNotifierToken(
592
			$member['email_address'],
593
			$member['password_salt'],
594
			$area . $extra . $time,
595
			$hash
596
		);
597
	}
598
599
	/**
600
	 * Used to disable email notification of a specific mention area
601
	 *
602
	 * @param int $memID
603
	 * @param string $area buddy, likemsg, mentionmem, quotedmem, rlikemsg
604
	 */
605
	private function _setUserEmailNotificationOff($memID, $area)
606
	{
607
		require_once(SUBSDIR . '/Profile.subs.php');
608
		Txt::load('Profile');
609
610
		$_POST['notify_submit'] = true;
611
612
		foreach (getMemberNotificationsProfile($memID) as $mention => $data)
613
		{
614
			foreach ($data['data'] as $type => $method)
615
			{
616
				// No email notifications for you
617
				if ($mention === $area && in_array($type, ['email', 'emaildaily', 'emailweekly']))
618
				{
619
					continue;
620
				}
621
622
				// All the rest as it was
623
				if ($method['enabled'])
624
				{
625
					$_POST['notify'][$mention]['status'][] = $method['name'];
626
				}
627
			}
628
		}
629
630
		makeNotificationChanges($memID);
631
	}
632
633
	/**
634
	 * Sets an unsubscribed string for template use
635
	 *
636
	 * @param string $area
637
	 * @param string $extra
638
	 */
639
	private function _prepareTemplateMessage($area, $extra, $email)
640
	{
641
		global $txt, $context;
642
643
		switch ($area)
644
		{
645
			case 'topic':
646
				require_once(SUBSDIR . '/Topic.subs.php');
647
				try
648
				{
649
					$subject = getSubject((int) $extra);
650
					$subject = $subject ?? $txt['notify_unsubscribed_generic'];
651
					$context['unsubscribe_message'] = sprintf($txt['notify_topic_unsubscribed'], $subject, $email);
652
				}
653
				catch (Exception)
654
				{
655
					$context['unsubscribe_message'] = $txt['notify_default_unsubscribed'];
656
				}
657
658
				break;
659
			case 'board':
660
				require_once(SUBSDIR . '/Boards.subs.php');
661
				$name = boardInfo((int) $extra);
662
				$name = $name === null ? $txt['notify_unsubscribed_generic'] : $name['name'];
0 ignored issues
show
The condition $name === null is always false.
Loading history...
663
				$context['unsubscribe_message'] = sprintf($txt['notify_board_unsubscribed'], $name, $email);
664
				break;
665
			case 'buddy':
666
			case 'likemsg':
667
			case 'mentionmem':
668
			case 'quotedmem':
669
			case 'rlikemsg':
670
				Txt::load('Mentions');
671
				$context['unsubscribe_message'] = sprintf($txt['notify_mention_unsubscribed'], $txt['mentions_type_' . $area], $email);
672
				break;
673
			default:
674
				$context['unsubscribe_message'] = $txt['notify_default_unsubscribed'];
675
				break;
676
		}
677
678
		theme()->getTemplates()->load('Notify');
679
		$context['page_title'] = $txt['notifications'];
680
		$context['sub_template'] = 'notify_unsubscribe';
681
	}
682
}
683