diff --git a/composer.json b/composer.json index 47f0f6c..d4b3571 100755 --- a/composer.json +++ b/composer.json @@ -4,7 +4,7 @@ "description": "Short messaging for PHP 5.3", "keywords": ["SMS"], "homepage": "http://github.com/xi-project/xi-sms", - "license": "BSD", + "license": "BSD-3-Clause", "authors": [ { "name": "Mikko Forsström", diff --git a/library/Xi/Sms/Gateway/ClickatellGateway.php b/library/Xi/Sms/Gateway/ClickatellGateway.php index 4979936..5edb8f8 100644 --- a/library/Xi/Sms/Gateway/ClickatellGateway.php +++ b/library/Xi/Sms/Gateway/ClickatellGateway.php @@ -5,6 +5,9 @@ * * For copyright and license information, please view the LICENSE * file that was distributed with this source code. + * + * This class implements Clickatell API + * @link https://www.clickatell.com/downloads/http/Clickatell_HTTP.pdf */ namespace Xi\Sms\Gateway; @@ -45,20 +48,111 @@ public function __construct( $this->endpoint = $endpoint; } + /** + * Authentication + * @return array + * @return bool|string Success|Session ID + */ + public function authenticate() + { + $params = array( + 'api_id' => $this->apiKey, + 'user' => $this->user, + 'password' => $this->password, + ); + + $response_string = $this->getClient()->get( + $this->endpoint . '/http/auth?'.http_build_query($params), + array() + ); + + $response = $this->parseResponse($response_string); + if ($response === false) { + return false; + } + if (!empty($response['ERR'])) { + return false; + } + if (empty($response['OK'])) { + return false; + } + return $response['OK']; + } + /** * @see GatewayInterface::send - * @todo Implement a smarter method of sending (batch) + * @param SmsMessage $message + * @return bool Success */ public function send(SmsMessage $message) { - $body = urlencode(utf8_decode($message->getBody())); - $from = urlencode($message->getFrom()); - - foreach ($message->getTo() as $to) { - $url = "{$this->endpoint}/http/sendmsg?api_id={$this->apiKey}&user={$this->user}" . - "&password={$this->password}&to={$to}&text={$body}&from={$from}"; - $this->getClient()->post($url, array()); - } - return true; + // Sending is limited to max 100 addressees + if (count($message->getTo()) > 100) { + foreach (array_chunk($message->getTo(), 100) as $tos) { + $message_alt = new SmsMessage( + $message->getBody(), + $message->getFrom(), + $tos + ); + $this->send($message_alt); + } + return true; + } + + $params = array( + 'api_id' => $this->apiKey, + 'user' => $this->user, + 'password' => $this->password, + 'to' => implode(',', $message->getTo()), + 'text' => utf8_decode($message->getBody()), + 'from' => $message->getFrom() + ); + + $response_string = $this->getClient()->get( + $this->endpoint . '/http/sendmsg?'.http_build_query($params), + array() + ); + $response = $this->parseResponse($response_string); + if (!empty($response['ERR'])) { + return false; + } + if (empty($response['ID'])) { + return false; + } + return true; } + + /** + * Parses a Clickatell HTTP API response + * @param string $response + * @return array error messages, messages IDs, phone numbers... + * @return bool|array Success|Parsed API response + */ + public static function parseResponse($response) { + $return = array( + 'id' => null, + 'error' => null + ); + if (preg_match_all('/((ERR|ID|OK): ([^\n]*))+/', $response, $matches)) { + for ($i = 0; $i < count($matches[0]); $i++) { + $phone_number = null; + if (preg_match('/(.*)( To: ([0-9]+))$/', $matches[3][$i], $ms)) { + $message = $ms[1]; + $phone_number = $ms[3]; + } else { + $message = $matches[3][$i]; + } + + $key = $matches[2][$i]; + if ($phone_number) { + $return[$key][$phone_number] = $message; + } else { + $return[$key] = $message; + } + } + return $return; + } else { + return false; + } + } } diff --git a/tests/Xi/Sms/Tests/Gateway/ClickatellGatewayTest.php b/tests/Xi/Sms/Tests/Gateway/ClickatellGatewayTest.php index 7215d78..8bcbf43 100644 --- a/tests/Xi/Sms/Tests/Gateway/ClickatellGatewayTest.php +++ b/tests/Xi/Sms/Tests/Gateway/ClickatellGatewayTest.php @@ -2,10 +2,210 @@ namespace Xi\Sms\Tests\Gateway; +use Xi\Sms\SmsMessage; +use Xi\Sms\SmsService; +use Xi\Sms\SmsException; use Xi\Sms\Gateway\ClickatellGateway; +use Buzz\Message\Response; class ClickatellGatewayTest extends \PHPUnit_Framework_TestCase { + /** + * @test + */ + public function authenticateFail() + { + $gateway = new ClickatellGateway('lussavain', 'lussuta', 'tussia'); + + $browser = $this->getMockBuilder('Buzz\Browser') + ->disableOriginalConstructor() + ->getMock(); + + $gateway->setClient($browser); + + $browser + ->expects($this->once()) + ->method('get') + ->with( + $this->callback(function($actual) { + $url = parse_url($actual); + parse_str($url['query'], $query); + return + $url['path'] === '/http/auth' && + $query['api_id'] === 'lussavain' && + $query['user'] === 'lussuta' && + $query['password'] === 'tussia'; + }), + array() + ) + ->will($this->returnValue('')); + + $this->assertFalse($gateway->authenticate()); + } + + /** + * @test + */ + public function authenticateOk() + { + $gateway = new ClickatellGateway('lussavain', 'lussuta', 'tussia', 'http://api.dr-kobros.com'); + + $browser = $this->getMockBuilder('Buzz\Browser') + ->disableOriginalConstructor() + ->getMock(); + + $gateway->setClient($browser); + + $browser + ->expects($this->once()) + ->method('get') + ->with( + $this->callback(function($actual) { + $url = parse_url($actual); + parse_str($url['query'], $query); + return + $url['path'] === '/http/auth' && + $query['api_id'] === 'lussavain' && + $query['user'] === 'lussuta' && + $query['password'] === 'tussia'; + }), + array() + ) + ->will($this->returnValue('OK: QWERTYUI12345678')); + + $this->assertEquals('QWERTYUI12345678', $gateway->authenticate()); + } + + /** + * @test + */ + public function sendMultipleMoreThan100() + { + $gateway = new ClickatellGateway('lussavain', 'lussuta', 'tussia', 'http://api.dr-kobros.com'); + + $browser = $this->getMockBuilder('Buzz\Browser') + ->disableOriginalConstructor() + ->getMock(); + + $gateway->setClient($browser); + + $addressees = array(); + for ($i = 0; $i < 345; $i++) { + $addressees[] = rand(); + } + + $browser + ->expects($this->exactly(4)) + ->method('get') + ->with( + $this->callback(function($actual) { + $url = parse_url($actual); + parse_str($url['query'], $query); + return count(explode(',', $query['to'])) === 100 || + count(explode(',', $query['to'])) === 45; + }), + $this->isType('array') + ) + ->will($this->returnValue("ID: QWERTYUI12345678 To: 358503028030\nID: 12345678QWERTYUI To: 49123456789")); + + $message = new \Xi\Sms\SmsMessage( + 'Pekkis tassa lussuttaa.', + '358503028030', + $addressees + ); + $ret = $gateway->send($message); + } + + /** + * @test + */ + public function sendMultiple() + { + $gateway = new ClickatellGateway('lussavain', 'lussuta', 'tussia', 'http://api.dr-kobros.com'); + + $browser = $this->getMockBuilder('Buzz\Browser') + ->disableOriginalConstructor() + ->getMock(); + + $gateway->setClient($browser); + + $browser + ->expects($this->once()) + ->method('get') + ->with( + $this->callback(function($actual) { + $url = parse_url($actual); + parse_str($url['query'], $query); + return $query['to'] === '358503028030,49123456789'; + }), + $this->isType('array') + ) + ->will($this->returnValue("ID: QWERTYUI12345678 To: 358503028030\nID: 12345678QWERTYUI To: 49123456789")); + + $message = new \Xi\Sms\SmsMessage( + 'Pekkis tassa lussuttaa.', + '358503028030', + array('358503028030', '49123456789') + ); + $ret = $gateway->send($message); + } + + /** + * @test + */ + public function parseResponseMassSendingError() + { + $response = ClickatellGateway::parseResponse("ERR: 114, Cannot route message To: 49123456789\nERR: 567, Bla bla bla To: 4987654321"); + $this->assertEquals('114, Cannot route message', $response['ERR']['49123456789']); + $this->assertEquals('567, Bla bla bla', $response['ERR']['4987654321']); + } + + /** + * @test + */ + public function parseResponseMassSendingId() + { + $response = ClickatellGateway::parseResponse("ID: CE07B3BFEFF35F4E2667B3A47116FDD2 To: 49123456789\nID: QWERTYUIO123456789ASDFGHJK To: 4987654321"); + $this->assertEquals('CE07B3BFEFF35F4E2667B3A47116FDD2', $response['ID']['49123456789']); + $this->assertEquals('QWERTYUIO123456789ASDFGHJK', $response['ID']['4987654321']); + } + + /** + * @test + */ + public function parseResponseErrorParse() + { + $response = ClickatellGateway::parseResponse('foo bar'); + $this->assertFalse($response); + } + + /** + * @test + */ + public function parseResponseOK() + { + $response = ClickatellGateway::parseResponse("OK: CE07B3BFEFF35F4E2667B3A47116FDD2"); + $this->assertEquals('CE07B3BFEFF35F4E2667B3A47116FDD2', $response['OK']); + } + + /** + * @test + */ + public function parseResponseId() + { + $response = ClickatellGateway::parseResponse('ID: CE07B3BFEFF35F4E2667B3A47116FDD2'); + $this->assertEquals('CE07B3BFEFF35F4E2667B3A47116FDD2', $response['ID']); + } + + /** + * @test + */ + public function parseResponseErrorApi() + { + $response = ClickatellGateway::parseResponse('ERR: 114, Cannot route message'); + $this->assertEquals('114, Cannot route message', $response['ERR']); + } + /** * @test */ @@ -21,12 +221,25 @@ public function sendsRequest() $browser ->expects($this->once()) - ->method('post') + ->method('get') ->with( - 'http://api.dr-kobros.com/http/sendmsg?api_id=lussavain&user=lussuta&password=' . - 'tussia&to=358503028030&text=Pekkis+tassa+lussuttaa.&from=358503028030', + $this->callback(function($actual) { + $url = parse_url($actual); + parse_str($url['query'], $query); + return + $url['scheme'] === 'http' && + $url['host'] === 'api.dr-kobros.com' && + $url['path'] === '/http/sendmsg' && + $query['api_id'] === 'lussavain' && + $query['user'] === 'lussuta' && + $query['password'] === 'tussia' && + $query['to'] === '358503028030' && + urldecode($query['text']) === 'Pekkis tassa lussuttaa.' && + $query['from'] === '358503028030'; + }), array() - ); + ) + ->will($this->returnValue('ID: QWERTYUI12345678')); $message = new \Xi\Sms\SmsMessage( 'Pekkis tassa lussuttaa.', @@ -34,7 +247,6 @@ public function sendsRequest() '358503028030' ); - $ret = $gateway->send($message); - $this->assertTrue($ret); + $this->assertTrue($gateway->send($message)); } }