├── config ├── tables.yml ├── routing.yml └── services.yml ├── styles └── prosilver │ ├── template │ ├── event │ │ ├── viewtopic_body_poll_before.html │ │ └── navbar_header_profile_list_after.html │ ├── action_bar_top.html │ ├── index_list.html │ ├── index_body.html │ ├── list_body.html │ ├── idea_body.html │ └── ideas.js │ └── theme │ └── ideas.css ├── language └── en │ ├── email │ └── status_notification.txt │ ├── info_acp_ideas.php │ ├── info_ucp_ideas.php │ ├── phpbb_ideas_acp.php │ └── common.php ├── acp ├── ideas_info.php └── ideas_module.php ├── migrations ├── m5_base_url_config.php ├── m8_cron_data.php ├── m12_drop_base_url_config.php ├── m3_acp_data.php ├── m13_set_permissions.php ├── m10_update_idea_schema.php ├── m8_implemented_version.php ├── m4_update_statuses.php ├── m2_initial_data.php ├── m7_drop_old_tables.php ├── m6_migrate_old_tables.php ├── m1_initial_schema.php ├── m11_reparse_old_ideas.php └── m9_remove_idea_bot.php ├── controller ├── livesearch_controller.php ├── post_controller.php ├── index_controller.php ├── list_controller.php ├── base.php ├── admin_controller.php └── idea_controller.php ├── README.md ├── composer.json ├── cron └── prune_orphaned_ideas.php ├── template └── twig │ └── extension │ └── ideas_status_icon.php ├── factory ├── livesearch.php ├── linkhelper.php ├── permission_helper.php ├── base.php ├── ideas.php └── idea.php ├── adm └── style │ └── acp_phpbb_ideas.html ├── ext.php ├── textreparser └── plugins │ └── clean_old_ideas.php ├── notification └── type │ └── status.php ├── event └── listener.php └── license.txt /config/tables.yml: -------------------------------------------------------------------------------- 1 | parameters: 2 | tables.ideas_ideas: '%core.table_prefix%ideas_ideas' 3 | tables.ideas_votes: '%core.table_prefix%ideas_votes' 4 | -------------------------------------------------------------------------------- /styles/prosilver/template/event/viewtopic_body_poll_before.html: -------------------------------------------------------------------------------- 1 | {% if IDEA_ID %} 2 | {% INCLUDECSS '@phpbb_ideas/ideas.css' %} 3 | {% include '@phpbb_ideas/idea_body.html' %} 4 | {% endif %} 5 | -------------------------------------------------------------------------------- /language/en/email/status_notification.txt: -------------------------------------------------------------------------------- 1 | Subject: Idea status - "{IDEA_TITLE}" 2 | 3 | Hello {USERNAME}, 4 | 5 | The status of your Idea topic "{IDEA_TITLE}" at "{SITENAME}" has been updated by {UPDATED_BY} to {STATUS}. 6 | 7 | If you want to view the Idea, click the following link: 8 | {U_VIEW_IDEA} 9 | 10 | {EMAIL_SIG} 11 | -------------------------------------------------------------------------------- /styles/prosilver/template/event/navbar_header_profile_list_after.html: -------------------------------------------------------------------------------- 1 | {% if U_SEARCH_MY_IDEAS %} 2 |
  • 3 | 4 | {{ lang('LIST_EGOSEARCH') }} 5 | 6 |
  • 7 | {% endif %} 8 | -------------------------------------------------------------------------------- /acp/ideas_info.php: -------------------------------------------------------------------------------- 1 | 7 | * @license GNU General Public License, version 2 (GPL-2.0) 8 | * 9 | */ 10 | 11 | namespace phpbb\ideas\acp; 12 | 13 | class ideas_info 14 | { 15 | public function module() 16 | { 17 | return array( 18 | 'filename' => '\phpbb\ideas\acp\ideas_module', 19 | 'title' => 'ACP_PHPBB_IDEAS', 20 | 'modes' => array( 21 | 'settings' => array( 22 | 'title' => 'ACP_PHPBB_IDEAS_SETTINGS', 23 | 'auth' => 'ext_phpbb/ideas && acl_a_board', 24 | 'cat' => array('ACP_PHPBB_IDEAS'), 25 | ), 26 | ), 27 | ); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /language/en/info_acp_ideas.php: -------------------------------------------------------------------------------- 1 | 7 | * @license GNU General Public License, version 2 (GPL-2.0) 8 | * 9 | */ 10 | 11 | if (!defined('IN_PHPBB')) 12 | { 13 | exit; 14 | } 15 | 16 | if (empty($lang) || !is_array($lang)) 17 | { 18 | $lang = array(); 19 | } 20 | 21 | $lang = array_merge($lang, array( 22 | // ACP module 23 | 'ACP_PHPBB_IDEAS' => 'phpBB Ideas', 24 | 'ACP_PHPBB_IDEAS_SETTINGS' => 'Ideas settings', 25 | 26 | // ACP Logs 27 | 'ACP_PHPBB_IDEAS_SETTINGS_LOG' => 'phpBB Ideas settings updated', 28 | 'ACP_PHPBB_IDEAS_FORUM_SETUP_LOG' => 'phpBB Ideas forum setup applied', 29 | )); 30 | -------------------------------------------------------------------------------- /migrations/m5_base_url_config.php: -------------------------------------------------------------------------------- 1 | 7 | * @license GNU General Public License, version 2 (GPL-2.0) 8 | * 9 | */ 10 | 11 | namespace phpbb\ideas\migrations; 12 | 13 | class m5_base_url_config extends \phpbb\db\migration\migration 14 | { 15 | public function effectively_installed() 16 | { 17 | return $this->config->offsetExists('ideas_base_url'); 18 | } 19 | 20 | public static function depends_on() 21 | { 22 | return array('\phpbb\ideas\migrations\m1_initial_schema'); 23 | } 24 | 25 | public function update_data() 26 | { 27 | return array( 28 | array('config.add', array('ideas_base_url', '')), 29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /migrations/m8_cron_data.php: -------------------------------------------------------------------------------- 1 | 7 | * @license GNU General Public License, version 2 (GPL-2.0) 8 | * 9 | */ 10 | 11 | namespace phpbb\ideas\migrations; 12 | 13 | class m8_cron_data extends \phpbb\db\migration\migration 14 | { 15 | public function effectively_installed() 16 | { 17 | return $this->config->offsetExists('ideas_cron_last_run'); 18 | } 19 | 20 | public static function depends_on() 21 | { 22 | return array('\phpbb\ideas\migrations\m1_initial_schema'); 23 | } 24 | 25 | public function update_data() 26 | { 27 | return array( 28 | array('config.add', array('ideas_cron_last_run', 0, true)), 29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /migrations/m12_drop_base_url_config.php: -------------------------------------------------------------------------------- 1 | 7 | * @license GNU General Public License, version 2 (GPL-2.0) 8 | * 9 | */ 10 | 11 | namespace phpbb\ideas\migrations; 12 | 13 | class m12_drop_base_url_config extends \phpbb\db\migration\migration 14 | { 15 | public function effectively_installed() 16 | { 17 | return !$this->config->offsetExists('ideas_base_url'); 18 | } 19 | 20 | public static function depends_on() 21 | { 22 | return [ 23 | '\phpbb\ideas\migrations\m1_initial_schema', 24 | '\phpbb\ideas\migrations\m5_base_url_config', 25 | ]; 26 | } 27 | 28 | public function update_data() 29 | { 30 | return [ 31 | ['config.remove', ['ideas_base_url']], 32 | ]; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /config/routing.yml: -------------------------------------------------------------------------------- 1 | phpbb_ideas_index_controller: 2 | path: /ideas{trailing} 3 | defaults: { _controller: phpbb.ideas.index_controller:index, trailing: '' } 4 | requirements: 5 | trailing: '/?' 6 | 7 | phpbb_ideas_idea_controller: 8 | path: /idea/{idea_id} 9 | defaults: { _controller: phpbb.ideas.idea_controller:idea } 10 | requirements: 11 | idea_id: \d+ 12 | 13 | phpbb_ideas_list_controller: 14 | path: /ideas/list/{sort} 15 | defaults: { _controller: phpbb.ideas.list_controller:ideas_list, sort: new } 16 | 17 | phpbb_ideas_post_controller: 18 | path: /ideas/post 19 | defaults: { _controller: phpbb.ideas.post_controller:post } 20 | 21 | phpbb_ideas_livesearch_controller: 22 | path: /ideas/livesearch/title 23 | defaults: { _controller: phpbb.ideas.livesearch_controller:title_search } 24 | -------------------------------------------------------------------------------- /controller/livesearch_controller.php: -------------------------------------------------------------------------------- 1 | 7 | * @license GNU General Public License, version 2 (GPL-2.0) 8 | * 9 | */ 10 | 11 | namespace phpbb\ideas\controller; 12 | 13 | /** 14 | * Ideas live search controller 15 | */ 16 | class livesearch_controller extends base 17 | { 18 | /* @var \phpbb\ideas\factory\livesearch */ 19 | protected $entity; 20 | 21 | /** 22 | * Title search handler 23 | * 24 | * @return \Symfony\Component\HttpFoundation\JsonResponse 25 | */ 26 | public function title_search() 27 | { 28 | $title_chars = $this->request->variable('duplicateeditinput', '', true); 29 | 30 | $matches = $this->entity->title_search($title_chars, 10); 31 | 32 | return new \Symfony\Component\HttpFoundation\JsonResponse([ 33 | 'keyword' => $title_chars, 34 | 'results' => $matches, 35 | ]); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # phpBB Ideas 2 | 3 | The official Ideas Centre used at [phpBB.com](https://www.phpbb.com/ideas/). This phpBB extension lets phpBB.com community members propose and vote on ideas to improve and enhance phpBB software. 4 | 5 | [![Build Status](https://github.com/phpbb/ideas/actions/workflows/tests.yml/badge.svg)](https://github.com/phpbb/ideas/actions) 6 | [![codecov](https://codecov.io/gh/phpbb/ideas/graph/badge.svg?token=74AITS9CPZ)](https://codecov.io/gh/phpbb/ideas) 7 | 8 | ## Contribute 9 | 10 | We welcome contributions to help make this extension even better. Please fork this repository, install it to a local development copy of phpBB, and send us a pull request with your bug fixes or feature improvements. 11 | 12 | ## Bugs and Support 13 | 14 | You can report bugs and suggest features in the [issue tracker](https://github.com/phpbb/ideas/issues). 15 | 16 | Support is not available for phpBB Ideas because it is not an officially released extension from the phpBB Extensions Team. It is also not recommended for use on any live phpBB forum. 17 | 18 | ## License 19 | [GNU General Public License v2](license.txt) 20 | -------------------------------------------------------------------------------- /migrations/m3_acp_data.php: -------------------------------------------------------------------------------- 1 | 7 | * @license GNU General Public License, version 2 (GPL-2.0) 8 | * 9 | */ 10 | 11 | namespace phpbb\ideas\migrations; 12 | 13 | class m3_acp_data extends \phpbb\db\migration\migration 14 | { 15 | public function effectively_installed() 16 | { 17 | return $this->config->offsetExists('ideas_forum_id') || $this->config->offsetExists('ideas_poster_id'); 18 | } 19 | 20 | public static function depends_on() 21 | { 22 | return array('\phpbb\ideas\migrations\m1_initial_schema'); 23 | } 24 | 25 | public function update_data() 26 | { 27 | return array( 28 | array('module.add', array('acp', 'ACP_CAT_DOT_MODS', 'ACP_PHPBB_IDEAS')), 29 | array('module.add', array( 30 | 'acp', 'ACP_PHPBB_IDEAS', array( 31 | 'module_basename' => '\phpbb\ideas\acp\ideas_module', 32 | 'modes' => array('settings'), 33 | ), 34 | )), 35 | 36 | array('config.add', array('ideas_forum_id', 0)), 37 | array('config.add', array('ideas_poster_id', 0)), 38 | ); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /controller/post_controller.php: -------------------------------------------------------------------------------- 1 | 7 | * @license GNU General Public License, version 2 (GPL-2.0) 8 | * 9 | */ 10 | 11 | namespace phpbb\ideas\controller; 12 | 13 | use phpbb\exception\http_exception; 14 | use Symfony\Component\HttpFoundation\RedirectResponse; 15 | 16 | class post_controller extends base 17 | { 18 | /** 19 | * Controller for /post 20 | * Redirects to the idea forum's posting page. 21 | * 22 | * @throws http_exception 23 | * @return RedirectResponse A Symfony Response object 24 | */ 25 | public function post() 26 | { 27 | if (!$this->is_available()) 28 | { 29 | throw new http_exception(404, 'IDEAS_NOT_AVAILABLE'); 30 | } 31 | 32 | if ($this->user->data['user_id'] == ANONYMOUS) 33 | { 34 | throw new http_exception(404, 'LOGGED_OUT'); 35 | } 36 | 37 | $params = [ 38 | 'mode' => 'post', 39 | 'f' => $this->config['ideas_forum_id'], 40 | ]; 41 | 42 | $url = append_sid(generate_board_url() . "/posting.$this->php_ext", $params, false); 43 | 44 | return new RedirectResponse($url); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "phpbb/ideas", 3 | "type": "phpbb-extension", 4 | "description": "phpBB Ideas centre", 5 | "homepage": "https://www.phpbb.com", 6 | "version": "2.6.2", 7 | "license": "GPL-2.0-only", 8 | "authors": [ 9 | { 10 | "name": "phpBB Website Team", 11 | "email": "website@phpbb.com", 12 | "homepage": "https://www.phpbb.com" 13 | }, 14 | { 15 | "name": "phpBB Extensions Team", 16 | "email": "operations@phpbb.com", 17 | "homepage": "https://www.phpbb.com" 18 | }, 19 | { 20 | "name": "Callum Macrae", 21 | "email": "callum@phpbb.com", 22 | "homepage": "https://macr.ae/", 23 | "role": "Original author" 24 | }, 25 | { 26 | "name": "Matt Friedman", 27 | "homepage": "https://imattpro.github.io", 28 | "role": "Extensions Development Team Lead" 29 | }, 30 | { 31 | "name": "Ruslan Uzdenov", 32 | "homepage": "https://www.phpbbguru.net", 33 | "role": "Extensions Development Team" 34 | } 35 | ], 36 | "require": { 37 | "php": "^7.2 || ^8.0.0", 38 | "composer/installers": "^1.0 || ^2.0" 39 | }, 40 | "extra": { 41 | "display-name": "phpBB Ideas", 42 | "soft-require": { 43 | "phpbb/phpbb": ">=3.3.0,<4.0.0@dev" 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /language/en/info_ucp_ideas.php: -------------------------------------------------------------------------------- 1 | 7 | * @license GNU General Public License, version 2 (GPL-2.0) 8 | * 9 | */ 10 | 11 | if (!defined('IN_PHPBB')) 12 | { 13 | exit; 14 | } 15 | 16 | if (empty($lang) || !is_array($lang)) 17 | { 18 | $lang = []; 19 | } 20 | 21 | // DEVELOPERS PLEASE NOTE 22 | // 23 | // All language files should use UTF-8 as their encoding and the files must not contain a BOM. 24 | // 25 | // Placeholders can now contain order information, e.g. instead of 26 | // 'Page %s of %s' you can (and should) write 'Page %1$s of %2$s', this allows 27 | // translators to re-order the output of data while ensuring it remains correct 28 | // 29 | // You do not need this where single placeholders are used, e.g. 'Message %d' is fine 30 | // equally where a string contains only two placeholders which are used to wrap text 31 | // in a url you again do not need to specify an order e.g., 'Click %sHERE%s' is fine 32 | // 33 | // Some characters you may want to copy&paste: 34 | // ’ » “ ” … 35 | // 36 | 37 | $lang = array_merge($lang, [ 38 | 'NOTIFICATION_TYPE_IDEAS' => 'Your Idea in the Ideas forum has a status change', 39 | ]); 40 | -------------------------------------------------------------------------------- /migrations/m13_set_permissions.php: -------------------------------------------------------------------------------- 1 | 7 | * @license GNU General Public License, version 2 (GPL-2.0) 8 | * 9 | */ 10 | 11 | namespace phpbb\ideas\migrations; 12 | 13 | class m13_set_permissions extends \phpbb\db\migration\migration 14 | { 15 | /** 16 | * {@inheritDoc} 17 | */ 18 | public static function depends_on() 19 | { 20 | return [ 21 | '\phpbb\ideas\migrations\m1_initial_schema', 22 | '\phpbb\ideas\migrations\m3_acp_data', 23 | '\phpbb\ideas\migrations\m12_drop_base_url_config', 24 | ]; 25 | } 26 | 27 | /** 28 | * {@inheritDoc} 29 | */ 30 | public function effectively_installed() 31 | { 32 | return (int) $this->config['ideas_forum_id'] === 0; 33 | } 34 | 35 | /** 36 | * @inheritDoc 37 | */ 38 | public function update_data() 39 | { 40 | return [ 41 | ['custom', [[$this, 'update_permissions']]], 42 | ]; 43 | } 44 | 45 | public function update_permissions() 46 | { 47 | $permission_helper = new \phpbb\ideas\factory\permission_helper($this->db, $this->phpbb_root_path, $this->php_ext); 48 | $permission_helper->set_ideas_forum_permissions($this->config['ideas_forum_id']); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /cron/prune_orphaned_ideas.php: -------------------------------------------------------------------------------- 1 | 7 | * @license GNU General Public License, version 2 (GPL-2.0) 8 | * 9 | */ 10 | 11 | namespace phpbb\ideas\cron; 12 | 13 | /** 14 | * Ideas cron task. 15 | */ 16 | class prune_orphaned_ideas extends \phpbb\cron\task\base 17 | { 18 | /** @var \phpbb\config\config */ 19 | protected $config; 20 | 21 | /** @var \phpbb\ideas\factory\ideas */ 22 | protected $ideas; 23 | 24 | /** 25 | * Constructor 26 | * 27 | * @param \phpbb\config\config $config Config object 28 | * @param \phpbb\ideas\factory\ideas $ideas Ideas factory object 29 | * @access public 30 | */ 31 | public function __construct(\phpbb\config\config $config, \phpbb\ideas\factory\ideas $ideas) 32 | { 33 | $this->config = $config; 34 | $this->ideas = $ideas; 35 | } 36 | 37 | /** 38 | * {@inheritdoc} 39 | */ 40 | public function run() 41 | { 42 | $this->ideas->delete_orphans(); 43 | $this->config->set('ideas_cron_last_run', time(), false); 44 | } 45 | 46 | /** 47 | * {@inheritdoc} 48 | */ 49 | public function should_run() 50 | { 51 | return $this->config['ideas_cron_last_run'] < strtotime('1 week ago'); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /migrations/m10_update_idea_schema.php: -------------------------------------------------------------------------------- 1 | 7 | * @license GNU General Public License, version 2 (GPL-2.0) 8 | * 9 | */ 10 | 11 | namespace phpbb\ideas\migrations; 12 | 13 | class m10_update_idea_schema extends \phpbb\db\migration\migration 14 | { 15 | /** 16 | * {@inheritDoc} 17 | */ 18 | public static function depends_on() 19 | { 20 | return [ 21 | '\phpbb\ideas\migrations\m1_initial_schema', 22 | '\phpbb\ideas\migrations\m6_migrate_old_tables', 23 | '\phpbb\ideas\migrations\m7_drop_old_tables', 24 | '\phpbb\ideas\migrations\m8_implemented_version', 25 | '\phpbb\ideas\migrations\m9_remove_idea_bot', 26 | ]; 27 | } 28 | 29 | /** 30 | * {@inheritDoc} 31 | * 32 | * Convert ideas title column to sortable text (same as topic titles) 33 | * to allow for case-insensitive SQL LIKE searches. 34 | */ 35 | public function update_schema() 36 | { 37 | return [ 38 | 'change_columns' => [ 39 | $this->table_prefix . 'ideas_ideas' => [ 40 | 'idea_title' => ['STEXT_UNI', '', 'true_sort'], 41 | ], 42 | ], 43 | ]; 44 | } 45 | 46 | /** 47 | * {@inheritDoc} 48 | */ 49 | public function revert_schema() 50 | { 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /migrations/m8_implemented_version.php: -------------------------------------------------------------------------------- 1 | 7 | * @license GNU General Public License, version 2 (GPL-2.0) 8 | * 9 | */ 10 | 11 | namespace phpbb\ideas\migrations; 12 | 13 | class m8_implemented_version extends \phpbb\db\migration\migration 14 | { 15 | public function effectively_installed() 16 | { 17 | return $this->db_tools->sql_column_exists($this->table_prefix . 'ideas_ideas', 'implemented_version'); 18 | } 19 | 20 | public static function depends_on() 21 | { 22 | return array( 23 | '\phpbb\ideas\migrations\m1_initial_schema', 24 | '\phpbb\ideas\migrations\m4_update_statuses', 25 | '\phpbb\ideas\migrations\m6_migrate_old_tables', 26 | '\phpbb\ideas\migrations\m7_drop_old_tables', 27 | ); 28 | } 29 | 30 | public function update_schema() 31 | { 32 | return array( 33 | 'add_columns' => array( 34 | $this->table_prefix . 'ideas_ideas' => array( 35 | 'implemented_version' => array('VCHAR', ''), 36 | ), 37 | ), 38 | ); 39 | } 40 | 41 | public function revert_schema() 42 | { 43 | return array( 44 | 'drop_columns' => array( 45 | $this->table_prefix . 'ideas_ideas' => array( 46 | 'implemented_version', 47 | ), 48 | ), 49 | ); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /template/twig/extension/ideas_status_icon.php: -------------------------------------------------------------------------------- 1 | 7 | * @license GNU General Public License, version 2 (GPL-2.0) 8 | * 9 | */ 10 | 11 | namespace phpbb\ideas\template\twig\extension; 12 | 13 | use phpbb\ideas\ext; 14 | 15 | class ideas_status_icon extends \Twig\Extension\AbstractExtension 16 | { 17 | /** 18 | * Get the name of this extension 19 | * 20 | * @return string 21 | */ 22 | public function getName() 23 | { 24 | return 'ideas_status_icon'; 25 | } 26 | 27 | /** 28 | * {@inheritDoc} 29 | */ 30 | public function getFunctions() 31 | { 32 | return [ 33 | new \Twig\TwigFunction('ideas_status_icon', [$this, 'get_status_icon']), 34 | ]; 35 | } 36 | 37 | /** 38 | * Generate a Font Awesome icon class name given an integer input 39 | * representing one of the Ideas Statuses. 40 | * 41 | * @return string Status class name or empty string if no match found. 42 | */ 43 | public function get_status_icon() 44 | { 45 | $args = func_get_args(); 46 | 47 | $icons = [ 48 | ext::$statuses['NEW'] => 'fa-lightbulb-o', 49 | ext::$statuses['IN_PROGRESS'] => 'fa-code-fork', 50 | ext::$statuses['IMPLEMENTED'] => 'fa-check', 51 | ext::$statuses['DUPLICATE'] => 'fa-files-o', 52 | ext::$statuses['INVALID'] => 'fa-ban', 53 | ]; 54 | 55 | return $icons[$args[0]] ?? ''; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /styles/prosilver/template/action_bar_top.html: -------------------------------------------------------------------------------- 1 |
    2 | {% if not S_IS_BOT %} 3 | 4 | {{ lang('NEW_IDEA') }} 5 | 6 | {% endif %} 7 | 8 | {% if S_DISPLAY_SEARCHBOX %} 9 | 23 | {% endif %} 24 | 25 | {% if IDEAS_COUNT %} 26 | 34 | {% endif %} 35 |
    36 | -------------------------------------------------------------------------------- /language/en/phpbb_ideas_acp.php: -------------------------------------------------------------------------------- 1 | 7 | * @license GNU General Public License, version 2 (GPL-2.0) 8 | * 9 | */ 10 | 11 | if (!defined('IN_PHPBB')) 12 | { 13 | exit; 14 | } 15 | 16 | if (empty($lang) || !is_array($lang)) 17 | { 18 | $lang = array(); 19 | } 20 | 21 | $lang = array_merge($lang, array( 22 | // ACP module 23 | 'ACP_IDEAS_FORUM_ID' => 'Ideas forum', 24 | 'ACP_IDEAS_FORUM_ID_EXPLAIN' => 'Select the forum that Ideas topics will be posted to. Once done, apply Ideas forum setup clicking the appropriate button below.', 25 | 'ACP_IDEAS_FORUM_SETUP' => 'Set up the Ideas forum', 26 | 'ACP_IDEAS_FORUM_SETUP_CONFIRM' => 'Are you sure you wish to set up the Ideas forum?', 27 | 'ACP_IDEAS_FORUM_SETUP_EXPLAIN' => 'Sets up the Ideas forum. Many permissions will be pre-configured. Additionally, auto-pruning will be disabled. Note: you have to set the Ideas forum first.', 28 | 'ACP_IDEAS_FORUM_SETUP_UPDATED' => 'phpBB Ideas forum settings successfully updated.', 29 | 'ACP_IDEAS_NO_FORUM' => 'No forum selected', 30 | 'ACP_IDEAS_SETTINGS_UPDATED' => 'phpBB Ideas settings updated.', 31 | 'ACP_IDEAS_UTILITIES' => 'Ideas utilities', 32 | 'ACP_PHPBB_IDEAS_EXPLAIN' => 'Here you can configure phpBB Ideas extension. phpBB Ideas is an ideas centre for phpBB. It allows users to suggest and vote on ideas that would help to improve and enhance phpBB.', 33 | )); 34 | -------------------------------------------------------------------------------- /factory/livesearch.php: -------------------------------------------------------------------------------- 1 | 7 | * @license GNU General Public License, version 2 (GPL-2.0) 8 | * 9 | */ 10 | 11 | namespace phpbb\ideas\factory; 12 | 13 | /** 14 | * Class for handling multiple ideas 15 | */ 16 | class livesearch extends base 17 | { 18 | /** @var array */ 19 | protected $sql; 20 | 21 | /** 22 | * Do a live search on idea titles. Return any matches based on a given search query. 23 | * 24 | * @param string $search The string of characters to search using LIKE 25 | * @param int $limit The number of results to return 26 | * 27 | * @return array An array of matching idea id/key and title/values 28 | */ 29 | public function title_search($search, $limit = 10) 30 | { 31 | $results = []; 32 | $sql = 'SELECT idea_title, idea_id 33 | FROM ' . $this->table_ideas . ' 34 | WHERE idea_title ' . $this->db->sql_like_expression($this->db->get_any_char() . $search . $this->db->get_any_char()); 35 | $result = $this->db->sql_query_limit($sql, $limit); 36 | while ($row = $this->db->sql_fetchrow($result)) 37 | { 38 | $results[] = [ 39 | 'idea_id' => $row['idea_id'], 40 | 'result' => $row['idea_id'], 41 | 'clean_title' => $row['idea_title'], 42 | 'display' => "{$row['idea_title']}", // spans are expected in phpBB's live search JS 43 | ]; 44 | } 45 | $this->db->sql_freeresult($result); 46 | 47 | return $results; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /adm/style/acp_phpbb_ideas.html: -------------------------------------------------------------------------------- 1 | {% include 'overall_header.html' %} 2 | 3 | 4 | 5 |

    {{ lang('ACP_PHPBB_IDEAS') }}

    6 | 7 |

    {{ lang('ACP_PHPBB_IDEAS_EXPLAIN') }}

    8 | 9 | {% if S_ERROR %} 10 |
    11 |

    {{ lang('WARNING') }}

    12 |

    {{ ERROR_MSG }}

    13 |
    14 | {% endif %} 15 | 16 |
    17 |
    18 | {{ lang('ACP_PHPBB_IDEAS_SETTINGS') }} 19 |
    20 |

    {{ lang('ACP_IDEAS_FORUM_ID_EXPLAIN') }}
    21 |
    {{ S_FORUM_SELECT_BOX }}
    22 |
    23 | 24 |
    25 |   26 | 27 | {{ S_FORM_TOKEN }} 28 |
    29 |
    30 |
    31 | 32 |
    33 |
    34 | {{ lang('ACP_IDEAS_UTILITIES') }} 35 |
    36 |

    {{ lang('ACP_IDEAS_FORUM_SETUP_EXPLAIN') }}
    37 |
    38 | 39 |
    40 |
    41 |
    42 |
    43 | 44 | {% include 'overall_footer.html' %} 45 | -------------------------------------------------------------------------------- /migrations/m4_update_statuses.php: -------------------------------------------------------------------------------- 1 | 7 | * @license GNU General Public License, version 2 (GPL-2.0) 8 | * 9 | */ 10 | 11 | namespace phpbb\ideas\migrations; 12 | 13 | class m4_update_statuses extends \phpbb\db\migration\migration 14 | { 15 | public function effectively_installed() 16 | { 17 | $sql = 'SELECT status_id 18 | FROM ' . $this->table_prefix . "ideas_statuses 19 | WHERE status_name='New'"; 20 | $result = $this->db->sql_query($sql); 21 | $row = $this->db->sql_fetchrow($result); 22 | $this->db->sql_freeresult($result); 23 | 24 | return $row === false; 25 | } 26 | 27 | public static function depends_on() 28 | { 29 | return array( 30 | '\phpbb\ideas\migrations\m1_initial_schema', 31 | '\phpbb\ideas\migrations\m2_initial_data', 32 | ); 33 | } 34 | 35 | public function update_data() 36 | { 37 | return array( 38 | array('custom', array(array($this, 'update_statuses'))), 39 | ); 40 | } 41 | 42 | public function update_statuses() 43 | { 44 | $status_updates = array( 45 | 'New' => 'NEW', 46 | 'In Progress' => 'IN_PROGRESS', 47 | 'Implemented' => 'IMPLEMENTED', 48 | 'Duplicate' => 'DUPLICATE', 49 | 'Invalid' => 'INVALID', 50 | ); 51 | 52 | $this->db->sql_transaction('begin'); 53 | 54 | foreach ($status_updates as $old => $new) 55 | { 56 | $sql = 'UPDATE ' . $this->table_prefix . "ideas_statuses 57 | SET status_name='" . $this->db->sql_escape($new) . "' 58 | WHERE status_name='" . $this->db->sql_escape($old) . "'"; 59 | $this->db->sql_query($sql); 60 | } 61 | 62 | $this->db->sql_transaction('commit'); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /migrations/m2_initial_data.php: -------------------------------------------------------------------------------- 1 | 7 | * @license GNU General Public License, version 2 (GPL-2.0) 8 | * 9 | */ 10 | 11 | namespace phpbb\ideas\migrations; 12 | 13 | class m2_initial_data extends \phpbb\db\migration\migration 14 | { 15 | public function effectively_installed() 16 | { 17 | $sql = 'SELECT * FROM ' . $this->table_prefix . 'ideas_statuses'; 18 | $result = $this->db->sql_query_limit($sql, 1); 19 | $row = $this->db->sql_fetchrow($result); 20 | $this->db->sql_freeresult($result); 21 | 22 | return $row !== false; 23 | } 24 | 25 | public static function depends_on() 26 | { 27 | return array('\phpbb\ideas\migrations\m1_initial_schema'); 28 | } 29 | 30 | public function update_data() 31 | { 32 | return array( 33 | array('custom', array(array($this, 'statuses_data'))), 34 | ); 35 | } 36 | 37 | public function statuses_data() 38 | { 39 | $statuses_data = array( 40 | array( 41 | 'status_id' => 1, 42 | 'status_name' => 'NEW', 43 | ), 44 | array( 45 | 'status_id' => 2, 46 | 'status_name' => 'IN_PROGRESS', 47 | ), 48 | array( 49 | 'status_id' => 3, 50 | 'status_name' => 'IMPLEMENTED', 51 | ), 52 | array( 53 | 'status_id' => 4, 54 | 'status_name' => 'DUPLICATE', 55 | ), 56 | array( 57 | 'status_id' => 5, 58 | 'status_name' => 'INVALID', 59 | ), 60 | ); 61 | 62 | $insert_buffer = new \phpbb\db\sql_insert_buffer($this->db, $this->table_prefix . 'ideas_statuses'); 63 | 64 | foreach ($statuses_data as $row) 65 | { 66 | $insert_buffer->insert($row); 67 | } 68 | 69 | $insert_buffer->flush(); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /styles/prosilver/template/index_list.html: -------------------------------------------------------------------------------- 1 | 39 | -------------------------------------------------------------------------------- /acp/ideas_module.php: -------------------------------------------------------------------------------- 1 | 7 | * @license GNU General Public License, version 2 (GPL-2.0) 8 | * 9 | */ 10 | 11 | namespace phpbb\ideas\acp; 12 | 13 | class ideas_module 14 | { 15 | /** @var string */ 16 | public $page_title; 17 | 18 | /** @var string */ 19 | public $tpl_name; 20 | 21 | /** @var string */ 22 | public $u_action; 23 | 24 | /** 25 | * Main ACP module 26 | * 27 | * @access public 28 | * @throws \Exception 29 | */ 30 | public function main() 31 | { 32 | global $phpbb_container; 33 | 34 | // Load a template from adm/style for our ACP page 35 | $this->tpl_name = 'acp_phpbb_ideas'; 36 | 37 | // Set the page title for our ACP page 38 | $this->page_title = 'ACP_PHPBB_IDEAS_SETTINGS'; 39 | 40 | $language = $phpbb_container->get('language'); 41 | $request = $phpbb_container->get('request'); 42 | 43 | // Get an instance of the admin controller 44 | /** @var \phpbb\ideas\controller\admin_controller $admin_controller */ 45 | $admin_controller = $phpbb_container->get('phpbb.ideas.admin.controller'); 46 | 47 | // Add the phpBB Ideas ACP lang file 48 | $language->add_lang('phpbb_ideas_acp', 'phpbb/ideas'); 49 | 50 | // Make the $u_action url available in the admin controller 51 | $admin_controller->set_page_url($this->u_action); 52 | 53 | // Create a form key for preventing CSRF attacks 54 | add_form_key('acp_phpbb_ideas_settings'); 55 | 56 | // Apply Ideas configuration settings 57 | if ($request->is_set_post('submit')) 58 | { 59 | $admin_controller->set_config_options(); 60 | } 61 | 62 | // Set Ideas forum options and registered user group forum permissions 63 | if ($request->is_set_post('ideas_forum_setup')) 64 | { 65 | $admin_controller->set_ideas_forum_options(); 66 | } 67 | 68 | // Display/set ACP configuration settings 69 | $admin_controller->display_options(); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /factory/linkhelper.php: -------------------------------------------------------------------------------- 1 | 7 | * @license GNU General Public License, version 2 (GPL-2.0) 8 | * 9 | */ 10 | 11 | namespace phpbb\ideas\factory; 12 | 13 | use phpbb\controller\helper; 14 | use phpbb\user_loader; 15 | 16 | /** 17 | * Class for helping with common links 18 | */ 19 | class linkhelper 20 | { 21 | /* @var helper */ 22 | protected $helper; 23 | 24 | /* @var user_loader */ 25 | protected $user_loader; 26 | 27 | /** 28 | * @param helper $helper 29 | * @param user_loader $user_loader 30 | */ 31 | public function __construct(helper $helper, user_loader $user_loader) 32 | { 33 | $this->helper = $helper; 34 | $this->user_loader = $user_loader; 35 | } 36 | 37 | /** 38 | * Shortcut method to get the link to a specified idea. 39 | * Optionally add mode and hash URL arguments. 40 | * 41 | * @param int $idea_id The ID of the idea 42 | * @param string $mode The mode argument (vote, delete, etc.) 43 | * @param bool $hash Add a link hash 44 | * @return string The route 45 | */ 46 | public function get_idea_link($idea_id, $mode = '', $hash = false) 47 | { 48 | $params = array('idea_id' => $idea_id); 49 | $params = $mode ? array_merge($params, array('mode' => $mode)) : $params; 50 | $params = $hash ? array_merge($params, array('hash' => generate_link_hash("{$mode}_$idea_id"))) : $params; 51 | 52 | return $this->helper->route('phpbb_ideas_idea_controller', $params); 53 | } 54 | 55 | /** 56 | * Returns a link to the users profile, complete with colour. 57 | * 58 | * Is there a function that already does this? This seems fairly database heavy. 59 | * 60 | * @param int $id The ID of the user 61 | * @return string An HTML link to the users profile 62 | */ 63 | public function get_user_link($id) 64 | { 65 | $this->user_loader->load_users(array($id)); 66 | return $this->user_loader->get_username($id, 'full'); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /styles/prosilver/template/index_body.html: -------------------------------------------------------------------------------- 1 | {% macro ideas_section(title, ideas, view_url, icon_class, view_text) %} 2 |
    3 |

    {{ title }}

    4 | {% if ideas %} 5 | {{ view_text }} 6 | {% endif %} 7 |
    8 |
    9 |
    10 | 18 | {% include '@phpbb_ideas/index_list.html' with {ideas: ideas} %} 19 |
    20 |
    21 | {% endmacro %} 22 | 23 | {% INCLUDECSS '@phpbb_ideas/ideas.css' %} 24 | 25 | {% include 'overall_header.html' %} 26 | 27 |

    {{ lang('IDEAS_TITLE') }}

    28 | 29 | {% include '@phpbb_ideas/action_bar_top.html' %} 30 | 31 | {# TOP IDEAS #} 32 | {{ _self.ideas_section(lang('TOP_IDEAS'), top_ideas, U_VIEW_TOP, 'fa-line-chart', lang('VIEW_TOP')) }} 33 | 34 | {# LATEST IDEAS #} 35 | {{ _self.ideas_section(lang('LATEST_IDEAS'), latest_ideas, U_VIEW_LATEST, 'fa-lightbulb-o', lang('VIEW_LATEST')) }} 36 | 37 | {# IMPLEMENTED IDEAS #} 38 | {{ _self.ideas_section(lang('IMPLEMENTED_IDEAS'), implemented_ideas, U_VIEW_IMPLEMENTED, 'fa-code-fork fa-flip-vertical', lang('VIEW_IMPLEMENTED')) }} 39 | 40 | {# IN PROGRESS IDEAS #} 41 | {{ _self.ideas_section(lang('IN_PROGRESS_IDEAS'), in_progress_ideas, U_VIEW_IN_PROGRESS, 'fa-clock-o', lang('VIEW_IN_PROGRESS')) }} 42 | 43 |
    44 |

    {{ lang('STATISTICS') }}

    45 |

    46 | {{ lang('TOTAL_POSTED_IDEAS') }} {{ STATISTICS.total }} • {{ lang('LIST_IMPLEMENTED') }} {{ STATISTICS.implemented }} • {{ lang('LIST_IN_PROGRESS') }} {{ STATISTICS.in_progress }} 47 |

    48 |
    49 | 50 | {% include 'overall_footer.html' %} 51 | -------------------------------------------------------------------------------- /styles/prosilver/template/list_body.html: -------------------------------------------------------------------------------- 1 | {% INCLUDECSS '@phpbb_ideas/ideas.css' %} 2 | 3 | {% include 'overall_header.html' %} 4 | 5 |

    {{ lang('IDEAS_TITLE') }} {{ STATUS_NAME }}

    6 | 7 | {% include '@phpbb_ideas/action_bar_top.html' %} 8 | 9 |
    10 |
    11 | 19 | {% include '@phpbb_ideas/index_list.html' %} 20 |
    21 |
    22 | 23 |
    24 |
    25 | {% if not S_IS_BOT %} 26 | 34 | 41 | 47 | 48 | {% endif %} 49 |
    50 |
    51 |
    52 | 53 |
    54 | 62 |
    63 | 64 |
    65 | 66 | {% include 'overall_footer.html' %} 67 | -------------------------------------------------------------------------------- /migrations/m7_drop_old_tables.php: -------------------------------------------------------------------------------- 1 | 7 | * @license GNU General Public License, version 2 (GPL-2.0) 8 | * 9 | */ 10 | 11 | namespace phpbb\ideas\migrations; 12 | 13 | class m7_drop_old_tables extends \phpbb\db\migration\migration 14 | { 15 | public function effectively_installed() 16 | { 17 | return !$this->db_tools->sql_table_exists($this->table_prefix . 'ideas_statuses'); 18 | } 19 | 20 | public static function depends_on() 21 | { 22 | return array( 23 | '\phpbb\ideas\migrations\m1_initial_schema', 24 | '\phpbb\ideas\migrations\m6_migrate_old_tables', 25 | ); 26 | } 27 | 28 | public function update_schema() 29 | { 30 | return array( 31 | 'drop_tables' => array( 32 | $this->table_prefix . 'ideas_statuses', 33 | $this->table_prefix . 'ideas_tickets', 34 | $this->table_prefix . 'ideas_rfcs', 35 | $this->table_prefix . 'ideas_duplicates', 36 | ), 37 | ); 38 | } 39 | 40 | public function revert_schema() 41 | { 42 | return array( 43 | 'add_tables' => array( 44 | $this->table_prefix . 'ideas_statuses' => array( 45 | 'COLUMNS' => array( 46 | 'status_id' => array('UINT', 0), 47 | 'status_name' => array('VCHAR', ''), 48 | ), 49 | 'PRIMARY_KEY' => 'status_id', 50 | ), 51 | $this->table_prefix . 'ideas_tickets' => array( 52 | 'COLUMNS' => array( 53 | 'idea_id' => array('UINT', 0), 54 | 'ticket_id' => array('UINT', 0), 55 | ), 56 | 'KEYS' => array( 57 | 'ticket_key' => array('INDEX', array('idea_id', 'ticket_id')), 58 | ), 59 | ), 60 | $this->table_prefix . 'ideas_rfcs' => array( 61 | 'COLUMNS' => array( 62 | 'idea_id' => array('UINT', 0), 63 | 'rfc_link' => array('VCHAR', ''), 64 | ), 65 | 'KEYS' => array( 66 | 'rfc_key' => array('INDEX', array('idea_id', 'rfc_link')), 67 | ), 68 | ), 69 | $this->table_prefix . 'ideas_duplicates' => array( 70 | 'COLUMNS' => array( 71 | 'idea_id' => array('UINT', 0), 72 | 'duplicate_id' => array('UINT', 0), 73 | ), 74 | 'KEYS' => array( 75 | 'dupe_key' => array('INDEX', array('idea_id', 'duplicate_id')), 76 | ), 77 | ), 78 | ), 79 | ); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /factory/permission_helper.php: -------------------------------------------------------------------------------- 1 | 7 | * @license GNU General Public License, version 2 (GPL-2.0) 8 | * 9 | */ 10 | 11 | namespace phpbb\ideas\factory; 12 | 13 | class permission_helper 14 | { 15 | /** 16 | * @var \phpbb\db\driver\driver_interface 17 | */ 18 | protected $db; 19 | 20 | /** 21 | * @var string 22 | */ 23 | protected $phpbb_root_path; 24 | 25 | /** 26 | * @var string 27 | */ 28 | protected $php_ext; 29 | 30 | /** 31 | * Constructor 32 | * 33 | * @param \phpbb\db\driver\driver_interface $db Database object 34 | * @param string $phpbb_root_path phpBB root path 35 | * @param string $php_ext php_ext 36 | * @access public 37 | */ 38 | public function __construct(\phpbb\db\driver\driver_interface $db, $phpbb_root_path, $php_ext) 39 | { 40 | $this->db = $db; 41 | $this->phpbb_root_path = $phpbb_root_path; 42 | $this->php_ext = $php_ext; 43 | } 44 | 45 | /** 46 | * Set the best permissions for an Ideas forum. 47 | * 48 | * @param int $forum_id A forum id 49 | */ 50 | public function set_ideas_forum_permissions($forum_id) 51 | { 52 | if (!class_exists('auth_admin')) 53 | { 54 | include $this->phpbb_root_path . 'includes/acp/auth.' . $this->php_ext; 55 | } 56 | $auth_admin = new \auth_admin(); 57 | 58 | // Get the REGISTERED usergroup ID 59 | $sql = 'SELECT group_id 60 | FROM ' . GROUPS_TABLE . " 61 | WHERE group_name = '" . $this->db->sql_escape('REGISTERED') . "'"; 62 | $result = $this->db->sql_query($sql); 63 | $group_id = (int) $this->db->sql_fetchfield('group_id'); 64 | $this->db->sql_freeresult($result); 65 | 66 | // Get 'f_' local REGISTERED users group permissions array for the ideas forum 67 | // Default undefined permissions to ACL_NO 68 | $hold_ary = $auth_admin->get_mask('set', false, $group_id, $forum_id, 'f_', 'local', ACL_NO); 69 | $auth_settings = $hold_ary[$group_id][$forum_id]; 70 | 71 | // Set 'Can start new topics' permissions to 'Yes' for the ideas forum 72 | $auth_settings['f_post'] = ACL_YES; 73 | 74 | // Can not post announcement or stickies, polls, use topic icons or lock own topic 75 | $auth_settings['f_announce'] = ACL_NEVER; 76 | $auth_settings['f_announce_global'] = ACL_NEVER; 77 | $auth_settings['f_sticky'] = ACL_NEVER; 78 | $auth_settings['f_poll'] = ACL_NEVER; 79 | $auth_settings['f_icons'] = ACL_NEVER; 80 | $auth_settings['f_user_lock'] = ACL_NEVER; 81 | 82 | // Update the registered usergroup permissions for selected Ideas forum... 83 | $auth_admin->acl_set('group', $forum_id, $group_id, $auth_settings); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /migrations/m6_migrate_old_tables.php: -------------------------------------------------------------------------------- 1 | 7 | * @license GNU General Public License, version 2 (GPL-2.0) 8 | * 9 | */ 10 | 11 | namespace phpbb\ideas\migrations; 12 | 13 | class m6_migrate_old_tables extends \phpbb\db\migration\migration 14 | { 15 | public function effectively_installed() 16 | { 17 | return $this->db_tools->sql_column_exists($this->table_prefix . 'ideas_ideas', 'duplicate_id'); 18 | } 19 | 20 | public static function depends_on() 21 | { 22 | return array( 23 | '\phpbb\ideas\migrations\m1_initial_schema', 24 | '\phpbb\ideas\migrations\m4_update_statuses', 25 | ); 26 | } 27 | 28 | public function update_schema() 29 | { 30 | return array( 31 | 'add_columns' => array( 32 | $this->table_prefix . 'ideas_ideas' => array( 33 | 'duplicate_id' => array('UINT', 0), 34 | 'ticket_id' => array('UINT', 0), 35 | 'rfc_link' => array('VCHAR', ''), 36 | ), 37 | ), 38 | ); 39 | } 40 | 41 | public function revert_schema() 42 | { 43 | return array( 44 | 'drop_columns' => array( 45 | $this->table_prefix . 'ideas_ideas' => array( 46 | 'duplicate_id', 47 | 'ticket_id', 48 | 'rfc_link', 49 | ), 50 | ), 51 | ); 52 | } 53 | 54 | public function update_data() 55 | { 56 | return array( 57 | array('custom', array(array($this, 'migrate_tables'))), 58 | ); 59 | } 60 | 61 | public function migrate_tables() 62 | { 63 | $this->move_table_data('ideas_rfcs', 'rfc_link'); 64 | $this->move_table_data('ideas_duplicates', 'duplicate_id'); 65 | $this->move_table_data('ideas_tickets', 'ticket_id'); 66 | } 67 | 68 | /** 69 | * Move data into the ideas table 70 | * 71 | * @param string $table The name of the old table to get data from 72 | * @param string $column The name of the column to get data from 73 | */ 74 | public function move_table_data($table, $column) 75 | { 76 | $data = array(); 77 | 78 | $sql = 'SELECT * 79 | FROM ' . $this->table_prefix . $table; 80 | $result = $this->db->sql_query($sql); 81 | while ($row = $this->db->sql_fetchrow($result)) 82 | { 83 | $data[$row['idea_id']] = $row[$column]; 84 | } 85 | $this->db->sql_freeresult($result); 86 | 87 | $this->db->sql_transaction('begin'); 88 | 89 | if (sizeof($data)) 90 | { 91 | foreach ($data as $idea_id => $value) 92 | { 93 | $sql = 'UPDATE ' . $this->table_prefix . "ideas_ideas 94 | SET $column = '" . $this->db->sql_escape($value) . "' 95 | WHERE idea_id = " . (int) $idea_id; 96 | $this->db->sql_query($sql); 97 | } 98 | } 99 | 100 | $this->db->sql_transaction('commit'); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /controller/index_controller.php: -------------------------------------------------------------------------------- 1 | 7 | * @license GNU General Public License, version 2 (GPL-2.0) 8 | * 9 | */ 10 | 11 | namespace phpbb\ideas\controller; 12 | 13 | use phpbb\exception\http_exception; 14 | use phpbb\ideas\ext; 15 | 16 | class index_controller extends base 17 | { 18 | /* @var \phpbb\ideas\factory\ideas */ 19 | protected $entity; 20 | 21 | /** 22 | * Controller for /ideas 23 | * 24 | * @throws http_exception 25 | * @return \Symfony\Component\HttpFoundation\Response A Symfony Response object 26 | */ 27 | public function index() 28 | { 29 | if (!$this->is_available()) 30 | { 31 | throw new http_exception(404, 'IDEAS_NOT_AVAILABLE'); 32 | } 33 | 34 | // Generate latest ideas 35 | $ideas = $this->entity->get_ideas(ext::NUM_IDEAS, ext::SORT_DATE, 'DESC'); 36 | $this->assign_template_block_vars('latest_ideas', $ideas); 37 | 38 | // Generate top ideas 39 | $ideas = $this->entity->get_ideas(ext::NUM_IDEAS, ext::SORT_TOP, 'DESC'); 40 | $this->assign_template_block_vars('top_ideas', $ideas); 41 | 42 | // Generate recently implemented 43 | $ideas = $this->entity->get_ideas(ext::NUM_IDEAS, ext::SORT_DATE, 'DESC', ext::$statuses['IMPLEMENTED']); 44 | $this->assign_template_block_vars('implemented_ideas', $ideas); 45 | 46 | // Generate in progress 47 | $ideas = $this->entity->get_ideas(ext::NUM_IDEAS, ext::SORT_DATE, 'DESC', ext::$statuses['IN_PROGRESS']); 48 | $this->assign_template_block_vars('in_progress_ideas', $ideas); 49 | 50 | $this->template->assign_vars(array( 51 | 'U_VIEW_TOP' => $this->helper->route('phpbb_ideas_list_controller', ['sort' => ext::SORT_TOP]), 52 | 'U_VIEW_LATEST' => $this->helper->route('phpbb_ideas_list_controller', ['sort' => ext::SORT_NEW]), 53 | 'U_VIEW_IMPLEMENTED'=> $this->helper->route('phpbb_ideas_list_controller', ['sort' => ext::SORT_DATE, 'status' => ext::$statuses['IMPLEMENTED']]), 54 | 'U_VIEW_IN_PROGRESS'=> $this->helper->route('phpbb_ideas_list_controller', ['sort' => ext::SORT_DATE, 'status' => ext::$statuses['IN_PROGRESS']]), 55 | 'U_POST_ACTION' => $this->helper->route('phpbb_ideas_post_controller'), 56 | 'U_MCP' => ($this->auth->acl_get('m_', $this->config['ideas_forum_id'])) ? append_sid("{$this->root_path}mcp.$this->php_ext", "f={$this->config['ideas_forum_id']}&i=main&mode=forum_view", true, $this->user->session_id) : '', 57 | 'STATISTICS' => $this->entity->get_statistics(), 58 | )); 59 | 60 | // Assign breadcrumb template vars 61 | $this->template->assign_block_vars('navlinks', array( 62 | 'U_VIEW_FORUM' => $this->helper->route('phpbb_ideas_index_controller'), 63 | 'FORUM_NAME' => $this->language->lang('IDEAS'), 64 | )); 65 | 66 | // Display common ideas template vars 67 | $this->display_common_vars(); 68 | 69 | return $this->helper->render('@phpbb_ideas/index_body.html', $this->language->lang('IDEAS_TITLE')); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /migrations/m1_initial_schema.php: -------------------------------------------------------------------------------- 1 | 7 | * @license GNU General Public License, version 2 (GPL-2.0) 8 | * 9 | */ 10 | 11 | namespace phpbb\ideas\migrations; 12 | 13 | class m1_initial_schema extends \phpbb\db\migration\migration 14 | { 15 | public function effectively_installed() 16 | { 17 | return $this->db_tools->sql_table_exists($this->table_prefix . 'ideas_ideas'); 18 | } 19 | 20 | public static function depends_on() 21 | { 22 | return array('\phpbb\db\migration\data\v31x\v314'); 23 | } 24 | 25 | public function update_schema() 26 | { 27 | return array( 28 | 'add_tables' => array( 29 | $this->table_prefix . 'ideas_ideas' => array( 30 | 'COLUMNS' => array( 31 | 'idea_id' => array('UINT', null, 'auto_increment'), 32 | 'idea_author' => array('UINT', 0), 33 | 'idea_title' => array('VCHAR', ''), 34 | 'idea_date' => array('TIMESTAMP', 0), 35 | 'idea_votes_up' => array('UINT', 0), 36 | 'idea_votes_down' => array('UINT', 0), 37 | 'idea_status' => array('UINT', 1), 38 | 'topic_id' => array('UINT', 0), 39 | ), 40 | 'PRIMARY_KEY' => 'idea_id', 41 | ), 42 | $this->table_prefix . 'ideas_statuses' => array( 43 | 'COLUMNS' => array( 44 | 'status_id' => array('UINT', 0), 45 | 'status_name' => array('VCHAR', ''), 46 | ), 47 | 'PRIMARY_KEY' => 'status_id', 48 | ), 49 | $this->table_prefix . 'ideas_tickets' => array( 50 | 'COLUMNS' => array( 51 | 'idea_id' => array('UINT', 0), 52 | 'ticket_id' => array('UINT', 0), 53 | ), 54 | 'KEYS' => array( 55 | 'ticket_key' => array('INDEX', array('idea_id', 'ticket_id')), 56 | ), 57 | ), 58 | $this->table_prefix . 'ideas_rfcs' => array( 59 | 'COLUMNS' => array( 60 | 'idea_id' => array('UINT', 0), 61 | 'rfc_link' => array('VCHAR', ''), 62 | ), 63 | 'KEYS' => array( 64 | 'rfc_key' => array('INDEX', array('idea_id', 'rfc_link')), 65 | ), 66 | ), 67 | $this->table_prefix . 'ideas_votes' => array( 68 | 'COLUMNS' => array( 69 | 'idea_id' => array('UINT', 0), 70 | 'user_id' => array('UINT', 0), 71 | 'vote_value' => array('BOOL', 0), 72 | ), 73 | 'KEYS' => array( 74 | 'idea_id' => array('INDEX', array('idea_id', 'user_id')), 75 | ), 76 | ), 77 | $this->table_prefix . 'ideas_duplicates' => array( 78 | 'COLUMNS' => array( 79 | 'idea_id' => array('UINT', 0), 80 | 'duplicate_id' => array('UINT', 0), 81 | ), 82 | 'KEYS' => array( 83 | 'dupe_key' => array('INDEX', array('idea_id', 'duplicate_id')), 84 | ), 85 | ), 86 | ), 87 | ); 88 | } 89 | 90 | public function revert_schema() 91 | { 92 | return array( 93 | 'drop_tables' => array( 94 | $this->table_prefix . 'ideas_ideas', 95 | $this->table_prefix . 'ideas_statuses', 96 | $this->table_prefix . 'ideas_tickets', 97 | $this->table_prefix . 'ideas_rfcs', 98 | $this->table_prefix . 'ideas_votes', 99 | $this->table_prefix . 'ideas_duplicates', 100 | ), 101 | ); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /migrations/m11_reparse_old_ideas.php: -------------------------------------------------------------------------------- 1 | 7 | * @license GNU General Public License, version 2 (GPL-2.0) 8 | * 9 | */ 10 | 11 | namespace phpbb\ideas\migrations; 12 | 13 | class m11_reparse_old_ideas extends \phpbb\db\migration\container_aware_migration 14 | { 15 | /** 16 | * {@inheritDoc} 17 | */ 18 | public static function depends_on() 19 | { 20 | return [ 21 | '\phpbb\ideas\migrations\m1_initial_schema', 22 | '\phpbb\ideas\migrations\m6_migrate_old_tables', 23 | '\phpbb\ideas\migrations\m7_drop_old_tables', 24 | '\phpbb\ideas\migrations\m8_implemented_version', 25 | '\phpbb\ideas\migrations\m10_update_idea_schema', 26 | ]; 27 | } 28 | 29 | /** 30 | * {@inheritDoc} 31 | */ 32 | public function effectively_installed() 33 | { 34 | /** @var \phpbb\textreparser\manager $reparser_manager */ 35 | $reparser_manager = $this->container->get('text_reparser.manager'); 36 | 37 | return !empty($reparser_manager->get_resume_data('phpbb.ideas.text_reparser.clean_old_ideas') || !$this->bbcode_exists('idea')); 38 | } 39 | 40 | /** 41 | * @inheritDoc 42 | */ 43 | public function update_data() 44 | { 45 | return [ 46 | ['custom', [[$this, 'reparse']]], 47 | ]; 48 | } 49 | 50 | /** 51 | * Run the clean old ideas reparser 52 | * 53 | * @param int $current An idea identifier 54 | * @return bool|int An idea identifier or true if finished 55 | */ 56 | public function reparse($current = 0) 57 | { 58 | /** @var \phpbb\textreparser\manager $reparser_manager */ 59 | $reparser_manager = $this->container->get('text_reparser.manager'); 60 | 61 | /** @var \phpbb\textformatter\s9e\utils $text_formatter_utils */ 62 | $text_formatter_utils = $this->container->get('text_formatter.utils'); 63 | 64 | $reparser = new \phpbb\ideas\textreparser\plugins\clean_old_ideas( 65 | $this->db, 66 | $text_formatter_utils, 67 | $this->container->getParameter('tables.posts'), 68 | $this->container->getParameter('tables.topics'), 69 | $this->container->getParameter('core.table_prefix') . 'ideas_ideas' 70 | ); 71 | 72 | if (empty($current)) 73 | { 74 | $current = $reparser->get_max_id(); 75 | } 76 | 77 | $limit = 50; // let's keep the reparsing conservative 78 | $start = max(1, $current + 1 - $limit); 79 | $end = max(1, $current); 80 | 81 | $reparser->reparse_range($start, $end); 82 | 83 | $current = $start - 1; 84 | 85 | if ($current === 0) 86 | { 87 | // Prevent CLI command from running this reparser again 88 | $reparser_manager->update_resume_data('phpbb.ideas.text_reparser.clean_old_ideas', 1, 0, $limit); 89 | 90 | return true; 91 | } 92 | 93 | return $current; 94 | } 95 | 96 | /** 97 | * Check if a bbcode exists 98 | * 99 | * @param string $tag BBCode's tag 100 | * @return bool True if bbcode exists, false if not 101 | */ 102 | public function bbcode_exists($tag) 103 | { 104 | $sql = 'SELECT bbcode_id 105 | FROM ' . $this->table_prefix . "bbcodes 106 | WHERE LOWER(bbcode_tag) = '" . $this->db->sql_escape(strtolower($tag)) . "' 107 | OR LOWER(bbcode_tag) = '" . $this->db->sql_escape(strtolower($tag)) . "='"; 108 | $result = $this->db->sql_query_limit($sql, 1); 109 | $bbcode_id = $this->db->sql_fetchfield('bbcode_id'); 110 | $this->db->sql_freeresult($result); 111 | 112 | return $bbcode_id !== false; 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /ext.php: -------------------------------------------------------------------------------- 1 | 7 | * @license GNU General Public License, version 2 (GPL-2.0) 8 | * 9 | */ 10 | 11 | namespace phpbb\ideas; 12 | 13 | /** 14 | * This ext class is optional and can be omitted if left empty. 15 | * However, you can add special (un)installation commands in the 16 | * methods enable_step(), disable_step() and purge_step(). As it is, 17 | * these methods are defined in \phpbb\extension\base, which this 18 | * class extends, but you can overwrite them to give special 19 | * instructions for those cases. 20 | */ 21 | class ext extends \phpbb\extension\base 22 | { 23 | public const SORT_AUTHOR = 'author'; 24 | public const SORT_DATE = 'date'; 25 | public const SORT_NEW = 'new'; 26 | public const SORT_SCORE = 'score'; 27 | public const SORT_TITLE = 'title'; 28 | public const SORT_TOP = 'top'; 29 | public const SORT_VOTES = 'votes'; 30 | public const SORT_MYIDEAS = 'egosearch'; 31 | public const SUBJECT_LENGTH = 120; 32 | public const NUM_IDEAS = 5; 33 | public const NOTIFICATION_TYPE_STATUS = 'phpbb.ideas.notification.type.status'; 34 | 35 | /** @var array Idea status names and IDs */ 36 | public static $statuses = [ 37 | 'NEW' => 1, 38 | 'IN_PROGRESS' => 2, 39 | 'IMPLEMENTED' => 3, 40 | 'DUPLICATE' => 4, 41 | 'INVALID' => 5, 42 | ]; 43 | 44 | /** @var array Cached flipped statuses array */ 45 | private static $status_names; 46 | 47 | /** 48 | * Return the status name from the status ID. 49 | * 50 | * @param int $id ID of the status. 51 | * @return string The status name. 52 | */ 53 | public static function status_name($id) 54 | { 55 | if (self::$status_names === null) 56 | { 57 | self::$status_names = array_flip(self::$statuses); 58 | } 59 | 60 | return self::$status_names[$id]; 61 | } 62 | 63 | /** 64 | * Check whether the extension can be enabled. 65 | * 66 | * Requires phpBB >= 3.3.0 due to use of PHP 7 features 67 | * Requires PHP >= 7.2.0 68 | * 69 | * @return bool 70 | */ 71 | public function is_enableable() 72 | { 73 | return PHP_VERSION_ID >= 70200 74 | && phpbb_version_compare(PHPBB_VERSION, '3.3.0', '>=') 75 | && phpbb_version_compare(PHPBB_VERSION, '4.0.0-dev', '<'); 76 | } 77 | 78 | /** 79 | * Handle notification management for extension lifecycle 80 | * 81 | * @param string $method The notification manager method to call 82 | * @return string 83 | */ 84 | private function handle_notifications($method) 85 | { 86 | $this->container->get('notification_manager')->$method(self::NOTIFICATION_TYPE_STATUS); 87 | return 'notification'; 88 | } 89 | 90 | /** 91 | * Enable notifications for the extension 92 | * 93 | * @param mixed $old_state 94 | * @return bool|string 95 | */ 96 | public function enable_step($old_state) 97 | { 98 | return $old_state === false ? $this->handle_notifications('enable_notifications') : parent::enable_step($old_state); 99 | } 100 | 101 | /** 102 | * Disable notifications for the extension 103 | * 104 | * @param mixed $old_state 105 | * @return bool|string 106 | */ 107 | public function disable_step($old_state) 108 | { 109 | return $old_state === false ? $this->handle_notifications('disable_notifications') : parent::disable_step($old_state); 110 | } 111 | 112 | /** 113 | * Purge notifications for the extension 114 | * 115 | * @param mixed $old_state 116 | * @return bool|string 117 | */ 118 | public function purge_step($old_state) 119 | { 120 | return $old_state === false ? $this->handle_notifications('purge_notifications') : parent::purge_step($old_state); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /factory/base.php: -------------------------------------------------------------------------------- 1 | 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\language\language; 17 | use phpbb\notification\manager as notification_manager; 18 | use phpbb\user; 19 | 20 | /** 21 | * Base ideas class 22 | */ 23 | class base 24 | { 25 | /** @var auth */ 26 | protected $auth; 27 | 28 | /* @var config */ 29 | protected $config; 30 | 31 | /* @var driver_interface */ 32 | protected $db; 33 | 34 | /** @var language */ 35 | protected $language; 36 | 37 | /** @var notification_manager */ 38 | protected $notification_manager; 39 | 40 | /* @var user */ 41 | protected $user; 42 | 43 | /** @var string */ 44 | protected $table_ideas; 45 | 46 | /** @var string */ 47 | protected $table_votes; 48 | 49 | /** @var string */ 50 | protected $table_topics; 51 | 52 | /** @var string */ 53 | protected $php_ext; 54 | 55 | /** 56 | * Constructor 57 | * 58 | * @param auth $auth 59 | * @param config $config 60 | * @param driver_interface $db 61 | * @param language $language 62 | * @param notification_manager $notification_manager 63 | * @param user $user 64 | * @param string $table_ideas 65 | * @param string $table_votes 66 | * @param string $table_topics 67 | * @param string $phpEx 68 | */ 69 | public function __construct(auth $auth, config $config, driver_interface $db, language $language, notification_manager $notification_manager, user $user, $table_ideas, $table_votes, $table_topics, $phpEx) 70 | { 71 | $this->auth = $auth; 72 | $this->config = $config; 73 | $this->db = $db; 74 | $this->language = $language; 75 | $this->notification_manager = $notification_manager; 76 | $this->user = $user; 77 | 78 | $this->php_ext = $phpEx; 79 | 80 | $this->table_ideas = $table_ideas; 81 | $this->table_votes = $table_votes; 82 | $this->table_topics = $table_topics; 83 | } 84 | 85 | /** 86 | * Helper method for inserting new idea data 87 | * 88 | * @param array $data The array of data to insert 89 | * @param string $table The name of the table 90 | * 91 | * @return int The ID of the inserted row 92 | */ 93 | protected function insert_idea_data(array $data, $table) 94 | { 95 | $sql = 'INSERT INTO ' . $table . ' 96 | ' . $this->db->sql_build_array('INSERT', $data); 97 | $this->db->sql_query($sql); 98 | 99 | return (int) $this->db->sql_nextid(); 100 | } 101 | 102 | /** 103 | * Helper method for updating idea data 104 | * 105 | * @param array $data The array of data to insert 106 | * @param int $id The ID of the idea 107 | * @param string $table The name of the table 108 | * 109 | * @return void 110 | */ 111 | protected function update_idea_data(array $data, $id, $table) 112 | { 113 | $sql = 'UPDATE ' . $table . ' 114 | SET ' . $this->db->sql_build_array('UPDATE', $data) . ' 115 | WHERE idea_id = ' . (int) $id; 116 | $this->db->sql_query($sql); 117 | } 118 | 119 | /** 120 | * Helper method for deleting idea data 121 | * 122 | * @param int $id The ID of the idea 123 | * @param string $table The name of the table 124 | * 125 | * @return bool True if idea was deleted, false otherwise 126 | */ 127 | protected function delete_idea_data($id, $table) 128 | { 129 | $sql = 'DELETE FROM ' . $table . ' 130 | WHERE idea_id = ' . (int) $id; 131 | $this->db->sql_query($sql); 132 | 133 | return (bool) $this->db->sql_affectedrows(); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /textreparser/plugins/clean_old_ideas.php: -------------------------------------------------------------------------------- 1 | 7 | * @license GNU General Public License, version 2 (GPL-2.0) 8 | * 9 | */ 10 | 11 | namespace phpbb\ideas\textreparser\plugins; 12 | 13 | class clean_old_ideas extends \phpbb\textreparser\row_based_plugin 14 | { 15 | /** @var \phpbb\textformatter\s9e\utils */ 16 | protected $text_formatter_utils; 17 | 18 | /** @var string */ 19 | protected $ideas_table; 20 | 21 | /** @var string */ 22 | protected $topics_table; 23 | 24 | /** 25 | * Constructor 26 | * 27 | * @param \phpbb\db\driver\driver_interface $db Database connection 28 | * @param \phpbb\textformatter\s9e\utils $text_formatter_utils Text formatter utilities object 29 | * @param string $table Posts Table 30 | * @param string $topics_table Topics Table 31 | * @param string $ideas_table Ideas Table 32 | */ 33 | public function __construct(\phpbb\db\driver\driver_interface $db, \phpbb\textformatter\s9e\utils $text_formatter_utils, $table, $topics_table, $ideas_table) 34 | { 35 | parent::__construct($db, $table); 36 | 37 | $this->text_formatter_utils = $text_formatter_utils; 38 | $this->ideas_table = $ideas_table; 39 | $this->topics_table = $topics_table; 40 | } 41 | 42 | /** 43 | * {@inheritdoc} 44 | */ 45 | public function get_columns() 46 | { 47 | return [ 48 | 'id' => 'post_id', 49 | 'text' => 'post_text', 50 | ]; 51 | } 52 | 53 | /** 54 | * {@inheritdoc} 55 | */ 56 | public function get_max_id() 57 | { 58 | $sql = 'SELECT MAX(idea_id) AS max_id FROM ' . $this->ideas_table; 59 | $result = $this->db->sql_query($sql); 60 | $max_id = (int) $this->db->sql_fetchfield('max_id'); 61 | $this->db->sql_freeresult($result); 62 | 63 | return $max_id; 64 | } 65 | 66 | /** 67 | * {@inheritdoc} 68 | */ 69 | protected function get_records_by_range_query($min_id, $max_id) 70 | { 71 | $columns = $this->get_columns(); 72 | 73 | $fields = []; 74 | foreach ($columns as $field_name => $column_name) 75 | { 76 | $fields[] = 'p.' . $column_name . ' AS ' . $field_name; 77 | } 78 | 79 | // Query the first post's text for ideas created prior to Sep. 2017 80 | return 'SELECT ' . implode(', ', $fields) . ' 81 | FROM ' . $this->table . ' p 82 | INNER JOIN ' . $this->ideas_table . ' i 83 | ON i.topic_id = p.topic_id 84 | INNER JOIN ' . $this->topics_table . ' t 85 | ON p.' . $columns['id'] . ' = t.topic_first_post_id 86 | WHERE p.post_time < ' . strtotime('September 1, 2017') . ' 87 | AND i.idea_id BETWEEN ' . $min_id . ' AND ' . $max_id; 88 | } 89 | 90 | /** 91 | * {@inheritdoc} 92 | */ 93 | protected function reparse_record(array $record, bool $force_bbcode_reparsing = false) 94 | { 95 | $text = $record['text']; 96 | 97 | // Remove the USER bbcode from the idea post 98 | $text = $this->text_formatter_utils->remove_bbcode($text, 'user'); 99 | 100 | // Remove the IDEA bbcode from the idea post 101 | $text = $this->text_formatter_utils->remove_bbcode($text, 'idea'); 102 | 103 | // Remove old strings from the idea post 104 | $text = str_replace([ 105 | "
    \n
    \n----------", 106 | "
    \n
    \nView idea at: ", 107 | "
    \n
    \nPosted by " 108 | ], ['', '', ''], $text); 109 | 110 | // Save the new text if it has changed, and it's not a dry run 111 | if ($text !== $record['text'] && $this->save_changes) 112 | { 113 | $record['text'] = $text; 114 | $this->save_record($record); 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /migrations/m9_remove_idea_bot.php: -------------------------------------------------------------------------------- 1 | 7 | * @license GNU General Public License, version 2 (GPL-2.0) 8 | * 9 | */ 10 | 11 | namespace phpbb\ideas\migrations; 12 | 13 | class m9_remove_idea_bot extends \phpbb\db\migration\migration 14 | { 15 | public function effectively_installed() 16 | { 17 | return !$this->config->offsetExists('ideas_poster_id'); 18 | } 19 | 20 | public static function depends_on() 21 | { 22 | return [ 23 | '\phpbb\ideas\migrations\m1_initial_schema', 24 | '\phpbb\ideas\migrations\m3_acp_data', 25 | '\phpbb\ideas\migrations\m4_update_statuses', 26 | '\phpbb\ideas\migrations\m6_migrate_old_tables', 27 | '\phpbb\ideas\migrations\m7_drop_old_tables', 28 | ]; 29 | } 30 | 31 | public function update_data() 32 | { 33 | return [ 34 | ['custom', [[$this, 'update_topic_authors']]], 35 | ['config.remove', ['ideas_poster_id']], 36 | ]; 37 | } 38 | 39 | /** 40 | * Replace the Ideas Bot stored in the posts and topics tables with the 41 | * original author's information. Bot gone, order restored to universe. 42 | */ 43 | public function update_topic_authors() 44 | { 45 | // Return if the Ideas Bot does not exist at this point for some reason. 46 | if (!$this->config->offsetExists('ideas_poster_id')) 47 | { 48 | return; 49 | } 50 | 51 | // Get real author info for ideas that were posted by the Ideas Bot 52 | $topics = []; 53 | $sql_array = [ 54 | 'SELECT' => 'i.topic_id, i.idea_author, u.username, u.user_colour, t.topic_first_post_id', 55 | 'FROM' => [ 56 | $this->table_prefix . 'ideas_ideas' => 'i', 57 | ], 58 | 'LEFT_JOIN' => [ 59 | [ 60 | 'FROM' => [$this->table_prefix . 'topics' => 't'], 61 | 'ON' => 't.topic_id = i.topic_id', 62 | ], 63 | [ 64 | 'FROM' => [$this->table_prefix . 'users' => 'u'], 65 | 'ON' => 'u.user_id = i.idea_author', 66 | ], 67 | ], 68 | 'WHERE' => 't.topic_poster = ' . (int) $this->config->offsetGet('ideas_poster_id'), 69 | ]; 70 | 71 | $sql = $this->db->sql_build_query('SELECT', $sql_array); 72 | $result = $this->db->sql_query($sql); 73 | while ($row = $this->db->sql_fetchrow($result)) 74 | { 75 | $topics[$row['topic_id']] = [ 76 | 'topic_poster_id' => $row['idea_author'] ?: ANONYMOUS, 77 | 'topic_poster_name' => $row['username'] ?: '', 78 | 'topic_poster_colour' => $row['user_colour'] ?: '', 79 | 'topic_first_post_id' => $row['topic_first_post_id'] ?: 0, 80 | ]; 81 | } 82 | $this->db->sql_freeresult($result); 83 | 84 | // Begin updating topics and posts 85 | $this->db->sql_transaction('begin'); 86 | foreach ($topics as $topic_id => $data) 87 | { 88 | // Update topic author (first poster) 89 | $sql = 'UPDATE ' . $this->table_prefix . 'topics 90 | SET ' . $this->db->sql_build_array('UPDATE', [ 91 | 'topic_poster' => $data['topic_poster_id'], 92 | 'topic_first_poster_name' => $data['topic_poster_name'], 93 | 'topic_first_poster_colour' => $data['topic_poster_colour'], 94 | ]) . ' 95 | WHERE topic_id = ' . (int) $topic_id; 96 | $this->db->sql_query($sql); 97 | 98 | // Update last poster if it's also the Ideas Bot (i.e: no replies) 99 | $sql = 'UPDATE ' . $this->table_prefix . 'topics 100 | SET ' . $this->db->sql_build_array('UPDATE', [ 101 | 'topic_last_poster_id' => $data['topic_poster_id'], 102 | 'topic_last_poster_name' => $data['topic_poster_name'], 103 | 'topic_last_poster_colour' => $data['topic_poster_colour'], 104 | ]) . ' 105 | WHERE topic_id = ' . (int) $topic_id . ' 106 | AND topic_last_poster_id = ' . (int) $this->config->offsetGet('ideas_poster_id'); 107 | $this->db->sql_query($sql); 108 | 109 | // Update first post's poster id if it's the Ideas Bot 110 | $sql = 'UPDATE ' . $this->table_prefix . 'posts' . ' 111 | SET poster_id = ' . (int) $data['topic_poster_id'] . ' 112 | WHERE post_id = ' . (int) $data['topic_first_post_id'] . ' 113 | AND poster_id = ' . (int) $this->config->offsetGet('ideas_poster_id'); 114 | $this->db->sql_query($sql); 115 | } 116 | $this->db->sql_transaction('commit'); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /config/services.yml: -------------------------------------------------------------------------------- 1 | imports: 2 | - { resource: tables.yml } 3 | 4 | services: 5 | phpbb.ideas.listener: 6 | class: phpbb\ideas\event\listener 7 | arguments: 8 | - '@auth' 9 | - '@config' 10 | - '@controller.helper' 11 | - '@phpbb.ideas.idea' 12 | - '@language' 13 | - '@phpbb.ideas.linkhelper' 14 | - '@template' 15 | - '@user' 16 | - '%core.php_ext%' 17 | tags: 18 | - { name: event.listener } 19 | 20 | # ----- Controllers ----- 21 | phpbb.ideas.admin.controller: 22 | class: phpbb\ideas\controller\admin_controller 23 | arguments: 24 | - '@config' 25 | - '@dbal.conn' 26 | - '@language' 27 | - '@log' 28 | - '@request' 29 | - '@template' 30 | - '@user' 31 | - '%core.root_path%' 32 | - '%core.php_ext%' 33 | 34 | phpbb.ideas.controller.base: 35 | abstract: true 36 | arguments: 37 | - '@auth' 38 | - '@config' 39 | - '@controller.helper' 40 | - '@language' 41 | - '@phpbb.ideas.linkhelper' 42 | - '@pagination' 43 | - '@request' 44 | - '@template' 45 | - '@user' 46 | - '%core.root_path%' 47 | - '%core.php_ext%' 48 | 49 | phpbb.ideas.index_controller: 50 | class: phpbb\ideas\controller\index_controller 51 | parent: phpbb.ideas.controller.base 52 | calls: 53 | - [get_entity, ['@phpbb.ideas.ideas']] 54 | 55 | phpbb.ideas.list_controller: 56 | class: phpbb\ideas\controller\list_controller 57 | parent: phpbb.ideas.controller.base 58 | calls: 59 | - [get_entity, ['@phpbb.ideas.ideas']] 60 | 61 | phpbb.ideas.post_controller: 62 | class: phpbb\ideas\controller\post_controller 63 | parent: phpbb.ideas.controller.base 64 | 65 | phpbb.ideas.idea_controller: 66 | class: phpbb\ideas\controller\idea_controller 67 | parent: phpbb.ideas.controller.base 68 | calls: 69 | - [get_entity, ['@phpbb.ideas.idea']] 70 | 71 | phpbb.ideas.livesearch_controller: 72 | class: phpbb\ideas\controller\livesearch_controller 73 | parent: phpbb.ideas.controller.base 74 | calls: 75 | - [get_entity, ['@phpbb.ideas.livesearch']] 76 | 77 | # ----- Idea Factory Classes ----- 78 | phpbb.ideas.base: 79 | class: phpbb\ideas\factory\ideas 80 | arguments: 81 | - '@auth' 82 | - '@config' 83 | - '@dbal.conn' 84 | - '@language' 85 | - '@notification_manager' 86 | - '@user' 87 | - '%tables.ideas_ideas%' 88 | - '%tables.ideas_votes%' 89 | - '%tables.topics%' 90 | - '%core.php_ext%' 91 | 92 | phpbb.ideas.ideas: 93 | class: phpbb\ideas\factory\ideas 94 | parent: phpbb.ideas.base 95 | 96 | phpbb.ideas.idea: 97 | class: phpbb\ideas\factory\idea 98 | parent: phpbb.ideas.base 99 | 100 | phpbb.ideas.livesearch: 101 | class: phpbb\ideas\factory\livesearch 102 | parent: phpbb.ideas.base 103 | 104 | phpbb.ideas.linkhelper: 105 | class: phpbb\ideas\factory\linkhelper 106 | arguments: 107 | - '@controller.helper' 108 | - '@user_loader' 109 | 110 | # ----- Twig extensions ----- 111 | phpbb.ideas.twig.extension.ideas_status_icon: 112 | class: phpbb\ideas\template\twig\extension\ideas_status_icon 113 | tags: 114 | - { name: twig.extension } 115 | 116 | # ----- Cron tasks ----- 117 | phpbb.ideas.cron.task.prune_orphaned_ideas: 118 | class: phpbb\ideas\cron\prune_orphaned_ideas 119 | arguments: 120 | - '@config' 121 | - '@phpbb.ideas.ideas' 122 | calls: 123 | - [set_name, [cron.task.prune_orphaned_ideas]] 124 | tags: 125 | - { name: cron.task } 126 | 127 | # ----- Notifications ----- 128 | phpbb.ideas.notification.type.status: 129 | class: phpbb\ideas\notification\type\status 130 | parent: notification.type.base 131 | shared: false 132 | calls: 133 | - [set_additional_services, ['@config', '@controller.helper', '@user_loader']] 134 | tags: 135 | - { name: notification.type } 136 | -------------------------------------------------------------------------------- /controller/list_controller.php: -------------------------------------------------------------------------------- 1 | 7 | * @license GNU General Public License, version 2 (GPL-2.0) 8 | * 9 | */ 10 | 11 | namespace phpbb\ideas\controller; 12 | 13 | use phpbb\exception\http_exception; 14 | use phpbb\ideas\ext; 15 | 16 | class list_controller extends base 17 | { 18 | /* @var \phpbb\ideas\factory\ideas */ 19 | protected $entity; 20 | 21 | /** 22 | * Controller for /list/{sort} 23 | * 24 | * @param $sort string The type of list to show (new|top|implemented) 25 | * @throws http_exception 26 | * @return \Symfony\Component\HttpFoundation\Response A Symfony Response object 27 | */ 28 | public function ideas_list($sort) 29 | { 30 | if (!$this->is_available()) 31 | { 32 | throw new http_exception(404, 'IDEAS_NOT_AVAILABLE'); 33 | } 34 | 35 | // Overwrite the $sort parameter if the url contains a sort query. 36 | // This is needed with the sort by options form at the footer of the list. 37 | $sort = $this->request->is_set('sort') ? (string) $this->request->variable('sort', ext::SORT_NEW) : $sort; 38 | 39 | // Get additional query values the url may contain 40 | $sort_direction = $this->request->variable('sd', 'd'); 41 | $status = $this->request->variable('status', 0); 42 | $start = $this->request->variable('start', 0); 43 | 44 | // Store original query params for use in breadcrumbs & pagination 45 | $u_sort = $sort; 46 | $u_status = $status; 47 | $u_sort_direction = $sort_direction; 48 | 49 | // convert the sort direction to ASC or DESC 50 | $sort_direction = ($sort_direction === 'd') ? 'DESC' : 'ASC'; 51 | 52 | // If sort by "new" we really use date 53 | if ($sort === ext::SORT_NEW) 54 | { 55 | $sort = ext::SORT_DATE; 56 | } 57 | 58 | // Set the name for displaying in the template 59 | $status_name = 'LIST_' . ($status > 0 ? ext::status_name($status) : strtoupper($sort)); 60 | $status_name = $this->language->is_set($status_name) ? $this->language->lang($status_name) : ''; 61 | 62 | // For special case where we want to request ALL ideas, 63 | // including the statuses normally hidden from lists. 64 | if ($status === -1) 65 | { 66 | $status = ext::$statuses; 67 | $status_name = $status_name ?: $this->language->lang('ALL_IDEAS'); 68 | } 69 | 70 | // Generate ideas 71 | $ideas = $this->entity->get_ideas($this->config['posts_per_page'], $sort, $sort_direction, $status, $start); 72 | $this->assign_template_block_vars('ideas', $ideas); 73 | 74 | // Build list page template output 75 | $this->template->assign_vars(array( 76 | 'U_LIST_ACTION' => $this->helper->route('phpbb_ideas_list_controller'), 77 | 'U_POST_ACTION' => $this->helper->route('phpbb_ideas_post_controller'), 78 | 'IDEAS_COUNT' => $this->entity->get_idea_count(), 79 | 'STATUS_NAME' => $status_name ?: $this->language->lang('OPEN_IDEAS'), 80 | 'STATUS_ARY' => ext::$statuses, 81 | 'STATUS' => $u_status, 82 | 'SORT_ARY' => array(ext::SORT_AUTHOR, ext::SORT_DATE, ext::SORT_SCORE, ext::SORT_TITLE, ext::SORT_TOP, ext::SORT_VOTES), 83 | 'SORT' => $sort, 84 | 'SORT_DIRECTION' => $sort_direction, 85 | 'U_MCP' => ($this->auth->acl_get('m_', $this->config['ideas_forum_id'])) ? append_sid("{$this->root_path}mcp.$this->php_ext", "f={$this->config['ideas_forum_id']}&i=main&mode=forum_view", true, $this->user->session_id) : '', 86 | 87 | )); 88 | 89 | // Recreate the url parameters for the current list 90 | $params = array( 91 | 'sort' => $u_sort ?: null, 92 | 'status' => $u_status ?: null, 93 | 'sd' => $u_sort_direction ?: null, 94 | ); 95 | 96 | // Assign breadcrumb template vars 97 | $this->template->assign_block_vars_array('navlinks', array( 98 | array( 99 | 'U_VIEW_FORUM' => $this->helper->route('phpbb_ideas_index_controller'), 100 | 'FORUM_NAME' => $this->language->lang('IDEAS'), 101 | ), 102 | array( 103 | 'U_VIEW_FORUM' => $this->helper->route('phpbb_ideas_list_controller', $params), 104 | 'FORUM_NAME' => $status_name ?: $this->language->lang('OPEN_IDEAS'), 105 | ), 106 | )); 107 | 108 | // Generate template pagination 109 | $this->pagination->generate_template_pagination( 110 | $this->helper->route('phpbb_ideas_list_controller', $params), 111 | 'pagination', 112 | 'start', 113 | $this->entity->get_idea_count(), 114 | $this->config['posts_per_page'], 115 | $start 116 | ); 117 | 118 | // Display common ideas template vars 119 | $this->display_common_vars(); 120 | 121 | return $this->helper->render('@phpbb_ideas/list_body.html', $this->language->lang('IDEA_LIST')); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /language/en/common.php: -------------------------------------------------------------------------------- 1 | 7 | * @license GNU General Public License, version 2 (GPL-2.0) 8 | * 9 | */ 10 | 11 | if (!defined('IN_PHPBB')) 12 | { 13 | exit; 14 | } 15 | 16 | if (empty($lang) || !is_array($lang)) 17 | { 18 | $lang = array(); 19 | } 20 | 21 | $lang = array_merge($lang, array( 22 | 'ADD' => 'Add', 23 | 'ALL_IDEAS' => 'All ideas', 24 | 'ALREADY_VOTED' => 'You have already voted on this idea.', 25 | 26 | 'CHANGE_STATUS' => 'Change status', 27 | 'CLICK_TO_VIEW' => 'Click to view votes.', 28 | 'CONFIRM_DELETE' => 'Are you sure you want to delete this idea?', 29 | 30 | 'DATE' => 'Date', 31 | 'DELETE_IDEA' => 'Delete idea', 32 | 'DUPLICATE' => 'Duplicate', 33 | 'DUPLICATE_PLACEHOLDER' => 'Start typing a title', 34 | 35 | 'EDIT' => 'Edit', 36 | 'ENABLE_JS' => 'Please enable JavaScript in your browser to use phpBB Ideas effectively.', 37 | 38 | 'IDEAS' => 'Ideas', 39 | 'IDEA_DELETED' => 'Idea successfully deleted.', 40 | 'IDEA_LIST' => 'Idea List', 41 | 'IDEA_NOT_FOUND' => 'Idea not found', 42 | 'IDEA_STATUS_CHANGE' => 'Idea status changed by %s:', 43 | 'IDEA_STORED_MOD' => 'Your idea has been submitted successfully, but it will need to be approved by a moderator before it is publicly viewable. You will be notified when your idea has been approved.

    Return to Ideas.', 44 | 'IDEAS_TITLE' => 'phpBB Ideas', 45 | 'IDEAS_NOT_AVAILABLE' => 'Ideas is not available at this time.', 46 | 'IMPLEMENTED' => 'Implemented', 47 | 'IMPLEMENTED_ERROR' => 'Must be a valid phpBB version number.', 48 | 'IMPLEMENTED_IDEAS' => 'Recently Implemented Ideas', 49 | 'IMPLEMENTED_VERSION' => 'phpBB version', 50 | 'IN_PROGRESS' => 'In Progress', 51 | 'IN_PROGRESS_IDEAS' => 'Ideas In Progress', 52 | 'INVALID' => 'Invalid', 53 | 'INVALID_IDEA_QUERY' => 'Invalid SQL query. Ideas failed to load.', 54 | 'INVALID_VOTE' => 'Invalid vote; the number you entered was invalid.', 55 | 56 | 'JS_DISABLED' => 'JavaScript is disabled', 57 | 58 | 'LATEST_IDEAS' => 'Latest Ideas', 59 | 'LIST_DUPLICATE' => 'Duplicate ideas', 60 | 'LIST_EGOSEARCH' => 'My Ideas', 61 | 'LIST_IMPLEMENTED' => 'Implemented ideas', 62 | 'LIST_IN_PROGRESS' => 'In Progress ideas', 63 | 'LIST_INVALID' => 'Invalid ideas', 64 | 'LIST_NEW' => 'New ideas', 65 | 'LIST_TOP' => 'Top ideas', 66 | 67 | 'LOGGED_OUT' => 'You must be logged in to do this.', 68 | 69 | 'NEW' => 'New', 70 | 'NEW_IDEA' => 'New Idea', 71 | 'NO_IDEAS_DISPLAY' => 'There are no ideas to display.', 72 | 'NOTIFICATION_STATUS' => 'Status: %s', 73 | 74 | 'OPEN_IDEAS' => 'Open ideas', 75 | 76 | 'POST_IDEA' => 'Post a new idea', 77 | 'POSTING_NEW_IDEA' => 'Posting a new idea', 78 | 79 | 'REMOVE_VOTE' => 'Remove my vote', 80 | 'RETURN_IDEAS' => '%sReturn to Ideas%s', 81 | 'RFC' => 'RFC', 82 | 'RFC_ERROR' => 'RFC must be a topic on Area51.', 83 | 'RFC_LINK_TEXT' => 'View RFC discussion on Area51', 84 | 85 | 'SEARCH_IDEAS' => 'Search ideas...', 86 | 'SCORE' => 'Score', 87 | 'SHOW_W_STATUS' => 'Display ideas with status', 88 | 'STATUS' => 'Status', 89 | 90 | 'TICKET' => 'Ticket', 91 | 'TICKET_ERROR' => 'Ticket ID must be of the format “PHPBB-#####” or “PHPBB3-#####”.', 92 | 'TICKET_ERROR_DUP' => 'You must click on an idea title from the live search results. To delete the duplicate, clear the field and press ENTER. To exit this field press ESC.', 93 | 'TITLE' => 'Title', 94 | 'TOP' => 'Top', 95 | 'TOP_IDEAS' => 'Top Ideas', 96 | 'TOTAL_IDEAS' => [ 97 | 1 => '%d idea', 98 | 2 => '%d ideas', 99 | ], 100 | 'TOTAL_POINTS' => [ 101 | 1 => '%s point.', 102 | 2 => '%s points.', 103 | ], 104 | 'TOTAL_POSTED_IDEAS' => 'Total ideas posted', 105 | 106 | 'UPDATED_VOTE' => 'Successfully updated vote!', 107 | 108 | 'USER_ALREADY_VOTED' => [ 109 | 0 => 'You voted against this idea', 110 | 1 => 'You voted for this idea', 111 | ], 112 | 113 | 'VIEW_IDEA' => 'View Idea', 114 | 'VIEW_IMPLEMENTED' => 'View all implemented ideas', 115 | 'VIEW_IN_PROGRESS' => 'View all in progress ideas', 116 | 'VIEW_LATEST' => 'View all open ideas', 117 | 'VIEW_TOP' => 'View all top voted ideas', 118 | 'VIEWING_IDEAS' => 'Viewing Ideas', 119 | 'VOTE' => 'Vote', 120 | 'VOTE_DOWN' => 'Vote Down', 121 | 'VOTE_ERROR' => 'An error occurred', 122 | 'VOTE_FAIL' => 'Failed to vote; check your connection.', 123 | 'VOTE_SUCCESS' => 'Successfully voted on this idea.', 124 | 'VOTE_UP' => 'Vote Up', 125 | 'VOTES' => 'Votes', 126 | )); 127 | -------------------------------------------------------------------------------- /styles/prosilver/theme/ideas.css: -------------------------------------------------------------------------------- 1 | /* flex box classes */ 2 | .flex-box { 3 | display: flex; 4 | } 5 | 6 | .flex-wrap { 7 | flex-wrap: wrap; 8 | } 9 | 10 | .flex-grow { 11 | flex-grow: 1; 12 | } 13 | 14 | .flex-justify { 15 | justify-content: space-between; 16 | } 17 | 18 | .flex-align-end { 19 | align-items: flex-end; 20 | } 21 | 22 | .flex-align-center { 23 | align-items: center; 24 | } 25 | 26 | /* Common Styles */ 27 | dd.topics { 28 | text-align: left; 29 | width: auto; 30 | padding-left: 10px !important; 31 | } 32 | 33 | .rtl dd.topics { 34 | text-align: right; 35 | padding-right: 10px !important; 36 | } 37 | 38 | .fa-lightbulb-o { 39 | font-weight: 400 !important; 40 | } 41 | 42 | /* Vote buttons */ 43 | .minivote { 44 | line-height: normal; 45 | white-space: nowrap; 46 | border-radius: 6px; 47 | color: #ffffff !important; 48 | position: relative; 49 | display: inline-block; 50 | float: left; 51 | box-sizing: border-box; 52 | width: 80px; 53 | margin-right: 10px; 54 | padding: 6px 10px; 55 | } 56 | 57 | .minivote .icon { 58 | font-size: 16px; 59 | padding-right: 0; 60 | padding-left: 0; 61 | } 62 | 63 | .minivote .vote-count { 64 | font-family: Verdana, Arial, sans-serif; 65 | font-size: 14px; 66 | text-align: center; 67 | display: inline-block; 68 | width: 40px; 69 | } 70 | 71 | .rtl .minivote { 72 | float: right; 73 | margin-right: auto; 74 | margin-left: 10px; 75 | } 76 | 77 | .vote-up, 78 | .vote-up.vote-disabled:hover { 79 | background: #00a9e2; 80 | border: solid 1px #00a9e2; 81 | } 82 | 83 | .vote-up:hover { 84 | background: #009ad1; 85 | } 86 | 87 | .vote-up:active { 88 | background: #10b2e5; 89 | } 90 | 91 | .vote-down, 92 | .vote-down.vote-disabled:hover { 93 | background: #f76c5e; 94 | border: solid 1px #f76c5e; 95 | } 96 | 97 | .vote-down:hover { 98 | background: #e56155; 99 | } 100 | 101 | .vote-down:active { 102 | background: #f87768; 103 | } 104 | 105 | .user-voted { 106 | font-size: 16px; 107 | color: #fffaf0; 108 | position: absolute; 109 | top: -4px; 110 | right: -4px; 111 | } 112 | 113 | .rtl .user-voted { 114 | right: auto; 115 | left: -4px; 116 | } 117 | 118 | /* Ideas Lists Styles */ 119 | .view-all { 120 | color: #536482; 121 | margin: 0.8em 0 0.2em; 122 | } 123 | 124 | .view-all:hover { 125 | color: #536482; 126 | } 127 | 128 | @media (min-width: 701px) and (max-width: 950px) { 129 | ul.topiclist dt { 130 | margin-right: -250px; 131 | } 132 | 133 | ul.topiclist dt .list-inner { 134 | margin-right: 250px; 135 | } 136 | } 137 | 138 | /* Idea Body Styles */ 139 | .idea-panel { 140 | font-size: 11px; 141 | width: 100%; 142 | } 143 | 144 | .idea-panel-inner { 145 | box-sizing: border-box; 146 | width: 400px; 147 | padding: 0 3px; 148 | } 149 | 150 | .votes, 151 | .successvoted { 152 | font-weight: bold; 153 | border-radius: 4px; 154 | margin-top: 3px; 155 | padding: 6px; 156 | } 157 | 158 | .votes { 159 | display: inline-block; 160 | } 161 | 162 | .votes:active, 163 | .votes:hover, 164 | .votes:link, 165 | .votes:visited { 166 | text-decoration: none; 167 | } 168 | 169 | .successvoted { 170 | display: none; 171 | } 172 | 173 | .voteslist { 174 | border-radius: 3px; 175 | display: none; 176 | margin-top: 6px; 177 | padding: 6px; 178 | list-style-type: none; 179 | } 180 | 181 | .voteslist ul, 182 | .voteslist ul li { 183 | list-style: none; 184 | } 185 | 186 | .voteslist ul li:not(:first-child) { 187 | border-top: dashed 1px #b9b9b9; 188 | } 189 | 190 | .voteslist ul li { 191 | margin-left: 16px; 192 | } 193 | 194 | .voteslist ul li i { 195 | margin-left: -16px; 196 | } 197 | 198 | .rtl .voteslist ul li { 199 | margin-right: 16px; 200 | margin-left: 0; 201 | } 202 | 203 | .rtl .voteslist ul li i { 204 | margin-right: -16px; 205 | margin-left: 0; 206 | } 207 | 208 | .voteslist .fa-thumbs-up { 209 | color: #00a9e2; 210 | } 211 | 212 | .voteslist .fa-thumbs-down { 213 | color: #f76c5e; 214 | } 215 | 216 | .vote-disabled { 217 | cursor: default; 218 | } 219 | 220 | .vote-disabled:hover { 221 | text-decoration: none; 222 | } 223 | 224 | .vote-disabled.removevote { 225 | display: none; 226 | } 227 | 228 | .hidden { 229 | display: none; 230 | } 231 | 232 | .ideainput { 233 | display: none; 234 | width: 200px; 235 | } 236 | 237 | .status-item { 238 | min-height: 18px; 239 | margin: 4px 0; 240 | } 241 | 242 | .status-item a, 243 | .status-item input { 244 | margin-left: 4px; 245 | } 246 | 247 | .status-badge { 248 | border-radius: 3px; 249 | color: #ffffff; 250 | display: inline-block; 251 | padding: 3px 6px; 252 | } 253 | 254 | .status-badge a { 255 | color: #ffffff; 256 | } 257 | 258 | .status-badge a:hover { 259 | color: #ffffff; 260 | } 261 | 262 | .status-dropdown { 263 | margin-top: 8px; 264 | } 265 | 266 | .status-1 { 267 | background-color: #6495ed; 268 | } 269 | 270 | .status-2 { 271 | background-color: #9acd32; 272 | } 273 | 274 | .status-3 { 275 | background-color: #006400; 276 | } 277 | 278 | .status-4 { 279 | background-color: #daa520; 280 | } 281 | 282 | .status-5 { 283 | background-color: #b22222; 284 | } 285 | -------------------------------------------------------------------------------- /controller/base.php: -------------------------------------------------------------------------------- 1 | 7 | * @license GNU General Public License, version 2 (GPL-2.0) 8 | * 9 | */ 10 | 11 | namespace phpbb\ideas\controller; 12 | 13 | use phpbb\auth\auth; 14 | use phpbb\config\config; 15 | use phpbb\controller\helper; 16 | use phpbb\ideas\factory\linkhelper; 17 | use phpbb\language\language; 18 | use phpbb\pagination; 19 | use phpbb\request\request; 20 | use phpbb\template\template; 21 | use phpbb\user; 22 | 23 | abstract class base 24 | { 25 | /** @var auth */ 26 | protected $auth; 27 | 28 | /* @var config */ 29 | protected $config; 30 | 31 | /* @var helper */ 32 | protected $helper; 33 | 34 | /* @var \phpbb\ideas\factory\base */ 35 | protected $entity; 36 | 37 | /** @var language */ 38 | protected $language; 39 | 40 | /* @var linkhelper */ 41 | protected $link_helper; 42 | 43 | /** @var pagination */ 44 | protected $pagination; 45 | 46 | /* @var request */ 47 | protected $request; 48 | 49 | /* @var template */ 50 | protected $template; 51 | 52 | /* @var user */ 53 | protected $user; 54 | 55 | /** @var string */ 56 | protected $root_path; 57 | 58 | /** @var string */ 59 | protected $php_ext; 60 | 61 | /** 62 | * @param auth $auth 63 | * @param config $config 64 | * @param helper $helper 65 | * @param language $language 66 | * @param linkhelper $link_helper 67 | * @param pagination $pagination 68 | * @param request $request 69 | * @param template $template 70 | * @param user $user 71 | * @param string $root_path 72 | * @param string $php_ext 73 | */ 74 | public function __construct(auth $auth, config $config, helper $helper, language $language, linkhelper $link_helper, pagination $pagination, request $request, template $template, user $user, $root_path, $php_ext) 75 | { 76 | $this->auth = $auth; 77 | $this->config = $config; 78 | $this->helper = $helper; 79 | $this->language = $language; 80 | $this->link_helper = $link_helper; 81 | $this->pagination = $pagination; 82 | $this->request = $request; 83 | $this->template = $template; 84 | $this->user = $user; 85 | $this->root_path = $root_path; 86 | $this->php_ext = $php_ext; 87 | 88 | $this->language->add_lang('common', 'phpbb/ideas'); 89 | } 90 | 91 | /** 92 | * Set the Ideas entity 93 | * 94 | * @param \phpbb\ideas\factory\base $entity 95 | */ 96 | public function get_entity($entity) 97 | { 98 | $this->entity = $entity; 99 | } 100 | 101 | /** 102 | * Check if Ideas is properly configured after installation 103 | * Ideas is available only after forum settings have been set in ACP 104 | * 105 | * @return bool Depending on whether the extension is properly configured 106 | */ 107 | public function is_available() 108 | { 109 | return (bool) $this->config['ideas_forum_id']; 110 | } 111 | 112 | /** 113 | * Assign idea lists template variables 114 | * 115 | * @param string $block The template block var name 116 | * @param array $rows The Idea row data 117 | * 118 | * @return void 119 | */ 120 | protected function assign_template_block_vars($block, $rows) 121 | { 122 | foreach ($rows as $row) 123 | { 124 | $this->template->assign_block_vars($block, array( 125 | 'ID' => $row['idea_id'], // (not currently implemented) 126 | 'LINK' => $this->link_helper->get_idea_link($row['idea_id']), 127 | 'TITLE' => $row['idea_title'], 128 | 'AUTHOR' => $this->link_helper->get_user_link($row['idea_author']), 129 | 'DATE' => $this->user->format_date($row['idea_date']), 130 | 'READ' => $row['read'], 131 | 'VOTES_UP' => $row['idea_votes_up'], 132 | 'VOTES_DOWN' => $row['idea_votes_down'], 133 | 'USER_VOTED' => $row['u_voted'], 134 | 'POINTS' => $row['idea_votes_up'] - $row['idea_votes_down'], // (not currently implemented) 135 | 'STATUS' => $row['idea_status'], // for status icons (not currently implemented) 136 | 'LOCKED' => $row['topic_status'] == ITEM_LOCKED, 137 | 'U_UNAPPROVED_IDEA' => (($row['topic_visibility'] == ITEM_UNAPPROVED || $row['topic_visibility'] == ITEM_REAPPROVE) && $this->auth->acl_get('m_approve', $this->config['ideas_forum_id'])) ? append_sid("{$this->root_path}mcp.$this->php_ext", 'i=queue&mode=approve_details&t=' . $row['topic_id'], true, $this->user->session_id) : '', 138 | )); 139 | } 140 | } 141 | 142 | /** 143 | * Assign common template variables for Ideas pages 144 | * 145 | * @return void 146 | */ 147 | protected function display_common_vars() 148 | { 149 | $this->template->assign_vars([ 150 | 'S_DISPLAY_SEARCHBOX' => $this->auth->acl_get('u_search') && $this->auth->acl_get('f_search', $this->config['ideas_forum_id']) && $this->config['load_search'], 151 | 'S_SEARCHBOX_ACTION' => append_sid("{$this->root_path}search.$this->php_ext"), 152 | 'S_SEARCH_IDEAS_HIDDEN_FIELDS' => build_hidden_fields(['fid' => [$this->config['ideas_forum_id']]]), 153 | ]); 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /notification/type/status.php: -------------------------------------------------------------------------------- 1 | 7 | * @license GNU General Public License, version 2 (GPL-2.0) 8 | * 9 | */ 10 | 11 | namespace phpbb\ideas\notification\type; 12 | 13 | use phpbb\config\config; 14 | use phpbb\controller\helper; 15 | use phpbb\ideas\ext; 16 | use phpbb\user_loader; 17 | use Symfony\Component\Routing\Generator\UrlGeneratorInterface; 18 | 19 | /** 20 | * Ideas status change notification class. 21 | */ 22 | class status extends \phpbb\notification\type\base 23 | { 24 | /** @var config */ 25 | protected $config; 26 | 27 | /** @var helper */ 28 | protected $helper; 29 | 30 | /** @var user_loader */ 31 | protected $user_loader; 32 | 33 | /** @var int */ 34 | protected $ideas_forum_id; 35 | 36 | /** 37 | * Set additional services and properties 38 | * 39 | * @param config $config 40 | * @param helper $helper 41 | * @param user_loader $user_loader 42 | * @return void 43 | */ 44 | public function set_additional_services(config $config, helper $helper, user_loader $user_loader) 45 | { 46 | $this->helper = $helper; 47 | $this->user_loader = $user_loader; 48 | $this->ideas_forum_id = (int) $config['ideas_forum_id']; 49 | } 50 | 51 | /** 52 | * Email template to use to send notifications 53 | * 54 | * @var string 55 | */ 56 | protected $email_template = '@phpbb_ideas/status_notification'; 57 | 58 | /** 59 | * Language key used to output the text 60 | * 61 | * @var string 62 | */ 63 | protected $language_key = 'IDEA_STATUS_CHANGE'; 64 | 65 | /** 66 | * {@inheritDoc} 67 | */ 68 | public static $notification_option = [ 69 | 'lang' => 'NOTIFICATION_TYPE_IDEAS', 70 | 'group' => 'NOTIFICATION_GROUP_MISCELLANEOUS', 71 | ]; 72 | 73 | /** 74 | * {@inheritDoc} 75 | */ 76 | public function get_type() 77 | { 78 | return ext::NOTIFICATION_TYPE_STATUS; 79 | } 80 | 81 | /** 82 | * {@inheritDoc} 83 | */ 84 | public static function get_item_id($type_data) 85 | { 86 | return (int) $type_data['idea_id']; 87 | } 88 | 89 | /** 90 | * {@inheritDoc} 91 | */ 92 | public static function get_item_parent_id($type_data) 93 | { 94 | return 0; 95 | } 96 | 97 | /** 98 | * {@inheritDoc} 99 | */ 100 | public function is_available() 101 | { 102 | return (bool) $this->auth->acl_get('f_read', $this->ideas_forum_id); 103 | } 104 | 105 | /** 106 | * {@inheritDoc} 107 | */ 108 | public function find_users_for_notification($type_data, $options = []) 109 | { 110 | $options = array_merge([ 111 | 'ignore_users' => [], 112 | ], $options); 113 | 114 | $users = [$type_data['idea_author']]; 115 | 116 | return $this->get_authorised_recipients($users, $this->ideas_forum_id, $options); 117 | } 118 | 119 | /** 120 | * {@inheritDoc} 121 | */ 122 | public function users_to_query() 123 | { 124 | return [$this->get_data('updater_id')]; 125 | } 126 | 127 | /** 128 | * {@inheritDoc} 129 | */ 130 | public function get_title() 131 | { 132 | if (!$this->language->is_set($this->language_key)) 133 | { 134 | $this->language->add_lang('common', 'phpbb/ideas'); 135 | } 136 | 137 | $username = $this->user_loader->get_username($this->get_data('updater_id'), 'no_profile'); 138 | 139 | return $this->language->lang($this->language_key, $username); 140 | } 141 | 142 | /** 143 | * {@inheritDoc} 144 | */ 145 | public function get_reference() 146 | { 147 | return $this->language->lang( 148 | 'NOTIFICATION_REFERENCE', 149 | censor_text($this->get_data('idea_title')) 150 | ); 151 | } 152 | 153 | /** 154 | * {@inheritDoc} 155 | */ 156 | public function get_reason() 157 | { 158 | return $this->language->lang( 159 | 'NOTIFICATION_STATUS', 160 | $this->language->lang(ext::status_name($this->get_data('status'))) 161 | ); 162 | } 163 | 164 | /** 165 | * {@inheritDoc} 166 | */ 167 | public function get_url($reference_type = UrlGeneratorInterface::ABSOLUTE_PATH) 168 | { 169 | $params = ['idea_id' => $this->get_data('idea_id')]; 170 | 171 | return $this->helper->route('phpbb_ideas_idea_controller', $params, true, false, $reference_type); 172 | } 173 | 174 | /** 175 | * {@inheritDoc} 176 | */ 177 | public function get_avatar() 178 | { 179 | return $this->user_loader->get_avatar($this->get_data('updater_id'), false, true); 180 | } 181 | 182 | /** 183 | * {@inheritDoc} 184 | */ 185 | public function get_email_template() 186 | { 187 | return $this->email_template; 188 | } 189 | 190 | /** 191 | * {@inheritDoc} 192 | */ 193 | public function get_email_template_variables() 194 | { 195 | return [ 196 | 'IDEA_TITLE' => html_entity_decode(censor_text($this->get_data('idea_title')), ENT_COMPAT), 197 | 'STATUS' => html_entity_decode($this->language->lang(ext::status_name($this->get_data('status'))), ENT_COMPAT), 198 | 'UPDATED_BY' => html_entity_decode($this->user_loader->get_username($this->get_data('updater_id'), 'username'), ENT_COMPAT), 199 | 'U_VIEW_IDEA' => $this->get_url(UrlGeneratorInterface::ABSOLUTE_URL), 200 | ]; 201 | } 202 | 203 | /** 204 | * {@inheritDoc} 205 | */ 206 | public function create_insert_array($type_data, $pre_create_data = []) 207 | { 208 | $this->set_data('idea_id', (int) $type_data['idea_id']); 209 | $this->set_data('status', (int) $type_data['status']); 210 | $this->set_data('updater_id', (int) $type_data['user_id']); 211 | $this->set_data('idea_title', $type_data['idea_title']); 212 | 213 | parent::create_insert_array($type_data, $pre_create_data); 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /controller/admin_controller.php: -------------------------------------------------------------------------------- 1 | 7 | * @license GNU General Public License, version 2 (GPL-2.0) 8 | * 9 | */ 10 | 11 | namespace phpbb\ideas\controller; 12 | 13 | /** 14 | * Admin controller 15 | */ 16 | class admin_controller 17 | { 18 | /** @var \phpbb\config\config */ 19 | protected $config; 20 | 21 | /** @var \phpbb\db\driver\driver_interface */ 22 | protected $db; 23 | 24 | /** @var \phpbb\language\language */ 25 | protected $language; 26 | 27 | /** @var \phpbb\log\log */ 28 | protected $log; 29 | 30 | /** @var \phpbb\request\request */ 31 | protected $request; 32 | 33 | /** @var \phpbb\template\template */ 34 | protected $template; 35 | 36 | /** @var \phpbb\user */ 37 | protected $user; 38 | 39 | /** @var string */ 40 | protected $phpbb_root_path; 41 | 42 | /** @var string */ 43 | protected $php_ext; 44 | 45 | /** @var string */ 46 | public $u_action; 47 | 48 | /** 49 | * Constructor 50 | * 51 | * @param \phpbb\config\config $config Config object 52 | * @param \phpbb\db\driver\driver_interface $db Database object 53 | * @param \phpbb\language\language $language Language object 54 | * @param \phpbb\log\log $log Log object 55 | * @param \phpbb\request\request $request Request object 56 | * @param \phpbb\template\template $template Template object 57 | * @param \phpbb\user $user User object 58 | * @param string $root_path phpBB root path 59 | * @param string $php_ext php_ext 60 | * @access public 61 | */ 62 | public function __construct(\phpbb\config\config $config, \phpbb\db\driver\driver_interface $db, \phpbb\language\language $language, \phpbb\log\log $log, \phpbb\request\request $request, \phpbb\template\template $template, \phpbb\user $user, $root_path, $php_ext) 63 | { 64 | $this->config = $config; 65 | $this->db = $db; 66 | $this->language = $language; 67 | $this->log = $log; 68 | $this->request = $request; 69 | $this->template = $template; 70 | $this->user = $user; 71 | $this->phpbb_root_path = $root_path; 72 | $this->php_ext = $php_ext; 73 | } 74 | 75 | /** 76 | * Display the options a user can configure for this extension 77 | * 78 | * @return void 79 | * @access public 80 | */ 81 | public function display_options() 82 | { 83 | $this->template->assign_vars(array( 84 | 'S_FORUM_SELECT_BOX' => $this->select_ideas_forum(), 85 | 'S_IDEAS_FORUM_ID' => !empty($this->config['ideas_forum_id']), 86 | 87 | 'U_ACTION' => $this->u_action, 88 | )); 89 | } 90 | 91 | /** 92 | * Set configuration options 93 | * 94 | * @return void 95 | * @access public 96 | */ 97 | public function set_config_options() 98 | { 99 | $errors = array(); 100 | 101 | // Check the form for validity 102 | if (!check_form_key('acp_phpbb_ideas_settings')) 103 | { 104 | $errors[] = $this->language->lang('FORM_INVALID'); 105 | } 106 | 107 | // Don't save settings if errors have occurred 108 | if (count($errors)) 109 | { 110 | $this->template->assign_vars(array( 111 | 'S_ERROR' => true, 112 | 'ERROR_MSG' => implode('
    ', $errors), 113 | )); 114 | 115 | return; 116 | } 117 | 118 | $cfg_array = $this->request->variable('config', array('' => '')); 119 | 120 | // Configuration options to list through 121 | $display_vars = array( 122 | 'ideas_forum_id', 123 | ); 124 | 125 | // We go through the display_vars to make sure no one is trying to set variables he/she is not allowed to 126 | foreach ($display_vars as $config_name) 127 | { 128 | if (!isset($cfg_array[$config_name])) 129 | { 130 | continue; 131 | } 132 | 133 | $this->config->set($config_name, $cfg_array[$config_name]); 134 | } 135 | 136 | $this->log->add('admin', $this->user->data['user_id'], $this->user->ip, 'ACP_PHPBB_IDEAS_SETTINGS_LOG'); 137 | trigger_error($this->language->lang('ACP_IDEAS_SETTINGS_UPDATED') . adm_back_link($this->u_action)); 138 | } 139 | 140 | /** 141 | * Set ideas forum options 142 | * 143 | * @return void 144 | * @access public 145 | */ 146 | public function set_ideas_forum_options() 147 | { 148 | // Check if Ideas forum is selected and apply relevant settings if it is 149 | // But display the confirmation box first 150 | if (confirm_box(true)) 151 | { 152 | if (empty($this->config['ideas_forum_id'])) 153 | { 154 | trigger_error($this->language->lang('ACP_IDEAS_NO_FORUM') . adm_back_link($this->u_action), E_USER_WARNING); 155 | } 156 | 157 | $forum_id = (int) $this->config['ideas_forum_id']; 158 | 159 | $permission_helper = new \phpbb\ideas\factory\permission_helper($this->db, $this->phpbb_root_path, $this->php_ext); 160 | $permission_helper->set_ideas_forum_permissions($forum_id); 161 | 162 | // Disable auto-pruning for ideas forum 163 | $sql = 'UPDATE ' . FORUMS_TABLE . ' 164 | SET ' . $this->db->sql_build_array('UPDATE', array('enable_prune' => false)) . ' 165 | WHERE forum_id = ' . $forum_id; 166 | $this->db->sql_query($sql); 167 | 168 | $this->log->add('admin', $this->user->data['user_id'], $this->user->ip, 'ACP_PHPBB_IDEAS_FORUM_SETUP_LOG'); 169 | trigger_error($this->language->lang('ACP_IDEAS_FORUM_SETUP_UPDATED') . adm_back_link($this->u_action)); 170 | } 171 | else 172 | { 173 | confirm_box(false, $this->language->lang('ACP_IDEAS_FORUM_SETUP_CONFIRM'), build_hidden_fields(array( 174 | 'ideas_forum_setup' => $this->request->is_set_post('ideas_forum_setup'), 175 | ))); 176 | } 177 | } 178 | 179 | /** 180 | * Generate ideas forum select options 181 | * 182 | * @return string Select menu HTML code 183 | * @access protected 184 | */ 185 | protected function select_ideas_forum() 186 | { 187 | $ideas_forum_id = (int) $this->config['ideas_forum_id']; 188 | $s_forums_list = ''; 192 | 193 | return $s_forums_list; 194 | } 195 | 196 | /** 197 | * Set page url 198 | * 199 | * @param string $u_action Custom form action 200 | * @return void 201 | * @access public 202 | */ 203 | public function set_page_url($u_action) 204 | { 205 | $this->u_action = $u_action; 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /controller/idea_controller.php: -------------------------------------------------------------------------------- 1 | 7 | * @license GNU General Public License, version 2 (GPL-2.0) 8 | * 9 | */ 10 | 11 | namespace phpbb\ideas\controller; 12 | 13 | use phpbb\exception\http_exception; 14 | use phpbb\ideas\ext; 15 | use Symfony\Component\HttpFoundation\JsonResponse; 16 | use Symfony\Component\HttpFoundation\RedirectResponse; 17 | 18 | class idea_controller extends base 19 | { 20 | /** @var array of idea data */ 21 | protected $data; 22 | 23 | /* @var \phpbb\ideas\factory\idea */ 24 | protected $entity; 25 | 26 | /** 27 | * Controller for /idea/{idea_id} 28 | * 29 | * @param $idea_id int The ID of the requested idea, maybe? 30 | * @throws http_exception 31 | * @return JsonResponse|RedirectResponse 32 | */ 33 | public function idea($idea_id) 34 | { 35 | if (!$this->is_available()) 36 | { 37 | throw new http_exception(404, 'IDEAS_NOT_AVAILABLE'); 38 | } 39 | 40 | $this->data = $this->entity->get_idea($idea_id); 41 | if (!$this->data) 42 | { 43 | throw new http_exception(404, 'IDEA_NOT_FOUND'); 44 | } 45 | 46 | $mode = $this->request->variable('mode', ''); 47 | if (!empty($mode) && $this->request->is_ajax()) 48 | { 49 | $result = $this->$mode(); 50 | 51 | return new JsonResponse($result); 52 | } 53 | 54 | $params = array( 55 | 'f' => $this->config['ideas_forum_id'], 56 | 't' => $this->data['topic_id'] 57 | ); 58 | 59 | if ($unread = ($this->request->variable('view', '') === 'unread')) 60 | { 61 | $params = array_merge($params, array('view' => 'unread')); 62 | } 63 | 64 | $url = append_sid(generate_board_url() . "/viewtopic.$this->php_ext", $params, false) . ($unread ? '#unread' : ''); 65 | 66 | return new RedirectResponse($url); 67 | } 68 | 69 | /** 70 | * Delete action (deletes an idea via confirm dialog) 71 | * 72 | * @throws http_exception 73 | * @return void 74 | * @access public 75 | */ 76 | public function delete() 77 | { 78 | if (!$this->is_mod()) 79 | { 80 | throw new http_exception(403, 'NO_AUTH_OPERATION'); 81 | } 82 | 83 | if (confirm_box(true)) 84 | { 85 | include $this->root_path . 'includes/functions_admin.' . $this->php_ext; 86 | $this->entity->delete($this->data['idea_id'], $this->data['topic_id']); 87 | 88 | $redirect = $this->helper->route('phpbb_ideas_index_controller'); 89 | $message = $this->language->lang('IDEA_DELETED') . '

    ' . $this->language->lang('RETURN_IDEAS', '', ''); 90 | meta_refresh(3, $redirect); 91 | trigger_error($message); // trigger error needed for data-ajax 92 | } 93 | else 94 | { 95 | confirm_box( 96 | false, 97 | $this->language->lang('CONFIRM_DELETE'), 98 | build_hidden_fields(array( 99 | 'idea_id' => $this->data['idea_id'], 100 | 'mode' => 'delete', 101 | )), 102 | 'confirm_body.html', 103 | $this->helper->route( 104 | 'phpbb_ideas_idea_controller', 105 | array( 106 | 'idea_id' => $this->data['idea_id'], 107 | 'mode' => 'delete', 108 | ) 109 | ) 110 | ); 111 | } 112 | } 113 | 114 | /** 115 | * Duplicate action (sets an idea's duplicate link) 116 | * 117 | * @return bool True if set, false if not 118 | * @access public 119 | */ 120 | public function duplicate() 121 | { 122 | if ($this->is_mod() && check_link_hash($this->get_hash(), "duplicate_{$this->data['idea_id']}")) 123 | { 124 | $duplicate = $this->request->variable('duplicate', 0); 125 | return $this->entity->set_duplicate($this->data['idea_id'], $duplicate); 126 | } 127 | 128 | return false; 129 | } 130 | 131 | /** 132 | * Remove vote action (remove a user's vote from an idea) 133 | * 134 | * @return array|false|string Array of vote data, an error message, or false if failed 135 | * @access public 136 | */ 137 | public function removevote() 138 | { 139 | if ($this->data['idea_status'] === ext::$statuses['IMPLEMENTED'] || $this->data['idea_status'] === ext::$statuses['DUPLICATE'] || !check_link_hash($this->get_hash(), "removevote_{$this->data['idea_id']}")) 140 | { 141 | return false; 142 | } 143 | 144 | if ($this->auth->acl_get('f_vote', (int) $this->config['ideas_forum_id'])) 145 | { 146 | $result = $this->entity->remove_vote($this->data, $this->user->data['user_id']); 147 | } 148 | else 149 | { 150 | $result = $this->language->lang('NO_AUTH_OPERATION'); 151 | } 152 | 153 | return $result; 154 | } 155 | 156 | /** 157 | * RFC action (sets an idea's rfc link) 158 | * 159 | * @return bool True if set, false if not 160 | * @access public 161 | */ 162 | public function rfc() 163 | { 164 | if (($this->is_own() || $this->is_mod()) && check_link_hash($this->get_hash(), "rfc_{$this->data['idea_id']}")) 165 | { 166 | $rfc = $this->request->variable('rfc', ''); 167 | return $this->entity->set_rfc($this->data['idea_id'], $rfc); 168 | } 169 | 170 | return false; 171 | } 172 | 173 | /** 174 | * Status action (sets an idea's status) 175 | * 176 | * @return bool True if set, false if not 177 | * @access public 178 | */ 179 | public function status() 180 | { 181 | $status = $this->request->variable('status', 0); 182 | 183 | if ($status && $this->is_mod() && check_link_hash($this->get_hash(), "status_{$this->data['idea_id']}")) 184 | { 185 | $this->entity->set_status($this->data['idea_id'], $status); 186 | return true; 187 | } 188 | 189 | return false; 190 | } 191 | 192 | /** 193 | * Ticket action (sets an idea's ticket link) 194 | * 195 | * @return bool True if set, false if not 196 | * @access public 197 | */ 198 | public function ticket() 199 | { 200 | if (($this->is_own() || $this->is_mod()) && check_link_hash($this->get_hash(), "ticket_{$this->data['idea_id']}")) 201 | { 202 | $ticket = $this->request->variable('ticket', 0); 203 | return $this->entity->set_ticket($this->data['idea_id'], $ticket); 204 | } 205 | 206 | return false; 207 | } 208 | 209 | /** 210 | * Implemented action (sets an idea's implemented phpBB version) 211 | * 212 | * @return bool True if set, false if not 213 | * @access public 214 | */ 215 | public function implemented() 216 | { 217 | if ($this->is_mod() && check_link_hash($this->get_hash(), "implemented_{$this->data['idea_id']}")) 218 | { 219 | $version = $this->request->variable('implemented', ''); 220 | return $this->entity->set_implemented($this->data['idea_id'], $version); 221 | } 222 | 223 | return false; 224 | } 225 | 226 | /** 227 | * Vote action (sets an idea's vote) 228 | * 229 | * @return array|false|string Array of vote data, an error message, or false if failed 230 | * @access public 231 | */ 232 | public function vote() 233 | { 234 | $vote = $this->request->variable('v', 1); 235 | 236 | if ($this->data['idea_status'] === ext::$statuses['IMPLEMENTED'] || $this->data['idea_status'] === ext::$statuses['DUPLICATE'] || !check_link_hash($this->get_hash(), "vote_{$this->data['idea_id']}")) 237 | { 238 | return false; 239 | } 240 | 241 | if ($this->auth->acl_get('f_vote', (int) $this->config['ideas_forum_id'])) 242 | { 243 | $result = $this->entity->vote($this->data, $this->user->data['user_id'], $vote); 244 | } 245 | else 246 | { 247 | $result = $this->language->lang('NO_AUTH_OPERATION'); 248 | } 249 | 250 | return $result; 251 | } 252 | 253 | /** 254 | * Get a hash query parameter 255 | * 256 | * @return string The hash 257 | * @access protected 258 | */ 259 | protected function get_hash() 260 | { 261 | return $this->request->variable('hash', ''); 262 | } 263 | 264 | /** 265 | * Does the user have moderator privileges? 266 | * 267 | * @return bool 268 | * @access protected 269 | */ 270 | protected function is_mod() 271 | { 272 | return $this->auth->acl_get('m_', (int) $this->config['ideas_forum_id']); 273 | } 274 | 275 | /** 276 | * Is the user the author of the idea? 277 | * 278 | * @return bool 279 | * @access protected 280 | */ 281 | protected function is_own() 282 | { 283 | return $this->data['idea_author'] === $this->user->data['user_id']; 284 | } 285 | } 286 | -------------------------------------------------------------------------------- /styles/prosilver/template/idea_body.html: -------------------------------------------------------------------------------- 1 | {% INCLUDEJS '@phpbb_ideas/ideas.js' %} 2 | 9 |
    10 |
    11 |
    12 | 21 |
    22 |

    {{ lang('STATUS') }}

    23 | 43 | {% if IDEA_RFC %} 44 |
    45 | {{ lang('RFC') ~ lang('COLON') }} 46 | 47 | {% if S_CAN_EDIT %} 48 | {% if IDEA_RFC %}{{ lang('EDIT') }}{% else %}{{ lang('ADD') }}{% endif %} 49 | 50 | {% endif %} 51 |
    52 | {% endif %} 53 | {% if IDEA_TICKET or S_CAN_EDIT %} 54 |
    55 | {{ lang('TICKET') ~ lang('COLON') }} 56 | 57 | {% if S_CAN_EDIT %} 58 | {% if IDEA_TICKET %}{{ lang('EDIT') }}{% else %}{{ lang('ADD') }}{% endif %} 59 | 60 | {% endif %} 61 |
    62 | {% endif %} 63 | {% if IDEA_DUPLICATE or S_IS_MOD %} 64 |
    65 | {{ lang('DUPLICATE') ~ lang('COLON') }} 66 | 67 | {% if S_IS_MOD %} 68 | {% if IDEA_DUPLICATE %}{{ lang('EDIT') }}{% else %}{{ lang('ADD') }}{% endif %} 69 | 93 | {% endif %} 94 |
    95 | {% endif %} 96 | {% if IDEA_IMPLEMENTED or S_IS_MOD %} 97 |
    98 | {{ lang('IMPLEMENTED_VERSION') ~ lang('COLON') }} 99 | 100 | {% if S_IS_MOD %} 101 | {% if IDEA_IMPLEMENTED %}{{ lang('EDIT') }}{% else %}{{ lang('ADD') }}{% endif %} 102 | 103 | {% endif %} 104 |
    105 | {% endif %} 106 |
    107 |
    108 |
    109 |
      110 | 118 | 126 | {% if S_CAN_VOTE %} 127 | 130 | {% endif %} 131 |
    132 |
    133 |
    134 |
    135 | -------------------------------------------------------------------------------- /factory/ideas.php: -------------------------------------------------------------------------------- 1 | 7 | * @license GNU General Public License, version 2 (GPL-2.0) 8 | * 9 | */ 10 | 11 | namespace phpbb\ideas\factory; 12 | 13 | use phpbb\exception\runtime_exception; 14 | use phpbb\ideas\ext; 15 | 16 | /** 17 | * Class for handling multiple ideas 18 | */ 19 | class ideas extends base 20 | { 21 | /** @var int */ 22 | protected $idea_count; 23 | 24 | /** @var array */ 25 | protected $sql; 26 | 27 | /** 28 | * Returns an array of ideas. Defaults to ten ideas ordered by date 29 | * excluding implemented, duplicate or invalid ideas. 30 | * 31 | * @param int $number The number of ideas to return 32 | * @param string $sort A sorting option/collection 33 | * @param string $direction Should either be ASC or DESC 34 | * @param array|int $status The id of the status(es) to load 35 | * @param int $start Start value for pagination 36 | * 37 | * @return array Array of row data 38 | */ 39 | public function get_ideas($number = 10, $sort = 'date', $direction = 'DESC', $status = [], $start = 0) 40 | { 41 | // Initialize a query to request ideas 42 | $sql = $this->query_ideas() 43 | ->query_sort($sort, $direction) 44 | ->query_status($status); 45 | 46 | // For pagination, get a count of the total ideas being requested 47 | if ($number >= $this->config['posts_per_page']) 48 | { 49 | $this->idea_count = $sql->query_count(); 50 | } 51 | 52 | $ideas = $sql->query_get($number, $start); 53 | 54 | if (count($ideas)) 55 | { 56 | $topic_ids = array_column($ideas, 'topic_id'); 57 | $idea_ids = array_column($ideas, 'idea_id'); 58 | 59 | $topic_tracking_info = get_complete_topic_tracking((int) $this->config['ideas_forum_id'], $topic_ids); 60 | $user_voting_info = $this->get_users_votes($this->user->data['user_id'], $idea_ids); 61 | 62 | foreach ($ideas as &$idea) 63 | { 64 | $idea['read'] = !(isset($topic_tracking_info[$idea['topic_id']]) && $idea['topic_last_post_time'] > $topic_tracking_info[$idea['topic_id']]); 65 | $idea['u_voted'] = isset($user_voting_info[$idea['idea_id']]) ? (int) $user_voting_info[$idea['idea_id']] : ''; 66 | } 67 | unset ($idea); 68 | } 69 | 70 | return $ideas; 71 | } 72 | 73 | /** 74 | * Initialize the $sql property with necessary SQL statements. 75 | * 76 | * @return \phpbb\ideas\factory\ideas $this For chaining calls 77 | */ 78 | protected function query_ideas() 79 | { 80 | $this->sql = []; 81 | 82 | $this->sql['SELECT'][] = 't.topic_last_post_time, t.topic_status, t.topic_visibility, i.*'; 83 | $this->sql['FROM'] = "$this->table_ideas i"; 84 | $this->sql['JOIN'] = "$this->table_topics t ON i.topic_id = t.topic_id"; 85 | $this->sql['WHERE'][] = 't.forum_id = ' . (int) $this->config['ideas_forum_id']; 86 | 87 | // Only get approved topics for regular users, Moderators will see unapproved topics 88 | if (!$this->auth->acl_get('m_', $this->config['ideas_forum_id'])) 89 | { 90 | $this->sql['WHERE'][] = 't.topic_visibility = ' . ITEM_APPROVED; 91 | } 92 | 93 | return $this; 94 | } 95 | 96 | /** 97 | * Update the $sql property with ORDER BY statements to obtain 98 | * the requested collection of Ideas. Some instances may add 99 | * additional WHERE or SELECT statements to refine the collection. 100 | * 101 | * @param string $sort A sorting option/collection 102 | * @param string $direction Will either be ASC or DESC 103 | * 104 | * @return \phpbb\ideas\factory\ideas $this For chaining calls 105 | */ 106 | protected function query_sort($sort, $direction) 107 | { 108 | $sort = strtolower($sort); 109 | $direction = $direction === 'DESC' ? 'DESC' : 'ASC'; 110 | 111 | // Most sorting relies on simple ORDER BY statements, but some may use a WHERE statement 112 | $statements = [ 113 | ext::SORT_DATE => ['ORDER_BY' => 'i.idea_date'], 114 | ext::SORT_TITLE => ['ORDER_BY' => 'i.idea_title'], 115 | ext::SORT_AUTHOR => ['ORDER_BY' => 'i.idea_author'], 116 | ext::SORT_SCORE => ['ORDER_BY' => 'CAST(i.idea_votes_up AS decimal) - CAST(i.idea_votes_down AS decimal)'], 117 | ext::SORT_VOTES => ['ORDER_BY' => 'i.idea_votes_up + i.idea_votes_down'], 118 | ext::SORT_TOP => ['WHERE' => 'i.idea_votes_up > i.idea_votes_down'], 119 | ext::SORT_MYIDEAS => ['ORDER_BY' => 'i.idea_date', 'WHERE' => 'i.idea_author = ' . (int) $this->user->data['user_id']], 120 | ]; 121 | 122 | // Append a new WHERE statement if the sort has one 123 | if (isset($statements[$sort]['WHERE'])) 124 | { 125 | $this->sql['WHERE'][] = $statements[$sort]['WHERE']; 126 | } 127 | 128 | // If we have an ORDER BY we use that. The absence of an ORDER BY 129 | // means we will default to sorting ideas by their calculated score. 130 | if (isset($statements[$sort]['ORDER_BY'])) 131 | { 132 | $this->sql['ORDER_BY'] = "{$statements[$sort]['ORDER_BY']} $direction"; 133 | } 134 | else 135 | { 136 | // https://www.evanmiller.org/how-not-to-sort-by-average-rating.html 137 | // switched SQRT(x) to POWER(x, 0.5) for SQLITE3 support 138 | $this->sql['SELECT'][] = '((i.idea_votes_up + 1.9208) / (i.idea_votes_up + i.idea_votes_down) - 139 | 1.96 * POWER((i.idea_votes_up * i.idea_votes_down) / (i.idea_votes_up + i.idea_votes_down) + 0.9604, 0.5) / 140 | (i.idea_votes_up + i.idea_votes_down)) / (1 + 3.8416 / (i.idea_votes_up + i.idea_votes_down)) 141 | AS ci_lower_bound'; 142 | 143 | $this->sql['ORDER_BY'] = "ci_lower_bound $direction"; 144 | } 145 | 146 | return $this; 147 | } 148 | 149 | /** 150 | * Update $sql property with additional SQL statements to filter ideas 151 | * by status. If $status is given we'll get those ideas. If no $status 152 | * is given, the default is to get all ideas excluding Duplicates, Invalid 153 | * and Implemented statuses (because they are considered done & dusted, 154 | * if they were gases they'd be inert). 155 | * 156 | * @param array|int $status The id(s) of the status(es) to load 157 | * 158 | * @return \phpbb\ideas\factory\ideas $this For chaining calls 159 | */ 160 | protected function query_status($status = []) 161 | { 162 | $this->sql['WHERE'][] = !empty($status) ? $this->db->sql_in_set('i.idea_status', $status) : $this->db->sql_in_set( 163 | 'i.idea_status', [ext::$statuses['IMPLEMENTED'], ext::$statuses['DUPLICATE'], ext::$statuses['INVALID'], 164 | ], true); 165 | 166 | return $this; 167 | } 168 | 169 | /** 170 | * Run a query using the $sql property to get a collection of ideas. 171 | * 172 | * @param int $number The number of ideas to return 173 | * @param int $start Start value for pagination 174 | * 175 | * @return mixed Nested array if the query had rows, false otherwise 176 | * @throws runtime_exception 177 | */ 178 | protected function query_get($number, $start) 179 | { 180 | if (empty($this->sql)) 181 | { 182 | throw new runtime_exception('INVALID_IDEA_QUERY'); 183 | } 184 | 185 | $sql = 'SELECT ' . implode(', ', $this->sql['SELECT']) . ' 186 | FROM ' . $this->sql['FROM'] . ' 187 | INNER JOIN ' . $this->sql['JOIN'] . ' 188 | WHERE ' . implode(' AND ', $this->sql['WHERE']) . ' 189 | ORDER BY ' . $this->sql['ORDER_BY']; 190 | 191 | $result = $this->db->sql_query_limit($sql, $number, $start); 192 | $rows = $this->db->sql_fetchrowset($result); 193 | $this->db->sql_freeresult($result); 194 | 195 | return $rows; 196 | } 197 | 198 | /** 199 | * Run a query using the $sql property to get a count of ideas. 200 | * 201 | * @return int The number of ideas 202 | * @throws runtime_exception 203 | */ 204 | protected function query_count() 205 | { 206 | if (empty($this->sql)) 207 | { 208 | throw new runtime_exception('INVALID_IDEA_QUERY'); 209 | } 210 | 211 | $sql = 'SELECT COUNT(i.idea_id) as count 212 | FROM ' . $this->sql['FROM'] . ' 213 | INNER JOIN ' . $this->sql['JOIN'] . ' 214 | WHERE ' . implode(' AND ', $this->sql['WHERE']); 215 | 216 | $result = $this->db->sql_query($sql); 217 | $count = (int) $this->db->sql_fetchfield('count'); 218 | $this->db->sql_freeresult($result); 219 | 220 | return $count; 221 | } 222 | 223 | /** 224 | * Get a user's votes from a group of ideas 225 | * 226 | * @param int $user_id The user's id 227 | * @param array $ids An array of idea ids 228 | * 229 | * @return array An array of ideas the user voted on and their vote result, or empty otherwise. 230 | * example: [idea_id => vote_result] 231 | * 1 => 1, idea 1, voted up by the user 232 | * 2 => 0, idea 2, voted down by the user 233 | */ 234 | public function get_users_votes($user_id, array $ids) 235 | { 236 | $results = []; 237 | $sql = 'SELECT idea_id, vote_value 238 | FROM ' . $this->table_votes . ' 239 | WHERE user_id = ' . (int) $user_id . ' 240 | AND ' . $this->db->sql_in_set('idea_id', $ids, false, true); 241 | $result = $this->db->sql_query($sql); 242 | while ($row = $this->db->sql_fetchrow($result)) 243 | { 244 | $results[$row['idea_id']] = $row['vote_value']; 245 | } 246 | $this->db->sql_freeresult($result); 247 | 248 | return $results; 249 | } 250 | 251 | /** 252 | * Delete orphaned ideas. Orphaned ideas may exist after a 253 | * topic has been deleted or moved to another forum. 254 | * 255 | * @return int Number of rows affected 256 | */ 257 | public function delete_orphans() 258 | { 259 | // Find any orphans 260 | $sql = 'SELECT idea_id FROM ' . $this->table_ideas . ' 261 | WHERE topic_id NOT IN (SELECT t.topic_id 262 | FROM ' . $this->table_topics . ' t 263 | WHERE t.forum_id = ' . (int) $this->config['ideas_forum_id'] . ')'; 264 | $result = $this->db->sql_query($sql); 265 | $rows = $this->db->sql_fetchrowset($result); 266 | $this->db->sql_freeresult($result); 267 | 268 | if (empty($rows)) 269 | { 270 | return 0; 271 | } 272 | 273 | $this->db->sql_transaction('begin'); 274 | 275 | foreach ($rows as $row) 276 | { 277 | // Delete idea 278 | $this->delete_idea_data($row['idea_id'], $this->table_ideas); 279 | 280 | // Delete votes 281 | $this->delete_idea_data($row['idea_id'], $this->table_votes); 282 | } 283 | 284 | $this->db->sql_transaction('commit'); 285 | 286 | return count($rows); 287 | } 288 | 289 | /** 290 | * Get the stored idea count 291 | * Note: this should only be called after get_ideas() 292 | * 293 | * @return int Count of ideas 294 | */ 295 | public function get_idea_count() 296 | { 297 | return $this->idea_count ?? 0; 298 | } 299 | 300 | /** 301 | * Get statistics - counts of total ideas and of each status type 302 | * 303 | * @return array 304 | */ 305 | public function get_statistics() 306 | { 307 | // the CASE/WHEN SQL approach is better for performance than processing in PHP 308 | $sql = 'SELECT 309 | COUNT(*) as total, 310 | SUM(CASE WHEN idea_status = ' . (int) ext::$statuses['IMPLEMENTED'] . ' THEN 1 ELSE 0 END) as implemented, 311 | SUM(CASE WHEN idea_status = ' . (int) ext::$statuses['IN_PROGRESS'] . ' THEN 1 ELSE 0 END) as in_progress, 312 | SUM(CASE WHEN idea_status = ' . (int) ext::$statuses['DUPLICATE'] . ' THEN 1 ELSE 0 END) as duplicate, 313 | SUM(CASE WHEN idea_status = ' . (int) ext::$statuses['INVALID'] . ' THEN 1 ELSE 0 END) as invalid, 314 | SUM(CASE WHEN idea_status = ' . (int) ext::$statuses['NEW'] . ' THEN 1 ELSE 0 END) as new 315 | FROM ' . $this->table_ideas; 316 | 317 | $result = $this->db->sql_query($sql); 318 | $row = $this->db->sql_fetchrow($result); 319 | $this->db->sql_freeresult($result); 320 | 321 | return [ 322 | 'total' => (int) $row['total'], 323 | 'implemented' => (int) $row['implemented'], 324 | 'in_progress' => (int) $row['in_progress'], 325 | 'duplicate' => (int) $row['duplicate'], 326 | 'invalid' => (int) $row['invalid'], 327 | 'new' => (int) $row['new'], 328 | ]; 329 | } 330 | } 331 | -------------------------------------------------------------------------------- /styles/prosilver/template/ideas.js: -------------------------------------------------------------------------------- 1 | (function($) { // Avoid conflicts with other libraries 2 | 3 | 'use strict'; 4 | 5 | var keymap = { 6 | TAB: 9, 7 | ENTER: 13, 8 | ESC: 27 9 | }, 10 | $obj = { 11 | ideaTitle: $('.topic-title > a'), 12 | duplicateEdit: $('#duplicateedit'), 13 | duplicateEditInput: $('#duplicateeditinput'), 14 | duplicateLink: $('#duplicatelink'), 15 | duplicateToggle: $('.duplicatetoggle'), 16 | ticketEdit: $('#ticketedit'), 17 | ticketEditInput: $('#ticketeditinput'), 18 | ticketLink: $('#ticketlink'), 19 | rfcEdit: $('#rfcedit'), 20 | rfcEditInput: $('#rfceditinput'), 21 | rfcLink: $('#rfclink'), 22 | implementedEdit: $('#implementededit'), 23 | implementedEditInput: $('#implementededitinput'), 24 | implementedVersion: $('#implementedversion'), 25 | implementedToggle: $('.implementedtoggle'), 26 | removeVote: $('.removevote'), 27 | status: $('.change-status'), 28 | statusLink: $('#status-link'), 29 | successVoted: $('.successvoted'), 30 | userVoted: $('.user-voted'), 31 | votes: $('.votes'), 32 | votesList: $('.voteslist'), 33 | voteDown: $('.vote-down'), 34 | voteUp: $('.vote-up'), 35 | voteRemove: $('#vote-remove') 36 | }, $loadingIndicator; 37 | 38 | /** 39 | * @param {Object} $this 40 | * @param {Object} result 41 | * @param {string} result.message 42 | * @param {string} result.points 43 | * @param {Object} result.voters 44 | * @param {string} result.votes_up 45 | * @param {string} result.votes_down 46 | */ 47 | function voteSuccess(result, $this) { 48 | if (typeof result === 'string') { 49 | phpbb.alert($this.attr('data-l-err'), $this.attr('data-l-msg') + ' ' + result); 50 | } else { 51 | $obj.voteUp.find('.vote-count').text(result.votes_up); 52 | $obj.voteDown.find('.vote-count').text(result.votes_down); 53 | $obj.votes.hide().text(function() { 54 | return result.points + ' ' + $(this).attr('data-l-msg'); 55 | }); 56 | $obj.successVoted.text(result.message) 57 | .css('display', 'inline-block') 58 | .delay(2000) 59 | .fadeOut(300, function() { 60 | $obj.votes.fadeIn(300); 61 | }); 62 | displayVoters(result.voters); 63 | } 64 | } 65 | 66 | function voteFailure() { 67 | $obj.votes.hide(); 68 | $obj.successVoted.text(function(){ 69 | return $(this).attr('data-l-err'); 70 | }) 71 | .show() 72 | .delay(2000) 73 | .fadeOut(300, function() { 74 | $obj.votes.fadeIn(300); 75 | }); 76 | } 77 | 78 | $obj.voteUp.add($obj.voteDown).on('click', function(e) { 79 | e.preventDefault(); 80 | 81 | var $this = $(this), 82 | url = $this.attr('href'), 83 | vote = $this.is($obj.voteUp) ? 1 : 0; 84 | 85 | if ($this.hasClass('vote-disabled')) { 86 | return false; 87 | } 88 | 89 | showLoadingIndicator(); 90 | $.get(url, { 91 | v: vote 92 | }, function(data) { 93 | voteSuccess(data, $this); 94 | resetVoteButtons($this); 95 | $obj.voteRemove.show(); 96 | }).fail(voteFailure).always(hideLoadingIndicator); 97 | }); 98 | 99 | $obj.votes.on('click', function(e) { 100 | e.preventDefault(); 101 | 102 | if ($obj.votesList.data('display')) { 103 | $obj.votesList.slideToggle(); 104 | } 105 | }); 106 | 107 | $obj.removeVote.on('click', function(e) { 108 | e.preventDefault(); 109 | 110 | var $this = $(this), 111 | url = $this.attr('href'); 112 | 113 | if ($this.hasClass('vote-disabled')) { 114 | return false; 115 | } 116 | 117 | showLoadingIndicator(); 118 | $.get(url, function(data) { 119 | voteSuccess(data, $this); 120 | resetVoteButtons(); 121 | $obj.voteRemove.hide(); 122 | }).fail(voteFailure).always(hideLoadingIndicator); 123 | }); 124 | 125 | $obj.status.on('click', function(e) { 126 | e.preventDefault(); 127 | 128 | var $this = $(this), 129 | data = { 130 | status: $this.attr('data-status') 131 | }; 132 | 133 | if (!data.status) { 134 | return; 135 | } 136 | 137 | showLoadingIndicator(); 138 | $.get($this.attr('href'), data, function(res) { 139 | if (res) { 140 | var href = $obj.statusLink.attr('href').replace(/status=\d/, 'status=' + data.status); 141 | 142 | $obj.statusLink.attr('href', href) 143 | .html($this.html()) 144 | .closest('span') 145 | .removeClass() 146 | .addClass('status-badge status-' + data.status); 147 | 148 | $obj.duplicateToggle.toggle(data.status === '4'); 149 | $obj.implementedToggle.toggle(data.status === '3'); 150 | } 151 | }).always([ 152 | hideLoadingIndicator, 153 | hideStatusDropDown 154 | ]); 155 | }); 156 | 157 | $obj.rfcEdit.on('click', function(e) { 158 | e.preventDefault(); 159 | 160 | $obj.rfcEdit.add($obj.rfcLink).hide(); 161 | $obj.rfcEditInput.show().trigger('focus'); 162 | }); 163 | 164 | $obj.rfcEditInput.on('keydown', function(e) { 165 | if (e.keyCode === keymap.ENTER) { 166 | e.preventDefault(); 167 | e.stopPropagation(); 168 | 169 | var $this = $(this), 170 | find = /^https?:\/\/area51\.phpbb\.com\/phpBB\/viewtopic\.php/, 171 | url = $obj.rfcEdit.attr('href'), 172 | value = $this.val(); 173 | 174 | if (value && !find.test(value)) { 175 | phpbb.alert($this.attr('data-l-err'), $this.attr('data-l-msg')); 176 | return; 177 | } 178 | 179 | showLoadingIndicator(); 180 | $.get(url, { 181 | rfc: value 182 | }, function(res) { 183 | if (res) { 184 | $obj.rfcLink.text(value) 185 | .attr('href', value); 186 | 187 | if (value) { 188 | $obj.rfcLink.show(); 189 | } 190 | 191 | $this.hide(); 192 | 193 | $obj.rfcEdit.toggleAddEdit(value); 194 | } 195 | }).always(hideLoadingIndicator); 196 | } else if (e.keyCode === keymap.ESC) { 197 | e.preventDefault(); 198 | 199 | var $link = $obj.rfcLink; 200 | 201 | $(this).hide(); 202 | $obj.rfcEdit.show(); 203 | 204 | if ($link.html()) { 205 | $link.show(); 206 | } 207 | } 208 | }); 209 | 210 | $obj.ticketEdit.on('click', function(e) { 211 | e.preventDefault(); 212 | 213 | $obj.ticketEdit.add($obj.ticketLink).hide(); 214 | $obj.ticketEditInput.show().trigger('focus'); 215 | }); 216 | 217 | $obj.ticketEditInput.on('keydown', function(e) { 218 | if (e.keyCode === keymap.ENTER) { 219 | e.preventDefault(); 220 | e.stopPropagation(); 221 | 222 | var $this = $(this), 223 | url = $obj.ticketEdit.attr('href'), 224 | value = $this.val(), 225 | info; 226 | 227 | if (value && !(info = /^PHPBB3?-(\d{1,6})$/.exec(value))) { 228 | phpbb.alert($this.attr('data-l-err'), $this.attr('data-l-msg')); 229 | return; 230 | } 231 | 232 | if (value) { 233 | value = 'PHPBB-' + info[1]; 234 | } 235 | 236 | showLoadingIndicator(); 237 | $.get(url, { 238 | ticket: value && info[1] 239 | }, function(res) { 240 | if (res) { 241 | $obj.ticketLink.text(value) 242 | .attr('href', 'https://tracker.phpbb.com/browse/' + value); 243 | 244 | if (value) { 245 | $obj.ticketLink.show(); 246 | } 247 | 248 | $this.hide(); 249 | 250 | $obj.ticketEdit.toggleAddEdit(value); 251 | } 252 | 253 | }).always(hideLoadingIndicator); 254 | } else if (e.keyCode === keymap.ESC) { 255 | e.preventDefault(); 256 | 257 | var $link = $obj.ticketLink; 258 | 259 | $(this).hide(); 260 | $obj.ticketEdit.show(); 261 | 262 | if ($link.html()) { 263 | $link.show(); 264 | } 265 | } 266 | }); 267 | 268 | $obj.duplicateEdit.on('click', function(e) { 269 | e.preventDefault(); 270 | 271 | $obj.duplicateEdit.add($obj.duplicateLink).hide(); 272 | $obj.duplicateEditInput.show().trigger('focus'); 273 | }); 274 | 275 | /** 276 | * This callback handles live idea title searches for duplicate ideas. 277 | */ 278 | phpbb.addAjaxCallback('idea_search', function(res) { 279 | phpbb.search.handleResponse(res, $(this), false, phpbb.getFunctionByName('phpbb.search.setDuplicateOnEvent')); 280 | }); 281 | 282 | /** 283 | * This performs actions on each result from the live idea title search for duplicate ideas. 284 | * 285 | * @param {jQuery} $input Search input|textarea. 286 | * @param {object} value Result object. 287 | * @param {jQuery} $row Result element. 288 | * @param {jQuery} $container jQuery object for the search container. 289 | */ 290 | phpbb.search.setDuplicateOnEvent = function($input, value, $row, $container) { 291 | $row.on('click', function() { 292 | setDuplicate($input, value); 293 | phpbb.search.closeResults($input, $container); 294 | }); 295 | }; 296 | 297 | /** 298 | * Assign a duplicate idea identifier to a given idea. 299 | * 300 | * @param {jQuery} $input Search input|textarea. 301 | * @param {object} value Result object. 302 | */ 303 | function setDuplicate($input, value) { 304 | if (value.result && isNaN(Number(value.result))) { 305 | phpbb.alert($input.attr('data-l-err'), $input.attr('data-l-msg')); 306 | return; 307 | } 308 | $input.val(value.clean_title); 309 | showLoadingIndicator(); 310 | $.get($obj.duplicateEdit.attr('href'), { 311 | duplicate: Number(value.result) 312 | }, function(res) { 313 | if (res) { 314 | if (value.result) { 315 | $obj.duplicateLink 316 | .text(value.clean_title) 317 | .attr('href', $obj.duplicateLink.attr('data-link').replace(/^(.*\/)(\d+)$/, '$1') + value.result) 318 | .show(); 319 | } else { 320 | $obj.duplicateLink 321 | .empty() 322 | .removeAttr('href'); 323 | } 324 | $input.hide(); 325 | $obj.duplicateEdit.toggleAddEdit(value.result); 326 | } 327 | }).always(hideLoadingIndicator); 328 | } 329 | 330 | /** 331 | * Handling of the duplicate idea input field. 332 | * ENTER: When the input field is empty clear any existing duplicate entry. Otherwise, just show an alert message. 333 | * ESC: Will clear and close the input field (if it isn't cleared, live search may unexpectedly run). 334 | */ 335 | $obj.duplicateEditInput.on('keydown.duplicate', function(e) { 336 | var $this = $(this), 337 | key = e.keyCode || e.which; 338 | if (key === keymap.ESC) { 339 | $this.val('').hide(); 340 | $obj.duplicateEdit.show(); 341 | $obj.duplicateLink.toggle($obj.duplicateLink.html().length !== 0); 342 | } else if (key === keymap.ENTER) { 343 | if ($this.val().length === 0) { 344 | setDuplicate($this, { 345 | 'result': '', 346 | 'clean_title': '' 347 | }); 348 | } else { 349 | e.stopPropagation(); 350 | phpbb.alert($this.attr('data-l-err'), $this.attr('data-l-msg')); 351 | } 352 | } 353 | }); 354 | 355 | $obj.implementedEdit.on('click', function(e) { 356 | e.preventDefault(); 357 | 358 | $obj.implementedEdit.add($obj.implementedVersion).hide(); 359 | $obj.implementedEditInput.show().trigger('focus'); 360 | }); 361 | 362 | $obj.implementedEditInput.on('keydown', function(e) { 363 | if (e.keyCode === keymap.ENTER) { 364 | e.preventDefault(); 365 | e.stopPropagation(); 366 | 367 | var $this = $(this), 368 | find = /^\d\.\d\.\d+(-\w+)?$/, 369 | url = $obj.implementedEdit.attr('href'), 370 | value = $this.val(); 371 | 372 | if (value && !find.test(value)) { 373 | phpbb.alert($this.attr('data-l-err'), $this.attr('data-l-msg')); 374 | return; 375 | } 376 | 377 | showLoadingIndicator(); 378 | $.get(url, { 379 | implemented: value 380 | }, function(res) { 381 | if (res) { 382 | $obj.implementedVersion.text(value); 383 | 384 | if (value) { 385 | $obj.implementedVersion.show(); 386 | } 387 | 388 | $this.hide(); 389 | 390 | $obj.implementedEdit.toggleAddEdit(value); 391 | } 392 | }).always(hideLoadingIndicator); 393 | } else if (e.keyCode === keymap.ESC) { 394 | e.preventDefault(); 395 | 396 | $(this).hide(); 397 | $obj.implementedEdit.show(); 398 | 399 | if ($obj.implementedVersion.text()) { 400 | $obj.implementedVersion.show(); 401 | } 402 | } 403 | }); 404 | 405 | $.fn.toggleAddEdit = function(value) { 406 | $(this).text(function() { 407 | return value ? $(this).attr('data-l-edit') : $(this).attr('data-l-add'); 408 | }).prepend($('').addClass(function() { 409 | return value ? 'fa-pencil' : 'fa-plus-circle'; 410 | })).show(); 411 | }; 412 | 413 | function hideStatusDropDown() { 414 | $('.status-dropdown').hide(); 415 | } 416 | 417 | /** 418 | * @param {Object} data 419 | * @param {number} data.length 420 | * @param {string} data.user 421 | * @param {string} data.vote_value 422 | */ 423 | function displayVoters(data) { 424 | 425 | var upVoters = [], 426 | downVoters = []; 427 | 428 | for (var i = 0; i < data.length; i++) { 429 | if (data[i].vote_value === '1') { 430 | upVoters.push(data[i].user); 431 | } else if (data[i].vote_value === '0') { 432 | downVoters.push(data[i].user); 433 | } 434 | } 435 | 436 | var hasUpVotes = upVoters.length > 0, 437 | hasDownVotes = downVoters.length > 0; 438 | 439 | $('#up-voters') 440 | .toggle(hasUpVotes) 441 | .find('span') 442 | .html(upVoters.join(', ')); 443 | $('#down-voters') 444 | .toggle(hasDownVotes) 445 | .find('span') 446 | .html(downVoters.join(', ')); 447 | 448 | $obj.votesList 449 | .attr('data-display', (hasUpVotes || hasDownVotes)) 450 | .toggle(($obj.votesList.is(':visible') && (hasUpVotes || hasDownVotes))); 451 | } 452 | 453 | function resetVoteButtons($this) { 454 | $obj.voteUp.add($obj.voteDown).removeClass('vote-disabled'); 455 | $obj.userVoted.hide(); 456 | 457 | if ($this) { 458 | $this.addClass('vote-disabled').find($obj.userVoted).show(); 459 | } 460 | } 461 | 462 | function showLoadingIndicator() { 463 | $loadingIndicator = phpbb.loadingIndicator(); 464 | } 465 | 466 | function hideLoadingIndicator() { 467 | if ($loadingIndicator && $loadingIndicator.is(':visible')) { 468 | $loadingIndicator.fadeOut(phpbb.alertTime); 469 | } 470 | } 471 | 472 | })(jQuery); // Avoid conflicts with other libraries 473 | -------------------------------------------------------------------------------- /factory/idea.php: -------------------------------------------------------------------------------- 1 | 7 | * @license GNU General Public License, version 2 (GPL-2.0) 8 | * 9 | */ 10 | 11 | namespace phpbb\ideas\factory; 12 | 13 | use phpbb\ideas\ext; 14 | 15 | /** 16 | * Class for handling a single idea 17 | */ 18 | class idea extends base 19 | { 20 | /** 21 | * Returns the specified idea. 22 | * 23 | * @param int $id The ID of the idea to return. 24 | * 25 | * @return array|false The idea row set, or false if not found. 26 | */ 27 | public function get_idea($id) 28 | { 29 | $sql = 'SELECT * 30 | FROM ' . $this->table_ideas . ' 31 | WHERE idea_id = ' . (int) $id; 32 | $result = $this->db->sql_query_limit($sql, 1); 33 | $row = $this->db->sql_fetchrow($result); 34 | $this->db->sql_freeresult($result); 35 | 36 | return $row; 37 | } 38 | 39 | /** 40 | * Returns an idea specified by its topic ID. 41 | * 42 | * @param int $id The ID of the idea to return. 43 | * 44 | * @return array|false The idea row set, or false if not found. 45 | */ 46 | public function get_idea_by_topic_id($id) 47 | { 48 | $sql = 'SELECT idea_id 49 | FROM ' . $this->table_ideas . ' 50 | WHERE topic_id = ' . (int) $id; 51 | $result = $this->db->sql_query_limit($sql, 1); 52 | $idea_id = (int) $this->db->sql_fetchfield('idea_id'); 53 | $this->db->sql_freeresult($result); 54 | 55 | return $this->get_idea($idea_id); 56 | } 57 | 58 | /** 59 | * Updates the status of an idea. 60 | * 61 | * @param int $idea_id The ID of the idea. 62 | * @param int $status The ID of the status. 63 | * 64 | * @return void 65 | */ 66 | public function set_status($idea_id, $status) 67 | { 68 | $sql_ary = array( 69 | 'idea_status' => (int) $status, 70 | ); 71 | 72 | $this->update_idea_data($sql_ary, $idea_id, $this->table_ideas); 73 | 74 | // Send a notification 75 | $idea = $this->get_idea($idea_id); 76 | $notifications = $this->notification_manager->get_notified_users(ext::NOTIFICATION_TYPE_STATUS, ['item_id' => (int) $idea_id]); 77 | $this->notification_manager->{empty($notifications) ? 'add_notifications' : 'update_notifications'}( 78 | ext::NOTIFICATION_TYPE_STATUS, 79 | [ 80 | 'idea_id' => (int) $idea_id, 81 | 'status' => (int) $status, 82 | 'user_id' => (int) $this->user->data['user_id'], 83 | 'idea_author' => (int) $idea['idea_author'], 84 | 'idea_title' => $idea['idea_title'], 85 | ] 86 | ); 87 | } 88 | 89 | /** 90 | * Sets the ID of the duplicate for an idea. 91 | * 92 | * @param int $idea_id ID of the idea to be updated. 93 | * @param string $duplicate Idea ID of duplicate. 94 | * 95 | * @return bool True if set, false if invalid. 96 | */ 97 | public function set_duplicate($idea_id, $duplicate) 98 | { 99 | if ($duplicate && !is_numeric($duplicate)) 100 | { 101 | return false; 102 | } 103 | 104 | $sql_ary = array( 105 | 'duplicate_id' => (int) $duplicate, 106 | ); 107 | 108 | $this->update_idea_data($sql_ary, $idea_id, $this->table_ideas); 109 | 110 | return true; 111 | } 112 | 113 | /** 114 | * Sets the RFC link of an idea. 115 | * 116 | * @param int $idea_id ID of the idea to be updated. 117 | * @param string $rfc Link to the RFC. 118 | * 119 | * @return bool True if set, false if invalid. 120 | */ 121 | public function set_rfc($idea_id, $rfc) 122 | { 123 | $match = '/^https?:\/\/area51\.phpbb\.com\/phpBB\/viewtopic\.php/'; 124 | if ($rfc && !preg_match($match, $rfc)) 125 | { 126 | return false; 127 | } 128 | 129 | $sql_ary = array( 130 | 'rfc_link' => $rfc, // string is escaped by build_array() 131 | ); 132 | 133 | $this->update_idea_data($sql_ary, $idea_id, $this->table_ideas); 134 | 135 | return true; 136 | } 137 | 138 | /** 139 | * Sets the ticket ID of an idea. 140 | * 141 | * @param int $idea_id ID of the idea to be updated. 142 | * @param string $ticket Ticket ID. 143 | * 144 | * @return bool True if set, false if invalid. 145 | */ 146 | public function set_ticket($idea_id, $ticket) 147 | { 148 | if ($ticket && !is_numeric($ticket)) 149 | { 150 | return false; 151 | } 152 | 153 | $sql_ary = array( 154 | 'ticket_id' => (int) $ticket, 155 | ); 156 | 157 | $this->update_idea_data($sql_ary, $idea_id, $this->table_ideas); 158 | 159 | return true; 160 | } 161 | 162 | /** 163 | * Sets the implemented version of an idea. 164 | * 165 | * @param int $idea_id ID of the idea to be updated. 166 | * @param string $version Version of phpBB the idea was implemented in. 167 | * 168 | * @return bool True if set, false if invalid. 169 | */ 170 | public function set_implemented($idea_id, $version) 171 | { 172 | $match = '/^\d\.\d\.\d+(-\w+)?$/'; 173 | if ($version && !preg_match($match, $version)) 174 | { 175 | return false; 176 | } 177 | 178 | $sql_ary = array( 179 | 'implemented_version' => $version, // string is escaped by build_array() 180 | ); 181 | 182 | $this->update_idea_data($sql_ary, $idea_id, $this->table_ideas); 183 | 184 | return true; 185 | } 186 | 187 | /** 188 | * Sets the title of an idea. 189 | * 190 | * @param int $idea_id ID of the idea to be updated. 191 | * @param string $title New title. 192 | * 193 | * @return boolean True if updated, false if invalid length. 194 | */ 195 | public function set_title($idea_id, $title) 196 | { 197 | if (utf8_clean_string($title) === '') 198 | { 199 | return false; 200 | } 201 | 202 | $sql_ary = array( 203 | 'idea_title' => truncate_string($title, ext::SUBJECT_LENGTH), 204 | ); 205 | 206 | $this->update_idea_data($sql_ary, $idea_id, $this->table_ideas); 207 | 208 | return true; 209 | } 210 | 211 | /** 212 | * Get the title of an idea. 213 | * 214 | * @param int $id ID of an idea 215 | * 216 | * @return string The idea's title, empty string if not found 217 | */ 218 | public function get_title($id) 219 | { 220 | $sql = 'SELECT idea_title 221 | FROM ' . $this->table_ideas . ' 222 | WHERE idea_id = ' . (int) $id; 223 | $result = $this->db->sql_query_limit($sql, 1); 224 | $idea_title = $this->db->sql_fetchfield('idea_title'); 225 | $this->db->sql_freeresult($result); 226 | 227 | return $idea_title ?: ''; 228 | } 229 | 230 | /** 231 | * Set the author of an idea 232 | * 233 | * @param int $idea_id 234 | * @param int $user_id 235 | * @return bool True if set, false if invalid. 236 | */ 237 | public function set_author($idea_id, $user_id) 238 | { 239 | if (!$user_id || !is_numeric($user_id)) 240 | { 241 | return false; 242 | } 243 | 244 | $sql_ary = array( 245 | 'idea_author' => (int) $user_id, 246 | ); 247 | 248 | $this->update_idea_data($sql_ary, $idea_id, $this->table_ideas); 249 | 250 | return true; 251 | } 252 | 253 | /** 254 | * Submit new idea data to the ideas table 255 | * 256 | * @param array $data An array of post data from a newly posted idea 257 | * 258 | * @return int The ID of the new idea. 259 | */ 260 | public function submit($data) 261 | { 262 | $sql_ary = [ 263 | 'idea_title' => $data['topic_title'], 264 | 'idea_author' => $data['poster_id'], 265 | 'idea_date' => $data['post_time'], 266 | 'topic_id' => $data['topic_id'], 267 | ]; 268 | 269 | $idea_id = $this->insert_idea_data($sql_ary, $this->table_ideas); 270 | 271 | // Initial vote 272 | if (($idea = $this->get_idea($idea_id)) !== false) 273 | { 274 | $this->vote($idea, $data['poster_id'], 1); 275 | } 276 | 277 | return $idea_id; 278 | } 279 | 280 | /** 281 | * Deletes an idea and the topic to go with it. 282 | * 283 | * @param int $id The ID of the idea to be deleted. 284 | * @param int $topic_id The ID of the idea topic. Optional, but preferred. 285 | * 286 | * @return boolean Whether the idea was deleted or not. 287 | */ 288 | public function delete($id, $topic_id = 0) 289 | { 290 | if (!$topic_id) 291 | { 292 | $idea = $this->get_idea($id); 293 | $topic_id = $idea ? $idea['topic_id'] : 0; 294 | } 295 | 296 | // Delete topic 297 | delete_posts('topic_id', $topic_id); 298 | 299 | // Delete idea 300 | $deleted = $this->delete_idea_data($id, $this->table_ideas); 301 | 302 | if ($deleted) 303 | { 304 | // Delete votes 305 | $this->delete_idea_data($id, $this->table_votes); 306 | 307 | // Delete notifications 308 | $this->notification_manager->delete_notifications(ext::NOTIFICATION_TYPE_STATUS, $id); 309 | } 310 | 311 | return $deleted; 312 | } 313 | 314 | /** 315 | * Submits a vote on an idea. 316 | * 317 | * @param array $idea The idea returned by get_idea(). 318 | * @param int $user_id The ID of the user voting. 319 | * @param int $value Up (1) or down (0)? 320 | * 321 | * @return array|string Array of information or string on error. 322 | */ 323 | public function vote(&$idea, $user_id, $value) 324 | { 325 | // Validate $vote - must be 0 or 1 326 | if ($value !== 0 && $value !== 1) 327 | { 328 | return 'INVALID_VOTE'; 329 | } 330 | 331 | // Check whether user has already voted - update if they have 332 | if ($row = $this->get_users_vote($idea['idea_id'], $user_id)) 333 | { 334 | if ($row['vote_value'] != $value) 335 | { 336 | $sql = 'UPDATE ' . $this->table_votes . ' 337 | SET vote_value = ' . $value . ' 338 | WHERE user_id = ' . (int) $user_id . ' 339 | AND idea_id = ' . (int) $idea['idea_id']; 340 | $this->db->sql_query($sql); 341 | 342 | if ($value == 1) 343 | { 344 | // Change to upvote 345 | $idea['idea_votes_up']++; 346 | $idea['idea_votes_down']--; 347 | } 348 | else 349 | { 350 | // Change to downvote 351 | $idea['idea_votes_up']--; 352 | $idea['idea_votes_down']++; 353 | } 354 | 355 | $sql_ary = array( 356 | 'idea_votes_up' => $idea['idea_votes_up'], 357 | 'idea_votes_down' => $idea['idea_votes_down'], 358 | ); 359 | 360 | $this->update_idea_data($sql_ary, $idea['idea_id'], $this->table_ideas); 361 | } 362 | 363 | return array( 364 | 'message' => $this->language->lang('UPDATED_VOTE'), 365 | 'votes_up' => $idea['idea_votes_up'], 366 | 'votes_down' => $idea['idea_votes_down'], 367 | 'points' => $this->language->lang('TOTAL_POINTS', $idea['idea_votes_up'] - $idea['idea_votes_down']), 368 | 'voters' => $this->get_voters($idea['idea_id']), 369 | ); 370 | } 371 | 372 | // Insert vote into votes table. 373 | $sql_ary = array( 374 | 'idea_id' => (int) $idea['idea_id'], 375 | 'user_id' => (int) $user_id, 376 | 'vote_value' => (int) $value, 377 | ); 378 | 379 | $this->insert_idea_data($sql_ary, $this->table_votes); 380 | 381 | // Update number of votes in ideas table 382 | $idea['idea_votes_' . ($value ? 'up' : 'down')]++; 383 | 384 | $sql_ary = array( 385 | 'idea_votes_up' => $idea['idea_votes_up'], 386 | 'idea_votes_down' => $idea['idea_votes_down'], 387 | ); 388 | 389 | $this->update_idea_data($sql_ary, $idea['idea_id'], $this->table_ideas); 390 | 391 | return array( 392 | 'message' => $this->language->lang('VOTE_SUCCESS'), 393 | 'votes_up' => $idea['idea_votes_up'], 394 | 'votes_down' => $idea['idea_votes_down'], 395 | 'points' => $this->language->lang('TOTAL_POINTS', $idea['idea_votes_up'] - $idea['idea_votes_down']), 396 | 'voters' => $this->get_voters($idea['idea_id']), 397 | ); 398 | } 399 | 400 | /** 401 | * Remove a user's vote from an idea 402 | * 403 | * @param array $idea The idea returned by get_idea(). 404 | * @param int $user_id The ID of the user voting. 405 | * 406 | * @return array Array of information. 407 | */ 408 | public function remove_vote(&$idea, $user_id) 409 | { 410 | // Only change something if user has already voted 411 | if ($row = $this->get_users_vote($idea['idea_id'], $user_id)) 412 | { 413 | $sql = 'DELETE FROM ' . $this->table_votes . ' 414 | WHERE idea_id = ' . (int) $idea['idea_id'] . ' 415 | AND user_id = ' . (int) $user_id; 416 | $this->db->sql_query($sql); 417 | 418 | $idea['idea_votes_' . ($row['vote_value'] == 1 ? 'up' : 'down')]--; 419 | 420 | $sql_ary = array( 421 | 'idea_votes_up' => $idea['idea_votes_up'], 422 | 'idea_votes_down' => $idea['idea_votes_down'], 423 | ); 424 | 425 | $this->update_idea_data($sql_ary, $idea['idea_id'], $this->table_ideas); 426 | } 427 | 428 | return array( 429 | 'message' => $this->language->lang('UPDATED_VOTE'), 430 | 'votes_up' => $idea['idea_votes_up'], 431 | 'votes_down' => $idea['idea_votes_down'], 432 | 'points' => $this->language->lang('TOTAL_POINTS', $idea['idea_votes_up'] - $idea['idea_votes_down']), 433 | 'voters' => $this->get_voters($idea['idea_id']), 434 | ); 435 | } 436 | 437 | /** 438 | * Returns voter info on an idea. 439 | * 440 | * @param int $id ID of the idea. 441 | * 442 | * @return array Array of row data 443 | */ 444 | public function get_voters($id) 445 | { 446 | $sql = 'SELECT iv.user_id, iv.vote_value, u.username, u.user_colour 447 | FROM ' . $this->table_votes . ' as iv, 448 | ' . USERS_TABLE . ' as u 449 | WHERE iv.idea_id = ' . (int) $id . ' 450 | AND iv.user_id = u.user_id 451 | ORDER BY u.username ASC'; 452 | $result = $this->db->sql_query($sql); 453 | $rows = $this->db->sql_fetchrowset($result); 454 | $this->db->sql_freeresult($result); 455 | 456 | // Process the username for the template now, so it is 457 | // ready to use in AJAX responses and DOM injections. 458 | $profile_url = append_sid(generate_board_url() . "/memberlist.$this->php_ext", array('mode' => 'viewprofile')); 459 | foreach ($rows as &$row) 460 | { 461 | $row['user'] = get_username_string('full', $row['user_id'], $row['username'], $row['user_colour'], false, $profile_url); 462 | } 463 | 464 | return $rows; 465 | } 466 | 467 | /** 468 | * Get a user's stored vote value for a given idea 469 | * 470 | * @param int $idea_id The idea id 471 | * @param int $user_id The user id 472 | * @return mixed Array with the row data, false if the row does not exist 473 | */ 474 | protected function get_users_vote($idea_id, $user_id) 475 | { 476 | $sql = 'SELECT idea_id, vote_value 477 | FROM ' . $this->table_votes . ' 478 | WHERE idea_id = ' . (int) $idea_id . ' 479 | AND user_id = ' . (int) $user_id; 480 | $result = $this->db->sql_query_limit($sql, 1); 481 | $row = $this->db->sql_fetchrow(); 482 | $this->db->sql_freeresult($result); 483 | 484 | return $row; 485 | } 486 | } 487 | -------------------------------------------------------------------------------- /event/listener.php: -------------------------------------------------------------------------------- 1 | 7 | * @license GNU General Public License, version 2 (GPL-2.0) 8 | * 9 | */ 10 | 11 | namespace phpbb\ideas\event; 12 | 13 | use phpbb\auth\auth; 14 | use phpbb\config\config; 15 | use phpbb\controller\helper; 16 | use phpbb\ideas\ext; 17 | use phpbb\ideas\factory\idea; 18 | use phpbb\ideas\factory\linkhelper; 19 | use phpbb\language\language; 20 | use phpbb\template\template; 21 | use phpbb\user; 22 | use Symfony\Component\EventDispatcher\EventSubscriberInterface; 23 | 24 | class listener implements EventSubscriberInterface 25 | { 26 | /** @var auth */ 27 | protected $auth; 28 | 29 | /* @var config */ 30 | protected $config; 31 | 32 | /* @var helper */ 33 | protected $helper; 34 | 35 | /* @var idea */ 36 | protected $idea; 37 | 38 | /** @var language */ 39 | protected $language; 40 | 41 | /* @var linkhelper */ 42 | protected $link_helper; 43 | 44 | /* @var template */ 45 | protected $template; 46 | 47 | /* @var user */ 48 | protected $user; 49 | 50 | /** @var string */ 51 | protected $php_ext; 52 | 53 | /** 54 | * @param auth $auth 55 | * @param config $config 56 | * @param helper $helper 57 | * @param idea $idea 58 | * @param language $language 59 | * @param linkhelper $link_helper 60 | * @param template $template 61 | * @param user $user 62 | * @param string $php_ext 63 | */ 64 | public function __construct(auth $auth, config $config, helper $helper, idea $idea, language $language, linkhelper $link_helper, template $template, user $user, $php_ext) 65 | { 66 | $this->auth = $auth; 67 | $this->config = $config; 68 | $this->helper = $helper; 69 | $this->idea = $idea; 70 | $this->language = $language; 71 | $this->link_helper = $link_helper; 72 | $this->template = $template; 73 | $this->user = $user; 74 | $this->php_ext = $php_ext; 75 | 76 | $this->language->add_lang('common', 'phpbb/ideas'); 77 | } 78 | 79 | /** 80 | * @inheritDoc 81 | */ 82 | public static function getSubscribedEvents() 83 | { 84 | return array( 85 | 'core.page_header' => 'global_template_vars', 86 | 'core.viewforum_get_topic_data' => 'ideas_forum_redirect', 87 | 'core.viewtopic_modify_post_row' => 'show_post_buttons', 88 | 'core.viewtopic_modify_page_title' => 'show_idea', 89 | 'core.viewtopic_add_quickmod_option_before' => 'adjust_quickmod_tools', 90 | 'core.viewonline_overwrite_location' => 'viewonline_ideas', 91 | 'core.posting_modify_template_vars' => 'submit_idea_template', 92 | 'core.posting_modify_submit_post_before' => 'submit_idea_before', 93 | 'core.posting_modify_submit_post_after' => [['submit_idea_after'], ['edit_idea_title']], 94 | 'core.mcp_change_poster_after' => 'change_idea_author', 95 | ); 96 | } 97 | 98 | /** 99 | * Assign global template variables 100 | * 101 | * @return void 102 | */ 103 | public function global_template_vars() 104 | { 105 | if ($this->user->data['is_registered'] && !$this->user->data['is_bot']) 106 | { 107 | $this->template->assign_var( 108 | 'U_SEARCH_MY_IDEAS', 109 | $this->helper->route('phpbb_ideas_list_controller', ['sort' => ext::SORT_MYIDEAS, 'status' => '-1']) 110 | ); 111 | } 112 | } 113 | 114 | /** 115 | * Redirect users from the forum to the Ideas centre 116 | * 117 | * @param \phpbb\event\data $event The event object 118 | * @return void 119 | * @access public 120 | */ 121 | public function ideas_forum_redirect($event) 122 | { 123 | if ($this->is_ideas_forum($event['forum_id'])) 124 | { 125 | redirect($this->helper->route('phpbb_ideas_index_controller')); 126 | } 127 | } 128 | 129 | /** 130 | * Show post buttons (hide delete, quote or warn user buttons) 131 | * 132 | * @param \phpbb\event\data $event The event object 133 | * @return void 134 | * @access public 135 | */ 136 | public function show_post_buttons($event) 137 | { 138 | if (!$this->is_ideas_forum($event['row']['forum_id'])) 139 | { 140 | return; 141 | } 142 | 143 | if ($this->is_first_post($event['topic_data']['topic_first_post_id'], $event['row']['post_id'])) 144 | { 145 | $event->update_subarray('post_row', 'U_DELETE', false); 146 | $event->update_subarray('post_row', 'U_WARN', false); 147 | } 148 | } 149 | 150 | /** 151 | * Show the idea related to the current topic 152 | * 153 | * @param \phpbb\event\data $event The event object 154 | * @return void 155 | * @access public 156 | */ 157 | public function show_idea($event) 158 | { 159 | if (!$this->is_ideas_forum($event['forum_id'])) 160 | { 161 | return; 162 | } 163 | 164 | $idea = $this->idea->get_idea_by_topic_id($event['topic_data']['topic_id']); 165 | 166 | if (!$idea) 167 | { 168 | return; 169 | } 170 | 171 | $mod = $this->auth->acl_get('m_', (int) $this->config['ideas_forum_id']); 172 | $own = $idea['idea_author'] === $this->user->data['user_id']; 173 | 174 | if ($mod) 175 | { 176 | $this->template->assign_var('STATUS_ARY', ext::$statuses); 177 | 178 | // Add quick mod option for deleting an idea 179 | $this->template->alter_block_array('quickmod', array( 180 | 'VALUE' => 'delete_topic', // delete topic is used here simply to enable ajax 181 | 'TITLE' => $this->language->lang('DELETE_IDEA'), 182 | 'LINK' => $this->link_helper->get_idea_link($idea['idea_id'], 'delete'), 183 | )); 184 | } 185 | 186 | $points = $idea['idea_votes_up'] - $idea['idea_votes_down']; 187 | $can_vote = ($idea['idea_status'] != ext::$statuses['IMPLEMENTED'] && 188 | $idea['idea_status'] != ext::$statuses['DUPLICATE'] && 189 | $this->auth->acl_get('f_vote', (int) $this->config['ideas_forum_id']) && 190 | $event['topic_data']['topic_status'] != ITEM_LOCKED); 191 | 192 | $s_voted_up = $s_voted_down = false; 193 | if ($idea['idea_votes_up'] || $idea['idea_votes_down']) 194 | { 195 | $votes = $this->idea->get_voters($idea['idea_id']); 196 | 197 | foreach ($votes as $vote) 198 | { 199 | $this->template->assign_block_vars('votes_' . ($vote['vote_value'] ? 'up' : 'down'), array( 200 | 'USER' => $vote['user'], 201 | )); 202 | 203 | if ($this->user->data['user_id'] == $vote['user_id']) 204 | { 205 | $s_voted_up = ((int) $vote['vote_value'] === 1); 206 | $s_voted_down = ((int) $vote['vote_value'] === 0); 207 | } 208 | } 209 | } 210 | 211 | $this->template->assign_vars(array( 212 | 'IDEA_ID' => $idea['idea_id'], 213 | 'IDEA_TITLE' => $idea['idea_title'], 214 | 'IDEA_VOTES' => $idea['idea_votes_up'] + $idea['idea_votes_down'], 215 | 'IDEA_VOTES_UP' => $idea['idea_votes_up'], 216 | 'IDEA_VOTES_DOWN' => $idea['idea_votes_down'], 217 | 'IDEA_POINTS' => $points, 218 | 'IDEA_STATUS_ID' => $idea['idea_status'], 219 | 'IDEA_STATUS_NAME' => $this->language->lang(ext::status_name($idea['idea_status'])), 220 | 221 | 'IDEA_DUPLICATE' => $idea['duplicate_id'] ? $this->idea->get_title($idea['duplicate_id']) : '', 222 | 'IDEA_RFC' => $idea['rfc_link'], 223 | 'IDEA_TICKET' => $idea['ticket_id'], 224 | 'IDEA_IMPLEMENTED' => $idea['implemented_version'], 225 | 226 | 'S_IS_MOD' => $mod, 227 | 'S_CAN_EDIT' => $mod || $own, 228 | 'S_CAN_VOTE' => $can_vote, 229 | 'S_CAN_VOTE_UP' => $can_vote && !$s_voted_up, 230 | 'S_CAN_VOTE_DOWN' => $can_vote && !$s_voted_down, 231 | 'S_VOTED' => $s_voted_up || $s_voted_down, 232 | 'S_VOTED_UP' => $s_voted_up, 233 | 'S_VOTED_DOWN' => $s_voted_down, 234 | 235 | 'U_CHANGE_STATUS' => $this->link_helper->get_idea_link($idea['idea_id'], 'status', true), 236 | 'U_EDIT_DUPLICATE' => $this->link_helper->get_idea_link($idea['idea_id'], 'duplicate', true), 237 | 'U_EDIT_RFC' => $this->link_helper->get_idea_link($idea['idea_id'], 'rfc', true), 238 | 'U_EDIT_IMPLEMENTED'=> $this->link_helper->get_idea_link($idea['idea_id'], 'implemented', true), 239 | 'U_EDIT_TICKET' => $this->link_helper->get_idea_link($idea['idea_id'], 'ticket', true), 240 | 'U_REMOVE_VOTE' => $this->link_helper->get_idea_link($idea['idea_id'], 'removevote', true), 241 | 'U_IDEA_VOTE' => $this->link_helper->get_idea_link($idea['idea_id'], 'vote', true), 242 | 'U_IDEA_DUPLICATE' => $this->link_helper->get_idea_link($idea['duplicate_id']), 243 | 'U_IDEA_STATUS_LINK'=> $this->helper->route('phpbb_ideas_list_controller', ['status' => $idea['idea_status']]), 244 | 'U_TITLE_LIVESEARCH'=> $this->helper->route('phpbb_ideas_livesearch_controller'), 245 | )); 246 | 247 | // Use Ideas breadcrumbs 248 | $this->template->destroy_block_vars('navlinks'); 249 | $this->template->assign_block_vars('navlinks', array( 250 | 'U_VIEW_FORUM' => $this->helper->route('phpbb_ideas_index_controller'), 251 | 'FORUM_NAME' => $this->language->lang('IDEAS'), 252 | )); 253 | } 254 | 255 | /** 256 | * Adjust the QuickMod tools displayed 257 | * (hide options to delete, restore, make global, sticky or announcement) 258 | * 259 | * @param \phpbb\event\data $event The event object 260 | * @return void 261 | * @access public 262 | */ 263 | public function adjust_quickmod_tools($event) 264 | { 265 | if (!$this->is_ideas_forum($event['forum_id'])) 266 | { 267 | return; 268 | } 269 | 270 | $quickmod_array = $event['quickmod_array']; 271 | 272 | //$quickmod_array['lock'][1] = false; 273 | //$quickmod_array['unlock'][1] = false; 274 | $quickmod_array['delete_topic'][1] = false; 275 | $quickmod_array['restore_topic'][1] = false; 276 | //$quickmod_array['move'][1] = false; 277 | //$quickmod_array['split'][1] = false; 278 | //$quickmod_array['merge'][1] = false; 279 | //$quickmod_array['merge_topic'][1] = false; 280 | //$quickmod_array['fork'][1] = false; 281 | $quickmod_array['make_normal'][1] = false; 282 | $quickmod_array['make_sticky'][1] = false; 283 | $quickmod_array['make_announce'][1] = false; 284 | $quickmod_array['make_global'][1] = false; 285 | 286 | $event['quickmod_array'] = $quickmod_array; 287 | } 288 | 289 | /** 290 | * Show users as viewing Ideas on Who Is Online page 291 | * 292 | * @param \phpbb\event\data $event The event object 293 | * @return void 294 | * @access public 295 | */ 296 | public function viewonline_ideas($event) 297 | { 298 | if (($event['on_page'][1] === 'viewtopic' && $event['row']['session_forum_id'] == $this->config['ideas_forum_id']) || 299 | ($event['on_page'][1] === 'app' && strrpos($event['row']['session_page'], 'app.' . $this->php_ext . '/ideas') === 0)) 300 | { 301 | $event['location'] = $this->language->lang('VIEWING_IDEAS'); 302 | $event['location_url'] = $this->helper->route('phpbb_ideas_index_controller'); 303 | } 304 | } 305 | 306 | /** 307 | * Modify the Ideas forum's posting page 308 | * 309 | * @param \phpbb\event\data $event The event object 310 | */ 311 | public function submit_idea_template($event) 312 | { 313 | if (!$this->is_ideas_forum($event['forum_id'])) 314 | { 315 | return; 316 | } 317 | 318 | // Alter some posting page template vars 319 | if ($event['mode'] === 'post') 320 | { 321 | $event['page_title'] = $this->language->lang('POST_IDEA'); 322 | $event->update_subarray('page_data', 'L_POST_A', $this->language->lang('POST_IDEA')); 323 | $event->update_subarray('page_data', 'U_VIEW_FORUM', $this->helper->route('phpbb_ideas_index_controller')); 324 | } 325 | 326 | // Alter posting page breadcrumbs to link to the ideas controller 327 | $this->template->alter_block_array('navlinks', [ 328 | 'U_BREADCRUMB' => $this->helper->route('phpbb_ideas_index_controller'), 329 | 'BREADCRUMB_NAME' => $this->language->lang('IDEAS'), 330 | ], false, 'change'); 331 | } 332 | 333 | /** 334 | * Prepare post data vars before posting a new idea/topic. 335 | * 336 | * @param \phpbb\event\data $event The event object 337 | */ 338 | public function submit_idea_before($event) 339 | { 340 | if (!$this->is_post_idea($event['mode'], $event['data']['forum_id'], empty($event['data']['topic_id']))) 341 | { 342 | return; 343 | } 344 | 345 | // We need $post_time after submit_post(), but it's not available in the post $data, unless we set it now 346 | $event->update_subarray('data', 'post_time', time()); 347 | } 348 | 349 | /** 350 | * Submit the idea data after posting a new idea/topic. 351 | * 352 | * @param \phpbb\event\data $event The event object 353 | */ 354 | public function submit_idea_after($event) 355 | { 356 | if (!$this->is_post_idea($event['mode'], $event['data']['forum_id'], !empty($event['data']['topic_id']))) 357 | { 358 | return; 359 | } 360 | 361 | $this->idea->submit($event['data']); 362 | 363 | // Show users whose posts need approval a special message 364 | if (!$this->auth->acl_get('f_noapprove', $event['data']['forum_id'])) 365 | { 366 | // Using refresh and trigger error because we can't throw http_exceptions from posting.php 367 | $url = $this->helper->route('phpbb_ideas_index_controller'); 368 | meta_refresh(10, $url); 369 | trigger_error($this->language->lang('IDEA_STORED_MOD', $url)); 370 | } 371 | } 372 | 373 | /** 374 | * Update the idea's title when post title is edited. 375 | * 376 | * @param \phpbb\event\data $event The event object 377 | * @return void 378 | * @access public 379 | */ 380 | public function edit_idea_title($event) 381 | { 382 | if ($event['mode'] !== 'edit' || 383 | !$event['update_subject'] || 384 | !$this->is_ideas_forum($event['forum_id']) || 385 | !$this->is_first_post($event['post_data']['topic_first_post_id'], $event['post_id'])) 386 | { 387 | return; 388 | } 389 | 390 | $idea = $this->idea->get_idea_by_topic_id($event['topic_id']); 391 | $this->idea->set_title($idea['idea_id'], $event['post_data']['post_subject']); 392 | } 393 | 394 | /** 395 | * Change an idea's author when the post author is changed 396 | * 397 | * @param \phpbb\event\data $event The event object 398 | * @return void 399 | */ 400 | public function change_idea_author($event) 401 | { 402 | $forum_id = (int) $event['post_info']['forum_id']; 403 | $topic_id = (int) $event['post_info']['topic_id']; 404 | $old_author_id = (int) $event['post_info']['poster_id']; 405 | $new_author_id = (int) $event['userdata']['user_id']; 406 | 407 | if ($old_author_id === $new_author_id || !$this->is_ideas_forum($forum_id)) 408 | { 409 | return; 410 | } 411 | 412 | $idea = $this->idea->get_idea_by_topic_id($topic_id); 413 | $this->idea->set_author($idea['idea_id'], $new_author_id); 414 | } 415 | 416 | /** 417 | * Test if we are on the posting page for a new idea 418 | * 419 | * @param string $mode Mode should be post 420 | * @param int $forum_id The forum posting is being made in 421 | * @param bool $topic_flag Flag for the state of the topic_id 422 | * 423 | * @return bool True if mode is post, forum is Ideas forum, and a topic id is 424 | * expected to exist yet, false if any of these tests failed. 425 | */ 426 | protected function is_post_idea($mode, $forum_id, $topic_flag = true) 427 | { 428 | if ($mode !== 'post') 429 | { 430 | return false; 431 | } 432 | 433 | if (!$this->is_ideas_forum($forum_id)) 434 | { 435 | return false; 436 | } 437 | 438 | return $topic_flag; 439 | } 440 | 441 | /** 442 | * Check if forum id is for the ideas the forum 443 | * 444 | * @param int $forum_id 445 | * @return bool 446 | * @access public 447 | */ 448 | protected function is_ideas_forum($forum_id) 449 | { 450 | return (int) $forum_id === (int) $this->config['ideas_forum_id']; 451 | } 452 | 453 | /** 454 | * Check if a post is the first post in a topic 455 | * 456 | * @param int|string $topic_first_post_id 457 | * @param int|string $post_id 458 | * @return bool 459 | * @access protected 460 | */ 461 | protected function is_first_post($topic_first_post_id, $post_id) 462 | { 463 | return (int) $topic_first_post_id === (int) $post_id; 464 | } 465 | } 466 | -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | 294 | Copyright (C) 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | , 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | --------------------------------------------------------------------------------