1<?php 2 3/** 4 * webtrees: online genealogy 5 * Copyright (C) 2023 webtrees development team 6 * This program is free software: you can redistribute it and/or modify 7 * it under the terms of the GNU General Public License as published by 8 * the Free Software Foundation, either version 3 of the License, or 9 * (at your option) any later version. 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * You should have received a copy of the GNU General Public License 15 * along with this program. If not, see <https://www.gnu.org/licenses/>. 16 */ 17 18declare(strict_types=1); 19 20namespace Fisharebest\Webtrees\Http\RequestHandlers; 21 22use Exception; 23use Fisharebest\Webtrees\Contracts\UserInterface; 24use Fisharebest\Webtrees\DB; 25use Fisharebest\Webtrees\FlashMessages; 26use Fisharebest\Webtrees\Http\Exceptions\HttpNotFoundException; 27use Fisharebest\Webtrees\Http\ViewResponseTrait; 28use Fisharebest\Webtrees\I18N; 29use Fisharebest\Webtrees\Log; 30use Fisharebest\Webtrees\NoReplyUser; 31use Fisharebest\Webtrees\Services\CaptchaService; 32use Fisharebest\Webtrees\Services\EmailService; 33use Fisharebest\Webtrees\Services\MessageService; 34use Fisharebest\Webtrees\Services\RateLimitService; 35use Fisharebest\Webtrees\Services\UserService; 36use Fisharebest\Webtrees\Session; 37use Fisharebest\Webtrees\Site; 38use Fisharebest\Webtrees\SiteUser; 39use Fisharebest\Webtrees\Tree; 40use Fisharebest\Webtrees\TreeUser; 41use Fisharebest\Webtrees\Validator; 42use Illuminate\Support\Str; 43use Psr\Http\Message\ResponseInterface; 44use Psr\Http\Message\ServerRequestInterface; 45use Psr\Http\Server\RequestHandlerInterface; 46 47use function view; 48 49/** 50 * Process a user registration. 51 */ 52class RegisterAction implements RequestHandlerInterface 53{ 54 use ViewResponseTrait; 55 56 private CaptchaService $captcha_service; 57 58 private EmailService $email_service; 59 60 private RateLimitService $rate_limit_service; 61 62 private UserService $user_service; 63 64 /** 65 * @param CaptchaService $captcha_service 66 * @param EmailService $email_service 67 * @param RateLimitService $rate_limit_service 68 * @param UserService $user_service 69 */ 70 public function __construct( 71 CaptchaService $captcha_service, 72 EmailService $email_service, 73 RateLimitService $rate_limit_service, 74 UserService $user_service 75 ) { 76 $this->captcha_service = $captcha_service; 77 $this->email_service = $email_service; 78 $this->rate_limit_service = $rate_limit_service; 79 $this->user_service = $user_service; 80 } 81 82 /** 83 * Perform a registration. 84 * 85 * @param ServerRequestInterface $request 86 * 87 * @return ResponseInterface 88 */ 89 public function handle(ServerRequestInterface $request): ResponseInterface 90 { 91 $this->checkRegistrationAllowed(); 92 93 $tree = Validator::attributes($request)->treeOptional(); 94 $comments = Validator::parsedBody($request)->string('comments'); 95 $email = Validator::parsedBody($request)->string('email'); 96 $password = Validator::parsedBody($request)->string('password'); 97 $realname = Validator::parsedBody($request)->string('realname'); 98 $username = Validator::parsedBody($request)->string('username'); 99 100 try { 101 if ($this->captcha_service->isRobot($request)) { 102 throw new Exception(I18N::translate('Please try again.')); 103 } 104 105 $this->doValidateRegistration($request, $username, $email, $realname, $comments, $password); 106 107 Session::forget('register_comments'); 108 Session::forget('register_email'); 109 Session::forget('register_realname'); 110 Session::forget('register_username'); 111 } catch (Exception $ex) { 112 FlashMessages::addMessage($ex->getMessage(), 'danger'); 113 114 Session::put('register_comments', $comments); 115 Session::put('register_email', $email); 116 Session::put('register_realname', $realname); 117 Session::put('register_username', $username); 118 119 return redirect(route(RegisterPage::class)); 120 } 121 122 $this->rate_limit_service->limitRateForSite(5, 300, 'rate-limit-registration'); 123 124 Log::addAuthenticationLog('User registration requested for: ' . $username); 125 126 $user = $this->user_service->create($username, $realname, $email, $password); 127 $token = Str::random(32); 128 129 $user->setPreference(UserInterface::PREF_LANGUAGE, I18N::languageTag()); 130 $user->setPreference(UserInterface::PREF_TIME_ZONE, Site::getPreference('TIMEZONE')); 131 $user->setPreference(UserInterface::PREF_IS_EMAIL_VERIFIED, ''); 132 $user->setPreference(UserInterface::PREF_IS_ACCOUNT_APPROVED, ''); 133 $user->setPreference(UserInterface::PREF_TIMESTAMP_REGISTERED, date('U')); 134 $user->setPreference(UserInterface::PREF_VERIFICATION_TOKEN, $token); 135 $user->setPreference(UserInterface::PREF_CONTACT_METHOD, MessageService::CONTACT_METHOD_INTERNAL_AND_EMAIL); 136 $user->setPreference(UserInterface::PREF_NEW_ACCOUNT_COMMENT, $comments); 137 $user->setPreference(UserInterface::PREF_IS_VISIBLE_ONLINE, '1'); 138 $user->setPreference(UserInterface::PREF_AUTO_ACCEPT_EDITS, ''); 139 $user->setPreference(UserInterface::PREF_IS_ADMINISTRATOR, ''); 140 $user->setPreference(UserInterface::PREF_TIMESTAMP_ACTIVE, '0'); 141 142 $base_url = Validator::attributes($request)->string('base_url'); 143 $reply_to = $tree instanceof Tree ? new TreeUser($tree) : new SiteUser(); 144 145 $verify_url = route(VerifyEmail::class, [ 146 'username' => $user->userName(), 147 'token' => $token, 148 'tree' => $tree?->name(), 149 ]); 150 151 // Send a verification message to the user. 152 /* I18N: %s is a server name/URL */ 153 $this->email_service->send( 154 new SiteUser(), 155 $user, 156 $reply_to, 157 I18N::translate('Your registration at %s', $base_url), 158 view('emails/register-user-text', ['user' => $user, 'base_url' => $base_url, 'verify_url' => $verify_url]), 159 view('emails/register-user-html', ['user' => $user, 'base_url' => $base_url, 'verify_url' => $verify_url]) 160 ); 161 162 // Tell the administrators about the registration. 163 foreach ($this->user_service->administrators() as $administrator) { 164 I18N::init($administrator->getPreference(UserInterface::PREF_LANGUAGE)); 165 166 /* I18N: %s is a server name/URL */ 167 $subject = I18N::translate('New registration at %s', $base_url); 168 169 $body_text = view('emails/register-notify-text', [ 170 'user' => $user, 171 'comments' => $comments, 172 'base_url' => $base_url, 173 'tree' => $tree, 174 ]); 175 176 $body_html = view('emails/register-notify-html', [ 177 'user' => $user, 178 'comments' => $comments, 179 'base_url' => $base_url, 180 'tree' => $tree, 181 ]); 182 183 /* I18N: %s is a server name/URL */ 184 $this->email_service->send( 185 new SiteUser(), 186 $administrator, 187 new NoReplyUser(), 188 $subject, 189 $body_text, 190 $body_html 191 ); 192 193 $mail1_method = $administrator->getPreference(UserInterface::PREF_CONTACT_METHOD); 194 if ( 195 $mail1_method !== MessageService::CONTACT_METHOD_EMAIL && 196 $mail1_method !== MessageService::CONTACT_METHOD_MAILTO && 197 $mail1_method !== MessageService::CONTACT_METHOD_NONE 198 ) { 199 DB::table('message')->insert([ 200 'sender' => $user->email(), 201 'ip_address' => $request->getAttribute('client-ip'), 202 'user_id' => $administrator->id(), 203 'subject' => $subject, 204 'body' => $body_text, 205 ]); 206 } 207 } 208 209 $title = I18N::translate('Request a new user account'); 210 211 return $this->viewResponse('register-success-page', [ 212 'title' => $title, 213 'tree' => $tree, 214 'user' => $user, 215 ]); 216 } 217 218 /** 219 * Check that visitors are allowed to register on this site. 220 * 221 * @return void 222 * @throws HttpNotFoundException 223 */ 224 private function checkRegistrationAllowed(): void 225 { 226 if (Site::getPreference('USE_REGISTRATION_MODULE') !== '1') { 227 throw new HttpNotFoundException(); 228 } 229 } 230 231 /** 232 * Check the registration details. 233 * 234 * @param ServerRequestInterface $request 235 * @param string $username 236 * @param string $email 237 * @param string $realname 238 * @param string $comments 239 * @param string $password 240 * 241 * @return void 242 * @throws Exception 243 */ 244 private function doValidateRegistration( 245 ServerRequestInterface $request, 246 string $username, 247 string $email, 248 string $realname, 249 string $comments, 250 #[\SensitiveParameter] string $password 251 ): void { 252 // All fields are required 253 if ($username === '' || $email === '' || $realname === '' || $comments === '' || $password === '') { 254 throw new Exception(I18N::translate('All fields must be completed.')); 255 } 256 257 // Username already exists 258 if ($this->user_service->findByUserName($username) !== null) { 259 throw new Exception(I18N::translate('Duplicate username. A user with that username already exists. Please choose another username.')); 260 } 261 262 // Email already exists 263 if ($this->user_service->findByEmail($email) !== null) { 264 throw new Exception(I18N::translate('Duplicate email address. A user with that email already exists.')); 265 } 266 267 $base_url = Validator::attributes($request)->string('base_url'); 268 269 // No external links 270 if (preg_match('/(?!' . preg_quote($base_url, '/') . ')(((?:http|https):\/\/)[a-zA-Z0-9.-]+)/', $comments, $match)) { 271 throw new Exception(I18N::translate('You are not allowed to send messages that contain external links.') . ' ' . I18N::translate('You should delete the “%1$s” from “%2$s” and try again.', e($match[2]), e($match[1]))); 272 } 273 } 274} 275