clockworksms_api.php 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643
  1. <?php
  2. /**
  3. * Clockwork PHP API
  4. *
  5. * @package Clockwork
  6. * @copyright Mediaburst Ltd 2012
  7. * @license ISC
  8. * @link http://www.clockworksms.com
  9. * @version 1.3.0
  10. */
  11. if ( !class_exists('ClockworkException') ) {
  12. require_once('exception.php');
  13. }
  14. /**
  15. * Main Clockwork API Class
  16. *
  17. * @package Clockwork
  18. * @since 1.0
  19. */
  20. class Clockwork {
  21. /*
  22. * Version of this class
  23. */
  24. const VERSION = '1.3.1';
  25. /**
  26. * All Clockwork API calls start with BASE_URL
  27. * @author Martin Steel
  28. */
  29. const API_BASE_URL = 'api.clockworksms.com/xml/';
  30. /**
  31. * string to append to API_BASE_URL to check authentication
  32. * @author Martin Steel
  33. */
  34. const API_AUTH_METHOD = 'authenticate';
  35. /**
  36. * string to append to API_BASE_URL for sending SMS
  37. * @author Martin Steel
  38. */
  39. const API_SMS_METHOD = 'sms';
  40. /**
  41. * string to append to API_BASE_URL for checking message credit
  42. * @author Martin Steel
  43. */
  44. const API_CREDIT_METHOD = 'credit';
  45. /**
  46. * string to append to API_BASE_URL for checking account balance
  47. * @author Martin Steel
  48. */
  49. const API_BALANCE_METHOD = 'balance';
  50. /**
  51. * Clockwork API Key
  52. *
  53. * @var string
  54. * @author Martin Steel
  55. */
  56. public $key;
  57. /**
  58. * Use SSL when making HTTP requests
  59. *
  60. * If this is not set, SSL will be used where PHP supports it
  61. *
  62. * @var bool
  63. * @author Martin Steel
  64. */
  65. public $ssl;
  66. /**
  67. * Proxy server hostname (Optional)
  68. *
  69. * @var string
  70. * @author Martin Steel
  71. */
  72. public $proxy_host;
  73. /**
  74. * Proxy server port (Optional)
  75. *
  76. * @var integer
  77. * @author Martin Steel
  78. */
  79. public $proxy_port;
  80. /**
  81. * From address used on text messages
  82. *
  83. * @var string (11 characters or 12 numbers)
  84. * @author Martin Steel
  85. */
  86. public $from;
  87. /**
  88. * Allow long SMS messages (Cost up to 3 credits)
  89. *
  90. * @var bool
  91. * @author Martin Steel
  92. */
  93. public $long;
  94. /**
  95. * Truncate message text if it is too long
  96. *
  97. * @var bool
  98. * @author Martin Steel
  99. */
  100. public $truncate;
  101. /**
  102. * Enables various logging of messages when true.
  103. *
  104. * @var bool
  105. * @author Martin Steel
  106. */
  107. public $log;
  108. /**
  109. * What Clockwork should do if you send an invalid character
  110. *
  111. * Possible values:
  112. * 'error' - Return an error (Messasge is not sent)
  113. * 'remove' - Remove the invalid character(s)
  114. * 'replace' - Replace invalid characters where possible, remove others
  115. * @author Martin Steel
  116. */
  117. public $invalid_char_action;
  118. /**
  119. * Create a new instance of the Clockwork wrapper
  120. *
  121. * @param string key Your Clockwork API Key
  122. * @param array options Optional parameters for sending SMS
  123. * @author Martin Steel
  124. */
  125. public function __construct($key, array $options = array()) {
  126. if (empty($key)) {
  127. throw new ClockworkException("Key can't be blank");
  128. } else {
  129. $this->key = $key;
  130. }
  131. $this->ssl = (array_key_exists('ssl', $options)) ? $options['ssl'] : null;
  132. $this->proxy_host = (array_key_exists('proxy_host', $options)) ? $options['proxy_host'] : null;
  133. $this->proxy_port = (array_key_exists('proxy_port', $options)) ? $options['proxy_port'] : null;
  134. $this->from = (array_key_exists('from', $options)) ? $options['from'] : null;
  135. $this->long = (array_key_exists('long', $options)) ? $options['long'] : null;
  136. $this->truncate = (array_key_exists('truncate', $options)) ? $options['truncate'] : null;
  137. $this->invalid_char_action = (array_key_exists('invalid_char_action', $options)) ? $options['invalid_char_action'] : null;
  138. $this->log = (array_key_exists('log', $options)) ? $options['log'] : false;
  139. }
  140. /**
  141. * Send some text messages
  142. *
  143. *
  144. * @author Martin Steel
  145. */
  146. public function send(array $sms) {
  147. if (!is_array($sms)) {
  148. throw new ClockworkException("sms parameter must be an array");
  149. }
  150. $single_message = $this->is_assoc($sms);
  151. if ($single_message) {
  152. $sms = array($sms);
  153. }
  154. $req_doc = new DOMDocument('1.0', 'UTF-8');
  155. $root = $req_doc->createElement('Message');
  156. $req_doc->appendChild($root);
  157. $user_node = $req_doc->createElement('Key');
  158. $user_node->appendChild($req_doc->createTextNode($this->key));
  159. $root->appendChild($user_node);
  160. for ($i = 0; $i < count($sms); $i++) {
  161. $single = $sms[$i];
  162. $sms_node = $req_doc->createElement('SMS');
  163. // Phone number
  164. $sms_node->appendChild($req_doc->createElement('To', $single['to']));
  165. // Message text
  166. $content_node = $req_doc->createElement('Content');
  167. $content_node->appendChild($req_doc->createTextNode($single['message']));
  168. $sms_node->appendChild($content_node);
  169. // From
  170. if (array_key_exists('from', $single) || isset($this->from)) {
  171. $from_node = $req_doc->createElement('From');
  172. $from_node->appendChild($req_doc->createTextNode(array_key_exists('from', $single) ? $single['from'] : $this->from));
  173. $sms_node->appendChild($from_node);
  174. }
  175. // Client ID
  176. if (array_key_exists('client_id', $single)) {
  177. $client_id_node = $req_doc->createElement('ClientID');
  178. $client_id_node->appendChild($req_doc->createTextNode($single['client_id']));
  179. $sms_node->appendChild($client_id_node);
  180. }
  181. // Long
  182. if (array_key_exists('long', $single) || isset($this->long)) {
  183. $long = array_key_exists('long', $single) ? $single['long'] : $this->long;
  184. $long_node = $req_doc->createElement('Long');
  185. $long_node->appendChild($req_doc->createTextNode($long ? 1 : 0));
  186. $sms_node->appendChild($long_node);
  187. }
  188. // Truncate
  189. if (array_key_exists('truncate', $single) || isset($this->truncate)) {
  190. $truncate = array_key_exists('truncate', $single) ? $single['truncate'] : $this->truncate;
  191. $trunc_node = $req_doc->createElement('Truncate');
  192. $trunc_node->appendChild($req_doc->createTextNode($truncate ? 1 : 0));
  193. $sms_node->appendChild($trunc_node);
  194. }
  195. // Invalid Char Action
  196. if (array_key_exists('invalid_char_action', $single) || isset($this->invalid_char_action)) {
  197. $action = array_key_exists('invalid_char_action', $single) ? $single['invalid_char_action'] : $this->invalid_char_action;
  198. switch (strtolower($action)) {
  199. case 'error':
  200. $sms_node->appendChild($req_doc->createElement('InvalidCharAction', 1));
  201. break;
  202. case 'remove':
  203. $sms_node->appendChild($req_doc->createElement('InvalidCharAction', 2));
  204. break;
  205. case 'replace':
  206. $sms_node->appendChild($req_doc->createElement('InvalidCharAction', 3));
  207. break;
  208. default:
  209. break;
  210. }
  211. }
  212. // Wrapper ID
  213. $sms_node->appendChild($req_doc->createElement('WrapperID', $i));
  214. $root->appendChild($sms_node);
  215. }
  216. $req_xml = $req_doc->saveXML();
  217. $resp_xml = $this->postToClockwork(self::API_SMS_METHOD, $req_xml);
  218. $resp_doc = new DOMDocument();
  219. $resp_doc->loadXML($resp_xml);
  220. $response = array();
  221. $err_no = null;
  222. $err_desc = null;
  223. foreach($resp_doc->documentElement->childNodes AS $doc_child) {
  224. switch(strtolower($doc_child->nodeName)) {
  225. case 'sms_resp':
  226. $resp = array();
  227. $wrapper_id = null;
  228. foreach($doc_child->childNodes AS $resp_node) {
  229. switch(strtolower($resp_node->nodeName)) {
  230. case 'messageid':
  231. $resp['id'] = $resp_node->nodeValue;
  232. break;
  233. case 'errno':
  234. $resp['error_code'] = $resp_node->nodeValue;
  235. break;
  236. case 'errdesc':
  237. $resp['error_message'] = $resp_node->nodeValue;
  238. break;
  239. case 'wrapperid':
  240. $wrapper_id = $resp_node->nodeValue;
  241. break;
  242. }
  243. }
  244. if( array_key_exists('error_code', $resp ) )
  245. {
  246. $resp['success'] = 0;
  247. } else {
  248. $resp['success'] = 1;
  249. }
  250. $resp['sms'] = $sms[$wrapper_id];
  251. array_push($response, $resp);
  252. break;
  253. case 'errno':
  254. $err_no = $doc_child->nodeValue;
  255. break;
  256. case 'errdesc':
  257. $err_desc = $doc_child->nodeValue;
  258. break;
  259. }
  260. }
  261. if (isset($err_no)) {
  262. throw new ClockworkException($err_desc, $err_no);
  263. }
  264. if ($single_message) {
  265. return $response[0];
  266. } else {
  267. return $response;
  268. }
  269. }
  270. /**
  271. * Check how many SMS credits you have available
  272. *
  273. * @return integer SMS credits remaining
  274. * @deprecated Use checkBalance() instead
  275. * @author Martin Steel
  276. */
  277. public function checkCredit() {
  278. // Create XML doc for request
  279. $req_doc = new DOMDocument('1.0', 'UTF-8');
  280. $root = $req_doc->createElement('Credit');
  281. $req_doc->appendChild($root);
  282. $root->appendChild($req_doc->createElement('Key', $this->key));
  283. $req_xml = $req_doc->saveXML();
  284. // POST XML to Clockwork
  285. $resp_xml = $this->postToClockwork(self::API_CREDIT_METHOD, $req_xml);
  286. // Create XML doc for response
  287. $resp_doc = new DOMDocument();
  288. $resp_doc->loadXML($resp_xml);
  289. // Parse the response to find credit value
  290. $credit;
  291. $err_no = null;
  292. $err_desc = null;
  293. foreach ($resp_doc->documentElement->childNodes AS $doc_child) {
  294. switch ($doc_child->nodeName) {
  295. case "Credit":
  296. $credit = $doc_child->nodeValue;
  297. break;
  298. case "ErrNo":
  299. $err_no = $doc_child->nodeValue;
  300. break;
  301. case "ErrDesc":
  302. $err_desc = $doc_child->nodeValue;
  303. break;
  304. default:
  305. break;
  306. }
  307. }
  308. if (isset($err_no)) {
  309. throw new ClockworkException($err_desc, $err_no);
  310. }
  311. return $credit;
  312. }
  313. /**
  314. * Check your account balance
  315. *
  316. * @return array Array of account balance:
  317. * @author Martin Steel
  318. */
  319. public function checkBalance() {
  320. // Create XML doc for request
  321. $req_doc = new DOMDocument('1.0', 'UTF-8');
  322. $root = $req_doc->createElement('Balance');
  323. $req_doc->appendChild($root);
  324. $root->appendChild($req_doc->createElement('Key', $this->key));
  325. $req_xml = $req_doc->saveXML();
  326. // POST XML to Clockwork
  327. $resp_xml = $this->postToClockwork(self::API_BALANCE_METHOD, $req_xml);
  328. // Create XML doc for response
  329. $resp_doc = new DOMDocument();
  330. $resp_doc->loadXML($resp_xml);
  331. // Parse the response to find balance value
  332. $balance = null;
  333. $err_no = null;
  334. $err_desc = null;
  335. foreach ($resp_doc->documentElement->childNodes as $doc_child) {
  336. switch ($doc_child->nodeName) {
  337. case "Balance":
  338. $balance = number_format(floatval($doc_child->nodeValue), 2);
  339. break;
  340. case "Currency":
  341. foreach ($doc_child->childNodes as $resp_node) {
  342. switch ($resp_node->tagName) {
  343. case "Symbol":
  344. $symbol = $resp_node->nodeValue;
  345. break;
  346. case "Code":
  347. $code = $resp_node->nodeValue;
  348. break;
  349. }
  350. }
  351. break;
  352. case "ErrNo":
  353. $err_no = $doc_child->nodeValue;
  354. break;
  355. case "ErrDesc":
  356. $err_desc = $doc_child->nodeValue;
  357. break;
  358. default:
  359. break;
  360. }
  361. }
  362. if (isset($err_no)) {
  363. throw new ClockworkException($err_desc, $err_no);
  364. }
  365. return array( 'symbol' => $symbol, 'balance' => $balance, 'code' => $code );
  366. }
  367. /**
  368. * Check whether the API Key is valid
  369. *
  370. * @return bool True indicates a valid key
  371. * @author Martin Steel
  372. */
  373. public function checkKey() {
  374. // Create XML doc for request
  375. $req_doc = new DOMDocument('1.0', 'UTF-8');
  376. $root = $req_doc->createElement('Authenticate');
  377. $req_doc->appendChild($root);
  378. $root->appendChild($req_doc->createElement('Key', $this->key));
  379. $req_xml = $req_doc->saveXML();
  380. // POST XML to Clockwork
  381. $resp_xml = $this->postToClockwork(self::API_AUTH_METHOD, $req_xml);
  382. // Create XML doc for response
  383. $resp_doc = new DOMDocument();
  384. $resp_doc->loadXML($resp_xml);
  385. // Parse the response to see if authenticated
  386. $cust_id;
  387. $err_no = null;
  388. $err_desc = null;
  389. foreach ($resp_doc->documentElement->childNodes AS $doc_child) {
  390. switch ($doc_child->nodeName) {
  391. case "CustID":
  392. $cust_id = $doc_child->nodeValue;
  393. break;
  394. case "ErrNo":
  395. $err_no = $doc_child->nodeValue;
  396. break;
  397. case "ErrDesc":
  398. $err_desc = $doc_child->nodeValue;
  399. break;
  400. default:
  401. break;
  402. }
  403. }
  404. if (isset($err_no)) {
  405. throw new ClockworkException($err_desc, $err_no);
  406. }
  407. return isset($cust_id);
  408. }
  409. /**
  410. * Make an HTTP POST to Clockwork
  411. *
  412. * @param string method Clockwork method to call (sms/credit)
  413. * @param string data Content of HTTP POST
  414. *
  415. * @return string Response from Clockwork
  416. * @author Martin Steel
  417. */
  418. protected function postToClockwork($method, $data) {
  419. if ($this->log) {
  420. $this->logXML("API $method Request XML", $data);
  421. }
  422. if( isset( $this->ssl ) ) {
  423. $ssl = $this->ssl;
  424. } else {
  425. $ssl = $this->sslSupport();
  426. }
  427. $url = $ssl ? 'https://' : 'http://';
  428. $url .= self::API_BASE_URL . $method;
  429. $response = $this->xmlPost($url, $data);
  430. if ($this->log) {
  431. $this->logXML("API $method Response XML", $response);
  432. }
  433. return $response;
  434. }
  435. /**
  436. * Make a HTTP POST
  437. *
  438. * cURL will be used if available, otherwise tries the PHP stream functions
  439. *
  440. * @param string url URL to send to
  441. * @param string data Data to POST
  442. * @return string Response returned by server
  443. * @author Martin Steel
  444. */
  445. protected function xmlPost($url, $data) {
  446. if(extension_loaded('curl')) {
  447. $ch = curl_init($url);
  448. curl_setopt($ch, CURLOPT_POST, 1);
  449. curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
  450. curl_setopt($ch, CURLOPT_HTTPHEADER, Array("Content-Type: text/xml"));
  451. curl_setopt($ch, CURLOPT_USERAGENT, 'Clockwork PHP Wrapper/1.0' . self::VERSION);
  452. curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
  453. if (isset($this->proxy_host) && isset($this->proxy_port)) {
  454. curl_setopt($ch, CURLOPT_PROXY, $this->proxy_host);
  455. curl_setopt($ch, CURLOPT_PROXYPORT, $this->proxy_port);
  456. }
  457. $response = curl_exec($ch);
  458. $info = curl_getinfo($ch);
  459. if ($response === false || $info['http_code'] != 200) {
  460. throw new Exception('HTTP Error calling Clockwork API - HTTP Status: ' . $info['http_code'] . ' - cURL Erorr: ' . curl_error($ch));
  461. } elseif (curl_errno($ch) > 0) {
  462. throw new Exception('HTTP Error calling Clockwork API - cURL Error: ' . curl_error($ch));
  463. }
  464. curl_close($ch);
  465. return $response;
  466. } elseif (function_exists('stream_get_contents')) {
  467. // Enable error Track Errors
  468. $track = ini_get('track_errors');
  469. ini_set('track_errors',true);
  470. $params = array('http' => array(
  471. 'method' => 'POST',
  472. 'header' => "Content-Type: text/xml\r\nUser-Agent: mediaburst PHP Wrapper/" . self::VERSION . "\r\n",
  473. 'content' => $data
  474. ));
  475. if (isset($this->proxy_host) && isset($this->proxy_port)) {
  476. $params['http']['proxy'] = 'tcp://'.$this->proxy_host . ':' . $this->proxy_port;
  477. $params['http']['request_fulluri'] = True;
  478. }
  479. $ctx = stream_context_create($params);
  480. $fp = @fopen($url, 'rb', false, $ctx);
  481. if (!$fp) {
  482. ini_set('track_errors',$track);
  483. throw new Exception("HTTP Error calling Clockwork API - fopen Error: $php_errormsg");
  484. }
  485. $response = @stream_get_contents($fp);
  486. if ($response === false) {
  487. ini_set('track_errors',$track);
  488. throw new Exception("HTTP Error calling Clockwork API - stream Error: $php_errormsg");
  489. }
  490. ini_set('track_errors',$track);
  491. return $response;
  492. } else {
  493. throw new Exception("Clockwork requires PHP5 with cURL or HTTP stream support");
  494. }
  495. }
  496. /**
  497. * Does the server/HTTP wrapper support SSL
  498. *
  499. * This is a best guess effort, some servers have weird setups where even
  500. * though cURL is compiled with SSL support is still fails to make
  501. * any requests.
  502. *
  503. * @return bool True if SSL is supported
  504. * @author Martin Steel
  505. */
  506. protected function sslSupport() {
  507. $ssl = false;
  508. // See if PHP is compiled with cURL
  509. if (extension_loaded('curl')) {
  510. $version = curl_version();
  511. $ssl = ($version['features'] & CURL_VERSION_SSL) ? true : false;
  512. } elseif (extension_loaded('openssl')) {
  513. $ssl = true;
  514. }
  515. return $ssl;
  516. }
  517. /**
  518. * Log some XML, tidily if possible, in the PHP error log
  519. *
  520. * @param string log_msg The log message to prepend to the XML
  521. * @param string xml An XML formatted string
  522. *
  523. * @return void
  524. * @author Martin Steel
  525. */
  526. protected function logXML($log_msg, $xml) {
  527. // Tidy if possible
  528. if (class_exists('tidy')) {
  529. $tidy = new tidy;
  530. $config = array(
  531. 'indent' => true,
  532. 'input-xml' => true,
  533. 'output-xml' => true,
  534. 'wrap' => 200
  535. );
  536. $tidy->parseString($xml, $config, 'utf8');
  537. $tidy->cleanRepair();
  538. $xml = $tidy;
  539. }
  540. // Output
  541. error_log("Clockwork $log_msg: $xml");
  542. }
  543. /**
  544. * Check if an array is associative
  545. *
  546. * @param array $array Array to check
  547. * @return bool
  548. * @author Martin Steel
  549. */
  550. protected function is_assoc($array) {
  551. return (bool)count(array_filter(array_keys($array), 'is_string'));
  552. }
  553. /**
  554. * Check if a number is a valid MSISDN
  555. *
  556. * @param string $val Value to check
  557. * @return bool True if valid MSISDN
  558. * @author James Inman
  559. * @since 1.3.0
  560. * @todo Take an optional country code and check that the number starts with it
  561. */
  562. public static function is_valid_msisdn($val) {
  563. return preg_match( '/^[1-9][0-9]{7,12}$/', $val );
  564. }
  565. }