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\Authentication\OAuth2Client;
28use Facebook\FileUpload\FacebookFile;
29use Facebook\FileUpload\FacebookResumableUploader;
30use Facebook\FileUpload\FacebookTransferChunk;
31use Facebook\FileUpload\FacebookVideo;
32use Facebook\GraphNodes\GraphEdge;
33use Facebook\Url\UrlDetectionInterface;
34use Facebook\Url\FacebookUrlDetectionHandler;
35use Facebook\PseudoRandomString\PseudoRandomStringGeneratorFactory;
36use Facebook\PseudoRandomString\PseudoRandomStringGeneratorInterface;
37use Facebook\HttpClients\HttpClientsFactory;
38use Facebook\PersistentData\PersistentDataFactory;
39use Facebook\PersistentData\PersistentDataInterface;
40use Facebook\Helpers\FacebookCanvasHelper;
41use Facebook\Helpers\FacebookJavaScriptHelper;
42use Facebook\Helpers\FacebookPageTabHelper;
43use Facebook\Helpers\FacebookRedirectLoginHelper;
44use Facebook\Exceptions\FacebookSDKException;
45
46/**
47 * Class Facebook
48 *
49 * @package Facebook
50 */
51class Facebook
52{
53    /**
54     * @const string Version number of the Facebook PHP SDK.
55     */
56    const VERSION = '5.6.2';
57
58    /**
59     * @const string Default Graph API version for requests.
60     */
61    const DEFAULT_GRAPH_VERSION = 'v2.10';
62
63    /**
64     * @const string The name of the environment variable that contains the app ID.
65     */
66    const APP_ID_ENV_NAME = 'FACEBOOK_APP_ID';
67
68    /**
69     * @const string The name of the environment variable that contains the app secret.
70     */
71    const APP_SECRET_ENV_NAME = 'FACEBOOK_APP_SECRET';
72
73    /**
74     * @var FacebookApp The FacebookApp entity.
75     */
76    protected $app;
77
78    /**
79     * @var FacebookClient The Facebook client service.
80     */
81    protected $client;
82
83    /**
84     * @var OAuth2Client The OAuth 2.0 client service.
85     */
86    protected $oAuth2Client;
87
88    /**
89     * @var UrlDetectionInterface|null The URL detection handler.
90     */
91    protected $urlDetectionHandler;
92
93    /**
94     * @var PseudoRandomStringGeneratorInterface|null The cryptographically secure pseudo-random string generator.
95     */
96    protected $pseudoRandomStringGenerator;
97
98    /**
99     * @var AccessToken|null The default access token to use with requests.
100     */
101    protected $defaultAccessToken;
102
103    /**
104     * @var string|null The default Graph version we want to use.
105     */
106    protected $defaultGraphVersion;
107
108    /**
109     * @var PersistentDataInterface|null The persistent data handler.
110     */
111    protected $persistentDataHandler;
112
113    /**
114     * @var FacebookResponse|FacebookBatchResponse|null Stores the last request made to Graph.
115     */
116    protected $lastResponse;
117
118    /**
119     * Instantiates a new Facebook super-class object.
120     *
121     * @param array $config
122     *
123     * @throws FacebookSDKException
124     */
125    public function __construct(array $config = [])
126    {
127        $config = array_merge([
128            'app_id' => getenv(static::APP_ID_ENV_NAME),
129            'app_secret' => getenv(static::APP_SECRET_ENV_NAME),
130            'default_graph_version' => static::DEFAULT_GRAPH_VERSION,
131            'enable_beta_mode' => false,
132            'http_client_handler' => null,
133            'persistent_data_handler' => null,
134            'pseudo_random_string_generator' => null,
135            'url_detection_handler' => null,
136        ], $config);
137
138        if (!$config['app_id']) {
139            throw new FacebookSDKException('Required "app_id" key not supplied in config and could not find fallback environment variable "' . static::APP_ID_ENV_NAME . '"');
140        }
141        if (!$config['app_secret']) {
142            throw new FacebookSDKException('Required "app_secret" key not supplied in config and could not find fallback environment variable "' . static::APP_SECRET_ENV_NAME . '"');
143        }
144
145        $this->app = new FacebookApp($config['app_id'], $config['app_secret']);
146        $this->client = new FacebookClient(
147            HttpClientsFactory::createHttpClient($config['http_client_handler']),
148            $config['enable_beta_mode']
149        );
150        $this->pseudoRandomStringGenerator = PseudoRandomStringGeneratorFactory::createPseudoRandomStringGenerator(
151            $config['pseudo_random_string_generator']
152        );
153        $this->setUrlDetectionHandler($config['url_detection_handler'] ?: new FacebookUrlDetectionHandler());
154        $this->persistentDataHandler = PersistentDataFactory::createPersistentDataHandler(
155            $config['persistent_data_handler']
156        );
157
158        if (isset($config['default_access_token'])) {
159            $this->setDefaultAccessToken($config['default_access_token']);
160        }
161
162        // @todo v6: Throw an InvalidArgumentException if "default_graph_version" is not set
163        $this->defaultGraphVersion = $config['default_graph_version'];
164    }
165
166    /**
167     * Returns the FacebookApp entity.
168     *
169     * @return FacebookApp
170     */
171    public function getApp()
172    {
173        return $this->app;
174    }
175
176    /**
177     * Returns the FacebookClient service.
178     *
179     * @return FacebookClient
180     */
181    public function getClient()
182    {
183        return $this->client;
184    }
185
186    /**
187     * Returns the OAuth 2.0 client service.
188     *
189     * @return OAuth2Client
190     */
191    public function getOAuth2Client()
192    {
193        if (!$this->oAuth2Client instanceof OAuth2Client) {
194            $app = $this->getApp();
195            $client = $this->getClient();
196            $this->oAuth2Client = new OAuth2Client($app, $client, $this->defaultGraphVersion);
197        }
198
199        return $this->oAuth2Client;
200    }
201
202    /**
203     * Returns the last response returned from Graph.
204     *
205     * @return FacebookResponse|FacebookBatchResponse|null
206     */
207    public function getLastResponse()
208    {
209        return $this->lastResponse;
210    }
211
212    /**
213     * Returns the URL detection handler.
214     *
215     * @return UrlDetectionInterface
216     */
217    public function getUrlDetectionHandler()
218    {
219        return $this->urlDetectionHandler;
220    }
221
222    /**
223     * Changes the URL detection handler.
224     *
225     * @param UrlDetectionInterface $urlDetectionHandler
226     */
227    private function setUrlDetectionHandler(UrlDetectionInterface $urlDetectionHandler)
228    {
229        $this->urlDetectionHandler = $urlDetectionHandler;
230    }
231
232    /**
233     * Returns the default AccessToken entity.
234     *
235     * @return AccessToken|null
236     */
237    public function getDefaultAccessToken()
238    {
239        return $this->defaultAccessToken;
240    }
241
242    /**
243     * Sets the default access token to use with requests.
244     *
245     * @param AccessToken|string $accessToken The access token to save.
246     *
247     * @throws \InvalidArgumentException
248     */
249    public function setDefaultAccessToken($accessToken)
250    {
251        if (is_string($accessToken)) {
252            $this->defaultAccessToken = new AccessToken($accessToken);
253
254            return;
255        }
256
257        if ($accessToken instanceof AccessToken) {
258            $this->defaultAccessToken = $accessToken;
259
260            return;
261        }
262
263        throw new \InvalidArgumentException('The default access token must be of type "string" or Facebook\AccessToken');
264    }
265
266    /**
267     * Returns the default Graph version.
268     *
269     * @return string
270     */
271    public function getDefaultGraphVersion()
272    {
273        return $this->defaultGraphVersion;
274    }
275
276    /**
277     * Returns the redirect login helper.
278     *
279     * @return FacebookRedirectLoginHelper
280     */
281    public function getRedirectLoginHelper()
282    {
283        return new FacebookRedirectLoginHelper(
284            $this->getOAuth2Client(),
285            $this->persistentDataHandler,
286            $this->urlDetectionHandler,
287            $this->pseudoRandomStringGenerator
288        );
289    }
290
291    /**
292     * Returns the JavaScript helper.
293     *
294     * @return FacebookJavaScriptHelper
295     */
296    public function getJavaScriptHelper()
297    {
298        return new FacebookJavaScriptHelper($this->app, $this->client, $this->defaultGraphVersion);
299    }
300
301    /**
302     * Returns the canvas helper.
303     *
304     * @return FacebookCanvasHelper
305     */
306    public function getCanvasHelper()
307    {
308        return new FacebookCanvasHelper($this->app, $this->client, $this->defaultGraphVersion);
309    }
310
311    /**
312     * Returns the page tab helper.
313     *
314     * @return FacebookPageTabHelper
315     */
316    public function getPageTabHelper()
317    {
318        return new FacebookPageTabHelper($this->app, $this->client, $this->defaultGraphVersion);
319    }
320
321    /**
322     * Sends a GET request to Graph and returns the result.
323     *
324     * @param string                  $endpoint
325     * @param AccessToken|string|null $accessToken
326     * @param string|null             $eTag
327     * @param string|null             $graphVersion
328     *
329     * @return FacebookResponse
330     *
331     * @throws FacebookSDKException
332     */
333    public function get($endpoint, $accessToken = null, $eTag = null, $graphVersion = null)
334    {
335        return $this->sendRequest(
336            'GET',
337            $endpoint,
338            $params = [],
339            $accessToken,
340            $eTag,
341            $graphVersion
342        );
343    }
344
345    /**
346     * Sends a POST request to Graph and returns the result.
347     *
348     * @param string                  $endpoint
349     * @param array                   $params
350     * @param AccessToken|string|null $accessToken
351     * @param string|null             $eTag
352     * @param string|null             $graphVersion
353     *
354     * @return FacebookResponse
355     *
356     * @throws FacebookSDKException
357     */
358    public function post($endpoint, array $params = [], $accessToken = null, $eTag = null, $graphVersion = null)
359    {
360        return $this->sendRequest(
361            'POST',
362            $endpoint,
363            $params,
364            $accessToken,
365            $eTag,
366            $graphVersion
367        );
368    }
369
370    /**
371     * Sends a DELETE request to Graph and returns the result.
372     *
373     * @param string                  $endpoint
374     * @param array                   $params
375     * @param AccessToken|string|null $accessToken
376     * @param string|null             $eTag
377     * @param string|null             $graphVersion
378     *
379     * @return FacebookResponse
380     *
381     * @throws FacebookSDKException
382     */
383    public function delete($endpoint, array $params = [], $accessToken = null, $eTag = null, $graphVersion = null)
384    {
385        return $this->sendRequest(
386            'DELETE',
387            $endpoint,
388            $params,
389            $accessToken,
390            $eTag,
391            $graphVersion
392        );
393    }
394
395    /**
396     * Sends a request to Graph for the next page of results.
397     *
398     * @param GraphEdge $graphEdge The GraphEdge to paginate over.
399     *
400     * @return GraphEdge|null
401     *
402     * @throws FacebookSDKException
403     */
404    public function next(GraphEdge $graphEdge)
405    {
406        return $this->getPaginationResults($graphEdge, 'next');
407    }
408
409    /**
410     * Sends a request to Graph for the previous page of results.
411     *
412     * @param GraphEdge $graphEdge The GraphEdge to paginate over.
413     *
414     * @return GraphEdge|null
415     *
416     * @throws FacebookSDKException
417     */
418    public function previous(GraphEdge $graphEdge)
419    {
420        return $this->getPaginationResults($graphEdge, 'previous');
421    }
422
423    /**
424     * Sends a request to Graph for the next page of results.
425     *
426     * @param GraphEdge $graphEdge The GraphEdge to paginate over.
427     * @param string    $direction The direction of the pagination: next|previous.
428     *
429     * @return GraphEdge|null
430     *
431     * @throws FacebookSDKException
432     */
433    public function getPaginationResults(GraphEdge $graphEdge, $direction)
434    {
435        $paginationRequest = $graphEdge->getPaginationRequest($direction);
436        if (!$paginationRequest) {
437            return null;
438        }
439
440        $this->lastResponse = $this->client->sendRequest($paginationRequest);
441
442        // Keep the same GraphNode subclass
443        $subClassName = $graphEdge->getSubClassName();
444        $graphEdge = $this->lastResponse->getGraphEdge($subClassName, false);
445
446        return count($graphEdge) > 0 ? $graphEdge : null;
447    }
448
449    /**
450     * Sends a request to Graph and returns the result.
451     *
452     * @param string                  $method
453     * @param string                  $endpoint
454     * @param array                   $params
455     * @param AccessToken|string|null $accessToken
456     * @param string|null             $eTag
457     * @param string|null             $graphVersion
458     *
459     * @return FacebookResponse
460     *
461     * @throws FacebookSDKException
462     */
463    public function sendRequest($method, $endpoint, array $params = [], $accessToken = null, $eTag = null, $graphVersion = null)
464    {
465        $accessToken = $accessToken ?: $this->defaultAccessToken;
466        $graphVersion = $graphVersion ?: $this->defaultGraphVersion;
467        $request = $this->request($method, $endpoint, $params, $accessToken, $eTag, $graphVersion);
468
469        return $this->lastResponse = $this->client->sendRequest($request);
470    }
471
472    /**
473     * Sends a batched request to Graph and returns the result.
474     *
475     * @param array                   $requests
476     * @param AccessToken|string|null $accessToken
477     * @param string|null             $graphVersion
478     *
479     * @return FacebookBatchResponse
480     *
481     * @throws FacebookSDKException
482     */
483    public function sendBatchRequest(array $requests, $accessToken = null, $graphVersion = null)
484    {
485        $accessToken = $accessToken ?: $this->defaultAccessToken;
486        $graphVersion = $graphVersion ?: $this->defaultGraphVersion;
487        $batchRequest = new FacebookBatchRequest(
488            $this->app,
489            $requests,
490            $accessToken,
491            $graphVersion
492        );
493
494        return $this->lastResponse = $this->client->sendBatchRequest($batchRequest);
495    }
496
497    /**
498     * Instantiates an empty FacebookBatchRequest entity.
499     *
500     * @param  AccessToken|string|null $accessToken  The top-level access token. Requests with no access token
501     *                                               will fallback to this.
502     * @param  string|null             $graphVersion The Graph API version to use.
503     * @return FacebookBatchRequest
504     */
505    public function newBatchRequest($accessToken = null, $graphVersion = null)
506    {
507        $accessToken = $accessToken ?: $this->defaultAccessToken;
508        $graphVersion = $graphVersion ?: $this->defaultGraphVersion;
509
510        return new FacebookBatchRequest(
511            $this->app,
512            [],
513            $accessToken,
514            $graphVersion
515        );
516    }
517
518    /**
519     * Instantiates a new FacebookRequest entity.
520     *
521     * @param string                  $method
522     * @param string                  $endpoint
523     * @param array                   $params
524     * @param AccessToken|string|null $accessToken
525     * @param string|null             $eTag
526     * @param string|null             $graphVersion
527     *
528     * @return FacebookRequest
529     *
530     * @throws FacebookSDKException
531     */
532    public function request($method, $endpoint, array $params = [], $accessToken = null, $eTag = null, $graphVersion = null)
533    {
534        $accessToken = $accessToken ?: $this->defaultAccessToken;
535        $graphVersion = $graphVersion ?: $this->defaultGraphVersion;
536
537        return new FacebookRequest(
538            $this->app,
539            $accessToken,
540            $method,
541            $endpoint,
542            $params,
543            $eTag,
544            $graphVersion
545        );
546    }
547
548    /**
549     * Factory to create FacebookFile's.
550     *
551     * @param string $pathToFile
552     *
553     * @return FacebookFile
554     *
555     * @throws FacebookSDKException
556     */
557    public function fileToUpload($pathToFile)
558    {
559        return new FacebookFile($pathToFile);
560    }
561
562    /**
563     * Factory to create FacebookVideo's.
564     *
565     * @param string $pathToFile
566     *
567     * @return FacebookVideo
568     *
569     * @throws FacebookSDKException
570     */
571    public function videoToUpload($pathToFile)
572    {
573        return new FacebookVideo($pathToFile);
574    }
575
576    /**
577     * Upload a video in chunks.
578     *
579     * @param int $target The id of the target node before the /videos edge.
580     * @param string $pathToFile The full path to the file.
581     * @param array $metadata The metadata associated with the video file.
582     * @param string|null $accessToken The access token.
583     * @param int $maxTransferTries The max times to retry a failed upload chunk.
584     * @param string|null $graphVersion The Graph API version to use.
585     *
586     * @return array
587     *
588     * @throws FacebookSDKException
589     */
590    public function uploadVideo($target, $pathToFile, $metadata = [], $accessToken = null, $maxTransferTries = 5, $graphVersion = null)
591    {
592        $accessToken = $accessToken ?: $this->defaultAccessToken;
593        $graphVersion = $graphVersion ?: $this->defaultGraphVersion;
594
595        $uploader = new FacebookResumableUploader($this->app, $this->client, $accessToken, $graphVersion);
596        $endpoint = '/'.$target.'/videos';
597        $file = $this->videoToUpload($pathToFile);
598        $chunk = $uploader->start($endpoint, $file);
599
600        do {
601            $chunk = $this->maxTriesTransfer($uploader, $endpoint, $chunk, $maxTransferTries);
602        } while (!$chunk->isLastChunk());
603
604        return [
605          'video_id' => $chunk->getVideoId(),
606          'success' => $uploader->finish($endpoint, $chunk->getUploadSessionId(), $metadata),
607        ];
608    }
609
610    /**
611     * Attempts to upload a chunk of a file in $retryCountdown tries.
612     *
613     * @param FacebookResumableUploader $uploader
614     * @param string $endpoint
615     * @param FacebookTransferChunk $chunk
616     * @param int $retryCountdown
617     *
618     * @return FacebookTransferChunk
619     *
620     * @throws FacebookSDKException
621     */
622    private function maxTriesTransfer(FacebookResumableUploader $uploader, $endpoint, FacebookTransferChunk $chunk, $retryCountdown)
623    {
624        $newChunk = $uploader->transfer($endpoint, $chunk, $retryCountdown < 1);
625
626        if ($newChunk !== $chunk) {
627            return $newChunk;
628        }
629
630        $retryCountdown--;
631
632        // If transfer() returned the same chunk entity, the transfer failed but is resumable.
633        return $this->maxTriesTransfer($uploader, $endpoint, $chunk, $retryCountdown);
634    }
635}
636