Completed
Pull Request — master (#124)
by Matt
01:18
created

ideas::query_sort()   A

Complexity

Conditions 4
Paths 8

Size

Total Lines 41

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 41
rs 9.264
c 0
b 0
f 0
cc 4
nc 8
nop 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\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
		$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 = $this->query_count();
126
		}
127
128
		$ideas = $this->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.
176
	 *
177
	 * @param string $sort      A sorting option/collection
178
	 * @param string $direction Will either be ASC or DESC
179
	 * @return \phpbb\ideas\factory\ideas $this For chaining calls
180
	 */
181
	protected function query_sort($sort, $direction)
182
	{
183
		$sort = strtolower($sort);
184
		$direction = $direction === 'DESC' ? 'DESC' : 'ASC';
185
186
		// Most sorting relies on simple ORDER BY statements, but some may use a WHERE statement
187
		$sorting = [
188
			self::SORT_DATE    => ['ORDER_BY' => 'i.idea_date'],
189
			self::SORT_TITLE   => ['ORDER_BY' => 'i.idea_title'],
190
			self::SORT_AUTHOR  => ['ORDER_BY' => 'i.idea_author'],
191
			self::SORT_SCORE   => ['ORDER_BY' => 'CAST(i.idea_votes_up AS decimal) - CAST(i.idea_votes_down AS decimal)'],
192
			self::SORT_VOTES   => ['ORDER_BY' => 'i.idea_votes_up + i.idea_votes_down'],
193
			self::SORT_TOP     => ['WHERE' => 'i.idea_votes_up > i.idea_votes_down'],
194
			self::SORT_MYIDEAS => ['ORDER_BY' => 'i.idea_date', 'WHERE' => 'i.idea_author = ' . (int) $this->user->data['user_id']],
195
		];
196
197
		// Append the new WHERE statement if the sort has one
198
		if (isset($sorting[$sort]['WHERE']))
199
		{
200
			$this->sql['WHERE'][] = $sorting[$sort]['WHERE'];
201
		}
202
203
		// If we have an ORDER BY that is our sort mode. The absence of an ORDER BY
204
		// means we will by default sort ideas based on their calculated score.
205
		if (isset($sorting[$sort]['ORDER_BY']))
206
		{
207
			$this->sql['ORDER_BY'] = "{$sorting[$sort]['ORDER_BY']} $direction";
208
		}
209
		else
210
		{
211
			// https://www.evanmiller.org/how-not-to-sort-by-average-rating.html
212
			$this->sql['SELECT'][] = '((i.idea_votes_up + 1.9208) / (i.idea_votes_up + i.idea_votes_down) -
213
				1.96 * SQRT((i.idea_votes_up * i.idea_votes_down) / (i.idea_votes_up + i.idea_votes_down) + 0.9604) /
214
				(i.idea_votes_up + i.idea_votes_down)) / (1 + 3.8416 / (i.idea_votes_up + i.idea_votes_down))
215
				AS ci_lower_bound';
216
217
			$this->sql['ORDER_BY'] = 'ci_lower_bound ' . $direction;
218
		}
219
220
		return $this;
221
	}
222
223
	/**
224
	 * Update $sql property with additional SQL statements that will filter
225
	 * the query to get ideas within or without certain statuses.
226
	 *
227
	 * @param array|int $status The id of the status(es) to load
228
	 * @return \phpbb\ideas\factory\ideas $this For chaining calls
229
	 */
230
	protected function query_status($status = [])
231
	{
232
		// If we are given some statuses, get ideas from those. Otherwise the default is
233
		// to get ideas excluding Duplicates, Invalid and Implemented statuses.
234
		$this->sql['WHERE'][] = !empty($status) ? $this->db->sql_in_set('i.idea_status', $status) : $this->db->sql_in_set(
235
			'i.idea_status', [self::$statuses['IMPLEMENTED'], self::$statuses['DUPLICATE'], self::$statuses['INVALID'],
236
		], true);
237
238
		return $this;
239
	}
240
241
	/**
242
	 * Run a query using the $sql property to get a collection of ideas.
243
	 *
244
	 * @param int $number The number of ideas to return
245
	 * @param int $start  Start value for pagination
246
	 * @return mixed      Nested array if the query had rows, false otherwise
247
	 * @throws \phpbb\exception\runtime_exception
248
	 */
249
	protected function query_get($number = 10, $start = 0)
250
	{
251
		if (empty($this->sql))
252
		{
253
			throw new runtime_exception('INVALID_IDEA_QUERY');
254
		}
255
256
		$sql = 'SELECT ' . implode(', ', $this->sql['SELECT']) . '
257
			FROM ' . $this->sql['FROM'] . '
258
			INNER JOIN ' . $this->sql['JOIN'] . '
259
			WHERE ' . implode(' AND ', $this->sql['WHERE']) . '
260
			ORDER BY ' . $this->db->sql_escape($this->sql['ORDER_BY']);
261
262
		$result = $this->db->sql_query_limit($sql, $number, $start);
263
		$rows = $this->db->sql_fetchrowset($result);
264
		$this->db->sql_freeresult($result);
265
266
		return $rows;
267
	}
268
269
	/**
270
	 * Run a query using the $sql property to get a count of ideas.
271
	 *
272
	 * @return int The number of ideas
273
	 * @throws \phpbb\exception\runtime_exception
274
	 */
275
	protected function query_count()
276
	{
277
		if (empty($this->sql))
278
		{
279
			throw new runtime_exception('INVALID_IDEA_QUERY');
280
		}
281
282
		$sql = 'SELECT COUNT(i.idea_id) as count
283
			FROM ' . $this->sql['FROM'] . '
284
       		INNER JOIN ' . $this->sql['JOIN'] . '
285
			WHERE ' . implode(' AND ', $this->sql['WHERE']);
286
		$result = $this->db->sql_query($sql);
287
		$count = (int) $this->db->sql_fetchfield('count');
288
		$this->db->sql_freeresult($result);
289
290
		return $count;
291
	}
292
293
	/**
294
	 * Returns the specified idea.
295
	 *
296
	 * @param int $id The ID of the idea to return.
297
	 *
298
	 * @return array|false The idea row set, or false if not found.
299
	 */
300 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...
301
	{
302
		$sql = 'SELECT *
303
			FROM ' . $this->table_ideas . '
304
			WHERE idea_id = ' . (int) $id;
305
		$result = $this->db->sql_query_limit($sql, 1);
306
		$row = $this->db->sql_fetchrow($result);
307
		$this->db->sql_freeresult($result);
308
309
		return $row;
310
	}
311
312
	/**
313
	 * Returns an idea specified by its topic ID.
314
	 *
315
	 * @param int $id The ID of the idea to return.
316
	 *
317
	 * @return array|false The idea row set, or false if not found.
318
	 */
319 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...
320
	{
321
		$sql = 'SELECT idea_id
322
			FROM ' . $this->table_ideas . '
323
			WHERE topic_id = ' . (int) $id;
324
		$result = $this->db->sql_query_limit($sql, 1);
325
		$idea_id = (int) $this->db->sql_fetchfield('idea_id');
326
		$this->db->sql_freeresult($result);
327
328
		return $this->get_idea($idea_id);
329
	}
330
331
	/**
332
	 * Do a live search on idea titles. Return any matches based on a given search query.
333
	 *
334
	 * @param string $search The string of characters to search using LIKE
335
	 * @param int    $limit  The number of results to return
336
	 * @return array An array of matching idea id/key and title/values
337
	 */
338
	public function ideas_title_livesearch($search, $limit = 10)
339
	{
340
		$results = [];
341
		$sql = 'SELECT idea_title, idea_id
342
			FROM ' . $this->table_ideas . '
343
			WHERE idea_title ' . $this->db->sql_like_expression($search . $this->db->get_any_char());
344
		$result = $this->db->sql_query_limit($sql, $limit);
345
		while ($row = $this->db->sql_fetchrow($result))
346
		{
347
			$results[] = [
348
				'idea_id'     => $row['idea_id'],
349
				'result'      => $row['idea_id'],
350
				'clean_title' => $row['idea_title'],
351
				'display'     => "<span>{$row['idea_title']}</span>", // spans are expected in phpBB's live search JS
352
			];
353
		}
354
		$this->db->sql_freeresult($result);
355
356
		return $results;
357
	}
358
359
	/**
360
	 * Returns the status name from the status ID specified.
361
	 *
362
	 * @param int $id ID of the status.
363
	 *
364
	 * @return string|bool The status name if it exists, false otherwise.
365
	 */
366
	public function get_status_from_id($id)
367
	{
368
		return $this->language->lang(array_search($id, self::$statuses));
369
	}
370
371
	/**
372
	 * Updates the status of an idea.
373
	 *
374
	 * @param int $idea_id The ID of the idea.
375
	 * @param int $status  The ID of the status.
376
	 *
377
	 * @return void
378
	 */
379
	public function change_status($idea_id, $status)
380
	{
381
		$sql_ary = array(
382
			'idea_status' => (int) $status,
383
		);
384
385
		$this->update_idea_data($sql_ary, $idea_id, $this->table_ideas);
386
	}
387
388
	/**
389
	 * Sets the ID of the duplicate for an idea.
390
	 *
391
	 * @param int    $idea_id   ID of the idea to be updated.
392
	 * @param string $duplicate Idea ID of duplicate.
393
	 *
394
	 * @return bool True if set, false if invalid.
395
	 */
396 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...
397
	{
398
		if ($duplicate && !is_numeric($duplicate))
399
		{
400
			return false;
401
		}
402
403
		$sql_ary = array(
404
			'duplicate_id'	=> (int) $duplicate,
405
		);
406
407
		$this->update_idea_data($sql_ary, $idea_id, $this->table_ideas);
408
409
		return true;
410
	}
411
412
	/**
413
	 * Sets the RFC link of an idea.
414
	 *
415
	 * @param int    $idea_id ID of the idea to be updated.
416
	 * @param string $rfc     Link to the RFC.
417
	 *
418
	 * @return bool True if set, false if invalid.
419
	 */
420 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...
421
	{
422
		$match = '/^https?:\/\/area51\.phpbb\.com\/phpBB\/viewtopic\.php/';
423
		if ($rfc && !preg_match($match, $rfc))
424
		{
425
			return false;
426
		}
427
428
		$sql_ary = array(
429
			'rfc_link'	=> $rfc, // string is escaped by build_array()
430
		);
431
432
		$this->update_idea_data($sql_ary, $idea_id, $this->table_ideas);
433
434
		return true;
435
	}
436
437
	/**
438
	 * Sets the ticket ID of an idea.
439
	 *
440
	 * @param int    $idea_id ID of the idea to be updated.
441
	 * @param string $ticket  Ticket ID.
442
	 *
443
	 * @return bool True if set, false if invalid.
444
	 */
445 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...
446
	{
447
		if ($ticket && !is_numeric($ticket))
448
		{
449
			return false;
450
		}
451
452
		$sql_ary = array(
453
			'ticket_id'	=> (int) $ticket,
454
		);
455
456
		$this->update_idea_data($sql_ary, $idea_id, $this->table_ideas);
457
458
		return true;
459
	}
460
461
	/**
462
	 * Sets the implemented version of an idea.
463
	 *
464
	 * @param int    $idea_id ID of the idea to be updated.
465
	 * @param string $version Version of phpBB the idea was implemented in.
466
	 *
467
	 * @return bool True if set, false if invalid.
468
	 */
469 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...
470
	{
471
		$match = '/^\d\.\d\.\d+(\-\w+)?$/';
472
		if ($version && !preg_match($match, $version))
473
		{
474
			return false;
475
		}
476
477
		$sql_ary = array(
478
			'implemented_version'	=> $version, // string is escaped by build_array()
479
		);
480
481
		$this->update_idea_data($sql_ary, $idea_id, $this->table_ideas);
482
483
		return true;
484
	}
485
486
	/**
487
	 * Sets the title of an idea.
488
	 *
489
	 * @param int    $idea_id ID of the idea to be updated.
490
	 * @param string $title   New title.
491
	 *
492
	 * @return boolean True if updated, false if invalid length.
493
	 */
494
	public function set_title($idea_id, $title)
495
	{
496
		if (utf8_clean_string($title) === '')
497
		{
498
			return false;
499
		}
500
501
		$sql_ary = array(
502
			'idea_title' => truncate_string($title, self::SUBJECT_LENGTH),
503
		);
504
505
		$this->update_idea_data($sql_ary, $idea_id, $this->table_ideas);
506
507
		return true;
508
	}
509
510
	/**
511
	 * Get the title of an idea.
512
	 *
513
	 * @param int $id ID of an idea
514
	 *
515
	 * @return string The idea's title
516
	 */
517 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...
518
	{
519
		$sql = 'SELECT idea_title
520
			FROM ' . $this->table_ideas . '
521
			WHERE idea_id = ' . (int) $id;
522
		$result = $this->db->sql_query_limit($sql, 1);
523
		$idea_title = $this->db->sql_fetchfield('idea_title');
524
		$this->db->sql_freeresult($result);
525
526
		return $idea_title;
527
	}
528
529
	/**
530
	 * Submits a vote on an idea.
531
	 *
532
	 * @param array $idea    The idea returned by get_idea().
533
	 * @param int   $user_id The ID of the user voting.
534
	 * @param int   $value   Up (1) or down (0)?
535
	 *
536
	 * @return array|string Array of information or string on error.
537
	 */
538
	public function vote(&$idea, $user_id, $value)
539
	{
540
		// Validate $vote - must be 0 or 1
541
		if ($value !== 0 && $value !== 1)
542
		{
543
			return 'INVALID_VOTE';
544
		}
545
546
		// Check whether user has already voted - update if they have
547
		$sql = 'SELECT idea_id, vote_value
548
			FROM ' . $this->table_votes . '
549
			WHERE idea_id = ' . (int) $idea['idea_id'] . '
550
				AND user_id = ' . (int) $user_id;
551
		$this->db->sql_query_limit($sql, 1);
552
		if ($row = $this->db->sql_fetchrow())
553
		{
554
			if ($row['vote_value'] != $value)
555
			{
556
				$sql = 'UPDATE ' . $this->table_votes . '
557
					SET vote_value = ' . $value . '
558
					WHERE user_id = ' . (int) $user_id . '
559
						AND idea_id = ' . (int) $idea['idea_id'];
560
				$this->db->sql_query($sql);
561
562
				if ($value == 1)
563
				{
564
					// Change to upvote
565
					$idea['idea_votes_up']++;
566
					$idea['idea_votes_down']--;
567
				}
568
				else
569
				{
570
					// Change to downvote
571
					$idea['idea_votes_up']--;
572
					$idea['idea_votes_down']++;
573
				}
574
575
				$sql_ary = array(
576
					'idea_votes_up'	    => $idea['idea_votes_up'],
577
					'idea_votes_down'	=> $idea['idea_votes_down'],
578
				);
579
580
				$this->update_idea_data($sql_ary, $idea['idea_id'], $this->table_ideas);
581
			}
582
583
			return array(
584
				'message'	    => $this->language->lang('UPDATED_VOTE'),
585
				'votes_up'	    => $idea['idea_votes_up'],
586
				'votes_down'	=> $idea['idea_votes_down'],
587
				'points'        => $this->language->lang('TOTAL_POINTS', $idea['idea_votes_up'] - $idea['idea_votes_down']),
588
				'voters'		=> $this->get_voters($idea['idea_id']),
589
			);
590
		}
591
592
		// Insert vote into votes table.
593
		$sql_ary = array(
594
			'idea_id'		=> (int) $idea['idea_id'],
595
			'user_id'		=> (int) $user_id,
596
			'vote_value'	=> (int) $value,
597
		);
598
599
		$this->insert_idea_data($sql_ary, $this->table_votes);
600
601
		// Update number of votes in ideas table
602
		$idea['idea_votes_' . ($value ? 'up' : 'down')]++;
603
604
		$sql_ary = array(
605
			'idea_votes_up'	    => $idea['idea_votes_up'],
606
			'idea_votes_down'	=> $idea['idea_votes_down'],
607
		);
608
609
		$this->update_idea_data($sql_ary, $idea['idea_id'], $this->table_ideas);
610
611
		return array(
612
			'message'	    => $this->language->lang('VOTE_SUCCESS'),
613
			'votes_up'	    => $idea['idea_votes_up'],
614
			'votes_down'	=> $idea['idea_votes_down'],
615
			'points'        => $this->language->lang('TOTAL_POINTS', $idea['idea_votes_up'] - $idea['idea_votes_down']),
616
			'voters'		=> $this->get_voters($idea['idea_id']),
617
		);
618
	}
619
620
	/**
621
	 * Remove a user's vote from an idea
622
	 *
623
	 * @param array   $idea    The idea returned by get_idea().
624
	 * @param int     $user_id The ID of the user voting.
625
	 *
626
	 * @return array Array of information.
627
	 */
628
	public function remove_vote(&$idea, $user_id)
629
	{
630
		// Only change something if user has already voted
631
		$sql = 'SELECT idea_id, vote_value
632
			FROM ' . $this->table_votes . '
633
			WHERE idea_id = ' . (int) $idea['idea_id'] . '
634
				AND user_id = ' . (int) $user_id;
635
		$this->db->sql_query_limit($sql, 1);
636
		if ($row = $this->db->sql_fetchrow())
637
		{
638
			$sql = 'DELETE FROM ' . $this->table_votes . '
639
				WHERE idea_id = ' . (int) $idea['idea_id'] . '
640
					AND user_id = ' . (int) $user_id;
641
			$this->db->sql_query($sql);
642
643
			$idea['idea_votes_' . ($row['vote_value'] == 1 ? 'up' : 'down')]--;
644
645
			$sql_ary = array(
646
				'idea_votes_up'	    => $idea['idea_votes_up'],
647
				'idea_votes_down'	=> $idea['idea_votes_down'],
648
			);
649
650
			$this->update_idea_data($sql_ary, $idea['idea_id'], $this->table_ideas);
651
		}
652
653
		return array(
654
			'message'	    => $this->language->lang('UPDATED_VOTE'),
655
			'votes_up'	    => $idea['idea_votes_up'],
656
			'votes_down'	=> $idea['idea_votes_down'],
657
			'points'        => $this->language->lang('TOTAL_POINTS', $idea['idea_votes_up'] - $idea['idea_votes_down']),
658
			'voters'		=> $this->get_voters($idea['idea_id']),
659
		);
660
	}
661
662
	/**
663
	 * Returns voter info on an idea.
664
	 *
665
	 * @param int $id ID of the idea.
666
	 *
667
	 * @return array Array of row data
668
	 */
669
	public function get_voters($id)
670
	{
671
		$sql = 'SELECT iv.user_id, iv.vote_value, u.username, u.user_colour
672
			FROM ' . $this->table_votes . ' as iv,
673
				' . USERS_TABLE . ' as u
674
			WHERE iv.idea_id = ' . (int) $id . '
675
				AND iv.user_id = u.user_id
676
			ORDER BY u.username ASC';
677
		$result = $this->db->sql_query($sql);
678
		$rows = $this->db->sql_fetchrowset($result);
679
		$this->db->sql_freeresult($result);
680
681
		// Process the username for the template now, so it is
682
		// ready to use in AJAX responses and DOM injections.
683
		foreach ($rows as &$row)
684
		{
685
			$row['user'] = get_username_string('full', $row['user_id'], $row['username'], $row['user_colour'], false, $this->profile_url());
686
		}
687
688
		return $rows;
689
	}
690
691
	/**
692
	 * Get a user's votes from a group of ideas
693
	 *
694
	 * @param int $user_id The user's id
695
	 * @param array $ids An array of idea ids
696
	 * @return array An array of ideas the user voted on and their vote result, or empty otherwise.
697
	 *               example: [idea_id => vote_result]
698
	 *                         1 => 1, idea 1, voted up by the user
699
	 *                         2 => 0, idea 2, voted down by the user
700
	 */
701
	public function get_users_votes($user_id, array $ids)
702
	{
703
		$results = [];
704
		$sql = 'SELECT idea_id, vote_value
705
			FROM ' . $this->table_votes . '
706
			WHERE user_id = ' . (int) $user_id . '
707
			AND ' . $this->db->sql_in_set('idea_id', $ids, false, true);
708
		$result = $this->db->sql_query($sql);
709
		while ($row = $this->db->sql_fetchrow($result))
710
		{
711
			$results[$row['idea_id']] = $row['vote_value'];
712
		}
713
		$this->db->sql_freeresult($result);
714
715
		return $results;
716
	}
717
718
	/**
719
	 * Submits a new idea.
720
	 *
721
	 * @param string $title   The title of the idea.
722
	 * @param string $message The description of the idea.
723
	 * @param int    $user_id The ID of the author.
724
	 *
725
	 * @return array|int Either an array of errors, or the ID of the new idea.
726
	 */
727
	public function submit($title, $message, $user_id)
728
	{
729
		$error = array();
730
		if (utf8_clean_string($title) === '')
731
		{
732
			$error[] = $this->language->lang('TITLE_TOO_SHORT');
733
		}
734
		if (utf8_strlen($title) > self::SUBJECT_LENGTH)
735
		{
736
			$error[] = $this->language->lang('TITLE_TOO_LONG', self::SUBJECT_LENGTH);
737
		}
738
		if (utf8_strlen($message) < $this->config['min_post_chars'])
739
		{
740
			$error[] = $this->language->lang('TOO_FEW_CHARS');
741
		}
742
		if ($this->config['max_post_chars'] != 0 && utf8_strlen($message) > $this->config['max_post_chars'])
743
		{
744
			$error[] = $this->language->lang('TOO_MANY_CHARS');
745
		}
746
747
		if (count($error))
748
		{
749
			return $error;
750
		}
751
752
		// Submit idea
753
		$sql_ary = array(
754
			'idea_title'		=> $title,
755
			'idea_author'		=> $user_id,
756
			'idea_date'			=> time(),
757
			'topic_id'			=> 0,
758
		);
759
760
		$idea_id = $this->insert_idea_data($sql_ary, $this->table_ideas);
761
762
		// Initial vote
763
		$idea = $this->get_idea($idea_id);
764
		$this->vote($idea, $this->user->data['user_id'], 1);
0 ignored issues
show
Security Bug introduced by
It seems like $idea defined by $this->get_idea($idea_id) on line 763 can also be of type false; however, phpbb\ideas\factory\ideas::vote() does only seem to accept array, did you maybe forget to handle an error condition?

This check looks for type mismatches where the missing type is false. This is usually indicative of an error condtion.

Consider the follow example

<?php

function getDate($date)
{
    if ($date !== null) {
        return new DateTime($date);
    }

    return false;
}

This function either returns a new DateTime object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returned false before passing on the value to another function or method that may not be able to handle a false.

Loading history...
765
766
		$uid = $bitfield = $options = '';
767
		generate_text_for_storage($message, $uid, $bitfield, $options, true, true, true);
768
769
		$data = array(
770
			'forum_id'			=> (int) $this->config['ideas_forum_id'],
771
			'topic_id'			=> 0,
772
			'icon_id'			=> false,
773
			'poster_id'			=> (int) $this->user->data['user_id'],
774
775
			'enable_bbcode'		=> true,
776
			'enable_smilies'	=> true,
777
			'enable_urls'		=> true,
778
			'enable_sig'		=> true,
779
780
			'message'			=> $message,
781
			'message_md5'		=> md5($message),
782
783
			'bbcode_bitfield'	=> $bitfield,
784
			'bbcode_uid'		=> $uid,
785
786
			'post_edit_locked'	=> 0,
787
			'topic_title'		=> $title,
788
789
			'notify_set'		=> false,
790
			'notify'			=> false,
791
			'post_time'			=> 0,
792
			'forum_name'		=> 'Ideas forum',
793
794
			'enable_indexing'	=> true,
795
796
			'force_approved_state'	=> (!$this->auth->acl_get('f_noapprove', $this->config['ideas_forum_id'])) ? ITEM_UNAPPROVED : true,
797
		);
798
799
		$poll = array();
800
		submit_post('post', $title, $this->user->data['username'], POST_NORMAL, $poll, $data);
801
802
		// Edit topic ID into idea; both should link to each other
803
		$sql_ary = array(
804
			'topic_id' => $data['topic_id'],
805
		);
806
807
		$this->update_idea_data($sql_ary, $idea_id, $this->table_ideas);
808
809
		return $idea_id;
810
	}
811
812
	/**
813
	 * Preview a new idea.
814
	 *
815
	 * @param string $message The description of the idea.
816
	 * @return string The idea parsed for display in preview.
817
	 */
818
	public function preview($message)
819
	{
820
		$uid = $bitfield = $flags = '';
821
		generate_text_for_storage($message, $uid, $bitfield, $flags, true, true, true);
822
		return generate_text_for_display($message, $uid, $bitfield, $flags);
823
	}
824
825
	/**
826
	 * Deletes an idea and the topic to go with it.
827
	 *
828
	 * @param int $id       The ID of the idea to be deleted.
829
	 * @param int $topic_id The ID of the idea topic. Optional, but preferred.
830
	 *
831
	 * @return boolean Whether the idea was deleted or not.
832
	 */
833
	public function delete($id, $topic_id = 0)
834
	{
835
		if (!$topic_id)
836
		{
837
			$idea = $this->get_idea($id);
838
			$topic_id = $idea['topic_id'];
839
		}
840
841
		// Delete topic
842
		delete_posts('topic_id', $topic_id);
843
844
		// Delete idea
845
		$deleted = $this->delete_idea_data($id, $this->table_ideas);
846
847
		// Delete votes
848
		$this->delete_idea_data($id, $this->table_votes);
849
850
		return $deleted;
851
	}
852
853
	/**
854
	 * Delete orphaned ideas. Orphaned ideas may exist after a
855
	 * topic has been deleted or moved to another forum.
856
	 *
857
	 * @return int Number of rows affected
858
	 */
859
	public function delete_orphans()
860
	{
861
		// Find any orphans
862
		$sql = 'SELECT idea_id FROM ' . $this->table_ideas . '
863
 			WHERE topic_id NOT IN (SELECT t.topic_id
864
 			FROM ' . $this->table_topics . ' t
865
 				WHERE t.forum_id = ' . (int) $this->config['ideas_forum_id'] . ')';
866
		$result = $this->db->sql_query($sql);
867
		$rows = $this->db->sql_fetchrowset($result);
868
		$this->db->sql_freeresult($result);
869
870
		if (empty($rows))
871
		{
872
			return 0;
873
		}
874
875
		$this->db->sql_transaction('begin');
876
877
		foreach ($rows as $row)
878
		{
879
			// Delete idea
880
			$this->delete_idea_data($row['idea_id'], $this->table_ideas);
881
882
			// Delete votes
883
			$this->delete_idea_data($row['idea_id'], $this->table_votes);
884
		}
885
886
		$this->db->sql_transaction('commit');
887
888
		return count($rows);
889
	}
890
891
	/**
892
	 * Helper method for inserting new idea data
893
	 *
894
	 * @param array  $data  The array of data to insert
895
	 * @param string $table The name of the table
896
	 *
897
	 * @return int The ID of the inserted row
898
	 */
899
	protected function insert_idea_data(array $data, $table)
900
	{
901
		$sql = 'INSERT INTO ' . $table . '
902
		' . $this->db->sql_build_array('INSERT', $data);
903
		$this->db->sql_query($sql);
904
905
		return (int) $this->db->sql_nextid();
906
	}
907
908
	/**
909
	 * Helper method for updating idea data
910
	 *
911
	 * @param array  $data  The array of data to insert
912
	 * @param int    $id    The ID of the idea
913
	 * @param string $table The name of the table
914
	 *
915
	 * @return void
916
	 */
917
	protected function update_idea_data(array $data, $id, $table)
918
	{
919
		$sql = 'UPDATE ' . $table . '
920
			SET ' . $this->db->sql_build_array('UPDATE', $data) . '
921
			WHERE idea_id = ' . (int) $id;
922
		$this->db->sql_query($sql);
923
	}
924
925
	/**
926
	 * Helper method for deleting idea data
927
	 *
928
	 * @param int    $id    The ID of the idea
929
	 * @param string $table The name of the table
930
	 *
931
	 * @return bool True if idea was deleted, false otherwise
932
	 */
933
	protected function delete_idea_data($id, $table)
934
	{
935
		$sql = 'DELETE FROM ' . $table . '
936
			WHERE idea_id = ' . (int) $id;
937
		$this->db->sql_query($sql);
938
939
		return (bool) $this->db->sql_affectedrows();
940
	}
941
942
	/**
943
	 * Get the stored idea count
944
	 * Note: this should only be called after get_ideas()
945
	 *
946
	 * @return int Count of ideas
947
	 */
948
	public function get_idea_count()
949
	{
950
		return isset($this->idea_count) ? $this->idea_count : 0;
951
	}
952
953
	/**
954
	 * Helper to generate the user profile URL with an
955
	 * absolute URL, which helps avoid problems when
956
	 * used in AJAX requests.
957
	 *
958
	 * @return string User profile URL
959
	 */
960
	protected function profile_url()
961
	{
962
		if (!isset($this->profile_url))
963
		{
964
			$this->profile_url = append_sid(generate_board_url() . "/memberlist.{$this->php_ext}", array('mode' => 'viewprofile'));
965
		}
966
967
		return $this->profile_url;
968
	}
969
}
970