PersonalMessage::action_index()   A
last analyzed

Complexity

Conditions 4
Paths 3

Size

Total Lines 40
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 20
CRAP Score 4.0016

Importance

Changes 0
Metric Value
cc 4
eloc 23
nc 3
nop 0
dl 0
loc 40
ccs 20
cts 21
cp 0.9524
crap 4.0016
rs 9.552
c 0
b 0
f 0
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\Helper\Util;
31
use ElkArte\Helper\ValuesContainer;
32
use ElkArte\Languages\Loader;
33
use ElkArte\Languages\Txt;
34
use ElkArte\MembersList;
35
use ElkArte\MessagesCallback\BodyParser\Normal;
36
use ElkArte\MessagesCallback\PmRenderer;
37
use ElkArte\Profile\Profile;
38
use ElkArte\Profile\ProfileFields;
39
use ElkArte\Profile\ProfileOptions;
40
use ElkArte\User;
41
use ElkArte\VerificationControls\VerificationControlsIntegrate;
42
43
/**
44
 * Class PersonalMessage
45
 * It allows viewing, sending, deleting, and marking personal messages
46
 *
47
 * @package ElkArte\Controller
48
 */
49
class PersonalMessage extends AbstractController
50
{
51
	/**
52
	 * @var array $_search_params will carry all settings that differ from the default
53
	 * search parameters. That way, the URLs involved in a search page will
54
	 * be kept as short as possible.
55
	 */
56
	private $_search_params = [];
57
58
	/** @var array $_searchq_parameters will carry all the values needed by S_search_params */
59
	private $_searchq_parameters = [];
60
61
	/** @var int display_mode key is as follows */
62
	private const DISPLAY_ALL_AT_ONCE = 0;
63
	private const DISPLAY_ONE_AT_TIME = 1;
64
	private const DISPLAY_AS_CONVERSATION = 2;
65
66
	/**
67
	 * This method is executed before any other in this file (when the class is
68
	 * loaded by the dispatcher).
69
	 *
70
	 * What it does:
71 4
	 *
72
	 * - It sets the context, load templates and language file(s), as necessary
73 4
	 * for the function that will be called.
74
	 */
75
	public function pre_dispatch()
76 4
	{
77
		global $txt, $context, $modSettings;
78
79 4
		// No guests!
80
		is_not_guest();
81
82 4
		// You're not supposed to be here at all, if you can't even read PMs.
83
		isAllowedTo('pm_read');
84
85 4
		// This file contains PM functions such as mark, send, delete
86 4
		require_once(SUBSDIR . '/PersonalMessage.subs.php');
87
88 4
		// Templates, language, javascripts
89
		Txt::load('PersonalMessage');
90
		loadJavascriptFile(['suggest.js', 'PersonalMessage.js']);
91
92
		if ($this->getApi() === false)
93 4
		{
94
			theme()->getTemplates()->load('PersonalMessage');
95
		}
96 4
97
		$this->_events->trigger('pre_dispatch', ['xml' => $this->getApi() !== false]);
98
99 4
		// Load up the members maximum message capacity.
100
		$this->_loadMessageLimit();
101
102
		// A previous message was sent successfully? show a small indication.
103
		if ($this->_req->getQuery('done') === 'sent')
104
		{
105 4
			$context['pm_sent'] = true;
106
		}
107 4
108
		// Load the label counts data.
109
		if (User::$settings['new_pm'] || !Cache::instance()->getVar($context['labels'], 'labelCounts:' . $this->user->id, 720))
0 ignored issues
show
Bug Best Practice introduced by
The property id does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
110 4
		{
111
			$this->_loadLabels();
112
113
			// Get the message count for each label
114 4
			$context['labels'] = loadPMLabels($context['labels']);
115
		}
116
117
		// Now we have the labels, and assuming we have unsorted mail, apply our rules!
118
		if (User::$settings['new_pm'])
119
		{
120
			// Apply our rules to the new PM's
121
			applyRules();
122
123
			require_once(SUBSDIR . '/Members.subs.php');
124
			updateMemberData($this->user->id, array('new_pm' => 0));
125
126
			// Turn the new PM's status off, for the popup alert, since they have entered the PM area
127 4
			toggleNewPM($this->user->id);
128
		}
129
130 4
		// This determines if we have more labels than just the standard inbox.
131 4
		$context['currently_using_labels'] = count($context['labels']) > 1 ? 1 : 0;
132 4
133
		// Some stuff for the labels...
134
		$label = $this->_req->getQuery('l', 'intval');
135 4
		$folder = $this->_req->getQuery('f', 'trim', '');
136 4
		$start = $this->_req->getQuery('start', 'trim');
137
		$context['current_label_id'] = isset($label, $context['labels'][$label]) ? (int) $label : -1;
138
		$context['current_label'] = &$context['labels'][$context['current_label_id']]['name'];
139 4
		$context['folder'] = $folder !== 'sent' ? 'inbox' : 'sent';
140 4
141 4
		// This is convenient.  Do you know how annoying it is to do this every time?!
142
		$context['current_label_redirect'] = 'action=pm;f=' . $context['folder'] . (isset($start) ? ';start=' . $start : '') . (empty($label) ? '' : ';l=' . $label);
143
		$context['can_issue_warning'] = featureEnabled('w') && allowedTo('issue_warning') && !empty($modSettings['warning_enable']);
144
145 4
		// Build the breadcrumbs for all the actions...
146 4
		$context['breadcrumbs'][] = [
147
			'url' => getUrl('action', ['action' => 'pm']),
148
			'name' => $txt['personal_messages']
149
		];
150
151 4
		// Preferences...
152
		$context['display_mode'] = (int) User::$settings['pm_prefs'] & 3;
153 4
	}
154
155 4
	/**
156
	 * Load a members message limit and prepares the limit bar
157
	 */
158 4
	private function _loadMessageLimit()
159
	{
160
		global $context, $txt;
161
162
		$context['message_limit'] = loadMessageLimit();
163
164
		// Prepare the context for the capacity bar.
165
		if (!empty($context['message_limit']))
166
		{
167
			$bar = ($this->user->messages * 100) / $context['message_limit'];
0 ignored issues
show
Bug Best Practice introduced by
The property messages does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
168
169
			$context['limit_bar'] = array(
170 4
				'messages' => $this->user->messages,
171
				'allowed' => $context['message_limit'],
172
				'percent' => $bar,
173
				'bar' => min(100, (int) $bar),
174
				'text' => sprintf($txt['pm_currently_using'], $this->user->messages, round($bar, 1)),
175 4
			);
176
		}
177 4
	}
178
179 4
	/**
180
	 * Loads the user defined label's for use in the template etc.
181 4
	 */
182
	private function _loadLabels()
183 4
	{
184
		global $context, $txt;
185 4
186
		$userLabels = explode(',', User::$settings['message_labels'] ?? '');
187
188
		foreach ($userLabels as $id_label => $label_name)
189
		{
190
			if (empty($label_name))
191
			{
192
				continue;
193
			}
194
195
			$context['labels'][$id_label] = [
196
				'id' => $id_label,
197 4
				'name' => trim($label_name),
198 4
				'messages' => 0,
199 4
				'unread_messages' => 0,
200 4
			];
201 4
		}
202
203 4
		// The default inbox is always available
204
		$context['labels'][-1] = [
205
			'id' => -1,
206
			'name' => $txt['pm_msg_label_inbox'],
207
			'messages' => 0,
208
			'unread_messages' => 0,
209
		];
210
	}
211
212
	/**
213
	 * This is the main function of personal messages, called before the action handler.
214
	 *
215
	 * What it does:
216 4
	 *
217
	 * - PersonalMessages is a menu-based controller.
218 4
	 * - It sets up the menu.
219
	 * - Calls from the menu the appropriate method/function for the current area.
220
	 *
221
	 * @see AbstractController::action_index
222 4
	 */
223 4
	public function action_index()
224 4
	{
225 4
		global $context;
226 4
227 4
		// Finally all the things we know how to do
228 4
		$subActions = array(
229 4
			'manlabels' => array($this, 'action_manlabels', 'permission' => 'pm_read'),
230 4
			'manrules' => array($this, 'action_manrules', 'permission' => 'pm_read'),
231 4
			'markunread' => array($this, 'action_markunread', 'permission' => 'pm_read'),
232 4
			'pmactions' => array($this, 'action_pmactions', 'permission' => 'pm_read'),
233 4
			'prune' => array($this, 'action_prune', 'permission' => 'pm_read'),
234 4
			'removeall' => array($this, 'action_removeall', 'permission' => 'pm_read'),
235 4
			'removeall2' => array($this, 'action_removeall2', 'permission' => 'pm_read'),
236
			'report' => array($this, 'action_report', 'permission' => 'pm_read'),
237
			'search' => array($this, 'action_search', 'permission' => 'pm_read'),
238
			'search2' => array($this, 'action_search2', 'permission' => 'pm_read'),
239 4
			'send' => array($this, 'action_send', 'permission' => 'pm_read'),
240
			'send2' => array($this, 'action_send2', 'permission' => 'pm_read'),
241
			'settings' => array($this, 'action_settings', 'permission' => 'pm_read'),
242 4
			'inbox' => array($this, 'action_folder', 'permission' => 'pm_read'),
243
		);
244
245 4
		// Set up our action array
246
		$action = new Action('pm_index');
247 2
248
		// Known action, go to it, otherwise the inbox for you
249 2
		$subAction = $action->initialize($subActions, 'inbox');
250
251
		// Set the right index bar for the action
252
		if ($subAction === 'inbox')
253
		{
254
			$this->_messageIndexBar($context['current_label_id'] === -1 ? $context['folder'] : 'label' . $context['current_label_id']);
255 4
		}
256 4
		elseif ($this->getApi() === false)
257
		{
258
			$this->_messageIndexBar($subAction);
259
		}
260
261
		// And off we go!
262
		$action->dispatch($subAction);
263
	}
264
265 2
	/**
266
	 * A menu to easily access different areas of the PM section
267 2
	 *
268
	 * @param string $area
269 2
	 */
270
	private function _messageIndexBar($area)
271
	{
272 1
		global $txt, $context;
273 2
274 2
		require_once(SUBSDIR . '/Menu.subs.php');
275
276
		$pm_areas = array(
277 2
			'folders' => array(
278 2
				'title' => $txt['pm_messages'],
279 2
				'counter' => 'unread_messages',
280
				'areas' => array(
281
					'inbox' => array(
282 2
						'label' => $txt['inbox'],
283 2
						'custom_url' => getUrl('action', ['action' => 'pm']),
284 2
						'counter' => 'unread_messages',
285
					),
286
					'send' => array(
287 2
						'label' => $txt['new_message'],
288 2
						'custom_url' => getUrl('action', ['action' => 'pm', 'sa' => 'send']),
289
						'permission' => 'pm_send',
290
					),
291
					'sent' => array(
292
						'label' => $txt['sent_items'],
293 2
						'custom_url' => getUrl('action', ['action' => 'pm', 'f' => 'sent']),
294 2
					),
295
				),
296
			),
297
			'labels' => array(
298 2
				'title' => $txt['pm_labels'],
299
				'counter' => 'labels_unread_total',
300
				'areas' => array(),
301 2
			),
302 2
			'actions' => array(
303
				'title' => $txt['pm_actions'],
304
				'areas' => array(
305 2
					'search' => array(
306 2
						'label' => $txt['pm_search_bar_title'],
307
						'custom_url' => getUrl('action', ['action' => 'pm', 'sa' => 'search']),
308
					),
309
					'prune' => array(
310
						'label' => $txt['pm_prune'],
311 2
						'custom_url' => getUrl('action', ['action' => 'pm', 'sa' => 'prune']),
312
					),
313
				),
314 2
			),
315 2
			'pref' => array(
316
				'title' => $txt['pm_preferences'],
317
				'areas' => array(
318 2
					'manlabels' => array(
319 2
						'label' => $txt['pm_manage_labels'],
320
						'custom_url' => getUrl('action', ['action' => 'pm', 'sa' => 'manlabels']),
321
					),
322 2
					'manrules' => array(
323 2
						'label' => $txt['pm_manage_rules'],
324
						'custom_url' => getUrl('action', ['action' => 'pm', 'sa' => 'manrules']),
325
					),
326
					'settings' => array(
327
						'label' => $txt['pm_settings'],
328
						'custom_url' => getUrl('action', ['action' => 'pm', 'sa' => 'settings']),
329
					),
330 2
				),
331 2
			),
332
		);
333 2
334
		// Handle labels.
335
		$label_counters = array('unread_messages' => $context['labels'][-1]['unread_messages']);
336
		if (empty($context['currently_using_labels']))
337
		{
338
			unset($pm_areas['labels']);
339
		}
340
		else
341
		{
342
			// Note we send labels by id as it will have fewer problems in the query string.
343
			$label_counters['labels_unread_total'] = 0;
344
			foreach ($context['labels'] as $label)
345
			{
346
				if ($label['id'] === -1)
347
				{
348
					continue;
349
				}
350
351
				// Count the amount of unread items in labels.
352
				$label_counters['labels_unread_total'] += $label['unread_messages'];
353
354
				// Add the label to the menu.
355
				$pm_areas['labels']['areas']['label' . $label['id']] = array(
356
					'label' => $label['name'],
357
					'custom_url' => getUrl('action', ['action' => 'pm', 'l' => $label['id']]),
358
					'counter' => 'label' . $label['id'],
359
					'messages' => $label['messages'],
360
				);
361
362 2
				$label_counters['label' . $label['id']] = $label['unread_messages'];
363
			}
364
		}
365
366
		// Do we have a limit on the amount of messages we can keep?
367
		if (!empty($context['message_limit']))
368
		{
369
			$bar = round(($this->user->messages * 100) / $context['message_limit'], 1);
0 ignored issues
show
Bug Best Practice introduced by
The property messages does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
370
371
			$context['limit_bar'] = array(
372
				'messages' => $this->user->messages,
373
				'allowed' => $context['message_limit'],
374
				'percent' => $bar,
375
				'bar' => $bar > 100 ? 100 : (int) $bar,
376
				'text' => sprintf($txt['pm_currently_using'], $this->user->messages, $bar)
377 2
			);
378 2
		}
379
380 2
		// Set a few options for the menu.
381
		$menuOptions = array(
382
			'current_area' => $area,
383
			'hook' => 'pm',
384 2
			'disable_url_session_check' => true,
385 2
			'counters' => empty($label_counters) ? 0 : $label_counters,
386
		);
387
388 2
		// Actually create the menu!
389 2
		$pm_include_data = createMenu($pm_areas, $menuOptions);
390
		unset($pm_areas);
391
392 2
		// Make a note of the Unique ID for this menu.
393
		$context['pm_menu_id'] = $context['max_menu_id'];
394
		$context['pm_menu_name'] = 'menu_data_' . $context['pm_menu_id'];
395 2
396
		// Set the selected item.
397
		$context['menu_item_selected'] = $pm_include_data['current_area'];
398
399
		// Set the template for this area and add the profile layer.
400 2
		if ($this->getApi() === false)
401
		{
402
			$template_layers = theme()->getLayers();
403
			$template_layers->add('pm');
404
		}
405
	}
406
407
	/**
408
	 * Display a folder, i.e. inbox/sent etc.
409
	 *
410 2
	 * Display mode: 0 = all at once, 1 = one at a time, 2 = as a conversation
411
	 *
412 2
	 * @throws Exception
413 2
	 * @uses subject_list, pm template layers
414
	 * @uses folder sub template
415
	 */
416 2
	public function action_folder()
417
	{
418
		global $txt, $scripturl, $modSettings, $context, $subjects_request, $messages_request, $options;
419
420
		// Changing view?
421
		if ($this->_req->isSet('view'))
422
		{
423
			$context['display_mode'] = (int) $context['display_mode'] > 1 ? 0 : (int) $context['display_mode'] + 1;
424 2
			require_once(SUBSDIR . '/Members.subs.php');
425
			updateMemberData($this->user->id, ['pm_prefs' => (User::$settings['pm_prefs'] & 252) | $context['display_mode']]);
0 ignored issues
show
Bug Best Practice introduced by
The property id does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
426 2
		}
427
428
		// Make sure the starting location is valid.
429
		$start = $this->_req->getQuery('start', 'trim');
430
		if (isset($start) && $start !== 'new')
431
		{
432
			$start = (int) $this->_req->query->start;
433
		}
434
		elseif (!isset($start) && !empty($options['view_newest_pm_first']))
435
		{
436
			$start = 0;
437
		}
438 2
		else
439 2
		{
440 2
			$start = 'new';
441
		}
442
443 2
		// Set up some basic template stuff.
444 2
		$context['from_or_to'] = $context['folder'] !== 'sent' ? 'from' : 'to';
445
		$context['signature_enabled'] = strpos($modSettings['signature_settings'], '1') === 0;
446 2
		$context['disabled_fields'] = isset($modSettings['disabled_profile_fields']) ? array_flip(explode(',', $modSettings['disabled_profile_fields'])) : array();
447 2
448
		// Set the template layers we need
449
		$template_layers = theme()->getLayers();
450 2
		$template_layers->addAfter('subject_list', 'pm');
451 2
452
		$labelQuery = $context['folder'] !== 'sent' ? '
453
				AND FIND_IN_SET(' . $context['current_label_id'] . ', pmr.labels) != 0' : '';
454 1
455
		// They didn't pick a sort, so we use the forum default.
456 2
		$sort_by = $this->_req->getQuery('sort', 'trim', 'date');
457 2
		$descending = isset($this->_req->query->desc);
458 2
459
		// Set our sort by query
460 2
		switch ($sort_by)
461
		{
462 2
			case 'date':
463
				$sort_by_query = 'pm.id_pm';
464
				if (!empty($options['view_newest_pm_first']) && !isset($this->_req->query->desc) && !isset($this->_req->query->asc))
465
				{
466
					$descending = true;
467
				}
468
469
				break;
470
			case 'name':
471
				$sort_by_query = "COALESCE(mem.real_name, '')";
472
				break;
473
			case 'subject':
474 2
				$sort_by_query = 'pm.subject';
475 2
				break;
476
			default:
477
				$sort_by_query = 'pm.id_pm';
478 2
		}
479
480 2
		// Set the text to resemble the current folder.
481 2
		$pmbox = $context['folder'] !== 'sent' ? $txt['inbox'] : $txt['sent_items'];
482 2
		$txt['delete_all'] = str_replace('PMBOX', $pmbox, $txt['delete_all']);
483
484
		// Now, build the link tree!
485
		if ($context['current_label_id'] === -1)
486
		{
487 2
			$context['breadcrumbs'][] = [
488
				'url' => getUrl('action', ['action' => 'pm', 'f' => $context['folder']]),
489
				'name' => $pmbox
490
			];
491
		}
492
493
		// Build it further if we also have a label.
494
		if ($context['current_label_id'] !== -1)
495
		{
496 2
			$context['breadcrumbs'][] = array(
497
				'url' => getUrl('action', ['action' => 'pm', 'f' => $context['folder'], 'l' => $context['current_label_id']]),
498
				'name' => $txt['pm_current_label'] . ': ' . $context['current_label']
499 2
			);
500
		}
501
502 2
		// Figure out how many messages there are.
503
		$max_messages = getPMCount(false, null, $labelQuery);
504 2
505
		// Only show the button if there are messages to delete.
506
		$context['show_delete'] = $max_messages > 0;
507
508
		// Start on the last page.
509
		if (!is_numeric($start) || $start >= $max_messages)
510
		{
511
			$start = ($max_messages - 1) - (($max_messages - 1) % $modSettings['defaultMaxMessages']);
512 2
		}
513
		elseif ($start < 0)
514
		{
515
			$start = 0;
516
		}
517
518
		// ... but wait - what if we want to start from a specific message?
519
		if ($this->_req->isSet('pmid'))
520
		{
521
			$pmID = $this->_req->getQuery('pmid', 'intval', 0);
522
523
			// Make sure you have access to this PM.
524
			if (!isAccessiblePM($pmID, $context['folder'] === 'sent' ? 'outbox' : 'inbox'))
525
			{
526
				throw new Exception('no_access', false);
527
			}
528
529
			$context['current_pm'] = $pmID;
530
531
			// With only one page of PM's we're gonna want page 1.
532
			if ($max_messages <= $modSettings['defaultMaxMessages'])
533
			{
534
				$start = 0;
535
			}
536
			// If we pass kstart we assume we're in the right place.
537
			elseif (!$this->_req->isSet('kstart'))
538
			{
539
				$start = getPMCount($descending, $pmID, $labelQuery);
540
541 2
				// To stop the page index's being abnormal, start the page on the page the message
542
				// would normally be located on...
543
				$start = $modSettings['defaultMaxMessages'] * (int) ($start / $modSettings['defaultMaxMessages']);
544
			}
545
		}
546
547
		// Sanitize and validate pmsg variable if set.
548
		if ($this->_req->isSet('pmsg'))
549
		{
550
			$pmsg = $this->_req->getQuery('pmsg', 'intval', 0);
551
552
			if (!isAccessiblePM($pmsg, $context['folder'] === 'sent' ? 'outbox' : 'inbox'))
553 2
			{
554 2
				throw new Exception('no_access', false);
555
			}
556 2
		}
557 2
558 2
		// Determine the navigation context
559
		$context['links'] += [
560
			'prev' => $start >= $modSettings['defaultMaxMessages'] ? $scripturl . '?action=pm;start=' . ($start - $modSettings['defaultMaxMessages']) : '',
561
			'next' => $start + $modSettings['defaultMaxMessages'] < $max_messages ? $scripturl . '?action=pm;start=' . ($start + $modSettings['defaultMaxMessages']) : '',
562 2
		];
563 2
564 2
		// We now know what they want, so lets fetch those PM's
565 2
		[$pms, $posters, $recipients, $lastData] = loadPMs([
566 2
			'sort_by_query' => $sort_by_query,
567 2
			'display_mode' => $context['display_mode'],
568 2
			'sort_by' => $sort_by,
569 2
			'label_query' => $labelQuery,
570 2
			'pmsg' => isset($pmsg) ? (int) $pmsg : 0,
571 2
			'descending' => $descending,
572 2
			'start' => $start,
573 2
			'limit' => $modSettings['defaultMaxMessages'],
574
			'folder' => $context['folder'],
575
			'pmid' => $pmID ?? 0,
576 2
		], $this->user->id);
577
578
		// Make sure that we have been given a correct head pm id if we are in conversation mode
579
		if ($context['display_mode'] === self::DISPLAY_AS_CONVERSATION && !empty($pmID) && $pmID != $lastData['id'])
580
		{
581
			throw new Exception('no_access', false);
582 2
		}
583
584
		// If loadPMs returned results, lets show the pm subject list
585
		if (!empty($pms))
586
		{
587
			// Tell the template if no pm has specifically been selected
588
			if (empty($pmID))
589
			{
590
				$context['current_pm'] = 0;
591
			}
592
593
			$display_pms = $context['display_mode'] === self::DISPLAY_ALL_AT_ONCE ? $pms : array($lastData['id']);
594
595
			// At this point we know the main id_pm's. But if we are looking at conversations we need
596
			// the PMs that make up the conversation
597
			if ($context['display_mode'] === self::DISPLAY_AS_CONVERSATION)
598
			{
599
				[$display_pms, $posters] = loadConversationList($lastData['head'], $recipients, $context['folder']);
600
601
				// Conversation list may expose additional PM's being displayed
602
				$all_pms = array_unique(array_merge($pms, $display_pms));
603
604
				// See if any of these 'listing' PMs are in a conversation thread that has unread entries
605
				$context['conversation_unread'] = loadConversationUnreadStatus($all_pms);
606
			}
607
			// This is pretty much EVERY pm!
608
			else
609
			{
610
				$all_pms = array_unique(array_merge($pms, $display_pms));
611
			}
612
613
			// Get recipients (don't include bcc-recipients for your inbox, you're not supposed to know :P).
614
			[$context['message_labels'], $context['message_replied'], $context['message_unread']] = loadPMRecipientInfo($all_pms, $recipients, $context['folder']);
615
616
			// Make sure we don't load any unnecessary data for one at a time mode
617
			if ($context['display_mode'] === self::DISPLAY_ONE_AT_TIME)
618
			{
619
				foreach ($posters as $pm_key => $sender)
620
				{
621
					if (!in_array($pm_key, $display_pms))
622
					{
623
						unset($posters[$pm_key]);
624
					}
625
				}
626
			}
627
628
			// Load some information about the message sender
629
			$posters = array_unique($posters);
630
			if (!empty($posters))
631
			{
632
				MembersList::load($posters);
633
			}
634
635
			// If we're on grouped/restricted view get a restricted list of messages.
636
			if ($context['display_mode'] !== self::DISPLAY_ALL_AT_ONCE)
637
			{
638
				// Get the order right.
639
				$orderBy = [];
640
				foreach (array_reverse($pms) as $pm)
641
				{
642
					$orderBy[] = 'pm.id_pm = ' . $pm;
643
				}
644
645
				// Separate query for these bits, the callback will use it as required
646
				$subjects_request = loadPMSubjectRequest($pms, $orderBy);
647
			}
648
649
			// Execute the load message query if a message has been chosen and let
650
			// the callback fetch the results.  Otherwise, just show the pm selection list
651
			if (empty($pmsg) && empty($pmID) && $context['display_mode'] !== self::DISPLAY_ALL_AT_ONCE)
652
			{
653
				$messages_request = false;
654
			}
655
			else
656
			{
657
				$messages_request = loadPMMessageRequest($display_pms, $sort_by_query, $sort_by, $descending, $context['display_mode'], $context['folder']);
658
			}
659 2
		}
660
		else
661
		{
662
			$messages_request = false;
663 2
		}
664 2
665 2
		// Initialize the subject and message render callbacks
666 2
		$bodyParser = new Normal(array(), false);
667
		$opt = new ValuesContainer(['recipients' => $recipients]);
668
		$renderer = new PmRenderer($messages_request, $this->user, $bodyParser, $opt);
669 2
		$subject_renderer = new PmRenderer($subjects_request ?? $messages_request, $this->user, $bodyParser, $opt);
670 2
671
		// Subject and Message
672
		$context['get_pmessage'] = [$renderer, 'getContext'];
673 2
		$context['get_psubject'] = [$subject_renderer, 'getContext'];
674 2
675 2
		// Prepare some items for the template
676 2
		$context['topic_starter_id'] = 0;
677 2
		$context['can_send_pm'] = allowedTo('pm_send');
678 2
		$context['can_send_email'] = allowedTo('send_email_to_members');
679 2
		$context['sub_template'] = 'folder';
680
		$context['page_title'] = $txt['pm_inbox'];
681 2
		$context['sort_direction'] = $descending ? 'down' : 'up';
682
		$context['sort_by'] = $sort_by;
683
684
		if ($messages_request !== false && !empty($context['show_delete']) && $messages_request->hasResults())
685
		{
686
			theme()->getLayers()->addEnd('pm_pages_and_buttons');
687
		}
688
689
		// Set up the page index.
690
		$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']);
691
		$context['start'] = $start;
692
693
		$context['pm_form_url'] = $scripturl . '?action=pm;sa=pmactions;' . ($context['display_mode'] === self::DISPLAY_AS_CONVERSATION ? 'conversation;' : '') . 'f=' . $context['folder'] . ';start=' . $context['start'] . ($context['current_label_id'] !== -1 ? ';l=' . $context['current_label_id'] : '');
694
695
		// Finally, mark the relevant messages as read.
696
		if ($context['folder'] !== 'sent' && !empty($context['labels'][(int) $context['current_label_id']]['unread_messages']))
697
		{
698
			// If the display mode is "old sk00l" do them all...
699
			if ($context['display_mode'] === self::DISPLAY_ALL_AT_ONCE)
700 2
			{
701 2
				markMessages(null, $context['current_label_id']);
702
			}
703 2
			// Otherwise do just the currently displayed ones!
704
			elseif (!empty($context['current_pm']))
705
			{
706 2
				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...
707
			}
708
		}
709
710
		// Build the conversation button array.
711
		if ($context['display_mode'] === self::DISPLAY_AS_CONVERSATION && !empty($context['current_pm']))
712
		{
713
			$context['conversation_buttons'] = array(
714
				'delete' => array(
715
					'text' => 'delete_conversation',
716
					'lang' => true,
717
					'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'],
718
					'custom' => 'onclick="return confirm(\'' . addslashes($txt['remove_message']) . '?\');"'
719
				),
720
			);
721 2
722
			// Allow mods to add additional buttons here
723
			call_integration_hook('integrate_conversation_buttons');
724
		}
725
	}
726
727
	/**
728
	 * Send a new personal message?
729
	 *
730
	 * @throws Exception pm_not_yours
731
	 */
732
	public function action_send()
733
	{
734
		global $txt, $modSettings, $context;
735
736 2
		// Load in some text and template dependencies
737
		Txt::load('PersonalMessage');
738
		theme()->getTemplates()->load('PersonalMessage');
739
740
		// Set the template we will use
741 2
		$context['sub_template'] = 'send';
742
743 2
		// Extract out the spam settings - cause it's neat.
744
		[$modSettings['max_pm_recipients'], $modSettings['pm_posts_verification'], $modSettings['pm_posts_per_hour']] = explode(',', $modSettings['pm_spam_settings']);
745
746 2
		// Set up some items for the template
747 2
		$context['page_title'] = $txt['send_message'];
748
		$context['reply'] = isset($this->_req->query->pmsg) || isset($this->_req->query->quote);
749
750 2
		// Check whether we've gone over the limit of messages we can send per hour.
751
		if (!empty($modSettings['pm_posts_per_hour'])
752
			&& !allowedTo(array('admin_forum', 'moderate_forum', 'send_mail'))
753 2
			&& $this->user->mod_cache['bq'] === '0=1'
0 ignored issues
show
Bug Best Practice introduced by
The property mod_cache does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
754
			&& $this->user->mod_cache['gq'] === '0=1')
755
		{
756 2
			// How many messages have they sent this last hour?
757 2
			$pmCount = pmCount($this->user->id, 3600);
0 ignored issues
show
Bug Best Practice introduced by
The property id does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
758
759
			if (!empty($pmCount) && $pmCount >= $modSettings['pm_posts_per_hour'])
760 2
			{
761
				throw new Exception('pm_too_many_per_hour', true, array($modSettings['pm_posts_per_hour']));
762
			}
763
		}
764
765
		try
766
		{
767
			$this->_events->trigger('before_set_context', array('pmsg' => $this->_req->query->pmsg ?? ($this->_req->query->quote ?? 0)));
768
		}
769
		catch (PmErrorException $pmErrorException)
770
		{
771
			$this->messagePostError($pmErrorException->namedRecipientList, $pmErrorException->recipientList, $pmErrorException->msgOptions);
0 ignored issues
show
Bug introduced by
$pmErrorException->msgOptions of type ElkArte\Helper\ValuesContainer is incompatible with the type array 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

771
			$this->messagePostError($pmErrorException->namedRecipientList, $pmErrorException->recipientList, /** @scrutinizer ignore-type */ $pmErrorException->msgOptions);
Loading history...
772
			return;
773 2
		}
774
775
		// Quoting / Replying to a message?
776
		if (!empty($this->_req->query->pmsg))
777
		{
778
			$pmsg = $this->_req->getQuery('pmsg', 'intval');
779
780
			// Make sure this is accessible (not deleted)
781 2
			if (!isAccessiblePM($pmsg))
782
			{
783
				throw new Exception('no_access', false);
784
			}
785
786
			// Validate that this is one has been received?
787
			$isReceived = checkPMReceived($pmsg);
788
789
			// Get the quoted message (and make sure you're allowed to see this quote!).
790
			$row_quoted = loadPMQuote($pmsg, $isReceived);
791
			if ($row_quoted === false)
0 ignored issues
show
introduced by
The condition $row_quoted === false is always false.
Loading history...
792
			{
793
				throw new Exception('pm_not_yours', false);
794
			}
795
796
			// Censor the message.
797
			$row_quoted['subject'] = censor($row_quoted['subject']);
798
			$row_quoted['body'] = censor($row_quoted['body']);
799
800
			// Let's make sure we mark this one as read
801
			markMessages($pmsg);
802
803
			// Figure out which flavor or 'Re: ' to use
804
			$context['response_prefix'] = response_prefix();
805
806
			$form_subject = $row_quoted['subject'];
807
808
			// Add 'Re: ' to it....
809
			if ($context['reply'] && trim($context['response_prefix']) !== '' && Util::strpos($form_subject, trim($context['response_prefix'])) !== 0)
810
			{
811
				$form_subject = $context['response_prefix'] . $form_subject;
812
			}
813
814
			// If quoting, lets clean up some things and set the quote header for the pm body
815
			if (isset($this->_req->query->quote))
816
			{
817
				// Remove any nested quotes and <br />...
818
				$form_message = preg_replace('~<br ?/?>~i', "\n", $row_quoted['body']);
819
				$form_message = removeNestedQuotes($form_message);
820
821
				if (empty($row_quoted['id_member']))
822
				{
823
					$form_message = '[quote author=&quot;' . $row_quoted['real_name'] . '&quot;]' . "\n" . $form_message . "\n" . '[/quote]';
824
				}
825
				else
826
				{
827
					$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]';
828
				}
829
			}
830
			else
831
			{
832
				$form_message = '';
833
			}
834
835
			// Allow them to QQ the message they are replying to
836
			loadJavascriptFile('quickQuote.js', ['defer' => true]);
837
			theme()->addInlineJavascript("
838
				document.addEventListener('DOMContentLoaded', () => new Elk_QuickQuote(), false);", true
839
			);
840
841
			// Do the BBC thang on the message.
842
			$bbc_parser = ParserWrapper::instance();
843
			$row_quoted['body'] = $bbc_parser->parsePM($row_quoted['body']);
844
845
			// Set up the quoted message array.
846
			$context['quoted_message'] = array(
847
				'id' => $row_quoted['id_pm'],
848
				'pm_head' => $row_quoted['pm_head'],
849
				'member' => array(
850
					'name' => $row_quoted['real_name'],
851
					'username' => $row_quoted['member_name'],
852
					'id' => $row_quoted['id_member'],
853
					'href' => empty($row_quoted['id_member']) ? '' : getUrl('profile', ['action' => 'profile', 'u' => $row_quoted['id_member']]),
854
					'link' => empty($row_quoted['id_member']) ? $row_quoted['real_name'] : '<a href="' . getUrl('profile', ['action' => 'profile', 'u' => $row_quoted['id_member']]) . '">' . $row_quoted['real_name'] . '</a>',
855
				),
856
				'subject' => $row_quoted['subject'],
857
				'time' => standardTime($row_quoted['msgtime']),
858
				'html_time' => htmlTime($row_quoted['msgtime']),
859
				'timestamp' => forum_time(true, $row_quoted['msgtime']),
860
				'body' => $row_quoted['body']
861
			);
862
		}
863
		// A new message it is then
864
		else
865 2
		{
866 2
			$context['quoted_message'] = false;
867 2
			$form_subject = '';
868
			$form_message = '';
869
		}
870
871 2
		// Start of like we don't know where this is going
872
		$context['recipients'] = array(
873
			'to' => array(),
874
			'bcc' => array(),
875
		);
876
877 2
		// Sending by ID?  Replying to all?  Fetch the real_name(s).
878
		if (isset($this->_req->query->u))
879
		{
880
			// If the user is replying to all, get all the other members this was sent to..
881
			if ($this->_req->query->u === 'all' && isset($row_quoted))
882
			{
883
				// Firstly, to reply to all we clearly already have $row_quoted - so have the original member from.
884
				if ($row_quoted['id_member'] != $this->user->id)
885
				{
886
					$context['recipients']['to'][] = array(
887
						'id' => $row_quoted['id_member'],
888
						'name' => htmlspecialchars($row_quoted['real_name'], ENT_COMPAT),
889
					);
890
				}
891
892
				// Now to get all the others.
893
				$context['recipients']['to'] = array_merge($context['recipients']['to'], isset($pmsg) ? loadPMRecipientsAll($pmsg) : array());
894
			}
895
			else
896
			{
897
				$users = array_map('intval', explode(',', $this->_req->query->u));
898
				$users = array_unique($users);
899
900
				// For all the member's this is going to, get their display name.
901
				require_once(SUBSDIR . '/Members.subs.php');
902
				$result = getBasicMemberData($users);
903
904
				foreach ($result as $row)
905
				{
906
					$context['recipients']['to'][] = array(
907
						'id' => $row['id_member'],
908
						'name' => $row['real_name'],
909
					);
910
				}
911
			}
912
913
			// Get a literal name list in case the user has JavaScript disabled.
914
			$names = array();
915
			foreach ($context['recipients']['to'] as $to)
916
			{
917
				$names[] = $to['name'];
918
			}
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['breadcrumbs'][] = [
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
			'smiley_container' => 'smileyBox_message',
950
			'bbc_container' => 'bbcBox_message',
951 2
			'preview_type' => 2,
952
		);
953 2
954
		// Trigger the prepare_send_context PM event
955
		$this->_events->trigger('prepare_send_context', array('editorOptions' => &$editorOptions));
956 2
957
		create_control_richedit($editorOptions);
958
959 2
		// No one is bcc'ed just yet
960 2
		$context['bcc_value'] = '';
961
962
		// Register this form and get a sequence number in $context.
963
		checkSubmitOnce('register');
964
	}
965
966
	/**
967
	 * An error in the message...
968
	 *
969
	 * @param array $named_recipients
970
	 * @param array $recipient_ids array keys of [bbc] => int[] and [to] => int[]
971
	 * @param array $msg_options body, subject and reply values
972
	 *
973
	 * @throws Exception pm_not_yours
974
	 */
975
	public function messagePostError($named_recipients, $recipient_ids = array(), $msg_options = null)
976
	{
977
		global $txt, $context, $modSettings;
978
979
		if ($this->getApi() !== false)
980
		{
981
			$context['sub_template'] = 'generic_preview';
982
		}
983
		else
984
		{
985
			$context['sub_template'] = 'send';
986
			$context['menu_data_' . $context['pm_menu_id']]['current_area'] = 'send';
987
		}
988
989
		$context['page_title'] = $txt['send_message'];
990
		$error_types = ErrorContext::context('pm', 1);
991
992
		// Got some known members?
993
		$context['recipients'] = array(
994
			'to' => array(),
995
			'bcc' => array(),
996
		);
997
998
		if (!empty($recipient_ids['to']) || !empty($recipient_ids['bcc']))
999
		{
1000
			$allRecipients = array_merge($recipient_ids['to'], $recipient_ids['bcc']);
1001
1002
			require_once(SUBSDIR . '/Members.subs.php');
1003
1004
			// Get the latest activated member's display name.
1005
			$result = getBasicMemberData($allRecipients);
1006
			foreach ($result as $row)
1007
			{
1008
				$recipientType = in_array($row['id_member'], $recipient_ids['bcc']) ? 'bcc' : 'to';
1009
				$context['recipients'][$recipientType][] = array(
1010
					'id' => $row['id_member'],
1011
					'name' => $row['real_name'],
1012
				);
1013
			}
1014
		}
1015
1016
		// Set everything up like before....
1017
		if (!empty($msg_options))
1018
		{
1019
			$context['subject'] = $msg_options->subject;
1020
			$context['message'] = $msg_options->body;
1021
			$context['reply'] = $msg_options->reply_to;
1022
		}
1023
		else
1024
		{
1025
			$context['subject'] = isset($this->_req->post->subject) ? Util::htmlspecialchars($this->_req->post->subject) : '';
1026
			$context['message'] = isset($this->_req->post->message) ? str_replace(array('  '), array('&nbsp; '), Util::htmlspecialchars($this->_req->post->message, ENT_QUOTES, 'UTF-8', true)) : '';
1027
			$context['reply'] = !empty($this->_req->post->replied_to);
1028
		}
1029
1030
		// If this is a reply to message, we need to reload the quote
1031
		if ($context['reply'])
1032
		{
1033
			$pmsg = (int) $this->_req->post->replied_to;
1034
			$isReceived = $context['folder'] !== 'sent';
1035
			$row_quoted = loadPMQuote($pmsg, $isReceived);
1036
			if ($row_quoted === false)
0 ignored issues
show
introduced by
The condition $row_quoted === false is always false.
Loading history...
1037
			{
1038
				if ($this->getApi() === false)
1039
				{
1040
					throw new Exception('pm_not_yours', false);
1041
				}
1042
1043
				$error_types->addError('pm_not_yours');
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']) ? $row_quoted['real_name'] : '<a href="' . getUrl('profile', ['action' => 'profile', 'u' => $row_quoted['id_member']]) . '">' . $row_quoted['real_name'] . '</a>',
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['breadcrumbs'][] = [
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
			'smiley_container' => 'smileyBox_message',
1096
			'bbc_container' => 'bbcBox_message',
1097
			'preview_type' => 2,
1098
		);
1099
1100
		// Trigger the prepare_send_context PM event
1101
		$this->_events->trigger('prepare_send_context', array('editorOptions' => &$editorOptions));
1102
1103
		create_control_richedit($editorOptions);
1104
1105
		// Check whether we need to show the code again.
1106
		$context['require_verification'] = $this->user->is_admin === false && !empty($modSettings['pm_posts_verification']) && $this->user->posts < $modSettings['pm_posts_verification'];
0 ignored issues
show
Bug Best Practice introduced by
The property is_admin does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
Bug Best Practice introduced by
The property posts does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
1107
		if ($context['require_verification'] && $this->getApi() === false)
1108
		{
1109
			$verificationOptions = array(
1110
				'id' => 'pm',
1111
			);
1112
			$context['require_verification'] = VerificationControlsIntegrate::create($verificationOptions);
1113
			$context['visual_verification_id'] = $verificationOptions['id'];
1114
		}
1115
1116
		$context['to_value'] = empty($named_recipients['to']) ? '' : '&quot;' . implode('&quot;, &quot;', $named_recipients['to']) . '&quot;';
1117
		$context['bcc_value'] = empty($named_recipients['bcc']) ? '' : '&quot;' . implode('&quot;, &quot;', $named_recipients['bcc']) . '&quot;';
1118
1119
		// No check for the previous submission is needed.
1120
		checkSubmitOnce('free');
1121
1122
		// Acquire a new form sequence number.
1123
		checkSubmitOnce('register');
1124
	}
1125 2
1126
	/**
1127 2
	 * Send a personal message.
1128
	 */
1129
	public function action_send2()
1130 2
	{
1131 2
		global $txt, $context, $modSettings;
1132
1133 2
		// All the helpers we need
1134
		require_once(SUBSDIR . '/Auth.subs.php');
1135
		require_once(SUBSDIR . '/Post.subs.php');
1136 2
1137
		Txt::load('PersonalMessage', false);
1138
1139 2
		// Extract out the spam settings - it saves database space!
1140
		[$modSettings['max_pm_recipients'], $modSettings['pm_posts_verification'], $modSettings['pm_posts_per_hour']] = explode(',', $modSettings['pm_spam_settings']);
1141
1142 2
		// Initialize the errors we're about to make.
1143 2
		$post_errors = ErrorContext::context('pm', 1);
1144 2
1145 2
		// Check whether we've gone over the limit of messages we can send per hour - fatal error if fails!
1146
		if (!empty($modSettings['pm_posts_per_hour'])
1147
			&& !allowedTo(array('admin_forum', 'moderate_forum', 'send_mail'))
1148
			&& $this->user->mod_cache['bq'] === '0=1'
0 ignored issues
show
Bug Best Practice introduced by
The property mod_cache does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
1149
			&& $this->user->mod_cache['gq'] === '0=1')
1150
		{
1151
			// How many have they sent this last hour?
1152
			$pmCount = pmCount($this->user->id, 3600);
0 ignored issues
show
Bug Best Practice introduced by
The property id does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
1153
1154
			if (!empty($pmCount) && $pmCount >= $modSettings['pm_posts_per_hour'])
1155
			{
1156
				if ($this->getApi() === false)
1157
				{
1158
					throw new Exception('pm_too_many_per_hour', true, array($modSettings['pm_posts_per_hour']));
1159
				}
1160
1161
				$post_errors->addError('pm_too_many_per_hour');
1162
			}
1163
		}
1164
1165 2
		// If your session timed out, show an error, but do allow to re-submit.
1166
		if ($this->getApi() === false && checkSession('post', '', false) !== '')
1167
		{
1168
			$post_errors->addError('session_timeout');
1169
		}
1170 2
1171 2
		$this->_req->post->subject = isset($this->_req->post->subject) ? strtr(Util::htmltrim($this->_req->post->subject), array("\r" => '', "\n" => '', "\t" => '')) : '';
1172 2
		$this->_req->post->to = $this->_req->getPost('to', 'trim', empty($this->_req->query->to) ? '' : $this->_req->query->to);
1173
		$this->_req->post->bcc = $this->_req->getPost('bcc', 'trim', empty($this->_req->query->bcc) ? '' : $this->_req->query->bcc);
1174
1175 2
		// Route the input from the 'u' parameter to the 'to'-list.
1176
		if (!empty($this->_req->post->u))
1177 2
		{
1178
			$this->_req->post->recipient_to = explode(',', $this->_req->post->u);
1179
		}
1180 2
1181
		$bbc_parser = ParserWrapper::instance();
1182
1183 2
		// Construct the list of recipients.
1184 2
		$recipientList = [];
1185 2
		$namedRecipientList = [];
1186 2
		$namesNotFound = [];
1187
		foreach (['to', 'bcc'] as $recipientType)
1188
		{
1189 2
			// First, let's see if there's user ID's given.
1190 2
			$recipientList[$recipientType] = [];
1191 2
			$type = 'recipient_' . $recipientType;
1192
			if (!empty($this->_req->post->{$type}) && is_array($this->_req->post->{$type}))
1193 2
			{
1194
				$recipientList[$recipientType] = array_map('intval', $this->_req->post->{$type});
1195
			}
1196
1197 2
			// Are there also literal names set?
1198
			if (!empty($this->_req->post->{$recipientType}))
1199
			{
1200 2
				// We're going to take out the "s anyway ;).
1201
				$recipientString = strtr($this->_req->post->{$recipientType}, array('\\"' => '"'));
1202 2
1203 2
				preg_match_all('~"([^"]+)"~', $recipientString, $matches);
1204
				$namedRecipientList[$recipientType] = array_unique(array_merge($matches[1], explode(',', preg_replace('~"[^"]+"~', '', $recipientString))));
1205
1206 2
				// Clean any literal names entered
1207
				foreach ($namedRecipientList[$recipientType] as $index => $recipient)
1208 2
				{
1209
					if (trim($recipient) !== '')
1210 2
					{
1211
						$namedRecipientList[$recipientType][$index] = Util::htmlspecialchars(Util::strtolower(trim($recipient)));
1212
					}
1213
					else
1214 1
					{
1215
						unset($namedRecipientList[$recipientType][$index]);
1216
					}
1217
				}
1218
1219 2
				// Now see if we can resolve any entered name (not suggest selected) to an actual user
1220
				if (!empty($namedRecipientList[$recipientType]))
1221 2
				{
1222
					$foundMembers = findMembers($namedRecipientList[$recipientType]);
1223
1224 2
					// Assume all are not found, until proven otherwise.
1225
					$namesNotFound[$recipientType] = $namedRecipientList[$recipientType];
1226
1227 2
					// Make sure we only have each member listed once, in case they did not use the select list
1228
					foreach ($foundMembers as $member)
1229
					{
1230 2
						$testNames = array(
1231 2
							Util::strtolower($member['username']),
1232 2
							Util::strtolower($member['name']),
1233
							Util::strtolower($member['email']),
1234
						);
1235 2
1236
						if (array_intersect($testNames, $namedRecipientList[$recipientType]) !== [])
1237 2
						{
1238
							$recipientList[$recipientType][] = $member['id'];
1239
1240 2
							// Get rid of this username, since we found it.
1241
							$namesNotFound[$recipientType] = array_diff($namesNotFound[$recipientType], $testNames);
1242
						}
1243
					}
1244
				}
1245
			}
1246
1247 2
			// Selected a recipient to be deleted? Remove them now.
1248
			if (!empty($this->_req->post->delete_recipient))
1249
			{
1250
				$recipientList[$recipientType] = array_diff($recipientList[$recipientType], array((int) $this->_req->post->delete_recipient));
1251
			}
1252
1253 2
			// Make sure we don't include the same name twice
1254
			$recipientList[$recipientType] = array_unique($recipientList[$recipientType]);
1255
		}
1256
1257 2
		// Are we changing the recipients somehow?
1258
		$is_recipient_change = !empty($this->_req->post->delete_recipient) || !empty($this->_req->post->to_submit) || !empty($this->_req->post->bcc_submit);
1259
1260 2
		// Check if there's at least one recipient.
1261
		if (empty($recipientList['to']) && empty($recipientList['bcc']))
1262
		{
1263
			$post_errors->addError('no_to');
1264
		}
1265
1266 2
		// Make sure that we remove the members who did get it from the screen.
1267
		if (!$is_recipient_change)
1268 2
		{
1269
			foreach (array_keys($recipientList) as $recipientType)
1270 2
			{
1271
				if (!empty($namesNotFound[$recipientType]))
1272
				{
1273
					$post_errors->addError('bad_' . $recipientType);
1274
1275
					// Since we already have a post error, remove the previous one.
1276
					$post_errors->removeError('no_to');
1277
1278
					foreach ($namesNotFound[$recipientType] as $name)
1279 1
					{
1280
						$context['send_log']['failed'][] = sprintf($txt['pm_error_user_not_found'], $name);
1281
					}
1282
				}
1283
			}
1284
		}
1285
1286 2
		// Did they make any mistakes like no subject or message?
1287
		if ($this->_req->post->subject === '')
1288
		{
1289
			$post_errors->addError('no_subject');
1290
		}
1291 2
1292
		if ($this->_req->getPost('message', 'trim', '') === '')
1293
		{
1294
			$post_errors->addError('no_message');
1295 2
		}
1296
		elseif (!empty($modSettings['max_messageLength']) && Util::strlen($this->_req->post->message) > $modSettings['max_messageLength'])
1297
		{
1298
			$post_errors->addError('long_message');
1299
		}
1300
		else
1301
		{
1302 2
			// Preparse the message.
1303 2
			$message = $this->_req->getPost('message', 'trim', '');
1304
			preparsecode($message);
1305
1306 2
			// Make sure there's still some content left without the tags.
1307
			if (Util::htmltrim(strip_tags($bbc_parser->parsePM(Util::htmlspecialchars($message, ENT_QUOTES)), '<img>')) === ''
1308
				&& (!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()
1316
			&& !$is_recipient_change
1317
			&& !$this->_req->isSet('preview')
1318
			&& $this->getApi() === false)
1319
		{
1320
			$this->messagePostError($namedRecipientList, $recipientList);
1321 2
1322
			return false;
1323
		}
1324
1325
		// Want to take a second glance before you send?
1326
		if ($this->_req->isSet('preview'))
1327
		{
1328
			// Set everything up to be displayed.
1329
			$context['preview_subject'] = Util::htmlspecialchars($this->_req->post->subject);
1330
			$context['preview_message'] = Util::htmlspecialchars($this->_req->post->message, ENT_QUOTES, 'UTF-8', true);
1331
			preparsecode($context['preview_message'], true);
1332
1333
			// Parse out the BBC if it is enabled.
1334
			$context['preview_message'] = $bbc_parser->parsePM($context['preview_message']);
1335
1336
			// Censor, as always.
1337
			$context['preview_subject'] = censor($context['preview_subject']);
1338
			$context['preview_message'] = censor($context['preview_message']);
1339
1340
			// Set a descriptive title.
1341
			$context['page_title'] = $txt['preview'] . ' - ' . $context['preview_subject'];
1342
1343
			// Pretend they messed up but don't ignore if they really did :P.
1344 2
			$this->messagePostError($namedRecipientList, $recipientList);
1345
1346
			return false;
1347
		}
1348
1349
		if ($is_recipient_change)
1350
		{
1351
			// Maybe we couldn't find one?
1352
			foreach ($namesNotFound as $recipientType => $names)
1353
			{
1354
				$post_errors->addError('bad_' . $recipientType);
1355
				foreach ($names as $name)
1356
				{
1357
					$context['send_log']['failed'][] = sprintf($txt['pm_error_user_not_found'], $name);
1358
				}
1359
			}
1360
1361
			$this->messagePostError($namedRecipientList, $recipientList);
1362
1363 2
			return true;
1364
		}
1365
1366
		// Adding a recipient cause javascript ain't working?
1367
		try
1368
		{
1369
			$this->_events->trigger('before_sending', array('namedRecipientList' => $namedRecipientList, 'recipientList' => $recipientList, 'namesNotFound' => $namesNotFound, 'post_errors' => $post_errors));
1370
		}
1371 2
		catch (ControllerRedirectException)
1372
		{
1373
			$this->messagePostError($namedRecipientList, $recipientList);
1374
1375
			return true;
1376
		}
1377
1378
		// Safety net, it may be a module may just add to the list of errors without actually throw the error
1379 2
		if ($post_errors->hasErrors() && !$this->_req->isSet('preview') && $this->getApi() === false)
1380
		{
1381
			$this->messagePostError($namedRecipientList, $recipientList);
1382
1383
			return false;
1384
		}
1385
1386
		// Before we send the PM, let's make sure we don't have an abuse of numbers.
1387
		if (!empty($modSettings['max_pm_recipients']) && count($recipientList['to']) + count($recipientList['bcc']) > $modSettings['max_pm_recipients'] && !allowedTo(array('moderate_forum', 'send_mail', 'admin_forum')))
1388
		{
1389
			$context['send_log'] = array(
1390
				'sent' => array(),
1391
				'failed' => array(sprintf($txt['pm_too_many_recipients'], $modSettings['max_pm_recipients'])),
1392 2
			);
1393
1394
			$this->messagePostError($namedRecipientList, $recipientList);
1395 2
1396
			return false;
1397
		}
1398 2
1399
		// Protect from message spamming.
1400 2
		spamProtection('pm');
1401
1402
		// Prevent double submission of this form.
1403
		checkSubmitOnce('check');
1404
1405
		// Finally do the actual sending of the PM.
1406
		if (!empty($recipientList['to']) || !empty($recipientList['bcc']))
1407
		{
1408
			$context['send_log'] = sendpm($recipientList, $this->_req->post->subject, $this->_req->post->message, true, null, empty($this->_req->post->pm_head) ? 0 : (int) $this->_req->post->pm_head);
1409
		}
1410
		else
1411 2
		{
1412
			$context['send_log'] = array(
1413
				'sent' => array(),
1414
				'failed' => array()
1415
			);
1416
		}
1417 2
1418 2
		// Mark the message as "replied to".
1419
		$replied_to = $this->_req->getPost('replied_to', 'intval', 0);
1420
		$box = $this->_req->getPost('f', 'trim', '');
1421 2
		if (!empty($context['send_log']['sent']) && !empty($replied_to) && $box === 'inbox')
1422
		{
1423
			require_once(SUBSDIR . '/PersonalMessage.subs.php');
1424
			setPMRepliedStatus($this->user->id, $replied_to);
1425
		}
1426
1427
		$failed = !empty($context['send_log']['failed']);
1428
		$this->_events->trigger('message_sent', array('failed' => $failed));
1429
1430
		// If one or more of the recipients were invalid, go back to the post screen with the failed usernames.
1431
		if ($failed)
1432
		{
1433 2
			$this->messagePostError($namesNotFound, array(
1434
				'to' => array_intersect($recipientList['to'], $context['send_log']['failed']),
1435
				'bcc' => array_intersect($recipientList['bcc'], $context['send_log']['failed'])
1436
			));
1437 2
1438
			return false;
1439 2
		}
1440
1441
		// Message sent successfully
1442
		$context['current_label_redirect'] .= ';done=sent';
1443
1444
		// Go back to the where they sent from, if possible...
1445
		redirectexit($context['current_label_redirect']);
1446
1447
		return true;
1448
	}
1449
1450
	/**
1451
	 * This function performs all additional actions including the deleting
1452
	 * and labeling of PM's
1453
	 */
1454
	public function action_pmactions()
1455
	{
1456
		global $context;
1457
1458
		checkSession('request');
1459
1460
		// Sending in the single pm choice via GET
1461
		$pm_actions = $this->_req->getQuery('pm_actions', null, '');
1462
1463
		// Set the action to apply to the PMs defined by pm_actions (yes it is that brilliant)
1464
		$pm_action = $this->_req->getPost('pm_action', 'trim', '');
1465
		$pm_action = empty($pm_action) && isset($this->_req->post->del_selected) ? 'delete' : $pm_action;
1466
1467
		// Create a list of PMs that we need to work on
1468
		if ($pm_action !== ''
1469
			&& !empty($this->_req->post->pms)
1470
			&& is_array($this->_req->post->pms))
1471
		{
1472
			$pm_actions = array();
1473
			foreach ($this->_req->post->pms as $pm)
1474
			{
1475
				$pm_actions[(int) $pm] = $pm_action;
1476
			}
1477
		}
1478
1479
		// No messages to action then bug out
1480
		if (empty($pm_actions))
1481
		{
1482
			redirectexit($context['current_label_redirect']);
1483
		}
1484
1485
		// If we are in conversation, we may need to apply this to every message in that conversation.
1486
		if ($context['display_mode'] === self::DISPLAY_AS_CONVERSATION && isset($this->_req->query->conversation))
1487
		{
1488
			$id_pms = array_map('intval', array_keys($pm_actions));
1489
			$pm_heads = getDiscussions($id_pms);
1490
			$pms = getPmsFromDiscussion(array_keys($pm_heads));
1491
1492
			// Copy the action from the single to PM to the others in the conversation.
1493
			foreach ($pms as $id_pm => $id_head)
1494
			{
1495
				if (isset($pm_heads[$id_head], $pm_actions[$pm_heads[$id_head]]))
1496
				{
1497
					$pm_actions[$id_pm] = $pm_actions[$pm_heads[$id_head]];
1498
				}
1499
			}
1500
		}
1501
1502
		// Get to doing what we've been told
1503
		$to_delete = array();
1504
		$to_label = array();
1505
		$label_type = array();
1506
		foreach ($pm_actions as $pm => $action)
1507
		{
1508
			// What are we doing with the selected messages, adding a label, removing, other?
1509
			switch (substr($action, 0, 4))
1510
			{
1511
				case 'dele':
1512
					$to_delete[] = (int) $pm;
1513
					break;
1514
				case 'add_':
1515
					$type = 'add';
1516
					$action = substr($action, 4);
1517
					break;
1518
				case 'rem_':
1519
					$type = 'rem';
1520
					$action = substr($action, 4);
1521
					break;
1522
				default:
1523
					$type = 'unk';
1524
			}
1525
1526
			if ((int) $action === -1 || (int) $action === 0 || (int) $action > 0)
1527
			{
1528
				$to_label[(int) $pm] = (int) $action;
1529
				$label_type[(int) $pm] = $type ?? '';
1530
			}
1531
		}
1532
1533
		// Deleting, it looks like?
1534
		if (!empty($to_delete))
1535
		{
1536
			deleteMessages($to_delete, $context['display_mode'] === self::DISPLAY_AS_CONVERSATION ? null : $context['folder']);
1537
		}
1538
1539
		// Are we labelling anything?
1540
		if (!empty($to_label) && $context['folder'] === 'inbox')
1541
		{
1542
			$updateErrors = changePMLabels($to_label, $label_type, $this->user->id);
1543
1544
			// Any errors?
1545
			if (!empty($updateErrors))
1546
			{
1547
				throw new Exception('labels_too_many', true, array($updateErrors));
1548
			}
1549
		}
1550
1551
		// Back to the folder.
1552
		$_SESSION['pm_selected'] = array_keys($to_label);
1553
		redirectexit($context['current_label_redirect'] . (count($to_label) === 1 ? '#msg_' . $_SESSION['pm_selected'][0] : ''));
1554
	}
1555
1556
	/**
1557
	 * Are you sure you want to PERMANENTLY (mostly) delete ALL your messages?
1558
	 */
1559
	public function action_removeall()
1560
	{
1561
		global $txt, $context;
1562
1563
		// Only have to set up the template....
1564
		$context['sub_template'] = 'ask_delete';
1565
		$context['page_title'] = $txt['delete_all'];
1566
		$context['delete_all'] = $this->_req->query->f === 'all';
1567
1568
		// And set the folder name...
1569
		$txt['delete_all'] = str_replace('PMBOX', $context['folder'] != 'sent' ? $txt['inbox'] : $txt['sent_items'], $txt['delete_all']);
1570
	}
1571
1572
	/**
1573
	 * Delete ALL the messages!
1574
	 */
1575
	public function action_removeall2()
1576
	{
1577
		global $context;
1578
1579
		checkSession('get');
1580
1581
		// If all then delete all messages the user has.
1582
		if ($this->_req->query->f === 'all')
1583
		{
1584
			deleteMessages(null);
1585
		}
1586
		// Otherwise just the selected folder.
1587
		else
1588
		{
1589
			deleteMessages(null, $this->_req->query->f != 'sent' ? 'inbox' : 'sent');
1590
		}
1591
1592
		// Done... all gone.
1593
		redirectexit($context['current_label_redirect']);
1594
	}
1595
1596
	/**
1597
	 * This function allows the user to prune (delete) all messages older than a supplied duration.
1598
	 */
1599
	public function action_prune()
1600
	{
1601
		global $txt, $context;
1602
1603
		// Actually delete the messages.
1604
		if (isset($this->_req->post->age))
1605
		{
1606
			checkSession();
1607
1608
			// Calculate the time to delete before.
1609
			$deleteTime = max(0, time() - (86400 * (int) $this->_req->post->age));
1610
1611
			// Select all the messages older than $deleteTime.
1612
			$toDelete = getPMsOlderThan($this->user->id, $deleteTime);
1613
1614
			// Delete the actual messages.
1615
			deleteMessages($toDelete);
1616
1617
			// Go back to their inbox.
1618
			redirectexit($context['current_label_redirect']);
1619
		}
1620
1621
		// Build the link tree elements.
1622
		$context['breadcrumbs'][] = array(
1623
			'url' => getUrl('action', ['action' => 'pm', 'sa' => 'prune']),
1624
			'name' => $txt['pm_prune']
1625
		);
1626
		$context['sub_template'] = 'prune';
1627
		$context['page_title'] = $txt['pm_prune'];
1628
	}
1629
1630
	/**
1631
	 * This function handles adding, deleting and editing labels on messages.
1632
	 */
1633
	public function action_manlabels()
1634
	{
1635
		global $txt, $context;
1636
1637
		require_once(SUBSDIR . '/PersonalMessage.subs.php');
1638
1639
		// Build the link tree elements...
1640
		$context['breadcrumbs'][] = array(
1641
			'url' => getUrl('action', ['action' => 'pm', 'sa' => 'manlabels']),
1642
			'name' => $txt['pm_manage_labels']
1643
		);
1644
1645
		// Some things for the template
1646
		$context['page_title'] = $txt['pm_manage_labels'];
1647
		$context['sub_template'] = 'labels';
1648
1649
		// Add all existing labels to the array to save, slashing them as necessary...
1650
		$the_labels = array();
1651
		foreach ($context['labels'] as $label)
1652
		{
1653
			if ($label['id'] !== -1)
1654
			{
1655
				$the_labels[$label['id']] = $label['name'];
1656
			}
1657
		}
1658
1659
		// Submitting changes?
1660
		if (isset($this->_req->post->add) || isset($this->_req->post->delete) || isset($this->_req->post->save))
1661
		{
1662
			checkSession();
1663
1664
			// This will be for updating messages.
1665
			$message_changes = array();
1666
			$new_labels = array();
1667
			$rule_changes = array();
1668
1669
			// Will most likely need this.
1670
			loadRules();
1671
1672
			// Adding a new label?
1673
			if (isset($this->_req->post->add))
1674
			{
1675
				$this->_req->post->label = strtr(Util::htmlspecialchars(trim($this->_req->post->label)), array(',' => '&#044;'));
1676
1677
				if (Util::strlen($this->_req->post->label) > 30)
1678
				{
1679
					$this->_req->post->label = Util::substr($this->_req->post->label, 0, 30);
1680
				}
1681
1682
				if ($this->_req->post->label !== '')
1683
				{
1684
					$the_labels[] = $this->_req->post->label;
1685
				}
1686
			}
1687
			// Deleting an existing label?
1688
			elseif (isset($this->_req->post->delete, $this->_req->post->delete_label))
1689
			{
1690
				$i = 0;
1691
				foreach (array_keys($the_labels) as $id)
1692
				{
1693
					if (isset($this->_req->post->delete_label[$id]))
1694
					{
1695
						unset($the_labels[$id]);
1696
						$message_changes[$id] = true;
1697
					}
1698
					else
1699
					{
1700
						$new_labels[$id] = $i++;
1701
					}
1702
				}
1703
			}
1704
			// The hardest one to deal with... changes.
1705
			elseif (isset($this->_req->post->save) && !empty($this->_req->post->label_name))
1706
			{
1707
				$i = 0;
1708
				foreach (array_keys($the_labels) as $id)
1709
				{
1710
					if ($id === -1)
1711
					{
1712
						continue;
1713
					}
1714
1715
					if (isset($this->_req->post->label_name[$id]))
1716
					{
1717
						// Prepare the label name
1718
						$this->_req->post->label_name[$id] = trim(strtr(Util::htmlspecialchars($this->_req->post->label_name[$id]), array(',' => '&#044;')));
1719
1720
						// Has to fit in the database as well
1721
						if (Util::strlen($this->_req->post->label_name[$id]) > 30)
1722
						{
1723
							$this->_req->post->label_name[$id] = Util::substr($this->_req->post->label_name[$id], 0, 30);
1724
						}
1725
1726
						if ($this->_req->post->label_name[$id] != '')
1727
						{
1728
							$the_labels[(int) $id] = $this->_req->post->label_name[$id];
1729
							$new_labels[$id] = $i++;
1730
						}
1731
						else
1732
						{
1733
							unset($the_labels[(int) $id]);
1734
							$message_changes[(int) $id] = true;
1735
						}
1736
					}
1737
					else
1738
					{
1739
						$new_labels[$id] = $i++;
1740
					}
1741
				}
1742
			}
1743
1744
			// Save the label status.
1745
			require_once(SUBSDIR . '/Members.subs.php');
1746
			updateMemberData($this->user->id, array('message_labels' => implode(',', $the_labels)));
1747
1748
			// Update all the messages currently with any label changes in them!
1749
			if (!empty($message_changes))
1750
			{
1751
				$searchArray = array_keys($message_changes);
1752
1753
				if (!empty($new_labels))
1754
				{
1755
					for ($i = max($searchArray) + 1, $n = max(array_keys($new_labels)); $i <= $n; $i++)
1756
					{
1757
						$searchArray[] = $i;
1758
					}
1759
				}
1760
1761
				updateLabelsToPM($searchArray, $new_labels, $this->user->id);
1762
1763
				// Now do the same the rules - check through each rule.
1764
				foreach ($context['rules'] as $k => $rule)
1765
				{
1766
					// Each action...
1767
					foreach ($rule['actions'] as $k2 => $action)
1768
					{
1769
						if ($action['t'] !== 'lab' || !in_array($action['v'], $searchArray))
1770
						{
1771
							continue;
1772
						}
1773
1774
						$rule_changes[] = $rule['id'];
1775
1776
						// If we're here we have a label which is either changed or gone...
1777
						if (isset($new_labels[$action['v']]))
1778
						{
1779
							$context['rules'][$k]['actions'][$k2]['v'] = $new_labels[$action['v']];
1780
						}
1781
						else
1782
						{
1783
							unset($context['rules'][$k]['actions'][$k2]);
1784
						}
1785
					}
1786
				}
1787
			}
1788
1789
			// If we have rules to change do so now.
1790
			if (!empty($rule_changes))
1791
			{
1792
				$rule_changes = array_unique($rule_changes);
1793
1794
				// Update/delete as appropriate.
1795
				foreach ($rule_changes as $k => $id)
1796
				{
1797
					if (!empty($context['rules'][$id]['actions']))
1798
					{
1799
						updatePMRuleAction($id, $this->user->id, $context['rules'][$id]['actions']);
1800
						unset($rule_changes[$k]);
1801
					}
1802
				}
1803
1804
				// Anything left here means it's lost all actions...
1805
				if (!empty($rule_changes))
1806
				{
1807
					deletePMRules($this->user->id, $rule_changes);
1808
				}
1809
			}
1810
1811
			// Make sure we're not caching this!
1812
			Cache::instance()->remove('labelCounts:' . $this->user->id);
1813
1814
			// To make the changes appear right away, redirect.
1815
			redirectexit('action=pm;sa=manlabels');
1816
		}
1817
	}
1818
1819
	/**
1820
	 * Allows to edit Personal Message Settings.
1821
	 *
1822
	 * @uses ProfileOptions controller. (@todo refactor this.)
1823
	 * @uses Profile template.
1824
	 * @uses Profile language file.
1825
	 */
1826
	public function action_settings()
1827
	{
1828
		global $txt, $context, $profile_vars, $cur_profile;
1829
1830
		require_once(SUBSDIR . '/Profile.subs.php');
1831
1832
		// Load the member data for editing
1833
		MembersList::load($this->user->id, false, 'profile');
0 ignored issues
show
Bug Best Practice introduced by
The property id does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
1834
		$cur_profile = MembersList::get($this->user->id);
1835
1836
		// Load up the profile template, its where PM settings are located
1837
		Txt::load('Profile');
1838
		theme()->getTemplates()->load('Profile');
1839
1840
		// We want them to submit back to here.
1841
		$context['profile_custom_submit_url'] = getUrl('action', ['action' => 'pm', 'sa' => 'settings', 'save']);
1842
1843
		$context['page_title'] = $txt['pm_settings'];
1844
		$context['user']['is_owner'] = true;
1845
		$context['id_member'] = $this->user->id;
1846
		$context['require_password'] = false;
1847
		$context['menu_item_selected'] = 'settings';
1848
		$context['submit_button_text'] = $txt['pm_settings'];
1849
1850
		// Add our position to the breadcrumbs.
1851
		$context['breadcrumbs'][] = [
1852
			'url' => getUrl('action', ['action' => 'pm', 'sa' => 'settings']),
1853
			'name' => $txt['pm_settings']
1854
		];
1855
1856
		// Are they saving?
1857
		if (isset($this->_req->post->save))
1858
		{
1859
			checkSession();
1860
1861
			// Mimic what profile would do.
1862
			// @todo fix this when Profile.subs is not dependant on this behavior
1863
			$_POST = Util::htmltrim__recursive((array) $this->_req->post);
1864
			$_POST = Util::htmlspecialchars__recursive($_POST);
1865
1866
			// Save the fields.
1867
			$profileFields = new ProfileFields();
1868
			$fields = ProfileOptions::getFields('contactprefs');
1869
			$profileFields->saveProfileFields($fields['fields'], $fields['hook']);
1870
1871
			if (!empty($profile_vars))
1872
			{
1873
				require_once(SUBSDIR . '/Members.subs.php');
1874
				updateMemberData($this->user->id, $profile_vars);
1875
			}
1876
1877
			// Invalidate any cached data and reload so we show the saved values
1878
			Cache::instance()->remove('member_data-profile-' . $this->user->id);
1879
			MembersList::load($this->user->id, false, 'profile');
1880
			$cur_profile = MembersList::get($this->user->id);
1881
		}
1882
1883
		// Load up the fields.
1884
		$controller = new ProfileOptions(new EventManager());
1885
		$controller->setUser(User::$info);
1886
		$controller->pre_dispatch();
1887
		$controller->action_pmprefs();
1888
	}
1889
1890
	/**
1891
	 * Allows the user to report a personal message to an administrator.
1892
	 *
1893
	 * What it does:
1894
	 *
1895
	 * - In the first instance requires that the ID of the message to report is passed through $_GET.
1896
	 * - It allows the user to report to either a particular administrator - or the whole admin team.
1897
	 * - It will forward on a copy of the original message without allowing the reporter to make changes.
1898
	 *
1899
	 * @uses report_message sub-template.
1900
	 */
1901
	public function action_report()
1902
	{
1903
		global $txt, $context, $language, $modSettings;
1904
1905
		// Check that this feature is even enabled!
1906
		if (empty($modSettings['enableReportPM']) || empty($this->_req->getPost('pmsg', 'intval', $this->_req->getQuery('pmsg', 'intval', 0))))
1907
		{
1908
			throw new Exception('no_access', false);
1909
		}
1910
1911
		$pmsg = $this->_req->getQuery('pmsg', 'intval', $this->_req->getPost('pmsg', 'intval', 0));
1912
1913
		if (!isAccessiblePM($pmsg, 'inbox'))
1914
		{
1915
			throw new Exception('no_access', false);
1916
		}
1917
1918
		$context['pm_id'] = $pmsg;
1919
		$context['page_title'] = $txt['pm_report_title'];
1920
		$context['sub_template'] = 'report_message';
1921
1922
		// We'll query some members, we will.
1923
		require_once(SUBSDIR . '/Members.subs.php');
1924
1925
		// If we're here, just send the user to the template, with a few useful context bits.
1926
		if (isset($this->_req->post->report))
1927
		{
1928
			$poster_comment = strtr(Util::htmlspecialchars($this->_req->post->reason), array("\r" => '', "\t" => ''));
1929
1930
			if (Util::strlen($poster_comment) > 254)
1931
			{
1932
				throw new Exception('post_too_long', false);
1933
			}
1934
1935
			// Check the session before proceeding any further!
1936
			checkSession();
1937
1938
			// First, load up the message they want to file a complaint against, and verify it actually went to them!
1939
			[$subject, $body, $time, $memberFromID, $memberFromName, $poster_name, $time_message] = loadPersonalMessage($pmsg);
1940
1941
			require_once(SUBSDIR . '/Messages.subs.php');
1942
1943
			recordReport(array(
1944
				'id_msg' => $pmsg,
1945
				'id_topic' => 0,
1946
				'id_board' => 0,
1947
				'type' => 'pm',
1948
				'id_poster' => $memberFromID,
1949
				'real_name' => $memberFromName,
1950
				'poster_name' => $poster_name,
1951
				'subject' => $subject,
1952
				'body' => $body,
1953
				'time_message' => $time_message,
1954
			), $poster_comment);
1955
1956
			// Remove the line breaks...
1957
			$body = preg_replace('~<br ?/?>~i', "\n", $body);
1958
1959
			$recipients = array();
1960
			$temp = loadPMRecipientsAll($context['pm_id'], true);
1961
			foreach ($temp as $recipient)
1962
			{
1963
				$recipients[] = $recipient['link'];
1964
			}
1965
1966
			// Now let's get out and loop through the admins.
1967
			$admins = admins(isset($this->_req->post->id_admin) ? (int) $this->_req->post->id_admin : 0);
1968
1969
			// Maybe we shouldn't advertise this?
1970
			if (empty($admins))
1971
			{
1972
				throw new Exception('no_access', false);
1973
			}
1974
1975
			$memberFromName = un_htmlspecialchars($memberFromName);
1976
1977
			// Prepare the message storage array.
1978
			$messagesToSend = array();
1979
1980
			// Loop through each admin, and add them to the right language pile...
1981
			foreach ($admins as $id_admin => $admin_info)
1982
			{
1983
				// Need to send in the correct language!
1984
				$cur_language = empty($admin_info['lngfile']) || empty($modSettings['userLanguage']) ? $language : $admin_info['lngfile'];
1985
1986
				if (!isset($messagesToSend[$cur_language]))
1987
				{
1988
					$mtxt = [];
1989
					$lang = new Loader($cur_language, $mtxt, database());
1990
					$lang->load('PersonalMessage', false);
1991
1992
					// Make the body.
1993
					$report_body = str_replace(array('{REPORTER}', '{SENDER}'), array(un_htmlspecialchars($this->user->name), $memberFromName), $mtxt['pm_report_pm_user_sent']);
1994
					$report_body .= "\n" . '[b]' . $this->_req->post->reason . '[/b]' . "\n\n";
1995
					if (!empty($recipients))
1996
					{
1997
						$report_body .= $mtxt['pm_report_pm_other_recipients'] . ' ' . implode(', ', $recipients) . "\n\n";
1998
					}
1999
2000
					$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]';
2001
2002
					// Plonk it in the array ;)
2003
					$messagesToSend[$cur_language] = array(
2004
						'subject' => (Util::strpos($subject, $mtxt['pm_report_pm_subject']) === false ? $mtxt['pm_report_pm_subject'] : '') . un_htmlspecialchars($subject),
2005
						'body' => $report_body,
2006
						'recipients' => array(
2007
							'to' => array(),
2008
							'bcc' => array()
2009
						),
2010
					);
2011
				}
2012
2013
				// Add them to the list.
2014
				$messagesToSend[$cur_language]['recipients']['to'][$id_admin] = $id_admin;
2015
			}
2016
2017
			// Send a different email for each language.
2018
			foreach ($messagesToSend as $message)
2019
			{
2020
				sendpm($message['recipients'], $message['subject'], $message['body']);
2021
			}
2022
2023
			// Leave them with a template.
2024
			$context['sub_template'] = 'report_message_complete';
2025
		}
2026
	}
2027
2028
	/**
2029
	 * List and allow adding/entering all man rules, such as
2030
	 *
2031
	 * What it does:
2032
	 *
2033
	 * - If it itches, it will be scratched.
2034
	 * - Yes or No are perfectly acceptable answers to almost every question.
2035
	 * - Men see in only 16 colors, Peach, for example, is a fruit, not a color.
2036
	 *
2037
	 * @uses sub template rules
2038
	 */
2039
	public function action_manrules()
2040
	{
2041
		global $txt, $context;
2042
2043
		require_once(SUBSDIR . '/PersonalMessage.subs.php');
2044
2045
		// The link tree - gotta have this :o
2046
		$context['breadcrumbs'][] = [
2047
			'url' => getUrl('action', ['action' => 'pm', 'sa' => 'manrules']),
2048
			'name' => $txt['pm_manage_rules']
2049
		];
2050
2051
		$context['page_title'] = $txt['pm_manage_rules'];
2052
		$context['sub_template'] = 'rules';
2053
2054
		// Load them... load them!!
2055
		loadRules();
2056
2057
		// Likely to need all the groups!
2058
		require_once(SUBSDIR . '/Membergroups.subs.php');
2059
		$context['groups'] = accessibleGroups();
2060
2061
		// Applying all rules?
2062
		if (isset($this->_req->query->apply))
2063
		{
2064
			checkSession('get');
2065
2066
			applyRules(true);
2067
			redirectexit('action=pm;sa=manrules');
2068
		}
2069
2070
		// Editing a specific rule?
2071
		if (isset($this->_req->query->add))
2072
		{
2073
			$rid = $this->_req->getQuery('rid', 'intval', 0);
2074
			$context['rid'] = isset($context['rules'][$rid]) ? $rid : 0;
2075
			$context['sub_template'] = 'add_rule';
2076
2077
			// Any known rule
2078
			$js_rules = [];
2079
			foreach ($context['known_rules'] as $rule)
2080
			{
2081
				$js_rules[$rule] = $txt['pm_rule_' . $rule];
2082
			}
2083
2084
			$js_rules = json_encode($js_rules);
2085
2086
			// Any known label
2087
			$js_labels = [];
2088
			foreach ($context['labels'] as $label)
2089
			{
2090
				if ($label['id'] !== -1)
2091
				{
2092
					$js_labels[$label['id'] + 1] = $label['name'];
2093
				}
2094
			}
2095
2096
			$js_labels = json_encode($js_labels);
2097
2098
			// And all the groups as well
2099
			$js_groups = json_encode($context['groups']);
2100
2101
			theme()->addJavascriptVar([
2102
				'criteriaNum' => 0,
2103
				'actionNum' => 0,
2104
				]
2105
			);
2106
2107
			// Oh my, we have a lot of text strings for this
2108
			theme()->addJavascriptVar(array(
2109
				'groups' => $js_groups,
2110
				'labels' => $js_labels,
2111
				'rules' => $js_rules,
2112
				'txt_pm_readable_and' => $txt['pm_readable_and'],
2113
				'txt_pm_readable_or' => $txt['pm_readable_or'],
2114
				'txt_pm_readable_member' => $txt['pm_readable_member'],
2115
				'txt_pm_readable_group' => $txt['pm_readable_group'],
2116
				'txt_pm_readable_subject ' => $txt['pm_readable_subject'],
2117
				'txt_pm_readable_body' => $txt['pm_readable_body'],
2118
				'txt_pm_readable_buddy' => $txt['pm_readable_buddy'],
2119
				'txt_pm_readable_label' => $txt['pm_readable_label'],
2120
				'txt_pm_readable_delete' => $txt['pm_readable_delete'],
2121
				'txt_pm_readable_start' => $txt['pm_readable_start'],
2122
				'txt_pm_readable_end' => $txt['pm_readable_end'],
2123
				'txt_pm_readable_then' => $txt['pm_readable_then'],
2124
				'txt_pm_rule_not_defined' => $txt['pm_rule_not_defined'],
2125
				'txt_pm_rule_criteria_pick' => $txt['pm_rule_criteria_pick'],
2126
				'txt_pm_rule_sel_group' => $txt['pm_rule_sel_group'],
2127
				'txt_pm_rule_sel_action' => $txt['pm_rule_sel_action'],
2128
				'txt_pm_rule_label' => $txt['pm_rule_label'],
2129
				'txt_pm_rule_delete' => $txt['pm_rule_delete'],
2130
				'txt_pm_rule_sel_label' => $txt['pm_rule_sel_label'],
2131
			), true);
2132
2133
			// Current rule information...
2134
			if ($context['rid'])
2135
			{
2136
				$context['rule'] = $context['rules'][$context['rid']];
2137
				$members = array();
2138
2139
				// Need to get member names!
2140
				foreach ($context['rule']['criteria'] as $k => $criteria)
2141
				{
2142
					if ($criteria['t'] !== 'mid')
2143
					{
2144
						continue;
2145
					}
2146
					if (empty($criteria['v']))
2147
					{
2148
						continue;
2149
					}
2150
					$members[(int) $criteria['v']] = $k;
2151
				}
2152
2153
				if (!empty($members))
2154
				{
2155
					require_once(SUBSDIR . '/Members.subs.php');
2156
					$result = getBasicMemberData(array_keys($members));
2157
					foreach ($result as $row)
2158
					{
2159
						$context['rule']['criteria'][$members[$row['id_member']]]['v'] = $row['member_name'];
2160
					}
2161
				}
2162
			}
2163
			else
2164
			{
2165
				$context['rule'] = array(
2166
					'id' => '',
2167
					'name' => '',
2168
					'criteria' => array(),
2169
					'actions' => array(),
2170
					'logic' => 'and',
2171
				);
2172
			}
2173
2174
			// Add a dummy criteria to allow expansion for none js users.
2175
			$context['rule']['criteria'][] = array('t' => '', 'v' => '');
2176
		}
2177
		// Saving?
2178
		elseif (isset($this->_req->query->save))
2179
		{
2180
			checkSession();
2181
			$rid = $this->_req->getQuery('rid', 'intval', 0);
2182
			$context['rid'] = isset($context['rules'][$rid]) ? $rid : 0;
2183
2184
			// Name is easy!
2185
			$ruleName = Util::htmlspecialchars(trim($this->_req->post->rule_name));
2186
			if (empty($ruleName))
2187
			{
2188
				throw new Exception('pm_rule_no_name', false);
2189
			}
2190
2191
			// Sanity check...
2192
			if (empty($this->_req->post->ruletype) || empty($this->_req->post->acttype))
2193
			{
2194
				throw new Exception('pm_rule_no_criteria', false);
2195
			}
2196
2197
			// Let's do the criteria first - it's also hardest!
2198
			$criteria = array();
2199
			foreach ($this->_req->post->ruletype as $ind => $type)
2200
			{
2201
				// Check everything is here...
2202
				if ($type === 'gid' && (!isset($this->_req->post->ruledefgroup[$ind], $context['groups'][$this->_req->post->ruledefgroup[$ind]])))
2203
				{
2204
					continue;
2205
				}
2206
2207
				if ($type !== 'bud' && !isset($this->_req->post->ruledef[$ind]))
2208
				{
2209
					continue;
2210
				}
2211
2212
				// Members need to be found.
2213
				if ($type === 'mid')
2214
				{
2215
					require_once(SUBSDIR . '/Members.subs.php');
2216
					$name = trim($this->_req->post->ruledef[$ind]);
2217
					$member = getMemberByName($name, true);
2218
					if (empty($member))
2219
					{
2220
						continue;
2221
					}
2222
2223
					$criteria[] = array('t' => 'mid', 'v' => $member['id_member']);
2224
				}
2225
				elseif ($type === 'bud')
2226
				{
2227
					$criteria[] = array('t' => 'bud', 'v' => 1);
2228
				}
2229
				elseif ($type === 'gid')
2230
				{
2231
					$criteria[] = array('t' => 'gid', 'v' => (int) $this->_req->post->ruledefgroup[$ind]);
2232
				}
2233
				elseif (in_array($type, array('sub', 'msg')) && trim($this->_req->post->ruledef[$ind]) !== '')
2234
				{
2235
					$criteria[] = array('t' => $type, 'v' => Util::htmlspecialchars(trim($this->_req->post->ruledef[$ind])));
2236
				}
2237
			}
2238
2239
			// Also do the actions!
2240
			$actions = array();
2241
			$doDelete = 0;
2242
			$isOr = $this->_req->post->rule_logic === 'or' ? 1 : 0;
2243
			foreach ($this->_req->post->acttype as $ind => $type)
2244
			{
2245
				// Picking a valid label?
2246
				if ($type === 'lab' && (!isset($this->_req->post->labdef[$ind], $context['labels'][(int) $this->_req->post->labdef[$ind] - 1])))
2247
				{
2248
					continue;
2249
				}
2250
2251
				// Record what we're doing.
2252
				if ($type === 'del')
2253
				{
2254
					$doDelete = 1;
2255
				}
2256
				elseif ($type === 'lab')
2257
				{
2258
					$actions[] = array('t' => 'lab', 'v' => (int) $this->_req->post->labdef[$ind] - 1);
2259
				}
2260
			}
2261
2262
			if (empty($criteria) || (empty($actions) && !$doDelete))
2263
			{
2264
				throw new Exception('pm_rule_no_criteria', false);
2265
			}
2266
2267
			// What are we storing?
2268
			$criteria = serialize($criteria);
2269
			$actions = serialize($actions);
2270
2271
			// Create the rule?
2272
			if (empty($context['rid']))
2273
			{
2274
				addPMRule($this->user->id, $ruleName, $criteria, $actions, $doDelete, $isOr);
2275
			}
2276
			else
2277
			{
2278
				updatePMRule($this->user->id, $context['rid'], $ruleName, $criteria, $actions, $doDelete, $isOr);
2279
			}
2280
2281
			redirectexit('action=pm;sa=manrules');
2282
		}
2283
		// Deleting?
2284
		elseif (isset($this->_req->post->delselected) && !empty($this->_req->post->delrule))
2285
		{
2286
			checkSession();
2287
			$toDelete = array();
2288
			foreach ($this->_req->post->delrule as $k => $v)
2289
			{
2290
				$toDelete[] = (int) $k;
2291
			}
2292
2293
			if (!empty($toDelete))
2294
			{
2295
				deletePMRules($this->user->id, $toDelete);
2296
			}
2297
2298
			redirectexit('action=pm;sa=manrules');
2299
		}
2300
	}
2301
2302
	/**
2303
	 * Actually do the search of personal messages and show the results
2304
	 *
2305
	 * What it does:
2306
	 *
2307
	 * - accessed with ?action=pm;sa=search2
2308
	 * - checks user input and searches the pm table for messages matching the query.
2309
	 * - uses the search_results sub template of the PersonalMessage template.
2310
	 * - show the results of the search query.
2311
	 */
2312
	public function action_search2()
2313
	{
2314
		global $scripturl, $modSettings, $context, $txt;
2315
2316
		// Make sure the server is able to do this right now
2317
		if (!empty($modSettings['loadavg_search']) && $modSettings['current_load'] >= $modSettings['loadavg_search'])
2318
		{
2319
			throw new Exception('loadavg_search_disabled', false);
2320
		}
2321
2322
		// Some useful general permissions.
2323
		$context['can_send_pm'] = allowedTo('pm_send');
2324
2325
		// Extract all the search parameters if coming in from pagination, etc
2326
		$this->_searchParamsFromString();
2327
2328
		// Set a start for pagination
2329
		$context['start'] = $this->_req->getQuery('start', 'intval', 0);
2330
2331
		// Set/clean search criteria
2332
		$this->_prepareSearchParams();
2333
2334
		$context['folder'] = empty($this->_search_params['sent_only']) ? 'inbox' : 'sent';
2335
2336
		// Searching for specific members
2337
		$userQuery = $this->_setUserQuery();
2338
2339
		// Set up the sorting variables...
2340
		$this->_setSortParams();
2341
2342
		// Sort out any labels we may be searching by.
2343
		$labelQuery = $this->_setLabelQuery();
2344
2345
		// Unfortunately, searching for words like this is going to be slow, so we're blocking them.
2346
		$blocklist_words = array('quote', 'the', 'is', 'it', 'are', 'if', 'in');
2347
2348
		// What are we actually searching for?
2349
		$this->_search_params['search'] = empty($this->_search_params['search']) ? $this->_req->post->search ?? '' : ($this->_search_params['search']);
2350
2351
		// If nothing is left to search on - we set an error!
2352
		if (!isset($this->_search_params['search']) || $this->_search_params['search'] === '')
2353
		{
2354
			$context['search_errors']['invalid_search_string'] = true;
2355
		}
2356
2357
		// Change non-word characters into spaces.
2358
		$stripped_query = preg_replace('~(?:[\x0B\0\x{A0}\t\r\s\n(){}\\[\\]<>!@$%^*.,:+=`\~\?/\\\\]+|&(?:amp|lt|gt|quot);)+~u', ' ', $this->_search_params['search']);
2359
2360
		// Make the query lower case since it will case-insensitive anyway.
2361
		$stripped_query = un_htmlspecialchars(Util::strtolower($stripped_query));
2362
2363
		// Extract phrase parts first (e.g. some words "this is a phrase" some more words.)
2364
		preg_match_all('/(?:^|\s)([-]?)"([^"]+)"(?:$|\s)/', $stripped_query, $matches, PREG_PATTERN_ORDER);
2365
		$phraseArray = $matches[2];
2366
2367
		// Remove the phrase parts and extract the words.
2368
		$wordArray = preg_replace('~(?:^|\s)(?:[-]?)"(?:[^"]+)"(?:$|\s)~u', ' ', $this->_search_params['search']);
2369
		$wordArray = explode(' ', Util::htmlspecialchars(un_htmlspecialchars($wordArray), ENT_QUOTES));
2370
2371
		// A minus sign in front of a word excludes the word.... so...
2372
		$excludedWords = array();
2373
2374
		// Check for things like -"some words", but not "-some words".
2375
		foreach ($matches[1] as $index => $word)
2376
		{
2377
			if ($word === '-')
2378
			{
2379
				if (($word = trim($phraseArray[$index], "-_' ")) !== '' && !in_array($word, $blocklist_words))
2380
				{
2381
					$excludedWords[] = $word;
2382
				}
2383
2384
				unset($phraseArray[$index]);
2385
			}
2386
		}
2387
2388
		// Now we look for -test, etc
2389
		foreach ($wordArray as $index => $word)
2390
		{
2391
			if (strpos(trim($word), '-') === 0)
2392
			{
2393
				if (($word = trim($word, "-_' ")) !== '' && !in_array($word, $blocklist_words))
2394
				{
2395
					$excludedWords[] = $word;
2396
				}
2397
2398
				unset($wordArray[$index]);
2399
			}
2400
		}
2401
2402
		// The remaining words and phrases are all included.
2403
		$searchArray = array_merge($phraseArray, $wordArray);
2404
2405
		// Trim everything and make sure there are no words that are the same.
2406
		foreach ($searchArray as $index => $value)
2407
		{
2408
			// Skip anything that's close to empty.
2409
			if (($searchArray[$index] = trim($value, "-_' ")) === '')
2410
			{
2411
				unset($searchArray[$index]);
2412
			}
2413
			// Skip blocked words. Make sure to note we skipped them as well
2414
			elseif (in_array($searchArray[$index], $blocklist_words))
2415
			{
2416
				$foundBlockListedWords = true;
2417
				unset($searchArray[$index]);
2418
2419
			}
2420
2421
			if (isset($searchArray[$index]))
2422
			{
2423
				$searchArray[$index] = Util::strtolower(trim($value));
2424
2425
				if ($searchArray[$index] === '')
2426
				{
2427
					unset($searchArray[$index]);
2428
				}
2429
				else
2430
				{
2431
					// Sort out entities first.
2432
					$searchArray[$index] = Util::htmlspecialchars($searchArray[$index]);
2433
				}
2434
			}
2435
		}
2436
2437
		$searchArray = array_slice(array_unique($searchArray), 0, 10);
2438
2439
		// This contains *everything*
2440
		$searchWords = array_merge($searchArray, $excludedWords);
2441
2442
		// Make sure at least one word is being searched for.
2443
		if (empty($searchArray))
2444
		{
2445
			$context['search_errors']['invalid_search_string' . (empty($foundBlockListedWords) ? '' : '_blocklist')] = true;
2446
		}
2447
2448
		// Sort out the search query so the user can edit it - if they want.
2449
		$context['search_params'] = $this->_search_params;
2450
		if (isset($context['search_params']['search']))
2451
		{
2452
			$context['search_params']['search'] = Util::htmlspecialchars($context['search_params']['search']);
2453
		}
2454
2455
		if (isset($context['search_params']['userspec']))
2456
		{
2457
			$context['search_params']['userspec'] = Util::htmlspecialchars($context['search_params']['userspec']);
2458
		}
2459
2460
		// Now we have all the parameters, combine them together for pagination and the like...
2461
		$context['params'] = $this->_compileURLparams();
2462
2463
		// Compile the subject query part.
2464
		$andQueryParts = array();
2465
		foreach ($searchWords as $index => $word)
2466
		{
2467
			if ($word === '')
2468
			{
2469
				continue;
2470
			}
2471
2472
			if ($this->_search_params['subject_only'])
2473
			{
2474
				$andQueryParts[] = 'pm.subject' . (in_array($word, $excludedWords) ? ' NOT' : '') . ' LIKE {string:search_' . $index . '}';
2475
			}
2476
			else
2477
			{
2478
				$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 . '})';
2479
			}
2480
2481
			$this->_searchq_parameters ['search_' . $index] = '%' . strtr($word, array('_' => '\\_', '%' => '\\%')) . '%';
2482
		}
2483
2484
		$searchQuery = ' 1=1';
2485
		if (!empty($andQueryParts))
2486
		{
2487
			$searchQuery = implode(!empty($this->_search_params['searchtype']) && $this->_search_params['searchtype'] == 2 ? ' OR ' : ' AND ', $andQueryParts);
2488
		}
2489
2490
		// Age limits?
2491
		$timeQuery = '';
2492
		if (!empty($this->_search_params['minage']))
2493
		{
2494
			$timeQuery .= ' AND pm.msgtime < ' . (time() - $this->_search_params['minage'] * 86400);
2495
		}
2496
2497
		if (!empty($this->_search_params['maxage']))
2498
		{
2499
			$timeQuery .= ' AND pm.msgtime > ' . (time() - $this->_search_params['maxage'] * 86400);
2500
		}
2501
2502
		// If we have errors - return back to the first screen...
2503
		if (!empty($context['search_errors']))
2504
		{
2505
			$this->_req->post->params = $context['params'];
2506
2507
			$this->action_search();
2508
2509
			return false;
2510
		}
2511
2512
		// Get the number of results.
2513
		$numResults = numPMSeachResults($userQuery, $labelQuery, $timeQuery, $searchQuery, $this->_searchq_parameters);
2514
2515
		// Get all the matching message ids, senders and head pm nodes
2516
		[$foundMessages, $posters, $head_pms] = loadPMSearchMessages($userQuery, $labelQuery, $timeQuery, $searchQuery, $this->_searchq_parameters, $this->_search_params);
2517
2518
		// Find the real head pm when in conversation view
2519
		if ($context['display_mode'] === self::DISPLAY_AS_CONVERSATION && !empty($head_pms))
2520
		{
2521
			$real_pm_ids = loadPMSearchHeads($head_pms);
2522
		}
2523
2524
		// Load the found user data
2525
		$posters = array_unique($posters);
2526
		if (!empty($posters))
2527
		{
2528
			MembersList::load($posters);
2529
		}
2530
2531
		// Sort out the page index.
2532
		$context['page_index'] = constructPageIndex('{scripturl}?action=pm;sa=search2;params=' . $context['params'], $this->_req->query->start, $numResults, $modSettings['search_results_per_page'], false);
2533
2534
		$context['message_labels'] = array();
2535
		$context['message_replied'] = array();
2536
		$context['personal_messages'] = array();
2537
		$context['first_label'] = array();
2538
2539
		// If we have results, we have work to do!
2540
		if (!empty($foundMessages))
2541
		{
2542
			$recipients = array();
2543
			[$context['message_labels'], $context['message_replied'], $context['message_unread'], $context['first_label']] = loadPMRecipientInfo($foundMessages, $recipients, $context['folder'], true);
2544
2545
			// Prepare for the callback!
2546
			$search_results = loadPMSearchResults($foundMessages, $this->_search_params);
2547
			$counter = 0;
2548
			$bbc_parser = ParserWrapper::instance();
2549
			foreach ($search_results as $row)
2550
			{
2551
				// If there's no subject, use the default.
2552
				$row['subject'] = $row['subject'] === '' ? $txt['no_subject'] : $row['subject'];
2553
2554
				// Load this poster context info, if not there, then fill in the essentials...
2555
				$member = MembersList::get($row['id_member_from']);
2556
				$member->loadContext(true);
2557
				if ($member->isEmpty())
2558
				{
2559
					$member['name'] = $row['from_name'];
2560
					$member['id'] = 0;
2561
					$member['group'] = $txt['guest_title'];
2562
					$member['link'] = $row['from_name'];
2563
					$member['email'] = '';
2564
					$member['show_email'] = showEmailAddress(0);
2565
					$member['is_guest'] = true;
2566
				}
2567
2568
				// Censor anything we don't want to see...
2569
				$row['body'] = censor($row['body']);
2570
				$row['subject'] = censor($row['subject']);
2571
2572
				// Parse out any BBC...
2573
				$row['body'] = $bbc_parser->parsePM($row['body']);
2574
2575
				// Highlight the hits
2576
				$body_highlighted = '';
2577
				$subject_highlighted = '';
2578
				foreach ($searchArray as $query)
2579
				{
2580
					// Fix the international characters in the keyword too.
2581
					$query = un_htmlspecialchars($query);
2582
					$query = trim($query, '\*+');
2583
					$query = strtr(Util::htmlspecialchars($query), array('\\\'' => "'"));
2584
2585
					$body_highlighted = preg_replace_callback('/((<[^>]*)|' . preg_quote(strtr($query, array("'" => '&#039;')), '/') . ')/iu',
2586
						fn($matches) => $this->_highlighted_callback($matches), $row['body']);
2587
					$subject_highlighted = preg_replace('/(' . preg_quote($query, '/') . ')/iu', '<strong class="highlight">$1</strong>', $row['subject']);
2588
				}
2589
2590
				// Set a link using the first label information
2591
				$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'] === self::DISPLAY_AS_CONVERSATION && 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'];
2592
2593
				$context['personal_messages'][] = [
2594
					'id' => $row['id_pm'],
2595
					'member' => $member,
2596
					'subject' => $subject_highlighted,
2597
					'body' => $body_highlighted,
2598
					'time' => standardTime($row['msgtime']),
2599
					'html_time' => htmlTime($row['msgtime']),
2600
					'timestamp' => forum_time(true, $row['msgtime']),
2601
					'recipients' => &$recipients[$row['id_pm']],
2602
					'labels' => &$context['message_labels'][$row['id_pm']],
2603
					'fully_labeled' => (empty($context['message_labels'][$row['id_pm']]) ? 0 : count($context['message_labels'][$row['id_pm']])) === count($context['labels']),
2604
					'is_replied_to' => &$context['message_replied'][$row['id_pm']],
2605
					'href' => $href,
2606
					'link' => '<a href="' . $href . '">' . $subject_highlighted . '</a>',
2607
					'counter' => ++$counter,
2608
					'pmbuttons' => $this->_setSearchPmButtons($row['id_pm'], $member),
2609
				];
2610
			}
2611
		}
2612
2613
		// Finish off the context.
2614
		$context['page_title'] = $txt['pm_search_title'];
2615
		$context['sub_template'] = 'search_results';
2616
		$context['menu_data_' . $context['pm_menu_id']]['current_area'] = 'search';
2617
		$context['breadcrumbs'][] = [
2618
			'url' => getUrl('action', ['action' => 'pm', 'sa' => 'search']),
2619
			'name' => $txt['pm_search_bar_title'],
2620
		];
2621
	}
2622
2623
	/**
2624
	 * Return buttons for search results, used when viewing full message as result
2625
	 *
2626
	 * @param int $id of the PM
2627
	 * @param ValuesContainer $member member information
2628
	 * @return array[]
2629
	 */
2630
	private function _setSearchPmButtons($id, $member)
2631
	{
2632
		global $context;
2633
2634
		$pmButtons = [
2635
			// Reply, Quote
2636
			'reply_button' => [
2637
				'text' => 'reply',
2638
				'url' => getUrl('action', ['action' => 'pm', 'sa' => 'send', 'f' => $context['folder'], 'pmsg' => $id, 'u' => $member['id']]) . ($context['current_label_id'] !== "-1" ? ';l=' . $context['current_label_id'] : ''),
2639
				'class' => 'reply_button',
2640
				'icon' => 'modify',
2641
				'enabled' => !$member['is_guest'] && $context['can_send_pm'],
2642
			],
2643
			'quote_button' => [
2644
				'text' => 'quote',
2645
				'url' => getUrl('action', ['action' => 'pm', 'sa' => 'send', 'f' => $context['folder'], 'pmsg' => $id, 'quote' => '']) . ($context['current_label_id'] !== "-1" ? ';l=' . $context['current_label_id'] : '') . ($context['folder'] === 'sent' ? '' : ';u=' . $member['id']),
2646
				'class' => 'quote_button',
2647
				'icon' => 'quote',
2648
				'enabled' => !$member['is_guest'] && $context['can_send_pm'],
2649
			],
2650
			// This is for "forwarding" - even if the member is gone.
2651
			'reply_quote_button' => [
2652
				'text' => 'reply_quote',
2653
				'url' => getUrl('action', ['action' => 'pm', 'sa' => 'send', 'f' => $context['folder'], 'pmsg' => $id, 'quote' => '']) . ($context['current_label_id'] !== "-1" ? ';l=' . $context['current_label_id'] : ''),
2654
				'class' => 'reply_button',
2655
				'icon' => 'modify',
2656
				'enabled' => $member['is_guest'] && $context['can_send_pm'],
2657
			]
2658
		];
2659
2660
		// Drop any non-enabled ones
2661
		return array_filter($pmButtons, static fn($button) => !isset($button['enabled']) || (bool) $button['enabled']);
2662
	}
2663
2664
	/**
2665
	 * Extract search params from a string
2666
	 *
2667
	 * What it does:
2668
	 *
2669
	 * - When paging search results, reads and decodes the passed parameters
2670
	 * - Places what it finds back in search_params
2671
	 */
2672
	private function _searchParamsFromString()
2673
	{
2674
		$this->_search_params = array();
2675
2676
		if (isset($this->_req->query->params) || isset($this->_req->post->params))
2677
		{
2678
			// Feed it
2679
			$temp_params = $this->_req->query->params ?? $this->_req->post->params;
2680
2681
			// Decode and replace the uri safe characters we added
2682
			$temp_params = base64_decode(str_replace(array('-', '_', '.'), array('+', '/', '='), $temp_params));
2683
2684
			$temp_params = explode('|"|', $temp_params);
2685
			foreach ($temp_params as $data)
2686
			{
2687
				[$k, $v] = array_pad(explode("|'|", $data), 2, '');
2688
				$this->_search_params[$k] = $v;
2689
			}
2690
		}
2691
2692
		return $this->_search_params;
2693
	}
2694
2695
	/**
2696
	 * Sets the search params for the query
2697
	 *
2698
	 * What it does:
2699
	 *
2700
	 * - Uses existing ones if coming from pagination or uses those passed from the search pm form
2701
	 * - Validates passed params are valid
2702
	 */
2703
	private function _prepareSearchParams()
2704
	{
2705
		// Store whether simple search was used (needed if the user wants to do another query).
2706
		if (!isset($this->_search_params['advanced']))
2707
		{
2708
			$this->_search_params['advanced'] = empty($this->_req->post->advanced) ? 0 : 1;
2709
		}
2710
2711
		// 1 => 'allwords' (default, don't set as param),  2 => 'anywords'.
2712
		if (!empty($this->_search_params['searchtype']) || (!empty($this->_req->post->searchtype) && $this->_req->post->searchtype == 2))
2713
		{
2714
			$this->_search_params['searchtype'] = 2;
2715
		}
2716
2717
		// Minimum age of messages. Default to zero (don't set param in that case).
2718
		if (!empty($this->_search_params['minage']) || (!empty($this->_req->post->minage) && $this->_req->post->minage > 0))
2719
		{
2720
			$this->_search_params['minage'] = empty($this->_search_params['minage']) ? (int) $this->_req->post->minage : (int) $this->_search_params['minage'];
2721
		}
2722
2723
		// Maximum age of messages. Default to infinite (9999 days: param not set).
2724
		if (!empty($this->_search_params['maxage']) || (!empty($this->_req->post->maxage) && $this->_req->post->maxage < 9999))
2725
		{
2726
			$this->_search_params['maxage'] = empty($this->_search_params['maxage']) ? (int) $this->_req->post->maxage : (int) $this->_search_params['maxage'];
2727
		}
2728
2729
		// Default the username to a wildcard matching every user (*).
2730
		if (!empty($this->_search_params['userspec']) || (!empty($this->_req->post->userspec) && $this->_req->post->userspec !== '*'))
2731
		{
2732
			$this->_search_params['userspec'] = $this->_search_params['userspec'] ?? $this->_req->post->userspec;
2733
		}
2734
2735
		// Search modifiers
2736
		$this->_search_params['subject_only'] = !empty($this->_search_params['subject_only']) || !empty($this->_req->post->subject_only);
2737
		$this->_search_params['show_complete'] = !empty($this->_search_params['show_complete']) || !empty($this->_req->post->show_complete);
2738
		$this->_search_params['sent_only'] = !empty($this->_search_params['sent_only']) || !empty($this->_req->post->sent_only);
2739
	}
2740
2741
	/**
2742
	 * Handles the parameters when searching on specific users
2743
	 *
2744
	 * What it does:
2745
	 *
2746
	 * - Returns the user query for use in the main search query
2747
	 * - Sets the parameters for use in the query
2748
	 *
2749
	 * @return string
2750
	 */
2751
	private function _setUserQuery()
2752
	{
2753
		global $context;
2754
2755
		// Hardcoded variables that can be tweaked if required.
2756
		$maxMembersToSearch = 500;
2757
2758
		// Init to not be searching based on members
2759
		$userQuery = '';
2760
2761
		// If there's no specific user, then don't mention it in the main query.
2762
		if (!empty($this->_search_params['userspec']))
2763
		{
2764
			// Set up, so we can search by username, wildcards, like, etc
2765
			$userString = strtr(Util::htmlspecialchars($this->_search_params['userspec'], ENT_QUOTES), array('&quot;' => '"'));
2766
			$userString = strtr($userString, array('%' => '\%', '_' => '\_', '*' => '%', '?' => '_'));
2767
2768
			preg_match_all('~"([^"]+)"~', $userString, $matches);
2769
			$possible_users = array_merge($matches[1], explode(',', preg_replace('~"[^"]+"~', '', $userString)));
2770
2771
			// Who matches those criteria?
2772
			require_once(SUBSDIR . '/Members.subs.php');
2773
			$members = membersBy('member_names', array('member_names' => $possible_users));
2774
2775
			foreach ($possible_users as $key => $possible_user)
2776
			{
2777
				$this->_searchq_parameters['guest_user_name_implode_' . $key] = '{string_case_insensitive:' . $possible_user . '}';
2778
			}
2779
2780
			// Simply do nothing if there are too many members matching the criteria.
2781
			if (count($members) > $maxMembersToSearch)
2782
			{
2783
				$userQuery = '';
2784
			}
2785
			elseif (count($members) === 0)
2786
			{
2787
				if ($context['folder'] === 'inbox')
2788
				{
2789
					$uq = array();
2790
					$name = '{column_case_insensitive:pm.from_name}';
2791
					foreach (array_keys($possible_users) as $key)
2792
					{
2793
						$uq[] = 'AND pm.id_member_from = 0 AND (' . $name . ' LIKE {string:guest_user_name_implode_' . $key . '})';
2794
					}
2795
2796
					$userQuery = implode(' ', $uq);
2797
					$this->_searchq_parameters['pm_from_name'] = $name;
2798
				}
2799
				else
2800
				{
2801
					$userQuery = '';
2802
				}
2803
			}
2804
			else
2805
			{
2806
				$memberlist = array();
2807
				foreach ($members as $id)
2808
				{
2809
					$memberlist[] = $id;
2810
				}
2811
2812
				// Use the name as sent from or sent to
2813
				if ($context['folder'] === 'inbox')
2814
				{
2815
					$uq = array();
2816
					$name = '{column_case_insensitive:pm.from_name}';
2817
2818
					foreach (array_keys($possible_users) as $key)
2819
					{
2820
						$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 . '})))';
2821
					}
2822
2823
					$userQuery = implode(' ', $uq);
2824
				}
2825
				else
2826
				{
2827
					$userQuery = 'AND (pmr.id_member IN ({array_int:member_list}))';
2828
				}
2829
2830
				$this->_searchq_parameters['pm_from_name'] = '{column_case_insensitive:pm.from_name}';
2831
				$this->_searchq_parameters['member_list'] = $memberlist;
2832
			}
2833
		}
2834
2835
		return $userQuery;
2836
	}
2837
2838
	/**
2839
	 * Read / Set the sort parameters for the results listing
2840
	 */
2841
	private function _setSortParams()
2842
	{
2843
		$sort_columns = array(
2844
			'pm.id_pm',
2845
		);
2846
2847
		if (empty($this->_search_params['sort']) && !empty($this->_req->post->sort))
2848
		{
2849
			[$this->_search_params['sort'], $this->_search_params['sort_dir']] = array_pad(explode('|', $this->_req->post->sort), 2, '');
2850
		}
2851
2852
		$this->_search_params['sort'] = !empty($this->_search_params['sort']) && in_array($this->_search_params['sort'], $sort_columns) ? $this->_search_params['sort'] : 'pm.id_pm';
2853
		$this->_search_params['sort_dir'] = !empty($this->_search_params['sort_dir']) && $this->_search_params['sort_dir'] === 'asc' ? 'asc' : 'desc';
2854
	}
2855
2856
	/**
2857
	 * Handles the parameters when searching on specific labels
2858
	 *
2859
	 * What it does:
2860
	 *
2861
	 * - Returns the label query for use in the main search query
2862
	 * - Sets the parameters for use in the query
2863
	 *
2864
	 * @return string
2865
	 * @throws \Exception
2866
	 */
2867
	private function _setLabelQuery()
2868
	{
2869
		global $context;
2870
2871
		$db = database();
2872
2873
		$labelQuery = '';
2874
2875
		if ($context['folder'] === 'inbox' && !empty($this->_search_params['advanced']) && $context['currently_using_labels'])
2876
		{
2877
			// Came here from pagination?  Put them back into $_REQUEST for sanitation.
2878
			if (isset($this->_search_params['labels']))
2879
			{
2880
				$this->_req->post->searchlabel = explode(',', $this->_search_params['labels']);
2881
			}
2882
2883
			// Assuming we have some labels - make them all integers.
2884
			if (!empty($this->_req->post->searchlabel) && is_array($this->_req->post->searchlabel))
2885
			{
2886
				$this->_req->post->searchlabel = array_map('intval', $this->_req->post->searchlabel);
2887
			}
2888
			else
2889
			{
2890
				$this->_req->post->searchlabel = array();
2891
			}
2892
2893
			// Now that everything is cleaned up a bit, make the labels a param.
2894
			$this->_search_params['labels'] = implode(',', $this->_req->post->searchlabel);
2895
2896
			// No labels selected? That must be an error!
2897
			if (empty($this->_req->post->searchlabel))
2898
			{
2899
				$context['search_errors']['no_labels_selected'] = true;
2900
			}
2901
			// Otherwise prepare the query!
2902
			elseif (count($this->_req->post->searchlabel) !== count($context['labels']))
2903
			{
2904
				$labelQuery = '
2905
				AND {raw:label_implode}';
2906
2907
				$labelStatements = array();
2908
				foreach ($this->_req->post->searchlabel as $label)
2909
				{
2910
					$labelStatements[] = $db->quote('FIND_IN_SET({string:label}, pmr.labels) != 0', array('label' => $label,));
2911
				}
2912
2913
				$this->_searchq_parameters ['label_implode'] = '(' . implode(' OR ', $labelStatements) . ')';
2914
			}
2915
		}
2916
2917
		return $labelQuery;
2918
	}
2919
2920
	/**
2921
	 * Encodes search params in a URL-compatible way
2922
	 *
2923
	 * @return string - the encoded string to be appended to the URL
2924
	 */
2925
	private function _compileURLparams()
2926
	{
2927
		$encoded = array();
2928
2929
		// Now we have all the parameters, combine them together for pagination and the like...
2930
		foreach ($this->_search_params as $k => $v)
2931
		{
2932
			$encoded[] = $k . "|'|" . $v;
2933
		}
2934
2935
		// Base64 encode, then replace +/= with uri safe ones that can be reverted
2936
		return str_replace(array('+', '/', '='), array('-', '_', '.'), base64_encode(implode('|"|', $encoded)));
2937
	}
2938
2939
	/**
2940
	 * Allows searching personal messages.
2941
	 *
2942
	 * What it does:
2943
	 *
2944
	 * - accessed with ?action=pm;sa=search
2945
	 * - shows the screen to search PMs (?action=pm;sa=search)
2946
	 * - uses the search sub template of the PersonalMessage template.
2947
	 * - decodes and loads search parameters given in the URL (if any).
2948
	 * - the form redirects to index.php?action=pm;sa=search2.
2949
	 *
2950
	 * @uses search sub template
2951
	 */
2952
	public function action_search()
2953
	{
2954
		global $context, $txt;
2955
2956
		// If they provided some search parameters, we need to extract them
2957
		if (isset($this->_req->post->params))
2958
		{
2959
			$context['search_params'] = $this->_searchParamsFromString();
2960
		}
2961
2962
		// Set up the search criteria, type, what, age, etc
2963
		if (isset($this->_req->post->search))
2964
		{
2965
			$context['search_params']['search'] = un_htmlspecialchars($this->_req->post->search);
2966
			$context['search_params']['search'] = htmlspecialchars($context['search_params']['search'], ENT_COMPAT);
2967
		}
2968
2969
		if (isset($context['search_params']['userspec']))
2970
		{
2971
			$context['search_params']['userspec'] = htmlspecialchars($context['search_params']['userspec'], ENT_COMPAT);
2972
		}
2973
2974
		// 1 => 'allwords' / 2 => 'anywords'.
2975
		if (!empty($context['search_params']['searchtype']))
2976
		{
2977
			$context['search_params']['searchtype'] = 2;
2978
		}
2979
2980
		// Minimum and Maximum age of the message
2981
		if (!empty($context['search_params']['minage']))
2982
		{
2983
			$context['search_params']['minage'] = (int) $context['search_params']['minage'];
2984
		}
2985
2986
		if (!empty($context['search_params']['maxage']))
2987
		{
2988
			$context['search_params']['maxage'] = (int) $context['search_params']['maxage'];
2989
		}
2990
2991
		$context['search_params']['show_complete'] = !empty($context['search_params']['show_complete']);
2992
		$context['search_params']['subject_only'] = !empty($context['search_params']['subject_only']);
2993
2994
		// Create the array of labels to be searched.
2995
		$context['search_labels'] = array();
2996
		$searchedLabels = isset($context['search_params']['labels']) && $context['search_params']['labels'] != '' ? explode(',', $context['search_params']['labels']) : array();
2997
		foreach ($context['labels'] as $label)
2998
		{
2999
			$context['search_labels'][] = array(
3000
				'id' => $label['id'],
3001
				'name' => $label['name'],
3002
				'checked' => empty($searchedLabels) || in_array($label['id'], $searchedLabels),
3003
			);
3004
		}
3005
3006
		// Are all the labels checked?
3007
		$context['check_all'] = empty($searchedLabels) || count($context['search_labels']) === count($searchedLabels);
3008
3009
		// Load the error text strings if there were errors in the search.
3010
		if (!empty($context['search_errors']))
3011
		{
3012
			Txt::load('Errors');
3013
			$context['search_errors']['messages'] = array();
3014
			foreach ($context['search_errors'] as $search_error => $dummy)
3015
			{
3016
				if ($search_error === 'messages')
3017
				{
3018
					continue;
3019
				}
3020
3021
				$context['search_errors']['messages'][] = $txt['error_' . $search_error];
3022
			}
3023
		}
3024
3025
		$context['page_title'] = $txt['pm_search_title'];
3026
		$context['sub_template'] = 'search';
3027
		$context['breadcrumbs'][] = [
3028
			'url' => getUrl('action', ['action' => 'pm', 'sa' => 'search']),
3029
			'name' => $txt['pm_search_bar_title'],
3030
		];
3031
	}
3032
3033
	/**
3034
	 * Used to highlight body text with strings that match the search term
3035
	 *
3036
	 * - Callback function used in $body_highlighted
3037
	 *
3038
	 * @param string[] $matches
3039
	 *
3040
	 * @return string
3041
	 */
3042
	private function _highlighted_callback($matches)
3043
	{
3044
		return isset($matches[2]) && $matches[2] === $matches[1] ? stripslashes($matches[1]) : '<strong class="highlight">' . $matches[1] . '</strong>';
3045
	}
3046
3047
	/**
3048
	 * Allows the user to mark a personal message as unread, so they remember to come back to it
3049
	 */
3050
	public function action_markunread()
3051
	{
3052
		global $context;
3053
3054
		checkSession('request');
3055
3056
		$pmsg = $this->_req->getQuery('pmsg', 'intval', null);
3057
3058
		// Marking a message as unread, we need a message that was sent to them
3059
		// Can't mark your own reply as unread, that would be weird
3060
		if (!is_null($pmsg) && checkPMReceived($pmsg))
3061
		{
3062
			// Make sure this is accessible, should be of course
3063
			if (!isAccessiblePM($pmsg, 'inbox'))
3064
			{
3065
				throw new Exception('no_access', false);
3066
			}
3067
3068
			// Well then, you get to hear about it all over again
3069
			markMessagesUnread($pmsg);
3070
		}
3071
3072
		// Back to the folder.
3073
		redirectexit($context['current_label_redirect']);
3074
	}
3075
}
3076