Lines 38.09% 32 / 84
Methods 78.57% 11 / 14
Classes 0.00% 0 / 1
Covered by tests of size
Name Lines Methods CRAP
 __construct 100.00% 1 / 1 100.00% 1 / 1 1
 createUser 0.00% 0 / 44 0.00% 0 / 1 182
 buildRegistrationResponse 0.00% 0 / 7 0.00% 0 / 1 2
 isDomainAllowed 93.75% 15 / 16 0.00% 0 / 1 8.02
 [phpMyFAQ\Helper\AbstractHelper] setCategory 100.00% 2 / 2 100.00% 1 / 1 1
 [phpMyFAQ\Helper\AbstractHelper] getCategory 100.00% 1 / 1 100.00% 1 / 1 1
 [phpMyFAQ\Helper\AbstractHelper] category 100.00% 1 / 1 100.00% 1 / 1 1
 [phpMyFAQ\Helper\AbstractHelper] plurals 100.00% 1 / 1 100.00% 1 / 1 1
 [phpMyFAQ\Helper\AbstractHelper] setCategoryRelation 100.00% 2 / 2 100.00% 1 / 1 1
 [phpMyFAQ\Helper\AbstractHelper] setTags 100.00% 2 / 2 100.00% 1 / 1 1
 [phpMyFAQ\Helper\AbstractHelper] setPlurals 100.00% 2 / 2 100.00% 1 / 1 1
 [phpMyFAQ\Helper\AbstractHelper] setSessionId 100.00% 2 / 2 100.00% 1 / 1 1
 [phpMyFAQ\Helper\AbstractHelper] setConfiguration 100.00% 2 / 2 100.00% 1 / 1 1
 [phpMyFAQ\Helper\AbstractHelper] getConfiguration 100.00% 1 / 1 100.00% 1 / 1 1
38class RegistrationHelper extends AbstractHelper
39{
40    /**
41     * RegistrationHelper constructor.
42     */
43    public function __construct(Configuration $configuration)
44    {
45        $this->configuration = $configuration;
46    }
47
48    /**
49     * Creates a new user account and saves the user data.
50     * If user generation was successful, account activation is sent via an email
51     * error message as an array.
52     * The password will be automatically generated and sent by email
53     * as soon if admin switches user to "active"
54     *
55     * @throws Exception|TransportExceptionInterface
56     */
57    public function createUser(string $userName, string $fullName, string $email, bool $isVisible): array
58    {
59        $user = new User($this->configuration);
60
61        // Account enumeration guard: a duplicate e-mail address or login name must be
62        // indistinguishable from a fresh registration. In those cases no account is created and
63        // the same generic "thank you" response is returned, so an attacker cannot probe which
64        // e-mails/logins already exist.
65        if ($email !== '' && $email !== '0') {
66            if (!$user->userdata instanceof UserData) {
67                $user->userdata = new UserData($this->configuration);
68            }
69
70            if ($user->userdata->emailExists($email)) {
71                return $this->buildRegistrationResponse();
72            }
73        }
74
75        try {
76            $created = $user->createUser($userName, '');
77        } catch (\Exception $exception) {
78            if (
79                $exception->getMessage() === User::ERROR_USER_LOGIN_NOT_UNIQUE
80                || $exception->getMessage() === User::ERROR_USER_EMAIL_NOT_UNIQUE
81            ) {
82                return $this->buildRegistrationResponse();
83            }
84
85            throw $exception;
86        }
87
88        if (!$created) {
89            return [
90                'registered' => false,
91                'error' => $user->error(),
92            ];
93        }
94
95        $user->userData()->set(['display_name', 'email', 'is_visible'], [$fullName, $email, $isVisible ? 1 : 0]);
96        $user->setStatus('blocked');
97
98        $isNowActive = !$this->configuration->get(item: 'spam.manualActivation') && $user->activateUser();
99        $adminMessage = 'To activate this user please use';
100        if ($isNowActive) {
101            // @todo add translation strings
102            $adminMessage =
103                'This user has been automatically activated, you can still'
104                . ' modify the users permissions or decline membership by visiting the admin section';
105        }
106
107        $text = sprintf(
108            'A new user has been registered:<br><br>Name: %s<br>Login name: %s<br><br>%s the administration at %s.',
109            $fullName,
110            $userName,
111            $adminMessage,
112            $this->configuration->getDefaultUrl() . '/admin/',
113        );
114        $mail = new Mail($this->configuration);
115        $mail->setReplyTo($email, $fullName);
116        $mail->addTo($this->configuration->getAdminEmail());
117
118        $emailRegSubject = Translation::get(key: 'emailRegSubject');
119        $mail->subject = Utils::resolveMarkers(
120            is_string($emailRegSubject) ? $emailRegSubject : '',
121            $this->configuration,
122        );
123        $mail->message = $text;
124        $mail->send();
125        unset($mail);
126
127        return $this->buildRegistrationResponse();
128    }
129
130    /**
131     * Builds the generic registration response. The same payload is returned for a real
132     * registration and for a suppressed duplicate, so the two cannot be told apart.
133     *
134     * @return array{registered: bool, success: string}
135     */
136    private function buildRegistrationResponse(): array
137    {
138        return [
139            'registered' => true,
140            'success' =>
141                trim(Translation::getString(key: 'successMessage'))
142                    . ' '
143                    . trim(Translation::getString(key: 'msgRegThankYou')),
144        ];
145    }
146
147    /**
148     * Returns true if the hostname of the given email address is allowed.
149     * otherwise false.
150     */
151    public function isDomainAllowed(string $email): bool
152    {
153        $whitelistedDomains = $this->configuration->get(item: 'security.domainWhiteListForRegistrations');
154
155        if ($whitelistedDomains === null || Strings::strlen(trim((string) $whitelistedDomains)) === 0) {
156            return true;
157        }
158
159        $whitelistedDomainList = explode(',', (string) $whitelistedDomains);
160
161        // Robust: validate email and extract domain safely; invalid emails are not allowed
162        $email = trim($email);
163        if ($email === '' || filter_var($email, FILTER_VALIDATE_EMAIL) === false) {
164            return false;
165        }
166
167        $atPos = strrpos(haystack: $email, needle: '@');
168        if ($atPos === false) {
169            return false; // should not happen after filter_var, but double-check
170        }
171
172        $hostnameToCheck = trim(substr($email, $atPos + 1));
173
174        foreach ($whitelistedDomainList as $hostname) {
175            if ($hostnameToCheck !== trim($hostname)) {
176                continue;
177            }
178
179            return true;
180        }
181
182        return false;
183    }
184}

Inherited from phpMyFAQ\Helper\AbstractHelper

47    public function setCategory(Category $Category): AbstractHelper
48    {
49        $this->Category = $Category;
50        return $this;
51    }
53    public function getCategory(): Category
54    {
55        return $this->category();
56    }
61    protected function category(): Category
62    {
63        return $this->Category ?? throw new \LogicException('setCategory() must be called before use.');
64    }
69    protected function plurals(): Plurals
70    {
71        return $this->plurals ?? throw new \LogicException('setPlurals() must be called before use.');
72    }
74    public function setCategoryRelation(Relation $categoryRelation): AbstractHelper
75    {
76        $this->categoryRelation = $categoryRelation;
77        return $this;
78    }
80    public function setTags(Tags $Tags): AbstractHelper
81    {
82        $this->Tags = $Tags;
83        return $this;
84    }
86    public function setPlurals(Plurals $plurals): AbstractHelper
87    {
88        $this->plurals = $plurals;
89        return $this;
90    }
92    public function setSessionId(int|string $sid): AbstractHelper
93    {
94        $this->sessionId = $sid;
95        return $this;
96    }
98    public function setConfiguration(Configuration $configuration): AbstractHelper
99    {
100        $this->configuration = $configuration;
101        return $this;
102    }
104    public function getConfiguration(): Configuration
105    {
106        return $this->configuration;
107    }