Completed
Pull Request — master (#25)
by Matt
02:07
created

similar_topics::is_mysql()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 4
rs 10
cc 2
eloc 2
nc 2
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 \vse\similartopics\driver\driver_interface */
46
	protected $similartopics_driver;
47
48
	/** @var string phpBB root path  */
49
	protected $root_path;
50
51
	/** @var string PHP file extension */
52
	protected $php_ext;
53
54
	/**
55
	 * Constructor
56
	 *
57
	 * @access public
58
	 * @param \phpbb\auth\auth                  $auth
59
	 * @param \phpbb\cache\service              $cache
60
	 * @param \phpbb\config\config              $config
61
	 * @param \phpbb\db\driver\driver_interface $db
62
	 * @param \phpbb\event\dispatcher_interface $dispatcher
63
	 * @param \phpbb\pagination                 $pagination
64
	 * @param \phpbb\request\request            $request
65
	 * @param \phpbb\template\template          $template
66
	 * @param \phpbb\user                       $user
67
	 * @param \phpbb\content_visibility         $content_visibility
68
	 * @param \vse\similartopics\driver\manager $similartopics_manager
69
	 * @param string                            $root_path
70
	 * @param string                            $php_ext
71
	 */
72
	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, \vse\similartopics\driver\manager $similartopics_manager, $root_path, $php_ext)
73
	{
74
		$this->auth = $auth;
75
		$this->cache = $cache;
76
		$this->config = $config;
77
		$this->db = $db;
78
		$this->dispatcher = $dispatcher;
79
		$this->pagination = $pagination;
80
		$this->request = $request;
81
		$this->template = $template;
82
		$this->user = $user;
83
		$this->content_visibility = $content_visibility;
84
		$this->root_path = $root_path;
85
		$this->php_ext = $php_ext;
86
87
		$this->similartopics_driver = $similartopics_manager->get_driver($db->get_sql_layer());
88
	}
89
90
	/**
91
	 * Is similar topics available?
92
	 *
93
	 * @access public
94
	 * @return bool True if available, false otherwise
95
	 */
96
	public function is_available()
97
	{
98
99
		return $this->is_enabled() && $this->is_viewable() && $this->similartopics_driver !== null;
100
	}
101
102
	/**
103
	 * Is similar topics configured?
104
	 *
105
	 * @access public
106
	 * @return bool True if configured, false otherwise
107
	 */
108
	public function is_enabled()
109
	{
110
		return !empty($this->config['similar_topics']) && !empty($this->config['similar_topics_limit']);
111
	}
112
113
	/**
114
	 * Is similar topics viewable bu the user?
115
	 *
116
	 * @access public
117
	 * @return bool True if viewable, false otherwise
118
	 */
119
	public function is_viewable()
120
	{
121
		return !empty($this->user->data['user_similar_topics']) && $this->auth->acl_get('u_similar_topics');
122
	}
123
124
	/**
125
	 * Get similar topics by matching topic titles
126
	 *
127
	 * NOTE: FULLTEXT has built-in English ignore words. We use phpBB's
128
	 * ignore words for non-English languages. We also remove any
129
	 * admin-defined special ignore words.
130
	 *
131
	 * @access public
132
	 * @param array $topic_data Array with topic data
133
	 */
134
	public function display_similar_topics($topic_data)
135
	{
136
		// If the forum should not display similar topics, no need to continue
137
		if ($topic_data['similar_topics_hide'])
138
		{
139
			return;
140
		}
141
142
		$topic_title = $this->clean_topic_title($topic_data['topic_title']);
143
144
		// If the cleaned up topic_title is empty, no need to continue
145
		if (empty($topic_title))
146
		{
147
			return;
148
		}
149
150
		// Get stored sensitivity value and divide by 10. In query it should be a number between 0.0 to 1.0.
151
		$sensitivity = $this->config->offsetExists('similar_topics_sense') ? $this->config['similar_topics_sense'] / 10 : '0.5';
152
153
		// Similar Topics SQL query is generated in similar topics driver
154
		$sql_array = $this->similartopics_driver->get_query($topic_data['topic_id'], $topic_title, $this->config['similar_topics_time'], $sensitivity);
155
156
		// Add topic tracking data to the query (only if query caching is off)
157
		if ($this->user->data['is_registered'] && $this->config['load_db_lastread'] && !$this->config['similar_topics_cache'])
158
		{
159
			$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']);
160
			$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']);
161
			$sql_array['SELECT'] .= ', tt.mark_time, ft.mark_time as f_mark_time';
162
		}
163
		else if ($this->config['load_anon_lastread'] || $this->user->data['is_registered'])
164
		{
165
			// Cookie based tracking copied from search.php
166
			$tracking_topics = $this->request->variable($this->config['cookie_name'] . '_track', '', true, \phpbb\request\request_interface::COOKIE);
167
			$tracking_topics = $tracking_topics ? tracking_unserialize($tracking_topics) : array();
168
		}
169
170
		// We need to exclude passworded forums so we do not leak the topic title
171
		$passworded_forums = $this->user->get_passworded_forums();
172
173
		// See if the admin set this forum to only search a specific group of other forums, and include them
174
		if (!empty($topic_data['similar_topic_forums']))
175
		{
176
			// Remove any passworded forums from this group of forums we will be searching
177
			$included_forums = array_diff(json_decode($topic_data['similar_topic_forums'], true), $passworded_forums);
178
			// if there's nothing left to display (user has no access to the forums we want to search)
179
			if (empty($included_forums))
180
			{
181
				return;
182
			}
183
184
			$sql_array['WHERE'] .= ' AND ' . $this->db->sql_in_set('f.forum_id', $included_forums);
185
		}
186
		// Otherwise exclude any ignored forums
187
		else
188
		{
189
			// Remove any passworded forums
190
			if (count($passworded_forums))
191
			{
192
				$sql_array['WHERE'] .= ' AND ' . $this->db->sql_in_set('f.forum_id', $passworded_forums, true);
193
			}
194
195
			$sql_array['WHERE'] .= ' AND f.similar_topics_ignore = 0';
196
		}
197
198
		/**
199
		 * Event to modify the sql_array for similar topics
200
		 *
201
		 * @event vse.similartopics.get_topic_data
202
		 * @var array sql_array SQL array to get similar topics data
203
		 * @since 1.3.0
204
		 */
205
		$vars = array('sql_array');
206
		extract($this->dispatcher->trigger_event('vse.similartopics.get_topic_data', compact($vars)));
207
208
		$rowset = array();
209
210
		$sql = $this->db->sql_build_query('SELECT', $sql_array);
211
		$result = $this->db->sql_query_limit($sql, $this->config['similar_topics_limit'], 0, $this->config['similar_topics_cache']);
212
		while ($row = $this->db->sql_fetchrow($result))
213
		{
214
			$rowset[(int) $row['topic_id']] = $row;
215
		}
216
		$this->db->sql_freeresult($result);
217
218
		// Grab icons
219
		$icons = $this->cache->obtain_icons();
220
221
		/**
222
		 * Modify the rowset data for similar topics
223
		 *
224
		 * @event vse.similartopics.modify_rowset
225
		 * @var	array rowset Array with the search results data
226
		 * @since 1.4.2
227
		 */
228
		$vars = array('rowset');
229
		extract($this->dispatcher->trigger_event('vse.similartopics.modify_rowset', compact($vars)));
230
231
		foreach ($rowset as $row)
232
		{
233
			$similar_forum_id = (int) $row['forum_id'];
234
			$similar_topic_id = (int) $row['topic_id'];
235
236
			if ($this->auth->acl_get('f_read', $similar_forum_id))
237
			{
238
				// Get topic tracking info
239
				if ($this->user->data['is_registered'] && $this->config['load_db_lastread'] && !$this->config['similar_topics_cache'])
240
				{
241
					$topic_tracking_info = get_topic_tracking($similar_forum_id, $similar_topic_id, $rowset, array($similar_forum_id => $row['f_mark_time']));
242
				}
243
				else if ($this->config['load_anon_lastread'] || $this->user->data['is_registered'])
244
				{
245
					$topic_tracking_info = get_complete_topic_tracking($similar_forum_id, $similar_topic_id);
246
247
					if (!$this->user->data['is_registered'])
248
					{
249
						$this->user->data['user_lastmark'] = isset($tracking_topics['l']) ? ((int) base_convert($tracking_topics['l'], 36, 10) + (int) $this->config['board_startdate']) : 0;
250
					}
251
				}
252
253
				// Replies
254
				$replies = $this->content_visibility->get_count('topic_posts', $row, $similar_forum_id) - 1;
255
256
				// Get folder img, topic status/type related information
257
				$folder_img = $folder_alt = $topic_type = '';
258
				$unread_topic = isset($topic_tracking_info[$similar_topic_id]) && $row['topic_last_post_time'] > $topic_tracking_info[$similar_topic_id];
259
				topic_status($row, $replies, $unread_topic, $folder_img, $folder_alt, $topic_type);
260
261
				$topic_unapproved = $row['topic_visibility'] == ITEM_UNAPPROVED && $this->auth->acl_get('m_approve', $similar_forum_id);
262
				$posts_unapproved = $row['topic_visibility'] == ITEM_APPROVED && $row['topic_posts_unapproved'] && $this->auth->acl_get('m_approve', $similar_forum_id);
263
				$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) : '';
264
265
				$base_url = append_sid("{$this->root_path}viewtopic.{$this->php_ext}", 'f=' . $similar_forum_id . '&amp;t=' . $similar_topic_id);
266
267
				$topic_row = array(
268
					'TOPIC_AUTHOR_FULL'		=> get_username_string('full', $row['topic_poster'], $row['topic_first_poster_name'], $row['topic_first_poster_colour']),
269
					'FIRST_POST_TIME'		=> $this->user->format_date($row['topic_time']),
270
					'LAST_POST_TIME'		=> $this->user->format_date($row['topic_last_post_time']),
271
					'LAST_POST_AUTHOR_FULL'	=> get_username_string('full', $row['topic_last_poster_id'], $row['topic_last_poster_name'], $row['topic_last_poster_colour']),
272
273
					'TOPIC_REPLIES'			=> $replies,
274
					'TOPIC_VIEWS'			=> $row['topic_views'],
275
					'TOPIC_TITLE'			=> censor_text($row['topic_title']),
276
					'FORUM_TITLE'			=> $row['forum_name'],
277
278
					'TOPIC_IMG_STYLE'		=> $folder_img,
279
					'TOPIC_FOLDER_IMG'		=> $this->user->img($folder_img, $folder_alt),
280
					'TOPIC_FOLDER_IMG_ALT'	=> $this->user->lang($folder_alt),
281
282
					'TOPIC_ICON_IMG'		=> (!empty($icons[$row['icon_id']])) ? $icons[$row['icon_id']]['img'] : '',
283
					'TOPIC_ICON_IMG_WIDTH'	=> (!empty($icons[$row['icon_id']])) ? $icons[$row['icon_id']]['width'] : '',
284
					'TOPIC_ICON_IMG_HEIGHT'	=> (!empty($icons[$row['icon_id']])) ? $icons[$row['icon_id']]['height'] : '',
285
					'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')) : '',
286
					'UNAPPROVED_IMG'		=> ($topic_unapproved || $posts_unapproved) ? $this->user->img('icon_topic_unapproved', $topic_unapproved ? 'TOPIC_UNAPPROVED' : 'POSTS_UNAPPROVED') : '',
287
288
					'S_UNREAD_TOPIC'		=> $unread_topic,
289
					'S_TOPIC_REPORTED'		=> !empty($row['topic_reported']) && $this->auth->acl_get('m_report', $similar_forum_id),
290
					'S_TOPIC_UNAPPROVED'	=> $topic_unapproved,
291
					'S_POSTS_UNAPPROVED'	=> $posts_unapproved,
292
					'S_HAS_POLL'			=> (bool) $row['poll_start'],
293
294
					'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',
295
					'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'],
296
					'U_VIEW_TOPIC'			=> append_sid("{$this->root_path}viewtopic.{$this->php_ext}", 'f=' . $similar_forum_id . '&amp;t=' . $similar_topic_id),
297
					'U_VIEW_FORUM'			=> append_sid("{$this->root_path}viewforum.{$this->php_ext}", 'f=' . $similar_forum_id),
298
					'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),
299
					'U_MCP_QUEUE'			=> $u_mcp_queue,
300
				);
301
302
				/**
303
				 * Event to modify the similar topics template block
304
				 *
305
				 * @event vse.similartopics.modify_topicrow
306
				 * @var array row       Array with similar topic data
307
				 * @var array topic_row Template block array
308
				 * @since 1.3.0
309
				 */
310
				$vars = array('row', 'topic_row');
311
				extract($this->dispatcher->trigger_event('vse.similartopics.modify_topicrow', compact($vars)));
312
313
				$this->template->assign_block_vars('similar', $topic_row);
314
315
				$this->pagination->generate_template_pagination($base_url, 'similar.pagination', 'start', $replies + 1, $this->config['posts_per_page'], 1, true, true);
316
			}
317
		}
318
319
		$this->user->add_lang_ext('vse/similartopics', 'similar_topics');
320
321
		$this->template->assign_vars(array(
322
			'L_SIMILAR_TOPICS'	=> $this->user->lang('SIMILAR_TOPICS'),
323
			'NEWEST_POST_IMG'	=> $this->user->img('icon_topic_newest', 'VIEW_NEWEST_POST'),
324
			'LAST_POST_IMG'		=> $this->user->img('icon_topic_latest', 'VIEW_LATEST_POST'),
325
			'REPORTED_IMG'		=> $this->user->img('icon_topic_reported', 'TOPIC_REPORTED'),
326
			'POLL_IMG'			=> $this->user->img('icon_topic_poll', 'TOPIC_POLL'),
327
			'S_PST_BRANCH'		=> phpbb_version_compare(max($this->config['phpbb_version'], PHPBB_VERSION), '3.2.0-dev', '<') ? '31x' : '32x',
328
		));
329
	}
330
331
	/**
332
	 * Clean topic title (and if needed, ignore-words)
333
	 *
334
	 * @access public
335
	 * @param string $text The topic title
336
	 * @return string The topic title
337
	 */
338
	public function clean_topic_title($text)
339
	{
340
		// Strip quotes, ampersands
341
		$text = str_replace(array('&quot;', '&amp;'), '', $text);
342
343
		if (!$this->english_lang() || $this->has_ignore_words())
344
		{
345
			$text = $this->strip_stop_words($text);
346
		}
347
348
		return $text;
349
	}
350
351
	/**
352
	 * Remove any non-english and/or custom defined ignore-words
353
	 *
354
	 * @access protected
355
	 * @param string $text The topic title
356
	 * @return string The topic title
357
	 */
358
	protected function strip_stop_words($text)
359
	{
360
		$words = array();
361
362
		// If non-English, look for a list of stop-words to be ignored
363
		// in either the core or the extension (deprecated from core)
364
		if (!$this->english_lang())
365
		{
366
			if (file_exists($search_ignore_words = "{$this->user->lang_path}{$this->user->lang_name}/search_ignore_words.{$this->php_ext}") ||
367
				file_exists($search_ignore_words = "{$this->root_path}ext/vse/similartopics/language/{$this->user->lang_name}/search_ignore_words.{$this->php_ext}"))
368
			{
369
				include($search_ignore_words);
370
			}
371
		}
372
373
		if ($this->has_ignore_words())
374
		{
375
			// Merge any custom defined ignore words from the ACP to the stop-words array
376
			$words = array_merge($this->make_word_array($this->config['similar_topics_words']), $words);
377
		}
378
379
		// Remove stop-words from the topic title text
380
		$words = array_diff($this->make_word_array($text), $words);
381
382
		// Convert our words array back to a string
383
		return implode(' ', $words);
384
	}
385
386
	/**
387
	 * Helper function to split string into an array of words
388
	 *
389
	 * @access protected
390
	 * @param string $text String of plain text words
391
	 * @return array Array of plaintext words
392
	 */
393
	protected function make_word_array($text)
394
	{
395
		// Strip out any non-alpha-numeric characters using PCRE regex syntax
396
		$text = trim(preg_replace('#[^\p{L}\p{N}]+#u', ' ', $text));
397
398
		$words = explode(' ', utf8_strtolower($text));
399
		foreach ($words as $key => $word)
400
		{
401
			// Strip words of 2 characters or less
402
			if (utf8_strlen(trim($word)) < 3)
403
			{
404
				unset($words[$key]);
405
			}
406
		}
407
408
		return $words;
409
	}
410
411
	/**
412
	 * Check if English is the current user's language
413
	 *
414
	 * @access protected
415
	 * @return bool True if lang is 'en' or 'en_us', false otherwise
416
	 */
417
	protected function english_lang()
418
	{
419
		return ($this->user->lang_name === 'en' || $this->user->lang_name === 'en_us');
420
	}
421
422
	/**
423
	 * Check if custom ignore words have been defined for similar topics
424
	 *
425
	 * @access protected
426
	 * @return bool True or false
427
	 */
428
	protected function has_ignore_words()
429
	{
430
		return !empty($this->config['similar_topics_words']);
431
	}
432
}
433