diff --git a/.gitignore b/.gitignore index 2b007f4..78ab9f5 100644 --- a/.gitignore +++ b/.gitignore @@ -8,8 +8,10 @@ scripts/ composer.lock env.* phinx.* +descartes/env.php data/test_write_sms.json data/test_read_sms.json +data/public/ !*.dist diff --git a/.htaccess b/.htaccess index e0be5de..ce35d68 100644 --- a/.htaccess +++ b/.htaccess @@ -1,4 +1,5 @@ RewriteEngine on RewriteRule ^assets - [L] RewriteRule ^.well-known - [L] +RewriteRule ^data/public/ - [L] RewriteRule . index.php diff --git a/VERSION b/VERSION index 545b772..6c8dc7e 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -v3.0.13 +v3.1.0 diff --git a/adapters/AdapterInterface.php b/adapters/AdapterInterface.php index 8760620..b7fa981 100644 --- a/adapters/AdapterInterface.php +++ b/adapters/AdapterInterface.php @@ -11,140 +11,196 @@ namespace adapters; +/** + * Interface for phones adapters + * Phone's adapters allow RaspiSMS to use a platform to communicate with a phone number. + * Its an adapter between internal and external code, as an API, command line software, physical modem, etc. + * + * All Phone Adapters must implement this interface + */ +interface AdapterInterface +{ /** - * Interface for phones adapters - * Phone's adapters allow RaspiSMS to use a platform to communicate with a phone number. - * Its an adapter between internal and external code, as an API, command line software, physical modem, etc. + * Adapter constructor, called when instanciated by RaspiSMS. * - * All Phone Adapters must implement this interface + * @param json string $data : JSON string of the data to configure interaction with the implemented service */ - interface AdapterInterface - { - /** - * Adapter constructor, called when instanciated by RaspiSMS. - * - * @param json string $data : JSON string of the data to configure interaction with the implemented service - */ - public function __construct(string $data); + public function __construct(string $data); - /** - * Classname of the adapter. - */ - public static function meta_classname(): string; + /** + * Classname of the adapter. + */ + public static function meta_classname(): string; - /** - * Uniq name of the adapter - * It should be the classname of the adapter un snakecase. - */ - public static function meta_uid(): string; + /** + * Uniq name of the adapter + * It should be the classname of the adapter un snakecase. + */ + public static function meta_uid(): string; - /** - * Should this adapter be hidden in user interface for phone creation and - * available to creation through API only. - */ - public static function meta_hidden(): bool; + /** + * Should this adapter be hidden in user interface for phone creation and + * available to creation through API only. + */ + public static function meta_hidden(): bool; - /** - * 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; + /** + * 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; - /** - * Description of the adapter. - * A short description of the service the adapter implements. - */ - public static function meta_description(): string; + /** + * Description of the adapter. + * A short description of the service the adapter implements. + */ + public static function meta_description(): string; - /** - * List of entries we want in data for the adapter. - * - * @return array : Eachline line is a field as an array with keys : name, title, description, required - */ - public static function meta_data_fields(): array; + /** + * List of entries we want in data for the adapter. + * + * @return array : Eachline line is a field as an array with keys : name, title, description, required + */ + public static function meta_data_fields(): array; - /** - * Does the implemented service support flash smss. - */ - public static function meta_support_flash(): bool; + /** + * Does the implemented service support flash smss. + */ + public static function meta_support_flash(): bool; - /** - * Does the implemented service support reading smss. - */ - public static function meta_support_read(): bool; + /** + * Does the implemented service support reading smss. + */ + public static function meta_support_read(): bool; - /** - * Does the implemented service support reception callback. - */ - public static function meta_support_reception(): bool; + /** + * Does the implemented service support reception callback. + */ + public static function meta_support_reception(): bool; - /** - * Does the implemented service support status change callback. - */ - public static function meta_support_status_change(): bool; + /** + * Does the implemented service support status change callback. + */ + public static function meta_support_status_change(): bool; - /** - * Method called to send a SMS to a number. - * - * @param string $destination : Phone number to send the sms to - * @param string $text : Text of the SMS to send - * @param bool $flash : Is the SMS a Flash SMS - * - * @return array : [ - * bool 'error' => false if no error, true else - * ?string 'error_message' => null if no error, else error message - * ?string 'uid' => Uid of the sms created on success - * ] - */ - public function send(string $destination, string $text, bool $flash = false); + /** + * Does the implemented service support mms reception + */ + public static function meta_support_mms_reception(): bool; - /** - * Method called to read SMSs of the number. - * - * @return array : [ - * bool 'error' => false if no error, true else - * ?string 'error_message' => null if no error, else error message - * array 'smss' => Array of the sms reads - * [ - * [ - * string 'at' => sms reception date, - * string 'text' => sms text, - * string 'origin' => phone number who sent the sms - * ], - * ... - * ] - * ] - */ - public function read(): array; + /** + * Does the implemented service support mms sending + */ + public static function meta_support_mms_sending(): bool; + + /** + * Does the implemented service support inbound call callback + */ + public static function meta_support_inbound_call_callback(): bool; + + /** + * Does the implemented service support end call callback + */ + public static function meta_support_end_call_callback(): bool; - /** - * Method called to verify if the adapter is working correctly - * should be use for exemple to verify that credentials and number are both valid. - * - * @return bool : False on error, true else - */ - public function test(): bool; + /** + * Method called to send a SMS to a number. + * + * @param string $destination : Phone number to send the sms to + * @param string $text : Text of the SMS to send + * @param bool $flash : Is the SMS a Flash SMS + * @param bool $mms : Is the SMS a MMS + * @param array $medias : Array of medias to link to the MMS, [['http_url' => HTTP public url of the media et 'local_uri' => local uri to media file]] + * + * @return array : [ + * bool 'error' => false if no error, true else + * ?string 'error_message' => null if no error, else error message + * array 'uid' => Uid of the sms created on success + * ] + */ + public function send(string $destination, string $text, bool $flash = false, bool $mms = false, array $medias = []) : array; - /** - * Method called on reception of a status update notification for a SMS. - * - * @return mixed : False on error, else array ['uid' => uid of the sms, 'status' => New status of the sms (\models\Sended::STATUS_UNKNOWN, \models\Sended::STATUS_DELIVERED, \models\Sended::STATUS_FAILED)] - */ - public static function status_change_callback(); + /** + * Method called to read SMSs of the number. + * + * @return array : [ + * bool 'error' => false if no error, true else + * ?string 'error_message' => null if no error, else error message + * array 'smss' => Array of the sms reads [[ + * (optional) bool 'mms' => default to false, true if mms + * (optional) array 'medias' => default to [], list of array representing medias to link to sms, with [ + * 'filepath' => local file copy of the media, + * 'extension' (optional) => extension of the media, + * 'mimetype' (optional) => mimetype of the media + * ] + * ], ...] + * ] + */ + public function read(): array; - /** - * Method called on reception of a sms notification. - * - * @return array : [ - * bool 'error' => false on success, true on error - * ?string 'error_message' => null on success, error message else - * array 'sms' => array [ - * string 'at' : Recepetion date format Y-m-d H:i:s, - * string 'text' : SMS body, - * string 'origin' : SMS sender, - * ] - * - * ] - */ - public static function reception_callback(): array; - } + /** + * Method called to verify if the adapter is working correctly + * should be use for exemple to verify that credentials and number are both valid. + * + * @return bool : False on error, true else + */ + public function test(): bool; + + /** + * Method called on reception of a status update notification for a SMS. + * + * @return mixed : False on error, else array ['uid' => uid of the sms, 'status' => New status of the sms (\models\Sended::STATUS_UNKNOWN, \models\Sended::STATUS_DELIVERED, \models\Sended::STATUS_FAILED)] + */ + public static function status_change_callback(); + + /** + * Method called on reception of a sms notification. + * + * @return array : [ + * bool 'error' => false on success, true on error + * ?string 'error_message' => null on success, error message else + * array 'sms' => array [ + * string 'at' : Recepetion date format Y-m-d H:i:s, + * string 'text' : SMS body, + * string 'origin' : SMS sender, + * (optional) array 'medias' => default to [], list of array representing medias to link to sms, with [ + * 'filepath' => local file copy of the media, + * 'extension' (optional) => extension of the media, + * 'mimetype' (optional) => mimetype of the media + * ] + * ] + * ] + */ + public static function reception_callback(): array; + + /** + * Method called on reception of an inbound_call notification + * + * @return array : [ + * bool 'error' => false on success, true on error + * ?string 'error_message' => null on success, error message else + * array 'call' => array [ + * string 'uid' : Uid of the call on the adapter plateform + * string 'start' : Start of the call date format Y-m-d H:i:s, + * ?string 'end' : End of the call date format Y-m-d H:i:s. If no known end, NULL + * string 'origin' : Emitter phone call number. International format. + * ] + * ] + */ + public function inbound_call_callback(): array; + + + /** + * Method called on reception of a end call notification + * + * @return array : [ + * bool 'error' => false on success, true on error + * ?string 'error_message' => null on success, error message else + * array 'call' => array [ + * string 'uid' : Uid of the call on the adapter plateform. Used to find the raspisms local call to update. + * string 'end' : End of the call date format Y-m-d H:i:s. + * ] + * ] + */ + public function end_call_callback(): array; +} diff --git a/adapters/BenchmarkAdapter.php b/adapters/BenchmarkAdapter.php index e874c8f..c872782 100644 --- a/adapters/BenchmarkAdapter.php +++ b/adapters/BenchmarkAdapter.php @@ -125,21 +125,34 @@ namespace adapters; { return false; } + + /** + * Does the implemented service support mms reception + */ + public static function meta_support_mms_reception(): bool + { + return false; + } /** - * Method called to send a SMS to a number. - * - * @param string $destination : Phone number to send the sms to - * @param string $text : Text of the SMS to send - * @param bool $flash : Is the SMS a Flash SMS - * - * @return array : [ - * bool 'error' => false if no error, true else - * ?string 'error_message' => null if no error, else error message - * int 'uid' => Uid of the sms created on success - * ] + * Does the implemented service support mms sending */ - public function send(string $destination, string $text, bool $flash = false) + 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, @@ -213,14 +226,18 @@ namespace adapters; return []; } - /** - * Method called to verify if the adapter is working correctly - * should be use for exemple to verify that credentials and number are both valid. - * - * @return bool : False on error, true else - */ public function test(): bool { return true; } + + public function inbound_call_callback(): array + { + return []; + } + + public function end_call_callback(): array + { + return []; + } } diff --git a/adapters/GammuAdapter.php b/adapters/GammuAdapter.php index cf1bb18..3fd8a45 100644 --- a/adapters/GammuAdapter.php +++ b/adapters/GammuAdapter.php @@ -135,21 +135,34 @@ namespace adapters; { return false; } + + /** + * Does the implemented service support mms reception + */ + public static function meta_support_mms_reception(): bool + { + return false; + } /** - * Method called to send a SMS to a number. - * - * @param string $destination : Phone number to send the sms to - * @param string $text : Text of the SMS to send - * @param bool $flash : Is the SMS a Flash SMS - * - * @return array : [ - * bool 'error' => false if no error, true else - * ?string 'error_message' => null if no error, else error message - * ?string 'uid' => Uid of the sms created on success, null on error - * ] + * Does the implemented service support mms sending */ - public function send(string $destination, string $text, bool $flash = false) + 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, @@ -230,15 +243,6 @@ namespace adapters; return $response; } - /** - * Method called to read SMSs of the number. - * - * @return array : [ - * bool 'error' => false if no error, true else - * ?string 'error_message' => null if no error, else error message - * array 'sms' => Array of the sms reads - * ] - */ public function read(): array { $response = [ @@ -287,46 +291,31 @@ namespace adapters; return $response; } - /** - * Method called to verify if the adapter is working correctly - * should be use for exemple to verify that credentials and number are both valid. - * - * @return bool : False on error, true else - */ public function test(): bool { //Always return true as we cannot test because we would be needing a root account return true; } - /** - * Method called on reception of a status update notification for a SMS. - * - * @return mixed : False on error, else array ['uid' => uid of the sms, 'status' => New status of the sms (\models\Sended::STATUS_UNKNOWN, \models\Sended::STATUS_DELIVERED, \models\Sended::STATUS_FAILED)] - */ public static function status_change_callback() { return false; } - /** - * Method called on reception of a sms notification. - * - * @return array : [ - * bool 'error' => false on success, true on error - * ?string 'error_message' => null on success, error message else - * array 'sms' => array [ - * string 'at' : Recepetion date format Y-m-d H:i:s, - * string 'text' : SMS body, - * string 'origin' : SMS sender, - * ] - * - * ] - */ public static function reception_callback(): array { return []; } + + public function inbound_call_callback(): array + { + return []; + } + + public function end_call_callback(): array + { + return []; + } /** * Function to unlock pin. diff --git a/adapters/OctopushShortcodeAdapter.php b/adapters/OctopushShortcodeAdapter.php index 667a872..e2d4240 100644 --- a/adapters/OctopushShortcodeAdapter.php +++ b/adapters/OctopushShortcodeAdapter.php @@ -172,21 +172,34 @@ class OctopushShortcodeAdapter implements AdapterInterface { return true; } + + /** + * Does the implemented service support mms reception + */ + public static function meta_support_mms_reception(): bool + { + return false; + } /** - * Method called to send a SMS to a number. - * - * @param string $destination : Phone number to send the sms to - * @param string $text : Text of the SMS to send - * @param bool $flash : Is the SMS a Flash SMS - * - * @return array : [ - * bool 'error' => false if no error, true else - * ?string 'error_message' => null if no error, else error message - * array 'uid' => Uid of the sms created on success - * ] + * Does the implemented service support mms sending */ - public function send(string $destination, string $text, bool $flash = false) + 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, @@ -268,26 +281,11 @@ class OctopushShortcodeAdapter implements AdapterInterface } } - /** - * Method called to read SMSs of the number. - * - * @return array : [ - * bool 'error' => false if no error, true else - * ?string 'error_message' => null if no error, else error message - * array 'sms' => Array of the sms reads - * ] - */ public function read(): array { return []; } - /** - * Method called to verify if the adapter is working correctly - * should be use for exemple to verify that credentials and number are both valid. - * - * @return bool : False on error, true else - */ public function test(): bool { try @@ -339,11 +337,6 @@ class OctopushShortcodeAdapter implements AdapterInterface } } - /** - * Method called on reception of a status update notification for a SMS. - * - * @return mixed : False on error, else array ['uid' => uid of the sms, 'status' => New status of the sms (\models\Sended::STATUS_UNKNOWN, \models\Sended::STATUS_DELIVERED, \models\Sended::STATUS_FAILED)] - */ public static function status_change_callback() { header('Connection: close'); @@ -383,20 +376,6 @@ class OctopushShortcodeAdapter implements AdapterInterface return ['uid' => $uid, 'status' => $status]; } - /** - * Method called on reception of a sms notification. - * - * @return array : [ - * bool 'error' => false on success, true on error - * ?string 'error_message' => null on success, error message else - * array 'sms' => array [ - * string 'at' : Recepetion date format Y-m-d H:i:s, - * string 'text' : SMS body, - * string 'origin' : SMS sender, - * ] - * - * ] - */ public static function reception_callback(): array { $response = [ @@ -438,4 +417,14 @@ class OctopushShortcodeAdapter implements AdapterInterface return $response; } + + public function inbound_call_callback(): array + { + return []; + } + + public function end_call_callback(): array + { + return []; + } } diff --git a/adapters/OctopushVirtualNumberAdapter.php b/adapters/OctopushVirtualNumberAdapter.php index 1fc5305..d38eb29 100644 --- a/adapters/OctopushVirtualNumberAdapter.php +++ b/adapters/OctopushVirtualNumberAdapter.php @@ -177,21 +177,34 @@ class OctopushVirtualNumberAdapter implements AdapterInterface { return true; } + + /** + * Does the implemented service support mms reception + */ + public static function meta_support_mms_reception(): bool + { + return false; + } /** - * Method called to send a SMS to a number. - * - * @param string $destination : Phone number to send the sms to - * @param string $text : Text of the SMS to send - * @param bool $flash : Is the SMS a Flash SMS - * - * @return array : [ - * bool 'error' => false if no error, true else - * ?string 'error_message' => null if no error, else error message - * array 'uid' => Uid of the sms created on success - * ] + * Does the implemented service support mms sending */ - public function send(string $destination, string $text, bool $flash = false) + 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, @@ -268,26 +281,11 @@ class OctopushVirtualNumberAdapter implements AdapterInterface } } - /** - * Method called to read SMSs of the number. - * - * @return array : [ - * bool 'error' => false if no error, true else - * ?string 'error_message' => null if no error, else error message - * array 'sms' => Array of the sms reads - * ] - */ public function read(): array { return []; } - /** - * Method called to verify if the adapter is working correctly - * should be use for exemple to verify that credentials and number are both valid. - * - * @return bool : False on error, true else - */ public function test(): bool { try @@ -339,11 +337,6 @@ class OctopushVirtualNumberAdapter implements AdapterInterface } } - /** - * Method called on reception of a status update notification for a SMS. - * - * @return mixed : False on error, else array ['uid' => uid of the sms, 'status' => New status of the sms (\models\Sended::STATUS_UNKNOWN, \models\Sended::STATUS_DELIVERED, \models\Sended::STATUS_FAILED)] - */ public static function status_change_callback() { header('Connection: close'); @@ -383,20 +376,6 @@ class OctopushVirtualNumberAdapter implements AdapterInterface return ['uid' => $uid, 'status' => $status]; } - /** - * Method called on reception of a sms notification. - * - * @return array : [ - * bool 'error' => false on success, true on error - * ?string 'error_message' => null on success, error message else - * array 'sms' => array [ - * string 'at' : Recepetion date format Y-m-d H:i:s, - * string 'text' : SMS body, - * string 'origin' : SMS sender, - * ] - * - * ] - */ public static function reception_callback(): array { $response = [ @@ -438,4 +417,14 @@ class OctopushVirtualNumberAdapter implements AdapterInterface return $response; } + + public function inbound_call_callback(): array + { + return []; + } + + public function end_call_callback(): array + { + return []; + } } diff --git a/adapters/OvhSmsShortcodeAdapter.php b/adapters/OvhSmsShortcodeAdapter.php index dd68aff..9c6137b 100644 --- a/adapters/OvhSmsShortcodeAdapter.php +++ b/adapters/OvhSmsShortcodeAdapter.php @@ -169,21 +169,34 @@ namespace adapters; { return false; } + + /** + * Does the implemented service support mms reception + */ + public static function meta_support_mms_reception(): bool + { + return false; + } /** - * Method called to send a SMS to a number. - * - * @param string $destination : Phone number to send the sms to - * @param string $text : Text of the SMS to send - * @param bool $flash : Is the SMS a Flash SMS - * - * @return array : [ - * bool 'error' => false if no error, true else - * ?string 'error_message' => null if no error, else error message - * ?string 'uid' => Uid of the sms created on success - * ] + * Does the implemented service support mms sending */ - public function send(string $destination, string $text, bool $flash = false) + 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, @@ -241,15 +254,6 @@ namespace adapters; } } - /** - * Method called to read SMSs of the number. - * - * @return array : [ - * bool 'error' => false if no error, true else - * ?string 'error_message' => null if no error, else error message - * array 'sms' => Array of the sms reads - * ] - */ public function read(): array { $response = [ @@ -306,12 +310,6 @@ namespace adapters; } } - /** - * Method called to verify if the adapter is working correctly - * should be use for exemple to verify that credentials and number are both valid. - * - * @return bool : False on error, true else - */ public function test(): bool { try @@ -335,11 +333,6 @@ namespace adapters; } } - /** - * Method called on reception of a status update notification for a SMS. - * - * @return mixed : False on error, else array ['uid' => uid of the sms, 'status' => New status of the sms (\models\Sended::STATUS_UNKNOWN, \models\Sended::STATUS_DELIVERED, \models\Sended::STATUS_FAILED)] - */ public static function status_change_callback() { $uid = $_GET['id'] ?? false; @@ -372,22 +365,18 @@ namespace adapters; return ['uid' => $uid, 'status' => $status]; } - /** - * Method called on reception of a sms notification. - * - * @return array : [ - * bool 'error' => false on success, true on error - * ?string 'error_message' => null on success, error message else - * array 'sms' => array [ - * string 'at' : Recepetion date format Y-m-d H:i:s, - * string 'text' : SMS body, - * string 'origin' : SMS sender, - * ] - * - * ] - */ public static function reception_callback(): array { return []; } + + public function inbound_call_callback(): array + { + return []; + } + + public function end_call_callback(): array + { + return []; + } } diff --git a/adapters/OvhSmsVirtualNumberAdapter.php b/adapters/OvhSmsVirtualNumberAdapter.php index 1217295..35925a3 100644 --- a/adapters/OvhSmsVirtualNumberAdapter.php +++ b/adapters/OvhSmsVirtualNumberAdapter.php @@ -180,21 +180,34 @@ namespace adapters; { return false; } + + /** + * Does the implemented service support mms reception + */ + public static function meta_support_mms_reception(): bool + { + return false; + } /** - * Method called to send a SMS to a number. - * - * @param string $destination : Phone number to send the sms to - * @param string $text : Text of the SMS to send - * @param bool $flash : Is the SMS a Flash SMS - * - * @return array : [ - * bool 'error' => false if no error, true else - * ?string 'error_message' => null if no error, else error message - * array 'uid' => Uid of the sms created on success - * ] + * Does the implemented service support mms sending */ - public function send(string $destination, string $text, bool $flash = false) + 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, @@ -245,15 +258,6 @@ namespace adapters; } } - /** - * Method called to read SMSs of the number. - * - * @return array : [ - * bool 'error' => false if no error, true else - * ?string 'error_message' => null if no error, else error message - * array 'sms' => Array of the sms reads - * ] - */ public function read(): array { $response = [ @@ -304,12 +308,6 @@ namespace adapters; } } - /** - * Method called to verify if the adapter is working correctly - * should be use for exemple to verify that credentials and number are both valid. - * - * @return bool : False on error, true else - */ public function test(): bool { try @@ -334,11 +332,6 @@ namespace adapters; } } - /** - * Method called on reception of a status update notification for a SMS. - * - * @return mixed : False on error, else array ['uid' => uid of the sms, 'status' => New status of the sms (\models\Sended::STATUS_UNKNOWN, \models\Sended::STATUS_DELIVERED, \models\Sended::STATUS_FAILED)] - */ public static function status_change_callback() { $uid = $_GET['id'] ?? false; @@ -371,22 +364,18 @@ namespace adapters; return ['uid' => $uid, 'status' => $status]; } - /** - * Method called on reception of a sms notification. - * - * @return array : [ - * bool 'error' => false on success, true on error - * ?string 'error_message' => null on success, error message else - * array 'sms' => array [ - * string 'at' : Recepetion date format Y-m-d H:i:s, - * string 'text' : SMS body, - * string 'origin' : SMS sender, - * ] - * - * ] - */ public static function reception_callback(): array { return []; } + + public function inbound_call_callback(): array + { + return []; + } + + public function end_call_callback(): array + { + return []; + } } diff --git a/adapters/TestAdapter.php b/adapters/TestAdapter.php index 2851fb6..3745c71 100644 --- a/adapters/TestAdapter.php +++ b/adapters/TestAdapter.php @@ -130,21 +130,34 @@ namespace adapters; { return false; } + + /** + * Does the implemented service support mms reception + */ + public static function meta_support_mms_reception(): bool + { + return true; + } /** - * Method called to send a SMS to a number. - * - * @param string $destination : Phone number to send the sms to - * @param string $text : Text of the SMS to send - * @param bool $flash : Is the SMS a Flash SMS - * - * @return array : [ - * bool 'error' => false if no error, true else - * ?string 'error_message' => null if no error, else error message - * array 'uid' => Uid of the sms created on success - * ] + * Does the implemented service support mms sending */ - public function send(string $destination, string $text, bool $flash = false) + public static function meta_support_mms_sending(): bool + { + return true; + } + + public static function meta_support_inbound_call_callback(): bool + { + return true; + } + + public static function meta_support_end_call_callback(): bool + { + return true; + } + + public function send(string $destination, string $text, bool $flash = false, bool $mms = false, array $medias = []) : array { $response = [ 'error' => false, @@ -155,7 +168,7 @@ namespace adapters; $uid = uniqid(); $at = (new \DateTime())->format('Y-m-d H:i:s'); - $success = file_put_contents($this->test_file_write, json_encode(['uid' => $uid, 'at' => $at, 'destination' => $destination, 'text' => $text, 'flash' => $flash]) . "\n", FILE_APPEND); + $success = file_put_contents($this->test_file_write, json_encode(['uid' => $uid, 'at' => $at, 'destination' => $destination, 'text' => $text, 'flash' => $flash, 'mms' => $mms, 'medias' => $medias]) . "\n", FILE_APPEND); if (false === $success) { $response['error'] = true; @@ -170,14 +183,19 @@ namespace adapters; } /** - * Method called to read SMSs of the number. - * - * @return array : [ - * bool 'error' => false if no error, true else - * ?string 'error_message' => null if no error, else error message - * array 'sms' => Array of the sms reads - * ] - */ + * Read from a files to simulate sms reception. + * In the file we expect a series of lines, each line beeing a SMS as a json string of format : + * { + * "at" : "2021-03-26 11:21:48", + * "medias" : [ + * "https://unsplash.com/photos/q4DJVtxES0w/download?force=true&w=640", + * "/tmp/somelocalfile.jpg" + * ], + * "mms" : true, + * "origin" : "+33612345678", + * "text" : "SMS Text" + * } + */ public function read(): array { $response = [ @@ -217,7 +235,36 @@ namespace adapters; continue; } - $response['smss'][] = $decode_sms; + $clean_sms = [ + 'at' => $decode_sms['at'], + 'text' => $decode_sms['text'], + 'origin' => $decode_sms['origin'], + 'mms' => $decode_sms['mms'], + 'medias' => [], + ]; + + //In medias we want a media URI or URL + foreach ($decode_sms['medias'] ?? [] as $media) + { + $tempfile = tempnam('/tmp', 'raspisms-media-'); + if (!$tempfile) + { + continue; + } + + $copy = copy($media, $tempfile); + if (!$copy) + { + continue; + } + + $clean_sms['medias'][] = [ + 'filepath' => $tempfile, + 'extension' => pathinfo($media, PATHINFO_EXTENSION) ?: null, + ]; + } + + $response['smss'][] = $clean_sms; } return $response; @@ -231,20 +278,11 @@ namespace adapters; } } - /** - * Method called to verify if the adapter is working correctly - * should be use for exemple to verify that credentials and number are both valid. - * - * @return bool : False on error, true else - */ public function test(): bool { return true; } - /** - * Method called on reception of a status update notification for a SMS. - */ public static function status_change_callback() { $uid = $_GET['uid'] ?? false; @@ -281,22 +319,62 @@ namespace adapters; return $return; } - /** - * Method called on reception of a sms notification. - * - * @return array : [ - * bool 'error' => false on success, true on error - * ?string 'error_message' => null on success, error message else - * array 'sms' => array [ - * string 'at' : Recepetion date format Y-m-d H:i:s, - * string 'text' : SMS body, - * string 'origin' : SMS sender, - * ] - * - * ] - */ public static function reception_callback(): array { return []; } + + public function inbound_call_callback(): array + { + $response = [ + 'error' => false, + 'error_message' => null, + 'call' => [], + ]; + + $uid = $_POST['uid'] ?? false; + $start = $_POST['start'] ?? false; + $end = $_POST['end'] ?? null; + $origin = $_POST['origin'] ?? false; + + if (!$uid || !$start || !$origin) + { + $response['error'] = true; + $response['error_message'] = 'Missing required argument.'; + + return $response; + } + + $response['call']['uid'] = $uid; + $response['call']['start'] = $start; + $response['call']['end'] = $end; + $response['call']['origin'] = $origin; + + return $response; + } + + public function end_call_callback(): array + { + $response = [ + 'error' => false, + 'error_message' => null, + 'call' => [], + ]; + + $uid = $_POST['uid'] ?? false; + $end = $_POST['end'] ?? null; + + if (!$uid || !$end) + { + $response['error'] = true; + $response['error_message'] = 'Missing required argument.'; + + return $response; + } + + $response['call']['uid'] = $uid; + $response['call']['end'] = $end; + + return $response; + } } diff --git a/adapters/TwilioVirtualNumberAdapter.php b/adapters/TwilioVirtualNumberAdapter.php index 2835d84..a753d9e 100644 --- a/adapters/TwilioVirtualNumberAdapter.php +++ b/adapters/TwilioVirtualNumberAdapter.php @@ -176,19 +176,32 @@ class TwilioVirtualNumberAdapter implements AdapterInterface } /** - * Method called to send a SMS to a number. - * - * @param string $destination : Phone number to send the sms to - * @param string $text : Text of the SMS to send - * @param bool $flash : Is the SMS a Flash SMS - * - * @return array : [ - * bool 'error' => false if no error, true else - * ?string 'error_message' => null if no error, else error message - * array 'uid' => Uid of the sms created on success - * ] + * Does the implemented service support mms reception */ - public function send(string $destination, string $text, bool $flash = false) + 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, @@ -228,15 +241,6 @@ class TwilioVirtualNumberAdapter implements AdapterInterface } } - /** - * Method called to read SMSs of the number. - * - * @return array : [ - * bool 'error' => false if no error, true else - * ?string 'error_message' => null if no error, else error message - * array 'sms' => Array of the sms reads - * ] - */ public function read(): array { $response = [ @@ -282,12 +286,6 @@ class TwilioVirtualNumberAdapter implements AdapterInterface } } - /** - * Method called to verify if the adapter is working correctly - * should be use for exemple to verify that credentials and number are both valid. - * - * @return bool : False on error, true else - */ public function test(): bool { try @@ -313,11 +311,6 @@ class TwilioVirtualNumberAdapter implements AdapterInterface } } - /** - * Method called on reception of a status update notification for a SMS. - * - * @return mixed : False on error, else array ['uid' => uid of the sms, 'status' => New status of the sms (\models\Sended::STATUS_UNKNOWN, \models\Sended::STATUS_DELIVERED, \models\Sended::STATUS_FAILED)] - */ public static function status_change_callback() { $sid = $_REQUEST['MessageSid'] ?? false; @@ -349,22 +342,18 @@ class TwilioVirtualNumberAdapter implements AdapterInterface return ['uid' => $sid, 'status' => $status]; } - /** - * Method called on reception of a sms notification. - * - * @return array : [ - * bool 'error' => false on success, true on error - * ?string 'error_message' => null on success, error message else - * array 'sms' => array [ - * string 'at' : Recepetion date format Y-m-d H:i:s, - * string 'text' : SMS body, - * string 'origin' : SMS sender, - * ] - * - * ] - */ public static function reception_callback(): array { return []; } + + public function inbound_call_callback(): array + { + return []; + } + + public function end_call_callback(): array + { + return []; + } } diff --git a/assets/css/style.css b/assets/css/style.css index 8be577c..1bda82d 100644 --- a/assets/css/style.css +++ b/assets/css/style.css @@ -205,6 +205,34 @@ footer img font-size: 0.9em; } +.discussion-message-medias img +{ + transition-duration: 0.1s; + max-width: 150px; + max-height: 150px; + line-height: 150px; + width: auto; + height: auto; +} + +.discussion-message-medias img:hover +{ + opacity: 0.6; +} + +.discussion-message-media +{ + display: inline-block; + line-height: 160px; + width: 160px; + height: 160px; + padding: 5px; + background-color: rgba(255, 255, 255, 0.3); + vertical-align: middle; + text-align: center; + margin: 5px; +} + .message-container { margin-bottom: 10px; @@ -254,6 +282,12 @@ footer img margin-top: 8px; } +.message-input input[type="file"] +{ + margin-top: 8px; + display: inline-block; +} + .message-in-progress-hover { position: absolute; @@ -408,3 +442,21 @@ footer img { margin-bottom: 30px; } + + +/** + * Contacts conditional deletion + */ +.conditional-deletion-preview-container +{ + margin-top: 10px; + margin-bottom: 20px; + text-align: right; +} + +.conditional-deletion-preview-container label, +.conditional-deletion-preview-container select +{ + display: inline-block; + width: auto; +} diff --git a/assets/js/custom.js b/assets/js/custom.js index eb2b6f9..b25910a 100644 --- a/assets/js/custom.js +++ b/assets/js/custom.js @@ -45,8 +45,9 @@ function verifReceived() /** * Cette fonction permet de scroller au dernier message + * @param force: bool : should we force the scroll */ -function scrollDownDiscussion() +function scrollDownDiscussion(force) { var discussion_height = jQuery('.discussion-container').innerHeight(); var discussion_scroll_height = jQuery('.discussion-container')[0].scrollHeight; @@ -54,7 +55,7 @@ function scrollDownDiscussion() var scroll_before_end = discussion_scroll_height - (discussion_scroll_top + discussion_height); //On scroll uniquement si on a pas remonté plus haut que la moitié de la fenetre de discussion - if (scroll_before_end <= discussion_height / 2) + if (force || scroll_before_end <= discussion_height / 2) { jQuery('.discussion-container').animate({scrollTop: 1000000}); } @@ -115,8 +116,8 @@ jQuery(document).ready(function() var form = jQuery(this); var message = form.find('textarea').val(); var formData = new FormData(form[0]); - jQuery('.discussion-container').find('#send-message-spiner').remove(); - jQuery('.discussion-container').append('
'); + jQuery('.discussion-container').find('#send-message-spinner').remove(); + jQuery('.discussion-container').append('
'); scrollDownDiscussion(); jQuery.ajax({ url: form.attr('action'), @@ -130,7 +131,7 @@ jQuery(document).ready(function() if (!data.success) { showMessage(data.message.replace(//g, ">"), 0); - jQuery('.discussion-container').find('#send-message-spiner').remove(); + jQuery('.discussion-container').find('#send-message-spinner').remove(); } } }).done(function() diff --git a/composer.json b/composer.json index 608752a..7522544 100644 --- a/composer.json +++ b/composer.json @@ -12,7 +12,9 @@ "ovh/ovh": "^2.0", "twilio/sdk": "^6.1", "symfony/yaml": "^5.0", - "phpmailer/phpmailer": "^6.1" + "phpmailer/phpmailer": "^6.1", + "ralouphie/mimey": "^2.1", + "kreait/firebase-php": "^5.14" }, "require-dev": { } diff --git a/controllers/internals/Adapter.php b/controllers/internals/Adapter.php index 50ef643..1fae465 100644 --- a/controllers/internals/Adapter.php +++ b/controllers/internals/Adapter.php @@ -20,9 +20,9 @@ namespace controllers\internals; private const ADAPTERS_META_START = 'meta_'; /** - * List adapters using internal metas. + * List adapters with filepath and internal metas. * - * @return array + * @return array : ['adapter_filepath' => ['meta...' => value, ...], ...] */ public function list_adapters() { @@ -42,7 +42,7 @@ namespace controllers\internals; continue; } - $adapters[] = $metas; + $adapters[$file] = $metas; } return $adapters; @@ -116,4 +116,29 @@ namespace controllers\internals; return $metas; } + + /** + * List all adapters for a meta value + * + * @param $search_name : Name of the meta + * @param $search_value : Value of the meta + * + * @return array : Array with ['adapter filepath' => ['search_name' => value, ...], ...] + */ + public function list_adapters_with_meta_equal($search_name, $search_value) + { + $adapters = $this->list_adapters(); + return array_filter($adapters, function($metas) use ($search_name, $search_value) { + $match = false; + foreach ($metas as $name => $value) + { + if ($name === $search_name && $value === $search_value) + { + $match = true; + } + } + + return $match; + }); + } } diff --git a/controllers/internals/Call.php b/controllers/internals/Call.php new file mode 100644 index 0000000..4852c72 --- /dev/null +++ b/controllers/internals/Call.php @@ -0,0 +1,138 @@ + + * + * This source file is subject to the GPL-3.0 license that is bundled + * with this source code in the file LICENSE. + */ + +namespace controllers\internals; + + class Call extends StandardController + { + protected $model; + + /** + * Create a call. + * + * @param int $id_user : Id of the user + * @param int $id_phone : Id of the phone that emitted (outbound) or received (inbound) the call + * @param string $uid : Uid of the phone call + * @param string $direction : Direction of the call, \models\Call::DIRECTION_INBOUND | \models\Call::DIRECTION_OUTBOUND + * @param string $start : Date of the call beginning + * @param ?string $end : Date of the call end + * @param ?string $origin : Origin of the call or null if outbound + * @param ?string $destination : Destination of the call or null if inbound + * + * @return mixed bool|int : false on error, new call id else + */ + public function create(int $id_user, int $id_phone, string $uid, string $direction, string $start, ?string $end = null, ?string $origin = null, ?string $destination = null) + { + $call = [ + 'id_user' => $id_user, + 'id_phone' => $id_phone, + 'uid' => $uid, + 'start' => $start, + 'end' => $end, + 'direction' => $direction, + 'origin' => $origin, + 'destination' => $destination, + ]; + + if (!$origin && !$destination) + { + return false; + } + + switch ($direction) + { + case \models\Call::DIRECTION_OUTBOUND : + if (null === $destination) { return false; } + break; + + case \models\Call::DIRECTION_INBOUND : + if (null === $origin) { return false; } + break; + + default : + return false; + } + + if (!\controllers\internals\Tool::validate_date($start, 'Y-m-d H:i:s')) + { + return false; + } + + if (null !== $end && !\controllers\internals\Tool::validate_date($end, 'Y-m-d H:i:s')) + { + return false; + } + + if (null !== $end && new \DateTime($end) < new \DateTime($start)) + { + return false; + } + + $new_call_id = $this->get_model()->insert($call); + if (!$new_call_id) + { + return false; + } + + $call['id'] = $new_call_id; + + $internal_webhook = new Webhook($this->bdd); + $internal_webhook->trigger($id_user, \models\Webhook::TYPE_INBOUND_CALL, $call); + + return $new_call_id; + } + + + /** + * End a call + * + * @param int $id_user : Id of the user to end call for + * @param int $id_phone : If of the phone to end call for + * @param string $uid : Uid of the call to end + * @param string $end : End date of the call, format Y-m-d H:i:s + * + * @return bool : False if cannot end phone call, true else + */ + public function end(int $id_user, int $id_phone, string $uid, string $end) + { + if (!\controllers\internals\Tool::validate_date($end, 'Y-m-d H:i:s')) + { + return false; + } + + $call = $this->get_model()->get_by_uid_and_phone_for_user($id_user, $id_phone, $uid); + if (!$call) + { + return false; + } + + if (new \DateTime($end) < new \DateTime($call['start'])) + { + return false; + } + + $datas = [ + 'end' => $end, + ]; + + return (bool) $this->get_model()->update_for_user($id_user, $call['id'], $datas); + } + + /** + * Get the model for the Controller. + */ + protected function get_model(): \descartes\Model + { + $this->model = $this->model ?? new \models\Call($this->bdd); + + return $this->model; + } + } diff --git a/controllers/internals/Console.php b/controllers/internals/Console.php index ccb48b6..f2f3986 100644 --- a/controllers/internals/Console.php +++ b/controllers/internals/Console.php @@ -151,4 +151,23 @@ namespace controllers\internals; exit($success ? 0 : 1); } + + + /** + * Delete medias that are no longer usefull + */ + public function clean_unused_medias() + { + $bdd = \descartes\Model::_connect(DATABASE_HOST, DATABASE_NAME, DATABASE_USER, DATABASE_PASSWORD, 'UTF8'); + $internal_media = new \controllers\internals\Media($bdd); + + $medias = $internal_media->gets_unused(); + + foreach ($medias as $media) + { + $success = $internal_media->delete_for_user($media['id_user'], $media['id']); + + echo ($success === false ? '[KO]' : '[OK]') . ' - ' . $media['path'] . "\n"; + } + } } diff --git a/controllers/internals/Mailer.php b/controllers/internals/Mailer.php index 2df5d3c..04d7204 100755 --- a/controllers/internals/Mailer.php +++ b/controllers/internals/Mailer.php @@ -101,10 +101,11 @@ class Mailer extends \descartes\Controller * @param string $destination : email address to send email to * @param array $settings : Email settings * @param array $data : Data to inject into email template + * @param array $attachments : List of paths of files to attach to the mail * * @return bool : true on success, false on error */ - public function enqueue(string $destination, array $settings, array $data): bool + public function enqueue(string $destination, array $settings, array $data, array $attachments = []): bool { $response = $this->generate_body($settings, $data); @@ -113,6 +114,7 @@ class Mailer extends \descartes\Controller 'subject' => $settings['subject'], 'body' => $response['body'], 'alt_body' => $response['alt_body'], + 'attachments' => $attachments, ]; $error_code = null; diff --git a/controllers/internals/Media.php b/controllers/internals/Media.php index b02ff5f..2722aab 100644 --- a/controllers/internals/Media.php +++ b/controllers/internals/Media.php @@ -11,129 +11,299 @@ namespace controllers\internals; - class Media extends StandardController +class Media extends StandardController +{ + const DEFAULT_CHMOD = 0660; + + protected $model; + + /** + * Create a media. + * + * @param int $id_user : Id of the user + * @param string $tmpfile_path : Path of the temporary local copy of the media + * @param ?string $extension : Extension to use for the media + * + * @return int : Exception on error, new media id else + */ + public function create(int $id_user, string $tmpfile_path, ?string $extension = null) { - protected $model; - - /** - * Create a media. - * - * @param int $id_user : Id of the user - * @param int $id_scheduled : Id of the scheduled - * @param array $media : $_FILES media array - * - * @return bool : false on error, new media id else - */ - public function create(int $id_user, int $id_scheduled, array $media): bool + $user_path = \controllers\internals\Tool::create_user_public_path($id_user); + if (!file_exists($tmpfile_path)) { - $internal_scheduled = new Scheduled($this->bdd); - $scheduled = $internal_scheduled->get_for_user($id_user, $id_scheduled); - if (!$scheduled) - { - return false; - } - - $result_upload_media = \controllers\internals\Tool::upload_file($media); - if (false === $result_upload_media['success']) - { - return false; - } - - $data = [ - 'id_scheduled' => $id_scheduled, - 'path' => $result_upload_media['content'], - ]; - - return (bool) $this->get_model()->insert($data); + throw new \Exception('File ' . $tmpfile_path . ' does not exists.'); + } + + if (!is_readable($tmpfile_path)) + { + throw new \Exception('File ' . $tmpfile_path . ' is not readable.'); } - /** - * Update a media for a user. - * - * @param int $id_user : user id - * @param int $id_media : Media id - * @param int $id_scheduled : Id of the scheduled - * @param string $path : Path of the file - * - * @return bool : false on error, true on success - */ - public function update_for_user(int $id_user, int $id_media, int $id_scheduled, string $path): bool + $mimey = new \Mimey\MimeTypes; + $extension = $extension ?? $mimey->getExtension(mime_content_type($tmpfile_path)); + + $new_file_name = \controllers\internals\Tool::random_uuid() . '.' . $extension; + $new_file_path = $user_path . '/' . $new_file_name; + $new_file_relpath = $id_user . '/' . $new_file_name; + + if (!file_put_contents($new_file_path, 'a')) { - $media = [ - 'id_scheduled' => $id_scheduled, - 'path' => $path, - ]; - - $internal_scheduled = new Scheduled($this->bdd); - $scheduled = $this->get_for_user($id_user, $id_scheduled); - if (!$scheduled) - { - return false; - } - - return (bool) $this->get_model()->update_for_user($id_user, $id_media, $media); + throw new \Exception('pute de merde'); } - /** - * Delete a media for a user. - * - * @param int $id_user : User id - * @param int $id : Entry id - * - * @return int : Number of removed rows - */ - public function delete_for_user(int $id_user, int $id_media): bool + if (!rename($tmpfile_path, $new_file_path)) { - $media = $this->get_model()->get_for_user($id_user, $id_media); - if (!$media) - { - return false; - } - - unlink($media['path']); - - return $this->get_model()->delete_for_user($id_user, $id_media); + throw new \Exception('Cannot create file ' . $new_file_path); } - /** - * Delete a media for a scheduled and a user. - * - * @param int $id_user : User id - * @param int $id_scheduled : Scheduled id to delete medias for - * - * @return int : Number of removed rows - */ - public function delete_for_scheduled_and_user(int $id_user, int $id_scheduled): bool + if (!chown($new_file_path, fileowner($user_path))) { - $media = $this->get_model()->get_for_scheduled_and_user($id_user, $id_scheduled); - if ($media) - { - unlink($media['path']); - } - - return $this->get_model()->delete_for_scheduled_and_user($id_user, $id_scheduled); + throw new \Exception('Cannot give file ' . $new_file_path . ' to user : ' . fileowner($user_path)); + } + + if (!chgrp($new_file_path, filegroup($user_path))) + { + throw new \Exception('Cannot give file ' . $new_file_path . ' to group : ' . filegroup($user_path)); } - /** - * Find medias for a scheduled and a user. - * - * @param int $id_user : User id - * @param int $id_scheduled : Scheduled id to delete medias for - * - * @return mixed : Medias || false - */ - public function get_for_scheduled_and_user(int $id_user, int $id_scheduled) + if (!chmod($new_file_path, self::DEFAULT_CHMOD)) { - return $this->get_model()->get_for_scheduled_and_user($id_user, $id_scheduled); + throw new \Exception('Cannot give file ' . $new_file_path . ' rights : ' . self::DEFAULT_CHMOD); } - /** - * Get the model for the Controller. - */ - protected function get_model(): \descartes\Model - { - $this->model = $this->model ?? new \models\Media($this->bdd); + $data = [ + 'path' => $new_file_relpath, + 'id_user' => $id_user, + ]; - return $this->model; + $new_media_id = $this->get_model()->insert($data); + if (!$new_media_id) + { + throw new \Exception('Cannot insert media in database.'); + } + + return $new_media_id; + } + + /** + * Upload and create a media + * + * @param int $id_user : Id of the user + * @param array $file : array representing uploaded file, extracted from $_FILES['yourfile'] + * @return int : Raise exception on error or return new media id on success + */ + public function create_from_uploaded_file_for_user(int $id_user, array $file) + { + $upload_result = \controllers\internals\Tool::read_uploaded_file($file); + if ($upload_result['success'] !== true) + { + throw new \Exception($upload_result['content']); + } + + //Move uploaded file to a tmp file + if (!$tmp_file = tempnam('/tmp', 'raspisms-media-')) + { + throw new \Exception('Cannot create tmp file in /tmp to store the uploaded file.'); + } + + if (!move_uploaded_file($upload_result['tmp_name'], $tmp_file)) + { + throw new \Exception('Cannot move uploaded file to : ' . $tmp_file); + } + + return $this->create($id_user, $tmp_file, $upload_result['extension']); + } + + /** + * Link a media to a scheduled, a received or a sended message + * @param int $id_media : Id of the media + * @param string $resource_type : Type of resource to link the media to ('scheduled', 'received' or 'sended') + * @param int $resource_id : Id of the resource to link the media to + * + * @return mixed bool|int : false on error, the new link id else + */ + public function link_to(int $id_media, string $resource_type, int $resource_id) + { + switch ($resource_type) + { + case 'scheduled': + return $this->get_model()->insert_media_scheduled($id_media, $resource_id); + break; + + case 'received': + return $this->get_model()->insert_media_received($id_media, $resource_id); + break; + + case 'sended': + return $this->get_model()->insert_media_sended($id_media, $resource_id); + break; + + default: + return false; } } + + + /** + * Unlink a media of a scheduled, a received or a sended message + * @param int $id_media : Id of the media + * @param string $resource_type : Type of resource to unlink the media of ('scheduled', 'received' or 'sended') + * @param int $resource_id : Id of the resource to unlink the media of + * + * @return mixed bool : false on error, true on success + */ + public function unlink_of(int $id_media, int $resource_type, int $resource_id) + { + switch ($resource_type) + { + case 'scheduled': + return $this->get_model()->delete_media_scheduled($id_media, $resource_id); + break; + + case 'received': + return $this->get_model()->delete_media_received($id_media, $resource_id); + break; + + case 'sended': + return $this->get_model()->delete_media_sended($id_media, $resource_id); + break; + + default: + return false; + } + } + + /** + * Unlink all medias of a scheduled, a received or a sended message + * @param string $resource_type : Type of resource to unlink the media of ('scheduled', 'received' or 'sended') + * @param int $resource_id : Id of the resource to unlink the media of + * + * @return mixed bool : false on error, true on success + */ + public function unlink_all_of(string $resource_type, int $resource_id) + { + switch ($resource_type) + { + case 'scheduled': + return $this->get_model()->delete_all_for_scheduled($resource_id); + break; + + case 'received': + return $this->get_model()->delete_all_for_received($resource_id); + break; + + case 'sended': + return $this->get_model()->delete_all_for_sended($resource_id); + break; + + default: + return false; + } + } + + /** + * Update a media for a user. + * + * @param int $id_user : user id + * @param int $id_media : Media id + * @param string $path : Path of the file + * + * @return bool : false on error, true on success + */ + public function update_for_user(int $id_user, int $id_media, string $path): bool + { + $media = [ + 'path' => $path, + ]; + + return (bool) $this->get_model()->update_for_user($id_user, $id_media, $media); + } + + /** + * Delete a media for a user. + * + * @param int $id_user : User id + * @param int $id : Entry id + * + * @return mixed bool|int : False on error, else number of removed rows + */ + public function delete_for_user(int $id_user, int $id_media): bool + { + $media = $this->get_model()->get_for_user($id_user, $id_media); + if (!$media) + { + return false; + } + + //Delete file + try + { + $filepath = PWD_DATA_PUBLIC . '/' . $media['path']; + if (file_exists($filepath)) + { + unlink($filepath); + } + } + catch (\Throwable $t) + { + return false; + } + + return $this->get_model()->delete_for_user($id_user, $id_media); + } + + /** + * Find medias for a scheduled. + * + * @param int $id_scheduled : Scheduled id to fin medias for + * + * @return mixed : Medias || false + */ + public function gets_for_scheduled(int $id_scheduled) + { + return $this->get_model()->gets_for_scheduled($id_scheduled); + } + + /** + * Find medias for a sended and a user. + * + * @param int $id_sended : Scheduled id to fin medias for + * + * @return mixed : Medias || false + */ + public function gets_for_sended(int $id_sended) + { + return $this->get_model()->gets_for_sended($id_sended); + } + + /** + * Find medias for a received and a user. + * + * @param int $id_received : Scheduled id to fin medias for + * + * @return mixed : Medias || false + */ + public function gets_for_received(int $id_received) + { + return $this->get_model()->gets_for_received($id_received); + } + + /** + * Find medias that are not used + * @return array + */ + public function gets_unused() + { + return $this->get_model()->gets_unused(); + } + + /** + * Get the model for the Controller. + */ + protected function get_model(): \descartes\Model + { + $this->model = $this->model ?? new \models\Media($this->bdd); + + return $this->model; + } +} diff --git a/controllers/internals/Phone.php b/controllers/internals/Phone.php index 63a0b96..3b54c86 100644 --- a/controllers/internals/Phone.php +++ b/controllers/internals/Phone.php @@ -13,6 +13,10 @@ namespace controllers\internals; class Phone extends StandardController { + const MMS_SENDING = 'sending'; + const MMS_RECEPTION = 'reception'; + const MMS_BOTH = 'both'; + protected $model; /** @@ -39,6 +43,63 @@ namespace controllers\internals; return $this->get_model()->get_by_name($name); } + /** + * Check if a phone support mms + * + * @param int $id : id of the phone to check + * @param $type : type of sms support, a const from Phone, MMS_SENDING, MMS_RECEPTION or MMS_BOTH + * @return bool : true if support, false else + */ + public function support_mms (int $id, string $type) + { + $phone = $this->get_model()->get($id); + if (!$phone) + { + return false; + } + + switch ($type) + { + case self::MMS_SENDING : + return $phone['adapter']::meta_support_mms_sending(); + break; + + case self::MMS_RECEPTION : + return $phone['adapter']::meta_support_mms_reception(); + break; + + case self::MMS_BOTH : + return $phone['adapter']::meta_support_mms_sending() && $phone['adapter']::meta_support_mms_reception(); + break; + + default: + return false; + } + } + + /** + * Get all phones supporting mms for a user + * + * @param int $id_user : id of the user + * @param $type : type of sms support, a const from Phone, MMS_SENDING, MMS_RECEPTION or MMS_BOTH + * @return array : array of phones supporting mms + */ + public function gets_phone_supporting_mms_for_user (int $id_user, string $type) + { + $phones = $this->get_model()->gets_for_user($id_user); + + $valid_phones = []; + foreach ($phones as $phone) + { + if ($this->support_mms($phone['id'], $type)) + { + $valid_phones[] = $phone; + } + } + + return $valid_phones; + } + /** * Return a phone for a user by a name. * diff --git a/controllers/internals/Received.php b/controllers/internals/Received.php index 2293693..c7c2b79 100644 --- a/controllers/internals/Received.php +++ b/controllers/internals/Received.php @@ -39,10 +39,12 @@ namespace controllers\internals; * @param string $origin : Number of the sender * @param string $status : Status of the received message * @param bool $command : Is the sms a command + * @param bool $mms : Is the sms a mms + * @param array $media_ids : Ids of the medias to link to received * * @return mixed : false on error, new received id else */ - public function create(int $id_user, int $id_phone, $at, string $text, string $origin, string $status = 'unread', bool $command = false) + public function create(int $id_user, int $id_phone, $at, string $text, string $origin, string $status = 'unread', bool $command = false, bool $mms = false, array $media_ids = []) { $received = [ 'id_user' => $id_user, @@ -52,9 +54,39 @@ namespace controllers\internals; 'origin' => $origin, 'status' => $status, 'command' => $command, + 'mms' => $mms, ]; - return $this->get_model()->insert($received); + //use a transaction to ensure received and media links are created at the same time + $this->bdd->beginTransaction(); + + $id_received = $this->get_model()->insert($received); + if (!$id_received) + { + $this->bdd->rollBack(); + return false; + } + + //Link medias + $internal_media = new Media($this->bdd); + foreach ($media_ids as $media_id) + { + $id_media_received = $internal_media->link_to($media_id, 'received', $id_received); + if (!$id_media_received) + { + $this->bdd->rollBack(); + return false; + } + } + + //All ok, commit + $success = $this->bdd->commit(); + if (!$success) + { + return false; + } + + return $id_received; } /** @@ -129,6 +161,19 @@ namespace controllers\internals; return $this->get_model()->gets_by_origin_and_user($id_user, $origin); } + /** + * Return receiveds for an origin and a user since a date. + * + * @param int $id_user : User id + * @param string $since : Date we want messages since format Y-m-d H:i:s + * @param string $origin : Number who sent the message + * @return array + */ + public function gets_since_date_by_origin_and_user(int $id_user, string $since, string $origin) + { + return $this->get_model()->gets_since_date_by_origin_and_user($id_user, $since, $origin); + } + /** * Get number of sended SMS for every date since a date for a specific user. * @@ -211,13 +256,18 @@ namespace controllers\internals; * @param string $origin : Number of the sender * @param ?string $at : Message reception date, if null use current date * @param string $status : Status of a the sms. By default \models\Received::STATUS_UNREAD + * @param bool $mms : Is the sms a mms + * @param array $medias : Empty array if no medias, or medias to create and link to the received message. Format : [[ + * string 'filepath' => local path to a readable copy of the media, + * ?string 'extension' => extension to use for the file or null + * ], ...] * * @return array : [ * bool 'error' => false if success, true else * ?string 'error_message' => null if success, error message else * ] */ - public function receive(int $id_user, int $id_phone, string $text, string $origin, ?string $at = null, string $status = \models\Received::STATUS_UNREAD): array + public function receive(int $id_user, int $id_phone, string $text, string $origin, ?string $at = null, string $status = \models\Received::STATUS_UNREAD, bool $mms = false, array $medias = []): array { $return = [ 'error' => false, @@ -235,8 +285,28 @@ namespace controllers\internals; $is_command = true; $text = $response; } + + //We create medias to link to the sms + $internal_media = new Media($this->bdd); + $media_ids = []; + if ($mms) + { + foreach ($medias as $media) + { + try + { + $new_media_id = $internal_media->create($id_user, $media['filepath'], $media['extension']); + $media_ids[] = $new_media_id; + } + catch (\Throwable $t) + { + $return['error_message'] = $t->getMessage(); + continue; //Better loose the media than the message + } + } + } - $received_id = $this->create($id_user, $id_phone, $at, $text, $origin, $status, $is_command); + $received_id = $this->create($id_user, $id_phone, $at, $text, $origin, $status, $is_command, $mms, $media_ids); if (!$received_id) { $return['error'] = true; @@ -251,10 +321,13 @@ namespace controllers\internals; 'text' => $text, 'destination' => $id_phone, 'origin' => $origin, + 'command' => $is_command, + 'mms' => $mms, + 'medias' => $internal_media->gets_in_for_user($id_user, $media_ids), ]; $internal_webhook = new Webhook($this->bdd); - $internal_webhook->trigger($id_user, \models\Webhook::TYPE_RECEIVE, $received); + $internal_webhook->trigger($id_user, \models\Webhook::TYPE_RECEIVE_SMS, $received); $internal_user = new User($this->bdd); $internal_user->transfer_received($id_user, $received); diff --git a/controllers/internals/Scheduled.php b/controllers/internals/Scheduled.php index ae99361..a7c65df 100644 --- a/controllers/internals/Scheduled.php +++ b/controllers/internals/Scheduled.php @@ -23,14 +23,16 @@ namespace controllers\internals; * @param string $text : Text of the message * @param ?int $id_phone : Id of the phone to send message with, null by default * @param bool $flash : Is the sms a flash sms, by default false + * @param bool $mms : Is the sms a mms, by default false * @param array $numbers : Numbers to send message to * @param array $contacts_ids : Contact ids to send message to * @param array $groups_ids : Group ids to send message to * @param array $conditional_group_ids : Conditional Groups ids to send message to + * @param array $media_ids : Ids of the medias to link to scheduled message * * @return bool : false on error, new id on success */ - public function create(int $id_user, $at, string $text, ?int $id_phone = null, bool $flash = false, array $numbers = [], array $contacts_ids = [], array $groups_ids = [], array $conditional_group_ids = []) + public function create(int $id_user, $at, string $text, ?int $id_phone = null, bool $flash = false, bool $mms = false, array $numbers = [], array $contacts_ids = [], array $groups_ids = [], array $conditional_group_ids = [], array $media_ids = []) { $scheduled = [ 'id_user' => $id_user, @@ -38,8 +40,14 @@ namespace controllers\internals; 'text' => $text, 'id_phone' => $id_phone, 'flash' => $flash, + 'mms' => $mms, ]; + if ($text === '') + { + return false; + } + if (null !== $id_phone) { $internal_phone = new Phone($this->bdd); @@ -51,12 +59,28 @@ namespace controllers\internals; } } + //Use transaction to garanty atomicity + $this->bdd->beginTransaction(); + $id_scheduled = $this->get_model()->insert($scheduled); if (!$id_scheduled) { + $this->bdd->rollBack(); return false; } + $internal_media = new Media($this->bdd); + foreach ($media_ids as $media_id) + { + $id_media_scheduled = $internal_media->link_to($media_id, 'scheduled', $id_scheduled); + if (!$id_media_scheduled) + { + $this->bdd->rollBack(); + return false; + } + } + + foreach ($numbers as $number) { $this->get_model()->insert_scheduled_number($id_scheduled, $number); @@ -98,6 +122,12 @@ namespace controllers\internals; $this->get_model()->insert_scheduled_conditional_group_relation($id_scheduled, $conditional_group_id); } + $success = $this->bdd->commit(); + if (!$success) + { + return false; + } + $date = date('Y-m-d H:i:s'); $internal_event = new Event($this->bdd); $internal_event->create($id_user, 'SCHEDULED_ADD', 'Ajout d\'un Sms pour le ' . $date . '.'); @@ -114,20 +144,23 @@ namespace controllers\internals; * @param string $text : Text of the message * @param ?int $id_phone : Id of the phone to send message with, null by default * @param bool $flash : Is the sms a flash sms, by default false + * @param bool $mms : Is the sms a mms, by default false * @param array $numbers : Numbers to send message to * @param array $contacts_ids : Contact ids to send message to * @param array $groups_ids : Group ids to send message to * @param array $conditional_group_ids : Conditional Groups ids to send message to + * @param array $media_ids : Ids of the medias to link to scheduled message * - * @return bool : false on error, new id on success + * @return bool : false on error, true on success */ - public function update_for_user(int $id_user, int $id_scheduled, $at, string $text, ?string $id_phone = null, bool $flash = false, array $numbers = [], array $contacts_ids = [], array $groups_ids = [], array $conditional_group_ids = []) + public function update_for_user(int $id_user, int $id_scheduled, $at, string $text, ?string $id_phone = null, bool $flash = false, bool $mms = false, array $numbers = [], array $contacts_ids = [], array $groups_ids = [], array $conditional_group_ids = [], array $media_ids = []) { $scheduled = [ 'id_user' => $id_user, 'at' => $at, 'text' => $text, 'id_phone' => $id_phone, + 'mms' => $mms, 'flash' => $flash, ]; @@ -142,12 +175,27 @@ namespace controllers\internals; } } + //Ensure atomicity + $this->bdd->beginTransaction(); + $success = (bool) $this->get_model()->update_for_user($id_user, $id_scheduled, $scheduled); $this->get_model()->delete_scheduled_numbers($id_scheduled); $this->get_model()->delete_scheduled_contact_relations($id_scheduled); $this->get_model()->delete_scheduled_group_relations($id_scheduled); $this->get_model()->delete_scheduled_conditional_group_relations($id_scheduled); + $internal_media = new Media($this->bdd); + $internal_media->unlink_all_of('scheduled', $id_scheduled); + + foreach ($media_ids as $media_id) + { + $id_media_scheduled = $internal_media->link_to($media_id, 'scheduled', $id_scheduled); + if (!$id_media_scheduled) + { + $this->bdd->rollBack(); + return false; + } + } foreach ($numbers as $number) { @@ -190,7 +238,7 @@ namespace controllers\internals; $this->get_model()->insert_scheduled_conditional_group_relation($id_scheduled, $conditional_group_id); } - return true; + return $this->bdd->commit(); } /** @@ -206,11 +254,25 @@ namespace controllers\internals; { return $this->get_model()->gets_before_date_for_number_and_user($id_user, $date, $number); } + + /** + * Get messages scheduled after a date for a number and a user. + * + * @param int $id_user : User id + * @param $date : Date after which we want messages + * @param string $number : Number for which we want messages + * + * @return array + */ + public function gets_after_date_for_number_and_user(int $id_user, $date, string $number) + { + return $this->get_model()->gets_after_date_for_number_and_user($id_user, $date, $number); + } /** * Get all messages to send and the number to use to send theme. * - * @return array : [['id_scheduled', 'text', 'id_phone', 'destination', 'flash'], ...] + * @return array : [['id_scheduled', 'text', 'id_phone', 'destination', 'flash', 'mms', 'medias'], ...] */ public function get_smss_to_send() { @@ -224,6 +286,7 @@ namespace controllers\internals; $users_settings = []; $users_phones = []; + $users_mms_phones = []; $now = new \DateTime(); $now = $now->format('Y-m-d H:i:s'); @@ -244,7 +307,16 @@ namespace controllers\internals; if (!isset($users_phones[$scheduled['id_user']])) { $phones = $internal_phone->gets_for_user($scheduled['id_user']); + $mms_phones = $internal_phone->gets_phone_supporting_mms_for_user($scheduled['id_user'], $internal_phone::MMS_SENDING); $users_phones[$scheduled['id_user']] = $phones ?: []; + $users_mms_phones[$scheduled['id_user']] = $mms_phones ?: []; + } + + //Add medias to mms + if ($scheduled['mms']) + { + $internal_media = new Media($this->bdd); + $scheduled['medias'] = $internal_media->gets_for_scheduled($scheduled['id']); } $phone_to_use = null; @@ -266,8 +338,16 @@ namespace controllers\internals; { if (null === $phone_to_use) { - $rnd_key = array_rand($users_phones[$scheduled['id_user']]); - $random_phone = $users_phones[$scheduled['id_user']][$rnd_key]; + if ($scheduled['mms'] && count($users_mms_phones)) + { + $rnd_key = array_rand($users_mms_phones[$scheduled['id_user']]); + $random_phone = $users_mms_phones[$scheduled['id_user']][$rnd_key]; + } + else + { + $rnd_key = array_rand($users_phones[$scheduled['id_user']]); + $random_phone = $users_phones[$scheduled['id_user']][$rnd_key]; + } } $message = [ @@ -276,6 +356,8 @@ namespace controllers\internals; 'id_phone' => $phone_to_use['id'] ?? $random_phone['id'], 'destination' => $number['number'], 'flash' => $scheduled['flash'], + 'mms' => $scheduled['mms'], + 'medias' => $scheduled['medias'], ]; if ((int) ($users_settings[$scheduled['id_user']]['templating'] ?? false)) @@ -326,8 +408,16 @@ namespace controllers\internals; if (null === $phone_to_use) { - $rnd_key = array_rand($users_phones[$scheduled['id_user']]); - $random_phone = $users_phones[$scheduled['id_user']][$rnd_key]; + if ($scheduled['mms'] && count($users_mms_phones)) + { + $rnd_key = array_rand($users_mms_phones[$scheduled['id_user']]); + $random_phone = $users_mms_phones[$scheduled['id_user']][$rnd_key]; + } + else + { + $rnd_key = array_rand($users_phones[$scheduled['id_user']]); + $random_phone = $users_phones[$scheduled['id_user']][$rnd_key]; + } } $message = [ @@ -336,6 +426,8 @@ namespace controllers\internals; 'id_phone' => $phone_to_use['id'] ?? $random_phone['id'], 'destination' => $contact['number'], 'flash' => $scheduled['flash'], + 'mms' => $scheduled['mms'], + 'medias' => $scheduled['medias'], ]; if ((int) ($users_settings[$scheduled['id_user']]['templating'] ?? false)) @@ -368,7 +460,7 @@ namespace controllers\internals; foreach ($messages as $message) { //Remove empty messages - if ('' === trim($message['text'])) + if ('' === trim($message['text']) && !$message['medias']) { continue; } diff --git a/controllers/internals/Sended.php b/controllers/internals/Sended.php index 82bd14e..2e24327 100644 --- a/controllers/internals/Sended.php +++ b/controllers/internals/Sended.php @@ -26,11 +26,13 @@ namespace controllers\internals; * @param string $uid : Uid of the sms on the adapter service used * @param string $adapter : Name of the adapter service used to send the message * @param bool $flash : Is the sms a flash + * @param bool $mms : Is the sms a MMS. By default false. + * @param array $medias : Array of medias to link to the MMS. * @param string $status : Status of a the sms. By default \models\Sended::STATUS_UNKNOWN * * @return mixed : false on error, new sended id else */ - public function create(int $id_user, int $id_phone, $at, string $text, string $destination, string $uid, string $adapter, bool $flash = false, ?string $status = \models\Sended::STATUS_UNKNOWN) + public function create(int $id_user, int $id_phone, $at, string $text, string $destination, string $uid, string $adapter, bool $flash = false, bool $mms = false, array $medias = [], ?string $status = \models\Sended::STATUS_UNKNOWN) { $sended = [ 'id_user' => $id_user, @@ -41,10 +43,33 @@ namespace controllers\internals; 'uid' => $uid, 'adapter' => $adapter, 'flash' => $flash, + 'mms' => $mms, 'status' => $status, ]; - return $this->get_model()->insert($sended); + //Ensure atomicity + $this->bdd->beginTransaction(); + + $id_sended = $this->get_model()->insert($sended); + if (!$id_sended) + { + $this->bdd->rollback(); + return false; + } + + //Link medias + $internal_media = new Media($this->bdd); + foreach ($medias as $media) + { + $internal_media->link_to($media['id'], 'sended', $id_sended); //No rollback on error, keeping track of mms is more important than integrity + } + + if (!$this->bdd->commit()) + { + return false; + } + + return $id_sended; } /** @@ -107,6 +132,19 @@ namespace controllers\internals; { return $this->get_model()->gets_by_destination_and_user($id_user, $origin); } + + /** + * Return sendeds for a destination and a user since a date. + * + * @param int $id_user : User id + * @param string $since : Date we want messages since format Y-m-d H:i:s + * @param string $origin : Number who sent the message + * @return array + */ + public function gets_since_date_by_destination_and_user(int $id_user, string $since, string $origin) + { + return $this->get_model()->gets_since_date_by_destination_and_user($id_user, $since, $origin); + } /** * Return sended for an uid and an adapter. @@ -165,6 +203,8 @@ namespace controllers\internals; * @param $text : Text of the message * @param string $destination : Number of the receiver * @param bool $flash : Is the sms a flash. By default false. + * @param bool $mms : Is the sms a MMS. By default false. + * @param array $medias : Array of medias to link to the MMS. * @param string $status : Status of a the sms. By default \models\Sended::STATUS_UNKNOWN * * @return array : [ @@ -172,7 +212,7 @@ namespace controllers\internals; * ?string 'error_message' => null if success, error message else * ] */ - public function send(\adapters\AdapterInterface $adapter, int $id_user, int $id_phone, string $text, string $destination, bool $flash = false, string $status = \models\Sended::STATUS_UNKNOWN): array + public function send(\adapters\AdapterInterface $adapter, int $id_user, int $id_phone, string $text, string $destination, bool $flash = false, bool $mms = false, array $medias = [], string $status = \models\Sended::STATUS_UNKNOWN): array { $return = [ 'error' => false, @@ -180,19 +220,40 @@ namespace controllers\internals; ]; $at = (new \DateTime())->format('Y-m-d H:i:s'); - $response = $adapter->send($destination, $text, $flash); + $media_uris = []; + foreach ($medias as $media) + { + $media_uris[] = [ + 'path' => $media['path'], + 'local_uri' => PWD_DATA_PUBLIC . '/' . $media['path'], + ]; + } + + //If adapter does not support mms and the message is a mms, add medias as link + if (!$adapter::meta_support_mms_sending() && $mms) + { + $media_urls = []; + foreach ($media_uris as $media_uri) + { + $media_urls[] = STATIC_HTTP_URL . '/data/public/' . $media_uri['path']; + } + + $text .= "\n" . join(' - ', $media_urls); + } + + $response = $adapter->send($destination, $text, $flash, $mms, $media_uris); if ($response['error']) { $return['error'] = true; $return['error_message'] = $response['error_message']; $status = \models\Sended::STATUS_FAILED; - $this->create($id_user, $id_phone, $at, $text, $destination, $response['uid'] ?? uniqid(), $adapter->meta_classname(), $flash, $status); + $this->create($id_user, $id_phone, $at, $text, $destination, $response['uid'] ?? uniqid(), $adapter->meta_classname(), $flash, $mms, $medias, $status); return $return; } - $sended_id = $this->create($id_user, $id_phone, $at, $text, $destination, $response['uid'] ?? uniqid(), $adapter->meta_classname(), $flash, $status); + $sended_id = $this->create($id_user, $id_phone, $at, $text, $destination, $response['uid'] ?? uniqid(), $adapter->meta_classname(), $flash, $mms, $medias, $status); $sended = [ 'id' => $sended_id, @@ -200,10 +261,12 @@ namespace controllers\internals; 'text' => $text, 'destination' => $destination, 'origin' => $id_phone, + 'mms' => $mms, + 'medias' => $medias, ]; $internal_webhook = new Webhook($this->bdd); - $internal_webhook->trigger($id_user, \models\Webhook::TYPE_SEND, $sended); + $internal_webhook->trigger($id_user, \models\Webhook::TYPE_SEND_SMS, $sended); return $return; } diff --git a/controllers/internals/Tool.php b/controllers/internals/Tool.php index 843d066..a207185 100644 --- a/controllers/internals/Tool.php +++ b/controllers/internals/Tool.php @@ -205,7 +205,7 @@ namespace controllers\internals; * * @param array $file : The array extracted from $_FILES['file'] * - * @return array : ['success' => bool, 'content' => file handler | error message, 'error_code' => $file['error']] + * @return array : ['success' => bool, 'content' => file handler | error message, 'error_code' => $file['error'], 'mime_type' => server side calculated mimetype, 'extension' => original extension, 'tmp_name' => name of the tmp_file] */ public static function read_uploaded_file(array $file) { @@ -213,8 +213,9 @@ namespace controllers\internals; 'success' => false, 'content' => 'Une erreur inconnue est survenue.', 'error_code' => $file['error'] ?? 99, - 'mime_type' => false, - 'extension' => false, + 'mime_type' => null, + 'extension' => null, + 'tmp_name' => null, ]; if (UPLOAD_ERR_OK !== $file['error']) @@ -266,7 +267,8 @@ namespace controllers\internals; return $result; } - $result['extension'] = pathinfo($file['name'])['extension']; + $result['tmp_name'] = $tmp_filename; + $result['extension'] = pathinfo($file['name'], PATHINFO_EXTENSION); $result['mime_type'] = mime_content_type($tmp_filename); $file_handler = fopen($tmp_filename, 'r'); @@ -277,96 +279,55 @@ namespace controllers\internals; } /** - * Allow to upload file. + * Generate a highly random uuid based on timestamp and strong cryptographic random * - * @param array $file : The array extracted from $_FILES['file'] - * - * @return array : ['success' => bool, 'content' => file path | error message, 'error_code' => $file['error']] + * @return string */ - public static function upload_file(array $file) + public static function random_uuid() { - $result = [ - 'success' => false, - 'content' => 'Une erreur inconnue est survenue.', - 'error_code' => $file['error'] ?? 99, - ]; + $bytes = random_bytes(16); + return time() . '-' . bin2hex($bytes); + } - if (UPLOAD_ERR_OK !== $file['error']) + + /** + * Create a user data public path + * @param int $id_user : The user id + * + * @return string : The created path + + * @exception Raise exception on error + */ + public static function create_user_public_path (int $id_user) + { + $new_dir = PWD_DATA_PUBLIC . '/' . $id_user; + if (file_exists($new_dir)) { - switch ($file['error']) - { - case UPLOAD_ERR_INI_SIZE: - $result['content'] = 'Impossible de télécharger le fichier car il dépasse les ' . ini_get('upload_max_filesize') / (1000 * 1000) . ' Mégaoctets.'; - - break; - - case UPLOAD_ERR_FORM_SIZE: - $result['content'] = 'Le fichier dépasse la limite de taille.'; - - break; - - case UPLOAD_ERR_PARTIAL: - $result['content'] = 'L\'envoi du fichier a été interrompu.'; - - break; - - case UPLOAD_ERR_NO_FILE: - $result['content'] = 'Aucun fichier n\'a été envoyé.'; - - break; - - case UPLOAD_ERR_NO_TMP_DIR: - $result['content'] = 'Le serveur ne dispose pas de fichier temporaire permettant l\'envoi de fichiers.'; - - break; - - case UPLOAD_ERR_CANT_WRITE: - $result['content'] = 'Impossible d\'envoyer le fichier car il n\'y a plus de place sur le serveur.'; - - break; - - case UPLOAD_ERR_EXTENSION: - $result['content'] = 'Le serveur a interrompu l\'envoi du fichier.'; - - break; - } - - return $result; + return $new_dir; } - $tmp_filename = $file['tmp_name'] ?? false; - if (!$tmp_filename || !is_readable($tmp_filename)) + clearstatcache(); + if (!mkdir($new_dir)) { - return $result; + throw new \Exception('Cannot create dir ' . $new_dir); } - $md5_filename = md5_file($tmp_filename); - if (!$md5_filename) + //We do chmod in two times because else umask fuck mkdir permissions + if (!chmod($new_dir, fileperms(PWD_DATA_PUBLIC) & 0777)) //Fileperms return garbage in addition to perms. Perms are only in weak bytes. We must use an octet notation with 0 { - return $result; + throw new \Exception('Cannot give dir ' . $new_dir . ' rights : ' . decoct(fileperms(PWD_DATA_PUBLIC) & 0777)); //Show error in dec } - $new_file_path = PWD_DATA . '/' . $md5_filename; - - if (file_exists($new_file_path)) + if (posix_getuid() === 0 && !chown($new_dir, fileowner(PWD_DATA_PUBLIC))) //If we are root, try to give the file to a proper user { - $result['success'] = true; - $result['content'] = $new_file_path; - - return $result; + throw new \Exception('Cannot give dir ' . $new_dir . ' to user : ' . fileowner(PWD_DATA_PUBLIC)); } - $success = move_uploaded_file($tmp_filename, $new_file_path); - if (!$success) + if (posix_getuid() === 0 && !chgrp($new_dir, filegroup(PWD_DATA_PUBLIC))) //If we are root, try to give the file to a proper group { - $result['content'] = 'Impossible d\'écrire le fichier sur le serveur.'; - - return $result; + throw new \Exception('Cannot give dir ' . $new_dir . ' to group : ' . filegroup(PWD_DATA_PUBLIC)); } - $result['success'] = true; - $result['content'] = $new_file_path; - - return $result; + return $new_dir; } } diff --git a/controllers/internals/User.php b/controllers/internals/User.php index 99a47cf..427cceb 100644 --- a/controllers/internals/User.php +++ b/controllers/internals/User.php @@ -264,6 +264,7 @@ namespace controllers\internals; * string 'text' => sms content, * string 'destination' => id of phone the sms was sent to * string 'origin' => phone number that sent the sms + * bool 'mms' => is the sms a mms * ] * * @return bool : False if no transfer, true else @@ -290,12 +291,20 @@ namespace controllers\internals; } $mailer = new Mailer(); + + $attachments = []; + + foreach ($received['medias'] ?? [] as $media) + { + $attachments[] = PWD_DATA_PUBLIC . '/' . $media['path']; + } return $mailer->enqueue($user['email'], EMAIL_TRANSFER_SMS, [ 'at' => $received['at'], 'origin' => $received['origin'], 'destination' => $phone['name'], 'text' => $received['text'], - ]); + 'mms' => $received['mms'] ?? false, + ], $attachments); } } diff --git a/controllers/internals/Webhook.php b/controllers/internals/Webhook.php index 8d25108..3cbaff0 100644 --- a/controllers/internals/Webhook.php +++ b/controllers/internals/Webhook.php @@ -94,17 +94,11 @@ class Webhook extends StandardController * * @param int $id_user : User to trigger the webhook for * @param string $type : Type of webhook to trigger - * @param array $sms : The sms [ - * int 'id' => SMS id, - * string 'at' => SMS date, - * string 'text' => sms body, - * string 'origin' => sms origin (number or phone id) - * string 'destination' => sms destination (number or phone id) - * ] + * @param array $body : The body, an array depending on webhook type * * @return bool : False if no trigger, true else */ - public function trigger(int $id_user, string $type, array $sms) + public function trigger(int $id_user, string $type, array $body) { $internal_setting = new Setting($this->bdd); $internal_user = new User($this->bdd); @@ -137,11 +131,7 @@ class Webhook extends StandardController 'webhook_type' => $webhook['type'], 'webhook_random_id' => $webhook_random_id, 'webhook_signature' => $webhook_signature, - 'id' => $sms['id'], - 'at' => $sms['at'], - 'text' => $sms['text'], - 'origin' => $sms['origin'], - 'destination' => $sms['destination'], + 'body' => json_encode($body), ], ]; diff --git a/controllers/publics/Api.php b/controllers/publics/Api.php index 02b09d7..c153e85 100644 --- a/controllers/publics/Api.php +++ b/controllers/publics/Api.php @@ -32,6 +32,7 @@ namespace controllers\publics; 'CANNOT_CREATE' => 8, 'SUSPENDED_USER' => 16, 'CANNOT_DELETE' => 32, + 'CANNOT_UPLOAD_FILE' => 64, ]; const ERROR_MESSAGES = [ @@ -41,6 +42,7 @@ namespace controllers\publics; 'CANNOT_CREATE' => 'Cannot create a new entry.', 'SUSPENDED_USER' => 'This user account is currently suspended.', 'CANNOT_DELETE' => 'Cannot delete this entry.', + 'CANNOT_UPLOAD_FILE' => 'Failed to upload or save an uploaded file : ', ]; private $internal_user; @@ -52,6 +54,8 @@ namespace controllers\publics; private $internal_group; private $internal_conditional_group; private $internal_adapter; + private $internal_media; + private $internal_setting; private $user; /** @@ -73,6 +77,8 @@ namespace controllers\publics; $this->internal_group = new \controllers\internals\Group($bdd); $this->internal_conditional_group = new \controllers\internals\ConditionalGroup($bdd); $this->internal_adapter = new \controllers\internals\Adapter(); + $this->internal_media = new \controllers\internals\Media($bdd); + $this->internal_setting = new \controllers\internals\Setting($bdd); //If no user, quit with error $this->user = false; @@ -93,6 +99,8 @@ namespace controllers\publics; exit(self::ERROR_CODES['INVALID_CREDENTIALS']); } + $this->user['settings'] = $this->internal_setting->gets_for_user($this->user['id']); + if (\models\User::STATUS_ACTIVE !== $this->user['status']) { $return = self::DEFAULT_RETURN; @@ -108,14 +116,14 @@ namespace controllers\publics; /** * List all entries of a certain type for the current user, sorted by id. * - * @param string $entry_type : Type of entries we want to list ['sended', 'received', 'scheduled', 'contact', 'group', 'conditional_group', 'phone'] + * @param string $entry_type : Type of entries we want to list ['sended', 'received', 'scheduled', 'contact', 'group', 'conditional_group', 'phone', 'media'] * @param int $page : Pagination number, Default = 0. Group of 25 results. * * @return : List of entries */ public function get_entries(string $entry_type, int $page = 0) { - $entry_types = ['sended', 'received', 'scheduled', 'contact', 'group', 'conditional_group', 'phone']; + $entry_types = ['sended', 'received', 'scheduled', 'contact', 'group', 'conditional_group', 'phone', 'media']; if (!\in_array($entry_type, $entry_types, true)) { @@ -143,6 +151,21 @@ namespace controllers\publics; $entries[$key]['contacts'] = $this->internal_scheduled->get_contacts($entry['id']); $entries[$key]['groups'] = $this->internal_scheduled->get_groups($entry['id']); $entries[$key]['conditional_groups'] = $this->internal_scheduled->get_conditional_groups($entry['id']); + $entries[$key]['medias'] = $this->internal_media->gets_for_scheduled($entry['id']); + } + } + elseif ('received' === $entry_type) + { + foreach ($entries as $key => $entry) + { + $entries[$key]['medias'] = $this->internal_media->gets_for_received($entry['id']); + } + } + elseif ('sended' === $entry_type) + { + foreach ($entries as $key => $entry) + { + $entries[$key]['medias'] = $this->internal_media->gets_for_sended($entry['id']); } } //Special case for group we must add contact because its a join @@ -179,6 +202,7 @@ namespace controllers\publics; * @param string $_POST['text'] : Text of the message to send * @param string $_POST['id_phone'] : Default null. Id of phone to send the message from. If null use a random phone * @param string $_POST['flash'] : Default false. Is the sms a flash sms. + * @param string $_POST['mms'] : Default false. Is the sms a mms. * @param string $_POST['numbers'] : Array of numbers to send message to * @param string $_POST['contacts'] : Array of ids of contacts to send message to * @param string $_POST['groups'] : Array of ids of groups to send message to @@ -192,16 +216,47 @@ namespace controllers\publics; $text = $_POST['text'] ?? false; $id_phone = empty($_POST['id_phone']) ? null : $_POST['id_phone']; $flash = (bool) ($_POST['flash'] ?? false); + $mms = (bool) ($_POST['mms'] ?? false); $numbers = $_POST['numbers'] ?? []; $contacts = $_POST['contacts'] ?? []; $groups = $_POST['groups'] ?? []; $conditional_groups = $_POST['conditional_groups'] ?? []; + $files = $_FILES['medias'] ?? false; $numbers = \is_array($numbers) ? $numbers : [$numbers]; $contacts = \is_array($contacts) ? $contacts : [$contacts]; $groups = \is_array($groups) ? $groups : [$groups]; $conditional_groups = \is_array($conditional_groups) ? $conditional_groups : [$conditional_groups]; + //Iterate over files to re-create individual $_FILES array + $files_arrays = []; + + if ($files === false) + { + $files_arrays = []; + } + elseif (!is_array($files['name'])) //Only one file uploaded + { + $files_arrays[] = $files; + } + else //multiple files + { + foreach ($files as $property_name => $files_values) + { + foreach ($files_values as $file_key => $property_value) + { + if (!isset($files_arrays[$file_key])) + { + $files_arrays[$file_key] = []; + } + + $files_arrays[$file_key][$property_name] = $property_value; + } + } + } + + $media_ids = []; + if (!$at) { $at = (new \DateTime())->format('Y-m-d H:i:s'); @@ -227,6 +282,16 @@ namespace controllers\publics; return $this->json($return); } + if (($this->user['settings']['mms'] ?? false) && $mms) + { + $return = self::DEFAULT_RETURN; + $return['error'] = self::ERROR_CODES['INVALID_PARAMETER']; + $return['message'] = self::ERROR_MESSAGES['INVALID_PARAMETER'] . 'mms is set to true, but mms are disabled in settings.'; + $this->auto_http_code(false); + + return $this->json($return); + } + foreach ($numbers as $key => $number) { $number = \controllers\internals\Tool::parse_phone($number); @@ -251,7 +316,13 @@ namespace controllers\publics; return $this->json($return); } - if ($id_phone && !$this->internal_phone->get_for_user($this->user['id'], $id_phone)) + $phone = null; + if ($id_phone) + { + $phone = $this->internal_phone->get_for_user($this->user['id'], $id_phone); + } + + if ($id_phone && !$phone) { $return = self::DEFAULT_RETURN; $return['error'] = self::ERROR_CODES['INVALID_PARAMETER']; @@ -261,7 +332,29 @@ namespace controllers\publics; return $this->json($return); } - $scheduled_id = $this->internal_scheduled->create($this->user['id'], $at, $text, $id_phone, $flash, $numbers, $contacts, $groups, $conditional_groups); + if ($mms) + { + foreach ($files_arrays as $file) + { + try + { + $new_media_id = $this->internal_media->upload_and_create_for_user($this->user['id'], $file); + } + catch (\Exception $e) + { + $return = self::DEFAULT_RETURN; + $return['error'] = self::ERROR_CODES['CANNOT_CREATE']; + $return['message'] = self::ERROR_MESSAGES['CANNOT_CREATE'] . ' : Cannot upload and create media file ' . $file['name'] . ' : ' . $e->getMessage(); + $this->auto_http_code(false); + + return $this->json($return); + } + + $media_ids[] = $new_media_id; + } + } + + $scheduled_id = $this->internal_scheduled->create($this->user['id'], $at, $text, $id_phone, $flash, $mms, $numbers, $contacts, $groups, $conditional_groups, $media_ids); if (!$scheduled_id) { $return = self::DEFAULT_RETURN; diff --git a/controllers/publics/Call.php b/controllers/publics/Call.php new file mode 100644 index 0000000..fb05bc2 --- /dev/null +++ b/controllers/publics/Call.php @@ -0,0 +1,93 @@ + + * + * This source file is subject to the GPL-3.0 license that is bundled + * with this source code in the file LICENSE. + */ + +namespace controllers\publics; + + /** + * Page des calls. + */ + class Call extends \descartes\Controller + { + private $internal_call; + + /** + * Cette fonction est appelée avant toute les autres : + * Elle vérifie que l'utilisateur est bien connecté. + * + * @return void; + */ + public function __construct() + { + $bdd = \descartes\Model::_connect(DATABASE_HOST, DATABASE_NAME, DATABASE_USER, DATABASE_PASSWORD); + + $this->internal_call = new \controllers\internals\Call($bdd); + + \controllers\internals\Tool::verifyconnect(); + } + + /** + * Page for showing calls list + */ + public function list() + { + $this->render('call/list'); + } + + /** + * Return calls list as json. + */ + public function list_json() + { + $entities = $this->internal_call->list_for_user($_SESSION['user']['id']); + foreach ($entities as &$entity) + { + switch ($entity['direction']) + { + case \models\Call::DIRECTION_INBOUND : + $entity['origin_formatted'] = \controllers\internals\Tool::phone_link($entity['origin']); + break; + + case \models\Call::DIRECTION_OUTBOUND : + $entity['destination_formatted'] = \controllers\internals\Tool::phone_link($entity['destination']); + break; + } + } + + header('Content-Type: application/json'); + echo json_encode(['data' => $entities]); + } + + /** + * Delete a list of calls + * + * @param array int $_GET['ids'] : Ids of calls to delete + * @param string $csrf : csrf token + * + * @return boolean; + */ + public function delete(string $csrf) + { + if (!$this->verify_csrf($csrf)) + { + \FlashMessage\FlashMessage::push('danger', 'Jeton CSRF invalid !'); + + return $this->redirect(\descartes\Router::url('Call', 'list')); + } + + $ids = $_GET['ids'] ?? []; + foreach ($ids as $id) + { + $this->internal_call->delete_for_user($_SESSION['user']['id'], $id); + } + + return $this->redirect(\descartes\Router::url('Call', 'list')); + } + } diff --git a/controllers/publics/Callback.php b/controllers/publics/Callback.php index c7d905d..d97a2e6 100644 --- a/controllers/publics/Callback.php +++ b/controllers/publics/Callback.php @@ -25,6 +25,9 @@ use Monolog\Logger; private $internal_sended; private $internal_received; private $internal_adapter; + private $internal_media; + private $internal_phone; + private $internal_call; public function __construct() { @@ -33,7 +36,10 @@ use Monolog\Logger; $this->internal_user = new \controllers\internals\User($bdd); $this->internal_sended = new \controllers\internals\Sended($bdd); $this->internal_received = new \controllers\internals\Received($bdd); + $this->internal_media = new \controllers\internals\Media($bdd); $this->internal_adapter = new \controllers\internals\Adapter(); + $this->internal_phone = new \controllers\internals\Phone($bdd); + $this->internal_call = new \controllers\internals\Call($bdd); //Logger $this->logger = new Logger('Callback ' . uniqid()); @@ -177,8 +183,10 @@ use Monolog\Logger; } $sms = $response['sms']; + $mms = (bool) $sms['mms'] ?? false; + $medias = empty($sms['medias']) ? [] : $sms['medias']; - $response = $this->internal_received->receive($this->user['id'], $id_phone, $sms['text'], $sms['origin'], $sms['at']); + $response = $this->internal_received->receive($this->user['id'], $id_phone, $sms['text'], $sms['origin'], $sms['at'], \models\Received::STATUS_UNREAD, $mms, $medias); if ($response['error']) { $this->logger->error('Failed receive message : ' . json_encode($sms) . ' with error : ' . $response['error_message']); @@ -190,4 +198,135 @@ use Monolog\Logger; return true; } + + + /** + * Function call on call reception notification + * We return nothing, and we let the adapter do his things. + * + * @param int $id_phone : Phone id + * + * @return bool : true on success, false on error + */ + public function inbound_call(int $id_phone) + { + $this->logger->info('Callback inbound_call call with phone : ' . $id_phone); + $phone = $this->internal_phone->get_for_user($this->user['id'], $id_phone); + + if (!$phone) + { + $this->logger->error('Callback inbound_call use non existing phone : ' . $id_phone); + + return false; + } + + if (!class_exists($phone['adapter'])) + { + $this->logger->error('Callback inbound_call use non existing adapter : ' . $phone['adapter']); + + return false; + } + + if (!$phone['adapter']::meta_support_inbound_call_callback()) + { + $this->logger->error('Callback inbound_call use adapter ' . $phone['adapter'] . ' which does not support inbound_call callback.'); + + return false; + } + + $response = $phone['adapter']::inbound_call_callback(); + if ($response['error']) + { + $this->logger->error('Callback inbound_call failed : ' . $response['error_message']); + + return false; + } + + $call = $response['call']; + + if (empty($call) || empty($call['uid']) || empty($call['start']) || empty($call['origin'])) + { + $this->logger->error('Callback inbound_call failed : missing required param in call return'); + + return false; + } + + $result = $this->internal_call->create($this->user['id'], $id_phone, $call['uid'], \models\Call::DIRECTION_INBOUND, $call['start'], $call['end'] ?? null, $call['origin']); + + if (!$result) + { + $this->logger->error('Callback inbound_call failed because cannot create call ' . json_encode($call)); + + return false; + } + + $this->logger->info('Callback inbound_call successfully received inbound call : ' . json_encode($call)); + + return true; + } + + + /** + * Function call on end call notification + * We return nothing, and we let the adapter do his things. + * + * @param int $id_phone : Phone id + * + * @return bool : true on success, false on error + */ + public function end_call(int $id_phone) + { + $this->logger->info('Callback end call with phone : ' . $id_phone); + $phone = $this->internal_phone->get_for_user($this->user['id'], $id_phone); + + if (!$phone) + { + $this->logger->error('Callback end call use non existing phone : ' . $id_phone); + + return false; + } + + if (!class_exists($phone['adapter'])) + { + $this->logger->error('Callback end call use non existing adapter : ' . $phone['adapter']); + + return false; + } + + if (!$phone['adapter']::meta_support_end_call_callback()) + { + $this->logger->error('Callback end call use adapter ' . $phone['adapter'] . ' which does not support end call callback.'); + + return false; + } + + $response = $phone['adapter']::end_call_callback(); + if ($response['error']) + { + $this->logger->error('Callback end call failed : ' . $response['error_message']); + + return false; + } + + $call = $response['call']; + if (empty($call) || empty($call['uid']) || empty($call['end'])) + { + $this->logger->error('Callback end call failed : missing required param in call return'); + + return false; + } + + $result = $this->internal_call->end($this->user['id'], $id_phone, $call['uid'], $call['end']); + + if (!$result) + { + $this->logger->error('Callback end call failed because cannot update call ' . json_encode($call)); + + return false; + } + + $this->logger->info('Callback end call successfully update call : ' . json_encode($call)); + + return true; + } } diff --git a/controllers/publics/ConditionalGroup.php b/controllers/publics/ConditionalGroup.php index 28ad23e..9ba60f9 100644 --- a/controllers/publics/ConditionalGroup.php +++ b/controllers/publics/ConditionalGroup.php @@ -234,7 +234,17 @@ namespace controllers\publics; $contacts_name[] = $contact['name']; } - $return['result'] = 'Contacts du groupe : ' . implode(', ', $contacts_name); + $visible_names = array_slice($contacts_name, 0, 5); + $how_many_more = count($contacts_name) - count($visible_names); + + $result_text = 'Contacts du groupe : ' . implode(', ', $visible_names); + + if ($how_many_more > 0) + { + $result_text .= ", et $how_many_more autres."; + } + + $return['result'] = $result_text; $return['success'] = true; echo json_encode($return); diff --git a/controllers/publics/Contact.php b/controllers/publics/Contact.php index 2e2b8b6..9d6e98f 100644 --- a/controllers/publics/Contact.php +++ b/controllers/publics/Contact.php @@ -18,6 +18,7 @@ namespace controllers\publics; { private $internal_contact; private $internal_event; + private $internal_conditional_group; /** * Cette fonction est appelée avant toute les autres : @@ -31,6 +32,7 @@ namespace controllers\publics; $this->internal_contact = new \controllers\internals\Contact($bdd); $this->internal_event = new \controllers\internals\Event($bdd); + $this->internal_conditional_group = new \controllers\internals\ConditionalGroup($bdd); \controllers\internals\Tool::verifyconnect(); } @@ -83,6 +85,40 @@ namespace controllers\publics; return $this->redirect(\descartes\Router::url('Contact', 'list')); } + + /** + * This function will delete a list of contacts depending on a condition + * + * @param string $_POST['condition'] : Condition to use to delete contacts + * @param mixed $csrf + * + * @return boolean; + */ + public function conditional_delete($csrf) + { + if (!$this->verify_csrf($csrf)) + { + \FlashMessage\FlashMessage::push('danger', 'Jeton CSRF invalid !'); + + return $this->redirect(\descartes\Router::url('Contact', 'list')); + } + + $condition = $_POST['condition'] ?? false; + if (!$condition) + { + \FlashMessage\FlashMessage::push('danger', 'Vous devez fournir une condition !'); + + return $this->redirect(\descartes\Router::url('Contact', 'list')); + } + + $contacts_to_delete = $this->internal_conditional_group->get_contacts_for_condition_and_user($_SESSION['user']['id'], $condition); + foreach ($contacts_to_delete as $contact) + { + $this->internal_contact->delete_for_user($_SESSION['user']['id'], $contact['id']); + } + + return $this->redirect(\descartes\Router::url('Contact', 'list')); + } /** * Cette fonction retourne la page d'ajout d'un contact. diff --git a/controllers/publics/Discussion.php b/controllers/publics/Discussion.php index 7c7b79a..1d385d8 100644 --- a/controllers/publics/Discussion.php +++ b/controllers/publics/Discussion.php @@ -21,6 +21,7 @@ namespace controllers\publics; private $internal_received; private $internal_contact; private $internal_phone; + private $internal_media; /** * Cette fonction est appelée avant toute les autres : @@ -37,6 +38,7 @@ namespace controllers\publics; $this->internal_received = new \controllers\internals\Received($bdd); $this->internal_contact = new \controllers\internals\Contact($bdd); $this->internal_phone = new \controllers\internals\Phone($bdd); + $this->internal_media = new \controllers\internals\Media($bdd); \controllers\internals\Tool::verifyconnect(); } @@ -112,12 +114,27 @@ namespace controllers\publics; foreach ($sendeds as $sended) { - $messages[] = [ + $medias = []; + if ($sended['mms']) + { + $medias = $this->internal_media->gets_for_sended($sended['id']); + foreach ($medias as &$media) + { + $media = HTTP_PWD_DATA_PUBLIC . '/' . $media['path']; + } + } + + $message = [ + 'uid' => 'sended-' . $sended['id'], 'date' => htmlspecialchars($sended['at']), 'text' => htmlspecialchars($sended['text']), 'type' => 'sended', + 'medias' => $medias, 'status' => $sended['status'], ]; + + + $messages[] = $message; } foreach ($receiveds as $received) @@ -126,21 +143,44 @@ namespace controllers\publics; { $this->internal_received->mark_as_read_for_user($id_user, $received['id']); } + + $medias = []; + if ($received['mms']) + { + $medias = $this->internal_media->gets_for_received($received['id']); + foreach ($medias as &$media) + { + $media = HTTP_PWD_DATA_PUBLIC . '/' . $media['path']; + } + } $messages[] = [ + 'uid' => 'received-' . $received['id'], 'date' => htmlspecialchars($received['at']), 'text' => htmlspecialchars($received['text']), 'type' => 'received', - 'md5' => md5($received['at'] . $received['text']), + 'medias' => $medias, ]; } foreach ($scheduleds as $scheduled) { + $medias = []; + if ($scheduled['mms']) + { + $medias = $this->internal_media->gets_for_scheduled($scheduled['id']); + foreach ($medias as &$media) + { + $media = HTTP_PWD_DATA_PUBLIC . '/' . $media['path']; + } + } + $messages[] = [ + 'uid' => 'scheduled-' . $scheduled['id'], 'date' => htmlspecialchars($scheduled['at']), 'text' => htmlspecialchars($scheduled['text']), 'type' => 'inprogress', + 'medias' => $medias, ]; } @@ -150,10 +190,15 @@ namespace controllers\publics; return strtotime($a['date']) - strtotime($b['date']); }); - //On récupère uniquement les 25 derniers messages sur l'ensemble + //Récupère uniquement les 25 derniers messages sur l'ensemble pour limiter la charge $messages = \array_slice($messages, -25); - echo json_encode(['transaction_id' => $transaction_id, 'messages' => $messages]); + $response = [ + 'transaction_id' => $transaction_id, + 'messages' => $messages, + ]; + + echo json_encode($response); return true; } @@ -165,6 +210,7 @@ namespace controllers\publics; * @param string $_POST['text'] : Le contenu du Sms * @param string $_POST['destination'] : Number to send sms to * @param string $_POST['id_phone'] : If of phone to send sms with + * @param array $_FILES['medias'] : Medias to upload and link to sms * * @return string : json string Le statut de l'envoi */ @@ -190,6 +236,43 @@ namespace controllers\publics; $text = $_POST['text'] ?? ''; $destination = $_POST['destination'] ?? false; $id_phone = $_POST['id_phone'] ?? false; + $files = $_FILES['medias'] ?? false; + + //Iterate over files to re-create individual $_FILES array + $files_arrays = []; + if ($files && is_array($files['name'])) + { + foreach ($files as $property_name => $files_values) + { + foreach ($files_values as $file_key => $property_value) + { + if (!isset($files_arrays[$file_key])) + { + $files_arrays[$file_key] = []; + } + + $files_arrays[$file_key][$property_name] = $property_value; + } + } + } + + //Remove empty files input + foreach ($files_arrays as $key => $file) + { + if ($file['error'] === UPLOAD_ERR_NO_FILE) + { + unset($files_arrays[$key]); + } + } + + if (!$text) + { + $return['success'] = false; + $return['message'] = 'Vous devez renseigner le texte de votre sms.'; + echo json_encode($return); + + return false; + } if (!$destination) { @@ -205,10 +288,36 @@ namespace controllers\publics; $id_phone = null; } + + //If mms is enable and we have medias uploaded + $media_ids = []; + if ($_SESSION['user']['settings']['mms'] && $files_arrays) + { + foreach ($files_arrays as $file) + { + try + { + $new_media_id = $this->internal_media->create_from_uploaded_file_for_user($_SESSION['user']['id'], $file); + } + catch (\Exception $e) + { + $return['success'] = false; + $return['message'] = $e->getMessage(); + echo json_encode($return); + + return false; + } + + $media_ids[] = $new_media_id; + } + } + + $mms = (bool) count($media_ids); + //Destinations must be an array of number $destinations = [$destination]; - if (!$this->internal_scheduled->create($id_user, $at, $text, $id_phone, false, $destinations)) + if (!$this->internal_scheduled->create($id_user, $at, $text, $id_phone, false, $mms, $destinations, [], [], [], $media_ids)) { $return['success'] = false; $return['message'] = 'Impossible de créer le Sms'; diff --git a/controllers/publics/Phone.php b/controllers/publics/Phone.php index 329572a..81d6249 100644 --- a/controllers/publics/Phone.php +++ b/controllers/publics/Phone.php @@ -33,43 +33,7 @@ class Phone extends \descartes\Controller */ public function list() { - $id_user = $_SESSION['user']['id']; - $api_key = $_SESSION['user']['api_key']; - $phones = $this->internal_phone->list_for_user($id_user); - - $adapters = []; - $adapters = $this->internal_adapter->list_adapters(); - foreach ($adapters as $key => $adapter) - { - unset($adapters[$key]); - $adapters[$adapter['meta_classname']] = $adapter; - } - - foreach ($phones as &$phone) - { - $adapter = $adapters[$phone['adapter']] ?? false; - - if (!$adapter) - { - $phone['adapter'] = 'Inconnu'; - - continue; - } - - $phone['adapter'] = $adapter['meta_name']; - - if ($adapter['meta_support_reception']) - { - $phone['callback_reception'] = \descartes\Router::url('Callback', 'reception', ['adapter_uid' => $adapter['meta_uid'], 'id_phone' => $phone['id']], ['api_key' => $api_key]); - } - - if ($adapter['meta_support_status_change']) - { - $phone['callback_status'] = \descartes\Router::url('Callback', 'update_sended_status', ['adapter_uid' => $adapter['meta_uid']], ['api_key' => $api_key]); - } - } - - $this->render('phone/list', ['phones' => $phones]); + $this->render('phone/list'); } /** @@ -111,6 +75,16 @@ class Phone extends \descartes\Controller { $phone['callback_status'] = \descartes\Router::url('Callback', 'update_sended_status', ['adapter_uid' => $adapter['meta_uid']], ['api_key' => $api_key]); } + + if ($adapter['meta_support_inbound_call_callback']) + { + $phone['callback_inbound_call'] = \descartes\Router::url('Callback', 'inbound_call', ['id_phone' => $phone['id']], ['api_key' => $api_key]); + } + + if ($adapter['meta_support_end_call_callback']) + { + $phone['callback_end_call'] = \descartes\Router::url('Callback', 'end_call', ['id_phone' => $phone['id']], ['api_key' => $api_key]); + } } header('Content-Type: application/json'); diff --git a/controllers/publics/Received.php b/controllers/publics/Received.php index e5ba67e..d6ab5a4 100644 --- a/controllers/publics/Received.php +++ b/controllers/publics/Received.php @@ -19,6 +19,7 @@ namespace controllers\publics; private $internal_received; private $internal_contact; private $internal_phone; + private $internal_media; /** * Cette fonction est appelée avant toute les autres : @@ -32,6 +33,7 @@ namespace controllers\publics; $this->internal_received = new \controllers\internals\Received($bdd); $this->internal_contact = new \controllers\internals\Contact($bdd); $this->internal_phone = new \controllers\internals\Phone($bdd); + $this->internal_media = new \controllers\internals\Media($bdd); \controllers\internals\Tool::verifyconnect(); } @@ -53,6 +55,10 @@ namespace controllers\publics; foreach ($entities as &$entity) { $entity['origin_formatted'] = \controllers\internals\Tool::phone_link($entity['origin']); + if ($entity['mms']) + { + $entity['medias'] = $this->internal_media->gets_for_received($entity['id']); + } } header('Content-Type: application/json'); @@ -76,6 +82,10 @@ namespace controllers\publics; foreach ($entities as &$entity) { $entity['origin_formatted'] = \controllers\internals\Tool::phone_link($entity['origin']); + if ($entity['mms']) + { + $entity['medias'] = $this->internal_media->gets_for_received($entity['id']); + } } header('Content-Type: application/json'); diff --git a/controllers/publics/Scheduled.php b/controllers/publics/Scheduled.php index 8f270df..b96c58d 100644 --- a/controllers/publics/Scheduled.php +++ b/controllers/publics/Scheduled.php @@ -56,6 +56,13 @@ namespace controllers\publics; public function list_json() { $entities = $this->internal_scheduled->list_for_user($_SESSION['user']['id']); + foreach ($entities as &$entity) + { + if ($entity['mms']) + { + $entity['medias'] = $this->internal_media->gets_for_scheduled($entity['id']); + } + } header('Content-Type: application/json'); echo json_encode(['data' => $entities]); @@ -205,8 +212,8 @@ namespace controllers\publics; $scheduleds[$key]['groups'][] = (int) $group['id']; } - $media = $this->internal_media->get_for_scheduled_and_user($id_user, $scheduled['id']); - $scheduleds[$key]['media'] = $media; + $medias = $this->internal_media->gets_for_scheduled($scheduled['id']); + $scheduleds[$key]['medias'] = $medias; $conditional_groups = $this->internal_scheduled->get_conditional_groups($scheduled['id']); foreach ($conditional_groups as $conditional_group) @@ -235,7 +242,7 @@ namespace controllers\publics; * @param ?array $_POST['contacts'] : Numbers to send the message to * @param ?array $_POST['groups'] : Numbers to send the message to * @param ?array $_POST['conditional_groups'] : Numbers to send the message to - * @param ?array $_FILES['media'] : The media to link to a scheduled + * @param ?array $_FILES['medias'] : The media to link to a scheduled */ public function create($csrf) { @@ -255,7 +262,34 @@ namespace controllers\publics; $contacts = $_POST['contacts'] ?? []; $groups = $_POST['groups'] ?? []; $conditional_groups = $_POST['conditional_groups'] ?? []; - $media = $_FILES['media'] ?? false; + $files = $_FILES['medias'] ?? false; + + //Iterate over files to re-create individual $_FILES array + $files_arrays = []; + if ($files && is_array($files['name'])) + { + foreach ($files as $property_name => $files_values) + { + foreach ($files_values as $file_key => $property_value) + { + if (!isset($files_arrays[$file_key])) + { + $files_arrays[$file_key] = []; + } + + $files_arrays[$file_key][$property_name] = $property_value; + } + } + } + + //Remove empty files input + foreach ($files_arrays as $key => $file) + { + if ($file['error'] === UPLOAD_ERR_NO_FILE) + { + unset($files_arrays[$key]); + } + } if (empty($text)) { @@ -291,8 +325,30 @@ namespace controllers\publics; return $this->redirect(\descartes\Router::url('Scheduled', 'add')); } + + //If mms is enable and we have medias uploaded + $media_ids = []; + if ($_SESSION['user']['settings']['mms'] && $files_arrays) + { + foreach ($files_arrays as $file) + { + try + { + $new_media_id = $this->internal_media->create_from_uploaded_file_for_user($_SESSION['user']['id'], $file); + } + catch (\Exception $e) + { + \FlashMessage\FlashMessage::push('danger', 'Impossible d\'upload et d\'enregistrer le fichier ' . $file['name'] . ':' . $e->getMessage()); + return $this->redirect(\descartes\Router::url('Scheduled', 'add')); + } - $scheduled_id = $this->internal_scheduled->create($id_user, $at, $text, $id_phone, $flash, $numbers, $contacts, $groups, $conditional_groups); + $media_ids[] = $new_media_id; + } + } + + $mms = (bool) count($media_ids); + + $scheduled_id = $this->internal_scheduled->create($id_user, $at, $text, $id_phone, $flash, $mms, $numbers, $contacts, $groups, $conditional_groups, $media_ids); if (!$scheduled_id) { \FlashMessage\FlashMessage::push('danger', 'Impossible de créer le Sms.'); @@ -300,22 +356,6 @@ namespace controllers\publics; return $this->redirect(\descartes\Router::url('Scheduled', 'add')); } - //If mms is enabled, try to process a media to link to the scheduled - if (!($_SESSION['user']['settings']['mms'] ?? false) || !$media) - { - \FlashMessage\FlashMessage::push('success', 'Le Sms a bien été créé pour le ' . $at . '.'); - - return $this->redirect(\descartes\Router::url('Scheduled', 'list')); - } - - $success = $this->internal_media->create($id_user, $scheduled_id, $media); - if (!$success) - { - \FlashMessage\FlashMessage::push('success', 'Le SMS a bien été créé mais le média n\'as pas pu être enregistré.'); - - return $this->redirect(\descartes\Router::url('Scheduled', 'list')); - } - \FlashMessage\FlashMessage::push('success', 'Le Sms a bien été créé pour le ' . $at . '.'); return $this->redirect(\descartes\Router::url('Scheduled', 'list')); @@ -352,13 +392,43 @@ namespace controllers\publics; $contacts = $scheduled['contacts'] ?? []; $groups = $scheduled['groups'] ?? []; $conditional_groups = $scheduled['conditional_groups'] ?? []; + $files = $_FILES['scheduleds_' . $id_scheduled . '_medias'] ?? false; + $media_ids = $scheduled['media_ids'] ?? []; + //Check scheduled exists and belong to user $scheduled = $this->internal_scheduled->get($id_scheduled); if (!$scheduled || $scheduled['id_user'] !== $id_user) { continue; } + //Iterate over files to re-create individual $_FILES array + $files_arrays = []; + if ($files && is_array($files['name'])) + { + foreach ($files as $property_name => $files_values) + { + foreach ($files_values as $file_key => $property_value) + { + if (!isset($files_arrays[$file_key])) + { + $files_arrays[$file_key] = []; + } + + $files_arrays[$file_key][$property_name] = $property_value; + } + } + } + + //Remove empty files input + foreach ($files_arrays as $key => $file) + { + if ($file['error'] === UPLOAD_ERR_NO_FILE) + { + unset($files_arrays[$key]); + } + } + if (empty($text)) { continue; @@ -386,32 +456,39 @@ namespace controllers\publics; { continue; } - - $success = $this->internal_scheduled->update_for_user($id_user, $id_scheduled, $at, $text, $id_phone, $flash, $numbers, $contacts, $groups, $conditional_groups); - - //Check for media - /* - $current_media = $scheduled['current_media'] ?? false; - if (!$current_media) + + //If mms is enable and we have medias uploaded + if ($_SESSION['user']['settings']['mms'] && $files_arrays) { - $this->internal_media->delete_for_scheduled_and_user($id_user, $id_scheduled); + foreach ($files_arrays as $file) + { + try + { + $new_media_id = $this->internal_media->create_from_uploaded_file_for_user($_SESSION['user']['id'], $file); + } + catch (\Exception $e) + { + continue 2; + } + + $media_ids[] = $new_media_id; + } } - $media = $_FILES['media_' . $id_scheduled] ?? false; - if (!$media) + //Ensure media_ids point to medias belongings to the current user + foreach ($media_ids as $key => $media_id) { - $nb_update += (int) $success; - continue; + $media = $this->internal_media->get($media_id); + if (!$media || $media['id_user'] !== $_SESSION['user']['id']) + { + unset($media_ids[$key]); + } } - $success = $this->internal_media->create($id_user, $id_scheduled, $media); - if (!$success) - { - continue; - } - */ + $mms = (bool) count($media_ids); - ++$nb_update; + $this->internal_scheduled->update_for_user($id_user, $id_scheduled, $at, $text, $id_phone, $flash, $mms, $numbers, $contacts, $groups, $conditional_groups, $media_ids); + $nb_update++; } if ($nb_update !== \count($scheduleds)) diff --git a/controllers/publics/Sended.php b/controllers/publics/Sended.php index fa0a67c..8227e38 100644 --- a/controllers/publics/Sended.php +++ b/controllers/publics/Sended.php @@ -19,6 +19,7 @@ namespace controllers\publics; private $internal_sended; private $internal_phone; private $internal_contact; + private $internal_media; /** * Cette fonction est appelée avant toute les autres : @@ -32,6 +33,7 @@ namespace controllers\publics; $this->internal_sended = new \controllers\internals\Sended($bdd); $this->internal_phone = new \controllers\internals\Phone($bdd); $this->internal_contact = new \controllers\internals\Contact($bdd); + $this->internal_media = new \controllers\internals\Media($bdd); \controllers\internals\Tool::verifyconnect(); } @@ -55,6 +57,10 @@ namespace controllers\publics; foreach ($entities as &$entity) { $entity['destination_formatted'] = \controllers\internals\Tool::phone_link($entity['destination']); + if ($entity['mms']) + { + $entity['medias'] = $this->internal_media->gets_for_sended($entity['id']); + } } header('Content-Type: application/json'); diff --git a/daemons/Mailer.php b/daemons/Mailer.php index c9f1ac9..a25b253 100644 --- a/daemons/Mailer.php +++ b/daemons/Mailer.php @@ -74,7 +74,7 @@ class Mailer extends AbstractDaemon $this->logger->info('Try sending email : ' . json_encode($message)); $mailer = new \controllers\internals\Mailer(); - $success = $mailer->send($message['destinations'], $message['subject'], $message['body'], $message['alt_body']); + $success = $mailer->send($message['destinations'], $message['subject'], $message['body'], $message['alt_body'], $message['attachments']); if (!$success) { $this->logger->error('Failed sending email'); diff --git a/daemons/Phone.php b/daemons/Phone.php index 83a4b01..48b99e9 100644 --- a/daemons/Phone.php +++ b/daemons/Phone.php @@ -139,7 +139,7 @@ class Phone extends AbstractDaemon //Do message sending $this->logger->info('Try send message : ' . json_encode($message)); - $response = $internal_sended->send($this->adapter, $this->phone['id_user'], $this->phone['id'], $message['text'], $message['destination'], $message['flash']); + $response = $internal_sended->send($this->adapter, $this->phone['id_user'], $this->phone['id'], $message['text'], $message['destination'], $message['flash'], $message['mms'], $message['medias']); if ($response['error']) { $this->logger->error('Failed send message : ' . json_encode($message) . ' with error : ' . $response['error_message']); @@ -181,7 +181,7 @@ class Phone extends AbstractDaemon foreach ($response['smss'] as $sms) { $this->logger->info('Receive message : ' . json_encode($sms)); - $response = $internal_received->receive($this->phone['id_user'], $this->phone['id'], $sms['text'], $sms['origin']); + $response = $internal_received->receive($this->phone['id_user'], $this->phone['id'], $sms['text'], $sms['origin'], $sms['at'], \models\Received::STATUS_UNREAD, $sms['mms'] ?? false, $sms['medias'] ?? []); if ($response['error']) { diff --git a/daemons/Sender.php b/daemons/Sender.php index 202349f..a5504bb 100644 --- a/daemons/Sender.php +++ b/daemons/Sender.php @@ -76,6 +76,8 @@ class Sender extends AbstractDaemon 'id_phone' => $sms['id_phone'], 'destination' => $sms['destination'], 'flash' => $sms['flash'], + 'mms' => $sms['mms'], + 'medias' => $sms['medias'] ?? [], ]; msg_send($this->queues[$queue_id], QUEUE_TYPE_SEND_MSG, $msg); diff --git a/data/public/.tokeep b/data/public/.tokeep new file mode 100644 index 0000000..e69de29 diff --git a/db/migrations/20210317214910_add_media_links_to_sms.php b/db/migrations/20210317214910_add_media_links_to_sms.php new file mode 100644 index 0000000..93e63a2 --- /dev/null +++ b/db/migrations/20210317214910_add_media_links_to_sms.php @@ -0,0 +1,87 @@ +table('media'); + + if ($table->hasColumn('id_scheduled')) + { + if ($table->hasForeignKey('id_scheduled')) + { + $table->dropForeignKey('id_scheduled'); + } + + $table->removeColumn('id_scheduled'); + $table->update(); + } + + if ($table->hasColumn('id_user')) + { + if ($table->hasForeignKey('id_user')) + { + $table->dropForeignKey('id_user'); + } + + $table->removeColumn('id_user'); + $table->update(); + } + + $table->addColumn('id_user', 'integer') + ->addForeignKey('id_user', 'user', 'id', ['delete' => 'CASCADE', 'update' => 'CASCADE']) + ->update(); + + //Add table to join scheduled and media + $table = $this->table('media_scheduled'); + $table->addColumn('id_media', 'integer') + ->addColumn('id_scheduled', 'integer') + ->addForeignKey('id_media', 'media', 'id', ['delete' => 'CASCADE', 'update' => 'CASCADE']) + ->addForeignKey('id_scheduled', 'scheduled', 'id', ['delete' => 'CASCADE', 'update' => 'CASCADE']) + ->create(); + + //Add table to join sended and media + $table = $this->table('media_sended'); + $table->addColumn('id_media', 'integer') + ->addColumn('id_sended', 'integer') + ->addForeignKey('id_media', 'media', 'id', ['delete' => 'CASCADE', 'update' => 'CASCADE']) + ->addForeignKey('id_sended', 'sended', 'id', ['delete' => 'CASCADE', 'update' => 'CASCADE']) + ->create(); + + //Add table to join received and media + $table = $this->table('media_received'); + $table->addColumn('id_media', 'integer') + ->addColumn('id_received', 'integer') + ->addForeignKey('id_media', 'media', 'id', ['delete' => 'CASCADE', 'update' => 'CASCADE']) + ->addForeignKey('id_received', 'received', 'id', ['delete' => 'CASCADE', 'update' => 'CASCADE']) + ->create(); + } +} diff --git a/db/migrations/20210318142759_add_is_mms.php b/db/migrations/20210318142759_add_is_mms.php new file mode 100644 index 0000000..1e7ec14 --- /dev/null +++ b/db/migrations/20210318142759_add_is_mms.php @@ -0,0 +1,46 @@ +table('scheduled'); + $table->addColumn('mms', 'boolean', ['default' => 0, 'null' => false]) + ->update(); + + $table = $this->table('sended'); + $table->addColumn('mms', 'boolean', ['default' => 0, 'null' => false]) + ->update(); + + $table = $this->table('received'); + $table->addColumn('mms', 'boolean', ['default' => 0, 'null' => false]) + ->update(); + } +} diff --git a/db/migrations/20210322193953_update_webhook_types.php b/db/migrations/20210322193953_update_webhook_types.php new file mode 100644 index 0000000..80f2392 --- /dev/null +++ b/db/migrations/20210322193953_update_webhook_types.php @@ -0,0 +1,16 @@ +execute('ALTER TABLE `webhook` MODIFY `type` ENUM(\'send_sms\', \'receive_sms\', \'inbound_call\')'); + } + + public function down() + { + $this->execute('ALTER TABLE `webhook` MODIFY `type` ENUM(\'send_sms\', \'receive_sms\')'); + } +} diff --git a/db/migrations/20210322223812_create_call.php b/db/migrations/20210322223812_create_call.php new file mode 100644 index 0000000..6060c09 --- /dev/null +++ b/db/migrations/20210322223812_create_call.php @@ -0,0 +1,47 @@ +table('call'); + $table->addColumn('id_user', 'integer') + ->addColumn('id_phone', 'integer', ['null' => true]) + ->addColumn('uid', 'string', ['limit' => 500]) + ->addColumn('start', 'datetime') + ->addColumn('end', 'datetime', ['null' => true]) + ->addColumn('direction', 'enum', ['values' => ['inbound', 'outbound']]) + ->addColumn('origin', 'string', ['limit' => 20, 'null' => true]) + ->addColumn('destination', 'string', ['limit' => 20, 'null' => true]) + ->addForeignKey('id_user', 'user', 'id', ['delete' => 'CASCADE', 'update' => 'CASCADE']) + ->addForeignKey('id_phone', 'phone', 'id', ['delete' => 'SET_NULL', 'update' => 'CASCADE']) + ->create(); + } +} diff --git a/db/migrations/20210423172422_add_created_at_and_updated_at.php b/db/migrations/20210423172422_add_created_at_and_updated_at.php new file mode 100644 index 0000000..31a19a5 --- /dev/null +++ b/db/migrations/20210423172422_add_created_at_and_updated_at.php @@ -0,0 +1,53 @@ +getAdapter()->getOption('name'); + $query = 'SELECT table_name FROM information_schema.tables WHERE table_schema = \'' . $database_name . '\''; + $tables = $this->query($query)->fetchAll(); + foreach ($tables as $table) + { + //Do not modify phinxlog + if ($table['table_name'] == 'phinxlog') + { + continue; + } + + //Foreach table add timestamps, created_at and updated_at whith default values + $table = $this->table($table['table_name']); + $table->addColumn('created_at', 'timestamp', ['null' => false, 'default' => 'CURRENT_TIMESTAMP']); + $table->addColumn('updated_at', 'timestamp', ['null' => true, 'update' => 'CURRENT_TIMESTAMP']); + $table->update(); + } + + } +} diff --git a/env.php.dist b/env.php.dist index 3c18bd8..5aa3260 100644 --- a/env.php.dist +++ b/env.php.dist @@ -16,6 +16,9 @@ 'HTTP_PWD_SOUND' => HTTP_PWD_ASSETS . '/sounds', 'PWD_ADAPTERS' => PWD . '/adapters', 'PWD_DATA' => PWD . '/data', + 'HTTP_PWD_DATA' => HTTP_PWD . '/data', + 'PWD_DATA_PUBLIC' => PWD . '/data/public', + 'HTTP_PWD_DATA_PUBLIC' => HTTP_PWD . '/data/public', 'PWD_LOGS' => '/var/log/raspisms', 'PWD_PID' => '/var/run/raspisms', 'APP_SECRET' => '%APP_SECRET%', @@ -66,7 +69,7 @@ 'preferred_phone_country' => 'fr,be,ca', 'default_phone_country' => 'fr', 'authorized_phone_country' => 'fr,be,ca', - 'mms' => 0, + 'mms' => 1, ], ]; diff --git a/env.prod.php.dist b/env.prod.php.dist index e41509d..8ff5fd5 100644 --- a/env.prod.php.dist +++ b/env.prod.php.dist @@ -1,5 +1,8 @@ '%APP_STATIC_HTTP_URL%', + //Database access 'DATABASE_HOST' => '%APP_DATABASE_HOST%', 'DATABASE_NAME' => '%APP_DATABASE_NAME%', diff --git a/models/Call.php b/models/Call.php new file mode 100644 index 0000000..e8e08ce --- /dev/null +++ b/models/Call.php @@ -0,0 +1,91 @@ + + * + * This source file is subject to the GPL-3.0 license that is bundled + * with this source code in the file LICENSE. + */ + +namespace models; + + /** + * Manage bdd operations for calls + */ + class Call extends StandardModel + { + const DIRECTION_INBOUND = 'inbound'; + const DIRECTION_OUTBOUND = 'outbound'; + + /** + * Return a list of call for a user. + * Add a column contact_name and phone_name when available. + * + * @param int $id_user : user id + * @param ?int $limit : Number of entry to return or null + * @param ?int $offset : Number of entry to ignore or null + * + * @return array + */ + public function list_for_user(int $id_user, $limit, $offset) + { + $query = ' + SELECT `call`.*, contact.name as contact_name, phone.name as phone_name + FROM `call` + LEFT JOIN contact + ON contact.number = `call`.destination + OR contact.number = `call`.origin + LEFT JOIN phone + ON phone.id = `call`.id_phone + WHERE `call`.id_user = :id_user + '; + + if (null !== $limit) + { + $limit = (int) $limit; + + $query .= ' LIMIT ' . $limit; + if (null !== $offset) + { + $offset = (int) $offset; + $query .= ' OFFSET ' . $offset; + } + } + + $params = [ + 'id_user' => $id_user, + ]; + + return $this->_run_query($query, $params); + } + + /** + * Get a call for a user by his phone and uid + * + * @param int $id_user : user id + * @param int $id_phone : phone id + * @param int $uid : call uid + * + * @return array : the call or an empty array + */ + public function get_by_uid_and_phone_for_user($id_user, $id_phone, $uid) + { + $where = [ + 'id_user' => $id_user, + 'id_phone' => $id_phone, + 'uid' => $uid, + ]; + + return $this->_select_one($this->get_table_name(), $where); + } + + /** + * Return table name. + */ + protected function get_table_name(): string + { + return 'call'; + } + } diff --git a/models/Media.php b/models/Media.php index 7b7cd83..830acd7 100644 --- a/models/Media.php +++ b/models/Media.php @@ -17,226 +17,255 @@ namespace models; class Media extends StandardModel { /** - * Return an entry by his id for a user. + * Return all medias for a scheduled. * - * @param int $id_user : user id - * @param int $id : entry id - * - * @return array - */ - public function get_for_user(int $id_user, int $id) - { - $query = ' - SELECT * FROM `' . $this->get_table_name() . '` - WHERE id_scheduled IN (SELECT id FROM scheduled WHERE id_user = :id_user) - AND id = :id - '; - - $params = [ - 'id' => $id, - 'id_user' => $id_user, - ]; - - $receiveds = $this->_run_query($query, $params); - - return $receiveds[0] ?? []; - } - - /** - * Return all entries for a user. - * - * @param int $id_user : user id - * - * @return array - */ - public function gets_for_user(int $id_user) - { - $query = ' - SELECT * FROM `' . $this->get_table_name() . '` - WHERE id_scheduled IN (SELECT id FROM scheduled WHERE id_user = :id_user) - '; - - $params = [ - 'id_user' => $id_user, - ]; - - $receiveds = $this->_run_query($query, $params); - } - - /** - * Return a media for a user and a scheduled. - * - * @param int $id_user : user id * @param int $id_scheduled : scheduled id * * @return array */ - public function get_for_scheduled_and_user(int $id_user, int $id_scheduled) + public function gets_for_scheduled(int $id_scheduled) { $query = ' - SELECT * FROM `' . $this->get_table_name() . '` - WHERE id_scheduled IN (SELECT id FROM scheduled WHERE id_user = :id_user) - AND id_scheduled = :id_scheduled + SELECT m.id as id, m.id_user as id_user, m.path as path + FROM `' . $this->get_table_name() . '` as m + INNER JOIN media_scheduled as ms + ON m.id = ms.id_media + WHERE ms.id_scheduled = :id_scheduled '; $params = [ - 'id_user' => $id_user, 'id_scheduled' => $id_scheduled, ]; - $receiveds = $this->_run_query($query, $params); - if (!$receiveds) - { - return false; - } - - return $receiveds[0]; - } - - /** - * Return a list of media for a user. - * - * @param int $id_user : User id - * @param int $limit : Max results to return - * @param int $offset : Number of results to ignore - */ - public function list_for_user($id_user, $limit, $offset) - { - $limit = (int) $limit; - $offset = (int) $offset; - - $query = ' - SELECT * FROM media - WHERE id_scheduled IN (SELECT id FROM scheduled WHERE id_user = :id_user) - LIMIT ' . $limit . ' OFFSET ' . $offset; - - $params = [ - 'id_user' => $id_user, - ]; - return $this->_run_query($query, $params); } - + /** - * Return a list of medias in a group of ids and for a user. + * Return all medias for a sended. * - * @param int $id_user : user id - * @param array $ids : ids of medias to find + * @param int $id_sended : sended id * * @return array */ - public function gets_in_for_user(int $id_user, $ids) + public function gets_for_sended(int $id_sended) { $query = ' - SELECT * FROM media - WHERE id_scheduled IN (SELECT id FROM scheduled WHERE id_user = :id_user) - AND id '; - - //On génère la clause IN et les paramètres adaptés depuis le tableau des id - $generated_in = $this->_generate_in_from_array($ids); - $query .= $generated_in['QUERY']; - $params = $generated_in['PARAMS']; - $params['id_user'] = $id_user; - - return $this->_run_query($query, $params); - } - - /** - * Delete a entry by his id for a user. - * - * @param int $id_user : User id - * @param int $id : Entry id - * - * @return int : Number of removed rows - */ - public function delete_for_user(int $id_user, int $id) - { - $query = ' - DELETE FROM media - WHERE id = :id - AND id_scheduled IN (SELECT id FROM scheduled WHERE id_user = :id_user) - '; - - $params = ['id_user' => $id_user, 'id' => $id]; - - return $this->_run_query($query, $params, self::ROWCOUNT); - } - - /** - * Delete a entry by his id for a user. - * - * @param int $id_user : User id - * @param int $id_scheduled : Scheduled id - * - * @return int : Number of removed rows - */ - public function delete_for_scheduled_and_user(int $id_user, int $id_scheduled) - { - $query = ' - DELETE FROM media - WHERE id_scheduled = :id_scheduled - AND id_scheduled IN (SELECT id FROM scheduled WHERE id_user = :id_user) - '; - - $params = ['id_user' => $id_user, 'id_scheduled' => $id_scheduled]; - - return $this->_run_query($query, $params, self::ROWCOUNT); - } - - /** - * Update a media sms for a user. - * - * @param int $id_user : User id - * @param int $id : Entry id - * @param array $data : data to update - * - * @return int : number of modified rows - */ - public function update_for_user(int $id_user, int $id, array $data) - { - $params = []; - $sets = []; - - foreach ($data as $label => $value) - { - $label = preg_replace('#[^a-zA-Z0-9_]#', '', $label); - $params['set_' . $label] = $value; - $sets[] = '`' . $label . '` = :set_' . $label . ' '; - } - - $query = ' - UPDATE `media` - SET ' . implode(', ', $sets) . ' - WHERE id = :id - AND id_scheduled IN (SELECT id FROM scheduled WHERE id_user = :id_user) - '; - - $params['id'] = $id; - $params['id_user'] = $id_user; - - return $this->_run_query($query, $params, self::ROWCOUNT); - } - - /** - * Count number of media sms for user. - * - * @param int $id_user : user id - * - * @return int : Number of media SMS for user - */ - public function count_for_user($id_user) - { - $query = ' - SELECT COUNT(id) as nb - FROM media - WHERE id_scheduled IN (SELECT id FROM scheduled WHERE id_user = :id_user) + SELECT m.id as id, m.id_user as id_user, m.path as path + FROM `' . $this->get_table_name() . '` as m + INNER JOIN media_sended as ms + ON m.id = ms.id_media + WHERE ms.id_sended = :id_sended '; $params = [ - 'id_user' => $id_user, + 'id_sended' => $id_sended, ]; - return $this->_run_query($query, $params)[0]['nb'] ?? 0; + return $this->_run_query($query, $params); + } + + /** + * Return all medias for a received. + * + * @param int $id_received : received id + * + * @return array + */ + public function gets_for_received(int $id_received) + { + $query = ' + SELECT m.id as id, m.id_user as id_user, m.path as path + FROM `' . $this->get_table_name() . '` as m + INNER JOIN media_received as mr + ON m.id = mr.id_media + WHERE mr.id_received = :id_received + '; + + $params = [ + 'id_received' => $id_received, + ]; + + return $this->_run_query($query, $params); + } + + /** + * Link a media to a scheduled + * + * @param int $id_media : Media id + * @param int $id_scheduled : Scheduled id + * + * @return bool | int + */ + public function insert_media_scheduled (int $id_media, int $id_scheduled) + { + $entry = [ + 'id_media' => $id_media, + 'id_scheduled' => $id_scheduled, + ]; + + return $this->_insert('media_scheduled', $entry) ? $this->_last_id() : false; + } + + /** + * Link a media to a received + * + * @param int $id_media : Media id + * @param int $id_received : Scheduled id + * + * @return bool | int + */ + public function insert_media_received (int $id_media, int $id_received) + { + $entry = [ + 'id_media' => $id_media, + 'id_received' => $id_received, + ]; + + return $this->_insert('media_received', $entry) ? $this->_last_id() : false; + } + + /** + * Link a media to a sended + * + * @param int $id_media : Media id + * @param int $id_sended : Scheduled id + * + * @return bool | int + */ + public function insert_media_sended (int $id_media, int $id_sended) + { + $entry = [ + 'id_media' => $id_media, + 'id_sended' => $id_sended, + ]; + + return $this->_insert('media_sended', $entry) ? $this->_last_id() : false; + } + + /** + * Unlink a media of a scheduled + * + * @param int $id_media : Media id + * @param int $id_scheduled : Scheduled id + * + * @return bool | int + */ + public function delete_media_scheduled (int $id_media, int $id_scheduled) + { + $where = [ + 'id_media' => $id_media, + 'id_scheduled' => $id_scheduled, + ]; + + return $this->_delete('media_scheduled', $where); + } + + /** + * Unlink a media of a received + * + * @param int $id_media : Media id + * @param int $id_received : Scheduled id + * + * @return bool | int + */ + public function delete_media_received (int $id_media, int $id_received) + { + $where = [ + 'id_media' => $id_media, + 'id_received' => $id_received, + ]; + + return $this->_delete('media_received', $where); + } + + /** + * Unlink a media of a sended + * + * @param int $id_media : Media id + * @param int $id_sended : Scheduled id + * + * @return bool | int + */ + public function delete_media_sended (int $id_media, int $id_sended) + { + $where = [ + 'id_media' => $id_media, + 'id_sended' => $id_sended, + ]; + + return $this->_delete('media_sended', $where); + } + + + /** + * Unlink all medias of a scheduled + * + * @param int $id_scheduled : Scheduled id + * + * @return bool | int + */ + public function delete_all_for_scheduled (int $id_scheduled) + { + $where = [ + 'id_scheduled' => $id_scheduled, + ]; + + return $this->_delete('media_scheduled', $where); + } + + /** + * Unlink all medias of a received + * + * @param int $id_received : Scheduled id + * + * @return bool | int + */ + public function delete_all_for_received (int $id_received) + { + $where = [ + 'id_received' => $id_received, + ]; + + return $this->_delete('media_received', $where); + } + + /** + * Unlink all medias of a sended + * + * @param int $id_sended : Scheduled id + * + * @return bool | int + */ + public function delete_all_for_sended (int $id_sended) + { + $where = [ + 'id_sended' => $id_sended, + ]; + + return $this->_delete('media_sended', $where); + } + + /** + * Find all unused medias + * @return array + */ + public function gets_unused () + { + $query = ' + SELECT `media`.* + FROM `media` + LEFT JOIN `media_sended` + ON `media`.id = `media_sended`.id_media + LEFT JOIN `media_received` + ON `media`.id = `media_received`.id_media + LEFT JOIN `media_scheduled` + ON `media`.id = `media_scheduled`.id_media + WHERE `media_sended`.id IS NULL + AND `media_received`.id IS NULL + AND `media_scheduled`.id IS NULL + '; + + return $this->_run_query($query); } /** diff --git a/models/Received.php b/models/Received.php index 23a8c05..28fabef 100644 --- a/models/Received.php +++ b/models/Received.php @@ -178,6 +178,34 @@ namespace models; return $this->_run_query($query, $params); } + + /** + * Return sendeds for an origin and a user since a date. + * + * @param int $id_user : User id + * @param string $since : Date we want messages since + * @param string $origin : Number who sent the message + * + * @return array + */ + public function gets_since_date_by_origin_and_user(int $id_user, string $since, string $origin) + { + $query = ' + SELECT * + FROM received + WHERE id_user = :id_user + AND origin = :origin + AND at > :since + '; + + $params = [ + 'id_user' => $id_user, + 'origin' => $origin, + 'since' => $since, + ]; + + return $this->_run_query($query, $params); + } /** * Get number of sended SMS for every date since a date for a specific user. diff --git a/models/Scheduled.php b/models/Scheduled.php index 7d7bc90..f1b2cdd 100644 --- a/models/Scheduled.php +++ b/models/Scheduled.php @@ -233,6 +233,63 @@ namespace models; return $this->_run_query($query, $params); } + + + /** + * Get messages scheduled after a date for a number and a user. + * + * @param int $id_user : User id + * @param $date : Date after which we want messages + * @param string $number : Number for which we want messages + * + * @return array + */ + public function gets_after_date_for_number_and_user(int $id_user, $date, string $number) + { + $query = ' + SELECT * + FROM scheduled + WHERE at > :date + AND id_user = :id_user + AND ( + id IN ( + SELECT id_scheduled + FROM scheduled_number + WHERE number = :number + ) + OR id IN ( + SELECT id_scheduled + FROM scheduled_contact + WHERE id_contact IN ( + SELECT id + FROM contact + WHERE number = :number + ) + ) + OR id IN ( + SELECT id_scheduled + FROM scheduled_group + WHERE id_group IN ( + SELECT id_group + FROM `group_contact` + WHERE id_contact IN ( + SELECT id + FROM contact + WHERE number = :number + ) + ) + ) + ) + '; + + $params = [ + 'id_user' => $id_user, + 'date' => $date, + 'number' => $number, + ]; + + return $this->_run_query($query, $params); + } /** * Get scheduleds before a date. diff --git a/models/Sended.php b/models/Sended.php index f0235ba..fa91346 100644 --- a/models/Sended.php +++ b/models/Sended.php @@ -112,6 +112,35 @@ namespace models; return $this->_run_query($query, $params); } + + + /** + * Return sendeds for an destination and a user since a date. + * + * @param int $id_user : User id + * @param string $since : Date we want messages since + * @param string $destination : Number who sent the message + * + * @return array + */ + public function gets_since_date_by_destination_and_user(int $id_user, string $since, string $destination) + { + $query = ' + SELECT * + FROM sended + WHERE id_user = :id_user + AND destination = :destination + AND at > :since + '; + + $params = [ + 'id_user' => $id_user, + 'destination' => $destination, + 'since' => $since, + ]; + + return $this->_run_query($query, $params); + } /** * Return sended for an uid and an adapter. diff --git a/models/StandardModel.php b/models/StandardModel.php index 32b9686..45bf967 100644 --- a/models/StandardModel.php +++ b/models/StandardModel.php @@ -158,9 +158,9 @@ namespace models; * * @return int : number of modified rows */ - public function update_for_user(int $id_user, int $id, array $entry) + public function update_for_user(int $id_user, int $id, array $data) { - return $this->_update($this->get_table_name(), $entry, ['id_user' => $id_user, 'id' => $id]); + return $this->_update($this->get_table_name(), $data, ['id_user' => $id_user, 'id' => $id]); } /** diff --git a/models/Webhook.php b/models/Webhook.php index 945cd82..48d4998 100644 --- a/models/Webhook.php +++ b/models/Webhook.php @@ -13,8 +13,9 @@ namespace models; class Webhook extends StandardModel { - const TYPE_SEND = 'send_sms'; - const TYPE_RECEIVE = 'receive_sms'; + const TYPE_SEND_SMS = 'send_sms'; + const TYPE_RECEIVE_SMS = 'receive_sms'; + const TYPE_INBOUND_CALL = 'inbound_call'; /** * Find all webhooks for a user and for a type of webhook. diff --git a/routes.php b/routes.php index 121d4ba..1e3adb1 100644 --- a/routes.php +++ b/routes.php @@ -43,6 +43,7 @@ 'update' => '/contact/update/{csrf}/', 'import' => '/contact/import/{csrf}/', 'export' => '/contact/export/{format}/', + 'conditional_delete' => '/contact/conditional_delete/{csrf}/', 'json_list' => '/contacts.json/', ], @@ -54,14 +55,14 @@ 'list_json' => '/discussion/json/', 'show' => '/discussion/show/{number}/', 'send' => '/discussion/send/{csrf}/', - 'get_messages' => '/discussion/getmessage/{number}/{transaction_id}/', + 'get_messages' => [ + '/discussion/getmessage/{number}/{transaction_id}/', + '/discussion/getmessage/{number}/{transaction_id}/{since}/', + ], ], 'Event' => [ - 'list' => [ - '/event/', - '/event/p/{page}/', - ], + 'list' => '/event/', 'list_json' => '/event/json/', 'delete' => '/event/delete/{csrf}/', ], @@ -155,6 +156,12 @@ 'delete' => '/phone/delete/{csrf}/', ], + 'Call' => [ + 'list' => '/call/', + 'list_json' => '/call/json/', + 'delete' => '/call/delete/{csrf}/', + ], + 'Webhook' => [ 'list' => '/webhook/', 'list_json' => '/webhook/json/', @@ -168,6 +175,8 @@ 'Callback' => [ 'update_sended_status' => '/callback/status/{adapter_uid}/', 'reception' => '/callback/reception/{adapter_uid}/{id_phone}/', + 'inbound_call' => '/callback/inbound_call/{id_phone}/', + 'end_call' => '/callback/end_call/{id_phone}/', ], 'Api' => [ diff --git a/templates/call/list.php b/templates/call/list.php new file mode 100644 index 0000000..76d2eaf --- /dev/null +++ b/templates/call/list.php @@ -0,0 +1,145 @@ +render('incs/head', ['title' => 'Appels - Show All']) +?> +
+render('incs/nav', ['page' => 'calls']) +?> +
+
+ +
+
+

+ Dashboard Appels +

+ +
+
+ + +
+
+
+
+

Liste des appels

+
+
+
+
+ + + + + + + + + + + + +
OrigineDestinataireDébut de l'appelFin de l'appelDirection
+
+
+
+ Action pour la séléction : + +
+
+
+
+
+
+
+
+
+
+ +render('incs/footer'); diff --git a/templates/command/list.php b/templates/command/list.php index 89a2ab1..df6b555 100644 --- a/templates/command/list.php +++ b/templates/command/list.php @@ -72,7 +72,7 @@ jQuery(document).ready(function () { jQuery('.datatable').DataTable({ "pageLength": 25, - "bLengthChange": false, + "lengthMenu": [[25, 50, 100, 1000, 10000, -1], [25, 50, 100, 1000, 10000, "All"]], "language": { "url": HTTP_PWD + "/assets/js/datatables/french.json", }, diff --git a/templates/conditional_group/add.php b/templates/conditional_group/add.php index 6e517ab..13a0ac9 100644 --- a/templates/conditional_group/add.php +++ b/templates/conditional_group/add.php @@ -48,7 +48,7 @@

- Les conditions vous permettent de définir dynamiquement les contacts qui appartiennent au groupe en utilisant leurs données additionnelles. Pour plus d'informations consultez la documentation relative à l'utilisation des groupes conditionnels.
+ Les conditions vous permettent de définir dynamiquement les contacts qui appartiennent au groupe en utilisant leurs données additionnelles. Pour plus d'informations consultez la documentation relative à l'utilisation des groupes conditionnels.
Vous pouvez prévisualiser les contacts qui feront parti du groupe en cliquant sur le bouton "Prévisualiser les contacts".

diff --git a/templates/conditional_group/list.php b/templates/conditional_group/list.php index ecb104e..bca9c0f 100644 --- a/templates/conditional_group/list.php +++ b/templates/conditional_group/list.php @@ -1,7 +1,7 @@ render('incs/head', ['title' => 'ConditionalGroupes Conditionnels - Show All']) + $this->render('incs/head', ['title' => 'Groupes Conditionnels - Show All']) ?>
Nom Condition + Date de création + Dernière modification ✓ @@ -72,7 +74,7 @@ jQuery(document).ready(function () { jQuery('.datatable').DataTable({ "pageLength": 25, - "bLengthChange": false, + "lengthMenu": [[25, 50, 100, 1000, 10000, -1], [25, 50, 100, 1000, 10000, "All"]], "language": { "url": HTTP_PWD + "/assets/js/datatables/french.json", }, @@ -87,7 +89,14 @@ jQuery(document).ready(function () }, "columns" : [ {data: 'name', render: jQuery.fn.dataTable.render.text()}, - {data: 'condition', render: jQuery.fn.dataTable.render.text()}, + { + data: 'condition', + render: function (data, type, row, meta) { + return '' + jQuery.fn.dataTable.render.text().display(data) + ''; + }, + }, + {data: 'created_at'}, + {data: 'updated_at'}, { data: 'id', render: function (data, type, row, meta) { diff --git a/templates/contact/list.php b/templates/contact/list.php index 47e3d18..c8f17e1 100644 --- a/templates/contact/list.php +++ b/templates/contact/list.php @@ -14,7 +14,8 @@

Dashboard Contacts - Exporter la liste des contacts + Supprimer une liste dynamique de contacts + Exporter la liste des contacts Importer une liste de contacts

+