BxMailchimpModule.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340
  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 Mailchimp Mailchimp integration module
  7. * @ingroup UnaModules
  8. *
  9. * @{
  10. */
  11. define('BX_MAILCHIMP_LIMIT', 5000);
  12. class BxMailchimpModule extends BxDolModule
  13. {
  14. protected $sHostnameAPI = 'https://{dc}.api.mailchimp.com/3.0';
  15. protected $aFields = array(
  16. 'ACCOUNT_ID' => 'number',
  17. 'PROFILE_ID' => 'number',
  18. 'MEMBERSHIP' => 'text',
  19. 'STATUS' => 'text',
  20. 'TYPE' => 'text',
  21. 'PROFILES' => 'text',
  22. 'PROFILE_URL' => 'url',
  23. 'IMAGE_URL' => 'imageurl',
  24. 'PHONE' => 'phone',
  25. );
  26. function __construct(&$aModule)
  27. {
  28. parent::__construct($aModule);
  29. if (($sKey = getParam('bx_mailchimp_option_api_key')) && ($a = explode('-', $sKey)) && !empty($a[1]))
  30. $this->sHostnameAPI = str_replace('{dc}', $a[1], $this->sHostnameAPI);
  31. else
  32. $this->sHostnameAPI = '';
  33. }
  34. public function serviceUpdateMergeFields()
  35. {
  36. if (!isAdmin() || !($sListId = getParam('bx_mailchimp_option_list_id')))
  37. return false;
  38. foreach ($this->aFields as $sName => $sType) {
  39. $s = $this->_request('/lists/' . $sListId . '/merge-fields', 'POST', array('name' => $sName, 'tag' => $sName, 'type' => $sType), $sErrorCurl);
  40. if (!($a = $this->_checkForErrors($s, $sErrorCurl)) || isset($a['error']))
  41. return false;
  42. }
  43. return true;
  44. }
  45. public function serviceGetLists($bAddEmptyValue = true)
  46. {
  47. if (!isAdmin())
  48. return false;
  49. $s = $this->_request('/lists?count=100', 'GET', array(), $sErrorCurl);
  50. if (!($a = $this->_checkForErrors($s, $sErrorCurl)) || isset($a['error']))
  51. return array();
  52. if (isset($a['type']) && isset($a['detail']))
  53. die(json_encode(array('error' => $a['detail'])));
  54. if (!isset($a['lists']))
  55. return array();
  56. $aRet = array();
  57. foreach ($a['lists'] as $r)
  58. $aRet[] = array('key' => $r['id'], 'value' => $r['name']);
  59. if ($bAddEmptyValue)
  60. array_unshift($aRet, _t('_sys_please_select'));
  61. return $aRet;
  62. }
  63. /**
  64. * remove user from mailchimp by email or account id
  65. */
  66. public function serviceRemoveAccount($mixed)
  67. {
  68. return $this->_accountAction($mixed, 'DELETE');
  69. }
  70. /**
  71. * update user in mailchimp by email or account id
  72. */
  73. public function serviceUpdateAccount($sEmail)
  74. {
  75. return $this->_accountAction($sEmail, 'PUT');
  76. }
  77. protected function _accountAction($sEmail, $sMethod)
  78. {
  79. if (!$sEmail || !($oAccount = BxDolAccount::getInstance($sEmail)) || !($sListId = getParam('bx_mailchimp_option_list_id')))
  80. return false;
  81. $aUserInfo = $this->_prepareUser($oAccount);
  82. $s = $this->_request('/lists/' . $sListId . '/members/' . md5(strtolower($aUserInfo['email_address'])), $sMethod, $aUserInfo, $sErrorCurl);
  83. if (!($a = $this->_checkForErrors($s, $sErrorCurl)) || isset($a['error']))
  84. return false;
  85. if (isset($a['type']) && isset($a['detail']))
  86. return false;
  87. return true;
  88. }
  89. /**
  90. * remove user from mailchimp by email or account id
  91. */
  92. public function actionAccountRemove($sEmail)
  93. {
  94. if (!isAdmin())
  95. die('{}');
  96. if (!$this->serviceRemoveAccount($sEmail))
  97. die(json_encode(array('error' => true, 'message' => "Delete failed")));
  98. echo json_encode(array('message' => "User was successfully deleted"));
  99. }
  100. /**
  101. * update user by email or account id
  102. */
  103. public function actionAccountUpdate($sEmail)
  104. {
  105. if (!isAdmin())
  106. die('{}');
  107. if (!$this->serviceUpdateAccount($sEmail))
  108. die(json_encode(array('error' => true, 'message' => "Update failed")));
  109. echo json_encode(array('message' => "User was successfully updated"));
  110. }
  111. /**
  112. * Add members in bulk from local database to mailchimp
  113. */
  114. public function actionBulkAdd($iLimit = BX_MAILCHIMP_LIMIT)
  115. {
  116. if (!isAdmin() || !($sListId = getParam('bx_mailchimp_option_list_id')))
  117. die('{}');
  118. set_time_limit(3600);
  119. $aIds = $this->_oDb->getInitialUsers($iLimit);
  120. $aChunks = array_chunk($aIds, 1000);
  121. $iTotal = count($aIds);
  122. $iCounter = 0;
  123. foreach ($aChunks as $aIds) {
  124. $aOperations = array();
  125. $iTotal2 = 0;
  126. foreach ($aIds as $iAccountId) {
  127. if (!($oAccount = BxDolAccount::getInstance($iAccountId)))
  128. continue;
  129. if (!($aUserInfo = $this->_prepareUser($oAccount)))
  130. continue;
  131. $aOperations[] = array(
  132. 'method' => 'PUT',
  133. 'path' => '/lists/' . $sListId . '/members/' . md5(strtolower($aUserInfo['email_address'])),
  134. 'body' => json_encode($aUserInfo),
  135. );
  136. ++$iTotal2;
  137. }
  138. $s = $this->_request('/batches', 'POST', array('operations' => $aOperations), $sErrorCurl);
  139. if (!($a = $this->_checkForErrors($s, $sErrorCurl)) || isset($a['error']))
  140. die(json_encode($a));
  141. if (isset($a['type']) && isset($a['detail']))
  142. die(json_encode(array('error' => $a['detail'])));
  143. $iCounter += $iTotal2;
  144. sleep(1);
  145. }
  146. echo json_encode(array('message' => _t('_bx_mailchimp_accounts_processed', $iCounter)));
  147. }
  148. /**
  149. * Test query to return some stats
  150. */
  151. public function actionStats()
  152. {
  153. if (!isAdmin() || !($sListId = getParam('bx_mailchimp_option_list_id')))
  154. die('{}');
  155. $s = $this->_request('/lists/' . $sListId, 'GET', array(), $sErrorCurl);
  156. if (!($a = $this->_checkForErrors($s, $sErrorCurl)) || isset($a['error']))
  157. die(json_encode($a));
  158. if (isset($a['type']) && isset($a['detail']))
  159. die(json_encode(array('error' => $a['detail'])));
  160. echo json_encode(array(
  161. 'unsubscribed' => $a['stats']['unsubscribe_count'],
  162. 'cleaned' => $a['stats']['cleaned_count'],
  163. 'total' => $a['stats']['member_count'],
  164. ));
  165. }
  166. protected function _prepareUser($oAccount)
  167. {
  168. $oProfile = BxDolProfile::getInstanceByAccount($oAccount->id());
  169. if (!$oProfile)
  170. return false;
  171. $aInfoProfile = $oProfile->getInfo();
  172. $aInfoAccount = $oAccount->getInfo();
  173. //$aInfoSession = $this->_oDb->getSessionRowByAccountId($oAccount->id());
  174. $aProfilesIds = $oAccount->getProfilesIds();
  175. // membership
  176. $aMembership = BxDolAcl::getInstance()->getMemberMembershipInfo($oProfile->id());
  177. // profiles
  178. $sProfiles = '';
  179. foreach ($aProfilesIds as $iId) {
  180. if (!($o = BxDolProfile::getInstance($iId)))
  181. continue;
  182. $sProfiles .= $o->getDisplayName() . ', ';
  183. }
  184. $sProfiles = trim($sProfiles, ', ');
  185. $sEmail = $oAccount->getEmail();
  186. $aInfo = $oAccount->getInfo();
  187. $iAccountId = $oAccount->id();
  188. $iProfileId = $oProfile->id();
  189. $aMarkers = array (
  190. 'FNAME' => $oProfile->getDisplayName(),
  191. 'ACCOUNT_ID' => $iAccountId,
  192. 'PROFILE_ID' => $iProfileId,
  193. 'MEMBERSHIP' => _t($aMembership['name']) . (isset($aMembership['date_expires']) && $aMembership['date_expires'] ? ' (expires:' . bx_time_utc($aMembership['date_expires']) . ')' : ''),
  194. 'STATUS' => $aInfoProfile['status'],
  195. 'TYPE' => $aInfoProfile['type'],
  196. 'PROFILES' => $sProfiles,
  197. 'PROFILEURL' => $oProfile->getUrl(),
  198. 'IMAGE_URL' => $oProfile->getAvatar(),
  199. 'PHONE' => $aInfo['phone'],
  200. 'RPASS_URL' => ''
  201. );
  202. if(($sResetPasswordUrl = bx_get_reset_password_link($sEmail)) !== false)
  203. $aMarkers['RPASS_URL'] = $sResetPasswordUrl;
  204. bx_alert($this->_aModule['name'], 'user_fields', $iAccountId, $iProfileId, array('email' => $sEmail, 'markers' => &$aMarkers, 'markers_ref' => &$aMarkers));
  205. return array (
  206. 'email_address' => $oAccount->getEmail(),
  207. 'status' => $aInfoAccount['receive_news'] ? 'subscribed' : 'unsubscribed',
  208. 'merge_fields' => $aMarkers
  209. );
  210. }
  211. /**
  212. * @return json error object on error or empty string on success
  213. */
  214. protected function _checkForErrors($s, $sErrorCurl)
  215. {
  216. if (!$s)
  217. return array('error' => 'curl error: ' . $sErrorCurl);
  218. $a = json_decode($s, true);
  219. if (null === $a)
  220. return array('error' => 'json decode failed:' . htmlspecialchars($s));
  221. return $a;
  222. }
  223. /**
  224. * Perform JSON request to the API endpoint and get JSON response
  225. * @return false on error
  226. */
  227. protected function _request($sURI, $sMethod, $aData, &$sError)
  228. {
  229. $sMethod = !empty($sMethod) ? $sMethod : 'GET';
  230. $aData = !empty($aData) ? $aData : array();
  231. $sApiKey = getParam('bx_mailchimp_option_api_key');
  232. if (!$sApiKey || !$this->sHostnameAPI) {
  233. $sError = "No API Key or hostname isn't configured";
  234. return false;
  235. }
  236. $r = curl_init();
  237. $aHeaders = array(
  238. 'Accept: application/json',
  239. );
  240. curl_setopt($r, CURLOPT_USERPWD, "user:" . $sApiKey);
  241. curl_setopt($r, CURLOPT_TIMEOUT, 30);
  242. curl_setopt($r, CURLOPT_URL, $this->sHostnameAPI . $sURI);
  243. curl_setopt($r, CURLOPT_HEADER, 0);
  244. curl_setopt($r, CURLOPT_RETURNTRANSFER, 1);
  245. curl_setopt($r, CURLOPT_FOLLOWLOCATION, 1);
  246. if ('POST' == $sMethod || 'PUT' == $sMethod || 'PATCH' == $sMethod) {
  247. $sDataJson = json_encode($aData);
  248. if ('POST' == $sMethod)
  249. curl_setopt($r, CURLOPT_POST, true);
  250. else
  251. curl_setopt($r, CURLOPT_CUSTOMREQUEST, $sMethod);
  252. curl_setopt($r, CURLOPT_POSTFIELDS, $sDataJson);
  253. $aHeaders = array_merge($aHeaders, array(
  254. 'Content-Type: application/json',
  255. 'Content-Length: ' . strlen($sDataJson)
  256. ));
  257. } elseif ('DELETE' == $sMethod) {
  258. curl_setopt($r, CURLOPT_CUSTOMREQUEST, "DELETE");
  259. }
  260. // $aHeaders[] = 'X-Trigger-Error: APIKeyMissing'; // test error handling
  261. curl_setopt($r, CURLOPT_HTTPHEADER, $aHeaders);
  262. $sResult = curl_exec($r);
  263. if (false === $sResult) {
  264. $sError = curl_error ($r);
  265. curl_close($r);
  266. return false;
  267. }
  268. curl_close($r);
  269. return $sResult;
  270. }
  271. }
  272. /** @} */