raspisms/controllers/publics/Phone.php

645 lines
21 KiB
PHP

<?php
/*
* This file is part of RaspiSMS.
*
* (c) Pierre-Lin Bonnemaison <plebwebsas@gmail.com>
*
* This source file is subject to the GPL-3.0 license that is bundled
* with this source code in the file LICENSE.
*/
namespace controllers\publics;
/**
* Page des phones.
*/
class Phone extends \descartes\Controller
{
private $internal_phone;
private $internal_adapter;
private $internal_sended;
public function __construct()
{
$bdd = \descartes\Model::_connect(DATABASE_HOST, DATABASE_NAME, DATABASE_USER, DATABASE_PASSWORD);
$this->internal_phone = new \controllers\internals\Phone($bdd);
$this->internal_sended = new \controllers\internals\Sended($bdd);
$this->internal_adapter = new \controllers\internals\Adapter();
\controllers\internals\Tool::verifyconnect();
}
/**
* Cette fonction retourne tous les phones, sous forme d'un tableau permettant l'administration de ces phones.
*/
public function list()
{
$this->render('phone/list');
}
/**
* Return phones as json with additionnals data about callbacks.
*/
public function list_json()
{
$id_user = $_SESSION['user']['id'];
$api_key = $_SESSION['user']['api_key'];
$phones = $this->internal_phone->list_for_user($id_user);
$adapters = [];
$adapters = $this->internal_adapter->list_adapters();
foreach ($adapters as $key => $adapter)
{
unset($adapters[$key]);
$adapters[$adapter['meta_classname']] = $adapter;
}
foreach ($phones as &$phone)
{
$limits = $this->internal_phone->get_limits($phone['id']);
$phone['limits'] = $limits;
$adapter = $adapters[$phone['adapter']] ?? false;
if (!$adapter)
{
$phone['adapter'] = 'Inconnu';
continue;
}
$phone['adapter'] = $adapter['meta_name'];
if ($adapter['meta_support_reception'])
{
$phone['callback_reception'] = \descartes\Router::url('Callback', 'reception', ['adapter_uid' => $adapter['meta_uid'], 'id_phone' => $phone['id']], ['api_key' => $api_key]);
}
if ($adapter['meta_support_status_change'])
{
$phone['callback_status'] = \descartes\Router::url('Callback', 'update_sended_status', ['adapter_uid' => $adapter['meta_uid']], ['api_key' => $api_key]);
}
if ($adapter['meta_support_inbound_call_callback'])
{
$phone['callback_inbound_call'] = \descartes\Router::url('Callback', 'inbound_call', ['id_phone' => $phone['id']], ['api_key' => $api_key]);
}
if ($adapter['meta_support_end_call_callback'])
{
$phone['callback_end_call'] = \descartes\Router::url('Callback', 'end_call', ['id_phone' => $phone['id']], ['api_key' => $api_key]);
}
if ($adapter['meta_support_phone_status'])
{
$phone['support_phone_status'] = true;
}
}
header('Content-Type: application/json');
echo json_encode(['data' => $phones]);
}
/**
* Cette fonction va supprimer une liste de phones.
*
* @param array int $_GET['ids'] : Les id des phonees à supprimer
* @param mixed $csrf
*
* @return boolean;
*/
public function delete($csrf)
{
if (!$this->verify_csrf($csrf))
{
\FlashMessage\FlashMessage::push('danger', 'Jeton CSRF invalid !');
return $this->redirect(\descartes\Router::url('Phone', 'list'));
}
$ids = $_GET['ids'] ?? [];
foreach ($ids as $id)
{
$this->internal_phone->delete_for_user($_SESSION['user']['id'], $id);
}
return $this->redirect(\descartes\Router::url('Phone', 'list'));
}
/**
* Cette fonction retourne la page d'ajout d'un phone.
*/
public function add()
{
$adapters = $this->internal_adapter->list_adapters();
return $this->render('phone/add', ['adapters' => $adapters]);
}
/**
* Create a new phone.
*
* @param $csrf : CSRF token
* @param string $_POST['name'] : Phone name
* @param string $_POST['adapter'] : Phone adapter
* @param ?array $_POST['adapter_data'] : Phone adapter data
* @param ?array $_POST['limits'] : Array of limits in number of SMS for a period to be applied to this phone.
* @param int $_POST['priority'] : Priority with which to use phone to send SMS. Default 0.
*/
public function create($csrf)
{
if (!$this->verify_csrf($csrf))
{
\FlashMessage\FlashMessage::push('danger', 'Jeton CSRF invalid !');
return $this->redirect(\descartes\Router::url('Phone', 'add'));
}
$id_user = $_SESSION['user']['id'];
$name = $_POST['name'] ?? false;
$priority = $_POST['priority'] ?? 0;
$priority = max(((int) $priority), 0);
$adapter = $_POST['adapter'] ?? false;
$adapter_data = !empty($_POST['adapter_data']) ? $_POST['adapter_data'] : [];
$limits = $_POST['limits'] ?? [];
$limits = is_array($limits) ? $limits : [$limits];
if (!$name || !$adapter)
{
\FlashMessage\FlashMessage::push('danger', 'Des champs obligatoires sont manquants.');
return $this->redirect(\descartes\Router::url('Phone', 'add'));
}
$name_exist = $this->internal_phone->get_by_name_and_user($id_user, $name);
if ($name_exist)
{
\FlashMessage\FlashMessage::push('danger', 'Ce nom est déjà utilisé pour un autre téléphone.');
return $this->redirect(\descartes\Router::url('Phone', 'add'));
}
if ($limits)
{
foreach ($limits as $key => $limit)
{
if (!is_array($limit))
{
unset($limits[$key]);
continue;
}
$startpoint = $limit['startpoint'] ?? false;
$volume = $limit['volume'] ?? false;
if (!$startpoint || !$volume)
{
unset($limits[$key]);
continue;
}
$volume = (int) $volume;
$limits[$key]['volume'] = max($volume, 1);
if (!\controllers\internals\Tool::validate_relative_date($startpoint))
{
unset($limits[$key]);
continue;
}
}
}
$adapters = $this->internal_adapter->list_adapters();
$find_adapter = false;
foreach ($adapters as $metas)
{
if ($metas['meta_classname'] === $adapter)
{
$find_adapter = $metas;
break;
}
}
if (!$find_adapter)
{
\FlashMessage\FlashMessage::push('danger', 'Ce type de téléphone n\'existe pas.');
return $this->redirect(\descartes\Router::url('Phone', 'add'));
}
if ($find_adapter['meta_hidden'])
{
\FlashMessage\FlashMessage::push('danger', 'Ce type de téléphone ne peux pas être créé via l\'interface graphique.');
return $this->redirect(\descartes\Router::url('Phone', 'add'));
}
//If missing required data fields, error
foreach ($find_adapter['meta_data_fields'] as $field)
{
if (false === $field['required'])
{
continue;
}
if (!empty($adapter_data[$field['name']]))
{
continue;
}
\FlashMessage\FlashMessage::push('danger', 'Vous n\'avez pas rempli certains champs obligatoires pour ce type de téléphone.');
return $this->redirect(\descartes\Router::url('Phone', 'add'));
}
//If field phone number is invalid
foreach ($find_adapter['meta_data_fields'] as $field)
{
if ('phone_number' !== ($field['type'] ?? false))
{
continue;
}
if (!empty($adapter_data[$field['name']]))
{
$adapter_data[$field['name']] = \controllers\internals\Tool::parse_phone($adapter_data[$field['name']]);
if ($adapter_data[$field['name']])
{
continue;
}
}
\FlashMessage\FlashMessage::push('danger', 'Vous avez fourni un numéro de téléphone avec un format invalide.');
return $this->redirect(\descartes\Router::url('Phone', 'add'));
}
$adapter_data = json_encode($adapter_data);
//Check adapter is working correctly with thoses names and data
$adapter_classname = $find_adapter['meta_classname'];
$adapter_instance = new $adapter_classname($adapter_data);
$adapter_working = $adapter_instance->test();
if (!$adapter_working)
{
\FlashMessage\FlashMessage::push('danger', 'Impossible d\'utiliser ce type de téléphone avec les données fournies. Vérifiez les réglages.');
return $this->redirect(\descartes\Router::url('Phone', 'add'));
}
$success = $this->internal_phone->create($id_user, $name, $adapter, $adapter_data, $priority, $limits);
if (!$success)
{
\FlashMessage\FlashMessage::push('danger', 'Impossible de créer ce téléphone.');
return $this->redirect(\descartes\Router::url('Phone', 'add'));
}
\FlashMessage\FlashMessage::push('success', 'Le téléphone a bien été créé.');
return $this->redirect(\descartes\Router::url('Phone', 'list'));
}
/**
* Return the edit page for phones
*
* @param int... $ids : Phones ids
*/
public function edit()
{
$ids = $_GET['ids'] ?? [];
$id_user = $_SESSION['user']['id'];
$phones = $this->internal_phone->gets_in_for_user($id_user, $ids);
if (!$phones)
{
return $this->redirect(\descartes\Router::url('Phone', 'list'));
}
$adapters = $this->internal_adapter->list_adapters();
foreach ($phones as &$phone)
{
$limits = $this->internal_phone->get_limits($phone['id']);
$phone['limits'] = $limits;
}
$this->render('phone/edit', [
'phones' => $phones,
'adapters' => $adapters,
]);
}
/**
* Update multiple phones.
*
* @param $csrf : CSRF token
* @param string $_POST['phones']['id']['name'] : Phone name
* @param string $_POST['phones']['id']['adapter'] : Phone adapter
* @param ?array $_POST['phones']['id']['adapter_data'] : Phone adapter data
* @param ?array $_POST['phones']['id']['limits'] : Array of limits in number of SMS for a period to be applied to this phone.
* @param int $_POST['phones']['id']['priority'] : Priority with which to use phone to send SMS. Default 0.
*/
public function update($csrf)
{
if (!$this->verify_csrf($csrf))
{
\FlashMessage\FlashMessage::push('danger', 'Jeton CSRF invalid !');
return $this->redirect(\descartes\Router::url('Phone', 'add'));
}
if (!$_POST['phones'])
{
return $this->redirect(\descartes\Router::url('Phone', 'list'));
}
$id_user = $_SESSION['user']['id'];
$nb_update = 0;
foreach ($_POST['phones'] as $id_phone => $phone)
{
$name = $phone['name'] ?? false;
$priority = $phone['priority'] ?? 0;
$priority = max(((int) $priority), 0);
$adapter = $phone['adapter'] ?? false;
$adapter_data = !empty($phone['adapter_data']) ? $phone['adapter_data'] : [];
$limits = $phone['limits'] ?? [];
$limits = is_array($limits) ? $limits : [$limits];
if (!$name || !$adapter)
{
continue;
}
$phone_with_same_name = $this->internal_phone->get_by_name_and_user($id_user, $name);
if ($phone_with_same_name && $phone_with_same_name['id'] != $id_phone)
{
continue;
}
if ($limits)
{
foreach ($limits as $key => $limit)
{
if (!is_array($limit))
{
unset($limits[$key]);
continue;
}
$startpoint = $limit['startpoint'] ?? false;
$volume = $limit['volume'] ?? false;
if (!$startpoint || !$volume)
{
unset($limits[$key]);
continue;
}
$volume = (int) $volume;
$limits[$key]['volume'] = max($volume, 1);
if (!\controllers\internals\Tool::validate_relative_date($startpoint))
{
unset($limits[$key]);
continue;
}
}
}
$adapters = $this->internal_adapter->list_adapters();
$find_adapter = false;
foreach ($adapters as $metas)
{
if ($metas['meta_classname'] === $adapter)
{
$find_adapter = $metas;
break;
}
}
if (!$find_adapter)
{
continue;
}
$current_phone = $this->internal_phone->get_for_user($id_user, $id_phone);
if (!$current_phone)
{
continue;
}
// We can only use an hidden adapter if it was already the adapter we was using
if ($find_adapter['meta_hidden'] && $adapter != $current_phone['adapter'])
{
continue;
}
//If missing required data fields, error
foreach ($find_adapter['meta_data_fields'] as $field)
{
if (false === $field['required'])
{
continue;
}
if (!empty($adapter_data[$field['name']]))
{
continue;
}
continue 2;
}
//If field phone number is invalid
foreach ($find_adapter['meta_data_fields'] as $field)
{
if ('phone_number' !== ($field['type'] ?? false))
{
continue;
}
if (!empty($adapter_data[$field['name']]))
{
$adapter_data[$field['name']] = \controllers\internals\Tool::parse_phone($adapter_data[$field['name']]);
if ($adapter_data[$field['name']])
{
continue;
}
}
continue 2;
}
$adapter_data = json_encode($adapter_data);
//Check adapter is working correctly with thoses names and data
$adapter_classname = $find_adapter['meta_classname'];
$adapter_instance = new $adapter_classname($adapter_data);
$adapter_working = $adapter_instance->test();
if (!$adapter_working)
{
continue;
}
$success = $this->internal_phone->update_for_user($id_user, $id_phone, $name, $adapter, $adapter_data, $priority, $limits);
if (!$success)
{
continue;
}
$nb_update ++;
}
if ($nb_update !== \count($_POST['phones']))
{
\FlashMessage\FlashMessage::push('danger', 'Certains téléphones n\'ont pas pu êtres mis à jour.');
return $this->redirect(\descartes\Router::url('Phone', 'list'));
}
\FlashMessage\FlashMessage::push('success', 'Tous les téléphones ont été modifiés avec succès.');
return $this->redirect(\descartes\Router::url('Phone', 'list'));
}
/**
* Re-check phone status
* @param array int $_GET['ids'] : ids of phones we want to update status
* @param $csrf : CSRF token
*/
public function update_status ($csrf)
{
if (!$this->verify_csrf($csrf))
{
\FlashMessage\FlashMessage::push('danger', 'Jeton CSRF invalid !');
return $this->redirect(\descartes\Router::url('Phone', 'add'));
}
$ids = $_GET['ids'] ?? [];
$id_user = $_SESSION['user']['id'];
foreach ($ids as $id)
{
$phone = $this->internal_phone->get_for_user($id_user, $id);
if (!$phone)
{
continue;
}
if ($phone['status'] === \models\Phone::STATUS_DISABLED)
{
\FlashMessage\FlashMessage::push('error', 'Certains téléphones ont été désactivés manuellements, vous devez les réactiver manuellement.');
continue;
}
// If user have activated phone limits, check if RaspiSMS phone limit have already been reached
$limit_reached = false;
if ((int) ($_SESSION['user']['settings']['phone_limit'] ?? false))
{
$limits = $this->internal_phone->get_limits($id);
$remaining_volume = PHP_INT_MAX;
foreach ($limits as $limit)
{
$startpoint = new \DateTime($limit['startpoint']);
$consumed = $this->internal_sended->count_since_for_phone_and_user($_SESSION['user']['id'], $id, $startpoint);
$remaining_volume = min(($limit['volume'] - $consumed), $remaining_volume);
}
if ($remaining_volume < 1)
{
$limit_reached = true;
}
}
if ($limit_reached)
{
$new_status = \models\Phone::STATUS_LIMIT_REACHED;
}
else
{
//Check status on provider side
$adapter_classname = $phone['adapter'];
if (!call_user_func([$adapter_classname, 'meta_support_phone_status']))
{
continue;
}
$adapter_instance = new $adapter_classname($phone['adapter_data']);
$new_status = $adapter_instance->check_phone_status();
}
$status_update = $this->internal_phone->update_status($id, $new_status);
}
\FlashMessage\FlashMessage::push('success', 'Les status des téléphones ont bien été mis à jour.');
return $this->redirect(\descartes\Router::url('Phone', 'list'));
}
/**
* Manually disable/enable phones
* @param array int $_GET['ids'] : ids of phones we want to update status
* @param string $new_status : New status of the phone, either 'disabled' or 'available'
* @param $csrf : CSRF token
*/
public function change_status ($new_status, $csrf)
{
if (!$this->verify_csrf($csrf))
{
\FlashMessage\FlashMessage::push('danger', 'Jeton CSRF invalid !');
return $this->redirect(\descartes\Router::url('Phone', 'add'));
}
if (!in_array($new_status, [\models\Phone::STATUS_AVAILABLE, \models\Phone::STATUS_DISABLED]))
{
\FlashMessage\FlashMessage::push('danger', 'Seul les status disponibles et désactivés peuvent être définis manuellement.');
return $this->redirect(\descartes\Router::url('Phone', 'add'));
}
$ids = $_GET['ids'] ?? [];
$id_user = $_SESSION['user']['id'];
foreach ($ids as $id)
{
$phone = $this->internal_phone->get_for_user($id_user, $id);
if (!$phone)
{
continue;
}
$status_update = $this->internal_phone->update_status($id, $new_status);
}
\FlashMessage\FlashMessage::push('success', 'Les status des téléphones ont bien été mis à jour manuellement.');
return $this->redirect(\descartes\Router::url('Phone', 'list'));
}
/**
* Return a list of phones as a JSON array
*/
public function json_list()
{
header('Content-Type: application/json');
echo json_encode($this->internal_phone->list_for_user($_SESSION['user']['id']));
}
}