egroupware_official/api/src/Categories.php

1155 lines
36 KiB
PHP

<?php
/**
* EGroupware API - Categories
*
* @link http://www.egroupware.org
* @author Joseph Engo <jengo@phpgroupware.org>
* @author Bettina Gille <ceb@phpgroupware.org>
* @author Ralf Becker <RalfBecker-AT-outdoor-training.de>
* Copyright (C) 2000, 2001 Joseph Engo, Bettina Gille
* Copyright (C) 2002, 2003 Bettina Gille
* Reworked 11/2005 by RalfBecker-AT-outdoor-training.de
* Reworked 12/2008 by RalfBecker-AT-outdoor-training.de to operate only on a catergory cache, no longer the db direct
* @license http://opensource.org/licenses/lgpl-license.php LGPL - GNU Lesser General Public License
* @package api
* @subpackage categories
* @access public
* @version $Id$
*/
namespace EGroupware\Api;
/**
* class to manage categories in eGroupWare
*
* Categories are read now once from the database into a static cache variable (by the static init_cache method).
* The egw object fills that cache ones per session, stores it in a private var, from which it restores it for each
* request of that session.
*
* $cat['data'] array:
* ------------------
* $cat['data'] array is stored serialized in the database to allow applications to simply add all
* sorts of values there (without the hassel of a DB schema change).
* Before 14.1 data was PHP serialize(d), if it was an array, now it get json_encode(d),
* we use json_php_unserialize(, true) to automatic cope with that (true allows strings too)!
* Static methods categories::read($cat_id) and categories::id2name now returns data already as array
* and add() or edit() methods automatically json-encode $cat['data'], if it's an array.
* return*() methods return $cat['data'] now as array by default!
*
* @ToDo The cache now contains a backlink from the parent to it's children. Use that link to simplyfy return_all_children
* and other functions needing to know if a cat has children. Be aware a user might not see all children, as they can
* belong to other users.
*/
class Categories
{
/**
* Account id this class is instanciated for (self::GLOBAL_ACCOUNT for global cats)
*
* @var int
*/
public $account_id;
/**
* Application this class is instancated for (self::GLOBAL_APPNAME for application global cats)
*
* @var string
*/
public $app_name;
/**
* @var Db
*/
private $db;
/**
* Total number of records of return_(sorted_)array (returning only a limited number of rows)
*
* @var int
*/
public $total_records;
/**
* Grants from other users for account_id and app_name (init by return array)
*
* @var array
*/
public $grants;
/**
* Name of the categories table
*/
const TABLE = 'egw_categories';
/**
* @deprecated use Categories::TABLE
* @var string
*/
public $table = self::TABLE;
/**
* Cache holding all categories, set via init_cache() method
*
* @var array cat_id => array of data
*/
protected static $cache;
const CACHE_APP = 'phpgwapi';
const CACHE_NAME = 'cat_cache';
/**
* Appname for global categories
*/
const GLOBAL_APPNAME = 'phpgw';
/**
* account_id for global categories
*/
const GLOBAL_ACCOUNT = 0;
/**
* Owners for global accounts
*
* Usually the users group memberships and self::GLOBAL_ACCOUNT
*
* @var array
*/
private $global_owners = array(self::GLOBAL_ACCOUNT);
/**
* string to postfix global cats
*
* @var string
*/
static public $global_marker;
/**
* constructor for Categories class
*
* @param int|string $accountid ='' account id or lid, default to current user
* @param string $app_name ='' app name defaults to current app
*/
function __construct($accountid='',$app_name = '')
{
if (!$app_name) $app_name = $GLOBALS['egw_info']['flags']['currentapp'];
if ($accountid === self::GLOBAL_ACCOUNT ||
(int)$accountid < 0 && $GLOBALS['egw']->accounts->exists($accountid) == 2)
{
$this->account_id = (int) $accountid;
}
else
{
$this->account_id = (int) get_account_id($accountid);
$this->global_owners = $this->account_id ? $GLOBALS['egw']->accounts->memberships($this->account_id, true) : array();
$this->global_owners[] = self::GLOBAL_ACCOUNT;
}
$this->app_name = $app_name;
$this->db = $GLOBALS['egw']->db;
if (is_null(self::$cache)) // should not be necessary, as cache is load and restored by egw object
{
self::init_cache();
}
if (is_null(self::$global_marker))
{
// as et2 adds options with .text(), it can't be entities, but php knows no string literals with utf-8
}
}
/**
* returns array with id's of all children from $cat_id and $cat_id itself!
*
* @param int|array $cat_id (array of) integer cat-id to search for
* @return array of cat-id's
*/
function return_all_children($cat_id)
{
$all_children = array_map('intval', (array)$cat_id);
$children = $this->return_array('subs',0,False,'','','',True,$cat_id,-1,'id');
if (is_array($children) && count($children))
{
$all_children = array_merge($all_children,$this->return_all_children($children));
}
return $all_children;
}
/**
* return an array populated with categories
*
* @param string $type ='all' 'subs','mains','appandmains','appandsubs','noglobal','noglobalapp', defaults to 'all'
* @param int $start =0 see $limit
* @param boolean|int $limit if true limited query to maxmatches rows (starting with $start)
* @param string $query ='' query-pattern
* @param string $sort ='ASC' sort order, defaults to 'ASC'
* @param string $order ='' order by, default cat_main, cat_level, cat_name ASC
* @param boolean|string $globals includes the global egroupware categories or not,
* 'all_no_acl' to return global and all non-private user categories independent of ACL
* @param array|int $parent_id =null return only subcats of $parent_id(s)
* @param int $lastmod = -1 if > 0 return only cats modified since then
* @param string $column ='' if column-name given only that column is returned, not the full array with all cat-data
* @param array $filter =null array with column-name (without cat_-prefix) => value pairs (! negates the value)
* @return array of cat-arrays or $column values
*/
function return_array($type='all', $start=0, $limit=true, $query='', $sort='ASC',$order='',$globals=false, $parent_id=null, $lastmod=-1, $column='', $filter=null)
{
//error_log(__METHOD__."($type,$start,$limit,$query,$sort,$order,globals=$globals,parent=".array2string($parent_id).",$lastmod,$column,filter=".array2string($filter).") account_id=$this->account_id, appname=$this->app_name: ".function_backtrace());
$cats = array();
foreach(self::$cache as $cat)
{
if ($filter) foreach($filter as $col => $val)
{
if (!is_array($val) && $val[0] === '!')
{
// also match against trimmed database entry on name and description fields
if (($col == 'name' || $col == 'description') && is_string($cat[$col]))
{
if ($cat[$col] == substr($val,1) || trim($cat[$col]) == substr($val,1)) continue 2;
}
else
{
if ($cat[$col] == substr($val,1)) continue 2;
}
}
elseif (is_array($val))
{
// also match against trimmed database entry on name and description fields
if (($col == 'name' || $col == 'description') && is_string($cat[$col]))
{
if (!in_array($cat[$col],$val) && !in_array(trim($cat[$col]),$val)) continue 2;
}
else
{
if (!in_array($cat[$col],$val)) continue 2;
}
}
else
{
// also match against trimmed database entry on name and description fields
if (($col == 'name' || $col == 'description') && is_string($cat[$col]))
{
if ($cat[$col] != $val && trim($cat[$col]) != $val) continue 2;
}
else
{
if ($cat[$col] != $val) continue 2;
}
}
}
// check if certain parent required
if ($parent_id && !in_array($cat['parent'],(array)$parent_id)) continue;
// return global categories just if $globals is set
if (!$globals && !empty($cat['appname']) && $cat['appname'] === self::GLOBAL_APPNAME)
{
continue;
}
// check for read permission
if(!$this->check_perms(Acl::READ, $cat, $globals === 'all_no_acl'))
{
continue;
}
// check if we have the correct type
switch ($type)
{
case 'subs':
if (!$cat['parent']) continue 2; // 2 for switch AND foreach!
break;
case 'mains':
if ($cat['parent']) continue 2;
break;
case 'appandmains':
if ($cat['appname'] != $this->app_name || $cat['parent']) continue 2;
break;
case 'appandsubs':
if ($cat['appname'] != $this->app_name || !$cat['parent']) continue 2;
break;
case 'noglobal':
if ($cat['appname'] == $this->app_name) continue 2;
break;
case 'noglobalapp':
if ($cat['appname'] != $this->app_name || $cat['owner'] == (int)$this->account_id) continue 2;
break;
}
// check name and description for $query
if ($query && stristr($cat['name'],$query) === false && stristr($cat['description'],$query) === false) continue;
// check if last modified since
if ($lastmod > 0 && $cat['last_mod'] <= $lastmod) continue;
$cat['data'] = $cat['data'] ? json_php_unserialize($cat['data'], true) : array();
$cats[] = $cat;
}
if (!($this->total_records = count($cats)))
{
//error_log(__METHOD__."($type,$start,$limit,$query,$sort,$order,$globals,parent=$parent_id,$lastmod,$column) account_id=$this->account_id, appname=$this->app_name = FALSE");
return array();
}
if (!$sort) $sort = 'ASC';
// order the entries if necessary (cache is already ordered in or default order: cat_main, cat_level, cat_name ASC)
if ($this->total_records > 1 && !empty($order) &&
preg_match('/^[a-zA-Z_(), ]+$/',$order) && preg_match('/^(ASC|DESC|asc|desc)$/',$sort))
{
if (strstr($order,'cat_data') !== false) $order = 'cat_data'; // sitemgr orders by round(cat_data)!
if (substr($order,0,4) == 'cat_') $order = substr($order,4);
$sign = strtoupper($sort) == 'DESC' ? -1 : 1;
usort($cats, function($a, $b) use ($order, $sign)
{
if (in_array($order, array('name','description','appname','app_name')))
{
return $sign * strcasecmp($a[$order], $b[$order]);
}
return $sign*(int)$a[$order] - $sign*(int)$b[$order];
});
}
// limit the number of returned rows
if ($limit)
{
if (!is_int($limit)) $limit = (int)$GLOBALS['egw_info']['user']['preferences']['common']['maxmatchs'];
$cats = array_slice($cats,(int)$start,$limit);
}
// return only a certain column (why not return is as value?)
if ($column)
{
foreach($cats as $k => $cat)
{
$cats[$k] = $cat[$column];
}
}
//error_log(__METHOD__."($type,$start,$limit,$query,$sort,$order,$globals,parent=".array2string($parent_id).",$lastmod,$column,filter=".array2string($filter).",$unserialize_data) account_id=$this->account_id, appname=$this->app_name = ".array2string($cats));
reset($cats); // some old code (eg. sitemgr) relies on the array-pointer!
return $cats;
}
/**
* return a sorted array populated with categories (main sorting criteria is hierachy!)
*
* @param int $start =0 see $limit
* @param boolean|int $limit if true limited query to maxmatches rows (starting with $start)
* @param string $query ='' query-pattern
* @param string $sort ='ASC' sort order, either defaults to 'ASC'
* @param string $order ='cat_name' order by
* @param boolean|string $globals includes the global egroupware categories or not,
* 'all_no_acl' to return global and all non-private user categories independent of ACL
* @param array|int $parent_id =0 return only subcats of $parent_id(s)
* @param boolean $unserialize_data =true return $cat['data'] as array (not serialized array)
* @return array with cats
*/
function return_sorted_array($start=0,$limit=True,$query='',$sort='ASC',$order='cat_name',$globals=False, $parent_id=0,$unserialize_data=true,$filter=null)
{
if (!$sort) $sort = 'ASC';
if (!$order) $order = 'cat_name';
//error_log(__METHOD__."($start,$limit,$query,$sort,$order,globals=$globals,parent=$parent_id,$unserialize_data) account_id=$this->account_id, appname=$this->app_name: ".function_backtrace());
$parents = $cats = array();
// Cast parent_id to array, but only if there is one
if($parent_id !== false && $parent_id !== null) $parent_id = (array)$parent_id;
if (!($cats = $this->return_array('all',0,false,$query,$sort,$order,$globals,$parent_id,-1,'',$filter,$unserialize_data)))
{
$cats = array();
}
foreach($cats as $cat)
{
$parents[] = $cat['id'];
}
if($parent_id || !$cats) // Avoid wiping search results
{
// Go find the children
while (count($parents))
{
if (!($subs = $this->return_array('all',0,false,$query,$sort,$order,$globals,$parents,-1,'',$filter,$unserialize_data)))
{
break;
}
$parents = $children = array();
foreach($subs as $cat)
{
$parents[] = $cat['id'];
$children[$cat['parent']][] = $cat;
}
// sort the cats into the mains
if (count($children))
{
$cats2 = $cats;
$cats = array();
foreach($cats2 as $cat)
{
$cats[] = $cat;
if (isset($children[$cat['id']]))
{
foreach($children[$cat['id']] as $child)
{
$cats[] = $child;
}
}
}
}
}
}
$this->total_records = count($cats);
// limit the number of returned rows
if ($limit)
{
if (!is_int($limit)) $limit = (int)$GLOBALS['egw_info']['user']['preferences']['common']['maxmatchs'];
$cats = array_slice($cats,(int)$start,$limit);
}
reset($cats); // some old code (eg. sitemgr) relies on the array-pointer!
return $cats;
}
/**
* Read a category
*
* We use a shared cache together with id2name
*
* Data array get automatically unserialized, if it was serialized!
*
* @param int $id id of category
* @return array|boolean array with cat-data or false if cat not found
*/
static function read($id)
{
if (is_null(self::$cache)) self::init_cache();
if (!isset(self::$cache[$id])) return false;
$cat = self::$cache[$id];
$cat['data'] = $cat['data'] ? ((($arr=json_php_unserialize($cat['data'], true)) !== false || $cat['data'] === 'b:0;') ?
$arr : $cat['data']) : array();
return $cat;
}
/**
* Add a category
*
* Owner and appname are set from the values used to instanciate the class!
*
* @param array $values cat-data
* @return int new cat-id
*/
function add($values)
{
if ((int)$values['parent'] > 0)
{
$values['level'] = $this->id2name($values['parent'],'level')+1;
$values['main'] = $this->id2name($values['parent'],'main');
}
else
{
$values['level'] = 0;
}
$this->db->insert(self::TABLE,$cat=array(
'cat_parent' => $values['parent'],
'cat_owner' => isset($values['owner']) ? $values['owner'] : $this->account_id,
'cat_access' => isset($values['access']) ? $values['access'] : 'public',
'cat_appname' => $this->app_name,
'cat_name' => $values['name'],
'cat_description' => isset($values['description']) ? $values['description'] : $values['descr'], // support old name different from returned one
'cat_data' => is_array($values['data']) ? json_encode($values['data']) : $values['data'],
'cat_main' => $values['main'],
'cat_level' => $values['level'],
'last_mod' => time(),
),(int)$values['id'] > 0 ? array('cat_id' => $values['id']) : array(),__LINE__,__FILE__);
$cat['cat_id'] = $id = (int)$values['id'] > 0 ? (int)$values['id'] : $this->db->get_last_insert_id(self::TABLE,'cat_id');
if (!(int)$values['parent'] && $id != $values['main'])
{
$this->db->update(self::TABLE,array('cat_main' => $id),array('cat_id' => $id),__LINE__,__FILE__);
$cat['cat_main'] = $id;
}
// update cache accordingly
self::invalidate_cache(Db::strip_array_keys($cat, 'cat_'));
return $id;
}
/**
* Checks category permissions for a given list of commaseparated category ids
* and truncates it by the ones the user does not have the requested permission on
*
* @param int $needed necessary ACL right: Acl::{READ|EDIT|DELETE}
* @param string $cat_list commaseparated list of category ids
* @return string truncated commaseparated list of category ids
*/
function check_list($needed, $cat_list)
{
if (empty($cat_list)) return $cat_list;
if (is_array($cat_list))
{
$cat_list = implode(',',$cat_list);
//error_log(__METHOD__.__LINE__.' string expected, array found for cat_list. Converted to:'.$cat_list);
}
$cat_arr = explode(',',$cat_list);
if (!empty($cat_arr) && is_array($cat_arr) && count($cat_arr) > 0)
{
foreach($cat_arr as $id=>$cat_id)
{
if (!$this->check_perms($needed, $cat_id, false, $needed == Acl::READ)) // allow reading all global cats
{
unset($cat_arr[$id]);
}
}
$cat_list = implode(',',$cat_arr);
}
return $cat_list;
}
/**
* Checks if the current user has the necessary ACL rights
*
* If the access of a category is set to private, one needs a private grant for the application
*
* @param int $needed necessary ACL right: Acl::{READ|EDIT|DELETE}
* @param mixed $category category as array or the category_id
* @param boolean $no_acl_check =false if true, grants are NOT checked, gives access to all non-private categories of all users
* @param boolean $allow_global_read if true, global cats are allowed (independent of app) for reading
* @return boolean true permission granted, false for permission denied, null for category does not exist
*/
public function check_perms($needed, $category, $no_acl_check=false, $allow_global_read=false)
{
if (!is_array($category) && !($category = self::read($category)))
{
return null;
}
// The user for the global cats has id self::GLOBAL_ACCOUNT, this one has full access to all global cats
if ($this->account_id == self::GLOBAL_ACCOUNT && ($category['appname'] == self::GLOBAL_APPNAME ||
$category['appname'] == $this->app_name && self::is_global($category)))
{
//echo "<p>".__METHOD__."($needed,$category[name]) access because class instanciated for GLOBAL ACCOUNT</p>\n";
return true;
}
// Read access to global categories
if ($needed == Acl::READ && (($is_global=isset($category['owner']) && array_intersect(explode(',',$category['owner']),$this->global_owners)) ||
$no_acl_check && $category['access'] == 'public') && // no_acl_check only means public cats
(($category['appname'] ?? null) === self::GLOBAL_APPNAME || ($category['appname'] ?? null) == $this->app_name ||
$is_global && $allow_global_read))
{
//echo "<p>".__METHOD__."($needed,$category[name]) access because global via memberships</p>\n";
return true;
}
// Full access to own categories
if (($category['appname'] ?? null) == $this->app_name && $category['owner'] == $this->account_id)
{
return true;
}
// if $no_acl_check is set, allow access to all public (non-private) categories
if ($no_acl_check && $category['access'] == 'public' && $this->account_id != self::GLOBAL_ACCOUNT && $category['appname'] == $this->app_name)
{
return true;
}
// Load the application grants
if (($category['appname'] ?? null) == $this->app_name && !isset($this->grants))
{
$this->grants = $GLOBALS['egw']->acl->get_grants($this->app_name,true);
}
// Check for ACL granted access, the self::GLOBAL_ACCOUNT user must not get access by ACL to keep old behaviour
$acl_grant = $this->account_id != self::GLOBAL_ACCOUNT && ($category['appname'] ?? null) == $this->app_name;
$owner_grant = false;
foreach(!empty($category['owner']) ? explode(',',$category['owner']) : [] as $owner)
{
$owner_grant = $owner_grant || (is_array($this->grants) && !empty($this->grants[$owner]) && ($this->grants[$owner] & $needed) &&
($category['access'] === 'public' || ($this->grants[$owner] & Acl::PRIVAT)));
}
return $acl_grant && $owner_grant;
}
/**
* delete a category
*
* @param int $cat_id category id
* @param boolean $drop_subs =false if true delete sub-cats too
* @param boolean $modify_subs =false if true make the subs owned by the parent of $cat_id
*/
function delete($cat_id, $drop_subs = False, $modify_subs = False)
{
//error_log(__METHOD__."(".array2string($cat_id).', drop_subs='.array2string($drop_subs).', modify_subs='.array2string($modify_subs).') '.function_backtrace());
if ($modify_subs)
{
$new_parent = $this->id2name($cat_id,'parent');
foreach ((array) $this->return_sorted_array('',False,'','','',False, $cat_id) as $cat)
{
if ($cat['level'] == 1)
{
$this->db->update(self::TABLE,array(
'cat_level' => 0,
'cat_parent' => 0,
'cat_main' => $cat['id'],
),array(
'cat_id' => $cat['id'],
'cat_appname' => $this->app_name,
),__LINE__,__FILE__);
$new_main = $cat['id'];
}
else
{
$update = array('cat_level' => $cat['level']-1);
if ($new_main) $update['cat_main'] = $new_main;
if ($cat['parent'] == $cat_id) $update['cat_parent'] = $new_parent;
$this->db->update(self::TABLE,$update,array(
'cat_id' => $cat['id'],
'cat_appname' => $this->app_name,
),__LINE__,__FILE__);
}
}
}
if ($drop_subs)
{
$where['cat_id'] = $this->return_all_children($cat_id);
}
else
{
$where['cat_id'] = $cat_id;
}
$where['cat_appname'] = $this->app_name;
$GLOBALS['hook_values'] = array(
'cat_id' => $cat_id,
'cat_name' => self::id2name($cat_id),
'drop_subs' => $drop_subs,
'modify_subs' => $modify_subs,
'location' => 'delete_category'
);
if($this->is_global($cat_id, true)) // true = application global (otherwise eg. global addressbook categories call all apps)
{
Hooks::process($GLOBALS['hook_values'],False,True); // called for every app now, not only enabled ones)
}
else
{
Hooks::single($GLOBALS['hook_values'], self::id2name($cat_id,'appname'));
}
$this->db->delete(self::TABLE,$where,__LINE__,__FILE__);
// update cache accordingly
self::invalidate_cache($modify_subs ? null : $where['cat_id']);
}
/**
* adapt_level_in_subtree of a category
*
* Owner and appname are set from the values used to instanciate the class!
*
* @param array $values array with cat-data (it need to be complete, as everything get's written)
* @return void
*/
function adapt_level_in_subtree($values)
{
foreach ((array) $this->return_sorted_array('',False,'','','',False, $values['id']) as $cat)
{
if ($cat['parent'] == $values['id'])
{
$this->db->update(self::TABLE,array(
'cat_level' => $values['level']+1,
'last_mod' => time(),
),array(
'cat_id' => $cat['id'],
'cat_appname' => $this->app_name,
),__LINE__,__FILE__);
$cat['level'] = $values['level'] + 1;
self::invalidate_cache($cat['id']);
$this->adapt_level_in_subtree($cat);
}
else
{
continue;
}
}
}
/**
* check_consistency4update - for edit
*
* @param array $values array with cat-data (it need to be complete, as everything get's written)
* @return mixed string/boolean errorstring if consitency check failed / true if the consistency check did not fail
*/
function check_consistency4update($values)
{
// check if we try to move an element down its own subtree, which will fail
foreach ($this->return_sorted_array('',False,'','','',False, $values['id']) as $cat)
{
if ($cat['id'] == $values['parent']) return lang('Cannot set a category as parent, which is part of this categorys subtree!');
}
// check if we try to be our own parent
if ($values['parent']==$values['id']) return lang('Cannot set this cat as its own parent!'); // deny to be our own parent
// check if parent still exists
if ((int)$values['parent']>0 && !$this->read($values['parent'])) return lang('Chosen parent category no longer exists');
return true;
}
/**
* edit / update a category
*
* Owner and appname are set from the values used to instanciate the class!
*
* @param array $values array with cat-data (it need to be complete, as everything get's written)
* @return int cat-id or false if it failed
*/
function edit($values)
{
if (isset($values['old_parent']) && (int)$values['old_parent'] != (int)$values['parent'])
{
$ret = $this->check_consistency4update($values);
if ($ret !== true) throw new Exception\WrongUserinput($ret);
// everything seems in order -> proceed
$values['level'] = ($values['parent'] ? $this->id2name($values['parent'],'level')+1:0);
$this->adapt_level_in_subtree($values);
return $this->add($values);
}
else
{
//echo "old parent not set <br>";
if ($values['parent'] > 0)
{
$ret = $this->check_consistency4update($values);
if ($ret !== true) throw new Exception\WrongUserinput($ret);
// everything seems in order -> proceed
$values['main'] = $this->id2name($values['parent'],'main');
$values['level'] = $this->id2name($values['parent'],'level') + 1;
}
else
{
//echo "new parent not set <br>";
$values['main'] = $values['id'];
$values['level'] = 0;
}
// adapt the level info in each child
$this->adapt_level_in_subtree($values);
}
$this->db->update(self::TABLE,$cat=array(
'cat_name' => $values['name'],
'cat_description' => isset($values['description']) ? $values['description'] : $values['descr'], // support old name different from the one read
'cat_data' => is_array($values['data']) ? json_encode($values['data']) : $values['data'],
'cat_parent' => $values['parent'],
'cat_access' => $values['access'],
'cat_owner' => isset($values['owner']) ? $values['owner'] : $this->account_id,
'cat_main' => $values['main'],
'cat_level' => $values['level'],
'last_mod' => time(),
),array(
'cat_id' => $values['id'],
'cat_appname' => $this->app_name,
),__LINE__,__FILE__);
$cat['cat_id'] = $values['id'];
$cat['cat_appname'] = $this->app_name;
// update cache accordingly
self::invalidate_cache(Db::strip_array_keys($cat, 'cat_'));
return (int)$values['id'];
}
/**
* return category id for a given name
*
* Cat's with the given name are returned in this order:
* - personal cats first
* - then application global categories
* - global categories
* - cat's of other user
*
* @param string $cat_name cat-name
* @param boolean|string $strip =false if true, strips 'X-' ($strip) from category names added by some SyncML clients
* @return int cat-id or 0 if not found
*/
function name2id($cat_name, $strip=false)
{
static $cache = array(); // a litle bit of caching
if (isset($cache[$cat_name])) return $cache[$cat_name];
if ($strip === true)
{
$strip = 'X-';
}
$cats = array($cat_name);
if ($strip && strncmp($strip, $cat_name, strlen($strip)) == 0)
{
$stripped_cat_name = substr($cat_name, strlen($strip));
if (isset($cache[$stripped_cat_name]))
{
$cache[$cat_name] = $cache[$stripped_cat_name];
return $cache[$stripped_cat_name];
}
$cats[] = $stripped_cat_name;
}
if (!($cats = $this->return_array('all',0,false,'','','',true,null,-1,'',array(
'name' => $cats,
'appname' => array($this->app_name, self::GLOBAL_APPNAME),
))))
{
return 0; // cat not found, dont cache it, as it might be created in this request
}
if (count($cats) > 1)
{
// if more the one cat matches we weight them by: exact name match; own, global, other users cat; appplication cats
foreach($cats as $k => $cat)
{
$cats[$k]['weight'] = 100 * ($cat['name'] == $cat_name) +
10 * ($cat['owner'] == $this->account_id ? 3 : ($cat['owner'] <= self::GLOBAL_ACCOUNT ? 2 : 1)) +
($cat['appname'] != self::GLOBAL_APPNAME);
}
// sort heighest weight to the top
usort($cats, function($a, $b)
{
return $b['weight'] - $a['weight'];
});
}
return $cache[$cat['cat_name']] = (int) $cats[0]['id'];
}
/**
* Check if category is global (owner <= 0 || appname == 'phpgw')
*
* @param int|array $cat
* @param boolean $application_global =false true check for application global categories only (appname == 'phpgw')
* @return boolean
*/
static function is_global($cat,$application_global=false)
{
if (!is_array($cat) && !($cat = self::read($cat))) return null; // cat not found
$global_owner = false;
foreach(explode(',',$cat['owner']) as $owner)
{
$global_owner = $global_owner || $owner <= self::GLOBAL_ACCOUNT;
}
return $global_owner && !$application_global || $cat['appname'] == self::GLOBAL_APPNAME;
}
/**
* return category information for a given id
*
* We use a shared cache together with read
* $item == 'data' is returned as array (not serialized array)!
*
* @param int $cat_id =0 cat-id
* @param string $item ='name' requested information, 'path' = / delimited path of category names (incl. parents)
* @return string information or '--' if not found or !$cat_id
*/
static function id2name($cat_id=0, $item='name')
{
if(!$cat_id) return '--';
if (!$item) $item = 'parent';
if (is_null(self::$cache)) self::init_cache();
$cat = self::$cache[$cat_id] ?? null;
if ($item == 'path')
{
if ($cat['parent'])
{
return self::id2name($cat['parent'],'path').' / '.$cat['name'];
}
$item = 'name';
}
if ($item == 'data')
{
return !empty($cat['data']) ? json_php_unserialize($cat['data'], true) : array();
}
elseif ($cat[$item])
{
return $cat[$item];
}
elseif ($item == 'name')
{
return '--';
}
return null;
}
/**
* check if a category id and/or name exists, if id AND name are given the check is for a category with same name and different id (!)
*
* @param string $type subs or mains
* @param string $cat_name ='' category name
* @param int $cat_id =0 category id
* @param int $parent =0 category id of parent
* @return int/boolean cat_id or false if cat not exists
*/
function exists($type,$cat_name = '',$cat_id = 0,$parent = 0)
{
if ($cat_name)
{
$filter['name'] = $cat_name;
if ($cat_id) $filter['id'] = '!'.(int)$cat_id;
}
elseif ($cat_id)
{
$filter['id'] = $cat_id;
}
if (!($cats = $this->return_array($type,0,false,'','','',true,$parent,-1,'id',$filter)))
{
$ret = false;
}
else
{
$ret = $cats[0];
}
//error_log(__METHOD__."($type,$cat_name,$cat_id,$parent) = ".$ret);
return $ret;
}
/**
* Change the owner of all cats owned by $owner to $to OR deletes them if !$to
*
* @param int $owner owner or the cats to delete or change
* @param int $to =0 new owner or 0 to delete the cats
* @param string $app ='' if given only cats matching $app are modifed/deleted
*/
function change_owner($owner,$to=0,$app='')
{
$where = array('cat_owner' => $owner);
if ($app) $where['cat_appname'] = $app;
if ((int)$to)
{
$this->db->update(self::TABLE,array('cat_owner' => $to),$where,__LINE__,__FILE__);
}
else
{
$this->db->delete(self::TABLE,$where,__LINE__,__FILE__);
}
// update cache accordingly
self::invalidate_cache();
}
/**
* Initialise or restore the categories cache
*
* We use the default ordering of return_array to avoid doing it again there
*/
public static function init_cache()
{
self::$cache = Cache::getInstance(self::CACHE_APP, self::CACHE_NAME);
if (is_null(self::$cache))
{
// check if we are already updated to global owner == 0, if not do it now
if (!$GLOBALS['egw']->db->select(self::TABLE,'COUNT(*)',array('cat_owner'=>'0'),__LINE__,__FILE__)->fetchColumn())
{
$GLOBALS['egw']->db->update(self::TABLE,array('cat_owner'=>'0'),"(cat_owner='-1' OR cat_appname='phpgw')",__LINE__,__FILE__);
$GLOBALS['egw']->db->insert(self::TABLE,array(
'cat_main' => 0,
'cat_parent' => 0,
'cat_level' => 0,
'cat_owner' => 0,
'cat_appname' => '*update*',
'cat_name' => 'global=0',
'cat_description' => 'global=0',
'last_mod' => time(),
),false,__LINE__,__FILE__);
}
self::$cache = array();
// read all cats (cant use $this->db!)
foreach($GLOBALS['egw']->db->select(self::TABLE,'*',false,__LINE__,__FILE__,
false,'ORDER BY cat_main, cat_level, cat_name ASC') as $cat)
{
$cat = Db::strip_array_keys($cat,'cat_');
if ($cat['appname'] == '*update*') continue; // --> ignore update marker
$cat['app_name'] = $cat['appname'];
// backlink children to their parent
if ($cat['parent'])
{
self::$cache[$cat['parent']]['children'][] = $cat['id'];
}
if (isset(self::$cache[$cat['id']]))
{
$cat['children'] = self::$cache[$cat['id']]['children'];
unset(self::$cache[$cat['id']]); // otherwise the order gets messed up!
}
self::$cache[$cat['id']] = $cat;
}
Cache::setInstance(self::CACHE_APP, self::CACHE_NAME, self::$cache);
}
//error_log(__METHOD__."() cache initialised: ".function_backtrace());
}
/**
* Invalidate the cache
*
* Currently we dont care for $cat_id, as changing cats happens very infrequently and
* also changes child categories (!)
*
* @param int|array $cat concerned id(s) or array with cat-data or null for all cats
*/
public static function invalidate_cache($cat=null)
{
//error_log(__METHOD__."(".array2string($cat).') '.function_backtrace());
// allways invalidate instance-global cache, as updating our own cache is not perfect and does not help other sessions
Cache::unsetInstance(self::CACHE_APP, self::CACHE_NAME);
// if cat given update our own cache, to work around failed sitemgr install via setup (cant read just added categories)
if ($cat)
{
if (!is_array($cat) || isset($cat[0]))
{
foreach((array)$cat as $cat_id)
{
unset(self::$cache[$cat_id]);
}
}
elseif($cat['id'])
{
self::$cache[$cat['id']] = $cat;
}
}
else
{
self::init_cache();
}
}
/**
* Have the framework include the CSS file
*
* We calculate the cachebuster timestamp from the last modified category in
* the application.
*
* @param string appname
*
* @return Returns the URL, but you do not need to do anything with it.
*/
public static function css($appname)
{
$cats = new Categories('',$appname);
$last_mod = $cats->return_array('all',0,1,'','DESC','last_mod', $appname == self::GLOBAL_APPNAME);
$time = count($last_mod) ? $last_mod[0]['last_mod'] : time();
$path = '/api/categories.php?app='.$appname.'&'.$time;
Framework::includeCSS($path);
return $path;
}
/**
* Get the color of a category
*
* For multiple cats, the first with a color is used
*
* @param int|string $_cats multiple comma-separated cat_id's
* @return string
*/
static function cats2color($_cats)
{
static $cat2color = array();
// ACL check
$cats = $GLOBALS['egw']->categories->check_list(Acl::READ, $_cats);
if (!$cats) return null;
if (isset($cat2color[$cats]))
{
return $cat2color[$cats];
}
foreach(explode(',', $cats) as $cat)
{
if (isset($cat2color[$cat]))
{
return $cat2color[$cat];
}
$data = self::id2name($cat, 'data');
if (is_array($data) && ($color = $data['color']))
{
return $cat2color[$cats] = $cat2color[$cat] = $color;
}
}
return null;
}
/**
* Delete categories belonging to a given account, when account got deleted
*
* @param int $account_id
* @param int $new_owner =null for users data can be transfered to new owner
* @return int number of deleted or modified categories
*/
public static function delete_account($account_id, $new_owner=null)
{
if (is_null(self::$cache)) self::init_cache();
$deleted = 0;
$cat = null;
foreach(self::$cache as $cat_id => $data)
{
if ($data['owner'] && ($owners = explode(',', $data['owner'])) && ($owner_key = array_search($account_id, $owners)) !== false)
{
// delete category if account_id is single owner and no new owner or owner is a group
if (count($owners) == 1 && (!$new_owner || $account_id < 0))
{
if (!isset($cat))
{
$cat = new Categories($new_owner, $data['appname']);
}
$cat->delete($cat_id, false, true);
}
else
{
unset($owners[$owner_key]);
if ($new_owner && $account_id > 0) $owners[] = $new_owner;
$data['owner'] = implode(',', $owners);
// app_name have to match cat to update!
if (!isset($cat) || $cat->app_name != $data['appname'])
{
$cat = new Categories($new_owner, $data['appname']);
}
$cat->add($data);
}
++$deleted;
}
}
return $deleted;
}
/**
* Delete categories with not (longer) existing owners
*
* @return int number of deleted categories
*/
public static function delete_orphans()
{
if (is_null(self::$cache)) self::init_cache();
$checked = array();
$deleted = 0;
foreach(self::$cache as $data)
{
foreach(explode(',', $data['owner']) as $owner)
{
if ($owner && !in_array($owner, $checked))
{
if (!$GLOBALS['egw']->accounts->exists($owner))
{
$deleted += self::delete_account($owner);
}
$checked[] = $owner;
}
}
}
return $deleted;
}
}