Passed
Push — development ( 09659b...ad8161 )
by Spuds
01:19 queued 20s
created

getNewPMs()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 41
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 2.1715

Importance

Changes 0
Metric Value
cc 2
eloc 15
c 0
b 0
f 0
nc 2
nop 2
dl 0
loc 41
ccs 13
cts 20
cp 0.65
crap 2.1715
rs 9.7666
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
 * Get the number of PMs.
164
 *
165
 * @param bool $descending
166
 * @param int|null $pmID
167
 * @param string $labelQuery
168
 * @return int
169
 * @package PersonalMessage
170
 */
171
function getPMCount($descending = false, $pmID = null, $labelQuery = '')
172
{
173
	global $context;
174
175
	$db = database();
176
177
	// Figure out how many messages there are.
178
	if ($context['folder'] === 'sent')
179
	{
180
		$request = $db->fetchQuery('
181
			SELECT
182
				COUNT(' . ($context['display_mode'] == 2 ? 'DISTINCT id_pm_head' : '*') . ')
183
			FROM {db_prefix}personal_messages
184
			WHERE id_member_from = {int:current_member}
185
				AND deleted_by_sender = {int:not_deleted}' . ($pmID !== null ? '
186
				AND id_pm ' . ($descending ? '>' : '<') . ' {int:id_pm}' : ''),
187
			[
188
				'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...
189
				'not_deleted' => 0,
190
				'id_pm' => $pmID,
191
			]
192
		);
193
	}
194
	else
195
	{
196
		$request = $db->fetchQuery('
197
			SELECT
198
				COUNT(' . ($context['display_mode'] == 2 ? 'DISTINCT pm.id_pm_head' : '*') . ')
199
			FROM {db_prefix}pm_recipients AS pmr' . ($context['display_mode'] == 2 ? '
200
				INNER JOIN {db_prefix}personal_messages AS pm ON (pm.id_pm = pmr.id_pm)' : '') . '
201
			WHERE pmr.id_member = {int:current_member}
202
				AND pmr.deleted = {int:not_deleted}' . $labelQuery . ($pmID !== null ? '
203
				AND pmr.id_pm ' . ($descending ? '>' : '<') . ' {int:id_pm}' : ''),
204
			[
205
				'current_member' => User::$info->id,
206
				'not_deleted' => 0,
207
				'id_pm' => $pmID,
208
			]
209
		);
210
	}
211
212
	list ($count) = $request->fetch_row();
213
	$request->free_result();
214
215
	return (int) $count;
216
}
217
218
/**
219
 * Delete the specified personal messages.
220
 *
221
 * @param int[]|null $personal_messages array of pm ids
222
 * @param string|null $folder = null
223
 * @param int|int[]|null $owner = null
224
 * @package PersonalMessage
225
 */
226
function deleteMessages($personal_messages, $folder = null, $owner = null)
227
{
228
	$db = database();
229
230
	if ($owner === null)
231
	{
232
		$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...
233
	}
234
	elseif (empty($owner))
235
	{
236
		return;
237
	}
238
	elseif (!is_array($owner))
239
	{
240
		$owner = [$owner];
241
	}
242
243
	if ($personal_messages !== null)
244
	{
245
		if (empty($personal_messages) || !is_array($personal_messages))
246
		{
247
			return;
248
		}
249
250
		foreach ($personal_messages as $index => $delete_id)
251
		{
252
			$personal_messages[$index] = (int) $delete_id;
253
		}
254
255
		$where = '
256
				AND id_pm IN ({array_int:pm_list})';
257
	}
258
	else
259
	{
260
		$where = '';
261
	}
262
263
	if ($folder === 'sent' || $folder === null)
264
	{
265
		$db->query('', '
266
			UPDATE {db_prefix}personal_messages
267
			SET 
268
				deleted_by_sender = {int:is_deleted}
269
			WHERE id_member_from IN ({array_int:member_list})
270
				AND deleted_by_sender = {int:not_deleted}' . $where,
271
			[
272
				'member_list' => $owner,
273
				'is_deleted' => 1,
274
				'not_deleted' => 0,
275
				'pm_list' => $personal_messages !== null ? array_unique($personal_messages) : [],
276
			]
277
		);
278
	}
279
280
	if ($folder !== 'sent')
281
	{
282
		require_once(SUBSDIR . '/Members.subs.php');
283
284
		// Calculate the number of messages each member's gonna lose...
285
		$db->fetchQuery('
286
			SELECT
287
				id_member, COUNT(*) AS num_deleted_messages, CASE WHEN is_read & 1 >= 1 THEN 1 ELSE 0 END AS is_read
288
			FROM {db_prefix}pm_recipients
289
			WHERE id_member IN ({array_int:member_list})
290
				AND deleted = {int:not_deleted}' . $where . '
291
			GROUP BY id_member, is_read',
292
			[
293
				'member_list' => $owner,
294
				'not_deleted' => 0,
295
				'pm_list' => $personal_messages !== null ? array_unique($personal_messages) : [],
296
			]
297
		)->fetch_callback(
298
			function ($row) use ($where) {
299
				// ...And update the statistics accordingly - now including unread messages!.
300
				if ($row['is_read'])
301
				{
302
					updateMemberData($row['id_member'], ['personal_messages' => $where === '' ? 0 : 'personal_messages - ' . $row['num_deleted_messages']]);
303
				}
304
				else
305
				{
306
					updateMemberData($row['id_member'], ['personal_messages' => $where === '' ? 0 : 'personal_messages - ' . $row['num_deleted_messages'], 'unread_messages' => $where === '' ? 0 : 'unread_messages - ' . $row['num_deleted_messages']]);
307
				}
308
309
				// If this is the current member we need to make their message count correct.
310
				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...
311
				{
312
					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...
313
					if (!($row['is_read']))
314
					{
315
						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...
316
					}
317
				}
318
			}
319
		);
320
321
		// Do the actual deletion.
322
		$db->query('', '
323
			UPDATE {db_prefix}pm_recipients
324
			SET 
325
				deleted = {int:is_deleted}
326
			WHERE id_member IN ({array_int:member_list})
327
				AND deleted = {int:not_deleted}' . $where,
328
			[
329
				'member_list' => $owner,
330
				'is_deleted' => 1,
331
				'not_deleted' => 0,
332
				'pm_list' => $personal_messages !== null ? array_unique($personal_messages) : [],
333
			]
334
		);
335
	}
336
337
	// If sender and recipients all have deleted their message, it can be removed.
338
	$remove_pms = [];
339
	$db->fetchQuery('
340
		SELECT
341
			pm.id_pm AS sender, pmr.id_pm
342
		FROM {db_prefix}personal_messages AS pm
343
			LEFT JOIN {db_prefix}pm_recipients AS pmr ON (pmr.id_pm = pm.id_pm AND pmr.deleted = {int:not_deleted})
344
		WHERE pm.deleted_by_sender = {int:is_deleted}
345
			' . str_replace('id_pm', 'pm.id_pm', $where) . '
346
		GROUP BY sender, pmr.id_pm
347
		HAVING pmr.id_pm IS null',
348
		[
349
			'not_deleted' => 0,
350
			'is_deleted' => 1,
351
			'pm_list' => $personal_messages !== null ? array_unique($personal_messages) : [],
352
		]
353
	)->fetch_callback(
354
		function ($row) use (&$remove_pms) {
355
			$remove_pms[] = $row['sender'];
356
		}
357
	);
358
359
	if (!empty($remove_pms))
360
	{
361
		$db->query('', '
362
			DELETE FROM {db_prefix}personal_messages
363
			WHERE id_pm IN ({array_int:pm_list})',
364
			[
365
				'pm_list' => $remove_pms,
366
			]
367
		);
368
369
		$db->query('', '
370
			DELETE FROM {db_prefix}pm_recipients
371
			WHERE id_pm IN ({array_int:pm_list})',
372
			[
373
				'pm_list' => $remove_pms,
374
			]
375
		);
376
	}
377
378
	// Any cached numbers may be wrong now.
379
	Cache::instance()->put('labelCounts:' . User::$info->id, null, 720);
380
}
381
382
/**
383
 * Mark the specified personal messages read.
384
 *
385
 * @param int[]|int|null $personal_messages null or array of pm ids
386
 * @param string|null $label = null, if label is set, only marks messages with that label
387
 * @param int|null $owner = null, if owner is set, marks messages owned by that member id
388
 * @package PersonalMessage
389
 */
390
function markMessages($personal_messages = null, $label = null, $owner = null)
391
{
392
	if ($owner === null)
393
	{
394
		$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...
395
	}
396
397
	if (!is_null($personal_messages) && !is_array($personal_messages))
398
	{
399
		$personal_messages = [$personal_messages];
400
	}
401
402
	$db = database();
403
404
	$request = $db->fetchQuery('
405
		UPDATE {db_prefix}pm_recipients
406
		SET 
407
			is_read = is_read | 1
408
		WHERE id_member = {int:id_member}
409
			AND NOT (is_read & 1 >= 1)' . ($label === null ? '' : '
410
			AND FIND_IN_SET({string:label}, labels) != 0') . ($personal_messages !== null ? '
411
			AND id_pm IN ({array_int:personal_messages})' : ''),
412
		[
413
			'personal_messages' => $personal_messages,
414
			'id_member' => $owner,
415
			'label' => $label,
416
		]
417
	);
418
419
	// If something wasn't marked as read, get the number of unread messages remaining.
420
	if ($request->affected_rows() > 0)
421
	{
422
		updatePMMenuCounts($owner);
423
	}
424
}
425
426
/**
427
 * Mark the specified personal messages as unread.
428
 *
429
 * @param int|int[] $personal_messages
430
 * @package PersonalMessage
431
 */
432
function markMessagesUnread($personal_messages)
433
{
434
	if (empty($personal_messages))
435
	{
436
		return;
437
	}
438
439
	if (!is_array($personal_messages))
440
	{
441
		$personal_messages = [$personal_messages];
442
	}
443
444
	$db = database();
445
446
	$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...
447
448
	// Flip the "read" bit on this
449
	$request = $db->fetchQuery('
450
		UPDATE {db_prefix}pm_recipients
451
		SET 
452
			is_read = is_read & 2
453
		WHERE id_member = {int:id_member}
454
			AND (is_read & 1 >= 1)
455
			AND id_pm IN ({array_int:personal_messages})',
456
		[
457
			'personal_messages' => $personal_messages,
458
			'id_member' => $owner,
459
		]
460
	);
461
462
	// If something was marked unread, update the number of unread messages remaining.
463
	if ($request->affected_rows() > 0)
464
	{
465
		updatePMMenuCounts($owner);
466
	}
467
}
468
469
/**
470
 * Updates the number of unread messages for a user
471
 *
472
 * - Updates the per label totals as well as the overall total
473
 *
474
 * @param int $owner
475
 * @package PersonalMessage
476
 */
477
function updatePMMenuCounts($owner)
478
{
479
	global $context;
480
481
	$db = database();
482
483
	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...
484
	{
485
		foreach ($context['labels'] as $label)
486
		{
487
			$context['labels'][(int) $label['id']]['unread_messages'] = 0;
488
		}
489
	}
490
491
	$total_unread = 0;
492
	$db->fetchQuery('
493
		SELECT
494
			labels, COUNT(*) AS num
495
		FROM {db_prefix}pm_recipients
496
		WHERE id_member = {int:id_member}
497
			AND NOT (is_read & 1 >= 1)
498
			AND deleted = {int:is_not_deleted}
499
		GROUP BY labels',
500
		[
501
			'id_member' => $owner,
502
			'is_not_deleted' => 0,
503
		]
504
	)->fetch_callback(
505
		function ($row) use ($context, &$total_unread, $owner) {
506
			$total_unread += $row['num'];
507
508
			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...
509
			{
510
				return;
511
			}
512
513
			$this_labels = explode(',', $row['labels']);
514
			foreach ($this_labels as $this_label)
515
			{
516
				$context['labels'][(int) $this_label]['unread_messages'] += $row['num'];
517
			}
518
		}
519
	);
520
521
	// Need to store all this.
522
	Cache::instance()->put('labelCounts:' . $owner, $context['labels'], 720);
523
	require_once(SUBSDIR . '/Members.subs.php');
524
	updateMemberData($owner, ['unread_messages' => $total_unread]);
525
526
	// If it was for the current member, reflect this in the User::$info array too.
527
	if ($owner == User::$info->id)
528
	{
529
		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...
530
	}
531
}
532
533
/**
534
 * Check if the PM is available to the current user.
535
 *
536
 * @param int $pmID
537
 * @param string $validFor
538
 * @return bool|null
539
 * @package PersonalMessage
540
 */
541
function isAccessiblePM($pmID, $validFor = 'in_or_outbox')
542
{
543
	$db = database();
544
545
	$request = $db->fetchQuery('
546
		SELECT
547
			pm.id_member_from = {int:id_current_member} AND pm.deleted_by_sender = {int:not_deleted} AS valid_for_outbox,
548
			pmr.id_pm IS NOT NULL AS valid_for_inbox
549
		FROM {db_prefix}personal_messages AS pm
550
			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})
551 2
		WHERE pm.id_pm = {int:id_pm}
552
			AND ((pm.id_member_from = {int:id_current_member} AND pm.deleted_by_sender = {int:not_deleted}) OR pmr.id_pm IS NOT NULL)',
553 2
		[
554
			'id_pm' => $pmID,
555
			'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...
556 2
			'not_deleted' => 0,
557
		]
558
	);
559 2
	if ($request->num_rows() === 0)
560 2
	{
561
		$request->free_result();
562
563
		return false;
564 2
	}
565
	$validationResult = $request->fetch_assoc();
566
	$request->free_result();
567
568 2
	switch ($validFor)
569
	{
570
		case 'inbox':
571 2
			return !empty($validationResult['valid_for_inbox']);
572 2
573 2
		case 'outbox':
574
			return !empty($validationResult['valid_for_outbox']);
575
576
		case 'in_or_outbox':
577
			return !empty($validationResult['valid_for_inbox']) || !empty($validationResult['valid_for_outbox']);
578
579
		default:
580
			trigger_error('Undefined validation type given', E_USER_ERROR);
581
	}
582
}
583 2
584
/**
585
 * Sends a personal message from the specified person to the specified people
586 2
 * ($from defaults to the user)
587 2
 *
588 2
 * @param array $recipients - an array containing the arrays 'to' and 'bcc', both containing id_member's.
589 2
 * @param string $subject - should have no slashes and no html entities
590
 * @param string $message - should have no slashes and no html entities
591
 * @param bool $store_outbox
592
 * @param array|null $from - an array with the id, name, and username of the member.
593
 * @param int $pm_head - the ID of the chain being replied to - if any.
594
 * @return array an array with log entries telling how many recipients were successful and which recipients it failed to send to.
595 2
 * @package PersonalMessage
596
 */
597
function sendpm($recipients, $subject, $message, $store_outbox = true, $from = null, $pm_head = 0)
598
{
599
	global $scripturl, $txt, $language, $modSettings, $webmaster_email;
600
601 2
	$db = database();
602 2
603
	// Make sure the PM language file is loaded, we might need something out of it.
604 2
	Txt::load('PersonalMessage');
605
606 2
	// Needed for our email and post functions
607
	require_once(SUBSDIR . '/Mail.subs.php');
608
	require_once(SUBSDIR . '/Post.subs.php');
609 1
610
	// Initialize log array.
611
	$log = [
612
		'failed' => [],
613
		'sent' => []
614 2
	];
615
616
	if ($from === null)
617
	{
618
		$from = [
619
			'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...
620
			'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...
621
			'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...
622
		];
623
	}
624
	// Probably not needed.  /me something should be of the typer.
625
	else
626
	{
627
		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...
628
	}
629
630
	// Integrated PMs
631
	call_integration_hook('integrate_personal_message', [&$recipients, &$from, &$subject, &$message]);
632
633
	// This is the one that will go in their inbox.
634
	$htmlmessage = Util::htmlspecialchars($message, ENT_QUOTES, 'UTF-8', true);
635
	preparsecode($htmlmessage);
636
	$htmlsubject = strtr(Util::htmlspecialchars($subject), ["\r" => '', "\n" => '', "\t" => '']);
637
	if (Util::strlen($htmlsubject) > 100)
638
	{
639
		$htmlsubject = Util::substr($htmlsubject, 0, 100);
640
	}
641
642
	// Make sure is an array
643
	if (!is_array($recipients))
0 ignored issues
show
introduced by
The condition is_array($recipients) is always true.
Loading history...
644
	{
645
		$recipients = [$recipients];
646
	}
647
648
	// Get a list of usernames and convert them to IDs.
649
	$usernames = [];
650
	foreach ($recipients as $rec_type => $rec)
651
	{
652
		foreach ($rec as $id => $member)
653
		{
654
			if (!is_numeric($recipients[$rec_type][$id]))
655
			{
656
				$recipients[$rec_type][$id] = Util::strtolower(trim(preg_replace('/[<>&"\'=\\\]/', '', $recipients[$rec_type][$id])));
657
				$usernames[$recipients[$rec_type][$id]] = 0;
658 2
			}
659
		}
660
	}
661 2
662
	if (!empty($usernames))
663
	{
664 2
		$request = $db->fetchQuery('
665
			SELECT
666
				id_member, member_name
667 2
			FROM {db_prefix}members
668 2
			WHERE {column_case_insensitive:member_name} IN ({array_string_case_insensitive:usernames})',
669
			[
670
				'usernames' => array_keys($usernames),
671
			]
672
		);
673
		while (($row = $request->fetch_assoc()))
674
		{
675 2
			if (isset($usernames[Util::strtolower($row['member_name'])]))
676 2
			{
677
				$usernames[Util::strtolower($row['member_name'])] = $row['id_member'];
678 2
			}
679
		}
680
		$request->free_result();
681
682
		// Replace the usernames with IDs. Drop usernames that couldn't be found.
683
		foreach ($recipients as $rec_type => $rec)
684
		{
685
			foreach ($rec as $id => $member)
686
			{
687
				if (is_numeric($recipients[$rec_type][$id]))
688
				{
689
					continue;
690
				}
691
692
				if (!empty($usernames[$member]))
693
				{
694
					$recipients[$rec_type][$id] = $usernames[$member];
695
				}
696
				else
697
				{
698
					$log['failed'][$id] = sprintf($txt['pm_error_user_not_found'], $recipients[$rec_type][$id]);
699
					unset($recipients[$rec_type][$id]);
700
				}
701
			}
702
		}
703
	}
704
705 2
	// Make sure there are no duplicate 'to' members.
706
	$recipients['to'] = array_unique($recipients['to']);
707
708
	// Only 'bcc' members that aren't already in 'to'.
709 2
	$recipients['bcc'] = array_diff(array_unique($recipients['bcc']), $recipients['to']);
710 2
711
	// Combine 'to' and 'bcc' recipients.
712
	$all_to = array_merge($recipients['to'], $recipients['bcc']);
713
714
	// Check no-one will want it deleted right away!
715
	$deletes = [];
716
	$db->fetchQuery('
717
		SELECT
718
			id_member, criteria, is_or
719
		FROM {db_prefix}pm_rules
720
		WHERE id_member IN ({array_int:to_members})
721
			AND delete_pm = {int:delete_pm}',
722
		[
723
			'to_members' => $all_to,
724
			'delete_pm' => 1,
725
		]
726 2
	)->fetch_callback(
727 2
		function ($row) use (&$deletes, $from, $subject, $message) {
728 2
			// Check whether we have to apply anything...
729
			$criteria = Util::unserialize($row['criteria']);
730
731
			// Note we don't check the buddy status, cause deletion from buddy = madness!
732
			$delete = false;
733
			foreach ($criteria as $criterium)
734 2
			{
735
				if (($criterium['t'] === 'mid' && $criterium['v'] == $from['id'])
736 2
					|| ($criterium['t'] === 'gid' && in_array($criterium['v'], 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...
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

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