Shout: Complete device editing form & processing
authorBen Klang <ben@alkaloid.net>
Sat, 2 Jan 2010 04:58:12 +0000 (23:58 -0500)
committerBen Klang <ben@alkaloid.net>
Sat, 2 Jan 2010 04:58:12 +0000 (23:58 -0500)
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
shout/lib/Driver.php
shout/lib/Driver/Sql.php
shout/lib/Forms/DeviceForm.php
shout/lib/Shout.php

index cac6319..924b094 100644 (file)
@@ -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 "<br>\n";
 
 require SHOUT_TEMPLATES . '/devices/' . $action . '.inc';
 
index 61bb56a..6eac8df 100644 (file)
@@ -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 <type> $context
+     * @param <type> $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."));
         }
     }
 
index 6358366..7bc69b8 100644 (file)
@@ -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();
         }
     }
-    
+
 }
index 951738c..bd8d9f3 100644 (file)
@@ -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
index 2dc4ebb..bce4144 100644 (file)
@@ -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);
+    }
 }