Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
70.07% covered (warning)
70.07%
103 / 147
33.33% covered (danger)
33.33%
3 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
add
70.07% covered (warning)
70.07%
103 / 147
33.33% covered (danger)
33.33%
3 / 9
46.55
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
1
 configure
100.00% covered (success)
100.00%
29 / 29
100.00% covered (success)
100.00%
1 / 1
1
 execute
87.50% covered (warning)
87.50%
21 / 24
0.00% covered (danger)
0.00%
0 / 1
5.05
 interact
76.92% covered (warning)
76.92%
20 / 26
0.00% covered (danger)
0.00%
0 / 1
6.44
 validate_user_data
93.33% covered (success)
93.33%
14 / 15
0.00% covered (danger)
0.00%
0 / 1
3.00
 get_group_id
90.00% covered (success)
90.00%
9 / 10
0.00% covered (danger)
0.00%
0 / 1
3.01
 send_activation_email
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 1
20
 get_activation_key
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
12
 ask_user
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
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
14namespace phpbb\console\command\user;
15
16use phpbb\config\config;
17use phpbb\console\command\command;
18use phpbb\db\driver\driver_interface;
19use phpbb\exception\runtime_exception;
20use phpbb\language\language;
21use phpbb\messenger\method\email;
22use phpbb\passwords\manager;
23use phpbb\user;
24use Symfony\Component\Console\Command\Command as symfony_command;
25use Symfony\Component\Console\Helper\QuestionHelper;
26use Symfony\Component\Console\Input\InputInterface;
27use Symfony\Component\Console\Input\InputOption;
28use Symfony\Component\Console\Output\OutputInterface;
29use Symfony\Component\Console\Question\Question;
30use Symfony\Component\Console\Style\SymfonyStyle;
31
32class 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)
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}