%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;
}
}