Your IP : 3.147.104.221


Current Path : /var/www/axolotl/data/www/msk.axolotls.ru/bitrix/modules/sender/lib/posting/
Upload File :
Current File : /var/www/axolotl/data/www/msk.axolotls.ru/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\Application;
use Bitrix\Main\Config\Option;
use Bitrix\Main\DB;
use Bitrix\Main\Event;
use Bitrix\Main\Localization\Loc;
use Bitrix\Main\Type;
use Bitrix\Sender\Entity\Campaign;
use Bitrix\Sender\Entity\Letter;
use Bitrix\Sender\Integration;
use Bitrix\Sender\Internals\Model;
use Bitrix\Sender\Internals\Model\PostingThreadTable;
use Bitrix\Sender\MailingTable;
use Bitrix\Sender\Message\Adapter;
use Bitrix\Sender\Posting\ThreadStrategy\IThreadStrategy;
use Bitrix\Sender\PostingRecipientTable;
use Bitrix\Sender\PostingTable;
use Bitrix\Sender\Recipient;

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;

	/**
	 * @var IThreadStrategy
	 */
	protected $threadStrategy;

	/**
	 * 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();
		$this->message->getConfiguration()->set('LETTER_ID', $this->letter->getId());
	}

	/**
	 * 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;
	}

	/**
	 * 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->threadStrategy->setPostingId($this->postingId);
		$this->threadStrategy->fillThreads();
		$this->threadStrategy->lockThread();
		$threadId = $this->threadStrategy->getThreadId();

		// lock posting for exclude double parallel sending

		if (is_null($threadId))
		{
			$this->resultCode = static::RESULT_CONTINUE;
			return;
		}

		if (static::lock($this->postingId, $threadId) === false)
		{
			throw new DB\Exception(Loc::getMessage('SENDER_POSTING_MANAGER_ERR_LOCK'));
		}

		if ($this->status == PostingTable::STATUS_NEW && $threadId !== 0)
		{
			$this->resultCode = static::RESULT_CONTINUE;
			$this->threadStrategy->updateStatus(PostingThreadTable::STATUS_NEW);
			return;
		}

		$this->initRecipients();
		$this->changeStatusToPart();

		// posting not in right status
		if ($this->status != PostingTable::STATUS_PART)
		{
			$this->resultCode = static::RESULT_ERROR;

			return;
		}

		if ($this->isTransportLimitsExceeded())
		{
			$this->resultCode = static::RESULT_CONTINUE;

			return;
		}

		$recipients = $this->threadStrategy->getRecipients($this->limit);
		if ($recipients->getSelectedRowsCount() > 0)
		{
			$this->message->getTransport()->setSendCount($recipients->getSelectedRowsCount());
			if (!$this->message->getTransport()->start())
			{
				$this->prevent();
			}
		}

		$this->sendToRecipients($recipients);

		$this->message->getTransport()->end();

		// unlock posting for exclude double parallel sending
		self::unlock($this->postingId, $threadId);
		if ($recipients->getSelectedRowsCount() === 0)
		{
			$this->threadStrategy->updateStatus(PostingThreadTable::STATUS_DONE);
		}
		else
		{
			$this->threadStrategy->updateStatus(PostingThreadTable::STATUS_NEW);
		}

		// update status of posting
		$status = self::updateActualStatus($this->postingId, $this->isPrevented());

		if ($threadId < $this->threadStrategy->lastThreadId())
		{
			$this->resultCode = static::RESULT_CONTINUE;

			return;
		}

		if ($this->threadStrategy->hasUnprocessedThreads())
		{
			$this->resultCode = static::RESULT_CONTINUE;

			return;
		}

		if (!PostingRecipientTable::hasUnprocessed($this->postingId))
		{
			$onAfterEndResult = $this->message->onAfterEnd();
			if (!$onAfterEndResult->isSuccess())
			{
				$this->resultCode = static::RESULT_CONTINUE;

				return;
			}
			$errorMessage = implode(', ', $onAfterEndResult->getErrorMessages());
			if (strlen($errorMessage))
			{
				Model\LetterTable::update($this->letterId, ['ERROR_MESSAGE' => $errorMessage]);
			}
		}

		// set result code to continue or end of sending
		$isContinue       = $status == PostingTable::STATUS_PART;
		$this->resultCode = $isContinue ? static::RESULT_CONTINUE : static::RESULT_SENT;

		if ($this->resultCode == static::RESULT_SENT)
		{
			$this->resultCode = !$this->threadStrategy->finalize() ? static::RESULT_CONTINUE : static::RESULT_SENT;
		}
	}

	/**
	 * Load posting.
	 *
	 * @param integer $postingId Posting ID.
	 *
	 * @return void
	 */
	public function load($postingId)
	{
		$postingDb = PostingTable::getList(
			[
				'select' => [
					'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' => [
					'=ID'                   => $postingId,
					'=MAILING.ACTIVE'       => 'Y',
					'=MAILING_CHAIN.STATUS' => [
						Model\LetterTable::STATUS_SEND,
						Model\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';
		}
	}

	/**
	 * Start time watch.
	 *
	 * @return void
	 */
	public function startTime()
	{
		if (!$this->timeout)
		{
			return;
		}

		$this->timeAtStart = getmicrotime();
		@set_time_limit(0);
	}

	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;
		}
		$onBeforeStartResult = $this->message->onBeforeStart();
		if ($onBeforeStartResult->isSuccess())
		{
			$this->status = PostingTable::STATUS_PART;
			Model\PostingTable::update($this->postingId, ['STATUS' => $this->status]);
		}
		else
		{
			static::updateActualStatus($this->postingId, true);
		}

		$errorMessage = implode(', ', $onBeforeStartResult->getErrorMessages());
		if($errorMessage <> '')
		{
			Model\LetterTable::update($this->letterId, ['ERROR_MESSAGE' => $errorMessage]);
		}
	}

	/**
	 * 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 = [
			'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;
		}

		Model\PostingTable::update($postingId, $postingUpdateFields);

		return $status;
	}

	/**
	 * Lock posting for preventing double sending
	 *
	 * @param integer $id ID.
	 * @param $threadId default 0
	 *
	 * @return bool
	 * @throws Main\Db\SqlQueryException
	 * @throws Main\SystemException
	 */
	public static function lock($id, $threadId = 0)
	{
		$id       = intval($id);
		$threadId = intval($threadId);

		$uniqueSalt = self::getLockUniqueSalt();
		$connection = Application::getInstance()->getConnection();
		if ($connection instanceof DB\MysqlCommonConnection)
		{
			$lockDb = $connection->query(
				sprintf(
					"SELECT GET_LOCK('%s_sendpost_%d_%d', 0) as L",
					$uniqueSalt,
					$id,
					$threadId
				),
				false,
				"File: ".__FILE__."<br>Line: ".__LINE__
			);
			$lock   = $lockDb->fetch();
			if ($lock["L"] == "1")
			{
				return true;
			}
			else
			{
				return false;
			}
		}

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

	/**
	 * Check transport limits.
	 *
	 * @return bool
	 */
	public function isTransportLimitsExceeded()
	{
		return $this->message->getTransport()->isLimitsExceeded($this->message);
	}

	protected function prevent()
	{
		return $this->isPrevented = true;
	}

	/**
	 * @param $recipients
	 *
	 * @return bool
	 * @throws \Exception
	 */
	private function sendToRecipients($recipients)
	{
		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;
			Model\Posting\RecipientTable::update(
				$recipient["ID"],
				[
					'STATUS'    => $sendResultStatus,
					'DATE_SENT' => new Type\DateTime()
				]
			);

			// send event
			$eventData = [
				'SEND_RESULT' => $sendResult,
				'RECIPIENT'   => $recipient,
				'POSTING'     => [
					'ID'               => $this->postingId,
					'STATUS'           => $this->status,
					'MAILING_ID'       => $this->mailingId,
					'MAILING_CHAIN_ID' => $this->letterId,
				]
			];
			$event     = new Event('sender', 'OnAfterPostingSendRecipient', [$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++;
		}

		return $sendResult;
	}

	protected function isPrevented()
	{
		return $this->isPrevented;
	}

	protected function isStoppedOnRun()
	{
		// check pause or stop status
		if (++$this->checkStatusCounter < $this->checkStatusStep)
		{
			return false;
		}

		$checkStatusDb = Model\LetterTable::getList(
			[
				'select' => ['ID'],
				'filter' => [
					'=ID'     => $this->letterId,
					'=STATUS' => Model\LetterTable::STATUS_SEND
				]
			]
		);
		if (!$checkStatusDb->fetch())
		{
			return true;
		}

		$this->checkStatusCounter = 0;

		return false;
	}

	protected function setPostingDateSend()
	{
		if ($this->letter->get('DATE_SEND'))
		{
			return;
		}

		Model\PostingTable::update($this->postingId, ['DATE_SEND' => new Type\DateTime()]);
	}

	protected function sendToRecipient($recipient)
	{
		self::applyRecipientToMessage($this->message, $recipient);

		// event before sending
		$eventSendParams = [
			'FIELDS'           => $this->message->getFields(),
			'TRACK_READ'       => $this->message->getReadTracker()->getArray(),
			'TRACK_CLICK'      => $this->message->getClickTracker()->getArray(),
			'MAILING_CHAIN_ID' => $this->letter->getId()
		];
		$linkDomain      = $this->message->getReadTracker()->getLinkDomain();
		if ($linkDomain)
		{
			$eventSendParams['LINK_DOMAIN'] = $linkDomain;
		}
		$event = new Main\Event('sender', 'OnBeforePostingSendRecipient', [$eventSendParams]);
		$event->send();
		foreach ($event->getResults() as $eventResult)
		{
			if ($eventResult->getType() == Main\EventResult::ERROR)
			{
				return false;
			}

			if (is_array($eventResult->getParameters()))
			{
				$eventSendParams = array_merge($eventSendParams, $eventResult->getParameters());
			}
		}
		if (count($event->getResults()) > 0)
		{
			$this->message->setFields($eventSendParams['FIELDS']);
			$this->message->getReadTracker()->setArray($eventSendParams['TRACK_READ']);
			$this->message->getReadTracker()->setArray($eventSendParams['TRACK_CLICK']);
		}

		try
		{
			$sendResult = $this->message->send();
		}
		catch (Main\Mail\StopException $e)
		{
			$sendResult = false;
			$this->prevent();
		}

		return $sendResult;
	}

	/**
	 * 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)
	{
		$siteId = MailingTable::getMailingSiteId($recipient['CAMPAIGN_ID'] ? : Campaign::getDefaultId(SITE_ID));
		$message->getReadTracker()->setModuleId('sender')->setFields(['RECIPIENT_ID' => $recipient["ID"]])
				->setHandlerUri(Option::get('sender', 'read_link'))->setSiteId($siteId);
		$message->getClickTracker()->setModuleId('sender')->setFields(['RECIPIENT_ID' => $recipient["ID"]])
				->setUriParameters(['bx_sender_conversion_id' => $recipient["ID"]])->setHandlerUri(
				Option::get('sender', 'click_link')
			)->setSiteId($siteId);
		$message->getUnsubTracker()->setModuleId('sender')->setFields(
			[
				'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'))->setSiteId($siteId);

		$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 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 = [
			'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;
	}

	/**
	 * 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);
	}

	/**
	 * UnLock posting that was locking for preventing double sending
	 *
	 * @param integer $id ID.
	 * @param int $threadId
	 *
	 * @return bool
	 * @throws Main\Db\SqlQueryException
	 * @throws Main\SystemException
	 */
	public static function unlock($id, $threadId = 0)
	{
		$id       = intval($id);
		$threadId = intval($threadId);

		$connection = Application::getInstance()->getConnection();
		if ($connection instanceof DB\MysqlCommonConnection)
		{
			$uniqueSalt = self::getLockUniqueSalt(false);
			if (!$uniqueSalt)
			{
				return false;
			}

			$lockDb = $connection->query(
				sprintf(
					"SELECT RELEASE_LOCK('%s_sendpost_%d_%d') as L",
					$uniqueSalt,
					$id,
					$threadId
				)
			);
			$lock   = $lockDb->fetch();
			if ($lock["L"] == "0")
			{
				return false;
			}
			else
			{
				return true;
			}
		}

		return false;
	}

	/**
	 * @return IThreadStrategy
	 */
	public function getThreadStrategy(): IThreadStrategy
	{
		return $this->threadStrategy;
	}

	/**
	 * @param IThreadStrategy $threadStrategy
	 *
	 * @return Sender
	 */
	public function setThreadStrategy(IThreadStrategy $threadStrategy): Sender
	{
		$this->threadStrategy = $threadStrategy;
		return $this;
	}
}