diff --git a/.gitignore b/.gitignore index 3f02437e02e80eb03aa1bdc0a0c90ad51a2a112e..a36f4e07b61ea6bddad624fa51870f5551c69f6e 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,4 @@ stage # Ignore mPDF temp files include/mpdf/ttfontdata include/mpdf/tmp +nbproject/ diff --git a/WHATSNEW.md b/WHATSNEW.md index 4523347765df7bf305e94ea25973092fae0b4518..61c3aaaaf3b589f35444e7126f6dd3a4768eb772 100644 --- a/WHATSNEW.md +++ b/WHATSNEW.md @@ -1,3 +1,39 @@ +osTicket v1.9.6 +=============== +### Enhancements + * New Message-Id system allowing for better threading in mail clients (#1549, + #1730) + * Fix forced session expiration after 24 hours (#1677) + * Staff panel logo is customizable (#1718) + * Priority fields have a selectable default (instead of system default) (#1732) + * Import/Export support for file contents via cli (#1661) + +### Improvements + * Fix broken links in documentation, thanks @Chefkeks (#1675) + * Fix handling of some Redmond-specific character set encoding names (#1698) + * Include the users name in the "To" field of outbound email (#1549) + * Delete collaborators when deleting tickets (#1709) + * Fix regression preventing auto-responses for staff new tickets (#1712) + * Fix empty export if ticket details form has multiple priority fields (#1732) + * Fix filtering by list item properties in ticket filters (#1741) + * Fix missing icon for "add new filter", thanks @Chefkeks (#1735) + * Support Firefox v6 - v12 on the file drop widget (#1776) + * Show update errors on access templates (#1778) + * Allow empty staff login banner on update (#1778) + * Fix corruption of text thread bodies for third-party collaborator email + posts (#1794) + * Add some hidden template variables to pop out content (#1781) + * Fix missing validation for user name and email address (#1816, eb8858e) + * Turn off search indexing when complete, disable incorrectly implemented + work breaking, squelch error 1062 email from search backend (afa9692) + * Fix possible out of memory crash in custom forms (#1707, 0440111) + +### Performance and Security + * Fix generation of random data on Windows® platforms (#1672) + * Fix possible DoS and brute force on login pages (#1727) + * Fix possible redirect away from HTTPS on client login page, thanks @ldrumm + (#1782) + osTicket v1.9.5.1 ================= ### Improvements diff --git a/assets/default/images/poweredby.png b/assets/default/images/poweredby.png index 9b2915505a144a89e12338f081b30dcd032b4e40..8f3d4821e88a50a6b3137a5fe55bd4ccfd524869 100644 Binary files a/assets/default/images/poweredby.png and b/assets/default/images/poweredby.png differ diff --git a/include/ajax.config.php b/include/ajax.config.php index c625d75502b7d734ffe4a01a0adcef6e8af5832c..c263bd4a253993240b27e4557060b1fadc689ae1 100644 --- a/include/ajax.config.php +++ b/include/ajax.config.php @@ -21,7 +21,7 @@ class ConfigAjaxAPI extends AjaxController { //config info UI might need. function scp() { - global $cfg; + global $cfg, $thisstaff; $lang = Internationalization::getCurrentLanguage(); $info = Internationalization::getLanguageInfo($lang); @@ -48,6 +48,7 @@ class ConfigAjaxAPI extends AjaxController { 'primary_lang_flag' => strtolower($primary_info['flag'] ?: $primary_locale ?: $primary_sl), 'primary_language' => $primary, 'secondary_languages' => $cfg->getSecondaryLanguages(), + 'page_size' => $thisstaff->getPageLimit(), ); return $this->json_encode($config); } diff --git a/include/ajax.content.php b/include/ajax.content.php index 8cd1d5bfd9958b9304c0fde5e5b3a60a1fef3fc4..9ea0d7d1431bdf58aac6b2ffada47acb7e4585bd 100644 --- a/include/ajax.content.php +++ b/include/ajax.content.php @@ -64,14 +64,15 @@ class ContentAjaxAPI extends AjaxController { <tr><td>%{ticket.create_date}</td><td>'.__('Date created').'</td></tr> <tr><td>%{ticket.due_date}</td><td>'.__('Due date').'</td></tr> <tr><td>%{ticket.close_date}</td><td>'.__('Date closed').'</td></tr> - <tr><td>%{recipient.ticket_link}</td><td>'.__('Auth. token used for auto-login').'</td></tr> - <tr><td>%{ticket.client_link}</td><td>'.__('Client\'s ticket view link').'</td></tr> - <tr><td>%{recipient.ticket_link}</td><td>'.__('Agent\'s ticket view link').'</td></tr> - <tr><td colspan="2" style="padding:5px 0 5px 0;"><em>'.__('Expandable Variables (See Wiki)').'</em></td></tr> - <tr><td>%{ticket.<b>topic</b>}</td><td>'.__('Help topic').'</td></tr> - <tr><td>%{ticket.<b>dept</b>}</td><td>'.__('Department').'</td></tr> - <tr><td>%{ticket.<b>staff</b>}</td><td>'.__('Assigned/closing agent').'</td></tr> - <tr><td>%{ticket.<b>team</b>}</td><td>'.__('Assigned/closing team').'</td></tr> + <tr><td>%{ticket.recipients}</td><td>'.__('List of all recipient names').'</td></tr> + <tr><td nowrap>%{recipient.ticket_link}</td><td>'.__('Auth. token used for auto-login').'<br/> + '.__('Agent\'s ticket view link').'</td></tr> + <tr><td colspan="2" style="padding:5px 0 5px 0;"><em><b>'.__('Expandable Variables').'</b></em></td></tr> + <tr><td>%{ticket.topic}</td><td>'.__('Help topic').'</td></tr> + <tr><td>%{ticket.dept}</td><td>'.__('Department').'</td></tr> + <tr><td>%{ticket.staff}</td><td>'.__('Assigned/closing agent').'</td></tr> + <tr><td>%{ticket.team}</td><td>'.__('Assigned/closing team').'</td></tr> + <tr><td>%{ticket.thread}</td><td>'.__('Ticket Thread').'</td></tr> </table> </td> <td valign="top"> @@ -89,14 +90,17 @@ class ContentAjaxAPI extends AjaxController { <table width="100%" border="0" cellspacing=1 cellpadding=1> <tr><td colspan="2"><b>'.__('Name Expansion').'</b></td></tr> <tr><td>.first</td><td>'.__('First Name').'</td></tr> - <tr><td>.middle</td><td>'.__('Middle Name(s)').'</td></tr> <tr><td>.last</td><td>'.__('Last Name').'</td></tr> <tr><td>.full</td><td>'.__('First Last').'</td></tr> - <tr><td>.legal</td><td>'.__('First M. Last').'</td></tr> <tr><td>.short</td><td>'.__('First L.').'</td></tr> - <tr><td>.formal</td><td>'.__('Mr. Last').'</td></tr> <tr><td>.shortformal</td><td>'.__('F. Last').'</td></tr> <tr><td>.lastfirst</td><td>'.__('Last, First').'</td></tr> + <tr><td colspan="2" style="padding:5px 0 5px 0;"><em><b>'.__('Ticket Thread expansions').'</b></em></td></tr> + <tr><td>.original</td><td>'.__('Original Message').'</td></tr> + <tr><td>.lastmessage</td><td>'.__('Last Message').'</td></tr> + <tr><td colspan="2" style="padding:5px 0 5px 0;"><em><b>'.__('Thread Entry expansions').'</b></em></td></tr> + <tr><td>.poster</td><td>'.__('Poster').'</td></tr> + <tr><td>.create_date</td><td>'.__('Date created').'</td></tr> </table> </td> </tr> @@ -139,12 +143,17 @@ class ContentAjaxAPI extends AjaxController { $langs = Internationalization::getConfiguredSystemLanguages(); $translations = $content->getAllTranslations(); - $info = array(); + $info = array( + 'title' => $content->getTitle(), + 'body' => $content->getBody(), + ); foreach ($translations as $t) { if (!($data = $t->getComplex())) continue; - $info['title'][$t->lang] = $data['name']; - $info['body'][$t->lang] = $data['body']; + $info['trans'][$t->lang] = array( + 'title' => $data['name'], + 'body' => $data['body'], + ); } include STAFFINC_DIR . 'templates/content-manage.tmpl.php'; @@ -159,6 +168,8 @@ class ContentAjaxAPI extends AjaxController { $langs = $cfg->getSecondaryLanguages(); $content = Page::lookupByType($type, $lang); + $info = $content->getHashtable(); + include STAFFINC_DIR . 'templates/content-manage.tmpl.php'; } @@ -167,19 +178,26 @@ class ContentAjaxAPI extends AjaxController { if (!$thisstaff) Http::response(403, 'Login Required'); - elseif (!$_POST['name'] || !$_POST['body']) - Http::response(422, 'Please submit name and body'); elseif (!($content = Page::lookup($id))) Http::response(404, 'No such content'); + if (!isset($_POST['body'])) + $_POST['body'] = ''; + $vars = array_merge($content->getHashtable(), $_POST); $errors = array(); - if (!$content->update($vars, $errors)) { - if ($errors['err']) - Http::response(422, $errors['err']); - else - Http::response(500, 'Unable to update content: '.print_r($errors, true)); + + // Allow empty content for the staff banner + if ($content->update($vars, $errors, + $content->getType() == 'banner-staff') + ) { + Http::response(201, 'Have a great day!'); } + if (!$errors['err']) + $errors['err'] = __('Correct the error(s) below and try again!'); + $info = $_POST; + $errors = Format::htmlchars($errors); + include STAFFINC_DIR . 'templates/content-manage.tmpl.php'; } } ?> diff --git a/include/ajax.sequence.php b/include/ajax.sequence.php index 37be03269c87483a77e8d99cc7aaf840ffaeae34..299e8c3223d7a1d8262cc40c8919ac44fbde534f 100644 --- a/include/ajax.sequence.php +++ b/include/ajax.sequence.php @@ -33,7 +33,7 @@ class SequenceAjaxAPI extends AjaxController { elseif (!($sequence = Sequence::lookup($id))) Http::response(404, 'No such object'); - return $sequence->current($_GET['format']); + return $sequence->current(Format::htmlchars($_GET['format'])); } /** diff --git a/include/ajax.tickets.php b/include/ajax.tickets.php index 9e416bef6a3961dcc549ff8381ed6bc93c2003ce..28ed7d2b4fbfcd6de7af37038c6321d02dc3e628 100644 --- a/include/ajax.tickets.php +++ b/include/ajax.tickets.php @@ -413,7 +413,7 @@ class TicketsAjaxAPI extends AjaxController { Http::response(404, 'No such ticket/user'); $errors = array(); - if($user->updateInfo($_POST, $errors)) + if($user->updateInfo($_POST, $errors, true)) Http::response(201, $user->to_json()); $forms = $user->getForms(); diff --git a/include/class.config.php b/include/class.config.php index 7ddc58855f0b68cb5f56f98ad893c43222e79e85..b7eae5da0e851d57f8766f1dc3ceaf44451b68a2 100644 --- a/include/class.config.php +++ b/include/class.config.php @@ -202,6 +202,13 @@ class OsticketConfig extends Config { } function isKnowledgebaseEnabled() { + global $thisclient; + + if ($this->get('restrict_kb', false) + && (!$thisclient || $thisclient->isGuest()) + ) { + return false; + } require_once(INCLUDE_DIR.'class.faq.php'); return ($this->get('enable_kb') && FAQ::countPublishedFAQs()); } @@ -1168,11 +1175,16 @@ class OsticketConfig extends Config { function updateKBSettings($vars, &$errors) { - if($errors) return false; + if ($vars['restrict_kb'] && !$this->isClientRegistrationEnabled()) + $errors['restrict_kb'] = + __('The knowledge base cannot be restricted unless client registration is enabled'); + + if ($errors) return false; return $this->updateAll(array( 'enable_kb'=>isset($vars['enable_kb'])?1:0, - 'enable_premade'=>isset($vars['enable_premade'])?1:0, + 'restrict_kb'=>isset($vars['restrict_kb'])?1:0, + 'enable_premade'=>isset($vars['enable_premade'])?1:0, )); } diff --git a/include/class.dynamic_forms.php b/include/class.dynamic_forms.php index 3c8289899b0b036a2dbb4cccf1c9354564d07cce..2efcbec3c2fca60c77f19d49c60a30c7557ee415 100644 --- a/include/class.dynamic_forms.php +++ b/include/class.dynamic_forms.php @@ -52,18 +52,17 @@ class DynamicForm extends VerySimpleModel { var $_dfields; function getFields($cache=true) { - if (!$cache) - $fields = false; - else - $fields = &$this->_fields; + if (!$cache) { + $this->_fields = null; + } - if (!$fields) { - $fields = new ListObject(); + if (!$this->_fields) { + $this->_fields = new ListObject(); foreach ($this->getDynamicFields() as $f) - $fields->append($f->getImpl($f)); + $this->_fields->append($f->getImpl($f)); } - return $fields; + return $this->_fields; } function getDynamicFields() { @@ -104,13 +103,39 @@ class DynamicForm extends VerySimpleModel { function getTitle() { return $this->getLocal('title'); } function getInstructions() { return $this->getLocal('instructions'); } + /** + * Drop field errors clean info etc. Useful when replacing the source + * content of the form. This is necessary because the field listing is + * cached under some circumstances. + */ + function reset() { + foreach ($this->getFields() as $f) + $f->reset(); + return $this; + } + function getForm($source=false) { - if (!$this->_form || $source) { - $fields = $this->getFields($this->_has_data); - $this->_form = new Form($fields, $source, array( - 'title'=>$this->getLocal('title'), 'instructions'=>$this->getLocal('instructions'))); + if ($source) + $this->reset(); + $fields = $this->getFields(); + $form = new Form($fields, $source, array( + 'title'=>$this->getLocal('title'), 'instructions'=>$this->getLocal('instructions'))); + return $form; + } + + function addErrors(array $formErrors, $replace=false) { + $fields = array(); + foreach ($this->getFields() as $f) + $fields[$f->get('id')] = $f; + foreach ($formErrors as $id => $fieldErrors) { + if (isset($fields[$id])) { + if ($replace) + $fields[$id]->_errors = $fieldErrors; + else + foreach ($fieldErrors as $E) + $fields[$id]->addError($E); + } } - return $this->_form; } function isDeletable() { @@ -414,8 +439,7 @@ class TicketForm extends DynamicForm { return; $f = $answer->getField(); - $name = $f->get('name') ? $f->get('name') - : 'field_'.$f->get('id'); + $name = $f->get('name') ?: ('field_'.$f->get('id')); $fields = sprintf('`%s`=', $name) . db_input( implode(',', $answer->getSearchKeys())); $sql = 'INSERT INTO `'.TABLE_PREFIX.'ticket__cdata` SET '.$fields @@ -867,26 +891,40 @@ class DynamicFormEntry extends VerySimpleModel { function getInstructions() { return $this->getForm()->getInstructions(); } function getForm() { - if (!isset($this->_form)) { - $this->_form = DynamicForm::lookup($this->get('form_id')); - if ($this->_form && isset($this->id)) - $this->_form->data($this); + $form = DynamicForm::lookup($this->get('form_id')); + if ($form) { + if (isset($this->id)) + $form->data($this); if (isset($this->extra)) { $x = JsonDataParser::decode($this->extra) ?: array(); - $this->_form->disableFields($x['disable'] ?: array()); + $form->disableFields($x['disable'] ?: array()); } + if ($this->errors()) + $form->addErrors($this->errors(), true); } - return $this->_form; + return $form; } function getFields() { if (!isset($this->_fields)) { $this->_fields = array(); + // Get all dynamic fields associated with the form + // even when stored elsewhere -- important during validation + foreach ($this->getForm()->getDynamicFields() as $field) { + $field = $field->getImpl($field); + if ($field instanceof ThreadEntryField) + continue; + $this->_fields[$field->get('id')] = $field; + } + // Get answers to entries foreach ($this->getAnswers() as $a) { - $T = $this->_fields[] = $a->getField(); - $T->setForm($this); + if (!($f = $a->getField())) continue; + $this->_fields[$f->get('id')] = $f; } } + foreach ($this->_fields as $F) + $F->setForm($this); + return $this->_fields; } @@ -1057,7 +1095,7 @@ class DynamicFormEntry extends VerySimpleModel { $a->deleted = false; // Add to list of answers $this->_values[] = $a; - $this->_fields[] = $fImpl; + $this->_fields[$field->get('id')] = $fImpl; $this->_form = null; // Omit fields without data and non-storable fields. @@ -1066,21 +1104,25 @@ class DynamicFormEntry extends VerySimpleModel { $a->save(); } - // Sort the form the way it is declared to be sorted - if ($this->_fields) - usort($this->_fields, - function($a, $b) { - return $a->get('sort') - $b->get('sort'); - }); + } + + // Sort the form the way it is declared to be sorted + if ($this->_fields) { + uasort($this->_fields, + function($a, $b) { + return $a->get('sort') - $b->get('sort'); + }); } } - function save() { + function save($refetch=false) { if (count($this->dirty)) $this->set('updated', new SqlFunction('NOW')); - parent::save(); + if (!parent::save($refetch || count($this->dirty))) + return false; + foreach ($this->getFields() as $field) { - if (!$field->isStorable()) + if (!($a = $field->getAnswer()) || !$field->isStorable()) continue; $a = $field->getAnswer(); diff --git a/include/class.export.php b/include/class.export.php index 288cb68480c0d850f57e91cb81938eae9fc4aa88..3210d362553eff74b002806ad6436aa39f46846a 100644 --- a/include/class.export.php +++ b/include/class.export.php @@ -290,6 +290,7 @@ class CsvResultsExporter extends ResultSetExporter { if (!$this->output) $this->output = fopen('php://output', 'w'); + fputs($this->output, chr(0xEF) . chr(0xBB) . chr(0xBF)); fputcsv($this->output, $this->getHeaders()); while ($row=$this->next()) fputcsv($this->output, $row); diff --git a/include/class.filter.php b/include/class.filter.php index 9647481cc3cbb9e4e07c9204bc728e848351be9c..ada35a25994684acda91afb84421b7090adc1206 100644 --- a/include/class.filter.php +++ b/include/class.filter.php @@ -438,7 +438,7 @@ class Filter { else //for everything-else...we assume it's valid. $rules[]=array('what'=>$vars["rule_w$i"], - 'how'=>$vars["rule_h$i"],'val'=>$vars["rule_v$i"]); + 'how'=>$vars["rule_h$i"],'val'=>trim($vars["rule_v$i"])); }elseif($vars["rule_v$i"]) { $errors["rule_$i"]=__('Incomplete selection'); } @@ -919,7 +919,18 @@ class RejectedException extends Exception { } } -class FilterDataChanged extends Exception {} +class FilterDataChanged extends Exception { + var $data; + + function __construct($data) { + parent::__construct('Ticket filter data changed'); + $this->data = $data; + } + + function getData() { + return $this->data; + } +} /** * Function: endsWith diff --git a/include/class.filter_action.php b/include/class.filter_action.php index f2367bc77b1f63e5a1d5eeb11a6d7b35af3b2013..531b997e3008354a0a1bc84f25d62121f1bdf50c 100644 --- a/include/class.filter_action.php +++ b/include/class.filter_action.php @@ -189,11 +189,15 @@ class FA_UseReplyTo extends TriggerAction { function apply(&$ticket, array $info) { $config = $this->getConfiguration(); - if ($config['enable'] && $info['reply-to']) { + $changed = $info['reply-to'] != $ticket['email'] + || ($info['reply-to-name'] && $ticket['name'] != $info['reply-to-name']); + if ($info['reply-to']) { $ticket['email'] = $info['reply-to']; if ($info['reply-to-name']) $ticket['name'] = $info['reply-to-name']; } + if ($changed) + throw new FilterDataChanged($ticket); } function getConfigurationOptions() { diff --git a/include/class.format.php b/include/class.format.php index 7b00d4adb277432b0c1a0801dbba691beb9a2696..61613331e0346da6a16f07f44f6c648e986bce92 100644 --- a/include/class.format.php +++ b/include/class.format.php @@ -690,7 +690,7 @@ class Format { // Drop leading and trailing whitespace $text = trim($text); - if (class_exists('IntlBreakIterator')) { + if (false && class_exists('IntlBreakIterator')) { // Split by word boundaries if ($tokenizer = IntlBreakIterator::createWordInstance( $lang ?: ($cfg ? $cfg->getPrimaryLanguage() : 'en_US')) diff --git a/include/class.forms.php b/include/class.forms.php index 0f89971ec862f548d026beff74155422e94e93bb..d65136a569aac97045ba5e578da18cc9ee883b1e 100644 --- a/include/class.forms.php +++ b/include/class.forms.php @@ -32,7 +32,7 @@ class Form { $this->fields = $fields; foreach ($fields as $k=>$f) { $f->setForm($this); - if (!$f->get('name') && $k) + if (!$f->get('name') && $k && !is_numeric($k)) $f->set('name', $k); } if (isset($options['title'])) @@ -761,6 +761,17 @@ class FormField { return null; } + /** + * Indicates if the field provides for searching for something other + * than keywords. For instance, textbox fields can have hits by keyword + * searches alone, but selection fields should provide the option to + * match a specific value or set of values and therefore need to + * participate on any search builder. + */ + function hasSpecialSearch() { + return true; + } + function getConfigurationForm($source=null) { if (!$this->_cform) { $type = static::getFieldType($this->get('type')); @@ -869,6 +880,10 @@ class TextboxField extends FormField { ); } + function hasSpecialSearch() { + return false; + } + function validateEntry($value) { parent::validateEntry($value); $config = $this->getConfiguration(); @@ -940,6 +955,10 @@ class TextareaField extends FormField { ); } + function hasSpecialSearch() { + return false; + } + function display($value) { $config = $this->getConfiguration(); if ($config['html']) @@ -992,6 +1011,10 @@ class PhoneField extends FormField { ); } + function hasSpecialSearch() { + return false; + } + function validateEntry($value) { parent::validateEntry($value); $config = $this->getConfiguration(); @@ -1137,7 +1160,7 @@ class ChoiceField extends FormField { $config = $this->getConfiguration(); if (!$config['multiselect'] && is_array($value) && count($value) < 2) { reset($value); - return key($value); + $value = key($value); } return $value; } @@ -1424,6 +1447,9 @@ class ThreadEntryField extends FormField { function isPresentationOnly() { return true; } + function hasSpecialSearch() { + return false; + } function getConfigurationOptions() { global $cfg; @@ -1461,8 +1487,8 @@ class ThreadEntryField extends FormField { } class PriorityField extends ChoiceField { - function getWidget() { - $widget = parent::getWidget(); + function getWidget($widgetClass=false) { + $widget = parent::getWidget($widgetClass); if ($widget->value instanceof Priority) $widget->value = $widget->value->getId(); return $widget; @@ -1472,13 +1498,10 @@ class PriorityField extends ChoiceField { return true; } - function getChoices() { - global $cfg; - $this->ht['default'] = $cfg->getDefaultPriorityId(); - + function getChoices($verbose=false) { $sql = 'SELECT priority_id, priority_desc FROM '.PRIORITY_TABLE .' ORDER BY priority_urgency DESC'; - $choices = array(); + $choices = array('' => '— '.__('Default').' —'); if (!($res = db_query($sql))) return $choices; @@ -1496,7 +1519,10 @@ class PriorityField extends ChoiceField { reset($id); $id = key($id); } - return Priority::lookup($id); + elseif ($id === false) + $id = $value; + if ($id) + return Priority::lookup($id); } function to_database($prio) { @@ -1515,14 +1541,31 @@ class PriorityField extends ChoiceField { } function getConfigurationOptions() { + $choices = $this->getChoices(); + $choices[''] = __('System Default'); return array( 'prompt' => new TextboxField(array( 'id'=>2, 'label'=>__('Prompt'), 'required'=>false, 'default'=>'', 'hint'=>__('Leading text shown before a value is selected'), 'configuration'=>array('size'=>40, 'length'=>40), )), + 'default' => new ChoiceField(array( + 'id'=>3, 'label'=>__('Default'), 'required'=>false, 'default'=>'', + 'choices' => $choices, + 'hint'=>__('Default selection for this field'), + 'configuration'=>array('size'=>20, 'length'=>40), + )), ); } + + function getConfiguration() { + global $cfg; + + $config = parent::getConfiguration(); + if (!isset($config['default'])) + $config['default'] = $cfg->getDefaultPriorityId(); + return $config; + } } FormField::addFieldTypes(/*@trans*/ 'Dynamic Fields', function() { return array( @@ -1696,7 +1739,7 @@ class TicketStateField extends ChoiceField { return false; } - function getChoices() { + function getChoices($verbose=false) { static $_choices; if (!isset($_choices)) { @@ -1780,7 +1823,7 @@ class TicketFlagField extends ChoiceField { return true; } - function getChoices() { + function getChoices($verbose=false) { $this->ht['default'] = ''; if (!$this->_choices) { @@ -1878,6 +1921,10 @@ class FileUploadField extends FormField { ); } + function hasSpecialSearch() { + return false; + } + /** * Called from the ajax handler for async uploads via web clients. */ @@ -2448,9 +2495,12 @@ class ChoicesWidget extends Widget { } function getValue() { - $value = parent::getValue(); - if (!$value) return null; + if (!($value = parent::getValue())) + return null; + + if ($value && !is_array($value)) + $value = array($value); // Assume multiselect $values = array(); @@ -2461,6 +2511,7 @@ class ChoicesWidget extends Widget { $values[$v] = $choices[$v]; } } + return $values; } @@ -2493,8 +2544,11 @@ class CheckboxWidget extends Widget { function getValue() { $data = $this->field->getSource(); - if (count($data)) + if (count($data)) { + if (!isset($data[$this->name])) + return false; return @in_array($this->field->get('id'), $data[$this->name]); + } return parent::getValue(); } diff --git a/include/class.mailer.php b/include/class.mailer.php index b5d759cb43801c662bac5a96049cf0a402984772..4c822f725f9b80ace27d7a42b67508cbbe57bd52 100644 --- a/include/class.mailer.php +++ b/include/class.mailer.php @@ -349,6 +349,7 @@ class Mailer { } // Make the best effort to add In-Reply-To and References headers + $reply_tag = $mid_token = ''; if (isset($options['thread']) && $options['thread'] instanceof ThreadEntry ) { @@ -367,6 +368,12 @@ class Mailer { 'References' => $parent->getEmailReferences(), ); } + + // Configure the reply tag and embedded message id token + $mid_token = $options['thread']->asMessageId($to); + if ($cfg && $cfg->stripQuotedReply() + && (!isset($options['reply-tag']) || $options['reply-tag'])) + $reply_tag = $cfg->getReplySeparator() . '<br/><br/>'; } // Use Mail_mime default initially @@ -395,13 +402,12 @@ class Mailer { // body $isHtml = true; if (!(isset($options['text']) && $options['text'])) { - $tag = ''; - if ($cfg && $cfg->stripQuotedReply() - && (!isset($options['reply-tag']) || $options['reply-tag'])) - $tag = '<div>'.$cfg->getReplySeparator() . '<br/><br/></div>'; // Embed the data-mid in such a way that it should be included // in a response - $message = "<div data-mid=\"$messageId\">{$tag}{$message}</div>"; + if ($reply_tag || $mid_token) { + $message = "<div style=\"display:none\" + data-mid=\"$mid_token\">$reply_tag</div>$message"; + } $txtbody = rtrim(Format::html2text($message, 90, false)) . ($messageId ? "\nRef-Mid: $messageId\n" : ''); $mime->setTXTBody($txtbody); diff --git a/include/class.nav.php b/include/class.nav.php index 92d08996a2bcd6d2076955021b36fc570c2e7dda..6dee08171b05fbf4c7fb7c6eb7658980b89c641a 100644 --- a/include/class.nav.php +++ b/include/class.nav.php @@ -116,7 +116,7 @@ class StaffNav { if(!$this->tabs) { $this->tabs = array(); $this->tabs['dashboard'] = array( - 'desc'=>__('Dashboard'),'href'=>'dashboard.php','title'=>__('Agent Dashboard') + 'desc'=>__('Dashboard'),'href'=>'dashboard.php','title'=>__('Agent Dashboard'), "class"=>"no-pjax" ); if ($thisstaff->getRole()->hasPerm(User::PERM_DIRECTORY)) { $this->tabs['users'] = array( diff --git a/include/class.page.php b/include/class.page.php index e55df9d728484beb9fa0661a6e6bce5688e91ab1..1aaa8ce3d8c8e0f934bd95e15f402e58fd74763b 100644 --- a/include/class.page.php +++ b/include/class.page.php @@ -242,7 +242,7 @@ class Page extends VerySimpleModel { } } - function update($vars, &$errors) { + function update($vars, &$errors, $allowempty=false) { //Cleanup. $vars['name']=Format::striptags(trim($vars['name'])); @@ -264,7 +264,7 @@ class Page extends VerySimpleModel { elseif(($pid=self::getIdByName($vars['name'])) && $pid!=$this->getId()) $errors['name'] = __('Name already exists'); - if(!$vars['body']) + if(!$vars['body'] && !$allowempty) $errors['body'] = __('Page body is required'); if($errors) return false; diff --git a/include/class.search.php b/include/class.search.php index 355feb0725ce8b0fea9d9f5e1ba46c269dc312e0..6bbc7a3657a53e61399812d0ff5b182e44c1c30f 100644 --- a/include/class.search.php +++ b/include/class.search.php @@ -223,19 +223,37 @@ class SearchInterface { } } +require_once(INCLUDE_DIR.'class.config.php'); +class MySqlSearchConfig extends Config { + var $table = CONFIG_TABLE; + + function __construct() { + parent::Config("mysqlsearch"); + } +} + class MysqlSearchBackend extends SearchBackend { static $id = 'mysql'; static $BATCH_SIZE = 30; // Only index 20 batches per cron run var $max_batches = 60; + var $_reindexed = 0; function __construct() { $this->SEARCH_TABLE = TABLE_PREFIX . '_search'; } + function getConfig() { + if (!isset($this->config)) + $this->config = new MySqlSearchConfig(); + return $this->config; + } + + function bootstrap() { - Signal::connect('cron', array($this, 'IndexOldStuff')); + if ($this->getConfig()->get('reindex', true)) + Signal::connect('cron', array($this, 'IndexOldStuff')); } function update($model, $id, $content, $new=false, $attrs=array()) { @@ -497,7 +515,10 @@ class MysqlSearchBackend extends SearchBackend { // FILES ------------------------------------ // Flush non-full batch of records - $this->__index(null, true); + if (!$this->_reindexed) { + // Stop rebuilding the index + $this->getConfig()->set('reindex', 0); + } } function __index($record, $force_flush=false) { @@ -517,9 +538,10 @@ class MysqlSearchBackend extends SearchBackend { $sql = 'INSERT INTO `'.TABLE_PREFIX.'_search` (`object_type`, `object_id`, `title`, `content`) VALUES '.implode(',', $queue); - if (!db_query($sql) || count($queue) != db_affected_rows()) + if (!db_query($sql, false) || count($queue) != db_affected_rows()) throw new Exception('Unable to index content'); + $this->_reindexed += count($queue); $queue = array(); if (!--$this->max_batches) diff --git a/include/class.thread.php b/include/class.thread.php index 72f0d9a197b3c49fb23ef60ba1d1f8ae2554a176..b537436d58a2c6bc6fc727f7321f1996e3cd5861 100644 --- a/include/class.thread.php +++ b/include/class.thread.php @@ -234,7 +234,13 @@ class Thread extends VerySimpleModel { //XXX: Are we potentially leaking the email address to // collaborators? // Try not to destroy the format of the body - $body->prepend(sprintf('Received From: %s', $mailinfo['email'])); + $header = sprintf("Received From: %s <%s>\n\n", $mailinfo['name'], + $mailinfo['email']); + if ($body instanceof HtmlThreadBody) + $header = nl2br(Format::htmlchars($header)); + // Add the banner to the top of the message + if ($body instanceof ThreadBody) + $body->prepend($header); $vars['message'] = $body; $vars['userId'] = 0; //Unknown user! //XXX: Assume ticket owner? $vars['origin'] = 'Email'; @@ -510,6 +516,33 @@ class ThreadEntry extends VerySimpleModel { return $references; } + /** + * Retrieve a list of all the recients of this message if the message + * was received via email. + * + * Returns: + * (array<RFC_822>) list of recipients parsed with the Mail/RFC822 + * address parsing utility. Returns an empty array if the message was + * not received via email. + */ + function getAllEmailRecipients() { + $headers = self::getEmailHeaderArray(); + $recipients = array(); + if (!$headers) + return $recipients; + + foreach (array('To', 'Cc') as $H) { + if (!isset($headers[$H])) + continue; + + if (!($all = Mail_Parse::parseAddressList($headers[$H]))) + continue; + + $recipients = array_merge($recipients, $all); + } + return $recipients; + } + function getUIDFromEmailReference($ref) { $info = unpack('Vtid/Vuid', @@ -979,7 +1012,7 @@ class ThreadEntry extends VerySimpleModel { return false; // Compute the value to be compared from $mails (which used to be in - // ThreadEntry::asMessageId() + // ThreadEntry::asMessageId() (#nolint) $domain = md5($ost->getConfig()->getURL()); $ticket = $entry->getThread()->getObject(); if (!$ticket instanceof Ticket) @@ -1280,6 +1313,14 @@ class ThreadEntryBody /* extends SplString */ { return $this->display('html'); } + function prepend($what) { + $this->body = $what . $this->body; + } + + function append($what) { + $this->body .= $what; + } + function asVar() { // Email template, assume HTML return $this->display('email'); diff --git a/include/class.ticket.php b/include/class.ticket.php index c5f364de9b756bbbb9e9db142ae1a4bbc3b3514e..cfc167ae9e9311783649cea00304b9c8d39fa259 100644 --- a/include/class.ticket.php +++ b/include/class.ticket.php @@ -1211,6 +1211,7 @@ implements RestrictedAccess, Threadable { $sql.=', closed=NOW(), lastupdate=NOW(), duedate=NULL '; if ($thisstaff && $set_closing_agent) $sql.=', staff_id='.db_input($thisstaff->getId()); + $this->clearOverdue(); $ecb = function($t) { $t->reload(); @@ -1475,15 +1476,24 @@ implements RestrictedAccess, Threadable { return; //Who posted the entry? - $uid = 0; + $skip = array(); if ($entry instanceof Message) { $poster = $entry->getUser(); // Skip the person who sent in the message - $uid = $entry->getUserId(); + $skip[$entry->getUserId()] = 1; + // Skip all the other recipients of the message + foreach ($entry->getAllEmailRecipients() as $R) { + foreach ($recipients as $R2) { + if ($R2->getEmail() == ($R->mailbox.'@'.$R->hostname)) { + $skip[$R2->getUserId()] = true; + break; + } + } + } } else { $poster = $entry->getStaff(); // Skip the ticket owner - $uid = $this->getUserId(); + $skip[$this->getUserId()] = 1; } $vars = array_merge($vars, array( @@ -1498,7 +1508,10 @@ implements RestrictedAccess, Threadable { $options = array('inreplyto' => $entry->getEmailMessageId(), 'thread' => $entry); foreach ($recipients as $recipient) { - if ($uid == $recipient->getUserId()) continue; + // Skip folks who have already been included on this part of + // the conversation + if (isset($skip[$recipient->getUserId()])) + continue; $notice = $this->replaceVars($msg, array('recipient' => $recipient)); $email->send($recipient, $notice['subj'], $notice['body'], $attachments, $options); @@ -2091,8 +2104,7 @@ implements RestrictedAccess, Threadable { $recipients[]=$this->getLastRespondent(); //Assigned staff if any...could be the last respondent - - if ($this->isAssigned()) { + if ($cfg->alertAssignedONNewMessage() && $this->isAssigned()) { if ($staff = $this->getStaff()) $recipients[] = $staff; elseif ($team = $this->getTeam()) @@ -2196,6 +2208,9 @@ implements RestrictedAccess, Threadable { if(!$vars['staffId'] && $thisstaff) $vars['staffId'] = $thisstaff->getId(); + if (!$vars['ip_address'] && $_SERVER['REMOTE_ADDR']) + $vars['ip_address'] = $_SERVER['REMOTE_ADDR']; + if(!($response = $this->getThread()->addResponse($vars, $errors))) return null; @@ -2319,6 +2334,8 @@ implements RestrictedAccess, Threadable { elseif (!isset($vars['poster'])) { $vars['poster'] = 'SYSTEM'; } + if (!$vars['ip_address'] && $_SERVER['REMOTE_ADDR']) + $vars['ip_address'] = $_SERVER['REMOTE_ADDR']; if(!($note=$this->getThread()->addNote($vars, $errors))) return null; @@ -2450,6 +2467,11 @@ implements RestrictedAccess, Threadable { $this->deleteDrafts(); + $sql = 'DELETE FROM '.TICKET_TABLE.'__cdata WHERE `ticket_id`=' + .db_input($this->getId()); + // If the CDATA table doesn't exist, that's not an error + db_query($sql, false); + // Log delete $log = sprintf(__('Ticket #%1$s deleted by %2$s'), $this->getNumber(), @@ -2778,7 +2800,7 @@ implements RestrictedAccess, Threadable { } catch (FilterDataChanged $ex) { // Don't pass user recursively, assume the user has changed - return self::filterTicketData($origin, $vars, $forms); + return self::filterTicketData($origin, $ex->getData(), $forms); } return $vars; } diff --git a/include/class.user.php b/include/class.user.php index 0ad21e055023ae751ebe044f54cb78068c1d9ad3..194b659a34777039b1484928c745f415310ce1bb 100644 --- a/include/class.user.php +++ b/include/class.user.php @@ -378,7 +378,7 @@ class User extends UserModel { } } - $this->_forms[] = $cd->getForm(); + $this->_forms[] = $cd; } } @@ -541,15 +541,15 @@ class User extends UserModel { function updateInfo($vars, &$errors, $staff=false) { $valid = true; - $forms = $this->getDynamicData(); + $forms = $this->getForms($vars); foreach ($forms as $cd) { $cd->setSource($vars); if ($staff && !$cd->isValidForStaff()) $valid = false; - elseif (!$cd->isValidForClient()) + elseif (!$staff && !$cd->isValidForClient()) $valid = false; - elseif ($cd->get('type') == 'U' - && ($form= $cd->getForm()) + elseif (($form= $cd->getForm()) + && $form->get('type') == 'U' && ($f=$form->getField('email')) && $f->getClean() && ($u=User::lookup(array('emails__address'=>$f->getClean()))) @@ -562,7 +562,7 @@ class User extends UserModel { if (!$valid) return false; - foreach ($this->getDynamicData() as $cd) { + foreach ($forms as $cd) { if (($f=$cd->getForm()) && $f->get('type') == 'U') { if (($name = $f->getField('name'))) { $this->name = $name->getClean(); diff --git a/include/client/login.inc.php b/include/client/login.inc.php index 2b688ee2de4b9f69b545d4ed89dbc57ab26170ba..5c6413f9b212dbc6d199859f3d2cdf70dccb129b 100644 --- a/include/client/login.inc.php +++ b/include/client/login.inc.php @@ -56,7 +56,7 @@ if ($cfg && $cfg->isClientRegistrationEnabled()) { <?php } ?> <div> <b><?php echo __("I'm an agent"); ?></b> — - <a href="<?php echo ROOT_PATH; ?>scp"><?php echo __('sign in here'); ?></a> + <a href="<?php echo ROOT_PATH; ?>scp/"><?php echo __('sign in here'); ?></a> </div> </div> </div> diff --git a/include/i18n/en_US/help/tips/settings.autoresponder.yaml b/include/i18n/en_US/help/tips/settings.autoresponder.yaml index 35faf7ae2f1558dd8b193c207fd56c41b2bf5a8f..c77da28ec34c30ca405a44dd15ce6fc7760f0389 100644 --- a/include/i18n/en_US/help/tips/settings.autoresponder.yaml +++ b/include/i18n/en_US/help/tips/settings.autoresponder.yaml @@ -58,4 +58,4 @@ overlimit_notice: href: /scp/templates.php?default_for=ticket.overlimit - title: Set <em>Maximum Open Tickets</em> - href: /scp/settings?t=tickets + href: /scp/settings.php?t=tickets diff --git a/include/i18n/en_US/help/tips/settings.kb.yaml b/include/i18n/en_US/help/tips/settings.kb.yaml index 12f4954725fd56d5befbda7ab26276487f7d1746..702dac9d573a5bf3094321069db57c20dc511331 100644 --- a/include/i18n/en_US/help/tips/settings.kb.yaml +++ b/include/i18n/en_US/help/tips/settings.kb.yaml @@ -28,6 +28,15 @@ knowledge_base_status: - title: Manage Knowledge Base href: /scp/kb.php +restrict_kb: + title: Resctrict Access to the Knowledge Base + content: > + Enable this setting to prevent unregistered users from accessing + your knowledge base articles on the client interface. + links: + - title: Access Control Settings + href: /scp/settings.php?t=access + canned_responses: title: Canned Responses content: > diff --git a/include/staff/departments.inc.php b/include/staff/departments.inc.php index 443c51c00e525c74bf82569d63d79636a4ac8b75..7941d6fa4631a866d6aafdc3b23f0e557a7b1f96 100644 --- a/include/staff/departments.inc.php +++ b/include/staff/departments.inc.php @@ -56,7 +56,7 @@ $showing = $pageNav->showing().' '._N('department', 'departments', $count); <th width="7px"> </th> <th width="200"><a <?php echo $name_sort; ?> href="departments.php?<?php echo $qstr; ?>&sort=name"><?php echo __('Name');?></a></th> <th width="80"><a <?php echo $type_sort; ?> href="departments.php?<?php echo $qstr; ?>&sort=type"><?php echo __('Type');?></a></th> - <th width="70"><a <?php echo $members_sort; ?>href="departments.php?<?php echo $qstr; ?>&sort=members"><?php echo __('Members');?></a></th> + <th width="70"><a <?php echo $users_sort; ?>href="departments.php?<?php echo $qstr; ?>&sort=users"><?php echo __('Agents');?></a></th> <th width="300"><a <?php echo $email_sort; ?> href="departments.php?<?php echo $qstr; ?>&sort=email"><?php echo __('Email Address');?></a></th> <th width="180"><a <?php echo $manager_sort; ?> href="departments.php?<?php echo $qstr; ?>&sort=manager"><?php echo __('Manager');?></a></th> </tr> diff --git a/include/staff/filters.inc.php b/include/staff/filters.inc.php index ce55919ca6274bd0e039cb641d5da886616731a6..8e1c4946b60d30f41907aebf9ea78fcca6dd32d9 100644 --- a/include/staff/filters.inc.php +++ b/include/staff/filters.inc.php @@ -49,7 +49,7 @@ else <h2><?php echo __('Ticket Filters');?></h2> </div> <div class="pull-right flush-right" style="padding-top:5px;padding-right:5px;"> - <b><a href="filters.php?a=add" class="Icon newEmailFilter"><?php echo __('Add New Filter');?></a></b></div> + <b><a href="filters.php?a=add" class="Icon newTicketFilter"><?php echo __('Add New Filter');?></a></b></div> <div class="clear"></div> <form action="filters.php" method="POST" name="filters"> <?php csrf_token(); ?> diff --git a/include/staff/pwreset.login.php b/include/staff/pwreset.login.php index 29670e28f6a1deed29d69fd37887c9f326dd48f7..54d57b62ca32c2f904c2ab75528d098cb59fcdea 100644 --- a/include/staff/pwreset.login.php +++ b/include/staff/pwreset.login.php @@ -5,7 +5,10 @@ $info = ($_POST)?Format::htmlchars($_POST):array(); ?> <div id="loginBox"> - <h1 id="logo"><a href="index.php">osTicket <?php echo __('Agent Password Reset'); ?></a></h1> + <h1 id="logo"><a href="index.php"> + <span class="valign-helper"></span> + <img src="logo.php?login" alt="osTicket :: <?php echo __('Agent Password Reset');?>" /> + </a></h1> <h3><?php echo Format::htmlchars($msg); ?></h3> <form action="pwreset.php" method="post"> diff --git a/include/staff/pwreset.php b/include/staff/pwreset.php index 22157a36cb563a50d9421ec88eef736dbb14dd76..93f8e9bb1ae7800eb9f17e98420e71a96ac52562 100644 --- a/include/staff/pwreset.php +++ b/include/staff/pwreset.php @@ -5,7 +5,10 @@ $info = ($_POST && $errors)?Format::htmlchars($_POST):array(); ?> <div id="loginBox"> - <h1 id="logo"><a href="index.php">osTicket <?php echo __('Agent Password Reset'); ?></a></h1> + <h1 id="logo"><a href="index.php"> + <span class="valign-helper"></span> + <img src="logo.php?login" alt="osTicket :: <?php echo __('Agent Password Reset');?>" /> + </a></h1> <h3><?php echo Format::htmlchars($msg); ?></h3> <form action="pwreset.php" method="post"> <?php csrf_token(); ?> diff --git a/include/staff/pwreset.sent.php b/include/staff/pwreset.sent.php index 2825c0c584872354e48bd58010fdd770bb268563..b0a46d8d1ff7fa58e756b6b33833960b5992e8d2 100644 --- a/include/staff/pwreset.sent.php +++ b/include/staff/pwreset.sent.php @@ -5,7 +5,10 @@ $info = ($_POST && $errors)?Format::htmlchars($_POST):array(); ?> <div id="loginBox"> - <h1 id="logo"><a href="index.php">osTicket <?php echo __('Agent Password Reset'); ?></a></h1> + <h1 id="logo"><a href="index.php"> + <span class="valign-helper"></span> + <img src="logo.php?login" alt="osTicket :: <?php echo __('Agent Password Reset');?>" /> + </a></h1> <h3><?php echo __('A confirmation email has been sent'); ?></h3> <h3 style="color:black;"><em><?php echo __( 'A password reset email was sent to the email on file for your account. Follow the link in the email to reset your password.' diff --git a/include/staff/settings-access.inc.php b/include/staff/settings-access.inc.php index 5b79ae43f44ec71c41f41dc033c9b6f8af95f6b4..5efac16cf87fe95a258cd6fd3b181d31856507eb 100644 --- a/include/staff/settings-access.inc.php +++ b/include/staff/settings-access.inc.php @@ -172,9 +172,14 @@ $manage_content = function($title, $content) use ($contents) { <i class="icon-file-text pull-left icon-2x" style="color:#bbb;margin:0 -36px"></i> <a href="#ajax.php/content/<?php echo $id; ?>/manage" onclick="javascript: - $.dialog($(this).attr('href').substr(1), 200); - return false;"> -<?php + $.dialog($(this).attr('href').substr(1), 201); + return false;" class="pull-left"><i class="icon-file-text icon-2x" + style="color:#bbb;"></i> </a> + <span style="display:inline-block;width:90%;padding-left:10px;line-height:1.2em"> + <a href="#ajax.php/content/<?php echo $id; ?>/manage" + onclick="javascript: + $.dialog($(this).attr('href').substr(1), 201); + return false;"><?php echo Format::htmlchars($title); ?></a><br/> <span class="faded"><?php echo Format::display($notes); ?> diff --git a/include/staff/settings-kb.inc.php b/include/staff/settings-kb.inc.php index cfac97418f92f1156c0c6022911dc4aaa3bd0310..941b08e30fed4e3ca89c6023ce472ae4a452288f 100644 --- a/include/staff/settings-kb.inc.php +++ b/include/staff/settings-kb.inc.php @@ -16,12 +16,16 @@ if(!defined('OSTADMININC') || !$thisstaff || !$thisstaff->isAdmin() || !$config) </thead> <tbody> <tr> - <td width="180"><?php echo __('Knowledge Base Status'); ?>:</td> + <td width="180" valign="top"><?php echo __('Knowledge Base Status'); ?>:</td> <td> <input type="checkbox" name="enable_kb" value="1" <?php echo $config['enable_kb']?'checked="checked"':''; ?>> <?php echo __('Enable Knowledge Base'); ?> - <font class="error"> <?php echo $errors['enable_kb']; ?></font> <i class="help-tip icon-question-sign" href="#knowledge_base_status"></i> + <div class="error"><?php echo $errors['enable_kb']; ?></div> + <input type="checkbox" name="restrict_kb" value="1" <?php echo $config['restrict_kb']?'checked="checked"':''; ?> > + <?php echo __('Require Client Login'); ?> + <i class="help-tip icon-question-sign" href="#restrict_kb"></i> + <div class="error"><?php echo $errors['restrict_kb']; ?></div> </td> </tr> <tr> diff --git a/include/staff/staff.inc.php b/include/staff/staff.inc.php index 1da74cc1aa0648324cc1003ad6003b9ce82454e3..e1e8fb461b01334a541d727c1948c86dab392e19 100644 --- a/include/staff/staff.inc.php +++ b/include/staff/staff.inc.php @@ -17,7 +17,7 @@ if($staff && $_REQUEST['a']!='add'){ $title=__('Add New Agent'); $action='create'; $submit_text=__('Add Agent'); - $passwd_text=__('Temporary password required only for "Local" authenication'); + $passwd_text=__('Temporary password required only for "Local" authentication'); //Some defaults for new staff. $info['change_passwd']=1; $info['welcome_email']=1; diff --git a/include/staff/templates/content-manage.tmpl.php b/include/staff/templates/content-manage.tmpl.php index f10927634df62ded7e925d613d931a2347c3f96b..28a016cb71163b256e32a819b8446b832766a60f 100644 --- a/include/staff/templates/content-manage.tmpl.php +++ b/include/staff/templates/content-manage.tmpl.php @@ -2,6 +2,11 @@ <a class="close" href=""><i class="icon-remove-circle"></i></a> <hr/> +<?php if ($errors['err']) { ?> +<div class="error-banner"> + <?php echo $errors['err']; ?> +</div> +<?php } ?> <form method="post" action="#content/<?php echo $content->getId(); ?>" style="clear:none"> <?php @@ -20,29 +25,32 @@ if (count($langs) > 1) { ?> } ?> <div id="translation-<?php echo $cfg->getPrimaryLanguage(); ?>" class="tab_content left-tabs" style="padding:0" lang="<?php echo $cfg->getPrimaryLanguage(); ?>"> + <div class="error"><?php echo $errors['name']; ?></div> <input type="text" style="width: 100%; font-size: 14pt" name="name" value="<?php - echo Format::htmlchars($content->getName()); ?>" /> + echo Format::htmlchars($info['title']); ?>" /> <div style="margin-top: 5px"> + <div class="error"><?php echo $errors['body']; ?></div> <textarea class="richtext no-bar" name="body"><?php - echo Format::htmlchars(Format::viewableImages($content->getBody())); + echo Format::htmlchars(Format::viewableImages($info['body'])); ?></textarea> </div> </div> <?php foreach ($langs as $tag=>$nfo) { if ($tag == $cfg->getPrimaryLanguage()) - continue; ?> + continue; + $trans = $info['trans'][$tag]; ?> <div id="translation-<?php echo $tag; ?>" class="tab_content left-tabs" style="display:none;padding:0" dir="<?php echo $nfo['direction']; ?>" lang="<?php echo $tag; ?>"> <input type="text" style="width: 100%; font-size: 14pt" name="trans[<?php echo $tag; ?>][title]" value="<?php - echo Format::htmlchars($info['title'][$tag]); ?>" + echo Format::htmlchars($trans['title']); ?>" placeholder="<?php echo __('Title'); ?>" /> <div style="margin-top: 5px"> <textarea class="richtext no-bar" data-direction=<?php echo $nfo['direction']; ?> placeholder="<?php echo __('Message content'); ?>" name="trans[<?php echo $tag; ?>][body]"><?php - echo Format::htmlchars(Format::viewableImages($info['body'][$tag])); + echo Format::htmlchars(Format::viewableImages($trans['body'])); ?></textarea> </div> </div> diff --git a/include/staff/templates/navigation.tmpl.php b/include/staff/templates/navigation.tmpl.php index b28ec05261c0c2d669763b66f79258de7bfd01c3..8f0444999c7d0c169acbc599dfbfa69285a2462d 100644 --- a/include/staff/templates/navigation.tmpl.php +++ b/include/staff/templates/navigation.tmpl.php @@ -1,7 +1,10 @@ <?php if(($tabs=$nav->getTabs()) && is_array($tabs)){ foreach($tabs as $name =>$tab) { - echo sprintf('<li class="%s"><a href="%s">%s</a>',$tab['active']?'active':'inactive',$tab['href'],$tab['desc']); + echo sprintf('<li class="%s %s"><a href="%s">%s</a>', + $tab['active'] ? 'active':'inactive', + @$tab['class'] ?: '', + $tab['href'],$tab['desc']); if(!$tab['active'] && ($subnav=$nav->getSubMenu($name))){ echo "<ul>\n"; foreach($subnav as $k => $item) { diff --git a/include/staff/user-view.inc.php b/include/staff/user-view.inc.php index 9e6fb90faf2f1aeac16daa97a53c5ca0d4325104..eae6c28cb223625efcdb99d813589e1faa19889c 100644 --- a/include/staff/user-view.inc.php +++ b/include/staff/user-view.inc.php @@ -110,10 +110,12 @@ if ($thisstaff->getRole()->hasPerm(User::PERM_EDIT)) { ?> if ($org) echo sprintf('<a href="#users/%d/org" class="user-action">%s</a>', $user->getId(), $org->getName()); - elseif ($thisstaff->getRole()->hasPerm(User::PERM_EDIT)) { ?> - <a href="#users/<?php echo $user->getId(); ?>/org" - class="user-action"><?php echo __('Add Organization'); ?></a> -<?php } + elseif ($thisstaff->getRole()->hasPerm(User::PERM_EDIT)) { + echo sprintf( + '<a href="#users/%d/org" class="user-action">%s</a>', + $user->getId(), + __('Add Organization')); + } ?> </span> </td> diff --git a/js/filedrop.field.js b/js/filedrop.field.js index e451028a03fef02fa385f09497178778c125134c..fc99870bdfd5fbe41d7679666344a1bd19ff3f78 100644 --- a/js/filedrop.field.js +++ b/js/filedrop.field.js @@ -329,7 +329,8 @@ globalProgressUpdated: empty, speedUpdated: empty }, - errors = ["BrowserNotSupported", "TooManyFiles", "FileTooLarge", "FileTypeNotAllowed", "NotFound", "NotReadable", "AbortError", "ReadError", "FileExtensionNotAllowed"]; + errors = ["BrowserNotSupported", "TooManyFiles", "FileTooLarge", "FileTypeNotAllowed", "NotFound", "NotReadable", "AbortError", "ReadError", "FileExtensionNotAllowed"], + Blob = window.WebKitBlob || window.MozBlob || window.Blob; $.fn.filedrop = function(options) { var opts = $.extend({}, default_opts, options), @@ -379,8 +380,7 @@ var dashdash = '--', crlf = '\r\n', builder = [], - paramname = opts.paramname, - Blob = window.WebKitBlob || window.Blob; + paramname = opts.paramname; if (opts.data) { var params = $.param(opts.data).replace(/\+/g, '%20').split(/&/); @@ -476,6 +476,10 @@ opts.error(errors[0]); return false; } + if (typeof Blob === "undefined") { + opts.error(errors[0]); + return false; + } if (opts.allowedfiletypes.push && opts.allowedfiletypes.length) { for(var fileIndex = files.length;fileIndex--;) { diff --git a/scp/css/login.css b/scp/css/login.css index aef7b3a789b44c4d55bc0e7df0965cc7f1bc2247..a5a682f08d04b513b643b044f817416988895d56 100644 --- a/scp/css/login.css +++ b/scp/css/login.css @@ -79,6 +79,8 @@ h1 { height: auto; width: auto; vertical-align: middle; + outline: none; + border: none; } .valign-helper { height: 100%; diff --git a/scp/css/scp.css b/scp/css/scp.css index c64d284d8fdf27bf2676339fd90845872d03e918..30b384e1436c824a598c231203d9335700c6aaba 100644 --- a/scp/css/scp.css +++ b/scp/css/scp.css @@ -133,6 +133,8 @@ div#header a { height: auto; width: auto; vertical-align: middle; + outline: none; + border: none; } .valign-helper { height: 100%; diff --git a/scp/helptopics.php b/scp/helptopics.php index adb1eb7c4fcc8c2830fd2b2e3e72b734a0a25d2b..035b04da810e8ebf2dc5187cc180c64c857e9730 100644 --- a/scp/helptopics.php +++ b/scp/helptopics.php @@ -89,7 +89,7 @@ if($_POST){ )); if ($num > 0) { if($num==$count) - $msg = sprintf(__('Successfully diabled %s'), + $msg = sprintf(__('Successfully disabled %s'), _N('selected help topic', 'selected help topics', $count)); else $warn = sprintf(__('%1$d of %2$d %3$s disabled'), $num, $count, @@ -106,7 +106,7 @@ if($_POST){ if($i && $i==$count) $msg = sprintf(__('Successfully deleted %s'), - _N('selected help topic', 'selected elp topics', $count)); + _N('selected help topic', 'selected help topics', $count)); elseif($i>0) $warn = sprintf(__('%1$d of %2$d %3$s deleted'), $i, $count, _N('selected help topic', 'selected help topics', $count)); diff --git a/scp/images/ost-logo.png b/scp/images/ost-logo.png index 33b09d35aa8dfbc935e85e70ee17bc5c69d0567e..90b4c77640396eb95e26521120557a26a5403181 100644 Binary files a/scp/images/ost-logo.png and b/scp/images/ost-logo.png differ diff --git a/scp/js/dashboard.inc.js b/scp/js/dashboard.inc.js index c902e4be2d77352909602d2d72b31758909136a8..683135e62c05b10975e6b79c897f0d33dc5557ec 100644 --- a/scp/js/dashboard.inc.js +++ b/scp/js/dashboard.inc.js @@ -1,5 +1,5 @@ (function ($) { - var current_tab; + var current_tab = null; function refresh(e) { $('#line-chart-here').empty(); $('#line-chart-legend').empty(); @@ -135,7 +135,12 @@ stop = this.period.value || 'now'; } + if (!current_tab) + current_tab = $('#tabular-navigation li:first-child a'); + var group = current_tab.attr('table-group'); + var pagesize = 25; + getConfig().then(function(c) { if (c.page_size) pagesize = c.page_size; }); $.ajax({ method: 'GET', dataType: 'json', @@ -144,7 +149,6 @@ success: function(json) { var q = $('<table>').attr({'class':'table table-condensed table-striped'}), h = $('<tr>').appendTo($('<thead>').appendTo(q)), - pagesize = 25, max = []; for (var c in json.columns) { h.append($('<th>').append(json.columns[c])); @@ -158,7 +162,7 @@ } for (var i in json.data) { if (i % pagesize === 0) - b = $('<tbody>').attr({'page':i/pagesize+1}).appendTo(q); + b = $('<tbody>').attr({'page':i/pagesize+1}).addClass('hidden').appendTo(q); row = json.data[i]; tr = $('<tr>').appendTo(b); for (var j in row) { @@ -194,30 +198,31 @@ $('<td>').attr('colspan','8').append( 'No data for this timeframe found'))).appendTo(q); } + $('tbody[page=1]', q).removeClass('hidden'); $('#table-here').empty().append(q); // ----------------------> Pagination <--------------------- function goabs(e) { - $('tbody', q).addClass('hide'); + $('tbody', q).addClass('hidden'); if (e.target) { page = e.target.text; - $('tbody[page='+page+']', q).removeClass('hide'); + $('tbody[page='+page+']', q).removeClass('hidden'); } else { - e.removeClass('hide'); + e.removeClass('hidden'); page = e.attr('page') } - enable_next_prev(page); + return enable_next_prev(page); } function goprev() { - current = $('tbody:not(.hide)', q).attr('page'); + current = $('tbody:not(.hidden)', q).attr('page'); page = Math.max(1, parseInt(current) - 1); - goabs($('tbody[page='+page+']', q)); + return goabs($('tbody[page='+page+']', q)); } function gonext() { - current = $('tbody:not(.hide)', q).attr('page'); + current = $('tbody:not(.hidden)', q).attr('page'); page = Math.min(Math.floor(json.data.length / pagesize) + 1, parseInt(current) + 1); - goabs($('tbody[page='+page+']', q)); + return goabs($('tbody[page='+page+']', q)); } function enable_next_prev(page) { $('#table-here div.pagination li[page]').removeClass('active'); @@ -229,6 +234,7 @@ if (page == Math.floor(json.data.length / pagesize) + 1) $('#report-page-next').addClass('disabled'); else $('#report-page-next').removeClass('disabled'); + return false; } var p = $('<ul>') @@ -254,15 +260,16 @@ .appendTo($('<li>') .appendTo(p)); - gonext(); + goprev(); } }); return false; } - - $(function() { - $('#timeframe-form').submit(refresh); + + $(function() { + var form = $('#timeframe-form'); + form.submit(refresh); //Trigger submit now...init. - $('#timeframe-form').submit(); - }); + form.submit(); + }); })(window.jQuery); diff --git a/scp/js/scp.js b/scp/js/scp.js index 79d14e3da1ecd8c5f06d16ad2f0630d890666ba7..9e1b5d714419856c0b3ebd434701a5265bc7b8ff 100644 --- a/scp/js/scp.js +++ b/scp/js/scp.js @@ -386,6 +386,7 @@ var scp_prep = function() { var fObj = $(this); var elem = $('#advanced-search'); $('#result-count').html(''); + fixupDatePickers.call(this); $.ajax({ url: "ajax.php/tickets/search", data: fObj.serialize(), @@ -450,18 +451,21 @@ var scp_prep = function() { $(document).ready(scp_prep); $(document).on('pjax:end', scp_prep); -$(document).on('submit', 'form', function() { +var fixupDatePickers = function() { // Reformat dates $('.dp', $(this)).each(function(i, e) { var $e = $(e), d = $e.datepicker('getDate'); - if (!d) return; + if (!d || $e.data('fixed')) return; var day = ('0'+d.getDate()).substr(-2), month = ('0'+(d.getMonth()+1)).substr(-2), year = d.getFullYear(); $e.val(year+'-'+month+'-'+day); + $e.data('fixed', true); + $e.on('change', function() { $(this).data('fixed', false); }); }); -}); +}; +$(document).on('submit', 'form', fixupDatePickers); /************ global inits *****************/ diff --git a/setup/cli/modules/class.module.php b/setup/cli/modules/class.module.php index e01e507abc8a677ca2534895b18214dc2de0856a..c6af32572dfde210a1014d884d91cff4acf18d9a 100644 --- a/setup/cli/modules/class.module.php +++ b/setup/cli/modules/class.module.php @@ -98,11 +98,10 @@ class Option { class OutputStream { var $stream; - function OutputStream() { - call_user_func_array(array($this, '__construct'), func_get_args()); - } function __construct($stream) { - $this->stream = fopen($stream, 'w'); + if (!($this->stream = fopen($stream, 'w'))) + throw new Exception(sprintf('%s: Cannot open for writing', + $stream)); } function write($what) { diff --git a/setup/cli/modules/file.php b/setup/cli/modules/file.php index ee83169958c3e25a9c514e979564b7f6b699d588..f5057230ac293cae0b23b0944e0b419aca033369 100644 --- a/setup/cli/modules/file.php +++ b/setup/cli/modules/file.php @@ -12,6 +12,7 @@ class FileManager extends Module { 'list' => 'List files matching criteria', 'export' => 'Export files from the system', 'import' => 'Load files exported via `export`', + 'zip' => 'Create a zip file of the matching files', 'dump' => 'Dump file content to stdout', 'load' => 'Load file contents from stdin', 'migrate' => 'Migrate a file to another backend', @@ -176,11 +177,224 @@ class FileManager extends Module { $this->stdout->write("Migrated $count files\n"); break; + /** + * export + * + * Export file contents to a stream file. The format of the stream + * will be a continuous stream of file information in the following + * format: + * + * AFIL<meta-length><data-length><meta><data>EOF\x1c + * + * Where + * A is the version code of the export + * "FIL" is the literal text 'FIL' + * meta-length is 'V' packed header length (bytes) + * data-length is 'V' packed data length (bytes) + * meta is the %file record, php serialized + * data is the raw content of the file + * "EOF" is the literal text 'EOF' + * \x1c is an ASCII 0x1c byte (file separator) + * + * Options: + * --file File to which to direct the stream output, default + * is stdout + */ case 'export': - // Create a temporary ZIP file $files = FileModel::objects(); $this->_applyCriteria($options, $files); + if (!$options['file'] || $options['file'] == '-') + $options['file'] = 'php://stdout'; + + if (!($stream = fopen($options['file'], 'wb'))) + $this->fail($options['file'].': Unable to open file for export stream'); + + foreach ($files as $m) { + $f = AttachmentFile::lookup($m->id); + if ($options['verbose']) + $this->stderr->write($m->name."\n"); + + // TODO: Log %attachment and %ticket_attachment entries + $info = array('file' => $f->getInfo()); + $header = serialize($info); + fwrite($stream, 'AFIL'.pack('VV', strlen($header), $f->getSize())); + fwrite($stream, $header); + $FS = $f->open(); + while ($block = $FS->read()) + fwrite($stream, $block); + fwrite($stream, "EOF\x1c"); + } + fclose($stream); + break; + + /** + * import + * + * Import a collection of file contents exported by the `export`. + * See the export function above for details about the stream + * format. + * + * Options: + * --file File from which to read the export stream, default + * is stdin + * --to Backend to receive the contents (@see `backends`) + * --verbose Show file names while importing + */ + case 'import': + if (!$options['file'] || $options['file'] == '-') + $options['file'] = 'php://stdin'; + + if (!($stream = fopen($options['file'], 'rb'))) + $this->fail($options['file'].': Unable to open import stream'); + + while (true) { + // Read the file header + // struct file_data_header { + // char[4] marker; // Four chars, 'AFIL' + // int lenMeta; + // int lenData; + // }; + if (!($header = fread($stream, 12))) + break; // EOF + + list(, $mark, $hlen, $dlen) = unpack('V3', $header); + + // AFIL written as little-endian 4-byte int is 0x4c4946xx (LIFA), + // where 'A' is the version code of the export + $version = $mark & 0xff; + if (($mark >> 8) != 0x4c4946) + $this->fail('Bad file record'); + + // Read the header + $header = fread($stream, $hlen); + if (strlen($header) != $hlen) + $this->fail('Short read getting header info'); + + $header = unserialize($header); + if (!$header) + $this->fail('Unable to decipher file header'); + + // Find or create the file record + $finfo = $header['file']; + // TODO: Consider the $version code + $f = AttachmentFile::lookup($finfo['id']); + if ($f) { + // Verify file information + if ($f->getSize() != $finfo['size'] + || $f->getSignature() != $finfo['signature'] + ) { + $this->fail(sprintf( + '%s: File data does not match existing file record', + $finfo['name'] + )); + } + // Drop existing file contents, if any + try { + if ($bk = $f->open()) + $bk->unlink(); + } + catch (Exception $e) {} + } + // Create a new file + else { + $fm = FileModel::create($finfo); + if (!$fm->save() || !($f = AttachmentFile::lookup($fm->id))) { + $this->fail(sprintf( + '%s: Unable to create new file record', + $finfo['name'])); + } + } + + // Determine the backend to recieve the file contents + if ($options['to']) { + $bk = FileStorageBackend::lookup($options['to'], $f); + } + // Use the system default + else { + $bk = AttachmentFile::getBackendForFile($f); + } + + if ($options['verbose']) + $this->stdout->write('Importing '.$f->getName()."\n"); + + // Write file contents to the backend + $md5 = hash_init('md5'); + $sha1 = hash_init('sha1'); + $written = 0; + + // Handle exceptions by dropping imported file contents and + // then returning the error to the error output stream. + try { + while ($dlen > 0) { + $read_size = min($dlen, $bk->getBlockSize()); + $contents = ''; + // reading from the stream will likely return an amount of + // data different from the backend requested block size. Loop + // until $read_size bytes are recieved. + while ($read_size > 0 && ($block = fread($stream, $read_size))) { + $contents .= $block; + $read_size -= strlen($block); + } + if ($read_size != 0) { + // short read + throw new Exception(sprintf( + '%s: Some contents are missing from the stream', + $f->getName() + )); + } + // Calculate MD5 and SHA1 hashes of the file to verify + // contents after successfully written to backend + if (!$bk->write($contents)) + throw new Exception( + 'Unable to send file contents to backend'); + hash_update($md5, $contents); + hash_update($sha1, $contents); + $dlen -= strlen($contents); + $written += strlen($contents); + } + // Some backends cannot handle flush() without a + // corresponding write() call. + if ($written && !$bk->flush()) + throw new Exception( + 'Unable to commit file contents to backend'); + + // Check the signature hash + if ($finfo['signature']) { + $md5 = base64_encode(hash_final($md5, true)); + $sha1 = base64_encode(hash_final($sha1, true)); + $sig = str_replace( + array('=','+','/'), + array('','-','_'), + substr($sha1, 0, 16) . substr($md5, 0, 16)); + if ($sig != $finfo['signature']) { + throw new Exception(sprintf( + '%s: Signature verification failed', + $f->getName() + )); + } + } + } // end try + catch (Exception $ex) { + if ($bk) $bk->unlink(); + $this->fail($ex->getMessage()); + } + + // Read file record footer + $footer = fread($stream, 4); + if (strlen($footer) != 4) + $this->fail('Unable to read file EOF marker'); + list(, $footer) = unpack('N', $footer); + // Footer should be EOF\x1c as an int + if ($footer != 0x454f461c) + $this->fail('Incorrect file EOF marker'); + } + break; + + case 'zip': + // Create a temporary ZIP file + $files = FileModel::objects(); + $this->_applyCriteria($options, $files); if (!$options['file']) $this->fail('Please specify zip file with `-f`'); @@ -189,30 +403,33 @@ class FileManager extends Module { ZipArchive::CREATE))) $this->fail($reason.': Unable to create zip file'); - $manifest = array(); foreach ($files as $m) { $f = AttachmentFile::lookup($m->id); - $zip->addFromString($f->getId(), $f->getData()); - $zip->setCommentName($f->getId(), $f->getName()); - // TODO: Log %attachment and %ticket_attachment entries - $info = array('file' => $f->getInfo()); - foreach ($m->tickets as $t) - $info['tickets'][] = $t->ht; - - $manifest[$f->getId()] = $info; + if ($options['verbose']) + $this->stderr->write($m->name."\n"); + $name = Format::encode(sprintf( + '%d-%s', $f->getId(), $f->getName() + ), 'utf-8', 'cp437'); + $zip->addFromString($name, $f->getData()); } - $zip->addFromString('MANIFEST', serialize($manifest)); $zip->close(); break; case 'expunge': - // Create a temporary ZIP file $files = FileModel::objects(); $this->_applyCriteria($options, $files); - foreach ($files as $f) { - $f->tickets->expunge(); - $f->unlink() && $f->delete(); + foreach ($files as $m) { + // Drop associated attachment links + $m->tickets->expunge(); + $f = AttachmentFile::lookup($m->id); + + // Drop file contents + if ($bk = $f->open()) + $bk->unlink(); + + // Drop file record + $f->delete(); } } } diff --git a/setup/cli/modules/i18n.php b/setup/cli/modules/i18n.php index fca97e411f50560c9e8a4e3d0b5b1d18f026edb5..9d5e6ca4c1fe6af88c246ff7798f3418853e195d 100644 --- a/setup/cli/modules/i18n.php +++ b/setup/cli/modules/i18n.php @@ -18,6 +18,10 @@ class i18n_Compiler extends Module { 'sign' => 'Sign a language pack', ), ), + 'file(s)' => array( + 'required' => false, + 'help' => 'File(s) to be signed, used with `sign`', + ), ); var $options = array( @@ -35,8 +39,15 @@ class i18n_Compiler extends Module { 'domain' => array('-D', '--domain', 'metavar'=>'name', 'default' => '', 'help' => 'Add a domain to the path/context of PO strings'), + 'dns' => array('-d', '--dns', 'default' => false, 'metavar' => 'zone-id', + 'help' => 'Write signature to DNS (via this AWS HostedZoneId)'), ); + var $epilog = "Note: If updating DNS, you will need to set + AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY in the AWS credentials + profile in your home folder or in your environment. See AWS + configuration docs for more information"; + static $project = 'osticket-official'; static $crowdin_api_url = 'http://i18n.osticket.com/api/project/{project}/{command}'; @@ -96,9 +107,14 @@ class i18n_Compiler extends Module { $this->_make_pot($options); break; case 'sign': - if (!$options['file'] || !file_exists($options['file'])) - $this->fail('Specify a language pack to sign with --file='); - $this->_sign($options['file'], $options); + if (count($args) < 2) + $this->fail('Specify a language pack to sign'); + foreach (range(1, count($args)-1, 1) as $i) { + $plugin = $args[$i]; + if (!is_file($args[$i])) + $this->fail($args[$i].': No such file'); + $this->_sign($args[$i], $options); + } break; } } @@ -303,9 +319,50 @@ class i18n_Compiler extends Module { $this->stdout->write(sprintf("Signature: %s\n", strtolower($signature['hash']))); - $this->stdout->write( - sprintf("Seal: \"v=1; i=%s; s=%s; V=%s;\"\n", - $info['Id'], base64_encode($seal), $info['Version'])); + $seal = + sprintf('"v=1; i=%s; s=%s; V=%s;"', + $info['Id'], base64_encode($seal), $info['Version']); + + if ($options['dns']) { + if (!is_file(INCLUDE_DIR . 'aws.phar')) + $this->fail('Unable to include AWS phar file. Download to INCLUDE_DIR'); + require_once INCLUDE_DIR . 'aws.phar'; + + $aws = Aws\Common\Aws::factory(array()); + $client = $aws->get('Route53'); + + try { + $resp = $client->changeResourceRecordSets(array( + 'HostedZoneId' => $options['dns'], + 'ChangeBatch' => array( + 'Changes' => array( + array( + 'Action' => 'CREATE', + 'ResourceRecordSet' => array( + 'Name' => "{$signature['hash']}.updates.osticket.com.", + 'Type' => 'TXT', + 'TTL' => 172800, + 'ResourceRecords' => array( + array( + 'Value' => $seal, + ), + ), + ), + ), + ), + ), + )); + $this->stdout->write(sprintf('%s: %s', $resp['ChangeInfo']['Comment'], + $resp['ChangeInfo']['Status'])); + } + catch (Exception $ex) { + $this->stdout->write("Seal: $seal\n"); + $this->fail('!! AWS Update Failed: '.$ex->getMessage()); + } + } + else { + $this->stdout->write("Seal: $seal\n"); + } } function __read_next_string($tokens) { diff --git a/setup/test/tests/stubs.php b/setup/test/tests/stubs.php index dbd057d515db677b957cd3d8345ad26872ecebd8..fa0706313a77a2edde33868231556c017d88b306 100644 --- a/setup/test/tests/stubs.php +++ b/setup/test/tests/stubs.php @@ -11,6 +11,7 @@ class mysqli { function select_db() {} function set_charset() {} function autocommit() {} + function rollback() {} } class mysqli_stmt { @@ -122,6 +123,7 @@ class IntlBreakIterator { class SqlFunction { static function NOW() {} + static function COALESCE() {} } class SqlExpression { diff --git a/tickets.php b/tickets.php index 052762488b814df35147478e39892fa07385cdb5..10f70676a4713d3c7b696f8304a2c6bf74067a70 100644 --- a/tickets.php +++ b/tickets.php @@ -88,7 +88,7 @@ if ($_POST && is_object($ticket) && $ticket->getId()) { Draft::deleteForNamespace('ticket.client.' . $ticket->getId()); // Drop attachments $attachments->reset(); - $tform->setSource(array()); + $attachments->getForm()->setSource(array()); } else { $errors['err']=__('Unable to post the message. Try again'); } @@ -130,6 +130,8 @@ if($ticket && $ticket->checkUserAccess($thisclient)) { } include(CLIENTINC_DIR.'header.inc.php'); include(CLIENTINC_DIR.$inc); +if ($tform instanceof DynamicFormEntry) + $tform = $tform->getForm(); print $tform->getMedia(); include(CLIENTINC_DIR.'footer.inc.php'); ?>