<commit_before><?php
// (c) Copyright 2002-2012 by authors of the Tiki Wiki CMS Groupware Project
// 
// All Rights Reserved. See copyright.txt for details and a complete list of authors.
// Licensed under the GNU LESSER GENERAL PUBLIC LICENSE. See license.txt for details.
// $Id$

/**
 * \brief Categories support class
 */

//this script may only be included - so its better to die if called directly.
if (strpos($_SERVER["SCRIPT_NAME"], basename(__FILE__)) !== false) {
  header("location: index.php");
  exit;
}

global $objectlib;require_once("lib/objectlib.php");

class CategLib extends ObjectLib
{

	// Returns a string representing the specified category's path.
	// The path includes all parent categories ordered from the root to the category's parent, and the category itself.
	// The string is a double colon (::) separated concatenation of category names.
	// Returns the empty string if the specified category does not exist.
	function get_category_path_string($categId)
	{
		$category = $this->get_category($categId);
		if ($category) {
			return $category['categpath'];
		} else {
			return '';
		}
	}

	/**
	 * Returns the path of the given category as a String in the format:
	 * "Root Category (TOP) > 1st Subcategory > 2nd Subcategory::..."	
	 */	
	function get_category_path_string_with_root($categId)
	{		
		$category = $this->get_category($categId);
		$tepath = array ('Top');
		foreach ($category['tepath'] as $pathelem) {
			$tepath[] = $pathelem;
		}
		return implode(" > ", $tepath);
	}
	
	// Returns false if the category is not found.
	// WARNING: permissions and the category filter are not considered.
	function get_category($categId)
	{
		if (!is_numeric($categId)) {
			throw new Exception('Invalid category identier');
		}
		$categories = $this->getCategories(array('identifier' => (int) $categId), false, false);
		return empty($categories) ? false : $categories[$categId];
	}
	
	function get_category_id($name)
	{
		$query = "select `categId` from `tiki_categories` where `name`=?";
		return $this->getOne($query, array((string)$name));
		
	
	}
	function get_category_name($categId,$real=false)
	{
	    if ( $categId==0 ) return 'Top';   
		$query = "select `name`,`parentId` from `tiki_categories` where `categId`=?";
		$result=$this->query($query, array((int) $categId));
		$res = $result->fetchRow();
		if ($real) return $res['name'];
		if (preg_match('/^Tracker ([0-9]+)$/', $res['name'])) {
		    $trackerId=preg_replace('/^Tracker ([0-9]+)$/', "$1", $res['name']);
		    return $this->getOne("select `name` from `tiki_trackers` where `trackerId`=?", array((int) $trackerId));
		}
		if (preg_match('/^Tracker Item ([0-9]+)$/', $res['name'])) {
		    global $trklib;require_once('lib/trackers/trackerlib.php');
		    $itemId=preg_replace('/^Tracker Item ([0-9]+)$/', "$1", $res['name']);
		    return $trklib->get_isMain_value(-1, $itemId);
		}
		return $res['name'];
	}
	
	// WARNING: This removes not only the specified category, but also all its descendants.
	function remove_category($categId)
	{
		global $cachelib; include_once('lib/cache/cachelib.php');

		$parentId=$this->get_category_parent($categId);
		$categoryName=$this->get_category_name($categId);
		$categoryPath=$this->get_category_path_string_with_root($categId);
		$description=$this->get_category_description($categId);

		$query = "delete from `tiki_categories` where `categId`=?";
		$result = $this->query($query, array((int) $categId));
		$query = "select `catObjectId` from `tiki_category_objects` where `categId`=?";
		$result = $this->query($query, array((int) $categId));

		while ($res = $result->fetchRow()) {
			$object = $res["catObjectId"];

			$query_cant = "select count(*) from `tiki_category_objects` where `catObjectId`=?";
			$cant = $this->getOne($query_cant, array($object));
			if ($cant <= 1) {
			$query2 = "delete from `tiki_categorized_objects` where `catObjectId`=?";
			$result2 = $this->query($query2, array($object));
			}
		}
		
		// remove any permissions assigned to this category
		$type = 'category';
		$object = $type . $categId;
		$query = "delete from `users_objectpermissions` where `objectId`=? and `objectType`=?";
		$result = $this->query($query, array(md5($object),$type));

		$query = "delete from `tiki_category_objects` where `categId`=?";
		$result = $this->query($query, array((int) $categId));
		$query = "select `categId` from `tiki_categories` where `parentId`=?";
		$result = $this->query($query, array((int) $categId));

		while ($res = $result->fetchRow()) {
			// Recursively remove the subcategory
			$this->remove_category($res["categId"]);
		}
		
		$cachelib->empty_type_cache('allcategs');
		$cachelib->empty_type_cache('fgals_perms');
	
		$values= array("categoryId"=>$categId, "categoryName"=>$categoryName, "categoryPath"=>$categoryPath,
			"description"=>$description, "parentId" => $parentId, "parentName" => $this->get_category_name($parentId),
			"action"=>"category removed");		
		$this->notify($values);

		$this->remove_category_from_watchlists($categId);
					
		return true;
	}

	// Throws an Exception if the category name conflicts
	function update_category($categId, $name, $description, $parentId)
	{
		global $cachelib; include_once('lib/cache/cachelib.php');

		$oldCategory=$this->get_category($categId);
		$oldCategoryName=$oldCategory['name'];
		$oldCategoryPath=$this->get_category_path_string_with_root($categId);
		$oldDescription=$oldCategory['description'];
		$oldParentId=$oldCategory['parentId'];
		$oldParentName=$this->get_category_name($oldParentId);

		if (($oldCategoryName != $name || $oldParentId != $parentId) && $this->exist_child_category($parentId, $name)) {
			throw new Exception(tr('A category named %0 already exists in %1.', $name, $this->get_category_name($parentId)));
		}
		
		// Make sure the description fits the column width
		if (strlen($description) > 250) {
			$description = substr($description, 0, 250);
		}

		$query = "update `tiki_categories` set `name`=?, `parentId`=?, `description`=? where `categId`=?";
		$result = $this->query($query, array($name,(int) $parentId, $description, (int) $categId));
		$cachelib->empty_type_cache('allcategs');
		$cachelib->empty_type_cache('fgals_perms');

		$values= array("categoryId"=>$categId, "categoryName"=>$name, "categoryPath"=>$this->get_category_path_string_with_root($categId),
			"description"=>$description, "parentId" => $parentId, "parentName" => $this->get_category_name($parentId),
			"action"=>"category updated","oldCategoryName"=>$oldCategoryName, "oldCategoryPath"=>$oldCategoryPath,
			"oldDescription"=>$oldDescription, "oldParentId" => $parentId, "oldParentName" => $oldParentName);			
		$this->notify($values);		
	}

	// Throws an Exception if the category name conflicts
	function add_category($parentId, $name, $description)
	{
		if ($this->exist_child_category($parentId, $name)) {
			throw new Exception(tr('A category named %0 already exists in %1.', $name, $this->get_category_name($parentId)));
		}
		global $cachelib; include_once('lib/cache/cachelib.php');
		
		// Make sure the description fits the column width
		// TODO: remove length constraint then remove this. See "Quiet truncation of data in database" thread on the development list
		if (strlen($description) > 250) {
			$description = substr($description, 0, 250);
		}

		$query = "insert into `tiki_categories`(`name`,`description`,`parentId`,`hits`) values(?,?,?,?)";
		$result = $this->query($query, array($name,$description,(int) $parentId, 0));
		$query = "select `categId` from `tiki_categories` where `name`=? and `parentId`=? order by `categId` desc";
		$id = $this->getOne($query, array($name,(int) $parentId));
		$cachelib->empty_type_cache('allcategs');
		$cachelib->empty_type_cache('fgals_perms');
		$values= array("categoryId"=>$id, "categoryName"=>$name, "categoryPath"=> $this->get_category_path_string_with_root($id),
			"description"=>$description, "parentId" => $parentId, "parentName" => $this->get_category_name($parentId),
			"action"=>"category created");		
		$this->notify($values);		 	
		return $id;
	}

	function is_categorized($type, $itemId)
	{
		if ( empty($itemId) ) return 0;

		if ( count($this->getCategories(NULL, false, false)) == 0 ) { // Optimization
			return 0;
		}

		$query = "select o.`objectId` from `tiki_categorized_objects` c, `tiki_objects` o, `tiki_category_objects` tco where c.`catObjectId`=o.`objectId` and o.`type`=? and o.`itemId`=? and tco.`catObjectId`=c.`catObjectId`";
		$bindvars = array($type,$itemId);
		settype($bindvars["1"], "string");
		$result = $this->query($query, $bindvars);

		if ( $result->numRows() ) {
			$res = $result->fetchRow();
			return $res["objectId"];
		} else {
			return 0;
		}
	}

	// $type The object's type, which has to be one of those handled by ObjectLib's add_object().
	// $checkHandled A boolean indicating whether only handled object types should be accepted when the object has no object record and no object information is given (legacy).
	// Returns the object's OID, or FALSE if the object type is not handled and $checkHandled is FALSE.
	function add_categorized_object($type, $itemId, $description = NULL, $name = NULL, $href = NULL, $checkHandled = FALSE)
	{
		$id = $this->add_object($type, $itemId, $checkHandled, $description, $name, $href);
		if ($id === FALSE) {
			return FALSE;
		}
		$query = "select `catObjectId` from `tiki_categorized_objects` where `catObjectId`=?";
		if (!$this->getOne($query, array($id))) {
			$query = "insert into `tiki_categorized_objects` (`catObjectId`) values (?)";
			$this->query($query, array($id));
			
			global $cachelib; include_once('lib/cache/cachelib.php');
			$cachelib->empty_type_cache('allcategs');
			$cachelib->empty_type_cache('fgals_perms');
		}
		return $id;
	}

	function categorize($catObjectId, $categId)
	{
		global $prefs;
		if (empty($categId)) {
			return;
		}
		$query = "delete from `tiki_category_objects` where `catObjectId`=? and `categId`=?";
		$result = $this->query($query, array((int) $catObjectId,(int) $categId), -1, -1, false);
	        
		$query = "insert into `tiki_category_objects`(`catObjectId`,`categId`) values(?,?)";
		$result = $this->query($query, array((int) $catObjectId,(int) $categId));

		global $cachelib;
		$cachelib->empty_type_cache("allcategs");
		$info = TikiLib::lib('object')->get_object_via_objectid($catObjectId);
		if ($prefs['feature_actionlog'] == 'y') {
			global $logslib; include_once('lib/logs/logslib.php');
			$logslib->add_action('Categorized', $info['itemId'], $info['type'], "categId=$categId");
		}
		require_once 'lib/search/refresh-functions.php';
		refresh_index($info['type'], $info['itemId']);
	}

	function uncategorize($catObjectId, $categId)
	{
		global $prefs;
		$query = "delete from `tiki_category_objects` where `catObjectId`=? and `categId`=?";
		$result = $this->query($query, array((int) $catObjectId,(int) $categId), -1, -1, false);

		global $cachelib;
		$cachelib->empty_type_cache("allcategs");
		$info = TikiLib::lib('object')->get_object_via_objectid($catObjectId);
		if ($prefs['feature_actionlog'] == 'y') {
			global $logslib; include_once('lib/logs/logslib.php');
			$logslib->add_action('Uncategorized', $info['itemId'], $info['type'], "categId=$categId");
		}
		require_once 'lib/search/refresh-functions.php';
		refresh_index($info['type'], $info['itemId']);
	}

	// WARNING: This may not do what you would think from the name.
	// Returns an array of the OIDs of a set of categories.
	// $categId is an integer.
	// If $categId is 0, that set is the set of all categories.
	// If $categId is the OID of a category, that set is the set of that category and its descendants.
	function get_category_descendants($categId)
	{
		if ($categId) {
			$category = $this->get_category($categId);
			if ($category == false) return false;
			return array_merge(array($categId), $category['descendants']);
		} else {
			return $this->getCategories(NULL, false, false);
		}
	}

	function list_category_objects($categId, $offset, $maxRecords, $sort_mode='pageName_asc', $type='', $find='', $deep=false, $and=false, $filter=null)
	{
		global $userlib, $prefs;
		if ($prefs['feature_sefurl'] == 'y') {
			include_once('tiki-sefurl.php');
		}
		if ($prefs['feature_trackers'] == 'y') {
			global $trklib;require_once('lib/trackers/trackerlib.php');
		}
	    
	    // Build the condition to restrict which categories objects must be in to be returned.
		$join = '';
		if (is_array($categId) && $and) {
			$categId = $this->get_jailed($categId);
			$i = count($categId)+1;
			$bindWhere = array();
			foreach ($categId as $c) {
				if (--$i) {
					$join .= " INNER JOIN tiki_category_objects tco$i on tco$i.`catObjectId`=o.`objectId` and tco$i.`categId`=? ";
					$bindWhere[] = $c;
				}
			}
		} elseif (is_array($categId)) {
			$bindWhere = $categId;
			if ($deep) {
				foreach ($categId as $c) {
					$bindWhere = array_merge($bindWhere, $this->get_category_descendants($c));
				}				
			}

			$bindWhere = $this->get_jailed($bindWhere);
			$bindWhere[] = -1;

			$where = " AND c.`categId` IN (".str_repeat("?,", count($bindWhere)-1)."?)";
		} else {
			if ($deep) {
				$bindWhere = $this->get_category_descendants($categId);
				$bindWhere[] = $categId;
				$bindWhere = $this->get_jailed($bindWhere);
				$bindWhere[] = -1;
				$where = " AND c.`categId` IN (".str_repeat("?,", count($bindWhere)-1)."?)";
			} else {<commit_msg>[FIX] categs: Deep search at root level was failing for me, this seems to fix it although i'm not entirely sure why<commit_after><?php
// (c) Copyright 2002-2012 by authors of the Tiki Wiki CMS Groupware Project
// 
// All Rights Reserved. See copyright.txt for details and a complete list of authors.
// Licensed under the GNU LESSER GENERAL PUBLIC LICENSE. See license.txt for details.
// $Id$

/**
 * \brief Categories support class
 */

//this script may only be included - so its better to die if called directly.
if (strpos($_SERVER["SCRIPT_NAME"], basename(__FILE__)) !== false) {
  header("location: index.php");
  exit;
}

global $objectlib;require_once("lib/objectlib.php");

class CategLib extends ObjectLib
{

	// Returns a string representing the specified category's path.
	// The path includes all parent categories ordered from the root to the category's parent, and the category itself.
	// The string is a double colon (::) separated concatenation of category names.
	// Returns the empty string if the specified category does not exist.
	function get_category_path_string($categId)
	{
		$category = $this->get_category($categId);
		if ($category) {
			return $category['categpath'];
		} else {
			return '';
		}
	}

	/**
	 * Returns the path of the given category as a String in the format:
	 * "Root Category (TOP) > 1st Subcategory > 2nd Subcategory::..."	
	 */	
	function get_category_path_string_with_root($categId)
	{		
		$category = $this->get_category($categId);
		$tepath = array ('Top');
		foreach ($category['tepath'] as $pathelem) {
			$tepath[] = $pathelem;
		}
		return implode(" > ", $tepath);
	}
	
	// Returns false if the category is not found.
	// WARNING: permissions and the category filter are not considered.
	function get_category($categId)
	{
		if (!is_numeric($categId)) {
			throw new Exception('Invalid category identier');
		}
		$categories = $this->getCategories(array('identifier' => (int) $categId), false, false);
		return empty($categories) ? false : $categories[$categId];
	}
	
	function get_category_id($name)
	{
		$query = "select `categId` from `tiki_categories` where `name`=?";
		return $this->getOne($query, array((string)$name));
		
	
	}
	function get_category_name($categId,$real=false)
	{
	    if ( $categId==0 ) return 'Top';   
		$query = "select `name`,`parentId` from `tiki_categories` where `categId`=?";
		$result=$this->query($query, array((int) $categId));
		$res = $result->fetchRow();
		if ($real) return $res['name'];
		if (preg_match('/^Tracker ([0-9]+)$/', $res['name'])) {
		    $trackerId=preg_replace('/^Tracker ([0-9]+)$/', "$1", $res['name']);
		    return $this->getOne("select `name` from `tiki_trackers` where `trackerId`=?", array((int) $trackerId));
		}
		if (preg_match('/^Tracker Item ([0-9]+)$/', $res['name'])) {
		    global $trklib;require_once('lib/trackers/trackerlib.php');
		    $itemId=preg_replace('/^Tracker Item ([0-9]+)$/', "$1", $res['name']);
		    return $trklib->get_isMain_value(-1, $itemId);
		}
		return $res['name'];
	}
	
	// WARNING: This removes not only the specified category, but also all its descendants.
	function remove_category($categId)
	{
		global $cachelib; include_once('lib/cache/cachelib.php');

		$parentId=$this->get_category_parent($categId);
		$categoryName=$this->get_category_name($categId);
		$categoryPath=$this->get_category_path_string_with_root($categId);
		$description=$this->get_category_description($categId);

		$query = "delete from `tiki_categories` where `categId`=?";
		$result = $this->query($query, array((int) $categId));
		$query = "select `catObjectId` from `tiki_category_objects` where `categId`=?";
		$result = $this->query($query, array((int) $categId));

		while ($res = $result->fetchRow()) {
			$object = $res["catObjectId"];

			$query_cant = "select count(*) from `tiki_category_objects` where `catObjectId`=?";
			$cant = $this->getOne($query_cant, array($object));
			if ($cant <= 1) {
			$query2 = "delete from `tiki_categorized_objects` where `catObjectId`=?";
			$result2 = $this->query($query2, array($object));
			}
		}
		
		// remove any permissions assigned to this category
		$type = 'category';
		$object = $type . $categId;
		$query = "delete from `users_objectpermissions` where `objectId`=? and `objectType`=?";
		$result = $this->query($query, array(md5($object),$type));

		$query = "delete from `tiki_category_objects` where `categId`=?";
		$result = $this->query($query, array((int) $categId));
		$query = "select `categId` from `tiki_categories` where `parentId`=?";
		$result = $this->query($query, array((int) $categId));

		while ($res = $result->fetchRow()) {
			// Recursively remove the subcategory
			$this->remove_category($res["categId"]);
		}
		
		$cachelib->empty_type_cache('allcategs');
		$cachelib->empty_type_cache('fgals_perms');
	
		$values= array("categoryId"=>$categId, "categoryName"=>$categoryName, "categoryPath"=>$categoryPath,
			"description"=>$description, "parentId" => $parentId, "parentName" => $this->get_category_name($parentId),
			"action"=>"category removed");		
		$this->notify($values);

		$this->remove_category_from_watchlists($categId);
					
		return true;
	}

	// Throws an Exception if the category name conflicts
	function update_category($categId, $name, $description, $parentId)
	{
		global $cachelib; include_once('lib/cache/cachelib.php');

		$oldCategory=$this->get_category($categId);
		$oldCategoryName=$oldCategory['name'];
		$oldCategoryPath=$this->get_category_path_string_with_root($categId);
		$oldDescription=$oldCategory['description'];
		$oldParentId=$oldCategory['parentId'];
		$oldParentName=$this->get_category_name($oldParentId);

		if (($oldCategoryName != $name || $oldParentId != $parentId) && $this->exist_child_category($parentId, $name)) {
			throw new Exception(tr('A category named %0 already exists in %1.', $name, $this->get_category_name($parentId)));
		}
		
		// Make sure the description fits the column width
		if (strlen($description) > 250) {
			$description = substr($description, 0, 250);
		}

		$query = "update `tiki_categories` set `name`=?, `parentId`=?, `description`=? where `categId`=?";
		$result = $this->query($query, array($name,(int) $parentId, $description, (int) $categId));
		$cachelib->empty_type_cache('allcategs');
		$cachelib->empty_type_cache('fgals_perms');

		$values= array("categoryId"=>$categId, "categoryName"=>$name, "categoryPath"=>$this->get_category_path_string_with_root($categId),
			"description"=>$description, "parentId" => $parentId, "parentName" => $this->get_category_name($parentId),
			"action"=>"category updated","oldCategoryName"=>$oldCategoryName, "oldCategoryPath"=>$oldCategoryPath,
			"oldDescription"=>$oldDescription, "oldParentId" => $parentId, "oldParentName" => $oldParentName);			
		$this->notify($values);		
	}

	// Throws an Exception if the category name conflicts
	function add_category($parentId, $name, $description)
	{
		if ($this->exist_child_category($parentId, $name)) {
			throw new Exception(tr('A category named %0 already exists in %1.', $name, $this->get_category_name($parentId)));
		}
		global $cachelib; include_once('lib/cache/cachelib.php');
		
		// Make sure the description fits the column width
		// TODO: remove length constraint then remove this. See "Quiet truncation of data in database" thread on the development list
		if (strlen($description) > 250) {
			$description = substr($description, 0, 250);
		}

		$query = "insert into `tiki_categories`(`name`,`description`,`parentId`,`hits`) values(?,?,?,?)";
		$result = $this->query($query, array($name,$description,(int) $parentId, 0));
		$query = "select `categId` from `tiki_categories` where `name`=? and `parentId`=? order by `categId` desc";
		$id = $this->getOne($query, array($name,(int) $parentId));
		$cachelib->empty_type_cache('allcategs');
		$cachelib->empty_type_cache('fgals_perms');
		$values= array("categoryId"=>$id, "categoryName"=>$name, "categoryPath"=> $this->get_category_path_string_with_root($id),
			"description"=>$description, "parentId" => $parentId, "parentName" => $this->get_category_name($parentId),
			"action"=>"category created");		
		$this->notify($values);		 	
		return $id;
	}

	function is_categorized($type, $itemId)
	{
		if ( empty($itemId) ) return 0;

		if ( count($this->getCategories(NULL, false, false)) == 0 ) { // Optimization
			return 0;
		}

		$query = "select o.`objectId` from `tiki_categorized_objects` c, `tiki_objects` o, `tiki_category_objects` tco where c.`catObjectId`=o.`objectId` and o.`type`=? and o.`itemId`=? and tco.`catObjectId`=c.`catObjectId`";
		$bindvars = array($type,$itemId);
		settype($bindvars["1"], "string");
		$result = $this->query($query, $bindvars);

		if ( $result->numRows() ) {
			$res = $result->fetchRow();
			return $res["objectId"];
		} else {
			return 0;
		}
	}

	// $type The object's type, which has to be one of those handled by ObjectLib's add_object().
	// $checkHandled A boolean indicating whether only handled object types should be accepted when the object has no object record and no object information is given (legacy).
	// Returns the object's OID, or FALSE if the object type is not handled and $checkHandled is FALSE.
	function add_categorized_object($type, $itemId, $description = NULL, $name = NULL, $href = NULL, $checkHandled = FALSE)
	{
		$id = $this->add_object($type, $itemId, $checkHandled, $description, $name, $href);
		if ($id === FALSE) {
			return FALSE;
		}
		$query = "select `catObjectId` from `tiki_categorized_objects` where `catObjectId`=?";
		if (!$this->getOne($query, array($id))) {
			$query = "insert into `tiki_categorized_objects` (`catObjectId`) values (?)";
			$this->query($query, array($id));
			
			global $cachelib; include_once('lib/cache/cachelib.php');
			$cachelib->empty_type_cache('allcategs');
			$cachelib->empty_type_cache('fgals_perms');
		}
		return $id;
	}

	function categorize($catObjectId, $categId)
	{
		global $prefs;
		if (empty($categId)) {
			return;
		}
		$query = "delete from `tiki_category_objects` where `catObjectId`=? and `categId`=?";
		$result = $this->query($query, array((int) $catObjectId,(int) $categId), -1, -1, false);
	        
		$query = "insert into `tiki_category_objects`(`catObjectId`,`categId`) values(?,?)";
		$result = $this->query($query, array((int) $catObjectId,(int) $categId));

		global $cachelib;
		$cachelib->empty_type_cache("allcategs");
		$info = TikiLib::lib('object')->get_object_via_objectid($catObjectId);
		if ($prefs['feature_actionlog'] == 'y') {
			global $logslib; include_once('lib/logs/logslib.php');
			$logslib->add_action('Categorized', $info['itemId'], $info['type'], "categId=$categId");
		}
		require_once 'lib/search/refresh-functions.php';
		refresh_index($info['type'], $info['itemId']);
	}

	function uncategorize($catObjectId, $categId)
	{
		global $prefs;
		$query = "delete from `tiki_category_objects` where `catObjectId`=? and `categId`=?";
		$result = $this->query($query, array((int) $catObjectId,(int) $categId), -1, -1, false);

		global $cachelib;
		$cachelib->empty_type_cache("allcategs");
		$info = TikiLib::lib('object')->get_object_via_objectid($catObjectId);
		if ($prefs['feature_actionlog'] == 'y') {
			global $logslib; include_once('lib/logs/logslib.php');
			$logslib->add_action('Uncategorized', $info['itemId'], $info['type'], "categId=$categId");
		}
		require_once 'lib/search/refresh-functions.php';
		refresh_index($info['type'], $info['itemId']);
	}

	// WARNING: This may not do what you would think from the name.
	// Returns an array of the OIDs of a set of categories.
	// $categId is an integer.
	// If $categId is 0, that set is the set of all categories.
	// If $categId is the OID of a category, that set is the set of that category and its descendants.
	function get_category_descendants($categId)
	{
		if ($categId) {
			$category = $this->get_category($categId);
			if ($category == false) return false;
			return array_merge(array($categId), $category['descendants']);
		} else {
			return $this->getCategories(NULL, false, false);
		}
	}

	function list_category_objects($categId, $offset, $maxRecords, $sort_mode='pageName_asc', $type='', $find='', $deep=false, $and=false, $filter=null)
	{
		global $userlib, $prefs;
		if ($prefs['feature_sefurl'] == 'y') {
			include_once('tiki-sefurl.php');
		}
		if ($prefs['feature_trackers'] == 'y') {
			global $trklib;require_once('lib/trackers/trackerlib.php');
		}
	    
	    // Build the condition to restrict which categories objects must be in to be returned.
		$join = '';
		if (is_array($categId) && $and) {
			$categId = $this->get_jailed($categId);
			$i = count($categId)+1;
			$bindWhere = array();
			foreach ($categId as $c) {
				if (--$i) {
					$join .= " INNER JOIN tiki_category_objects tco$i on tco$i.`catObjectId`=o.`objectId` and tco$i.`categId`=? ";
					$bindWhere[] = $c;
				}
			}
		} elseif (is_array($categId)) {
			$bindWhere = $categId;
			if ($deep) {
				foreach ($categId as $c) {
					$bindWhere = array_merge($bindWhere, $this->get_category_descendants($c));
				}				
			}

			$bindWhere = $this->get_jailed($bindWhere);
			$bindWhere[] = -1;

			$where = " AND c.`categId` IN (".str_repeat("?,", count($bindWhere)-1)."?)";
		} else {
			if ($deep) {
				$bindWhere = array_keys($this->get_category_descendants($categId));
				$bindWhere[] = (int) $categId;
				$bindWhere = $this->get_jailed($bindWhere);
				$bindWhere[] = -1;
				$where = " AND c.`categId` IN (".str_repeat("?,", count($bindWhere)-1)."?)";
			} else {
{'fraction_non_alphanumeric': 0.17711424332344214, 'fraction_numerical': 0.004525222551928784, 'mean_word_length': 4.945093715545755, 'pattern_counts': {'":': 0, '<': 7, '<?xml version=': 0, '>': 211, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '895606', 'n_tokens_mistral': 10089, 'n_tokens_neox': 9224, 'n_words': 2094}