diff --git a/VERSION b/VERSION index 66cfae5..6eef43b 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -v3.1.3 +v3.1.4 diff --git a/adapters/AdapterInterface.php b/adapters/AdapterInterface.php index b7fa981..426dcab3 100644 --- a/adapters/AdapterInterface.php +++ b/adapters/AdapterInterface.php @@ -84,22 +84,22 @@ interface AdapterInterface public static function meta_support_status_change(): bool; /** - * Does the implemented service support mms reception + * Does the implemented service support mms reception. */ public static function meta_support_mms_reception(): bool; /** - * Does the implemented service support mms sending + * Does the implemented service support mms sending. */ public static function meta_support_mms_sending(): bool; - + /** - * Does the implemented service support inbound call callback + * 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 + * Does the implemented service support end call callback. */ public static function meta_support_end_call_callback(): bool; @@ -118,23 +118,23 @@ interface AdapterInterface * 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; + public function send(string $destination, string $text, bool $flash = false, bool $mms = false, array $medias = []): array; /** * 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 - * ] - * ], ...] - * ] + * 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; @@ -157,50 +157,49 @@ interface AdapterInterface * 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 - * ] - * ] - * ] + * 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 + * 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. - * ] - * ] + * 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 + * 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. - * ] - * ] + * 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 c872782..e9c5195 100644 --- a/adapters/BenchmarkAdapter.php +++ b/adapters/BenchmarkAdapter.php @@ -125,9 +125,9 @@ namespace adapters; { return false; } - + /** - * Does the implemented service support mms reception + * Does the implemented service support mms reception. */ public static function meta_support_mms_reception(): bool { @@ -135,24 +135,24 @@ namespace adapters; } /** - * Does the implemented service support mms sending + * 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 + public function send(string $destination, string $text, bool $flash = false, bool $mms = false, array $medias = []): array { $response = [ 'error' => false, @@ -230,12 +230,12 @@ namespace adapters; { 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 3fd8a45..bc894d3 100644 --- a/adapters/GammuAdapter.php +++ b/adapters/GammuAdapter.php @@ -135,9 +135,9 @@ namespace adapters; { return false; } - + /** - * Does the implemented service support mms reception + * Does the implemented service support mms reception. */ public static function meta_support_mms_reception(): bool { @@ -145,24 +145,24 @@ namespace adapters; } /** - * Does the implemented service support mms sending + * 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 + public function send(string $destination, string $text, bool $flash = false, bool $mms = false, array $medias = []): array { $response = [ 'error' => false, @@ -306,12 +306,12 @@ namespace adapters; { return []; } - + public function inbound_call_callback(): array { return []; } - + public function end_call_callback(): array { return []; diff --git a/adapters/OctopushShortcodeAdapter.php b/adapters/OctopushShortcodeAdapter.php index e2d4240..4a645e6 100644 --- a/adapters/OctopushShortcodeAdapter.php +++ b/adapters/OctopushShortcodeAdapter.php @@ -172,9 +172,9 @@ class OctopushShortcodeAdapter implements AdapterInterface { return true; } - + /** - * Does the implemented service support mms reception + * Does the implemented service support mms reception. */ public static function meta_support_mms_reception(): bool { @@ -182,24 +182,24 @@ class OctopushShortcodeAdapter implements AdapterInterface } /** - * Does the implemented service support mms sending + * 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 + public function send(string $destination, string $text, bool $flash = false, bool $mms = false, array $medias = []): array { $response = [ 'error' => false, @@ -417,12 +417,12 @@ 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 d38eb29..ca3c986 100644 --- a/adapters/OctopushVirtualNumberAdapter.php +++ b/adapters/OctopushVirtualNumberAdapter.php @@ -177,9 +177,9 @@ class OctopushVirtualNumberAdapter implements AdapterInterface { return true; } - + /** - * Does the implemented service support mms reception + * Does the implemented service support mms reception. */ public static function meta_support_mms_reception(): bool { @@ -187,24 +187,24 @@ class OctopushVirtualNumberAdapter implements AdapterInterface } /** - * Does the implemented service support mms sending + * 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 + public function send(string $destination, string $text, bool $flash = false, bool $mms = false, array $medias = []): array { $response = [ 'error' => false, @@ -417,12 +417,12 @@ 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 9c6137b..01004c9 100644 --- a/adapters/OvhSmsShortcodeAdapter.php +++ b/adapters/OvhSmsShortcodeAdapter.php @@ -169,9 +169,9 @@ namespace adapters; { return false; } - + /** - * Does the implemented service support mms reception + * Does the implemented service support mms reception. */ public static function meta_support_mms_reception(): bool { @@ -179,24 +179,24 @@ namespace adapters; } /** - * Does the implemented service support mms sending + * 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 + public function send(string $destination, string $text, bool $flash = false, bool $mms = false, array $medias = []): array { $response = [ 'error' => false, @@ -369,12 +369,12 @@ namespace adapters; { 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 35925a3..59da733 100644 --- a/adapters/OvhSmsVirtualNumberAdapter.php +++ b/adapters/OvhSmsVirtualNumberAdapter.php @@ -180,9 +180,9 @@ namespace adapters; { return false; } - + /** - * Does the implemented service support mms reception + * Does the implemented service support mms reception. */ public static function meta_support_mms_reception(): bool { @@ -190,24 +190,24 @@ namespace adapters; } /** - * Does the implemented service support mms sending + * 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 + public function send(string $destination, string $text, bool $flash = false, bool $mms = false, array $medias = []): array { $response = [ 'error' => false, @@ -368,12 +368,12 @@ namespace adapters; { 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 3745c71..7203b58 100644 --- a/adapters/TestAdapter.php +++ b/adapters/TestAdapter.php @@ -130,9 +130,9 @@ namespace adapters; { return false; } - + /** - * Does the implemented service support mms reception + * Does the implemented service support mms reception. */ public static function meta_support_mms_reception(): bool { @@ -140,24 +140,24 @@ namespace adapters; } /** - * Does the implemented service support mms sending + * Does the implemented service support mms sending. */ 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 + public function send(string $destination, string $text, bool $flash = false, bool $mms = false, array $medias = []): array { $response = [ 'error' => false, @@ -194,8 +194,8 @@ namespace adapters; * "mms" : true, * "origin" : "+33612345678", * "text" : "SMS Text" - * } - */ + * }. + */ public function read(): array { $response = [ @@ -323,7 +323,7 @@ namespace adapters; { return []; } - + public function inbound_call_callback(): array { $response = [ @@ -352,7 +352,7 @@ namespace adapters; return $response; } - + public function end_call_callback(): array { $response = [ diff --git a/adapters/TwilioVirtualNumberAdapter.php b/adapters/TwilioVirtualNumberAdapter.php index a753d9e..3e39c85 100644 --- a/adapters/TwilioVirtualNumberAdapter.php +++ b/adapters/TwilioVirtualNumberAdapter.php @@ -176,7 +176,7 @@ class TwilioVirtualNumberAdapter implements AdapterInterface } /** - * Does the implemented service support mms reception + * Does the implemented service support mms reception. */ public static function meta_support_mms_reception(): bool { @@ -184,24 +184,24 @@ class TwilioVirtualNumberAdapter implements AdapterInterface } /** - * Does the implemented service support mms sending + * 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 + public function send(string $destination, string $text, bool $flash = false, bool $mms = false, array $medias = []): array { $response = [ 'error' => false, @@ -346,12 +346,12 @@ class TwilioVirtualNumberAdapter implements AdapterInterface { 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 1bda82d..1cc9809 100644 --- a/assets/css/style.css +++ b/assets/css/style.css @@ -89,6 +89,13 @@ footer img opacity: 0.7; } +/** DASHBOARD **/ +.dashboard-panel-chart .panel-title +{ + margin-bottom: 10px; +} + + /** GROUPES **/ .list-contacts { @@ -360,6 +367,12 @@ footer img font-weight: bold; } +.credit-estimation-container +{ + margin-top: 10px; + text-align: right; +} + /* AUDIO RECEPTION MESSAGE */ #reception-sound { diff --git a/controllers/internals/Adapter.php b/controllers/internals/Adapter.php index 1fae465..65be090 100644 --- a/controllers/internals/Adapter.php +++ b/controllers/internals/Adapter.php @@ -118,8 +118,8 @@ namespace controllers\internals; } /** - * List all adapters for a meta value - * + * List all adapters for a meta value. + * * @param $search_name : Name of the meta * @param $search_value : Value of the meta * @@ -128,7 +128,9 @@ namespace controllers\internals; 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) { + + return array_filter($adapters, function ($metas) use ($search_name, $search_value) + { $match = false; foreach ($metas as $name => $value) { diff --git a/controllers/internals/Call.php b/controllers/internals/Call.php index 4852c72..9f47aaa 100644 --- a/controllers/internals/Call.php +++ b/controllers/internals/Call.php @@ -18,11 +18,11 @@ namespace controllers\internals; /** * 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 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 @@ -49,15 +49,23 @@ namespace controllers\internals; switch ($direction) { - case \models\Call::DIRECTION_OUTBOUND : - if (null === $destination) { return false; } - break; - - case \models\Call::DIRECTION_INBOUND : - if (null === $origin) { return false; } + case \models\Call::DIRECTION_OUTBOUND: + if (null === $destination) + { + return false; + } + break; - default : + case \models\Call::DIRECTION_INBOUND: + if (null === $origin) + { + return false; + } + + break; + + default: return false; } @@ -75,7 +83,7 @@ namespace controllers\internals; { return false; } - + $new_call_id = $this->get_model()->insert($call); if (!$new_call_id) { @@ -83,21 +91,20 @@ namespace controllers\internals; } $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 + * 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 + * @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 */ @@ -125,7 +132,7 @@ namespace controllers\internals; return (bool) $this->get_model()->update_for_user($id_user, $call['id'], $datas); } - + /** * Get the model for the Controller. */ diff --git a/controllers/internals/Console.php b/controllers/internals/Console.php index f2f3986..4401ec3 100644 --- a/controllers/internals/Console.php +++ b/controllers/internals/Console.php @@ -103,7 +103,15 @@ namespace controllers\internals; if ($user) { $api_key = $api_key ?? $internal_user->generate_random_api_key(); - $success = $internal_user->update($user['id'], $email, $password, $admin, $api_key, $status, $encrypt_password); + $user = [ + 'email' => $email, + 'password' => $encrypt_password ? password_hash($password, PASSWORD_DEFAULT) : $password, + 'admin' => $admin, + 'api_key' => $api_key, + 'status' => $status, + ]; + + $success = $internal_user->update($user['id'], $user); echo json_encode(['id' => $user['id']]); exit($success ? 0 : 1); @@ -152,9 +160,8 @@ namespace controllers\internals; exit($success ? 0 : 1); } - /** - * Delete medias that are no longer usefull + * Delete medias that are no longer usefull. */ public function clean_unused_medias() { @@ -167,7 +174,27 @@ namespace controllers\internals; { $success = $internal_media->delete_for_user($media['id_user'], $media['id']); - echo ($success === false ? '[KO]' : '[OK]') . ' - ' . $media['path'] . "\n"; + echo (false === $success ? '[KO]' : '[OK]') . ' - ' . $media['path'] . "\n"; } } + + /** + * Do alerting for quota limits. + */ + public function quota_limit_alerting() + { + $bdd = \descartes\Model::_connect(DATABASE_HOST, DATABASE_NAME, DATABASE_USER, DATABASE_PASSWORD, 'UTF8'); + $internal_quota = new \controllers\internals\Quota($bdd); + $internal_quota->alerting_for_limit_close_and_reached(); + } + + /** + * Do quota renewal. + */ + public function renew_quotas() + { + $bdd = \descartes\Model::_connect(DATABASE_HOST, DATABASE_NAME, DATABASE_USER, DATABASE_PASSWORD, 'UTF8'); + $internal_quota = new \controllers\internals\Quota($bdd); + $internal_quota->renew_quotas(); + } } diff --git a/controllers/internals/Event.php b/controllers/internals/Event.php index c4fc6c6..d43de0e 100644 --- a/controllers/internals/Event.php +++ b/controllers/internals/Event.php @@ -56,6 +56,21 @@ namespace controllers\internals; return $this->get_model()->insert($event); } + /** + * Gets events for a type, since a date and eventually until a date (both included). + * + * @param int $id_user : User id + * @param string $type : Event type we want + * @param \DateTime $since : Date to get events since + * @param ?\DateTime $until (optional) : Date until wich we want events, if not specified no limit + * + * @return array + */ + public function get_events_by_type_and_date_for_user(int $id_user, string $type, \DateTime $since, ?\DateTime $until = null) + { + $this->get_model()->get_events_by_type_and_date_for_user($id_user, $type, $since, $until); + } + /** * Get the model for the Controller. */ diff --git a/controllers/internals/Media.php b/controllers/internals/Media.php index 2722aab..8d230fc 100644 --- a/controllers/internals/Media.php +++ b/controllers/internals/Media.php @@ -20,8 +20,8 @@ class Media extends StandardController /** * 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 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 @@ -33,13 +33,13 @@ class Media extends StandardController { throw new \Exception('File ' . $tmpfile_path . ' does not exists.'); } - + if (!is_readable($tmpfile_path)) { throw new \Exception('File ' . $tmpfile_path . ' is not readable.'); } - $mimey = new \Mimey\MimeTypes; + $mimey = new \Mimey\MimeTypes(); $extension = $extension ?? $mimey->getExtension(mime_content_type($tmpfile_path)); $new_file_name = \controllers\internals\Tool::random_uuid() . '.' . $extension; @@ -60,7 +60,7 @@ class Media extends StandardController { 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)); @@ -86,16 +86,17 @@ class Media extends StandardController } /** - * Upload and create a media - * - * @param int $id_user : Id of the user - * @param array $file : array representing uploaded file, extracted from $_FILES['yourfile'] + * 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) + if (true !== $upload_result['success']) { throw new \Exception($upload_result['content']); } @@ -110,15 +111,16 @@ class Media extends StandardController { 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 + * 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 + * @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 */ @@ -128,14 +130,17 @@ class Media extends StandardController { 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: @@ -143,12 +148,12 @@ class Media extends StandardController } } - /** - * Unlink a media of a scheduled, a received or a sended message - * @param int $id_media : Id of the media + * 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 + * @param int $resource_id : Id of the resource to unlink the media of * * @return mixed bool : false on error, true on success */ @@ -158,14 +163,17 @@ class Media extends StandardController { 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: @@ -174,9 +182,10 @@ class Media extends StandardController } /** - * Unlink all medias of a scheduled, a received or a sended message + * 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 + * @param int $resource_id : Id of the resource to unlink the media of * * @return mixed bool : false on error, true on success */ @@ -186,14 +195,17 @@ class Media extends StandardController { 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: @@ -204,9 +216,9 @@ class Media extends StandardController /** * 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 + * @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 */ @@ -289,7 +301,8 @@ class Media extends StandardController } /** - * Find medias that are not used + * Find medias that are not used. + * * @return array */ public function gets_unused() diff --git a/controllers/internals/Phone.php b/controllers/internals/Phone.php index 3b54c86..b473a02 100644 --- a/controllers/internals/Phone.php +++ b/controllers/internals/Phone.php @@ -44,13 +44,14 @@ namespace controllers\internals; } /** - * Check if a phone support mms + * 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 + * @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) + public function support_mms(int $id, string $type) { $phone = $this->get_model()->get($id); if (!$phone) @@ -60,16 +61,19 @@ namespace controllers\internals; switch ($type) { - case self::MMS_SENDING : + case self::MMS_SENDING: return $phone['adapter']::meta_support_mms_sending(); + break; - case self::MMS_RECEPTION : + case self::MMS_RECEPTION: return $phone['adapter']::meta_support_mms_reception(); + break; - case self::MMS_BOTH : + case self::MMS_BOTH: return $phone['adapter']::meta_support_mms_sending() && $phone['adapter']::meta_support_mms_reception(); + break; default: @@ -78,13 +82,14 @@ namespace controllers\internals; } /** - * Get all phones supporting mms for a user + * 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 + * @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) + public function gets_phone_supporting_mms_for_user(int $id_user, string $type) { $phones = $this->get_model()->gets_for_user($id_user); diff --git a/controllers/internals/Quota.php b/controllers/internals/Quota.php new file mode 100644 index 0000000..e1bcaf1 --- /dev/null +++ b/controllers/internals/Quota.php @@ -0,0 +1,297 @@ + + * + * 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 Quota extends StandardController +{ + protected $model; + + /** + * Create a new quota. + * + * @param int $id_user : User id + * @param int $credit : Credit for this quota + * @param int $additional : Additionals credits + * @param bool $report_unused : Should unused credits be re-credited + * @param bool $report_unused_additional : Should unused additional credits be re-credited + * @param bool $auto_renew : Should the quota be automatically renewed after expiration_date + * @param string $renew_interval : Period to use for setting new expiration_date on renewal (format ISO_8601#Durations) + * @param \DateTime $start_date : Starting date for the quota + * @param \DateTime $expiration_date : Ending date for the quota + * + * @return mixed bool|int : False if cannot create quota, id of the new quota else + */ + public function create(int $id_user, int $credit, int $additional, bool $report_unused, bool $report_unused_additional, bool $auto_renew, string $renew_interval, \DateTime $start_date, \DateTime $expiration_date) + { + $quota = [ + 'id_user' => $id_user, + 'credit' => $credit, + 'report_unused' => $report_unused, + 'report_unused_additional' => $report_unused_additional, + 'start_date' => $start_date->format('Y-m-d H:i:s'), + 'expiration_date' => $expiration_date->format('Y-m-d H:i:s'), + 'auto_renew' => $auto_renew, + 'renew_interval' => $renew_interval, + 'additional' => $additional, + ]; + + return $this->get_model()->insert($quota); + } + + /** + * Update a quota. + * + * @param int $id_user : User id + * @param int $id_quota : Quota to update id + * @param array $quota : Fields to update whith new values + * + * @return int : number of updated lines + */ + public function update_for_user(int $id_user, $id_quota, array $quota) + { + return $this->get_model()->update_for_user($id_user, $id_quota, $quota); + } + + /** + * Check if we have enough credit. + * + * @param int $id_user : User id + * @param int $needed : Number of credits we need + * + * @return bool : true if we have enough credit, false else + */ + public function has_enough_credit(int $id_user, int $needed) + { + $remaining_credit = $this->get_model()->get_remaining_credit($id_user, new \DateTime()); + + return $remaining_credit >= $needed; + } + + /** + * Consume some credit. + * + * @param int $id_user : User id + * @param int $quantity : Number of credits to consume + * + * @return bool : True on success, false else + */ + public function consume_credit(int $id_user, int $quantity) + { + $result = $this->get_model()->consume_credit($id_user, $quantity); + + //Write event + $internal_event = new Event($this->bdd); + $internal_event->create($id_user, 'QUOTA_CONSUME', 'Consume ' . $quantity . ' credits of SMS quota.'); + + return $result; + } + + /** + * Get quota usage percentage. + * + * @param int $id_user : User id + * + * @return float : percentage of quota used + */ + public function get_usage_percentage(int $id_user) + { + return $this->get_model()->get_usage_percentage($id_user, new \DateTime()); + } + + /** + * Compute how many credit a message represent + * this function count 160 chars per SMS if it can be send as GSM 03.38 encoding and 70 chars per SMS if it can only be send as UTF8. + * + * @param string $text : Message to send + * + * @return int : Number of credit to send this message + */ + public static function compute_credits_for_message($text) + { + //Gsm 03.38 charset to detect if message is compatible or must use utf8 + $gsm0338 = [ + '@', 'Δ', ' ', '0', '¡', 'P', '¿', 'p', + '£', '_', '!', '1', 'A', 'Q', 'a', 'q', + '$', 'Φ', '"', '2', 'B', 'R', 'b', 'r', + '¥', 'Γ', '#', '3', 'C', 'S', 'c', 's', + 'è', 'Λ', '¤', '4', 'D', 'T', 'd', 't', + 'é', 'Ω', '%', '5', 'E', 'U', 'e', 'u', + 'ù', 'Π', '&', '6', 'F', 'V', 'f', 'v', + 'ì', 'Ψ', '\'', '7', 'G', 'W', 'g', 'w', + 'ò', 'Σ', '(', '8', 'H', 'X', 'h', 'x', + 'Ç', 'Θ', ')', '9', 'I', 'Y', 'i', 'y', + "\n", 'Ξ', '*', ':', 'J', 'Z', 'j', 'z', + 'Ø', "\x1B", '+', ';', 'K', 'Ä', 'k', 'ä', + 'ø', 'Æ', ',', '<', 'L', 'Ö', 'l', 'ö', + "\r", 'æ', '-', '=', 'M', 'Ñ', 'm', 'ñ', + 'Å', 'ß', '.', '>', 'N', 'Ü', 'n', 'ü', + 'å', 'É', '/', '?', 'O', '§', 'o', 'à', + ]; + + $is_gsm0338 = true; + + $len = mb_strlen($text); + for ($i = 0; $i < $len; ++$i) + { + if (!in_array(mb_substr($text, $i, 1), $gsm0338)) + { + $is_gsm0338 = false; + + break; + } + } + + return $is_gsm0338 ? ceil($len / 160) : ceil($len / 70); + } + + /** + * Do email alerting for quotas limit close and quotas limit reached. + */ + public function alerting_for_limit_close_and_reached() + { + $internal_user = new User($this->bdd); + $internal_event = new Event($this->bdd); + + $quotas_limit_close = $this->get_model()->get_quotas_for_limit_close(); + $quotas_limit_reached = $this->get_model()->get_quotas_for_limit_reached(); + + foreach ($quotas_limit_close as $quota) + { + $user = $internal_user->get($quota['id_user']); + + if (!$user) + { + continue; + } + + $quota_percentage = $quota['consumed'] / ($quota['credit'] + $quota['additional']); + + $mailer = new \controllers\internals\Mailer(); + $success = $mailer->enqueue($user['email'], EMAIL_QUOTA_LIMIT_CLOSE, ['percent' => $quota_percentage]); + + if (!$success) + { + echo 'Cannot enqueue alert for quota limit close for quota : ' . $quota['id'] . "\n"; + + continue; + } + + echo 'Enqueue alert for quota limit close for quota : ' . $quota['id'] . "\n"; + $internal_event->create($quota['id_user'], 'QUOTA_LIMIT_CLOSE', round($quota_percentage * 100, 2) . '% of SMS quota limit reached.'); + } + + foreach ($quotas_limit_reached as $quota) + { + $user = $internal_user->get($quota['id_user']); + + if (!$user) + { + continue; + } + + $quota_percentage = $quota['consumed'] / ($quota['credit'] + $quota['additional']); + + $mailer = new \controllers\internals\Mailer(); + $success = $mailer->enqueue($user['email'], EMAIL_QUOTA_LIMIT_REACHED, ['expiration_date' => $quota['expiration_date']]); + + if (!$success) + { + echo 'Cannot enqueue alert for quota limit reached for quota : ' . $quota['id'] . "\n"; + + continue; + } + + echo 'Enqueue alert for quota limit reached for quota : ' . $quota['id'] . "\n"; + $internal_event->create($quota['id_user'], 'QUOTA_LIMIT_REACHED', 'Reached SMS quota limit.'); + } + } + + /** + * Do quota renewing. + */ + public function renew_quotas() + { + $internal_user = new User($this->bdd); + $internal_event = new Event($this->bdd); + $quotas = $this->get_model()->get_quotas_to_be_renewed(new \DateTime()); + + foreach ($quotas as $quota) + { + $user = $internal_user->get($quota['id_user']); + + if (!$user) + { + continue; + } + + $unused_credit = $quota['credit'] - $quota['consumed']; + $unused_additional = $unused_credit > 0 ? $quota['additional'] : $quota['additional'] + $unused_credit; + + $renew_interval = $quota['renew_interval'] ?? 'P0D'; + $new_start_date = new \DateTime($quota['expiration_date']); + $new_expiration_date = clone $new_start_date; + $new_expiration_date->add(new \DateInterval($quota['renew_interval'])); + + $report = 0; + if ($quota['report_unused'] && $unused_credit > 0) + { + $report += $unused_credit; + } + + if ($quota['report_unused_additional'] && $unused_additional > 0) + { + $report += $unused_additional; + } + + $updated_fields = [ + 'start_date' => $new_start_date->format('Y-m-d H:i:s'), + 'expiration_date' => $new_expiration_date->format('Y-m-d H:i:s'), + 'additional' => $report, + 'consumed' => 0, + ]; + + $success = $this->update_for_user($user['id'], $quota['id'], $updated_fields); + + if (!$success) + { + echo 'Cannot update quota : ' . $quota['id'] . "\n"; + + continue; + } + + echo 'Update quota : ' . $quota['id'] . "\n"; + $internal_event->create($quota['id_user'], 'QUOTA_RENEWAL', 'Renew quota and report ' . $report . ' credits.'); + } + } + + /** + * Return the quota for a user if it exists. + * + * @param int $id_user : user id + * + * @return array + */ + public function get_user_quota(int $id_user) + { + return $this->get_model()->get_user_quota($id_user); + } + + /** + * Get the model for the Controller. + */ + protected function get_model(): \descartes\Model + { + $this->model = $this->model ?? new \models\Quota($this->bdd); + + return $this->model; + } +} diff --git a/controllers/internals/Received.php b/controllers/internals/Received.php index 048f1d6..60aefba 100644 --- a/controllers/internals/Received.php +++ b/controllers/internals/Received.php @@ -36,11 +36,11 @@ namespace controllers\internals; * @param int $id_phone : Id of the number the message was send with * @param $at : Reception date * @param $text : Text of the message - * @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 + * @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 */ @@ -64,6 +64,7 @@ namespace controllers\internals; if (!$id_received) { $this->bdd->rollBack(); + return false; } @@ -75,6 +76,7 @@ namespace controllers\internals; if (!$id_media_received) { $this->bdd->rollBack(); + return false; } } @@ -167,6 +169,7 @@ namespace controllers\internals; * @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) @@ -256,11 +259,11 @@ 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 - * ], ...] + * @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 @@ -285,7 +288,7 @@ namespace controllers\internals; $is_command = true; $text = $response; } - + //We create medias to link to the sms $internal_media = new Media($this->bdd); $media_ids = []; @@ -301,6 +304,7 @@ namespace controllers\internals; catch (\Throwable $t) { $return['error_message'] = $t->getMessage(); + continue; //Better loose the media than the message } } diff --git a/controllers/internals/Scheduled.php b/controllers/internals/Scheduled.php index a7c65df..1a0c75c 100644 --- a/controllers/internals/Scheduled.php +++ b/controllers/internals/Scheduled.php @@ -43,7 +43,7 @@ namespace controllers\internals; 'mms' => $mms, ]; - if ($text === '') + if ('' === $text) { return false; } @@ -66,6 +66,7 @@ namespace controllers\internals; if (!$id_scheduled) { $this->bdd->rollBack(); + return false; } @@ -76,11 +77,11 @@ namespace controllers\internals; if (!$id_media_scheduled) { $this->bdd->rollBack(); + return false; } } - foreach ($numbers as $number) { $this->get_model()->insert_scheduled_number($id_scheduled, $number); @@ -193,6 +194,7 @@ namespace controllers\internals; if (!$id_media_scheduled) { $this->bdd->rollBack(); + return false; } } @@ -254,7 +256,7 @@ 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. * @@ -349,7 +351,7 @@ namespace controllers\internals; $random_phone = $users_phones[$scheduled['id_user']][$rnd_key]; } } - + $message = [ 'id_user' => $scheduled['id_user'], 'id_scheduled' => $scheduled['id'], @@ -405,7 +407,7 @@ namespace controllers\internals; } $added_contacts[$contact['id']] = true; - + if (null === $phone_to_use) { if ($scheduled['mms'] && count($users_mms_phones)) diff --git a/controllers/internals/Sended.php b/controllers/internals/Sended.php index 2e24327..203eaf6 100644 --- a/controllers/internals/Sended.php +++ b/controllers/internals/Sended.php @@ -27,7 +27,7 @@ namespace controllers\internals; * @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 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 @@ -49,14 +49,15 @@ namespace controllers\internals; //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) @@ -132,13 +133,14 @@ 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) @@ -204,7 +206,7 @@ namespace controllers\internals; * @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 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 : [ @@ -219,6 +221,17 @@ namespace controllers\internals; 'error_message' => null, ]; + //If we reached our max quota, do not send the message + $internal_quota = new Quota($this->bdd); + $nb_credits = $internal_quota::compute_credits_for_message($text); //Calculate how much credit the message require + if (!$internal_quota->has_enough_credit($id_user, $nb_credits)) + { + $return['error'] = false; + $return['error_message'] = 'Not enough credit to send message.'; + + return $return; + } + $at = (new \DateTime())->format('Y-m-d H:i:s'); $media_uris = []; foreach ($medias as $media) @@ -253,6 +266,8 @@ namespace controllers\internals; return $return; } + $internal_quota->consume_credit($id_user, $nb_credits); + $sended_id = $this->create($id_user, $id_phone, $at, $text, $destination, $response['uid'] ?? uniqid(), $adapter->meta_classname(), $flash, $mms, $medias, $status); $sended = [ diff --git a/controllers/internals/Tool.php b/controllers/internals/Tool.php index a207185..26ec6d9 100644 --- a/controllers/internals/Tool.php +++ b/controllers/internals/Tool.php @@ -123,6 +123,26 @@ namespace controllers\internals; break; + case 'QUOTA_LIMIT_CLOSE': + $logo = 'fa-exclamation'; + + break; + + case 'QUOTA_LIMIT_REACHED': + $logo = 'fa-exclamation-triangle'; + + break; + + case 'QUOTA_RENEWAL': + $logo = 'fa-retweet'; + + break; + + case 'QUOTA_CONSUME': + $logo = 'fa-euro'; + + break; + default: $logo = 'fa-question'; } @@ -145,6 +165,27 @@ namespace controllers\internals; return $objectDate && $objectDate->format($format) === $date; } + /** + * Check if a sting represent a valid PHP period for creating an interval. + * + * @param string $period : Period string to check + * + * @return bool : True if valid period, false else + */ + public static function validate_period($period) + { + try + { + $interval = new \DateInterval($period); + } + catch (\Throwable $e) + { + return false; + } + + return true; + } + /** * Cette fonction retourne un mot de passe généré aléatoirement. * @@ -279,26 +320,27 @@ namespace controllers\internals; } /** - * Generate a highly random uuid based on timestamp and strong cryptographic random + * Generate a highly random uuid based on timestamp and strong cryptographic random. * * @return string */ public static function random_uuid() { $bytes = random_bytes(16); + return time() . '-' . bin2hex($bytes); } - /** - * Create a user data public path + * 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) + public static function create_user_public_path(int $id_user) { $new_dir = PWD_DATA_PUBLIC . '/' . $id_user; if (file_exists($new_dir)) @@ -313,18 +355,18 @@ namespace controllers\internals; } //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 - { + 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 throw new \Exception('Cannot give dir ' . $new_dir . ' rights : ' . decoct(fileperms(PWD_DATA_PUBLIC) & 0777)); //Show error in dec } - if (posix_getuid() === 0 && !chown($new_dir, fileowner(PWD_DATA_PUBLIC))) //If we are root, try to give the file to a proper user - { + if (0 === posix_getuid() && !chown($new_dir, fileowner(PWD_DATA_PUBLIC))) + { //If we are root, try to give the file to a proper user throw new \Exception('Cannot give dir ' . $new_dir . ' to user : ' . fileowner(PWD_DATA_PUBLIC)); } - if (posix_getuid() === 0 && !chgrp($new_dir, filegroup(PWD_DATA_PUBLIC))) //If we are root, try to give the file to a proper group - { + if (0 === posix_getuid() && !chgrp($new_dir, filegroup(PWD_DATA_PUBLIC))) + { //If we are root, try to give the file to a proper group throw new \Exception('Cannot give dir ' . $new_dir . ' to group : ' . filegroup(PWD_DATA_PUBLIC)); } diff --git a/controllers/internals/User.php b/controllers/internals/User.php index 427cceb..6dd93ac 100644 --- a/controllers/internals/User.php +++ b/controllers/internals/User.php @@ -16,6 +16,7 @@ namespace controllers\internals; */ class User extends \descartes\InternalController { + private $bdd; private $model_user; private $internal_event; private $internal_setting; @@ -23,12 +24,25 @@ namespace controllers\internals; public function __construct(\PDO $bdd) { + $this->bdd = $bdd; $this->model_user = new \models\User($bdd); $this->internal_event = new \controllers\internals\Event($bdd); $this->internal_setting = new \controllers\internals\Setting($bdd); $this->internal_phone = new Phone($bdd); } + /** + * Return a list of users by their ids. + * + * @param array $ids : ids of entries to find + * + * @return array + */ + public function gets_in_by_id(array $ids) + { + return $this->model_user->gets_in_by_id($ids); + } + /** * Return list of users as an array. * @@ -180,27 +194,56 @@ namespace controllers\internals; /** * Update a user by his id. * - * @param mixed $id - * @param mixed $email - * @param mixed $password - * @param mixed $admin - * @param mixed $api_key - * @param string $status : User status - * @param bool $encrypt_password : Should the password be encrypted, by default true + * @param mixed $id : User id + * @param array $user : Array of fields to update for user + * @param mixed (?array|bool) $quota : Quota to update for the user, by default null -> no update, if false, remove quota * - * @return int : Number of modified user + * @return bool : True on success, false on error */ - public function update($id, $email, $password, $admin, $api_key, $status, bool $encrypt_password = true) + public function update($id, array $user, $quota = null) { - $user = [ - 'email' => $email, - 'password' => $encrypt_password ? password_hash($password, PASSWORD_DEFAULT) : $password, - 'admin' => $admin, - 'api_key' => $api_key, - 'status' => $status, - ]; + $internal_quota = new Quota($this->bdd); + $current_quota = $internal_quota->get_user_quota($id); - return $this->model_user->update($id, $user); + $this->bdd->beginTransaction(); + + $this->model_user->update($id, $user); + + if ($current_quota && false === $quota) + { + $success = $internal_quota->delete_for_user($id, $current_quota['id']); + if (!$success) + { + $this->bdd->rollback(); + + return false; + } + } + + if ($quota) + { + if ($current_quota) + { + $internal_quota->update_for_user($id, $current_quota['id'], $quota); + } + else + { + $success = $internal_quota->create($id, $quota['credit'], $quota['additional'], $quota['report_unused'], $quota['report_unused_additional'], $quota['auto_renew'], $quota['renew_interval'], new \DateTime($quota['start_date']), new \DateTime($quota['expiration_date'])); + if (!$success) + { + $this->bdd->rollback(); + + return false; + } + } + } + + if (!$this->bdd->commit()) + { + return false; + } + + return true; } /** @@ -212,10 +255,11 @@ namespace controllers\internals; * @param ?string $api_key : The api key of the user, if null generate randomly * @param string $status : User status, default \models\User::STATUS_ACTIVE * @param bool $encrypt_password : Should the password be encrypted, by default true + * @param ?array $quota : Quota to create for the user, by default null -> no quota * * @return mixed bool|int : false on error, id of the new user else */ - public function create($email, $password, $admin, ?string $api_key = null, string $status = \models\User::STATUS_ACTIVE, bool $encrypt_password = true) + public function create($email, $password, $admin, ?string $api_key = null, string $status = \models\User::STATUS_ACTIVE, bool $encrypt_password = true, ?array $quota = null) { $user = [ 'email' => $email, @@ -225,22 +269,39 @@ namespace controllers\internals; 'status' => $status, ]; - $new_id_user = $this->model_user->insert($user); + $this->bdd->beginTransaction(); + $new_id_user = $this->model_user->insert($user); if (!$new_id_user) { return false; } $success = $this->internal_setting->create_defaults_for_user($new_id_user); - if (!$success) { - $this->delete($new_id_user); + $this->bdd->rollback(); return false; } + if (null !== $quota) + { + $internal_quota = new Quota($this->bdd); + $success = $internal_quota->create($new_id_user, $quota['credit'], $quota['additional'], $quota['report_unused'], $quota['report_unused_additional'], $quota['auto_renew'], $quota['renew_interval'], $quota['start_date'], $quota['expiration_date']); + if (!$success) + { + $this->bdd->rollback(); + + return false; + } + } + + if (!$this->bdd->commit()) + { + return false; + } + return $new_id_user; } @@ -291,9 +352,9 @@ namespace controllers\internals; } $mailer = new Mailer(); - + $attachments = []; - + foreach ($received['medias'] ?? [] as $media) { $attachments[] = PWD_DATA_PUBLIC . '/' . $media['path']; diff --git a/controllers/publics/Account.php b/controllers/publics/Account.php index 0b4544d..84e8d55 100644 --- a/controllers/publics/Account.php +++ b/controllers/publics/Account.php @@ -14,11 +14,13 @@ namespace controllers\publics; class Account extends \descartes\Controller { public $internal_user; + public $internal_quota; public function __construct() { $bdd = \descartes\Model::_connect(DATABASE_HOST, DATABASE_NAME, DATABASE_USER, DATABASE_PASSWORD); $this->internal_user = new \controllers\internals\User($bdd); + $this->internal_quota = new \controllers\internals\Quota($bdd); \controllers\internals\Tool::verifyconnect(); } @@ -28,7 +30,9 @@ namespace controllers\publics; */ public function show() { - $this->render('account/show'); + $quota = $this->internal_quota->get_user_quota($_SESSION['user']['id']); + $quota_percent = $this->internal_quota->get_usage_percentage($_SESSION['user']['id']); + $this->render('account/show', ['quota' => $quota, 'quota_percent' => $quota_percent]); } /** diff --git a/controllers/publics/Api.php b/controllers/publics/Api.php index c153e85..ec55514 100644 --- a/controllers/publics/Api.php +++ b/controllers/publics/Api.php @@ -231,16 +231,16 @@ namespace controllers\publics; //Iterate over files to re-create individual $_FILES array $files_arrays = []; - if ($files === false) + if (false === $files) { $files_arrays = []; } - elseif (!is_array($files['name'])) //Only one file uploaded - { + elseif (!is_array($files['name'])) + { //Only one file uploaded $files_arrays[] = $files; } - else //multiple files - { + else + { //multiple files foreach ($files as $property_name => $files_values) { foreach ($files_values as $file_key => $property_value) @@ -320,8 +320,8 @@ namespace controllers\publics; if ($id_phone) { $phone = $this->internal_phone->get_for_user($this->user['id'], $id_phone); - } - + } + if ($id_phone && !$phone) { $return = self::DEFAULT_RETURN; diff --git a/controllers/publics/Call.php b/controllers/publics/Call.php index fb05bc2..26bfb02 100644 --- a/controllers/publics/Call.php +++ b/controllers/publics/Call.php @@ -34,7 +34,7 @@ namespace controllers\publics; } /** - * Page for showing calls list + * Page for showing calls list. */ public function list() { @@ -51,12 +51,14 @@ namespace controllers\publics; { switch ($entity['direction']) { - case \models\Call::DIRECTION_INBOUND : + case \models\Call::DIRECTION_INBOUND: $entity['origin_formatted'] = \controllers\internals\Tool::phone_link($entity['origin']); + break; - - case \models\Call::DIRECTION_OUTBOUND : + + case \models\Call::DIRECTION_OUTBOUND: $entity['destination_formatted'] = \controllers\internals\Tool::phone_link($entity['destination']); + break; } } @@ -66,10 +68,10 @@ namespace controllers\publics; } /** - * Delete a list of calls + * Delete a list of calls. * * @param array int $_GET['ids'] : Ids of calls to delete - * @param string $csrf : csrf token + * @param string $csrf : csrf token * * @return boolean; */ diff --git a/controllers/publics/Callback.php b/controllers/publics/Callback.php index ff1fecc..db68632 100644 --- a/controllers/publics/Callback.php +++ b/controllers/publics/Callback.php @@ -206,13 +206,12 @@ 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 + * @param int $id_phone : Phone id * * @return bool : true on success, false on error */ @@ -269,16 +268,15 @@ use Monolog\Logger; } $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 + * @param int $id_phone : Phone id * * @return bool : true on success, false on error */ @@ -334,7 +332,7 @@ use Monolog\Logger; } $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 9ba60f9..2a1aad8 100644 --- a/controllers/publics/ConditionalGroup.php +++ b/controllers/publics/ConditionalGroup.php @@ -241,7 +241,7 @@ namespace controllers\publics; if ($how_many_more > 0) { - $result_text .= ", et $how_many_more autres."; + $result_text .= ", et {$how_many_more} autres."; } $return['result'] = $result_text; diff --git a/controllers/publics/Contact.php b/controllers/publics/Contact.php index 9d6e98f..600e5e8 100644 --- a/controllers/publics/Contact.php +++ b/controllers/publics/Contact.php @@ -85,12 +85,12 @@ namespace controllers\publics; return $this->redirect(\descartes\Router::url('Contact', 'list')); } - + /** - * This function will delete a list of contacts depending on a condition + * 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 + * @param mixed $csrf * * @return boolean; */ @@ -338,11 +338,11 @@ namespace controllers\publics; break; default: - if ($read_file['extension'] === 'csv') + if ('csv' === $read_file['extension']) { $result = $this->internal_contact->import_csv($id_user, $read_file['content']); } - elseif ($read_file['extension'] === 'json') + elseif ('json' === $read_file['extension']) { $result = $this->internal_contact->import_json($id_user, $read_file['content']); } diff --git a/controllers/publics/Dashboard.php b/controllers/publics/Dashboard.php index 50f2779..280026f 100644 --- a/controllers/publics/Dashboard.php +++ b/controllers/publics/Dashboard.php @@ -22,6 +22,7 @@ namespace controllers\publics; private $internal_group; private $internal_scheduled; private $internal_event; + private $internal_quota; /** * Cette fonction est appelée avant toute les autres : @@ -39,6 +40,7 @@ namespace controllers\publics; $this->internal_group = new \controllers\internals\Group($bdd); $this->internal_scheduled = new \controllers\internals\Scheduled($bdd); $this->internal_event = new \controllers\internals\Event($bdd); + $this->internal_quota = new \controllers\internals\Quota($bdd); \controllers\internals\Tool::verifyconnect(); } @@ -60,40 +62,52 @@ namespace controllers\publics; $nb_sendeds = $this->internal_sended->count_for_user($id_user); $nb_receiveds = $this->internal_received->count_for_user($id_user); - //Création de la date d'il y a une semaine - $now = new \DateTime(); - $one_week = new \DateInterval('P7D'); - $date = $now->sub($one_week); - $formated_date = $date->format('Y-m-d'); - //Récupération des 10 derniers Sms envoyés, Sms reçus et evenements enregistrés. Par date. $sendeds = $this->internal_sended->get_lasts_by_date_for_user($id_user, 10); $receiveds = $this->internal_received->get_lasts_by_date_for_user($id_user, 10); $events = $this->internal_event->get_lasts_by_date_for_user($id_user, 10); - //Récupération du nombre de Sms envoyés et reçus depuis les 7 derniers jours - $nb_sendeds_by_day = $this->internal_sended->count_by_day_since_for_user($id_user, $formated_date); - $nb_receiveds_by_day = $this->internal_received->count_by_day_since_for_user($id_user, $formated_date); + //Récupération du nombre de Sms envoyés et reçus depuis 1 mois jours ou depuis le début du quota si il existe + + //Création de la date d'il y a 30 jours + $now = new \DateTime(); + $one_month = new \DateInterval('P1M'); + $stats_start_date = clone $now; + $stats_start_date->sub($one_month); + $stats_start_date_formated = $stats_start_date->format('Y-m-d'); + + //If user have a quota and the quota start before today, use quota start date instead + $quota_limit = false; + $quota = $this->internal_quota->get_user_quota($id_user); + if ($quota && (new \DateTime($quota['start_date']) <= $now) && (new \DateTime($quota['expiration_date']) > $now)) + { + $quota_limit = $quota['credit'] + $quota['additional']; + + $stats_start_date = new \DateTime($quota['start_date']); + $stats_start_date_formated = $stats_start_date->format('Y-m-d'); + } + + $nb_sendeds_by_day = $this->internal_sended->count_by_day_since_for_user($id_user, $stats_start_date_formated); + $nb_receiveds_by_day = $this->internal_received->count_by_day_since_for_user($id_user, $stats_start_date_formated); //On va traduire ces données pour les afficher en graphique $array_area_chart = []; - $today_less_7_day = new \DateTime(); - $today_less_7_day->sub(new \DateInterval('P7D')); - $increment_day = new \DateInterval('P1D'); + $date = clone $stats_start_date; + $one_day = new \DateInterval('P1D'); $i = 0; //On va construire un tableau avec la date en clef, et les données pour chaque date - while ($i < 7) + while ($date <= $now) { - $today_less_7_day->add($increment_day); - ++$i; - $date_f = $today_less_7_day->format('Y-m-d'); + $date_f = $date->format('Y-m-d'); $array_area_chart[$date_f] = [ 'period' => $date_f, 'sendeds' => 0, 'receiveds' => 0, ]; + + $date->add($one_day); } $total_sendeds = 0; @@ -112,8 +126,9 @@ namespace controllers\publics; $total_receiveds += $nb_received; } - $avg_sendeds = round($total_sendeds / 7, 2); - $avg_receiveds = round($total_receiveds / 7, 2); + $nb_days = $stats_start_date->diff($now)->days + 1; + $avg_sendeds = round($total_sendeds / $nb_days, 2); + $avg_receiveds = round($total_receiveds / $nb_days, 2); $array_area_chart = array_values($array_area_chart); @@ -126,6 +141,7 @@ namespace controllers\publics; 'nb_unreads' => $nb_unreads, 'avg_sendeds' => $avg_sendeds, 'avg_receiveds' => $avg_receiveds, + 'quota_limit' => $quota_limit, 'sendeds' => $sendeds, 'receiveds' => $receiveds, 'events' => $events, diff --git a/controllers/publics/Discussion.php b/controllers/publics/Discussion.php index 1d385d8..0a49e25 100644 --- a/controllers/publics/Discussion.php +++ b/controllers/publics/Discussion.php @@ -125,7 +125,7 @@ namespace controllers\publics; } $message = [ - 'uid' => 'sended-' . $sended['id'], + 'uid' => 'sended-' . $sended['id'], 'date' => htmlspecialchars($sended['at']), 'text' => htmlspecialchars($sended['text']), 'type' => 'sended', @@ -133,7 +133,6 @@ namespace controllers\publics; 'status' => $sended['status'], ]; - $messages[] = $message; } @@ -143,7 +142,7 @@ namespace controllers\publics; { $this->internal_received->mark_as_read_for_user($id_user, $received['id']); } - + $medias = []; if ($received['mms']) { @@ -155,7 +154,7 @@ namespace controllers\publics; } $messages[] = [ - 'uid' => 'received-' . $received['id'], + 'uid' => 'received-' . $received['id'], 'date' => htmlspecialchars($received['at']), 'text' => htmlspecialchars($received['text']), 'type' => 'received', @@ -176,7 +175,7 @@ namespace controllers\publics; } $messages[] = [ - 'uid' => 'scheduled-' . $scheduled['id'], + 'uid' => 'scheduled-' . $scheduled['id'], 'date' => htmlspecialchars($scheduled['at']), 'text' => htmlspecialchars($scheduled['text']), 'type' => 'inprogress', @@ -210,7 +209,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 + * @param array $_FILES['medias'] : Medias to upload and link to sms * * @return string : json string Le statut de l'envoi */ @@ -237,9 +236,9 @@ namespace controllers\publics; $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 = []; + $files_arrays = []; if ($files && is_array($files['name'])) { foreach ($files as $property_name => $files_values) @@ -248,7 +247,7 @@ namespace controllers\publics; { if (!isset($files_arrays[$file_key])) { - $files_arrays[$file_key] = []; + $files_arrays[$file_key] = []; } $files_arrays[$file_key][$property_name] = $property_value; @@ -259,7 +258,7 @@ namespace controllers\publics; //Remove empty files input foreach ($files_arrays as $key => $file) { - if ($file['error'] === UPLOAD_ERR_NO_FILE) + if (UPLOAD_ERR_NO_FILE === $file['error']) { unset($files_arrays[$key]); } @@ -288,7 +287,6 @@ namespace controllers\publics; $id_phone = null; } - //If mms is enable and we have medias uploaded $media_ids = []; if ($_SESSION['user']['settings']['mms'] && $files_arrays) diff --git a/controllers/publics/Phone.php b/controllers/publics/Phone.php index 81d6249..52ba392 100644 --- a/controllers/publics/Phone.php +++ b/controllers/publics/Phone.php @@ -75,12 +75,12 @@ 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]); diff --git a/controllers/publics/Scheduled.php b/controllers/publics/Scheduled.php index b96c58d..5c309f3 100644 --- a/controllers/publics/Scheduled.php +++ b/controllers/publics/Scheduled.php @@ -242,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['medias'] : The media to link to a scheduled + * @param ?array $_FILES['medias'] : The media to link to a scheduled */ public function create($csrf) { @@ -265,7 +265,7 @@ namespace controllers\publics; $files = $_FILES['medias'] ?? false; //Iterate over files to re-create individual $_FILES array - $files_arrays = []; + $files_arrays = []; if ($files && is_array($files['name'])) { foreach ($files as $property_name => $files_values) @@ -281,11 +281,11 @@ namespace controllers\publics; } } } - + //Remove empty files input foreach ($files_arrays as $key => $file) { - if ($file['error'] === UPLOAD_ERR_NO_FILE) + if (UPLOAD_ERR_NO_FILE === $file['error']) { unset($files_arrays[$key]); } @@ -325,7 +325,7 @@ 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) @@ -339,15 +339,16 @@ namespace controllers\publics; 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')); } - $media_ids[] = $new_media_id; + $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) { @@ -403,7 +404,7 @@ namespace controllers\publics; } //Iterate over files to re-create individual $_FILES array - $files_arrays = []; + $files_arrays = []; if ($files && is_array($files['name'])) { foreach ($files as $property_name => $files_values) @@ -423,7 +424,7 @@ namespace controllers\publics; //Remove empty files input foreach ($files_arrays as $key => $file) { - if ($file['error'] === UPLOAD_ERR_NO_FILE) + if (UPLOAD_ERR_NO_FILE === $file['error']) { unset($files_arrays[$key]); } @@ -456,7 +457,7 @@ namespace controllers\publics; { continue; } - + //If mms is enable and we have medias uploaded if ($_SESSION['user']['settings']['mms'] && $files_arrays) { @@ -471,7 +472,7 @@ namespace controllers\publics; continue 2; } - $media_ids[] = $new_media_id; + $media_ids[] = $new_media_id; } } @@ -488,7 +489,7 @@ namespace controllers\publics; $mms = (bool) count($media_ids); $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++; + ++$nb_update; } if ($nb_update !== \count($scheduleds)) diff --git a/controllers/publics/Setting.php b/controllers/publics/Setting.php index 28ef605..c5a34e6 100644 --- a/controllers/publics/Setting.php +++ b/controllers/publics/Setting.php @@ -39,7 +39,8 @@ namespace controllers\publics; * * @param string $setting_name : Name of the setting to modify * @param $csrf : CSRF token - * @param string $_POST['setting_value'] : Setting's new value + * @param string $_POST['setting_value'] : Setting's new value + * @param bool $_POST['allow_no_value'] : Default false, if true then allow $_POST['setting_value'] to dont exists, and treat it as empty string * * @return boolean; */ @@ -53,6 +54,13 @@ namespace controllers\publics; } $setting_value = $_POST['setting_value'] ?? false; + $allow_no_value = $_POST['allow_no_value'] ?? false; + + //if no value allowed and no value fund, default to '' + if ($allow_no_value && (false === $setting_value)) + { + $setting_value = ''; + } if (false === $setting_value) { @@ -61,6 +69,12 @@ namespace controllers\publics; return $this->redirect(\descartes\Router::url('Setting', 'show')); } + //If setting is an array, join with comas + if (is_array($setting_value)) + { + $setting_value = json_encode($setting_value); + } + $update_setting_result = $this->internal_setting->update_for_user($_SESSION['user']['id'], $setting_name, $setting_value); if (false === $update_setting_result) { diff --git a/controllers/publics/Templating.php b/controllers/publics/Templating.php index 3545b7c..026622e 100644 --- a/controllers/publics/Templating.php +++ b/controllers/publics/Templating.php @@ -15,6 +15,7 @@ namespace controllers\publics; { private $internal_contact; private $internal_templating; + private $internal_quota; /** * Cette fonction est appelée avant toute les autres : @@ -27,6 +28,7 @@ namespace controllers\publics; $bdd = \descartes\Model::_connect(DATABASE_HOST, DATABASE_NAME, DATABASE_USER, DATABASE_PASSWORD); $this->internal_contact = new \controllers\internals\Contact($bdd); $this->internal_templating = new \controllers\internals\Templating(); + $this->internal_quota = new \controllers\internals\Quota($bdd); \controllers\internals\Tool::verifyconnect(); } @@ -44,6 +46,7 @@ namespace controllers\publics; $return = [ 'success' => false, 'result' => 'Une erreur inconnue est survenue.', + 'estimation_credit' => 0, ]; $template = $_POST['template'] ?? false; @@ -79,11 +82,15 @@ namespace controllers\publics; $result = $this->internal_templating->render($template, $data); $return = $result; + if (!trim($result['result'])) { $return['result'] = 'Message vide, il ne sera pas envoyé.'; } + //Add credit estimation + $return['estimation_credit'] = $this->internal_quota->compute_credits_for_message($return['result']); + echo json_encode($return); return true; diff --git a/controllers/publics/User.php b/controllers/publics/User.php index 2b22a6a..ab4df63 100644 --- a/controllers/publics/User.php +++ b/controllers/publics/User.php @@ -17,6 +17,7 @@ namespace controllers\publics; class User extends \descartes\Controller { private $internal_user; + private $internal_quota; /** * Cette fonction est appelée avant toute les autres : @@ -28,6 +29,7 @@ class User extends \descartes\Controller { $bdd = \descartes\Model::_connect(DATABASE_HOST, DATABASE_NAME, DATABASE_USER, DATABASE_PASSWORD); $this->internal_user = new \controllers\internals\User($bdd); + $this->internal_quota = new \controllers\internals\Quota($bdd); \controllers\internals\Tool::verifyconnect(); @@ -51,6 +53,24 @@ class User extends \descartes\Controller public function list_json() { $entities = $this->internal_user->list(); + + foreach ($entities as &$entity) + { + $quota_percentage = $this->internal_quota->get_usage_percentage($entity['id']); + $entity['quota_percentage'] = $quota_percentage * 100; + + $quota = $this->internal_quota->get_user_quota($entity['id']); + if (!$quota) + { + continue; + } + + if (new \DateTime() > new \DateTime($quota['expiration_date'])) + { + $entity['quota_expired_at'] = $quota['expiration_date']; + } + } + header('Content-Type: application/json'); echo json_encode(['data' => $entities]); } @@ -58,9 +78,9 @@ class User extends \descartes\Controller /** * Update status of users. * - * @param array int $_GET['ids'] : User ids + * @param array int $_GET['user_ids'] : User ids * @param mixed $csrf - * @param int $status : 1 -> active, 0 -> suspended + * @param int $status : 1 -> active, 0 -> suspended * * @return boolean; */ @@ -82,7 +102,7 @@ class User extends \descartes\Controller $status = \models\User::STATUS_ACTIVE; } - $ids = $_GET['ids'] ?? []; + $ids = $_GET['user_ids'] ?? []; foreach ($ids as $id) { $this->internal_user->update_status($id, $status); @@ -94,7 +114,7 @@ class User extends \descartes\Controller /** * Cette fonction va supprimer une liste de users. * - * @param array int $_GET['ids'] : Les id des useres à supprimer + * @param array int $_GET['user_ids'] : Les id des useres à supprimer * @param mixed $csrf * * @return boolean; @@ -115,7 +135,7 @@ class User extends \descartes\Controller return $this->redirect(\descartes\Router::url('User', 'list')); } - $ids = $_GET['ids'] ?? []; + $ids = $_GET['user_ids'] ?? []; foreach ($ids as $id) { $this->internal_user->delete($id); @@ -129,18 +149,28 @@ class User extends \descartes\Controller */ public function add() { - return $this->render('user/add'); + $now = new \DateTime(); + $now = $now->format('Y-m-d H:i:00'); + + return $this->render('user/add', ['now' => $now]); } /** * Cette fonction insert un nouveau user. * * @param $csrf : Le jeton CSRF - * @param string $_POST['email'] : L'email de l'utilisateur - * @param string $_POST['email_confirm'] : Verif de l'email de l'utilisateur - * @param optional string $_POST['password'] : Le mot de passe de l'utilisateur (si vide, généré automatiquement) - * @param optional string $_POST['password_confirm'] : Confirmation du mot de passe de l'utilisateur - * @param optional boolean $_POST['admin'] : Si vrai, l'utilisateur est admin, si vide non + * @param string $_POST['email'] : User email + * @param optional string $_POST['password'] : User password, (if empty the password is randomly generated) + * @param optional boolean $_POST['admin'] : If true user is admin + * @param optional boolean $_POST['quota_enable'] : If true create a quota for the user + * @param bool $_POST['quota_enable'] : If true create a quota for the user + * @param optional int $_POST['quota_credit'] : credit for quota + * @param optional int $_POST['quota_additional'] : additional credit + * @param optional string $_POST['quota_start_date'] : quota beginning date + * @param optional string $_POST['quota_renewal_interval'] : period to use on renewal to calculate new expiration date. Also use to calculate first expiration date. + * @param optional boolean $_POST['quota_auto_renew'] : Should the quota be automatically renewed on expiration + * @param optional boolean $_POST['quota_report_unused'] : Should unused credit be reported next month + * @param optional boolean $_POST['quota_report_unused_additional'] : Should unused additional credit be transfered next month */ public function create($csrf) { @@ -155,6 +185,14 @@ class User extends \descartes\Controller $password = !empty($_POST['password']) ? $_POST['password'] : \controllers\internals\Tool::generate_password(rand(6, 12)); $admin = $_POST['admin'] ?? false; $status = 'active'; + $quota_enable = $_POST['quota_enable'] ?? false; + $quota_credit = $_POST['quota_credit'] ?? false; + $quota_additional = $_POST['quota_additional'] ?? false; + $quota_start_date = $_POST['quota_start_date'] ?? false; + $quota_renew_interval = $_POST['quota_renew_interval'] ?? false; + $quota_auto_renew = $_POST['quota_auto_renew'] ?? false; + $quota_report_unused = $_POST['quota_report_unused'] ?? false; + $quota_report_unused_additional = $_POST['quota_report_unused_additional'] ?? false; if (!$email) { @@ -170,10 +208,42 @@ class User extends \descartes\Controller return $this->redirect(\descartes\Router::url('User', 'add')); } - $id_user = $this->internal_user->create($email, $password, $admin); + //Forge quota for user if needed + $quota = null; + if ($quota_enable) + { + $quota = []; + $quota['credit'] = (int) $quota_credit; + $quota['additional'] = (int) $quota_additional; + + if (false === $quota_start_date || !\controllers\internals\Tool::validate_date($quota_start_date, 'Y-m-d H:i:s')) + { + \FlashMessage\FlashMessage::push('danger', 'Vous devez définir une date de début valide pour le quota.'); + + return $this->redirect(\descartes\Router::url('User', 'add')); + } + $quota['start_date'] = new \DateTime($quota_start_date); + + if (false === $quota_renew_interval || !\controllers\internals\Tool::validate_period($quota_renew_interval)) + { + \FlashMessage\FlashMessage::push('danger', 'Vous devez définir une durée de quota parmis la liste proposée.'); + + return $this->redirect(\descartes\Router::url('User', 'add')); + } + $quota['renew_interval'] = $quota_renew_interval; + + $quota['expiration_date'] = clone $quota['start_date']; + $quota['expiration_date']->add(new \DateInterval($quota_renew_interval)); + + $quota['auto_renew'] = (bool) $quota_auto_renew; + $quota['report_unused'] = (bool) $quota_report_unused; + $quota['report_unused_additional'] = (bool) $quota_report_unused_additional; + } + + $id_user = $this->internal_user->create($email, $password, $admin, null, \models\User::STATUS_ACTIVE, true, $quota); if (!$id_user) { - \FlashMessage\FlashMessage::push('danger', 'Impossible de créer ce user.'); + \FlashMessage\FlashMessage::push('danger', 'Impossible de créer cet utilisateur.'); return $this->redirect(\descartes\Router::url('User', 'add')); } @@ -189,4 +259,152 @@ class User extends \descartes\Controller return $this->redirect(\descartes\Router::url('User', 'list')); } + + /** + * Return the edition page for the users. + * + * @param int... $ids : users ids + */ + public function edit() + { + $ids = $_GET['user_ids'] ?? []; + $id_user = $_SESSION['user']['id']; + + $users = $this->internal_user->gets_in_by_id($ids); + + if (!$users) + { + return $this->redirect(\descartes\Router::url('User', 'list')); + } + + foreach ($users as &$user) + { + $user['quota'] = $this->internal_quota->get_user_quota($user['id']); + } + + $now = new \DateTime(); + $now = $now->format('Y-m-d H:i:00'); + + $this->render('user/edit', [ + 'users' => $users, + 'now' => $now, + ]); + } + + /** + * Update a list of users. + * + * @param $csrf : Le jeton CSRF + * @param array $_POST['users'] : Array of the users and new values, id as key. Quota may also be defined. + */ + public function update($csrf) + { + if (!$this->verify_csrf($csrf)) + { + \FlashMessage\FlashMessage::push('danger', 'Jeton CSRF invalid !'); + + return $this->redirect(\descartes\Router::url('User', 'add')); + } + + $nb_update = 0; + $users = $_POST['users'] ?? []; + foreach ($users as $id_user => $user) + { + $email = $user['email'] ?? false; + $password = !empty($user['password']) ? $user['password'] : null; + $admin = $user['admin'] ?? false; + + $quota_enable = $user['quota_enable'] ?? false; + $quota_consumed = $user['quota_consumed'] ?? false; + $quota_credit = $user['quota_credit'] ?? false; + $quota_additional = $user['quota_additional'] ?? false; + $quota_start_date = $user['quota_start_date'] ?? false; + $quota_renew_interval = $user['quota_renew_interval'] ?? false; + $quota_auto_renew = $user['quota_auto_renew'] ?? false; + $quota_report_unused = $user['quota_report_unused'] ?? false; + $quota_report_unused_additional = $user['quota_report_unused_additional'] ?? false; + + if (!$email) + { + \FlashMessage\FlashMessage::push('danger', 'L\'utilisateur #' . (int) $id_user . ' n\'as pas pu être mis à jour car l\'adresse e-mail n\'as pas été fournie.'); + + continue; + } + + if (!filter_var($email, FILTER_VALIDATE_EMAIL)) + { + \FlashMessage\FlashMessage::push('danger', 'L\'utilisateur #' . (int) $id_user . ' n\'as pas pu être mis à jour car l\'adresse e-mail fournie n\'est pas valide.'); + + return $this->redirect(\descartes\Router::url('User', 'add')); + } + + //Forge quota for user if needed + $quota = false; + if ($quota_enable) + { + $quota = []; + $quota['credit'] = (int) $quota_credit; + $quota['consumed'] = (int) $quota_consumed; + $quota['additional'] = (int) $quota_additional; + + if (false === $quota_start_date || !\controllers\internals\Tool::validate_date($quota_start_date, 'Y-m-d H:i:s')) + { + \FlashMessage\FlashMessage::push('danger', 'L\'utilisateur #' . (int) $id_user . ' n\'as pas pu être mis à jour car la date de début du quota associé n\'est pas valide.'); + + continue; + } + $quota['start_date'] = new \DateTime($quota_start_date); + + if (false === $quota_renew_interval || !\controllers\internals\Tool::validate_period($quota_renew_interval)) + { + \FlashMessage\FlashMessage::push('danger', 'L\'utilisateur #' . (int) $id_user . ' n\'as pas pu être mis à jour car la durée du quota associé n\'est pas valide.'); + + continue; + } + $quota['renew_interval'] = $quota_renew_interval; + + $quota['expiration_date'] = clone $quota['start_date']; + $quota['expiration_date']->add(new \DateInterval($quota_renew_interval)); + + $quota['auto_renew'] = (bool) $quota_auto_renew; + $quota['report_unused'] = (bool) $quota_report_unused; + $quota['report_unused_additional'] = (bool) $quota_report_unused_additional; + + //Format dates + $quota['start_date'] = $quota['start_date']->format('Y-m-d H:i:s'); + $quota['expiration_date'] = $quota['expiration_date']->format('Y-m-d H:i:s'); + } + + $updated_user = [ + 'email' => $email, + 'admin' => $admin, + ]; + + if ($password) + { + $updated_user['password'] = password_hash($password, PASSWORD_DEFAULT); + } + + $success = $this->internal_user->update($id_user, $updated_user, $quota); + if (!$success) + { + \FlashMessage\FlashMessage::push('danger', 'L\'utilisateur #' . (int) $id_user . ' n\'as pas pu être mis à jour.'); + + continue; + } + + ++$nb_update; + } + + if ($nb_update != count($users)) + { + \FlashMessage\FlashMessage::push('danger', 'Certains utilisateurs n\'ont pas pu être mis à jour.'); + + return $this->redirect(\descartes\Router::url('User', 'list')); + } + + \FlashMessage\FlashMessage::push('success', 'Tous les utilisateurs ont bien été mis à jour.'); + + return $this->redirect(\descartes\Router::url('User', 'list')); + } } diff --git a/db/migrations/20210607123506_add_quotas.php b/db/migrations/20210607123506_add_quotas.php new file mode 100644 index 0000000..523b6b8 --- /dev/null +++ b/db/migrations/20210607123506_add_quotas.php @@ -0,0 +1,52 @@ +table('quota'); + $table->addColumn('id_user', 'integer', ['null' => false]) + ->addColumn('consumed', 'integer', ['null' => false, 'default' => 0]) + ->addColumn('credit', 'integer', ['null' => false]) + ->addColumn('additional', 'integer', ['null' => false, 'default' => 0]) + ->addColumn('report_unused', 'boolean', ['null' => false]) + ->addColumn('report_unused_additional', 'boolean', ['null' => false]) + ->addColumn('auto_renew', 'boolean', ['null' => false, 'default' => false]) + ->addColumn('renew_interval', 'string', ['null' => false, 'default' => NULL]) + ->addColumn('start_date', 'datetime', ['null' => false]) + ->addColumn('expiration_date', 'datetime', ['null' => false]) + ->addColumn('created_at', 'timestamp', ['null' => false, 'default' => 'CURRENT_TIMESTAMP']) + ->addColumn('updated_at', 'timestamp', ['null' => true, 'update' => 'CURRENT_TIMESTAMP']) + ->addForeignKey('id_user', 'user', 'id', ['delete' => 'CASCADE', 'update' => 'CASCADE']) + ->addIndex(['id_user'], ['unique' => true]) + ->create(); + + } +} diff --git a/db/migrations/20210616015711_create_new_settings_defaults.php b/db/migrations/20210616015711_create_new_settings_defaults.php new file mode 100644 index 0000000..9e9ce8c --- /dev/null +++ b/db/migrations/20210616015711_create_new_settings_defaults.php @@ -0,0 +1,48 @@ +execute($query); + + + $query = 'INSERT INTO setting (id_user, name, value) + SELECT id, \'alert_quota_limit_reached\', \'1\' FROM user WHERE id NOT IN (SELECT id_user FROM setting WHERE name = \'alert_quota_limit_reached\')'; + $this->execute($query); + + + $query = 'INSERT INTO setting (id_user, name, value) + SELECT id, \'alert_quota_limit_close\', \'0.9\' FROM user WHERE id NOT IN (SELECT id_user FROM setting WHERE name = \'alert_quota_limit_close\')'; + $this->execute($query); + } +} diff --git a/env.php.dist b/env.php.dist index 5aa3260..bfee5df 100644 --- a/env.php.dist +++ b/env.php.dist @@ -41,6 +41,16 @@ 'subject' => 'Vous avez reçu un SMS', 'template' => 'email/transfer-sms', ], + 'EMAIL_QUOTA_LIMIT_CLOSE' => [ + 'type' => 'email_quota_limit_close', + 'subject' => 'Vous avez presque atteint votre limite de SMS', + 'template' => 'email/quota-limit-close', + ], + 'EMAIL_QUOTA_LIMIT_REACHED' => [ + 'type' => 'email_quota_limit_reached', + 'subject' => 'Vous avez atteint votre limite de SMS', + 'template' => 'email/quota-limit-reached', + ], //Phone messages types 'QUEUE_ID_PHONE_PREFIX' => ftok(__FILE__, 'p'), @@ -70,6 +80,9 @@ 'default_phone_country' => 'fr', 'authorized_phone_country' => 'fr,be,ca', 'mms' => 1, + 'alert_quota_limit_reached' => 1, + 'alert_quota_limit_close' => 0.9, + 'hide_menus' => '', ], ]; diff --git a/models/Call.php b/models/Call.php index e8e08ce..a09395d 100644 --- a/models/Call.php +++ b/models/Call.php @@ -12,13 +12,13 @@ namespace models; /** - * Manage bdd operations for calls - */ + * 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. @@ -62,11 +62,11 @@ namespace models; } /** - * Get a call for a user by his phone and uid - * - * @param int $id_user : user id + * 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 + * @param int $uid : call uid * * @return array : the call or an empty array */ diff --git a/models/Event.php b/models/Event.php index ef73445..0026750 100644 --- a/models/Event.php +++ b/models/Event.php @@ -26,6 +26,32 @@ namespace models; return $this->_select('event', ['id_user' => $id_user], 'at', true, $nb_entry); } + /** + * Gets events for a type, since a date and eventually until a date (both included). + * + * @param int $id_user : User id + * @param string $type : Event type we want + * @param \DateTime $since : Date to get events since + * @param ?\DateTime $until (optional) : Date until wich we want events, if not specified no limit + * + * @return array + */ + public function get_events_by_type_and_date_for_user(int $id_user, string $type, \DateTime $since, ?\DateTime $until = null) + { + $where = [ + 'id_user' => $id_user, + 'type' => $type, + '>=at' => $since->format('Y-m-d H:i:s'), + ]; + + if (null !== $until) + { + $where['<=at'] = $until->format('Y-m-d H:i:s'); + } + + return $this->_select('event', $where, 'at'); + } + /** * Return table name. */ diff --git a/models/Media.php b/models/Media.php index 830acd7..51db85c 100644 --- a/models/Media.php +++ b/models/Media.php @@ -39,7 +39,7 @@ namespace models; return $this->_run_query($query, $params); } - + /** * Return all medias for a sended. * @@ -63,7 +63,7 @@ namespace models; return $this->_run_query($query, $params); } - + /** * Return all medias for a received. * @@ -87,16 +87,16 @@ namespace models; return $this->_run_query($query, $params); } - + /** - * Link a media to a scheduled - * - * @param int $id_media : Media id + * 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) + public function insert_media_scheduled(int $id_media, int $id_scheduled) { $entry = [ 'id_media' => $id_media, @@ -105,16 +105,16 @@ namespace models; return $this->_insert('media_scheduled', $entry) ? $this->_last_id() : false; } - + /** - * Link a media to a received - * - * @param int $id_media : Media id + * 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) + public function insert_media_received(int $id_media, int $id_received) { $entry = [ 'id_media' => $id_media, @@ -123,16 +123,16 @@ namespace models; return $this->_insert('media_received', $entry) ? $this->_last_id() : false; } - + /** - * Link a media to a sended - * - * @param int $id_media : Media id + * 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) + public function insert_media_sended(int $id_media, int $id_sended) { $entry = [ 'id_media' => $id_media, @@ -141,16 +141,16 @@ namespace models; return $this->_insert('media_sended', $entry) ? $this->_last_id() : false; } - + /** - * Unlink a media of a scheduled - * - * @param int $id_media : Media id + * 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) + public function delete_media_scheduled(int $id_media, int $id_scheduled) { $where = [ 'id_media' => $id_media, @@ -161,14 +161,14 @@ namespace models; } /** - * Unlink a media of a received - * - * @param int $id_media : Media id + * 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) + public function delete_media_received(int $id_media, int $id_received) { $where = [ 'id_media' => $id_media, @@ -177,16 +177,16 @@ namespace models; return $this->_delete('media_received', $where); } - + /** - * Unlink a media of a sended - * - * @param int $id_media : Media id + * 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) + public function delete_media_sended(int $id_media, int $id_sended) { $where = [ 'id_media' => $id_media, @@ -195,16 +195,15 @@ namespace models; return $this->_delete('media_sended', $where); } - - + /** - * Unlink all medias of a scheduled - * + * Unlink all medias of a scheduled. + * * @param int $id_scheduled : Scheduled id * * @return bool | int */ - public function delete_all_for_scheduled (int $id_scheduled) + public function delete_all_for_scheduled(int $id_scheduled) { $where = [ 'id_scheduled' => $id_scheduled, @@ -212,15 +211,15 @@ namespace models; return $this->_delete('media_scheduled', $where); } - + /** - * Unlink all medias of a received - * + * Unlink all medias of a received. + * * @param int $id_received : Scheduled id * * @return bool | int */ - public function delete_all_for_received (int $id_received) + public function delete_all_for_received(int $id_received) { $where = [ 'id_received' => $id_received, @@ -230,13 +229,13 @@ namespace models; } /** - * Unlink all medias of a sended - * + * Unlink all medias of a sended. + * * @param int $id_sended : Scheduled id * * @return bool | int */ - public function delete_all_for_sended (int $id_sended) + public function delete_all_for_sended(int $id_sended) { $where = [ 'id_sended' => $id_sended, @@ -246,10 +245,11 @@ namespace models; } /** - * Find all unused medias + * Find all unused medias. + * * @return array */ - public function gets_unused () + public function gets_unused() { $query = ' SELECT `media`.* @@ -264,7 +264,7 @@ namespace models; AND `media_received`.id IS NULL AND `media_scheduled`.id IS NULL '; - + return $this->_run_query($query); } diff --git a/models/Quota.php b/models/Quota.php new file mode 100644 index 0000000..5035f64 --- /dev/null +++ b/models/Quota.php @@ -0,0 +1,200 @@ + + * + * This source file is subject to the GPL-3.0 license that is bundled + * with this source code in the file LICENSE. + */ + +namespace models; + + class Quota extends StandardModel + { + /** + * Return the quota for a user if it exists. + * + * @param int $id_user : user id + * + * @return array : quota if found, else empty array + */ + public function get_user_quota(int $id_user) + { + return $this->_select_one($this->get_table_name(), ['id_user' => $id_user]); + } + + /** + * Get remaining credit for a date + * if no quota for this user return max int. + * + * @param int $id_user : User id + * @param \DateTime $at : date to get credit at + * + * @return int : number of remaining credits + */ + public function get_remaining_credit(int $id_user, \DateTime $at): int + { + $query = ' + SELECT (credit + additional - consumed) AS remaining_credit + FROM quota + WHERE id_user = :id_user + AND start_date <= :at + AND expiration_date > :at'; + + $params = [ + 'id_user' => $id_user, + 'at' => $at->format('Y-m-d H:i:s'), + ]; + + $result = $this->_run_query($query, $params); + + return $result[0]['remaining_credit'] ?? PHP_INT_MAX; + } + + /** + * Get credit usage percent for a date + * if no quota for this user return 0. + * + * @param int $id_user : User id + * @param \DateTime $at : date to get usage percent at + * + * @return float : percent of used credits + */ + public function get_usage_percentage(int $id_user, \DateTime $at): float + { + $query = ' + SELECT (consumed / (credit + additional)) AS usage_percentage + FROM quota + WHERE id_user = :id_user + AND start_date <= :at + AND expiration_date > :at'; + + $params = [ + 'id_user' => $id_user, + 'at' => $at->format('Y-m-d H:i:s'), + ]; + + $result = $this->_run_query($query, $params); + + return (float) ($result[0]['usage_percentage'] ?? 0); + } + + /** + * Consume some credit for a user. + * + * @param int $id_user : User id + * @param int $quantity : Number of credits to consume + * + * @return bool + */ + public function consume_credit(int $id_user, int $quantity): int + { + $query = ' + UPDATE quota + SET consumed = consumed + :quantity + WHERE id_user = :id_user'; + + $params = [ + 'id_user' => $id_user, + 'quantity' => $quantity, + ]; + + return (bool) $this->_run_query($query, $params, \descartes\Model::ROWCOUNT); + } + + /** + * Get all quotas we need to send an alert for close limit to users they belongs to + * do not return quotas when user already had an event QUOTA_LIMIT_CLOSE since quota start_date. + */ + public function get_quotas_for_limit_close(): array + { + $query = ' + SELECT quota.* + FROM quota + INNER JOIN setting + ON ( + quota.id_user = setting.id_user + AND setting.NAME = :setting_name + AND setting.value != 0 + ) + WHERE + quota.consumed / ( quota.credit + quota.additional ) >= setting.value + AND ( + SELECT COUNT(id) + FROM event + WHERE + id_user = quota.id_user + AND type = :event_type + AND at >= quota.start_date + ) = 0; +'; + + $params = [ + 'setting_name' => 'alert_quota_limit_close', + 'event_type' => 'QUOTA_LIMIT_CLOSE', + ]; + + return $this->_run_query($query, $params); + } + + /** + * Get all quotas we need to send an alert for limit reached to users they belongs to + * do not return quotas when user already had an event QUOTA_LIMIT_REACHED since quota start_date. + */ + public function get_quotas_for_limit_reached(): array + { + $query = ' + SELECT quota.* + FROM quota + INNER JOIN setting + ON ( + quota.id_user = setting.id_user + AND setting.NAME = :setting_name + AND setting.value = 1 + ) + WHERE + quota.consumed >= (quota.credit + quota.additional) + AND ( + SELECT COUNT(id) + FROM event + WHERE + id_user = quota.id_user + AND type = :event_type + AND at >= quota.start_date + ) = 0; + '; + + $params = [ + 'setting_name' => 'alert_quota_limit_reached', + 'event_type' => 'QUOTA_LIMIT_REACHED', + ]; + + return $this->_run_query($query, $params); + } + + /** + * Get list of quotas to be renewed as to a date. + * + * @param \DateTime $at : Date to get quotas to be renewed before + */ + public function get_quotas_to_be_renewed(\DateTime $at): array + { + $at = $at->format('Y-m-d H:i:s'); + $where = [ + '<=expiration_date' => $at, + 'auto_renew' => true, + ]; + + return $this->_select('quota', $where); + } + + /** + * Return table name. + */ + protected function get_table_name(): string + { + return 'quota'; + } + } diff --git a/models/Received.php b/models/Received.php index 28fabef..c25289f 100644 --- a/models/Received.php +++ b/models/Received.php @@ -178,13 +178,13 @@ 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 int $id_user : User id * @param string $since : Date we want messages since - * @param string $origin : Number who sent the message + * @param string $origin : Number who sent the message * * @return array */ diff --git a/models/Scheduled.php b/models/Scheduled.php index f1b2cdd..8911ca4 100644 --- a/models/Scheduled.php +++ b/models/Scheduled.php @@ -233,8 +233,7 @@ namespace models; return $this->_run_query($query, $params); } - - + /** * Get messages scheduled after a date for a number and a user. * diff --git a/models/Sended.php b/models/Sended.php index fa91346..04429a5 100644 --- a/models/Sended.php +++ b/models/Sended.php @@ -112,13 +112,12 @@ 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 $since : Date we want messages since * @param string $destination : Number who sent the message * * @return array diff --git a/models/User.php b/models/User.php index f48274f..43a1560 100644 --- a/models/User.php +++ b/models/User.php @@ -31,6 +31,33 @@ namespace models; return $this->_select_one('user', ['id' => $id]); } + /** + * Find user by ids. + * + * @param array $ids : users ids + * + * @return array + */ + public function gets_in_by_id($ids) + { + if (!$ids) + { + return []; + } + + $query = ' + SELECT * FROM `user` + WHERE id '; + + $params = []; + + $generated_in = $this->_generate_in_from_array($ids); + $query .= $generated_in['QUERY']; + $params = $generated_in['PARAMS']; + + return $this->_run_query($query, $params); + } + /** * Find a user using his email. * diff --git a/models/Webhook.php b/models/Webhook.php index 48d4998..f55bf85 100644 --- a/models/Webhook.php +++ b/models/Webhook.php @@ -16,6 +16,8 @@ namespace models; const TYPE_SEND_SMS = 'send_sms'; const TYPE_RECEIVE_SMS = 'receive_sms'; const TYPE_INBOUND_CALL = 'inbound_call'; + const TYPE_QUOTA_LEVEL_ALERT = 'quota_level'; + const TYPE_QUOTA_REACHED = 'quota_reached'; /** * Find all webhooks for a user and for a type of webhook. diff --git a/routes.php b/routes.php index 1e3adb1..a0fab1f 100644 --- a/routes.php +++ b/routes.php @@ -145,6 +145,8 @@ 'add' => '/user/add/', 'create' => '/user/create/{csrf}/', 'delete' => '/user/delete/{csrf}/', + 'edit' => '/user/edit/', + 'update' => '/user/update/{csrf}/', 'update_status' => '/user/delete/{status}/{csrf}/', ], diff --git a/templates/account/show.php b/templates/account/show.php index 5a5574d..6d1355d 100644 --- a/templates/account/show.php +++ b/templates/account/show.php @@ -53,7 +53,7 @@