AntonLV 8 лет назад
Родитель
Сommit
06b2c0e45d

+ 1 - 1
modules/boonex/intercom/install/config.php

@@ -17,7 +17,7 @@ $aConfig = array(
     'name' => 'bx_intercom',
     'title' => 'Intercom',
     'note' => 'Intercom integration.',
-    'version' => '9.0.6.DEV',
+    'version' => '9.0.7.DEV',
     'vendor' => 'BoonEx',
 	'help_url' => 'http://feed.una.io/?section={module_name}',
 

+ 51 - 0
modules/boonex/intercom/updates/9.0.5_9.0.6/install/config.php

@@ -0,0 +1,51 @@
+<?php
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ */
+
+$aConfig = array(
+    /**
+     * Main Section.
+     */
+    'title' => 'Intercom',
+    'version_from' => '9.0.5',
+	'version_to' => '9.0.6',
+    'vendor' => 'BoonEx',
+
+    'compatible_with' => array(
+        '9.0.0-RC10'
+    ),
+
+    /**
+     * 'home_dir' and 'home_uri' - should be unique. Don't use spaces in 'home_uri' and the other special chars.
+     */
+    'home_dir' => 'boonex/intercom/updates/update_9.0.5_9.0.6/',
+	'home_uri' => 'intercom_update_905_906',
+
+	'module_dir' => 'boonex/intercom/',
+	'module_uri' => 'intercom',
+
+    'db_prefix' => 'bx_intercom_',
+    'class_prefix' => 'BxIntercom',
+
+    /**
+     * Installation/Uninstallation Section.
+     */
+    'install' => array(
+		'execute_sql' => 0,
+        'update_files' => 1,
+        'update_languages' => 0,
+		'clear_db_cache' => 0,
+    ),
+
+	/**
+     * Category for language keys.
+     */
+    'language_category' => 'Intercom',
+
+	/**
+     * Files Section
+     */
+    'delete_files' => array(),
+);

+ 13 - 0
modules/boonex/intercom/updates/9.0.5_9.0.6/install/installer.php

@@ -0,0 +1,13 @@
+<?php
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ */
+
+class BxIntercomUpdater extends BxDolStudioUpdater
+{
+    function __construct($aConfig)
+	{
+        parent::__construct($aConfig);
+    }
+}

+ 353 - 0
modules/boonex/intercom/updates/9.0.5_9.0.6/source/classes/BxIntercomModule.php

@@ -0,0 +1,353 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    Intercom Intercom integration module
+ * @ingroup     UnaModules
+ *
+ * @{
+ */
+
+class BxIntercomModule extends BxDolModule
+{
+    protected $sHostnameAPI = 'https://api.intercom.io';
+
+    function __construct(&$aModule)
+    {
+        parent::__construct($aModule);
+    }
+
+    public function serviceIntegrationCode ()
+    {
+        $sAppId = getParam('bx_intercom_option_app_id');
+        if (!$sAppId)
+            return '';
+
+        $aSettings = array(
+            'app_id' => $sAppId,
+            'hide_default_launcher' => ('on' == getParam('bx_intercom_option_hide_launcher') || 'messenger' == bx_get('i')),
+            'alignment' => getParam('bx_intercom_option_alignment'),
+            'horizontal_padding' => getParam('bx_intercom_option_horizontal_padding'),
+            'vertical_padding' => getParam('bx_intercom_option_vertical_padding'),
+        );
+
+        $oProfile = null;
+        if (isLogged() && ($oProfile = BxDolProfile::getInstance()) && ($oAccountObject = $oProfile->getAccountObject())) {
+            $aInfoProfile = $oProfile->getInfo();
+            $aInfoAccount = $oAccountObject->getInfo();
+            $aProfilesIds = $oAccountObject->getProfilesIds();
+
+            // membership
+            $aMembership = BxDolAcl::getInstance()->getMemberMembershipInfo($oProfile->id());
+
+            // profiles
+            $sProfiles = '';
+            foreach ($aProfilesIds as $iId) {
+                if (!($o = BxDolProfile::getInstance($iId)))
+                    continue;
+                $sProfiles .= $o->getDisplayName() . ', ';
+            }
+            $sProfiles = trim($sProfiles, ', ');
+
+            $aSettings['user_id'] = $oAccountObject->id();
+            $aSettings['name'] = $oProfile->getDisplayName();
+            $aSettings['email'] = $oAccountObject->getEmail();
+            $aSettings['created_at'] = $aInfoAccount['added'];
+
+            $aSettings['link'] = $oProfile->getUrl();
+            $aSettings['email_confirmed'] = $aInfoAccount['email_confirmed'] ? 'yes' : 'no';
+            $aSettings['email_receive_news'] = $aInfoAccount['receive_news'] ? 'yes' : 'no';
+            $aSettings['email_receive_updates'] = $aInfoAccount['receive_updates'] ? 'yes' : 'no';
+
+            $aSettings['membership'] = _t($aMembership['name']) . (isset($aMembership['date_expires']) && $aMembership['date_expires'] ? ' (expires:' . bx_time_utc($aMembership['date_expires']) . ')' : '');
+            $aSettings['status'] = $aInfoProfile['status'];
+            $aSettings['type'] = $aInfoProfile['type'];
+
+            $aSettings['all profiles'] = $sProfiles;
+        }
+
+        bx_alert('bx_intercom', 'integration', $oProfile ? $oProfile->id() : 0, $oProfile ? $oProfile->id() : 0, array('settings' => &$aSettings));
+        
+        $sSettings = json_encode($aSettings);
+
+        return <<<EOS
+
+        <!-- BEGIN Intercom integration -->
+        <script>
+            window.intercomSettings = $sSettings;
+        </script>
+        <script>(function(){var w=window;var ic=w.Intercom;if(typeof ic==="function"){ic('reattach_activator');ic('update',intercomSettings);}else{var d=document;var i=function(){i.c(arguments)};i.q=[];i.c=function(args){i.q.push(args)};w.Intercom=i;function l(){var s=d.createElement('script');s.type='text/javascript';s.async=true;s.src='https://widget.intercom.io/widget/{$sAppId}';var x=d.getElementsByTagName('script')[0];x.parentNode.insertBefore(s,x);}if(w.attachEvent){w.attachEvent('onload',l);}else{w.addEventListener('load',l,false);}}})()</script>
+        <!-- END Intercom integration -->
+
+EOS;
+    }
+
+    /**
+     * remove user from intercom by email or account id
+     */      
+    public function serviceRemoveAccount($mixed)
+    {
+        if (!$mixed)
+            return false;
+
+        if ($mixed && !(int)$mixed) {
+            $sId = BxDolAccount::getID($mixed);
+        } else {
+            $sId = (int)$mixed;
+        }
+        
+        $s = $this->_request('/users?user_id=' . $sId, 'DELETE', array(), $sErrorCurl);
+        if ($sErrorJson = $this->_checkForErrors($s, $sErrorCurl))
+            return false;
+    
+        return true;
+    }
+
+    /**
+     * update user by email or account id
+     */      
+    public function serviceUpdateAccount($sEmail)
+    {
+        if (!$sEmail || !($oAccount = BxDolAccount::getInstance($sEmail)))
+            return false;
+
+        $aUserInfo = $this->_prepareUser($oAccount);
+
+        $s = $this->_request('/users', 'POST', $aUserInfo, $sErrorCurl);
+        if ($sErrorJson = $this->_checkForErrors($s, $sErrorCurl))
+            return false;
+
+        return true;
+    }
+    
+    /**
+     * remove user from intercom by email or account id
+     */      
+    public function actionAccountRemove($sEmail)
+    {
+        if (!isAdmin())
+            die('{}');
+
+        if (!$this->serviceRemoveAccount($sEmail))
+            die(json_encode(array('error' => true, 'message' => "Delete failed")));
+
+        echo json_encode(array('message' => "User was successfully deleted"));
+    }
+
+    /**
+     * update user by email or account id
+     */  
+    public function actionAccountUpdate($sEmail)
+    {
+        if (!isAdmin())
+            die('{}');
+
+        if (!$this->serviceUpdateAccount($sEmail))
+            die(json_encode(array('error' => true, 'message' => "Update failed")));
+
+        echo json_encode(array('message' => "User was successfully updated"));
+    }
+
+    /**
+     * Add members in bulk from local database to Intercom
+     */
+    public function actionBulkAdd($iLimit = 5000)
+    {
+        if (!isAdmin())
+            die('{}');
+
+        $this->serviceBulkAdd($iLimit);
+    }
+
+    public function serviceBulkAdd($iLimit = 5000)
+    {
+        set_time_limit(3600);
+        $aIds = $this->_oDb->getInitialUsers($iLimit);
+        $aChunks = array_chunk($aIds, 10);
+        $iTotal = count($aIds);
+        $iCounter = 0;
+        foreach ($aChunks as $aIds) {
+            $aUsers = array();
+            foreach ($aIds as $iAccountId) {
+                if (!($oAccount = BxDolAccount::getInstance($iAccountId)))
+                    continue;
+                if (!($aUserInfo = $this->_prepareUser($oAccount)))
+                    continue;
+                $aUsers[] = $aUserInfo;
+            }
+            $s = $this->_request('/users/bulk', 'POST', array('users' => $aUsers), $sErrorCurl);
+            if (trim($s) && $sErrorJson = $this->_checkForErrors($s, $sErrorCurl))
+                die($sErrorJson);
+            $iCounter += count($aUsers);
+            header('X-Users-Processed: ' . $iCounter . '/' . $iTotal);
+            flush();
+            sleep(1);
+        }
+        echo json_encode(array('message' => "{$iCounter} users were successfully added"));
+    }
+
+    /**
+     * Test query to return number of users registered in intercom
+     */     
+    public function actionNum()
+    {
+        if (!isAdmin())
+            die('{}');
+
+        $s = $this->_request('/users', 'GET', array(), $sErrorCurl);
+        if ($sErrorJson = $this->_checkForErrors($s, $sErrorCurl))
+            die($sErrorJson);
+
+        $a = json_decode($s);
+        
+        echo json_encode(array(
+            'type' => $a->type,
+            'total_count' => $a->total_count,
+        ));
+    }
+
+    protected function _prepareUser($oAccount)
+    {
+        $oProfile = BxDolProfile::getInstanceByAccount($oAccount->id());
+        if (!$oProfile)
+            return false;
+
+        $aInfoProfile = $oProfile->getInfo();
+        $aInfoAccount = $oAccount->getInfo();
+        $aInfoSession = $this->_oDb->getSessionRowByAccountId($oAccount->id());
+        $aProfilesIds = $oAccount->getProfilesIds();
+
+        // membership
+        $aMembership = BxDolAcl::getInstance()->getMemberMembershipInfo($oProfile->id());
+
+        // profiles
+        $sProfiles = '';
+        foreach ($aProfilesIds as $iId) {
+            if (!($o = BxDolProfile::getInstance($iId)))
+                continue;
+            $sProfiles .= $o->getDisplayName() . ', ';
+        }
+        $sProfiles = trim($sProfiles, ', ');
+
+        $aSettings = array (
+            'user_id' => $oAccount->id(),
+            'name' => $oProfile->getDisplayName(),
+            'email' => $oAccount->getEmail(),
+            'unsubscribed_from_emails' => !$aInfoAccount['receive_news'],
+            'signed_up_at' => $aInfoAccount['added'],
+            //'last_seen_ip' => ?,
+            'last_request_at' => isset($aInfoSession['date']) ? $aInfoSession['date'] : $aInfoAccount['logged'],
+            'custom_attributes' => array (
+                'link' => $oProfile->getUrl(),
+
+                'email_confirmed' => $aInfoAccount['email_confirmed'] ? 'yes' : 'no',
+                'email_receive_news' => $aInfoAccount['receive_news'] ? 'yes' : 'no',
+                'email_receive_updates' => $aInfoAccount['receive_updates'] ? 'yes' : 'no',
+
+                'membership' => _t($aMembership['name']) . (isset($aMembership['date_expires']) && $aMembership['date_expires'] ? ' (expires:' . bx_time_utc($aMembership['date_expires']) . ')' : ''),
+                'status' => $aInfoProfile['status'],
+                'type' => $aInfoProfile['type'],
+
+                'all profiles' => $sProfiles,
+            ),
+        );
+
+        bx_alert('bx_intercom', 'user_info', $oProfile->id(), $oProfile->id(), array('settings' => &$aSettings));
+
+        return $aSettings;
+    }
+
+    /**
+     * @return json error object on error or empty string on success
+     */
+    protected function _checkForErrors($s, $sErrorCurl, $sReturnType = 'json')
+    {
+        if (!$s)
+            return $this->_returnErrors(array('error' => 'curl error: ' . $sErrorCurl), $sReturnType);
+
+        $a = json_decode($s);
+        if (null === $a)
+            return $this->_returnErrors(array('error' => 'json decode failed:' . htmlspecialchars($s)), $sReturnType);
+
+        if (0 === strncmp('error', $a->type, 5)) {
+            $sError = '';
+            foreach ($a->errors as $o)
+                $sError .= $o->code . ": " . $o->message . "\n";
+            return $this->_returnErrors(array('error' => $sError), $sReturnType);
+        }
+
+        return '';
+    }
+
+    protected function _returnErrors($mixed, $sReturnType = 'json')
+    {
+        switch ($sReturnType) {
+            case 'array':
+                return $mixed;
+            case 'json':
+            default:
+                return json_encode($mixed);
+        }
+    }
+
+    /**
+     * Perform JSON request to the API endpoint and get JSON response
+     * @return false on error
+     */
+    protected function _request($sURI, $sMethod = 'GET', $aData = array(), &$sError)
+    {
+        $sAppId = getParam('bx_intercom_option_app_id');
+        $sApiKey = getParam('bx_intercom_option_api_key');
+        if (!$sAppId || !$sApiKey) {
+            $sError = "No App ID or API Key";
+            return false;
+        }
+        
+        $r = curl_init();
+    
+        $aHeaders = array(
+            'Accept: application/json',
+        );
+
+        curl_setopt($r, CURLOPT_USERPWD, $sApiKey . ":");
+        curl_setopt($r, CURLOPT_TIMEOUT, 10);
+        curl_setopt($r, CURLOPT_URL, $this->sHostnameAPI . $sURI);
+        curl_setopt($r, CURLOPT_HEADER, 0);
+        curl_setopt($r, CURLOPT_RETURNTRANSFER, 1);
+        curl_setopt($r, CURLOPT_FOLLOWLOCATION, 1);
+
+        if ('POST' == $sMethod) {
+
+            $sDataJson = json_encode($aData); 
+
+            curl_setopt($r, CURLOPT_POST, true);
+            curl_setopt($r, CURLOPT_POSTFIELDS, $sDataJson);
+
+            $aHeaders = array_merge($aHeaders, array(
+                'Content-Type: application/json',
+                'Content-Length: ' . strlen($sDataJson)
+            )); 
+
+        } elseif ('DELETE' == $sMethod) {
+
+            curl_setopt($r, CURLOPT_CUSTOMREQUEST, "DELETE");
+
+        }
+
+        curl_setopt($r, CURLOPT_HTTPHEADER, $aHeaders); 
+
+        $sResult = curl_exec($r);        
+
+        if (false === $sResult) {
+            $sError = curl_error ($r);
+            curl_close($r);
+            return false;
+        }
+
+        curl_close($r);
+        return $sResult;
+    }
+}
+
+/** @} */

+ 68 - 0
modules/boonex/intercom/updates/9.0.5_9.0.6/source/install/config.php

@@ -0,0 +1,68 @@
+<?php
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    Intercom Intercom integration module
+ * @ingroup     UnaModules
+ *
+ * @{
+ */
+
+$aConfig = array(
+    /**
+     * Main Section.
+     */
+    'type' => BX_DOL_MODULE_TYPE_MODULE,
+    'name' => 'bx_intercom',
+    'title' => 'Intercom',
+    'note' => 'Intercom integration.',
+    'version' => '9.0.6',
+    'vendor' => 'BoonEx',
+	'help_url' => 'http://feed.una.io/?section={module_name}',
+
+    'compatible_with' => array(
+        '9.0.0-RC10'
+    ),
+
+    /**
+     * 'home_dir' and 'home_uri' - should be unique. Don't use spaces in 'home_uri' and the other special chars.
+     */
+    'home_dir' => 'boonex/intercom/',
+    'home_uri' => 'intercom',
+
+    'db_prefix' => 'bx_intercom_',
+    'class_prefix' => 'BxIntercom',
+
+    /**
+     * Category for language keys.
+     */
+    'language_category' => 'Intercom',
+
+    /**
+     * Installation/Uninstallation Section.
+     */
+    'install' => array(
+        'execute_sql' => 1,
+        'update_languages' => 1,
+    ),
+    'uninstall' => array (
+        'execute_sql' => 1,
+        'update_languages' => 1,
+    ),
+    'enable' => array(
+        'execute_sql' => 1,
+        'clear_db_cache' => 1,
+    ),
+    'disable' => array(
+        'execute_sql' => 1,
+        'clear_db_cache' => 1,
+    ),
+
+    /**
+     * Dependencies Section
+     */
+    'dependencies' => array(),
+);
+
+/** @} */

+ 1 - 1
modules/boonex/massmailer/install/config.php

@@ -18,7 +18,7 @@ $aConfig = array(
     'name' => 'bx_massmailer',
     'title' => 'Mass mailer',
     'note' => 'Mass mailer module.',
-    'version' => '9.0.1.DEV',
+    'version' => '9.0.3.DEV',
     'vendor' => 'BoonEx',
     'help_url' => 'http://feed.una.io/?section={module_name}',
 

+ 51 - 0
modules/boonex/massmailer/updates/9.0.1_9.0.2/install/config.php

@@ -0,0 +1,51 @@
+<?php
+/**
+ * Copyright (c) BoonEx Pty Limited - http://www.boonex.com/
+ * CC-BY License - http://creativecommons.org/licenses/by/3.0/
+ */
+
+$aConfig = array(
+    /**
+     * Main Section.
+     */
+    'title' => 'Mass mailer',
+    'version_from' => '9.0.1',
+	'version_to' => '9.0.2',
+    'vendor' => 'BoonEx',
+
+    'compatible_with' => array(
+        '9.0.0-RC10'
+    ),
+
+    /**
+     * 'home_dir' and 'home_uri' - should be unique. Don't use spaces in 'home_uri' and the other special chars.
+     */
+    'home_dir' => 'boonex/massmailer/updates/update_9.0.1_9.0.2/',
+	'home_uri' => 'massmailer_update_901_902',
+
+	'module_dir' => 'boonex/massmailer/',
+	'module_uri' => 'massmailer',
+
+    'db_prefix' => 'bx_massmailer_',
+    'class_prefix' => 'BxMassMailer',
+
+    /**
+     * Installation/Uninstallation Section.
+     */
+    'install' => array(
+		'execute_sql' => 0,
+        'update_files' => 1,
+        'update_languages' => 0,
+		'clear_db_cache' => 0,
+    ),
+
+	/**
+     * Category for language keys.
+     */
+    'language_category' => 'MassMailer',
+
+	/**
+     * Files Section
+     */
+    'delete_files' => array(),
+);

+ 13 - 0
modules/boonex/massmailer/updates/9.0.1_9.0.2/install/installer.php

@@ -0,0 +1,13 @@
+<?php
+/**
+ * Copyright (c) BoonEx Pty Limited - http://www.boonex.com/
+ * CC-BY License - http://creativecommons.org/licenses/by/3.0/
+ */
+
+class BxMassMailerUpdater extends BxDolStudioUpdater
+{
+    function __construct($aConfig)
+	{
+        parent::__construct($aConfig);
+    }
+}

+ 232 - 0
modules/boonex/massmailer/updates/9.0.1_9.0.2/source/classes/BxMassMailerDb.php

@@ -0,0 +1,232 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    MassMailer Mass Mailer
+ * @ingroup     UnaModules
+ *
+ * @{
+ */
+
+class BxMassMailerDb extends BxBaseModGeneralDb
+{
+    function __construct(&$oConfig)
+    {
+        parent::__construct($oConfig);
+    }
+    
+    public function copyCampaign($iCampaignId)
+    {
+        $CNF = &$this->_oConfig->CNF;
+        $aBindings = array(
+            'campaign_id' => $iCampaignId,
+            'date_created' => time(),
+            'copy_title' => _t('_bx_massmailer_txt_copy_title')
+        );
+        $this->query("INSERT INTO `" . $CNF['TABLE_CAMPAIGNS'] . "` (`" . $CNF['FIELD_AUTHOR'] . "`, `" . $CNF['FIELD_TITLE'] . "`, `" . $CNF['FIELD_BODY'] . "`, `" . $CNF['FIELD_SEGMENTS'] . "`, `" . $CNF['FIELD_ADDED'] . "`, `" . $CNF['FIELD_SUBJECT'] . "`, `" . $CNF['FIELD_FROM_NAME'] . "`, `" . $CNF['FIELD_REPLY_TO'] . "`) SELECT `" . $CNF['FIELD_AUTHOR'] . "`, CONCAT(`" . $CNF['FIELD_TITLE'] . "`, :copy_title), `" . $CNF['FIELD_BODY'] . "`, `" . $CNF['FIELD_SEGMENTS'] . "`, :date_created, `" . $CNF['FIELD_SUBJECT'] . "`, `" . $CNF['FIELD_FROM_NAME'] . "`, `" . $CNF['FIELD_REPLY_TO'] . "` FROM `" . $CNF['TABLE_CAMPAIGNS'] . "` WHERE `" . $CNF['FIELD_ID'] . "` = :campaign_id", $aBindings);
+        return $this->lastId();
+    }
+        
+    public function sendCampaign($iCampaignId, $sEmailList)
+    {
+        $CNF = &$this->_oConfig->CNF;
+        $aBindings = array(
+            'campaign_id' => $iCampaignId,
+            'email_list' => $sEmailList,
+            'date_sent' => time()
+        );
+        $this->query("UPDATE `" . $CNF['TABLE_CAMPAIGNS'] . "` SET `" . $CNF['FIELD_EMAIL_LIST'] . "` = :email_list, `" . $CNF['FIELD_DATE_SENT'] . "` = :date_sent WHERE `" . $CNF['FIELD_ID'] . "` = :campaign_id", $aBindings);
+    }
+    
+    public function getCampaignInfoById ($iCampaignId)
+    {
+        $CNF = &$this->_oConfig->CNF;
+        $sQuery = $this->prepare ("SELECT * FROM `" . $CNF['TABLE_CAMPAIGNS'] . "` WHERE `" . $CNF['FIELD_ID'] . "` = ?", $iCampaignId);
+        return $this->getRow($sQuery);
+    }
+    
+    public function getSegmentInfoById ($iSegmentId)
+    {
+        $CNF = &$this->_oConfig->CNF;
+        $sQuery = $this->prepare ("SELECT * FROM `" . $CNF['TABLE_SEGMENTS'] . "` WHERE `" . $CNF['FIELD_ID'] . "` = ?", $iSegmentId);
+        return $this->getRow($sQuery);
+    }
+    
+    public function getSegments()
+    {
+        $CNF = &$this->_oConfig->CNF;
+        $sQuery = $this->prepare ("SELECT `" . $CNF['FIELD_ID'] . "`, `" . $CNF['FIELD_TITLE'] . "` FROM `" . $CNF['TABLE_SEGMENTS'] . "`");
+        return $this->getPairs($sQuery, $CNF['FIELD_ID'], $CNF['FIELD_TITLE']);
+    }
+    
+    public function getAccountsByTerms($sTerms = "")
+    {
+        $sSql = "SELECT `ta`.`email`, `tp`.`id` AS `profile_id` FROM `sys_accounts` AS `ta` INNER JOIN `sys_profiles` AS `tp` ON `tp`.`account_id`=`ta`.`id` " . $sTerms . "  WHERE `ta`.`receive_news` <> 0";
+        return $this->getAll($sSql);
+    }
+    
+    public function getAccountsByTermsStat($iDateFrom, $iDateTo, $sTerms = "")
+    {
+        $aBindings = array(
+           'datefrom' => $iDateFrom,
+           'dateto' => $iDateTo
+       );
+        $sSql = "SELECT DATE(FROM_UNIXTIME(`ta`.`added`)) AS `period`, YEAR(FROM_UNIXTIME(`ta`.`added`)) AS `year`, COUNT(*) AS `count` FROM `sys_accounts` AS `ta` INNER JOIN `sys_profiles` AS `tp` ON `tp`.`account_id`=`ta`.`id` " . $sTerms . " WHERE `ta`.`receive_news` <> 0 AND `ta`.`added` >= :datefrom AND `ta`.`added` <= :dateto GROUP BY `period`, `year` ORDER BY `year`, `period` ASC";
+        return $this->getAll($sSql, $aBindings);
+    }
+    
+    public function getAccountsByTermsStatUnsubscribe($iDateFrom, $iDateTo, $sTerms = "")
+    {
+        $CNF = &$this->_oConfig->CNF;
+        $aBindings = array(
+           'datefrom' => $iDateFrom,
+           'dateto' => $iDateTo
+       );
+        $sSql = "SELECT DATE(FROM_UNIXTIME(`tu`.`unsubscribed`)) AS `period`, YEAR(FROM_UNIXTIME(`tu`.`unsubscribed`)) AS `year`, COUNT(*) AS `count` FROM `sys_accounts` AS `ta` INNER JOIN `" . $CNF['TABLE_UNSUBSCRIBE'] . "` AS `tu` ON `tu`.`account_id`=`ta`.`id` INNER JOIN `sys_profiles` AS `tp` ON `tp`.`account_id`=`ta`.`id` " . $sTerms . " WHERE `tu`.`unsubscribed` >= :datefrom AND `tu`.`unsubscribed` <= :dateto GROUP BY `period`, `year` ORDER BY `year`, `period` ASC";
+        return $this->getAll($sSql, $aBindings);
+    }
+    
+    public function getAccountsByTermsStatInitValue($iDateFrom, $sTerms)
+    {
+        $aBindings = array(
+            'datefrom' => $iDateFrom
+        );
+        $sQuery = "SELECT COUNT(`ta`.`id`) AS `count` FROM `sys_accounts` AS `ta` INNER JOIN `sys_profiles` AS `tp` ON `tp`.`account_id`=`ta`.`id` " . $sTerms . " WHERE `ta`.`added` < :datefrom ";
+        return $this->getOne($sQuery, $aBindings);
+    }
+    
+    public function getAccountsByTermsStatInitValueUnsubscribe($iDateFrom, $sTerms)
+    {
+        $CNF = &$this->_oConfig->CNF;
+        $aBindings = array(
+            'datefrom' => $iDateFrom
+        );
+        $sQuery = "SELECT COUNT(`tu`.`id`) AS `count` FROM `sys_accounts` AS `ta` INNER JOIN `" . $CNF['TABLE_UNSUBSCRIBE'] . "` AS `tu` ON `tu`.`account_id`=`ta`.`id` INNER JOIN `sys_profiles` AS `tp` ON `tp`.`account_id`=`ta`.`id` " . $sTerms . " WHERE `tu`.`unsubscribed` < :datefrom ";
+        return $this->getOne($sQuery, $aBindings);
+    }
+    
+    public function addLetter($iCampaignId, $sEmail)
+    {
+        $iTime = time();
+        $sCode = md5($iCampaignId . $sEmail . $iTime);
+        $CNF = &$this->_oConfig->CNF;
+        $aBindings = array(
+            'campaign_id' => $iCampaignId,
+            'email' => $sEmail,
+            'date_sent' => $iTime,
+            'code' => $sCode,
+        );
+        $this->query("INSERT INTO `" . $CNF['TABLE_LETTERS'] . "` (`" . $CNF['FIELD_CAMPAIGN_ID'] . "`, `" . $CNF['FIELD_EMAIL'] . "`, `" . $CNF['FIELD_DATE_SENT'] . "`, `" . $CNF['FIELD_HASH'] . "`) VALUES (:campaign_id, :email, :date_sent, :code)", $aBindings);
+        return $sCode;
+    }
+    
+    public function updateDateSeenForLetter($sCode)
+    {
+        $CNF = &$this->_oConfig->CNF;
+        $aBindings = array(
+            'code' => $sCode,
+            'date_seen' => time()
+        );
+        $this->query("UPDATE `" . $CNF['TABLE_LETTERS'] . "` SET `" . $CNF['FIELD_DATE_SEEN'] . "` = :date_seen WHERE `" . $CNF['FIELD_HASH'] . "` = :code", $aBindings);
+    }
+    
+    public function deleteCampaignData($iCampaignId)
+    {
+        $CNF = &$this->_oConfig->CNF;
+        $aBindings = array(
+            'campaign_id' => $iCampaignId
+        );
+        $this->query("DELETE FROM `" . $CNF['TABLE_LETTERS'] . "` WHERE `" . $CNF['FIELD_CAMPAIGN_ID'] . "` = :campaign_id", $aBindings);
+        $this->query("DELETE FROM `" . $CNF['TABLE_LINKS'] . "` WHERE `" . $CNF['FIELD_CAMPAIGN_ID'] . "` = :campaign_id", $aBindings);
+    }
+    
+    public function getLettersByCampaignId ($iCampaignId)
+    {
+        $CNF = &$this->_oConfig->CNF;
+        $sQuery = $this->prepare ("SELECT * FROM `" . $CNF['TABLE_LETTERS'] . "` WHERE `" . $CNF['FIELD_CAMPAIGN_ID'] . "` = ?", $iCampaignId);
+        return $this->getAll($sQuery);
+    }
+    
+    public function getClicksByCampaignId ($iCampaignId)
+    {
+        $CNF = &$this->_oConfig->CNF;
+        $sQuery = $this->prepare ("SELECT `" . $CNF['FIELD_LINK'] . "`, `" . $CNF['FIELD_TITLE'] . "`, COUNT( `" . $CNF['FIELD_ID'] . "`) AS `click_count`, MAX( `" . $CNF['FIELD_DATE_CLICK'] . "`) AS `last_click` FROM `" . $CNF['TABLE_LINKS'] . "` WHERE `" . $CNF['FIELD_CAMPAIGN_ID'] . "` = ? AND `" . $CNF['FIELD_DATE_CLICK'] . "` IS NOT NULL GROUP BY `" . $CNF['FIELD_LINK'] . "`, `" . $CNF['FIELD_TITLE'] . "` ORDER BY COUNT( `" . $CNF['FIELD_ID'] . "`)  DESC", $iCampaignId);
+        return $this->getAll($sQuery);
+    }
+    
+    public function getStatByCampaignId ($iCampaignId)
+    {
+        $CNF = &$this->_oConfig->CNF;
+         $aBindings = array(
+            'campaign_id' => $iCampaignId
+        );
+        $q = "
+        SELECT COUNT(*) AS `count`, 'total' AS `title` FROM `" . $CNF['TABLE_LETTERS'] . "` WHERE `" . $CNF['FIELD_CAMPAIGN_ID'] . "` = :campaign_id UNION
+        SELECT COUNT(*), 'seen' FROM `" . $CNF['TABLE_LETTERS'] . "` WHERE `" . $CNF['FIELD_CAMPAIGN_ID'] . "` = :campaign_id AND `" . $CNF['FIELD_DATE_SEEN'] . "` > 0 UNION
+        SELECT COUNT(*), 'clicked' FROM `" . $CNF['TABLE_LETTERS'] . "` WHERE `" . $CNF['FIELD_CAMPAIGN_ID'] . "` = :campaign_id AND `" . $CNF['FIELD_DATE_CLICK'] . "` > 0 UNION 
+        SELECT COUNT(*), 'unsubscribed' FROM `" . $CNF['TABLE_UNSUBSCRIBE'] . "` WHERE `" . $CNF['FIELD_CAMPAIGN_ID'] . "` = :campaign_id 
+        ";
+        return $this->getPairs($q, 'title', 'count', $aBindings);
+    }
+    
+    public function getLetterByCode ($sCode)
+    {
+        $CNF = &$this->_oConfig->CNF;
+        $sQuery = $this->prepare ("SELECT * FROM `" . $CNF['TABLE_LETTERS'] . "` WHERE `" . $CNF['FIELD_HASH'] . "` = ?", $sCode);
+        return $this->getRow($sQuery);
+    }
+    
+    public function addLink($sLetterHash, $sLink, $sTitle, $iCampaignId)
+    {
+        $CNF = &$this->_oConfig->CNF;
+        $sCode = md5($sLetterHash . $sLink . $sTitle);
+        $aBindings = array(
+            'letter_hash' => $sLetterHash,
+            'hash' => $sCode,
+            'link' => $sLink,
+            'title' => $sTitle,
+            'campaign_id' => $iCampaignId,
+        );
+        $this->query("INSERT INTO `" . $CNF['TABLE_LINKS'] . "` (`" . $CNF['FIELD_LETTER_HASH'] . "`, `" . $CNF['FIELD_HASH'] . "`, `" . $CNF['FIELD_LINK'] . "`, `" . $CNF['FIELD_TITLE'] . "`, `" . $CNF['FIELD_CAMPAIGN_ID'] . "`) VALUES (:letter_hash, :hash, :link, :title, :campaign_id)", $aBindings);
+        return $sCode;
+    }
+    
+    public function updateDateClickForLink($sCode)
+    {
+        $CNF = &$this->_oConfig->CNF;
+        $aBindings = array(
+            'code' => $sCode,
+            'date_click' => time()
+        );
+        $this->query("UPDATE `" . $CNF['TABLE_LINKS'] . "` SET `" . $CNF['FIELD_DATE_CLICK'] . "` = :date_click WHERE `" . $CNF['FIELD_HASH'] . "` = :code", $aBindings);
+        $aData = $this->getRow("SELECT * FROM `" . $CNF['TABLE_LINKS'] . "` WHERE `" . $CNF['FIELD_HASH'] . "` = :code", array('code' => $sCode));
+        $aBindings['code'] = $aData[$CNF['FIELD_LETTER_HASH']];
+        if ($aData[$CNF['FIELD_LETTER_HASH']] != '')
+            $this->query("UPDATE `" . $CNF['TABLE_LETTERS'] . "` SET `" . $CNF['FIELD_DATE_CLICK'] . "` = :date_click WHERE `" . $CNF['FIELD_HASH'] . "` = :code", $aBindings);
+        
+        return $aData[$CNF['FIELD_LINK']];
+    }
+    
+    
+    public function updateUnsubscribe($iAccountId, $iValue, $iCampaignId)
+    {
+        $CNF = &$this->_oConfig->CNF;
+        if ($iValue != 1){
+            $aBindings = array(
+                'account_id' => $iAccountId,
+                'campaign_id' => $iCampaignId,
+                'date_unsubscribed' => time()
+            );
+            $this->query("INSERT INTO `" . $CNF['TABLE_UNSUBSCRIBE'] . "` (`" . $CNF['FIELD_DATE_UNSUBSCRIBED'] . "`, `" . $CNF['FIELD_ACCOUNT_ID'] . "`, `" . $CNF['FIELD_CAMPAIGN_ID'] . "`) VALUES (:date_unsubscribed, :account_id, :campaign_id)", $aBindings);
+        }
+        else{
+            $aBindings = array(
+                'account_id' => $iAccountId
+            );
+            $this->query("DELETE FROM `" . $CNF['TABLE_UNSUBSCRIBE'] . "` WHERE `" . $CNF['FIELD_ACCOUNT_ID'] . "` = :account_id", $aBindings);
+        }
+    }
+}
+
+/** @} */

+ 621 - 0
modules/boonex/massmailer/updates/9.0.1_9.0.2/source/classes/BxMassMailerModule.php

@@ -0,0 +1,621 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+* Copyright (c) UNA, Inc - https://una.io
+* MIT License - https://opensource.org/licenses/MIT
+*
+* @defgroup    MassMailer Mass Mailer
+* @ingroup     UnaModules
+*
+* @{
+*/
+
+class BxMassMailerModule extends BxBaseModGeneralModule
+{
+    private $aSegments = array();
+          
+    function __construct(&$aModule)
+    {
+        parent::__construct($aModule);
+        $CNF = &$this->_oConfig->CNF;
+        $aRv = array('lvl:0' => _t('_bx_massmailer_segments_all'));
+        $oAccountQuery = BxDolAclQuery::getInstance();
+              
+        $aTmp = array();
+        $oAccountQuery->getLevels(array('type' => 'all_active_pair'), $aTmp);
+        foreach ($aTmp as $sKey => $sTmp){
+            if ($sKey != MEMBERSHIP_ID_NON_MEMBER && $sKey != MEMBERSHIP_ID_ACCOUNT)
+                $aRv['lvl:' . $sKey] = _t($sTmp);
+        };
+              
+        $aTmp = $this->_oDb->getSegments();
+        foreach ($aTmp as $sKey => $sTmp){
+            $aRv['cst:' . $sKey] = _t($sTmp);
+        };
+        $this->aSegments = $aRv;
+    }
+          
+    /**
+    * ACTION METHODS
+    */
+    public function actionTrack($sActionName, $sHash)
+    {
+        if (isset($sHash) && trim($sHash) != ""){
+            switch ($sActionName) {
+                case 'click':
+                    $sLink = $this->_oDb->updateDateClickForLink($sHash);
+                    if (substr_count($sLink, '://') == 0)
+                        $sLink = BX_DOL_URL_ROOT . $sLink;
+                    header('Location: ' . $sLink);
+                    break;
+                    
+                case 'seen':
+                    header('Content-Type: image/png');
+                    $this->_oDb->updateDateSeenForLetter($sHash);
+                    break;
+            }
+        }
+    }
+    
+    public function actionGetReportsData($sReportName, $sReportType, $sDateFrom, $sDateTo, $sSegment)
+    {
+        if(!BxDolAcl::getInstance()->isMemberLevelInSet(128))
+            return '';
+        
+        header('Content-Type: application/json');
+        
+        $aColors = array('#3366CC', '#DC3912', '#FF9900', '#109618', '#990099');
+        $iDateFrom = strtotime($sDateFrom);
+        $iDateTo = strtotime($sDateTo);
+        $sType = "bar";
+        $bIsTimeX = false;
+        $iMinValueY = 0;
+        $iMaxValueY = 0;
+        $aValues = array('labels' => array(), 'values' => array(array('legend' => '', 'data' => array())), 'links' => array(), 'strings' => array(0 => _t('_bx_massmailer_txt_item'), 1 => _t('_bx_massmailer_txt_value')));
+        
+        switch ($sReportName) {
+            case "SUBSCRIBERS_INFO":
+                $sType = "line";
+                $bIsTimeX = true;
+                $aValues['strings'][0] = _t('_bx_massmailer_txt_date');
+                $aValues['strings'][1] = '';
+                $sSql = $this->getSqlBySegment($sSegment);
+                $aData = $this->_oDb->getAccountsByTermsStat($iDateFrom, $iDateTo, $sSql);
+                $aData2 = $this->_oDb->getAccountsByTermsStatUnsubscribe($iDateFrom, $iDateTo, $sSql);
+                $iValuePrev = $this->_oDb->getAccountsByTermsStatInitValue($iDateFrom, $sSql);
+                $iValuePrev2 = $this->_oDb->getAccountsByTermsStatInitValueUnsubscribe($iDateFrom, $sSql);
+                $aTmpDates = array();
+                $aTmpDates2 = array();
+                $aValues['values'][0]['legend'] = _t('_bx_massmailer_txt_subscribed');
+                $aValues['values'][1] = array('legend' => _t('_bx_massmailer_txt_unsubscribed') , 'data' => array());
+                foreach ($aData as $aValue) {
+                    $aTmpDates[$aValue['period']] = $aValue['count'];
+                }
+                foreach ($aData2 as $aValue) {
+                    $aTmpDates2[$aValue['period']] = $aValue['count'];
+                }
+                for ($i = $iDateFrom; $i < $iDateTo ; $i = $i + 86400 ){
+                    $sX = date('Y-m-d', $i);
+                    
+                    if (!array_key_exists($sX, $aTmpDates)){
+                        array_push($aValues['values'][0]['data'], array('x' => $sX, 'y' => $sReportType == 'content_total' ? $iValuePrev : 0));
+                        
+                    }
+                    else{
+                        array_push($aValues['values'][0]['data'], array('x' => $sX, 'y' => $sReportType == 'content_total' ? $iValuePrev : $aTmpDates[$sX]));
+                        $iValuePrev += $aTmpDates[$sX];
+                    }
+                    
+                    if (!array_key_exists($sX, $aTmpDates2)){
+                        array_push($aValues['values'][1]['data'], array('x' => $sX, 'y' => $sReportType == 'content_total' ? $iValuePrev2 : 0));
+                        
+                    }
+                    else{
+                        array_push($aValues['values'][1]['data'], array('x' => $sX, 'y' => $sReportType == 'content_total' ? $iValuePrev2 : $aTmpDates2[$sX]));
+                        $iValuePrev2 += $aTmpDates2[$sX];
+                    }
+                    
+                    if ($sReportType == 'content_total'){
+                        if ($iValuePrev > $iMaxValueY)
+                            $iMaxValueY = $iValuePrev;
+                        if ($iValuePrev2 > $iMaxValueY)
+                            $iMaxValueY = $iValuePrev2;
+                    }
+                    else{
+                        if (array_key_exists($sX, $aTmpDates) && $aTmpDates[$sX] > $iMaxValueY)
+                            $iMaxValueY = $aTmpDates[$sX];
+                        if (array_key_exists($sX, $aTmpDates2) && $aTmpDates2[$sX] > $iMaxValueY)
+                            $iMaxValueY = $aTmpDates2[$sX];
+                    }
+                }
+                break;
+            
+            case "CAMPAIGN_REPORT":
+                $aDataStat = $this->_oDb->getStatByCampaignId($sReportType);
+                $aValues['values'][0]['legend'] = _t('_bx_massmailer_txt_sent_total_title', $aDataStat['total']);
+                array_push($aValues['values'][0]['data'], $aDataStat['total']);
+                $aValues['values'][1] = array('legend' => _t('_bx_massmailer_txt_opened_total_title', $aDataStat['seen']) , 'data' => array($aDataStat['seen']));
+                $aValues['values'][2] = array('legend' => _t('_bx_massmailer_txt_unopened_total_title', $aDataStat['total'] - $aDataStat['seen']), 'data' => array($aDataStat['total'] - $aDataStat['seen']));
+                $aValues['values'][3] = array('legend' => _t('_bx_massmailer_txt_clicked_total_title', $aDataStat['clicked']), 'data' => array($aDataStat['clicked']));
+                $aValues['values'][4] = array('legend' => _t('_bx_massmailer_txt_unsubscribed_total_title', $aDataStat['unsubscribed']), 'data' => array($aDataStat['unsubscribed']));
+                $iMaxValueY = $aDataStat['total'];
+                break;
+        }
+        
+        $iMaxValueY = ceil($iMaxValueY * 1.1);
+        
+        $aDataForChartXAxes = array();
+        if ($bIsTimeX){
+            $sUnit = 'day';
+            $iInterval = ($iDateTo - $iDateFrom) / 86400;
+            if ($iInterval > 50)
+                $sUnit = 'week';
+            if ($iInterval > 100)
+                $sUnit = 'month';
+            
+            $aDataForChartXAxes = array(
+                'type' => 'time',
+                'time' => array(
+                    'tooltipFormat' => 'DD.MM.YYYY',
+                    'unit' => $sUnit,
+                 ),
+                'display' => true,
+                'distribution' => 'linear',
+                'ticks' => array(
+                    'display' => true,
+                    'autoSkip' => true,
+                )
+            );
+        }
+        else{
+            $aDataForChartXAxes = array(
+                'ticks' => array(
+                    'autoSkip' => false
+                ),
+                'display' => true
+            );
+        }
+        
+        $aDataForChartDatasets = array();
+        for($i = 0; $i < count($aValues['values']); $i++){
+            $aDataForChartDatasets[] = array(
+                'label' => $aValues['values'][$i]['legend'],
+                'fill' => false,
+                'backgroundColor' => $aColors[$i],
+                'borderColor' => $aColors[$i],
+                'borderWidth' => 1,
+                'data' => $aValues['values'][$i]['data']
+            );
+        }
+        
+        $aDataForChart = array(
+            'type' => $sType,
+            'data' => array(
+                'labels' => $aValues['labels'],
+                'datasets' => $aDataForChartDatasets
+            ),
+            'options' => array(
+                'legend' => array(
+                    'position' => 'bottom',
+                    'display' => count($aValues['values']) == 1 ? false : true
+                ),
+                'scales' => array(
+                    'yAxes' => array(
+                        array(
+                            'ticks' => array(
+                                'max' => $iMaxValueY,
+                                'min' => $iMinValueY,
+                                'stepSize' => $this->getReportStep($iMinValueY, $iMaxValueY),
+                                'autoSkip' => true
+                            )
+                        )
+                    ),
+                    'xAxes' => array($aDataForChartXAxes)
+                )
+            ),
+            'links' => $aValues['links'],
+            'strings' => $aValues['strings'],
+        );
+        
+        echo json_encode($aDataForChart);
+    }
+  
+    /**
+    * SERVICE METHODS
+    */
+          
+    /**
+    * @page service Service Calls
+    * @section bx_massmailer Mass mailer
+    * @subsection bx_massmailer-page_blocks Page Blocks
+     * @subsubsection bx_massmailer-campagn_subscribers campagn_subscribers
+    * 
+     * @code bx_srv('bx_massmailer', 'campagn_subscribers', [...]); @endcode
+    * 
+    * Get page block with the campagn's subscribers list
+    *
+    * @param $aParams an array with search params.
+    * @return HTML string with block content to display on the site. All necessary CSS and JS files are automatically added to the HEAD section of the site HTML.
+    * 
+    * @see BxMassMailerModule::serviceCampagnSubscribers
+    */
+    /** 
+    * @ref bx_massmailer-campagn_subscribers "entity_view"
+    */
+    public function serviceCampagnSubscribers ($iContentId = 0, $sDisplay = false)
+    {
+        if(!$iContentId)
+            $iContentId = bx_process_input(bx_get('id'), BX_DATA_INT);
+
+        if(!$iContentId)
+            return false;
+              
+        $aData = $this->_oDb->getLettersByCampaignId($iContentId);
+        if (count($aData) > 0)
+            return $this->_oTemplate->getSubscribers($aData);
+        return $iContentId;
+    }
+    
+    /**
+     * @page service Service Calls
+     * @section bx_massmailer Mass mailer
+     * @subsection bx_massmailer-page_blocks Page Blocks
+     * @subsubsection bx_massmailer-campagn_info campagn_info
+     * 
+     * @code bx_srv('bx_massmailer', 'campagn_info', [...]); @endcode
+     * 
+     * Get page block with the campagn info
+     *
+     * @param $aParams an array with search params.
+     * @return HTML string with block content to display on the site. All necessary CSS and JS files are automatically added to the HEAD section of the site HTML.
+     * 
+     * @see BxMassMailerModule::serviceCampagnInfo
+     */
+    /** 
+     * @ref bx_massmailer-campagn_info "campagn_info"
+     */
+    public function serviceCampagnInfo ($iContentId = 0, $sDisplay = false)
+    {
+        if(!$iContentId)
+            $iContentId = bx_process_input(bx_get('id'), BX_DATA_INT);
+
+        if(!$iContentId)
+            return false;
+        
+        return $this->_oTemplate->getInfo($iContentId);
+    }
+    
+    /**
+     * @page service Service Calls
+     * @section bx_massmailer Mass mailer
+     * @subsection bx_massmailer-page_blocks Page Blocks
+     * @subsubsection bx_massmailer-campagn_links campagn_links
+     * 
+     * @code bx_srv('bx_massmailer', 'campagn_links', [...]); @endcode
+     * 
+     * Get page block with the campagn links list
+     *
+     * @param $aParams an array with search params.
+     * @return HTML string with block content to display on the site. All necessary CSS and JS files are automatically added to the HEAD section of the site HTML.
+     * 
+     * @see BxMassMailerModule::serviceCampagnLinks
+     */
+    /** 
+     * @ref bx_massmailer-campagn_links "campagn_links"
+     */
+    public function serviceCampagnLinks ($iContentId = 0, $sDisplay = false)
+    {
+        if(!$iContentId)
+            $iContentId = bx_process_input(bx_get('id'), BX_DATA_INT);
+
+        if(!$iContentId)
+            return false;
+        
+        $aData = $this->_oDb->getClicksByCampaignId($iContentId);
+        if (count($aData) > 0)
+            return $this->_oTemplate->getClicks($aData);
+        return $iContentId;
+    }
+    
+    /**
+     * @page service Service Calls
+     * @section bx_massmailer Mass mailer
+     * @subsection bx_massmailer-page_blocks Page Blocks
+     * @subsubsection bx_massmailer-total_subscribers total_subscribers
+     * 
+     * @code bx_srv('bx_massmailer', 'total_subscribers', [...]); @endcode
+     * 
+     * Get page block with subscribers charts
+     *
+     * @param $aParams an array with search params.
+     * @return HTML string with block content to display on the site. All necessary CSS and JS files are automatically added to the HEAD section of the site HTML.
+     * 
+     * @see BxMassMailerModule::serviceTotalSubscribers
+     */
+    /** 
+     * @ref bx_massmailer-total_subscribers "total_subscribers"
+     */
+    public function serviceTotalSubscribers ()
+    {
+        return $this->_oTemplate->getTotalSubscribers();
+    }
+    
+    /**
+     * @page service Service Calls
+     * @section bx_massmailer Mass mailer
+     * @subsection bx_massmailer-page_blocks Page Blocks
+     * @subsubsection bx_massmailer-entity_breadcrumb entity_breadcrumb
+     * 
+     * @code bx_srv('bx_massmailer', 'entity_breadcrumb', [...]); @endcode
+     * 
+     * Get page block with page's breadcrumbs
+     *
+     * @param $aParams an array with search params.
+     * @return HTML string with block content to display on the site. All necessary CSS and JS files are automatically added to the HEAD section of the site HTML.
+     * 
+     * @see BxMassMailerModule::serviceEntityBreadcrumb
+     */
+    /** 
+     * @ref bx_massmailer-entity_breadcrumb "entity_breadcrumb"
+     */
+    public function serviceEntityBreadcrumb ($iContentId = 0)
+    {
+        $CNF = &$this->_oConfig->CNF;
+        $oPermalink = BxDolPermalinks::getInstance();
+        if (!$iContentId)
+            $iContentId = bx_process_input(bx_get('id'), BX_DATA_INT);
+        if (!$iContentId){
+            $aContentInfo[$CNF['FIELD_TITLE']] = _t('_bx_massmailer_txt_breadcrumb_title_new_campaign');
+            $aContentInfo['url'] = $oPermalink->permalink('page.php?i=' . $CNF['URI_ADD_CAMPAIGN']);
+        }
+        else{
+            $aContentInfo = $this->_oDb->getCampaignInfoById($iContentId);
+            if (!$aContentInfo)
+                return false;
+            $aContentInfo['url'] = $oPermalink->permalink('page.php?i=' . $CNF['URI_VIEW_CAMPAIGN'] . '&id=' . $aContentInfo[$CNF['FIELD_ID']]);
+        }
+        return $this->_oTemplate->entryBreadcrumb($aContentInfo);
+    }
+    
+    /**
+     * @page service Service Calls
+     * @section bx_massmailer Mass mailer
+     * @subsection bx_massmailer-page_blocks Page Blocks
+     * @subsubsection bx_massmailer-attributes attributes
+     * 
+     * @code bx_srv('bx_massmailer', 'attributes', [...]); @endcode
+     * 
+     * Get page block with attributes list
+     *
+     * @param $aParams an array with search params.
+     * @return HTML string with block content to display on the site. All necessary CSS and JS files are automatically added to the HEAD section of the site HTML.
+     * 
+     * @see BxMassMailerModule::serviceAttributes
+     */
+    /** 
+     * @ref bx_massmailer-attributes "attributes"
+     */
+    public function serviceAttributes ()
+    {
+        $aAttributesParts = array();
+        $aAttributesParts['account'] = array(_t('_bx_massmailer_txt_attribute_global'), array('email' => _t('_bx_massmailer_txt_attribute_email'), 'account_name' => _t('_bx_massmailer_txt_attribute_account_name'), 'account_id' => _t('_bx_massmailer_txt_attribute_account_id'), 'unsubscribe_url' => _t('_bx_massmailer_txt_attribute_unsubscribe_url')));
+        $BxDolModuleQuery = BxDolModuleQuery::getInstance();
+        $aModules = $BxDolModuleQuery->getModulesBy(array('type' => 'modules', 'active' => 1));
+        foreach($aModules as $aModule){
+            if(BxDolRequest::serviceExists($aModule['name'], 'act_as_profile') && BxDolService::call($aModule['name'], 'act_as_profile') == true){
+                $oModule = BxDolModule::getInstance($aModule['name']);
+                $aTmp2 = array();
+                $aTmp = $oModule->serviceGetSearchableFieldsExtended('view');
+                foreach($aTmp as $sKey => $aField){
+                    $aTmp2[$sKey] = _t($aField['caption']);
+                }
+                $aAttributesParts[$aModule['name']] = array($aModule['title'], $aTmp2);
+            }
+        }
+        return $this->_oTemplate->getAttributes($aAttributesParts);
+    }
+    
+    /**
+     * @return CHECK_ACTION_RESULT_ALLOWED if access is granted or error message if access is forbidden. So make sure to make strict(===) checking.
+     */
+    public function checkAllowedAdd ($isPerformAction = false)
+    {
+        return $this->checkAllowed($isPerformAction);
+    }
+    
+    /**
+     * @return CHECK_ACTION_RESULT_ALLOWED if access is granted or error message if access is forbidden. So make sure to make strict(===) checking.
+     */
+    public function checkAllowedEdit ($aDataEntry, $isPerformAction = false)
+    {
+        return $this->checkAllowed($isPerformAction);
+    }
+
+    /**
+     * @return CHECK_ACTION_RESULT_ALLOWED if access is granted or error message if access is forbidden. So make sure to make strict(===) checking.
+     */
+    public function checkAllowedDelete (&$aDataEntry, $isPerformAction = false)
+    {
+        return $this->checkAllowed($isPerformAction);
+    }
+          
+    public function getSegments($sKey = "")
+    {
+        if ($sKey == ""){
+            return $this->aSegments;
+        }
+        else{
+            if (isset($this->aSegments[$sKey]))
+                return $this->aSegments[$sKey];
+            return false;
+        }
+    }
+          
+    public function sendTest($sEmail, $iCampaignId)
+    {
+        $aTmp = $this->getDataForCampaign($iCampaignId);
+        $aTemplate = $aTmp[0];
+        $aCustomHeaders = $aTmp[1];
+              
+        if (!$aTemplate)
+            return false;
+        
+        return $this->sendLetter($sEmail, $iCampaignId, $aCustomHeaders, bx_get_logged_profile_id(), $aTemplate, false);
+    }
+          
+    public function sendAll($iCampaignId)
+    {
+        $aEmails = array();
+        $aTmp = $this->getDataForCampaign($iCampaignId);
+        $aTemplate = $aTmp[0];
+        $aCustomHeaders = $aTmp[1];
+        $aCampaign = $aTmp[2];
+              
+        if (!$aTemplate)
+            return false;
+        $this->_oDb->deleteCampaignData($iCampaignId);
+        $aAccounts = $this->getEmailsBySegment($aCampaign['segments']);
+        foreach ($aAccounts as $aAccountInfo){
+            array_push($aEmails, $aAccountInfo['email']);
+            $this->sendLetter($aAccountInfo['email'], $iCampaignId, $aCustomHeaders, $aAccountInfo['profile_id'], $aTemplate, true);
+        }
+              
+        $this->_oDb->sendCampaign($iCampaignId, implode(',', $aEmails));
+        return true;
+    }
+    
+    private function sendLetter($sEmail, $iCampaignId, $aCustomHeaders, $iProfileId, $aTemplate, $bAddToQueue)
+    {
+        $sLetterCode = $this->_oDb->addLetter($iCampaignId, $sEmail);
+       
+        $regexp = "<a\s[^>]*href=(\"??)([^\" >]*?)\\1[^>]*>(.*)<\/a>";
+        $aTemplate['Body'] = preg_replace_callback("/$regexp/siU",
+            function ($aMatch) use ($iCampaignId, $sLetterCode) {
+                if ($aMatch[2] != '{unsubscribe_url}'){
+                    $sLinkCode = $this->_oDb->addLink($sLetterCode, $aMatch[2], $aMatch[3], $iCampaignId);
+                    return str_replace($aMatch[2], BX_DOL_URL_ROOT . $this->_oConfig->getBaseUri() . 'track/click/' . $sLinkCode . "/", $aMatch[0]);
+                }
+                else{
+                    return $aMatch[0];
+                }
+            },
+            $aTemplate['Body']);
+        return sendMail($sEmail, $aTemplate['Subject'], $aTemplate['Body'], 0, $this->addMarkers($iProfileId, $sLetterCode), BX_EMAIL_MASS, 'html', false, $aCustomHeaders, $bAddToQueue);
+    }
+
+    public function getEmailCountInSegment($iCampaignId)
+    {
+        $aTmp = $this->getDataForCampaign($iCampaignId);
+        $aCampaign = $aTmp[2];
+        $aAccounts = $this->getEmailsBySegment($aCampaign['segments']);
+        return count($aAccounts);
+    }
+          
+    private function getEmailsBySegment($sSegment)
+    {
+        return $this->_oDb->getAccountsByTerms($this->getSqlBySegment($sSegment));
+    }
+    
+    private function checkAllowed($isPerformAction = false)
+    {
+        $aCheck = checkActionModule($this->_iProfileId, 'use massmailer', $this->getName(), $isPerformAction);
+        if ($aCheck[CHECK_ACTION_RESULT] !== CHECK_ACTION_RESULT_ALLOWED)
+             return _t('_sys_txt_access_denied');
+        return CHECK_ACTION_RESULT_ALLOWED;
+    }
+    
+    private function getSqlBySegment($sSegment)
+    {
+        $sRv = '';
+        if (substr_count($sSegment, 'lvl:') > 0){
+            $sLvl = str_replace('lvl:', '', $sSegment);
+            switch ($sLvl) {
+                case 0:
+                    $sRv = '';
+                    break;
+                case MEMBERSHIP_ID_UNCONFIRMED:
+                    $sRv = " AND `ta`.`email_confirmed` = 0 AND `ta`.`phone_confirmed` = 0 ";
+                    break;
+                case MEMBERSHIP_ID_SUSPENDED:
+                    $sRv = " AND `tp`.`status` = 'suspended' ";
+                    break;
+                case MEMBERSHIP_ID_PENDING:
+                    $sRv = " AND `tp`.`status` = 'pending' ";
+                    break;
+                case MEMBERSHIP_ID_STANDARD:
+                    $sRv = " AND `tp`.`id` NOT IN (SELECT `IDMember` FROM `sys_acl_levels_members`) ";
+                    break;
+                default:
+                    $sRv = $this->_oDb->prepareAsString(" AND `tp`.`id` IN (SELECT `IDMember` FROM `sys_acl_levels_members` WHERE IDLevel = ?) ", $sLvl);
+                    break;
+            }
+            $sRv .= " AND `tp`.`type` IN ('" . implode("','", $this->getProfileModules()) . "')"; 
+        }
+        return $sRv;
+    }
+    
+    private function getProfileModules()
+    {
+        $aResult = array();
+        $BxDolModuleQuery = BxDolModuleQuery::getInstance();
+        $aModules = $BxDolModuleQuery->getModulesBy(array('type' => 'modules', 'active' => 1));
+        foreach($aModules as $aModule){
+            if(BxDolRequest::serviceExists($aModule['name'], 'act_as_profile') && BxDolService::call($aModule['name'], 'act_as_profile') == true){
+                array_push($aResult, $aModule['name']);
+            }
+        }
+        return $aResult;
+    }
+          
+    private function getDataForCampaign($iCampaignId)
+    {
+        $aCampaign = $this->_oDb->getCampaignInfoById($iCampaignId);
+        $oEmailTemplates = BxDolEmailTemplates::getInstance();
+        $aTemplate = $oEmailTemplates->parseTemplate('bx_massmailer_email', array('content' => $aCampaign['body']));
+        $aTemplate['Subject'] = $aCampaign['subject'];
+              
+        $aCustomHeaders = array();
+        $sFrom = $aCampaign['from_name'] != '' ? $aCampaign['from_name'] : getParam('bx_massmailer_initial_from_email');
+        $aCustomHeaders['From'] = "=?UTF-8?B?" . base64_encode($sFrom) . "?= <" . getParam('site_email_notify') . ">" ;
+        if ($aCampaign['reply_to'] != ''){
+            $sFrom = $aCampaign['from_name'] != '' ? $aCampaign['from_name'] : getParam('site_title');
+            $aCustomHeaders['Reply-To'] = "=?UTF-8?B?" . base64_encode($sFrom) . '?= <' . bx_process_output($aCampaign['reply_to']) . '>';
+            $aCustomHeaders['X-Original-From'] = "=?UTF-8?B?" . base64_encode($sFrom) . '?= <' . bx_process_output($aCampaign['reply_to']) . '>';
+        }
+              
+        return array($aTemplate, $aCustomHeaders, $aCampaign);
+    }
+          
+    private function addMarkers($iProfileId, $sLetterCode)
+    {
+        $CNF = &$this->_oConfig->CNF;
+        $aMarkers = array();
+        $oProfile = BxDolProfile::getInstance($iProfileId);
+        if ($oProfile){
+            $oAccount = BxDolAccount::getInstance($oProfile->getAccountId());
+            $aMarkers['email'] = $oAccount->getEmail();
+            $aMarkers['account_name'] = $oAccount->getDisplayName();
+            $aMarkers['account_id'] = $oProfile->getAccountId();
+            $oModule = BxDolModule::getInstance($oProfile->getModule());
+            if (BxDolRequest::serviceExists($oProfile->getModule(), 'get_info')){
+                $aProfileInfo = $oModule->serviceGetInfo($oProfile->getContentId(), false);
+                foreach ($aProfileInfo as $sKey => $sValue){
+                    if (!isset($aMarkers[$sKey]))
+                    $aMarkers[$sKey] = $sValue;
+                }
+            
+                $aMarkers['seen_image_url'] = BX_DOL_URL_ROOT . $this->_oConfig->getBaseUri() . 'track/seen/' . $sLetterCode . "/";
+                $aMarkers['unsubscribe_url'] = BX_DOL_URL_ROOT . $oAccount->getUnsubscribeLink(BX_EMAIL_MASS) . "&lhash=" . $sLetterCode;
+            }
+            
+        }
+        return $aMarkers;
+    }
+    
+    private function getReportStep($iMin, $iMax)
+    {
+        $iCount = 10;
+        return ceil(($iMax - $iMin) / $iCount);
+    }
+}
+
+/** @} */

+ 77 - 0
modules/boonex/massmailer/updates/9.0.1_9.0.2/source/install/config.php

@@ -0,0 +1,77 @@
+<?php
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    MassMailer Mass Mailer
+ * @ingroup     UnaModules
+ *
+ * @{
+ */
+
+$aConfig = array(
+
+    /**
+     * Main Section.
+     */
+    'type' => BX_DOL_MODULE_TYPE_MODULE,
+    'name' => 'bx_massmailer',
+    'title' => 'Mass mailer',
+    'note' => 'Mass mailer module.',
+    'version' => '9.0.2',
+    'vendor' => 'BoonEx',
+    'help_url' => 'http://feed.una.io/?section={module_name}',
+
+    'compatible_with' => array(
+        '9.0.0-RC10'
+    ),
+
+    /**
+     * 'home_dir' and 'home_uri' - should be unique. Don't use spaces in 'home_uri' and the other special chars.
+     */
+    'home_dir' => 'boonex/massmailer/',
+    'home_uri' => 'massmailer',
+
+    'db_prefix' => 'bx_massmailer_',
+    'class_prefix' => 'BxMassMailer',
+
+    /**
+     * Category for language keys.
+     */
+    'language_category' => 'MassMailer',
+
+    /**
+     * Installation/Uninstallation Section.
+     */
+    'install' => array(
+        'execute_sql' => 1,
+        'update_languages' => 1,
+        'clear_db_cache' => 1,
+    ),
+    'uninstall' => array (
+        'execute_sql' => 1,
+        'update_languages' => 1,
+        'clear_db_cache' => 1,
+    ),
+    'enable' => array(
+        'execute_sql' => 1,
+        'clear_db_cache' => 1,
+    ),
+    'enable_success' => array(
+        'clear_db_cache' => 1,
+    ),
+    'disable' => array (
+        'execute_sql' => 1,
+        'clear_db_cache' => 1,
+    ),
+    'disable_failed' => array (
+        'clear_db_cache' => 1,
+    ),
+
+    /**
+     * Dependencies Section
+     */
+    'dependencies' => array(),
+);
+
+/** @} */