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