Passed
Pull Request — master (#132)
by Matt
01:19
created

ideas   A

Complexity

Total Complexity 27

Size/Duplication

Total Lines 307
Duplicated Lines 0 %

Importance

Changes 35
Bugs 2 Features 1
Metric Value
wmc 27
eloc 108
dl 0
loc 307
rs 10
c 35
b 2
f 1

10 Methods

Rating   Name   Duplication   Size   Complexity  
A livesearch() 0 19 2
A query_count() 0 17 2
A query_get() 0 18 2
A query_status() 0 7 2
A get_users_votes() 0 15 2
A delete_orphans() 0 30 3
A query_sort() 0 40 4
A get_ideas() 0 32 6
A query_ideas() 0 16 2
A get_idea_count() 0 3 2
1
<?php
2
/**
3
 *
4
 * Ideas extension for the phpBB Forum Software package.
5
 *
6
 * @copyright (c) phpBB Limited <https://www.phpbb.com>
7
 * @license GNU General Public License, version 2 (GPL-2.0)
8
 *
9
 */
10
11
namespace phpbb\ideas\factory;
12
13
use phpbb\exception\runtime_exception;
14
use phpbb\ideas\ext;
15
16
/**
17
 * Class for handling multiple ideas
18
 */
19
class ideas extends base
20
{
21
	/** @var int */
22
	protected $idea_count;
23
24
	/** @var array */
25
	protected $sql;
26
27
	/**
28
	 * Returns an array of ideas. Defaults to ten ideas ordered by date
29
	 * excluding implemented, duplicate or invalid ideas.
30
	 *
31
	 * @param int       $number    The number of ideas to return
32
	 * @param string    $sort      A sorting option/collection
33
	 * @param string    $direction Should either be ASC or DESC
34
	 * @param array|int $status    The id of the status(es) to load
35
	 * @param int       $start     Start value for pagination
36
	 *
37
	 * @return array Array of row data
38
	 */
39
	public function get_ideas($number = 10, $sort = 'date', $direction = 'DESC', $status = [], $start = 0)
40
	{
41
		// Initialize a query to request ideas
42
		$sql = $this->query_ideas()
43
			->query_sort($sort, $direction)
44
			->query_status($status);
45
46
		// For pagination, get a count of the total ideas being requested
47
		if ($number >= $this->config['posts_per_page'])
48
		{
49
			$this->idea_count = $sql->query_count();
50
		}
51
52
		$ideas = $sql->query_get($number, $start);
53
54
		if (count($ideas))
55
		{
56
			$topic_ids = array_column($ideas, 'topic_id');
57
			$idea_ids = array_column($ideas, 'idea_id');
58
59
			$topic_tracking_info = get_complete_topic_tracking((int) $this->config['ideas_forum_id'], $topic_ids);
60
			$user_voting_info = $this->get_users_votes($this->user->data['user_id'], $idea_ids);
61
62
			foreach ($ideas as &$idea)
63
			{
64
				$idea['read'] = !(isset($topic_tracking_info[$idea['topic_id']]) && $idea['topic_last_post_time'] > $topic_tracking_info[$idea['topic_id']]);
65
				$idea['u_voted'] = isset($user_voting_info[$idea['idea_id']]) ? (int) $user_voting_info[$idea['idea_id']] : '';
66
			}
67
			unset ($idea);
68
		}
69
70
		return $ideas;
71
	}
72
73
	/**
74
	 * Initialize the $sql property with necessary SQL statements.
75
	 *
76
	 * @return \phpbb\ideas\factory\ideas $this For chaining calls
77
	 */
78
	protected function query_ideas()
79
	{
80
		$this->sql = [];
81
82
		$this->sql['SELECT'][] = 't.topic_last_post_time, t.topic_status, t.topic_visibility, i.*';
83
		$this->sql['FROM'] = "{$this->table_ideas} i";
84
		$this->sql['JOIN'] = "{$this->table_topics} t ON i.topic_id = t.topic_id";
85
		$this->sql['WHERE'][] = 't.forum_id = ' . (int) $this->config['ideas_forum_id'];
86
87
		// Only get approved topics for regular users, Moderators will see unapproved topics
88
		if (!$this->auth->acl_get('m_', $this->config['ideas_forum_id']))
89
		{
90
			$this->sql['WHERE'][] = 't.topic_visibility = ' . ITEM_APPROVED;
91
		}
92
93
		return $this;
94
	}
95
96
	/**
97
	 * Update the $sql property with ORDER BY statements to obtain
98
	 * the requested collection of Ideas. Some instances may add
99
	 * additional WHERE or SELECT statements to refine the collection.
100
	 *
101
	 * @param string $sort      A sorting option/collection
102
	 * @param string $direction Will either be ASC or DESC
103
	 *
104
	 * @return \phpbb\ideas\factory\ideas $this For chaining calls
105
	 */
106
	protected function query_sort($sort, $direction)
107
	{
108
		$sort = strtolower($sort);
109
		$direction = $direction === 'DESC' ? 'DESC' : 'ASC';
110
111
		// Most sorting relies on simple ORDER BY statements, but some may use a WHERE statement
112
		$statements = [
113
			ext::SORT_DATE    => ['ORDER_BY' => 'i.idea_date'],
114
			ext::SORT_TITLE   => ['ORDER_BY' => 'i.idea_title'],
115
			ext::SORT_AUTHOR  => ['ORDER_BY' => 'i.idea_author'],
116
			ext::SORT_SCORE   => ['ORDER_BY' => 'CAST(i.idea_votes_up AS decimal) - CAST(i.idea_votes_down AS decimal)'],
117
			ext::SORT_VOTES   => ['ORDER_BY' => 'i.idea_votes_up + i.idea_votes_down'],
118
			ext::SORT_TOP     => ['WHERE' => 'i.idea_votes_up > i.idea_votes_down'],
119
			ext::SORT_MYIDEAS => ['ORDER_BY' => 'i.idea_date', 'WHERE' => 'i.idea_author = ' . (int) $this->user->data['user_id']],
120
		];
121
122
		// Append a new WHERE statement if the sort has one
123
		if (isset($statements[$sort]['WHERE']))
124
		{
125
			$this->sql['WHERE'][] = $statements[$sort]['WHERE'];
126
		}
127
128
		// If we have an ORDER BY we use that. The absence of an ORDER BY
129
		// means we will default to sorting ideas by their calculated score.
130
		if (isset($statements[$sort]['ORDER_BY']))
131
		{
132
			$this->sql['ORDER_BY'] = "{$statements[$sort]['ORDER_BY']} $direction";
133
		}
134
		else
135
		{
136
			// https://www.evanmiller.org/how-not-to-sort-by-average-rating.html
137
			$this->sql['SELECT'][] = '((i.idea_votes_up + 1.9208) / (i.idea_votes_up + i.idea_votes_down) -
138
				1.96 * SQRT((i.idea_votes_up * i.idea_votes_down) / (i.idea_votes_up + i.idea_votes_down) + 0.9604) /
139
				(i.idea_votes_up + i.idea_votes_down)) / (1 + 3.8416 / (i.idea_votes_up + i.idea_votes_down))
140
				AS ci_lower_bound';
141
142
			$this->sql['ORDER_BY'] = "ci_lower_bound $direction";
143
		}
144
145
		return $this;
146
	}
147
148
	/**
149
	 * Update $sql property with additional SQL statements to filter ideas
150
	 * by status. If $status is given we'll get those ideas. If no $status
151
	 * is given, the default is to get all ideas excluding Duplicates, Invalid
152
	 * and Implemented statuses (because they are considered done & dusted,
153
	 * if they were gases they'd be inert).
154
	 *
155
	 * @param array|int $status The id(s) of the status(es) to load
156
	 *
157
	 * @return \phpbb\ideas\factory\ideas $this For chaining calls
158
	 */
159
	protected function query_status($status = [])
160
	{
161
		$this->sql['WHERE'][] = !empty($status) ? $this->db->sql_in_set('i.idea_status', $status) : $this->db->sql_in_set(
162
			'i.idea_status', [ext::$statuses['IMPLEMENTED'], ext::$statuses['DUPLICATE'], ext::$statuses['INVALID'],
163
		], true);
164
165
		return $this;
166
	}
167
168
	/**
169
	 * Run a query using the $sql property to get a collection of ideas.
170
	 *
171
	 * @param int $number The number of ideas to return
172
	 * @param int $start  Start value for pagination
173
	 *
174
	 * @return mixed      Nested array if the query had rows, false otherwise
175
	 * @throws \phpbb\exception\runtime_exception
176
	 */
177
	protected function query_get($number, $start)
178
	{
179
		if (empty($this->sql))
180
		{
181
			throw new runtime_exception('INVALID_IDEA_QUERY');
182
		}
183
184
		$sql = 'SELECT ' . implode(', ', $this->sql['SELECT']) . '
185
			FROM ' . $this->sql['FROM'] . '
186
			INNER JOIN ' . $this->sql['JOIN'] . '
187
			WHERE ' . implode(' AND ', $this->sql['WHERE']) . '
188
			ORDER BY ' . $this->sql['ORDER_BY'];
189
190
		$result = $this->db->sql_query_limit($sql, $number, $start);
191
		$rows = $this->db->sql_fetchrowset($result);
192
		$this->db->sql_freeresult($result);
193
194
		return $rows;
195
	}
196
197
	/**
198
	 * Run a query using the $sql property to get a count of ideas.
199
	 *
200
	 * @return int The number of ideas
201
	 * @throws \phpbb\exception\runtime_exception
202
	 */
203
	protected function query_count()
204
	{
205
		if (empty($this->sql))
206
		{
207
			throw new runtime_exception('INVALID_IDEA_QUERY');
208
		}
209
210
		$sql = 'SELECT COUNT(i.idea_id) as count
211
			FROM ' . $this->sql['FROM'] . '
212
       		INNER JOIN ' . $this->sql['JOIN'] . '
213
			WHERE ' . implode(' AND ', $this->sql['WHERE']);
214
215
		$result = $this->db->sql_query($sql);
216
		$count = (int) $this->db->sql_fetchfield('count');
217
		$this->db->sql_freeresult($result);
218
219
		return $count;
220
	}
221
222
	/**
223
	 * Do a live search on idea titles. Return any matches based on a given search query.
224
	 *
225
	 * @param string $search The string of characters to search using LIKE
226
	 * @param int    $limit  The number of results to return
227
	 *
228
	 * @return array An array of matching idea id/key and title/values
229
	 */
230
	public function livesearch($search, $limit = 10)
231
	{
232
		$results = [];
233
		$sql = 'SELECT idea_title, idea_id
234
			FROM ' . $this->table_ideas . '
235
			WHERE idea_title ' . $this->db->sql_like_expression($this->db->get_any_char() . $search . $this->db->get_any_char());
236
		$result = $this->db->sql_query_limit($sql, $limit);
237
		while ($row = $this->db->sql_fetchrow($result))
238
		{
239
			$results[] = [
240
				'idea_id'     => $row['idea_id'],
241
				'result'      => $row['idea_id'],
242
				'clean_title' => $row['idea_title'],
243
				'display'     => "<span>{$row['idea_title']}</span>", // spans are expected in phpBB's live search JS
244
			];
245
		}
246
		$this->db->sql_freeresult($result);
247
248
		return $results;
249
	}
250
251
	/**
252
	 * Get a user's votes from a group of ideas
253
	 *
254
	 * @param int $user_id The user's id
255
	 * @param array $ids An array of idea ids
256
	 *
257
	 * @return array An array of ideas the user voted on and their vote result, or empty otherwise.
258
	 *               example: [idea_id => vote_result]
259
	 *                         1 => 1, idea 1, voted up by the user
260
	 *                         2 => 0, idea 2, voted down by the user
261
	 */
262
	public function get_users_votes($user_id, array $ids)
263
	{
264
		$results = [];
265
		$sql = 'SELECT idea_id, vote_value
266
			FROM ' . $this->table_votes . '
267
			WHERE user_id = ' . (int) $user_id . '
268
			AND ' . $this->db->sql_in_set('idea_id', $ids, false, true);
269
		$result = $this->db->sql_query($sql);
270
		while ($row = $this->db->sql_fetchrow($result))
271
		{
272
			$results[$row['idea_id']] = $row['vote_value'];
273
		}
274
		$this->db->sql_freeresult($result);
275
276
		return $results;
277
	}
278
279
	/**
280
	 * Delete orphaned ideas. Orphaned ideas may exist after a
281
	 * topic has been deleted or moved to another forum.
282
	 *
283
	 * @return int Number of rows affected
284
	 */
285
	public function delete_orphans()
286
	{
287
		// Find any orphans
288
		$sql = 'SELECT idea_id FROM ' . $this->table_ideas . '
289
 			WHERE topic_id NOT IN (SELECT t.topic_id
290
 			FROM ' . $this->table_topics . ' t
291
 				WHERE t.forum_id = ' . (int) $this->config['ideas_forum_id'] . ')';
292
		$result = $this->db->sql_query($sql);
293
		$rows = $this->db->sql_fetchrowset($result);
294
		$this->db->sql_freeresult($result);
295
296
		if (empty($rows))
297
		{
298
			return 0;
299
		}
300
301
		$this->db->sql_transaction('begin');
302
303
		foreach ($rows as $row)
304
		{
305
			// Delete idea
306
			$this->delete_idea_data($row['idea_id'], $this->table_ideas);
307
308
			// Delete votes
309
			$this->delete_idea_data($row['idea_id'], $this->table_votes);
310
		}
311
312
		$this->db->sql_transaction('commit');
313
314
		return count($rows);
315
	}
316
317
	/**
318
	 * Get the stored idea count
319
	 * Note: this should only be called after get_ideas()
320
	 *
321
	 * @return int Count of ideas
322
	 */
323
	public function get_idea_count()
324
	{
325
		return isset($this->idea_count) ? $this->idea_count : 0;
326
	}
327
}
328