1<?php
2
3namespace OAuth\OAuth1\Service;
4
5use OAuth\Common\Consumer\CredentialsInterface;
6use OAuth\Common\Storage\TokenStorageInterface;
7use OAuth\Common\Http\Exception\TokenResponseException;
8use OAuth\Common\Http\Client\ClientInterface;
9use OAuth\Common\Http\Uri\UriInterface;
10use OAuth\OAuth1\Signature\SignatureInterface;
11use OAuth\OAuth1\Token\TokenInterface;
12use OAuth\OAuth1\Token\StdOAuth1Token;
13use OAuth\Common\Service\AbstractService as BaseAbstractService;
14
15abstract class AbstractService extends BaseAbstractService implements ServiceInterface
16{
17    /** @const OAUTH_VERSION */
18    const OAUTH_VERSION = 1;
19
20    /** @var SignatureInterface */
21    protected $signature;
22
23    /** @var UriInterface|null */
24    protected $baseApiUri;
25
26    /**
27     * {@inheritDoc}
28     */
29    public function __construct(
30        CredentialsInterface $credentials,
31        ClientInterface $httpClient,
32        TokenStorageInterface $storage,
33        SignatureInterface $signature,
34        UriInterface $baseApiUri = null
35    ) {
36        parent::__construct($credentials, $httpClient, $storage);
37
38        $this->signature = $signature;
39        $this->baseApiUri = $baseApiUri;
40
41        $this->signature->setHashingAlgorithm($this->getSignatureMethod());
42    }
43
44    /**
45     * {@inheritDoc}
46     */
47    public function requestRequestToken()
48    {
49        $authorizationHeader = array('Authorization' => $this->buildAuthorizationHeaderForTokenRequest());
50        $headers = array_merge($authorizationHeader, $this->getExtraOAuthHeaders());
51
52        $responseBody = $this->httpClient->retrieveResponse($this->getRequestTokenEndpoint(), array(), $headers);
53
54        $token = $this->parseRequestTokenResponse($responseBody);
55        $this->storage->storeAccessToken($this->service(), $token);
56
57        return $token;
58    }
59
60    /**
61     * {@inheritdoc}
62     */
63    public function getAuthorizationUri(array $additionalParameters = array())
64    {
65        // Build the url
66        $url = clone $this->getAuthorizationEndpoint();
67        foreach ($additionalParameters as $key => $val) {
68            $url->addToQuery($key, $val);
69        }
70
71        return $url;
72    }
73
74    /**
75     * {@inheritDoc}
76     */
77    public function requestAccessToken($token, $verifier, $tokenSecret = null)
78    {
79        if (is_null($tokenSecret)) {
80            $storedRequestToken = $this->storage->retrieveAccessToken($this->service());
81            $tokenSecret = $storedRequestToken->getRequestTokenSecret();
82        }
83        $this->signature->setTokenSecret($tokenSecret);
84
85        $bodyParams = array(
86            'oauth_verifier' => $verifier,
87        );
88
89        $authorizationHeader = array(
90            'Authorization' => $this->buildAuthorizationHeaderForAPIRequest(
91                'POST',
92                $this->getAccessTokenEndpoint(),
93                $this->storage->retrieveAccessToken($this->service()),
94                $bodyParams
95            )
96        );
97
98        $headers = array_merge($authorizationHeader, $this->getExtraOAuthHeaders());
99
100        $responseBody = $this->httpClient->retrieveResponse($this->getAccessTokenEndpoint(), $bodyParams, $headers);
101
102        $token = $this->parseAccessTokenResponse($responseBody);
103        $this->storage->storeAccessToken($this->service(), $token);
104
105        return $token;
106    }
107
108    /**
109     * Refreshes an OAuth1 access token
110     * @param  TokenInterface $token
111     * @return TokenInterface $token
112     */
113    public function refreshAccessToken(TokenInterface $token)
114    {
115    }
116
117    /**
118     * Sends an authenticated API request to the path provided.
119     * If the path provided is not an absolute URI, the base API Uri (must be passed into constructor) will be used.
120     *
121     * @param string|UriInterface $path
122     * @param string              $method       HTTP method
123     * @param array               $body         Request body if applicable (key/value pairs)
124     * @param array               $extraHeaders Extra headers if applicable.
125     *                                          These will override service-specific any defaults.
126     *
127     * @return string
128     */
129    public function request($path, $method = 'GET', $body = null, array $extraHeaders = array())
130    {
131        $uri = $this->determineRequestUriFromPath($path, $this->baseApiUri);
132
133        /** @var $token StdOAuth1Token */
134        $token = $this->storage->retrieveAccessToken($this->service());
135        $extraHeaders = array_merge($this->getExtraApiHeaders(), $extraHeaders);
136        $authorizationHeader = array(
137            'Authorization' => $this->buildAuthorizationHeaderForAPIRequest($method, $uri, $token, $body)
138        );
139        $headers = array_merge($authorizationHeader, $extraHeaders);
140
141        return $this->httpClient->retrieveResponse($uri, $body, $headers, $method);
142    }
143
144    /**
145     * Return any additional headers always needed for this service implementation's OAuth calls.
146     *
147     * @return array
148     */
149    protected function getExtraOAuthHeaders()
150    {
151        return array();
152    }
153
154    /**
155     * Return any additional headers always needed for this service implementation's API calls.
156     *
157     * @return array
158     */
159    protected function getExtraApiHeaders()
160    {
161        return array();
162    }
163
164    /**
165     * Builds the authorization header for getting an access or request token.
166     *
167     * @param array $extraParameters
168     *
169     * @return string
170     */
171    protected function buildAuthorizationHeaderForTokenRequest(array $extraParameters = array())
172    {
173        $parameters = $this->getBasicAuthorizationHeaderInfo();
174        $parameters = array_merge($parameters, $extraParameters);
175        $parameters['oauth_signature'] = $this->signature->getSignature(
176            $this->getRequestTokenEndpoint(),
177            $parameters,
178            'POST'
179        );
180
181        $authorizationHeader = 'OAuth ';
182        $delimiter = '';
183        foreach ($parameters as $key => $value) {
184            $authorizationHeader .= $delimiter . rawurlencode($key) . '="' . rawurlencode($value) . '"';
185
186            $delimiter = ', ';
187        }
188
189        return $authorizationHeader;
190    }
191
192    /**
193     * Builds the authorization header for an authenticated API request
194     *
195     * @param string         $method
196     * @param UriInterface   $uri        The uri the request is headed
197     * @param TokenInterface $token
198     * @param array          $bodyParams Request body if applicable (key/value pairs)
199     *
200     * @return string
201     */
202    protected function buildAuthorizationHeaderForAPIRequest(
203        $method,
204        UriInterface $uri,
205        TokenInterface $token,
206        $bodyParams = null
207    ) {
208        $this->signature->setTokenSecret($token->getAccessTokenSecret());
209        $authParameters = $this->getBasicAuthorizationHeaderInfo();
210        if (isset($authParameters['oauth_callback'])) {
211            unset($authParameters['oauth_callback']);
212        }
213
214        $authParameters = array_merge($authParameters, array('oauth_token' => $token->getAccessToken()));
215
216        $signatureParams = (is_array($bodyParams)) ? array_merge($authParameters, $bodyParams) : $authParameters;
217        $authParameters['oauth_signature'] = $this->signature->getSignature($uri, $signatureParams, $method);
218
219        if (is_array($bodyParams) && isset($bodyParams['oauth_session_handle'])) {
220            $authParameters['oauth_session_handle'] = $bodyParams['oauth_session_handle'];
221            unset($bodyParams['oauth_session_handle']);
222        }
223
224        $authorizationHeader = 'OAuth ';
225        $delimiter = '';
226
227        foreach ($authParameters as $key => $value) {
228            $authorizationHeader .= $delimiter . rawurlencode($key) . '="' . rawurlencode($value) . '"';
229            $delimiter = ', ';
230        }
231
232        return $authorizationHeader;
233    }
234
235    /**
236     * Builds the authorization header array.
237     *
238     * @return array
239     */
240    protected function getBasicAuthorizationHeaderInfo()
241    {
242        $dateTime = new \DateTime();
243        $headerParameters = array(
244            'oauth_callback'         => $this->credentials->getCallbackUrl(),
245            'oauth_consumer_key'     => $this->credentials->getConsumerId(),
246            'oauth_nonce'            => $this->generateNonce(),
247            'oauth_signature_method' => $this->getSignatureMethod(),
248            'oauth_timestamp'        => $dateTime->format('U'),
249            'oauth_version'          => $this->getVersion(),
250        );
251
252        return $headerParameters;
253    }
254
255    /**
256     * Pseudo random string generator used to build a unique string to sign each request
257     *
258     * @param int $length
259     *
260     * @return string
261     */
262    protected function generateNonce($length = 32)
263    {
264        $characters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890';
265
266        $nonce = '';
267        $maxRand = strlen($characters)-1;
268        for ($i = 0; $i < $length; $i++) {
269            $nonce.= $characters[rand(0, $maxRand)];
270        }
271
272        return $nonce;
273    }
274
275    /**
276     * @return string
277     */
278    protected function getSignatureMethod()
279    {
280        return 'HMAC-SHA1';
281    }
282
283    /**
284     * This returns the version used in the authorization header of the requests
285     *
286     * @return string
287     */
288    protected function getVersion()
289    {
290        return '1.0';
291    }
292
293    /**
294     * Parses the request token response and returns a TokenInterface.
295     * This is only needed to verify the `oauth_callback_confirmed` parameter. The actual
296     * parsing logic is contained in the access token parser.
297     *
298     * @abstract
299     *
300     * @param string $responseBody
301     *
302     * @return TokenInterface
303     *
304     * @throws TokenResponseException
305     */
306    abstract protected function parseRequestTokenResponse($responseBody);
307
308    /**
309     * Parses the access token response and returns a TokenInterface.
310     *
311     * @abstract
312     *
313     * @param string $responseBody
314     *
315     * @return TokenInterface
316     *
317     * @throws TokenResponseException
318     */
319    abstract protected function parseAccessTokenResponse($responseBody);
320}
321