1<?php
2/**
3 * Copyright 2017 Facebook, Inc.
4 *
5 * You are hereby granted a non-exclusive, worldwide, royalty-free license to
6 * use, copy, modify, and distribute this software in source code or binary
7 * form for use in connection with the web services and APIs provided by
8 * Facebook.
9 *
10 * As with any software that integrates with the Facebook platform, your use
11 * of this software is subject to the Facebook Developer Principles and
12 * Policies [http://developers.facebook.com/policy/]. This copyright notice
13 * shall be included in all copies or substantial portions of the software.
14 *
15 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
18 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
20 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
21 * DEALINGS IN THE SOFTWARE.
22 *
23 */
24namespace Facebook;
25
26use Facebook\Authentication\AccessToken;
27use Facebook\Url\FacebookUrlManipulator;
28use Facebook\FileUpload\FacebookFile;
29use Facebook\FileUpload\FacebookVideo;
30use Facebook\Http\RequestBodyMultipart;
31use Facebook\Http\RequestBodyUrlEncoded;
32use Facebook\Exceptions\FacebookSDKException;
33
34/**
35 * Class Request
36 *
37 * @package Facebook
38 */
39class FacebookRequest
40{
41    /**
42     * @var FacebookApp The Facebook app entity.
43     */
44    protected $app;
45
46    /**
47     * @var string|null The access token to use for this request.
48     */
49    protected $accessToken;
50
51    /**
52     * @var string The HTTP method for this request.
53     */
54    protected $method;
55
56    /**
57     * @var string The Graph endpoint for this request.
58     */
59    protected $endpoint;
60
61    /**
62     * @var array The headers to send with this request.
63     */
64    protected $headers = [];
65
66    /**
67     * @var array The parameters to send with this request.
68     */
69    protected $params = [];
70
71    /**
72     * @var array The files to send with this request.
73     */
74    protected $files = [];
75
76    /**
77     * @var string ETag to send with this request.
78     */
79    protected $eTag;
80
81    /**
82     * @var string Graph version to use for this request.
83     */
84    protected $graphVersion;
85
86    /**
87     * Creates a new Request entity.
88     *
89     * @param FacebookApp|null        $app
90     * @param AccessToken|string|null $accessToken
91     * @param string|null             $method
92     * @param string|null             $endpoint
93     * @param array|null              $params
94     * @param string|null             $eTag
95     * @param string|null             $graphVersion
96     */
97    public function __construct(FacebookApp $app = null, $accessToken = null, $method = null, $endpoint = null, array $params = [], $eTag = null, $graphVersion = null)
98    {
99        $this->setApp($app);
100        $this->setAccessToken($accessToken);
101        $this->setMethod($method);
102        $this->setEndpoint($endpoint);
103        $this->setParams($params);
104        $this->setETag($eTag);
105        $this->graphVersion = $graphVersion ?: Facebook::DEFAULT_GRAPH_VERSION;
106    }
107
108    /**
109     * Set the access token for this request.
110     *
111     * @param AccessToken|string|null
112     *
113     * @return FacebookRequest
114     */
115    public function setAccessToken($accessToken)
116    {
117        $this->accessToken = $accessToken;
118        if ($accessToken instanceof AccessToken) {
119            $this->accessToken = $accessToken->getValue();
120        }
121
122        return $this;
123    }
124
125    /**
126     * Sets the access token with one harvested from a URL or POST params.
127     *
128     * @param string $accessToken The access token.
129     *
130     * @return FacebookRequest
131     *
132     * @throws FacebookSDKException
133     */
134    public function setAccessTokenFromParams($accessToken)
135    {
136        $existingAccessToken = $this->getAccessToken();
137        if (!$existingAccessToken) {
138            $this->setAccessToken($accessToken);
139        } elseif ($accessToken !== $existingAccessToken) {
140            throw new FacebookSDKException('Access token mismatch. The access token provided in the FacebookRequest and the one provided in the URL or POST params do not match.');
141        }
142
143        return $this;
144    }
145
146    /**
147     * Return the access token for this request.
148     *
149     * @return string|null
150     */
151    public function getAccessToken()
152    {
153        return $this->accessToken;
154    }
155
156    /**
157     * Return the access token for this request as an AccessToken entity.
158     *
159     * @return AccessToken|null
160     */
161    public function getAccessTokenEntity()
162    {
163        return $this->accessToken ? new AccessToken($this->accessToken) : null;
164    }
165
166    /**
167     * Set the FacebookApp entity used for this request.
168     *
169     * @param FacebookApp|null $app
170     */
171    public function setApp(FacebookApp $app = null)
172    {
173        $this->app = $app;
174    }
175
176    /**
177     * Return the FacebookApp entity used for this request.
178     *
179     * @return FacebookApp
180     */
181    public function getApp()
182    {
183        return $this->app;
184    }
185
186    /**
187     * Generate an app secret proof to sign this request.
188     *
189     * @return string|null
190     */
191    public function getAppSecretProof()
192    {
193        if (!$accessTokenEntity = $this->getAccessTokenEntity()) {
194            return null;
195        }
196
197        return $accessTokenEntity->getAppSecretProof($this->app->getSecret());
198    }
199
200    /**
201     * Validate that an access token exists for this request.
202     *
203     * @throws FacebookSDKException
204     */
205    public function validateAccessToken()
206    {
207        $accessToken = $this->getAccessToken();
208        if (!$accessToken) {
209            throw new FacebookSDKException('You must provide an access token.');
210        }
211    }
212
213    /**
214     * Set the HTTP method for this request.
215     *
216     * @param string
217     */
218    public function setMethod($method)
219    {
220        $this->method = strtoupper($method);
221    }
222
223    /**
224     * Return the HTTP method for this request.
225     *
226     * @return string
227     */
228    public function getMethod()
229    {
230        return $this->method;
231    }
232
233    /**
234     * Validate that the HTTP method is set.
235     *
236     * @throws FacebookSDKException
237     */
238    public function validateMethod()
239    {
240        if (!$this->method) {
241            throw new FacebookSDKException('HTTP method not specified.');
242        }
243
244        if (!in_array($this->method, ['GET', 'POST', 'DELETE'])) {
245            throw new FacebookSDKException('Invalid HTTP method specified.');
246        }
247    }
248
249    /**
250     * Set the endpoint for this request.
251     *
252     * @param string
253     *
254     * @return FacebookRequest
255     *
256     * @throws FacebookSDKException
257     */
258    public function setEndpoint($endpoint)
259    {
260        // Harvest the access token from the endpoint to keep things in sync
261        $params = FacebookUrlManipulator::getParamsAsArray($endpoint);
262        if (isset($params['access_token'])) {
263            $this->setAccessTokenFromParams($params['access_token']);
264        }
265
266        // Clean the token & app secret proof from the endpoint.
267        $filterParams = ['access_token', 'appsecret_proof'];
268        $this->endpoint = FacebookUrlManipulator::removeParamsFromUrl($endpoint, $filterParams);
269
270        return $this;
271    }
272
273    /**
274     * Return the endpoint for this request.
275     *
276     * @return string
277     */
278    public function getEndpoint()
279    {
280        // For batch requests, this will be empty
281        return $this->endpoint;
282    }
283
284    /**
285     * Generate and return the headers for this request.
286     *
287     * @return array
288     */
289    public function getHeaders()
290    {
291        $headers = static::getDefaultHeaders();
292
293        if ($this->eTag) {
294            $headers['If-None-Match'] = $this->eTag;
295        }
296
297        return array_merge($this->headers, $headers);
298    }
299
300    /**
301     * Set the headers for this request.
302     *
303     * @param array $headers
304     */
305    public function setHeaders(array $headers)
306    {
307        $this->headers = array_merge($this->headers, $headers);
308    }
309
310    /**
311     * Sets the eTag value.
312     *
313     * @param string $eTag
314     */
315    public function setETag($eTag)
316    {
317        $this->eTag = $eTag;
318    }
319
320    /**
321     * Set the params for this request.
322     *
323     * @param array $params
324     *
325     * @return FacebookRequest
326     *
327     * @throws FacebookSDKException
328     */
329    public function setParams(array $params = [])
330    {
331        if (isset($params['access_token'])) {
332            $this->setAccessTokenFromParams($params['access_token']);
333        }
334
335        // Don't let these buggers slip in.
336        unset($params['access_token'], $params['appsecret_proof']);
337
338        // @TODO Refactor code above with this
339        //$params = $this->sanitizeAuthenticationParams($params);
340        $params = $this->sanitizeFileParams($params);
341        $this->dangerouslySetParams($params);
342
343        return $this;
344    }
345
346    /**
347     * Set the params for this request without filtering them first.
348     *
349     * @param array $params
350     *
351     * @return FacebookRequest
352     */
353    public function dangerouslySetParams(array $params = [])
354    {
355        $this->params = array_merge($this->params, $params);
356
357        return $this;
358    }
359
360    /**
361     * Iterate over the params and pull out the file uploads.
362     *
363     * @param array $params
364     *
365     * @return array
366     */
367    public function sanitizeFileParams(array $params)
368    {
369        foreach ($params as $key => $value) {
370            if ($value instanceof FacebookFile) {
371                $this->addFile($key, $value);
372                unset($params[$key]);
373            }
374        }
375
376        return $params;
377    }
378
379    /**
380     * Add a file to be uploaded.
381     *
382     * @param string       $key
383     * @param FacebookFile $file
384     */
385    public function addFile($key, FacebookFile $file)
386    {
387        $this->files[$key] = $file;
388    }
389
390    /**
391     * Removes all the files from the upload queue.
392     */
393    public function resetFiles()
394    {
395        $this->files = [];
396    }
397
398    /**
399     * Get the list of files to be uploaded.
400     *
401     * @return array
402     */
403    public function getFiles()
404    {
405        return $this->files;
406    }
407
408    /**
409     * Let's us know if there is a file upload with this request.
410     *
411     * @return boolean
412     */
413    public function containsFileUploads()
414    {
415        return !empty($this->files);
416    }
417
418    /**
419     * Let's us know if there is a video upload with this request.
420     *
421     * @return boolean
422     */
423    public function containsVideoUploads()
424    {
425        foreach ($this->files as $file) {
426            if ($file instanceof FacebookVideo) {
427                return true;
428            }
429        }
430
431        return false;
432    }
433
434    /**
435     * Returns the body of the request as multipart/form-data.
436     *
437     * @return RequestBodyMultipart
438     */
439    public function getMultipartBody()
440    {
441        $params = $this->getPostParams();
442
443        return new RequestBodyMultipart($params, $this->files);
444    }
445
446    /**
447     * Returns the body of the request as URL-encoded.
448     *
449     * @return RequestBodyUrlEncoded
450     */
451    public function getUrlEncodedBody()
452    {
453        $params = $this->getPostParams();
454
455        return new RequestBodyUrlEncoded($params);
456    }
457
458    /**
459     * Generate and return the params for this request.
460     *
461     * @return array
462     */
463    public function getParams()
464    {
465        $params = $this->params;
466
467        $accessToken = $this->getAccessToken();
468        if ($accessToken) {
469            $params['access_token'] = $accessToken;
470            $params['appsecret_proof'] = $this->getAppSecretProof();
471        }
472
473        return $params;
474    }
475
476    /**
477     * Only return params on POST requests.
478     *
479     * @return array
480     */
481    public function getPostParams()
482    {
483        if ($this->getMethod() === 'POST') {
484            return $this->getParams();
485        }
486
487        return [];
488    }
489
490    /**
491     * The graph version used for this request.
492     *
493     * @return string
494     */
495    public function getGraphVersion()
496    {
497        return $this->graphVersion;
498    }
499
500    /**
501     * Generate and return the URL for this request.
502     *
503     * @return string
504     */
505    public function getUrl()
506    {
507        $this->validateMethod();
508
509        $graphVersion = FacebookUrlManipulator::forceSlashPrefix($this->graphVersion);
510        $endpoint = FacebookUrlManipulator::forceSlashPrefix($this->getEndpoint());
511
512        $url = $graphVersion . $endpoint;
513
514        if ($this->getMethod() !== 'POST') {
515            $params = $this->getParams();
516            $url = FacebookUrlManipulator::appendParamsToUrl($url, $params);
517        }
518
519        return $url;
520    }
521
522    /**
523     * Return the default headers that every request should use.
524     *
525     * @return array
526     */
527    public static function getDefaultHeaders()
528    {
529        return [
530            'User-Agent' => 'fb-php-' . Facebook::VERSION,
531            'Accept-Encoding' => '*',
532        ];
533    }
534}
535