<?php
/*
* Amazon Pay V2 for EC-CUBE4.2
* Copyright(c) 2023 EC-CUBE CO.,LTD. all rights reserved.
*
* https://www.ec-cube.co.jp/
*
* This program is not free software.
* It applies to terms of service.
*
*/
namespace Plugin\AmazonPayV2_42;
use Eccube\Event\TemplateEvent;
use Eccube\Event\EventArgs;
use Eccube\Event\EccubeEvents;
use Eccube\Common\EccubeConfig;
use Eccube\Repository\PaymentRepository;
use Eccube\Repository\PluginRepository;
use Eccube\Service\OrderHelper;
use Eccube\Service\CartService;
use Plugin\AmazonPayV2_42\Repository\ConfigRepository;
use Plugin\AmazonPayV2_42\Service\AmazonRequestService;
use Plugin\AmazonPayV2_42\Service\Method\AmazonPay;
use Plugin\AmazonPayV2_42\phpseclib\Crypt\Random;
use Psr\Container\ContainerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Eccube\Repository\DeliveryRepository;
use Eccube\Repository\PaymentOptionRepository;
use Plugin\AmazonPayV2_42\Entity\AmazonBanner;
use Plugin\AmazonPayV2_42\Service\AmazonBannerService;
class AmazonPayEvent implements EventSubscriberInterface
{
/**
* @var string プロファイル情報キー
*/
private $sessionAmazonProfileKey = 'amazon_pay_v2.profile';
/**
* @var string プロファイル情報キー
*/
private $sessionAmazonCheckoutSessionIdKey = 'amazon_pay_v2.checkout_session_id';
/**
* @var string Amazonログインステート
*/
private $sessionAmazonLoginStateKey = 'amazon_pay_v2.amazon_login_state';
/**
* @var EccubeConfig
*/
protected $eccubeConfig;
/**
* @var UrlGeneratorInterface
*/
private $router;
/**
* @var ConfigRepository
*/
protected $configRepository;
/**
* @var AmazonRequestService
*/
protected $amazonRequestService;
/**
* @var DeliveryRepository
*/
protected $deliveryRepository;
/**
* @var DeliveryRepository
*/
protected $paymentOptionRepository;
/**
* Amazon Payバナーサービス
*
* @var AmazonBannerService
*/
protected $amazonBannerService;
/**
* AmazonPayEvent
*
* @param eccubeConfig $eccubeConfig
* @param ConfigRepository $configRepository
*/
public function __construct(
RequestStack $requestStack,
TokenStorageInterface $tokenStorage,
EccubeConfig $eccubeConfig,
UrlGeneratorInterface $router,
PaymentRepository $paymentRepository,
PluginRepository $pluginRepository,
ConfigRepository $configRepository,
ContainerInterface $container,
OrderHelper $orderHelper,
CartService $cartService,
AmazonRequestService $amazonRequestService,
DeliveryRepository $deliveryRepository,
PaymentOptionRepository $paymentOptionRepository,
AmazonBannerService $amazonBannerService
) {
$this->requestStack = $requestStack;
$this->session = $requestStack->getSession();
$this->tokenStorage = $tokenStorage;
$this->eccubeConfig = $eccubeConfig;
$this->router = $router;
$this->paymentRepository = $paymentRepository;
$this->pluginRepository = $pluginRepository;
$this->configRepository = $configRepository;
$this->container = $container;
$this->orderHelper = $orderHelper;
$this->cartService = $cartService;
$this->amazonRequestService = $amazonRequestService;
$this->deliveryRepository = $deliveryRepository;
$this->paymentOptionRepository = $paymentOptionRepository;
$this->amazonBannerService = $amazonBannerService;
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents()
{
return [
EccubeEvents::FRONT_CART_BUYSTEP_COMPLETE => 'amazon_cart_buystep',
'Cart/index.twig' => 'cart',
'Shopping/index.twig' => 'amazon_pay_shopping',
'Mypage/login.twig' => 'mypage_login',
'Shopping/confirm.twig' => 'amazon_pay_shopping_confirm',
'index.twig' => 'add_banner_on_top',
//ショッピングページでも使用できるよう追加
'Shopping/index.twig' => 'shopping',
'Shopping/login.twig' => 'shopping_login',
'lp/prostate-care.twig' => ['lp_prostate_care', -99999],
];
}
/**
* トップページのイベント
* Amazon様バナーを挿入する
*
* @param TemplateEvent $event
* @return void
*/
public function add_banner_on_top(TemplateEvent $event)
{
$Config = $this->configRepository->get();
if ($Config->getUseAmazonBannerOnTop() == $this->eccubeConfig['amazon_pay_v2']['toggle']['off']) {
return;
}
if ($Config->getAmazonBannerPlaceOnTop() == $this->eccubeConfig['amazon_pay_v2']['button_place']['auto']) {
$event->addSnippet('@AmazonPayV2_42/default/amazon_banner_auto_on_top.twig');
}
$event->addSnippet($this->amazonBannerService->getBannerCodeOnTop(), false);
}
public function cart(TemplateEvent $event)
{
$Config = $this->configRepository->get();
if ($Config->getUseAmazonBannerOnCart() != $this->eccubeConfig['amazon_pay_v2']['toggle']['off']) {
if ($Config->getAmazonBannerPlaceOnCart() == $this->eccubeConfig['amazon_pay_v2']['button_place']['auto']) {
$event->addSnippet('@AmazonPayV2_42/default/amazon_banner_auto_on_cart.twig');
}
$event->addSnippet($this->amazonBannerService->getBannerCodeOnCart(), false);
}
$parameters = $event->getParameters();
if (empty($parameters['Carts'])) {
return;
}
if ($Config->getUseCartButton() == $this->eccubeConfig['amazon_pay_v2']['toggle']['off']) {
return;
}
// AmazonPayに紐づく商品種別の取得
$Payment = $this->paymentRepository->findOneBy(['method_class' => AmazonPay::class]);
$AmazonDeliveries = $this->paymentOptionRepository->findBy(['payment_id' => $Payment->getId()]);
$AmazonSaleTypes = [];
foreach ($AmazonDeliveries as $AmazonDelivery) {
$Delivery = $this->deliveryRepository->findOneBy(['id' => $AmazonDelivery->getDelivery()->getId()]);
$AmazonSaleTypes[] = $Delivery->getSaleType()->getId();
}
$parameters['AmazonSaleTypes'] = $AmazonSaleTypes;
foreach ($parameters['Carts'] as $Cart) {
$cartKey = $Cart->getCartKey();
$payload = $this->amazonRequestService->createCheckoutSessionPayload($Cart->getCartKey());
$signature = $this->amazonRequestService->signaturePayload($payload);
$parameters['cart'][$cartKey]['payload'] = $payload;
$parameters['cart'][$cartKey]['signature'] = $signature;
}
$parameters['AmazonPayV2Config'] = $Config;
if ($Config->getEnv() == $this->eccubeConfig['amazon_pay_v2']['env']['prod']) {
$parameters['AmazonPayV2Api'] = $this->eccubeConfig['amazon_pay_v2']['api']['prod'];
} else {
$parameters['AmazonPayV2Api'] = $this->eccubeConfig['amazon_pay_v2']['api']['sandbox'];
}
$event->setParameters($parameters);
$event->addSnippet('@AmazonPayV2_42/default/Cart/amazon_pay_js.twig');
if ($Config->getCartButtonPlace() == $this->eccubeConfig['amazon_pay_v2']['button_place']['auto']) {
$event->addSnippet('@AmazonPayV2_42/default/Cart/button.twig');
}
}
public function amazon_cart_buystep(EventArgs $event)
{
// Amazonログインによる仮会員情報がセッションにセットされていたら
if ($this->orderHelper->getNonmember() && $this->session->get($this->sessionAmazonProfileKey)) {
// 仮会員情報を削除
$this->session->remove(OrderHelper::SESSION_NON_MEMBER);
$this->session->remove($this->sessionAmazonProfileKey);
$this->cartService->setPreOrderId(null);
$this->cartService->save();
}
}
public function amazon_pay_shopping(TemplateEvent $event)
{
$request = $this->requestStack->getMainRequest();
$uri = $request->getUri();
$parameters = $event->getParameters();
if (preg_match('/shopping\/amazon_pay/', $uri) == false) {
$referer = $request->headers->get('referer');
$Order = $parameters['Order'];
$Payment = $Order->getPayment();
// AmazonPay決済確認画面→クーポン入力画面→決済確認画面への遷移時にAmazonPay決済確認画面へ戻す
if ($Payment && $Payment->getMethodClass() === AmazonPay::class && preg_match('/shopping_coupon/', $referer)) {
header("Location:" . $this->router->generate('amazon_pay_shopping'));
exit;
}
return;
}
$Config = $this->configRepository->get();
$event->addSnippet('@AmazonPayV2_42/default/Shopping/widgets.twig');
$event->addSnippet('@AmazonPayV2_42/default/Shopping/customer_regist_v2.twig');
// チェックアウトセッションIDを取得する
$amazonCheckoutSessionId = $this->session->get($this->sessionAmazonCheckoutSessionIdKey);
$parameters = $event->getParameters();
$parameters['amazonCheckoutSessionId'] = $amazonCheckoutSessionId;
$parameters['AmazonPayV2Config'] = $Config;
// メルマガプラグイン利用時はチェックボックスを追加
if (
$this->pluginRepository->findOneBy(['code' => 'MailMagazine42', 'enabled' => true])
|| $this->pluginRepository->findOneBy(['code' => 'PostCarrier42', 'enabled' => true])
) {
$useMailMagazine = true;
} else {
$useMailMagazine = false;
}
$parameters['useMailMagazine'] = $useMailMagazine;
if ($Config->getEnv() == $this->eccubeConfig['amazon_pay_v2']['env']['prod']) {
$parameters['AmazonPayV2Api'] = $this->eccubeConfig['amazon_pay_v2']['api']['prod'];
} else {
$parameters['AmazonPayV2Api'] = $this->eccubeConfig['amazon_pay_v2']['api']['sandbox'];
}
$event->setParameters($parameters);
}
public function amazon_pay_shopping_confirm(TemplateEvent $event)
{
$request = $this->requestStack->getMainRequest();
$uri = $request->getUri();
if (preg_match('/shopping\/amazon_pay/', $uri) == false) {
return;
}
$Config = $this->configRepository->get();
$event->addSnippet('@AmazonPayV2_42/default/Shopping/confirm_widgets.twig');
$event->addSnippet('@AmazonPayV2_42/default/Shopping/confirm_customer_regist_v2.twig');
$parameters = $event->getParameters();
$parameters['AmazonPayV2Config'] = $Config;
// メルマガプラグイン利用時はチェックボックスを追加
if (
$this->pluginRepository->findOneBy(['code' => 'MailMagazine42', 'enabled' => true])
|| $this->pluginRepository->findOneBy(['code' => 'PostCarrier42', 'enabled' => true])
) {
$useMailMagazine = true;
} else {
$useMailMagazine = false;
}
$parameters['useMailMagazine'] = $useMailMagazine;
if ($Config->getEnv() == $this->eccubeConfig['amazon_pay_v2']['env']['prod']) {
$parameters['AmazonPayV2Api'] = $this->eccubeConfig['amazon_pay_v2']['api']['prod'];
} else {
$parameters['AmazonPayV2Api'] = $this->eccubeConfig['amazon_pay_v2']['api']['sandbox'];
}
$event->setParameters($parameters);
}
public function mypage_login(TemplateEvent $event)
{
$Config = $this->configRepository->get();
if ($Config->getUseMypageLoginButton() == $this->eccubeConfig['amazon_pay_v2']['toggle']['off']) {
return;
}
$state = $this->session->get($this->sessionAmazonLoginStateKey);
if (!$state) {
// stateが生成されていなければ、生成及びセッションセット
$state = bin2hex(Random::string(16));
$this->session->set($this->sessionAmazonLoginStateKey, $state);
}
$returnUrl = $this->router->generate('login_with_amazon', ['state' => $state], UrlGeneratorInterface::ABSOLUTE_URL);
$parameters = $event->getParameters();
$payload = $this->amazonRequestService->createSigninPayload($returnUrl);
$signature = $this->amazonRequestService->signaturePayload($payload);
$parameters['payload'] = $payload;
$parameters['signature'] = $signature;
$parameters['buttonColor'] = $Config->getMypageLoginButtonColor();
$parameters['AmazonPayV2Config'] = $Config;
if ($Config->getEnv() == $this->eccubeConfig['amazon_pay_v2']['env']['prod']) {
$parameters['AmazonPayV2Api'] = $this->eccubeConfig['amazon_pay_v2']['api']['prod'];
} else {
$parameters['AmazonPayV2Api'] = $this->eccubeConfig['amazon_pay_v2']['api']['sandbox'];
}
$event->setParameters($parameters);
if ($Config->getMypageLoginButtonPlace() == $this->eccubeConfig['amazon_pay_v2']['button_place']['auto']) {
$event->addSnippet('@AmazonPayV2_42/default/Mypage/login_page_button.twig');
}
$event->addSnippet('@AmazonPayV2_42/default/Mypage/amazon_login_js.twig');
}
// ショッピングページでも使用できるよう追加
public function shopping(TemplateEvent $event)
{
$parameters = $event->getParameters();
if (empty($parameters['Carts'])) {
$parameters['Carts'] = $this->cartService->getCarts();
}
$event->setParameters($parameters);
$this->amazon_pay_shopping($event);
$this->cart($event);
}
// ショッピングページでも使用できるよう追加
public function shopping_login(TemplateEvent $event)
{
$parameters = $event->getParameters();
if (empty($parameters['Carts'])) {
$parameters['Carts'] = $this->cartService->getCarts();
}
$event->setParameters($parameters);
$this->cart($event);
}
public function lp_prostate_care(TemplateEvent $event)
{
$req = $this->requestStack->getMainRequest();
$this->amzDebug('lp_prostate_care fired path=' . ($req ? $req->getPathInfo() : 'null') . ' qs=' . ($req ? $req->getQueryString() : 'null'));
$Config = $this->configRepository->get();
$parameters = $event->getParameters();
if (empty($parameters['Carts'])) {
$parameters['Carts'] = $this->cartService->getCarts();
}
if (!isset($parameters['cart']) || !is_array($parameters['cart'])) {
$parameters['cart'] = [];
}
// Always set these (LP JS needs them)
$parameters['AmazonPayV2Config'] = $Config;
$parameters['AmazonPayV2Api'] =
($Config->getEnv() == $this->eccubeConfig['amazon_pay_v2']['env']['prod'])
? $this->eccubeConfig['amazon_pay_v2']['api']['prod']
: $this->eccubeConfig['amazon_pay_v2']['api']['sandbox'];
// Only guard the signing, NOT the snippet injection
if (empty($parameters['_lp_amz_signed'])) {
$parameters['_lp_amz_signed'] = true;
if (!empty($parameters['Carts'])) {
foreach ($parameters['Carts'] as $Cart) {
$cartKey = $Cart->getCartKey();
$payload = $this->amazonRequestService->createCheckoutSessionPayload($cartKey);
$signature = $this->amazonRequestService->signaturePayload($payload);
$parameters['cart'][$cartKey]['payload'] = $payload;
$parameters['cart'][$cartKey]['signature'] = $signature;
$this->amzDebug(sprintf(
'cartKey=%s env=%s sellerId=%s publicKeyId=%s payloadLen=%d payloadSha256=%s sigLen=%d',
$cartKey,
(string) $Config->getEnv(),
(string) $Config->getSellerId(),
(string) $Config->getPublicKeyId(),
strlen($payload),
hash('sha256', $payload),
strlen($signature)
));
if ($this->amzDebugEnabled()) {
$sigPlugin = $this->amazonRequestService->signaturePayload($payload);
$this->amzDebug('sig_equal=' . (hash_equals($sigPlugin, $signature) ? '1' : '0'));
}
}
}
}
$event->setParameters($parameters);
// ALWAYS do this
$event->addSnippet('lp/amazon_pay_lp_js.twig');
}
public function lp_prostate_care_frame(TemplateEvent $event)
{
$req = $this->requestStack->getMainRequest();
if (!$req) {
return;
}
// Only for the LP URL
if ($req->getPathInfo() !== '/lp/prostate-care') {
return;
}
// Reuse your existing LP logic (payload + addSnippet)
$this->lp_prostate_care($event);
}
private function amzDebugEnabled(): bool
{
$req = $this->requestStack->getMainRequest();
if (!$req) return false;
// Only when you visit with ?amzdebug=1
return $req->query->get('amzdebug') === '1';
}
private function amzDebug(string $msg): void
{
if ($this->amzDebugEnabled()) {
error_log('[AMZDEBUG] ' . $msg);
}
}
/**
* Resolve the private key PEM from the plugin Config.
* Supports both "path stored in config" and "PEM stored in config" patterns.
*/
private function resolveAmazonPayPrivateKeyPem($Config): string
{
$candidates = [];
foreach ([
'getPrivateKeyPem',
'getPrivateKey',
'getPrivateKeyPath',
'getPrivateKeyFile',
'getPrivateKeyFilePath',
'getSecretKeyPath',
'getSecretKey',
] as $method) {
if (is_object($Config) && method_exists($Config, $method)) {
try {
$candidates[] = $Config->$method();
} catch (\Throwable $e) {
// ignore and continue
}
}
}
foreach ($candidates as $v) {
if (!is_string($v) || $v === '') continue;
// Direct PEM
if (strpos($v, 'BEGIN') !== false && strpos($v, 'PRIVATE KEY') !== false) {
return $v;
}
// Path to PEM
if (is_readable($v)) {
$pem = file_get_contents($v);
if (is_string($pem) && strpos($pem, 'BEGIN') !== false) {
return $pem;
}
}
}
throw new \RuntimeException('AmazonPay: could not resolve private key PEM from Config.');
}
/**
* Create Amazon Pay CV2 signature (RSA-PSS, SHA256, saltLength=20) for the given payload.
* Avoids relying on OPENSSL_PKCS1_PSS_PADDING which may be unavailable on some hosts.
*/
private function signPayloadForAmazonPay(string $payload, $Config): string
{
// JS string-literal interpretation unescapes '\/' -> '/', so normalize before hashing/signing.
$payloadForSign = str_replace('\\\/', '/', $payload);
$payloadHash = hash('sha256', $payloadForSign);
$stringToSign = "AMZN-PAY-RSASSA-PSS\n" . $payloadHash;
try {
$pem = $this->resolveAmazonPayPrivateKeyPem($Config);
} catch (\Throwable $e) {
// Fallback to the plugin's existing signer to avoid breaking the page.
return $this->amazonRequestService->signaturePayload($payload);
}
// Prefer OpenSSL PSS if available
if (defined('OPENSSL_PKCS1_PSS_PADDING')) {
$privateKey = openssl_pkey_get_private($pem);
if ($privateKey) {
$signatureBin = '';
$ok = openssl_sign(
$stringToSign,
$signatureBin,
$privateKey,
OPENSSL_ALGO_SHA256,
[
'rsaPadding' => OPENSSL_PKCS1_PSS_PADDING,
'saltLength' => 20,
]
);
if ($ok) {
return base64_encode($signatureBin);
}
}
}
// Fallback: phpseclib3 (pure PHP) RSA-PSS
if (class_exists('\phpseclib3\Crypt\PublicKeyLoader') && class_exists('\phpseclib3\Crypt\RSA')) {
$private = \phpseclib3\Crypt\PublicKeyLoader::loadPrivateKey($pem)
->withPadding(\phpseclib3\Crypt\RSA::SIGNATURE_PSS)
->withHash('sha256')
->withMGFHash('sha256')
->withSaltLength(20);
return base64_encode($private->sign($stringToSign));
}
throw new \RuntimeException('AmazonPay: RSA-PSS signing unavailable. OPENSSL_PKCS1_PSS_PADDING is missing and phpseclib3 is not installed.');
}
}