BxAdsSourceShopifyPartner.php 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199
  1. <?php defined('BX_DOL') or die('hack attempt');
  2. /**
  3. * Copyright (c) UNA, Inc - https://una.io
  4. * MIT License - https://opensource.org/licenses/MIT
  5. *
  6. * @defgroup Ads Ads
  7. * @ingroup UnaModules
  8. *
  9. * @{
  10. */
  11. require_once('BxAdsSource.php');
  12. /*
  13. * Note. A custom app created via Partner Dashboard should use the OAuth authentication flow.
  14. * Methods makeAuthorization and authorizeApplication are needed for this purpose.
  15. *
  16. * Login for Partner account:
  17. * https://accounts.shopify.com/
  18. *
  19. *
  20. * Integration:
  21. * 0. Create custom app in Partner dashboard.
  22. *
  23. * 1. Authorization flow:
  24. * https://shopify.dev/docs/apps/auth/oauth
  25. *
  26. */
  27. class BxAdsSourceShopifyPartner extends BxAdsSource
  28. {
  29. public static $NAME = 'shopify_partner';
  30. protected $_sShopDomain;
  31. protected $_sApiKey;
  32. protected $_sApiSecretKey;
  33. protected $_aScopes;
  34. protected $_sEndpoint;
  35. protected $_sToken;
  36. protected $_aLastResponseHeaders;
  37. protected $_sCacheKey;
  38. protected $_iCacheLifetime;
  39. protected $_oCache;
  40. public function __construct($iProfile, $aSource, $oModule)
  41. {
  42. $this->_sName = self::$NAME;
  43. parent::__construct($iProfile, $aSource, &$oModule);
  44. $this->_sShopDomain = $this->getOption('shop_domain');
  45. $this->_sApiKey = $this->getOption('api_key');
  46. $this->_sApiSecretKey = $this->getOption('api_secret_key');
  47. $this->_sToken = $this->getOption('token');
  48. $this->_aScopes = [];
  49. $this->_sEndpoint = "https://{$this->_sShopDomain}";
  50. $this->_sCacheKey = $this->_oModule->_oConfig->getCacheKeyShopify();
  51. $this->_iCacheLifetime = $this->_oModule->_oConfig->getCacheLifetimeShopify();
  52. $sCacheEngine = $this->_oModule->_oConfig->getCacheEngineShopify();
  53. $this->_oCache = bx_instance('BxDolCache' . $sCacheEngine);
  54. if(!$this->_oCache->isAvailable())
  55. $this->_oCache = bx_instance('BxDolCacheFile');
  56. }
  57. public function makeAuthorization($aScopes = [])
  58. {
  59. $sNonce = bin2hex(random_bytes(10));
  60. if(empty($aScopes))
  61. $aScopes = $this->_aScopes;
  62. $aCached = $this->_oCache->getData($this->_sCacheKey, $this->_iCacheLifetime);
  63. if(empty($aCached))
  64. $aCached = [];
  65. $aCached[$this->_sShopDomain] = ['nonce' => $sNonce, 'scopes' => $aScopes];
  66. $this->_oCache->setData($this->_sCacheKey, $aCached, $this->_iCacheLifetime);
  67. return $this->_getAuthorizeUrl($aScopes, BX_DOL_URL_ROOT . $this->_oModule->_oConfig->getBaseUri() . 'shopify_handle_callback', $sNonce);
  68. }
  69. public function authorizeApplication($aRequestData)
  70. {
  71. $aRequiredKeys = ['code', 'hmac', 'state', 'shop'];
  72. foreach($aRequiredKeys as $sRk)
  73. if(!in_array($sRk, array_keys($aRequestData)))
  74. return $this->_log("The provided request data is missing one of the following keys: " . implode(', ', $aRequiredKeys));
  75. $aCached = $this->_oCache->getData($this->_sCacheKey, $this->_iCacheLifetime);
  76. if(!isset($aCached[$this->_sShopDomain]))
  77. return $this->_log("Nonce cannot be found: " . $this->_sShopDomain);
  78. $sNonce = $aCached[$this->_sShopDomain]['nonce'];
  79. if($aRequestData['state'] !== $sNonce)
  80. return $this->_log("The provided nonce ($sNonce) did not match the nonce provided by Shopify ({$aRequestData['state']})");
  81. if(!filter_var($aRequestData['shop'], FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME))
  82. return $this->_log("The shop provided by Shopify ({$aRequestData['shop']}) is an invalid hostname.");
  83. if($aRequestData['shop'] !== $this->_sShopDomain)
  84. return $this->_log("The shop provided by Shopify ({$aRequestData['shop']}) does not match the shop provided to this API ({$this->_sShopDomain})");
  85. // Check HMAC signature. See https://help.shopify.com/api/getting-started/authentication/oauth#verification
  86. $aHmacSource = [];
  87. foreach($aRequestData as $sKey => $sValue) {
  88. if($sKey === 'hmac')
  89. continue;
  90. // Replace the characters as specified by Shopify in the keys and values
  91. $aPatterns = ['&' => '%26', '%' => '%25'];
  92. $aPatternsKey = array_merge($aPatterns, ['=' => '%3D']);
  93. $sKey = str_replace(array_keys($aPatternsKey), array_values($aPatternsKey), $sKey);
  94. $sValue = str_replace(array_keys($aPatterns), array_values($aPatterns), $sValue);
  95. $aHmacSource[] = $sKey . '=' . $sValue;
  96. }
  97. // Sort the key value pairs lexographically and then generate the HMAC signature of the provided data
  98. sort($aHmacSource);
  99. $sHmacBase = implode('&', $aHmacSource);
  100. $sHmacString = hash_hmac('sha256', $sHmacBase, $this->_sApiSecretKey);
  101. // Verify that the signatures match
  102. if($sHmacString !== $aRequestData['hmac'])
  103. return $this->_log("The HMAC provided by Shopify ({$requestData['hmac']}) doesn't match the HMAC verification ($sHmacString).");
  104. // Make the access token request to Shopify
  105. $aResponse = $this->_callAuthorize(['code' => $requestData['code']]);
  106. /**
  107. * TODO: Decode data and retrieve Access token.
  108. *
  109. // Decode the response from Shopify
  110. $data = json_decode($response->getBody());
  111. // Set the access token
  112. $this->setToken($data->access_token);
  113. */
  114. return $aResponse;
  115. }
  116. /**
  117. * Internal methods.
  118. */
  119. protected function _getAuthorizeUrl($aScopes, $sRedirectUrl, $sNonce, $bOnlineAccessMode = false)
  120. {
  121. $aParams = [
  122. 'client_id' => $this->_sApiKey,
  123. 'scope' => implode(',', $aScopes),
  124. 'redirect_uri' => $sRedirectUrl,
  125. 'state' => $sNonce,
  126. ];
  127. if ($bOnlineAccessMode)
  128. $args['grant_options[]'] = 'per-user';
  129. return bx_append_url_params("https://" . $this->_sShopDomain . "/admin/oauth/authorize", $aParams);
  130. }
  131. protected function _callAuthorize($aParams)
  132. {
  133. $aParams = array_merge([
  134. 'client_id' => $this->_sApiKey,
  135. 'client_secret' => $this->_sApiSecretKey
  136. ], $aParams);
  137. return $this->_call('admin/oauth/access_token', $aParams, 'post-json', ['Content-Type' => 'application/json']);
  138. }
  139. protected function _call($sRequest, $aParams, $sMethod = 'post-json', $aHeaders = [])
  140. {
  141. $aHeaders[] = 'Content-Type: application/json';
  142. if(!empty($this->_sToken))
  143. $aHeaders[] = 'X-Shopify-Access-Token: ' . $this->_sToken;
  144. else
  145. $aHeaders[] = 'Authorization: Basic ' . base64_encode($this->_sApiKey . ':' . $this->_sApiSecretKey);
  146. return bx_file_get_contents($this->_sEndpoint . $sRequest, $aParams, $sMethod, $aHeaders);
  147. }
  148. protected function _log($sMessage, $bUseLog = false)
  149. {
  150. if($bUseLog) {
  151. //TODO: Use bx_log here with ERROR log level
  152. }
  153. else
  154. throw new Exception($sMessage);
  155. return false;
  156. }
  157. }