<?php
namespace XF\Service\User;
use function intval;
class Registration extends \XF\Service\AbstractService
{
use \XF\Service\ValidateAndSavableTrait;
/**
* @var \XF\Entity\User
*/
protected $user;
protected $fieldMap = [
'username' => 'username',
'email' => 'email',
'timezone' => 'timezone',
'location' => 'Profile.location',
];
protected $logIp = true;
protected $avatarUrl = null;
protected $preRegActionKey = null;
protected $preRegContent = null;
protected $skipEmailConfirm = false;
protected function setup()
{
$this->user = $this->app->repository('XF:User')->setupBaseUser();
}
public function getUser()
{
return $this->user;
}
public function setMapped(array $input)
{
foreach ($this->fieldMap AS $inputKey => $entityKey)
{
if (!isset($input[$inputKey]))
{
continue;
}
$value = $input[$inputKey];
if (strpos($entityKey, '.'))
{
list($relation, $relationKey) = explode('.', $entityKey, 2);
$this->user->{$relation}->{$relationKey} = $value;
}
else
{
$this->user->{$entityKey} = $value;
}
}
}
public function setPassword($password, $passwordConfirm = '', $doPasswordConfirmation = true)
{
if ($doPasswordConfirmation)
{
if ($password !== $passwordConfirm)
{
$this->user->error(\XF::phrase('passwords_did_not_match'));
return false;
}
}
if ($this->user->Auth->setPassword($password))
{
$this->user->Profile->password_date = \XF::$time;
}
return true;
}
public function setNoPassword()
{
$this->user->Auth->setNoPassword();
$this->user->Profile->password_date = \XF::$time;
}
public function setDob($day, $month, $year)
{
return $this->user->Profile->setDob($day, $month, $year);
}
public function setCustomFields(array $values)
{
/** @var \XF\CustomField\Set $fieldSet */
$fieldSet = $this->user->Profile->custom_fields;
$fieldDefinition = $fieldSet->getDefinitionSet()
->filterEditable($fieldSet, 'user')
->filter('registration');
$customFieldsShown = array_keys($fieldDefinition->getFieldDefinitions());
if ($customFieldsShown)
{
$fieldSet->bulkSet($values, $customFieldsShown);
}
}
public function setFromInput(array $input)
{
$this->setMapped($input);
if (isset($input['password']))
{
$password = $input['password'];
if (isset($input['password_confirm']))
{
$passwordConfirm = $input['password_confirm'];
$doPasswordConfirmation = true;
}
else
{
$passwordConfirm = '';
$doPasswordConfirmation = false;
}
$this->setPassword($password, $passwordConfirm, $doPasswordConfirmation);
}
if (isset($input['dob_day'], $input['dob_month'], $input['dob_year']))
{
$day = $input['dob_day'] ?? 0;
$month = $input['dob_month'] ?? 0;
$year = $input['dob_year'] ?? 0;
$this->setDob($day, $month, $year);
}
if (isset($input['custom_fields']))
{
$this->setCustomFields($input['custom_fields']);
}
if (isset($input['email_choice']))
{
$this->setReceiveAdminEmail($input['email_choice']);
$this->setReceiveActivitySummary($input['email_choice']);
}
}
public function setReceiveAdminEmail($choice)
{
$this->user->Option->receive_admin_email = $choice;
}
public function setReceiveActivitySummary(bool $choice)
{
$this->user->last_summary_email_date = $choice ? \XF::$time : null;
}
public function setAvatarUrl($url)
{
$this->avatarUrl = $url;
}
public function setPreRegActionKey($key)
{
$this->preRegActionKey = $key;
}
public function getPreRegContent()
{
return $this->preRegContent;
}
public function checkForSpam()
{
$user = $this->user;
$userChecker = $this->app->spam()->userChecker();
$userChecker->check($user, ['preRegActionKey' => $this->preRegActionKey]);
$decision = $userChecker->getFinalDecision();
switch ($decision)
{
case 'denied':
$phrase = \XF::phrase('spam_prevention_registration_rejected')->render();
$user->setUserRejected($this->app->stringFormatter()->wholeWordTrim($phrase, 200));
break;
case 'moderated':
$user->user_state = 'moderated';
break;
}
}
public function skipEmailConfirmation($skip = true)
{
$this->skipEmailConfirm = $skip;
}
protected function _validate()
{
$this->finalSetup();
$user = $this->user;
$user->preSave();
$this->applyExtraValidation();
return $user->getErrors();
}
protected function finalSetup()
{
$user = $this->user;
if (!$user->getErrors() && $user->email && !$this->avatarUrl)
{
if ($this->app->options()->gravatarEnable && $this->app->validator('Gravatar')->isValid($user->email))
{
$user->gravatar = $user->email;
}
}
$this->setInitialUserState();
$this->setPolicyAcceptance();
}
protected function setInitialUserState()
{
$user = $this->user;
$options = $this->app->options();
if ($user->user_state != 'valid')
{
return; // We have likely already set the user state elsewhere, e.g. spam trigger
}
if ($options->registrationSetup['emailConfirmation'] && !$this->skipEmailConfirm)
{
$user->user_state = 'email_confirm';
}
else if ($options->registrationSetup['moderation'])
{
$user->user_state = 'moderated';
}
else
{
$user->user_state = 'valid';
}
}
protected function setPolicyAcceptance()
{
$user = $this->user;
if ($this->app->container('privacyPolicyUrl'))
{
$user->privacy_policy_accepted = \XF::$time;
}
if ($this->app->container('tosUrl'))
{
$user->terms_accepted = \XF::$time;
}
}
protected function applyExtraValidation()
{
$user = $this->user;
$options = $this->app->options();
$age = $user->Profile->getAge(true);
if ($options->registrationSetup['requireDob'])
{
if (!$age)
{
// incomplete dob
$user->error(\XF::phrase('please_enter_valid_date_of_birth'), 'dob');
}
else if ($options->registrationSetup['minimumAge'])
{
if ($age < intval($options->registrationSetup['minimumAge']))
{
$user->error(\XF::phrase('sorry_you_too_young_to_create_an_account'), 'dob');
}
}
}
if (!empty($options->registrationSetup['requireLocation']) && !$user->Profile->location)
{
$user->error(\XF::phrase('please_enter_valid_location'), 'location');
}
}
protected function _save()
{
$user = $this->user;
$user->save();
$this->app->spam()->userChecker()->logSpamTrigger('user', $user->user_id);
if ($this->logIp)
{
$ip = ($this->logIp === true ? $this->app->request()->getIp() : $this->logIp);
$this->writeIpLog($ip);
}
if ($this->preRegActionKey)
{
/** @var \XF\Repository\PreRegAction $preRegActionRepo */
$preRegActionRepo = $this->repository('XF:PreRegAction');
$preRegActionRepo->associateActionWithUser($this->preRegActionKey, $user->user_id);
}
$this->writeInitialChangeLogs();
$this->updateUserAchievements();
$this->sendRegistrationContact();
if ($this->avatarUrl)
{
// Only apply the avatar if the user would have permission. This reads the permission set directly
// to ensure that we check their "real" permissions. Otherwise, if this is set and the user hasn't gone
// directly to the valid state, the permission is likely to be a false negative.
$permissions = $this->app->permissionCache()->getPermissionSet(
$user->getValue('permission_combination_id')
);
if ($permissions->hasGlobalPermission('avatar', 'allowed'))
{
$this->applyAvatarFromUrl($this->avatarUrl);
}
}
return $user;
}
protected function writeIpLog($ip)
{
$user = $this->user;
/** @var \XF\Repository\IP $ipRepo */
$ipRepo = $this->repository('XF:Ip');
$ipRepo->logIp($user->user_id, $ip, 'user', $user->user_id, 'register');
}
protected function writeInitialChangeLogs()
{
/** @var \XF\Repository\ChangeLog $changeLogRepo */
$changeLogRepo = $this->repository('XF:ChangeLog');
$user = $this->user;
$changes = [];
if ($this->app->options()->registrationSetup['requireEmailChoice'])
{
$changes['receive_admin_email'] = [0, $user->Option->receive_admin_email ? 1 : 0];
}
if ($this->app->container('privacyPolicyUrl'))
{
$changes['privacy_policy_accepted'] = [0, \XF::$time];
}
if ($this->app->container('tosUrl'))
{
$changes['terms_accepted'] = [0, \XF::$time];
}
if ($changes)
{
$changeLogRepo->logChanges('user', $user->user_id, $changes, $user->user_id);
}
}
protected function updateUserAchievements()
{
/** @var \XF\Repository\UserGroupPromotion $userGroupPromotionRepo */
$userGroupPromotionRepo = $this->repository('XF:UserGroupPromotion');
$userGroupPromotionRepo->updatePromotionsForUser($this->user);
if ($this->app->options()->enableTrophies)
{
/** @var \XF\Repository\Trophy $trophyRepo */
$trophyRepo = $this->repository('XF:Trophy');
$trophyRepo->updateTrophiesForUser($this->user);
}
}
protected function sendRegistrationContact()
{
$user = $this->user;
if ($user->user_state == 'email_confirm')
{
/** @var \XF\Service\User\EmailConfirmation $emailConfirmation */
$emailConfirmation = $this->service('XF:User\EmailConfirmation', $user);
$emailConfirmation->triggerConfirmation();
}
else if ($user->user_state == 'valid')
{
/** @var \XF\Service\User\RegistrationComplete $regComplete */
$regComplete = $this->service('XF:User\RegistrationComplete', $user);
$regComplete->triggerCompletionActions();
$this->preRegContent = $regComplete->getPreRegContent();
}
}
public function applyAvatarFromUrl($url)
{
if (!$this->user->user_id)
{
throw new \LogicException("User is not saved yet");
}
$app = $this->app;
$validator = $app->validator('Url');
if (!$validator->isValid($url))
{
return false;
}
$tempFile = \XF\Util\File::getTempFile();
if ($app->http()->reader()->getUntrusted($url, [], $tempFile))
{
/** @var \XF\Service\User\Avatar $avatarService */
$avatarService = $this->service('XF:User\Avatar', $this->user);
if (!$avatarService->setImage($tempFile))
{
return false;
}
return $avatarService->updateAvatar();
}
else
{
return false;
}
}
}