Completed
Pull Request — development (#2330)
by Joshua
41:37 queued 30:33
created

PersonalMessage_Controller   F

Complexity

Total Complexity 481

Size/Duplication

Total Lines 2898
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 12

Test Coverage

Coverage 0%
Metric Value
wmc 481
lcom 1
cbo 12
dl 0
loc 2898
ccs 0
cts 1940
cp 0
rs 3.4839

27 Methods

Rating   Name   Duplication   Size   Complexity  
F pre_dispatch() 0 78 15
A _loadMessageLimit() 0 20 2
B _loadLabels() 0 24 3
B action_index() 0 41 4
C _messageIndexBar() 0 149 12
F action_folder() 0 318 64
F action_send() 0 217 31
F action_send2() 0 310 62
F messagePostError() 0 150 23
F action_pmactions() 0 97 27
A action_removeall() 0 12 2
A action_removeall2() 0 20 3
B action_prune() 0 30 2
D action_manlabels() 0 179 31
A action_settings() 0 60 3
F action_report() 0 125 18
F action_manrules() 0 238 47
F action_search2() 0 303 49
B _setSortParams() 0 14 7
C _setLabelQuery() 0 50 10
C _setUserQuery() 0 77 15
F _prepareSearchParams() 0 37 21
B _searchParamsFromString() 0 22 5
A _compileURLparams() 0 13 2
F action_search() 0 79 15
B action_markunread() 0 25 5
A _highlighted_callback() 0 4 3

How to fix   Complexity   

Complex Class

Complex classes like PersonalMessage_Controller often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use PersonalMessage_Controller, and based on these observations, apply Extract Interface, too.

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
 * @name      ElkArte Forum
9
 * @copyright ElkArte Forum contributors
10
 * @license   BSD http://opensource.org/licenses/BSD-3-Clause
11
 *
12
 * This software is a derived product, based on:
13
 *
14
 * Simple Machines Forum (SMF)
15
 * copyright:	2011 Simple Machines (http://www.simplemachines.org)
16
 * license:  	BSD, See included LICENSE.TXT for terms and conditions.
17
 *
18
 * @version 1.1 dev
19
 *
20
 */
21
22
if (!defined('ELK'))
23
{
24
	die('No access...');
25
}
26
27
/**
28
 * Personal Message Controller, It allows viewing, sending, deleting, and
29
 * marking personal messages
30
 *
31
 * @package PersonalMessage
32
 */
33
class PersonalMessage_Controller extends Action_Controller
34
{
35
	/**
36
	 * $_search_params will carry all settings that differ from the default
37
	 * search parameters.
38
	 * That way, the URLs involved in a search page will be kept as short
39
	 * as possible.
40
	 * @var array
41
	 */
42
	private $_search_params = array();
43
44
	/**
45
	 * $_searchq_parameters will carry all the values needed by S_search_params
46
	 * @var array
47
	 */
48
	private $_searchq_parameters = array();
49
50
	/**
51
	 * This method is executed before any other in this file (when the class is
52
	 * loaded by the dispatcher).
53
	 *
54
	 * What it does:
55
	 * - It sets the context, load 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, $scripturl, $context, $user_info, $user_settings, $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 the our PM functions such as mark, send, delete
69
		require_once(SUBSDIR . '/PersonalMessage.subs.php');
70
71
		// Templates, language, javascripts
72
		loadLanguage('PersonalMessage');
73
		loadJavascriptFile(array('PersonalMessage.js', 'suggest.js'));
74
75
		if (!isset($this->_req->query->xml))
76
		{
77
			loadTemplate('PersonalMessage');
78
		}
79
80
		$this->_events->trigger('pre_dispatch', array('xml' => isset($this->_req->query->xml)));
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
		// Now we have the labels, and assuming we have unsorted mail, apply our rules!
92
		if ($user_settings['new_pm'])
93
		{
94
			$this->_loadLabels();
95
96
			// Apply our rules to the new PM's
97
			applyRules();
98
99
			require_once(SUBSDIR . '/Members.subs.php');
100
			updateMemberData($user_info['id'], array('new_pm' => 0));
101
102
			// Turn the new PM's status off, for the popup alert, since they have entered the PM area
103
			toggleNewPM($user_info['id']);
104
		}
105
106
		// Load the label counts data.
107
		if ($user_settings['new_pm'] || ($context['labels'] = Cache::instance()->get('labelCounts:' . $user_info['id'], 720)) === null)
108
		{
109
			$this->_loadLabels();
110
111
			// Get the message count for each label
112
			$context['labels'] = loadPMLabels($context['labels']);
113
		}
114
115
		// This determines if we have more labels than just the standard inbox.
116
		$context['currently_using_labels'] = count($context['labels']) > 1 ? 1 : 0;
117
118
		// Some stuff for the labels...
119
		$context['current_label_id'] = isset($this->_req->query->l) && isset($context['labels'][(int) $this->_req->query->l]) ? (int) $this->_req->query->l : -1;
120
		$context['current_label'] = &$context['labels'][(int) $context['current_label_id']]['name'];
121
		$context['folder'] = !isset($this->_req->query->f) || $this->_req->query->f !== 'sent' ? 'inbox' : 'sent';
122
123
		// This is convenient.  Do you know how annoying it is to do this every time?!
124
		$context['current_label_redirect'] = 'action=pm;f=' . $context['folder'] . (isset($this->_req->query->start) ? ';start=' . $this->_req->query->start : '') . (isset($this->_req->query->l) ? ';l=' . $this->_req->query->l : '');
125
		$context['can_issue_warning'] = in_array('w', $context['admin_features']) && allowedTo('issue_warning') && !empty($modSettings['warning_enable']);
126
127
		// Build the linktree for all the actions...
128
		$context['linktree'][] = array(
129
			'url' => $scripturl . '?action=pm',
130
			'name' => $txt['personal_messages']
131
		);
132
133
		// Preferences...
134
		$context['display_mode'] = $user_settings['pm_prefs'] & 3;
135
	}
136
137
	/**
138
	 * Load a members message limit and prepares the limit bar
139
	 */
140
	private function _loadMessageLimit()
141
	{
142
		global $context, $txt, $user_info;
143
144
		$context['message_limit'] = loadMessageLimit();
145
146
		// Prepare the context for the capacity bar.
147
		if (!empty($context['message_limit']))
148
		{
149
			$bar = ($user_info['messages'] * 100) / $context['message_limit'];
150
151
			$context['limit_bar'] = array(
152
				'messages' => $user_info['messages'],
153
				'allowed' => $context['message_limit'],
154
				'percent' => $bar,
155
				'bar' => min(100, (int) $bar),
156
				'text' => sprintf($txt['pm_currently_using'], $user_info['messages'], round($bar, 1)),
157
			);
158
		}
159
	}
160
161
	/**
162
	 * Loads the user defined label's for use in the template etc.
163
	 */
164
	private function _loadLabels()
165
	{
166
		global $context, $txt, $user_settings;
167
168
		$context['labels'] = $user_settings['message_labels'] === '' ? array() : explode(',', $user_settings['message_labels']);
169
170
		foreach ($context['labels'] as $id_label => $label_name)
171
		{
172
			$context['labels'][(int) $id_label] = array(
173
				'id' => $id_label,
174
				'name' => trim($label_name),
175
				'messages' => 0,
176
				'unread_messages' => 0,
177
			);
178
		}
179
180
		// The default inbox is always available
181
		$context['labels'][-1] = array(
182
			'id' => -1,
183
			'name' => $txt['pm_msg_label_inbox'],
184
			'messages' => 0,
185
			'unread_messages' => 0,
186
		);
187
	}
188
189
	/**
190
	 * This is the main function of personal messages, called before the action handler.
191
	 *
192
	 * What it does:
193
	 * - PersonalMessages is a menu-based controller.
194
	 * - It sets up the menu.
195
	 * - Calls from the menu the appropriate method/function for the current area.
196
	 *
197
	 * @see Action_Controller::action_index()
198
	 */
199
	public function action_index()
200
	{
201
		global $context;
202
203
		// Finally all the things we know how to do
204
		$subActions = array(
205
			'manlabels' => array($this, 'action_manlabels', 'permission' => 'pm_read'),
206
			'manrules' => array($this, 'action_manrules', 'permission' => 'pm_read'),
207
			'markunread' => array($this, 'action_markunread', 'permission' => 'pm_read'),
208
			'pmactions' => array($this, 'action_pmactions', 'permission' => 'pm_read'),
209
			'prune' => array($this, 'action_prune', 'permission' => 'pm_read'),
210
			'removeall' => array($this, 'action_removeall', 'permission' => 'pm_read'),
211
			'removeall2' => array($this, 'action_removeall2', 'permission' => 'pm_read'),
212
			'report' => array($this, 'action_report', 'permission' => 'pm_read'),
213
			'search' => array($this, 'action_search', 'permission' => 'pm_read'),
214
			'search2' => array($this, 'action_search2', 'permission' => 'pm_read'),
215
			'send' => array($this, 'action_send', 'permission' => 'pm_read'),
216
			'send2' => array($this, 'action_send2', 'permission' => 'pm_read'),
217
			'settings' => array($this, 'action_settings', 'permission' => 'pm_read'),
218
			'inbox' => array($this, 'action_folder', 'permission' => 'pm_read'),
219
		);
220
221
		// Set up our action array
222
		$action = new Action('pm_index');
223
224
		// Known action, go to it, otherwise the inbox for you
225
		$subAction = $action->initialize($subActions, 'inbox');
226
227
		// Set the right index bar for the action
228
		if ($subAction === 'inbox')
229
		{
230
			$this->_messageIndexBar($context['current_label_id'] == -1 ? $context['folder'] : 'label' . $context['current_label_id']);
231
		}
232
		elseif (!isset($this->_req->query->xml))
233
		{
234
			$this->_messageIndexBar($subAction);
235
		}
236
237
		// And off we go!
238
		$action->dispatch($subAction);
239
	}
240
241
	/**
242
	 * A menu to easily access different areas of the PM section
243
	 *
244
	 * @param string $area
245
	 */
246
	private function _messageIndexBar($area)
247
	{
248
		global $txt, $context, $scripturl, $user_info;
249
250
		require_once(SUBSDIR . '/Menu.subs.php');
251
252
		$pm_areas = array(
253
			'folders' => array(
254
				'title' => $txt['pm_messages'],
255
				'counter' => 'unread_messages',
256
				'areas' => array(
257
					'inbox' => array(
258
						'label' => $txt['inbox'],
259
						'custom_url' => $scripturl . '?action=pm',
260
						'counter' => 'unread_messages',
261
					),
262
					'send' => array(
263
						'label' => $txt['new_message'],
264
						'custom_url' => $scripturl . '?action=pm;sa=send',
265
						'permission' => 'pm_send',
266
					),
267
					'sent' => array(
268
						'label' => $txt['sent_items'],
269
						'custom_url' => $scripturl . '?action=pm;f=sent',
270
					),
271
				),
272
			),
273
			'labels' => array(
274
				'title' => $txt['pm_labels'],
275
				'counter' => 'labels_unread_total',
276
				'areas' => array(),
277
			),
278
			'actions' => array(
279
				'title' => $txt['pm_actions'],
280
				'areas' => array(
281
					'search' => array(
282
						'label' => $txt['pm_search_bar_title'],
283
						'custom_url' => $scripturl . '?action=pm;sa=search',
284
					),
285
					'prune' => array(
286
						'label' => $txt['pm_prune'],
287
						'custom_url' => $scripturl . '?action=pm;sa=prune'
288
					),
289
				),
290
			),
291
			'pref' => array(
292
				'title' => $txt['pm_preferences'],
293
				'areas' => array(
294
					'manlabels' => array(
295
						'label' => $txt['pm_manage_labels'],
296
						'custom_url' => $scripturl . '?action=pm;sa=manlabels',
297
					),
298
					'manrules' => array(
299
						'label' => $txt['pm_manage_rules'],
300
						'custom_url' => $scripturl . '?action=pm;sa=manrules',
301
					),
302
					'settings' => array(
303
						'label' => $txt['pm_settings'],
304
						'custom_url' => $scripturl . '?action=pm;sa=settings',
305
					),
306
				),
307
			),
308
		);
309
310
		// Handle labels.
311
		$label_counters = array('unread_messages' => $context['labels'][-1]['unread_messages']);
312
		if (empty($context['currently_using_labels']))
313
		{
314
			unset($pm_areas['labels']);
315
		}
316
		else
317
		{
318
			// Note we send labels by id as it will have less problems in the querystring.
319
			$label_counters['labels_unread_total'] = 0;
320
			foreach ($context['labels'] as $label)
321
			{
322
				if ($label['id'] == -1)
323
				{
324
					continue;
325
				}
326
327
				// Count the amount of unread items in labels.
328
				$label_counters['labels_unread_total'] += $label['unread_messages'];
329
330
				// Add the label to the menu.
331
				$pm_areas['labels']['areas']['label' . $label['id']] = array(
332
					'label' => $label['name'],
333
					'custom_url' => $scripturl . '?action=pm;l=' . $label['id'],
334
					'counter' => 'label' . $label['id'],
335
					'messages' => $label['messages'],
336
				);
337
338
				$label_counters['label' . $label['id']] = $label['unread_messages'];
339
			}
340
		}
341
342
		// Do we have a limit on the amount of messages we can keep?
343
		if (!empty($context['message_limit']))
344
		{
345
			$bar = round(($user_info['messages'] * 100) / $context['message_limit'], 1);
346
347
			$context['limit_bar'] = array(
348
				'messages' => $user_info['messages'],
349
				'allowed' => $context['message_limit'],
350
				'percent' => $bar,
351
				'bar' => $bar > 100 ? 100 : (int) $bar,
352
				'text' => sprintf($txt['pm_currently_using'], $user_info['messages'], $bar)
353
			);
354
		}
355
356
		// Set a few options for the menu.
357
		$menuOptions = array(
358
			'current_area' => $area,
359
			'hook' => 'pm',
360
			'disable_url_session_check' => true,
361
			'counters' => !empty($label_counters) ? $label_counters : 0,
362
			'default_include_dir' => CONTROLLERDIR,
363
		);
364
365
		// Actually create the menu!
366
		$pm_include_data = createMenu($pm_areas, $menuOptions);
367
		unset($pm_areas);
368
369
		// No menu means no access.
370
		if (!$pm_include_data && (!$user_info['is_guest'] || validateSession() !== true))
371
		{
372
			Errors::instance()->fatal_lang_error('no_access', false);
373
		}
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
		// Grab the file needed for this action
383
		if (isset($pm_include_data['file']))
384
		{
385
			require_once($pm_include_data['file']);
386
		}
387
388
		// Set the template for this area and add the profile layer.
389
		if (!isset($this->_req->query->xml))
390
		{
391
			$template_layers = Template_Layers::getInstance();
392
			$template_layers->add('pm');
393
		}
394
	}
395
396
	/**
397
	 * Display a folder, ie. inbox/sent etc.
398
	 *
399
	 * @uses folder sub template
400
	 * @uses subject_list, pm template layers
401
	 */
402
	public function action_folder()
403
	{
404
		global $txt, $scripturl, $modSettings, $context, $subjects_request;
405
		global $messages_request, $user_info, $recipients, $options, $user_settings;
406
407
		// Changing view?
408
		if (isset($this->_req->query->view))
409
		{
410
			$context['display_mode'] = $context['display_mode'] > 1 ? 0 : $context['display_mode'] + 1;
411
			require_once(SUBSDIR . '/Members.subs.php');
412
			updateMemberData($user_info['id'], array('pm_prefs' => ($user_settings['pm_prefs'] & 252) | $context['display_mode']));
413
		}
414
415
		// Make sure the starting location is valid.
416
		if (isset($this->_req->query->start) && $this->_req->query->start !== 'new')
417
		{
418
			$start = (int) $this->_req->query->start;
419
		}
420
		elseif (!isset($this->_req->query->start) && !empty($options['view_newest_pm_first']))
421
		{
422
			$start = 0;
423
		}
424
		else
425
		{
426
			$start = 'new';
427
		}
428
429
		// Set up some basic template stuff.
430
		$context['from_or_to'] = $context['folder'] !== 'sent' ? 'from' : 'to';
431
		$context['get_pmessage'] = 'preparePMContext_callback';
432
		$context['signature_enabled'] = substr($modSettings['signature_settings'], 0, 1) == 1;
433
		$context['disabled_fields'] = isset($modSettings['disabled_profile_fields']) ? array_flip(explode(',', $modSettings['disabled_profile_fields'])) : array();
434
435
		// Set the template layers we need
436
		$template_layers = Template_Layers::getInstance();
437
		$template_layers->addAfter('subject_list', 'pm');
438
439
		$labelQuery = $context['folder'] != 'sent' ? '
440
				AND FIND_IN_SET(' . $context['current_label_id'] . ', pmr.labels) != 0' : '';
441
442
		// They didn't pick a sort, so we use the forum default.
443
		$sort_by = !isset($this->_req->query->sort) ? 'date' : $this->_req->query->sort;
444
		$descending = isset($this->_req->query->desc);
445
446
		// Set our sort by query
447
		switch ($sort_by)
448
		{
449
			case 'date':
450
				$sort_by_query = 'pm.id_pm';
451
				$descending = !empty($options['view_newest_pm_first']);
452
				break;
453
			case 'name':
454
				$sort_by_query = 'IFNULL(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['linktree'][] = array(
471
				'url' => $scripturl . '?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['linktree'][] = array(
480
				'url' => $scripturl . '?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 (isset($this->_req->query->pmid))
503
		{
504
			$pmID = (int) $this->_req->query->pmid;
505
506
			// Make sure you have access to this PM.
507
			if (!isAccessiblePM($pmID, $context['folder'] === 'sent' ? 'outbox' : 'inbox'))
508
			{
509
				Errors::instance()->fatal_lang_error('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 (!isset($this->_req->query->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 (isset($this->_req->query->pmsg))
532
		{
533
			$pmsg = (int) $this->_req->query->pmsg;
534
535
			if (!isAccessiblePM($pmsg, $context['folder'] === 'sent' ? 'outbox' : 'inbox'))
536
			{
537
				Errors::instance()->fatal_lang_error('no_access', false);
538
			}
539
		}
540
541
		// Determine the navigation context
542
		$context['links'] += array(
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
		$context['page_info'] = array(
547
			'current_page' => $start / $modSettings['defaultMaxMessages'] + 1,
548
			'num_pages' => floor(($max_messages - 1) / $modSettings['defaultMaxMessages']) + 1
549
		);
550
551
		// We now know what they want, so lets fetch those PM's
552
		list ($pms, $posters, $recipients, $lastData) = loadPMs(array(
553
			'sort_by_query' => $sort_by_query,
554
			'display_mode' => $context['display_mode'],
555
			'sort_by' => $sort_by,
556
			'label_query' => $labelQuery,
557
			'pmsg' => isset($pmsg) ? (int) $pmsg : 0,
558
			'descending' => $descending,
559
			'start' => $start,
560
			'limit' => $modSettings['defaultMaxMessages'],
561
			'folder' => $context['folder'],
562
			'pmid' => isset($pmID) ? $pmID : 0,
563
		), $user_info['id']);
564
565
		// Make sure that we have been given a correct head pm id if we are in conversation mode
566
		if ($context['display_mode'] == 2 && !empty($pmID) && $pmID != $lastData['id'])
567
		{
568
			Errors::instance()->fatal_lang_error('no_access', false);
569
		}
570
571
		// If loadPMs returned results, lets show the pm subject list
572
		if (!empty($pms))
573
		{
574
			// Tell the template if no pm has specifically been selected
575
			if (empty($pmID))
576
			{
577
				$context['current_pm'] = 0;
578
			}
579
580
			// This is a list of the pm's that are used for "show all" display.
581
			if ($context['display_mode'] == 0)
582
			{
583
				$display_pms = $pms;
584
			}
585
			// Just use the last pm the user received to start things off
586
			else
587
			{
588
				$display_pms = array($lastData['id']);
589
			}
590
591
			// At this point we know the main id_pm's. But if we are looking at conversations we need
592
			// the PMs that make up the conversation
593
			if ($context['display_mode'] == 2)
594
			{
595
				list($display_pms, $posters) = loadConversationList($lastData['head'], $recipients, $context['folder']);
596
597
				// Conversation list may expose additional PM's being displayed
598
				$all_pms = array_unique(array_merge($pms, $display_pms));
599
600
				// See if any of these 'listing' PM's are in a conversation thread that has unread entries
601
				$context['conversation_unread'] = loadConversationUnreadStatus($all_pms);
602
			}
603
			// This is pretty much EVERY pm!
604
			else
605
			{
606
				$all_pms = array_unique(array_merge($pms, $display_pms));
607
			}
608
609
			// Get recipients (don't include bcc-recipients for your inbox, you're not supposed to know :P).
610
			list($context['message_labels'], $context['message_replied'], $context['message_unread']) = loadPMRecipientInfo($all_pms, $recipients, $context['folder']);
611
612
			// Make sure we don't load any unnecessary data for one at a time mode
613
			if ($context['display_mode'] == 1)
614
			{
615
				foreach ($posters as $pm_key => $sender)
616
				{
617
					if (!in_array($pm_key, $display_pms))
618
					{
619
						unset($posters[$pm_key]);
620
					}
621
				}
622
			}
623
624
			// Load some information about the message sender
625
			$posters = array_unique($posters);
626
			if (!empty($posters))
627
			{
628
				loadMemberData($posters);
629
			}
630
631
			// If we're on grouped/restricted view get a restricted list of messages.
632
			if ($context['display_mode'] != 0)
633
			{
634
				// Get the order right.
635
				$orderBy = array();
636
				foreach (array_reverse($pms) as $pm)
637
					$orderBy[] = 'pm.id_pm = ' . $pm;
638
639
				// Separate query for these bits, preparePMContext_callback will use it as required
640
				$subjects_request = loadPMSubjectRequest($pms, $orderBy);
641
			}
642
643
			// Execute the load message query if a message has been chosen and let
644
			// preparePMContext_callback fetch the results.  Otherwise just show the pm selection list
645
			if (empty($pmsg) && empty($pmID) && $context['display_mode'] != 0)
646
			{
647
				$messages_request = false;
648
			}
649
			else
650
			{
651
				$messages_request = loadPMMessageRequest($display_pms, $sort_by_query, $sort_by, $descending, $context['display_mode'], $context['folder']);
652
			}
653
		}
654
		else
655
		{
656
			$messages_request = false;
657
		}
658
659
		// Prepare some items for the template
660
		$context['can_send_pm'] = allowedTo('pm_send');
661
		$context['can_send_email'] = allowedTo('send_email_to_members');
662
		$context['sub_template'] = 'folder';
663
		$context['page_title'] = $txt['pm_inbox'];
664
		$context['sort_direction'] = $descending ? 'down' : 'up';
665
		$context['sort_by'] = $sort_by;
666
667
		// Auto video embedding enabled, someone may have a link in a PM
668
		if (!empty($messages_request) && !empty($modSettings['enableVideoEmbeding']))
669
		{
670
			addInlineJavascript('
671
		$(document).ready(function() {
672
			$().linkifyvideo(oEmbedtext);
673
		});', true
674
			);
675
		}
676
677
		if (!empty($messages_request) && !empty($context['show_delete']))
678
		{
679
			Template_Layers::getInstance()->addEnd('pm_pages_and_buttons');
680
		}
681
682
		// Set up the page index.
683
		$context['page_index'] = constructPageIndex($scripturl . '?action=pm;f=' . $context['folder'] . (isset($this->_req->query->l) ? ';l=' . (int) $this->_req->query->l : '') . ';sort=' . $context['sort_by'] . ($descending ? ';desc' : ''), $start, $max_messages, $modSettings['defaultMaxMessages']);
684
		$context['start'] = $start;
685
686
		$context['pm_form_url'] = $scripturl . '?action=pm;sa=pmactions;' . ($context['display_mode'] == 2 ? 'conversation;' : '') . 'f=' . $context['folder'] . ';start=' . $context['start'] . ($context['current_label_id'] != -1 ? ';l=' . $context['current_label_id'] : '');
687
688
		// Finally mark the relevant messages as read.
689
		if ($context['folder'] !== 'sent' && !empty($context['labels'][(int) $context['current_label_id']]['unread_messages']))
690
		{
691
			// If the display mode is "old sk00l" do them all...
692
			if ($context['display_mode'] == 0)
693
			{
694
				markMessages(null, $context['current_label_id']);
695
			}
696
			// Otherwise do just the currently displayed ones!
697
			elseif (!empty($context['current_pm']))
698
			{
699
				markMessages($display_pms, $context['current_label_id']);
0 ignored issues
show
Bug introduced by
The variable $display_pms does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
700
			}
701
		}
702
703
		// Build the conversation button array.
704
		if ($context['display_mode'] === 2 && !empty($context['current_pm']))
705
		{
706
			$context['conversation_buttons'] = array(
707
				'delete' => array(
708
					'text' => 'delete_conversation',
709
					'image' => 'delete.png',
710
					'lang' => true,
711
					'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'],
712
					'custom' => 'onclick="return confirm(\'' . addslashes($txt['remove_message']) . '?\');"'
713
				),
714
			);
715
716
			// Allow mods to add additional buttons here
717
			call_integration_hook('integrate_conversation_buttons');
718
		}
719
	}
720
721
	/**
722
	 * Send a new personal message?
723
	 */
724
	public function action_send()
725
	{
726
		global $txt, $scripturl, $modSettings, $context, $user_info;
727
728
		// Load in some text and template dependencies
729
		loadLanguage('PersonalMessage');
730
		loadTemplate('PersonalMessage');
731
732
		// Set the template we will use
733
		$context['sub_template'] = 'send';
734
735
		// Extract out the spam settings - cause it's neat.
736
		list ($modSettings['max_pm_recipients'], $modSettings['pm_posts_verification'], $modSettings['pm_posts_per_hour']) = explode(',', $modSettings['pm_spam_settings']);
737
738
		// Set up some items for the template
739
		$context['page_title'] = $txt['send_message'];
740
		$context['reply'] = isset($this->_req->query->pmsg) || isset($this->_req->query->quote);
741
742
		// Check whether we've gone over the limit of messages we can send per hour.
743
		if (!empty($modSettings['pm_posts_per_hour']) && !allowedTo(array('admin_forum', 'moderate_forum', 'send_mail')) && $user_info['mod_cache']['bq'] === '0=1' && $user_info['mod_cache']['gq'] === '0=1')
744
		{
745
			// How many messages have they sent this last hour?
746
			$pmCount = pmCount($user_info['id'], 3600);
747
748
			if (!empty($pmCount) && $pmCount >= $modSettings['pm_posts_per_hour'])
749
			{
750
				Errors::instance()->fatal_lang_error('pm_too_many_per_hour', true, array($modSettings['pm_posts_per_hour']));
751
			}
752
		}
753
754
		try
755
		{
756
			$this->_events->trigger('before_set_context', array('pmsg' => isset($this->_req->query->pmsg) ? $this->_req->query->pmsg : (isset($this->_req->query->quote) ? $this->_req->query->quote : 0)));
757
		}
758
		catch (Pm_Error_Exception $e)
0 ignored issues
show
Bug introduced by
The class Pm_Error_Exception does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
759
		{
760
			return $this->messagePostError($e->namedRecipientList, $e->recipientList, $e->msgOptions);
761
		}
762
763
		// Quoting / Replying to a message?
764
		if (!empty($this->_req->query->pmsg))
765
		{
766
			$pmsg = $this->_req->getQuery('pmsg', 'intval');
767
768
			// Make sure this is accessible (not deleted)
769
			if (!isAccessiblePM($pmsg))
770
			{
771
				Errors::instance()->fatal_lang_error('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)
780
			{
781
				Errors::instance()->fatal_lang_error('pm_not_yours', false);
782
			}
783
784
			// Censor the message.
785
			censorText($row_quoted['subject']);
786
			censorText($row_quoted['body']);
787
788
			// Lets 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, lets clean up some things and set the quote header for the pm body
803
			if (isset($this->_req->query->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
			// Do the BBC thang on the message.
824
			$bbc_parser = \BBC\ParserWrapper::getInstance();
825
			$row_quoted['body'] = $bbc_parser->parsePM($row_quoted['body']);
826
827
			// Set up the quoted message array.
828
			$context['quoted_message'] = array(
829
				'id' => $row_quoted['id_pm'],
830
				'pm_head' => $row_quoted['pm_head'],
831
				'member' => array(
832
					'name' => $row_quoted['real_name'],
833
					'username' => $row_quoted['member_name'],
834
					'id' => $row_quoted['id_member'],
835
					'href' => !empty($row_quoted['id_member']) ? $scripturl . '?action=profile;u=' . $row_quoted['id_member'] : '',
836
					'link' => !empty($row_quoted['id_member']) ? '<a href="' . $scripturl . '?action=profile;u=' . $row_quoted['id_member'] . '">' . $row_quoted['real_name'] . '</a>' : $row_quoted['real_name'],
837
				),
838
				'subject' => $row_quoted['subject'],
839
				'time' => standardTime($row_quoted['msgtime']),
840
				'html_time' => htmlTime($row_quoted['msgtime']),
841
				'timestamp' => forum_time(true, $row_quoted['msgtime']),
842
				'body' => $row_quoted['body']
843
			);
844
		}
845
		// A new message it is then
846
		else
847
		{
848
			$context['quoted_message'] = false;
849
			$form_subject = '';
850
			$form_message = '';
851
		}
852
853
		// Start of like we don't know where this is going
854
		$context['recipients'] = array(
855
			'to' => array(),
856
			'bcc' => array(),
857
		);
858
859
		// Sending by ID?  Replying to all?  Fetch the real_name(s).
860
		if (isset($this->_req->query->u))
861
		{
862
			// If the user is replying to all, get all the other members this was sent to..
863
			if ($this->_req->query->u === 'all' && isset($row_quoted))
864
			{
865
				// Firstly, to reply to all we clearly already have $row_quoted - so have the original member from.
866
				if ($row_quoted['id_member'] != $user_info['id'])
867
				{
868
					$context['recipients']['to'][] = array(
869
						'id' => $row_quoted['id_member'],
870
						'name' => htmlspecialchars($row_quoted['real_name'], ENT_COMPAT, 'UTF-8'),
871
					);
872
				}
873
874
				// Now to get all the others.
875
				$context['recipients']['to'] = array_merge($context['recipients']['to'], isset($pmsg) ? loadPMRecipientsAll($pmsg) : array());
876
			}
877
			else
878
			{
879
				$users = array_map('intval', explode(',', $this->_req->query->u));
880
				$users = array_unique($users);
881
882
				// For all the member's this is going to, get their display name.
883
				require_once(SUBSDIR . '/Members.subs.php');
884
				$result = getBasicMemberData($users);
885
886
				foreach ($result as $row)
887
				{
888
					$context['recipients']['to'][] = array(
889
						'id' => $row['id_member'],
890
						'name' => $row['real_name'],
891
					);
892
				}
893
			}
894
895
			// Get a literal name list in case the user has JavaScript disabled.
896
			$names = array();
897
			foreach ($context['recipients']['to'] as $to)
898
				$names[] = $to['name'];
899
			$context['to_value'] = empty($names) ? '' : '&quot;' . implode('&quot;, &quot;', $names) . '&quot;';
900
		}
901
		else
902
		{
903
			$context['to_value'] = '';
904
		}
905
906
		// Set the defaults...
907
		$context['subject'] = $form_subject;
908
		$context['message'] = str_replace(array('"', '<', '>', '&nbsp;'), array('&quot;', '&lt;', '&gt;', ' '), $form_message);
909
910
		// And build the link tree.
911
		$context['linktree'][] = array(
912
			'url' => $scripturl . '?action=pm;sa=send',
913
			'name' => $txt['new_message']
914
		);
915
916
		// Needed for the editor.
917
		require_once(SUBSDIR . '/Editor.subs.php');
918
919
		// Now create the editor.
920
		$editorOptions = array(
921
			'id' => 'message',
922
			'value' => $context['message'],
923
			'height' => '250px',
924
			'width' => '100%',
925
			'labels' => array(
926
				'post_button' => $txt['send_message'],
927
			),
928
			'preview_type' => 2,
929
		);
930
931
		$this->_events->trigger('prepare_send_context', array('pmsg' => isset($this->_req->query->pmsg) ? $this->_req->query->pmsg : (isset($this->_req->query->quote) ? $this->_req->query->quote : 0), 'editorOptions' => &$editorOptions, 'recipientList' => &$recipientList));
0 ignored issues
show
Bug introduced by
The variable $recipientList does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
932
933
		create_control_richedit($editorOptions);
934
935
		// No one is bcc'ed just yet
936
		$context['bcc_value'] = '';
937
938
		// Register this form and get a sequence number in $context.
939
		checkSubmitOnce('register');
940
	}
941
942
	/**
943
	 * Send a personal message.
944
	 */
945
	public function action_send2()
946
	{
947
		global $txt, $context, $user_info, $modSettings;
948
949
		// All the helpers we need
950
		require_once(SUBSDIR . '/Auth.subs.php');
951
		require_once(SUBSDIR . '/Post.subs.php');
952
953
		loadLanguage('PersonalMessage', '', false);
954
955
		// Extract out the spam settings - it saves database space!
956
		list ($modSettings['max_pm_recipients'], $modSettings['pm_posts_verification'], $modSettings['pm_posts_per_hour']) = explode(',', $modSettings['pm_spam_settings']);
957
958
		// Initialize the errors we're about to make.
959
		$post_errors = Error_Context::context('pm', 1);
960
961
		// Check whether we've gone over the limit of messages we can send per hour - fatal error if fails!
962
		if (!empty($modSettings['pm_posts_per_hour'])
963
			&& !allowedTo(array('admin_forum', 'moderate_forum', 'send_mail'))
964
			&& $user_info['mod_cache']['bq'] === '0=1'
965
			&& $user_info['mod_cache']['gq'] === '0=1'
966
		)
967
		{
968
			// How many have they sent this last hour?
969
			$pmCount = pmCount($user_info['id'], 3600);
970
971
			if (!empty($pmCount) && $pmCount >= $modSettings['pm_posts_per_hour'])
972
			{
973
				if (!isset($this->_req->query->xml))
974
				{
975
					Errors::instance()->fatal_lang_error('pm_too_many_per_hour', true, array($modSettings['pm_posts_per_hour']));
976
				}
977
				else
978
				{
979
					$post_errors->addError('pm_too_many_per_hour');
980
				}
981
			}
982
		}
983
984
		// If your session timed out, show an error, but do allow to re-submit.
985
		if (!isset($this->_req->query->xml) && checkSession('post', '', false) != '')
986
		{
987
			$post_errors->addError('session_timeout');
988
		}
989
990
		$this->_req->post->subject = isset($this->_req->post->subject) ? strtr(Util::htmltrim($this->_req->post->subject), array("\r" => '', "\n" => '', "\t" => '')) : '';
991
		$this->_req->post->to = $this->_req->getPost('to', 'trim', empty($this->_req->query->to) ? '' : $this->_req->query->to);
992
		$this->_req->post->bcc = $this->_req->getPost('bcc', 'trim', empty($this->_req->query->bcc) ? '' : $this->_req->query->bcc);
993
994
		// Route the input from the 'u' parameter to the 'to'-list.
995
		if (!empty($this->_req->post->u))
996
		{
997
			$this->_req->post->recipient_to = explode(',', $this->_req->post->u);
998
		}
999
1000
		$bbc_parser = \BBC\ParserWrapper::getInstance();
1001
1002
		// Construct the list of recipients.
1003
		$recipientList = array();
1004
		$namedRecipientList = array();
1005
		$namesNotFound = array();
1006
		foreach (array('to', 'bcc') as $recipientType)
1007
		{
1008
			// First, let's see if there's user ID's given.
1009
			$recipientList[$recipientType] = array();
1010
			$type = 'recipient_' . $recipientType;
1011
			if (!empty($this->_req->post->$type) && is_array($this->_req->post->$type))
1012
			{
1013
				$recipientList[$recipientType] = array_map('intval', $this->_req->post->$type);
1014
			}
1015
1016
			// Are there also literal names set?
1017
			if (!empty($this->_req->post->$recipientType))
1018
			{
1019
				// We're going to take out the "s anyway ;).
1020
				$recipientString = strtr($this->_req->post->$recipientType, array('\\"' => '"'));
1021
1022
				preg_match_all('~"([^"]+)"~', $recipientString, $matches);
1023
				$namedRecipientList[$recipientType] = array_unique(array_merge($matches[1], explode(',', preg_replace('~"[^"]+"~', '', $recipientString))));
1024
1025
				// Clean any literal names entered
1026
				foreach ($namedRecipientList[$recipientType] as $index => $recipient)
1027
				{
1028
					if (strlen(trim($recipient)) > 0)
1029
					{
1030
						$namedRecipientList[$recipientType][$index] = Util::htmlspecialchars(Util::strtolower(trim($recipient)));
1031
					}
1032
					else
1033
					{
1034
						unset($namedRecipientList[$recipientType][$index]);
1035
					}
1036
				}
1037
1038
				// Now see if we can resolve the entered name to an actual user
1039
				if (!empty($namedRecipientList[$recipientType]))
1040
				{
1041
					$foundMembers = findMembers($namedRecipientList[$recipientType]);
1042
1043
					// Assume all are not found, until proven otherwise.
1044
					$namesNotFound[$recipientType] = $namedRecipientList[$recipientType];
1045
1046
					// Make sure we only have each member listed once, in case they did not use the select list
1047
					foreach ($foundMembers as $member)
1048
					{
1049
						$testNames = array(
1050
							Util::strtolower($member['username']),
1051
							Util::strtolower($member['name']),
1052
							Util::strtolower($member['email']),
1053
						);
1054
1055
						if (count(array_intersect($testNames, $namedRecipientList[$recipientType])) !== 0)
1056
						{
1057
							$recipientList[$recipientType][] = $member['id'];
1058
1059
							// Get rid of this username, since we found it.
1060
							$namesNotFound[$recipientType] = array_diff($namesNotFound[$recipientType], $testNames);
1061
						}
1062
					}
1063
				}
1064
			}
1065
1066
			// Selected a recipient to be deleted? Remove them now.
1067
			if (!empty($this->_req->post->delete_recipient))
1068
			{
1069
				$recipientList[$recipientType] = array_diff($recipientList[$recipientType], array((int) $this->_req->post->delete_recipient));
1070
			}
1071
1072
			// Make sure we don't include the same name twice
1073
			$recipientList[$recipientType] = array_unique($recipientList[$recipientType]);
1074
		}
1075
1076
		// Are we changing the recipients some how?
1077
		$is_recipient_change = !empty($this->_req->post->delete_recipient) || !empty($this->_req->post->to_submit) || !empty($this->_req->post->bcc_submit);
1078
1079
		// Check if there's at least one recipient.
1080
		if (empty($recipientList['to']) && empty($recipientList['bcc']))
1081
		{
1082
			$post_errors->addError('no_to');
1083
		}
1084
1085
		// Make sure that we remove the members who did get it from the screen.
1086
		if (!$is_recipient_change)
1087
		{
1088
			foreach (array_keys($recipientList) as $recipientType)
1089
			{
1090
				if (!empty($namesNotFound[$recipientType]))
1091
				{
1092
					$post_errors->addError('bad_' . $recipientType);
1093
1094
					// Since we already have a post error, remove the previous one.
1095
					$post_errors->removeError('no_to');
1096
1097
					foreach ($namesNotFound[$recipientType] as $name)
1098
						$context['send_log']['failed'][] = sprintf($txt['pm_error_user_not_found'], $name);
1099
				}
1100
			}
1101
		}
1102
1103
		// Did they make any mistakes like no subject or message?
1104
		if ($this->_req->post->subject === '')
1105
		{
1106
			$post_errors->addError('no_subject');
1107
		}
1108
1109
		if (!isset($this->_req->post->message) || $this->_req->post->message === '')
1110
		{
1111
			$post_errors->addError('no_message');
1112
		}
1113
		elseif (!empty($modSettings['max_messageLength']) && Util::strlen($this->_req->post->message) > $modSettings['max_messageLength'])
1114
		{
1115
			$post_errors->addError('long_message');
1116
		}
1117
		else
1118
		{
1119
			// Preparse the message.
1120
			$message = $this->_req->post->message;
1121
			preparsecode($message);
1122
1123
			// Make sure there's still some content left without the tags.
1124
			if (Util::htmltrim(strip_tags($bbc_parser->parsePM(Util::htmlspecialchars($message, ENT_QUOTES)), '<img>')) === '' && (!allowedTo('admin_forum') || strpos($message, '[html]') === false))
1125
			{
1126
				$post_errors->addError('no_message');
1127
			}
1128
		}
1129
1130
		// If they made any errors, give them a chance to make amends.
1131
		if ($post_errors->hasErrors() && !$is_recipient_change && !isset($this->_req->post->preview) && !isset($this->_req->query->xml))
1132
		{
1133
			$this->messagePostError($namedRecipientList, $recipientList);
1134
1135
			return false;
1136
		}
1137
1138
		// Want to take a second glance before you send?
1139
		if (isset($this->_req->post->preview))
1140
		{
1141
			// Set everything up to be displayed.
1142
			$context['preview_subject'] = Util::htmlspecialchars($this->_req->post->subject);
1143
			$context['preview_message'] = Util::htmlspecialchars($this->_req->post->message, ENT_QUOTES, 'UTF-8', true);
1144
			preparsecode($context['preview_message'], true);
1145
1146
			// Parse out the BBC if it is enabled.
1147
			$context['preview_message'] = $bbc_parser->parsePM($context['preview_message']);
1148
1149
			// Censor, as always.
1150
			censorText($context['preview_subject']);
1151
			censorText($context['preview_message']);
1152
1153
			// Set a descriptive title.
1154
			$context['page_title'] = $txt['preview'] . ' - ' . $context['preview_subject'];
1155
1156
			// Pretend they messed up but don't ignore if they really did :P.
1157
			$this->messagePostError($namedRecipientList, $recipientList);
1158
1159
			return false;
1160
		}
1161
		// Adding a recipient cause javascript ain't working?
1162
		elseif ($is_recipient_change)
1163
		{
1164
			// Maybe we couldn't find one?
1165
			foreach ($namesNotFound as $recipientType => $names)
1166
			{
1167
				$post_errors->addError('bad_' . $recipientType);
1168
				foreach ($names as $name)
1169
					$context['send_log']['failed'][] = sprintf($txt['pm_error_user_not_found'], $name);
1170
			}
1171
1172
			$this->messagePostError($namedRecipientList, $recipientList);
1173
			return true;
1174
		}
1175
1176
		try
1177
		{
1178
			$this->_events->trigger('before_sending', array('namedRecipientList' => $namedRecipientList, 'recipientList' => $recipientList, 'namesNotFound' => $namesNotFound, 'post_errors' => $post_errors));
1179
		}
1180
		catch (Controller_Redirect_Exception $e)
1181
		{
1182
			return $this->messagePostError($namedRecipientList, $recipientList);
1183
		}
1184
1185
		// Safety net, it may be a module may just add to the list of errors without actually throw the error
1186
		if ($post_errors->hasErrors() && !isset($this->_req->post->preview) && !isset($this->_req->query->xml))
1187
		{
1188
			$this->messagePostError($namedRecipientList, $recipientList);
1189
1190
			return false;
1191
		}
1192
1193
		// Before we send the PM, let's make sure we don't have an abuse of numbers.
1194
		if (!empty($modSettings['max_pm_recipients']) && count($recipientList['to']) + count($recipientList['bcc']) > $modSettings['max_pm_recipients'] && !allowedTo(array('moderate_forum', 'send_mail', 'admin_forum')))
1195
		{
1196
			$context['send_log'] = array(
1197
				'sent' => array(),
1198
				'failed' => array(sprintf($txt['pm_too_many_recipients'], $modSettings['max_pm_recipients'])),
1199
			);
1200
1201
			$this->messagePostError($namedRecipientList, $recipientList);
1202
			return false;
1203
		}
1204
1205
		// Protect from message spamming.
1206
		spamProtection('pm');
1207
1208
		// Prevent double submission of this form.
1209
		checkSubmitOnce('check');
1210
1211
		// Finally do the actual sending of the PM.
1212
		if (!empty($recipientList['to']) || !empty($recipientList['bcc']))
1213
		{
1214
			$context['send_log'] = sendpm($recipientList, $this->_req->post->subject, $this->_req->post->message, true, null, !empty($this->_req->post->pm_head) ? (int) $this->_req->post->pm_head : 0);
1215
		}
1216
		else
1217
		{
1218
			$context['send_log'] = array(
1219
				'sent' => array(),
1220
				'failed' => array()
1221
			);
1222
		}
1223
1224
		// Mark the message as "replied to".
1225
		if (!empty($context['send_log']['sent']) && !empty($this->_req->post->replied_to) && $this->_req->getQuery('f') === 'inbox')
1226
		{
1227
			require_once(SUBSDIR . '/PersonalMessage.subs.php');
1228
			setPMRepliedStatus($user_info['id'], (int) $this->_req->post->replied_to);
1229
		}
1230
1231
		$failed = !empty($context['send_log']['failed']);
1232
		$this->_events->trigger('message_sent', array('failed' => $failed));
1233
1234
		// If one or more of the recipients were invalid, go back to the post screen with the failed usernames.
1235
		if ($failed)
1236
		{
1237
			$this->messagePostError($namesNotFound, array(
1238
				'to' => array_intersect($recipientList['to'], $context['send_log']['failed']),
1239
				'bcc' => array_intersect($recipientList['bcc'], $context['send_log']['failed'])
1240
			));
1241
1242
			return false;
1243
		}
1244
		// Message sent successfully?
1245
		else
1246
		{
1247
			$context['current_label_redirect'] = $context['current_label_redirect'] . ';done=sent';
1248
		}
1249
1250
		// Go back to the where they sent from, if possible...
1251
		redirectexit($context['current_label_redirect']);
1252
1253
		return true;
1254
	}
1255
1256
	/**
1257
	 * An error in the message...
1258
	 *
1259
	 * @param mixed[] $named_recipients
1260
	 * @param mixed[] $recipient_ids array keys of [bbc] => int[] and [to] => int[]
1261
	 * @param mixed[] $msg_options body, subject and reply values
0 ignored issues
show
Documentation introduced by
Should the type for parameter $msg_options not be array|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
1262
	 */
1263
	public function messagePostError($named_recipients, $recipient_ids = array(), $msg_options = null)
1264
	{
1265
		global $txt, $context, $scripturl, $modSettings, $user_info;
1266
1267
		if (isset($this->_req->query->xml))
1268
		{
1269
			$context['sub_template'] = 'generic_preview';
1270
		}
1271
		else
1272
		{
1273
			$context['sub_template'] = 'send';
1274
			$context['menu_data_' . $context['pm_menu_id']]['current_area'] = 'send';
1275
		}
1276
1277
		$context['page_title'] = $txt['send_message'];
1278
		$error_types = Error_Context::context('pm', 1);
1279
1280
		// Got some known members?
1281
		$context['recipients'] = array(
1282
			'to' => array(),
1283
			'bcc' => array(),
1284
		);
1285
1286
		if (!empty($recipient_ids['to']) || !empty($recipient_ids['bcc']))
1287
		{
1288
			$allRecipients = array_merge($recipient_ids['to'], $recipient_ids['bcc']);
1289
1290
			require_once(SUBSDIR . '/Members.subs.php');
1291
1292
			// Get the latest activated member's display name.
1293
			$result = getBasicMemberData($allRecipients);
1294
			foreach ($result as $row)
1295
			{
1296
				$recipientType = in_array($row['id_member'], $recipient_ids['bcc']) ? 'bcc' : 'to';
1297
				$context['recipients'][$recipientType][] = array(
1298
					'id' => $row['id_member'],
1299
					'name' => $row['real_name'],
1300
				);
1301
			}
1302
		}
1303
1304
		// Set everything up like before....
1305
		if (!empty($msg_options))
1306
		{
1307
			$context['subject'] = $msg_options->subject;
1308
			$context['message'] = $msg_options->body;
1309
			$context['reply'] = $msg_options->reply_to;
1310
		}
1311
		else
1312
		{
1313
			$context['subject'] = isset($this->_req->post->subject) ? Util::htmlspecialchars($this->_req->post->subject) : '';
1314
			$context['message'] = isset($this->_req->post->message) ? str_replace(array('  '), array('&nbsp; '), Util::htmlspecialchars($this->_req->post->message, ENT_QUOTES, 'UTF-8', true)) : '';
1315
			$context['reply'] = !empty($this->_req->post->replied_to);
1316
		}
1317
1318
		// If this is a reply to message, we need to reload the quote
1319
		if ($context['reply'])
1320
		{
1321
			$pmsg = (int) $this->_req->post->replied_to;
1322
			$isReceived = $context['folder'] !== 'sent';
1323
			$row_quoted = loadPMQuote($pmsg, $isReceived);
1324
			if ($row_quoted === false)
1325
			{
1326
				if (!isset($this->_req->query->xml))
1327
				{
1328
					Errors::instance()->fatal_lang_error('pm_not_yours', false);
1329
				}
1330
				else
1331
				{
1332
					$error_types->addError('pm_not_yours');
1333
				}
1334
			}
1335
			else
1336
			{
1337
				censorText($row_quoted['subject']);
1338
				censorText($row_quoted['body']);
1339
				$bbc_parser = \BBC\ParserWrapper::getInstance();
1340
1341
				$context['quoted_message'] = array(
1342
					'id' => $row_quoted['id_pm'],
1343
					'pm_head' => $row_quoted['pm_head'],
1344
					'member' => array(
1345
						'name' => $row_quoted['real_name'],
1346
						'username' => $row_quoted['member_name'],
1347
						'id' => $row_quoted['id_member'],
1348
						'href' => !empty($row_quoted['id_member']) ? $scripturl . '?action=profile;u=' . $row_quoted['id_member'] : '',
1349
						'link' => !empty($row_quoted['id_member']) ? '<a href="' . $scripturl . '?action=profile;u=' . $row_quoted['id_member'] . '">' . $row_quoted['real_name'] . '</a>' : $row_quoted['real_name'],
1350
					),
1351
					'subject' => $row_quoted['subject'],
1352
					'time' => standardTime($row_quoted['msgtime']),
1353
					'html_time' => htmlTime($row_quoted['msgtime']),
1354
					'timestamp' => forum_time(true, $row_quoted['msgtime']),
1355
					'body' => $bbc_parser->parsePM($row_quoted['body']),
1356
				);
1357
			}
1358
		}
1359
1360
		// Build the link tree....
1361
		$context['linktree'][] = array(
1362
			'url' => $scripturl . '?action=pm;sa=send',
1363
			'name' => $txt['new_message']
1364
		);
1365
1366
		// Set each of the errors for the template.
1367
		$context['post_error'] = array(
1368
			'errors' => $error_types->prepareErrors(),
1369
			'type' => $error_types->getErrorType() == 0 ? 'minor' : 'serious',
1370
			'title' => $txt['error_while_submitting'],
1371
		);
1372
1373
		// We need to load the editor once more.
1374
		require_once(SUBSDIR . '/Editor.subs.php');
1375
1376
		// Create it...
1377
		$editorOptions = array(
1378
			'id' => 'message',
1379
			'value' => $context['message'],
1380
			'width' => '100%',
1381
			'height' => '250px',
1382
			'labels' => array(
1383
				'post_button' => $txt['send_message'],
1384
			),
1385
			'preview_type' => 2,
1386
		);
1387
1388
		$this->_events->trigger('prepare_send_context', array('pmsg' => isset($this->_req->query->pmsg) ? $this->_req->query->pmsg : (isset($this->_req->query->quote) ? $this->_req->query->quote : 0), 'editorOptions' => &$editorOptions, 'recipientList' => &$recipientList));
0 ignored issues
show
Bug introduced by
The variable $recipientList does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
1389
1390
		create_control_richedit($editorOptions);
1391
1392
		// Check whether we need to show the code again.
1393
		$context['require_verification'] = !$user_info['is_admin'] && !empty($modSettings['pm_posts_verification']) && $user_info['posts'] < $modSettings['pm_posts_verification'];
1394
		if ($context['require_verification'] && !isset($this->_req->query->xml))
1395
		{
1396
			require_once(SUBSDIR . '/VerificationControls.class.php');
1397
			$verificationOptions = array(
1398
				'id' => 'pm',
1399
			);
1400
			$context['require_verification'] = create_control_verification($verificationOptions);
1401
			$context['visual_verification_id'] = $verificationOptions['id'];
1402
		}
1403
1404
		$context['to_value'] = empty($named_recipients['to']) ? '' : '&quot;' . implode('&quot;, &quot;', $named_recipients['to']) . '&quot;';
1405
		$context['bcc_value'] = empty($named_recipients['bcc']) ? '' : '&quot;' . implode('&quot;, &quot;', $named_recipients['bcc']) . '&quot;';
1406
1407
		// No check for the previous submission is needed.
1408
		checkSubmitOnce('free');
1409
1410
		// Acquire a new form sequence number.
1411
		checkSubmitOnce('register');
1412
	}
1413
1414
	/**
1415
	 * This function performs all additional actions including the deleting
1416
	 * and labeling of PM's
1417
	 */
1418
	public function action_pmactions()
0 ignored issues
show
Coding Style introduced by
action_pmactions uses the super-global variable $_SESSION which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
1419
	{
1420
		global $context, $user_info;
1421
1422
		checkSession('request');
1423
1424
		// Sending in the single pm choice via GET
1425
		$pm_actions = $this->_req->getQuery('pm_actions', null, '');
1426
1427
		// Set the action to apply to the pm's defined by pm_actions (yes its that brilliant)
1428
		$pm_action = $this->_req->getPost('pm_action', 'trim', '');
1429
		$pm_action = empty($pm_action) && isset($this->_req->post->del_selected) ? 'delete' : '';
1430
1431
		// Create a list of pm's that we need to work on
1432
		if ($pm_action != ''
1433
			&& !empty($this->_req->post->pms)
1434
			&& is_array($this->_req->post->pms))
1435
		{
1436
			$pm_actions = array();
1437
			foreach ($this->_req->post->pms as $pm)
1438
				$pm_actions[(int) $pm] = $pm_action;
1439
		}
1440
1441
		// No messages to action then bug out
1442
		if (empty($pm_actions))
1443
			redirectexit($context['current_label_redirect']);
1444
1445
		// If we are in conversation, we may need to apply this to every message in that conversation.
1446
		if ($context['display_mode'] == 2 && isset($this->_req->query->conversation))
1447
		{
1448
			$id_pms = array_map('intval', array_keys($pm_actions));
1449
			$pm_heads = getDiscussions($id_pms);
1450
			$pms = getPmsFromDiscussion(array_keys($pm_heads));
1451
1452
			// Copy the action from the single to PM to the others in the conversation.
1453
			foreach ($pms as $id_pm => $id_head)
1454
			{
1455
				if (isset($pm_heads[$id_head]) && isset($pm_actions[$pm_heads[$id_head]]))
1456
				{
1457
					$pm_actions[$id_pm] = $pm_actions[$pm_heads[$id_head]];
1458
				}
1459
			}
1460
		}
1461
1462
		// Lets get to doing what we've been told
1463
		$to_delete = array();
1464
		$to_label = array();
1465
		$label_type = array();
1466
		foreach ($pm_actions as $pm => $action)
1467
		{
1468
			// What are we doing with the selected messages, adding a label, removing, other?
1469
			switch (substr($action, 0, 4))
1470
			{
1471
				case 'dele':
1472
					$to_delete[] = (int) $pm;
1473
					break;
1474
				case 'add_':
1475
					$type = 'add';
1476
					$action = substr($action, 4);
1477
					break;
1478
				case 'rem_':
1479
					$type = 'rem';
1480
					$action = substr($action, 4);
1481
					break;
1482
				default:
1483
					$type = 'unk';
1484
			}
1485
1486
			if ($action === '-1' || $action === '0' || (int) $action > 0)
1487
			{
1488
				$to_label[(int) $pm] = (int) $action;
1489
				$label_type[(int) $pm] = $type;
0 ignored issues
show
Bug introduced by
The variable $type does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
1490
			}
1491
		}
1492
1493
		// Deleting, it looks like?
1494
		if (!empty($to_delete))
1495
		{
1496
			deleteMessages($to_delete, $context['display_mode'] == 2 ? null : $context['folder']);
1497
		}
1498
1499
		// Are we labelling anything?
1500
		if (!empty($to_label) && $context['folder'] === 'inbox')
1501
		{
1502
			$updateErrors = changePMLabels($to_label, $label_type, $user_info['id']);
1503
1504
			// Any errors?
1505
			if (!empty($updateErrors))
1506
			{
1507
				Errors::instance()->fatal_lang_error('labels_too_many', true, array($updateErrors));
1508
			}
1509
		}
1510
1511
		// Back to the folder.
1512
		$_SESSION['pm_selected'] = array_keys($to_label);
1513
		redirectexit($context['current_label_redirect'] . (count($to_label) == 1 ? '#msg_' . $_SESSION['pm_selected'][0] : ''), count($to_label) == 1 && isBrowser('ie'));
1514
	}
1515
1516
	/**
1517
	 * Are you sure you want to PERMANENTLY (mostly) delete ALL your messages?
1518
	 */
1519
	public function action_removeall()
1520
	{
1521
		global $txt, $context;
1522
1523
		// Only have to set up the template....
1524
		$context['sub_template'] = 'ask_delete';
1525
		$context['page_title'] = $txt['delete_all'];
1526
		$context['delete_all'] = $this->_req->query->f === 'all';
1527
1528
		// And set the folder name...
1529
		$txt['delete_all'] = str_replace('PMBOX', $context['folder'] != 'sent' ? $txt['inbox'] : $txt['sent_items'], $txt['delete_all']);
1530
	}
1531
1532
	/**
1533
	 * Delete ALL the messages!
1534
	 */
1535
	public function action_removeall2()
1536
	{
1537
		global $context;
1538
1539
		checkSession('get');
1540
1541
		// If all then delete all messages the user has.
1542
		if ($this->_req->query->f === 'all')
1543
		{
1544
			deleteMessages(null, null);
1545
		}
1546
		// Otherwise just the selected folder.
1547
		else
1548
		{
1549
			deleteMessages(null, $this->_req->query->f != 'sent' ? 'inbox' : 'sent');
1550
		}
1551
1552
		// Done... all gone.
1553
		redirectexit($context['current_label_redirect']);
1554
	}
1555
1556
	/**
1557
	 * This function allows the user to prune (delete) all messages older than a supplied duration.
1558
	 */
1559
	public function action_prune()
1560
	{
1561
		global $txt, $context, $user_info, $scripturl;
1562
1563
		// Actually delete the messages.
1564
		if (isset($this->_req->post->age))
1565
		{
1566
			checkSession();
1567
1568
			// Calculate the time to delete before.
1569
			$deleteTime = max(0, time() - (86400 * (int) $this->_req->post->age));
1570
1571
			// Select all the messages older than $deleteTime.
1572
			$toDelete = getPMsOlderThan($user_info['id'], $deleteTime);
1573
1574
			// Delete the actual messages.
1575
			deleteMessages($toDelete);
1576
1577
			// Go back to their inbox.
1578
			redirectexit($context['current_label_redirect']);
1579
		}
1580
1581
		// Build the link tree elements.
1582
		$context['linktree'][] = array(
1583
			'url' => $scripturl . '?action=pm;sa=prune',
1584
			'name' => $txt['pm_prune']
1585
		);
1586
		$context['sub_template'] = 'prune';
1587
		$context['page_title'] = $txt['pm_prune'];
1588
	}
1589
1590
	/**
1591
	 * This function handles adding, deleting and editing labels on messages.
1592
	 */
1593
	public function action_manlabels()
1594
	{
1595
		global $txt, $context, $user_info, $scripturl;
1596
1597
		require_once(SUBSDIR . '/PersonalMessage.subs.php');
1598
1599
		// Build the link tree elements...
1600
		$context['linktree'][] = array(
1601
			'url' => $scripturl . '?action=pm;sa=manlabels',
1602
			'name' => $txt['pm_manage_labels']
1603
		);
1604
1605
		// Some things for the template
1606
		$context['page_title'] = $txt['pm_manage_labels'];
1607
		$context['sub_template'] = 'labels';
1608
1609
		// Add all existing labels to the array to save, slashing them as necessary...
1610
		$the_labels = array();
1611
		foreach ($context['labels'] as $label)
1612
		{
1613
			if ($label['id'] != -1)
1614
			{
1615
				$the_labels[$label['id']] = $label['name'];
1616
			}
1617
		}
1618
1619
		// Submitting changes?
1620
		if (isset($this->_req->post->add) || isset($this->_req->post->delete) || isset($this->_req->post->save))
1621
		{
1622
			checkSession('post');
1623
1624
			// This will be for updating messages.
1625
			$message_changes = array();
1626
			$new_labels = array();
1627
			$rule_changes = array();
1628
1629
			// Will most likely need this.
1630
			loadRules();
1631
1632
			// Adding a new label?
1633
			if (isset($this->_req->post->add))
1634
			{
1635
				$this->_req->post->label = strtr(Util::htmlspecialchars(trim($this->_req->post->label)), array(',' => '&#044;'));
1636
1637
				if (Util::strlen($this->_req->post->label) > 30)
1638
				{
1639
					$this->_req->post->label = Util::substr($this->_req->post->label, 0, 30);
1640
				}
1641
				if ($this->_req->post->label != '')
1642
				{
1643
					$the_labels[] = $this->_req->post->label;
1644
				}
1645
			}
1646
			// Deleting an existing label?
1647
			elseif (isset($this->_req->post->delete, $this->_req->post->delete_label))
1648
			{
1649
				$i = 0;
1650
				foreach ($the_labels as $id => $name)
1651
				{
1652
					if (isset($this->_req->post->delete_label[$id]))
1653
					{
1654
						unset($the_labels[$id]);
1655
						$message_changes[$id] = true;
1656
					}
1657
					else
1658
					{
1659
						$new_labels[$id] = $i++;
1660
					}
1661
				}
1662
			}
1663
			// The hardest one to deal with... changes.
1664
			elseif (isset($this->_req->post->save) && !empty($this->_req->post->label_name))
1665
			{
1666
				$i = 0;
1667
				foreach ($the_labels as $id => $name)
1668
				{
1669
					if ($id == -1)
1670
					{
1671
						continue;
1672
					}
1673
					elseif (isset($this->_req->post->label_name[$id]))
1674
					{
1675
						// Prepare the label name
1676
						$this->_req->post->label_name[$id] = trim(strtr(Util::htmlspecialchars($this->_req->post->label_name[$id]), array(',' => '&#044;')));
1677
1678
						// Has to fit in the database as well
1679
						if (Util::strlen($this->_req->post->label_name[$id]) > 30)
1680
						{
1681
							$this->_req->post->label_name[$id] = Util::substr($this->_req->post->label_name[$id], 0, 30);
1682
						}
1683
1684
						if ($this->_req->post->label_name[$id] != '')
1685
						{
1686
							$the_labels[(int) $id] = $this->_req->post->label_name[$id];
1687
							$new_labels[$id] = $i++;
1688
						}
1689
						else
1690
						{
1691
							unset($the_labels[(int) $id]);
1692
							$message_changes[(int) $id] = true;
1693
						}
1694
					}
1695
					else
1696
					{
1697
						$new_labels[$id] = $i++;
1698
					}
1699
				}
1700
			}
1701
1702
			// Save the label status.
1703
			require_once(SUBSDIR . '/Members.subs.php');
1704
			updateMemberData($user_info['id'], array('message_labels' => implode(',', $the_labels)));
1705
1706
			// Update all the messages currently with any label changes in them!
1707
			if (!empty($message_changes))
1708
			{
1709
				$searchArray = array_keys($message_changes);
1710
1711
				if (!empty($new_labels))
1712
				{
1713
					for ($i = max($searchArray) + 1, $n = max(array_keys($new_labels)); $i <= $n; $i++)
1714
						$searchArray[] = $i;
1715
				}
1716
1717
				updateLabelsToPM($searchArray, $new_labels, $user_info['id']);
1718
1719
				// Now do the same the rules - check through each rule.
1720
				foreach ($context['rules'] as $k => $rule)
1721
				{
1722
					// Each action...
1723
					foreach ($rule['actions'] as $k2 => $action)
1724
					{
1725
						if ($action['t'] != 'lab' || !in_array($action['v'], $searchArray))
1726
						{
1727
							continue;
1728
						}
1729
1730
						$rule_changes[] = $rule['id'];
1731
1732
						// If we're here we have a label which is either changed or gone...
1733
						if (isset($new_labels[$action['v']]))
1734
						{
1735
							$context['rules'][$k]['actions'][$k2]['v'] = $new_labels[$action['v']];
1736
						}
1737
						else
1738
						{
1739
							unset($context['rules'][$k]['actions'][$k2]);
1740
						}
1741
					}
1742
				}
1743
			}
1744
1745
			// If we have rules to change do so now.
1746
			if (!empty($rule_changes))
1747
			{
1748
				$rule_changes = array_unique($rule_changes);
1749
1750
				// Update/delete as appropriate.
1751
				foreach ($rule_changes as $k => $id)
1752
					if (!empty($context['rules'][$id]['actions']))
1753
					{
1754
						updatePMRuleAction($id, $user_info['id'], $context['rules'][$id]['actions']);
1755
						unset($rule_changes[$k]);
1756
					}
1757
1758
				// Anything left here means it's lost all actions...
1759
				if (!empty($rule_changes))
1760
				{
1761
					deletePMRules($user_info['id'], $rule_changes);
1762
				}
1763
			}
1764
1765
			// Make sure we're not caching this!
1766
			Cache::instance()->put('labelCounts:' . $user_info['id'], null, 720);
1767
1768
			// To make the changes appear right away, redirect.
1769
			redirectexit('action=pm;sa=manlabels');
1770
		}
1771
	}
1772
1773
	/**
1774
	 * Allows to edit Personal Message Settings.
1775
	 *
1776
	 * @uses ProfileOptions controller. (@todo refactor this.)
1777
	 * @uses Profile template.
1778
	 * @uses Profile language file.
1779
	 */
1780
	public function action_settings()
0 ignored issues
show
Coding Style introduced by
action_settings uses the super-global variable $_POST which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
1781
	{
1782
		global $txt, $user_info, $context, $scripturl, $profile_vars, $cur_profile, $user_profile;
1783
1784
		require_once(SUBSDIR . '/Profile.subs.php');
1785
1786
		// Load the member data for editing
1787
		loadMemberData($user_info['id'], false, 'profile');
1788
		$cur_profile = $user_profile[$user_info['id']];
1789
1790
		// Load up the profile template, its where PM settings are located
1791
		loadLanguage('Profile');
1792
		loadTemplate('Profile');
1793
1794
		// We want them to submit back to here.
1795
		$context['profile_custom_submit_url'] = $scripturl . '?action=pm;sa=settings;save';
1796
1797
		$context['page_title'] = $txt['pm_settings'];
1798
		$context['user']['is_owner'] = true;
1799
		$context['id_member'] = $user_info['id'];
1800
		$context['require_password'] = false;
1801
		$context['menu_item_selected'] = 'settings';
1802
		$context['submit_button_text'] = $txt['pm_settings'];
1803
1804
		// Add our position to the linktree.
1805
		$context['linktree'][] = array(
1806
			'url' => $scripturl . '?action=pm;sa=settings',
1807
			'name' => $txt['pm_settings']
1808
		);
1809
1810
		// Are they saving?
1811
		if (isset($this->_req->post->save))
1812
		{
1813
			checkSession('post');
1814
1815
			// Mimic what profile would do.
1816
			// @todo fix this when Profile.subs is not dependant on this behavior
1817
			$_POST = htmltrim__recursive((array) $this->_req->post);
1818
			$_POST = htmlspecialchars__recursive($_POST);
1819
1820
			// Save the fields.
1821
			saveProfileFields();
1822
1823
			if (!empty($profile_vars))
1824
			{
1825
				require_once(SUBSDIR . '/Members.subs.php');
1826
				updateMemberData($user_info['id'], $profile_vars);
1827
			}
1828
1829
			// Invalidate any cached data and reload so we show the saved values
1830
			Cache::instance()->put('member_data-profile-' . $user_info['id'], null, 0);
1831
			loadMemberData($user_info['id'], false, 'profile');
1832
			$cur_profile = $user_profile[$user_info['id']];
1833
		}
1834
1835
		// Load up the fields.
1836
		$controller = new ProfileOptions_Controller(new Event_Manager());
1837
		$controller->pre_dispatch();
1838
		$controller->action_pmprefs();
1839
	}
1840
1841
	/**
1842
	 * Allows the user to report a personal message to an administrator.
1843
	 *
1844
	 * What it does:
1845
	 * - In the first instance requires that the ID of the message to report is passed through $_GET.
1846
	 * - It allows the user to report to either a particular administrator - or the whole admin team.
1847
	 * - It will forward on a copy of the original message without allowing the reporter to make changes.
1848
	 *
1849
	 * @uses report_message sub-template.
1850
	 */
1851
	public function action_report()
1852
	{
1853
		global $txt, $context, $user_info, $language, $modSettings;
1854
1855
		// Check that this feature is even enabled!
1856
		if (empty($modSettings['enableReportPM']) || empty($this->_req->query->pmsg))
1857
		{
1858
			Errors::instance()->fatal_lang_error('no_access', false);
1859
		}
1860
1861
		$pmsg = $this->_req->getQuery('pmsg', 'intval', $this->_req->getPost('pmsg', 'intval', 0));
1862
1863
		if (!isAccessiblePM($pmsg, 'inbox'))
1864
		{
1865
			Errors::instance()->fatal_lang_error('no_access', false);
1866
		}
1867
1868
		$context['pm_id'] = $pmsg;
1869
		$context['page_title'] = $txt['pm_report_title'];
1870
		$context['sub_template'] = 'report_message';
1871
1872
		// We'll query some members, we will.
1873
		require_once(SUBSDIR . '/Members.subs.php');
1874
1875
		// If we're here, just send the user to the template, with a few useful context bits.
1876
		if (isset($this->_req->post->report))
1877
		{
1878
			$poster_comment = strtr(Util::htmlspecialchars($this->_req->post->reason), array("\r" => '', "\t" => ''));
1879
1880
			if (Util::strlen($poster_comment) > 254)
1881
			{
1882
				Errors::instance()->fatal_lang_error('post_too_long', false);
1883
			}
1884
1885
			// Check the session before proceeding any further!
1886
			checkSession('post');
1887
1888
			// First, load up the message they want to file a complaint against, and verify it actually went to them!
1889
			list ($subject, $body, $time, $memberFromID, $memberFromName, $poster_name, $time_message) = loadPersonalMessage($pmsg);
1890
1891
			require_once(SUBSDIR . '/Messages.subs.php');
1892
1893
			recordReport(array(
1894
				'id_msg' => $pmsg,
1895
				'id_topic' => 0,
1896
				'id_board' => 0,
1897
				'type' => 'pm',
1898
				'id_poster' => $memberFromID,
1899
				'real_name' => $memberFromName,
1900
				'poster_name' => $poster_name,
1901
				'subject' => $subject,
1902
				'body' => $body,
1903
				'time_message' => $time_message,
1904
			), $poster_comment);
1905
1906
			// Remove the line breaks...
1907
			$body = preg_replace('~<br ?/?' . '>~i', "\n", $body);
1908
1909
			$recipients = array();
1910
			$temp = loadPMRecipientsAll($context['pm_id'], true);
1911
			foreach ($temp as $recipient)
1912
				$recipients[] = $recipient['link'];
1913
1914
			// Now let's get out and loop through the admins.
1915
			$admins = admins(isset($this->_req->post->id_admin) ? (int) $this->_req->post->id_admin : 0);
1916
1917
			// Maybe we shouldn't advertise this?
1918
			if (empty($admins))
1919
			{
1920
				Errors::instance()->fatal_lang_error('no_access', false);
1921
			}
1922
1923
			$memberFromName = un_htmlspecialchars($memberFromName);
1924
1925
			// Prepare the message storage array.
1926
			$messagesToSend = array();
1927
1928
			// Loop through each admin, and add them to the right language pile...
1929
			foreach ($admins as $id_admin => $admin_info)
1930
			{
1931
				// Need to send in the correct language!
1932
				$cur_language = empty($admin_info['lngfile']) || empty($modSettings['userLanguage']) ? $language : $admin_info['lngfile'];
1933
1934
				if (!isset($messagesToSend[$cur_language]))
1935
				{
1936
					loadLanguage('PersonalMessage', $cur_language, false);
1937
1938
					// Make the body.
1939
					$report_body = str_replace(array('{REPORTER}', '{SENDER}'), array(un_htmlspecialchars($user_info['name']), $memberFromName), $txt['pm_report_pm_user_sent']);
1940
					$report_body .= "\n" . '[b]' . $this->_req->post->reason . '[/b]' . "\n\n";
1941
					if (!empty($recipients))
1942
					{
1943
						$report_body .= $txt['pm_report_pm_other_recipients'] . ' ' . implode(', ', $recipients) . "\n\n";
1944
					}
1945
					$report_body .= $txt['pm_report_pm_unedited_below'] . "\n" . '[quote author=' . (empty($memberFromID) ? '&quot;' . $memberFromName . '&quot;' : $memberFromName . ' link=action=profile;u=' . $memberFromID . ' date=' . $time) . ']' . "\n" . un_htmlspecialchars($body) . '[/quote]';
1946
1947
					// Plonk it in the array ;)
1948
					$messagesToSend[$cur_language] = array(
1949
						'subject' => (Util::strpos($subject, $txt['pm_report_pm_subject']) === false ? $txt['pm_report_pm_subject'] : '') . un_htmlspecialchars($subject),
1950
						'body' => $report_body,
1951
						'recipients' => array(
1952
							'to' => array(),
1953
							'bcc' => array()
1954
						),
1955
					);
1956
				}
1957
1958
				// Add them to the list.
1959
				$messagesToSend[$cur_language]['recipients']['to'][$id_admin] = $id_admin;
1960
			}
1961
1962
			// Send a different email for each language.
1963
			foreach ($messagesToSend as $lang => $message)
1964
				sendpm($message['recipients'], $message['subject'], $message['body']);
1965
1966
			// Give the user their own language back!
1967
			if (!empty($modSettings['userLanguage']))
1968
			{
1969
				loadLanguage('PersonalMessage', '', false);
1970
			}
1971
1972
			// Leave them with a template.
1973
			$context['sub_template'] = 'report_message_complete';
1974
		}
1975
	}
1976
1977
	/**
1978
	 * List and allow adding/entering all man rules, such as
1979
	 *
1980
	 * What it does:
1981
	 * - If it itches, it will be scratched.
1982
	 * - Yes or No are perfectly acceptable answers to almost every question.
1983
	 * - Men see in only 16 colors, Peach, for example, is a fruit, not a color.
1984
	 *
1985
	 * @uses sub template rules
1986
	 */
1987
	public function action_manrules()
1988
	{
1989
		global $txt, $context, $user_info, $scripturl;
1990
1991
		require_once(SUBSDIR . '/PersonalMessage.subs.php');
1992
1993
		// The link tree - gotta have this :o
1994
		$context['linktree'][] = array(
1995
			'url' => $scripturl . '?action=pm;sa=manrules',
1996
			'name' => $txt['pm_manage_rules']
1997
		);
1998
1999
		$context['page_title'] = $txt['pm_manage_rules'];
2000
		$context['sub_template'] = 'rules';
2001
2002
		// Load them... load them!!
2003
		loadRules();
2004
2005
		// Likely to need all the groups!
2006
		require_once(SUBSDIR . '/Membergroups.subs.php');
2007
		$context['groups'] = accessibleGroups();
2008
2009
		// Applying all rules?
2010
		if (isset($this->_req->query->apply))
2011
		{
2012
			checkSession('get');
2013
2014
			applyRules(true);
2015
			redirectexit('action=pm;sa=manrules');
2016
		}
2017
2018
		// Editing a specific rule?
2019
		if (isset($this->_req->query->add))
2020
		{
2021
			$context['rid'] = isset($this->_req->query->rid) && isset($context['rules'][$this->_req->query->rid]) ? (int) $this->_req->query->rid : 0;
2022
			$context['sub_template'] = 'add_rule';
2023
2024
			// Any known rule
2025
			$js_rules = '';
2026
			foreach ($context['known_rules'] as $rule)
2027
				$js_rules[$rule] = $txt['pm_rule_' . $rule];
2028
			$js_rules = json_encode($js_rules);
2029
2030
			// Any known label
2031
			$js_labels = '';
2032
			foreach ($context['labels'] as $label)
2033
				if ($label['id'] != -1)
2034
				{
2035
					$js_labels[$label['id'] + 1] = $label['name'];
2036
				}
2037
			$js_labels = json_encode($js_labels);
2038
2039
			// And all of the groups as well
2040
			$js_groups = json_encode($context['groups']);
2041
2042
			// Oh my, we have a lot of text strings for this
2043
			addJavascriptVar(array(
2044
				'criteriaNum' => 0,
2045
				'actionNum' => 0,
2046
				'groups' => $js_groups,
2047
				'labels' => $js_labels,
2048
				'rules' => $js_rules,
2049
				'txt_pm_readable_and' => $txt['pm_readable_and'],
2050
				'txt_pm_readable_or' => $txt['pm_readable_or'],
2051
				'txt_pm_readable_member' => $txt['pm_readable_member'],
2052
				'txt_pm_readable_group' => $txt['pm_readable_group'],
2053
				'txt_pm_readable_subject ' => $txt['pm_readable_subject'],
2054
				'txt_pm_readable_body' => $txt['pm_readable_body'],
2055
				'txt_pm_readable_buddy' => $txt['pm_readable_buddy'],
2056
				'txt_pm_readable_label' => $txt['pm_readable_label'],
2057
				'txt_pm_readable_delete' => $txt['pm_readable_delete'],
2058
				'txt_pm_readable_start' => $txt['pm_readable_start'],
2059
				'txt_pm_readable_end' => $txt['pm_readable_end'],
2060
				'txt_pm_readable_then' => $txt['pm_readable_then'],
2061
				'txt_pm_rule_not_defined' => $txt['pm_rule_not_defined'],
2062
				'txt_pm_rule_criteria_pick' => $txt['pm_rule_criteria_pick'],
2063
				'txt_pm_rule_sel_group' => $txt['pm_rule_sel_group'],
2064
				'txt_pm_rule_sel_action' => $txt['pm_rule_sel_action'],
2065
				'txt_pm_rule_label' => $txt['pm_rule_label'],
2066
				'txt_pm_rule_delete' => $txt['pm_rule_delete'],
2067
				'txt_pm_rule_sel_label' => $txt['pm_rule_sel_label'],
2068
			), true);
2069
2070
			// Current rule information...
2071
			if ($context['rid'])
2072
			{
2073
				$context['rule'] = $context['rules'][$context['rid']];
2074
				$members = array();
2075
2076
				// Need to get member names!
2077
				foreach ($context['rule']['criteria'] as $k => $criteria)
2078
					if ($criteria['t'] === 'mid' && !empty($criteria['v']))
2079
					{
2080
						$members[(int) $criteria['v']] = $k;
2081
					}
2082
2083
				if (!empty($members))
2084
				{
2085
					require_once(SUBSDIR . '/Members.subs.php');
2086
					$result = getBasicMemberData(array_keys($members));
2087
					foreach ($result as $row)
2088
						$context['rule']['criteria'][$members[$row['id_member']]]['v'] = $row['member_name'];
2089
				}
2090
			}
2091
			else
2092
			{
2093
				$context['rule'] = array(
2094
					'id' => '',
2095
					'name' => '',
2096
					'criteria' => array(),
2097
					'actions' => array(),
2098
					'logic' => 'and',
2099
				);
2100
			}
2101
2102
			// Add a dummy criteria to allow expansion for none js users.
2103
			$context['rule']['criteria'][] = array('t' => '', 'v' => '');
2104
		}
2105
		// Saving?
2106
		elseif (isset($this->_req->query->save))
2107
		{
2108
			checkSession('post');
2109
			$context['rid'] = isset($this->_req->query->rid) && isset($context['rules'][$this->_req->query->rid]) ? (int) $this->_req->query->rid : 0;
2110
2111
			// Name is easy!
2112
			$ruleName = Util::htmlspecialchars(trim($this->_req->post->rule_name));
2113
			if (empty($ruleName))
2114
			{
2115
				Errors::instance()->fatal_lang_error('pm_rule_no_name', false);
2116
			}
2117
2118
			// Sanity check...
2119
			if (empty($this->_req->post->ruletype) || empty($this->_req->post->acttype))
2120
			{
2121
				Errors::instance()->fatal_lang_error('pm_rule_no_criteria', false);
2122
			}
2123
2124
			// Let's do the criteria first - it's also hardest!
2125
			$criteria = array();
2126
			foreach ($this->_req->post->ruletype as $ind => $type)
2127
			{
2128
				// Check everything is here...
2129
				if ($type === 'gid' && (!isset($this->_req->post->ruledefgroup[$ind]) || !isset($context['groups'][$this->_req->post->ruledefgroup[$ind]])))
2130
				{
2131
					continue;
2132
				}
2133
				elseif ($type != 'bud' && !isset($this->_req->post->ruledef[$ind]))
2134
				{
2135
					continue;
2136
				}
2137
2138
				// Members need to be found.
2139
				if ($type === 'mid')
2140
				{
2141
					require_once(SUBSDIR . '/Members.subs.php');
2142
					$name = trim($this->_req->post->ruledef[$ind]);
2143
					$member = getMemberByName($name, true);
2144
					if (empty($member))
2145
					{
2146
						continue;
2147
					}
2148
2149
					$criteria[] = array('t' => 'mid', 'v' => $member['id_member']);
2150
				}
2151
				elseif ($type === 'bud')
2152
				{
2153
					$criteria[] = array('t' => 'bud', 'v' => 1);
2154
				}
2155
				elseif ($type === 'gid')
2156
				{
2157
					$criteria[] = array('t' => 'gid', 'v' => (int) $this->_req->post->ruledefgroup[$ind]);
2158
				}
2159
				elseif (in_array($type, array('sub', 'msg')) && trim($this->_req->post->ruledef[$ind]) != '')
2160
				{
2161
					$criteria[] = array('t' => $type, 'v' => Util::htmlspecialchars(trim($this->_req->post->ruledef[$ind])));
2162
				}
2163
			}
2164
2165
			// Also do the actions!
2166
			$actions = array();
2167
			$doDelete = 0;
2168
			$isOr = $this->_req->post->rule_logic === 'or' ? 1 : 0;
2169
			foreach ($this->_req->post->acttype as $ind => $type)
2170
			{
2171
				// Picking a valid label?
2172
				if ($type === 'lab' && (!isset($this->_req->post->labdef[$ind]) || !isset($context['labels'][$this->_req->post->labdef[$ind] - 1])))
2173
				{
2174
					continue;
2175
				}
2176
2177
				// Record what we're doing.
2178
				if ($type === 'del')
2179
				{
2180
					$doDelete = 1;
2181
				}
2182
				elseif ($type === 'lab')
2183
				{
2184
					$actions[] = array('t' => 'lab', 'v' => (int) $this->_req->post->labdef[$ind] - 1);
2185
				}
2186
			}
2187
2188
			if (empty($criteria) || (empty($actions) && !$doDelete))
2189
			{
2190
				Errors::instance()->fatal_lang_error('pm_rule_no_criteria', false);
2191
			}
2192
2193
			// What are we storing?
2194
			$criteria = serialize($criteria);
2195
			$actions = serialize($actions);
2196
2197
			// Create the rule?
2198
			if (empty($context['rid']))
2199
			{
2200
				addPMRule($user_info['id'], $ruleName, $criteria, $actions, $doDelete, $isOr);
2201
			}
2202
			else
2203
			{
2204
				updatePMRule($user_info['id'], $context['rid'], $ruleName, $criteria, $actions, $doDelete, $isOr);
2205
			}
2206
2207
			redirectexit('action=pm;sa=manrules');
2208
		}
2209
		// Deleting?
2210
		elseif (isset($this->_req->post->delselected) && !empty($this->_req->post->delrule))
2211
		{
2212
			checkSession('post');
2213
			$toDelete = array();
2214
			foreach ($this->_req->post->delrule as $k => $v)
2215
				$toDelete[] = (int) $k;
2216
2217
			if (!empty($toDelete))
2218
			{
2219
				deletePMRules($user_info['id'], $toDelete);
2220
			}
2221
2222
			redirectexit('action=pm;sa=manrules');
2223
		}
2224
	}
2225
2226
	/**
2227
	 * Actually do the search of personal messages and show the results
2228
	 *
2229
	 * What it does:
2230
	 * - accessed with ?action=pm;sa=search2
2231
	 * - checks user input and searches the pm table for messages matching the query.
2232
	 * - uses the search_results sub template of the PersonalMessage template.
2233
	 * - show the results of the search query.
2234
	 */
2235
	public function action_search2()
2236
	{
2237
		global $scripturl, $modSettings, $context, $txt, $memberContext;
2238
2239
		// Make sure the server is able to do this right now
2240
		if (!empty($modSettings['loadavg_search']) && $modSettings['current_load'] >= $modSettings['loadavg_search'])
2241
		{
2242
			Errors::instance()->fatal_lang_error('loadavg_search_disabled', false);
2243
		}
2244
2245
		// Some useful general permissions.
2246
		$context['can_send_pm'] = allowedTo('pm_send');
2247
2248
		// Extract all the search parameters if coming in from pagination, etc
2249
		$this->_searchParamsFromString();
2250
2251
		// Set a start for pagination
2252
		$context['start'] = $this->_req->getQuery('start', 'intval', 0);
2253
2254
		// Set/clean search criteria
2255
		$this->_prepareSearchParams();
2256
2257
		$context['folder'] = empty($this->_search_params['sent_only']) ? 'inbox' : 'sent';
2258
2259
		// Searching for specific members
2260
		$userQuery = $this->_setUserQuery();
2261
2262
		// Setup the sorting variables...
2263
		$this->_setSortParams();
2264
2265
		// Sort out any labels we may be searching by.
2266
		$labelQuery = $this->_setLabelQuery();
2267
2268
		// Unfortunately, searching for words like this is going to be slow, so we're blacklisting them.
2269
		$blacklisted_words = array('quote', 'the', 'is', 'it', 'are', 'if');
2270
2271
		// What are we actually searching for?
2272
		$this->_search_params['search'] = !empty($this->_search_params['search']) ? $this->_search_params['search'] : (isset($this->_req->post->search) ? $this->_req->post->search : '');
2273
2274
		// If nothing is left to search on - we set an error!
2275
		if (!isset($this->_search_params['search']) || $this->_search_params['search'] === '')
2276
		{
2277
			$context['search_errors']['invalid_search_string'] = true;
2278
		}
2279
2280
		// Change non-word characters into spaces.
2281
		$stripped_query = preg_replace('~(?:[\x0B\0\x{A0}\t\r\s\n(){}\\[\\]<>!@$%^*.,:+=`\~\?/\\\\]+|&(?:amp|lt|gt|quot);)+~u', ' ', $this->_search_params['search']);
2282
2283
		// Make the query lower case since it will case insensitive anyway.
2284
		$stripped_query = un_htmlspecialchars(Util::strtolower($stripped_query));
2285
2286
		// Extract phrase parts first (e.g. some words "this is a phrase" some more words.)
2287
		preg_match_all('/(?:^|\s)([-]?)"([^"]+)"(?:$|\s)/', $stripped_query, $matches, PREG_PATTERN_ORDER);
2288
		$phraseArray = $matches[2];
2289
2290
		// Remove the phrase parts and extract the words.
2291
		$wordArray = preg_replace('~(?:^|\s)(?:[-]?)"(?:[^"]+)"(?:$|\s)~u', ' ', $this->_search_params['search']);
2292
		$wordArray = explode(' ', Util::htmlspecialchars(un_htmlspecialchars($wordArray), ENT_QUOTES));
2293
2294
		// A minus sign in front of a word excludes the word.... so...
2295
		$excludedWords = array();
2296
2297
		// Check for things like -"some words", but not "-some words".
2298
		foreach ($matches[1] as $index => $word)
2299
		{
2300
			if ($word === '-')
2301
			{
2302
				if (($word = trim($phraseArray[$index], '-_\' ')) !== '' && !in_array($word, $blacklisted_words))
2303
				{
2304
					$excludedWords[] = $word;
2305
				}
2306
				unset($phraseArray[$index]);
2307
			}
2308
		}
2309
2310
		// Now we look for -test, etc
2311
		foreach ($wordArray as $index => $word)
2312
		{
2313
			if (strpos(trim($word), '-') === 0)
2314
			{
2315
				if (($word = trim($word, '-_\' ')) !== '' && !in_array($word, $blacklisted_words))
2316
				{
2317
					$excludedWords[] = $word;
2318
				}
2319
				unset($wordArray[$index]);
2320
			}
2321
		}
2322
2323
		// The remaining words and phrases are all included.
2324
		$searchArray = array_merge($phraseArray, $wordArray);
2325
2326
		// Trim everything and make sure there are no words that are the same.
2327
		foreach ($searchArray as $index => $value)
2328
		{
2329
			// Skip anything that's close to empty.
2330
			if (($searchArray[$index] = trim($value, '-_\' ')) === '')
2331
			{
2332
				unset($searchArray[$index]);
2333
			}
2334
			// Skip blacklisted words. Make sure to note we skipped them as well
2335
			elseif (in_array($searchArray[$index], $blacklisted_words))
2336
			{
2337
				$foundBlackListedWords = true;
2338
				unset($searchArray[$index]);
2339
2340
			}
2341
2342
			if (isset($searchArray[$index]))
2343
			{
2344
				$searchArray[$index] = Util::strtolower(trim($value));
2345
2346
				if ($searchArray[$index] === '')
2347
				{
2348
					unset($searchArray[$index]);
2349
				}
2350
				else
2351
				{
2352
					// Sort out entities first.
2353
					$searchArray[$index] = Util::htmlspecialchars($searchArray[$index]);
2354
				}
2355
			}
2356
		}
2357
2358
		$searchArray = array_slice(array_unique($searchArray), 0, 10);
2359
2360
		// This contains *everything*
2361
		$searchWords = array_merge($searchArray, $excludedWords);
2362
2363
		// Make sure at least one word is being searched for.
2364
		if (empty($searchArray))
2365
		{
2366
			$context['search_errors']['invalid_search_string' . (!empty($foundBlackListedWords) ? '_blacklist' : '')] = true;
2367
		}
2368
2369
		// Sort out the search query so the user can edit it - if they want.
2370
		$context['search_params'] = $this->_search_params;
2371
		if (isset($context['search_params']['search']))
2372
		{
2373
			$context['search_params']['search'] = Util::htmlspecialchars($context['search_params']['search']);
2374
		}
2375
2376
		if (isset($context['search_params']['userspec']))
2377
		{
2378
			$context['search_params']['userspec'] = Util::htmlspecialchars($context['search_params']['userspec']);
2379
		}
2380
2381
		// Now we have all the parameters, combine them together for pagination and the like...
2382
		$context['params'] = $this->_compileURLparams();
2383
2384
		// Compile the subject query part.
2385
		$andQueryParts = array();
2386
		foreach ($searchWords as $index => $word)
2387
		{
2388
			if ($word === '')
2389
			{
2390
				continue;
2391
			}
2392
2393
			if ($this->_search_params['subject_only'])
2394
			{
2395
				$andQueryParts[] = 'pm.subject' . (in_array($word, $excludedWords) ? ' NOT' : '') . ' LIKE {string:search_' . $index . '}';
2396
			}
2397
			else
2398
			{
2399
				$andQueryParts[] = '(pm.subject' . (in_array($word, $excludedWords) ? ' NOT' : '') . ' LIKE {string:search_' . $index . '} ' . (in_array($word, $excludedWords) ? 'AND pm.body NOT' : 'OR pm.body') . ' LIKE {string:search_' . $index . '})';
2400
			}
2401
2402
			$this->_searchq_parameters ['search_' . $index] = '%' . strtr($word, array('_' => '\\_', '%' => '\\%')) . '%';
2403
		}
2404
2405
		$searchQuery = ' 1=1';
2406
		if (!empty($andQueryParts))
2407
		{
2408
			$searchQuery = implode(!empty($this->_search_params['searchtype']) && $this->_search_params['searchtype'] == 2 ? ' OR ' : ' AND ', $andQueryParts);
2409
		}
2410
2411
		// Age limits?
2412
		$timeQuery = '';
2413
		if (!empty($this->_search_params['minage']))
2414
		{
2415
			$timeQuery .= ' AND pm.msgtime < ' . (time() - $this->_search_params['minage'] * 86400);
2416
		}
2417
2418
		if (!empty($this->_search_params['maxage']))
2419
		{
2420
			$timeQuery .= ' AND pm.msgtime > ' . (time() - $this->_search_params['maxage'] * 86400);
2421
		}
2422
2423
		// If we have errors - return back to the first screen...
2424
		if (!empty($context['search_errors']))
2425
		{
2426
			$this->_req->post->params = $context['params'];
2427
2428
			$this->action_search();
2429
			return false;
2430
		}
2431
2432
		// Get the number of results.
2433
		$numResults = numPMSeachResults($userQuery, $labelQuery, $timeQuery, $searchQuery, $this->_searchq_parameters);
2434
2435
		// Get all the matching message ids, senders and head pm nodes
2436
		list($foundMessages, $posters, $head_pms) = loadPMSearchMessages($userQuery, $labelQuery, $timeQuery, $searchQuery, $this->_searchq_parameters, $this->_search_params);
2437
2438
		// Find the real head pm when in conversation view
2439
		if ($context['display_mode'] == 2 && !empty($head_pms))
2440
		{
2441
			$real_pm_ids = loadPMSearchHeads($head_pms);
2442
		}
2443
2444
		// Load the found user data
2445
		$posters = array_unique($posters);
2446
		if (!empty($posters))
2447
		{
2448
			loadMemberData($posters);
2449
		}
2450
2451
		// Sort out the page index.
2452
		$context['page_index'] = constructPageIndex($scripturl . '?action=pm;sa=search2;params=' . $context['params'], $this->_req->query->start, $numResults, $modSettings['search_results_per_page'], false);
2453
2454
		$context['message_labels'] = array();
2455
		$context['message_replied'] = array();
2456
		$context['personal_messages'] = array();
2457
		$context['first_label'] = array();
2458
2459
		// If we have results, we have work to do!
2460
		if (!empty($foundMessages))
2461
		{
2462
			$recipients = array();
2463
			list($context['message_labels'], $context['message_replied'], $context['message_unread'], $context['first_label']) = loadPMRecipientInfo($foundMessages, $recipients, $context['folder'], true);
2464
2465
			// Prepare for the callback!
2466
			$search_results = loadPMSearchResults($foundMessages, $this->_search_params);
2467
			$counter = 0;
2468
			$bbc_parser = \BBC\ParserWrapper::getInstance();
2469
			foreach ($search_results as $row)
2470
			{
2471
				// If there's no subject, use the default.
2472
				$row['subject'] = $row['subject'] === '' ? $txt['no_subject'] : $row['subject'];
2473
2474
				// Load this posters context info, if its not there then fill in the essentials...
2475
				if (!loadMemberContext($row['id_member_from'], true))
2476
				{
2477
					$memberContext[$row['id_member_from']]['name'] = $row['from_name'];
2478
					$memberContext[$row['id_member_from']]['id'] = 0;
2479
					$memberContext[$row['id_member_from']]['group'] = $txt['guest_title'];
2480
					$memberContext[$row['id_member_from']]['link'] = $row['from_name'];
2481
					$memberContext[$row['id_member_from']]['email'] = '';
2482
					$memberContext[$row['id_member_from']]['show_email'] = showEmailAddress(true, 0);
2483
					$memberContext[$row['id_member_from']]['is_guest'] = true;
2484
				}
2485
2486
				// Censor anything we don't want to see...
2487
				censorText($row['body']);
2488
				censorText($row['subject']);
2489
2490
				// Parse out any BBC...
2491
				$row['body'] = $bbc_parser->parsePM($row['body']);
2492
2493
				// Highlight the hits
2494
				$body_highlighted = '';
2495
				$subject_highlighted = '';
2496
				foreach ($searchArray as $query)
2497
				{
2498
					// Fix the international characters in the keyword too.
2499
					$query = un_htmlspecialchars($query);
2500
					$query = trim($query, '\*+');
2501
					$query = strtr(Util::htmlspecialchars($query), array('\\\'' => '\''));
2502
2503
					$body_highlighted = preg_replace_callback('/((<[^>]*)|' . preg_quote(strtr($query, array('\'' => '&#039;')), '/') . ')/iu', array($this, '_highlighted_callback'), $row['body']);
2504
					$subject_highlighted = preg_replace('/(' . preg_quote($query, '/') . ')/iu', '<strong class="highlight">$1</strong>', $row['subject']);
2505
				}
2506
2507
				// Set a link using the first label information
2508
				$href = $scripturl . '?action=pm;f=' . $context['folder'] . (isset($context['first_label'][$row['id_pm']]) ? ';l=' . $context['first_label'][$row['id_pm']] : '') . ';pmid=' . ($context['display_mode'] == 2 && isset($real_pm_ids[$head_pms[$row['id_pm']]]) && $context['folder'] === 'inbox' ? $real_pm_ids[$head_pms[$row['id_pm']]] : $row['id_pm']) . '#msg_' . $row['id_pm'];
2509
2510
				$context['personal_messages'][] = array(
2511
					'id' => $row['id_pm'],
2512
					'member' => &$memberContext[$row['id_member_from']],
2513
					'subject' => $subject_highlighted,
2514
					'body' => $body_highlighted,
2515
					'time' => standardTime($row['msgtime']),
2516
					'html_time' => htmlTime($row['msgtime']),
2517
					'timestamp' => forum_time(true, $row['msgtime']),
2518
					'recipients' => &$recipients[$row['id_pm']],
2519
					'labels' => &$context['message_labels'][$row['id_pm']],
2520
					'fully_labeled' => count($context['message_labels'][$row['id_pm']]) == count($context['labels']),
2521
					'is_replied_to' => &$context['message_replied'][$row['id_pm']],
2522
					'href' => $href,
2523
					'link' => '<a href="' . $href . '">' . $subject_highlighted . '</a>',
2524
					'counter' => ++$counter,
2525
				);
2526
			}
2527
		}
2528
2529
		// Finish off the context.
2530
		$context['page_title'] = $txt['pm_search_title'];
2531
		$context['sub_template'] = 'search_results';
2532
		$context['menu_data_' . $context['pm_menu_id']]['current_area'] = 'search';
2533
		$context['linktree'][] = array(
2534
			'url' => $scripturl . '?action=pm;sa=search',
2535
			'name' => $txt['pm_search_bar_title'],
2536
		);
2537
	}
2538
2539
	/**
2540
	 * Read / Set the sort parameters for the results listing
2541
	 */
2542
	private function _setSortParams()
2543
	{
2544
		$sort_columns = array(
2545
			'pm.id_pm',
2546
		);
2547
2548
		if (empty($this->_search_params['sort']) && !empty($this->_req->post->sort))
2549
		{
2550
			list ($this->_search_params['sort'], $this->_search_params['sort_dir']) = array_pad(explode('|', $this->_req->post->sort), 2, '');
2551
		}
2552
2553
		$this->_search_params['sort'] = !empty($this->_search_params['sort']) && in_array($this->_search_params['sort'], $sort_columns) ? $this->_search_params['sort'] : 'pm.id_pm';
2554
		$this->_search_params['sort_dir'] = !empty($this->_search_params['sort_dir']) && $this->_search_params['sort_dir'] === 'asc' ? 'asc' : 'desc';
2555
	}
2556
2557
	/**
2558
	 * Handles the parameters when searching on specific labels
2559
	 *
2560
	 * What it does:
2561
	 * - Returns the label query for use in the main search query
2562
	 * - Sets the parameters for use in the query
2563
	 *
2564
	 * @return string
2565
	 */
2566
	private function _setLabelQuery()
2567
	{
2568
		global $context;
2569
2570
		$db = database();
2571
2572
		$labelQuery = '';
2573
2574
		if ($context['folder'] === 'inbox' && !empty($this->_search_params['advanced']) && $context['currently_using_labels'])
2575
		{
2576
			// Came here from pagination?  Put them back into $_REQUEST for sanitation.
2577
			if (isset($this->_search_params['labels']))
2578
			{
2579
				$this->_req->post->searchlabel = explode(',', $this->_search_params['labels']);
2580
			}
2581
2582
			// Assuming we have some labels - make them all integers.
2583
			if (!empty($this->_req->post->searchlabel) && is_array($this->_req->post->searchlabel))
2584
			{
2585
				$this->_req->post->searchlabel = array_map('intval', $this->_req->post->searchlabel);
2586
			}
2587
			else
2588
			{
2589
				$this->_req->post->searchlabel = array();
2590
			}
2591
2592
			// Now that everything is cleaned up a bit, make the labels a param.
2593
			$this->_search_params['labels'] = implode(',', $this->_req->post->searchlabel);
2594
2595
			// No labels selected? That must be an error!
2596
			if (empty($this->_req->post->searchlabel))
2597
			{
2598
				$context['search_errors']['no_labels_selected'] = true;
2599
			}
2600
			// Otherwise prepare the query!
2601
			elseif (count($this->_req->post->searchlabel) != count($context['labels']))
2602
			{
2603
				$labelQuery = '
2604
				AND {raw:label_implode}';
2605
2606
				$labelStatements = array();
2607
				foreach ($this->_req->post->searchlabel as $label)
2608
					$labelStatements[] = $db->quote('FIND_IN_SET({string:label}, pmr.labels) != 0', array('label' => $label,));
2609
2610
				$this->_searchq_parameters ['label_implode'] = '(' . implode(' OR ', $labelStatements) . ')';
2611
			}
2612
		}
2613
2614
		return $labelQuery;
2615
	}
2616
2617
	/**
2618
	 * Handles the parameters when searching on specific users
2619
	 *
2620
	 * What it does:
2621
	 * - Returns the user query for use in the main search query
2622
	 * - Sets the parameters for use in the query
2623
	 *
2624
	 * @return string
2625
	 */
2626
	private function _setUserQuery()
2627
	{
2628
		global $context;
2629
2630
		// Hardcoded variables that can be tweaked if required.
2631
		$maxMembersToSearch = 500;
2632
2633
		// Init to not be searching based on members
2634
		$userQuery = '';
2635
2636
		// If there's no specific user, then don't mention it in the main query.
2637
		if (!empty($this->_search_params['userspec']))
2638
		{
2639
			// Set up so we can search by user name, wildcards, like, etc
2640
			$userString = strtr(Util::htmlspecialchars($this->_search_params['userspec'], ENT_QUOTES), array('&quot;' => '"'));
2641
			$userString = strtr($userString, array('%' => '\%', '_' => '\_', '*' => '%', '?' => '_'));
2642
2643
			preg_match_all('~"([^"]+)"~', $userString, $matches);
2644
			$possible_users = array_merge($matches[1], explode(',', preg_replace('~"[^"]+"~', '', $userString)));
2645
2646
			// Who matches those criteria?
2647
			require_once(SUBSDIR . '/Members.subs.php');
2648
			$members = membersBy('member_names', array('member_names' => $possible_users));
2649
2650
			foreach ($possible_users as $key => $possible_user)
2651
				$this->_searchq_parameters ['guest_user_name_implode_' . $key] = defined('DB_CASE_SENSITIVE') ? strtolower($possible_user) : $possible_user;
2652
2653
			// Simply do nothing if there are too many members matching the criteria.
2654
			if (count($members) > $maxMembersToSearch)
2655
			{
2656
				$userQuery = '';
2657
			}
2658
			elseif (count($members) == 0)
2659
			{
2660
				if ($context['folder'] === 'inbox')
2661
				{
2662
					$uq = array();
2663
					$name = defined('DB_CASE_SENSITIVE') ? 'LOWER(pm.from_name)' : 'pm.from_name';
2664
					foreach (array_keys($possible_users) as $key)
2665
						$uq[] = 'AND pm.id_member_from = 0 AND (' . $name . ' LIKE {string:guest_user_name_implode_' . $key . '})';
2666
					$userQuery = implode(' ', $uq);
2667
					$this->_searchq_parameters ['pm_from_name'] = defined('DB_CASE_SENSITIVE') ? 'LOWER(pm.from_name)' : 'pm.from_name';
2668
				}
2669
				else
2670
				{
2671
					$userQuery = '';
2672
				}
2673
			}
2674
			else
2675
			{
2676
				$memberlist = array();
2677
				foreach ($members as $id)
2678
					$memberlist[] = $id;
2679
2680
				// Use the name as as sent from or sent to
2681
				if ($context['folder'] === 'inbox')
2682
				{
2683
					$uq = array();
2684
					$name = defined('DB_CASE_SENSITIVE') ? 'LOWER(pm.from_name)' : 'pm.from_name';
2685
2686
					foreach (array_keys($possible_users) as $key)
2687
						$uq[] = 'AND (pm.id_member_from IN ({array_int:member_list}) OR (pm.id_member_from = 0 AND (' . $name . ' LIKE {string:guest_user_name_implode_' . $key . '})))';
2688
2689
					$userQuery = implode(' ', $uq);
2690
				}
2691
				else
2692
				{
2693
					$userQuery = 'AND (pmr.id_member IN ({array_int:member_list}))';
2694
				}
2695
2696
				$this->_searchq_parameters ['pm_from_name'] = defined('DB_CASE_SENSITIVE') ? 'LOWER(pm.from_name)' : 'pm.from_name';
2697
				$this->_searchq_parameters ['member_list'] = $memberlist;
2698
			}
2699
		}
2700
2701
		return $userQuery;
2702
	}
2703
2704
	/**
2705
	 * Sets the search params for the query
2706
	 *
2707
	 * What it does:
2708
	 * - Uses existing ones if coming from pagination or uses those passed from the search pm form
2709
	 * - Validates passed params are valid
2710
	 */
2711
	private function _prepareSearchParams()
2712
	{
2713
		// Store whether simple search was used (needed if the user wants to do another query).
2714
		if (!isset($this->_search_params['advanced']))
2715
		{
2716
			$this->_search_params['advanced'] = empty($this->_req->post->advanced) ? 0 : 1;
2717
		}
2718
2719
		// 1 => 'allwords' (default, don't set as param),  2 => 'anywords'.
2720
		if (!empty($this->_search_params['searchtype']) || (!empty($this->_req->post->searchtype) && $this->_req->post->searchtype == 2))
2721
		{
2722
			$this->_search_params['searchtype'] = 2;
2723
		}
2724
2725
		// Minimum age of messages. Default to zero (don't set param in that case).
2726
		if (!empty($this->_search_params['minage']) || (!empty($this->_req->post->minage) && $this->_req->post->minage > 0))
2727
		{
2728
			$this->_search_params['minage'] = !empty($this->_search_params['minage']) ? (int) $this->_search_params['minage'] : (int) $this->_req->post->minage;
2729
		}
2730
2731
		// Maximum age of messages. Default to infinite (9999 days: param not set).
2732
		if (!empty($this->_search_params['maxage']) || (!empty($this->_req->post->maxage) && $this->_req->post->maxage < 9999))
2733
		{
2734
			$this->_search_params['maxage'] = !empty($this->_search_params['maxage']) ? (int) $this->_search_params['maxage'] : (int) $this->_req->post->maxage;
2735
		}
2736
2737
		// Default the user name to a wildcard matching every user (*).
2738
		if (!empty($this->_search_params['userspec']) || (!empty($this->_req->post->userspec) && $this->_req->post->userspec != '*'))
2739
		{
2740
			$this->_search_params['userspec'] = isset($this->_search_params['userspec']) ? $this->_search_params['userspec'] : $this->_req->post->userspec;
2741
		}
2742
2743
		// Search modifiers
2744
		$this->_search_params['subject_only'] = !empty($this->_search_params['subject_only']) || !empty($this->_req->post->subject_only);
2745
		$this->_search_params['show_complete'] = !empty($this->_search_params['show_complete']) || !empty($this->_req->post->show_complete);
2746
		$this->_search_params['sent_only'] = !empty($this->_search_params['sent_only']) || !empty($this->_req->post->sent_only);
2747
	}
2748
2749
	/**
2750
	 * Extract search params from a string
2751
	 *
2752
	 * What it does:
2753
	 * - When paging search results, reads and decodes the passed parameters
2754
	 * - Places what it finds back in search_params
2755
	 */
2756
	private function _searchParamsFromString()
2757
	{
2758
		$this->_search_params = array();
2759
2760
		if (isset($this->_req->query->params) || isset($this->_req->post->params))
2761
		{
2762
			// Feed it
2763
			$temp_params = isset($this->_req->query->params) ? $this->_req->query->params : $this->_req->post->params;
2764
2765
			// Decode and replace the uri safe characters we added
2766
			$temp_params = base64_decode(str_replace(array('-', '_', '.'), array('+', '/', '='), $temp_params));
2767
2768
			$temp_params = explode('|"|', $temp_params);
2769
			foreach ($temp_params as $i => $data)
2770
			{
2771
				list ($k, $v) = array_pad(explode('|\'|', $data), 2, '');
2772
				$this->_search_params[$k] = $v;
2773
			}
2774
		}
2775
2776
		return $this->_search_params;
2777
	}
2778
2779
	/**
2780
	 * Encodes search params in an URL-compatible way
2781
	 *
2782
	 * @return string - the encoded string to be appended to the URL
2783
	 */
2784
	private function _compileURLparams()
2785
	{
2786
		$encoded = array();
2787
2788
		// Now we have all the parameters, combine them together for pagination and the like...
2789
		foreach ($this->_search_params as $k => $v)
2790
			$encoded[] = $k . '|\'|' . $v;
2791
2792
		// Base64 encode, then replace +/= with uri safe ones that can be reverted
2793
		$encoded = str_replace(array('+', '/', '='), array('-', '_', '.'), base64_encode(implode('|"|', $encoded)));
2794
2795
		return $encoded;
2796
	}
2797
2798
	/**
2799
	 * Allows to search through personal messages.
2800
	 *
2801
	 * What it does:
2802
	 * - accessed with ?action=pm;sa=search
2803
	 * - shows the screen to search pm's (?action=pm;sa=search)
2804
	 * - uses the search sub template of the PersonalMessage template.
2805
	 * - decodes and loads search parameters given in the URL (if any).
2806
	 * - the form redirects to index.php?action=pm;sa=search2.
2807
	 *
2808
	 * @uses search sub template
2809
	 */
2810
	public function action_search()
2811
	{
2812
		global $context, $txt, $scripturl;
2813
2814
		// If they provided some search parameters, we need to extract them
2815
		if (isset($this->_req->post->params))
2816
		{
2817
			$context['search_params'] = $this->_searchParamsFromString();
2818
		}
2819
2820
		// Set up the search criteria, type, what, age, etc
2821
		if (isset($this->_req->post->search))
2822
		{
2823
			$context['search_params']['search'] = un_htmlspecialchars($this->_req->post->search);
2824
			$context['search_params']['search'] = htmlspecialchars($context['search_params']['search'], ENT_COMPAT, 'UTF-8');
2825
		}
2826
2827
		if (isset($context['search_params']['userspec']))
2828
		{
2829
			$context['search_params']['userspec'] = htmlspecialchars($context['search_params']['userspec'], ENT_COMPAT, 'UTF-8');
2830
		}
2831
2832
		// 1 => 'allwords' / 2 => 'anywords'.
0 ignored issues
show
Unused Code Comprehensibility introduced by
40% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
2833
		if (!empty($context['search_params']['searchtype']))
2834
		{
2835
			$context['search_params']['searchtype'] = 2;
2836
		}
2837
2838
		// Minimum and Maximum age of the message
2839
		if (!empty($context['search_params']['minage']))
2840
		{
2841
			$context['search_params']['minage'] = (int) $context['search_params']['minage'];
2842
		}
2843
		if (!empty($context['search_params']['maxage']))
2844
		{
2845
			$context['search_params']['maxage'] = (int) $context['search_params']['maxage'];
2846
		}
2847
2848
		$context['search_params']['show_complete'] = !empty($context['search_params']['show_complete']);
2849
		$context['search_params']['subject_only'] = !empty($context['search_params']['subject_only']);
2850
2851
		// Create the array of labels to be searched.
2852
		$context['search_labels'] = array();
2853
		$searchedLabels = isset($context['search_params']['labels']) && $context['search_params']['labels'] != '' ? explode(',', $context['search_params']['labels']) : array();
2854
		foreach ($context['labels'] as $label)
2855
		{
2856
			$context['search_labels'][] = array(
2857
				'id' => $label['id'],
2858
				'name' => $label['name'],
2859
				'checked' => !empty($searchedLabels) ? in_array($label['id'], $searchedLabels) : true,
2860
			);
2861
		}
2862
2863
		// Are all the labels checked?
2864
		$context['check_all'] = empty($searchedLabels) || count($context['search_labels']) == count($searchedLabels);
2865
2866
		// Load the error text strings if there were errors in the search.
2867
		if (!empty($context['search_errors']))
2868
		{
2869
			loadLanguage('Errors');
2870
			$context['search_errors']['messages'] = array();
2871
			foreach ($context['search_errors'] as $search_error => $dummy)
0 ignored issues
show
Bug introduced by
The expression $context['search_errors'] of type array|boolean is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
2872
			{
2873
				if ($search_error === 'messages')
2874
				{
2875
					continue;
2876
				}
2877
2878
				$context['search_errors']['messages'][] = $txt['error_' . $search_error];
2879
			}
2880
		}
2881
2882
		$context['page_title'] = $txt['pm_search_title'];
2883
		$context['sub_template'] = 'search';
2884
		$context['linktree'][] = array(
2885
			'url' => $scripturl . '?action=pm;sa=search',
2886
			'name' => $txt['pm_search_bar_title'],
2887
		);
2888
	}
2889
2890
	/**
2891
	 * Allows the user to mark a personal message as unread so they remember to come back to it
2892
	 */
2893
	public function action_markunread()
2894
	{
2895
		global $context;
2896
2897
		checkSession('request');
2898
2899
		$pmsg = !empty($this->_req->query->pmsg) ? (int) $this->_req->query->pmsg : null;
2900
2901
		// Marking a message as unread, we need a message that was sent to them
2902
		// Can't mark your own reply as unread, that would be weird
2903
		if (!is_null($pmsg) && checkPMReceived($pmsg))
2904
		{
2905
			// Make sure this is accessible, should be of course
2906
			if (!isAccessiblePM($pmsg, 'inbox'))
2907
			{
2908
				Errors::instance()->fatal_lang_error('no_access', false);
2909
			}
2910
2911
			// Well then, you get to hear about it all over again
2912
			markMessagesUnread($pmsg);
2913
		}
2914
2915
		// Back to the folder.
2916
		redirectexit($context['current_label_redirect']);
2917
	}
2918
2919
	/**
2920
	 * Used to highlight body text with strings that match the search term
2921
	 *
2922
	 * - Callback function used in $body_highlighted
2923
	 *
2924
	 * @param string[] $matches
2925
	 */
2926
	private function _highlighted_callback($matches)
2927
	{
2928
		return isset($matches[2]) && $matches[2] == $matches[1] ? stripslashes($matches[1]) : '<strong class="highlight">' . $matches[1] . '</strong>';
2929
	}
2930
}
2931
2932
/**
2933
 * Get a personal message for the template. (used to save memory.)
2934
 *
2935
 * - This is a callback function that will fetch the actual results, as needed, of a previously run
2936
 * subject (loadPMSubjectRequest) or message (loadPMMessageRequest) query.
2937
 *
2938
 * @param string $type
2939
 * @param boolean $reset
2940
 */
2941
function preparePMContext_callback($type = 'subject', $reset = false)
0 ignored issues
show
Coding Style introduced by
preparePMContext_callback uses the super-global variable $_SESSION which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
2942
{
2943
	global $txt, $scripturl, $modSettings, $settings, $context, $memberContext, $recipients, $user_info;
2944
	global $subjects_request, $messages_request;
2945
	static $counter = null, $temp_pm_selected = null;
2946
2947
	// We need this
2948
	$db = database();
2949
2950
	// Count the current message number....
2951
	if ($counter === null || $reset)
2952
	{
2953
		$counter = $context['start'];
2954
	}
2955
2956
	if ($temp_pm_selected === null)
2957
	{
2958
		$temp_pm_selected = isset($_SESSION['pm_selected']) ? $_SESSION['pm_selected'] : array();
2959
		$_SESSION['pm_selected'] = array();
2960
	}
2961
2962
	// If we're in non-boring view do something exciting!
2963
	if ($context['display_mode'] != 0 && $subjects_request && $type === 'subject')
2964
	{
2965
		$subject = $db->fetch_assoc($subjects_request);
2966
		if (!$subject)
2967
		{
2968
			$db->free_result($subjects_request);
2969
2970
			return false;
2971
		}
2972
2973
		// Make sure we have a subject
2974
		$subject['subject'] = $subject['subject'] === '' ? $txt['no_subject'] : $subject['subject'];
2975
		censorText($subject['subject']);
2976
2977
		$output = array(
2978
			'id' => $subject['id_pm'],
2979
			'member' => array(
2980
				'id' => $subject['id_member_from'],
2981
				'name' => $subject['from_name'],
2982
				'link' => $subject['not_guest'] ? '<a href="' . $scripturl . '?action=profile;u=' . $subject['id_member_from'] . '">' . $subject['from_name'] . '</a>' : $subject['from_name'],
2983
			),
2984
			'recipients' => &$recipients[$subject['id_pm']],
2985
			'subject' => $subject['subject'],
2986
			'time' => standardTime($subject['msgtime']),
2987
			'html_time' => htmlTime($subject['msgtime']),
2988
			'timestamp' => forum_time(true, $subject['msgtime']),
2989
			'number_recipients' => count($recipients[$subject['id_pm']]['to']),
2990
			'labels' => &$context['message_labels'][$subject['id_pm']],
2991
			'fully_labeled' => count($context['message_labels'][$subject['id_pm']]) == count($context['labels']),
2992
			'is_replied_to' => &$context['message_replied'][$subject['id_pm']],
2993
			'is_unread' => &$context['message_unread'][$subject['id_pm']],
2994
			'is_selected' => !empty($temp_pm_selected) && in_array($subject['id_pm'], $temp_pm_selected),
2995
		);
2996
2997
		// In conversation view we need to indicate on the subject listing if any message inside of
2998
		// that conversation is unread, not just if the latest is unread.
2999
		if ($context['display_mode'] == 2 && isset($context['conversation_unread'][$output['id']]))
3000
		{
3001
			$output['is_unread'] = true;
3002
		}
3003
3004
		return $output;
3005
	}
3006
3007
	// Bail if it's false, ie. no messages.
3008
	if ($messages_request == false)
3009
	{
3010
		return false;
3011
	}
3012
3013
	// Reset the data?
3014
	if ($reset === true)
3015
	{
3016
		return $db->data_seek($messages_request, 0);
3017
	}
3018
3019
	// Get the next one... bail if anything goes wrong.
3020
	$message = $db->fetch_assoc($messages_request);
3021
	if (!$message)
3022
	{
3023
		if ($type != 'subject')
3024
		{
3025
			$db->free_result($messages_request);
3026
		}
3027
3028
		return false;
3029
	}
3030
3031
	// Use '(no subject)' if none was specified.
3032
	$message['subject'] = $message['subject'] === '' ? $txt['no_subject'] : $message['subject'];
3033
3034
	// Load the message's information - if it's not there, load the guest information.
3035
	if (!loadMemberContext($message['id_member_from'], true))
3036
	{
3037
		$memberContext[$message['id_member_from']]['name'] = $message['from_name'];
3038
		$memberContext[$message['id_member_from']]['id'] = 0;
3039
3040
		// Sometimes the forum sends messages itself (Warnings are an example) - in this case don't label it from a guest.
3041
		$memberContext[$message['id_member_from']]['group'] = $message['from_name'] == $context['forum_name'] ? '' : $txt['guest_title'];
3042
		$memberContext[$message['id_member_from']]['link'] = $message['from_name'];
3043
		$memberContext[$message['id_member_from']]['email'] = '';
3044
		$memberContext[$message['id_member_from']]['show_email'] = showEmailAddress(true, 0);
3045
		$memberContext[$message['id_member_from']]['is_guest'] = true;
3046
	}
3047
	else
3048
	{
3049
		$memberContext[$message['id_member_from']]['can_view_profile'] = allowedTo('profile_view_any') || ($message['id_member_from'] == $user_info['id'] && allowedTo('profile_view_own'));
3050
		$memberContext[$message['id_member_from']]['can_see_warning'] = !isset($context['disabled_fields']['warning_status']) && $memberContext[$message['id_member_from']]['warning_status'] && ($context['user']['can_mod'] || (!empty($modSettings['warning_show']) && ($modSettings['warning_show'] > 1 || $message['id_member_from'] == $user_info['id'])));
3051
	}
3052
3053
	$memberContext[$message['id_member_from']]['show_profile_buttons'] = $settings['show_profile_buttons'] && (!empty($memberContext[$message['id_member_from']]['can_view_profile']) || (!empty($memberContext[$message['id_member_from']]['website']['url']) && !isset($context['disabled_fields']['website'])) || (in_array($memberContext[$message['id_member_from']]['show_email'], array('yes', 'yes_permission_override', 'no_through_forum'))) || $context['can_send_pm']);
3054
3055
	// Censor all the important text...
3056
	censorText($message['body']);
3057
	censorText($message['subject']);
3058
3059
	// Run BBC interpreter on the message.
3060
	$bbc_parser = \BBC\ParserWrapper::getInstance();
3061
	$message['body'] = $bbc_parser->parsePM($message['body']);
3062
3063
	// Return the array.
3064
	$output = array(
3065
		'alternate' => $counter % 2,
3066
		'id' => $message['id_pm'],
3067
		'member' => &$memberContext[$message['id_member_from']],
3068
		'subject' => $message['subject'],
3069
		'time' => standardTime($message['msgtime']),
3070
		'html_time' => htmlTime($message['msgtime']),
3071
		'timestamp' => forum_time(true, $message['msgtime']),
3072
		'counter' => $counter,
3073
		'body' => $message['body'],
3074
		'recipients' => &$recipients[$message['id_pm']],
3075
		'number_recipients' => count($recipients[$message['id_pm']]['to']),
3076
		'labels' => &$context['message_labels'][$message['id_pm']],
3077
		'fully_labeled' => count($context['message_labels'][$message['id_pm']]) == count($context['labels']),
3078
		'is_replied_to' => &$context['message_replied'][$message['id_pm']],
3079
		'is_unread' => &$context['message_unread'][$message['id_pm']],
3080
		'is_selected' => !empty($temp_pm_selected) && in_array($message['id_pm'], $temp_pm_selected),
3081
		'is_message_author' => $message['id_member_from'] == $user_info['id'],
3082
		'can_report' => !empty($modSettings['enableReportPM']),
3083
		'can_see_ip' => allowedTo('moderate_forum') || ($message['id_member_from'] == $user_info['id'] && !empty($user_info['id'])),
3084
	);
3085
3086
	$context['additional_pm_drop_buttons'] = array();
3087
3088
	// Can they report this message
3089
	if (!empty($output['can_report']) && $context['folder'] !== 'sent' && $output['member']['id'] != $user_info['id'])
3090
	{
3091
		$context['additional_pm_drop_buttons']['warn_button'] = array(
3092
			'href' => $scripturl . '?action=pm;sa=report;l=' . $context['current_label_id'] . ';pmsg=' . $output['id'] . ';' . $context['session_var'] . '=' . $context['session_id'],
3093
			'text' => $txt['pm_report_to_admin']
3094
		);
3095
	}
3096
3097
	// Or mark it as unread
3098
	if (empty($output['is_unread']) && $context['folder'] !== 'sent' && $output['member']['id'] != $user_info['id'])
3099
	{
3100
		$context['additional_pm_drop_buttons']['restore_button'] = array(
3101
			'href' => $scripturl . '?action=pm;sa=markunread;l=' . $context['current_label_id'] . ';pmsg=' . $output['id'] . ';' . $context['session_var'] . '=' . $context['session_id'],
3102
			'text' => $txt['pm_mark_unread']
3103
		);
3104
	}
3105
3106
	// Or give / take karma for a PM
3107
	if (!empty($output['member']['karma']['allow']))
3108
	{
3109
		$output['member']['karma'] += array(
3110
			'applaud_url' => $scripturl . '?action=karma;sa=applaud;uid=' . $output['member']['id'] . ';f=' . $context['folder'] . ';start=' . $context['start'] . ($context['current_label_id'] != -1 ? ';l=' . $context['current_label_id'] : '') . ';pm=' . $output['id'] . ';' . $context['session_var'] . '=' . $context['session_id'],
3111
			'smite_url' => $scripturl . '?action=karma;sa=smite;uid=' . $output['member']['id'] . ';f=' . $context['folder'] . ';start=' . $context['start'] . ($context['current_label_id'] != -1 ? ';l=' . $context['current_label_id'] : '') . ';pm=' . $output['id'] . ';' . $context['session_var'] . '=' . $context['session_id'],
3112
		);
3113
	}
3114
3115
	$counter++;
3116
3117
	return $output;
3118
}