tools - add missing comment blocks
[civicrm-core.git] / tools / extensions / org.civicrm.sms.clickatell / org_civicrm_sms_clickatell.php
1 <?php
2 /*
3 +--------------------------------------------------------------------+
4 | CiviCRM version 4.5 |
5 +--------------------------------------------------------------------+
6 | Copyright CiviCRM LLC (c) 2004-2014 |
7 +--------------------------------------------------------------------+
8 | This file is a part of CiviCRM. |
9 | |
10 | CiviCRM is free software; you can copy, modify, and distribute it |
11 | under the terms of the GNU Affero General Public License |
12 | Version 3, 19 November 2007 and the CiviCRM Licensing Exception. |
13 | |
14 | CiviCRM is distributed in the hope that it will be useful, but |
15 | WITHOUT ANY WARRANTY; without even the implied warranty of |
16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. |
17 | See the GNU Affero General Public License for more details. |
18 | |
19 | You should have received a copy of the GNU Affero General Public |
20 | License and the CiviCRM Licensing Exception along |
21 | with this program; if not, contact CiviCRM LLC |
22 | at info[AT]civicrm[DOT]org. If you have questions about the |
23 | GNU Affero General Public License or the licensing of CiviCRM, |
24 | see the CiviCRM license FAQ at http://civicrm.org/licensing |
25 +--------------------------------------------------------------------+
26 */
27
28 /**
29 *
30 * @package CRM
31 * @copyright CiviCRM LLC (c) 2004-2014
32 * $Id$
33 *
34 */
35 class org_civicrm_sms_clickatell extends CRM_SMS_Provider {
36
37 /**
38 * api type to use to send a message
39 * @var string
40 */
41 protected $_apiType = 'http';
42
43 /**
44 * provider details
45 * @var string
46 */
47 protected $_providerInfo = array();
48
49 /**
50 * Clickatell API Server Session ID
51 *
52 * @var string
53 */
54 protected $_sessionID = NULL;
55
56 /**
57 * Curl handle resource id
58 *
59 */
60 protected $_ch;
61
62 /**
63 * Temporary file resource id
64 * @var resource
65 */
66 protected $_fp;
67
68 public $_apiURL = "https://api.clickatell.com";
69
70 protected $_messageType = array(
71 'SMS_TEXT',
72 'SMS_FLASH',
73 'SMS_NOKIA_OLOGO',
74 'SMS_NOKIA_GLOGO',
75 'SMS_NOKIA_PICTURE',
76 'SMS_NOKIA_RINGTONE',
77 'SMS_NOKIA_RTTL',
78 'SMS_NOKIA_CLEAN',
79 'SMS_NOKIA_VCARD',
80 'SMS_NOKIA_VCAL',
81 );
82
83 protected $_messageStatus = array(
84 '001' => 'Message unknown',
85 '002' => 'Message queued',
86 '003' => 'Delivered',
87 '004' => 'Received by recipient',
88 '005' => 'Error with message',
89 '006' => 'User cancelled message delivery',
90 '007' => 'Error delivering message',
91 '008' => 'OK',
92 '009' => 'Routing error',
93 '010' => 'Message expired',
94 '011' => 'Message queued for later delivery',
95 '012' => 'Out of credit',
96 );
97
98 /**
99 * We only need one instance of this object. So we use the singleton
100 * pattern and cache the instance in this variable
101 *
102 * @var object
103 * @static
104 */
105 static private $_singleton = array();
106
107 /**
108 * Constructor
109 *
110 * Create and auth a Clickatell session.
111 *
112 * @param array $provider
113 * @param bool $skipAuth
114 *
115 * @return \org_civicrm_sms_clickatell
116 */
117 function __construct($provider = array( ), $skipAuth = FALSE) {
118 // initialize vars
119 $this->_apiType = CRM_Utils_Array::value('api_type', $provider, 'http');
120 $this->_providerInfo = $provider;
121
122 if ($skipAuth) {
123 return TRUE;
124 }
125
126 // first create the curl handle
127
128 /**
129 * Reuse the curl handle
130 */
131 $this->_ch = curl_init();
132 if (!$this->_ch || !is_resource($this->_ch)) {
133 return PEAR::raiseError('Cannot initialise a new curl handle.');
134 }
135
136 curl_setopt($this->_ch, CURLOPT_TIMEOUT, 20);
137 curl_setopt($this->_ch, CURLOPT_VERBOSE, 1);
138 curl_setopt($this->_ch, CURLOPT_FAILONERROR, 1);
139 if (ini_get('open_basedir') == '' && ini_get('safe_mode') == 'Off') {
140 curl_setopt($this->_ch, CURLOPT_FOLLOWLOCATION, 1);
141 }
142 curl_setopt($this->_ch, CURLOPT_COOKIEJAR, "/dev/null");
143 curl_setopt($this->_ch, CURLOPT_SSL_VERIFYHOST, 2);
144 curl_setopt($this->_ch, CURLOPT_USERAGENT, 'CiviCRM - http://civicrm.org/');
145
146 $this->authenticate();
147 }
148
149 /**
150 * singleton function used to manage this object
151 *
152 * @param array $providerParams
153 * @param bool $force
154 * @return object
155 * @static
156 */
157 static function &singleton($providerParams = array(
158 ), $force = FALSE) {
159 $providerID = CRM_Utils_Array::value('provider_id', $providerParams);
160 $skipAuth = $providerID ? FALSE : TRUE;
161 $cacheKey = (int) $providerID;
162
163 if (!isset(self::$_singleton[$cacheKey]) || $force) {
164 $provider = array();
165 if ($providerID) {
166 $provider = CRM_SMS_BAO_Provider::getProviderInfo($providerID);
167 }
168 self::$_singleton[$cacheKey] = new org_civicrm_sms_clickatell($provider, $skipAuth);
169 }
170 return self::$_singleton[$cacheKey];
171 }
172
173 /**
174 * Authenticate to the Clickatell API Server.
175 *
176 * @return mixed true on sucess or PEAR_Error object
177 * @access public
178 * @since 1.1
179 */
180 function authenticate() {
181 $url = $this->_providerInfo['api_url'] . "/http/auth";
182
183 $postDataArray = array(
184 'user' => $this->_providerInfo['username'],
185 'password' => $this->_providerInfo['password'],
186 'api_id' => $this->_providerInfo['api_params']['api_id']
187 );
188
189 if (array_key_exists('is_test', $this->_providerInfo['api_params']) &&
190 $this->_providerInfo['api_params']['is_test'] == 1 ) {
191 $response = array('data' => 'OK:' . rand());
192 } else {
193 $postData = $this->urlEncode($postDataArray);
194 $response = $this->curl($url, $postData);
195 }
196 if (PEAR::isError($response)) {
197 return $response;
198 }
199 $sess = explode(":", $response['data']);
200
201 $this->_sessionID = trim($sess[1]);
202
203 if ($sess[0] == "OK") {
204 return TRUE;
205 }
206 else {
207 return PEAR::raiseError($response['data']);
208 }
209 }
210
211 /**
212 * @param $url
213 * @param $postDataArray
214 * @param null $id
215 *
216 * @return object|string
217 */
218 function formURLPostData($url, &$postDataArray, $id = NULL) {
219 $url = $this->_providerInfo['api_url'] . $url;
220 $postDataArray['session_id'] = $this->_sessionID;
221 if ($id) {
222 if (strlen($id) < 32 || strlen($id) > 32) {
223 return PEAR::raiseError('Invalid API Message Id');
224 }
225 $postDataArray['apimsgid'] = $id;
226 }
227 return $url;
228 }
229
230 /**
231 * Send an SMS Message via the Clickatell API Server
232 *
233 * @param $recipients
234 * @param $header
235 * @param $message
236 * @param null $jobID
237 * @param null $userID
238 * @internal param \the $array message with a to/from/text
239 *
240 * @return mixed true on sucess or PEAR_Error object
241 * @access public
242 */
243 function send($recipients, $header, $message, $jobID = NULL, $userID = NULL) {
244 if ($this->_apiType == 'http') {
245 $postDataArray = array( );
246 $url = $this->formURLPostData("/http/sendmsg", $postDataArray);
247
248 if (array_key_exists('from', $this->_providerInfo['api_params'])) {
249 $postDataArray['from'] = $this->_providerInfo['api_params']['from'];
250 }
251 if (array_key_exists('concat', $this->_providerInfo['api_params'])) {
252 $postDataArray['concat'] = $this->_providerInfo['api_params']['concat'];
253 }
254 //TODO:
255 $postDataArray['to'] = $header['To'];
256 $postDataArray['text'] = utf8_decode(substr($message, 0, 460)); // max of 460 characters, is probably not multi-lingual
257 if (array_key_exists('mo', $this->_providerInfo['api_params'])) {
258 $postDataArray['mo'] = $this->_providerInfo['api_params']['mo'];
259 }
260 // sendmsg with callback request:
261 $postDataArray['callback'] = 3;
262
263 $isTest = 0;
264 if (array_key_exists('is_test', $this->_providerInfo['api_params']) &&
265 $this->_providerInfo['api_params']['is_test'] == 1
266 ) {
267 $isTest = 1;
268 }
269
270 /**
271 * Check if we are using a queue when sending as each account
272 * with Clickatell is assigned three queues namely 1, 2 and 3.
273 */
274 if (isset($header['queue']) && is_numeric($header['queue'])) {
275 if (in_array($header['queue'], range(1, 3))) {
276 $postDataArray['queue'] = $header['queue'];
277 }
278 }
279
280 /**
281 * Must we escalate message delivery if message is stuck in
282 * the queue at Clickatell?
283 */
284 if (isset($header['escalate']) && !empty($header['escalate'])) {
285 if (is_numeric($header['escalate'])) {
286 if (in_array($header['escalate'], range(1, 2))) {
287 $postDataArray['escalate'] = $header['escalate'];
288 }
289 }
290 }
291
292 if ($isTest == 1) {
293 $response = array('data' => 'ID:' . rand());
294 }
295 else {
296 $postData = $this->urlEncode($postDataArray);
297 $response = $this->curl($url, $postData);
298 }
299 if (PEAR::isError($response)) {
300 return $response;
301 }
302 $send = explode(":", $response['data']);
303
304 if ($send[0] == "ID") {
305 $this->createActivity($send[1], $message, $header, $jobID, $userID);
306 return $send[1];
307 }
308 else {
309 // TODO: Should add a failed activity instead.
310 CRM_Core_Error::debug_log_message($response['data'] . " - for phone: {$postDataArray['to']}");
311 return PEAR::raiseError($response['data'], null, PEAR_ERROR_RETURN);
312 }
313 }
314 }
315
316 /**
317 * @return bool
318 */
319 function callback() {
320 $apiMsgID = $this->retrieve('apiMsgId', 'String');
321
322 $activity = new CRM_Activity_DAO_Activity();
323 $activity->result = $apiMsgID;
324
325 if ($activity->find(TRUE)) {
326 $actStatusIDs = array_flip(CRM_Core_OptionGroup::values('activity_status'));
327
328 $status = $this->retrieve('status', 'String');
329 switch ($status) {
330 case "001":
331 $statusID = $actStatusIDs['Cancelled'];
332 $clickStat = $this->_messageStatus[$status] . " - Message Unknown";
333 break;
334
335 case "002":
336 $statusID = $actStatusIDs['Scheduled'];
337 $clickStat = $this->_messageStatus[$status] . " - Message Queued";
338 break;
339
340 case "003":
341 $statusID = $actStatusIDs['Completed'];
342 $clickStat = $this->_messageStatus[$status] . " - Delivered to Gateway";
343 break;
344
345 case "004":
346 $statusID = $actStatusIDs['Completed'];
347 $clickStat = $this->_messageStatus[$status] . " - Received by Recipient";
348 break;
349
350 case "005":
351 $statusID = $actStatusIDs['Cancelled'];
352 $clickStat = $this->_messageStatus[$status] . " - Error with Message";
353 break;
354
355 case "006":
356 $statusID = $actStatusIDs['Cancelled'];
357 $clickStat = $this->_messageStatus[$status] . " - User cancelled message";
358 break;
359
360 case "007":
361 $statusID = $actStatusIDs['Cancelled'];
362 $clickStat = $this->_messageStatus[$status] . " - Error delivering message";
363 break;
364
365 case "008":
366 $statusID = $actStatusIDs['Completed'];
367 $clickStat = $this->_messageStatus[$status] . " - Ok, Message Received by Gateway";
368 break;
369
370 case "009":
371 $statusID = $actStatusIDs['Cancelled'];
372 $clickStat = $this->_messageStatus[$status] . " - Routing Error";
373 break;
374
375 case "010":
376 $statusID = $actStatusIDs['Cancelled'];
377 $clickStat = $this->_messageStatus[$status] . " - Message Expired";
378 break;
379
380 case "011":
381 $statusID = $actStatusIDs['Scheduled'];
382 $clickStat = $this->_messageStatus[$status] . " - Message Queued for Later";
383 break;
384
385 case "012":
386 $statusID = $actStatusIDs['Cancelled'];
387 $clickStat = $this->_messageStatus[$status] . " - Out of Credit";
388 break;
389 }
390
391 if ($statusID) {
392 // update activity with status + msg in location
393 $activity->status_id = $statusID;
394 $activity->location = $clickStat;
395 $activity->activity_date_time = CRM_Utils_Date::isoToMysql($activity->activity_date_time);
396 $activity->save();
397 CRM_Core_Error::debug_log_message("SMS Response updated for apiMsgId={$apiMsgID}.");
398 return TRUE;
399 }
400 }
401
402 // if no update is done
403 CRM_Core_Error::debug_log_message("Could not update SMS Response for apiMsgId={$apiMsgID}.");
404 return FALSE;
405 }
406
407 /**
408 * @return $this|null|object
409 */
410 function inbound() {
411 $like = "";
412 $fromPhone = $this->retrieve('from', 'String');
413 $fromPhone = $this->formatPhone($this->stripPhone($fromPhone), $like, "like");
414
415 return parent::processInbound($fromPhone, $this->retrieve('text', 'String'), NULL, $this->retrieve('moMsgId', 'String'));
416 }
417
418 /**
419 * Perform curl stuff
420 *
421 * @param string URL to call
422 * @param string HTTP Post Data
423 *
424 * @return mixed HTTP response body or PEAR Error Object
425 * @access private
426 */
427 function curl($url, $postData) {
428 $this->_fp = tmpfile();
429
430 curl_setopt($this->_ch, CURLOPT_URL, $url);
431 curl_setopt($this->_ch, CURLOPT_POST, 1);
432 curl_setopt($this->_ch, CURLOPT_POSTFIELDS, $postData);
433 curl_setopt($this->_ch, CURLOPT_FILE, $this->_fp);
434
435 $status = curl_exec($this->_ch);
436 $response['http_code'] = curl_getinfo($this->_ch, CURLINFO_HTTP_CODE);
437
438 if (empty($response['http_code'])) {
439 return PEAR::raiseError('No HTTP Status Code was returned.');
440 }
441 elseif ($response['http_code'] === 0) {
442 return PEAR::raiseError('Cannot connect to the Clickatell API Server.');
443 }
444
445 if ($status) {
446 $response['error'] = curl_error($this->_ch);
447 $response['errno'] = curl_errno($this->_ch);
448 }
449
450 rewind($this->_fp);
451
452 $pairs = "";
453 while ($str = fgets($this->_fp, 4096)) {
454 $pairs .= $str;
455 }
456 fclose($this->_fp);
457
458 $response['data'] = $pairs;
459 unset($pairs);
460 asort($response);
461
462 return ($response);
463 }
464 }
465