Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
65 / 65
100.00% covered (success)
100.00%
10 / 10
CRAP
100.00% covered (success)
100.00%
1 / 1
Email
100.00% covered (success)
100.00%
65 / 65
100.00% covered (success)
100.00%
10 / 10
24
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 authenticate
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
3
 getLogoSource
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getThemeColor
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 sendCode
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
4
 sendCodeMailer
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 sendCodeInternal
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
2
 validateCode
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
6
 getUserByEmail
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 generateCode
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2
3namespace Miniframe\SocialLogin\Provider;
4
5use Miniframe\Core\Config;
6use Miniframe\Core\Registry;
7use Miniframe\Core\Request;
8use Miniframe\Core\Response;
9use Miniframe\Mailer\Middleware\Mailer;
10use Miniframe\Mailer\Model\Recipient;
11use Miniframe\Middleware\Session;
12use Miniframe\Response\RedirectResponse;
13use Miniframe\SocialLogin\Middleware\SocialLogin;
14use Miniframe\SocialLogin\Model\User;
15
16class Email implements ProviderInterface
17{
18    /**
19     * Reference to the Request object.
20     *
21     * @var Request
22     */
23    protected $request;
24    /**
25     * Reference to the Config object.
26     *
27     * @var Config
28     */
29    protected $config;
30    /**
31     * Reference to the Session object.
32     *
33     * @var Session
34     */
35    protected $session;
36    /**
37     * The base URL for the social login page.
38     *
39     * @var string
40     */
41    protected $socialLoginPrefixUrl;
42
43    /**
44     * Initializes the Email service
45     *
46     * @param Request      $request              Reference to the Request object.
47     * @param Config       $config               Reference to the Config object.
48     * @param Session|null $session              Reference to the user session.
49     * @param string|null  $socialLoginPrefixUrl The base URL for the social login page.
50     */
51    public function __construct(
52        Request $request,
53        Config $config,
54        Session $session = null,
55        string $socialLoginPrefixUrl = null
56    ) {
57        // Parameters are required, but can't be defined as required by its interface
58        if ($session === null || $socialLoginPrefixUrl === null) {
59            throw new \BadFunctionCallException(
60                'The Email provider works differently, to enable this provider, see '
61                . 'https://bitbucket.org/miniframe/miniframe-social-login/src/v1/src/Provider/Email.md'
62            );
63        }
64
65        $this->request = $request;
66        $this->config = $config;
67        $this->session = $session;
68        $this->socialLoginPrefixUrl = $socialLoginPrefixUrl;
69    }
70
71    /**
72     * Starts the authentication process
73     *
74     * @return User
75     */
76    public function authenticate(): User
77    {
78        $state = SocialLogin::parseState($this->request->getRequest('state') ?? $this->request->getPost('state'));
79
80        // Validate code when specified
81        if ($this->request->getPost('code') !== null) {
82            // Validate the code and return the User object
83            return $this->getUserByEmail($this->validateCode($this->request->getPost('code')));
84        }
85
86        // When no code is specified, send email with code
87        if ($this->request->getPost('email') !== null) {
88            $this->sendCode($this->request->getPost('email'));
89
90
91            // Redirect to Code page
92            throw new RedirectResponse(
93                $this->socialLoginPrefixUrl . 'email/code?state=' . rawurlencode(SocialLogin::generateState($state))
94            );
95        }
96
97        // No email address, nor a code specified
98        throw new RedirectResponse(
99            $this->socialLoginPrefixUrl . 'login?state=' . rawurlencode(SocialLogin::generateState($state))
100        );
101    }
102
103    /**
104     * Returns the image source for the logo of this provider.
105     *
106     * @return string
107     */
108    public static function getLogoSource(): string
109    {
110        return 'data:image/svg+xml;base64,'
111            . base64_encode(file_get_contents(__DIR__ . '/../../templates/logos/Email.svg'));
112    }
113
114    /**
115     * Returns the theme color for this provider.
116     *
117     * @return string
118     */
119    public static function getThemeColor(): string
120    {
121        return '#c0c0c0';
122    }
123
124    /**
125     * Sends a code to a specific mail address
126     *
127     * @param string $email The mail address.
128     *
129     * @return void
130     */
131    protected function sendCode(string $email): void
132    {
133        // Validate email address
134        if (!trim($email) || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
135            throw new \InvalidArgumentException('Invalid email specified');
136        }
137
138        // Modify session
139        $code = $this->generateCode();
140        $session = $this->session->get('_SOCIALLOGIN');
141        $session['email']['address'] = $email;
142        $session['email']['expires'] = time() + (10 * 60);
143        $session['email']['code'] = $code;
144        $this->session->set('_SOCIALLOGIN', $session);
145
146        if (Registry::has(Mailer::class)) {
147            // Send the mail with the Mailer middleware
148            $mailer = 'sendCodeMailer';
149        } else {
150            // Send the mail with the internal mailer
151            $mailer = 'sendCodeInternal';
152        }
153        $this->$mailer(
154            $session['email']['code'],
155            $email,
156            $this->config->get('sociallogin-email', 'from_name'),
157            $this->config->get('sociallogin-email', 'from_email')
158        );
159    }
160
161    /**
162     * Send the code with the Mailer middleware
163     *
164     * @param string $code      The code.
165     * @param string $email     Recipient.
166     * @param string $fromName  Sender name.
167     * @param string $fromEmail Sender email.
168     *
169     * @return void
170     */
171    protected function sendCodeMailer(string $code, string $email, string $fromName, string $fromEmail): void
172    {
173        $mailer = Registry::get(Mailer::class);
174        $mailer->sendMail(
175            new Recipient($email),
176            'Your code to login',
177            new Response('Your code is ' . $code . "\r\n"),
178            false,
179            null,
180            [
181                'from_name' => $fromName,
182                'from_email' => $fromEmail,
183            ]
184        );
185    }
186
187    /**
188     * Send the code with the internal mail() function
189     *
190     * @param string $code      The code.
191     * @param string $email     Recipient.
192     * @param string $fromName  Sender name.
193     * @param string $fromEmail Sender email.
194     *
195     * @return void
196     */
197    protected function sendCodeInternal(string $code, string $email, string $fromName, string $fromEmail): void
198    {
199       // Define some variables
200        $recipient = '"' . ucfirst(explode('@', $email)[0]) . '" <' . $email . '>';
201        $sender = '"' . $fromName . '" <' . $fromEmail . '>';
202
203        // Set up mail
204        $headers = [
205            'From: ' . $sender,
206            'Reply-To: ' . $sender,
207            'To: ' . $recipient,
208            'X-Mailer: Miniframe Social Login',
209        ];
210        $body = 'Your code is ' . $code . "\r\n";
211
212        if (
213            !mail(
214                $recipient,
215                'Your code to login',
216                $body,
217                implode("\r\n", $headers)
218            )
219        ) {
220            throw new \RuntimeException('Couldn\t send email.');
221        }
222    }
223
224    /**
225     * Validates the code and returns the email address.
226     *
227     * @param string $code The code.
228     *
229     * @return string
230     */
231    protected function validateCode(string $code): string
232    {
233        // Validate session
234        $session = $this->session->get('_SOCIALLOGIN');
235        if (!isset($session['email']) || !isset($session['email']['code']) || !isset($session['email']['expires'])) {
236            throw new \InvalidArgumentException('No session available. Please try again.');
237        }
238        // Validate expiration
239        if ($session['email']['expires'] < time()) {
240            throw new \InvalidArgumentException('Session expired. Please try again.');
241        }
242        // Validate code
243        if (strtoupper($session['email']['code']) != strtoupper(trim($code))) {
244            throw new \InvalidArgumentException('Invalid code. Please try again.');
245        }
246        // Destroy session
247        $return = $session['email']['address'];
248        unset($session['email']);
249        $this->session->set('_SOCIALLOGIN', $session);
250
251        return $return;
252    }
253
254    /**
255     * Creates a User object based on an email address.
256     *
257     * @param string $email The mail address.
258     *
259     * @return User
260     */
261    protected function getUserByEmail(string $email): User
262    {
263        return new User(
264            $email,
265            $email,
266            ucfirst(explode('@', $email)[0]),
267            'https://s.gravatar.com/avatar/' . md5(strtolower(trim($email))) . '?s=80&d=identicon',
268            static::class,
269            array()
270        );
271    }
272
273    /**
274     * Generates a random code
275     *
276     * @param integer $digits Length of the code.
277     * @param string  $chars  The characters in the code (1 and I are removed because they look similar).
278     *
279     * @return string
280     */
281    protected function generateCode(int $digits = 6, string $chars = '23456789ABCDEFGHJKLMNPQRSTUVWXYZ'): string
282    {
283        $return = '';
284        for ($iterator = 0; $iterator < $digits; ++$iterator) {
285            $return .= substr($chars, rand(0, strlen($chars) - 1), 1);
286        }
287        return $return;
288    }
289}