From 120f56fad7a91eeabee2abe68635dc75729a04da Mon Sep 17 00:00:00 2001 From: osaajani <> Date: Tue, 8 Jun 2021 02:00:48 +0200 Subject: [PATCH 01/16] First step of quota and using daemon --- controllers/internals/.Mailer.php.swp | Bin 0 -> 16384 bytes controllers/internals/Event.php | 15 ++ controllers/internals/Quota.php | 180 ++++++++++++++++++++ controllers/internals/Sended.php | 14 ++ daemons/.Quota.php.swp | Bin 0 -> 16384 bytes daemons/Quota.php | 159 +++++++++++++++++ db/migrations/20210607123506_add_quotas.php | 51 ++++++ models/Event.php | 26 +++ models/Quota.php | 96 +++++++++++ models/Webhook.php | 3 + templates/email/quota-usage-close.php | 4 + templates/email/quota-usage-full.php | 4 + 12 files changed, 552 insertions(+) create mode 100644 controllers/internals/.Mailer.php.swp create mode 100644 controllers/internals/Quota.php create mode 100644 daemons/.Quota.php.swp create mode 100644 daemons/Quota.php create mode 100644 db/migrations/20210607123506_add_quotas.php create mode 100644 models/Quota.php create mode 100644 templates/email/quota-usage-close.php create mode 100644 templates/email/quota-usage-full.php diff --git a/controllers/internals/.Mailer.php.swp b/controllers/internals/.Mailer.php.swp new file mode 100644 index 0000000000000000000000000000000000000000..4565396555bf5693cdcdc1dfe89ae861c3e05a5e GIT binary patch literal 16384 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 E0dOwy=Kufz literal 0 HcmV?d00001 diff --git a/controllers/internals/Event.php b/controllers/internals/Event.php index c4fc6c6..10b0720 100644 --- a/controllers/internals/Event.php +++ b/controllers/internals/Event.php @@ -55,6 +55,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 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/Quota.php b/controllers/internals/Quota.php new file mode 100644 index 0000000..bbd46b6 --- /dev/null +++ b/controllers/internals/Quota.php @@ -0,0 +1,180 @@ + + * + * 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 bool $report_unused : Should unused credits be re-credited + * @param bool $report_unused_additional : Should unused additional credits be re-credited + * @param \DateTime $start_date : Starting date for the quota + * @param ?\DateTime $expiration_date (optional) : Ending date for the quota + * @param bool $auto_renew (optional) : Should the quota be automatically renewed after expiration_date + * @param ?\DateInterval $renew_interval (optional) : Period to use for setting expiration_date on renewal + * @param int $additional (optional) : Additionals credits + * + * @return mixed bool|int : False if cannot create smsstop, id of the new smsstop else + */ + public function create(int $id_user, int $credit, bool $report_unused, bool $report_unused_additional, \DateTime $start_date, ?\DateTime $expiration_date = null, bool $auto_renew= false, ?\DateInterval $renew_interval = null, int $additional = 0) + { + $quota = [ + 'id_user' => $id_user, + 'credit' => $credit, + 'report_unused' => $report_unused, + 'report_unused_additional' => $report_unused_additional, + 'start_date' => $start_date, + 'expiration_date' => $expiration_date, + '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 : Id of the quota to update + * @param int $credit : Credit for this quota + * @param bool $report_unused : Should unused credits be re-credited + * @param bool $report_unused_additional : Should unused additional credits be re-credited + * @param \DateTime $start_date : Starting date for the quota + * @param ?\DateTime $expiration_date (optional) : Ending date for the quota + * @param bool $auto_renew (optional) : Should the quota be automatically renewed after expiration_date + * @param ?\DateInterval $renew_interval (optional) : Period to use for setting expiration_date on renewal + * @param int $additional (optional) : Additionals credits + * @param int $consumed (optional) : Number of consumed credits + * + * @return mixed bool|int : False if cannot create smsstop, id of the new smsstop else + */ + public function update_for_user(int $id_user, int $id_quota, int $credit, bool $report_unused, bool $report_unused_additional, \DateTime $start_date, ?\DateTime $expiration_date = null, bool $auto_renew= false, ?\DateInterval $renew_interval = null, int $additional = 0, int $consumed = 0) + { + $quota = [ + 'id_user' => $id_user, + 'id_quota' => $id_quota, + 'credit' => $credit, + 'report_unused' => $report_unused, + 'report_unused_additional' => $report_unused_additional, + 'start_date' => $start_date, + 'expiration_date' => $expiration_date, + 'auto_renew' => $auto_renew, + 'renew_interval' => $renew_interval, + 'additional' => $additional, + 'consumed' => $consumed, + ]; + + return $this->get_model()->insert($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); + + //Enqueue verifications for quotas alerting + $queue = msg_get_queue(QUEUE_ID_QUOTA); + $message = ['id_user' => $id_user]; + msg_send($queue, QUEUE_TYPE_QUOTA, $message, true, true); + + 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 = array( + '@','Δ',' ','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($utf8_string, $i, 1), $gsm0338)) + { + $is_gsm0338 = false; + break; + } + } + + return ($is_gsm0338 ? ceil($len / 160) : ceil($len / 70)); + } + + /** + * 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/Sended.php b/controllers/internals/Sended.php index 2e24327..e2eaaab 100644 --- a/controllers/internals/Sended.php +++ b/controllers/internals/Sended.php @@ -219,6 +219,18 @@ 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 +265,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/daemons/.Quota.php.swp b/daemons/.Quota.php.swp new file mode 100644 index 0000000000000000000000000000000000000000..ddb6410f41e93ba5d7b382f241ae66ab18e576d1 GIT binary patch literal 16384 zcmeHNU2Ggz6}~N{g_4j^UP>R(b=hXume=c$5)vnNv#~dhB|F}DZIUVu!|d$c-AQI= zHgjj}mv~Naat6N<|b2c;KPH6RHH2KuGYNduMiM zcGpg!eJIk5^l4}A@44SO_vf6u_34i+t?*-0hZ&xOj5*)@!N)#&Z}pB_JB*oEOxLFL z+dQgV@nW@BJba{h{K)k5F6B$1^h>gB`l95UvTMtZEHzBg@myItA9{f~)opioshcHd z3}g(v0S3aLSvb(=sW&>TlD>K^m}a}Kl;pmG6pgRG6pgRG6pgRG6pgRG6pgR zG6pgRG6pgR-hvF64aOdTT(6O%e~_`*|NrL!#{L1k2D}8k2z(j%5>Nvk0>%Ii><9J% ze|a}!zXo0ceg%9Vcnb)oj`~tWIJOSJQJm5R;WNZhx3fu+! z{vC|{4)_u9J>a{*Q^4l{3#bEUff?X9@bW#3{Ra3fFasP1-UIyMZg2p61^7I$0n7je z-~r%$z~A1^*ek%}Knr*jxCrb6zI7L4*MTcQ6*vu)fxjXUa})SC0ye(|ehz#axCwj> zr~#zQzsG@_kv7K@PcCTNvN-2>t(NeMbGF;`3i);4rcZ7FDCcxt0i`nnr!c2zI){z(|Y+BsM^i5lZ2w_)sbN%ac5Req(jL zY;4rZixp#jd9_x_PpTnqE}-(d-AVE&1Je&wtHXQ_$OI{gs7)fl_Z(0yH)E;gW`#ys zi1b@;G=_Au)C9+1ys-n7M7mbgaA~`ih;-hUXOahP;c~m>dOnmyntij>lMBg+jIQu4 z2#r^MlIN7o8Cqq^f(8^eR41u4Raq--u-qsm4Up3(S{weJM4zX*ktRB2S+Cb>Va|Ny zbkvi`mb_t0v+iID{qIw9cxsG|BKkH17#uNOT_3HfJnG}o1PQSy} z&|j8Q1*veZX=Bxlwhb6TL?~s&Pg--N^RalJ8Ba6+X7@}I4De<7c3@kkQ^;3*A3I^6 zr)!}I1?_4LnA0Q|Dw}r2XrIif6oXO*lssyHGPEr0rTow#zEWE>D(6=5!^dV}Qi*n0 z%#o5bt~1L!vSs+f680AM61}DCX`+Oj>y(Q3s|n-OLVs?a7zZ{~Gre_Xxk z-5e{uAL5$?S*p$(=TWG*z+5$9isJ|81m&@3%$! zSm-!|vns9|=ca}p$&9KGPai)zJ$)k8c&RjRI!@esFRRb{cH4FY7K5ZQ_Gf=88C?(? z3F#t&v8N($vmvB~EnCRV6=>R-nSsV#S6D$|p}bx`U9MG(v#T|**ZVwIUa2HLH)@r& z#Amf!t6f-KTfn(tWBtsrd}KRX_HI~rY>PKT*9vUU<$mZEqB+r@3tjxj)(vK6)Ye@< z{T=~vjzKa?3=2}Kr4zclh{ z)mvU&tl@B+kN?v=g{s(?mes*}W@e#sdSfwpK92ZCuhgC{rgf1ZROtpq@@w>u>Do$- zFQ|V^bH!X<3p&~|k}8fYd!nQVJ;6_b8MDJp-#2%l@V4jDqE+VlEucdNhY9&RQH8i( zPraT$rP4|h6I#7p-`=9Ldk(Wgi`7eUOc}%rzF`Ifxrt@!1bd8GC@uByxVRn&w;_4C z4pTL)K#!eq8plLbMw=K{)uxyd2OBS6@mvoI*wnn9V9B&dYnv$@Vv^7iLUv7@e08Ie zCXViF+YVq*@RVRnAxqIYlj?=_D_9fADrsgc zR+o!MrlvV2vv47A&^EDTN?yk@i31*5p@s_9IIr4RC`EA@ot>t2rDNLgzxc_nBkJ3t zE=_r=g*0c%@;Y--JtaWgGgKXLugPN*B9<6C`3U{np8Wsk;KzOyzAyRz@%?@Xe?9%%8wgJ5aD7erV{o?v-c~*eoT(iZs*j4( z$(DSR*n7}%{G9gV_Vh1xdxnQIs~`J)2%)JChy2db!ol6@B0$1da7e|SLUdqPCu(?J zN=$^-J3II&kb|Us4bG#OM#tL{hKgdufekwCLK>Bp^p$kj1E>DPO~$p!dF}gHP**)*VlS0Wg+SkH;2ziymp7<_|;6zSBY4`W_ z6B4zS@R;BlQsjrOCNxF8v8gCeYHWxUa&pDus0fG00dJ%i2;c!OB5pTGNIpEuP&z_% z3rDBnvK*dPPpW4fY*b=PjvEGlhoh_%=(-{M`tDmfls_1Ti!40~l1cc5#2|S?4n;_m z+*FO8bVFKYI!8y4hi@BZ5xP3ukN;5NQVI7XO@yWRUX4>p3?sUOV)U7O2K28&UWNR; z>C#*kuC_CE!_gZ}BEYCFE~erDP~7hDc*b&3ATT|GI2A^M5FgE%;#|~c0Tc-TQeN|i zD=IY=bZ9VnPc_Le<)cfPyb6;K-lbelD4GO}I*OtiyWC9al^_+EqPl6dQA(d#`bs_n z#A@q^c%_&gr}1&M$Y71x``$9Tr%MKBsH;&tQ>z0ctByv-{dc)%VySV&_jU0tqrL(i mRdttp#Qa)xtx8w3q}PFqG&Lp1Y4v}Iq5u>_QZrH6)qen{dRP4b literal 0 HcmV?d00001 diff --git a/daemons/Quota.php b/daemons/Quota.php new file mode 100644 index 0000000..e85131f --- /dev/null +++ b/daemons/Quota.php @@ -0,0 +1,159 @@ + + * + * This source file is subject to the GPL-3.0 license that is bundled + * with this source code in the file LICENSE. + */ + +namespace daemons; + +use Monolog\Handler\StreamHandler; +use Monolog\Logger; + +/** + * Quota daemon class. + */ +class Quota extends AbstractDaemon +{ + private $quota_queue; + private $last_message_at; + private $bdd; + + /** + * Constructor. + * + * @param array $phone : A phone table entry + */ + public function __construct() + { + $name = 'RaspiSMS Daemon Quota'; + $logger = new Logger($name); + $logger->pushHandler(new StreamHandler(PWD_LOGS . '/daemons.log', Logger::DEBUG)); + $pid_dir = PWD_PID; + $no_parent = false; //Rattach to parent so parent can stop it + $additional_signals = []; + $uniq = true; //Quota should be uniq + + //Construct the daemon + parent::__construct($name, $logger, $pid_dir, $no_parent, $additional_signals, $uniq); + + parent::start(); + } + + public function run() + { + $this->bdd = \descartes\Model::_connect(DATABASE_HOST, DATABASE_NAME, DATABASE_USER, DATABASE_PASSWORD, 'UTF8'); + + $find_message = true; + while ($find_message) + { + //Call message + $maxsize = 409600; + $message = null; + + $error_code = null; + $success = msg_receive($this->quota_queue, QUEUE_TYPE_QUOTA, $msgtype, $maxsize, $message, true, MSG_IPC_NOWAIT, $error_code); //MSG_IPC_NOWAIT == dont wait if no message found + if (!$success && MSG_ENOMSG !== $error_code) + { + $this->logger->critical('Error for quota queue reading, error code : ' . $error_code); + $find_message = false; + + continue; + } + + if (!$message) + { + $find_message = false; + + continue; + } + + $this->logger->info('Check alert level for quota : ' . json_encode($message['id'])); + + $internal_settings = new \controllers\internals\Setting($this->bdd); + $settings = $internal_user->gets_for_user($message['id_user']); + + $quota_alert_level = false; + foreach ($settings as $name => $value) + { + if ('quota_alert_level', $name) + { + $quota_alert_level = (float) $value; + break; + } + } + + if (!$quota_alert_level) + { + $this->logger->info('Alert is disabled for quota : ' . json_encode($message['id'])); + continue; + } + + $internal_quota = new \controllers\internals\Quota($this->bdd); + $usage_percentage = $internal_quota->get_usage_percentage($message['id_user']); + if ($usage_percentage < $quota_alert_level) + { + continue; + } + + //If already an alert event since quota start_date, then ignore alert + $internal_event = new \controllers\internals\Event($this->bdd); + $alert_events = $internal_event->get_events_by_type_and_date_for_user($message['id_user'], 'QUOTA_USAGE_CLOSE', new \DateTime($message['start_date'])); + if (count($alert_events)) + { + continue; + } + + //Alert level reached and no previous alert, we create a new alert + $this->logger->info('Trigger alert for quota : ' . json_encode($message['id'])); + $internal_event->create($message['id_user'], 'QUOTA_USAGE_CLOSE', 'Reached ' . ($usage_percentage * 100) . '% of SMS quota.'); + + $user = $internal_user->get($message['id_user']); + if (!$user) + { + $this->logger->info('Cannot find user with id : ' . json_encode($message['id_user'])); + continue; + } + + $mailer = new \controllers\internals\Mailer(); + $success = $mailer->enqueue($user['email'], EMAIL_QUOTA_USAGE_CLOSE, ['percent' => $usage_percentage]); + if (!$success) + { + $this->logger->error('Cannot enqueue alerting email for quota usage.'); + + continue; + } + + $this->logger->info('Success sending email'); + } + + //Check quotas every 60 seconds + usleep(60 * 1000000); + } + + public function on_start() + { + //Set last message at to construct time + $this->quota_queue = msg_get_queue(QUEUE_ID_QUOTA); + + $this->logger->info('Starting Quota daemon with pid ' . getmypid()); + } + + public function on_stop() + { + //Delete queue on daemon close + $this->logger->info('Closing queue : ' . QUEUE_ID_EMAIL); + msg_remove_queue($this->mailer_queue); + + $this->logger->info('Stopping Mailer daemon with pid ' . getmypid()); + } + + public function handle_other_signals($signal) + { + $this->logger->info('Signal not handled by ' . $this->name . ' Daemon : ' . $signal); + } +} diff --git a/db/migrations/20210607123506_add_quotas.php b/db/migrations/20210607123506_add_quotas.php new file mode 100644 index 0000000..6c45e87 --- /dev/null +++ b/db/migrations/20210607123506_add_quotas.php @@ -0,0 +1,51 @@ +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' => true, 'default' => NULL]) + ->addColumn('start_date', 'datetime', ['null' => false]) + ->addColumn('expiration_date', 'datetime', ['null' => true]) + ->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']) + ->create(); + + } +} diff --git a/models/Event.php b/models/Event.php index ef73445..c5d77b1 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 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 ($until !== null) + { + $where['<=at' => $until->format('Y-m-d H:i:s')]; + } + + return $this->_select('event', $where, 'at'); + } + /** * Return table name. */ diff --git a/models/Quota.php b/models/Quota.php new file mode 100644 index 0000000..cd15e00 --- /dev/null +++ b/models/Quota.php @@ -0,0 +1,96 @@ + + * + * 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 + { + /** + * 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 end_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): int + { + $query = ' + SELECT (consumed / (credit + additional)) AS usage_percentage + FROM quota + WHERE id_user = :id_user + AND start_date <= :at + AND end_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]['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); + } + + /** + * Return table name. + */ + protected function get_table_name(): string + { + return 'quota'; + } + } diff --git a/models/Webhook.php b/models/Webhook.php index 48d4998..43dd72e 100644 --- a/models/Webhook.php +++ b/models/Webhook.php @@ -16,6 +16,9 @@ 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/templates/email/quota-usage-close.php b/templates/email/quota-usage-close.php new file mode 100644 index 0000000..d4b57fa --- /dev/null +++ b/templates/email/quota-usage-close.php @@ -0,0 +1,4 @@ +Vous avez atteint % de votre quota de SMS. + +-------------------------------------------------------------------------------------------- +Pour plus d'informations sur le système RaspiSMS, rendez-vous sur le site https://raspisms.fr diff --git a/templates/email/quota-usage-full.php b/templates/email/quota-usage-full.php new file mode 100644 index 0000000..4d373a9 --- /dev/null +++ b/templates/email/quota-usage-full.php @@ -0,0 +1,4 @@ +Vous avez épuisé votre quota de SMS, vous ne pourrez plus envoyer de SMS tant que votre quota de SMS n'aura pas été augmenté ou remis à zéro. + +-------------------------------------------------------------------------------------------- +Pour plus d'informations sur le système RaspiSMS, rendez-vous sur le site https://raspisms.fr From cb38447feb27262c0ac0f88603438cd1623d7948 Mon Sep 17 00:00:00 2001 From: osaajani <> Date: Tue, 8 Jun 2021 21:01:26 +0200 Subject: [PATCH 02/16] Pass quota limit as a console method --- controllers/internals/.Mailer.php.swp | Bin 16384 -> 0 bytes controllers/internals/Console.php | 10 ++ controllers/internals/Event.php | 4 +- controllers/internals/Quota.php | 63 ++++++- daemons/.Quota.php.swp | Bin 16384 -> 0 bytes daemons/Quota.php | 159 ------------------ models/Event.php | 4 +- models/Quota.php | 71 ++++++++ ...-usage-close.php => quota-limit-close.php} | 0 ...usage-full.php => quota-limit-reached.php} | 1 + 10 files changed, 148 insertions(+), 164 deletions(-) delete mode 100644 controllers/internals/.Mailer.php.swp delete mode 100644 daemons/.Quota.php.swp delete mode 100644 daemons/Quota.php rename templates/email/{quota-usage-close.php => quota-limit-close.php} (100%) rename templates/email/{quota-usage-full.php => quota-limit-reached.php} (73%) diff --git a/controllers/internals/.Mailer.php.swp b/controllers/internals/.Mailer.php.swp deleted file mode 100644 index 4565396555bf5693cdcdc1dfe89ae861c3e05a5e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16384 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 E0dOwy=Kufz diff --git a/controllers/internals/Console.php b/controllers/internals/Console.php index f2f3986..eb58087 100644 --- a/controllers/internals/Console.php +++ b/controllers/internals/Console.php @@ -170,4 +170,14 @@ namespace controllers\internals; echo ($success === false ? '[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(); + } } diff --git a/controllers/internals/Event.php b/controllers/internals/Event.php index 10b0720..2a04688 100644 --- a/controllers/internals/Event.php +++ b/controllers/internals/Event.php @@ -66,9 +66,9 @@ namespace controllers\internals; * * @return array */ - public get_events_by_type_and_date_for_user (int $id_user, string $type, \DateTime $since, ?\DateTime $until = null) + 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); + $this->get_model()->get_events_by_type_and_date_for_user ($id_user, $type, $since, $until); } /** diff --git a/controllers/internals/Quota.php b/controllers/internals/Quota.php index bbd46b6..10bd832 100644 --- a/controllers/internals/Quota.php +++ b/controllers/internals/Quota.php @@ -158,7 +158,7 @@ class Quota extends StandardController $len = mb_strlen($text); for ($i = 0; $i < $len; $i++) { - if (!in_array(mb_substr($utf8_string, $i, 1), $gsm0338)) + if (!in_array(mb_substr($text, $i, 1), $gsm0338)) { $is_gsm0338 = false; break; @@ -168,6 +168,67 @@ class Quota extends StandardController 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.'); + } + } + /** * Get the model for the Controller. */ diff --git a/daemons/.Quota.php.swp b/daemons/.Quota.php.swp deleted file mode 100644 index ddb6410f41e93ba5d7b382f241ae66ab18e576d1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16384 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 diff --git a/daemons/Quota.php b/daemons/Quota.php deleted file mode 100644 index e85131f..0000000 --- a/daemons/Quota.php +++ /dev/null @@ -1,159 +0,0 @@ - - * - * This source file is subject to the GPL-3.0 license that is bundled - * with this source code in the file LICENSE. - */ - -namespace daemons; - -use Monolog\Handler\StreamHandler; -use Monolog\Logger; - -/** - * Quota daemon class. - */ -class Quota extends AbstractDaemon -{ - private $quota_queue; - private $last_message_at; - private $bdd; - - /** - * Constructor. - * - * @param array $phone : A phone table entry - */ - public function __construct() - { - $name = 'RaspiSMS Daemon Quota'; - $logger = new Logger($name); - $logger->pushHandler(new StreamHandler(PWD_LOGS . '/daemons.log', Logger::DEBUG)); - $pid_dir = PWD_PID; - $no_parent = false; //Rattach to parent so parent can stop it - $additional_signals = []; - $uniq = true; //Quota should be uniq - - //Construct the daemon - parent::__construct($name, $logger, $pid_dir, $no_parent, $additional_signals, $uniq); - - parent::start(); - } - - public function run() - { - $this->bdd = \descartes\Model::_connect(DATABASE_HOST, DATABASE_NAME, DATABASE_USER, DATABASE_PASSWORD, 'UTF8'); - - $find_message = true; - while ($find_message) - { - //Call message - $maxsize = 409600; - $message = null; - - $error_code = null; - $success = msg_receive($this->quota_queue, QUEUE_TYPE_QUOTA, $msgtype, $maxsize, $message, true, MSG_IPC_NOWAIT, $error_code); //MSG_IPC_NOWAIT == dont wait if no message found - if (!$success && MSG_ENOMSG !== $error_code) - { - $this->logger->critical('Error for quota queue reading, error code : ' . $error_code); - $find_message = false; - - continue; - } - - if (!$message) - { - $find_message = false; - - continue; - } - - $this->logger->info('Check alert level for quota : ' . json_encode($message['id'])); - - $internal_settings = new \controllers\internals\Setting($this->bdd); - $settings = $internal_user->gets_for_user($message['id_user']); - - $quota_alert_level = false; - foreach ($settings as $name => $value) - { - if ('quota_alert_level', $name) - { - $quota_alert_level = (float) $value; - break; - } - } - - if (!$quota_alert_level) - { - $this->logger->info('Alert is disabled for quota : ' . json_encode($message['id'])); - continue; - } - - $internal_quota = new \controllers\internals\Quota($this->bdd); - $usage_percentage = $internal_quota->get_usage_percentage($message['id_user']); - if ($usage_percentage < $quota_alert_level) - { - continue; - } - - //If already an alert event since quota start_date, then ignore alert - $internal_event = new \controllers\internals\Event($this->bdd); - $alert_events = $internal_event->get_events_by_type_and_date_for_user($message['id_user'], 'QUOTA_USAGE_CLOSE', new \DateTime($message['start_date'])); - if (count($alert_events)) - { - continue; - } - - //Alert level reached and no previous alert, we create a new alert - $this->logger->info('Trigger alert for quota : ' . json_encode($message['id'])); - $internal_event->create($message['id_user'], 'QUOTA_USAGE_CLOSE', 'Reached ' . ($usage_percentage * 100) . '% of SMS quota.'); - - $user = $internal_user->get($message['id_user']); - if (!$user) - { - $this->logger->info('Cannot find user with id : ' . json_encode($message['id_user'])); - continue; - } - - $mailer = new \controllers\internals\Mailer(); - $success = $mailer->enqueue($user['email'], EMAIL_QUOTA_USAGE_CLOSE, ['percent' => $usage_percentage]); - if (!$success) - { - $this->logger->error('Cannot enqueue alerting email for quota usage.'); - - continue; - } - - $this->logger->info('Success sending email'); - } - - //Check quotas every 60 seconds - usleep(60 * 1000000); - } - - public function on_start() - { - //Set last message at to construct time - $this->quota_queue = msg_get_queue(QUEUE_ID_QUOTA); - - $this->logger->info('Starting Quota daemon with pid ' . getmypid()); - } - - public function on_stop() - { - //Delete queue on daemon close - $this->logger->info('Closing queue : ' . QUEUE_ID_EMAIL); - msg_remove_queue($this->mailer_queue); - - $this->logger->info('Stopping Mailer daemon with pid ' . getmypid()); - } - - public function handle_other_signals($signal) - { - $this->logger->info('Signal not handled by ' . $this->name . ' Daemon : ' . $signal); - } -} diff --git a/models/Event.php b/models/Event.php index c5d77b1..10f8d7e 100644 --- a/models/Event.php +++ b/models/Event.php @@ -36,7 +36,7 @@ namespace models; * * @return array */ - public get_events_by_type_and_date_for_user (int $id_user, string $type, \DateTime $since, ?\DateTime $until = null) + 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, @@ -46,7 +46,7 @@ namespace models; if ($until !== null) { - $where['<=at' => $until->format('Y-m-d H:i:s')]; + $where['<=at'] = $until->format('Y-m-d H:i:s'); } return $this->_select('event', $where, 'at'); diff --git a/models/Quota.php b/models/Quota.php index cd15e00..1091240 100644 --- a/models/Quota.php +++ b/models/Quota.php @@ -86,6 +86,77 @@ namespace models; 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 + * @return array + */ + 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 + * @return array + */ + 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); + } + /** * Return table name. */ diff --git a/templates/email/quota-usage-close.php b/templates/email/quota-limit-close.php similarity index 100% rename from templates/email/quota-usage-close.php rename to templates/email/quota-limit-close.php diff --git a/templates/email/quota-usage-full.php b/templates/email/quota-limit-reached.php similarity index 73% rename from templates/email/quota-usage-full.php rename to templates/email/quota-limit-reached.php index 4d373a9..9fa6348 100644 --- a/templates/email/quota-usage-full.php +++ b/templates/email/quota-limit-reached.php @@ -1,4 +1,5 @@ Vous avez épuisé votre quota de SMS, vous ne pourrez plus envoyer de SMS tant que votre quota de SMS n'aura pas été augmenté ou remis à zéro. +Votre quota sera remis à zéro le s($expiration_date); ?>. -------------------------------------------------------------------------------------------- Pour plus d'informations sur le système RaspiSMS, rendez-vous sur le site https://raspisms.fr From 8d6236113e2eed19c0f7fc5bfbfcdcaa7c56f213 Mon Sep 17 00:00:00 2001 From: osaajani <> Date: Wed, 9 Jun 2021 01:20:42 +0200 Subject: [PATCH 03/16] add setting --- templates/setting/show.php | 43 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/templates/setting/show.php b/templates/setting/show.php index 80f04b0..e075efd 100644 --- a/templates/setting/show.php +++ b/templates/setting/show.php @@ -143,6 +143,25 @@ +
+
+

Alerte limite de SMS atteinte

+
+
+
+
+ + +
+
+ +
+
+
+

Affichage de l'aide

@@ -275,6 +294,30 @@
+
+
+

Alerte limite de SMS proche

+
+
+
+
+ + +
+
+ +
+
+
+
From 9b7fb75eecd147e149e78c5e6e161286c1a85d1e Mon Sep 17 00:00:00 2001 From: osaajani <> Date: Wed, 9 Jun 2021 01:22:11 +0200 Subject: [PATCH 04/16] add email settings for quota alerting and user settings for quota alerting --- env.php.dist | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/env.php.dist b/env.php.dist index 5aa3260..2a602e8 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,8 @@ 'default_phone_country' => 'fr', 'authorized_phone_country' => 'fr,be,ca', 'mms' => 1, + 'alert_quota_limit_reached' => 1, + 'alert_quota_limit_close' => 0.9, ], ]; From 4c47de3fc5bf1fa51bdbda388864219d4f20582c Mon Sep 17 00:00:00 2001 From: osaajani <> Date: Thu, 10 Jun 2021 01:31:53 +0200 Subject: [PATCH 05/16] add quota renewal --- controllers/internals/Console.php | 10 +++++ controllers/internals/Quota.php | 62 ++++++++++++++++++++++++++++--- models/Quota.php | 19 ++++++++++ 3 files changed, 85 insertions(+), 6 deletions(-) diff --git a/controllers/internals/Console.php b/controllers/internals/Console.php index eb58087..7440ee8 100644 --- a/controllers/internals/Console.php +++ b/controllers/internals/Console.php @@ -180,4 +180,14 @@ namespace controllers\internals; $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/Quota.php b/controllers/internals/Quota.php index 10bd832..046d5c7 100644 --- a/controllers/internals/Quota.php +++ b/controllers/internals/Quota.php @@ -59,21 +59,21 @@ class Quota extends StandardController * @param \DateTime $start_date : Starting date for the quota * @param ?\DateTime $expiration_date (optional) : Ending date for the quota * @param bool $auto_renew (optional) : Should the quota be automatically renewed after expiration_date - * @param ?\DateInterval $renew_interval (optional) : Period to use for setting expiration_date on renewal + * @param ?string $renew_interval (optional) : Period to use for setting expiration_date on renewal * @param int $additional (optional) : Additionals credits * @param int $consumed (optional) : Number of consumed credits * * @return mixed bool|int : False if cannot create smsstop, id of the new smsstop else */ - public function update_for_user(int $id_user, int $id_quota, int $credit, bool $report_unused, bool $report_unused_additional, \DateTime $start_date, ?\DateTime $expiration_date = null, bool $auto_renew= false, ?\DateInterval $renew_interval = null, int $additional = 0, int $consumed = 0) + public function update_for_user(int $id_user, int $id_quota, int $credit, bool $report_unused, bool $report_unused_additional, \DateTime $start_date, ?\DateTime $expiration_date = null, bool $auto_renew= false, ?string $renew_interval = null, int $additional = 0, int $consumed = 0) { + $expiration_date = $expiration_date === null ? $expiration_date : $expiration_date->format('Y-m-d H:i:s'); + $quota = [ - 'id_user' => $id_user, - 'id_quota' => $id_quota, 'credit' => $credit, 'report_unused' => $report_unused, 'report_unused_additional' => $report_unused_additional, - 'start_date' => $start_date, + 'start_date' => $start_date->format('Y-m-d H:i:s'), 'expiration_date' => $expiration_date, 'auto_renew' => $auto_renew, 'renew_interval' => $renew_interval, @@ -81,7 +81,7 @@ class Quota extends StandardController 'consumed' => $consumed, ]; - return $this->get_model()->insert($quota); + return $this->get_model()->update_for_user($id_user, $id_quota, $quota); } /** @@ -229,6 +229,56 @@ class Quota extends StandardController } } + /** + * 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; + } + + $success = $this->update_for_user($user['id'], $quota['id'], $quota['credit'], $quota['report_unused'], $quota['report_unused_additional'], $new_start_date, $new_expiration_date, $quota['auto_renew'], $quota['renew_interval'], $report, 0); + + 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 ' . $quota['id'] . ' report ' . $report . ' credits.'); + } + } + /** * Get the model for the Controller. */ diff --git a/models/Quota.php b/models/Quota.php index 1091240..2b3bc97 100644 --- a/models/Quota.php +++ b/models/Quota.php @@ -89,6 +89,7 @@ namespace models; /** * 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 * @return array */ public function get_quotas_for_limit_close() : array @@ -124,6 +125,7 @@ namespace models; /** * 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 * @return array */ public function get_quotas_for_limit_reached() : array @@ -157,6 +159,23 @@ namespace models; 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 + * @return array + */ + public function get_quotas_to_be_renewed (\DateTime $at): array + { + $at = $at->format('Y-m-d H:i:s'); + $where = [ + '!=expiration_date' => null, + '<=expiration_date' => $at, + 'auto_renew' => true, + ]; + + return $this->_select('quota', $where); + } + /** * Return table name. */ From f9e0312c890a3d9b18c6fb620a79f44881ea39ed Mon Sep 17 00:00:00 2001 From: osaajani <> Date: Fri, 11 Jun 2021 02:16:41 +0200 Subject: [PATCH 06/16] update template add user to add quota, still need to show/hidde quota based on enable or not and impement backend processing --- controllers/publics/User.php | 9 ++- templates/user/add.php | 120 ++++++++++++++++++++++++++++++++--- 2 files changed, 119 insertions(+), 10 deletions(-) diff --git a/controllers/publics/User.php b/controllers/publics/User.php index 2b22a6a..97f0d13 100644 --- a/controllers/publics/User.php +++ b/controllers/publics/User.php @@ -129,7 +129,14 @@ class User extends \descartes\Controller */ public function add() { - return $this->render('user/add'); + $now = new \DateTime(); + $now_plus_one_month = clone $now; + $now_plus_one_month->add(new \DateInterval('P1M')); + + $now = $now->format('Y-m-d H:i:00'); + $now_plus_one_month = $now_plus_one_month->format('Y-m-d H:i:00'); + + return $this->render('user/add', ['now' => $now, 'now_plus_one_month' => $now_plus_one_month]); } /** diff --git a/templates/user/add.php b/templates/user/add.php index 974f243..9125c85 100644 --- a/templates/user/add.php +++ b/templates/user/add.php @@ -52,15 +52,105 @@ - -
- -
- /> Oui - /> Non -
-
- +
+ +
+ /> Oui + /> Non +
+
+
+ Quota de SMS + +
+ +

+ Définir un quota pour un utilisateur vous permet de choisir combien de SMS cet utilisateur pourras envoyer sur une période donnée. +

+ +
+ /> Oui + /> Non +
+
+ +
+ + +
+
+ +

+ SMS venants s'ajouter au crédit de base. Vous pouvez par exemple utiliser des SMS additionels pour augmenter temporairement la limite de SMS d'un utilisateur. +

+ +
+ +
+ + +
+ +
+ +

+ Sur quelle durée le quota doit-il s'appliqué. Une fois cette durée passée, le quota sera soit désactivé soit renouvelé automatiquement. +

+
+ +
+
+ +
+ +

+ Si activé, le crédit consommé sera automatiquement remis à zéro et le quota renouvelé pour la même durée à chaque fois qu'il arrivera à sa fin. +

+
+ /> Oui + /> Non +
+
+ +
+ +

+ Si activé, les SMS non consommés serons reportés au mois suivant sous la forme de crédit additionel. Sinon, les SMS non utilisés seront simplement perdus. +

+
+ /> Oui + /> Non +
+
+ +
+ +

+ Si activé, les SMS additionels non consommés serons reportés au mois suivant sous la forme de crédit additionel. Sinon, les SMS additionels non utilisés seront simplement perdus. +

+
+ /> Oui + /> Non +
+
+
+ Annuler @@ -71,5 +161,17 @@ + render('incs/footer'); From 4a39865903c3fef1079fe9b2f38db58d8cba7032 Mon Sep 17 00:00:00 2001 From: osaajani <> Date: Sat, 12 Jun 2021 23:23:15 +0200 Subject: [PATCH 07/16] add capacity to modify user --- controllers/internals/Console.php | 11 +- controllers/internals/Quota.php | 67 +++--- controllers/internals/Tool.php | 21 ++ controllers/internals/User.php | 106 ++++++++-- controllers/publics/User.php | 222 ++++++++++++++++++-- db/migrations/20210607123506_add_quotas.php | 5 +- models/Quota.php | 13 +- models/User.php | 26 +++ routes.php | 2 + templates/user/add.php | 150 +++++++------ templates/user/edit.php | 202 ++++++++++++++++++ templates/user/list.php | 5 +- 12 files changed, 687 insertions(+), 143 deletions(-) create mode 100644 templates/user/edit.php diff --git a/controllers/internals/Console.php b/controllers/internals/Console.php index 7440ee8..adf8663 100644 --- a/controllers/internals/Console.php +++ b/controllers/internals/Console.php @@ -103,7 +103,16 @@ 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); diff --git a/controllers/internals/Quota.php b/controllers/internals/Quota.php index 046d5c7..a9b60c9 100644 --- a/controllers/internals/Quota.php +++ b/controllers/internals/Quota.php @@ -20,25 +20,25 @@ class Quota extends StandardController * * @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 (optional) : Ending date for the quota - * @param bool $auto_renew (optional) : Should the quota be automatically renewed after expiration_date - * @param ?\DateInterval $renew_interval (optional) : Period to use for setting expiration_date on renewal - * @param int $additional (optional) : Additionals credits + * @param \DateTime $expiration_date : Ending date for the quota * - * @return mixed bool|int : False if cannot create smsstop, id of the new smsstop else + * @return mixed bool|int : False if cannot create quota, id of the new quota else */ - public function create(int $id_user, int $credit, bool $report_unused, bool $report_unused_additional, \DateTime $start_date, ?\DateTime $expiration_date = null, bool $auto_renew= false, ?\DateInterval $renew_interval = null, int $additional = 0) + 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, - 'expiration_date' => $expiration_date, + '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, @@ -52,35 +52,13 @@ class Quota extends StandardController * * * @param int $id_user : User id - * @param int $id_quota : Id of the quota to update - * @param int $credit : Credit for this quota - * @param bool $report_unused : Should unused credits be re-credited - * @param bool $report_unused_additional : Should unused additional credits be re-credited - * @param \DateTime $start_date : Starting date for the quota - * @param ?\DateTime $expiration_date (optional) : Ending date for the quota - * @param bool $auto_renew (optional) : Should the quota be automatically renewed after expiration_date - * @param ?string $renew_interval (optional) : Period to use for setting expiration_date on renewal - * @param int $additional (optional) : Additionals credits - * @param int $consumed (optional) : Number of consumed credits + * @param int $id_quota : Quota to update id + * @param array $quota : Fields to update whith new values * - * @return mixed bool|int : False if cannot create smsstop, id of the new smsstop else + * @return int : number of updated lines */ - public function update_for_user(int $id_user, int $id_quota, int $credit, bool $report_unused, bool $report_unused_additional, \DateTime $start_date, ?\DateTime $expiration_date = null, bool $auto_renew= false, ?string $renew_interval = null, int $additional = 0, int $consumed = 0) + public function update_for_user(int $id_user, $id_quota, array $quota) { - $expiration_date = $expiration_date === null ? $expiration_date : $expiration_date->format('Y-m-d H:i:s'); - - $quota = [ - '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, - 'auto_renew' => $auto_renew, - 'renew_interval' => $renew_interval, - 'additional' => $additional, - 'consumed' => $consumed, - ]; - return $this->get_model()->update_for_user($id_user, $id_quota, $quota); } @@ -266,7 +244,14 @@ class Quota extends StandardController $report += $unused_additional; } - $success = $this->update_for_user($user['id'], $quota['id'], $quota['credit'], $quota['report_unused'], $quota['report_unused_additional'], $new_start_date, $new_expiration_date, $quota['auto_renew'], $quota['renew_interval'], $report, 0); + $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) { @@ -278,6 +263,18 @@ class Quota extends StandardController $internal_event->create($quota['id_user'], 'QUOTA_RENEWAL', 'Renew quota ' . $quota['id'] . ' 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. diff --git a/controllers/internals/Tool.php b/controllers/internals/Tool.php index a207185..4481777 100644 --- a/controllers/internals/Tool.php +++ b/controllers/internals/Tool.php @@ -144,6 +144,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. diff --git a/controllers/internals/User.php b/controllers/internals/User.php index 427cceb..11a57a9 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 && $quota === false) + { + $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,42 @@ 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); + $success = $this->internal_setting->create_defaults_for_user($new_id_user); if (!$success) { - $this->delete($new_id_user); + $this->bdd->rollback(); return false; } + + if ($quota !== null) + { + $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; } diff --git a/controllers/publics/User.php b/controllers/publics/User.php index 97f0d13..f0bf916 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(); @@ -58,7 +60,7 @@ 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 * @@ -82,7 +84,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 +96,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 +117,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); @@ -130,24 +132,27 @@ class User extends \descartes\Controller public function add() { $now = new \DateTime(); - $now_plus_one_month = clone $now; - $now_plus_one_month->add(new \DateInterval('P1M')); - $now = $now->format('Y-m-d H:i:00'); - $now_plus_one_month = $now_plus_one_month->format('Y-m-d H:i:00'); - return $this->render('user/add', ['now' => $now, 'now_plus_one_month' => $now_plus_one_month]); + 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 boolean $_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) { @@ -162,6 +167,15 @@ 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) { @@ -177,14 +191,49 @@ 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 ($quota_start_date === false || !\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 ($quota_renew_interval === false || !\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')); } + $mailer = new \controllers\internals\Mailer(); $email_send = $mailer->enqueue($email, EMAIL_CREATE_USER, ['email' => $email, 'password' => $password]); if (!$email_send) @@ -196,4 +245,145 @@ 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')); + } + + $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 ($quota_start_date === false || !\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 ($quota_renew_interval === false || !\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; + } + + $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; + } + + return $this->redirect(\descartes\Router::url('User', 'list')); + } + + + } } diff --git a/db/migrations/20210607123506_add_quotas.php b/db/migrations/20210607123506_add_quotas.php index 6c45e87..523b6b8 100644 --- a/db/migrations/20210607123506_add_quotas.php +++ b/db/migrations/20210607123506_add_quotas.php @@ -39,12 +39,13 @@ class AddQuotas extends AbstractMigration ->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' => true, 'default' => NULL]) + ->addColumn('renew_interval', 'string', ['null' => false, 'default' => NULL]) ->addColumn('start_date', 'datetime', ['null' => false]) - ->addColumn('expiration_date', 'datetime', ['null' => true]) + ->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/models/Quota.php b/models/Quota.php index 2b3bc97..2cefc04 100644 --- a/models/Quota.php +++ b/models/Quota.php @@ -13,6 +13,18 @@ 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 @@ -168,7 +180,6 @@ namespace models; { $at = $at->format('Y-m-d H:i:s'); $where = [ - '!=expiration_date' => null, '<=expiration_date' => $at, 'auto_renew' => true, ]; diff --git a/models/User.php b/models/User.php index f48274f..b738611 100644 --- a/models/User.php +++ b/models/User.php @@ -31,6 +31,32 @@ 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/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/user/add.php b/templates/user/add.php index 9125c85..ad6e5e6 100644 --- a/templates/user/add.php +++ b/templates/user/add.php @@ -49,7 +49,7 @@
- +
@@ -74,79 +74,81 @@
-
- - -
-
- -

- SMS venants s'ajouter au crédit de base. Vous pouvez par exemple utiliser des SMS additionels pour augmenter temporairement la limite de SMS d'un utilisateur. -

- -
+ @@ -94,7 +95,7 @@ jQuery(document).ready(function () { data: 'id', render: function (data, type, row, meta) { - return ''; + return ''; }, }, ], From 03ae69b82acc1ebe32ee8b9e482539e5e31a85c9 Mon Sep 17 00:00:00 2001 From: osaajani <> Date: Sun, 13 Jun 2021 00:14:54 +0200 Subject: [PATCH 08/16] add quota to user list --- controllers/publics/User.php | 7 +++++++ models/Quota.php | 6 +++--- templates/user/list.php | 31 +++++++++++++++++++------------ 3 files changed, 29 insertions(+), 15 deletions(-) diff --git a/controllers/publics/User.php b/controllers/publics/User.php index f0bf916..ad158e9 100644 --- a/controllers/publics/User.php +++ b/controllers/publics/User.php @@ -53,6 +53,13 @@ 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; + } + header('Content-Type: application/json'); echo json_encode(['data' => $entities]); } diff --git a/models/Quota.php b/models/Quota.php index 2cefc04..7d6f07c 100644 --- a/models/Quota.php +++ b/models/Quota.php @@ -58,14 +58,14 @@ namespace models; * @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): int + 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 end_date > :at'; + AND expiration_date > :at'; $params = [ 'id_user' => $id_user, @@ -74,7 +74,7 @@ namespace models; $result = $this->_run_query($query, $params); - return ($result[0]['usage_percentage'] ?? 0); + return (float) ($result[0]['usage_percentage'] ?? 0); } /** diff --git a/templates/user/list.php b/templates/user/list.php index 865ee5d..56a5de1 100644 --- a/templates/user/list.php +++ b/templates/user/list.php @@ -36,12 +36,13 @@
- +
+ @@ -49,17 +50,16 @@
Email Admin StatutCrédit utilisé
-
- -
- Action pour la séléction : - - - - -
+
+ +
+ Action pour la séléction : + + + +
@@ -92,6 +92,13 @@ jQuery(document).ready(function () {data: 'email', render: jQuery.fn.dataTable.render.text()}, {data: 'admin', render: jQuery.fn.dataTable.render.text()}, {data: 'status', render: jQuery.fn.dataTable.render.text()}, + { + data: 'quota_percentage', + render: function (data, type, row, meta) { + return jQuery.fn.dataTable.render.text().display(data) + "%"; + return ''; + }, + }, { data: 'id', render: function (data, type, row, meta) { From c3637ab3abe3ea8b50971adb6c1a93f340635685 Mon Sep 17 00:00:00 2001 From: osaajani <> Date: Mon, 14 Jun 2021 19:48:42 +0200 Subject: [PATCH 09/16] add stats about credit in use account and update dates for home graph --- controllers/internals/Quota.php | 9 +++--- controllers/internals/Sended.php | 2 +- controllers/publics/Account.php | 6 +++- controllers/publics/Dashboard.php | 47 ++++++++++++++++++++----------- controllers/publics/User.php | 28 ++++++++++++++++-- models/Quota.php | 2 +- templates/account/show.php | 17 ++++++++++- templates/user/edit.php | 3 ++ templates/user/list.php | 7 +++-- 9 files changed, 90 insertions(+), 31 deletions(-) diff --git a/controllers/internals/Quota.php b/controllers/internals/Quota.php index a9b60c9..54cf937 100644 --- a/controllers/internals/Quota.php +++ b/controllers/internals/Quota.php @@ -84,10 +84,9 @@ class Quota extends StandardController { $result = $this->get_model()->consume_credit($id_user, $quantity); - //Enqueue verifications for quotas alerting - $queue = msg_get_queue(QUEUE_ID_QUOTA); - $message = ['id_user' => $id_user]; - msg_send($queue, QUEUE_TYPE_QUOTA, $message, true, true); + //Write event + $internal_event = new Event($this->bdd); + $internal_event->create($id_user, 'QUOTA_CONSUME', 'Consume ' . $quantity . ' credits of SMS quota.'); return $result; } @@ -260,7 +259,7 @@ class Quota extends StandardController } echo "Update quota : " . $quota['id'] . "\n"; - $internal_event->create($quota['id_user'], 'QUOTA_RENEWAL', 'Renew quota ' . $quota['id'] . ' report ' . $report . ' credits.'); + $internal_event->create($quota['id_user'], 'QUOTA_RENEWAL', 'Renew quota and report ' . $report . ' credits.'); } } diff --git a/controllers/internals/Sended.php b/controllers/internals/Sended.php index e2eaaab..2e0c102 100644 --- a/controllers/internals/Sended.php +++ b/controllers/internals/Sended.php @@ -222,7 +222,7 @@ namespace controllers\internals; //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)) + if (!$internal_quota->has_enough_credit($id_user, $nb_credits)) { $return['error'] = false; $return['error_message'] = 'Not enough credit to send message.'; 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/Dashboard.php b/controllers/publics/Dashboard.php index 50f2779..ad9ec6d 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,50 @@ 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 = $this->internal_quota->get_user_quota($id_user); + if ($quota && (new \DateTime($quota['start_date']) <= $now) && (new \DateTime($quota['expiration_date']) > $now)) + { + $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 +124,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; + $avg_sendeds = round($total_sendeds / $nb_days, 2); + $avg_receiveds = round($total_receiveds / $nb_days, 2); $array_area_chart = array_values($array_area_chart); diff --git a/controllers/publics/User.php b/controllers/publics/User.php index ad158e9..4a4da9f 100644 --- a/controllers/publics/User.php +++ b/controllers/publics/User.php @@ -58,6 +58,17 @@ class User extends \descartes\Controller { $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'); @@ -300,6 +311,7 @@ class User extends \descartes\Controller return $this->redirect(\descartes\Router::url('User', 'add')); } + $nb_update = 0; $users = $_POST['users'] ?? []; foreach ($users as $id_user => $user) { @@ -331,6 +343,7 @@ class User extends \descartes\Controller return $this->redirect(\descartes\Router::url('User', 'add')); } + //Forge quota for user if needed $quota = false; if ($quota_enable) @@ -384,13 +397,22 @@ class User extends \descartes\Controller if (!$success) { \FlashMessage\FlashMessage::push('danger', 'L\'utilisateur #' . (int) $id_user . ' n\'as pas pu être mis à jour.'); - + continue; } - return $this->redirect(\descartes\Router::url('User', 'list')); + $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/models/Quota.php b/models/Quota.php index 7d6f07c..e971578 100644 --- a/models/Quota.php +++ b/models/Quota.php @@ -39,7 +39,7 @@ namespace models; FROM quota WHERE id_user = :id_user AND start_date <= :at - AND end_date > :at'; + AND expiration_date > :at'; $params = [ 'id_user' => $id_user, diff --git a/templates/account/show.php b/templates/account/show.php index 5a5574d..1ad0e88 100644 --- a/templates/account/show.php +++ b/templates/account/show.php @@ -81,7 +81,22 @@
-
+
+ +
+
+

Quota de SMS

+
+
+ Crédit de base : s($quota['credit']); ?>
+ Crédit additionel : s($quota['additional']); ?>
+ Crédit consommés : s($quota['consumed']); ?> (%)
+ Renouvellement automatique : s(($quota['auto_renew'] ? 'Oui, renouvellement le ' : 'Non, fin le ') . $quota['expiration_date']); ?>
+ Report des crédits non utilisés :
+ Report des crédits additionels non utilisés :
+
+
+

Modifier e-mail

diff --git a/templates/user/edit.php b/templates/user/edit.php index a2867ff..0042b7d 100644 --- a/templates/user/edit.php +++ b/templates/user/edit.php @@ -62,6 +62,9 @@
Quota de SMS + new \DateTime($user['quota']['expiration_date']))) { ?> +
Le quota de cet utilisateur est expiré depuis le s($user['quota']['expiration_date']); ?> est n'as pas été renouvelé, il n'est donc plus appliqué !
+
diff --git a/templates/user/list.php b/templates/user/list.php index 56a5de1..d849cad 100644 --- a/templates/user/list.php +++ b/templates/user/list.php @@ -95,8 +95,11 @@ jQuery(document).ready(function () { data: 'quota_percentage', render: function (data, type, row, meta) { - return jQuery.fn.dataTable.render.text().display(data) + "%"; - return ''; + var html = jQuery.fn.dataTable.render.text().display(data) + "%"; + if (row['quota_expired_at'] !== undefined) { + html += ' - Quota expiré le ' + jQuery.fn.dataTable.render.text().display(row['quota_expired_at']) + ''; + } + return html; }, }, { From ca9b7c7c6edb1e0e017d926f728f635562c4fbe9 Mon Sep 17 00:00:00 2001 From: osaajani <> Date: Mon, 14 Jun 2021 20:21:06 +0200 Subject: [PATCH 10/16] improve icons for credit --- assets/css/style.css | 7 +++++++ controllers/internals/Tool.php | 20 ++++++++++++++++++++ controllers/publics/Dashboard.php | 6 +++++- templates/account/show.php | 2 +- templates/dashboard/show.php | 10 +++++++--- templates/setting/show.php | 4 ++-- 6 files changed, 42 insertions(+), 7 deletions(-) diff --git a/assets/css/style.css b/assets/css/style.css index 1bda82d..3cb0422 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 { diff --git a/controllers/internals/Tool.php b/controllers/internals/Tool.php index 4481777..65829c0 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'; } diff --git a/controllers/publics/Dashboard.php b/controllers/publics/Dashboard.php index ad9ec6d..2e1c033 100644 --- a/controllers/publics/Dashboard.php +++ b/controllers/publics/Dashboard.php @@ -78,9 +78,12 @@ namespace controllers\publics; $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'); } @@ -124,7 +127,7 @@ namespace controllers\publics; $total_receiveds += $nb_received; } - $nb_days = $stats_start_date->diff($now)->days; + $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); @@ -139,6 +142,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/templates/account/show.php b/templates/account/show.php index 1ad0e88..35b66e9 100644 --- a/templates/account/show.php +++ b/templates/account/show.php @@ -85,7 +85,7 @@
-

Quota de SMS

+

Quota de SMS

Crédit de base : s($quota['credit']); ?>
diff --git a/templates/dashboard/show.php b/templates/dashboard/show.php index f64c3d0..b461521 100644 --- a/templates/dashboard/show.php +++ b/templates/dashboard/show.php @@ -118,11 +118,15 @@
-
+

Activité de la semaine :

SMS envoyés (moyenne = par jour).
SMS reçus (moyenne = par jour). + +
+ Limite max de SMS sur la période (). +
@@ -250,8 +254,8 @@ ykeys: ['sendeds', 'receiveds'], labels: ['SMS envoyés', 'SMS reçus'], lineColors: ['#5CB85C', '#EDAB4D'], - goals: [, ], - goalLineColors: ['#5CB85C', '#EDAB4D'], + goals: [, ], + goalLineColors: ['#5CB85C', '#EDAB4D', '#d9534f'], goalStrokeWidth: 2, pointSize: 4, hideHover: 'auto', diff --git a/templates/setting/show.php b/templates/setting/show.php index e075efd..1f3d9be 100644 --- a/templates/setting/show.php +++ b/templates/setting/show.php @@ -145,7 +145,7 @@
-

Alerte limite de SMS atteinte

+

Alerte limite de SMS atteinte

@@ -296,7 +296,7 @@
-

Alerte limite de SMS proche

+

Alerte limite de SMS proche

From 17d91873d41cb187f7e96f5a2213f17a6dec67d6 Mon Sep 17 00:00:00 2001 From: osaajani <> Date: Mon, 14 Jun 2021 21:33:06 +0200 Subject: [PATCH 11/16] add setting to hide menus for users --- controllers/publics/Setting.php | 14 ++++++++ env.php.dist | 1 + templates/incs/nav.php | 60 ++++++++++++++++++++------------- templates/setting/show.php | 39 ++++++++++++++++++++- 4 files changed, 90 insertions(+), 24 deletions(-) diff --git a/controllers/publics/Setting.php b/controllers/publics/Setting.php index 28ef605..abb41dd 100644 --- a/controllers/publics/Setting.php +++ b/controllers/publics/Setting.php @@ -40,6 +40,7 @@ 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 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 && ($setting_value === false)) + { + $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/env.php.dist b/env.php.dist index 2a602e8..bfee5df 100644 --- a/env.php.dist +++ b/env.php.dist @@ -82,6 +82,7 @@ 'mms' => 1, 'alert_quota_limit_reached' => 1, 'alert_quota_limit_close' => 0.9, + 'hide_menus' => '', ], ]; diff --git a/templates/incs/nav.php b/templates/incs/nav.php index 243c0d2..5a5d3c3 100644 --- a/templates/incs/nav.php +++ b/templates/incs/nav.php @@ -68,37 +68,51 @@ - -
  • - Logs -
  • - -
  • > - Commandes + +
  • + Logs +
  • + + + +
  • > + Commandes +
  • +
  • > Webhooks
  • -
  • > - Téléphones -
  • -
  • > - Réglages -
  • + +
  • > + Téléphones +
  • + + +
  • > + Réglages +
  • +
  • > Utilisateurs diff --git a/templates/setting/show.php b/templates/setting/show.php index 1f3d9be..7a6325a 100644 --- a/templates/setting/show.php +++ b/templates/setting/show.php @@ -318,6 +318,23 @@
  • +
    +
    +

    Cacher des menus

    +
    +
    +
    + +
    + + +
    +
    + +
    +
    +
    +
    @@ -339,7 +356,27 @@ url += '/' + jQuery(this).val(); }); window.location = url; - }); + }); + + jQuery('.add-hide-menus').each(function() + { + jQuery(this).magicSuggest({ + data: [ + {'id': 'logs', 'name': 'Logs'}, + {"id": "smsstop", "name": "SMS Stop"}, + {"id": "calls", "name": "Appels"}, + {"id": "events", "name": "Évènements"}, + {"id": "commands", "name": "Commandes"}, + {"id": "phones", "name": "Téléphones"}, + {"id": "settings", "name": "Réglages"}, + ], + valueField: 'id', + displayField: 'name', + name: 'hide_menus[]', + maxSelection: null, + }); + }); + }); Date: Mon, 14 Jun 2021 21:43:03 +0200 Subject: [PATCH 12/16] disable password autocomplete and fix password not hashed on update --- controllers/publics/User.php | 2 +- templates/account/show.php | 2 +- templates/user/add.php | 2 +- templates/user/edit.php | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/controllers/publics/User.php b/controllers/publics/User.php index 4a4da9f..67fa502 100644 --- a/controllers/publics/User.php +++ b/controllers/publics/User.php @@ -390,7 +390,7 @@ class User extends \descartes\Controller if ($password) { - $updated_user['password'] = $password; + $updated_user['password'] = password_hash($password, PASSWORD_DEFAULT); } $success = $this->internal_user->update($id_user, $updated_user, $quota); diff --git a/templates/account/show.php b/templates/account/show.php index 35b66e9..6d1355d 100644 --- a/templates/account/show.php +++ b/templates/account/show.php @@ -53,7 +53,7 @@
    - +
    diff --git a/templates/user/add.php b/templates/user/add.php index ad6e5e6..658f543 100644 --- a/templates/user/add.php +++ b/templates/user/add.php @@ -49,7 +49,7 @@
    - +
    diff --git a/templates/user/edit.php b/templates/user/edit.php index 0042b7d..88cbf23 100644 --- a/templates/user/edit.php +++ b/templates/user/edit.php @@ -50,7 +50,7 @@
    - +
    From fa8ee399e9ec1c6228d07b587eda4935c1cc869d Mon Sep 17 00:00:00 2001 From: osaajani <> Date: Tue, 15 Jun 2021 01:49:47 +0200 Subject: [PATCH 13/16] add credit estimation on message --- assets/css/style.css | 6 ++++++ controllers/publics/Templating.php | 8 ++++++++ templates/scheduled/add.php | 16 +++++++++++++--- templates/scheduled/edit.php | 10 ++++++++++ 4 files changed, 37 insertions(+), 3 deletions(-) diff --git a/assets/css/style.css b/assets/css/style.css index 3cb0422..1cc9809 100644 --- a/assets/css/style.css +++ b/assets/css/style.css @@ -367,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/publics/Templating.php b/controllers/publics/Templating.php index 3545b7c..158a520 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,16 @@ 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/templates/scheduled/add.php b/templates/scheduled/add.php index 4ce6619..491bb42 100644 --- a/templates/scheduled/add.php +++ b/templates/scheduled/add.php @@ -45,8 +45,8 @@

    - Vous pouvez utilisez des fonctionnalités de templating pour indiquer des valeures génériques qui seront remplacées par les données du contact au moment de l'envoie. Pour plus d'information, consultez la documentation sur l'utilisation des templates.
    - Vous pouvez obtenir une prévisualisation du résultat pour un contact en cliquant sur le boutton "Prévisualiser". + Vous pouvez utilisez des fonctionnalités de templating pour indiquer des valeures génériques qui seront remplacées par les données du contact au moment de l'envoie. Pour plus d'information, consultez la documentation sur l'utilisation des templates.
    + Vous pouvez obtenir une prévisualisation du résultat pour un contact, ainsi qu'une estimation du nombre de crédits qui seront utilisés par SMS, en cliquant sur le boutton "Prévisualiser".

    @@ -67,7 +67,7 @@

    - L'ajout d'un média nécessite un téléphone supportant l'envoi de MMS. Pour plus d'information, consultez la documentation sur l'utilisation des MMS.. + L'ajout d'un média nécessite un téléphone supportant l'envoi de MMS. Pour plus d'information, consultez la documentation sur l'utilisation des MMS.

    @@ -141,6 +141,9 @@