Oops
[squirrelmail.git] / functions / options.php
index 24b094effe714a843a9a709a6e4bf3b4f430d4b4..4608d798433f1c5ec0cfef53655e98bb221e6719 100644 (file)
@@ -5,44 +5,13 @@
  *
  * Functions needed to display the options pages.
  *
- * @copyright © 1999-2006 The SquirrelMail Project Team
+ * @copyright © 1999-2007 The SquirrelMail Project Team
  * @license http://opensource.org/licenses/gpl-license.php GNU Public License
  * @version $Id$
  * @package squirrelmail
  * @subpackage prefs
  */
 
-/**********************************************/
-/* Define constants used in the options code. */
-/**********************************************/
-
-/* Define constants for the various option types. */
-define('SMOPT_TYPE_STRING', 0);
-define('SMOPT_TYPE_STRLIST', 1);
-define('SMOPT_TYPE_TEXTAREA', 2);
-define('SMOPT_TYPE_INTEGER', 3);
-define('SMOPT_TYPE_FLOAT', 4);
-define('SMOPT_TYPE_BOOLEAN', 5);
-define('SMOPT_TYPE_HIDDEN', 6);
-define('SMOPT_TYPE_COMMENT', 7);
-define('SMOPT_TYPE_FLDRLIST', 8);
-
-/* Define constants for the options refresh levels. */
-define('SMOPT_REFRESH_NONE', 0);
-define('SMOPT_REFRESH_FOLDERLIST', 1);
-define('SMOPT_REFRESH_ALL', 2);
-
-/* Define constants for the options size. */
-define('SMOPT_SIZE_TINY', 0);
-define('SMOPT_SIZE_SMALL', 1);
-define('SMOPT_SIZE_MEDIUM', 2);
-define('SMOPT_SIZE_LARGE', 3);
-define('SMOPT_SIZE_HUGE', 4);
-define('SMOPT_SIZE_NORMAL', 5);
-
-define('SMOPT_SAVE_DEFAULT', 'save_option');
-define('SMOPT_SAVE_NOOP', 'save_option_noop');
-
 /**
  * SquirrelOption: An option for SquirrelMail.
  *
@@ -50,6 +19,11 @@ define('SMOPT_SAVE_NOOP', 'save_option_noop');
  * @subpackage prefs
  */
 class SquirrelOption {
+    /**
+     * The original option configuration array
+     * @var array
+     */
+    var $raw_option_array;
     /**
      * The name of this setting
      * @var string
@@ -60,6 +34,11 @@ class SquirrelOption {
      * @var string
      */
     var $caption;
+    /**
+     * Whether or not the caption text is allowed to wrap
+     * @var boolean
+     */
+    var $caption_wrap;
     /**
      * The type of INPUT element
      *
@@ -90,6 +69,40 @@ class SquirrelOption {
      * @var string
      */
     var $trailing_text;
+    /**
+     * Text that overrides the "Yes" label for boolean 
+     * radio option widgets
+     *
+     * @var string
+     */
+    var $yes_text;
+    /**
+     * Text that overrides the "No" label for boolean 
+     * radio option widgets
+     *
+     * @var string
+     */
+    var $no_text;
+    /**
+     * Some widgets support more than one layout type
+     *
+     * @var int
+     */
+    var $layout_type;
+    /**
+     * Indicates if the Add widget should be included
+     * with edit lists.
+     *
+     * @var boolean
+     */
+    var $use_add_widget;
+    /**
+     * Indicates if the Delete widget should be included
+     * with edit lists.
+     *
+     * @var boolean
+     */
+    var $use_delete_widget;
     /**
      * text displayed to the user
      *
@@ -98,10 +111,12 @@ class SquirrelOption {
      */
     var $comment;
     /**
-     * additional javascript or other code added to the user input
-     * @var string
+     * additional javascript or other widget attributes added to the 
+     * user input; must be an array where keys are attribute names
+     * ("onclick", etc) and values are the attribute values.
+     * @var array
      */
-    var $script;
+    var $aExtraAttribs;
     /**
      * script (usually Javascript) that will be placed after (outside of)
      * the INPUT tag
@@ -137,13 +152,13 @@ class SquirrelOption {
      * disables html sanitizing.
      *
      * WARNING - don't use it, if user input is possible in option
-     * or use own sanitizing functions. Currently works only with
-     * SMOPT_TYPE_STRLIST.
+     * or use own sanitizing functions. Currently only works for SMOPT_TYPE_STRLIST.
      * @var bool
      */
     var $htmlencoded=false;
     /**
-     * Controls folder list limits in SMOPT_TYPE_FLDRLIST widget.
+     * Controls folder list limits in SMOPT_TYPE_FLDRLIST and
+     * SMOPT_TYPE_FLDRLIST_MULTI widgets.
      * See $flag argument in sqimap_mailbox_option_list() function.
      * @var string
      * @since 1.5.1
@@ -152,6 +167,7 @@ class SquirrelOption {
 
     /**
      * Constructor function
+     * @param array $raw_option_array
      * @param string $name
      * @param string $caption
      * @param integer $type
@@ -161,18 +177,25 @@ class SquirrelOption {
      * @param bool $htmlencoded
      */
     function SquirrelOption
-    ($name, $caption, $type, $refresh_level, $initial_value = '', $possible_values = '', $htmlencoded = false) {
+    ($raw_option_array, $name, $caption, $type, $refresh_level, $initial_value = '', $possible_values = '', $htmlencoded = false) {
         /* Set the basic stuff. */
+        $this->raw_option_array = $raw_option_array;
         $this->name = $name;
         $this->caption = $caption;
+        $this->caption_wrap = TRUE;
         $this->type = $type;
         $this->refresh_level = $refresh_level;
         $this->possible_values = $possible_values;
         $this->htmlencoded = $htmlencoded;
-        $this->size = SMOPT_SIZE_MEDIUM;
+        $this->size = SMOPT_SIZE_NORMAL;
         $this->trailing_text = '';
+        $this->yes_text = '';
+        $this->no_text = '';
         $this->comment = '';
-        $this->script = '';
+        $this->layout_type = 0;
+        $this->use_add_widget = TRUE;
+        $this->use_delete_widget = TRUE;
+        $this->aExtraAttribs = array();
         $this->post_script = '';
 
         //Check for a current value.  
@@ -186,17 +209,27 @@ class SquirrelOption {
 
         /* Check for a new value. */
         if ( !sqgetGlobalVar("new_$name", $this->new_value, SQ_POST ) ) {
-            $this->new_value = '';
+            $this->new_value = NULL;
         }
 
         /* Set the default save function. */
-        if (($type != SMOPT_TYPE_HIDDEN) && ($type != SMOPT_TYPE_COMMENT)) {
+        if ($type != SMOPT_TYPE_HIDDEN
+         && $type != SMOPT_TYPE_INFO
+         && $type != SMOPT_TYPE_COMMENT) {
             $this->save_function = SMOPT_SAVE_DEFAULT;
         } else {
             $this->save_function = SMOPT_SAVE_NOOP;
         }
     }
 
+    /** Convenience function that identifies which types of
+        widgets are stored as (serialized) array values. */
+    function is_multiple_valued() {
+        return ($this->type == SMOPT_TYPE_FLDRLIST_MULTI
+             || $this->type == SMOPT_TYPE_STRLIST_MULTI
+             || $this->type == SMOPT_TYPE_EDIT_LIST);
+    }
+
     /**
      * Set the value for this option.
      * @param mixed $value
@@ -213,6 +246,14 @@ class SquirrelOption {
         $this->new_value = $new_value;
     }
 
+    /**
+     * Set whether the caption is allowed to wrap for this option.
+     * @param boolean $caption_wrap
+     */
+    function setCaptionWrap($caption_wrap) {
+        $this->caption_wrap = $caption_wrap;
+    }
+
     /**
      * Set the size for this option.
      * @param integer $size
@@ -229,6 +270,40 @@ class SquirrelOption {
         $this->trailing_text = $trailing_text;
     }
 
+    /**
+     * Set the yes_text for this option.
+     * @param string $yes_text
+     */
+    function setYesText($yes_text) {
+        $this->yes_text = $yes_text;
+    }
+
+    /**
+     * Set the no_text for this option.
+     * @param string $no_text
+     */
+    function setNoText($no_text) {
+        $this->no_text = $no_text;
+    }
+
+    /* Set the "use add widget" value for this option. */
+    function setUseAddWidget($use_add_widget) {
+        $this->use_add_widget = $use_add_widget;
+    }
+
+    /* Set the "use delete widget" value for this option. */
+    function setUseDeleteWidget($use_delete_widget) {
+        $this->use_delete_widget = $use_delete_widget;
+    }
+
+    /**
+     * Set the layout type for this option.
+     * @param int $layout_type
+     */
+    function setLayoutType($layout_type) {
+        $this->layout_type = $layout_type;
+    }
+
     /**
      * Set the comment for this option.
      * @param string $comment
@@ -238,11 +313,11 @@ class SquirrelOption {
     }
 
     /**
-     * Set the script for this option.
-     * @param string $script
+     * Set the extra attributes for this option.
+     * @param array $aExtraAttribs
      */
-    function setScript($script) {
-        $this->script = $script;
+    function setExtraAttributes($aExtraAttribs) {
+        $this->aExtraAttribs = $aExtraAttribs;
     }
 
     /**
@@ -262,7 +337,7 @@ class SquirrelOption {
     }
 
     /**
-     * Set the trailing_text for this option.
+     * Set the folder_filter for this option.
      * @param string $folder_filter
      * @since 1.5.1
      */
@@ -273,20 +348,25 @@ class SquirrelOption {
     /**
      * Creates fields on option pages according to option type
      *
-     * Function that calls other createWidget* functions.
-     * @return string html formated option field
+     * This is the function that calls all other createWidget* functions.
+     *
+     * @return string The formated option field
+     *
      */
-    function createHTMLWidget() {
+    function createWidget() {
         global $color;
 
         // Use new value if available
-        if (!empty($this->new_value)) {
+        if (!is_null($this->new_value)) {
             $tempValue = $this->value;
             $this->value = $this->new_value;
         }
 
         /* Get the widget for this option type. */
         switch ($this->type) {
+            case SMOPT_TYPE_PASSWORD:
+                $result = $this->createWidget_String(TRUE);
+                break;
             case SMOPT_TYPE_STRING:
                 $result = $this->createWidget_String();
                 break;
@@ -305,6 +385,12 @@ class SquirrelOption {
             case SMOPT_TYPE_BOOLEAN:
                 $result = $this->createWidget_Boolean();
                 break;
+            case SMOPT_TYPE_BOOLEAN_CHECKBOX:
+                $result = $this->createWidget_Boolean(TRUE);
+                break;
+            case SMOPT_TYPE_BOOLEAN_RADIO:
+                $result = $this->createWidget_Boolean(FALSE);
+                break;
             case SMOPT_TYPE_HIDDEN:
                 $result = $this->createWidget_Hidden();
                 break;
@@ -314,29 +400,61 @@ class SquirrelOption {
             case SMOPT_TYPE_FLDRLIST:
                 $result = $this->createWidget_FolderList();
                 break;
+            case SMOPT_TYPE_FLDRLIST_MULTI:
+                $result = $this->createWidget_FolderList(TRUE);
+                break;
+            case SMOPT_TYPE_EDIT_LIST:
+                $result = $this->createWidget_EditList();
+                break;
+            case SMOPT_TYPE_STRLIST_MULTI:
+                $result = $this->createWidget_StrList(TRUE);
+                break;
+            case SMOPT_TYPE_STRLIST_RADIO:
+                $result = $this->createWidget_StrList(FALSE, TRUE);
+                break;
+            case SMOPT_TYPE_SUBMIT:
+                $result = $this->createWidget_Submit();
+                break;
+            case SMOPT_TYPE_INFO:
+                $result = $this->createWidget_Info();
+                break;
             default:
-               $result = '<font color="' . $color[2] . '">'
-                       . sprintf(_("Option Type '%s' Not Found"), $this->type)
-                       . '</font>';
+                error_box ( 
+                    sprintf(_("Option Type '%s' Not Found"), $this->type)
+                    );
         }
 
         /* Add the "post script" for this option. */
         $result .= $this->post_script;
 
         // put correct value back if need be
-        if (!empty($this->new_value)) {
+        if (!is_null($this->new_value)) {
             $this->value = $tempValue;
         }
 
         /* Now, return the created widget. */
-        return ($result);
+        return $result;
+    }
+
+    /**
+     * Creates info block
+     * @return string html formated output
+     */
+    function createWidget_Info() {
+        return sq_htmlspecialchars($this->value);
     }
 
     /**
      * Create string field
+     *
+     * @param boolean $password When TRUE, the text in the input
+     *                          widget will be obscured (OPTIONAL;
+     *                          default = FALSE).
+     *
      * @return string html formated option field
+     *
      */
-    function createWidget_String() {
+    function createWidget_String($password=FALSE) {
         switch ($this->size) {
             case SMOPT_SIZE_TINY:
                 $width = 5;
@@ -355,86 +473,140 @@ class SquirrelOption {
                 $width = 25;
         }
 
-        $result = "<input type=\"text\" name=\"new_$this->name\" value=\"" .
-            htmlspecialchars($this->value) .
-            "\" size=\"$width\" $this->script />$this->trailing_text\n";
-        return ($result);
+//TODO: might be better to have a separate template file for all widgets, because then the layout of the widget and the "trailing text" can be customized - they are still hard coded here
+        if ($password)
+            return addPwField('new_' . $this->name, $this->value, $width, 0, $this->aExtraAttribs) . ' ' . htmlspecialchars($this->trailing_text);
+        else
+            return addInput('new_' . $this->name, $this->value, $width, 0, $this->aExtraAttribs) . ' ' . htmlspecialchars($this->trailing_text);
     }
 
     /**
-     * Create selection box
-     * @return string html formated selection box
+     * Create selection box or radio button group
+     *
+     * When $this->htmlencoded is TRUE, the keys and values in 
+     * $this->possible_values are assumed to be display-safe.  
+     * Use with care!
+     *
+     * Note that when building radio buttons instead of a select
+     * widget, if the "size" attribute is SMOPT_SIZE_TINY, the
+     * radio buttons will be output one after another without
+     * linebreaks between them.  Otherwise, each radio button
+     * goes on a line of its own.
+     *
+     * @param boolean $multiple_select When TRUE, the select widget
+     *                                 will allow multiple selections
+     *                                 (OPTIONAL; default is FALSE
+     *                                 (single select list))
+     * @param boolean $radio_buttons   When TRUE, the widget will
+     *                                 instead be built as a group
+     *                                 of radio buttons (and
+     *                                 $multiple_select will be
+     *                                 forced to FALSE) (OPTIONAL;
+     *                                 default is FALSE (select widget))
+     *
+     * @return string html formated selection box or radio buttons
+     *
      */
-    function createWidget_StrList() {
-        /* Begin the select tag. */
-        $result = "<select name=\"new_$this->name\" $this->script>\n";
-
-        /* Add each possible value to the select list. */
-        foreach ($this->possible_values as $real_value => $disp_value) {
-            /* Start the next new option string. */
-            $new_option = '<option value="' .
-                ($this->htmlencoded ? $real_value : htmlspecialchars($real_value)) . '"';
-
-            /* If this value is the current value, select it. */
-            if ($real_value == $this->value) {
-               $new_option .= ' selected="selected"';
+    function createWidget_StrList($multiple_select=FALSE, $radio_buttons=FALSE) {
+//FIXME: Currently, $this->htmlencoded is ignored here -- was removed when changing to template-based output; a fix is available as part of proposed centralized sanitizing patch
+
+        // radio buttons instead of select widget?
+        //
+        if ($radio_buttons) {
+
+            global $br, $nbsp;
+            $result = '';
+            foreach ($this->possible_values as $real_value => $disp_value) {
+                $result .= addRadioBox('new_' . $this->name, ($this->value == $real_value), $real_value, array_merge(array('id' => 'new_' . $this->name . '_' . $real_value), $this->aExtraAttribs)) . $nbsp . create_label($disp_value, 'new_' . $this->name . '_' . $real_value);
+                if ($this->size != SMOPT_SIZE_TINY)
+                    $result .= $br;
             }
 
-            /* Add the display value to our option string. */
-            $new_option .= '>' . ($this->htmlencoded ? $disp_value : htmlspecialchars($disp_value)) . "</option>\n";
+            return $result;
+        }
+
 
-            /* And add the new option string to our select tag. */
-            $result .= $new_option;
+        // everything below applies to select widgets
+        //
+        switch ($this->size) {
+//FIXME: not sure about these sizes... seems like we could add another on the "large" side...
+            case SMOPT_SIZE_TINY:
+                $height = 3;
+                break;
+            case SMOPT_SIZE_SMALL:
+                $height = 8;
+                break;
+            case SMOPT_SIZE_LARGE:
+                $height = 15;
+                break;
+            case SMOPT_SIZE_HUGE:
+                $height = 25;
+                break;
+            case SMOPT_SIZE_NORMAL:
+            default:
+                $height = 5;
         }
 
-        /* Close the select tag and return our happy result. */
-        $result .= "</select>$this->trailing_text\n";
-        return ($result);
+        return addSelect('new_' . $this->name, $this->possible_values, $this->value, TRUE, $this->aExtraAttribs, $multiple_select, $height, !$this->htmlencoded) . htmlspecialchars($this->trailing_text);
+
     }
 
     /**
      * Create folder selection box
+     *
+     * @param boolean $multiple_select When TRUE, the select widget 
+     *                                 will allow multiple selections
+     *                                 (OPTIONAL; default is FALSE 
+     *                                 (single select list))
+     *
      * @return string html formated selection box
+     *
      */
-    function createWidget_FolderList() {
-        $selected = array(strtolower($this->value));
+    function createWidget_FolderList($multiple_select=FALSE) {
 
-        /* set initial value */
-        $result = '';
+        switch ($this->size) {
+//FIXME: not sure about these sizes... seems like we could add another on the "large" side...
+            case SMOPT_SIZE_TINY:
+                $height = 3;
+                break;
+            case SMOPT_SIZE_SMALL:
+                $height = 8;
+                break;
+            case SMOPT_SIZE_LARGE:
+                $height = 15;
+                break;
+            case SMOPT_SIZE_HUGE:
+                $height = 25;
+                break;
+            case SMOPT_SIZE_NORMAL:
+            default:
+                $height = 5;
+        }
 
-        /* Add each possible value to the select list. */
-        foreach ($this->possible_values as $real_value => $disp_value) {
-            if ( is_array($disp_value) ) {
-              /* For folder list, we passed in the array of boxes.. */
-              $new_option = sqimap_mailbox_option_list(0, $selected, 0, $disp_value, $this->folder_filter);
-            } else {
-              /* Start the next new option string. */
-              $new_option = '<option value="' . htmlspecialchars($real_value) . '"';
+        // possible values might include a nested array of 
+        // possible values (list of folders)
+        //
+        $option_list = array();
+        foreach ($this->possible_values as $value => $text) {
 
-              /* If this value is the current value, select it. */
-              if ($real_value == $this->value) {
-                 $new_option .= ' selected="selected"';
-              }
+            // list of folders (boxes array)
+            //
+            if (is_array($text)) {
+              $option_list = array_merge($option_list, sqimap_mailbox_option_array(0, 0, $text, $this->folder_filter));
 
-              /* Add the display value to our option string. */
-              $new_option .= '>' . htmlspecialchars($disp_value) . "</option>\n";
+            // just one option here
+            //
+            } else {
+              $option_list = array_merge($option_list, array($value => $text));
             }
-            /* And add the new option string to our select tag. */
-            $result .= $new_option;
+
         }
+        if (empty($option_list))
+            $option_list = array('ignore' => _("unavailable"));
 
 
-        if (empty($result)) {
-            // string is displayed when interface can't build folder selection box
-            return _("unavailable");
-        } else {
-            /* Begin the select tag. */
-            $ret = "<select name=\"new_$this->name\" $this->script>\n";
-            $ret.= $result;
-            /* Close the select tag and return our happy result. */
-            $ret.= "</select>\n";
-            return ($ret);
-        }
+        return addSelect('new_' . $this->name, $option_list, $this->value, TRUE, $this->aExtraAttribs, $multiple_select, $height) . htmlspecialchars($this->trailing_text);
+
     }
 
     /**
@@ -450,8 +622,6 @@ class SquirrelOption {
             case SMOPT_SIZE_NORMAL:
             default: $rows = 5; $cols =  50;
         }
-//FIXME: we need to change $this->script into $this->aExtraAttribs, and anyone who wants to add some javascript or other attributes to an options widget can put them in an array and pass them as extra attributes (key == attrib name, value == attrib value).... for now, this is the only place it is used, and there is no place in the code that text areas get extra attribs or javascript... in fact the only place that was using $this->script is include/options/display.php:200, so that's easy to change.... just have to go through this file and change all the places that use "script"
-$this->aExtraAttribs = array();
         return addTextArea('new_' . $this->name, $this->value, $cols, $rows, $this->aExtraAttribs);
     }
 
@@ -466,12 +636,12 @@ $this->aExtraAttribs = array();
         // add onChange javascript handler to a regular string widget
         // which will strip out all non-numeric chars
         if (checkForJavascript())
-           return preg_replace('/\/>/', ' onChange="origVal=this.value; newVal=\'\'; '
+           $this->aExtraAttribs['onchange'] = 'origVal=this.value; newVal=\'\'; '
                     . 'for (i=0;i<origVal.length;i++) { if (origVal.charAt(i)>=\'0\' '
                     . '&& origVal.charAt(i)<=\'9\') newVal += origVal.charAt(i); } '
-                    . 'this.value=newVal;" />', $this->createWidget_String());
-        else
-           return $this->createWidget_String();
+                    . 'this.value=newVal;';
+
+        return $this->createWidget_String();
     }
 
     /**
@@ -484,43 +654,56 @@ $this->aExtraAttribs = array();
         // add onChange javascript handler to a regular string widget
         // which will strip out all non-numeric (period also OK) chars
         if (checkForJavascript())
-           return preg_replace('/\/>/', ' onChange="origVal=this.value; newVal=\'\'; '
+           $this->aExtraAttribs['onchange'] = 'origVal=this.value; newVal=\'\'; '
                     . 'for (i=0;i<origVal.length;i++) { if ((origVal.charAt(i)>=\'0\' '
                     . '&& origVal.charAt(i)<=\'9\') || origVal.charAt(i)==\'.\') '
-                    . 'newVal += origVal.charAt(i); } this.value=newVal;" />'
-                , $this->createWidget_String());
-        else
-           return $this->createWidget_String();
+                    . 'newVal += origVal.charAt(i); } this.value=newVal;';
+
+        return $this->createWidget_String();
     }
 
     /**
-     * Creates radio field (yes/no)
-     * @return string html formated radio field
+     * Create boolean widget
+     *
+     * When creating Yes/No radio buttons, the "yes_text"
+     * and "no_text" option attributes are used to override
+     * the typical "Yes" and "No" text.
+     *
+     * @param boolean $checkbox When TRUE, the widget will be
+     *                          constructed as a checkbox,
+     *                          otherwise it will be a set of
+     *                          Yes/No radio buttons (OPTIONAL;
+     *                          default is TRUE (checkbox)).
+     *
+     * @return string html formated boolean widget
+     *
      */
-    function createWidget_Boolean() {
-        /* Do the whole current value thing. */
-        if ($this->value != SMPREF_NO) {
-            $yes_chk = ' checked="checked"';
-            $no_chk = '';
-        } else {
-            $yes_chk = '';
-            $no_chk = ' checked="checked"';
+    function createWidget_Boolean($checkbox=TRUE) {
+
+        global $oTemplate, $nbsp;
+
+
+        // checkbox...
+        //
+        if ($checkbox) {
+            $result = addCheckbox('new_' . $this->name, ($this->value != SMPREF_NO), SMPREF_YES, array_merge(array('id' => 'new_' . $this->name), $this->aExtraAttribs)) . $nbsp . create_label($this->trailing_text, 'new_' . $this->name);
         }
 
-        /* Build the yes choice. */
-        $yes_option = '<input type="radio" id="new_' . $this->name . '_yes" '
-                    . 'name="new_' . $this->name . '" value="' . SMPREF_YES . '"'
-                    . $yes_chk . ' ' . $this->script . ' />&nbsp;'
-                    . '<label for="new_'.$this->name.'_yes">' . _("Yes") . '</label>';
+        // radio buttons...
+        //
+        else {
+
+            /* Build the yes choice. */
+            $yes_option = addRadioBox('new_' . $this->name, ($this->value != SMPREF_NO), SMPREF_YES, array_merge(array('id' => 'new_' . $this->name . '_yes'), $this->aExtraAttribs)) . $nbsp . create_label((!empty($this->yes_text) ? $this->yes_text : _("Yes")), 'new_' . $this->name . '_yes');
+
+            /* Build the no choice. */
+            $no_option = addRadioBox('new_' . $this->name, ($this->value == SMPREF_NO), SMPREF_NO, array_merge(array('id' => 'new_' . $this->name . '_no'), $this->aExtraAttribs)) . $nbsp . create_label((!empty($this->no_text) ? $this->no_text : _("No")), 'new_' . $this->name . '_no');
 
-        /* Build the no choice. */
-        $no_option = '<input type="radio" id="new_' . $this->name . '_no" '
-                   . 'name="new_' . $this->name . '" value="' . SMPREF_NO . '"'
-                   . $no_chk . ' ' . $this->script . ' />&nbsp;'
-                    . '<label for="new_'.$this->name.'_no">' . _("No") . '</label>';
+            /* Build the combined "boolean widget". */
+            $result = "$yes_option$nbsp$nbsp$nbsp$nbsp$no_option";
+
+        }
 
-        /* Build and return the combined "boolean widget". */
-        $result = "$yes_option&nbsp;&nbsp;&nbsp;&nbsp;$no_option";
         return ($result);
     }
 
@@ -529,10 +712,7 @@ $this->aExtraAttribs = array();
      * @return string html formated hidden input field
      */
     function createWidget_Hidden() {
-        $result = '<input type="hidden" name="new_' . $this->name
-                . '" value="' . htmlspecialchars($this->value)
-                . '" ' . $this->script . ' />';
-        return ($result);
+        return addHidden('new_' . $this->name, $this->value, $this->aExtraAttribs);
     }
 
     /**
@@ -544,6 +724,78 @@ $this->aExtraAttribs = array();
         return ($result);
     }
 
+    /**
+     * Creates an edit list
+     *
+     * Note that multiple layout types are supported for this widget.
+     * $this->layout_type must be one of the SMOPT_EDIT_LIST_LAYOUT_*
+     * constants.
+     *
+     * @return string html formated list of edit fields and
+     *                their associated controls
+     */
+    function createWidget_EditList() {
+
+        global $oTemplate;
+
+        switch ($this->size) {
+            case SMOPT_SIZE_TINY:
+                $height = 3;
+                break;
+            case SMOPT_SIZE_SMALL:
+                $height = 8;
+                break;
+            case SMOPT_SIZE_MEDIUM:
+                $height = 15;
+                break;
+            case SMOPT_SIZE_LARGE:
+                $height = 25;
+                break;
+            case SMOPT_SIZE_HUGE:
+                $height = 40;
+                break;
+            case SMOPT_SIZE_NORMAL:
+            default:
+                $height = 5;
+        }
+
+        if (empty($this->possible_values)) $this->possible_values = array();
+        if (!is_array($this->possible_values)) $this->possible_values = array($this->possible_values);
+
+//FIXME: $this->aExtraAttribs probably should only be used in one place
+        $oTemplate->assign('input_widget', addInput('add_' . $this->name, '', 38, 0, $this->aExtraAttribs));
+        $oTemplate->assign('use_input_widget', $this->use_add_widget);
+        $oTemplate->assign('use_delete_widget', $this->use_delete_widget);
+
+        $oTemplate->assign('trailing_text', $this->trailing_text);
+        $oTemplate->assign('possible_values', $this->possible_values);
+        $oTemplate->assign('select_widget', addSelect('new_' . $this->name, $this->possible_values, $this->value, FALSE, !checkForJavascript() ? $this->aExtraAttribs : array_merge(array('onchange' => 'if (typeof(window.addinput_' . $this->name . ') == \'undefined\') { var f = document.forms.length; var i = 0; var pos = -1; while( pos == -1 && i < f ) { var e = document.forms[i].elements.length; var j = 0; while( pos == -1 && j < e ) { if ( document.forms[i].elements[j].type == \'text\' && document.forms[i].elements[j].name == \'add_' . $this->name . '\' ) { pos = j; } j++; } i++; } if( pos >= 0 ) { window.addinput_' . $this->name . ' = document.forms[i-1].elements[pos]; } } for (x = 0; x < this.length; x++) { if (this.options[x].selected) { window.addinput_' . $this->name . '.value = this.options[x].value; break; } }'), $this->aExtraAttribs), TRUE, $height));
+        $oTemplate->assign('checkbox_widget', addCheckBox('delete_' . $this->name, FALSE, SMPREF_YES, array_merge(array('id' => 'delete_' . $this->name), $this->aExtraAttribs)));
+        $oTemplate->assign('name', $this->name);
+
+        switch ($this->layout_type) {
+            case SMOPT_EDIT_LIST_LAYOUT_SELECT:
+                return $oTemplate->fetch('edit_list_widget.tpl');
+            case SMOPT_EDIT_LIST_LAYOUT_LIST:
+                return $oTemplate->fetch('edit_list_widget_list_style.tpl');
+            default:
+                error_box(sprintf(_("Edit List Layout Type '%s' Not Found"), $layout_type));
+        }
+
+    }
+
+    /**
+     * Creates a submit button
+     *
+     * @return string html formated submit button widget
+     *
+     */
+    function createWidget_Submit() {
+
+        return addSubmit($this->comment, $this->name, $this->aExtraAttribs) . htmlspecialchars($this->trailing_text);
+
+    }
+
     /**
      *
      */
@@ -556,21 +808,89 @@ $this->aExtraAttribs = array();
      *
      */
     function changed() {
+
+        // edit lists have a lot going on, so we'll always process them
+        //
+        if ($this->type == SMOPT_TYPE_EDIT_LIST) return TRUE;
+
         return ($this->value != $this->new_value);
     }
 } /* End of SquirrelOption class*/
 
 /**
- * Saves option
+ * Saves the option value (this is the default save function
+ * unless overridden by the user)
+ *
  * @param object $option object that holds option name and new_value
  */
 function save_option($option) {
+
+    // Can't save the pref if we don't have the username
+    //
     if ( !sqgetGlobalVar('username', $username, SQ_SESSION ) ) {
-        /* Can't save the pref if we don't have the username */
         return;
     }
+
     global $data_dir;
-    setPref($data_dir, $username, $option->name, $option->new_value);
+
+    // edit lists: first add new elements to list, then
+    // remove any selected ones (note that we must add
+    // before deleting because the javascript that populates
+    // the "add" textbox when selecting items in the list
+    // (for deletion))
+    //
+    if ($option->type == SMOPT_TYPE_EDIT_LIST) {
+
+        if (empty($option->possible_values)) $option->possible_values = array();
+        if (!is_array($option->possible_values)) $option->possible_values = array($option->possible_values);
+
+        // add element if given
+        //
+        if ((isset($option->use_add_widget) && $option->use_add_widget)
+         && sqGetGlobalVar('add_' . $option->name, $new_element, SQ_POST)) {
+            $new_element = trim($new_element);
+            if (!empty($new_element)
+             && !in_array($new_element, $option->possible_values))
+                $option->possible_values[] = $new_element;
+        }
+        
+        // delete selected elements if needed
+        //
+        if ((isset($option->use_delete_widget) && $option->use_delete_widget)
+         && is_array($option->new_value)
+         && sqGetGlobalVar('delete_' . $option->name, $ignore, SQ_POST))
+            $option->possible_values = array_diff($option->possible_values, $option->new_value);
+
+        // save full list (stored in "possible_values")
+        //
+        setPref($data_dir, $username, $option->name, serialize($option->possible_values));
+
+    // Certain option types need to be serialized because
+    // they are not scalar
+    //
+    } else if ($option->is_multiple_valued())
+        setPref($data_dir, $username, $option->name, serialize($option->new_value));
+
+    // Checkboxes, when unchecked, don't submit anything in
+    // the POST, so set to SMPREF_OFF if not found
+    //
+    else if (($option->type == SMOPT_TYPE_BOOLEAN
+           || $option->type == SMOPT_TYPE_BOOLEAN_CHECKBOX)
+          && empty($option->new_value)) 
+        setPref($data_dir, $username, $option->name, SMPREF_OFF);
+
+    else
+        setPref($data_dir, $username, $option->name, $option->new_value);
+
+
+    // if a checkbox or multi select is zeroed/cleared out, it
+    // needs to have an empty value pushed into its "new_value" slot
+    //
+    if (($option->type == SMOPT_TYPE_STRLIST_MULTI
+      || $option->type == SMOPT_TYPE_BOOLEAN_CHECKBOX)
+     && is_null($option->new_value))
+        $option->new_value = '';
+
 }
 
 /**
@@ -587,7 +907,7 @@ function save_option_noop($option) {
  * @return string html formated hidden input field
  */
 function create_optpage_element($optpage) {
-    return create_hidden_element('optpage', $optpage);
+    return addHidden('optpage', $optpage);
 }
 
 /**
@@ -596,20 +916,7 @@ function create_optpage_element($optpage) {
  * @return string html formated hidden input field
  */
 function create_optmode_element($optmode) {
-    return create_hidden_element('optmode', $optmode);
-}
-
-/**
- * Create hidden field.
- * @param string $name field name
- * @param string $value field value
- * @return string html formated hidden input field
- */
-function create_hidden_element($name, $value) {
-    $result = '<input type="hidden" '
-            . 'name="' . $name . '" '
-            . 'value="' . htmlspecialchars($value) . '" />';
-    return ($result);
+    return addHidden('optmode', $optmode);
 }
 
 /**
@@ -633,6 +940,7 @@ function create_option_groups($optgrps, $optvals) {
         foreach ($grpopts as $optset) {
             /* Create a new option with all values given. */
             $next_option = new SquirrelOption(
+                    $optset,
                     $optset['name'],
                     $optset['caption'],
                     $optset['type'],
@@ -642,6 +950,11 @@ function create_option_groups($optgrps, $optvals) {
                     (isset($optset['htmlencoded']) ? $optset['htmlencoded'] : false)
                     );
 
+            /* If provided, set if the caption is allowed to wrap for this option. */
+            if (isset($optset['caption_wrap'])) {
+                $next_option->setCaptionWrap($optset['caption_wrap']);
+            }
+
             /* If provided, set the size for this option. */
             if (isset($optset['size'])) {
                 $next_option->setSize($optset['size']);
@@ -652,6 +965,31 @@ function create_option_groups($optgrps, $optvals) {
                 $next_option->setTrailingText($optset['trailing_text']);
             }
 
+            /* If provided, set the yes_text for this option. */
+            if (isset($optset['yes_text'])) {
+                $next_option->setYesText($optset['yes_text']);
+            }
+
+            /* If provided, set the no_text for this option. */
+            if (isset($optset['no_text'])) {
+                $next_option->setNoText($optset['no_text']);
+            }
+
+            /* If provided, set the layout type for this option. */
+            if (isset($optset['layout_type'])) {
+                $next_option->setLayoutType($optset['layout_type']);
+            }
+
+            /* If provided, set the use_add_widget value for this option. */
+            if (isset($optset['use_add_widget'])) {
+                $next_option->setUseAddWidget($optset['use_add_widget']);
+            }
+
+            /* If provided, set the use_delete_widget value for this option. */
+            if (isset($optset['use_delete_widget'])) {
+                $next_option->setUseDeleteWidget($optset['use_delete_widget']);
+            }
+
             /* If provided, set the comment for this option. */
             if (isset($optset['comment'])) {
                 $next_option->setComment($optset['comment']);
@@ -662,9 +1000,9 @@ function create_option_groups($optgrps, $optvals) {
                 $next_option->setSaveFunction($optset['save']);
             }
 
-            /* If provided, set the script for this option. */
-            if (isset($optset['script'])) {
-                $next_option->setScript($optset['script']);
+            /* If provided, set the extra attributes for this option. */
+            if (isset($optset['extra_attributes'])) {
+                $next_option->setExtraAttributes($optset['extra_attributes']);
             }
 
             /* If provided, set the "post script" for this option. */
@@ -686,4 +1024,3 @@ function create_option_groups($optgrps, $optvals) {
     return ($result);
 }
 
-// vim: et ts=4