Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
70.07% |
103 / 147 |
|
33.33% |
3 / 9 |
CRAP | |
0.00% |
0 / 1 |
| add | |
70.07% |
103 / 147 |
|
33.33% |
3 / 9 |
46.55 | |
0.00% |
0 / 1 |
| __construct | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
1 | |||
| configure | |
100.00% |
29 / 29 |
|
100.00% |
1 / 1 |
1 | |||
| execute | |
87.50% |
21 / 24 |
|
0.00% |
0 / 1 |
5.05 | |||
| interact | |
76.92% |
20 / 26 |
|
0.00% |
0 / 1 |
6.44 | |||
| validate_user_data | |
93.33% |
14 / 15 |
|
0.00% |
0 / 1 |
3.00 | |||
| get_group_id | |
90.00% |
9 / 10 |
|
0.00% |
0 / 1 |
3.01 | |||
| send_activation_email | |
0.00% |
0 / 21 |
|
0.00% |
0 / 1 |
20 | |||
| get_activation_key | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
12 | |||
| ask_user | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| 1 | <?php |
| 2 | /** |
| 3 | * |
| 4 | * This file is part of the phpBB Forum Software package. |
| 5 | * |
| 6 | * @copyright (c) phpBB Limited <https://www.phpbb.com> |
| 7 | * @license GNU General Public License, version 2 (GPL-2.0) |
| 8 | * |
| 9 | * For full copyright and license information, please see |
| 10 | * the docs/CREDITS.txt file. |
| 11 | * |
| 12 | */ |
| 13 | |
| 14 | namespace phpbb\console\command\user; |
| 15 | |
| 16 | use phpbb\config\config; |
| 17 | use phpbb\console\command\command; |
| 18 | use phpbb\db\driver\driver_interface; |
| 19 | use phpbb\exception\runtime_exception; |
| 20 | use phpbb\language\language; |
| 21 | use phpbb\messenger\method\email; |
| 22 | use phpbb\passwords\manager; |
| 23 | use phpbb\user; |
| 24 | use Symfony\Component\Console\Command\Command as symfony_command; |
| 25 | use Symfony\Component\Console\Helper\QuestionHelper; |
| 26 | use Symfony\Component\Console\Input\InputInterface; |
| 27 | use Symfony\Component\Console\Input\InputOption; |
| 28 | use Symfony\Component\Console\Output\OutputInterface; |
| 29 | use Symfony\Component\Console\Question\Question; |
| 30 | use Symfony\Component\Console\Style\SymfonyStyle; |
| 31 | |
| 32 | class add extends command |
| 33 | { |
| 34 | /** @var array Array of interactively acquired options */ |
| 35 | protected $data; |
| 36 | |
| 37 | /** @var driver_interface */ |
| 38 | protected $db; |
| 39 | |
| 40 | /** @var config */ |
| 41 | protected $config; |
| 42 | |
| 43 | /** @var email */ |
| 44 | protected $email_method; |
| 45 | |
| 46 | /** @var language */ |
| 47 | protected $language; |
| 48 | |
| 49 | /** @var manager */ |
| 50 | protected $password_manager; |
| 51 | |
| 52 | /** |
| 53 | * phpBB root path |
| 54 | * |
| 55 | * @var string |
| 56 | */ |
| 57 | protected $phpbb_root_path; |
| 58 | |
| 59 | /** |
| 60 | * PHP extension. |
| 61 | * |
| 62 | * @var string |
| 63 | */ |
| 64 | protected $php_ext; |
| 65 | |
| 66 | /** |
| 67 | * Construct method |
| 68 | * |
| 69 | * @param user $user |
| 70 | * @param driver_interface $db |
| 71 | * @param config $config |
| 72 | * @param language $language |
| 73 | * @param service_collection $messenger |
| 74 | * @param manager $password_manager |
| 75 | * @param string $phpbb_root_path |
| 76 | * @param string $php_ext |
| 77 | */ |
| 78 | public function __construct(user $user, driver_interface $db, config $config, language $language, email $email_method, manager $password_manager, $phpbb_root_path, $php_ext) |
| 79 | { |
| 80 | $this->config = $config; |
| 81 | $this->db = $db; |
| 82 | $this->email_method = $email_method; |
| 83 | $this->language = $language; |
| 84 | $this->password_manager = $password_manager; |
| 85 | $this->phpbb_root_path = $phpbb_root_path; |
| 86 | $this->php_ext = $php_ext; |
| 87 | |
| 88 | $this->language->add_lang('ucp'); |
| 89 | parent::__construct($user); |
| 90 | } |
| 91 | |
| 92 | /** |
| 93 | * Sets the command name and description |
| 94 | * |
| 95 | * @return void |
| 96 | */ |
| 97 | protected function configure() |
| 98 | { |
| 99 | $this |
| 100 | ->setName('user:add') |
| 101 | ->setDescription($this->language->lang('CLI_DESCRIPTION_USER_ADD')) |
| 102 | ->setHelp($this->language->lang('CLI_HELP_USER_ADD')) |
| 103 | ->addOption( |
| 104 | 'username', |
| 105 | 'U', |
| 106 | InputOption::VALUE_REQUIRED, |
| 107 | $this->language->lang('CLI_DESCRIPTION_USER_ADD_OPTION_USERNAME') |
| 108 | ) |
| 109 | ->addOption( |
| 110 | 'password', |
| 111 | 'P', |
| 112 | InputOption::VALUE_REQUIRED, |
| 113 | $this->language->lang('CLI_DESCRIPTION_USER_ADD_OPTION_PASSWORD') |
| 114 | ) |
| 115 | ->addOption( |
| 116 | 'email', |
| 117 | 'E', |
| 118 | InputOption::VALUE_REQUIRED, |
| 119 | $this->language->lang('CLI_DESCRIPTION_USER_ADD_OPTION_EMAIL') |
| 120 | ) |
| 121 | ->addOption( |
| 122 | 'send-email', |
| 123 | null, |
| 124 | InputOption::VALUE_NONE, |
| 125 | $this->language->lang('CLI_DESCRIPTION_USER_ADD_OPTION_NOTIFY') |
| 126 | ) |
| 127 | ; |
| 128 | } |
| 129 | |
| 130 | /** |
| 131 | * Executes the command user:add |
| 132 | * |
| 133 | * Adds a new user to the database. If options are not provided, it will ask for the username, password and email. |
| 134 | * User is added to the registered user group. Language and timezone default to $config settings. |
| 135 | * |
| 136 | * @param InputInterface $input The input stream used to get the options |
| 137 | * @param OutputInterface $output The output stream, used to print messages |
| 138 | * |
| 139 | * @return int 0 if all is well, 1 if any errors occurred |
| 140 | */ |
| 141 | protected function execute(InputInterface $input, OutputInterface $output): int |
| 142 | { |
| 143 | $io = new SymfonyStyle($input, $output); |
| 144 | |
| 145 | try |
| 146 | { |
| 147 | $this->validate_user_data(); |
| 148 | $group_id = $this->get_group_id(); |
| 149 | } |
| 150 | catch (runtime_exception $e) |
| 151 | { |
| 152 | $io->error($e->getMessage()); |
| 153 | return symfony_command::FAILURE; |
| 154 | } |
| 155 | |
| 156 | $user_row = array( |
| 157 | 'username' => $this->data['username'], |
| 158 | 'user_password' => $this->password_manager->hash($this->data['new_password']), |
| 159 | 'user_email' => $this->data['email'], |
| 160 | 'group_id' => $group_id, |
| 161 | 'user_timezone' => $this->config['board_timezone'], |
| 162 | 'user_lang' => $this->config['default_lang'], |
| 163 | 'user_type' => USER_NORMAL, |
| 164 | 'user_regdate' => time(), |
| 165 | ); |
| 166 | |
| 167 | $user_id = (int) user_add($user_row); |
| 168 | |
| 169 | if (!$user_id) |
| 170 | { |
| 171 | $io->error($this->language->lang('AUTH_NO_PROFILE_CREATED')); |
| 172 | return symfony_command::FAILURE; |
| 173 | } |
| 174 | |
| 175 | if ($input->getOption('send-email') && $this->config['email_enable']) |
| 176 | { |
| 177 | $this->send_activation_email($user_id); |
| 178 | } |
| 179 | |
| 180 | $io->success($this->language->lang('CLI_USER_ADD_SUCCESS', $this->data['username'])); |
| 181 | |
| 182 | return symfony_command::SUCCESS; |
| 183 | } |
| 184 | |
| 185 | /** |
| 186 | * Interacts with the user. |
| 187 | * |
| 188 | * @param InputInterface $input An InputInterface instance |
| 189 | * @param OutputInterface $output An OutputInterface instance |
| 190 | */ |
| 191 | protected function interact(InputInterface $input, OutputInterface $output) |
| 192 | { |
| 193 | $helper = $this->getHelper('question'); |
| 194 | if (!$helper instanceof QuestionHelper) |
| 195 | { |
| 196 | return; |
| 197 | } |
| 198 | |
| 199 | $this->data = array( |
| 200 | 'username' => $input->getOption('username'), |
| 201 | 'new_password' => $input->getOption('password'), |
| 202 | 'email' => $input->getOption('email'), |
| 203 | ); |
| 204 | |
| 205 | if (!$this->data['username']) |
| 206 | { |
| 207 | $question = new Question($this->ask_user('USERNAME')); |
| 208 | $this->data['username'] = $helper->ask($input, $output, $question); |
| 209 | } |
| 210 | |
| 211 | if (!$this->data['new_password']) |
| 212 | { |
| 213 | $question = new Question($this->ask_user('PASSWORD')); |
| 214 | $question->setValidator(function ($value) use ($helper, $input, $output) { |
| 215 | $question = new Question($this->ask_user('CONFIRM_PASSWORD')); |
| 216 | $question->setHidden(true); |
| 217 | if ($helper->ask($input, $output, $question) != $value) |
| 218 | { |
| 219 | throw new runtime_exception($this->language->lang('NEW_PASSWORD_ERROR')); |
| 220 | } |
| 221 | return $value; |
| 222 | }); |
| 223 | $question->setHidden(true); |
| 224 | $question->setMaxAttempts(5); |
| 225 | |
| 226 | $this->data['new_password'] = $helper->ask($input, $output, $question); |
| 227 | } |
| 228 | |
| 229 | if (!$this->data['email']) |
| 230 | { |
| 231 | $question = new Question($this->ask_user('EMAIL_ADDRESS')); |
| 232 | $this->data['email'] = $helper->ask($input, $output, $question); |
| 233 | } |
| 234 | } |
| 235 | |
| 236 | /** |
| 237 | * Validate the submitted user data |
| 238 | * |
| 239 | * @throws runtime_exception if any data fails validation |
| 240 | * @return void |
| 241 | */ |
| 242 | protected function validate_user_data() |
| 243 | { |
| 244 | if (!function_exists('validate_data')) |
| 245 | { |
| 246 | require($this->phpbb_root_path . 'includes/functions_user.' . $this->php_ext); |
| 247 | } |
| 248 | |
| 249 | $error = validate_data($this->data, array( |
| 250 | 'username' => array( |
| 251 | array('string', false, $this->config['min_name_chars'], $this->config['max_name_chars']), |
| 252 | array('username', '')), |
| 253 | 'new_password' => array( |
| 254 | array('string', false, $this->config['min_pass_chars'], 0), |
| 255 | array('password')), |
| 256 | 'email' => array( |
| 257 | array('string', false, 6, 60), |
| 258 | array('user_email')), |
| 259 | )); |
| 260 | |
| 261 | if ($error) |
| 262 | { |
| 263 | throw new runtime_exception(implode("\n", array_map(array($this->language, 'lang'), $error))); |
| 264 | } |
| 265 | } |
| 266 | |
| 267 | /** |
| 268 | * Get the group id |
| 269 | * |
| 270 | * Go and find in the database the group_id corresponding to 'REGISTERED' |
| 271 | * |
| 272 | * @throws runtime_exception if the group id does not exist in database. |
| 273 | * @return null |
| 274 | */ |
| 275 | protected function get_group_id() |
| 276 | { |
| 277 | $sql = 'SELECT group_id |
| 278 | FROM ' . GROUPS_TABLE . " |
| 279 | WHERE group_name = '" . $this->db->sql_escape('REGISTERED') . "' |
| 280 | AND group_type = " . GROUP_SPECIAL; |
| 281 | $result = $this->db->sql_query($sql); |
| 282 | $row = $this->db->sql_fetchrow($result); |
| 283 | $this->db->sql_freeresult($result); |
| 284 | |
| 285 | if (!$row || !$row['group_id']) |
| 286 | { |
| 287 | throw new runtime_exception($this->language->lang('NO_GROUP')); |
| 288 | } |
| 289 | |
| 290 | return $row['group_id']; |
| 291 | } |
| 292 | |
| 293 | /** |
| 294 | * Send account activation email |
| 295 | * |
| 296 | * @param int $user_id The new user's id |
| 297 | * @return void |
| 298 | */ |
| 299 | protected function send_activation_email($user_id) |
| 300 | { |
| 301 | switch ($this->config['require_activation']) |
| 302 | { |
| 303 | case USER_ACTIVATION_SELF: |
| 304 | $email_template = 'user_welcome_inactive'; |
| 305 | break; |
| 306 | case USER_ACTIVATION_ADMIN: |
| 307 | $email_template = 'admin_welcome_inactive'; |
| 308 | break; |
| 309 | default: |
| 310 | $email_template = 'user_welcome'; |
| 311 | break; |
| 312 | } |
| 313 | |
| 314 | $user_actkey = $this->get_activation_key($user_id); |
| 315 | |
| 316 | $this->email_method->set_use_queue(false); |
| 317 | $this->email_method->template($email_template, $this->user->lang_name); |
| 318 | $this->email_method->to($this->data['email'], $this->data['username']); |
| 319 | $this->email_method->anti_abuse_headers($this->config, $this->user); |
| 320 | $this->email_method->assign_vars([ |
| 321 | 'WELCOME_MSG' => html_entity_decode($this->language->lang('WELCOME_SUBJECT', $this->config['sitename']), ENT_COMPAT), |
| 322 | 'USERNAME' => html_entity_decode($this->data['username'], ENT_COMPAT), |
| 323 | 'PASSWORD' => html_entity_decode($this->data['new_password'], ENT_COMPAT), |
| 324 | 'U_ACTIVATE' => generate_board_url() . "/ucp.{$this->php_ext}?mode=activate&u=$user_id&k=$user_actkey", |
| 325 | ]); |
| 326 | $this->email_method->send(); |
| 327 | } |
| 328 | |
| 329 | /** |
| 330 | * Get user activation key |
| 331 | * |
| 332 | * @param int $user_id User ID |
| 333 | * |
| 334 | * @return string User activation key for user |
| 335 | */ |
| 336 | protected function get_activation_key(int $user_id): string |
| 337 | { |
| 338 | $user_actkey = ''; |
| 339 | |
| 340 | if ($this->config['require_activation'] == USER_ACTIVATION_SELF || $this->config['require_activation'] == USER_ACTIVATION_ADMIN) |
| 341 | { |
| 342 | $user_actkey = gen_rand_string(mt_rand(6, 10)); |
| 343 | |
| 344 | $sql_ary = [ |
| 345 | 'user_actkey' => $user_actkey, |
| 346 | 'user_actkey_expiration' => user::get_token_expiration(), |
| 347 | ]; |
| 348 | |
| 349 | $sql = 'UPDATE ' . USERS_TABLE . ' |
| 350 | SET ' . $this->db->sql_build_array('UPDATE', $sql_ary) . ' |
| 351 | WHERE user_id = ' . (int) $user_id; |
| 352 | $this->db->sql_query($sql); |
| 353 | } |
| 354 | |
| 355 | return $user_actkey; |
| 356 | } |
| 357 | |
| 358 | /** |
| 359 | * Helper to translate questions to the user |
| 360 | * |
| 361 | * @param string $key The language key |
| 362 | * @return string The language key translated with a colon and space appended |
| 363 | */ |
| 364 | protected function ask_user($key) |
| 365 | { |
| 366 | return $this->language->lang($key) . $this->language->lang('COLON') . ' '; |
| 367 | } |
| 368 | } |