From e642c5508620bf90f32e9bb09dad34cf6c31e3d6 Mon Sep 17 00:00:00 2001
From: Jared Hancock <jared@osticket.com>
Date: Thu, 4 Dec 2014 11:14:25 -0600
Subject: [PATCH] forms: Implement more granular visibility settings
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Allow fields to be configured for view / edit / required for both agents and
end users. Fields can also be disabled now so that the field remains in the
form but is no longer displayed for new entries.

Allow tickets to be created without a subject — use the help topic full name
instead.
---
 account.php                                   |   2 +-
 include/Spyc.php                              |   2 +
 include/ajax.forms.php                        |  26 ++-
 include/class.auth.php                        |   2 +-
 include/class.config.php                      |   5 -
 include/class.dynamic_forms.php               | 137 +++++++++++-----
 include/class.forms.php                       |   5 +
 include/class.ticket.php                      |  23 ++-
 .../client/templates/dynamic-form.tmpl.php    |  14 +-
 include/client/view.inc.php                   |   8 +-
 include/i18n/en_US/form.yaml                  |  68 +++-----
 include/i18n/en_US/list.yaml                  |   6 +-
 include/staff/dynamic-form.inc.php            |  16 +-
 include/staff/settings-tickets.inc.php        |   8 -
 .../templates/dynamic-field-config.tmpl.php   | 152 ++++++++++++++++++
 include/staff/templates/dynamic-form.tmpl.php |   6 +-
 tickets.php                                   |   4 +-
 17 files changed, 345 insertions(+), 139 deletions(-)

diff --git a/account.php b/account.php
index 97c8b5efd..81f542eb2 100644
--- a/account.php
+++ b/account.php
@@ -58,7 +58,7 @@ elseif ($_POST) {
         $user_form->getField('email')->value = $thisclient->getEmail();
     }
 
-    if (!$user_form->isValid(function($f) { return !$f->get('private'); }))
+    if (!$user_form->isValid(function($f) { return !$f->isVisibleToUsers(); }))
         $errors['err'] = __('Incomplete client information');
     elseif (!$_POST['backend'] && !$_POST['passwd1'])
         $errors['passwd1'] = __('New password is required');
diff --git a/include/Spyc.php b/include/Spyc.php
index 1c0fc31a8..e92a4bece 100644
--- a/include/Spyc.php
+++ b/include/Spyc.php
@@ -617,6 +617,8 @@ class Spyc {
 
     if (is_numeric($value)) {
       if ($value === '0') return 0;
+      if (stripos($value, '0x') === 0)
+        $value = hexdec($value);
       if (rtrim ($value, 0) === $value)
         $value = (float)$value;
       return $value;
diff --git a/include/ajax.forms.php b/include/ajax.forms.php
index e53b91348..6c35f1bb2 100644
--- a/include/ajax.forms.php
+++ b/include/ajax.forms.php
@@ -48,7 +48,31 @@ class DynamicFormsAjaxAPI extends AjaxController {
     }
 
     function saveFieldConfiguration($field_id) {
-        $field = DynamicFormField::lookup($field_id);
+        if (!($field = DynamicFormField::lookup($field_id)))
+            Http::response(404, 'No such field');
+
+        $DFF = 'DynamicFormField';
+
+        // Capture flags which should remain unchanged
+        $p_mask = $DFF::MASK_MASK_ALL;
+        if ($field->isPrivacyForced()) {
+            $p_mask |= $DFF::FLAG_CLIENT_VIEW | $DFF::FLAG_AGENT_VIEW;
+        }
+        if ($field->isRequirementForced()) {
+            $p_mask |= $DFF::FLAG_CLIENT_REQUIRED | $DFF::FLAG_AGENT_REQUIRED;
+        }
+        if ($field->hasFlag($DFF::FLAG_MASK_DISABLE)) {
+            $p_mask |= $DFF::FLAG_ENABLED;
+        }
+
+        // Capture current state of immutable flags
+        $preserve = $field->flags & $p_mask;
+
+        // Set admin-configured flag states
+        $flags = array_reduce($_POST['flags'],
+            function($a, $b) { return $a | $b; }, 0);
+        $field->flags = $flags | $preserve;
+
         if (!$field->setConfiguration()) {
             include STAFFINC_DIR . 'templates/dynamic-field-config.tmpl.php';
             return;
diff --git a/include/class.auth.php b/include/class.auth.php
index fb055a5c3..e6fed10c5 100644
--- a/include/class.auth.php
+++ b/include/class.auth.php
@@ -130,7 +130,7 @@ class ClientCreateRequest {
         if ($bk->supportsInteractiveAuthentication())
             // User can only be authenticated against this backend
             $defaults['backend'] = $bk::$id;
-        if ($this_form->isValid(function($f) { return !$f->get('private'); })
+        if ($this_form->isValid(function($f) { return !$f->isVisibleToUsers(); })
                 && ($U = User::fromVars($this_form->getClean()))
                 && ($acct = ClientAccount::createForUser($U, $defaults))
                 // Confirm and save the account
diff --git a/include/class.config.php b/include/class.config.php
index d47d4144f..164171a92 100644
--- a/include/class.config.php
+++ b/include/class.config.php
@@ -156,7 +156,6 @@ class OsticketConfig extends Config {
         'auto_claim_tickets'=>  true,
         'system_language' =>    'en_US',
         'default_storage_bk' => 'D',
-        'allow_client_updates' => false,
         'message_autoresponder_collabs' => true,
         'add_email_collabs' => true,
         'clients_only' => false,
@@ -350,10 +349,6 @@ class OsticketConfig extends Config {
         return $this->get('enable_html_thread');
     }
 
-    function allowClientUpdates() {
-        return $this->get('allow_client_updates');
-    }
-
     function getClientTimeout() {
         return $this->getClientSessionTimeout();
     }
diff --git a/include/class.dynamic_forms.php b/include/class.dynamic_forms.php
index 4bed6b6fd..3620cc1e8 100644
--- a/include/class.dynamic_forms.php
+++ b/include/class.dynamic_forms.php
@@ -423,14 +423,27 @@ class DynamicFormField extends VerySimpleModel {
 
     var $_field;
 
-    const REQUIRE_NOBODY = 0;
-    const REQUIRE_EVERYONE = 1;
-    const REQUIRE_ENDUSER = 2;
-    const REQUIRE_AGENT = 3;
+    const FLAG_ENABLED          = 0x0001;
+    const FLAG_STORABLE         = 0x0002;
+    const FLAG_CLOSE_REQUIRED   = 0x0004;
 
-    const VISIBLE_EVERYONE = 0;
-    const VISIBLE_AGENTONLY = 1;
-    const VISIBLE_ENDUSERONLY = 2;
+    const FLAG_MASK_CHANGE      = 0x0010;
+    const FLAG_MASK_DELETE      = 0x0020;
+    const FLAG_MASK_EDIT        = 0x0040;
+    const FLAG_MASK_DISABLE     = 0x0080;
+    const FLAG_MASK_REQUIRE     = 0x10000;
+    const FLAG_MASK_VIEW        = 0x20000;
+    const FLAG_MASK_NAME        = 0x40000;
+
+    const MASK_MASK_ALL         = 0x700F0;
+
+    const FLAG_CLIENT_VIEW      = 0x0100;
+    const FLAG_CLIENT_EDIT      = 0x0200;
+    const FLAG_CLIENT_REQUIRED  = 0x0400;
+
+    const FLAG_AGENT_VIEW       = 0x1000;
+    const FLAG_AGENT_EDIT       = 0x2000;
+    const FLAG_AGENT_REQUIRED   = 0x4000;
 
     // Multiple inheritance -- delegate to FormField
     function __call($what, $args) {
@@ -481,24 +494,61 @@ class DynamicFormField extends VerySimpleModel {
     }
 
     function isDeletable() {
-        return (($this->get('edit_mask') & 1) == 0);
+        return !$this->hasFlag(self::FLAG_MASK_DELETE);
     }
     function isNameForced() {
-        return $this->get('edit_mask') & 2;
+        return $this->hasFlag(self::FLAG_MASK_NAME);
     }
     function isPrivacyForced() {
-        return $this->get('edit_mask') & 4;
+        return $this->hasFlag(self::FLAG_MASK_VIEW);
     }
     function isRequirementForced() {
-        return $this->get('edit_mask') & 8;
+        return $this->hasFlag(self::FLAG_MASK_REQUIRE);
     }
 
     function  isChangeable() {
-        return (($this->get('edit_mask') & 16) == 0);
+        return $this->hasFlag(self::FLAG_MASK_CHANGE);
     }
 
     function  isEditable() {
-        return (($this->get('edit_mask') & 32) == 0);
+        return $this->hasFlag(self::FLAG_MASK_EDIT);
+    }
+
+    function hasFlag($flag) {
+        return ($this->flags & $flag) != 0;
+    }
+
+    function getVisibilityDescription() {
+        $F = $this->flags;
+
+        if (!$this->hasFlag(self::FLAG_ENABLED))
+            return __('Disabled');
+
+        $impl = $this->getImpl();
+
+        $hints = array();
+        $VIEW = self::FLAG_CLIENT_VIEW | self::FLAG_AGENT_VIEW;
+        if (($F & $VIEW) == 0) {
+            $hints[] = __('Hidden');
+        }
+        elseif (~$F & self::FLAG_CLIENT_VIEW) {
+            $hints[] = __('Internal');
+        }
+        elseif (~$F & self::FLAG_AGENT_VIEW) {
+            $hints[] = __('For EndUsers Only');
+        }
+        if ($impl->hasData()) {
+            if (~$F & (self::FLAG_CLIENT_REQUIRED | self::FLAG_AGENT_REQUIRED)) {
+                $hints[] = __('Optional');
+            }
+            else {
+                $hints[] = __('Required');
+            }
+            if (!($F & (self::FLAG_CLIENT_EDIT | self::FLAG_AGENT_EDIT))) {
+                $hints[] = __('Immutable');
+            }
+        }
+        return implode(', ', $hints);
     }
     function getTranslateTag($subtag) {
         return _H(sprintf('field.%s.%s', $subtag, $this->id));
@@ -512,19 +562,28 @@ class DynamicFormField extends VerySimpleModel {
     function allRequirementModes() {
         return array(
             'a' => array('desc' => __('Optional'),
-                'private' => self::VISIBLE_EVERYONE, 'required' => self::REQUIRE_NOBODY),
+                'flags' => self::FLAG_CLIENT_VIEW | self::FLAG_AGENT_VIEW
+                    | self::FLAG_CLIENT_EDIT | self::FLAG_AGENT_EDIT),
             'b' => array('desc' => __('Required'),
-                'private' => self::VISIBLE_EVERYONE, 'required' => self::REQUIRE_EVERYONE),
+                'flags' => self::FLAG_CLIENT_VIEW | self::FLAG_AGENT_VIEW
+                    | self::FLAG_CLIENT_EDIT | self::FLAG_AGENT_EDIT
+                    | self::FLAG_CLIENT_REQUIRED | self::FLAG_AGENT_REQUIRED),
             'c' => array('desc' => __('Required for EndUsers'),
-                'private' => self::VISIBLE_EVERYONE, 'required' => self::REQUIRE_ENDUSER),
+                'flags' => self::FLAG_CLIENT_VIEW | self::FLAG_AGENT_VIEW
+                    | self::FLAG_CLIENT_EDIT | self::FLAG_AGENT_EDIT
+                    | self::FLAG_CLIENT_REQUIRED),
             'd' => array('desc' => __('Required for Agents'),
-                'private' => self::VISIBLE_EVERYONE, 'required' => self::REQUIRE_AGENT),
+                'flags' => self::FLAG_CLIENT_VIEW | self::FLAG_AGENT_VIEW
+                    | self::FLAG_CLIENT_EDIT | self::FLAG_AGENT_EDIT
+                    | self::FLAG_AGENT_REQUIRED),
             'e' => array('desc' => __('Internal, Optional'),
-                'private' => self::VISIBLE_AGENTONLY, 'required' => self::REQUIRE_NOBODY),
+                'flags' => self::FLAG_AGENT_VIEW | self::FLAG_AGENT_EDIT),
             'f' => array('desc' => __('Internal, Required'),
-                'private' => self::VISIBLE_AGENTONLY, 'required' => self::REQUIRE_EVERYONE),
+                'flags' => self::FLAG_AGENT_VIEW | self::FLAG_AGENT_EDIT
+                    | self::FLAG_AGENT_REQUIRED),
             'g' => array('desc' => __('For EndUsers Only'),
-                'private' => self::VISIBLE_ENDUSERONLY, 'required' => self::REQUIRE_ENDUSER),
+                'flags' => self::FLAG_CLIENT_VIEW | self::FLAG_CLIENT_EDIT
+                    | self::FLAG_CLIENT_REQUIRED),
         );
     }
 
@@ -533,7 +592,7 @@ class DynamicFormField extends VerySimpleModel {
         if ($this->isPrivacyForced()) {
             // Required to be internal
             foreach ($modes as $m=>$info) {
-                if ($info['private'] != $this->get('private'))
+                if ($info['flags'] & (self::FLAG_CLIENT_VIEW | self::FLAG_AGENT_VIEW))
                     unset($modes[$m]);
             }
         }
@@ -541,47 +600,43 @@ class DynamicFormField extends VerySimpleModel {
         if ($this->isRequirementForced()) {
             // Required to be required
             foreach ($modes as $m=>$info) {
-                if ($info['required'] != $this->get('required'))
+                if ($info['flags'] & (self::FLAG_CLIENT_REQUIRED | self::FLAG_AGENT_REQUIRED))
                     unset($modes[$m]);
             }
         }
         return $modes;
     }
 
-    function getRequirementMode() {
-        foreach ($this->getAllRequirementModes() as $m=>$info) {
-            if ($this->get('private') == $info['private']
-                    && $this->get('required') == $info['required'])
-                return $m;
-        }
-        return false;
-    }
-
     function setRequirementMode($mode) {
         $modes = $this->getAllRequirementModes();
         if (!isset($modes[$mode]))
             return false;
 
         $info = $modes[$mode];
-        $this->set('required', $info['required']);
-        $this->set('private', $info['private']);
+        $this->set('flags', $info['flags']);
     }
 
     function isRequiredForStaff() {
-        return in_array($this->get('required'),
-            array(self::REQUIRE_EVERYONE, self::REQUIRE_AGENT));
+        return $this->hasFlag(self::FLAG_AGENT_REQUIRED);
     }
     function isRequiredForUsers() {
-        return in_array($this->get('required'),
-            array(self::REQUIRE_EVERYONE, self::REQUIRE_ENDUSER));
+        return $this->hasFlag(self::FLAG_CLIENT_REQUIRED);
+    }
+    function isEditableToStaff() {
+        return $this->hasFlag(self::FLAG_ENABLED)
+            && $this->hasFlag(self::FLAG_AGENT_EDIT);
     }
     function isVisibleToStaff() {
-        return in_array($this->get('private'),
-            array(self::VISIBLE_EVERYONE, self::VISIBLE_AGENTONLY));
+        return $this->hasFlag(self::FLAG_ENABLED)
+            && $this->hasFlag(self::FLAG_AGENT_VIEW);
+    }
+    function isEditableToUsers() {
+        return $this->hasFlag(self::FLAG_ENABLED)
+            && $this->hasFlag(self::FLAG_CLIENT_EDIT);
     }
     function isVisibleToUsers() {
-        return in_array($this->get('private'),
-            array(self::VISIBLE_EVERYONE, self::VISIBLE_ENDUSERONLY));
+        return $this->hasFlag(self::FLAG_ENABLED)
+            && $this->hasFlag(self::FLAG_CLIENT_VIEW);
     }
 
     /**
diff --git a/include/class.forms.php b/include/class.forms.php
index 7cebfb42f..3410477d7 100644
--- a/include/class.forms.php
+++ b/include/class.forms.php
@@ -1448,6 +1448,11 @@ class ThreadEntryField extends FormField {
         if ($cfg->getAllowedFileTypes())
             $fileupload_config['extensions']->set('default', $cfg->getAllowedFileTypes());
 
+        foreach ($fileupload_config as $C) {
+            $C->set('visibility', new VisibilityConstraint(new Q(array(
+                'attachments__eq'=>true,
+            )), VisibilityConstraint::HIDDEN));
+        }
         return array(
             'attachments' => new BooleanField(array(
                 'label'=>__('Enable Attachments'),
diff --git a/include/class.ticket.php b/include/class.ticket.php
index 7caac30d9..33400acbd 100644
--- a/include/class.ticket.php
+++ b/include/class.ticket.php
@@ -791,6 +791,15 @@ class Ticket {
         return $this->recipients;
     }
 
+    function hasClientEditableFields() {
+        $forms = DynamicFormEntry::forTicket($this->getId());
+        foreach ($forms as $form) {
+            foreach ($form->getFields() as $field) {
+                if ($field->isEditableToUsers())
+                    return true;
+            }
+        }
+    }
 
     function addCollaborator($user, $vars, &$errors) {
 
@@ -2221,7 +2230,7 @@ class Ticket {
             if (!in_array($form->getId(), $vars['forms']))
                 continue;
             $form->setSource($_POST);
-            if (!$form->isValid())
+            if (!$form->isValidForStaff())
                 $errors = array_merge($errors, $form->errors());
         }
 
@@ -2456,10 +2465,9 @@ class Ticket {
                 case 'staff':
                     // Required 'Contact Information' fields aren't required
                     // when staff open tickets
-                    return $type != 'user'
-                        || in_array($f->get('name'), array('name','email'));
+                    return $f->isVisibleToStaff();
                 case 'web':
-                    return !$f->get('private');
+                    return $f->isVisibleToUsers();
                 default:
                     return true;
                 }
@@ -2772,6 +2780,13 @@ class Ticket {
         /* -------------------- POST CREATE ------------------------ */
 
         // Save the (common) dynamic form
+        // Ensure we have a subject
+        $subject = $form->getAnswer('subject');
+        if ($subject && !$subject->getValue()) {
+            if ($topic) {
+                $form->setAnswer('subject', $topic->getFullName());
+            }
+        }
         $form->setTicketId($id);
         $form->save();
 
diff --git a/include/client/templates/dynamic-form.tmpl.php b/include/client/templates/dynamic-form.tmpl.php
index 66b957a49..0672b2263 100644
--- a/include/client/templates/dynamic-form.tmpl.php
+++ b/include/client/templates/dynamic-form.tmpl.php
@@ -5,7 +5,7 @@
     ?>
     <tr><td colspan="2"><hr />
     <div class="form-header" style="margin-bottom:0.5em">
-    <?php print ($form instanceof DynamicFormEntry) 
+    <?php print ($form instanceof DynamicFormEntry)
         ? $form->getForm()->getMedia() : $form->getMedia(); ?>
     <h3><?php echo Format::htmlchars($form->getTitle()); ?></h3>
     <em><?php echo Format::htmlchars($form->getInstructions()); ?></em>
@@ -16,19 +16,18 @@
     // 'private' are not included in the output for clients
     global $thisclient;
     foreach ($form->getFields() as $field) {
-        if (!$field->isVisibleToUsers())
+        if (!$field->isEditableToUsers())
             continue;
         ?>
         <tr>
             <td colspan="2" style="padding-top:8px;">
             <?php if (!$field->isBlockLevel()) { ?>
                 <label for="<?php echo $field->getFormName(); ?>"><span class="<?php
-                    if ($field->get('required')) echo 'required'; ?>">
+                    if ($field->isRequiredForUsers()) echo 'required'; ?>">
                 <?php echo Format::htmlchars($field->getLocal('label')); ?>
-            <?php if ($field->get('required')) { ?>
+            <?php if ($field->isRequiredForUsers()) { ?>
                 <span class="error">*</span>
-            <?php
-                }
+            <?php }
             ?></span><?php
                 if ($field->get('hint')) { ?>
                     <br /><em style="color:gray;display:inline-block"><?php
@@ -41,8 +40,7 @@
             $field->render('client');
             ?></label><?php
             foreach ($field->errors() as $e) { ?>
-                <br />
-                <font class="error"><?php echo $e; ?></font>
+                <div class="error"><?php echo $e; ?></div>
             <?php }
             $field->renderExtras('client');
             ?>
diff --git a/include/client/view.inc.php b/include/client/view.inc.php
index cd0b2ccd3..6fb76a9d9 100644
--- a/include/client/view.inc.php
+++ b/include/client/view.inc.php
@@ -32,12 +32,12 @@ if ($thisclient && $thisclient->isGuest()
         <td colspan="2" width="100%">
             <h1>
                 <?php echo sprintf(__('Ticket #%s'), $ticket->getNumber()); ?> &nbsp;
-                <a href="tickets.php?id=<?php echo $ticket->getId(); ?>" title="Reload"><span class="Icon refresh">&nbsp;</span></a>
-<?php if ($cfg->allowClientUpdates()
+                <a href="tickets.php?id=<?php echo $ticket->getId(); ?>" title="<?php echo __('Reload'); ?>"><span class="Icon refresh">&nbsp;</span></a>
+<?php if ($ticket->hasClientEditableFields()
         // Only ticket owners can edit the ticket details (and other forms)
         && $thisclient->getId() == $ticket->getUserId()) { ?>
                 <a class="action-button pull-right" href="tickets.php?a=edit&id=<?php
-                     echo $ticket->getId(); ?>"><i class="icon-edit"></i> Edit</a>
+                     echo $ticket->getId(); ?>"><i class="icon-edit"></i> <?php echo __('Edit'); ?></a>
 <?php } ?>
             </h1>
         </td>
@@ -88,7 +88,7 @@ foreach (DynamicFormEntry::forTicket($ticket->getId()) as $idx=>$form) {
     <?php foreach ($answers as $answer) {
         if (in_array($answer->getField()->get('name'), array('name', 'email', 'subject')))
             continue;
-        elseif ($answer->getField()->get('private'))
+        elseif (!$answer->getField()->isVisibleToUsers())
             continue;
         ?>
         <tr>
diff --git a/include/i18n/en_US/form.yaml b/include/i18n/en_US/form.yaml
index d8759c8fc..85c8fc00a 100644
--- a/include/i18n/en_US/form.yaml
+++ b/include/i18n/en_US/form.yaml
@@ -16,10 +16,15 @@
 #               will be used to retrieve the data from the field.
 #   hint:       Help text shown with the field
 #   flags:      Bit mask for settings & options
-#   edit_mask:  Mask out edits to the field (1=>delete, 2=>change name,
-#                   4=>privacy setting, 8=>requirement setting)
-#   private:    True if the field should be hidden from the client
-#   required:   True if entry for the field is required
+#     # From class DynamicFormField
+#     const FLAG_MASK_CHANGE      = 0x0010;     # Type cannot change
+#     const FLAG_MASK_DELETE      = 0x0020;     # Cannot be deleted
+#     const FLAG_MASK_EDIT        = 0x0040;     # Data cannot be edited
+#     const FLAG_MASK_DISABLE     = 0x0080;     # Field cannot be disabled
+#     const FLAG_MASK_REQUIRE     = 0x10000;    # Requirement cannot be changed
+#     const FLAG_MASK_VIEW        = 0x20000;    # View settings cannot be changed
+#     const FLAG_MASK_NAME        = 0x40000;    # Name cannot be changed
+#
 #   configuration: Field-specific configuration
 #     size:     (text) width of the field
 #     length:   (text) maximum size of the data in the field
@@ -35,10 +40,8 @@
     - type: text # notrans
       name: email # notrans
       label: Email Address
-      required: true
       sort: 1
-      flags: 3
-      edit_mask: 15
+      flags: 0x777A3
       configuration:
         size: 40
         length: 64
@@ -46,25 +49,21 @@
     - type: text # notrans
       name: name # notrans
       label: Full Name
-      required: true
       sort: 2
-      flags: 3
-      edit_mask: 15
+      flags: 0x777A3
       configuration:
         size: 40
         length: 64
     - type: phone # notrans
       name: phone # notrans
       label: Phone Number
-      required: false
       sort: 3
-      flags: 1
+      flags: 0x3301
     - type: memo # notrans
       name: notes
       label: Internal Notes
-      required: false
-      private: true
       sort: 4
+      flags: 0x3001
       configuration:
         rows: 4
         cols: 40
@@ -82,10 +81,8 @@
       type: text # notrans
       name: subject # notrans
       label: Issue Summary
-      required: true
-      edit_mask: 15
       sort: 1
-      flags: 1
+      flags: 0x77721
       configuration:
         size: 40
         length: 50
@@ -94,18 +91,13 @@
       name: message # notrans
       label: Issue Details
       hint: Details on the reason(s) for opening the ticket.
-      required: true
-      edit_mask: 15
       sort: 2
-      flags: 3
+      flags: 0x75523
     - id: 22
       type: priority # notrans
       name: priority # notrans
       label: Priority Level
-      required: false
-      private: true
-      edit_mask: 3
-      sort: 3
+      sort: 0x430A3
       flags: 1
 - type: C # notrans
   title: Company Information
@@ -115,10 +107,8 @@
     - type: text # notrans
       name: name # notrans
       label: Company Name
-      required: true
       sort: 1
-      flags: 1
-      edit_mask: 3
+      flags: 0x471A1
       configuration:
         size: 40
         length: 64
@@ -126,24 +116,22 @@
       name: website # notrans
       label: Website
       sort: 2
-      flags: 1
+      flags: 0x3101
       configuration:
         size: 40
         length: 64
     - type: phone # notrans
       name: phone # notrans
       label: Phone Number
-      required: false
       sort: 3
-      flags: 1
+      flags: 0x3101
       configuration:
         ext: false
     - type: memo # notrans
       name: address
       label: Address
-      required: false
       sort: 4
-      flags: 1
+      flags: 0x3101
       configuration:
         rows: 2
         cols: 40
@@ -157,19 +145,16 @@
     - type: text # notrans
       name: name # notrans
       label: Name
-      required: true
       sort: 1
-      flags: 3
-      edit_mask: 15
+      flags: 0x777A3
       configuration:
         size: 40
         length: 64
     - type: memo
       name: address
       label: Address
-      required: false
       sort: 2
-      flags: 1
+      flags: 0x3301
       configuration:
         rows: 2
         cols: 40
@@ -178,24 +163,21 @@
     - type: phone
       name: phone
       label: Phone
-      required: false
       sort: 3
-      flags: 1
+      flags: 0x3301
     - type: text
       name: website
       label: Website
-      required: false
       sort: 4
-      flags: 1
+      flags: 0x3301
       configuration:
         size: 40
         length: 0
     - type: memo # notrans
       name: notes
       label: Internal Notes
-      required: false
       sort: 5
-      flags: 1
+      flags: 0x3001
       configuration:
         rows: 4
         cols: 40
diff --git a/include/i18n/en_US/list.yaml b/include/i18n/en_US/list.yaml
index 613ca4a2f..2dbd2e7a4 100644
--- a/include/i18n/en_US/list.yaml
+++ b/include/i18n/en_US/list.yaml
@@ -37,17 +37,15 @@
       - type: state # notrans
         name: state # notrans
         label: State
-        required: true
         sort: 1
-        edit_mask: 63
+        flags: 0x770F1
         configuration:
             prompt: State of a ticket
       - type: memo # notrans
         name: description # notrans
         label: Description
-        required: false
         sort: 3
-        edit_mask: 15
+        flags: 0x73021
         configuration:
             rows: 2
             cols: 40
diff --git a/include/staff/dynamic-form.inc.php b/include/staff/dynamic-form.inc.php
index b79a4320d..bf68e752f 100644
--- a/include/staff/dynamic-form.inc.php
+++ b/include/staff/dynamic-form.inc.php
@@ -83,17 +83,13 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
         $uform = UserForm::objects()->all();
         $ftypes = FormField::allTypes();
         foreach ($uform[0]->getFields() as $f) {
-            if ($f->get('private')) continue;
+            if (!$f->isVisibleToUsers()) continue;
         ?>
         <tr>
             <td></td>
             <td><?php echo $f->get('label'); ?></td>
             <td><?php $t=FormField::getFieldType($f->get('type')); echo __($t[0]); ?></td>
-            <td><?php
-                $rmode = $f->getRequirementMode();
-                $modes = $f->getAllRequirementModes();
-                echo $modes[$rmode]['desc'];
-            ?></td>
+            <td><?php echo $f->getVisibilityDescription(); ?></td>
             <td><?php echo $f->get('name'); ?></td>
             <td><input type="checkbox" disabled="disabled"/></td></tr>
 
@@ -127,7 +123,6 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
         $id = $f->get('id');
         $deletable = !$f->isDeletable() ? 'disabled="disabled"' : '';
         $force_name = $f->isNameForced() ? 'disabled="disabled"' : '';
-        $rmode = $f->getRequirementMode();
         $fi = $f->getImpl();
         $ferrors = $f->errors(); ?>
         <tr>
@@ -162,12 +157,7 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
                     "><i class="icon-edit"></i> <?php echo __('Config'); ?></a>
             <?php } ?></td>
             <td>
-                <select name="visibility-<?php echo $id; ?>">
-<?php foreach ($f->getAllRequirementModes() as $m=>$I) { ?>
-    <option value="<?php echo $m; ?>" <?php if ($rmode == $m)
-         echo 'selected="selected"'; ?>><?php echo $I['desc']; ?></option>
-<?php } ?>
-                <select>
+                <?php echo $f->getVisibilityDescription(); ?>
             </td>
             <td>
                 <input type="text" size="20" name="name-<?php echo $id; ?>"
diff --git a/include/staff/settings-tickets.inc.php b/include/staff/settings-tickets.inc.php
index d64c52ed1..603f43dfa 100644
--- a/include/staff/settings-tickets.inc.php
+++ b/include/staff/settings-tickets.inc.php
@@ -200,14 +200,6 @@ if(!($maxfileuploads=ini_get('max_file_uploads')))
                 <i class="help-tip icon-question-sign" href="#enable_html_ticket_thread"></i>
             </td>
         </tr>
-        <tr>
-            <td><?php echo __('Allow Client Updates'); ?>:</td>
-            <td>
-                <input type="checkbox" name="allow_client_updates" <?php
-                echo $config['allow_client_updates']?'checked="checked"':''; ?>>
-                <?php echo __('Allow clients to update ticket details via the web portal'); ?>
-            </td>
-        </tr>
         <tr>
             <th colspan="2">
                 <em><b><?php echo __('Attachments');?></b>:  <?php echo __('Size and maximum uploads setting mainly apply to web tickets.');?></em>
diff --git a/include/staff/templates/dynamic-field-config.tmpl.php b/include/staff/templates/dynamic-field-config.tmpl.php
index d9db95352..65932bf65 100644
--- a/include/staff/templates/dynamic-field-config.tmpl.php
+++ b/include/staff/templates/dynamic-field-config.tmpl.php
@@ -3,6 +3,123 @@
     <hr/>
     <form method="post" action="#form/field-config/<?php
             echo $field->get('id'); ?>">
+<ul class="tabs">
+    <li><a href="#config" class="active"><i class="icon-cogs"></i> Field Setup</a></li>
+    <li><a href="#visibility"><i class="icon-beaker"></i> Settings</a></li>
+</ul>
+
+<div class="tab_content" id="visibility" style="display:none">
+    <div>
+    <div class="span4">
+        <div style="margin-bottom:5px"><strong>Enabled</strong>
+        <i class="help-tip icon-question-sign"
+            data-title="Enabled"
+            data-content="This field can be disabled which will remove it
+            from the form for new entries, but will preserve the data on all
+            current entries."></i>
+        </div>
+    </div>
+    <div class="span6">
+    <input type="checkbox" name="flags[]" value="<?php
+            echo DynamicFormField::FLAG_ENABLED; ?>" <?php
+            if ($field->hasFlag(DynamicFormField::FLAG_ENABLED)) echo 'checked="checked"';
+            if ($field->hasFlag(DynamicFormField::FLAG_MASK_DISABLE)) echo ' disabled="disabled"';
+        ?>> Enabled<br/>
+    </div>
+    <hr class="faded"/>
+
+    <div class="span4">
+        <div style="margin-bottom:5px"><strong>Visible</strong>
+        <i class="help-tip icon-question-sign"
+            data-title="Visible"
+            data-content="Making fields <em>visible</em> allows agents and
+            endusers to view and create information in this field."></i>
+        </div>
+    </div>
+    <div class="span3">
+        <input type="checkbox" name="flags[]" value="<?php
+            echo DynamicFormField::FLAG_CLIENT_VIEW; ?>" <?php
+            if ($field->hasFlag(DynamicFormField::FLAG_CLIENT_VIEW)) echo 'checked="checked"';
+            if ($field->isPrivacyForced()) echo ' disabled="disabled"';
+        ?>> For Clients<br/>
+    </div>
+    <div class="span3">
+        <input type="checkbox" name="flags[]" value="<?php
+            echo DynamicFormField::FLAG_AGENT_VIEW; ?>" <?php
+            if ($field->hasFlag(DynamicFormField::FLAG_AGENT_VIEW)) echo 'checked="checked"';
+            if ($field->isPrivacyForced()) echo ' disabled="disabled"';
+        ?>> For Agents<br/>
+    </div>
+
+<?php if ($field->getImpl()->hasData()) { ?>
+    <hr class="faded"/>
+
+    <div class="span4">
+        <div style="margin-bottom:5px"><strong>Required</strong>
+        <i class="help-tip icon-question-sign"
+            data-title="Required"
+            data-content="New entries cannot be created unless all
+            <em>required</em> fields have valid data."></i>
+        </div>
+    </div>
+    <div class="span3">
+        <input type="checkbox" name="flags[]" value="<?php
+            echo DynamicFormField::FLAG_CLIENT_REQUIRED; ?>" <?php
+            if ($field->hasFlag(DynamicFormField::FLAG_CLIENT_REQUIRED)) echo 'checked="checked"';
+            if ($field->isRequirementForced()) echo ' disabled="disabled"';
+        ?>> For Clients<br/>
+    </div>
+    <div class="span3">
+        <input type="checkbox" name="flags[]" value="<?php
+            echo DynamicFormField::FLAG_AGENT_REQUIRED; ?>" <?php
+            if ($field->hasFlag(DynamicFormField::FLAG_AGENT_REQUIRED)) echo 'checked="checked"';
+            if ($field->isRequirementForced()) echo ' disabled="disabled"';
+        ?>> For Agents<br/>
+    </div>
+    <hr class="faded"/>
+
+    <div class="span4">
+        <div style="margin-bottom:5px"><strong>Editable</strong>
+        <i class="help-tip icon-question-sign"
+            data-content="Fields marked editable allow agents and endusers to update the
+            content of this field after the form entry has been created."
+            data-title="Editable"></i>
+        </div>
+    </div>
+
+    <div class="span3">
+        <input type="checkbox" name="flags[]" value="<?php
+            echo DynamicFormField::FLAG_CLIENT_EDIT; ?>" <?php
+            if ($field->hasFlag(DynamicFormField::FLAG_CLIENT_EDIT)) echo 'checked="checked"';
+        ?>> For Clients<br/>
+    </div>
+    <div class="span3">
+        <input type="checkbox" name="flags[]" value="<?php
+            echo DynamicFormField::FLAG_AGENT_EDIT; ?>" <?php
+            if ($field->hasFlag(DynamicFormField::FLAG_AGENT_EDIT)) echo 'checked="checked"';
+        ?>> For Agents<br/>
+    </div>
+    <hr class="faded"/>
+
+    <div class="span4">
+        <div style="margin-bottom:5px"><strong>Data Integrity</strong>
+        <i class="help-tip icon-question-sign"
+            data-title="Required to close a ticket"
+            data-content="Optionally, this field can prevent closing a
+            ticket until it has valid data."></i>
+        </div>
+    </div>
+    <div class="span6">
+        <input type="checkbox" name="flags[]" value="<?php
+            echo DynamicFormField::FLAG_CLOSE_REQUIRED; ?>" <?php
+            if ($field->hasFlag(DynamicFormField::FLAG_CLOSE_REQUIRED)) echo 'checked="checked"';
+        ?>> Required to close a ticket<br/>
+    </div>
+<?php } ?>
+    </div>
+</div>
+
+<div class="tab_content" id="config">
         <?php
         echo csrf_token();
         $form = $field->getConfigurationForm();
@@ -50,6 +167,7 @@
             echo Format::htmlchars($field->get('hint')); ?></textarea>
         </div>
         </div>
+</div>
         <hr>
         <p class="full-width">
             <span class="buttons pull-left">
@@ -62,7 +180,41 @@
          </p>
     </form>
     <div class="clear"></div>
+
 <script type="text/javascript">
    // Make translatable fields translatable
    $('input[data-translate-tag], textarea[data-translate-tag]').translatable();
 </script>
+
+<style type="text/css">
+.span3 {
+    width: 22.25%;
+    margin: 0 1%;
+    display: inline-block;
+    vertical-align: top;
+}
+.span4 {
+    width: 30.25%;
+    margin: 0 1%;
+    display: inline-block;
+    vertical-align: top;
+}
+.span6 {
+    width: 47.25%;
+    margin: 0 1%;
+    display: inline-block;
+    vertical-align: top;
+}
+.span12 {
+    width: 97%;
+    margin: 0 1%;
+    display: inline-block;
+    vertical-align: top;
+}
+.dialog input, .dialog select {
+    margin: 2px;
+}
+hr.faded {
+    opacity: 0.3;
+}
+</style>
diff --git a/include/staff/templates/dynamic-form.tmpl.php b/include/staff/templates/dynamic-form.tmpl.php
index f0db379b8..4efb94a21 100644
--- a/include/staff/templates/dynamic-form.tmpl.php
+++ b/include/staff/templates/dynamic-form.tmpl.php
@@ -38,7 +38,7 @@ if (isset($options['entry']) && $options['mode'] == 'edit') { ?>
     }
     foreach ($form->getFields() as $field) {
         try {
-            if (!$field->isVisibleToStaff())
+            if (!$field->isEditableToStaff())
                 continue;
         }
         catch (Exception $e) {
@@ -50,14 +50,14 @@ if (isset($options['entry']) && $options['mode'] == 'edit') { ?>
                 <?php
             }
             else { ?>
-                <td class="multi-line <?php if ($field->get('required')) echo 'required';
+                <td class="multi-line <?php if ($field->isRequiredForStaff()) echo 'required';
                 ?>" style="min-width:120px;" <?php if ($options['width'])
                     echo "width=\"{$options['width']}\""; ?>>
                 <?php echo Format::htmlchars($field->getLocal('label')); ?>:</td>
                 <td><div style="position:relative"><?php
             }
             $field->render(); ?>
-            <?php if (!$field->isBlockLevel() && $field->get('required')) { ?>
+            <?php if (!$field->isBlockLevel() && $field->isRequiredForStaff()) { ?>
                 <span class="error">*</span>
             <?php
             }
diff --git a/tickets.php b/tickets.php
index 5b15b0015..2161b67b2 100644
--- a/tickets.php
+++ b/tickets.php
@@ -47,8 +47,6 @@ if($_POST && is_object($ticket) && $ticket->getId()):
         if(!$ticket->checkUserAccess($thisclient) //double check perm again!
                 || $thisclient->getId() != $ticket->getUserId())
             $errors['err']=__('Access Denied. Possibly invalid ticket ID');
-        elseif (!$cfg || !$cfg->allowClientUpdates())
-            $errors['err']=__('Access Denied. Client updates are currently disabled');
         else {
             $forms=DynamicFormEntry::forTicket($ticket->getId());
             foreach ($forms as $form) {
@@ -107,7 +105,7 @@ endif;
 $nav->setActiveNav('tickets');
 if($ticket && $ticket->checkUserAccess($thisclient)) {
     if (isset($_REQUEST['a']) && $_REQUEST['a'] == 'edit'
-            && $cfg->allowClientUpdates()) {
+            && $ticket->hasClientEditableFields()) {
         $inc = 'edit.inc.php';
         if (!$forms) $forms=DynamicFormEntry::forTicket($ticket->getId());
         // Auto add new fields to the entries
-- 
GitLab