Completed
Push — master ( 03c5d4...7f0f9f )
by
unknown
01:43 queued 11s
created

ideas   F

Complexity

Total Complexity 69

Size/Duplication

Total Lines 891
Duplicated Lines 12.23 %

Coupling/Cohesion

Components 1
Dependencies 0

Importance

Changes 0
Metric Value
wmc 69
lcom 1
cbo 0
dl 109
loc 891
rs 2.589
c 0
b 0
f 0

31 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 14 14 1
B get_ideas() 0 33 6
A query_ideas() 0 17 2
A query_sort() 0 41 4
A query_status() 0 8 2
A query_get() 0 19 2
A query_count() 0 18 2
A get_idea() 11 11 1
A get_idea_by_topic_id() 11 11 1
A ideas_title_livesearch() 0 20 2
A get_status_from_id() 0 4 1
A change_status() 0 8 1
A set_duplicate() 15 15 3
A set_rfc() 16 16 3
A set_ticket() 15 15 3
A set_implemented() 16 16 3
A set_title() 0 15 2
A get_title() 11 11 1
B vote() 0 81 7
A remove_vote() 0 33 3
A get_voters() 0 21 2
A get_users_votes() 0 16 2
A submit() 0 19 2
A preview() 0 6 1
A delete() 0 19 2
A delete_orphans() 0 31 3
A insert_idea_data() 0 8 1
A update_idea_data() 0 7 1
A delete_idea_data() 0 8 1
A get_idea_count() 0 4 2
A profile_url() 0 9 2

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like ideas often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ideas, and based on these observations, apply Extract Interface, too.

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\auth\auth;
14
use phpbb\config\config;
15
use phpbb\db\driver\driver_interface;
16
use phpbb\exception\runtime_exception;
17
use phpbb\language\language;
18
use phpbb\user;
19
20
class ideas
21
{
22
	const SORT_AUTHOR = 'author';
23
	const SORT_DATE = 'date';
24
	const SORT_NEW = 'new';
25
	const SORT_SCORE = 'score';
26
	const SORT_TITLE = 'title';
27
	const SORT_TOP = 'top';
28
	const SORT_VOTES = 'votes';
29
	const SORT_MYIDEAS = 'egosearch';
30
	const SUBJECT_LENGTH = 120;
31
32
	/** @var array Idea status names and IDs */
33
	public static $statuses = array(
34
		'NEW'			=> 1,
35
		'IN_PROGRESS'	=> 2,
36
		'IMPLEMENTED'	=> 3,
37
		'DUPLICATE'		=> 4,
38
		'INVALID'		=> 5,
39
	);
40
41
	/** @var auth */
42
	protected $auth;
43
44
	/* @var config */
45
	protected $config;
46
47
	/* @var driver_interface */
48
	protected $db;
49
50
	/** @var language */
51
	protected $language;
52
53
	/* @var user */
54
	protected $user;
55
56
	/** @var string */
57
	protected $table_ideas;
58
59
	/** @var string */
60
	protected $table_votes;
61
62
	/** @var string */
63
	protected $table_topics;
64
65
	/** @var int */
66
	protected $idea_count;
67
68
	/** @var string */
69
	protected $php_ext;
70
71
	/** @var string */
72
	protected $profile_url;
73
74
	/** @var array */
75
	protected $sql;
76
77
	/**
78
	 * @param auth             $auth
79
	 * @param config           $config
80
	 * @param driver_interface $db
81
	 * @param language         $language
82
	 * @param user             $user
83
	 * @param string           $table_ideas
84
	 * @param string           $table_votes
85
	 * @param string           $table_topics
86
	 * @param string           $phpEx
87
	 */
88 View Code Duplication
	public function __construct(auth $auth, config $config, driver_interface $db, language $language, user $user, $table_ideas, $table_votes, $table_topics, $phpEx)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
89
	{
90
		$this->auth = $auth;
91
		$this->config = $config;
92
		$this->db = $db;
93
		$this->language = $language;
94
		$this->user = $user;
95
96
		$this->php_ext = $phpEx;
97
98
		$this->table_ideas = $table_ideas;
99
		$this->table_votes = $table_votes;
100
		$this->table_topics = $table_topics;
101
	}
102
103
	/**
104
	 * Returns an array of ideas. Defaults to ten ideas ordered by date
105
	 * excluding implemented, duplicate or invalid ideas.
106
	 *
107
	 * @param int       $number    The number of ideas to return
108
	 * @param string    $sort      A sorting option/collection
109
	 * @param string    $direction Should either be ASC or DESC
110
	 * @param array|int $status    The id of the status(es) to load
111
	 * @param int       $start     Start value for pagination
112
	 *
113
	 * @return array Array of row data
114
	 */
115
	public function get_ideas($number = 10, $sort = 'date', $direction = 'DESC', $status = [], $start = 0)
116
	{
117
		// Initialize a query to request ideas
118
		$sql = $this->query_ideas()
119
			->query_sort($sort, $direction)
120
			->query_status($status);
121
122
		// For pagination, get a count of the total ideas being requested
123
		if ($number >= $this->config['posts_per_page'])
124
		{
125
			$this->idea_count = $sql->query_count();
126
		}
127
128
		$ideas = $sql->query_get($number, $start);
129
130
		if (count($ideas))
131
		{
132
			$topic_ids = array_column($ideas, 'topic_id');
133
			$idea_ids = array_column($ideas, 'idea_id');
134
135
			$topic_tracking_info = get_complete_topic_tracking((int) $this->config['ideas_forum_id'], $topic_ids);
136
			$user_voting_info = $this->get_users_votes($this->user->data['user_id'], $idea_ids);
137
138
			foreach ($ideas as &$idea)
139
			{
140
				$idea['read'] = !(isset($topic_tracking_info[$idea['topic_id']]) && $idea['topic_last_post_time'] > $topic_tracking_info[$idea['topic_id']]);
141
				$idea['u_voted'] = isset($user_voting_info[$idea['idea_id']]) ? (int) $user_voting_info[$idea['idea_id']] : '';
142
			}
143
			unset ($idea);
144
		}
145
146
		return $ideas;
147
	}
148
149
	/**
150
	 * Initialize the $sql property with necessary SQL statements.
151
	 *
152
	 * @return \phpbb\ideas\factory\ideas $this For chaining calls
153
	 */
154
	protected function query_ideas()
155
	{
156
		$this->sql = [];
157
158
		$this->sql['SELECT'][] = 't.topic_last_post_time, t.topic_status, t.topic_visibility, i.*';
159
		$this->sql['FROM'] = "{$this->table_ideas} i";
160
		$this->sql['JOIN'] = "{$this->table_topics} t ON i.topic_id = t.topic_id";
161
		$this->sql['WHERE'][] = 't.forum_id = ' . (int) $this->config['ideas_forum_id'];
162
163
		// Only get approved topics for regular users, Moderators will see unapproved topics
164
		if (!$this->auth->acl_get('m_', $this->config['ideas_forum_id']))
165
		{
166
			$this->sql['WHERE'][] = 't.topic_visibility = ' . ITEM_APPROVED;
167
		}
168
169
		return $this;
170
	}
171
172
	/**
173
	 * Update the $sql property with ORDER BY statements to obtain
174
	 * the requested collection of Ideas. Some instances may add
175
	 * additional WHERE or SELECT statements to refine the collection.
176
	 *
177
	 * @param string $sort      A sorting option/collection
178
	 * @param string $direction Will either be ASC or DESC
179
	 *
180
	 * @return \phpbb\ideas\factory\ideas $this For chaining calls
181
	 */
182
	protected function query_sort($sort, $direction)
183
	{
184
		$sort = strtolower($sort);
185
		$direction = $direction === 'DESC' ? 'DESC' : 'ASC';
186
187
		// Most sorting relies on simple ORDER BY statements, but some may use a WHERE statement
188
		$statements = [
189
			self::SORT_DATE    => ['ORDER_BY' => 'i.idea_date'],
190
			self::SORT_TITLE   => ['ORDER_BY' => 'i.idea_title'],
191
			self::SORT_AUTHOR  => ['ORDER_BY' => 'i.idea_author'],
192
			self::SORT_SCORE   => ['ORDER_BY' => 'CAST(i.idea_votes_up AS decimal) - CAST(i.idea_votes_down AS decimal)'],
193
			self::SORT_VOTES   => ['ORDER_BY' => 'i.idea_votes_up + i.idea_votes_down'],
194
			self::SORT_TOP     => ['WHERE' => 'i.idea_votes_up > i.idea_votes_down'],
195
			self::SORT_MYIDEAS => ['ORDER_BY' => 'i.idea_date', 'WHERE' => 'i.idea_author = ' . (int) $this->user->data['user_id']],
196
		];
197
198
		// Append a new WHERE statement if the sort has one
199
		if (isset($statements[$sort]['WHERE']))
200
		{
201
			$this->sql['WHERE'][] = $statements[$sort]['WHERE'];
202
		}
203
204
		// If we have an ORDER BY we use that. The absence of an ORDER BY
205
		// means we will default to sorting ideas by their calculated score.
206
		if (isset($statements[$sort]['ORDER_BY']))
207
		{
208
			$this->sql['ORDER_BY'] = "{$statements[$sort]['ORDER_BY']} $direction";
209
		}
210
		else
211
		{
212
			// https://www.evanmiller.org/how-not-to-sort-by-average-rating.html
213
			$this->sql['SELECT'][] = '((i.idea_votes_up + 1.9208) / (i.idea_votes_up + i.idea_votes_down) -
214
				1.96 * SQRT((i.idea_votes_up * i.idea_votes_down) / (i.idea_votes_up + i.idea_votes_down) + 0.9604) /
215
				(i.idea_votes_up + i.idea_votes_down)) / (1 + 3.8416 / (i.idea_votes_up + i.idea_votes_down))
216
				AS ci_lower_bound';
217
218
			$this->sql['ORDER_BY'] = "ci_lower_bound $direction";
219
		}
220
221
		return $this;
222
	}
223
224
	/**
225
	 * Update $sql property with additional SQL statements to filter ideas
226
	 * by status. If $status is given we'll get those ideas. If no $status
227
	 * is given, the default is to get all ideas excluding Duplicates, Invalid
228
	 * and Implemented statuses (because they are considered done & dusted,
229
	 * if they were gases they'd be inert).
230
	 *
231
	 * @param array|int $status The id(s) of the status(es) to load
232
	 *
233
	 * @return \phpbb\ideas\factory\ideas $this For chaining calls
234
	 */
235
	protected function query_status($status = [])
236
	{
237
		$this->sql['WHERE'][] = !empty($status) ? $this->db->sql_in_set('i.idea_status', $status) : $this->db->sql_in_set(
238
			'i.idea_status', [self::$statuses['IMPLEMENTED'], self::$statuses['DUPLICATE'], self::$statuses['INVALID'],
239
		], true);
240
241
		return $this;
242
	}
243
244
	/**
245
	 * Run a query using the $sql property to get a collection of ideas.
246
	 *
247
	 * @param int $number The number of ideas to return
248
	 * @param int $start  Start value for pagination
249
	 *
250
	 * @return mixed      Nested array if the query had rows, false otherwise
251
	 * @throws \phpbb\exception\runtime_exception
252
	 */
253
	protected function query_get($number, $start)
254
	{
255
		if (empty($this->sql))
256
		{
257
			throw new runtime_exception('INVALID_IDEA_QUERY');
258
		}
259
260
		$sql = 'SELECT ' . implode(', ', $this->sql['SELECT']) . '
261
			FROM ' . $this->sql['FROM'] . '
262
			INNER JOIN ' . $this->sql['JOIN'] . '
263
			WHERE ' . implode(' AND ', $this->sql['WHERE']) . '
264
			ORDER BY ' . $this->sql['ORDER_BY'];
265
266
		$result = $this->db->sql_query_limit($sql, $number, $start);
267
		$rows = $this->db->sql_fetchrowset($result);
268
		$this->db->sql_freeresult($result);
269
270
		return $rows;
271
	}
272
273
	/**
274
	 * Run a query using the $sql property to get a count of ideas.
275
	 *
276
	 * @return int The number of ideas
277
	 * @throws \phpbb\exception\runtime_exception
278
	 */
279
	protected function query_count()
280
	{
281
		if (empty($this->sql))
282
		{
283
			throw new runtime_exception('INVALID_IDEA_QUERY');
284
		}
285
286
		$sql = 'SELECT COUNT(i.idea_id) as count
287
			FROM ' . $this->sql['FROM'] . '
288
       		INNER JOIN ' . $this->sql['JOIN'] . '
289
			WHERE ' . implode(' AND ', $this->sql['WHERE']);
290
291
		$result = $this->db->sql_query($sql);
292
		$count = (int) $this->db->sql_fetchfield('count');
293
		$this->db->sql_freeresult($result);
294
295
		return $count;
296
	}
297
298
	/**
299
	 * Returns the specified idea.
300
	 *
301
	 * @param int $id The ID of the idea to return.
302
	 *
303
	 * @return array|false The idea row set, or false if not found.
304
	 */
305 View Code Duplication
	public function get_idea($id)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
306
	{
307
		$sql = 'SELECT *
308
			FROM ' . $this->table_ideas . '
309
			WHERE idea_id = ' . (int) $id;
310
		$result = $this->db->sql_query_limit($sql, 1);
311
		$row = $this->db->sql_fetchrow($result);
312
		$this->db->sql_freeresult($result);
313
314
		return $row;
315
	}
316
317
	/**
318
	 * Returns an idea specified by its topic ID.
319
	 *
320
	 * @param int $id The ID of the idea to return.
321
	 *
322
	 * @return array|false The idea row set, or false if not found.
323
	 */
324 View Code Duplication
	public function get_idea_by_topic_id($id)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
325
	{
326
		$sql = 'SELECT idea_id
327
			FROM ' . $this->table_ideas . '
328
			WHERE topic_id = ' . (int) $id;
329
		$result = $this->db->sql_query_limit($sql, 1);
330
		$idea_id = (int) $this->db->sql_fetchfield('idea_id');
331
		$this->db->sql_freeresult($result);
332
333
		return $this->get_idea($idea_id);
334
	}
335
336
	/**
337
	 * Do a live search on idea titles. Return any matches based on a given search query.
338
	 *
339
	 * @param string $search The string of characters to search using LIKE
340
	 * @param int    $limit  The number of results to return
341
	 *
342
	 * @return array An array of matching idea id/key and title/values
343
	 */
344
	public function ideas_title_livesearch($search, $limit = 10)
345
	{
346
		$results = [];
347
		$sql = 'SELECT idea_title, idea_id
348
			FROM ' . $this->table_ideas . '
349
			WHERE idea_title ' . $this->db->sql_like_expression($search . $this->db->get_any_char());
350
		$result = $this->db->sql_query_limit($sql, $limit);
351
		while ($row = $this->db->sql_fetchrow($result))
352
		{
353
			$results[] = [
354
				'idea_id'     => $row['idea_id'],
355
				'result'      => $row['idea_id'],
356
				'clean_title' => $row['idea_title'],
357
				'display'     => "<span>{$row['idea_title']}</span>", // spans are expected in phpBB's live search JS
358
			];
359
		}
360
		$this->db->sql_freeresult($result);
361
362
		return $results;
363
	}
364
365
	/**
366
	 * Returns the status name from the status ID specified.
367
	 *
368
	 * @param int $id ID of the status.
369
	 *
370
	 * @return string|bool The status name if it exists, false otherwise.
371
	 */
372
	public function get_status_from_id($id)
373
	{
374
		return $this->language->lang(array_search($id, self::$statuses));
375
	}
376
377
	/**
378
	 * Updates the status of an idea.
379
	 *
380
	 * @param int $idea_id The ID of the idea.
381
	 * @param int $status  The ID of the status.
382
	 *
383
	 * @return void
384
	 */
385
	public function change_status($idea_id, $status)
386
	{
387
		$sql_ary = array(
388
			'idea_status' => (int) $status,
389
		);
390
391
		$this->update_idea_data($sql_ary, $idea_id, $this->table_ideas);
392
	}
393
394
	/**
395
	 * Sets the ID of the duplicate for an idea.
396
	 *
397
	 * @param int    $idea_id   ID of the idea to be updated.
398
	 * @param string $duplicate Idea ID of duplicate.
399
	 *
400
	 * @return bool True if set, false if invalid.
401
	 */
402 View Code Duplication
	public function set_duplicate($idea_id, $duplicate)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
403
	{
404
		if ($duplicate && !is_numeric($duplicate))
405
		{
406
			return false;
407
		}
408
409
		$sql_ary = array(
410
			'duplicate_id'	=> (int) $duplicate,
411
		);
412
413
		$this->update_idea_data($sql_ary, $idea_id, $this->table_ideas);
414
415
		return true;
416
	}
417
418
	/**
419
	 * Sets the RFC link of an idea.
420
	 *
421
	 * @param int    $idea_id ID of the idea to be updated.
422
	 * @param string $rfc     Link to the RFC.
423
	 *
424
	 * @return bool True if set, false if invalid.
425
	 */
426 View Code Duplication
	public function set_rfc($idea_id, $rfc)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
427
	{
428
		$match = '/^https?:\/\/area51\.phpbb\.com\/phpBB\/viewtopic\.php/';
429
		if ($rfc && !preg_match($match, $rfc))
430
		{
431
			return false;
432
		}
433
434
		$sql_ary = array(
435
			'rfc_link'	=> $rfc, // string is escaped by build_array()
436
		);
437
438
		$this->update_idea_data($sql_ary, $idea_id, $this->table_ideas);
439
440
		return true;
441
	}
442
443
	/**
444
	 * Sets the ticket ID of an idea.
445
	 *
446
	 * @param int    $idea_id ID of the idea to be updated.
447
	 * @param string $ticket  Ticket ID.
448
	 *
449
	 * @return bool True if set, false if invalid.
450
	 */
451 View Code Duplication
	public function set_ticket($idea_id, $ticket)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
452
	{
453
		if ($ticket && !is_numeric($ticket))
454
		{
455
			return false;
456
		}
457
458
		$sql_ary = array(
459
			'ticket_id'	=> (int) $ticket,
460
		);
461
462
		$this->update_idea_data($sql_ary, $idea_id, $this->table_ideas);
463
464
		return true;
465
	}
466
467
	/**
468
	 * Sets the implemented version of an idea.
469
	 *
470
	 * @param int    $idea_id ID of the idea to be updated.
471
	 * @param string $version Version of phpBB the idea was implemented in.
472
	 *
473
	 * @return bool True if set, false if invalid.
474
	 */
475 View Code Duplication
	public function set_implemented($idea_id, $version)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
476
	{
477
		$match = '/^\d\.\d\.\d+(\-\w+)?$/';
478
		if ($version && !preg_match($match, $version))
479
		{
480
			return false;
481
		}
482
483
		$sql_ary = array(
484
			'implemented_version'	=> $version, // string is escaped by build_array()
485
		);
486
487
		$this->update_idea_data($sql_ary, $idea_id, $this->table_ideas);
488
489
		return true;
490
	}
491
492
	/**
493
	 * Sets the title of an idea.
494
	 *
495
	 * @param int    $idea_id ID of the idea to be updated.
496
	 * @param string $title   New title.
497
	 *
498
	 * @return boolean True if updated, false if invalid length.
499
	 */
500
	public function set_title($idea_id, $title)
501
	{
502
		if (utf8_clean_string($title) === '')
503
		{
504
			return false;
505
		}
506
507
		$sql_ary = array(
508
			'idea_title' => truncate_string($title, self::SUBJECT_LENGTH),
509
		);
510
511
		$this->update_idea_data($sql_ary, $idea_id, $this->table_ideas);
512
513
		return true;
514
	}
515
516
	/**
517
	 * Get the title of an idea.
518
	 *
519
	 * @param int $id ID of an idea
520
	 *
521
	 * @return string The idea's title
522
	 */
523 View Code Duplication
	public function get_title($id)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
524
	{
525
		$sql = 'SELECT idea_title
526
			FROM ' . $this->table_ideas . '
527
			WHERE idea_id = ' . (int) $id;
528
		$result = $this->db->sql_query_limit($sql, 1);
529
		$idea_title = $this->db->sql_fetchfield('idea_title');
530
		$this->db->sql_freeresult($result);
531
532
		return $idea_title;
533
	}
534
535
	/**
536
	 * Submits a vote on an idea.
537
	 *
538
	 * @param array $idea    The idea returned by get_idea().
539
	 * @param int   $user_id The ID of the user voting.
540
	 * @param int   $value   Up (1) or down (0)?
541
	 *
542
	 * @return array|string Array of information or string on error.
543
	 */
544
	public function vote(&$idea, $user_id, $value)
545
	{
546
		// Validate $vote - must be 0 or 1
547
		if ($value !== 0 && $value !== 1)
548
		{
549
			return 'INVALID_VOTE';
550
		}
551
552
		// Check whether user has already voted - update if they have
553
		$sql = 'SELECT idea_id, vote_value
554
			FROM ' . $this->table_votes . '
555
			WHERE idea_id = ' . (int) $idea['idea_id'] . '
556
				AND user_id = ' . (int) $user_id;
557
		$this->db->sql_query_limit($sql, 1);
558
		if ($row = $this->db->sql_fetchrow())
559
		{
560
			if ($row['vote_value'] != $value)
561
			{
562
				$sql = 'UPDATE ' . $this->table_votes . '
563
					SET vote_value = ' . $value . '
564
					WHERE user_id = ' . (int) $user_id . '
565
						AND idea_id = ' . (int) $idea['idea_id'];
566
				$this->db->sql_query($sql);
567
568
				if ($value == 1)
569
				{
570
					// Change to upvote
571
					$idea['idea_votes_up']++;
572
					$idea['idea_votes_down']--;
573
				}
574
				else
575
				{
576
					// Change to downvote
577
					$idea['idea_votes_up']--;
578
					$idea['idea_votes_down']++;
579
				}
580
581
				$sql_ary = array(
582
					'idea_votes_up'	    => $idea['idea_votes_up'],
583
					'idea_votes_down'	=> $idea['idea_votes_down'],
584
				);
585
586
				$this->update_idea_data($sql_ary, $idea['idea_id'], $this->table_ideas);
587
			}
588
589
			return array(
590
				'message'	    => $this->language->lang('UPDATED_VOTE'),
591
				'votes_up'	    => $idea['idea_votes_up'],
592
				'votes_down'	=> $idea['idea_votes_down'],
593
				'points'        => $this->language->lang('TOTAL_POINTS', $idea['idea_votes_up'] - $idea['idea_votes_down']),
594
				'voters'		=> $this->get_voters($idea['idea_id']),
595
			);
596
		}
597
598
		// Insert vote into votes table.
599
		$sql_ary = array(
600
			'idea_id'		=> (int) $idea['idea_id'],
601
			'user_id'		=> (int) $user_id,
602
			'vote_value'	=> (int) $value,
603
		);
604
605
		$this->insert_idea_data($sql_ary, $this->table_votes);
606
607
		// Update number of votes in ideas table
608
		$idea['idea_votes_' . ($value ? 'up' : 'down')]++;
609
610
		$sql_ary = array(
611
			'idea_votes_up'	    => $idea['idea_votes_up'],
612
			'idea_votes_down'	=> $idea['idea_votes_down'],
613
		);
614
615
		$this->update_idea_data($sql_ary, $idea['idea_id'], $this->table_ideas);
616
617
		return array(
618
			'message'	    => $this->language->lang('VOTE_SUCCESS'),
619
			'votes_up'	    => $idea['idea_votes_up'],
620
			'votes_down'	=> $idea['idea_votes_down'],
621
			'points'        => $this->language->lang('TOTAL_POINTS', $idea['idea_votes_up'] - $idea['idea_votes_down']),
622
			'voters'		=> $this->get_voters($idea['idea_id']),
623
		);
624
	}
625
626
	/**
627
	 * Remove a user's vote from an idea
628
	 *
629
	 * @param array   $idea    The idea returned by get_idea().
630
	 * @param int     $user_id The ID of the user voting.
631
	 *
632
	 * @return array Array of information.
633
	 */
634
	public function remove_vote(&$idea, $user_id)
635
	{
636
		// Only change something if user has already voted
637
		$sql = 'SELECT idea_id, vote_value
638
			FROM ' . $this->table_votes . '
639
			WHERE idea_id = ' . (int) $idea['idea_id'] . '
640
				AND user_id = ' . (int) $user_id;
641
		$this->db->sql_query_limit($sql, 1);
642
		if ($row = $this->db->sql_fetchrow())
643
		{
644
			$sql = 'DELETE FROM ' . $this->table_votes . '
645
				WHERE idea_id = ' . (int) $idea['idea_id'] . '
646
					AND user_id = ' . (int) $user_id;
647
			$this->db->sql_query($sql);
648
649
			$idea['idea_votes_' . ($row['vote_value'] == 1 ? 'up' : 'down')]--;
650
651
			$sql_ary = array(
652
				'idea_votes_up'	    => $idea['idea_votes_up'],
653
				'idea_votes_down'	=> $idea['idea_votes_down'],
654
			);
655
656
			$this->update_idea_data($sql_ary, $idea['idea_id'], $this->table_ideas);
657
		}
658
659
		return array(
660
			'message'	    => $this->language->lang('UPDATED_VOTE'),
661
			'votes_up'	    => $idea['idea_votes_up'],
662
			'votes_down'	=> $idea['idea_votes_down'],
663
			'points'        => $this->language->lang('TOTAL_POINTS', $idea['idea_votes_up'] - $idea['idea_votes_down']),
664
			'voters'		=> $this->get_voters($idea['idea_id']),
665
		);
666
	}
667
668
	/**
669
	 * Returns voter info on an idea.
670
	 *
671
	 * @param int $id ID of the idea.
672
	 *
673
	 * @return array Array of row data
674
	 */
675
	public function get_voters($id)
676
	{
677
		$sql = 'SELECT iv.user_id, iv.vote_value, u.username, u.user_colour
678
			FROM ' . $this->table_votes . ' as iv,
679
				' . USERS_TABLE . ' as u
680
			WHERE iv.idea_id = ' . (int) $id . '
681
				AND iv.user_id = u.user_id
682
			ORDER BY u.username ASC';
683
		$result = $this->db->sql_query($sql);
684
		$rows = $this->db->sql_fetchrowset($result);
685
		$this->db->sql_freeresult($result);
686
687
		// Process the username for the template now, so it is
688
		// ready to use in AJAX responses and DOM injections.
689
		foreach ($rows as &$row)
690
		{
691
			$row['user'] = get_username_string('full', $row['user_id'], $row['username'], $row['user_colour'], false, $this->profile_url());
692
		}
693
694
		return $rows;
695
	}
696
697
	/**
698
	 * Get a user's votes from a group of ideas
699
	 *
700
	 * @param int $user_id The user's id
701
	 * @param array $ids An array of idea ids
702
	 *
703
	 * @return array An array of ideas the user voted on and their vote result, or empty otherwise.
704
	 *               example: [idea_id => vote_result]
705
	 *                         1 => 1, idea 1, voted up by the user
706
	 *                         2 => 0, idea 2, voted down by the user
707
	 */
708
	public function get_users_votes($user_id, array $ids)
709
	{
710
		$results = [];
711
		$sql = 'SELECT idea_id, vote_value
712
			FROM ' . $this->table_votes . '
713
			WHERE user_id = ' . (int) $user_id . '
714
			AND ' . $this->db->sql_in_set('idea_id', $ids, false, true);
715
		$result = $this->db->sql_query($sql);
716
		while ($row = $this->db->sql_fetchrow($result))
717
		{
718
			$results[$row['idea_id']] = $row['vote_value'];
719
		}
720
		$this->db->sql_freeresult($result);
721
722
		return $results;
723
	}
724
725
	/**
726
	 * Submit new idea data to the ideas table
727
	 *
728
	 * @param array $data An array of post data from a newly posted idea
729
	 *
730
	 * @return int The ID of the new idea.
731
	 */
732
	public function submit($data)
733
	{
734
		$sql_ary = [
735
			'idea_title'	=> $data['topic_title'],
736
			'idea_author'	=> $data['poster_id'],
737
			'idea_date'		=> $data['post_time'],
738
			'topic_id'		=> $data['topic_id'],
739
		];
740
741
		$idea_id = $this->insert_idea_data($sql_ary, $this->table_ideas);
742
743
		// Initial vote
744
		if (($idea = $this->get_idea($idea_id)) !== false)
745
		{
746
			$this->vote($idea, $data['poster_id'], 1);
747
		}
748
749
		return $idea_id;
750
	}
751
752
	/**
753
	 * Preview a new idea.
754
	 *
755
	 * @param string $message The description of the idea.
756
	 *
757
	 * @return string The idea parsed for display in preview.
758
	 */
759
	public function preview($message)
760
	{
761
		$uid = $bitfield = $flags = '';
762
		generate_text_for_storage($message, $uid, $bitfield, $flags, true, true, true);
763
		return generate_text_for_display($message, $uid, $bitfield, $flags);
764
	}
765
766
	/**
767
	 * Deletes an idea and the topic to go with it.
768
	 *
769
	 * @param int $id       The ID of the idea to be deleted.
770
	 * @param int $topic_id The ID of the idea topic. Optional, but preferred.
771
	 *
772
	 * @return boolean Whether the idea was deleted or not.
773
	 */
774
	public function delete($id, $topic_id = 0)
775
	{
776
		if (!$topic_id)
777
		{
778
			$idea = $this->get_idea($id);
779
			$topic_id = $idea['topic_id'];
780
		}
781
782
		// Delete topic
783
		delete_posts('topic_id', $topic_id);
784
785
		// Delete idea
786
		$deleted = $this->delete_idea_data($id, $this->table_ideas);
787
788
		// Delete votes
789
		$this->delete_idea_data($id, $this->table_votes);
790
791
		return $deleted;
792
	}
793
794
	/**
795
	 * Delete orphaned ideas. Orphaned ideas may exist after a
796
	 * topic has been deleted or moved to another forum.
797
	 *
798
	 * @return int Number of rows affected
799
	 */
800
	public function delete_orphans()
801
	{
802
		// Find any orphans
803
		$sql = 'SELECT idea_id FROM ' . $this->table_ideas . '
804
 			WHERE topic_id NOT IN (SELECT t.topic_id
805
 			FROM ' . $this->table_topics . ' t
806
 				WHERE t.forum_id = ' . (int) $this->config['ideas_forum_id'] . ')';
807
		$result = $this->db->sql_query($sql);
808
		$rows = $this->db->sql_fetchrowset($result);
809
		$this->db->sql_freeresult($result);
810
811
		if (empty($rows))
812
		{
813
			return 0;
814
		}
815
816
		$this->db->sql_transaction('begin');
817
818
		foreach ($rows as $row)
819
		{
820
			// Delete idea
821
			$this->delete_idea_data($row['idea_id'], $this->table_ideas);
822
823
			// Delete votes
824
			$this->delete_idea_data($row['idea_id'], $this->table_votes);
825
		}
826
827
		$this->db->sql_transaction('commit');
828
829
		return count($rows);
830
	}
831
832
	/**
833
	 * Helper method for inserting new idea data
834
	 *
835
	 * @param array  $data  The array of data to insert
836
	 * @param string $table The name of the table
837
	 *
838
	 * @return int The ID of the inserted row
839
	 */
840
	protected function insert_idea_data(array $data, $table)
841
	{
842
		$sql = 'INSERT INTO ' . $table . '
843
		' . $this->db->sql_build_array('INSERT', $data);
844
		$this->db->sql_query($sql);
845
846
		return (int) $this->db->sql_nextid();
847
	}
848
849
	/**
850
	 * Helper method for updating idea data
851
	 *
852
	 * @param array  $data  The array of data to insert
853
	 * @param int    $id    The ID of the idea
854
	 * @param string $table The name of the table
855
	 *
856
	 * @return void
857
	 */
858
	protected function update_idea_data(array $data, $id, $table)
859
	{
860
		$sql = 'UPDATE ' . $table . '
861
			SET ' . $this->db->sql_build_array('UPDATE', $data) . '
862
			WHERE idea_id = ' . (int) $id;
863
		$this->db->sql_query($sql);
864
	}
865
866
	/**
867
	 * Helper method for deleting idea data
868
	 *
869
	 * @param int    $id    The ID of the idea
870
	 * @param string $table The name of the table
871
	 *
872
	 * @return bool True if idea was deleted, false otherwise
873
	 */
874
	protected function delete_idea_data($id, $table)
875
	{
876
		$sql = 'DELETE FROM ' . $table . '
877
			WHERE idea_id = ' . (int) $id;
878
		$this->db->sql_query($sql);
879
880
		return (bool) $this->db->sql_affectedrows();
881
	}
882
883
	/**
884
	 * Get the stored idea count
885
	 * Note: this should only be called after get_ideas()
886
	 *
887
	 * @return int Count of ideas
888
	 */
889
	public function get_idea_count()
890
	{
891
		return isset($this->idea_count) ? $this->idea_count : 0;
892
	}
893
894
	/**
895
	 * Helper to generate the user profile URL with an
896
	 * absolute URL, which helps avoid problems when
897
	 * used in AJAX requests.
898
	 *
899
	 * @return string User profile URL
900
	 */
901
	protected function profile_url()
902
	{
903
		if (!isset($this->profile_url))
904
		{
905
			$this->profile_url = append_sid(generate_board_url() . "/memberlist.{$this->php_ext}", array('mode' => 'viewprofile'));
906
		}
907
908
		return $this->profile_url;
909
	}
910
}
911