Completed
Push — master ( de120c...f5f15c )
by
unknown
13s queued 11s
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
		$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
	 * @return array An array of matching idea id/key and title/values
342
	 */
343
	public function ideas_title_livesearch($search, $limit = 10)
344
	{
345
		$results = [];
346
		$sql = 'SELECT idea_title, idea_id
347
			FROM ' . $this->table_ideas . '
348
			WHERE idea_title ' . $this->db->sql_like_expression($search . $this->db->get_any_char());
349
		$result = $this->db->sql_query_limit($sql, $limit);
350
		while ($row = $this->db->sql_fetchrow($result))
351
		{
352
			$results[] = [
353
				'idea_id'     => $row['idea_id'],
354
				'result'      => $row['idea_id'],
355
				'clean_title' => $row['idea_title'],
356
				'display'     => "<span>{$row['idea_title']}</span>", // spans are expected in phpBB's live search JS
357
			];
358
		}
359
		$this->db->sql_freeresult($result);
360
361
		return $results;
362
	}
363
364
	/**
365
	 * Returns the status name from the status ID specified.
366
	 *
367
	 * @param int $id ID of the status.
368
	 *
369
	 * @return string|bool The status name if it exists, false otherwise.
370
	 */
371
	public function get_status_from_id($id)
372
	{
373
		return $this->language->lang(array_search($id, self::$statuses));
374
	}
375
376
	/**
377
	 * Updates the status of an idea.
378
	 *
379
	 * @param int $idea_id The ID of the idea.
380
	 * @param int $status  The ID of the status.
381
	 *
382
	 * @return void
383
	 */
384
	public function change_status($idea_id, $status)
385
	{
386
		$sql_ary = array(
387
			'idea_status' => (int) $status,
388
		);
389
390
		$this->update_idea_data($sql_ary, $idea_id, $this->table_ideas);
391
	}
392
393
	/**
394
	 * Sets the ID of the duplicate for an idea.
395
	 *
396
	 * @param int    $idea_id   ID of the idea to be updated.
397
	 * @param string $duplicate Idea ID of duplicate.
398
	 *
399
	 * @return bool True if set, false if invalid.
400
	 */
401 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...
402
	{
403
		if ($duplicate && !is_numeric($duplicate))
404
		{
405
			return false;
406
		}
407
408
		$sql_ary = array(
409
			'duplicate_id'	=> (int) $duplicate,
410
		);
411
412
		$this->update_idea_data($sql_ary, $idea_id, $this->table_ideas);
413
414
		return true;
415
	}
416
417
	/**
418
	 * Sets the RFC link of an idea.
419
	 *
420
	 * @param int    $idea_id ID of the idea to be updated.
421
	 * @param string $rfc     Link to the RFC.
422
	 *
423
	 * @return bool True if set, false if invalid.
424
	 */
425 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...
426
	{
427
		$match = '/^https?:\/\/area51\.phpbb\.com\/phpBB\/viewtopic\.php/';
428
		if ($rfc && !preg_match($match, $rfc))
429
		{
430
			return false;
431
		}
432
433
		$sql_ary = array(
434
			'rfc_link'	=> $rfc, // string is escaped by build_array()
435
		);
436
437
		$this->update_idea_data($sql_ary, $idea_id, $this->table_ideas);
438
439
		return true;
440
	}
441
442
	/**
443
	 * Sets the ticket ID of an idea.
444
	 *
445
	 * @param int    $idea_id ID of the idea to be updated.
446
	 * @param string $ticket  Ticket ID.
447
	 *
448
	 * @return bool True if set, false if invalid.
449
	 */
450 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...
451
	{
452
		if ($ticket && !is_numeric($ticket))
453
		{
454
			return false;
455
		}
456
457
		$sql_ary = array(
458
			'ticket_id'	=> (int) $ticket,
459
		);
460
461
		$this->update_idea_data($sql_ary, $idea_id, $this->table_ideas);
462
463
		return true;
464
	}
465
466
	/**
467
	 * Sets the implemented version of an idea.
468
	 *
469
	 * @param int    $idea_id ID of the idea to be updated.
470
	 * @param string $version Version of phpBB the idea was implemented in.
471
	 *
472
	 * @return bool True if set, false if invalid.
473
	 */
474 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...
475
	{
476
		$match = '/^\d\.\d\.\d+(\-\w+)?$/';
477
		if ($version && !preg_match($match, $version))
478
		{
479
			return false;
480
		}
481
482
		$sql_ary = array(
483
			'implemented_version'	=> $version, // string is escaped by build_array()
484
		);
485
486
		$this->update_idea_data($sql_ary, $idea_id, $this->table_ideas);
487
488
		return true;
489
	}
490
491
	/**
492
	 * Sets the title of an idea.
493
	 *
494
	 * @param int    $idea_id ID of the idea to be updated.
495
	 * @param string $title   New title.
496
	 *
497
	 * @return boolean True if updated, false if invalid length.
498
	 */
499
	public function set_title($idea_id, $title)
500
	{
501
		if (utf8_clean_string($title) === '')
502
		{
503
			return false;
504
		}
505
506
		$sql_ary = array(
507
			'idea_title' => truncate_string($title, self::SUBJECT_LENGTH),
508
		);
509
510
		$this->update_idea_data($sql_ary, $idea_id, $this->table_ideas);
511
512
		return true;
513
	}
514
515
	/**
516
	 * Get the title of an idea.
517
	 *
518
	 * @param int $id ID of an idea
519
	 *
520
	 * @return string The idea's title
521
	 */
522 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...
523
	{
524
		$sql = 'SELECT idea_title
525
			FROM ' . $this->table_ideas . '
526
			WHERE idea_id = ' . (int) $id;
527
		$result = $this->db->sql_query_limit($sql, 1);
528
		$idea_title = $this->db->sql_fetchfield('idea_title');
529
		$this->db->sql_freeresult($result);
530
531
		return $idea_title;
532
	}
533
534
	/**
535
	 * Submits a vote on an idea.
536
	 *
537
	 * @param array $idea    The idea returned by get_idea().
538
	 * @param int   $user_id The ID of the user voting.
539
	 * @param int   $value   Up (1) or down (0)?
540
	 *
541
	 * @return array|string Array of information or string on error.
542
	 */
543
	public function vote(&$idea, $user_id, $value)
544
	{
545
		// Validate $vote - must be 0 or 1
546
		if ($value !== 0 && $value !== 1)
547
		{
548
			return 'INVALID_VOTE';
549
		}
550
551
		// Check whether user has already voted - update if they have
552
		$sql = 'SELECT idea_id, vote_value
553
			FROM ' . $this->table_votes . '
554
			WHERE idea_id = ' . (int) $idea['idea_id'] . '
555
				AND user_id = ' . (int) $user_id;
556
		$this->db->sql_query_limit($sql, 1);
557
		if ($row = $this->db->sql_fetchrow())
558
		{
559
			if ($row['vote_value'] != $value)
560
			{
561
				$sql = 'UPDATE ' . $this->table_votes . '
562
					SET vote_value = ' . $value . '
563
					WHERE user_id = ' . (int) $user_id . '
564
						AND idea_id = ' . (int) $idea['idea_id'];
565
				$this->db->sql_query($sql);
566
567
				if ($value == 1)
568
				{
569
					// Change to upvote
570
					$idea['idea_votes_up']++;
571
					$idea['idea_votes_down']--;
572
				}
573
				else
574
				{
575
					// Change to downvote
576
					$idea['idea_votes_up']--;
577
					$idea['idea_votes_down']++;
578
				}
579
580
				$sql_ary = array(
581
					'idea_votes_up'	    => $idea['idea_votes_up'],
582
					'idea_votes_down'	=> $idea['idea_votes_down'],
583
				);
584
585
				$this->update_idea_data($sql_ary, $idea['idea_id'], $this->table_ideas);
586
			}
587
588
			return array(
589
				'message'	    => $this->language->lang('UPDATED_VOTE'),
590
				'votes_up'	    => $idea['idea_votes_up'],
591
				'votes_down'	=> $idea['idea_votes_down'],
592
				'points'        => $this->language->lang('TOTAL_POINTS', $idea['idea_votes_up'] - $idea['idea_votes_down']),
593
				'voters'		=> $this->get_voters($idea['idea_id']),
594
			);
595
		}
596
597
		// Insert vote into votes table.
598
		$sql_ary = array(
599
			'idea_id'		=> (int) $idea['idea_id'],
600
			'user_id'		=> (int) $user_id,
601
			'vote_value'	=> (int) $value,
602
		);
603
604
		$this->insert_idea_data($sql_ary, $this->table_votes);
605
606
		// Update number of votes in ideas table
607
		$idea['idea_votes_' . ($value ? 'up' : 'down')]++;
608
609
		$sql_ary = array(
610
			'idea_votes_up'	    => $idea['idea_votes_up'],
611
			'idea_votes_down'	=> $idea['idea_votes_down'],
612
		);
613
614
		$this->update_idea_data($sql_ary, $idea['idea_id'], $this->table_ideas);
615
616
		return array(
617
			'message'	    => $this->language->lang('VOTE_SUCCESS'),
618
			'votes_up'	    => $idea['idea_votes_up'],
619
			'votes_down'	=> $idea['idea_votes_down'],
620
			'points'        => $this->language->lang('TOTAL_POINTS', $idea['idea_votes_up'] - $idea['idea_votes_down']),
621
			'voters'		=> $this->get_voters($idea['idea_id']),
622
		);
623
	}
624
625
	/**
626
	 * Remove a user's vote from an idea
627
	 *
628
	 * @param array   $idea    The idea returned by get_idea().
629
	 * @param int     $user_id The ID of the user voting.
630
	 *
631
	 * @return array Array of information.
632
	 */
633
	public function remove_vote(&$idea, $user_id)
634
	{
635
		// Only change something if user has already voted
636
		$sql = 'SELECT idea_id, vote_value
637
			FROM ' . $this->table_votes . '
638
			WHERE idea_id = ' . (int) $idea['idea_id'] . '
639
				AND user_id = ' . (int) $user_id;
640
		$this->db->sql_query_limit($sql, 1);
641
		if ($row = $this->db->sql_fetchrow())
642
		{
643
			$sql = 'DELETE FROM ' . $this->table_votes . '
644
				WHERE idea_id = ' . (int) $idea['idea_id'] . '
645
					AND user_id = ' . (int) $user_id;
646
			$this->db->sql_query($sql);
647
648
			$idea['idea_votes_' . ($row['vote_value'] == 1 ? 'up' : 'down')]--;
649
650
			$sql_ary = array(
651
				'idea_votes_up'	    => $idea['idea_votes_up'],
652
				'idea_votes_down'	=> $idea['idea_votes_down'],
653
			);
654
655
			$this->update_idea_data($sql_ary, $idea['idea_id'], $this->table_ideas);
656
		}
657
658
		return array(
659
			'message'	    => $this->language->lang('UPDATED_VOTE'),
660
			'votes_up'	    => $idea['idea_votes_up'],
661
			'votes_down'	=> $idea['idea_votes_down'],
662
			'points'        => $this->language->lang('TOTAL_POINTS', $idea['idea_votes_up'] - $idea['idea_votes_down']),
663
			'voters'		=> $this->get_voters($idea['idea_id']),
664
		);
665
	}
666
667
	/**
668
	 * Returns voter info on an idea.
669
	 *
670
	 * @param int $id ID of the idea.
671
	 *
672
	 * @return array Array of row data
673
	 */
674
	public function get_voters($id)
675
	{
676
		$sql = 'SELECT iv.user_id, iv.vote_value, u.username, u.user_colour
677
			FROM ' . $this->table_votes . ' as iv,
678
				' . USERS_TABLE . ' as u
679
			WHERE iv.idea_id = ' . (int) $id . '
680
				AND iv.user_id = u.user_id
681
			ORDER BY u.username ASC';
682
		$result = $this->db->sql_query($sql);
683
		$rows = $this->db->sql_fetchrowset($result);
684
		$this->db->sql_freeresult($result);
685
686
		// Process the username for the template now, so it is
687
		// ready to use in AJAX responses and DOM injections.
688
		foreach ($rows as &$row)
689
		{
690
			$row['user'] = get_username_string('full', $row['user_id'], $row['username'], $row['user_colour'], false, $this->profile_url());
691
		}
692
693
		return $rows;
694
	}
695
696
	/**
697
	 * Get a user's votes from a group of ideas
698
	 *
699
	 * @param int $user_id The user's id
700
	 * @param array $ids An array of idea ids
701
	 * @return array An array of ideas the user voted on and their vote result, or empty otherwise.
702
	 *               example: [idea_id => vote_result]
703
	 *                         1 => 1, idea 1, voted up by the user
704
	 *                         2 => 0, idea 2, voted down by the user
705
	 */
706
	public function get_users_votes($user_id, array $ids)
707
	{
708
		$results = [];
709
		$sql = 'SELECT idea_id, vote_value
710
			FROM ' . $this->table_votes . '
711
			WHERE user_id = ' . (int) $user_id . '
712
			AND ' . $this->db->sql_in_set('idea_id', $ids, false, true);
713
		$result = $this->db->sql_query($sql);
714
		while ($row = $this->db->sql_fetchrow($result))
715
		{
716
			$results[$row['idea_id']] = $row['vote_value'];
717
		}
718
		$this->db->sql_freeresult($result);
719
720
		return $results;
721
	}
722
723
	/**
724
	 * Submits a new idea.
725
	 *
726
	 * @param string $title   The title of the idea.
727
	 * @param string $message The description of the idea.
728
	 * @param int    $user_id The ID of the author.
729
	 *
730
	 * @return array|int Either an array of errors, or the ID of the new idea.
731
	 */
732
	public function submit($title, $message, $user_id)
733
	{
734
		$error = array();
735
		if (utf8_clean_string($title) === '')
736
		{
737
			$error[] = $this->language->lang('TITLE_TOO_SHORT');
738
		}
739
		if (utf8_strlen($title) > self::SUBJECT_LENGTH)
740
		{
741
			$error[] = $this->language->lang('TITLE_TOO_LONG', self::SUBJECT_LENGTH);
742
		}
743
		if (utf8_strlen($message) < $this->config['min_post_chars'])
744
		{
745
			$error[] = $this->language->lang('TOO_FEW_CHARS');
746
		}
747
		if ($this->config['max_post_chars'] != 0 && utf8_strlen($message) > $this->config['max_post_chars'])
748
		{
749
			$error[] = $this->language->lang('TOO_MANY_CHARS');
750
		}
751
752
		if (count($error))
753
		{
754
			return $error;
755
		}
756
757
		// Submit idea
758
		$sql_ary = array(
759
			'idea_title'		=> $title,
760
			'idea_author'		=> $user_id,
761
			'idea_date'			=> time(),
762
			'topic_id'			=> 0,
763
		);
764
765
		$idea_id = $this->insert_idea_data($sql_ary, $this->table_ideas);
766
767
		// Initial vote
768
		$idea = $this->get_idea($idea_id);
769
		$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 768 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...
770
771
		$uid = $bitfield = $options = '';
772
		generate_text_for_storage($message, $uid, $bitfield, $options, true, true, true);
773
774
		$data = array(
775
			'forum_id'			=> (int) $this->config['ideas_forum_id'],
776
			'topic_id'			=> 0,
777
			'icon_id'			=> false,
778
			'poster_id'			=> (int) $this->user->data['user_id'],
779
780
			'enable_bbcode'		=> true,
781
			'enable_smilies'	=> true,
782
			'enable_urls'		=> true,
783
			'enable_sig'		=> true,
784
785
			'message'			=> $message,
786
			'message_md5'		=> md5($message),
787
788
			'bbcode_bitfield'	=> $bitfield,
789
			'bbcode_uid'		=> $uid,
790
791
			'post_edit_locked'	=> 0,
792
			'topic_title'		=> $title,
793
794
			'notify_set'		=> false,
795
			'notify'			=> false,
796
			'post_time'			=> 0,
797
			'forum_name'		=> 'Ideas forum',
798
799
			'enable_indexing'	=> true,
800
801
			'force_approved_state'	=> (!$this->auth->acl_get('f_noapprove', $this->config['ideas_forum_id'])) ? ITEM_UNAPPROVED : true,
802
		);
803
804
		$poll = array();
805
		submit_post('post', $title, $this->user->data['username'], POST_NORMAL, $poll, $data);
806
807
		// Edit topic ID into idea; both should link to each other
808
		$sql_ary = array(
809
			'topic_id' => $data['topic_id'],
810
		);
811
812
		$this->update_idea_data($sql_ary, $idea_id, $this->table_ideas);
813
814
		return $idea_id;
815
	}
816
817
	/**
818
	 * Preview a new idea.
819
	 *
820
	 * @param string $message The description of the idea.
821
	 * @return string The idea parsed for display in preview.
822
	 */
823
	public function preview($message)
824
	{
825
		$uid = $bitfield = $flags = '';
826
		generate_text_for_storage($message, $uid, $bitfield, $flags, true, true, true);
827
		return generate_text_for_display($message, $uid, $bitfield, $flags);
828
	}
829
830
	/**
831
	 * Deletes an idea and the topic to go with it.
832
	 *
833
	 * @param int $id       The ID of the idea to be deleted.
834
	 * @param int $topic_id The ID of the idea topic. Optional, but preferred.
835
	 *
836
	 * @return boolean Whether the idea was deleted or not.
837
	 */
838
	public function delete($id, $topic_id = 0)
839
	{
840
		if (!$topic_id)
841
		{
842
			$idea = $this->get_idea($id);
843
			$topic_id = $idea['topic_id'];
844
		}
845
846
		// Delete topic
847
		delete_posts('topic_id', $topic_id);
848
849
		// Delete idea
850
		$deleted = $this->delete_idea_data($id, $this->table_ideas);
851
852
		// Delete votes
853
		$this->delete_idea_data($id, $this->table_votes);
854
855
		return $deleted;
856
	}
857
858
	/**
859
	 * Delete orphaned ideas. Orphaned ideas may exist after a
860
	 * topic has been deleted or moved to another forum.
861
	 *
862
	 * @return int Number of rows affected
863
	 */
864
	public function delete_orphans()
865
	{
866
		// Find any orphans
867
		$sql = 'SELECT idea_id FROM ' . $this->table_ideas . '
868
 			WHERE topic_id NOT IN (SELECT t.topic_id
869
 			FROM ' . $this->table_topics . ' t
870
 				WHERE t.forum_id = ' . (int) $this->config['ideas_forum_id'] . ')';
871
		$result = $this->db->sql_query($sql);
872
		$rows = $this->db->sql_fetchrowset($result);
873
		$this->db->sql_freeresult($result);
874
875
		if (empty($rows))
876
		{
877
			return 0;
878
		}
879
880
		$this->db->sql_transaction('begin');
881
882
		foreach ($rows as $row)
883
		{
884
			// Delete idea
885
			$this->delete_idea_data($row['idea_id'], $this->table_ideas);
886
887
			// Delete votes
888
			$this->delete_idea_data($row['idea_id'], $this->table_votes);
889
		}
890
891
		$this->db->sql_transaction('commit');
892
893
		return count($rows);
894
	}
895
896
	/**
897
	 * Helper method for inserting new idea data
898
	 *
899
	 * @param array  $data  The array of data to insert
900
	 * @param string $table The name of the table
901
	 *
902
	 * @return int The ID of the inserted row
903
	 */
904
	protected function insert_idea_data(array $data, $table)
905
	{
906
		$sql = 'INSERT INTO ' . $table . '
907
		' . $this->db->sql_build_array('INSERT', $data);
908
		$this->db->sql_query($sql);
909
910
		return (int) $this->db->sql_nextid();
911
	}
912
913
	/**
914
	 * Helper method for updating idea data
915
	 *
916
	 * @param array  $data  The array of data to insert
917
	 * @param int    $id    The ID of the idea
918
	 * @param string $table The name of the table
919
	 *
920
	 * @return void
921
	 */
922
	protected function update_idea_data(array $data, $id, $table)
923
	{
924
		$sql = 'UPDATE ' . $table . '
925
			SET ' . $this->db->sql_build_array('UPDATE', $data) . '
926
			WHERE idea_id = ' . (int) $id;
927
		$this->db->sql_query($sql);
928
	}
929
930
	/**
931
	 * Helper method for deleting idea data
932
	 *
933
	 * @param int    $id    The ID of the idea
934
	 * @param string $table The name of the table
935
	 *
936
	 * @return bool True if idea was deleted, false otherwise
937
	 */
938
	protected function delete_idea_data($id, $table)
939
	{
940
		$sql = 'DELETE FROM ' . $table . '
941
			WHERE idea_id = ' . (int) $id;
942
		$this->db->sql_query($sql);
943
944
		return (bool) $this->db->sql_affectedrows();
945
	}
946
947
	/**
948
	 * Get the stored idea count
949
	 * Note: this should only be called after get_ideas()
950
	 *
951
	 * @return int Count of ideas
952
	 */
953
	public function get_idea_count()
954
	{
955
		return isset($this->idea_count) ? $this->idea_count : 0;
956
	}
957
958
	/**
959
	 * Helper to generate the user profile URL with an
960
	 * absolute URL, which helps avoid problems when
961
	 * used in AJAX requests.
962
	 *
963
	 * @return string User profile URL
964
	 */
965
	protected function profile_url()
966
	{
967
		if (!isset($this->profile_url))
968
		{
969
			$this->profile_url = append_sid(generate_board_url() . "/memberlist.{$this->php_ext}", array('mode' => 'viewprofile'));
970
		}
971
972
		return $this->profile_url;
973
	}
974
}
975