Completed
Pull Request — patch_1-1-7 (#3482)
by Spuds
06:13
created

Notify_Controller   F

Complexity

Total Complexity 77

Size/Duplication

Total Lines 634
Duplicated Lines 0 %

Importance

Changes 3
Bugs 2 Features 0
Metric Value
eloc 238
c 3
b 2
f 0
dl 0
loc 634
rs 2.24
wmc 77

17 Methods

Rating   Name   Duplication   Size   Complexity  
A pre_dispatch() 0 5 1
A _toggle_topic_notification() 0 6 1
A action_notify() 0 47 4
A action_unsubscribe() 0 19 2
A action_unwatchtopic() 0 16 3
B action_notify_api() 0 51 10
A action_index() 0 16 1
A _toggle_topic_watch() 0 5 1
A _toggle_board_notification() 0 9 1
A action_notifyboard() 0 40 3
B action_notifyboard_api() 0 51 11
B action_unwatchtopic_api() 0 50 8
B _validateUnsubscribeToken() 0 45 6
B _unsubscribeToggle() 0 36 9
A _setUserNotificationArea() 0 34 6
A _unsubscribeModuleToggle() 0 13 2
B _prepareTemplateMessage() 0 32 8

How to fix   Complexity   

Complex Class

Complex classes like Notify_Controller 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.

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 Notify_Controller, and based on these observations, apply Extract Interface, too.

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