PersonalMessage::action_prune()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 30
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 13
nc 2
nop 0
dl 0
loc 30
rs 9.8333
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 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
				break;
452
			case 'name':
453
				$sort_by_query = "COALESCE(mem.real_name, '')";
454
				break;
455
			case 'subject':
456
				$sort_by_query = 'pm.subject';
457
				break;
458
			default:
459
				$sort_by_query = 'pm.id_pm';
460
		}
461
462
		// Set the text to resemble the current folder.
463
		$pmbox = $context['folder'] !== 'sent' ? $txt['inbox'] : $txt['sent_items'];
464
		$txt['delete_all'] = str_replace('PMBOX', $pmbox, $txt['delete_all']);
465
466
		// Now, build the link tree!
467
		if ($context['current_label_id'] === -1)
468
		{
469
			$context['breadcrumbs'][] = [
470
				'url' => getUrl('action', ['action' => 'pm', 'f' => $context['folder']]),
471
				'name' => $pmbox
472
			];
473
		}
474
475
		// Build it further if we also have a label.
476
		if ($context['current_label_id'] !== -1)
477
		{
478
			$context['breadcrumbs'][] = [
479
				'url' => getUrl('action', ['action' => 'pm', 'f' => $context['folder'], 'l' => $context['current_label_id']]),
480
				'name' => $txt['pm_current_label'] . ': ' . $context['current_label']
481
			];
482
		}
483
484
		// Figure out how many messages there are.
485
		$max_messages = getPMCount(false, null, $labelQuery);
486
487
		// Only show the button if there are messages to delete.
488
		$context['show_delete'] = $max_messages > 0;
489
490
		// Start on the last page.
491
		if (!is_numeric($start) || $start >= $max_messages)
492
		{
493
			$start = ($max_messages - 1) - (($max_messages - 1) % $modSettings['defaultMaxMessages']);
494
		}
495
		elseif ($start < 0)
496
		{
497
			$start = 0;
498
		}
499
500
		// ... but wait - what if we want to start from a specific message?
501
		if ($this->_req->isSet('pmid'))
502
		{
503
			$pmID = $this->_req->getQuery('pmid', 'intval', 0);
504
505
			// Make sure you have access to this PM.
506
			if (!isAccessiblePM($pmID, $context['folder'] === 'sent' ? 'outbox' : 'inbox'))
507
			{
508
				throw new Exception('no_access', false);
509
			}
510
511
			$context['current_pm'] = $pmID;
512
513
			// With only one page of PM's we're gonna want page 1.
514
			if ($max_messages <= $modSettings['defaultMaxMessages'])
515
			{
516
				$start = 0;
517
			}
518
			// If we pass kstart we assume we're in the right place.
519
			elseif (!$this->_req->isSet('kstart'))
520
			{
521
				$start = getPMCount($descending, $pmID, $labelQuery);
522
523
				// To stop the page index's being abnormal, start the page on the page the message
524
				// would normally be located on...
525
				$start = $modSettings['defaultMaxMessages'] * (int) ($start / $modSettings['defaultMaxMessages']);
526
			}
527
		}
528
529
		// Sanitize and validate pmsg variable if set.
530
		if ($this->_req->isSet('pmsg'))
531
		{
532
			$pmsg = $this->_req->getQuery('pmsg', 'intval', 0);
533
534
			if (!isAccessiblePM($pmsg, $context['folder'] === 'sent' ? 'outbox' : 'inbox'))
535
			{
536
				throw new Exception('no_access', false);
537
			}
538
		}
539
540
		// Determine the navigation context
541
		$context['links'] += [
542
			'prev' => $start >= $modSettings['defaultMaxMessages'] ? $scripturl . '?action=pm;start=' . ($start - $modSettings['defaultMaxMessages']) : '',
543
			'next' => $start + $modSettings['defaultMaxMessages'] < $max_messages ? $scripturl . '?action=pm;start=' . ($start + $modSettings['defaultMaxMessages']) : '',
544
		];
545
546
		// We now know what they want, so let's fetch those PM's
547
		[$pms, $posters, $recipients, $lastData] = loadPMs([
548
			'sort_by_query' => $sort_by_query,
549
			'display_mode' => $context['display_mode'],
550
			'sort_by' => $sort_by,
551
			'label_query' => $labelQuery,
552
			'pmsg' => isset($pmsg) ? (int) $pmsg : 0,
553
			'descending' => $descending,
554
			'start' => $start,
555
			'limit' => $modSettings['defaultMaxMessages'],
556
			'folder' => $context['folder'],
557
			'pmid' => $pmID ?? 0,
558
		], $this->user->id);
559
560
		// Make sure that we have been given a correct head pm id if we are in conversation mode
561
		if ($context['display_mode'] === PmHelper::DISPLAY_AS_CONVERSATION && !empty($pmID) && $pmID != $lastData['id'])
562
		{
563
			throw new Exception('no_access', false);
564
		}
565
566
		// If loadPMs returned results, let's show the pm subject list
567
		if (!empty($pms))
568
		{
569
			// Tell the template if no pm has specifically been selected
570
			if (empty($pmID))
571
			{
572
				$context['current_pm'] = 0;
573
			}
574
575
			$display_pms = $context['display_mode'] === PmHelper::DISPLAY_ALL_AT_ONCE ? $pms : [$lastData['id']];
576
577
			// At this point we know the main id_pm's. But if we are looking at conversations, we need
578
			// the PMs that make up the conversation
579
			if ($context['display_mode'] === PmHelper::DISPLAY_AS_CONVERSATION)
580
			{
581
				[$display_pms, $posters] = loadConversationList($lastData['head'], $recipients, $context['folder']);
582
583
				// Conversation list may expose additional PM's being displayed
584
				$all_pms = array_unique(array_merge($pms, $display_pms));
585
586
				// See if any of these 'listing' PMs are in a conversation thread that has unread entries
587
				$context['conversation_unread'] = loadConversationUnreadStatus($all_pms);
588
			}
589
			// This is pretty much EVERY pm!
590
			else
591
			{
592
				$all_pms = array_unique(array_merge($pms, $display_pms));
593
			}
594
595
			// Get recipients (don't include bcc-recipients for your inbox, you're not supposed to know :P).
596
			[$context['message_labels'], $context['message_replied'], $context['message_unread']] = loadPMRecipientInfo($all_pms, $recipients, $context['folder']);
597
598
			// Make sure we don't load any unnecessary data for one at a time mode
599
			if ($context['display_mode'] === PmHelper::DISPLAY_ONE_AT_TIME)
600
			{
601
				foreach ($posters as $pm_key => $sender)
602
				{
603
					if (!in_array($pm_key, $display_pms))
604
					{
605
						unset($posters[$pm_key]);
606
					}
607
				}
608
			}
609
610
			// Load some information about the message sender
611
			$posters = array_unique($posters);
612
			if (!empty($posters))
613
			{
614
				MembersList::load($posters);
615
			}
616
617
			// Always build the subject list request when we have PMs, so the subject list
618
			// renders in all modes (0: all-at-once, 1: one-at-a-time, 2: conversation).
619
			// This also prevents sharing a DB result between subject and message renderers.
620
			// Get the order right.
621
			$orderBy = [];
622
			foreach (array_reverse($pms) as $pm)
623
			{
624
				$orderBy[] = 'pm.id_pm = ' . $pm;
625
			}
626
627
			// Separate query for these bits, the callback will use it as required
628
			$subjects_request = loadPMSubjectRequest($pms, $orderBy);
629
630
			// Execute the load message query if a message has been chosen and let
631
			// the callback fetch the results.  Otherwise, just show the pm selection list
632
			if (empty($pmsg) && empty($pmID) && $context['display_mode'] !== PmHelper::DISPLAY_ALL_AT_ONCE)
633
			{
634
				$messages_request = false;
635
			}
636
			else
637
			{
638
				$messages_request = loadPMMessageRequest($display_pms, $sort_by_query, $sort_by, $descending, $context['display_mode'], $context['folder']);
639
			}
640
		}
641
		else
642
		{
643
			$messages_request = false;
644
		}
645
646
		// Initialize the subject and message render callbacks
647
		$bodyParser = new Normal([], false);
648
		$opt = new ValuesContainer(['recipients' => $recipients]);
649
		$renderer = new PmRenderer($messages_request, $this->user, $bodyParser, $opt);
650
		$subject_renderer = new PmRenderer($subjects_request ?? $messages_request, $this->user, $bodyParser, $opt);
651
652
		// Subject and Message
653
		$context['get_pmessage'] = [$renderer, 'getContext'];
654
		$context['get_psubject'] = [$subject_renderer, 'getContext'];
655
656
		// Prepare some items for the template
657
		$context['topic_starter_id'] = 0;
658
		$context['can_send_pm'] = allowedTo('pm_send');
659
		$context['can_send_email'] = allowedTo('send_email_to_members');
660
		$context['sub_template'] = 'folder';
661
		$context['page_title'] = $txt['pm_inbox'];
662
		$context['sort_direction'] = $descending ? 'down' : 'up';
663
		$context['sort_by'] = $sort_by;
664
665
		if ($messages_request !== false && !empty($context['show_delete']) && $messages_request->hasResults())
666
		{
667
			theme()->getLayers()->addEnd('pm_pages_and_buttons');
668
		}
669
670
		// Set up the page index.
671
		$label_for_index = $this->_req->getQuery('l', 'intval');
672
		$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']);
673
		$context['start'] = $start;
674
675
		$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'] : '');
676
677
		// Finally, mark the relevant messages as read.
678
		if ($context['folder'] !== 'sent' && !empty($context['labels'][(int) $context['current_label_id']]['unread_messages']))
679
		{
680
			// If the display mode is "old sk00l" do them all...
681
			if ($context['display_mode'] === PmHelper::DISPLAY_ALL_AT_ONCE)
682
			{
683
				markMessages(null, $context['current_label_id']);
684
			}
685
			// Otherwise do just the currently displayed ones!
686
			elseif (!empty($context['current_pm']))
687
			{
688
				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...
689
			}
690
		}
691
692
		// Build the conversation button array.
693
		if ($context['display_mode'] === PmHelper::DISPLAY_AS_CONVERSATION && !empty($context['current_pm']))
694
		{
695
			$context['conversation_buttons'] = [
696
				'delete' => [
697
					'text' => 'delete_conversation',
698
					'lang' => true,
699
					'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'],
700
					'custom' => 'onclick="return confirm(\'' . addslashes($txt['remove_message']) . '?\');"'
701
				],
702
			];
703
704
			// Allow mods to add additional buttons here
705
			call_integration_hook('integrate_conversation_buttons');
706
		}
707
	}
708
709
	/**
710
	 * Send a new personal message?
711
	 *
712
	 * @throws Exception pm_not_yours
713
	 */
714
	public function action_send(): void
715
	{
716
		global $txt, $modSettings, $context;
717
718
		// Load in some text and template dependencies
719
		Txt::load('PersonalMessage');
720
		theme()->getTemplates()->load('PersonalMessage');
721
722
		// Set the template we will use
723
		$context['sub_template'] = 'send';
724
725
		// Extract out the spam settings - cause it's neat.
726
		[$modSettings['max_pm_recipients'], $modSettings['pm_posts_verification'], $modSettings['pm_posts_per_hour']] = explode(',', $modSettings['pm_spam_settings']);
727
728
		// Set up some items for the template
729
		$context['page_title'] = $txt['send_message'];
730
		$context['reply'] = $this->_req->hasQuery('pmsg') || $this->_req->hasQuery('quote');
731
732
		// Check whether we've gone over the limit of messages we can send per hour.
733
		if (!empty($modSettings['pm_posts_per_hour'])
734
			&& !allowedTo(['admin_forum', 'moderate_forum', 'send_mail'])
735
			&& $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...
736
			&& $this->user->mod_cache['gq'] === '0=1')
737
		{
738
			// How many messages did they send this last hour?
739
			$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...
740
741
			if (!empty($pmCount) && $pmCount >= $modSettings['pm_posts_per_hour'])
742
			{
743
				throw new Exception('pm_too_many_per_hour', true, [$modSettings['pm_posts_per_hour']]);
744
			}
745
		}
746
747
		try
748
		{
749
			$pmsg_event = $this->_req->getQuery('pmsg', 'intval');
750
			$pmsg_event_quote = $this->_req->getQuery('quote', 'trim', '');
751
			if ($pmsg_event !== null)
752
			{
753
				$this->_events->trigger('before_set_context', ['pmsg' => $pmsg_event, 'quote' => $pmsg_event_quote]);
754
			}
755
		}
756
		catch (PmErrorException $pmErrorException)
757
		{
758
			$this->messagePostError($pmErrorException->namedRecipientList, $pmErrorException->recipientList, $pmErrorException->msgOptions);
759
			return;
760
		}
761
762
		// Quoting / Replying to a message?
763
		if ($this->_req->hasQuery('pmsg'))
764
		{
765
			$pmsg = $this->_req->getQuery('pmsg', 'intval');
766
767
			// Make sure this is accessible (not deleted)
768
			if (!isAccessiblePM($pmsg))
769
			{
770
				throw new Exception('no_access', false);
771
			}
772
773
			// Validate that this is one has been received?
774
			$isReceived = checkPMReceived($pmsg);
775
776
			// Get the quoted message (and make sure you're allowed to see this quote!).
777
			$row_quoted = loadPMQuote($pmsg, $isReceived);
778
			if ($row_quoted === false)
0 ignored issues
show
introduced by
The condition $row_quoted === false is always false.
Loading history...
779
			{
780
				throw new Exception('pm_not_yours', false);
781
			}
782
783
			// Censor the message.
784
			$row_quoted['subject'] = censor($row_quoted['subject']);
785
			$row_quoted['body'] = censor($row_quoted['body']);
786
787
			// Let's make sure we mark this one as read
788
			markMessages($pmsg);
789
790
			// Figure out which flavor or 'Re: ' to use
791
			$context['response_prefix'] = response_prefix();
792
793
			$form_subject = $row_quoted['subject'];
794
795
			// Add 'Re: ' to it....
796
			if ($context['reply'] && trim($context['response_prefix']) !== '' && Util::strpos($form_subject, trim($context['response_prefix'])) !== 0)
797
			{
798
				$form_subject = $context['response_prefix'] . $form_subject;
799
			}
800
801
			// If quoting, let's clean up some things and set the quote header for the pm body
802
			if ($this->_req->hasQuery('quote'))
803
			{
804
				// Remove any nested quotes and <br />...
805
				$form_message = preg_replace('~<br ?/?>~i', "\n", $row_quoted['body']);
806
				$form_message = removeNestedQuotes($form_message);
807
808
				if (empty($row_quoted['id_member']))
809
				{
810
					$form_message = '[quote author=&quot;' . $row_quoted['real_name'] . '&quot;]' . "\n" . $form_message . "\n" . '[/quote]';
811
				}
812
				else
813
				{
814
					$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]';
815
				}
816
			}
817
			else
818
			{
819
				$form_message = '';
820
			}
821
822
			// Allow them to QQ the message they are replying to
823
			loadJavascriptFile('quickQuote.js', ['defer' => true]);
824
			theme()->addInlineJavascript("
825
				document.addEventListener('DOMContentLoaded', () => new Elk_QuickQuote(), false);", true
826
			);
827
828
			// Do the BBC thang on the message.
829
			$bbc_parser = ParserWrapper::instance();
830
			$row_quoted['body'] = $bbc_parser->parsePM($row_quoted['body']);
831
832
			// Set up the quoted message array.
833
			$context['quoted_message'] = [
834
				'id' => $row_quoted['id_pm'],
835
				'pm_head' => $row_quoted['pm_head'],
836
				'member' => [
837
					'name' => $row_quoted['real_name'],
838
					'username' => $row_quoted['member_name'],
839
					'id' => $row_quoted['id_member'],
840
					'href' => empty($row_quoted['id_member']) ? '' : getUrl('profile', ['action' => 'profile', 'u' => $row_quoted['id_member']]),
841
					'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>',
842
				],
843
				'subject' => $row_quoted['subject'],
844
				'time' => standardTime($row_quoted['msgtime']),
845
				'html_time' => htmlTime($row_quoted['msgtime']),
846
				'timestamp' => forum_time(true, $row_quoted['msgtime']),
847
				'body' => $row_quoted['body']
848
			];
849
		}
850
		// A new message it is then
851
		else
852
		{
853
			$context['quoted_message'] = false;
854
			$form_subject = '';
855
			$form_message = '';
856
		}
857
858
		// Start of like we don't know where this is going
859
		$context['recipients'] = [
860
			'to' => [],
861
			'bcc' => [],
862
		];
863
864
		// Sending by ID?  Replying to all?  Fetch the real_name(s).
865
		if ($this->_req->hasQuery('u'))
866
		{
867
			// If the user is replying to all, get all the other members this was sent to.
868
			$u_param = $this->_req->getQuery('u', 'trim|strval', '');
869
			if ($u_param === 'all' && isset($row_quoted))
870
			{
871
				// Firstly, to reply to all, we clearly already have $row_quoted - so have the original member from.
872
				if ($row_quoted['id_member'] != $this->user->id)
873
				{
874
					$context['recipients']['to'][] = [
875
						'id' => $row_quoted['id_member'],
876
						'name' => htmlspecialchars($row_quoted['real_name'], ENT_COMPAT),
877
					];
878
				}
879
880
				// Now to get all the others.
881
				$context['recipients']['to'] = array_merge($context['recipients']['to'], isset($pmsg) ? loadPMRecipientsAll($pmsg) : []);
882
			}
883
			else
884
			{
885
				$users_csv = $u_param;
886
				$users = $users_csv === '' ? [] : array_map('intval', explode(',', $users_csv));
887
				$users = array_unique($users);
888
889
				// For all the member's this is going to get their display name.
890
				require_once(SUBSDIR . '/Members.subs.php');
891
				$result = getBasicMemberData($users);
892
893
				foreach ($result as $row)
894
				{
895
					$context['recipients']['to'][] = [
896
						'id' => $row['id_member'],
897
						'name' => $row['real_name'],
898
					];
899
				}
900
			}
901
902
			// Get a literal name list in case the user has JavaScript disabled.
903
			$names = [];
904
			foreach ($context['recipients']['to'] as $to)
905
			{
906
				$names[] = $to['name'];
907
			}
908
909
			$context['to_value'] = empty($names) ? '' : '&quot;' . implode('&quot;, &quot;', $names) . '&quot;';
910
		}
911
		else
912
		{
913
			$context['to_value'] = '';
914
		}
915
916
		// Set the defaults...
917
		$context['subject'] = $form_subject;
918
		$context['message'] = str_replace(['"', '<', '>', '&nbsp;'], ['&quot;', '&lt;', '&gt;', ' '], $form_message);
919
920
		// And build the link tree.
921
		$context['breadcrumbs'][] = [
922
			'url' => getUrl('action', ['action' => 'pm', 'sa' => 'send']),
923
			'name' => $txt['new_message']
924
		];
925
926
		// Needed for the editor.
927
		require_once(SUBSDIR . '/Editor.subs.php');
928
929
		// Now create the editor.
930
		$editorOptions = [
931
			'id' => 'message',
932
			'value' => $context['message'],
933
			'height' => '250px',
934
			'width' => '100%',
935
			'labels' => [
936
				'post_button' => $txt['send_message'],
937
			],
938
			'smiley_container' => 'smileyBox_message',
939
			'bbc_container' => 'bbcBox_message',
940
			'preview_type' => 2,
941
		];
942
943
		// Trigger the prepare_send_context PM event
944
		$this->_events->trigger('prepare_send_context', ['editorOptions' => &$editorOptions]);
945
946
		create_control_richedit($editorOptions);
947
948
		// No one is bcc'ed just yet
949
		$context['bcc_value'] = '';
950
951
		// Register this form and get a sequence number in $context.
952
		checkSubmitOnce('register');
953
	}
954
955
	/**
956
	 * An error in the message...
957
	 *
958
	 * @param array $named_recipients
959
	 * @param array $recipient_ids array keys of [bbc] => int[] and [to] => int[]
960
	 * @param object $msg_options body, subject, and reply values
961
	 *
962
	 * @throws Exception pm_not_yours
963
	 */
964
	public function messagePostError($named_recipients, $recipient_ids = [], $msg_options = null): void
965
	{
966
		global $txt, $context, $modSettings;
967
968
		if ($this->getApi() !== false)
969
		{
970
			$context['sub_template'] = 'generic_preview';
971
		}
972
		else
973
		{
974
			$context['sub_template'] = 'send';
975
			$context['menu_data_' . $context['pm_menu_id']]['current_area'] = 'send';
976
		}
977
978
		$context['page_title'] = $txt['send_message'];
979
		$error_types = ErrorContext::context('pm', 1);
980
981
		// Got some known members?
982
		$context['recipients'] = [
983
			'to' => [],
984
			'bcc' => [],
985
		];
986
987
		if (!empty($recipient_ids['to']) || !empty($recipient_ids['bcc']))
988
		{
989
			$allRecipients = array_merge($recipient_ids['to'], $recipient_ids['bcc']);
990
991
			require_once(SUBSDIR . '/Members.subs.php');
992
993
			// Get the latest activated member's display name.
994
			$result = getBasicMemberData($allRecipients);
995
			foreach ($result as $row)
996
			{
997
				$recipientType = in_array($row['id_member'], $recipient_ids['bcc']) ? 'bcc' : 'to';
998
				$context['recipients'][$recipientType][] = [
999
					'id' => $row['id_member'],
1000
					'name' => $row['real_name'],
1001
				];
1002
			}
1003
		}
1004
1005
		// Set everything up like before...
1006
		if (!empty($msg_options))
1007
		{
1008
			$context['subject'] = $msg_options->subject;
1009
			$context['message'] = $msg_options->body;
1010
			$context['reply'] = $msg_options->reply_to;
1011
		}
1012
		else
1013
		{
1014
			$subject_in = $this->_req->getPost('subject', 'trim|Util::htmlspecialchars', '');
1015
			$message_in = $this->_req->getPost('message', 'trim|strval|cleanhtml', '');
1016
			$context['subject'] = $subject_in;
1017
			$context['message'] = str_replace(['  '], ['&nbsp; '], $message_in);
1018
			$context['reply'] = $this->_req->getPost('replied_to', 'intval', 0) > 0;
1019
		}
1020
1021
		// If this is a reply to a message, we need to reload the quote
1022
		if ($context['reply'])
1023
		{
1024
			$pmsg = $this->_req->getPost('replied_to', 'intval', 0);
1025
			$isReceived = $context['folder'] !== 'sent';
1026
			$row_quoted = loadPMQuote($pmsg, $isReceived);
1027
			if ($row_quoted === false)
0 ignored issues
show
introduced by
The condition $row_quoted === false is always false.
Loading history...
1028
			{
1029
				if ($this->getApi() === false)
1030
				{
1031
					throw new Exception('pm_not_yours', false);
1032
				}
1033
1034
				$error_types->addError('pm_not_yours');
1035
			}
1036
			else
1037
			{
1038
				$row_quoted['subject'] = censor($row_quoted['subject']);
1039
				$row_quoted['body'] = censor($row_quoted['body']);
1040
				$bbc_parser = ParserWrapper::instance();
1041
1042
				$context['quoted_message'] = [
1043
					'id' => $row_quoted['id_pm'],
1044
					'pm_head' => $row_quoted['pm_head'],
1045
					'member' => [
1046
						'name' => $row_quoted['real_name'],
1047
						'username' => $row_quoted['member_name'],
1048
						'id' => $row_quoted['id_member'],
1049
						'href' => empty($row_quoted['id_member']) ? '' : getUrl('profile', ['action' => 'profile', 'u' => $row_quoted['id_member']]),
1050
						'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>',
1051
					],
1052
					'subject' => $row_quoted['subject'],
1053
					'time' => standardTime($row_quoted['msgtime']),
1054
					'html_time' => htmlTime($row_quoted['msgtime']),
1055
					'timestamp' => forum_time(true, $row_quoted['msgtime']),
1056
					'body' => $bbc_parser->parsePM($row_quoted['body']),
1057
				];
1058
			}
1059
		}
1060
1061
		// Build the link tree...
1062
		$context['breadcrumbs'][] = [
1063
			'url' => getUrl('action', ['action' => 'pm', 'sa' => 'send']),
1064
			'name' => $txt['new_message']
1065
		];
1066
1067
		// Set each of the errors for the template.
1068
		$context['post_error'] = [
1069
			'errors' => $error_types->prepareErrors(),
1070
			'type' => $error_types->getErrorType() == 0 ? 'minor' : 'serious',
1071
			'title' => $txt['error_while_submitting'],
1072
		];
1073
1074
		// We need to load the editor once more.
1075
		require_once(SUBSDIR . '/Editor.subs.php');
1076
1077
		// Create it...
1078
		$editorOptions = [
1079
			'id' => 'message',
1080
			'value' => $context['message'],
1081
			'width' => '100%',
1082
			'height' => '250px',
1083
			'labels' => [
1084
				'post_button' => $txt['send_message'],
1085
			],
1086
			'smiley_container' => 'smileyBox_message',
1087
			'bbc_container' => 'bbcBox_message',
1088
			'preview_type' => 2,
1089
		];
1090
1091
		// Trigger the prepare_send_context PM event
1092
		$this->_events->trigger('prepare_send_context', ['editorOptions' => &$editorOptions]);
1093
1094
		create_control_richedit($editorOptions);
1095
1096
		// Check whether we need to show the code again.
1097
		$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...
1098
		if ($context['require_verification'] && $this->getApi() === false)
1099
		{
1100
			$verificationOptions = [
1101
				'id' => 'pm',
1102
			];
1103
			$context['require_verification'] = VerificationControlsIntegrate::create($verificationOptions);
1104
			$context['visual_verification_id'] = $verificationOptions['id'];
1105
		}
1106
1107
		$context['to_value'] = empty($named_recipients['to']) ? '' : '&quot;' . implode('&quot;, &quot;', $named_recipients['to']) . '&quot;';
1108
		$context['bcc_value'] = empty($named_recipients['bcc']) ? '' : '&quot;' . implode('&quot;, &quot;', $named_recipients['bcc']) . '&quot;';
1109
1110
		// No check for the previous submission is needed.
1111
		checkSubmitOnce('free');
1112
1113
		// Acquire a new form sequence number.
1114
		checkSubmitOnce('register');
1115
	}
1116
1117
	/**
1118
	 * Send a personal message.
1119
 	 */
1120
	public function action_send2()
1121
	{
1122
		global $txt, $context, $modSettings;
1123
1124
		// All the helpers we need
1125
		require_once(SUBSDIR . '/Auth.subs.php');
1126
		require_once(SUBSDIR . '/Post.subs.php');
1127
1128
		Txt::load('PersonalMessage', false);
1129
1130
		// Extract out the spam settings - it saves database space!
1131
		[$modSettings['max_pm_recipients'], $modSettings['pm_posts_verification'], $modSettings['pm_posts_per_hour']] = explode(',', $modSettings['pm_spam_settings']);
1132
1133
		// Initialize the errors we're about to make.
1134
		$post_errors = ErrorContext::context('pm', 1);
1135
1136
		// Check whether we've gone over the limit of messages we can send per hour - fatal error if fails!
1137
		if (!empty($modSettings['pm_posts_per_hour'])
1138
			&& !allowedTo(['admin_forum', 'moderate_forum', 'send_mail'])
1139
			&& $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...
1140
			&& $this->user->mod_cache['gq'] === '0=1')
1141
		{
1142
			// How many they sent this last hour?
1143
			$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...
1144
1145
			if (!empty($pmCount) && $pmCount >= $modSettings['pm_posts_per_hour'])
1146
			{
1147
				if ($this->getApi() === false)
1148
				{
1149
					throw new Exception('pm_too_many_per_hour', true, [$modSettings['pm_posts_per_hour']]);
1150
				}
1151
1152
				$post_errors->addError('pm_too_many_per_hour');
1153
			}
1154
		}
1155
1156
		// If your session timed out, show an error, but do allow to re-submit.
1157
		if ($this->getApi() === false && checkSession('post', '', false) !== '')
1158
		{
1159
			$post_errors->addError('session_timeout');
1160
		}
1161
1162
		// Local sanitized copies used for preview/sending
1163
		$subject = $this->_req->getPost('subject', 'trim|strval', '');
1164
		$subject = strtr(Util::htmltrim($subject), ["\r" => '', "\n" => '', "\t" => '']);
1165
		$message = $this->_req->getPost('message', 'trim|strval', '');
1166
1167
		$this->_req->post->to = $this->_req->getPost('to', 'trim', empty($this->_req->query->to) ? '' : $this->_req->query->to);
1168
		$this->_req->post->bcc = $this->_req->getPost('bcc', 'trim', empty($this->_req->query->bcc) ? '' : $this->_req->query->bcc);
1169
1170
		// Route the input from the 'u' parameter to the 'to'-list.
1171
		if (!empty($this->_req->post->u))
1172
		{
1173
			$this->_req->post->recipient_to = explode(',', $this->_req->post->u);
1174
		}
1175
1176
		$bbc_parser = ParserWrapper::instance();
1177
1178
		// Construct the list of recipients.
1179
		$recipientList = [];
1180
		$namedRecipientList = [];
1181
		$namesNotFound = [];
1182
		foreach (['to', 'bcc'] as $recipientType)
1183
		{
1184
			// First, let's see if there's user ID's given.
1185
			$recipientList[$recipientType] = [];
1186
			$type = 'recipient_' . $recipientType;
1187
			if (!empty($this->_req->post->{$type}) && is_array($this->_req->post->{$type}))
1188
			{
1189
				$recipientList[$recipientType] = array_map('intval', $this->_req->post->{$type});
1190
			}
1191
1192
			// Are there also literal names set?
1193
			if (!empty($this->_req->post->{$recipientType}))
1194
			{
1195
				// We're going to take out the "s anyway ;).
1196
				$recipientString = strtr($this->_req->post->{$recipientType}, ['\\"' => '"']);
1197
1198
				preg_match_all('~"([^"]+)"~', $recipientString, $matches);
1199
				$namedRecipientList[$recipientType] = array_unique(array_merge($matches[1], explode(',', preg_replace('~"[^"]+"~', '', $recipientString))));
1200
1201
				// Clean any literal names entered
1202
				foreach ($namedRecipientList[$recipientType] as $index => $recipient)
1203
				{
1204
					if (trim($recipient) !== '')
1205
					{
1206
						$namedRecipientList[$recipientType][$index] = Util::htmlspecialchars(Util::strtolower(trim($recipient)));
1207
					}
1208
					else
1209
					{
1210
						unset($namedRecipientList[$recipientType][$index]);
1211
					}
1212
				}
1213
1214
				// Now see if we can resolve any entered name (not suggest selected) to an actual user
1215
				if (!empty($namedRecipientList[$recipientType]))
1216
				{
1217
					$foundMembers = findMembers($namedRecipientList[$recipientType]);
1218
1219
					// Assume all are not found until proven otherwise.
1220
					$namesNotFound[$recipientType] = $namedRecipientList[$recipientType];
1221
1222
					// Make sure we only have each member listed once, in case they did not use the select list
1223
					foreach ($foundMembers as $member)
1224
					{
1225
						$testNames = [
1226
							Util::strtolower($member['username']),
1227
							Util::strtolower($member['name']),
1228
							Util::strtolower($member['email']),
1229
						];
1230
1231
						if (array_intersect($testNames, $namedRecipientList[$recipientType]) !== [])
1232
						{
1233
							$recipientList[$recipientType][] = $member['id'];
1234
1235
							// Get rid of this username, since we found it.
1236
							$namesNotFound[$recipientType] = array_diff($namesNotFound[$recipientType], $testNames);
1237
						}
1238
					}
1239
				}
1240
			}
1241
1242
			// Selected a recipient to be deleted? Remove them now.
1243
			$delete_recipient = $this->_req->getPost('delete_recipient', 'intval', 0);
1244
			if (!empty($delete_recipient))
1245
			{
1246
				$recipientList[$recipientType] = array_diff($recipientList[$recipientType], [$delete_recipient]);
1247
			}
1248
1249
			// Make sure we don't include the same name twice
1250
			$recipientList[$recipientType] = array_unique($recipientList[$recipientType]);
1251
		}
1252
1253
		// Are we changing the recipients somehow?
1254
		$is_recipient_change = $this->_req->hasPost('delete_recipient') || $this->_req->hasPost('to_submit') || $this->_req->hasPost('bcc_submit');
1255
1256
		// Check if there's at least one recipient.
1257
		if (empty($recipientList['to']) && empty($recipientList['bcc']))
1258
		{
1259
			$post_errors->addError('no_to');
1260
		}
1261
1262
		// Make sure that we remove the members who did get it from the screen.
1263
		if (!$is_recipient_change)
1264
		{
1265
			foreach (array_keys($recipientList) as $recipientType)
1266
			{
1267
				if (!empty($namesNotFound[$recipientType]))
1268
				{
1269
					$post_errors->addError('bad_' . $recipientType);
1270
1271
					// Since we already have a post error, remove the previous one.
1272
					$post_errors->removeError('no_to');
1273
1274
					foreach ($namesNotFound[$recipientType] as $name)
1275
					{
1276
						$context['send_log']['failed'][] = sprintf($txt['pm_error_user_not_found'], $name);
1277
					}
1278
				}
1279
			}
1280
		}
1281
1282
		// Did they make any mistakes like no subject or message?
1283
		if ($subject === '')
1284
		{
1285
			$post_errors->addError('no_subject');
1286
		}
1287
1288
		if ($message === '')
1289
		{
1290
			$post_errors->addError('no_message');
1291
		}
1292
		elseif (!empty($modSettings['max_messageLength']) && Util::strlen($message) > $modSettings['max_messageLength'])
1293
		{
1294
			$post_errors->addError('long_message');
1295
		}
1296
		else
1297
		{
1298
			// Preparse the message.
1299
			preparsecode($message);
1300
1301
			// Make sure there's still some content left without the tags.
1302
			if (Util::htmltrim(strip_tags($bbc_parser->parsePM(Util::htmlspecialchars($message, ENT_QUOTES)), '<img>')) === ''
1303
				&& (!allowedTo('admin_forum') || !str_contains($message, '[html]')))
1304
			{
1305
				$post_errors->addError('no_message');
1306
			}
1307
		}
1308
1309
		// If they made any errors, give them a chance to make amends.
1310
		if ($post_errors->hasErrors()
1311
			&& !$is_recipient_change
1312
			&& !$this->_req->isSet('preview')
1313
			&& $this->getApi() === false)
1314
		{
1315
			$this->messagePostError($namedRecipientList, $recipientList);
1316
1317
			return false;
1318
		}
1319
1320
		// Want to take a second glance before you send?
1321
		if ($this->_req->isSet('preview'))
1322
		{
1323
			// Set everything up to be displayed.
1324
			$context['preview_subject'] = Util::htmlspecialchars($this->_req->getPost('subject', 'trim|strval', ''));
1325
			$context['preview_message'] = Util::htmlspecialchars($this->_req->getPost('message', 'trim|strval',''),ENT_QUOTES, 'UTF-8', true);
1326
			preparsecode($context['preview_message'], true);
1327
1328
			// Parse out the BBC if it is enabled.
1329
			$context['preview_message'] = $bbc_parser->parsePM($context['preview_message']);
1330
1331
			// Censor, as always.
1332
			$context['preview_subject'] = censor($context['preview_subject']);
1333
			$context['preview_message'] = censor($context['preview_message']);
1334
1335
			// Set a descriptive title.
1336
			$context['page_title'] = $txt['preview'] . ' - ' . $context['preview_subject'];
1337
1338
			// Pretend they messed up but don't ignore if they really did :P.
1339
			$this->messagePostError($namedRecipientList, $recipientList);
1340
1341
			return false;
1342
		}
1343
1344
		if ($is_recipient_change)
1345
		{
1346
			// Maybe we couldn't find one?
1347
			foreach ($namesNotFound as $recipientType => $names)
1348
			{
1349
				$post_errors->addError('bad_' . $recipientType);
1350
				foreach ($names as $name)
1351
				{
1352
					$context['send_log']['failed'][] = sprintf($txt['pm_error_user_not_found'], $name);
1353
				}
1354
			}
1355
1356
			$this->messagePostError($namedRecipientList, $recipientList);
1357
1358
			return true;
1359
		}
1360
1361
		// Adding a recipient cause JavaScript ain't working?
1362
		try
1363
		{
1364
			$this->_events->trigger('before_sending', ['namedRecipientList' => $namedRecipientList, 'recipientList' => $recipientList, 'namesNotFound' => $namesNotFound, 'post_errors' => $post_errors]);
1365
		}
1366
		catch (ControllerRedirectException)
1367
		{
1368
			$this->messagePostError($namedRecipientList, $recipientList);
1369
1370
			return true;
1371
		}
1372
1373
		// Safety net, it may be a module may just add to the list of errors without actually throw the error
1374
		if ($post_errors->hasErrors() && !$this->_req->isSet('preview') && $this->getApi() === false)
1375
		{
1376
			$this->messagePostError($namedRecipientList, $recipientList);
1377
1378
			return false;
1379
		}
1380
1381
		// Before we send the PM, let's make sure we don't have an abuse of numbers.
1382
		if (!empty($modSettings['max_pm_recipients']) && count($recipientList['to']) + count($recipientList['bcc']) > $modSettings['max_pm_recipients'] && !allowedTo(['moderate_forum', 'send_mail', 'admin_forum']))
1383
		{
1384
			$context['send_log'] = [
1385
				'sent' => [],
1386
				'failed' => [sprintf($txt['pm_too_many_recipients'], $modSettings['max_pm_recipients'])],
1387
			];
1388
1389
			$this->messagePostError($namedRecipientList, $recipientList);
1390
1391
			return false;
1392
		}
1393
1394
		// Protect from message spamming.
1395
		spamProtection('pm');
1396
1397
		// Prevent double submission of this form.
1398
		checkSubmitOnce('check');
1399
1400
		// Finally, do the actual sending of the PM.
1401
		if (!empty($recipientList['to']) || !empty($recipientList['bcc']))
1402
		{
1403
			// Reset the message to pre-check condition, sendpm will do the rest.
1404
			$subject = $this->_req->getPost('subject', 'trim|strval', '');
1405
			$message = $this->_req->getPost('message', 'trim|strval', '');
1406
			$context['send_log'] = sendpm($recipientList, $subject, $message, true, null, empty($this->_req->post->pm_head) ? 0 : (int) $this->_req->post->pm_head);
1407
		}
1408
		else
1409
		{
1410
			$context['send_log'] = [
1411
				'sent' => [],
1412
				'failed' => []
1413
			];
1414
		}
1415
1416
		// Mark the message as "replied to".
1417
		$replied_to = $this->_req->getPost('replied_to', 'intval', 0);
1418
		$box = $this->_req->getPost('f', 'trim', '');
1419
		if (!empty($context['send_log']['sent']) && !empty($replied_to) && $box === 'inbox')
1420
		{
1421
			require_once(SUBSDIR . '/PersonalMessage.subs.php');
1422
			setPMRepliedStatus($this->user->id, $replied_to);
1423
		}
1424
1425
		$failed = !empty($context['send_log']['failed']);
1426
		$this->_events->trigger('message_sent', ['failed' => $failed]);
1427
1428
		// If one or more of the recipients were invalid, go back to the post screen with the failed usernames.
1429
		if ($failed)
1430
		{
1431
			$this->messagePostError($namesNotFound, [
1432
				'to' => array_intersect($recipientList['to'], $context['send_log']['failed']),
1433
				'bcc' => array_intersect($recipientList['bcc'], $context['send_log']['failed'])
1434
			]);
1435
1436
			return false;
1437
		}
1438
1439
		// Message sent successfully
1440
		$context['current_label_redirect'] .= ';done=sent';
1441
1442
		// Go back to where they sent from, if possible...
1443
		redirectexit($context['current_label_redirect']);
1444
	}
1445
1446
	/**
1447
	 * This function performs all additional actions including the deleting
1448
	 * and labeling of PM's
1449
	 */
1450
	public function action_pmactions(): void
1451
	{
1452
		global $context;
1453
1454
		checkSession('request');
1455
1456
		// Sending in the single pm choice via GET
1457
		$pm_actions = $this->_req->getQuery('pm_actions', null, '');
1458
1459
		// Set the action to apply to the PMs defined by pm_actions (yes, it is that brilliant)
1460
		$pm_action = $this->_req->getPost('pm_action', 'trim', '');
1461
		$pm_action = empty($pm_action) && $this->_req->hasPost('del_selected') ? 'delete' : $pm_action;
1462
1463
		// Create a list of PMs that we need to work on
1464
		$pms_list = $this->_req->getPost('pms', null, []);
1465
		if ($pm_action !== ''
1466
			&& !empty($pms_list)
1467
			&& is_array($pms_list))
1468
		{
1469
			$pm_actions = [];
1470
			foreach ($pms_list as $pm)
1471
			{
1472
				$pm_actions[(int) $pm] = $pm_action;
1473
			}
1474
		}
1475
1476
		// No messages to action then bug out
1477
		if (empty($pm_actions))
1478
		{
1479
			redirectexit($context['current_label_redirect']);
1480
		}
1481
1482
		// If we are in conversation, we may need to apply this to every message in that conversation.
1483
		if ($context['display_mode'] === PmHelper::DISPLAY_AS_CONVERSATION && $this->_req->hasQuery('conversation'))
1484
		{
1485
			$id_pms = array_map('intval', array_keys($pm_actions));
1486
			$pm_heads = getDiscussions($id_pms);
1487
			$pms = getPmsFromDiscussion(array_keys($pm_heads));
1488
1489
			// Copy the action from the single to PM to the others in the conversation.
1490
			foreach ($pms as $id_pm => $id_head)
1491
			{
1492
				if (isset($pm_heads[$id_head], $pm_actions[$pm_heads[$id_head]]))
1493
				{
1494
					$pm_actions[$id_pm] = $pm_actions[$pm_heads[$id_head]];
1495
				}
1496
			}
1497
		}
1498
1499
		// Get to doing what we've been told
1500
		$to_delete = [];
1501
		$to_label = [];
1502
		$label_type = [];
1503
		foreach ($pm_actions as $pm => $action)
1504
		{
1505
			// What are we doing with the selected messages, adding a label, removing, other?
1506
			switch (substr($action, 0, 4))
1507
			{
1508
				case 'dele':
1509
					$to_delete[] = (int) $pm;
1510
					break;
1511
				case 'add_':
1512
					$type = 'add';
1513
					$action = substr($action, 4);
1514
					break;
1515
				case 'rem_':
1516
					$type = 'rem';
1517
					$action = substr($action, 4);
1518
					break;
1519
				default:
1520
					$type = 'unk';
1521
			}
1522
1523
			if ((int) $action === -1 || (int) $action === 0 || (int) $action > 0)
1524
			{
1525
				$to_label[(int) $pm] = (int) $action;
1526
				$label_type[(int) $pm] = $type ?? '';
1527
			}
1528
		}
1529
1530
		// Deleting, it looks like?
1531
		if (!empty($to_delete))
1532
		{
1533
			deleteMessages($to_delete, $context['display_mode'] === PmHelper::DISPLAY_AS_CONVERSATION ? null : $context['folder']);
1534
		}
1535
1536
		// Are we labeling anything?
1537
		if (!empty($to_label) && $context['folder'] === 'inbox')
1538
		{
1539
			$updateErrors = changePMLabels($to_label, $label_type, $this->user->id);
1540
1541
			// Any errors?
1542
			if (!empty($updateErrors))
1543
			{
1544
				throw new Exception('labels_too_many', false, [$updateErrors]);
1545
			}
1546
		}
1547
1548
		// Back to the folder.
1549
		$_SESSION['pm_selected'] = array_keys($to_label);
1550
		redirectexit($context['current_label_redirect'] . (count($to_label) === 1 ? '#msg_' . $_SESSION['pm_selected'][0] : ''));
1551
	}
1552
1553
	/**
1554
	 * Are you sure you want to PERMANENTLY (mostly) delete ALL your messages?
1555
	 */
1556
	public function action_removeall(): void
1557
	{
1558
		global $txt, $context;
1559
1560
		// Only have to set up the template...
1561
		$context['sub_template'] = 'ask_delete';
1562
		$context['page_title'] = $txt['delete_all'];
1563
		$folder_flag = $this->_req->getQuery('f', 'trim|strval', '');
1564
		$context['delete_all'] = $folder_flag === 'all';
1565
1566
		// And set the folder name...
1567
		$txt['delete_all'] = str_replace('PMBOX', $context['folder'] != 'sent' ? $txt['inbox'] : $txt['sent_items'], $txt['delete_all']);
1568
	}
1569
1570
	/**
1571
	 * Delete ALL the messages!
1572
	 */
1573
	public function action_removeall2(): void
1574
	{
1575
		global $context;
1576
1577
		checkSession('get');
1578
1579
		// If all, then delete all messages the user has.
1580
		$folder_flag = $this->_req->getQuery('f', 'trim|strval', '');
1581
		if ($folder_flag === 'all')
1582
		{
1583
			deleteMessages(null);
1584
		}
1585
		// Otherwise just the selected folder.
1586
		else
1587
		{
1588
			deleteMessages(null, $folder_flag !== 'sent' ? 'inbox' : 'sent');
1589
		}
1590
1591
		// Done... all gone.
1592
		redirectexit($context['current_label_redirect']);
1593
	}
1594
1595
	/**
1596
	 * This function allows the user to prune (delete) all messages older than a supplied duration.
1597
	 */
1598
	public function action_prune(): void
1599
	{
1600
		global $txt, $context;
1601
1602
		// Actually delete the messages.
1603
		if ($this->_req->hasPost('age'))
1604
		{
1605
			checkSession();
1606
1607
			// Calculate the time to delete before.
1608
			$age_days = $this->_req->getPost('age', 'intval', 0);
1609
			$deleteTime = max(0, time() - (86400 * $age_days));
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'][] = [
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(): void
1634
	{
1635
		$controller = new Labels($this->_events);
1636
		$controller->setUser($this->user);
1637
		$controller->action_manlabels();
1638
	}
1639
1640
	/**
1641
	 * Allows editing Personal Message Settings.
1642
	 *
1643
	 * @uses ProfileOptions controller. (@todo refactor this.)
1644
	 * @uses Profile template.
1645
	 * @uses Profile language file.
1646
	 */
1647
	public function action_settings(): void
1648
	{
1649
		$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...
1650
		$controller->setUser($this->user);
1651
		$controller->action_settings();
1652
	}
1653
1654
	/**
1655
	 * Allows the user to report a personal message to an administrator.
1656
	 *
1657
	 * What it does:
1658
	 *
1659
	 * - In the first instance requires that the ID of the message to report is passed through $_GET.
1660
	 * - It allows the user to report to either a particular administrator - or the whole admin team.
1661
	 * - It will forward on a copy of the original message without allowing the reporter to make changes.
1662
	 *
1663
	 * @uses report_message sub-template.
1664
	 */
1665
	public function action_report(): void
1666
	{
1667
		$controller = new Report($this->_events);
1668
		$controller->setUser($this->user);
1669
		$controller->action_report();
1670
	}
1671
1672
	/**
1673
	 * List and allow adding/entering all man rules
1674
	 *
1675
	 * @uses sub template rules
1676
	 */
1677
	public function action_manrules(): void
1678
	{
1679
		$controller = new Rules($this->_events);
1680
		$controller->setUser($this->user);
1681
		$controller->action_rules();
1682
	}
1683
1684
	/**
1685
	 * Actually do the search of personal messages and show the results
1686
	 *
1687
	 * What it does:
1688
	 *
1689
	 * - Accessed with ?action=pm;sa=search2
1690
	 * - Checks user input and searches the pm table for messages matching the query.
1691
	 * - Uses the search_results sub template of the PersonalMessage template.
1692
	 * - Show the results of the search query.
1693
	 */
1694
	public function action_search2(): ?bool
1695
	{
1696
		$controller = new Search($this->_events);
1697
		$controller->setUser($this->user);
1698
1699
		return $controller->action_search2();
1700
	}
1701
1702
1703
	/**
1704
	 * Allows searching personal messages.
1705
	 *
1706
	 * What it does:
1707
	 *
1708
	 * - Accessed with ?action=pm;sa=search
1709
	 * - Shows the screen to search PMs (?action=pm;sa=search)
1710
	 * - Uses the search sub template of the PersonalMessage template.
1711
	 * - Decodes and loads search parameters given in the URL (if any).
1712
	 * - The form redirects to index.php?action=pm;sa=search2.
1713
	 *
1714
	 * @uses search sub template
1715
	 */
1716
	public function action_search(): void
1717
	{
1718
		$controller = new Search($this->_events);
1719
		$controller->setUser($this->user);
1720
		$controller->action_search();
1721
	}
1722
1723
1724
	/**
1725
	 * Allows the user to mark a personal message as unread, so they remember to come back to it
1726
	 */
1727
	public function action_markunread(): void
1728
	{
1729
		global $context;
1730
1731
		checkSession('request');
1732
1733
		$pmsg = $this->_req->getQuery('pmsg', 'intval');
1734
1735
		// Marking a message as unread, we need a message that was sent to them
1736
		// Can't mark your own reply as unread, that would be weird
1737
		if (!is_null($pmsg) && checkPMReceived($pmsg))
1738
		{
1739
			// Make sure this is accessible, should be, of course
1740
			if (!isAccessiblePM($pmsg, 'inbox'))
1741
			{
1742
				throw new Exception('no_access', false);
1743
			}
1744
1745
			// Well then, you get to hear about it all over again
1746
			markMessagesUnread($pmsg);
1747
		}
1748
1749
		// Back to the folder.
1750
		redirectexit($context['current_label_redirect']);
1751
	}
1752
}
1753