%PDF- %PDF-
Direktori : /home/bitrix/www/bitrix/modules/sender/lib/posting/ |
Current File : /home/bitrix/www/bitrix/modules/sender/lib/posting/sender.php |
<?php /** * Bitrix Framework * @package bitrix * @subpackage sender * @copyright 2001-2012 Bitrix */ namespace Bitrix\Sender\Posting; use Bitrix\Main; use Bitrix\Main\Entity\ReferenceField; use Bitrix\Main\Localization\Loc; use Bitrix\Main\DB; use Bitrix\Main\Type; use Bitrix\Main\Application; use Bitrix\Main\Config\Option; use Bitrix\Main\Event; use Bitrix\Sender\PostingRecipientTable; use Bitrix\Sender\PostingTable; use Bitrix\Sender\Entity\Letter; use Bitrix\Sender\Integration; use Bitrix\Sender\Internals\Model\LetterTable; use Bitrix\Sender\Recipient; use Bitrix\Sender\Message\Adapter; Loc::loadMessages(__FILE__); /** * Class Sender * @package Bitrix\Sender\Posting */ class Sender { const RESULT_NONE = 0; const RESULT_SENT = 1; const RESULT_CONTINUE = 2; const RESULT_ERROR = 3; /** @var Letter $letter Letter. */ protected $letter; /** @var Adapter $message Message. */ protected $message; /** @var integer|null $timeout Timeout. */ protected $timeout; /** @var integer|null $timeAtStart Time at start. */ protected $timeAtStart; /** @var integer|null $limit Limit. */ protected $limit; /** @var integer $sentCount Count of sent. */ protected $sentCount = 0; /** @var integer $checkStatusStep Step for status checking. */ protected $checkStatusStep = 25; /** @var integer $checkStatusCounter Counter for status checking. */ protected $checkStatusCounter = 0; /** @var boolean $isPrevented Is prevented. */ protected $isPrevented = false; /** @var boolean $isTrigger Is trigger. */ protected $isTrigger = false; /** @var boolean $isReiterate Is reiterate. */ protected $isReiterate = false; /** @var integer $mailingId Campaign ID. */ protected $mailingId; /** @var integer $postingId Posting ID. */ protected $postingId; /** @var integer $letterId Letter ID. */ protected $letterId; /** @var string $status Status. */ protected $status; /** @var integer $sendCount Count of send. */ protected $sendCount = 0; /** @var string $resultCode Code of result. */ protected $resultCode = self::RESULT_NONE; /** * Sender constructor. * @param Letter $letter Letter. */ public function __construct(Letter $letter) { $this->letter = $letter; $this->checkStatusStep = (int) Option::get('sender', 'send_check_status_step', $this->checkStatusStep); $this->message = $letter->getMessage(); } /** * Load posting. * * @param integer $postingId Posting ID. * @return void */ public function load($postingId) { $postingDb = PostingTable::getList(array( 'select' => array( 'ID', 'STATUS', 'MAILING_ID', 'MAILING_CHAIN_ID', 'MAILING_CHAIN_REITERATE' => 'MAILING_CHAIN.REITERATE', 'MAILING_CHAIN_IS_TRIGGER' => 'MAILING_CHAIN.IS_TRIGGER', 'COUNT_SEND_ALL' ), 'filter' => array( '=ID' => $postingId, '=MAILING.ACTIVE' => 'Y', '=MAILING_CHAIN.STATUS' => array( LetterTable::STATUS_SEND, LetterTable::STATUS_PLAN ), ) )); if ($postingData = $postingDb->fetch()) { $this->postingId = $postingData['ID']; $this->status = $postingData['STATUS']; $this->mailingId = $postingData['MAILING_ID']; $this->letterId = $postingData['MAILING_CHAIN_ID']; $this->sendCount = $postingData['COUNT_SEND_ALL']; $this->isReiterate = $postingData['MAILING_CHAIN_REITERATE'] == 'Y'; $this->isTrigger = $postingData['MAILING_CHAIN_IS_TRIGGER'] == 'Y'; } } /** * Set limit. * * @param integer $limit Limit. * @return $this */ public function setLimit($limit) { $this->limit = $limit; return $this; } /** * Set timeout. * * @param integer $timeout Timeout. * @return $this */ public function setTimeout($timeout) { $this->timeout = $timeout; return $this; } /** * Start time watch. * * @return void */ public function startTime() { if (!$this->timeout) { return; } $this->timeAtStart = getmicrotime(); @set_time_limit(0); } /** * Check timeout. * * @return bool */ public function isTimeout() { if (!$this->timeout) { return false; } return (getmicrotime() - $this->timeAtStart >= $this->timeout); } /** * Check limits. * * @return bool */ public function isLimitExceeded() { if (!$this->limit) { return false; } return ($this->sentCount > $this->limit); } /** * Check transport limits. * * @return bool */ public function isTransportLimitsExceeded() { return $this->message->getTransport()->isLimitsExceeded($this->message); } /** * Lock posting for preventing double sending * * @param integer $id ID. * @return bool * @throws \Bitrix\Main\Db\SqlQueryException * @throws \Exception */ public static function lock($id) { $id = intval($id); $uniqueSalt = self::getLockUniqueSalt(); $connection = Application::getInstance()->getConnection(); if($connection instanceof DB\MysqlCommonConnection) { $lockDb = $connection->query( "SELECT GET_LOCK('" . $uniqueSalt . "_sendpost_" . $id . "', 0) as L", false, "File: " . __FILE__ . "<br>Line: " . __LINE__ ); $lock = $lockDb->fetch(); if($lock["L"] == "1") { return true; } else { return false; } } return false; } /** * UnLock posting that was locking for preventing double sending * * @param integer $id ID. * @return bool */ public static function unlock($id) { $id = intval($id); $connection = Application::getInstance()->getConnection(); if($connection instanceof DB\MysqlCommonConnection) { $uniqueSalt = self::getLockUniqueSalt(false); if(!$uniqueSalt) { return false; } $lockDb = $connection->query("SELECT RELEASE_LOCK('" . $uniqueSalt . "_sendpost_" . $id . "') as L"); $lock = $lockDb->fetch(); if($lock["L"] == "0") { return false; } else { return true; } } return false; } protected static function getLockUniqueSalt($generate = true) { $uniqueSalt = Option::get("main", "server_uniq_id", ""); if($uniqueSalt == '' && $generate) { $uniqueSalt = md5(uniqid(rand(), true)); Option::set("main", "server_uniq_id", $uniqueSalt); } return $uniqueSalt; } /** * Apply recipient data to message. * * @param Adapter $message Message. * @param array $recipient Recipient. * @param bool $isTest Is test. * @return void */ public static function applyRecipientToMessage(Adapter $message, array $recipient, $isTest = false) { $message->getReadTracker() ->setModuleId('sender') ->setFields(array('RECIPIENT_ID' => $recipient["ID"])) ->setHandlerUri(Option::get('sender', 'read_link')); $message->getClickTracker() ->setModuleId('sender') ->setFields(array('RECIPIENT_ID' => $recipient["ID"])) ->setUriParameters(array('bx_sender_conversion_id' => $recipient["ID"])) ->setHandlerUri(Option::get('sender', 'click_link')); $message->getUnsubTracker() ->setModuleId('sender') ->setFields(array( 'RECIPIENT_ID' => $recipient["ID"], 'MAILING_ID' => isset($recipient['CAMPAIGN_ID']) ? $recipient['CAMPAIGN_ID'] : 0, 'EMAIL' => $message->getRecipientCode(), 'CODE' => $message->getRecipientCode(), 'TEST' => $isTest ? 'Y' : 'N' )) ->setHandlerUri(Option::get('sender', 'unsub_link')); $fields = self::prepareRecipientFields($recipient); $message->setFields($fields); $message->setRecipientId($recipient['ID']); $message->setRecipientCode($recipient['CONTACT_CODE']); $message->setRecipientType(Recipient\Type::getCode($recipient['CONTACT_TYPE_ID'])); $message->setRecipientData($recipient); } protected function sendToRecipient($recipient) { self::applyRecipientToMessage($this->message, $recipient); try { $sendResult = $this->message->send(); } catch(Main\Mail\StopException $e) { $sendResult = false; $this->prevent(); } return $sendResult; } protected function initRecipients() { // if posting in new status, then import recipients from groups // and set right status for sending if(!$this->postingId) { return; } if($this->isTrigger) { return; } if($this->status != PostingTable::STATUS_NEW) { return; } Builder::create()->run($this->postingId); } protected function changeStatusToPart() { if(!$this->postingId) { return; } if($this->status == PostingTable::STATUS_PART) { return; } if ($this->status != PostingTable::STATUS_NEW && !$this->isTrigger) { return; } $this->status = PostingTable::STATUS_PART; PostingTable::update(array('ID' => $this->postingId), array('STATUS' => $this->status)); } /** * Get result code. * * @return int */ public function getResultCode() { return $this->resultCode; } /** * Send. * * @return void * @throws DB\Exception */ public function send() { $this->load($this->letter->get('POSTING_ID')); if(!$this->postingId) { $this->resultCode = self::RESULT_ERROR; return; } $this->startTime(); $this->initRecipients(); $this->changeStatusToPart(); // posting not in right status if($this->status != PostingTable::STATUS_PART) { $this->resultCode = static::RESULT_ERROR; return; } // lock posting for exclude double parallel sending if(static::lock($this->postingId) === false) { throw new DB\Exception(Loc::getMessage('SENDER_POSTING_MANAGER_ERR_LOCK')); } if ($this->isTransportLimitsExceeded()) { $this->resultCode = static::RESULT_CONTINUE; return; } $recipients = $this->getRecipients(); if ($recipients->getSelectedRowsCount() > 0) { $this->message->getTransport()->setSendCount($this->sendCount); if (!$this->message->getTransport()->start()) { $this->prevent(); } } foreach ($recipients as $recipient) { if ($this->isPrevented()) { break; } if ($this->isStoppedOnRun()) { break; } $this->setPostingDateSend(); if ( empty($recipient['CONTACT_CODE']) || $recipient['CONTACT_BLACKLISTED'] === 'Y' || $recipient['CONTACT_UNSUBSCRIBED'] === 'Y' ) { $sendResult = false; } else { $sendResult = $this->sendToRecipient($recipient); if ($this->isPrevented()) { break; } } $sendResultStatus = $sendResult ? PostingRecipientTable::SEND_RESULT_SUCCESS : PostingRecipientTable::SEND_RESULT_ERROR; PostingRecipientTable::update( array('ID' => $recipient["ID"]), array( 'STATUS' => $sendResultStatus, 'DATE_SENT' => new Type\DateTime() ) ); // send event $eventData = array( 'SEND_RESULT' => $sendResult, 'RECIPIENT' => $recipient, 'POSTING' => array( 'ID' => $this->postingId, 'STATUS' => $this->status, 'MAILING_ID' => $this->mailingId, 'MAILING_CHAIN_ID' => $this->letterId, ) ); $event = new Event('sender', 'OnAfterPostingSendRecipient', array($eventData, $this->letter)); $event->send(); Integration\EventHandler::onAfterPostingSendRecipient($eventData, $this->letter); // limit executing script by time if ($this->isTimeout() || $this->isLimitExceeded() || $this->isTransportLimitsExceeded()) { break; } // increment sending statistic $this->sentCount++; } $this->message->getTransport()->end(); // unlock posting for exclude double parallel sending self::unlock($this->postingId); // update status of posting $status = self::updateActualStatus($this->postingId, $this->isPrevented()); // set result code to continue or end of sending $isContinue = $status == PostingTable::STATUS_PART; $this->resultCode = $isContinue ? static::RESULT_CONTINUE : static::RESULT_SENT; } protected function setPostingDateSend() { if ($this->letter->get('DATE_SEND')) { return; } PostingTable::update(array('ID' => $this->postingId), array('DATE_SEND' => new Type\DateTime())); } /** * Update actual status. * * @param int $postingId Posting ID. * @param bool $isPrevented Is sending prevented. * @return string */ public static function updateActualStatus($postingId, $isPrevented = false) { //set status and delivered and error emails $statusList = PostingTable::getRecipientCountByStatus($postingId); $hasStatusError = array_key_exists(PostingRecipientTable::SEND_RESULT_ERROR, $statusList); $hasStatusNone = array_key_exists(PostingRecipientTable::SEND_RESULT_NONE, $statusList); if($isPrevented) { $status = PostingTable::STATUS_ABORT; } elseif(!$hasStatusNone) { $status = $hasStatusError ? PostingTable::STATUS_SENT_WITH_ERRORS : PostingTable::STATUS_SENT; } else { $status = PostingTable::STATUS_PART; } $postingUpdateFields = array( 'STATUS' => $status, 'DATE_SENT' => $status == PostingTable::STATUS_PART ? null : new Type\DateTime(), 'COUNT_SEND_ALL' => 0 ); $recipientStatusToPostingFieldMap = PostingTable::getRecipientStatusToPostingFieldMap(); foreach($recipientStatusToPostingFieldMap as $recipientStatus => $postingFieldName) { if(!array_key_exists($recipientStatus, $statusList)) { $postingCountFieldValue = 0; } else { $postingCountFieldValue = $statusList[$recipientStatus]; } $postingUpdateFields['COUNT_SEND_ALL'] += $postingCountFieldValue; $postingUpdateFields[$postingFieldName] = $postingCountFieldValue; } PostingTable::update(array('ID' => $postingId), $postingUpdateFields); return $status; } protected function getRecipients() { // select all recipients of posting, only not processed $recipients = PostingRecipientTable::getList(array( 'select' => array( '*', 'NAME' => 'CONTACT.NAME', 'CONTACT_CODE' => 'CONTACT.CODE', 'CONTACT_TYPE_ID' => 'CONTACT.TYPE_ID', 'CONTACT_IS_SEND_SUCCESS' => 'CONTACT.IS_SEND_SUCCESS', 'CONTACT_BLACKLISTED' => 'CONTACT.BLACKLISTED', 'CONTACT_UNSUBSCRIBED' => 'MAILING_SUB.IS_UNSUB', 'CAMPAIGN_ID' => 'POSTING.MAILING_ID' ), 'filter' => array( '=POSTING_ID' => $this->postingId, '=STATUS' => PostingRecipientTable::SEND_RESULT_NONE ), 'runtime' => [ new ReferenceField( 'MAILING_SUB', 'Bitrix\\Sender\\MailingSubscriptionTable', [ '=this.CONTACT_ID' => 'ref.CONTACT_ID', '=this.POSTING.MAILING_ID' => 'ref.MAILING_ID' ], ['join_type' => 'LEFT'] ) ], 'limit' => $this->limit )); $recipients->addFetchDataModifier( function ($row) { $row['FIELDS'] = is_array($row['FIELDS']) ? $row['FIELDS'] : array(); return $row; } ); return $recipients; } protected static function prepareRecipientFields($recipient) { // create name from email if(empty($recipient["NAME"])) { $recipient["NAME"] = Recipient\Field::getDefaultName(); } $senderChainId = (int)$recipient["MAILING_CHAIN_ID"] > 0 ? (int)$recipient["MAILING_CHAIN_ID"] : (int)$recipient['CAMPAIGN_ID']; // prepare params for send $fields = array( 'EMAIL_TO' => $recipient['CONTACT_CODE'], 'NAME' => $recipient['NAME'], 'USER_ID' => $recipient["USER_ID"], 'SENDER_CHAIN_ID' => $senderChainId, 'SENDER_CHAIN_CODE' => 'sender_chain_item_' . $senderChainId ); if(is_array($recipient['FIELDS']) && count($recipient) > 0) { $fields = $fields + $recipient['FIELDS']; } return $fields; } protected function isPrevented() { return $this->isPrevented; } protected function prevent() { return $this->isPrevented = true; } protected function isStoppedOnRun() { // check pause or stop status if(++$this->checkStatusCounter < $this->checkStatusStep) { return false; } $checkStatusDb = LetterTable::getList(array( 'select' => array('ID'), 'filter' => array( '=ID' => $this->letterId, '=STATUS' => LetterTable::STATUS_SEND ) )); if(!$checkStatusDb->fetch()) { return true; } $this->checkStatusCounter = 0; return false; } }