Passed
Pull Request — development (#3540)
by Emanuele
07:01
created

PersonalMessage::action_report()   D

Complexity

Conditions 17
Paths 154

Size

Total Lines 123
Code Lines 60

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 306

Importance

Changes 0
Metric Value
cc 17
eloc 60
nc 154
nop 0
dl 0
loc 123
ccs 0
cts 56
cp 0
crap 306
rs 4.7666
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/**
4
 * This file is mainly meant for controlling the actions related to personal
5
 * messages. It allows viewing, sending, deleting, and marking.
6
 * For compatibility reasons, they are often called "instant messages".
7
 *
8
 * @package   ElkArte Forum
9
 * @copyright ElkArte Forum contributors
10
 * @license   BSD http://opensource.org/licenses/BSD-3-Clause (see accompanying LICENSE.txt file)
11
 *
12
 * This file contains code covered by:
13
 * copyright: 2011 Simple Machines (http://www.simplemachines.org)
14
 *
15
 * @version 2.0 dev
16
 *
17
 */
18
19
namespace ElkArte\Controller;
20
21
use BBC\ParserWrapper;
22
use ElkArte\AbstractController;
23
use ElkArte\Action;
24
use ElkArte\Cache\Cache;
25
use ElkArte\Errors\ErrorContext;
26
use ElkArte\EventManager;
27
use ElkArte\Exceptions\ControllerRedirectException;
28
use ElkArte\Exceptions\Exception;
29
use ElkArte\Exceptions\PmErrorException;
30
use ElkArte\MembersList;
31
use ElkArte\MessagesCallback\BodyParser\Normal;
32
use ElkArte\MessagesCallback\PmRenderer;
33
use ElkArte\Languages\Txt;
34
use ElkArte\User;
35
use ElkArte\Util;
36
use ElkArte\ValuesContainer;
37
use ElkArte\VerificationControls\VerificationControlsIntegrate;
38
use ElkArte\Languages\Loader;
39
40
/**
41
 * Class PersonalMessage
42
 * It allows viewing, sending, deleting, and marking personal messages
43
 *
44
 * @package ElkArte\Controller
45
 */
46
class PersonalMessage extends AbstractController
47
{
48
	/**
49
	 * $_search_params will carry all settings that differ from the default
50
	 * search parameters. That way, the URLs involved in a search page will
51
	 * be kept as short as possible.
52
	 *
53
	 * @var array
54
	 */
55
	private $_search_params = array();
56
57
	/**
58
	 * $_searchq_parameters will carry all the values needed by S_search_params
59
	 *
60
	 * @var array
61
	 */
62
	private $_searchq_parameters = array();
63
64
	/**
65
	 * This method is executed before any other in this file (when the class is
66
	 * loaded by the dispatcher).
67
	 *
68
	 * What it does:
69
	 *
70
	 * - It sets the context, load templates and language file(s), as necessary
71 4
	 * for the function that will be called.
72
	 */
73 4
	public function pre_dispatch()
74
	{
75
		global $txt, $context, $modSettings;
76 4
77
		// No guests!
78
		is_not_guest();
79 4
80
		// You're not supposed to be here at all, if you can't even read PMs.
81
		isAllowedTo('pm_read');
82 4
83
		// This file contains the our PM functions such as mark, send, delete
84
		require_once(SUBSDIR . '/PersonalMessage.subs.php');
85 4
86 4
		// Templates, language, javascripts
87
		Txt::load('PersonalMessage');
88 4
		loadJavascriptFile(array('PersonalMessage.js', 'suggest.js'));
89
90
		if (!isset($this->_req->query->xml))
91
		{
92
			theme()->getTemplates()->load('PersonalMessage');
93 4
		}
94
95
		$this->_events->trigger('pre_dispatch', array('xml' => isset($this->_req->query->xml)));
96 4
97
		// Load up the members maximum message capacity.
98
		$this->_loadMessageLimit();
99 4
100
		// A previous message was sent successfully? show a small indication.
101
		if ($this->_req->getQuery('done') === 'sent')
102
		{
103
			$context['pm_sent'] = true;
104
		}
105 4
106
		// Load the label counts data.
107 4
		if (User::$settings['new_pm'] || !Cache::instance()->getVar($context['labels'], 'labelCounts:' . $this->user->id, 720))
108
		{
109
			$this->_loadLabels();
110 4
111
			// Get the message count for each label
112
			$context['labels'] = loadPMLabels($context['labels']);
113
		}
114 4
115
		// Now we have the labels, and assuming we have unsorted mail, apply our rules!
116
		if (User::$settings['new_pm'])
117
		{
118
			// Apply our rules to the new PM's
119
			applyRules();
120
121
			require_once(SUBSDIR . '/Members.subs.php');
122
			updateMemberData($this->user->id, array('new_pm' => 0));
123
124
			// Turn the new PM's status off, for the popup alert, since they have entered the PM area
125
			toggleNewPM($this->user->id);
126
		}
127 4
128
		// This determines if we have more labels than just the standard inbox.
129
		$context['currently_using_labels'] = count($context['labels']) > 1 ? 1 : 0;
130 4
131 4
		// Some stuff for the labels...
132 4
		$context['current_label_id'] = isset($this->_req->query->l) && isset($context['labels'][(int) $this->_req->query->l]) ? (int) $this->_req->query->l : -1;
133
		$context['current_label'] = &$context['labels'][(int) $context['current_label_id']]['name'];
134
		$context['folder'] = !isset($this->_req->query->f) || $this->_req->query->f !== 'sent' ? 'inbox' : 'sent';
135 4
136 4
		// This is convenient.  Do you know how annoying it is to do this every time?!
137
		$context['current_label_redirect'] = 'action=pm;f=' . $context['folder'] . (isset($this->_req->query->start) ? ';start=' . $this->_req->query->start : '') . (isset($this->_req->query->l) ? ';l=' . $this->_req->query->l : '');
138
		$context['can_issue_warning'] = featureEnabled('w') && allowedTo('issue_warning') && !empty($modSettings['warning_enable']);
139 4
140 4
		// Build the linktree for all the actions...
141 4
		$context['linktree'][] = array(
142
			'url' => getUrl('action', ['action' => 'pm']),
143
			'name' => $txt['personal_messages']
144
		);
145 4
146 4
		// Preferences...
147
		$context['display_mode'] = User::$settings['pm_prefs'] & 3;
148
	}
149
150
	/**
151 4
	 * Load a members message limit and prepares the limit bar
152
	 */
153 4
	private function _loadMessageLimit()
154
	{
155 4
		global $context, $txt;
156
157
		$context['message_limit'] = loadMessageLimit();
158 4
159
		// Prepare the context for the capacity bar.
160
		if (!empty($context['message_limit']))
161
		{
162
			$bar = ($this->user->messages * 100) / $context['message_limit'];
163
164
			$context['limit_bar'] = array(
165
				'messages' => $this->user->messages,
166
				'allowed' => $context['message_limit'],
167
				'percent' => $bar,
168
				'bar' => min(100, (int) $bar),
169
				'text' => sprintf($txt['pm_currently_using'], $this->user->messages, round($bar, 1)),
170 4
			);
171
		}
172
	}
173
174
	/**
175 4
	 * Loads the user defined label's for use in the template etc.
176
	 */
177 4
	private function _loadLabels()
178
	{
179 4
		global $context, $txt;
180
181 4
		$userLabels = explode(',', User::$settings['message_labels']);
0 ignored issues
show
Bug introduced by
It seems like ElkArte\User::settings['message_labels'] can also be of type array; however, parameter $string of explode() does only seem to accept string, 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

181
		$userLabels = explode(',', /** @scrutinizer ignore-type */ User::$settings['message_labels']);
Loading history...
182
183 4
		foreach ($userLabels as $id_label => $label_name)
184
		{
185 4
			if (empty($label_name))
186
			{
187
				continue;
188
			}
189
190
			$context['labels'][(int) $id_label] = array(
191
				'id' => $id_label,
192
				'name' => trim($label_name),
193
				'messages' => 0,
194
				'unread_messages' => 0,
195
			);
196
		}
197 4
198 4
		// The default inbox is always available
199 4
		$context['labels'][-1] = array(
200 4
			'id' => -1,
201 4
			'name' => $txt['pm_msg_label_inbox'],
202
			'messages' => 0,
203 4
			'unread_messages' => 0,
204
		);
205
	}
206
207
	/**
208
	 * This is the main function of personal messages, called before the action handler.
209
	 *
210
	 * What it does:
211
	 *
212
	 * - PersonalMessages is a menu-based controller.
213
	 * - It sets up the menu.
214
	 * - Calls from the menu the appropriate method/function for the current area.
215
	 *
216 4
	 * @see \ElkArte\AbstractController::action_index()
217
	 */
218 4
	public function action_index()
219
	{
220
		global $context;
221
222 4
		// Finally all the things we know how to do
223 4
		$subActions = array(
224 4
			'manlabels' => array($this, 'action_manlabels', 'permission' => 'pm_read'),
225 4
			'manrules' => array($this, 'action_manrules', 'permission' => 'pm_read'),
226 4
			'markunread' => array($this, 'action_markunread', 'permission' => 'pm_read'),
227 4
			'pmactions' => array($this, 'action_pmactions', 'permission' => 'pm_read'),
228 4
			'prune' => array($this, 'action_prune', 'permission' => 'pm_read'),
229 4
			'removeall' => array($this, 'action_removeall', 'permission' => 'pm_read'),
230 4
			'removeall2' => array($this, 'action_removeall2', 'permission' => 'pm_read'),
231 4
			'report' => array($this, 'action_report', 'permission' => 'pm_read'),
232 4
			'search' => array($this, 'action_search', 'permission' => 'pm_read'),
233 4
			'search2' => array($this, 'action_search2', 'permission' => 'pm_read'),
234 4
			'send' => array($this, 'action_send', 'permission' => 'pm_read'),
235 4
			'send2' => array($this, 'action_send2', 'permission' => 'pm_read'),
236
			'settings' => array($this, 'action_settings', 'permission' => 'pm_read'),
237
			'inbox' => array($this, 'action_folder', 'permission' => 'pm_read'),
238
		);
239 4
240
		// Set up our action array
241
		$action = new Action('pm_index');
242 4
243
		// Known action, go to it, otherwise the inbox for you
244
		$subAction = $action->initialize($subActions, 'inbox');
245 4
246
		// Set the right index bar for the action
247 2
		if ($subAction === 'inbox')
248
		{
249 2
			$this->_messageIndexBar($context['current_label_id'] == -1 ? $context['folder'] : 'label' . $context['current_label_id']);
250
		}
251
		elseif (!isset($this->_req->query->xml))
252
		{
253
			$this->_messageIndexBar($subAction);
254
		}
255 4
256 4
		// And off we go!
257
		$action->dispatch($subAction);
258
	}
259
260
	/**
261
	 * A menu to easily access different areas of the PM section
262
	 *
263
	 * @param string $area
264
	 *
265 2
	 * @throws \ElkArte\Exceptions\Exception no_access
266
	 */
267 2
	private function _messageIndexBar($area)
268
	{
269 2
		global $txt, $context;
270
271
		require_once(SUBSDIR . '/Menu.subs.php');
272 1
273 2
		$pm_areas = array(
274 2
			'folders' => array(
275
				'title' => $txt['pm_messages'],
276
				'counter' => 'unread_messages',
277 2
				'areas' => array(
278 2
					'inbox' => array(
279 2
						'label' => $txt['inbox'],
280
						'custom_url' => getUrl('action', ['action' => 'pm']),
281
						'counter' => 'unread_messages',
282 2
					),
283 2
					'send' => array(
284 2
						'label' => $txt['new_message'],
285
						'custom_url' => getUrl('action', ['action' => 'pm', 'sa' => 'send']),
286
						'permission' => 'pm_send',
287 2
					),
288 2
					'sent' => array(
289
						'label' => $txt['sent_items'],
290
						'custom_url' => getUrl('action', ['action' => 'pm', 'f' => 'sent']),
291
					),
292
				),
293 2
			),
294 2
			'labels' => array(
295
				'title' => $txt['pm_labels'],
296
				'counter' => 'labels_unread_total',
297
				'areas' => array(),
298 2
			),
299
			'actions' => array(
300
				'title' => $txt['pm_actions'],
301 2
				'areas' => array(
302 2
					'search' => array(
303
						'label' => $txt['pm_search_bar_title'],
304
						'custom_url' => getUrl('action', ['action' => 'pm', 'sa' => 'search']),
305 2
					),
306 2
					'prune' => array(
307
						'label' => $txt['pm_prune'],
308
						'custom_url' => getUrl('action', ['action' => 'pm', 'sa' => 'prune']),
309
					),
310
				),
311 2
			),
312
			'pref' => array(
313
				'title' => $txt['pm_preferences'],
314 2
				'areas' => array(
315 2
					'manlabels' => array(
316
						'label' => $txt['pm_manage_labels'],
317
						'custom_url' => getUrl('action', ['action' => 'pm', 'sa' => 'manlabels']),
318 2
					),
319 2
					'manrules' => array(
320
						'label' => $txt['pm_manage_rules'],
321
						'custom_url' => getUrl('action', ['action' => 'pm', 'sa' => 'manrules']),
322 2
					),
323 2
					'settings' => array(
324
						'label' => $txt['pm_settings'],
325
						'custom_url' => getUrl('action', ['action' => 'pm', 'sa' => 'settings']),
326
					),
327
				),
328
			),
329
		);
330 2
331 2
		// Handle labels.
332
		$label_counters = array('unread_messages' => $context['labels'][-1]['unread_messages']);
333 2
		if (empty($context['currently_using_labels']))
334
		{
335
			unset($pm_areas['labels']);
336
		}
337
		else
338
		{
339
			// Note we send labels by id as it will have less problems in the query string.
340
			$label_counters['labels_unread_total'] = 0;
341
			foreach ($context['labels'] as $label)
342
			{
343
				if ($label['id'] == -1)
344
				{
345
					continue;
346
				}
347
348
				// Count the amount of unread items in labels.
349
				$label_counters['labels_unread_total'] += $label['unread_messages'];
350
351
				// Add the label to the menu.
352
				$pm_areas['labels']['areas']['label' . $label['id']] = array(
353
					'label' => $label['name'],
354
					'custom_url' => getUrl('action', ['action' => 'pm', 'l' => $label['id']]),
355
					'counter' => 'label' . $label['id'],
356
					'messages' => $label['messages'],
357
				);
358
359
				$label_counters['label' . $label['id']] = $label['unread_messages'];
360
			}
361
		}
362 2
363
		// Do we have a limit on the amount of messages we can keep?
364
		if (!empty($context['message_limit']))
365
		{
366
			$bar = round(($this->user->messages * 100) / $context['message_limit'], 1);
367
368
			$context['limit_bar'] = array(
369
				'messages' => $this->user->messages,
370
				'allowed' => $context['message_limit'],
371
				'percent' => $bar,
372
				'bar' => $bar > 100 ? 100 : (int) $bar,
373
				'text' => sprintf($txt['pm_currently_using'], $this->user->messages, $bar)
374
			);
375
		}
376
377 2
		// Set a few options for the menu.
378 2
		$menuOptions = array(
379
			'current_area' => $area,
380 2
			'hook' => 'pm',
381
			'disable_url_session_check' => true,
382
			'counters' => !empty($label_counters) ? $label_counters : 0,
383
		);
384 2
385 2
		// Actually create the menu!
386
		$pm_include_data = createMenu($pm_areas, $menuOptions);
387
		unset($pm_areas);
388 2
389 2
		// Make a note of the Unique ID for this menu.
390
		$context['pm_menu_id'] = $context['max_menu_id'];
391
		$context['pm_menu_name'] = 'menu_data_' . $context['pm_menu_id'];
392 2
393
		// Set the selected item.
394
		$context['menu_item_selected'] = $pm_include_data['current_area'];
395 2
396
		// Set the template for this area and add the profile layer.
397
		if (!isset($this->_req->query->xml))
398
		{
399
			$template_layers = theme()->getLayers();
400 2
			$template_layers->add('pm');
401
		}
402
	}
403
404
	/**
405
	 * Display a folder, ie. inbox/sent etc.
406
	 *
407
	 * Display mode: 0 = all at once, 1 = one at a time, 2 = as a conversation
408
	 *
409
	 * @uses folder sub template
410 2
	 * @uses subject_list, pm template layers
411
	 */
412 2
	public function action_folder()
413 2
	{
414
		global $txt, $scripturl, $modSettings, $context, $subjects_request;
415
		global $messages_request, $options;
416 2
417
		// Changing view?
418
		if (isset($this->_req->query->view))
419
		{
420
			$context['display_mode'] = $context['display_mode'] > 1 ? 0 : $context['display_mode'] + 1;
421
			require_once(SUBSDIR . '/Members.subs.php');
422
			updateMemberData($this->user->id, array('pm_prefs' => (User::$settings['pm_prefs'] & 252) | $context['display_mode']));
423
		}
424 2
425
		// Make sure the starting location is valid.
426 2
		if (isset($this->_req->query->start) && $this->_req->query->start !== 'new')
427
		{
428
			$start = (int) $this->_req->query->start;
429
		}
430
		elseif (!isset($this->_req->query->start) && !empty($options['view_newest_pm_first']))
431
		{
432
			$start = 0;
433
		}
434
		else
435
		{
436
			$start = 'new';
437
		}
438 2
439 2
		// Set up some basic template stuff.
440 2
		$context['from_or_to'] = $context['folder'] !== 'sent' ? 'from' : 'to';
441
		$context['signature_enabled'] = substr($modSettings['signature_settings'], 0, 1) == 1;
442
		$context['disabled_fields'] = isset($modSettings['disabled_profile_fields']) ? array_flip(explode(',', $modSettings['disabled_profile_fields'])) : array();
443 2
444 2
		// Set the template layers we need
445
		$template_layers = theme()->getLayers();
446 2
		$template_layers->addAfter('subject_list', 'pm');
447 2
448
		$labelQuery = $context['folder'] !== 'sent' ? '
449
				AND FIND_IN_SET(' . $context['current_label_id'] . ', pmr.labels) != 0' : '';
450 2
451 2
		// They didn't pick a sort, so we use the forum default.
452
		$sort_by = !isset($this->_req->query->sort) ? 'date' : $this->_req->query->sort;
453
		$descending = isset($this->_req->query->desc);
454 1
455
		// Set our sort by query
456 2
		switch ($sort_by)
457 2
		{
458 2
			case 'date':
459
				$sort_by_query = 'pm.id_pm';
460 2
				if (!empty($options['view_newest_pm_first']) && !isset($this->_req->query->desc) && !isset($this->_req->query->asc))
461
				{
462 2
					$descending = true;
463
				}
464
				break;
465
			case 'name':
466
				$sort_by_query = 'COALESCE(mem.real_name, \'\')';
467
				break;
468
			case 'subject':
469
				$sort_by_query = 'pm.subject';
470
				break;
471
			default:
472
				$sort_by_query = 'pm.id_pm';
473
		}
474 2
475 2
		// Set the text to resemble the current folder.
476
		$pmbox = $context['folder'] !== 'sent' ? $txt['inbox'] : $txt['sent_items'];
477
		$txt['delete_all'] = str_replace('PMBOX', $pmbox, $txt['delete_all']);
478 2
479
		// Now, build the link tree!
480 2
		if ($context['current_label_id'] === -1)
481 2
		{
482 2
			$context['linktree'][] = array(
483
				'url' => getUrl('action', ['action' => 'pm', 'f' => $context['folder']]),
484
				'name' => $pmbox
485
			);
486
		}
487 2
488
		// Build it further if we also have a label.
489
		if ($context['current_label_id'] !== -1)
490
		{
491
			$context['linktree'][] = array(
492
				'url' => getUrl('action', ['action' => 'pm', 'f' => $context['folder'], 'l' => $context['current_label_id']]),
493
				'name' => $txt['pm_current_label'] . ': ' . $context['current_label']
494
			);
495
		}
496 2
497
		// Figure out how many messages there are.
498
		$max_messages = getPMCount(false, null, $labelQuery);
499 2
500
		// Only show the button if there are messages to delete.
501
		$context['show_delete'] = $max_messages > 0;
502 2
503
		// Start on the last page.
504 2
		if (!is_numeric($start) || $start >= $max_messages)
505
		{
506
			$start = ($max_messages - 1) - (($max_messages - 1) % $modSettings['defaultMaxMessages']);
507
		}
508
		elseif ($start < 0)
509
		{
510
			$start = 0;
511
		}
512 2
513
		// ... but wait - what if we want to start from a specific message?
514
		if (isset($this->_req->query->pmid))
515
		{
516
			$pmID = (int) $this->_req->query->pmid;
517
518
			// Make sure you have access to this PM.
519
			if (!isAccessiblePM($pmID, $context['folder'] === 'sent' ? 'outbox' : 'inbox'))
520
			{
521
				throw new Exception('no_access', false);
522
			}
523
524
			$context['current_pm'] = $pmID;
525
526
			// With only one page of PM's we're gonna want page 1.
527
			if ($max_messages <= $modSettings['defaultMaxMessages'])
528
			{
529
				$start = 0;
530
			}
531
			// If we pass kstart we assume we're in the right place.
532
			elseif (!isset($this->_req->query->kstart))
533
			{
534
				$start = getPMCount($descending, $pmID, $labelQuery);
535
536
				// To stop the page index's being abnormal, start the page on the page the message
537
				// would normally be located on...
538
				$start = $modSettings['defaultMaxMessages'] * (int) ($start / $modSettings['defaultMaxMessages']);
539
			}
540
		}
541 2
542
		// Sanitize and validate pmsg variable if set.
543
		if (isset($this->_req->query->pmsg))
544
		{
545
			$pmsg = (int) $this->_req->query->pmsg;
546
547
			if (!isAccessiblePM($pmsg, $context['folder'] === 'sent' ? 'outbox' : 'inbox'))
548
			{
549
				throw new Exception('no_access', false);
550
			}
551
		}
552
553 2
		// Determine the navigation context
554 2
		$context['links'] += array(
555
			'prev' => $start >= $modSettings['defaultMaxMessages'] ? $scripturl . '?action=pm;start=' . ($start - $modSettings['defaultMaxMessages']) : '',
556 2
			'next' => $start + $modSettings['defaultMaxMessages'] < $max_messages ? $scripturl . '?action=pm;start=' . ($start + $modSettings['defaultMaxMessages']) : '',
557 2
		);
558 2
		$context['page_info'] = array(
559
			'current_page' => $start / $modSettings['defaultMaxMessages'] + 1,
560
			'num_pages' => floor(($max_messages - 1) / $modSettings['defaultMaxMessages']) + 1
561
		);
562 2
563 2
		// We now know what they want, so lets fetch those PM's
564 2
		list ($pms, $posters, $recipients, $lastData) = loadPMs(array(
565 2
			'sort_by_query' => $sort_by_query,
566 2
			'display_mode' => $context['display_mode'],
567 2
			'sort_by' => $sort_by,
568 2
			'label_query' => $labelQuery,
569 2
			'pmsg' => isset($pmsg) ? (int) $pmsg : 0,
570 2
			'descending' => $descending,
571 2
			'start' => $start,
572 2
			'limit' => $modSettings['defaultMaxMessages'],
573 2
			'folder' => $context['folder'],
574
			'pmid' => $pmID ?? 0,
575
		), $this->user->id);
576 2
577
		// Make sure that we have been given a correct head pm id if we are in conversation mode
578
		if ($context['display_mode'] == 2 && !empty($pmID) && $pmID != $lastData['id'])
579
		{
580
			throw new Exception('no_access', false);
581
		}
582 2
583
		// If loadPMs returned results, lets show the pm subject list
584
		if (!empty($pms))
585
		{
586
			// Tell the template if no pm has specifically been selected
587
			if (empty($pmID))
588
			{
589
				$context['current_pm'] = 0;
590
			}
591
592
			$display_pms = $context['display_mode'] == 0 ? $pms : array($lastData['id']);
593
594
			// At this point we know the main id_pm's. But if we are looking at conversations we need
595
			// the PMs that make up the conversation
596
			if ($context['display_mode'] == 2)
597
			{
598
				list($display_pms, $posters) = loadConversationList($lastData['head'], $recipients, $context['folder']);
599
600
				// Conversation list may expose additional PM's being displayed
601
				$all_pms = array_unique(array_merge($pms, $display_pms));
602
603
				// See if any of these 'listing' PM's are in a conversation thread that has unread entries
604
				$context['conversation_unread'] = loadConversationUnreadStatus($all_pms);
605
			}
606
			// This is pretty much EVERY pm!
607
			else
608
			{
609
				$all_pms = array_unique(array_merge($pms, $display_pms));
610
			}
611
612
			// Get recipients (don't include bcc-recipients for your inbox, you're not supposed to know :P).
613
			list($context['message_labels'], $context['message_replied'], $context['message_unread']) = loadPMRecipientInfo($all_pms, $recipients, $context['folder']);
614
615
			// Make sure we don't load any unnecessary data for one at a time mode
616
			if ($context['display_mode'] == 1)
617
			{
618
				foreach ($posters as $pm_key => $sender)
619
				{
620
					if (!in_array($pm_key, $display_pms))
621
					{
622
						unset($posters[$pm_key]);
623
					}
624
				}
625
			}
626
627
			// Load some information about the message sender
628
			$posters = array_unique($posters);
629
			if (!empty($posters))
630
			{
631
				MembersList::load($posters);
632
			}
633
634
			// If we're on grouped/restricted view get a restricted list of messages.
635
			if ($context['display_mode'] != 0)
636
			{
637
				// Get the order right.
638
				$orderBy = array();
639
				foreach (array_reverse($pms) as $pm)
640
				{
641
					$orderBy[] = 'pm.id_pm = ' . $pm;
642
				}
643
644
				// Separate query for these bits, the callback will use it as required
645
				$subjects_request = loadPMSubjectRequest($pms, $orderBy);
646
			}
647
648
			// Execute the load message query if a message has been chosen and let
649
			// the callback fetch the results.  Otherwise just show the pm selection list
650
			if (empty($pmsg) && empty($pmID) && $context['display_mode'] != 0)
651
			{
652
				$messages_request = false;
653
			}
654
			else
655
			{
656
				$messages_request = loadPMMessageRequest($display_pms, $sort_by_query, $sort_by, $descending, $context['display_mode'], $context['folder']);
657
			}
658
		}
659 2
		else
660
		{
661
			$messages_request = false;
662
		}
663 2
664 2
		// Initialize the subject and message render callbacks
665 2
		$bodyParser = new Normal(array(), false);
666 2
		$opt = new ValuesContainer(['recipients' => $recipients]);
667
		$renderer = new PmRenderer($messages_request, $this->user, $bodyParser, $opt);
668
		$subject_renderer = new PmRenderer($subjects_request, $this->user, $bodyParser, $opt);
669 2
670 2
		// Subject and Message
671
		$context['get_pmessage'] = array($renderer, 'getContext');
672
		$context['get_psubject'] = array($subject_renderer, 'getContext');
673 2
674 2
		// Prepare some items for the template
675 2
		$context['topic_starter_id'] = 0;
676 2
		$context['can_send_pm'] = allowedTo('pm_send');
677 2
		$context['can_send_email'] = allowedTo('send_email_to_members');
678 2
		$context['sub_template'] = 'folder';
679 2
		$context['page_title'] = $txt['pm_inbox'];
680
		$context['sort_direction'] = $descending ? 'down' : 'up';
681 2
		$context['sort_by'] = $sort_by;
682
683
		if ($messages_request !== false && $messages_request->hasResults())
684
		{
685
			// Auto video embedding enabled, someone may have a link in a PM
686
			if (!empty($modSettings['enableVideoEmbeding']))
687
			{
688
				theme()->addInlineJavascript('
689
					$(function() {
690
						$().linkifyvideo(oEmbedtext);
691
					});', true
692
				);
693
			}
694
695
			if (!empty($context['show_delete']))
696
			{
697
				theme()->getLayers()->addEnd('pm_pages_and_buttons');
698
			}
699
		}
700 2
701 2
		// Set up the page index.
702
		$context['page_index'] = constructPageIndex('{scripturl}?action=pm;f=' . $context['folder'] . (isset($this->_req->query->l) ? ';l=' . (int) $this->_req->query->l : '') . ';sort=' . $context['sort_by'] . ($descending ? ';desc' : ''), $start, $max_messages, $modSettings['defaultMaxMessages']);
703 2
		$context['start'] = $start;
704
705
		$context['pm_form_url'] = $scripturl . '?action=pm;sa=pmactions;' . ($context['display_mode'] == 2 ? 'conversation;' : '') . 'f=' . $context['folder'] . ';start=' . $context['start'] . ($context['current_label_id'] != -1 ? ';l=' . $context['current_label_id'] : '');
706 2
707
		// Finally mark the relevant messages as read.
708
		if ($context['folder'] !== 'sent' && !empty($context['labels'][(int) $context['current_label_id']]['unread_messages']))
709
		{
710
			// If the display mode is "old sk00l" do them all...
711
			if ($context['display_mode'] == 0)
712
			{
713
				markMessages(null, $context['current_label_id']);
714
			}
715
			// Otherwise do just the currently displayed ones!
716
			elseif (!empty($context['current_pm']))
717
			{
718
				markMessages($display_pms, $context['current_label_id']);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $display_pms does not seem to be defined for all execution paths leading up to this point.
Loading history...
719
			}
720
		}
721 2
722
		// Build the conversation button array.
723
		if ($context['display_mode'] === 2 && !empty($context['current_pm']))
724
		{
725
			$context['conversation_buttons'] = array(
726
				'delete' => array(
727
					'text' => 'delete_conversation',
728
					'image' => 'delete.png',
729
					'lang' => true,
730
					'url' => $scripturl . '?action=pm;sa=pmactions;pm_actions%5B' . $context['current_pm'] . '%5D=delete;conversation;f=' . $context['folder'] . ';start=' . $context['start'] . ($context['current_label_id'] != -1 ? ';l=' . $context['current_label_id'] : '') . ';' . $context['session_var'] . '=' . $context['session_id'],
731
					'custom' => 'onclick="return confirm(\'' . addslashes($txt['remove_message']) . '?\');"'
732
				),
733
			);
734
735
			// Allow mods to add additional buttons here
736 2
			call_integration_hook('integrate_conversation_buttons');
737
		}
738
	}
739
740
	/**
741 2
	 * Send a new personal message?
742
	 */
743 2
	public function action_send()
744
	{
745
		global $txt, $modSettings, $context;
746 2
747 2
		// Load in some text and template dependencies
748
		Txt::load('PersonalMessage');
749
		theme()->getTemplates()->load('PersonalMessage');
750 2
751
		// Set the template we will use
752
		$context['sub_template'] = 'send';
753 2
754
		// Extract out the spam settings - cause it's neat.
755
		list ($modSettings['max_pm_recipients'], $modSettings['pm_posts_verification'], $modSettings['pm_posts_per_hour']) = explode(',', $modSettings['pm_spam_settings']);
756 2
757 2
		// Set up some items for the template
758
		$context['page_title'] = $txt['send_message'];
759
		$context['reply'] = isset($this->_req->query->pmsg) || isset($this->_req->query->quote);
760 2
761
		// Check whether we've gone over the limit of messages we can send per hour.
762
		if (!empty($modSettings['pm_posts_per_hour']) && !allowedTo(array('admin_forum', 'moderate_forum', 'send_mail')) && $this->user->mod_cache['bq'] === '0=1' && $this->user->mod_cache['gq'] === '0=1')
763
		{
764
			// How many messages have they sent this last hour?
765
			$pmCount = pmCount($this->user->id, 3600);
766
767
			if (!empty($pmCount) && $pmCount >= $modSettings['pm_posts_per_hour'])
768
			{
769
				throw new Exception('pm_too_many_per_hour', true, array($modSettings['pm_posts_per_hour']));
770
			}
771
		}
772
773 2
		try
774
		{
775
			$this->_events->trigger('before_set_context', array('pmsg' => $this->_req->query->pmsg ?? ($this->_req->query->quote ?? 0)));
776
		}
777
		catch (PmErrorException $e)
778
		{
779
			return $this->messagePostError($e->namedRecipientList, $e->recipientList, $e->msgOptions);
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->messagePostError(...ntList, $e->msgOptions) targeting ElkArte\Controller\Perso...age::messagePostError() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
Bug introduced by
$e->msgOptions of type ElkArte\ValuesContainer is incompatible with the type array<mixed,mixed> expected by parameter $msg_options of ElkArte\Controller\Perso...age::messagePostError(). ( Ignorable by Annotation )

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

779
			return $this->messagePostError($e->namedRecipientList, $e->recipientList, /** @scrutinizer ignore-type */ $e->msgOptions);
Loading history...
780
		}
781 2
782
		// Quoting / Replying to a message?
783
		if (!empty($this->_req->query->pmsg))
784
		{
785
			$pmsg = $this->_req->getQuery('pmsg', 'intval');
786
787
			// Make sure this is accessible (not deleted)
788
			if (!isAccessiblePM($pmsg))
789
			{
790
				throw new Exception('no_access', false);
791
			}
792
793
			// Validate that this is one has been received?
794
			$isReceived = checkPMReceived($pmsg);
795
796
			// Get the quoted message (and make sure you're allowed to see this quote!).
797
			$row_quoted = loadPMQuote($pmsg, $isReceived);
798
			if ($row_quoted === false)
799
			{
800
				throw new Exception('pm_not_yours', false);
801
			}
802
803
			// Censor the message.
804
			$row_quoted['subject'] = censor($row_quoted['subject']);
805
			$row_quoted['body'] = censor($row_quoted['body']);
806
807
			// Lets make sure we mark this one as read
808
			markMessages($pmsg);
809
810
			// Figure out which flavor or 'Re: ' to use
811
			$context['response_prefix'] = response_prefix();
812
813
			$form_subject = $row_quoted['subject'];
814
815
			// Add 'Re: ' to it....
816
			if ($context['reply'] && trim($context['response_prefix']) !== '' && Util::strpos($form_subject, trim($context['response_prefix'])) !== 0)
817
			{
818
				$form_subject = $context['response_prefix'] . $form_subject;
819
			}
820
821
			// If quoting, lets clean up some things and set the quote header for the pm body
822
			if (isset($this->_req->query->quote))
823
			{
824
				// Remove any nested quotes and <br />...
825
				$form_message = preg_replace('~<br ?/?' . '>~i', "\n", $row_quoted['body']);
826
				$form_message = removeNestedQuotes($form_message);
827
828
				if (empty($row_quoted['id_member']))
829
				{
830
					$form_message = '[quote author=&quot;' . $row_quoted['real_name'] . '&quot;]' . "\n" . $form_message . "\n" . '[/quote]';
831
				}
832
				else
833
				{
834
					$form_message = '[quote author=' . $row_quoted['real_name'] . ' link=action=profile;u=' . $row_quoted['id_member'] . ' date=' . $row_quoted['msgtime'] . ']' . "\n" . $form_message . "\n" . '[/quote]';
835
				}
836
			}
837
			else
838
			{
839
				$form_message = '';
840
			}
841
842
			// Do the BBC thang on the message.
843
			$bbc_parser = ParserWrapper::instance();
844
			$row_quoted['body'] = $bbc_parser->parsePM($row_quoted['body']);
845
846
			// Set up the quoted message array.
847
			$context['quoted_message'] = array(
848
				'id' => $row_quoted['id_pm'],
849
				'pm_head' => $row_quoted['pm_head'],
850
				'member' => array(
851
					'name' => $row_quoted['real_name'],
852
					'username' => $row_quoted['member_name'],
853
					'id' => $row_quoted['id_member'],
854
					'href' => !empty($row_quoted['id_member']) ? getUrl('profile', ['action' => 'profile', 'u' => $row_quoted['id_member']]) : '',
855
					'link' => !empty($row_quoted['id_member']) ? '<a href="' . getUrl('profile', ['action' => 'profile', 'u' => $row_quoted['id_member']]) . '">' . $row_quoted['real_name'] . '</a>' : $row_quoted['real_name'],
856
				),
857
				'subject' => $row_quoted['subject'],
858
				'time' => standardTime($row_quoted['msgtime']),
859
				'html_time' => htmlTime($row_quoted['msgtime']),
860
				'timestamp' => forum_time(true, $row_quoted['msgtime']),
861
				'body' => $row_quoted['body']
862
			);
863
		}
864
		// A new message it is then
865 2
		else
866 2
		{
867 2
			$context['quoted_message'] = false;
868
			$form_subject = '';
869
			$form_message = '';
870
		}
871 2
872
		// Start of like we don't know where this is going
873
		$context['recipients'] = array(
874
			'to' => array(),
875
			'bcc' => array(),
876
		);
877 2
878
		// Sending by ID?  Replying to all?  Fetch the real_name(s).
879
		if (isset($this->_req->query->u))
880
		{
881
			// If the user is replying to all, get all the other members this was sent to..
882
			if ($this->_req->query->u === 'all' && isset($row_quoted))
883
			{
884
				// Firstly, to reply to all we clearly already have $row_quoted - so have the original member from.
885
				if ($row_quoted['id_member'] != $this->user->id)
886
				{
887
					$context['recipients']['to'][] = array(
888
						'id' => $row_quoted['id_member'],
889
						'name' => htmlspecialchars($row_quoted['real_name'], ENT_COMPAT, 'UTF-8'),
890
					);
891
				}
892
893
				// Now to get all the others.
894
				$context['recipients']['to'] = array_merge($context['recipients']['to'], isset($pmsg) ? loadPMRecipientsAll($pmsg) : array());
895
			}
896
			else
897
			{
898
				$users = array_map('intval', explode(',', $this->_req->query->u));
899
				$users = array_unique($users);
900
901
				// For all the member's this is going to, get their display name.
902
				require_once(SUBSDIR . '/Members.subs.php');
903
				$result = getBasicMemberData($users);
904
905
				foreach ($result as $row)
906
				{
907
					$context['recipients']['to'][] = array(
908
						'id' => $row['id_member'],
909
						'name' => $row['real_name'],
910
					);
911
				}
912
			}
913
914
			// Get a literal name list in case the user has JavaScript disabled.
915
			$names = array();
916
			foreach ($context['recipients']['to'] as $to)
917
			{
918
				$names[] = $to['name'];
919
			}
920
			$context['to_value'] = empty($names) ? '' : '&quot;' . implode('&quot;, &quot;', $names) . '&quot;';
921
		}
922 2
		else
923
		{
924
			$context['to_value'] = '';
925
		}
926 2
927 2
		// Set the defaults...
928
		$context['subject'] = $form_subject;
929
		$context['message'] = str_replace(array('"', '<', '>', '&nbsp;'), array('&quot;', '&lt;', '&gt;', ' '), $form_message);
930 2
931 2
		// And build the link tree.
932 2
		$context['linktree'][] = array(
933
			'url' => getUrl('action', ['action' => 'pm', 'sa' => 'send']),
934
			'name' => $txt['new_message']
935
		);
936 2
937
		// Needed for the editor.
938
		require_once(SUBSDIR . '/Editor.subs.php');
939
940 2
		// Now create the editor.
941 2
		$editorOptions = array(
942 2
			'id' => 'message',
943 2
			'value' => $context['message'],
944
			'height' => '250px',
945 2
			'width' => '100%',
946
			'labels' => array(
947 2
				'post_button' => $txt['send_message'],
948
			),
949
			'preview_type' => 2,
950
		);
951 2
952
		// Trigger the prepare_send_context PM event
953 2
		$this->_events->trigger('prepare_send_context', array('editorOptions' => &$editorOptions));
954
955
		create_control_richedit($editorOptions);
956 2
957
		// No one is bcc'ed just yet
958
		$context['bcc_value'] = '';
959 2
960 2
		// Register this form and get a sequence number in $context.
961
		checkSubmitOnce('register');
962
	}
963
964
	/**
965
	 * An error in the message...
966
	 *
967
	 * @param mixed[] $named_recipients
968
	 * @param mixed[] $recipient_ids array keys of [bbc] => int[] and [to] => int[]
969
	 * @param mixed[] $msg_options body, subject and reply values
970
	 *
971
	 * @throws \ElkArte\Exceptions\Exception pm_not_yours
972
	 */
973
	public function messagePostError($named_recipients, $recipient_ids = array(), $msg_options = null)
974
	{
975
		global $txt, $context, $modSettings;
976
977
		if (isset($this->_req->query->xml))
978
		{
979
			$context['sub_template'] = 'generic_preview';
980
		}
981
		else
982
		{
983
			$context['sub_template'] = 'send';
984
			$context['menu_data_' . $context['pm_menu_id']]['current_area'] = 'send';
985
		}
986
987
		$context['page_title'] = $txt['send_message'];
988
		$error_types = ErrorContext::context('pm', 1);
989
990
		// Got some known members?
991
		$context['recipients'] = array(
992
			'to' => array(),
993
			'bcc' => array(),
994
		);
995
996
		if (!empty($recipient_ids['to']) || !empty($recipient_ids['bcc']))
997
		{
998
			$allRecipients = array_merge($recipient_ids['to'], $recipient_ids['bcc']);
999
1000
			require_once(SUBSDIR . '/Members.subs.php');
1001
1002
			// Get the latest activated member's display name.
1003
			$result = getBasicMemberData($allRecipients);
1004
			foreach ($result as $row)
1005
			{
1006
				$recipientType = in_array($row['id_member'], $recipient_ids['bcc']) ? 'bcc' : 'to';
1007
				$context['recipients'][$recipientType][] = array(
1008
					'id' => $row['id_member'],
1009
					'name' => $row['real_name'],
1010
				);
1011
			}
1012
		}
1013
1014
		// Set everything up like before....
1015
		if (!empty($msg_options))
1016
		{
1017
			$context['subject'] = $msg_options->subject;
1018
			$context['message'] = $msg_options->body;
1019
			$context['reply'] = $msg_options->reply_to;
1020
		}
1021
		else
1022
		{
1023
			$context['subject'] = isset($this->_req->post->subject) ? Util::htmlspecialchars($this->_req->post->subject) : '';
1024
			$context['message'] = isset($this->_req->post->message) ? str_replace(array('  '), array('&nbsp; '), Util::htmlspecialchars($this->_req->post->message, ENT_QUOTES, 'UTF-8', true)) : '';
1025
			$context['reply'] = !empty($this->_req->post->replied_to);
1026
		}
1027
1028
		// If this is a reply to message, we need to reload the quote
1029
		if ($context['reply'])
1030
		{
1031
			$pmsg = (int) $this->_req->post->replied_to;
1032
			$isReceived = $context['folder'] !== 'sent';
1033
			$row_quoted = loadPMQuote($pmsg, $isReceived);
1034
			if ($row_quoted === false)
1035
			{
1036
				if (!isset($this->_req->query->xml))
1037
				{
1038
					throw new Exception('pm_not_yours', false);
1039
				}
1040
				else
1041
				{
1042
					$error_types->addError('pm_not_yours');
1043
				}
1044
			}
1045
			else
1046
			{
1047
				$row_quoted['subject'] = censor($row_quoted['subject']);
1048
				$row_quoted['body'] = censor($row_quoted['body']);
1049
				$bbc_parser = ParserWrapper::instance();
1050
1051
				$context['quoted_message'] = array(
1052
					'id' => $row_quoted['id_pm'],
1053
					'pm_head' => $row_quoted['pm_head'],
1054
					'member' => array(
1055
						'name' => $row_quoted['real_name'],
1056
						'username' => $row_quoted['member_name'],
1057
						'id' => $row_quoted['id_member'],
1058
						'href' => !empty($row_quoted['id_member']) ? getUrl('profile', ['action' => 'profile', 'u' => $row_quoted['id_member']]) : '',
1059
						'link' => !empty($row_quoted['id_member']) ? '<a href="' . getUrl('profile', ['action' => 'profile', 'u' => $row_quoted['id_member']]) . '">' . $row_quoted['real_name'] . '</a>' : $row_quoted['real_name'],
1060
					),
1061
					'subject' => $row_quoted['subject'],
1062
					'time' => standardTime($row_quoted['msgtime']),
1063
					'html_time' => htmlTime($row_quoted['msgtime']),
1064
					'timestamp' => forum_time(true, $row_quoted['msgtime']),
1065
					'body' => $bbc_parser->parsePM($row_quoted['body']),
1066
				);
1067
			}
1068
		}
1069
1070
		// Build the link tree....
1071
		$context['linktree'][] = array(
1072
			'url' => getUrl('action', ['action' => 'pm', 'sa' => 'send']),
1073
			'name' => $txt['new_message']
1074
		);
1075
1076
		// Set each of the errors for the template.
1077
		$context['post_error'] = array(
1078
			'errors' => $error_types->prepareErrors(),
1079
			'type' => $error_types->getErrorType() == 0 ? 'minor' : 'serious',
1080
			'title' => $txt['error_while_submitting'],
1081
		);
1082
1083
		// We need to load the editor once more.
1084
		require_once(SUBSDIR . '/Editor.subs.php');
1085
1086
		// Create it...
1087
		$editorOptions = array(
1088
			'id' => 'message',
1089
			'value' => $context['message'],
1090
			'width' => '100%',
1091
			'height' => '250px',
1092
			'labels' => array(
1093
				'post_button' => $txt['send_message'],
1094
			),
1095
			'preview_type' => 2,
1096
		);
1097
1098
		// Trigger the prepare_send_context PM event
1099
		$this->_events->trigger('prepare_send_context', array('editorOptions' => &$editorOptions));
1100
1101
		create_control_richedit($editorOptions);
1102
1103
		// Check whether we need to show the code again.
1104
		$context['require_verification'] = $this->user->is_admin === false && !empty($modSettings['pm_posts_verification']) && $this->user->posts < $modSettings['pm_posts_verification'];
1105
		if ($context['require_verification'] && !isset($this->_req->query->xml))
1106
		{
1107
			$verificationOptions = array(
1108
				'id' => 'pm',
1109
			);
1110
			$context['require_verification'] = VerificationControlsIntegrate::create($verificationOptions);
1111
			$context['visual_verification_id'] = $verificationOptions['id'];
1112
		}
1113
1114
		$context['to_value'] = empty($named_recipients['to']) ? '' : '&quot;' . implode('&quot;, &quot;', $named_recipients['to']) . '&quot;';
1115
		$context['bcc_value'] = empty($named_recipients['bcc']) ? '' : '&quot;' . implode('&quot;, &quot;', $named_recipients['bcc']) . '&quot;';
1116
1117
		// No check for the previous submission is needed.
1118
		checkSubmitOnce('free');
1119
1120
		// Acquire a new form sequence number.
1121
		checkSubmitOnce('register');
1122
	}
1123
1124
	/**
1125 2
	 * Send a personal message.
1126
	 */
1127 2
	public function action_send2()
1128
	{
1129
		global $txt, $context, $modSettings;
1130 2
1131 2
		// All the helpers we need
1132
		require_once(SUBSDIR . '/Auth.subs.php');
1133 2
		require_once(SUBSDIR . '/Post.subs.php');
1134
1135
		Txt::load('PersonalMessage', '', false);
1136 2
1137
		// Extract out the spam settings - it saves database space!
1138
		list ($modSettings['max_pm_recipients'], $modSettings['pm_posts_verification'], $modSettings['pm_posts_per_hour']) = explode(',', $modSettings['pm_spam_settings']);
1139 2
1140
		// Initialize the errors we're about to make.
1141
		$post_errors = ErrorContext::context('pm', 1);
1142 2
1143 2
		// Check whether we've gone over the limit of messages we can send per hour - fatal error if fails!
1144 2
		if (!empty($modSettings['pm_posts_per_hour'])
1145 2
			&& !allowedTo(array('admin_forum', 'moderate_forum', 'send_mail'))
1146
			&& $this->user->mod_cache['bq'] === '0=1'
1147
			&& $this->user->mod_cache['gq'] === '0=1'
1148
		)
1149
		{
1150
			// How many have they sent this last hour?
1151
			$pmCount = pmCount($this->user->id, 3600);
1152
1153
			if (!empty($pmCount) && $pmCount >= $modSettings['pm_posts_per_hour'])
1154
			{
1155
				if (!isset($this->_req->query->xml))
1156
				{
1157
					throw new Exception('pm_too_many_per_hour', true, array($modSettings['pm_posts_per_hour']));
1158
				}
1159
				else
1160
				{
1161
					$post_errors->addError('pm_too_many_per_hour');
1162
				}
1163
			}
1164
		}
1165 2
1166
		// If your session timed out, show an error, but do allow to re-submit.
1167
		if (!isset($this->_req->query->xml) && checkSession('post', '', false) != '')
1168
		{
1169
			$post_errors->addError('session_timeout');
1170 2
		}
1171 2
1172 2
		$this->_req->post->subject = isset($this->_req->post->subject) ? strtr(Util::htmltrim($this->_req->post->subject), array("\r" => '', "\n" => '', "\t" => '')) : '';
1173
		$this->_req->post->to = $this->_req->getPost('to', 'trim', empty($this->_req->query->to) ? '' : $this->_req->query->to);
1174
		$this->_req->post->bcc = $this->_req->getPost('bcc', 'trim', empty($this->_req->query->bcc) ? '' : $this->_req->query->bcc);
1175 2
1176
		// Route the input from the 'u' parameter to the 'to'-list.
1177 2
		if (!empty($this->_req->post->u))
1178
		{
1179
			$this->_req->post->recipient_to = explode(',', $this->_req->post->u);
1180 2
		}
1181
1182
		$bbc_parser = ParserWrapper::instance();
1183 2
1184 2
		// Construct the list of recipients.
1185 2
		$recipientList = array();
1186 2
		$namedRecipientList = array();
1187
		$namesNotFound = array();
1188
		foreach (array('to', 'bcc') as $recipientType)
1189 2
		{
1190 2
			// First, let's see if there's user ID's given.
1191 2
			$recipientList[$recipientType] = array();
1192
			$type = 'recipient_' . $recipientType;
1193 2
			if (!empty($this->_req->post->{$type}) && is_array($this->_req->post->{$type}))
1194
			{
1195
				$recipientList[$recipientType] = array_map('intval', $this->_req->post->{$type});
1196
			}
1197 2
1198
			// Are there also literal names set?
1199
			if (!empty($this->_req->post->{$recipientType}))
1200 2
			{
1201
				// We're going to take out the "s anyway ;).
1202 2
				$recipientString = strtr($this->_req->post->{$recipientType}, array('\\"' => '"'));
1203 2
1204
				preg_match_all('~"([^"]+)"~', $recipientString, $matches);
1205
				$namedRecipientList[$recipientType] = array_unique(array_merge($matches[1], explode(',', preg_replace('~"[^"]+"~', '', $recipientString))));
1206 2
1207
				// Clean any literal names entered
1208 2
				foreach ($namedRecipientList[$recipientType] as $index => $recipient)
1209
				{
1210 2
					if (strlen(trim($recipient)) > 0)
1211
					{
1212
						$namedRecipientList[$recipientType][$index] = Util::htmlspecialchars(Util::strtolower(trim($recipient)));
1213
					}
1214 1
					else
1215
					{
1216
						unset($namedRecipientList[$recipientType][$index]);
1217
					}
1218
				}
1219 2
1220
				// Now see if we can resolve the entered name to an actual user
1221 2
				if (!empty($namedRecipientList[$recipientType]))
1222
				{
1223
					$foundMembers = findMembers($namedRecipientList[$recipientType]);
1224 2
1225
					// Assume all are not found, until proven otherwise.
1226
					$namesNotFound[$recipientType] = $namedRecipientList[$recipientType];
1227 2
1228
					// Make sure we only have each member listed once, in case they did not use the select list
1229
					foreach ($foundMembers as $member)
1230 2
					{
1231 2
						$testNames = array(
1232 2
							Util::strtolower($member['username']),
1233
							Util::strtolower($member['name']),
1234
							Util::strtolower($member['email']),
1235 2
						);
1236
1237 2
						if (count(array_intersect($testNames, $namedRecipientList[$recipientType])) !== 0)
1238
						{
1239
							$recipientList[$recipientType][] = $member['id'];
1240 2
1241
							// Get rid of this username, since we found it.
1242
							$namesNotFound[$recipientType] = array_diff($namesNotFound[$recipientType], $testNames);
1243
						}
1244
					}
1245
				}
1246
			}
1247 2
1248
			// Selected a recipient to be deleted? Remove them now.
1249
			if (!empty($this->_req->post->delete_recipient))
1250
			{
1251
				$recipientList[$recipientType] = array_diff($recipientList[$recipientType], array((int) $this->_req->post->delete_recipient));
1252
			}
1253 2
1254
			// Make sure we don't include the same name twice
1255
			$recipientList[$recipientType] = array_unique($recipientList[$recipientType]);
1256
		}
1257 2
1258
		// Are we changing the recipients some how?
1259
		$is_recipient_change = !empty($this->_req->post->delete_recipient) || !empty($this->_req->post->to_submit) || !empty($this->_req->post->bcc_submit);
1260 2
1261
		// Check if there's at least one recipient.
1262
		if (empty($recipientList['to']) && empty($recipientList['bcc']))
1263
		{
1264
			$post_errors->addError('no_to');
1265
		}
1266 2
1267
		// Make sure that we remove the members who did get it from the screen.
1268 2
		if (!$is_recipient_change)
1269
		{
1270 2
			foreach (array_keys($recipientList) as $recipientType)
1271
			{
1272
				if (!empty($namesNotFound[$recipientType]))
1273
				{
1274
					$post_errors->addError('bad_' . $recipientType);
1275
1276
					// Since we already have a post error, remove the previous one.
1277
					$post_errors->removeError('no_to');
1278
1279 1
					foreach ($namesNotFound[$recipientType] as $name)
1280
					{
1281
						$context['send_log']['failed'][] = sprintf($txt['pm_error_user_not_found'], $name);
1282
					}
1283
				}
1284
			}
1285
		}
1286 2
1287
		// Did they make any mistakes like no subject or message?
1288
		if ($this->_req->post->subject === '')
1289
		{
1290
			$post_errors->addError('no_subject');
1291 2
		}
1292
1293
		if (!isset($this->_req->post->message) || $this->_req->post->message === '')
1294
		{
1295 2
			$post_errors->addError('no_message');
1296
		}
1297
		elseif (!empty($modSettings['max_messageLength']) && Util::strlen($this->_req->post->message) > $modSettings['max_messageLength'])
1298
		{
1299
			$post_errors->addError('long_message');
1300
		}
1301
		else
1302 2
		{
1303 2
			// Preparse the message.
1304
			$message = $this->_req->post->message;
1305
			preparsecode($message);
1306 2
1307
			// Make sure there's still some content left without the tags.
1308
			if (Util::htmltrim(strip_tags($bbc_parser->parsePM(Util::htmlspecialchars($message, ENT_QUOTES)), '<img>')) === '' && (!allowedTo('admin_forum') || strpos($message, '[html]') === false))
1309
			{
1310
				$post_errors->addError('no_message');
1311
			}
1312
		}
1313 2
1314
		// If they made any errors, give them a chance to make amends.
1315
		if ($post_errors->hasErrors() && !$is_recipient_change && !isset($this->_req->query->preview) && !isset($this->_req->query->xml))
1316
		{
1317
			$this->messagePostError($namedRecipientList, $recipientList);
1318
1319
			return false;
1320
		}
1321 2
1322
		// Want to take a second glance before you send?
1323
		if (isset($this->_req->query->preview))
1324
		{
1325
			// Set everything up to be displayed.
1326
			$context['preview_subject'] = Util::htmlspecialchars($this->_req->post->subject);
1327
			$context['preview_message'] = Util::htmlspecialchars($this->_req->post->message, ENT_QUOTES, 'UTF-8', true);
1328
			preparsecode($context['preview_message'], true);
1329
1330
			// Parse out the BBC if it is enabled.
1331
			$context['preview_message'] = $bbc_parser->parsePM($context['preview_message']);
1332
1333
			// Censor, as always.
1334
			$context['preview_subject'] = censor($context['preview_subject']);
1335
			$context['preview_message'] = censor($context['preview_message']);
1336
1337
			// Set a descriptive title.
1338
			$context['page_title'] = $txt['preview'] . ' - ' . $context['preview_subject'];
1339
1340
			// Pretend they messed up but don't ignore if they really did :P.
1341
			$this->messagePostError($namedRecipientList, $recipientList);
1342
1343
			return false;
1344 2
		}
1345
		// Adding a recipient cause javascript ain't working?
1346
		elseif ($is_recipient_change)
1347
		{
1348
			// Maybe we couldn't find one?
1349
			foreach ($namesNotFound as $recipientType => $names)
1350
			{
1351
				$post_errors->addError('bad_' . $recipientType);
1352
				foreach ($names as $name)
1353
				{
1354
					$context['send_log']['failed'][] = sprintf($txt['pm_error_user_not_found'], $name);
1355
				}
1356
			}
1357
1358
			$this->messagePostError($namedRecipientList, $recipientList);
1359
1360
			return true;
1361
		}
1362
1363 2
		try
1364
		{
1365
			$this->_events->trigger('before_sending', array('namedRecipientList' => $namedRecipientList, 'recipientList' => $recipientList, 'namesNotFound' => $namesNotFound, 'post_errors' => $post_errors));
1366
		}
1367
		catch (ControllerRedirectException $e)
1368
		{
1369
			return $this->messagePostError($namedRecipientList, $recipientList);
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->messagePostError(...ntList, $recipientList) targeting ElkArte\Controller\Perso...age::messagePostError() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
1370
		}
1371 2
1372
		// Safety net, it may be a module may just add to the list of errors without actually throw the error
1373
		if ($post_errors->hasErrors() && !isset($this->_req->query->preview) && !isset($this->_req->query->xml))
1374
		{
1375
			$this->messagePostError($namedRecipientList, $recipientList);
1376
1377
			return false;
1378
		}
1379 2
1380
		// Before we send the PM, let's make sure we don't have an abuse of numbers.
1381
		if (!empty($modSettings['max_pm_recipients']) && count($recipientList['to']) + count($recipientList['bcc']) > $modSettings['max_pm_recipients'] && !allowedTo(array('moderate_forum', 'send_mail', 'admin_forum')))
1382
		{
1383
			$context['send_log'] = array(
1384
				'sent' => array(),
1385
				'failed' => array(sprintf($txt['pm_too_many_recipients'], $modSettings['max_pm_recipients'])),
1386
			);
1387
1388
			$this->messagePostError($namedRecipientList, $recipientList);
1389
1390
			return false;
1391
		}
1392 2
1393
		// Protect from message spamming.
1394
		spamProtection('pm');
1395 2
1396
		// Prevent double submission of this form.
1397
		checkSubmitOnce('check');
1398 2
1399
		// Finally do the actual sending of the PM.
1400 2
		if (!empty($recipientList['to']) || !empty($recipientList['bcc']))
1401
		{
1402
			$context['send_log'] = sendpm($recipientList, $this->_req->post->subject, $this->_req->post->message, true, null, !empty($this->_req->post->pm_head) ? (int) $this->_req->post->pm_head : 0);
1403
		}
1404
		else
1405
		{
1406
			$context['send_log'] = array(
1407
				'sent' => array(),
1408
				'failed' => array()
1409
			);
1410
		}
1411 2
1412
		// Mark the message as "replied to".
1413
		if (!empty($context['send_log']['sent']) && !empty($this->_req->post->replied_to) && $this->_req->getQuery('f') === 'inbox')
1414
		{
1415
			require_once(SUBSDIR . '/PersonalMessage.subs.php');
1416
			setPMRepliedStatus($this->user->id, (int) $this->_req->post->replied_to);
1417 2
		}
1418 2
1419
		$failed = !empty($context['send_log']['failed']);
1420
		$this->_events->trigger('message_sent', array('failed' => $failed));
1421 2
1422
		// If one or more of the recipients were invalid, go back to the post screen with the failed usernames.
1423
		if ($failed)
1424
		{
1425
			$this->messagePostError($namesNotFound, array(
1426
				'to' => array_intersect($recipientList['to'], $context['send_log']['failed']),
1427
				'bcc' => array_intersect($recipientList['bcc'], $context['send_log']['failed'])
1428
			));
1429
1430
			return false;
1431
		}
1432
		// Message sent successfully?
1433 2
		else
1434
		{
1435
			$context['current_label_redirect'] .= ';done=sent';
1436
		}
1437 2
1438
		// Go back to the where they sent from, if possible...
1439 2
		redirectexit($context['current_label_redirect']);
1440
1441
		return true;
1442
	}
1443
1444
	/**
1445
	 * This function performs all additional actions including the deleting
1446
	 * and labeling of PM's
1447
	 */
1448
	public function action_pmactions()
1449
	{
1450
		global $context;
1451
1452
		checkSession('request');
1453
1454
		// Sending in the single pm choice via GET
1455
		$pm_actions = $this->_req->getQuery('pm_actions', null, '');
1456
1457
		// Set the action to apply to the pm's defined by pm_actions (yes its that brilliant)
1458
		$pm_action = $this->_req->getPost('pm_action', 'trim', '');
1459
		$pm_action = empty($pm_action) && isset($this->_req->post->del_selected) ? 'delete' : $pm_action;
1460
1461
		// Create a list of pm's that we need to work on
1462
		if ($pm_action != ''
1463
			&& !empty($this->_req->post->pms)
1464
			&& is_array($this->_req->post->pms))
1465
		{
1466
			$pm_actions = array();
1467
			foreach ($this->_req->post->pms as $pm)
1468
			{
1469
				$pm_actions[(int) $pm] = $pm_action;
1470
			}
1471
		}
1472
1473
		// No messages to action then bug out
1474
		if (empty($pm_actions))
1475
		{
1476
			redirectexit($context['current_label_redirect']);
1477
		}
1478
1479
		// If we are in conversation, we may need to apply this to every message in that conversation.
1480
		if ($context['display_mode'] == 2 && isset($this->_req->query->conversation))
1481
		{
1482
			$id_pms = array_map('intval', array_keys($pm_actions));
1483
			$pm_heads = getDiscussions($id_pms);
1484
			$pms = getPmsFromDiscussion(array_keys($pm_heads));
1485
1486
			// Copy the action from the single to PM to the others in the conversation.
1487
			foreach ($pms as $id_pm => $id_head)
1488
			{
1489
				if (isset($pm_heads[$id_head]) && isset($pm_actions[$pm_heads[$id_head]]))
1490
				{
1491
					$pm_actions[$id_pm] = $pm_actions[$pm_heads[$id_head]];
1492
				}
1493
			}
1494
		}
1495
1496
		// Lets get to doing what we've been told
1497
		$to_delete = array();
1498
		$to_label = array();
1499
		$label_type = array();
1500
		foreach ($pm_actions as $pm => $action)
1501
		{
1502
			// What are we doing with the selected messages, adding a label, removing, other?
1503
			switch (substr($action, 0, 4))
1504
			{
1505
				case 'dele':
1506
					$to_delete[] = (int) $pm;
1507
					break;
1508
				case 'add_':
1509
					$type = 'add';
1510
					$action = substr($action, 4);
1511
					break;
1512
				case 'rem_':
1513
					$type = 'rem';
1514
					$action = substr($action, 4);
1515
					break;
1516
				default:
1517
					$type = 'unk';
1518
			}
1519
1520
			if ($action === '-1' || $action === '0' || (int) $action > 0)
1521
			{
1522
				$to_label[(int) $pm] = (int) $action;
1523
				$label_type[(int) $pm] = $type;
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $type does not seem to be defined for all execution paths leading up to this point.
Loading history...
1524
			}
1525
		}
1526
1527
		// Deleting, it looks like?
1528
		if (!empty($to_delete))
1529
		{
1530
			deleteMessages($to_delete, $context['display_mode'] == 2 ? null : $context['folder']);
1531
		}
1532
1533
		// Are we labelling anything?
1534
		if (!empty($to_label) && $context['folder'] === 'inbox')
1535
		{
1536
			$updateErrors = changePMLabels($to_label, $label_type, $this->user->id);
1537
1538
			// Any errors?
1539
			if (!empty($updateErrors))
1540
			{
1541
				throw new Exception('labels_too_many', true, array($updateErrors));
1542
			}
1543
		}
1544
1545
		// Back to the folder.
1546
		$_SESSION['pm_selected'] = array_keys($to_label);
1547
		redirectexit($context['current_label_redirect'] . (count($to_label) === 1 ? '#msg_' . $_SESSION['pm_selected'][0] : ''));
1548
	}
1549
1550
	/**
1551
	 * Are you sure you want to PERMANENTLY (mostly) delete ALL your messages?
1552
	 */
1553
	public function action_removeall()
1554
	{
1555
		global $txt, $context;
1556
1557
		// Only have to set up the template....
1558
		$context['sub_template'] = 'ask_delete';
1559
		$context['page_title'] = $txt['delete_all'];
1560
		$context['delete_all'] = $this->_req->query->f === 'all';
1561
1562
		// And set the folder name...
1563
		$txt['delete_all'] = str_replace('PMBOX', $context['folder'] != 'sent' ? $txt['inbox'] : $txt['sent_items'], $txt['delete_all']);
1564
	}
1565
1566
	/**
1567
	 * Delete ALL the messages!
1568
	 */
1569
	public function action_removeall2()
1570
	{
1571
		global $context;
1572
1573
		checkSession('get');
1574
1575
		// If all then delete all messages the user has.
1576
		if ($this->_req->query->f === 'all')
1577
		{
1578
			deleteMessages(null, null);
1579
		}
1580
		// Otherwise just the selected folder.
1581
		else
1582
		{
1583
			deleteMessages(null, $this->_req->query->f != 'sent' ? 'inbox' : 'sent');
1584
		}
1585
1586
		// Done... all gone.
1587
		redirectexit($context['current_label_redirect']);
1588
	}
1589
1590
	/**
1591
	 * This function allows the user to prune (delete) all messages older than a supplied duration.
1592
	 */
1593
	public function action_prune()
1594
	{
1595
		global $txt, $context;
1596
1597
		// Actually delete the messages.
1598
		if (isset($this->_req->post->age))
1599
		{
1600
			checkSession();
1601
1602
			// Calculate the time to delete before.
1603
			$deleteTime = max(0, time() - (86400 * (int) $this->_req->post->age));
1604
1605
			// Select all the messages older than $deleteTime.
1606
			$toDelete = getPMsOlderThan($this->user->id, $deleteTime);
1607
1608
			// Delete the actual messages.
1609
			deleteMessages($toDelete);
1610
1611
			// Go back to their inbox.
1612
			redirectexit($context['current_label_redirect']);
1613
		}
1614
1615
		// Build the link tree elements.
1616
		$context['linktree'][] = array(
1617
			'url' => getUrl('action', ['action' => 'pm', 'sa' => 'prune']),
1618
			'name' => $txt['pm_prune']
1619
		);
1620
		$context['sub_template'] = 'prune';
1621
		$context['page_title'] = $txt['pm_prune'];
1622
	}
1623
1624
	/**
1625
	 * This function handles adding, deleting and editing labels on messages.
1626
	 */
1627
	public function action_manlabels()
1628
	{
1629
		global $txt, $context;
1630
1631
		require_once(SUBSDIR . '/PersonalMessage.subs.php');
1632
1633
		// Build the link tree elements...
1634
		$context['linktree'][] = array(
1635
			'url' => getUrl('action', ['action' => 'pm', 'sa' => 'manlabels']),
1636
			'name' => $txt['pm_manage_labels']
1637
		);
1638
1639
		// Some things for the template
1640
		$context['page_title'] = $txt['pm_manage_labels'];
1641
		$context['sub_template'] = 'labels';
1642
1643
		// Add all existing labels to the array to save, slashing them as necessary...
1644
		$the_labels = array();
1645
		foreach ($context['labels'] as $label)
1646
		{
1647
			if ($label['id'] != -1)
1648
			{
1649
				$the_labels[$label['id']] = $label['name'];
1650
			}
1651
		}
1652
1653
		// Submitting changes?
1654
		if (isset($this->_req->post->add) || isset($this->_req->post->delete) || isset($this->_req->post->save))
1655
		{
1656
			checkSession('post');
1657
1658
			// This will be for updating messages.
1659
			$message_changes = array();
1660
			$new_labels = array();
1661
			$rule_changes = array();
1662
1663
			// Will most likely need this.
1664
			loadRules();
1665
1666
			// Adding a new label?
1667
			if (isset($this->_req->post->add))
1668
			{
1669
				$this->_req->post->label = strtr(Util::htmlspecialchars(trim($this->_req->post->label)), array(',' => '&#044;'));
1670
1671
				if (Util::strlen($this->_req->post->label) > 30)
1672
				{
1673
					$this->_req->post->label = Util::substr($this->_req->post->label, 0, 30);
1674
				}
1675
				if ($this->_req->post->label !== '')
1676
				{
1677
					$the_labels[] = $this->_req->post->label;
1678
				}
1679
			}
1680
			// Deleting an existing label?
1681
			elseif (isset($this->_req->post->delete, $this->_req->post->delete_label))
1682
			{
1683
				$i = 0;
1684
				foreach ($the_labels as $id => $name)
1685
				{
1686
					if (isset($this->_req->post->delete_label[$id]))
1687
					{
1688
						unset($the_labels[$id]);
1689
						$message_changes[$id] = true;
1690
					}
1691
					else
1692
					{
1693
						$new_labels[$id] = $i++;
1694
					}
1695
				}
1696
			}
1697
			// The hardest one to deal with... changes.
1698
			elseif (isset($this->_req->post->save) && !empty($this->_req->post->label_name))
1699
			{
1700
				$i = 0;
1701
				foreach ($the_labels as $id => $name)
1702
				{
1703
					if ($id == -1)
1704
					{
1705
						continue;
1706
					}
1707
					elseif (isset($this->_req->post->label_name[$id]))
1708
					{
1709
						// Prepare the label name
1710
						$this->_req->post->label_name[$id] = trim(strtr(Util::htmlspecialchars($this->_req->post->label_name[$id]), array(',' => '&#044;')));
1711
1712
						// Has to fit in the database as well
1713
						if (Util::strlen($this->_req->post->label_name[$id]) > 30)
1714
						{
1715
							$this->_req->post->label_name[$id] = Util::substr($this->_req->post->label_name[$id], 0, 30);
1716
						}
1717
1718
						if ($this->_req->post->label_name[$id] != '')
1719
						{
1720
							$the_labels[(int) $id] = $this->_req->post->label_name[$id];
1721
							$new_labels[$id] = $i++;
1722
						}
1723
						else
1724
						{
1725
							unset($the_labels[(int) $id]);
1726
							$message_changes[(int) $id] = true;
1727
						}
1728
					}
1729
					else
1730
					{
1731
						$new_labels[$id] = $i++;
1732
					}
1733
				}
1734
			}
1735
1736
			// Save the label status.
1737
			require_once(SUBSDIR . '/Members.subs.php');
1738
			updateMemberData($this->user->id, array('message_labels' => implode(',', $the_labels)));
1739
1740
			// Update all the messages currently with any label changes in them!
1741
			if (!empty($message_changes))
1742
			{
1743
				$searchArray = array_keys($message_changes);
1744
1745
				if (!empty($new_labels))
1746
				{
1747
					for ($i = max($searchArray) + 1, $n = max(array_keys($new_labels)); $i <= $n; $i++)
1748
					{
1749
						$searchArray[] = $i;
1750
					}
1751
				}
1752
1753
				updateLabelsToPM($searchArray, $new_labels, $this->user->id);
1754
1755
				// Now do the same the rules - check through each rule.
1756
				foreach ($context['rules'] as $k => $rule)
1757
				{
1758
					// Each action...
1759
					foreach ($rule['actions'] as $k2 => $action)
1760
					{
1761
						if ($action['t'] != 'lab' || !in_array($action['v'], $searchArray))
1762
						{
1763
							continue;
1764
						}
1765
1766
						$rule_changes[] = $rule['id'];
1767
1768
						// If we're here we have a label which is either changed or gone...
1769
						if (isset($new_labels[$action['v']]))
1770
						{
1771
							$context['rules'][$k]['actions'][$k2]['v'] = $new_labels[$action['v']];
1772
						}
1773
						else
1774
						{
1775
							unset($context['rules'][$k]['actions'][$k2]);
1776
						}
1777
					}
1778
				}
1779
			}
1780
1781
			// If we have rules to change do so now.
1782
			if (!empty($rule_changes))
1783
			{
1784
				$rule_changes = array_unique($rule_changes);
1785
1786
				// Update/delete as appropriate.
1787
				foreach ($rule_changes as $k => $id)
1788
				{
1789
					if (!empty($context['rules'][$id]['actions']))
1790
					{
1791
						updatePMRuleAction($id, $this->user->id, $context['rules'][$id]['actions']);
1792
						unset($rule_changes[$k]);
1793
					}
1794
				}
1795
1796
				// Anything left here means it's lost all actions...
1797
				if (!empty($rule_changes))
1798
				{
1799
					deletePMRules($this->user->id, $rule_changes);
1800
				}
1801
			}
1802
1803
			// Make sure we're not caching this!
1804
			Cache::instance()->remove('labelCounts:' . $this->user->id);
1805
1806
			// To make the changes appear right away, redirect.
1807
			redirectexit('action=pm;sa=manlabels');
1808
		}
1809
	}
1810
1811
	/**
1812
	 * Allows to edit Personal Message Settings.
1813
	 *
1814
	 * @uses ProfileOptions controller. (@todo refactor this.)
1815
	 * @uses Profile template.
1816
	 * @uses Profile language file.
1817
	 */
1818
	public function action_settings()
1819
	{
1820
		global $txt, $context, $profile_vars, $cur_profile;
1821
1822
		require_once(SUBSDIR . '/Profile.subs.php');
1823
1824
		// Load the member data for editing
1825
		MembersList::load($this->user->id, false, 'profile');
1826
		$cur_profile = MembersList::get($this->user->id);
1827
1828
		// Load up the profile template, its where PM settings are located
1829
		Txt::load('Profile');
1830
		theme()->getTemplates()->load('Profile');
1831
1832
		// We want them to submit back to here.
1833
		$context['profile_custom_submit_url'] = getUrl('action', ['action' => 'pm', 'sa' => 'settings', 'save']);
1834
1835
		$context['page_title'] = $txt['pm_settings'];
1836
		$context['user']['is_owner'] = true;
1837
		$context['id_member'] = $this->user->id;
1838
		$context['require_password'] = false;
1839
		$context['menu_item_selected'] = 'settings';
1840
		$context['submit_button_text'] = $txt['pm_settings'];
1841
1842
		// Add our position to the linktree.
1843
		$context['linktree'][] = array(
1844
			'url' => getUrl('action', ['action' => 'pm', 'sa' => 'settings']),
1845
			'name' => $txt['pm_settings']
1846
		);
1847
1848
		// Are they saving?
1849
		if (isset($this->_req->post->save))
1850
		{
1851
			checkSession('post');
1852
1853
			// Mimic what profile would do.
1854
			// @todo fix this when Profile.subs is not dependant on this behavior
1855
			$_POST = Util::htmltrim__recursive((array) $this->_req->post);
1856
			$_POST = Util::htmlspecialchars__recursive($_POST);
1857
1858
			// Save the fields.
1859
			$fields = ProfileOptions::getFields('contactprefs');
1860
			saveProfileFields($fields['fields'], $fields['hook']);
1861
1862
			if (!empty($profile_vars))
1863
			{
1864
				require_once(SUBSDIR . '/Members.subs.php');
1865
				updateMemberData($this->user->id, $profile_vars);
1866
			}
1867
1868
			// Invalidate any cached data and reload so we show the saved values
1869
			Cache::instance()->remove('member_data-profile-' . $this->user->id);
1870
			MembersList::load($this->user->id, false, 'profile');
1871
			$cur_profile = MembersList::get($this->user->id);
1872
		}
1873
1874
		// Load up the fields.
1875
		$controller = new ProfileOptions(new EventManager());
1876
		$controller->setUser(User::$info);
1877
		$controller->pre_dispatch();
1878
		$controller->action_pmprefs();
1879
	}
1880
1881
	/**
1882
	 * Allows the user to report a personal message to an administrator.
1883
	 *
1884
	 * What it does:
1885
	 *
1886
	 * - In the first instance requires that the ID of the message to report is passed through $_GET.
1887
	 * - It allows the user to report to either a particular administrator - or the whole admin team.
1888
	 * - It will forward on a copy of the original message without allowing the reporter to make changes.
1889
	 *
1890
	 * @uses report_message sub-template.
1891
	 */
1892
	public function action_report()
1893
	{
1894
		global $txt, $context, $language, $modSettings;
1895
1896
		// Check that this feature is even enabled!
1897
		if (empty($modSettings['enableReportPM']) || empty($this->_req->getPost('pmsg', 'intval', $this->_req->getQuery('pmsg', 'intval', 0))))
1898
		{
1899
			throw new Exception('no_access', false);
1900
		}
1901
1902
		$pmsg = $this->_req->getQuery('pmsg', 'intval', $this->_req->getPost('pmsg', 'intval', 0));
1903
1904
		if (!isAccessiblePM($pmsg, 'inbox'))
1905
		{
1906
			throw new Exception('no_access', false);
1907
		}
1908
1909
		$context['pm_id'] = $pmsg;
1910
		$context['page_title'] = $txt['pm_report_title'];
1911
		$context['sub_template'] = 'report_message';
1912
1913
		// We'll query some members, we will.
1914
		require_once(SUBSDIR . '/Members.subs.php');
1915
1916
		// If we're here, just send the user to the template, with a few useful context bits.
1917
		if (isset($this->_req->post->report))
1918
		{
1919
			$poster_comment = strtr(Util::htmlspecialchars($this->_req->post->reason), array("\r" => '', "\t" => ''));
1920
1921
			if (Util::strlen($poster_comment) > 254)
1922
			{
1923
				throw new Exception('post_too_long', false);
1924
			}
1925
1926
			// Check the session before proceeding any further!
1927
			checkSession('post');
1928
1929
			// First, load up the message they want to file a complaint against, and verify it actually went to them!
1930
			list ($subject, $body, $time, $memberFromID, $memberFromName, $poster_name, $time_message) = loadPersonalMessage($pmsg);
1931
1932
			require_once(SUBSDIR . '/Messages.subs.php');
1933
1934
			recordReport(array(
1935
				'id_msg' => $pmsg,
1936
				'id_topic' => 0,
1937
				'id_board' => 0,
1938
				'type' => 'pm',
1939
				'id_poster' => $memberFromID,
1940
				'real_name' => $memberFromName,
1941
				'poster_name' => $poster_name,
1942
				'subject' => $subject,
1943
				'body' => $body,
1944
				'time_message' => $time_message,
1945
			), $poster_comment);
1946
1947
			// Remove the line breaks...
1948
			$body = preg_replace('~<br ?/?' . '>~i', "\n", $body);
1949
1950
			$recipients = array();
1951
			$temp = loadPMRecipientsAll($context['pm_id'], true);
1952
			foreach ($temp as $recipient)
1953
			{
1954
				$recipients[] = $recipient['link'];
1955
			}
1956
1957
			// Now let's get out and loop through the admins.
1958
			$admins = admins(isset($this->_req->post->id_admin) ? (int) $this->_req->post->id_admin : 0);
1959
1960
			// Maybe we shouldn't advertise this?
1961
			if (empty($admins))
1962
			{
1963
				throw new Exception('no_access', false);
1964
			}
1965
1966
			$memberFromName = un_htmlspecialchars($memberFromName);
1967
1968
			// Prepare the message storage array.
1969
			$messagesToSend = array();
1970
1971
			// Loop through each admin, and add them to the right language pile...
1972
			foreach ($admins as $id_admin => $admin_info)
1973
			{
1974
				// Need to send in the correct language!
1975
				$cur_language = empty($admin_info['lngfile']) || empty($modSettings['userLanguage']) ? $language : $admin_info['lngfile'];
1976
1977
				if (!isset($messagesToSend[$cur_language]))
1978
				{
1979
					$mtxt = [];
1980
					$lang = new Loader($cur_language, $mtxt);
1981
					$lang->load('PersonalMessage', false);
1982
1983
					// Make the body.
1984
					$report_body = str_replace(array('{REPORTER}', '{SENDER}'), array(un_htmlspecialchars($this->user->name), $memberFromName), $mtxt['pm_report_pm_user_sent']);
1985
					$report_body .= "\n" . '[b]' . $this->_req->post->reason . '[/b]' . "\n\n";
1986
					if (!empty($recipients))
1987
					{
1988
						$report_body .= $mtxt['pm_report_pm_other_recipients'] . ' ' . implode(', ', $recipients) . "\n\n";
1989
					}
1990
					$report_body .= $mtxt['pm_report_pm_unedited_below'] . "\n" . '[quote author=' . (empty($memberFromID) ? '&quot;' . $memberFromName . '&quot;' : $memberFromName . ' link=action=profile;u=' . $memberFromID . ' date=' . $time) . ']' . "\n" . un_htmlspecialchars($body) . '[/quote]';
1991
1992
					// Plonk it in the array ;)
1993
					$messagesToSend[$cur_language] = array(
1994
						'subject' => (Util::strpos($subject, $mtxt['pm_report_pm_subject']) === false ? $mtxt['pm_report_pm_subject'] : '') . un_htmlspecialchars($subject),
1995
						'body' => $report_body,
1996
						'recipients' => array(
1997
							'to' => array(),
1998
							'bcc' => array()
1999
						),
2000
					);
2001
				}
2002
2003
				// Add them to the list.
2004
				$messagesToSend[$cur_language]['recipients']['to'][$id_admin] = $id_admin;
2005
			}
2006
2007
			// Send a different email for each language.
2008
			foreach ($messagesToSend as $lang => $message)
2009
			{
2010
				sendpm($message['recipients'], $message['subject'], $message['body']);
2011
			}
2012
2013
			// Leave them with a template.
2014
			$context['sub_template'] = 'report_message_complete';
2015
		}
2016
	}
2017
2018
	/**
2019
	 * List and allow adding/entering all man rules, such as
2020
	 *
2021
	 * What it does:
2022
	 *
2023
	 * - If it itches, it will be scratched.
2024
	 * - Yes or No are perfectly acceptable answers to almost every question.
2025
	 * - Men see in only 16 colors, Peach, for example, is a fruit, not a color.
2026
	 *
2027
	 * @uses sub template rules
2028
	 */
2029
	public function action_manrules()
2030
	{
2031
		global $txt, $context;
2032
2033
		require_once(SUBSDIR . '/PersonalMessage.subs.php');
2034
2035
		// The link tree - gotta have this :o
2036
		$context['linktree'][] = array(
2037
			'url' => getUrl('action', ['action' => 'pm', 'sa' => 'manrules']),
2038
			'name' => $txt['pm_manage_rules']
2039
		);
2040
2041
		$context['page_title'] = $txt['pm_manage_rules'];
2042
		$context['sub_template'] = 'rules';
2043
2044
		// Load them... load them!!
2045
		loadRules();
2046
2047
		// Likely to need all the groups!
2048
		require_once(SUBSDIR . '/Membergroups.subs.php');
2049
		$context['groups'] = accessibleGroups();
2050
2051
		// Applying all rules?
2052
		if (isset($this->_req->query->apply))
2053
		{
2054
			checkSession('get');
2055
2056
			applyRules(true);
2057
			redirectexit('action=pm;sa=manrules');
2058
		}
2059
2060
		// Editing a specific rule?
2061
		if (isset($this->_req->query->add))
2062
		{
2063
			$rid = $this->_req->getQuery('rid', 'intval', 0);
2064
			$context['rid'] = isset($context['rules'][$rid]) ? $rid : 0;
2065
			$context['sub_template'] = 'add_rule';
2066
2067
			// Any known rule
2068
			$js_rules = [];
2069
			foreach ($context['known_rules'] as $rule)
2070
			{
2071
				$js_rules[$rule] = $txt['pm_rule_' . $rule];
2072
			}
2073
			$js_rules = json_encode($js_rules);
2074
2075
			// Any known label
2076
			$js_labels = [];
2077
			foreach ($context['labels'] as $label)
2078
			{
2079
				if ($label['id'] != -1)
2080
				{
2081
					$js_labels[$label['id'] + 1] = $label['name'];
2082
				}
2083
			}
2084
			$js_labels = json_encode($js_labels);
2085
2086
			// And all of the groups as well
2087
			$js_groups = json_encode($context['groups']);
2088
2089
			// Oh my, we have a lot of text strings for this
2090
			theme()->addJavascriptVar(array(
2091
				'criteriaNum' => 0,
2092
				'actionNum' => 0,
2093
				'groups' => $js_groups,
2094
				'labels' => $js_labels,
2095
				'rules' => $js_rules,
2096
				'txt_pm_readable_and' => $txt['pm_readable_and'],
2097
				'txt_pm_readable_or' => $txt['pm_readable_or'],
2098
				'txt_pm_readable_member' => $txt['pm_readable_member'],
2099
				'txt_pm_readable_group' => $txt['pm_readable_group'],
2100
				'txt_pm_readable_subject ' => $txt['pm_readable_subject'],
2101
				'txt_pm_readable_body' => $txt['pm_readable_body'],
2102
				'txt_pm_readable_buddy' => $txt['pm_readable_buddy'],
2103
				'txt_pm_readable_label' => $txt['pm_readable_label'],
2104
				'txt_pm_readable_delete' => $txt['pm_readable_delete'],
2105
				'txt_pm_readable_start' => $txt['pm_readable_start'],
2106
				'txt_pm_readable_end' => $txt['pm_readable_end'],
2107
				'txt_pm_readable_then' => $txt['pm_readable_then'],
2108
				'txt_pm_rule_not_defined' => $txt['pm_rule_not_defined'],
2109
				'txt_pm_rule_criteria_pick' => $txt['pm_rule_criteria_pick'],
2110
				'txt_pm_rule_sel_group' => $txt['pm_rule_sel_group'],
2111
				'txt_pm_rule_sel_action' => $txt['pm_rule_sel_action'],
2112
				'txt_pm_rule_label' => $txt['pm_rule_label'],
2113
				'txt_pm_rule_delete' => $txt['pm_rule_delete'],
2114
				'txt_pm_rule_sel_label' => $txt['pm_rule_sel_label'],
2115
			), true);
2116
2117
			// Current rule information...
2118
			if ($context['rid'])
2119
			{
2120
				$context['rule'] = $context['rules'][$context['rid']];
2121
				$members = array();
2122
2123
				// Need to get member names!
2124
				foreach ($context['rule']['criteria'] as $k => $criteria)
2125
				{
2126
					if ($criteria['t'] === 'mid' && !empty($criteria['v']))
2127
					{
2128
						$members[(int) $criteria['v']] = $k;
2129
					}
2130
				}
2131
2132
				if (!empty($members))
2133
				{
2134
					require_once(SUBSDIR . '/Members.subs.php');
2135
					$result = getBasicMemberData(array_keys($members));
2136
					foreach ($result as $row)
2137
					{
2138
						$context['rule']['criteria'][$members[$row['id_member']]]['v'] = $row['member_name'];
2139
					}
2140
				}
2141
			}
2142
			else
2143
			{
2144
				$context['rule'] = array(
2145
					'id' => '',
2146
					'name' => '',
2147
					'criteria' => array(),
2148
					'actions' => array(),
2149
					'logic' => 'and',
2150
				);
2151
			}
2152
2153
			// Add a dummy criteria to allow expansion for none js users.
2154
			$context['rule']['criteria'][] = array('t' => '', 'v' => '');
2155
		}
2156
		// Saving?
2157
		elseif (isset($this->_req->query->save))
2158
		{
2159
			checkSession('post');
2160
			$rid = $this->_req->getQuery('rid', 'intval', 0);
2161
			$context['rid'] = isset($context['rules'][$rid]) ? $rid : 0;
2162
2163
			// Name is easy!
2164
			$ruleName = Util::htmlspecialchars(trim($this->_req->post->rule_name));
2165
			if (empty($ruleName))
2166
			{
2167
				throw new Exception('pm_rule_no_name', false);
2168
			}
2169
2170
			// Sanity check...
2171
			if (empty($this->_req->post->ruletype) || empty($this->_req->post->acttype))
2172
			{
2173
				throw new Exception('pm_rule_no_criteria', false);
2174
			}
2175
2176
			// Let's do the criteria first - it's also hardest!
2177
			$criteria = array();
2178
			foreach ($this->_req->post->ruletype as $ind => $type)
2179
			{
2180
				// Check everything is here...
2181
				if ($type === 'gid' && (!isset($this->_req->post->ruledefgroup[$ind]) || !isset($context['groups'][$this->_req->post->ruledefgroup[$ind]])))
2182
				{
2183
					continue;
2184
				}
2185
				elseif ($type != 'bud' && !isset($this->_req->post->ruledef[$ind]))
2186
				{
2187
					continue;
2188
				}
2189
2190
				// Members need to be found.
2191
				if ($type === 'mid')
2192
				{
2193
					require_once(SUBSDIR . '/Members.subs.php');
2194
					$name = trim($this->_req->post->ruledef[$ind]);
2195
					$member = getMemberByName($name, true);
2196
					if (empty($member))
2197
					{
2198
						continue;
2199
					}
2200
2201
					$criteria[] = array('t' => 'mid', 'v' => $member['id_member']);
2202
				}
2203
				elseif ($type === 'bud')
2204
				{
2205
					$criteria[] = array('t' => 'bud', 'v' => 1);
2206
				}
2207
				elseif ($type === 'gid')
2208
				{
2209
					$criteria[] = array('t' => 'gid', 'v' => (int) $this->_req->post->ruledefgroup[$ind]);
2210
				}
2211
				elseif (in_array($type, array('sub', 'msg')) && trim($this->_req->post->ruledef[$ind]) !== '')
2212
				{
2213
					$criteria[] = array('t' => $type, 'v' => Util::htmlspecialchars(trim($this->_req->post->ruledef[$ind])));
2214
				}
2215
			}
2216
2217
			// Also do the actions!
2218
			$actions = array();
2219
			$doDelete = 0;
2220
			$isOr = $this->_req->post->rule_logic === 'or' ? 1 : 0;
2221
			foreach ($this->_req->post->acttype as $ind => $type)
2222
			{
2223
				// Picking a valid label?
2224
				if ($type === 'lab' && (!isset($this->_req->post->labdef[$ind]) || !isset($context['labels'][$this->_req->post->labdef[$ind] - 1])))
2225
				{
2226
					continue;
2227
				}
2228
2229
				// Record what we're doing.
2230
				if ($type === 'del')
2231
				{
2232
					$doDelete = 1;
2233
				}
2234
				elseif ($type === 'lab')
2235
				{
2236
					$actions[] = array('t' => 'lab', 'v' => (int) $this->_req->post->labdef[$ind] - 1);
2237
				}
2238
			}
2239
2240
			if (empty($criteria) || (empty($actions) && !$doDelete))
2241
			{
2242
				throw new Exception('pm_rule_no_criteria', false);
2243
			}
2244
2245
			// What are we storing?
2246
			$criteria = serialize($criteria);
2247
			$actions = serialize($actions);
2248
2249
			// Create the rule?
2250
			if (empty($context['rid']))
2251
			{
2252
				addPMRule($this->user->id, $ruleName, $criteria, $actions, $doDelete, $isOr);
2253
			}
2254
			else
2255
			{
2256
				updatePMRule($this->user->id, $context['rid'], $ruleName, $criteria, $actions, $doDelete, $isOr);
2257
			}
2258
2259
			redirectexit('action=pm;sa=manrules');
2260
		}
2261
		// Deleting?
2262
		elseif (isset($this->_req->post->delselected) && !empty($this->_req->post->delrule))
2263
		{
2264
			checkSession('post');
2265
			$toDelete = array();
2266
			foreach ($this->_req->post->delrule as $k => $v)
2267
			{
2268
				$toDelete[] = (int) $k;
2269
			}
2270
2271
			if (!empty($toDelete))
2272
			{
2273
				deletePMRules($this->user->id, $toDelete);
2274
			}
2275
2276
			redirectexit('action=pm;sa=manrules');
2277
		}
2278
	}
2279
2280
	/**
2281
	 * Actually do the search of personal messages and show the results
2282
	 *
2283
	 * What it does:
2284
	 *
2285
	 * - accessed with ?action=pm;sa=search2
2286
	 * - checks user input and searches the pm table for messages matching the query.
2287
	 * - uses the search_results sub template of the PersonalMessage template.
2288
	 * - show the results of the search query.
2289
	 */
2290
	public function action_search2()
2291
	{
2292
		global $scripturl, $modSettings, $context, $txt;
2293
2294
		// Make sure the server is able to do this right now
2295
		if (!empty($modSettings['loadavg_search']) && $modSettings['current_load'] >= $modSettings['loadavg_search'])
2296
		{
2297
			throw new Exception('loadavg_search_disabled', false);
2298
		}
2299
2300
		// Some useful general permissions.
2301
		$context['can_send_pm'] = allowedTo('pm_send');
2302
2303
		// Extract all the search parameters if coming in from pagination, etc
2304
		$this->_searchParamsFromString();
2305
2306
		// Set a start for pagination
2307
		$context['start'] = $this->_req->getQuery('start', 'intval', 0);
2308
2309
		// Set/clean search criteria
2310
		$this->_prepareSearchParams();
2311
2312
		$context['folder'] = empty($this->_search_params['sent_only']) ? 'inbox' : 'sent';
2313
2314
		// Searching for specific members
2315
		$userQuery = $this->_setUserQuery();
2316
2317
		// Setup the sorting variables...
2318
		$this->_setSortParams();
2319
2320
		// Sort out any labels we may be searching by.
2321
		$labelQuery = $this->_setLabelQuery();
2322
2323
		// Unfortunately, searching for words like this is going to be slow, so we're blocking them.
2324
		$blocklist_words = array('quote', 'the', 'is', 'it', 'are', 'if', 'in');
2325
2326
		// What are we actually searching for?
2327
		$this->_search_params['search'] = !empty($this->_search_params['search']) ? $this->_search_params['search'] : ($this->_req->post->search ?? '');
2328
2329
		// If nothing is left to search on - we set an error!
2330
		if (!isset($this->_search_params['search']) || $this->_search_params['search'] === '')
2331
		{
2332
			$context['search_errors']['invalid_search_string'] = true;
2333
		}
2334
2335
		// Change non-word characters into spaces.
2336
		$stripped_query = preg_replace('~(?:[\x0B\0\x{A0}\t\r\s\n(){}\\[\\]<>!@$%^*.,:+=`\~\?/\\\\]+|&(?:amp|lt|gt|quot);)+~u', ' ', $this->_search_params['search']);
2337
2338
		// Make the query lower case since it will case insensitive anyway.
2339
		$stripped_query = un_htmlspecialchars(Util::strtolower($stripped_query));
2340
2341
		// Extract phrase parts first (e.g. some words "this is a phrase" some more words.)
2342
		preg_match_all('/(?:^|\s)([-]?)"([^"]+)"(?:$|\s)/', $stripped_query, $matches, PREG_PATTERN_ORDER);
2343
		$phraseArray = $matches[2];
2344
2345
		// Remove the phrase parts and extract the words.
2346
		$wordArray = preg_replace('~(?:^|\s)(?:[-]?)"(?:[^"]+)"(?:$|\s)~u', ' ', $this->_search_params['search']);
2347
		$wordArray = explode(' ', Util::htmlspecialchars(un_htmlspecialchars($wordArray), ENT_QUOTES));
2348
2349
		// A minus sign in front of a word excludes the word.... so...
2350
		$excludedWords = array();
2351
2352
		// Check for things like -"some words", but not "-some words".
2353
		foreach ($matches[1] as $index => $word)
2354
		{
2355
			if ($word === '-')
2356
			{
2357
				if (($word = trim($phraseArray[$index], '-_\' ')) !== '' && !in_array($word, $blocklist_words))
2358
				{
2359
					$excludedWords[] = $word;
2360
				}
2361
				unset($phraseArray[$index]);
2362
			}
2363
		}
2364
2365
		// Now we look for -test, etc
2366
		foreach ($wordArray as $index => $word)
2367
		{
2368
			if (strpos(trim($word), '-') === 0)
2369
			{
2370
				if (($word = trim($word, '-_\' ')) !== '' && !in_array($word, $blocklist_words))
2371
				{
2372
					$excludedWords[] = $word;
2373
				}
2374
				unset($wordArray[$index]);
2375
			}
2376
		}
2377
2378
		// The remaining words and phrases are all included.
2379
		$searchArray = array_merge($phraseArray, $wordArray);
2380
2381
		// Trim everything and make sure there are no words that are the same.
2382
		foreach ($searchArray as $index => $value)
2383
		{
2384
			// Skip anything that's close to empty.
2385
			if (($searchArray[$index] = trim($value, '-_\' ')) === '')
2386
			{
2387
				unset($searchArray[$index]);
2388
			}
2389
			// Skip blocked words. Make sure to note we skipped them as well
2390
			elseif (in_array($searchArray[$index], $blocklist_words))
2391
			{
2392
				$foundBlockListedWords = true;
2393
				unset($searchArray[$index]);
2394
2395
			}
2396
2397
			if (isset($searchArray[$index]))
2398
			{
2399
				$searchArray[$index] = Util::strtolower(trim($value));
2400
2401
				if ($searchArray[$index] === '')
2402
				{
2403
					unset($searchArray[$index]);
2404
				}
2405
				else
2406
				{
2407
					// Sort out entities first.
2408
					$searchArray[$index] = Util::htmlspecialchars($searchArray[$index]);
2409
				}
2410
			}
2411
		}
2412
2413
		$searchArray = array_slice(array_unique($searchArray), 0, 10);
2414
2415
		// This contains *everything*
2416
		$searchWords = array_merge($searchArray, $excludedWords);
2417
2418
		// Make sure at least one word is being searched for.
2419
		if (empty($searchArray))
2420
		{
2421
			$context['search_errors']['invalid_search_string' . (!empty($foundBlockListedWords) ? '_blocklist' : '')] = true;
2422
		}
2423
2424
		// Sort out the search query so the user can edit it - if they want.
2425
		$context['search_params'] = $this->_search_params;
2426
		if (isset($context['search_params']['search']))
2427
		{
2428
			$context['search_params']['search'] = Util::htmlspecialchars($context['search_params']['search']);
2429
		}
2430
2431
		if (isset($context['search_params']['userspec']))
2432
		{
2433
			$context['search_params']['userspec'] = Util::htmlspecialchars($context['search_params']['userspec']);
2434
		}
2435
2436
		// Now we have all the parameters, combine them together for pagination and the like...
2437
		$context['params'] = $this->_compileURLparams();
2438
2439
		// Compile the subject query part.
2440
		$andQueryParts = array();
2441
		foreach ($searchWords as $index => $word)
2442
		{
2443
			if ($word === '')
2444
			{
2445
				continue;
2446
			}
2447
2448
			if ($this->_search_params['subject_only'])
2449
			{
2450
				$andQueryParts[] = 'pm.subject' . (in_array($word, $excludedWords) ? ' NOT' : '') . ' LIKE {string:search_' . $index . '}';
2451
			}
2452
			else
2453
			{
2454
				$andQueryParts[] = '(pm.subject' . (in_array($word, $excludedWords) ? ' NOT' : '') . ' LIKE {string:search_' . $index . '} ' . (in_array($word, $excludedWords) ? 'AND pm.body NOT' : 'OR pm.body') . ' LIKE {string:search_' . $index . '})';
2455
			}
2456
2457
			$this->_searchq_parameters ['search_' . $index] = '%' . strtr($word, array('_' => '\\_', '%' => '\\%')) . '%';
2458
		}
2459
2460
		$searchQuery = ' 1=1';
2461
		if (!empty($andQueryParts))
2462
		{
2463
			$searchQuery = implode(!empty($this->_search_params['searchtype']) && $this->_search_params['searchtype'] == 2 ? ' OR ' : ' AND ', $andQueryParts);
2464
		}
2465
2466
		// Age limits?
2467
		$timeQuery = '';
2468
		if (!empty($this->_search_params['minage']))
2469
		{
2470
			$timeQuery .= ' AND pm.msgtime < ' . (time() - $this->_search_params['minage'] * 86400);
2471
		}
2472
2473
		if (!empty($this->_search_params['maxage']))
2474
		{
2475
			$timeQuery .= ' AND pm.msgtime > ' . (time() - $this->_search_params['maxage'] * 86400);
2476
		}
2477
2478
		// If we have errors - return back to the first screen...
2479
		if (!empty($context['search_errors']))
2480
		{
2481
			$this->_req->post->params = $context['params'];
2482
2483
			$this->action_search();
2484
2485
			return false;
2486
		}
2487
2488
		// Get the number of results.
2489
		$numResults = numPMSeachResults($userQuery, $labelQuery, $timeQuery, $searchQuery, $this->_searchq_parameters);
2490
2491
		// Get all the matching message ids, senders and head pm nodes
2492
		list($foundMessages, $posters, $head_pms) = loadPMSearchMessages($userQuery, $labelQuery, $timeQuery, $searchQuery, $this->_searchq_parameters, $this->_search_params);
2493
2494
		// Find the real head pm when in conversation view
2495
		if ($context['display_mode'] == 2 && !empty($head_pms))
2496
		{
2497
			$real_pm_ids = loadPMSearchHeads($head_pms);
2498
		}
2499
2500
		// Load the found user data
2501
		$posters = array_unique($posters);
2502
		if (!empty($posters))
2503
		{
2504
			MembersList::load($posters);
2505
		}
2506
2507
		// Sort out the page index.
2508
		$context['page_index'] = constructPageIndex('{scripturl}?action=pm;sa=search2;params=' . $context['params'], $this->_req->query->start, $numResults, $modSettings['search_results_per_page'], false);
2509
2510
		$context['message_labels'] = array();
2511
		$context['message_replied'] = array();
2512
		$context['personal_messages'] = array();
2513
		$context['first_label'] = array();
2514
2515
		// If we have results, we have work to do!
2516
		if (!empty($foundMessages))
2517
		{
2518
			$recipients = array();
2519
			list($context['message_labels'], $context['message_replied'], $context['message_unread'], $context['first_label']) = loadPMRecipientInfo($foundMessages, $recipients, $context['folder'], true);
2520
2521
			// Prepare for the callback!
2522
			$search_results = loadPMSearchResults($foundMessages, $this->_search_params);
2523
			$counter = 0;
2524
			$bbc_parser = ParserWrapper::instance();
2525
			foreach ($search_results as $row)
2526
			{
2527
				// If there's no subject, use the default.
2528
				$row['subject'] = $row['subject'] === '' ? $txt['no_subject'] : $row['subject'];
2529
2530
				// Load this posters context info, if its not there then fill in the essentials...
2531
				$member = MembersList::get($row['id_member_from']);
2532
				$member->loadContext(true);
2533
				if (!$member->isEmpty())
2534
				{
2535
					$member['name'] = $row['from_name'];
2536
					$member['id'] = 0;
2537
					$member['group'] = $txt['guest_title'];
2538
					$member['link'] = $row['from_name'];
2539
					$member['email'] = '';
2540
					$member['show_email'] = showEmailAddress(true, 0);
2541
					$member['is_guest'] = true;
2542
				}
2543
2544
				// Censor anything we don't want to see...
2545
				$row['body'] = censor($row['body']);
2546
				$row['subject'] = censor($row['subject']);
2547
2548
				// Parse out any BBC...
2549
				$row['body'] = $bbc_parser->parsePM($row['body']);
2550
2551
				// Highlight the hits
2552
				$body_highlighted = '';
2553
				$subject_highlighted = '';
2554
				foreach ($searchArray as $query)
2555
				{
2556
					// Fix the international characters in the keyword too.
2557
					$query = un_htmlspecialchars($query);
2558
					$query = trim($query, '\*+');
2559
					$query = strtr(Util::htmlspecialchars($query), array('\\\'' => '\''));
2560
2561
					$body_highlighted = preg_replace_callback('/((<[^>]*)|' . preg_quote(strtr($query, array('\'' => '&#039;')), '/') . ')/iu',
2562
						function ($matches) {
2563
							return $this->_highlighted_callback($matches);
2564
						}, $row['body']);
2565
					$subject_highlighted = preg_replace('/(' . preg_quote($query, '/') . ')/iu', '<strong class="highlight">$1</strong>', $row['subject']);
2566
				}
2567
2568
				// Set a link using the first label information
2569
				$href = $scripturl . '?action=pm;f=' . $context['folder'] . (isset($context['first_label'][$row['id_pm']]) ? ';l=' . $context['first_label'][$row['id_pm']] : '') . ';pmid=' . ($context['display_mode'] == 2 && isset($real_pm_ids[$head_pms[$row['id_pm']]]) && $context['folder'] === 'inbox' ? $real_pm_ids[$head_pms[$row['id_pm']]] : $row['id_pm']) . '#msg_' . $row['id_pm'];
2570
2571
				$context['personal_messages'][] = array(
2572
					'id' => $row['id_pm'],
2573
					'member' => $member,
2574
					'subject' => $subject_highlighted,
2575
					'body' => $body_highlighted,
2576
					'time' => standardTime($row['msgtime']),
2577
					'html_time' => htmlTime($row['msgtime']),
2578
					'timestamp' => forum_time(true, $row['msgtime']),
2579
					'recipients' => &$recipients[$row['id_pm']],
2580
					'labels' => &$context['message_labels'][$row['id_pm']],
2581
					'fully_labeled' => (!empty($context['message_labels'][$row['id_pm']]) ? count($context['message_labels'][$row['id_pm']]) : 0) == count($context['labels']),
2582
					'is_replied_to' => &$context['message_replied'][$row['id_pm']],
2583
					'href' => $href,
2584
					'link' => '<a href="' . $href . '">' . $subject_highlighted . '</a>',
2585
					'counter' => ++$counter,
2586
				);
2587
			}
2588
		}
2589
2590
		// Finish off the context.
2591
		$context['page_title'] = $txt['pm_search_title'];
2592
		$context['sub_template'] = 'search_results';
2593
		$context['menu_data_' . $context['pm_menu_id']]['current_area'] = 'search';
2594
		$context['linktree'][] = array(
2595
			'url' => getUrl('action', ['action' => 'pm', 'sa' => 'search']),
2596
			'name' => $txt['pm_search_bar_title'],
2597
		);
2598
	}
2599
2600
	/**
2601
	 * Extract search params from a string
2602
	 *
2603
	 * What it does:
2604
	 *
2605
	 * - When paging search results, reads and decodes the passed parameters
2606
	 * - Places what it finds back in search_params
2607
	 */
2608
	private function _searchParamsFromString()
2609
	{
2610
		$this->_search_params = array();
2611
2612
		if (isset($this->_req->query->params) || isset($this->_req->post->params))
2613
		{
2614
			// Feed it
2615
			$temp_params = $this->_req->query->params ?? $this->_req->post->params;
2616
2617
			// Decode and replace the uri safe characters we added
2618
			$temp_params = base64_decode(str_replace(array('-', '_', '.'), array('+', '/', '='), $temp_params));
2619
2620
			$temp_params = explode('|"|', $temp_params);
2621
			foreach ($temp_params as $i => $data)
2622
			{
2623
				list ($k, $v) = array_pad(explode('|\'|', $data), 2, '');
2624
				$this->_search_params[$k] = $v;
2625
			}
2626
		}
2627
2628
		return $this->_search_params;
2629
	}
2630
2631
	/**
2632
	 * Sets the search params for the query
2633
	 *
2634
	 * What it does:
2635
	 *
2636
	 * - Uses existing ones if coming from pagination or uses those passed from the search pm form
2637
	 * - Validates passed params are valid
2638
	 */
2639
	private function _prepareSearchParams()
2640
	{
2641
		// Store whether simple search was used (needed if the user wants to do another query).
2642
		if (!isset($this->_search_params['advanced']))
2643
		{
2644
			$this->_search_params['advanced'] = empty($this->_req->post->advanced) ? 0 : 1;
2645
		}
2646
2647
		// 1 => 'allwords' (default, don't set as param),  2 => 'anywords'.
2648
		if (!empty($this->_search_params['searchtype']) || (!empty($this->_req->post->searchtype) && $this->_req->post->searchtype == 2))
2649
		{
2650
			$this->_search_params['searchtype'] = 2;
2651
		}
2652
2653
		// Minimum age of messages. Default to zero (don't set param in that case).
2654
		if (!empty($this->_search_params['minage']) || (!empty($this->_req->post->minage) && $this->_req->post->minage > 0))
2655
		{
2656
			$this->_search_params['minage'] = !empty($this->_search_params['minage']) ? (int) $this->_search_params['minage'] : (int) $this->_req->post->minage;
2657
		}
2658
2659
		// Maximum age of messages. Default to infinite (9999 days: param not set).
2660
		if (!empty($this->_search_params['maxage']) || (!empty($this->_req->post->maxage) && $this->_req->post->maxage < 9999))
2661
		{
2662
			$this->_search_params['maxage'] = !empty($this->_search_params['maxage']) ? (int) $this->_search_params['maxage'] : (int) $this->_req->post->maxage;
2663
		}
2664
2665
		// Default the user name to a wildcard matching every user (*).
2666
		if (!empty($this->_search_params['userspec']) || (!empty($this->_req->post->userspec) && $this->_req->post->userspec != '*'))
2667
		{
2668
			$this->_search_params['userspec'] = $this->_search_params['userspec'] ?? $this->_req->post->userspec;
2669
		}
2670
2671
		// Search modifiers
2672
		$this->_search_params['subject_only'] = !empty($this->_search_params['subject_only']) || !empty($this->_req->post->subject_only);
2673
		$this->_search_params['show_complete'] = !empty($this->_search_params['show_complete']) || !empty($this->_req->post->show_complete);
2674
		$this->_search_params['sent_only'] = !empty($this->_search_params['sent_only']) || !empty($this->_req->post->sent_only);
2675
	}
2676
2677
	/**
2678
	 * Handles the parameters when searching on specific users
2679
	 *
2680
	 * What it does:
2681
	 *
2682
	 * - Returns the user query for use in the main search query
2683
	 * - Sets the parameters for use in the query
2684
	 *
2685
	 * @return string
2686
	 */
2687
	private function _setUserQuery()
2688
	{
2689
		global $context;
2690
2691
		// Hardcoded variables that can be tweaked if required.
2692
		$maxMembersToSearch = 500;
2693
2694
		// Init to not be searching based on members
2695
		$userQuery = '';
2696
2697
		// If there's no specific user, then don't mention it in the main query.
2698
		if (!empty($this->_search_params['userspec']))
2699
		{
2700
			// Set up so we can search by user name, wildcards, like, etc
2701
			$userString = strtr(Util::htmlspecialchars($this->_search_params['userspec'], ENT_QUOTES), array('&quot;' => '"'));
2702
			$userString = strtr($userString, array('%' => '\%', '_' => '\_', '*' => '%', '?' => '_'));
2703
2704
			preg_match_all('~"([^"]+)"~', $userString, $matches);
2705
			$possible_users = array_merge($matches[1], explode(',', preg_replace('~"[^"]+"~', '', $userString)));
2706
2707
			// Who matches those criteria?
2708
			require_once(SUBSDIR . '/Members.subs.php');
2709
			$members = membersBy('member_names', array('member_names' => $possible_users));
2710
2711
			foreach ($possible_users as $key => $possible_user)
2712
			{
2713
				$this->_searchq_parameters['guest_user_name_implode_' . $key] = '{string_case_insensitive:' . $possible_user . '}';
2714
			}
2715
2716
			// Simply do nothing if there are too many members matching the criteria.
2717
			if (count($members) > $maxMembersToSearch)
2718
			{
2719
				$userQuery = '';
2720
			}
2721
			elseif (count($members) === 0)
2722
			{
2723
				if ($context['folder'] === 'inbox')
2724
				{
2725
					$uq = array();
2726
					$name = '{column_case_insensitive:pm.from_name}';
2727
					foreach (array_keys($possible_users) as $key)
2728
					{
2729
						$uq[] = 'AND pm.id_member_from = 0 AND (' . $name . ' LIKE {string:guest_user_name_implode_' . $key . '})';
2730
					}
2731
					$userQuery = implode(' ', $uq);
2732
					$this->_searchq_parameters['pm_from_name'] = $name;
2733
				}
2734
				else
2735
				{
2736
					$userQuery = '';
2737
				}
2738
			}
2739
			else
2740
			{
2741
				$memberlist = array();
2742
				foreach ($members as $id)
2743
				{
2744
					$memberlist[] = $id;
2745
				}
2746
2747
				// Use the name as as sent from or sent to
2748
				if ($context['folder'] === 'inbox')
2749
				{
2750
					$uq = array();
2751
					$name = '{column_case_insensitive:pm.from_name}';
2752
2753
					foreach (array_keys($possible_users) as $key)
2754
					{
2755
						$uq[] = 'AND (pm.id_member_from IN ({array_int:member_list}) OR (pm.id_member_from = 0 AND (' . $name . ' LIKE {string:guest_user_name_implode_' . $key . '})))';
2756
					}
2757
2758
					$userQuery = implode(' ', $uq);
2759
				}
2760
				else
2761
				{
2762
					$userQuery = 'AND (pmr.id_member IN ({array_int:member_list}))';
2763
				}
2764
2765
				$this->_searchq_parameters['pm_from_name'] = '{column_case_insensitive:pm.from_name}';
2766
				$this->_searchq_parameters['member_list'] = $memberlist;
2767
			}
2768
		}
2769
2770
		return $userQuery;
2771
	}
2772
2773
	/**
2774
	 * Read / Set the sort parameters for the results listing
2775
	 */
2776
	private function _setSortParams()
2777
	{
2778
		$sort_columns = array(
2779
			'pm.id_pm',
2780
		);
2781
2782
		if (empty($this->_search_params['sort']) && !empty($this->_req->post->sort))
2783
		{
2784
			list ($this->_search_params['sort'], $this->_search_params['sort_dir']) = array_pad(explode('|', $this->_req->post->sort), 2, '');
2785
		}
2786
2787
		$this->_search_params['sort'] = !empty($this->_search_params['sort']) && in_array($this->_search_params['sort'], $sort_columns) ? $this->_search_params['sort'] : 'pm.id_pm';
2788
		$this->_search_params['sort_dir'] = !empty($this->_search_params['sort_dir']) && $this->_search_params['sort_dir'] === 'asc' ? 'asc' : 'desc';
2789
	}
2790
2791
	/**
2792
	 * Handles the parameters when searching on specific labels
2793
	 *
2794
	 * What it does:
2795
	 *
2796
	 * - Returns the label query for use in the main search query
2797
	 * - Sets the parameters for use in the query
2798
	 *
2799
	 * @return string
2800
	 * @throws \Exception
2801
	 */
2802
	private function _setLabelQuery()
2803
	{
2804
		global $context;
2805
2806
		$db = database();
2807
2808
		$labelQuery = '';
2809
2810
		if ($context['folder'] === 'inbox' && !empty($this->_search_params['advanced']) && $context['currently_using_labels'])
2811
		{
2812
			// Came here from pagination?  Put them back into $_REQUEST for sanitation.
2813
			if (isset($this->_search_params['labels']))
2814
			{
2815
				$this->_req->post->searchlabel = explode(',', $this->_search_params['labels']);
2816
			}
2817
2818
			// Assuming we have some labels - make them all integers.
2819
			if (!empty($this->_req->post->searchlabel) && is_array($this->_req->post->searchlabel))
2820
			{
2821
				$this->_req->post->searchlabel = array_map('intval', $this->_req->post->searchlabel);
2822
			}
2823
			else
2824
			{
2825
				$this->_req->post->searchlabel = array();
2826
			}
2827
2828
			// Now that everything is cleaned up a bit, make the labels a param.
2829
			$this->_search_params['labels'] = implode(',', $this->_req->post->searchlabel);
2830
2831
			// No labels selected? That must be an error!
2832
			if (empty($this->_req->post->searchlabel))
2833
			{
2834
				$context['search_errors']['no_labels_selected'] = true;
2835
			}
2836
			// Otherwise prepare the query!
2837
			elseif (count($this->_req->post->searchlabel) !== count($context['labels']))
2838
			{
2839
				$labelQuery = '
2840
				AND {raw:label_implode}';
2841
2842
				$labelStatements = array();
2843
				foreach ($this->_req->post->searchlabel as $label)
2844
				{
2845
					$labelStatements[] = $db->quote('FIND_IN_SET({string:label}, pmr.labels) != 0', array('label' => $label,));
2846
				}
2847
2848
				$this->_searchq_parameters ['label_implode'] = '(' . implode(' OR ', $labelStatements) . ')';
2849
			}
2850
		}
2851
2852
		return $labelQuery;
2853
	}
2854
2855
	/**
2856
	 * Encodes search params in an URL-compatible way
2857
	 *
2858
	 * @return string - the encoded string to be appended to the URL
2859
	 */
2860
	private function _compileURLparams()
2861
	{
2862
		$encoded = array();
2863
2864
		// Now we have all the parameters, combine them together for pagination and the like...
2865
		foreach ($this->_search_params as $k => $v)
2866
		{
2867
			$encoded[] = $k . '|\'|' . $v;
2868
		}
2869
2870
		// Base64 encode, then replace +/= with uri safe ones that can be reverted
2871
		$encoded = str_replace(array('+', '/', '='), array('-', '_', '.'), base64_encode(implode('|"|', $encoded)));
2872
2873
		return $encoded;
2874
	}
2875
2876
	/**
2877
	 * Allows to search through personal messages.
2878
	 *
2879
	 * What it does:
2880
	 *
2881
	 * - accessed with ?action=pm;sa=search
2882
	 * - shows the screen to search pm's (?action=pm;sa=search)
2883
	 * - uses the search sub template of the PersonalMessage template.
2884
	 * - decodes and loads search parameters given in the URL (if any).
2885
	 * - the form redirects to index.php?action=pm;sa=search2.
2886
	 *
2887
	 * @uses search sub template
2888
	 */
2889
	public function action_search()
2890
	{
2891
		global $context, $txt;
2892
2893
		// If they provided some search parameters, we need to extract them
2894
		if (isset($this->_req->post->params))
2895
		{
2896
			$context['search_params'] = $this->_searchParamsFromString();
2897
		}
2898
2899
		// Set up the search criteria, type, what, age, etc
2900
		if (isset($this->_req->post->search))
2901
		{
2902
			$context['search_params']['search'] = un_htmlspecialchars($this->_req->post->search);
2903
			$context['search_params']['search'] = htmlspecialchars($context['search_params']['search'], ENT_COMPAT, 'UTF-8');
2904
		}
2905
2906
		if (isset($context['search_params']['userspec']))
2907
		{
2908
			$context['search_params']['userspec'] = htmlspecialchars($context['search_params']['userspec'], ENT_COMPAT, 'UTF-8');
2909
		}
2910
2911
		// 1 => 'allwords' / 2 => 'anywords'.
2912
		if (!empty($context['search_params']['searchtype']))
2913
		{
2914
			$context['search_params']['searchtype'] = 2;
2915
		}
2916
2917
		// Minimum and Maximum age of the message
2918
		if (!empty($context['search_params']['minage']))
2919
		{
2920
			$context['search_params']['minage'] = (int) $context['search_params']['minage'];
2921
		}
2922
		if (!empty($context['search_params']['maxage']))
2923
		{
2924
			$context['search_params']['maxage'] = (int) $context['search_params']['maxage'];
2925
		}
2926
2927
		$context['search_params']['show_complete'] = !empty($context['search_params']['show_complete']);
2928
		$context['search_params']['subject_only'] = !empty($context['search_params']['subject_only']);
2929
2930
		// Create the array of labels to be searched.
2931
		$context['search_labels'] = array();
2932
		$searchedLabels = isset($context['search_params']['labels']) && $context['search_params']['labels'] != '' ? explode(',', $context['search_params']['labels']) : array();
2933
		foreach ($context['labels'] as $label)
2934
		{
2935
			$context['search_labels'][] = array(
2936
				'id' => $label['id'],
2937
				'name' => $label['name'],
2938
				'checked' => !empty($searchedLabels) ? in_array($label['id'], $searchedLabels) : true,
2939
			);
2940
		}
2941
2942
		// Are all the labels checked?
2943
		$context['check_all'] = empty($searchedLabels) || count($context['search_labels']) === count($searchedLabels);
2944
2945
		// Load the error text strings if there were errors in the search.
2946
		if (!empty($context['search_errors']))
2947
		{
2948
			Txt::load('Errors');
2949
			$context['search_errors']['messages'] = array();
2950
			foreach ($context['search_errors'] as $search_error => $dummy)
2951
			{
2952
				if ($search_error === 'messages')
2953
				{
2954
					continue;
2955
				}
2956
2957
				$context['search_errors']['messages'][] = $txt['error_' . $search_error];
2958
			}
2959
		}
2960
2961
		$context['page_title'] = $txt['pm_search_title'];
2962
		$context['sub_template'] = 'search';
2963
		$context['linktree'][] = array(
2964
			'url' => getUrl('action', ['action' => 'pm', 'sa' => 'search']),
2965
			'name' => $txt['pm_search_bar_title'],
2966
		);
2967
	}
2968
2969
	/**
2970
	 * Used to highlight body text with strings that match the search term
2971
	 *
2972
	 * - Callback function used in $body_highlighted
2973
	 *
2974
	 * @param string[] $matches
2975
	 *
2976
	 * @return string
2977
	 */
2978
	private function _highlighted_callback($matches)
2979
	{
2980
		return isset($matches[2]) && $matches[2] == $matches[1] ? stripslashes($matches[1]) : '<strong class="highlight">' . $matches[1] . '</strong>';
2981
	}
2982
2983
	/**
2984
	 * Allows the user to mark a personal message as unread so they remember to come back to it
2985
	 */
2986
	public function action_markunread()
2987
	{
2988
		global $context;
2989
2990
		checkSession('request');
2991
2992
		$pmsg = !empty($this->_req->query->pmsg) ? (int) $this->_req->query->pmsg : null;
2993
2994
		// Marking a message as unread, we need a message that was sent to them
2995
		// Can't mark your own reply as unread, that would be weird
2996
		if (!is_null($pmsg) && checkPMReceived($pmsg))
2997
		{
2998
			// Make sure this is accessible, should be of course
2999
			if (!isAccessiblePM($pmsg, 'inbox'))
3000
			{
3001
				throw new Exception('no_access', false);
3002
			}
3003
3004
			// Well then, you get to hear about it all over again
3005
			markMessagesUnread($pmsg);
3006
		}
3007
3008
		// Back to the folder.
3009
		redirectexit($context['current_label_redirect']);
3010
	}
3011
}
3012