BxMailchimpModule.php 10 KB

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