Newer
Older
<?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;
}
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)),
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)),
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)),
array(
'title'=> Format::searchable($model->getName()),
'created'=> $model->getCreateDate(),
)
);
break;
case $model instanceof FAQ:
$this->update($model, $model->getId(),
$model->getSearchableAnswer(),
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 (!$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;
function __construct() {
$this->SEARCH_TABLE = TABLE_PREFIX . '_search';
}
function getConfig() {
if (!isset($this->config))
$this->config = new MySqlSearchConfig();
return $this->config;
}
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'];
$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
Loading
Loading full blame...