diff --git a/common/framework/drivers/sms/base.php b/common/framework/drivers/sms/base.php index f48f0ff51..26e38ea62 100644 --- a/common/framework/drivers/sms/base.php +++ b/common/framework/drivers/sms/base.php @@ -12,6 +12,11 @@ abstract class Base implements \Rhymix\Framework\Drivers\SMSInterface */ protected $_config = null; + /** + * The driver specification is stored here. + */ + protected static $_spec = array(); + /** * Direct invocation of the constructor is not permitted. */ @@ -21,7 +26,7 @@ abstract class Base implements \Rhymix\Framework\Drivers\SMSInterface } /** - * Create a new instance of the current mail driver, using the given settings. + * Create a new instance of the current SMS driver, using the given settings. * * @param array $config * @return object @@ -32,7 +37,7 @@ abstract class Base implements \Rhymix\Framework\Drivers\SMSInterface } /** - * Get the human-readable name of this mail driver. + * Get the human-readable name of this SMS driver. * * @return string */ @@ -42,7 +47,7 @@ abstract class Base implements \Rhymix\Framework\Drivers\SMSInterface } /** - * Get the list of configuration fields required by this mail driver. + * Get the list of configuration fields required by this SMS driver. * * @return array */ @@ -52,7 +57,7 @@ abstract class Base implements \Rhymix\Framework\Drivers\SMSInterface } /** - * Get the list of API types supported by this mail driver. + * Get the list of API types supported by this SMS driver. * * @return array */ @@ -62,7 +67,17 @@ abstract class Base implements \Rhymix\Framework\Drivers\SMSInterface } /** - * Check if the current mail driver is supported on this server. + * Get the spec for this SMS driver. + * + * @return array + */ + public static function getAPISpec() + { + return static::$_spec; + } + + /** + * Check if the current SMS driver is supported on this server. * * This method returns true on success and false on failure. * @@ -78,10 +93,10 @@ abstract class Base implements \Rhymix\Framework\Drivers\SMSInterface * * This method returns true on success and false on failure. * - * @param object $message + * @param array $messages * @return bool */ - public function send(\Rhymix\Framework\SMS $message) + public function send(array $messages) { return false; } diff --git a/common/framework/drivers/sms/coolsms.php b/common/framework/drivers/sms/coolsms.php index 7d081127e..d08778c21 100644 --- a/common/framework/drivers/sms/coolsms.php +++ b/common/framework/drivers/sms/coolsms.php @@ -8,14 +8,29 @@ namespace Rhymix\Framework\Drivers\SMS; class CoolSMS extends Base implements \Rhymix\Framework\Drivers\SMSInterface { /** - * Maximum length of an SMS. + * API specifications. */ - protected $_maxlength_sms = 90; - - /** - * Maximum length of an LMS. - */ - protected $_maxlength_lms = 2000; + protected static $_spec = array( + 'max_recipients' => 1000, + 'sms_max_length' => 90, + 'sms_max_length_in_charset' => 'CP949', + 'lms_supported' => true, + 'lms_supported_country_codes' => array(0, 82), + 'lms_max_length' => 2000, + 'lms_max_length_in_charset' => 'CP949', + 'lms_subject_supported' => true, + 'lms_subject_max_length' => 40, + 'mms_supported' => true, + 'mms_supported_country_codes' => array(0, 82), + 'mms_max_length' => 2000, + 'mms_max_length_in_charset' => 'CP949', + 'mms_subject_supported' => true, + 'mms_subject_max_length' => 40, + 'image_allowed_types' => array('jpg', 'gif', 'png'), + 'image_max_dimensions' => array(2048, 2048), + 'image_max_filesize' => 300000, + 'delay_supported' => true, + ); /** * Get the list of configuration fields required by this mail driver. @@ -27,143 +42,52 @@ class CoolSMS extends Base implements \Rhymix\Framework\Drivers\SMSInterface return array('api_key', 'api_secret', 'sender_key'); } - /** - * Get the list of API types supported by this mail driver. - * - * @return array - */ - public static function getAPITypes() - { - return array(); - } - /** * Send a message. * * This method returns true on success and false on failure. * - * @param object $message + * @param array $messages * @return bool */ - public function send(\Rhymix\Framework\SMS $message) + public function send(array $messages) { try { - // Initialize the sender. $sender = new \Nurigo\Api\Message($this->_config['api_key'], $this->_config['api_secret']); $status = true; - // Get the list of recipients. - $recipients = $message->getRecipientsGroupedByCountry(); - - // Group the recipients by country code. - foreach ($recipients as $country => $country_recipients) + foreach ($messages as $i => $message) { - // Merge recipients into groups of 1000. - $country_recipients = array_map(function($chunk) { - return implode(',', $chunk); - }, array_chunk($country_recipients, 1000)); - - // Send to each set of merged recipients. - foreach ($country_recipients as $recipient_number) + $options = new \stdClass; + $options->type = $message->type; + $options->from = $message->from; + $options->to = implode(',', $message->to); + $options->text = $message->content ?: 'SMS'; + $options->charset = 'utf8'; + if ($message->delay && $message->delay > time()) { - // Populate the options object. - $options = new \stdClass; - $options->from = $message->getFrom(); - $options->to = $recipient_number; - $options->charset = 'utf8'; - - // Determine when to send this message. - if ($datetime = $message->getDelay()) - { - if ($datetime > time()) - { - $options->datetime = gmdate('YmdHis', $datetime + (3600 * 9)); - } - } - - // Determine the message type based on the length. - $content_full = $message->getContent(); - $detected_type = $message->checkLength($content_full, $this->_maxlength_sms) ? 'SMS' : 'LMS'; - $options->type = $detected_type; - - // If the message has a subject, it must be an LMS. - if ($subject = $message->getSubject()) - { - $options->subject = $subject; - $options->type = 'LMS'; - } - - // If the message has an attachment, it must be an MMS. - if ($attachments = $message->getAttachments()) - { - $options->type = 'MMS'; - } - - // If the recipient is not a Korean number, force SMS. - if ($message->isForceSMS() || ($country > 0 && $country != 82)) - { - unset($options->subject); - $attachments = array(); - $options->country = $country; - $options->type = 'SMS'; - $message->forceSMS(); - } - - // Split the message if necessary. - if ($options->type === 'SMS' && $detected_type !== 'SMS') - { - $content_split = $message->splitMessage($content_full, $this->_maxlength_sms); - } - elseif ($options->type !== 'SMS' && !$message->checkLength($content_full, $this->_maxlength_lms)) - { - $content_split = $message->splitMessage($content_full, $this->_maxlength_lms); - } - else - { - $content_split = array($content_full); - } - - // Send all parts of the split message. - $message_count = max(count($content_split), count($attachments)); - $last_content = 'MMS'; - for ($i = 1; $i <= $message_count; $i++) - { - // Get the message content. - if ($content = array_shift($content_split)) - { - $options->text = $last_content = $content; - } - else - { - $options->text = $last_content ?: 'MMS'; - } - - // Get the attachment. - if ($attachment = array_shift($attachments)) - { - $options->image = $attachment->local_filename; - } - else - { - unset($options->image); - } - - // Determine the best message type for this combination of content and attachment. - if (!$message->isForceSMS()) - { - $options->type = $attachment ? 'MMS' : ($message->checkLength($content, $this->_maxlength_sms) ? 'SMS' : 'LMS'); - } - - // Send the current part of the message. - $result = $sender->send($options); - if (!$result->success_count) - { - $error_codes = implode(', ', $result->error_list ?: array('Unknown')); - $message->errors[] = 'Error (' . $error_codes . ') while sending message ' . $i . ' of ' . $message_count . ' to ' . $options->to; - $status = false; - } - } + $options->datetime = gmdate('YmdHis', $message->delay + (3600 * 9)); + } + if ($message->country && $message->country != 82) + { + $options->country = $message->country; + } + if ($message->subject) + { + $options->subject = $message->subject; + } + if ($message->image) + { + $options->image = $message->image; + } + + $result = $sender->send($options); + if (!$result->success_count) + { + $error_codes = implode(', ', $result->error_list ?: array('Unknown')); + $message->errors[] = 'Error (' . $error_codes . ') while sending message ' . $i . ' of ' . count($messages) . ' to ' . $options->to; + $status = false; } } diff --git a/common/framework/drivers/smsinterface.php b/common/framework/drivers/smsinterface.php index b9b1d0396..269b87650 100644 --- a/common/framework/drivers/smsinterface.php +++ b/common/framework/drivers/smsinterface.php @@ -36,6 +36,13 @@ interface SMSInterface */ public static function getAPITypes(); + /** + * Get the spec for this SMS driver. + * + * @return array + */ + public static function getAPISpec(); + /** * Check if the current SMS driver is supported on this server. * @@ -50,8 +57,8 @@ interface SMSInterface * * This method returns true on success and false on failure. * - * @param object $message + * @param array $messages * @return bool */ - public function send(\Rhymix\Framework\SMS $message); + public function send(array $messages); } diff --git a/common/framework/sms.php b/common/framework/sms.php index 41d5dd479..797192cfe 100644 --- a/common/framework/sms.php +++ b/common/framework/sms.php @@ -467,7 +467,16 @@ class SMS { if ($this->driver) { - $this->sent = $this->driver->send($this) ? true : false; + $messages = $this->_formatSpec($this->driver->getAPISpec()); + if (count($messages)) + { + $this->sent = $this->driver->send($messages) ? true : false; + } + else + { + $this->errors[] = 'No recipients selected'; + $this->sent = false; + } } else { @@ -477,7 +486,7 @@ class SMS } catch(\Exception $e) { - $this->errors[] = $e->getMessage(); + $this->errors[] = class_basename($e) . ': ' . $e->getMessage(); $this->sent = false; } @@ -521,43 +530,200 @@ class SMS } /** - * Check if a message is no longer than the given length. + * Format the current message according to an API spec. * - * This is useful when checking whether a message can fit into a single SMS. - * - * @param string $message - * @param int $maxlength - * @param string $measure_in_charset (optional) - * @return + * @param array $spec API specifications + * @return array */ - public function checkLength($message, $maxlength, $measure_in_charset = 'CP949') + protected function _formatSpec(array $spec) { - $message = @iconv('UTF-8', $measure_in_charset . '//IGNORE', $message); - return strlen($message) <= $maxlength; + // Initialize the return array. + $result = array(); + + // Get the list of recipients. + $recipients = $this->getRecipientsGroupedByCountry(); + + // Group the recipients by country code. + foreach ($recipients as $country_code => $country_recipients) + { + // Merge recipients into groups. + if ($spec['max_recipients'] > 1) + { + $country_recipients = array_chunk($country_recipients, $spec['max_recipients']); + } + + // Send to each set of merged recipients. + foreach ($country_recipients as $recipient_numbers) + { + // Populate the item. + $item = new \stdClass; + $item->type = 'SMS'; + $item->from = $this->getFrom(); + $item->to = $recipient_numbers; + $item->country = $country_code; + if ($spec['delay_supported']) + { + $item->delay = $this->getDelay() ?: 0; + } + + // Get message content. + $subject = $this->getSubject(); + $content = $this->getContent(); + $attachments = $attachments = $this->getAttachments(); + $last_content = 'MMS'; + + // Determine the message type. + if (!$this->isForceSMS() && ($spec['lms_supported'] || $spec['mms_supported'])) + { + // Check attachments, subject, and message length. + if ($spec['mms_supported'] && count($attachments)) + { + $item->type = 'MMS'; + } + elseif ($spec['lms_supported'] && $subject) + { + $item->subject = $subject; + $item->type = 'LMS'; + } + elseif ($spec['lms_supported'] && $this->_getLengthInCharset($content, $spec['sms_max_length_in_charset']) > $spec['sms_max_length']) + { + $item->type = 'LMS'; + } + else + { + $item->type = 'SMS'; + } + + // Check the country code. + if ($item->type === 'MMS' && is_array($spec['mms_supported_country_codes']) && !in_array($country_code, $spec['mms_supported_country_codes'])) + { + $item->type = 'LMS'; + } + if ($item->type === 'LMS' && is_array($spec['lms_supported_country_codes']) && !in_array($country_code, $spec['lms_supported_country_codes'])) + { + $item->type = 'SMS'; + } + } + + // Remove subject and attachments if the message type is SMS. + if ($item->type === 'SMS') + { + if ($item->subject) + { + $content = $item->subject . "\n" . $content; + unset($item->subject); + } + $attachments = array(); + } + + // If message subject is not supported, prepend it to the content instead. + if ($item->subject && !$spec[strtolower($item->type) . '_subject_supported']) + { + $content = $item->subject . "\n" . $content; + unset($item->subject); + } + elseif ($item->subject && $this->_getLengthInCharset($item->subject, $spec[strtolower($item->type) . '_max_length_in_charset']) > $spec[strtolower($item->type) . '_subject_max_length']) + { + $subject_parts = $this->_splitString($item->subject, $spec[strtolower($item->type) . '_subject_max_length'], $spec[strtolower($item->type) . '_max_length_in_charset']); + $subject_short = array_shift($subject_parts); + $subject_remainder = utf8_trim(substr($item->subject, strlen($subject_short))); + $item->subject = $subject_short; + $content = $subject_remainder . "\n" . $content; + } + + // Split the content if necessary. + if (($item->type === 'SMS' && $this->allow_split_sms) || ($item->type !== 'SMS' && $this->allow_split_lms)) + { + if ($this->_getLengthInCharset($content, $spec[strtolower($item->type) . '_max_length_in_charset']) > $spec[strtolower($item->type) . '_max_length']) + { + $content_parts = $this->_splitString($content, $spec[strtolower($item->type) . '_max_length'], $spec[strtolower($item->type) . '_max_length_in_charset']); + } + else + { + $content_parts = array($content); + } + } + else + { + $content_parts = array($content); + } + + // Generate a message for each part of the content and attachments. + $message_count = max(count($content_parts), count($attachments)); + for ($i = 1; $i <= $message_count; $i++) + { + // Get the message content. + if ($content_part = array_shift($content_parts)) + { + $item->content = $last_content = $content_part; + } + else + { + $item->content = $last_content ?: 'MMS'; + } + + // Get the attachment. + if ($attachment = array_shift($attachments)) + { + $item->image = $attachment->local_filename; + } + else + { + unset($item->image); + } + + // Clone the item to make a part. + $cloneitem = clone $item; + + // Determine the best message type for this part. + if ($cloneitem->type !== 'SMS') + { + $cloneitem->type = $attachment ? 'MMS' : ($this->_getLengthInCharset($content_part, $spec['sms_max_length_in_charset']) > $spec['sms_max_length'] ? 'LMS' : 'SMS'); + } + + // Add the cloned part to the result array. + $result[] = $cloneitem; + } + } + } + + // Return the message parts. + return $result; } /** - * Split a message into several short messages. + * Get the length of a string in another character set. * - * This is useful when sending a long message as a series of SMS. + * @param string $str String to measure + * @param string $charset Character set to measure length + * @return + */ + protected function _getLengthInCharset($str, $charset) + { + $str = @iconv('UTF-8', $charset . '//IGNORE', $str); + return strlen($str); + } + + /** + * Split a string into several short chunks. * - * @param string $message - * @param int $maxlength - * @param string $measure_in_charset (optional) + * @param string $str String to split + * @param int $max_length Maximum length of a chunk + * @param string $charset Character set to measure length * @return array */ - public function splitMessage($message, $maxlength, $measure_in_charset = 'CP949') + protected function _splitString($str, $max_length, $charset) { - $message = utf8_trim(utf8_normalize_spaces($message, true)); - $chars = preg_split('//u', $message, -1, PREG_SPLIT_NO_EMPTY); + $str = utf8_trim(utf8_normalize_spaces($str, true)); + $chars = preg_split('//u', $str, -1, \PREG_SPLIT_NO_EMPTY); $result = array(); $current_entry = ''; $current_length = 0; foreach ($chars as $char) { - $char_length = strlen(@iconv('UTF-8', $measure_in_charset . '//IGNORE', $char)); - if ($current_length + $char_length > $maxlength) + $char_length = strlen(@iconv('UTF-8', $charset . '//IGNORE', $char)); + if (($current_length + $char_length > $max_length) || ($current_length + $char_length > $max_length - 7 && ctype_space($char))) { $result[] = $current_entry; $current_entry = $char;