From 9e75169ea59b223b9207c8cdea58abcf009786ff Mon Sep 17 00:00:00 2001 From: Jared Hancock <jared@osticket.com> Date: Wed, 21 Aug 2013 17:37:45 -0500 Subject: [PATCH] Dynamic data for osTicket *This is a major redesign / rework of the osTicket base* This patch drops the concept of static ticket metadata and allows for an admin-configurable arbitrary data that is attachable to tickets The system is architected such that the base osTicket install now comes with a "default" form that has fields for subject, name, email, and phone number. This form is editable to allow for the addition of arbitrary other fields; however, the basic fields must remain in order to be associated with a help-topic and attached to a ticket. This concept can be expanded to allow for arbitrary data associated with registered clients or ticket thread items. Forms are comprised of sections. Sections have a title and instructions properties and a list of fields. Fields have various implementations to represent different data such as text, long answer, phone number, datetime, yes/no, and selections, and are configurable to define the look and feel and interpretation of the respective form field. Dropdown lists are represented as "Dynamic Lists", which are admin-configurable lists of items. Dropdowns can be optionally represented as Bootstrap typeahead fields. This also adds the start of a simple ORM which will hopefully be expanded in the future to support multiple database platforms. Currently, only MySQL is implemented. --- ajax.php | 3 + bootstrap.php | 11 + include/ajax.forms.php | 46 + include/ajax.tickets.php | 26 +- include/api.tickets.php | 33 +- include/class.api.php | 4 +- include/class.client.php | 42 +- include/class.dynamic_forms.php | 712 +++++++++++++++ include/class.forms.php | 695 ++++++++++++++ include/class.i18n.php | 21 +- include/class.json.php | 10 +- include/class.nav.php | 6 + include/class.orm.php | 672 ++++++++++++++ include/class.pdf.php | 29 +- include/class.ticket.php | 101 +- include/class.topic.php | 10 + include/client/header.inc.php | 6 +- include/client/open.inc.php | 61 +- .../client/templates/dynamic-form.tmpl.php | 53 ++ include/client/tickets.inc.php | 20 +- include/client/view.inc.php | 24 + include/i18n/en_US/forms.yaml | 47 + include/i18n/en_US/help_topic.yaml | 2 + include/mysqli.php | 5 + include/staff/dynamic-form-section.inc.php | 142 +++ include/staff/dynamic-form-sections.inc.php | 41 + include/staff/dynamic-form.inc.php | 104 +++ include/staff/dynamic-forms.inc.php | 41 + include/staff/dynamic-list.inc.php | 119 +++ include/staff/dynamic-lists.inc.php | 43 + include/staff/header.inc.php | 3 +- include/staff/helptopic.inc.php | 17 +- .../templates/dynamic-field-config.tmpl.php | 79 ++ include/staff/templates/dynamic-form.tmpl.php | 28 + include/staff/ticket-edit.inc.php | 46 +- include/staff/ticket-open.inc.php | 66 +- include/staff/ticket-view.inc.php | 36 + include/staff/tickets.inc.php | 105 ++- include/upgrader/streams/core.sig | 2 +- .../streams/core/d51f303a-DYNAMICF.patch.sql | 216 +++++ main.inc.php | 56 +- open.php | 24 + scp/ajax.php | 5 + scp/css/scp.css | 2 + scp/dynamic-form-sections.php | 79 ++ scp/dynamic-forms.php | 83 ++ scp/dynamic-lists.php | 66 ++ scp/helptopics.php | 4 + scp/js/scp.js | 18 + scp/tickets.php | 36 +- setup/inc/class.installer.php | 28 +- setup/inc/sql/osTicket-mysql.sql | 861 ++++++++++++++++++ setup/inc/streams/core/install-mysql.sql | 105 ++- 53 files changed, 4755 insertions(+), 339 deletions(-) create mode 100644 include/ajax.forms.php create mode 100644 include/class.dynamic_forms.php create mode 100644 include/class.forms.php create mode 100644 include/class.orm.php create mode 100644 include/client/templates/dynamic-form.tmpl.php create mode 100644 include/i18n/en_US/forms.yaml create mode 100644 include/staff/dynamic-form-section.inc.php create mode 100644 include/staff/dynamic-form-sections.inc.php create mode 100644 include/staff/dynamic-form.inc.php create mode 100644 include/staff/dynamic-forms.inc.php create mode 100644 include/staff/dynamic-list.inc.php create mode 100644 include/staff/dynamic-lists.inc.php create mode 100644 include/staff/templates/dynamic-field-config.tmpl.php create mode 100644 include/staff/templates/dynamic-form.tmpl.php create mode 100644 include/upgrader/streams/core/d51f303a-DYNAMICF.patch.sql create mode 100644 scp/dynamic-form-sections.php create mode 100644 scp/dynamic-forms.php create mode 100644 scp/dynamic-lists.php create mode 100644 setup/inc/sql/osTicket-mysql.sql diff --git a/ajax.php b/ajax.php index 0786b41a4..57b6274cc 100644 --- a/ajax.php +++ b/ajax.php @@ -34,6 +34,9 @@ $dispatcher = patterns('', url_post('^(?P<id>\d+)/attach$', 'uploadInlineImageClient'), url_get('^(?P<namespace>[\w.]+)$', 'getDraftClient'), url_post('^(?P<namespace>[\w.]+)$', 'createDraftClient') + )), + url('^/form/', patterns('ajax.forms.php:DynamicFormsAjaxAPI', + url_get('^help-topic/(?P<id>\d+)$', 'getClientFormsForHelpTopic') )) ); print $dispatcher->resolve($ost->get_path_info()); diff --git a/bootstrap.php b/bootstrap.php index b2540b159..671f56927 100644 --- a/bootstrap.php +++ b/bootstrap.php @@ -87,6 +87,17 @@ class Bootstrap { define('TICKET_PRIORITY_TABLE',$prefix.'ticket_priority'); define('PRIORITY_TABLE',TICKET_PRIORITY_TABLE); + define('FORM_SEC_TABLE',$prefix.'form_section'); + define('FORM_FIELD_TABLE',$prefix.'form_field'); + define('FORMSET_TABLE',$prefix.'formset'); + define('FORMSET_SEC_TABLE',$prefix.'formset_sections'); + + define('LIST_TABLE',$prefix.'list'); + define('LIST_ITEM_TABLE',$prefix.'list_items'); + + define('FORM_ENTRY_TABLE',$prefix.'form_entry'); + define('FORM_ANSWER_TABLE',$prefix.'form_entry_values'); + define('TOPIC_TABLE',$prefix.'help_topic'); define('SLA_TABLE', $prefix.'sla'); diff --git a/include/ajax.forms.php b/include/ajax.forms.php new file mode 100644 index 000000000..08b0b396c --- /dev/null +++ b/include/ajax.forms.php @@ -0,0 +1,46 @@ +<?php + +require_once(INCLUDE_DIR . 'class.topic.php'); +require_once(INCLUDE_DIR . 'class.dynamic_forms.php'); + +class DynamicFormsAjaxAPI extends AjaxController { + function getForm($form_id) { + $form = DynamicFormSection::lookup($form_id); + if (!$form) return; + + foreach ($form->getFields() as $field) { + $field->render(); + } + } + + function getFormsForHelpTopic($topic_id, $client=false) { + $topic = Topic::lookup($topic_id); + foreach (DynamicFormset::lookup($topic->ht['formset_id'])->getForms() as $form) { + $set=$form; + $form=$form->getForm(); + if ($client) + include(CLIENTINC_DIR . 'templates/dynamic-form.tmpl.php'); + else + include(STAFFINC_DIR . 'templates/dynamic-form.tmpl.php'); + } + } + + function getClientFormsForHelpTopic($topic_id) { + return $this->getFormsForHelpTopic($topic_id, true); + } + + function getFieldConfiguration($field_id) { + $field = DynamicFormField::lookup($field_id); + include(STAFFINC_DIR . 'templates/dynamic-field-config.tmpl.php'); + } + + function saveFieldConfiguration($field_id) { + $field = DynamicFormField::lookup($field_id); + if (!$field->setConfiguration()) + include(STAFFINC_DIR . 'templates/dynamic-field-config.tmpl.php'); + else + $field->save(); + } +} + +?> diff --git a/include/ajax.tickets.php b/include/ajax.tickets.php index 34c46325e..a981e5ae4 100644 --- a/include/ajax.tickets.php +++ b/include/ajax.tickets.php @@ -30,9 +30,13 @@ class TicketsAjaxAPI extends AjaxController { $limit = isset($_REQUEST['limit']) ? (int) $_REQUEST['limit']:25; $tickets=array(); - $sql='SELECT DISTINCT ticketID, email' - .' FROM '.TICKET_TABLE - .' WHERE ticketID LIKE \''.db_input($_REQUEST['q'], false).'%\''; + $sql='SELECT DISTINCT ticketID, email.value AS email' + .' FROM '.TICKET_TABLE.' ticket' + .' LEFT JOIN '.FORM_ENTRY_TABLE.' entry ON entry.ticket_id = ticket.ticket_id ' + .' LEFT JOIN '.FORM_ANSWER_TABLE.' email ON email.entry_id = entry.id ' + .' LEFT JOIN '.FORM_FIELD_TABLE.' field ON email.field_id = field.id ' + .' WHERE field.name = "email"' + .' AND ticketID LIKE \''.db_input($_REQUEST['q'], false).'%\''; $sql.=' AND ( staff_id='.db_input($thisstaff->getId()); @@ -43,7 +47,7 @@ class TicketsAjaxAPI extends AjaxController { $sql.=' OR dept_id IN ('.implode(',', db_input($depts)).')'; $sql.=' ) ' - .' ORDER BY created LIMIT '.$limit; + .' ORDER BY ticket.created LIMIT '.$limit; if(($res=db_query($sql)) && db_num_rows($res)) { while(list($id, $email)=db_fetch_row($res)) @@ -60,9 +64,13 @@ class TicketsAjaxAPI extends AjaxController { $limit = isset($_REQUEST['limit']) ? (int) $_REQUEST['limit']:25; $tickets=array(); - $sql='SELECT email, count(ticket_id) as tickets ' - .' FROM '.TICKET_TABLE - .' WHERE email LIKE \'%'.db_input(strtolower($_REQUEST['q']), false).'%\' '; + $sql='SELECT email.value AS email, count(ticket.ticket_id) as tickets ' + .' FROM '.TICKET_TABLE.' ticket' + .' JOIN '.FORM_ENTRY_TABLE.' entry ON entry.ticket_id = ticket.ticket_id ' + .' JOIN '.FORM_ANSWER_TABLE.' email ON email.entry_id = entry.id ' + .' JOIN '.FORM_FIELD_TABLE.' field ON email.field_id = field.id ' + .' WHERE field.name = "email"' + .' AND email.value LIKE \'%'.db_input(strtolower($_REQUEST['q']), false).'%\' '; $sql.=' AND ( staff_id='.db_input($thisstaff->getId()); @@ -73,8 +81,8 @@ class TicketsAjaxAPI extends AjaxController { $sql.=' OR dept_id IN ('.implode(',', db_input($depts)).')'; $sql.=' ) ' - .' GROUP BY email ' - .' ORDER BY created LIMIT '.$limit; + .' GROUP BY email.value ' + .' ORDER BY ticket.created LIMIT '.$limit; if(($res=db_query($sql)) && db_num_rows($res)) { while(list($email, $count)=db_fetch_row($res)) diff --git a/include/api.tickets.php b/include/api.tickets.php index f49680ce7..a442d5b7f 100644 --- a/include/api.tickets.php +++ b/include/api.tickets.php @@ -8,15 +8,23 @@ class TicketApiController extends ApiController { # Supported arguments -- anything else is an error. These items will be # inspected _after_ the fixup() method of the ApiXxxDataParser classes # so that all supported input formats should be supported - function getRequestStructure($format) { + function getRequestStructure($format, $data=null) { $supported = array( "alert", "autorespond", "source", "topicId", - "name", "email", "subject", "phone", "phone_ext", "attachments" => array("*" => array("name", "type", "data", "encoding") ), "message", "ip", "priorityId" ); + # Fetch dynamic form field names for the given help topic and add + # the names to the supported request structure + if (isset($data['topicId'])) { + $topic=Topic::lookup($data['topicId']); + $formset=DynamicFormset::lookup($topic->ht['formset_id']); + foreach ($formset->getForms() as $form) + foreach ($form->getForm()->getFields() as $field) + $supported[] = $field->get('name'); + } if(!strcasecmp($format, 'email')) { $supported = array_merge($supported, array('header', 'mid', @@ -90,6 +98,21 @@ class TicketApiController extends ApiController { # Create the ticket with the data (attempt to anyway) $errors = array(); + + $topic=Topic::lookup($data['topicId']); + $forms=DynamicFormset::lookup($topic->ht['formset_id'])->getForms(); + foreach ($forms as $idx=>$f) { + $forms[$idx] = $form = $f->getForm()->instanciate($f->sort); + # Collect name, email address, and subject for banning and such + foreach ($form->getFields() as $field) { + $fname = $field->get('name'); + if ($fname && isset($data[$fname])) + $field->value = $data[$fname]; + } + if (!$form->isValid()) + $errors = array_merge($errors, $form->errors()); + } + $ticket = Ticket::create($data, $errors, $data['source'], $autorespond, $alert); # Return errors (?) if (count($errors)) { @@ -105,6 +128,12 @@ class TicketApiController extends ApiController { return $this->exerr(500, "Unable to create new ticket: unknown error"); } + # Save dynamic forms + foreach ($forms as $f) { + $f->set('ticket_id', $ticket->getId()); + $f->save(); + } + return $ticket; } diff --git a/include/class.api.php b/include/class.api.php index 019d3fba3..418b8da76 100644 --- a/include/class.api.php +++ b/include/class.api.php @@ -235,7 +235,7 @@ class ApiController { * Structure to validate the request against -- must be overridden to be * useful */ - function getRequestStructure($format) { return array(); } + function getRequestStructure($format, $data=null) { return array(); } /** * Simple validation that makes sure the keys of a parsed request are * expected. It is assumed that the functions actually implementing the @@ -266,7 +266,7 @@ class ApiController { function validate(&$data, $format) { return $this->validateRequestStructure( $data, - $this->getRequestStructure($format) + $this->getRequestStructure($format, $data) ); } diff --git a/include/class.client.php b/include/class.client.php index 5ff5911c2..8cf29e31d 100644 --- a/include/class.client.php +++ b/include/class.client.php @@ -40,11 +40,17 @@ class Client { if(!$id && !($id=$this->getId())) return false; - $sql='SELECT ticket_id, ticketID, name, email, phone, phone_ext ' - .' FROM '.TICKET_TABLE - .' WHERE ticketID='.db_input($id); + $sql='SELECT ticket.ticket_id, ticketID, email.value as email, phone.value as phone ' + .' FROM '.TICKET_TABLE.' ticket ' + .' LEFT JOIN '.FORM_ENTRY_TABLE.' entry ON entry.ticket_id = ticket.ticket_id ' + .' LEFT JOIN '.FORM_ANSWER_TABLE.' email ON email.entry_id = entry.id ' + .' LEFT JOIN '.FORM_FIELD_TABLE.' field1 ON email.field_id = field1.id ' + .' LEFT JOIN '.FORM_ANSWER_TABLE.' phone ON email.entry_id = entry.id ' + .' LEFT JOIN '.FORM_FIELD_TABLE.' field2 ON phone.field_id = field2.id ' + .' WHERE field1.name = "email" AND field2.name="phone" AND ticketID='.db_input($id); + if($email) - $sql.=' AND email='.db_input($email); + $sql.=' AND email.value = '.db_input($email); if(!($res=db_query($sql)) || !db_num_rows($res)) return NULL; @@ -53,12 +59,17 @@ class Client { $this->id = $this->ht['ticketID']; //placeholder $this->ticket_id = $this->ht['ticket_id']; $this->ticketID = $this->ht['ticketID']; - $this->fullname = ucfirst($this->ht['name']); + + $entry = DynamicFormEntry::forTicket($this->ticket_id); + foreach ($entry as $form) + if ($form->getAnswer('name')) + $this->fullname = $form->getAnswer('name'); + $this->username = $this->ht['email']; $this->email = $this->ht['email']; $this->stats = array(); - + return($this->id); } @@ -93,7 +104,7 @@ class Client { function getPhoneExt() { return $this->ht['phone_ext']; } - + function getTicketID() { return $this->ticketID; } @@ -120,9 +131,12 @@ class Client { /* ------------- Static ---------------*/ function getLastTicketIdByEmail($email) { - $sql='SELECT ticketID FROM '.TICKET_TABLE - .' WHERE email='.db_input($email) - .' ORDER BY created ' + $sql='SELECT ticket.ticketID '.TICKET_TABLE.' ticket ' + .' LEFT JOIN '.FORM_ENTRY_TABLE.' entry ON entry.ticket_id = ticket.ticket_id ' + .' LEFT JOIN '.FORM_ANSWER_TABLE.' email ON email.entry_id = entry.id ' + .' LEFT JOIN '.FORM_FIELD_TABLE.' field ON email.field_id = field.id ' + .' WHERE field.name = "email" AND email.value = '.db_input($email) + .' ORDER BY ticket.created ' .' LIMIT 1'; if(($res=db_query($sql)) && db_num_rows($res)) list($tid) = db_fetch_row($res); @@ -175,12 +189,12 @@ class Client { //See if we can fetch local ticket id associated with the ID given if(($ticket=Ticket::lookupByExtId($ticketID, $email)) && $ticket->getId()) { //At this point we know the ticket ID is valid. - //TODO: 1) Check how old the ticket is...3 months max?? 2) Must be the latest 5 tickets?? + //TODO: 1) Check how old the ticket is...3 months max?? 2) Must be the latest 5 tickets?? //Check the email given. # Require auth token for automatic logins (GET METHOD). if (!strcasecmp($ticket->getEmail(), $email) && (!$auto_login || $auth === $ticket->getAuthToken())) { - + //valid match...create session goodies for the client. $user = new ClientSession($email,$ticket->getExtId()); $_SESSION['_client'] = array(); //clear. @@ -193,7 +207,7 @@ class Client { //Log login info... $msg=sprintf('%s/%s logged in [%s]', $ticket->getEmail(), $ticket->getExtId(), $_SERVER['REMOTE_ADDR']); $ost->logDebug('User login', $msg); - + //Regenerate session ID. $sid=session_id(); //Current session id. session_regenerate_id(TRUE); //get new ID. @@ -202,7 +216,7 @@ class Client { return $user; - } + } } //If we get to this point we know the login failed. diff --git a/include/class.dynamic_forms.php b/include/class.dynamic_forms.php new file mode 100644 index 000000000..c197127ce --- /dev/null +++ b/include/class.dynamic_forms.php @@ -0,0 +1,712 @@ +<?php +/********************************************************************* + class.dynamic_forms.php + + Forms models built on the VerySimpleModel paradigm. Allows for arbitrary + data to be associated with tickets. Eventually this model can be + extended to associate arbitrary data with registered clients and thread + entries. + + Jared Hancock <jared@osticket.com> + Copyright (c) 2006-2013 osTicket + http://www.osticket.com + + Released under the GNU General Public License WITHOUT ANY WARRANTY. + See LICENSE.TXT for details. + + vim: expandtab sw=4 ts=4 sts=4: +**********************************************************************/ +require_once(INCLUDE_DIR . 'class.orm.php'); +require_once(INCLUDE_DIR . 'class.forms.php'); + +/** + * Form template, used for designing the custom form and for entering custom + * data for a ticket + */ +class DynamicFormSection extends VerySimpleModel { + + static $meta = array( + 'table' => FORM_SEC_TABLE, + 'ordering' => array('title'), + 'pk' => array('id'), + ); + + var $_fields; + var $_dfields; + + function getFields() { + if (!isset($this->_fields)) { + $this->_fields = array(); + foreach ($this->getDynamicFields() as $f) + $this->_fields[] = $f->getImpl(); + } + return $this->_fields; + } + + function getDynamicFields() { + if (!isset($this->_dfields)) + $this->_dfields = DynamicFormField::objects() + ->filter(array('section_id'=>$this->id)) + ->all(); + return $this->_dfields; + } + + function getTitle() { return $this->get('title'); } + function getInstructions() { return $this->get('instructions'); } + + function getForm() { + $fields = $this->getFields(); + foreach ($fields as &$f) + $f = $f->getField(); + return new Form($fields, $this->title, $this->instructions); + } + + function instanciate($sort=1) { + return DynamicFormEntry::create(array( + 'section_id'=>$this->get('id'), 'sort'=>$sort)); + } + + function save() { + if (count($this->dirty)) + $this->set('updated', new SqlFunction('NOW')); + return parent::save(); + } + + static function create($ht=false) { + $inst = parent::create($ht); + $inst->set('created', new SqlFunction('NOW')); + if (isset($ht['fields'])) { + $inst->save(); + foreach ($ht['fields'] as $f) { + $f = DynamicFormField::create($f); + $f->section_id = $inst->id; + $f->save(); + } + } + return $inst; + } +} + +require_once(INCLUDE_DIR . "class.json.php"); + +class DynamicFormField extends VerySimpleModel { + + static $meta = array( + 'table' => FORM_FIELD_TABLE, + 'ordering' => array('sort'), + 'pk' => array('id'), + 'joins' => array( + 'form' => array( + 'null' => true, + 'constraint' => array('section_id' => 'DynamicFormSection.id'), + ), + ), + ); + + var $_field; + + // Multiple inheritance -- delegate to FormField + function __call($what, $args) { + return call_user_func_array( + array($this->getField(), $what), $args); + } + + function getField() { + if (!isset($this->_field)) + $this->_field = new FormField($this->ht); + return $this->_field; + } + + function getAnswer() { return $this->answer; } + + /** + * setConfiguration + * + * Used in the POST request of the configuration process. The + * ::getConfigurationForm() method should be used to retrieve a + * configuration form for this field. That form should be submitted via + * a POST request, and this method should be called in that request. The + * data from the POST request will be interpreted and will adjust the + * configuration of this field + * + * Parameters: + * errors - (OUT array) receives validation errors of the parsed + * configuration form + * + * Returns: + * (bool) true if the configuration was updated, false if there were + * errors. If false, the errors were written into the received errors + * array. + */ + function setConfiguration($errors) { + $errors = $config = array(); + foreach ($this->getConfigurationForm() as $name=>$field) { + $config[$name] = $field->getClean(); + $errors = array_merge($errors, $field->errors()); + } + if (count($errors) === 0) + $this->set('configuration', JsonDataEncoder::encode($config)); + $this->set('hint', $_POST['hint']); + return count($errors) === 0; + } + + function delete() { + // Don't really delete form fields as that will screw up the data + // model. Instead, just drop the association with the form section + // which will give the appearance of deletion. Not deleting means + // that the field will continue to exist on form entries it may + // already have answers on, but since it isn't associated with the + // form section, it won't be available for new form submittals. + $this->set('section_id', 0); + $this->save(); + } + + function save() { + if (count($this->dirty)) + $this->set('updated', new SqlFunction('NOW')); + return parent::save(); + } + + static function create($ht=false) { + $inst = parent::create($ht); + $inst->set('created', new SqlFunction('NOW')); + if (isset($ht['configuration'])) + $inst->configuration = JsonDataEncoder::encode($ht['configuration']); + return $inst; + } +} + +/** + * Represents an entry to a dynamic form. Used to render the completed form + * in reference to the attached ticket, etc. A form is used to represent the + * template of enterable data. This represents the data entered into an + * instance of that template. + * + * The data of the entry is called 'answers' in this model. This model + * represents an instance of a form entry. The data / answers to that entry + * are represented individually in the DynamicFormEntryAnswer model. + */ +class DynamicFormEntry extends VerySimpleModel { + + static $meta = array( + 'table' => FORM_ENTRY_TABLE, + 'ordering' => array('sort'), + 'pk' => array('id'), + 'joins' => array( + 'form' => array( + 'null' => true, + 'constraint' => array('section_id' => 'DynamicFormSection.id'), + ), + ), + ); + + var $_values; + var $_fields; + var $_form; + + function getAnswers() { + if (!isset($this->_values)) { + $this->_values = DynamicFormEntryAnswer::objects() + ->filter(array('entry_id'=>$this->get('id'))) + ->all(); + foreach ($this->_values as $v) + $v->entry = $this; + } + return $this->_values; + } + + function getAnswer($name) { + foreach ($this->getAnswers() as $ans) + if ($ans->getField()->get('name') == $name) + return $ans->getValue(); + return null; + } + + function errors() { + return $this->_errors; + } + + function getTitle() { return $this->getForm()->getTitle(); } + function getInstructions() { return $this->getForm()->getInstructions(); } + + function getForm() { + if (!$this->_form) + $this->_form = DynamicFormSection::lookup($this->get('section_id')); + return $this->_form; + } + + function getFields() { + if (!$this->_fields) { + $this->_fields = array(); + foreach ($this->getAnswers() as $a) + $this->_fields[] = $a->getField(); + } + return $this->_fields; + } + + function isValid() { + if (!is_array($this->_errors)) { + $this->_errors = array(); + $this->getClean(); + foreach ($this->getFields() as $field) + if ($field->errors()) + $this->_errors[$field->get('id')] = $field->errors(); + } + return !$this->_errors; + } + + function getClean() { + if (!$this->_clean) { + $this->_clean = array(); + foreach ($this->getFields() as $field) + $this->_clean[$field->get('id')] = $field->getClean(); + } + return $this->_clean; + } + + function forTicket($ticket_id) { + static $entries = array(); + if (!isset($entries[$ticket_id])) + $entries[$ticket_id] = DynamicFormEntry::objects() + ->filter(array('ticket_id'=>$ticket_id)); + return $entries[$ticket_id]; + } + + /** + * addMissingFields + * + * Adds fields that have been added to the linked form section (field + * set) since this entry was originally created. If fields are added to + * the form section, the method will automatically add the fields and + * null answers to the entry. + */ + function addMissingFields() { + foreach ($this->getForm()->getFields() as $field) { + $found = false; + foreach ($this->getAnswers() as $answer) { + if ($answer->get('field_id') == $field->get('id')) { + $found = true; break; + } + } + if (!$found) { + # Section ID is auto set in the ::save method + $a = DynamicFormEntryAnswer::create( + array('field_id'=>$field->get('id'), 'entry_id'=>$this->id)); + $a->field = $field; + // Add to list of answers + $this->_values[] = $a; + $a->save(); + } + } + } + + function save() { + if (count($this->dirty)) + $this->set('updated', new SqlFunction('NOW')); + parent::save(); + foreach ($this->getAnswers() as $a) { + $a->set('value', $a->getField()->to_database($a->getField()->getClean())); + $a->set('entry_id', $this->get('id')); + $a->save(); + } + $this->_values = array(); + } + + static function create($ht=false) { + $inst = parent::create($ht); + $inst->set('created', new SqlFunction('NOW')); + foreach ($inst->getForm()->getFields() as $f) { + $a = DynamicFormEntryAnswer::create( + array('field_id'=>$f->get('id'))); + $a->field = $f; + $inst->_values[] = $a; + } + return $inst; + } +} + +/** + * Represents a single answer to a single field on a dynamic form section. + * The data / answer to the field is linked back to the form section and + * field which was originally used for the submission. + */ +class DynamicFormEntryAnswer extends VerySimpleModel { + + static $meta = array( + 'table' => FORM_ANSWER_TABLE, + 'ordering' => array('field__sort'), + 'pk' => array('entry_id', 'field_id'), + 'joins' => array( + 'field' => array( + 'constraint' => array('field_id' => 'DynamicFormField.id'), + ), + 'entry' => array( + 'constraint' => array('entry_id' => 'DynamicFormEntry.id'), + ), + ), + ); + + var $field; + var $form; + var $entry; + var $_value; + + function getEntry() { + return $this->entry; + } + + function getForm() { + if (!$this->form) + $this->form = $this->getEntry()->getForm(); + return $this->form; + } + + function getField() { + if (!isset($this->field)) { + $this->field = DynamicFormField::lookup($this->get('field_id'))->getImpl(); + $this->field->answer = $this; + } + return $this->field; + } + + function getValue() { + if (!$this->_value) + $this->_value = $this->getField()->to_php($this->get('value')); + return $this->_value; + } + + function toString() { + return $this->getField()->toString($this->getValue()); + } +} + +/** + * A collection of form sections makes up a "form" in the context of dynamic + * forms. This model represents that list of sections. The individual + * association of form sections to this form are delegated to the + * DynamicFormsetSections model + */ +class DynamicFormset extends VerySimpleModel { + + static $meta = array( + 'table' => FORMSET_TABLE, + 'ordering' => array('title'), + 'pk' => array('id'), + ); + + var $_forms; + + function getForms() { + if (!isset($this->_forms)) + $this->_forms = DynamicFormsetSections::objects()->filter( + array('formset_id'=>$this->id))->all(); + return $this->_forms; + } + + function hasField($name) { + foreach ($this->getForms() as $form) + foreach ($form->getForm()->getFields() as $f) + if ($f->get('name') == $name) + return true; + } + + function errors() { + return $this->_errors; + } + + function isValid() { + if (!$this->_errors) $this->_errors = array(); + return count($this->_errors) === 0; + } + + function save() { + if (count($this->dirty)) + $this->set('updated', new SqlFunction('NOW')); + return parent::save(); + } + + static function create($ht=false) { + $inst = parent::create($ht); + $inst->set('created', new SqlFunction('NOW')); + if (isset($ht['sections'])) { + $inst->save(); + foreach ($ht['sections'] as $s) { + $sort = 1; + if (isset($s['sort'])) { + $sort = $s['sort']; + unset($s['sort']); + } + $sec = DynamicFormSection::create($s); + $sec->save(); + DynamicFormsetSections::create(array( + 'formset_id' => $inst->id, + 'section_id' => $sec->id, + 'sort' => $sort + ))->save(); + } + } + return $inst; + } +} + +/** + * Represents an assocation of form section (DynamicFormSection) with a + * "form" (DynamicFormset). + */ +class DynamicFormsetSections extends VerySimpleModel { + static $meta = array( + 'table' => FORMSET_SEC_TABLE, + 'ordering' => array('sort'), + 'pk' => array('id'), + ); + + var $_section; + + function getForm() { + if (!isset($this->_section)) + $this->_section = DynamicFormSection::lookup($this->get('section_id')); + return $this->_section; + } + + function errors() { + return $this->_errors; + } + + function isValid() { + if (!$this->_errors) $this->_errors = array(); + if (!is_numeric($this->get('sort'))) + $this->_errors['sort'] = 'Enter a number'; + return count($this->errors()) === 0; + } +} + +/** + * Dynamic lists are used to represent list of arbitrary data that can be + * used as dropdown or typeahead selections in dynamic forms. This model + * defines a list. The individual items are stored in the DynamicListItem + * model. + */ +class DynamicList extends VerySimpleModel { + + static $meta = array( + 'table' => LIST_TABLE, + 'ordering' => array('name'), + 'pk' => array('id'), + ); + + var $_items; + + function getSortModes() { + return array( + 'Alpha' => 'Alphabetical', + '-Alpha' => 'Alphabetical (Reversed)', + 'SortCol' => 'Manually Sorted' + ); + } + + function getListOrderBy() { + switch ($this->sort_mode) { + case 'Alpha': return 'value'; + case '-Alpha': return '-value'; + case 'SortCol': return 'sort'; + } + } + + function getPluralName() { + if ($name = $this->get('plural_name')) + return $name; + else + return $this->get('name') . 's'; + } + + function getItems($limit=false, $offset=false) { + if (!$this->_items) { + $this->_items = DynamicListItem::objects()->filter( + array('list_id'=>$this->get('id'))) + ->order_by($this->getListOrderBy()); + if ($limit) + $this->_items->limit($limit); + if ($offset) + $this->_items->offset($offset); + } + return $this->_items; + } + + function getItemCount() { + return DynamicListItem::objects()->filter(array('list_id'=>$this->id)) + ->count(); + } + + function save($refetch=false) { + if (count($this->dirty)) + $this->set('updated', new SqlFunction('NOW')); + return parent::save($refetch); + } + + static function create($ht=false) { + $inst = parent::create($ht); + $inst->set('created', new SqlFunction('NOW')); + return $inst; + } + + static function getSelections() { + $selections = array(); + foreach (DynamicList::objects() as $list) { + $selections['list-'.$list->id] = + array('Selection: ' . $list->getPluralName(), + SelectionField, $list->get('id')); + } + return $selections; + } +} +FormField::addFieldTypes(array(DynamicList, 'getSelections')); + +/** + * Represents a single item in a dynamic list + * + * Fields: + * value - (char * 255) Actual list item content + * extra - (char * 255) Other values that represent the same item in the + * list, such as an abbreviation. In practice, should be a + * space-separated list of tokens which should hit this list item in a + * search + * sort - (int) If sorting by this field, represents the numeric sort order + * that this item should come in the dropdown list + */ +class DynamicListItem extends VerySimpleModel { + + static $meta = array( + 'table' => LIST_ITEM_TABLE, + 'pk' => array('id'), + 'joins' => array( + 'list' => array( + 'null' => true, + 'constraint' => array('list_id' => 'DynamicList.id'), + ), + ), + ); + + function toString() { + return $this->get('value'); + } + + function delete() { + # Don't really delete, just unset the list_id to un-associate it with + # the list + $this->set('list_id', null); + return $this->save(); + } +} + +class SelectionField extends FormField { + function getList() { + if (!$this->_list) { + $list_id = explode('-', $this->get('type')); + $list_id = $list_id[1]; + $this->_list = DynamicList::lookup($list_id); + } + return $this->_list; + } + + function getWidget() { + return new SelectionWidget($this); + } + + function parse($id) { + return $this->to_php($id); + } + + function to_php($id) { + if (!$id) + return null; + list($id, $value) = explode(':', $id); + $item = DynamicListItem::lookup($id); + # Attempt item lookup by name too + if (!$item) { + $item = DynamicListItem::objects()->filter(array( + 'value'=>$id, + 'list_id'=>$this->getList()->get('id'))); + $item = (count($item)) ? $item[0] : null; + } + return $item; + } + + function to_database($item) { + if ($item && $item->get('id')) + return $item->id . ':' . $item->value; + return null; + } + + function toString($item) { + return ($item) ? $item->toString() : ''; + } + + function getConfigurationOptions() { + return array( + 'typeahead' => new ChoiceField(array( + 'id'=>1, 'label'=>'Widget', 'required'=>false, + 'default'=>false, + 'choices'=>array(false=>'Drop Down', true=>'Typeahead'), + 'hint'=>'Typeahead will work better for large lists')), + ); + } +} + +class SelectionWidget extends ChoicesWidget { + function render() { + $config = $this->field->getConfiguration(); + $value = false; + if ($this->value && get_class($this->value) == 'DynamicListItem') { + // Loaded from database + $value = $this->value->get('id'); + $name = $this->value->get('value'); + } elseif ($this->value) { + // Loaded from POST + $value = $this->value; + $name = DynamicListItem::lookup($this->value); + $name = ($name) ? $name->get('value') : null; + } + + if (!$config['typeahead']) { + $this->value = $value; + return parent::render(); + } + + $source = array(); + foreach ($this->field->getList()->getItems() as $i) + $source[] = array( + 'info' => $i->get('value'), + 'value' => strtolower($i->get('value').' '.$i->get('extra')), + 'id' => $i->get('id')); + ?> + <span style="display:inline-block"> + <input type="hidden" name="<?php echo $this->name; ?>" + value="<?php echo $value; ?>" /> + <input type="text" size="30" id="<?php echo $this->name; ?>" + value="<?php echo $name; ?>" /> + <script type="text/javascript"> + $(function() { + $('#<?php echo $this->name; ?>').typeahead({ + source: <?php echo JsonDataEncoder::encode($source); ?>, + onselect: function(item) { + $('#<?php echo $this->name; ?>').val(item['info']) + $('input[name="<?php echo $this->name; ?>"]').val(item['id']) + } + }); + }); + </script> + </span> + <?php + } + + function getChoices() { + if (!$this->_choices) { + $this->_choices = array(); + foreach ($this->field->getList()->getItems() as $i) + $this->_choices[$i->get('id')] = $i->get('value'); + } + return $this->_choices; + } +} + +?> diff --git a/include/class.forms.php b/include/class.forms.php new file mode 100644 index 000000000..d01017843 --- /dev/null +++ b/include/class.forms.php @@ -0,0 +1,695 @@ +<?php +/********************************************************************* + class.forms.php + + osTicket forms framework + + Jared Hancock <jared@osticket.com> + Copyright (c) 2006-2013 osTicket + http://www.osticket.com + + Released under the GNU General Public License WITHOUT ANY WARRANTY. + See LICENSE.TXT for details. + + vim: expandtab sw=4 ts=4 sts=4: +**********************************************************************/ + +/** + * Form template, used for designing the custom form and for entering custom + * data for a ticket + */ +class Form { + var $fields = array(); + var $title = 'Unnamed Form'; + var $instructions = ''; + + function Form() { + call_user_func_array(array($this, '__construct'), func_get_args()); + } + function __construct($fields=array(), $title='Unnamed', $instructions='') { + $this->fields = $fields; + $this->title = $title; + $this->instructions = $instructions; + } + + function getFields() { + return $this->fields; + } + function getTitle() { return $this->title; } + function getInstructions() { return $this->instructions; } + + function isValid() { + $this->validate(); + foreach ($this->fields as $f) + if (!$f->isValidEntry()) + return false; + return true; + } + + function validate() { + foreach ($this->fields as $f) + $f->validateEntry(); + } +} + +require_once(INCLUDE_DIR . "class.json.php"); + +class FormField { + var $ht = array( + 'label' => 'Unlabeled', + 'required' => false, + 'default' => false, + 'configuration' => array(), + ); + + var $_cform; + + static $types = array( + 'text' => array('Short Answer', TextboxField), + 'memo' => array('Long Answer', TextareaField), + 'datetime' => array('Date and Time', DatetimeField), + 'phone' => array('Phone Number', PhoneField), + 'bool' => array('Checkbox', BooleanField), + 'choices' => array('Choices', ChoiceField), + ); + static $more_types = array(); + + function FormField() { + call_user_func_array(array($this, '__construct'), func_get_args()); + } + function __construct($options=array()) { + static $uid = 100; + $this->ht = array_merge($this->ht, $options); + if (!isset($this->ht['id'])) + $this->ht['id'] = $uid++; + } + + static function addFieldTypes($callable) { + static::$more_types[] = $callable; + } + + static function allTypes() { + if (static::$more_types) { + foreach (static::$more_types as $c) + static::$types = array_merge(static::$types, + call_user_func($c)); + static::$more_types = array(); + } + return static::$types; + } + + function get($what) { + return $this->ht[$what]; + } + + /** + * getClean + * + * Validates and cleans inputs from POST request. This is performed on a + * field instance, after a DynamicFormSet / DynamicFormSection is + * submitted via POST, in order to kick off parsing and validation of + * user-entered data. + */ + function getClean() { + $value = $this->getWidget()->value; + $value = $this->parse($value); + $this->validateEntry($value); + return $value; + } + + function errors() { + if (!$this->_errors) return array(); + else return $this->_errors; + } + + function isValidEntry() { + $this->validateEntry(); + return count($this->_errors) == 0; + } + + /** + * validateEntry + * + * Validates user entry on an instance of the field on a dynamic form. + * This is called when an instance of this field (like a TextboxField) + * receives data from the user and that value should be validated. + * + * Parameters: + * $value - (string) input from the user + */ + function validateEntry($value) { + # Validates a user-input into an instance of this field on a dynamic + # form + if (!is_array($this->_errors)) { + $this->_errors = array(); + + if ($this->get('required') && !$value) + $this->_errors[] = $this->getLabel() . ' is a required field'; + } + } + + /** + * parse + * + * Used to transform user-submitted data to a PHP value. This value is + * not yet considered valid. The ::validateEntry() method will be called + * on the value to determine if the entry is valid. Therefore, if the + * data is clearly invalid, return something like NULL that can easily + * be deemed invalid in ::validateEntry(), however, can still produce a + * useful error message indicating what is wrong with the input. + */ + function parse($value) { + return $value; + } + + /** + * to_php + * + * Transforms the data from the value stored in the database to a PHP + * value. The ::to_database() method is used to produce the database + * valse, so this method is the compliment to ::to_database(). + * + * Parameters: + * $value - (string or null) database representation of the field's + * content + */ + function to_php($value) { + return $value; + } + + /** + * to_database + * + * Determines the value to be stored in the database. The database + * backend for all fields is a text field, so this method should return + * a text value or NULL to represent the value of the field. The + * ::to_php() method will convert this value back to PHP. + * + * Paremeters: + * $value - PHP value of the field's content + */ + function to_database($value) { + return $value; + } + + /** + * toString + * + * Converts the PHP value created in ::parse() or ::to_php() to a + * pretty-printed value to show to the user. This is especially useful + * for something like dates which are stored considerably different in + * the database from their respective human-friendly versions. + * Furthermore, this method allows for internationalization and + * localization. + * + * Parametes: + * $value - PHP value of the field's content + */ + function toString($value) { + return $value; + } + + function getLabel() { return $this->get('label'); } + + /** + * getImpl + * + * Magic method that will return an implementation instance of this + * field based on the simple text value of the 'type' value of this + * field instance. The list of registered fields is determined by the + * global get_dynamic_field_types() function. The data from this model + * will be used to initialize the returned instance. + * + * For instance, if the value of this field is 'text', a TextField + * instance will be returned. + */ + function getImpl() { + // Allow registration with ::addFieldTypes and delayed calling + $types = static::allTypes(); + $clazz = $types[$this->get('type')][1]; + return new $clazz($this->ht); + } + + function getAnswer() { return $this->answer; } + + function getFormName() { + return '-field-id-'.$this->get('id'); + } + + function render() { + $this->getWidget()->render(); + } + + function getConfigurationOptions() { + return array(); + } + + /** + * getConfiguration + * + * Loads configuration information from database into hashtable format. + * Also, the defaults from ::getConfigurationOptions() are integrated + * into the database-backed options, so that if options have not yet + * been set or a new option has been added and not saved for this field, + * the default value will be reflected in the returned configuration. + */ + function getConfiguration() { + if (!$this->_config) { + $this->_config = $this->get('configuration'); + if (is_string($this->_config)) + $this->_config = JsonDataParser::parse($this->_config); + elseif (!$this->_config) + $this->_config = array(); + foreach ($this->getConfigurationOptions() as $name=>$field) + if (!isset($this->_config[$name])) + $this->_config[$name] = $field->get('default'); + } + return $this->_config; + } + + function isConfigurable() { + return true; + } + + function getConfigurationForm() { + if (!$this->_cform) { + $types = static::allTypes(); + $clazz = $types[$this->get('type')][1]; + $T = new $clazz(); + $this->_cform = $T->getConfigurationOptions(); + } + return $this->_cform; + } + +} + +class TextboxField extends FormField { + function getWidget() { + return new TextboxWidget($this); + } + + function getConfigurationOptions() { + return array( + 'size' => new TextboxField(array( + 'id'=>1, 'label'=>'Size', 'required'=>false, 'default'=>16, + 'validator' => 'number')), + 'length' => new TextboxField(array( + 'id'=>2, 'label'=>'Max Length', 'required'=>false, 'default'=>30, + 'validator' => 'number')), + 'validator' => new ChoiceField(array( + 'id'=>3, 'label'=>'Validator', 'required'=>false, 'default'=>'', + 'choices' => array('phone'=>'Phone Number','email'=>'Email Address', + 'ip'=>'IP Address', 'number'=>'Number', ''=>'None'))), + ); + } + + function validateEntry($value) { + parent::validateEntry($value); + $validators = array( + '' => null, + 'email' => array(array('Validator', 'is_email'), + 'Enter a valid email address'), + 'phone' => array(array('Validator', 'is_phone'), + 'Enter a valid phone number'), + 'ip' => array(array('Validator', 'is_ip'), + 'Enter a valid IP address'), + 'number' => array('is_numeric', 'Enter a number') + ); + // Support configuration forms, as well as GUI-based form fields + $valid = $this->get('validator'); + if (!$valid) { + $config = $this->getConfiguration(); + $valid = $config['validator']; + } + $func = $validators[$valid]; + if (is_array($func) && is_callable($func[0])) + if (!call_user_func($func[0], $value)) + $this->_errors[] = $func[1]; + } +} + +class TextareaField extends FormField { + function getWidget() { + return new TextareaWidget($this); + } + function getConfigurationOptions() { + return array( + 'cols' => new TextboxField(array( + 'id'=>1, 'label'=>'Width (chars)', 'required'=>true, 'default'=>40)), + 'rows' => new TextboxField(array( + 'id'=>2, 'label'=>'Height (rows)', 'required'=>false, 'default'=>4)), + 'length' => new TextboxField(array( + 'id'=>3, 'label'=>'Max Length', 'required'=>false, 'default'=>30)) + ); + } +} + +class PhoneField extends FormField { + function validateEntry($value) { + parent::validateEntry($value); + # Run validator against $this->value for email type + list($phone, $ext) = explode("X", $value, 2); + if ($phone && !Validator::is_phone($phone)) + $this->_errors[] = "Enter a valid phone number"; + if ($ext) { + if (!is_numeric($ext)) + $this->_errors[] = "Enter a valide phone extension"; + elseif (!$phone) + $this->_errors[] = "Enter a phone number for the extension"; + } + } + function getWidget() { + return new PhoneNumberWidget($this); + } + + function toString($value) { + list($phone, $ext) = explode("X", $value, 2); + $phone=Format::phone($phone); + if ($ext) + $phone.=" x$ext"; + return $phone; + } +} + +class BooleanField extends FormField { + function getWidget() { + return new CheckboxWidget($this); + } + + function getConfigurationOptions() { + return array( + 'desc' => new TextareaField(array( + 'id'=>1, 'label'=>'Description', 'required'=>false, 'default'=>'', + 'hint'=>'Text shown inline with the widget', + 'configuration'=>array('rows'=>2))) + ); + } + + function to_database($value) { + return ($value) ? '1' : '0'; + } + + function to_php($value) { + return ((int)$value) ? true : false; + } + + function toString($value) { + return ($value) ? 'Yes' : 'No'; + } +} + +class ChoiceField extends FormField { + function getWidget() { + return new ChoicesWidget($this); + } + + function getConfigurationOptions() { + return array( + 'choices' => new TextareaField(array( + 'id'=>1, 'label'=>'Choices', 'required'=>false, 'default'=>'')), + ); + } +} + +class DatetimeField extends FormField { + function getWidget() { + return new DatetimePickerWidget($this); + } + + function to_database($value) { + // Store time in gmt time, unix epoch format + return (string) $value; + } + + function to_php($value) { + if (!$value) + return $value; + else + return (int) $value; + } + + function parse($value) { + if (!$value) return null; + $config = $this->getConfiguration(); + return ($config['gmt']) ? Misc::db2gmtime($value) : strtotime($value); + } + + function toString($value) { + global $cfg; + $config = $this->getConfiguration(); + $format = ($config['time']) + ? $cfg->getDateTimeFormat() : $cfg->getDateFormat(); + if ($config['gmt']) + // Return time local to user's timezone + return Format::userdate($format, $value); + else + return Format::date($format, $value); + } + + function getConfigurationOptions() { + return array( + 'time' => new BooleanField(array( + 'id'=>1, 'label'=>'Time', 'required'=>false, 'default'=>false, + 'configuration'=>array( + 'desc'=>'Show time selection with date picker'))), + 'gmt' => new BooleanField(array( + 'id'=>2, 'label'=>'Timezone Aware', 'required'=>false, + 'configuration'=>array( + 'desc'=>"Show date/time relative to user's timezone"))), + 'min' => new DatetimeField(array( + 'id'=>3, 'label'=>'Earliest', 'required'=>false, + 'hint'=>'Earliest date selectable')), + 'max' => new DatetimeField(array( + 'id'=>4, 'label'=>'Latest', 'required'=>false, + 'default'=>null)), + 'future' => new BooleanField(array( + 'id'=>5, 'label'=>'Allow Future Dates', 'required'=>false, + 'default'=>true, 'configuration'=>array( + 'desc'=>'Allow entries into the future'))), + ); + } + + function validateEntry($value) { + $config = $this->getConfiguration(); + parent::validateEntry($value); + if (!$value) return; + if ($config['min'] and $value < $config['min']) + $this->_errors[] = 'Selected date is earlier than permitted'; + elseif ($config['max'] and $value > $config['max']) + $this->_errors[] = 'Selected date is later than permitted'; + // strtotime returns -1 on error for PHP < 5.1.0 and false thereafter + elseif ($value === -1 or $value === false) + $this->_errors[] = 'Enter a valid date'; + } +} + +class Widget { + function Widget() { + # Not called in PHP5 + call_user_func_array(array(&$this, '__construct'), func_get_args()); + } + + function __construct($field) { + $this->field = $field; + $this->name = $field->getFormName(); + if ($_SERVER['REQUEST_METHOD'] == 'POST') + $this->value = $this->getValue(); + elseif (is_object($field->getAnswer())) + $this->value = $field->getAnswer()->getValue(); + if (!$this->value && $field->value) + $this->value = $field->value; + } + + function getValue() { + return $_POST[$this->name]; + } +} + +class TextboxWidget extends Widget { + function render() { + $config = $this->field->getConfiguration(); + if (isset($config['size'])) + $size = "size=\"{$config['size']}\""; + if (isset($config['length'])) + $maxlength = "maxlength=\"{$config['length']}\""; + ?> + <span style="display:inline-block"> + <input type="text" id="<?php echo $this->name; ?>" + <?php echo $size . " " . $maxlength; ?> + name="<?php echo $this->name; ?>" + value="<?php echo Format::htmlchars($this->value); ?>"/> + </span> + <?php + } +} + +class TextareaWidget extends Widget { + function render() { + $config = $this->field->getConfiguration(); + if (isset($config['rows'])) + $rows = "rows=\"{$config['rows']}\""; + if (isset($config['cols'])) + $cols = "cols=\"{$config['cols']}\""; + if (isset($config['length'])) + $maxlength = "maxlength=\"{$config['length']}\""; + ?> + <span style="display:inline-block"> + <textarea <?php echo $rows." ".$cols." ".$length; ?> + name="<?php echo $this->name; ?>"><?php + echo Format::htmlchars($this->value); + ?></textarea> + </span> + <?php + } +} + +class PhoneNumberWidget extends Widget { + function render() { + list($phone, $ext) = explode("X", $this->value); + ?> + <input type="text" name="<?php echo $this->name; ?>" value="<?php + echo $phone; ?>"/> Ext: <input type="text" name="<?php + echo $this->name; ?>-ext" value="<?php echo $ext; ?>" size="5"/> + <?php + } + + function getValue() { + $ext = $_POST["{$this->name}-ext"]; + if ($ext) $ext = 'X'.$ext; + return parent::getValue() . $ext; + } +} + +class ChoicesWidget extends Widget { + function render() { + $config = $this->field->getConfiguration(); + // Determine the value for the default (the one listed if nothing is + // selected) + $def_key = $this->field->get('default'); + $choices = $this->getChoices(); + $have_def = isset($choices[$def_key]); + if (!$have_def) + $def_val = 'Select '.$this->field->get('label'); + else + $def_val = $choices[$def_key]; + ?> <span style="display:inline-block"> + <select name="<?php echo $this->name; ?>"> + <?php if (!$have_def) { ?> + <option value="<?php echo $def_key; ?>">— <?php + echo $def_val; ?> —</option> + <?php } + foreach ($choices as $key=>$name) { + if (!$have_def && $key == $def_key) + continue; ?> + <option value="<?php echo $key; ?>" + <?php if ($this->value == $key) echo 'selected="selected"'; + ?>><?php echo $name; ?></option> + <?php } ?> + </select> + </span> + <?php + } + + function getChoices() { + if ($this->_choices === null) { + // Allow choices to be set in this->ht (for configurationOptions) + $this->_choices = $this->field->get('choices'); + if (!$this->_choices) { + $this->_choices = array(); + $config = $this->field->getConfiguration(); + $choices = explode("\n", $config['choices']); + foreach ($choices as $choice) { + // Allow choices to be key: value + list($key, $val) = explode(':', $choice); + if ($val == null) + $val = $key; + $this->_choices[trim($key)] = trim($val); + } + } + } + return $this->_choices; + } +} + +class CheckboxWidget extends Widget { + function __construct($field) { + parent::__construct($field); + $this->name = '_field-checkboxes'; + } + + function render() { + $config = $this->field->getConfiguration(); + ?> + <input type="checkbox" name="<?php echo $this->name; ?>[]" <?php + if ($this->value) echo 'checked="checked"'; ?> value="<?php + echo $this->field->get('id'); ?>"/> + <?php + if ($config['desc']) { ?> + <em style="display:inline-block"><?php + echo Format::htmlchars($config['desc']); ?></em> + <?php } + } + + function getValue() { + if (count($_POST)) + return @in_array($this->field->get('id'), $_POST[$this->name]); + return parent::getValue(); + } +} + +class DatetimePickerWidget extends Widget { + function render() { + $config = $this->field->getConfiguration(); + if ($this->value) { + $this->value = (is_int($this->value) ? $this->value : + strtotime($this->value)); + if ($config['gmt']) + $this->value += 3600 * + $_SESSION['TZ_OFFSET']+($_SESSION['TZ_DST']?date('I',$time):0); + + list($hr, $min) = explode(':', date('H:i', $this->value)); + $this->value = date('m/d/Y', $this->value); + } + ?> + <input type="text" name="<?php echo $this->name; ?>" + value="<?php echo Format::htmlchars($this->value); ?>" size="12" + autocomplete="off" /> + <script type="text/javascript"> + $(function() { + $('input[name="<?php echo $this->name; ?>"]').datepicker({ + <?php + if ($config['min']) + echo "minDate: new Date({$config['min']}000),"; + if ($config['max']) + echo "maxDate: new Date({$config['max']}000),"; + elseif (!$config['future']) + echo "maxDate: new Date().getTime(),"; + ?> + numberOfMonths: 2, + showButtonPanel: true, + buttonImage: './images/cal.png', + showOn:'both' + }); + }); + </script> + <?php + if ($config['time']) + // TODO: Add time picker -- requires time picker or selection with + // Misc::timeDropdown + echo ' ' . Misc::timeDropdown($hr, $min, $this->name . ':time'); + } + + /** + * Function: getValue + * Combines the datepicker date value and the time dropdown selected + * time value into a single date and time string value. + */ + function getValue() { + $datetime = parent::getValue(); + if ($datetime && isset($_POST[$this->name . ':time'])) + $datetime .= ' ' . $_POST[$this->name . ':time']; + return $datetime; + } +} + +?> diff --git a/include/class.i18n.php b/include/class.i18n.php index 0f8bc8443..b7e8e95d8 100644 --- a/include/class.i18n.php +++ b/include/class.i18n.php @@ -41,17 +41,20 @@ class Internationalization { * system language and reload the data. */ function loadDefaultData() { + # notrans -- do not translate the contents of this array $models = array( - 'email_template_group.yaml' => 'EmailTemplateGroup', # notrans - 'department.yaml' => 'Dept', # notrans - 'sla.yaml' => 'SLA', # notrans - // Note that department and sla are required for help_topic - 'help_topic.yaml' => 'Topic', # notrans - 'filter.yaml' => 'Filter', # notrans - 'team.yaml' => 'Team', # notrans + 'email_template_group.yaml' => 'EmailTemplateGroup', + 'department.yaml' => 'Dept', + 'sla.yaml' => 'SLA', + 'forms.yaml' => 'DynamicFormset', + // Note that department, sla, and forms are required for + // help_topic + 'help_topic.yaml' => 'Topic', + 'filter.yaml' => 'Filter', + 'team.yaml' => 'Team', // Note that group requires department - 'group.yaml' => 'Group', # notrans - 'file.yaml' => 'AttachmentFile', # notrans + 'group.yaml' => 'Group', + 'file.yaml' => 'AttachmentFile', ); $errors = array(); diff --git a/include/class.json.php b/include/class.json.php index b5a589cfa..f3dfd282c 100644 --- a/include/class.json.php +++ b/include/class.json.php @@ -22,10 +22,12 @@ include_once "JSON.php"; class JsonDataParser { function parse($stream) { - $contents = ''; - while (!feof($stream)) { - $contents .= fread($stream, 8192); - } + if (is_resource($stream)) { + $contents = ''; + while (!feof($stream)) + $contents .= fread($stream, 8192); + } else + $contents = $stream; return self::decode($contents); } diff --git a/include/class.nav.php b/include/class.nav.php index a92a5629e..04080f255 100644 --- a/include/class.nav.php +++ b/include/class.nav.php @@ -175,6 +175,7 @@ class AdminNav extends StaffNav{ $tabs['dashboard']=array('desc'=>'Dashboard','href'=>'logs.php','title'=>'Admin Dashboard'); $tabs['settings']=array('desc'=>'Settings','href'=>'settings.php','title'=>'System Settings'); $tabs['manage']=array('desc'=>'Manage','href'=>'helptopics.php','title'=>'Manage Options'); + $tabs['forms']=array('desc'=>'Forms','href'=>'dynamic-forms.php','title'=>'Manage Forms'); $tabs['emails']=array('desc'=>'Emails','href'=>'emails.php','title'=>'Email Settings'); $tabs['staff']=array('desc'=>'Staff','href'=>'staff.php','title'=>'Manage Staff'); $this->tabs=$tabs; @@ -209,6 +210,11 @@ class AdminNav extends StaffNav{ $subnav[]=array('desc'=>'API Keys','href'=>'apikeys.php','iconclass'=>'api'); $subnav[]=array('desc'=>'Site Pages', 'href'=>'pages.php','title'=>'Pages','iconclass'=>'pages'); break; + case 'forms': + $subnav[]=array('desc'=>'Dynamic Forms','href'=>'dynamic-forms.php','iconclass'=>''); + $subnav[]=array('desc'=>'Form Sections','href'=>'dynamic-form-sections.php','iconclass'=>''); + $subnav[]=array('desc'=>'Dynamic Lists','href'=>'dynamic-lists.php','iconclass'=>''); + break; case 'emails': $subnav[]=array('desc'=>'Emails','href'=>'emails.php', 'title'=>'Email Addresses', 'iconclass'=>'emailSettings'); $subnav[]=array('desc'=>'Banlist','href'=>'banlist.php', diff --git a/include/class.orm.php b/include/class.orm.php new file mode 100644 index 000000000..c782de0b3 --- /dev/null +++ b/include/class.orm.php @@ -0,0 +1,672 @@ +<?php +/********************************************************************* + class.orm.php + + Simple ORM (Object Relational Mapper) for PHPv4 based on Django's ORM, + except that complex filter operations are not supported. The ORM simply + supports ANDed filter operations without any GROUP BY support. + + Jared Hancock <jared@osticket.com> + Copyright (c) 2006-2013 osTicket + http://www.osticket.com + + Released under the GNU General Public License WITHOUT ANY WARRANTY. + See LICENSE.TXT for details. + + vim: expandtab sw=4 ts=4 sts=4: +**********************************************************************/ + +class VerySimpleModel { + static $meta = array( + 'table' => false, + 'ordering' => false, + 'pk' => false + ); + + var $ht; + var $dirty; + var $__new__ = false; + + function __construct($row) { + $this->ht = $row; + $this->dirty = array(); + } + + function get($field) { + return $this->ht[$field]; + } + function __get($field) { + if (array_key_exists($field, $this->ht)) + return $this->ht[$field]; + return $this->{$field}; + } + + function set($field, $value) { + // XXX: Fully support or die if updating pk + // XXX: The contents of $this->dirty should be the value after the + // previous fetch or save. For instance, if the value is changed more + // than once, the original value should be preserved in the dirty list + // on the second edit. + $old = isset($this->ht[$field]) ? $this->ht[$field] : null; + if ($old != $value) { + $this->dirty[$field] = $old; + $this->ht[$field] = $value; + } + } + function __set($field, $value) { + return $this->set($field, $value); + } + + function setAll($props) { + foreach ($props as $field=>$value) + $this->set($field, $value); + } + + function _inspect() { + if (!static::$meta['table']) + throw new OrmConfigurationError( + 'Model does not define meta.table', $this); + } + + static function objects() { + return new QuerySet(get_called_class()); + } + + static function lookup($criteria) { + if (!is_array($criteria)) + // Model::lookup(1), where >1< is the pk value + $criteria = array(static::$meta['pk'][0] => $criteria); + $list = static::objects()->filter($criteria)->limit(1); + // TODO: Throw error if more than one result from database + return $list[0]; + } + + function delete($pk=false) { + $table = static::$meta['table']; + $sql = 'DELETE FROM '.$table; + + if (!$pk) $pk = static::$meta['pk']; + if (!is_array($pk)) $pk=array($pk); + + foreach ($pk as $p) + $filter[] = $p.' = '.$this->input($this->get($p)); + $sql .= ' WHERE '.implode(' AND ', $filter).' LIMIT 1'; + return db_affected_rows(db_query($sql)) == 1; + } + + function save($refetch=false) { + $pk = static::$meta['pk']; + if (!$this->isValid()) + return false; + if (!is_array($pk)) $pk=array($pk); + if ($this->__new__) + $sql = 'INSERT INTO '.static::$meta['table']; + else + $sql = 'UPDATE '.static::$meta['table']; + $filter = $fields = array(); + if (count($this->dirty) === 0) + return; + foreach ($this->dirty as $field=>$old) + if ($this->__new__ or !in_array($field, $pk)) + if (@get_class($this->get($field)) == 'SqlFunction') + $fields[] = $field.' = '.$this->get($field)->toSql(); + else + $fields[] = $field.' = '.db_input($this->get($field)); + foreach ($pk as $p) + $filter[] = $p.' = '.db_input($this->get($p)); + $sql .= ' SET '.implode(', ', $fields); + if (!$this->__new__) { + $sql .= ' WHERE '.implode(' AND ', $filter); + $sql .= ' LIMIT 1'; + } + if (db_affected_rows(db_query($sql)) != 1) { + throw new Exception(db_error()); + return false; + } + if ($this->__new__) { + if (count($pk) == 1) + $this->ht[$pk[0]] = db_insert_id(); + $this->__new__ = false; + } + # Refetch row from database + # XXX: Too much voodoo + if ($refetch) { + # XXX: Support composite PK + $criteria = array($pk[0] => $this->get($pk[0])); + $self = static::lookup($criteria); + $this->ht = $self->ht; + } + $this->dirty = array(); + return $this->get($pk[0]); + } + + static function create($ht=false) { + if (!$ht) $ht=array(); + $class = get_called_class(); + $i = new $class(array()); + $i->__new__ = true; + foreach ($ht as $field=>$value) + if (!is_array($value)) + $i->set($field, $value); + return $i; + } + + /** + * isValid + * + * Validates the contents of $this->ht before the model should be + * committed to the database. This is the validation for the field + * template -- edited in the admin panel for a form section. + */ + function isValid() { + return true; + } +} + +class SqlFunction { + function SqlFunction($name) { + $this->func = $name; + $this->args = array_slice(func_get_args(), 1); + } + + function toSql() { + $args = (count($this->args)) ? implode(',', db_input($this->args)) : ""; + return sprintf('%s(%s)', $this->func, $args); + } +} + +class QuerySet implements IteratorAggregate, ArrayAccess { + var $model; + + var $constraints = array(); + var $exclusions = array(); + var $ordering = array(); + var $limit = false; + var $offset = 0; + var $related = array(); + var $values = array(); + + var $compiler = 'MySqlCompiler'; + var $iterator = 'ModelInstanceIterator'; + + var $params; + var $query; + + function __construct($model) { + $this->model = $model; + } + + function filter() { + // Multiple arrays passes means OR + $this->constraints[] = func_get_args(); + return $this; + } + + function exclude() { + $this->exclusions[] = func_get_args(); + return $this; + } + + function order_by() { + $this->ordering = array_merge($this->ordering, func_get_args()); + return $this; + } + + function limit($count) { + $this->limit = $count; + return $this; + } + + function offset($at) { + $this->offset = $at; + return $this; + } + + function select_related() { + $this->related = array_merge($this->related, func_get_args()); + return $this; + } + + function values() { + $this->values = func_get_args(); + $this->iterator = 'HashArrayIterator'; + return $this; + } + + function all() { + return $this->getIterator()->asArray(); + } + + function count() { + $compiler = new $this->compiler(); + return $compiler->compileCount($this); + } + + // IteratorAggregate interface + function getIterator() { + if (!isset($this->_iterator)) + $this->_iterator = new $this->iterator($this); + return $this->_iterator; + } + + // ArrayAccess interface + function offsetExists($offset) { + return $this->getIterator()->offsetExists($offset); + } + function offsetGet($offset) { + return $this->getIterator()->offsetGet($offset); + } + function offsetUnset($a) { + throw new Exception('QuerySet is read-only'); + } + function offsetSet($a, $b) { + throw new Exception('QuerySet is read-only'); + } + + function __toString() { + return (string)$this->getQuery(); + } + + function getQuery() { + if (isset($this->query)) + return $this->query; + + // Load defaults from model + $model = $this->model; + if (!$this->ordering && isset($model::$meta['ordering'])) + $this->ordering = $model::$meta['ordering']; + + $compiler = new $this->compiler(); + $this->query = $compiler->compileSelect($this); + + return $this->query; + } +} + +class ModelInstanceIterator implements Iterator, ArrayAccess { + var $model; + var $resource; + var $cache = array(); + var $position = 0; + var $queryset; + + function __construct($queryset) { + $this->model = $queryset->model; + $this->resource = $queryset->getQuery(); + } + + function buildModel($row) { + // TODO: Traverse to foreign keys + return new $this->model($row); + } + + function fillTo($index) { + while ($this->resource && $index >= count($this->cache)) { + if ($row = $this->resource->getArray()) { + $this->cache[] = $this->buildModel($row); + } else { + $this->resource->close(); + $this->resource = null; + break; + } + } + } + + function asArray() { + $this->fillTo(PHP_INT_MAX); + return $this->cache; + } + + // Iterator interface + function rewind() { + $this->position = 0; + } + function current() { + $this->fillTo($this->position); + return $this->cache[$this->position]; + } + function key() { + return $this->position; + } + function next() { + $this->position++; + } + function valid() { + $this->fillTo($this->position); + return count($this->cache) > $this->position; + } + + // ArrayAccess interface + function offsetExists($offset) { + $this->fillTo($offset); + return $this->position >= $offset; + } + function offsetGet($offset) { + $this->fillTo($offset); + return $this->cache[$offset]; + } + function offsetUnset($a) { + throw new Exception(sprintf('%s is read-only', get_class($this))); + } + function offsetSet($a, $b) { + throw new Exception(sprintf('%s is read-only', get_class($this))); + } +} + +class MySqlCompiler { + var $params = array(); + + static $operators = array( + 'exact' => '%1$s = %2$s', + 'contains' => array('self', '__contains'), + 'gt' => '%1$s > %2$s', + 'lt' => '%1$s < %2$s', + 'isnull' => '%1$s IS NULL', + 'like' => '%1$s LIKE %2$s', + ); + + function __contains($a, $b) { + # {%a} like %{$b}% + return sprintf('%s LIKE %s', $a, $this->input("%$b%")); + } + + function _get_joins_and_field($field, $model, $options=array()) { + $joins = array(); + + // Break apart the field descriptor by __ (double-underbars). The + // first part is assumed to be the root field in the given model. + // The parts after each of the __ pieces are links to other tables. + // The last item (after the last __) is allowed to be an operator + // specifiction. + $parts = explode('__', $field); + $field = array_pop($parts); + if (array_key_exists($field, self::$operators)) { + $operator = self::$operators[$field]; + $field = array_pop($parts); + } else { + $operator = self::$operators['exact']; + } + + // Form the official join path (with the operator and foreign field + // removed) + $spec = implode('__', $parts); + + // TODO: If the join-spec already exists in the compiler, maybe use + // table aliases for the join a second time + + // Traverse through the parts and establish joins between the tables + // if the field is joined to a foreign model + if (count($parts) && isset($model::$meta['joins'][$parts[0]])) { + foreach ($parts as $p) { + $constraints = array(); + $info = $model::$meta['joins'][$p]; + $join = ' JOIN '; + if (isset($info['null']) && $info['null']) + $join = ' LEFT'.$join; + foreach ($info['constraint'] as $local => $foreign) { + $table = $model::$meta['table']; + list($model, $right) = explode('.', $foreign); + $constraints[] = sprintf("%s.%s = %s.%s", + $this->quote($table), $this->quote($local), + $this->quote($model::$meta['table']), $this->quote($right) + ); + } + $joins[] = $join.$this->quote($model::$meta['table']) + .' ON ('.implode(' AND ', $constraints).')'; + } + } + if (isset($options['table']) && $options['table']) + $field = $this->quote($model::$meta['table']); + elseif ($table) + $field = $this->quote($model::$meta['table']).'.'.$this->quote($field); + else + $field = $this->quote($field); + return array($joins, $field, $operator); + } + + function _compile_where($where, $model) { + $joins = array(); + $constrints = array(); + foreach ($where as $constraint) { + $filter = array(); + foreach ($constraint as $field=>$value) { + list($js, $field, $op) = self::_get_joins_and_field($field, $model); + $joins = array_merge($joins, $js); + // Allow operators to be callable rather than sprintf + // strings + if (is_callable($op)) + $filter[] = $op($field, $value); + else + $filter[] = sprintf($op, $field, $this->input($value)); + } + // Multiple constraints here are ANDed together + $constraints[] = implode(' AND ', $filter); + } + // Multiple constrains here are ORed together + $filter = implode(' OR ', $constraints); + if (count($constraints) > 1) + $filter = '(' . $filter . ')'; + return array($joins, $filter); + } + + function input($what) { + $this->params[] = $what; + return '?'; + } + + function quote($what) { + return "`$what`"; + } + + function getParams() { + return $this->params; + } + + function compileCount($queryset) { + $model = $queryset->model; + $table = $model::$meta['table']; + $where_pos = array(); + $where_neg = array(); + $joins = array(); + foreach ($queryset->constraints as $where) { + list($_joins, $filter) = $this->_compile_where($where, $model); + $where_pos[] = $filter; + $joins = array_merge($joins, $_joins); + } + foreach ($queryset->exclusions as $where) { + list($_joins, $filter) = $this->_compile_where($where, $model); + $where_neg[] = $filter; + $joins = array_merge($joins, $_joins); + } + + $where = ''; + if ($where_pos || $where_neg) { + $where = ' WHERE '.implode(' AND ', $where_pos) + .implode(' AND NOT ', $where_neg); + } + $sql = 'SELECT COUNT(*) AS count FROM '.$this->quote($table).$joins.$where; + $exec = new MysqlExecutor($sql, $this->params); + $row = $exec->getArray(); + return $row['count']; + } + + function compileSelect($queryset) { + $model = $queryset->model; + $where_pos = array(); + $where_neg = array(); + $joins = array(); + foreach ($queryset->constraints as $where) { + list($_joins, $filter) = $this->_compile_where($where, $model); + $where_pos[] = $filter; + $joins = array_merge($joins, $_joins); + } + foreach ($queryset->exclusions as $where) { + list($_joins, $filter) = $this->_compile_where($where, $model); + $where_neg[] = $filter; + $joins = array_merge($joins, $_joins); + } + + $where = ''; + if ($where_pos || $where_neg) { + $where = ' WHERE '.implode(' AND ', $where_pos) + .implode(' AND NOT ', $where_neg); + } + + $sort = ''; + if ($queryset->ordering) { + $orders = array(); + foreach ($queryset->ordering as $sort) { + $dir = 'ASC'; + if (substr($sort, 0, 1) == '-') { + $dir = 'DESC'; + $sort = substr($sort, 1); + } + list($js, $field) = $this->_get_joins_and_field($sort, $model); + $joins = ($joins) ? array_merge($joins, $js) : $js; + $orders[] = $field.' '.$dir; + } + $sort = ' ORDER BY '.implode(', ', $orders); + } + + // Include related tables + $fields = array(); + $table = $model::$meta['table']; + if ($queryset->related) { + $tables = array($this->quote($table)); + foreach ($queryset->related as $rel) { + list($js, $t) = $this->_get_joins_and_field($rel, $model, + array('table'=>true)); + $fields[] = $t.'.*'; + $joins = array_merge($joins, $js); + } + // Support only retrieving a list of values rather than a model + } elseif ($queryset->values) { + foreach ($queryset->values as $v) { + list($js, $fields[]) = $this->_get_joins_and_field($v, $model); + $joins = array_merge($joins, $js); + } + } else { + $fields[] = $this->quote($table).'.*'; + } + + if (is_array($joins)) + # XXX: This will change the order of the joins + $joins = implode('', array_unique($joins)); + $sql = 'SELECT '.implode(', ', $fields).' FROM ' + .$this->quote($table).$joins.$where.$sort; + if ($queryset->limit) + $sql .= ' LIMIT '.$queryset->limit; + if ($queryset->offset) + $sql .= ' OFFSET '.$queryset->offset; + + return new MysqlExecutor($sql, $this->params); + } + + function compileUpdate() { + } + + function compileInsert() { + } + + function compileDelete() { + } + + // Returns meta data about the table used to build queries + function inspectTable($table) { + } +} + +class MysqlExecutor { + + var $stmt; + var $fields = array(); + + var $sql; + var $params; + + function __construct($sql, $params) { + $this->sql = $sql; + $this->params = $params; + } + + function _prepare() { + if (!($this->stmt = db_prepare($this->sql))) + throw new Exception('Unable to prepare query: '.db_error() + .' '.$this->sql); + if (count($this->params)) + $this->_bind($this->params); + $this->stmt->execute(); + $this->_setup_output(); + $this->stmt->store_result(); + } + + function _bind($params) { + if (count($params) != $this->stmt->param_count) + throw new Exception('Parameter count does not match query'); + + $types = ''; + $ps = array(); + foreach ($params as $p) { + if (is_int($p)) + $types .= 'i'; + elseif (is_string($p)) + $types .= 's'; + $ps[] = &$p; + } + array_unshift($ps, $types); + call_user_func_array(array($this->stmt,'bind_param'), $ps); + } + + function _setup_output() { + $meta = $this->stmt->result_metadata(); + while ($f = $meta->fetch_field()) + $this->fields[] = $f; + } + + // Iterator interface + function rewind() { + if (!isset($this->stmt)) + $this->_prepare(); + $this->stmt->data_seek(0); + } + + function next() { + $status = $this->stmt->fetch(); + if ($status === false) + throw new Exception($this->stmt->error_list . db_error()); + elseif ($status === null) { + $this->close(); + return false; + } + return true; + } + + function getArray() { + $output = array(); + $variables = array(); + + if (!isset($this->stmt)) + $this->_prepare(); + + foreach ($this->fields as $f) + $variables[] = &$output[$f->name]; // pass by reference + + call_user_func_array(array($this->stmt, 'bind_result'), $variables); + if (!$this->next()) + return false; + return $output; + } + + function close() { + if (!$this->stmt) + return; + + $this->stmt->close(); + $this->stmt = null; + } + + function __toString() { + return $this->sql; + } +} +?> diff --git a/include/class.pdf.php b/include/class.pdf.php index ea6583786..b20eaf7ac 100644 --- a/include/class.pdf.php +++ b/include/class.pdf.php @@ -109,7 +109,7 @@ class Ticket2PDF extends FPDF function WriteText($w, $text, $border) { $this->SetFont('Times','',11); - $this->MultiCell($w, 5, $text, $border, 'L'); + $this->MultiCell($w, 7, $text, $border, 'L'); } @@ -187,7 +187,7 @@ class Ticket2PDF extends FPDF if($ticket->getIP()) $source.=' ('.$ticket->getIP().')'; $this->Cell($c, 7, $source, 1, 0, 'L', true); - $this->Ln(15); + $this->Ln(12); $this->SetFont('Arial', 'B', 11); if($ticket->isOpen()) { @@ -233,7 +233,30 @@ class Ticket2PDF extends FPDF $this->Cell($l, 7, 'Last Message', 1, 0, 'L', true); $this->SetFont(''); $this->Cell($c, 7, Format::db_datetime($ticket->getLastMsgDate()), 1, 1, 'L', true); - $this->Ln(5); + + $this->SetFillColor(255, 255, 255); + foreach (DynamicFormEntry::forTicket($ticket->getId()) as $form) { + $idx = 0; + foreach ($form->getAnswers() as $a) { + if (in_array($a->getField()->get('name'), + array('email','name','subject','phone'))) + continue; + $this->SetFont('Arial', 'B', 11); + if ($idx++ === 0) { + $this->Ln(5); + $this->SetFillColor(244, 250, 255); + $this->Cell(($l+$c)*2, 7, $a->getForm()->get('title'), + 1, 0, 'L', true); + $this->Ln(7); + $this->SetFillColor(255, 255, 255); + } + $this->Cell($l*2, 7, $a->getField()->get('label'), 1, 0, 'L', true); + $this->SetFont(''); + $this->WriteText($c*2, $a->toString(), 1); + } + } + $this->SetFillColor(244, 250, 255); + $this->Ln(10); $this->SetFont('Arial', 'B', 11); $this->cMargin = 0; diff --git a/include/class.ticket.php b/include/class.ticket.php index 2ab7ba1e6..2f22a1169 100644 --- a/include/class.ticket.php +++ b/include/class.ticket.php @@ -30,6 +30,7 @@ include_once(INCLUDE_DIR.'class.variable.php'); include_once(INCLUDE_DIR.'class.priority.php'); include_once(INCLUDE_DIR.'class.sla.php'); include_once(INCLUDE_DIR.'class.canned.php'); +require_once(INCLUDE_DIR.'class.dynamic_forms.php'); class Ticket { @@ -85,6 +86,8 @@ class Ticket { $this->id = $this->ht['ticket_id']; $this->number = $this->ht['ticketID']; + $this->loadDynamicData(); + //Reset the sub classes (initiated ondemand)...good for reloads. $this->staff = null; $this->client = null; @@ -102,6 +105,14 @@ class Ticket { return true; } + function loadDynamicData() { + $this->_answers = array(); + foreach (DynamicFormEntry::forTicket($this->getId()) as $form) + foreach ($form->getAnswers() as $answer) + $this->_answers[$answer->getField()->get('name')] = + $answer->toString(); + } + function reload() { return $this->load(); } @@ -170,8 +181,8 @@ class Ticket { return $this->number; } - function getEmail() { - return $this->ht['email']; + function getEmail(){ + return $this->_answers['email']; } function getAuthToken() { @@ -179,12 +190,12 @@ class Ticket { return md5($this->getId() . $this->getEmail() . SECRET_SALT); } - function getName() { - return $this->ht['name']; + function getName(){ + return $this->_answers['name']; } function getSubject() { - return $this->ht['subject']; + return $this->_answers['subject']; } /* Help topic title - NOT object -> $topic */ @@ -259,19 +270,17 @@ class Ticket { } function getPhone() { - return $this->ht['phone']; + list($phone, $ext) = explode(" ", $this->_answers['phone'], 2); + return $phone; } function getPhoneExt() { - return $this->ht['phone_ext']; + list($phone, $ext) = explode(" ", $this->_answers['phone'], 2); + return $ext; } function getPhoneNumber() { - $phone=Format::phone($this->getPhone()); - if(($ext=$this->getPhoneExt())) - $phone.=" $ext"; - - return $phone; + return $this->_answers['phone']; } function getSource() { @@ -288,11 +297,8 @@ class Ticket { function getUpdateInfo() { - $info=array('name' => $this->getName(), - 'email' => $this->getEmail(), - 'phone' => $this->getPhone(), + $info=array('phone' => $this->getPhone(), 'phone_ext' => $this->getPhoneExt(), - 'subject' => $this->getSubject(), 'source' => $this->getSource(), 'topicId' => $this->getTopicId(), 'priorityId' => $this->getPriorityId(), @@ -1069,6 +1075,12 @@ class Ticket { return $closedate; break; + default: + if (isset($this->_answers[$tag])) + # TODO: Use DynamicField to format the value, also, + # private field data should never go external, via + # email, for instance + return $this->_answers[$tag]; } return false; @@ -1625,13 +1637,9 @@ class Ticket { return false; $fields=array(); - $fields['name'] = array('type'=>'string', 'required'=>1, 'error'=>'Name required'); - $fields['email'] = array('type'=>'email', 'required'=>1, 'error'=>'Valid email required'); - $fields['subject'] = array('type'=>'string', 'required'=>1, 'error'=>'Subject required'); $fields['topicId'] = array('type'=>'int', 'required'=>1, 'error'=>'Help topic required'); $fields['priorityId'] = array('type'=>'int', 'required'=>1, 'error'=>'Priority required'); $fields['slaId'] = array('type'=>'int', 'required'=>0, 'error'=>'Select SLA'); - $fields['phone'] = array('type'=>'phone', 'required'=>0, 'error'=>'Valid phone # required'); $fields['duedate'] = array('type'=>'date', 'required'=>0, 'error'=>'Invalid date - must be MM/DD/YY'); $fields['note'] = array('type'=>'text', 'required'=>1, 'error'=>'Reason for the update required'); @@ -1650,22 +1658,9 @@ class Ticket { $errors['duedate']='Due date must be in the future'; } - //Make sure phone extension is valid - if($vars['phone_ext'] ) { - if(!is_numeric($vars['phone_ext']) && !$errors['phone']) - $errors['phone']='Invalid phone ext.'; - elseif(!$vars['phone']) //make sure they just didn't enter ext without phone # - $errors['phone']='Phone number required'; - } - if($errors) return false; $sql='UPDATE '.TICKET_TABLE.' SET updated=NOW() ' - .' ,email='.db_input($vars['email']) - .' ,name='.db_input(Format::striptags($vars['name'])) - .' ,subject='.db_input(Format::striptags($vars['subject'])) - .' ,phone="'.db_input($vars['phone'],false).'"' - .' ,phone_ext='.db_input($vars['phone_ext']?$vars['phone_ext']:NULL) .' ,priority_id='.db_input($vars['priorityId']) .' ,topic_id='.db_input($vars['topicId']) .' ,sla_id='.db_input($vars['slaId']) @@ -1708,11 +1703,14 @@ class Ticket { if(!$extId || !is_numeric($extId)) return 0; - $sql ='SELECT ticket_id FROM '.TICKET_TABLE.' ticket ' - .' WHERE ticketID='.db_input($extId); + $sql ='SELECT ticket.ticket_id FROM '.TICKET_TABLE.' ticket ' + .' LEFT JOIN '.FORM_ENTRY_TABLE.' entry ON entry.ticket_id = ticket.ticket_id ' + .' LEFT JOIN '.FORM_ANSWER_TABLE.' email ON email.entry_id = entry.id ' + .' LEFT JOIN '.FORM_FIELD_TABLE.' field ON email.field_id = field.id ' + .' WHERE field.name = "email" AND ticket.ticketID='.db_input($extId); if($email) - $sql.=' AND email='.db_input($email); + $sql .= ' AND email.value = '.db_input($email); if(($res=db_query($sql)) && db_num_rows($res)) list($id)=db_fetch_row($res); @@ -1763,15 +1761,6 @@ class Ticket { return $id; } - function getOpenTicketsByEmail($email) { - - $sql='SELECT count(*) as open FROM '.TICKET_TABLE.' WHERE status='.db_input('open').' AND email='.db_input($email); - if(($res=db_query($sql)) && db_num_rows($res)) - list($num)=db_fetch_row($res); - - return $num; - } - /* Quick staff's tickets stats */ function getStaffStats($staff) { global $cfg; @@ -1835,7 +1824,10 @@ class Ticket { ON (open.ticket_id=ticket.ticket_id AND open.status=\'open\') ' .' LEFT JOIN '.TICKET_TABLE.' closed ON (closed.ticket_id=ticket.ticket_id AND closed.status=\'closed\')' - .' WHERE ticket.email='.db_input($email); + .' LEFT JOIN '.FORM_ENTRY_TABLE.' entry ON entry.ticket_id = ticket.ticket_id ' + .' LEFT JOIN '.FORM_ANSWER_TABLE.' email ON email.entry_id = entry.id ' + .' LEFT JOIN '.FORM_FIELD_TABLE.' field ON email.field_id = field.id ' + .' WHERE field.name = "email" AND email.value = '.db_input($email); return db_fetch_array(db_query($sql)); } @@ -1895,9 +1887,6 @@ class Ticket { $id=0; $fields=array(); - $fields['name'] = array('type'=>'string', 'required'=>1, 'error'=>'Name required'); - $fields['email'] = array('type'=>'email', 'required'=>1, 'error'=>'Valid email required'); - $fields['subject'] = array('type'=>'string', 'required'=>1, 'error'=>'Subject required'); $fields['message'] = array('type'=>'text', 'required'=>1, 'error'=>'Message required'); switch (strtolower($origin)) { case 'web': @@ -1918,19 +1907,10 @@ class Ticket { $errors['err']=$errors['origin'] = 'Invalid origin given'; } $fields['priorityId'] = array('type'=>'int', 'required'=>0, 'error'=>'Invalid Priority'); - $fields['phone'] = array('type'=>'phone', 'required'=>0, 'error'=>'Valid phone # required'); if(!Validator::process($fields, $vars, $errors) && !$errors['err']) $errors['err'] ='Missing or invalid data - check the errors and try again'; - //Make sure phone extension is valid - if($vars['phone_ext'] ) { - if(!is_numeric($vars['phone_ext']) && !$errors['phone']) - $errors['phone']='Invalid phone ext.'; - elseif(!$vars['phone']) //make sure they just didn't enter ext without phone # XXX: reconsider allowing! - $errors['phone']='Phone number required'; - } - //Make sure the due date is valid if($vars['duedate']) { if(!$vars['time'] || strpos($vars['time'],':')===false) @@ -1996,11 +1976,6 @@ class Ticket { .' ,dept_id='.db_input($deptId) .' ,topic_id='.db_input($topicId) .' ,priority_id='.db_input($priorityId) - .' ,email='.db_input($vars['email']) - .' ,name='.db_input(Format::striptags($vars['name'])) - .' ,subject='.db_input(Format::striptags($vars['subject'])) - .' ,phone="'.db_input($vars['phone'],false).'"' - .' ,phone_ext='.db_input($vars['phone_ext']?$vars['phone_ext']:'') .' ,ip_address='.db_input($ipaddress) .' ,source='.db_input($source); diff --git a/include/class.topic.php b/include/class.topic.php index 801e6e684..0bbcdc8b3 100644 --- a/include/class.topic.php +++ b/include/class.topic.php @@ -209,6 +209,15 @@ class Topic { elseif(($tid=self::getIdByName($vars['topic'], $vars['pid'])) && $tid!=$id) $errors['topic']='Topic already exists'; + if (!$vars['formset_id']) + $errors['formset_id'] = 'You must select a form'; + else { + $group=DynamicFormset::lookup($vars['formset_id']); + foreach (array('name', 'email', 'subject') as $f) + if (!$group->hasField($f)) + $errors['formset_id']="Form set must define the '$f' field"; + } + if(!$vars['dept_id']) $errors['dept_id']='You must select a department'; @@ -223,6 +232,7 @@ class Topic { .',dept_id='.db_input($vars['dept_id']) .',priority_id='.db_input($vars['priority_id']) .',sla_id='.db_input($vars['sla_id']) + .',formset_id='.db_input($vars['formset_id']) .',page_id='.db_input($vars['page_id']) .',isactive='.db_input($vars['isactive']) .',ispublic='.db_input($vars['ispublic']) diff --git a/include/client/header.inc.php b/include/client/header.inc.php index 2849ccbe3..e03f36cbb 100644 --- a/include/client/header.inc.php +++ b/include/client/header.inc.php @@ -13,7 +13,10 @@ header("Content-Type: text/html; charset=UTF-8\r\n"); <link rel="stylesheet" href="<?php echo ROOT_PATH; ?>css/osticket.css" media="screen"> <link rel="stylesheet" href="<?php echo ASSETS_PATH; ?>css/theme.css" media="screen"> <link rel="stylesheet" href="<?php echo ASSETS_PATH; ?>css/print.css" media="print"> - <link type="text/css" href="<?php echo ROOT_PATH; ?>css/ui-lightness/jquery-ui-1.10.3.custom.min.css" rel="stylesheet" /> + <link rel="stylesheet" href="<?php echo ROOT_PATH; ?>scp/css/typeahead.css" + media="screen" /> + <link type="text/css" href="<?php echo ROOT_PATH; ?>css/ui-lightness/jquery-ui-1.10.3.custom.min.css" + rel="stylesheet" media="screen" /> <link rel="stylesheet" href="<?php echo ROOT_PATH; ?>css/thread.css" media="screen"> <link rel="stylesheet" href="<?php echo ROOT_PATH; ?>css/redactor.css" media="screen"> <link type="text/css" rel="stylesheet" href="<?php echo ROOT_PATH; ?>css/font-awesome.min.css"> @@ -21,6 +24,7 @@ header("Content-Type: text/html; charset=UTF-8\r\n"); <script type="text/javascript" src="<?php echo ROOT_PATH; ?>js/jquery-ui-1.10.3.custom.min.js"></script> <script src="<?php echo ROOT_PATH; ?>js/jquery.multifile.js"></script> <script src="<?php echo ROOT_PATH; ?>js/osticket.js"></script> + <script src="<?php echo ROOT_PATH; ?>scp/js/bootstrap-typeahead.js"></script> <script type="text/javascript" src="<?php echo ROOT_PATH; ?>js/redactor.min.js"></script> <script type="text/javascript" src="<?php echo ROOT_PATH; ?>js/redactor-osticket.js"></script> <?php diff --git a/include/client/open.inc.php b/include/client/open.inc.php index a0a97b7a3..f39a0ded1 100644 --- a/include/client/open.inc.php +++ b/include/client/open.inc.php @@ -16,47 +16,14 @@ $info=($_POST && $errors)?Format::htmlchars($_POST):$info; <?php csrf_token(); ?> <input type="hidden" name="a" value="open"> <table width="800" cellpadding="1" cellspacing="0" border="0"> - <tr> - <th class="required" width="160">Full Name:</th> - <td> - <?php - if($thisclient && $thisclient->isValid()) { - echo $thisclient->getName(); - } else { ?> - <input id="name" type="text" name="name" size="30" value="<?php echo $info['name']; ?>"> - <font class="error">* <?php echo $errors['name']; ?></font> - <?php - } ?> - </td> - </tr> - <tr> - <th class="required" width="160">Email Address:</th> - <td> - <?php - if($thisclient && $thisclient->isValid()) { - echo $thisclient->getEmail(); - } else { ?> - <input id="email" type="text" name="email" size="30" value="<?php echo $info['email']; ?>"> - <font class="error">* <?php echo $errors['email']; ?></font> - <?php - } ?> - </td> - </tr> - <tr> - <th>Telephone:</th> - <td> - - <input id="phone" type="text" name="phone" size="17" value="<?php echo $info['phone']; ?>"> - <label for="ext" class="inline">Ext.:</label> - <input id="ext" type="text" name="phone_ext" size="3" value="<?php echo $info['phone_ext']; ?>"> - <font class="error"> <?php echo $errors['phone']; ?> <?php echo $errors['phone_ext']; ?></font> - </td> - </tr> - <tr><td colspan=2> </td></tr> + <tbody> <tr> <td class="required">Help Topic:</td> <td> - <select id="topicId" name="topicId"> + <select id="topicId" name="topicId" onchange="javascript: + $('#dynamic-form').load( + 'ajax.php/form/help-topic/' + this.value); + "> <option value="" selected="selected">— Select a Help Topic —</option> <?php if($topics=Topic::getPublicHelpTopics()) { @@ -72,13 +39,16 @@ $info=($_POST && $errors)?Format::htmlchars($_POST):$info; <font class="error">* <?php echo $errors['topicId']; ?></font> </td> </tr> - <tr> - <td class="required">Subject:</td> - <td> - <input id="subject" type="text" name="subject" size="40" value="<?php echo $info['subject']; ?>"> - <font class="error">* <?php echo $errors['subject']; ?></font> - </td> - </tr> + </tbody> + <tbody id="dynamic-form"> + <?php if ($forms) { + foreach ($forms as $form) { + include(CLIENTINC_DIR . 'templates/dynamic-form.tmpl.php'); + } + } ?> + </tbody> + <tbody> + <tr><td colspan="2"><hr /></td></tr> <tr> <td class="required">Message:</td> <td> @@ -144,6 +114,7 @@ $info=($_POST && $errors)?Format::htmlchars($_POST):$info; <?php } ?> <tr><td colspan=2> </td></tr> + </tbody> </table> <p style="padding-left:150px;"> <input type="submit" value="Create Ticket"> diff --git a/include/client/templates/dynamic-form.tmpl.php b/include/client/templates/dynamic-form.tmpl.php new file mode 100644 index 000000000..49ff01697 --- /dev/null +++ b/include/client/templates/dynamic-form.tmpl.php @@ -0,0 +1,53 @@ +<?php + // Form headline and deck with a horizontal divider above and an extra + // space below. + // XXX: Would be nice to handle the decoration with a CSS class + ?> + <tr><td colspan="2"><hr /> + <div class="form-header" style="margin-bottom:0.5em"> + <h3><?php echo Format::htmlchars($form->getTitle()); ?></h3> + <em><?php echo Format::htmlchars($form->getInstructions()); ?></em> + </div> + </td></tr> + <?php + // Form fields, each with corresponding errors follows. Fields marked + // 'private' are not included in the output for clients + global $thisclient; + foreach ($form->getFields() as $field) { + if ($thisclient) { + switch ($field->get('name')) { + case 'name': + $field->value = $thisclient->getName(); + break; + case 'email': + $field->value = $thisclient->getEmail(); + break; + case 'phone': + $field->value = $thisclient->getPhone(); + break; + } + } + if ($field->get('private')) + continue; + ?> + <tr><td class="<?php if ($field->get('required')) echo 'required'; ?>"> + <?php echo Format::htmlchars($field->get('label')); ?>:</td> + <td><?php $field->render(); ?> + <?php if ($field->get('required')) { ?> + <font class="error">*</font> + <?php + } + if ($field->get('hint')) { ?> + <br /><em style="color:gray;display:inline-block"><?php + echo Format::htmlchars($field->get('hint')); ?></em> + <?php + } + foreach ($field->errors() as $e) { ?> + <br /> + <font class="error"><?php echo $e; ?></font> + <?php } ?> + </td> + </tr> + <?php + } +?> diff --git a/include/client/tickets.inc.php b/include/client/tickets.inc.php index 8eda7eca3..81284b271 100644 --- a/include/client/tickets.inc.php +++ b/include/client/tickets.inc.php @@ -38,13 +38,25 @@ if($order_by && strpos($order_by,',')) $x=$sort.'_sort'; $$x=' class="'.strtolower($order).'" '; -$qselect='SELECT ticket.ticket_id,ticket.ticketID,ticket.dept_id,isanswered, dept.ispublic, ticket.subject, ticket.name, ticket.email '. - ',dept_name,ticket. status, ticket.source, ticket.created '; +$qselect='SELECT ticket.ticket_id,ticket.ticketID,ticket.dept_id,isanswered, ' + .'dept.ispublic, subject.value as subject, name.value as name, email.value as email, ' + .'dept_name,ticket. status, ticket.source, ticket.created '; + +$dynfields='(SELECT entry.ticket_id, value FROM '.FORM_ANSWER_TABLE.' ans '. + 'LEFT JOIN '.FORM_ENTRY_TABLE.' entry ON entry.id=ans.entry_id '. + 'LEFT JOIN '.FORM_FIELD_TABLE.' field ON field.id=ans.field_id '. + 'WHERE field.name = "%1$s") %1$s ON ticket.ticket_id = %1$s.ticket_id '; +$subject_sql = sprintf($dynfields, 'subject'); +$email_sql = sprintf($dynfields, 'email'); +$name_sql = sprintf($dynfields, 'name'); $qfrom='FROM '.TICKET_TABLE.' ticket ' - .' LEFT JOIN '.DEPT_TABLE.' dept ON (ticket.dept_id=dept.dept_id) '; + .' LEFT JOIN '.DEPT_TABLE.' dept ON (ticket.dept_id=dept.dept_id) ' + .' LEFT JOIN '.$subject_sql + .' LEFT JOIN '.$email_sql + .' LEFT JOIN '.$name_sql; -$qwhere =' WHERE ticket.email='.db_input($thisclient->getEmail()); +$qwhere =' WHERE email.value='.db_input($thisclient->getEmail()); if($status){ $qwhere.=' AND ticket.status='.db_input($status); diff --git a/include/client/view.inc.php b/include/client/view.inc.php index f85cff8da..0ec5b9941 100644 --- a/include/client/view.inc.php +++ b/include/client/view.inc.php @@ -52,6 +52,30 @@ if(!$dept || !$dept->isPublic()) </table> </td> </tr> + <tr> +<?php +foreach (DynamicFormEntry::forTicket($ticket->getId()) as $idx=>$form) { + $answers = $form->getAnswers(); + if ($idx > 0 and $idx % 2 == 0) { ?> + </tr><tr> + <?php } ?> + <td width="50%"> + <table class="infoTable" cellspacing="1" cellpadding="3" width="100%" border="0"> + <?php foreach ($answers as $answer) { + if (in_array($answer->getField()->get('name'), array('name', 'email', 'subject'))) + continue; + elseif ($answer->getField()->get('private')) + continue; + ?> + <tr> + <th width="100"><?php echo $answer->getField()->get('label'); + ?>:</th> + <td><?php echo $answer->toString(); ?></td> + </tr> + <?php } ?> + </table></td> +<?php } ?> +</tr> </table> <br> <h2>Subject:<?php echo Format::htmlchars($ticket->getSubject()); ?></h2> diff --git a/include/i18n/en_US/forms.yaml b/include/i18n/en_US/forms.yaml new file mode 100644 index 000000000..451889b14 --- /dev/null +++ b/include/i18n/en_US/forms.yaml @@ -0,0 +1,47 @@ +# +# Default (dynamic) form configuration +# +# Fields: +--- +- id: 1 + title: Default + sections: + - title: User Information + sort: 10 + fields: + - type: text # notrans + name: email # notrans + label: Email Address + required: true + sort: 10 + configuration: + size: 40 + length: 40 + validator: email # notrans + + - type: text # notrans + name: name # notrans + label: Full Name + required: true + sort: 20 + configuration: + size: 40 + length: 40 + + - type: phone # notrans + name: phone # notrans + label: Phone Number + required: false + sort: 30 + + - title: Ticket Details + sort: 20 + fields: + - type: text # notrans + name: subject # notrans + label: Subject + hint: Issue Summary + sort: 10 + configuration: + size: 40 + length: 50 diff --git a/include/i18n/en_US/help_topic.yaml b/include/i18n/en_US/help_topic.yaml index 11d7e819c..2c99305c0 100644 --- a/include/i18n/en_US/help_topic.yaml +++ b/include/i18n/en_US/help_topic.yaml @@ -23,6 +23,7 @@ dept_id: 1 sla_id: 1 priority_id: 2 + formset_id: 1 topic: Support notes: | Tickets that primarily concern the support deparment @@ -32,6 +33,7 @@ dept_id: 1 sla_id: 0 priority_id: 2 + formset_id: 1 topic: Billing notes: | Tickets that primarily concern the sales and billing deparments diff --git a/include/mysqli.php b/include/mysqli.php index ad545a5db..13ce86ec5 100644 --- a/include/mysqli.php +++ b/include/mysqli.php @@ -225,6 +225,11 @@ function db_field_type($res, $col=0) { return $res->fetch_field_direct($col); # nolint } +function db_prepare($stmt) { + global $__db; + return $__db->prepare($stmt); +} + function db_connect_error() { global $__db; return $__db->connect_error; diff --git a/include/staff/dynamic-form-section.inc.php b/include/staff/dynamic-form-section.inc.php new file mode 100644 index 000000000..800f1b97b --- /dev/null +++ b/include/staff/dynamic-form-section.inc.php @@ -0,0 +1,142 @@ +<?php + +$info=array(); +if($form && $_REQUEST['a']!='add') { + $title = 'Update dynamic form section'; + $action = 'update'; + $submit_text='Save Changes'; + $info = $form->ht; + $newcount=2; +} else { + $title = 'Add new dynamic form section'; + $action = 'add'; + $submit_text='Add Form Section'; + $newcount=4; +} +$info=Format::htmlchars(($errors && $_POST)?$_POST:$info); + +?> +<form action="?" method="post" id="save"> + <?php csrf_token(); ?> + <input type="hidden" name="do" value="<?php echo $action; ?>"> + <input type="hidden" name="id" value="<?php echo $info['id']; ?>"> + <h2>Dynamic Form Section</h2> + <table class="form_table" width="940" border="0" cellspacing="0" cellpadding="2"> + <thead> + <tr> + <th colspan="2"> + <h4><?php echo $title; ?></h4> + <em>Dynamic forms are used to allow custom data to be + associated with tickets</em> + </th> + </tr> + </thead> + <tbody> + <tr> + <td width="180" class="required">Title:</td> + <td><input type="text" name="title" size="40" value="<?php + echo $info['title']; ?>"/></td> + </tr> + <tr> + <td width="180">Instructions:</td> + <td><textarea name="instructions" rows="3" cols="40"><?php + echo $info['instructions']; ?></textarea> + </td> + </tr> + <tr> + <td width="180">Internal Notes:</td> + <td><textarea name="notes" rows="4" cols="80"><?php + echo $info['notes']; ?></textarea> + </td> + </tr> + </tbody> + </table> + <table class="form_table" width="940" border="0" cellspacing="0" cellpadding="2"> + <thead> + <tr> + <th colspan="7"> + <em>Form Fields</em> + </th> + </tr> + <tr> + <th>Delete</th> + <th>Label</th> + <th>Type</th> + <th>Name</th> + <th>Private</th> + <th>Required</th> + </tr> + </thead> + <tbody class="sortable-rows" data-sort="sort-"> + <?php if ($form) foreach ($form->getFields() as $f) { + $id = $f->get('id'); + $errors = $f->errors(); ?> + <tr> + <td><input type="checkbox" name="delete-<?php echo $id; ?>"/> + <input type="hidden" name="sort-<?php echo $id; ?>" + value="<?php echo $f->get('sort'); ?>"/> + <font class="error"><?php + if ($errors['sort']) echo '<br/>'; echo $errors['sort']; + ?></font> + </td> + <td><input type="text" size="32" name="label-<?php echo $id; ?>" + value="<?php echo $f->get('label'); ?>"/></td> + <td><select name="type-<?php echo $id; ?>"> + <?php foreach (FormField::allTypes() as $type=>$nfo) { ?> + <option value="<?php echo $type; ?>" <?php + if ($f->get('type') == $type) echo 'selected="selected"'; ?>> + <?php echo $nfo[0]; ?></option> + <?php } ?> + </select> + <?php if ($f->isConfigurable()) { ?> + <a class="action-button" style="float:none" + href="ajax.php/form/field-config/<?php + echo $f->get('id'); ?>" + onclick="javascript: + $('#overlay').show(); + $('#field-config .body').load(this.href); + $('#field-config').show(); + return false; + "><i class="icon-edit"></i> Config</a> + <?php } ?></td> + <td> + <input type="text" size="20" name="name-<?php echo $id; ?>" + value="<?php echo $f->get('name'); ?>"/> + <font class="error"><?php + if ($errors['name']) echo '<br/>'; echo $errors['name']; + ?></font> + </td> + <td><input type="checkbox" name="private-<?php echo $id; ?>" + <?php if ($f->get('private')) echo 'checked="checked"'; ?>/></td> + <td><input type="checkbox" name="required-<?php echo $id; ?>" + <?php if ($f->get('required')) echo 'checked="checked"'; ?>/></td> + </tr> + <?php + } + for ($i=0; $i<$newcount; $i++) { ?> + <td><em>add</em> + <input type="hidden" name="sort-new-<?php echo $i; ?>"/></td> + <td><input type="text" size="32" name="label-new-<?php echo $i; ?>"/></td> + <td><select name="type-new-<?php echo $i; ?>"> + <?php foreach (FormField::allTypes() as $type=>$nfo) { ?> + <option value="<?php echo $type; ?>"> + <?php echo $nfo[0]; ?></option> + <?php } ?> + </select></td> + <td><input type="text" size="20" name="name-new-<?php echo $i; ?>"/></td> + <td><input type="checkbox" name="private-new-<?php echo $i; ?>"/></td> + <td><input type="checkbox" name="required-new-<?php echo $i; ?>"/></td> + </tr> + <?php } ?> + </tbody> + </table> +<p style="padding-left:225px;"> + <input type="submit" name="submit" value="<?php echo $submit_text; ?>"> + <input type="reset" name="reset" value="Reset"> + <input type="button" name="cancel" value="Cancel" onclick='window.location.href="?"'> +</p> +</form> + +<div style="display:none;" class="dialog" id="field-config"> + <div class="body"></div> +</div> diff --git a/include/staff/dynamic-form-sections.inc.php b/include/staff/dynamic-form-sections.inc.php new file mode 100644 index 000000000..ed715668a --- /dev/null +++ b/include/staff/dynamic-form-sections.inc.php @@ -0,0 +1,41 @@ +<div style="width:700;padding-top:5px; float:left;"> + <h2>Dynamic Form Sections</h2> +</div> +<div style="float:right;text-align:right;padding-top:5px;padding-right:5px;"> + <b><a href="dynamic-form-sections.php?a=add" class="Icon">Add New Dynamic Form Section</a></b></div> +<div class="clear"></div> + +<?php +$page = ($_GET['p'] && is_numeric($_GET['p'])) ? $_GET['p'] : 1; +$count = DynamicFormSection::objects()->count(); +$pageNav = new Pagenate($count, $page, PAGE_LIMIT); +$pageNav->setURL('dynamic-form-sections.php'); +$showing=$pageNav->showing().' form sections'; +?> + +<table class="list" border="0" cellspacing="1" cellpadding="0" width="940"> + <caption><?php echo $showing; ?></caption> + <thead> + <tr> + <th width="7"> </th> + <th>Title</th> + <th>Last Updated</th> + </tr> + </thead> + <tbody> + <?php foreach (DynamicFormSection::objects()->order_by('title') + ->limit($pageNav->getLimit()) + ->offset($pageNav->getStart()) as $form) { ?> + <tr> + <td/> + <td><a href="?id=<?php echo $form->get('id'); ?>"><?php echo $form->get('title'); ?></a></td> + <td><?php echo $form->get('updated'); ?></td> + </tr> + <?php } + ?> + </tbody> +</table> +<?php +if ($count) //Show options.. + echo '<div> Page:'.$pageNav->getPageLinks().' </div>'; +?> diff --git a/include/staff/dynamic-form.inc.php b/include/staff/dynamic-form.inc.php new file mode 100644 index 000000000..c2753063d --- /dev/null +++ b/include/staff/dynamic-form.inc.php @@ -0,0 +1,104 @@ +<?php + +$info=array(); +if($group && $_REQUEST['a']!='add') { + $title = 'Update dynamic form'; + $action = 'update'; + $submit_text='Save Changes'; + $info = $group->ht; + $newcount=2; +} else { + $title = 'Add new dynamic form'; + $action = 'add'; + $submit_text='Add Form'; + $newcount=4; +} +$info=Format::htmlchars(($errors && $_POST)?$_POST:$info); + +?> +<form action="?" method="post" id="save"> + <?php csrf_token(); ?> + <input type="hidden" name="do" value="<?php echo $action; ?>"> + <input type="hidden" name="id" value="<?php echo $info['id']; ?>"> + <h2>Dynamic Form</h2> + <table class="form_table" width="940" border="0" cellspacing="0" cellpadding="2"> + <thead> + <tr> + <th colspan="4"> + <h4><?php echo $title; ?></h4> + <em>Dynamic forms are used to combine several form sections + into a larger form for use in the ticketing system. This + allows for common sections to be reused among various forms + </em> + </th> + </tr> + </thead> + <tbody> + <tr> + <td width="180" class="required">Title:</td> + <td colspan="3"><input size="40" type="text" name="title" + value="<?php echo $info['title']; ?>"/></td> + </tr> + <tr> + <td width="180">Description:</td> + <td colspan="3"><textarea name="notes" rows="3" cols="40"><?php + echo $info['notes']; ?></textarea> + </td> + </tr> + </tbody> + <tbody> + <tr><th>Delete</th><th>Form name</th></tr> + </tbody> + <tbody class="sortable-rows" data-sort="sort-"> + <?php if ($group) foreach ($group->getForms() as $formatt) { + $form = $formatt->getForm(); + $errors = $formatt->errors(); ?> + <tr> + <td> + <input type="checkbox" name="delete-<?php echo $formatt->get('id'); ?>"/> + <input type="hidden" name="sort-<?php echo $formatt->get('id'); ?>" + value="<?php echo $formatt->get('sort'); ?>"/> + <font class="error"><?php + if ($errors['sort']) echo '<br/>'; echo $errors['sort']; + ?></font> + </td><td> + <select name="section_id-<?php echo $formatt->get('id'); ?>"> + <?php foreach (DynamicFormSection::objects() as $form) { ?> + <option value="<?php echo $form->get('id'); ?>" <?php + if ($formatt->get('section_id') == $form->get('id')) + echo 'selected="selected"'; ?>> + <?php echo $form->get('title'); ?> + </option> + <?php } ?> + </select> + <a class="action-button" style="float:none" + href="dynamic-form-sections.php?id=<?php + echo $formatt->get('section_id'); ?>"><i class="icon-edit"></i + > Edit</a> + </td> + </tr> + <?php } + for ($i=0; $i<$newcount; $i++) { ?> + <tr> + <td><em>add</em> + <input type="hidden" name="sort-new-<?php echo $i; ?>" size="4"/> + </td><td> + <select name="section_id-new-<?php echo $i; ?>"> + <option value="0">— Select Form —</option> + <?php foreach (DynamicFormSection::objects() as $form) { ?> + <option value="<?php echo $form->get('id'); ?>"> + <?php echo $form->get('title'); ?> + </option> + <?php } ?> + </select> + </td> + </tr> + <?php } ?> + </tbody> +</table> +<p style="padding-left:225px;"> + <input type="submit" name="submit" value="<?php echo $submit_text; ?>"> + <input type="reset" name="reset" value="Reset"> + <input type="button" name="cancel" value="Cancel" onclick='window.location.href="?"'> +</p> +</form> diff --git a/include/staff/dynamic-forms.inc.php b/include/staff/dynamic-forms.inc.php new file mode 100644 index 000000000..fdb93ca79 --- /dev/null +++ b/include/staff/dynamic-forms.inc.php @@ -0,0 +1,41 @@ +<div style="width:700;padding-top:5px; float:left;"> + <h2>Dynamic Forms</h2> +</div> +<div style="float:right;text-align:right;padding-top:5px;padding-right:5px;"> + <b><a href="dynamic-forms.php?a=add" class="Icon">Add Dynamic Form</a></b></div> +<div class="clear"></div> + +<?php +$page = ($_GET['p'] && is_numeric($_GET['p'])) ? $_GET['p'] : 1; +$count = DynamicFormset::objects()->count(); +$pageNav = new Pagenate($count, $page, PAGE_LIMIT); +$pageNav->setURL('dynamic-lists.php'); +$showing=$pageNav->showing().' dynamic forms'; +?> + +<table class="list" border="0" cellspacing="1" cellpadding="0" width="940"> + <caption><?php echo $showing; ?></caption> + <thead> + <tr> + <th width="7"> </th> + <th>Title</th> + <th>Last Updated</th> + </tr> + </thead> + <tbody> + <?php foreach (DynamicFormset::objects()->order_by('title') + ->limit($pageNav->getLimit()) + ->offset($pageNav->getStart()) as $form) { ?> + <tr> + <td/> + <td><a href="?id=<?php echo $form->get('id'); ?>"><?php echo $form->get('title'); ?></a></td> + <td><?php echo $form->get('updated'); ?></td> + </tr> + <?php } + ?> + </tbody> +</table> +<?php +if ($count) //Show options.. + echo '<div> Page:'.$pageNav->getPageLinks().' </div>'; +?> diff --git a/include/staff/dynamic-list.inc.php b/include/staff/dynamic-list.inc.php new file mode 100644 index 000000000..02d5e9314 --- /dev/null +++ b/include/staff/dynamic-list.inc.php @@ -0,0 +1,119 @@ +<?php + +$info=array(); +if($list && $_REQUEST['a']!='add') { + $title = 'Update dynamic list'; + $action = 'update'; + $submit_text='Save Changes'; + $info = $list->ht; + $newcount=2; +} else { + $title = 'Add new dynamic list'; + $action = 'add'; + $submit_text='Add List'; + $newcount=4; +} +$info=Format::htmlchars(($errors && $_POST)?$_POST:$info); + +?> +<form action="?" method="post" id="save"> + <?php csrf_token(); ?> + <input type="hidden" name="do" value="<?php echo $action; ?>"> + <input type="hidden" name="id" value="<?php echo $info['id']; ?>"> + <h2>Dynamic List</h2> + <table class="form_table" width="940" border="0" cellspacing="0" cellpadding="2"> + <thead> + <tr> + <th colspan="2"> + <h4><?php echo $title; ?></h4> + <em>Dynamic lists are used to provide selection boxes for dynamic forms</em> + </th> + </tr> + </thead> + <tbody> + <tr> + <td width="180" class="required">Name:</td> + <td><input size="50" type="text" name="name" value="<?php echo $info['name']; ?>"/></td> + </tr> + <tr> + <td width="180">Plural Name:</td> + <td><input size="50" type="text" name="name_plural" value="<?php echo $info['name_plural']; ?>"/></td> + </tr> + <tr> + <td width="180">Sort Order:</td> + <td><select name="sort_mode"> + <?php foreach (DynamicList::getSortModes() as $key=>$desc) { ?> + <option value="<?php echo $key; ?>" <?php + if ($key == $info['sort_mode']) echo 'selected="selected"'; + ?>><?php echo $desc; ?></option> + <?php } ?> + </select></td> + </tr> + <tr> + <td width="180">Description:</td> + <td><textarea name="notes" rows="3" cols="40"><?php + echo $info['notes']; ?></textarea> + </td> + </tr> + </tbody> + </table> + <table class="form_table" width="940" border="0" cellspacing="0" cellpadding="2"> + <thead> + <?php if ($list) { + $page = ($_GET['p'] && is_numeric($_GET['p'])) ? $_GET['p'] : 1; + $count = $list->getItemCount(); + $pageNav = new Pagenate($count, $page, PAGE_LIMIT); + $pageNav->setURL('dynamic-list.php', 'id='.urlencode($_REQUEST['id'])); + $showing=$pageNav->showing().' list items'; + ?> + <?php } + ?> + <tr> + <th colspan="4"> + <em><?php echo $showing; ?></em> + </th> + </tr> + <tr> + <th>Delete</th> + <th>Value</th> + <th>Extra</th> + </tr> + </thead> + <tbody <?php if ($info['sort_mode'] == 'SortCol') { ?> + class="sortable-rows" data-sort="sort-"<?php } ?>> + <?php if ($list) + $icon = ($info['sort_mode'] == 'SortCol') + ? '<i class="icon-sort"></i> ' : ''; + foreach ($list->getItems($pageNav->getLimit(), $pageNav->getStart()) as $i) { + $id = $i->get('id'); ?> + <tr> + <td><?php echo $icon; ?> + <input type="checkbox" name="delete-<?php echo $id; ?>"/> + <input type="hidden" name="sort-<?php echo $id; ?>" + value="<?php echo $i->get('sort'); ?>"/></td> + <td><input type="text" size="40" name="value-<?php echo $id; ?>" + value="<?php echo $i->get('value'); ?>"/></td> + <td><input type="text" size="20" name="extra-<?php echo $id; ?>" + value="<?php echo $i->get('extra'); ?>"/></td> + </tr> + <?php } + for ($i=0; $i<$newcount; $i++) { ?> + <tr> + <td><?php echo $icon; ?> <em>add</em> + <input type="hidden" name="sort-new-<?php echo $i; ?>"/></td> + <td><input type="text" size="40" name="value-new-<?php echo $i; ?>"/></td> + <td><input type="text" size="20" name="extra-new-<?php echo $i; ?>"/></td> + </tr> + <?php } ?> + </tbody> + </table> +<?php +if ($count) //Show options.. + echo '<div> Page:'.$pageNav->getPageLinks().' </div>'; +?> +<p style="padding-left:225px;"> + <input type="submit" name="submit" value="<?php echo $submit_text; ?>"> + <input type="reset" name="reset" value="Reset"> + <input type="button" name="cancel" value="Cancel" onclick='window.location.href="?"'> +</p> +</form> diff --git a/include/staff/dynamic-lists.inc.php b/include/staff/dynamic-lists.inc.php new file mode 100644 index 000000000..0427666d6 --- /dev/null +++ b/include/staff/dynamic-lists.inc.php @@ -0,0 +1,43 @@ +<div style="width:700;padding-top:5px; float:left;"> + <h2>Dynamic Lists</h2> +</div> +<div style="float:right;text-align:right;padding-top:5px;padding-right:5px;"> + <b><a href="dynamic-lists.php?a=add" class="Icon">Add New Dynamic List</a></b></div> +<div class="clear"></div> + +<?php +$page = ($_GET['p'] && is_numeric($_GET['p'])) ? $_GET['p'] : 1; +$count = DynamicList::objects()->count(); +$pageNav = new Pagenate($count, $page, PAGE_LIMIT); +$pageNav->setURL('dynamic-lists.php'); +$showing=$pageNav->showing().' dynamic lists'; +?> + +<table class="list" border="0" cellspacing="1" cellpadding="0" width="940"> + <caption><?php echo $showing; ?></caption> + <thead> + <tr> + <th width="7"> </th> + <th>List Name</th> + <th>Created</th> + <th>Last Updated</th> + </tr> + </thead> + <tbody> + <?php foreach (DynamicList::objects()->order_by('name') + ->limit($pageNav->getLimit()) + ->offset($pageNav->getStart()) as $list) { ?> + <tr> + <td/> + <td><a href="?id=<?php echo $list->get('id'); ?>"><?php echo $list->get('name'); ?></a></td> + <td><?php echo $list->get('created'); ?></td> + <td><?php echo $list->get('updated'); ?></td> + </tr> + <?php } + ?> + </tbody> +</table> +<?php +if ($count) //Show options.. + echo '<div> Page:'.$pageNav->getPageLinks().' </div>'; +?> diff --git a/include/staff/header.inc.php b/include/staff/header.inc.php index e5b04bff5..d4e2ce612 100644 --- a/include/staff/header.inc.php +++ b/include/staff/header.inc.php @@ -23,7 +23,8 @@ <link rel="stylesheet" href="./css/scp.css" media="screen"> <link rel="stylesheet" href="<?php echo ROOT_PATH; ?>css/redactor.css" media="screen"> <link rel="stylesheet" href="./css/typeahead.css" media="screen"> - <link type="text/css" href="<?php echo ROOT_PATH; ?>css/ui-lightness/jquery-ui-1.10.3.custom.min.css" rel="stylesheet" /> + <link type="text/css" href="<?php echo ROOT_PATH; ?>css/ui-lightness/jquery-ui-1.10.3.custom.min.css" + rel="stylesheet" media="screen" /> <link type="text/css" rel="stylesheet" href="../css/font-awesome.min.css"> <link type="text/css" rel="stylesheet" href="./css/dropdown.css"> <script type="text/javascript" src="./js/jquery.dropdown.js"></script> diff --git a/include/staff/helptopic.inc.php b/include/staff/helptopic.inc.php index fdf82a404..07bc83a70 100644 --- a/include/staff/helptopic.inc.php +++ b/include/staff/helptopic.inc.php @@ -89,6 +89,22 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info); </tr> <tr><th colspan="2"><em>New ticket options</em></th></tr> + <tr> + <td><strong>Formset</strong>:</td> + <td><select name="formset_id"> + <option value="0">— Select a Formset —</option> + <?php foreach (DynamicFormset::objects() as $group) { ?> + <option value="<?php echo $group->get('id'); ?>" + <?php if ($group->get('id') == $info['formset_id']) + echo 'selected="selected"'; ?>> + <?php echo $group->get('title'); ?> + </option> + <?php } ?> + </select> + <em>Information for tickets associated with this help topic</em> + <span class="error"> <?php echo $errors['formset_id']; ?></span> + </td> + </tr> <tr> <td width="180" class="required"> Priority: @@ -217,7 +233,6 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info); this topic (Overrides Dept. settings). </td> </tr> - <tr> <th colspan="2"> <em><strong>Admin Notes</strong>: Internal notes about the help topic. </em> diff --git a/include/staff/templates/dynamic-field-config.tmpl.php b/include/staff/templates/dynamic-field-config.tmpl.php new file mode 100644 index 000000000..3751670f4 --- /dev/null +++ b/include/staff/templates/dynamic-field-config.tmpl.php @@ -0,0 +1,79 @@ + <h3>Field Configuration — <?php echo $field->get('label') ?></h3> + <a class="close" href="">×</a> + <hr/> + <form method="post" action="ajax.php/form/field-config/<?php + echo $field->get('id'); ?>" onsubmit="javascript: + var form = $(this); + $.post(this.action, form.serialize(), function(data, status, xhr) { + if (!data.length) { + form.closest('.dialog').hide(); + $('#overlay').hide(); + } else { + form.closest('.dialog').empty().append(data); + } + }); + return false; + "> + <table> + <?php + echo csrf_token(); + $config = $field->getConfiguration(); + foreach ($field->getConfigurationForm() as $name=>$f) { + if (isset($config[$name])) + $f->value = $config[$name]; + else if ($f->get('default')) + $f->value = $f->get('default'); + ?> + <tr><td> + <label for="<?php echo $f->getWidget()->name; ?>" + style="vertical-align:top;padding-top:0.2em"> + <?php echo Format::htmlchars($f->get('label')); ?>:</label> + </td><td> + <span style="display:inline-block"> + <?php + $f->render(); + if ($f->get('required')) { ?> + <font class="error">*</font> + <?php + } + if ($f->get('hint')) { ?> + <br /><em style="color:gray;display:inline-block"><?php + echo Format::htmlchars($f->get('hint')); ?></em> + <?php + } + ?> + </span> + <?php + foreach ($f->errors() as $e) { ?> + <br /> + <font class="error"><?php echo $e; ?></font> + <?php } ?> + </td></tr> + <?php + } + ?> + <tr><td> + <label for="hint" + style="vertical-align:top;padding-top:0.2em">Help Text:</label> + </td><td> + <span style="display:inline-block"> + <textarea name="hint" rows="2" cols="40"><?php + echo Format::htmlchars($field->get('hint')); ?></textarea> + <br /> + <em style="color:gray;display:inline-block"> + Help text shown with the field</em> + </span> + </td></tr> + </table> + <hr style="margin-top:3em"/> + <p class="full-width"> + <span class="buttons" style="float:left"> + <input type="reset" value="Reset"> + <input type="button" value="Cancel" class="close"> + </span> + <span class="buttons" style="float:right"> + <input type="submit" value="Save"> + </span> + </p> + </form> + <div class="clear"></div> diff --git a/include/staff/templates/dynamic-form.tmpl.php b/include/staff/templates/dynamic-form.tmpl.php new file mode 100644 index 000000000..93bb577ea --- /dev/null +++ b/include/staff/templates/dynamic-form.tmpl.php @@ -0,0 +1,28 @@ + <tr><th colspan="2"> + <em><strong><?php echo Format::htmlchars($form->getTitle()); ?></strong>: + <?php echo Format::htmlchars($form->getInstructions()); ?></em> + </th></tr> + <?php + foreach ($form->getFields() as $field) { + ?> + <tr><td class="multi-line <?php if ($field->get('required')) echo 'required'; ?>"> + <?php echo Format::htmlchars($field->get('label')); ?>:</td> + <td><?php $field->render(); ?> + <?php if ($field->get('required')) { ?> + <font class="error">*</font> + <?php + } + if ($field->get('hint')) { ?> + <br /><em style="color:gray;display:inline-block"><?php + echo Format::htmlchars($field->get('hint')); ?></em> + <?php + } + foreach ($field->errors() as $e) { ?> + <br /> + <font class="error"><?php echo $e; ?></font> + <?php } ?> + </td> + </tr> + <?php + } +?> diff --git a/include/staff/ticket-edit.inc.php b/include/staff/ticket-edit.inc.php index 4a641d782..fca5a366c 100644 --- a/include/staff/ticket-edit.inc.php +++ b/include/staff/ticket-edit.inc.php @@ -19,35 +19,6 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$ticket->getUpdateInfo()); </tr> </thead> <tbody> - <tr> - <td width="160" class="required"> - Full Name: - </td> - <td> - <input type="text" size="50" name="name" value="<?php echo $info['name']; ?>"> - <span class="error">* <?php echo $errors['name']; ?></span> - </td> - </tr> - <tr> - <td width="160" class="required"> - Email Address: - </td> - <td> - <input type="text" size="50" name="email" value="<?php echo $info['email']; ?>"> - <span class="error">* <?php echo $errors['email']; ?></span> - </td> - </tr> - <tr> - <td width="160"> - Phone Number: - </td> - <td> - <input type="text" size="20" name="phone" value="<?php echo $info['phone']; ?>"> - <span class="error"> <?php echo $errors['phone']; ?></span> - Ext <input type="text" size="6" name="phone_ext" value="<?php echo $info['phone_ext']; ?>"> - <span class="error"> <?php echo $errors['phone_ext']; ?></span> - </td> - </tr> <tr> <th colspan="2"> <em><strong>Ticket Information</strong>: Due date overrides SLA's grace period.</em> @@ -107,15 +78,6 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$ticket->getUpdateInfo()); <font class="error">* <?php echo $errors['priorityId']; ?></font> </td> </tr> - <tr> - <td width="160" class="required"> - Subject: - </td> - <td> - <input type="text" name="subject" size="60" value="<?php echo $info['subject']; ?>"> - <font class="error">* <?php $errors['subject']; ?></font> - </td> - </tr> <tr> <td width="160"> SLA Plan: @@ -153,6 +115,14 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$ticket->getUpdateInfo()); <em>Time is based on your time zone (GMT <?php echo $thisstaff->getTZoffset(); ?>)</em> </td> </tr> + </tbody> + <tbody id="dynamic-form"> + <?php if ($forms) + foreach ($forms as $form) { + include(STAFFINC_DIR . 'templates/dynamic-form.tmpl.php'); + } ?> + </tbody> + <tbody> <tr> <th colspan="2"> <em><strong>Internal Note</strong>: Reason for editing the ticket (required) <font class="error"> <?php echo $errors['note'];?></font></em> diff --git a/include/staff/ticket-open.inc.php b/include/staff/ticket-open.inc.php index cf9c49dfb..ab0eba20e 100644 --- a/include/staff/ticket-open.inc.php +++ b/include/staff/ticket-open.inc.php @@ -13,48 +13,25 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info); <tr> <th colspan="2"> <h4>New Ticket</h4> - <em><strong>User Information</strong></em> </th> </tr> </thead> <tbody> + <?php + if($cfg->notifyONNewStaffTicket()) { ?> <tr> - <td width="160" class="required"> - Email Address: - </td> - <td> - - <input type="text" size="50" name="email" id="email" class="typeahead" value="<?php echo $info['email']; ?>" - autocomplete="off" autocorrect="off" autocapitalize="off"> - <span class="error">* <?php echo $errors['email']; ?></span> - <?php - if($cfg->notifyONNewStaffTicket()) { ?> - - <input type="checkbox" name="alertuser" <?php echo (!$errors || $info['alertuser'])? 'checked="checked"': ''; ?>>Send alert to user. - <?php - } ?> - </td> - </tr> - <tr> - <td width="160" class="required"> - Full Name: - </td> - <td> - <input type="text" size="50" name="name" id="name" value="<?php echo $info['name']; ?>"> - <span class="error">* <?php echo $errors['name']; ?></span> - </td> + <th> + <em><strong>User Information</strong></em> + </th> </tr> - <tr> - <td width="160"> - Phone Number: - </td> + <td width="160">Alert:</td> <td> - <input type="text" size="20" name="phone" id="phone" value="<?php echo $info['phone']; ?>"> - <span class="error"> <?php echo $errors['phone']; ?></span> - Ext <input type="text" size="6" name="phone_ext" id="phone_ext" value="<?php echo $info['phone_ext']; ?>"> - <span class="error"> <?php echo $errors['phone_ext']; ?></span> + + <input type="checkbox" name="alertuser" <?php echo (!$errors || $info['alertuser'])? 'checked="checked"': ''; ?>>Send alert to user. </td> </tr> + <?php + } ?> <tr> <th colspan="2"> <em><strong>Ticket Information & Options</strong>:</em> @@ -99,7 +76,10 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info); Help Topic: </td> <td> - <select name="topicId"> + <select name="topicId" onchange="javascript: + $('#dynamic-form').load( + 'ajax.php/form/help-topic/' + this.value); + "> <option value="" selected >— Select Help Topic —</option> <?php if($topics=Topic::getHelpTopics()) { @@ -204,6 +184,15 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info); </tr> <?php } ?> + </tbody> + <tbody id="dynamic-form"> + <?php if ($forms) { + foreach ($forms as $form) { + include(STAFFINC_DIR . 'templates/dynamic-form.tmpl.php'); + } + } ?> + </tbody> + <tbody> <tr> <th colspan="2"> <em><strong>Issue</strong>: The user will be able to see the issue summary below and any associated responses.</em> @@ -211,14 +200,7 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info); </tr> <tr> <td colspan=2> - <div> - <em><strong>Subject</strong>: Issue summary </em> <font class="error">* <?php echo $errors['subject']; ?></font><br> - <input type="text" name="subject" size="60" value="<?php echo $info['subject']; ?>"> - </div> - <div style="margin-top:0.5em; margin-bottom:0.5em"> - <em><strong>Issue</strong></em> - <font class="error">* <?php echo $errors['issue']; ?></font> - </div> + <div><em><strong>Issue</strong>: Details on the reason(s) for opening the ticket.</em> <font class="error">* <?php echo $errors['issue']; ?></font></div> <textarea class="richtext ifhtml draft draft-delete" placeholder="Details on the reason(s) for opening the ticket." data-draft-namespace="ticket.staff" name="issue" diff --git a/include/staff/ticket-view.inc.php b/include/staff/ticket-view.inc.php index c8dcde436..5b4359c78 100644 --- a/include/staff/ticket-view.inc.php +++ b/include/staff/ticket-view.inc.php @@ -265,6 +265,42 @@ if($ticket->isOverdue()) </td> </tr> </table> +<br> +<table class="ticket_info" cellspacing="0" cellpadding="0" width="940" border="0"> +<?php +$idx = 0; +foreach (DynamicFormEntry::forTicket($ticket->getId()) as $form) { + // Skip core fields shown earlier in the ticket view + // TODO: Rewrite getAnswers() so that one could write + // ->getAnswers()->filter(not(array('field__name__in'=> + // array('email', ...)))); + $answers = array_filter($form->getAnswers(), function ($a) { + return !in_array($a->getField()->get('name'), + array('email','subject','name','phone')); + }); + if (count($answers) == 0) + continue; + ?> + </tr><tr> + <td colspan="2"> + <table cellspacing="0" cellpadding="4" width="100%" border="0"> + <?php foreach($answers as $a) { ?> + <tr> + <th width="100"><?php + echo $a->getField()->get('label'); + ?>:</th> + <td><?php + echo $a->toString(); + ?></td> + </tr> + <?php } ?> + </table> + </td> + <?php + $idx++; + } ?> + </tr> +</table> <div class="clear"></div> <h2 style="padding:10px 0 5px 0; font-size:11pt;"><?php echo Format::htmlchars($ticket->getSubject()); ?></h2> <?php diff --git a/include/staff/tickets.inc.php b/include/staff/tickets.inc.php index ac38d5ff8..732c5c277 100644 --- a/include/staff/tickets.inc.php +++ b/include/staff/tickets.inc.php @@ -12,7 +12,7 @@ $searchTerm=''; //make sure the search query is 3 chars min...defaults to no query with warning message if($search) { $searchTerm=$_REQUEST['query']; - if( ($_REQUEST['query'] && strlen($_REQUEST['query'])<3) + if( ($_REQUEST['query'] && strlen($_REQUEST['query'])<3) || (!$_REQUEST['query'] && isset($_REQUEST['basic_search'])) ){ //Why do I care about this crap... $search=false; //Instead of an error page...default back to regular query..with no search. $errors['err']='Search term must be more than 3 chars'; @@ -21,7 +21,7 @@ if($search) { } $showoverdue=$showanswered=false; $staffId=0; //Nothing for now...TODO: Allow admin and manager to limit tickets to single staff level. -$showassigned= true; //show Assigned To column - defaults to true +$showassigned= true; //show Assigned To column - defaults to true //Get status we are actually going to use on the query...making sure it is clean! $status=null; @@ -54,12 +54,12 @@ switch(strtolower($_REQUEST['status'])){ //Status is overloaded } $qwhere =''; -/* +/* STRICT DEPARTMENTS BASED PERMISSION! User can also see tickets assigned to them regardless of the ticket's dept. */ -$depts=$thisstaff->getDepts(); +$depts=$thisstaff->getDepts(); $qwhere =' WHERE ( ' .' ticket.staff_id='.db_input($thisstaff->getId()); @@ -73,7 +73,7 @@ $qwhere .= ' )'; //STATUS if($status) { - $qwhere.=' AND status='.db_input(strtolower($status)); + $qwhere.=' AND status='.db_input(strtolower($status)); } //Queues: Overloaded sub-statuses - you've got to just have faith! @@ -87,10 +87,10 @@ if($staffId && ($staffId==$thisstaff->getId())) { //My tickets $qwhere.=' AND isanswered=1 '; }elseif(!strcasecmp($status, 'open') && !$search) { //Open queue (on search OPEN means all open tickets - regardless of state). //Showing answered tickets on open queue?? - if(!$cfg->showAnsweredTickets()) + if(!$cfg->showAnsweredTickets()) $qwhere.=' AND isanswered=0 '; - /* Showing assigned tickets on open queue? + /* Showing assigned tickets on open queue? Don't confuse it with show assigned To column -> F'it it's confusing - just trust me! */ if(!($cfg->showAssignedTickets() || $thisstaff->showAssignedTickets())) { @@ -99,7 +99,7 @@ if($staffId && ($staffId==$thisstaff->getId())) { //My tickets } } -//Search?? Somebody...get me some coffee +//Search?? Somebody...get me some coffee $deep_search=false; if($search): $qstr.='&a='.urlencode($_REQUEST['a']); @@ -114,17 +114,11 @@ if($search): }elseif(strpos($searchTerm,'@') && Validator::is_email($searchTerm)){ //pulling all tricks! # XXX: What about searching for email addresses in the body of # the thread message - $qwhere.=" AND ticket.email='$queryterm'"; + $qwhere.=" AND email.value='$queryterm'"; }else{//Deep search! - //This sucks..mass scan! search anything that moves! - + //This sucks..mass scan! search anything that moves! + $deep_search=true; - $qwhere.=" AND ( ticket.email LIKE '%$queryterm%'". - " OR ticket.name LIKE '%$queryterm%'". - " OR ticket.subject LIKE '%$queryterm%'". - " OR thread.body LIKE '%$queryterm%'". - " OR thread.title LIKE '%$queryterm%'". - ' ) '; } } //department @@ -139,24 +133,24 @@ if($search): $qwhere.=' AND ticket.topic_id='.db_input($_REQUEST['topicId']); $qstr.='&topicId='.urlencode($_REQUEST['topicId']); } - - //Assignee + + //Assignee if(isset($_REQUEST['assignee']) && strcasecmp($_REQUEST['status'], 'closed')) { $id=preg_replace("/[^0-9]/", "", $_REQUEST['assignee']); $assignee = $_REQUEST['assignee']; $qstr.='&assignee='.urlencode($_REQUEST['assignee']); - $qwhere.= ' AND ( + $qwhere.= ' AND ( ( ticket.status="open" '; - + if($assignee[0]=='t') $qwhere.=' AND ticket.team_id='.db_input($id); elseif($assignee[0]=='s') $qwhere.=' AND ticket.staff_id='.db_input($id); elseif(is_numeric($id)) $qwhere.=' AND ticket.staff_id='.db_input($id); - + $qwhere.=' ) '; - + if($_REQUEST['staffId'] && !$_REQUEST['status']) { //Assigned TO + Closed By $qwhere.= ' OR (ticket.staff_id='.db_input($_REQUEST['staffId']). ' AND ticket.status="closed") '; $qstr.='&staffId='.urlencode($_REQUEST['staffId']); @@ -164,7 +158,7 @@ if($search): $qwhere.= ' OR ticket.status="closed" '; $qstr.='&staffId='.urlencode($_REQUEST['staffId']); } - + $qwhere.= ' ) '; } elseif($_REQUEST['staffId']) { $qwhere.=' AND (ticket.staff_id='.db_input($_REQUEST['staffId']).' AND ticket.status="closed") '; @@ -182,7 +176,7 @@ if($search): if($startTime){ $qwhere.=' AND ticket.created>=FROM_UNIXTIME('.$startTime.')'; $qstr.='&startDate='.urlencode($_REQUEST['startDate']); - + } if($endTime){ $qwhere.=' AND ticket.created<=FROM_UNIXTIME('.$endTime.')'; @@ -192,8 +186,8 @@ if($search): endif; -$sortOptions=array('date'=>'ticket.created','ID'=>'ticketID','pri'=>'priority_urgency','name'=>'ticket.name', - 'subj'=>'ticket.subject','status'=>'ticket.status','assignee'=>'assigned','staff'=>'staff', +$sortOptions=array('date'=>'ticket.created','ID'=>'ticketID','pri'=>'priority_urgency','name'=>'name.value', + 'subj'=>'subject.value','status'=>'ticket.status','assignee'=>'assigned','staff'=>'staff', 'dept'=>'dept_name'); $orderWays=array('DESC'=>'DESC','ASC'=>'ASC'); @@ -222,13 +216,13 @@ if($_REQUEST['sort'] && $queue) { //Set default sort by columns. if(!$order_by ) { - if($showanswered) + if($showanswered) $order_by='ticket.lastresponse, ticket.created'; //No priority sorting for answered tickets. elseif(!strcasecmp($status,'closed')) $order_by='ticket.closed, ticket.created'; //No priority sorting for closed tickets. elseif($showoverdue) //priority> duedate > age in ASC order. $order_by='priority_urgency ASC, ISNULL(duedate) ASC, duedate ASC, effective_date ASC, ticket.created'; - else //XXX: Add due date here?? No - + else //XXX: Add due date here?? No - $order_by='priority_urgency ASC, effective_date DESC, ticket.created'; } @@ -243,12 +237,23 @@ $$x=' class="'.strtolower($order).'" '; if($_GET['limit']) $qstr.='&limit='.urlencode($_GET['limit']); +$dynfields='(SELECT entry.ticket_id, value FROM '.FORM_ANSWER_TABLE.' ans '. + 'LEFT JOIN '.FORM_ENTRY_TABLE.' entry ON entry.id=ans.entry_id '. + 'LEFT JOIN '.FORM_FIELD_TABLE.' field ON field.id=ans.field_id '. + 'WHERE field.name = "%1$s")'; +$subject_sql=sprintf($dynfields, 'subject'); +$name_sql=sprintf($dynfields, 'name'); +$email_sql=sprintf($dynfields, 'email'); + $qselect ='SELECT DISTINCT ticket.ticket_id,lock_id,ticketID,ticket.dept_id,ticket.staff_id,ticket.team_id ' - .' ,ticket.subject,ticket.name,ticket.email,dept_name ' + .' ,subject.value as subject,name.value as name,email.value as email,dept_name ' .' ,ticket.status,ticket.source,isoverdue,isanswered,ticket.created,pri.* '; $qfrom=' FROM '.TICKET_TABLE.' ticket '. - ' LEFT JOIN '.DEPT_TABLE.' dept ON ticket.dept_id=dept.dept_id '; + ' LEFT JOIN '.DEPT_TABLE.' dept ON ticket.dept_id=dept.dept_id '. + ' LEFT JOIN '.$subject_sql.' subject ON subject.ticket_id = ticket.ticket_id '. + ' LEFT JOIN '.$name_sql.' name ON name.ticket_id = ticket.ticket_id '. + ' LEFT JOIN '.$email_sql.' email ON email.ticket_id = ticket.ticket_id '; $sjoin=''; if($search && $deep_search) { @@ -274,7 +279,7 @@ $qselect.=' ,count(attach.attach_id) as attachments ' .' ,IF(ptopic.topic_pid IS NULL, topic.topic, CONCAT_WS(" / ", ptopic.topic, topic.topic)) as helptopic '; $qfrom.=' LEFT JOIN '.TICKET_PRIORITY_TABLE.' pri ON (ticket.priority_id=pri.priority_id) ' - .' LEFT JOIN '.TICKET_LOCK_TABLE.' tlock ON (ticket.ticket_id=tlock.ticket_id AND tlock.expire>NOW() + .' LEFT JOIN '.TICKET_LOCK_TABLE.' tlock ON (ticket.ticket_id=tlock.ticket_id AND tlock.expire>NOW() AND tlock.staff_id!='.db_input($thisstaff->getId()).') ' .' LEFT JOIN '.TICKET_ATTACHMENT_TABLE.' attach ON (ticket.ticket_id=attach.ticket_id) ' .' LEFT JOIN '.TICKET_THREAD_TABLE.' thread ON ( ticket.ticket_id=thread.ticket_id) ' @@ -333,13 +338,13 @@ $negorder=$order=='DESC'?'ASC':'DESC'; //Negate the sorting.. <th width="8px"> </th> <?php } ?> <th width="70"> - <a <?php echo $id_sort; ?> href="tickets.php?sort=ID&order=<?php echo $negorder; ?><?php echo $qstr; ?>" + <a <?php echo $id_sort; ?> href="tickets.php?sort=ID&order=<?php echo $negorder; ?><?php echo $qstr; ?>" title="Sort By Ticket ID <?php echo $negorder; ?>">Ticket</a></th> <th width="70"> - <a <?php echo $date_sort; ?> href="tickets.php?sort=date&order=<?php echo $negorder; ?><?php echo $qstr; ?>" + <a <?php echo $date_sort; ?> href="tickets.php?sort=date&order=<?php echo $negorder; ?><?php echo $qstr; ?>" title="Sort By Date <?php echo $negorder; ?>">Date</a></th> <th width="280"> - <a <?php echo $subj_sort; ?> href="tickets.php?sort=subj&order=<?php echo $negorder; ?><?php echo $qstr; ?>" + <a <?php echo $subj_sort; ?> href="tickets.php?sort=subj&order=<?php echo $negorder; ?><?php echo $qstr; ?>" title="Sort By Subject <?php echo $negorder; ?>">Subject</a></th> <th width="170"> <a <?php echo $name_sort; ?> href="tickets.php?sort=name&order=<?php echo $negorder; ?><?php echo $qstr; ?>" @@ -352,27 +357,27 @@ $negorder=$order=='DESC'?'ASC':'DESC'; //Negate the sorting.. <?php } else { ?> <th width="60" <?php echo $pri_sort;?>> - <a <?php echo $pri_sort; ?> href="tickets.php?sort=pri&order=<?php echo $negorder; ?><?php echo $qstr; ?>" + <a <?php echo $pri_sort; ?> href="tickets.php?sort=pri&order=<?php echo $negorder; ?><?php echo $qstr; ?>" title="Sort By Priority <?php echo $negorder; ?>">Priority</a></th> <?php } - if($showassigned ) { + if($showassigned ) { //Closed by if(!strcasecmp($status,'closed')) { ?> <th width="150"> - <a <?php echo $staff_sort; ?> href="tickets.php?sort=staff&order=<?php echo $negorder; ?><?php echo $qstr; ?>" + <a <?php echo $staff_sort; ?> href="tickets.php?sort=staff&order=<?php echo $negorder; ?><?php echo $qstr; ?>" title="Sort By Closing Staff Name <?php echo $negorder; ?>">Closed By</a></th> <?php } else { //assigned to ?> <th width="150"> - <a <?php echo $assignee_sort; ?> href="tickets.php?sort=assignee&order=<?php echo $negorder; ?><?php echo $qstr; ?>" + <a <?php echo $assignee_sort; ?> href="tickets.php?sort=assignee&order=<?php echo $negorder; ?><?php echo $qstr; ?>" title="Sort By Assignee <?php echo $negorder;?>">Assigned To</a></th> <?php } } else { ?> <th width="150"> - <a <?php echo $dept_sort; ?> href="tickets.php?sort=dept&order=<?php echo $negorder;?><?php echo $qstr; ?>" + <a <?php echo $dept_sort; ?> href="tickets.php?sort=dept&order=<?php echo $negorder;?><?php echo $qstr; ?>" title="Sort By Department <?php echo $negorder; ?>">Department</a></th> <?php } ?> @@ -411,8 +416,8 @@ $negorder=$order=='DESC'?'ASC':'DESC'; //Negate the sorting.. } ?> <tr id="<?php echo $row['ticket_id']; ?>"> - <?php if($thisstaff->canManageTickets()) { - + <?php if($thisstaff->canManageTickets()) { + $sel=false; if($ids && in_array($row['ticket_id'], $ids)) $sel=true; @@ -422,17 +427,17 @@ $negorder=$order=='DESC'?'ASC':'DESC'; //Negate the sorting.. </td> <?php } ?> <td align="center" title="<?php echo $row['email']; ?>" nowrap> - <a class="Icon <?php echo strtolower($row['source']); ?>Ticket ticketPreview" title="Preview Ticket" + <a class="Icon <?php echo strtolower($row['source']); ?>Ticket ticketPreview" title="Preview Ticket" href="tickets.php?id=<?php echo $row['ticket_id']; ?>"><?php echo $tid; ?></a></td> <td align="center" nowrap><?php echo Format::db_date($row['created']); ?></td> - <td><a <?php if($flag) { ?> class="Icon <?php echo $flag; ?>Ticket" title="<?php echo ucfirst($flag); ?> Ticket" <?php } ?> + <td><a <?php if($flag) { ?> class="Icon <?php echo $flag; ?>Ticket" title="<?php echo ucfirst($flag); ?> Ticket" <?php } ?> href="tickets.php?id=<?php echo $row['ticket_id']; ?>"><?php echo $subject; ?></a> <?php echo ($threadcount>1)?" <small>($threadcount)</small> ":''?> <?php echo $row['attachments']?"<span class='Icon file'> </span>":''; ?> </td> <td nowrap> <?php echo Format::truncate($row['name'],22,strpos($row['name'],'@')); ?> </td> - <?php + <?php if($search && !$status){ $displaystatus=ucfirst($row['status']); if(!strcasecmp($row['status'],'open')) @@ -442,14 +447,14 @@ $negorder=$order=='DESC'?'ASC':'DESC'; //Negate the sorting.. <td class="nohover" align="center" style="background-color:<?php echo $row['priority_color']; ?>;"> <?php echo $row['priority_desc']; ?></td> <?php - } + } ?> <td nowrap> <?php echo $lc; ?></td> </tr> <?php } //end of while. else: //not tickets found!! set fetch error. - $ferror='There are no tickets here. (Leave a little early today).'; + $ferror='There are no tickets here. (Leave a little early today).'; endif; ?> </tbody> <tfoot> @@ -477,7 +482,7 @@ $negorder=$order=='DESC'?'ASC':'DESC'; //Negate the sorting.. ?> <?php if($thisstaff->canManageTickets()) { ?> - <p class="centered" id="actions"> + <p class="centered" id="actions"> <?php $status=$_REQUEST['status']?$_REQUEST['status']:$status; switch (strtolower($status)) { @@ -572,7 +577,7 @@ $negorder=$order=='DESC'?'ASC':'DESC'; //Negate the sorting.. <?php if(($mydepts = $thisstaff->getDepts()) && ($depts=Dept::getDepartments())) { foreach($depts as $id =>$name) { - if(!in_array($id, $mydepts)) continue; + if(!in_array($id, $mydepts)) continue; echo sprintf('<option value="%d">%s</option>', $id, $name); } } @@ -594,7 +599,7 @@ $negorder=$order=='DESC'?'ASC':'DESC'; //Negate the sorting.. } echo '</OPTGROUP>'; } - + if(($teams=Team::getTeams())) { echo '<OPTGROUP label="Teams ('.count($teams).')">'; foreach($teams as $id => $name) { diff --git a/include/upgrader/streams/core.sig b/include/upgrader/streams/core.sig index 8ced61f0c..a54c1899f 100644 --- a/include/upgrader/streams/core.sig +++ b/include/upgrader/streams/core.sig @@ -1 +1 @@ -dad45ca24a3800102f8b92c7279347db +4e69fdfbda59040e51c2d93c874fcf22 diff --git a/include/upgrader/streams/core/d51f303a-DYNAMICF.patch.sql b/include/upgrader/streams/core/d51f303a-DYNAMICF.patch.sql new file mode 100644 index 000000000..940b9a12b --- /dev/null +++ b/include/upgrader/streams/core/d51f303a-DYNAMICF.patch.sql @@ -0,0 +1,216 @@ +/** + * @version v1.7.1 + * @signature 0000000000000000000000000000000 + * + * Adds the database structure for the dynamic forms feature and migrates + * the database from the legacy <=1.7 format to the new format with the + * dynamic forms feature. Basically, a default form is installed with the + * fields found in the legacy version of osTicket, the data is migrated from + * the fields in the ticket table to the new forms tables, and then the + * fields are dropped from the ticket table. + */ + +DROP TABLE IF EXISTS `%TABLE_PREFIX%dynamic_formset`; +CREATE TABLE `%TABLE_PREFIX%dynamic_formset` ( + `id` int(11) unsigned auto_increment, + `title` varchar(255) NOT NULL, + `instructions` varchar(512), + `notes` text, + `created` datetime NOT NULL, + `updated` datetime NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + +DROP TABLE IF EXISTS `%TABLE_PREFIX%dynamic_formset_sections`; +CREATE TABLE `%TABLE_PREFIX%dynamic_formset_sections` ( + `id` int(11) unsigned NOT NULL auto_increment, + `formset_id` int(11) NOT NULL, + `section_id` int(11) NOT NULL, + `title` varchar(255), + `instructions` text, + -- Allow more than one form, sorted in this order + `sort` int(11) NOT NULL DEFAULT 1, + PRIMARY KEY (`id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + +DROP TABLE IF EXISTS `%TABLE_PREFIX%dynamic_form_section`; +CREATE TABLE `%TABLE_PREFIX%dynamic_form` ( + `id` int(11) unsigned NOT NULL auto_increment, + `title` varchar(255) NOT NULL, + `instructions` varchar(512), + `notes` text, + `created` datetime NOT NULL, + `updated` datetime NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + +DROP TABLE IF EXISTS `%TABLE_PREFIX%dynamic_form_field`; +CREATE TABLE `%TABLE_PREFIX%dynamic_form_field` ( + `id` int(11) unsigned NOT NULL auto_increment, + `section_id` int(11) unsigned NOT NULL, + `type` varchar(255) NOT NULL DEFAULT 'text', + `label` varchar(255) NOT NULL, + `required` tinyint(1) NOT NULL DEFAULT 0, + `private` tinyint(1) NOT NULL DEFAULT 0, + `name` varchar(64) NOT NULL, + `configuration` text, + `sort` int(11) unsigned NOT NULL, + `hint` varchar(512), + `created` datetime NOT NULL, + `updated` datetime NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + +-- Create a default form to mimic the previous default form of osTicket < 1.7.1 +INSERT INTO `%TABLE_PREFIX%dynamic_form_section` SET + `id` = 1, `title` = 'User Information', `created` = NOW(), + `updated` = NOW(); +INSERT INTO `%TABLE_PREFIX%dynamic_form_section` SET + `id` = 2, `title` = 'Ticket Details', `created` = NOW(), + `updated` = NOW(); + +INSERT INTO `%TABLE_PREFIX%dynamic_formset` SET + `id` = 1, `title` = 'Default', `created` = NOW(), `updated` = NOW(); + +INSERT INTO `%TABLE_PREFIX%dynamic_formset_sections` SET + `formset_id` = 1, `section_id` = 1, `sort` = 10; +INSERT INTO `%TABLE_PREFIX%dynamic_formset_sections` SET + `formset_id` = 1, `section_id` = 2, `sort` = 20; + +INSERT INTO `%TABLE_PREFIX%dynamic_form_field` SET + `section_id` = 1, `type` = 'text', `label` = 'Email Address', + `required` = 1, `configuration` = '{"size":40,"length":120,"validator":"email"}', + `name` = 'email', `sort` = 10, `created` = NOW(), `updated` = NOW(); +INSERT INTO `%TABLE_PREFIX%dynamic_form_field` SET + `section_id` = 1, `type` = 'text', `label` = 'Full Name', + `required` = 1, `configuration` = '{"size":40,"length":32}', + `name` = 'name', `sort` = 20, `created` = NOW(), `updated` = NOW(); +INSERT INTO `%TABLE_PREFIX%dynamic_form_field` SET + `section_id` = 1, `type` = 'phone', `label` = 'Phone Number', + `name` = 'phone', `sort` = 30, `created` = NOW(), `updated` = NOW(); + +INSERT INTO `%TABLE_PREFIX%dynamic_form_field` SET + `section_id` = 2, `type` = 'text', `label` = 'Subject', + `hint` = 'Issue summary', `required` = 1, + `configuration` = '{"size":40,"length":64}', + `name` = 'subject', `sort` = 10, `created` = NOW(), `updated` = NOW(); + +DROP TABLE IF EXISTS `%TABLE_PREFIX%dynamic_form_entry`; +CREATE TABLE `%TABLE_PREFIX%dynamic_form_entry` ( + `id` int(11) unsigned NOT NULL auto_increment, + `section_id` int(11) unsigned NOT NULL, + `ticket_id` int(11) unsigned, + `sort` int(11) unsigned NOT NULL DEFAULT 1, + `created` datetime NOT NULL, + `updated` datetime NOT NULL, + PRIMARY KEY (`id`), + KEY `ticket_dyn_form_lookup` (`ticket_id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + +DROP TABLE IF EXISTS `%TABLE_PREFIX%dynamic_form_entry_values`; +CREATE TABLE `%TABLE_PREFIX%dynamic_form_entry_values` ( + -- references dynamic_form_entry.id + `entry_id` int(11) unsigned NOT NULL, + `field_id` int(11) unsigned NOT NULL, + `value` text, + PRIMARY KEY (`entry_id`, `field_id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + +DROP TABLE IF EXISTS `%TABLE_PREFIX%dynamic_list`; +CREATE TABLE `%TABLE_PREFIX%dynamic_list` ( + `id` int(11) unsigned NOT NULL auto_increment, + `name` varchar(255) NOT NULL, + `name_plural` varchar(255), + `sort_mode` enum('Alpha', '-Alpha', 'SortCol') NOT NULL DEFAULT 'Alpha', + `notes` text, + `created` datetime NOT NULL, + `updated` datetime NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + +DROP TABLE IF EXISTS `%TABLE_PREFIX%dynamic_list_items`; +CREATE TABLE `%TABLE_PREFIX%dynamic_list_items` ( + `id` int(11) unsigned NOT NULL auto_increment, + `list_id` int(11), + `value` varchar(255) NOT NULL, + -- extra value such as abbreviation + `extra` varchar(255), + `sort` int(11) NOT NULL DEFAULT 1, + PRIMARY KEY (`id`), + KEY `dynamic_list_item_lookup` (`list_id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + +ALTER TABLE `%TABLE_PREFIX%help_topic` + ADD `formset_id` int(11) unsigned NOT NULL default '0' AFTER `sla_id`; + +-- All help topics will link to the default formset +UPDATE `%TABLE_PREFIX%help_topic` SET `formset_id` = 1; + +-- Port data from the ticket table +-- 1. Create form entries for each ticket +INSERT INTO `%TABLE_PREFIX%dynamic_form_entry` ( + `section_id`, `ticket_id`, `sort`, `created`, `updated`) + SELECT 1, `ticket_id`, 10, `created`, `updated` + FROM `%TABLE_PREFIX%ticket`; + +INSERT INTO `%TABLE_PREFIX%dynamic_form_entry` ( + `section_id`, `ticket_id`, `sort`, `created`, `updated`) + SELECT 2, `ticket_id`, 20, `created`, `updated` + FROM `%TABLE_PREFIX%ticket`; + +-- 2. Copy Name, Email, and Phone from the ticket table to section #1 +INSERT INTO `%TABLE_PREFIX%dynamic_form_entry_values` ( + `field_id`, `entry_id`, `value`) + SELECT A3.`field_id`, A2.`id`, A1.`name` + FROM `%TABLE_PREFIX%ticket` A1 + INNER JOIN `%TABLE_PREFIX%dynamic_form_entry` A2 ON (A1.`ticket_id` + = A2.`ticket_id` AND A2.`section_id` = 1), + INNER JOIN `%TABLE_PREFIX%dynamic_form_field` A3 ON (A2.`section_id` + = A3.`section_id`) + WHERE A3.`name` = 'name' AND LENGTH(A1.`name`); + +INSERT INTO `%TABLE_PREFIX%dynamic_form_entry_values` ( + `field_id`, `entry_id`, `value`) + SELECT A3.`field_id`, A2.`id`, A1.`email` + FROM `%TABLE_PREFIX%ticket` A1 + INNER JOIN `%TABLE_PREFIX%dynamic_form_entry` A2 ON (A1.`ticket_id` + = A2.`ticket_id` AND A2.`section_id` = 1), + INNER JOIN `%TABLE_PREFIX%dynamic_form_field` A3 ON (A2.`section_id` + = A3.`section_id`) + WHERE A3.`name` = 'email' AND LENGTH(A1.`email`); + +INSERT INTO `%TABLE_PREFIX%dynamic_form_entry_values` ( + `field_id`, `entry_id`, `value`) + SELECT A3.`field_id`, A2.`id`, CONCAT(A1.`phone`, 'X', A1.`phone_ext`) + FROM `%TABLE_PREFIX%ticket` A1 + INNER JOIN `%TABLE_PREFIX%dynamic_form_entry` A2 ON (A1.`ticket_id` + = A2.`ticket_id` AND A2.`section_id` = 1), + INNER JOIN `%TABLE_PREFIX%dynamic_form_field` A3 ON (A2.`section_id` + = A3.`section_id`) + WHERE A3.`name` = 'phone' AND LENGTH(A1.`phone`); + +-- 3. Copy subject lines from the ticket table into section #2 +INSERT INTO `%TABLE_PREFIX%dynamic_form_entry_values` ( + `field_id`, `entry_id`, `value`) + SELECT A3.`field_id`, A2.`id`, A1.`subject` + FROM `%TABLE_PREFIX%ticket` A1 + INNER JOIN `%TABLE_PREFIX%dynamic_form_entry` A2 ON (A1.`ticket_id` + = A2.`ticket_id` AND A2.`section_id` = 2), + INNER JOIN `%TABLE_PREFIX%dynamic_form_field` A3 ON (A2.`section_id` + = A3.`section_id`) + WHERE A3.`name` = 'subject'; + +-- 4. Remove columns from ticket table +ALTER TABLE `%TABLE_PREFIX%ticket` + DROP COLUMN `name`, + DROP COLUMN `email`, + DROP COLUMN `phone`, + DROP COLUMN `phone_ext`, + DROP COLUMN `subject`; + +-- 5. Cleanup ticket table with dropped varchar columns +OPTIMIZE TABLE `%TABLE_PREFIX%ticket`; + +-- update schema signature. +UPDATE `%TABLE_PREFIX%config` + SET `schema_signature`='0000000000000000000000000000000'; diff --git a/main.inc.php b/main.inc.php index 5dd4109d3..286f3ef11 100644 --- a/main.inc.php +++ b/main.inc.php @@ -1,42 +1,42 @@ <?php /********************************************************************* - main.inc.php +main.inc.php - Master include file which must be included at the start of every file. - The brain of the whole sytem. Don't monkey with it. +Master include file which must be included at the start of every file. +The brain of the whole sytem. Don't monkey with it. - Peter Rotich <peter@osticket.com> - Copyright (c) 2006-2013 osTicket - http://www.osticket.com +Peter Rotich <peter@osticket.com> +Copyright (c) 2006-2013 osTicket +http://www.osticket.com - Released under the GNU General Public License WITHOUT ANY WARRANTY. - See LICENSE.TXT for details. +Released under the GNU General Public License WITHOUT ANY WARRANTY. +See LICENSE.TXT for details. - vim: expandtab sw=4 ts=4 sts=4: +vim: expandtab sw=4 ts=4 sts=4: **********************************************************************/ - #Disable direct access. - if(!strcasecmp(basename($_SERVER['SCRIPT_NAME']),basename(__FILE__))) die('kwaheri rafiki!'); +#Disable direct access. +if(!strcasecmp(basename($_SERVER['SCRIPT_NAME']),basename(__FILE__))) die('kwaheri rafiki!'); - require('bootstrap.php'); - Bootstrap::loadConfig(); - Bootstrap::defineTables(TABLE_PREFIX); - Bootstrap::connect(); +require('bootstrap.php'); +Bootstrap::loadConfig(); +Bootstrap::defineTables(TABLE_PREFIX); +Bootstrap::connect(); - if(!($ost=osTicket::start()) || !($cfg = $ost->getConfig())) - Bootstrap::croak('Unable to load config info from DB. Get tech support.'); +if(!($ost=osTicket::start()) || !($cfg = $ost->getConfig())) +Bootstrap::croak('Unable to load config info from DB. Get tech support.'); - //Init - $session = $ost->getSession(); +//Init +$session = $ost->getSession(); - //System defaults we might want to make global// - #pagenation default - user can override it! - define('DEFAULT_PAGE_LIMIT', $cfg->getPageSize()?$cfg->getPageSize():25); +//System defaults we might want to make global// +#pagenation default - user can override it! +define('DEFAULT_PAGE_LIMIT', $cfg->getPageSize()?$cfg->getPageSize():25); - #Cleanup magic quotes crap. - if(function_exists('get_magic_quotes_gpc') && get_magic_quotes_gpc()) { - $_POST=Format::strip_slashes($_POST); - $_GET=Format::strip_slashes($_GET); - $_REQUEST=Format::strip_slashes($_REQUEST); - } +#Cleanup magic quotes crap. +if(function_exists('get_magic_quotes_gpc') && get_magic_quotes_gpc()) { +$_POST=Format::strip_slashes($_POST); +$_GET=Format::strip_slashes($_GET); +$_REQUEST=Format::strip_slashes($_REQUEST); +} ?> diff --git a/open.php b/open.php index db51e85d3..7d7742cdf 100644 --- a/open.php +++ b/open.php @@ -30,6 +30,24 @@ if($_POST): $errors['captcha']='Invalid - try again!'; } + $interest=array('name','email','subject'); + $topic=Topic::lookup($vars['topicId']); + $forms=DynamicFormset::lookup($topic->ht['formset_id'])->getForms(); + foreach ($forms as $idx=>$f) { + $form=$f->getForm()->instanciate($f->sort); + # Collect name, email, and subject address for banning and such + foreach ($form->getAnswers() as $answer) { + $fname = $answer->getField()->get('name'); + if (in_array($fname, $interest)) + # XXX: Assigning to _POST not considered great PHP + # coding style + $vars[$fname] = $answer->getField()->getClean(); + } + $forms[$idx] = $form; + if (!$form->isValid()) + $errors = array_merge($errors, $form->errors()); + } + if(!$errors && $cfg->allowOnlineAttachments() && $_FILES['attachments']) $vars['files'] = AttachmentFile::format($_FILES['attachments'], true); @@ -37,6 +55,12 @@ if($_POST): if(($ticket=Ticket::create($vars, $errors, SOURCE))){ $msg='Support ticket request created'; Draft::deleteForNamespace('ticket.client.'.substr(session_id(), -12)); + # TODO: Save dynamic form(s) + foreach ($forms as $f) { + $f->set('ticket_id', $ticket->getId()); + $f->save(); + } + $ticket->loadDynamicData(); //Logged in...simply view the newly created ticket. if($thisclient && $thisclient->isValid()) { if(!$cfg->showRelatedTickets()) diff --git a/scp/ajax.php b/scp/ajax.php index 3eeca0f00..7fae11f1f 100644 --- a/scp/ajax.php +++ b/scp/ajax.php @@ -45,6 +45,11 @@ $dispatcher = patterns('', url('^/config/', patterns('ajax.config.php:ConfigAjaxAPI', url_get('^scp', 'scp') )), + url('^/form/', patterns('ajax.forms.php:DynamicFormsAjaxAPI', + url_get('^help-topic/(?P<id>\d+)$', 'getFormsForHelpTopic'), + url_get('^field-config/(?P<id>\d+)$', 'getFieldConfiguration'), + url_post('^field-config/(?P<id>\d+)$', 'saveFieldConfiguration') + )), url('^/report/overview/', patterns('ajax.reports.php:OverviewReportAjaxAPI', # Send url_get('^graph$', 'getPlotData'), diff --git a/scp/css/scp.css b/scp/css/scp.css index 648d05c13..4fa5ba620 100644 --- a/scp/css/scp.css +++ b/scp/css/scp.css @@ -536,6 +536,8 @@ a.print { .form_table td.multi-line { vertical-align:top; + padding-top: 0.4em; + padding-bottom: 0.4em; } .form_table input[type=text], .form_table input[type=password], .form_table textarea { diff --git a/scp/dynamic-form-sections.php b/scp/dynamic-form-sections.php new file mode 100644 index 000000000..eb1881847 --- /dev/null +++ b/scp/dynamic-form-sections.php @@ -0,0 +1,79 @@ +<?php +require('admin.inc.php'); +require_once(INCLUDE_DIR."/class.dynamic_forms.php"); + +$form=null; +if($_REQUEST['id'] && !($form=DynamicFormSection::lookup($_REQUEST['id']))) + $errors['err']='Unknown or invalid dynamic form ID.'; + +if($_POST) { + $fields = array('title', 'notes', 'instructions'); + $required = array('name','email','subject'); + switch(strtolower($_POST['do'])) { + case 'update': + foreach ($fields as $f) + if (isset($_POST[$f])) + $form->set($f, $_POST[$f]); + if ($form->isValid()) + $form->save(); + foreach ($form->getDynamicFields() as $field) { + $id = $field->get('id'); + if ($_POST["delete-$id"] == 'on') { + $field->delete(); + continue; + } + foreach (array('sort','label','type','name') as $f) + if (isset($_POST["$f-$id"])) + $field->set($f, $_POST["$f-$id"]); + # TODO: make sure all help topics still have all required fields + $field->set('required', $_POST["required-$id"] == 'on' ? 1 : 0); + $field->set('private', $_POST["private-$id"] == 'on' ? 1 : 0); + # Core fields are forced required and public + if (in_array($field->get('name'), $required)) { + $field->set('required', 1); + $field->set('private', 0); + } + if ($field->isValid()) + $field->save(); + } + break; + case 'add': + $form = DynamicFormSection::create(array( + 'title'=>$_POST['title'], + 'instructions'=>$_POST['instructions'], + 'notes'=>$_POST['notes'])); + if ($form->isValid()) + $form->save(); + break; + } + + if ($form) { + for ($i=0; isset($_POST["sort-new-$i"]); $i++) { + if (!$_POST["label-new-$i"]) + continue; + $field = DynamicFormField::create(array( + 'section_id'=>$form->get('id'), + 'sort'=>$_POST["sort-new-$i"], + 'label'=>$_POST["label-new-$i"], + 'type'=>$_POST["type-new-$i"], + 'name'=>$_POST["name-new-$i"], + 'private'=>$_POST["private-new-$i"] == 'on' ? 1 : 0, + 'required'=>$_POST["required-new-$i"] == 'on' ? 1 : 0 + )); + if ($field->isValid()) + $field->save(); + } + // XXX: Move to an instrumented list that can handle this better + $form->_dfields = $form->_fields = null; + } +} + +$page='dynamic-form-sections.inc.php'; +if($form || ($_REQUEST['a'] && !strcasecmp($_REQUEST['a'],'add'))) + $page='dynamic-form-section.inc.php'; + +$nav->setTabActive('forms'); +require(STAFFINC_DIR.'header.inc.php'); +require(STAFFINC_DIR.$page); +include(STAFFINC_DIR.'footer.inc.php'); +?> diff --git a/scp/dynamic-forms.php b/scp/dynamic-forms.php new file mode 100644 index 000000000..13de499b0 --- /dev/null +++ b/scp/dynamic-forms.php @@ -0,0 +1,83 @@ +<?php +require('admin.inc.php'); +require_once(INCLUDE_DIR."/class.dynamic_forms.php"); + +$group=null; +if($_REQUEST['id'] && !($group=DynamicFormset::lookup($_REQUEST['id']))) + $errors['err']='Unknown or invalid dynamic form ID.'; + +if($_POST) { + $fields = array('title', 'notes'); + $deleted = array(); + switch(strtolower($_POST['do'])) { + case 'update': + foreach ($fields as $f) + if (isset($_POST[$f])) + $group->set($f, $_POST[$f]); + foreach ($group->getForms() as $idx=>$form) { + $id = $form->get('id'); + if ($_POST["delete-$id"] == 'on') { + // Don't delete yet, in case this makes the formset + // invalid. XXX: When is osTicket going to adopt database + // transactions? + unset($group->_forms[$idx]); + $deleted[] = $form; + continue; + } + foreach (array('sort','section_id') as $f) + if (isset($_POST["$f-$id"])) + $form->set($f, $_POST["$f-$id"]); + if ($form->isValid()) + $form->save(); + } + break; + case 'add': + $group = DynamicFormset::create(array( + 'title'=>$_POST['title'], + 'notes'=>$_POST['notes'])); + break; + } + + if ($group) { + for ($i=0; isset($_POST["sort-new-$i"]); $i++) { + if (!$_POST["section_id-new-$i"]) + continue; + $form = DynamicFormsetSections::create(array( + 'formset_id'=>$group->get('id'), + 'sort'=>$_POST["sort-new-$i"], + 'section_id'=>$_POST["section_id-new-$i"], + )); + // XXX: Use an instrumented list to make this better + $group->_forms[] = $form; + if ($form->isValid()) + $form->save(); + } + + if ($group->isValid()) { + $new = $group->__new__; + $group->save(); + // Add the correct 'id' value to the attached form sections + if ($new) { + foreach ($group->getForms() as $form) { + $form->set('formset_id', $group->get('id')); + $form->save(); + } + } + // Now delete requested items + foreach ($deleted as $form) + $form->delete(); + } + else + $errors = array_merge($errors, $group->errors()); + } +} + +$page='dynamic-forms.inc.php'; +if($group || ($_REQUEST['a'] && !strcasecmp($_REQUEST['a'],'add'))) + $page='dynamic-form.inc.php'; + +$nav->setTabActive('forms'); +require(STAFFINC_DIR.'header.inc.php'); +require(STAFFINC_DIR.$page); +include(STAFFINC_DIR.'footer.inc.php'); +?> diff --git a/scp/dynamic-lists.php b/scp/dynamic-lists.php new file mode 100644 index 000000000..ec8b77fe1 --- /dev/null +++ b/scp/dynamic-lists.php @@ -0,0 +1,66 @@ +<?php +require('admin.inc.php'); +require_once(INCLUDE_DIR."/class.dynamic_forms.php"); + +$list=null; +if($_REQUEST['id'] && !($list=DynamicList::lookup($_REQUEST['id']))) + $errors['err']='Unknown or invalid dynamic list ID.'; + +if($_POST) { + $fields = array('name', 'name_plural', 'sort_mode', 'notes'); + switch(strtolower($_POST['do'])) { + case 'update': + foreach ($fields as $f) + if (isset($_POST[$f])) + $list->set($f, $_POST[$f]); + if ($list->isValid()) + $list->save(true); + foreach ($list->getItems() as $item) { + $id = $item->get('id'); + if ($_POST["delete-$id"] == 'on') { + $item->delete(); + continue; + } + foreach (array('sort','value','extra') as $i) + if (isset($_POST["$i-$id"])) + $item->set($i, $_POST["$i-$id"]); + if ($item->isValid()) + $item->save(); + } + break; + case 'add': + $list = DynamicList::create(array( + 'name'=>$_POST['name'], + 'notes'=>$_POST['notes'])); + if ($list->isValid()) + $list->save(); + break; + } + + if ($list) { + for ($i=0; isset($_POST["sort-new-$i"]); $i++) { + if (!$_POST["value-new-$i"]) + continue; + $item = DynamicListItem::create(array( + 'list_id'=>$list->get('id'), + 'sort'=>$_POST["sort-new-$i"], + 'value'=>$_POST["value-new-$i"], + 'extra'=>$_POST["extra-new-$i"] + )); + if ($item->isValid()) + $item->save(); + } + # Invalidate items cache + $list->_items = false; + } +} + +$page='dynamic-lists.inc.php'; +if($list || ($_REQUEST['a'] && !strcasecmp($_REQUEST['a'],'add'))) + $page='dynamic-list.inc.php'; + +$nav->setTabActive('forms'); +require(STAFFINC_DIR.'header.inc.php'); +require(STAFFINC_DIR.$page); +include(STAFFINC_DIR.'footer.inc.php'); +?> diff --git a/scp/helptopics.php b/scp/helptopics.php index 7cb150741..9f34fab9e 100644 --- a/scp/helptopics.php +++ b/scp/helptopics.php @@ -15,6 +15,7 @@ **********************************************************************/ require('admin.inc.php'); include_once(INCLUDE_DIR.'class.topic.php'); +require_once(INCLUDE_DIR.'class.dynamic_forms.php'); $topic=null; if($_REQUEST['id'] && !($topic=Topic::lookup($_REQUEST['id']))) @@ -95,6 +96,9 @@ if($_POST){ $errors['err']='Unknown command/action'; break; } + if ($id or $topic) { + if (!$id) $id=$topic->getId(); + } } $page='helptopics.inc.php'; diff --git a/scp/js/scp.js b/scp/js/scp.js index d7db69d13..d4e90e1b8 100644 --- a/scp/js/scp.js +++ b/scp/js/scp.js @@ -419,6 +419,24 @@ $(document).ready(function(){ $('.buttons', elem).show(); }); }); + + // Return a helper with preserved width of cells + var fixHelper = function(e, ui) { + ui.children().each(function() { + $(this).width($(this).width()); + }); + return ui; + }; + // Sortable tables for dynamic forms objects + $('.sortable-rows').sortable({ + 'helper': fixHelper, + 'stop': function(e, ui) { + var attr = ui.item.parent('tbody').data('sort'); + $('input[name^='+attr+']', ui.item.parent('tbody')).each(function(i, el) { + $(el).val(i+1); + }); + } + }); }); // NOTE: getConfig should be global diff --git a/scp/tickets.php b/scp/tickets.php index 45d0cd44a..16d84ee6c 100644 --- a/scp/tickets.php +++ b/scp/tickets.php @@ -20,6 +20,7 @@ require_once(INCLUDE_DIR.'class.dept.php'); require_once(INCLUDE_DIR.'class.filter.php'); require_once(INCLUDE_DIR.'class.canned.php'); require_once(INCLUDE_DIR.'class.json.php'); +require_once(INCLUDE_DIR.'class.dynamic_forms.php'); $page=''; @@ -193,12 +194,17 @@ if($_POST && !$errors): break; case 'edit': case 'update': + $forms=DynamicFormEntry::forTicket($ticket->getId()); + foreach ($forms as $form) + if (!$form->isValid()) + $errors = array_merge($errors, $form->errors()); if(!$ticket || !$thisstaff->canEditTickets()) $errors['err']='Perm. Denied. You are not allowed to edit tickets'; elseif($ticket->update($_POST,$errors)) { $msg='Ticket updated successfully'; $_REQUEST['a'] = null; //Clear edit action - going back to view. //Check to make sure the staff STILL has access post-update (e.g dept change). + foreach ($forms as $f) $f->save(); if(!$ticket->checkStaffAccess($thisstaff)) $ticket=null; } elseif(!$errors['err']) { @@ -451,6 +457,23 @@ if($_POST && !$errors): break; case 'open': $ticket=null; + $interest=array('name','email','subject'); + $topic=Topic::lookup($_POST['topicId']); + $forms=DynamicFormset::lookup($topic->ht['formset_id'])->getForms(); + foreach ($forms as $idx=>$f) { + $form=$f->getForm()->instanciate($f->sort); + # Collect name, email, and subject address for banning and such + foreach ($form->getAnswers() as $answer) { + $fname = $answer->getField()->get('name'); + if (in_array($fname, $interest)) + # XXX: Assigning to _POST not considered great PHP + # coding style + $_POST[$fname] = $answer->getField()->getClean(); + } + $forms[$idx] = $form; + if (!$form->isValid()) + $errors = array_merge($errors, $form->errors()); + } if(!$thisstaff || !$thisstaff->canCreateTickets()) { $errors['err']='You do not have permission to create tickets. Contact admin for such access'; } else { @@ -461,6 +484,12 @@ if($_POST && !$errors): if(($ticket=Ticket::open($vars, $errors))) { $msg='Ticket created successfully'; $_REQUEST['a']=null; + # TODO: Save dynamic form(s) + foreach ($forms as $f) { + $f->set('ticket_id', $ticket->getId()); + $f->save(); + } + $ticket->loadDynamicData(); if(!$ticket->checkStaffAccess($thisstaff) || $ticket->isClosed()) $ticket=null; Draft::deleteForNamespace('ticket.staff%', $thisstaff->getId()); @@ -555,9 +584,12 @@ if($ticket) { $ost->setPageTitle('Ticket #'.$ticket->getNumber()); $nav->setActiveSubMenu(-1); $inc = 'ticket-view.inc.php'; - if($_REQUEST['a']=='edit' && $thisstaff->canEditTickets()) + if($_REQUEST['a']=='edit' && $thisstaff->canEditTickets()) { $inc = 'ticket-edit.inc.php'; - elseif($_REQUEST['a'] == 'print' && !$ticket->pdfExport($_REQUEST['psize'], $_REQUEST['notes'])) + if (!$forms) $forms=DynamicFormEntry::forTicket($ticket->getId()); + // Auto add new fields to the entries + foreach ($forms as $f) $f->addMissingFields(); + } elseif($_REQUEST['a'] == 'print' && !$ticket->pdfExport($_REQUEST['psize'], $_REQUEST['notes'])) $errors['err'] = 'Internal error: Unable to export the ticket to PDF for print.'; } else { $inc = 'tickets.inc.php'; diff --git a/setup/inc/class.installer.php b/setup/inc/class.installer.php index 78fd35849..6ec2fa717 100644 --- a/setup/inc/class.installer.php +++ b/setup/inc/class.installer.php @@ -141,26 +141,26 @@ class Installer extends SetupWizard { } } - // TODO: Use language selected from install worksheet - $i18n = new Internationalization('en_US'); - $i18n->loadDefaultData(); + if(!$this->errors) { + // TODO: Use language selected from install worksheet + $i18n = new Internationalization('en_US'); + $i18n->loadDefaultData(); - $sql='SELECT `id` FROM '.PREFIX.'sla ORDER BY `id` LIMIT 1'; - $sla_id_1 = db_result(db_query($sql, false), 0); + $sql='SELECT `id` FROM '.PREFIX.'sla ORDER BY `id` LIMIT 1'; + $sla_id_1 = db_result(db_query($sql, false), 0); - $sql='SELECT `dept_id` FROM '.PREFIX.'department ORDER BY `dept_id` LIMIT 1'; - $dept_id_1 = db_result(db_query($sql, false), 0); + $sql='SELECT `dept_id` FROM '.PREFIX.'department ORDER BY `dept_id` LIMIT 1'; + $dept_id_1 = db_result(db_query($sql, false), 0); - $sql='SELECT `tpl_id` FROM '.PREFIX.'email_template_group ORDER BY `tpl_id` LIMIT 1'; - $template_id_1 = db_result(db_query($sql, false), 0); + $sql='SELECT `tpl_id` FROM '.PREFIX.'email_template_group ORDER BY `tpl_id` LIMIT 1'; + $template_id_1 = db_result(db_query($sql, false), 0); - $sql='SELECT `group_id` FROM '.PREFIX.'groups ORDER BY `group_id` LIMIT 1'; - $group_id_1 = db_result(db_query($sql, false), 0); + $sql='SELECT `group_id` FROM '.PREFIX.'groups ORDER BY `group_id` LIMIT 1'; + $group_id_1 = db_result(db_query($sql, false), 0); - $sql='SELECT `id` FROM '.PREFIX.'timezone WHERE offset=-5.0 LIMIT 1'; - $eastern_timezone = db_result(db_query($sql, false), 0); + $sql='SELECT `id` FROM '.PREFIX.'timezone WHERE offset=-5.0 LIMIT 1'; + $eastern_timezone = db_result(db_query($sql, false), 0); - if(!$this->errors) { //Create admin user. $sql='INSERT INTO '.PREFIX.'staff SET created=NOW() ' .", isactive=1, isadmin=1, group_id=$group_id_1, dept_id=$dept_id_1" diff --git a/setup/inc/sql/osTicket-mysql.sql b/setup/inc/sql/osTicket-mysql.sql new file mode 100644 index 000000000..4d613ca61 --- /dev/null +++ b/setup/inc/sql/osTicket-mysql.sql @@ -0,0 +1,861 @@ + +DROP TABLE IF EXISTS `%TABLE_PREFIX%api_key`; +CREATE TABLE `%TABLE_PREFIX%api_key` ( + `id` int(10) unsigned NOT NULL auto_increment, + `isactive` tinyint(1) NOT NULL default '1', + `ipaddr` varchar(64) NOT NULL, + `apikey` varchar(255) NOT NULL, + `can_create_tickets` TINYINT( 1 ) UNSIGNED NOT NULL DEFAULT '1', + `can_exec_cron` TINYINT( 1 ) UNSIGNED NOT NULL DEFAULT '1', + `notes` text, + `updated` datetime NOT NULL, + `created` datetime NOT NULL, + PRIMARY KEY (`id`), + KEY `ipaddr` (`ipaddr`), + UNIQUE KEY `apikey` (`apikey`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + +DROP TABLE IF EXISTS `%TABLE_PREFIX%faq`; +CREATE TABLE IF NOT EXISTS `%TABLE_PREFIX%faq` ( + `faq_id` int(10) unsigned NOT NULL auto_increment, + `category_id` int(10) unsigned NOT NULL default '0', + `ispublished` tinyint(1) unsigned NOT NULL default '0', + `question` varchar(255) NOT NULL, + `answer` text NOT NULL, + `keywords` tinytext, + `notes` text, + `created` date NOT NULL, + `updated` date NOT NULL, + PRIMARY KEY (`faq_id`), + UNIQUE KEY `question` (`question`), + KEY `category_id` (`category_id`), + KEY `ispublished` (`ispublished`), + FULLTEXT KEY `faq` (`question`,`answer`,`keywords`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + +DROP TABLE IF EXISTS `%TABLE_PREFIX%faq_attachment`; +CREATE TABLE IF NOT EXISTS `%TABLE_PREFIX%faq_attachment` ( + `faq_id` int(10) unsigned NOT NULL, + `file_id` int(10) unsigned NOT NULL, + PRIMARY KEY (`faq_id`,`file_id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + +DROP TABLE IF EXISTS `%TABLE_PREFIX%faq_category`; +CREATE TABLE IF NOT EXISTS `%TABLE_PREFIX%faq_category` ( + `category_id` int(10) unsigned NOT NULL auto_increment, + `ispublic` TINYINT( 1 ) UNSIGNED NOT NULL DEFAULT '0', + `name` varchar(125) default NULL, + `description` TEXT NOT NULL, + `notes` tinytext NOT NULL, + `created` date NOT NULL, + `updated` date NOT NULL, + PRIMARY KEY (`category_id`), + KEY (`ispublic`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + +DROP TABLE IF EXISTS `%TABLE_PREFIX%faq_topic`; +CREATE TABLE IF NOT EXISTS `%TABLE_PREFIX%faq_topic` ( + `faq_id` int(10) unsigned NOT NULL, + `topic_id` int(10) unsigned NOT NULL, + PRIMARY KEY (`faq_id`,`topic_id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + +DROP TABLE IF EXISTS `%TABLE_PREFIX%config`; +CREATE TABLE `%TABLE_PREFIX%config` ( + `id` tinyint(1) unsigned NOT NULL auto_increment, + `isonline` tinyint(1) unsigned NOT NULL default '0', + `timezone_offset` float(3,1) NOT NULL default '0.0', + `enable_daylight_saving` tinyint(1) unsigned NOT NULL default '0', + `staff_ip_binding` tinyint(1) unsigned NOT NULL default '1', + `staff_max_logins` tinyint(3) unsigned NOT NULL default '4', + `staff_login_timeout` int(10) unsigned NOT NULL default '2', + `staff_session_timeout` int(10) unsigned NOT NULL default '30', + `passwd_reset_period` int(10) unsigned NOT NULL default '0', + `client_max_logins` tinyint(3) unsigned NOT NULL default '4', + `client_login_timeout` int(10) unsigned NOT NULL default '2', + `client_session_timeout` int(10) unsigned NOT NULL default '30', + `max_page_size` tinyint(3) unsigned NOT NULL default '25', + `max_open_tickets` tinyint(3) unsigned NOT NULL default '0', + `max_file_size` int(11) unsigned NOT NULL default '1048576', + `max_user_file_uploads` tinyint(3) unsigned NOT NULL, + `max_staff_file_uploads` tinyint(3) unsigned NOT NULL, + `autolock_minutes` tinyint(3) unsigned NOT NULL default '3', + `overdue_grace_period` int(10) unsigned NOT NULL default '0', + `alert_email_id` tinyint(4) unsigned NOT NULL default '0', + `default_email_id` tinyint(4) unsigned NOT NULL default '0', + `default_dept_id` tinyint(3) unsigned NOT NULL default '0', + `default_sla_id` int(10) unsigned NOT NULL default '0', + `default_priority_id` tinyint(2) unsigned NOT NULL default '2', + `default_template_id` tinyint(4) unsigned NOT NULL default '1', + `default_timezone_id` int(10) unsigned NOT NULL default '0', + `default_smtp_id` tinyint(4) unsigned NOT NULL default '0', + `allow_email_spoofing` tinyint(1) unsigned NOT NULL default '0', + `clickable_urls` tinyint(1) unsigned NOT NULL default '1', + `allow_priority_change` tinyint(1) unsigned NOT NULL default '0', + `use_email_priority` tinyint(1) unsigned NOT NULL default '0', + `enable_kb` tinyint(1) unsigned NOT NULL default '0', + `enable_premade` tinyint(1) unsigned NOT NULL default '1', + `enable_captcha` tinyint(1) unsigned NOT NULL default '0', + `enable_auto_cron` tinyint(1) unsigned NOT NULL default '0', + `enable_mail_polling` tinyint(1) unsigned NOT NULL default '0', + `send_sys_errors` tinyint(1) unsigned NOT NULL default '1', + `send_sql_errors` tinyint(1) unsigned NOT NULL default '1', + `send_mailparse_errors` tinyint(1) unsigned NOT NULL default '1', + `send_login_errors` tinyint(1) unsigned NOT NULL default '1', + `save_email_headers` tinyint(1) unsigned NOT NULL default '1', + `strip_quoted_reply` tinyint(1) unsigned NOT NULL default '1', + `log_ticket_activity` tinyint(1) unsigned NOT NULL default '1', + `ticket_autoresponder` tinyint(1) unsigned NOT NULL default '0', + `message_autoresponder` tinyint(1) unsigned NOT NULL default '0', + `ticket_notice_active` tinyint(1) unsigned NOT NULL default '0', + `ticket_alert_active` tinyint(1) unsigned NOT NULL default '0', + `ticket_alert_admin` tinyint(1) unsigned NOT NULL default '1', + `ticket_alert_dept_manager` tinyint(1) unsigned NOT NULL default '1', + `ticket_alert_dept_members` tinyint(1) unsigned NOT NULL default '0', + `message_alert_active` tinyint(1) unsigned NOT NULL default '0', + `message_alert_laststaff` tinyint(1) unsigned NOT NULL default '1', + `message_alert_assigned` tinyint(1) unsigned NOT NULL default '1', + `message_alert_dept_manager` tinyint(1) unsigned NOT NULL default '0', + `note_alert_active` tinyint(1) unsigned NOT NULL default '0', + `note_alert_laststaff` tinyint(1) unsigned NOT NULL default '1', + `note_alert_assigned` tinyint(1) unsigned NOT NULL default '1', + `note_alert_dept_manager` tinyint(1) unsigned NOT NULL default '0', + `transfer_alert_active` tinyint(1) unsigned NOT NULL default '0', + `transfer_alert_assigned` tinyint(1) unsigned NOT NULL default '0', + `transfer_alert_dept_manager` tinyint(1) unsigned NOT NULL default '1', + `transfer_alert_dept_members` tinyint(1) unsigned NOT NULL default '0', + `overdue_alert_active` tinyint(1) unsigned NOT NULL default '0', + `overdue_alert_assigned` tinyint(1) unsigned NOT NULL default '1', + `overdue_alert_dept_manager` tinyint(1) unsigned NOT NULL default '1', + `overdue_alert_dept_members` tinyint(1) unsigned NOT NULL default '0', + `assigned_alert_active` tinyint(1) unsigned NOT NULL default '1', + `assigned_alert_staff` tinyint(1) unsigned NOT NULL default '1', + `assigned_alert_team_lead` tinyint(1) unsigned NOT NULL default '0', + `assigned_alert_team_members` tinyint(1) unsigned NOT NULL default '0', + `auto_assign_reopened_tickets` tinyint(1) unsigned NOT NULL default '1', + `show_related_tickets` tinyint(1) unsigned NOT NULL default '1', + `show_assigned_tickets` tinyint(1) unsigned NOT NULL default '1', + `show_answered_tickets` tinyint(1) unsigned NOT NULL default '0', + `show_notes_inline` tinyint(1) unsigned NOT NULL default '1', + `hide_staff_name` tinyint(1) unsigned NOT NULL default '0', + `overlimit_notice_active` tinyint(1) unsigned NOT NULL default '0', + `email_attachments` tinyint(1) unsigned NOT NULL default '1', + `allow_attachments` tinyint(1) unsigned NOT NULL default '0', + `allow_email_attachments` tinyint(1) unsigned NOT NULL default '0', + `allow_online_attachments` tinyint(1) unsigned NOT NULL default '0', + `allow_online_attachments_onlogin` tinyint(1) unsigned NOT NULL default + '0', + `random_ticket_ids` tinyint(1) unsigned NOT NULL default '1', + `log_level` tinyint(1) unsigned NOT NULL default '2', + `log_graceperiod` int(10) unsigned NOT NULL default '12', + `upload_dir` varchar(255) NOT NULL default '', + `allowed_filetypes` varchar(255) NOT NULL default '.doc, .pdf', + `time_format` varchar(32) NOT NULL default ' h:i A', + `date_format` varchar(32) NOT NULL default 'm/d/Y', + `datetime_format` varchar(60) NOT NULL default 'm/d/Y g:i a', + `daydatetime_format` varchar(60) NOT NULL default 'D, M j Y g:ia', + `reply_separator` varchar(60) NOT NULL default '-- do not edit --', + `admin_email` varchar(125) NOT NULL default '', + `helpdesk_title` varchar(255) NOT NULL default + 'osTicket Support Ticket System', + `helpdesk_url` varchar(255) NOT NULL default '', + `schema_signature` char(32) NOT NULL default '', + `updated` timestamp NOT NULL default CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + KEY `isoffline` (`isonline`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + +DROP TABLE IF EXISTS `%TABLE_PREFIX%dynamic_formset`; +CREATE TABLE `%TABLE_PREFIX%dynamic_formset` ( + `id` int(11) unsigned auto_increment, + `title` varchar(255) NOT NULL, + `instructions` varchar(512), + `notes` text, + `created` datetime NOT NULL, + `updated` datetime NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + +DROP TABLE IF EXISTS `%TABLE_PREFIX%dynamic_formset_sections`; +CREATE TABLE `%TABLE_PREFIX%dynamic_formset_sections` ( + `id` int(11) unsigned NOT NULL auto_increment, + `formset_id` int(11) NOT NULL, + `section_id` int(11) NOT NULL, + `title` varchar(255), + `instructions` text, + -- Allow more than one form, sorted in this order + `sort` int(11) NOT NULL DEFAULT 1, + PRIMARY KEY (`id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + +DROP TABLE IF EXISTS `%TABLE_PREFIX%dynamic_form_section`; +CREATE TABLE `%TABLE_PREFIX%dynamic_form_section` ( + `id` int(11) unsigned NOT NULL auto_increment, + `title` varchar(255) NOT NULL, + `instructions` varchar(512), + `notes` text, + `created` datetime NOT NULL, + `updated` datetime NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + +DROP TABLE IF EXISTS `%TABLE_PREFIX%dynamic_form_field`; +CREATE TABLE `%TABLE_PREFIX%dynamic_form_field` ( + `id` int(11) unsigned NOT NULL auto_increment, + `section_id` int(11) unsigned NOT NULL, + `type` varchar(255) NOT NULL DEFAULT 'text', + `label` varchar(255) NOT NULL, + `required` tinyint(1) NOT NULL DEFAULT 0, + `private` tinyint(1) NOT NULL DEFAULT 0, + `name` varchar(64) NOT NULL, + `configuration` text, + `sort` int(11) unsigned NOT NULL, + `hint` varchar(512), + `created` datetime NOT NULL, + `updated` datetime NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + +-- Create a default form to mimic the previous default form of osTicket < 1.7.1 +INSERT INTO `%TABLE_PREFIX%dynamic_form_section` SET + `id` = 1, `title` = 'User Information', `created` = NOW(), + `updated` = NOW(); +INSERT INTO `%TABLE_PREFIX%dynamic_form_section` SET + `id` = 2, `title` = 'Ticket Details', `created` = NOW(), + `updated` = NOW(); + +INSERT INTO `%TABLE_PREFIX%dynamic_formset` SET + `id` = 1, `title` = 'Default', `created` = NOW(), `updated` = NOW(); + +INSERT INTO `%TABLE_PREFIX%dynamic_formset_sections` SET + `formset_id` = 1, `section_id` = 1, `sort` = 10; +INSERT INTO `%TABLE_PREFIX%dynamic_formset_sections` SET + `formset_id` = 1, `section_id` = 2, `sort` = 20; + +INSERT INTO `%TABLE_PREFIX%dynamic_form_field` SET + `section_id` = 1, `type` = 'text', `label` = 'Email Address', + `required` = 1, `configuration` = '{"size":40,"length":40,"validator":"email"}', + `name` = 'email', `sort` = 10, `created` = NOW(), `updated` = NOW(); +INSERT INTO `%TABLE_PREFIX%dynamic_form_field` SET + `section_id` = 1, `type` = 'text', `label` = 'Full Name', + `required` = 1, `configuration` = '{"size":40,"length":40}', + `name` = 'name', `sort` = 20, `created` = NOW(), `updated` = NOW(); +INSERT INTO `%TABLE_PREFIX%dynamic_form_field` SET + `section_id` = 1, `type` = 'phone', `label` = 'Phone Number', + `name` = 'phone', `sort` = 30, `created` = NOW(), `updated` = NOW(); + +INSERT INTO `%TABLE_PREFIX%dynamic_form_field` SET + `section_id` = 2, `type` = 'text', `label` = 'Subject', + `hint` = 'Issue summary', `required` = 1, + `configuration` = '{"size":40,"length":50}', + `name` = 'subject', `sort` = 10, `created` = NOW(), `updated` = NOW(); + +DROP TABLE IF EXISTS `%TABLE_PREFIX%dynamic_form_entry`; +CREATE TABLE `%TABLE_PREFIX%dynamic_form_entry` ( + `id` int(11) unsigned NOT NULL auto_increment, + `section_id` int(11) unsigned NOT NULL, + `ticket_id` int(11) unsigned, + `sort` int(11) unsigned NOT NULL DEFAULT 1, + `created` datetime NOT NULL, + `updated` datetime NOT NULL, + PRIMARY KEY (`id`), + KEY `ticket_dyn_form_lookup` (`ticket_id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + +DROP TABLE IF EXISTS `%TABLE_PREFIX%dynamic_form_entry_values`; +CREATE TABLE `%TABLE_PREFIX%dynamic_form_entry_values` ( + -- references dynamic_form_entry.id + `entry_id` int(11) unsigned NOT NULL, + `field_id` int(11) unsigned NOT NULL, + `value` text, + PRIMARY KEY (`entry_id`, `field_id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + +DROP TABLE IF EXISTS `%TABLE_PREFIX%dynamic_list`; +CREATE TABLE `%TABLE_PREFIX%dynamic_list` ( + `id` int(11) unsigned NOT NULL auto_increment, + `name` varchar(255) NOT NULL, + `name_plural` varchar(255), + `sort_mode` enum('Alpha', '-Alpha', 'SortCol') NOT NULL DEFAULT 'Alpha', + `notes` text, + `created` datetime NOT NULL, + `updated` datetime NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + +DROP TABLE IF EXISTS `%TABLE_PREFIX%dynamic_list_items`; +CREATE TABLE `%TABLE_PREFIX%dynamic_list_items` ( + `id` int(11) unsigned NOT NULL auto_increment, + `list_id` int(11), + `value` varchar(255) NOT NULL, + -- extra value such as abbreviation + `extra` varchar(255), + `sort` int(11) NOT NULL DEFAULT 1, + PRIMARY KEY (`id`), + KEY `dynamic_list_item_lookup` (`list_id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + +DROP TABLE IF EXISTS `%TABLE_PREFIX%department`; +CREATE TABLE `%TABLE_PREFIX%department` ( + `dept_id` int(11) unsigned NOT NULL auto_increment, + `tpl_id` int(10) unsigned NOT NULL default '0', + `sla_id` int(10) unsigned NOT NULL default '0', + `email_id` int(10) unsigned NOT NULL default '0', + `autoresp_email_id` int(10) unsigned NOT NULL default '0', + `manager_id` int(10) unsigned NOT NULL default '0', + `dept_name` varchar(128) NOT NULL default '', + `dept_signature` tinytext NOT NULL, + `ispublic` tinyint(1) unsigned NOT NULL default '1', + `group_membership` tinyint(1) NOT NULL default '0', + `ticket_auto_response` tinyint(1) NOT NULL default '1', + `message_auto_response` tinyint(1) NOT NULL default '0', + `updated` datetime NOT NULL, + `created` datetime NOT NULL, + PRIMARY KEY (`dept_id`), + UNIQUE KEY `dept_name` (`dept_name`), + KEY `manager_id` (`manager_id`), + KEY `autoresp_email_id` (`autoresp_email_id`), + KEY `tpl_id` (`tpl_id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + +INSERT INTO `%TABLE_PREFIX%department` (`dept_id`, `tpl_id`, `sla_id`, `email_id`, `autoresp_email_id`, `manager_id`, `dept_name`, `dept_signature`, `ispublic`, `ticket_auto_response`, `message_auto_response`) VALUES + (1, 0, 0, 1, 1, 0, 'Support', 'Support Dept', 1, 1, 1), + (2, 0, 1, 1, 1, 0, 'Billing', 'Billing Dept', 1, 1, 1); + +DROP TABLE IF EXISTS `%TABLE_PREFIX%email`; +CREATE TABLE `%TABLE_PREFIX%email` ( + `email_id` int(11) unsigned NOT NULL auto_increment, + `noautoresp` tinyint(1) unsigned NOT NULL default '0', + `priority_id` tinyint(3) unsigned NOT NULL default '2', + `dept_id` tinyint(3) unsigned NOT NULL default '0', + `email` varchar(255) NOT NULL default '', + `name` varchar(255) NOT NULL default '', + `userid` varchar(255) NOT NULL, + `userpass` varchar(125) NOT NULL, + `mail_active` tinyint(1) NOT NULL default '0', + `mail_host` varchar(255) NOT NULL, + `mail_protocol` enum('POP','IMAP') NOT NULL default 'POP', + `mail_encryption` enum('NONE','SSL') NOT NULL, + `mail_port` int(6) default NULL, + `mail_fetchfreq` tinyint(3) NOT NULL default '5', + `mail_fetchmax` tinyint(4) NOT NULL default '30', + `mail_archivefolder` varchar(255) default NULL, + `mail_delete` tinyint(1) NOT NULL default '0', + `mail_errors` tinyint(3) NOT NULL default '0', + `mail_lasterror` datetime default NULL, + `mail_lastfetch` datetime default NULL, + `smtp_active` tinyint(1) default '0', + `smtp_host` varchar(255) NOT NULL, + `smtp_port` int(6) default NULL, + `smtp_secure` tinyint(1) NOT NULL default '1', + `smtp_auth` tinyint(1) NOT NULL default '1', + `smtp_spoofing` tinyint(1) unsigned NOT NULL default '0', + `notes` text, + `created` datetime NOT NULL, + `updated` datetime NOT NULL, + PRIMARY KEY (`email_id`), + UNIQUE KEY `email` (`email`), + KEY `priority_id` (`priority_id`), + KEY `dept_id` (`dept_id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + +DROP TABLE IF EXISTS `%TABLE_PREFIX%filter`; +CREATE TABLE `%TABLE_PREFIX%filter` ( + `id` int(11) unsigned NOT NULL auto_increment, + `execorder` int(10) unsigned NOT NULL default '99', + `isactive` tinyint(1) unsigned NOT NULL default '1', + `match_all_rules` tinyint(1) unsigned NOT NULL default '0', + `stop_onmatch` tinyint(1) unsigned NOT NULL default '0', + `reject_ticket` tinyint(1) unsigned NOT NULL default '0', + `use_replyto_email` tinyint(1) unsigned NOT NULL default '0', + `disable_autoresponder` tinyint(1) unsigned NOT NULL default '0', + `canned_response_id` int(11) unsigned NOT NULL default '0', + `email_id` int(10) unsigned NOT NULL default '0', + `priority_id` int(10) unsigned NOT NULL default '0', + `dept_id` int(10) unsigned NOT NULL default '0', + `staff_id` int(10) unsigned NOT NULL default '0', + `team_id` int(10) unsigned NOT NULL default '0', + `sla_id` int(10) unsigned NOT NULL default '0', + `target` ENUM( 'Any', 'Web', 'Email', 'API' ) NOT NULL DEFAULT 'Any', + `name` varchar(32) NOT NULL default '', + `notes` text, + `created` datetime NOT NULL, + `updated` datetime NOT NULL, + PRIMARY KEY (`id`), + KEY `target` (`target`), + KEY `email_id` (`email_id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + + +INSERT INTO `%TABLE_PREFIX%filter` ( + `id`,`isactive`,`execorder`,`reject_ticket`,`name`,`notes`,`created`) + VALUES (1, 1, 99, 1, 'SYSTEM BAN LIST', 'Internal list for email banning. Do not remove', NOW()); + +DROP TABLE IF EXISTS `%TABLE_PREFIX%filter_rule`; +CREATE TABLE `%TABLE_PREFIX%filter_rule` ( + `id` int(11) unsigned NOT NULL auto_increment, + `filter_id` int(10) unsigned NOT NULL default '0', + `what` enum('name','email','subject','body','header') NOT NULL, + `how` enum('equal','not_equal','contains','dn_contain','starts','ends') NOT NULL, + `val` varchar(255) NOT NULL, + `isactive` tinyint(1) unsigned NOT NULL DEFAULT '1', + `notes` tinytext NOT NULL, + `created` datetime NOT NULL, + `updated` datetime NOT NULL, + PRIMARY KEY (`id`), + KEY `filter_id` (`filter_id`), + UNIQUE `filter` (`filter_id`, `what`, `how`, `val`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + +INSERT INTO `%TABLE_PREFIX%filter_rule` ( + `id`, `filter_id`, `isactive`, `what`,`how`,`val`,`created`) + VALUES (1, 1, 1, 'email', 'equal', 'test@example.com',NOW()); + +DROP TABLE IF EXISTS `%TABLE_PREFIX%email_template`; +CREATE TABLE `%TABLE_PREFIX%email_template` ( + `tpl_id` int(11) NOT NULL auto_increment, + `cfg_id` int(10) unsigned NOT NULL default '0', + `isactive` tinyint(1) unsigned NOT NULL default '0', + `name` varchar(32) NOT NULL default '', + `notes` text, + `ticket_autoresp_subj` varchar(255) NOT NULL default '', + `ticket_autoresp_body` text NOT NULL, + `ticket_autoreply_subj` varchar(255) NOT NULL default '', + `ticket_autoreply_body` text NOT NULL, + `ticket_notice_subj` varchar(255) NOT NULL, + `ticket_notice_body` text NOT NULL, + `ticket_alert_subj` varchar(255) NOT NULL default '', + `ticket_alert_body` text NOT NULL, + `message_autoresp_subj` varchar(255) NOT NULL default '', + `message_autoresp_body` text NOT NULL, + `message_alert_subj` varchar(255) NOT NULL default '', + `message_alert_body` text NOT NULL, + `note_alert_subj` varchar(255) NOT NULL, + `note_alert_body` text NOT NULL, + `assigned_alert_subj` varchar(255) NOT NULL default '', + `assigned_alert_body` text NOT NULL, + `transfer_alert_subj` varchar(255) NOT NULL default '', + `transfer_alert_body` text NOT NULL, + `ticket_overdue_subj` varchar(255) NOT NULL default '', + `ticket_overdue_body` text NOT NULL, + `ticket_overlimit_subj` varchar(255) NOT NULL default '', + `ticket_overlimit_body` text NOT NULL, + `ticket_reply_subj` varchar(255) NOT NULL default '', + `ticket_reply_body` text NOT NULL, + `created` datetime NOT NULL, + `updated` datetime NOT NULL, + PRIMARY KEY (`tpl_id`), + KEY `cfg_id` (`cfg_id`), + FULLTEXT KEY `message_subj` (`ticket_reply_subj`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + +-- TODO: Dump revised copy before release!!! +INSERT INTO `%TABLE_PREFIX%email_template` (`tpl_id`, `cfg_id`, `isactive`, `name`, `notes`, `ticket_autoresp_subj`, `ticket_autoresp_body`, `ticket_autoreply_subj`, `ticket_autoreply_body`, `ticket_notice_subj`, `ticket_notice_body`, `ticket_alert_subj`, `ticket_alert_body`, `message_autoresp_subj`, `message_autoresp_body`, `message_alert_subj`, `message_alert_body`, `note_alert_subj`, `note_alert_body`, `assigned_alert_subj`, `assigned_alert_body`, `transfer_alert_subj`, `transfer_alert_body`, `ticket_overdue_subj`, `ticket_overdue_body`, `ticket_overlimit_subj`, `ticket_overlimit_body`, `ticket_reply_subj`, `ticket_reply_body`, `created`, `updated`) VALUES +(1, 1, 1, 'osTicket Default Template', 'Default osTicket templates', 'Support Ticket Opened [#%{ticket.number}]', '%{ticket.name},\r\n\r\nA request for support has been created and assigned ticket #%{ticket.number}. A representative will follow-up with you as soon as possible.\r\n\r\nYou can view this ticket''s progress online here: %{ticket.client_link}.\r\n\r\nIf you wish to send additional comments or information regarding this issue, please don''t open a new ticket. Simply login using the link above and update the ticket.\r\n\r\n%{signature}', 'Support Ticket Opened [#%{ticket.number}]', '%{ticket.name},\r\n\r\nA request for support has been created and assigned ticket #%{ticket.number} with the following auto-reply:\r\n\r\n%{response}\r\n\r\n\r\nWe hope this response has sufficiently answered your questions. If not, please do not open another ticket. If need be, representative will follow-up with you as soon as possible.\r\n\r\nYou can view this ticket''s progress online here: %{ticket.client_link}.', '[#%{ticket.number}] %{ticket.subject}', '%{ticket.name},\r\n\r\nOur customer care team has created a ticket, #%{ticket.number} on your behalf, with the following message.\r\n\r\n%{message}\r\n\r\nIf you wish to provide additional comments or information regarding this issue, please don''t open a new ticket. You can update or view this ticket''s progress online here: %{ticket.client_link}.\r\n\r\n%{signature}', 'New Ticket Alert', '%{recipient},\r\n\r\nNew ticket #%{ticket.number} created.\r\n\r\n-----------------------\r\nName: %{ticket.name}\r\nEmail: %{ticket.email}\r\nDept: %{ticket.dept.name}\r\n\r\n%{message}\r\n-----------------------\r\n\r\nTo view/respond to the ticket, please login to the support ticket system.\r\n\r\n%{ticket.staff_link}\r\n\r\n- Your friendly Customer Support System - powered by osTicket.', '[#%{ticket.number}] Message Added', '%{ticket.name},\r\n\r\nYour reply to support request #%{ticket.number} has been noted.\r\n\r\nYou can view this support request progress online here: %{ticket.client_link}.\r\n\r\n%{signature}', 'New Message Alert', '%{recipient},\r\n\r\nNew message appended to ticket #%{ticket.number}\r\n\r\n----------------------\r\nName: %{ticket.name}\r\nEmail: %{ticket.email}\r\nDept: %{ticket.dept.name}\r\n\r\n%{message}\r\n----------------------\r\n\r\nTo view/respond to the ticket, please login to the support ticket system.\r\n\r\n%{ticket.staff_link}\r\n\r\n- Your friendly Customer Support System - powered by osTicket.', 'New Internal Note Alert', '%{recipient},\r\n\r\nInternal note appended to ticket #%{ticket.number}\r\n\r\n----------------------\r\n* %{note.title} *\r\n\r\n%{note.message}\r\n----------------------\r\n\r\nTo view/respond to the ticket, please login to the support ticket system.\r\n\r\n%{ticket.staff_link}\r\n\r\n- Your friendly Customer Support System - powered by osTicket.', 'Ticket #%{ticket.number} Assigned to you', '%{assignee},\r\n\r\nTicket #%{ticket.number} has been assigned to you by %{assigner}\r\n\r\n----------------------\r\n\r\n%{comments}\r\n\r\n----------------------\r\n\r\nTo view complete details, simply login to the support system.\r\n\r\n%{ticket.staff_link}\r\n\r\n- Your friendly Support Ticket System - powered by osTicket.', 'Ticket Transfer #%{ticket.number} - %{ticket.dept.name}', '%{recipient},\r\n\r\nTicket #%{ticket.number} has been transferred to %{ticket.dept.name} department by %{staff.name}\r\n\r\n----------------------\r\n\r\n%{comments}\r\n\r\n----------------------\r\n\r\nTo view/respond to the ticket, please login to the support ticket system.\r\n\r\n%{ticket.staff_link}\r\n\r\n- Your friendly Customer Support System - powered by osTicket.', 'Stale Ticket Alert', '%{recipient},\r\n\r\nA ticket, #%{ticket.number} assigned to you or in your department is seriously overdue.\r\n\r\n%{ticket.staff_link}\r\n\r\nWe should all work hard to guarantee that all tickets are being addressed in a timely manner.\r\n\r\n- Your friendly (although with limited patience) Support Ticket System - powered by osTicket.', 'Open Tickets Limit Reached', '%{ticket.name}\r\n\r\nYou have reached the maximum number of open tickets allowed.\r\n\r\nTo be able to open another ticket, one of your pending tickets must be closed. To update or add comments to an open ticket simply login using the link below.\r\n\r\n%{url}/tickets.php?e=%{ticket.email}\r\n\r\nThank you.\r\n\r\nSupport Ticket System', '[#%{ticket.number}] %{ticket.subject}', '%{ticket.name},\r\n\r\nA customer support staff member has replied to your support request, #%{ticket.number} with the following response:\r\n\r\n%{response}\r\n\r\nWe hope this response has sufficiently answered your questions. If not, please do not send another email. Instead, reply to this email or login to your account for a complete archive of all your support requests and responses.\r\n\r\n%{ticket.client_link}\r\n\r\n%{signature}', NOW(), NOW()); + +DROP TABLE IF EXISTS `%TABLE_PREFIX%file`; +CREATE TABLE `%TABLE_PREFIX%file` ( + `id` int(11) NOT NULL auto_increment, + `type` varchar(255) NOT NULL default '', + `size` varchar(25) NOT NULL default '', + `hash` varchar(125) NOT NULL, + `name` varchar(255) NOT NULL default '', + `created` datetime NOT NULL, + PRIMARY KEY (`id`), + KEY `hash` (`hash`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + +INSERT INTO `%TABLE_PREFIX%file` (`id`, `type`, `size`, `hash`, `name`, `created`) VALUES +(1, 'text/plain', '25', '670c6cc1d1dfc97fad20e5470251b255', 'osTicket.txt', NOW()); + +DROP TABLE IF EXISTS `%TABLE_PREFIX%file_chunk`; +CREATE TABLE `%TABLE_PREFIX%file_chunk` ( + `file_id` int(11) NOT NULL, + `chunk_id` int(11) NOT NULL, + `filedata` longblob NOT NULL, + PRIMARY KEY (`file_id`, `chunk_id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + +INSERT INTO `%TABLE_PREFIX%file_chunk` (`file_id`, `chunk_id`, `filedata`) +VALUES (1, 0, 0x43616e6e6564206174746163686d656e747320726f636b210a); + +DROP TABLE IF EXISTS `%TABLE_PREFIX%groups`; +CREATE TABLE `%TABLE_PREFIX%groups` ( + `group_id` int(10) unsigned NOT NULL auto_increment, + `group_enabled` tinyint(1) unsigned NOT NULL default '1', + `group_name` varchar(50) NOT NULL default '', + `can_create_tickets` tinyint(1) unsigned NOT NULL default '1', + `can_edit_tickets` tinyint(1) unsigned NOT NULL default '1', + `can_post_ticket_reply` tinyint( 1 ) unsigned NOT NULL DEFAULT '1', + `can_delete_tickets` tinyint(1) unsigned NOT NULL default '0', + `can_close_tickets` tinyint(1) unsigned NOT NULL default '1', + `can_assign_tickets` tinyint(1) unsigned NOT NULL default '1', + `can_transfer_tickets` tinyint(1) unsigned NOT NULL default '1', + `can_ban_emails` tinyint(1) unsigned NOT NULL default '0', + `can_manage_premade` tinyint(1) unsigned NOT NULL default '0', + `can_manage_faq` tinyint(1) unsigned NOT NULL default '0', + `can_view_staff_stats` tinyint( 1 ) unsigned NOT NULL DEFAULT '0', + `notes` text, + `created` datetime NOT NULL, + `updated` datetime NOT NULL, + PRIMARY KEY (`group_id`), + KEY `group_active` (`group_enabled`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + +INSERT INTO `%TABLE_PREFIX%groups` (`group_id`, `group_enabled`, `group_name`, `can_create_tickets`, `can_edit_tickets`, `can_delete_tickets`, `can_close_tickets`, `can_assign_tickets`, `can_transfer_tickets`, `can_ban_emails`, `can_manage_premade`, `can_manage_faq`, `notes`, `created`, `updated`) VALUES + (1, 1, 'Admins', 1, 1, 1, 1, 1, 1, 1, 1, 1, 'overlords', NOW(), NOW()), + (2, 1, 'Managers', 1, 1, 1, 1, 1, 1, 1, 1, 1, '', NOW(), NOW()), + (3, 1, 'Staff', 1, 1, 0, 1, 1, 1, 0, 0, 0, '', NOW(), NOW()); + +DROP TABLE IF EXISTS `%TABLE_PREFIX%group_dept_access`; +CREATE TABLE `%TABLE_PREFIX%group_dept_access` ( + `group_id` int(10) unsigned NOT NULL default '0', + `dept_id` int(10) unsigned NOT NULL default '0', + UNIQUE KEY `group_dept` (`group_id`,`dept_id`), + KEY `dept_id` (`dept_id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + +INSERT INTO `%TABLE_PREFIX%group_dept_access` (`group_id`, `dept_id`) VALUES + (1, 1), (1, 2), (2, 1), (2, 2), (3, 1), (3, 2); + +DROP TABLE IF EXISTS `%TABLE_PREFIX%help_topic`; +CREATE TABLE `%TABLE_PREFIX%help_topic` ( + `topic_id` int(11) unsigned NOT NULL auto_increment, + `topic_pid` int(10) unsigned NOT NULL default '0', + `isactive` tinyint(1) unsigned NOT NULL default '1', + `ispublic` tinyint(1) unsigned NOT NULL default '1', + `noautoresp` tinyint(3) unsigned NOT NULL default '0', + `priority_id` tinyint(3) unsigned NOT NULL default '0', + `dept_id` tinyint(3) unsigned NOT NULL default '0', + `staff_id` int(10) unsigned NOT NULL default '0', + `team_id` int(10) unsigned NOT NULL default '0', + `sla_id` int(10) unsigned NOT NULL default '0', + `formset_id` int(11) unsigned NOT NULL default '0', + `topic` varchar(32) NOT NULL default '', + `notes` text, + `created` datetime NOT NULL, + `updated` datetime NOT NULL, + PRIMARY KEY (`topic_id`), + UNIQUE KEY `topic` ( `topic` , `topic_pid` ), + KEY `topic_pid` (`topic_pid`), + KEY `priority_id` (`priority_id`), + KEY `dept_id` (`dept_id`), + KEY `staff_id` (`staff_id`,`team_id`), + KEY `sla_id` (`sla_id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + +INSERT INTO `%TABLE_PREFIX%help_topic` (`topic_id`, `isactive`, `ispublic`, + `noautoresp`, `priority_id`, `dept_id`, `staff_id`, `team_id`, + `sla_id`, `formset_id`, `topic`, `notes`) VALUES + (1, 1, 1, 0, 2, 1, 0, 0, 1, 1, 'Support', NULL), + (2, 1, 1, 0, 3, 1, 0, 0, 0, 1, 'Billing', NULL); + +DROP TABLE IF EXISTS `%TABLE_PREFIX%canned_response`; +CREATE TABLE `%TABLE_PREFIX%canned_response` ( + `canned_id` int(10) unsigned NOT NULL auto_increment, + `dept_id` int(10) unsigned NOT NULL default '0', + `isenabled` tinyint(1) unsigned NOT NULL default '1', + `title` varchar(255) NOT NULL default '', + `response` text NOT NULL, + `notes` text, + `created` datetime NOT NULL, + `updated` datetime NOT NULL, + PRIMARY KEY (`canned_id`), + UNIQUE KEY `title` (`title`), + KEY `dept_id` (`dept_id`), + KEY `active` (`isenabled`), + FULLTEXT KEY `resp` (`title`,`response`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + +INSERT INTO `%TABLE_PREFIX%canned_response` (`canned_id`, `dept_id`, `isenabled`, `title`, `response`) VALUES + (1, 0, 1, 'What is osTicket (sample)?', '\r\nosTicket is a widely-used open source support ticket system, an attractive alternative to higher-cost and complex customer support systems - simple, lightweight, reliable, open source, web-based and easy to setup and use.'), + (2, 0, 1, 'Sample (with variables)', '\r\n%{ticket.name},\r\n\r\nYour ticket #%{ticket.number} created on %{ticket.create_date} is in %{ticket.dept.name} department.\r\n\r\n'); + +DROP TABLE IF EXISTS `%TABLE_PREFIX%canned_attachment`; +CREATE TABLE IF NOT EXISTS `%TABLE_PREFIX%canned_attachment` ( + `canned_id` int(10) unsigned NOT NULL, + `file_id` int(10) unsigned NOT NULL, + PRIMARY KEY (`canned_id`,`file_id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + +INSERT INTO `%TABLE_PREFIX%canned_attachment` (`canned_id`, `file_id`) VALUES (1,1); + +DROP TABLE IF EXISTS `%TABLE_PREFIX%session`; +CREATE TABLE `%TABLE_PREFIX%session` ( + `session_id` varchar(256) collate utf8_unicode_ci NOT NULL default '', + `session_data` longtext collate utf8_unicode_ci, + `session_expire` datetime default NULL, + `session_updated` datetime default NULL, + `user_id` int(10) unsigned NOT NULL default '0' COMMENT 'osTicket staff ID', + `user_ip` varchar(64) NOT NULL, + `user_agent` varchar(255) collate utf8_unicode_ci NOT NULL, + PRIMARY KEY (`session_id`), + KEY `updated` (`session_updated`), + KEY `user_id` (`user_id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; + +DROP TABLE IF EXISTS `%TABLE_PREFIX%sla`; +CREATE TABLE `%TABLE_PREFIX%sla` ( + `id` int(11) unsigned NOT NULL auto_increment, + `isactive` tinyint(1) unsigned NOT NULL default '1', + `enable_priority_escalation` tinyint(1) unsigned NOT NULL default '1', + `disable_overdue_alerts` tinyint(1) unsigned NOT NULL default '0', + `grace_period` int(10) unsigned NOT NULL default '0', + `name` varchar(64) NOT NULL default '', + `notes` text, + `created` datetime NOT NULL, + `updated` datetime NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `name` (`name`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + +INSERT INTO `%TABLE_PREFIX%sla` (`isactive`, `enable_priority_escalation`, + `disable_overdue_alerts`, `grace_period`, `name`, `notes`, `created`, `updated`) + VALUES (1, 1, 0, 48, 'Default SLA', NULL, NOW(), NOW()); + +DROP TABLE IF EXISTS `%TABLE_PREFIX%staff`; +CREATE TABLE `%TABLE_PREFIX%staff` ( + `staff_id` int(11) unsigned NOT NULL auto_increment, + `group_id` int(10) unsigned NOT NULL default '0', + `dept_id` int(10) unsigned NOT NULL default '0', + `timezone_id` int(10) unsigned NOT NULL default '0', + `username` varchar(32) NOT NULL default '', + `firstname` varchar(32) default NULL, + `lastname` varchar(32) default NULL, + `passwd` varchar(128) default NULL, + `email` varchar(128) default NULL, + `phone` varchar(24) NOT NULL default '', + `phone_ext` varchar(6) default NULL, + `mobile` varchar(24) NOT NULL default '', + `signature` tinytext NOT NULL, + `notes` text, + `isactive` tinyint(1) NOT NULL default '1', + `isadmin` tinyint(1) NOT NULL default '0', + `isvisible` tinyint(1) unsigned NOT NULL default '1', + `onvacation` tinyint(1) unsigned NOT NULL default '0', + `assigned_only` tinyint(1) unsigned NOT NULL default '0', + `show_assigned_tickets` tinyint(1) unsigned NOT NULL default '0', + `daylight_saving` tinyint(1) unsigned NOT NULL default '0', + `change_passwd` tinyint(1) unsigned NOT NULL default '0', + `max_page_size` int(11) unsigned NOT NULL default '0', + `auto_refresh_rate` int(10) unsigned NOT NULL default '0', + `default_signature_type` ENUM( 'none', 'mine', 'dept' ) NOT NULL DEFAULT 'none', + `default_paper_size` ENUM( 'Letter', 'Legal', 'Ledger', 'A4', 'A3' ) NOT NULL DEFAULT 'Letter', + `created` datetime NOT NULL, + `lastlogin` datetime default NULL, + `passwdreset` datetime default NULL, + `updated` datetime NOT NULL, + PRIMARY KEY (`staff_id`), + UNIQUE KEY `username` (`username`), + KEY `dept_id` (`dept_id`), + KEY `issuperuser` (`isadmin`), + KEY `group_id` (`group_id`,`staff_id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + +DROP TABLE IF EXISTS `%TABLE_PREFIX%syslog`; +CREATE TABLE `%TABLE_PREFIX%syslog` ( + `log_id` int(11) unsigned NOT NULL auto_increment, + `log_type` enum('Debug','Warning','Error') NOT NULL, + `title` varchar(255) NOT NULL, + `log` text NOT NULL, + `logger` varchar(64) NOT NULL, + `ip_address` varchar(64) NOT NULL, + `created` datetime NOT NULL, + `updated` datetime NOT NULL, + PRIMARY KEY (`log_id`), + KEY `log_type` (`log_type`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + +DROP TABLE IF EXISTS `%TABLE_PREFIX%team`; +CREATE TABLE `%TABLE_PREFIX%team` ( + `team_id` int(10) unsigned NOT NULL auto_increment, + `lead_id` int(10) unsigned NOT NULL default '0', + `isenabled` tinyint(1) unsigned NOT NULL default '1', + `noalerts` tinyint(1) unsigned NOT NULL default '0', + `name` varchar(125) NOT NULL default '', + `notes` text, + `created` datetime NOT NULL, + `updated` datetime NOT NULL, + PRIMARY KEY (`team_id`), + UNIQUE KEY `name` (`name`), + KEY `isnabled` (`isenabled`), + KEY `lead_id` (`lead_id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + +INSERT INTO `%TABLE_PREFIX%team` (`lead_id`, `isenabled`, `noalerts`, `name`, `notes`, `created`, `updated`) + VALUES (0, 1, 0, 'Level I Support', '', NOW(), NOW()); + +DROP TABLE IF EXISTS `%TABLE_PREFIX%team_member`; +CREATE TABLE `%TABLE_PREFIX%team_member` ( + `team_id` int(10) unsigned NOT NULL default '0', + `staff_id` int(10) unsigned NOT NULL, + `updated` datetime NOT NULL, + PRIMARY KEY (`team_id`,`staff_id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + +DROP TABLE IF EXISTS `%TABLE_PREFIX%ticket`; +CREATE TABLE `%TABLE_PREFIX%ticket` ( + `ticket_id` int(11) unsigned NOT NULL auto_increment, + `ticketID` int(11) unsigned NOT NULL default '0', + `dept_id` int(10) unsigned NOT NULL default '1', + `sla_id` int(10) unsigned NOT NULL default '0', + `priority_id` int(10) unsigned NOT NULL default '2', + `topic_id` int(10) unsigned NOT NULL default '0', + `staff_id` int(10) unsigned NOT NULL default '0', + `team_id` int(10) unsigned NOT NULL default '0', + `ip_address` varchar(64) NOT NULL default '', + `status` enum('open','closed') NOT NULL default 'open', + `source` enum('Web','Email','Phone','API','Other') NOT NULL default +'Other', + `isoverdue` tinyint(1) unsigned NOT NULL default '0', + `isanswered` tinyint(1) unsigned NOT NULL default '0', + `duedate` datetime default NULL, + `reopened` datetime default NULL, + `closed` datetime default NULL, + `lastmessage` datetime default NULL, + `lastresponse` datetime default NULL, + `created` datetime NOT NULL, + `updated` datetime NOT NULL, + PRIMARY KEY (`ticket_id`), + KEY `dept_id` (`dept_id`), + KEY `staff_id` (`staff_id`), + KEY `team_id` (`staff_id`), + KEY `status` (`status`), + KEY `priority_id` (`priority_id`), + KEY `created` (`created`), + KEY `closed` (`closed`), + KEY `duedate` (`duedate`), + KEY `topic_id` (`topic_id`), + KEY `sla_id` (`sla_id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + +DROP TABLE IF EXISTS `%TABLE_PREFIX%ticket_attachment`; +CREATE TABLE `%TABLE_PREFIX%ticket_attachment` ( + `attach_id` int(11) unsigned NOT NULL auto_increment, + `ticket_id` int(11) unsigned NOT NULL default '0', + `file_id` int(10) unsigned NOT NULL default '0', + `ref_id` int(11) unsigned NOT NULL default '0', + `ref_type` enum('M','R','N') NOT NULL default 'M', + `created` datetime NOT NULL, + PRIMARY KEY (`attach_id`), + KEY `ticket_id` (`ticket_id`), + KEY `ref_type` (`ref_type`), + KEY `ref_id` (`ref_id`), + KEY `file_id` (`file_id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + +DROP TABLE IF EXISTS `%TABLE_PREFIX%ticket_lock`; +CREATE TABLE `%TABLE_PREFIX%ticket_lock` ( + `lock_id` int(11) unsigned NOT NULL auto_increment, + `ticket_id` int(11) unsigned NOT NULL default '0', + `staff_id` int(10) unsigned NOT NULL default '0', + `expire` datetime default NULL, + `created` datetime NOT NULL, + PRIMARY KEY (`lock_id`), + UNIQUE KEY `ticket_id` (`ticket_id`), + KEY `staff_id` (`staff_id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + +DROP TABLE IF EXISTS `%TABLE_PREFIX%ticket_email_info`; +CREATE TABLE `%TABLE_PREFIX%ticket_email_info` ( + `message_id` int(11) unsigned NOT NULL, + `email_mid` varchar(255) NOT NULL, + `headers` text, + KEY `message_id` (`email_mid`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + +DROP TABLE IF EXISTS `%TABLE_PREFIX%ticket_event`; +CREATE TABLE `%TABLE_PREFIX%ticket_event` ( + `ticket_id` int(11) unsigned NOT NULL default '0', + `staff_id` int(11) unsigned NOT NULL, + `team_id` int(11) unsigned NOT NULL, + `dept_id` int(11) unsigned NOT NULL, + `topic_id` int(11) unsigned NOT NULL, + `state` enum('created','closed','reopened','assigned','transferred','overdue') NOT NULL, + `staff` varchar(255) NOT NULL default 'SYSTEM', + `annulled` tinyint(1) unsigned NOT NULL default '0', + `timestamp` datetime NOT NULL, + KEY `ticket_state` (`ticket_id`, `state`, `timestamp`), + KEY `ticket_stats` (`timestamp`, `state`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + +DROP TABLE IF EXISTS `%TABLE_PREFIX%ticket_priority`; +CREATE TABLE `%TABLE_PREFIX%ticket_priority` ( + `priority_id` tinyint(4) NOT NULL auto_increment, + `priority` varchar(60) NOT NULL default '', + `priority_desc` varchar(30) NOT NULL default '', + `priority_color` varchar(7) NOT NULL default '', + `priority_urgency` tinyint(1) unsigned NOT NULL default '0', + `ispublic` tinyint(1) NOT NULL default '1', + PRIMARY KEY (`priority_id`), + UNIQUE KEY `priority` (`priority`), + KEY `priority_urgency` (`priority_urgency`), + KEY `ispublic` (`ispublic`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + +INSERT INTO `%TABLE_PREFIX%ticket_priority` (`priority_id`, `priority`, `priority_desc`, `priority_color`, `priority_urgency`, `ispublic`) VALUES + (1, 'low', 'Low', '#DDFFDD', 4, 1), + (2, 'normal', 'Normal', '#FFFFF0', 3, 1), + (3, 'high', 'High', '#FEE7E7', 2, 1), + (4, 'emergency', 'Emergency', '#FEE7E7', 1, 0); + +DROP TABLE IF EXISTS `%TABLE_PREFIX%ticket_thread`; +CREATE TABLE `%TABLE_PREFIX%ticket_thread` ( + `id` int(11) unsigned NOT NULL auto_increment, + `pid` int(11) unsigned NOT NULL default '0', + `ticket_id` int(11) unsigned NOT NULL default '0', + `staff_id` int(11) unsigned NOT NULL default '0', + `thread_type` enum('M','R','N') NOT NULL, + `poster` varchar(128) NOT NULL default '', + `source` varchar(32) NOT NULL default '', + `title` varchar(255), + `body` text NOT NULL, + `ip_address` varchar(64) NOT NULL default '', + `created` datetime NOT NULL, + `updated` datetime NOT NULL, + PRIMARY KEY (`id`), + KEY `ticket_id` (`ticket_id`), + KEY `staff_id` (`staff_id`), + KEY `pid` (`pid`), + FULLTEXT KEY `body` (`body`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + +DROP TABLE IF EXISTS `%TABLE_PREFIX%timezone`; +CREATE TABLE `%TABLE_PREFIX%timezone` ( + `id` int(11) unsigned NOT NULL auto_increment, + `offset` float(3,1) NOT NULL default '0.0', + `timezone` varchar(255) NOT NULL default '', + PRIMARY KEY (`id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + +INSERT INTO `%TABLE_PREFIX%timezone` (`id`, `offset`, `timezone`) VALUES +(1, -12.0, 'Eniwetok, Kwajalein'), +(2, -11.0, 'Midway Island, Samoa'), +(3, -10.0, 'Hawaii'), +(4, -9.0, 'Alaska'), +(5, -8.0, 'Pacific Time (US & Canada)'), +(6, -7.0, 'Mountain Time (US & Canada)'), +(7, -6.0, 'Central Time (US & Canada), Mexico City'), +(8, -5.0, 'Eastern Time (US & Canada), Bogota, Lima'), +(9, -4.0, 'Atlantic Time (Canada), Caracas, La Paz'), +(10, -3.5, 'Newfoundland'), +(11, -3.0, 'Brazil, Buenos Aires, Georgetown'), +(12, -2.0, 'Mid-Atlantic'), +(13, -1.0, 'Azores, Cape Verde Islands'), +(14, 0.0, 'Western Europe Time, London, Lisbon, Casablanca'), +(15, 1.0, 'Brussels, Copenhagen, Madrid, Paris'), +(16, 2.0, 'Kaliningrad, South Africa'), +(17, 3.0, 'Baghdad, Riyadh, Moscow, St. Petersburg'), +(18, 3.5, 'Tehran'), +(19, 4.0, 'Abu Dhabi, Muscat, Baku, Tbilisi'), +(20, 4.5, 'Kabul'), +(21, 5.0, 'Ekaterinburg, Islamabad, Karachi, Tashkent'), +(22, 5.5, 'Bombay, Calcutta, Madras, New Delhi'), +(23, 6.0, 'Almaty, Dhaka, Colombo'), +(24, 7.0, 'Bangkok, Hanoi, Jakarta'), +(25, 8.0, 'Beijing, Perth, Singapore, Hong Kong'), +(26, 9.0, 'Tokyo, Seoul, Osaka, Sapporo, Yakutsk'), +(27, 9.5, 'Adelaide, Darwin'), +(28, 10.0, 'Eastern Australia, Guam, Vladivostok'), +(29, 11.0, 'Magadan, Solomon Islands, New Caledonia'), +(30, 12.0, 'Auckland, Wellington, Fiji, Kamchatka'); diff --git a/setup/inc/streams/core/install-mysql.sql b/setup/inc/streams/core/install-mysql.sql index 4891bcb0f..d37ff15db 100644 --- a/setup/inc/streams/core/install-mysql.sql +++ b/setup/inc/streams/core/install-mysql.sql @@ -170,6 +170,103 @@ INSERT INTO `%TABLE_PREFIX%config` (`namespace`, `key`, `value`) VALUES ('core', 'helpdesk_url', ''), ('core', 'schema_signature', ''); +DROP TABLE IF EXISTS `%TABLE_PREFIX%formset`; +CREATE TABLE `%TABLE_PREFIX%formset` ( + `id` int(11) unsigned auto_increment, + `title` varchar(255) NOT NULL, + `instructions` varchar(512), + `notes` text, + `created` datetime NOT NULL, + `updated` datetime NOT NULL, + PRIMARY KEY (`id`) +) DEFAULT CHARSET=utf8; + +DROP TABLE IF EXISTS `%TABLE_PREFIX%formset_sections`; +CREATE TABLE `%TABLE_PREFIX%formset_sections` ( + `id` int(11) unsigned NOT NULL auto_increment, + `formset_id` int(11) NOT NULL, + `section_id` int(11) NOT NULL, + `title` varchar(255), + `instructions` text, + -- Allow more than one form, sorted in this order + `sort` int(11) NOT NULL DEFAULT 1, + PRIMARY KEY (`id`) +) DEFAULT CHARSET=utf8; + +DROP TABLE IF EXISTS `%TABLE_PREFIX%form_section`; +CREATE TABLE `%TABLE_PREFIX%form_section` ( + `id` int(11) unsigned NOT NULL auto_increment, + `title` varchar(255) NOT NULL, + `instructions` varchar(512), + `notes` text, + `created` datetime NOT NULL, + `updated` datetime NOT NULL, + PRIMARY KEY (`id`) +) DEFAULT CHARSET=utf8; + +DROP TABLE IF EXISTS `%TABLE_PREFIX%form_field`; +CREATE TABLE `%TABLE_PREFIX%form_field` ( + `id` int(11) unsigned NOT NULL auto_increment, + `section_id` int(11) unsigned NOT NULL, + `type` varchar(255) NOT NULL DEFAULT 'text', + `label` varchar(255) NOT NULL, + `required` tinyint(1) NOT NULL DEFAULT 0, + `private` tinyint(1) NOT NULL DEFAULT 0, + `name` varchar(64) NOT NULL, + `configuration` text, + `sort` int(11) unsigned NOT NULL, + `hint` varchar(512), + `created` datetime NOT NULL, + `updated` datetime NOT NULL, + PRIMARY KEY (`id`) +) DEFAULT CHARSET=utf8; + +DROP TABLE IF EXISTS `%TABLE_PREFIX%form_entry`; +CREATE TABLE `%TABLE_PREFIX%form_entry` ( + `id` int(11) unsigned NOT NULL auto_increment, + `section_id` int(11) unsigned NOT NULL, + `ticket_id` int(11) unsigned, + `sort` int(11) unsigned NOT NULL DEFAULT 1, + `created` datetime NOT NULL, + `updated` datetime NOT NULL, + PRIMARY KEY (`id`), + KEY `ticket_dyn_form_lookup` (`ticket_id`) +) DEFAULT CHARSET=utf8; + +DROP TABLE IF EXISTS `%TABLE_PREFIX%form_entry_values`; +CREATE TABLE `%TABLE_PREFIX%form_entry_values` ( + -- references form_entry.id + `entry_id` int(11) unsigned NOT NULL, + `field_id` int(11) unsigned NOT NULL, + `value` text, + PRIMARY KEY (`entry_id`, `field_id`) +) DEFAULT CHARSET=utf8; + +DROP TABLE IF EXISTS `%TABLE_PREFIX%list`; +CREATE TABLE `%TABLE_PREFIX%list` ( + `id` int(11) unsigned NOT NULL auto_increment, + `name` varchar(255) NOT NULL, + `name_plural` varchar(255), + `sort_mode` enum('Alpha', '-Alpha', 'SortCol') NOT NULL DEFAULT 'Alpha', + `notes` text, + `created` datetime NOT NULL, + `updated` datetime NOT NULL, + PRIMARY KEY (`id`) +) DEFAULT CHARSET=utf8; + +DROP TABLE IF EXISTS `%TABLE_PREFIX%list_items`; +CREATE TABLE `%TABLE_PREFIX%list_items` ( + `id` int(11) unsigned NOT NULL auto_increment, + `list_id` int(11), + `value` varchar(255) NOT NULL, + -- extra value such as abbreviation + `extra` varchar(255), + `sort` int(11) NOT NULL DEFAULT 1, + PRIMARY KEY (`id`), + KEY `list_item_lookup` (`list_id`) +) DEFAULT CHARSET=utf8; + + DROP TABLE IF EXISTS `%TABLE_PREFIX%department`; CREATE TABLE `%TABLE_PREFIX%department` ( `dept_id` int(11) unsigned NOT NULL auto_increment, @@ -258,6 +355,7 @@ CREATE TABLE `%TABLE_PREFIX%filter` ( `staff_id` int(10) unsigned NOT NULL default '0', `team_id` int(10) unsigned NOT NULL default '0', `sla_id` int(10) unsigned NOT NULL default '0', + `formset_id` int(11) unsigned NOT NULL default '0', `target` ENUM( 'Any', 'Web', 'Email', 'API' ) NOT NULL DEFAULT 'Any', `name` varchar(32) NOT NULL default '', `notes` text, @@ -376,6 +474,7 @@ CREATE TABLE `%TABLE_PREFIX%help_topic` ( `team_id` int(10) unsigned NOT NULL default '0', `sla_id` int(10) unsigned NOT NULL default '0', `page_id` int(10) unsigned NOT NULL default '0', + `formset_id` int(10) unsigned NOT NULL default '0', `topic` varchar(32) NOT NULL default '', `notes` text, `created` datetime NOT NULL, @@ -508,11 +607,6 @@ CREATE TABLE `%TABLE_PREFIX%ticket` ( `topic_id` int(10) unsigned NOT NULL default '0', `staff_id` int(10) unsigned NOT NULL default '0', `team_id` int(10) unsigned NOT NULL default '0', - `email` varchar(255) NOT NULL default '', - `name` varchar(255) NOT NULL default '', - `subject` varchar(255) NOT NULL default '[no subject]', - `phone` varchar(16) default NULL, - `phone_ext` varchar(8) default NULL, `ip_address` varchar(64) NOT NULL default '', `status` enum('open','closed') NOT NULL default 'open', `source` enum('Web','Email','Phone','API','Other') NOT NULL default 'Other', @@ -526,7 +620,6 @@ CREATE TABLE `%TABLE_PREFIX%ticket` ( `created` datetime NOT NULL, `updated` datetime NOT NULL, PRIMARY KEY (`ticket_id`), - UNIQUE KEY `email_extid` (`ticketID`,`email`), KEY `dept_id` (`dept_id`), KEY `staff_id` (`staff_id`), KEY `team_id` (`staff_id`), -- GitLab