<?php
/**
* Item Name: Ultimate SMS - Bulk SMS Application For Marketing
* Author: Codeglen
* Author URL: https://codecanyon.net/user/codeglen
*/
namespace App\Library;
use stdClass;
class SMSCounter
{
/**
* GSM 7BIT encoding name.
*
* @var string
*/
const GSM_7BIT = 'GSM_7BIT';
/**
* GSM 7BIT Extended encoding name.
*
* @var string
*/
const GSM_7BIT_EX = 'GSM_7BIT_EX';
/**
* UTF16 or UNICODE encoding name.
*
* @var string
*/
const UTF16 = 'UTF16';
/**
* Message length for GSM 7 Bit charset.
*
* @var int
*/
const GSM_7BIT_LEN = 160;
/**
* Message length for GSM 7 Bit charset with extended characters.
*
* @var int
*/
const GSM_7BIT_EX_LEN = 160;
/**
* Message length for UTF16/Unicode charset.
*
* @var int
*/
const UTF16_LEN = 70;
/**
* Message length for multipart message in GSM 7 Bit encoding.
*
* @var int
*/
const GSM_7BIT_LEN_MULTIPART = 153;
/**
* Message length for multipart message in GSM 7 Bit encoding.
*
* @var int
*/
const GSM_7BIT_EX_LEN_MULTIPART = 153;
/**
* Message length for multipart message in GSM 7 Bit encoding.
*
* @var int
*/
const UTF16_LEN_MULTIPART = 67;
/**
* @return int[]
*/
public function getGsm7bitMap(): array
{
return [
10, 12, 13, 32, 33, 34, 35, 36,
37, 38, 39, 40, 41, 42, 43, 44,
45, 46, 47, 48, 49, 50, 51, 52,
53, 54, 55, 56, 57, 58, 59, 60,
61, 62, 63, 64, 65, 66, 67, 68,
69, 70, 71, 72, 73, 74, 75, 76,
77, 78, 79, 80, 81, 82, 83, 84,
85, 86, 87, 88, 89, 90, 91, 92,
93, 94, 95, 97, 98, 99, 100, 101,
102, 103, 104, 105, 106, 107, 108,
109, 110, 111, 112, 113, 114, 115,
116, 117, 118, 119, 120, 121, 122,
123, 124, 125, 126, 161, 163, 164,
165, 167, 191, 196, 197, 198, 199,
201, 209, 214, 216, 220, 223, 224,
228, 229, 230, 232, 233, 236, 241,
242, 246, 248, 249, 252, 915, 916,
920, 923, 926, 928, 931, 934, 936,
937, 8364,
];
}
/**
* @return int[]
*/
public function getAddedGsm7bitExMap(): array
{
return [12, 91, 92, 93, 94, 123, 124, 125, 126, 8364];
}
/**
* @return array
*/
public function getGsm7bitExMap(): array
{
return array_merge(
$this->getGsm7bitMap(),
$this->getAddedGsm7bitExMap()
);
}
/**
* @return int[]
*/
public function getTurkishGsm7bitMap(): array
{
return [
10, 12, 13, 32, 33, 34, 35, 36,
37, 38, 39, 40, 41, 42, 43, 44,
45, 46, 47, 48, 49, 50, 51, 52,
53, 54, 55, 56, 57, 58, 59, 60,
61, 62, 63, 64, 65, 66, 67, 68,
69, 70, 71, 72, 73, 74, 75, 76,
77, 78, 79, 80, 81, 82, 83, 84,
85, 86, 87, 88, 89, 90, 91, 92,
93, 94, 95, 97, 98, 99, 100, 101,
102, 103, 104, 105, 106, 107, 108,
109, 110, 111, 112, 113, 114, 115,
116, 117, 118, 119, 120, 121, 122,
123, 124, 125, 126, 163, 164, 165,
167, 196, 197, 199, 201, 209, 214,
220, 223, 224, 228, 229, 231, 233,
241, 242, 246, 249, 252, 286, 287,
304, 305, 350, 351, 915, 916, 920,
923, 926, 928, 931, 934, 936, 937,
8364,
];
}
/**
* @return int[]
*/
public function getAddedTurkishGsm7bitExMap(): array
{
return [12, 91, 92, 93, 94, 123, 124, 125, 126, 286, 287, 304, 305, 350, 351, 8364];
}
/**
* @return int[]
*/
public function getAddedSpanishGsm7bitExMap(): array
{
return [12, 91, 92, 93, 94, 123, 124, 125, 126, 193, 205, 211, 218, 225, 231, 237, 243, 250, 8364];
}
/**
* @return int[]
*/
public function getPortugueseGsm7bitMap(): array
{
return [
10, 12, 13, 32, 33, 34, 35, 36,
37, 38, 39, 40, 41, 42, 43, 44,
45, 46, 47, 48, 49, 50, 51, 52,
53, 54, 55, 56, 57, 58, 59, 60,
61, 62, 63, 64, 65, 66, 67, 68,
69, 70, 71, 72, 73, 74, 75, 76,
77, 78, 79, 80, 81, 82, 83, 84,
85, 86, 87, 88, 89, 90, 91, 92,
93, 94, 95, 96, 97, 98, 99, 100,
101, 102, 103, 104, 105, 106, 107, 108,
109, 110, 111, 112, 113, 114, 115, 116,
117, 118, 119, 120, 121, 122, 123, 124,
125, 126, 163, 165, 167, 170, 186, 192,
193, 194, 195, 199, 201, 202, 205, 211,
212, 213, 218, 220, 224, 225, 226, 227,
231, 233, 234, 237, 242, 243, 244, 245,
250, 252, 915, 916, 920, 928, 931, 934,
936, 937, 8364, 8734,
];
}
/**
* @return int[]
*/
public function getAddedPortugueseGsm7bitExMap(): array
{
return [
12, 91, 92, 93, 94, 123, 124, 125,
126, 193, 194, 195, 202, 205, 211, 212,
213, 218, 225, 226, 227, 231, 234, 237,
242, 243, 245, 250, 915, 920, 928, 931,
934, 936, 937, 8364,
];
}
/**
* Detects the encoding, Counts the characters, message length, remaining characters.
*
* @return stdClass Object with params encoding,length, per_message, remaining, messages
*/
public function count($text): stdClass
{
return $this->doCount($text, false);
}
/**
* Detects the encoding, Counts the characters, message length, remaining characters.
* Supports language shift tables characters.
*
* @return stdClass Object with params encoding,length, per_message, remaining, messages
*/
public function countWithShiftTables($text): stdClass
{
return $this->doCount($text, true);
}
/**
* @return stdClass Object with params encoding,length, per_message, remaining, messages
*/
private function doCount($text, $supportShiftTables): stdClass
{
$unicodeArray = $this->utf8ToUnicode($text);
// variable to catch if any ex chars while encoding detection.
$exChars = [];
$encoding = $supportShiftTables
? $this->detectEncodingWithShiftTables($text, $exChars)
: $this->detectEncoding($text, $exChars);
$length = count($unicodeArray);
if ($encoding === self::GSM_7BIT_EX) {
$lengthExtrachars = count($exChars);
// Each exchar in the GSM 7 Bit encoding takes one more space
// Hence the length increases by one char for each of those Ex chars.
$length += $lengthExtrachars;
} elseif ($encoding === self::UTF16) {
// Unicode chars over U+10000 occupy an extra byte
$lengthExtra = array_reduce(
$unicodeArray,
function ($carry, $char) {
if ($char >= 65536) {
$carry++;
}
return $carry;
},
0
);
$length += $lengthExtra;
}
// Select the per message length according to encoding and the message length
switch ($encoding) {
case self::GSM_7BIT:
$perMessage = self::GSM_7BIT_LEN;
if ($length > self::GSM_7BIT_LEN) {
$perMessage = self::GSM_7BIT_LEN_MULTIPART;
}
break;
case self::GSM_7BIT_EX:
$perMessage = self::GSM_7BIT_EX_LEN;
if ($length > self::GSM_7BIT_EX_LEN) {
$perMessage = self::GSM_7BIT_EX_LEN_MULTIPART;
}
break;
default:
$perMessage = self::UTF16_LEN;
if ($length > self::UTF16_LEN) {
$perMessage = self::UTF16_LEN_MULTIPART;
}
break;
}
$messages = (int) ceil($length / $perMessage);
if ($encoding === self::UTF16 && $length > $perMessage) {
$count = 0;
foreach ($unicodeArray as $char) {
if ($count === $perMessage) {
$count = 0;
} elseif ($count > $perMessage) {
$count = 2;
}
$count += $char >= 65536 ? 2 : 1;
}
$remaining = $perMessage - ($count > $perMessage ? 2 : $count);
} else {
$remaining = ($perMessage * $messages) - $length;
}
$return_set = new stdClass();
$return_set->encoding = $encoding;
$return_set->length = $length;
$return_set->per_message = $perMessage;
$return_set->remaining = $remaining;
$return_set->messages = $messages;
return $return_set;
}
/**
* Detects the encoding of a particular text.
*
* @return string (GSM_7BIT|GSM_7BIT_EX|UTF16)
*/
public function detectEncoding($text, &$exChars): string
{
if ( ! is_array($text)) {
$text = $this->utf8ToUnicode($text);
}
$utf16Chars = array_diff($text, $this->getGsm7bitExMap());
if (count($utf16Chars)) {
return self::UTF16;
}
$exChars = array_intersect($text, $this->getAddedGsm7bitExMap());
if (count($exChars)) {
return self::GSM_7BIT_EX;
}
return self::GSM_7BIT;
}
/**
* Detects the encoding of a particular text.
* Supports language shift tables characters.
*
* @return string (GSM_7BIT|GSM_7BIT_EX|UTF16)
*/
public function detectEncodingWithShiftTables($text, &$exChars): string
{
if ( ! is_array($text)) {
$text = $this->utf8ToUnicode($text);
}
$gsmCharMap = array_merge(
$this->getGsm7bitExMap(),
$this->getTurkishGsm7bitMap(),
$this->getAddedTurkishGsm7bitExMap(),
$this->getAddedSpanishGsm7bitExMap(),
$this->getPortugueseGsm7bitMap(),
$this->getAddedPortugueseGsm7bitExMap()
);
$utf16Chars = array_diff($text, $gsmCharMap);
if (count($utf16Chars)) {
return self::UTF16;
}
$addedGsmCharMap = array_merge(
$this->getAddedGsm7bitExMap(),
$this->getAddedTurkishGsm7bitExMap(),
$this->getAddedSpanishGsm7bitExMap(),
$this->getAddedPortugueseGsm7bitExMap()
);
$exChars = array_intersect($text, $addedGsmCharMap);
if (count($exChars)) {
return self::GSM_7BIT_EX;
}
return self::GSM_7BIT;
}
/**
* Generates array of unicode points for the utf8 string.
*
* @param $str
*
* @return array
*/
public function utf8ToUnicode($str): array
{
$unicode = [];
$values = [];
$lookingFor = 1;
$len = strlen($str);
for ($i = 0; $i < $len; $i++) {
$thisValue = ord($str[$i]);
if ($thisValue < 128) {
$unicode[] = $thisValue;
}
if ($thisValue >= 128) {
if (count($values) === 0) {
$lookingFor = 2;
if ($thisValue >= 240) {
$lookingFor = 4;
} elseif ($thisValue >= 224) {
$lookingFor = 3;
}
}
$values[] = $thisValue;
if (count($values) === $lookingFor) {
switch ($lookingFor) {
case 4:
$number = (($values[0] % 16) * 262144) + (($values[1] % 64) * 4096) + (($values[2] % 64) * 64) + ($values[3] % 64);
break;
case 3:
$number = (($values[0] % 16) * 4096) + (($values[1] % 64) * 64) + ($values[2] % 64);
break;
case 2:
$number = (($values[0] % 32) * 64) + ($values[1] % 64);
break;
}
/** @var TYPE_NAME $number */
$unicode[] = $number;
$values = [];
$lookingFor = 1;
}
}
}
return $unicode;
}
/**
* Unicode equivalent chr() function.
*
* @return string characters
*/
public function utf8Chr($unicode): string
{
$unicode = intval($unicode);
$utf8char = chr(240 | ($unicode >> 18));
$utf8char .= chr(128 | (($unicode >> 12) & 0x3F));
$utf8char .= chr(128 | (($unicode >> 6) & 0x3F));
$utf8char .= chr(128 | ($unicode & 0x3F));
if ($unicode < 128) {
$utf8char = chr($unicode);
} elseif ($unicode >= 128 && $unicode < 2048) {
$utf8char = chr(192 | ($unicode >> 6)).chr(128 | ($unicode & 0x3F));
} elseif ($unicode >= 2048 && $unicode < 65536) {
$utf8char = chr(224 | ($unicode >> 12)).chr(128 | (($unicode >> 6) & 0x3F)).chr(128 | ($unicode & 0x3F));
}
return $utf8char;
}
/**
* Converts unicode code points array to a utf8 str.
*
* @param array $array unicode codepoints array
*
* @return string utf8 encoded string
*/
public function unicodeToUtf8($array): string
{
$str = '';
foreach ($array as $a) {
$str .= $this->utf8Chr($a);
}
return $str;
}
/**
* Removes non GSM characters from a string.
*
* @return string
*/
public function removeNonGsmChars($str)
{
return $this->replaceNonGsmChars($str);
}
/**
* Replaces non GSM characters from a string.
*
* @param string $str String to be replaced
* @param null $replacement String of characters to be replaced with
*
* @return false|string (string|false) if replacement string is more than 1 character
* in length
*/
public function replaceNonGsmChars($str, $replacement = null)
{
$validChars = $this->getGsm7bitExMap();
$allChars = $this->utf8ToUnicode($str);
if (strlen($replacement) > 1) {
return false;
}
$replacementArray = [];
$unicodeArray = $this->utf8ToUnicode($replacement);
$replacementUnicode = array_pop($unicodeArray);
foreach ($allChars as $key => $char) {
if ( ! in_array($char, $validChars)) {
$replacementArray[] = $key;
}
}
if ($replacement) {
foreach ($replacementArray as $key) {
$allChars[$key] = $replacementUnicode;
}
}
if ( ! $replacement) {
foreach ($replacementArray as $key) {
unset($allChars[$key]);
}
}
return $this->unicodeToUtf8($allChars);
}
/**
* @param $str
*
* @return false|string
*/
public function sanitizeToGSM($str)
{
$str = $this->removeAccents($str);
return $this->removeNonGsmChars($str);
}
/**
* @param string $str Message text
*
* @return string Sanitized message text
*/
public function removeAccents($str): string
{
if ( ! preg_match('/[\x80-\xff]/', $str)) {
return $str;
}
$chars = [
// Decompositions for Latin-1 Supplement
'ª' => 'a', 'º' => 'o',
'À' => 'A', 'Á' => 'A',
'Â' => 'A', 'Ã' => 'A',
'È' => 'E',
'Ê' => 'E', 'Ë' => 'E',
'Ì' => 'I', 'Í' => 'I',
'Î' => 'I', 'Ï' => 'I',
'Ð' => 'D',
'Ò' => 'O', 'Ó' => 'O',
'Ô' => 'O', 'Õ' => 'O',
'Ù' => 'U',
'Ú' => 'U', 'Û' => 'U',
'Ý' => 'Y',
'Þ' => 'TH',
'á' => 'a',
'â' => 'a', 'ã' => 'a',
'ç' => 'c',
'ê' => 'e', 'ë' => 'e',
'í' => 'i',
'î' => 'i', 'ï' => 'i',
'ð' => 'd',
'ó' => 'o',
'ô' => 'o', 'õ' => 'o',
'ú' => 'u',
'û' => 'u',
'ý' => 'y', 'þ' => 'th',
'ÿ' => 'y',
// Decompositions for Latin Extended-A
'Ā' => 'A', 'ā' => 'a',
'Ă' => 'A', 'ă' => 'a',
'Ą' => 'A', 'ą' => 'a',
'Ć' => 'C', 'ć' => 'c',
'Ĉ' => 'C', 'ĉ' => 'c',
'Ċ' => 'C', 'ċ' => 'c',
'Č' => 'C', 'č' => 'c',
'Ď' => 'D', 'ď' => 'd',
'Đ' => 'D', 'đ' => 'd',
'Ē' => 'E', 'ē' => 'e',
'Ĕ' => 'E', 'ĕ' => 'e',
'Ė' => 'E', 'ė' => 'e',
'Ę' => 'E', 'ę' => 'e',
'Ě' => 'E', 'ě' => 'e',
'Ĝ' => 'G', 'ĝ' => 'g',
'Ğ' => 'G', 'ğ' => 'g',
'Ġ' => 'G', 'ġ' => 'g',
'Ģ' => 'G', 'ģ' => 'g',
'Ĥ' => 'H', 'ĥ' => 'h',
'Ħ' => 'H', 'ħ' => 'h',
'Ĩ' => 'I', 'ĩ' => 'i',
'Ī' => 'I', 'ī' => 'i',
'Ĭ' => 'I', 'ĭ' => 'i',
'Į' => 'I', 'į' => 'i',
'İ' => 'I', 'ı' => 'i',
'IJ' => 'IJ', 'ij' => 'ij',
'Ĵ' => 'J', 'ĵ' => 'j',
'Ķ' => 'K', 'ķ' => 'k',
'ĸ' => 'k', 'Ĺ' => 'L',
'ĺ' => 'l', 'Ļ' => 'L',
'ļ' => 'l', 'Ľ' => 'L',
'ľ' => 'l', 'Ŀ' => 'L',
'ŀ' => 'l', 'Ł' => 'L',
'ł' => 'l', 'Ń' => 'N',
'ń' => 'n', 'Ņ' => 'N',
'ņ' => 'n', 'Ň' => 'N',
'ň' => 'n', 'ʼn' => 'n',
'Ŋ' => 'N', 'ŋ' => 'n',
'Ō' => 'O', 'ō' => 'o',
'Ŏ' => 'O', 'ŏ' => 'o',
'Ő' => 'O', 'ő' => 'o',
'Œ' => 'OE', 'œ' => 'oe',
'Ŕ' => 'R', 'ŕ' => 'r',
'Ŗ' => 'R', 'ŗ' => 'r',
'Ř' => 'R', 'ř' => 'r',
'Ś' => 'S', 'ś' => 's',
'Ŝ' => 'S', 'ŝ' => 's',
'Ş' => 'S', 'ş' => 's',
'Š' => 'S', 'š' => 's',
'Ţ' => 'T', 'ţ' => 't',
'Ť' => 'T', 'ť' => 't',
'Ŧ' => 'T', 'ŧ' => 't',
'Ũ' => 'U', 'ũ' => 'u',
'Ū' => 'U', 'ū' => 'u',
'Ŭ' => 'U', 'ŭ' => 'u',
'Ů' => 'U', 'ů' => 'u',
'Ű' => 'U', 'ű' => 'u',
'Ų' => 'U', 'ų' => 'u',
'Ŵ' => 'W', 'ŵ' => 'w',
'Ŷ' => 'Y', 'ŷ' => 'y',
'Ÿ' => 'Y', 'Ź' => 'Z',
'ź' => 'z', 'Ż' => 'Z',
'ż' => 'z', 'Ž' => 'Z',
'ž' => 'z', 'ſ' => 's',
// Decompositions for Latin Extended-B
'Ș' => 'S', 'ș' => 's',
'Ț' => 'T', 'ț' => 't',
// Vowels with diacritic (Vietnamese)
// unmarked
'Ơ' => 'O', 'ơ' => 'o',
'Ư' => 'U', 'ư' => 'u',
// grave accent
'Ầ' => 'A', 'ầ' => 'a',
'Ằ' => 'A', 'ằ' => 'a',
'Ề' => 'E', 'ề' => 'e',
'Ồ' => 'O', 'ồ' => 'o',
'Ờ' => 'O', 'ờ' => 'o',
'Ừ' => 'U', 'ừ' => 'u',
'Ỳ' => 'Y', 'ỳ' => 'y',
// hook
'Ả' => 'A', 'ả' => 'a',
'Ẩ' => 'A', 'ẩ' => 'a',
'Ẳ' => 'A', 'ẳ' => 'a',
'Ẻ' => 'E', 'ẻ' => 'e',
'Ể' => 'E', 'ể' => 'e',
'Ỉ' => 'I', 'ỉ' => 'i',
'Ỏ' => 'O', 'ỏ' => 'o',
'Ổ' => 'O', 'ổ' => 'o',
'Ở' => 'O', 'ở' => 'o',
'Ủ' => 'U', 'ủ' => 'u',
'Ử' => 'U', 'ử' => 'u',
'Ỷ' => 'Y', 'ỷ' => 'y',
// tilde
'Ẫ' => 'A', 'ẫ' => 'a',
'Ẵ' => 'A', 'ẵ' => 'a',
'Ẽ' => 'E', 'ẽ' => 'e',
'Ễ' => 'E', 'ễ' => 'e',
'Ỗ' => 'O', 'ỗ' => 'o',
'Ỡ' => 'O', 'ỡ' => 'o',
'Ữ' => 'U', 'ữ' => 'u',
'Ỹ' => 'Y', 'ỹ' => 'y',
// acute accent
'Ấ' => 'A', 'ấ' => 'a',
'Ắ' => 'A', 'ắ' => 'a',
'Ế' => 'E', 'ế' => 'e',
'Ố' => 'O', 'ố' => 'o',
'Ớ' => 'O', 'ớ' => 'o',
'Ứ' => 'U', 'ứ' => 'u',
// dot below
'Ạ' => 'A', 'ạ' => 'a',
'Ậ' => 'A', 'ậ' => 'a',
'Ặ' => 'A', 'ặ' => 'a',
'Ẹ' => 'E', 'ẹ' => 'e',
'Ệ' => 'E', 'ệ' => 'e',
'Ị' => 'I', 'ị' => 'i',
'Ọ' => 'O', 'ọ' => 'o',
'Ộ' => 'O', 'ộ' => 'o',
'Ợ' => 'O', 'ợ' => 'o',
'Ụ' => 'U', 'ụ' => 'u',
'Ự' => 'U', 'ự' => 'u',
'Ỵ' => 'Y', 'ỵ' => 'y',
// Vowels with diacritic (Chinese, Hanyu Pinyin)
'ɑ' => 'a',
// macron
'Ǖ' => 'U', 'ǖ' => 'u',
// acute accent
'Ǘ' => 'U', 'ǘ' => 'u',
// caron
'Ǎ' => 'A', 'ǎ' => 'a',
'Ǐ' => 'I', 'ǐ' => 'i',
'Ǒ' => 'O', 'ǒ' => 'o',
'Ǔ' => 'U', 'ǔ' => 'u',
'Ǚ' => 'U', 'ǚ' => 'u',
// grave accent
'Ǜ' => 'U', 'ǜ' => 'u',
// spaces
' ' => ' ', ' ' => ' ',
];
return strtr($str, $chars);
}
/**
* Truncated to the limit of chars allowed by number of SMS. It will detect
* the encoding an multipart limits to apply the truncate.
*
* @param string $str Message text
* @param int $limitSms Number of SMS allowed
*
* @return string Truncated message
*/
public function truncate($str, $limitSms): string
{
return $this->doTruncate($str, $limitSms, false);
}
/**
* Truncated to the limit of chars allowed by number of SMS. It will detect
* the encoding an multipart limits to apply the truncate.
* Supports language shift tables characters.
*
* @param string $str Message text
* @param int $limitSms Number of SMS allowed
*
* @return string Truncated message
*/
public function truncateWithShiftTables($str, $limitSms): string
{
return $this->doTruncate($str, $limitSms, true);
}
/**
* @return string Truncated message
*/
private function doTruncate($str, $limitSms, $supportShiftTables): string
{
$count = $supportShiftTables
? $this->countWithShiftTables($str)
: $this->count($str);
if ($count->messages <= $limitSms) {
return $str;
}
if ($count->encoding === 'UTF16') {
$limit = self::UTF16_LEN;
if ($limitSms > 2) {
$limit = self::UTF16_LEN_MULTIPART;
}
} else {
$limit = self::GSM_7BIT_LEN;
if ($limitSms > 2) {
$limit = self::GSM_7BIT_LEN_MULTIPART;
}
}
do {
$str = mb_substr($str, 0, $limit * $limitSms);
$count = $supportShiftTables
? $this->countWithShiftTables($str)
: $this->count($str);
$limit = $limit - 1;
} while ($count->messages > $limitSms);
return $str;
}
}