mirror of
https://github.com/EGroupware/egroupware.git
synced 2024-11-26 18:03:39 +01:00
387 lines
11 KiB
PHP
387 lines
11 KiB
PHP
<?php
|
|
/**
|
|
* API - eGW wide index over all applications (super-index)
|
|
*
|
|
* @link http://www.egroupware.org
|
|
* @author Ralf Becker <RalfBecker-AT-outdoor-training.de>
|
|
* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
|
|
* @package api
|
|
* @subpackage link
|
|
* @version $Id$
|
|
*/
|
|
|
|
/**
|
|
* eGW wide index over all applications (superindex)
|
|
*
|
|
* This index allows a fulltext search over all applications (or of cause also a single app).
|
|
* Whenever an applications stores an entry it calls:
|
|
*
|
|
* boolean egw_index::save($app,$id,$owner,array $fields,array $cat_id=null),
|
|
*
|
|
* which calls, as the application do when is deletes an entry (!),
|
|
*
|
|
* boolean egw_index::delete($app,$id)
|
|
*
|
|
* and then splits all fields into keywords and add these to the index by
|
|
*
|
|
* boolean private egw_index::add($app,$id,$keyword).
|
|
*
|
|
* Applications can then use the index to search for a given keyword (and optional application):
|
|
*
|
|
* array egw_index::search($keyword,$app=null) or
|
|
*
|
|
* foreach(new egw_index($keyword,$app=null) as $app_id => $title)
|
|
*
|
|
* To also allow to search by a category or keyword part of it, the index also tracks the categories
|
|
* of the entries. Applications can choose to only use it for category storage, or cat do it redundant in
|
|
* there own table too. To retrieve the categories of one or multiple entries:
|
|
*
|
|
* array egw_index::cats($app,$ids)
|
|
*
|
|
* Applications can use a sql (sub-)query to get the id's of there app matching a certain keyword and
|
|
* include that in there own queries:
|
|
*
|
|
* string egw_index::sql_ids_by_keyword($app,$keyword)
|
|
*
|
|
* Please note: the index knows nothing about ACL, so it's the task of the application to ensure ACL rights.
|
|
*/
|
|
|
|
class egw_index implements IteratorAggregate
|
|
{
|
|
const INDEX_TABLE = 'egw_index';
|
|
const KEYWORD_TABLE = 'egw_index_keywords';
|
|
const INDEX_CAT_TABLE = 'egw_cat2entry';
|
|
const CAT_TABLE = 'egw_categories';
|
|
const SEPARATORS = "/[ ,;.:\"'!\/?=()+*><|\n\r-]+/";
|
|
const MIN_KEYWORD_LEN = 4;
|
|
|
|
/**
|
|
* Private reference to the global db object
|
|
*
|
|
* @var egw_db
|
|
*/
|
|
private static $db;
|
|
/**
|
|
* Search parameters of the constructor
|
|
*
|
|
* @var array
|
|
*/
|
|
private $search_params;
|
|
|
|
/**
|
|
* Constructor for the search iterator
|
|
*
|
|
* @param string $keyword
|
|
* @param string $app=null
|
|
* @param string $order='app' ordered by column: 'owner', 'id', 'app' (default)
|
|
* @param string $sort='ASC' sorting 'ASC' (default) or 'DESC'
|
|
* @param int $start=null if not null return limited resultset starting with row $start
|
|
* @param int $num_rows=0 number of rows for a limited resultset, defaul maxmatches from the user prefs
|
|
*/
|
|
function __construct($keyword,$app=null,$order='title',$sort='ASC',$start=null,$num_rows=0)
|
|
{
|
|
$this->search_params = func_get_args();
|
|
}
|
|
|
|
/**
|
|
* Return the result of egw_index::search() as ArrayIterator
|
|
*
|
|
* @return ArrayIterator
|
|
*/
|
|
function getIterator()
|
|
{
|
|
return new ArrayIterator(call_user_func_array(array(__CLASS__,'search'),$this->search_params));
|
|
}
|
|
|
|
/**
|
|
* Search for keywords
|
|
*
|
|
* @param string $keyword
|
|
* @param string $app=null
|
|
* @param string $order='app' ordered by column: 'keyword', 'id', 'app' (default)
|
|
* @param string $sort='ASC' sorting 'ASC' (default) or 'DESC'
|
|
* @param int $start=null if not null return limited resultset starting with row $start
|
|
* @param int $num_rows=null number of rows for a limited resultset, defaul maxmatches from the user prefs
|
|
* @return array with "$app:$id" or $id => $title pairs
|
|
*/
|
|
static function &search($keyword,$app=null,$order='title',$sort='ASC',$start=null,$num_rows=null)
|
|
{
|
|
if (!in_array(strtoupper($sort),array('ASC','DESC'))) $sort = 'ASC';
|
|
if (substr($order,0,3) != 'si_') $order = 'si_'.$order;
|
|
if (!in_array($order,array('si_app','si_id','si_owner'))) $order = 'si_app';
|
|
|
|
$rs = self::$db->union(array(
|
|
array(
|
|
'table' => self::INDEX_TABLE,
|
|
'cols' => 'si_app,si_app_id,si_owner',
|
|
'where' => array('keyword' => $keyword)+
|
|
($app ? array('ce_app' => $app) : array()),
|
|
),
|
|
array(
|
|
'table' => self::INDEX_CAT_TABLE,
|
|
'cols' => 'ce_app,ce_app_id,ce_owner',
|
|
'where' => array('cat_id IN (SELECT cat_id FROM '.self::CAT_TABLE.' WHERE cat_title '.
|
|
self::$db->capabilities['case_insensitive_like'].' '.self::$db->quote('%'.$keyword.'%').')')+
|
|
($app ? array('ce_app' => $app) : array()),
|
|
),
|
|
),__LINE__,__FILE__,$order.' '.$sort,$start,$num_rows);
|
|
|
|
// agregate the ids by app
|
|
$app_ids = $titles = $rows = array();
|
|
foreach($rs as $row)
|
|
{
|
|
$app_ids[$row['si_app']] = $row['si_app_id'];
|
|
$rows[] = $row;
|
|
}
|
|
unset($rs);
|
|
|
|
// query the titles app-wise
|
|
foreach($app_ids as $id_app => $ids)
|
|
{
|
|
$titles[$id_app] = bolink::titles($id_app,$ids);
|
|
}
|
|
$matches = array();
|
|
foreach($rows as $row)
|
|
{
|
|
$key = $app ? $row['si_app_id'] : $row['si_app'].':'.$row['si_app_id'];
|
|
$title = $titles[$row['si_app']][$row['si_app_id']];
|
|
if (is_null($title)) // entry does not exist
|
|
{
|
|
self::delete($row['si_app'],$row['si_app_id']);
|
|
error_log(__METHOD__.": not existing entry (is_null(title($row[si_app],$row[si_app_id])) deleted from index!");
|
|
continue;
|
|
}
|
|
elseif($title === false)
|
|
{
|
|
$title = lang('Not readable %1 entry of user %2',lang($row['si_app']),$GLOBALS['egw']->common->grab_owner_name($row['si_owner']));
|
|
}
|
|
$matches[$key] = $title;
|
|
}
|
|
return $matches;
|
|
//return iterator_to_array(new egw_index($keyword,$app,$order,$sort,$start,$num_rows),true);
|
|
}
|
|
|
|
/**
|
|
* Stores the keywords for an entry in the index
|
|
*
|
|
* @param string $app
|
|
* @param string|int $id
|
|
* @param string $owner eGW account_id of the owner of the entry, used to create a "private entry of ..." title
|
|
* @param array $fields
|
|
* @param array|int|string $cat_ids=null optional cat_id(s) either comma-separated or as array
|
|
* @param array $ignore_fields=array() keys of fields NOT to index
|
|
* @return int|boolean false on error, othwerwise number off added keywords
|
|
*/
|
|
static function save($app,$id,$owner,array $fields,$cat_ids=null,array $ignore_fields=array())
|
|
{
|
|
if (!$app || !$id)
|
|
{
|
|
return false;
|
|
}
|
|
// collect the keywords of all fields
|
|
$keywords = array();
|
|
foreach($fields as $field)
|
|
{
|
|
if ($ignore_fields && in_array($field, $ignore_fields)) continue;
|
|
|
|
foreach(preg_split(self::SEPARATORS, $field) as $keyword)
|
|
{
|
|
if (!in_array($keyword,$keywords) && strlen($keyword) >= self::MIN_KEYWORD_LEN && !is_numeric($keyword))
|
|
{
|
|
$keywords[] = $keyword;
|
|
}
|
|
}
|
|
}
|
|
// delete evtl. existing current keywords
|
|
self::delete($app,$id);
|
|
|
|
// add the keywords
|
|
foreach($keywords as $key => &$keyword)
|
|
{
|
|
if (!self::add($app,$id,$keyword,$owner)) // add can reject keywords
|
|
{
|
|
unset($keywords[$key]);
|
|
}
|
|
}
|
|
|
|
// delete the existing cats
|
|
self::delete_cats($app,$id);
|
|
|
|
// add the cats
|
|
if ($cat_ids)
|
|
{
|
|
self::add_cats($app,$id,$cat_ids,$owner);
|
|
}
|
|
return count($keywords);
|
|
}
|
|
|
|
/**
|
|
* Delete the keywords for an entry or an entire application
|
|
*
|
|
* @param string $app
|
|
* @param string|int $id=null
|
|
*/
|
|
static function delete($app,$id=null)
|
|
{
|
|
if (!$app)
|
|
{
|
|
return false;
|
|
}
|
|
$where = array('si_app' => $app);
|
|
if ($id)
|
|
{
|
|
$where['si_app_id'] = $id;
|
|
}
|
|
return !!self::$db->delete(self::INDEX_TABLE,$where,__LINE__,__FILE__);
|
|
}
|
|
|
|
/**
|
|
* Returns the cats of an entry or multiple entries
|
|
*
|
|
* @param string $app
|
|
* @param string|int|array $ids
|
|
* @return array with cats or single id or id => array with cats pairs
|
|
*/
|
|
static function cats($app,$ids)
|
|
{
|
|
if (!$app || !$ids)
|
|
{
|
|
return array();
|
|
}
|
|
$cats = array();
|
|
foreach(self::$db->select(self::INDEX_CAT_TABLE,'cat_id,ce_app_id',array(
|
|
'ce_app' => $app,
|
|
'ce_app_id' => $ids,
|
|
),__LINE__,__FILE__) as $row)
|
|
{
|
|
$cats[$row['ce_app_id']][] = $row['cat_id'];
|
|
}
|
|
return is_array($ids) ? $cats : $cats[(int)$ids];
|
|
}
|
|
|
|
/**
|
|
* Get the SQL to fetch (eg. as subquery) the id's of a given app matching a keyword
|
|
*
|
|
* @param string $keyword
|
|
* @param string $app
|
|
* @return string
|
|
*/
|
|
static function sql_ids_by_keyword($keyword,$app)
|
|
{
|
|
return '(SELECT si_id FROM '.self::INDEX_TABLE.' WHERE si_app='.self::$db->quote($app).
|
|
' AND si_keyword = '.self::$db->quote($keyword).') UNION '.
|
|
'(SELECT ce_id FROM '.self::INDEX_CAT_TABLE.' WHERE si_app='.self::$db->quote($app).
|
|
' AND cat_id IN (SELECT cat_id FROM '.self::CAT_TABLE.' WHERE cat_title '.self::$db->capabilities['case_insensitive_like'].' '.
|
|
self::$db->quote('%'.$keyword.'%').'))';
|
|
}
|
|
|
|
/**
|
|
* Stores one keyword for an entry in the index
|
|
*
|
|
* @todo reject keywords which are common words ...
|
|
* @param string $app
|
|
* @param string|int $id
|
|
* @param string $keyword
|
|
* @param int $owner=null
|
|
* @return boolean true if keyword added, false if it was rejected in future
|
|
*/
|
|
static private function add($app,$id,$keyword,$owner=null)
|
|
{
|
|
// todo: reject keywords which are common words, not sure how to do that for all languages
|
|
// mayby we can come up with some own little statistic analysis:
|
|
// all keywords more common then N % of the entries get deleted and moved to a separate table ...
|
|
if (!($si = self::$db->select(self::KEYWORD_TABLE, '*', array('si_keyword' => $keyword))->fetch()))
|
|
{
|
|
self::$db->insert(self::KEYWORD_TABLE, array(
|
|
'si_keyword' => $keyword,
|
|
), false, __LINE__, __FILE__);
|
|
|
|
$si_id = self::$db->get_last_insert_id(self::KEYWORD_TABLE, 'si_id');
|
|
}
|
|
elseif ($si['si_ignore'])
|
|
{
|
|
return false;
|
|
}
|
|
else
|
|
{
|
|
$si_id = $si['si_id'];
|
|
}
|
|
self::$db->insert(self::INDEX_TABLE,array(
|
|
'si_app' => $app,
|
|
'si_app_id' => $id,
|
|
'si_id' => $si_id,
|
|
'si_owner' => $owner,
|
|
),false,__LINE__,__FILE__);
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Stores the cat_id(s) for an entry
|
|
*
|
|
* @param string $app
|
|
* @param string|int $id
|
|
* @param array|int|string $cat_ids=null optional cat_id(s) either comma-separated or as array
|
|
* @param int $owner=null
|
|
* @return boolean true on success, false on error
|
|
*/
|
|
static private function add_cats($app,$id,$cat_ids,$owner=null)
|
|
{
|
|
if (!$app)
|
|
{
|
|
return false;
|
|
}
|
|
if (!$cat_ids)
|
|
{
|
|
return true; // nothing to do
|
|
}
|
|
foreach(is_array($cat_ids) ? $cat_ids : explode(',',$cat_ids) as $cat_id)
|
|
{
|
|
self::$db->insert(self::INDEX_CAT_TABLE,array(
|
|
'cat_id' => $cat_id,
|
|
'ce_app' => $app,
|
|
'ce_app_id' => $id,
|
|
'ce_owner' => $owner,
|
|
),false,__LINE__,__FILE__);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Delete the cat for an entry or an entire application
|
|
*
|
|
* @param string $app
|
|
* @param string|int $id=null
|
|
*/
|
|
static private function delete_cats($app,$id=null)
|
|
{
|
|
if (!$app)
|
|
{
|
|
return false;
|
|
}
|
|
$where = array('ce_app' => $app);
|
|
if ($id)
|
|
{
|
|
$where['ce_app_id'] = $id;
|
|
}
|
|
return !!self::$db->delete(self::INDEX_CAT_TABLE,$where,__LINE__,__FILE__);
|
|
}
|
|
|
|
/**
|
|
* Init our static vars
|
|
*/
|
|
static function _init_static()
|
|
{
|
|
if (!is_object($GLOBALS['egw_setup']))
|
|
{
|
|
self::$db = $GLOBALS['egw']->db;
|
|
}
|
|
else
|
|
{
|
|
self::$db = $GLOBALS['egw_setup']->db;
|
|
}
|
|
}
|
|
}
|
|
|
|
egw_index::_init_static();
|