2021-11-05 23:26:09 +01:00
< ? php
/*
* This file is part of RaspiSMS .
*
* ( c ) Pierre - Lin Bonnemaison < plebwebsas @ gmail . com >
*
* This source file is subject to the GPL - 3.0 license that is bundled
* with this source code in the file LICENSE .
*/
namespace adapters ;
2021-11-05 23:59:38 +01:00
use controllers\internals\Quota ;
2021-11-05 23:26:09 +01:00
use controllers\internals\Tool ;
/**
* Kannel adapter .
*/
class KannelAdapter implements AdapterInterface
{
const KANNEL_SENDSMS_RESULTS_ACCEPTED = 0 ;
const KANNEL_SENDSMS_RESULTS_QUEUED = 3 ;
const KANNEL_SENDSMS_HTTP_CODE_ACCEPTED = 202 ;
const KANNEL_SENDSMS_HTTP_CODE_QUEUED = 202 ;
2021-11-05 23:59:38 +01:00
const KANNEL_CODING_7_BITS = 0 ;
const KANNEL_CODING_8_BITS = 1 ;
const KANNEL_CODING_UCS_2 = 2 ;
2021-11-05 23:26:09 +01:00
/**
2022-03-15 02:24:28 +01:00
* DLR mask to transmit to kannel .
*
2021-11-05 23:26:09 +01:00
* 1 -> Delivered to phone
* 2 -> not delivered
* 16 -> non delivered to SMSC
2022-03-15 02:24:28 +01:00
*
2021-11-05 23:26:09 +01:00
* ( see https :// gist . github . com / grantpullen / 3 d550f31c454e80fda8fc0d5b9105fd0 )
*/
2022-03-15 02:24:28 +01:00
const KANNEL_DLR_BITMASK = 1 + 2 + 16 ;
2021-11-05 23:26:09 +01:00
/**
* Data used to configure interaction with the implemented service . ( e . g : Api credentials , ports numbers , etc . ) .
*/
private $data ;
/**
2022-03-15 02:24:28 +01:00
* Kannel send - sms service url .
2021-11-05 23:26:09 +01:00
*/
private $kannel_sendsms_url ;
/**
* Kannel send - sms username .
*/
private $username ;
/**
* Kannel send - sms password .
*/
private $password ;
/**
2022-03-15 02:24:28 +01:00
* Phone number of the sender , this number may or may not actually be overrided by the SMSC .
2021-11-05 23:26:09 +01:00
*/
private $from ;
/**
2022-03-15 02:24:28 +01:00
* SMSC ' s id to use for sending the message .
2021-11-05 23:26:09 +01:00
*/
private $smsc ;
/**
2022-03-15 02:24:28 +01:00
* SMS Delivery Report Url .
2021-11-05 23:26:09 +01:00
*/
private $dlr_url ;
/**
* Adapter constructor , called when instanciated by RaspiSMS .
*
* @ param string $number : Phone number the adapter is used for
* @ param json string $data : JSON string of the data to configure interaction with the implemented service
*/
public function __construct ( string $data )
{
$this -> data = json_decode ( $data , true );
$this -> kannel_sendsms_url = $this -> data [ 'kannel_sendsms_url' ];
$this -> username = $this -> data [ 'username' ];
$this -> password = $this -> data [ 'password' ];
$this -> from = $this -> data [ 'from' ];
$this -> dlr_url = $this -> data [ 'dlr_url' ];
2022-03-15 02:24:28 +01:00
2021-11-05 23:26:09 +01:00
$this -> smsc = $this -> data [ 'smsc' ] ? ? null ;
}
/**
* Classname of the adapter .
*/
public static function meta_classname () : string
{
return __CLASS__ ;
}
/**
* Uniq name of the adapter
* It should be the classname of the adapter un snakecase .
*/
public static function meta_uid () : string
{
return 'kannel_adapter' ;
}
/**
* Should this adapter be hidden in user interface for phone creation and
* available to creation through API only .
*/
public static function meta_hidden () : bool
{
return false ;
}
2022-03-28 01:54:38 +02:00
/**
* Should this adapter data be hidden after creation
* this help to prevent API credentials to other service leak if an attacker gain access to RaspiSMS through user credentials .
*/
public static function meta_hide_data () : bool
{
return false ;
}
2021-11-05 23:26:09 +01:00
/**
* Name of the adapter .
* It should probably be the name of the service it adapt ( e . g : Gammu SMSD , OVH SMS , SIM800L , etc . ) .
*/
public static function meta_name () : string
{
return 'Kannel' ;
}
/**
* Description of the adapter .
* A short description of the service the adapter implements .
*/
public static function meta_description () : string
{
$kannel_homepage = 'https://www.kannel.org' ;
return '
Envoi de SMS via le logiciel Kannel , pour plus d\ 'information sur Kannel, voir <a target="_blank" href="' . $kannel_homepage . ' " >le site du projet.</a><br/>
Pour plus d\ ' information sur l\ ' utilisation de ce type de téléphone , reportez - vous à < a href = " https://documentation.raspisms.fr/users/adapters/kannel.html " target = " _blank " > la documentation sur le téléphone " Kannel " .</ a >
' ;
}
/**
* List of entries we want in data for the adapter .
*
* @ return array : Every line is a field as an array with keys : name , title , description , required
*/
public static function meta_data_fields () : array
{
return [
[
'name' => 'kannel_sendsms_url' ,
'title' => 'Adresse URL du service kannel sendsms' ,
'description' => 'Adresse URL du service sendsms de Kannel (ex : http://smsbox.host.name:13013/cgi-bin/sendsms)' ,
'required' => true ,
],
[
'name' => 'username' ,
'title' => 'Nom de l\'utilisateur' ,
'description' => 'Nom d\'utilisateur du service send-sms de Kannel.' ,
'required' => true ,
],
[
'name' => 'password' ,
'title' => 'Mot de passe de l\'utilisateur' ,
'description' => 'Mot de passe de l\'utilisateur du service send-sms de Kannel.' ,
'required' => true ,
],
[
'name' => 'from' ,
'title' => 'Numéro de téléphone émetteur ou nom de l\'émetteur' ,
'description' => 'Numéro de téléphone à transmettre au SMS Center, ou nom à afficher à la place du numéro (dans ce cas, entre 3 et 11 caractères), dans la très grande majorité des cas, ce numéro ou ce nom sera écrasé par le SMSC.' ,
'required' => true ,
],
[
'name' => 'dlr_url' ,
'title' => 'Adresse URL de livraison du Delivery Report du SMS' ,
'description' => 'Adresse URL de livraison du Delivery Report du SMS qui sera transmis à Kannel. Vous devriez probablement laisser ce champs tel quel.' ,
'required' => true ,
'default_value' => \descartes\Router :: url ( 'Callback' , 'update_sended_status' , [ 'adapter_uid' => self :: meta_uid ()], [ 'api_key' => $_SESSION [ 'user' ][ 'api_key' ] ? ? '' ]),
],
[
'name' => 'smsc' ,
'title' => 'Identifiant unique du SMSC' ,
'description' => ' Identifiant du SMSC ( sms - id ) à utiliser pour envoyer le message .< br />
< b > Laissez vide pour laisser Kannel décider du routage vers le SMSC .</ b > ' ,
'required' => false ,
],
];
}
/**
* Does the implemented service support reading smss .
*/
public static function meta_support_read () : bool
{
return false ;
}
2023-02-18 16:39:07 +01:00
/**
* Does the implemented service support updating phone status .
*/
public static function meta_support_phone_status () : bool
{
return false ;
}
2021-11-05 23:26:09 +01:00
/**
* Does the implemented service support flash smss .
*/
public static function meta_support_flash () : bool
{
return false ;
}
/**
* Does the implemented service support status change .
*/
public static function meta_support_status_change () : bool
{
return true ;
}
/**
* Does the implemented service support reception callback .
*/
public static function meta_support_reception () : bool
{
return true ;
}
/**
* Does the implemented service support mms reception .
*/
public static function meta_support_mms_reception () : bool
{
return false ;
}
/**
* Does the implemented service support mms sending .
*/
public static function meta_support_mms_sending () : bool
{
return false ;
}
public static function meta_support_inbound_call_callback () : bool
{
return false ;
}
public static function meta_support_end_call_callback () : bool
{
return false ;
}
public function send ( string $destination , string $text , bool $flash = false , bool $mms = false , array $medias = []) : array
{
$response = [
'error' => false ,
'error_message' => null ,
'uid' => null ,
];
try
{
//As kannel does not return uid of the SMS when sending it, we create our own uid and will pass it to kannel's delivery report url
//in order to retrieve it in raspisms and update the status
$sms_uid = Tool :: random_uuid ();
//Forge dlr Url by adding new query parts to url provided within phone settings
$dlr_url_parts = parse_url ( $this -> dlr_url );
//Append sms uid and delivery report value to the original dlr_url query parts
$dlr_url_parts [ 'query' ] = $dlr_url_parts [ 'query' ] ? ? '' ;
$dlr_url_query_parts = [];
parse_str ( $dlr_url_parts [ 'query' ], $dlr_url_query_parts );
unset ( $dlr_url_query_parts [ 'type' ]);
$dlr_url_query_parts [ 'sms_uid' ] = $sms_uid ; //Pass uid as param so raspisms can identify sms to update
$dlr_url_parts [ 'query' ] = http_build_query ( $dlr_url_query_parts ) . '&type=%d' ; //Kannel will replace %d by the delivery report value. We cannot set type in bild query or it get double encoded
$forged_dlr_url = Tool :: unparse_url ( $dlr_url_parts );
$data = [
'username' => $this -> username ,
'password' => $this -> password ,
'text' => $text ,
'to' => $destination ,
'from' => $this -> from ,
'dlr-mask' => self :: KANNEL_DLR_BITMASK ,
'dlr-url' => $forged_dlr_url ,
];
2021-11-05 23:59:38 +01:00
//If necessary, use utf8 sms to represent special chars
$use_utf8_sms = ! Quota :: is_gsm0338 ( $text );
if ( $use_utf8_sms )
{
$data [ 'coding' ] = self :: KANNEL_CODING_8_BITS ;
}
2021-11-05 23:26:09 +01:00
if ( $this -> smsc )
{
$data [ 'smsc' ] = $this -> smsc ;
}
$endpoint = $this -> kannel_sendsms_url . '?' . http_build_query ( $data );
$curl = curl_init ();
curl_setopt ( $curl , CURLOPT_URL , $endpoint );
curl_setopt ( $curl , CURLOPT_PROTOCOLS , CURLPROTO_HTTP | CURLPROTO_HTTPS );
curl_setopt ( $curl , CURLOPT_RETURNTRANSFER , true );
curl_setopt ( $curl , CURLOPT_FOLLOWLOCATION , true );
$curl_response = curl_exec ( $curl );
$http_code = ( int ) curl_getinfo ( $curl , CURLINFO_HTTP_CODE );
curl_close ( $curl );
if ( false === $curl_response )
{
$response [ 'error' ] = true ;
$response [ 'error_message' ] = 'HTTP query failed.' ;
return $response ;
}
if ( ! in_array ( $http_code , [ self :: KANNEL_SENDSMS_HTTP_CODE_ACCEPTED , self :: KANNEL_SENDSMS_HTTP_CODE_QUEUED ]))
{
$response [ 'error' ] = true ;
$response [ 'error_message' ] = 'Response error with HTTP code : ' . $http_code . ' -> ' . $curl_response ;
return $response ;
}
$response [ 'uid' ] = $sms_uid ;
return $response ;
}
catch ( \Throwable $t )
{
$response [ 'error' ] = true ;
$response [ 'error_message' ] = $t -> getMessage ();
return $response ;
}
}
public function read () : array
{
return [];
}
2023-02-18 16:39:07 +01:00
/**
* Method called to verify phone status
*
* @ return string : Return one phone status among 'available' , 'unavailable' , 'no_credit'
*/
public function check_phone_status () : string
{
return \models\Phone :: STATUS_AVAILABLE ;
}
2021-11-05 23:26:09 +01:00
public function test () : bool
{
try
{
if ( ! $this -> username || ! $this -> password || ! $this -> from || ! $this -> dlr_url )
{
return false ;
}
//Check kannel url is a valid http/https url to protect against ssrf
//This is mainly cosmetic, the real protection is in CURLOPT_PROTOCOLS
if ( ! mb_ereg_match ( '^http(s?)://' , $this -> kannel_sendsms_url ))
{
return false ;
}
//Check credentials and kannel url
$data = [
'username' => $this -> username ,
'password' => $this -> password ,
];
$endpoint = $this -> kannel_sendsms_url . '?' . http_build_query ( $data );
$curl = curl_init ();
curl_setopt ( $curl , CURLOPT_URL , $endpoint );
curl_setopt ( $curl , CURLOPT_PROTOCOLS , CURLPROTO_HTTP | CURLPROTO_HTTPS ); //Protect curl against non http(s) queries and redirects
curl_setopt ( $curl , CURLOPT_RETURNTRANSFER , true );
curl_setopt ( $curl , CURLOPT_FOLLOWLOCATION , true );
$curl_response = curl_exec ( $curl );
$http_code = ( int ) curl_getinfo ( $curl , CURLINFO_HTTP_CODE );
curl_close ( $curl );
if ( false === $curl_response )
{
return false ;
}
switch ( true )
{
2022-03-15 02:24:28 +01:00
case 403 == $http_code : //Bad credentials
case 404 == $http_code : //Cannot find url
2021-11-05 23:26:09 +01:00
return false ;
2022-03-15 02:24:28 +01:00
case $http_code >= 500 : //Server error
2021-11-05 23:26:09 +01:00
return false ;
}
if ( ! filter_var ( $this -> dlr_url , FILTER_VALIDATE_URL ))
{
return false ;
}
return true ;
}
catch ( \Throwable $t )
{
return false ;
}
}
public static function status_change_callback ()
{
$status = $_GET [ 'type' ] ? ? false ;
$uid = $_GET [ 'sms_uid' ] ? ? false ;
2022-03-15 02:24:28 +01:00
2021-11-05 23:26:09 +01:00
if ( ! $status || ! $uid )
{
return false ;
}
switch (( int ) $status )
{
case 1 :
$status = \models\Sended :: STATUS_DELIVERED ;
break ;
case 2 :
case 16 :
$status = \models\Sended :: STATUS_FAILED ;
break ;
default :
$status = \models\Sended :: STATUS_UNKNOWN ;
break ;
}
return [ 'uid' => $uid , 'status' => $status ];
}
public static function reception_callback () : array
{
$response = [
'error' => false ,
'error_message' => null ,
'sms' => null ,
];
header ( 'Connection: close' );
header ( 'Content-Encoding: none' );
header ( 'Content-Length: 0' );
$text = file_get_contents ( 'php://input' );
$number = $_SERVER [ 'HTTP_X_KANNEL_TO' ] ? ? false ;
$at = $_SERVER [ 'HTTP_X_KANNEL_TIME' ] ? ? false ;
if ( ! $number || ! $text || ! $at )
{
$response [ 'error' ] = true ;
$response [ 'error_message' ] = 'One required data of the callback is missing.' ;
return $response ;
}
$origin = \controllers\internals\Tool :: parse_phone ( $number );
if ( ! $origin )
{
$response [ 'error' ] = true ;
$response [ 'error_message' ] = 'Invalid origin number : ' . $number ;
return $response ;
}
$response [ 'sms' ] = [
'at' => $at ,
'text' => $text ,
'origin' => $origin ,
];
return $response ;
}
public function inbound_call_callback () : array
{
return [];
}
public function end_call_callback () : array
{
return [];
}
}