Skip to content

Commit b7a3c9a

Browse files
philipreinkenkyteinsky
authored andcommitted
Enable adding E-Mail addresses to new user accounts using the CLI
Signed-off-by: Philip Gatzka <philip.gatzka@mailbox.org>
1 parent 529bce2 commit b7a3c9a

3 files changed

Lines changed: 248 additions & 6 deletions

File tree

core/Command/User/Add.php

Lines changed: 120 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,18 @@
2525
*/
2626
namespace OC\Core\Command\User;
2727

28+
use Egulias\EmailValidator\EmailValidator;
29+
use Egulias\EmailValidator\Validation\RFCValidation;
2830
use OC\Files\Filesystem;
31+
use OCA\Settings\Mailer\NewUserMailHelper;
32+
use OCP\EventDispatcher\IEventDispatcher;
33+
use OCP\IConfig;
2934
use OCP\IGroup;
3035
use OCP\IGroupManager;
3136
use OCP\IUser;
3237
use OCP\IUserManager;
38+
use OCP\Security\Events\GenerateSecurePasswordEvent;
39+
use OCP\Security\ISecureRandom;
3340
use Symfony\Component\Console\Command\Command;
3441
use Symfony\Component\Console\Helper\QuestionHelper;
3542
use Symfony\Component\Console\Input\InputArgument;
@@ -39,11 +46,63 @@
3946
use Symfony\Component\Console\Question\Question;
4047

4148
class Add extends Command {
49+
/**
50+
* @var IUserManager
51+
*/
52+
protected $userManager;
53+
54+
/**
55+
* @var IGroupManager
56+
*/
57+
protected $groupManager;
58+
59+
/**
60+
* @var EmailValidator
61+
*/
62+
protected $emailValidator;
63+
64+
/**
65+
* @var IConfig
66+
*/
67+
private $config;
68+
69+
/**
70+
* @var NewUserMailHelper
71+
*/
72+
private $mailHelper;
73+
74+
/**
75+
* @var IEventDispatcher
76+
*/
77+
private $eventDispatcher;
78+
79+
/**
80+
* @var ISecureRandom
81+
*/
82+
private $secureRandom;
83+
84+
/**
85+
* @param IUserManager $userManager
86+
* @param IGroupManager $groupManager
87+
* @param EmailValidator $emailValidator
88+
*/
4289
public function __construct(
43-
protected IUserManager $userManager,
44-
protected IGroupManager $groupManager,
90+
IUserManager $userManager,
91+
IGroupManager $groupManager,
92+
EmailValidator $emailValidator,
93+
IConfig $config,
94+
NewUserMailHelper $mailHelper,
95+
IEventDispatcher $eventDispatcher,
96+
ISecureRandom $secureRandom
4597
) {
4698
parent::__construct();
99+
$this->userManager = $userManager;
100+
$this->groupManager = $groupManager;
101+
$this->emailValidator = $emailValidator;
102+
$this->config = $config;
103+
$this->mailHelper = $mailHelper;
104+
$this->eventDispatcher = $eventDispatcher;
105+
$this->secureRandom = $secureRandom;
47106
}
48107

49108
protected function configure() {
@@ -72,18 +131,30 @@ protected function configure() {
72131
'g',
73132
InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY,
74133
'groups the user should be added to (The group will be created if it does not exist)'
134+
)
135+
->addOption(
136+
'email',
137+
null,
138+
InputOption::VALUE_REQUIRED,
139+
'When set, users may register using the default E-Mail verification workflow'
75140
);
76141
}
77142

78143
protected function execute(InputInterface $input, OutputInterface $output): int {
79144
$uid = $input->getArgument('uid');
145+
$emailIsSet = \is_string($input->getOption('email')) && \mb_strlen($input->getOption('email')) > 0;
146+
$emailIsValid = $this->emailValidator->isValid($input->getOption('email') ?? '', new RFCValidation());
147+
$password = '';
148+
$temporaryPassword = '';
149+
80150
if ($this->userManager->userExists($uid)) {
81151
$output->writeln('<error>The user "' . $uid . '" already exists.</error>');
82152
return 1;
83153
}
84154

85155
if ($input->getOption('password-from-env')) {
86156
$password = getenv('OC_PASS');
157+
87158
if (!$password) {
88159
$output->writeln('<error>--password-from-env given, but OC_PASS is empty!</error>');
89160
return 1;
@@ -109,17 +180,32 @@ protected function execute(InputInterface $input, OutputInterface $output): int
109180
return 1;
110181
}
111182

183+
if (trim($password) === '' && $emailIsSet) {
184+
if ($emailIsValid) {
185+
$output->writeln('Setting a temporary password.');
186+
187+
$temporaryPassword = $this->getTemporaryPassword();
188+
} else {
189+
$output->writeln(\sprintf(
190+
'<error>The given E-Mail address "%s" is invalid: %s</error>',
191+
$input->getOption('email'),
192+
$this->emailValidator->getError()->description()
193+
));
194+
195+
return 1;
196+
}
197+
}
198+
112199
try {
113200
$user = $this->userManager->createUser(
114201
$input->getArgument('uid'),
115-
$password
202+
$password ?: $temporaryPassword
116203
);
117204
} catch (\Exception $e) {
118205
$output->writeln('<error>' . $e->getMessage() . '</error>');
119206
return 1;
120207
}
121208

122-
123209
if ($user instanceof IUser) {
124210
$output->writeln('<info>The user "' . $user->getUID() . '" was created successfully</info>');
125211
} else {
@@ -129,7 +215,24 @@ protected function execute(InputInterface $input, OutputInterface $output): int
129215

130216
if ($input->getOption('display-name')) {
131217
$user->setDisplayName($input->getOption('display-name'));
132-
$output->writeln('Display name set to "' . $user->getDisplayName() . '"');
218+
$output->writeln(sprintf('Display name set to "%s"', $user->getDisplayName()));
219+
}
220+
221+
if ($emailIsSet && $emailIsValid) {
222+
$user->setSystemEMailAddress($input->getOption('email'));
223+
$output->writeln(sprintf('E-Mail set to "%s"', (string) $user->getSystemEMailAddress()));
224+
225+
if (trim($password) === '' && $this->config->getAppValue('core', 'newUser.sendEmail', 'yes') === 'yes') {
226+
try {
227+
$this->mailHelper->sendMail(
228+
$user,
229+
$this->mailHelper->generateTemplate($user, true)
230+
);
231+
$output->writeln('Invitation E-Mail sent.');
232+
} catch (\Exception $e) {
233+
$output->writeln(\sprintf('Unable to send the invitation mail to %s', $user->getEMailAddress()));
234+
}
235+
}
133236
}
134237

135238
$groups = $input->getOption('group');
@@ -156,4 +259,16 @@ protected function execute(InputInterface $input, OutputInterface $output): int
156259
}
157260
return 0;
158261
}
262+
263+
/**
264+
* @return string
265+
*/
266+
protected function getTemporaryPassword(): string
267+
{
268+
$passwordEvent = new GenerateSecurePasswordEvent();
269+
270+
$this->eventDispatcher->dispatchTyped($passwordEvent);
271+
272+
return $passwordEvent->getPassword() ?? $this->secureRandom->generate(20);
273+
}
159274
}

core/register_command.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
* along with this program. If not, see <http://www.gnu.org/licenses/>
4949
*
5050
*/
51+
5152
use Psr\Log\LoggerInterface;
5253

5354
$application->add(new \Stecman\Component\Symfony\Console\BashCompletion\CompletionCommand());
@@ -182,7 +183,7 @@
182183
$application->add(\OC::$server->query(\OC\Core\Command\Preview\Repair::class));
183184
$application->add(\OC::$server->query(\OC\Core\Command\Preview\ResetRenderedTexts::class));
184185

185-
$application->add(new OC\Core\Command\User\Add(\OC::$server->getUserManager(), \OC::$server->getGroupManager()));
186+
$application->add(new OC\Core\Command\User\Add(\OC::$server->getUserManager(), \OC::$server->getGroupManager(), \OC::$server->get(Egulias\EmailValidator\EmailValidator::class), \OC::$server->get(\OC\AllConfig::class), \OC::$server->get(\OCA\Settings\Mailer\NewUserMailHelper::class), \OC::$server->get(\OCP\EventDispatcher\IEventDispatcher::class), \OC::$server->get(\OCP\Security\ISecureRandom::class)));
186187
$application->add(new OC\Core\Command\User\Delete(\OC::$server->getUserManager()));
187188
$application->add(new OC\Core\Command\User\Disable(\OC::$server->getUserManager()));
188189
$application->add(new OC\Core\Command\User\Enable(\OC::$server->getUserManager()));
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
<?php
2+
/**
3+
* @copyright Copyright (c) 2021, Philip Gatzka (philip.gatzka@mailbox.org)
4+
*
5+
* @author Philip Gatzka <philip.gatzka@mailbox.org>
6+
*
7+
* @license GNU AGPL version 3 or any later version
8+
*
9+
* This program is free software: you can redistribute it and/or modify
10+
* it under the terms of the GNU Affero General Public License as
11+
* published by the Free Software Foundation, either version 3 of the
12+
* License, or (at your option) any later version.
13+
*
14+
* This program is distributed in the hope that it will be useful,
15+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
16+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17+
* GNU Affero General Public License for more details.
18+
*
19+
* You should have received a copy of the GNU Affero General Public License
20+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
21+
*
22+
*/
23+
24+
declare(strict_types=1);
25+
26+
namespace Core\Command\User;
27+
28+
use Egulias\EmailValidator\EmailValidator;
29+
use OC\Core\Command\User\Add;
30+
use OCA\Settings\Mailer\NewUserMailHelper;
31+
use OCP\EventDispatcher\IEventDispatcher;
32+
use OCP\IConfig;
33+
use OCP\IGroupManager;
34+
use OCP\IUser;
35+
use OCP\IUserManager;
36+
use OCP\Mail\IEMailTemplate;
37+
use OCP\Security\ISecureRandom;
38+
use Symfony\Component\Console\Input\InputInterface;
39+
use Symfony\Component\Console\Output\OutputInterface;
40+
use Test\TestCase;
41+
42+
class AddTest extends TestCase {
43+
44+
/**
45+
* @dataProvider addEmailDataProvider
46+
*/
47+
public function testAddEmail(?string $email, bool $isValid, bool $shouldSendMail): void {
48+
$userManager = static::createMock(IUserManager::class);
49+
$groupManager = static::createStub(IGroupManager::class);
50+
$user = static::createMock(IUser::class);
51+
$config = static::createMock(IConfig::class);
52+
$mailHelper = static::createMock(NewUserMailHelper::class);
53+
$eventDispatcher = static::createStub(IEventDispatcher::class);
54+
$secureRandom = static::createStub(ISecureRandom::class);
55+
56+
$consoleInput = static::createMock(InputInterface::class);
57+
$consoleOutput = static::createMock(OutputInterface::class);
58+
59+
$user->expects($isValid ? static::once() : static::never())
60+
->method('setSystemEMailAddress')
61+
->with(static::equalTo($email));
62+
63+
$userManager->method('createUser')
64+
->willReturn($user);
65+
66+
$config->method('getAppValue')
67+
->willReturn($shouldSendMail ? 'yes' : 'no');
68+
69+
$mailHelper->method('generateTemplate')
70+
->willReturn(static::createMock(IEMailTemplate::class));
71+
72+
$mailHelper->expects($isValid && $shouldSendMail ? static::once() : static::never())
73+
->method('sendMail');
74+
75+
$consoleInput->method('getOption')
76+
->will(static::returnValueMap([
77+
['password-from-env', ''],
78+
['email', $email],
79+
['group', []],
80+
]));
81+
82+
$addCommand = new Add(
83+
$userManager,
84+
$groupManager,
85+
new EmailValidator(),
86+
$config,
87+
$mailHelper,
88+
$eventDispatcher,
89+
$secureRandom
90+
);
91+
92+
$this->invokePrivate($addCommand, 'execute', [
93+
$consoleInput,
94+
$consoleOutput
95+
]);
96+
}
97+
98+
/**
99+
* @return \Generator<string, array>
100+
*/
101+
public function addEmailDataProvider(): \Generator {
102+
yield 'Valid E-Mail' => [
103+
'info@example.com',
104+
true,
105+
true,
106+
];
107+
108+
yield 'Invalid E-Mail' => [
109+
'info@@example.com',
110+
false,
111+
true,
112+
];
113+
114+
yield 'No E-Mail' => [
115+
'',
116+
false,
117+
true,
118+
];
119+
120+
yield 'Valid E-Mail, but no mail should be sent' => [
121+
'info@example.com',
122+
true,
123+
false,
124+
];
125+
}
126+
}

0 commit comments

Comments
 (0)