sendpm()   F
last analyzed

Complexity

Conditions 84
Paths > 20000

Size

Total Lines 458
Code Lines 197

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 96
CRAP Score 979.8308

Importance

Changes 0
Metric Value
cc 84
eloc 197
nc 233574912
nop 6
dl 0
loc 458
ccs 96
cts 193
cp 0.4974
crap 979.8308
rs 0
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
3
/**
4
 * This file handles tasks related to personal messages. It performs all
5
 * the necessary (database updates, statistics updates) to add, delete, mark
6
 * etc personal messages.
7
 *
8
 * The functions in this file do NOT check permissions.
9
 *
10
 * @package   ElkArte Forum
11
 * @copyright ElkArte Forum contributors
12
 * @license   BSD http://opensource.org/licenses/BSD-3-Clause (see accompanying LICENSE.txt file)
13
 *
14
 * This file contains code covered by:
15
 * copyright: 2011 Simple Machines (http://www.simplemachines.org)
16
 *
17
 * @version 2.0 dev
18
 *
19
 */
20
21
use ElkArte\Cache\Cache;
22
use ElkArte\Helper\Util;
23
use ElkArte\Languages\Txt;
24
use ElkArte\Mail\BuildMail;
25
use ElkArte\Mail\PreparseMail;
26
use ElkArte\User;
27
28
/**
29
 * Loads information about the users personal message limit.
30
 *
31
 * @package PersonalMessage
32 4
 */
33
function loadMessageLimit()
34 4
{
35 4
	$db = database();
36
37 4
	$message_limit = 0;
38
	if (User::$info->is_admin)
0 ignored issues
show
Bug Best Practice introduced by
The property is_admin does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
39
	{
40
		$message_limit = 0;
41
	}
42
	elseif (!Cache::instance()->getVar($message_limit, 'msgLimit:' . User::$info->id, 360))
0 ignored issues
show
Bug Best Practice introduced by
The property id does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
43
	{
44
		$db->fetchQuery('
45
			SELECT
46
				MAX(max_messages) AS top_limit, MIN(max_messages) AS bottom_limit
47
			FROM {db_prefix}membergroups
48
			WHERE id_group IN ({array_int:users_groups})',
49
			[
50
				'users_groups' => User::$info->groups,
0 ignored issues
show
Bug Best Practice introduced by
The property groups does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
51
			]
52
		)->fetch_callback(
53
			function ($row) use (&$message_limit) {
54
				$message_limit = $row['top_limit'] == 0 ? 0 : $row['bottom_limit'];
55
			}
56
		);
57
58
		// Save us doing it again!
59 4
		Cache::instance()->put('msgLimit:' . User::$info->id, $message_limit, 360);
60
	}
61
62
	return $message_limit;
63
}
64
65
/**
66
 * Loads the count of messages on a per label basis.
67
 *
68
 * @param $labels array array of labels that we are calculating the message count
69
 *
70
 * @return array
71
 * @package PersonalMessage
72
 */
73 4
function loadPMLabels($labels)
74
{
75
	$db = database();
76 4
77
	// Looks like we need to reseek!
78
	$db->fetchQuery('
79
		SELECT
80
			labels, is_read, COUNT(*) AS num
81
		FROM {db_prefix}pm_recipients
82
		WHERE id_member = {int:current_member}
83
			AND deleted = {int:not_deleted}
84 4
		GROUP BY labels, is_read',
85 4
		[
86
			'current_member' => User::$info->id,
0 ignored issues
show
Bug Best Practice introduced by
The property id does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
87 4
			'not_deleted' => 0,
88
		]
89
	)->fetch_callback(
90
		function ($row) use (&$labels) {
91
			$this_labels = explode(',', $row['labels']);
92
			foreach ($this_labels as $this_label)
93
			{
94
				$labels[(int) $this_label]['messages'] += $row['num'];
95
96
				if (!($row['is_read'] & 1))
97
				{
98
					$labels[(int) $this_label]['unread_messages'] += $row['num'];
99 4
				}
100
			}
101
		}
102
	);
103 4
104
	// Store it please!
105 4
	Cache::instance()->put('labelCounts:' . User::$info->id, $labels, 720);
106
107
	return $labels;
108
}
109
110
/**
111
 * Counts all unread personal messages received by a member after a certain time.
112
 * If the timestamp is empty/zero, returns the total number of unread PMs.
113
 *
114
 * @param int $id_member
115
 * @param int $timestamp
116
 * @return int Number of new unread PMs since the timestamp
117
 * @package PersonalMessage
118
 */
119
function getNewPMs($id_member, $timestamp)
120 2
{
121
    $db = database();
122 2
123
    if (empty($timestamp))
124
    {
125 2
		$result = $db->fetchQuery('
126
			SELECT 
127
				COUNT(*) AS c
128
			FROM {db_prefix}pm_recipients AS pr
129
			WHERE pr.id_member = {int:member}
130
				AND pr.deleted = {int:not_deleted}
131
				AND (pr.is_read & 1) = {int:is_unread}',
132
			[
133
				'member' => (int) $id_member,
134
				'not_deleted' => 0,
135
				'is_unread' => 0,
136
			]
137
		)->fetch_assoc();
138
    }
139
    else
140
    {
141
	    $result = $db->fetchQuery('
142
			SELECT
143 2
				COUNT(*) AS c
144
			FROM {db_prefix}pm_recipients AS pr
145 2
				INNER JOIN {db_prefix}personal_messages AS pm ON (pm.id_pm = pr.id_pm)
146 2
			WHERE pr.id_member = {int:member}
147 2
				AND pr.deleted = {int:not_deleted}
148
				AND (pr.is_read & 1) = {int:is_unread}
149 2
				AND pm.msgtime > {int:last_seen}',
150 2
			[
151
				'member' => (int) $id_member,
152 2
				'not_deleted' => 0,
153 2
				'is_unread' => 0,
154 2
				'last_seen' => (int) $timestamp,
155
			]
156
	    )->fetch_assoc();
157
    }
158
159 2
	return (int) $result['c'];
160 2
}
161
162 2
163
/**
164
 * Fetch the latest timestamp of a personal message sent to a specific user.
165
 *
166
 * Sent to a user means messages where the member is a recipient (not the sender).
167
 * Returns 0 if the member has no received messages.
168
 *
169
 * @param int $id_member The member id to check
170
 * @return int Unix timestamp of the latest received PM or 0 if none
171
 * @package PersonalMessage
172
 */
173
function getLastPMSentTime($id_member)
174
{
175
	$db = database();
176
177
	$request = $db->fetchQuery('
178
		SELECT
179
			MAX(pm.msgtime) AS last_time
180
		FROM {db_prefix}pm_recipients AS pr
181
			INNER JOIN {db_prefix}personal_messages AS pm ON (pm.id_pm = pr.id_pm)
182
		WHERE pr.id_member = {int:member}
183
			AND pr.deleted = {int:not_deleted}',
184
		[
185
			'member' => (int) $id_member,
186
			'not_deleted' => 0,
187
		]
188
	);
189
190
	$row = $request->fetch_assoc();
191
	$request->free_result();
192
193
	return empty($row['last_time']) ? 0 : (int) $row['last_time'];
194
}
195
196
/**
197
 * Get the number of PMs.
198
 *
199
 * @param bool $descending
200
 * @param int|null $pmID
201
 * @param string $labelQuery
202
 * @return int
203
 * @package PersonalMessage
204
 */
205
function getPMCount($descending = false, $pmID = null, $labelQuery = '')
206
{
207
	global $context;
208
209
	$db = database();
210
211
	// Figure out how many messages there are.
212
	if ($context['folder'] === 'sent')
213
	{
214
		$request = $db->fetchQuery('
215
			SELECT
216
				COUNT(' . ($context['display_mode'] == 2 ? 'DISTINCT id_pm_head' : '*') . ')
217
			FROM {db_prefix}personal_messages
218
			WHERE id_member_from = {int:current_member}
219
				AND deleted_by_sender = {int:not_deleted}' . ($pmID !== null ? '
220
				AND id_pm ' . ($descending ? '>' : '<') . ' {int:id_pm}' : ''),
221
			[
222
				'current_member' => User::$info->id,
0 ignored issues
show
Bug Best Practice introduced by
The property id does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
223
				'not_deleted' => 0,
224
				'id_pm' => $pmID,
225
			]
226
		);
227
	}
228
	else
229
	{
230
		$request = $db->fetchQuery('
231
			SELECT
232
				COUNT(' . ($context['display_mode'] == 2 ? 'DISTINCT pm.id_pm_head' : '*') . ')
233
			FROM {db_prefix}pm_recipients AS pmr' . ($context['display_mode'] == 2 ? '
234
				INNER JOIN {db_prefix}personal_messages AS pm ON (pm.id_pm = pmr.id_pm)' : '') . '
235
			WHERE pmr.id_member = {int:current_member}
236
				AND pmr.deleted = {int:not_deleted}' . $labelQuery . ($pmID !== null ? '
237
				AND pmr.id_pm ' . ($descending ? '>' : '<') . ' {int:id_pm}' : ''),
238
			[
239
				'current_member' => User::$info->id,
240
				'not_deleted' => 0,
241
				'id_pm' => $pmID,
242
			]
243
		);
244
	}
245
246
	list ($count) = $request->fetch_row();
247
	$request->free_result();
248
249
	return (int) $count;
250
}
251
252
/**
253
 * Delete the specified personal messages.
254
 *
255
 * @param int[]|null $personal_messages array of pm ids
256
 * @param string|null $folder = null
257
 * @param int|int[]|null $owner = null
258
 * @package PersonalMessage
259
 */
260
function deleteMessages($personal_messages, $folder = null, $owner = null)
261
{
262
	$db = database();
263
264
	if ($owner === null)
265
	{
266
		$owner = [User::$info->id];
0 ignored issues
show
Bug Best Practice introduced by
The property id does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
267
	}
268
	elseif (empty($owner))
269
	{
270
		return;
271
	}
272
	elseif (!is_array($owner))
273
	{
274
		$owner = [$owner];
275
	}
276
277
	if ($personal_messages !== null)
278
	{
279
		if (empty($personal_messages) || !is_array($personal_messages))
280
		{
281
			return;
282
		}
283
284
		foreach ($personal_messages as $index => $delete_id)
285
		{
286
			$personal_messages[$index] = (int) $delete_id;
287
		}
288
289
		$where = '
290
				AND id_pm IN ({array_int:pm_list})';
291
	}
292
	else
293
	{
294
		$where = '';
295
	}
296
297
	if ($folder === 'sent' || $folder === null)
298
	{
299
		$db->query('', '
300
			UPDATE {db_prefix}personal_messages
301
			SET 
302
				deleted_by_sender = {int:is_deleted}
303
			WHERE id_member_from IN ({array_int:member_list})
304
				AND deleted_by_sender = {int:not_deleted}' . $where,
305
			[
306
				'member_list' => $owner,
307
				'is_deleted' => 1,
308
				'not_deleted' => 0,
309
				'pm_list' => $personal_messages !== null ? array_unique($personal_messages) : [],
310
			]
311
		);
312
	}
313
314
	if ($folder !== 'sent')
315
	{
316
		require_once(SUBSDIR . '/Members.subs.php');
317
318
		// Calculate the number of messages each member's gonna lose...
319
		$db->fetchQuery('
320
			SELECT
321
				id_member, COUNT(*) AS num_deleted_messages, CASE WHEN is_read & 1 >= 1 THEN 1 ELSE 0 END AS is_read
322
			FROM {db_prefix}pm_recipients
323
			WHERE id_member IN ({array_int:member_list})
324
				AND deleted = {int:not_deleted}' . $where . '
325
			GROUP BY id_member, is_read',
326
			[
327
				'member_list' => $owner,
328
				'not_deleted' => 0,
329
				'pm_list' => $personal_messages !== null ? array_unique($personal_messages) : [],
330
			]
331
		)->fetch_callback(
332
			function ($row) use ($where) {
333
				// ...And update the statistics accordingly - now including unread messages!.
334
				if ($row['is_read'])
335
				{
336
					updateMemberData($row['id_member'], ['personal_messages' => $where === '' ? 0 : 'personal_messages - ' . $row['num_deleted_messages']]);
337
				}
338
				else
339
				{
340
					updateMemberData($row['id_member'], ['personal_messages' => $where === '' ? 0 : 'personal_messages - ' . $row['num_deleted_messages'], 'unread_messages' => $where === '' ? 0 : 'unread_messages - ' . $row['num_deleted_messages']]);
341
				}
342
343
				// If this is the current member we need to make their message count correct.
344
				if (User::$info->id == $row['id_member'])
0 ignored issues
show
Bug Best Practice introduced by
The property id does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
345
				{
346
					User::$info->messages -= $row['num_deleted_messages'];
0 ignored issues
show
Bug Best Practice introduced by
The property messages does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
347
					if (!($row['is_read']))
348
					{
349
						User::$info->unread_messages -= $row['num_deleted_messages'];
0 ignored issues
show
Bug Best Practice introduced by
The property unread_messages does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
350
					}
351
				}
352
			}
353
		);
354
355
		// Do the actual deletion.
356
		$db->query('', '
357
			UPDATE {db_prefix}pm_recipients
358
			SET 
359
				deleted = {int:is_deleted}
360
			WHERE id_member IN ({array_int:member_list})
361
				AND deleted = {int:not_deleted}' . $where,
362
			[
363
				'member_list' => $owner,
364
				'is_deleted' => 1,
365
				'not_deleted' => 0,
366
				'pm_list' => $personal_messages !== null ? array_unique($personal_messages) : [],
367
			]
368
		);
369
	}
370
371
	// If sender and recipients all have deleted their message, it can be removed.
372
	$remove_pms = [];
373
	$db->fetchQuery('
374
		SELECT
375
			pm.id_pm AS sender, pmr.id_pm
376
		FROM {db_prefix}personal_messages AS pm
377
			LEFT JOIN {db_prefix}pm_recipients AS pmr ON (pmr.id_pm = pm.id_pm AND pmr.deleted = {int:not_deleted})
378
		WHERE pm.deleted_by_sender = {int:is_deleted}
379
			' . str_replace('id_pm', 'pm.id_pm', $where) . '
380
		GROUP BY sender, pmr.id_pm
381
		HAVING pmr.id_pm IS null',
382
		[
383
			'not_deleted' => 0,
384
			'is_deleted' => 1,
385
			'pm_list' => $personal_messages !== null ? array_unique($personal_messages) : [],
386
		]
387
	)->fetch_callback(
388
		function ($row) use (&$remove_pms) {
389
			$remove_pms[] = $row['sender'];
390
		}
391
	);
392
393
	if (!empty($remove_pms))
394
	{
395
		$db->query('', '
396
			DELETE FROM {db_prefix}personal_messages
397
			WHERE id_pm IN ({array_int:pm_list})',
398
			[
399
				'pm_list' => $remove_pms,
400
			]
401
		);
402
403
		$db->query('', '
404
			DELETE FROM {db_prefix}pm_recipients
405
			WHERE id_pm IN ({array_int:pm_list})',
406
			[
407
				'pm_list' => $remove_pms,
408
			]
409
		);
410
	}
411
412
	// Any cached numbers may be wrong now.
413
	Cache::instance()->put('labelCounts:' . User::$info->id, null, 720);
414
}
415
416
/**
417
 * Mark the specified personal messages read.
418
 *
419
 * @param int[]|int|null $personal_messages null or array of pm ids
420
 * @param string|null $label = null, if label is set, only marks messages with that label
421
 * @param int|null $owner = null, if owner is set, marks messages owned by that member id
422
 * @package PersonalMessage
423
 */
424
function markMessages($personal_messages = null, $label = null, $owner = null)
425
{
426
	if ($owner === null)
427
	{
428
		$owner = User::$info->id;
0 ignored issues
show
Bug Best Practice introduced by
The property id does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
429
	}
430
431
	if (!is_null($personal_messages) && !is_array($personal_messages))
432
	{
433
		$personal_messages = [$personal_messages];
434
	}
435
436
	$db = database();
437
438
	$request = $db->fetchQuery('
439
		UPDATE {db_prefix}pm_recipients
440
		SET 
441
			is_read = is_read | 1
442
		WHERE id_member = {int:id_member}
443
			AND NOT (is_read & 1 >= 1)' . ($label === null ? '' : '
444
			AND FIND_IN_SET({string:label}, labels) != 0') . ($personal_messages !== null ? '
445
			AND id_pm IN ({array_int:personal_messages})' : ''),
446
		[
447
			'personal_messages' => $personal_messages,
448
			'id_member' => $owner,
449
			'label' => $label,
450
		]
451
	);
452
453
	// If something wasn't marked as read, get the number of unread messages remaining.
454
	if ($request->affected_rows() > 0)
455
	{
456
		updatePMMenuCounts($owner);
457
	}
458
}
459
460
/**
461
 * Mark the specified personal messages as unread.
462
 *
463
 * @param int|int[] $personal_messages
464
 * @package PersonalMessage
465
 */
466
function markMessagesUnread($personal_messages)
467
{
468
	if (empty($personal_messages))
469
	{
470
		return;
471
	}
472
473
	if (!is_array($personal_messages))
474
	{
475
		$personal_messages = [$personal_messages];
476
	}
477
478
	$db = database();
479
480
	$owner = User::$info->id;
0 ignored issues
show
Bug Best Practice introduced by
The property id does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
481
482
	// Flip the "read" bit on this
483
	$request = $db->fetchQuery('
484
		UPDATE {db_prefix}pm_recipients
485
		SET 
486
			is_read = is_read & 2
487
		WHERE id_member = {int:id_member}
488
			AND (is_read & 1 >= 1)
489
			AND id_pm IN ({array_int:personal_messages})',
490
		[
491
			'personal_messages' => $personal_messages,
492
			'id_member' => $owner,
493
		]
494
	);
495
496
	// If something was marked unread, update the number of unread messages remaining.
497
	if ($request->affected_rows() > 0)
498
	{
499
		updatePMMenuCounts($owner);
500
	}
501
}
502
503
/**
504
 * Updates the number of unread messages for a user
505
 *
506
 * - Updates the per label totals as well as the overall total
507
 *
508
 * @param int $owner
509
 * @package PersonalMessage
510
 */
511
function updatePMMenuCounts($owner)
512
{
513
	global $context;
514
515
	$db = database();
516
517
	if ($owner == User::$info->id)
0 ignored issues
show
Bug Best Practice introduced by
The property id does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
518
	{
519
		foreach ($context['labels'] as $label)
520
		{
521
			$context['labels'][(int) $label['id']]['unread_messages'] = 0;
522
		}
523
	}
524
525
	$total_unread = 0;
526
	$db->fetchQuery('
527
		SELECT
528
			labels, COUNT(*) AS num
529
		FROM {db_prefix}pm_recipients
530
		WHERE id_member = {int:id_member}
531
			AND NOT (is_read & 1 >= 1)
532
			AND deleted = {int:is_not_deleted}
533
		GROUP BY labels',
534
		[
535
			'id_member' => $owner,
536
			'is_not_deleted' => 0,
537
		]
538
	)->fetch_callback(
539
		function ($row) use ($context, &$total_unread, $owner) {
540
			$total_unread += $row['num'];
541
542
			if ($owner != User::$info->id)
0 ignored issues
show
Bug Best Practice introduced by
The property id does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
543
			{
544
				return;
545
			}
546
547
			$this_labels = explode(',', $row['labels']);
548
			foreach ($this_labels as $this_label)
549
			{
550
				$context['labels'][(int) $this_label]['unread_messages'] += $row['num'];
551 2
			}
552
		}
553 2
	);
554
555
	// Need to store all this.
556 2
	Cache::instance()->put('labelCounts:' . $owner, $context['labels'], 720);
557
	require_once(SUBSDIR . '/Members.subs.php');
558
	updateMemberData($owner, ['unread_messages' => $total_unread]);
559 2
560 2
	// If it was for the current member, reflect this in the User::$info array too.
561
	if ($owner == User::$info->id)
562
	{
563
		User::$info->unread_messages = $total_unread;
0 ignored issues
show
Bug Best Practice introduced by
The property unread_messages does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __set, consider adding a @property annotation.
Loading history...
564 2
	}
565
}
566
567
/**
568 2
 * Check if the PM is available to the current user.
569
 *
570
 * @param int $pmID
571 2
 * @param string $validFor
572 2
 * @return bool|null
573 2
 * @package PersonalMessage
574
 */
575
function isAccessiblePM($pmID, $validFor = 'in_or_outbox')
576
{
577
	$db = database();
578
579
	$request = $db->fetchQuery('
580
		SELECT
581
			pm.id_member_from = {int:id_current_member} AND pm.deleted_by_sender = {int:not_deleted} AS valid_for_outbox,
582
			pmr.id_pm IS NOT NULL AS valid_for_inbox
583 2
		FROM {db_prefix}personal_messages AS pm
584
			LEFT JOIN {db_prefix}pm_recipients AS pmr ON (pmr.id_pm = pm.id_pm AND pmr.id_member = {int:id_current_member} AND pmr.deleted = {int:not_deleted})
585
		WHERE pm.id_pm = {int:id_pm}
586 2
			AND ((pm.id_member_from = {int:id_current_member} AND pm.deleted_by_sender = {int:not_deleted}) OR pmr.id_pm IS NOT NULL)',
587 2
		[
588 2
			'id_pm' => $pmID,
589 2
			'id_current_member' => User::$info->id,
0 ignored issues
show
Bug Best Practice introduced by
The property id does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
590
			'not_deleted' => 0,
591
		]
592
	);
593
	if ($request->num_rows() === 0)
594
	{
595 2
		$request->free_result();
596
597
		return false;
598
	}
599
	$validationResult = $request->fetch_assoc();
600
	$request->free_result();
601 2
602 2
	switch ($validFor)
603
	{
604 2
		case 'inbox':
605
			return !empty($validationResult['valid_for_inbox']);
606 2
607
		case 'outbox':
608
			return !empty($validationResult['valid_for_outbox']);
609 1
610
		case 'in_or_outbox':
611
			return !empty($validationResult['valid_for_inbox']) || !empty($validationResult['valid_for_outbox']);
612
613
		default:
614 2
			trigger_error('Undefined validation type given', E_USER_ERROR);
615
	}
616
}
617
618
/**
619
 * Sends a personal message from the specified person to the specified people
620
 * ($from defaults to the user)
621
 *
622
 * @param array $recipients - an array containing the arrays 'to' and 'bcc', both containing id_member's.
623
 * @param string $subject - should have no slashes and no html entities
624
 * @param string $message - should have no slashes and no html entities
625
 * @param bool $store_outbox
626
 * @param array|null $from - an array with the id, name, and username of the member.
627
 * @param int $pm_head - the ID of the chain being replied to - if any.
628
 * @return array an array with log entries telling how many recipients were successful and which recipients it failed to send to.
629
 * @package PersonalMessage
630
 */
631
function sendpm($recipients, $subject, $message, $store_outbox = true, $from = null, $pm_head = 0)
632
{
633
	global $scripturl, $txt, $language, $modSettings, $webmaster_email;
634
635
	$db = database();
636
637
	// Make sure the PM language file is loaded, we might need something out of it.
638
	Txt::load('PersonalMessage');
639
640
	// Needed for our email and post functions
641
	require_once(SUBSDIR . '/Mail.subs.php');
642
	require_once(SUBSDIR . '/Post.subs.php');
643
644
	// Initialize log array.
645
	$log = [
646
		'failed' => [],
647
		'sent' => []
648
	];
649
650
	if ($from === null)
651
	{
652
		$from = [
653
			'id' => User::$info->id,
0 ignored issues
show
Bug Best Practice introduced by
The property id does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
654
			'name' => User::$info->name,
0 ignored issues
show
Bug Best Practice introduced by
The property name does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
655
			'username' => User::$info->username
0 ignored issues
show
Bug Best Practice introduced by
The property username does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
656
		];
657
	}
658 2
	// Probably not needed.  /me something should be of the typer.
659
	else
660
	{
661 2
		User::$info->name = $from['name'];
0 ignored issues
show
Bug Best Practice introduced by
The property name does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __set, consider adding a @property annotation.
Loading history...
662
	}
663
664 2
	// Integrated PMs
665
	call_integration_hook('integrate_personal_message', [&$recipients, &$from, &$subject, &$message]);
666
667 2
	// This is the one that will go in their inbox.
668 2
	$htmlmessage = Util::htmlspecialchars($message, ENT_QUOTES, 'UTF-8', true);
669
	preparsecode($htmlmessage);
670
	$htmlsubject = strtr(Util::htmlspecialchars($subject), ["\r" => '', "\n" => '', "\t" => '']);
671
	if (Util::strlen($htmlsubject) > 100)
672
	{
673
		$htmlsubject = Util::substr($htmlsubject, 0, 100);
674
	}
675 2
676 2
	// Make sure is an array
677
	if (!is_array($recipients))
0 ignored issues
show
introduced by
The condition is_array($recipients) is always true.
Loading history...
678 2
	{
679
		$recipients = [$recipients];
680
	}
681
682
	// Get a list of usernames and convert them to IDs.
683
	$usernames = [];
684
	foreach ($recipients as $rec_type => $rec)
685
	{
686
		foreach ($rec as $id => $member)
687
		{
688
			if (!is_numeric($recipients[$rec_type][$id]))
689
			{
690
				$recipients[$rec_type][$id] = Util::strtolower(trim(preg_replace('/[<>&"\'=\\\]/', '', $recipients[$rec_type][$id])));
691
				$usernames[$recipients[$rec_type][$id]] = 0;
692
			}
693
		}
694
	}
695
696
	if (!empty($usernames))
697
	{
698
		$request = $db->fetchQuery('
699
			SELECT
700
				id_member, member_name
701
			FROM {db_prefix}members
702
			WHERE {column_case_insensitive:member_name} IN ({array_string_case_insensitive:usernames})',
703
			[
704
				'usernames' => array_keys($usernames),
705 2
			]
706
		);
707
		while (($row = $request->fetch_assoc()))
708
		{
709 2
			if (isset($usernames[Util::strtolower($row['member_name'])]))
710 2
			{
711
				$usernames[Util::strtolower($row['member_name'])] = $row['id_member'];
712
			}
713
		}
714
		$request->free_result();
715
716
		// Replace the usernames with IDs. Drop usernames that couldn't be found.
717
		foreach ($recipients as $rec_type => $rec)
718
		{
719
			foreach ($rec as $id => $member)
720
			{
721
				if (is_numeric($recipients[$rec_type][$id]))
722
				{
723
					continue;
724
				}
725
726 2
				if (!empty($usernames[$member]))
727 2
				{
728 2
					$recipients[$rec_type][$id] = $usernames[$member];
729
				}
730
				else
731
				{
732
					$log['failed'][$id] = sprintf($txt['pm_error_user_not_found'], $recipients[$rec_type][$id]);
733
					unset($recipients[$rec_type][$id]);
734 2
				}
735
			}
736 2
		}
737
	}
738 2
739
	// Make sure there are no duplicate 'to' members.
740
	$recipients['to'] = array_unique($recipients['to']);
741
742
	// Only 'bcc' members that aren't already in 'to'.
743
	$recipients['bcc'] = array_diff(array_unique($recipients['bcc']), $recipients['to']);
744 2
745
	// Combine 'to' and 'bcc' recipients.
746 2
	$all_to = array_merge($recipients['to'], $recipients['bcc']);
747
748
	// Check no-one will want it deleted right away!
749 2
	$deletes = [];
750
	$db->fetchQuery('
751 2
		SELECT
752
			id_member, criteria, is_or
753
		FROM {db_prefix}pm_rules
754 2
		WHERE id_member IN ({array_int:to_members})
755
			AND delete_pm = {int:delete_pm}',
756
		[
757 2
			'to_members' => $all_to,
758
			'delete_pm' => 1,
759
		]
760 2
	)->fetch_callback(
761
		function ($row) use (&$deletes, $from, $subject, $message) {
762
			// Check whether we have to apply anything...
763
			$criteria = Util::unserialize($row['criteria']);
764
765
			// Note we don't check the buddy status, cause deletion from buddy = madness!
766
			$delete = false;
767
			foreach ($criteria as $criterium)
768 2
			{
769 2
				if (($criterium['t'] === 'mid' && $criterium['v'] == $from['id'])
770 2
					|| ($criterium['t'] === 'gid' && in_array($criterium['v'], User::$info->groups))
0 ignored issues
show
Bug introduced by
It seems like ElkArte\User::info->groups can also be of type null; however, parameter $haystack of in_array() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

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

770
					|| ($criterium['t'] === 'gid' && in_array($criterium['v'], /** @scrutinizer ignore-type */ User::$info->groups))
Loading history...
Bug Best Practice introduced by
The property groups does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
771 2
					|| ($criterium['t'] === 'sub' && str_contains($subject, $criterium['v']))
772 2
					|| ($criterium['t'] === 'msg' && str_contains($message, $criterium['v'])))
773 2
				{
774
					$delete = true;
775
				}
776 2
				// If we're adding and one criteria don't match then we stop!
777 2
				elseif (!$row['is_or'])
778
				{
779
					$delete = false;
780 2
					break;
781
				}
782
			}
783
			if ($delete)
784
			{
785
				$deletes[$row['id_member']] = 1;
786 2
			}
787 2
		}
788 2
	);
789
790 2
	// Load the membergroup message limits.
791
	static $message_limit_cache = [];
792
	if (!allowedTo('moderate_forum') && empty($message_limit_cache))
793 2
	{
794
		$db->fetchQuery('
795
			SELECT
796
				id_group, max_messages
797
			FROM {db_prefix}membergroups',
798
			[]
799
		)->fetch_callback(
800
			function ($row) use (&$message_limit_cache) {
801
				$message_limit_cache[$row['id_group']] = $row['max_messages'];
802
			}
803
		);
804
	}
805
806
	// Load the groups that are allowed to read PMs.
807
	// @todo move into a separate function on $permission.
808
	$allowed_groups = [];
809
	$disallowed_groups = [];
810
	$db->fetchQuery('
811
		SELECT
812
			id_group, add_deny
813
		FROM {db_prefix}permissions
814
		WHERE permission = {string:read_permission}',
815
		[
816
			'read_permission' => 'pm_read',
817
		]
818
	)->fetch_callback(
819
		function ($row) use (&$disallowed_groups, &$allowed_groups) {
820 2
			if (empty($row['add_deny']))
821
			{
822
				$disallowed_groups[] = $row['id_group'];
823
			}
824
			else
825
			{
826
				$allowed_groups[] = $row['id_group'];
827
			}
828 2
		}
829
	);
830
831
	if (empty($modSettings['permission_enable_deny']))
832
	{
833
		$disallowed_groups = [];
834
	}
835
836 2
	$request = $db->fetchQuery('
837
		SELECT
838
			member_name, real_name, id_member, email_address, lngfile,
839
			pm_email_notify, personal_messages,' . (allowedTo('moderate_forum') ? ' 0' : '
840
			(receive_from = {int:admins_only}' . (empty($modSettings['enable_buddylist']) ? '' : ' OR
841 2
			(receive_from = {int:buddies_only} AND FIND_IN_SET({string:from_id}, buddy_list) = 0) OR
842
			(receive_from = {int:not_on_ignore_list} AND FIND_IN_SET({string:from_id}, pm_ignore_list) != 0)') . ')') . ' AS ignored,
843 2
			FIND_IN_SET({string:from_id}, buddy_list) != 0 AS is_buddy, is_activated,
844
			additional_groups, id_group, id_post_group
845
		FROM {db_prefix}members
846 2
		WHERE id_member IN ({array_int:recipients})
847
		ORDER BY lngfile
848
		LIMIT {int:count_recipients}',
849
		[
850
			'not_on_ignore_list' => 1,
851
			'buddies_only' => 2,
852 2
			'admins_only' => 3,
853
			'recipients' => $all_to,
854 2
			'count_recipients' => count($all_to),
855
			'from_id' => $from['id'],
856
		]
857
	);
858 2
	$notifications = [];
859 2
	while (($row = $request->fetch_assoc()))
860
	{
861 2
		// Don't do anything for members to be deleted!
862
		if (isset($deletes[$row['id_member']]))
863
		{
864
			continue;
865 2
		}
866 2
867
		// We need to know this members groups.
868 2
		$groups = array_merge([$row['id_group'], $row['id_post_group']], (empty($row['additional_groups']) ? [] : explode(',', $row['additional_groups'])));
869
870 2
		$message_limit = -1;
871
872
		// For each group see whether they've gone over their limit - assuming they're not an admin.
873 2
		if (!in_array(1, $groups))
874 2
		{
875
			foreach ($groups as $id)
876
			{
877 2
				if (isset($message_limit_cache[$id]) && $message_limit != 0 && $message_limit < $message_limit_cache[$id])
878
				{
879 2
					$message_limit = $message_limit_cache[$id];
880
				}
881
			}
882
883
			if ($message_limit > 0 && $message_limit <= $row['personal_messages'])
884
			{
885 2
				$log['failed'][$row['id_member']] = sprintf($txt['pm_error_data_limit_reached'], $row['real_name']);
886
				unset($all_to[array_search($row['id_member'], $all_to)]);
887
				continue;
888
			}
889
890
			// Do they have any of the allowed groups?
891 2
			if (count(array_intersect($allowed_groups, $groups)) == 0 || count(array_intersect($disallowed_groups, $groups)) != 0)
892
			{
893
				$log['failed'][$row['id_member']] = sprintf($txt['pm_error_user_cannot_read'], $row['real_name']);
894
				unset($all_to[array_search($row['id_member'], $all_to)]);
895 2
				continue;
896
			}
897
		}
898
899 2
		// Note that PostgreSQL can return a lowercase t/f for FIND_IN_SET
900 2
		if (!empty($row['ignored']) && $row['ignored'] !== 'f' && $row['id_member'] != $from['id'])
901
		{
902 2
			$log['failed'][$row['id_member']] = sprintf($txt['pm_error_ignored_by_user'], $row['real_name']);
903 2
			unset($all_to[array_search($row['id_member'], $all_to)]);
904
			continue;
905 2
		}
906
907
		// If the receiving account is banned (>=10) or pending deletion (4), refuse to send the PM.
908
		if ($row['is_activated'] >= 10 || ($row['is_activated'] == 4 && User::$info->is_admin === false))
0 ignored issues
show
Bug Best Practice introduced by
The property is_admin does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
909 2
		{
910 2
			$log['failed'][$row['id_member']] = sprintf($txt['pm_error_user_cannot_read'], $row['real_name']);
911
			unset($all_to[array_search($row['id_member'], $all_to)]);
912 2
			continue;
913
		}
914 1
915 2
		// Send a notification, if enabled - taking the buddy list into account.
916
		if (!empty($row['email_address']) && ($row['pm_email_notify'] == 1 || ($row['pm_email_notify'] > 1 && (!empty($modSettings['enable_buddylist']) && $row['is_buddy']))) && $row['is_activated'] == 1)
917
		{
918
			$notifications[empty($row['lngfile']) || empty($modSettings['userLanguage']) ? $language : $row['lngfile']][] = $row['email_address'];
919 2
		}
920
921
		$log['sent'][$row['id_member']] = sprintf($txt['pm_successfully_sent'] ?? '', $row['real_name']);
922 2
	}
923
	$request->free_result();
924
925
	// Only 'send' the message if there are any recipients left.
926
	if (empty($all_to))
927
	{
928
		return $log;
929 2
	}
930 2
931
	// Track the pm count for our stats
932
	if (!empty($modSettings['trackStats']))
933 2
	{
934 2
		trackStats(['pm' => '+']);
935
	}
936
937
	// Insert the message itself and then grab the last insert id.
938
	$db->insert('',
939
		'{db_prefix}personal_messages',
940
		[
941
			'id_pm_head' => 'int', 'id_member_from' => 'int', 'deleted_by_sender' => 'int',
942
			'from_name' => 'string-255', 'msgtime' => 'int', 'subject' => 'string-255', 'body' => 'string-65534',
943
		],
944
		[
945 2
			$pm_head, $from['id'], ($store_outbox ? 0 : 1),
946 2
			$from['username'], time(), $htmlsubject, $htmlmessage,
947 2
		],
948 2
		['id_pm']
949 2
	);
950 2
	$id_pm = $db->insert_id('{db_prefix}personal_messages');
951
952
	// Add the recipients.
953
	$to_list = [];
954 2
	if (!empty($id_pm))
955
	{
956 2
		// If this is new we need to set it part of it's own conversation.
957
		if (empty($pm_head))
958
		{
959
			$db->query('', '
960
				UPDATE {db_prefix}personal_messages
961
				SET 
962
					id_pm_head = {int:id_pm_head}
963
				WHERE id_pm = {int:id_pm_head}',
964
				[
965
					'id_pm_head' => $id_pm,
966
				]
967
			);
968
		}
969
970
		// Some people think manually deleting personal_messages is fun... it's not. We protect against it though :)
971
		$db->query('', '
972
			DELETE FROM {db_prefix}pm_recipients
973
			WHERE id_pm = {int:id_pm}',
974
			[
975
				'id_pm' => $id_pm,
976
			]
977
		);
978
979
		$insertRows = [];
980
		foreach ($all_to as $to)
981 2
		{
982
			$insertRows[] = [$id_pm, $to, in_array($to, $recipients['bcc']) ? 1 : 0, isset($deletes[$to]) ? 1 : 0, 1];
983
			if (!in_array($to, $recipients['bcc']))
984 2
			{
985
				$to_list[] = $to;
986
			}
987 2
		}
988
989 2
		$db->insert('insert',
990
			'{db_prefix}pm_recipients',
991 1
			[
992
				'id_pm' => 'int', 'id_member' => 'int', 'bcc' => 'int', 'deleted' => 'int', 'is_new' => 'int'
993
			],
994
			$insertRows,
995 2
			['id_pm', 'id_member']
996
		);
997 2
	}
998 2
999
	$maillist = !empty($modSettings['maillist_enabled']) && !empty($modSettings['pbe_pm_enabled']);
1000
1001 2
	// If they have post by email enabled, override disallow_sendBody
1002
	if (!$maillist && !empty($modSettings['disallow_sendBody']))
1003
	{
1004
		$message = '';
1005
	}
1006
1007
	$to_names = [];
1008
	if (count($to_list) > 1)
1009
	{
1010
		require_once(SUBSDIR . '/Members.subs.php');
1011
		$result = getBasicMemberData($to_list);
1012
		foreach ($result as $row)
1013
		{
1014
			$to_names[] = un_htmlspecialchars($row['real_name']);
1015
		}
1016
	}
1017
1018
	$mailPreparse = new PreparseMail();
1019
	$replacements = [
1020
		'SUBJECT' => $mailPreparse->preparseSubject($subject),
1021
		'MESSAGE' => $mailPreparse->preparseHtml($message),
1022
		'SENDER' => un_htmlspecialchars($from['name']),
1023
		'READLINK' => $scripturl . '?action=pm;pmsg=' . $id_pm . '#msg' . $id_pm,
1024
		'REPLYLINK' => $scripturl . '?action=pm;sa=send;f=inbox;pmsg=' . $id_pm . ';quote;u=' . $from['id'],
1025
		'TOLIST' => implode(', ', $to_names),
1026 2
		'UNSUBSCRIBELINK' => $scripturl . '?action=pm;sa=settings',
1027
	];
1028 2
1029
	// Select the right template
1030
	$email_template = ($maillist && empty($modSettings['disallow_sendBody']) ? 'pbe_' : '') . 'new_pm' . (empty($modSettings['disallow_sendBody']) ? '_body' : '') . (!empty($to_names) ? '_tolist' : '');
1031
1032 2
	foreach ($notifications as $lang => $notification_list)
1033
	{
1034
		$sendMail = new BuildMail();
1035
		$sendMail->setEmailReplacements($replacements);
1036
1037
		// Using maillist functionality
1038
		if ($maillist)
1039
		{
1040
			$sender_details = query_sender_wrapper($from['id']);
1041
1042
			// @todo guest contact us is routing through here
1043
			if (empty($sender_details))
1044
			{
1045
				continue;
1046
			}
1047
1048
			$from_wrapper = !empty($modSettings['maillist_mail_from']) ? $modSettings['maillist_mail_from'] : (empty($modSettings['maillist_sitename_address']) ? $webmaster_email : $modSettings['maillist_sitename_address']);
1049
1050
			// Add in the signature
1051
			$replacements['SIGNATURE'] = $mailPreparse->preparseSignature($sender_details['signature']);
1052
1053
			// And off it goes, looking a bit more personal
1054
			$mail = loadEmailTemplate($email_template, $replacements, $lang, true);
1055
			$reference = !empty($pm_head) ? $pm_head : null;
1056
1057
			$sendMail->buildEmail($notification_list, $mail['subject'], $mail['body'], $from['name'], 'p' . $id_pm, true, 2, true, $from_wrapper, $reference);
1058
		}
1059
		else
1060
		{
1061
			// Off the notification email goes!
1062
			$mail = loadEmailTemplate($email_template, $replacements, $lang, true);
1063
			$sendMail->buildEmail($notification_list, $mail['subject'], $mail['body'], null, 'p' . $id_pm, true, 2, true);
1064
		}
1065
	}
1066
1067
	// Integrated After PMs
1068
	call_integration_hook('integrate_personal_message_after', [&$id_pm, &$log, &$recipients, &$from, &$subject, &$message]);
1069
1070
	// Back to what we were on before!
1071
	Txt::load('index+PersonalMessage');
1072
1073
	// Add one to their unread and read message counts.
1074
	foreach ($all_to as $k => $id)
1075
	{
1076
		if (isset($deletes[$id]))
1077
		{
1078
			unset($all_to[$k]);
1079
		}
1080
	}
1081
1082
	if (!empty($all_to))
1083
	{
1084
		require_once(SUBSDIR . '/Members.subs.php');
1085
		updateMemberData($all_to, ['personal_messages' => '+', 'unread_messages' => '+', 'new_pm' => 1]);
1086
	}
1087
1088
	return $log;
1089
}
1090
1091
/**
1092
 * Fetches the senders email wrapper details
1093
 *
1094
 * - Gets the senders signature for inclusion in the email
1095
 * - Gets the senders email address and visibility flag
1096
 *
1097
 * @param string $from
1098
 * @return array
1099
 */
1100
function query_sender_wrapper($from)
1101
{
1102
	$db = database();
1103
1104
	// The signature and email visibility details
1105
	$request = $db->query('', '
1106
		SELECT
1107
			email_address, signature
1108
		FROM {db_prefix}members
1109
		WHERE id_member  = {int:uid}
1110
			AND is_activated = {int:act}
1111
		LIMIT 1',
1112
		[
1113
			'uid' => $from,
1114
			'act' => 1,
1115
		]
1116
	);
1117
	$result = $request->fetch_assoc();
1118
	$request->free_result();
1119
1120
	return $result;
1121
}
1122
1123 2
/**
1124
 * Load personal messages.
1125
 *
1126 2
 * This function loads messages considering the options given, an array of:
1127
 * - 'display_mode' - the PMs display mode (i.e. conversation, all)
1128
 * - 'is_postgres' - (temporary) boolean to allow choice of PostgreSQL-specific sorting query
1129
 * - 'sort_by_query' - query to sort by
1130
 * - 'descending' - whether to sort descending
1131 2
 * - 'sort_by' - field to sort by
1132 2
 * - 'pmgs' - personal message id (if any). Note: it may not be set.
1133 2
 * - 'label_query' - query by labels
1134 2
 * - 'start' - start id, if any
1135 2
 *
1136 2
 * @param array $pm_options options for loading
1137 2
 * @param int $id_member id member
1138
 *
1139 2
 * @return array
1140 2
 * @package PersonalMessage
1141 2
 */
1142 2
function loadPMs($pm_options, $id_member)
1143 2
{
1144
	global $options;
1145
1146
	$db = database();
1147
1148 2
	// First work out what messages we need to see - if grouped is a little trickier...
1149 2
	// Conversation mode
1150 2
	if ($pm_options['display_mode'] === 2)
1151 2
	{
1152 2
		// On a non-default sort, when using PostgreSQL we have to do a harder sort.
1153
		if ($db->title() === 'PostgreSQL' && $pm_options['sort_by_query'] !== 'pm.id_pm')
1154
		{
1155
			$sub_pms = [];
1156
			$db->fetchQuery('
1157
				SELECT
1158
					MAX({raw:sort}) AS sort_param, pm.id_pm_head
1159
				FROM {db_prefix}personal_messages AS pm' . ($pm_options['folder'] === 'sent' ? ($pm_options['sort_by'] === 'name' ? '
1160
					LEFT JOIN {db_prefix}pm_recipients AS pmr ON (pmr.id_pm = pm.id_pm)' : '') : '
1161
					INNER JOIN {db_prefix}pm_recipients AS pmr ON (pmr.id_pm = pm.id_pm
1162
						AND pmr.id_member = {int:current_member}
1163
						AND pmr.deleted = {int:not_deleted}
1164
						' . $pm_options['label_query'] . ')') . ($pm_options['sort_by'] === 'name' ? ('
1165
					LEFT JOIN {db_prefix}members AS mem ON (mem.id_member = {raw:id_member})') : '') . '
1166
				WHERE ' . ($pm_options['folder'] === 'sent' ? 'pm.id_member_from = {int:current_member}
1167
					AND pm.deleted_by_sender = {int:not_deleted}' : '1=1') . (empty($pm_options['pmsg']) ? '' : '
1168
					AND pm.id_pm = {int:id_pm}') . '
1169
				GROUP BY pm.id_pm_head
1170
				ORDER BY sort_param' . ($pm_options['descending'] ? ' DESC' : ' ASC') . (empty($pm_options['pmsg']) ? '
1171
				LIMIT ' . $pm_options['limit'] . ' OFFSET ' . $pm_options['start'] : ''),
1172
				[
1173
					'current_member' => $id_member,
1174
					'not_deleted' => 0,
1175
					'id_member' => $pm_options['folder'] === 'sent' ? 'pmr.id_member' : 'pm.id_member_from',
1176
					'id_pm' => $pm_options['pmsg'] ?? '0',
1177
					'sort' => $pm_options['sort_by_query'],
1178 2
				]
1179
			)->fetch_callback(
1180 2
				function ($row) use (&$sub_pms) {
1181
					$sub_pms[$row['id_pm_head']] = $row['sort_param'];
1182
				}
1183
			);
1184
1185
			// Now we use those results in the next query
1186
			$request = $db->query('', '
1187
				SELECT
1188
					pm.id_pm AS id_pm, pm.id_pm_head
1189
				FROM {db_prefix}personal_messages AS pm' . ($pm_options['folder'] === 'sent' ? ($pm_options['sort_by'] === 'name' ? '
1190
					LEFT JOIN {db_prefix}pm_recipients AS pmr ON (pmr.id_pm = pm.id_pm)' : '') : '
1191
					INNER JOIN {db_prefix}pm_recipients AS pmr ON (pmr.id_pm = pm.id_pm
1192
						AND pmr.id_member = {int:current_member}
1193
						AND pmr.deleted = {int:not_deleted}
1194
						' . $pm_options['label_query'] . ')') . ($pm_options['sort_by'] === 'name' ? ('
1195
					LEFT JOIN {db_prefix}members AS mem ON (mem.id_member = {raw:id_member})') : '') . '
1196
				WHERE ' . (empty($sub_pms) ? '0=1' : 'pm.id_pm IN ({array_int:pm_list})') . '
1197
				ORDER BY ' . ($pm_options['sort_by_query'] === 'pm.id_pm' && $pm_options['folder'] !== 'sent' ? 'id_pm' : '{raw:sort}') . ($pm_options['descending'] ? ' DESC' : ' ASC') . (empty($pm_options['pmsg']) ? '
1198
				LIMIT ' . $pm_options['limit'] . ' OFFSET ' . $pm_options['start'] : ''),
1199
				[
1200
					'current_member' => $id_member,
1201
					'pm_list' => array_keys($sub_pms),
1202
					'not_deleted' => 0,
1203
					'sort' => $pm_options['sort_by_query'],
1204
					'id_member' => $pm_options['folder'] === 'sent' ? 'pmr.id_member' : 'pm.id_member_from',
1205
				]
1206
			);
1207
		}
1208
		// Otherwise we can just use the pm_conversation_list option
1209
		else
1210
		{
1211
			$request = $db->query('pm_conversation_list', '
1212
				SELECT
1213
					MAX(pm.id_pm) AS id_pm, pm.id_pm_head
1214
				FROM {db_prefix}personal_messages AS pm' . ($pm_options['folder'] === 'sent' ? ($pm_options['sort_by'] === 'name' ? '
1215
					LEFT JOIN {db_prefix}pm_recipients AS pmr ON (pmr.id_pm = pm.id_pm)' : '') : '
1216
					INNER JOIN {db_prefix}pm_recipients AS pmr ON (pmr.id_pm = pm.id_pm
1217
						AND pmr.id_member = {int:current_member}
1218
						AND pmr.deleted = {int:deleted_by}
1219
						' . $pm_options['label_query'] . ')') . ($pm_options['sort_by'] === 'name' ? ('
1220
					LEFT JOIN {db_prefix}members AS mem ON (mem.id_member = {raw:pm_member})') : '') . '
1221
				WHERE ' . ($pm_options['folder'] === 'sent' ? 'pm.id_member_from = {int:current_member}
1222
					AND pm.deleted_by_sender = {int:deleted_by}' : '1=1') . (empty($pm_options['pmsg']) ? '' : '
1223
					AND pm.id_pm = {int:pmsg}') . '
1224
				GROUP BY pm.id_pm_head
1225
				ORDER BY ' . ($pm_options['sort_by_query'] === 'pm.id_pm' && $pm_options['folder'] !== 'sent' ? 'id_pm' : '{raw:sort}') . ($pm_options['descending'] ? ' DESC' : ' ASC') . (isset($pm_options['pmsg']) ? '
1226
				LIMIT ' . $pm_options['limit'] . ' OFFSET ' . $pm_options['start'] : ''),
1227
				[
1228
					'current_member' => $id_member,
1229
					'deleted_by' => 0,
1230
					'sort' => $pm_options['sort_by_query'],
1231
					'pm_member' => $pm_options['folder'] === 'sent' ? 'pmr.id_member' : 'pm.id_member_from',
1232
					'pmsg' => isset($pm_options['pmsg']) ? (int) $pm_options['pmsg'] : 0,
1233
				]
1234
			);
1235
		}
1236
	}
1237
	// If not in conversation view, then this is kinda simple!
1238
	else
1239
	{
1240
		// @todo SLOW This query uses a filesort. (inbox only.)
1241
		$request = $db->query('', '
1242
			SELECT
1243
				pm.id_pm, pm.id_pm_head, pm.id_member_from
1244
			FROM {db_prefix}personal_messages AS pm' . ($pm_options['folder'] === 'sent' ? ($pm_options['sort_by'] === 'name' ? '
1245
				LEFT JOIN {db_prefix}pm_recipients AS pmr ON (pmr.id_pm = pm.id_pm)' : '') : '
1246
				INNER JOIN {db_prefix}pm_recipients AS pmr ON (pmr.id_pm = pm.id_pm
1247
					AND pmr.id_member = {int:current_member}
1248
					AND pmr.deleted = {int:is_deleted}
1249
					' . $pm_options['label_query'] . ')') . ($pm_options['sort_by'] === 'name' ? ('
1250
				LEFT JOIN {db_prefix}members AS mem ON (mem.id_member = {raw:pm_member})') : '') . '
1251
			WHERE ' . ($pm_options['folder'] === 'sent' ? 'pm.id_member_from = {raw:current_member}
1252
				AND pm.deleted_by_sender = {int:is_deleted}' : '1=1') . (empty($pm_options['pmsg']) ? '' : '
1253
				AND pm.id_pm = {int:pmsg}') . '
1254
			ORDER BY ' . ($pm_options['sort_by_query'] === 'pm.id_pm' && $pm_options['folder'] !== 'sent' ? 'pmr.id_pm' : '{raw:sort}') . ($pm_options['descending'] ? ' DESC' : ' ASC') . (isset($pm_options['pmsg']) ? '
1255
			LIMIT ' . $pm_options['limit'] . ' OFFSET ' . $pm_options['start'] : ''),
1256
			[
1257
				'current_member' => $id_member,
1258
				'is_deleted' => 0,
1259
				'sort' => $pm_options['sort_by_query'],
1260
				'pm_member' => $pm_options['folder'] === 'sent' ? 'pmr.id_member' : 'pm.id_member_from',
1261
				'pmsg' => isset($pm_options['pmsg']) ? (int) $pm_options['pmsg'] : 0,
1262
			]
1263
		);
1264
	}
1265
	// Load the id_pms and initialize recipients.
1266
	$pms = [];
1267
	$lastData = [];
1268
	$posters = $pm_options['folder'] === 'sent' ? [$id_member] : [];
1269
	$recipients = [];
1270
	while (($row = $request->fetch_assoc()))
1271
	{
1272
		if (!isset($recipients[$row['id_pm']]))
1273
		{
1274
			if (isset($row['id_member_from']))
1275
			{
1276
				$posters[$row['id_pm']] = $row['id_member_from'];
1277
			}
1278
1279
			$pms[$row['id_pm']] = $row['id_pm'];
1280
1281
			$recipients[$row['id_pm']] = [
1282
				'to' => [],
1283
				'bcc' => []
1284
			];
1285
		}
1286
1287
		// Keep track of the last message, so we know what the head is without another query!
1288
		if ((empty($pm_options['pmid']) && (empty($options['view_newest_pm_first']) || !isset($lastData))) || empty($lastData) || (!empty($pm_options['pmid']) && $pm_options['pmid'] == $row['id_pm']))
1289
		{
1290
			$lastData = [
1291
				'id' => $row['id_pm'],
1292
				'head' => $row['id_pm_head'],
1293
			];
1294
		}
1295
	}
1296
	$request->free_result();
1297
1298
	return [$pms, $posters, $recipients, $lastData];
1299
}
1300
1301
/**
1302
 * How many PMs have you sent lately?
1303
 *
1304
 * @param int $id_member id member
1305
 * @param int $time time interval (in seconds)
1306
 *
1307
 * @return mixed
1308
 * @package PersonalMessage
1309
 */
1310
function pmCount($id_member, $time)
1311
{
1312
	$db = database();
1313
1314
	$db->fetchQuery('
1315
		SELECT
1316
			COUNT(*) AS post_count
1317
		FROM {db_prefix}personal_messages AS pm
1318
			INNER JOIN {db_prefix}pm_recipients AS pr ON (pr.id_pm = pm.id_pm)
1319
		WHERE pm.id_member_from = {int:current_member}
1320
			AND pm.msgtime > {int:msgtime}',
1321
		[
1322
			'current_member' => $id_member,
1323
			'msgtime' => time() - $time,
1324
		]
1325
	)->fetch_callback(
1326
		function ($row) use (&$pmCount) {
1327
			$pmCount = $row['post_count'];
1328
		}
1329
	);
1330
1331
	return $pmCount;
1332
}
1333
1334
/**
1335
 * This will apply rules to all unread messages.
1336
 *
1337
 * - If all_messages is set will, clearly, do it to all!
1338
 *
1339
 * @param bool $all_messages = false
1340
 * @package PersonalMessage
1341
 */
1342
function applyRules($all_messages = false)
1343
{
1344
	global $context, $options;
1345
1346
	$db = database();
1347
1348
	// Want this - duh!
1349
	loadRules();
1350
1351
	// No rules?
1352
	if (empty($context['rules']))
1353
	{
1354
		return;
1355
	}
1356
1357
	// Just unread ones?
1358
	$ruleQuery = $all_messages ? '' : ' AND pmr.is_new = 1';
1359
1360
	// @todo Apply all should have timeout protection!
1361
	// Get all the messages that match this.
1362
	$actions = [];
1363
	$db->fetchQuery('
1364
		SELECT
1365
			pmr.id_pm, pm.id_member_from, pm.subject, pm.body, mem.id_group, pmr.labels
1366
		FROM {db_prefix}pm_recipients AS pmr
1367
			INNER JOIN {db_prefix}personal_messages AS pm ON (pm.id_pm = pmr.id_pm)
1368
			LEFT JOIN {db_prefix}members AS mem ON (mem.id_member = pm.id_member_from)
1369
		WHERE pmr.id_member = {int:current_member}
1370
			AND pmr.deleted = {int:not_deleted}
1371
			' . $ruleQuery,
1372
		[
1373
			'current_member' => User::$info->id,
0 ignored issues
show
Bug Best Practice introduced by
The property id does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
1374
			'not_deleted' => 0,
1375
		]
1376
	)->fetch_callback(
1377
		function ($row) use ($context, &$actions) {
1378
			foreach ($context['rules'] as $rule)
1379
			{
1380
				$match = false;
1381
1382
				// Loop through all the criteria hoping to make a match.
1383
				foreach ($rule['criteria'] as $criterium)
1384
				{
1385
					if (($criterium['t'] === 'mid' && $criterium['v'] == $row['id_member_from'])
1386
						|| ($criterium['t'] === 'gid' && $criterium['v'] == $row['id_group'])
1387
						|| ($criterium['t'] === 'sub' && str_contains($row['subject'], $criterium['v']))
1388
						|| ($criterium['t'] === 'msg' && str_contains($row['body'], $criterium['v'])))
1389
					{
1390
						$match = true;
1391
					}
1392
					// If we're adding and one criteria don't match then we stop!
1393
					elseif ($rule['logic'] === 'and')
1394
					{
1395
						$match = false;
1396
						break;
1397
					}
1398
				}
1399
1400
				// If we have a match the rule must be true - act!
1401
				if ($match)
1402
				{
1403
					if ($rule['delete'])
1404
					{
1405
						$actions['deletes'][] = $row['id_pm'];
1406
					}
1407
					else
1408
					{
1409
						foreach ($rule['actions'] as $ruleAction)
1410
						{
1411
							if ($ruleAction['t'] === 'lab')
1412
							{
1413
								// Get a basic pot started!
1414
								if (!isset($actions['labels'][$row['id_pm']]))
1415
								{
1416
									$actions['labels'][$row['id_pm']] = empty($row['labels'])
1417
										? []
1418
										: explode(',', $row['labels']);
1419
								}
1420
1421
								$actions['labels'][$row['id_pm']][] = $ruleAction['v'];
1422
							}
1423
						}
1424
					}
1425
				}
1426
			}
1427
		}
1428
	);
1429
1430
	// Deletes are easy!
1431
	if (!empty($actions['deletes']))
1432
	{
1433
		deleteMessages($actions['deletes']);
1434
	}
1435
1436
	// Re-label?
1437
	if (!empty($actions['labels']))
1438
	{
1439
		foreach ($actions['labels'] as $pm => $labels)
1440
		{
1441
			// Quickly check each label is valid!
1442
			$realLabels = [];
1443
			foreach ($context['labels'] as $label)
1444
			{
1445
				if (in_array($label['id'], $labels) && ($label['id'] !== "-1" || empty($options['pm_remove_inbox_label'])))
1446
				{
1447
					$realLabels[] = $label['id'];
1448
				}
1449
			}
1450
1451
			$db->query('', '
1452
				UPDATE {db_prefix}pm_recipients
1453
				SET 
1454
					labels = {string:new_labels}
1455
				WHERE id_pm = {int:id_pm}
1456
					AND id_member = {int:current_member}',
1457
				[
1458
					'current_member' => User::$info->id,
1459
					'id_pm' => $pm,
1460
					'new_labels' => empty($realLabels) ? '' : implode(',', $realLabels),
1461
				]
1462
			);
1463
		}
1464
	}
1465
}
1466
1467
/**
1468
 * Load up all the rules for the current user.
1469
 *
1470
 * @param bool $reload = false
1471
 * @package PersonalMessage
1472
 */
1473
function loadRules($reload = false)
1474
{
1475
	global $context;
1476
1477
	$db = database();
1478
1479
	if (isset($context['rules']) && !$reload)
1480
	{
1481
		return;
1482
	}
1483
1484
	// This is just a simple list of "all" known rules
1485
	$context['known_rules'] = [
1486
		// member_id == "Sender Name"
1487
		'mid',
1488
		// group_id == "Sender's Groups"
1489
		'gid',
1490
		// subject == "Message Subject Contains"
1491
		'sub',
1492
		// message == "Message Body Contains"
1493
		'msg',
1494
		// buddy == "Sender is Buddy"
1495
		'bud',
1496
	];
1497
1498
	$context['rules'] = [];
1499
	$db->fetchQuery('
1500
		SELECT
1501
			id_rule, rule_name, criteria, actions, delete_pm, is_or
1502
		FROM {db_prefix}pm_rules
1503
		WHERE id_member = {int:current_member}',
1504
		[
1505
			'current_member' => User::$info->id,
0 ignored issues
show
Bug Best Practice introduced by
The property id does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
1506
		]
1507
	)->fetch_callback(
1508
		function ($row) use (&$context) {
1509
			$context['rules'][$row['id_rule']] = [
1510
				'id' => $row['id_rule'],
1511
				'name' => $row['rule_name'],
1512
				'criteria' => Util::unserialize($row['criteria']),
1513
				'actions' => Util::unserialize($row['actions']),
1514
				'delete' => $row['delete_pm'],
1515
				'logic' => $row['is_or'] ? 'or' : 'and',
1516
			];
1517
1518
			if ($row['delete_pm'])
1519
			{
1520
				$context['rules'][$row['id_rule']]['actions'][] = ['t' => 'del', 'v' => 1];
1521
			}
1522
		}
1523
	);
1524
}
1525
1526
/**
1527
 * Update PM recipient when they receive or read a new PM
1528
 *
1529
 * @param int $id_member
1530
 * @param bool $new = false
1531
 * @package PersonalMessage
1532
 */
1533
function toggleNewPM($id_member, $new = false)
1534
{
1535
	$db = database();
1536
1537
	$db->fetchQuery('
1538
		UPDATE {db_prefix}pm_recipients
1539
		SET 
1540
			is_new = ' . ($new ? '{int:new}' : '{int:not_new}') . '
1541
		WHERE id_member = {int:current_member}',
1542
		[
1543
			'current_member' => $id_member,
1544
			'new' => 1,
1545
			'not_new' => 0
1546
		]
1547
	);
1548
}
1549
1550
/**
1551
 * Load the PM limits for each group or for a specified group
1552
 *
1553
 * @param int|bool $id_group (optional) the id of a membergroup
1554
 *
1555
 * @return array
1556
 * @package PersonalMessage
1557
 */
1558
function loadPMLimits($id_group = false)
1559
{
1560
	$db = database();
1561
1562
	$groups = [];
1563
	$db->fetchQuery('
1564
		SELECT
1565
			id_group, group_name, max_messages
1566
		FROM {db_prefix}membergroups' . ($id_group ? '
1567
		WHERE id_group = {int:id_group}' : '
1568
		ORDER BY min_posts, CASE WHEN id_group < {int:newbie_group} THEN id_group ELSE 4 END, group_name'),
1569
		[
1570
			'id_group' => $id_group,
1571
			'newbie_group' => 4,
1572
		]
1573
	)->fetch_callback(
1574
		function ($row) use (&$groups) {
1575
			if ($row['id_group'] != 1)
1576
			{
1577
				$groups[$row['id_group']] = $row;
1578
			}
1579
		}
1580
	);
1581
1582
	return $groups;
1583
}
1584
1585
/**
1586
 * Retrieve the discussion one or more PMs belong to
1587
 *
1588
 * @param int[] $id_pms
1589
 *
1590
 * @return array
1591
 * @package PersonalMessage
1592
 */
1593
function getDiscussions($id_pms)
1594
{
1595
	$db = database();
1596
1597
	$pm_heads = [];
1598
	$db->fetchQuery('
1599
		SELECT
1600
			id_pm_head, id_pm
1601
		FROM {db_prefix}personal_messages
1602
		WHERE id_pm IN ({array_int:id_pms})',
1603
		[
1604
			'id_pms' => $id_pms,
1605
		]
1606
	)->fetch_callback(
1607
		function ($row) use (&$pm_heads) {
1608
			$pm_heads[$row['id_pm_head']] = $row['id_pm'];
1609
		}
1610
	);
1611
1612
	return $pm_heads;
1613
}
1614
1615
/**
1616
 * Return all the PMs belonging to one or more discussions
1617
 *
1618
 * @param int[] $pm_heads array of pm id head nodes
1619
 *
1620
 * @return array
1621
 * @package PersonalMessage
1622
 */
1623
function getPmsFromDiscussion($pm_heads)
1624
{
1625
	$db = database();
1626
1627
	$pms = [];
1628
	$db->fetchQuery('
1629
		SELECT
1630
			id_pm, id_pm_head
1631
		FROM {db_prefix}personal_messages
1632
		WHERE id_pm_head IN ({array_int:pm_heads})',
1633
		[
1634
			'pm_heads' => $pm_heads,
1635
		]
1636
	)->fetch_callback(
1637
		function ($row) use (&$pms) {
1638
			// Copy the action from the single to PM to the others.
1639
			$pms[$row['id_pm']] = $row['id_pm_head'];
1640
		}
1641
	);
1642
1643
	return $pms;
1644
}
1645
1646
/**
1647
 * Determines the PMs which need an updated label.
1648
 *
1649
 * @param array $to_label
1650
 * @param string[] $label_type
1651
 * @param int $user_id
1652
 * @return int|null
1653
 * @package PersonalMessage
1654
 */
1655
function changePMLabels($to_label, $label_type, $user_id)
1656
{
1657
	global $options;
1658
1659
	$db = database();
1660
1661
	$to_update = [];
1662
1663
	// Get information about each message...
1664
	$db->fetchQuery('
1665
		SELECT
1666
			id_pm, labels
1667
		FROM {db_prefix}pm_recipients
1668
		WHERE id_member = {int:current_member}
1669
			AND id_pm IN ({array_int:to_label})
1670
		LIMIT ' . count($to_label),
1671
		[
1672
			'current_member' => $user_id,
1673
			'to_label' => array_keys($to_label),
1674
		]
1675
	)->fetch_callback(
1676
		function ($row) use ($options, &$to_update, &$to_label, &$label_type) {
1677
			$labels = $row['labels'] === '' ? ['-1'] : explode(',', trim($row['labels']));
1678
1679
			// Already exists?  Then... unset it!
1680
			$id_label = array_search($to_label[$row['id_pm']], $labels);
1681
1682
			if ($id_label !== false && $label_type[$row['id_pm']] !== 'add')
1683
			{
1684
				unset($labels[$id_label]);
1685
			}
1686
			elseif ($label_type[$row['id_pm']] !== 'rem')
1687
			{
1688
				$labels[] = $to_label[$row['id_pm']];
1689
			}
1690
1691
			if (!empty($options['pm_remove_inbox_label'])
1692
				&& $to_label[$row['id_pm']] !== '-1'
1693
				&& ($key = array_search('-1', $labels)) !== false)
1694
			{
1695
				unset($labels[$key]);
1696
			}
1697
1698
			$set = implode(',', array_unique($labels));
1699
			if ($set === '')
1700
			{
1701
				$set = '-1';
1702
			}
1703
1704
			$to_update[$row['id_pm']] = $set;
1705
		}
1706
	);
1707
1708
	if (!empty($to_update))
1709
	{
1710
		return updatePMLabels($to_update, $user_id);
1711
	}
1712
}
1713
1714
/**
1715
 * Detects personal messages which need a new label.
1716
 *
1717
 * @param array $searchArray
1718
 * @param array $new_labels
1719
 * @param int $user_id
1720
 * @return int|null
1721
 * @package PersonalMessage
1722
 */
1723
function updateLabelsToPM($searchArray, $new_labels, $user_id)
1724
{
1725
	$db = database();
1726
1727
	$to_update = [];
1728
1729
	// Now find the messages to change.
1730
	$db->fetchQuery('
1731
		SELECT
1732
			id_pm, labels
1733
		FROM {db_prefix}pm_recipients
1734
		WHERE FIND_IN_SET({raw:find_label_implode}, labels) != 0
1735
			AND id_member = {int:current_member}',
1736
		[
1737
			'current_member' => $user_id,
1738
			'find_label_implode' => '\'' . implode('\', labels) != 0 OR FIND_IN_SET(\'', $searchArray) . '\'',
1739
		]
1740
	)->fetch_callback(
1741
		function ($row) use (&$to_update, $searchArray, $new_labels) {
1742
			// Do the long task of updating them...
1743
			$toChange = explode(',', $row['labels']);
1744
1745
			foreach ($toChange as $key => $value)
1746
			{
1747
				if (in_array($value, $searchArray))
1748
				{
1749
					if (isset($searchArray[$value]))
1750
					{
1751
						$toChange[$key] = $new_labels[$value];
1752
					}
1753
					else
1754
					{
1755
						unset($toChange[$key]);
1756
					}
1757
				}
1758
			}
1759
1760
			if (empty($toChange))
1761
			{
1762
				$toChange[] = '-1';
1763
			}
1764
1765
			$to_update[$row['id_pm']] = implode(',', array_unique($toChange));
1766
		}
1767
	);
1768
1769
	if (!empty($to_update))
1770
	{
1771
		return updatePMLabels($to_update, $user_id);
1772
	}
1773
}
1774
1775
/**
1776
 * Updates PMs with their new label.
1777
 *
1778
 * @param array $to_update
1779
 * @param int $user_id
1780
 * @return int
1781
 * @package PersonalMessage
1782
 */
1783
function updatePMLabels($to_update, $user_id)
1784
{
1785
	$db = database();
1786
1787
	$updateErrors = 0;
1788
1789
	foreach ($to_update as $id_pm => $set)
1790
	{
1791
		// Check that this string isn't going to be too large for the database.
1792
		if (strlen($set) > 60)
1793
		{
1794
			$updateErrors++;
1795
1796
			// Make the string as long as possible and update anyway
1797
			$set = substr($set, 0, 60);
1798
			$set = substr($set, 0, strrpos($set, ','));
1799
		}
1800
1801
		$db->query('', '
1802
			UPDATE {db_prefix}pm_recipients
1803
			SET 
1804
				labels = {string:labels}
1805
			WHERE id_pm = {int:id_pm}
1806
				AND id_member = {int:current_member}',
1807
			[
1808
				'current_member' => $user_id,
1809
				'id_pm' => $id_pm,
1810
				'labels' => $set,
1811
			]
1812
		);
1813
	}
1814
1815
	return $updateErrors;
1816
}
1817
1818
/**
1819
 * Gets PMs older than a specific date.
1820
 *
1821
 * @param int $user_id the user's id.
1822
 * @param int $time timestamp with a specific date
1823
 * @return array
1824
 * @package PersonalMessage
1825
 */
1826
function getPMsOlderThan($user_id, $time)
1827
{
1828
	$db = database();
1829
1830
	// Array to store the IDs in.
1831
	$pm_ids = [];
1832
1833
	// Select all the messages they have sent older than $time.
1834
	$db->fetchQuery('
1835
		SELECT
1836
			id_pm
1837
		FROM {db_prefix}personal_messages
1838
		WHERE deleted_by_sender = {int:not_deleted}
1839
			AND id_member_from = {int:current_member}
1840
			AND msgtime < {int:msgtime}',
1841
		[
1842
			'current_member' => $user_id,
1843
			'not_deleted' => 0,
1844
			'msgtime' => $time,
1845
		]
1846
	)->fetch_callback(
1847
		function ($row) use (&$pm_ids) {
1848
			$pm_ids[] = $row['id_pm'];
1849
		}
1850
	);
1851
1852
	// This is the inbox
1853
	$db->fetchQuery('
1854
		SELECT
1855
			pmr.id_pm
1856
		FROM {db_prefix}pm_recipients AS pmr
1857
			INNER JOIN {db_prefix}personal_messages AS pm ON (pm.id_pm = pmr.id_pm)
1858
		WHERE pmr.deleted = {int:not_deleted}
1859
			AND pmr.id_member = {int:current_member}
1860
			AND pm.msgtime < {int:msgtime}',
1861
		[
1862
			'current_member' => $user_id,
1863
			'not_deleted' => 0,
1864
			'msgtime' => $time,
1865
		]
1866
	)->fetch_callback(
1867
		function ($row) use (&$pm_ids) {
1868
			$pm_ids[] = $row['id_pm'];
1869
		}
1870
	);
1871
1872
	return $pm_ids;
1873
}
1874
1875
/**
1876
 * Used to delete PM rules from the given member.
1877
 *
1878
 * @param int $id_member
1879
 * @param int[] $rule_changes
1880
 * @package PersonalMessage
1881
 */
1882
function deletePMRules($id_member, $rule_changes)
1883
{
1884
	$db = database();
1885
1886
	$db->query('', '
1887
		DELETE FROM {db_prefix}pm_rules
1888
		WHERE id_rule IN ({array_int:rule_list})
1889
		AND id_member = {int:current_member}',
1890
		[
1891
			'current_member' => $id_member,
1892
			'rule_list' => $rule_changes,
1893
		]
1894
	);
1895
}
1896
1897
/**
1898
 * Updates a personal messaging rule action for the given member.
1899
 *
1900
 * @param int $id_rule
1901
 * @param int $id_member
1902
 * @param array $actions
1903
 * @package PersonalMessage
1904
 */
1905
function updatePMRuleAction($id_rule, $id_member, $actions)
1906
{
1907
	$db = database();
1908
1909
	$db->query('', '
1910
		UPDATE {db_prefix}pm_rules
1911
		SET 
1912
			actions = {string:actions}
1913
		WHERE id_rule = {int:id_rule}
1914
			AND id_member = {int:current_member}',
1915
		[
1916
			'current_member' => $id_member,
1917
			'id_rule' => $id_rule,
1918
			'actions' => serialize($actions),
1919
		]
1920
	);
1921
}
1922
1923
/**
1924
 * Add a new PM rule to the database.
1925
 *
1926
 * @param int $id_member
1927
 * @param string $ruleName
1928
 * @param string $criteria
1929
 * @param string $actions
1930
 * @param int $doDelete
1931
 * @param int $isOr
1932
 * @package PersonalMessage
1933
 */
1934
function addPMRule($id_member, $ruleName, $criteria, $actions, $doDelete, $isOr)
1935
{
1936
	$db = database();
1937
1938
	$db->insert('',
1939
		'{db_prefix}pm_rules',
1940
		[
1941
			'id_member' => 'int', 'rule_name' => 'string', 'criteria' => 'string', 'actions' => 'string',
1942
			'delete_pm' => 'int', 'is_or' => 'int',
1943
		],
1944
		[
1945
			$id_member, $ruleName, $criteria, $actions, $doDelete, $isOr,
1946
		],
1947
		['id_rule']
1948
	);
1949
}
1950
1951
/**
1952
 * Updates a personal messaging rule for the given member.
1953
 *
1954
 * @param int $id_member
1955
 * @param int $id_rule
1956
 * @param string $ruleName
1957
 * @param string $criteria
1958
 * @param string $actions
1959
 * @param int $doDelete
1960
 * @param int $isOr
1961
 * @package PersonalMessage
1962
 */
1963
function updatePMRule($id_member, $id_rule, $ruleName, $criteria, $actions, $doDelete, $isOr)
1964
{
1965
	$db = database();
1966
1967
	$db->query('', '
1968
		UPDATE {db_prefix}pm_rules
1969
		SET 
1970
			rule_name = {string:rule_name}, criteria = {string:criteria}, actions = {string:actions},
1971
			delete_pm = {int:delete_pm}, is_or = {int:is_or}
1972
		WHERE id_rule = {int:id_rule}
1973
			AND id_member = {int:current_member}',
1974
		[
1975
			'current_member' => $id_member,
1976
			'delete_pm' => $doDelete,
1977
			'is_or' => $isOr,
1978
			'id_rule' => $id_rule,
1979
			'rule_name' => $ruleName,
1980
			'criteria' => $criteria,
1981
			'actions' => $actions,
1982
		]
1983
	);
1984
}
1985
1986
/**
1987
 * Used to set a replied status for a given PM.
1988
 *
1989
 * @param int $id_member
1990
 * @param int $replied_to
1991
 * @package PersonalMessage
1992
 */
1993
function setPMRepliedStatus($id_member, $replied_to)
1994
{
1995
	$db = database();
1996
1997
	$db->query('', '
1998
		UPDATE {db_prefix}pm_recipients
1999
		SET 
2000
			is_read = is_read | 2
2001
		WHERE id_pm = {int:replied_to}
2002
			AND id_member = {int:current_member}',
2003
		[
2004
			'current_member' => $id_member,
2005
			'replied_to' => $replied_to,
2006
		]
2007
	);
2008
}
2009
2010
/**
2011
 * Given the head PM, loads all other PM's that share the same head node
2012
 *
2013
 * - Used to load the conversation view of a PM
2014
 *
2015
 * @param int $head id of the head pm of the conversation
2016
 * @param array $recipients
2017
 * @param string $folder the current folder we are working in
2018
 *
2019
 * @return array
2020
 * @package PersonalMessage
2021
 */
2022
function loadConversationList($head, &$recipients, $folder = '')
2023
{
2024
	$db = database();
2025
2026
	$display_pms = [];
2027
	$posters = [];
2028
	$db->fetchQuery('
2029
		SELECT
2030
			pm.id_pm, pm.id_member_from, pm.deleted_by_sender, pmr.id_member, pmr.deleted
2031
		FROM {db_prefix}personal_messages AS pm
2032
			INNER JOIN {db_prefix}pm_recipients AS pmr ON (pmr.id_pm = pm.id_pm)
2033
		WHERE pm.id_pm_head = {int:id_pm_head}
2034
			AND ((pm.id_member_from = {int:current_member} AND pm.deleted_by_sender = {int:not_deleted})
2035
				OR (pmr.id_member = {int:current_member} AND pmr.deleted = {int:not_deleted}))
2036
		ORDER BY pm.id_pm',
2037
		[
2038
			'current_member' => User::$info->id,
0 ignored issues
show
Bug Best Practice introduced by
The property id does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
2039
			'id_pm_head' => $head,
2040
			'not_deleted' => 0,
2041
		]
2042
	)->fetch_callback(
2043
		function ($row) use ($folder, &$recipients, &$display_pms, &$posters) {
2044
			// This is, frankly, a joke.
2045
			// We will put in a workaround for people sending to themselves - yawn!
2046
			if ($folder === 'sent' && $row['id_member_from'] == User::$info->id && $row['deleted_by_sender'] == 1)
0 ignored issues
show
Bug Best Practice introduced by
The property id does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
2047
			{
2048
				return;
2049
			}
2050
2051
			if (($row['id_member'] == User::$info->id) && $row['deleted'] == 1)
2052
			{
2053
				return;
2054
			}
2055
2056
			if (!isset($recipients[$row['id_pm']]))
2057
			{
2058
				$recipients[$row['id_pm']] = [
2059
					'to' => [],
2060
					'bcc' => []
2061
				];
2062
			}
2063
2064
			$display_pms[] = $row['id_pm'];
2065
			$posters[$row['id_pm']] = $row['id_member_from'];
2066
		}
2067
	);
2068
2069
	return [$display_pms, $posters];
2070
}
2071
2072
/**
2073
 * Used to determine if any message in a conversation thread is unread
2074
 *
2075
 * - Returns array of keys with the head id and value details of the newest
2076
 * unread message.
2077
 *
2078
 * @param int[] $pms array of pm ids to search
2079
 *
2080
 * @return array
2081
 * @package PersonalMessage
2082
 */
2083
function loadConversationUnreadStatus($pms)
2084
{
2085
	$db = database();
2086
2087
	// Make it an array if its not
2088
	if (!is_array($pms))
0 ignored issues
show
introduced by
The condition is_array($pms) is always true.
Loading history...
2089
	{
2090
		$pms = [$pms];
2091
	}
2092
2093
	// Find the heads for this group of PM's
2094
	$head_pms = [];
2095
	$db->fetchQuery('
2096
		SELECT
2097
			id_pm_head, id_pm
2098
		FROM {db_prefix}personal_messages
2099
		WHERE id_pm IN ({array_int:id_pm})',
2100
		[
2101
			'id_pm' => $pms,
2102
		]
2103
	)->fetch_callback(
2104
		function ($row) use (&$head_pms) {
2105
			$head_pms[$row['id_pm_head']] = $row['id_pm'];
2106
		}
2107
	);
2108
2109
	// Find any unread PM's this member has under these head pm id's
2110
	$unread_pms = [];
2111
	$db->fetchQuery('
2112
		SELECT
2113
			MAX(pm.id_pm) AS id_pm, pm.id_pm_head
2114
		FROM {db_prefix}personal_messages AS pm
2115
			INNER JOIN {db_prefix}pm_recipients AS pmr ON (pmr.id_pm = pm.id_pm)
2116
		WHERE pm.id_pm_head IN ({array_int:id_pm_head})
2117
			AND (pmr.id_member = {int:current_member} AND pmr.deleted = {int:not_deleted})
2118
			AND (pmr.is_read & 1 = 0)
2119
		GROUP BY pm.id_pm_head',
2120
		[
2121
			'current_member' => User::$info->id,
0 ignored issues
show
Bug Best Practice introduced by
The property id does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
2122
			'id_pm_head' => array_keys($head_pms),
2123
			'not_deleted' => 0,
2124
		]
2125
	)->fetch_callback(
2126
		function ($row) use ($head_pms, &$unread_pms) {
2127
			// Return the results under the original index since that's what we are
2128
			// displaying in the subject list
2129
			$index = $head_pms[$row['id_pm_head']];
2130
			$unread_pms[$index] = $row;
2131
		}
2132
	);
2133
2134
	return $unread_pms;
2135
}
2136
2137
/**
2138
 * Get all recipients for a given group of PM's, loads some basic member information for each
2139
 *
2140
 * - Will not include bcc-recipients for an inbox
2141
 * - Keeps track if a message has been replied / read
2142
 * - Tracks any message labels in use
2143
 * - If optional search parameter is set to true will return message first label, useful for linking
2144
 *
2145
 * @param int[] $all_pms
2146
 * @param array $recipients
2147
 * @param string $folder
2148
 * @param bool $search
2149
 *
2150
 * @return array
2151
 * @package PersonalMessage
2152
 *
2153
 */
2154
function loadPMRecipientInfo($all_pms, &$recipients, $folder = '', $search = false)
2155
{
2156
	global $txt, $scripturl, $context;
2157
2158
	$db = database();
2159
2160
	$message_labels = [];
2161
	foreach ($all_pms as $pmid)
2162
	{
2163
		$message_labels[$pmid] = [];
2164
	}
2165
	$message_replied = [];
2166
	$message_unread = [];
2167
	$message_first_label = [];
2168
2169
	// Get the recipients for all these PM's
2170
	$request = $db->fetchQuery('
2171
		SELECT
2172
			pmr.id_pm, pmr.bcc, pmr.labels, pmr.is_read,
2173
			mem_to.id_member AS id_member_to, mem_to.real_name AS to_name
2174
		FROM {db_prefix}pm_recipients AS pmr
2175
			LEFT JOIN {db_prefix}members AS mem_to ON (mem_to.id_member = pmr.id_member)
2176
		WHERE pmr.id_pm IN ({array_int:pm_list})',
2177
		[
2178
			'pm_list' => $all_pms,
2179
		]
2180
	);
2181
	while (($row = $request->fetch_assoc()))
2182
	{
2183
		// Sent folder recipients
2184
		if ($folder === 'sent' || empty($row['bcc']))
2185
		{
2186
			$recipients[$row['id_pm']][empty($row['bcc']) ? 'to' : 'bcc'][] = empty($row['id_member_to']) ? $txt['guest_title'] : '<a href="' . $scripturl . '?action=profile;u=' . $row['id_member_to'] . '">' . $row['to_name'] . '</a>';
2187
		}
2188
2189
		// Don't include bcc-recipients if its your inbox, you're not supposed to know :P
2190
		if ($row['id_member_to'] == User::$info->id && $folder !== 'sent')
0 ignored issues
show
Bug Best Practice introduced by
The property id does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
2191
		{
2192
			// Read and replied to status for this message
2193
			$message_replied[$row['id_pm']] = $row['is_read'] & 2;
2194
			$message_unread[$row['id_pm']] = $row['is_read'] == 0;
2195
			$message_labels[$row['id_pm']] = [];
2196
2197
			$row['labels'] = $row['labels'] === '' ? [] : explode(',', $row['labels']);
2198
			foreach ($row['labels'] as $v)
2199
			{
2200
				if (isset($context['labels'][(int) $v]))
2201
				{
2202
					$message_labels[$row['id_pm']][(int) $v] = ['id' => $v, 'name' => $context['labels'][(int) $v]['name']];
2203
				}
2204
2205
				// Here we find the first label on a message - used for linking to posts
2206
				if ($search && (!isset($message_first_label[$row['id_pm']]) && !in_array('-1', $row['labels'])))
2207
				{
2208
					$message_first_label[$row['id_pm']] = (int) $v;
2209
				}
2210
			}
2211
		}
2212
	}
2213
	$request->free_result();
2214
2215
	return [$message_labels, $message_replied, $message_unread, ($search ? $message_first_label : '')];
2216
}
2217
2218
/**
2219
 * This is used by preparePMContext_callback.
2220
 *
2221
 * - That function uses these query results and handles the free_result action as well.
2222
 *
2223
 * @param int[] $pms array of PM ids to fetch
2224
 * @param string[] $orderBy raw query defining how to order the results
2225
 * @return bool|\ElkArte\Database\AbstractResult
2226
 * @package PersonalMessage
2227
 */
2228
function loadPMSubjectRequest($pms, $orderBy)
2229
{
2230
	$db = database();
2231
2232
	// Separate query for these bits!
2233
	return $db->query('', '
2234
		SELECT
2235
			pm.id_pm, pm.subject, pm.id_member_from, pm.msgtime, COALESCE(mem.real_name, pm.from_name) AS from_name,
2236
			COALESCE(mem.id_member, 0) AS not_guest,
2237
			{string:empty} as body, {int:smileys_enabled} as smileys_enabled
2238
		FROM {db_prefix}personal_messages AS pm
2239
			LEFT JOIN {db_prefix}members AS mem ON (mem.id_member = pm.id_member_from)
2240
		WHERE pm.id_pm IN ({array_int:pm_list})
2241
		ORDER BY ' . implode(', ', $orderBy) . '
2242
		LIMIT ' . count($pms),
2243
		[
2244
			'pm_list' => $pms,
2245
			'empty' => '',
2246
			'smileys_enabled' => 1,
2247
			'from_time' => 0,
2248
		]
2249
	);
2250
}
2251
2252
/**
2253
 * Similar to loadSubjectRequest, this is used by preparePMContext_callback.
2254
 *
2255
 * - That function uses these query results and handles the free_result action as well.
2256
 *
2257
 * @param int[] $display_pms list of PM's to fetch
2258
 * @param string $sort_by_query raw query used in the sorting option
2259
 * @param string $sort_by used to signal when addition joins are needed
2260
 * @param bool $descending if true descending order of display
2261
 * @param int|string $display_mode how are they being viewed, all, conversation, etc
2262
 * @param string $folder current pm folder
2263
 * @return bool|\ElkArte\Database\AbstractResult
2264
 * @package PersonalMessage
2265
 */
2266
function loadPMMessageRequest($display_pms, $sort_by_query, $sort_by, $descending, $display_mode = '', $folder = '')
2267
{
2268
	$db = database();
2269
2270
	return $db->query('', '
2271
		SELECT
2272
			pm.id_pm, pm.subject, pm.id_member_from, pm.body, pm.msgtime, pm.from_name,
2273
			{int:smileys_enabled} as smileys_enabled
2274
		FROM {db_prefix}personal_messages AS pm' . ($folder === 'sent' ? '
2275
			LEFT JOIN {db_prefix}pm_recipients AS pmr ON (pmr.id_pm = pm.id_pm)' : '') . ($sort_by === 'name' ? '
2276
			LEFT JOIN {db_prefix}members AS mem ON (mem.id_member = {raw:id_member})' : '') . '
2277
		WHERE pm.id_pm IN ({array_int:display_pms})' . ($folder === 'sent' ? '
2278
		GROUP BY pm.id_pm, pm.subject, pm.id_member_from, pm.body, pm.msgtime, pm.from_name' : '') . '
2279
		ORDER BY ' . ($display_mode == 2 ? 'pm.id_pm' : $sort_by_query) . ($descending ? ' DESC' : ' ASC') . '
2280
		LIMIT ' . count($display_pms),
2281
		[
2282
			'display_pms' => $display_pms,
2283
			'id_member' => $folder === 'sent' ? 'pmr.id_member' : 'pm.id_member_from',
2284
			'smileys_enabled' => 1,
2285
		]
2286
	);
2287
}
2288
2289
/**
2290
 * Simple function to validate that a PM was sent to the current user
2291
 *
2292
 * @param int $pmsg id of the pm we are checking
2293
 *
2294
 * @return bool
2295
 * @package PersonalMessage
2296
 */
2297
function checkPMReceived($pmsg)
2298
{
2299
	$db = database();
2300
2301
	$request = $db->fetchQuery('
2302
		SELECT
2303
			id_pm
2304
		FROM {db_prefix}pm_recipients
2305
		WHERE id_pm = {int:id_pm}
2306
			AND id_member = {int:current_member}
2307
		LIMIT 1',
2308
		[
2309
			'current_member' => User::$info->id,
0 ignored issues
show
Bug Best Practice introduced by
The property id does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
2310
			'id_pm' => $pmsg,
2311
		]
2312
	);
2313
	$isReceived = $request->num_rows() !== 0;
2314
	$request->free_result();
2315
2316
	return $isReceived;
2317
}
2318
2319
/**
2320
 * Loads a pm by ID for use as a quoted pm in a new message
2321
 *
2322
 * @param int $pmsg
2323
 * @param bool $isReceived
2324
 *
2325
 * @return array
2326
 * @package PersonalMessage
2327
 */
2328
function loadPMQuote($pmsg, $isReceived)
2329
{
2330
	$db = database();
2331
2332
	// Get the quoted message (and make sure you're allowed to see this quote!).
2333
	$request = $db->fetchQuery('
2334
		SELECT
2335
			pm.id_pm, CASE WHEN pm.id_pm_head = {int:id_pm_head_empty} THEN pm.id_pm ELSE pm.id_pm_head END AS pm_head,
2336
			pm.body, pm.subject, pm.msgtime,
2337
			mem.member_name, COALESCE(mem.id_member, 0) AS id_member, COALESCE(mem.real_name, pm.from_name) AS real_name
2338
		FROM {db_prefix}personal_messages AS pm' . (!$isReceived ? '' : '
2339
			INNER JOIN {db_prefix}pm_recipients AS pmr ON (pmr.id_pm = {int:id_pm})') . '
2340
			LEFT JOIN {db_prefix}members AS mem ON (mem.id_member = pm.id_member_from)
2341
		WHERE pm.id_pm = {int:id_pm}' . (!$isReceived ? '
2342
			AND pm.id_member_from = {int:current_member}' : '
2343
			AND pmr.id_member = {int:current_member}') . '
2344
		LIMIT 1',
2345
		[
2346
			'current_member' => User::$info->id,
0 ignored issues
show
Bug Best Practice introduced by
The property id does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
2347
			'id_pm_head_empty' => 0,
2348
			'id_pm' => $pmsg,
2349
		]
2350
	);
2351
	$row_quoted = $request->fetch_assoc();
2352
	$request->free_result();
2353
2354
	return empty($row_quoted) ? false : $row_quoted;
0 ignored issues
show
Bug Best Practice introduced by
The expression return empty($row_quoted) ? false : $row_quoted could also return false which is incompatible with the documented return type array. Did you maybe forget to handle an error condition?

If the returned type also contains false, it is an indicator that maybe an error condition leading to the specific return statement remains unhandled.

Loading history...
2355
}
2356
2357
/**
2358
 * For a given PM ID, loads all "other" recipients, (excludes the current member)
2359
 *
2360
 * - Will optionally count the number of bcc recipients and return that count
2361
 *
2362
 * @param int $pmsg
2363
 * @param bool $bcc_count
2364
 *
2365
 * @return array
2366
 * @package PersonalMessage
2367
 */
2368
function loadPMRecipientsAll($pmsg, $bcc_count = false)
2369
{
2370
	global $scripturl, $txt;
2371
2372
	$db = database();
2373
2374
	$recipients = [];
2375
	$hidden_recipients = 0;
2376
	$db->fetchQuery('
2377
		SELECT
2378
			mem.id_member, mem.real_name, pmr.bcc
2379
		FROM {db_prefix}pm_recipients AS pmr
2380
			INNER JOIN {db_prefix}members AS mem ON (mem.id_member = pmr.id_member)
2381
		WHERE pmr.id_pm = {int:id_pm}
2382
			AND pmr.id_member != {int:current_member}' . ($bcc_count === true ? '' : '
2383
			AND pmr.bcc = {int:not_bcc}'),
2384
		[
2385
			'current_member' => User::$info->id,
0 ignored issues
show
Bug Best Practice introduced by
The property id does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
2386
			'id_pm' => $pmsg,
2387
			'not_bcc' => 0,
2388
		]
2389
	)->fetch_callback(
2390
		function ($row) use (&$recipients, &$hidden_recipients, $bcc_count, $scripturl) {
2391
			// If it's hidden we still don't reveal their names
2392
			if ($bcc_count && $row['bcc'])
2393
			{
2394
				$hidden_recipients++;
2395
			}
2396
2397
			$recipients[] = [
2398
				'id' => $row['id_member'],
2399
				'name' => htmlspecialchars($row['real_name'], ENT_COMPAT, 'UTF-8'),
2400
				'link' => '[url=' . $scripturl . '?action=profile;u=' . $row['id_member'] . ']' . $row['real_name'] . '[/url]',
2401
			];
2402
		}
2403
	);
2404
2405
	// If bcc count was requested, we return the number of bcc members, but not the names
2406
	if ($bcc_count)
2407
	{
2408
		$recipients[] = [
2409
			'id' => 'bcc',
2410
			'name' => sprintf($txt['pm_report_pm_hidden'], $hidden_recipients),
2411
			'link' => sprintf($txt['pm_report_pm_hidden'], $hidden_recipients)
2412
		];
2413
	}
2414
2415
	return $recipients;
2416
}
2417
2418
/**
2419
 * Simply loads a personal message by ID
2420
 *
2421
 * - Supplied ID must have been sent to the user id requesting it and it must not have been deleted
2422
 *
2423
 * @param int $pm_id
2424
 *
2425
 * @return array
2426
 * @throws \ElkArte\Exceptions\Exception no_access
2427
 * @package PersonalMessage
2428
 *
2429
 */
2430
function loadPersonalMessage($pm_id)
2431
{
2432
	$db = database();
2433
2434
	// First, pull out the message contents, and verify it actually went to them!
2435
	$request = $db->fetchQuery('
2436
		SELECT
2437
			pm.subject, pm.body, pm.msgtime, pm.id_member_from,
2438
			COALESCE(m.real_name, pm.from_name) AS sender_name,
2439
			pm.from_name AS poster_name, msgtime
2440
		FROM {db_prefix}personal_messages AS pm
2441
			INNER JOIN {db_prefix}pm_recipients AS pmr ON (pmr.id_pm = pm.id_pm)
2442
			LEFT JOIN {db_prefix}members AS m ON (m.id_member = pm.id_member_from)
2443
		WHERE pm.id_pm = {int:id_pm}
2444
			AND pmr.id_member = {int:current_member}
2445
			AND pmr.deleted = {int:not_deleted}
2446
		LIMIT 1',
2447
		[
2448
			'current_member' => User::$info->id,
0 ignored issues
show
Bug Best Practice introduced by
The property id does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
2449
			'id_pm' => $pm_id,
2450
			'not_deleted' => 0,
2451
		]
2452
	);
2453
	// Can only be a hacker here!
2454
	if ($request->num_rows() === 0)
2455
	{
2456
		throw new \ElkArte\Exceptions\Exception('no_access', false);
2457
	}
2458
	$pm_details = $request->fetch_row();
2459
	$request->free_result();
2460
2461
	return $pm_details;
2462
}
2463
2464
/**
2465
 * Finds the number of results that a search would produce
2466
 *
2467
 * @param string $userQuery raw query, used if we are searching for specific users
2468
 * @param string $labelQuery raw query, used if we are searching only specific labels
2469
 * @param string $timeQuery raw query, used if we are limiting results to time periods
2470
 * @param string $searchQuery raw query, the actual thing you are searching for in the subject and/or body
2471
 * @param array $searchq_parameters value parameters used in the above query
2472
 * @return int
2473
 * @package PersonalMessage
2474
 */
2475
function numPMSeachResults($userQuery, $labelQuery, $timeQuery, $searchQuery, $searchq_parameters)
2476
{
2477
	global $context;
2478
2479
	$db = database();
2480
2481
	// Get the amount of results.
2482
	$request = $db->fetchQuery('
2483
		SELECT
2484
			COUNT(*)
2485
		FROM {db_prefix}pm_recipients AS pmr
2486
			INNER JOIN {db_prefix}personal_messages AS pm ON (pm.id_pm = pmr.id_pm)
2487
		WHERE ' . ($context['folder'] === 'inbox' ? '
2488
			pmr.id_member = {int:current_member}
2489
			AND pmr.deleted = {int:not_deleted}' : '
2490
			pm.id_member_from = {int:current_member}
2491
			AND pm.deleted_by_sender = {int:not_deleted}') . '
2492
			' . $userQuery . $labelQuery . $timeQuery . '
2493
			AND (' . $searchQuery . ')',
2494
		array_merge($searchq_parameters, [
2495
			'current_member' => User::$info->id,
0 ignored issues
show
Bug Best Practice introduced by
The property id does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
2496
			'not_deleted' => 0,
2497
		])
2498
	);
2499
	list ($numResults) = $request->fetch_row();
2500
	$request->free_result();
2501
2502
	return $numResults;
2503
}
2504
2505
/**
2506
 * Gets all the matching message ids, senders and head pm nodes, using standard search only (No caching and the like!)
2507
 *
2508
 * @param string $userQuery raw query, used if we are searching for specific users
2509
 * @param string $labelQuery raw query, used if we are searching only specific labels
2510
 * @param string $timeQuery raw query, used if we are limiting results to time periods
2511
 * @param string $searchQuery raw query, the actual thing you are searching for in the subject and/or body
2512
 * @param array $searchq_parameters value parameters used in the above query
2513
 * @param array $search_params additional search parameters, like sort and direction
2514
 *
2515
 * @return array
2516
 * @package PersonalMessage
2517
 */
2518
function loadPMSearchMessages($userQuery, $labelQuery, $timeQuery, $searchQuery, $searchq_parameters, $search_params)
2519
{
2520
	global $context, $modSettings;
2521
2522
	$db = database();
2523
2524
	$foundMessages = [];
2525
	$posters = [];
2526
	$head_pms = [];
2527
	$db->fetchQuery('
2528
		SELECT
2529
			pm.id_pm, pm.id_pm_head, pm.id_member_from
2530
		FROM {db_prefix}pm_recipients AS pmr
2531
			INNER JOIN {db_prefix}personal_messages AS pm ON (pm.id_pm = pmr.id_pm)
2532
		WHERE ' . ($context['folder'] === 'inbox' ? '
2533
			pmr.id_member = {int:current_member}
2534
			AND pmr.deleted = {int:not_deleted}' : '
2535
			pm.id_member_from = {int:current_member}
2536
			AND pm.deleted_by_sender = {int:not_deleted}') . '
2537
			' . $userQuery . $labelQuery . $timeQuery . '
2538
			AND (' . $searchQuery . ')
2539
		ORDER BY ' . $search_params['sort'] . ' ' . $search_params['sort_dir'] . '
2540
		LIMIT ' . $context['start'] . ', ' . $modSettings['search_results_per_page'],
2541
		array_merge($searchq_parameters, [
2542
			'current_member' => User::$info->id,
0 ignored issues
show
Bug Best Practice introduced by
The property id does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
2543
			'not_deleted' => 0,
2544
		])
2545
	)->fetch_callback(
2546
		function ($row) use (&$foundMessages, &$posters, &$head_pms)
2547
		{
2548
			$foundMessages[] = $row['id_pm'];
2549
			$posters[] = $row['id_member_from'];
2550
			$head_pms[$row['id_pm']] = $row['id_pm_head'];
2551
		}
2552
	);
2553
2554
	return [$foundMessages, $posters, $head_pms];
2555
}
2556
2557
/**
2558
 * When we are in conversation view, we need to find the base head pm of the
2559
 * conversation.  This will set the root head id to each of the node heads
2560
 *
2561
 * @param int[] $head_pms array of pm ids that were found in the id_pm_head col
2562
 * during the initial search
2563
 *
2564
 * @return array
2565
 * @package PersonalMessage
2566
 */
2567
function loadPMSearchHeads($head_pms)
2568
{
2569
	$db = database();
2570
2571
	$real_pm_ids = [];
2572
	$db->fetchQuery('
2573
		SELECT
2574
			MAX(pm.id_pm) AS id_pm, pm.id_pm_head
2575
		FROM {db_prefix}personal_messages AS pm
2576
			INNER JOIN {db_prefix}pm_recipients AS pmr ON (pmr.id_pm = pm.id_pm)
2577
		WHERE pm.id_pm_head IN ({array_int:head_pms})
2578
			AND pmr.id_member = {int:current_member}
2579
			AND pmr.deleted = {int:not_deleted}
2580
		GROUP BY pm.id_pm_head
2581
		LIMIT {int:limit}',
2582
		[
2583
			'head_pms' => array_unique($head_pms),
2584
			'current_member' => User::$info->id,
0 ignored issues
show
Bug Best Practice introduced by
The property id does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
2585
			'not_deleted' => 0,
2586
			'limit' => count($head_pms),
2587
		]
2588
	)->fetch_callback(
2589
		function ($row) use (&$real_pm_ids)
2590
		{
2591
			$real_pm_ids[$row['id_pm_head']] = $row['id_pm'];
2592
		}
2593
	);
2594
2595
	return $real_pm_ids;
2596
}
2597
2598
/**
2599
 * Loads the actual details of the PM's that were found during the search stage
2600
 *
2601
 * @param int[] $foundMessages array of found message id's
2602
 * @param array $search_params as specified in the form, here used for sorting
2603
 *
2604
 * @return array
2605
 * @package PersonalMessage
2606
 */
2607
function loadPMSearchResults($foundMessages, $search_params)
2608
{
2609
	$db = database();
2610
2611
	// Prepare the query for the callback
2612
	$search_results = [];
2613
	$db->fetchQuery('
2614
		SELECT
2615
			pm.id_pm, pm.subject, pm.id_member_from, pm.body, pm.msgtime, pm.from_name
2616
		FROM {db_prefix}personal_messages AS pm
2617
		WHERE pm.id_pm IN ({array_int:message_list})
2618
		ORDER BY ' . $search_params['sort'] . ' ' . $search_params['sort_dir'] . '
2619
		LIMIT ' . count($foundMessages),
2620
		[
2621
			'message_list' => $foundMessages,
2622
		]
2623
	)->fetch_callback(
2624
		function ($row) use (&$search_results) {
2625
			$search_results[] = $row;
2626
		}
2627
	);
2628
2629
	return $search_results;
2630
}
2631