Completed
Pull Request — master (#124)
by Matt
02:22 queued 59s
created

ideas::ideas_title_livesearch()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

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