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

ideas::preview()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

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