Back | Home
الـ Path الحالي: /home/picotech/domains/instantly.picotech.app/public_html/public/./../app/.././../../finland.picotech.app/public_html/storage/../vendor/./nikic/../dompdf/../alexandr-mironov/../monolog/../unicodeveloper/../dragonmantank/../telnyx/.././vlucas/.././phenx/../firebase/../maatwebsite/../alexandr-mironov/php8-smpp/src
الملفات الموجودة في هذا الـ Path:
.
..
Address.php
Client.php
Collection.php
DefaultLogger.php
DeliveryReceipt.php
Host.php
HostCollection.php
ItemInterface.php
LoggerAwareInterface.php
LoggerDecorator.php
LoggerInterface.php
Pdu.php
Smpp.php
Sms.php
Tag.php
exceptions
helpers
transport
مشاهدة ملف: Client.php
<?php
declare(strict_types=1);
namespace smpp;
use DateInterval;
use DateTime;
use Exception;
use smpp\exceptions\ClosedTransportException;
use smpp\exceptions\SmppException;
use smpp\exceptions\SmppInvalidArgumentException;
use smpp\exceptions\SocketTransportException;
use smpp\transport\Socket;
/**
* Class for receiving or sending sms through SMPP protocol.
* This is a reduced implementation of the SMPP protocol, and as such not all features will or ought to be available.
* The purpose is to create a lightweight and simplified SMPP client.
*
* @author hd@onlinecity.dk, paladin, Alexandr Mironov
* @see http://en.wikipedia.org/wiki/Short_message_peer-to-peer_protocol - SMPP 3.4 protocol specification
* Derived from work done by paladin, see: http://sourceforge.net/projects/phpsmppapi/
*
* Copyright (C) 2020 Alexandr Mironov
* Copyright (C) 2011 OnlineCity
* Copyright (C) 2006 Paladin
*
* This library is free software; you can redistribute it and/or modify it under the terms of
* the GNU Lesser General Public License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU Lesser General Public License for more details.
*
* This license can be read at: http://www.opensource.org/licenses/lgpl-2.1.php
*/
class Client
{
// Available modes
/** @var string */
public const MODE_TRANSMITTER = 'transmitter';
/** @var string */
public const MODE_TRANSCEIVER = 'transceiver';
/** @var string */
public const MODE_RECEIVER = 'receiver';
/** @var integer Use sar_msg_ref_num and sar_total_segments with 16 bit tags */
public const CSMS_16BIT_TAGS = 0;
/** @var integer Use message payload for CSMS */
public const CSMS_PAYLOAD = 1;
/** @var integer Embed a UDH in the message with 8-bit reference. */
public const CSMS_8BIT_UDH = 2;
// SMPP bind parameters
/** @var string */
public static string $systemType = "WWW";
/** @var int */
public static int $interfaceVersion = 0x34;
/** @var int */
public static int $addrTon = 0;
/** @var int */
public static int $addrNPI = 0;
/** @var string */
public static string $addressRange = "";
// ESME transmitter parameters
/** @var string */
public static string $smsServiceType = "";
/** @var int */
public static int $smsEsmClass = 0x00;
/** @var int */
public static int $smsProtocolID = 0x00;
/** @var int */
public static int $smsPriorityFlag = 0x00;
/** @var int */
public static int $smsRegisteredDeliveryFlag = 0x00;
/** @var int */
public static int $smsReplaceIfPresentFlag = 0x00;
/** @var int */
public static int $smsSmDefaultMessageID = 0x00;
/**
* SMPP v3.4 says octet string are "not necessarily NULL terminated".
* Switch to toggle this feature
* @var boolean
*
* set NULL terminate octetstrings FALSE as default
*/
public static bool $smsNullTerminateOctetstrings = false;
/** @var int */
public static int $csmsMethod = self::CSMS_16BIT_TAGS;
/** @var Pdu[] */
protected array $pduQueue = [];
// Used for reconnect
/** @var string */
protected string $mode;
/** @var string $login Login of SMPP gateway */
private string $login = '';
/** @var string $pass Password of SMPP gateway */
private string $pass = '';
/** @var int */
protected int $sequenceNumber = 1;
/** @var int */
protected int $sarMessageReferenceNumber;
/** @var LoggerDecorator */
public LoggerDecorator $logger;
/**
* Construct the SMPP class
*
* @param Socket $transport
* @param LoggerInterface ...$loggers
*/
public function __construct(
public Socket $transport,
LoggerInterface ...$loggers
)
{
LoggerDecorator::$debug = Socket::$defaultDebug;
$this->logger = new LoggerDecorator(...$loggers);
}
/**
* Binds the receiver. One object can be bound only as receiver or only as transmitter.
* @param string $login - ESME system_id
* @param string $pass - ESME password
*
* @return void
*
* @throws SmppException
* @throws ClosedTransportException
* @throws Exception
*/
public function bindReceiver(string $login, string $pass): void
{
if (!$this->transport->isOpen()) {
throw new ClosedTransportException();
}
$this->logger->info('Binding receiver...');
$response = $this->bind($login, $pass, Smpp::BIND_RECEIVER);
$this->logger->info("Binding status : " . $response->status);
$this->mode = self::MODE_RECEIVER;
$this->login = $login;
$this->pass = $pass;
}
/**
* Binds the transmitter. One object can be bound only as receiver or only as transmitter.
*
* @param string $login - ESME system_id
* @param string $pass - ESME password
*
* @return void
* @throws Exception
*/
public function bindTransmitter(string $login, string $pass): void
{
if (!$this->transport->isOpen()) {
throw new ClosedTransportException();
}
$this->logger->info('Binding transmitter...');
$response = $this->bind($login, $pass, Smpp::BIND_TRANSMITTER);
$this->logger->info("Binding status : " . $response->status);
$this->mode = self::MODE_TRANSMITTER;
$this->login = $login;
$this->pass = $pass;
}
/**
* Bind transceiver, this object bound as receiver and transmitter at same time,
* only if available in SMPP gateway
*
* @param string $login - ESME system_id
* @param string $pass - ESME password
*
* @return void
* @throws Exception
*/
public function bindTransceiver(string $login, string $pass): void
{
if (!$this->transport->isOpen()) {
throw new ClosedTransportException();
}
$this->logger->info('Binding transciever...');
$response = $this->bind($login, $pass, Smpp::BIND_TRANSCEIVER);
$this->logger->info("Binding status : " . $response->status);
$this->mode = self::MODE_TRANSCEIVER;
$this->login = $login;
$this->pass = $pass;
}
/**
* Closes the session on the SMSC server.
*
* @return void
* @throws Exception
*/
public function close(): void
{
if (!$this->transport->isOpen()) {
return;
}
$this->logger->info('Unbinding...');
$response = $this->sendCommand(Smpp::UNBIND, "");
$this->logger->info("Unbind status : " . $response->status);
$this->transport->close();
}
/**
* Parse a time string as formatted by SMPP v3.4 section 7.1.
* Returns an object of either DateTime or DateInterval is returned.
*
* @param string $input
*
* @return DateTime|DateInterval|null
*
* @throws Exception
*/
public function parseSmppTime(string $input): null|DateTime|DateInterval
{
if (
!preg_match(
'/^(\\d{2})(\\d{2})(\\d{2})(\\d{2})(\\d{2})(\\d{2})(\\d{1})(\\d{2})([R+-])$/',
$input,
$matches
)
) {
return null;
}
/**
* @var int $y
* @var int $m
* @var int $d
* @var int $h
* @var int $i
* @var int $s
* @var int $n
* @var string $p
*/
[$whole, $y, $m, $d, $h, $i, $s, $t, $n, $p] = $matches;
if ($p == 'R') {
$spec = "P";
if ($y) {
$spec .= $y . 'Y';
}
if ($m) {
$spec .= $m . 'M';
}
if ($d) {
$spec .= $d . 'D';
}
if ($h || $i || $s) {
$spec .= 'T';
}
if ($h) {
$spec .= $h . 'H';
}
if ($i) {
$spec .= $i . 'M';
}
if ($s) {
$spec .= $s . 'S';
}
return new DateInterval($spec);
} else {
$offsetHours = floor($n / 4);
$offsetMinutes = ($n % 4) * 15;
// Not Y3K safe
$time = sprintf(
"20%02s-%02s-%02sT%02s:%02s:%02s%s%02s:%02s",
$y,
$m,
$d,
$h,
$i,
$s,
$p,
$offsetHours,
$offsetMinutes
);
return new DateTime($time);
}
}
/**
* Query the SMSC about current state/status of a previous sent SMS.
* You must specify the SMSC assigned message id and source of the sent SMS.
* Returns an associative array with elements: message_id, final_date, message_state and error_code.
* message_state would be one of the SMPP::STATE_* constants. (SMPP v3.4 section 5.2.28)
* error_code depends on the telco network, so could be anything.
*
* @param string $messageID
* @param Address $source
*
* @return null|array<string, mixed>
*
* @throws Exception
*/
public function queryStatus(string $messageID, Address $source): null|array
{
$pduBody = pack(
'a' . (strlen($messageID) + 1) . 'cca' . (strlen($source->value) + 1),
$messageID,
$source->ton,
$source->npi,
$source->value
);
$reply = $this->sendCommand(Smpp::QUERY_SM, $pduBody);
if ($reply->status !== Smpp::ESME_ROK) {
return null;
}
// Parse reply
$posID = strpos($reply->body, "\0", 0);
$posDate = strpos($reply->body, "\0", $posID + 1);
if ($posID === false) {
// todo: replace exception and add message
throw new Exception();
}
$data = [
'message_id' => substr($reply->body, 0, $posID),
'final_date' => substr($reply->body, $posID, (int)$posDate - $posID),
];
$data['final_date'] = $data['final_date'] ? $this->parseSmppTime(trim($data['final_date'])) : null;
/** @var false|array{message_state: mixed, error_code: mixed} $status */
$status = unpack("cmessage_state/cerror_code", substr($reply->body, $posDate + 1));
if (!$status) {
// todo: replace exception and add message
throw new Exception();
}
return array_merge($data, $status);
}
/**
* Read one SMS from SMSC. Can be executed only after bindReceiver() call.
* This method blocks. Method returns on socket timeout or enquire_link signal from SMSC.
*
* @return DeliveryReceipt|Sms|bool
* @throws Exception
*/
public function readSMS(): bool|DeliveryReceipt|Sms
{
// Check the queue
$queueLength = count($this->pduQueue);
for ($i = 0; $i < $queueLength; $i++) {
$pdu = $this->pduQueue[$i];
if ($pdu->id === Smpp::DELIVER_SM) {
//remove response
array_splice($this->pduQueue, $i, 1);
return $this->parseSMS($pdu);
}
}
// Read pdu
do {
$pdu = $this->readPDU();
if ($pdu === false) {
return false;
} // TSocket v. 0.6.0+ returns false on timeout
//check for enquire link command
if ($pdu->id === Smpp::ENQUIRE_LINK) {
$response = new Pdu(Smpp::ENQUIRE_LINK_RESP, Smpp::ESME_ROK, $pdu->sequence, "\x00");
$this->sendPDU($response);
} else if ($pdu->id !== Smpp::DELIVER_SM) { // if this is not the correct PDU add to queue
array_push($this->pduQueue, $pdu);
}
} while ($pdu->id !== Smpp::DELIVER_SM);
return $this->parseSMS($pdu);
}
/**
* Send one SMS to SMSC. Can be executed only after bindTransmitter() call.
* $message is always in octets regardless of the data encoding.
* For correct handling of Concatenated SMS,
* message must be encoded with GSM 03.38 (data_coding 0x00) or UCS-2BE (0x08).
* Concatenated SMS'es uses 16-bit reference numbers, which gives 152 GSM 03.38 chars or 66 UCS-2BE chars per CSMS.
* If we are using 8-bit ref numbers in the UDH for CSMS it's 153 GSM 03.38 chars
*
* @param Address $from
* @param Address $to
* @param string $message
* @param Tag[]|null $tags (optional)
* @param int $dataCoding (optional)
* @param int $priority (optional)
* @param null $scheduleDeliveryTime (optional)
* @param null $validityPeriod (optional)
*
* @return bool|string message id
*
* @throws Exception
*/
public function sendSMS(
Address $from,
Address $to,
string $message,
array $tags = null,
int $dataCoding = Smpp::DATA_CODING_DEFAULT,
int $priority = 0x00,
$scheduleDeliveryTime = null,
$validityPeriod = null
): bool|string
{
$messageLength = strlen($message);
if ($messageLength > 160 && !in_array($dataCoding, [Smpp::DATA_CODING_UCS2, Smpp::DATA_CODING_DEFAULT])) {
return false;
}
switch ($dataCoding) {
case Smpp::DATA_CODING_UCS2:
// in octets, 70 UCS-2 chars
$singleSmsOctetLimit = 140;
// There are 133 octets available, but this would split the UCS the middle so use 132 instead
$csmsSplit = 132;
$message = mb_convert_encoding($message, 'UCS-2');
//Update message length with current encoding
$messageLength = mb_strlen($message);
break;
case Smpp::DATA_CODING_DEFAULT:
// we send data in octets, but GSM 03.38 will be packed in septets (7-bit) by SMSC.
$singleSmsOctetLimit = 160;
// send 152/153 chars in each SMS (SMSC will format data)
$csmsSplit = (self::$csmsMethod == self::CSMS_8BIT_UDH) ? 153 : 152;
break;
default:
$singleSmsOctetLimit = 254; // From SMPP standard
break;
}
// Figure out if we need to do CSMS, since it will affect our PDU
if ($messageLength > $singleSmsOctetLimit) {
$doCsms = true;
if (self::$csmsMethod != self::CSMS_PAYLOAD) {
$parts = $this->splitMessageString($message, $csmsSplit, $dataCoding);
$shortMessage = reset($parts);
$csmsReference = $this->getCsmsReference();
}
} else {
$shortMessage = $message;
$doCsms = false;
}
// Deal with CSMS
if ($doCsms) {
if (self::$csmsMethod == self::CSMS_PAYLOAD) {
$payload = new Tag(Tag::MESSAGE_PAYLOAD, $message, $messageLength);
// todo: replace array to k=>v storage (Collection??), where key is tag id
$tags[] = $payload;
return $this->submitShortMessage(
$from,
$to,
null,
$tags,
$dataCoding,
$priority,
$scheduleDeliveryTime,
$validityPeriod
);
} elseif (self::$csmsMethod == self::CSMS_8BIT_UDH) {
$sequenceNumber = 1;
foreach ($parts as $part) {
$udh = pack(
'cccccc',
5,
0,
3,
substr((string)$csmsReference, 1, 1),
count($parts),
$sequenceNumber
);
$res = $this->submitShortMessage(
$from,
$to,
$udh . $part,
$tags,
$dataCoding,
$priority,
$scheduleDeliveryTime,
$validityPeriod,
(string)(self::$smsEsmClass | 0x40) //todo: check this
);
$sequenceNumber++;
}
return $res;
} else {
$sarMessageRefNumber = new Tag(Tag::SAR_MSG_REF_NUM, $csmsReference, 2, 'n');
$sarTotalSegments = new Tag(Tag::SAR_TOTAL_SEGMENTS, count($parts), 1, 'c');
$sequenceNumber = 1;
foreach ($parts as $part) {
$sartags = [
$sarMessageRefNumber,
$sarTotalSegments,
new Tag(Tag::SAR_SEGMENT_SEQNUM, $sequenceNumber, 1, 'c')
];
$res = $this->submitShortMessage(
$from,
$to,
(string)$part,
(empty($tags) ? $sartags : array_merge($tags, $sartags)),
$dataCoding,
$priority,
$scheduleDeliveryTime,
$validityPeriod
);
$sequenceNumber++;
}
return $res;
}
}
return $this->submitShortMessage($from, $to, (string)($shortMessage ?? ''), $tags, $dataCoding, $priority);
}
/**
* Perform the actual submit_sm call to send SMS.
* Implemented as a protected method to allow automatic sms concatenation.
* Tags must be an array of already packed and encoded TLV-params.
*
* @param Address $source
* @param Address $destination
* @param string|null $shortMessage
* @param Tag[]|null $tags
* @param integer $dataCoding
* @param integer $priority
* @param string|null $scheduleDeliveryTime
* @param string|null $validityPeriod
* @param string|null $esmClass
*
* @return string message id
*
* @throws Exception
*/
protected function submitShortMessage(
Address $source,
Address $destination,
string $shortMessage = null,
array $tags = null,
int $dataCoding = Smpp::DATA_CODING_DEFAULT,
int $priority = 0x00,
string $scheduleDeliveryTime = null,
string $validityPeriod = null,
string $esmClass = null
): string
{
if (is_null($esmClass)) {
$esmClass = self::$smsEsmClass;
}
$shortMessageLength = strlen($shortMessage);
// Construct PDU with mandatory fields
$pdu = pack(
'a1cca' . (strlen($source->value) + 1)
. 'cca' . (strlen($destination->value) + 1)
. 'ccc' . ($scheduleDeliveryTime ? 'a16x' : 'a1') . ($validityPeriod ? 'a16x' : 'a1')
. 'ccccca' . ($shortMessageLength + (self::$smsNullTerminateOctetstrings ? 1 : 0)),
self::$smsServiceType,
$source->ton,
$source->npi,
$source->value,
$destination->ton,
$destination->npi,
$destination->value,
$esmClass,
self::$smsProtocolID,
$priority,
$scheduleDeliveryTime,
$validityPeriod,
self::$smsRegisteredDeliveryFlag,
self::$smsReplaceIfPresentFlag,
$dataCoding,
self::$smsSmDefaultMessageID,
$shortMessageLength, //sm_length
$shortMessage //short_message
);
// Add any tags
if (!empty($tags)) {
foreach ($tags as $tag) {
$pdu .= $tag->getBinary();
}
}
$response = $this->sendCommand(Smpp::SUBMIT_SM, $pdu);
/** @var array{msgid: string}|false $body */
$body = unpack("a*msgid", $response->body);
if (!$body) {
throw new SmppException('unable to unpack response body:' . $response->body);
}
return $body['msgid'];
}
/**
* Get a CSMS reference number for sar_msg_ref_num.
* Initializes with a random value, and then returns the number in sequence with each call.
*
* @return int
*/
protected function getCsmsReference(): int
{
$limit = (self::$csmsMethod == self::CSMS_8BIT_UDH) ? 255 : 65535;
if (!isset($this->sarMessageReferenceNumber)) {
$this->sarMessageReferenceNumber = mt_rand(0, $limit);
}
$this->sarMessageReferenceNumber++;
if ($this->sarMessageReferenceNumber > $limit) {
$this->sarMessageReferenceNumber = 0;
}
return $this->sarMessageReferenceNumber;
}
/**
* Split a message into multiple parts, taking the encoding into account.
* A character represented by an GSM 03.38 escape-sequence shall not be split in the middle.
* Uses str_split if at all possible, and will examine all split points for escape chars if it's required.
*
* @param string $message
* @param int<1,max> $split
* @param integer $dataCoding (optional)
*
* @return array<int|string>
*/
protected function splitMessageString(
string $message,
int $split,
int $dataCoding = Smpp::DATA_CODING_DEFAULT
): array
{
switch ($dataCoding) {
case Smpp::DATA_CODING_DEFAULT:
$messageLength = strlen($message);
// Do we need to do php based split?
$numParts = floor($messageLength / $split);
if ($messageLength % $split == 0) {
$numParts--;
}
$slowSplit = false;
for ($i = 1; $i <= $numParts; $i++) {
if ($message[$i * $split - 1] == "\x1B") {
$slowSplit = true;
break;
}
}
if (!$slowSplit) {
return str_split($message, $split);
}
// Split the message char-by-char
$parts = [];
$part = null;
$n = 0;
for ($i = 0; $i < $messageLength; $i++) {
$c = $message[$i];
// reset on $split or if last char is a GSM 03.38 escape char
if ($n == $split || ($n == ($split - 1) && $c == "\x1B")) {
$parts[] = $part;
$n = 0;
$part = null;
}
$part .= $c;
}
$parts[] = $part;
return $parts;
/**
* UCS2-BE can just use str_split since we send 132 octets per message,
* which gives a fine split using UCS2
*/
case Smpp::DATA_CODING_UCS2:
default:
return str_split($message, $split);
}
}
/**
* Binds the socket and opens the session on SMSC
*
* @param string $login - ESME system_id
* @param string $pass
* @param int $commandID (todo replace to ENUM in php 8.1)
*
* @return Pdu
*
* @throws Exception
*/
protected function bind(string $login, string $pass, int $commandID): Pdu
{
// Make PDU body
$pduBody = pack(
'a' . (strlen($login) + 1)
. 'a' . (strlen($pass) + 1)
. 'a' . (strlen(self::$systemType) + 1)
. 'CCCa' . (strlen(self::$addressRange) + 1),
$login,
$pass,
self::$systemType,
self::$interfaceVersion,
self::$addrTon,
self::$addrNPI,
self::$addressRange
);
$response = $this->sendCommand($commandID, $pduBody);
if ($response->status != Smpp::ESME_ROK) {
throw new SmppException(Smpp::getStatusMessage($response->status), $response->status);
}
return $response;
}
/**
* Parse received PDU from SMSC.
* @param Pdu $pdu - received PDU from SMSC.
*
* @return DeliveryReceipt|Sms parsed PDU as array.
*
* @throws Exception
*/
protected function parseSMS(Pdu $pdu): DeliveryReceipt|Sms
{
// Check command id
if ($pdu->id != Smpp::DELIVER_SM) {
throw new SmppInvalidArgumentException('PDU is not an received SMS');
}
// Unpack PDU
$ar = unpack("C*", $pdu->body);
if (!$ar) {
throw new SmppException(''); // todo: update message
}
// Read mandatory params
$serviceType = $this->getString($ar, 6, true);
//
$sourceAddrTon = next($ar);
$sourceAddrNPI = next($ar);
$sourceAddr = $this->getString($ar, 21);
$source = new Address($sourceAddr, $sourceAddrTon, $sourceAddrNPI);
//
$destinationAddrTon = next($ar);
$destinationAddrNPI = next($ar);
$destinationAddr = $this->getString($ar, 21);
$destination = new Address($destinationAddr, $destinationAddrTon, $destinationAddrNPI);
$esmClass = next($ar);
$protocolId = next($ar);
$priorityFlag = next($ar);
next($ar); // schedule_delivery_time
next($ar); // validity_period
$registeredDelivery = next($ar);
next($ar); // replace_if_present_flag
$dataCoding = next($ar);
next($ar); // sm_default_msg_id
$sm_length = next($ar);
$message = $this->getString($ar, $sm_length);
// Check for optional params, and parse them
if (current($ar) !== false) {
$tags = [];
do {
$tag = $this->parseTag($ar);
if ($tag !== false) {
$tags[] = $tag;
}
} while (current($ar) !== false);
} else {
$tags = null;
}
if (($esmClass & Smpp::ESM_DELIVER_SMSC_RECEIPT) != 0) {
$sms = new DeliveryReceipt(
$pdu->id,
$pdu->status,
$pdu->sequence,
$pdu->body,
$serviceType,
$source,
$destination,
$esmClass,
$protocolId,
$priorityFlag,
$registeredDelivery,
$dataCoding,
$message,
$tags
);
$sms->parseDeliveryReceipt();
} else {
$sms = new Sms(
$pdu->id,
$pdu->status,
$pdu->sequence,
$pdu->body,
$serviceType,
$source,
$destination,
$esmClass,
$protocolId,
$priorityFlag,
$registeredDelivery,
$dataCoding,
$message,
$tags
);
}
$this->logger->info("Received sms:\n" . print_r($sms, true));
// Send response of recieving sms
$response = new Pdu(Smpp::DELIVER_SM_RESP, Smpp::ESME_ROK, $pdu->sequence, "\x00");
$this->sendPDU($response);
return $sms;
}
/**
* Send the enquire link command.
* @return Pdu
* @throws Exception
*/
public function enquireLink(): Pdu
{
return $this->sendCommand(Smpp::ENQUIRE_LINK, null);
}
/**
* Respond to any enquire link we might have waiting.
* If will check the queue first and respond to any enquire links we have there.
* Then it will move on to the transport, and if the first PDU is enquire link respond,
* otherwise add it to the queue and return.
*
*/
public function respondEnquireLink(): void
{
// Check the queue first
$queueLength = count($this->pduQueue);
for ($i = 0; $i < $queueLength; $i++) {
$pdu = $this->pduQueue[$i];
if ($pdu->id == Smpp::ENQUIRE_LINK) {
//remove response
array_splice($this->pduQueue, $i, 1);
$this->sendPDU(new Pdu(Smpp::ENQUIRE_LINK_RESP, Smpp::ESME_ROK, $pdu->sequence, "\x00"));
}
}
// Check the transport for data
if ($this->transport->hasData()) {
$pdu = $this->readPDU();
if ($pdu && $pdu->id == Smpp::ENQUIRE_LINK) {
$this->sendPDU(new Pdu(Smpp::ENQUIRE_LINK_RESP, Smpp::ESME_ROK, $pdu->sequence, "\x00"));
} elseif ($pdu) {
array_push($this->pduQueue, $pdu);
}
}
}
/**
* Reconnect to SMSC.
* This is mostly to deal with the situation were we run out of sequence numbers
*
* @throws SmppException|Exception
*/
protected function reconnect(): void
{
$this->close();
sleep(1);
$this->transport->open();
$this->sequenceNumber = 1;
match ($this->mode) {
self::MODE_TRANSMITTER => $this->bindTransmitter($this->login, $this->pass),
self::MODE_RECEIVER => $this->bindReceiver($this->login, $this->pass),
self::MODE_TRANSCEIVER => $this->bindTransceiver($this->login, $this->pass),
default => throw new SmppException('Invalid mode: ' . $this->mode)
};
}
/**
* Sends the PDU command to the SMSC and waits for response.
* @param int $id - command ID
* @param ?string $pduBody - PDU body
* @return Pdu
*
* @throws Exception
*/
protected function sendCommand(int $id, ?string $pduBody): Pdu
{
if (!$this->transport->isOpen()) {
throw new SocketTransportException('Socket is closed');
//return false;
}
$pdu = new Pdu($id, 0, $this->sequenceNumber, $pduBody);
$this->sendPDU($pdu);
$response = $this->readPduResponse($this->sequenceNumber, $pdu->id);
if ($response === false) {
throw new SmppException('Failed to read reply to command: 0x' . dechex($id));
}
if ($response->status != Smpp::ESME_ROK) {
throw new SmppException(Smpp::getStatusMessage($response->status), $response->status);
}
$this->sequenceNumber++;
// Reached max sequence number, spec does not state what happens now, so we re-connect
if ($this->sequenceNumber >= 0x7FFFFFFF) {
$this->reconnect();
}
return $response;
}
/**
* Prepares and sends PDU to SMSC.
* @param Pdu $pdu
* @throws Exception
*/
protected function sendPDU(Pdu $pdu): void
{
$length = strlen($pdu->body) + 16;
$header = pack("NNNN", $length, $pdu->id, $pdu->status, $pdu->sequence);
$this->logger->info("Read PDU : $length bytes");
$this->logger->info(' ' . chunk_split(bin2hex($header . $pdu->body), 2, " "));
$this->logger->info(' command_id : 0x' . dechex($pdu->id));
$this->logger->info(' sequence number : ' . $pdu->sequence);
$this->transport->write($header . $pdu->body, $length);
}
/**
* Waits for SMSC response on specific PDU.
* If a GENERIC_NACK with a matching sequence number, or null sequence is received instead it's also accepted.
* Some SMPP servers, ie. logica returns GENERIC_NACK on errors.
*
* @param int $sequenceNumber - PDU sequence number
* @param int $commandID - PDU command ID
*
* @return Pdu|false
* @throws SmppException
*/
protected function readPduResponse(int $sequenceNumber, int $commandID): Pdu|false
{
// Get response cmd id from command ID
$commandID = $commandID | Smpp::GENERIC_NACK;
// Check the queue first
$queueLength = count($this->pduQueue);
for ($i = 0; $i < $queueLength; $i++) {
$pdu = $this->pduQueue[$i];
if (
($pdu->sequence == $sequenceNumber && ($pdu->id == $commandID || $pdu->id == Smpp::GENERIC_NACK))
||
($pdu->sequence == null && $pdu->id == Smpp::GENERIC_NACK)
) {
// remove response pdu from queue
array_splice($this->pduQueue, $i, 1);
return $pdu;
}
}
// Read PDUs until the one we are looking for shows up, or a generic nack pdu with matching sequence or null sequence
do {
$pdu = $this->readPDU();
if ($pdu) {
if (
$pdu->sequence == $sequenceNumber
&& ($pdu->id == $commandID || $pdu->id == Smpp::GENERIC_NACK)
) {
return $pdu;
}
if ($pdu->sequence == null && $pdu->id == Smpp::GENERIC_NACK) {
return $pdu;
}
array_push($this->pduQueue, $pdu); // unknown PDU push to queue
}
} while ($pdu);
return false;
}
/**
* Reads incoming PDU from SMSC.
* @return false|Pdu
*/
protected function readPDU(): Pdu|false
{
// Read PDU length
$bufLength = $this->transport->read(4);
if (!$bufLength) {
return false;
}
$extract = unpack("Nlength", $bufLength);
if (!$extract) {
throw new SmppException('unable to unpack string');
}
/**
* extraction define next variables:
* @var $length
*/
extract($extract);
// Read PDU headers
$bufHeaders = $this->transport->read(12);
if (!$bufHeaders) {
return false;
}
$extract = unpack("Ncommand_id/Ncommand_status/Nsequence_number", $bufHeaders);
if (!$extract) {
throw new SmppException('unable to unpack string');
}
/**
* @var $command_id
* @var $command_status
* @var $sequence_number
*/
extract($extract);
if (!isset($command_id, $command_status, $sequence_number, $length)) {
return false; // todo: maybe replace to exception??
}
// Read PDU body
$bodyLength = $length - 16;
if ($bodyLength > 0) {
if (!$body = $this->transport->readAll($bodyLength)) {
throw new SmppException('Could not read PDU body');
}
} else {
$body = null;
}
$this->logger->info("Read PDU : $length bytes");
$this->logger->info(' ' . chunk_split(bin2hex($bufLength . $bufHeaders . $body), 2, " "));
$this->logger->info(" command id : 0x" . dechex($command_id));
$this->logger->info(" command status : 0x" . dechex($command_status) . " " . Smpp::getStatusMessage($command_status));
$this->logger->info(' sequence number : ' . $sequence_number);
return new Pdu($command_id, $command_status, $sequence_number, $body);
}
/**
* Reads C style null padded string from the char array.
* Reads until $maxlen or null byte.
*
* @param array<mixed> $ar - input array
* @param integer $maxLength - maximum length to read.
* @param boolean $firstRead - is this the first bytes read from array?
* @return string.
*/
protected function getString(array &$ar, int $maxLength = 255, bool $firstRead = false): string
{
$s = "";
$i = 0;
do {
$c = ($firstRead && $i == 0) ? current($ar) : next($ar);
if ($c != 0) $s .= chr($c);
$i++;
} while ($i < $maxLength && $c != 0);
return $s;
}
/**
* Read a specific number of octets from the char array.
* Does not stop at null byte
*
* @param array<mixed> $ar - input array
* @param int $length
* @return string
*/
protected function getOctets(array &$ar, int $length): string
{
$s = "";
for ($i = 0; $i < $length; $i++) {
$c = next($ar);
if ($c === false) {
return $s;
}
$s .= chr($c);
}
return $s;
}
/**
* @param array<mixed> $ar
* @return false|Tag
*/
protected function parseTag(array &$ar): false|Tag
{
$unpackedData = unpack(
'nid/nlength',
pack("C2C2", next($ar), next($ar), next($ar), next($ar))
);
if (!$unpackedData) {
throw new SmppInvalidArgumentException('Could not read tag data');
}
/**
* Extraction create variables:
* @var $length
* @var $id
*/
extract($unpackedData);
// Sometimes SMSC return an extra null byte at the end
if (!isset($id, $length) || ($length == 0 && $id == 0)) {
return false;
}
$value = $this->getOctets($ar, $length);
$tag = new Tag($id, $value, $length);
$this->logger->info("Parsed tag:");
$this->logger->info(" id :0x" . dechex($tag->id));
$this->logger->info(" length :" . $tag->length);
$this->logger->info(" value :" . chunk_split(bin2hex((string)$tag->value), 2, " "));
return $tag;
}
}