Passed
Pull Request — development (#3834)
by Spuds
07:36
created

PersonalMessage::pre_dispatch()   F

Complexity

Conditions 13
Paths 1536

Size

Total Lines 78
Code Lines 33

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 13
eloc 33
nc 1536
nop 0
dl 0
loc 78
rs 2.45
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
3
/**
4
 * This file is mainly meant for controlling the actions related to personal
5
 * messages. It allows viewing, sending, deleting, and marking.
6
 * For compatibility reasons, they are often called "instant messages".
7
 *
8
 * @package   ElkArte Forum
9
 * @copyright ElkArte Forum contributors
10
 * @license   BSD http://opensource.org/licenses/BSD-3-Clause (see accompanying LICENSE.txt file)
11
 *
12
 * This file contains code covered by:
13
 * copyright: 2011 Simple Machines (http://www.simplemachines.org)
14
 *
15
 * @version 2.0 Beta 1
16
 *
17
 */
18
19
namespace ElkArte\PersonalMessage;
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\Exceptions\ControllerRedirectException;
27
use ElkArte\Exceptions\Exception;
28
use ElkArte\Exceptions\PmErrorException;
29
use ElkArte\Helper\Util;
30
use ElkArte\Helper\ValuesContainer;
31
use ElkArte\Languages\Txt;
32
use ElkArte\MembersList;
33
use ElkArte\MessagesCallback\BodyParser\Normal;
34
use ElkArte\MessagesCallback\PmRenderer;
35
use ElkArte\Profile\Profile;
36
use ElkArte\Profile\ProfileOptions;
37
use ElkArte\User;
38
use ElkArte\VerificationControls\VerificationControlsIntegrate;
39
40
/**
41
 * Class PersonalMessage
42
 *
43
 * It allows viewing, sending, deleting, and marking personal messages
44
 *
45
 * @package ElkArte\PersonalMessage
46
 */
47
class PersonalMessage extends AbstractController
48
{
49
	/**
50
	 * This method is executed before any other in this file (when the class is
51
	 * loaded by the dispatcher).
52
	 *
53
	 * What it does:
54
	 *
55
	 * - It sets the context, loads templates and language file(s), as necessary
56
	 * for the function that will be called.
57
	 */
58
	public function pre_dispatch()
59
	{
60
		global $txt, $context, $modSettings;
61
62
		// No guests!
63
		is_not_guest();
64
65
		// You're not supposed to be here at all if you can't even read PMs.
66
		isAllowedTo('pm_read');
67
68
		// This file contains PM functions such as mark, send, delete
69
		require_once(SUBSDIR . '/PersonalMessage.subs.php');
70
71
		// Templates, language, javascript
72
		Txt::load('PersonalMessage');
73
		loadJavascriptFile(['suggest.js', 'PersonalMessage.js']);
74
75
		if ($this->getApi() === false)
76
		{
77
			theme()->getTemplates()->load('PersonalMessage');
78
		}
79
80
		$this->_events->trigger('pre_dispatch', ['xml' => $this->getApi() !== false]);
81
82
		// Load up the members' maximum message capacity.
83
		$this->_loadMessageLimit();
84
85
		// A previous message was sent successfully? show a small indication.
86
		if ($this->_req->getQuery('done') === 'sent')
87
		{
88
			$context['pm_sent'] = true;
89
		}
90
91
		// Load the label counts data.
92
		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...
93
		{
94
			$this->_loadLabels();
95
96
			// Get the message count for each label
97
			$context['labels'] = loadPMLabels($context['labels']);
98
		}
99
100
		// Now we have the labels, and assuming we have unsorted mail, apply our rules!
101
		if (User::$settings['new_pm'])
102
		{
103
			// Apply our rules to the new PM's
104
			applyRules();
105
106
			require_once(SUBSDIR . '/Members.subs.php');
107
			updateMemberData($this->user->id, ['new_pm' => 0]);
108
109
			// Turn the new PM's status off, for the popup alert, since they have entered the PM area
110
			toggleNewPM($this->user->id);
111
		}
112
113
		// This determines if we have more labels than just the standard inbox.
114
		$context['currently_using_labels'] = count($context['labels']) > 1 ? 1 : 0;
115
116
		// Some stuff for the labels...
117
		$label = $this->_req->getQuery('l', 'intval');
118
		$folder = $this->_req->getQuery('f', 'trim', '');
119
		$start = $this->_req->getQuery('start', 'trim');
120
		$context['current_label_id'] = isset($label, $context['labels'][$label]) ? (int) $label : -1;
121
		$context['current_label'] = &$context['labels'][$context['current_label_id']]['name'];
122
		$context['folder'] = $folder !== 'sent' ? 'inbox' : 'sent';
123
124
		// This is convenient.  Do you know how annoying it is to do this every time?!
125
		$context['current_label_redirect'] = 'action=pm;f=' . $context['folder'] . (isset($start) ? ';start=' . $start : '') . (empty($label) ? '' : ';l=' . $label);
126
		$context['can_issue_warning'] = featureEnabled('w') && allowedTo('issue_warning') && !empty($modSettings['warning_enable']);
127
128
		// Build the breadcrumbs for all the actions...
129
		$context['breadcrumbs'][] = [
130
			'url' => getUrl('action', ['action' => 'pm']),
131
			'name' => $txt['personal_messages']
132
		];
133
134
		// Preferences...
135
		$context['display_mode'] = PmHelper::getDisplayMode();
136
	}
137
138
	/**
139
	 * Load a members message limit and prepares the limit bar
140
	 */
141
	private function _loadMessageLimit(): void
142
	{
143
		global $context, $txt;
144
145
		$context['message_limit'] = loadMessageLimit();
146
147
		// Prepare the context for the capacity bar.
148
		if (!empty($context['message_limit']))
149
		{
150
			$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...
151
152
			$context['limit_bar'] = [
153
				'messages' => $this->user->messages,
154
				'allowed' => $context['message_limit'],
155
				'percent' => $bar,
156
				'bar' => min(100, (int) $bar),
157
				'text' => sprintf($txt['pm_currently_using'], $this->user->messages, round($bar, 1)),
158
			];
159
		}
160
	}
161
162
	/**
163
	 * Loads the user defined label's for use in the template etc.
164
	 */
165
	private function _loadLabels(): void
166
	{
167
		global $context, $txt;
168
169
		$userLabels = explode(',', User::$settings['message_labels'] ?? '');
170
171
		foreach ($userLabels as $id_label => $label_name)
172
		{
173
			if (empty($label_name))
174
			{
175
				continue;
176
			}
177
178
			$context['labels'][$id_label] = [
179
				'id' => $id_label,
180
				'name' => trim($label_name),
181
				'messages' => 0,
182
				'unread_messages' => 0,
183
			];
184
		}
185
186
		// The default inbox is always available
187
		$context['labels'][-1] = [
188
			'id' => -1,
189
			'name' => $txt['pm_msg_label_inbox'],
190
			'messages' => 0,
191
			'unread_messages' => 0,
192
		];
193
	}
194
195
	/**
196
	 * This is the main function of personal messages, called before the action handler.
197
	 *
198
	 * What it does:
199
	 *
200
	 * - PersonalMessages is a menu-based controller.
201
	 * - It sets up the menu.
202
	 * - Calls from the menu the appropriate method/function for the current area.
203
	 *
204
	 * @see AbstractController::action_index
205
	 */
206
	public function action_index()
207
	{
208
		global $context;
209
210
		// Finally, all the things we know how to do
211
		$subActions = [
212
			'manlabels' => [$this, 'action_manlabels', 'permission' => 'pm_read'],
213
			'manrules' => [$this, 'action_manrules', 'permission' => 'pm_read'],
214
			'markunread' => [$this, 'action_markunread', 'permission' => 'pm_read'],
215
			'pmactions' => [$this, 'action_pmactions', 'permission' => 'pm_read'],
216
			'prune' => [$this, 'action_prune', 'permission' => 'pm_read'],
217
			'removeall' => [$this, 'action_removeall', 'permission' => 'pm_read'],
218
			'removeall2' => [$this, 'action_removeall2', 'permission' => 'pm_read'],
219
			'report' => [$this, 'action_report', 'permission' => 'pm_read'],
220
			'search' => [$this, 'action_search', 'permission' => 'pm_read'],
221
			'search2' => [$this, 'action_search2', 'permission' => 'pm_read'],
222
			'send' => [$this, 'action_send', 'permission' => 'pm_read'],
223
			'send2' => [$this, 'action_send2', 'permission' => 'pm_read'],
224
			'settings' => [$this, 'action_settings', 'permission' => 'pm_read'],
225
			'inbox' => [$this, 'action_folder', 'permission' => 'pm_read'],
226
		];
227
228
		// Set up our action array
229
		$action = new Action('pm_index');
230
231
		// Known action, go to it, otherwise the inbox for you
232
		$subAction = $action->initialize($subActions, 'inbox');
233
234
		// Set the right index bar for the action
235
		if ($subAction === 'inbox')
236
		{
237
			$this->_messageIndexBar($context['current_label_id'] === -1 ? $context['folder'] : 'label' . $context['current_label_id']);
238
		}
239
		elseif ($this->getApi() === false)
240
		{
241
			$this->_messageIndexBar($subAction);
242
		}
243
244
		// And off we go!
245
		$action->dispatch($subAction);
246
	}
247
248
	/**
249
	 * A menu to easily access different areas of the PM section
250
	 *
251
	 * @param string $area
252
	 */
253
	private function _messageIndexBar($area): void
254
	{
255
		global $txt, $context;
256
257
		require_once(SUBSDIR . '/Menu.subs.php');
258
259
		$pm_areas = [
260
			'folders' => [
261
				'title' => $txt['pm_messages'],
262
				'counter' => 'unread_messages',
263
				'areas' => [
264
					'inbox' => [
265
						'label' => $txt['inbox'],
266
						'custom_url' => getUrl('action', ['action' => 'pm']),
267
						'counter' => 'unread_messages',
268
					],
269
					'send' => [
270
						'label' => $txt['new_message'],
271
						'custom_url' => getUrl('action', ['action' => 'pm', 'sa' => 'send']),
272
						'permission' => 'pm_send',
273
					],
274
					'sent' => [
275
						'label' => $txt['sent_items'],
276
						'custom_url' => getUrl('action', ['action' => 'pm', 'f' => 'sent']),
277
					],
278
				],
279
			],
280
			'labels' => [
281
				'title' => $txt['pm_labels'],
282
				'counter' => 'labels_unread_total',
283
				'areas' => [],
284
			],
285
			'actions' => [
286
				'title' => $txt['pm_actions'],
287
				'areas' => [
288
					'search' => [
289
						'label' => $txt['pm_search_bar_title'],
290
						'custom_url' => getUrl('action', ['action' => 'pm', 'sa' => 'search']),
291
					],
292
					'prune' => [
293
						'label' => $txt['pm_prune'],
294
						'custom_url' => getUrl('action', ['action' => 'pm', 'sa' => 'prune']),
295
					],
296
				],
297
			],
298
			'pref' => [
299
				'title' => $txt['pm_preferences'],
300
				'areas' => [
301
					'manlabels' => [
302
						'label' => $txt['pm_manage_labels'],
303
						'custom_url' => getUrl('action', ['action' => 'pm', 'sa' => 'manlabels']),
304
					],
305
					'manrules' => [
306
						'label' => $txt['pm_manage_rules'],
307
						'custom_url' => getUrl('action', ['action' => 'pm', 'sa' => 'manrules']),
308
					],
309
					'settings' => [
310
						'label' => $txt['pm_settings'],
311
						'custom_url' => getUrl('action', ['action' => 'pm', 'sa' => 'settings']),
312
					],
313
				],
314
			],
315
		];
316
317
		// Handle labels.
318
		$label_counters = ['unread_messages' => $context['labels'][-1]['unread_messages']];
319
		if (empty($context['currently_using_labels']))
320
		{
321
			unset($pm_areas['labels']);
322
		}
323
		else
324
		{
325
			// Note we send labels by id as it will have fewer problems in the query string.
326
			$label_counters['labels_unread_total'] = 0;
327
			foreach ($context['labels'] as $label)
328
			{
329
				if ($label['id'] === -1)
330
				{
331
					continue;
332
				}
333
334
				// Count the number of unread items in labels.
335
				$label_counters['labels_unread_total'] += $label['unread_messages'];
336
337
				// Add the label to the menu.
338
				$pm_areas['labels']['areas']['label' . $label['id']] = [
339
					'label' => $label['name'],
340
					'custom_url' => getUrl('action', ['action' => 'pm', 'l' => $label['id']]),
341
					'counter' => 'label' . $label['id'],
342
					'messages' => $label['messages'],
343
				];
344
345
				$label_counters['label' . $label['id']] = $label['unread_messages'];
346
			}
347
		}
348
349
		// Do we have a limit on the number of messages we can keep?
350
		if (!empty($context['message_limit']))
351
		{
352
			$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...
353
354
			$context['limit_bar'] = [
355
				'messages' => $this->user->messages,
356
				'allowed' => $context['message_limit'],
357
				'percent' => $bar,
358
				'bar' => $bar > 100 ? 100 : (int) $bar,
359
				'text' => sprintf($txt['pm_currently_using'], $this->user->messages, $bar)
360
			];
361
		}
362
363
		// Set a few options for the menu.
364
		$menuOptions = [
365
			'current_area' => $area,
366
			'hook' => 'pm',
367
			'disable_url_session_check' => true,
368
			'counters' => empty($label_counters) ? 0 : $label_counters,
369
		];
370
371
		// Actually create the menu!
372
		$pm_include_data = createMenu($pm_areas, $menuOptions);
373
		unset($pm_areas);
374
375
		// Make a note of the Unique ID for this menu.
376
		$context['pm_menu_id'] = $context['max_menu_id'];
377
		$context['pm_menu_name'] = 'menu_data_' . $context['pm_menu_id'];
378
379
		// Set the selected item.
380
		$context['menu_item_selected'] = $pm_include_data['current_area'];
381
382
		// Set the template for this area and add the profile layer.
383
		if ($this->getApi() === false)
384
		{
385
			$template_layers = theme()->getLayers();
386
			$template_layers->add('pm');
387
		}
388
	}
389
390
	/**
391
	 * Display a folder, i.e., inbox/sent etc.
392
	 *
393
	 * Display mode: 0 = all at once, 1 = one at a time, 2 = as a conversation
394
	 *
395
	 * @throws Exception
396
	 * @uses subject_list, pm template layers
397
	 * @uses folder sub template
398
	 */
399
	public function action_folder(): void
400
	{
401
		global $txt, $scripturl, $modSettings, $context, $subjects_request, $messages_request, $options;
402
403
		// Changing view?
404
		if ($this->_req->isSet('view'))
405
		{
406
			$context['display_mode'] = (int) $context['display_mode'] > 1 ? 0 : (int) $context['display_mode'] + 1;
407
			require_once(SUBSDIR . '/Members.subs.php');
408
			updateMemberData($this->user->id, ['pm_prefs' => (((int) 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...
409
		}
410
411
		// Make sure the starting location is valid.
412
		$start_raw = $this->_req->getQuery('start', 'trim');
413
		if ($start_raw !== null && $start_raw !== 'new')
414
		{
415
			$start = (int) $start_raw;
416
		}
417
		elseif ($start_raw === null && !empty($options['view_newest_pm_first']))
418
		{
419
			$start = 0;
420
		}
421
		else
422
		{
423
			$start = 'new';
424
		}
425
426
		// Set up some basic template stuff.
427
		$context['from_or_to'] = $context['folder'] !== 'sent' ? 'from' : 'to';
428
		$context['signature_enabled'] = str_starts_with($modSettings['signature_settings'], '1');
429
		$context['disabled_fields'] = isset($modSettings['disabled_profile_fields']) ? array_flip(explode(',', $modSettings['disabled_profile_fields'])) : [];
430
431
		// Set the template layers we need
432
		$template_layers = theme()->getLayers();
433
		$template_layers->addAfter('subject_list', 'pm');
434
435
		$labelQuery = $context['folder'] !== 'sent' ? '
436
				AND FIND_IN_SET(' . $context['current_label_id'] . ', pmr.labels) != 0' : '';
437
438
		// They didn't pick a sort, so we use the forum by default.
439
		$sort_by = $this->_req->getQuery('sort', 'trim', 'date');
440
		$descending = $this->_req->hasQuery('desc');
441
442
		// Set our sort by query.
443
		switch ($sort_by)
444
		{
445
			case 'date':
446
				$sort_by_query = 'pm.id_pm';
447
				if (!empty($options['view_newest_pm_first']) && !$this->_req->hasQuery('desc') && !$this->_req->hasQuery('asc'))
448
				{
449
					$descending = true;
450
				}
451
452
				break;
453
			case 'name':
454
				$sort_by_query = "COALESCE(mem.real_name, '')";
455
				break;
456
			case 'subject':
457
				$sort_by_query = 'pm.subject';
458
				break;
459
			default:
460
				$sort_by_query = 'pm.id_pm';
461
		}
462
463
		// Set the text to resemble the current folder.
464
		$pmbox = $context['folder'] !== 'sent' ? $txt['inbox'] : $txt['sent_items'];
465
		$txt['delete_all'] = str_replace('PMBOX', $pmbox, $txt['delete_all']);
466
467
		// Now, build the link tree!
468
		if ($context['current_label_id'] === -1)
469
		{
470
			$context['breadcrumbs'][] = [
471
				'url' => getUrl('action', ['action' => 'pm', 'f' => $context['folder']]),
472
				'name' => $pmbox
473
			];
474
		}
475
476
		// Build it further if we also have a label.
477
		if ($context['current_label_id'] !== -1)
478
		{
479
			$context['breadcrumbs'][] = [
480
				'url' => getUrl('action', ['action' => 'pm', 'f' => $context['folder'], 'l' => $context['current_label_id']]),
481
				'name' => $txt['pm_current_label'] . ': ' . $context['current_label']
482
			];
483
		}
484
485
		// Figure out how many messages there are.
486
		$max_messages = getPMCount(false, null, $labelQuery);
487
488
		// Only show the button if there are messages to delete.
489
		$context['show_delete'] = $max_messages > 0;
490
491
		// Start on the last page.
492
		if (!is_numeric($start) || $start >= $max_messages)
493
		{
494
			$start = ($max_messages - 1) - (($max_messages - 1) % $modSettings['defaultMaxMessages']);
495
		}
496
		elseif ($start < 0)
497
		{
498
			$start = 0;
499
		}
500
501
		// ... but wait - what if we want to start from a specific message?
502
		if ($this->_req->isSet('pmid'))
503
		{
504
			$pmID = $this->_req->getQuery('pmid', 'intval', 0);
505
506
			// Make sure you have access to this PM.
507
			if (!isAccessiblePM($pmID, $context['folder'] === 'sent' ? 'outbox' : 'inbox'))
508
			{
509
				throw new Exception('no_access', false);
510
			}
511
512
			$context['current_pm'] = $pmID;
513
514
			// With only one page of PM's we're gonna want page 1.
515
			if ($max_messages <= $modSettings['defaultMaxMessages'])
516
			{
517
				$start = 0;
518
			}
519
			// If we pass kstart we assume we're in the right place.
520
			elseif (!$this->_req->isSet('kstart'))
521
			{
522
				$start = getPMCount($descending, $pmID, $labelQuery);
523
524
				// To stop the page index's being abnormal, start the page on the page the message
525
				// would normally be located on...
526
				$start = $modSettings['defaultMaxMessages'] * (int) ($start / $modSettings['defaultMaxMessages']);
527
			}
528
		}
529
530
		// Sanitize and validate pmsg variable if set.
531
		if ($this->_req->isSet('pmsg'))
532
		{
533
			$pmsg = $this->_req->getQuery('pmsg', 'intval', 0);
534
535
			if (!isAccessiblePM($pmsg, $context['folder'] === 'sent' ? 'outbox' : 'inbox'))
536
			{
537
				throw new Exception('no_access', false);
538
			}
539
		}
540
541
		// Determine the navigation context
542
		$context['links'] += [
543
			'prev' => $start >= $modSettings['defaultMaxMessages'] ? $scripturl . '?action=pm;start=' . ($start - $modSettings['defaultMaxMessages']) : '',
544
			'next' => $start + $modSettings['defaultMaxMessages'] < $max_messages ? $scripturl . '?action=pm;start=' . ($start + $modSettings['defaultMaxMessages']) : '',
545
		];
546
547
		// We now know what they want, so let's fetch those PM's
548
		[$pms, $posters, $recipients, $lastData] = loadPMs([
549
			'sort_by_query' => $sort_by_query,
550
			'display_mode' => $context['display_mode'],
551
			'sort_by' => $sort_by,
552
			'label_query' => $labelQuery,
553
			'pmsg' => isset($pmsg) ? (int) $pmsg : 0,
554
			'descending' => $descending,
555
			'start' => $start,
556
			'limit' => $modSettings['defaultMaxMessages'],
557
			'folder' => $context['folder'],
558
			'pmid' => $pmID ?? 0,
559
		], $this->user->id);
560
561
		// Make sure that we have been given a correct head pm id if we are in conversation mode
562
		if ($context['display_mode'] === PmHelper::DISPLAY_AS_CONVERSATION && !empty($pmID) && $pmID != $lastData['id'])
563
		{
564
			throw new Exception('no_access', false);
565
		}
566
567
		// If loadPMs returned results, let's show the pm subject list
568
		if (!empty($pms))
569
		{
570
			// Tell the template if no pm has specifically been selected
571
			if (empty($pmID))
572
			{
573
				$context['current_pm'] = 0;
574
			}
575
576
			$display_pms = $context['display_mode'] === PmHelper::DISPLAY_ALL_AT_ONCE ? $pms : [$lastData['id']];
577
578
			// At this point we know the main id_pm's. But if we are looking at conversations, we need
579
			// the PMs that make up the conversation
580
			if ($context['display_mode'] === PmHelper::DISPLAY_AS_CONVERSATION)
581
			{
582
				[$display_pms, $posters] = loadConversationList($lastData['head'], $recipients, $context['folder']);
583
584
				// Conversation list may expose additional PM's being displayed
585
				$all_pms = array_unique(array_merge($pms, $display_pms));
586
587
				// See if any of these 'listing' PMs are in a conversation thread that has unread entries
588
				$context['conversation_unread'] = loadConversationUnreadStatus($all_pms);
589
			}
590
			// This is pretty much EVERY pm!
591
			else
592
			{
593
				$all_pms = array_unique(array_merge($pms, $display_pms));
594
			}
595
596
			// Get recipients (don't include bcc-recipients for your inbox, you're not supposed to know :P).
597
			[$context['message_labels'], $context['message_replied'], $context['message_unread']] = loadPMRecipientInfo($all_pms, $recipients, $context['folder']);
598
599
			// Make sure we don't load any unnecessary data for one at a time mode
600
			if ($context['display_mode'] === PmHelper::DISPLAY_ONE_AT_TIME)
601
			{
602
				foreach ($posters as $pm_key => $sender)
603
				{
604
					if (!in_array($pm_key, $display_pms))
605
					{
606
						unset($posters[$pm_key]);
607
					}
608
				}
609
			}
610
611
			// Load some information about the message sender
612
			$posters = array_unique($posters);
613
			if (!empty($posters))
614
			{
615
				MembersList::load($posters);
616
			}
617
618
			// Always build the subject list request when we have PMs, so the subject list
619
			// renders in all modes (0: all-at-once, 1: one-at-a-time, 2: conversation).
620
			// This also prevents sharing a DB result between subject and message renderers.
621
			// Get the order right.
622
			$orderBy = [];
623
			foreach (array_reverse($pms) as $pm)
624
			{
625
				$orderBy[] = 'pm.id_pm = ' . $pm;
626
			}
627
628
			// Separate query for these bits, the callback will use it as required
629
			$subjects_request = loadPMSubjectRequest($pms, $orderBy);
630
631
			// Execute the load message query if a message has been chosen and let
632
			// the callback fetch the results.  Otherwise, just show the pm selection list
633
			if (empty($pmsg) && empty($pmID) && $context['display_mode'] !== PmHelper::DISPLAY_ALL_AT_ONCE)
634
			{
635
				$messages_request = false;
636
			}
637
			else
638
			{
639
				$messages_request = loadPMMessageRequest($display_pms, $sort_by_query, $sort_by, $descending, $context['display_mode'], $context['folder']);
640
			}
641
		}
642
		else
643
		{
644
			$messages_request = false;
645
		}
646
647
		// Initialize the subject and message render callbacks
648
		$bodyParser = new Normal([], false);
649
		$opt = new ValuesContainer(['recipients' => $recipients]);
650
		$renderer = new PmRenderer($messages_request, $this->user, $bodyParser, $opt);
651
		$subject_renderer = new PmRenderer($subjects_request ?? $messages_request, $this->user, $bodyParser, $opt);
652
653
		// Subject and Message
654
		$context['get_pmessage'] = [$renderer, 'getContext'];
655
		$context['get_psubject'] = [$subject_renderer, 'getContext'];
656
657
		// Prepare some items for the template
658
		$context['topic_starter_id'] = 0;
659
		$context['can_send_pm'] = allowedTo('pm_send');
660
		$context['can_send_email'] = allowedTo('send_email_to_members');
661
		$context['sub_template'] = 'folder';
662
		$context['page_title'] = $txt['pm_inbox'];
663
		$context['sort_direction'] = $descending ? 'down' : 'up';
664
		$context['sort_by'] = $sort_by;
665
666
		if ($messages_request !== false && !empty($context['show_delete']) && $messages_request->hasResults())
667
		{
668
			theme()->getLayers()->addEnd('pm_pages_and_buttons');
669
		}
670
671
		// Set up the page index.
672
		$label_for_index = $this->_req->getQuery('l', 'intval');
673
		$context['page_index'] = constructPageIndex('{scripturl}?action=pm;f=' . $context['folder'] . ($label_for_index !== null ? ';l=' . (int) $label_for_index : '') . ';sort=' . $context['sort_by'] . ($descending ? ';desc' : ''), $start, $max_messages, $modSettings['defaultMaxMessages']);
674
		$context['start'] = $start;
675
676
		$context['pm_form_url'] = $scripturl . '?action=pm;sa=pmactions;' . ($context['display_mode'] === PmHelper::DISPLAY_AS_CONVERSATION ? 'conversation;' : '') . 'f=' . $context['folder'] . ';start=' . $context['start'] . ($context['current_label_id'] !== -1 ? ';l=' . $context['current_label_id'] : '');
677
678
		// Finally, mark the relevant messages as read.
679
		if ($context['folder'] !== 'sent' && !empty($context['labels'][(int) $context['current_label_id']]['unread_messages']))
680
		{
681
			// If the display mode is "old sk00l" do them all...
682
			if ($context['display_mode'] === PmHelper::DISPLAY_ALL_AT_ONCE)
683
			{
684
				markMessages(null, $context['current_label_id']);
685
			}
686
			// Otherwise do just the currently displayed ones!
687
			elseif (!empty($context['current_pm']))
688
			{
689
				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...
690
			}
691
		}
692
693
		// Build the conversation button array.
694
		if ($context['display_mode'] === PmHelper::DISPLAY_AS_CONVERSATION && !empty($context['current_pm']))
695
		{
696
			$context['conversation_buttons'] = [
697
				'delete' => [
698
					'text' => 'delete_conversation',
699
					'lang' => true,
700
					'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'],
701
					'custom' => 'onclick="return confirm(\'' . addslashes($txt['remove_message']) . '?\');"'
702
				],
703
			];
704
705
			// Allow mods to add additional buttons here
706
			call_integration_hook('integrate_conversation_buttons');
707
		}
708
	}
709
710
	/**
711
	 * Send a new personal message?
712
	 *
713
	 * @throws Exception pm_not_yours
714
	 */
715
	public function action_send(): void
716
	{
717
		global $txt, $modSettings, $context;
718
719
		// Load in some text and template dependencies
720
		Txt::load('PersonalMessage');
721
		theme()->getTemplates()->load('PersonalMessage');
722
723
		// Set the template we will use
724
		$context['sub_template'] = 'send';
725
726
		// Extract out the spam settings - cause it's neat.
727
		[$modSettings['max_pm_recipients'], $modSettings['pm_posts_verification'], $modSettings['pm_posts_per_hour']] = explode(',', $modSettings['pm_spam_settings']);
728
729
		// Set up some items for the template
730
		$context['page_title'] = $txt['send_message'];
731
		$context['reply'] = $this->_req->hasQuery('pmsg') || $this->_req->hasQuery('quote');
732
733
		// Check whether we've gone over the limit of messages we can send per hour.
734
		if (!empty($modSettings['pm_posts_per_hour'])
735
			&& !allowedTo(['admin_forum', 'moderate_forum', 'send_mail'])
736
			&& $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...
737
			&& $this->user->mod_cache['gq'] === '0=1')
738
		{
739
			// How many messages did they send this last hour?
740
			$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...
741
742
			if (!empty($pmCount) && $pmCount >= $modSettings['pm_posts_per_hour'])
743
			{
744
				throw new Exception('pm_too_many_per_hour', true, [$modSettings['pm_posts_per_hour']]);
745
			}
746
		}
747
748
		try
749
		{
750
			$pmsg_event = $this->_req->getQuery('pmsg', 'intval');
751
			$pmsg_event_quote = $this->_req->getQuery('quote', 'trim', '');
752
			if ($pmsg_event !== null)
753
			{
754
				$this->_events->trigger('before_set_context', ['pmsg' => $pmsg_event, 'quote' => $pmsg_event_quote]);
755
			}
756
		}
757
		catch (PmErrorException $pmErrorException)
758
		{
759
			$this->messagePostError($pmErrorException->namedRecipientList, $pmErrorException->recipientList, $pmErrorException->msgOptions);
760
			return;
761
		}
762
763
		// Quoting / Replying to a message?
764
		if ($this->_req->hasQuery('pmsg'))
765
		{
766
			$pmsg = $this->_req->getQuery('pmsg', 'intval');
767
768
			// Make sure this is accessible (not deleted)
769
			if (!isAccessiblePM($pmsg))
770
			{
771
				throw new Exception('no_access', false);
772
			}
773
774
			// Validate that this is one has been received?
775
			$isReceived = checkPMReceived($pmsg);
776
777
			// Get the quoted message (and make sure you're allowed to see this quote!).
778
			$row_quoted = loadPMQuote($pmsg, $isReceived);
779
			if ($row_quoted === false)
0 ignored issues
show
introduced by
The condition $row_quoted === false is always false.
Loading history...
780
			{
781
				throw new Exception('pm_not_yours', false);
782
			}
783
784
			// Censor the message.
785
			$row_quoted['subject'] = censor($row_quoted['subject']);
786
			$row_quoted['body'] = censor($row_quoted['body']);
787
788
			// Let's make sure we mark this one as read
789
			markMessages($pmsg);
790
791
			// Figure out which flavor or 'Re: ' to use
792
			$context['response_prefix'] = response_prefix();
793
794
			$form_subject = $row_quoted['subject'];
795
796
			// Add 'Re: ' to it....
797
			if ($context['reply'] && trim($context['response_prefix']) !== '' && Util::strpos($form_subject, trim($context['response_prefix'])) !== 0)
798
			{
799
				$form_subject = $context['response_prefix'] . $form_subject;
800
			}
801
802
			// If quoting, let's clean up some things and set the quote header for the pm body
803
			if ($this->_req->hasQuery('quote'))
804
			{
805
				// Remove any nested quotes and <br />...
806
				$form_message = preg_replace('~<br ?/?>~i', "\n", $row_quoted['body']);
807
				$form_message = removeNestedQuotes($form_message);
808
809
				if (empty($row_quoted['id_member']))
810
				{
811
					$form_message = '[quote author=&quot;' . $row_quoted['real_name'] . '&quot;]' . "\n" . $form_message . "\n" . '[/quote]';
812
				}
813
				else
814
				{
815
					$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]';
816
				}
817
			}
818
			else
819
			{
820
				$form_message = '';
821
			}
822
823
			// Allow them to QQ the message they are replying to
824
			loadJavascriptFile('quickQuote.js', ['defer' => true]);
825
			theme()->addInlineJavascript("
826
				document.addEventListener('DOMContentLoaded', () => new Elk_QuickQuote(), false);", true
827
			);
828
829
			// Do the BBC thang on the message.
830
			$bbc_parser = ParserWrapper::instance();
831
			$row_quoted['body'] = $bbc_parser->parsePM($row_quoted['body']);
832
833
			// Set up the quoted message array.
834
			$context['quoted_message'] = [
835
				'id' => $row_quoted['id_pm'],
836
				'pm_head' => $row_quoted['pm_head'],
837
				'member' => [
838
					'name' => $row_quoted['real_name'],
839
					'username' => $row_quoted['member_name'],
840
					'id' => $row_quoted['id_member'],
841
					'href' => empty($row_quoted['id_member']) ? '' : getUrl('profile', ['action' => 'profile', 'u' => $row_quoted['id_member']]),
842
					'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>',
843
				],
844
				'subject' => $row_quoted['subject'],
845
				'time' => standardTime($row_quoted['msgtime']),
846
				'html_time' => htmlTime($row_quoted['msgtime']),
847
				'timestamp' => forum_time(true, $row_quoted['msgtime']),
848
				'body' => $row_quoted['body']
849
			];
850
		}
851
		// A new message it is then
852
		else
853
		{
854
			$context['quoted_message'] = false;
855
			$form_subject = '';
856
			$form_message = '';
857
		}
858
859
		// Start of like we don't know where this is going
860
		$context['recipients'] = [
861
			'to' => [],
862
			'bcc' => [],
863
		];
864
865
		// Sending by ID?  Replying to all?  Fetch the real_name(s).
866
		if ($this->_req->hasQuery('u'))
867
		{
868
			// If the user is replying to all, get all the other members this was sent to.
869
			$u_param = $this->_req->getQuery('u', 'trim|strval', '');
870
			if ($u_param === 'all' && isset($row_quoted))
871
			{
872
				// Firstly, to reply to all, we clearly already have $row_quoted - so have the original member from.
873
				if ($row_quoted['id_member'] != $this->user->id)
874
				{
875
					$context['recipients']['to'][] = [
876
						'id' => $row_quoted['id_member'],
877
						'name' => htmlspecialchars($row_quoted['real_name'], ENT_COMPAT),
878
					];
879
				}
880
881
				// Now to get all the others.
882
				$context['recipients']['to'] = array_merge($context['recipients']['to'], isset($pmsg) ? loadPMRecipientsAll($pmsg) : []);
883
			}
884
			else
885
			{
886
				$users_csv = $u_param;
887
				$users = $users_csv === '' ? [] : array_map('intval', explode(',', $users_csv));
888
				$users = array_unique($users);
889
890
				// For all the member's this is going to get their display name.
891
				require_once(SUBSDIR . '/Members.subs.php');
892
				$result = getBasicMemberData($users);
893
894
				foreach ($result as $row)
895
				{
896
					$context['recipients']['to'][] = [
897
						'id' => $row['id_member'],
898
						'name' => $row['real_name'],
899
					];
900
				}
901
			}
902
903
			// Get a literal name list in case the user has JavaScript disabled.
904
			$names = [];
905
			foreach ($context['recipients']['to'] as $to)
906
			{
907
				$names[] = $to['name'];
908
			}
909
910
			$context['to_value'] = empty($names) ? '' : '&quot;' . implode('&quot;, &quot;', $names) . '&quot;';
911
		}
912
		else
913
		{
914
			$context['to_value'] = '';
915
		}
916
917
		// Set the defaults...
918
		$context['subject'] = $form_subject;
919
		$context['message'] = str_replace(['"', '<', '>', '&nbsp;'], ['&quot;', '&lt;', '&gt;', ' '], $form_message);
920
921
		// And build the link tree.
922
		$context['breadcrumbs'][] = [
923
			'url' => getUrl('action', ['action' => 'pm', 'sa' => 'send']),
924
			'name' => $txt['new_message']
925
		];
926
927
		// Needed for the editor.
928
		require_once(SUBSDIR . '/Editor.subs.php');
929
930
		// Now create the editor.
931
		$editorOptions = [
932
			'id' => 'message',
933
			'value' => $context['message'],
934
			'height' => '250px',
935
			'width' => '100%',
936
			'labels' => [
937
				'post_button' => $txt['send_message'],
938
			],
939
			'smiley_container' => 'smileyBox_message',
940
			'bbc_container' => 'bbcBox_message',
941
			'preview_type' => 2,
942
		];
943
944
		// Trigger the prepare_send_context PM event
945
		$this->_events->trigger('prepare_send_context', ['editorOptions' => &$editorOptions]);
946
947
		create_control_richedit($editorOptions);
948
949
		// No one is bcc'ed just yet
950
		$context['bcc_value'] = '';
951
952
		// Register this form and get a sequence number in $context.
953
		checkSubmitOnce('register');
954
	}
955
956
	/**
957
	 * An error in the message...
958
	 *
959
	 * @param array $named_recipients
960
	 * @param array $recipient_ids array keys of [bbc] => int[] and [to] => int[]
961
	 * @param object $msg_options body, subject, and reply values
962
	 *
963
	 * @throws Exception pm_not_yours
964
	 */
965
	public function messagePostError($named_recipients, $recipient_ids = [], $msg_options = null): void
966
	{
967
		global $txt, $context, $modSettings;
968
969
		if ($this->getApi() !== false)
970
		{
971
			$context['sub_template'] = 'generic_preview';
972
		}
973
		else
974
		{
975
			$context['sub_template'] = 'send';
976
			$context['menu_data_' . $context['pm_menu_id']]['current_area'] = 'send';
977
		}
978
979
		$context['page_title'] = $txt['send_message'];
980
		$error_types = ErrorContext::context('pm', 1);
981
982
		// Got some known members?
983
		$context['recipients'] = [
984
			'to' => [],
985
			'bcc' => [],
986
		];
987
988
		if (!empty($recipient_ids['to']) || !empty($recipient_ids['bcc']))
989
		{
990
			$allRecipients = array_merge($recipient_ids['to'], $recipient_ids['bcc']);
991
992
			require_once(SUBSDIR . '/Members.subs.php');
993
994
			// Get the latest activated member's display name.
995
			$result = getBasicMemberData($allRecipients);
996
			foreach ($result as $row)
997
			{
998
				$recipientType = in_array($row['id_member'], $recipient_ids['bcc']) ? 'bcc' : 'to';
999
				$context['recipients'][$recipientType][] = [
1000
					'id' => $row['id_member'],
1001
					'name' => $row['real_name'],
1002
				];
1003
			}
1004
		}
1005
1006
		// Set everything up like before...
1007
		if (!empty($msg_options))
1008
		{
1009
			$context['subject'] = $msg_options->subject;
1010
			$context['message'] = $msg_options->body;
1011
			$context['reply'] = $msg_options->reply_to;
1012
		}
1013
		else
1014
		{
1015
			$subject_in = $this->_req->getPost('subject', 'trim|Util::htmlspecialchars', '');
1016
			$message_in = $this->_req->getPost('message', 'trim|strval|cleanhtml', '');
1017
			$context['subject'] = $subject_in;
1018
			$context['message'] = str_replace(['  '], ['&nbsp; '], $message_in);
1019
			$context['reply'] = $this->_req->getPost('replied_to', 'intval', 0) > 0;
1020
		}
1021
1022
		// If this is a reply to a message, we need to reload the quote
1023
		if ($context['reply'])
1024
		{
1025
			$pmsg = $this->_req->getPost('replied_to', 'intval', 0);
1026
			$isReceived = $context['folder'] !== 'sent';
1027
			$row_quoted = loadPMQuote($pmsg, $isReceived);
1028
			if ($row_quoted === false)
0 ignored issues
show
introduced by
The condition $row_quoted === false is always false.
Loading history...
1029
			{
1030
				if ($this->getApi() === false)
1031
				{
1032
					throw new Exception('pm_not_yours', false);
1033
				}
1034
1035
				$error_types->addError('pm_not_yours');
1036
			}
1037
			else
1038
			{
1039
				$row_quoted['subject'] = censor($row_quoted['subject']);
1040
				$row_quoted['body'] = censor($row_quoted['body']);
1041
				$bbc_parser = ParserWrapper::instance();
1042
1043
				$context['quoted_message'] = [
1044
					'id' => $row_quoted['id_pm'],
1045
					'pm_head' => $row_quoted['pm_head'],
1046
					'member' => [
1047
						'name' => $row_quoted['real_name'],
1048
						'username' => $row_quoted['member_name'],
1049
						'id' => $row_quoted['id_member'],
1050
						'href' => empty($row_quoted['id_member']) ? '' : getUrl('profile', ['action' => 'profile', 'u' => $row_quoted['id_member']]),
1051
						'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>',
1052
					],
1053
					'subject' => $row_quoted['subject'],
1054
					'time' => standardTime($row_quoted['msgtime']),
1055
					'html_time' => htmlTime($row_quoted['msgtime']),
1056
					'timestamp' => forum_time(true, $row_quoted['msgtime']),
1057
					'body' => $bbc_parser->parsePM($row_quoted['body']),
1058
				];
1059
			}
1060
		}
1061
1062
		// Build the link tree...
1063
		$context['breadcrumbs'][] = [
1064
			'url' => getUrl('action', ['action' => 'pm', 'sa' => 'send']),
1065
			'name' => $txt['new_message']
1066
		];
1067
1068
		// Set each of the errors for the template.
1069
		$context['post_error'] = [
1070
			'errors' => $error_types->prepareErrors(),
1071
			'type' => $error_types->getErrorType() == 0 ? 'minor' : 'serious',
1072
			'title' => $txt['error_while_submitting'],
1073
		];
1074
1075
		// We need to load the editor once more.
1076
		require_once(SUBSDIR . '/Editor.subs.php');
1077
1078
		// Create it...
1079
		$editorOptions = [
1080
			'id' => 'message',
1081
			'value' => $context['message'],
1082
			'width' => '100%',
1083
			'height' => '250px',
1084
			'labels' => [
1085
				'post_button' => $txt['send_message'],
1086
			],
1087
			'smiley_container' => 'smileyBox_message',
1088
			'bbc_container' => 'bbcBox_message',
1089
			'preview_type' => 2,
1090
		];
1091
1092
		// Trigger the prepare_send_context PM event
1093
		$this->_events->trigger('prepare_send_context', ['editorOptions' => &$editorOptions]);
1094
1095
		create_control_richedit($editorOptions);
1096
1097
		// Check whether we need to show the code again.
1098
		$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 posts 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 is_admin does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
1099
		if ($context['require_verification'] && $this->getApi() === false)
1100
		{
1101
			$verificationOptions = [
1102
				'id' => 'pm',
1103
			];
1104
			$context['require_verification'] = VerificationControlsIntegrate::create($verificationOptions);
1105
			$context['visual_verification_id'] = $verificationOptions['id'];
1106
		}
1107
1108
		$context['to_value'] = empty($named_recipients['to']) ? '' : '&quot;' . implode('&quot;, &quot;', $named_recipients['to']) . '&quot;';
1109
		$context['bcc_value'] = empty($named_recipients['bcc']) ? '' : '&quot;' . implode('&quot;, &quot;', $named_recipients['bcc']) . '&quot;';
1110
1111
		// No check for the previous submission is needed.
1112
		checkSubmitOnce('free');
1113
1114
		// Acquire a new form sequence number.
1115
		checkSubmitOnce('register');
1116
	}
1117
1118
	/**
1119
	 * Send a personal message.
1120
 	 */
1121
	public function action_send2()
1122
	{
1123
		global $txt, $context, $modSettings;
1124
1125
		// All the helpers we need
1126
		require_once(SUBSDIR . '/Auth.subs.php');
1127
		require_once(SUBSDIR . '/Post.subs.php');
1128
1129
		Txt::load('PersonalMessage', false);
1130
1131
		// Extract out the spam settings - it saves database space!
1132
		[$modSettings['max_pm_recipients'], $modSettings['pm_posts_verification'], $modSettings['pm_posts_per_hour']] = explode(',', $modSettings['pm_spam_settings']);
1133
1134
		// Initialize the errors we're about to make.
1135
		$post_errors = ErrorContext::context('pm', 1);
1136
1137
		// Check whether we've gone over the limit of messages we can send per hour - fatal error if fails!
1138
		if (!empty($modSettings['pm_posts_per_hour'])
1139
			&& !allowedTo(['admin_forum', 'moderate_forum', 'send_mail'])
1140
			&& $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...
1141
			&& $this->user->mod_cache['gq'] === '0=1')
1142
		{
1143
			// How many they sent this last hour?
1144
			$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...
1145
1146
			if (!empty($pmCount) && $pmCount >= $modSettings['pm_posts_per_hour'])
1147
			{
1148
				if ($this->getApi() === false)
1149
				{
1150
					throw new Exception('pm_too_many_per_hour', true, [$modSettings['pm_posts_per_hour']]);
1151
				}
1152
1153
				$post_errors->addError('pm_too_many_per_hour');
1154
			}
1155
		}
1156
1157
		// If your session timed out, show an error, but do allow to re-submit.
1158
		if ($this->getApi() === false && checkSession('post', '', false) !== '')
1159
		{
1160
			$post_errors->addError('session_timeout');
1161
		}
1162
1163
		// Local sanitized copies used for preview/sending
1164
		$subject = $this->_req->getPost('subject', 'trim|strval', '');
1165
		$subject = strtr(Util::htmltrim($subject), ["\r" => '', "\n" => '', "\t" => '']);
1166
		$message = $this->_req->getPost('message', 'trim|strval', '');
1167
1168
		$this->_req->post->to = $this->_req->getPost('to', 'trim', empty($this->_req->query->to) ? '' : $this->_req->query->to);
1169
		$this->_req->post->bcc = $this->_req->getPost('bcc', 'trim', empty($this->_req->query->bcc) ? '' : $this->_req->query->bcc);
1170
1171
		// Route the input from the 'u' parameter to the 'to'-list.
1172
		if (!empty($this->_req->post->u))
1173
		{
1174
			$this->_req->post->recipient_to = explode(',', $this->_req->post->u);
1175
		}
1176
1177
		$bbc_parser = ParserWrapper::instance();
1178
1179
		// Construct the list of recipients.
1180
		$recipientList = [];
1181
		$namedRecipientList = [];
1182
		$namesNotFound = [];
1183
		foreach (['to', 'bcc'] as $recipientType)
1184
		{
1185
			// First, let's see if there's user ID's given.
1186
			$recipientList[$recipientType] = [];
1187
			$type = 'recipient_' . $recipientType;
1188
			if (!empty($this->_req->post->{$type}) && is_array($this->_req->post->{$type}))
1189
			{
1190
				$recipientList[$recipientType] = array_map('intval', $this->_req->post->{$type});
1191
			}
1192
1193
			// Are there also literal names set?
1194
			if (!empty($this->_req->post->{$recipientType}))
1195
			{
1196
				// We're going to take out the "s anyway ;).
1197
				$recipientString = strtr($this->_req->post->{$recipientType}, ['\\"' => '"']);
1198
1199
				preg_match_all('~"([^"]+)"~', $recipientString, $matches);
1200
				$namedRecipientList[$recipientType] = array_unique(array_merge($matches[1], explode(',', preg_replace('~"[^"]+"~', '', $recipientString))));
1201
1202
				// Clean any literal names entered
1203
				foreach ($namedRecipientList[$recipientType] as $index => $recipient)
1204
				{
1205
					if (trim($recipient) !== '')
1206
					{
1207
						$namedRecipientList[$recipientType][$index] = Util::htmlspecialchars(Util::strtolower(trim($recipient)));
1208
					}
1209
					else
1210
					{
1211
						unset($namedRecipientList[$recipientType][$index]);
1212
					}
1213
				}
1214
1215
				// Now see if we can resolve any entered name (not suggest selected) to an actual user
1216
				if (!empty($namedRecipientList[$recipientType]))
1217
				{
1218
					$foundMembers = findMembers($namedRecipientList[$recipientType]);
1219
1220
					// Assume all are not found until proven otherwise.
1221
					$namesNotFound[$recipientType] = $namedRecipientList[$recipientType];
1222
1223
					// Make sure we only have each member listed once, in case they did not use the select list
1224
					foreach ($foundMembers as $member)
1225
					{
1226
						$testNames = [
1227
							Util::strtolower($member['username']),
1228
							Util::strtolower($member['name']),
1229
							Util::strtolower($member['email']),
1230
						];
1231
1232
						if (array_intersect($testNames, $namedRecipientList[$recipientType]) !== [])
1233
						{
1234
							$recipientList[$recipientType][] = $member['id'];
1235
1236
							// Get rid of this username, since we found it.
1237
							$namesNotFound[$recipientType] = array_diff($namesNotFound[$recipientType], $testNames);
1238
						}
1239
					}
1240
				}
1241
			}
1242
1243
			// Selected a recipient to be deleted? Remove them now.
1244
			$delete_recipient = $this->_req->getPost('delete_recipient', 'intval', 0);
1245
			if (!empty($delete_recipient))
1246
			{
1247
				$recipientList[$recipientType] = array_diff($recipientList[$recipientType], [$delete_recipient]);
1248
			}
1249
1250
			// Make sure we don't include the same name twice
1251
			$recipientList[$recipientType] = array_unique($recipientList[$recipientType]);
1252
		}
1253
1254
		// Are we changing the recipients somehow?
1255
		$is_recipient_change = $this->_req->hasPost('delete_recipient') || $this->_req->hasPost('to_submit') || $this->_req->hasPost('bcc_submit');
1256
1257
		// Check if there's at least one recipient.
1258
		if (empty($recipientList['to']) && empty($recipientList['bcc']))
1259
		{
1260
			$post_errors->addError('no_to');
1261
		}
1262
1263
		// Make sure that we remove the members who did get it from the screen.
1264
		if (!$is_recipient_change)
1265
		{
1266
			foreach (array_keys($recipientList) as $recipientType)
1267
			{
1268
				if (!empty($namesNotFound[$recipientType]))
1269
				{
1270
					$post_errors->addError('bad_' . $recipientType);
1271
1272
					// Since we already have a post error, remove the previous one.
1273
					$post_errors->removeError('no_to');
1274
1275
					foreach ($namesNotFound[$recipientType] as $name)
1276
					{
1277
						$context['send_log']['failed'][] = sprintf($txt['pm_error_user_not_found'], $name);
1278
					}
1279
				}
1280
			}
1281
		}
1282
1283
		// Did they make any mistakes like no subject or message?
1284
		if ($subject === '')
1285
		{
1286
			$post_errors->addError('no_subject');
1287
		}
1288
1289
		if ($message === '')
1290
		{
1291
			$post_errors->addError('no_message');
1292
		}
1293
		elseif (!empty($modSettings['max_messageLength']) && Util::strlen($message) > $modSettings['max_messageLength'])
1294
		{
1295
			$post_errors->addError('long_message');
1296
		}
1297
		else
1298
		{
1299
			// Preparse the message.
1300
			preparsecode($message);
1301
1302
			// Make sure there's still some content left without the tags.
1303
			if (Util::htmltrim(strip_tags($bbc_parser->parsePM(Util::htmlspecialchars($message, ENT_QUOTES)), '<img>')) === ''
1304
				&& (!allowedTo('admin_forum') || !str_contains($message, '[html]')))
1305
			{
1306
				$post_errors->addError('no_message');
1307
			}
1308
		}
1309
1310
		// If they made any errors, give them a chance to make amends.
1311
		if ($post_errors->hasErrors()
1312
			&& !$is_recipient_change
1313
			&& !$this->_req->isSet('preview')
1314
			&& $this->getApi() === false)
1315
		{
1316
			$this->messagePostError($namedRecipientList, $recipientList);
1317
1318
			return false;
1319
		}
1320
1321
		// Want to take a second glance before you send?
1322
		if ($this->_req->isSet('preview'))
1323
		{
1324
			// Set everything up to be displayed.
1325
			$context['preview_subject'] = Util::htmlspecialchars($this->_req->getPost('subject', 'trim|strval', ''));
1326
			$context['preview_message'] = Util::htmlspecialchars($this->_req->getPost('message', 'trim|strval',''),ENT_QUOTES, 'UTF-8', true);
1327
			preparsecode($context['preview_message'], true);
1328
1329
			// Parse out the BBC if it is enabled.
1330
			$context['preview_message'] = $bbc_parser->parsePM($context['preview_message']);
1331
1332
			// Censor, as always.
1333
			$context['preview_subject'] = censor($context['preview_subject']);
1334
			$context['preview_message'] = censor($context['preview_message']);
1335
1336
			// Set a descriptive title.
1337
			$context['page_title'] = $txt['preview'] . ' - ' . $context['preview_subject'];
1338
1339
			// Pretend they messed up but don't ignore if they really did :P.
1340
			$this->messagePostError($namedRecipientList, $recipientList);
1341
1342
			return false;
1343
		}
1344
1345
		if ($is_recipient_change)
1346
		{
1347
			// Maybe we couldn't find one?
1348
			foreach ($namesNotFound as $recipientType => $names)
1349
			{
1350
				$post_errors->addError('bad_' . $recipientType);
1351
				foreach ($names as $name)
1352
				{
1353
					$context['send_log']['failed'][] = sprintf($txt['pm_error_user_not_found'], $name);
1354
				}
1355
			}
1356
1357
			$this->messagePostError($namedRecipientList, $recipientList);
1358
1359
			return true;
1360
		}
1361
1362
		// Adding a recipient cause JavaScript ain't working?
1363
		try
1364
		{
1365
			$this->_events->trigger('before_sending', ['namedRecipientList' => $namedRecipientList, 'recipientList' => $recipientList, 'namesNotFound' => $namesNotFound, 'post_errors' => $post_errors]);
1366
		}
1367
		catch (ControllerRedirectException)
1368
		{
1369
			$this->messagePostError($namedRecipientList, $recipientList);
1370
1371
			return true;
1372
		}
1373
1374
		// Safety net, it may be a module may just add to the list of errors without actually throw the error
1375
		if ($post_errors->hasErrors() && !$this->_req->isSet('preview') && $this->getApi() === false)
1376
		{
1377
			$this->messagePostError($namedRecipientList, $recipientList);
1378
1379
			return false;
1380
		}
1381
1382
		// Before we send the PM, let's make sure we don't have an abuse of numbers.
1383
		if (!empty($modSettings['max_pm_recipients']) && count($recipientList['to']) + count($recipientList['bcc']) > $modSettings['max_pm_recipients'] && !allowedTo(['moderate_forum', 'send_mail', 'admin_forum']))
1384
		{
1385
			$context['send_log'] = [
1386
				'sent' => [],
1387
				'failed' => [sprintf($txt['pm_too_many_recipients'], $modSettings['max_pm_recipients'])],
1388
			];
1389
1390
			$this->messagePostError($namedRecipientList, $recipientList);
1391
1392
			return false;
1393
		}
1394
1395
		// Protect from message spamming.
1396
		spamProtection('pm');
1397
1398
		// Prevent double submission of this form.
1399
		checkSubmitOnce('check');
1400
1401
		// Finally, do the actual sending of the PM.
1402
		if (!empty($recipientList['to']) || !empty($recipientList['bcc']))
1403
		{
1404
			// Reset the message to pre-check condition, sendpm will do the rest.
1405
			$subject = $this->_req->getPost('subject', 'trim|strval', '');
1406
			$message = $this->_req->getPost('message', 'trim|strval', '');
1407
			$context['send_log'] = sendpm($recipientList, $subject, $message, true, null, empty($this->_req->post->pm_head) ? 0 : (int) $this->_req->post->pm_head);
1408
		}
1409
		else
1410
		{
1411
			$context['send_log'] = [
1412
				'sent' => [],
1413
				'failed' => []
1414
			];
1415
		}
1416
1417
		// Mark the message as "replied to".
1418
		$replied_to = $this->_req->getPost('replied_to', 'intval', 0);
1419
		$box = $this->_req->getPost('f', 'trim', '');
1420
		if (!empty($context['send_log']['sent']) && !empty($replied_to) && $box === 'inbox')
1421
		{
1422
			require_once(SUBSDIR . '/PersonalMessage.subs.php');
1423
			setPMRepliedStatus($this->user->id, $replied_to);
1424
		}
1425
1426
		$failed = !empty($context['send_log']['failed']);
1427
		$this->_events->trigger('message_sent', ['failed' => $failed]);
1428
1429
		// If one or more of the recipients were invalid, go back to the post screen with the failed usernames.
1430
		if ($failed)
1431
		{
1432
			$this->messagePostError($namesNotFound, [
1433
				'to' => array_intersect($recipientList['to'], $context['send_log']['failed']),
1434
				'bcc' => array_intersect($recipientList['bcc'], $context['send_log']['failed'])
1435
			]);
1436
1437
			return false;
1438
		}
1439
1440
		// Message sent successfully
1441
		$context['current_label_redirect'] .= ';done=sent';
1442
1443
		// Go back to where they sent from, if possible...
1444
		redirectexit($context['current_label_redirect']);
1445
	}
1446
1447
	/**
1448
	 * This function performs all additional actions including the deleting
1449
	 * and labeling of PM's
1450
	 */
1451
	public function action_pmactions(): void
1452
	{
1453
		global $context;
1454
1455
		checkSession('request');
1456
1457
		// Sending in the single pm choice via GET
1458
		$pm_actions = $this->_req->getQuery('pm_actions', null, '');
1459
1460
		// Set the action to apply to the PMs defined by pm_actions (yes, it is that brilliant)
1461
		$pm_action = $this->_req->getPost('pm_action', 'trim', '');
1462
		$pm_action = empty($pm_action) && $this->_req->hasPost('del_selected') ? 'delete' : $pm_action;
1463
1464
		// Create a list of PMs that we need to work on
1465
		$pms_list = $this->_req->getPost('pms', null, []);
1466
		if ($pm_action !== ''
1467
			&& !empty($pms_list)
1468
			&& is_array($pms_list))
1469
		{
1470
			$pm_actions = [];
1471
			foreach ($pms_list as $pm)
1472
			{
1473
				$pm_actions[(int) $pm] = $pm_action;
1474
			}
1475
		}
1476
1477
		// No messages to action then bug out
1478
		if (empty($pm_actions))
1479
		{
1480
			redirectexit($context['current_label_redirect']);
1481
		}
1482
1483
		// If we are in conversation, we may need to apply this to every message in that conversation.
1484
		if ($context['display_mode'] === PmHelper::DISPLAY_AS_CONVERSATION && $this->_req->hasQuery('conversation'))
1485
		{
1486
			$id_pms = array_map('intval', array_keys($pm_actions));
1487
			$pm_heads = getDiscussions($id_pms);
1488
			$pms = getPmsFromDiscussion(array_keys($pm_heads));
1489
1490
			// Copy the action from the single to PM to the others in the conversation.
1491
			foreach ($pms as $id_pm => $id_head)
1492
			{
1493
				if (isset($pm_heads[$id_head], $pm_actions[$pm_heads[$id_head]]))
1494
				{
1495
					$pm_actions[$id_pm] = $pm_actions[$pm_heads[$id_head]];
1496
				}
1497
			}
1498
		}
1499
1500
		// Get to doing what we've been told
1501
		$to_delete = [];
1502
		$to_label = [];
1503
		$label_type = [];
1504
		foreach ($pm_actions as $pm => $action)
1505
		{
1506
			// What are we doing with the selected messages, adding a label, removing, other?
1507
			switch (substr($action, 0, 4))
1508
			{
1509
				case 'dele':
1510
					$to_delete[] = (int) $pm;
1511
					break;
1512
				case 'add_':
1513
					$type = 'add';
1514
					$action = substr($action, 4);
1515
					break;
1516
				case 'rem_':
1517
					$type = 'rem';
1518
					$action = substr($action, 4);
1519
					break;
1520
				default:
1521
					$type = 'unk';
1522
			}
1523
1524
			if ((int) $action === -1 || (int) $action === 0 || (int) $action > 0)
1525
			{
1526
				$to_label[(int) $pm] = (int) $action;
1527
				$label_type[(int) $pm] = $type ?? '';
1528
			}
1529
		}
1530
1531
		// Deleting, it looks like?
1532
		if (!empty($to_delete))
1533
		{
1534
			deleteMessages($to_delete, $context['display_mode'] === PmHelper::DISPLAY_AS_CONVERSATION ? null : $context['folder']);
1535
		}
1536
1537
		// Are we labeling anything?
1538
		if (!empty($to_label) && $context['folder'] === 'inbox')
1539
		{
1540
			$updateErrors = changePMLabels($to_label, $label_type, $this->user->id);
1541
1542
			// Any errors?
1543
			if (!empty($updateErrors))
1544
			{
1545
				throw new Exception('labels_too_many', false, [$updateErrors]);
1546
			}
1547
		}
1548
1549
		// Back to the folder.
1550
		$_SESSION['pm_selected'] = array_keys($to_label);
1551
		redirectexit($context['current_label_redirect'] . (count($to_label) === 1 ? '#msg_' . $_SESSION['pm_selected'][0] : ''));
1552
	}
1553
1554
	/**
1555
	 * Are you sure you want to PERMANENTLY (mostly) delete ALL your messages?
1556
	 */
1557
	public function action_removeall(): void
1558
	{
1559
		global $txt, $context;
1560
1561
		// Only have to set up the template...
1562
		$context['sub_template'] = 'ask_delete';
1563
		$context['page_title'] = $txt['delete_all'];
1564
		$folder_flag = $this->_req->getQuery('f', 'trim|strval', '');
1565
		$context['delete_all'] = $folder_flag === 'all';
1566
1567
		// And set the folder name...
1568
		$txt['delete_all'] = str_replace('PMBOX', $context['folder'] != 'sent' ? $txt['inbox'] : $txt['sent_items'], $txt['delete_all']);
1569
	}
1570
1571
	/**
1572
	 * Delete ALL the messages!
1573
	 */
1574
	public function action_removeall2(): void
1575
	{
1576
		global $context;
1577
1578
		checkSession('get');
1579
1580
		// If all, then delete all messages the user has.
1581
		$folder_flag = $this->_req->getQuery('f', 'trim|strval', '');
1582
		if ($folder_flag === 'all')
1583
		{
1584
			deleteMessages(null);
1585
		}
1586
		// Otherwise just the selected folder.
1587
		else
1588
		{
1589
			deleteMessages(null, $folder_flag !== '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(): void
1600
	{
1601
		global $txt, $context;
1602
1603
		// Actually delete the messages.
1604
		if ($this->_req->hasPost('age'))
1605
		{
1606
			checkSession();
1607
1608
			// Calculate the time to delete before.
1609
			$age_days = $this->_req->getPost('age', 'intval', 0);
1610
			$deleteTime = max(0, time() - (86400 * $age_days));
1611
1612
			// Select all the messages older than $deleteTime.
1613
			$toDelete = getPMsOlderThan($this->user->id, $deleteTime);
1614
1615
			// Delete the actual messages.
1616
			deleteMessages($toDelete);
1617
1618
			// Go back to their inbox.
1619
			redirectexit($context['current_label_redirect']);
1620
		}
1621
1622
		// Build the link tree elements.
1623
		$context['breadcrumbs'][] = [
1624
			'url' => getUrl('action', ['action' => 'pm', 'sa' => 'prune']),
1625
			'name' => $txt['pm_prune']
1626
		];
1627
		$context['sub_template'] = 'prune';
1628
		$context['page_title'] = $txt['pm_prune'];
1629
	}
1630
1631
	/**
1632
	 * This function handles adding, deleting, and editing labels on messages.
1633
	 */
1634
	public function action_manlabels(): void
1635
	{
1636
		$controller = new Labels($this->_events);
1637
		$controller->setUser($this->user);
1638
		$controller->action_manlabels();
1639
	}
1640
1641
	/**
1642
	 * Allows editing Personal Message Settings.
1643
	 *
1644
	 * @uses ProfileOptions controller. (@todo refactor this.)
1645
	 * @uses Profile template.
1646
	 * @uses Profile language file.
1647
	 */
1648
	public function action_settings(): void
1649
	{
1650
		$controller = new Settings($this->_events);
0 ignored issues
show
Bug introduced by
The type ElkArte\PersonalMessage\Settings was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
1651
		$controller->setUser($this->user);
1652
		$controller->action_settings();
1653
	}
1654
1655
	/**
1656
	 * Allows the user to report a personal message to an administrator.
1657
	 *
1658
	 * What it does:
1659
	 *
1660
	 * - In the first instance requires that the ID of the message to report is passed through $_GET.
1661
	 * - It allows the user to report to either a particular administrator - or the whole admin team.
1662
	 * - It will forward on a copy of the original message without allowing the reporter to make changes.
1663
	 *
1664
	 * @uses report_message sub-template.
1665
	 */
1666
	public function action_report(): void
1667
	{
1668
		$controller = new Report($this->_events);
1669
		$controller->setUser($this->user);
1670
		$controller->action_report();
1671
	}
1672
1673
	/**
1674
	 * List and allow adding/entering all man rules
1675
	 *
1676
	 * @uses sub template rules
1677
	 */
1678
	public function action_manrules(): void
1679
	{
1680
		$controller = new Rules($this->_events);
1681
		$controller->setUser($this->user);
1682
		$controller->action_manrules();
1683
	}
1684
1685
	/**
1686
	 * Actually do the search of personal messages and show the results
1687
	 *
1688
	 * What it does:
1689
	 *
1690
	 * - Accessed with ?action=pm;sa=search2
1691
	 * - Checks user input and searches the pm table for messages matching the query.
1692
	 * - Uses the search_results sub template of the PersonalMessage template.
1693
	 * - Show the results of the search query.
1694
	 */
1695
	public function action_search2(): ?bool
1696
	{
1697
		$controller = new Search($this->_events);
1698
		$controller->setUser($this->user);
1699
1700
		return $controller->action_search2();
1701
	}
1702
1703
1704
	/**
1705
	 * Allows searching personal messages.
1706
	 *
1707
	 * What it does:
1708
	 *
1709
	 * - Accessed with ?action=pm;sa=search
1710
	 * - Shows the screen to search PMs (?action=pm;sa=search)
1711
	 * - Uses the search sub template of the PersonalMessage template.
1712
	 * - Decodes and loads search parameters given in the URL (if any).
1713
	 * - The form redirects to index.php?action=pm;sa=search2.
1714
	 *
1715
	 * @uses search sub template
1716
	 */
1717
	public function action_search(): void
1718
	{
1719
		$controller = new Search($this->_events);
1720
		$controller->setUser($this->user);
1721
		$controller->action_search();
1722
	}
1723
1724
1725
	/**
1726
	 * Allows the user to mark a personal message as unread, so they remember to come back to it
1727
	 */
1728
	public function action_markunread(): void
1729
	{
1730
		global $context;
1731
1732
		checkSession('request');
1733
1734
		$pmsg = $this->_req->getQuery('pmsg', 'intval');
1735
1736
		// Marking a message as unread, we need a message that was sent to them
1737
		// Can't mark your own reply as unread, that would be weird
1738
		if (!is_null($pmsg) && checkPMReceived($pmsg))
1739
		{
1740
			// Make sure this is accessible, should be, of course
1741
			if (!isAccessiblePM($pmsg, 'inbox'))
1742
			{
1743
				throw new Exception('no_access', false);
1744
			}
1745
1746
			// Well then, you get to hear about it all over again
1747
			markMessagesUnread($pmsg);
1748
		}
1749
1750
		// Back to the folder.
1751
		redirectexit($context['current_label_redirect']);
1752
	}
1753
}
1754