<?php
namespace Plugin\ECCUBE4LineLoginIntegration42\Controller;
use Plugin\ECCUBE4LineLoginIntegration42\Consts\ApiUrl;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Eccube\Controller\AbstractController;
use Eccube\Entity\Master\CustomerStatus;
use Eccube\Repository\CustomerRepository;
use Plugin\ECCUBE4LineLoginIntegration42\Entity\LineLoginIntegration;
use Plugin\ECCUBE4LineLoginIntegration42\Controller\Admin\LineLoginIntegrationAdminController;
use Plugin\ECCUBE4LineLoginIntegration42\Repository\LineLoginIntegrationSettingRepository;
use Plugin\ECCUBE4LineLoginIntegration42\Repository\LineLoginIntegrationRepository;
use Symfony\Component\Security\Http\Event\InteractiveLoginEvent;
use Symfony\Component\Security\Http\SecurityEvents;
use Symfony\Component\Routing\Annotation\Route;
class LineLoginIntegrationController extends AbstractController
{
private $lineChannelId;
private $lineChannelSecret;
private $lineIntegrationSettingRepository;
private $lineIntegrationRepository;
private $customerRepository;
private $tokenStorage;
protected $apiUrl;
const PLUGIN_LINE_LOGIN_INTEGRATION_SSO_USERID = 'plugin.line_login_integration.sso.userid';
const PLUGIN_LINE_LOGIN_INTEGRATION_SSO_USERNAME = 'plugin.line_login_integration.sso.userName';
const PLUGIN_LINE_LOGIN_INTEGRATION_SSO_USERAVATOR = 'plugin.line_login_integration.sso.userAvator';
const PLUGIN_LINE_LOGIN_INTEGRATION_SSO_STATE = 'plugin.line_login_integration.sso.state';
public function __construct(
LineLoginIntegrationSettingRepository $lineIntegrationSettingRepository,
LineLoginIntegrationRepository $lineIntegrationRepository,
CustomerRepository $customerRepository,
TokenStorageInterface $tokenStorage,
ApiUrl $apiUrl
)
{
$this->lineIntegrationSettingRepository = $lineIntegrationSettingRepository;
$this->lineIntegrationRepository = $lineIntegrationRepository;
$lineIntegrationSetting = $this->getLineLoginIntegrationSetting();
$this->lineChannelId = $lineIntegrationSetting->getLineChannelId();
$this->lineChannelSecret = $lineIntegrationSetting->getLineChannelSecret();
$this->customerRepository = $customerRepository;
$this->tokenStorage = $tokenStorage;
$this->apiUrl = $apiUrl;
}
/**
* ログイン画面の表示
*
* @Route("/plugin_line_login", name="plugin_line_login")
* @param Request $request
* @return \Symfony\Component\HttpFoundation\RedirectResponse
*/
public function login(Request $request)
{
$url = $this->generateUrl('plugin_line_login_callback',array(),0);
$state = uniqid();
$session = $request->getSession();
$session->set(self::PLUGIN_LINE_LOGIN_INTEGRATION_SSO_STATE, $state);
$sendto = $request->query->get('sendto');
if ($sendto !== null) {
$session->set('sendto', $sendto);
}
$param = $request->query->get('param');
if ($param !== null) {
$session->set('param', $param);
}
$previousUrl = parse_url(
$request->headers->get('referer'),PHP_URL_PATH);
$session->set('$previousUrl' ,$previousUrl);
// TODO bot_prompt
// bot_prompt=normal or aggressive
// https://developers.line.me/ja/docs/line-login/web/link-a-bot/
$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';
return $this->redirect($lineAuthUrl);
}
/**
* ログインのコールバック処理
*
* @Route("/plugin_line_login_callback", name="plugin_line_login_callback")
* @param Request $request
*
* @return \Symfony\Component\HttpFoundation\RedirectResponse|\Symfony\Component\HttpFoundation\Response
*/
public function loginCallback(Request $request)
{
$code = $request->get('code');
$state = $request->get('state');
$session = $request->getSession();
$originalState = $session->get(self::PLUGIN_LINE_LOGIN_INTEGRATION_SSO_STATE);
$session->remove(self::PLUGIN_LINE_LOGIN_INTEGRATION_SSO_STATE);
$shopLoginUrl = '/shopping/login';
$shopLoginUrlLength = strlen($shopLoginUrl);
$shopNonmemberUrl = '/shopping/nonmember';
$shopNonmemberUrlLength = strlen($shopNonmemberUrl);
// APIアクセスの為のパラメータ検証
$results = $this->validateParameter($code, $state, $originalState);
if($results !== null) {
return $results;
}
// アクセストークン発行
$tokenJson = $this->publishAccessToken($code);
if (isset($tokenJson['error'])) {
//errorレスポンスはないため、この処理は起こり得ない(コード記述ミス?)
log_error('LINE API エラー(4)' . $tokenJson['error'] . ' ' . $tokenJson['error_description']);
return $this->render('error.twig', [
'error_title' => 'エラーが発生しました。',
'error_message' => 'スマートフォンの標準ブラウザもしくはLINEアプリ内ブラウザから、LINEのID連携やLINEログインをお試しください。' . PHP_EOL . PHP_EOL .
'※標準ブラウザとは?' . PHP_EOL .
'スマートフォンに標準搭載されているブラウザのことで、iOSのSafariやAndroidのChromeを指します。' . PHP_EOL . PHP_EOL .
'または、Safariのプライベートブラウズモードや、Chromeのシークレットモードをお使いの場合は、画面が進まない場合があります。標準モードでご利用ください。',
]);
}
if (!array_key_exists("access_token", $tokenJson)) {
log_error('LINE API エラー(5)');
return $this->render('error.twig', [
'error_title' => 'エラーが発生しました。',
'error_message' => 'スマートフォンの標準ブラウザもしくはLINEアプリ内ブラウザから、LINEのID連携やLINEログインをお試しください。' . PHP_EOL . PHP_EOL .
'※標準ブラウザとは?' . PHP_EOL .
'スマートフォンに標準搭載されているブラウザのことで、iOSのSafariやAndroidのChromeを指します。' . PHP_EOL . PHP_EOL .
'または、Safariのプライベートブラウズモードや、Chromeのシークレットモードをお使いの場合は、画面が進まない場合があります。標準モードでご利用ください。',
]);
}
// LineId取得
$profile = $this->getProfile($tokenJson['access_token']);
if (!array_key_exists("userId", $profile)) {
log_error('LINE API エラー(6): LINE IDの取得失敗');
return $this->render('error.twig', [
'error_title' => 'エラーが発生しました。',
'error_message' => 'スマートフォンの標準ブラウザもしくはLINEアプリ内ブラウザから、LINEのID連携やLINEログインをお試しください。' . PHP_EOL . PHP_EOL .
'※標準ブラウザとは?' . PHP_EOL .
'スマートフォンに標準搭載されているブラウザのことで、iOSのSafariやAndroidのChromeを指します。' . PHP_EOL . PHP_EOL .
'または、Safariのプライベートブラウズモードや、Chromeのシークレットモードをお使いの場合は、画面が進まない場合があります。標準モードでご利用ください。',
]);
}
if (empty($profile['userId'])) {
//LINE API エラー(6)とほぼ同じ(コード記述ミス?)
log_error('LINE API エラー(7): LINE IDが不正');
return $this->render('error.twig', [
'error_title' => 'エラーが発生しました。',
'error_message' => 'スマートフォンの標準ブラウザもしくはLINEアプリ内ブラウザから、LINEのID連携やLINEログインをお試しください。' . PHP_EOL . PHP_EOL .
'※標準ブラウザとは?' . PHP_EOL .
'スマートフォンに標準搭載されているブラウザのことで、iOSのSafariやAndroidのChromeを指します。' . PHP_EOL . PHP_EOL .
'または、Safariのプライベートブラウズモードや、Chromeのシークレットモードをお使いの場合は、画面が進まない場合があります。標準モードでご利用ください。',
]);
}
$lineUserId = $profile['userId'];
$lineUserName = $profile['displayName'];
$lineUserAvator = $profile['pictureUrl'];
$session->set(self::PLUGIN_LINE_LOGIN_INTEGRATION_SSO_USERID, $lineUserId);
$session->set(self::PLUGIN_LINE_LOGIN_INTEGRATION_SSO_USERNAME, $lineUserName);
$session->set(self::PLUGIN_LINE_LOGIN_INTEGRATION_SSO_USERAVATOR, $lineUserAvator);
// LINE連携レコードを取得
$lineIntegration = $this->lineIntegrationRepository->
findOneBy(['line_user_id' => $lineUserId]);
// LINE連携レコードの顧客IDを取得
isset($lineIntegration['customer_id']) ?
$customerId = $lineIntegration['customer_id'] :
$customerId = null;
// 顧客レコードから顧客取得
$this->customerRepository->findOneBy(['id' => $customerId]) ?
$customer =
$this->customerRepository->findOneBy(['id' => $customerId]) :
$customer = null;
// LINE連携レコードがあり、LINE連携レコードに紐づく顧客レコードが見つからない場合、LINE連携レコード削除
if (!is_null($lineIntegration)) {
// DB上にLINE IDの登録はあるが、Customerオブジェクトが未発見の場合、LINE IDの削除
if (is_null($customer)) {
log_info('削除されたユーザ(customer_id:' . $customerId . ')とのLINE IDのレコードを削除します');
$this->lineIntegrationRepository->deleteLineAssociation($lineIntegration);
// DB上にLINE IDの登録はあるが、Customerが退会済み扱いのときも、LINE IDを削除する
} else if ($customer->getStatus()['id'] == CustomerStatus::WITHDRAWING) {
log_info('退会しているユーザ(customer_id:' . $customerId . ')とのLINE IDのレコードを削除します');
$this->lineIntegrationRepository->deleteLineAssociation($lineIntegration);
$customer = null; // 会員を存在しなかった扱いにすることで、新規登録フローに流す
}
// 削除後はそのままスルーし、普通のフローに
}
// EC-CUBEにログインしているとき(会員情報編集からの遷移)、LINE連携レコードと紐付け
if ($this->isGranted('ROLE_USER')) {
log_info('LINEコールバック:ログイン済み。');
// LINE連携レコードに紐づく、顧客が存在しない場合
if (is_null($customer)) {
$this->associationCustomerAndLineid($lineUserId, $lineUserName, $lineUserAvator);
} else {
// 既にDBにLINE IDと紐づけられている顧客ID
$registeredCustomerId = $customer->getId();
// 新たにLINE IDと紐付けようと申請する顧客ID
$nowLoggedInCustomerId = $this->getUser()->getId();
if($nowLoggedInCustomerId != $registeredCustomerId) {
log_info('すでに連携済みのLINE IDを別のアカウントの連携に使おうとしました。 $lineUserId:'.$lineUserId);
return $this->render('error.twig', [
'error_title' => '重複したLINE IDです',
'error_message' => "既に別のアカウントで、同じLINE IDが登録されています。",
]);
}
}
return $this->redirectToRoute('homepage');
}
// EC-CUBEに未ログインであるとき
else {
log_info('LINEコールバック: 未ログイン');
// LINE連携レコードがなかったら、会員登録へ
if (is_null($lineIntegration)) {
log_info('LINE連携レコードなし');
return $this->redirectToRoute('entry');
}
// LINE連携レコードがあっても、顧客レコードがない場合は会員登録へ
if (is_null($customer)) {
log_info('顧客レコードが取得できなかった為、会員登録へ');
return $this->redirectToRoute('entry');
}
// 仮会員の場合ログインへ
if ($customer->getStatus()->getId() == 1) {
log_info('仮会員のため、ログインへ customer_id:'.$customerId);
if (substr($session->get('$previousUrl'), -$shopLoginUrlLength) === $shopLoginUrl) {
return $this->redirectToRoute('shopping_login');
}
return $this->redirectToRoute('mypage_login');
}
// 本会員かつ、LINE連携レコード・顧客レコードが存在するのでログイン処理
if ($customer->getStatus()->getId() == 2) {
$token = new UsernamePasswordToken($customer, 'customer', array('ROLE_USER'));
$this->tokenStorage->setToken($token);
log_info('ログイン済に変更。dtb_customer.id:'.$this->getUser()->getId());
// カートのマージなどの処理
$loginEvent = new InteractiveLoginEvent($request, $token);
$this->eventDispatcher->dispatch(
$loginEvent,
SecurityEvents::INTERACTIVE_LOGIN
);
// 遷移元がカート経由のログインだった場合、購入画面。そうでない場合マイページに遷移
if (substr($session->get('$previousUrl'), -$shopLoginUrlLength) === $shopLoginUrl) {
return $this->redirectToRoute('shopping');
} else if(substr($session->get('$previousUrl'), -$shopNonmemberUrlLength) === $shopNonmemberUrl){
return $this->redirectToRoute('shopping');
}
if ($redirect = $this->getSendtoRedirectResponse($session)) {
return $redirect;
}
return $this->redirectToRoute('homepage');
}
// 例外としてログインページに戻す
return $this->redirectToRoute('login');
}
}
/**
* 設定レコードを取得します
* @return string
*/
private function getLineLoginIntegrationSetting()
{
$lineIntegrationSetting = $this->lineIntegrationSettingRepository
->find(LineLoginIntegrationAdminController::LINE_LOGIN_INTEGRATION_SETTING_TABLE_ID);
return $lineIntegrationSetting;
}
private function getSendtoRedirectResponse(SessionInterface $session)
{
$sendto = $session->get('sendto');
$param = $session->get('param');
if ($sendto === 'detail' && $param) {
$this->clearSendtoSession($session);
return $this->redirectToRoute('product_detail', ['id' => $param]);
}
if ($sendto === 'list' && $param) {
$this->clearSendtoSession($session);
return $this->redirectToRoute('product_list', ['name' => $param]);
}
if ($sendto === 'homepage') {
$this->clearSendtoSession($session);
return $this->redirectToRoute('homepage');
}
return null;
}
private function clearSendtoSession(SessionInterface $session)
{
$session->remove('sendto');
$session->remove('param');
}
/**
* LINE APIからアクセストークンを取得する為の、パラメータを検証します
* @param $code
* @param $state
* @param $originalState
*
* @return \Symfony\Component\HttpFoundation\Response|null
*/
private function validateParameter($code, $state, $originalState){
if (empty($code)) {
log_error('LINE API エラー(0): 認可コードが空');
$config = $this->lineIntegrationSettingRepository->find(1);
if (is_null($config) || is_null($config->getLineAddCancelRedirectUrl())) {
log_error("[LineIntegration] 設定を取得できませんでした");
return $this->render('error.twig', [
'error_title' => 'エラーが発生しました。',
'error_message' => 'スマートフォンの標準ブラウザもしくはLINEアプリ内ブラウザから、LINEのID連携やLINEログインをお試しください。' . PHP_EOL . PHP_EOL .
'※標準ブラウザとは?' . PHP_EOL .
'スマートフォンに標準搭載されているブラウザのことで、iOSのSafariやAndroidのChromeを指します。' . PHP_EOL . PHP_EOL .
'または、Safariのプライベートブラウズモードや、Chromeのシークレットモードをお使いの場合は、画面が進まない場合があります。標準モードでご利用ください。',
]);
} else {
return $this->redirect($config->getLineAddCancelRedirectUrl());
}
}
if (empty($state)) {
log_error('LINE API エラー(1): CSRF防止用の固有な英数字の文字列が空');
return $this->render('error.twig', [
'error_title' => 'エラーが発生しました。',
'error_message' => 'スマートフォンの標準ブラウザもしくはLINEアプリ内ブラウザから、LINEのID連携やLINEログインをお試しください。' . PHP_EOL . PHP_EOL .
'※標準ブラウザとは?' . PHP_EOL .
'スマートフォンに標準搭載されているブラウザのことで、iOSのSafariやAndroidのChromeを指します。' . PHP_EOL . PHP_EOL .
'または、Safariのプライベートブラウズモードや、Chromeのシークレットモードをお使いの場合は、画面が進まない場合があります。標準モードでご利用ください。',
]);
}
if (empty($originalState)) {
log_error('LINE API エラー(2): セッションタイムアウト');
return $this->render('error.twig', [
'error_title' => 'エラーが発生しました。',
'error_message' => 'スマートフォンの標準ブラウザもしくはLINEアプリ内ブラウザから、LINEのID連携やLINEログインをお試しください。' . PHP_EOL . PHP_EOL .
'※標準ブラウザとは?' . PHP_EOL .
'スマートフォンに標準搭載されているブラウザのことで、iOSのSafariやAndroidのChromeを指します。' . PHP_EOL . PHP_EOL .
'または、Safariのプライベートブラウズモードや、Chromeのシークレットモードをお使いの場合は、画面が進まない場合があります。標準モードでご利用ください。',
]);
}
if ($state != $originalState) {
log_error('LINE API エラー(3): CSRF防止用の固有な英数字の文字列がセッションのものと異なる');
return $this->render('error.twig', [
'error_title' => 'エラーが発生しました。',
'error_message' => 'スマートフォンの標準ブラウザもしくはLINEアプリ内ブラウザから、LINEのID連携やLINEログインをお試しください。' . PHP_EOL . PHP_EOL .
'※標準ブラウザとは?' . PHP_EOL .
'スマートフォンに標準搭載されているブラウザのことで、iOSのSafariやAndroidのChromeを指します。' . PHP_EOL . PHP_EOL .
'または、Safariのプライベートブラウズモードや、Chromeのシークレットモードをお使いの場合は、画面が進まない場合があります。標準モードでご利用ください。',
]);
}
return null;
}
/**
* LINE APIでアクセストークンを発行します
* @param $code
*
* @return mixed
*/
private function publishAccessToken($code){
$url = $this->generateUrl('plugin_line_login_callback',array(),0);
$accessTokenUrl = $this->apiUrl->getApiUrl() . "/oauth2/v2.1/token";
$accessTokenData = array(
"grant_type" => "authorization_code",
"code" => $code,
"redirect_uri" => $url,
"client_id" => $this->lineChannelId,
"client_secret" => $this->lineChannelSecret,
);
$accessTokenData = http_build_query($accessTokenData, "", "&");
$header = array(
"Content-Type: application/x-www-form-urlencoded",
"Content-Length: " . strlen($accessTokenData)
);
$context = array(
"http" => array(
"method" => "POST",
"header" => implode("\r\n", $header),
"content" => $accessTokenData
)
);
$response = file_get_contents($accessTokenUrl, false, stream_context_create($context));
$tokenJson = json_decode($response, true);
return $tokenJson;
}
/**
* LINE APIからLINE IDを取得します
* @param $accessToken
*
* @return mixed
*/
private function getProfile($accessToken){
$lineProfileUrl = $this->apiUrl->getApiUrl() . "/v2/profile";
$context = array(
"http" => array(
"method" => "GET",
"header" => "Authorization: Bearer " . $accessToken
)
);
$response = file_get_contents($lineProfileUrl, false, stream_context_create($context));
$profileJson = json_decode($response, true);
return $profileJson;
}
/**
* 顧客とLINE連携レコードの紐付けを行います
* @param $customer
* @param $lineUserId
* @param $lineUserName
* @param $lineUserAvator
*
* @return \Symfony\Component\HttpFoundation\Response
*/
private function associationCustomerAndLineid($lineUserId, $lineUserName, $lineUserAvator){
log_info('plg_line_login_integrationレコードなし');
$lineIntegration = new LineLoginIntegration();
$lineIntegration->setLineUserId($lineUserId);
// 追加
$lineIntegration->setLineUserName($lineUserName);
$lineIntegration->setLineUserAvatar($lineUserAvator);
$lineIntegration->setCustomerId($this->getUser()->getId());
$lineIntegration->setCustomer($this->getUser());
$this->entityManager->persist($lineIntegration);
$this->entityManager->flush();
log_info('LINE IDとユーザーの関連付け終了');
}
}