golden hour
/home/doctorbruno/public_html/doctorbruno.info/taeionline/support/include
⬆️ Go Up
Upload
File/Folder
Size
Actions
.MANIFEST
194.97 KB
Del
OK
.htaccess
14 B
Del
OK
JSON.php
33.13 KB
Del
OK
PasswordHash.php
6.92 KB
Del
OK
Spyc.php
31.74 KB
Del
OK
UniversalClassLoader.php
8.61 KB
Del
OK
ajax.admin.php
7.29 KB
Del
OK
ajax.config.php
4.61 KB
Del
OK
ajax.content.php
9.76 KB
Del
OK
ajax.draft.php
13.03 KB
Del
OK
ajax.email.php
2.33 KB
Del
OK
ajax.export.php
1006 B
Del
OK
ajax.filter.php
874 B
Del
OK
ajax.forms.php
13.84 KB
Del
OK
ajax.i18n.php
5.1 KB
Del
OK
ajax.kbase.php
2.93 KB
Del
OK
ajax.note.php
2.05 KB
Del
OK
ajax.orgs.php
11.73 KB
Del
OK
ajax.plugins.php
2.15 KB
Del
OK
ajax.schedule.php
4.29 KB
Del
OK
ajax.search.php
12.72 KB
Del
OK
ajax.sequence.php
3.2 KB
Del
OK
ajax.staff.php
11.62 KB
Del
OK
ajax.tasks.php
33.87 KB
Del
OK
ajax.thread.php
8.81 KB
Del
OK
ajax.tickets.php
79.7 KB
Del
OK
ajax.tips.php
1.66 KB
Del
OK
ajax.upgrader.php
2.24 KB
Del
OK
ajax.users.php
18.33 KB
Del
OK
api.cron.php
1.06 KB
Del
OK
api.tickets.php
10.36 KB
Del
OK
class.2fa.php
7.76 KB
Del
OK
class.ajax.php
1.45 KB
Del
OK
class.api.php
15.31 KB
Del
OK
class.app.php
1.49 KB
Del
OK
class.attachment.php
6.95 KB
Del
OK
class.auth.php
50.73 KB
Del
OK
class.avatar.php
6.44 KB
Del
OK
class.banlist.php
2.58 KB
Del
OK
class.base32.php
4.07 KB
Del
OK
class.businesshours.php
7.08 KB
Del
OK
class.canned.php
9.05 KB
Del
OK
class.captcha.php
1.73 KB
Del
OK
class.category.php
11.14 KB
Del
OK
class.charset.php
3.41 KB
Del
OK
class.cli.php
9.48 KB
Del
OK
class.client.php
15.39 KB
Del
OK
class.collaborator.php
5.65 KB
Del
OK
class.company.php
2.65 KB
Del
OK
class.config.php
60.36 KB
Del
OK
class.controller.php
1.15 KB
Del
OK
class.cron.php
3.6 KB
Del
OK
class.crypto.php
18.92 KB
Del
OK
class.csrf.php
2.38 KB
Del
OK
class.dept.php
33.21 KB
Del
OK
class.dispatcher.php
7.12 KB
Del
OK
class.draft.php
6.23 KB
Del
OK
class.dynamic_forms.php
64.98 KB
Del
OK
class.email.php
49 KB
Del
OK
class.error.php
1.69 KB
Del
OK
class.export.php
32.92 KB
Del
OK
class.faq.php
15.05 KB
Del
OK
class.file.php
34.59 KB
Del
OK
class.filter.php
31.9 KB
Del
OK
class.filter_action.php
23.67 KB
Del
OK
class.format.php
44.3 KB
Del
OK
class.forms.php
195.58 KB
Del
OK
class.http.php
7.16 KB
Del
OK
class.i18n.php
24.28 KB
Del
OK
class.import.php
6.9 KB
Del
OK
class.json.php
2.71 KB
Del
OK
class.knowledgebase.php
5.8 KB
Del
OK
class.list.php
42 KB
Del
OK
class.lock.php
4.05 KB
Del
OK
class.log.php
1.55 KB
Del
OK
class.mail.php
35.76 KB
Del
OK
class.mailer.php
25.75 KB
Del
OK
class.mailfetch.php
9.72 KB
Del
OK
class.mailparse.php
30.99 KB
Del
OK
class.message.php
6.42 KB
Del
OK
class.migrater.php
5.2 KB
Del
OK
class.misc.php
7.5 KB
Del
OK
class.model.php
2.3 KB
Del
OK
class.nav.php
14.15 KB
Del
OK
class.note.php
2.39 KB
Del
OK
class.oauth2.php
4.04 KB
Del
OK
class.organization.php
22.45 KB
Del
OK
class.orm.php
120.9 KB
Del
OK
class.osticket.php
20.64 KB
Del
OK
class.ostsession.php
21.86 KB
Del
OK
class.page.php
10.59 KB
Del
OK
class.pagenate.php
5.53 KB
Del
OK
class.passwd.php
1.21 KB
Del
OK
class.pdf.php
3.83 KB
Del
OK
class.plugin.php
36.13 KB
Del
OK
class.priority.php
1.81 KB
Del
OK
class.queue.php
103.51 KB
Del
OK
class.report.php
11.76 KB
Del
OK
class.role.php
11.27 KB
Del
OK
class.schedule.php
46.28 KB
Del
OK
class.search.php
61.66 KB
Del
OK
class.sequence.php
7.27 KB
Del
OK
class.session.php
19.25 KB
Del
OK
class.setup.php
3.55 KB
Del
OK
class.signal.php
4.16 KB
Del
OK
class.sla.php
9.11 KB
Del
OK
class.staff.php
60.21 KB
Del
OK
class.task.php
56.21 KB
Del
OK
class.team.php
12.31 KB
Del
OK
class.template.php
23.45 KB
Del
OK
class.thread.php
108.96 KB
Del
OK
class.thread_actions.php
17.08 KB
Del
OK
class.ticket.php
167.39 KB
Del
OK
class.timezone.php
21.94 KB
Del
OK
class.topic.php
20.06 KB
Del
OK
class.translation.php
34.79 KB
Del
OK
class.upgrader.php
13.76 KB
Del
OK
class.user.php
43.4 KB
Del
OK
class.usersession.php
7.55 KB
Del
OK
class.util.php
10.07 KB
Del
OK
class.validator.php
12.94 KB
Del
OK
class.variable.php
11.93 KB
Del
OK
class.xml.php
3.23 KB
Del
OK
class.yaml.php
1.15 KB
Del
OK
cli
-
Del
OK
client
-
Del
OK
config
-
Del
OK
fpdf
-
Del
OK
htmLawed.php
53.53 KB
Del
OK
html2text.php
33.71 KB
Del
OK
i18n
-
Del
OK
index.php
37 B
Del
OK
laminas-mail
-
Del
OK
mpdf
-
Del
OK
mysqli.php
9.55 KB
Del
OK
ost-config.php
5.63 KB
Del
OK
ost-sampleconfig.php
6.24 KB
Del
OK
pear
-
Del
OK
plugins
-
Del
OK
staff
-
Del
OK
tnef_decoder.php
19.82 KB
Del
OK
upgrader
-
Del
OK
Edit: class.search.php
<?php /********************************************************************* module.search.php Search Engine for osTicket This module defines the pieces for a search engine for osTicket. Searching can be performed by various search engine backends which can make use of the features of various search providers. A reference search engine backend is provided which uses MySQL MyISAM tables. This default backend should not be used on Galera clusters. Jared Hancock <jared@osticket.com> Peter Rotich <peter@osticket.com> Copyright (c) 2006-2013 osTicket http://www.osticket.com Released under the GNU General Public License WITHOUT ANY WARRANTY. See LICENSE.TXT for details. vim: expandtab sw=4 ts=4 sts=4: **********************************************************************/ require_once INCLUDE_DIR . 'class.role.php'; require_once INCLUDE_DIR . 'class.list.php'; require_once INCLUDE_DIR . 'class.queue.php'; abstract class SearchBackend { static $id = false; static $registry = array(); const SORT_RELEVANCE = 1; const SORT_RECENT = 2; const SORT_OLDEST = 3; const PERM_EVERYTHING = 'search.all'; static protected $perms = array( self::PERM_EVERYTHING => array( 'title' => /* @trans */ 'Search', 'desc' => /* @trans */ 'See all tickets in search results, regardless of access', 'primary' => true, ), ); abstract function update($model, $id, $content, $new=false, $attrs=array()); abstract function find($query, QuerySet $criteria, $addRelevance=true); static function register($backend=false) { $backend = $backend ?: get_called_class(); if ($backend::$id == false) throw new Exception('SearchBackend must define an ID'); static::$registry[$backend::$id] = $backend; } static function getInstance($id) { if (!isset(self::$registry[$id])) return null; return new self::$registry[$id](); } static function getPermissions() { return self::$perms; } } RolePermission::register(/* @trans */ 'Miscellaneous', SearchBackend::getPermissions()); // Register signals to intercept saving of various content throughout the // system class SearchInterface { var $backend; function __construct() { $this->bootstrap(); } function find($query, QuerySet $criteria, $addRelevance=true) { $query = Format::searchable($query); return $this->backend->find($query, $criteria, $addRelevance); } function update($model, $id, $content, $new=false, $attrs=array()) { if ($this->backend) $this->backend->update($model, $id, $content, $new, $attrs); } function createModel($model) { return $this->updateModel($model, true); } function deleteModel($model) { if ($this->backend) $this->backend->delete($model); } function updateModel($model, $new=false) { // The MySQL backend does not need to index attributes of the // various models, because those other attributes are available in // the local database in other tables. switch (true) { case $model instanceof ThreadEntry: // Only index an entry for threads if a human created the // content if (!$model->getUserId() && !$model->getStaffId()) break; $this->update($model, $model->getId(), $model->getBody()->getSearchable(), $new === true, array( 'title' => $model->getTitle(), 'created' => $model->getCreateDate(), ) ); break; case $model instanceof Ticket: $cdata = array(); foreach ($model->loadDynamicData() as $a) if ($v = $a->getSearchable()) $cdata[] = $v; $this->update($model, $model->getId(), trim(implode("\n", $cdata)), $new === true, array( 'title'=> Format::searchable($model->getSubject()), 'number'=> $model->getNumber(), 'status'=> $model->getStatus(), 'topic_id'=> $model->getTopicId(), 'priority_id'=> $model->getPriorityId(), // Stats (comments, attachments) // Access constraints 'dept_id'=> $model->getDeptId(), 'staff_id'=> $model->getStaffId(), 'team_id'=> $model->getTeamId(), // Sorting and ranging preferences 'created'=> $model->getCreateDate(), // Add last-updated timestamp ) ); break; case $model instanceof User: $cdata = array(); foreach ($model->getDynamicData($false) as $e) foreach ($e->getAnswers() as $tag=>$a) if ($tag != 'subject' && ($v = $a->getSearchable())) $cdata[] = $v; $this->update($model, $model->getId(), trim(implode("\n", $cdata)), $new === true, array( 'title'=> Format::searchable($model->getFullName()), 'emails'=> $model->emails->asArray(), 'org_id'=> $model->getOrgId(), 'created'=> $model->getCreateDate(), ) ); break; case $model instanceof Organization: $cdata = array(); foreach ($model->getDynamicData(false) as $e) foreach ($e->getAnswers() as $a) if ($v = $a->getSearchable()) $cdata[] = $v; $this->update($model, $model->getId(), trim(implode("\n", $cdata)), $new === true, array( 'title'=> Format::searchable($model->getName()), 'created'=> $model->getCreateDate(), ) ); break; case $model instanceof FAQ: $this->update($model, $model->getId(), $model->getSearchableAnswer(), $new === true, array( 'title'=> Format::searchable($model->getQuestion()), 'keywords'=> $model->getKeywords(), 'topics'=> $model->getHelpTopicsIds(), 'category_id'=> $model->getCategoryId(), 'created'=> $model->getCreateDate(), ) ); break; default: // Not indexed break; } } function bootstrap() { // Determine the backend if (defined('SEARCH_BACKEND')) $bk = SearchBackend::getInstance(SEARCH_BACKEND); if (!isset($bk) && !($bk = SearchBackend::getInstance('mysql'))) // No backend registered or defined return false; $this->backend = $bk; $this->backend->bootstrap(); $self = $this; // Thread entries // Tickets, which can be edited as well // Knowledgebase articles (FAQ and canned responses) // Users, organizations Signal::connect('threadentry.created', array($this, 'createModel')); Signal::connect('ticket.created', array($this, 'createModel')); Signal::connect('user.created', array($this, 'createModel')); Signal::connect('organization.created', array($this, 'createModel')); Signal::connect('model.created', array($this, 'createModel'), 'FAQ'); Signal::connect('model.updated', array($this, 'updateModel')); Signal::connect('model.deleted', array($this, 'deleteModel')); } } require_once(INCLUDE_DIR.'class.config.php'); class MySqlSearchConfig extends Config { var $table = CONFIG_TABLE; function __construct() { parent::__construct("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; var $SEARCH_TABLE; function __construct() { $this->SEARCH_TABLE = TABLE_PREFIX . '_search'; } function getConfig() { if (!isset($this->config)) $this->config = new MySqlSearchConfig(); return $this->config; } function bootstrap() { if ($this->getConfig()->get('reindex', true)) Signal::connect('cron', array($this, 'IndexOldStuff')); } function update($model, $id, $content, $new=false, $attrs=array()) { if (!($type=ObjectModel::getType($model))) return; if ($model instanceof Ticket) $attrs['title'] = $attrs['number'].' '.$attrs['title']; elseif ($model instanceof User) $content .=' '.implode("\n", $attrs['emails']); $title = $attrs['title'] ?: ''; if (!$content && !$title) return; if (!$id) return; $sql = 'REPLACE INTO '.$this->SEARCH_TABLE . ' SET object_type='.db_input($type) . ', object_id='.db_input($id) . ', content='.db_input($content) . ', title='.db_input($title); return db_query($sql, false); } function delete($model) { switch (true) { case $model instanceof Thread: $sql = 'DELETE s.* FROM '.$this->SEARCH_TABLE . " s JOIN ".THREAD_ENTRY_TABLE." h ON (h.id = s.object_id) " . " WHERE s.object_type='H'" . ' AND h.thread_id='.db_input($model->getId()); return db_query($sql); default: if (!($type = ObjectModel::getType($model))) return; $sql = 'DELETE FROM '.$this->SEARCH_TABLE . ' WHERE object_type='.db_input($type) . ' AND object_id='.db_input($model->getId()); return db_query($sql); } } // Quote things like email addresses function quote($query) { $parts = array(); if (!preg_match_all('`(?:([^\s"\']+)|"[^"]*"|\'[^\']*\')(\s*)`', $query, $parts, PREG_SET_ORDER)) return $query; $results = array(); foreach ($parts as $m) { // Check for quoting if ($m[1] // Already quoted? && preg_match('`@`u', $m[0]) ) { $char = strpos($m[1], '"') ? "'" : '"'; $m[0] = $char . $m[0] . $char; } $results[] = $m[0].$m[2]; } return implode('', $results); } function find($query, QuerySet $criteria, $addRelevance=true) { global $thisstaff; // MySQL usually doesn't handle words shorter than three letters // (except with special configuration) if (strlen($query) < 3) return $criteria; $criteria = clone $criteria; $mode = ' IN NATURAL LANGUAGE MODE'; // According to the MySQL full text boolean mode, this grammar is // assumed: // see http://dev.mysql.com/doc/refman/5.6/en/fulltext-boolean.html // // PREOP = [<>~+-] // POSTOP = [*] // WORD = [\w][\w-]* // TERM = PREOP? WORD POSTOP? // QWORD = " [^"]+ " // PARENS = \( { { TERM | QWORD } { \s+ { TERM | QWORD } }+ } \) // EXPR = { PREOP? PARENS | TERM | QWORD } // BOOLEAN = EXPR { \s+ EXPR }* // // Changing '{' for (?: and '}' for ')', collapsing whitespace, we // have this regular expression $BOOLEAN = '(?:[<>~+-]?\((?:(?:[<>~+-]?[\w][\w-]*[*]?|"[^"]+")(?:\s+(?:[<>~+-]?[\w][\w-]*[*]?|"[^"]+"))+)\)|[<>~+-]?[\w][\w-]*[*]?|"[^"]+")(?:\s+(?:[<>~+-]?\((?:(?:[<>~+-]?[\w][\w-]*[*]?|"[^"]+")(?:\s+(?:[<>~+-]?[\w][\w-]*[*]?|"[^"]+"))+)\)|[<>~+-]?[\w][\w-]*[*]?|"[^"]+"))*'; // Require the use of at least one operator and conform to the // boolean mode grammar $T = array(); if (preg_match('`(^|\s)["()<>~+-]`u', $query, $T) && preg_match("`^{$BOOLEAN}$`u", $query, $T) ) { // If using boolean operators, search in boolean mode. This regex // will ensure proper placement of operators, whitespace, and quotes // in an effort to avoid crashing the query at MySQL $query = $this->quote($query); $mode = ' IN BOOLEAN MODE'; } #elseif (count(explode(' ', $query)) == 1) # $mode = ' WITH QUERY EXPANSION'; // Sanitize query to avoid possible SQL injection via parameter markers // This regex matches one or more colons followed by one or more digits, // and then replaces the match with only the digits (i.e. stripping the colon(s)). $query = preg_replace('/:+(\d+)/', '$1', $query); // escape query and using it as search $search = 'MATCH (Z1.title, Z1.content) AGAINST ('.db_input($query).$mode.')'; switch ($criteria->model) { case false: case 'Ticket': if ($addRelevance) { $criteria = $criteria->extra(array( 'select' => array( '__relevance__' => 'Z1.`relevance`', ), )); } $criteria->extra(array( 'tables' => array( str_replace(array(':', '{}'), array(TABLE_PREFIX, $search), "(SELECT COALESCE(Z3.`object_id`, Z5.`ticket_id`, Z8.`ticket_id`) as `ticket_id`, Z1.relevance FROM (SELECT Z1.`object_id`, Z1.`object_type`, {} AS `relevance` FROM `:_search` Z1 WHERE {} ORDER BY relevance DESC) Z1 LEFT JOIN `:thread_entry` Z2 ON (Z1.`object_type` = 'H' AND Z1.`object_id` = Z2.`id`) LEFT JOIN `:thread` Z3 ON (Z2.`thread_id` = Z3.`id` AND (Z3.`object_type` = 'T' OR Z3.`object_type` = 'C')) LEFT JOIN `:ticket` Z5 ON (Z1.`object_type` = 'T' AND Z1.`object_id` = Z5.`ticket_id`) LEFT JOIN `:user` Z6 ON (Z6.`id` = Z1.`object_id` and Z1.`object_type` = 'U') LEFT JOIN `:organization` Z7 ON (Z7.`id` = Z1.`object_id` AND Z7.`id` = Z6.`org_id` AND Z1.`object_type` = 'O') LEFT JOIN `:ticket` Z8 ON (Z8.`user_id` = Z6.`id`)) Z1"), ), )); $criteria->extra(array('order_by' => array(array(new SqlCode('Z1.relevance', 'DESC'))))); $criteria->filter(array('ticket_id'=>new SqlCode('Z1.`ticket_id`'))); break; case 'User': $criteria->extra(array( 'select' => array( '__relevance__' => 'Z1.`relevance`', ), 'tables' => array( str_replace(array(':', '{}'), array(TABLE_PREFIX, $search), "(SELECT Z6.`id` as `user_id`, {} AS `relevance` FROM `:_search` Z1 LEFT JOIN `:user` Z6 ON (Z6.`id` = Z1.`object_id` and Z1.`object_type` = 'U') LEFT JOIN `:organization` Z7 ON (Z7.`id` = Z1.`object_id` AND Z7.`id` = Z6.`org_id` AND Z1.`object_type` = 'O') WHERE {}) Z1"), ) )); $criteria->filter(array('id'=>new SqlCode('Z1.`user_id`'))); break; case 'Organization': $criteria->extra(array( 'select' => array( '__relevance__' => 'Z1.`relevance`', ), 'tables' => array( str_replace(array(':', '{}'), array(TABLE_PREFIX, $search), "(SELECT Z2.`id` as `org_id`, {} AS `relevance` FROM `:_search` Z1 LEFT JOIN `:organization` Z2 ON (Z2.`id` = Z1.`object_id` AND Z1.`object_type` = 'O') WHERE {}) Z1"), ) )); $criteria->filter(array('id'=>new SqlCode('Z1.`org_id`'))); break; } // TODO: Ensure search table exists; if (false) { // TODO: Create the search table automatically // $class::createSearchTable(); } return $criteria; } static function createSearchTable() { // Use InnoDB with Galera, MyISAM with v5.5, and the database // default otherwise $sql = "select count(*) from information_schema.tables where table_schema='information_schema' and table_name = 'INNODB_FT_CONFIG'"; $mysql56 = db_result(db_query($sql)); $sql = "show status like 'wsrep_local_state'"; $galera = db_result(db_query($sql)); if ($galera && !$mysql56) throw new Exception('Galera cannot be used with MyISAM tables. Upgrade to MariaDB 10 / MySQL 5.6 is required'); $engine = $galera ? 'InnodB' : ($mysql56 ? '' : 'MyISAM'); if ($engine) $engine = 'ENGINE='.$engine; $sql = 'CREATE TABLE IF NOT EXISTS '.TABLE_PREFIX."_search ( `object_type` varchar(8) not null, `object_id` int(11) unsigned not null, `title` text collate utf8_general_ci, `content` text collate utf8_general_ci, primary key `object` (`object_type`, `object_id`), fulltext key `search` (`title`, `content`) ) $engine CHARSET=utf8"; if (!db_query($sql)) return false; // Start rebuilding the index $config = new MySqlSearchConfig(); $config->set('reindex', 1); return true; } /** * Cooperates with the cron system to automatically find content that is * not indexed in the _search table and add it to the index. */ function IndexOldStuff() { $class = get_class($this); $auto_create = function($db_error) use ($class) { if ($db_error != 1146) // Perform the standard error handling return true; // Create the search table automatically $class::__init(); }; // THREADS ---------------------------------- $sql = "SELECT A1.`id`, A1.`title`, A1.`body`, A1.`format` FROM `".THREAD_ENTRY_TABLE."` A1 LEFT JOIN `".TABLE_PREFIX."_search` A2 ON (A1.`id` = A2.`object_id` AND A2.`object_type`='H') WHERE A2.`object_id` IS NULL AND (A1.poster <> 'SYSTEM') AND (IFNULL(LENGTH(A1.`title`), 0) + IFNULL(LENGTH(A1.`body`), 0) > 0) ORDER BY A1.`id` DESC LIMIT 500"; if (!($res = db_query_unbuffered($sql, $auto_create))) return false; while ($row = db_fetch_row($res)) { $body = ThreadEntryBody::fromFormattedText($row[2], $row[3]); $body = $body->getSearchable(); $title = Format::searchable($row[1]); if (!$body && !$title) continue; $record = array('H', $row[0], $title, $body); if (!$this->__index($record)) return; } // TICKETS ---------------------------------- $sql = "SELECT A1.`ticket_id` FROM `".TICKET_TABLE."` A1 LEFT JOIN `".TABLE_PREFIX."_search` A2 ON (A1.`ticket_id` = A2.`object_id` AND A2.`object_type`='T') WHERE A2.`object_id` IS NULL ORDER BY A1.`ticket_id` DESC LIMIT 300"; if (!($res = db_query_unbuffered($sql, $auto_create))) return false; while ($row = db_fetch_row($res)) { if (!($ticket = Ticket::lookup($row[0]))) continue; $cdata = $ticket->loadDynamicData(); $content = array(); foreach ($cdata as $k=>$a) if ($k != 'subject' && ($v = $a->getSearchable())) $content[] = $v; $record = array('T', $ticket->getId(), Format::searchable($ticket->getNumber().' '.$ticket->getSubject()), implode("\n", $content)); if (!$this->__index($record)) return; } // USERS ------------------------------------ $sql = "SELECT A1.`id` FROM `".USER_TABLE."` A1 LEFT JOIN `".TABLE_PREFIX."_search` A2 ON (A1.`id` = A2.`object_id` AND A2.`object_type`='U') WHERE A2.`object_id` IS NULL"; if (!($res = db_query_unbuffered($sql, $auto_create))) return false; while ($row = db_fetch_row($res)) { $user = User::lookup($row[0]); $cdata = $user->getDynamicData(); $content = array(); foreach ($user->emails as $e) $content[] = $e->address; foreach ($cdata as $e) foreach ($e->getAnswers() as $a) if ($c = $a->getSearchable()) $content[] = $c; $record = array('U', $user->getId(), Format::searchable($user->getFullName()), trim(implode("\n", $content))); if (!$this->__index($record)) return; } // ORGANIZATIONS ---------------------------- $sql = "SELECT A1.`id` FROM `".ORGANIZATION_TABLE."` A1 LEFT JOIN `".TABLE_PREFIX."_search` A2 ON (A1.`id` = A2.`object_id` AND A2.`object_type`='O') WHERE A2.`object_id` IS NULL"; if (!($res = db_query_unbuffered($sql, $auto_create))) return false; while ($row = db_fetch_row($res)) { $org = Organization::lookup($row[0]); $cdata = $org->getDynamicData(); $content = array(); foreach ($cdata as $e) foreach ($e->getAnswers() as $a) if ($c = $a->getSearchable()) $content[] = $c; $record = array('O', $org->getId(), Format::searchable($org->getName()), trim(implode("\n", $content))); if (!$this->__index($record)) return null; } // KNOWLEDGEBASE ---------------------------- require_once INCLUDE_DIR . 'class.faq.php'; $sql = "SELECT A1.`faq_id` FROM `".FAQ_TABLE."` A1 LEFT JOIN `".TABLE_PREFIX."_search` A2 ON (A1.`faq_id` = A2.`object_id` AND A2.`object_type`='K') WHERE A2.`object_id` IS NULL"; if (!($res = db_query_unbuffered($sql, $auto_create))) return false; while ($row = db_fetch_row($res)) { if (!($faq = FAQ::lookup($row[0]))) continue; $q = $faq->getQuestion(); if ($k = $faq->getKeywords()) $q = $k.' '.$q; $record = array('K', $faq->getId(), Format::searchable($q), $faq->getSearchableAnswer()); if (!$this->__index($record)) return; } // 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) { static $queue = array(); if ($record) $queue[] = $record; elseif (!$queue) return; if (!$force_flush && count($queue) < $this::$BATCH_SIZE) return true; foreach ($queue as &$r) $r = sprintf('(%s)', implode(',', db_input($r))); unset($r); $sql = 'INSERT INTO `'.TABLE_PREFIX.'_search` (`object_type`, `object_id`, `title`, `content`) VALUES '.implode(',', $queue); 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) return null; return true; } static function __init() { self::createSearchTable(); } } Signal::connect('system.install', array('MysqlSearchBackend', '__init')); MysqlSearchBackend::register(); // Saved search system /** * Custom Queue truly represent a saved advanced search. */ class SavedQueue extends CustomQueue { // Override the ORM relationship to force no children private $children = false; private $_config; private $_criteria; private $_columns; private $_settings; private $_form; private $_sorts; function __onload() { global $thisstaff; // Load custom settings for this staff if ($thisstaff) { $this->_config = QueueConfig::lookup(array( 'queue_id' => $this->getId(), 'staff_id' => $thisstaff->getId()) ); } } static function forStaff(Staff $agent) { return static::objects()->filter(Q::any(array( 'staff_id' => $agent->getId(), 'flags__hasbit' => self::FLAG_PUBLIC, ))) ->exclude(array('flags__hasbit'=>self::FLAG_QUEUE)); } private function getSettings() { if (!isset($this->_settings)) { $this->_settings = array(); if ($this->_config) $this->_settings = $this->_config->getSettings(); } return $this->_settings; } private function getCustomColumns() { if (!isset($this->_columns)) { $this->_columns = array(); if ($this->_config && $this->_config->columns->count()) $this->_columns = $this->_config->columns; } return $this->_columns; } static function getHierarchicalQueues(Staff $staff, $pid = 0, $primary = true) { return CustomQueue::getHierarchicalQueues($staff, 0, false); } /* * Determine if sort is inherited */ function isDefaultSortInherited() { if ($this->parent && $this->getSettings() && @$this->_settings['inherit-sort']) return true; return parent::isDefaultSortInherited(); } function getSortOptions() { if (!isset($this->_sorts)) { // See if the queue has sort options if (($sorts=parent::getSortOptions()) && $sorts->count()) $this->_sorts = $sorts; // otherwise return all sorts else $this->_sorts = QueueSort::objects(); } return $this->_sorts; } function getDefaultSort() { if ($this->getSettings() && $this->_settings['sort_id'] && ($sort = QueueSort::lookup($this->_settings['sort_id']))) return $sort; return parent::getDefaultSort(); } /** * Fetch an AdvancedSearchForm instance for use in displaying or * configuring this search in the user interface. * */ function getForm($source=null, $searchable=array()) { $searchable = null; if ($this->isAQueue()) // Only allow supplemental matches. $searchable = array_intersect_key($this->getCurrentSearchFields($source), $this->getSupplementalMatches()); return parent::getForm($source, $searchable); } /** * Get get supplemental matches for public queues. * */ function getSupplementalMatches() { // Target flags $flags = array('isoverdue', 'isassigned', 'isreopened', 'isanswered'); $current = array(); // Check for closed state - whih disables above flags foreach (parent::getCriteria() as $c) { if (!strcasecmp($c[0], 'status__state') && isset($c[2]['closed'])) return array(); $current[] = $c[0]; } // Filter out fields already in criteria $matches = array_intersect_key($this->getSupportedMatches(), array_flip(array_diff($flags, $current))); return $matches; } function criteriaRequired() { return !$this->isAQueue(); } function describeCriteria($criteria=false){ $criteria = $criteria ?: parent::getCriteria(); return parent::describeCriteria($criteria); } function getCriteria($include_parent=true) { if (!isset($this->_criteria)) { $this->getSettings(); $this->_criteria = $this->_settings['criteria'] ?? array(); } $criteria = $this->_criteria; if ($include_parent) $criteria = array_merge($criteria, parent::getCriteria($include_parent)); return $criteria; } function getSupplementalCriteria() { return $this->getCriteria(false); } function useStandardColumns() { $this->getSettings(); if ($this->getCustomColumns() && isset($this->_settings['inherit-columns'])) return $this->_settings['inherit-columns']; // owner?? edit away. if ($this->_config && $this->_config->staff_id == $this->staff_id) return false; return parent::useStandardColumns(); } function inheritColumns() { if ($this->getSettings() && isset($this->_settings['inherit-columns'])) return $this->_settings['inherit-columns']; return parent::inheritColumns(); } function getStandardColumns() { return parent::getColumns(is_null($this->parent)); } function getColumns($use_template=false) { if (!$this->useStandardColumns() && ($columns=$this->getCustomColumns())) return $columns; return parent::getColumns($use_template); } function update($vars, &$errors=array()) { global $thisstaff; if (!$this->checkAccess($thisstaff)) return false; if ($this->checkOwnership($thisstaff)) { // Owner of the queue - can update everything if (!parent::update($vars, $errors)) return false; // Personal queues _always_ inherit from their parent $this->setFlag(self::FLAG_INHERIT_CRITERIA, $this->parent_id > 0); return true; } // Agent's config for public queue. if (!$this->_config) $this->_config = QueueConfig::create(array( 'queue_id' => $this->getId(), 'staff_id' => $thisstaff->getId())); // Validate & isolate supplemental criteria (if any) $vars['criteria'] = array(); if (isset($vars['fields'])) { $form = $this->getForm($vars, $thisstaff); if ($form->isValid()) { $criteria = self::isolateCriteria($form->getClean(), $this->getRoot()); $allowed = $this->getSupplementalMatches(); foreach ($criteria as $k => $c) if (!isset($allowed[$c[0]])) unset($criteria[$k]); $vars['criteria'] = $criteria ?: array(); } else { $errors['criteria'] = __('Validation errors exist on supplimental criteria'); } } if (!$errors && $this->_config->update($vars, $errors)) { // reset settings $this->_settings = $this->_criteria = null; // Reset chached queue options unset($_SESSION['sort'][$this->getId()]); } return (!$errors); } function getTotal($agent=null) { $query = $this->getQuery(); if ($agent) $query = $agent->applyVisibility($query); $query->filter(Q::any([ 'ticket_pid__isnull' => true, 'flags__hasbit' => Ticket::FLAG_LINKED ]))->limit(false)->offset(false)->order_by(false); try { return $query->count(); } catch (Exception $e) { return null; } } function getCount($agent, $cached=true) { $count = null; if ($cached && ($counts = self::counts($agent, $cached))) $count = $counts["q{$this->getId()}"]; if ($count == null) $count = $this->getTotal($agent); return $count; } // Get ticket counts for queues the agent has acces to. static function counts($agent, $cached=true, $criteria=array()) { if (!$agent instanceof Staff) return null; // Cache TLS in seconds $ttl = 5*60; // Cache key based on agent and salt of the installation $key = "counts.queues.{$agent->getId()}.".SECRET_SALT; if ($criteria && is_array($criteria)) // Consider additional criteria. $key .= '.'.md5(serialize($criteria)); // only consider cache if requesed if ($cached && ($counts=self::getCounts($key, $ttl))) return $counts; $queues = static::objects() ->filter(Q::any(array( 'flags__hasbit' => CustomQueue::FLAG_QUEUE, 'staff_id' => $agent->getId(), ))) ->filter(Q::not(array( 'flags__hasbit' => CustomQueue::FLAG_DISABLED, ))); if ($criteria && is_array($criteria)) $queues->filter($criteria); $counts = array(); $query = Ticket::objects(); // Apply tickets visibility for the agent $query = $agent->applyVisibility($query, true); // Aggregate constraints foreach ($queues as $queue) { $Q = $queue->getBasicQuery(); // only get counts for regular tickets (not children tickets) unless // queue is a saved search if ($queue->isAQueue() || $queue->isASubQueue()) { $reg = Q::any(array('thread__object_type' => 'T')); $Q->constraints[] = $reg; } if ($Q->constraints) { $empty = false; if (count($Q->constraints) > 1) { foreach ($Q->constraints as $value) { if (!$value->constraints) $empty = true; } } } // Add extra tables joins (if any) if ($Q->extra && isset($Q->extra['tables'])) { // skip counting keyword searches. Display them as '-' $counts['q'.$queue->getId()] = '-'; continue; $contraints = array(); if ($Q->constraints) $constraints = new Q($Q->constraints); foreach ($Q->extra['tables'] as $T) $query->addExtraJoin(array($T, $constraints, '')); } if ($Q->constraints && !$empty) { $constraints = $Q->constraints; // Add path_constraints to get the correct counts foreach ($Q->path_constraints as $pc) { if (!empty($pc[0]->constraints)) $constraints[] = $pc[0]; } $expr = SqlCase::N()->when(new SqlExpr(new Q($constraints)), new SqlField('ticket_id')); $query->aggregate(array( "q{$queue->id}" => SqlAggregate::COUNT($expr, true) )); } else //display skipped counts as '-' $counts['q'.$queue->getId()] = '-'; } try { $counts = array_merge($counts, $query->values()->one()); } catch (Exception $ex) { foreach ($queues as $q) $counts['q'.$q->getId()] = $q->getTotal(); } // Always cache the results self::storeCounts($key, $counts, $ttl); return $counts; } static function getCounts($key, $ttl) { if (!$key) { return array(); } elseif (function_exists('apcu_store')) { $found = false; $counts = apcu_fetch($key, $found); if ($found === true) return $counts; } elseif (isset($_SESSION['qcounts'][$key]) && (time() - $_SESSION['qcounts'][$key]['time']) < $ttl) { return $_SESSION['qcounts'][$key]['counts']; } else { // Auto clear missed session cache (if any) unset($_SESSION['qcounts'][$key]); } } static function storeCounts($key, $counts, $ttl) { if (function_exists('apcu_store')) { apcu_store($key, $counts, $ttl); } else { // Poor man's cache $_SESSION['qcounts'][$key]['counts'] = $counts; $_SESSION['qcounts'][$key]['time'] = time(); } } static function clearCounts() { if (function_exists('apcu_store')) { if (class_exists('APCUIterator')) { $regex = '/^counts.queues.\d+.' . preg_quote(SECRET_SALT, '/') . '$/'; foreach (new APCUIterator($regex, APC_ITER_KEY) as $key) { apcu_delete($key); } } // Also clear rough counts apcu_delete("rough.counts.".SECRET_SALT); } } static function lookup($criteria) { $queue = parent::lookup($criteria); // Annoted cusom settings (if any) if (($c=$queue->_config)) { $queue->_settings = $c->getSettings() ?: array(); $queue = AnnotatedModel::wrap($queue, array_intersect_key($queue->_settings, array_flip(array('sort_id', 'filter')))); $queue->_config = $c; } return $queue; } static function create($vars=false) { $search = parent::create($vars); $search->clearFlag(self::FLAG_QUEUE); return $search; } } class SavedSearch extends SavedQueue { function isSaved() { return (!$this->__new__); } function getCount($agent, $cached=true) { return 500; } } class AdhocSearch extends SavedSearch { function isSaved() { return false; } function isOwner(Staff $staff) { return $this->ht['staff_id'] == $staff->getId(); } function checkAccess(Staff $staff) { return true; } function getName() { return $this->title ?: $this->describeCriteria(); } static function load($key) { global $thisstaff; if (strpos($key, 'adhoc') === 0) list(, $key) = explode(',', $key, 2); if (!$key || !isset($_SESSION['advsearch']) || !($config=$_SESSION['advsearch'][$key])) return null; $queue = new AdhocSearch(array( 'id' => "adhoc,$key", 'root' => 'T', 'staff_id' => $thisstaff->getId(), 'title' => __('Advanced Search'), )); $queue->config = $config; return $queue; } } // AdvacedSearchForm class AdvancedSearchForm extends SimpleForm { static $id = 1337; function getNumFieldsSelected() { $selected = 0; foreach ($this->getFields() as $F) { if (substr($F->get('name'), -7) == '+search' && $F->getClean()) $selected += 1; // Consider keyword searches elseif ($F->get('name') == ':keywords' && $F->getClean()) $selected += 1; } return $selected; } } // Advanced search special fields class AdvancedSearchSelectionField extends ChoiceField { function hasIdValue() { return false; } function getSearchQ($method, $value, $name=false) { switch ($method) { case 'includes': case '!includes': if (!$value) return; $Q = new Q(); if (count($value) > 1) $Q->add(array("{$name}__in" => array_keys($value))); else $Q->add(array($name => key($value))); if ($method == '!includes') $Q->negate(); return $Q; break; // osTicket commonly uses `0` to represent an unset state, so // the set and unset checks need to check for both not null and // nonzero case 'nset': return new Q([$name => 0]); case 'set': return Q::not([$name => 0]); default: return parent::getSearchQ($method, $value, $name); } } } class HelpTopicChoiceField extends AdvancedSearchSelectionField { static $_topics; function hasIdValue() { return true; } function getChoices($verbose=false, $options=array()) { global $thisstaff; if (!isset($this->_topics)) { $this->_topics = $thisstaff ? $thisstaff->getTopicNames(false, Topic::DISPLAY_DISABLED) : Topic::getHelpTopics(false, Topic::DISPLAY_DISABLED);; } return $this->_topics; } } class SLAChoiceField extends AdvancedSearchSelectionField { static $_slas; function hasIdValue() { return true; } function getChoices($verbose=false, $options=array()) { if (!isset($this->_slas)) $this->_slas = SLA::getSLAs(array('nameOnly' => true)); return $this->_slas; } } require_once INCLUDE_DIR . 'class.dept.php'; class DepartmentChoiceField extends AdvancedSearchSelectionField { static $_depts; static $_alldepts; var $_choices; function getDepts($criteria=array()) { global $thisstaff; $staff = $criteria['staff']; $depts = array(); if ($staff) foreach ($staff->getDepartmentNames(true) as $id => $name) $depts[$id] = $name; else foreach (Dept::getDepartments() as $id => $name) $depts[$id] = $name; return $depts; } function getChoices($verbose=false, $options=array()) { global $thisstaff; $config = $this->getConfiguration(); $criteria = array( 'staff' => $config['staff'] ?: $thisstaff ); if (!isset($this->_choices)) $this->_choices = $this->getDepts($criteria); return $this->_choices; } function toString($value) { if (!isset($this->_alldepts)) $this->_alldepts = $this->getDepts(); $choices = $this->_alldepts; $selection = array(); if (!is_array($value)) $value = array($value => $value); foreach ($value as $k => $v) if (isset($choices[$k])) $selection[] = $choices[$k]; return $selection ? implode(',', $selection) : parent::toString($value); } function getQuickFilterChoices() { global $thisstaff; if (!isset($this->_choices)) { $depts = $thisstaff ? $thisstaff->getDepts() : array(); foreach ($this->getChoices() as $id => $name) { if (!$depts || in_array($id, $depts)) $this->_choices[$id] = $name; } } return $this->_choices; } function getSearchMethods() { return array( 'includes' => __('is'), '!includes' => __('is not'), ); } function addToQuery($query, $name=false) { return $query->values('dept_id', 'dept__name'); } function applyOrderBy($query, $reverse=false, $name=false) { $reverse = $reverse ? '-' : ''; return $query->order_by("{$reverse}dept__name"); } } class AssigneeChoiceField extends ChoiceField { protected $_items; function getChoices($verbose=false, $options=array()) { global $thisstaff; if (!isset($this->_items)) { $items = array( 'M' => __('Me'), 'T' => __('One of my teams'), ); $assignees = Staff::getStaffMembers(array('staff' => $thisstaff)); foreach ($assignees as $id=>$name) { // Don't include $thisstaff (since that's 'Me') if ($thisstaff && $thisstaff->getId() == $id) continue; $items['s' . $id] = $name; } foreach (Team::getTeams() as $id=>$name) { $items['t' . $id] = $name; } $this->_items = $items; } return $this->_items; } function getChoice($k) { $choices = $this->getChoices(); return $choices[$k] ?: null; } function getSearchMethods() { return array( 'assigned' => __('assigned'), '!assigned' => __('unassigned'), 'includes' => __('includes'), '!includes' => __('does not include'), ); } function getSearchMethodWidgets($options=array()) { return array( 'assigned' => null, '!assigned' => null, 'includes' => array('ChoiceField', array( 'choices' => $this->getChoices(false, $options), 'configuration' => array('multiselect' => true), )), '!includes' => array('ChoiceField', array( 'choices' => $this->getChoices(false, $options), 'configuration' => array('multiselect' => true), )), ); } function getSearchQ($method, $value, $name=false) { global $thisstaff; $Q = new Q(); switch ($method) { case 'assigned': $Q->negate(); case '!assigned': $Q->add(array('team_id' => 0, 'staff_id' => 0)); break; case '!includes': $Q->negate(); case 'includes': $teams = $agents = array(); $matches = count($value); foreach ($value as $id => $ST) { switch ($id[0]) { case 'M': $agents[] = $thisstaff->getId(); break; case 's': $agents[] = (int) substr($id, 1); break; case 'T': if ($thisstaff && ($staffTeams = $thisstaff->getTeams())) $teams = array_merge($staffTeams); elseif ($matches == 1) return Q::any(['team_id' => null]); break; case 't': $teams[] = (int) substr($id, 1); break; } } $constraints = array(); if ($teams) $constraints['team_id__in'] = $teams; if ($agents) $constraints['staff_id__in'] = $agents; $Q->add(Q::any($constraints)); } return $Q; } function describeSearchMethod($method) { switch ($method) { case 'assigned': return __('assigned'); case '!assigned': return __('unassigned'); default: return parent::describeSearchMethod($method); } } function addToQuery($query, $name=false) { $fields = array(); foreach(Staff::getsortby('staff__') as $key) $fields[] = new SqlField($key); $fields[] = new SqlField('team__name'); $fields[] = 'zzz'; $expr = call_user_func_array(array('SqlFunction', 'COALESCE'), $fields); $query->annotate(array($name ?: 'assignee' => $expr)); return $query->values('staff__firstname', 'staff__lastname', 'team__name', 'team_id'); } function from_query($row, $name=false) { if ($row['staff__firstname']) return new AgentsName(array('first' => $row['staff__firstname'], 'last' => $row['staff__lastname'])); if ($row['team_id']) return Team::getLocalById($row['team_id'], 'name', $row['team__name']); } function display($value) { return (string) $value; } function toString($value) { if (!is_array($value)) $value = array($value => $value); $selection = array(); foreach ($value as $k => $v) $selection[] = $this->getChoice($k) ?: (string) $v; return implode(', ', $selection); } } class AssignedField extends AssigneeChoiceField { function getChoices($verbose=false, $options=array()) { return array( 'assigned' => __('Assigned'), '!assigned' => __('Unassigned'), ); } function getSearchMethods() { return array( 'assigned' => __('assigned'), '!assigned' => __('unassigned'), ); } function addToQuery($query, $name=false) { return $query->values('staff_id', 'team_id'); } function from_query($row, $name=false) { return ($row['staff_id'] || $row['staff_id']) ? __('Yes') : __('No'); } } class MergedField extends FormField { function getSearchMethods() { return array( 'set' => __('checked'), 'nset' => __('unchecked'), ); } function addToQuery($query, $name=false) { $query->annotate(array( 'merged' => new SqlExpr(new Q(array( Q::any(array( 'flags__hasbit' => Ticket::FLAG_SEPARATE_THREADS, 'flags__hasbit' => Ticket::FLAG_COMBINE_THREADS, ))) )))); return $query->values('merged'); } function getSearchQ($method, $value, $name=false) { global $thisstaff; $Q = new Q(); switch ($method) { case 'set': $visibility = Q::any(array( 'flags__hasbit' => Ticket::FLAG_SEPARATE_THREADS, )); $visibility->add(Q::any(array( 'flags__hasbit' => Ticket::FLAG_COMBINE_THREADS ))); $visibility->ored = true; return $visibility; case 'nset': $visibility = Q::all(array()); $visibility->add(Q::not(array( 'flags__hasbit' => Ticket::FLAG_SEPARATE_THREADS, ))); $visibility->add(Q::not(array( 'flags__hasbit' => (Ticket::FLAG_COMBINE_THREADS) ))); return $visibility; break; } } function from_query($row, $name=false) { $flags = $row['flags']; $combine = ($flags & Ticket::FLAG_COMBINE_THREADS) != 0; $separate = ($flags & Ticket::FLAG_SEPARATE_THREADS) != 0; return ($combine || $separate) ? __('Yes') : __('No'); } } class LinkedField extends FormField { function getSearchMethods() { return array( 'set' => __('checked'), 'nset' => __('unchecked'), ); } function addToQuery($query, $name=false) { return $query->values('ticket_pid', 'flags'); } function getSearchQ($method, $value, $name=false) { global $thisstaff; $Q = new Q(); switch ($method) { case 'set': return Q::any(array( 'flags__hasbit' => Ticket::FLAG_LINKED, )); case 'nset': return Q::not(array( 'flags__hasbit' => Ticket::FLAG_LINKED,)); break; } } function from_query($row, $name=false) { $flags = $row['flags']; $linked = ($flags & Ticket::FLAG_LINKED) != 0; return ($linked) ? __('Yes') : __('No'); } } /** * Simple trait which changes the SQL for "has a value" and "does not have a * value" to check for zero or non-zero. Useful for not nullable fields. */ trait ZeroMeansUnset { function getSearchQ($method, $value, $name=false) { $name = $name ?: $this->get('name'); switch ($method) { // osTicket commonly uses `0` to represent an unset state, so // the set and unset checks need to check for both not null and // nonzero case 'nset': return new Q([$name => 0]); case 'set': return Q::not([$name => 0]); } return parent::getSearchQ($method, $value, $name); } } class AgentSelectionField extends AdvancedSearchSelectionField { use ZeroMeansUnset; static $_allagents; static $_agents; function getAgents($criteria=array()) { $dept = $criteria['dept'] ?: null; $staff = $criteria['staff'] ?: null; $agents = array(); if ($dept) { foreach ($dept->getAssignees(array('staff' => $staff)) as $a) $agents[$a->getId()] = $a; } else { foreach (Staff::getStaffMembers(array('staff' => $staff)) as $id => $name) { if ($staff && $staff->getId() == $id) $agents['M'] = __('Me'); $agents[$id] = $name; } } return $agents; } function getChoices($verbose=false, $options=array()) { global $thisstaff; $config = $this->getConfiguration(); $criteria = array( 'dept' => $config['dept'] ?: null, 'staff' => $config['staff'] ?: $thisstaff ); if (!isset($this->_choices)) $this->_choices = $this->getAgents($criteria); return $this->_choices; } function toString($value) { if (!isset($this->_allagents)) $this->_allagents = $this->getAgents(); $choices = $this->_allagents; $selection = array(); if (!is_array($value)) $value = array($value => $value); foreach ($value as $k => $v) if (isset($choices[$k])) $selection[] = $choices[$k]; return $selection ? implode(',', $selection) : parent::toString($value); } function getSearchQ($method, $value, $name=false) { global $thisstaff; // unpack me if (isset($value['M']) && $thisstaff) { $value[$thisstaff->getId()] = $thisstaff->getName(); unset($value['M']); } return parent::getSearchQ($method, $value, $name); } function getSortKeys($path='') { return Staff::getsortby('staff__'); } function applyOrderBy($query, $reverse=false, $name=false) { $reverse = $reverse ? '-' : ''; return Staff::nsort($query, "{$reverse}staff__"); } } class DepartmentManagerSelectionField extends AgentSelectionField { static $_members; function getChoices($verbose=false, $options=array()) { global $thisstaff; if (!isset($this->_members)) { $managers = array(); $mgr = Dept::objects()->filter(array('manager_id__gt' => 0))->values_flat('manager_id'); $staff = $thisstaff->getDeptAgents(array('available' => true, 'namesOnly' => true)); foreach ($mgr as $mid) { $mid = $mid[0]; if (array_key_exists($mid, $staff)) $managers['s'.$mid] = $staff[$mid]->getName()->name; } $this->_members = $managers; } return $this->_members; } function getSearchQ($method, $value, $name=false) { return parent::getSearchQ($method, $value, 'dept__manager_id'); } } class TeamSelectionField extends AdvancedSearchSelectionField { static $_teams; function getChoices($verbose=false, $options=array()) { if (!isset($this->_teams) && $teams = Team::getTeams()) $this->_teams = array('T' => __('One of my teams')) + $teams; return $this->_teams; } function getSearchQ($method, $value, $name=false) { global $thisstaff; // Unpack my teams if (isset($value['T'])) { if (!$thisstaff || !($teams = $thisstaff->getTeams())) return Q::any(['team_id' => null]); unset($value['T']); $value = $value + array_flip($teams); } return parent::getSearchQ($method, $value, $name); } function getSortKeys($path) { return array('team__name'); } function applyOrderBy($query, $reverse=false, $name=false) { $reverse = $reverse ? '-' : ''; return $query->order_by("{$reverse}team__name"); } function toString($value) { $choices = $this->getChoices(); $selection = array(); if (!is_array($value)) $value = array($value => $value); foreach ($value as $k => $v) if (isset($choices[$k])) $selection[] = $choices[$k]; return $selection ? implode(',', $selection) : parent::toString($value); } } class TicketStateChoiceField extends AdvancedSearchSelectionField { function getChoices($verbose=false, $options=array()) { return array( 'open' => __('Open'), 'closed' => __('Closed'), 'archived' => _P('ticket state name', 'Archived'), 'deleted' => _P('ticket state name','Deleted'), ); } function getSearchMethods() { return array( 'includes' => __('is'), '!includes' => __('is not'), ); } function getSearchQ($method, $value, $name=false) { return parent::getSearchQ($method, $value, 'status__state'); } } class TicketFlagChoiceField extends ChoiceField { function getChoices($verbose=false, $options=array()) { return array( 'isanswered' => __('Answered'), 'isoverdue' => __('Overdue'), ); } function getSearchMethods() { return array( 'includes' => __('is'), '!includes' => __('is not'), ); } function getSearchQ($method, $value, $name=false) { $Q = new Q(); if (isset($value['isanswered'])) $Q->add(array('isanswered' => 1)); if (isset($value['isoverdue'])) $Q->add(array('isoverdue' => 1)); if ($method == '!includes') $Q->negate(); if ($Q->constraints) return $Q; } } class TicketSourceChoiceField extends ChoiceField { function getChoices($verbose=false, $options=array()) { return Ticket::getSources(); } function getSearchMethods() { return array( 'includes' => __('is'), '!includes' => __('is not'), ); } function getSearchQ($method, $value, $name=false) { return parent::getSearchQ($method, $value, 'source'); } } class OpenClosedTicketStatusList extends TicketStatusList { function getItems($criteria=array()) { $rv = array(); $base = parent::getItems($criteria); foreach ($base as $idx=>$S) { if (in_array($S->state, array('open', 'closed'))) $rv[$idx] = $S; } return $rv; } } class TicketStatusChoiceField extends SelectionField { static $widget = 'ChoicesWidget'; function getList() { return new OpenClosedTicketStatusList( DynamicList::lookup( array('type' => 'ticket-status')) ); } function getSearchMethods() { return array( 'includes' => __('is'), '!includes' => __('is not'), ); } function getSearchQ($method, $value, $name=false) { $name = $name ?: $this->get('name'); if (!$value) return false; switch ($method) { case '!includes': return Q::not(array("{$name}__in" => array_keys($value))); case 'includes': return new Q(array("{$name}__in" => array_keys($value))); default: return parent::getSearchQ($method, $value, $name); } } function applyOrderBy($query, $reverse=false, $name=false) { $reverse = $reverse ? '-' : ''; return $query->order_by("{$reverse}status__name"); } } /* * Implemented by annotated fields * */ interface AnnotatedField { // Add the annotation to a QuerySet function annotate($query, $name); } class TicketThreadCountField extends NumericField implements AnnotatedField { function addToQuery($query, $name=false) { return TicketThreadCount::addToQuery($query, $name); } function from_query($row, $name=false) { return TicketThreadCount::from_query($row, $name); } function annotate($query, $name) { return TicketThreadCount::annotate($query, $name); } } class TicketReopenCountField extends NumericField implements AnnotatedField { function addToQuery($query, $name=false) { return TicketReopenCount::addToQuery($query, $name); } function from_query($row, $name=false) { return TicketReopenCount::from_query($row, $name); } function annotate($query, $name) { return TicketReopenCount::annotate($query, $name); } } class ThreadAttachmentCountField extends NumericField implements AnnotatedField { function addToQuery($query, $name=false) { return ThreadAttachmentCount::addToQuery($query, $name); } function from_query($row, $name=false) { return ThreadAttachmentCount::from_query($row, $name); } function annotate($query, $name) { return ThreadAttachmentCount::annotate($query, $name); } } class ThreadCollaboratorCountField extends NumericField implements AnnotatedField { function addToQuery($query, $name=false) { return ThreadCollaboratorCount::addToQuery($query, $name); } function from_query($row, $name=false) { return ThreadCollaboratorCount::from_query($row, $name); } function annotate($query, $name) { return ThreadCollaboratorCount::annotate($query, $name); } } class TicketTasksCountField extends NumericField implements AnnotatedField { function addToQuery($query, $name=false) { return TicketTasksCount::addToQuery($query, $name); } function from_query($row, $name=false) { return TicketTasksCount::from_query($row, $name); } function annotate($query, $name) { return TicketTasksCount::annotate($query, $name); } } interface Searchable { // Fetch an array of [ orm__path => Field() ] pairs. The field label is // used when this list is rendered in a dropdown, and the field search // mechanisms are use to apply query filtering based on the field. static function getSearchableFields(); // Determine if the object supports abritrary form additions, through // the "Manage Forms" dialog usually static function supportsCustomData(); }
Save