From a69b15698068becea532fb5e6f35294578e94de4 Mon Sep 17 00:00:00 2001 From: Ben Klang Date: Fri, 1 Jan 2010 23:58:12 -0500 Subject: [PATCH] Shout: Complete device editing form & processing Still some rough edges here, and delete likely doesn't work. But most of the functionality necessary to add or edit devices is in place. Editing has been lightly tested. --- shout/devices.php | 109 +++++++++++++++++++++++++---------------- shout/lib/Driver.php | 67 +++++++++++++++++++++++-- shout/lib/Driver/Sql.php | 90 ++++++++++++++++++++++++++++------ shout/lib/Forms/DeviceForm.php | 69 +++++++++++++++++++++++++- shout/lib/Shout.php | 48 ++++++++++++++++++ 5 files changed, 322 insertions(+), 61 deletions(-) diff --git a/shout/devices.php b/shout/devices.php index cac6319b0..924b0944c 100644 --- a/shout/devices.php +++ b/shout/devices.php @@ -10,70 +10,97 @@ require_once SHOUT_BASE . '/lib/base.php'; require_once SHOUT_BASE . '/lib/Forms/DeviceForm.php'; $action = Horde_Util::getFormData('action'); -$devices = $shout_devices->getDevices($context); -$devid = Horde_Util::getFormData('devid'); $vars = Horde_Variables::getDefaultVariables(); //$tabs = Shout::getTabs($context, $vars); $RENDERER = new Horde_Form_Renderer(); -$section = 'devices'; $title = _("Devices: "); switch ($action) { - case 'save': - $Form = new DeviceDetailsForm($vars); - - // Show the list if the save was successful, otherwise back to edit. - if ($Form->isSubmitted() && $Form->isValid()) { - try { - $shout_devices->saveDevice($Form->getVars()); - $notification->push(_("Device settings saved.")); - } catch (Exception $e) { - $notification->push($e); - } +case 'add': +case 'edit': + $vars = Horde_Variables::getDefaultVariables(); + $vars->set('context', $context); + $Form = new DeviceDetailsForm($vars); + + // Show the list if the save was successful, otherwise back to edit. + if ($Form->isSubmitted() && $Form->isValid()) { + // Form is Valid and Submitted + try { + $devid = Horde_Util::getFormData('devid'); + + $Form->execute(); + $notification->push(_("Device information updated."), + 'horde.success'); $action = 'list'; break; - } else { - $action = 'edit'; - } - case 'add': - case 'edit': - if ($action == 'add') { - $title .= _("New Device"); - // Treat adds just like an empty edit - $action = 'edit'; - } else { - $title .= sprintf(_("Edit Device %s"), $extension); + } catch (Exception $e) { + $notification->push($e); } + } elseif ($Form->isSubmitted()) { + // Submitted but not valid + $notification->push(_("Problem processing the form. Please check below and try again."), 'horde.warning'); + } + + // Create a new add/edit form + $devid = Horde_Util::getFormData('devid'); + $devices = $shout_devices->getDevices($context); + $vars = new Horde_Variables($devices[$devid]); + + $vars->set('action', $action); + $Form = new DeviceDetailsForm($vars); + $Form->open($RENDERER, $vars, Horde::applicationUrl('devices.php'), 'post'); + // Make sure we get the right template below. + $action = 'edit'; + + break; +case 'delete': + $title .= sprintf(_("Delete Devices %s"), $extension); + $devid = Horde_Util::getFormData('devid'); + + $vars = Horde_Variables::getDefaultVariables(); + $vars->set('context', $context); + $Form = new DeviceDeleteForm($vars); + + $FormValid = $Form->validate($vars, true); + + if ($Form->isSubmitted() && $FormValid) { + try { + $Form->execute(); + $notification->push(_("Device Deleted.")); + $action = 'list'; + } catch (Exception $e) { + $notification->push($e); + } + } elseif ($Form->isSubmitted()) { + $notification->push(_("Problem processing the form. Please check below and try again."), 'horde.warning'); + } - $FormName = 'DeviceDetailsForm'; - $vars = new Horde_Variables($devices[$devid]); - $Form = new DeviceDetailsForm($vars); - - $Form->open($RENDERER, $vars, Horde::applicationUrl('devices.php'), 'post'); - - break; - + $vars = Horde_Variables::getDefaultVariables(array()); + $vars->set('context', $context); + $Form = new DeviceDeleteForm($vars); + $Form->open($RENDERER, $vars, Horde::applicationUrl('devices.php'), 'post'); - case 'delete': - $notification->push("Not supported."); - break; + break; - case 'list': - default: - $action = 'list'; - $title .= _("List Users"); +case 'list': +default: + $action = 'list'; + $title .= _("List Devices"); } +// Fetch the (possibly updated) list of extensions +$devices = $shout_devices->getDevices($context); + require SHOUT_TEMPLATES . '/common-header.inc'; require SHOUT_TEMPLATES . '/menu.inc'; $notification->notify(); -//echo $tabs->render($section); +echo "
\n"; require SHOUT_TEMPLATES . '/devices/' . $action . '.inc'; diff --git a/shout/lib/Driver.php b/shout/lib/Driver.php index 61bb56a26..6eac8dfc5 100644 --- a/shout/lib/Driver.php +++ b/shout/lib/Driver.php @@ -114,7 +114,11 @@ class Shout_Driver { } /** - * Save an extension to the LDAP tree + * Save an extension to the backend. + * + * This method is intended to be overridden by a child class. However it + * also implements some basic checks, so a typical backend will still + * call this method via parent:: * * @param string $context Context to which the user should be added * @@ -142,9 +146,66 @@ class Shout_Driver { throw new Shout_Exception(_("Invalid extension.")); } - if (!Shout::checkRights("shout:contexts:$context:users", + if (!Shout::checkRights("shout:contexts:$context:extensions", + PERMS_DELETE, 1)) { + throw new Shout_Exception(_("Permission denied to delete extensions in this context.")); + } + } + + /** + * Save a device to the backend. + * + * This method is intended to be overridden by a child class. However it + * also implements some basic checks, so a typical backend will still + * call this method via parent:: + * + * @param string $context Context to which the user should be added + * + * @param string $extension Extension to be saved + * + * @param array $details Phone numbers, PIN, options, etc to be saved + * + * @return TRUE on success, PEAR::Error object on error + * @throws Shout_Exception + */ + public function saveDevice($context, $devid, &$details) + { + if (empty($context)) { + throw new Shout_Exception(_("Invalid device information.")); + } + + if (!Shout::checkRights("shout:contexts:$context:devices", PERMS_EDIT, 1)) { + throw new Shout_Exception(_("Permission denied to save devices in this context.")); + } + + if (empty($devid) || !empty($details['genauthtok'])) { + list($devid, $password) = Shout::genDeviceAuth($context); + $details['devid'] = $devid; + $details['password'] = $password; + } + + + } + + /** + * Delete a device from the backend. + * + * This method is intended to be overridden by a child class. However it + * also implements some basic checks, so a typical backend will still + * call this method via parent:: + * + * @param $context + * @param $devid + */ + public function deleteDevice($context, $devid) + { + if (empty($context) || empty($devid)) { + throw new Shout_Exception(_("Invalid device.")); + } + + if (!Shout::checkRights("shout:contexts:$context:devices", PERMS_DELETE, 1)) { - throw new Shout_Exception(_("Permission denied to delete users in this context.")); + throw new Shout_Exception(_("Permission denied to delete devices in this context.")); } } diff --git a/shout/lib/Driver/Sql.php b/shout/lib/Driver/Sql.php index 63583663a..7bc69b845 100644 --- a/shout/lib/Driver/Sql.php +++ b/shout/lib/Driver/Sql.php @@ -9,6 +9,12 @@ class Shout_Driver_Sql extends Shout_Driver protected $_db = null; /** + * Handle for the current writable database connection. + * @var object $_db + */ + protected $_write_db = null; + + /** * Boolean indicating whether or not we're connected to the LDAP * server. * @var boolean $_connected @@ -35,6 +41,8 @@ class Shout_Driver_Sql extends Shout_Driver $sql = sprintf($sql, $this->_params['table']); $vars = array(); + $msg = 'SQL query in Shout_Driver_Sql#getContexts(): ' . $sql; + Horde::logMessage($msg, __FILE__, __LINE__, PEAR_LOG_DEBUG); $result = $this->_db->query($sql, $vars); if ($result instanceof PEAR_Error) { throw Shout_Exception($result); @@ -74,7 +82,10 @@ class Shout_Driver_Sql extends Shout_Driver 'FROM %s WHERE accountcode = ?'; $sql = sprintf($sql, $this->_params['table']); $args = array($context); - $result = $this->_db->query($sql, $args); + $msg = 'SQL query in Shout_Driver_Sql#getDevices(): ' . $sql; + Horde::logMessage($msg, __FILE__, __LINE__, PEAR_LOG_DEBUG); + $sth = $this->_db->prepare($sql); + $result = $this->_db->execute($sth, $args); if ($result instanceof PEAR_Error) { throw new Shout_Exception($result); } @@ -88,7 +99,7 @@ class Shout_Driver_Sql extends Shout_Driver while ($row && !($row instanceof PEAR_Error)) { // Asterisk uses the "name" field to indicate the registration // identifier. We use the field "alias" to put a friendly name on - // the device. Thus devid -> name and name => alias + // the device. Thus devid => name and name => alias $devid = $row['name']; $row['devid'] = $devid; $row['name'] = $row['alias']; @@ -97,6 +108,10 @@ class Shout_Driver_Sql extends Shout_Driver // Trim off the context from the mailbox number list($row['mailbox']) = explode('@', $row['mailbox']); + // The UI calls the 'secret' a 'password' + $row['password'] = $row['secret']; + unset($row['secret']); + // Hide the DB internal ID from the front-end unset($row['id']); @@ -114,29 +129,74 @@ class Shout_Driver_Sql extends Shout_Driver * Save a device (add or edit) to the backend. * * @param string $context The context in which this device is valid - * @param array $info Array of device details + * @param string $devid Device ID to save + * @param array $details Array of device details */ - public function saveDevice($context, $info) + public function saveDevice($context, $devid, &$details) { + // Check permissions and possibly update the authentication tokens + parent::saveDevice($context, $devid, $details); + // See getDevices() for an explanation of these conversions - $info['alias'] = $info['name']; - $info['mailbox'] = $info['mailbox'] . '@' . $context; + $details['alias'] = $details['name']; + $details['name'] = $details['devid']; + $details['mailbox'] .= '@' . $context; - if ($info['devid']) { + if (!empty($devid)) { // This is an edit - $info['name'] = $info['devid']; - $sql = 'UPDATE %s SET '; + $details['name'] = $details['devid']; + $sql = 'UPDATE %s SET accountcode = ?, callerid = ?, ' . + 'mailbox = ?, secret = ?, alias = ?, canreinvite = "no", ' . + 'nat = "yes", type = "peer" WHERE name = ?'; } else { // This is an add. Generate a new unique ID and secret - $devid = $context . uniqid(); - $secret = md5(uniqid(mt_rand)); - $sql = 'INSERT INTO %s (name, accountcode, callerid, mailbox, ' . - 'secret, alias, canreinvite, nat, type) ' . - ' VALUES (?, ?, ?, ?, ?, ?, "no", "yes", "peer")'; + $sql = 'INSERT INTO %s (accountcode, callerid, mailbox, ' . + 'secret, alias, name, canreinvite, nat, type) ' . + 'VALUES (?, ?, ?, ?, ?, ?, "no", "yes", "peer")'; + + } + + $sql = sprintf($sql, $this->_params['table']); + $args = array( + $context, + $details['callerid'], + $details['mailbox'], + $details['password'], + $details['alias'], + $details['name'], + ); + + $msg = 'SQL query in Shout_Driver_Sql#saveDevice(): ' . $sql; + Horde::logMessage($msg, __FILE__, __LINE__, PEAR_LOG_DEBUG); + $sth = $this->_write_db->prepare($sql); + $result = $this->_write_db->execute($sth, $args); + if ($result instanceof PEAR_Error) { + $msg = $result->getMessage() . ': ' . $result->getDebugInfo(); + Horde::logMessage($msg, __FILE__, __LINE__, PEAR_LOG_ERR); + throw new Shout_Exception(_("Internal database error. Details have been logged for the administrator.")); } + return true; + } + + public function deleteDevice($context, $devid) + { + parent::deleteDevice($context, $devid); + + $sql = 'DELETE FROM %s WHERE devid = ?'; + $sql = sprintf($sql, $this->_params['table']); + $values = array($devid); + + $msg = 'SQL query in Shout_Driver_Sql#deleteDevice(): ' . $sql; + Horde::logMessage($msg, __FILE__, __LINE__, PEAR_LOG_DEBUG); + $res = $this->_write_db->query($sql); + if ($res instanceof PEAR_Error) { + throw new Shout_Exception($res->getMessage(), $res->getCode()); + } + + return true; } /** @@ -260,5 +320,5 @@ class Shout_Driver_Sql extends Shout_Driver $this->_write_db->disconnect(); } } - + } diff --git a/shout/lib/Forms/DeviceForm.php b/shout/lib/Forms/DeviceForm.php index 951738cc7..bd8d9f3cd 100644 --- a/shout/lib/Forms/DeviceForm.php +++ b/shout/lib/Forms/DeviceForm.php @@ -27,7 +27,6 @@ class DeviceDetailsForm extends Horde_Form { parent::__construct($vars, _("$formtitle - Context: $context")); $this->addHidden('', 'action', 'text', true); - $vars->set('action', 'save'); if ($edit) { $this->addHidden('', 'devid', 'text', true); @@ -35,9 +34,75 @@ class DeviceDetailsForm extends Horde_Form { $this->addVariable(_("Device Name"), 'name', 'text', false); $this->addVariable(_("Mailbox"), 'mailbox', 'int', false); $this->addVariable(_("CallerID"), 'callerid', 'text', false); - + $this->addVariable(_("Reset authentication token?"), 'genauthtok', + 'boolean', false, false);//, + // _("If checked, the system will generate new device ID and password. The associated device will need to be reconfigured with the new information.")); return true; } + public function execute() + { + global $shout_devices; + + $action = $this->_vars->get('action'); + $context = $this->_vars->get('context'); + $devid = $this->_vars->get('devid'); + + // For safety, we force the device ID and password rather than rely + // on the form to pass them around. + if ($action == 'add') { + // The device ID should be empty so it can be generated. + $devid = null; + $password = null; + } else { // $action must be 'edit' + $devices = $shout_devices->getDevices($context); + if (!isset($devices[$devid])) { + // The device requested doesn't already exist. This can't + // be a valid edit. + throw new Shout_Exception(_("That device does not exist."), + 'horde.error'); + } else { + $password = $devices[$devid]['password']; + } + } + + $details = array( + 'devid' => $devid, + 'name' => $this->_vars->get('name'), + 'mailbox' => $this->_vars->get('mailbox'), + 'callerid' => $this->_vars->get('callerid'), + 'genauthtok' => $this->_vars->get('genauthtok'), + 'password' => $password, + ); + + $shout_devices->saveDevice($context, $devid, $details); + } + +} + +class DeviceDeleteForm extends Horde_Form +{ + function __construct(&$vars) + { + $devid = $vars->get('devid'); + $context = $vars->get('context'); + + $title = _("Delete Device %s - Context: %s"); + $title = sprintf($title, $devid, $context); + parent::__construct($vars, $title); + + $this->addHidden('', 'context', 'text', true); + $this->addHidden('', 'devid', 'text', true); + $this->addHidden('', 'action', 'text', true); + $this->setButtons(array(_("Delete"), _("Cancel"))); + } + + function execute() + { + global $shout_devices; + $context = $this->_vars->get('context'); + $devid = $this->_vars->get('devid'); + $shout_devices->deleteDevice($context, $devid); + } } \ No newline at end of file diff --git a/shout/lib/Shout.php b/shout/lib/Shout.php index 2dc4ebbaa..bce4144a4 100644 --- a/shout/lib/Shout.php +++ b/shout/lib/Shout.php @@ -132,4 +132,52 @@ class Shout return ($test & $permmask) == $permmask; } + + /** + * Generate new device authentication tokens. + * + * This method is designed to generate random strings for the + * authentication ID and password. The result is intended to be used + * for automatically generated device information. The user is prevented + * from specifying usernames and passwords for these reasons: + * 1) If a username and/or password can be easily guessed, monetary loss + * is likely through the fraudulent placing of telephone calls. + * This has been observed in the wild far too many times already. + * + * 2) The username and password are only needed to be programmed into the + * device once, and then stored semi-permanently. In some cases, the + * provisioning can be done automatically. For these reasons, having + * user-friendly usernames and passswords is not terribly important. + * + * @param string $context Context for this credential pair + * + * @return array Array of (string $deviceID, string $devicePassword) + */ + static public function genDeviceAuth($context) + { + $devid = uniqid($context); + $password = uudecode(md5(uniqid(mt_rand(), true))); + + // This simple password generation algorithm inspired by Jon Haworth + // http://www.laughing-buddha.net/jon/php/password/ + + // define possible characters + // Vowels excluded to avoid potential pronounceability + $possible = "0123456789bcdfghjkmnpqrstvwxyzBCDFGHJKLMNPQRSTVWXYZ"; + + $password = ""; + $i = 0; + while ($i < 12) { + // pick a random character from the possible ones + $char = substr($possible, mt_rand(0, strlen($possible)-1), 1); + + // we don't want this character if it's already in the password + if (!strstr($password, $char)) { + $password .= $char; + $i++; + } + } + + return array($devid, $password); + } } -- 2.11.0