f339920f507bf8a2db6741f2e9d29b48ba993cf2
[GitHub/WoltLab/WCF.git] / wcfsetup / install / files / lib / form / RegisterForm.class.php
1 <?php
2
3 namespace wcf\form;
4
5 use ParagonIE\ConstantTime\Hex;
6 use wcf\acp\form\UserAddForm;
7 use wcf\data\blacklist\entry\BlacklistEntry;
8 use wcf\data\object\type\ObjectType;
9 use wcf\data\user\avatar\Gravatar;
10 use wcf\data\user\group\UserGroup;
11 use wcf\data\user\User;
12 use wcf\data\user\UserAction;
13 use wcf\data\user\UserEditor;
14 use wcf\system\captcha\CaptchaHandler;
15 use wcf\system\email\Email;
16 use wcf\system\email\mime\MimePartFacade;
17 use wcf\system\email\mime\RecipientAwareTextMimePart;
18 use wcf\system\email\UserMailbox;
19 use wcf\system\event\EventHandler;
20 use wcf\system\exception\NamedUserException;
21 use wcf\system\exception\PermissionDeniedException;
22 use wcf\system\exception\SystemException;
23 use wcf\system\exception\UserInputException;
24 use wcf\system\request\LinkHandler;
25 use wcf\system\user\group\assignment\UserGroupAssignmentHandler;
26 use wcf\system\user\notification\object\UserRegistrationUserNotificationObject;
27 use wcf\system\user\notification\UserNotificationHandler;
28 use wcf\system\WCF;
29 use wcf\util\HeaderUtil;
30 use wcf\util\JSON;
31 use wcf\util\StringUtil;
32 use wcf\util\UserRegistrationUtil;
33 use wcf\util\UserUtil;
34
35 /**
36 * Shows the user registration form.
37 *
38 * @author Marcel Werk
39 * @copyright 2001-2020 WoltLab GmbH
40 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
41 * @package WoltLabSuite\Core\Form
42 */
43 class RegisterForm extends UserAddForm
44 {
45 /**
46 * true if external authentication is used
47 * @var bool
48 */
49 public $isExternalAuthentication = false;
50
51 /**
52 * @inheritDoc
53 */
54 public $neededPermissions = [];
55
56 /**
57 * holds a language variable with information about the registration process
58 * e.g. if you need to activate your account
59 * @var string
60 */
61 public $message = '';
62
63 /**
64 * captcha object type object
65 * @var ObjectType
66 */
67 public $captchaObjectType;
68
69 /**
70 * name of the captcha object type; if empty, captcha is disabled
71 * @var string
72 */
73 public $captchaObjectTypeName = CAPTCHA_TYPE;
74
75 /**
76 * true if captcha is used
77 * @var bool
78 */
79 public $useCaptcha = REGISTER_USE_CAPTCHA;
80
81 /**
82 * field names
83 * @var array
84 */
85 public $randomFieldNames = [];
86
87 /**
88 * list of fields that have matches in the blacklist
89 * @var string[]
90 * @since 5.2
91 */
92 public $blacklistMatches = [];
93
94 /**
95 * min number of seconds between form request and submit
96 * @var int
97 */
98 public static $minRegistrationTime = 10;
99
100 /**
101 * @var mixed[]
102 */
103 public $passwordStrengthVerdict = [];
104
105 /**
106 * @inheritDoc
107 */
108 public function readParameters()
109 {
110 parent::readParameters();
111
112 // user is already registered
113 if (WCF::getUser()->userID) {
114 throw new PermissionDeniedException();
115 }
116
117 // registration disabled
118 if (REGISTER_DISABLED) {
119 throw new NamedUserException(WCF::getLanguage()->get('wcf.user.register.error.disabled'));
120 }
121
122 // check disclaimer
123 if (REGISTER_ENABLE_DISCLAIMER && !WCF::getSession()->getVar('disclaimerAccepted')) {
124 HeaderUtil::redirect(LinkHandler::getInstance()->getLink('Disclaimer'));
125
126 exit;
127 }
128
129 if (WCF::getSession()->getVar('__3rdPartyProvider')) {
130 $this->isExternalAuthentication = true;
131 }
132 }
133
134 /**
135 * @inheritDoc
136 */
137 public function readFormParameters()
138 {
139 parent::readFormParameters();
140
141 if (!empty($this->username) || !empty($this->email)) {
142 throw new PermissionDeniedException();
143 }
144
145 $this->randomFieldNames = WCF::getSession()->getVar('registrationRandomFieldNames');
146 if ($this->randomFieldNames === null) {
147 throw new PermissionDeniedException();
148 }
149
150 if (isset($_POST[$this->randomFieldNames['username']])) {
151 $this->username = StringUtil::trim($_POST[$this->randomFieldNames['username']]);
152 }
153 if (isset($_POST[$this->randomFieldNames['email']])) {
154 $this->email = StringUtil::trim($_POST[$this->randomFieldNames['email']]);
155 }
156 if (isset($_POST[$this->randomFieldNames['confirmEmail']])) {
157 $this->confirmEmail = StringUtil::trim($_POST[$this->randomFieldNames['confirmEmail']]);
158 }
159 if (isset($_POST[$this->randomFieldNames['password']])) {
160 $this->password = $_POST[$this->randomFieldNames['password']];
161 }
162 if (isset($_POST[$this->randomFieldNames['password'] . '_passwordStrengthVerdict'])) {
163 try {
164 $this->passwordStrengthVerdict = JSON::decode(
165 $_POST[$this->randomFieldNames['password'] . '_passwordStrengthVerdict']
166 );
167 } catch (SystemException $e) {
168 // ignore
169 }
170 }
171 if (isset($_POST[$this->randomFieldNames['confirmPassword']])) {
172 $this->confirmPassword = $_POST[$this->randomFieldNames['confirmPassword']];
173 }
174
175 $this->groupIDs = [];
176
177 if ($this->captchaObjectType) {
178 $this->captchaObjectType->getProcessor()->readFormParameters();
179 }
180 }
181
182 /**
183 * @inheritDoc
184 */
185 protected function initOptionHandler()
186 {
187 /** @noinspection PhpUndefinedMethodInspection */
188 $this->optionHandler->setInRegistration();
189 parent::initOptionHandler();
190 }
191
192 /**
193 * @inheritDoc
194 */
195 public function validate()
196 {
197 // validate captcha first
198 $this->validateCaptcha();
199
200 parent::validate();
201
202 // validate registration time
203 if (
204 !$this->isExternalAuthentication
205 && (
206 !WCF::getSession()->getVar('registrationStartTime')
207 || (TIME_NOW - WCF::getSession()->getVar('registrationStartTime')) < self::$minRegistrationTime
208 )
209 ) {
210 throw new UserInputException('registrationStartTime', []);
211 }
212
213 if (BLACKLIST_SFS_ENABLE) {
214 $this->blacklistMatches = BlacklistEntry::getMatches(
215 $this->username,
216 $this->email,
217 UserUtil::getIpAddress()
218 );
219 if (!empty($this->blacklistMatches) && BLACKLIST_SFS_ACTION === 'block') {
220 throw new NamedUserException('wcf.user.register.error.blacklistMatches');
221 }
222 }
223 }
224
225 /**
226 * @inheritDoc
227 */
228 public function readData()
229 {
230 if ($this->useCaptcha && $this->captchaObjectTypeName) {
231 $this->captchaObjectType = CaptchaHandler::getInstance()->getObjectTypeByName($this->captchaObjectTypeName);
232 if ($this->captchaObjectType === null) {
233 throw new SystemException("Unknown captcha object type with id '" . $this->captchaObjectTypeName . "'");
234 }
235
236 if (!$this->captchaObjectType->getProcessor()->isAvailable()) {
237 $this->captchaObjectType = null;
238 }
239
240 if (WCF::getSession()->getVar('noRegistrationCaptcha')) {
241 $this->captchaObjectType = null;
242 }
243 }
244
245 parent::readData();
246
247 if (empty($_POST)) {
248 $this->languageID = WCF::getLanguage()->languageID;
249
250 if (WCF::getSession()->getVar('__username')) {
251 $this->username = WCF::getSession()->getVar('__username');
252 }
253 if (WCF::getSession()->getVar('__email')) {
254 $this->email = $this->confirmEmail = WCF::getSession()->getVar('__email');
255 }
256
257 WCF::getSession()->register('registrationStartTime', TIME_NOW);
258
259 // generate random field names
260 $this->randomFieldNames = [
261 'username' => UserRegistrationUtil::getRandomFieldName('username'),
262 'email' => UserRegistrationUtil::getRandomFieldName('email'),
263 'confirmEmail' => UserRegistrationUtil::getRandomFieldName('confirmEmail'),
264 'password' => UserRegistrationUtil::getRandomFieldName('password'),
265 'confirmPassword' => UserRegistrationUtil::getRandomFieldName('confirmPassword'),
266 ];
267
268 WCF::getSession()->register('registrationRandomFieldNames', $this->randomFieldNames);
269 }
270 }
271
272 /**
273 * Reads option tree on page init.
274 */
275 protected function readOptionTree()
276 {
277 $this->optionTree = $this->optionHandler->getOptionTree('profile');
278 }
279
280 /**
281 * @inheritDoc
282 */
283 public function assignVariables()
284 {
285 parent::assignVariables();
286
287 WCF::getTPL()->assign([
288 'captchaObjectType' => $this->captchaObjectType,
289 'isExternalAuthentication' => $this->isExternalAuthentication,
290 'randomFieldNames' => $this->randomFieldNames,
291 'passwordRulesAttributeValue' => UserRegistrationUtil::getPasswordRulesAttributeValue(),
292 ]);
293 }
294
295 /**
296 * @inheritDoc
297 */
298 public function show()
299 {
300 AbstractForm::show();
301 }
302
303 /**
304 * Validates the captcha.
305 */
306 protected function validateCaptcha()
307 {
308 if ($this->captchaObjectType) {
309 try {
310 $this->captchaObjectType->getProcessor()->validate();
311 } catch (UserInputException $e) {
312 $this->errorType[$e->getField()] = $e->getType();
313 }
314 }
315 }
316
317 /**
318 * @inheritDoc
319 */
320 protected function validateUsername($username)
321 {
322 parent::validateUsername($username);
323
324 // check for min-max length
325 if (!UserRegistrationUtil::isValidUsername($username)) {
326 throw new UserInputException('username', 'invalid');
327 }
328 }
329
330 /**
331 * @inheritDoc
332 */
333 protected function validatePassword($password, $confirmPassword)
334 {
335 if (!$this->isExternalAuthentication) {
336 parent::validatePassword($password, $confirmPassword);
337
338 // check security of the given password
339 if (($this->passwordStrengthVerdict['score'] ?? 4) < PASSWORD_MIN_SCORE) {
340 throw new UserInputException('password', 'notSecure');
341 }
342 }
343 }
344
345 /**
346 * @inheritDoc
347 */
348 protected function validateEmail($email, $confirmEmail)
349 {
350 parent::validateEmail($email, $confirmEmail);
351
352 if (!UserRegistrationUtil::isValidEmail($email)) {
353 throw new UserInputException('email', 'invalid');
354 }
355 }
356
357 /**
358 * @inheritDoc
359 */
360 public function save()
361 {
362 AbstractForm::save();
363
364 // get options
365 $saveOptions = $this->optionHandler->save();
366 $registerVia3rdParty = false;
367
368 if ($this->isExternalAuthentication) {
369 $provider = WCF::getSession()->getVar('__3rdPartyProvider');
370 switch ($provider) {
371 case 'github':
372 case 'facebook':
373 case 'google':
374 if (($oauthUser = WCF::getSession()->getVar('__oauthUser'))) {
375 $this->additionalFields['authData'] = $provider . ':' . $oauthUser->getId();
376 }
377 break;
378 case 'twitter':
379 // Twitter
380 if (WCF::getSession()->getVar('__twitterData')) {
381 $twitterData = WCF::getSession()->getVar('__twitterData');
382 $this->additionalFields['authData'] = 'twitter:' . ($twitterData['id'] ?? $twitterData['user_id']);
383
384 WCF::getSession()->unregister('__twitterData');
385
386 if (
387 WCF::getSession()->getVar('__email')
388 && WCF::getSession()->getVar('__email') == $this->email
389 ) {
390 $registerVia3rdParty = true;
391 }
392 }
393 break;
394 }
395
396 // Accounts connected to a 3rdParty login do not have passwords.
397 $this->password = null;
398
399 if (WCF::getSession()->getVar('__email') && WCF::getSession()->getVar('__email') == $this->email) {
400 $registerVia3rdParty = true;
401 }
402 }
403
404 $eventParameters = [
405 'saveOptions' => $saveOptions,
406 'registerVia3rdParty' => $registerVia3rdParty,
407 ];
408 EventHandler::getInstance()->fireAction($this, 'registerVia3rdParty', $eventParameters);
409 $saveOptions = $eventParameters['saveOptions'];
410 $registerVia3rdParty = $eventParameters['registerVia3rdParty'];
411
412 $this->additionalFields['languageID'] = $this->languageID;
413 if (LOG_IP_ADDRESS) {
414 $this->additionalFields['registrationIpAddress'] = UserUtil::getIpAddress();
415 }
416
417 // generate activation code
418 $addDefaultGroups = true;
419 if (
420 !empty($this->blacklistMatches)
421 || (REGISTER_ACTIVATION_METHOD & User::REGISTER_ACTIVATION_USER && !$registerVia3rdParty)
422 || (REGISTER_ACTIVATION_METHOD & User::REGISTER_ACTIVATION_ADMIN)
423 ) {
424 $activationCode = UserRegistrationUtil::getActivationCode();
425 $emailConfirmCode = Hex::encode(\random_bytes(20));
426 $this->additionalFields['activationCode'] = $activationCode;
427 $this->additionalFields['emailConfirmed'] = $emailConfirmCode;
428 $addDefaultGroups = false;
429 $this->groupIDs = UserGroup::getGroupIDsByType([UserGroup::EVERYONE, UserGroup::GUESTS]);
430 }
431
432 // check gravatar support
433 if (MODULE_GRAVATAR && Gravatar::test($this->email)) {
434 $this->additionalFields['enableGravatar'] = 1;
435 }
436
437 // create user
438 $data = [
439 'data' => \array_merge($this->additionalFields, [
440 'username' => $this->username,
441 'email' => $this->email,
442 'password' => $this->password,
443 'blacklistMatches' => (!empty($this->blacklistMatches)) ? JSON::encode($this->blacklistMatches) : '',
444 'signatureEnableHtml' => 1,
445 ]),
446 'groups' => $this->groupIDs,
447 'languageIDs' => $this->visibleLanguages,
448 'options' => $saveOptions,
449 'addDefaultGroups' => $addDefaultGroups,
450 ];
451 $this->objectAction = new UserAction([], 'create', $data);
452 $result = $this->objectAction->executeAction();
453 /** @var User $user */
454 $user = $result['returnValues'];
455 $userEditor = new UserEditor($user);
456
457 // update session
458 WCF::getSession()->changeUser($user);
459
460 // activation management
461 if (REGISTER_ACTIVATION_METHOD == User::REGISTER_ACTIVATION_NONE && empty($this->blacklistMatches)) {
462 $this->message = 'wcf.user.register.success';
463
464 UserGroupAssignmentHandler::getInstance()->checkUsers([$user->userID]);
465 } elseif (REGISTER_ACTIVATION_METHOD & User::REGISTER_ACTIVATION_USER && empty($this->blacklistMatches)) {
466 // registering via 3rdParty leads to instant activation
467 if ($registerVia3rdParty) {
468 $this->message = 'wcf.user.register.success';
469 } else {
470 $email = new Email();
471 $email->addRecipient(new UserMailbox(WCF::getUser()));
472 $email->setSubject(
473 WCF::getLanguage()->getDynamicVariable('wcf.user.register.needActivation.mail.subject')
474 );
475 $email->setBody(new MimePartFacade([
476 new RecipientAwareTextMimePart('text/html', 'email_registerNeedActivation'),
477 new RecipientAwareTextMimePart('text/plain', 'email_registerNeedActivation'),
478 ]));
479 $email->send();
480 $this->message = 'wcf.user.register.success.needActivation';
481 }
482 } elseif (REGISTER_ACTIVATION_METHOD & User::REGISTER_ACTIVATION_ADMIN || !empty($this->blacklistMatches)) {
483 $this->message = 'wcf.user.register.success.awaitActivation';
484 }
485
486 $this->fireNotificationEvent($user);
487
488 if ($this->captchaObjectType) {
489 $this->captchaObjectType->getProcessor()->reset();
490 }
491
492 if (WCF::getSession()->getVar('noRegistrationCaptcha')) {
493 WCF::getSession()->unregister('noRegistrationCaptcha');
494 }
495
496 // login user
497 WCF::getSession()->unregister('registrationRandomFieldNames');
498 WCF::getSession()->unregister('registrationStartTime');
499 $this->saved();
500
501 // forward to index page
502 HeaderUtil::delayedRedirect(
503 LinkHandler::getInstance()->getLink(),
504 WCF::getLanguage()->getDynamicVariable($this->message, ['user' => $user]),
505 15
506 );
507
508 exit;
509 }
510
511 /**
512 * @param User $user
513 * @since 5.2
514 */
515 protected function fireNotificationEvent(User $user)
516 {
517 $recipientIDs = $this->getRecipientsForNotificationEvent();
518 if (!empty($recipientIDs)) {
519 UserNotificationHandler::getInstance()->fireEvent(
520 'registration',
521 'com.woltlab.wcf.user.registration.notification',
522 new UserRegistrationUserNotificationObject($user),
523 $recipientIDs
524 );
525 }
526 }
527
528 /**
529 * @return int[]
530 * @since 5.2
531 */
532 protected function getRecipientsForNotificationEvent()
533 {
534 $sql = "SELECT userID
535 FROM wcf" . WCF_N . "_user_to_group
536 WHERE groupID IN (
537 SELECT groupID
538 FROM wcf" . WCF_N . "_user_group_option_value
539 WHERE optionID IN (
540 SELECT optionID
541 FROM wcf" . WCF_N . "_user_group_option
542 WHERE optionName = ?
543 )
544 AND optionValue = ?
545 )";
546 $statement = WCF::getDB()->prepareStatement($sql, 100);
547 $statement->execute([
548 'admin.user.canSearchUser',
549 1,
550 ]);
551
552 return $statement->fetchAll(\PDO::FETCH_COLUMN);
553 }
554 }