app/Plugin/ECCUBE4LineLoginIntegration42/Controller/LineLoginIntegrationController.php line 79

Open in your IDE?
  1. <?php
  2. namespace Plugin\ECCUBE4LineLoginIntegration42\Controller;
  3. use Plugin\ECCUBE4LineLoginIntegration42\Consts\ApiUrl;
  4. use Symfony\Component\HttpFoundation\Request;
  5. use Symfony\Component\HttpFoundation\Session\SessionInterface;
  6. use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
  7. use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
  8. use Eccube\Controller\AbstractController;
  9. use Eccube\Entity\Master\CustomerStatus;
  10. use Eccube\Repository\CustomerRepository;
  11. use Plugin\ECCUBE4LineLoginIntegration42\Entity\LineLoginIntegration;
  12. use Plugin\ECCUBE4LineLoginIntegration42\Controller\Admin\LineLoginIntegrationAdminController;
  13. use Plugin\ECCUBE4LineLoginIntegration42\Repository\LineLoginIntegrationSettingRepository;
  14. use Plugin\ECCUBE4LineLoginIntegration42\Repository\LineLoginIntegrationRepository;
  15. use Symfony\Component\Security\Http\Event\InteractiveLoginEvent;
  16. use Symfony\Component\Security\Http\SecurityEvents;
  17. use Symfony\Component\Routing\Annotation\Route;
  18. class LineLoginIntegrationController extends AbstractController
  19. {
  20.     private $lineChannelId;
  21.     private $lineChannelSecret;
  22.     private $lineIntegrationSettingRepository;
  23.     private $lineIntegrationRepository;
  24.     private $customerRepository;
  25.     private $tokenStorage;
  26.     protected $apiUrl;
  27.     const PLUGIN_LINE_LOGIN_INTEGRATION_SSO_USERID 'plugin.line_login_integration.sso.userid';
  28.     const PLUGIN_LINE_LOGIN_INTEGRATION_SSO_USERNAME 'plugin.line_login_integration.sso.userName';
  29.     const PLUGIN_LINE_LOGIN_INTEGRATION_SSO_USERAVATOR 'plugin.line_login_integration.sso.userAvator';
  30.     const PLUGIN_LINE_LOGIN_INTEGRATION_SSO_STATE 'plugin.line_login_integration.sso.state';
  31.     public function __construct(
  32.         LineLoginIntegrationSettingRepository $lineIntegrationSettingRepository,
  33.         LineLoginIntegrationRepository $lineIntegrationRepository,
  34.         CustomerRepository $customerRepository,
  35.         TokenStorageInterface $tokenStorage,
  36.         ApiUrl $apiUrl
  37.     )
  38.     {
  39.         $this->lineIntegrationSettingRepository $lineIntegrationSettingRepository;
  40.         $this->lineIntegrationRepository $lineIntegrationRepository;
  41.         $lineIntegrationSetting $this->getLineLoginIntegrationSetting();
  42.         $this->lineChannelId $lineIntegrationSetting->getLineChannelId();
  43.         $this->lineChannelSecret $lineIntegrationSetting->getLineChannelSecret();
  44.         $this->customerRepository $customerRepository;
  45.         $this->tokenStorage $tokenStorage;
  46.         $this->apiUrl $apiUrl;
  47.     }
  48.     /**
  49.      * ログイン画面の表示
  50.      *
  51.      * @Route("/plugin_line_login", name="plugin_line_login")
  52.      * @param Request $request
  53.      * @return \Symfony\Component\HttpFoundation\RedirectResponse
  54.      */
  55.     public function login(Request $request)
  56.     {
  57.         $url $this->generateUrl('plugin_line_login_callback',array(),0);
  58.         $state uniqid();
  59.         $session $request->getSession();
  60.         $session->set(self::PLUGIN_LINE_LOGIN_INTEGRATION_SSO_STATE$state);
  61.         $sendto $request->query->get('sendto');
  62.         if ($sendto !== null) {
  63.             $session->set('sendto'$sendto);
  64.         }
  65.         $param $request->query->get('param');
  66.         if ($param !== null) {
  67.             $session->set('param'$param);
  68.         }
  69.         $previousUrl parse_url(
  70.             $request->headers->get('referer'),PHP_URL_PATH);
  71.         $session->set('$previousUrl' ,$previousUrl);
  72.         // TODO bot_prompt
  73.         // bot_prompt=normal or aggressive
  74.         // https://developers.line.me/ja/docs/line-login/web/link-a-bot/
  75.         $lineAuthUrl $this->apiUrl->getAccessUrl() . '/oauth2/v2.1/authorize?response_type=code&client_id=' $this->lineChannelId '&redirect_uri=' rawurlencode($url) . '&state=' $state '&scope=profile&bot_prompt=aggressive';
  76.         return $this->redirect($lineAuthUrl);
  77.     }
  78.     /**
  79.      * ログインのコールバック処理
  80.      *
  81.      * @Route("/plugin_line_login_callback", name="plugin_line_login_callback")
  82.      * @param Request $request
  83.      *
  84.      * @return \Symfony\Component\HttpFoundation\RedirectResponse|\Symfony\Component\HttpFoundation\Response
  85.      */
  86.     public function loginCallback(Request $request)
  87.     {
  88.         $code $request->get('code');
  89.         $state $request->get('state');
  90.         $session $request->getSession();
  91.         $originalState $session->get(self::PLUGIN_LINE_LOGIN_INTEGRATION_SSO_STATE);
  92.         $session->remove(self::PLUGIN_LINE_LOGIN_INTEGRATION_SSO_STATE);
  93.         $shopLoginUrl '/shopping/login';
  94.         $shopLoginUrlLength strlen($shopLoginUrl);
  95.         $shopNonmemberUrl '/shopping/nonmember';
  96.         $shopNonmemberUrlLength strlen($shopNonmemberUrl);
  97.         // APIアクセスの為のパラメータ検証
  98.         $results $this->validateParameter($code$state$originalState);
  99.         if($results !== null) {
  100.             return $results;
  101.         }
  102.         // アクセストークン発行
  103.         $tokenJson $this->publishAccessToken($code);
  104.         if (isset($tokenJson['error'])) {
  105.             //errorレスポンスはないため、この処理は起こり得ない(コード記述ミス?)
  106.             log_error('LINE API エラー(4)' $tokenJson['error'] . ' ' $tokenJson['error_description']);
  107.             return $this->render('error.twig', [
  108.                 'error_title'   => 'エラーが発生しました。',
  109.                 'error_message' => 'スマートフォンの標準ブラウザもしくはLINEアプリ内ブラウザから、LINEのID連携やLINEログインをお試しください。' PHP_EOL PHP_EOL .
  110.                 '※標準ブラウザとは?' PHP_EOL .
  111.                 'スマートフォンに標準搭載されているブラウザのことで、iOSのSafariやAndroidのChromeを指します。' PHP_EOL PHP_EOL .
  112.                 'または、Safariのプライベートブラウズモードや、Chromeのシークレットモードをお使いの場合は、画面が進まない場合があります。標準モードでご利用ください。',
  113.             ]);
  114.         }
  115.         if (!array_key_exists("access_token"$tokenJson)) {
  116.             log_error('LINE API エラー(5)');
  117.             return $this->render('error.twig', [
  118.                 'error_title'   => 'エラーが発生しました。',
  119.                 'error_message' => 'スマートフォンの標準ブラウザもしくはLINEアプリ内ブラウザから、LINEのID連携やLINEログインをお試しください。' PHP_EOL PHP_EOL .
  120.                 '※標準ブラウザとは?' PHP_EOL .
  121.                 'スマートフォンに標準搭載されているブラウザのことで、iOSのSafariやAndroidのChromeを指します。' PHP_EOL PHP_EOL .
  122.                 'または、Safariのプライベートブラウズモードや、Chromeのシークレットモードをお使いの場合は、画面が進まない場合があります。標準モードでご利用ください。',
  123.             ]);
  124.         }
  125.         // LineId取得
  126.         $profile $this->getProfile($tokenJson['access_token']);
  127.         if (!array_key_exists("userId"$profile)) {
  128.             log_error('LINE API エラー(6): LINE IDの取得失敗');
  129.             return $this->render('error.twig', [
  130.                 'error_title'   => 'エラーが発生しました。',
  131.                 'error_message' => 'スマートフォンの標準ブラウザもしくはLINEアプリ内ブラウザから、LINEのID連携やLINEログインをお試しください。' PHP_EOL PHP_EOL .
  132.                 '※標準ブラウザとは?' PHP_EOL .
  133.                 'スマートフォンに標準搭載されているブラウザのことで、iOSのSafariやAndroidのChromeを指します。' PHP_EOL PHP_EOL .
  134.                 'または、Safariのプライベートブラウズモードや、Chromeのシークレットモードをお使いの場合は、画面が進まない場合があります。標準モードでご利用ください。',
  135.             ]);
  136.         }
  137.         if (empty($profile['userId'])) {
  138.             //LINE API エラー(6)とほぼ同じ(コード記述ミス?)
  139.             log_error('LINE API エラー(7): LINE IDが不正');
  140.             return $this->render('error.twig', [
  141.                 'error_title'   => 'エラーが発生しました。',
  142.                 'error_message' => 'スマートフォンの標準ブラウザもしくはLINEアプリ内ブラウザから、LINEのID連携やLINEログインをお試しください。' PHP_EOL PHP_EOL .
  143.                 '※標準ブラウザとは?' PHP_EOL .
  144.                 'スマートフォンに標準搭載されているブラウザのことで、iOSのSafariやAndroidのChromeを指します。' PHP_EOL PHP_EOL .
  145.                 'または、Safariのプライベートブラウズモードや、Chromeのシークレットモードをお使いの場合は、画面が進まない場合があります。標準モードでご利用ください。',
  146.             ]);
  147.         }
  148.         $lineUserId $profile['userId'];
  149.         $lineUserName $profile['displayName'];
  150.         $lineUserAvator $profile['pictureUrl'];
  151.         $session->set(self::PLUGIN_LINE_LOGIN_INTEGRATION_SSO_USERID$lineUserId);
  152.         $session->set(self::PLUGIN_LINE_LOGIN_INTEGRATION_SSO_USERNAME$lineUserName);
  153.         $session->set(self::PLUGIN_LINE_LOGIN_INTEGRATION_SSO_USERAVATOR$lineUserAvator);
  154.         // LINE連携レコードを取得
  155.         $lineIntegration $this->lineIntegrationRepository->
  156.             findOneBy(['line_user_id' => $lineUserId]);
  157.         // LINE連携レコードの顧客IDを取得
  158.         isset($lineIntegration['customer_id']) ?
  159.             $customerId $lineIntegration['customer_id'] :
  160.             $customerId null;
  161.         // 顧客レコードから顧客取得
  162.         $this->customerRepository->findOneBy(['id' => $customerId]) ?
  163.             $customer =
  164.                 $this->customerRepository->findOneBy(['id' => $customerId]) :
  165.             $customer null;
  166.         // LINE連携レコードがあり、LINE連携レコードに紐づく顧客レコードが見つからない場合、LINE連携レコード削除
  167.         if (!is_null($lineIntegration)) {
  168.             // DB上にLINE IDの登録はあるが、Customerオブジェクトが未発見の場合、LINE IDの削除
  169.             if (is_null($customer)) {
  170.                 log_info('削除されたユーザ(customer_id:' $customerId ')とのLINE IDのレコードを削除します');
  171.                 $this->lineIntegrationRepository->deleteLineAssociation($lineIntegration);
  172.                 // DB上にLINE IDの登録はあるが、Customerが退会済み扱いのときも、LINE IDを削除する
  173.             } else if ($customer->getStatus()['id'] == CustomerStatus::WITHDRAWING) {
  174.                 log_info('退会しているユーザ(customer_id:' $customerId ')とのLINE IDのレコードを削除します');
  175.                 $this->lineIntegrationRepository->deleteLineAssociation($lineIntegration);
  176.                 $customer null// 会員を存在しなかった扱いにすることで、新規登録フローに流す
  177.             }
  178.             // 削除後はそのままスルーし、普通のフローに
  179.         }
  180.         // EC-CUBEにログインしているとき(会員情報編集からの遷移)、LINE連携レコードと紐付け
  181.         if ($this->isGranted('ROLE_USER')) {
  182.             log_info('LINEコールバック:ログイン済み。');
  183.             //  LINE連携レコードに紐づく、顧客が存在しない場合
  184.             if (is_null($customer)) {
  185.                 $this->associationCustomerAndLineid($lineUserId$lineUserName$lineUserAvator);
  186.             } else {
  187.                 // 既にDBにLINE IDと紐づけられている顧客ID
  188.                 $registeredCustomerId $customer->getId();
  189.                 // 新たにLINE IDと紐付けようと申請する顧客ID
  190.                 $nowLoggedInCustomerId $this->getUser()->getId();
  191.                 if($nowLoggedInCustomerId != $registeredCustomerId) {
  192.                     log_info('すでに連携済みのLINE IDを別のアカウントの連携に使おうとしました。 $lineUserId:'.$lineUserId);
  193.                     return $this->render('error.twig', [
  194.                         'error_title'   => '重複したLINE IDです',
  195.                         'error_message' => "既に別のアカウントで、同じLINE IDが登録されています。",
  196.                     ]);
  197.                 }
  198.             }
  199.             return $this->redirectToRoute('homepage');
  200.         }
  201.         // EC-CUBEに未ログインであるとき
  202.         else {
  203.             log_info('LINEコールバック: 未ログイン');
  204.             // LINE連携レコードがなかったら、会員登録へ
  205.             if (is_null($lineIntegration)) {
  206.                 log_info('LINE連携レコードなし');
  207.                 return $this->redirectToRoute('entry');
  208.             }
  209.             // LINE連携レコードがあっても、顧客レコードがない場合は会員登録へ
  210.             if (is_null($customer)) {
  211.                 log_info('顧客レコードが取得できなかった為、会員登録へ');
  212.                 return $this->redirectToRoute('entry');
  213.             }
  214.             // 仮会員の場合ログインへ
  215.             if ($customer->getStatus()->getId() == 1) {
  216.                 log_info('仮会員のため、ログインへ customer_id:'.$customerId);
  217.                 if (substr($session->get('$previousUrl'), -$shopLoginUrlLength) === $shopLoginUrl) {
  218.                     return $this->redirectToRoute('shopping_login');
  219.                 }
  220.                 return $this->redirectToRoute('mypage_login');
  221.             }
  222.             // 本会員かつ、LINE連携レコード・顧客レコードが存在するのでログイン処理
  223.             if ($customer->getStatus()->getId() == 2) {
  224.                 $token = new UsernamePasswordToken($customer'customer', array('ROLE_USER'));
  225.                 $this->tokenStorage->setToken($token);
  226.                 log_info('ログイン済に変更。dtb_customer.id:'.$this->getUser()->getId());
  227.                 // カートのマージなどの処理
  228.                 $loginEvent = new InteractiveLoginEvent($request$token);
  229.                 $this->eventDispatcher->dispatch(
  230.                     $loginEvent,
  231.                     SecurityEvents::INTERACTIVE_LOGIN
  232.                 );
  233.                 // 遷移元がカート経由のログインだった場合、購入画面。そうでない場合マイページに遷移
  234.                 if (substr($session->get('$previousUrl'), -$shopLoginUrlLength) === $shopLoginUrl) {
  235.                     return $this->redirectToRoute('shopping');
  236.                 } else if(substr($session->get('$previousUrl'), -$shopNonmemberUrlLength) === $shopNonmemberUrl){
  237.                     return $this->redirectToRoute('shopping');
  238.                 }
  239.                 if ($redirect $this->getSendtoRedirectResponse($session)) {
  240.                     return $redirect;
  241.                 }
  242.                 return $this->redirectToRoute('homepage');
  243.             }
  244.             // 例外としてログインページに戻す
  245.             return $this->redirectToRoute('login');
  246.         }
  247.     }
  248.     /**
  249.      * 設定レコードを取得します
  250.      * @return string
  251.      */
  252.     private function getLineLoginIntegrationSetting()
  253.     {
  254.         $lineIntegrationSetting $this->lineIntegrationSettingRepository
  255.             ->find(LineLoginIntegrationAdminController::LINE_LOGIN_INTEGRATION_SETTING_TABLE_ID);
  256.         return $lineIntegrationSetting;
  257.     }
  258.     private function getSendtoRedirectResponse(SessionInterface $session)
  259.     {
  260.         $sendto $session->get('sendto');
  261.         $param $session->get('param');
  262.         if ($sendto === 'detail' && $param) {
  263.             $this->clearSendtoSession($session);
  264.             return $this->redirectToRoute('product_detail', ['id' => $param]);
  265.         }
  266.         if ($sendto === 'list' && $param) {
  267.             $this->clearSendtoSession($session);
  268.             return $this->redirectToRoute('product_list', ['name' => $param]);
  269.         }
  270.         if ($sendto === 'homepage') {
  271.             $this->clearSendtoSession($session);
  272.             return $this->redirectToRoute('homepage');
  273.         }
  274.         return null;
  275.     }
  276.     private function clearSendtoSession(SessionInterface $session)
  277.     {
  278.         $session->remove('sendto');
  279.         $session->remove('param');
  280.     }
  281.     /**
  282.      * LINE APIからアクセストークンを取得する為の、パラメータを検証します
  283.      * @param $code
  284.      * @param $state
  285.      * @param $originalState
  286.      *
  287.      * @return \Symfony\Component\HttpFoundation\Response|null
  288.      */
  289.     private function validateParameter($code$state$originalState){
  290.         if (empty($code)) {
  291.             log_error('LINE API エラー(0): 認可コードが空');
  292.             $config $this->lineIntegrationSettingRepository->find(1);
  293.             if (is_null($config) || is_null($config->getLineAddCancelRedirectUrl())) {
  294.                 log_error("[LineIntegration] 設定を取得できませんでした");
  295.                 return $this->render('error.twig', [
  296.                     'error_title'   => 'エラーが発生しました。',
  297.                     'error_message' => 'スマートフォンの標準ブラウザもしくはLINEアプリ内ブラウザから、LINEのID連携やLINEログインをお試しください。' PHP_EOL PHP_EOL .
  298.                     '※標準ブラウザとは?' PHP_EOL .
  299.                     'スマートフォンに標準搭載されているブラウザのことで、iOSのSafariやAndroidのChromeを指します。' PHP_EOL PHP_EOL .
  300.                     'または、Safariのプライベートブラウズモードや、Chromeのシークレットモードをお使いの場合は、画面が進まない場合があります。標準モードでご利用ください。',
  301.                         ]);
  302.             } else {
  303.                 return $this->redirect($config->getLineAddCancelRedirectUrl());
  304.             }
  305.         }
  306.         if (empty($state)) {
  307.             log_error('LINE API エラー(1): CSRF防止用の固有な英数字の文字列が空');
  308.             return $this->render('error.twig', [
  309.                 'error_title'   => 'エラーが発生しました。',
  310.                 'error_message' => 'スマートフォンの標準ブラウザもしくはLINEアプリ内ブラウザから、LINEのID連携やLINEログインをお試しください。' PHP_EOL PHP_EOL .
  311.                 '※標準ブラウザとは?' PHP_EOL .
  312.                 'スマートフォンに標準搭載されているブラウザのことで、iOSのSafariやAndroidのChromeを指します。' PHP_EOL PHP_EOL .
  313.                 'または、Safariのプライベートブラウズモードや、Chromeのシークレットモードをお使いの場合は、画面が進まない場合があります。標準モードでご利用ください。',
  314.             ]);
  315.         }
  316.         if (empty($originalState)) {
  317.             log_error('LINE API エラー(2): セッションタイムアウト');
  318.             return $this->render('error.twig', [
  319.                 'error_title'   => 'エラーが発生しました。',
  320.                 'error_message' => 'スマートフォンの標準ブラウザもしくはLINEアプリ内ブラウザから、LINEのID連携やLINEログインをお試しください。' PHP_EOL PHP_EOL .
  321.                 '※標準ブラウザとは?' PHP_EOL .
  322.                 'スマートフォンに標準搭載されているブラウザのことで、iOSのSafariやAndroidのChromeを指します。' PHP_EOL PHP_EOL .
  323.                 'または、Safariのプライベートブラウズモードや、Chromeのシークレットモードをお使いの場合は、画面が進まない場合があります。標準モードでご利用ください。',
  324.             ]);
  325.         }
  326.         if ($state != $originalState) {
  327.             log_error('LINE API エラー(3): CSRF防止用の固有な英数字の文字列がセッションのものと異なる');
  328.             return $this->render('error.twig', [
  329.                 'error_title'   => 'エラーが発生しました。',
  330.                 'error_message' => 'スマートフォンの標準ブラウザもしくはLINEアプリ内ブラウザから、LINEのID連携やLINEログインをお試しください。' PHP_EOL PHP_EOL .
  331.                 '※標準ブラウザとは?' PHP_EOL .
  332.                 'スマートフォンに標準搭載されているブラウザのことで、iOSのSafariやAndroidのChromeを指します。' PHP_EOL PHP_EOL .
  333.                 'または、Safariのプライベートブラウズモードや、Chromeのシークレットモードをお使いの場合は、画面が進まない場合があります。標準モードでご利用ください。',
  334.             ]);
  335.         }
  336.         return null;
  337.     }
  338.     /**
  339.      * LINE APIでアクセストークンを発行します
  340.      * @param $code
  341.      *
  342.      * @return mixed
  343.      */
  344.     private function publishAccessToken($code){
  345.         $url $this->generateUrl('plugin_line_login_callback',array(),0);
  346.         $accessTokenUrl $this->apiUrl->getApiUrl() . "/oauth2/v2.1/token";
  347.         $accessTokenData = array(
  348.             "grant_type" => "authorization_code",
  349.             "code" => $code,
  350.             "redirect_uri" => $url,
  351.             "client_id" => $this->lineChannelId,
  352.             "client_secret" => $this->lineChannelSecret,
  353.         );
  354.         $accessTokenData http_build_query($accessTokenData"""&");
  355.         $header = array(
  356.             "Content-Type: application/x-www-form-urlencoded",
  357.             "Content-Length: " strlen($accessTokenData)
  358.         );
  359.         $context = array(
  360.             "http" => array(
  361.                 "method" => "POST",
  362.                 "header" => implode("\r\n"$header),
  363.                 "content" => $accessTokenData
  364.             )
  365.         );
  366.         $response file_get_contents($accessTokenUrlfalsestream_context_create($context));
  367.         $tokenJson json_decode($responsetrue);
  368.         return $tokenJson;
  369.     }
  370.     /**
  371.      * LINE APIからLINE IDを取得します
  372.      * @param $accessToken
  373.      *
  374.      * @return mixed
  375.      */
  376.     private function getProfile($accessToken){
  377.         $lineProfileUrl $this->apiUrl->getApiUrl() . "/v2/profile";
  378.         $context = array(
  379.             "http" => array(
  380.                 "method" => "GET",
  381.                 "header" => "Authorization: Bearer " $accessToken
  382.             )
  383.         );
  384.         $response file_get_contents($lineProfileUrlfalsestream_context_create($context));
  385.         $profileJson json_decode($responsetrue);
  386.         return $profileJson;
  387.     }
  388.     /**
  389.      * 顧客とLINE連携レコードの紐付けを行います
  390.      * @param $customer
  391.      * @param $lineUserId
  392.      * @param $lineUserName
  393.      * @param $lineUserAvator
  394.      *
  395.      * @return \Symfony\Component\HttpFoundation\Response
  396.      */
  397.     private function associationCustomerAndLineid($lineUserId$lineUserName$lineUserAvator){
  398.         log_info('plg_line_login_integrationレコードなし');
  399.         $lineIntegration = new LineLoginIntegration();
  400.         $lineIntegration->setLineUserId($lineUserId);
  401.         // 追加
  402.         $lineIntegration->setLineUserName($lineUserName);
  403.         $lineIntegration->setLineUserAvatar($lineUserAvator);
  404.         $lineIntegration->setCustomerId($this->getUser()->getId());
  405.         $lineIntegration->setCustomer($this->getUser());
  406.         $this->entityManager->persist($lineIntegration);
  407.         $this->entityManager->flush();
  408.         log_info('LINE IDとユーザーの関連付け終了');
  409.     }
  410. }