diff --git a/.gitignore b/.gitignore index 3bdc4e066866d0433aafc3341fa4b43b0b420b58..6e7535b0b92c9fd5d8c741d3c95ba4fe7c19630e 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,6 @@ Vagrantfile # Staging directory used for packaging script stage + +# Ignore packaged plugins and language packs +*.phar diff --git a/ajax.php b/ajax.php index 57b6274ccee12f4292f97aa7df5b4f9c9cd938ce..3374776c7a54ef4cbaa1c0bf7a8139d2179be2dc 100644 --- a/ajax.php +++ b/ajax.php @@ -39,5 +39,6 @@ $dispatcher = patterns('', url_get('^help-topic/(?P<id>\d+)$', 'getClientFormsForHelpTopic') )) ); +Signal::send('ajax.client', $dispatcher); print $dispatcher->resolve($ost->get_path_info()); ?> diff --git a/bootstrap.php b/bootstrap.php index d726e8c0452c7ee2141a02d6065e01b36f308285..a82796ebd2d7ecba1eb453884502f10a193ded53 100644 --- a/bootstrap.php +++ b/bootstrap.php @@ -177,7 +177,7 @@ class Bootstrap { require(INCLUDE_DIR.'class.log.php'); require(INCLUDE_DIR.'class.crypto.php'); require(INCLUDE_DIR.'class.timezone.php'); - require(INCLUDE_DIR.'class.signal.php'); + require_once(INCLUDE_DIR.'class.signal.php'); require(INCLUDE_DIR.'class.nav.php'); require(INCLUDE_DIR.'class.page.php'); require_once(INCLUDE_DIR.'class.format.php'); //format helpers diff --git a/include/ajax.tickets.php b/include/ajax.tickets.php index e6c0fdb36a4aa6268f89feb437c53e713d2ce350..f52a4341205f6c0e66e12ffff07ebff15d14767f 100644 --- a/include/ajax.tickets.php +++ b/include/ajax.tickets.php @@ -103,12 +103,10 @@ class TicketsAjaxAPI extends AjaxController { global $thisstaff, $cfg; $result=array(); - $select = 'SELECT DISTINCT ticket.ticket_id'; + $select = 'SELECT ticket.ticket_id'; $from = ' FROM '.TICKET_TABLE.' ticket '; - $where = ' WHERE 1 '; - //Access control. - $where.=' AND ( ticket.staff_id='.db_input($thisstaff->getId()); + $where = ' WHERE ( ticket.staff_id='.db_input($thisstaff->getId()); if(($teams=$thisstaff->getTeams()) && count(array_filter($teams))) $where.=' OR ticket.team_id IN('.implode(',', db_input(array_filter($teams))).')'; @@ -179,57 +177,65 @@ class TicketsAjaxAPI extends AjaxController { $where.=' AND ticket.created<=FROM_UNIXTIME('.$endTime.')'; //Query + $joins = array(); if($req['query']) { $queryterm=db_real_escape($req['query'], false); - $from.=' LEFT JOIN '.TICKET_THREAD_TABLE.' thread ON (ticket.ticket_id=thread.ticket_id )' - .' LEFT JOIN '.FORM_ENTRY_TABLE.' tentry ON (tentry.object_id = ticket.ticket_id - AND tentry.object_type="T") - LEFT JOIN '.FORM_ANSWER_TABLE.' tans ON (tans.entry_id = tentry.id - AND tans.value_id IS NULL) - LEFT JOIN '.FORM_ENTRY_TABLE.' uentry ON (uentry.object_id = ticket.user_id + // Setup sets of joins and queries + $joins[] = array( + 'from' => + 'LEFT JOIN '.TICKET_THREAD_TABLE.' thread ON (ticket.ticket_id=thread.ticket_id )', + 'where' => "thread.title LIKE '%$queryterm%' OR thread.body LIKE '%$queryterm%'" + ); + $joins[] = array( + 'from' => + 'LEFT JOIN '.FORM_ENTRY_TABLE.' tentry ON (tentry.object_id = ticket.ticket_id AND tentry.object_type="T") + LEFT JOIN '.FORM_ANSWER_TABLE.' tans ON (tans.entry_id = tentry.id AND tans.value_id IS NULL)', + 'where' => "tans.value LIKE '%$queryterm%'" + ); + $joins[] = array( + 'from' => + 'LEFT JOIN '.FORM_ENTRY_TABLE.' uentry ON (uentry.object_id = ticket.user_id AND uentry.object_type="U") LEFT JOIN '.FORM_ANSWER_TABLE.' uans ON (uans.entry_id = uentry.id AND uans.value_id IS NULL) LEFT JOIN '.USER_TABLE.' user ON (ticket.user_id = user.id) - LEFT JOIN '.USER_EMAIL_TABLE.' uemail ON (user.id = uemail.user_id)'; - - $where.=" AND ( uemail.address LIKE '%$queryterm%'" - ." OR user.name LIKE '%$queryterm%'" - ." OR tans.value LIKE '%$queryterm%'" - ." OR uans.value LIKE '%$queryterm%'" - ." OR thread.title LIKE '%$queryterm%'" - ." OR thread.body LIKE '%$queryterm%'" - .' )'; + LEFT JOIN '.USER_EMAIL_TABLE.' uemail ON (user.id = uemail.user_id)', + 'where' => + "uemail.address LIKE '%$queryterm%' OR user.name LIKE '%$queryterm%' OR uans.value LIKE '%$queryterm%'", + ); } // Dynamic fields - $dynfields='(SELECT entry.object_id, %s '. - 'FROM '.FORM_ANSWER_TABLE.' ans '. - 'JOIN '.FORM_ENTRY_TABLE.' entry ON entry.id=ans.entry_id '. - 'JOIN '.FORM_FIELD_TABLE.' field ON field.id=ans.field_id '. - 'WHERE entry.object_type="T" GROUP BY entry.object_id)'; - $vals = array(); + $cdata_search = false; foreach (TicketForm::getInstance()->getFields() as $f) { if (isset($req[$f->getFormName()]) && ($val = $req[$f->getFormName()])) { - $id = $f->get('id'); - $vals[] = "MAX(IF(field.id = '$id', ans.value_id, NULL)) as `f_{$id}_id`"; - $vals[] = "MAX(IF(field.id = '$id', ans.value, NULL)) as `f_$id`"; - $where .= " AND (dyn.`f_{$id}_id` = ".db_input($val) - . " OR dyn.`f_$id` LIKE '%".db_real_escape($val)."%')"; + $name = $f->get('name') ? $f->get('name') : 'field_'.$f->get('id'); + $cwhere = "cdata.`$name` LIKE '%".db_real_escape($val)."%'"; + if ($f->getImpl()->hasIdValue() && is_numeric($val)) + $cwhere .= " OR cdata.`{$name}_id` = ".db_input($val); + $where .= ' AND ('.$cwhere.')'; + $cdata_search = true; } } - if ($vals) - $from .= ' LEFT JOIN '.sprintf($dynfields, implode(',', $vals)) - ." dyn ON (dyn.object_id = ticket.ticket_id)"; + if ($cdata_search) + $from .= 'LEFT JOIN '.TABLE_PREFIX.'ticket__cdata ' + ." cdata ON (cdata.ticket_id = ticket.ticket_id)"; + + $sections = array(); + foreach ($joins as $j) { + $sections[] = "$select $from {$j['from']} $where AND ({$j['where']})"; + } + if (!$joins) + $sections[] = "$select $from $where"; - $sql="$select $from $where"; + $sql=implode(' union ', $sections); $res = db_query($sql); $tickets = array(); - while (list($tickets[]) = db_fetch_row($res)); - $tickets = array_filter($tickets); + while ($row = db_fetch_row($res)) + $tickets[] = $row[0]; return $tickets; } diff --git a/include/ajax.tips.php b/include/ajax.tips.php index ed560403e945933600ad3390dea1aa508530ea8b..e81d1301b53622478520050bcd6e75528b28e36e 100644 --- a/include/ajax.tips.php +++ b/include/ajax.tips.php @@ -20,8 +20,13 @@ if(!defined('INCLUDE_DIR')) die('!'); require_once(INCLUDE_DIR.'class.i18n.php'); class HelpTipAjaxAPI extends AjaxController { - function getTipsJson($namespace, $lang='en_US') { - global $ost; + function getTipsJson($namespace, $lang=false) { + global $ost, $thisstaff; + + if (!$lang) + $lang = ($thisstaff) + ? $thisstaff->getLanguage() + : Internationalization::getDefaultLanguage(); $i18n = new Internationalization($lang); $tips = $i18n->getTemplate("help/tips/$namespace.yaml"); diff --git a/include/class.config.php b/include/class.config.php index 06607fdc52393e084f8fdc9dde376b4c8108a1e1..f5c7077d7dcbe07cddca31d10e83d1e63a7cb32e 100644 --- a/include/class.config.php +++ b/include/class.config.php @@ -148,6 +148,7 @@ class OsticketConfig extends Config { 'allow_online_attachments_onlogin' => false, 'name_format' => 'full', # First Last 'auto_claim_tickets'=> true, + 'system_language' => 'en_US', ); function OsticketConfig($section=null) { @@ -709,6 +710,10 @@ class OsticketConfig extends Config { return ($this->allowAttachments() && $this->get('allow_email_attachments')); } + function getSystemLanguage() { + return $this->get('system_language'); + } + //TODO: change db field to allow_api_attachments - which will include email/json/xml attachments // terminology changed on the UI function allowAPIAttachments() { diff --git a/include/class.dispatcher.php b/include/class.dispatcher.php index 0448f5024eb3e9244f1bf1bbe1b8b462d171117a..d586fd0ba77e010549d72196bcaec0a5f94c44d2 100644 --- a/include/class.dispatcher.php +++ b/include/class.dispatcher.php @@ -153,7 +153,10 @@ class UrlMatcher { function apply_prefix() { if (is_array($this->func)) { list($class, $func) = $this->func; } else { $func = $this->func; $class = ""; } - $class = $this->prefix . $class; + if (is_object($class)) + return array(false, $this->func); + if ($this->prefix) + $class = $this->prefix . $class; if (strpos($class, ":")) { list($file, $class) = explode(":", $class, 2); diff --git a/include/class.dynamic_forms.php b/include/class.dynamic_forms.php index 52e3109356db760e6a9d412c7782b5697b9891ec..f164138994cc1846eec67baccc16b99f1fe49c97 100644 --- a/include/class.dynamic_forms.php +++ b/include/class.dynamic_forms.php @@ -18,6 +18,7 @@ **********************************************************************/ require_once(INCLUDE_DIR . 'class.orm.php'); require_once(INCLUDE_DIR . 'class.forms.php'); +require_once(INCLUDE_DIR . 'class.signal.php'); /** * Form template, used for designing the custom form and for entering custom @@ -130,6 +131,7 @@ class DynamicForm extends VerySimpleModel { foreach ($ht['fields'] as $f) { $f = DynamicFormField::create($f); $f->form_id = $inst->id; + $f->setForm($inst); $f->save(); } } @@ -196,6 +198,89 @@ class TicketForm extends DynamicForm { static::$instance = $o[0]->instanciate(); return static::$instance; } + + // Materialized View for Ticket custom data (MySQL FlexViews would be + // nice) + // + // @see http://code.google.com/p/flexviews/ + static function getDynamicDataViewFields() { + $fields = array(); + foreach (self::getInstance()->getFields() as $f) { + $impl = $f->getImpl(); + if (!$impl->hasData() || $impl->isPresentationOnly()) + continue; + + $name = ($f->get('name')) ? $f->get('name') + : 'field_'.$f->get('id'); + + $fields[] = sprintf( + 'MAX(IF(field.name=\'%1$s\',ans.value,NULL)) as `%1$s`', + $name); + if ($impl->hasIdValue()) { + $fields[] = sprintf( + 'MAX(IF(field.name=\'%1$s\',ans.value_id,NULL)) as `%1$s_id`', + $name); + } + } + return $fields; + } + + static function ensureDynamicDataView() { + $sql = 'SHOW TABLES LIKE \''.TABLE_PREFIX.'ticket__cdata\''; + if (!db_num_rows(db_query($sql))) + return static::buildDynamicDataView(); + } + + static function buildDynamicDataView() { + // create table __cdata (primary key (ticket_id)) as select + // entry.object_id as ticket_id, MAX(IF(field.name = 'subject', + // ans.value, NULL)) as `subject`,MAX(IF(field.name = 'priority', + // ans.value, NULL)) as `priority_desc`,MAX(IF(field.name = + // 'priority', ans.value_id, NULL)) as `priority_id` + // FROM ost_form_entry entry LEFT JOIN ost_form_entry_values ans ON + // ans.entry_id = entry.id LEFT JOIN ost_form_field field ON + // field.id=ans.field_id + // where entry.object_type='T' group by entry.object_id; + $fields = static::getDynamicDataViewFields(); + $sql = 'CREATE TABLE `'.TABLE_PREFIX.'ticket__cdata` (PRIMARY KEY (ticket_id)) AS + SELECT entry.`object_id` AS ticket_id, '.implode(',', $fields) + .' FROM ost_form_entry entry + JOIN ost_form_entry_values ans ON ans.entry_id = entry.id + JOIN ost_form_field field ON field.id=ans.field_id + WHERE entry.object_type=\'T\' GROUP BY entry.object_id'; + db_query($sql); + } + + static function dropDynamicDataView() { + db_query('DROP TABLE IF EXISTS `'.TABLE_PREFIX.'ticket__cdata`'); + } + + static function updateDynamicDataView($answer, $data) { + // TODO: Detect $data['dirty'] for value and value_id + // We're chiefly concerned with Ticket form answers + if (!($e = $answer->getEntry()) || $e->get('object_type') != 'T') + return; + + // If the `name` column is in the dirty list, we would be renaming a + // column. Delete the view instead. + if (isset($data['dirty']) && isset($data['dirty']['name'])) + return self::dropDynamicDataView(); + + // $record = array(); + // $record[$f] = $answer->value' + // TicketFormData::objects()->filter(array('ticket_id'=>$a)) + // ->merge($record); + $f = $answer->getField(); + $name = $f->get('name') ? $f->get('name') : 'field_'.$f->get('id'); + $ids = $f->hasIdValue(); + $fields = sprintf('`%s`=', $name) . db_input($answer->get('value')); + if ($f->hasIdValue()) + $fields .= sprintf(',`%s_id`=', $name) . db_input($answer->getIdValue()); + $sql = 'INSERT INTO `'.TABLE_PREFIX.'ticket__cdata` SET '.$fields + .', `ticket_id`='.db_input($answer->getEntry()->get('object_id')) + .' ON DUPLICATE KEY UPDATE '.$fields; + db_query($sql); + } } // Add fields from the standard ticket form to the ticket filterable fields Filter::addSupportedMatches('Custom Fields', function() { @@ -207,6 +292,23 @@ Filter::addSupportedMatches('Custom Fields', function() { } return $matches; }); +// Manage materialized view on custom data updates +Signal::connect('model.created', + array('TicketForm', 'updateDynamicDataView'), + 'DynamicFormEntryAnswer'); +Signal::connect('model.updated', + array('TicketForm', 'updateDynamicDataView'), + 'DynamicFormEntryAnswer'); +// Recreate the dynamic view after new or removed fields to the ticket +// details form +Signal::connect('model.created', + array('TicketForm', 'dropDynamicDataView'), + 'DynamicFormField', + function($o) { return $o->getForm()->get('type') == 'T'; }); +Signal::connect('model.deleted', + array('TicketForm', 'dropDynamicDataView'), + 'DynamicFormField', + function($o) { return $o->getForm()->get('type') == 'T'; }); require_once(INCLUDE_DIR . "class.json.php"); @@ -555,6 +657,7 @@ class DynamicFormEntry extends VerySimpleModel { array('field_id'=>$f->get('id'))); $a->field = $f; $a->field->setAnswer($a); + $a->entry = $inst; $inst->_values[] = $a; } return $inst; diff --git a/include/class.forms.php b/include/class.forms.php index b5a7802408378eac68b0d41efc302b7af944ef75..9d0bdc87e460396ee901a708b6e1ff31948fb767 100644 --- a/include/class.forms.php +++ b/include/class.forms.php @@ -440,6 +440,14 @@ class FormField { return $this->presentation_only; } + /** + * Indicates if the field places data in the `value_id` column. This + * is currently used by the materialized view system + */ + function hasIdValue() { + return false; + } + function getConfigurationForm() { if (!$this->_cform) { $type = static::getFieldType($this->get('type')); @@ -806,6 +814,10 @@ class PriorityField extends ChoiceField { return $widget; } + function hasIdValue() { + return true; + } + function getChoices() { $this->ht['default'] = 0; @@ -1140,6 +1152,7 @@ class ThreadEntryWidget extends Widget { <input type="file" class="multifile" name="attachments[]" id="attachments" size="30" value="" /> </div> <font class="error"> <?php echo $errors['attachments']; ?></font> + </div> <hr/> <?php } diff --git a/include/class.http.php b/include/class.http.php index 3aea32b6e93029e541bf0d0c9fd28bef15b20dc3..ef917845d76a1f943e022167304eb7a2b0d131c5 100644 --- a/include/class.http.php +++ b/include/class.http.php @@ -43,9 +43,14 @@ class Http { exit; } - function redirect($url,$delay=0,$msg='') { + function redirect($url,$delay=0,$msg='') { - if(strstr($_SERVER['SERVER_SOFTWARE'], 'IIS')){ + $iis = strpos($_SERVER['SERVER_SOFTWARE'], 'IIS') !== false; + @list($name, $version) = explode('/', $_SERVER['SERVER_SOFTWARE']); + // Legacy code for older versions of IIS that would not emit the + // correct HTTP status and headers when using the `Location` + // header alone + if ($iis && version_compare($version, '7.0', '<')) { header("Refresh: $delay; URL=$url"); }else{ header("Location: $url"); diff --git a/include/class.i18n.php b/include/class.i18n.php index 7891d7ef937ecacf2bc148ae4a4e441801a8e003..b4b2564b7ec96e5983b5fafbf245140ab771fff4 100644 --- a/include/class.i18n.php +++ b/include/class.i18n.php @@ -16,7 +16,6 @@ **********************************************************************/ require_once INCLUDE_DIR.'class.error.php'; require_once INCLUDE_DIR.'class.yaml.php'; -require_once INCLUDE_DIR.'class.config.php'; class Internationalization { @@ -43,7 +42,6 @@ class Internationalization { function loadDefaultData() { # notrans -- do not translate the contents of this array $models = array( - 'email_template_group.yaml' => 'EmailTemplateGroup', 'department.yaml' => 'Dept', 'sla.yaml' => 'SLA', 'form.yaml' => 'DynamicForm', @@ -77,6 +75,7 @@ class Internationalization { } // Configuration + require_once INCLUDE_DIR.'class.config.php'; if (($tpl = $this->getTemplate('config.yaml')) && ($data = $tpl->getData())) { foreach ($data as $section=>$items) { @@ -101,6 +100,8 @@ class Internationalization { if (db_query($sql) && ($id = db_insert_id())) $_config->set("{$type}_page_id", $id); } + // Default Language + $_config->set('system_language', $this->langs[0]); // Canned response examples if (($tpl = $this->getTemplate('templates/premade.yaml')) @@ -118,6 +119,13 @@ class Internationalization { // Email templates // TODO: Lookup tpl_id + if ($objects = $this->getTemplate('email_template_group.yaml')->getData()) { + foreach ($objects as $o) { + $o['lang_id'] = $this->langs[0]; + $tpl = EmailTemplateGroup::create($o, $errors); + } + } + // This shouldn't be necessary $tpl = EmailTemplateGroup::lookup(1); foreach ($tpl->all_names as $name=>$info) { if (($tp = $this->getTemplate("templates/email/$name.yaml")) @@ -131,6 +139,122 @@ class Internationalization { } } } + + static function availableLanguages($base=I18N_DIR) { + $langs = (include I18N_DIR . 'langs.php'); + + // Consider all subdirectories and .phar files in the base dir + $dirs = glob(I18N_DIR . '*', GLOB_ONLYDIR | GLOB_NOSORT); + $phars = glob(I18N_DIR . '*.phar', GLOB_NOSORT); + + $installed = array(); + foreach (array_merge($dirs, $phars) as $f) { + $base = basename($f, '.phar'); + @list($code, $locale) = explode('_', $base); + if (isset($langs[$code])) { + $installed[strtolower($base)] = + $langs[$code] + array( + 'lang' => $code, + 'locale' => $locale, + 'path' => $f, + 'code' => $base, + 'desc' => sprintf("%s%s (%s)", + $langs[$code]['nativeName'], + $locale ? sprintf(' - %s', $locale) : '', + $langs[$code]['name']), + ); + } + } + usort($installed, function($a, $b) { return strcasecmp($a['code'], $b['code']); }); + + return $installed; + } + + // TODO: Move this to the REQUEST class or some middleware when that + // exists. + // Algorithm borrowed from Drupal 7 (locale.inc) + static function getDefaultLanguage() { + global $cfg; + + if (empty($_SERVER["HTTP_ACCEPT_LANGUAGE"])) + return $cfg->getSystemLanguage(); + + $languages = self::availableLanguages(); + + // The Accept-Language header contains information about the + // language preferences configured in the user's browser / operating + // system. RFC 2616 (section 14.4) defines the Accept-Language + // header as follows: + // Accept-Language = "Accept-Language" ":" + // 1#( language-range [ ";" "q" "=" qvalue ] ) + // language-range = ( ( 1*8ALPHA *( "-" 1*8ALPHA ) ) | "*" ) + // Samples: "hu, en-us;q=0.66, en;q=0.33", "hu,en-us;q=0.5" + $browser_langcodes = array(); + $matches = array(); + if (preg_match_all('@(?<=[, ]|^)([a-zA-Z-]+|\*)(?:;q=([0-9.]+))?(?:$|\s*,\s*)@', + trim($_SERVER['HTTP_ACCEPT_LANGUAGE']), $matches, PREG_SET_ORDER)) { + foreach ($matches as $match) { + // We can safely use strtolower() here, tags are ASCII. + // RFC2616 mandates that the decimal part is no more than three + // digits, so we multiply the qvalue by 1000 to avoid floating + // point comparisons. + $langcode = strtolower($match[1]); + $qvalue = isset($match[2]) ? (float) $match[2] : 1; + $browser_langcodes[$langcode] = (int) ($qvalue * 1000); + } + } + + // We should take pristine values from the HTTP headers, but + // Internet Explorer from version 7 sends only specific language + // tags (eg. fr-CA) without the corresponding generic tag (fr) + // unless explicitly configured. In that case, we assume that the + // lowest value of the specific tags is the value of the generic + // language to be as close to the HTTP 1.1 spec as possible. + // + // References: + // http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.4 + // http://blogs.msdn.com/b/ie/archive/2006/10/17/accept-language-header-for-internet-explorer-7.aspx + asort($browser_langcodes); + foreach ($browser_langcodes as $langcode => $qvalue) { + $generic_tag = strtok($langcode, '-'); + if (!isset($browser_langcodes[$generic_tag])) { + $browser_langcodes[$generic_tag] = $qvalue; + } + } + + // Find the enabled language with the greatest qvalue, following the rules + // of RFC 2616 (section 14.4). If several languages have the same qvalue, + // prefer the one with the greatest weight. + $best_match_langcode = FALSE; + $max_qvalue = 0; + foreach ($languages as $langcode => $language) { + // Language tags are case insensitive (RFC2616, sec 3.10). + // We use _ as the location separator + $langcode = str_replace('_','-',strtolower($langcode)); + + // If nothing matches below, the default qvalue is the one of the wildcard + // language, if set, or is 0 (which will never match). + $qvalue = isset($browser_langcodes['*']) ? $browser_langcodes['*'] : 0; + + // Find the longest possible prefix of the browser-supplied language + // ('the language-range') that matches this site language ('the language tag'). + $prefix = $langcode; + do { + if (isset($browser_langcodes[$prefix])) { + $qvalue = $browser_langcodes[$prefix]; + break; + } + } while ($prefix = substr($prefix, 0, strrpos($prefix, '-'))); + + // Find the best match. + if ($qvalue > $max_qvalue) { + $best_match_langcode = $language['code']; + $max_qvalue = $qvalue; + } + } + + return $best_match_langcode; + } } class DataTemplate { @@ -153,6 +277,12 @@ class DataTemplate { $this->filepath = realpath("{$this->base}/$l/$path"); break; } + elseif (Phar::isValidPharFilename("{$this->base}/$l.phar") + && file_exists("phar://{$this->base}/$l.phar/$path")) { + $this->lang = $l; + $this->filepath = "phar://{$this->base}/$l.phar/$path"; + break; + } } } diff --git a/include/class.mailer.php b/include/class.mailer.php index a57f8d7be1ac5b7e15f79fd3d8430cbfd2d58819..21a7d157d68a28191b88e798dade0633d3c15725 100644 --- a/include/class.mailer.php +++ b/include/class.mailer.php @@ -139,12 +139,11 @@ class Mailer { $mime = new Mail_mime(); + // If the message is not explicitly declared to be a text message, + // then assume that it needs html processing to create a valid text + // body $isHtml = true; - // Ensure that the 'text' option / hint is not set to true and that - // the message appears to be HTML -- that is, the first - // non-whitespace char is a '<' character - if (!(isset($options['text']) && $options['text']) - && (!$cfg || $cfg->isHtmlThreadEnabled())) { + if (!(isset($options['text']) && $options['text'])) { // Make sure nothing unsafe has creeped into the message $message = Format::safe_html($message); //XXX?? $mime->setTXTBody(Format::html2text($message, 90, false)); diff --git a/include/class.orm.php b/include/class.orm.php index 38d3482daf79c91fddeb9ef0f9b81240d4a4ed42..dee287cea77adc73d323db573e79a83151a998de 100644 --- a/include/class.orm.php +++ b/include/class.orm.php @@ -147,7 +147,10 @@ class VerySimpleModel { foreach ($pk as $p) $filter[] = $p.' = '.db_input($this->get($p)); $sql .= ' WHERE '.implode(' AND ', $filter).' LIMIT 1'; - return db_query($sql) && db_affected_rows() == 1; + if (!db_query($sql) || db_affected_rows() != 1) + throw new Exception(db_error()); + Signal::send('model.deleted', $this); + return true; } function save($refetch=false) { @@ -183,6 +186,11 @@ class VerySimpleModel { $this->__new__ = false; // Setup lists again $this->__setupForeignLists(); + Signal::send('model.created', $this); + } + else { + $data = array('dirty' => $this->dirty); + Signal::send('model.updated', $this, $data); } # Refetch row from database # XXX: Too much voodoo diff --git a/include/class.plugin.php b/include/class.plugin.php index 107ff4f46d8b451c038ae4b850f4731a6ada8b7e..29dddf0f982c9549e5300e40a462e6906de83e08 100644 --- a/include/class.plugin.php +++ b/include/class.plugin.php @@ -114,25 +114,39 @@ class PluginManager { if (!($res = db_query($sql))) return static::$plugin_list; - $infos = static::allInfos(); while ($ht = db_fetch_array($res)) { // XXX: Only read active plugins here. allInfos() will // read all plugins - if (isset($infos[$ht['install_path']])) { - $info = $infos[$ht['install_path']]; - if ($ht['isactive']) { - list($path, $class) = explode(':', $info['plugin']); - if (!$class) - $class = $path; - else - require_once(INCLUDE_DIR . $ht['install_path'] - . '/' . $path); - static::$plugin_list[$ht['install_path']] - = new $class($ht['id']); - } - else { - static::$plugin_list[$ht['install_path']] = $ht; - } + $info = static::getInfoForPath( + INCLUDE_DIR . $ht['install_path'], $ht['isphar']); + list($path, $class) = explode(':', $info['plugin']); + if (!$class) + $class = $path; + elseif ($ht['isphar']) + require_once('phar://' . INCLUDE_DIR . $ht['install_path'] + . '/' . $path); + else + require_once(INCLUDE_DIR . $ht['install_path'] + . '/' . $path); + if ($ht['isactive']) { + static::$plugin_list[$ht['install_path']] + = new $class($ht['id']); + } + else { + // Get instance without calling the constructor. Thanks + // http://stackoverflow.com/a/2556089 + $a = unserialize( + sprintf( + 'O:%d:"%s":0:{}', + strlen($class), $class + ) + ); + // Simulate __construct() and load() + $a->id = $ht['id']; + $a->ht = $ht; + $a->info = $info; + static::$plugin_list[$ht['install_path']] = &$a; + unset($a); } } return static::$plugin_list; @@ -146,6 +160,10 @@ class PluginManager { return $plugins; } + function throwException($errno, $errstr) { + throw new RuntimeException($errstr); + } + /** * allInfos * @@ -158,34 +176,62 @@ class PluginManager { * queried to determine if the plugin is installed */ static function allInfos() { - static $defaults = array( - 'include' => 'include/', - 'stream' => false, - ); + foreach (glob(INCLUDE_DIR . 'plugins/*', + GLOB_NOSORT|GLOB_BRACE) as $p) { + $is_phar = false; + if (substr($p, strlen($p) - 5) == '.phar' + && Phar::isValidPharFilename($p)) { + try { + // When public key is invalid, openssl throws a + // 'supplied key param cannot be coerced into a public key' warning + // and phar ignores sig verification. + // We need to protect from that by catching the warning + // Thanks, https://github.com/koto/phar-util + set_error_handler(array('self', 'throwException')); + $ph = new Phar($p); + restore_error_handler(); + // Verify the signature + $ph->getSignature(); + $p = 'phar://' . $p; + $is_phar = true; + } catch (UnexpectedValueException $e) { + // Cannot find signature file + } catch (RuntimeException $e) { + // Invalid signature file + } - if (static::$plugin_info) - return static::$plugin_info; + } - foreach (glob(INCLUDE_DIR . 'plugins/*', GLOB_ONLYDIR) as $p) { if (!is_file($p . '/plugin.php')) // Invalid plugin -- must define "/plugin.php" continue; - // plugin.php is require to return an array of informaiton about - // the plugin. - $info = array_merge($defaults, (include $p . '/plugin.php')); - $info['install_path'] = str_replace(INCLUDE_DIR, '', $p); - // XXX: Ensure 'id' key isset - static::$plugin_info[$info['install_path']] = $info; + // Cache the info into static::$plugin_info + static::getInfoForPath($p, $is_phar); } return static::$plugin_info; } - static function getInfoForPath($path) { - $infos = static::allInfos(); - if (isset($infos[$path])) - return $infos[$path]; - return null; + static function getInfoForPath($path, $is_phar=false) { + static $defaults = array( + 'include' => 'include/', + 'stream' => false, + ); + + $install_path = str_replace(INCLUDE_DIR, '', $path); + $install_path = str_replace('phar://', '', $install_path); + if ($is_phar && substr($path, 0, 7) != 'phar://') + $path = 'phar://' . $path; + if (!isset(static::$plugin_info[$install_path])) { + // plugin.php is require to return an array of informaiton about + // the plugin. + $info = array_merge($defaults, (include $path . '/plugin.php')); + $info['install_path'] = $install_path; + + // XXX: Ensure 'id' key isset + static::$plugin_info[$install_path] = $info; + } + return static::$plugin_info[$install_path]; } function getInstance($path) { @@ -215,17 +261,22 @@ class PluginManager { * registered in the plugin registry -- the %plugin table. */ function install($path) { - if (!($info = $this->getInfoForPath($path))) + $is_phar = substr($path, strlen($path) - 5) == '.phar'; + if (!($info = $this->getInfoForPath(INCLUDE_DIR . $path, $is_phar))) return false; $sql='INSERT INTO '.PLUGIN_TABLE.' SET installed=NOW() ' .', install_path='.db_input($path) - .', name='.db_input($info['name']); + .', name='.db_input($info['name']) + .', isphar='.db_input($is_phar); if (!db_query($sql) || !db_affected_rows()) return false; + static::clearCache(); + return true; + } + static function clearCache() { static::$plugin_list = array(); - return true; } } @@ -255,7 +306,8 @@ class Plugin { `id`='.db_input($this->id); if (($res = db_query($sql)) && ($ht=db_fetch_array($res))) $this->ht = $ht; - $this->info = PluginManager::getInfoForPath($this->ht['install_path']); + $this->info = PluginManager::getInfoForPath($this->ht['install_path'], + $this->isPhar()); } function getId() { return $this->id; } @@ -283,6 +335,7 @@ class Plugin { $sql = 'DELETE FROM '.PLUGIN_TABLE .' WHERE id='.db_input($this->getId()); + PluginManager::clearCache(); if (db_query($sql) && db_affected_rows()) return $this->getConfig()->purge(); return false; @@ -302,12 +355,14 @@ class Plugin { function enable() { $sql = 'UPDATE '.PLUGIN_TABLE .' SET isactive=1 WHERE id='.db_input($this->getId()); + PluginManager::clearCache(); return (db_query($sql) && db_affected_rows()); } function disable() { $sql = 'UPDATE '.PLUGIN_TABLE .' SET isactive=0 WHERE id='.db_input($this->getId()); + PluginManager::clearCache(); return (db_query($sql) && db_affected_rows()); } diff --git a/include/class.signal.php b/include/class.signal.php index f931acebc50c8beaf020859279c2ba0cba56f1de..928c15c4d2392ae767a0c465b6730d859f3dc85f 100644 --- a/include/class.signal.php +++ b/include/class.signal.php @@ -93,7 +93,7 @@ class Signal { list($s, $callable, $check) = $sub; if ($s && !is_a($object, $s)) continue; - elseif ($check && !call_user_func($check, $data)) + elseif ($check && !call_user_func($check, $object, $data)) continue; call_user_func($callable, $object, $data); } diff --git a/include/class.staff.php b/include/class.staff.php index 3883ab3636b0d32dfb4b4b0108edb7e685cda6ce..efd4f341c03f40cb3fa79756cc038533b7c86c7f 100644 --- a/include/class.staff.php +++ b/include/class.staff.php @@ -65,6 +65,7 @@ class Staff extends AuthenticatedUser { $this->teams = $this->ht['teams'] = array(); $this->group = $this->dept = null; $this->departments = $this->stats = array(); + $this->config = new Config('staff.'.$this->id); //WE have to patch info here to support upgrading from old versions. if(($time=strtotime($this->ht['passwdreset']?$this->ht['passwdreset']:$this->ht['added']))) @@ -91,7 +92,7 @@ class Staff extends AuthenticatedUser { } function getInfo() { - return $this->getHastable(); + return $this->config->getInfo() + $this->getHastable(); } /*compares user password*/ @@ -255,6 +256,17 @@ class Staff extends AuthenticatedUser { return $this->dept; } + function getLanguage() { + static $cached = false; + if (!$cached) $cached = &$_SESSION['staff:lang']; + + if (!$cached) { + $cached = $this->config->get('lang'); + if (!$cached) + $cached = Internationalization::getDefaultLanguage(); + } + return $cached; + } function isManager() { return (($dept=$this->getDept()) && $dept->getManagerId()==$this->getId()); @@ -466,6 +478,9 @@ class Staff extends AuthenticatedUser { if($errors) return false; + $this->config->set('lang', $vars['lang']); + $_SESSION['staff:lang'] = null; + $sql='UPDATE '.STAFF_TABLE.' SET updated=NOW() ' .' ,firstname='.db_input($vars['firstname']) .' ,lastname='.db_input($vars['lastname']) diff --git a/include/class.template.php b/include/class.template.php index e135034cd4943d45190d70b710e9868f4df6a43f..70af7fce0d960ce494638398869ecfd34b858add 100644 --- a/include/class.template.php +++ b/include/class.template.php @@ -118,7 +118,7 @@ class EmailTemplateGroup { } function getLanguage() { - return 'en_US'; + return $this->ht['lang']; } function isInUse(){ @@ -320,6 +320,10 @@ class EmailTemplateGroup { .' ,isactive='.db_input($vars['isactive']) .' ,notes='.db_input(Format::sanitize($vars['notes'])); + if ($vars['lang_id']) + // TODO: Validation of lang_id + $sql .= ',lang='.db_input($vars['lang_id']); + if($id) { $sql='UPDATE '.EMAIL_TEMPLATE_GRP_TABLE.' SET '.$sql.' WHERE tpl_id='.db_input($id); if(db_query($sql)) diff --git a/include/class.thread.php b/include/class.thread.php index 3ebdc83a6729e9130134c256040d246b589512bb..5e6c88ea7a9c9ccdb6a4074d0c550729136662f0 100644 --- a/include/class.thread.php +++ b/include/class.thread.php @@ -746,7 +746,7 @@ Class ThreadEntry { $subject = $mailinfo['subject']; $match = array(); if ($subject && $mailinfo['email'] - && preg_match("/\[#([0-9]{1,10})\]/", $subject, $match) + && preg_match("/#[\p{L}-]+?([0-9]{1,10})/u", $subject, $match) && ($tid = Ticket::getIdByExtId((int)$match[1], $mailinfo['email'])) ) // Return last message for the thread diff --git a/include/class.ticket.php b/include/class.ticket.php index bb07895ae17cb779e64ccad3a0c71aa8452350ca..97d83f293029e7c916afafabe510b414431fe5c7 100644 --- a/include/class.ticket.php +++ b/include/class.ticket.php @@ -1486,7 +1486,6 @@ class Ticket { && ($tpl = $dept->getTemplate()) && ($msg = $tpl->getNewMessageAlertMsgTemplate())) { - $attachments = $message->getAttachments(); $msg = $this->replaceVars($msg->asArray(), $variables); //Build list of recipients and fire the alerts. @@ -1508,7 +1507,7 @@ class Ticket { foreach( $recipients as $k=>$staff) { if(!$staff || !$staff->getEmail() || !$staff->isAvailable() || in_array($staff->getEmail(), $sentlist)) continue; $alert = $this->replaceVars($msg, array('recipient' => $staff)); - $email->sendAlert($staff->getEmail(), $alert['subj'], $alert['body'], $attachments, $options); + $email->sendAlert($staff->getEmail(), $alert['subj'], $alert['body'], null, $options); $sentlist[] = $staff->getEmail(); } } @@ -1557,8 +1556,6 @@ class Ticket { else $signature=''; - $attachments =($cfg->emailAttachments() && $files)?$response->getAttachments():array(); - $msg = $this->replaceVars($msg->asArray(), array('response' => $response, 'signature' => $signature)); @@ -1718,8 +1715,6 @@ class Ticket { && ($tpl = $dept->getTemplate()) && ($msg=$tpl->getNoteAlertMsgTemplate())) { - $attachments = $note->getAttachments(); - $msg = $this->replaceVars($msg->asArray(), array('note' => $note)); @@ -1738,7 +1733,6 @@ class Ticket { if($cfg->alertDeptManagerONNewNote() && $dept && $dept->getManagerId()) $recipients[]=$dept->getManager(); - $attachments = $note->getAttachments(); $options = array( 'inreplyto'=>$note->getEmailMessageId(), 'references'=>$note->getEmailReferences()); @@ -1750,7 +1744,7 @@ class Ticket { || $note->getStaffId() == $staff->getId()) //No need to alert the poster! continue; $alert = $this->replaceVars($msg, array('recipient' => $staff)); - $email->sendAlert($staff->getEmail(), $alert['subj'], $alert['body'], $attachments, $options); + $email->sendAlert($staff->getEmail(), $alert['subj'], $alert['body'], null, $options); $sentlist[] = $staff->getEmail(); } } diff --git a/include/client/header.inc.php b/include/client/header.inc.php index 7266060fbf2f50160e54b1a275f3b4e90fafc87e..9f20098b4b1b4fcfc2d891973717c42876296fde 100644 --- a/include/client/header.inc.php +++ b/include/client/header.inc.php @@ -3,6 +3,7 @@ $title=($cfg && is_object($cfg) && $cfg->getTitle())?$cfg->getTitle():'osTicket header("Content-Type: text/html; charset=UTF-8\r\n"); ?> <!DOCTYPE html> +<html> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"> diff --git a/include/i18n/en_US/help/tips/install.yaml b/include/i18n/en_US/help/tips/install.yaml index 2cb86b36dfeee5b7a282c2e797f4fd992fa06234..b65340da0096ba4152e4660bbc639b058a3b340f 100644 --- a/include/i18n/en_US/help/tips/install.yaml +++ b/include/i18n/en_US/help/tips/install.yaml @@ -20,6 +20,16 @@ system_email: <p>Default email address e.g support@yourcompany.com - you can add more later!</p> +default_lang: + title: Default System Language + content: | + <p>Initial data for this language will be installed into the + database. For instance email templates and default system pages will + be installed for this language.</p> + links: + - title: osTicket Language Packs + href: http://osticket.com/download?product=langs + first_name: title: First Name content: | diff --git a/include/i18n/langs.php b/include/i18n/langs.php new file mode 100644 index 0000000000000000000000000000000000000000..09957c42be514963c8d7a40f99051b996e728b42 --- /dev/null +++ b/include/i18n/langs.php @@ -0,0 +1,735 @@ +<?php +/** + * @author Phil Teare + * using wikipedia data + */ +return array( + "ab" => array( + "name" => "Abkhaz", + "nativeName" => "аҧсуа" + ), + "aa" => array( + "name" => "Afar", + "nativeName" => "Afaraf" + ), + "af" => array( + "name" => "Afrikaans", + "nativeName" => "Afrikaans" + ), + "ak" => array( + "name" => "Akan", + "nativeName" => "Akan" + ), + "sq" => array( + "name" => "Albanian", + "nativeName" => "Shqip" + ), + "am" => array( + "name" => "Amharic", + "nativeName" => "አማርኛ" + ), + "ar" => array( + "name" => "Arabic", + "nativeName" => "العربية" + ), + "an" => array( + "name" => "Aragonese", + "nativeName" => "Aragonés" + ), + "hy" => array( + "name" => "Armenian", + "nativeName" => "Հայերեն" + ), + "as" => array( + "name" => "Assamese", + "nativeName" => "অসমীয়া" + ), + "av" => array( + "name" => "Avaric", + "nativeName" => "авар мацӀ, магӀарул мацӀ" + ), + "ae" => array( + "name" => "Avestan", + "nativeName" => "avesta" + ), + "ay" => array( + "name" => "Aymara", + "nativeName" => "aymar aru" + ), + "az" => array( + "name" => "Azerbaijani", + "nativeName" => "azərbaycan dili" + ), + "bm" => array( + "name" => "Bambara", + "nativeName" => "bamanankan" + ), + "ba" => array( + "name" => "Bashkir", + "nativeName" => "башҡорт теле" + ), + "eu" => array( + "name" => "Basque", + "nativeName" => "euskara, euskera" + ), + "be" => array( + "name" => "Belarusian", + "nativeName" => "Беларуская" + ), + "bn" => array( + "name" => "Bengali", + "nativeName" => "বাংলা" + ), + "bh" => array( + "name" => "Bihari", + "nativeName" => "भोजपुरी" + ), + "bi" => array( + "name" => "Bislama", + "nativeName" => "Bislama" + ), + "bs" => array( + "name" => "Bosnian", + "nativeName" => "bosanski jezik" + ), + "br" => array( + "name" => "Breton", + "nativeName" => "brezhoneg" + ), + "bg" => array( + "name" => "Bulgarian", + "nativeName" => "български език" + ), + "my" => array( + "name" => "Burmese", + "nativeName" => "ဗမာစာ" + ), + "ca" => array( + "name" => "Catalan; Valencian", + "nativeName" => "Català" + ), + "ch" => array( + "name" => "Chamorro", + "nativeName" => "Chamoru" + ), + "ce" => array( + "name" => "Chechen", + "nativeName" => "нохчийн мотт" + ), + "ny" => array( + "name" => "Chichewa; Chewa; Nyanja", + "nativeName" => "chiCheŵa, chinyanja" + ), + "zh" => array( + "name" => "Chinese", + "nativeName" => "中文 (Zhōngwén), 汉语, 漢語" + ), + "cv" => array( + "name" => "Chuvash", + "nativeName" => "чӑваш чӗлхи" + ), + "kw" => array( + "name" => "Cornish", + "nativeName" => "Kernewek" + ), + "co" => array( + "name" => "Corsican", + "nativeName" => "corsu, lingua corsa" + ), + "cr" => array( + "name" => "Cree", + "nativeName" => "ᓀᐦᐃᔭᐍᐏᐣ" + ), + "hr" => array( + "name" => "Croatian", + "nativeName" => "hrvatski" + ), + "cs" => array( + "name" => "Czech", + "nativeName" => "česky, čeština" + ), + "da" => array( + "name" => "Danish", + "nativeName" => "dansk" + ), + "dv" => array( + "name" => "Divehi; Dhivehi; Maldivian;", + "nativeName" => "ދިވެހި" + ), + "nl" => array( + "name" => "Dutch", + "nativeName" => "Nederlands, Vlaams" + ), + "en" => array( + "name" => "English", + "nativeName" => "English" + ), + "eo" => array( + "name" => "Esperanto", + "nativeName" => "Esperanto" + ), + "et" => array( + "name" => "Estonian", + "nativeName" => "eesti, eesti keel" + ), + "ee" => array( + "name" => "Ewe", + "nativeName" => "Eʋegbe" + ), + "fo" => array( + "name" => "Faroese", + "nativeName" => "føroyskt" + ), + "fj" => array( + "name" => "Fijian", + "nativeName" => "vosa Vakaviti" + ), + "fi" => array( + "name" => "Finnish", + "nativeName" => "suomi, suomen kieli" + ), + "fr" => array( + "name" => "French", + "nativeName" => "français, langue française" + ), + "ff" => array( + "name" => "Fula; Fulah; Pulaar; Pular", + "nativeName" => "Fulfulde, Pulaar, Pular" + ), + "gl" => array( + "name" => "Galician", + "nativeName" => "Galego" + ), + "ka" => array( + "name" => "Georgian", + "nativeName" => "ქართული" + ), + "de" => array( + "name" => "German", + "nativeName" => "Deutsch" + ), + "el" => array( + "name" => "Greek, Modern", + "nativeName" => "Ελληνικά" + ), + "gn" => array( + "name" => "Guaraní", + "nativeName" => "Avañeẽ" + ), + "gu" => array( + "name" => "Gujarati", + "nativeName" => "ગુજરાતી" + ), + "ht" => array( + "name" => "Haitian; Haitian Creole", + "nativeName" => "Kreyòl ayisyen" + ), + "ha" => array( + "name" => "Hausa", + "nativeName" => "Hausa, هَوُسَ" + ), + "he" => array( + "name" => "Hebrew (modern)", + "nativeName" => "עברית" + ), + "hz" => array( + "name" => "Herero", + "nativeName" => "Otjiherero" + ), + "hi" => array( + "name" => "Hindi", + "nativeName" => "हिन्दी, हिंदी" + ), + "ho" => array( + "name" => "Hiri Motu", + "nativeName" => "Hiri Motu" + ), + "hu" => array( + "name" => "Hungarian", + "nativeName" => "Magyar" + ), + "ia" => array( + "name" => "Interlingua", + "nativeName" => "Interlingua" + ), + "id" => array( + "name" => "Indonesian", + "nativeName" => "Bahasa Indonesia" + ), + "ie" => array( + "name" => "Interlingue", + "nativeName" => "Originally called Occidental; then Interlingue after WWII" + ), + "ga" => array( + "name" => "Irish", + "nativeName" => "Gaeilge" + ), + "ig" => array( + "name" => "Igbo", + "nativeName" => "Asụsụ Igbo" + ), + "ik" => array( + "name" => "Inupiaq", + "nativeName" => "Iñupiaq, Iñupiatun" + ), + "io" => array( + "name" => "Ido", + "nativeName" => "Ido" + ), + "is" => array( + "name" => "Icelandic", + "nativeName" => "Íslenska" + ), + "it" => array( + "name" => "Italian", + "nativeName" => "Italiano" + ), + "iu" => array( + "name" => "Inuktitut", + "nativeName" => "ᐃᓄᒃᑎᑐᑦ" + ), + "ja" => array( + "name" => "Japanese", + "nativeName" => "日本語 (にほんご/にっぽんご)" + ), + "jv" => array( + "name" => "Javanese", + "nativeName" => "basa Jawa" + ), + "kl" => array( + "name" => "Kalaallisut, Greenlandic", + "nativeName" => "kalaallisut, kalaallit oqaasii" + ), + "kn" => array( + "name" => "Kannada", + "nativeName" => "ಕನ್ನಡ" + ), + "kr" => array( + "name" => "Kanuri", + "nativeName" => "Kanuri" + ), + "ks" => array( + "name" => "Kashmiri", + "nativeName" => "कश्मीरी, كشميري" + ), + "kk" => array( + "name" => "Kazakh", + "nativeName" => "Қазақ тілі" + ), + "km" => array( + "name" => "Khmer", + "nativeName" => "ភាសាខ្មែរ" + ), + "ki" => array( + "name" => "Kikuyu, Gikuyu", + "nativeName" => "Gĩkũyũ" + ), + "rw" => array( + "name" => "Kinyarwanda", + "nativeName" => "Ikinyarwanda" + ), + "ky" => array( + "name" => "Kirghiz, Kyrgyz", + "nativeName" => "кыргыз тили" + ), + "kv" => array( + "name" => "Komi", + "nativeName" => "коми кыв" + ), + "kg" => array( + "name" => "Kongo", + "nativeName" => "KiKongo" + ), + "ko" => array( + "name" => "Korean", + "nativeName" => "한국어 (韓國語), 조선말 (朝鮮語)" + ), + "ku" => array( + "name" => "Kurdish", + "nativeName" => "Kurdî, كوردی" + ), + "kj" => array( + "name" => "Kwanyama, Kuanyama", + "nativeName" => "Kuanyama" + ), + "la" => array( + "name" => "Latin", + "nativeName" => "latine, lingua latina" + ), + "lb" => array( + "name" => "Luxembourgish, Letzeburgesch", + "nativeName" => "Lëtzebuergesch" + ), + "lg" => array( + "name" => "Luganda", + "nativeName" => "Luganda" + ), + "li" => array( + "name" => "Limburgish, Limburgan, Limburger", + "nativeName" => "Limburgs" + ), + "ln" => array( + "name" => "Lingala", + "nativeName" => "Lingála" + ), + "lo" => array( + "name" => "Lao", + "nativeName" => "ພາສາລາວ" + ), + "lt" => array( + "name" => "Lithuanian", + "nativeName" => "lietuvių kalba" + ), + "lu" => array( + "name" => "Luba-Katanga", + "nativeName" => "" + ), + "lv" => array( + "name" => "Latvian", + "nativeName" => "latviešu valoda" + ), + "gv" => array( + "name" => "Manx", + "nativeName" => "Gaelg, Gailck" + ), + "mk" => array( + "name" => "Macedonian", + "nativeName" => "македонски јазик" + ), + "mg" => array( + "name" => "Malagasy", + "nativeName" => "Malagasy fiteny" + ), + "ms" => array( + "name" => "Malay", + "nativeName" => "bahasa Melayu, بهاس ملايو" + ), + "ml" => array( + "name" => "Malayalam", + "nativeName" => "മലയാളം" + ), + "mt" => array( + "name" => "Maltese", + "nativeName" => "Malti" + ), + "mi" => array( + "name" => "Māori", + "nativeName" => "te reo Māori" + ), + "mr" => array( + "name" => "Marathi (Marāṭhī)", + "nativeName" => "मराठी" + ), + "mh" => array( + "name" => "Marshallese", + "nativeName" => "Kajin M̧ajeļ" + ), + "mn" => array( + "name" => "Mongolian", + "nativeName" => "монгол" + ), + "na" => array( + "name" => "Nauru", + "nativeName" => "Ekakairũ Naoero" + ), + "nv" => array( + "name" => "Navajo, Navaho", + "nativeName" => "Diné bizaad, Dinékʼehǰí" + ), + "nb" => array( + "name" => "Norwegian Bokmål", + "nativeName" => "Norsk bokmål" + ), + "nd" => array( + "name" => "North Ndebele", + "nativeName" => "isiNdebele" + ), + "ne" => array( + "name" => "Nepali", + "nativeName" => "नेपाली" + ), + "ng" => array( + "name" => "Ndonga", + "nativeName" => "Owambo" + ), + "nn" => array( + "name" => "Norwegian Nynorsk", + "nativeName" => "Norsk nynorsk" + ), + "no" => array( + "name" => "Norwegian", + "nativeName" => "Norsk" + ), + "ii" => array( + "name" => "Nuosu", + "nativeName" => "ꆈꌠ꒿ Nuosuhxop" + ), + "nr" => array( + "name" => "South Ndebele", + "nativeName" => "isiNdebele" + ), + "oc" => array( + "name" => "Occitan", + "nativeName" => "Occitan" + ), + "oj" => array( + "name" => "Ojibwe, Ojibwa", + "nativeName" => "ᐊᓂᔑᓈᐯᒧᐎᓐ" + ), + "cu" => array( + "name" => "Old Church Slavonic, Church Slavic, Church Slavonic, Old Bulgarian, Old Slavonic", + "nativeName" => "ѩзыкъ словѣньскъ" + ), + "om" => array( + "name" => "Oromo", + "nativeName" => "Afaan Oromoo" + ), + "or" => array( + "name" => "Oriya", + "nativeName" => "ଓଡ଼ିଆ" + ), + "os" => array( + "name" => "Ossetian, Ossetic", + "nativeName" => "ирон æвзаг" + ), + "pa" => array( + "name" => "Panjabi, Punjabi", + "nativeName" => "ਪੰਜਾਬੀ, پنجابی" + ), + "pi" => array( + "name" => "Pāli", + "nativeName" => "पाऴि" + ), + "fa" => array( + "name" => "Persian", + "nativeName" => "فارسی" + ), + "pl" => array( + "name" => "Polish", + "nativeName" => "polski" + ), + "ps" => array( + "name" => "Pashto, Pushto", + "nativeName" => "پښتو" + ), + "pt" => array( + "name" => "Portuguese", + "nativeName" => "Português" + ), + "qu" => array( + "name" => "Quechua", + "nativeName" => "Runa Simi, Kichwa" + ), + "rm" => array( + "name" => "Romansh", + "nativeName" => "rumantsch grischun" + ), + "rn" => array( + "name" => "Kirundi", + "nativeName" => "kiRundi" + ), + "ro" => array( + "name" => "Romanian, Moldavian, Moldovan", + "nativeName" => "română" + ), + "ru" => array( + "name" => "Russian", + "nativeName" => "русский язык" + ), + "sa" => array( + "name" => "Sanskrit (Saṁskṛta)", + "nativeName" => "संस्कृतम्" + ), + "sc" => array( + "name" => "Sardinian", + "nativeName" => "sardu" + ), + "sd" => array( + "name" => "Sindhi", + "nativeName" => "सिन्धी, سنڌي، سندھی" + ), + "se" => array( + "name" => "Northern Sami", + "nativeName" => "Davvisámegiella" + ), + "sm" => array( + "name" => "Samoan", + "nativeName" => "gagana faa Samoa" + ), + "sg" => array( + "name" => "Sango", + "nativeName" => "yângâ tî sängö" + ), + "sr" => array( + "name" => "Serbian", + "nativeName" => "српски језик" + ), + "gd" => array( + "name" => "Scottish Gaelic; Gaelic", + "nativeName" => "Gàidhlig" + ), + "sn" => array( + "name" => "Shona", + "nativeName" => "chiShona" + ), + "si" => array( + "name" => "Sinhala, Sinhalese", + "nativeName" => "සිංහල" + ), + "sk" => array( + "name" => "Slovak", + "nativeName" => "slovenčina" + ), + "sl" => array( + "name" => "Slovene", + "nativeName" => "slovenščina" + ), + "so" => array( + "name" => "Somali", + "nativeName" => "Soomaaliga, af Soomaali" + ), + "st" => array( + "name" => "Southern Sotho", + "nativeName" => "Sesotho" + ), + "es" => array( + "name" => "Spanish; Castilian", + "nativeName" => "español, castellano" + ), + "su" => array( + "name" => "Sundanese", + "nativeName" => "Basa Sunda" + ), + "sw" => array( + "name" => "Swahili", + "nativeName" => "Kiswahili" + ), + "ss" => array( + "name" => "Swati", + "nativeName" => "SiSwati" + ), + "sv" => array( + "name" => "Swedish", + "nativeName" => "svenska" + ), + "ta" => array( + "name" => "Tamil", + "nativeName" => "தமிழ்" + ), + "te" => array( + "name" => "Telugu", + "nativeName" => "తెలుగు" + ), + "tg" => array( + "name" => "Tajik", + "nativeName" => "тоҷикӣ, toğikī, تاجیکی" + ), + "th" => array( + "name" => "Thai", + "nativeName" => "ไทย" + ), + "ti" => array( + "name" => "Tigrinya", + "nativeName" => "ትግርኛ" + ), + "bo" => array( + "name" => "Tibetan Standard, Tibetan, Central", + "nativeName" => "བོད་ཡིག" + ), + "tk" => array( + "name" => "Turkmen", + "nativeName" => "Türkmen, Түркмен" + ), + "tl" => array( + "name" => "Tagalog", + "nativeName" => "Wikang Tagalog, ᜏᜒᜃᜅ᜔ ᜆᜄᜎᜓᜄ᜔" + ), + "tn" => array( + "name" => "Tswana", + "nativeName" => "Setswana" + ), + "to" => array( + "name" => "Tonga (Tonga Islands)", + "nativeName" => "faka Tonga" + ), + "tr" => array( + "name" => "Turkish", + "nativeName" => "Türkçe" + ), + "ts" => array( + "name" => "Tsonga", + "nativeName" => "Xitsonga" + ), + "tt" => array( + "name" => "Tatar", + "nativeName" => "татарча, tatarça, تاتارچا" + ), + "tw" => array( + "name" => "Twi", + "nativeName" => "Twi" + ), + "ty" => array( + "name" => "Tahitian", + "nativeName" => "Reo Tahiti" + ), + "ug" => array( + "name" => "Uighur, Uyghur", + "nativeName" => "Uyƣurqə, ئۇيغۇرچە" + ), + "uk" => array( + "name" => "Ukrainian", + "nativeName" => "українська" + ), + "ur" => array( + "name" => "Urdu", + "nativeName" => "اردو" + ), + "uz" => array( + "name" => "Uzbek", + "nativeName" => "zbek, Ўзбек, أۇزبېك" + ), + "ve" => array( + "name" => "Venda", + "nativeName" => "Tshivenḓa" + ), + "vi" => array( + "name" => "Vietnamese", + "nativeName" => "Tiếng Việt" + ), + "vo" => array( + "name" => "Volapük", + "nativeName" => "Volapük" + ), + "wa" => array( + "name" => "Walloon", + "nativeName" => "Walon" + ), + "cy" => array( + "name" => "Welsh", + "nativeName" => "Cymraeg" + ), + "wo" => array( + "name" => "Wolof", + "nativeName" => "Wollof" + ), + "fy" => array( + "name" => "Western Frisian", + "nativeName" => "Frysk" + ), + "xh" => array( + "name" => "Xhosa", + "nativeName" => "isiXhosa" + ), + "yi" => array( + "name" => "Yiddish", + "nativeName" => "ייִדיש" + ), + "yo" => array( + "name" => "Yoruba", + "nativeName" => "Yorùbá" + ), + "za" => array( + "name" => "Zhuang, Chuang", + "nativeName" => "Saɯ cueŋƅ, Saw cuengh" + ) +); diff --git a/include/mysqli.php b/include/mysqli.php index 34fa57479b64ebcda7e1ac381f4c70a6069773aa..7245d9ec20eeab75197500a8b9f5d8f06af8e567 100644 --- a/include/mysqli.php +++ b/include/mysqli.php @@ -39,18 +39,24 @@ function db_connect($host, $user, $passwd, $options = array()) { return NULL; $port = ini_get("mysqli.default_port"); + $socket = ini_get("mysqli.default_socket"); if (strpos($host, ':') !== false) { - list($host, $port) = explode(':', $host); + list($host, $portspec) = explode(':', $host); // PHP may not honor the port number if connecting to 'localhost' - if (!strcasecmp($host, 'localhost')) - // XXX: Looks like PHP gethostbyname() is IPv4 only - $host = gethostbyname($host); - $port = (int) $port; + if ($portspec && is_numeric($portspec)) { + if (!strcasecmp($host, 'localhost')) + // XXX: Looks like PHP gethostbyname() is IPv4 only + $host = gethostbyname($host); + $port = (int) $portspec; + } + elseif ($portspec) { + $socket = $portspec; + } } // Connect $start = microtime(true); - if (!@$__db->real_connect($host, $user, $passwd, null, $port)) + if (!@$__db->real_connect($host, $user, $passwd, null, $port, $socket)) return NULL; //Select the database, if any. diff --git a/include/pear/Net/DNS2/Packet.php b/include/pear/Net/DNS2/Packet.php index ccd633eb7d2dbbbde819a02c40ae369f95b0f33a..12c67063c3f741d0283d30c776b43c4027e54935 100644 --- a/include/pear/Net/DNS2/Packet.php +++ b/include/pear/Net/DNS2/Packet.php @@ -190,7 +190,7 @@ class Net_DNS2_Packet /** * applies a standard DNS name compression on the given name/offset * - * This logic was based on the Net::DNS::Packet::dn_comp() function + * This logic was based on the Net::DNS::Packet::dn_comp() function (nolint) * by Michanel Fuhr * * @param string $name the name to be compressed @@ -250,7 +250,7 @@ class Net_DNS2_Packet /** * applies a standard DNS name compression on the given name/offset * - * This logic was based on the Net::DNS::Packet::dn_comp() function + * This logic was based on the Net::DNS::Packet::dn_comp() function (nolint) * by Michanel Fuhr * * @param string $name the name to be compressed @@ -283,7 +283,7 @@ class Net_DNS2_Packet /** * expands the domain name stored at a given offset in a DNS Packet * - * This logic was based on the Net::DNS::Packet::dn_expand() function + * This logic was based on the Net::DNS::Packet::dn_expand() function (nolint) * by Michanel Fuhr * * @param Net_DNS2_Packet &$packet the DNS packet to look in for the domain name diff --git a/include/staff/header.inc.php b/include/staff/header.inc.php index ded810d8996c70fed985b75c690abdb5f9e8974b..d2a3e43b82277bb13eb38718e06bfd84340457f9 100644 --- a/include/staff/header.inc.php +++ b/include/staff/header.inc.php @@ -2,6 +2,7 @@ <html> <head> <meta http-equiv="content-type" content="text/html; charset=UTF-8"> + <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"> <meta http-equiv="cache-control" content="no-cache" /> <meta http-equiv="pragma" content="no-cache" /> <title><?php echo ($ost && ($title=$ost->getPageTitle()))?$title:'osTicket :: Staff Control Panel'; ?></title> diff --git a/include/staff/plugins.inc.php b/include/staff/plugins.inc.php index eab4a796ee6a3240cd30d35fac1e20f2071b17f0..4b9c5bccc3d83a0474fc4e953b8dfb5c161d0ef9 100644 --- a/include/staff/plugins.inc.php +++ b/include/staff/plugins.inc.php @@ -35,19 +35,11 @@ foreach ($ost->plugins->allInstalled() as $p) { <?php echo $sel?'checked="checked"':''; ?>></td> <td><a href="plugins.php?id=<?php echo $p->getId(); ?>" ><?php echo $p->getName(); ?></a></td> - <td>Enabled</td> + <td><?php echo ($p->isActive()) + ? 'Enabled' : '<strong>Disabled</strong>'; ?></td> <td><?php echo Format::db_datetime($p->getInstallDate()); ?></td> </tr> - <?php } else { - $p = $ost->plugins->getInfoForPath($p['install_path']); ?> - <tr> - <td><input type="checkbox" class="ckb" name="ids[]" value="<?php echo $p['install_path']; ?>" - <?php echo $sel?'checked="checked"':''; ?>></td> - <td><?php echo $p['name']; ?></td> - <td><strong>Disabled</strong></td> - <td></td> - </tr> - <?php } ?> + <?php } else {} ?> <?php } ?> </tbody> <tfoot> diff --git a/include/staff/profile.inc.php b/include/staff/profile.inc.php index 8ceaca328452056ad3829a00ae69f64c85dae52e..116a39eab0617425965850687298f47999bfb77d 100644 --- a/include/staff/profile.inc.php +++ b/include/staff/profile.inc.php @@ -100,6 +100,24 @@ $info['id']=$staff->getId(); <span class="error">* <?php echo $errors['timezone_id']; ?></span> </td> </tr> + <tr> + <td width="180"> + Preferred Language: + </td> + <td> + <?php + $langs = Internationalization::availableLanguages(); ?> + <select name="lang"> + <option value="">— Use Browser Preference —</option> +<?php foreach($langs as $l) { + $selected = ($info['lang'] == $l['code']) ? 'selected="selected"' : ''; ?> + <option value="<?php echo $l['code']; ?>" <?php echo $selected; + ?>><?php echo $l['desc']; ?></option> +<?php } ?> + </select> + <span class="error"> <?php echo $errors['lang']; ?></span> + </td> + </tr> <tr> <td width="180"> Daylight Saving: diff --git a/include/staff/template.inc.php b/include/staff/template.inc.php index 8fee6a2ce6a8bae63c9273b99eafa178c48d0214..2d05235eecb07d2a20c5d389fc58b1aaf763a022 100644 --- a/include/staff/template.inc.php +++ b/include/staff/template.inc.php @@ -15,6 +15,7 @@ if($template && $_REQUEST['a']!='add'){ $action='add'; $submit_text='Add Template'; $info['isactive']=isset($info['isactive'])?$info['isactive']:0; + $info['lang_id'] = $cfg->getSystemLanguage(); $qstr.='&a='.urlencode($_REQUEST['a']); } $info=Format::htmlchars(($errors && $_POST)?$_POST:$info); @@ -54,19 +55,22 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info); <span class="error">* <?php echo $errors['isactive']; ?></span> </td> </tr> + <?php + if($template){ ?> <tr> <td width="180" class="required"> Language: </td> <td> - <select name="lang_id"> - <option value="en" selected="selected">English (US)</option> - </select> - <span class="error">* <?php echo $errors['lang_id']; ?></span> + <?php + $langs = Internationalization::availableLanguages(); + $lang = strtolower($info['lang']); + if (isset($langs[$lang])) + echo $langs[$lang]['desc']; + else + echo $info['lang']; ?> </td> </tr> - <?php - if($template){ ?> <tr> <th colspan="2"> <em><strong>Template Messages</strong>: Click on the message to edit. @@ -100,6 +104,23 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info); } } }else{ ?> + <tr> + <td width="180" class="required"> + Language: + </td> + <td> + <?php + $langs = Internationalization::availableLanguages(); ?> + <select name="lang_id"> +<?php foreach($langs as $l) { + $selected = ($info['lang_id'] == $l['code']) ? 'selected="selected"' : ''; ?> + <option value="<?php echo $l['code']; ?>" <?php echo $selected; + ?>><?php echo $l['desc']; ?></option> +<?php } ?> + </select> + <span class="error">* <?php echo $errors['lang_id']; ?></span> + </td> + </tr> <tr> <td width="180" class="required"> Template To Clone: diff --git a/include/staff/tickets.inc.php b/include/staff/tickets.inc.php index e463e515d88a0afa7c7e013f57adf9f411d0dc9a..1415ce76d059cb8617c26f7f66456e51ba9bf4b9 100644 --- a/include/staff/tickets.inc.php +++ b/include/staff/tickets.inc.php @@ -129,6 +129,9 @@ if($search): if (count($tickets)) $qwhere .= ' AND ticket.ticket_id IN ('. implode(',',db_input($tickets)).')'; + else + // No hits -- there should be an empty list of results + $qwhere .= ' AND false'; } } @@ -192,10 +195,7 @@ $$x=' class="'.strtolower($order).'" '; if($_GET['limit']) $qstr.='&limit='.urlencode($_GET['limit']); -$qselect ='SELECT DISTINCT ticket.ticket_id,lock_id,ticketID,ticket.dept_id,ticket.staff_id,ticket.team_id ' - .',MAX(IF(field.name = \'subject\', ans.value, NULL)) as `subject`' - .',MAX(IF(field.name = \'priority\', ans.value, NULL)) as `priority_desc`' - .',MAX(IF(field.name = \'priority\', ans.value_id, NULL)) as `priority_id`' +$qselect ='SELECT ticket.ticket_id,lock_id,ticketID,ticket.dept_id,ticket.staff_id,ticket.team_id ' .' ,user.name' .' ,email.address as email, dept_name ' .' ,ticket.status,ticket.source,isoverdue,isanswered,ticket.created '; @@ -203,18 +203,13 @@ $qselect ='SELECT DISTINCT ticket.ticket_id,lock_id,ticketID,ticket.dept_id,tick $qfrom=' FROM '.TICKET_TABLE.' ticket '. ' LEFT JOIN '.USER_TABLE.' user ON user.id = ticket.user_id'. ' LEFT JOIN '.USER_EMAIL_TABLE.' email ON user.id = email.user_id'. - ' LEFT JOIN '.DEPT_TABLE.' dept ON ticket.dept_id=dept.dept_id '. - ' LEFT JOIN '.FORM_ENTRY_TABLE.' entry ON entry.object_type=\'T\' - and entry.object_id=ticket.ticket_id'. - ' LEFT JOIN '.FORM_ANSWER_TABLE.' ans ON ans.entry_id = entry.id'. - ' LEFT JOIN '.FORM_FIELD_TABLE.' field ON field.id=ans.field_id'; + ' LEFT JOIN '.DEPT_TABLE.' dept ON ticket.dept_id=dept.dept_id '; $sjoin=''; if($search && $deep_search) { $sjoin=' LEFT JOIN '.TICKET_THREAD_TABLE.' thread ON (ticket.ticket_id=thread.ticket_id )'; } -$qgroup=' GROUP BY ticket.ticket_id'; //get ticket count based on the query so far.. $total=db_count("SELECT count(DISTINCT ticket.ticket_id) $qfrom $sjoin $qwhere"); //pagenate @@ -224,23 +219,23 @@ $pageNav=new Pagenate($total,$page,$pagelimit); $pageNav->setURL('tickets.php',$qstr.'&sort='.urlencode($_REQUEST['sort']).'&order='.urlencode($_REQUEST['order'])); //ADD attachment,priorities, lock and other crap -$qselect.=' ,count(attach.attach_id) as attachments ' - .' ,count(DISTINCT thread.id) as thread_count ' - .' ,IF(ticket.duedate IS NULL,IF(sla.id IS NULL, NULL, DATE_ADD(ticket.created, INTERVAL sla.grace_period HOUR)), ticket.duedate) as duedate ' +$qselect.=' ,IF(ticket.duedate IS NULL,IF(sla.id IS NULL, NULL, DATE_ADD(ticket.created, INTERVAL sla.grace_period HOUR)), ticket.duedate) as duedate ' .' ,CAST(GREATEST(IFNULL(ticket.lastmessage, 0), IFNULL(ticket.reopened, 0), ticket.created) as datetime) as effective_date ' .' ,CONCAT_WS(" ", staff.firstname, staff.lastname) as staff, team.name as team ' .' ,IF(staff.staff_id IS NULL,team.name,CONCAT_WS(" ", staff.lastname, staff.firstname)) as assigned ' - .' ,IF(ptopic.topic_pid IS NULL, topic.topic, CONCAT_WS(" / ", ptopic.topic, topic.topic)) as helptopic '; + .' ,IF(ptopic.topic_pid IS NULL, topic.topic, CONCAT_WS(" / ", ptopic.topic, topic.topic)) as helptopic ' + .' ,cdata.priority_id, cdata.subject'; $qfrom.=' 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) ' .' LEFT JOIN '.STAFF_TABLE.' staff ON (ticket.staff_id=staff.staff_id) ' .' LEFT JOIN '.TEAM_TABLE.' team ON (ticket.team_id=team.team_id) ' .' LEFT JOIN '.SLA_TABLE.' sla ON (ticket.sla_id=sla.id AND sla.isactive=1) ' .' LEFT JOIN '.TOPIC_TABLE.' topic ON (ticket.topic_id=topic.topic_id) ' - .' LEFT JOIN '.TOPIC_TABLE.' ptopic ON (ptopic.topic_id=topic.topic_pid) '; + .' LEFT JOIN '.TOPIC_TABLE.' ptopic ON (ptopic.topic_id=topic.topic_pid) ' + .' LEFT JOIN '.TABLE_PREFIX.'ticket__cdata cdata ON (cdata.ticket_id = ticket.ticket_id) '; + +TicketForm::ensureDynamicDataView(); // Fetch priority information $res = db_query('select * from '.PRIORITY_TABLE); @@ -248,7 +243,7 @@ $prios = array(); while ($row = db_fetch_array($res)) $prios[$row['priority_id']] = $row; -$query="$qselect $qfrom $qwhere $qgroup ORDER BY $order_by $order LIMIT ".$pageNav->getStart().",".$pageNav->getLimit(); +$query="$qselect $qfrom $qwhere ORDER BY $order_by $order LIMIT ".$pageNav->getStart().",".$pageNav->getLimit(); //echo $query; $hash = md5($query); $_SESSION['search_'.$hash] = $query; @@ -262,6 +257,27 @@ if($search) $negorder=$order=='DESC'?'ASC':'DESC'; //Negate the sorting.. +// Fetch the results +$results = array(); +while ($row = db_fetch_array($res)) { + $results[$row['ticket_id']] = $row; +} + +// Fetch attachment and thread entry counts +if ($results) { + $counts_sql = 'SELECT ticket.ticket_id, count(attach.attach_id) as attachments, + count(DISTINCT thread.id) as thread_count + FROM '.TICKET_TABLE.' ticket + 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) ' + .' WHERE ticket.ticket_id IN ('.implode(',', db_input(array_keys($results))).') + GROUP BY ticket.ticket_id'; + $ids_res = db_query($counts_sql); + while ($row = db_fetch_array($ids_res)) { + $results[$row['ticket_id']] += $row; + } +} + //YOU BREAK IT YOU FIX IT. ?> <!-- SEARCH FORM START --> @@ -345,9 +361,9 @@ $negorder=$order=='DESC'?'ASC':'DESC'; //Negate the sorting.. <?php $class = "row1"; $total=0; - if($res && ($num=db_num_rows($res))): + if($res && ($num=count($results))): $ids=($errors && $_POST['tids'] && is_array($_POST['tids']))?$_POST['tids']:null; - while ($row = db_fetch_array($res)) { + foreach ($results as $row) { $tag=$row['staff_id']?'assigned':'openticket'; $flag=null; if($row['lock_id']) diff --git a/include/upgrader/streams/core/c00511c7-7be60a84.patch.sql b/include/upgrader/streams/core/c00511c7-7be60a84.patch.sql index e565227249787174009fddfb6e7f06ed82f01d52..97c2bf97b45d6e5fa1368e8011d05e07d48c545e 100644 --- a/include/upgrader/streams/core/c00511c7-7be60a84.patch.sql +++ b/include/upgrader/streams/core/c00511c7-7be60a84.patch.sql @@ -224,7 +224,7 @@ CREATE TABLE `%TABLE_PREFIX%email_filter_rule` ( -- SYSTEM BAN LIST was the first filter created, with ID of '1' INSERT INTO `%TABLE_PREFIX%email_filter_rule` (`filter_id`, `what`, `how`, `val`) - SELECT LAST_INSERT_ID(), 'email', 'equals', email FROM `%TABLE_PREFIX%email_banlist`; + SELECT LAST_INSERT_ID(), 'email', 'equal', email FROM `%TABLE_PREFIX%email_banlist`; -- Create table session DROP TABLE IF EXISTS `%TABLE_PREFIX%session`; diff --git a/js/redactor-osticket.js b/js/redactor-osticket.js index 0fed815da1831b9425a22d6f2702766e1ca81362..337d525a5d1cbc5a85601313cf2e941b242d00d4 100644 --- a/js/redactor-osticket.js +++ b/js/redactor-osticket.js @@ -140,7 +140,12 @@ $(function() { var el = $(el), options = { 'air': el.hasClass('no-bar'), - 'airButtons': ['formatting', '|', 'bold', 'italic', 'deleted', '|', 'unorderedlist', 'orderedlist', 'outdent', 'indent', '|', 'image'], + 'airButtons': ['formatting', '|', 'bold', 'italic', 'underline', 'deleted', '|', 'unorderedlist', 'orderedlist', 'outdent', 'indent', '|', 'image'], + 'buttons': ['html', '|', 'formatting', '|', 'bold', + 'italic', 'underline', 'deleted', '|', 'unorderedlist', + 'orderedlist', 'outdent', 'indent', '|', 'image', 'video', + 'file', 'table', 'link', '|', 'alignment', '|', + 'horizontalrule'], 'autoresize': !el.hasClass('no-bar'), 'minHeight': el.hasClass('small') ? 75 : 150, 'focus': false, diff --git a/scp/ajax.php b/scp/ajax.php index 01b41867dd53bc052d5a7b9c8765327b140e3739..7e990934b8abe59b1b9a1aecfd3ccd91cfb06271 100644 --- a/scp/ajax.php +++ b/scp/ajax.php @@ -101,8 +101,8 @@ $dispatcher = patterns('', )), url_post('^/upgrader', array('ajax.upgrader.php:UpgraderAjaxAPI', 'upgrade')), url('^/help/', patterns('ajax.tips.php:HelpTipAjaxAPI', - url_get('tips/(?P<namespace>[\w_.]+)$', 'getTipsJson'), - url_get('(?P<lang>\w{2}_\w{2})?/tips/(?P<namespace>[\w_.]+)$', 'getTipsForLangJson') + url_get('^tips/(?P<namespace>[\w_.]+)$', 'getTipsJson'), + url_get('^(?P<lang>[\w_]+)?/tips/(?P<namespace>[\w_.]+)$', 'getTipsJsonForLang') )) ); diff --git a/scp/forms.php b/scp/forms.php index 5af2dd327781d18adca740b46ad654eb272929db..6f14be6c1a2bec06aea87c6f1c4c97d9db177efd 100644 --- a/scp/forms.php +++ b/scp/forms.php @@ -99,6 +99,7 @@ if($_POST) { 'private'=>$_POST["private-new-$i"] == 'on' ? 1 : 0, 'required'=>$_POST["required-new-$i"] == 'on' ? 1 : 0 )); + $field->setForm($form); if ($field->isValid()) $field->save(); else diff --git a/scp/plugins.php b/scp/plugins.php index c5e7e8918027296970b717289626fb4c5982d6ac..44dda73b80897b668caafe38638c163321a31e64 100644 --- a/scp/plugins.php +++ b/scp/plugins.php @@ -19,8 +19,8 @@ if($_POST) { $count = count($_POST['ids']); switch(strtolower($_POST['a'])) { case 'enable': - foreach ($_POST['ids'] as $path) { - if ($p = $ost->plugins->getInstance($path)) { + foreach ($_POST['ids'] as $id) { + if ($p = Plugin::lookup($id)) { $p->enable(); } } @@ -35,7 +35,6 @@ if($_POST) { case 'delete': foreach ($_POST['ids'] as $id) { if ($p = Plugin::lookup($id)) { - var_dump($p); $p->uninstall(); } } diff --git a/setup/ajax.php b/setup/ajax.php index 9c2c7b282a325f135f3dde4e8e80491860666b31..97e45daddc88ece9cd65e24ea71dfb9f0d7ac8b9 100644 --- a/setup/ajax.php +++ b/setup/ajax.php @@ -23,8 +23,8 @@ require_once INCLUDE_DIR.'/class.ajax.php'; $dispatcher = patterns('', url('^/help/', patterns('ajax.tips.php:HelpTipAjaxAPI', - url_get('tips/(?P<namespace>[\w_]+)$', 'getTipsJson'), - url_get('(?P<lang>\w{2}_\w{2})?/tips/(?P<namespace>[\w_]+)$', 'getTipsForLangJson') + url_get('^tips/(?P<namespace>[\w_.]+)$', 'getTipsJson'), + url_get('^(?P<lang>[\w_]+)?/tips/(?P<namespace>[\w_.]+)$', 'getTipsJsonForLang') )) ); print $dispatcher->resolve(Osticket::get_path_info()); diff --git a/setup/cli/cli.inc.php b/setup/cli/cli.inc.php new file mode 100644 index 0000000000000000000000000000000000000000..31bdbfe8993cdadadbc4896c1e9257b3909b8cc0 --- /dev/null +++ b/setup/cli/cli.inc.php @@ -0,0 +1,31 @@ +<?php +/********************************************************************* + cli.inc.php + + Master include file which must be included at the start of every file. + This is a modification of main.inc.php to support running cli scripts. + + 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. + + vim: expandtab sw=4 ts=4 sts=4: +**********************************************************************/ + +#Disable direct access. +if(!strcasecmp(basename($_SERVER['SCRIPT_NAME']),basename(__FILE__))) die('kwaheri rafiki!'); + +define('ROOT_PATH', '/'); +define('INC_DIR',dirname(__file__).'/../inc/'); //local include dir! + +require_once(dirname(__file__).'/../../bootstrap.php'); + +Bootstrap::loadConfig(); +Bootstrap::defineTables(TABLE_PREFIX); +Bootstrap::loadCode(); +Bootstrap::i18n_prep(); + +?> diff --git a/setup/cli/modules/class.module.php b/setup/cli/modules/class.module.php index 437f87c609eb6b63f6799978ce324f437a456ca5..421e49bd11a22f115af0e9d3c288099f07859e3f 100644 --- a/setup/cli/modules/class.module.php +++ b/setup/cli/modules/class.module.php @@ -227,6 +227,11 @@ class Module { function run($args, $options) { } + function fail($message) { + $this->stderr->write($message . "\n"); + die(); + } + /* static */ function register($action, $class) { global $registered_modules; diff --git a/setup/cli/modules/i18n.php b/setup/cli/modules/i18n.php new file mode 100644 index 0000000000000000000000000000000000000000..a9a4117c4b15f59d6914dbd11edf741b1c5544b7 --- /dev/null +++ b/setup/cli/modules/i18n.php @@ -0,0 +1,130 @@ +<?php + +require_once dirname(__file__) . "/class.module.php"; +require_once dirname(__file__) . "/../cli.inc.php"; +require_once INCLUDE_DIR . 'class.format.php'; + +class i18n_Compiler extends Module { + + var $prologue = "Manages translation files from Crowdin"; + + var $arguments = array( + "command" => "Action to be performed. + list - Show list of available translations" + ); + + var $options = array( + "key" => array('-k','--key','metavar'=>'API-KEY', + 'help'=>'Crowdin project API key. This can be omitted if + CROWDIN_API_KEY is defined in the ost-config.php file'), + "lang" => array('-L', '--lang', 'metavar'=>'code', + 'help'=>'Language code (used for building)'), + ); + + static $crowdin_api_url = 'http://i18n.osticket.com/api/project/osticket-official/{command}'; + + function _http_get($url) { + #curl post + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_USERAGENT, 'osTicket/'.THIS_VERSION); + curl_setopt($ch, CURLOPT_HEADER, FALSE); + curl_setopt($ch, CURLOPT_FOLLOWLOCATION, FALSE); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE); + $result=curl_exec($ch); + $code = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + return array($code, $result); + } + + function _request($command, $args=array()) { + + $url = str_replace('{command}', $command, self::$crowdin_api_url); + + $args += array('key' => $this->key); + foreach ($args as &$a) + $a = urlencode($a); + unset($a); + $url .= '?' . Format::array_implode('=', '&', $args); + + return $this->_http_get($url); + } + + function run($args, $options) { + $this->key = $options['key']; + if (!$this->key && defined('CROWDIN_API_KEY')) + $this->key = CROWDIN_API_KEY; + + switch (strtolower($args['command'])) { + case 'list': + if (!$this->key) + $this->fail('API key is required'); + $this->_list(); + break; + case 'build': + if (!$this->key) + $this->fail('API key is required'); + if (!$options['lang']) + $this->fail('Language code is required. See `list`'); + $this->_build($options['lang']); + break; + } + } + + function _list() { + error_reporting(E_ALL); + list($code, $body) = $this->_request('status'); + $d = new DOMDocument(); + $d->loadXML($body); + + $xp = new DOMXpath($d); + foreach ($xp->query('//language') as $c) { + $name = $code = ''; + foreach ($c->childNodes as $n) { + switch (strtolower($n->nodeName)) { + case 'name': + $name = $n->textContent; + break; + case 'code': + $code = $n->textContent; + break; + } + } + if (!$code) + continue; + $this->stdout->write(sprintf("%s (%s)\n", $code, $name)); + } + } + + function _build($lang) { + list($code, $zip) = $this->_request("download/$lang.zip"); + + if ($code !== 200) + $this->fail('Language is not available'."\n"); + + $temp = tempnam('/tmp', 'osticket-cli'); + $f = fopen($temp, 'w'); + fwrite($f, $zip); + fclose($f); + $zip = new ZipArchive(); + $zip->open($temp); + unlink($temp); + + $lang = str_replace('-','_',$lang); + @unlink(I18N_DIR."$lang.phar"); + $phar = new Phar(I18N_DIR."$lang.phar"); + + for ($i=0; $i<$zip->numFiles; $i++) { + $info = $zip->statIndex($i); + $phar->addFromString($info['name'], $zip->getFromIndex($i)); + } + + // TODO: Add i18n extras (like fonts) + + // TODO: Sign files + } +} + +Module::register('i18n', 'i18n_Compiler'); +?> diff --git a/setup/inc/class.installer.php b/setup/inc/class.installer.php index d57fa0b449095e9bb88a1cb3b9004dd0a1325c5a..3912a3a99a8d66049adb9de64b51b49e837c248d 100644 --- a/setup/inc/class.installer.php +++ b/setup/inc/class.installer.php @@ -15,6 +15,7 @@ **********************************************************************/ require_once INCLUDE_DIR.'class.migrater.php'; require_once INCLUDE_DIR.'class.setup.php'; +require_once INCLUDE_DIR.'class.i18n.php'; class Installer extends SetupWizard { @@ -81,9 +82,7 @@ class Installer extends SetupWizard { // Support port number specified in the hostname with a colon (:) list($host, $port) = explode(':', $vars['dbhost']); - if ($port && (!is_numeric($port) || !((int)$port))) - $this->errors['db'] = 'Database port number must be a number'; - elseif ($port && ($port < 1 || $port > 65535)) + if ($port && is_numeric($port) && ($port < 1 || $port > 65535)) $this->errors['db'] = 'Invalid database port number'; //MYSQL: Connect to the DB and check the version & database (create database if it doesn't exist!) @@ -149,10 +148,9 @@ class Installer extends SetupWizard { } if(!$this->errors) { - // TODO: Use language selected from install worksheet - require_once INCLUDE_DIR.'class.i18n.php'; - $i18n = new Internationalization('en_US'); + // TODO: Use language selected from install worksheet + $i18n = new Internationalization($vars['lang_id']); $i18n->loadDefaultData(); $sql='SELECT `id` FROM '.PREFIX.'sla ORDER BY `id` LIMIT 1'; diff --git a/setup/inc/install.inc.php b/setup/inc/install.inc.php index e2d6a5a0f3b87b28f7466a91780b2d5d932956fb..f58943a2fc450022077fd2d1ba59f752b636c66f 100644 --- a/setup/inc/install.inc.php +++ b/setup/inc/install.inc.php @@ -1,8 +1,8 @@ -<?php +<?php if(!defined('SETUPINC')) die('Kwaheri!'); -$info=($_POST && $errors)?Format::htmlchars($_POST):array('prefix'=>'ost_','dbhost'=>'localhost'); +$info=($_POST && $errors)?Format::htmlchars($_POST):array('prefix'=>'ost_','dbhost'=>'localhost','lang_id'=>'en_US'); ?> -<div id="main" class="step2"> +<div id="main" class="step2"> <h1>osTicket Basic Installation</h1> <p>Please fill out the information below to continue your osTicket installation. All fields are required.</p> <font class="error"><strong><?php echo $errors['err']; ?></strong></font> @@ -26,6 +26,19 @@ $info=($_POST && $errors)?Format::htmlchars($_POST):array('prefix'=>'ost_','dbho <a class="tip" href="#system_email"><i class="icon-question-sign help-tip"></i></a> <font class="error"><?php echo $errors['email']; ?></font> </div> + <div class="row"> + <label>Default Language:</label> +<?php $langs = Internationalization::availableLanguages(); ?> + <select name="lang_id"> +<?php foreach($langs as $l) { + $selected = ($info['lang_id'] == $l['code']) ? 'selected="selected"' : ''; ?> + <option value="<?php echo $l['code']; ?>" <?php echo $selected; + ?>><?php echo $l['desc']; ?></option> +<?php } ?> + </select> + <a class="tip" href="#default_lang"><i class="icon-question-sign help-tip"></i></a> + <font class="error"> <?php echo $errors['lang_id']; ?></font> + </div> <h4 class="head admin">Admin User</h4> <span class="subhead">Your primary administrator account - you can add more users later.</span> diff --git a/setup/test/tests/stubs.php b/setup/test/tests/stubs.php index 591700ec50f0ef88f208b141c0347e170ae662c9..a26014a11ad13e4ddad120d22d263c32d092ea91 100644 --- a/setup/test/tests/stubs.php +++ b/setup/test/tests/stubs.php @@ -45,6 +45,7 @@ class DomElement { class DomDocument { function loadHTML() {} + function loadXML() {} } class Exception { @@ -83,4 +84,12 @@ class DateTimeZone { static function listIdentifiers() {} } +class Phar { + static function isValidPharFilename() {} +} + +class ZipArchive { + function statIndex() {} + function getFromIndex() {} +} ?>