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