Issues (1686)

sources/ElkArte/Unread.php (1 issue)

1
<?php
2
3
/**
4
 * Find and retrieve information about recently posted topics, messages, and the like.
5
 *
6
 * @package   ElkArte Forum
7
 * @copyright ElkArte Forum contributors
8
 * @license   BSD http://opensource.org/licenses/BSD-3-Clause (see accompanying LICENSE.txt file)
9
 *
10
 * This file contains code covered by:
11
 * copyright: 2011 Simple Machines (http://www.simplemachines.org)
12
 *
13
 * @version 2.0 dev
14
 *
15
 */
16
17
namespace ElkArte;
18
19
use ElkArte\Database\QueryInterface;
20
21
/**
22
 * Unread posts and replies Controller
23
 */
24
class Unread
25
{
26
	/** @var int */
27
	public const UNREAD = 0;
28
29
	/** @var int */
30
	public const UNREADREPLIES = 1;
31
32
	/** @var bool */
33
	private $_ascending = false;
34
35
	/** @var string */
36
	private $_sort_query = '';
37
38
	/** @var int */
39
	private $_num_topics = 0;
40
41
	/** @var int */
42
	private $_min_message = 0;
43
44
	/** @var int */
45
	private $_action = self::UNREAD;
46
47
	/** @var int */
48
	private $_earliest_msg = 0;
49
50
	/** @var bool */
51
	private $_showing_all_topics;
52
53
	/** @var int */
54
	private $_user_id = 0;
55
56
	/** @var bool */
57
	private $_post_mod;
58
59
	/** @var bool */
60
	private $_unwatch;
61
62
	/** @var QueryInterface */
63
	private $_db;
64
65
	/** @var int|string */
66
	private $_preview_bodies = 0;
67
68
	/**
69
	 * Parameters for the main query.
70
	 */
71
	private $_query_parameters = [];
72
73
	/**
74
	 * Constructor
75
	 *
76
	 * @param int $user - ID of the user
77
	 * @param bool|int $post_mod - if post moderation is active or not
78
	 * @param bool|int $unwatch - if unwatch topics is active or not
79
	 * @param bool|int $showing_all_topics - Is the user looking at all the unread replies, or the recent topics?
80
	 */
81
	public function __construct($user, $post_mod, $unwatch, $showing_all_topics = false)
82
	{
83
		$this->_user_id = (int) $user;
84
		$this->_post_mod = (bool) $post_mod;
85
		$this->_unwatch = (bool) $unwatch;
86
		$this->_showing_all_topics = (bool) $showing_all_topics;
87
88
		$this->_db = database();
89
	}
90
91
	/**
92
	 * Sets the boards the member is looking at
93
	 *
94
	 * @param int|int[] $boards - the id of the boards
95
	 */
96
	public function setBoards($boards)
97
	{
98
		$this->_query_parameters['boards'] = is_array($boards) ? $boards : [$boards];
99
	}
100
101
	/**
102
	 * The action the user is performing
103
	 *
104
	 * @param int $action - Unread::UNREAD, Unread::UNREADREPLIES
105
	 */
106
	public function setAction($action)
107
	{
108
		if (in_array($action, [self::UNREAD, self::UNREADREPLIES]))
109
		{
110
			$this->_action = $action;
111
		}
112
	}
113
114
	/**
115
	 * Sets the lower message id to be taken in consideration
116
	 *
117
	 * @param int $msg_id - id of the earliest message to consider
118
	 */
119
	public function setEarliestMsg($msg_id)
120
	{
121
		$this->_earliest_msg = (int) $msg_id;
122
	}
123
124
	/**
125
	 * Sets the sorting query and the direction
126
	 *
127
	 * @param string $query - The query to be used in the ORDER clause
128
	 * @param bool|int $asc - If the sorting is ascending or not
129
	 */
130
	public function setSorting($query, $asc)
131
	{
132
		$this->_sort_query = $query;
133
		$this->_ascending = $asc;
0 ignored issues
show
Documentation Bug introduced by
It seems like $asc can also be of type integer. However, the property $_ascending is declared as type boolean. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

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

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
134
	}
135
136
	/**
137
	 * Return the sorting direction
138
	 *
139
	 * @return bool
140
	 */
141
	public function isSortAsc()
142
	{
143
		return $this->_ascending;
144
	}
145
146
	/**
147
	 * Sets if the data returned by the class will include a shorted version
148
	 * of the body of the last message.
149
	 *
150
	 * @param bool|int $chars - The number of chars to retrieve.
151
	 *                 If true it will return the entire body,
152
	 *                 if 0 no preview will be generated.
153
	 */
154
	public function bodyPreview($chars)
155
	{
156
		$this->_preview_bodies = $chars === true ? 'all' : (int) $chars;
157
	}
158
159
	/**
160
	 * Counts the number of unread topics or messages
161
	 *
162
	 * @param bool $first_login - If this is the first login of the user
163
	 * @param int $id_msg_last_visit - highest id_msg found during the last visit
164
	 *
165
	 * @return int
166
	 */
167
	public function numUnreads($first_login = false, $id_msg_last_visit = 0)
168
	{
169
		if ($this->_action === self::UNREAD)
170
		{
171
			$this->_countRecentTopics($first_login, $id_msg_last_visit);
172
		}
173
		else
174
		{
175
			$this->_countUnreadReplies();
176
		}
177
178
		return $this->_num_topics;
179
	}
180
181
	/**
182
	 * Counts unread topics, used in *all* unread replies and
183
	 * new posts since last visit
184
	 *
185
	 * @param bool $is_first_login - if the member has already logged in at least
186
	 *             once, then there is an $id_msg_last_visit
187
	 * @param int $id_msg_last_visit - highest id_msg found during the last visit
188
	 */
189
	private function _countRecentTopics($is_first_login, $id_msg_last_visit = 0)
190
	{
191
		$request = $this->_db->fetchQuery('
192
			SELECT 
193
				COUNT(*), MIN(t.id_last_msg)
194
			FROM {db_prefix}topics AS t
195
				LEFT JOIN {db_prefix}log_topics AS lt ON (lt.id_topic = t.id_topic AND lt.id_member = {int:current_member})
196
				LEFT JOIN {db_prefix}log_mark_read AS lmr ON (lmr.id_board = t.id_board AND lmr.id_member = {int:current_member})
197
			WHERE t.id_board IN ({array_int:boards})' . ($this->_showing_all_topics && !empty($this->_earliest_msg) ? '
198
				AND t.id_last_msg > {int:earliest_msg}' : (!$this->_showing_all_topics && $is_first_login ? '
199
				AND t.id_last_msg > {int:id_msg_last_visit}' : '')) . '
200
				AND COALESCE(lt.id_msg, lmr.id_msg, 0) < t.id_last_msg' .
201
			($this->_post_mod ? ' AND t.approved = {int:is_approved}' : '') .
202
			($this->_unwatch ? ' AND COALESCE(lt.unwatched, 0) != 1' : ''),
203
			array_merge($this->_query_parameters, [
204
				'current_member' => $this->_user_id,
205
				'earliest_msg' => $this->_earliest_msg,
206
				'id_msg_last_visit' => $id_msg_last_visit,
207
				'is_approved' => 1,
208
			])
209
		);
210
		[$this->_num_topics, $this->_min_message] = $request->fetch_row();
211
		$request->free_result();
212
	}
213
214
	/**
215
	 * Counts unread replies
216
	 */
217
	private function _countUnreadReplies()
218
	{
219
		$request = $this->_db->fetchQuery('
220
			SELECT 
221
				COUNT(DISTINCT t.id_topic), MIN(t.id_last_msg)
222
			FROM {db_prefix}topics AS t
223
				INNER JOIN {db_prefix}messages AS m ON (m.id_topic = t.id_topic)
224
				LEFT JOIN {db_prefix}log_topics AS lt ON (lt.id_topic = t.id_topic AND lt.id_member = {int:current_member})
225
				LEFT JOIN {db_prefix}log_mark_read AS lmr ON (lmr.id_board = t.id_board AND lmr.id_member = {int:current_member})
226
			WHERE t.id_board IN ({array_int:boards})
227
				AND m.id_member = {int:current_member}
228
				AND COALESCE(lt.id_msg, lmr.id_msg, 0) < t.id_last_msg' . ($this->_post_mod ? '
229
				AND t.approved = {int:is_approved}' : '') . ($this->_unwatch ? '
230
				AND COALESCE(lt.unwatched, 0) != 1' : ''),
231
			array_merge($this->_query_parameters, [
232
				'current_member' => $this->_user_id,
233
				'is_approved' => 1,
234
			])
235
		);
236
		[$this->_num_topics, $this->_min_message] = $request->fetch_row();
237
		$request->free_result();
238
	}
239
240
	/**
241
	 * Retrieves unread topics or messages
242
	 *
243
	 * @param string $join - kind of "JOIN" to execute. If 'topic' JOINs boards on
244
	 * the topics table, otherwise ('message') the JOIN is on the messages table
245
	 * @param int $start - position to start the query
246
	 * @param int $limit - number of entries to grab
247
	 * @param bool $include_avatars - if avatars should be retrieved as well
248
	 * @return array - see \ElkArte\TopicUtil::prepareContext
249
	 */
250
	public function getUnreads($join, $start, $limit, $include_avatars)
251
	{
252
		if ($this->_action === self::UNREAD)
253
		{
254
			return $this->_getUnreadTopics($join, $start, $limit, $include_avatars);
255
		}
256
257
		return $this->_getUnreadReplies($start, $limit, $include_avatars);
258
	}
259
260
	/**
261
	 * Retrieves unread topics, used in *all* unread replies and
262
	 * new posts since last visit
263
	 *
264
	 * @param string $join - kind of "JOIN" to execute. If 'topic' JOINs boards on
265
	 *                       the topics table, otherwise ('message') the JOIN is on
266
	 *                       the messages table
267
	 * @param int $start - position to start the query
268
	 * @param int $limit - number of entries to grab
269
	 * @param bool|int $include_avatars - if avatars should be retrieved as well
270
	 * @return array - see \ElkArte\TopicUtil::prepareContext
271
	 */
272
	private function _getUnreadTopics($join, $start, $limit, $include_avatars = false)
273
	{
274
		$body_query = $this->_setBodyQuery();
275
276
		if (!empty($include_avatars))
277
		{
278
			if ($include_avatars === 1 || $include_avatars === 3)
279
			{
280
				$custom_selects = ['meml.avatar', 'COALESCE(a.id_attach, 0) AS id_attach', 'a.filename', 'a.attachment_type', 'meml.email_address'];
281
				$custom_joins = ['LEFT JOIN {db_prefix}attachments AS a ON (a.id_member = ml.id_member AND a.id_member != 0)'];
282
			}
283
			else
284
			{
285
				$custom_selects = [];
286
				$custom_joins = [];
287
			}
288
289
			if ($include_avatars === 2 || $include_avatars === 3)
290
			{
291
				$custom_selects = array_merge($custom_selects, ['memf.avatar AS avatar_first', 'COALESCE(af.id_attach, 0) AS id_attach_first', 'af.filename AS filename_first', 'af.attachment_type AS attachment_type_first', 'memf.email_address AS email_address_first']);
292
				$custom_joins = array_merge($custom_joins, ['LEFT JOIN {db_prefix}attachments AS af ON (af.id_member = mf.id_member AND af.id_member != 0)']);
293
			}
294
		}
295
296
		$request = $this->_db->fetchQuery('
297
			SELECT
298
				ms.subject AS first_subject, ms.poster_time AS first_poster_time, ms.poster_name AS first_member_name,
299
				ms.id_topic, t.id_board, b.name AS bname, t.num_replies, t.num_views, t.num_likes, t.approved,
300
				ms.id_member AS first_id_member, ml.id_member AS last_id_member, ml.poster_name AS last_member_name,
301
				ml.poster_time AS last_poster_time, COALESCE(mems.real_name, ms.poster_name) AS first_display_name,
302
				COALESCE(meml.real_name, ml.poster_name) AS last_display_name, ml.subject AS last_subject,
303
				ml.icon AS last_icon, ms.icon AS first_icon, t.id_poll, t.is_sticky, t.locked, ml.modified_time AS last_modified_time,
304
				COALESCE(lt.id_msg, lmr.id_msg, -1) + 1 AS new_from,
305
				' . $body_query . '
306
				' . (empty($custom_selects) ? '' : implode(',', $custom_selects) . ', ') . '
307
				ml.smileys_enabled AS last_smileys, ms.smileys_enabled AS first_smileys, t.id_first_msg, t.id_last_msg
308
			FROM {db_prefix}messages AS ms
309
				INNER JOIN {db_prefix}topics AS t ON (t.id_topic = ms.id_topic AND t.id_first_msg = ms.id_msg)
310
				INNER JOIN {db_prefix}messages AS ml ON (ml.id_msg = t.id_last_msg)' . ($join === 'topics' ? '
311
				LEFT JOIN {db_prefix}boards AS b ON (b.id_board = t.id_board)' : '
312
				LEFT JOIN {db_prefix}boards AS b ON (b.id_board = ms.id_board)') . '
313
				LEFT JOIN {db_prefix}members AS mems ON (mems.id_member = ms.id_member)
314
				LEFT JOIN {db_prefix}members AS meml ON (meml.id_member = ml.id_member)
315
				LEFT JOIN {db_prefix}log_topics AS lt ON (lt.id_topic = t.id_topic AND lt.id_member = {int:current_member})' . (empty($custom_joins) ? '' : implode("\n\t\t\t\t", $custom_joins)) . '
316
				LEFT JOIN {db_prefix}log_mark_read AS lmr ON (lmr.id_board = t.id_board AND lmr.id_member = {int:current_member})
317
			WHERE t.id_board IN ({array_int:boards})
318
				AND t.id_last_msg >= {int:min_message}
319
				AND COALESCE(lt.id_msg, lmr.id_msg, 0) < ml.id_msg' .
320
			($this->_post_mod ? ' AND ms.approved = {int:is_approved}' : '') .
321
			($this->_unwatch ? ' AND COALESCE(lt.unwatched, 0) != 1' : '') . '
322
			ORDER BY {raw:order}
323
			LIMIT {int:limit} OFFSET {int:offset}',
324
			array_merge($this->_query_parameters, [
325
				'current_member' => $this->_user_id,
326
				'min_message' => $this->_min_message,
327
				'is_approved' => 1,
328
				'order' => $this->_sort_query . ($this->_ascending ? '' : ' DESC'),
329
				'offset' => $start,
330
				'limit' => $limit,
331
			])
332
		);
333
		$topics = $request->fetch_all();
334
		$request->free_result();
335
336
		return TopicUtil::prepareContext($topics, true, ((int) $this->_preview_bodies) + 128);
337
	}
338
339
	/**
340
	 * Retrieves unread replies since last visit
341
	 *
342
	 * @param int $start - position to start the query
343
	 * @param int $limit - number of entries to grab
344
	 * @param bool|int $include_avatars - if avatars should be retrieved as well
345
	 * @return array|bool - see \ElkArte\TopicUtil::prepareContext
346
	 */
347
	private function _getUnreadReplies($start, $limit, $include_avatars = false)
348
	{
349
		$request = $this->_db->fetchQuery('
350
			SELECT 
351
				t.id_topic, ' . $this->_sort_query . '
352
			FROM {db_prefix}topics AS t
353
				INNER JOIN {db_prefix}messages AS m ON (m.id_topic = t.id_topic AND m.id_member = {int:current_member})' . (strpos($this->_sort_query, 'ms.') === false ? '' : '
354
				INNER JOIN {db_prefix}messages AS ms ON (ms.id_msg = t.id_first_msg)') . (strpos($this->_sort_query, 'mems.') === false ? '' : '
355
				LEFT JOIN {db_prefix}members AS mems ON (mems.id_member = ms.id_member)') . '
356
				LEFT JOIN {db_prefix}log_topics AS lt ON (lt.id_topic = t.id_topic AND lt.id_member = {int:current_member})
357
				LEFT JOIN {db_prefix}log_mark_read AS lmr ON (lmr.id_board = t.id_board AND lmr.id_member = {int:current_member})
358
			WHERE t.id_board IN ({array_int:boards})
359
				AND t.id_last_msg >= {int:min_message}
360
				AND COALESCE(lt.id_msg, lmr.id_msg, 0) < t.id_last_msg' .
361
			($this->_post_mod ? ' AND t.approved = {int:is_approved}' : '') .
362
			($this->_unwatch ? ' AND COALESCE(lt.unwatched, 0) != 1' : '') . '
363
			ORDER BY {raw:order}
364
			LIMIT {int:limit} OFFSET {int:offset}',
365
			array_merge($this->_query_parameters, [
366
				'current_member' => $this->_user_id,
367
				'min_message' => $this->_min_message,
368
				'is_approved' => 1,
369
				'order' => $this->_sort_query . ($this->_ascending ? '' : ' DESC'),
370
				'offset' => $start,
371
				'limit' => $limit,
372
			])
373
		);
374
		$topics = [];
375
		while (($row = $request->fetch_assoc()))
376
		{
377
			$topics[] = $row['id_topic'];
378
		}
379
380
		$request->free_result();
381
382
		// Sanity... where have you gone?
383
		if (empty($topics))
384
		{
385
			return false;
386
		}
387
388
		$body_query = $this->_setBodyQuery();
389
390
		if (!empty($include_avatars))
391
		{
392
			if ($include_avatars === 1 || $include_avatars === 3)
393
			{
394
				$custom_selects = ['meml.avatar', 'COALESCE(a.id_attach, 0) AS id_attach', 'a.filename', 'a.attachment_type', 'meml.email_address'];
395
				$custom_joins = ['LEFT JOIN {db_prefix}attachments AS a ON (a.id_member = ml.id_member AND a.id_member != 0)'];
396
			}
397
			else
398
			{
399
				$custom_selects = [];
400
				$custom_joins = [];
401
			}
402
403
			if ($include_avatars === 2 || $include_avatars === 3)
404
			{
405
				$custom_selects = array_merge($custom_selects, ['memf.avatar AS avatar_first', 'COALESCE(af.id_attach, 0) AS id_attach_first', 'af.filename AS filename_first', 'af.attachment_type AS attachment_type_first', 'memf.email_address AS email_address_first']);
406
				$custom_joins = array_merge($custom_joins, ['LEFT JOIN {db_prefix}attachments AS af ON (af.id_member = ms.id_member AND af.id_member != 0)']);
407
			}
408
		}
409
410
		$request = $this->_db->fetchQuery('
411
			SELECT
412
				ms.subject AS first_subject, ms.poster_time AS first_poster_time, ms.id_topic, t.id_board, b.name AS bname,
413
				ms.poster_name AS first_member_name, ml.poster_name AS last_member_name, t.approved,
414
				t.num_replies, t.num_views, t.num_likes, ms.id_member AS first_id_member, ml.id_member AS last_id_member,
415
				ml.poster_time AS last_poster_time, COALESCE(mems.real_name, ms.poster_name) AS first_display_name,
416
				COALESCE(meml.real_name, ml.poster_name) AS last_display_name, ml.subject AS last_subject,
417
				ml.icon AS last_icon, ms.icon AS first_icon, t.id_poll, t.is_sticky, t.locked, ml.modified_time AS last_modified_time,
418
				COALESCE(lt.id_msg, lmr.id_msg, -1) + 1 AS new_from,
419
				' . $body_query . '
420
				' . (empty($custom_selects) ? '' : implode(',', $custom_selects) . ', ') . '
421
				ml.smileys_enabled AS last_smileys, ms.smileys_enabled AS first_smileys, t.id_first_msg, t.id_last_msg
422
			FROM {db_prefix}topics AS t
423
				INNER JOIN {db_prefix}messages AS ms ON (ms.id_topic = t.id_topic AND ms.id_msg = t.id_first_msg)
424
				INNER JOIN {db_prefix}messages AS ml ON (ml.id_msg = t.id_last_msg)
425
				INNER JOIN {db_prefix}boards AS b ON (b.id_board = t.id_board)
426
				LEFT JOIN {db_prefix}members AS mems ON (mems.id_member = ms.id_member)
427
				LEFT JOIN {db_prefix}members AS meml ON (meml.id_member = ml.id_member)
428
				LEFT JOIN {db_prefix}log_topics AS lt ON (lt.id_topic = t.id_topic AND lt.id_member = {int:current_member})
429
				LEFT JOIN {db_prefix}log_mark_read AS lmr ON (lmr.id_board = t.id_board AND lmr.id_member = {int:current_member})' . (empty($custom_joins) ? '' : implode("\n\t\t\t\t", $custom_joins)) . '
430
			WHERE t.id_topic IN ({array_int:topic_list})
431
			ORDER BY {raw:order}
432
			LIMIT {int:limit}',
433
			[
434
				'current_member' => $this->_user_id,
435
				'order' => $this->_sort_query . ($this->_ascending ? '' : ' DESC'),
436
				'topic_list' => $topics,
437
				'limit' => count($topics),
438
			]
439
		);
440
		$return = $request->fetch_all();
441
		$request->free_result();
442
443
		return TopicUtil::prepareContext($return, true, ((int) $this->_preview_bodies) + 128);
444
	}
445
446
	/**
447
	 * Set the body query
448
	 *
449
	 * @return string
450
	 */
451
	private function _setBodyQuery()
452
	{
453
		if ($this->_preview_bodies === 'all')
454
		{
455
			return 'ml.body AS last_body, ms.body AS first_body,';
456
		}
457
458
		// If empty, no preview at all
459
		if (empty($this->_preview_bodies))
460
		{
461
			return '';
462
		}
463
464
		// Default: a SUBSTRING
465
		return 'SUBSTRING(ml.body, 1, ' . ($this->_preview_bodies + 256) . ') AS last_body, SUBSTRING(ms.body, 1, ' . ($this->_preview_bodies + 256) . ') AS first_body,';
466
	}
467
}
468