diff --git a/ajax.php b/ajax.php index 0786b41a43691fa1667212753c01d48d2fde05ad..57b6274ccee12f4292f97aa7df5b4f9c9cd938ce 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 b2540b15936cfc82778b024fb9a0b42a1d7ad31b..671f569278863d360e0d4bb97a323d47503e448d 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 0000000000000000000000000000000000000000..08b0b396c4b0aaa8460105b62053b94a9648a83a --- /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 34c46325ed84d304c7a8c031bb8a5b686ea337a1..a981e5ae43605e459ddf5a03459216e8f061a9e0 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 f49680ce7f60c62b9e61cfe5d6fdbd8e21f7ab44..a442d5b7f6fe4a3685271057ef011d919c3f98d3 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 019d3fba39e46286e98cbe6664e229645e88adfc..418b8da76824c3574b89a508bd298bcccfa1476f 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 5ff5911c26297cea963bf37b50e66ee955ba9b1f..8cf29e31d031a0e327eec982c5b5dda64a9da21a 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 0000000000000000000000000000000000000000..c197127ce866a4d2cd32477b1e39293ec5152008 --- /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 0000000000000000000000000000000000000000..d0101784328340b7dd9e8ec06d110f9f57e08eec --- /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 0f8bc844301d1dcc42db87f1e92b98d51e89a622..b7e8e95d8f3e6b91df36782296fd19ffb93f80ce 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 b5a589cfac122e008acc207f3bd2942931f31fe7..f3dfd282cdf565522c45b4f7f173da45e7e40bfa 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 a92a5629ec149579cd3d7883be2aa8ff2ef661fc..04080f25573f66b9bca5b973b94fe8883a428e5b 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 0000000000000000000000000000000000000000..c782de0b32bc9fdc4248869373a35321dedb702f --- /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 ea658378685c04f8345a5e3e73d7042499b1eeb1..b20eaf7ac53df9290a0532fafcb7ac62d466e491 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 2ab7ba1e683c0bfe84c8185b6626ad205a9fda99..2f22a11691851c30f5e49147d9632233cc6eb57f 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 801e6e684a0b9cc08f480d7ee80ecf7abaf0b66d..0bbcdc8b308fdb64b5fbc0151af87ef6a39de630 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 2849ccbe3221aafd203cc8dcaf2bf6c28d39fd51..e03f36cbb1aa29b762fc8f4e5eaadba7a51aa489 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 a0a97b7a3aa711a7446236f249cff0cb084fec8b..f39a0ded119c0fbc4d3407b22ed1e340e333b154 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 0000000000000000000000000000000000000000..49ff0169731bbb1d1c4e15f512dd698bcb04e1ef --- /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 8eda7eca323e9cf5a9cc8b142575acd9a72d59de..81284b2718f05589976e3c484dac1596c42a55a6 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 f85cff8daeeb59620f5d188978b7aa2ba1a0ba51..0ec5b9941224397b5f5e99239454bc05b35be33e 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 0000000000000000000000000000000000000000..451889b14422c9ca4edb9682b18449f2d0048dd3 --- /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 11d7e819ccdaf3b5469e622bc4a974536130192d..2c99305c05216d8e33d38159663ef464a0ee69f1 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 ad545a5dbfd8a9fae8206c0e6e14189809e1105b..13ce86ec5ffa293cf91a23de4bfe2dc8e5fe4044 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 0000000000000000000000000000000000000000..800f1b97bcd633fb17cd4a1e60d54f8223a7a853 --- /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 0000000000000000000000000000000000000000..ed715668a958304e5265b034ef6f3fe34a553880 --- /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 0000000000000000000000000000000000000000..c2753063d2b1dbe6b334c30d3d69cfff05677bf2 --- /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 0000000000000000000000000000000000000000..fdb93ca79a095f4f4187d2977e3cd4a826927039 --- /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 0000000000000000000000000000000000000000..02d5e9314f8dbcbd504af686cacf74bfc63c8546 --- /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 0000000000000000000000000000000000000000..0427666d66f22357c1bdc67b881a0fb4c7b29e0a --- /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 e5b04bff53e4fec7dd0880423aee27b43704e41f..d4e2ce612239c405dc5335650a2c0cf869ff6996 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 fdf82a404c187cf9d8f0ba6421b635f70a7e3f4a..07bc83a700c7fdb55152ad584163fb2ca38048eb 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 0000000000000000000000000000000000000000..3751670f4c7c0a78b1a0e24575aadc4b20f6da64 --- /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 0000000000000000000000000000000000000000..93bb577ea4dfe458c3180ebb4ec67973cc9dcde1 --- /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 4a641d782b41005e0e17b4197b1bff668523d8d7..fca5a366c9c38f76d3449ef95d5672bc12e7e5a8 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 cf9c49dfbec5be1211cb9b74696ba9595e223b79..ab0eba20e9b1ea9f4066bf64f5f9fc63d879dfa6 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 c8dcde436704a486d0c42ee112adb1b88b1e6483..5b4359c78ec0a17a3fb25eb4b35288dc40c701d7 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 ac38d5ff8c87eddf718c1a547315f292fed5ccb7..732c5c277602f44980396d0c293c6a653b058a3f 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 8ced61f0cdd122a0aaeead6269f21f5ee31c8d88..a54c1899f357f51302848268ea0db4aee340dc0e 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 0000000000000000000000000000000000000000..940b9a12b3d260e5ab8c5925f11254be49484726 --- /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 5dd4109d3f6f4b940e95243cfba7901958f877fd..286f3ef11f8038848fa88f38da74a2e9dadbdca4 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 db51e85d395fab9225dbb4ca0efb88df7f7139cb..7d7742cdfd326bf547a2d604a1b98fa90919f6fc 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 3eeca0f0062e868f67aa660739943b356edd43fe..7fae11f1f622b0442bbab3ce3310d7a4cad03750 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 648d05c1348e68f8ea600ef8338caaf7d5dea101..4fa5ba620bf69d2b35ba45820db1bb9344c41231 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 0000000000000000000000000000000000000000..eb1881847bb516961c958b7f1204e9eccacde023 --- /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 0000000000000000000000000000000000000000..13de499b0ab97fb40cdd4d9c16bd58c8c27b4410 --- /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 0000000000000000000000000000000000000000..ec8b77fe1e75926ec540c4e6ca4a74fcfae7337f --- /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 7cb150741e307951f5d80d1a4ba5d5b30ffeee54..9f34fab9e848a01f36928a266cc6b69d2cb09d09 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 d7db69d13008ea9d98e15b757f0a1b217f735493..d4e90e1b80a9cae6d6479d1d634c080085862c36 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 45d0cd44a9634e4662ab249dba0ae2a73e7b29cb..16d84ee6c3efa0bf8c73f199328ab6324f5e3a60 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 78fd35849c546468251c9151152c4efa73816435..6ec2fa7170c8534fc55f47cb44546bb5f4a52adb 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 0000000000000000000000000000000000000000..4d613ca612d21adfce466c604b7abd9b35ca5701 --- /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 4891bcb0f9023b7cf97a1facd84b7bd19e54cc7c..d37ff15db887044a94bb89b1bfa2cec6e9a359b5 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`),