1<?php
2/**
3 * @package Net_SMS
4 */
5
6/**
7 * HTTP_Request class.
8 */
9require_once 'HTTP/Request2.php';
10require_once 'Net/SMS.php';
11
12/**
13 * Net_SMS_clickatell_http Class implements the HTTP API for accessing the
14 * Clickatell (www.clickatell.com) SMS gateway.
15 *
16 * Copyright 2003-2009 The Horde Project (http://www.horde.org/)
17 *
18 * See the enclosed file COPYING for license information (LGPL). If you
19 * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
20 *
21 * $Horde: framework/Net_SMS/SMS/clickatell_http.php,v 1.29 2008/01/02 11:12:11 jan Exp $
22 *
23 * @see http://www.clickatell.com/developers/api_http.php
24 * @author Marko Djukic <marko@oblo.com>
25 * @package Net_SMS
26 */
27class Net_SMS_clickatell_http extends Net_SMS {
28
29    var $_session_id = null;
30    var $_base_url = 'http://api.clickatell.com/http/';
31
32    protected $request;
33
34    protected $username;
35    protected $password;
36    protected $api_id;
37
38    public function __construct($params = null, HTTP_Request2 $request)
39    {
40        parent::__construct($params);
41        $this->setRequest($request);
42        $this->setUsername(isset($params['user']) ? $params['user'] : null);
43        $this->setPassword(isset($params['password']) ? $params['password'] : null);
44        $this->setAPIId(isset($params['api_id']) ? $params['api_id'] : null);
45    }
46
47    public function setRequest(HTTP_Request2 $request) {
48        $this->request = $request;
49    }
50
51    /**
52     * An array of capabilities, so that the driver can report which operations
53     * it supports and which it doesn't. Possible values are:<pre>
54     *   auth        - The gateway require authentication before sending;
55     *   batch       - Batch sending is supported;
56     *   multi       - Sending of messages to multiple recipients is supported;
57     *   receive     - Whether this driver is capable of receiving SMS;
58     *   credit      - Is use of the gateway based on credits;
59     *   addressbook - Are gateway addressbooks supported;
60     *   lists       - Gateway support for distribution lists.
61     * </pre>
62     *
63     * @var array
64     */
65    var $capabilities = array('auth'        => true,
66                              'batch'       => 100,
67                              'multi'       => true,
68                              'receive'     => false,
69                              'credit'      => true,
70                              'addressbook' => false,
71                              'lists'       => false);
72
73    /**
74     * Authenticate at the gateway and set a session id if successful. Caching
75     * is used to minimise the http calls for subsequent messages.
76     *
77     * @access private
78     *
79     * @return mixed  True on success or PEAR Error on failure.
80     */
81    function _authenticate()
82    {
83        /* We have already authenticated so return true. */
84        if (!empty($this->_session_id)) {
85            return true;
86        }
87
88
89        /* Set up the http authentication url. */
90        $url = sprintf('auth?user=%s&password=%s&api_id=%s',
91                       urlencode($this->getUsername()),
92                       urlencode($this->getPassword()),
93                       $this->getAPIId());
94
95        /* Do the HTTP authentication and get the response. */
96        $response = Net_SMS_clickatell_http::_callURL($url);
97        if (is_a($response, 'PEAR_Error')) {
98            return PEAR::raiseError(sprintf(_("Authentication failed. %s"), $response->getMessage()));
99        }
100
101        /* Split up the response. */
102        $response = explode(':', $response);
103        if ($response[0] == 'OK') {
104            $this->_session_id = trim($response[1]);
105            return true;
106        } else {
107            return $this->getError($response[1], _("Authentication failed. %s"));
108        }
109    }
110
111    /**
112     * This function does the actual sending of the message.
113     *
114     * @access private
115     *
116     * @param array $message  The array containing the message and its send
117     *                        parameters.
118     * @param array $to       The recipients.
119     *
120     * @return array  An array with the success status and additional
121     *                information.
122     */
123    function _send($message, $to)
124    {
125        /* Set up the http sending url. */
126        $url = sprintf('sendmsg?session_id=%s&text=%s',
127                       $this->_session_id,
128                       urlencode($message['text']));
129
130        $req_feat = 0;
131        if (!empty($message['send_params']['from'])) {
132            /* If source from is set, require it for transit gateways and append
133               to url. */
134            $req_feat =+ 16;
135            $url .= '&from=' . urlencode($message['send_params']['from']);
136        }
137        if (!empty($message['send_params']['msg_type']) &&
138            $message['send_params']['msg_type'] == 'SMS_FLASH') {
139            /* If message type is flash, require it for transit gateways. */
140            $req_feat =+ 512;
141            $url .= '&msg_type=' . $message['send_params']['msg_type'];
142        }
143        if (!empty($req_feat)) {
144            /* If features have been required, add to url. */
145            $url .= '&req_feat=' . $req_feat;
146        }
147
148        /* Append the recipients of this message and call the url. */
149        foreach ($to as $key => $val) {
150            if (preg_match('/^.*?<?\+?(\d{7,})(>|$)/', $val, $matches)) {
151                $to[$key] = $matches[1];
152            } else {
153                /* FIXME: Silently drop bad recipients. This should be logged
154                 * and/or reported. */
155                unset($to[$key]);
156            }
157        }
158        $to = implode(',', $to);
159        $url .= '&to=' . $to;
160        $response = trim($this->_callURL($url));
161
162        /* Ugly parsing of the response, but that's how it comes back. */
163        $lines = explode("\n", $response);
164        $response = array();
165
166        if (count($lines) > 1) {
167            foreach ($lines as $line) {
168                $parts = explode('To:', $line);
169                $recipient = trim($parts[1]);
170                $outcome = explode(':', $parts[0]);
171                $response[$recipient] = array(($outcome[0] == 'ID' ? 1 : 0), $outcome[1]);
172            }
173        } else {
174            /* Single recipient. */
175            $outcome = explode(':', $lines[0]);
176            $response[$to] = array(($outcome[0] == 'ID' ? 1 : 0), $outcome[1]);
177        }
178
179        return $response;
180    }
181
182    /**
183     * Returns the current credit balance on the gateway.
184     *
185     * @access private
186     *
187     * @return integer  The credit balance available on the gateway.
188     */
189    function _getBalance()
190    {
191        /* Set up the url and call it. */
192        $url = sprintf('getbalance?session_id=%s',
193                       $this->_session_id);
194        $response = trim($this->_callURL($url));
195
196        /* Try splitting up the response. */
197        $lines = explode('=', $response);
198
199        /* Split up the response. */
200        $response = explode(':', $response);
201        if ($response[0] == 'Credit') {
202            return trim($response[1]);
203        } else {
204            return $this->getError($response[1], _("Could not check balance. %s"));
205        }
206    }
207
208    /**
209     * Identifies this gateway driver and returns a brief description.
210     *
211     * @return array  Array of driver info.
212     */
213    function getInfo()
214    {
215        return array(
216            'name' => _("Clickatell via HTTP"),
217            'desc' => _("This driver allows sending of messages through the Clickatell (http://clickatell.com) gateway, using the HTTP API"),
218        );
219    }
220
221    /**
222     * Returns the required parameters for this gateway driver.
223     *
224     * @return array  Array of required parameters.
225     */
226    function getParams()
227    {
228        return array(
229            'user' => array('label' => _("Username"), 'type' => 'text'),
230            'password' => array('label' => _("Password"), 'type' => 'text'),
231            'api_id' => array('label' => _("API ID"), 'type' => 'text'),
232        );
233    }
234
235    /**
236     * Returns the parameters that can be set as default for sending messages
237     * using this gateway driver and displayed when sending messages.
238     *
239     * @return array  Array of parameters that can be set as default.
240     * @todo  Set up batch fields/params, would be nice to have ringtone/logo
241     *        support too, queue choice, unicode choice.
242     */
243    function getDefaultSendParams()
244    {
245        $params = array();
246        $params['from'] = array(
247            'label' => _("Source address"),
248            'type' => 'text');
249
250        $params['deliv_time'] = array(
251            'label' => _("Delivery time"),
252            'type' => 'enum',
253            'params' => array(array('now' => _("immediate"), 'user' => _("user select"))));
254
255        $types = array('SMS_TEXT' => _("Standard"), 'SMS_FLASH' => _("Flash"));
256        $params['msg_type'] = array(
257            'label' => _("Message type"),
258            'type' => 'keyval_multienum',
259            'params' => array($types));
260
261        return $params;
262    }
263
264    /**
265     * Returns the parameters for sending messages using this gateway driver,
266     * displayed when sending messages. These are filtered out using the
267     * default values set for the gateway.
268     *
269     * @return array  Array of required parameters.
270     * @todo  Would be nice to use a time/date setup rather than minutes from
271     *        now for the delivery time. Upload field for ringtones/logos?
272     */
273    function getSendParams($params)
274    {
275        if (empty($params['from'])) {
276            $params['from'] = array(
277                'label' => _("Source address"),
278                'type' => 'text');
279        }
280
281        if ($params['deliv_time'] == 'user') {
282            $params['deliv_time'] = array(
283                'label' => _("Delivery time"),
284                'type' => 'int',
285                'desc' => _("Value in minutes from now."));
286        }
287
288        if (count($params['msg_type']) > 1) {
289            $params['msg_type'] = array(
290                'label' => _("Message type"),
291                'type' => 'enum',
292                'params' => array($params['msg_type']));
293        } else {
294            $params['msg_type'] = $params['msg_type'][0];
295        }
296
297        return $params;
298    }
299
300    /**
301     * Returns a string representation of an error code.
302     *
303     * @param integer $error  The error code to look up.
304     * @param string $text    An existing error text to use to raise a
305     *                        PEAR Error.
306     *
307     * @return mixed  A textual message corresponding to the error code or a
308     *                PEAR Error if passed an existing error text.
309     *
310     * @todo  Check which of these are actually required and trim down the
311     *        list.
312     */
313    function getError($error, $error_text = '')
314    {
315        /* Make sure we get only the number at the start of an error. */
316        list($error) = explode(',', $error);
317        $error = trim($error);
318
319        /* An array of error codes returned by the gateway. */
320        $errors = array('001' => _("Authentication failed"),
321                        '002' => _("Unknown username or password."),
322                        '003' => _("Session ID expired."),
323                        '004' => _("Account frozen."),
324                        '005' => _("Missing session ID."),
325                        '007' => _("IP lockdown violation."),
326                        '101' => _("Invalid or missing parameters."),
327                        '102' => _("Invalid UDH. (User Data Header)."),
328                        '103' => _("Unknown apimsgid (API Message ID)."),
329                        '104' => _("Unknown climsgid (Client Message ID)."),
330                        '105' => _("Invalid destination address."),
331                        '106' => _("Invalid source address."),
332                        '107' => _("Empty message."),
333                        '108' => _("Invalid or missing api_id."),
334                        '109' => _("Missing message ID."),
335                        '110' => _("Error with email message."),
336                        '111' => _("Invalid protocol."),
337                        '112' => _("Invalid msg_type."),
338                        '113' => _("Max message parts exceeded."),
339                        '114' => _("Cannot route message to specified number."),
340                        '115' => _("Message expired."),
341                        '116' => _("Invalid unicode data."),
342                        '201' => _("Invalid batch ID."),
343                        '202' => _("No batch template."),
344                        '301' => _("No credit left."),
345                        '302' => _("Max allowed credit."));
346
347        if (empty($error_text)) {
348            return $errors[$error];
349        } else {
350            return PEAR::raiseError(sprintf($error_text, $errors[$error]));
351        }
352    }
353
354    /**
355     * Do the http call using a url passed to the function.
356     *
357     * @access private
358     *
359     * @param string $url  The url to call.
360     *
361     * @return mixed  The response on success or PEAR Error on failure.
362     */
363    function _callURL($url)
364    {
365        /** @todo Shift to factory */
366        $this->request->setMethod('POST');
367        $this->request->setConfig('timeout', 5);
368        $this->request->setConfig('follow_redirects', true);
369
370
371        $this->request->setURL($this->_base_url . $url);
372
373        $response = $this->request->send();
374        if ($response->getStatus() != 200) {
375            throw new Net_SMS_Exception(sprintf(_("Could not open %s."), $url));
376        }
377
378        return $response->getBody();
379    }
380
381    public function getUsername() {
382        return $this->username;
383    }
384
385    public function getPassword() {
386        return $this->password;
387    }
388
389    public function getAPIId() {
390        return $this->api_id;
391    }
392
393    public function setAPIId($id) {
394        $this->api_id = $id;
395    }
396
397    public function setUsername($username) {
398        $this->username = $username;
399    }
400
401    public function setPassword($password) {
402        $this->password = $password;
403    }
404}
405