src/App/Service/Social/AbstractMetaService.php line 56

Open in your IDE?
  1. <?php
  2. namespace App\Service\Social;
  3. use App\Util\Json;
  4. use Facebook\Facebook;
  5. use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
  6. use Symfony\Component\HttpFoundation\Request;
  7. abstract class AbstractMetaService
  8. {
  9.     public const NAME null;
  10.     private const SCOPES = [
  11.         // common scopes
  12.         'email',
  13.         // facebook scopes
  14.         'pages_manage_metadata',
  15.         'pages_manage_posts',
  16.         'pages_read_engagement',
  17.         'pages_show_list',
  18.         // instagram scopes
  19.         'instagram_basic',
  20.         'instagram_content_publish',
  21.         // business scopes
  22.         'business_management',
  23.     ];
  24.     protected const CHECK_SCOPES = [
  25.         'email',
  26.     ];
  27.     protected const TASKS = [
  28.         'CREATE_CONTENT',
  29.     ];
  30.     protected MetaPersistentDataHandler $metaPersistentDataHandler;
  31.     /**
  32.      * @var Facebook
  33.      */
  34.     protected Facebook $client;
  35.     /**
  36.      * @param MetaPersistentDataHandler $metaPersistentDataHandler
  37.      * @param ParameterBagInterface $params
  38.      */
  39.     public function __construct(
  40.         MetaPersistentDataHandler $metaPersistentDataHandler,
  41.         ParameterBagInterface $params
  42.     )
  43.     {
  44.         $this->metaPersistentDataHandler $metaPersistentDataHandler;
  45.         // TODO: likely should update the param to reflect generic "meta" naming...
  46.         if ( ! $params->has('facebook')) {
  47.             throw new \LogicException();
  48.         }
  49.         $config $params->get('facebook');
  50.         $this->client = new Facebook(
  51.             [
  52.                 'app_id' => $config['key'],
  53.                 'app_secret' => $config['secret'],
  54.                 'default_graph_version' => 'v20.0',
  55.                 'persistent_data_handler' => $metaPersistentDataHandler,
  56.             ],
  57.         );
  58.     }
  59.     /**
  60.      * Get the URL to start the OAuth flow with.
  61.      *
  62.      * @param string $redirect
  63.      * @param array $state
  64.      * @return string
  65.      */
  66.     public function requestAuthenticationUrl(
  67.         string $redirect,
  68.         array $state = []
  69.     ): string
  70.     {
  71.         if ($state) {
  72.             $this->metaPersistentDataHandler->set(
  73.                 'state',
  74.                 base64_encode(
  75.                     Json::encode($state),
  76.                 ),
  77.             );
  78.         }
  79.         return $this->client->getRedirectLoginHelper()->getLoginUrl(
  80.             $redirect,
  81.             self::SCOPES,
  82.         );
  83.     }
  84.     /**
  85.      * @param Request $request
  86.      * @return array
  87.      */
  88.     public function parseAuthenticationState(Request $request): array
  89.     {
  90.         $requestState $request->query->get('state');
  91.         $sessionState $this->metaPersistentDataHandler->get('state');
  92.         if ($requestState !== $sessionState) {
  93.             throw new \RuntimeException();
  94.         }
  95.         return Json::decode(
  96.             base64_decode($requestState),
  97.             true,
  98.         );
  99.     }
  100.     /**
  101.      * Use this on the return from an OAuth authentication flow.
  102.      *
  103.      * @param string $redirect
  104.      * @return string
  105.      */
  106.     public function processAuthenticationCallback(string $redirect): string
  107.     {
  108.         // check for any errors
  109.         if ($code $this->client->getRedirectLoginHelper()->getErrorCode()) {
  110.             throw new \RuntimeException(
  111.                 sprintf(
  112.                     'Facebook authentication error: %s.',
  113.                     $code,
  114.                 ),
  115.             );
  116.         }
  117.         // get an access token
  118.         // this will be likely be a short-lived user token
  119.         $accessToken $this->client->getRedirectLoginHelper()->getAccessToken(
  120.             $redirect,
  121.         );
  122.         // if we didn't get an access token for some reason, we have a problem
  123.         if ( ! $accessToken) {
  124.             throw new \RuntimeException(
  125.                 'Facebook returned an empty access token.',
  126.             );
  127.         }
  128.         // if the token is not a long-lived one, then we need to attempt to get a long-lived token
  129.         if ( ! $accessToken->isLongLived()) {
  130.             $accessToken $this->client->getOAuth2Client()->getLongLivedAccessToken(
  131.                 $accessToken,
  132.             );
  133.         }
  134.         // TODO: should we eventually return the full token so we can maybe log more information about it (like expiration)???
  135.         return $accessToken->getValue();
  136.     }
  137.     /**
  138.      * Gets data about the user for the given access token.
  139.      *
  140.      * @param string $accessToken
  141.      * @return object
  142.      */
  143.     public function me(string $accessToken): object
  144.     {
  145.         $result $this->client->get(
  146.             '/me',
  147.             $accessToken,
  148.         );
  149.         if ($result->isError()) {
  150.             $result->throwException();
  151.         }
  152.         $data $result->getDecodedBody();
  153.         return (object) [
  154.             'id' => $data['id'],
  155.             'name' => $data['name'],
  156.         ];
  157.     }
  158.     /**
  159.      * Check if page publishing permissions are set for a user.
  160.      *
  161.      * @param string $accessToken
  162.      * @return bool
  163.      */
  164.     public function hasRequiredScopes(string $accessToken): bool
  165.     {
  166.         $result $this->client->get(
  167.             '/me/permissions',
  168.             $accessToken
  169.         );
  170.         if ($result->isError()) {
  171.             $result->throwException();
  172.         }
  173.         $scopes = [];
  174.         foreach (static::CHECK_SCOPES as $scope) {
  175.             $scopes[$scope] = false;
  176.             foreach ($result->getDecodedBody()['data'] as $perm) {
  177.                 if ($perm['permission'] === $scope) {
  178.                     $scopes[$scope] = ($perm['status'] === 'granted');
  179.                     break;
  180.                 }
  181.             }
  182.         }
  183.         $missing array_filter(
  184.             $scopes,
  185.             static function (bool $granted) {
  186.                 return !$granted;
  187.             },
  188.         );
  189.         return (count($missing) === 0);
  190.     }
  191.     /**
  192.      * @param string $accessToken
  193.      * @return array<object>
  194.      */
  195.     abstract public function getAccounts(string $accessToken): array;
  196.     /**
  197.      * Will get a normalized list of pages a user has access to.
  198.      *
  199.      * @param string $accessToken
  200.      * @return array<object>
  201.      */
  202.     protected function getPages(string $accessToken): array
  203.     {
  204.         $result $this->client->get(
  205.             '/me/accounts',
  206.             $accessToken,
  207.         );
  208.         if ($result->isError()) {
  209.             $result->throwException();
  210.         }
  211.         $pages = [];
  212.         foreach ($result->getDecodedBody()['data'] as $data) {
  213.             if ($this->hasRequiredTasks($data['tasks'] ?? null)) {
  214.                 $pages[$data['id']] = (object) [
  215.                     'id' => $data['id'],
  216.                     'name' => $data['name'],
  217.                     'token' => $data['access_token'],
  218.                 ];
  219.             }
  220.         }
  221.         return $pages;
  222.     }
  223.     /**
  224.      * Determines whether a set of params for page access are sufficient.
  225.      *
  226.      * @param array|null $tasks
  227.      * @return bool
  228.      */
  229.     private function hasRequiredTasks(?array $tasks): bool
  230.     {
  231.         // if the data set did not have tasks defined (so null input), we can assume tasks are not used...
  232.         if ($tasks === null) {
  233.             return true;
  234.         }
  235.         // otherwise, tasks are defined
  236.         // check for all the ones we need to have for our system to work
  237.         foreach (self::TASKS as $task) {
  238.             if ( ! in_array($task$taskstrue)) {
  239.                 return false;
  240.             }
  241.         }
  242.         // no rejection yet, which means we should have matched everything we need
  243.         return true;
  244.     }
  245. }