Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
58 / 58
100.00% covered (success)
100.00%
3 / 3
CRAP
100.00% covered (success)
100.00%
1 / 1
AbstractOAuthProvider
100.00% covered (success)
100.00%
58 / 58
100.00% covered (success)
100.00%
3 / 3
20
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 getCurrentUri
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
4
 curlRequest
100.00% covered (success)
100.00%
42 / 42
100.00% covered (success)
100.00%
1 / 1
15
1<?php
2
3namespace Miniframe\SocialLogin\Provider;
4
5use Miniframe\Core\Config;
6use Miniframe\Core\Request;
7
8abstract class AbstractOAuthProvider implements ProviderInterface
9{
10    /**
11     * Short name of the provider (Example: 'twitter')
12     *
13     * @var string
14     */
15    protected $shortName;
16    /**
17     * Reference to the Request object
18     *
19     * @var Request
20     */
21    protected $request;
22    /**
23     * Base href for this application
24     *
25     * @var string
26     */
27    protected $baseHref;
28    /**
29     * User agent for the cURL requests
30     *
31     * @var string
32     */
33    protected $userAgent = 'Miniframe Social Login client (https://bitbucket.org/miniframe/miniframe-social-login/)';
34
35    /**
36     * Creates a new OAuth provider
37     *
38     * @param Request $request Reference to the Request object.
39     * @param Config  $config  Reference to the Config object.
40     */
41    public function __construct(Request $request, Config $config)
42    {
43        $this->request = $request;
44        $this->shortName = strtolower(array_reverse(explode('\\', get_called_class()))[0]);
45        $this->baseHref = $config->get('framework', 'base_href');
46    }
47
48    /**
49     * Returns the current URI
50     *
51     * @return string
52     */
53    protected function getCurrentUri(): string
54    {
55        $parts = explode(':', $this->request->getServer('HTTP_HOST'), 2);
56        if (count($parts) == 2) {
57            $serverHost = $parts[0];
58            $serverPort = $parts[1];
59        } else {
60            $serverHost = $this->request->getServer('HTTP_HOST');
61            $serverPort = (int)$this->request->getServer('SERVER_PORT');
62        }
63        $requestPath = explode('?', $this->request->getServer('REQUEST_URI'))[0];
64
65        if ($this->request->getServer('HTTPS')) {
66            $serverProtocol = 'https://';
67            $defaultPort = 443;
68        } else {
69            $serverProtocol = 'http://';
70            $defaultPort = 80;
71        }
72        return $serverProtocol . $serverHost . ($defaultPort == $serverPort ? '' : ':' . $serverPort) . $requestPath;
73    }
74
75
76    /**
77     * Performs a cURL request
78     *
79     * @param string     $url           The URL.
80     * @param string     $requestMethod The request method (GET, POST).
81     * @param array|null $data          Request data.
82     * @param array      $headers       Request headers.
83     * @param boolean    $raw           Set to true to get the raw response.
84     *
85     * @return array
86     */
87    protected function curlRequest(
88        string $url,
89        string $requestMethod,
90        array $data = null,
91        array $headers = array(),
92        bool $raw = false
93    ): array {
94        // Default options
95        $curlOptions = array();
96        $curlOptions[CURLOPT_URL] = $url;
97        $curlOptions[CURLOPT_RETURNTRANSFER] = true;
98        $curlOptions[CURLOPT_HEADER] = true;
99        $curlOptions[CURLOPT_HTTPHEADER] = $headers;
100        $curlOptions[CURLOPT_USERAGENT] = $this->userAgent;
101
102        if (strtoupper($requestMethod) == 'GET') {
103            $curlOptions[CURLOPT_HTTPGET] = true;
104            if ($data) {
105                $curlOptions[CURLOPT_URL] .= (strpos('?', $curlOptions[CURLOPT_URL]) !== false ? '&' : '?')
106                    . http_build_query($data);
107            }
108        } elseif (strtoupper($requestMethod) == 'POST') {
109            // When doing a regular POST request, CURLOPT_POSTFIELDS is provided as string, the content-type will be
110            // 'application/x-www-form-urlencoded'.
111            $curlOptions[CURLOPT_POSTFIELDS] = http_build_query($data);
112            $curlOptions[CURLOPT_POST] = true;
113        } else {
114            throw new \RuntimeException('Can\'t handle request method "' . strtoupper($requestMethod) . '"');
115        }
116
117        // Executes the request
118        $ch = curl_init();
119        curl_setopt_array($ch, $curlOptions);
120        $data = curl_exec($ch);
121
122        // When something went wrong, throw an exception
123        if ($data === false) {
124            throw new \RuntimeException('Unexpected cURL error #' . curl_errno($ch) . ': ' . curl_error($ch));
125        }
126
127        // Fetch info and close handle
128        $info = curl_getinfo($ch);
129        curl_close($ch);
130
131        // We'll expect all OK variants, but nothing else.
132        if ($info['http_code'] < 200 || $info['http_code'] > 299) {
133            throw new \RuntimeException('HTTP #' . $info['http_code'] . ' error');
134        }
135
136        // Splits data
137        $responseHeaders = substr($data, 0, $info['header_size']);
138        $responseBody = substr($data, $info['header_size']);
139        $contentType = $info['content_type'] ?? 'application/json';
140        if (strpos($contentType, ';') !== false) {
141            $contentType = explode(';', $contentType)[0];
142        }
143
144        // Raw response
145        if ($raw) {
146            return array(
147                'responseHeaders' => $responseHeaders,
148                'responseBody' => $responseBody,
149                'contentType' => $contentType,
150                'connectionInfo' => $info,
151            );
152        }
153
154        // Parsed response
155        switch ($contentType) {
156            case 'application/json':
157                $responseData = json_decode($responseBody, true, 512, JSON_THROW_ON_ERROR);
158                break;
159            case 'application/x-www-form-urlencoded':
160                parse_str($responseBody, $responseData);
161                break;
162            default:
163                // Try to autodetect; some providers (Twitter!) don't use the correct Content-type header.
164                if (preg_match('/^[a-z=0-9_\-&%]+$/i', $responseBody) && strpos($responseBody, '=') !== false) {
165                    parse_str($responseBody, $responseData);
166                } else {
167                    throw new \RuntimeException('Could not decode the ' . $contentType . ' response');
168                }
169        }
170
171        return $responseData;
172    }
173}