Completed
Push — master ( 4e9006...600a44 )
by Matt
02:55
created

similar_topics::is_available()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 1
Metric Value
c 2
b 0
f 1
dl 0
loc 4
rs 10
cc 3
eloc 2
nc 3
nop 0
1
<?php
2
/**
3
*
4
* Precise Similar Topics
5
*
6
* @copyright (c) 2013 Matt Friedman
7
* @license GNU General Public License, version 2 (GPL-2.0)
8
*
9
*/
10
11
namespace vse\similartopics\core;
12
13
class similar_topics
14
{
15
	/** @var \phpbb\auth\auth */
16
	protected $auth;
17
18
	/** @var \phpbb\cache\service */
19
	protected $cache;
20
21
	/** @var \phpbb\config\config */
22
	protected $config;
23
24
	/** @var \phpbb\db\driver\driver_interface */
25
	protected $db;
26
27
	/** @var \phpbb\event\dispatcher_interface */
28
	protected $dispatcher;
29
30
	/** @var \phpbb\pagination */
31
	protected $pagination;
32
33
	/** @var \phpbb\request\request */
34
	protected $request;
35
36
	/** @var \phpbb\template\template */
37
	protected $template;
38
39
	/** @var \phpbb\user */
40
	protected $user;
41
42
	/** @var \phpbb\content_visibility */
43
	protected $content_visibility;
44
45
	/** @var string phpBB root path  */
46
	protected $root_path;
47
48
	/** @var string PHP file extension */
49
	protected $php_ext;
50
51
	/**
52
	* Constructor
53
	*
54
	* @param \phpbb\auth\auth $auth
55
	* @param \phpbb\cache\service $cache
56
	* @param \phpbb\config\config $config
57
	* @param \phpbb\db\driver\driver_interface $db
58
	* @param \phpbb\event\dispatcher_interface $dispatcher
59
	* @param \phpbb\pagination $pagination
60
	* @param \phpbb\request\request $request
61
	* @param \phpbb\template\template $template
62
	* @param \phpbb\user $user
63
	* @param \phpbb\content_visibility $content_visibility
64
	* @param string $root_path
65
	* @param string $php_ext
66
	* @access public
67
	*/
68
	public function __construct(\phpbb\auth\auth $auth, \phpbb\cache\service $cache, \phpbb\config\config $config, \phpbb\db\driver\driver_interface $db, \phpbb\event\dispatcher_interface $dispatcher, \phpbb\pagination $pagination, \phpbb\request\request $request, \phpbb\template\template $template, \phpbb\user $user, \phpbb\content_visibility $content_visibility, $root_path, $php_ext)
69
	{
70
		$this->auth = $auth;
71
		$this->cache = $cache;
72
		$this->config = $config;
73
		$this->db = $db;
74
		$this->dispatcher = $dispatcher;
75
		$this->pagination = $pagination;
76
		$this->request = $request;
77
		$this->template = $template;
78
		$this->user = $user;
79
		$this->content_visibility = $content_visibility;
80
		$this->root_path = $root_path;
81
		$this->php_ext = $php_ext;
82
	}
83
84
	/**
85
	* Is similar topics available?
86
	*
87
	* @return bool True if available, false otherwise
88
	* @access public
89
	*/
90
	public function is_available()
91
	{
92
		return $this->is_enabled() && $this->is_viewable() && $this->is_mysql();
93
	}
94
95
	/**
96
	 * Is similar topics configured?
97
	 *
98
	 * @return bool True if configured, false otherwise
99
	 * @access public
100
	 */
101
	public function is_enabled()
102
	{
103
		return !empty($this->config['similar_topics']) && !empty($this->config['similar_topics_limit']);
104
	}
105
106
	/**
107
	 * Is similar topics viewable bu the user?
108
	 *
109
	 * @return bool True if viewable, false otherwise
110
	 * @access public
111
	 */
112
	public function is_viewable()
113
	{
114
		return !empty($this->user->data['user_similar_topics']) && $this->auth->acl_get('u_similar_topics');
115
	}
116
117
	/**
118
	* Is the forum available for displaying similar topics
119
	*
120
	* @param int $forum_id A forum identifier
121
	* @return bool True if available, false otherwise
122
	* @access public
123
	*/
124
	public function forum_available($forum_id)
125
	{
126
		return !in_array($forum_id, explode(',', $this->config['similar_topics_hide']));
127
	}
128
129
	/**
130
	* Get similar topics by matching topic titles
131
	*
132
	* NOTE: Currently requires MySQL due to the use of FULLTEXT indexes
133
	* and MATCH and AGAINST and UNIX_TIMESTAMP. MySQL FULLTEXT has built-in
134
	* English ignore words. We use phpBB's ignore words for non-English
135
	* languages. We also remove any admin-defined special ignore words.
136
	*
137
	* @param	array	$topic_data	Array with topic data
138
	* @return 	null
139
	* @access	public
140
	*/
141
	public function display_similar_topics($topic_data)
142
	{
143
		$topic_title = $this->clean_topic_title($topic_data['topic_title']);
144
145
		// If the cleaned up topic_title is empty, no need to continue
146
		if (empty($topic_title))
147
		{
148
			return;
149
		}
150
151
		// Similar Topics query
152
		$sql_array = array(
153
			'SELECT'	=> "f.forum_id, f.forum_name, t.*,
154
				MATCH (t.topic_title) AGAINST ('" . $this->db->sql_escape($topic_title) . "') AS score",
155
156
			'FROM'		=> array(
157
				TOPICS_TABLE	=> 't',
158
			),
159
			'LEFT_JOIN'	=> array(
160
				array(
161
					'FROM'	=>	array(FORUMS_TABLE	=> 'f'),
162
					'ON'	=> 'f.forum_id = t.forum_id',
163
				),
164
			),
165
			'WHERE'		=> "MATCH (t.topic_title) AGAINST ('" . $this->db->sql_escape($topic_title) . "') >= 0.5
166
				AND t.topic_status <> " . ITEM_MOVED . '
167
				AND t.topic_visibility = ' . ITEM_APPROVED . '
168
				AND t.topic_time > (UNIX_TIMESTAMP() - ' . $this->config['similar_topics_time'] . ')
169
				AND t.topic_id <> ' . (int) $topic_data['topic_id'],
170
			//'GROUP_BY'	=> 't.topic_id',
0 ignored issues
show
Unused Code Comprehensibility introduced by
67% of this comment could be valid code. Did you maybe forget this after debugging?

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

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

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

Loading history...
171
			//'ORDER_BY'	=> 'score DESC', // this is done automatically by MySQL when not using IN BOOLEAN MODE
0 ignored issues
show
Unused Code Comprehensibility introduced by
72% of this comment could be valid code. Did you maybe forget this after debugging?

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

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

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

Loading history...
172
		);
173
174
		// Add topic tracking data to the query (only if query caching is off)
175
		if ($this->user->data['is_registered'] && $this->config['load_db_lastread'] && !$this->config['similar_topics_cache'])
176
		{
177
			$sql_array['LEFT_JOIN'][] = array('FROM' => array(TOPICS_TRACK_TABLE => 'tt'), 'ON' => 'tt.topic_id = t.topic_id AND tt.user_id = ' . $this->user->data['user_id']);
178
			$sql_array['LEFT_JOIN'][] = array('FROM' => array(FORUMS_TRACK_TABLE => 'ft'), 'ON' => 'ft.forum_id = f.forum_id AND ft.user_id = ' . $this->user->data['user_id']);
179
			$sql_array['SELECT'] .= ', tt.mark_time, ft.mark_time as f_mark_time';
180
		}
181
		else if ($this->config['load_anon_lastread'] || $this->user->data['is_registered'])
182
		{
183
			// Cookie based tracking copied from search.php
184
			$tracking_topics = $this->request->variable($this->config['cookie_name'] . '_track', '', true, \phpbb\request\request_interface::COOKIE);
185
			$tracking_topics = ($tracking_topics) ? tracking_unserialize($tracking_topics) : array();
186
		}
187
188
		// We need to exclude passworded forums so we do not leak the topic title
189
		$passworded_forums = $this->user->get_passworded_forums();
190
191
		// See if the admin set this forum to only search a specific group of other forums, and include them
192
		if (!empty($topic_data['similar_topic_forums']))
193
		{
194
			// Remove any passworded forums from this group of forums we will be searching
195
			$included_forums = array_diff(explode(',', $topic_data['similar_topic_forums']), $passworded_forums);
196
			// if there's nothing left to display (user has no access to the forums we want to search)
197
			if (empty($included_forums))
198
			{
199
				return;
200
			}
201
202
			$sql_array['WHERE'] .= ' AND ' . $this->db->sql_in_set('f.forum_id', $included_forums);
203
		}
204
		// Otherwise, see what forums are not allowed to be searched, and exclude them
205
		else if (!empty($this->config['similar_topics_ignore']))
206
		{
207
			// Add passworded forums to the exlude array
208
			$excluded_forums = array_unique(array_merge(explode(',', $this->config['similar_topics_ignore']), $passworded_forums));
209
			$sql_array['WHERE'] .= ' AND ' . $this->db->sql_in_set('f.forum_id', $excluded_forums, true);
210
		}
211
		// In all other cases, exclude any passworded forums the user is not allowed to view
212
		else if (!empty($passworded_forums))
213
		{
214
			$sql_array['WHERE'] .= ' AND ' . $this->db->sql_in_set('f.forum_id', $passworded_forums, true);
215
		}
216
217
		/**
218
		* Event to modify the sql_array for similar topics
219
		*
220
		* @event vse.similartopics.get_topic_data
221
		* @var	array	sql_array	SQL array to get similar topics data
222
		* @since 1.3.0
223
		*/
224
		$vars = array('sql_array');
225
		extract($this->dispatcher->trigger_event('vse.similartopics.get_topic_data', compact($vars)));
226
227
		$sql = $this->db->sql_build_query('SELECT', $sql_array);
228
		$result = $this->db->sql_query_limit($sql, $this->config['similar_topics_limit'], 0, $this->config['similar_topics_cache']);
229
230
		// Grab icons
231
		$icons = $this->cache->obtain_icons();
232
233
		$rowset = array();
234
235
		while ($row = $this->db->sql_fetchrow($result))
236
		{
237
			$similar_forum_id = (int) $row['forum_id'];
238
			$similar_topic_id = (int) $row['topic_id'];
239
			$rowset[$similar_topic_id] = $row;
240
241
			if ($this->auth->acl_get('f_read', $similar_forum_id))
242
			{
243
				// Get topic tracking info
244
				if ($this->user->data['is_registered'] && $this->config['load_db_lastread'] && !$this->config['similar_topics_cache'])
245
				{
246
					$topic_tracking_info = get_topic_tracking($similar_forum_id, $similar_topic_id, $rowset, array($similar_forum_id => $row['f_mark_time']));
247
				}
248
				else if ($this->config['load_anon_lastread'] || $this->user->data['is_registered'])
249
				{
250
					$topic_tracking_info = get_complete_topic_tracking($similar_forum_id, $similar_topic_id);
251
252
					if (!$this->user->data['is_registered'])
253
					{
254
						$this->user->data['user_lastmark'] = (isset($tracking_topics['l'])) ? (int) (base_convert($tracking_topics['l'], 36, 10) + $this->config['board_startdate']) : 0;
255
					}
256
				}
257
258
				// Replies
259
				$replies = $this->content_visibility->get_count('topic_posts', $row, $similar_forum_id) - 1;
260
261
				// Get folder img, topic status/type related information
262
				$folder_img = $folder_alt = $topic_type = '';
263
				$unread_topic = (isset($topic_tracking_info[$similar_topic_id]) && $row['topic_last_post_time'] > $topic_tracking_info[$similar_topic_id]) ? true : false;
264
				topic_status($row, $replies, $unread_topic, $folder_img, $folder_alt, $topic_type);
265
266
				$topic_unapproved = ($row['topic_visibility'] == ITEM_UNAPPROVED && $this->auth->acl_get('m_approve', $similar_forum_id)) ? true : false;
267
				$posts_unapproved = ($row['topic_visibility'] == ITEM_APPROVED && $row['topic_posts_unapproved'] && $this->auth->acl_get('m_approve', $similar_forum_id)) ? true : false;
268
				//$topic_deleted = $row['topic_visibility'] == ITEM_DELETED;
0 ignored issues
show
Unused Code Comprehensibility introduced by
47% of this comment could be valid code. Did you maybe forget this after debugging?

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

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

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

Loading history...
269
				$u_mcp_queue = ($topic_unapproved || $posts_unapproved) ? append_sid("{$this->root_path}mcp.{$this->php_ext}", 'i=queue&amp;mode=' . (($topic_unapproved) ? 'approve_details' : 'unapproved_posts') . "&amp;t=$similar_topic_id", true, $this->user->session_id) : '';
270
				//$u_mcp_queue = (!$u_mcp_queue && $topic_deleted) ? append_sid("{$this->root_path}mcp.{$this->php_ext}", "i=queue&amp;mode=deleted_topics&amp;t=$similar_topic_id", true, $this->user->session_id) : $u_mcp_queue;
0 ignored issues
show
Unused Code Comprehensibility introduced by
60% of this comment could be valid code. Did you maybe forget this after debugging?

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

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

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

Loading history...
271
272
				$base_url = append_sid("{$this->root_path}viewtopic.{$this->php_ext}", 'f=' . $similar_forum_id . '&amp;t=' . $similar_topic_id);
273
274
				$topic_row = array(
275
					'TOPIC_AUTHOR_FULL'		=> get_username_string('full', $row['topic_poster'], $row['topic_first_poster_name'], $row['topic_first_poster_colour']),
276
					'FIRST_POST_TIME'		=> $this->user->format_date($row['topic_time']),
277
					'LAST_POST_TIME'		=> $this->user->format_date($row['topic_last_post_time']),
278
					'LAST_POST_AUTHOR_FULL'	=> get_username_string('full', $row['topic_last_poster_id'], $row['topic_last_poster_name'], $row['topic_last_poster_colour']),
279
280
					'TOPIC_REPLIES'			=> $replies,
281
					'TOPIC_VIEWS'			=> $row['topic_views'],
282
					'TOPIC_TITLE'			=> censor_text($row['topic_title']),
283
					'FORUM_TITLE'			=> $row['forum_name'],
284
285
					'TOPIC_IMG_STYLE'		=> $folder_img,
286
					'TOPIC_FOLDER_IMG'		=> $this->user->img($folder_img, $folder_alt),
287
					'TOPIC_FOLDER_IMG_ALT'	=> $this->user->lang($folder_alt),
288
289
					'TOPIC_ICON_IMG'		=> (!empty($icons[$row['icon_id']])) ? $icons[$row['icon_id']]['img'] : '',
290
					'TOPIC_ICON_IMG_WIDTH'	=> (!empty($icons[$row['icon_id']])) ? $icons[$row['icon_id']]['width'] : '',
291
					'TOPIC_ICON_IMG_HEIGHT'	=> (!empty($icons[$row['icon_id']])) ? $icons[$row['icon_id']]['height'] : '',
292
					'ATTACH_ICON_IMG'		=> ($this->auth->acl_get('u_download') && $this->auth->acl_get('f_download', $similar_forum_id) && $row['topic_attachment']) ? $this->user->img('icon_topic_attach', $this->user->lang('TOTAL_ATTACHMENTS')) : '',
293
					'UNAPPROVED_IMG'		=> ($topic_unapproved || $posts_unapproved) ? $this->user->img('icon_topic_unapproved', ($topic_unapproved) ? 'TOPIC_UNAPPROVED' : 'POSTS_UNAPPROVED') : '',
294
295
					'S_UNREAD_TOPIC'		=> $unread_topic,
296
					'S_TOPIC_REPORTED'		=> (!empty($row['topic_reported']) && $this->auth->acl_get('m_report', $similar_forum_id)) ? true : false,
297
					'S_TOPIC_UNAPPROVED'	=> $topic_unapproved,
298
					'S_POSTS_UNAPPROVED'	=> $posts_unapproved,
299
					//'S_TOPIC_DELETED'		=> $topic_deleted,
0 ignored issues
show
Unused Code Comprehensibility introduced by
67% of this comment could be valid code. Did you maybe forget this after debugging?

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

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

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

Loading history...
300
					'S_HAS_POLL'			=> ($row['poll_start']) ? true : false,
301
302
					'U_NEWEST_POST'			=> append_sid("{$this->root_path}viewtopic.{$this->php_ext}", 'f=' . $similar_forum_id . '&amp;t=' . $similar_topic_id . '&amp;view=unread') . '#unread',
303
					'U_LAST_POST'			=> append_sid("{$this->root_path}viewtopic.{$this->php_ext}", 'f=' . $similar_forum_id . '&amp;t=' . $similar_topic_id . '&amp;p=' . $row['topic_last_post_id']) . '#p' . $row['topic_last_post_id'],
304
					'U_VIEW_TOPIC'			=> append_sid("{$this->root_path}viewtopic.{$this->php_ext}", 'f=' . $similar_forum_id . '&amp;t=' . $similar_topic_id),
305
					'U_VIEW_FORUM'			=> append_sid("{$this->root_path}viewforum.{$this->php_ext}", 'f=' . $similar_forum_id),
306
					'U_MCP_REPORT'			=> append_sid("{$this->root_path}mcp.{$this->php_ext}", 'i=reports&amp;mode=reports&amp;f=' . $similar_forum_id . '&amp;t=' . $similar_topic_id, true, $this->user->session_id),
307
					'U_MCP_QUEUE'			=> $u_mcp_queue,
308
				);
309
310
				/**
311
				* Event to modify the similar topics template block
312
				*
313
				* @event vse.similartopics.modify_topicrow
314
				* @var	array	row			Array with similar topic data
315
				* @var	array	topic_row	Template block array
316
				* @since 1.3.0
317
				*/
318
				$vars = array('row', 'topic_row');
319
				extract($this->dispatcher->trigger_event('vse.similartopics.modify_topicrow', compact($vars)));
320
321
				$this->template->assign_block_vars('similar', $topic_row);
322
323
				$this->pagination->generate_template_pagination($base_url, 'similar.pagination', 'start', $replies + 1, $this->config['posts_per_page'], 1, true, true);
324
			}
325
		}
326
327
		$this->db->sql_freeresult($result);
328
329
		$this->user->add_lang_ext('vse/similartopics', 'similar_topics');
330
331
		$this->template->assign_vars(array(
332
			'L_SIMILAR_TOPICS'	=> $this->user->lang('SIMILAR_TOPICS'),
333
			'NEWEST_POST_IMG'	=> $this->user->img('icon_topic_newest', 'VIEW_NEWEST_POST'),
334
			'LAST_POST_IMG'		=> $this->user->img('icon_topic_latest', 'VIEW_LATEST_POST'),
335
			'REPORTED_IMG'		=> $this->user->img('icon_topic_reported', 'TOPIC_REPORTED'),
336
			//'DELETED_IMG'		=> $this->user->img('icon_topic_deleted', 'TOPIC_DELETED'),
0 ignored issues
show
Unused Code Comprehensibility introduced by
69% of this comment could be valid code. Did you maybe forget this after debugging?

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

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

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

Loading history...
337
			'POLL_IMG'			=> $this->user->img('icon_topic_poll', 'TOPIC_POLL'),
338
		));
339
	}
340
341
	/**
342
	* Clean topic title (and if needed, ignore-words)
343
	*
344
	* @param	string	$text	The topic title
345
	* @return	string	The topic title
346
	* @access	public
347
	*/
348
	public function clean_topic_title($text)
349
	{
350
		// Strip quotes, ampersands
351
		$text = str_replace(array('&quot;', '&amp;'), '', $text);
352
353
		if (!$this->english_lang() || $this->has_ignore_words())
354
		{
355
			$text = $this->strip_stop_words($text);
356
		}
357
358
		return $text;
359
	}
360
361
	/**
362
	* Remove any non-english and/or custom defined ignore-words
363
	*
364
	* @param	string	$text			The topic title
365
	* @return	string	The topic title
366
	* @access	protected
367
	*/
368
	protected function strip_stop_words($text)
369
	{
370
		$words = array();
371
372
		// Retrieve a language dependent list of words to be ignored (method copied from search.php)
373
		$search_ignore_words = "{$this->user->lang_path}{$this->user->lang_name}/search_ignore_words.{$this->php_ext}";
374
		if (!$this->english_lang() && file_exists($search_ignore_words))
375
		{
376
			include($search_ignore_words);
377
		}
378
379
		if ($this->has_ignore_words())
380
		{
381
			// Merge any custom defined ignore words from the ACP to the stop-words array
382
			$words = array_merge($this->make_word_array($this->config['similar_topics_words']), $words);
383
		}
384
385
		// Remove stop-words from the topic title text
386
		$words = array_diff($this->make_word_array($text), $words);
387
388
		// Convert our words array back to a string
389
		$text = (!empty($words)) ? implode(' ', $words) : '';
390
391
		return $text;
392
	}
393
394
	/**
395
	* Helper function to split string into an array of words
396
	*
397
	* @param	string	$text	String of plain text words
398
	* @return	array	Array of plaintext words
399
	* @access	protected
400
	*/
401
	protected function make_word_array($text)
402
	{
403
		// Strip out any non-alpha-numeric characters using PCRE regex syntax
404
		$text = trim(preg_replace('#[^\p{L}\p{N}]+#u', ' ', $text));
405
406
		$words = explode(' ', utf8_strtolower($text));
407
		foreach ($words as $key => $word)
408
		{
409
			// Strip words of 2 characters or less
410
			if (utf8_strlen(trim($word)) < 3)
411
			{
412
				unset($words[$key]);
413
			}
414
		}
415
416
		return $words;
417
	}
418
419
	/**
420
	* Check if English is the current user's language
421
	*
422
	* @return	bool	True if lang is 'en' or 'en_us', false otherwise
423
	* @access	protected
424
	*/
425
	protected function english_lang()
426
	{
427
		return ($this->user->lang_name == 'en' || $this->user->lang_name == 'en_us');
428
	}
429
430
	/**
431
	* Check if custom ignore words have been defined for similar topics
432
	*
433
	* @return	bool	True or false
434
	* @access	protected
435
	*/
436
	protected function has_ignore_words()
437
	{
438
		return !empty($this->config['similar_topics_words']);
439
	}
440
441
	/**
442
	* Check if the database layer is MySQL4 or later
443
	*
444
	* @return	bool	True is MySQL4 or later, false otherwise
445
	* @access	protected
446
	*/
447
	protected function is_mysql()
448
	{
449
		return ($this->db->get_sql_layer() == 'mysql4' || $this->db->get_sql_layer() == 'mysqli');
450
	}
451
}
452